Fully move to protobufjs

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

View File

@ -347,7 +347,6 @@
type="text/javascript"
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>

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,63 +0,0 @@
// Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global window, postMessage, textsecure, close */
/* eslint-disable more/no-then, no-global-assign, no-restricted-globals, no-unused-vars */
/*
* Load this script in a Web Worker to generate new prekeys without
* tying up the main thread.
* https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
*
* Because workers don't have access to the window or localStorage, we
* create our own version that proxies back to the caller for actual
* storage.
*
* Example usage:
*
var myWorker = new Worker('/js/key_worker.js');
myWorker.onmessage = function(e) {
switch(e.data.method) {
case 'set':
localStorage.setItem(e.data.key, e.data.value);
break;
case 'remove':
localStorage.removeItem(e.data.key);
break;
case 'done':
console.error(e.data.keys);
}
};
*/
let store = {};
window.textsecure.storage.impl = {
/** ***************************
*** Override Storage Routines ***
**************************** */
put(key, value) {
if (value === undefined) throw new Error('Tried to store undefined');
store[key] = value;
postMessage({ method: 'set', key, value });
},
get(key, defaultValue) {
if (key in store) {
return store[key];
}
return defaultValue;
},
remove(key) {
delete store[key];
postMessage({ method: 'remove', key });
},
};
// eslint-disable-next-line no-undef
onmessage = e => {
store = e.data;
textsecure.protocol_wrapper.generateKeys().then(keys => {
postMessage({ method: 'done', keys });
close();
});
};

View File

@ -1,70 +0,0 @@
// Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global window, dcodeIO, textsecure */
// eslint-disable-next-line func-names
(function () {
const FILES_TO_LOAD = [
'SignalService.proto',
'SignalStorage.proto',
'SubProtocol.proto',
'DeviceMessages.proto',
'Stickers.proto',
// Just for encrypting device names
'DeviceName.proto',
// Metadata-specific protos
'UnidentifiedDelivery.proto',
// Groups
'Groups.proto',
];
let remainingFilesToLoad = FILES_TO_LOAD.length;
const hasFinishedLoading = () => remainingFilesToLoad <= 0;
let onLoadCallbacks = [];
window.textsecure = window.textsecure || {};
window.textsecure.protobuf = {
onLoad: callback => {
if (hasFinishedLoading()) {
setTimeout(callback, 0);
} else {
onLoadCallbacks.push(callback);
}
},
};
FILES_TO_LOAD.forEach(filename => {
dcodeIO.ProtoBuf.loadProtoFile(
{ root: window.PROTO_ROOT, file: filename },
(error, result) => {
if (error) {
const text = `Error loading protos from ${filename} (root: ${
window.PROTO_ROOT
}) ${error && error.stack ? error.stack : error}`;
window.log.error(text);
throw error;
}
const protos = result.build('signalservice');
if (!protos) {
const text = `Error loading protos from ${filename} - no exported types! (root: ${window.PROTO_ROOT})`;
window.log.error(text);
throw new Error(text);
}
// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const protoName in protos) {
textsecure.protobuf[protoName] = protos[protoName];
}
remainingFilesToLoad -= 1;
if (hasFinishedLoading()) {
onLoadCallbacks.forEach(callback => callback());
onLoadCallbacks = [];
}
}
);
});
})();

View File

@ -17,10 +17,8 @@ module.exports = {
globals: {
assert: true,
assertEqualArrayBuffers: true,
dcodeIO: true,
getString: true,
hexToArrayBuffer: true,
PROTO_ROOT: true,
stringToArrayBuffer: true,
},
};

View File

@ -5,7 +5,6 @@
mocha.setup('bdd');
window.assert = chai.assert;
window.PROTO_ROOT = '../../protos';
const OriginalReporter = mocha._reporter;

View File

@ -1,144 +0,0 @@
// Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global textsecure */
describe('encrypting and decrypting profile data', () => {
const NAME_PADDED_LENGTH = 53;
describe('encrypting and decrypting profile names', () => {
it('pads, encrypts, decrypts, and unpads a short string', () => {
const name = 'Alice';
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
const key = window.Signal.Crypto.getRandomBytes(32);
return textsecure.crypto
.encryptProfileName(buffer, key)
.then(encrypted => {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto
.decryptProfileName(encrypted, key)
.then(({ given, family }) => {
assert.strictEqual(family, null);
assert.strictEqual(
dcodeIO.ByteBuffer.wrap(given).toString('utf8'),
name
);
});
});
});
it('handles a given name of the max, 53 characters', () => {
const name = '01234567890123456789012345678901234567890123456789123';
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
const key = window.Signal.Crypto.getRandomBytes(32);
return textsecure.crypto
.encryptProfileName(buffer, key)
.then(encrypted => {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto
.decryptProfileName(encrypted, key)
.then(({ given, family }) => {
assert.strictEqual(
dcodeIO.ByteBuffer.wrap(given).toString('utf8'),
name
);
assert.strictEqual(family, null);
});
});
});
it('handles family/given name of the max, 53 characters', () => {
const name = '01234567890123456789\u000001234567890123456789012345678912';
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
const key = window.Signal.Crypto.getRandomBytes(32);
return textsecure.crypto
.encryptProfileName(buffer, key)
.then(encrypted => {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto
.decryptProfileName(encrypted, key)
.then(({ given, family }) => {
assert.strictEqual(
dcodeIO.ByteBuffer.wrap(given).toString('utf8'),
'01234567890123456789'
);
assert.strictEqual(
dcodeIO.ByteBuffer.wrap(family).toString('utf8'),
'01234567890123456789012345678912'
);
});
});
});
it('handles a string with family/given name', () => {
const name = 'Alice\0Jones';
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
const key = window.Signal.Crypto.getRandomBytes(32);
return textsecure.crypto
.encryptProfileName(buffer, key)
.then(encrypted => {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto
.decryptProfileName(encrypted, key)
.then(({ given, family }) => {
assert.strictEqual(
dcodeIO.ByteBuffer.wrap(given).toString('utf8'),
'Alice'
);
assert.strictEqual(
dcodeIO.ByteBuffer.wrap(family).toString('utf8'),
'Jones'
);
});
});
});
it('works for empty string', async () => {
const name = dcodeIO.ByteBuffer.wrap('').toArrayBuffer();
const key = window.Signal.Crypto.getRandomBytes(32);
const encrypted = await textsecure.crypto.encryptProfileName(name, key);
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
const { given, family } = await textsecure.crypto.decryptProfileName(
encrypted,
key
);
assert.strictEqual(family, null);
assert.strictEqual(given.byteLength, 0);
assert.strictEqual(dcodeIO.ByteBuffer.wrap(given).toString('utf8'), '');
});
});
describe('encrypting and decrypting profile avatars', () => {
it('encrypts and decrypts', () => {
const buffer = dcodeIO.ByteBuffer.wrap(
'This is an avatar'
).toArrayBuffer();
const key = window.Signal.Crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfile(buffer, key).then(encrypted => {
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
return textsecure.crypto
.decryptProfile(encrypted, key)
.then(decrypted => {
assertEqualArrayBuffers(buffer, decrypted);
});
});
});
it('throws when decrypting with the wrong key', () => {
const buffer = dcodeIO.ByteBuffer.wrap(
'This is an avatar'
).toArrayBuffer();
const key = window.Signal.Crypto.getRandomBytes(32);
const badKey = window.Signal.Crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfile(buffer, key).then(encrypted => {
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
return textsecure.crypto
.decryptProfile(encrypted, badKey)
.catch(error => {
assert.strictEqual(error.name, 'ProfileDecryptError');
});
});
});
});
});

