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