Fully move to protobufjs

This commit is contained in:
Fedor Indutny 2021-07-13 11:54:53 -07:00 committed by GitHub
parent 20ea409d9e
commit 570fb182d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1133 additions and 12401 deletions

View file

@ -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>

View file

@ -13,12 +13,6 @@
"devDependencies": { "devDependencies": {
}, },
"preen": { "preen": {
"bytebuffer": [
"dist/ByteBufferAB.js"
],
"long": [
"dist/Long.js"
],
"mp3lameencoder": [ "mp3lameencoder": [
"lib/Mp3LameEncoder.js" "lib/Mp3LameEncoder.js"
], ],

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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();
});
};

View file

@ -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 = [];
}
}
);
});
})();

View file

@ -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,
}, },
}; };

View file

@ -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;

View file

@ -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');
});
});
});
});
});

View file

@ -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>

View file

@ -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",

View 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

View file

@ -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));

View file

@ -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>

View file

@ -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');

View file

@ -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,
}, },

View file

@ -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;

View file

@ -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))
)
)
);
});
});
});

View file

@ -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>

View file

@ -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');
});
});

View file

@ -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),

View file

@ -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);
}

View file

@ -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();
} }

View file

@ -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
View file

@ -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;

View file

@ -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

View file

@ -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) {

View file

@ -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));

View file

@ -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);
}); });

View 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)))
)
);
});
});
});

View file

@ -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();

View file

@ -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));
});
});

View 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

File diff suppressed because it is too large Load diff

View file

@ -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,
}; };
}); });

View file

@ -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))
); );
} }

View file

@ -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;
} }

View file

@ -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(

View file

@ -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;
} }
); );
} }

View file

@ -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;
}

View file

@ -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."
} }
] ]

View file

@ -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/.+',

View file

@ -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
View file

@ -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,

View file

@ -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"