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"
|
type="text/javascript"
|
||||||
src="libtextsecure/protocol_wrapper.js"
|
src="libtextsecure/protocol_wrapper.js"
|
||||||
></script>
|
></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/notifications.js"></script>
|
||||||
<script type="text/javascript" src="js/libphonenumber-util.js"></script>
|
<script type="text/javascript" src="js/libphonenumber-util.js"></script>
|
||||||
|
|
|
@ -13,12 +13,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
},
|
},
|
||||||
"preen": {
|
"preen": {
|
||||||
"bytebuffer": [
|
|
||||||
"dist/ByteBufferAB.js"
|
|
||||||
],
|
|
||||||
"long": [
|
|
||||||
"dist/Long.js"
|
|
||||||
],
|
|
||||||
"mp3lameencoder": [
|
"mp3lameencoder": [
|
||||||
"lib/Mp3LameEncoder.js"
|
"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: {
|
globals: {
|
||||||
assert: true,
|
assert: true,
|
||||||
assertEqualArrayBuffers: true,
|
assertEqualArrayBuffers: true,
|
||||||
dcodeIO: true,
|
|
||||||
getString: true,
|
getString: true,
|
||||||
hexToArrayBuffer: true,
|
hexToArrayBuffer: true,
|
||||||
PROTO_ROOT: true,
|
|
||||||
stringToArrayBuffer: true,
|
stringToArrayBuffer: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
mocha.setup('bdd');
|
mocha.setup('bdd');
|
||||||
window.assert = chai.assert;
|
window.assert = chai.assert;
|
||||||
window.PROTO_ROOT = '../../protos';
|
|
||||||
|
|
||||||
const OriginalReporter = mocha._reporter;
|
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>
|
||||||
|
|
||||||
<script type="text/javascript" src="../components.js"></script>
|
<script type="text/javascript" src="../components.js"></script>
|
||||||
<script type="text/javascript" src="../protobufs.js" data-cover></script>
|
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="../protocol_wrapper.js"
|
src="../protocol_wrapper.js"
|
||||||
|
@ -38,7 +37,6 @@
|
||||||
></script>
|
></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="helpers_test.js"></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="generate_keys_test.js"></script>
|
||||||
<script type="text/javascript" src="task_with_timeout_test.js"></script>
|
<script type="text/javascript" src="task_with_timeout_test.js"></script>
|
||||||
<script type="text/javascript" src="account_manager_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 -->
|
<!-- Uncomment to start tests without code coverage enabled -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.textsecure.protobuf.onLoad(() => {
|
mocha.run();
|
||||||
mocha.run();
|
window.Signal.conversationControllerStart();
|
||||||
window.Signal.conversationControllerStart();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -268,7 +268,7 @@
|
||||||
"node-sass-import-once": "1.2.0",
|
"node-sass-import-once": "1.2.0",
|
||||||
"npm-run-all": "4.1.5",
|
"npm-run-all": "4.1.5",
|
||||||
"nyc": "11.4.1",
|
"nyc": "11.4.1",
|
||||||
"patch-package": "6.1.2",
|
"patch-package": "6.4.7",
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"react-docgen-typescript": "1.2.6",
|
"react-docgen-typescript": "1.2.6",
|
||||||
"sass-loader": "7.2.0",
|
"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.sqlInitializer = require('./ts/sql/initialize');
|
||||||
|
|
||||||
window.PROTO_ROOT = 'protos';
|
|
||||||
const config = require('url').parse(window.location.toString(), true).query;
|
const config = require('url').parse(window.location.toString(), true).query;
|
||||||
|
|
||||||
setEnvironment(parseEnvironment(config.environment));
|
setEnvironment(parseEnvironment(config.environment));
|
||||||
|
|
|
@ -15,9 +15,5 @@
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="../../libtextsecure/protocol_wrapper.js"
|
src="../../libtextsecure/protocol_wrapper.js"
|
||||||
></script>
|
></script>
|
||||||
<script
|
|
||||||
type="text/javascript"
|
|
||||||
src="../../libtextsecure/protobufs.js"
|
|
||||||
></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -36,7 +36,6 @@ setEnvironment(parseEnvironment(config.environment));
|
||||||
window.sqlInitializer = require('../ts/sql/initialize');
|
window.sqlInitializer = require('../ts/sql/initialize');
|
||||||
|
|
||||||
window.ROOT_PATH = window.location.href.startsWith('file') ? '../../' : '/';
|
window.ROOT_PATH = window.location.href.startsWith('file') ? '../../' : '/';
|
||||||
window.PROTO_ROOT = '../../protos';
|
|
||||||
window.getEnvironment = getEnvironment;
|
window.getEnvironment = getEnvironment;
|
||||||
window.getVersion = () => config.version;
|
window.getVersion = () => config.version;
|
||||||
window.getGuid = require('uuid/v4');
|
window.getGuid = require('uuid/v4');
|
||||||
|
|
|
@ -12,10 +12,8 @@ module.exports = {
|
||||||
globals: {
|
globals: {
|
||||||
assert: true,
|
assert: true,
|
||||||
assertEqualArrayBuffers: true,
|
assertEqualArrayBuffers: true,
|
||||||
dcodeIO: true,
|
|
||||||
getString: true,
|
getString: true,
|
||||||
hexToArrayBuffer: true,
|
hexToArrayBuffer: true,
|
||||||
PROTO_ROOT: true,
|
|
||||||
stringToArrayBuffer: true,
|
stringToArrayBuffer: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
mocha.setup('bdd');
|
mocha.setup('bdd');
|
||||||
window.assert = chai.assert;
|
window.assert = chai.assert;
|
||||||
window.PROTO_ROOT = '../protos';
|
|
||||||
|
|
||||||
const OriginalReporter = mocha._reporter;
|
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"
|
type="text/javascript"
|
||||||
src="../libtextsecure/protocol_wrapper.js"
|
src="../libtextsecure/protocol_wrapper.js"
|
||||||
></script>
|
></script>
|
||||||
<script type="text/javascript" src="../libtextsecure/protobufs.js"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="../js/libphonenumber-util.js"></script>
|
<script type="text/javascript" src="../js/libphonenumber-util.js"></script>
|
||||||
<script
|
<script
|
||||||
|
@ -411,10 +410,8 @@
|
||||||
<script type="text/javascript" src="libphonenumber_util_test.js"></script>
|
<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="keychange_listener_test.js"></script>
|
||||||
<script type="text/javascript" src="reliable_trigger_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="database_test.js"></script>
|
||||||
<script type="text/javascript" src="i18n_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>
|
<script type="text/javascript" src="stickers_test.js"></script>
|
||||||
|
|
||||||
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->
|
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->
|
||||||
|
@ -423,15 +420,13 @@
|
||||||
|
|
||||||
<!-- Uncomment to start tests without code coverage enabled -->
|
<!-- Uncomment to start tests without code coverage enabled -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.textsecure.protobuf.onLoad(() => {
|
window.Signal.conversationControllerStart();
|
||||||
window.Signal.conversationControllerStart();
|
|
||||||
|
|
||||||
window.test.pendingDescribeCalls.forEach(args => {
|
window.test.pendingDescribeCalls.forEach(args => {
|
||||||
describe(...args);
|
describe(...args);
|
||||||
});
|
|
||||||
|
|
||||||
mocha.run();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mocha.run();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 chai = require('chai');
|
||||||
const chaiAsPromised = require('chai-as-promised');
|
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 { setEnvironment, Environment } = require('../ts/environment');
|
||||||
const { Context: SignalContext } = require('../ts/context');
|
const { Context: SignalContext } = require('../ts/context');
|
||||||
const { isValidGuid } = require('../ts/util/isValidGuid');
|
const { isValidGuid } = require('../ts/util/isValidGuid');
|
||||||
|
@ -27,10 +25,6 @@ global.window = {
|
||||||
error: (...args) => console.error(...args),
|
error: (...args) => console.error(...args),
|
||||||
},
|
},
|
||||||
i18n: key => `i18n(${key})`,
|
i18n: key => `i18n(${key})`,
|
||||||
dcodeIO: {
|
|
||||||
ByteBuffer,
|
|
||||||
Long,
|
|
||||||
},
|
|
||||||
storage: {
|
storage: {
|
||||||
get: key => storageMap.get(key),
|
get: key => storageMap.get(key),
|
||||||
put: async (key, value) => storageMap.set(key, value),
|
put: async (key, value) => storageMap.set(key, value),
|
||||||
|
|
|
@ -37,7 +37,7 @@ export function toString(data: Uint8Array): string {
|
||||||
return bytes.toString(data);
|
return bytes.toString(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function concatenate(list: Array<Uint8Array>): Uint8Array {
|
export function concatenate(list: ReadonlyArray<Uint8Array>): Uint8Array {
|
||||||
return bytes.concatenate(list);
|
return bytes.concatenate(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,3 +50,10 @@ export function isNotEmpty(
|
||||||
): data is Uint8Array {
|
): data is Uint8Array {
|
||||||
return !bytes.isEmpty(data);
|
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
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { Buffer } from 'buffer';
|
||||||
import pProps from 'p-props';
|
import pProps from 'p-props';
|
||||||
import { chunk } from 'lodash';
|
import { chunk } from 'lodash';
|
||||||
|
import Long from 'long';
|
||||||
import { HKDF } from '@signalapp/signal-client';
|
import { HKDF } from '@signalapp/signal-client';
|
||||||
|
|
||||||
import { calculateAgreement, generateKeyPair } from './Curve';
|
import { calculateAgreement, generateKeyPair } from './Curve';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -34,37 +37,43 @@ export function typedArrayToArrayBuffer(typedArray: Uint8Array): ArrayBuffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function arrayBufferToBase64(arrayBuffer: ArrayBuffer): string {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
export function bytesFromHexString(string: string): ArrayBuffer {
|
||||||
return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();
|
return typedArrayToArrayBuffer(Buffer.from(string, 'hex'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deriveStickerPackKey(
|
export async function deriveStickerPackKey(
|
||||||
|
@ -115,10 +124,16 @@ export async function computeHash(data: ArrayBuffer): Promise<string> {
|
||||||
|
|
||||||
// High-level Operations
|
// High-level Operations
|
||||||
|
|
||||||
|
export type EncryptedDeviceName = {
|
||||||
|
ephemeralPublic: ArrayBuffer;
|
||||||
|
syntheticIv: ArrayBuffer;
|
||||||
|
ciphertext: ArrayBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
export async function encryptDeviceName(
|
export async function encryptDeviceName(
|
||||||
deviceName: string,
|
deviceName: string,
|
||||||
identityPublic: ArrayBuffer
|
identityPublic: ArrayBuffer
|
||||||
): Promise<Record<string, ArrayBuffer>> {
|
): Promise<EncryptedDeviceName> {
|
||||||
const plaintext = bytesFromString(deviceName);
|
const plaintext = bytesFromString(deviceName);
|
||||||
const ephemeralKeyPair = generateKeyPair();
|
const ephemeralKeyPair = generateKeyPair();
|
||||||
const masterSecret = calculateAgreement(
|
const masterSecret = calculateAgreement(
|
||||||
|
@ -143,15 +158,7 @@ export async function encryptDeviceName(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decryptDeviceName(
|
export async function decryptDeviceName(
|
||||||
{
|
{ ephemeralPublic, syntheticIv, ciphertext }: EncryptedDeviceName,
|
||||||
ephemeralPublic,
|
|
||||||
syntheticIv,
|
|
||||||
ciphertext,
|
|
||||||
}: {
|
|
||||||
ephemeralPublic: ArrayBuffer;
|
|
||||||
syntheticIv: ArrayBuffer;
|
|
||||||
ciphertext: ArrayBuffer;
|
|
||||||
},
|
|
||||||
identityPrivate: ArrayBuffer
|
identityPrivate: ArrayBuffer
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const masterSecret = calculateAgreement(ephemeralPublic, identityPrivate);
|
const masterSecret = calculateAgreement(ephemeralPublic, identityPrivate);
|
||||||
|
@ -661,21 +668,18 @@ export async function encryptCdsDiscoveryRequest(
|
||||||
phoneNumbers: ReadonlyArray<string>
|
phoneNumbers: ReadonlyArray<string>
|
||||||
): Promise<Record<string, unknown>> {
|
): Promise<Record<string, unknown>> {
|
||||||
const nonce = getRandomBytes(32);
|
const nonce = getRandomBytes(32);
|
||||||
const numbersArray = new window.dcodeIO.ByteBuffer(
|
const numbersArray = Buffer.concat(
|
||||||
phoneNumbers.length * 8,
|
phoneNumbers.map(number => {
|
||||||
window.dcodeIO.ByteBuffer.BIG_ENDIAN
|
// Long.fromString handles numbers with or without a leading '+'
|
||||||
|
return new Uint8Array(Long.fromString(number).toBytesBE());
|
||||||
|
})
|
||||||
);
|
);
|
||||||
phoneNumbers.forEach(number => {
|
|
||||||
// Long.fromString handles numbers with or without a leading '+'
|
|
||||||
numbersArray.writeLong(window.dcodeIO.ByteBuffer.Long.fromString(number));
|
|
||||||
});
|
|
||||||
|
|
||||||
// We've written to the array, so offset === byteLength; we need to reset it. Then we'll
|
// 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.
|
// have access to everything in the array when we generate an ArrayBuffer from it.
|
||||||
numbersArray.reset();
|
|
||||||
const queryDataPlaintext = concatenateBytes(
|
const queryDataPlaintext = concatenateBytes(
|
||||||
nonce,
|
nonce,
|
||||||
numbersArray.toArrayBuffer()
|
typedArrayToArrayBuffer(numbersArray)
|
||||||
);
|
);
|
||||||
|
|
||||||
const queryDataKey = getRandomBytes(32);
|
const queryDataKey = getRandomBytes(32);
|
||||||
|
@ -785,7 +789,5 @@ export function trimForDisplay(arrayBuffer: ArrayBuffer): ArrayBuffer {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return window.dcodeIO.ByteBuffer.wrap(padded)
|
return typedArrayToArrayBuffer(padded.slice(0, paddingEnd));
|
||||||
.slice(0, paddingEnd)
|
|
||||||
.toArrayBuffer();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,4 +54,15 @@ export class Bytes {
|
||||||
public isNotEmpty(data: Uint8Array | null | undefined): data is Uint8Array {
|
public isNotEmpty(data: Uint8Array | null | undefined): data is Uint8Array {
|
||||||
return !this.isEmpty(data);
|
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 { DeviceType } from './textsecure/Types';
|
||||||
import { SendOptionsType } from './textsecure/SendMessage';
|
import { SendOptionsType } from './textsecure/SendMessage';
|
||||||
import { SendMessageChallengeData } from './textsecure/Errors';
|
import { SendMessageChallengeData } from './textsecure/Errors';
|
||||||
import {
|
|
||||||
AccessRequiredEnum,
|
|
||||||
MemberRoleEnum,
|
|
||||||
SyncMessageClass,
|
|
||||||
} from './textsecure.d';
|
|
||||||
import { UserMessage } from './types/Message';
|
import { UserMessage } from './types/Message';
|
||||||
import { MessageModel } from './models/messages';
|
import { MessageModel } from './models/messages';
|
||||||
import { ConversationModel } from './models/conversations';
|
import { ConversationModel } from './models/conversations';
|
||||||
|
@ -24,6 +19,10 @@ import { GroupNameCollisionsWithIdsByTitle } from './util/groupMemberNameCollisi
|
||||||
import { ConversationColorType } from './types/Colors';
|
import { ConversationColorType } from './types/Colors';
|
||||||
import { AttachmentType, ThumbnailType } from './types/Attachment';
|
import { AttachmentType, ThumbnailType } from './types/Attachment';
|
||||||
import { ContactType } from './types/Contact';
|
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;
|
export type WhatIsThis = any;
|
||||||
|
|
||||||
|
|
|
@ -6,19 +6,14 @@ import pMap from 'p-map';
|
||||||
|
|
||||||
import Crypto from '../textsecure/Crypto';
|
import Crypto from '../textsecure/Crypto';
|
||||||
import dataInterface from '../sql/Client';
|
import dataInterface from '../sql/Client';
|
||||||
|
import * as Bytes from '../Bytes';
|
||||||
import {
|
import {
|
||||||
arrayBufferToBase64,
|
arrayBufferToBase64,
|
||||||
base64ToArrayBuffer,
|
base64ToArrayBuffer,
|
||||||
deriveStorageItemKey,
|
deriveStorageItemKey,
|
||||||
deriveStorageManifestKey,
|
deriveStorageManifestKey,
|
||||||
|
typedArrayToArrayBuffer,
|
||||||
} from '../Crypto';
|
} from '../Crypto';
|
||||||
import {
|
|
||||||
ManifestRecordClass,
|
|
||||||
ManifestRecordIdentifierClass,
|
|
||||||
StorageItemClass,
|
|
||||||
StorageManifestClass,
|
|
||||||
StorageRecordClass,
|
|
||||||
} from '../textsecure.d';
|
|
||||||
import {
|
import {
|
||||||
mergeAccountRecord,
|
mergeAccountRecord,
|
||||||
mergeContactRecord,
|
mergeContactRecord,
|
||||||
|
@ -30,16 +25,24 @@ import {
|
||||||
toGroupV2Record,
|
toGroupV2Record,
|
||||||
} from './storageRecordOps';
|
} from './storageRecordOps';
|
||||||
import { ConversationModel } from '../models/conversations';
|
import { ConversationModel } from '../models/conversations';
|
||||||
|
import { strictAssert } from '../util/assert';
|
||||||
import { BackOff } from '../util/BackOff';
|
import { BackOff } from '../util/BackOff';
|
||||||
import { storageJobQueue } from '../util/JobQueue';
|
import { storageJobQueue } from '../util/JobQueue';
|
||||||
import { sleep } from '../util/sleep';
|
import { sleep } from '../util/sleep';
|
||||||
import { isMoreRecentThan } from '../util/timestamp';
|
import { isMoreRecentThan } from '../util/timestamp';
|
||||||
|
import { normalizeNumber } from '../util/normalizeNumber';
|
||||||
import { isStorageWriteFeatureEnabled } from '../storage/isFeatureEnabled';
|
import { isStorageWriteFeatureEnabled } from '../storage/isFeatureEnabled';
|
||||||
import { ourProfileKeyService } from './ourProfileKey';
|
import { ourProfileKeyService } from './ourProfileKey';
|
||||||
import {
|
import {
|
||||||
ConversationTypes,
|
ConversationTypes,
|
||||||
typeofConversation,
|
typeofConversation,
|
||||||
} from '../util/whatTypeOfConversation';
|
} 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 {
|
const {
|
||||||
eraseStorageServiceStateFromConversations,
|
eraseStorageServiceStateFromConversations,
|
||||||
|
@ -82,9 +85,9 @@ type UnknownRecord = RemoteRecord;
|
||||||
|
|
||||||
async function encryptRecord(
|
async function encryptRecord(
|
||||||
storageID: string | undefined,
|
storageID: string | undefined,
|
||||||
storageRecord: StorageRecordClass
|
storageRecord: Proto.IStorageRecord
|
||||||
): Promise<StorageItemClass> {
|
): Promise<Proto.StorageItem> {
|
||||||
const storageItem = new window.textsecure.protobuf.StorageItem();
|
const storageItem = new Proto.StorageItem();
|
||||||
|
|
||||||
const storageKeyBuffer = storageID
|
const storageKeyBuffer = storageID
|
||||||
? base64ToArrayBuffer(String(storageID))
|
? base64ToArrayBuffer(String(storageID))
|
||||||
|
@ -101,12 +104,12 @@ async function encryptRecord(
|
||||||
);
|
);
|
||||||
|
|
||||||
const encryptedRecord = await Crypto.encryptProfile(
|
const encryptedRecord = await Crypto.encryptProfile(
|
||||||
storageRecord.toArrayBuffer(),
|
typedArrayToArrayBuffer(Proto.StorageRecord.encode(storageRecord).finish()),
|
||||||
storageItemKey
|
storageItemKey
|
||||||
);
|
);
|
||||||
|
|
||||||
storageItem.key = storageKeyBuffer;
|
storageItem.key = new FIXMEU8(storageKeyBuffer);
|
||||||
storageItem.value = encryptedRecord;
|
storageItem.value = new FIXMEU8(encryptedRecord);
|
||||||
|
|
||||||
return storageItem;
|
return storageItem;
|
||||||
}
|
}
|
||||||
|
@ -121,13 +124,13 @@ type GeneratedManifestType = {
|
||||||
storageID: string | undefined;
|
storageID: string | undefined;
|
||||||
}>;
|
}>;
|
||||||
deleteKeys: Array<ArrayBuffer>;
|
deleteKeys: Array<ArrayBuffer>;
|
||||||
newItems: Set<StorageItemClass>;
|
newItems: Set<Proto.IStorageItem>;
|
||||||
storageManifest: StorageManifestClass;
|
storageManifest: Proto.IStorageManifest;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function generateManifest(
|
async function generateManifest(
|
||||||
version: number,
|
version: number,
|
||||||
previousManifest?: ManifestRecordClass,
|
previousManifest?: Proto.IManifestRecord,
|
||||||
isNewManifest = false
|
isNewManifest = false
|
||||||
): Promise<GeneratedManifestType> {
|
): Promise<GeneratedManifestType> {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
|
@ -138,39 +141,39 @@ async function generateManifest(
|
||||||
|
|
||||||
await window.ConversationController.checkForConflicts();
|
await window.ConversationController.checkForConflicts();
|
||||||
|
|
||||||
const ITEM_TYPE = window.textsecure.protobuf.ManifestRecord.Identifier.Type;
|
const ITEM_TYPE = Proto.ManifestRecord.Identifier.Type;
|
||||||
|
|
||||||
const conversationsToUpdate = [];
|
const conversationsToUpdate = [];
|
||||||
const insertKeys: Array<string> = [];
|
const insertKeys: Array<string> = [];
|
||||||
const deleteKeys: Array<ArrayBuffer> = [];
|
const deleteKeys: Array<ArrayBuffer> = [];
|
||||||
const manifestRecordKeys: Set<ManifestRecordIdentifierClass> = new Set();
|
const manifestRecordKeys: Set<IManifestRecordIdentifier> = new Set();
|
||||||
const newItems: Set<StorageItemClass> = new Set();
|
const newItems: Set<Proto.IStorageItem> = new Set();
|
||||||
|
|
||||||
const conversations = window.getConversations();
|
const conversations = window.getConversations();
|
||||||
for (let i = 0; i < conversations.length; i += 1) {
|
for (let i = 0; i < conversations.length; i += 1) {
|
||||||
const conversation = conversations.models[i];
|
const conversation = conversations.models[i];
|
||||||
const identifier = new window.textsecure.protobuf.ManifestRecord.Identifier();
|
const identifier = new Proto.ManifestRecord.Identifier();
|
||||||
|
|
||||||
let storageRecord;
|
let storageRecord;
|
||||||
|
|
||||||
const conversationType = typeofConversation(conversation.attributes);
|
const conversationType = typeofConversation(conversation.attributes);
|
||||||
if (conversationType === ConversationTypes.Me) {
|
if (conversationType === ConversationTypes.Me) {
|
||||||
storageRecord = new window.textsecure.protobuf.StorageRecord();
|
storageRecord = new Proto.StorageRecord();
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
storageRecord.account = await toAccountRecord(conversation);
|
storageRecord.account = await toAccountRecord(conversation);
|
||||||
identifier.type = ITEM_TYPE.ACCOUNT;
|
identifier.type = ITEM_TYPE.ACCOUNT;
|
||||||
} else if (conversationType === ConversationTypes.Direct) {
|
} else if (conversationType === ConversationTypes.Direct) {
|
||||||
storageRecord = new window.textsecure.protobuf.StorageRecord();
|
storageRecord = new Proto.StorageRecord();
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
storageRecord.contact = await toContactRecord(conversation);
|
storageRecord.contact = await toContactRecord(conversation);
|
||||||
identifier.type = ITEM_TYPE.CONTACT;
|
identifier.type = ITEM_TYPE.CONTACT;
|
||||||
} else if (conversationType === ConversationTypes.GroupV2) {
|
} else if (conversationType === ConversationTypes.GroupV2) {
|
||||||
storageRecord = new window.textsecure.protobuf.StorageRecord();
|
storageRecord = new Proto.StorageRecord();
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
storageRecord.groupV2 = await toGroupV2Record(conversation);
|
storageRecord.groupV2 = await toGroupV2Record(conversation);
|
||||||
identifier.type = ITEM_TYPE.GROUPV2;
|
identifier.type = ITEM_TYPE.GROUPV2;
|
||||||
} else if (conversationType === ConversationTypes.GroupV1) {
|
} else if (conversationType === ConversationTypes.GroupV1) {
|
||||||
storageRecord = new window.textsecure.protobuf.StorageRecord();
|
storageRecord = new Proto.StorageRecord();
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
storageRecord.groupV1 = await toGroupV1Record(conversation);
|
storageRecord.groupV1 = await toGroupV1Record(conversation);
|
||||||
identifier.type = ITEM_TYPE.GROUPV1;
|
identifier.type = ITEM_TYPE.GROUPV1;
|
||||||
|
@ -256,9 +259,9 @@ async function generateManifest(
|
||||||
// When updating the manifest, ensure all "unknown" keys are added to the
|
// 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
|
// new manifest, so we don't inadvertently delete something we don't understand
|
||||||
unknownRecordsArray.forEach((record: UnknownRecord) => {
|
unknownRecordsArray.forEach((record: UnknownRecord) => {
|
||||||
const identifier = new window.textsecure.protobuf.ManifestRecord.Identifier();
|
const identifier = new Proto.ManifestRecord.Identifier();
|
||||||
identifier.type = record.itemType;
|
identifier.type = record.itemType;
|
||||||
identifier.raw = base64ToArrayBuffer(record.storageID);
|
identifier.raw = Bytes.fromBase64(record.storageID);
|
||||||
|
|
||||||
manifestRecordKeys.add(identifier);
|
manifestRecordKeys.add(identifier);
|
||||||
});
|
});
|
||||||
|
@ -276,9 +279,9 @@ async function generateManifest(
|
||||||
// These records failed to merge in the previous fetchManifest, but we still
|
// These records failed to merge in the previous fetchManifest, but we still
|
||||||
// need to include them so that the manifest is complete
|
// need to include them so that the manifest is complete
|
||||||
recordsWithErrors.forEach((record: UnknownRecord) => {
|
recordsWithErrors.forEach((record: UnknownRecord) => {
|
||||||
const identifier = new window.textsecure.protobuf.ManifestRecord.Identifier();
|
const identifier = new Proto.ManifestRecord.Identifier();
|
||||||
identifier.type = record.itemType;
|
identifier.type = record.itemType;
|
||||||
identifier.raw = base64ToArrayBuffer(record.storageID);
|
identifier.raw = Bytes.fromBase64(record.storageID);
|
||||||
|
|
||||||
manifestRecordKeys.add(identifier);
|
manifestRecordKeys.add(identifier);
|
||||||
});
|
});
|
||||||
|
@ -293,7 +296,8 @@ async function generateManifest(
|
||||||
// This can be broken down into two parts:
|
// This can be broken down into two parts:
|
||||||
// There are no duplicate type+raw pairs
|
// There are no duplicate type+raw pairs
|
||||||
// There are no duplicate raw bytes
|
// 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}`;
|
const typeAndRaw = `${identifier.type}+${storageID}`;
|
||||||
if (
|
if (
|
||||||
rawDuplicates.has(identifier.raw) ||
|
rawDuplicates.has(identifier.raw) ||
|
||||||
|
@ -335,11 +339,13 @@ async function generateManifest(
|
||||||
rawDuplicates.clear();
|
rawDuplicates.clear();
|
||||||
typeRawDuplicates.clear();
|
typeRawDuplicates.clear();
|
||||||
|
|
||||||
const storageKeyDuplicates = new Set();
|
const storageKeyDuplicates = new Set<string>();
|
||||||
|
|
||||||
newItems.forEach(storageItem => {
|
newItems.forEach(storageItem => {
|
||||||
// Ensure there are no duplicate StorageIdentifiers in your list of inserts
|
// 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)) {
|
if (storageKeyDuplicates.has(storageID)) {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
'storageService.generateManifest: removing duplicate identifier from inserts',
|
'storageService.generateManifest: removing duplicate identifier from inserts',
|
||||||
|
@ -360,16 +366,18 @@ async function generateManifest(
|
||||||
const pendingDeletes: Set<string> = new Set();
|
const pendingDeletes: Set<string> = new Set();
|
||||||
|
|
||||||
const remoteKeys: Set<string> = new Set();
|
const remoteKeys: Set<string> = new Set();
|
||||||
previousManifest.keys.forEach(
|
(previousManifest.keys ?? []).forEach(
|
||||||
(identifier: ManifestRecordIdentifierClass) => {
|
(identifier: IManifestRecordIdentifier) => {
|
||||||
const storageID = arrayBufferToBase64(identifier.raw.toArrayBuffer());
|
strictAssert(identifier.raw, 'Identifier without raw field');
|
||||||
|
const storageID = Bytes.toBase64(identifier.raw);
|
||||||
remoteKeys.add(storageID);
|
remoteKeys.add(storageID);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const localKeys: Set<string> = new Set();
|
const localKeys: Set<string> = new Set();
|
||||||
manifestRecordKeys.forEach((identifier: ManifestRecordIdentifierClass) => {
|
manifestRecordKeys.forEach((identifier: IManifestRecordIdentifier) => {
|
||||||
const storageID = arrayBufferToBase64(identifier.raw);
|
strictAssert(identifier.raw, 'Identifier without raw field');
|
||||||
|
const storageID = Bytes.toBase64(identifier.raw);
|
||||||
localKeys.add(storageID);
|
localKeys.add(storageID);
|
||||||
|
|
||||||
if (!remoteKeys.has(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.version = version;
|
||||||
manifestRecord.keys = Array.from(manifestRecordKeys);
|
manifestRecord.keys = Array.from(manifestRecordKeys);
|
||||||
|
|
||||||
|
@ -420,13 +428,15 @@ async function generateManifest(
|
||||||
version
|
version
|
||||||
);
|
);
|
||||||
const encryptedManifest = await Crypto.encryptProfile(
|
const encryptedManifest = await Crypto.encryptProfile(
|
||||||
manifestRecord.toArrayBuffer(),
|
typedArrayToArrayBuffer(
|
||||||
|
Proto.ManifestRecord.encode(manifestRecord).finish()
|
||||||
|
),
|
||||||
storageManifestKey
|
storageManifestKey
|
||||||
);
|
);
|
||||||
|
|
||||||
const storageManifest = new window.textsecure.protobuf.StorageManifest();
|
const storageManifest = new Proto.StorageManifest();
|
||||||
storageManifest.version = version;
|
storageManifest.version = version;
|
||||||
storageManifest.value = encryptedManifest;
|
storageManifest.value = new FIXMEU8(encryptedManifest);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
conversationsToUpdate,
|
conversationsToUpdate,
|
||||||
|
@ -462,14 +472,16 @@ async function uploadManifest(
|
||||||
deleteKeys.length
|
deleteKeys.length
|
||||||
);
|
);
|
||||||
|
|
||||||
const writeOperation = new window.textsecure.protobuf.WriteOperation();
|
const writeOperation = new Proto.WriteOperation();
|
||||||
writeOperation.manifest = storageManifest;
|
writeOperation.manifest = storageManifest;
|
||||||
writeOperation.insertItem = Array.from(newItems);
|
writeOperation.insertItem = Array.from(newItems);
|
||||||
writeOperation.deleteKey = deleteKeys;
|
writeOperation.deleteKey = deleteKeys.map(key => new FIXMEU8(key));
|
||||||
|
|
||||||
window.log.info('storageService.uploadManifest: uploading...', version);
|
window.log.info('storageService.uploadManifest: uploading...', version);
|
||||||
await window.textsecure.messaging.modifyStorageRecords(
|
await window.textsecure.messaging.modifyStorageRecords(
|
||||||
writeOperation.toArrayBuffer(),
|
typedArrayToArrayBuffer(
|
||||||
|
Proto.WriteOperation.encode(writeOperation).finish()
|
||||||
|
),
|
||||||
{
|
{
|
||||||
credentials,
|
credentials,
|
||||||
}
|
}
|
||||||
|
@ -565,8 +577,8 @@ async function createNewManifest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function decryptManifest(
|
async function decryptManifest(
|
||||||
encryptedManifest: StorageManifestClass
|
encryptedManifest: Proto.IStorageManifest
|
||||||
): Promise<ManifestRecordClass> {
|
): Promise<Proto.ManifestRecord> {
|
||||||
const { version, value } = encryptedManifest;
|
const { version, value } = encryptedManifest;
|
||||||
|
|
||||||
const storageKeyBase64 = window.storage.get('storageKey');
|
const storageKeyBase64 = window.storage.get('storageKey');
|
||||||
|
@ -576,20 +588,21 @@ async function decryptManifest(
|
||||||
const storageKey = base64ToArrayBuffer(storageKeyBase64);
|
const storageKey = base64ToArrayBuffer(storageKeyBase64);
|
||||||
const storageManifestKey = await deriveStorageManifestKey(
|
const storageManifestKey = await deriveStorageManifestKey(
|
||||||
storageKey,
|
storageKey,
|
||||||
typeof version === 'number' ? version : version.toNumber()
|
normalizeNumber(version ?? 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
strictAssert(value, 'StorageManifest has no value field');
|
||||||
const decryptedManifest = await Crypto.decryptProfile(
|
const decryptedManifest = await Crypto.decryptProfile(
|
||||||
typeof value.toArrayBuffer === 'function' ? value.toArrayBuffer() : value,
|
typedArrayToArrayBuffer(value),
|
||||||
storageManifestKey
|
storageManifestKey
|
||||||
);
|
);
|
||||||
|
|
||||||
return window.textsecure.protobuf.ManifestRecord.decode(decryptedManifest);
|
return Proto.ManifestRecord.decode(new FIXMEU8(decryptedManifest));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchManifest(
|
async function fetchManifest(
|
||||||
manifestVersion: number
|
manifestVersion: number
|
||||||
): Promise<ManifestRecordClass | undefined> {
|
): Promise<Proto.ManifestRecord | undefined> {
|
||||||
window.log.info('storageService.fetchManifest');
|
window.log.info('storageService.fetchManifest');
|
||||||
|
|
||||||
if (!window.textsecure.messaging) {
|
if (!window.textsecure.messaging) {
|
||||||
|
@ -606,8 +619,8 @@ async function fetchManifest(
|
||||||
greaterThanVersion: manifestVersion,
|
greaterThanVersion: manifestVersion,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const encryptedManifest = window.textsecure.protobuf.StorageManifest.decode(
|
const encryptedManifest = Proto.StorageManifest.decode(
|
||||||
manifestBinary
|
new FIXMEU8(manifestBinary)
|
||||||
);
|
);
|
||||||
|
|
||||||
// if we don't get a value we're assuming that there's no newer manifest
|
// 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 = {
|
type MergeableItemType = {
|
||||||
itemType: number;
|
itemType: number;
|
||||||
storageID: string;
|
storageID: string;
|
||||||
storageRecord: StorageRecordClass;
|
storageRecord: Proto.IStorageRecord;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MergedRecordType = UnknownRecord & {
|
type MergedRecordType = UnknownRecord & {
|
||||||
|
@ -659,7 +672,7 @@ async function mergeRecord(
|
||||||
): Promise<MergedRecordType> {
|
): Promise<MergedRecordType> {
|
||||||
const { itemType, storageID, storageRecord } = itemToMerge;
|
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 hasConflict = false;
|
||||||
let isUnsupported = false;
|
let isUnsupported = false;
|
||||||
|
@ -709,18 +722,16 @@ async function mergeRecord(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processManifest(
|
async function processManifest(
|
||||||
manifest: ManifestRecordClass
|
manifest: Proto.IManifestRecord
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!window.textsecure.messaging) {
|
if (!window.textsecure.messaging) {
|
||||||
throw new Error('storageService.processManifest: We are offline!');
|
throw new Error('storageService.processManifest: We are offline!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const remoteKeysTypeMap = new Map();
|
const remoteKeysTypeMap = new Map();
|
||||||
manifest.keys.forEach((identifier: ManifestRecordIdentifierClass) => {
|
(manifest.keys || []).forEach(({ raw, type }: IManifestRecordIdentifier) => {
|
||||||
remoteKeysTypeMap.set(
|
strictAssert(raw, 'Identifier without raw field');
|
||||||
arrayBufferToBase64(identifier.raw.toArrayBuffer()),
|
remoteKeysTypeMap.set(Bytes.toBase64(raw), type);
|
||||||
identifier.type
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const remoteKeys = new Set(remoteKeysTypeMap.keys());
|
const remoteKeys = new Set(remoteKeysTypeMap.keys());
|
||||||
|
@ -820,21 +831,21 @@ async function processRemoteRecords(
|
||||||
remoteOnlyRecords.size
|
remoteOnlyRecords.size
|
||||||
);
|
);
|
||||||
|
|
||||||
const readOperation = new window.textsecure.protobuf.ReadOperation();
|
const readOperation = new Proto.ReadOperation();
|
||||||
readOperation.readKey = Array.from(remoteOnlyRecords.keys()).map(
|
readOperation.readKey = Array.from(remoteOnlyRecords.keys()).map(
|
||||||
base64ToArrayBuffer
|
Bytes.fromBase64
|
||||||
);
|
);
|
||||||
|
|
||||||
const credentials = window.storage.get('storageCredentials');
|
const credentials = window.storage.get('storageCredentials');
|
||||||
const storageItemsBuffer = await window.textsecure.messaging.getStorageRecords(
|
const storageItemsBuffer = await window.textsecure.messaging.getStorageRecords(
|
||||||
readOperation.toArrayBuffer(),
|
typedArrayToArrayBuffer(Proto.ReadOperation.encode(readOperation).finish()),
|
||||||
{
|
{
|
||||||
credentials,
|
credentials,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const storageItems = window.textsecure.protobuf.StorageItems.decode(
|
const storageItems = Proto.StorageItems.decode(
|
||||||
storageItemsBuffer
|
new FIXMEU8(storageItemsBuffer)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!storageItems.items) {
|
if (!storageItems.items) {
|
||||||
|
@ -847,7 +858,7 @@ async function processRemoteRecords(
|
||||||
const decryptedStorageItems = await pMap(
|
const decryptedStorageItems = await pMap(
|
||||||
storageItems.items,
|
storageItems.items,
|
||||||
async (
|
async (
|
||||||
storageRecordWrapper: StorageItemClass
|
storageRecordWrapper: Proto.IStorageItem
|
||||||
): Promise<MergeableItemType> => {
|
): Promise<MergeableItemType> => {
|
||||||
const { key, value: storageItemCiphertext } = storageRecordWrapper;
|
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(
|
const storageItemKey = await deriveStorageItemKey(
|
||||||
storageKey,
|
storageKey,
|
||||||
|
@ -871,7 +882,7 @@ async function processRemoteRecords(
|
||||||
let storageItemPlaintext;
|
let storageItemPlaintext;
|
||||||
try {
|
try {
|
||||||
storageItemPlaintext = await Crypto.decryptProfile(
|
storageItemPlaintext = await Crypto.decryptProfile(
|
||||||
storageItemCiphertext.toArrayBuffer(),
|
typedArrayToArrayBuffer(storageItemCiphertext),
|
||||||
storageItemKey
|
storageItemKey
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -882,8 +893,8 @@ async function processRemoteRecords(
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storageRecord = window.textsecure.protobuf.StorageRecord.decode(
|
const storageRecord = Proto.StorageRecord.decode(
|
||||||
storageItemPlaintext
|
new FIXMEU8(storageItemPlaintext)
|
||||||
);
|
);
|
||||||
|
|
||||||
const remoteRecord = remoteOnlyRecords.get(base64ItemID);
|
const remoteRecord = remoteOnlyRecords.get(base64ItemID);
|
||||||
|
@ -906,7 +917,7 @@ async function processRemoteRecords(
|
||||||
// Merge Account records last since it contains the pinned conversations
|
// Merge Account records last since it contains the pinned conversations
|
||||||
// and we need all other records merged first before we can find the pinned
|
// and we need all other records merged first before we can find the pinned
|
||||||
// records in our db
|
// 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) =>
|
const sortedStorageItems = decryptedStorageItems.sort((_, b) =>
|
||||||
b.itemType === ITEM_TYPE.ACCOUNT ? -1 : 1
|
b.itemType === ITEM_TYPE.ACCOUNT ? -1 : 1
|
||||||
);
|
);
|
||||||
|
@ -995,7 +1006,7 @@ async function processRemoteRecords(
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sync(): Promise<ManifestRecordClass | undefined> {
|
async function sync(): Promise<Proto.ManifestRecord | undefined> {
|
||||||
if (!isStorageWriteFeatureEnabled()) {
|
if (!isStorageWriteFeatureEnabled()) {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
'storageService.sync: Not starting desktop.storage is falsey'
|
'storageService.sync: Not starting desktop.storage is falsey'
|
||||||
|
@ -1010,7 +1021,7 @@ async function sync(): Promise<ManifestRecordClass | undefined> {
|
||||||
|
|
||||||
window.log.info('storageService.sync: starting...');
|
window.log.info('storageService.sync: starting...');
|
||||||
|
|
||||||
let manifest: ManifestRecordClass | undefined;
|
let manifest: Proto.ManifestRecord | undefined;
|
||||||
try {
|
try {
|
||||||
// If we've previously interacted with strage service, update 'fetchComplete' record
|
// If we've previously interacted with strage service, update 'fetchComplete' record
|
||||||
const previousFetchComplete = window.storage.get('storageFetchComplete');
|
const previousFetchComplete = window.storage.get('storageFetchComplete');
|
||||||
|
@ -1028,7 +1039,11 @@ async function sync(): Promise<ManifestRecordClass | undefined> {
|
||||||
return 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(
|
window.log.info(
|
||||||
`storageService.sync: manifest versions - previous: ${localManifestVersion}, current: ${version}`
|
`storageService.sync: manifest versions - previous: ${localManifestVersion}, current: ${version}`
|
||||||
|
@ -1095,7 +1110,7 @@ async function upload(fromSync = false): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let previousManifest: ManifestRecordClass | undefined;
|
let previousManifest: Proto.ManifestRecord | undefined;
|
||||||
if (!fromSync) {
|
if (!fromSync) {
|
||||||
// Syncing before we upload so that we repair any unknown records and
|
// 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
|
// 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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { isEqual, isNumber } from 'lodash';
|
import { isEqual, isNumber } from 'lodash';
|
||||||
|
import Long from 'long';
|
||||||
|
|
||||||
import {
|
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
|
||||||
arrayBufferToBase64,
|
|
||||||
base64ToArrayBuffer,
|
|
||||||
deriveMasterKeyFromGroupV1,
|
|
||||||
fromEncodedBinaryToArrayBuffer,
|
|
||||||
} from '../Crypto';
|
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
import dataInterface from '../sql/Client';
|
import dataInterface from '../sql/Client';
|
||||||
import {
|
|
||||||
AccountRecordClass,
|
|
||||||
ContactRecordClass,
|
|
||||||
GroupV1RecordClass,
|
|
||||||
GroupV2RecordClass,
|
|
||||||
PinnedConversationClass,
|
|
||||||
} from '../textsecure.d';
|
|
||||||
import {
|
import {
|
||||||
deriveGroupFields,
|
deriveGroupFields,
|
||||||
waitThenMaybeUpdateGroup,
|
waitThenMaybeUpdateGroup,
|
||||||
|
@ -46,6 +35,7 @@ import {
|
||||||
} from '../util/universalExpireTimer';
|
} from '../util/universalExpireTimer';
|
||||||
import { ourProfileKeyService } from './ourProfileKey';
|
import { ourProfileKeyService } from './ourProfileKey';
|
||||||
import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation';
|
import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation';
|
||||||
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
|
||||||
const { updateConversation } = dataInterface;
|
const { updateConversation } = dataInterface;
|
||||||
|
|
||||||
|
@ -53,14 +43,14 @@ const { updateConversation } = dataInterface;
|
||||||
const FIXMEU8 = Uint8Array;
|
const FIXMEU8 = Uint8Array;
|
||||||
|
|
||||||
type RecordClass =
|
type RecordClass =
|
||||||
| AccountRecordClass
|
| Proto.IAccountRecord
|
||||||
| ContactRecordClass
|
| Proto.IContactRecord
|
||||||
| GroupV1RecordClass
|
| Proto.IGroupV1Record
|
||||||
| GroupV2RecordClass;
|
| Proto.IGroupV2Record;
|
||||||
|
|
||||||
function toRecordVerified(verified: number): number {
|
function toRecordVerified(verified: number): Proto.ContactRecord.IdentityState {
|
||||||
const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus;
|
const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus;
|
||||||
const STATE_ENUM = window.textsecure.protobuf.ContactRecord.IdentityState;
|
const STATE_ENUM = Proto.ContactRecord.IdentityState;
|
||||||
|
|
||||||
switch (verified) {
|
switch (verified) {
|
||||||
case VERIFIED_ENUM.VERIFIED:
|
case VERIFIED_ENUM.VERIFIED:
|
||||||
|
@ -82,7 +72,9 @@ function addUnknownFields(
|
||||||
conversation.idForLogging()
|
conversation.idForLogging()
|
||||||
);
|
);
|
||||||
conversation.set({
|
conversation.set({
|
||||||
storageUnknownFields: arrayBufferToBase64(record.__unknownFields),
|
storageUnknownFields: Bytes.toBase64(
|
||||||
|
Bytes.concatenate(record.__unknownFields)
|
||||||
|
),
|
||||||
});
|
});
|
||||||
} else if (conversation.get('storageUnknownFields')) {
|
} else if (conversation.get('storageUnknownFields')) {
|
||||||
// If the record doesn't have unknown fields attached but we have them
|
// If the record doesn't have unknown fields attached but we have them
|
||||||
|
@ -106,41 +98,43 @@ function applyUnknownFields(
|
||||||
conversation.get('id')
|
conversation.get('id')
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
record.__unknownFields = base64ToArrayBuffer(storageUnknownFields);
|
record.__unknownFields = [Bytes.fromBase64(storageUnknownFields)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toContactRecord(
|
export async function toContactRecord(
|
||||||
conversation: ConversationModel
|
conversation: ConversationModel
|
||||||
): Promise<ContactRecordClass> {
|
): Promise<Proto.ContactRecord> {
|
||||||
const contactRecord = new window.textsecure.protobuf.ContactRecord();
|
const contactRecord = new Proto.ContactRecord();
|
||||||
if (conversation.get('uuid')) {
|
const uuid = conversation.get('uuid');
|
||||||
contactRecord.serviceUuid = conversation.get('uuid');
|
if (uuid) {
|
||||||
|
contactRecord.serviceUuid = uuid;
|
||||||
}
|
}
|
||||||
if (conversation.get('e164')) {
|
const e164 = conversation.get('e164');
|
||||||
contactRecord.serviceE164 = conversation.get('e164');
|
if (e164) {
|
||||||
|
contactRecord.serviceE164 = e164;
|
||||||
}
|
}
|
||||||
if (conversation.get('profileKey')) {
|
const profileKey = conversation.get('profileKey');
|
||||||
contactRecord.profileKey = base64ToArrayBuffer(
|
if (profileKey) {
|
||||||
String(conversation.get('profileKey'))
|
contactRecord.profileKey = Bytes.fromBase64(String(profileKey));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const identityKey = await window.textsecure.storage.protocol.loadIdentityKey(
|
const identityKey = await window.textsecure.storage.protocol.loadIdentityKey(
|
||||||
conversation.id
|
conversation.id
|
||||||
);
|
);
|
||||||
if (identityKey) {
|
if (identityKey) {
|
||||||
contactRecord.identityKey = identityKey;
|
contactRecord.identityKey = new FIXMEU8(identityKey);
|
||||||
}
|
}
|
||||||
if (conversation.get('verified')) {
|
const verified = conversation.get('verified');
|
||||||
contactRecord.identityState = toRecordVerified(
|
if (verified) {
|
||||||
Number(conversation.get('verified'))
|
contactRecord.identityState = toRecordVerified(Number(verified));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (conversation.get('profileName')) {
|
const profileName = conversation.get('profileName');
|
||||||
contactRecord.givenName = conversation.get('profileName');
|
if (profileName) {
|
||||||
|
contactRecord.givenName = profileName;
|
||||||
}
|
}
|
||||||
if (conversation.get('profileFamilyName')) {
|
const profileFamilyName = conversation.get('profileFamilyName');
|
||||||
contactRecord.familyName = conversation.get('profileFamilyName');
|
if (profileFamilyName) {
|
||||||
|
contactRecord.familyName = profileFamilyName;
|
||||||
}
|
}
|
||||||
contactRecord.blocked = conversation.isBlocked();
|
contactRecord.blocked = conversation.isBlocked();
|
||||||
contactRecord.whitelisted = Boolean(conversation.get('profileSharing'));
|
contactRecord.whitelisted = Boolean(conversation.get('profileSharing'));
|
||||||
|
@ -157,11 +151,11 @@ export async function toContactRecord(
|
||||||
|
|
||||||
export async function toAccountRecord(
|
export async function toAccountRecord(
|
||||||
conversation: ConversationModel
|
conversation: ConversationModel
|
||||||
): Promise<AccountRecordClass> {
|
): Promise<Proto.AccountRecord> {
|
||||||
const accountRecord = new window.textsecure.protobuf.AccountRecord();
|
const accountRecord = new Proto.AccountRecord();
|
||||||
|
|
||||||
if (conversation.get('profileKey')) {
|
if (conversation.get('profileKey')) {
|
||||||
accountRecord.profileKey = base64ToArrayBuffer(
|
accountRecord.profileKey = Bytes.fromBase64(
|
||||||
String(conversation.get('profileKey'))
|
String(conversation.get('profileKey'))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -198,7 +192,7 @@ export async function toAccountRecord(
|
||||||
}
|
}
|
||||||
|
|
||||||
const PHONE_NUMBER_SHARING_MODE_ENUM =
|
const PHONE_NUMBER_SHARING_MODE_ENUM =
|
||||||
window.textsecure.protobuf.AccountRecord.PhoneNumberSharingMode;
|
Proto.AccountRecord.PhoneNumberSharingMode;
|
||||||
const phoneNumberSharingMode = parsePhoneNumberSharingMode(
|
const phoneNumberSharingMode = parsePhoneNumberSharingMode(
|
||||||
window.storage.get('phoneNumberSharingMode')
|
window.storage.get('phoneNumberSharingMode')
|
||||||
);
|
);
|
||||||
|
@ -239,7 +233,7 @@ export async function toAccountRecord(
|
||||||
const pinnedConversation = window.ConversationController.get(id);
|
const pinnedConversation = window.ConversationController.get(id);
|
||||||
|
|
||||||
if (pinnedConversation) {
|
if (pinnedConversation) {
|
||||||
const pinnedConversationRecord = new window.textsecure.protobuf.AccountRecord.PinnedConversation();
|
const pinnedConversationRecord = new Proto.AccountRecord.PinnedConversation();
|
||||||
|
|
||||||
if (pinnedConversation.get('type') === 'private') {
|
if (pinnedConversation.get('type') === 'private') {
|
||||||
pinnedConversationRecord.identifier = 'contact';
|
pinnedConversationRecord.identifier = 'contact';
|
||||||
|
@ -255,9 +249,7 @@ export async function toAccountRecord(
|
||||||
'toAccountRecord: trying to pin a v1 Group without groupId'
|
'toAccountRecord: trying to pin a v1 Group without groupId'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
pinnedConversationRecord.legacyGroupId = fromEncodedBinaryToArrayBuffer(
|
pinnedConversationRecord.legacyGroupId = Bytes.fromBinary(groupId);
|
||||||
groupId
|
|
||||||
);
|
|
||||||
} else if (isGroupV2(pinnedConversation.attributes)) {
|
} else if (isGroupV2(pinnedConversation.attributes)) {
|
||||||
pinnedConversationRecord.identifier = 'groupMasterKey';
|
pinnedConversationRecord.identifier = 'groupMasterKey';
|
||||||
const masterKey = pinnedConversation.get('masterKey');
|
const masterKey = pinnedConversation.get('masterKey');
|
||||||
|
@ -266,9 +258,7 @@ export async function toAccountRecord(
|
||||||
'toAccountRecord: trying to pin a v2 Group without masterKey'
|
'toAccountRecord: trying to pin a v2 Group without masterKey'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
pinnedConversationRecord.groupMasterKey = base64ToArrayBuffer(
|
pinnedConversationRecord.groupMasterKey = Bytes.fromBase64(masterKey);
|
||||||
masterKey
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return pinnedConversationRecord;
|
return pinnedConversationRecord;
|
||||||
|
@ -279,7 +269,7 @@ export async function toAccountRecord(
|
||||||
.filter(
|
.filter(
|
||||||
(
|
(
|
||||||
pinnedConversationClass
|
pinnedConversationClass
|
||||||
): pinnedConversationClass is PinnedConversationClass =>
|
): pinnedConversationClass is Proto.AccountRecord.PinnedConversation =>
|
||||||
pinnedConversationClass !== undefined
|
pinnedConversationClass !== undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -296,12 +286,10 @@ export async function toAccountRecord(
|
||||||
|
|
||||||
export async function toGroupV1Record(
|
export async function toGroupV1Record(
|
||||||
conversation: ConversationModel
|
conversation: ConversationModel
|
||||||
): Promise<GroupV1RecordClass> {
|
): Promise<Proto.GroupV1Record> {
|
||||||
const groupV1Record = new window.textsecure.protobuf.GroupV1Record();
|
const groupV1Record = new Proto.GroupV1Record();
|
||||||
|
|
||||||
groupV1Record.id = fromEncodedBinaryToArrayBuffer(
|
groupV1Record.id = Bytes.fromBinary(String(conversation.get('groupId')));
|
||||||
String(conversation.get('groupId'))
|
|
||||||
);
|
|
||||||
groupV1Record.blocked = conversation.isBlocked();
|
groupV1Record.blocked = conversation.isBlocked();
|
||||||
groupV1Record.whitelisted = Boolean(conversation.get('profileSharing'));
|
groupV1Record.whitelisted = Boolean(conversation.get('profileSharing'));
|
||||||
groupV1Record.archived = Boolean(conversation.get('isArchived'));
|
groupV1Record.archived = Boolean(conversation.get('isArchived'));
|
||||||
|
@ -317,12 +305,12 @@ export async function toGroupV1Record(
|
||||||
|
|
||||||
export async function toGroupV2Record(
|
export async function toGroupV2Record(
|
||||||
conversation: ConversationModel
|
conversation: ConversationModel
|
||||||
): Promise<GroupV2RecordClass> {
|
): Promise<Proto.GroupV2Record> {
|
||||||
const groupV2Record = new window.textsecure.protobuf.GroupV2Record();
|
const groupV2Record = new Proto.GroupV2Record();
|
||||||
|
|
||||||
const masterKey = conversation.get('masterKey');
|
const masterKey = conversation.get('masterKey');
|
||||||
if (masterKey !== undefined) {
|
if (masterKey !== undefined) {
|
||||||
groupV2Record.masterKey = base64ToArrayBuffer(masterKey);
|
groupV2Record.masterKey = Bytes.fromBase64(masterKey);
|
||||||
}
|
}
|
||||||
groupV2Record.blocked = conversation.isBlocked();
|
groupV2Record.blocked = conversation.isBlocked();
|
||||||
groupV2Record.whitelisted = Boolean(conversation.get('profileSharing'));
|
groupV2Record.whitelisted = Boolean(conversation.get('profileSharing'));
|
||||||
|
@ -337,14 +325,13 @@ export async function toGroupV2Record(
|
||||||
return groupV2Record;
|
return groupV2Record;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageRequestCapableRecord = ContactRecordClass | GroupV1RecordClass;
|
type MessageRequestCapableRecord = Proto.IContactRecord | Proto.IGroupV1Record;
|
||||||
|
|
||||||
function applyMessageRequestState(
|
function applyMessageRequestState(
|
||||||
record: MessageRequestCapableRecord,
|
record: MessageRequestCapableRecord,
|
||||||
conversation: ConversationModel
|
conversation: ConversationModel
|
||||||
): void {
|
): void {
|
||||||
const messageRequestEnum =
|
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||||
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
|
||||||
|
|
||||||
if (record.blocked) {
|
if (record.blocked) {
|
||||||
conversation.applyMessageRequestResponse(messageRequestEnum.BLOCK, {
|
conversation.applyMessageRequestResponse(messageRequestEnum.BLOCK, {
|
||||||
|
@ -394,15 +381,14 @@ function doRecordsConflict(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return localKeys.reduce((hasConflict: boolean, key: string): boolean => {
|
return localKeys.some((key: string): boolean => {
|
||||||
const localValue = localRecord[key];
|
const localValue = localRecord[key];
|
||||||
const remoteValue = remoteRecord[key];
|
const remoteValue = remoteRecord[key];
|
||||||
|
|
||||||
// Sometimes we have a ByteBuffer and an ArrayBuffer, this ensures that we
|
// Sometimes we have a ByteBuffer and an ArrayBuffer, this ensures that we
|
||||||
// are comparing them both equally by converting them into base64 string.
|
// are comparing them both equally by converting them into base64 string.
|
||||||
if (Object.prototype.toString.call(localValue) === '[object ArrayBuffer]') {
|
if (localValue instanceof Uint8Array) {
|
||||||
const areEqual =
|
const areEqual = Bytes.areEqual(localValue, remoteValue);
|
||||||
arrayBufferToBase64(localValue) === arrayBufferToBase64(remoteValue);
|
|
||||||
if (!areEqual) {
|
if (!areEqual) {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
'storageService.doRecordsConflict: Conflict found for ArrayBuffer',
|
'storageService.doRecordsConflict: Conflict found for ArrayBuffer',
|
||||||
|
@ -410,15 +396,18 @@ function doRecordsConflict(
|
||||||
idForLogging
|
idForLogging
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return hasConflict || !areEqual;
|
return !areEqual;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If both types are Long we can use Long's equals to compare them
|
// If both types are Long we can use Long's equals to compare them
|
||||||
if (
|
if (localValue instanceof Long || typeof localValue === 'number') {
|
||||||
window.dcodeIO.Long.isLong(localValue) &&
|
if (!(remoteValue instanceof Long) || typeof remoteValue !== 'number') {
|
||||||
window.dcodeIO.Long.isLong(remoteValue)
|
return true;
|
||||||
) {
|
}
|
||||||
const areEqual = localValue.equals(remoteValue);
|
|
||||||
|
const areEqual = Long.fromValue(localValue).equals(
|
||||||
|
Long.fromValue(remoteValue)
|
||||||
|
);
|
||||||
if (!areEqual) {
|
if (!areEqual) {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
'storageService.doRecordsConflict: Conflict found for Long',
|
'storageService.doRecordsConflict: Conflict found for Long',
|
||||||
|
@ -426,7 +415,7 @@ function doRecordsConflict(
|
||||||
idForLogging
|
idForLogging
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return hasConflict || !areEqual;
|
return !areEqual;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === 'pinnedConversations') {
|
if (key === 'pinnedConversations') {
|
||||||
|
@ -437,11 +426,11 @@ function doRecordsConflict(
|
||||||
idForLogging
|
idForLogging
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return hasConflict || !areEqual;
|
return !areEqual;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localValue === remoteValue) {
|
if (localValue === remoteValue) {
|
||||||
return hasConflict || false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sometimes we get `null` values from Protobuf and they should default to
|
// Sometimes we get `null` values from Protobuf and they should default to
|
||||||
|
@ -452,9 +441,9 @@ function doRecordsConflict(
|
||||||
(localValue === false ||
|
(localValue === false ||
|
||||||
localValue === '' ||
|
localValue === '' ||
|
||||||
localValue === 0 ||
|
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);
|
const areEqual = isEqual(localValue, remoteValue);
|
||||||
|
@ -468,7 +457,7 @@ function doRecordsConflict(
|
||||||
}
|
}
|
||||||
|
|
||||||
return !areEqual;
|
return !areEqual;
|
||||||
}, false);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function doesRecordHavePendingChanges(
|
function doesRecordHavePendingChanges(
|
||||||
|
@ -497,13 +486,13 @@ function doesRecordHavePendingChanges(
|
||||||
|
|
||||||
export async function mergeGroupV1Record(
|
export async function mergeGroupV1Record(
|
||||||
storageID: string,
|
storageID: string,
|
||||||
groupV1Record: GroupV1RecordClass
|
groupV1Record: Proto.IGroupV1Record
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!groupV1Record.id) {
|
if (!groupV1Record.id) {
|
||||||
throw new Error(`No ID for ${storageID}`);
|
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
|
// Attempt to fetch an existing group pertaining to the `groupId` or create
|
||||||
// a new group and populate it with the attributes from the record.
|
// 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
|
// 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
|
// retrieve the master key and find the conversation locally. If we
|
||||||
// are successful then we continue setting and applying state.
|
// 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 fields = deriveGroupFields(new FIXMEU8(masterKeyBuffer));
|
||||||
const derivedGroupV2Id = Bytes.toBase64(fields.id);
|
const derivedGroupV2Id = Bytes.toBase64(fields.id);
|
||||||
|
|
||||||
|
@ -599,12 +590,12 @@ export async function mergeGroupV1Record(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getGroupV2Conversation(
|
async function getGroupV2Conversation(
|
||||||
masterKeyBuffer: ArrayBuffer
|
masterKeyBuffer: Uint8Array
|
||||||
): Promise<ConversationModel> {
|
): Promise<ConversationModel> {
|
||||||
const groupFields = deriveGroupFields(new FIXMEU8(masterKeyBuffer));
|
const groupFields = deriveGroupFields(masterKeyBuffer);
|
||||||
|
|
||||||
const groupId = Bytes.toBase64(groupFields.id);
|
const groupId = Bytes.toBase64(groupFields.id);
|
||||||
const masterKey = arrayBufferToBase64(masterKeyBuffer);
|
const masterKey = Bytes.toBase64(masterKeyBuffer);
|
||||||
const secretParams = Bytes.toBase64(groupFields.secretParams);
|
const secretParams = Bytes.toBase64(groupFields.secretParams);
|
||||||
const publicParams = Bytes.toBase64(groupFields.publicParams);
|
const publicParams = Bytes.toBase64(groupFields.publicParams);
|
||||||
|
|
||||||
|
@ -647,13 +638,13 @@ async function getGroupV2Conversation(
|
||||||
|
|
||||||
export async function mergeGroupV2Record(
|
export async function mergeGroupV2Record(
|
||||||
storageID: string,
|
storageID: string,
|
||||||
groupV2Record: GroupV2RecordClass
|
groupV2Record: Proto.IGroupV2Record
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!groupV2Record.masterKey) {
|
if (!groupV2Record.masterKey) {
|
||||||
throw new Error(`No master key for ${storageID}`);
|
throw new Error(`No master key for ${storageID}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const masterKeyBuffer = groupV2Record.masterKey.toArrayBuffer();
|
const masterKeyBuffer = groupV2Record.masterKey;
|
||||||
const conversation = await getGroupV2Conversation(masterKeyBuffer);
|
const conversation = await getGroupV2Conversation(masterKeyBuffer);
|
||||||
|
|
||||||
window.log.info(
|
window.log.info(
|
||||||
|
@ -720,7 +711,7 @@ export async function mergeGroupV2Record(
|
||||||
|
|
||||||
export async function mergeContactRecord(
|
export async function mergeContactRecord(
|
||||||
storageID: string,
|
storageID: string,
|
||||||
originalContactRecord: ContactRecordClass
|
originalContactRecord: Proto.IContactRecord
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const contactRecord = {
|
const contactRecord = {
|
||||||
...originalContactRecord,
|
...originalContactRecord,
|
||||||
|
@ -757,10 +748,9 @@ export async function mergeContactRecord(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (contactRecord.profileKey) {
|
if (contactRecord.profileKey) {
|
||||||
await conversation.setProfileKey(
|
await conversation.setProfileKey(Bytes.toBase64(contactRecord.profileKey), {
|
||||||
arrayBufferToBase64(contactRecord.profileKey.toArrayBuffer()),
|
viaStorageServiceSync: true,
|
||||||
{ viaStorageServiceSync: true }
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const verified = await conversation.safeGetVerified();
|
const verified = await conversation.safeGetVerified();
|
||||||
|
@ -768,11 +758,11 @@ export async function mergeContactRecord(
|
||||||
if (verified !== storageServiceVerified) {
|
if (verified !== storageServiceVerified) {
|
||||||
const verifiedOptions = {
|
const verifiedOptions = {
|
||||||
key: contactRecord.identityKey
|
key: contactRecord.identityKey
|
||||||
? contactRecord.identityKey.toArrayBuffer()
|
? typedArrayToArrayBuffer(contactRecord.identityKey)
|
||||||
: undefined,
|
: undefined,
|
||||||
viaStorageServiceSync: true,
|
viaStorageServiceSync: true,
|
||||||
};
|
};
|
||||||
const STATE_ENUM = window.textsecure.protobuf.ContactRecord.IdentityState;
|
const STATE_ENUM = Proto.ContactRecord.IdentityState;
|
||||||
|
|
||||||
switch (storageServiceVerified) {
|
switch (storageServiceVerified) {
|
||||||
case STATE_ENUM.VERIFIED:
|
case STATE_ENUM.VERIFIED:
|
||||||
|
@ -816,7 +806,7 @@ export async function mergeContactRecord(
|
||||||
|
|
||||||
export async function mergeAccountRecord(
|
export async function mergeAccountRecord(
|
||||||
storageID: string,
|
storageID: string,
|
||||||
accountRecord: AccountRecordClass
|
accountRecord: Proto.IAccountRecord
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const {
|
const {
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
|
@ -855,7 +845,7 @@ export async function mergeAccountRecord(
|
||||||
setUniversalExpireTimer(universalExpireTimer || 0);
|
setUniversalExpireTimer(universalExpireTimer || 0);
|
||||||
|
|
||||||
const PHONE_NUMBER_SHARING_MODE_ENUM =
|
const PHONE_NUMBER_SHARING_MODE_ENUM =
|
||||||
window.textsecure.protobuf.AccountRecord.PhoneNumberSharingMode;
|
Proto.AccountRecord.PhoneNumberSharingMode;
|
||||||
let phoneNumberSharingModeToStore: PhoneNumberSharingMode;
|
let phoneNumberSharingModeToStore: PhoneNumberSharingMode;
|
||||||
switch (phoneNumberSharingMode) {
|
switch (phoneNumberSharingMode) {
|
||||||
case undefined:
|
case undefined:
|
||||||
|
@ -885,7 +875,7 @@ export async function mergeAccountRecord(
|
||||||
window.storage.put('phoneNumberDiscoverability', discoverability);
|
window.storage.put('phoneNumberDiscoverability', discoverability);
|
||||||
|
|
||||||
if (profileKey) {
|
if (profileKey) {
|
||||||
ourProfileKeyService.set(profileKey.toArrayBuffer());
|
ourProfileKeyService.set(typedArrayToArrayBuffer(profileKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pinnedConversations) {
|
if (pinnedConversations) {
|
||||||
|
@ -928,48 +918,29 @@ export async function mergeAccountRecord(
|
||||||
);
|
);
|
||||||
|
|
||||||
const remotelyPinnedConversationPromises = pinnedConversations.map(
|
const remotelyPinnedConversationPromises = pinnedConversations.map(
|
||||||
async pinnedConversation => {
|
async ({ contact, legacyGroupId, groupMasterKey }) => {
|
||||||
let conversationId;
|
let conversationId: string | undefined;
|
||||||
|
|
||||||
switch (pinnedConversation.identifier) {
|
if (contact) {
|
||||||
case 'contact': {
|
conversationId = window.ConversationController.ensureContactIds(
|
||||||
if (!pinnedConversation.contact) {
|
contact
|
||||||
throw new Error('mergeAccountRecord: no contact found');
|
);
|
||||||
}
|
} else if (legacyGroupId && legacyGroupId.length) {
|
||||||
conversationId = window.ConversationController.ensureContactIds(
|
conversationId = Bytes.toBinary(legacyGroupId);
|
||||||
pinnedConversation.contact
|
} else if (groupMasterKey && groupMasterKey.length) {
|
||||||
);
|
const groupFields = deriveGroupFields(groupMasterKey);
|
||||||
break;
|
const groupId = Bytes.toBase64(groupFields.id);
|
||||||
}
|
|
||||||
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);
|
|
||||||
const groupId = Bytes.toBase64(groupFields.id);
|
|
||||||
|
|
||||||
conversationId = groupId;
|
conversationId = groupId;
|
||||||
break;
|
} else {
|
||||||
}
|
window.log.error(
|
||||||
default: {
|
'storageService.mergeAccountRecord: Invalid identifier received'
|
||||||
window.log.error(
|
);
|
||||||
'storageService.mergeAccountRecord: Invalid identifier received'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!conversationId) {
|
if (!conversationId) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'storageService.mergeAccountRecord: missing conversation id. looking based on',
|
'storageService.mergeAccountRecord: missing conversation id.'
|
||||||
pinnedConversation.identifier
|
|
||||||
);
|
);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -1036,9 +1007,7 @@ export async function mergeAccountRecord(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (accountRecord.profileKey) {
|
if (accountRecord.profileKey) {
|
||||||
await conversation.setProfileKey(
|
await conversation.setProfileKey(Bytes.toBase64(accountRecord.profileKey));
|
||||||
arrayBufferToBase64(accountRecord.profileKey.toArrayBuffer())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (avatarUrl) {
|
if (avatarUrl) {
|
||||||
|
|
|
@ -3,38 +3,32 @@
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { arePinnedConversationsEqual } from '../../util/arePinnedConversationsEqual';
|
import { arePinnedConversationsEqual } from '../../util/arePinnedConversationsEqual';
|
||||||
import { PinnedConversationClass } from '../../textsecure.d';
|
import { SignalService as Proto } from '../../protobuf';
|
||||||
|
|
||||||
|
import PinnedConversation = Proto.AccountRecord.IPinnedConversation;
|
||||||
|
|
||||||
describe('arePinnedConversationsEqual', () => {
|
describe('arePinnedConversationsEqual', () => {
|
||||||
it('is equal if both have same values at same indices', () => {
|
it('is equal if both have same values at same indices', () => {
|
||||||
const localValue = [
|
const localValue = [
|
||||||
{
|
{
|
||||||
identifier: 'contact' as const,
|
|
||||||
contact: {
|
contact: {
|
||||||
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
||||||
e164: '+13055551234',
|
e164: '+13055551234',
|
||||||
},
|
},
|
||||||
toArrayBuffer: () => new ArrayBuffer(0),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: 'groupMasterKey' as const,
|
groupMasterKey: new Uint8Array(32),
|
||||||
groupMasterKey: new ArrayBuffer(32),
|
|
||||||
toArrayBuffer: () => new ArrayBuffer(0),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const remoteValue = [
|
const remoteValue = [
|
||||||
{
|
{
|
||||||
identifier: 'contact' as const,
|
|
||||||
contact: {
|
contact: {
|
||||||
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
||||||
e164: '+13055551234',
|
e164: '+13055551234',
|
||||||
},
|
},
|
||||||
toArrayBuffer: () => new ArrayBuffer(0),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: 'groupMasterKey' as const,
|
groupMasterKey: new Uint8Array(32),
|
||||||
groupMasterKey: new ArrayBuffer(32),
|
|
||||||
toArrayBuffer: () => new ArrayBuffer(0),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -44,38 +38,30 @@ describe('arePinnedConversationsEqual', () => {
|
||||||
it('is not equal if values are mixed', () => {
|
it('is not equal if values are mixed', () => {
|
||||||
const localValue = [
|
const localValue = [
|
||||||
{
|
{
|
||||||
identifier: 'contact' as const,
|
|
||||||
contact: {
|
contact: {
|
||||||
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
||||||
e164: '+13055551234',
|
e164: '+13055551234',
|
||||||
},
|
},
|
||||||
toArrayBuffer: () => new ArrayBuffer(0),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: 'contact' as const,
|
|
||||||
contact: {
|
contact: {
|
||||||
uuid: 'f59a9fed-9e91-4bb4-a015-d49e58b47e25',
|
uuid: 'f59a9fed-9e91-4bb4-a015-d49e58b47e25',
|
||||||
e164: '+17865554321',
|
e164: '+17865554321',
|
||||||
},
|
},
|
||||||
toArrayBuffer: () => new ArrayBuffer(0),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const remoteValue = [
|
const remoteValue = [
|
||||||
{
|
{
|
||||||
identifier: 'contact' as const,
|
|
||||||
contact: {
|
contact: {
|
||||||
uuid: 'f59a9fed-9e91-4bb4-a015-d49e58b47e25',
|
uuid: 'f59a9fed-9e91-4bb4-a015-d49e58b47e25',
|
||||||
e164: '+17865554321',
|
e164: '+17865554321',
|
||||||
},
|
},
|
||||||
toArrayBuffer: () => new ArrayBuffer(0),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: 'contact' as const,
|
|
||||||
contact: {
|
contact: {
|
||||||
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
||||||
e164: '+13055551234',
|
e164: '+13055551234',
|
||||||
},
|
},
|
||||||
toArrayBuffer: () => new ArrayBuffer(0),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -85,34 +71,28 @@ describe('arePinnedConversationsEqual', () => {
|
||||||
it('is not equal if lengths are not same', () => {
|
it('is not equal if lengths are not same', () => {
|
||||||
const localValue = [
|
const localValue = [
|
||||||
{
|
{
|
||||||
identifier: 'contact' as const,
|
|
||||||
contact: {
|
contact: {
|
||||||
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
||||||
e164: '+13055551234',
|
e164: '+13055551234',
|
||||||
},
|
},
|
||||||
toArrayBuffer: () => new ArrayBuffer(0),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const remoteValue: Array<PinnedConversationClass> = [];
|
const remoteValue: Array<PinnedConversation> = [];
|
||||||
assert.isFalse(arePinnedConversationsEqual(localValue, remoteValue));
|
assert.isFalse(arePinnedConversationsEqual(localValue, remoteValue));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is not equal if content does not match', () => {
|
it('is not equal if content does not match', () => {
|
||||||
const localValue = [
|
const localValue = [
|
||||||
{
|
{
|
||||||
identifier: 'contact' as const,
|
|
||||||
contact: {
|
contact: {
|
||||||
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
uuid: '72313cde-2784-4a6f-a92a-abbe23763a60',
|
||||||
e164: '+13055551234',
|
e164: '+13055551234',
|
||||||
},
|
},
|
||||||
toArrayBuffer: () => new ArrayBuffer(0),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const remoteValue = [
|
const remoteValue = [
|
||||||
{
|
{
|
||||||
identifier: 'groupMasterKey' as const,
|
groupMasterKey: new Uint8Array(32),
|
||||||
groupMasterKey: new ArrayBuffer(32),
|
|
||||||
toArrayBuffer: () => new ArrayBuffer(0),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
assert.isFalse(arePinnedConversationsEqual(localValue, remoteValue));
|
assert.isFalse(arePinnedConversationsEqual(localValue, remoteValue));
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
import Long from 'long';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getSafeLongFromTimestamp,
|
getSafeLongFromTimestamp,
|
||||||
|
@ -9,8 +10,6 @@ import {
|
||||||
} from '../../util/timestampLongUtils';
|
} from '../../util/timestampLongUtils';
|
||||||
|
|
||||||
describe('getSafeLongFromTimestamp', () => {
|
describe('getSafeLongFromTimestamp', () => {
|
||||||
const { Long } = window.dcodeIO;
|
|
||||||
|
|
||||||
it('returns zero when passed undefined', () => {
|
it('returns zero when passed undefined', () => {
|
||||||
assert(getSafeLongFromTimestamp(undefined).isZero());
|
assert(getSafeLongFromTimestamp(undefined).isZero());
|
||||||
});
|
});
|
||||||
|
@ -31,8 +30,6 @@ describe('getSafeLongFromTimestamp', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getTimestampFromLong', () => {
|
describe('getTimestampFromLong', () => {
|
||||||
const { Long } = window.dcodeIO;
|
|
||||||
|
|
||||||
it('returns zero when passed 0 Long', () => {
|
it('returns zero when passed 0 Long', () => {
|
||||||
assert.equal(getTimestampFromLong(Long.fromNumber(0)), 0);
|
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 { assert } from 'chai';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import Long from 'long';
|
||||||
import * as Bytes from '../../Bytes';
|
import * as Bytes from '../../Bytes';
|
||||||
import { typedArrayToArrayBuffer } from '../../Crypto';
|
import { typedArrayToArrayBuffer } from '../../Crypto';
|
||||||
import { SenderCertificateMode } from '../../textsecure/OutgoingMessage';
|
import { SenderCertificateMode } from '../../textsecure/OutgoingMessage';
|
||||||
|
@ -42,9 +43,7 @@ describe('SenderCertificateService', () => {
|
||||||
fakeValidCertificate = new SenderCertificate();
|
fakeValidCertificate = new SenderCertificate();
|
||||||
fakeValidCertificateExpiry = Date.now() + 604800000;
|
fakeValidCertificateExpiry = Date.now() + 604800000;
|
||||||
const certificate = new SenderCertificate.Certificate();
|
const certificate = new SenderCertificate.Certificate();
|
||||||
certificate.expires = global.window.dcodeIO.Long.fromNumber(
|
certificate.expires = Long.fromNumber(fakeValidCertificateExpiry);
|
||||||
fakeValidCertificateExpiry
|
|
||||||
);
|
|
||||||
fakeValidCertificate.certificate = SenderCertificate.Certificate.encode(
|
fakeValidCertificate.certificate = SenderCertificate.Certificate.encode(
|
||||||
certificate
|
certificate
|
||||||
).finish();
|
).finish();
|
||||||
|
@ -215,9 +214,7 @@ describe('SenderCertificateService', () => {
|
||||||
|
|
||||||
const expiredCertificate = new SenderCertificate();
|
const expiredCertificate = new SenderCertificate();
|
||||||
const certificate = new SenderCertificate.Certificate();
|
const certificate = new SenderCertificate.Certificate();
|
||||||
certificate.expires = global.window.dcodeIO.Long.fromNumber(
|
certificate.expires = Long.fromNumber(Date.now() - 1000);
|
||||||
Date.now() - 1000
|
|
||||||
);
|
|
||||||
expiredCertificate.certificate = SenderCertificate.Certificate.encode(
|
expiredCertificate.certificate = SenderCertificate.Certificate.encode(
|
||||||
certificate
|
certificate
|
||||||
).finish();
|
).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,
|
hmacSha256,
|
||||||
sha256,
|
sha256,
|
||||||
verifyHmacSha256,
|
verifyHmacSha256,
|
||||||
|
base64ToArrayBuffer,
|
||||||
|
typedArrayToArrayBuffer,
|
||||||
} from '../Crypto';
|
} from '../Crypto';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -335,10 +337,7 @@ const Crypto = {
|
||||||
encryptedProfileName: string,
|
encryptedProfileName: string,
|
||||||
key: ArrayBuffer
|
key: ArrayBuffer
|
||||||
): Promise<{ given: ArrayBuffer; family: ArrayBuffer | null }> {
|
): Promise<{ given: ArrayBuffer; family: ArrayBuffer | null }> {
|
||||||
const data = window.dcodeIO.ByteBuffer.wrap(
|
const data = base64ToArrayBuffer(encryptedProfileName);
|
||||||
encryptedProfileName,
|
|
||||||
'base64'
|
|
||||||
).toArrayBuffer();
|
|
||||||
return Crypto.decryptProfile(data, key).then(decrypted => {
|
return Crypto.decryptProfile(data, key).then(decrypted => {
|
||||||
const padded = new Uint8Array(decrypted);
|
const padded = new Uint8Array(decrypted);
|
||||||
|
|
||||||
|
@ -364,13 +363,9 @@ const Crypto = {
|
||||||
const foundFamilyName = familyEnd > givenEnd + 1;
|
const foundFamilyName = familyEnd > givenEnd + 1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
given: window.dcodeIO.ByteBuffer.wrap(padded)
|
given: typedArrayToArrayBuffer(padded.slice(0, givenEnd)),
|
||||||
.slice(0, givenEnd)
|
|
||||||
.toArrayBuffer(),
|
|
||||||
family: foundFamilyName
|
family: foundFamilyName
|
||||||
? window.dcodeIO.ByteBuffer.wrap(padded)
|
? typedArrayToArrayBuffer(padded.slice(givenEnd + 1, familyEnd))
|
||||||
.slice(givenEnd + 1, familyEnd)
|
|
||||||
.toArrayBuffer()
|
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,27 +6,14 @@
|
||||||
/* eslint-disable no-proto */
|
/* eslint-disable no-proto */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import { ByteBufferClass } from '../window.d';
|
|
||||||
|
|
||||||
let ByteBuffer: ByteBufferClass | undefined;
|
|
||||||
const arrayBuffer = new ArrayBuffer(0);
|
const arrayBuffer = new ArrayBuffer(0);
|
||||||
const uint8Array = new Uint8Array();
|
const uint8Array = new Uint8Array();
|
||||||
|
|
||||||
let StaticByteBufferProto: any;
|
|
||||||
const StaticArrayBufferProto = (arrayBuffer as any).__proto__;
|
const StaticArrayBufferProto = (arrayBuffer as any).__proto__;
|
||||||
const StaticUint8ArrayProto = (uint8Array as any).__proto__;
|
const StaticUint8ArrayProto = (uint8Array as any).__proto__;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
function getString(thing: any): string {
|
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 === Object(thing)) {
|
||||||
if (thing.__proto__ === StaticUint8ArrayProto) {
|
if (thing.__proto__ === StaticUint8ArrayProto) {
|
||||||
return String.fromCharCode.apply(null, thing);
|
return String.fromCharCode.apply(null, thing);
|
||||||
|
@ -34,9 +21,6 @@ function getString(thing: any): string {
|
||||||
if (thing.__proto__ === StaticArrayBufferProto) {
|
if (thing.__proto__ === StaticArrayBufferProto) {
|
||||||
return getString(new Uint8Array(thing));
|
return getString(new Uint8Array(thing));
|
||||||
}
|
}
|
||||||
if (thing.__proto__ === StaticByteBufferProto) {
|
|
||||||
return thing.toString('binary');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return thing;
|
return thing;
|
||||||
}
|
}
|
||||||
|
@ -48,8 +32,7 @@ function getStringable(thing: any): boolean {
|
||||||
typeof thing === 'boolean' ||
|
typeof thing === 'boolean' ||
|
||||||
(thing === Object(thing) &&
|
(thing === Object(thing) &&
|
||||||
(thing.__proto__ === StaticArrayBufferProto ||
|
(thing.__proto__ === StaticArrayBufferProto ||
|
||||||
thing.__proto__ === StaticUint8ArrayProto ||
|
thing.__proto__ === StaticUint8ArrayProto))
|
||||||
thing.__proto__ === StaticByteBufferProto))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -193,7 +193,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
|
|
||||||
server: WebAPIType;
|
server: WebAPIType;
|
||||||
|
|
||||||
serverTrustRoot: ArrayBuffer;
|
serverTrustRoot: Uint8Array;
|
||||||
|
|
||||||
signalingKey: ArrayBuffer;
|
signalingKey: ArrayBuffer;
|
||||||
|
|
||||||
|
@ -239,9 +239,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
if (!options.serverTrustRoot) {
|
if (!options.serverTrustRoot) {
|
||||||
throw new Error('Server trust root is required!');
|
throw new Error('Server trust root is required!');
|
||||||
}
|
}
|
||||||
this.serverTrustRoot = MessageReceiverInner.stringToArrayBufferBase64(
|
this.serverTrustRoot = Bytes.fromBase64(options.serverTrustRoot);
|
||||||
options.serverTrustRoot
|
|
||||||
);
|
|
||||||
|
|
||||||
this.number_id = oldUsername
|
this.number_id = oldUsername
|
||||||
? utils.unencodeNumber(oldUsername)[0]
|
? 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> {
|
async connect(socket?: WebSocket): Promise<void> {
|
||||||
if (this.calledClose) {
|
if (this.calledClose) {
|
||||||
return;
|
return;
|
||||||
|
@ -2479,8 +2465,8 @@ class MessageReceiverInner extends EventTarget {
|
||||||
|
|
||||||
const paddedData = await Crypto.decryptAttachment(
|
const paddedData = await Crypto.decryptAttachment(
|
||||||
encrypted,
|
encrypted,
|
||||||
MessageReceiverInner.stringToArrayBufferBase64(key),
|
typedArrayToArrayBuffer(Bytes.fromBase64(key)),
|
||||||
MessageReceiverInner.stringToArrayBufferBase64(digest)
|
typedArrayToArrayBuffer(Bytes.fromBase64(digest))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isNumber(size)) {
|
if (!isNumber(size)) {
|
||||||
|
@ -2688,14 +2674,4 @@ export default class MessageReceiver {
|
||||||
checkSocket: () => void;
|
checkSocket: () => void;
|
||||||
|
|
||||||
getProcessedCount: () => number;
|
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 { v4 as getGuid } from 'uuid';
|
||||||
import { client as WebSocketClient, connection as WebSocket } from 'websocket';
|
import { client as WebSocketClient, connection as WebSocket } from 'websocket';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import Long from 'long';
|
||||||
|
|
||||||
import { Long } from '../window.d';
|
|
||||||
import { assert } from '../util/assert';
|
import { assert } from '../util/assert';
|
||||||
import { getUserAgent } from '../util/getUserAgent';
|
import { getUserAgent } from '../util/getUserAgent';
|
||||||
import { toWebSafeBase64 } from '../util/webSafeBase64';
|
import { toWebSafeBase64 } from '../util/webSafeBase64';
|
||||||
import { isPackIdValid, redactPackId } from '../types/Stickers';
|
import { isPackIdValid, redactPackId } from '../types/Stickers';
|
||||||
|
import * as Bytes from '../Bytes';
|
||||||
import {
|
import {
|
||||||
arrayBufferToBase64,
|
arrayBufferToBase64,
|
||||||
base64ToArrayBuffer,
|
base64ToArrayBuffer,
|
||||||
bytesFromHexString,
|
|
||||||
bytesFromString,
|
bytesFromString,
|
||||||
concatenateBytes,
|
concatenateBytes,
|
||||||
constantTimeEqual,
|
constantTimeEqual,
|
||||||
decryptAesGcm,
|
decryptAesGcm,
|
||||||
deriveSecrets,
|
deriveSecrets,
|
||||||
encryptCdsDiscoveryRequest,
|
encryptCdsDiscoveryRequest,
|
||||||
getBytes,
|
|
||||||
getRandomValue,
|
getRandomValue,
|
||||||
splitUuids,
|
splitUuids,
|
||||||
typedArrayToArrayBuffer,
|
typedArrayToArrayBuffer,
|
||||||
|
@ -84,7 +83,7 @@ type SgxConstantsType = {
|
||||||
let sgxConstantCache: SgxConstantsType | null = null;
|
let sgxConstantCache: SgxConstantsType | null = null;
|
||||||
|
|
||||||
function makeLong(value: string): Long {
|
function makeLong(value: string): Long {
|
||||||
return window.dcodeIO.Long.fromString(value);
|
return Long.fromString(value);
|
||||||
}
|
}
|
||||||
function getSgxConstants() {
|
function getSgxConstants() {
|
||||||
if (sgxConstantCache) {
|
if (sgxConstantCache) {
|
||||||
|
@ -2434,34 +2433,38 @@ export function initialize({
|
||||||
|
|
||||||
function validateAttestationQuote({
|
function validateAttestationQuote({
|
||||||
serverStaticPublic,
|
serverStaticPublic,
|
||||||
quote,
|
quote: quoteArrayBuffer,
|
||||||
}: {
|
}: {
|
||||||
serverStaticPublic: ArrayBuffer;
|
serverStaticPublic: ArrayBuffer;
|
||||||
quote: ArrayBuffer;
|
quote: ArrayBuffer;
|
||||||
}) {
|
}) {
|
||||||
const SGX_CONSTANTS = getSgxConstants();
|
const SGX_CONSTANTS = getSgxConstants();
|
||||||
const byteBuffer = window.dcodeIO.ByteBuffer.wrap(
|
const quote = Buffer.from(quoteArrayBuffer);
|
||||||
quote,
|
|
||||||
'binary',
|
|
||||||
window.dcodeIO.ByteBuffer.LITTLE_ENDIAN
|
|
||||||
);
|
|
||||||
|
|
||||||
const quoteVersion = byteBuffer.readShort(0) & 0xffff;
|
let off = 0;
|
||||||
|
|
||||||
|
const quoteVersion = quote.readInt32LE(off) & 0xffff;
|
||||||
|
off += 4;
|
||||||
if (quoteVersion < 0 || quoteVersion > 2) {
|
if (quoteVersion < 0 || quoteVersion > 2) {
|
||||||
throw new Error(`Unknown version ${quoteVersion}`);
|
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)) {
|
if (!miscSelect.every(byte => byte === 0)) {
|
||||||
throw new Error('Quote miscSelect invalid!');
|
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)) {
|
if (!reserved1.every(byte => byte === 0)) {
|
||||||
throw new Error('Quote reserved1 invalid!');
|
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 (
|
if (
|
||||||
flags.and(SGX_CONSTANTS.SGX_FLAGS_RESERVED).notEquals(0) ||
|
flags.and(SGX_CONSTANTS.SGX_FLAGS_RESERVED).notEquals(0) ||
|
||||||
flags.and(SGX_CONSTANTS.SGX_FLAGS_INITTED).equals(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()}`);
|
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)) {
|
if (xfrm.and(SGX_CONSTANTS.SGX_XFRM_RESERVED).notEquals(0)) {
|
||||||
throw new Error(`Quote xfrm invalid ${xfrm}`);
|
throw new Error(`Quote xfrm invalid ${xfrm}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mrenclave = new Uint8Array(getBytes(quote, 112, 32));
|
const mrenclave = quote.slice(off, off + 32);
|
||||||
const enclaveIdBytes = new Uint8Array(
|
off += 32;
|
||||||
bytesFromHexString(directoryEnclaveId)
|
const enclaveIdBytes = Bytes.fromHex(directoryEnclaveId);
|
||||||
);
|
if (mrenclave.compare(enclaveIdBytes) !== 0) {
|
||||||
if (!mrenclave.every((byte, index) => byte === enclaveIdBytes[index])) {
|
|
||||||
throw new Error('Quote mrenclave invalid!');
|
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)) {
|
if (!reserved2.every(byte => byte === 0)) {
|
||||||
throw new Error('Quote reserved2 invalid!');
|
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);
|
const serverStaticPublicBytes = new Uint8Array(serverStaticPublic);
|
||||||
if (
|
if (
|
||||||
!reportData.every((byte, index) => {
|
!reportData.every((byte, index) => {
|
||||||
|
@ -2501,22 +2508,26 @@ export function initialize({
|
||||||
throw new Error('Quote report_data invalid!');
|
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)) {
|
if (!reserved3.every(byte => byte === 0)) {
|
||||||
throw new Error('Quote reserved3 invalid!');
|
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)) {
|
if (!reserved4.every(byte => byte === 0)) {
|
||||||
throw new Error('Quote reserved4 invalid!');
|
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) {
|
if (signatureLength !== quote.byteLength - 436) {
|
||||||
throw new Error(`Bad signatureLength ${signatureLength}`);
|
throw new Error(`Bad signatureLength ${signatureLength}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// const signature = Uint8Array.from(getBytes(quote, 436, signatureLength));
|
// const signature = quote.slice(off, signatureLength);
|
||||||
|
// off += signatureLength
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateAttestationSignatureBody(
|
function validateAttestationSignatureBody(
|
||||||
|
|
|
@ -1,46 +1,51 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { arrayBufferToBase64 } from '../Crypto';
|
import * as Bytes from '../Bytes';
|
||||||
import { PinnedConversationClass } from '../textsecure.d';
|
|
||||||
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
|
||||||
|
import PinnedConversation = Proto.AccountRecord.IPinnedConversation;
|
||||||
|
|
||||||
export function arePinnedConversationsEqual(
|
export function arePinnedConversationsEqual(
|
||||||
localValue: Array<PinnedConversationClass>,
|
localValue: Array<PinnedConversation>,
|
||||||
remoteValue: Array<PinnedConversationClass>
|
remoteValue: Array<PinnedConversation>
|
||||||
): boolean {
|
): boolean {
|
||||||
if (localValue.length !== remoteValue.length) {
|
if (localValue.length !== remoteValue.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return localValue.every(
|
return localValue.every(
|
||||||
(localPinnedConversation: PinnedConversationClass, index: number) => {
|
(localPinnedConversation: PinnedConversation, index: number) => {
|
||||||
const remotePinnedConversation = remoteValue[index];
|
const remotePinnedConversation = remoteValue[index];
|
||||||
if (
|
|
||||||
localPinnedConversation.identifier !==
|
const {
|
||||||
remotePinnedConversation.identifier
|
contact,
|
||||||
) {
|
groupMasterKey,
|
||||||
return false;
|
legacyGroupId,
|
||||||
|
} = localPinnedConversation;
|
||||||
|
|
||||||
|
if (contact) {
|
||||||
|
return (
|
||||||
|
remotePinnedConversation.contact &&
|
||||||
|
contact.uuid === remotePinnedConversation.contact.uuid
|
||||||
|
);
|
||||||
}
|
}
|
||||||
switch (localPinnedConversation.identifier) {
|
|
||||||
case 'contact':
|
if (groupMasterKey && groupMasterKey.length) {
|
||||||
return (
|
return Bytes.areEqual(
|
||||||
localPinnedConversation.contact &&
|
groupMasterKey,
|
||||||
remotePinnedConversation.contact &&
|
remotePinnedConversation.groupMasterKey
|
||||||
localPinnedConversation.contact.uuid ===
|
);
|
||||||
remotePinnedConversation.contact.uuid
|
|
||||||
);
|
|
||||||
case 'groupMasterKey':
|
|
||||||
return (
|
|
||||||
arrayBufferToBase64(localPinnedConversation.groupMasterKey) ===
|
|
||||||
arrayBufferToBase64(remotePinnedConversation.groupMasterKey)
|
|
||||||
);
|
|
||||||
case 'legacyGroupId':
|
|
||||||
return (
|
|
||||||
arrayBufferToBase64(localPinnedConversation.legacyGroupId) ===
|
|
||||||
arrayBufferToBase64(remotePinnedConversation.legacyGroupId)
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (legacyGroupId && legacyGroupId.length) {
|
||||||
|
return Bytes.areEqual(
|
||||||
|
legacyGroupId,
|
||||||
|
remotePinnedConversation.legacyGroupId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2021-05-20T20:01:50.505Z"
|
"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(",
|
"rule": "jQuery-html(",
|
||||||
"path": "node_modules/marked/lib/marked.js",
|
"path": "node_modules/marked/lib/marked.js",
|
||||||
|
@ -12995,13 +12988,6 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2021-05-07T18:18:03.022Z"
|
"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-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "node_modules/uri-js/dist/es5/uri.all.min.js",
|
"path": "node_modules/uri-js/dist/es5/uri.all.min.js",
|
||||||
|
@ -14164,4 +14150,4 @@
|
||||||
"updated": "2021-03-18T21:41:28.361Z",
|
"updated": "2021-03-18T21:41:28.361Z",
|
||||||
"reasonDetail": "A generic hook. Typically not to be used with non-DOM values."
|
"reasonDetail": "A generic hook. Typically not to be used with non-DOM values."
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -244,6 +244,8 @@ const excludedFilesRegexps = [
|
||||||
'^node_modules/xmldom/.+',
|
'^node_modules/xmldom/.+',
|
||||||
'^node_modules/yargs-unparser/',
|
'^node_modules/yargs-unparser/',
|
||||||
'^node_modules/yargs/.+',
|
'^node_modules/yargs/.+',
|
||||||
|
'^node_modules/find-yarn-workspace-root/.+',
|
||||||
|
'^node_modules/update-notifier/.+',
|
||||||
|
|
||||||
// Used by Storybook
|
// Used by Storybook
|
||||||
'^node_modules/@emotion/.+',
|
'^node_modules/@emotion/.+',
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 {
|
export function getSafeLongFromTimestamp(timestamp = 0): Long {
|
||||||
if (timestamp >= Number.MAX_SAFE_INTEGER) {
|
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) {
|
if (!value) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const num = value.toNumber();
|
const num = normalizeNumber(value);
|
||||||
|
|
||||||
if (num >= Number.MAX_SAFE_INTEGER) {
|
if (num >= Number.MAX_SAFE_INTEGER) {
|
||||||
return 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,
|
ReactionAttributesType,
|
||||||
ReactionModelType,
|
ReactionModelType,
|
||||||
} from './model-types.d';
|
} from './model-types.d';
|
||||||
import {
|
import { TextSecureType, DownloadAttachmentType } from './textsecure.d';
|
||||||
ContactRecordIdentityState,
|
|
||||||
TextSecureType,
|
|
||||||
DownloadAttachmentType,
|
|
||||||
} from './textsecure.d';
|
|
||||||
import { Storage } from './textsecure/Storage';
|
import { Storage } from './textsecure/Storage';
|
||||||
import {
|
import {
|
||||||
ChallengeHandler,
|
ChallengeHandler,
|
||||||
|
@ -177,7 +173,6 @@ declare global {
|
||||||
baseAttachmentsPath: string;
|
baseAttachmentsPath: string;
|
||||||
baseStickersPath: string;
|
baseStickersPath: string;
|
||||||
baseTempPath: string;
|
baseTempPath: string;
|
||||||
dcodeIO: DCodeIOType;
|
|
||||||
receivedAtCounter: number;
|
receivedAtCounter: number;
|
||||||
enterKeyboardMode: () => void;
|
enterKeyboardMode: () => void;
|
||||||
enterMouseMode: () => 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 {
|
export class CertificateValidatorType {
|
||||||
validate: (cerficate: any, certificateTime: number) => Promise<void>;
|
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 {
|
export class GumVideoCapturer {
|
||||||
constructor(
|
constructor(
|
||||||
maxWidth: number,
|
maxWidth: number,
|
||||||
|
|
84
yarn.lock
84
yarn.lock
|
@ -4824,7 +4824,7 @@ boom@2.x.x:
|
||||||
dependencies:
|
dependencies:
|
||||||
hoek "2.x.x"
|
hoek "2.x.x"
|
||||||
|
|
||||||
boxen@^1.2.1, boxen@^1.3.0:
|
boxen@^1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
|
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
|
||||||
integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==
|
integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==
|
||||||
|
@ -5875,18 +5875,6 @@ config@1.28.1:
|
||||||
json5 "0.4.0"
|
json5 "0.4.0"
|
||||||
os-homedir "1.0.2"
|
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:
|
configstore@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
|
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"
|
no-case "^3.0.4"
|
||||||
tslib "^2.0.3"
|
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:
|
dot-prop@^5.2.0:
|
||||||
version "5.2.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb"
|
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"
|
locate-path "^5.0.0"
|
||||||
path-exists "^4.0.0"
|
path-exists "^4.0.0"
|
||||||
|
|
||||||
find-yarn-workspace-root@^1.2.1:
|
find-yarn-workspace-root@^2.0.0:
|
||||||
version "1.2.1"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz#40eb8e6e7c2502ddfaa2577c176f221422f860db"
|
resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd"
|
||||||
integrity sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q==
|
integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
fs-extra "^4.0.3"
|
micromatch "^4.0.2"
|
||||||
micromatch "^3.1.4"
|
|
||||||
|
|
||||||
findup-sync@^4.0.0:
|
findup-sync@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
|
@ -8727,15 +8707,6 @@ fs-extra@^2.0.0:
|
||||||
graceful-fs "^4.1.2"
|
graceful-fs "^4.1.2"
|
||||||
jsonfile "^2.1.0"
|
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:
|
fs-extra@^7.0.1:
|
||||||
version "7.0.1"
|
version "7.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
|
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"
|
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
||||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
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:
|
is-obj@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
|
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
|
||||||
|
@ -11317,7 +11283,7 @@ language-tags@^1.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
language-subtag-registry "~0.3.2"
|
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"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
|
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
|
||||||
integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=
|
integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=
|
||||||
|
@ -13196,6 +13162,14 @@ open@^7.0.3:
|
||||||
is-docker "^2.0.0"
|
is-docker "^2.0.0"
|
||||||
is-wsl "^2.1.1"
|
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:
|
opn@^5.5.0:
|
||||||
version "5.5.0"
|
version "5.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
|
resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
|
||||||
|
@ -13656,24 +13630,24 @@ pascalcase@^0.1.1:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
|
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
|
||||||
|
|
||||||
patch-package@6.1.2:
|
patch-package@6.4.7:
|
||||||
version "6.1.2"
|
version "6.4.7"
|
||||||
resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.1.2.tgz#9ed0b3defb5c34ecbef3f334ddfb13e01b3d3ff6"
|
resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.4.7.tgz#2282d53c397909a0d9ef92dae3fdeb558382b148"
|
||||||
integrity sha512-5GnzR8lEyeleeariG+hGabUnD2b1yL7AIGFjlLo95zMGRWhZCel58IpeKD46wwPb7i+uNhUI8unV56ogk8Bgqg==
|
integrity sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@yarnpkg/lockfile" "^1.1.0"
|
"@yarnpkg/lockfile" "^1.1.0"
|
||||||
chalk "^2.4.2"
|
chalk "^2.4.2"
|
||||||
cross-spawn "^6.0.5"
|
cross-spawn "^6.0.5"
|
||||||
find-yarn-workspace-root "^1.2.1"
|
find-yarn-workspace-root "^2.0.0"
|
||||||
fs-extra "^7.0.1"
|
fs-extra "^7.0.1"
|
||||||
is-ci "^2.0.0"
|
is-ci "^2.0.0"
|
||||||
klaw-sync "^6.0.0"
|
klaw-sync "^6.0.0"
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
|
open "^7.4.2"
|
||||||
rimraf "^2.6.3"
|
rimraf "^2.6.3"
|
||||||
semver "^5.6.0"
|
semver "^5.6.0"
|
||||||
slash "^2.0.0"
|
slash "^2.0.0"
|
||||||
tmp "^0.0.33"
|
tmp "^0.0.33"
|
||||||
update-notifier "^2.5.0"
|
|
||||||
|
|
||||||
path-browserify@0.0.1:
|
path-browserify@0.0.1:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068"
|
||||||
integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==
|
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:
|
update-notifier@^5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9"
|
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue