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

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