View File

@ -20,7 +20,6 @@
></script>
<script 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>

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

View File

@ -0,0 +1,84 @@
diff --git a/node_modules/protobufjs/cli/lib/tsd-jsdoc/publish.js b/node_modules/protobufjs/cli/lib/tsd-jsdoc/publish.js
index 3846a99..6c5688a 100644
--- a/node_modules/protobufjs/cli/lib/tsd-jsdoc/publish.js
+++ b/node_modules/protobufjs/cli/lib/tsd-jsdoc/publish.js
@@ -558,6 +558,13 @@ function handleClass(element, parent) {
handleElement(child, element);
});
+ writeln();
+ if (is_interface) {
+ writeln("__unknownFields?: ReadonlyArray<Uint8Array>;");
+ } else {
+ writeln("public __unknownFields?: ReadonlyArray<Uint8Array>;");
+ }
+
--indent;
writeln("}");
diff --git a/node_modules/protobufjs/src/decoder.js b/node_modules/protobufjs/src/decoder.js
index 491dd30..ec03e9f 100644
--- a/node_modules/protobufjs/src/decoder.js
+++ b/node_modules/protobufjs/src/decoder.js
@@ -21,6 +21,7 @@ function decoder(mtype) {
("r=Reader.create(r)")
("var c=l===undefined?r.len:r.pos+l,m=new this.ctor" + (mtype.fieldsArray.filter(function(field) { return field.map; }).length ? ",k,value" : ""))
("while(r.pos<c){")
+ ("var unknownStartPos = r.pos")
("var t=r.uint32()");
if (mtype.group) gen
("if((t&7)===4)")
@@ -28,6 +29,8 @@ function decoder(mtype) {
gen
("switch(t>>>3){");
+ var unknownRef = "m" + util.safeProp("__unknownFields");
+
var i = 0;
for (; i < /* initializes */ mtype.fieldsArray.length; ++i) {
var field = mtype._fieldsArray[i].resolve(),
@@ -109,6 +112,11 @@ function decoder(mtype) {
} gen
("default:")
("r.skipType(t&7)")
+ ("if (!(%s)) {", unknownRef)
+ ("%s = []", unknownRef)
+ ("}")
+
+ ("%s.push(r.buf.slice(unknownStartPos, r.pos))", unknownRef)
("break")
("}")
diff --git a/node_modules/protobufjs/src/encoder.js b/node_modules/protobufjs/src/encoder.js
index c803e99..d3c6e86 100644
--- a/node_modules/protobufjs/src/encoder.js
+++ b/node_modules/protobufjs/src/encoder.js
@@ -94,6 +94,13 @@ function encoder(mtype) {
}
}
+ var unknownRef = "m" + util.safeProp("__unknownFields");
+ gen
+ ("if (%s) {", unknownRef)
+ ("for (var i=0;i<%s.length;++i)", unknownRef)
+ ("w.__unknownField(%s[i])", unknownRef)
+ ("}")
+
return gen
("return w");
/* eslint-enable no-unexpected-multiline, block-scoped-var, no-redeclare */
diff --git a/node_modules/protobufjs/src/writer.js b/node_modules/protobufjs/src/writer.js
index cc84a00..3fb6139 100644
--- a/node_modules/protobufjs/src/writer.js
+++ b/node_modules/protobufjs/src/writer.js
@@ -383,6 +383,10 @@ Writer.prototype.bytes = function write_bytes(value) {
return this.uint32(len)._push(writeBytes, len, value);
};
+Writer.prototype.__unknownField = function __unknownField(field) {
+ return this._push(writeBytes, field.length, field);
+};
+
/**
* Writes a string.
* @param {string} value Value to write

View File

@ -30,7 +30,6 @@ try {
window.sqlInitializer = require('./ts/sql/initialize');
window.PROTO_ROOT = 'protos';
const config = require('url').parse(window.location.toString(), true).query;
setEnvironment(parseEnvironment(config.environment));

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>

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

View File

@ -12,10 +12,8 @@ module.exports = {
globals: {
assert: true,
assertEqualArrayBuffers: true,
dcodeIO: true,
getString: true,
hexToArrayBuffer: true,
PROTO_ROOT: true,
stringToArrayBuffer: true,
},

View File

@ -5,7 +5,6 @@
mocha.setup('bdd');
window.assert = chai.assert;
window.PROTO_ROOT = '../protos';
const OriginalReporter = mocha._reporter;

View File

@ -1,499 +0,0 @@
// Copyright 2014-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
'use strict';
describe('Crypto', () => {
describe('generateRegistrationId', () => {
it('generates an integer between 0 and 16383 (inclusive)', () => {
for (let i = 0; i < 100; i += 1) {
const id = window.Signal.Crypto.generateRegistrationId();
assert.isAtLeast(id, 0);
assert.isAtMost(id, 16383);
assert(Number.isInteger(id));
}
});
});
describe('deriveSecrets', () => {
it('derives key parts via HKDF', () => {
const input = window.Signal.Crypto.getRandomBytes(32);
const salt = window.Signal.Crypto.getRandomBytes(32);
const info = window.Signal.Crypto.bytesFromString('Hello world');
const result = window.Signal.Crypto.deriveSecrets(input, salt, info);
assert.lengthOf(result, 3);
result.forEach(part => {
// This is a smoke test; HKDF is tested as part of @signalapp/signal-client.
assert.instanceOf(part, ArrayBuffer);
assert.strictEqual(part.byteLength, 32);
});
});
});
describe('accessKey/profileKey', () => {
it('verification roundtrips', async () => {
const profileKey = await window.Signal.Crypto.getRandomBytes(32);
const accessKey = await window.Signal.Crypto.deriveAccessKey(profileKey);
const verifier = await window.Signal.Crypto.getAccessKeyVerifier(
accessKey
);
const correct = await window.Signal.Crypto.verifyAccessKey(
accessKey,
verifier
);
assert.strictEqual(correct, true);
});
});
describe('deriveMasterKeyFromGroupV1', () => {
const vectors = [
{
gv1: '00000000000000000000000000000000',
masterKey:
'dbde68f4ee9169081f8814eabc65523fea1359235c8cfca32b69e31dce58b039',
},
{
gv1: '000102030405060708090a0b0c0d0e0f',
masterKey:
'70884f78f07a94480ee36b67a4b5e975e92e4a774561e3df84c9076e3be4b9bf',
},
{
gv1: '7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f',
masterKey:
'e69bf7c183b288b4ea5745b7c52b651a61e57769fafde683a6fdf1240f1905f2',
},
{
gv1: 'ffffffffffffffffffffffffffffffff',
masterKey:
'dd3a7de23d10f18b64457fbeedc76226c112a730e4b76112e62c36c4432eb37d',
},
];
vectors.forEach((vector, index) => {
it(`vector ${index}`, async () => {
const gv1 = window.Signal.Crypto.hexToArrayBuffer(vector.gv1);
const expectedHex = vector.masterKey;
const actual = await window.Signal.Crypto.deriveMasterKeyFromGroupV1(
gv1
);
const actualHex = window.Signal.Crypto.arrayBufferToHex(actual);
assert.strictEqual(actualHex, expectedHex);
});
});
});
describe('symmetric encryption', () => {
it('roundtrips', async () => {
const message = 'this is my message';
const plaintext = dcodeIO.ByteBuffer.wrap(
message,
'binary'
).toArrayBuffer();
const key = window.Signal.Crypto.getRandomBytes(32);
const encrypted = await window.Signal.Crypto.encryptSymmetric(
key,
plaintext
);
const decrypted = await window.Signal.Crypto.decryptSymmetric(
key,
encrypted
);
const equal = window.Signal.Crypto.constantTimeEqual(
plaintext,
decrypted
);
if (!equal) {
throw new Error('The output and input did not match!');
}
});
it('roundtrip fails if nonce is modified', async () => {
const message = 'this is my message';
const plaintext = dcodeIO.ByteBuffer.wrap(
message,
'binary'
).toArrayBuffer();
const key = window.Signal.Crypto.getRandomBytes(32);
const encrypted = await window.Signal.Crypto.encryptSymmetric(
key,
plaintext
);
const uintArray = new Uint8Array(encrypted);
uintArray[2] += 2;
try {
await window.Signal.Crypto.decryptSymmetric(
key,
window.window.Signal.Crypto.typedArrayToArrayBuffer(uintArray)
);
} catch (error) {
assert.strictEqual(
error.message,
'decryptSymmetric: Failed to decrypt; MAC verification failed'
);
return;
}
throw new Error('Expected error to be thrown');
});
it('roundtrip fails if mac is modified', async () => {
const message = 'this is my message';
const plaintext = dcodeIO.ByteBuffer.wrap(
message,
'binary'
).toArrayBuffer();
const key = window.Signal.Crypto.getRandomBytes(32);
const encrypted = await window.Signal.Crypto.encryptSymmetric(
key,
plaintext
);
const uintArray = new Uint8Array(encrypted);
uintArray[uintArray.length - 3] += 2;
try {
await window.Signal.Crypto.decryptSymmetric(
key,
window.window.Signal.Crypto.typedArrayToArrayBuffer(uintArray)
);
} catch (error) {
assert.strictEqual(
error.message,
'decryptSymmetric: Failed to decrypt; MAC verification failed'
);
return;
}
throw new Error('Expected error to be thrown');
});
it('roundtrip fails if encrypted contents are modified', async () => {
const message = 'this is my message';
const plaintext = dcodeIO.ByteBuffer.wrap(
message,
'binary'
).toArrayBuffer();
const key = window.Signal.Crypto.getRandomBytes(32);
const encrypted = await window.Signal.Crypto.encryptSymmetric(
key,
plaintext
);
const uintArray = new Uint8Array(encrypted);
uintArray[35] += 9;
try {
await window.Signal.Crypto.decryptSymmetric(
key,
window.window.Signal.Crypto.typedArrayToArrayBuffer(uintArray)
);
} catch (error) {
assert.strictEqual(
error.message,
'decryptSymmetric: Failed to decrypt; MAC verification failed'
);
return;
}
throw new Error('Expected error to be thrown');
});
});
describe('encrypted device name', () => {
it('roundtrips', async () => {
const deviceName = 'v1.19.0 on Windows 10';
const identityKey = window.Signal.Curve.generateKeyPair();
const encrypted = await window.Signal.Crypto.encryptDeviceName(
deviceName,
identityKey.pubKey
);
const decrypted = await window.Signal.Crypto.decryptDeviceName(
encrypted,
identityKey.privKey
);
assert.strictEqual(decrypted, deviceName);
});
it('fails if iv is changed', async () => {
const deviceName = 'v1.19.0 on Windows 10';
const identityKey = window.Signal.Curve.generateKeyPair();
const encrypted = await window.Signal.Crypto.encryptDeviceName(
deviceName,
identityKey.pubKey
);
encrypted.syntheticIv = window.Signal.Crypto.getRandomBytes(16);
try {
await window.Signal.Crypto.decryptDeviceName(
encrypted,
identityKey.privKey
);
} catch (error) {
assert.strictEqual(
error.message,
'decryptDeviceName: synthetic IV did not match'
);
}
});
});
describe('attachment encryption', () => {
it('roundtrips', async () => {
const staticKeyPair = window.Signal.Curve.generateKeyPair();
const message = 'this is my message';
const plaintext = window.Signal.Crypto.bytesFromString(message);
const path =
'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa';
const encrypted = await window.Signal.Crypto.encryptAttachment(
staticKeyPair.pubKey.slice(1),
path,
plaintext
);
const decrypted = await window.Signal.Crypto.decryptAttachment(
staticKeyPair.privKey,
path,
encrypted
);
const equal = window.Signal.Crypto.constantTimeEqual(
plaintext,
decrypted
);
if (!equal) {
throw new Error('The output and input did not match!');
}
});
});
describe('verifyHmacSha256', () => {
it('rejects if their MAC is too short', async () => {
const key = window.Signal.Crypto.getRandomBytes(32);
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
const ourMac = await window.Signal.Crypto.hmacSha256(key, plaintext);
const theirMac = ourMac.slice(0, -1);
let error;
try {
await window.Signal.Crypto.verifyHmacSha256(
plaintext,
key,
theirMac,
ourMac.byteLength
);
} catch (err) {
error = err;
}
assert.instanceOf(error, Error);
assert.strictEqual(error.message, 'Bad MAC length');
});
it('rejects if their MAC is too long', async () => {
const key = window.Signal.Crypto.getRandomBytes(32);
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
const ourMac = await window.Signal.Crypto.hmacSha256(key, plaintext);
const theirMac = window.Signal.Crypto.concatenateBytes(
ourMac,
new Uint8Array([0xff])
);
let error;
try {
await window.Signal.Crypto.verifyHmacSha256(
plaintext,
key,
theirMac,
ourMac.byteLength
);
} catch (err) {
error = err;
}
assert.instanceOf(error, Error);
assert.strictEqual(error.message, 'Bad MAC length');
});
it('rejects if our MAC is shorter than the specified length', async () => {
const key = window.Signal.Crypto.getRandomBytes(32);
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
const ourMac = await window.Signal.Crypto.hmacSha256(key, plaintext);
const theirMac = ourMac;
let error;
try {
await window.Signal.Crypto.verifyHmacSha256(
plaintext,
key,
theirMac,
ourMac.byteLength + 1
);
} catch (err) {
error = err;
}
assert.instanceOf(error, Error);
assert.strictEqual(error.message, 'Bad MAC length');
});
it("rejects if the MACs don't match", async () => {
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
const ourKey = window.Signal.Crypto.getRandomBytes(32);
const ourMac = await window.Signal.Crypto.hmacSha256(ourKey, plaintext);
const theirKey = window.Signal.Crypto.getRandomBytes(32);
const theirMac = await window.Signal.Crypto.hmacSha256(
theirKey,
plaintext
);
let error;
try {
await window.Signal.Crypto.verifyHmacSha256(
plaintext,
ourKey,
theirMac,
ourMac.byteLength
);
} catch (err) {
error = err;
}
assert.instanceOf(error, Error);
assert.strictEqual(error.message, 'Bad MAC');
});
it('resolves with undefined if the MACs match exactly', async () => {
const key = window.Signal.Crypto.getRandomBytes(32);
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
const theirMac = await window.Signal.Crypto.hmacSha256(key, plaintext);
const result = await window.Signal.Crypto.verifyHmacSha256(
plaintext,
key,
theirMac,
theirMac.byteLength
);
assert.isUndefined(result);
});
it('resolves with undefined if the first `length` bytes of the MACs match', async () => {
const key = window.Signal.Crypto.getRandomBytes(32);
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
const theirMac = (
await window.Signal.Crypto.hmacSha256(key, plaintext)
).slice(0, -5);
const result = await window.Signal.Crypto.verifyHmacSha256(
plaintext,
key,
theirMac,
theirMac.byteLength
);
assert.isUndefined(result);
});
});
describe('uuidToArrayBuffer', () => {
const { uuidToArrayBuffer } = window.Signal.Crypto;
it('converts valid UUIDs to ArrayBuffers', () => {
const expectedResult = window.window.Signal.Crypto.typedArrayToArrayBuffer(
new Uint8Array([
0x22,
0x6e,
0x44,
0x02,
0x7f,
0xfc,
0x45,
0x43,
0x85,
0xc9,
0x46,
0x22,
0xc5,
0x0a,
0x5b,
0x14,
])
);
assert.deepEqual(
uuidToArrayBuffer('226e4402-7ffc-4543-85c9-4622c50a5b14'),
expectedResult
);
assert.deepEqual(
uuidToArrayBuffer('226E4402-7FFC-4543-85C9-4622C50A5B14'),
expectedResult
);
});
it('returns an empty ArrayBuffer for strings of the wrong length', () => {
assert.deepEqual(uuidToArrayBuffer(''), new ArrayBuffer(0));
assert.deepEqual(uuidToArrayBuffer('abc'), new ArrayBuffer(0));
assert.deepEqual(
uuidToArrayBuffer('032deadf0d5e4ee78da28e75b1dfb284'),
new ArrayBuffer(0)
);
assert.deepEqual(
uuidToArrayBuffer('deaed5eb-d983-456a-a954-9ad7a006b271aaaaaaaaaa'),
new ArrayBuffer(0)
);
});
});
describe('arrayBufferToUuid', () => {
const { arrayBufferToUuid } = window.Signal.Crypto;
it('converts valid ArrayBuffers to UUID strings', () => {
const buf = window.window.Signal.Crypto.typedArrayToArrayBuffer(
new Uint8Array([
0x22,
0x6e,
0x44,
0x02,
0x7f,
0xfc,
0x45,
0x43,
0x85,
0xc9,
0x46,
0x22,
0xc5,
0x0a,
0x5b,
0x14,
])
);
assert.deepEqual(
arrayBufferToUuid(buf),
'226e4402-7ffc-4543-85c9-4622c50a5b14'
);
});
it('returns undefined if passed an all-zero buffer', () => {
assert.isUndefined(arrayBufferToUuid(new ArrayBuffer(16)));
});
it('returns undefined if passed the wrong number of bytes', () => {
assert.isUndefined(arrayBufferToUuid(new ArrayBuffer(0)));
assert.isUndefined(
arrayBufferToUuid(
window.window.Signal.Crypto.typedArrayToArrayBuffer(
new Uint8Array([0x22])
)
)
);
assert.isUndefined(
arrayBufferToUuid(
window.window.Signal.Crypto.typedArrayToArrayBuffer(
new Uint8Array(Array(17).fill(0x22))
)
)
);
});
});
});

View File

@ -332,7 +332,6 @@
type="text/javascript"
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>

View File

@ -1,126 +0,0 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
describe('ProtoBuf.js', () => {
const { ProtoBuf } = window.dcodeIO;
const sampleProto = `message Simple_v1 {
optional string knownName = 1;
optional string knownValue = 3;
}
message Simple_v2 {
optional string knownName = 1;
optional int32 unknownFlags = 2;
optional string knownValue = 3;
optional string unknownString = 4;
}`;
it('retains unknown fields', () => {
const builder = ProtoBuf.loadProto(sampleProto);
const protos = builder.build();
const v2 = new protos.Simple_v2();
v2.knownName = 'version2';
v2.unknownFlags = 42;
v2.knownValue = 'known value';
v2.unknownString = 'f';
const v1 = protos.Simple_v1.decode(v2.encode());
const result = protos.Simple_v2.decode(v1.encode());
assert.equal(result.knownName, v2.knownName, 'known fields');
assert.equal(42, result.unknownFlags, 'unknown flag');
assert.equal('f', result.unknownString, 'unknown string');
assert.equal('known value', result.knownValue, 'known value');
});
it('supports nested unknown fields', () => {
const nestedProto = `
${sampleProto}
message Container_v1 {
optional Simple_v1 elem = 1;
}
message Container_v2 {
optional Simple_v2 elem = 1;
}`;
const builder = ProtoBuf.loadProto(nestedProto);
const protos = builder.build();
const v2 = new protos.Container_v2();
v2.elem = {
knownName: 'nested v2',
unknownFlags: 10,
knownValue: 'hello world',
};
const v1 = protos.Container_v1.decode(v2.encode());
const result = protos.Container_v2.decode(v1.encode());
assert.equal(
v2.elem.knownName,
result.elem.knownName,
'nested: known fields'
);
assert.equal(10, result.elem.unknownFlags, 'nested: unknown flags');
assert.equal('hello world', result.elem.knownValue, 'known value');
});
it('allows multi-byte id', () => {
const proto = `message Simple_v1 {
optional string knownName = 1;
optional string knownValue = 3;
}
message Simple_v2 {
optional string knownName = 1;
optional int32 unknownFlags = 296;
optional string knownValue = 3;
}`;
const builder = ProtoBuf.loadProto(proto);
const protos = builder.build();
const v2 = new protos.Simple_v2();
v2.knownName = 'v2 multibyte';
v2.unknownFlags = 16;
v2.knownValue = 'foo bar';
const v1 = protos.Simple_v1.decode(v2.encode());
const result = protos.Simple_v2.decode(v1.encode());
assert.equal(result.knownName, v2.knownName, 'multibyte: known fields');
assert.equal(16, result.unknownFlags, 'multibyte: unknown fields');
assert.equal('foo bar', result.knownValue, 'multibyte: known value');
});
it('retains fields with 64bit type', () => {
const proto = `message Simple_v1 {
optional string knownName = 1;
optional string knownValue = 3;
}
message Simple_v2 {
optional string knownName = 1;
optional double unknownFlags = 2;
optional string knownValue = 3;
}`;
const builder = ProtoBuf.loadProto(proto);
const protos = builder.build();
const v2 = new protos.Simple_v2();
v2.knownName = 'v2 double';
v2.unknownFlags = 0;
v2.knownValue = 'double double';
const v1 = protos.Simple_v1.decode(v2.encode());
const result = protos.Simple_v2.decode(v1.encode());
assert.equal(result.knownName, v2.knownName, 'double: known fields');
assert.equal(0, result.unknownFlags, 'double: unknown fields');
assert.equal('double double', result.knownValue, 'double: known value');
});
});

View File

@ -6,8 +6,6 @@
const chai = require('chai');
const 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),

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

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

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

9
ts/model-types.d.ts vendored
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;

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

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

View File

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

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

View File

@ -0,0 +1,564 @@
// Copyright 2015-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import * as Curve from '../Curve';
import * as Crypto from '../Crypto';
import TSCrypto from '../textsecure/Crypto';
describe('Crypto', () => {
describe('encrypting and decrypting profile data', () => {
const NAME_PADDED_LENGTH = 53;
describe('encrypting and decrypting profile names', () => {
it('pads, encrypts, decrypts, and unpads a short string', async () => {
const name = 'Alice';
const buffer = Crypto.bytesFromString(name);
const key = Crypto.getRandomBytes(32);
const encrypted = await TSCrypto.encryptProfileName(buffer, key);
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
const { given, family } = await TSCrypto.decryptProfileName(
Crypto.arrayBufferToBase64(encrypted),
key
);
assert.strictEqual(family, null);
assert.strictEqual(Crypto.stringFromBytes(given), name);
});
it('handles a given name of the max, 53 characters', async () => {
const name = '01234567890123456789012345678901234567890123456789123';
const buffer = Crypto.bytesFromString(name);
const key = Crypto.getRandomBytes(32);
const encrypted = await TSCrypto.encryptProfileName(buffer, key);
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
const { given, family } = await TSCrypto.decryptProfileName(
Crypto.arrayBufferToBase64(encrypted),
key
);
assert.strictEqual(Crypto.stringFromBytes(given), name);
assert.strictEqual(family, null);
});
it('handles family/given name of the max, 53 characters', async () => {
const name =
'01234567890123456789\u000001234567890123456789012345678912';
const buffer = Crypto.bytesFromString(name);
const key = Crypto.getRandomBytes(32);
const encrypted = await TSCrypto.encryptProfileName(buffer, key);
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
const { given, family } = await TSCrypto.decryptProfileName(
Crypto.arrayBufferToBase64(encrypted),
key
);
assert.strictEqual(
Crypto.stringFromBytes(given),
'01234567890123456789'
);
assert.strictEqual(
family && Crypto.stringFromBytes(family),
'01234567890123456789012345678912'
);
});
it('handles a string with family/given name', async () => {
const name = 'Alice\0Jones';
const buffer = Crypto.bytesFromString(name);
const key = Crypto.getRandomBytes(32);
const encrypted = await TSCrypto.encryptProfileName(buffer, key);
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
const { given, family } = await TSCrypto.decryptProfileName(
Crypto.arrayBufferToBase64(encrypted),
key
);
assert.strictEqual(Crypto.stringFromBytes(given), 'Alice');
assert.strictEqual(family && Crypto.stringFromBytes(family), 'Jones');
});
it('works for empty string', async () => {
const name = Crypto.bytesFromString('');
const key = Crypto.getRandomBytes(32);
const encrypted = await TSCrypto.encryptProfileName(name, key);
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
const { given, family } = await TSCrypto.decryptProfileName(
Crypto.arrayBufferToBase64(encrypted),
key
);
assert.strictEqual(family, null);
assert.strictEqual(given.byteLength, 0);
assert.strictEqual(Crypto.stringFromBytes(given), '');
});
});
describe('encrypting and decrypting profile avatars', () => {
it('encrypts and decrypts', async () => {
const buffer = Crypto.bytesFromString('This is an avatar');
const key = Crypto.getRandomBytes(32);
const encrypted = await TSCrypto.encryptProfile(buffer, key);
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
const decrypted = await TSCrypto.decryptProfile(encrypted, key);
assert(Crypto.constantTimeEqual(buffer, decrypted));
});
it('throws when decrypting with the wrong key', async () => {
const buffer = Crypto.bytesFromString('This is an avatar');
const key = Crypto.getRandomBytes(32);
const badKey = Crypto.getRandomBytes(32);
const encrypted = await TSCrypto.encryptProfile(buffer, key);
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
await assert.isRejected(
TSCrypto.decryptProfile(encrypted, badKey),
'Failed to decrypt profile data. Most likely the profile key has changed.'
);
});
});
});
describe('generateRegistrationId', () => {
it('generates an integer between 0 and 16383 (inclusive)', () => {
for (let i = 0; i < 100; i += 1) {
const id = Crypto.generateRegistrationId();
assert.isAtLeast(id, 0);
assert.isAtMost(id, 16383);
assert(Number.isInteger(id));
}
});
});
describe('deriveSecrets', () => {
it('derives key parts via HKDF', () => {
const input = Crypto.getRandomBytes(32);
const salt = Crypto.getRandomBytes(32);
const info = Crypto.bytesFromString('Hello world');
const result = Crypto.deriveSecrets(input, salt, info);
assert.lengthOf(result, 3);
result.forEach(part => {
// This is a smoke test; HKDF is tested as part of @signalapp/signal-client.
assert.instanceOf(part, ArrayBuffer);
assert.strictEqual(part.byteLength, 32);
});
});
});
describe('accessKey/profileKey', () => {
it('verification roundtrips', async () => {
const profileKey = await Crypto.getRandomBytes(32);
const accessKey = await Crypto.deriveAccessKey(profileKey);
const verifier = await Crypto.getAccessKeyVerifier(accessKey);
const correct = await Crypto.verifyAccessKey(accessKey, verifier);
assert.strictEqual(correct, true);
});
});
describe('deriveMasterKeyFromGroupV1', () => {
const vectors = [
{
gv1: '00000000000000000000000000000000',
masterKey:
'dbde68f4ee9169081f8814eabc65523fea1359235c8cfca32b69e31dce58b039',
},
{
gv1: '000102030405060708090a0b0c0d0e0f',
masterKey:
'70884f78f07a94480ee36b67a4b5e975e92e4a774561e3df84c9076e3be4b9bf',
},
{
gv1: '7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f',
masterKey:
'e69bf7c183b288b4ea5745b7c52b651a61e57769fafde683a6fdf1240f1905f2',
},
{
gv1: 'ffffffffffffffffffffffffffffffff',
masterKey:
'dd3a7de23d10f18b64457fbeedc76226c112a730e4b76112e62c36c4432eb37d',
},
];
vectors.forEach((vector, index) => {
it(`vector ${index}`, async () => {
const gv1 = Crypto.hexToArrayBuffer(vector.gv1);
const expectedHex = vector.masterKey;
const actual = await Crypto.deriveMasterKeyFromGroupV1(gv1);
const actualHex = Crypto.arrayBufferToHex(actual);
assert.strictEqual(actualHex, expectedHex);
});
});
});
describe('symmetric encryption', () => {
it('roundtrips', async () => {
const message = 'this is my message';
const plaintext = Crypto.bytesFromString(message);
const key = Crypto.getRandomBytes(32);
const encrypted = await Crypto.encryptSymmetric(key, plaintext);
const decrypted = await Crypto.decryptSymmetric(key, encrypted);
const equal = Crypto.constantTimeEqual(plaintext, decrypted);
if (!equal) {
throw new Error('The output and input did not match!');
}
});
it('roundtrip fails if nonce is modified', async () => {
const message = 'this is my message';
const plaintext = Crypto.bytesFromString(message);
const key = Crypto.getRandomBytes(32);
const encrypted = await Crypto.encryptSymmetric(key, plaintext);
const uintArray = new Uint8Array(encrypted);
uintArray[2] += 2;
try {
await Crypto.decryptSymmetric(
key,
Crypto.typedArrayToArrayBuffer(uintArray)
);
} catch (error) {
assert.strictEqual(
error.message,
'decryptSymmetric: Failed to decrypt; MAC verification failed'
);
return;
}
throw new Error('Expected error to be thrown');
});
it('roundtrip fails if mac is modified', async () => {
const message = 'this is my message';
const plaintext = Crypto.bytesFromString(message);
const key = Crypto.getRandomBytes(32);
const encrypted = await Crypto.encryptSymmetric(key, plaintext);
const uintArray = new Uint8Array(encrypted);
uintArray[uintArray.length - 3] += 2;
try {
await Crypto.decryptSymmetric(
key,
Crypto.typedArrayToArrayBuffer(uintArray)
);
} catch (error) {
assert.strictEqual(
error.message,
'decryptSymmetric: Failed to decrypt; MAC verification failed'
);
return;
}
throw new Error('Expected error to be thrown');
});
it('roundtrip fails if encrypted contents are modified', async () => {
const message = 'this is my message';
const plaintext = Crypto.bytesFromString(message);
const key = Crypto.getRandomBytes(32);
const encrypted = await Crypto.encryptSymmetric(key, plaintext);
const uintArray = new Uint8Array(encrypted);
uintArray[35] += 9;
try {
await Crypto.decryptSymmetric(
key,
Crypto.typedArrayToArrayBuffer(uintArray)
);
} catch (error) {
assert.strictEqual(
error.message,
'decryptSymmetric: Failed to decrypt; MAC verification failed'
);
return;
}
throw new Error('Expected error to be thrown');
});
});
describe('encrypted device name', () => {
it('roundtrips', async () => {
const deviceName = 'v1.19.0 on Windows 10';
const identityKey = Curve.generateKeyPair();
const encrypted = await Crypto.encryptDeviceName(
deviceName,
identityKey.pubKey
);
const decrypted = await Crypto.decryptDeviceName(
encrypted,
identityKey.privKey
);
assert.strictEqual(decrypted, deviceName);
});
it('fails if iv is changed', async () => {
const deviceName = 'v1.19.0 on Windows 10';
const identityKey = Curve.generateKeyPair();
const encrypted = await Crypto.encryptDeviceName(
deviceName,
identityKey.pubKey
);
encrypted.syntheticIv = Crypto.getRandomBytes(16);
try {
await Crypto.decryptDeviceName(encrypted, identityKey.privKey);
} catch (error) {
assert.strictEqual(
error.message,
'decryptDeviceName: synthetic IV did not match'
);
}
});
});
describe('attachment encryption', () => {
it('roundtrips', async () => {
const staticKeyPair = Curve.generateKeyPair();
const message = 'this is my message';
const plaintext = Crypto.bytesFromString(message);
const path =
'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa';
const encrypted = await Crypto.encryptAttachment(
staticKeyPair.pubKey.slice(1),
path,
plaintext
);
const decrypted = await Crypto.decryptAttachment(
staticKeyPair.privKey,
path,
encrypted
);
const equal = Crypto.constantTimeEqual(plaintext, decrypted);
if (!equal) {
throw new Error('The output and input did not match!');
}
});
});
describe('verifyHmacSha256', () => {
it('rejects if their MAC is too short', async () => {
const key = Crypto.getRandomBytes(32);
const plaintext = Crypto.bytesFromString('Hello world');
const ourMac = await Crypto.hmacSha256(key, plaintext);
const theirMac = ourMac.slice(0, -1);
let error;
try {
await Crypto.verifyHmacSha256(
plaintext,
key,
theirMac,
ourMac.byteLength
);
} catch (err) {
error = err;
}
assert.instanceOf(error, Error);
assert.strictEqual(error.message, 'Bad MAC length');
});
it('rejects if their MAC is too long', async () => {
const key = Crypto.getRandomBytes(32);
const plaintext = Crypto.bytesFromString('Hello world');
const ourMac = await Crypto.hmacSha256(key, plaintext);
const theirMac = Crypto.concatenateBytes(ourMac, new Uint8Array([0xff]));
let error;
try {
await Crypto.verifyHmacSha256(
plaintext,
key,
theirMac,
ourMac.byteLength
);
} catch (err) {
error = err;
}
assert.instanceOf(error, Error);
assert.strictEqual(error.message, 'Bad MAC length');
});
it('rejects if our MAC is shorter than the specified length', async () => {
const key = Crypto.getRandomBytes(32);
const plaintext = Crypto.bytesFromString('Hello world');
const ourMac = await Crypto.hmacSha256(key, plaintext);
const theirMac = ourMac;
let error;
try {
await Crypto.verifyHmacSha256(
plaintext,
key,
theirMac,
ourMac.byteLength + 1
);
} catch (err) {
error = err;
}
assert.instanceOf(error, Error);
assert.strictEqual(error.message, 'Bad MAC length');
});
it("rejects if the MACs don't match", async () => {
const plaintext = Crypto.bytesFromString('Hello world');
const ourKey = Crypto.getRandomBytes(32);
const ourMac = await Crypto.hmacSha256(ourKey, plaintext);
const theirKey = Crypto.getRandomBytes(32);
const theirMac = await Crypto.hmacSha256(theirKey, plaintext);
let error;
try {
await Crypto.verifyHmacSha256(
plaintext,
ourKey,
theirMac,
ourMac.byteLength
);
} catch (err) {
error = err;
}
assert.instanceOf(error, Error);
assert.strictEqual(error.message, 'Bad MAC');
});
it('resolves with undefined if the MACs match exactly', async () => {
const key = Crypto.getRandomBytes(32);
const plaintext = Crypto.bytesFromString('Hello world');
const theirMac = await Crypto.hmacSha256(key, plaintext);
const result = await Crypto.verifyHmacSha256(
plaintext,
key,
theirMac,
theirMac.byteLength
);
assert.isUndefined(result);
});
it('resolves with undefined if the first `length` bytes of the MACs match', async () => {
const key = Crypto.getRandomBytes(32);
const plaintext = Crypto.bytesFromString('Hello world');
const theirMac = (await Crypto.hmacSha256(key, plaintext)).slice(0, -5);
const result = await Crypto.verifyHmacSha256(
plaintext,
key,
theirMac,
theirMac.byteLength
);
assert.isUndefined(result);
});
});
describe('uuidToArrayBuffer', () => {
const { uuidToArrayBuffer } = Crypto;
it('converts valid UUIDs to ArrayBuffers', () => {
const expectedResult = Crypto.typedArrayToArrayBuffer(
new Uint8Array([
0x22,
0x6e,
0x44,
0x02,
0x7f,
0xfc,
0x45,
0x43,
0x85,
0xc9,
0x46,
0x22,
0xc5,
0x0a,
0x5b,
0x14,
])
);
assert.deepEqual(
uuidToArrayBuffer('226e4402-7ffc-4543-85c9-4622c50a5b14'),
expectedResult
);
assert.deepEqual(
uuidToArrayBuffer('226E4402-7FFC-4543-85C9-4622C50A5B14'),
expectedResult
);
});
it('returns an empty ArrayBuffer for strings of the wrong length', () => {
assert.deepEqual(uuidToArrayBuffer(''), new ArrayBuffer(0));
assert.deepEqual(uuidToArrayBuffer('abc'), new ArrayBuffer(0));
assert.deepEqual(
uuidToArrayBuffer('032deadf0d5e4ee78da28e75b1dfb284'),
new ArrayBuffer(0)
);
assert.deepEqual(
uuidToArrayBuffer('deaed5eb-d983-456a-a954-9ad7a006b271aaaaaaaaaa'),
new ArrayBuffer(0)
);
});
});
describe('arrayBufferToUuid', () => {
const { arrayBufferToUuid } = Crypto;
it('converts valid ArrayBuffers to UUID strings', () => {
const buf = Crypto.typedArrayToArrayBuffer(
new Uint8Array([
0x22,
0x6e,
0x44,
0x02,
0x7f,
0xfc,
0x45,
0x43,
0x85,
0xc9,
0x46,
0x22,
0xc5,
0x0a,
0x5b,
0x14,
])
);
assert.deepEqual(
arrayBufferToUuid(buf),
'226e4402-7ffc-4543-85c9-4622c50a5b14'
);
});
it('returns undefined if passed an all-zero buffer', () => {
assert.isUndefined(arrayBufferToUuid(new ArrayBuffer(16)));
});
it('returns undefined if passed the wrong number of bytes', () => {
assert.isUndefined(arrayBufferToUuid(new ArrayBuffer(0)));
assert.isUndefined(
arrayBufferToUuid(
Crypto.typedArrayToArrayBuffer(new Uint8Array([0x22]))
)
);
assert.isUndefined(
arrayBufferToUuid(
Crypto.typedArrayToArrayBuffer(new Uint8Array(Array(17).fill(0x22)))
)
);
});
});
});

View File

@ -7,6 +7,7 @@
import { assert } from 'chai';
import * 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();

View File

@ -1,31 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { isByteBufferEmpty } from '../../util/isByteBufferEmpty';
describe('isByteBufferEmpty', () => {
it('returns true for undefined', () => {
assert.isTrue(isByteBufferEmpty(undefined));
});
it('returns true for object missing limit', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const brokenByteBuffer: any = {};
assert.isTrue(isByteBufferEmpty(brokenByteBuffer));
});
it('returns true for object limit', () => {
const emptyByteBuffer = new window.dcodeIO.ByteBuffer(0);
assert.isTrue(isByteBufferEmpty(emptyByteBuffer));
});
it('returns false for object limit', () => {
const byteBuffer = window.dcodeIO.ByteBuffer.wrap('AABBCC', 'hex');
assert.isFalse(isByteBufferEmpty(byteBuffer));
});
});

View File

@ -0,0 +1,95 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { Root } from 'protobufjs';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { Partial, Full } = (Root as any).fromJSON({
nested: {
test: {
nested: {
Partial: {
fields: {
a: {
type: 'string',
id: 1,
},
c: {
type: 'int32',
id: 3,
},
},
},
Full: {
fields: {
a: {
type: 'string',
id: 1,
},
b: {
type: 'bool',
id: 2,
},
c: {
type: 'int32',
id: 3,
},
d: {
type: 'bytes',
id: 4,
},
},
},
},
},
},
}).nested.test;
describe('Proto#__unknownFields', () => {
it('should encode and decode with unknown fields', () => {
const full = Full.encode({
a: 'hello',
b: true,
c: 42,
d: Buffer.from('ohai'),
}).finish();
const partial = Partial.decode(full);
assert.strictEqual(partial.a, 'hello');
assert.strictEqual(partial.c, 42);
assert.strictEqual(partial.__unknownFields.length, 2);
assert.strictEqual(
Buffer.from(partial.__unknownFields[0]).toString('hex'),
'1001'
);
assert.strictEqual(
Buffer.from(partial.__unknownFields[1]).toString('hex'),
'22046f686169'
);
const encoded = Partial.encode({
a: partial.a,
c: partial.c,
__unknownFields: partial.__unknownFields,
}).finish();
const decoded = Full.decode(encoded);
assert.strictEqual(decoded.a, 'hello');
assert.strictEqual(decoded.b, true);
assert.strictEqual(decoded.c, 42);
assert.strictEqual(Buffer.from(decoded.d).toString(), 'ohai');
const concat = Partial.encode({
a: partial.a,
c: partial.c,
__unknownFields: [Buffer.concat(partial.__unknownFields)],
}).finish();
const decodedConcat = Full.decode(concat);
assert.strictEqual(decodedConcat.a, 'hello');
assert.strictEqual(decodedConcat.b, true);
assert.strictEqual(decodedConcat.c, 42);
assert.strictEqual(Buffer.from(decodedConcat.d).toString(), 'ohai');
});
});

1145
ts/textsecure.d.ts vendored

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,8 @@ import {
hmacSha256,
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,
};
});

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

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

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(

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

View File

@ -1,10 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isNumber } from 'lodash';
import { ByteBufferClass } from '../window.d';
export function isByteBufferEmpty(data?: ByteBufferClass): boolean {
return !data || !isNumber(data.limit) || data.limit === 0;
}

View File

@ -8561,13 +8561,6 @@
"reasonCategory": "falseMatch",
"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."
}
]
]

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

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;

48
ts/window.d.ts vendored
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,

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"