Fully move to protobufjs
This commit is contained in:
parent
20ea409d9e
commit
570fb182d4
46 changed files with 1133 additions and 12401 deletions
|
@ -347,7 +347,6 @@
|
|||
type="text/javascript"
|
||||
src="libtextsecure/protocol_wrapper.js"
|
||||
></script>
|
||||
<script type="text/javascript" src="libtextsecure/protobufs.js"></script>
|
||||
|
||||
<script type="text/javascript" src="js/notifications.js"></script>
|
||||
<script type="text/javascript" src="js/libphonenumber-util.js"></script>
|
||||
|
|
|
@ -13,12 +13,6 @@
|
|||
"devDependencies": {
|
||||
},
|
||||
"preen": {
|
||||
"bytebuffer": [
|
||||
"dist/ByteBufferAB.js"
|
||||
],
|
||||
"long": [
|
||||
"dist/Long.js"
|
||||
],
|
||||
"mp3lameencoder": [
|
||||
"lib/Mp3LameEncoder.js"
|
||||
],
|
||||
|
|
3293
components/bytebuffer/dist/ByteBufferAB.js
vendored
3293
components/bytebuffer/dist/ByteBufferAB.js
vendored
File diff suppressed because it is too large
Load diff
1209
components/long/dist/Long.js
vendored
1209
components/long/dist/Long.js
vendored
File diff suppressed because it is too large
Load diff
5250
components/protobuf/dist/ProtoBuf.js
vendored
5250
components/protobuf/dist/ProtoBuf.js
vendored
File diff suppressed because it is too large
Load diff
|
@ -1,63 +0,0 @@
|
|||
// Copyright 2015-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global window, postMessage, textsecure, close */
|
||||
|
||||
/* eslint-disable more/no-then, no-global-assign, no-restricted-globals, no-unused-vars */
|
||||
|
||||
/*
|
||||
* Load this script in a Web Worker to generate new prekeys without
|
||||
* tying up the main thread.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
|
||||
*
|
||||
* Because workers don't have access to the window or localStorage, we
|
||||
* create our own version that proxies back to the caller for actual
|
||||
* storage.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
var myWorker = new Worker('/js/key_worker.js');
|
||||
myWorker.onmessage = function(e) {
|
||||
switch(e.data.method) {
|
||||
case 'set':
|
||||
localStorage.setItem(e.data.key, e.data.value);
|
||||
break;
|
||||
case 'remove':
|
||||
localStorage.removeItem(e.data.key);
|
||||
break;
|
||||
case 'done':
|
||||
console.error(e.data.keys);
|
||||
}
|
||||
};
|
||||
*/
|
||||
let store = {};
|
||||
window.textsecure.storage.impl = {
|
||||
/** ***************************
|
||||
*** Override Storage Routines ***
|
||||
**************************** */
|
||||
put(key, value) {
|
||||
if (value === undefined) throw new Error('Tried to store undefined');
|
||||
store[key] = value;
|
||||
postMessage({ method: 'set', key, value });
|
||||
},
|
||||
|
||||
get(key, defaultValue) {
|
||||
if (key in store) {
|
||||
return store[key];
|
||||
}
|
||||
return defaultValue;
|
||||
},
|
||||
|
||||
remove(key) {
|
||||
delete store[key];
|
||||
postMessage({ method: 'remove', key });
|
||||
},
|
||||
};
|
||||
// eslint-disable-next-line no-undef
|
||||
onmessage = e => {
|
||||
store = e.data;
|
||||
textsecure.protocol_wrapper.generateKeys().then(keys => {
|
||||
postMessage({ method: 'done', keys });
|
||||
close();
|
||||
});
|
||||
};
|
|
@ -1,70 +0,0 @@
|
|||
// Copyright 2015-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global window, dcodeIO, textsecure */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
const FILES_TO_LOAD = [
|
||||
'SignalService.proto',
|
||||
'SignalStorage.proto',
|
||||
'SubProtocol.proto',
|
||||
'DeviceMessages.proto',
|
||||
'Stickers.proto',
|
||||
|
||||
// Just for encrypting device names
|
||||
'DeviceName.proto',
|
||||
|
||||
// Metadata-specific protos
|
||||
'UnidentifiedDelivery.proto',
|
||||
|
||||
// Groups
|
||||
'Groups.proto',
|
||||
];
|
||||
|
||||
let remainingFilesToLoad = FILES_TO_LOAD.length;
|
||||
const hasFinishedLoading = () => remainingFilesToLoad <= 0;
|
||||
let onLoadCallbacks = [];
|
||||
|
||||
window.textsecure = window.textsecure || {};
|
||||
window.textsecure.protobuf = {
|
||||
onLoad: callback => {
|
||||
if (hasFinishedLoading()) {
|
||||
setTimeout(callback, 0);
|
||||
} else {
|
||||
onLoadCallbacks.push(callback);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
FILES_TO_LOAD.forEach(filename => {
|
||||
dcodeIO.ProtoBuf.loadProtoFile(
|
||||
{ root: window.PROTO_ROOT, file: filename },
|
||||
(error, result) => {
|
||||
if (error) {
|
||||
const text = `Error loading protos from ${filename} (root: ${
|
||||
window.PROTO_ROOT
|
||||
}) ${error && error.stack ? error.stack : error}`;
|
||||
window.log.error(text);
|
||||
throw error;
|
||||
}
|
||||
const protos = result.build('signalservice');
|
||||
if (!protos) {
|
||||
const text = `Error loading protos from ${filename} - no exported types! (root: ${window.PROTO_ROOT})`;
|
||||
window.log.error(text);
|
||||
throw new Error(text);
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-syntax, guard-for-in
|
||||
for (const protoName in protos) {
|
||||
textsecure.protobuf[protoName] = protos[protoName];
|
||||
}
|
||||
|
||||
remainingFilesToLoad -= 1;
|
||||
if (hasFinishedLoading()) {
|
||||
onLoadCallbacks.forEach(callback => callback());
|
||||
onLoadCallbacks = [];
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
})();
|
|
@ -17,10 +17,8 @@ module.exports = {
|
|||
globals: {
|
||||
assert: true,
|
||||
assertEqualArrayBuffers: true,
|
||||
dcodeIO: true,
|
||||
getString: true,
|
||||
hexToArrayBuffer: true,
|
||||
PROTO_ROOT: true,
|
||||
stringToArrayBuffer: true,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
mocha.setup('bdd');
|
||||
window.assert = chai.assert;
|
||||
window.PROTO_ROOT = '../../protos';
|
||||
|
||||
const OriginalReporter = mocha._reporter;
|
||||
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
// Copyright 2015-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global textsecure */
|
||||
|
||||
describe('encrypting and decrypting profile data', () => {
|
||||
const NAME_PADDED_LENGTH = 53;
|
||||
describe('encrypting and decrypting profile names', () => {
|
||||
it('pads, encrypts, decrypts, and unpads a short string', () => {
|
||||
const name = 'Alice';
|
||||
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto
|
||||
.encryptProfileName(buffer, key)
|
||||
.then(encrypted => {
|
||||
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
|
||||
return textsecure.crypto
|
||||
.decryptProfileName(encrypted, key)
|
||||
.then(({ given, family }) => {
|
||||
assert.strictEqual(family, null);
|
||||
assert.strictEqual(
|
||||
dcodeIO.ByteBuffer.wrap(given).toString('utf8'),
|
||||
name
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('handles a given name of the max, 53 characters', () => {
|
||||
const name = '01234567890123456789012345678901234567890123456789123';
|
||||
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto
|
||||
.encryptProfileName(buffer, key)
|
||||
.then(encrypted => {
|
||||
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
|
||||
return textsecure.crypto
|
||||
.decryptProfileName(encrypted, key)
|
||||
.then(({ given, family }) => {
|
||||
assert.strictEqual(
|
||||
dcodeIO.ByteBuffer.wrap(given).toString('utf8'),
|
||||
name
|
||||
);
|
||||
assert.strictEqual(family, null);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('handles family/given name of the max, 53 characters', () => {
|
||||
const name = '01234567890123456789\u000001234567890123456789012345678912';
|
||||
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto
|
||||
.encryptProfileName(buffer, key)
|
||||
.then(encrypted => {
|
||||
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
|
||||
return textsecure.crypto
|
||||
.decryptProfileName(encrypted, key)
|
||||
.then(({ given, family }) => {
|
||||
assert.strictEqual(
|
||||
dcodeIO.ByteBuffer.wrap(given).toString('utf8'),
|
||||
'01234567890123456789'
|
||||
);
|
||||
assert.strictEqual(
|
||||
dcodeIO.ByteBuffer.wrap(family).toString('utf8'),
|
||||
'01234567890123456789012345678912'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('handles a string with family/given name', () => {
|
||||
const name = 'Alice\0Jones';
|
||||
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto
|
||||
.encryptProfileName(buffer, key)
|
||||
.then(encrypted => {
|
||||
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
|
||||
return textsecure.crypto
|
||||
.decryptProfileName(encrypted, key)
|
||||
.then(({ given, family }) => {
|
||||
assert.strictEqual(
|
||||
dcodeIO.ByteBuffer.wrap(given).toString('utf8'),
|
||||
'Alice'
|
||||
);
|
||||
assert.strictEqual(
|
||||
dcodeIO.ByteBuffer.wrap(family).toString('utf8'),
|
||||
'Jones'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('works for empty string', async () => {
|
||||
const name = dcodeIO.ByteBuffer.wrap('').toArrayBuffer();
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await textsecure.crypto.encryptProfileName(name, key);
|
||||
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
|
||||
|
||||
const { given, family } = await textsecure.crypto.decryptProfileName(
|
||||
encrypted,
|
||||
key
|
||||
);
|
||||
assert.strictEqual(family, null);
|
||||
assert.strictEqual(given.byteLength, 0);
|
||||
assert.strictEqual(dcodeIO.ByteBuffer.wrap(given).toString('utf8'), '');
|
||||
});
|
||||
});
|
||||
describe('encrypting and decrypting profile avatars', () => {
|
||||
it('encrypts and decrypts', () => {
|
||||
const buffer = dcodeIO.ByteBuffer.wrap(
|
||||
'This is an avatar'
|
||||
).toArrayBuffer();
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto.encryptProfile(buffer, key).then(encrypted => {
|
||||
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
|
||||
return textsecure.crypto
|
||||
.decryptProfile(encrypted, key)
|
||||
.then(decrypted => {
|
||||
assertEqualArrayBuffers(buffer, decrypted);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('throws when decrypting with the wrong key', () => {
|
||||
const buffer = dcodeIO.ByteBuffer.wrap(
|
||||
'This is an avatar'
|
||||
).toArrayBuffer();
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
const badKey = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto.encryptProfile(buffer, key).then(encrypted => {
|
||||
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
|
||||
return textsecure.crypto
|
||||
.decryptProfile(encrypted, badKey)
|
||||
.catch(error => {
|
||||
assert.strictEqual(error.name, 'ProfileDecryptError');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -20,7 +20,6 @@
|
|||
></script>
|
||||
|
||||
<script type="text/javascript" src="../components.js"></script>
|
||||
<script type="text/javascript" src="../protobufs.js" data-cover></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="../protocol_wrapper.js"
|
||||
|
@ -38,7 +37,6 @@
|
|||
></script>
|
||||
|
||||
<script type="text/javascript" src="helpers_test.js"></script>
|
||||
<script type="text/javascript" src="crypto_test.js"></script>
|
||||
<script type="text/javascript" src="generate_keys_test.js"></script>
|
||||
<script type="text/javascript" src="task_with_timeout_test.js"></script>
|
||||
<script type="text/javascript" src="account_manager_test.js"></script>
|
||||
|
@ -50,10 +48,8 @@
|
|||
|
||||
<!-- Uncomment to start tests without code coverage enabled -->
|
||||
<script type="text/javascript">
|
||||
window.textsecure.protobuf.onLoad(() => {
|
||||
mocha.run();
|
||||
window.Signal.conversationControllerStart();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"node-sass-import-once": "1.2.0",
|
||||
"npm-run-all": "4.1.5",
|
||||
"nyc": "11.4.1",
|
||||
"patch-package": "6.1.2",
|
||||
"patch-package": "6.4.7",
|
||||
"prettier": "^2.2.1",
|
||||
"react-docgen-typescript": "1.2.6",
|
||||
"sass-loader": "7.2.0",
|
||||
|
|
84
patches/protobufjs+6.10.2.patch
Normal file
84
patches/protobufjs+6.10.2.patch
Normal file
|
@ -0,0 +1,84 @@
|
|||
diff --git a/node_modules/protobufjs/cli/lib/tsd-jsdoc/publish.js b/node_modules/protobufjs/cli/lib/tsd-jsdoc/publish.js
|
||||
index 3846a99..6c5688a 100644
|
||||
--- a/node_modules/protobufjs/cli/lib/tsd-jsdoc/publish.js
|
||||
+++ b/node_modules/protobufjs/cli/lib/tsd-jsdoc/publish.js
|
||||
@@ -558,6 +558,13 @@ function handleClass(element, parent) {
|
||||
handleElement(child, element);
|
||||
});
|
||||
|
||||
+ writeln();
|
||||
+ if (is_interface) {
|
||||
+ writeln("__unknownFields?: ReadonlyArray<Uint8Array>;");
|
||||
+ } else {
|
||||
+ writeln("public __unknownFields?: ReadonlyArray<Uint8Array>;");
|
||||
+ }
|
||||
+
|
||||
--indent;
|
||||
writeln("}");
|
||||
|
||||
diff --git a/node_modules/protobufjs/src/decoder.js b/node_modules/protobufjs/src/decoder.js
|
||||
index 491dd30..ec03e9f 100644
|
||||
--- a/node_modules/protobufjs/src/decoder.js
|
||||
+++ b/node_modules/protobufjs/src/decoder.js
|
||||
@@ -21,6 +21,7 @@ function decoder(mtype) {
|
||||
("r=Reader.create(r)")
|
||||
("var c=l===undefined?r.len:r.pos+l,m=new this.ctor" + (mtype.fieldsArray.filter(function(field) { return field.map; }).length ? ",k,value" : ""))
|
||||
("while(r.pos<c){")
|
||||
+ ("var unknownStartPos = r.pos")
|
||||
("var t=r.uint32()");
|
||||
if (mtype.group) gen
|
||||
("if((t&7)===4)")
|
||||
@@ -28,6 +29,8 @@ function decoder(mtype) {
|
||||
gen
|
||||
("switch(t>>>3){");
|
||||
|
||||
+ var unknownRef = "m" + util.safeProp("__unknownFields");
|
||||
+
|
||||
var i = 0;
|
||||
for (; i < /* initializes */ mtype.fieldsArray.length; ++i) {
|
||||
var field = mtype._fieldsArray[i].resolve(),
|
||||
@@ -109,6 +112,11 @@ function decoder(mtype) {
|
||||
} gen
|
||||
("default:")
|
||||
("r.skipType(t&7)")
|
||||
+ ("if (!(%s)) {", unknownRef)
|
||||
+ ("%s = []", unknownRef)
|
||||
+ ("}")
|
||||
+
|
||||
+ ("%s.push(r.buf.slice(unknownStartPos, r.pos))", unknownRef)
|
||||
("break")
|
||||
|
||||
("}")
|
||||
diff --git a/node_modules/protobufjs/src/encoder.js b/node_modules/protobufjs/src/encoder.js
|
||||
index c803e99..d3c6e86 100644
|
||||
--- a/node_modules/protobufjs/src/encoder.js
|
||||
+++ b/node_modules/protobufjs/src/encoder.js
|
||||
@@ -94,6 +94,13 @@ function encoder(mtype) {
|
||||
}
|
||||
}
|
||||
|
||||
+ var unknownRef = "m" + util.safeProp("__unknownFields");
|
||||
+ gen
|
||||
+ ("if (%s) {", unknownRef)
|
||||
+ ("for (var i=0;i<%s.length;++i)", unknownRef)
|
||||
+ ("w.__unknownField(%s[i])", unknownRef)
|
||||
+ ("}")
|
||||
+
|
||||
return gen
|
||||
("return w");
|
||||
/* eslint-enable no-unexpected-multiline, block-scoped-var, no-redeclare */
|
||||
diff --git a/node_modules/protobufjs/src/writer.js b/node_modules/protobufjs/src/writer.js
|
||||
index cc84a00..3fb6139 100644
|
||||
--- a/node_modules/protobufjs/src/writer.js
|
||||
+++ b/node_modules/protobufjs/src/writer.js
|
||||
@@ -383,6 +383,10 @@ Writer.prototype.bytes = function write_bytes(value) {
|
||||
return this.uint32(len)._push(writeBytes, len, value);
|
||||
};
|
||||
|
||||
+Writer.prototype.__unknownField = function __unknownField(field) {
|
||||
+ return this._push(writeBytes, field.length, field);
|
||||
+};
|
||||
+
|
||||
/**
|
||||
* Writes a string.
|
||||
* @param {string} value Value to write
|
|
@ -30,7 +30,6 @@ try {
|
|||
|
||||
window.sqlInitializer = require('./ts/sql/initialize');
|
||||
|
||||
window.PROTO_ROOT = 'protos';
|
||||
const config = require('url').parse(window.location.toString(), true).query;
|
||||
|
||||
setEnvironment(parseEnvironment(config.environment));
|
||||
|
|
|
@ -15,9 +15,5 @@
|
|||
type="text/javascript"
|
||||
src="../../libtextsecure/protocol_wrapper.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="../../libtextsecure/protobufs.js"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -36,7 +36,6 @@ setEnvironment(parseEnvironment(config.environment));
|
|||
window.sqlInitializer = require('../ts/sql/initialize');
|
||||
|
||||
window.ROOT_PATH = window.location.href.startsWith('file') ? '../../' : '/';
|
||||
window.PROTO_ROOT = '../../protos';
|
||||
window.getEnvironment = getEnvironment;
|
||||
window.getVersion = () => config.version;
|
||||
window.getGuid = require('uuid/v4');
|
||||
|
|
|
@ -12,10 +12,8 @@ module.exports = {
|
|||
globals: {
|
||||
assert: true,
|
||||
assertEqualArrayBuffers: true,
|
||||
dcodeIO: true,
|
||||
getString: true,
|
||||
hexToArrayBuffer: true,
|
||||
PROTO_ROOT: true,
|
||||
stringToArrayBuffer: true,
|
||||
},
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
mocha.setup('bdd');
|
||||
window.assert = chai.assert;
|
||||
window.PROTO_ROOT = '../protos';
|
||||
|
||||
const OriginalReporter = mocha._reporter;
|
||||
|
||||
|
|
|
@ -1,499 +0,0 @@
|
|||
// Copyright 2014-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('Crypto', () => {
|
||||
describe('generateRegistrationId', () => {
|
||||
it('generates an integer between 0 and 16383 (inclusive)', () => {
|
||||
for (let i = 0; i < 100; i += 1) {
|
||||
const id = window.Signal.Crypto.generateRegistrationId();
|
||||
assert.isAtLeast(id, 0);
|
||||
assert.isAtMost(id, 16383);
|
||||
assert(Number.isInteger(id));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveSecrets', () => {
|
||||
it('derives key parts via HKDF', () => {
|
||||
const input = window.Signal.Crypto.getRandomBytes(32);
|
||||
const salt = window.Signal.Crypto.getRandomBytes(32);
|
||||
const info = window.Signal.Crypto.bytesFromString('Hello world');
|
||||
const result = window.Signal.Crypto.deriveSecrets(input, salt, info);
|
||||
assert.lengthOf(result, 3);
|
||||
result.forEach(part => {
|
||||
// This is a smoke test; HKDF is tested as part of @signalapp/signal-client.
|
||||
assert.instanceOf(part, ArrayBuffer);
|
||||
assert.strictEqual(part.byteLength, 32);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('accessKey/profileKey', () => {
|
||||
it('verification roundtrips', async () => {
|
||||
const profileKey = await window.Signal.Crypto.getRandomBytes(32);
|
||||
const accessKey = await window.Signal.Crypto.deriveAccessKey(profileKey);
|
||||
|
||||
const verifier = await window.Signal.Crypto.getAccessKeyVerifier(
|
||||
accessKey
|
||||
);
|
||||
|
||||
const correct = await window.Signal.Crypto.verifyAccessKey(
|
||||
accessKey,
|
||||
verifier
|
||||
);
|
||||
|
||||
assert.strictEqual(correct, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveMasterKeyFromGroupV1', () => {
|
||||
const vectors = [
|
||||
{
|
||||
gv1: '00000000000000000000000000000000',
|
||||
masterKey:
|
||||
'dbde68f4ee9169081f8814eabc65523fea1359235c8cfca32b69e31dce58b039',
|
||||
},
|
||||
{
|
||||
gv1: '000102030405060708090a0b0c0d0e0f',
|
||||
masterKey:
|
||||
'70884f78f07a94480ee36b67a4b5e975e92e4a774561e3df84c9076e3be4b9bf',
|
||||
},
|
||||
{
|
||||
gv1: '7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f',
|
||||
masterKey:
|
||||
'e69bf7c183b288b4ea5745b7c52b651a61e57769fafde683a6fdf1240f1905f2',
|
||||
},
|
||||
{
|
||||
gv1: 'ffffffffffffffffffffffffffffffff',
|
||||
masterKey:
|
||||
'dd3a7de23d10f18b64457fbeedc76226c112a730e4b76112e62c36c4432eb37d',
|
||||
},
|
||||
];
|
||||
|
||||
vectors.forEach((vector, index) => {
|
||||
it(`vector ${index}`, async () => {
|
||||
const gv1 = window.Signal.Crypto.hexToArrayBuffer(vector.gv1);
|
||||
const expectedHex = vector.masterKey;
|
||||
|
||||
const actual = await window.Signal.Crypto.deriveMasterKeyFromGroupV1(
|
||||
gv1
|
||||
);
|
||||
const actualHex = window.Signal.Crypto.arrayBufferToHex(actual);
|
||||
|
||||
assert.strictEqual(actualHex, expectedHex);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('symmetric encryption', () => {
|
||||
it('roundtrips', async () => {
|
||||
const message = 'this is my message';
|
||||
const plaintext = dcodeIO.ByteBuffer.wrap(
|
||||
message,
|
||||
'binary'
|
||||
).toArrayBuffer();
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await window.Signal.Crypto.encryptSymmetric(
|
||||
key,
|
||||
plaintext
|
||||
);
|
||||
const decrypted = await window.Signal.Crypto.decryptSymmetric(
|
||||
key,
|
||||
encrypted
|
||||
);
|
||||
|
||||
const equal = window.Signal.Crypto.constantTimeEqual(
|
||||
plaintext,
|
||||
decrypted
|
||||
);
|
||||
if (!equal) {
|
||||
throw new Error('The output and input did not match!');
|
||||
}
|
||||
});
|
||||
|
||||
it('roundtrip fails if nonce is modified', async () => {
|
||||
const message = 'this is my message';
|
||||
const plaintext = dcodeIO.ByteBuffer.wrap(
|
||||
message,
|
||||
'binary'
|
||||
).toArrayBuffer();
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await window.Signal.Crypto.encryptSymmetric(
|
||||
key,
|
||||
plaintext
|
||||
);
|
||||
const uintArray = new Uint8Array(encrypted);
|
||||
uintArray[2] += 2;
|
||||
|
||||
try {
|
||||
await window.Signal.Crypto.decryptSymmetric(
|
||||
key,
|
||||
window.window.Signal.Crypto.typedArrayToArrayBuffer(uintArray)
|
||||
);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected error to be thrown');
|
||||
});
|
||||
|
||||
it('roundtrip fails if mac is modified', async () => {
|
||||
const message = 'this is my message';
|
||||
const plaintext = dcodeIO.ByteBuffer.wrap(
|
||||
message,
|
||||
'binary'
|
||||
).toArrayBuffer();
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await window.Signal.Crypto.encryptSymmetric(
|
||||
key,
|
||||
plaintext
|
||||
);
|
||||
const uintArray = new Uint8Array(encrypted);
|
||||
uintArray[uintArray.length - 3] += 2;
|
||||
|
||||
try {
|
||||
await window.Signal.Crypto.decryptSymmetric(
|
||||
key,
|
||||
window.window.Signal.Crypto.typedArrayToArrayBuffer(uintArray)
|
||||
);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected error to be thrown');
|
||||
});
|
||||
|
||||
it('roundtrip fails if encrypted contents are modified', async () => {
|
||||
const message = 'this is my message';
|
||||
const plaintext = dcodeIO.ByteBuffer.wrap(
|
||||
message,
|
||||
'binary'
|
||||
).toArrayBuffer();
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await window.Signal.Crypto.encryptSymmetric(
|
||||
key,
|
||||
plaintext
|
||||
);
|
||||
const uintArray = new Uint8Array(encrypted);
|
||||
uintArray[35] += 9;
|
||||
|
||||
try {
|
||||
await window.Signal.Crypto.decryptSymmetric(
|
||||
key,
|
||||
window.window.Signal.Crypto.typedArrayToArrayBuffer(uintArray)
|
||||
);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected error to be thrown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('encrypted device name', () => {
|
||||
it('roundtrips', async () => {
|
||||
const deviceName = 'v1.19.0 on Windows 10';
|
||||
const identityKey = window.Signal.Curve.generateKeyPair();
|
||||
|
||||
const encrypted = await window.Signal.Crypto.encryptDeviceName(
|
||||
deviceName,
|
||||
identityKey.pubKey
|
||||
);
|
||||
const decrypted = await window.Signal.Crypto.decryptDeviceName(
|
||||
encrypted,
|
||||
identityKey.privKey
|
||||
);
|
||||
|
||||
assert.strictEqual(decrypted, deviceName);
|
||||
});
|
||||
|
||||
it('fails if iv is changed', async () => {
|
||||
const deviceName = 'v1.19.0 on Windows 10';
|
||||
const identityKey = window.Signal.Curve.generateKeyPair();
|
||||
|
||||
const encrypted = await window.Signal.Crypto.encryptDeviceName(
|
||||
deviceName,
|
||||
identityKey.pubKey
|
||||
);
|
||||
encrypted.syntheticIv = window.Signal.Crypto.getRandomBytes(16);
|
||||
try {
|
||||
await window.Signal.Crypto.decryptDeviceName(
|
||||
encrypted,
|
||||
identityKey.privKey
|
||||
);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'decryptDeviceName: synthetic IV did not match'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('attachment encryption', () => {
|
||||
it('roundtrips', async () => {
|
||||
const staticKeyPair = window.Signal.Curve.generateKeyPair();
|
||||
const message = 'this is my message';
|
||||
const plaintext = window.Signal.Crypto.bytesFromString(message);
|
||||
const path =
|
||||
'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa';
|
||||
|
||||
const encrypted = await window.Signal.Crypto.encryptAttachment(
|
||||
staticKeyPair.pubKey.slice(1),
|
||||
path,
|
||||
plaintext
|
||||
);
|
||||
const decrypted = await window.Signal.Crypto.decryptAttachment(
|
||||
staticKeyPair.privKey,
|
||||
path,
|
||||
encrypted
|
||||
);
|
||||
|
||||
const equal = window.Signal.Crypto.constantTimeEqual(
|
||||
plaintext,
|
||||
decrypted
|
||||
);
|
||||
if (!equal) {
|
||||
throw new Error('The output and input did not match!');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyHmacSha256', () => {
|
||||
it('rejects if their MAC is too short', async () => {
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
|
||||
const ourMac = await window.Signal.Crypto.hmacSha256(key, plaintext);
|
||||
const theirMac = ourMac.slice(0, -1);
|
||||
let error;
|
||||
try {
|
||||
await window.Signal.Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
ourMac.byteLength
|
||||
);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert.instanceOf(error, Error);
|
||||
assert.strictEqual(error.message, 'Bad MAC length');
|
||||
});
|
||||
|
||||
it('rejects if their MAC is too long', async () => {
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
|
||||
const ourMac = await window.Signal.Crypto.hmacSha256(key, plaintext);
|
||||
const theirMac = window.Signal.Crypto.concatenateBytes(
|
||||
ourMac,
|
||||
new Uint8Array([0xff])
|
||||
);
|
||||
let error;
|
||||
try {
|
||||
await window.Signal.Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
ourMac.byteLength
|
||||
);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert.instanceOf(error, Error);
|
||||
assert.strictEqual(error.message, 'Bad MAC length');
|
||||
});
|
||||
|
||||
it('rejects if our MAC is shorter than the specified length', async () => {
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
|
||||
const ourMac = await window.Signal.Crypto.hmacSha256(key, plaintext);
|
||||
const theirMac = ourMac;
|
||||
let error;
|
||||
try {
|
||||
await window.Signal.Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
ourMac.byteLength + 1
|
||||
);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert.instanceOf(error, Error);
|
||||
assert.strictEqual(error.message, 'Bad MAC length');
|
||||
});
|
||||
|
||||
it("rejects if the MACs don't match", async () => {
|
||||
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
|
||||
const ourKey = window.Signal.Crypto.getRandomBytes(32);
|
||||
const ourMac = await window.Signal.Crypto.hmacSha256(ourKey, plaintext);
|
||||
const theirKey = window.Signal.Crypto.getRandomBytes(32);
|
||||
const theirMac = await window.Signal.Crypto.hmacSha256(
|
||||
theirKey,
|
||||
plaintext
|
||||
);
|
||||
let error;
|
||||
try {
|
||||
await window.Signal.Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
ourKey,
|
||||
theirMac,
|
||||
ourMac.byteLength
|
||||
);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert.instanceOf(error, Error);
|
||||
assert.strictEqual(error.message, 'Bad MAC');
|
||||
});
|
||||
|
||||
it('resolves with undefined if the MACs match exactly', async () => {
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
|
||||
const theirMac = await window.Signal.Crypto.hmacSha256(key, plaintext);
|
||||
const result = await window.Signal.Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
theirMac.byteLength
|
||||
);
|
||||
assert.isUndefined(result);
|
||||
});
|
||||
|
||||
it('resolves with undefined if the first `length` bytes of the MACs match', async () => {
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
|
||||
const theirMac = (
|
||||
await window.Signal.Crypto.hmacSha256(key, plaintext)
|
||||
).slice(0, -5);
|
||||
const result = await window.Signal.Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
theirMac.byteLength
|
||||
);
|
||||
assert.isUndefined(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uuidToArrayBuffer', () => {
|
||||
const { uuidToArrayBuffer } = window.Signal.Crypto;
|
||||
|
||||
it('converts valid UUIDs to ArrayBuffers', () => {
|
||||
const expectedResult = window.window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([
|
||||
0x22,
|
||||
0x6e,
|
||||
0x44,
|
||||
0x02,
|
||||
0x7f,
|
||||
0xfc,
|
||||
0x45,
|
||||
0x43,
|
||||
0x85,
|
||||
0xc9,
|
||||
0x46,
|
||||
0x22,
|
||||
0xc5,
|
||||
0x0a,
|
||||
0x5b,
|
||||
0x14,
|
||||
])
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
uuidToArrayBuffer('226e4402-7ffc-4543-85c9-4622c50a5b14'),
|
||||
expectedResult
|
||||
);
|
||||
assert.deepEqual(
|
||||
uuidToArrayBuffer('226E4402-7FFC-4543-85C9-4622C50A5B14'),
|
||||
expectedResult
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty ArrayBuffer for strings of the wrong length', () => {
|
||||
assert.deepEqual(uuidToArrayBuffer(''), new ArrayBuffer(0));
|
||||
assert.deepEqual(uuidToArrayBuffer('abc'), new ArrayBuffer(0));
|
||||
assert.deepEqual(
|
||||
uuidToArrayBuffer('032deadf0d5e4ee78da28e75b1dfb284'),
|
||||
new ArrayBuffer(0)
|
||||
);
|
||||
assert.deepEqual(
|
||||
uuidToArrayBuffer('deaed5eb-d983-456a-a954-9ad7a006b271aaaaaaaaaa'),
|
||||
new ArrayBuffer(0)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arrayBufferToUuid', () => {
|
||||
const { arrayBufferToUuid } = window.Signal.Crypto;
|
||||
|
||||
it('converts valid ArrayBuffers to UUID strings', () => {
|
||||
const buf = window.window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([
|
||||
0x22,
|
||||
0x6e,
|
||||
0x44,
|
||||
0x02,
|
||||
0x7f,
|
||||
0xfc,
|
||||
0x45,
|
||||
0x43,
|
||||
0x85,
|
||||
0xc9,
|
||||
0x46,
|
||||
0x22,
|
||||
0xc5,
|
||||
0x0a,
|
||||
0x5b,
|
||||
0x14,
|
||||
])
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
arrayBufferToUuid(buf),
|
||||
'226e4402-7ffc-4543-85c9-4622c50a5b14'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns undefined if passed an all-zero buffer', () => {
|
||||
assert.isUndefined(arrayBufferToUuid(new ArrayBuffer(16)));
|
||||
});
|
||||
|
||||
it('returns undefined if passed the wrong number of bytes', () => {
|
||||
assert.isUndefined(arrayBufferToUuid(new ArrayBuffer(0)));
|
||||
assert.isUndefined(
|
||||
arrayBufferToUuid(
|
||||
window.window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([0x22])
|
||||
)
|
||||
)
|
||||
);
|
||||
assert.isUndefined(
|
||||
arrayBufferToUuid(
|
||||
window.window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array(Array(17).fill(0x22))
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -332,7 +332,6 @@
|
|||
type="text/javascript"
|
||||
src="../libtextsecure/protocol_wrapper.js"
|
||||
></script>
|
||||
<script type="text/javascript" src="../libtextsecure/protobufs.js"></script>
|
||||
|
||||
<script type="text/javascript" src="../js/libphonenumber-util.js"></script>
|
||||
<script
|
||||
|
@ -411,10 +410,8 @@
|
|||
<script type="text/javascript" src="libphonenumber_util_test.js"></script>
|
||||
<script type="text/javascript" src="keychange_listener_test.js"></script>
|
||||
<script type="text/javascript" src="reliable_trigger_test.js"></script>
|
||||
<script type="text/javascript" src="crypto_test.js"></script>
|
||||
<script type="text/javascript" src="database_test.js"></script>
|
||||
<script type="text/javascript" src="i18n_test.js"></script>
|
||||
<script type="text/javascript" src="protobuf_test.js"></script>
|
||||
<script type="text/javascript" src="stickers_test.js"></script>
|
||||
|
||||
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->
|
||||
|
@ -423,7 +420,6 @@
|
|||
|
||||
<!-- Uncomment to start tests without code coverage enabled -->
|
||||
<script type="text/javascript">
|
||||
window.textsecure.protobuf.onLoad(() => {
|
||||
window.Signal.conversationControllerStart();
|
||||
|
||||
window.test.pendingDescribeCalls.forEach(args => {
|
||||
|
@ -431,7 +427,6 @@
|
|||
});
|
||||
|
||||
mocha.run();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
describe('ProtoBuf.js', () => {
|
||||
const { ProtoBuf } = window.dcodeIO;
|
||||
|
||||
const sampleProto = `message Simple_v1 {
|
||||
optional string knownName = 1;
|
||||
optional string knownValue = 3;
|
||||
}
|
||||
message Simple_v2 {
|
||||
optional string knownName = 1;
|
||||
optional int32 unknownFlags = 2;
|
||||
optional string knownValue = 3;
|
||||
optional string unknownString = 4;
|
||||
}`;
|
||||
|
||||
it('retains unknown fields', () => {
|
||||
const builder = ProtoBuf.loadProto(sampleProto);
|
||||
const protos = builder.build();
|
||||
|
||||
const v2 = new protos.Simple_v2();
|
||||
v2.knownName = 'version2';
|
||||
v2.unknownFlags = 42;
|
||||
v2.knownValue = 'known value';
|
||||
v2.unknownString = 'f';
|
||||
|
||||
const v1 = protos.Simple_v1.decode(v2.encode());
|
||||
|
||||
const result = protos.Simple_v2.decode(v1.encode());
|
||||
|
||||
assert.equal(result.knownName, v2.knownName, 'known fields');
|
||||
assert.equal(42, result.unknownFlags, 'unknown flag');
|
||||
assert.equal('f', result.unknownString, 'unknown string');
|
||||
assert.equal('known value', result.knownValue, 'known value');
|
||||
});
|
||||
|
||||
it('supports nested unknown fields', () => {
|
||||
const nestedProto = `
|
||||
${sampleProto}
|
||||
message Container_v1 {
|
||||
optional Simple_v1 elem = 1;
|
||||
}
|
||||
message Container_v2 {
|
||||
optional Simple_v2 elem = 1;
|
||||
}`;
|
||||
|
||||
const builder = ProtoBuf.loadProto(nestedProto);
|
||||
const protos = builder.build();
|
||||
|
||||
const v2 = new protos.Container_v2();
|
||||
v2.elem = {
|
||||
knownName: 'nested v2',
|
||||
unknownFlags: 10,
|
||||
knownValue: 'hello world',
|
||||
};
|
||||
|
||||
const v1 = protos.Container_v1.decode(v2.encode());
|
||||
|
||||
const result = protos.Container_v2.decode(v1.encode());
|
||||
|
||||
assert.equal(
|
||||
v2.elem.knownName,
|
||||
result.elem.knownName,
|
||||
'nested: known fields'
|
||||
);
|
||||
assert.equal(10, result.elem.unknownFlags, 'nested: unknown flags');
|
||||
assert.equal('hello world', result.elem.knownValue, 'known value');
|
||||
});
|
||||
|
||||
it('allows multi-byte id', () => {
|
||||
const proto = `message Simple_v1 {
|
||||
optional string knownName = 1;
|
||||
optional string knownValue = 3;
|
||||
}
|
||||
message Simple_v2 {
|
||||
optional string knownName = 1;
|
||||
optional int32 unknownFlags = 296;
|
||||
optional string knownValue = 3;
|
||||
}`;
|
||||
|
||||
const builder = ProtoBuf.loadProto(proto);
|
||||
const protos = builder.build();
|
||||
|
||||
const v2 = new protos.Simple_v2();
|
||||
v2.knownName = 'v2 multibyte';
|
||||
v2.unknownFlags = 16;
|
||||
v2.knownValue = 'foo bar';
|
||||
|
||||
const v1 = protos.Simple_v1.decode(v2.encode());
|
||||
|
||||
const result = protos.Simple_v2.decode(v1.encode());
|
||||
|
||||
assert.equal(result.knownName, v2.knownName, 'multibyte: known fields');
|
||||
assert.equal(16, result.unknownFlags, 'multibyte: unknown fields');
|
||||
assert.equal('foo bar', result.knownValue, 'multibyte: known value');
|
||||
});
|
||||
|
||||
it('retains fields with 64bit type', () => {
|
||||
const proto = `message Simple_v1 {
|
||||
optional string knownName = 1;
|
||||
optional string knownValue = 3;
|
||||
}
|
||||
message Simple_v2 {
|
||||
optional string knownName = 1;
|
||||
optional double unknownFlags = 2;
|
||||
optional string knownValue = 3;
|
||||
}`;
|
||||
|
||||
const builder = ProtoBuf.loadProto(proto);
|
||||
const protos = builder.build();
|
||||
|
||||
const v2 = new protos.Simple_v2();
|
||||
v2.knownName = 'v2 double';
|
||||
v2.unknownFlags = 0;
|
||||
v2.knownValue = 'double double';
|
||||
|
||||
const v1 = protos.Simple_v1.decode(v2.encode());
|
||||
|
||||
const result = protos.Simple_v2.decode(v1.encode());
|
||||
|
||||
assert.equal(result.knownName, v2.knownName, 'double: known fields');
|
||||
assert.equal(0, result.unknownFlags, 'double: unknown fields');
|
||||
assert.equal('double double', result.knownValue, 'double: known value');
|
||||
});
|
||||
});
|
|
@ -6,8 +6,6 @@
|
|||
const chai = require('chai');
|
||||
const chaiAsPromised = require('chai-as-promised');
|
||||
|
||||
const ByteBuffer = require('../components/bytebuffer/dist/ByteBufferAB.js');
|
||||
const Long = require('../components/long/dist/Long.js');
|
||||
const { setEnvironment, Environment } = require('../ts/environment');
|
||||
const { Context: SignalContext } = require('../ts/context');
|
||||
const { isValidGuid } = require('../ts/util/isValidGuid');
|
||||
|
@ -27,10 +25,6 @@ global.window = {
|
|||
error: (...args) => console.error(...args),
|
||||
},
|
||||
i18n: key => `i18n(${key})`,
|
||||
dcodeIO: {
|
||||
ByteBuffer,
|
||||
Long,
|
||||
},
|
||||
storage: {
|
||||
get: key => storageMap.get(key),
|
||||
put: async (key, value) => storageMap.set(key, value),
|
||||
|
|
|
@ -37,7 +37,7 @@ export function toString(data: Uint8Array): string {
|
|||
return bytes.toString(data);
|
||||
}
|
||||
|
||||
export function concatenate(list: Array<Uint8Array>): Uint8Array {
|
||||
export function concatenate(list: ReadonlyArray<Uint8Array>): Uint8Array {
|
||||
return bytes.concatenate(list);
|
||||
}
|
||||
|
||||
|
@ -50,3 +50,10 @@ export function isNotEmpty(
|
|||
): data is Uint8Array {
|
||||
return !bytes.isEmpty(data);
|
||||
}
|
||||
|
||||
export function areEqual(
|
||||
a: Uint8Array | null | undefined,
|
||||
b: Uint8Array | null | undefined
|
||||
): boolean {
|
||||
return bytes.areEqual(a, b);
|
||||
}
|
||||
|
|
64
ts/Crypto.ts
64
ts/Crypto.ts
|
@ -1,9 +1,12 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { Buffer } from 'buffer';
|
||||
import pProps from 'p-props';
|
||||
import { chunk } from 'lodash';
|
||||
import Long from 'long';
|
||||
import { HKDF } from '@signalapp/signal-client';
|
||||
|
||||
import { calculateAgreement, generateKeyPair } from './Curve';
|
||||
|
||||
import {
|
||||
|
@ -34,37 +37,43 @@ export function typedArrayToArrayBuffer(typedArray: Uint8Array): ArrayBuffer {
|
|||
}
|
||||
|
||||
export function arrayBufferToBase64(arrayBuffer: ArrayBuffer): string {
|
||||
return window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
|
||||
// NOTE: We can't use `Bytes.toBase64` here because this runs in both
|
||||
// node and electron contexts.
|
||||
return Buffer.from(arrayBuffer).toString('base64');
|
||||
}
|
||||
|
||||
export function arrayBufferToHex(arrayBuffer: ArrayBuffer): string {
|
||||
return window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('hex');
|
||||
return Buffer.from(arrayBuffer).toString('hex');
|
||||
}
|
||||
|
||||
export function base64ToArrayBuffer(base64string: string): ArrayBuffer {
|
||||
return window.dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();
|
||||
return typedArrayToArrayBuffer(Buffer.from(base64string, 'base64'));
|
||||
}
|
||||
|
||||
export function hexToArrayBuffer(hexString: string): ArrayBuffer {
|
||||
return window.dcodeIO.ByteBuffer.wrap(hexString, 'hex').toArrayBuffer();
|
||||
return typedArrayToArrayBuffer(Buffer.from(hexString, 'hex'));
|
||||
}
|
||||
|
||||
export function fromEncodedBinaryToArrayBuffer(key: string): ArrayBuffer {
|
||||
return window.dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();
|
||||
return typedArrayToArrayBuffer(Buffer.from(key, 'binary'));
|
||||
}
|
||||
|
||||
export function arrayBufferToEncodedBinary(arrayBuffer: ArrayBuffer): string {
|
||||
return Buffer.from(arrayBuffer).toString('binary');
|
||||
}
|
||||
|
||||
export function bytesFromString(string: string): ArrayBuffer {
|
||||
return window.dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();
|
||||
return typedArrayToArrayBuffer(Buffer.from(string));
|
||||
}
|
||||
export function stringFromBytes(buffer: ArrayBuffer): string {
|
||||
return window.dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');
|
||||
return Buffer.from(buffer).toString();
|
||||
}
|
||||
export function hexFromBytes(buffer: ArrayBuffer): string {
|
||||
return window.dcodeIO.ByteBuffer.wrap(buffer).toString('hex');
|
||||
return Buffer.from(buffer).toString('hex');
|
||||
}
|
||||
|
||||
export function bytesFromHexString(string: string): ArrayBuffer {
|
||||
return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();
|
||||
return typedArrayToArrayBuffer(Buffer.from(string, 'hex'));
|
||||
}
|
||||
|
||||
export async function deriveStickerPackKey(
|
||||
|
@ -115,10 +124,16 @@ export async function computeHash(data: ArrayBuffer): Promise<string> {
|
|||
|
||||
// High-level Operations
|
||||
|
||||
export type EncryptedDeviceName = {
|
||||
ephemeralPublic: ArrayBuffer;
|
||||
syntheticIv: ArrayBuffer;
|
||||
ciphertext: ArrayBuffer;
|
||||
};
|
||||
|
||||
export async function encryptDeviceName(
|
||||
deviceName: string,
|
||||
identityPublic: ArrayBuffer
|
||||
): Promise<Record<string, ArrayBuffer>> {
|
||||
): Promise<EncryptedDeviceName> {
|
||||
const plaintext = bytesFromString(deviceName);
|
||||
const ephemeralKeyPair = generateKeyPair();
|
||||
const masterSecret = calculateAgreement(
|
||||
|
@ -143,15 +158,7 @@ export async function encryptDeviceName(
|
|||
}
|
||||
|
||||
export async function decryptDeviceName(
|
||||
{
|
||||
ephemeralPublic,
|
||||
syntheticIv,
|
||||
ciphertext,
|
||||
}: {
|
||||
ephemeralPublic: ArrayBuffer;
|
||||
syntheticIv: ArrayBuffer;
|
||||
ciphertext: ArrayBuffer;
|
||||
},
|
||||
{ ephemeralPublic, syntheticIv, ciphertext }: EncryptedDeviceName,
|
||||
identityPrivate: ArrayBuffer
|
||||
): Promise<string> {
|
||||
const masterSecret = calculateAgreement(ephemeralPublic, identityPrivate);
|
||||
|
@ -661,21 +668,18 @@ export async function encryptCdsDiscoveryRequest(
|
|||
phoneNumbers: ReadonlyArray<string>
|
||||
): Promise<Record<string, unknown>> {
|
||||
const nonce = getRandomBytes(32);
|
||||
const numbersArray = new window.dcodeIO.ByteBuffer(
|
||||
phoneNumbers.length * 8,
|
||||
window.dcodeIO.ByteBuffer.BIG_ENDIAN
|
||||
);
|
||||
phoneNumbers.forEach(number => {
|
||||
const numbersArray = Buffer.concat(
|
||||
phoneNumbers.map(number => {
|
||||
// Long.fromString handles numbers with or without a leading '+'
|
||||
numbersArray.writeLong(window.dcodeIO.ByteBuffer.Long.fromString(number));
|
||||
});
|
||||
return new Uint8Array(Long.fromString(number).toBytesBE());
|
||||
})
|
||||
);
|
||||
|
||||
// We've written to the array, so offset === byteLength; we need to reset it. Then we'll
|
||||
// have access to everything in the array when we generate an ArrayBuffer from it.
|
||||
numbersArray.reset();
|
||||
const queryDataPlaintext = concatenateBytes(
|
||||
nonce,
|
||||
numbersArray.toArrayBuffer()
|
||||
typedArrayToArrayBuffer(numbersArray)
|
||||
);
|
||||
|
||||
const queryDataKey = getRandomBytes(32);
|
||||
|
@ -785,7 +789,5 @@ export function trimForDisplay(arrayBuffer: ArrayBuffer): ArrayBuffer {
|
|||
break;
|
||||
}
|
||||
}
|
||||
return window.dcodeIO.ByteBuffer.wrap(padded)
|
||||
.slice(0, paddingEnd)
|
||||
.toArrayBuffer();
|
||||
return typedArrayToArrayBuffer(padded.slice(0, paddingEnd));
|
||||
}
|
||||
|
|
|
@ -54,4 +54,15 @@ export class Bytes {
|
|||
public isNotEmpty(data: Uint8Array | null | undefined): data is Uint8Array {
|
||||
return !this.isEmpty(data);
|
||||
}
|
||||
|
||||
public areEqual(
|
||||
a: Uint8Array | null | undefined,
|
||||
b: Uint8Array | null | undefined
|
||||
): boolean {
|
||||
if (!a || !b) {
|
||||
return !a && !b;
|
||||
}
|
||||
|
||||
return Buffer.compare(a, b) === 0;
|
||||
}
|
||||
}
|
||||
|
|
9
ts/model-types.d.ts
vendored
9
ts/model-types.d.ts
vendored
|
@ -10,11 +10,6 @@ import { CustomColorType } from './types/Colors';
|
|||
import { DeviceType } from './textsecure/Types';
|
||||
import { SendOptionsType } from './textsecure/SendMessage';
|
||||
import { SendMessageChallengeData } from './textsecure/Errors';
|
||||
import {
|
||||
AccessRequiredEnum,
|
||||
MemberRoleEnum,
|
||||
SyncMessageClass,
|
||||
} from './textsecure.d';
|
||||
import { UserMessage } from './types/Message';
|
||||
import { MessageModel } from './models/messages';
|
||||
import { ConversationModel } from './models/conversations';
|
||||
|
@ -24,6 +19,10 @@ import { GroupNameCollisionsWithIdsByTitle } from './util/groupMemberNameCollisi
|
|||
import { ConversationColorType } from './types/Colors';
|
||||
import { AttachmentType, ThumbnailType } from './types/Attachment';
|
||||
import { ContactType } from './types/Contact';
|
||||
import { SignalService as Proto } from './protobuf';
|
||||
|
||||
import AccessRequiredEnum = Proto.AccessControl.AccessRequired;
|
||||
import MemberRoleEnum = Proto.Member.Role;
|
||||
|
||||
export type WhatIsThis = any;
|
||||
|
||||
|
|
|
@ -6,19 +6,14 @@ import pMap from 'p-map';
|
|||
|
||||
import Crypto from '../textsecure/Crypto';
|
||||
import dataInterface from '../sql/Client';
|
||||
import * as Bytes from '../Bytes';
|
||||
import {
|
||||
arrayBufferToBase64,
|
||||
base64ToArrayBuffer,
|
||||
deriveStorageItemKey,
|
||||
deriveStorageManifestKey,
|
||||
typedArrayToArrayBuffer,
|
||||
} from '../Crypto';
|
||||
import {
|
||||
ManifestRecordClass,
|
||||
ManifestRecordIdentifierClass,
|
||||
StorageItemClass,
|
||||
StorageManifestClass,
|
||||
StorageRecordClass,
|
||||
} from '../textsecure.d';
|
||||
import {
|
||||
mergeAccountRecord,
|
||||
mergeContactRecord,
|
||||
|
@ -30,16 +25,24 @@ import {
|
|||
toGroupV2Record,
|
||||
} from './storageRecordOps';
|
||||
import { ConversationModel } from '../models/conversations';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { BackOff } from '../util/BackOff';
|
||||
import { storageJobQueue } from '../util/JobQueue';
|
||||
import { sleep } from '../util/sleep';
|
||||
import { isMoreRecentThan } from '../util/timestamp';
|
||||
import { normalizeNumber } from '../util/normalizeNumber';
|
||||
import { isStorageWriteFeatureEnabled } from '../storage/isFeatureEnabled';
|
||||
import { ourProfileKeyService } from './ourProfileKey';
|
||||
import {
|
||||
ConversationTypes,
|
||||
typeofConversation,
|
||||
} from '../util/whatTypeOfConversation';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier;
|
||||
|
||||
// TODO: remove once we move away from ArrayBuffers
|
||||
const FIXMEU8 = Uint8Array;
|
||||
|
||||
const {
|
||||
eraseStorageServiceStateFromConversations,
|
||||
|
@ -82,9 +85,9 @@ type UnknownRecord = RemoteRecord;
|
|||
|
||||
async function encryptRecord(
|
||||
storageID: string | undefined,
|
||||
storageRecord: StorageRecordClass
|
||||
): Promise<StorageItemClass> {
|
||||
const storageItem = new window.textsecure.protobuf.StorageItem();
|
||||
storageRecord: Proto.IStorageRecord
|
||||
): Promise<Proto.StorageItem> {
|
||||
const storageItem = new Proto.StorageItem();
|
||||
|
||||
const storageKeyBuffer = storageID
|
||||
? base64ToArrayBuffer(String(storageID))
|
||||
|
@ -101,12 +104,12 @@ async function encryptRecord(
|
|||
);
|
||||
|
||||
const encryptedRecord = await Crypto.encryptProfile(
|
||||
storageRecord.toArrayBuffer(),
|
||||
typedArrayToArrayBuffer(Proto.StorageRecord.encode(storageRecord).finish()),
|
||||
storageItemKey
|
||||
);
|
||||
|
||||
storageItem.key = storageKeyBuffer;
|
||||
storageItem.value = encryptedRecord;
|
||||
storageItem.key = new FIXMEU8(storageKeyBuffer);
|
||||
storageItem.value = new FIXMEU8(encryptedRecord);
|
||||
|
||||
return storageItem;
|
||||
}
|
||||
|
@ -121,13 +124,13 @@ type GeneratedManifestType = {
|
|||
storageID: string | undefined;
|
||||
}>;
|
||||
deleteKeys: Array<ArrayBuffer>;
|
||||
newItems: Set<StorageItemClass>;
|
||||
storageManifest: StorageManifestClass;
|
||||
newItems: Set<Proto.IStorageItem>;
|
||||
storageManifest: Proto.IStorageManifest;
|
||||
};
|
||||
|
||||
async function generateManifest(
|
||||
version: number,
|
||||
previousManifest?: ManifestRecordClass,
|
||||
previousManifest?: Proto.IManifestRecord,
|
||||
isNewManifest = false
|
||||
): Promise<GeneratedManifestType> {
|
||||
window.log.info(
|
||||
|
@ -138,39 +141,39 @@ async function generateManifest(
|
|||
|
||||
await window.ConversationController.checkForConflicts();
|
||||
|
||||
const ITEM_TYPE = window.textsecure.protobuf.ManifestRecord.Identifier.Type;
|
||||
const ITEM_TYPE = Proto.ManifestRecord.Identifier.Type;
|
||||
|
||||
const conversationsToUpdate = [];
|
||||
const insertKeys: Array<string> = [];
|
||||
const deleteKeys: Array<ArrayBuffer> = [];
|
||||
const manifestRecordKeys: Set<ManifestRecordIdentifierClass> = new Set();
|
||||
const newItems: Set<StorageItemClass> = new Set();
|
||||
const manifestRecordKeys: Set<IManifestRecordIdentifier> = new Set();
|
||||
const newItems: Set<Proto.IStorageItem> = new Set();
|
||||
|
||||
const conversations = window.getConversations();
|
||||
for (let i = 0; i < conversations.length; i += 1) {
|
||||
const conversation = conversations.models[i];
|
||||
const identifier = new window.textsecure.protobuf.ManifestRecord.Identifier();
|
||||
const identifier = new Proto.ManifestRecord.Identifier();
|
||||
|
||||
let storageRecord;
|
||||
|
||||
const conversationType = typeofConversation(conversation.attributes);
|
||||
if (conversationType === ConversationTypes.Me) {
|
||||
storageRecord = new window.textsecure.protobuf.StorageRecord();
|
||||
storageRecord = new Proto.StorageRecord();
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
storageRecord.account = await toAccountRecord(conversation);
|
||||
identifier.type = ITEM_TYPE.ACCOUNT;
|
||||
} else if (conversationType === ConversationTypes.Direct) {
|
||||
storageRecord = new window.textsecure.protobuf.StorageRecord();
|
||||
storageRecord = new Proto.StorageRecord();
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
storageRecord.contact = await toContactRecord(conversation);
|
||||
identifier.type = ITEM_TYPE.CONTACT;
|
||||
} else if (conversationType === ConversationTypes.GroupV2) {
|
||||
storageRecord = new window.textsecure.protobuf.StorageRecord();
|
||||
storageRecord = new Proto.StorageRecord();
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
storageRecord.groupV2 = await toGroupV2Record(conversation);
|
||||
identifier.type = ITEM_TYPE.GROUPV2;
|
||||
} else if (conversationType === ConversationTypes.GroupV1) {
|
||||
storageRecord = new window.textsecure.protobuf.StorageRecord();
|
||||
storageRecord = new Proto.StorageRecord();
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
storageRecord.groupV1 = await toGroupV1Record(conversation);
|
||||
identifier.type = ITEM_TYPE.GROUPV1;
|
||||
|
@ -256,9 +259,9 @@ async function generateManifest(
|
|||
// When updating the manifest, ensure all "unknown" keys are added to the
|
||||
// new manifest, so we don't inadvertently delete something we don't understand
|
||||
unknownRecordsArray.forEach((record: UnknownRecord) => {
|
||||
const identifier = new window.textsecure.protobuf.ManifestRecord.Identifier();
|
||||
const identifier = new Proto.ManifestRecord.Identifier();
|
||||
identifier.type = record.itemType;
|
||||
identifier.raw = base64ToArrayBuffer(record.storageID);
|
||||
identifier.raw = Bytes.fromBase64(record.storageID);
|
||||
|
||||
manifestRecordKeys.add(identifier);
|
||||
});
|
||||
|
@ -276,9 +279,9 @@ async function generateManifest(
|
|||
// These records failed to merge in the previous fetchManifest, but we still
|
||||
// need to include them so that the manifest is complete
|
||||
recordsWithErrors.forEach((record: UnknownRecord) => {
|
||||
const identifier = new window.textsecure.protobuf.ManifestRecord.Identifier();
|
||||
const identifier = new Proto.ManifestRecord.Identifier();
|
||||
identifier.type = record.itemType;
|
||||
identifier.raw = base64ToArrayBuffer(record.storageID);
|
||||
identifier.raw = Bytes.fromBase64(record.storageID);
|
||||
|
||||
manifestRecordKeys.add(identifier);
|
||||
});
|
||||
|
@ -293,7 +296,8 @@ async function generateManifest(
|
|||
// This can be broken down into two parts:
|
||||
// There are no duplicate type+raw pairs
|
||||
// There are no duplicate raw bytes
|
||||
const storageID = arrayBufferToBase64(identifier.raw);
|
||||
strictAssert(identifier.raw, 'manifest record key without raw identifier');
|
||||
const storageID = Bytes.toBase64(identifier.raw);
|
||||
const typeAndRaw = `${identifier.type}+${storageID}`;
|
||||
if (
|
||||
rawDuplicates.has(identifier.raw) ||
|
||||
|
@ -335,11 +339,13 @@ async function generateManifest(
|
|||
rawDuplicates.clear();
|
||||
typeRawDuplicates.clear();
|
||||
|
||||
const storageKeyDuplicates = new Set();
|
||||
const storageKeyDuplicates = new Set<string>();
|
||||
|
||||
newItems.forEach(storageItem => {
|
||||
// Ensure there are no duplicate StorageIdentifiers in your list of inserts
|
||||
const storageID = storageItem.key;
|
||||
strictAssert(storageItem.key, 'New storage item without key');
|
||||
|
||||
const storageID = Bytes.toBase64(storageItem.key);
|
||||
if (storageKeyDuplicates.has(storageID)) {
|
||||
window.log.info(
|
||||
'storageService.generateManifest: removing duplicate identifier from inserts',
|
||||
|
@ -360,16 +366,18 @@ async function generateManifest(
|
|||
const pendingDeletes: Set<string> = new Set();
|
||||
|
||||
const remoteKeys: Set<string> = new Set();
|
||||
previousManifest.keys.forEach(
|
||||
(identifier: ManifestRecordIdentifierClass) => {
|
||||
const storageID = arrayBufferToBase64(identifier.raw.toArrayBuffer());
|
||||
(previousManifest.keys ?? []).forEach(
|
||||
(identifier: IManifestRecordIdentifier) => {
|
||||
strictAssert(identifier.raw, 'Identifier without raw field');
|
||||
const storageID = Bytes.toBase64(identifier.raw);
|
||||
remoteKeys.add(storageID);
|
||||
}
|
||||
);
|
||||
|
||||
const localKeys: Set<string> = new Set();
|
||||
manifestRecordKeys.forEach((identifier: ManifestRecordIdentifierClass) => {
|
||||
const storageID = arrayBufferToBase64(identifier.raw);
|
||||
manifestRecordKeys.forEach((identifier: IManifestRecordIdentifier) => {
|
||||
strictAssert(identifier.raw, 'Identifier without raw field');
|
||||
const storageID = Bytes.toBase64(identifier.raw);
|
||||
localKeys.add(storageID);
|
||||
|
||||
if (!remoteKeys.has(storageID)) {
|
||||
|
@ -406,7 +414,7 @@ async function generateManifest(
|
|||
});
|
||||
}
|
||||
|
||||
const manifestRecord = new window.textsecure.protobuf.ManifestRecord();
|
||||
const manifestRecord = new Proto.ManifestRecord();
|
||||
manifestRecord.version = version;
|
||||
manifestRecord.keys = Array.from(manifestRecordKeys);
|
||||
|
||||
|
@ -420,13 +428,15 @@ async function generateManifest(
|
|||
version
|
||||
);
|
||||
const encryptedManifest = await Crypto.encryptProfile(
|
||||
manifestRecord.toArrayBuffer(),
|
||||
typedArrayToArrayBuffer(
|
||||
Proto.ManifestRecord.encode(manifestRecord).finish()
|
||||
),
|
||||
storageManifestKey
|
||||
);
|
||||
|
||||
const storageManifest = new window.textsecure.protobuf.StorageManifest();
|
||||
const storageManifest = new Proto.StorageManifest();
|
||||
storageManifest.version = version;
|
||||
storageManifest.value = encryptedManifest;
|
||||
storageManifest.value = new FIXMEU8(encryptedManifest);
|
||||
|
||||
return {
|
||||
conversationsToUpdate,
|
||||
|
@ -462,14 +472,16 @@ async function uploadManifest(
|
|||
deleteKeys.length
|
||||
);
|
||||
|
||||
const writeOperation = new window.textsecure.protobuf.WriteOperation();
|
||||
const writeOperation = new Proto.WriteOperation();
|
||||
writeOperation.manifest = storageManifest;
|
||||
writeOperation.insertItem = Array.from(newItems);
|
||||
writeOperation.deleteKey = deleteKeys;
|
||||
writeOperation.deleteKey = deleteKeys.map(key => new FIXMEU8(key));
|
||||
|
||||
window.log.info('storageService.uploadManifest: uploading...', version);
|
||||
await window.textsecure.messaging.modifyStorageRecords(
|
||||
writeOperation.toArrayBuffer(),
|
||||
typedArrayToArrayBuffer(
|
||||
Proto.WriteOperation.encode(writeOperation).finish()
|
||||
),
|
||||
{
|
||||
credentials,
|
||||
}
|
||||
|
@ -565,8 +577,8 @@ async function createNewManifest() {
|
|||
}
|
||||
|
||||
async function decryptManifest(
|
||||
encryptedManifest: StorageManifestClass
|
||||
): Promise<ManifestRecordClass> {
|
||||
encryptedManifest: Proto.IStorageManifest
|
||||
): Promise<Proto.ManifestRecord> {
|
||||
const { version, value } = encryptedManifest;
|
||||
|
||||
const storageKeyBase64 = window.storage.get('storageKey');
|
||||
|
@ -576,20 +588,21 @@ async function decryptManifest(
|
|||
const storageKey = base64ToArrayBuffer(storageKeyBase64);
|
||||
const storageManifestKey = await deriveStorageManifestKey(
|
||||
storageKey,
|
||||
typeof version === 'number' ? version : version.toNumber()
|
||||
normalizeNumber(version ?? 0)
|
||||
);
|
||||
|
||||
strictAssert(value, 'StorageManifest has no value field');
|
||||
const decryptedManifest = await Crypto.decryptProfile(
|
||||
typeof value.toArrayBuffer === 'function' ? value.toArrayBuffer() : value,
|
||||
typedArrayToArrayBuffer(value),
|
||||
storageManifestKey
|
||||
);
|
||||
|
||||
return window.textsecure.protobuf.ManifestRecord.decode(decryptedManifest);
|
||||
return Proto.ManifestRecord.decode(new FIXMEU8(decryptedManifest));
|
||||
}
|
||||
|
||||
async function fetchManifest(
|
||||
manifestVersion: number
|
||||
): Promise<ManifestRecordClass | undefined> {
|
||||
): Promise<Proto.ManifestRecord | undefined> {
|
||||
window.log.info('storageService.fetchManifest');
|
||||
|
||||
if (!window.textsecure.messaging) {
|
||||
|
@ -606,8 +619,8 @@ async function fetchManifest(
|
|||
greaterThanVersion: manifestVersion,
|
||||
}
|
||||
);
|
||||
const encryptedManifest = window.textsecure.protobuf.StorageManifest.decode(
|
||||
manifestBinary
|
||||
const encryptedManifest = Proto.StorageManifest.decode(
|
||||
new FIXMEU8(manifestBinary)
|
||||
);
|
||||
|
||||
// if we don't get a value we're assuming that there's no newer manifest
|
||||
|
@ -645,7 +658,7 @@ async function fetchManifest(
|
|||
type MergeableItemType = {
|
||||
itemType: number;
|
||||
storageID: string;
|
||||
storageRecord: StorageRecordClass;
|
||||
storageRecord: Proto.IStorageRecord;
|
||||
};
|
||||
|
||||
type MergedRecordType = UnknownRecord & {
|
||||
|
@ -659,7 +672,7 @@ async function mergeRecord(
|
|||
): Promise<MergedRecordType> {
|
||||
const { itemType, storageID, storageRecord } = itemToMerge;
|
||||
|
||||
const ITEM_TYPE = window.textsecure.protobuf.ManifestRecord.Identifier.Type;
|
||||
const ITEM_TYPE = Proto.ManifestRecord.Identifier.Type;
|
||||
|
||||
let hasConflict = false;
|
||||
let isUnsupported = false;
|
||||
|
@ -709,18 +722,16 @@ async function mergeRecord(
|
|||
}
|
||||
|
||||
async function processManifest(
|
||||
manifest: ManifestRecordClass
|
||||
manifest: Proto.IManifestRecord
|
||||
): Promise<boolean> {
|
||||
if (!window.textsecure.messaging) {
|
||||
throw new Error('storageService.processManifest: We are offline!');
|
||||
}
|
||||
|
||||
const remoteKeysTypeMap = new Map();
|
||||
manifest.keys.forEach((identifier: ManifestRecordIdentifierClass) => {
|
||||
remoteKeysTypeMap.set(
|
||||
arrayBufferToBase64(identifier.raw.toArrayBuffer()),
|
||||
identifier.type
|
||||
);
|
||||
(manifest.keys || []).forEach(({ raw, type }: IManifestRecordIdentifier) => {
|
||||
strictAssert(raw, 'Identifier without raw field');
|
||||
remoteKeysTypeMap.set(Bytes.toBase64(raw), type);
|
||||
});
|
||||
|
||||
const remoteKeys = new Set(remoteKeysTypeMap.keys());
|
||||
|
@ -820,21 +831,21 @@ async function processRemoteRecords(
|
|||
remoteOnlyRecords.size
|
||||
);
|
||||
|
||||
const readOperation = new window.textsecure.protobuf.ReadOperation();
|
||||
const readOperation = new Proto.ReadOperation();
|
||||
readOperation.readKey = Array.from(remoteOnlyRecords.keys()).map(
|
||||
base64ToArrayBuffer
|
||||
Bytes.fromBase64
|
||||
);
|
||||
|
||||
const credentials = window.storage.get('storageCredentials');
|
||||
const storageItemsBuffer = await window.textsecure.messaging.getStorageRecords(
|
||||
readOperation.toArrayBuffer(),
|
||||
typedArrayToArrayBuffer(Proto.ReadOperation.encode(readOperation).finish()),
|
||||
{
|
||||
credentials,
|
||||
}
|
||||
);
|
||||
|
||||
const storageItems = window.textsecure.protobuf.StorageItems.decode(
|
||||
storageItemsBuffer
|
||||
const storageItems = Proto.StorageItems.decode(
|
||||
new FIXMEU8(storageItemsBuffer)
|
||||
);
|
||||
|
||||
if (!storageItems.items) {
|
||||
|
@ -847,7 +858,7 @@ async function processRemoteRecords(
|
|||
const decryptedStorageItems = await pMap(
|
||||
storageItems.items,
|
||||
async (
|
||||
storageRecordWrapper: StorageItemClass
|
||||
storageRecordWrapper: Proto.IStorageItem
|
||||
): Promise<MergeableItemType> => {
|
||||
const { key, value: storageItemCiphertext } = storageRecordWrapper;
|
||||
|
||||
|
@ -861,7 +872,7 @@ async function processRemoteRecords(
|
|||
);
|
||||
}
|
||||
|
||||
const base64ItemID = arrayBufferToBase64(key.toArrayBuffer());
|
||||
const base64ItemID = Bytes.toBase64(key);
|
||||
|
||||
const storageItemKey = await deriveStorageItemKey(
|
||||
storageKey,
|
||||
|
@ -871,7 +882,7 @@ async function processRemoteRecords(
|
|||
let storageItemPlaintext;
|
||||
try {
|
||||
storageItemPlaintext = await Crypto.decryptProfile(
|
||||
storageItemCiphertext.toArrayBuffer(),
|
||||
typedArrayToArrayBuffer(storageItemCiphertext),
|
||||
storageItemKey
|
||||
);
|
||||
} catch (err) {
|
||||
|
@ -882,8 +893,8 @@ async function processRemoteRecords(
|
|||
throw err;
|
||||
}
|
||||
|
||||
const storageRecord = window.textsecure.protobuf.StorageRecord.decode(
|
||||
storageItemPlaintext
|
||||
const storageRecord = Proto.StorageRecord.decode(
|
||||
new FIXMEU8(storageItemPlaintext)
|
||||
);
|
||||
|
||||
const remoteRecord = remoteOnlyRecords.get(base64ItemID);
|
||||
|
@ -906,7 +917,7 @@ async function processRemoteRecords(
|
|||
// Merge Account records last since it contains the pinned conversations
|
||||
// and we need all other records merged first before we can find the pinned
|
||||
// records in our db
|
||||
const ITEM_TYPE = window.textsecure.protobuf.ManifestRecord.Identifier.Type;
|
||||
const ITEM_TYPE = Proto.ManifestRecord.Identifier.Type;
|
||||
const sortedStorageItems = decryptedStorageItems.sort((_, b) =>
|
||||
b.itemType === ITEM_TYPE.ACCOUNT ? -1 : 1
|
||||
);
|
||||
|
@ -995,7 +1006,7 @@ async function processRemoteRecords(
|
|||
return 0;
|
||||
}
|
||||
|
||||
async function sync(): Promise<ManifestRecordClass | undefined> {
|
||||
async function sync(): Promise<Proto.ManifestRecord | undefined> {
|
||||
if (!isStorageWriteFeatureEnabled()) {
|
||||
window.log.info(
|
||||
'storageService.sync: Not starting desktop.storage is falsey'
|
||||
|
@ -1010,7 +1021,7 @@ async function sync(): Promise<ManifestRecordClass | undefined> {
|
|||
|
||||
window.log.info('storageService.sync: starting...');
|
||||
|
||||
let manifest: ManifestRecordClass | undefined;
|
||||
let manifest: Proto.ManifestRecord | undefined;
|
||||
try {
|
||||
// If we've previously interacted with strage service, update 'fetchComplete' record
|
||||
const previousFetchComplete = window.storage.get('storageFetchComplete');
|
||||
|
@ -1028,7 +1039,11 @@ async function sync(): Promise<ManifestRecordClass | undefined> {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const version = manifest.version.toNumber();
|
||||
strictAssert(
|
||||
manifest.version !== undefined && manifest.version !== null,
|
||||
'Manifest without version'
|
||||
);
|
||||
const version = normalizeNumber(manifest.version);
|
||||
|
||||
window.log.info(
|
||||
`storageService.sync: manifest versions - previous: ${localManifestVersion}, current: ${version}`
|
||||
|
@ -1095,7 +1110,7 @@ async function upload(fromSync = false): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
let previousManifest: ManifestRecordClass | undefined;
|
||||
let previousManifest: Proto.ManifestRecord | undefined;
|
||||
if (!fromSync) {
|
||||
// Syncing before we upload so that we repair any unknown records and
|
||||
// records with errors as well as ensure that we have the latest up to date
|
||||
|
|
|
@ -2,22 +2,11 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isEqual, isNumber } from 'lodash';
|
||||
import Long from 'long';
|
||||
|
||||
import {
|
||||
arrayBufferToBase64,
|
||||
base64ToArrayBuffer,
|
||||
deriveMasterKeyFromGroupV1,
|
||||
fromEncodedBinaryToArrayBuffer,
|
||||
} from '../Crypto';
|
||||
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
|
||||
import * as Bytes from '../Bytes';
|
||||
import dataInterface from '../sql/Client';
|
||||
import {
|
||||
AccountRecordClass,
|
||||
ContactRecordClass,
|
||||
GroupV1RecordClass,
|
||||
GroupV2RecordClass,
|
||||
PinnedConversationClass,
|
||||
} from '../textsecure.d';
|
||||
import {
|
||||
deriveGroupFields,
|
||||
waitThenMaybeUpdateGroup,
|
||||
|
@ -46,6 +35,7 @@ import {
|
|||
} from '../util/universalExpireTimer';
|
||||
import { ourProfileKeyService } from './ourProfileKey';
|
||||
import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
const { updateConversation } = dataInterface;
|
||||
|
||||
|
@ -53,14 +43,14 @@ const { updateConversation } = dataInterface;
|
|||
const FIXMEU8 = Uint8Array;
|
||||
|
||||
type RecordClass =
|
||||
| AccountRecordClass
|
||||
| ContactRecordClass
|
||||
| GroupV1RecordClass
|
||||
| GroupV2RecordClass;
|
||||
| Proto.IAccountRecord
|
||||
| Proto.IContactRecord
|
||||
| Proto.IGroupV1Record
|
||||
| Proto.IGroupV2Record;
|
||||
|
||||
function toRecordVerified(verified: number): number {
|
||||
function toRecordVerified(verified: number): Proto.ContactRecord.IdentityState {
|
||||
const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus;
|
||||
const STATE_ENUM = window.textsecure.protobuf.ContactRecord.IdentityState;
|
||||
const STATE_ENUM = Proto.ContactRecord.IdentityState;
|
||||
|
||||
switch (verified) {
|
||||
case VERIFIED_ENUM.VERIFIED:
|
||||
|
@ -82,7 +72,9 @@ function addUnknownFields(
|
|||
conversation.idForLogging()
|
||||
);
|
||||
conversation.set({
|
||||
storageUnknownFields: arrayBufferToBase64(record.__unknownFields),
|
||||
storageUnknownFields: Bytes.toBase64(
|
||||
Bytes.concatenate(record.__unknownFields)
|
||||
),
|
||||
});
|
||||
} else if (conversation.get('storageUnknownFields')) {
|
||||
// If the record doesn't have unknown fields attached but we have them
|
||||
|
@ -106,41 +98,43 @@ function applyUnknownFields(
|
|||
conversation.get('id')
|
||||
);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
record.__unknownFields = base64ToArrayBuffer(storageUnknownFields);
|
||||
record.__unknownFields = [Bytes.fromBase64(storageUnknownFields)];
|
||||
}
|
||||
}
|
||||
|
||||
export async function toContactRecord(
|
||||
conversation: ConversationModel
|
||||
): Promise<ContactRecordClass> {
|
||||
const contactRecord = new window.textsecure.protobuf.ContactRecord();
|
||||
if (conversation.get('uuid')) {
|
||||
contactRecord.serviceUuid = conversation.get('uuid');
|
||||
): Promise<Proto.ContactRecord> {
|
||||
const contactRecord = new Proto.ContactRecord();
|
||||
const uuid = conversation.get('uuid');
|
||||
if (uuid) {
|
||||
contactRecord.serviceUuid = uuid;
|
||||
}
|
||||
if (conversation.get('e164')) {
|
||||
contactRecord.serviceE164 = conversation.get('e164');
|
||||
const e164 = conversation.get('e164');
|
||||
if (e164) {
|
||||
contactRecord.serviceE164 = e164;
|
||||
}
|
||||
if (conversation.get('profileKey')) {
|
||||
contactRecord.profileKey = base64ToArrayBuffer(
|
||||
String(conversation.get('profileKey'))
|
||||
);
|
||||
const profileKey = conversation.get('profileKey');
|
||||
if (profileKey) {
|
||||
contactRecord.profileKey = Bytes.fromBase64(String(profileKey));
|
||||
}
|
||||
const identityKey = await window.textsecure.storage.protocol.loadIdentityKey(
|
||||
conversation.id
|
||||
);
|
||||
if (identityKey) {
|
||||
contactRecord.identityKey = identityKey;
|
||||
contactRecord.identityKey = new FIXMEU8(identityKey);
|
||||
}
|
||||
if (conversation.get('verified')) {
|
||||
contactRecord.identityState = toRecordVerified(
|
||||
Number(conversation.get('verified'))
|
||||
);
|
||||
const verified = conversation.get('verified');
|
||||
if (verified) {
|
||||
contactRecord.identityState = toRecordVerified(Number(verified));
|
||||
}
|
||||
if (conversation.get('profileName')) {
|
||||
contactRecord.givenName = conversation.get('profileName');
|
||||
const profileName = conversation.get('profileName');
|
||||
if (profileName) {
|
||||
contactRecord.givenName = profileName;
|
||||
}
|
||||
if (conversation.get('profileFamilyName')) {
|
||||
contactRecord.familyName = conversation.get('profileFamilyName');
|
||||
const profileFamilyName = conversation.get('profileFamilyName');
|
||||
if (profileFamilyName) {
|
||||
contactRecord.familyName = profileFamilyName;
|
||||
}
|
||||
contactRecord.blocked = conversation.isBlocked();
|
||||
contactRecord.whitelisted = Boolean(conversation.get('profileSharing'));
|
||||
|
@ -157,11 +151,11 @@ export async function toContactRecord(
|
|||
|
||||
export async function toAccountRecord(
|
||||
conversation: ConversationModel
|
||||
): Promise<AccountRecordClass> {
|
||||
const accountRecord = new window.textsecure.protobuf.AccountRecord();
|
||||
): Promise<Proto.AccountRecord> {
|
||||
const accountRecord = new Proto.AccountRecord();
|
||||
|
||||
if (conversation.get('profileKey')) {
|
||||
accountRecord.profileKey = base64ToArrayBuffer(
|
||||
accountRecord.profileKey = Bytes.fromBase64(
|
||||
String(conversation.get('profileKey'))
|
||||
);
|
||||
}
|
||||
|
@ -198,7 +192,7 @@ export async function toAccountRecord(
|
|||
}
|
||||
|
||||
const PHONE_NUMBER_SHARING_MODE_ENUM =
|
||||
window.textsecure.protobuf.AccountRecord.PhoneNumberSharingMode;
|
||||
Proto.AccountRecord.PhoneNumberSharingMode;
|
||||
const phoneNumberSharingMode = parsePhoneNumberSharingMode(
|
||||
window.storage.get('phoneNumberSharingMode')
|
||||
);
|
||||
|
@ -239,7 +233,7 @@ export async function toAccountRecord(
|
|||
const pinnedConversation = window.ConversationController.get(id);
|
||||
|
||||
if (pinnedConversation) {
|
||||
const pinnedConversationRecord = new window.textsecure.protobuf.AccountRecord.PinnedConversation();
|
||||
const pinnedConversationRecord = new Proto.AccountRecord.PinnedConversation();
|
||||
|
||||
if (pinnedConversation.get('type') === 'private') {
|
||||
pinnedConversationRecord.identifier = 'contact';
|
||||
|
@ -255,9 +249,7 @@ export async function toAccountRecord(
|
|||
'toAccountRecord: trying to pin a v1 Group without groupId'
|
||||
);
|
||||
}
|
||||
pinnedConversationRecord.legacyGroupId = fromEncodedBinaryToArrayBuffer(
|
||||
groupId
|
||||
);
|
||||
pinnedConversationRecord.legacyGroupId = Bytes.fromBinary(groupId);
|
||||
} else if (isGroupV2(pinnedConversation.attributes)) {
|
||||
pinnedConversationRecord.identifier = 'groupMasterKey';
|
||||
const masterKey = pinnedConversation.get('masterKey');
|
||||
|
@ -266,9 +258,7 @@ export async function toAccountRecord(
|
|||
'toAccountRecord: trying to pin a v2 Group without masterKey'
|
||||
);
|
||||
}
|
||||
pinnedConversationRecord.groupMasterKey = base64ToArrayBuffer(
|
||||
masterKey
|
||||
);
|
||||
pinnedConversationRecord.groupMasterKey = Bytes.fromBase64(masterKey);
|
||||
}
|
||||
|
||||
return pinnedConversationRecord;
|
||||
|
@ -279,7 +269,7 @@ export async function toAccountRecord(
|
|||
.filter(
|
||||
(
|
||||
pinnedConversationClass
|
||||
): pinnedConversationClass is PinnedConversationClass =>
|
||||
): pinnedConversationClass is Proto.AccountRecord.PinnedConversation =>
|
||||
pinnedConversationClass !== undefined
|
||||
);
|
||||
|
||||
|
@ -296,12 +286,10 @@ export async function toAccountRecord(
|
|||
|
||||
export async function toGroupV1Record(
|
||||
conversation: ConversationModel
|
||||
): Promise<GroupV1RecordClass> {
|
||||
const groupV1Record = new window.textsecure.protobuf.GroupV1Record();
|
||||
): Promise<Proto.GroupV1Record> {
|
||||
const groupV1Record = new Proto.GroupV1Record();
|
||||
|
||||
groupV1Record.id = fromEncodedBinaryToArrayBuffer(
|
||||
String(conversation.get('groupId'))
|
||||
);
|
||||
groupV1Record.id = Bytes.fromBinary(String(conversation.get('groupId')));
|
||||
groupV1Record.blocked = conversation.isBlocked();
|
||||
groupV1Record.whitelisted = Boolean(conversation.get('profileSharing'));
|
||||
groupV1Record.archived = Boolean(conversation.get('isArchived'));
|
||||
|
@ -317,12 +305,12 @@ export async function toGroupV1Record(
|
|||
|
||||
export async function toGroupV2Record(
|
||||
conversation: ConversationModel
|
||||
): Promise<GroupV2RecordClass> {
|
||||
const groupV2Record = new window.textsecure.protobuf.GroupV2Record();
|
||||
): Promise<Proto.GroupV2Record> {
|
||||
const groupV2Record = new Proto.GroupV2Record();
|
||||
|
||||
const masterKey = conversation.get('masterKey');
|
||||
if (masterKey !== undefined) {
|
||||
groupV2Record.masterKey = base64ToArrayBuffer(masterKey);
|
||||
groupV2Record.masterKey = Bytes.fromBase64(masterKey);
|
||||
}
|
||||
groupV2Record.blocked = conversation.isBlocked();
|
||||
groupV2Record.whitelisted = Boolean(conversation.get('profileSharing'));
|
||||
|
@ -337,14 +325,13 @@ export async function toGroupV2Record(
|
|||
return groupV2Record;
|
||||
}
|
||||
|
||||
type MessageRequestCapableRecord = ContactRecordClass | GroupV1RecordClass;
|
||||
type MessageRequestCapableRecord = Proto.IContactRecord | Proto.IGroupV1Record;
|
||||
|
||||
function applyMessageRequestState(
|
||||
record: MessageRequestCapableRecord,
|
||||
conversation: ConversationModel
|
||||
): void {
|
||||
const messageRequestEnum =
|
||||
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
|
||||
if (record.blocked) {
|
||||
conversation.applyMessageRequestResponse(messageRequestEnum.BLOCK, {
|
||||
|
@ -394,15 +381,14 @@ function doRecordsConflict(
|
|||
return true;
|
||||
}
|
||||
|
||||
return localKeys.reduce((hasConflict: boolean, key: string): boolean => {
|
||||
return localKeys.some((key: string): boolean => {
|
||||
const localValue = localRecord[key];
|
||||
const remoteValue = remoteRecord[key];
|
||||
|
||||
// Sometimes we have a ByteBuffer and an ArrayBuffer, this ensures that we
|
||||
// are comparing them both equally by converting them into base64 string.
|
||||
if (Object.prototype.toString.call(localValue) === '[object ArrayBuffer]') {
|
||||
const areEqual =
|
||||
arrayBufferToBase64(localValue) === arrayBufferToBase64(remoteValue);
|
||||
if (localValue instanceof Uint8Array) {
|
||||
const areEqual = Bytes.areEqual(localValue, remoteValue);
|
||||
if (!areEqual) {
|
||||
window.log.info(
|
||||
'storageService.doRecordsConflict: Conflict found for ArrayBuffer',
|
||||
|
@ -410,15 +396,18 @@ function doRecordsConflict(
|
|||
idForLogging
|
||||
);
|
||||
}
|
||||
return hasConflict || !areEqual;
|
||||
return !areEqual;
|
||||
}
|
||||
|
||||
// If both types are Long we can use Long's equals to compare them
|
||||
if (
|
||||
window.dcodeIO.Long.isLong(localValue) &&
|
||||
window.dcodeIO.Long.isLong(remoteValue)
|
||||
) {
|
||||
const areEqual = localValue.equals(remoteValue);
|
||||
if (localValue instanceof Long || typeof localValue === 'number') {
|
||||
if (!(remoteValue instanceof Long) || typeof remoteValue !== 'number') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const areEqual = Long.fromValue(localValue).equals(
|
||||
Long.fromValue(remoteValue)
|
||||
);
|
||||
if (!areEqual) {
|
||||
window.log.info(
|
||||
'storageService.doRecordsConflict: Conflict found for Long',
|
||||
|
@ -426,7 +415,7 @@ function doRecordsConflict(
|
|||
idForLogging
|
||||
);
|
||||
}
|
||||
return hasConflict || !areEqual;
|
||||
return !areEqual;
|
||||
}
|
||||
|
||||
if (key === 'pinnedConversations') {
|
||||
|
@ -437,11 +426,11 @@ function doRecordsConflict(
|
|||
idForLogging
|
||||
);
|
||||
}
|
||||
return hasConflict || !areEqual;
|
||||
return !areEqual;
|
||||
}
|
||||
|
||||
if (localValue === remoteValue) {
|
||||
return hasConflict || false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sometimes we get `null` values from Protobuf and they should default to
|
||||
|
@ -452,9 +441,9 @@ function doRecordsConflict(
|
|||
(localValue === false ||
|
||||
localValue === '' ||
|
||||
localValue === 0 ||
|
||||
(window.dcodeIO.Long.isLong(localValue) && localValue.toNumber() === 0))
|
||||
(Long.isLong(localValue) && localValue.toNumber() === 0))
|
||||
) {
|
||||
return hasConflict || false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const areEqual = isEqual(localValue, remoteValue);
|
||||
|
@ -468,7 +457,7 @@ function doRecordsConflict(
|
|||
}
|
||||
|
||||
return !areEqual;
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
|
||||
function doesRecordHavePendingChanges(
|
||||
|
@ -497,13 +486,13 @@ function doesRecordHavePendingChanges(
|
|||
|
||||
export async function mergeGroupV1Record(
|
||||
storageID: string,
|
||||
groupV1Record: GroupV1RecordClass
|
||||
groupV1Record: Proto.IGroupV1Record
|
||||
): Promise<boolean> {
|
||||
if (!groupV1Record.id) {
|
||||
throw new Error(`No ID for ${storageID}`);
|
||||
}
|
||||
|
||||
const groupId = groupV1Record.id.toBinary();
|
||||
const groupId = Bytes.toBinary(groupV1Record.id);
|
||||
|
||||
// Attempt to fetch an existing group pertaining to the `groupId` or create
|
||||
// a new group and populate it with the attributes from the record.
|
||||
|
@ -524,7 +513,9 @@ export async function mergeGroupV1Record(
|
|||
// It's possible this group was migrated to a GV2 if so we attempt to
|
||||
// retrieve the master key and find the conversation locally. If we
|
||||
// are successful then we continue setting and applying state.
|
||||
const masterKeyBuffer = await deriveMasterKeyFromGroupV1(groupId);
|
||||
const masterKeyBuffer = await deriveMasterKeyFromGroupV1(
|
||||
typedArrayToArrayBuffer(groupV1Record.id)
|
||||
);
|
||||
const fields = deriveGroupFields(new FIXMEU8(masterKeyBuffer));
|
||||
const derivedGroupV2Id = Bytes.toBase64(fields.id);
|
||||
|
||||
|
@ -599,12 +590,12 @@ export async function mergeGroupV1Record(
|
|||
}
|
||||
|
||||
async function getGroupV2Conversation(
|
||||
masterKeyBuffer: ArrayBuffer
|
||||
masterKeyBuffer: Uint8Array
|
||||
): Promise<ConversationModel> {
|
||||
const groupFields = deriveGroupFields(new FIXMEU8(masterKeyBuffer));
|
||||
const groupFields = deriveGroupFields(masterKeyBuffer);
|
||||
|
||||
const groupId = Bytes.toBase64(groupFields.id);
|
||||
const masterKey = arrayBufferToBase64(masterKeyBuffer);
|
||||
const masterKey = Bytes.toBase64(masterKeyBuffer);
|
||||
const secretParams = Bytes.toBase64(groupFields.secretParams);
|
||||
const publicParams = Bytes.toBase64(groupFields.publicParams);
|
||||
|
||||
|
@ -647,13 +638,13 @@ async function getGroupV2Conversation(
|
|||
|
||||
export async function mergeGroupV2Record(
|
||||
storageID: string,
|
||||
groupV2Record: GroupV2RecordClass
|
||||
groupV2Record: Proto.IGroupV2Record
|
||||
): Promise<boolean> {
|
||||
if (!groupV2Record.masterKey) {
|
||||
throw new Error(`No master key for ${storageID}`);
|
||||
}
|
||||
|
||||
const masterKeyBuffer = groupV2Record.masterKey.toArrayBuffer();
|
||||
const masterKeyBuffer = groupV2Record.masterKey;
|
||||
const conversation = await getGroupV2Conversation(masterKeyBuffer);
|
||||
|
||||
window.log.info(
|
||||
|
@ -720,7 +711,7 @@ export async function mergeGroupV2Record(
|
|||
|
||||
export async function mergeContactRecord(
|
||||
storageID: string,
|
||||
originalContactRecord: ContactRecordClass
|
||||
originalContactRecord: Proto.IContactRecord
|
||||
): Promise<boolean> {
|
||||
const contactRecord = {
|
||||
...originalContactRecord,
|
||||
|
@ -757,10 +748,9 @@ export async function mergeContactRecord(
|
|||
);
|
||||
|
||||
if (contactRecord.profileKey) {
|
||||
await conversation.setProfileKey(
|
||||
arrayBufferToBase64(contactRecord.profileKey.toArrayBuffer()),
|
||||
{ viaStorageServiceSync: true }
|
||||
);
|
||||
await conversation.setProfileKey(Bytes.toBase64(contactRecord.profileKey), {
|
||||
viaStorageServiceSync: true,
|
||||
});
|
||||
}
|
||||
|
||||
const verified = await conversation.safeGetVerified();
|
||||
|
@ -768,11 +758,11 @@ export async function mergeContactRecord(
|
|||
if (verified !== storageServiceVerified) {
|
||||
const verifiedOptions = {
|
||||
key: contactRecord.identityKey
|
||||
? contactRecord.identityKey.toArrayBuffer()
|
||||
? typedArrayToArrayBuffer(contactRecord.identityKey)
|
||||
: undefined,
|
||||
viaStorageServiceSync: true,
|
||||
};
|
||||
const STATE_ENUM = window.textsecure.protobuf.ContactRecord.IdentityState;
|
||||
const STATE_ENUM = Proto.ContactRecord.IdentityState;
|
||||
|
||||
switch (storageServiceVerified) {
|
||||
case STATE_ENUM.VERIFIED:
|
||||
|
@ -816,7 +806,7 @@ export async function mergeContactRecord(
|
|||
|
||||
export async function mergeAccountRecord(
|
||||
storageID: string,
|
||||
accountRecord: AccountRecordClass
|
||||
accountRecord: Proto.IAccountRecord
|
||||
): Promise<boolean> {
|
||||
const {
|
||||
avatarUrl,
|
||||
|
@ -855,7 +845,7 @@ export async function mergeAccountRecord(
|
|||
setUniversalExpireTimer(universalExpireTimer || 0);
|
||||
|
||||
const PHONE_NUMBER_SHARING_MODE_ENUM =
|
||||
window.textsecure.protobuf.AccountRecord.PhoneNumberSharingMode;
|
||||
Proto.AccountRecord.PhoneNumberSharingMode;
|
||||
let phoneNumberSharingModeToStore: PhoneNumberSharingMode;
|
||||
switch (phoneNumberSharingMode) {
|
||||
case undefined:
|
||||
|
@ -885,7 +875,7 @@ export async function mergeAccountRecord(
|
|||
window.storage.put('phoneNumberDiscoverability', discoverability);
|
||||
|
||||
if (profileKey) {
|
||||
ourProfileKeyService.set(profileKey.toArrayBuffer());
|
||||
ourProfileKeyService.set(typedArrayToArrayBuffer(profileKey));
|
||||
}
|
||||
|
||||
if (pinnedConversations) {
|
||||
|
@ -928,48 +918,29 @@ export async function mergeAccountRecord(
|
|||
);
|
||||
|
||||
const remotelyPinnedConversationPromises = pinnedConversations.map(
|
||||
async pinnedConversation => {
|
||||
let conversationId;
|
||||
async ({ contact, legacyGroupId, groupMasterKey }) => {
|
||||
let conversationId: string | undefined;
|
||||
|
||||
switch (pinnedConversation.identifier) {
|
||||
case 'contact': {
|
||||
if (!pinnedConversation.contact) {
|
||||
throw new Error('mergeAccountRecord: no contact found');
|
||||
}
|
||||
if (contact) {
|
||||
conversationId = window.ConversationController.ensureContactIds(
|
||||
pinnedConversation.contact
|
||||
contact
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'legacyGroupId': {
|
||||
if (!pinnedConversation.legacyGroupId) {
|
||||
throw new Error('mergeAccountRecord: no legacyGroupId found');
|
||||
}
|
||||
conversationId = pinnedConversation.legacyGroupId.toBinary();
|
||||
break;
|
||||
}
|
||||
case 'groupMasterKey': {
|
||||
if (!pinnedConversation.groupMasterKey) {
|
||||
throw new Error('mergeAccountRecord: no groupMasterKey found');
|
||||
}
|
||||
const masterKeyBuffer = pinnedConversation.groupMasterKey.toArrayBuffer();
|
||||
const groupFields = deriveGroupFields(masterKeyBuffer);
|
||||
} else if (legacyGroupId && legacyGroupId.length) {
|
||||
conversationId = Bytes.toBinary(legacyGroupId);
|
||||
} else if (groupMasterKey && groupMasterKey.length) {
|
||||
const groupFields = deriveGroupFields(groupMasterKey);
|
||||
const groupId = Bytes.toBase64(groupFields.id);
|
||||
|
||||
conversationId = groupId;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
} else {
|
||||
window.log.error(
|
||||
'storageService.mergeAccountRecord: Invalid identifier received'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!conversationId) {
|
||||
window.log.error(
|
||||
'storageService.mergeAccountRecord: missing conversation id. looking based on',
|
||||
pinnedConversation.identifier
|
||||
'storageService.mergeAccountRecord: missing conversation id.'
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
@ -1036,9 +1007,7 @@ export async function mergeAccountRecord(
|
|||
});
|
||||
|
||||
if (accountRecord.profileKey) {
|
||||
await conversation.setProfileKey(
|
||||
arrayBufferToBase64(accountRecord.profileKey.toArrayBuffer())
|
||||
);
|
||||
await conversation.setProfileKey(Bytes.toBase64(accountRecord.profileKey));
|
||||
}
|
||||
|
||||
if (avatarUrl) {
|
||||
|
|
|
@ -3,38 +3,32 @@
|
|||
|
||||
import { assert } from 'chai';
|
||||
import { arePinnedConversationsEqual } from '../../util/arePinnedConversationsEqual';
|
||||
import { PinnedConversationClass } from '../../textsecure.d';
|
||||
import { SignalService as Proto } from '../../protobuf';
|
||||
|
||||
import PinnedConversation = Proto.AccountRecord.IPinnedConversation;
|
||||
|
||||
describe('arePinnedConversationsEqual', () => {
|
||||
it('is equal if both have same values at same indices', () => {
|
||||
const localValue = [
|
||||
{
|
||||
identifier: 'contact' as const,
|
||||
contact: {
|
||||
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
||||
e164: '+13055551234',
|
||||
},
|
||||
toArrayBuffer: () => new ArrayBuffer(0),
|
||||
},
|
||||
{
|
||||
identifier: 'groupMasterKey' as const,
|
||||
groupMasterKey: new ArrayBuffer(32),
|
||||
toArrayBuffer: () => new ArrayBuffer(0),
|
||||
groupMasterKey: new Uint8Array(32),
|
||||
},
|
||||
];
|
||||
const remoteValue = [
|
||||
{
|
||||
identifier: 'contact' as const,
|
||||
contact: {
|
||||
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
||||
e164: '+13055551234',
|
||||
},
|
||||
toArrayBuffer: () => new ArrayBuffer(0),
|
||||
},
|
||||
{
|
||||
identifier: 'groupMasterKey' as const,
|
||||
groupMasterKey: new ArrayBuffer(32),
|
||||
toArrayBuffer: () => new ArrayBuffer(0),
|
||||
groupMasterKey: new Uint8Array(32),
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -44,38 +38,30 @@ describe('arePinnedConversationsEqual', () => {
|
|||
it('is not equal if values are mixed', () => {
|
||||
const localValue = [
|
||||
{
|
||||
identifier: 'contact' as const,
|
||||
contact: {
|
||||
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
||||
e164: '+13055551234',
|
||||
},
|
||||
toArrayBuffer: () => new ArrayBuffer(0),
|
||||
},
|
||||
{
|
||||
identifier: 'contact' as const,
|
||||
contact: {
|
||||
uuid: 'f59a9fed-9e91-4bb4-a015-d49e58b47e25',
|
||||
e164: '+17865554321',
|
||||
},
|
||||
toArrayBuffer: () => new ArrayBuffer(0),
|
||||
},
|
||||
];
|
||||
const remoteValue = [
|
||||
{
|
||||
identifier: 'contact' as const,
|
||||
contact: {
|
||||
uuid: 'f59a9fed-9e91-4bb4-a015-d49e58b47e25',
|
||||
e164: '+17865554321',
|
||||
},
|
||||
toArrayBuffer: () => new ArrayBuffer(0),
|
||||
},
|
||||
{
|
||||
identifier: 'contact' as const,
|
||||
contact: {
|
||||
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
||||
e164: '+13055551234',
|
||||
},
|
||||
toArrayBuffer: () => new ArrayBuffer(0),
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -85,34 +71,28 @@ describe('arePinnedConversationsEqual', () => {
|
|||
it('is not equal if lengths are not same', () => {
|
||||
const localValue = [
|
||||
{
|
||||
identifier: 'contact' as const,
|
||||
contact: {
|
||||
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
||||
e164: '+13055551234',
|
||||
},
|
||||
toArrayBuffer: () => new ArrayBuffer(0),
|
||||
},
|
||||
];
|
||||
const remoteValue: Array<PinnedConversationClass> = [];
|
||||
const remoteValue: Array<PinnedConversation> = [];
|
||||
assert.isFalse(arePinnedConversationsEqual(localValue, remoteValue));
|
||||
});
|
||||
|
||||
it('is not equal if content does not match', () => {
|
||||
const localValue = [
|
||||
{
|
||||
identifier: 'contact' as const,
|
||||
contact: {
|
||||
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
||||
e164: '+13055551234',
|
||||
},
|
||||
toArrayBuffer: () => new ArrayBuffer(0),
|
||||
},
|
||||
];
|
||||
const remoteValue = [
|
||||
{
|
||||
identifier: 'groupMasterKey' as const,
|
||||
groupMasterKey: new ArrayBuffer(32),
|
||||
toArrayBuffer: () => new ArrayBuffer(0),
|
||||
groupMasterKey: new Uint8Array(32),
|
||||
},
|
||||
];
|
||||
assert.isFalse(arePinnedConversationsEqual(localValue, remoteValue));
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import Long from 'long';
|
||||
|
||||
import {
|
||||
getSafeLongFromTimestamp,
|
||||
|
@ -9,8 +10,6 @@ import {
|
|||
} from '../../util/timestampLongUtils';
|
||||
|
||||
describe('getSafeLongFromTimestamp', () => {
|
||||
const { Long } = window.dcodeIO;
|
||||
|
||||
it('returns zero when passed undefined', () => {
|
||||
assert(getSafeLongFromTimestamp(undefined).isZero());
|
||||
});
|
||||
|
@ -31,8 +30,6 @@ describe('getSafeLongFromTimestamp', () => {
|
|||
});
|
||||
|
||||
describe('getTimestampFromLong', () => {
|
||||
const { Long } = window.dcodeIO;
|
||||
|
||||
it('returns zero when passed 0 Long', () => {
|
||||
assert.equal(getTimestampFromLong(Long.fromNumber(0)), 0);
|
||||
});
|
||||
|
|
564
ts/test-electron/Crypto_test.ts
Normal file
564
ts/test-electron/Crypto_test.ts
Normal file
|
@ -0,0 +1,564 @@
|
|||
// Copyright 2015-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import * as Curve from '../Curve';
|
||||
import * as Crypto from '../Crypto';
|
||||
import TSCrypto from '../textsecure/Crypto';
|
||||
|
||||
describe('Crypto', () => {
|
||||
describe('encrypting and decrypting profile data', () => {
|
||||
const NAME_PADDED_LENGTH = 53;
|
||||
describe('encrypting and decrypting profile names', () => {
|
||||
it('pads, encrypts, decrypts, and unpads a short string', async () => {
|
||||
const name = 'Alice';
|
||||
const buffer = Crypto.bytesFromString(name);
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await TSCrypto.encryptProfileName(buffer, key);
|
||||
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
|
||||
|
||||
const { given, family } = await TSCrypto.decryptProfileName(
|
||||
Crypto.arrayBufferToBase64(encrypted),
|
||||
key
|
||||
);
|
||||
assert.strictEqual(family, null);
|
||||
assert.strictEqual(Crypto.stringFromBytes(given), name);
|
||||
});
|
||||
|
||||
it('handles a given name of the max, 53 characters', async () => {
|
||||
const name = '01234567890123456789012345678901234567890123456789123';
|
||||
const buffer = Crypto.bytesFromString(name);
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await TSCrypto.encryptProfileName(buffer, key);
|
||||
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
|
||||
const { given, family } = await TSCrypto.decryptProfileName(
|
||||
Crypto.arrayBufferToBase64(encrypted),
|
||||
key
|
||||
);
|
||||
|
||||
assert.strictEqual(Crypto.stringFromBytes(given), name);
|
||||
assert.strictEqual(family, null);
|
||||
});
|
||||
|
||||
it('handles family/given name of the max, 53 characters', async () => {
|
||||
const name =
|
||||
'01234567890123456789\u000001234567890123456789012345678912';
|
||||
const buffer = Crypto.bytesFromString(name);
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await TSCrypto.encryptProfileName(buffer, key);
|
||||
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
|
||||
const { given, family } = await TSCrypto.decryptProfileName(
|
||||
Crypto.arrayBufferToBase64(encrypted),
|
||||
key
|
||||
);
|
||||
assert.strictEqual(
|
||||
Crypto.stringFromBytes(given),
|
||||
'01234567890123456789'
|
||||
);
|
||||
assert.strictEqual(
|
||||
family && Crypto.stringFromBytes(family),
|
||||
'01234567890123456789012345678912'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles a string with family/given name', async () => {
|
||||
const name = 'Alice\0Jones';
|
||||
const buffer = Crypto.bytesFromString(name);
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await TSCrypto.encryptProfileName(buffer, key);
|
||||
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
|
||||
const { given, family } = await TSCrypto.decryptProfileName(
|
||||
Crypto.arrayBufferToBase64(encrypted),
|
||||
key
|
||||
);
|
||||
assert.strictEqual(Crypto.stringFromBytes(given), 'Alice');
|
||||
assert.strictEqual(family && Crypto.stringFromBytes(family), 'Jones');
|
||||
});
|
||||
|
||||
it('works for empty string', async () => {
|
||||
const name = Crypto.bytesFromString('');
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await TSCrypto.encryptProfileName(name, key);
|
||||
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
|
||||
|
||||
const { given, family } = await TSCrypto.decryptProfileName(
|
||||
Crypto.arrayBufferToBase64(encrypted),
|
||||
key
|
||||
);
|
||||
assert.strictEqual(family, null);
|
||||
assert.strictEqual(given.byteLength, 0);
|
||||
assert.strictEqual(Crypto.stringFromBytes(given), '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('encrypting and decrypting profile avatars', () => {
|
||||
it('encrypts and decrypts', async () => {
|
||||
const buffer = Crypto.bytesFromString('This is an avatar');
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await TSCrypto.encryptProfile(buffer, key);
|
||||
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
|
||||
|
||||
const decrypted = await TSCrypto.decryptProfile(encrypted, key);
|
||||
assert(Crypto.constantTimeEqual(buffer, decrypted));
|
||||
});
|
||||
|
||||
it('throws when decrypting with the wrong key', async () => {
|
||||
const buffer = Crypto.bytesFromString('This is an avatar');
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
const badKey = Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await TSCrypto.encryptProfile(buffer, key);
|
||||
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
|
||||
await assert.isRejected(
|
||||
TSCrypto.decryptProfile(encrypted, badKey),
|
||||
'Failed to decrypt profile data. Most likely the profile key has changed.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateRegistrationId', () => {
|
||||
it('generates an integer between 0 and 16383 (inclusive)', () => {
|
||||
for (let i = 0; i < 100; i += 1) {
|
||||
const id = Crypto.generateRegistrationId();
|
||||
assert.isAtLeast(id, 0);
|
||||
assert.isAtMost(id, 16383);
|
||||
assert(Number.isInteger(id));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveSecrets', () => {
|
||||
it('derives key parts via HKDF', () => {
|
||||
const input = Crypto.getRandomBytes(32);
|
||||
const salt = Crypto.getRandomBytes(32);
|
||||
const info = Crypto.bytesFromString('Hello world');
|
||||
const result = Crypto.deriveSecrets(input, salt, info);
|
||||
assert.lengthOf(result, 3);
|
||||
result.forEach(part => {
|
||||
// This is a smoke test; HKDF is tested as part of @signalapp/signal-client.
|
||||
assert.instanceOf(part, ArrayBuffer);
|
||||
assert.strictEqual(part.byteLength, 32);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('accessKey/profileKey', () => {
|
||||
it('verification roundtrips', async () => {
|
||||
const profileKey = await Crypto.getRandomBytes(32);
|
||||
const accessKey = await Crypto.deriveAccessKey(profileKey);
|
||||
|
||||
const verifier = await Crypto.getAccessKeyVerifier(accessKey);
|
||||
|
||||
const correct = await Crypto.verifyAccessKey(accessKey, verifier);
|
||||
|
||||
assert.strictEqual(correct, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveMasterKeyFromGroupV1', () => {
|
||||
const vectors = [
|
||||
{
|
||||
gv1: '00000000000000000000000000000000',
|
||||
masterKey:
|
||||
'dbde68f4ee9169081f8814eabc65523fea1359235c8cfca32b69e31dce58b039',
|
||||
},
|
||||
{
|
||||
gv1: '000102030405060708090a0b0c0d0e0f',
|
||||
masterKey:
|
||||
'70884f78f07a94480ee36b67a4b5e975e92e4a774561e3df84c9076e3be4b9bf',
|
||||
},
|
||||
{
|
||||
gv1: '7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f',
|
||||
masterKey:
|
||||
'e69bf7c183b288b4ea5745b7c52b651a61e57769fafde683a6fdf1240f1905f2',
|
||||
},
|
||||
{
|
||||
gv1: 'ffffffffffffffffffffffffffffffff',
|
||||
masterKey:
|
||||
'dd3a7de23d10f18b64457fbeedc76226c112a730e4b76112e62c36c4432eb37d',
|
||||
},
|
||||
];
|
||||
|
||||
vectors.forEach((vector, index) => {
|
||||
it(`vector ${index}`, async () => {
|
||||
const gv1 = Crypto.hexToArrayBuffer(vector.gv1);
|
||||
const expectedHex = vector.masterKey;
|
||||
|
||||
const actual = await Crypto.deriveMasterKeyFromGroupV1(gv1);
|
||||
const actualHex = Crypto.arrayBufferToHex(actual);
|
||||
|
||||
assert.strictEqual(actualHex, expectedHex);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('symmetric encryption', () => {
|
||||
it('roundtrips', async () => {
|
||||
const message = 'this is my message';
|
||||
const plaintext = Crypto.bytesFromString(message);
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await Crypto.encryptSymmetric(key, plaintext);
|
||||
const decrypted = await Crypto.decryptSymmetric(key, encrypted);
|
||||
|
||||
const equal = Crypto.constantTimeEqual(plaintext, decrypted);
|
||||
if (!equal) {
|
||||
throw new Error('The output and input did not match!');
|
||||
}
|
||||
});
|
||||
|
||||
it('roundtrip fails if nonce is modified', async () => {
|
||||
const message = 'this is my message';
|
||||
const plaintext = Crypto.bytesFromString(message);
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await Crypto.encryptSymmetric(key, plaintext);
|
||||
const uintArray = new Uint8Array(encrypted);
|
||||
uintArray[2] += 2;
|
||||
|
||||
try {
|
||||
await Crypto.decryptSymmetric(
|
||||
key,
|
||||
Crypto.typedArrayToArrayBuffer(uintArray)
|
||||
);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected error to be thrown');
|
||||
});
|
||||
|
||||
it('roundtrip fails if mac is modified', async () => {
|
||||
const message = 'this is my message';
|
||||
const plaintext = Crypto.bytesFromString(message);
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await Crypto.encryptSymmetric(key, plaintext);
|
||||
const uintArray = new Uint8Array(encrypted);
|
||||
uintArray[uintArray.length - 3] += 2;
|
||||
|
||||
try {
|
||||
await Crypto.decryptSymmetric(
|
||||
key,
|
||||
Crypto.typedArrayToArrayBuffer(uintArray)
|
||||
);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected error to be thrown');
|
||||
});
|
||||
|
||||
it('roundtrip fails if encrypted contents are modified', async () => {
|
||||
const message = 'this is my message';
|
||||
const plaintext = Crypto.bytesFromString(message);
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await Crypto.encryptSymmetric(key, plaintext);
|
||||
const uintArray = new Uint8Array(encrypted);
|
||||
uintArray[35] += 9;
|
||||
|
||||
try {
|
||||
await Crypto.decryptSymmetric(
|
||||
key,
|
||||
Crypto.typedArrayToArrayBuffer(uintArray)
|
||||
);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected error to be thrown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('encrypted device name', () => {
|
||||
it('roundtrips', async () => {
|
||||
const deviceName = 'v1.19.0 on Windows 10';
|
||||
const identityKey = Curve.generateKeyPair();
|
||||
|
||||
const encrypted = await Crypto.encryptDeviceName(
|
||||
deviceName,
|
||||
identityKey.pubKey
|
||||
);
|
||||
const decrypted = await Crypto.decryptDeviceName(
|
||||
encrypted,
|
||||
identityKey.privKey
|
||||
);
|
||||
|
||||
assert.strictEqual(decrypted, deviceName);
|
||||
});
|
||||
|
||||
it('fails if iv is changed', async () => {
|
||||
const deviceName = 'v1.19.0 on Windows 10';
|
||||
const identityKey = Curve.generateKeyPair();
|
||||
|
||||
const encrypted = await Crypto.encryptDeviceName(
|
||||
deviceName,
|
||||
identityKey.pubKey
|
||||
);
|
||||
encrypted.syntheticIv = Crypto.getRandomBytes(16);
|
||||
try {
|
||||
await Crypto.decryptDeviceName(encrypted, identityKey.privKey);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'decryptDeviceName: synthetic IV did not match'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('attachment encryption', () => {
|
||||
it('roundtrips', async () => {
|
||||
const staticKeyPair = Curve.generateKeyPair();
|
||||
const message = 'this is my message';
|
||||
const plaintext = Crypto.bytesFromString(message);
|
||||
const path =
|
||||
'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa';
|
||||
|
||||
const encrypted = await Crypto.encryptAttachment(
|
||||
staticKeyPair.pubKey.slice(1),
|
||||
path,
|
||||
plaintext
|
||||
);
|
||||
const decrypted = await Crypto.decryptAttachment(
|
||||
staticKeyPair.privKey,
|
||||
path,
|
||||
encrypted
|
||||
);
|
||||
|
||||
const equal = Crypto.constantTimeEqual(plaintext, decrypted);
|
||||
if (!equal) {
|
||||
throw new Error('The output and input did not match!');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyHmacSha256', () => {
|
||||
it('rejects if their MAC is too short', async () => {
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
const plaintext = Crypto.bytesFromString('Hello world');
|
||||
const ourMac = await Crypto.hmacSha256(key, plaintext);
|
||||
const theirMac = ourMac.slice(0, -1);
|
||||
let error;
|
||||
try {
|
||||
await Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
ourMac.byteLength
|
||||
);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert.instanceOf(error, Error);
|
||||
assert.strictEqual(error.message, 'Bad MAC length');
|
||||
});
|
||||
|
||||
it('rejects if their MAC is too long', async () => {
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
const plaintext = Crypto.bytesFromString('Hello world');
|
||||
const ourMac = await Crypto.hmacSha256(key, plaintext);
|
||||
const theirMac = Crypto.concatenateBytes(ourMac, new Uint8Array([0xff]));
|
||||
let error;
|
||||
try {
|
||||
await Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
ourMac.byteLength
|
||||
);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert.instanceOf(error, Error);
|
||||
assert.strictEqual(error.message, 'Bad MAC length');
|
||||
});
|
||||
|
||||
it('rejects if our MAC is shorter than the specified length', async () => {
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
const plaintext = Crypto.bytesFromString('Hello world');
|
||||
const ourMac = await Crypto.hmacSha256(key, plaintext);
|
||||
const theirMac = ourMac;
|
||||
let error;
|
||||
try {
|
||||
await Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
ourMac.byteLength + 1
|
||||
);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert.instanceOf(error, Error);
|
||||
assert.strictEqual(error.message, 'Bad MAC length');
|
||||
});
|
||||
|
||||
it("rejects if the MACs don't match", async () => {
|
||||
const plaintext = Crypto.bytesFromString('Hello world');
|
||||
const ourKey = Crypto.getRandomBytes(32);
|
||||
const ourMac = await Crypto.hmacSha256(ourKey, plaintext);
|
||||
const theirKey = Crypto.getRandomBytes(32);
|
||||
const theirMac = await Crypto.hmacSha256(theirKey, plaintext);
|
||||
let error;
|
||||
try {
|
||||
await Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
ourKey,
|
||||
theirMac,
|
||||
ourMac.byteLength
|
||||
);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert.instanceOf(error, Error);
|
||||
assert.strictEqual(error.message, 'Bad MAC');
|
||||
});
|
||||
|
||||
it('resolves with undefined if the MACs match exactly', async () => {
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
const plaintext = Crypto.bytesFromString('Hello world');
|
||||
const theirMac = await Crypto.hmacSha256(key, plaintext);
|
||||
const result = await Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
theirMac.byteLength
|
||||
);
|
||||
assert.isUndefined(result);
|
||||
});
|
||||
|
||||
it('resolves with undefined if the first `length` bytes of the MACs match', async () => {
|
||||
const key = Crypto.getRandomBytes(32);
|
||||
const plaintext = Crypto.bytesFromString('Hello world');
|
||||
const theirMac = (await Crypto.hmacSha256(key, plaintext)).slice(0, -5);
|
||||
const result = await Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
theirMac.byteLength
|
||||
);
|
||||
assert.isUndefined(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uuidToArrayBuffer', () => {
|
||||
const { uuidToArrayBuffer } = Crypto;
|
||||
|
||||
it('converts valid UUIDs to ArrayBuffers', () => {
|
||||
const expectedResult = Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([
|
||||
0x22,
|
||||
0x6e,
|
||||
0x44,
|
||||
0x02,
|
||||
0x7f,
|
||||
0xfc,
|
||||
0x45,
|
||||
0x43,
|
||||
0x85,
|
||||
0xc9,
|
||||
0x46,
|
||||
0x22,
|
||||
0xc5,
|
||||
0x0a,
|
||||
0x5b,
|
||||
0x14,
|
||||
])
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
uuidToArrayBuffer('226e4402-7ffc-4543-85c9-4622c50a5b14'),
|
||||
expectedResult
|
||||
);
|
||||
assert.deepEqual(
|
||||
uuidToArrayBuffer('226E4402-7FFC-4543-85C9-4622C50A5B14'),
|
||||
expectedResult
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty ArrayBuffer for strings of the wrong length', () => {
|
||||
assert.deepEqual(uuidToArrayBuffer(''), new ArrayBuffer(0));
|
||||
assert.deepEqual(uuidToArrayBuffer('abc'), new ArrayBuffer(0));
|
||||
assert.deepEqual(
|
||||
uuidToArrayBuffer('032deadf0d5e4ee78da28e75b1dfb284'),
|
||||
new ArrayBuffer(0)
|
||||
);
|
||||
assert.deepEqual(
|
||||
uuidToArrayBuffer('deaed5eb-d983-456a-a954-9ad7a006b271aaaaaaaaaa'),
|
||||
new ArrayBuffer(0)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arrayBufferToUuid', () => {
|
||||
const { arrayBufferToUuid } = Crypto;
|
||||
|
||||
it('converts valid ArrayBuffers to UUID strings', () => {
|
||||
const buf = Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([
|
||||
0x22,
|
||||
0x6e,
|
||||
0x44,
|
||||
0x02,
|
||||
0x7f,
|
||||
0xfc,
|
||||
0x45,
|
||||
0x43,
|
||||
0x85,
|
||||
0xc9,
|
||||
0x46,
|
||||
0x22,
|
||||
0xc5,
|
||||
0x0a,
|
||||
0x5b,
|
||||
0x14,
|
||||
])
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
arrayBufferToUuid(buf),
|
||||
'226e4402-7ffc-4543-85c9-4622c50a5b14'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns undefined if passed an all-zero buffer', () => {
|
||||
assert.isUndefined(arrayBufferToUuid(new ArrayBuffer(16)));
|
||||
});
|
||||
|
||||
it('returns undefined if passed the wrong number of bytes', () => {
|
||||
assert.isUndefined(arrayBufferToUuid(new ArrayBuffer(0)));
|
||||
assert.isUndefined(
|
||||
arrayBufferToUuid(
|
||||
Crypto.typedArrayToArrayBuffer(new Uint8Array([0x22]))
|
||||
)
|
||||
);
|
||||
assert.isUndefined(
|
||||
arrayBufferToUuid(
|
||||
Crypto.typedArrayToArrayBuffer(new Uint8Array(Array(17).fill(0x22)))
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,6 +7,7 @@
|
|||
import { assert } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import Long from 'long';
|
||||
import * as Bytes from '../../Bytes';
|
||||
import { typedArrayToArrayBuffer } from '../../Crypto';
|
||||
import { SenderCertificateMode } from '../../textsecure/OutgoingMessage';
|
||||
|
@ -42,9 +43,7 @@ describe('SenderCertificateService', () => {
|
|||
fakeValidCertificate = new SenderCertificate();
|
||||
fakeValidCertificateExpiry = Date.now() + 604800000;
|
||||
const certificate = new SenderCertificate.Certificate();
|
||||
certificate.expires = global.window.dcodeIO.Long.fromNumber(
|
||||
fakeValidCertificateExpiry
|
||||
);
|
||||
certificate.expires = Long.fromNumber(fakeValidCertificateExpiry);
|
||||
fakeValidCertificate.certificate = SenderCertificate.Certificate.encode(
|
||||
certificate
|
||||
).finish();
|
||||
|
@ -215,9 +214,7 @@ describe('SenderCertificateService', () => {
|
|||
|
||||
const expiredCertificate = new SenderCertificate();
|
||||
const certificate = new SenderCertificate.Certificate();
|
||||
certificate.expires = global.window.dcodeIO.Long.fromNumber(
|
||||
Date.now() - 1000
|
||||
);
|
||||
certificate.expires = Long.fromNumber(Date.now() - 1000);
|
||||
expiredCertificate.certificate = SenderCertificate.Certificate.encode(
|
||||
certificate
|
||||
).finish();
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { isByteBufferEmpty } from '../../util/isByteBufferEmpty';
|
||||
|
||||
describe('isByteBufferEmpty', () => {
|
||||
it('returns true for undefined', () => {
|
||||
assert.isTrue(isByteBufferEmpty(undefined));
|
||||
});
|
||||
|
||||
it('returns true for object missing limit', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const brokenByteBuffer: any = {};
|
||||
|
||||
assert.isTrue(isByteBufferEmpty(brokenByteBuffer));
|
||||
});
|
||||
|
||||
it('returns true for object limit', () => {
|
||||
const emptyByteBuffer = new window.dcodeIO.ByteBuffer(0);
|
||||
|
||||
assert.isTrue(isByteBufferEmpty(emptyByteBuffer));
|
||||
});
|
||||
|
||||
it('returns false for object limit', () => {
|
||||
const byteBuffer = window.dcodeIO.ByteBuffer.wrap('AABBCC', 'hex');
|
||||
|
||||
assert.isFalse(isByteBufferEmpty(byteBuffer));
|
||||
});
|
||||
});
|
95
ts/test-node/Proto_unknown_field_test.ts
Normal file
95
ts/test-node/Proto_unknown_field_test.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { Root } from 'protobufjs';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { Partial, Full } = (Root as any).fromJSON({
|
||||
nested: {
|
||||
test: {
|
||||
nested: {
|
||||
Partial: {
|
||||
fields: {
|
||||
a: {
|
||||
type: 'string',
|
||||
id: 1,
|
||||
},
|
||||
c: {
|
||||
type: 'int32',
|
||||
id: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
Full: {
|
||||
fields: {
|
||||
a: {
|
||||
type: 'string',
|
||||
id: 1,
|
||||
},
|
||||
b: {
|
||||
type: 'bool',
|
||||
id: 2,
|
||||
},
|
||||
c: {
|
||||
type: 'int32',
|
||||
id: 3,
|
||||
},
|
||||
d: {
|
||||
type: 'bytes',
|
||||
id: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}).nested.test;
|
||||
|
||||
describe('Proto#__unknownFields', () => {
|
||||
it('should encode and decode with unknown fields', () => {
|
||||
const full = Full.encode({
|
||||
a: 'hello',
|
||||
b: true,
|
||||
c: 42,
|
||||
d: Buffer.from('ohai'),
|
||||
}).finish();
|
||||
|
||||
const partial = Partial.decode(full);
|
||||
assert.strictEqual(partial.a, 'hello');
|
||||
assert.strictEqual(partial.c, 42);
|
||||
assert.strictEqual(partial.__unknownFields.length, 2);
|
||||
assert.strictEqual(
|
||||
Buffer.from(partial.__unknownFields[0]).toString('hex'),
|
||||
'1001'
|
||||
);
|
||||
assert.strictEqual(
|
||||
Buffer.from(partial.__unknownFields[1]).toString('hex'),
|
||||
'22046f686169'
|
||||
);
|
||||
|
||||
const encoded = Partial.encode({
|
||||
a: partial.a,
|
||||
c: partial.c,
|
||||
__unknownFields: partial.__unknownFields,
|
||||
}).finish();
|
||||
const decoded = Full.decode(encoded);
|
||||
|
||||
assert.strictEqual(decoded.a, 'hello');
|
||||
assert.strictEqual(decoded.b, true);
|
||||
assert.strictEqual(decoded.c, 42);
|
||||
assert.strictEqual(Buffer.from(decoded.d).toString(), 'ohai');
|
||||
|
||||
const concat = Partial.encode({
|
||||
a: partial.a,
|
||||
c: partial.c,
|
||||
__unknownFields: [Buffer.concat(partial.__unknownFields)],
|
||||
}).finish();
|
||||
const decodedConcat = Full.decode(concat);
|
||||
|
||||
assert.strictEqual(decodedConcat.a, 'hello');
|
||||
assert.strictEqual(decodedConcat.b, true);
|
||||
assert.strictEqual(decodedConcat.c, 42);
|
||||
assert.strictEqual(Buffer.from(decodedConcat.d).toString(), 'ohai');
|
||||
});
|
||||
});
|
1145
ts/textsecure.d.ts
vendored
1145
ts/textsecure.d.ts
vendored
File diff suppressed because it is too large
Load diff
|
@ -11,6 +11,8 @@ import {
|
|||
hmacSha256,
|
||||
sha256,
|
||||
verifyHmacSha256,
|
||||
base64ToArrayBuffer,
|
||||
typedArrayToArrayBuffer,
|
||||
} from '../Crypto';
|
||||
|
||||
declare global {
|
||||
|
@ -335,10 +337,7 @@ const Crypto = {
|
|||
encryptedProfileName: string,
|
||||
key: ArrayBuffer
|
||||
): Promise<{ given: ArrayBuffer; family: ArrayBuffer | null }> {
|
||||
const data = window.dcodeIO.ByteBuffer.wrap(
|
||||
encryptedProfileName,
|
||||
'base64'
|
||||
).toArrayBuffer();
|
||||
const data = base64ToArrayBuffer(encryptedProfileName);
|
||||
return Crypto.decryptProfile(data, key).then(decrypted => {
|
||||
const padded = new Uint8Array(decrypted);
|
||||
|
||||
|
@ -364,13 +363,9 @@ const Crypto = {
|
|||
const foundFamilyName = familyEnd > givenEnd + 1;
|
||||
|
||||
return {
|
||||
given: window.dcodeIO.ByteBuffer.wrap(padded)
|
||||
.slice(0, givenEnd)
|
||||
.toArrayBuffer(),
|
||||
given: typedArrayToArrayBuffer(padded.slice(0, givenEnd)),
|
||||
family: foundFamilyName
|
||||
? window.dcodeIO.ByteBuffer.wrap(padded)
|
||||
.slice(givenEnd + 1, familyEnd)
|
||||
.toArrayBuffer()
|
||||
? typedArrayToArrayBuffer(padded.slice(givenEnd + 1, familyEnd))
|
||||
: null,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -6,27 +6,14 @@
|
|||
/* eslint-disable no-proto */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { ByteBufferClass } from '../window.d';
|
||||
|
||||
let ByteBuffer: ByteBufferClass | undefined;
|
||||
const arrayBuffer = new ArrayBuffer(0);
|
||||
const uint8Array = new Uint8Array();
|
||||
|
||||
let StaticByteBufferProto: any;
|
||||
const StaticArrayBufferProto = (arrayBuffer as any).__proto__;
|
||||
const StaticUint8ArrayProto = (uint8Array as any).__proto__;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
function getString(thing: any): string {
|
||||
// Note: we must make this at runtime because it's loaded in the browser context
|
||||
if (!ByteBuffer) {
|
||||
ByteBuffer = new window.dcodeIO.ByteBuffer();
|
||||
}
|
||||
|
||||
if (!StaticByteBufferProto) {
|
||||
StaticByteBufferProto = (ByteBuffer as any).__proto__;
|
||||
}
|
||||
|
||||
if (thing === Object(thing)) {
|
||||
if (thing.__proto__ === StaticUint8ArrayProto) {
|
||||
return String.fromCharCode.apply(null, thing);
|
||||
|
@ -34,9 +21,6 @@ function getString(thing: any): string {
|
|||
if (thing.__proto__ === StaticArrayBufferProto) {
|
||||
return getString(new Uint8Array(thing));
|
||||
}
|
||||
if (thing.__proto__ === StaticByteBufferProto) {
|
||||
return thing.toString('binary');
|
||||
}
|
||||
}
|
||||
return thing;
|
||||
}
|
||||
|
@ -48,8 +32,7 @@ function getStringable(thing: any): boolean {
|
|||
typeof thing === 'boolean' ||
|
||||
(thing === Object(thing) &&
|
||||
(thing.__proto__ === StaticArrayBufferProto ||
|
||||
thing.__proto__ === StaticUint8ArrayProto ||
|
||||
thing.__proto__ === StaticByteBufferProto))
|
||||
thing.__proto__ === StaticUint8ArrayProto))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -193,7 +193,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
|
||||
server: WebAPIType;
|
||||
|
||||
serverTrustRoot: ArrayBuffer;
|
||||
serverTrustRoot: Uint8Array;
|
||||
|
||||
signalingKey: ArrayBuffer;
|
||||
|
||||
|
@ -239,9 +239,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
if (!options.serverTrustRoot) {
|
||||
throw new Error('Server trust root is required!');
|
||||
}
|
||||
this.serverTrustRoot = MessageReceiverInner.stringToArrayBufferBase64(
|
||||
options.serverTrustRoot
|
||||
);
|
||||
this.serverTrustRoot = Bytes.fromBase64(options.serverTrustRoot);
|
||||
|
||||
this.number_id = oldUsername
|
||||
? utils.unencodeNumber(oldUsername)[0]
|
||||
|
@ -286,18 +284,6 @@ class MessageReceiverInner extends EventTarget {
|
|||
});
|
||||
}
|
||||
|
||||
static stringToArrayBuffer = (string: string): ArrayBuffer =>
|
||||
window.dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();
|
||||
|
||||
static arrayBufferToString = (arrayBuffer: ArrayBuffer): string =>
|
||||
window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');
|
||||
|
||||
static stringToArrayBufferBase64 = (string: string): ArrayBuffer =>
|
||||
window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();
|
||||
|
||||
static arrayBufferToStringBase64 = (arrayBuffer: ArrayBuffer): string =>
|
||||
window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
|
||||
|
||||
async connect(socket?: WebSocket): Promise<void> {
|
||||
if (this.calledClose) {
|
||||
return;
|
||||
|
@ -2479,8 +2465,8 @@ class MessageReceiverInner extends EventTarget {
|
|||
|
||||
const paddedData = await Crypto.decryptAttachment(
|
||||
encrypted,
|
||||
MessageReceiverInner.stringToArrayBufferBase64(key),
|
||||
MessageReceiverInner.stringToArrayBufferBase64(digest)
|
||||
typedArrayToArrayBuffer(Bytes.fromBase64(key)),
|
||||
typedArrayToArrayBuffer(Bytes.fromBase64(digest))
|
||||
);
|
||||
|
||||
if (!isNumber(size)) {
|
||||
|
@ -2688,14 +2674,4 @@ export default class MessageReceiver {
|
|||
checkSocket: () => void;
|
||||
|
||||
getProcessedCount: () => number;
|
||||
|
||||
static stringToArrayBuffer = MessageReceiverInner.stringToArrayBuffer;
|
||||
|
||||
static arrayBufferToString = MessageReceiverInner.arrayBufferToString;
|
||||
|
||||
static stringToArrayBufferBase64 =
|
||||
MessageReceiverInner.stringToArrayBufferBase64;
|
||||
|
||||
static arrayBufferToStringBase64 =
|
||||
MessageReceiverInner.arrayBufferToStringBase64;
|
||||
}
|
||||
|
|
|
@ -28,23 +28,22 @@ import PQueue from 'p-queue';
|
|||
import { v4 as getGuid } from 'uuid';
|
||||
import { client as WebSocketClient, connection as WebSocket } from 'websocket';
|
||||
import { z } from 'zod';
|
||||
import Long from 'long';
|
||||
|
||||
import { Long } from '../window.d';
|
||||
import { assert } from '../util/assert';
|
||||
import { getUserAgent } from '../util/getUserAgent';
|
||||
import { toWebSafeBase64 } from '../util/webSafeBase64';
|
||||
import { isPackIdValid, redactPackId } from '../types/Stickers';
|
||||
import * as Bytes from '../Bytes';
|
||||
import {
|
||||
arrayBufferToBase64,
|
||||
base64ToArrayBuffer,
|
||||
bytesFromHexString,
|
||||
bytesFromString,
|
||||
concatenateBytes,
|
||||
constantTimeEqual,
|
||||
decryptAesGcm,
|
||||
deriveSecrets,
|
||||
encryptCdsDiscoveryRequest,
|
||||
getBytes,
|
||||
getRandomValue,
|
||||
splitUuids,
|
||||
typedArrayToArrayBuffer,
|
||||
|
@ -84,7 +83,7 @@ type SgxConstantsType = {
|
|||
let sgxConstantCache: SgxConstantsType | null = null;
|
||||
|
||||
function makeLong(value: string): Long {
|
||||
return window.dcodeIO.Long.fromString(value);
|
||||
return Long.fromString(value);
|
||||
}
|
||||
function getSgxConstants() {
|
||||
if (sgxConstantCache) {
|
||||
|
@ -2434,34 +2433,38 @@ export function initialize({
|
|||
|
||||
function validateAttestationQuote({
|
||||
serverStaticPublic,
|
||||
quote,
|
||||
quote: quoteArrayBuffer,
|
||||
}: {
|
||||
serverStaticPublic: ArrayBuffer;
|
||||
quote: ArrayBuffer;
|
||||
}) {
|
||||
const SGX_CONSTANTS = getSgxConstants();
|
||||
const byteBuffer = window.dcodeIO.ByteBuffer.wrap(
|
||||
quote,
|
||||
'binary',
|
||||
window.dcodeIO.ByteBuffer.LITTLE_ENDIAN
|
||||
);
|
||||
const quote = Buffer.from(quoteArrayBuffer);
|
||||
|
||||
const quoteVersion = byteBuffer.readShort(0) & 0xffff;
|
||||
let off = 0;
|
||||
|
||||
const quoteVersion = quote.readInt32LE(off) & 0xffff;
|
||||
off += 4;
|
||||
if (quoteVersion < 0 || quoteVersion > 2) {
|
||||
throw new Error(`Unknown version ${quoteVersion}`);
|
||||
}
|
||||
|
||||
const miscSelect = new Uint8Array(getBytes(quote, 64, 4));
|
||||
const miscSelect = quote.slice(off, off + 64);
|
||||
off += 64;
|
||||
if (!miscSelect.every(byte => byte === 0)) {
|
||||
throw new Error('Quote miscSelect invalid!');
|
||||
}
|
||||
|
||||
const reserved1 = new Uint8Array(getBytes(quote, 68, 28));
|
||||
const reserved1 = quote.slice(off, off + 28);
|
||||
off += 28;
|
||||
if (!reserved1.every(byte => byte === 0)) {
|
||||
throw new Error('Quote reserved1 invalid!');
|
||||
}
|
||||
|
||||
const flags = byteBuffer.readLong(96);
|
||||
const flags = Long.fromBytesLE(
|
||||
Array.from(quote.slice(off, off + 8).values())
|
||||
);
|
||||
off += 8;
|
||||
if (
|
||||
flags.and(SGX_CONSTANTS.SGX_FLAGS_RESERVED).notEquals(0) ||
|
||||
flags.and(SGX_CONSTANTS.SGX_FLAGS_INITTED).equals(0) ||
|
||||
|
@ -2470,25 +2473,29 @@ export function initialize({
|
|||
throw new Error(`Quote flags invalid ${flags.toString()}`);
|
||||
}
|
||||
|
||||
const xfrm = byteBuffer.readLong(104);
|
||||
const xfrm = Long.fromBytesLE(
|
||||
Array.from(quote.slice(off, off + 8).values())
|
||||
);
|
||||
off += 8;
|
||||
if (xfrm.and(SGX_CONSTANTS.SGX_XFRM_RESERVED).notEquals(0)) {
|
||||
throw new Error(`Quote xfrm invalid ${xfrm}`);
|
||||
}
|
||||
|
||||
const mrenclave = new Uint8Array(getBytes(quote, 112, 32));
|
||||
const enclaveIdBytes = new Uint8Array(
|
||||
bytesFromHexString(directoryEnclaveId)
|
||||
);
|
||||
if (!mrenclave.every((byte, index) => byte === enclaveIdBytes[index])) {
|
||||
const mrenclave = quote.slice(off, off + 32);
|
||||
off += 32;
|
||||
const enclaveIdBytes = Bytes.fromHex(directoryEnclaveId);
|
||||
if (mrenclave.compare(enclaveIdBytes) !== 0) {
|
||||
throw new Error('Quote mrenclave invalid!');
|
||||
}
|
||||
|
||||
const reserved2 = new Uint8Array(getBytes(quote, 144, 32));
|
||||
const reserved2 = quote.slice(off, off + 32);
|
||||
off += 32;
|
||||
if (!reserved2.every(byte => byte === 0)) {
|
||||
throw new Error('Quote reserved2 invalid!');
|
||||
}
|
||||
|
||||
const reportData = new Uint8Array(getBytes(quote, 368, 64));
|
||||
const reportData = quote.slice(off, off + 64);
|
||||
off += 64;
|
||||
const serverStaticPublicBytes = new Uint8Array(serverStaticPublic);
|
||||
if (
|
||||
!reportData.every((byte, index) => {
|
||||
|
@ -2501,22 +2508,26 @@ export function initialize({
|
|||
throw new Error('Quote report_data invalid!');
|
||||
}
|
||||
|
||||
const reserved3 = new Uint8Array(getBytes(quote, 208, 96));
|
||||
const reserved3 = quote.slice(off, off + 96);
|
||||
off += 96;
|
||||
if (!reserved3.every(byte => byte === 0)) {
|
||||
throw new Error('Quote reserved3 invalid!');
|
||||
}
|
||||
|
||||
const reserved4 = new Uint8Array(getBytes(quote, 308, 60));
|
||||
const reserved4 = quote.slice(off, off + 60);
|
||||
off += 60;
|
||||
if (!reserved4.every(byte => byte === 0)) {
|
||||
throw new Error('Quote reserved4 invalid!');
|
||||
}
|
||||
|
||||
const signatureLength = byteBuffer.readInt(432) & 0xffff_ffff;
|
||||
const signatureLength = quote.readInt32LE(432) >>> 0;
|
||||
off += 4;
|
||||
if (signatureLength !== quote.byteLength - 436) {
|
||||
throw new Error(`Bad signatureLength ${signatureLength}`);
|
||||
}
|
||||
|
||||
// const signature = Uint8Array.from(getBytes(quote, 436, signatureLength));
|
||||
// const signature = quote.slice(off, signatureLength);
|
||||
// off += signatureLength
|
||||
}
|
||||
|
||||
function validateAttestationSignatureBody(
|
||||
|
|
|
@ -1,46 +1,51 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { arrayBufferToBase64 } from '../Crypto';
|
||||
import { PinnedConversationClass } from '../textsecure.d';
|
||||
import * as Bytes from '../Bytes';
|
||||
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
import PinnedConversation = Proto.AccountRecord.IPinnedConversation;
|
||||
|
||||
export function arePinnedConversationsEqual(
|
||||
localValue: Array<PinnedConversationClass>,
|
||||
remoteValue: Array<PinnedConversationClass>
|
||||
localValue: Array<PinnedConversation>,
|
||||
remoteValue: Array<PinnedConversation>
|
||||
): boolean {
|
||||
if (localValue.length !== remoteValue.length) {
|
||||
return false;
|
||||
}
|
||||
return localValue.every(
|
||||
(localPinnedConversation: PinnedConversationClass, index: number) => {
|
||||
(localPinnedConversation: PinnedConversation, index: number) => {
|
||||
const remotePinnedConversation = remoteValue[index];
|
||||
if (
|
||||
localPinnedConversation.identifier !==
|
||||
remotePinnedConversation.identifier
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
switch (localPinnedConversation.identifier) {
|
||||
case 'contact':
|
||||
|
||||
const {
|
||||
contact,
|
||||
groupMasterKey,
|
||||
legacyGroupId,
|
||||
} = localPinnedConversation;
|
||||
|
||||
if (contact) {
|
||||
return (
|
||||
localPinnedConversation.contact &&
|
||||
remotePinnedConversation.contact &&
|
||||
localPinnedConversation.contact.uuid ===
|
||||
remotePinnedConversation.contact.uuid
|
||||
contact.uuid === remotePinnedConversation.contact.uuid
|
||||
);
|
||||
case 'groupMasterKey':
|
||||
return (
|
||||
arrayBufferToBase64(localPinnedConversation.groupMasterKey) ===
|
||||
arrayBufferToBase64(remotePinnedConversation.groupMasterKey)
|
||||
}
|
||||
|
||||
if (groupMasterKey && groupMasterKey.length) {
|
||||
return Bytes.areEqual(
|
||||
groupMasterKey,
|
||||
remotePinnedConversation.groupMasterKey
|
||||
);
|
||||
case 'legacyGroupId':
|
||||
return (
|
||||
arrayBufferToBase64(localPinnedConversation.legacyGroupId) ===
|
||||
arrayBufferToBase64(remotePinnedConversation.legacyGroupId)
|
||||
}
|
||||
|
||||
if (legacyGroupId && legacyGroupId.length) {
|
||||
return Bytes.areEqual(
|
||||
legacyGroupId,
|
||||
remotePinnedConversation.legacyGroupId
|
||||
);
|
||||
default:
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
import { ByteBufferClass } from '../window.d';
|
||||
|
||||
export function isByteBufferEmpty(data?: ByteBufferClass): boolean {
|
||||
return !data || !isNumber(data.limit) || data.limit === 0;
|
||||
}
|
|
@ -8561,13 +8561,6 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"updated": "2021-05-20T20:01:50.505Z"
|
||||
},
|
||||
{
|
||||
"rule": "thenify-multiArgs",
|
||||
"path": "node_modules/make-dir/node_modules/pify/index.js",
|
||||
"line": "\t\t\t\t} else if (opts.multiArgs) {",
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-19T18:06:35.446Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-html(",
|
||||
"path": "node_modules/marked/lib/marked.js",
|
||||
|
@ -12995,13 +12988,6 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"updated": "2021-05-07T18:18:03.022Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "node_modules/update-notifier/index.js",
|
||||
"line": "\t\t\t\t\tchalk().cyan(format(' sudo chown -R $USER:$(id -gn $USER) %s ', xdgBasedir().config));",
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-19T21:59:32.770Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "node_modules/uri-js/dist/es5/uri.all.min.js",
|
||||
|
|
|
@ -244,6 +244,8 @@ const excludedFilesRegexps = [
|
|||
'^node_modules/xmldom/.+',
|
||||
'^node_modules/yargs-unparser/',
|
||||
'^node_modules/yargs/.+',
|
||||
'^node_modules/find-yarn-workspace-root/.+',
|
||||
'^node_modules/update-notifier/.+',
|
||||
|
||||
// Used by Storybook
|
||||
'^node_modules/@emotion/.+',
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { Long } from '../window.d';
|
||||
import Long from 'long';
|
||||
|
||||
import { normalizeNumber } from './normalizeNumber';
|
||||
|
||||
export function getSafeLongFromTimestamp(timestamp = 0): Long {
|
||||
if (timestamp >= Number.MAX_SAFE_INTEGER) {
|
||||
return window.dcodeIO.Long.MAX_VALUE;
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
return window.dcodeIO.Long.fromNumber(timestamp);
|
||||
return Long.fromNumber(timestamp);
|
||||
}
|
||||
|
||||
export function getTimestampFromLong(value: Long | null): number {
|
||||
export function getTimestampFromLong(value?: Long | number | null): number {
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const num = value.toNumber();
|
||||
const num = normalizeNumber(value);
|
||||
|
||||
if (num >= Number.MAX_SAFE_INTEGER) {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
|
|
48
ts/window.d.ts
vendored
48
ts/window.d.ts
vendored
|
@ -18,11 +18,7 @@ import {
|
|||
ReactionAttributesType,
|
||||
ReactionModelType,
|
||||
} from './model-types.d';
|
||||
import {
|
||||
ContactRecordIdentityState,
|
||||
TextSecureType,
|
||||
DownloadAttachmentType,
|
||||
} from './textsecure.d';
|
||||
import { TextSecureType, DownloadAttachmentType } from './textsecure.d';
|
||||
import { Storage } from './textsecure/Storage';
|
||||
import {
|
||||
ChallengeHandler,
|
||||
|
@ -177,7 +173,6 @@ declare global {
|
|||
baseAttachmentsPath: string;
|
||||
baseStickersPath: string;
|
||||
baseTempPath: string;
|
||||
dcodeIO: DCodeIOType;
|
||||
receivedAtCounter: number;
|
||||
enterKeyboardMode: () => void;
|
||||
enterMouseMode: () => void;
|
||||
|
@ -553,51 +548,10 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
export type DCodeIOType = {
|
||||
ByteBuffer: typeof ByteBufferClass & {
|
||||
BIG_ENDIAN: number;
|
||||
LITTLE_ENDIAN: number;
|
||||
Long: DCodeIOType['Long'];
|
||||
};
|
||||
Long: Long & {
|
||||
MAX_VALUE: Long;
|
||||
equals: (other: Long | number | string) => boolean;
|
||||
fromBits: (low: number, high: number, unsigned: boolean) => number;
|
||||
fromNumber: (value: number, unsigned?: boolean) => Long;
|
||||
fromString: (str: string | null) => Long;
|
||||
isLong: (obj: unknown) => obj is Long;
|
||||
};
|
||||
ProtoBuf: WhatIsThis;
|
||||
};
|
||||
|
||||
export class CertificateValidatorType {
|
||||
validate: (cerficate: any, certificateTime: number) => Promise<void>;
|
||||
}
|
||||
|
||||
export class ByteBufferClass {
|
||||
constructor(value?: any, littleEndian?: number);
|
||||
static wrap: (
|
||||
value: any,
|
||||
encoding?: string,
|
||||
littleEndian?: number
|
||||
) => ByteBufferClass;
|
||||
buffer: ArrayBuffer;
|
||||
toString: (type: string) => string;
|
||||
toArrayBuffer: () => ArrayBuffer;
|
||||
toBinary: () => string;
|
||||
slice: (start: number, end?: number) => ByteBufferClass;
|
||||
append: (data: ArrayBuffer) => void;
|
||||
limit: number;
|
||||
offset: 0;
|
||||
readInt: (offset: number) => number;
|
||||
readLong: (offset: number) => Long;
|
||||
readShort: (offset: number) => number;
|
||||
readVarint32: () => number;
|
||||
reset: () => void;
|
||||
writeLong: (l: Long) => void;
|
||||
skip: (length: number) => void;
|
||||
}
|
||||
|
||||
export class GumVideoCapturer {
|
||||
constructor(
|
||||
maxWidth: number,
|
||||
|
|
84
yarn.lock
84
yarn.lock
|
@ -4824,7 +4824,7 @@ boom@2.x.x:
|
|||
dependencies:
|
||||
hoek "2.x.x"
|
||||
|
||||
boxen@^1.2.1, boxen@^1.3.0:
|
||||
boxen@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
|
||||
integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==
|
||||
|
@ -5875,18 +5875,6 @@ config@1.28.1:
|
|||
json5 "0.4.0"
|
||||
os-homedir "1.0.2"
|
||||
|
||||
configstore@^3.0.0:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f"
|
||||
integrity sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==
|
||||
dependencies:
|
||||
dot-prop "^4.1.0"
|
||||
graceful-fs "^4.1.2"
|
||||
make-dir "^1.0.0"
|
||||
unique-string "^1.0.0"
|
||||
write-file-atomic "^2.0.0"
|
||||
xdg-basedir "^3.0.0"
|
||||
|
||||
configstore@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
|
||||
|
@ -6942,13 +6930,6 @@ dot-case@^3.0.4:
|
|||
no-case "^3.0.4"
|
||||
tslib "^2.0.3"
|
||||
|
||||
dot-prop@^4.1.0:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.1.tgz#45884194a71fc2cda71cbb4bceb3a4dd2f433ba4"
|
||||
integrity sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==
|
||||
dependencies:
|
||||
is-obj "^1.0.0"
|
||||
|
||||
dot-prop@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb"
|
||||
|
@ -8492,13 +8473,12 @@ find-up@^4.0.0:
|
|||
locate-path "^5.0.0"
|
||||
path-exists "^4.0.0"
|
||||
|
||||
find-yarn-workspace-root@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz#40eb8e6e7c2502ddfaa2577c176f221422f860db"
|
||||
integrity sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q==
|
||||
find-yarn-workspace-root@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd"
|
||||
integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==
|
||||
dependencies:
|
||||
fs-extra "^4.0.3"
|
||||
micromatch "^3.1.4"
|
||||
micromatch "^4.0.2"
|
||||
|
||||
findup-sync@^4.0.0:
|
||||
version "4.0.0"
|
||||
|
@ -8727,15 +8707,6 @@ fs-extra@^2.0.0:
|
|||
graceful-fs "^4.1.2"
|
||||
jsonfile "^2.1.0"
|
||||
|
||||
fs-extra@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94"
|
||||
integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==
|
||||
dependencies:
|
||||
graceful-fs "^4.1.2"
|
||||
jsonfile "^4.0.0"
|
||||
universalify "^0.1.0"
|
||||
|
||||
fs-extra@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
|
||||
|
@ -10686,11 +10657,6 @@ is-number@^7.0.0:
|
|||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
||||
|
||||
is-obj@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
|
||||
integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
|
||||
|
||||
is-obj@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
|
||||
|
@ -11317,7 +11283,7 @@ language-tags@^1.0.5:
|
|||
dependencies:
|
||||
language-subtag-registry "~0.3.2"
|
||||
|
||||
latest-version@^3.0.0, latest-version@^3.1.0:
|
||||
latest-version@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
|
||||
integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=
|
||||
|
@ -13196,6 +13162,14 @@ open@^7.0.3:
|
|||
is-docker "^2.0.0"
|
||||
is-wsl "^2.1.1"
|
||||
|
||||
open@^7.4.2:
|
||||
version "7.4.2"
|
||||
resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
|
||||
integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
|
||||
dependencies:
|
||||
is-docker "^2.0.0"
|
||||
is-wsl "^2.1.1"
|
||||
|
||||
opn@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
|
||||
|
@ -13656,24 +13630,24 @@ pascalcase@^0.1.1:
|
|||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
|
||||
|
||||
patch-package@6.1.2:
|
||||
version "6.1.2"
|
||||
resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.1.2.tgz#9ed0b3defb5c34ecbef3f334ddfb13e01b3d3ff6"
|
||||
integrity sha512-5GnzR8lEyeleeariG+hGabUnD2b1yL7AIGFjlLo95zMGRWhZCel58IpeKD46wwPb7i+uNhUI8unV56ogk8Bgqg==
|
||||
patch-package@6.4.7:
|
||||
version "6.4.7"
|
||||
resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.4.7.tgz#2282d53c397909a0d9ef92dae3fdeb558382b148"
|
||||
integrity sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ==
|
||||
dependencies:
|
||||
"@yarnpkg/lockfile" "^1.1.0"
|
||||
chalk "^2.4.2"
|
||||
cross-spawn "^6.0.5"
|
||||
find-yarn-workspace-root "^1.2.1"
|
||||
find-yarn-workspace-root "^2.0.0"
|
||||
fs-extra "^7.0.1"
|
||||
is-ci "^2.0.0"
|
||||
klaw-sync "^6.0.0"
|
||||
minimist "^1.2.0"
|
||||
open "^7.4.2"
|
||||
rimraf "^2.6.3"
|
||||
semver "^5.6.0"
|
||||
slash "^2.0.0"
|
||||
tmp "^0.0.33"
|
||||
update-notifier "^2.5.0"
|
||||
|
||||
path-browserify@0.0.1:
|
||||
version "0.0.1"
|
||||
|
@ -18172,22 +18146,6 @@ upath@^1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068"
|
||||
integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==
|
||||
|
||||
update-notifier@^2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6"
|
||||
integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==
|
||||
dependencies:
|
||||
boxen "^1.2.1"
|
||||
chalk "^2.0.1"
|
||||
configstore "^3.0.0"
|
||||
import-lazy "^2.1.0"
|
||||
is-ci "^1.0.10"
|
||||
is-installed-globally "^0.1.0"
|
||||
is-npm "^1.0.0"
|
||||
latest-version "^3.0.0"
|
||||
semver-diff "^2.0.0"
|
||||
xdg-basedir "^3.0.0"
|
||||
|
||||
update-notifier@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9"
|
||||
|
|
Loading…
Reference in a new issue