decrypt/encrypt with libsignal-client, remove libsignal-protocol-javascript

This commit is contained in:
Scott Nonnenberg 2021-04-16 16:13:13 -07:00
parent 37ff4a1df4
commit 86d2a4b5dd
60 changed files with 2508 additions and 28714 deletions

View file

@ -7,7 +7,6 @@ release/**
# Generated files # Generated files
js/curve/* js/curve/*
js/components.js js/components.js
js/libtextsecure.js
js/util_worker.js js/util_worker.js
libtextsecure/components.js libtextsecure/components.js
libtextsecure/test/test.js libtextsecure/test/test.js
@ -18,7 +17,6 @@ sticker-creator/dist/**
js/Mp3LameEncoder.min.js js/Mp3LameEncoder.min.js
js/WebAudioRecorderMp3.js js/WebAudioRecorderMp3.js
js/libphonenumber-util.js js/libphonenumber-util.js
libtextsecure/libsignal-protocol.js
libtextsecure/test/blanket_mocha.js libtextsecure/test/blanket_mocha.js
test/blanket_mocha.js test/blanket_mocha.js

1
.gitignore vendored
View file

@ -20,7 +20,6 @@ tsconfig.tsbuildinfo
# generated files # generated files
js/components.js js/components.js
js/util_worker.js js/util_worker.js
js/libtextsecure.js
libtextsecure/components.js libtextsecure/components.js
libtextsecure/test/test.js libtextsecure/test/test.js
stylesheets/*.css stylesheets/*.css

View file

@ -7,7 +7,6 @@ config/local.json
dist/** dist/**
js/components.js js/components.js
js/util_worker.js js/util_worker.js
js/libtextsecure.js
libtextsecure/components.js libtextsecure/components.js
libtextsecure/test/test.js libtextsecure/test/test.js
stylesheets/*.css stylesheets/*.css
@ -25,7 +24,6 @@ components/**
js/curve/** js/curve/**
js/Mp3LameEncoder.min.js js/Mp3LameEncoder.min.js
js/WebAudioRecorderMp3.js js/WebAudioRecorderMp3.js
libtextsecure/libsignal-protocol.js
libtextsecure/test/blanket_mocha.js libtextsecure/test/blanket_mocha.js
test/blanket_mocha.js test/blanket_mocha.js

View file

@ -49,22 +49,6 @@ module.exports = grunt => {
], ],
dest: 'test/test.js', dest: 'test/test.js',
}, },
// TODO: Move errors back down?
libtextsecure: {
options: {
banner: ';(function() {\n',
footer: '})();\n',
},
src: [
'libtextsecure/libsignal-protocol.js',
'libtextsecure/protocol_wrapper.js',
'libtextsecure/storage/user.js',
'libtextsecure/storage/unprocessed.js',
'libtextsecure/protobufs.js',
],
dest: 'js/libtextsecure.js',
},
libtextsecuretest: { libtextsecuretest: {
src: [ src: [
'node_modules/jquery/dist/jquery.js', 'node_modules/jquery/dist/jquery.js',

View file

@ -329,7 +329,11 @@
<script type='text/javascript' src='js/reliable_trigger.js'></script> <script type='text/javascript' src='js/reliable_trigger.js'></script>
<script type='text/javascript' src='js/database.js'></script> <script type='text/javascript' src='js/database.js'></script>
<script type='text/javascript' src='js/storage.js'></script> <script type='text/javascript' src='js/storage.js'></script>
<script type='text/javascript' src='js/libtextsecure.js'></script>
<script type='text/javascript' src='libtextsecure/protocol_wrapper.js'></script>
<script type='text/javascript' src='libtextsecure/storage/user.js'></script>
<script type='text/javascript' src='libtextsecure/storage/unprocessed.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/notifications.js'></script>
<script type='text/javascript' src='js/delivery_receipts.js'></script> <script type='text/javascript' src='js/delivery_receipts.js'></script>

View file

@ -6,6 +6,7 @@
const { bindActionCreators } = require('redux'); const { bindActionCreators } = require('redux');
const Backbone = require('../../ts/backbone'); const Backbone = require('../../ts/backbone');
const Crypto = require('../../ts/Crypto'); const Crypto = require('../../ts/Crypto');
const Curve = require('../../ts/Curve');
const { const {
start: conversationControllerStart, start: conversationControllerStart,
} = require('../../ts/ConversationController'); } = require('../../ts/ConversationController');
@ -430,6 +431,7 @@ exports.setup = (options = {}) => {
Backbone, Backbone,
Components, Components,
Crypto, Crypto,
Curve,
conversationControllerStart, conversationControllerStart,
Data, Data,
Emojis, Emojis,

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
// Copyright 2016-2020 Signal Messenger, LLC // Copyright 2016-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global window, textsecure, SignalProtocolStore, libsignal */ /* global window, textsecure, SignalProtocolStore */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function () {
@ -9,8 +9,4 @@
window.textsecure.storage = window.textsecure.storage || {}; window.textsecure.storage = window.textsecure.storage || {};
textsecure.storage.protocol = new SignalProtocolStore(); textsecure.storage.protocol = new SignalProtocolStore();
textsecure.ProvisioningCipher = libsignal.ProvisioningCipher;
textsecure.startWorker = libsignal.worker.startWorker;
textsecure.stopWorker = libsignal.worker.stopWorker;
})(); })();

View file

@ -1,8 +1,6 @@
// Copyright 2017-2020 Signal Messenger, LLC // Copyright 2017-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global libsignal */
describe('AccountManager', () => { describe('AccountManager', () => {
let accountManager; let accountManager;
@ -16,7 +14,7 @@ describe('AccountManager', () => {
const DAY = 1000 * 60 * 60 * 24; const DAY = 1000 * 60 * 60 * 24;
beforeEach(async () => { beforeEach(async () => {
const identityKey = await libsignal.KeyHelper.generateIdentityKeyPair(); const identityKey = window.Signal.Curve.generateKeyPair();
originalProtocolStorage = window.textsecure.storage.protocol; originalProtocolStorage = window.textsecure.storage.protocol;
window.textsecure.storage.protocol = { window.textsecure.storage.protocol = {

View file

@ -64,7 +64,9 @@ describe('GroupBuffer', () => {
avatarBuffer.limit = avatarBuffer.offset; avatarBuffer.limit = avatarBuffer.offset;
avatarBuffer.offset = 0; avatarBuffer.offset = 0;
const groupInfo = new window.textsecure.protobuf.GroupDetails({ const groupInfo = new window.textsecure.protobuf.GroupDetails({
id: new Uint8Array([1, 3, 3, 7]).buffer, id: window.Signal.Crypto.typedArrayToArrayBuffer(
new Uint8Array([1, 3, 3, 7])
),
name: 'Hackers', name: 'Hackers',
membersE164: ['cereal', 'burn', 'phreak', 'joey'], membersE164: ['cereal', 'burn', 'phreak', 'joey'],
avatar: { contentType: 'image/jpeg', length: avatarLen }, avatar: { contentType: 'image/jpeg', length: avatarLen },
@ -92,7 +94,9 @@ describe('GroupBuffer', () => {
assert.strictEqual(group.name, 'Hackers'); assert.strictEqual(group.name, 'Hackers');
assertEqualArrayBuffers( assertEqualArrayBuffers(
group.id.toArrayBuffer(), group.id.toArrayBuffer(),
new Uint8Array([1, 3, 3, 7]).buffer window.Signal.Crypto.typedArrayToArrayBuffer(
new Uint8Array([1, 3, 3, 7])
)
); );
assert.sameMembers(group.membersE164, [ assert.sameMembers(group.membersE164, [
'cereal', 'cereal',

View file

@ -1,7 +1,7 @@
// Copyright 2015-2020 Signal Messenger, LLC // Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global libsignal, textsecure */ /* global textsecure */
describe('encrypting and decrypting profile data', () => { describe('encrypting and decrypting profile data', () => {
const NAME_PADDED_LENGTH = 53; const NAME_PADDED_LENGTH = 53;
@ -9,7 +9,7 @@ describe('encrypting and decrypting profile data', () => {
it('pads, encrypts, decrypts, and unpads a short string', () => { it('pads, encrypts, decrypts, and unpads a short string', () => {
const name = 'Alice'; const name = 'Alice';
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer(); const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
const key = libsignal.crypto.getRandomBytes(32); const key = window.Signal.Crypto.getRandomBytes(32);
return textsecure.crypto return textsecure.crypto
.encryptProfileName(buffer, key) .encryptProfileName(buffer, key)
@ -29,7 +29,7 @@ describe('encrypting and decrypting profile data', () => {
it('handles a given name of the max, 53 characters', () => { it('handles a given name of the max, 53 characters', () => {
const name = '01234567890123456789012345678901234567890123456789123'; const name = '01234567890123456789012345678901234567890123456789123';
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer(); const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
const key = libsignal.crypto.getRandomBytes(32); const key = window.Signal.Crypto.getRandomBytes(32);
return textsecure.crypto return textsecure.crypto
.encryptProfileName(buffer, key) .encryptProfileName(buffer, key)
@ -49,7 +49,7 @@ describe('encrypting and decrypting profile data', () => {
it('handles family/given name of the max, 53 characters', () => { it('handles family/given name of the max, 53 characters', () => {
const name = '01234567890123456789\u000001234567890123456789012345678912'; const name = '01234567890123456789\u000001234567890123456789012345678912';
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer(); const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
const key = libsignal.crypto.getRandomBytes(32); const key = window.Signal.Crypto.getRandomBytes(32);
return textsecure.crypto return textsecure.crypto
.encryptProfileName(buffer, key) .encryptProfileName(buffer, key)
@ -72,7 +72,7 @@ describe('encrypting and decrypting profile data', () => {
it('handles a string with family/given name', () => { it('handles a string with family/given name', () => {
const name = 'Alice\0Jones'; const name = 'Alice\0Jones';
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer(); const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
const key = libsignal.crypto.getRandomBytes(32); const key = window.Signal.Crypto.getRandomBytes(32);
return textsecure.crypto return textsecure.crypto
.encryptProfileName(buffer, key) .encryptProfileName(buffer, key)
@ -92,25 +92,20 @@ describe('encrypting and decrypting profile data', () => {
}); });
}); });
}); });
it('works for empty string', () => { it('works for empty string', async () => {
const name = dcodeIO.ByteBuffer.wrap('').toArrayBuffer(); const name = dcodeIO.ByteBuffer.wrap('').toArrayBuffer();
const key = libsignal.crypto.getRandomBytes(32); const key = window.Signal.Crypto.getRandomBytes(32);
return textsecure.crypto const encrypted = await textsecure.crypto.encryptProfileName(name, key);
.encryptProfileName(name.buffer, key) assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
.then(encrypted => {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12); const { given, family } = await textsecure.crypto.decryptProfileName(
return textsecure.crypto encrypted,
.decryptProfileName(encrypted, key) key
.then(({ given, family }) => { );
assert.strictEqual(family, null); assert.strictEqual(family, null);
assert.strictEqual(given.byteLength, 0); assert.strictEqual(given.byteLength, 0);
assert.strictEqual( assert.strictEqual(dcodeIO.ByteBuffer.wrap(given).toString('utf8'), '');
dcodeIO.ByteBuffer.wrap(given).toString('utf8'),
''
);
});
});
}); });
}); });
describe('encrypting and decrypting profile avatars', () => { describe('encrypting and decrypting profile avatars', () => {
@ -118,7 +113,7 @@ describe('encrypting and decrypting profile data', () => {
const buffer = dcodeIO.ByteBuffer.wrap( const buffer = dcodeIO.ByteBuffer.wrap(
'This is an avatar' 'This is an avatar'
).toArrayBuffer(); ).toArrayBuffer();
const key = libsignal.crypto.getRandomBytes(32); const key = window.Signal.Crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfile(buffer, key).then(encrypted => { return textsecure.crypto.encryptProfile(buffer, key).then(encrypted => {
assert(encrypted.byteLength === buffer.byteLength + 16 + 12); assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
@ -133,8 +128,8 @@ describe('encrypting and decrypting profile data', () => {
const buffer = dcodeIO.ByteBuffer.wrap( const buffer = dcodeIO.ByteBuffer.wrap(
'This is an avatar' 'This is an avatar'
).toArrayBuffer(); ).toArrayBuffer();
const key = libsignal.crypto.getRandomBytes(32); const key = window.Signal.Crypto.getRandomBytes(32);
const badKey = libsignal.crypto.getRandomBytes(32); const badKey = window.Signal.Crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfile(buffer, key).then(encrypted => { return textsecure.crypto.encryptProfile(buffer, key).then(encrypted => {
assert(encrypted.byteLength === buffer.byteLength + 16 + 12); assert(encrypted.byteLength === buffer.byteLength + 16 + 12);

View file

@ -1,7 +1,7 @@
// Copyright 2015-2020 Signal Messenger, LLC // Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global libsignal, textsecure */ /* global textsecure */
describe('Key generation', function thisNeeded() { describe('Key generation', function thisNeeded() {
const count = 10; const count = 10;
@ -43,11 +43,10 @@ describe('Key generation', function thisNeeded() {
}); });
} }
before(() => { before(async () => {
localStorage.clear(); localStorage.clear();
return libsignal.KeyHelper.generateIdentityKeyPair().then(keyPair => const keyPair = window.Signal.Curve.generateKeyPair();
textsecure.storage.protocol.put('identityKey', keyPair) await textsecure.storage.protocol.put('identityKey', keyPair);
);
}); });
describe('the first time', () => { describe('the first time', () => {

View file

@ -6,7 +6,6 @@ function SignalProtocolStore() {
} }
SignalProtocolStore.prototype = { SignalProtocolStore.prototype = {
Direction: { SENDING: 1, RECEIVING: 2 },
VerifiedStatus: { VerifiedStatus: {
DEFAULT: 0, DEFAULT: 0,
VERIFIED: 1, VERIFIED: 1,

View file

@ -21,7 +21,6 @@
<script type="text/javascript" src="in_memory_signal_protocol_store.js"></script> <script type="text/javascript" src="in_memory_signal_protocol_store.js"></script>
<script type="text/javascript" src="../components.js"></script> <script type="text/javascript" src="../components.js"></script>
<script type="text/javascript" src="../libsignal-protocol.js"></script>
<script type="text/javascript" src="../protobufs.js" data-cover></script> <script type="text/javascript" src="../protobufs.js" data-cover></script>
<script type="text/javascript" src="../storage/user.js" data-cover></script> <script type="text/javascript" src="../storage/user.js" data-cover></script>
<script type="text/javascript" src="../storage/unprocessed.js" data-cover></script> <script type="text/javascript" src="../storage/unprocessed.js" data-cover></script>
@ -36,7 +35,6 @@
<script type="text/javascript" src="helpers_test.js"></script> <script type="text/javascript" src="helpers_test.js"></script>
<script type="text/javascript" src="storage_test.js"></script> <script type="text/javascript" src="storage_test.js"></script>
<script type="text/javascript" src="crypto_test.js"></script> <script type="text/javascript" src="crypto_test.js"></script>
<script type="text/javascript" src="protocol_wrapper_test.js"></script>
<script type="text/javascript" src="contacts_parser_test.js"></script> <script type="text/javascript" src="contacts_parser_test.js"></script>
<script type="text/javascript" src="generate_keys_test.js"></script> <script type="text/javascript" src="generate_keys_test.js"></script>
<script type="text/javascript" src="websocket-resources_test.js"></script> <script type="text/javascript" src="websocket-resources_test.js"></script>

View file

@ -1,14 +1,14 @@
// Copyright 2015-2020 Signal Messenger, LLC // Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global libsignal, textsecure */ /* global textsecure */
describe('MessageReceiver', () => { describe('MessageReceiver', () => {
const { WebSocket } = window; const { WebSocket } = window;
const number = '+19999999999'; const number = '+19999999999';
const uuid = 'AAAAAAAA-BBBB-4CCC-9DDD-EEEEEEEEEEEE'; const uuid = 'AAAAAAAA-BBBB-4CCC-9DDD-EEEEEEEEEEEE';
const deviceId = 1; const deviceId = 1;
const signalingKey = libsignal.crypto.getRandomBytes(32 + 20); const signalingKey = window.Signal.Crypto.getRandomBytes(32 + 20);
before(() => { before(() => {
localStorage.clear(); localStorage.clear();
@ -34,7 +34,7 @@ describe('MessageReceiver', () => {
sourceUuid: uuid, sourceUuid: uuid,
sourceDevice: deviceId, sourceDevice: deviceId,
timestamp: Date.now(), timestamp: Date.now(),
content: libsignal.crypto.getRandomBytes(200), content: window.Signal.Crypto.getRandomBytes(200),
}; };
const body = new textsecure.protobuf.Envelope(attrs).toArrayBuffer(); const body = new textsecure.protobuf.Envelope(attrs).toArrayBuffer();
@ -54,8 +54,8 @@ describe('MessageReceiver', () => {
}); });
const messageReceiver = new textsecure.MessageReceiver( const messageReceiver = new textsecure.MessageReceiver(
'oldUsername', 'oldUsername.2',
'username', 'username.2',
'password', 'password',
'signalingKey', 'signalingKey',
{ {
@ -77,8 +77,8 @@ describe('MessageReceiver', () => {
mockServer = new MockServer('ws://localhost:8081'); mockServer = new MockServer('ws://localhost:8081');
messageReceiver = new textsecure.MessageReceiver( messageReceiver = new textsecure.MessageReceiver(
'oldUsername', 'oldUsername.3',
'username', 'username.3',
'password', 'password',
'signalingKey', 'signalingKey',
{ {

View file

@ -1,136 +0,0 @@
// Copyright 2016-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global libsignal, textsecure */
describe('Protocol Wrapper', function protocolWrapperDescribe() {
const store = textsecure.storage.protocol;
const identifier = '+15559999999';
this.timeout(5000);
before(async function thisNeeded() {
localStorage.clear();
this.identityKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
await textsecure.storage.protocol.saveIdentity(
identifier,
this.identityKeyPair.pubKey
);
});
describe('processPreKey', () => {
beforeEach(function thisNeeded() {
const address = new libsignal.SignalProtocolAddress(identifier, 1);
this.builder = new libsignal.SessionBuilder(store, address);
});
it('can process prekeys', async function thisNeeded() {
const signedPreKey = await libsignal.KeyHelper.generateSignedPreKey(
this.identityKeyPair,
123
);
await this.builder.processPreKey({
identityKey: this.identityKeyPair.pubKey,
registrationId: 1,
preKey: {
keyId: 1,
publicKey: this.identityKeyPair.pubKey,
},
signedPreKey: {
keyId: 123,
publicKey: signedPreKey.keyPair.pubKey,
signature: signedPreKey.signature,
},
});
});
it('rejects if the identity key changes', function thisNeeded() {
return this.builder
.processPreKey({
identityKey: textsecure.crypto.getRandomBytes(33),
})
.then(() => {
throw new Error('Allowed to overwrite identity key');
})
.catch(e => {
assert.strictEqual(e.message, 'Identity key changed');
});
});
it('rejects with a bad prekey signature', async function thisNeeded() {
const signedPreKey = await libsignal.KeyHelper.generateSignedPreKey(
this.identityKeyPair,
123
);
const bogusSignature = textsecure.crypto.getRandomBytes(64);
return this.builder
.processPreKey({
identityKey: this.identityKeyPair.pubKey,
signedPreKey: {
keyId: 123,
publicKey: signedPreKey.keyPair.pubKey,
signature: bogusSignature,
},
})
.then(() => {
throw new Error("Didn't reject an invalid signature");
})
.catch(e => {
assert.strictEqual(e.message, 'Signature verification failed');
});
});
it('rejects with a prekey signature for a different identity', async function thisNeeded() {
const bogusSignedPreKey = await libsignal.KeyHelper.generateSignedPreKey(
await libsignal.KeyHelper.generateIdentityKeyPair(),
123
);
return this.builder
.processPreKey({
identityKey: this.identityKeyPair.pubKey,
signedPreKey: {
keyId: 123,
publicKey: bogusSignedPreKey.keyPair.pubKey,
signature: bogusSignedPreKey.signature,
},
})
.then(() => {
throw new Error("Didn't reject an invalid signature");
})
.catch(e => {
assert.strictEqual(e.message, 'Signature verification failed');
});
});
});
describe('cleanOldMessageKeys', () => {
it('should clean old message keys', () => {
const messageKeys = {};
const LIMIT = 2000;
for (let i = 0; i < 2 * LIMIT; i += 1) {
messageKeys[i] = i;
}
libsignal.SessionCipher.cleanOldMessageKeys(messageKeys);
for (let i = 0; i < LIMIT; i += 1) {
assert(
!Object.prototype.hasOwnProperty.call(messageKeys, i),
`should delete old key ${i}`
);
}
for (let i = LIMIT; i < 2 * LIMIT; i += 1) {
assert(
Object.prototype.hasOwnProperty.call(messageKeys, i),
`should have fresh key ${i}`
);
}
});
});
});

View file

@ -1,18 +1,18 @@
// Copyright 2015-2020 Signal Messenger, LLC // Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global libsignal, textsecure, storage, ConversationController */ /* global textsecure, storage, ConversationController */
describe('SignalProtocolStore', () => { describe('SignalProtocolStore', () => {
const store = textsecure.storage.protocol; const store = textsecure.storage.protocol;
const identifier = '+5558675309'; const identifier = '+5558675309';
const identityKey = { const identityKey = {
pubKey: libsignal.crypto.getRandomBytes(33), pubKey: window.Signal.Crypto.getRandomBytes(33),
privKey: libsignal.crypto.getRandomBytes(32), privKey: window.Signal.Crypto.getRandomBytes(32),
}; };
const testKey = { const testKey = {
pubKey: libsignal.crypto.getRandomBytes(33), pubKey: window.Signal.Crypto.getRandomBytes(33),
privKey: libsignal.crypto.getRandomBytes(32), privKey: window.Signal.Crypto.getRandomBytes(32),
}; };
before(async () => { before(async () => {
localStorage.clear(); localStorage.clear();
@ -40,7 +40,7 @@ describe('SignalProtocolStore', () => {
assertEqualArrayBuffers(key, testKey.pubKey); assertEqualArrayBuffers(key, testKey.pubKey);
}); });
it('returns whether a key is trusted', async () => { it('returns whether a key is trusted', async () => {
const newIdentity = libsignal.crypto.getRandomBytes(33); const newIdentity = window.Signal.Crypto.getRandomBytes(33);
await store.saveIdentity(identifier, testKey.pubKey); await store.saveIdentity(identifier, testKey.pubKey);
const trusted = await store.isTrustedIdentity(identifier, newIdentity); const trusted = await store.isTrustedIdentity(identifier, newIdentity);

View file

@ -30,7 +30,9 @@ describe('WebSocket-Resource', () => {
assert.strictEqual(request.path, '/some/path'); assert.strictEqual(request.path, '/some/path');
assertEqualArrayBuffers( assertEqualArrayBuffers(
request.body.toArrayBuffer(), request.body.toArrayBuffer(),
new Uint8Array([1, 2, 3]).buffer window.Signal.Crypto.typedArrayToArrayBuffer(
new Uint8Array([1, 2, 3])
)
); );
request.respond(200, 'OK'); request.respond(200, 'OK');
}, },
@ -45,7 +47,9 @@ describe('WebSocket-Resource', () => {
id: requestId, id: requestId,
verb: 'PUT', verb: 'PUT',
path: '/some/path', path: '/some/path',
body: new Uint8Array([1, 2, 3]).buffer, body: window.Signal.Crypto.typedArrayToArrayBuffer(
new Uint8Array([1, 2, 3])
),
}, },
}) })
.encode() .encode()
@ -70,7 +74,9 @@ describe('WebSocket-Resource', () => {
assert.strictEqual(message.request.path, '/some/path'); assert.strictEqual(message.request.path, '/some/path');
assertEqualArrayBuffers( assertEqualArrayBuffers(
message.request.body.toArrayBuffer(), message.request.body.toArrayBuffer(),
new Uint8Array([1, 2, 3]).buffer window.Signal.Crypto.typedArrayToArrayBuffer(
new Uint8Array([1, 2, 3])
)
); );
requestId = message.request.id; requestId = message.request.id;
}, },
@ -82,7 +88,9 @@ describe('WebSocket-Resource', () => {
resource.sendRequest({ resource.sendRequest({
verb: 'PUT', verb: 'PUT',
path: '/some/path', path: '/some/path',
body: new Uint8Array([1, 2, 3]).buffer, body: window.Signal.Crypto.typedArrayToArrayBuffer(
new Uint8Array([1, 2, 3])
),
error: done, error: done,
success(message, status) { success(message, status) {
assert.strictEqual(message, 'OK'); assert.strictEqual(message, 'OK');

View file

@ -100,7 +100,7 @@
"intl-tel-input": "12.1.15", "intl-tel-input": "12.1.15",
"jquery": "3.5.0", "jquery": "3.5.0",
"js-yaml": "3.13.1", "js-yaml": "3.13.1",
"libsignal-client": "https://github.com/signalapp/libsignal-client-node.git#f10fbd04eb6efb396eb12c25429761e8785dc9d0", "libsignal-client": "https://github.com/signalapp/libsignal-client-node.git#afa8f40eaa218b7fff94278d9b2ab2e13a2ee04b",
"linkify-it": "2.2.0", "linkify-it": "2.2.0",
"lodash": "4.17.20", "lodash": "4.17.20",
"lru-cache": "6.0.0", "lru-cache": "6.0.0",
@ -386,6 +386,7 @@
"_locales/**", "_locales/**",
"protos/*", "protos/*",
"js/**", "js/**",
"libtextsecure/**",
"ts/**/*.js", "ts/**/*.js",
"ts/*.js", "ts/*.js",
"stylesheets/*.css", "stylesheets/*.css",

View file

@ -11,7 +11,6 @@ let preloadEndTime = 0;
try { try {
const electron = require('electron'); const electron = require('electron');
const semver = require('semver'); const semver = require('semver');
const client = require('libsignal-client');
const _ = require('lodash'); const _ = require('lodash');
const { installGetter, installSetter } = require('./preload_utils'); const { installGetter, installSetter } = require('./preload_utils');
const { const {
@ -426,6 +425,7 @@ try {
window.nodeSetImmediate = setImmediate; window.nodeSetImmediate = setImmediate;
window.Backbone = require('backbone');
window.textsecure = require('./ts/textsecure').default; window.textsecure = require('./ts/textsecure').default;
window.synchronousCrypto = require('./ts/util/synchronousCrypto'); window.synchronousCrypto = require('./ts/util/synchronousCrypto');
@ -506,7 +506,6 @@ try {
window.ReactDOM = require('react-dom'); window.ReactDOM = require('react-dom');
window.moment = require('moment'); window.moment = require('moment');
window.PQueue = require('p-queue').default; window.PQueue = require('p-queue').default;
window.Backbone = require('backbone');
const Signal = require('./js/modules/signal'); const Signal = require('./js/modules/signal');
const i18n = require('./js/modules/i18n'); const i18n = require('./js/modules/i18n');
@ -548,128 +547,9 @@ try {
require('./ts/backbone/views/whisper_view'); require('./ts/backbone/views/whisper_view');
require('./ts/backbone/views/toast_view'); require('./ts/backbone/views/toast_view');
require('./ts/views/conversation_view'); require('./ts/views/conversation_view');
require('./ts/LibSignalStore'); require('./ts/SignalProtocolStore');
require('./ts/background'); require('./ts/background');
function wrapWithPromise(fn) {
return (...args) => Promise.resolve(fn(...args));
}
const externalCurve = {
generateKeyPair: () => {
const privKey = client.PrivateKey.generate();
const pubKey = privKey.getPublicKey();
return {
privKey: privKey.serialize().buffer,
pubKey: pubKey.serialize().buffer,
};
},
createKeyPair: incomingKey => {
const incomingKeyBuffer = Buffer.from(incomingKey);
if (incomingKeyBuffer.length !== 32) {
throw new Error('key must be 32 bytes long');
}
// eslint-disable-next-line no-bitwise
incomingKeyBuffer[0] &= 248;
// eslint-disable-next-line no-bitwise
incomingKeyBuffer[31] &= 127;
// eslint-disable-next-line no-bitwise
incomingKeyBuffer[31] |= 64;
const privKey = client.PrivateKey.deserialize(incomingKeyBuffer);
const pubKey = privKey.getPublicKey();
return {
privKey: privKey.serialize().buffer,
pubKey: pubKey.serialize().buffer,
};
},
calculateAgreement: (pubKey, privKey) => {
const pubKeyBuffer = Buffer.from(pubKey);
const privKeyBuffer = Buffer.from(privKey);
const pubKeyObj = client.PublicKey.deserialize(
Buffer.concat([
Buffer.from([0x05]),
externalCurve.validatePubKeyFormat(pubKeyBuffer),
])
);
const privKeyObj = client.PrivateKey.deserialize(privKeyBuffer);
const sharedSecret = privKeyObj.agree(pubKeyObj);
return sharedSecret.buffer;
},
verifySignature: (pubKey, message, signature) => {
const pubKeyBuffer = Buffer.from(pubKey);
const messageBuffer = Buffer.from(message);
const signatureBuffer = Buffer.from(signature);
const pubKeyObj = client.PublicKey.deserialize(pubKeyBuffer);
const result = !pubKeyObj.verify(messageBuffer, signatureBuffer);
return result;
},
calculateSignature: (privKey, message) => {
const privKeyBuffer = Buffer.from(privKey);
const messageBuffer = Buffer.from(message);
const privKeyObj = client.PrivateKey.deserialize(privKeyBuffer);
const signature = privKeyObj.sign(messageBuffer);
return signature.buffer;
},
validatePubKeyFormat: pubKey => {
if (
pubKey === undefined ||
((pubKey.byteLength !== 33 || new Uint8Array(pubKey)[0] !== 5) &&
pubKey.byteLength !== 32)
) {
throw new Error('Invalid public key');
}
if (pubKey.byteLength === 33) {
return pubKey.slice(1);
}
return pubKey;
},
};
externalCurve.ECDHE = externalCurve.calculateAgreement;
externalCurve.Ed25519Sign = externalCurve.calculateSignature;
externalCurve.Ed25519Verify = externalCurve.verifySignature;
const externalCurveAsync = {
generateKeyPair: wrapWithPromise(externalCurve.generateKeyPair),
createKeyPair: wrapWithPromise(externalCurve.createKeyPair),
calculateAgreement: wrapWithPromise(externalCurve.calculateAgreement),
verifySignature: async (...args) => {
// The async verifySignature function has a different signature than the
// sync function
const verifyFailed = externalCurve.verifySignature(...args);
if (verifyFailed) {
throw new Error('Invalid signature');
}
},
calculateSignature: wrapWithPromise(externalCurve.calculateSignature),
validatePubKeyFormat: wrapWithPromise(externalCurve.validatePubKeyFormat),
ECDHE: wrapWithPromise(externalCurve.ECDHE),
Ed25519Sign: wrapWithPromise(externalCurve.Ed25519Sign),
Ed25519Verify: wrapWithPromise(externalCurve.Ed25519Verify),
};
window.libsignal = window.libsignal || {};
window.libsignal.externalCurve = externalCurve;
window.libsignal.externalCurveAsync = externalCurveAsync;
window.libsignal.HKDF = {};
window.libsignal.HKDF.deriveSecrets = (input, salt, info) => {
const hkdf = client.HKDF.new(3);
const output = hkdf.deriveSecrets(
3 * 32,
Buffer.from(input),
Buffer.from(info),
Buffer.from(salt)
);
return [output.slice(0, 32), output.slice(32, 64), output.slice(64, 96)];
};
// Pulling these in separately since they access filesystem, electron // Pulling these in separately since they access filesystem, electron
window.Signal.Backup = require('./js/modules/backup'); window.Signal.Backup = require('./js/modules/backup');
window.Signal.Debug = require('./js/modules/debug'); window.Signal.Debug = require('./js/modules/debug');

View file

@ -11,6 +11,10 @@
<script type="text/javascript" src="../../js/components.js"></script> <script type="text/javascript" src="../../js/components.js"></script>
<script type="text/javascript" src="../../ts/backbonejQuery.js"></script> <script type="text/javascript" src="../../ts/backbonejQuery.js"></script>
<script type="text/javascript" src="../../js/storage.js"></script> <script type="text/javascript" src="../../js/storage.js"></script>
<script type="text/javascript" src="../../js/libtextsecure.js"></script>
<script type='text/javascript' src='libtextsecure/protocol_wrapper.js'></script>
<script type='text/javascript' src='libtextsecure/storage/user.js'></script>
<script type='text/javascript' src='libtextsecure/storage/unprocessed.js'></script>
<script type='text/javascript' src='libtextsecure/protobufs.js'></script>
</body> </body>
</html> </html>

View file

@ -43,7 +43,7 @@ window.localeMessages = ipc.sendSync('locale-data');
require('../ts/logging/set_up_renderer_logging').initialize(); require('../ts/logging/set_up_renderer_logging').initialize();
require('../ts/LibSignalStore'); require('../ts/SignalProtocolStore');
window.log.info('sticker-creator starting up...'); window.log.info('sticker-creator starting up...');
@ -202,9 +202,9 @@ window.encryptAndUpload = async (
const { value: oldUsername } = oldUsernameItem; const { value: oldUsername } = oldUsernameItem;
const { value: password } = passwordItem; const { value: password } = passwordItem;
const packKey = window.libsignal.crypto.getRandomBytes(32); const packKey = window.Signal.Crypto.getRandomBytes(32);
const encryptionKey = await deriveStickerPackKey(packKey); const encryptionKey = await deriveStickerPackKey(packKey);
const iv = window.libsignal.crypto.getRandomBytes(16); const iv = window.Signal.Crypto.getRandomBytes(16);
const server = WebAPI.connect({ const server = WebAPI.connect({
username: username || oldUsername, username: username || oldUsername,
@ -239,7 +239,7 @@ window.encryptAndUpload = async (
); );
const encryptedStickers = await pMap( const encryptedStickers = await pMap(
uniqueStickers, uniqueStickers,
({ imageData }) => encrypt(imageData.buffer, encryptionKey, iv), ({ imageData }) => encrypt(imageData, encryptionKey, iv),
{ {
concurrency: 3, concurrency: 3,
timeout: 1000 * 60 * 2, timeout: 1000 * 60 * 2,

View file

@ -1,7 +1,7 @@
// Copyright 2017-2020 Signal Messenger, LLC // Copyright 2017-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global Signal, Whisper, textsecure, _, libsignal */ /* global Signal, Whisper, textsecure, _ */
/* eslint-disable no-console */ /* eslint-disable no-console */
@ -292,7 +292,7 @@ describe('Backup', () => {
loadAttachmentData, loadAttachmentData,
} = window.Signal.Migrations; } = window.Signal.Migrations;
const staticKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair(); const staticKeyPair = window.Signal.Curve.generateKeyPair();
const attachmentsPattern = normalizePath( const attachmentsPattern = normalizePath(
path.join(attachmentsPath, '**') path.join(attachmentsPath, '**')
); );
@ -303,13 +303,8 @@ describe('Backup', () => {
const CONVERSATION_ID = 'bdaa7f4f-e9bd-493e-ab0d-8331ad604269'; const CONVERSATION_ID = 'bdaa7f4f-e9bd-493e-ab0d-8331ad604269';
const toArrayBuffer = nodeBuffer => const getFixture = target =>
nodeBuffer.buffer.slice( window.Signal.Crypto.typedArrayToArrayBuffer(fse.readFileSync(target));
nodeBuffer.byteOffset,
nodeBuffer.byteOffset + nodeBuffer.byteLength
);
const getFixture = target => toArrayBuffer(fse.readFileSync(target));
const FIXTURES = { const FIXTURES = {
gif: getFixture('fixtures/giphy-7GFfijngKbeNy.gif'), gif: getFixture('fixtures/giphy-7GFfijngKbeNy.gif'),

View file

@ -1,19 +1,48 @@
// Copyright 2014-2020 Signal Messenger, LLC // Copyright 2014-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global Signal, textsecure, libsignal */
'use strict'; 'use strict';
describe('Crypto', () => { 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 libsignal-client.
assert.instanceOf(part, ArrayBuffer);
assert.strictEqual(part.byteLength, 32);
});
});
});
describe('accessKey/profileKey', () => { describe('accessKey/profileKey', () => {
it('verification roundtrips', async () => { it('verification roundtrips', async () => {
const profileKey = await Signal.Crypto.getRandomBytes(32); const profileKey = await window.Signal.Crypto.getRandomBytes(32);
const accessKey = await Signal.Crypto.deriveAccessKey(profileKey); const accessKey = await window.Signal.Crypto.deriveAccessKey(profileKey);
const verifier = await Signal.Crypto.getAccessKeyVerifier(accessKey); const verifier = await window.Signal.Crypto.getAccessKeyVerifier(
accessKey
);
const correct = await Signal.Crypto.verifyAccessKey(accessKey, verifier); const correct = await window.Signal.Crypto.verifyAccessKey(
accessKey,
verifier
);
assert.strictEqual(correct, true); assert.strictEqual(correct, true);
}); });
@ -45,11 +74,13 @@ describe('Crypto', () => {
vectors.forEach((vector, index) => { vectors.forEach((vector, index) => {
it(`vector ${index}`, async () => { it(`vector ${index}`, async () => {
const gv1 = Signal.Crypto.hexToArrayBuffer(vector.gv1); const gv1 = window.Signal.Crypto.hexToArrayBuffer(vector.gv1);
const expectedHex = vector.masterKey; const expectedHex = vector.masterKey;
const actual = await Signal.Crypto.deriveMasterKeyFromGroupV1(gv1); const actual = await window.Signal.Crypto.deriveMasterKeyFromGroupV1(
const actualHex = Signal.Crypto.arrayBufferToHex(actual); gv1
);
const actualHex = window.Signal.Crypto.arrayBufferToHex(actual);
assert.strictEqual(actualHex, expectedHex); assert.strictEqual(actualHex, expectedHex);
}); });
@ -63,12 +94,21 @@ describe('Crypto', () => {
message, message,
'binary' 'binary'
).toArrayBuffer(); ).toArrayBuffer();
const key = textsecure.crypto.getRandomBytes(32); const key = window.Signal.Crypto.getRandomBytes(32);
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext); const encrypted = await window.Signal.Crypto.encryptSymmetric(
const decrypted = await Signal.Crypto.decryptSymmetric(key, encrypted); key,
plaintext
);
const decrypted = await window.Signal.Crypto.decryptSymmetric(
key,
encrypted
);
const equal = Signal.Crypto.constantTimeEqual(plaintext, decrypted); const equal = window.Signal.Crypto.constantTimeEqual(
plaintext,
decrypted
);
if (!equal) { if (!equal) {
throw new Error('The output and input did not match!'); throw new Error('The output and input did not match!');
} }
@ -80,14 +120,20 @@ describe('Crypto', () => {
message, message,
'binary' 'binary'
).toArrayBuffer(); ).toArrayBuffer();
const key = textsecure.crypto.getRandomBytes(32); const key = window.Signal.Crypto.getRandomBytes(32);
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext); const encrypted = await window.Signal.Crypto.encryptSymmetric(
key,
plaintext
);
const uintArray = new Uint8Array(encrypted); const uintArray = new Uint8Array(encrypted);
uintArray[2] += 2; uintArray[2] += 2;
try { try {
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer); await window.Signal.Crypto.decryptSymmetric(
key,
window.window.Signal.Crypto.typedArrayToArrayBuffer(uintArray)
);
} catch (error) { } catch (error) {
assert.strictEqual( assert.strictEqual(
error.message, error.message,
@ -105,14 +151,20 @@ describe('Crypto', () => {
message, message,
'binary' 'binary'
).toArrayBuffer(); ).toArrayBuffer();
const key = textsecure.crypto.getRandomBytes(32); const key = window.Signal.Crypto.getRandomBytes(32);
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext); const encrypted = await window.Signal.Crypto.encryptSymmetric(
key,
plaintext
);
const uintArray = new Uint8Array(encrypted); const uintArray = new Uint8Array(encrypted);
uintArray[uintArray.length - 3] += 2; uintArray[uintArray.length - 3] += 2;
try { try {
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer); await window.Signal.Crypto.decryptSymmetric(
key,
window.window.Signal.Crypto.typedArrayToArrayBuffer(uintArray)
);
} catch (error) { } catch (error) {
assert.strictEqual( assert.strictEqual(
error.message, error.message,
@ -130,14 +182,20 @@ describe('Crypto', () => {
message, message,
'binary' 'binary'
).toArrayBuffer(); ).toArrayBuffer();
const key = textsecure.crypto.getRandomBytes(32); const key = window.Signal.Crypto.getRandomBytes(32);
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext); const encrypted = await window.Signal.Crypto.encryptSymmetric(
key,
plaintext
);
const uintArray = new Uint8Array(encrypted); const uintArray = new Uint8Array(encrypted);
uintArray[35] += 9; uintArray[35] += 9;
try { try {
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer); await window.Signal.Crypto.decryptSymmetric(
key,
window.window.Signal.Crypto.typedArrayToArrayBuffer(uintArray)
);
} catch (error) { } catch (error) {
assert.strictEqual( assert.strictEqual(
error.message, error.message,
@ -153,13 +211,13 @@ describe('Crypto', () => {
describe('encrypted device name', () => { describe('encrypted device name', () => {
it('roundtrips', async () => { it('roundtrips', async () => {
const deviceName = 'v1.19.0 on Windows 10'; const deviceName = 'v1.19.0 on Windows 10';
const identityKey = await libsignal.KeyHelper.generateIdentityKeyPair(); const identityKey = window.Signal.Curve.generateKeyPair();
const encrypted = await Signal.Crypto.encryptDeviceName( const encrypted = await window.Signal.Crypto.encryptDeviceName(
deviceName, deviceName,
identityKey.pubKey identityKey.pubKey
); );
const decrypted = await Signal.Crypto.decryptDeviceName( const decrypted = await window.Signal.Crypto.decryptDeviceName(
encrypted, encrypted,
identityKey.privKey identityKey.privKey
); );
@ -169,15 +227,18 @@ describe('Crypto', () => {
it('fails if iv is changed', async () => { it('fails if iv is changed', async () => {
const deviceName = 'v1.19.0 on Windows 10'; const deviceName = 'v1.19.0 on Windows 10';
const identityKey = await libsignal.KeyHelper.generateIdentityKeyPair(); const identityKey = window.Signal.Curve.generateKeyPair();
const encrypted = await Signal.Crypto.encryptDeviceName( const encrypted = await window.Signal.Crypto.encryptDeviceName(
deviceName, deviceName,
identityKey.pubKey identityKey.pubKey
); );
encrypted.syntheticIv = Signal.Crypto.getRandomBytes(16); encrypted.syntheticIv = window.Signal.Crypto.getRandomBytes(16);
try { try {
await Signal.Crypto.decryptDeviceName(encrypted, identityKey.privKey); await window.Signal.Crypto.decryptDeviceName(
encrypted,
identityKey.privKey
);
} catch (error) { } catch (error) {
assert.strictEqual( assert.strictEqual(
error.message, error.message,
@ -189,52 +250,174 @@ describe('Crypto', () => {
describe('attachment encryption', () => { describe('attachment encryption', () => {
it('roundtrips', async () => { it('roundtrips', async () => {
const staticKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair(); const staticKeyPair = window.Signal.Curve.generateKeyPair();
const message = 'this is my message'; const message = 'this is my message';
const plaintext = Signal.Crypto.bytesFromString(message); const plaintext = window.Signal.Crypto.bytesFromString(message);
const path = const path =
'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa'; 'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa';
const encrypted = await Signal.Crypto.encryptAttachment( const encrypted = await window.Signal.Crypto.encryptAttachment(
staticKeyPair.pubKey.slice(1), staticKeyPair.pubKey.slice(1),
path, path,
plaintext plaintext
); );
const decrypted = await Signal.Crypto.decryptAttachment( const decrypted = await window.Signal.Crypto.decryptAttachment(
staticKeyPair.privKey, staticKeyPair.privKey,
path, path,
encrypted encrypted
); );
const equal = Signal.Crypto.constantTimeEqual(plaintext, decrypted); const equal = window.Signal.Crypto.constantTimeEqual(
plaintext,
decrypted
);
if (!equal) { if (!equal) {
throw new Error('The output and input did not match!'); 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', () => { describe('uuidToArrayBuffer', () => {
const { uuidToArrayBuffer } = Signal.Crypto; const { uuidToArrayBuffer } = window.Signal.Crypto;
it('converts valid UUIDs to ArrayBuffers', () => { it('converts valid UUIDs to ArrayBuffers', () => {
const expectedResult = new Uint8Array([ const expectedResult = window.window.Signal.Crypto.typedArrayToArrayBuffer(
0x22, new Uint8Array([
0x6e, 0x22,
0x44, 0x6e,
0x02, 0x44,
0x7f, 0x02,
0xfc, 0x7f,
0x45, 0xfc,
0x43, 0x45,
0x85, 0x43,
0xc9, 0x85,
0x46, 0xc9,
0x22, 0x46,
0xc5, 0x22,
0x0a, 0xc5,
0x5b, 0x0a,
0x14, 0x5b,
]).buffer; 0x14,
])
);
assert.deepEqual( assert.deepEqual(
uuidToArrayBuffer('226e4402-7ffc-4543-85c9-4622c50a5b14'), uuidToArrayBuffer('226e4402-7ffc-4543-85c9-4622c50a5b14'),
@ -261,27 +444,29 @@ describe('Crypto', () => {
}); });
describe('arrayBufferToUuid', () => { describe('arrayBufferToUuid', () => {
const { arrayBufferToUuid } = Signal.Crypto; const { arrayBufferToUuid } = window.Signal.Crypto;
it('converts valid ArrayBuffers to UUID strings', () => { it('converts valid ArrayBuffers to UUID strings', () => {
const buf = new Uint8Array([ const buf = window.window.Signal.Crypto.typedArrayToArrayBuffer(
0x22, new Uint8Array([
0x6e, 0x22,
0x44, 0x6e,
0x02, 0x44,
0x7f, 0x02,
0xfc, 0x7f,
0x45, 0xfc,
0x43, 0x45,
0x85, 0x43,
0xc9, 0x85,
0x46, 0xc9,
0x22, 0x46,
0xc5, 0x22,
0x0a, 0xc5,
0x5b, 0x0a,
0x14, 0x5b,
]).buffer; 0x14,
])
);
assert.deepEqual( assert.deepEqual(
arrayBufferToUuid(buf), arrayBufferToUuid(buf),
@ -295,9 +480,19 @@ describe('Crypto', () => {
it('returns undefined if passed the wrong number of bytes', () => { it('returns undefined if passed the wrong number of bytes', () => {
assert.isUndefined(arrayBufferToUuid(new ArrayBuffer(0))); assert.isUndefined(arrayBufferToUuid(new ArrayBuffer(0)));
assert.isUndefined(arrayBufferToUuid(new Uint8Array([0x22]).buffer));
assert.isUndefined( assert.isUndefined(
arrayBufferToUuid(new Uint8Array(Array(17).fill(0x22)).buffer) 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

@ -339,7 +339,12 @@
<script type="text/javascript" src="../js/database.js" data-cover></script> <script type="text/javascript" src="../js/database.js" data-cover></script>
<script type="text/javascript" src="../js/storage.js" data-cover></script> <script type="text/javascript" src="../js/storage.js" data-cover></script>
<script type="text/javascript" src="../js/libtextsecure.js" data-cover></script>
<script type="text/javascript" src="../libtextsecure/protocol_wrapper.js"></script>
<script type="text/javascript" src="../libtextsecure/storage/user.js"></script>
<script type="text/javascript" src="../libtextsecure/storage/unprocessed.js"></script>
<script type="text/javascript" src="../libtextsecure/protobufs.js"></script>
<script type="text/javascript" src="../js/libphonenumber-util.js"></script> <script type="text/javascript" src="../js/libphonenumber-util.js"></script>
<script type="text/javascript" src="../js/models/blockedNumbers.js" data-cover></script> <script type="text/javascript" src="../js/models/blockedNumbers.js" data-cover></script>

View file

@ -1,23 +1,20 @@
// Copyright 2017-2020 Signal Messenger, LLC // Copyright 2017-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global ConversationController, libsignal, SignalProtocolStore, Whisper */ /* global ConversationController, SignalProtocolStore, Whisper */
describe('KeyChangeListener', () => { describe('KeyChangeListener', () => {
const phoneNumberWithKeyChange = '+13016886524'; // nsa const phoneNumberWithKeyChange = '+13016886524'; // nsa
const address = new libsignal.SignalProtocolAddress( const addressString = `${phoneNumberWithKeyChange}.1`;
phoneNumberWithKeyChange, const oldKey = window.Signal.Crypto.getRandomBytes(33);
1 const newKey = window.Signal.Crypto.getRandomBytes(33);
);
const oldKey = libsignal.crypto.getRandomBytes(33);
const newKey = libsignal.crypto.getRandomBytes(33);
let store; let store;
beforeEach(async () => { beforeEach(async () => {
store = new SignalProtocolStore(); store = new SignalProtocolStore();
await store.hydrateCaches(); await store.hydrateCaches();
Whisper.KeyChangeListener.init(store); Whisper.KeyChangeListener.init(store);
return store.saveIdentity(address.toString(), oldKey); return store.saveIdentity(addressString, oldKey);
}); });
afterEach(() => { afterEach(() => {
@ -31,9 +28,7 @@ describe('KeyChangeListener', () => {
id: phoneNumberWithKeyChange, id: phoneNumberWithKeyChange,
type: 'private', type: 'private',
}); });
await window.Signal.Data.saveConversation(convo.attributes, { await window.Signal.Data.saveConversation(convo.attributes);
Conversation: Whisper.Conversation,
});
}); });
after(async () => { after(async () => {
@ -49,11 +44,11 @@ describe('KeyChangeListener', () => {
it('generates a key change notice in the private conversation with this contact', done => { it('generates a key change notice in the private conversation with this contact', done => {
const original = convo.addKeyChange; const original = convo.addKeyChange;
convo.addKeyChange = keyChangedId => { convo.addKeyChange = keyChangedId => {
assert.equal(address.getName(), keyChangedId); assert.equal(phoneNumberWithKeyChange, keyChangedId);
convo.addKeyChange = original; convo.addKeyChange = original;
done(); done();
}; };
store.saveIdentity(address.toString(), newKey); store.saveIdentity(addressString, newKey);
}); });
}); });
@ -70,12 +65,8 @@ describe('KeyChangeListener', () => {
type: 'group', type: 'group',
members: [convo.id], members: [convo.id],
}); });
await window.Signal.Data.saveConversation(convo.attributes, { await window.Signal.Data.saveConversation(convo.attributes);
Conversation: Whisper.Conversation, await window.Signal.Data.saveConversation(groupConvo.attributes);
});
await window.Signal.Data.saveConversation(groupConvo.attributes, {
Conversation: Whisper.Conversation,
});
}); });
after(async () => { after(async () => {
await window.Signal.Data.removeAllMessagesInConversation(groupConvo.id, { await window.Signal.Data.removeAllMessagesInConversation(groupConvo.id, {
@ -93,12 +84,12 @@ describe('KeyChangeListener', () => {
it('generates a key change notice in the group conversation with this contact', done => { it('generates a key change notice in the group conversation with this contact', done => {
const original = groupConvo.addKeyChange; const original = groupConvo.addKeyChange;
groupConvo.addKeyChange = keyChangedId => { groupConvo.addKeyChange = keyChangedId => {
assert.equal(address.getName(), keyChangedId); assert.equal(phoneNumberWithKeyChange, keyChangedId);
groupConvo.addKeyChange = original; groupConvo.addKeyChange = original;
done(); done();
}; };
store.saveIdentity(address.toString(), newKey); store.saveIdentity(addressString, newKey);
}); });
}); });
}); });

View file

@ -3,6 +3,8 @@
import pProps from 'p-props'; import pProps from 'p-props';
import { chunk } from 'lodash'; import { chunk } from 'lodash';
import { HKDF } from 'libsignal-client';
import { calculateAgreement, generateKeyPair } from './Curve';
import { import {
CipherType, CipherType,
@ -13,6 +15,14 @@ import {
sign, sign,
} from './util/synchronousCrypto'; } from './util/synchronousCrypto';
// Generate a number between zero and 16383
export function generateRegistrationId(): number {
const id = new Uint16Array(getRandomBytes(2))[0];
// eslint-disable-next-line no-bitwise
return id & 0x3fff;
}
export function typedArrayToArrayBuffer(typedArray: Uint8Array): ArrayBuffer { export function typedArrayToArrayBuffer(typedArray: Uint8Array): ArrayBuffer {
const ab = new ArrayBuffer(typedArray.length); const ab = new ArrayBuffer(typedArray.length);
// Create a new Uint8Array backed by the ArrayBuffer and copy all values from // Create a new Uint8Array backed by the ArrayBuffer and copy all values from
@ -63,26 +73,37 @@ export async function deriveStickerPackKey(
const salt = getZeroes(32); const salt = getZeroes(32);
const info = bytesFromString('Sticker Pack'); const info = bytesFromString('Sticker Pack');
const [part1, part2] = await window.libsignal.HKDF.deriveSecrets( const [part1, part2] = await deriveSecrets(packKey, salt, info);
packKey,
salt,
info
);
return concatenateBytes(part1, part2); return concatenateBytes(part1, part2);
} }
export function deriveSecrets(
input: ArrayBuffer,
salt: ArrayBuffer,
info: ArrayBuffer
): [ArrayBuffer, ArrayBuffer, ArrayBuffer] {
const hkdf = HKDF.new(3);
const output = hkdf.deriveSecrets(
3 * 32,
Buffer.from(input),
Buffer.from(info),
Buffer.from(salt)
);
return [
typedArrayToArrayBuffer(output.slice(0, 32)),
typedArrayToArrayBuffer(output.slice(32, 64)),
typedArrayToArrayBuffer(output.slice(64, 96)),
];
}
export async function deriveMasterKeyFromGroupV1( export async function deriveMasterKeyFromGroupV1(
groupV1Id: ArrayBuffer groupV1Id: ArrayBuffer
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
const salt = getZeroes(32); const salt = getZeroes(32);
const info = bytesFromString('GV2 Migration'); const info = bytesFromString('GV2 Migration');
const [part1] = await window.libsignal.HKDF.deriveSecrets( const [part1] = await deriveSecrets(groupV1Id, salt, info);
groupV1Id,
salt,
info
);
return part1; return part1;
} }
@ -99,8 +120,8 @@ export async function encryptDeviceName(
identityPublic: ArrayBuffer identityPublic: ArrayBuffer
): Promise<Record<string, ArrayBuffer>> { ): Promise<Record<string, ArrayBuffer>> {
const plaintext = bytesFromString(deviceName); const plaintext = bytesFromString(deviceName);
const ephemeralKeyPair = await window.libsignal.KeyHelper.generateIdentityKeyPair(); const ephemeralKeyPair = generateKeyPair();
const masterSecret = await window.libsignal.Curve.async.calculateAgreement( const masterSecret = calculateAgreement(
identityPublic, identityPublic,
ephemeralKeyPair.privKey ephemeralKeyPair.privKey
); );
@ -133,10 +154,7 @@ export async function decryptDeviceName(
}, },
identityPrivate: ArrayBuffer identityPrivate: ArrayBuffer
): Promise<string> { ): Promise<string> {
const masterSecret = await window.libsignal.Curve.async.calculateAgreement( const masterSecret = calculateAgreement(ephemeralPublic, identityPrivate);
ephemeralPublic,
identityPrivate
);
const key2 = await hmacSha256(masterSecret, bytesFromString('cipher')); const key2 = await hmacSha256(masterSecret, bytesFromString('cipher'));
const cipherKey = await hmacSha256(key2, syntheticIv); const cipherKey = await hmacSha256(key2, syntheticIv);
@ -187,8 +205,8 @@ export async function encryptFile(
uniqueId: ArrayBuffer, uniqueId: ArrayBuffer,
plaintext: ArrayBuffer plaintext: ArrayBuffer
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
const ephemeralKeyPair = await window.libsignal.KeyHelper.generateIdentityKeyPair(); const ephemeralKeyPair = generateKeyPair();
const agreement = await window.libsignal.Curve.async.calculateAgreement( const agreement = calculateAgreement(
staticPublicKey, staticPublicKey,
ephemeralKeyPair.privKey ephemeralKeyPair.privKey
); );
@ -206,10 +224,7 @@ export async function decryptFile(
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
const ephemeralPublicKey = getFirstBytes(data, PUB_KEY_LENGTH); const ephemeralPublicKey = getFirstBytes(data, PUB_KEY_LENGTH);
const ciphertext = getBytes(data, PUB_KEY_LENGTH, data.byteLength); const ciphertext = getBytes(data, PUB_KEY_LENGTH, data.byteLength);
const agreement = await window.libsignal.Curve.async.calculateAgreement( const agreement = calculateAgreement(ephemeralPublicKey, staticPrivateKey);
ephemeralPublicKey,
staticPrivateKey
);
const key = await hmacSha256(agreement, uniqueId); const key = await hmacSha256(agreement, uniqueId);
@ -275,14 +290,14 @@ export async function encryptSymmetric(
const cipherKey = await hmacSha256(key, nonce); const cipherKey = await hmacSha256(key, nonce);
const macKey = await hmacSha256(key, cipherKey); const macKey = await hmacSha256(key, cipherKey);
const cipherText = await _encryptAes256CbcPkcsPadding( const ciphertext = await encryptAes256CbcPkcsPadding(
cipherKey, cipherKey,
iv, plaintext,
plaintext iv
); );
const mac = getFirstBytes(await hmacSha256(macKey, cipherText), MAC_LENGTH); const mac = getFirstBytes(await hmacSha256(macKey, ciphertext), MAC_LENGTH);
return concatenateBytes(nonce, cipherText, mac); return concatenateBytes(nonce, ciphertext, mac);
} }
export async function decryptSymmetric( export async function decryptSymmetric(
@ -292,7 +307,7 @@ export async function decryptSymmetric(
const iv = getZeroes(IV_LENGTH); const iv = getZeroes(IV_LENGTH);
const nonce = getFirstBytes(data, NONCE_LENGTH); const nonce = getFirstBytes(data, NONCE_LENGTH);
const cipherText = getBytes( const ciphertext = getBytes(
data, data,
NONCE_LENGTH, NONCE_LENGTH,
data.byteLength - NONCE_LENGTH - MAC_LENGTH data.byteLength - NONCE_LENGTH - MAC_LENGTH
@ -303,7 +318,7 @@ export async function decryptSymmetric(
const macKey = await hmacSha256(key, cipherKey); const macKey = await hmacSha256(key, cipherKey);
const ourMac = getFirstBytes( const ourMac = getFirstBytes(
await hmacSha256(macKey, cipherText), await hmacSha256(macKey, ciphertext),
MAC_LENGTH MAC_LENGTH
); );
if (!constantTimeEqual(theirMac, ourMac)) { if (!constantTimeEqual(theirMac, ourMac)) {
@ -312,7 +327,7 @@ export async function decryptSymmetric(
); );
} }
return _decryptAes256CbcPkcsPadding(cipherKey, iv, cipherText); return decryptAes256CbcPkcsPadding(cipherKey, ciphertext, iv);
} }
export function constantTimeEqual( export function constantTimeEqual(
@ -343,10 +358,37 @@ export async function hmacSha256(
return sign(key, plaintext); return sign(key, plaintext);
} }
export async function _encryptAes256CbcPkcsPadding( // We use part of the constantTimeEqual algorithm from below here, but we allow ourMac
// to be longer than the passed-in length. This allows easy comparisons against
// arbitrary MAC lengths.
export async function verifyHmacSha256(
plaintext: ArrayBuffer,
key: ArrayBuffer, key: ArrayBuffer,
iv: ArrayBuffer, theirMac: ArrayBuffer,
plaintext: ArrayBuffer length: number
): Promise<void> {
const ourMac = await hmacSha256(key, plaintext);
if (theirMac.byteLength !== length || ourMac.byteLength < length) {
throw new Error('Bad MAC length');
}
const a = new Uint8Array(theirMac);
const b = new Uint8Array(ourMac);
let result = 0;
for (let i = 0; i < theirMac.byteLength; i += 1) {
// eslint-disable-next-line no-bitwise
result |= a[i] ^ b[i];
}
if (result !== 0) {
throw new Error('Bad MAC');
}
}
export async function encryptAes256CbcPkcsPadding(
key: ArrayBuffer,
plaintext: ArrayBuffer,
iv: ArrayBuffer
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
const algorithm = { const algorithm = {
name: 'AES-CBC', name: 'AES-CBC',
@ -369,10 +411,10 @@ export async function _encryptAes256CbcPkcsPadding(
return window.crypto.subtle.encrypt(algorithm, cryptoKey, plaintext); return window.crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
} }
export async function _decryptAes256CbcPkcsPadding( export async function decryptAes256CbcPkcsPadding(
key: ArrayBuffer, key: ArrayBuffer,
iv: ArrayBuffer, ciphertext: ArrayBuffer,
plaintext: ArrayBuffer iv: ArrayBuffer
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
const algorithm = { const algorithm = {
name: 'AES-CBC', name: 'AES-CBC',
@ -392,7 +434,7 @@ export async function _decryptAes256CbcPkcsPadding(
['decrypt'] ['decrypt']
); );
return window.crypto.subtle.decrypt(algorithm, cryptoKey, plaintext); return window.crypto.subtle.decrypt(algorithm, cryptoKey, ciphertext);
} }
export async function encryptAesCtr( export async function encryptAesCtr(
@ -531,7 +573,7 @@ export function getViewOfArrayBuffer(
const source = new Uint8Array(buffer); const source = new Uint8Array(buffer);
const result = source.slice(start, finish); const result = source.slice(start, finish);
return result.buffer; return window.Signal.Crypto.typedArrayToArrayBuffer(result);
} }
export function concatenateBytes( export function concatenateBytes(
@ -627,7 +669,10 @@ export async function encryptCdsDiscoveryRequest(
// Long.fromString handles numbers with or without a leading '+' // Long.fromString handles numbers with or without a leading '+'
numbersArray.writeLong(window.dcodeIO.ByteBuffer.Long.fromString(number)); numbersArray.writeLong(window.dcodeIO.ByteBuffer.Long.fromString(number));
}); });
const queryDataPlaintext = concatenateBytes(nonce, numbersArray.buffer); const queryDataPlaintext = concatenateBytes(
nonce,
numbersArray.toArrayBuffer()
);
const queryDataKey = getRandomBytes(32); const queryDataKey = getRandomBytes(32);
const commitment = sha256(queryDataPlaintext); const commitment = sha256(queryDataPlaintext);
const iv = getRandomBytes(12); const iv = getRandomBytes(12);
@ -680,9 +725,11 @@ export function uuidToArrayBuffer(uuid: string): ArrayBuffer {
return new ArrayBuffer(0); return new ArrayBuffer(0);
} }
return Uint8Array.from( return typedArrayToArrayBuffer(
chunk(uuid.replace(/-/g, ''), 2).map(pair => parseInt(pair.join(''), 16)) Uint8Array.from(
).buffer; chunk(uuid.replace(/-/g, ''), 2).map(pair => parseInt(pair.join(''), 16))
)
);
} }
export function arrayBufferToUuid( export function arrayBufferToUuid(

176
ts/Curve.ts Normal file
View file

@ -0,0 +1,176 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as client from 'libsignal-client';
import { constantTimeEqual, typedArrayToArrayBuffer } from './Crypto';
import {
KeyPairType,
CompatPreKeyType,
CompatSignedPreKeyType,
} from './textsecure/Types.d';
export function isNonNegativeInteger(n: unknown): n is number {
return typeof n === 'number' && n % 1 === 0 && n >= 0;
}
export function generateSignedPreKey(
identityKeyPair: KeyPairType,
keyId: number
): CompatSignedPreKeyType {
if (!isNonNegativeInteger(keyId)) {
throw new TypeError(
`generateSignedPreKey: Invalid argument for keyId: ${keyId}`
);
}
if (
!(identityKeyPair.privKey instanceof ArrayBuffer) ||
identityKeyPair.privKey.byteLength !== 32 ||
!(identityKeyPair.pubKey instanceof ArrayBuffer) ||
identityKeyPair.pubKey.byteLength !== 33
) {
throw new TypeError(
'generateSignedPreKey: Invalid argument for identityKeyPair'
);
}
const keyPair = generateKeyPair();
const signature = calculateSignature(identityKeyPair.privKey, keyPair.pubKey);
return {
keyId,
keyPair,
signature,
};
}
export function generatePreKey(keyId: number): CompatPreKeyType {
if (!isNonNegativeInteger(keyId)) {
throw new TypeError(`generatePreKey: Invalid argument for keyId: ${keyId}`);
}
const keyPair = generateKeyPair();
return {
keyId,
keyPair,
};
}
export function generateKeyPair(): KeyPairType {
const privKey = client.PrivateKey.generate();
const pubKey = privKey.getPublicKey();
return {
privKey: typedArrayToArrayBuffer(privKey.serialize()),
pubKey: typedArrayToArrayBuffer(pubKey.serialize()),
};
}
export function copyArrayBuffer(source: ArrayBuffer): ArrayBuffer {
const sourceArray = new Uint8Array(source);
const target = new ArrayBuffer(source.byteLength);
const targetArray = new Uint8Array(target);
targetArray.set(sourceArray, 0);
return target;
}
export function createKeyPair(incomingKey: ArrayBuffer): KeyPairType {
const copy = copyArrayBuffer(incomingKey);
clampPrivateKey(copy);
if (!constantTimeEqual(copy, incomingKey)) {
window.log.warn('createKeyPair: incoming private key was not clamped!');
}
const incomingKeyBuffer = Buffer.from(incomingKey);
if (incomingKeyBuffer.length !== 32) {
throw new Error('key must be 32 bytes long');
}
const privKey = client.PrivateKey.deserialize(incomingKeyBuffer);
const pubKey = privKey.getPublicKey();
return {
privKey: typedArrayToArrayBuffer(privKey.serialize()),
pubKey: typedArrayToArrayBuffer(pubKey.serialize()),
};
}
export function calculateAgreement(
pubKey: ArrayBuffer,
privKey: ArrayBuffer
): ArrayBuffer {
const privKeyBuffer = Buffer.from(privKey);
const pubKeyObj = client.PublicKey.deserialize(
Buffer.concat([
Buffer.from([0x05]),
Buffer.from(validatePubKeyFormat(pubKey)),
])
);
const privKeyObj = client.PrivateKey.deserialize(privKeyBuffer);
const sharedSecret = privKeyObj.agree(pubKeyObj);
return typedArrayToArrayBuffer(sharedSecret);
}
export function verifySignature(
pubKey: ArrayBuffer,
message: ArrayBuffer,
signature: ArrayBuffer
): boolean {
const pubKeyBuffer = Buffer.from(pubKey);
const messageBuffer = Buffer.from(message);
const signatureBuffer = Buffer.from(signature);
const pubKeyObj = client.PublicKey.deserialize(pubKeyBuffer);
const result = pubKeyObj.verify(messageBuffer, signatureBuffer);
return result;
}
export function calculateSignature(
privKey: ArrayBuffer,
plaintext: ArrayBuffer
): ArrayBuffer {
const privKeyBuffer = Buffer.from(privKey);
const plaintextBuffer = Buffer.from(plaintext);
const privKeyObj = client.PrivateKey.deserialize(privKeyBuffer);
const signature = privKeyObj.sign(plaintextBuffer);
return typedArrayToArrayBuffer(signature);
}
export function validatePubKeyFormat(pubKey: ArrayBuffer): ArrayBuffer {
if (
pubKey === undefined ||
((pubKey.byteLength !== 33 || new Uint8Array(pubKey)[0] !== 5) &&
pubKey.byteLength !== 32)
) {
throw new Error('Invalid public key');
}
if (pubKey.byteLength === 33) {
return pubKey.slice(1);
}
return pubKey;
}
export function setPublicKeyTypeByte(publicKey: ArrayBuffer): void {
const byteArray = new Uint8Array(publicKey);
byteArray[0] = 5;
}
export function clampPrivateKey(privateKey: ArrayBuffer): void {
const byteArray = new Uint8Array(privateKey);
// eslint-disable-next-line no-bitwise
byteArray[0] &= 248;
// eslint-disable-next-line no-bitwise
byteArray[31] &= 127;
// eslint-disable-next-line no-bitwise
byteArray[31] |= 64;
}

157
ts/LibSignalStores.ts Normal file
View file

@ -0,0 +1,157 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
/* eslint-disable class-methods-use-this */
import { isNumber } from 'lodash';
import {
Direction,
ProtocolAddress,
SessionStore,
SessionRecord,
IdentityKeyStore,
PreKeyRecord,
PreKeyStore,
PrivateKey,
PublicKey,
SignedPreKeyStore,
SignedPreKeyRecord,
} from 'libsignal-client';
import { freezePreKey, freezeSignedPreKey } from './SignalProtocolStore';
import { typedArrayToArrayBuffer } from './Crypto';
function encodedNameFromAddress(address: ProtocolAddress): string {
const name = address.name();
const deviceId = address.deviceId();
const encodedName = `${name}.${deviceId}`;
return encodedName;
}
export class Sessions extends SessionStore {
async saveSession(
address: ProtocolAddress,
record: SessionRecord
): Promise<void> {
await window.textsecure.storage.protocol.storeSession(
encodedNameFromAddress(address),
record
);
}
async getSession(name: ProtocolAddress): Promise<SessionRecord | null> {
const encodedName = encodedNameFromAddress(name);
const record = await window.textsecure.storage.protocol.loadSession(
encodedName
);
return record || null;
}
}
export class IdentityKeys extends IdentityKeyStore {
async getIdentityKey(): Promise<PrivateKey> {
const keyPair = await window.textsecure.storage.protocol.getIdentityKeyPair();
if (!keyPair) {
throw new Error('IdentityKeyStore/getIdentityKey: No identity key!');
}
return PrivateKey.deserialize(Buffer.from(keyPair.privKey));
}
async getLocalRegistrationId(): Promise<number> {
const id = await window.textsecure.storage.protocol.getLocalRegistrationId();
if (!isNumber(id)) {
throw new Error(
'IdentityKeyStore/getLocalRegistrationId: No registration id!'
);
}
return id;
}
async getIdentity(address: ProtocolAddress): Promise<PublicKey | null> {
const encodedName = encodedNameFromAddress(address);
const key = await window.textsecure.storage.protocol.loadIdentityKey(
encodedName
);
if (!key) {
return null;
}
return PublicKey.deserialize(Buffer.from(key));
}
async saveIdentity(name: ProtocolAddress, key: PublicKey): Promise<boolean> {
const encodedName = encodedNameFromAddress(name);
const publicKey = typedArrayToArrayBuffer(key.serialize());
return window.textsecure.storage.protocol.saveIdentity(
encodedName,
publicKey
);
}
async isTrustedIdentity(
name: ProtocolAddress,
key: PublicKey,
direction: Direction
): Promise<boolean> {
const encodedName = encodedNameFromAddress(name);
const publicKey = typedArrayToArrayBuffer(key.serialize());
return window.textsecure.storage.protocol.isTrustedIdentity(
encodedName,
publicKey,
direction
);
}
}
export class PreKeys extends PreKeyStore {
async savePreKey(id: number, record: PreKeyRecord): Promise<void> {
await window.textsecure.storage.protocol.storePreKey(
id,
freezePreKey(record)
);
}
async getPreKey(id: number): Promise<PreKeyRecord> {
const preKey = await window.textsecure.storage.protocol.loadPreKey(id);
if (preKey === undefined) {
throw new Error(`getPreKey: PreKey ${id} not found`);
}
return preKey;
}
async removePreKey(id: number): Promise<void> {
await window.textsecure.storage.protocol.removePreKey(id);
}
}
export class SignedPreKeys extends SignedPreKeyStore {
async saveSignedPreKey(
id: number,
record: SignedPreKeyRecord
): Promise<void> {
await window.textsecure.storage.protocol.storeSignedPreKey(
id,
freezeSignedPreKey(record),
true
);
}
async getSignedPreKey(id: number): Promise<SignedPreKeyRecord> {
const signedPreKey = await window.textsecure.storage.protocol.loadSignedPreKey(
id
);
if (!signedPreKey) {
throw new Error(`getSignedPreKey: SignedPreKey ${id} not found`);
}
return signedPreKey;
}
}

File diff suppressed because it is too large Load diff

229
ts/libsignal.d.ts vendored
View file

@ -1,229 +0,0 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export type LibSignalType = {
externalCurve?: CurveType;
crypto: {
encrypt: (
key: ArrayBuffer,
data: ArrayBuffer,
iv: ArrayBuffer
) => Promise<ArrayBuffer>;
decrypt: (
key: ArrayBuffer,
data: ArrayBuffer,
iv: ArrayBuffer
) => Promise<ArrayBuffer>;
calculateMAC: (key: ArrayBuffer, data: ArrayBuffer) => Promise<ArrayBuffer>;
verifyMAC: (
data: ArrayBuffer,
key: ArrayBuffer,
mac: ArrayBuffer,
length: number
) => Promise<void>;
getRandomBytes: (size: number) => ArrayBuffer;
};
externalCurveAsync: {
calculateAgreement: (
pubKey: ArrayBuffer,
privKey: ArrayBuffer
) => Promise<ArrayBuffer>;
generateKeyPair: () => Promise<{
privKey: ArrayBuffer;
pubKey: ArrayBuffer;
}>;
};
KeyHelper: {
generateIdentityKeyPair: () => Promise<{
privKey: ArrayBuffer;
pubKey: ArrayBuffer;
}>;
generateRegistrationId: () => number;
generateSignedPreKey: (
identityKeyPair: KeyPairType,
signedKeyId: number
) => Promise<SignedPreKeyType>;
generatePreKey: (keyId: number) => Promise<PreKeyType>;
};
Curve: {
generateKeyPair: () => KeyPairType;
createKeyPair: (privKey: ArrayBuffer) => KeyPairType;
calculateAgreement: (
pubKey: ArrayBuffer,
privKey: ArrayBuffer
) => ArrayBuffer;
verifySignature: (
pubKey: ArrayBuffer,
msg: ArrayBuffer,
sig: ArrayBuffer
) => void;
calculateSignature: (
privKey: ArrayBuffer,
message: ArrayBuffer
) => ArrayBuffer | Promise<ArrayBuffer>;
validatePubKeyFormat: (buffer: ArrayBuffer) => ArrayBuffer;
async: CurveType;
};
HKDF: {
deriveSecrets: (
packKey: ArrayBuffer,
salt: ArrayBuffer,
// The string is a bit crazy, but ProvisioningCipher currently passes in a string
info?: ArrayBuffer | string
) => Promise<Array<ArrayBuffer>>;
};
worker: {
startWorker: () => void;
stopWorker: () => void;
};
FingerprintGenerator: typeof FingerprintGeneratorClass;
SessionBuilder: typeof SessionBuilderClass;
SessionCipher: typeof SessionCipherClass;
SignalProtocolAddress: typeof SignalProtocolAddressClass;
};
export type KeyPairType = {
pubKey: ArrayBuffer;
privKey: ArrayBuffer;
};
export type SignedPreKeyType = {
keyId: number;
keyPair: KeyPairType;
signature: ArrayBuffer;
};
export type PreKeyType = {
keyId: number;
keyPair: KeyPairType;
};
type RecordType = {
archiveCurrentState: () => void;
deleteAllSessions: () => void;
getOpenSession: () => void;
getSessionByBaseKey: () => void;
getSessions: () => void;
haveOpenSession: () => void;
promoteState: () => void;
serialize: () => void;
updateSessionState: () => void;
};
type CurveType = {
generateKeyPair: () => Promise<KeyPairType>;
createKeyPair: (privKey: ArrayBuffer) => Promise<KeyPairType>;
calculateAgreement: (
pubKey: ArrayBuffer,
privKey: ArrayBuffer
) => Promise<ArrayBuffer>;
verifySignature: (
pubKey: ArrayBuffer,
msg: ArrayBuffer,
sig: ArrayBuffer
) => Promise<void>;
calculateSignature: (
privKey: ArrayBuffer,
message: ArrayBuffer
) => ArrayBuffer | Promise<ArrayBuffer>;
validatePubKeyFormat: (buffer: ArrayBuffer) => ArrayBuffer;
};
type SessionRecordType = any;
export type StorageType = {
Direction: {
SENDING: number;
RECEIVING: number;
};
getIdentityKeyPair: () => Promise<KeyPairType>;
getLocalRegistrationId: () => Promise<number>;
isTrustedIdentity: () => Promise<void>;
loadPreKey: (
encodedAddress: string,
publicKey: ArrayBuffer | undefined,
direction: number
) => Promise<void>;
loadSession: (encodedAddress: string) => Promise<SessionRecordType>;
loadSignedPreKey: (keyId: number) => Promise<SignedPreKeyType>;
removePreKey: (keyId: number) => Promise<void>;
saveIdentity: (
encodedAddress: string,
publicKey: ArrayBuffer,
nonblockingApproval?: boolean
) => Promise<boolean>;
storeSession: (
encodedAddress: string,
record: SessionRecordType
) => Promise<void>;
};
declare class FingerprintGeneratorClass {
constructor(iterations: number);
createFor: (
localIdentifier: string,
localIdentityKey: ArrayBuffer,
remoteIdentifier: string,
remoteIdentityKey: ArrayBuffer
) => string;
}
export declare class SignalProtocolAddressClass {
static fromString(encodedAddress: string): SignalProtocolAddressClass;
constructor(name: string, deviceId: number);
getName: () => string;
getDeviceId: () => number;
toString: () => string;
equals: (other: SignalProtocolAddressClass) => boolean;
}
type DeviceType = {
deviceId: number;
identityKey: ArrayBuffer;
registrationId: number;
signedPreKey: {
keyId: number;
publicKey: ArrayBuffer;
signature: ArrayBuffer;
};
preKey?: {
keyId: number;
publicKey: ArrayBuffer;
};
};
declare class SessionBuilderClass {
constructor(storage: StorageType, remoteAddress: SignalProtocolAddressClass);
processPreKey: (device: DeviceType) => Promise<void>;
processV3: (record: RecordType, message: any) => Promise<void>;
}
export declare class SessionCipherClass {
constructor(
storage: StorageType,
remoteAddress: SignalProtocolAddressClass | string,
options?: { messageKeysLimit?: number | boolean }
);
closeOpenSessionForDevice: () => Promise<void>;
decryptPreKeyWhisperMessage: (
buffer: ArrayBuffer,
encoding?: string
) => Promise<ArrayBuffer>;
decryptWhisperMessage: (
buffer: ArrayBuffer,
encoding?: string
) => Promise<ArrayBuffer>;
deleteAllSessionsForDevice: () => Promise<void>;
encrypt: (
buffer: ArrayBuffer | Uint8Array,
encoding?: string
) => Promise<{
type: number;
registrationId: number;
body: string;
}>;
getRecord: () => Promise<RecordType>;
getSessionVersion: () => Promise<number>;
getRemoteRegistrationId: () => Promise<number>;
hasOpenSession: () => Promise<boolean>;
}

View file

@ -11,6 +11,8 @@ import * as path from 'path';
import pino from 'pino'; import pino from 'pino';
import { createStream } from 'rotating-file-stream'; import { createStream } from 'rotating-file-stream';
import { initLogger, LogLevel as SignalClientLogLevel } from 'libsignal-client';
import { uploadDebugLogs } from './debuglogs'; import { uploadDebugLogs } from './debuglogs';
import { redactAll } from '../../js/modules/privacy'; import { redactAll } from '../../js/modules/privacy';
import { import {
@ -178,3 +180,36 @@ window.addEventListener('unhandledrejection', rejectionEvent => {
error && error.stack ? error.stack : JSON.stringify(error); error && error.stack ? error.stack : JSON.stringify(error);
window.log.error(`Top-level unhandled promise rejection: ${errorString}`); window.log.error(`Top-level unhandled promise rejection: ${errorString}`);
}); });
initLogger(
SignalClientLogLevel.Trace,
(
level: unknown,
target: string,
file: string | null,
line: number | null,
message: string
) => {
let fileString = '';
if (file && line) {
fileString = ` ${file}:${line}`;
} else if (file) {
fileString = ` ${file}`;
}
const logString = `libsignal-client ${message} ${target}${fileString}`;
if (level === SignalClientLogLevel.Trace) {
log.trace(logString);
} else if (level === SignalClientLogLevel.Debug) {
log.debug(logString);
} else if (level === SignalClientLogLevel.Info) {
log.info(logString);
} else if (level === SignalClientLogLevel.Warn) {
log.warn(logString);
} else if (level === SignalClientLogLevel.Error) {
log.error(logString);
} else {
log.error(`${logString} (unknown log level ${level})`);
}
}
);

View file

@ -1,14 +0,0 @@
// Copyright 2018-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export const CURRENT_VERSION = 3;
// This matches Envelope.Type.CIPHERTEXT
export const WHISPER_TYPE = 1;
// This matches Envelope.Type.PREKEY_BUNDLE
export const PREKEY_TYPE = 3;
export const SENDERKEY_TYPE = 4;
export const SENDERKEY_DISTRIBUTION_TYPE = 5;
export const ENCRYPTED_MESSAGE_OVERHEAD = 53;

View file

@ -1,781 +0,0 @@
// Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable class-methods-use-this */
import * as z from 'zod';
import * as CiphertextMessage from './CiphertextMessage';
import {
bytesFromString,
concatenateBytes,
constantTimeEqual,
decryptAesCtr,
encryptAesCtr,
fromEncodedBinaryToArrayBuffer,
getViewOfArrayBuffer,
getZeroes,
highBitsToInt,
hmacSha256,
intsToByteHighAndLow,
splitBytes,
trimBytes,
} from '../Crypto';
import { SignalProtocolAddressClass } from '../libsignal.d';
const REVOKED_CERTIFICATES: Array<number> = [];
const CIPHERTEXT_VERSION = 1;
const UNIDENTIFIED_DELIVERY_PREFIX = 'UnidentifiedDelivery';
type MeType = {
number?: string;
uuid?: string;
deviceId: number;
};
type ValidatorType = {
validate(
certificate: SenderCertificateType,
validationTime: number
): Promise<void>;
};
export const enum SenderCertificateMode {
WithE164,
WithoutE164,
}
export const serializedCertificateSchema = z
.object({
expires: z.number().optional(),
serialized: z.instanceof(ArrayBuffer),
})
.nonstrict();
export type SerializedCertificateType = z.infer<
typeof serializedCertificateSchema
>;
type ServerCertificateType = {
id: number;
key: ArrayBuffer;
};
type ServerCertificateWrapperType = {
certificate: ArrayBuffer;
signature: ArrayBuffer;
};
type SenderCertificateType = {
sender?: string;
senderUuid?: string;
senderDevice: number;
expires: number;
identityKey: ArrayBuffer;
signer: ServerCertificateType;
};
type SenderCertificateWrapperType = {
certificate: ArrayBuffer;
signature: ArrayBuffer;
};
type MessageType = {
ephemeralPublic: ArrayBuffer;
encryptedStatic: ArrayBuffer;
encryptedMessage: ArrayBuffer;
};
type InnerMessageType = {
type: number;
senderCertificate: SenderCertificateWrapperType;
content: ArrayBuffer;
};
export type ExplodedServerCertificateType = ServerCertificateType &
ServerCertificateWrapperType &
SerializedCertificateType;
export type ExplodedSenderCertificateType = SenderCertificateType &
SenderCertificateWrapperType &
SerializedCertificateType & {
signer: ExplodedServerCertificateType;
};
type ExplodedMessageType = MessageType &
SerializedCertificateType & { version: number };
type ExplodedInnerMessageType = InnerMessageType &
SerializedCertificateType & {
senderCertificate: ExplodedSenderCertificateType;
};
// public CertificateValidator(ECPublicKey trustRoot)
export function createCertificateValidator(
trustRoot: ArrayBuffer
): ValidatorType {
return {
// public void validate(SenderCertificate certificate, long validationTime)
async validate(
certificate: ExplodedSenderCertificateType,
validationTime: number
): Promise<void> {
const serverCertificate = certificate.signer;
await window.libsignal.Curve.async.verifySignature(
trustRoot,
serverCertificate.certificate,
serverCertificate.signature
);
const serverCertId = serverCertificate.id;
if (REVOKED_CERTIFICATES.includes(serverCertId)) {
throw new Error(
`Server certificate id ${serverCertId} has been revoked`
);
}
await window.libsignal.Curve.async.verifySignature(
serverCertificate.key,
certificate.certificate,
certificate.signature
);
if (validationTime > certificate.expires) {
throw new Error('Certificate is expired');
}
},
};
}
function _decodePoint(serialized: ArrayBuffer, offset = 0): ArrayBuffer {
const view =
offset > 0
? getViewOfArrayBuffer(serialized, offset, serialized.byteLength)
: serialized;
return window.libsignal.Curve.validatePubKeyFormat(view);
}
// public ServerCertificate(byte[] serialized)
export function _createServerCertificateFromBuffer(
serialized: ArrayBuffer
): ExplodedServerCertificateType {
const wrapper = window.textsecure.protobuf.ServerCertificate.decode(
serialized
);
if (!wrapper.certificate || !wrapper.signature) {
throw new Error('Missing fields');
}
const certificate = window.textsecure.protobuf.ServerCertificate.Certificate.decode(
wrapper.certificate.toArrayBuffer()
);
if (!certificate.id || !certificate.key) {
throw new Error('Missing fields');
}
return {
id: certificate.id,
key: certificate.key.toArrayBuffer(),
serialized,
certificate: wrapper.certificate.toArrayBuffer(),
signature: wrapper.signature.toArrayBuffer(),
};
}
// public SenderCertificate(byte[] serialized)
export function _createSenderCertificateFromBuffer(
serialized: ArrayBuffer
): ExplodedSenderCertificateType {
const wrapper = window.textsecure.protobuf.SenderCertificate.decode(
serialized
);
const { signature, certificate } = wrapper;
if (!signature || !certificate) {
throw new Error('Missing fields');
}
const senderCertificate = window.textsecure.protobuf.SenderCertificate.Certificate.decode(
wrapper.certificate.toArrayBuffer()
);
const {
signer,
identityKey,
senderDevice,
expires,
sender,
senderUuid,
} = senderCertificate;
if (
!signer ||
!identityKey ||
!senderDevice ||
!expires ||
!(sender || senderUuid)
) {
throw new Error('Missing fields');
}
return {
sender,
senderUuid,
senderDevice,
expires: expires.toNumber(),
identityKey: identityKey.toArrayBuffer(),
signer: _createServerCertificateFromBuffer(signer.toArrayBuffer()),
certificate: certificate.toArrayBuffer(),
signature: signature.toArrayBuffer(),
serialized,
};
}
// public UnidentifiedSenderMessage(byte[] serialized)
function _createUnidentifiedSenderMessageFromBuffer(
serialized: ArrayBuffer
): ExplodedMessageType {
const uintArray = new Uint8Array(serialized);
const version = highBitsToInt(uintArray[0]);
if (version > CIPHERTEXT_VERSION) {
throw new Error(`Unknown version: ${version}`);
}
const view = getViewOfArrayBuffer(serialized, 1, serialized.byteLength);
const unidentifiedSenderMessage = window.textsecure.protobuf.UnidentifiedSenderMessage.decode(
view
);
if (
!unidentifiedSenderMessage.ephemeralPublic ||
!unidentifiedSenderMessage.encryptedStatic ||
!unidentifiedSenderMessage.encryptedMessage
) {
throw new Error('Missing fields');
}
return {
version,
ephemeralPublic: unidentifiedSenderMessage.ephemeralPublic.toArrayBuffer(),
encryptedStatic: unidentifiedSenderMessage.encryptedStatic.toArrayBuffer(),
encryptedMessage: unidentifiedSenderMessage.encryptedMessage.toArrayBuffer(),
serialized,
};
}
// public UnidentifiedSenderMessage(
// ECPublicKey ephemeral, byte[] encryptedStatic, byte[] encryptedMessage) {
function _createUnidentifiedSenderMessage(
ephemeralPublic: ArrayBuffer,
encryptedStatic: ArrayBuffer,
encryptedMessage: ArrayBuffer
): ExplodedMessageType {
const versionBytes = new Uint8Array([
intsToByteHighAndLow(CIPHERTEXT_VERSION, CIPHERTEXT_VERSION),
]);
const unidentifiedSenderMessage = new window.textsecure.protobuf.UnidentifiedSenderMessage();
unidentifiedSenderMessage.encryptedMessage = encryptedMessage;
unidentifiedSenderMessage.encryptedStatic = encryptedStatic;
unidentifiedSenderMessage.ephemeralPublic = ephemeralPublic;
const messageBytes = unidentifiedSenderMessage.toArrayBuffer();
return {
version: CIPHERTEXT_VERSION,
ephemeralPublic,
encryptedStatic,
encryptedMessage,
serialized: concatenateBytes(versionBytes, messageBytes),
};
}
// public UnidentifiedSenderMessageContent(byte[] serialized)
function _createUnidentifiedSenderMessageContentFromBuffer(
serialized: ArrayBuffer
): ExplodedInnerMessageType {
const TypeEnum =
window.textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
const message = window.textsecure.protobuf.UnidentifiedSenderMessage.Message.decode(
serialized
);
if (!message.type || !message.senderCertificate || !message.content) {
throw new Error('Missing fields');
}
let type;
switch (message.type) {
case TypeEnum.MESSAGE:
type = CiphertextMessage.WHISPER_TYPE;
break;
case TypeEnum.PREKEY_MESSAGE:
type = CiphertextMessage.PREKEY_TYPE;
break;
default:
throw new Error(`Unknown type: ${message.type}`);
}
return {
type,
senderCertificate: _createSenderCertificateFromBuffer(
message.senderCertificate.toArrayBuffer()
),
content: message.content.toArrayBuffer(),
serialized,
};
}
// private int getProtoType(int type)
function _getProtoMessageType(type: number): number {
const TypeEnum =
window.textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
switch (type) {
case CiphertextMessage.WHISPER_TYPE:
return TypeEnum.MESSAGE;
case CiphertextMessage.PREKEY_TYPE:
return TypeEnum.PREKEY_MESSAGE;
default:
throw new Error(`_getProtoMessageType: type '${type}' does not exist`);
}
}
// public UnidentifiedSenderMessageContent(
// int type, SenderCertificate senderCertificate, byte[] content)
function _createUnidentifiedSenderMessageContent(
type: number,
senderCertificate: SerializedCertificateType,
content: ArrayBuffer
): ArrayBuffer {
const innerMessage = new window.textsecure.protobuf.UnidentifiedSenderMessage.Message();
innerMessage.type = _getProtoMessageType(type);
innerMessage.senderCertificate = window.textsecure.protobuf.SenderCertificate.decode(
senderCertificate.serialized
);
innerMessage.content = content;
return innerMessage.toArrayBuffer();
}
export class SecretSessionCipher {
storage: typeof window.textsecure.storage.protocol;
options: { messageKeysLimit?: number | boolean };
SessionCipher: typeof window.libsignal.SessionCipher;
constructor(
storage: typeof window.textsecure.storage.protocol,
options?: { messageKeysLimit?: number | boolean }
) {
this.storage = storage;
// Do this on construction because libsignal won't be available when this file loads
const { SessionCipher } = window.libsignal;
this.SessionCipher = SessionCipher;
this.options = options || {};
}
// public byte[] encrypt(
// SignalProtocolAddress destinationAddress,
// SenderCertificate senderCertificate,
// byte[] paddedPlaintext
// )
async encrypt(
destinationAddress: SignalProtocolAddressClass,
senderCertificate: SerializedCertificateType,
paddedPlaintext: ArrayBuffer
): Promise<ArrayBuffer> {
// Capture this.xxx variables to replicate Java's implicit this syntax
const { SessionCipher } = this;
const signalProtocolStore = this.storage;
const sessionCipher = new SessionCipher(
signalProtocolStore,
destinationAddress,
this.options
);
const message = await sessionCipher.encrypt(paddedPlaintext);
const ourIdentity = await signalProtocolStore.getIdentityKeyPair();
const theirIdentityData = await signalProtocolStore.loadIdentityKey(
destinationAddress.getName()
);
if (!theirIdentityData) {
throw new Error(
'SecretSessionCipher.encrypt: No identity data for recipient!'
);
}
const theirIdentity =
typeof theirIdentityData === 'string'
? fromEncodedBinaryToArrayBuffer(theirIdentityData)
: theirIdentityData;
const ephemeral = await window.libsignal.Curve.async.generateKeyPair();
const ephemeralSalt = concatenateBytes(
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
theirIdentity,
ephemeral.pubKey
);
const ephemeralKeys = await this._calculateEphemeralKeys(
theirIdentity,
ephemeral.privKey,
ephemeralSalt
);
const staticKeyCiphertext = await this._encryptWithSecretKeys(
ephemeralKeys.cipherKey,
ephemeralKeys.macKey,
ourIdentity.pubKey
);
const staticSalt = concatenateBytes(
ephemeralKeys.chainKey,
staticKeyCiphertext
);
const staticKeys = await this._calculateStaticKeys(
theirIdentity,
ourIdentity.privKey,
staticSalt
);
const serializedMessage = _createUnidentifiedSenderMessageContent(
message.type,
senderCertificate,
fromEncodedBinaryToArrayBuffer(message.body)
);
const messageBytes = await this._encryptWithSecretKeys(
staticKeys.cipherKey,
staticKeys.macKey,
serializedMessage
);
const unidentifiedSenderMessage = _createUnidentifiedSenderMessage(
ephemeral.pubKey,
staticKeyCiphertext,
messageBytes
);
return unidentifiedSenderMessage.serialized;
}
// public Pair<SignalProtocolAddress, byte[]> decrypt(
// CertificateValidator validator, byte[] ciphertext, long timestamp)
async decrypt(
validator: ValidatorType,
ciphertext: ArrayBuffer,
timestamp: number,
me?: MeType
): Promise<{
isMe?: boolean;
sender?: SignalProtocolAddressClass;
senderUuid?: SignalProtocolAddressClass;
content?: ArrayBuffer;
}> {
const signalProtocolStore = this.storage;
const ourIdentity = await signalProtocolStore.getIdentityKeyPair();
const wrapper = _createUnidentifiedSenderMessageFromBuffer(ciphertext);
const ephemeralSalt = concatenateBytes(
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
ourIdentity.pubKey,
wrapper.ephemeralPublic
);
const ephemeralKeys = await this._calculateEphemeralKeys(
wrapper.ephemeralPublic,
ourIdentity.privKey,
ephemeralSalt
);
const staticKeyBytes = await this._decryptWithSecretKeys(
ephemeralKeys.cipherKey,
ephemeralKeys.macKey,
wrapper.encryptedStatic
);
const staticKey = _decodePoint(staticKeyBytes, 0);
const staticSalt = concatenateBytes(
ephemeralKeys.chainKey,
wrapper.encryptedStatic
);
const staticKeys = await this._calculateStaticKeys(
staticKey,
ourIdentity.privKey,
staticSalt
);
const messageBytes = await this._decryptWithSecretKeys(
staticKeys.cipherKey,
staticKeys.macKey,
wrapper.encryptedMessage
);
const content = _createUnidentifiedSenderMessageContentFromBuffer(
messageBytes
);
await validator.validate(content.senderCertificate, timestamp);
if (
!constantTimeEqual(content.senderCertificate.identityKey, staticKeyBytes)
) {
throw new Error(
"Sender's certificate key does not match key used in message"
);
}
const { sender, senderUuid, senderDevice } = content.senderCertificate;
if (
me &&
((sender && me.number && sender === me.number) ||
(senderUuid && me.uuid && senderUuid === me.uuid)) &&
senderDevice === me.deviceId
) {
return {
isMe: true,
};
}
const addressE164 = sender
? new window.libsignal.SignalProtocolAddress(sender, senderDevice)
: undefined;
const addressUuid = senderUuid
? new window.libsignal.SignalProtocolAddress(
senderUuid.toLowerCase(),
senderDevice
)
: undefined;
try {
return {
sender: addressE164,
senderUuid: addressUuid,
content: await this._decryptWithUnidentifiedSenderMessage(content),
};
} catch (error) {
if (!error) {
// eslint-disable-next-line no-ex-assign
error = new Error('Decryption error was falsey!');
}
error.sender = addressE164;
error.senderUuid = addressUuid;
throw error;
}
}
// public int getSessionVersion(SignalProtocolAddress remoteAddress) {
getSessionVersion(
remoteAddress: SignalProtocolAddressClass
): Promise<number> {
const { SessionCipher } = this;
const signalProtocolStore = this.storage;
const cipher = new SessionCipher(
signalProtocolStore,
remoteAddress,
this.options
);
return cipher.getSessionVersion();
}
// public int getRemoteRegistrationId(SignalProtocolAddress remoteAddress) {
getRemoteRegistrationId(
remoteAddress: SignalProtocolAddressClass
): Promise<number> {
const { SessionCipher } = this;
const signalProtocolStore = this.storage;
const cipher = new SessionCipher(
signalProtocolStore,
remoteAddress,
this.options
);
return cipher.getRemoteRegistrationId();
}
// Used by outgoing_message.js
closeOpenSessionForDevice(
remoteAddress: SignalProtocolAddressClass
): Promise<void> {
const { SessionCipher } = this;
const signalProtocolStore = this.storage;
const cipher = new SessionCipher(
signalProtocolStore,
remoteAddress,
this.options
);
return cipher.closeOpenSessionForDevice();
}
// private EphemeralKeys calculateEphemeralKeys(
// ECPublicKey ephemeralPublic, ECPrivateKey ephemeralPrivate, byte[] salt)
private async _calculateEphemeralKeys(
ephemeralPublic: ArrayBuffer,
ephemeralPrivate: ArrayBuffer,
salt: ArrayBuffer
): Promise<{
chainKey: ArrayBuffer;
cipherKey: ArrayBuffer;
macKey: ArrayBuffer;
}> {
const ephemeralSecret = await window.libsignal.Curve.async.calculateAgreement(
ephemeralPublic,
ephemeralPrivate
);
const ephemeralDerivedParts = await window.libsignal.HKDF.deriveSecrets(
ephemeralSecret,
salt,
new ArrayBuffer(0)
);
// private EphemeralKeys(byte[] chainKey, byte[] cipherKey, byte[] macKey)
return {
chainKey: ephemeralDerivedParts[0],
cipherKey: ephemeralDerivedParts[1],
macKey: ephemeralDerivedParts[2],
};
}
// private StaticKeys calculateStaticKeys(
// ECPublicKey staticPublic, ECPrivateKey staticPrivate, byte[] salt)
private async _calculateStaticKeys(
staticPublic: ArrayBuffer,
staticPrivate: ArrayBuffer,
salt: ArrayBuffer
): Promise<{ cipherKey: ArrayBuffer; macKey: ArrayBuffer }> {
const staticSecret = await window.libsignal.Curve.async.calculateAgreement(
staticPublic,
staticPrivate
);
const staticDerivedParts = await window.libsignal.HKDF.deriveSecrets(
staticSecret,
salt,
new ArrayBuffer(0)
);
// private StaticKeys(byte[] cipherKey, byte[] macKey)
return {
cipherKey: staticDerivedParts[1],
macKey: staticDerivedParts[2],
};
}
// private byte[] decrypt(UnidentifiedSenderMessageContent message)
private _decryptWithUnidentifiedSenderMessage(
message: ExplodedInnerMessageType
): Promise<ArrayBuffer> {
const { SessionCipher } = this;
const signalProtocolStore = this.storage;
if (!message.senderCertificate) {
throw new Error(
'_decryptWithUnidentifiedSenderMessage: Message had no senderCertificate'
);
}
const { senderUuid, sender, senderDevice } = message.senderCertificate;
const target = senderUuid || sender;
if (!senderDevice || !target) {
throw new Error(
'_decryptWithUnidentifiedSenderMessage: Missing sender information in senderCertificate'
);
}
const address = new window.libsignal.SignalProtocolAddress(
target,
senderDevice
);
switch (message.type) {
case CiphertextMessage.WHISPER_TYPE:
return new SessionCipher(
signalProtocolStore,
address,
this.options
).decryptWhisperMessage(message.content);
case CiphertextMessage.PREKEY_TYPE:
return new SessionCipher(
signalProtocolStore,
address,
this.options
).decryptPreKeyWhisperMessage(message.content);
default:
throw new Error(`Unknown type: ${message.type}`);
}
}
// private byte[] encrypt(
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] plaintext)
private async _encryptWithSecretKeys(
cipherKey: ArrayBuffer,
macKey: ArrayBuffer,
plaintext: ArrayBuffer
): Promise<ArrayBuffer> {
// Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding');
// cipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
// Mac const mac = Mac.getInstance('HmacSHA256');
// mac.init(macKey);
// byte[] const ciphertext = cipher.doFinal(plaintext);
const ciphertext = await encryptAesCtr(cipherKey, plaintext, getZeroes(16));
// byte[] const ourFullMac = mac.doFinal(ciphertext);
const ourFullMac = await hmacSha256(macKey, ciphertext);
const ourMac = trimBytes(ourFullMac, 10);
return concatenateBytes(ciphertext, ourMac);
}
// private byte[] decrypt(
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] ciphertext)
private async _decryptWithSecretKeys(
cipherKey: ArrayBuffer,
macKey: ArrayBuffer,
ciphertext: ArrayBuffer
): Promise<ArrayBuffer> {
if (ciphertext.byteLength < 10) {
throw new Error('Ciphertext not long enough for MAC!');
}
const ciphertextParts = splitBytes(
ciphertext,
ciphertext.byteLength - 10,
10
);
// Mac const mac = Mac.getInstance('HmacSHA256');
// mac.init(macKey);
// byte[] const digest = mac.doFinal(ciphertextParts[0]);
const digest = await hmacSha256(macKey, ciphertextParts[0]);
const ourMac = trimBytes(digest, 10);
const theirMac = ciphertextParts[1];
if (!constantTimeEqual(ourMac, theirMac)) {
throw new Error('SecretSessionCipher/_decryptWithSecretKeys: Bad MAC!');
}
// Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding');
// cipher.init(Cipher.DECRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
// return cipher.doFinal(ciphertextParts[0]);
return decryptAesCtr(cipherKey, ciphertextParts[0], getZeroes(16));
}
}

View file

@ -57,7 +57,7 @@ import {
import { import {
SenderCertificateMode, SenderCertificateMode,
SerializedCertificateType, SerializedCertificateType,
} from '../metadata/SecretSessionCipher'; } from '../textsecure/OutgoingMessage';
import { senderCertificateService } from '../services/senderCertificate'; import { senderCertificateService } from '../services/senderCertificate';
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
@ -2056,8 +2056,7 @@ export class ConversationModel extends window.Backbone.Model<
keyChange = await window.textsecure.storage.protocol.processVerifiedMessage( keyChange = await window.textsecure.storage.protocol.processVerifiedMessage(
this.id, this.id,
verified, verified,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion options.key || undefined
options.key!
); );
} else { } else {
keyChange = await window.textsecure.storage.protocol.setVerified( keyChange = await window.textsecure.storage.protocol.setVerified(
@ -2201,7 +2200,7 @@ export class ConversationModel extends window.Backbone.Model<
); );
} }
setApproved(): boolean | void { async setApproved(): Promise<void> {
if (!this.isPrivate()) { if (!this.isPrivate()) {
throw new Error( throw new Error(
'You cannot set a group conversation as trusted. ' + 'You cannot set a group conversation as trusted. ' +
@ -4523,16 +4522,9 @@ export class ConversationModel extends window.Backbone.Model<
if (changed) { if (changed) {
// save identity will close all sessions except for .1, so we // save identity will close all sessions except for .1, so we
// must close that one manually. // must close that one manually.
const address = new window.libsignal.SignalProtocolAddress( await window.textsecure.storage.protocol.archiveSession(
identifier, `${identifier}.1`
1
); );
window.log.info('closing session for', address.toString());
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address
);
await sessionCipher.closeOpenSessionForDevice();
} }
const accessKey = c.get('accessKey'); const accessKey = c.get('accessKey');

View file

@ -5,7 +5,7 @@ import {
SenderCertificateMode, SenderCertificateMode,
serializedCertificateSchema, serializedCertificateSchema,
SerializedCertificateType, SerializedCertificateType,
} from '../metadata/SecretSessionCipher'; } from '../textsecure/OutgoingMessage';
import { SenderCertificateClass } from '../textsecure'; import { SenderCertificateClass } from '../textsecure';
import { base64ToArrayBuffer } from '../Crypto'; import { base64ToArrayBuffer } from '../Crypto';
import { assert } from '../util/assert'; import { assert } from '../util/assert';

View file

@ -740,7 +740,12 @@ export async function mergeContactRecord(
const verified = await conversation.safeGetVerified(); const verified = await conversation.safeGetVerified();
const storageServiceVerified = contactRecord.identityState || 0; const storageServiceVerified = contactRecord.identityState || 0;
if (verified !== storageServiceVerified) { if (verified !== storageServiceVerified) {
const verifiedOptions = { viaStorageServiceSync: true }; const verifiedOptions = {
key: contactRecord.identityKey
? contactRecord.identityKey.toArrayBuffer()
: undefined,
viaStorageServiceSync: true,
};
const STATE_ENUM = window.textsecure.protobuf.ContactRecord.IdentityState; const STATE_ENUM = window.textsecure.protobuf.ContactRecord.IdentityState;
switch (storageServiceVerified) { switch (storageServiceVerified) {

View file

@ -53,6 +53,7 @@ import {
StickerPackType, StickerPackType,
StickerType, StickerType,
UnprocessedType, UnprocessedType,
UnprocessedUpdateType,
} from './Interface'; } from './Interface';
import Server from './Server'; import Server from './Server';
import { MessageModel } from '../models/messages'; import { MessageModel } from '../models/messages';
@ -1304,11 +1305,14 @@ async function saveUnprocesseds(
async function updateUnprocessedAttempts(id: string, attempts: number) { async function updateUnprocessedAttempts(id: string, attempts: number) {
await channels.updateUnprocessedAttempts(id, attempts); await channels.updateUnprocessedAttempts(id, attempts);
} }
async function updateUnprocessedWithData(id: string, data: UnprocessedType) { async function updateUnprocessedWithData(
id: string,
data: UnprocessedUpdateType
) {
await channels.updateUnprocessedWithData(id, data); await channels.updateUnprocessedWithData(id, data);
} }
async function updateUnprocessedsWithData( async function updateUnprocessedsWithData(
array: Array<{ id: string; data: UnprocessedType }> array: Array<{ id: string; data: UnprocessedUpdateType }>
) { ) {
await channels.updateUnprocessedsWithData(array); await channels.updateUnprocessedsWithData(array);
} }

View file

@ -70,6 +70,7 @@ export type SessionType = {
conversationId: string; conversationId: string;
deviceId: number; deviceId: number;
record: string; record: string;
version?: number;
}; };
export type SignedPreKeyType = { export type SignedPreKeyType = {
confirmed: boolean; confirmed: boolean;
@ -128,6 +129,14 @@ export type UnprocessedType = {
decrypted?: string; decrypted?: string;
}; };
export type UnprocessedUpdateType = {
source?: string;
sourceUuid?: string;
sourceDevice?: string;
serverTimestamp?: number;
decrypted?: string;
};
export type DataInterface = { export type DataInterface = {
close: () => Promise<void>; close: () => Promise<void>;
removeDB: () => Promise<void>; removeDB: () => Promise<void>;
@ -210,10 +219,10 @@ export type DataInterface = {
updateUnprocessedAttempts: (id: string, attempts: number) => Promise<void>; updateUnprocessedAttempts: (id: string, attempts: number) => Promise<void>;
updateUnprocessedWithData: ( updateUnprocessedWithData: (
id: string, id: string,
data: UnprocessedType data: UnprocessedUpdateType
) => Promise<void>; ) => Promise<void>;
updateUnprocessedsWithData: ( updateUnprocessedsWithData: (
array: Array<{ id: string; data: UnprocessedType }> array: Array<{ id: string; data: UnprocessedUpdateType }>
) => Promise<void>; ) => Promise<void>;
getUnprocessedById: (id: string) => Promise<UnprocessedType | undefined>; getUnprocessedById: (id: string) => Promise<UnprocessedType | undefined>;
saveUnprocesseds: ( saveUnprocesseds: (

View file

@ -53,6 +53,7 @@ import {
StickerPackType, StickerPackType,
StickerType, StickerType,
UnprocessedType, UnprocessedType,
UnprocessedUpdateType,
} from './Interface'; } from './Interface';
declare global { declare global {
@ -3517,7 +3518,7 @@ async function updateUnprocessedAttempts(
} }
async function updateUnprocessedWithData( async function updateUnprocessedWithData(
id: string, id: string,
data: UnprocessedType data: UnprocessedUpdateType
): Promise<void> { ): Promise<void> {
const db = getInstance(); const db = getInstance();
const { source, sourceUuid, sourceDevice, serverTimestamp, decrypted } = data; const { source, sourceUuid, sourceDevice, serverTimestamp, decrypted } = data;
@ -3543,7 +3544,7 @@ async function updateUnprocessedWithData(
}); });
} }
async function updateUnprocessedsWithData( async function updateUnprocessedsWithData(
arrayOfUnprocessed: Array<{ id: string; data: UnprocessedType }> arrayOfUnprocessed: Array<{ id: string; data: UnprocessedUpdateType }>
): Promise<void> { ): Promise<void> {
const db = getInstance(); const db = getInstance();

View file

@ -83,17 +83,17 @@ describe('sessionTranslation', () => {
const expected = { const expected = {
currentSession: { currentSession: {
sessionVersion: 1, sessionVersion: 3,
localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444', localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444',
remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA', remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA',
rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=', rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=',
previousCounter: 2, previousCounter: 3,
senderChain: { senderChain: {
senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x', senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x',
senderRatchetKeyPrivate: senderRatchetKeyPrivate:
'5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=', '5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=',
chainKey: { chainKey: {
index: -1, index: 0,
key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=', key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=',
}, },
}, },
@ -101,18 +101,18 @@ describe('sessionTranslation', () => {
{ {
senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz',
chainKey: { chainKey: {
index: 5, index: 6,
key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=',
}, },
messageKeys: [ messageKeys: [
{ {
index: 0, index: 1,
cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=', cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=',
iv: 'TcRanSxZVWbuIq0xDRGnEw==', iv: 'TcRanSxZVWbuIq0xDRGnEw==',
macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=', macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=',
}, },
{ {
index: 4, index: 5,
cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=', cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=',
iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==', iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==',
macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=', macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=',
@ -320,17 +320,17 @@ describe('sessionTranslation', () => {
const expected = { const expected = {
currentSession: { currentSession: {
sessionVersion: 1, sessionVersion: 3,
localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444', localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444',
remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA', remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA',
rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=', rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=',
previousCounter: 2, previousCounter: 3,
senderChain: { senderChain: {
senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x', senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x',
senderRatchetKeyPrivate: senderRatchetKeyPrivate:
'5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=', '5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=',
chainKey: { chainKey: {
index: -1, index: 0,
key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=', key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=',
}, },
}, },
@ -338,18 +338,18 @@ describe('sessionTranslation', () => {
{ {
senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz',
chainKey: { chainKey: {
index: 5, index: 6,
key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=',
}, },
messageKeys: [ messageKeys: [
{ {
index: 0, index: 1,
cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=', cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=',
iv: 'TcRanSxZVWbuIq0xDRGnEw==', iv: 'TcRanSxZVWbuIq0xDRGnEw==',
macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=', macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=',
}, },
{ {
index: 4, index: 5,
cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=', cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=',
iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==', iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==',
macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=', macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=',
@ -359,11 +359,11 @@ describe('sessionTranslation', () => {
{ {
senderRatchetKey: 'BTpb20+IlnBkryDC2ecQT96Hd3t9/Qh3ljnA3509kxRa', senderRatchetKey: 'BTpb20+IlnBkryDC2ecQT96Hd3t9/Qh3ljnA3509kxRa',
chainKey: { chainKey: {
index: 1, index: 2,
}, },
messageKeys: [ messageKeys: [
{ {
index: 0, index: 1,
cipherKey: 'aAbSz5jOagUTgQKo3aqExcl8hyZANrY+HvrLc/OgoQI=', cipherKey: 'aAbSz5jOagUTgQKo3aqExcl8hyZANrY+HvrLc/OgoQI=',
iv: 'JcyLzw0fL67Kd4tfGJ2OUQ==', iv: 'JcyLzw0fL67Kd4tfGJ2OUQ==',
macKey: 'dt+RXeaeIx+ASrKSk7D4guwTE1IUYl3LiLG9aI4sZm8=', macKey: 'dt+RXeaeIx+ASrKSk7D4guwTE1IUYl3LiLG9aI4sZm8=',
@ -373,29 +373,29 @@ describe('sessionTranslation', () => {
{ {
senderRatchetKey: 'Bd5nlMVr6YMBE5eh//tOWMgoOQakkneYri/YuVJpi0pJ', senderRatchetKey: 'Bd5nlMVr6YMBE5eh//tOWMgoOQakkneYri/YuVJpi0pJ',
chainKey: { chainKey: {
index: 11, index: 12,
}, },
messageKeys: [ messageKeys: [
{ {
index: 0, index: 1,
cipherKey: 'pjcY/7MoRGtGHwNN/E8KqoKCx/5mdKp0VCmrmkBAj+M=', cipherKey: 'pjcY/7MoRGtGHwNN/E8KqoKCx/5mdKp0VCmrmkBAj+M=',
iv: 'eBpAEoDj94NsI0vsf+4Hrw==', iv: 'eBpAEoDj94NsI0vsf+4Hrw==',
macKey: 'P7Jz2KkOXC7B0mLkz7JaU/d0vdaYZjAfuKJ86xXB19U=', macKey: 'P7Jz2KkOXC7B0mLkz7JaU/d0vdaYZjAfuKJ86xXB19U=',
}, },
{ {
index: 2, index: 3,
cipherKey: 'EGDj0sc/1TMtSycYDCrpZdl6UCzCzDuMwlAvVVAs2OQ=', cipherKey: 'EGDj0sc/1TMtSycYDCrpZdl6UCzCzDuMwlAvVVAs2OQ=',
iv: 'A+1OA9M2Z8gGlARtA231RA==', iv: 'A+1OA9M2Z8gGlARtA231RA==',
macKey: 'oQ/PQxJDD52qrkShSy6hD3fASEfhWnlmY3qsSPuOY/o=', macKey: 'oQ/PQxJDD52qrkShSy6hD3fASEfhWnlmY3qsSPuOY/o=',
}, },
{ {
index: 3, index: 4,
cipherKey: 'WM3UUILGdECXjO8jZbBVYrPAnzRM8RdiU+PSAyHUT5U=', cipherKey: 'WM3UUILGdECXjO8jZbBVYrPAnzRM8RdiU+PSAyHUT5U=',
iv: 'CWuQIuIyGqApA6MQgnDR5Q==', iv: 'CWuQIuIyGqApA6MQgnDR5Q==',
macKey: 'hg+/xrOKFzn2eK1BnJ5C+ERsFgaWAOaBxQTc4q3b/g8=', macKey: 'hg+/xrOKFzn2eK1BnJ5C+ERsFgaWAOaBxQTc4q3b/g8=',
}, },
{ {
index: 9, index: 10,
cipherKey: 'T0cBaGAseFz+s2njVr4sqbFf1pUH5PoPvdMBoizIT+Y=', cipherKey: 'T0cBaGAseFz+s2njVr4sqbFf1pUH5PoPvdMBoizIT+Y=',
iv: 'hkT2kqgqhlORAjBI7ZDsig==', iv: 'hkT2kqgqhlORAjBI7ZDsig==',
macKey: 'uE/Dd4WSQWkYNRgolcQtOd+HpaHP5wGogMzErkZj+AQ=', macKey: 'uE/Dd4WSQWkYNRgolcQtOd+HpaHP5wGogMzErkZj+AQ=',
@ -405,29 +405,29 @@ describe('sessionTranslation', () => {
{ {
senderRatchetKey: 'BYSxQO1OIs0ZSFN7JI/vF5Rb0VwaKjs+UAAfDkhOYfkp', senderRatchetKey: 'BYSxQO1OIs0ZSFN7JI/vF5Rb0VwaKjs+UAAfDkhOYfkp',
chainKey: { chainKey: {
index: 5, index: 6,
}, },
messageKeys: [ messageKeys: [
{ {
index: 0, index: 1,
cipherKey: 'ni6XhRCoLFud2Zk1zoel4he8znDG/t+TWVBASO35GlQ=', cipherKey: 'ni6XhRCoLFud2Zk1zoel4he8znDG/t+TWVBASO35GlQ=',
iv: 'rKy/sxLmQ4j2DSxbDZTO5A==', iv: 'rKy/sxLmQ4j2DSxbDZTO5A==',
macKey: 'MKxs29AmNOnp6zZOsIbrmSqcVXYJL01kuvIaqwjRNvQ=', macKey: 'MKxs29AmNOnp6zZOsIbrmSqcVXYJL01kuvIaqwjRNvQ=',
}, },
{ {
index: 2, index: 3,
cipherKey: 'Pp7GOD72vfjvb3qx7qm1YVoZKPqnyXC2uqCt89ZA/yc=', cipherKey: 'Pp7GOD72vfjvb3qx7qm1YVoZKPqnyXC2uqCt89ZA/yc=',
iv: 'NuDf5iM0lD/o0YzjHZo4mA==', iv: 'NuDf5iM0lD/o0YzjHZo4mA==',
macKey: 'JkBZiaxmwFr1xh/zzTQE6mlUIVJmSIrqSIQVlaoTz7M=', macKey: 'JkBZiaxmwFr1xh/zzTQE6mlUIVJmSIrqSIQVlaoTz7M=',
}, },
{ {
index: 3, index: 4,
cipherKey: 'zORWRvJEUe2F4UnBwe2YRqPS4GzUFE1lWptcqMzWf2U=', cipherKey: 'zORWRvJEUe2F4UnBwe2YRqPS4GzUFE1lWptcqMzWf2U=',
iv: 'Og7jF9JJhiLtPD8W2OgTnw==', iv: 'Og7jF9JJhiLtPD8W2OgTnw==',
macKey: 'Lxbcl9fL9x5Javtdz7tOV7Bbr8ar3rWxSIsi1Focv9w=', macKey: 'Lxbcl9fL9x5Javtdz7tOV7Bbr8ar3rWxSIsi1Focv9w=',
}, },
{ {
index: 5, index: 6,
cipherKey: 'T/TZNw04+ZfB0s2ltOT9qbzRPnCFn7VvxqHHAvORFx0=', cipherKey: 'T/TZNw04+ZfB0s2ltOT9qbzRPnCFn7VvxqHHAvORFx0=',
iv: 'DpOAK77ErIr2QFTsRnfOew==', iv: 'DpOAK77ErIr2QFTsRnfOew==',
macKey: 'k/fxafepBiA0dQOTpohL+EKm2+1jpFwRigVWt02U/Jg=', macKey: 'k/fxafepBiA0dQOTpohL+EKm2+1jpFwRigVWt02U/Jg=',
@ -437,23 +437,23 @@ describe('sessionTranslation', () => {
{ {
senderRatchetKey: 'BbXSFD/IoivRUvfnPzOaRLqDXEAwi4YEristfwiOj3IJ', senderRatchetKey: 'BbXSFD/IoivRUvfnPzOaRLqDXEAwi4YEristfwiOj3IJ',
chainKey: { chainKey: {
index: 2, index: 3,
}, },
}, },
{ {
senderRatchetKey: 'BRRAnr1NhizgCPPzmYV9qGBpvwCpSQH0Rx+UOtl78wUg', senderRatchetKey: 'BRRAnr1NhizgCPPzmYV9qGBpvwCpSQH0Rx+UOtl78wUg',
chainKey: { chainKey: {
index: 0, index: 1,
}, },
}, },
{ {
senderRatchetKey: 'BZvOKPA+kXiCg8TIP/52fu1reCDirC7wb5nyRGce3y4N', senderRatchetKey: 'BZvOKPA+kXiCg8TIP/52fu1reCDirC7wb5nyRGce3y4N',
chainKey: { chainKey: {
index: 6, index: 7,
}, },
messageKeys: [ messageKeys: [
{ {
index: 4, index: 5,
cipherKey: 'PB44plPzHam/o2LZnyjo8HLRuAvp3uE6ixO5+GUCUsA=', cipherKey: 'PB44plPzHam/o2LZnyjo8HLRuAvp3uE6ixO5+GUCUsA=',
iv: 'JBbgRb10X/dDsn0GKg69dA==', iv: 'JBbgRb10X/dDsn0GKg69dA==',
macKey: 'jKV1Rmlb0HATZHndLDIMONPgOXqT3kwE1QEstxXVe+o=', macKey: 'jKV1Rmlb0HATZHndLDIMONPgOXqT3kwE1QEstxXVe+o=',
@ -463,23 +463,23 @@ describe('sessionTranslation', () => {
{ {
senderRatchetKey: 'Ba9q9bHjMHfbUNDCU8+0O7cmEcIluq+wk3/d2f7q+ThG', senderRatchetKey: 'Ba9q9bHjMHfbUNDCU8+0O7cmEcIluq+wk3/d2f7q+ThG',
chainKey: { chainKey: {
index: 3, index: 4,
}, },
messageKeys: [ messageKeys: [
{ {
index: 0, index: 1,
cipherKey: '4buOJSqRFIpWwo4pXYwQTCTxas4+amBLpZ/CuEWXbPg=', cipherKey: '4buOJSqRFIpWwo4pXYwQTCTxas4+amBLpZ/CuEWXbPg=',
iv: '9uD8ECO/fxtK28OvlCFXuQ==', iv: '9uD8ECO/fxtK28OvlCFXuQ==',
macKey: 'LI0ZSdX7k+cd5bTgs6XEYYIWY+2cxhWI97vAGFpoZIc=', macKey: 'LI0ZSdX7k+cd5bTgs6XEYYIWY+2cxhWI97vAGFpoZIc=',
}, },
{ {
index: 1, index: 2,
cipherKey: 'oNbFxcy2eebUQhoD+NLf12fgkXzhn4EU0Pgqn1bVKOs=', cipherKey: 'oNbFxcy2eebUQhoD+NLf12fgkXzhn4EU0Pgqn1bVKOs=',
iv: 'o1mm4rCN6Q0J1hA7I5jjgA==', iv: 'o1mm4rCN6Q0J1hA7I5jjgA==',
macKey: 'dfHB14sCIdun+RaKnAoyaQPC6qRDMewjqOIDZGmn3Es=', macKey: 'dfHB14sCIdun+RaKnAoyaQPC6qRDMewjqOIDZGmn3Es=',
}, },
{ {
index: 2, index: 3,
cipherKey: '/aU3zX2IdA91GAcB+7H57yzRe+6CgZ61tlW4M/rkCJI=', cipherKey: '/aU3zX2IdA91GAcB+7H57yzRe+6CgZ61tlW4M/rkCJI=',
iv: 'v8VJF467QDD1ZCr1JD8pbQ==', iv: 'v8VJF467QDD1ZCr1JD8pbQ==',
macKey: 'MjK5iYjhZtQTJ4Eu3+qGOdYxn0G23EGRtTcusbzy9OA=', macKey: 'MjK5iYjhZtQTJ4Eu3+qGOdYxn0G23EGRtTcusbzy9OA=',
@ -489,19 +489,19 @@ describe('sessionTranslation', () => {
{ {
senderRatchetKey: 'BTwX5SmcUeBG7mwyOZ3YgxyXIN0ktzuEdWTfBUmPfGYG', senderRatchetKey: 'BTwX5SmcUeBG7mwyOZ3YgxyXIN0ktzuEdWTfBUmPfGYG',
chainKey: { chainKey: {
index: 1, index: 2,
}, },
}, },
{ {
senderRatchetKey: 'BV7ECvKbwKIAD61BXDYr0xr3JtckuKzR1Hw8cVPWGtlo', senderRatchetKey: 'BV7ECvKbwKIAD61BXDYr0xr3JtckuKzR1Hw8cVPWGtlo',
chainKey: { chainKey: {
index: 2, index: 3,
}, },
}, },
{ {
senderRatchetKey: 'BTC7rQqoykGR5Aaix7RkAhI5fSXufc6pVGN9OIC8EW5c', senderRatchetKey: 'BTC7rQqoykGR5Aaix7RkAhI5fSXufc6pVGN9OIC8EW5c',
chainKey: { chainKey: {
index: 0, index: 1,
}, },
}, },
], ],
@ -576,17 +576,17 @@ describe('sessionTranslation', () => {
const expected = { const expected = {
currentSession: { currentSession: {
sessionVersion: 1, sessionVersion: 3,
localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444', localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444',
remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA', remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA',
rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=', rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=',
previousCounter: 2, previousCounter: 3,
senderChain: { senderChain: {
senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x', senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x',
senderRatchetKeyPrivate: senderRatchetKeyPrivate:
'5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=', '5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=',
chainKey: { chainKey: {
index: -1, index: 0,
key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=', key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=',
}, },
}, },
@ -594,18 +594,18 @@ describe('sessionTranslation', () => {
{ {
senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz',
chainKey: { chainKey: {
index: 5, index: 6,
key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=',
}, },
messageKeys: [ messageKeys: [
{ {
index: 0, index: 1,
cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=', cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=',
iv: 'TcRanSxZVWbuIq0xDRGnEw==', iv: 'TcRanSxZVWbuIq0xDRGnEw==',
macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=', macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=',
}, },
{ {
index: 4, index: 5,
cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=', cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=',
iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==', iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==',
macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=', macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=',
@ -766,17 +766,17 @@ describe('sessionTranslation', () => {
const expected = { const expected = {
currentSession: { currentSession: {
sessionVersion: 1, sessionVersion: 3,
localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444', localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444',
remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA', remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA',
rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=', rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=',
previousCounter: 2, previousCounter: 3,
senderChain: { senderChain: {
senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x', senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x',
senderRatchetKeyPrivate: senderRatchetKeyPrivate:
'5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=', '5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=',
chainKey: { chainKey: {
index: -1, index: 0,
key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=', key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=',
}, },
}, },
@ -784,18 +784,18 @@ describe('sessionTranslation', () => {
{ {
senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz',
chainKey: { chainKey: {
index: 5, index: 6,
key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=',
}, },
messageKeys: [ messageKeys: [
{ {
index: 0, index: 1,
cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=', cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=',
iv: 'TcRanSxZVWbuIq0xDRGnEw==', iv: 'TcRanSxZVWbuIq0xDRGnEw==',
macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=', macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=',
}, },
{ {
index: 4, index: 5,
cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=', cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=',
iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==', iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==',
macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=', macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=',
@ -809,17 +809,17 @@ describe('sessionTranslation', () => {
}, },
previousSessions: [ previousSessions: [
{ {
sessionVersion: 1, sessionVersion: 3,
localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444', localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444',
remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA', remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA',
rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=', rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=',
previousCounter: 2, previousCounter: 3,
senderChain: { senderChain: {
senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x', senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x',
senderRatchetKeyPrivate: senderRatchetKeyPrivate:
'5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=', '5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=',
chainKey: { chainKey: {
index: -1, index: 0,
key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=', key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=',
}, },
}, },
@ -827,18 +827,18 @@ describe('sessionTranslation', () => {
{ {
senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz',
chainKey: { chainKey: {
index: 5, index: 6,
key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=',
}, },
messageKeys: [ messageKeys: [
{ {
index: 1, index: 2,
cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=', cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=',
iv: 'TcRanSxZVWbuIq0xDRGnEw==', iv: 'TcRanSxZVWbuIq0xDRGnEw==',
macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=', macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=',
}, },
{ {
index: 5, index: 6,
cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=', cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=',
iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==', iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==',
macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=', macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=',
@ -851,17 +851,17 @@ describe('sessionTranslation', () => {
aliceBaseKey: 'BUFOv0MAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N', aliceBaseKey: 'BUFOv0MAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N',
}, },
{ {
sessionVersion: 1, sessionVersion: 3,
localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444', localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444',
remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA', remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA',
rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=', rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=',
previousCounter: 2, previousCounter: 3,
senderChain: { senderChain: {
senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x', senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x',
senderRatchetKeyPrivate: senderRatchetKeyPrivate:
'5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=', '5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=',
chainKey: { chainKey: {
index: -1, index: 0,
key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=', key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=',
}, },
}, },
@ -869,18 +869,18 @@ describe('sessionTranslation', () => {
{ {
senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz',
chainKey: { chainKey: {
index: 5, index: 6,
key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=',
}, },
messageKeys: [ messageKeys: [
{ {
index: 2, index: 3,
cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=', cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=',
iv: 'TcRanSxZVWbuIq0xDRGnEw==', iv: 'TcRanSxZVWbuIq0xDRGnEw==',
macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=', macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=',
}, },
{ {
index: 3, index: 4,
cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=', cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=',
iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==', iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==',
macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=', macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=',
@ -956,20 +956,20 @@ describe('sessionTranslation', () => {
preKeyId: 386, preKeyId: 386,
signedPreKeyId: 2995, signedPreKeyId: 2995,
}, },
previousCounter: 0, previousCounter: 1,
remoteIdentityPublic: 'BRmB2uSNpwbXZJjisIh1p/VgRctUZSVIoiEm2ThjiHoq', remoteIdentityPublic: 'BRmB2uSNpwbXZJjisIh1p/VgRctUZSVIoiEm2ThjiHoq',
remoteRegistrationId: 3188, remoteRegistrationId: 3188,
rootKey: 'GzGfNozK5vDKqL4+fdqpiMRIuHNOndM6iMhGubNR1mk=', rootKey: 'GzGfNozK5vDKqL4+fdqpiMRIuHNOndM6iMhGubNR1mk=',
senderChain: { senderChain: {
chainKey: { chainKey: {
index: 0, index: 1,
key: 'tl5Eby9q7n8PVeiriKoRjHhu9Y0RxvJ90PMq5MfKwgA=', key: 'tl5Eby9q7n8PVeiriKoRjHhu9Y0RxvJ90PMq5MfKwgA=',
}, },
senderRatchetKey: 'BRSm55wC8hrG5Rp7l9gxtOhugp5ulcco20upOFCPyyJo', senderRatchetKey: 'BRSm55wC8hrG5Rp7l9gxtOhugp5ulcco20upOFCPyyJo',
senderRatchetKeyPrivate: senderRatchetKeyPrivate:
'IC0mCV0kFVAf+Q4cHid5hR7vy+5F0SvpYYaqsSA6d00=', 'IC0mCV0kFVAf+Q4cHid5hR7vy+5F0SvpYYaqsSA6d00=',
}, },
sessionVersion: 1, sessionVersion: 3,
}, },
}; };

View file

@ -0,0 +1,181 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import {
arrayBufferToHex,
constantTimeEqual,
getRandomBytes,
hexToArrayBuffer,
typedArrayToArrayBuffer,
} from '../Crypto';
import {
calculateSignature,
clampPrivateKey,
createKeyPair,
copyArrayBuffer,
generateKeyPair,
generatePreKey,
generateSignedPreKey,
isNonNegativeInteger,
verifySignature,
} from '../Curve';
describe('Curve', () => {
it('verifySignature roundtrip', () => {
const message = typedArrayToArrayBuffer(Buffer.from('message'));
const { pubKey, privKey } = generateKeyPair();
const signature = calculateSignature(privKey, message);
const verified = verifySignature(pubKey, message, signature);
assert.isTrue(verified);
});
describe('#isNonNegativeInteger', () => {
it('returns false for -1, Infinity, NaN, a string, etc.', () => {
assert.isFalse(isNonNegativeInteger(-1));
assert.isFalse(isNonNegativeInteger(NaN));
assert.isFalse(isNonNegativeInteger(Infinity));
assert.isFalse(isNonNegativeInteger('woo!'));
});
it('returns true for 0 and positive integgers', () => {
assert.isTrue(isNonNegativeInteger(0));
assert.isTrue(isNonNegativeInteger(1));
assert.isTrue(isNonNegativeInteger(3));
assert.isTrue(isNonNegativeInteger(400_000));
});
});
describe('#generateSignedPrekey', () => {
it('geernates proper signature for created signed prekeys', () => {
const keyId = 4;
const identityKeyPair = generateKeyPair();
const signedPreKey = generateSignedPreKey(identityKeyPair, keyId);
assert.equal(keyId, signedPreKey.keyId);
const verified = verifySignature(
identityKeyPair.pubKey,
signedPreKey.keyPair.pubKey,
signedPreKey.signature
);
assert.isTrue(verified);
});
});
describe('#generatePrekey', () => {
it('returns keys of the right length', () => {
const keyId = 7;
const preKey = generatePreKey(keyId);
assert.equal(keyId, preKey.keyId);
assert.equal(33, preKey.keyPair.pubKey.byteLength);
assert.equal(32, preKey.keyPair.privKey.byteLength);
});
});
describe('#copyArrayBuffer', () => {
it('copy matches original', () => {
const data = getRandomBytes(200);
const dataHex = arrayBufferToHex(data);
const copy = copyArrayBuffer(data);
assert.equal(data.byteLength, copy.byteLength);
assert.isTrue(constantTimeEqual(data, copy));
assert.equal(dataHex, arrayBufferToHex(data));
assert.equal(dataHex, arrayBufferToHex(copy));
});
it('copies into new memory location', () => {
const data = getRandomBytes(200);
const dataHex = arrayBufferToHex(data);
const copy = copyArrayBuffer(data);
const view = new Uint8Array(copy);
view[0] += 1;
view[1] -= 1;
assert.equal(data.byteLength, copy.byteLength);
assert.isFalse(constantTimeEqual(data, copy));
assert.equal(dataHex, arrayBufferToHex(data));
assert.notEqual(dataHex, arrayBufferToHex(copy));
});
});
describe('#createKeyPair', () => {
it('does not modify unclamped private key', () => {
const initialHex =
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const privateKey = hexToArrayBuffer(initialHex);
const copyOfPrivateKey = copyArrayBuffer(privateKey);
assert.isTrue(
constantTimeEqual(privateKey, copyOfPrivateKey),
'initial copy check'
);
const keyPair = createKeyPair(privateKey);
assert.equal(32, keyPair.privKey.byteLength);
assert.equal(33, keyPair.pubKey.byteLength);
// The original incoming key is not modified
assert.isTrue(
constantTimeEqual(privateKey, copyOfPrivateKey),
'second copy check'
);
// But the keypair that comes out has indeed been updated
assert.notEqual(
initialHex,
arrayBufferToHex(keyPair.privKey),
'keypair check'
);
assert.isFalse(
constantTimeEqual(keyPair.privKey, privateKey),
'keypair vs incoming value'
);
});
it('does not modify clamped private key', () => {
const initialHex =
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const privateKey = hexToArrayBuffer(initialHex);
clampPrivateKey(privateKey);
const postClampHex = arrayBufferToHex(privateKey);
const copyOfPrivateKey = copyArrayBuffer(privateKey);
assert.notEqual(postClampHex, initialHex, 'initial clamp check');
assert.isTrue(
constantTimeEqual(privateKey, copyOfPrivateKey),
'initial copy check'
);
const keyPair = createKeyPair(privateKey);
assert.equal(32, keyPair.privKey.byteLength);
assert.equal(33, keyPair.pubKey.byteLength);
// The original incoming key is not modified
assert.isTrue(
constantTimeEqual(privateKey, copyOfPrivateKey),
'second copy check'
);
// The keypair that comes out hasn't been updated either
assert.equal(
postClampHex,
arrayBufferToHex(keyPair.privKey),
'keypair check'
);
assert.isTrue(
constantTimeEqual(privateKey, keyPair.privKey),
'keypair vs incoming value'
);
});
});
});

View file

@ -1,34 +1,92 @@
// Copyright 2015-2020 Signal Messenger, LLC // Copyright 2015-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global _, textsecure, libsignal, storage, ConversationController */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use strict'; import { assert } from 'chai';
import { Direction, SessionRecord } from 'libsignal-client';
import { signal } from '../protobuf/compiled';
import { sessionStructureToArrayBuffer } from '../util/sessionTranslation';
import { getRandomBytes, constantTimeEqual } from '../Crypto';
import { clampPrivateKey, setPublicKeyTypeByte } from '../Curve';
import { SignalProtocolStore } from '../SignalProtocolStore';
import { IdentityKeyType, KeyPairType } from '../textsecure/Types.d';
const { RecordStructure, SessionStructure } = signal.proto.storage;
describe('SignalProtocolStore', () => { describe('SignalProtocolStore', () => {
const number = '+5558675309'; const number = '+5558675309';
let store; let store: SignalProtocolStore;
let identityKey; let identityKey: KeyPairType;
let testKey; let testKey: KeyPairType;
function getSessionRecord(isOpen?: boolean): SessionRecord {
const proto = new RecordStructure();
proto.previousSessions = [];
if (isOpen) {
proto.currentSession = new SessionStructure();
proto.currentSession.aliceBaseKey = toUint8Array(getPublicKey());
proto.currentSession.localIdentityPublic = toUint8Array(getPublicKey());
proto.currentSession.localRegistrationId = 435;
proto.currentSession.previousCounter = 1;
proto.currentSession.remoteIdentityPublic = toUint8Array(getPublicKey());
proto.currentSession.remoteRegistrationId = 243;
proto.currentSession.rootKey = toUint8Array(getPrivateKey());
proto.currentSession.sessionVersion = 3;
}
return SessionRecord.deserialize(
Buffer.from(sessionStructureToArrayBuffer(proto))
);
}
function toUint8Array(buffer: ArrayBuffer): Uint8Array {
return new Uint8Array(buffer);
}
function getPrivateKey() {
const key = getRandomBytes(32);
clampPrivateKey(key);
return key;
}
function getPublicKey() {
const key = getRandomBytes(33);
setPublicKeyTypeByte(key);
return key;
}
before(async () => { before(async () => {
store = textsecure.storage.protocol; store = window.textsecure.storage.protocol;
store.hydrateCaches(); store.hydrateCaches();
identityKey = { identityKey = {
pubKey: libsignal.crypto.getRandomBytes(33), pubKey: getPublicKey(),
privKey: libsignal.crypto.getRandomBytes(32), privKey: getPrivateKey(),
}; };
testKey = { testKey = {
pubKey: libsignal.crypto.getRandomBytes(33), pubKey: getPublicKey(),
privKey: libsignal.crypto.getRandomBytes(32), privKey: getPrivateKey(),
}; };
storage.put('registrationId', 1337); setPublicKeyTypeByte(identityKey.pubKey);
storage.put('identityKey', identityKey); setPublicKeyTypeByte(testKey.pubKey);
await storage.fetch();
ConversationController.reset(); clampPrivateKey(identityKey.privKey);
await ConversationController.load(); clampPrivateKey(testKey.privKey);
await ConversationController.getOrCreateAndWait(number, 'private');
window.storage.put('registrationId', 1337);
window.storage.put('identityKey', identityKey);
await window.storage.fetch();
window.ConversationController.reset();
await window.ConversationController.load();
await window.ConversationController.getOrCreateAndWait(number, 'private');
}); });
describe('getLocalRegistrationId', () => { describe('getLocalRegistrationId', () => {
@ -42,23 +100,29 @@ describe('SignalProtocolStore', () => {
it('retrieves my identity key', async () => { it('retrieves my identity key', async () => {
await store.hydrateCaches(); await store.hydrateCaches();
const key = await store.getIdentityKeyPair(); const key = await store.getIdentityKeyPair();
assertEqualArrayBuffers(key.pubKey, identityKey.pubKey); if (!key) {
assertEqualArrayBuffers(key.privKey, identityKey.privKey); throw new Error('Missing key!');
}
assert.isTrue(constantTimeEqual(key.pubKey, identityKey.pubKey));
assert.isTrue(constantTimeEqual(key.privKey, identityKey.privKey));
}); });
}); });
describe('saveIdentity', () => { describe('saveIdentity', () => {
const address = new libsignal.SignalProtocolAddress(number, 1); const identifier = `${number}.1`;
const identifier = address.toString();
it('stores identity keys', async () => { it('stores identity keys', async () => {
await store.saveIdentity(identifier, testKey.pubKey); await store.saveIdentity(identifier, testKey.pubKey);
const key = await store.loadIdentityKey(number); const key = await store.loadIdentityKey(number);
if (!key) {
throw new Error('Missing key!');
}
assertEqualArrayBuffers(key, testKey.pubKey); assert.isTrue(constantTimeEqual(key, testKey.pubKey));
}); });
it('allows key changes', async () => { it('allows key changes', async () => {
const newIdentity = libsignal.crypto.getRandomBytes(33); const newIdentity = getPublicKey();
await store.saveIdentity(identifier, testKey.pubKey); await store.saveIdentity(identifier, testKey.pubKey);
await store.saveIdentity(identifier, newIdentity); await store.saveIdentity(identifier, newIdentity);
}); });
@ -70,19 +134,28 @@ describe('SignalProtocolStore', () => {
}); });
it('marks the key firstUse', async () => { it('marks the key firstUse', async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert(identity.firstUse); assert(identity.firstUse);
}); });
it('sets the timestamp', async () => { it('sets the timestamp', async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert(identity.timestamp); assert(identity.timestamp);
}); });
it('sets the verified status to DEFAULT', async () => { it('sets the verified status to DEFAULT', async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT); assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT);
}); });
}); });
describe('When there is a different existing key (non first use)', () => { describe('When there is a different existing key (non first use)', () => {
const newIdentity = libsignal.crypto.getRandomBytes(33); const newIdentity = getPublicKey();
const oldTimestamp = Date.now(); const oldTimestamp = Date.now();
before(async () => { before(async () => {
@ -100,10 +173,16 @@ describe('SignalProtocolStore', () => {
}); });
it('marks the key not firstUse', async () => { it('marks the key not firstUse', async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert(!identity.firstUse); assert(!identity.firstUse);
}); });
it('updates the timestamp', async () => { it('updates the timestamp', async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.notEqual(identity.timestamp, oldTimestamp); assert.notEqual(identity.timestamp, oldTimestamp);
}); });
@ -123,6 +202,9 @@ describe('SignalProtocolStore', () => {
}); });
it('sets the new key to default', async () => { it('sets the new key to default', async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT); assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT);
}); });
}); });
@ -142,7 +224,9 @@ describe('SignalProtocolStore', () => {
}); });
it('sets the new key to unverified', async () => { it('sets the new key to unverified', async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual( assert.strictEqual(
identity.verified, identity.verified,
store.VerifiedStatus.UNVERIFIED store.VerifiedStatus.UNVERIFIED
@ -165,6 +249,9 @@ describe('SignalProtocolStore', () => {
}); });
it('sets the new key to unverified', async () => { it('sets the new key to unverified', async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual( assert.strictEqual(
identity.verified, identity.verified,
store.VerifiedStatus.UNVERIFIED store.VerifiedStatus.UNVERIFIED
@ -180,6 +267,7 @@ describe('SignalProtocolStore', () => {
publicKey: testKey.pubKey, publicKey: testKey.pubKey,
timestamp: oldTimestamp, timestamp: oldTimestamp,
nonblockingApproval: false, nonblockingApproval: false,
firstUse: false,
verified: store.VerifiedStatus.DEFAULT, verified: store.VerifiedStatus.DEFAULT,
}); });
await store.hydrateCaches(); await store.hydrateCaches();
@ -187,6 +275,9 @@ describe('SignalProtocolStore', () => {
describe('If it is marked firstUse', () => { describe('If it is marked firstUse', () => {
before(async () => { before(async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
identity.firstUse = true; identity.firstUse = true;
await window.Signal.Data.createOrUpdateIdentityKey(identity); await window.Signal.Data.createOrUpdateIdentityKey(identity);
await store.hydrateCaches(); await store.hydrateCaches();
@ -195,6 +286,9 @@ describe('SignalProtocolStore', () => {
await store.saveIdentity(identifier, testKey.pubKey, true); await store.saveIdentity(identifier, testKey.pubKey, true);
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert(!identity.nonblockingApproval); assert(!identity.nonblockingApproval);
assert.strictEqual(identity.timestamp, oldTimestamp); assert.strictEqual(identity.timestamp, oldTimestamp);
}); });
@ -202,17 +296,23 @@ describe('SignalProtocolStore', () => {
describe('If it is not marked firstUse', () => { describe('If it is not marked firstUse', () => {
before(async () => { before(async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
identity.firstUse = false; identity.firstUse = false;
await window.Signal.Data.createOrUpdateIdentityKey(identity); await window.Signal.Data.createOrUpdateIdentityKey(identity);
await store.hydrateCaches(); await store.hydrateCaches();
}); });
describe('If nonblocking approval is required', () => { describe('If nonblocking approval is required', () => {
let now; let now: number;
before(async () => { before(async () => {
now = Date.now(); now = Date.now();
const identity = await window.Signal.Data.getIdentityKeyById( const identity = await window.Signal.Data.getIdentityKeyById(
number number
); );
if (!identity) {
throw new Error('Missing identity!');
}
identity.timestamp = now; identity.timestamp = now;
await window.Signal.Data.createOrUpdateIdentityKey(identity); await window.Signal.Data.createOrUpdateIdentityKey(identity);
await store.hydrateCaches(); await store.hydrateCaches();
@ -223,6 +323,9 @@ describe('SignalProtocolStore', () => {
const identity = await window.Signal.Data.getIdentityKeyById( const identity = await window.Signal.Data.getIdentityKeyById(
number number
); );
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual(identity.nonblockingApproval, true); assert.strictEqual(identity.nonblockingApproval, true);
assert.strictEqual(identity.timestamp, now); assert.strictEqual(identity.timestamp, now);
@ -233,12 +336,13 @@ describe('SignalProtocolStore', () => {
}); });
}); });
describe('saveIdentityWithAttributes', () => { describe('saveIdentityWithAttributes', () => {
let now; let now: number;
let validAttributes; let validAttributes: IdentityKeyType;
before(async () => { before(async () => {
now = Date.now(); now = Date.now();
validAttributes = { validAttributes = {
id: number,
publicKey: testKey.pubKey, publicKey: testKey.pubKey,
firstUse: true, firstUse: true,
timestamp: now, timestamp: now,
@ -255,29 +359,44 @@ describe('SignalProtocolStore', () => {
it('publicKey is saved', async () => { it('publicKey is saved', async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); if (!identity) {
throw new Error('Missing identity!');
}
assert.isTrue(constantTimeEqual(identity.publicKey, testKey.pubKey));
}); });
it('firstUse is saved', async () => { it('firstUse is saved', async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual(identity.firstUse, true); assert.strictEqual(identity.firstUse, true);
}); });
it('timestamp is saved', async () => { it('timestamp is saved', async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual(identity.timestamp, now); assert.strictEqual(identity.timestamp, now);
}); });
it('verified is saved', async () => { it('verified is saved', async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED); assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED);
}); });
it('nonblockingApproval is saved', async () => { it('nonblockingApproval is saved', async () => {
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual(identity.nonblockingApproval, false); assert.strictEqual(identity.nonblockingApproval, false);
}); });
}); });
describe('with invalid attributes', () => { describe('with invalid attributes', () => {
let attributes; let attributes: IdentityKeyType;
beforeEach(() => { beforeEach(() => {
attributes = _.clone(validAttributes); attributes = window._.clone(validAttributes);
}); });
async function testInvalidAttributes() { async function testInvalidAttributes() {
@ -290,23 +409,23 @@ describe('SignalProtocolStore', () => {
} }
it('rejects an invalid publicKey', async () => { it('rejects an invalid publicKey', async () => {
attributes.publicKey = 'a string'; attributes.publicKey = 'a string' as any;
await testInvalidAttributes(); await testInvalidAttributes();
}); });
it('rejects invalid firstUse', async () => { it('rejects invalid firstUse', async () => {
attributes.firstUse = 0; attributes.firstUse = 0 as any;
await testInvalidAttributes(); await testInvalidAttributes();
}); });
it('rejects invalid timestamp', async () => { it('rejects invalid timestamp', async () => {
attributes.timestamp = NaN; attributes.timestamp = NaN as any;
await testInvalidAttributes(); await testInvalidAttributes();
}); });
it('rejects invalid verified', async () => { it('rejects invalid verified', async () => {
attributes.verified = null; attributes.verified = null as any;
await testInvalidAttributes(); await testInvalidAttributes();
}); });
it('rejects invalid nonblockingApproval', async () => { it('rejects invalid nonblockingApproval', async () => {
attributes.nonblockingApproval = 0; attributes.nonblockingApproval = 0 as any;
await testInvalidAttributes(); await testInvalidAttributes();
}); });
}); });
@ -315,6 +434,9 @@ describe('SignalProtocolStore', () => {
it('sets nonblockingApproval', async () => { it('sets nonblockingApproval', async () => {
await store.setApproval(number, true); await store.setApproval(number, true);
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual(identity.nonblockingApproval, true); assert.strictEqual(identity.nonblockingApproval, true);
}); });
@ -337,8 +459,12 @@ describe('SignalProtocolStore', () => {
await store.setVerified(number, store.VerifiedStatus.VERIFIED); await store.setVerified(number, store.VerifiedStatus.VERIFIED);
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED); assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED);
assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); assert.isTrue(constantTimeEqual(identity.publicKey, testKey.pubKey));
}); });
}); });
describe('with the current public key', () => { describe('with the current public key', () => {
@ -351,12 +477,16 @@ describe('SignalProtocolStore', () => {
); );
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED); assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED);
assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); assert.isTrue(constantTimeEqual(identity.publicKey, testKey.pubKey));
}); });
}); });
describe('with a mismatching public key', () => { describe('with a mismatching public key', () => {
const newIdentity = libsignal.crypto.getRandomBytes(33); const newIdentity = getPublicKey();
before(saveRecordDefault); before(saveRecordDefault);
it('does not change the record.', async () => { it('does not change the record.', async () => {
await store.setVerified( await store.setVerified(
@ -366,14 +496,18 @@ describe('SignalProtocolStore', () => {
); );
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT); assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT);
assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); assert.isTrue(constantTimeEqual(identity.publicKey, testKey.pubKey));
}); });
}); });
}); });
describe('processContactSyncVerificationState', () => { describe('processContactSyncVerificationState', () => {
const newIdentity = libsignal.crypto.getRandomBytes(33); const newIdentity = getPublicKey();
let keychangeTriggered; let keychangeTriggered: number;
beforeEach(() => { beforeEach(() => {
keychangeTriggered = 0; keychangeTriggered = 0;
@ -436,12 +570,17 @@ describe('SignalProtocolStore', () => {
const identity = await window.Signal.Data.getIdentityKeyById( const identity = await window.Signal.Data.getIdentityKeyById(
number number
); );
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual( assert.strictEqual(
identity.verified, identity.verified,
store.VerifiedStatus.VERIFIED store.VerifiedStatus.VERIFIED
); );
assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); assert.isTrue(
constantTimeEqual(identity.publicKey, testKey.pubKey)
);
assert.strictEqual(keychangeTriggered, 0); assert.strictEqual(keychangeTriggered, 0);
}); });
}); });
@ -468,9 +607,14 @@ describe('SignalProtocolStore', () => {
const identity = await window.Signal.Data.getIdentityKeyById( const identity = await window.Signal.Data.getIdentityKeyById(
number number
); );
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT); assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT);
assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); assert.isTrue(
constantTimeEqual(identity.publicKey, testKey.pubKey)
);
assert.strictEqual(keychangeTriggered, 0); assert.strictEqual(keychangeTriggered, 0);
}); });
}); });
@ -514,12 +658,15 @@ describe('SignalProtocolStore', () => {
); );
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual( assert.strictEqual(
identity.verified, identity.verified,
store.VerifiedStatus.UNVERIFIED store.VerifiedStatus.UNVERIFIED
); );
assertEqualArrayBuffers(identity.publicKey, newIdentity); assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity));
assert.strictEqual(keychangeTriggered, 0); assert.strictEqual(keychangeTriggered, 0);
}); });
}); });
@ -547,12 +694,15 @@ describe('SignalProtocolStore', () => {
const identity = await window.Signal.Data.getIdentityKeyById( const identity = await window.Signal.Data.getIdentityKeyById(
number number
); );
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual( assert.strictEqual(
identity.verified, identity.verified,
store.VerifiedStatus.UNVERIFIED store.VerifiedStatus.UNVERIFIED
); );
assertEqualArrayBuffers(identity.publicKey, newIdentity); assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity));
assert.strictEqual(keychangeTriggered, 1); assert.strictEqual(keychangeTriggered, 1);
}); });
}); });
@ -578,12 +728,17 @@ describe('SignalProtocolStore', () => {
const identity = await window.Signal.Data.getIdentityKeyById( const identity = await window.Signal.Data.getIdentityKeyById(
number number
); );
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual( assert.strictEqual(
identity.verified, identity.verified,
store.VerifiedStatus.UNVERIFIED store.VerifiedStatus.UNVERIFIED
); );
assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); assert.isTrue(
constantTimeEqual(identity.publicKey, testKey.pubKey)
);
assert.strictEqual(keychangeTriggered, 0); assert.strictEqual(keychangeTriggered, 0);
}); });
}); });
@ -626,9 +781,12 @@ describe('SignalProtocolStore', () => {
newIdentity newIdentity
); );
const identity = await window.Signal.Data.getIdentityKeyById(number); const identity = await window.Signal.Data.getIdentityKeyById(number);
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED); assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED);
assertEqualArrayBuffers(identity.publicKey, newIdentity); assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity));
assert.strictEqual(keychangeTriggered, 0); assert.strictEqual(keychangeTriggered, 0);
}); });
}); });
@ -656,12 +814,15 @@ describe('SignalProtocolStore', () => {
const identity = await window.Signal.Data.getIdentityKeyById( const identity = await window.Signal.Data.getIdentityKeyById(
number number
); );
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual( assert.strictEqual(
identity.verified, identity.verified,
store.VerifiedStatus.VERIFIED store.VerifiedStatus.VERIFIED
); );
assertEqualArrayBuffers(identity.publicKey, newIdentity); assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity));
assert.strictEqual(keychangeTriggered, 1); assert.strictEqual(keychangeTriggered, 1);
}); });
}); });
@ -687,12 +848,17 @@ describe('SignalProtocolStore', () => {
const identity = await window.Signal.Data.getIdentityKeyById( const identity = await window.Signal.Data.getIdentityKeyById(
number number
); );
if (!identity) {
throw new Error('Missing identity!');
}
assert.strictEqual( assert.strictEqual(
identity.verified, identity.verified,
store.VerifiedStatus.VERIFIED store.VerifiedStatus.VERIFIED
); );
assertEqualArrayBuffers(identity.publicKey, testKey.pubKey); assert.isTrue(
constantTimeEqual(identity.publicKey, testKey.pubKey)
);
assert.strictEqual(keychangeTriggered, 0); assert.strictEqual(keychangeTriggered, 0);
}); });
}); });
@ -795,13 +961,12 @@ describe('SignalProtocolStore', () => {
}); });
}); });
describe('isTrustedIdentity', () => { describe('isTrustedIdentity', () => {
const address = new libsignal.SignalProtocolAddress(number, 1); const identifier = `${number}.1`;
const identifier = address.toString();
describe('When invalid direction is given', () => { describe('When invalid direction is given', () => {
it('should fail', async () => { it('should fail', async () => {
try { try {
await store.isTrustedIdentity(number, testKey.pubKey); await store.isTrustedIdentity(number, testKey.pubKey, 'dir' as any);
throw new Error('isTrustedIdentity should have failed'); throw new Error('isTrustedIdentity should have failed');
} catch (error) { } catch (error) {
// good // good
@ -810,13 +975,13 @@ describe('SignalProtocolStore', () => {
}); });
describe('When direction is RECEIVING', () => { describe('When direction is RECEIVING', () => {
it('always returns true', async () => { it('always returns true', async () => {
const newIdentity = libsignal.crypto.getRandomBytes(33); const newIdentity = getPublicKey();
await store.saveIdentity(identifier, testKey.pubKey); await store.saveIdentity(identifier, testKey.pubKey);
const trusted = await store.isTrustedIdentity( const trusted = await store.isTrustedIdentity(
identifier, identifier,
newIdentity, newIdentity,
store.Direction.RECEIVING Direction.Receiving
); );
if (!trusted) { if (!trusted) {
@ -830,11 +995,11 @@ describe('SignalProtocolStore', () => {
await store.removeIdentityKey(number); await store.removeIdentityKey(number);
}); });
it('returns true', async () => { it('returns true', async () => {
const newIdentity = libsignal.crypto.getRandomBytes(33); const newIdentity = getPublicKey();
const trusted = await store.isTrustedIdentity( const trusted = await store.isTrustedIdentity(
identifier, identifier,
newIdentity, newIdentity,
store.Direction.SENDING Direction.Sending
); );
if (!trusted) { if (!trusted) {
throw new Error('isTrusted returned false on first use'); throw new Error('isTrusted returned false on first use');
@ -847,11 +1012,11 @@ describe('SignalProtocolStore', () => {
}); });
describe('When the existing key is different', () => { describe('When the existing key is different', () => {
it('returns false', async () => { it('returns false', async () => {
const newIdentity = libsignal.crypto.getRandomBytes(33); const newIdentity = getPublicKey();
const trusted = await store.isTrustedIdentity( const trusted = await store.isTrustedIdentity(
identifier, identifier,
newIdentity, newIdentity,
store.Direction.SENDING Direction.Sending
); );
if (trusted) { if (trusted) {
throw new Error('isTrusted returned true on untrusted key'); throw new Error('isTrusted returned true on untrusted key');
@ -859,7 +1024,7 @@ describe('SignalProtocolStore', () => {
}); });
}); });
describe('When the existing key matches the new key', () => { describe('When the existing key matches the new key', () => {
const newIdentity = libsignal.crypto.getRandomBytes(33); const newIdentity = getPublicKey();
before(async () => { before(async () => {
await store.saveIdentity(identifier, newIdentity); await store.saveIdentity(identifier, newIdentity);
}); });
@ -867,7 +1032,7 @@ describe('SignalProtocolStore', () => {
const trusted = await store.isTrustedIdentity( const trusted = await store.isTrustedIdentity(
identifier, identifier,
newIdentity, newIdentity,
store.Direction.SENDING Direction.Sending
); );
if (trusted) { if (trusted) {
@ -880,7 +1045,7 @@ describe('SignalProtocolStore', () => {
const trusted = await store.isTrustedIdentity( const trusted = await store.isTrustedIdentity(
identifier, identifier,
newIdentity, newIdentity,
store.Direction.SENDING Direction.Sending
); );
if (!trusted) { if (!trusted) {
throw new Error('isTrusted returned false on an approved key'); throw new Error('isTrusted returned false on an approved key');
@ -894,8 +1059,21 @@ describe('SignalProtocolStore', () => {
it('stores prekeys', async () => { it('stores prekeys', async () => {
await store.storePreKey(1, testKey); await store.storePreKey(1, testKey);
const key = await store.loadPreKey(1); const key = await store.loadPreKey(1);
assertEqualArrayBuffers(key.pubKey, testKey.pubKey); if (!key) {
assertEqualArrayBuffers(key.privKey, testKey.privKey); throw new Error('Missing key!');
}
const keyPair = {
pubKey: window.Signal.Crypto.typedArrayToArrayBuffer(
key.publicKey().serialize()
),
privKey: window.Signal.Crypto.typedArrayToArrayBuffer(
key.privateKey().serialize()
),
};
assert.isTrue(constantTimeEqual(keyPair.pubKey, testKey.pubKey));
assert.isTrue(constantTimeEqual(keyPair.privKey, testKey.privKey));
}); });
}); });
describe('removePreKey', () => { describe('removePreKey', () => {
@ -903,7 +1081,7 @@ describe('SignalProtocolStore', () => {
await store.storePreKey(2, testKey); await store.storePreKey(2, testKey);
}); });
it('deletes prekeys', async () => { it('deletes prekeys', async () => {
await store.removePreKey(2, testKey); await store.removePreKey(2);
const key = await store.loadPreKey(2); const key = await store.loadPreKey(2);
assert.isUndefined(key); assert.isUndefined(key);
@ -912,10 +1090,22 @@ describe('SignalProtocolStore', () => {
describe('storeSignedPreKey', () => { describe('storeSignedPreKey', () => {
it('stores signed prekeys', async () => { it('stores signed prekeys', async () => {
await store.storeSignedPreKey(3, testKey); await store.storeSignedPreKey(3, testKey);
const key = await store.loadSignedPreKey(3); const key = await store.loadSignedPreKey(3);
assertEqualArrayBuffers(key.pubKey, testKey.pubKey); if (!key) {
assertEqualArrayBuffers(key.privKey, testKey.privKey); throw new Error('Missing key!');
}
const keyPair = {
pubKey: window.Signal.Crypto.typedArrayToArrayBuffer(
key.publicKey().serialize()
),
privKey: window.Signal.Crypto.typedArrayToArrayBuffer(
key.privateKey().serialize()
),
};
assert.isTrue(constantTimeEqual(keyPair.pubKey, testKey.pubKey));
assert.isTrue(constantTimeEqual(keyPair.privKey, testKey.privKey));
}); });
}); });
describe('removeSignedPreKey', () => { describe('removeSignedPreKey', () => {
@ -923,7 +1113,7 @@ describe('SignalProtocolStore', () => {
await store.storeSignedPreKey(4, testKey); await store.storeSignedPreKey(4, testKey);
}); });
it('deletes signed prekeys', async () => { it('deletes signed prekeys', async () => {
await store.removeSignedPreKey(4, testKey); await store.removeSignedPreKey(4);
const key = await store.loadSignedPreKey(4); const key = await store.loadSignedPreKey(4);
assert.isUndefined(key); assert.isUndefined(key);
@ -931,24 +1121,25 @@ describe('SignalProtocolStore', () => {
}); });
describe('storeSession', () => { describe('storeSession', () => {
it('stores sessions', async () => { it('stores sessions', async () => {
const testRecord = 'an opaque string'; const testRecord = getSessionRecord();
await store.storeSession(`${number}.1`, testRecord); await store.storeSession(`${number}.1`, testRecord);
const record = await store.loadSession(`${number}.1`); const record = await store.loadSession(`${number}.1`);
if (!record) {
throw new Error('Missing record!');
}
assert.deepEqual(record, testRecord); assert.equal(record, testRecord);
}); });
}); });
describe('removeAllSessions', () => { describe('removeAllSessions', () => {
it('removes all sessions for a number', async () => { it('removes all sessions for a number', async () => {
const testRecord = 'an opaque string';
const devices = [1, 2, 3].map(deviceId => { const devices = [1, 2, 3].map(deviceId => {
return [number, deviceId].join('.'); return [number, deviceId].join('.');
}); });
await Promise.all( await Promise.all(
devices.map(async encodedNumber => { devices.map(async encodedNumber => {
await store.storeSession(encodedNumber, testRecord + encodedNumber); await store.storeSession(encodedNumber, getSessionRecord());
}) })
); );
@ -965,7 +1156,7 @@ describe('SignalProtocolStore', () => {
}); });
describe('clearSessionStore', () => { describe('clearSessionStore', () => {
it('clears the session store', async () => { it('clears the session store', async () => {
const testRecord = 'an opaque string'; const testRecord = getSessionRecord();
await store.storeSession(`${number}.1`, testRecord); await store.storeSession(`${number}.1`, testRecord);
await store.clearSessionStore(); await store.clearSessionStore();
@ -975,17 +1166,7 @@ describe('SignalProtocolStore', () => {
}); });
describe('getDeviceIds', () => { describe('getDeviceIds', () => {
it('returns deviceIds for a number', async () => { it('returns deviceIds for a number', async () => {
const openRecord = JSON.stringify({ const openRecord = getSessionRecord(true);
version: 'v1',
sessions: {
ephemeralKey: {
registrationId: 25,
indexInfo: {
closed: -1,
},
},
},
});
const openDevices = [1, 2, 3, 10].map(deviceId => { const openDevices = [1, 2, 3, 10].map(deviceId => {
return [number, deviceId].join('.'); return [number, deviceId].join('.');
}); });
@ -995,22 +1176,13 @@ describe('SignalProtocolStore', () => {
}) })
); );
const closedRecord = JSON.stringify({ const closedRecord = getSessionRecord(false);
version: 'v1',
sessions: {
ephemeralKey: {
registrationId: 24,
indexInfo: {
closed: Date.now(),
},
},
},
});
await store.storeSession([number, 11].join('.'), closedRecord); await store.storeSession([number, 11].join('.'), closedRecord);
const deviceIds = await store.getDeviceIds(number); const deviceIds = await store.getDeviceIds(number);
assert.sameMembers(deviceIds, [1, 2, 3, 10]); assert.sameMembers(deviceIds, [1, 2, 3, 10]);
}); });
it('returns empty array for a number with no device ids', async () => { it('returns empty array for a number with no device ids', async () => {
const deviceIds = await store.getDeviceIds('foo'); const deviceIds = await store.getDeviceIds('foo');
assert.sameMembers(deviceIds, []); assert.sameMembers(deviceIds, []);
@ -1026,9 +1198,27 @@ describe('SignalProtocolStore', () => {
it('adds three and gets them back', async () => { it('adds three and gets them back', async () => {
await Promise.all([ await Promise.all([
store.addUnprocessed({ id: 2, envelope: 'second', timestamp: 2 }), store.addUnprocessed({
store.addUnprocessed({ id: 3, envelope: 'third', timestamp: 3 }), id: '2-two',
store.addUnprocessed({ id: 1, envelope: 'first', timestamp: 1 }), envelope: 'second',
timestamp: 2,
version: 2,
attempts: 0,
}),
store.addUnprocessed({
id: '3-three',
envelope: 'third',
timestamp: 3,
version: 2,
attempts: 0,
}),
store.addUnprocessed({
id: '1-one',
envelope: 'first',
timestamp: 1,
version: 2,
attempts: 0,
}),
]); ]);
const items = await store.getAllUnprocessed(); const items = await store.getAllUnprocessed();
@ -1041,8 +1231,14 @@ describe('SignalProtocolStore', () => {
}); });
it('saveUnprocessed successfully updates item', async () => { it('saveUnprocessed successfully updates item', async () => {
const id = 1; const id = '1-one';
await store.addUnprocessed({ id, envelope: 'first', timestamp: 1 }); await store.addUnprocessed({
id,
envelope: 'first',
timestamp: 1,
version: 2,
attempts: 0,
});
await store.updateUnprocessedWithData(id, { decrypted: 'updated' }); await store.updateUnprocessedWithData(id, { decrypted: 'updated' });
const items = await store.getAllUnprocessed(); const items = await store.getAllUnprocessed();
@ -1052,8 +1248,14 @@ describe('SignalProtocolStore', () => {
}); });
it('removeUnprocessed successfully deletes item', async () => { it('removeUnprocessed successfully deletes item', async () => {
const id = 1; const id = '1-one';
await store.addUnprocessed({ id, envelope: 'first', timestamp: 1 }); await store.addUnprocessed({
id,
envelope: 'first',
timestamp: 1,
version: 2,
attempts: 0,
});
await store.removeUnprocessed(id); await store.removeUnprocessed(id);
const items = await store.getAllUnprocessed(); const items = await store.getAllUnprocessed();

View file

@ -1,454 +0,0 @@
// Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable @typescript-eslint/no-explicit-any */
import { assert } from 'chai';
import {
ExplodedSenderCertificateType,
SecretSessionCipher,
createCertificateValidator,
_createSenderCertificateFromBuffer,
_createServerCertificateFromBuffer,
} from '../../metadata/SecretSessionCipher';
import {
bytesFromString,
stringFromBytes,
arrayBufferToBase64,
} from '../../Crypto';
import { KeyPairType } from '../../libsignal.d';
function toString(thing: string | ArrayBuffer): string {
if (typeof thing === 'string') {
return thing;
}
return arrayBufferToBase64(thing);
}
class InMemorySignalProtocolStore {
store: Record<string, any> = {};
Direction = {
SENDING: 1,
RECEIVING: 2,
};
getIdentityKeyPair(): Promise<{ privKey: ArrayBuffer; pubKey: ArrayBuffer }> {
return Promise.resolve(this.get('identityKey'));
}
getLocalRegistrationId(): Promise<string> {
return Promise.resolve(this.get('registrationId'));
}
put(key: string, value: any): void {
if (
key === undefined ||
value === undefined ||
key === null ||
value === null
) {
throw new Error('Tried to store undefined/null');
}
this.store[key] = value;
}
get(key: string, defaultValue?: any): any {
if (key === null || key === undefined) {
throw new Error('Tried to get value for undefined/null key');
}
if (key in this.store) {
return this.store[key];
}
return defaultValue;
}
remove(key: string): void {
if (key === null || key === undefined) {
throw new Error('Tried to remove value for undefined/null key');
}
delete this.store[key];
}
isTrustedIdentity(
identifier: string,
identityKey: ArrayBuffer
): Promise<boolean> {
if (identifier === null || identifier === undefined) {
throw new Error('tried to check identity key for undefined/null key');
}
if (!(identityKey instanceof ArrayBuffer)) {
throw new Error('Expected identityKey to be an ArrayBuffer');
}
const trusted = this.get(`identityKey${identifier}`);
if (trusted === undefined) {
return Promise.resolve(true);
}
return Promise.resolve(toString(identityKey) === toString(trusted));
}
loadIdentityKey(identifier: string): any {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to get identity key for undefined/null key');
}
return Promise.resolve(this.get(`identityKey${identifier}`));
}
saveIdentity(identifier: string, identityKey: ArrayBuffer): any {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to put identity key for undefined/null key');
}
const address = window.libsignal.SignalProtocolAddress.fromString(
identifier
);
const existing = this.get(`identityKey${address.getName()}`);
this.put(`identityKey${address.getName()}`, identityKey);
if (existing && toString(identityKey) !== toString(existing)) {
return Promise.resolve(true);
}
return Promise.resolve(false);
}
/* Returns a prekeypair object or undefined */
loadPreKey(keyId: number): any {
let res = this.get(`25519KeypreKey${keyId}`);
if (res !== undefined) {
res = { pubKey: res.pubKey, privKey: res.privKey };
}
return Promise.resolve(res);
}
storePreKey(keyId: number, keyPair: any): Promise<void> {
return Promise.resolve(this.put(`25519KeypreKey${keyId}`, keyPair));
}
removePreKey(keyId: number): Promise<void> {
return Promise.resolve(this.remove(`25519KeypreKey${keyId}`));
}
/* Returns a signed keypair object or undefined */
loadSignedPreKey(keyId: number): any {
let res = this.get(`25519KeysignedKey${keyId}`);
if (res !== undefined) {
res = { pubKey: res.pubKey, privKey: res.privKey };
}
return Promise.resolve(res);
}
storeSignedPreKey(keyId: number, keyPair: any): Promise<void> {
return Promise.resolve(this.put(`25519KeysignedKey${keyId}`, keyPair));
}
removeSignedPreKey(keyId: number): Promise<void> {
return Promise.resolve(this.remove(`25519KeysignedKey${keyId}`));
}
loadSession(identifier: string): Promise<any> {
return Promise.resolve(this.get(`session${identifier}`));
}
storeSession(identifier: string, record: any): Promise<void> {
return Promise.resolve(this.put(`session${identifier}`, record));
}
removeSession(identifier: string): Promise<void> {
return Promise.resolve(this.remove(`session${identifier}`));
}
removeAllSessions(identifier: string): Promise<void> {
// eslint-disable-next-line no-restricted-syntax
for (const id in this.store) {
if (id.startsWith(`session${identifier}`)) {
delete this.store[id];
}
}
return Promise.resolve();
}
}
describe('SecretSessionCipher', () => {
it('successfully roundtrips', async function thisNeeded() {
this.timeout(4000);
const aliceStore = new InMemorySignalProtocolStore();
const bobStore = new InMemorySignalProtocolStore();
await _initializeSessions(aliceStore, bobStore);
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor(
trustRoot,
'+14151111111',
1,
aliceIdentityKey.pubKey,
31337
);
const aliceCipher = new SecretSessionCipher(aliceStore as any);
const ciphertext = await aliceCipher.encrypt(
new window.libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized },
bytesFromString('smert za smert')
);
const bobCipher = new SecretSessionCipher(bobStore as any);
const decryptResult = await bobCipher.decrypt(
createCertificateValidator(trustRoot.pubKey),
ciphertext,
31335
);
if (!decryptResult.content) {
throw new Error('decryptResult.content is null!');
}
if (!decryptResult.sender) {
throw new Error('decryptResult.sender is null!');
}
assert.strictEqual(
stringFromBytes(decryptResult.content),
'smert za smert'
);
assert.strictEqual(decryptResult.sender.toString(), '+14151111111.1');
});
it('fails when untrusted', async function thisNeeded() {
this.timeout(4000);
const aliceStore = new InMemorySignalProtocolStore();
const bobStore = new InMemorySignalProtocolStore();
await _initializeSessions(aliceStore, bobStore);
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
const falseTrustRoot = await window.libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor(
falseTrustRoot,
'+14151111111',
1,
aliceIdentityKey.pubKey,
31337
);
const aliceCipher = new SecretSessionCipher(aliceStore as any);
const ciphertext = await aliceCipher.encrypt(
new window.libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized },
bytesFromString('и вот я')
);
const bobCipher = new SecretSessionCipher(bobStore as any);
try {
await bobCipher.decrypt(
createCertificateValidator(trustRoot.pubKey),
ciphertext,
31335
);
throw new Error('It did not fail!');
} catch (error) {
assert.strictEqual(error.message, 'Invalid signature');
}
});
it('fails when expired', async function thisNeeded() {
this.timeout(4000);
const aliceStore = new InMemorySignalProtocolStore();
const bobStore = new InMemorySignalProtocolStore();
await _initializeSessions(aliceStore, bobStore);
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor(
trustRoot,
'+14151111111',
1,
aliceIdentityKey.pubKey,
31337
);
const aliceCipher = new SecretSessionCipher(aliceStore as any);
const ciphertext = await aliceCipher.encrypt(
new window.libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized },
bytesFromString('и вот я')
);
const bobCipher = new SecretSessionCipher(bobStore as any);
try {
await bobCipher.decrypt(
createCertificateValidator(trustRoot.pubKey),
ciphertext,
31338
);
throw new Error('It did not fail!');
} catch (error) {
assert.strictEqual(error.message, 'Certificate is expired');
}
});
it('fails when wrong identity', async function thisNeeded() {
this.timeout(4000);
const aliceStore = new InMemorySignalProtocolStore();
const bobStore = new InMemorySignalProtocolStore();
await _initializeSessions(aliceStore, bobStore);
const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
const randomKeyPair = await window.libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor(
trustRoot,
'+14151111111',
1,
randomKeyPair.pubKey,
31337
);
const aliceCipher = new SecretSessionCipher(aliceStore as any);
const ciphertext = await aliceCipher.encrypt(
new window.libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized },
bytesFromString('smert za smert')
);
const bobCipher = new SecretSessionCipher(bobStore as any);
try {
await bobCipher.decrypt(
createCertificateValidator(trustRoot.pubKey),
ciphertext,
31335
);
throw new Error('It did not fail!');
} catch (error) {
assert.strictEqual(
error.message,
"Sender's certificate key does not match key used in message"
);
}
});
// private SenderCertificate _createCertificateFor(
// ECKeyPair trustRoot
// String sender
// int deviceId
// ECPublicKey identityKey
// long expires
// )
async function _createSenderCertificateFor(
trustRoot: KeyPairType,
sender: string,
deviceId: number,
identityKey: ArrayBuffer,
expires: number
): Promise<ExplodedSenderCertificateType> {
const serverKey = await window.libsignal.Curve.async.generateKeyPair();
const serverCertificateCertificateProto = new window.textsecure.protobuf.ServerCertificate.Certificate();
serverCertificateCertificateProto.id = 1;
serverCertificateCertificateProto.key = serverKey.pubKey;
const serverCertificateCertificateBytes = serverCertificateCertificateProto.toArrayBuffer();
const serverCertificateSignature = await window.libsignal.Curve.async.calculateSignature(
trustRoot.privKey,
serverCertificateCertificateBytes
);
const serverCertificateProto = new window.textsecure.protobuf.ServerCertificate();
serverCertificateProto.certificate = serverCertificateCertificateBytes;
serverCertificateProto.signature = serverCertificateSignature;
const serverCertificate = _createServerCertificateFromBuffer(
serverCertificateProto.toArrayBuffer()
);
const senderCertificateCertificateProto = new window.textsecure.protobuf.SenderCertificate.Certificate();
senderCertificateCertificateProto.sender = sender;
senderCertificateCertificateProto.senderDevice = deviceId;
senderCertificateCertificateProto.identityKey = identityKey;
senderCertificateCertificateProto.expires = expires;
senderCertificateCertificateProto.signer = window.textsecure.protobuf.ServerCertificate.decode(
serverCertificate.serialized
);
const senderCertificateBytes = senderCertificateCertificateProto.toArrayBuffer();
const senderCertificateSignature = await window.libsignal.Curve.async.calculateSignature(
serverKey.privKey,
senderCertificateBytes
);
const senderCertificateProto = new window.textsecure.protobuf.SenderCertificate();
senderCertificateProto.certificate = senderCertificateBytes;
senderCertificateProto.signature = senderCertificateSignature;
return _createSenderCertificateFromBuffer(
senderCertificateProto.toArrayBuffer()
);
}
// private void _initializeSessions(
// SignalProtocolStore aliceStore, SignalProtocolStore bobStore)
async function _initializeSessions(
aliceStore: InMemorySignalProtocolStore,
bobStore: InMemorySignalProtocolStore
): Promise<void> {
const aliceAddress = new window.libsignal.SignalProtocolAddress(
'+14152222222',
1
);
await aliceStore.put(
'identityKey',
await window.libsignal.Curve.generateKeyPair()
);
await bobStore.put(
'identityKey',
await window.libsignal.Curve.generateKeyPair()
);
await aliceStore.put('registrationId', 57);
await bobStore.put('registrationId', 58);
const bobPreKey = await window.libsignal.Curve.async.generateKeyPair();
const bobIdentityKey = await bobStore.getIdentityKeyPair();
const bobSignedPreKey = await window.libsignal.KeyHelper.generateSignedPreKey(
bobIdentityKey,
2
);
const bobBundle = {
deviceId: 3,
identityKey: bobIdentityKey.pubKey,
registrationId: 1,
signedPreKey: {
keyId: 2,
publicKey: bobSignedPreKey.keyPair.pubKey,
signature: bobSignedPreKey.signature,
},
preKey: {
keyId: 1,
publicKey: bobPreKey.pubKey,
},
};
const aliceSessionBuilder = new window.libsignal.SessionBuilder(
aliceStore as any,
aliceAddress
);
await aliceSessionBuilder.processPreKey(bobBundle);
await bobStore.storeSignedPreKey(2, bobSignedPreKey.keyPair);
await bobStore.storePreKey(1, bobPreKey);
}
});

View file

@ -11,7 +11,7 @@ import * as sinon from 'sinon';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { arrayBufferToBase64 } from '../../Crypto'; import { arrayBufferToBase64 } from '../../Crypto';
import { SenderCertificateClass } from '../../textsecure'; import { SenderCertificateClass } from '../../textsecure';
import { SenderCertificateMode } from '../../metadata/SecretSessionCipher'; import { SenderCertificateMode } from '../../textsecure/OutgoingMessage';
import { SenderCertificateService } from '../../services/senderCertificate'; import { SenderCertificateService } from '../../services/senderCertificate';

70
ts/textsecure.d.ts vendored
View file

@ -1,12 +1,6 @@
// Copyright 2020-2021 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import {
KeyPairType,
SessionRecordType,
SignedPreKeyType,
StorageType,
} from './libsignal.d';
import Crypto from './textsecure/Crypto'; import Crypto from './textsecure/Crypto';
import MessageReceiver from './textsecure/MessageReceiver'; import MessageReceiver from './textsecure/MessageReceiver';
import MessageSender from './textsecure/SendMessage'; import MessageSender from './textsecure/SendMessage';
@ -18,6 +12,7 @@ import { WebAPIType } from './textsecure/WebAPI';
import utils from './textsecure/Helpers'; import utils from './textsecure/Helpers';
import { CallingMessage as CallingMessageClass } from 'ringrtc'; import { CallingMessage as CallingMessageClass } from 'ringrtc';
import { WhatIsThis } from './window.d'; import { WhatIsThis } from './window.d';
import { SignalProtocolStore } from './SignalProtocolStore';
export type UnprocessedType = { export type UnprocessedType = {
attempts: number; attempts: number;
@ -79,7 +74,7 @@ export type TextSecureType = {
get: (key: string, defaultValue?: any) => any; get: (key: string, defaultValue?: any) => any;
put: (key: string, value: any) => Promise<void>; put: (key: string, value: any) => Promise<void>;
remove: (key: string | Array<string>) => Promise<void>; remove: (key: string | Array<string>) => Promise<void>;
protocol: StorageProtocolType; protocol: SignalProtocolStore;
}; };
messageReceiver: MessageReceiver; messageReceiver: MessageReceiver;
messageSender: MessageSender; messageSender: MessageSender;
@ -94,67 +89,6 @@ export type TextSecureType = {
SyncRequest: typeof SyncRequest; SyncRequest: typeof SyncRequest;
}; };
type StoredSignedPreKeyType = SignedPreKeyType & {
confirmed?: boolean;
created_at: number;
};
type IdentityKeyRecord = {
publicKey: ArrayBuffer;
firstUse: boolean;
timestamp: number;
verified: number;
nonblockingApproval: boolean;
};
export type StorageProtocolType = StorageType & {
VerifiedStatus: {
DEFAULT: number;
VERIFIED: number;
UNVERIFIED: number;
};
archiveSiblingSessions: (identifier: string) => Promise<void>;
removeSession: (identifier: string) => Promise<void>;
getDeviceIds: (identifier: string) => Promise<Array<number>>;
getIdentityRecord: (identifier: string) => IdentityKeyRecord | undefined;
getVerified: (id: string) => Promise<number>;
hydrateCaches: () => Promise<void>;
clearPreKeyStore: () => Promise<void>;
clearSignedPreKeysStore: () => Promise<void>;
clearSessionStore: () => Promise<void>;
isTrustedIdentity: () => void;
isUntrusted: (id: string) => boolean;
storePreKey: (keyId: number, keyPair: KeyPairType) => Promise<void>;
storeSignedPreKey: (
keyId: number,
keyPair: KeyPairType,
confirmed?: boolean
) => Promise<void>;
loadIdentityKey: (identifier: string) => Promise<ArrayBuffer | undefined>;
loadSignedPreKeys: () => Promise<Array<StoredSignedPreKeyType>>;
processVerifiedMessage: (
identifier: string,
verifiedStatus: number,
publicKey: ArrayBuffer
) => Promise<boolean>;
removeIdentityKey: (identifier: string) => Promise<void>;
saveIdentityWithAttributes: (
number: string,
options: IdentityKeyRecord
) => Promise<void>;
setApproval: (id: string, something: boolean) => void;
setVerified: (
encodedAddress: string,
verifiedStatus: number,
publicKey?: ArrayBuffer
) => Promise<void>;
removeSignedPreKey: (keyId: number) => Promise<void>;
removeAllSessions: (identifier: string) => Promise<void>;
removeAllData: () => Promise<void>;
on: (key: string, callback: () => void) => WhatIsThis;
removeAllConfiguration: () => Promise<void>;
};
// Protobufs // Protobufs
type DeviceMessagesProtobufTypes = { type DeviceMessagesProtobufTypes = {

View file

@ -11,16 +11,28 @@ import PQueue from 'p-queue';
import EventTarget from './EventTarget'; import EventTarget from './EventTarget';
import { WebAPIType } from './WebAPI'; import { WebAPIType } from './WebAPI';
import MessageReceiver from './MessageReceiver'; import MessageReceiver from './MessageReceiver';
import { KeyPairType, SignedPreKeyType } from '../libsignal.d'; import { KeyPairType, CompatSignedPreKeyType } from './Types.d';
import utils from './Helpers'; import utils from './Helpers';
import ProvisioningCipher from './ProvisioningCipher'; import ProvisioningCipher from './ProvisioningCipher';
import WebSocketResource, { import WebSocketResource, {
IncomingWebSocketRequest, IncomingWebSocketRequest,
} from './WebsocketResources'; } from './WebsocketResources';
import {
deriveAccessKey,
generateRegistrationId,
getRandomBytes,
} from '../Crypto';
import {
generateKeyPair,
generateSignedPreKey,
generatePreKey,
} from '../Curve';
import { isMoreRecentThan, isOlderThan } from '../util/timestamp'; import { isMoreRecentThan, isOlderThan } from '../util/timestamp';
const ARCHIVE_AGE = 30 * 24 * 60 * 60 * 1000; const ARCHIVE_AGE = 30 * 24 * 60 * 60 * 1000;
const PREKEY_ROTATION_AGE = 24 * 60 * 60 * 1000; const PREKEY_ROTATION_AGE = 24 * 60 * 60 * 1000;
const PROFILE_KEY_LENGTH = 32;
const SIGNED_KEY_GEN_BATCH_SIZE = 100;
function getIdentifier(id: string) { function getIdentifier(id: string) {
if (!id || !id.length) { if (!id || !id.length) {
@ -97,6 +109,9 @@ export default class AccountManager extends EventTarget {
async decryptDeviceName(base64: string) { async decryptDeviceName(base64: string) {
const identityKey = await window.textsecure.storage.protocol.getIdentityKeyPair(); const identityKey = await window.textsecure.storage.protocol.getIdentityKeyPair();
if (!identityKey) {
throw new Error('decryptDeviceName: No identity key pair!');
}
const arrayBuffer = MessageReceiver.stringToArrayBufferBase64(base64); const arrayBuffer = MessageReceiver.stringToArrayBufferBase64(base64);
const proto = window.textsecure.protobuf.DeviceName.decode(arrayBuffer); const proto = window.textsecure.protobuf.DeviceName.decode(arrayBuffer);
@ -139,39 +154,28 @@ export default class AccountManager extends EventTarget {
} }
async registerSingleDevice(number: string, verificationCode: string) { async registerSingleDevice(number: string, verificationCode: string) {
const registerKeys = this.server.registerKeys.bind(this.server); return this.queueTask(async () => {
const createAccount = this.createAccount.bind(this); const identityKeyPair = generateKeyPair();
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); const profileKey = getRandomBytes(PROFILE_KEY_LENGTH);
const generateKeys = this.generateKeys.bind(this, 100); const accessKey = await deriveAccessKey(profileKey);
const confirmKeys = this.confirmKeys.bind(this);
const registrationDone = this.registrationDone.bind(this);
return this.queueTask(async () =>
window.libsignal.KeyHelper.generateIdentityKeyPair().then(
async identityKeyPair => {
const profileKey = window.libsignal.crypto.getRandomBytes(32);
const accessKey = await window.Signal.Crypto.deriveAccessKey(
profileKey
);
return createAccount( await this.createAccount(
number, number,
verificationCode, verificationCode,
identityKeyPair, identityKeyPair,
profileKey, profileKey,
null, null,
null, null,
null, null,
{ accessKey } { accessKey }
) );
.then(clearSessionsAndPreKeys)
.then(async () => generateKeys()) await this.clearSessionsAndPreKeys();
.then(async (keys: GeneratedKeysType) => const keys = await this.generateKeys(SIGNED_KEY_GEN_BATCH_SIZE);
registerKeys(keys).then(async () => confirmKeys(keys)) await this.server.registerKeys(keys);
) await this.confirmKeys(keys);
.then(async () => registrationDone()); await this.registrationDone();
} });
)
);
} }
async registerSecondDevice( async registerSecondDevice(
@ -181,7 +185,11 @@ export default class AccountManager extends EventTarget {
) { ) {
const createAccount = this.createAccount.bind(this); const createAccount = this.createAccount.bind(this);
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
const generateKeys = this.generateKeys.bind(this, 100, progressCallback); const generateKeys = this.generateKeys.bind(
this,
SIGNED_KEY_GEN_BATCH_SIZE,
progressCallback
);
const confirmKeys = this.confirmKeys.bind(this); const confirmKeys = this.confirmKeys.bind(this);
const registrationDone = this.registrationDone.bind(this); const registrationDone = this.registrationDone.bind(this);
const registerKeys = this.server.registerKeys.bind(this.server); const registerKeys = this.server.registerKeys.bind(this.server);
@ -296,7 +304,10 @@ export default class AccountManager extends EventTarget {
} }
async refreshPreKeys() { async refreshPreKeys() {
const generateKeys = this.generateKeys.bind(this, 100); const generateKeys = this.generateKeys.bind(
this,
SIGNED_KEY_GEN_BATCH_SIZE
);
const registerKeys = this.server.registerKeys.bind(this.server); const registerKeys = this.server.registerKeys.bind(this.server);
return this.queueTask(async () => return this.queueTask(async () =>
@ -338,11 +349,13 @@ export default class AccountManager extends EventTarget {
return store return store
.getIdentityKeyPair() .getIdentityKeyPair()
.then( .then(
async (identityKey: KeyPairType) => async (identityKey: KeyPairType | undefined) => {
window.libsignal.KeyHelper.generateSignedPreKey( if (!identityKey) {
identityKey, throw new Error('rotateSignedPreKey: No identity key pair!');
signedKeyId }
),
return generateSignedPreKey(identityKey, signedKeyId);
},
() => { () => {
// We swallow any error here, because we don't want to get into // We swallow any error here, because we don't want to get into
// a loop of repeated retries. // a loop of repeated retries.
@ -352,7 +365,7 @@ export default class AccountManager extends EventTarget {
return null; return null;
} }
) )
.then(async (res: SignedPreKeyType | null) => { .then(async (res: CompatSignedPreKeyType | null) => {
if (!res) { if (!res) {
return null; return null;
} }
@ -489,11 +502,9 @@ export default class AccountManager extends EventTarget {
options: { accessKey?: ArrayBuffer; uuid?: string } = {} options: { accessKey?: ArrayBuffer; uuid?: string } = {}
): Promise<void> { ): Promise<void> {
const { accessKey, uuid } = options; const { accessKey, uuid } = options;
let password = btoa( let password = btoa(utils.getString(getRandomBytes(16)));
utils.getString(window.libsignal.crypto.getRandomBytes(16))
);
password = password.substring(0, password.length - 2); password = password.substring(0, password.length - 2);
const registrationId = window.libsignal.KeyHelper.generateRegistrationId(); const registrationId = generateRegistrationId();
const previousNumber = getIdentifier( const previousNumber = getIdentifier(
window.textsecure.storage.get('number_id') window.textsecure.storage.get('number_id')
@ -677,6 +688,10 @@ export default class AccountManager extends EventTarget {
const store = window.textsecure.storage.protocol; const store = window.textsecure.storage.protocol;
return store.getIdentityKeyPair().then(async identityKey => { return store.getIdentityKeyPair().then(async identityKey => {
if (!identityKey) {
throw new Error('generateKeys: No identity key pair!');
}
const result: any = { const result: any = {
preKeys: [], preKeys: [],
identityKey: identityKey.pubKey, identityKey: identityKey.pubKey,
@ -685,7 +700,7 @@ export default class AccountManager extends EventTarget {
for (let keyId = startId; keyId < startId + count; keyId += 1) { for (let keyId = startId; keyId < startId + count; keyId += 1) {
promises.push( promises.push(
window.libsignal.KeyHelper.generatePreKey(keyId).then(async res => { Promise.resolve(generatePreKey(keyId)).then(async res => {
await store.storePreKey(res.keyId, res.keyPair); await store.storePreKey(res.keyId, res.keyPair);
result.preKeys.push({ result.preKeys.push({
keyId: res.keyId, keyId: res.keyId,
@ -699,19 +714,18 @@ export default class AccountManager extends EventTarget {
} }
promises.push( promises.push(
window.libsignal.KeyHelper.generateSignedPreKey( Promise.resolve(generateSignedPreKey(identityKey, signedKeyId)).then(
identityKey, async res => {
signedKeyId await store.storeSignedPreKey(res.keyId, res.keyPair);
).then(async res => { result.signedPreKey = {
await store.storeSignedPreKey(res.keyId, res.keyPair); keyId: res.keyId,
result.signedPreKey = { publicKey: res.keyPair.pubKey,
keyId: res.keyId, signature: res.signature,
publicKey: res.keyPair.pubKey, // server.registerKeys doesn't use keyPair, confirmKeys does
signature: res.signature, keyPair: res.keyPair,
// server.registerKeys doesn't use keyPair, confirmKeys does };
keyPair: res.keyPair, }
}; )
})
); );
promises.push( promises.push(

View file

@ -5,6 +5,14 @@
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
import { ByteBufferClass } from '../window.d'; import { ByteBufferClass } from '../window.d';
import {
decryptAes256CbcPkcsPadding,
encryptAes256CbcPkcsPadding,
getRandomBytes as outerGetRandomBytes,
hmacSha256,
sha256,
verifyHmacSha256,
} from '../Crypto';
declare global { declare global {
// this is fixed in already, and won't be necessary when the new definitions // this is fixed in already, and won't be necessary when the new definitions
@ -134,10 +142,6 @@ async function verifyDigest(
}); });
} }
function calculateDigest(data: ArrayBuffer) {
return window.crypto.subtle.digest({ name: 'SHA-256' }, data);
}
const Crypto = { const Crypto = {
// Decrypts message into a raw string // Decrypts message into a raw string
async decryptWebsocketMessage( async decryptWebsocketMessage(
@ -175,11 +179,9 @@ const Crypto = {
decodedMessage.byteLength decodedMessage.byteLength
); );
return window.libsignal.crypto await verifyHmacSha256(ivAndCiphertext, macKey, mac, 10);
.verifyMAC(ivAndCiphertext, macKey, mac, 10)
.then(async () => return decryptAes256CbcPkcsPadding(aesKey, ciphertext, iv);
window.libsignal.crypto.decrypt(aesKey, ciphertext, iv)
);
}, },
async decryptAttachment( async decryptAttachment(
@ -205,18 +207,13 @@ const Crypto = {
encryptedBin.byteLength encryptedBin.byteLength
); );
return window.libsignal.crypto await verifyHmacSha256(ivAndCiphertext, macKey, mac, 32);
.verifyMAC(ivAndCiphertext, macKey, mac, 32)
.then(async () => {
if (theirDigest) {
return verifyDigest(encryptedBin, theirDigest);
}
return null; if (theirDigest) {
}) await verifyDigest(encryptedBin, theirDigest);
.then(async () => }
window.libsignal.crypto.decrypt(aesKey, ciphertext, iv)
); return decryptAes256CbcPkcsPadding(aesKey, ciphertext, iv);
}, },
async encryptAttachment( async encryptAttachment(
@ -239,36 +236,30 @@ const Crypto = {
const aesKey = keys.slice(0, 32); const aesKey = keys.slice(0, 32);
const macKey = keys.slice(32, 64); const macKey = keys.slice(32, 64);
return window.libsignal.crypto const ciphertext = await encryptAes256CbcPkcsPadding(aesKey, plaintext, iv);
.encrypt(aesKey, plaintext, iv)
.then(async ciphertext => {
const ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength);
ivAndCiphertext.set(new Uint8Array(iv));
ivAndCiphertext.set(new Uint8Array(ciphertext), 16);
return window.libsignal.crypto const ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength);
.calculateMAC(macKey, ivAndCiphertext.buffer as ArrayBuffer) ivAndCiphertext.set(new Uint8Array(iv));
.then(async mac => { ivAndCiphertext.set(new Uint8Array(ciphertext), 16);
const encryptedBin = new Uint8Array(
16 + ciphertext.byteLength + 32 const mac = await hmacSha256(macKey, ivAndCiphertext.buffer as ArrayBuffer);
);
encryptedBin.set(ivAndCiphertext); const encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32);
encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength); encryptedBin.set(ivAndCiphertext);
return calculateDigest(encryptedBin.buffer as ArrayBuffer).then( encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength);
digest => ({ const digest = await sha256(encryptedBin.buffer as ArrayBuffer);
ciphertext: encryptedBin.buffer,
digest, return {
}) ciphertext: encryptedBin.buffer,
); digest,
}); };
});
}, },
async encryptProfile( async encryptProfile(
data: ArrayBuffer, data: ArrayBuffer,
key: ArrayBuffer key: ArrayBuffer
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
const iv = window.libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH); const iv = outerGetRandomBytes(PROFILE_IV_LENGTH);
if (key.byteLength !== PROFILE_KEY_LENGTH) { if (key.byteLength !== PROFILE_KEY_LENGTH) {
throw new Error('Got invalid length profile key'); throw new Error('Got invalid length profile key');
} }
@ -389,7 +380,7 @@ const Crypto = {
}, },
getRandomBytes(size: number): ArrayBuffer { getRandomBytes(size: number): ArrayBuffer {
return window.libsignal.crypto.getRandomBytes(size); return outerGetRandomBytes(size);
}, },
}; };

View file

@ -36,25 +36,6 @@ export class ReplayableError extends Error {
} }
} }
export class IncomingIdentityKeyError extends ReplayableError {
identifier: string;
identityKey: ArrayBuffer;
// Note: Data to resend message is no longer captured
constructor(incomingIdentifier: string, _m: ArrayBuffer, key: ArrayBuffer) {
const identifer = incomingIdentifier.split('.')[0];
super({
name: 'IncomingIdentityKeyError',
message: `The identity of ${identifer} has changed.`,
});
this.identifier = identifer;
this.identityKey = key;
}
}
export class OutgoingIdentityKeyError extends ReplayableError { export class OutgoingIdentityKeyError extends ReplayableError {
identifier: string; identifier: string;

View file

@ -13,10 +13,27 @@ import { isNumber, map, omit, noop } from 'lodash';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import { v4 as getGuid } from 'uuid'; import { v4 as getGuid } from 'uuid';
import { SessionCipherClass, SignalProtocolAddressClass } from '../libsignal.d'; import {
PreKeySignalMessage,
ProtocolAddress,
PublicKey,
SealedSenderDecryptionResult,
sealedSenderDecryptMessage,
sealedSenderDecryptToUsmc,
signalDecrypt,
signalDecryptPreKey,
SignalMessage,
} from 'libsignal-client';
import {
IdentityKeys,
PreKeys,
Sessions,
SignedPreKeys,
} from '../LibSignalStores';
import { BatcherType, createBatcher } from '../util/batcher'; import { BatcherType, createBatcher } from '../util/batcher';
import { assert } from '../util/assert'; import { assert } from '../util/assert';
import { parseIntOrThrow } from '../util/parseIntOrThrow';
import EventTarget from './EventTarget'; import EventTarget from './EventTarget';
import { WebAPIType } from './WebAPI'; import { WebAPIType } from './WebAPI';
import utils from './Helpers'; import utils from './Helpers';
@ -24,13 +41,8 @@ import WebSocketResource, {
IncomingWebSocketRequest, IncomingWebSocketRequest,
} from './WebsocketResources'; } from './WebsocketResources';
import Crypto from './Crypto'; import Crypto from './Crypto';
import { deriveMasterKeyFromGroupV1 } from '../Crypto'; import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
import { ContactBuffer, GroupBuffer } from './ContactsParser'; import { ContactBuffer, GroupBuffer } from './ContactsParser';
import { IncomingIdentityKeyError } from './Errors';
import {
createCertificateValidator,
SecretSessionCipher,
} from '../metadata/SecretSessionCipher';
import { import {
AttachmentPointerClass, AttachmentPointerClass,
@ -93,8 +105,6 @@ declare global {
interface Error { interface Error {
reason?: any; reason?: any;
stackForLog?: string; stackForLog?: string;
sender?: SignalProtocolAddressClass;
senderUuid?: SignalProtocolAddressClass;
} }
} }
@ -196,7 +206,10 @@ class MessageReceiverInner extends EventTarget {
this.uuid_id = username ? utils.unencodeNumber(username)[0] : undefined; this.uuid_id = username ? utils.unencodeNumber(username)[0] : undefined;
this.deviceId = this.deviceId =
username || oldUsername username || oldUsername
? parseInt(utils.unencodeNumber(username || oldUsername)[1], 10) ? parseIntOrThrow(
utils.unencodeNumber(username || oldUsername)[1],
'MessageReceiver.constructor: username || oldUsername'
)
: undefined; : undefined;
this.incomingQueue = new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 }); this.incomingQueue = new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 });
@ -611,9 +624,12 @@ class MessageReceiverInner extends EventTarget {
envelope.source = envelope.source || item.source; envelope.source = envelope.source || item.source;
envelope.sourceUuid = envelope.sourceUuid || item.sourceUuid; envelope.sourceUuid = envelope.sourceUuid || item.sourceUuid;
envelope.sourceDevice = envelope.sourceDevice || item.sourceDevice; envelope.sourceDevice = envelope.sourceDevice || item.sourceDevice;
envelope.serverTimestamp = envelope.serverTimestamp envelope.serverTimestamp =
? envelope.serverTimestamp.toNumber() item.serverTimestamp || envelope.serverTimestamp;
: item.serverTimestamp;
if (envelope.serverTimestamp && envelope.serverTimestamp.toNumber) {
envelope.serverTimestamp = envelope.serverTimestamp.toNumber();
}
const { decrypted } = item; const { decrypted } = item;
if (decrypted) { if (decrypted) {
@ -947,195 +963,214 @@ class MessageReceiverInner extends EventTarget {
async decrypt( async decrypt(
envelope: EnvelopeClass, envelope: EnvelopeClass,
ciphertext: any ciphertext: any
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer | null> {
const { serverTrustRoot } = this; const { serverTrustRoot } = this;
let promise;
const identifier = envelope.sourceUuid || envelope.source; const identifier = envelope.sourceUuid || envelope.source;
const { sourceDevice } = envelope;
const address = new window.libsignal.SignalProtocolAddress( const localE164 = window.textsecure.storage.user.getNumber();
// Using source as opposed to sourceUuid allows us to get the existing const localUuid = window.textsecure.storage.user.getUuid();
// session if we haven't yet harvested the incoming uuid const localDeviceId = parseIntOrThrow(
identifier as any, window.textsecure.storage.user.getDeviceId(),
envelope.sourceDevice as any 'MessageReceiver.decrypt: localDeviceId'
); );
const ourNumber = window.textsecure.storage.user.getNumber(); if (!localUuid) {
const ourUuid = window.textsecure.storage.user.getUuid(); throw new Error('MessageReceiver.decrypt: Failed to fetch local UUID');
const options: any = {};
// No limit on message keys if we're communicating with our other devices
if (
(envelope.source && ourNumber && ourNumber === envelope.source) ||
(envelope.sourceUuid && ourUuid && ourUuid === envelope.sourceUuid)
) {
options.messageKeysLimit = false;
} }
const sessionCipher = new window.libsignal.SessionCipher( const sessionStore = new Sessions();
window.textsecure.storage.protocol, const identityKeyStore = new IdentityKeys();
address, const preKeyStore = new PreKeys();
options const signedPreKeyStore = new SignedPreKeys();
);
const secretSessionCipher = new SecretSessionCipher(
window.textsecure.storage.protocol,
options
);
const me = { let promise: Promise<
number: ourNumber, ArrayBuffer | { isMe: boolean } | { isBlocked: boolean } | undefined
uuid: ourUuid, >;
deviceId: parseInt(
window.textsecure.storage.user.getDeviceId() as string,
10
),
};
switch (envelope.type) { if (envelope.type === window.textsecure.protobuf.Envelope.Type.CIPHERTEXT) {
case window.textsecure.protobuf.Envelope.Type.CIPHERTEXT: window.log.info('message from', this.getEnvelopeId(envelope));
window.log.info('message from', this.getEnvelopeId(envelope)); if (!identifier) {
promise = sessionCipher throw new Error(
.decryptWhisperMessage(ciphertext) 'MessageReceiver.decrypt: No identifier for CIPHERTEXT message'
.then(this.unpad);
break;
case window.textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
window.log.info('prekey message from', this.getEnvelopeId(envelope));
promise = this.decryptPreKeyWhisperMessage(
ciphertext,
sessionCipher,
address
); );
break; }
case window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER: if (!sourceDevice) {
window.log.info('received unidentified sender message'); throw new Error(
promise = secretSessionCipher 'MessageReceiver.decrypt: No sourceDevice for CIPHERTEXT message'
.decrypt( );
createCertificateValidator(serverTrustRoot), }
ciphertext.toArrayBuffer(), const signalMessage = SignalMessage.deserialize(
Math.min(envelope.serverTimestamp || Date.now(), Date.now()), Buffer.from(ciphertext.toArrayBuffer())
me );
)
.then(
result => {
const { isMe, sender, senderUuid, content } = result;
// We need to drop incoming messages from ourself since server can't const address = `${identifier}.${sourceDevice}`;
// do it for us promise = window.textsecure.storage.protocol.enqueueSessionJob(
if (isMe) { address,
return { isMe: true }; () =>
} signalDecrypt(
signalMessage,
ProtocolAddress.new(identifier, sourceDevice),
sessionStore,
identityKeyStore
).then(plaintext => this.unpad(typedArrayToArrayBuffer(plaintext)))
);
} else if (
envelope.type === window.textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE
) {
window.log.info('prekey message from', this.getEnvelopeId(envelope));
if (!identifier) {
throw new Error(
'MessageReceiver.decrypt: No identifier for PREKEY_BUNDLE message'
);
}
if (!sourceDevice) {
throw new Error(
'MessageReceiver.decrypt: No sourceDevice for PREKEY_BUNDLE message'
);
}
const preKeySignalMessage = PreKeySignalMessage.deserialize(
Buffer.from(ciphertext.toArrayBuffer())
);
if ( const address = `${identifier}.${sourceDevice}`;
(sender && this.isBlocked(sender.getName())) || promise = window.textsecure.storage.protocol.enqueueSessionJob(
(senderUuid && this.isUuidBlocked(senderUuid.getName())) address,
) { () =>
window.log.info( signalDecryptPreKey(
'Dropping blocked message after sealed sender decryption' preKeySignalMessage,
); ProtocolAddress.new(identifier, sourceDevice),
return { isBlocked: true }; sessionStore,
} identityKeyStore,
preKeyStore,
signedPreKeyStore
).then(plaintext => this.unpad(typedArrayToArrayBuffer(plaintext)))
);
} else if (
envelope.type ===
window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER
) {
window.log.info('received unidentified sender message');
const buffer = Buffer.from(ciphertext.toArrayBuffer());
// Here we take this sender information and attach it back to the envelope const decryptSealedSender = async (): Promise<
// to make the rest of the app work properly. SealedSenderDecryptionResult | null | { isBlocked: true }
> => {
const messageContent = await sealedSenderDecryptToUsmc(
buffer,
identityKeyStore
);
const originalSource = envelope.source; // Here we take this sender information and attach it back to the envelope
const originalSourceUuid = envelope.sourceUuid; // to make the rest of the app work properly.
const certificate = messageContent.senderCertificate();
// eslint-disable-next-line no-param-reassign const originalSource = envelope.source;
envelope.source = sender && sender.getName(); const originalSourceUuid = envelope.sourceUuid;
// eslint-disable-next-line no-param-reassign
envelope.sourceUuid = senderUuid && senderUuid.getName();
window.normalizeUuids(
envelope,
['sourceUuid'],
'message_receiver::decrypt::UNIDENTIFIED_SENDER'
);
// eslint-disable-next-line no-param-reassign
envelope.sourceDevice =
(sender && sender.getDeviceId()) ||
(senderUuid && senderUuid.getDeviceId());
// eslint-disable-next-line no-param-reassign
envelope.unidentifiedDeliveryReceived = !(
originalSource || originalSourceUuid
);
if (!content) { // eslint-disable-next-line no-param-reassign
throw new Error( envelope.source = certificate.senderE164() || undefined;
'MessageReceiver.decrypt: Content returned was falsey!' // eslint-disable-next-line no-param-reassign
); envelope.sourceUuid = certificate.senderUuid();
} window.normalizeUuids(
envelope,
['sourceUuid'],
'message_receiver::decrypt::UNIDENTIFIED_SENDER'
);
// eslint-disable-next-line no-param-reassign
envelope.sourceDevice = certificate.senderDeviceId();
// eslint-disable-next-line no-param-reassign
envelope.unidentifiedDeliveryReceived = !(
originalSource || originalSourceUuid
);
// Return just the content because that matches the signature of the other if (
// decrypt methods used above. (envelope.source && this.isBlocked(envelope.source)) ||
return this.unpad(content); (envelope.sourceUuid && this.isUuidBlocked(envelope.sourceUuid))
}, ) {
(error: Error) => { window.log.info(
const { sender, senderUuid } = error || {}; 'MessageReceiver.decrypt: Dropping blocked message after partial sealed sender decryption'
if (sender || senderUuid) {
const originalSource = envelope.source;
const originalSourceUuid = envelope.sourceUuid;
if (
(sender && this.isBlocked(sender.getName())) ||
(senderUuid && this.isUuidBlocked(senderUuid.getName()))
) {
window.log.info(
'Dropping blocked message with error after sealed sender decryption'
);
return { isBlocked: true };
}
// eslint-disable-next-line no-param-reassign
envelope.source = sender && sender.getName();
// eslint-disable-next-line no-param-reassign
envelope.sourceUuid =
senderUuid && senderUuid.getName().toLowerCase();
window.normalizeUuids(
envelope,
['sourceUuid'],
'message_receiver::decrypt::UNIDENTIFIED_SENDER::error'
);
// eslint-disable-next-line no-param-reassign
envelope.sourceDevice =
(sender && sender.getDeviceId()) ||
(senderUuid && senderUuid.getDeviceId());
// eslint-disable-next-line no-param-reassign
envelope.unidentifiedDeliveryReceived = !(
originalSource || originalSourceUuid
);
throw error;
}
this.removeFromCache(envelope);
throw error;
}
); );
break; return { isBlocked: true };
default: }
promise = Promise.reject(new Error('Unknown message type'));
if (!envelope.serverTimestamp) {
throw new Error(
'MessageReceiver.decrypt: Sealed sender message was missing serverTimestamp'
);
}
const sealedSenderIdentifier = envelope.sourceUuid || envelope.source;
const address = `${sealedSenderIdentifier}.${envelope.sourceDevice}`;
return window.textsecure.storage.protocol.enqueueSessionJob(
address,
() =>
sealedSenderDecryptMessage(
buffer,
PublicKey.deserialize(Buffer.from(serverTrustRoot)),
envelope.serverTimestamp,
localE164,
localUuid,
localDeviceId,
sessionStore,
identityKeyStore,
preKeyStore,
signedPreKeyStore
)
);
};
promise = decryptSealedSender().then(result => {
if (result === null) {
return { isMe: true };
}
if ('isBlocked' in result) {
return result;
}
const content = typedArrayToArrayBuffer(result.message());
if (!content) {
throw new Error(
'MessageReceiver.decrypt: Content returned was falsey!'
);
}
// Return just the content because that matches the signature of the other
// decrypt methods used above.
return this.unpad(content);
});
} else {
promise = Promise.reject(new Error('Unknown message type'));
} }
return promise return promise
.then((plaintext: any) => { .then(
const { isMe, isBlocked } = plaintext || {}; (
if (isMe || isBlocked) { plaintext:
this.removeFromCache(envelope); | ArrayBuffer
return null; | { isMe: boolean }
} | { isBlocked: boolean }
| undefined
) => {
if (!plaintext || 'isMe' in plaintext || 'isBlocked' in plaintext) {
this.removeFromCache(envelope);
return null;
}
// Note: this is an out of band update; there are cases where the item in the // Note: this is an out of band update; there are cases where the item in the
// cache has already been deleted by the time this runs. That's okay. // cache has already been deleted by the time this runs. That's okay.
try { try {
this.updateCache(envelope, plaintext); this.updateCache(envelope, plaintext);
} catch (error) { } catch (error) {
const errorString = error && error.stack ? error.stack : error; const errorString = error && error.stack ? error.stack : error;
window.log.error(`decrypt: updateCache failed: ${errorString}`); window.log.error(`decrypt: updateCache failed: ${errorString}`);
} }
return plaintext; return plaintext;
}) }
)
.catch(async error => { .catch(async error => {
this.removeFromCache(envelope); this.removeFromCache(envelope);
@ -1143,7 +1178,10 @@ class MessageReceiverInner extends EventTarget {
const deviceId = envelope.sourceDevice; const deviceId = envelope.sourceDevice;
// We don't do a light session reset if it's just a duplicated message // We don't do a light session reset if it's just a duplicated message
if (error && error.name === 'MessageCounterError') { if (
error?.message?.includes &&
error.message.includes('message with old counter')
) {
throw error; throw error;
} }
@ -1248,43 +1286,15 @@ class MessageReceiverInner extends EventTarget {
window.log.warn(`lightSessionReset/${id}: Resetting session`); window.log.warn(`lightSessionReset/${id}: Resetting session`);
// Archive open session with this device // Archive open session with this device
const address = new window.libsignal.SignalProtocolAddress(uuid, deviceId); await window.textsecure.storage.protocol.archiveSession(
const sessionCipher = new window.libsignal.SessionCipher( `${uuid}.${deviceId}`
window.textsecure.storage.protocol,
address
); );
await sessionCipher.closeOpenSessionForDevice();
// Send a null message with newly-created session // Send a null message with newly-created session
const sendOptions = await conversation.getSendOptions(); const sendOptions = await conversation.getSendOptions();
await window.textsecure.messaging.sendNullMessage({ uuid }, sendOptions); await window.textsecure.messaging.sendNullMessage({ uuid }, sendOptions);
} }
async decryptPreKeyWhisperMessage(
ciphertext: ArrayBuffer,
sessionCipher: SessionCipherClass,
address: SignalProtocolAddressClass
) {
const padded = await sessionCipher.decryptPreKeyWhisperMessage(ciphertext);
try {
return this.unpad(padded);
} catch (e) {
if (e.message === 'Unknown identity key') {
// create an error that the UI will pick up and ask the
// user if they want to re-negotiate
const buffer = window.dcodeIO.ByteBuffer.wrap(ciphertext);
throw new IncomingIdentityKeyError(
address.toString(),
buffer.toArrayBuffer(),
e.identityKey
);
}
throw e;
}
}
async handleSentMessage( async handleSentMessage(
envelope: EnvelopeClass, envelope: EnvelopeClass,
sentContainer: SyncMessageClass.Sent sentContainer: SyncMessageClass.Sent
@ -1866,7 +1876,10 @@ class MessageReceiverInner extends EventTarget {
} }
this.removeFromCache(envelope); this.removeFromCache(envelope);
throw new Error('Got empty SyncMessage'); window.log.warn(
`handleSyncMessage/${this.getEnvelopeId(envelope)}: Got empty SyncMessage`
);
return Promise.resolve();
} }
async handleConfiguration( async handleConfiguration(
@ -2213,29 +2226,8 @@ class MessageReceiverInner extends EventTarget {
} }
async handleEndSession(identifier: string) { async handleEndSession(identifier: string) {
window.log.info('got end session'); window.log.info(`handleEndSession: closing sessions for ${identifier}`);
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds( await window.textsecure.storage.protocol.archiveAllSessions(identifier);
identifier
);
return Promise.all(
deviceIds.map(async deviceId => {
const address = new window.libsignal.SignalProtocolAddress(
identifier,
deviceId
);
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address
);
window.log.info(
'handleEndSession: closing sessions for',
address.toString()
);
return sessionCipher.closeOpenSessionForDevice();
})
);
} }
async processDecrypted(envelope: EnvelopeClass, decrypted: DataMessageClass) { async processDecrypted(envelope: EnvelopeClass, decrypted: DataMessageClass) {

View file

@ -9,9 +9,21 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { reject } from 'lodash'; import { reject } from 'lodash';
import * as z from 'zod';
import {
CiphertextMessageType,
PreKeyBundle,
processPreKeyBundle,
ProtocolAddress,
PublicKey,
sealedSenderEncryptMessage,
SenderCertificate,
signalEncrypt,
} from 'libsignal-client';
import { ServerKeysType, WebAPIType } from './WebAPI'; import { ServerKeysType, WebAPIType } from './WebAPI';
import { isEnabled as isRemoteFlagEnabled } from '../RemoteConfig'; import { isEnabled as isRemoteFlagEnabled } from '../RemoteConfig';
import { SignalProtocolAddressClass } from '../libsignal.d';
import { ContentClass, DataMessageClass } from '../textsecure.d'; import { ContentClass, DataMessageClass } from '../textsecure.d';
import { import {
CallbackResultType, CallbackResultType,
@ -26,12 +38,40 @@ import {
UnregisteredUserError, UnregisteredUserError,
} from './Errors'; } from './Errors';
import { isValidNumber } from '../types/PhoneNumber'; import { isValidNumber } from '../types/PhoneNumber';
import { SecretSessionCipher } from '../metadata/SecretSessionCipher'; import { Sessions, IdentityKeys } from '../LibSignalStores';
export const enum SenderCertificateMode {
WithE164,
WithoutE164,
}
export const serializedCertificateSchema = z
.object({
expires: z.number().optional(),
serialized: z.instanceof(ArrayBuffer),
})
.nonstrict();
export type SerializedCertificateType = z.infer<
typeof serializedCertificateSchema
>;
type OutgoingMessageOptionsType = SendOptionsType & { type OutgoingMessageOptionsType = SendOptionsType & {
online?: boolean; online?: boolean;
}; };
function ciphertextMessageTypeToEnvelopeType(type: number) {
if (type === CiphertextMessageType.PreKey) {
return window.textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE;
}
if (type === CiphertextMessageType.Whisper) {
return window.textsecure.protobuf.Envelope.Type.CIPHERTEXT;
}
throw new Error(
`ciphertextMessageTypeToEnvelopeType: Unrecognized type ${type}`
);
}
export default class OutgoingMessage { export default class OutgoingMessage {
server: WebAPIType; server: WebAPIType;
@ -142,7 +182,7 @@ export default class OutgoingMessage {
if (deviceIds.length === 0) { if (deviceIds.length === 0) {
this.registerError( this.registerError(
identifier, identifier,
'Got empty device list when loading device keys', 'reloadDevicesAndSend: Got empty device list when loading device keys',
undefined undefined
); );
return undefined; return undefined;
@ -153,44 +193,76 @@ export default class OutgoingMessage {
async getKeysForIdentifier( async getKeysForIdentifier(
identifier: string, identifier: string,
updateDevices: Array<number> updateDevices: Array<number> | undefined
): Promise<void | Array<void | null>> { ): Promise<void | Array<void | null>> {
const handleResult = async (response: ServerKeysType) => const handleResult = async (response: ServerKeysType) => {
Promise.all( const sessionStore = new Sessions();
const identityKeyStore = new IdentityKeys();
return Promise.all(
response.devices.map(async device => { response.devices.map(async device => {
const { deviceId, registrationId, preKey, signedPreKey } = device;
if ( if (
updateDevices === undefined || updateDevices === undefined ||
updateDevices.indexOf(device.deviceId) > -1 updateDevices.indexOf(deviceId) > -1
) { ) {
const address = new window.libsignal.SignalProtocolAddress(
identifier,
device.deviceId
);
const builder = new window.libsignal.SessionBuilder(
window.textsecure.storage.protocol,
address
);
if (device.registrationId === 0) { if (device.registrationId === 0) {
window.log.info('device registrationId 0!'); window.log.info('device registrationId 0!');
} }
if (!signedPreKey) {
throw new Error(
`getKeysForIdentifier/${identifier}: Missing signed prekey for deviceId ${deviceId}`
);
}
const protocolAddress = ProtocolAddress.new(identifier, deviceId);
const preKeyId = preKey?.keyId || null;
const preKeyObject = preKey
? PublicKey.deserialize(Buffer.from(preKey.publicKey))
: null;
const signedPreKeyObject = PublicKey.deserialize(
Buffer.from(signedPreKey.publicKey)
);
const identityKey = PublicKey.deserialize(
Buffer.from(response.identityKey)
);
const deviceForProcess = { const preKeyBundle = PreKeyBundle.new(
...device, registrationId,
identityKey: response.identityKey, deviceId,
}; preKeyId,
return builder.processPreKey(deviceForProcess).catch(error => { preKeyObject,
if (error.message === 'Identity key changed') { signedPreKey.keyId,
error.timestamp = this.timestamp; signedPreKeyObject,
error.originalMessage = this.message.toArrayBuffer(); Buffer.from(signedPreKey.signature),
error.identityKey = response.identityKey; identityKey
} );
throw error;
}); const address = `${identifier}.${deviceId}`;
await window.textsecure.storage.protocol
.enqueueSessionJob(address, () =>
processPreKeyBundle(
preKeyBundle,
protocolAddress,
sessionStore,
identityKeyStore
)
)
.catch(error => {
if (
error?.message?.includes('untrusted identity for address')
) {
error.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer();
error.identityKey = response.identityKey;
}
throw error;
});
} }
return null; return null;
}) })
); );
};
const { sendMetadata } = this; const { sendMetadata } = this;
const info = const info =
@ -329,13 +401,6 @@ export default class OutgoingMessage {
deviceIds: Array<number>, deviceIds: Array<number>,
recurse?: boolean recurse?: boolean
): Promise<void> { ): Promise<void> {
const ciphers: {
[key: number]: {
closeOpenSessionForDevice: (
address: SignalProtocolAddressClass
) => Promise<void>;
};
} = {};
const plaintext = this.getPlaintext(); const plaintext = this.getPlaintext();
const { sendMetadata } = this; const { sendMetadata } = this;
@ -364,54 +429,59 @@ export default class OutgoingMessage {
); );
} }
const sessionStore = new Sessions();
const identityKeyStore = new IdentityKeys();
return Promise.all( return Promise.all(
deviceIds.map(async deviceId => { deviceIds.map(async destinationDeviceId => {
const address = new window.libsignal.SignalProtocolAddress( const protocolAddress = ProtocolAddress.new(
identifier, identifier,
deviceId destinationDeviceId
); );
const options: any = {}; const activeSession = await sessionStore.getSession(protocolAddress);
if (!activeSession) {
// No limit on message keys if we're communicating with our other devices throw new Error('OutgoingMessage.doSendMessage: No active sesssion!');
if (ourNumber === identifier || ourUuid === identifier) {
options.messageKeysLimit = false;
} }
if (sealedSender && senderCertificate) { const destinationRegistrationId = activeSession.remoteRegistrationId();
const secretSessionCipher = new SecretSessionCipher(
window.textsecure.storage.protocol
);
ciphers[address.getDeviceId()] = secretSessionCipher;
const ciphertext = await secretSessionCipher.encrypt( if (sealedSender && senderCertificate) {
address, const certificate = SenderCertificate.deserialize(
senderCertificate, Buffer.from(senderCertificate.serialized)
plaintext );
const buffer = await sealedSenderEncryptMessage(
Buffer.from(plaintext),
protocolAddress,
certificate,
sessionStore,
identityKeyStore
); );
return { return {
type: window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER, type: window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
destinationDeviceId: address.getDeviceId(), destinationDeviceId,
destinationRegistrationId: await secretSessionCipher.getRemoteRegistrationId( destinationRegistrationId,
address content: buffer.toString('base64'),
),
content: window.Signal.Crypto.arrayBufferToBase64(ciphertext),
}; };
} }
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address,
options
);
ciphers[address.getDeviceId()] = sessionCipher;
const ciphertext = await sessionCipher.encrypt(plaintext); const ciphertextMessage = await signalEncrypt(
Buffer.from(plaintext),
protocolAddress,
sessionStore,
identityKeyStore
);
const type = ciphertextMessageTypeToEnvelopeType(
ciphertextMessage.type()
);
return { return {
type: ciphertext.type, type,
destinationDeviceId: address.getDeviceId(), destinationDeviceId,
destinationRegistrationId: ciphertext.registrationId, destinationRegistrationId,
content: btoa(ciphertext.body), content: ciphertextMessage.serialize().toString('base64'),
}; };
}) })
) )
@ -474,14 +544,11 @@ export default class OutgoingMessage {
); );
} else { } else {
p = Promise.all( p = Promise.all(
error.response.staleDevices.map(async (deviceId: number) => error.response.staleDevices.map(async (deviceId: number) => {
ciphers[deviceId].closeOpenSessionForDevice( await window.textsecure.storage.protocol.archiveSession(
new window.libsignal.SignalProtocolAddress( `${identifier}.${deviceId}`
identifier, );
deviceId })
)
)
)
); );
} }
@ -497,7 +564,7 @@ export default class OutgoingMessage {
); );
}); });
} }
if (error.message === 'Identity key changed') { if (error?.message?.includes('untrusted identity for address')) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
error.timestamp = this.timestamp; error.timestamp = this.timestamp;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@ -509,34 +576,19 @@ export default class OutgoingMessage {
); );
window.log.info('closing all sessions for', identifier); window.log.info('closing all sessions for', identifier);
const address = new window.libsignal.SignalProtocolAddress( window.textsecure.storage.protocol
identifier, .archiveAllSessions(identifier)
1 .then(
); () => {
throw error;
const sessionCipher = new window.libsignal.SessionCipher( },
window.textsecure.storage.protocol, innerError => {
address window.log.error(
); `doSendMessage: Error closing sessions: ${innerError.stack}`
window.log.info('closing session for', address.toString()); );
return Promise.all([ throw error;
// Primary device }
sessionCipher.closeOpenSessionForDevice(), );
// The rest of their devices
window.textsecure.storage.protocol.archiveSiblingSessions(
address.toString()
),
]).then(
() => {
throw error;
},
innerError => {
window.log.error(
`doSendMessage: Error closing sessions: ${innerError.stack}`
);
throw error;
}
);
} }
this.registerError( this.registerError(
@ -551,32 +603,30 @@ export default class OutgoingMessage {
async getStaleDeviceIdsForIdentifier( async getStaleDeviceIdsForIdentifier(
identifier: string identifier: string
): Promise<Array<number>> { ): Promise<Array<number> | undefined> {
return window.textsecure.storage.protocol const sessionStore = new Sessions();
.getDeviceIds(identifier)
.then(async deviceIds => { const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
if (deviceIds.length === 0) { identifier
return [1]; );
if (deviceIds.length === 0) {
return undefined;
}
const updateDevices: Array<number> = [];
await Promise.all(
deviceIds.map(async deviceId => {
const record = await sessionStore.getSession(
ProtocolAddress.new(identifier, deviceId)
);
if (!record || !record.hasCurrentState()) {
updateDevices.push(deviceId);
} }
const updateDevices: Array<number> = []; })
return Promise.all( );
deviceIds.map(async deviceId => {
const address = new window.libsignal.SignalProtocolAddress( return updateDevices;
identifier,
deviceId
);
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address
);
return sessionCipher.hasOpenSession().then(hasSession => {
if (!hasSession) {
updateDevices.push(deviceId);
}
});
})
).then(() => updateDevices);
});
} }
async removeDeviceIdsForIdentifier( async removeDeviceIdsForIdentifier(
@ -585,15 +635,9 @@ export default class OutgoingMessage {
): Promise<void> { ): Promise<void> {
await Promise.all( await Promise.all(
deviceIdsToRemove.map(async deviceId => { deviceIdsToRemove.map(async deviceId => {
const address = new window.libsignal.SignalProtocolAddress( await window.textsecure.storage.protocol.archiveSession(
identifier, `${identifier}.${deviceId}`
deviceId
); );
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address
);
await sessionCipher.closeOpenSessionForDevice();
}) })
); );
} }
@ -652,14 +696,14 @@ export default class OutgoingMessage {
await this.getKeysForIdentifier(identifier, updateDevices); await this.getKeysForIdentifier(identifier, updateDevices);
await this.reloadDevicesAndSend(identifier, true)(); await this.reloadDevicesAndSend(identifier, true)();
} catch (error) { } catch (error) {
if (error.message === 'Identity key changed') { if (error?.message?.includes('untrusted identity for address')) {
const newError = new OutgoingIdentityKeyError( const newError = new OutgoingIdentityKeyError(
identifier, identifier,
error.originalMessage, error.originalMessage,
error.timestamp, error.timestamp,
error.identityKey error.identityKey
); );
this.registerError(identifier, 'Identity key changed', newError); this.registerError(identifier, 'Untrusted identity', newError);
} else { } else {
this.registerError( this.registerError(
identifier, identifier,

View file

@ -4,8 +4,15 @@
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import { KeyPairType } from '../libsignal.d'; import { KeyPairType } from './Types.d';
import { ProvisionEnvelopeClass } from '../textsecure.d'; import { ProvisionEnvelopeClass } from '../textsecure.d';
import {
decryptAes256CbcPkcsPadding,
deriveSecrets,
bytesFromString,
verifyHmacSha256,
} from '../Crypto';
import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve';
type ProvisionDecryptResult = { type ProvisionDecryptResult = {
identityKeyPair: KeyPairType; identityKeyPair: KeyPairType;
@ -38,73 +45,55 @@ class ProvisioningCipherInner {
throw new Error('ProvisioningCipher.decrypt: No keypair!'); throw new Error('ProvisioningCipher.decrypt: No keypair!');
} }
return window.libsignal.Curve.async const ecRes = calculateAgreement(masterEphemeral, this.keyPair.privKey);
.calculateAgreement(masterEphemeral, this.keyPair.privKey) const keys = deriveSecrets(
.then(async ecRes => ecRes,
window.libsignal.HKDF.deriveSecrets( new ArrayBuffer(32),
ecRes, bytesFromString('TextSecure Provisioning Message')
new ArrayBuffer(32), );
'TextSecure Provisioning Message' await verifyHmacSha256(ivAndCiphertext, keys[1], mac, 32);
)
)
.then(async keys =>
window.libsignal.crypto
.verifyMAC(ivAndCiphertext, keys[1], mac, 32)
.then(async () =>
window.libsignal.crypto.decrypt(keys[0], ciphertext, iv)
)
)
.then(async plaintext => {
const provisionMessage = window.textsecure.protobuf.ProvisionMessage.decode(
plaintext
);
const privKey = provisionMessage.identityKeyPrivate.toArrayBuffer();
return window.libsignal.Curve.async const plaintext = await decryptAes256CbcPkcsPadding(
.createKeyPair(privKey) keys[0],
.then(keyPair => { ciphertext,
window.normalizeUuids( iv
provisionMessage, );
['uuid'], const provisionMessage = window.textsecure.protobuf.ProvisionMessage.decode(
'ProvisioningCipher.decrypt' plaintext
); );
const privKey = provisionMessage.identityKeyPrivate.toArrayBuffer();
const ret: ProvisionDecryptResult = { const keyPair = createKeyPair(privKey);
identityKeyPair: keyPair, window.normalizeUuids(
number: provisionMessage.number, provisionMessage,
uuid: provisionMessage.uuid, ['uuid'],
provisioningCode: provisionMessage.provisioningCode, 'ProvisioningCipher.decrypt'
userAgent: provisionMessage.userAgent, );
readReceipts: provisionMessage.readReceipts,
}; const ret: ProvisionDecryptResult = {
if (provisionMessage.profileKey) { identityKeyPair: keyPair,
ret.profileKey = provisionMessage.profileKey.toArrayBuffer(); number: provisionMessage.number,
} uuid: provisionMessage.uuid,
return ret; provisioningCode: provisionMessage.provisioningCode,
}); userAgent: provisionMessage.userAgent,
}); readReceipts: provisionMessage.readReceipts,
};
if (provisionMessage.profileKey) {
ret.profileKey = provisionMessage.profileKey.toArrayBuffer();
}
return ret;
} }
async getPublicKey(): Promise<ArrayBuffer> { async getPublicKey(): Promise<ArrayBuffer> {
return Promise.resolve() if (!this.keyPair) {
.then(async () => { this.keyPair = generateKeyPair();
if (!this.keyPair) { }
return window.libsignal.Curve.async
.generateKeyPair()
.then(keyPair => {
this.keyPair = keyPair;
});
}
return null; if (!this.keyPair) {
}) throw new Error('ProvisioningCipher.decrypt: No keypair!');
.then(() => { }
if (!this.keyPair) {
throw new Error('ProvisioningCipher.decrypt: No keypair!');
}
return this.keyPair.pubKey; return this.keyPair.pubKey;
});
} }
} }

View file

@ -19,11 +19,12 @@ import {
WebAPIType, WebAPIType,
} from './WebAPI'; } from './WebAPI';
import createTaskWithTimeout from './TaskWithTimeout'; import createTaskWithTimeout from './TaskWithTimeout';
import OutgoingMessage from './OutgoingMessage'; import OutgoingMessage, { SerializedCertificateType } from './OutgoingMessage';
import Crypto from './Crypto'; import Crypto from './Crypto';
import { import {
base64ToArrayBuffer, base64ToArrayBuffer,
concatenateBytes, concatenateBytes,
getRandomBytes,
getZeroes, getZeroes,
hexToArrayBuffer, hexToArrayBuffer,
} from '../Crypto'; } from '../Crypto';
@ -46,7 +47,6 @@ import {
LinkPreviewImage, LinkPreviewImage,
LinkPreviewMetadata, LinkPreviewMetadata,
} from '../linkPreviews/linkPreviewFetch'; } from '../linkPreviews/linkPreviewFetch';
import { SerializedCertificateType } from '../metadata/SecretSessionCipher';
function stringToArrayBuffer(str: string): ArrayBuffer { function stringToArrayBuffer(str: string): ArrayBuffer {
if (typeof str !== 'string') { if (typeof str !== 'string') {
@ -473,8 +473,8 @@ export default class MessageSender {
} }
const padded = this.getPaddedAttachment(data); const padded = this.getPaddedAttachment(data);
const key = window.libsignal.crypto.getRandomBytes(64); const key = getRandomBytes(64);
const iv = window.libsignal.crypto.getRandomBytes(16); const iv = getRandomBytes(16);
const result = await Crypto.encryptAttachment(padded, key, iv); const result = await Crypto.encryptAttachment(padded, key, iv);
const id = await this.server.putAttachment(result.ciphertext); const id = await this.server.putAttachment(result.ciphertext);
@ -1368,11 +1368,11 @@ export default class MessageSender {
getRandomPadding(): ArrayBuffer { getRandomPadding(): ArrayBuffer {
// Generate a random int from 1 and 512 // Generate a random int from 1 and 512
const buffer = window.libsignal.crypto.getRandomBytes(2); const buffer = getRandomBytes(2);
const paddingLength = (new Uint16Array(buffer)[0] & 0x1ff) + 1; const paddingLength = (new Uint16Array(buffer)[0] & 0x1ff) + 1;
// Generate a random padding buffer of the chosen size // Generate a random padding buffer of the chosen size
return window.libsignal.crypto.getRandomBytes(paddingLength); return getRandomBytes(paddingLength);
} }
async sendNullMessage( async sendNullMessage(
@ -1607,31 +1607,10 @@ export default class MessageSender {
window.log.error(prefix, error && error.stack ? error.stack : error); window.log.error(prefix, error && error.stack ? error.stack : error);
throw error; throw error;
}; };
const closeAllSessions = async (targetIdentifier: string) =>
window.textsecure.storage.protocol
.getDeviceIds(targetIdentifier)
.then(async deviceIds =>
Promise.all(
deviceIds.map(async deviceId => {
const address = new window.libsignal.SignalProtocolAddress(
targetIdentifier,
deviceId
);
window.log.info(
'resetSession: closing sessions for',
address.toString()
);
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address
);
return sessionCipher.closeOpenSessionForDevice();
})
)
);
const sendToContactPromise = closeAllSessions(identifier) const sendToContactPromise = window.textsecure.storage.protocol
.catch(logError('resetSession/closeAllSessions1 error:')) .archiveAllSessions(identifier)
.catch(logError('resetSession/archiveAllSessions1 error:'))
.then(async () => { .then(async () => {
window.log.info( window.log.info(
'resetSession: finished closing local sessions, now sending to contact' 'resetSession: finished closing local sessions, now sending to contact'
@ -1645,9 +1624,9 @@ export default class MessageSender {
).catch(logError('resetSession/sendToContact error:')); ).catch(logError('resetSession/sendToContact error:'));
}) })
.then(async () => .then(async () =>
closeAllSessions(identifier).catch( window.textsecure.storage.protocol
logError('resetSession/closeAllSessions2 error:') .archiveAllSessions(identifier)
) .catch(logError('resetSession/archiveAllSessions2 error:'))
); );
const myNumber = window.textsecure.storage.user.getNumber(); const myNumber = window.textsecure.storage.user.getNumber();

40
ts/textsecure/Types.d.ts vendored Normal file
View file

@ -0,0 +1,40 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export {
IdentityKeyType,
SignedPreKeyType,
PreKeyType,
UnprocessedType,
UnprocessedUpdateType,
SessionType,
} from '../sql/Interface';
// How the legacy APIs generate these types
export type CompatSignedPreKeyType = {
keyId: number;
keyPair: KeyPairType;
signature: ArrayBuffer;
};
export type CompatPreKeyType = {
keyId: number;
keyPair: KeyPairType;
};
// How we work with these types thereafter
export type KeyPairType = {
privKey: ArrayBuffer;
pubKey: ArrayBuffer;
};
export type OuterSignedPrekeyType = {
confirmed: boolean;
// eslint-disable-next-line camelcase
created_at: number;
keyId: number;
privKey: ArrayBuffer;
pubKey: ArrayBuffer;
};

View file

@ -39,11 +39,13 @@ import {
concatenateBytes, concatenateBytes,
constantTimeEqual, constantTimeEqual,
decryptAesGcm, decryptAesGcm,
deriveSecrets,
encryptCdsDiscoveryRequest, encryptCdsDiscoveryRequest,
getBytes, getBytes,
getRandomValue, getRandomValue,
splitUuids, splitUuids,
} from '../Crypto'; } from '../Crypto';
import { calculateAgreement, generateKeyPair } from '../Curve';
import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch'; import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch';
import { import {
@ -2406,7 +2408,7 @@ export function initialize({
username: string; username: string;
password: string; password: string;
}) { }) {
const keyPair = await window.libsignal.externalCurveAsync.generateKeyPair(); const keyPair = generateKeyPair();
const { privKey, pubKey } = keyPair; const { privKey, pubKey } = keyPair;
// Remove first "key type" byte from public key // Remove first "key type" byte from public key
const slicedPubKey = pubKey.slice(1); const slicedPubKey = pubKey.slice(1);
@ -2476,11 +2478,11 @@ export function initialize({
); );
// Derive key // Derive key
const ephemeralToEphemeral = await window.libsignal.externalCurveAsync.calculateAgreement( const ephemeralToEphemeral = calculateAgreement(
decoded.serverEphemeralPublic, decoded.serverEphemeralPublic,
privKey privKey
); );
const ephemeralToStatic = await window.libsignal.externalCurveAsync.calculateAgreement( const ephemeralToStatic = calculateAgreement(
decoded.serverStaticPublic, decoded.serverStaticPublic,
privKey privKey
); );
@ -2493,10 +2495,7 @@ export function initialize({
decoded.serverEphemeralPublic, decoded.serverEphemeralPublic,
decoded.serverStaticPublic decoded.serverStaticPublic
); );
const [ const [clientKey, serverKey] = await deriveSecrets(
clientKey,
serverKey,
] = await window.libsignal.HKDF.deriveSecrets(
masterSecret, masterSecret,
publicKeys, publicKeys,
new ArrayBuffer(0) new ArrayBuffer(0)

View file

@ -15940,22 +15940,6 @@
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-07-21T18:34:59.251Z" "updated": "2020-07-21T18:34:59.251Z"
}, },
{
"rule": "jQuery-load(",
"path": "ts/LibSignalStore.js",
"line": " await window.ConversationController.load();",
"lineNumber": 811,
"reasonCategory": "falseMatch",
"updated": "2021-02-27T00:48:49.313Z"
},
{
"rule": "jQuery-load(",
"path": "ts/LibSignalStore.ts",
"line": " await window.ConversationController.load();",
"lineNumber": 1190,
"reasonCategory": "falseMatch",
"updated": "2021-02-27T00:48:49.313Z"
},
{ {
"rule": "DOM-innerHTML", "rule": "DOM-innerHTML",
"path": "ts/backbone/views/Lightbox.js", "path": "ts/backbone/views/Lightbox.js",
@ -16925,150 +16909,6 @@
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-02-07T19:52:28.522Z" "updated": "2020-02-07T19:52:28.522Z"
}, },
{
"rule": "jQuery-append(",
"path": "ts/textsecure/ContactsParser.js",
"line": " this.buffer.append(arrayBuffer);",
"lineNumber": 10,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-append(",
"path": "ts/textsecure/ContactsParser.ts",
"line": " this.buffer.append(arrayBuffer);",
"lineNumber": 33,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/Crypto.js",
"line": " const data = window.dcodeIO.ByteBuffer.wrap(encryptedProfileName, 'base64').toArrayBuffer();",
"lineNumber": 157,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/Crypto.js",
"line": " given: window.dcodeIO.ByteBuffer.wrap(padded)",
"lineNumber": 176,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/Crypto.js",
"line": " ? window.dcodeIO.ByteBuffer.wrap(padded)",
"lineNumber": 180,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/Crypto.ts",
"line": " const data = window.dcodeIO.ByteBuffer.wrap(",
"lineNumber": 350,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/Crypto.ts",
"line": " given: window.dcodeIO.ByteBuffer.wrap(padded)",
"lineNumber": 379,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/Crypto.ts",
"line": " ? window.dcodeIO.ByteBuffer.wrap(padded)",
"lineNumber": 383,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.js",
"line": " wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));",
"lineNumber": 44,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.js",
"line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));",
"lineNumber": 46,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.js",
"line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))",
"lineNumber": 48,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.js",
"line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));",
"lineNumber": 51,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.ts",
"line": " wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));",
"lineNumber": 72,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.ts",
"line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));",
"lineNumber": 75,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.ts",
"line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))",
"lineNumber": 78,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.ts",
"line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));",
"lineNumber": 81,
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/WebAPI.js",
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(quote, 'binary', window.dcodeIO.ByteBuffer.LITTLE_ENDIAN);",
"lineNumber": 1302,
"reasonCategory": "falseMatch",
"updated": "2020-09-08T23:07:22.682Z"
},
{
"rule": "jQuery-wrap(",
"path": "ts/textsecure/WebAPI.ts",
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(",
"lineNumber": 2234,
"reasonCategory": "falseMatch",
"updated": "2020-09-08T23:07:22.682Z"
},
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/util/hooks.js", "path": "ts/util/hooks.js",

View file

@ -65,11 +65,14 @@ const excludedFilesRegexps = [
'^ts/textsecure/MessageReceiver.ts', '^ts/textsecure/MessageReceiver.ts',
'^ts/ConversationController.js', '^ts/ConversationController.js',
'^ts/ConversationController.ts', '^ts/ConversationController.ts',
'^ts/SignalProtocolStore.ts',
'^ts/SignalProtocolStore.js',
'^ts/textsecure/[^./]+.ts',
'^ts/textsecure/[^./]+.js',
// Generated files // Generated files
'^js/components.js', '^js/components.js',
'^js/curve/', '^js/curve/',
'^js/libtextsecure.js',
'^js/util_worker.js', '^js/util_worker.js',
'^libtextsecure/components.js', '^libtextsecure/components.js',
'^libtextsecure/test/test.js', '^libtextsecure/test/test.js',
@ -77,9 +80,6 @@ const excludedFilesRegexps = [
'^test/test.js', '^test/test.js',
'^ts/test[^/]*/.+', '^ts/test[^/]*/.+',
// From libsignal-protocol-javascript project
'^libtextsecure/libsignal-protocol.js',
// Copied from dependency // Copied from dependency
'^js/Mp3LameEncoder.min.js', '^js/Mp3LameEncoder.min.js',

View file

@ -2,11 +2,11 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { get, isFinite, isInteger, isString } from 'lodash'; import { get, isFinite, isInteger, isString } from 'lodash';
import { HKDF } from 'libsignal-client';
import { signal } from '../protobuf/compiled'; import { signal } from '../protobuf/compiled';
import { import {
bytesFromString, bytesFromString,
deriveSecrets,
fromEncodedBinaryToArrayBuffer, fromEncodedBinaryToArrayBuffer,
typedArrayToArrayBuffer, typedArrayToArrayBuffer,
} from '../Crypto'; } from '../Crypto';
@ -142,7 +142,8 @@ function toProtobufSession(
proto.localIdentityPublic = new Uint8Array(ourData.identityKeyPublic); proto.localIdentityPublic = new Uint8Array(ourData.identityKeyPublic);
proto.localRegistrationId = ourData.registrationId; proto.localRegistrationId = ourData.registrationId;
proto.previousCounter = getInteger(session, 'currentRatchet.previousCounter'); proto.previousCounter =
getInteger(session, 'currentRatchet.previousCounter') + 1;
proto.remoteIdentityPublic = binaryToUint8Array( proto.remoteIdentityPublic = binaryToUint8Array(
session, session,
'indexInfo.remoteIdentityKey', 'indexInfo.remoteIdentityKey',
@ -150,7 +151,7 @@ function toProtobufSession(
); );
proto.remoteRegistrationId = getInteger(session, 'registrationId'); proto.remoteRegistrationId = getInteger(session, 'registrationId');
proto.rootKey = binaryToUint8Array(session, 'currentRatchet.rootKey', 32); proto.rootKey = binaryToUint8Array(session, 'currentRatchet.rootKey', 32);
proto.sessionVersion = 1; proto.sessionVersion = 3;
// Note: currently unused // Note: currently unused
// proto.needsRefresh = null; // proto.needsRefresh = null;
@ -291,7 +292,7 @@ function toProtobufChain(
const proto = new Chain(); const proto = new Chain();
const protoChainKey = new Chain.ChainKey(); const protoChainKey = new Chain.ChainKey();
protoChainKey.index = getInteger(chain, 'chainKey.counter'); protoChainKey.index = getInteger(chain, 'chainKey.counter') + 1;
if (chain.chainKey?.key !== undefined) { if (chain.chainKey?.key !== undefined) {
protoChainKey.key = binaryToUint8Array(chain, 'chainKey.key', 32); protoChainKey.key = binaryToUint8Array(chain, 'chainKey.key', 32);
} }
@ -300,7 +301,7 @@ function toProtobufChain(
const messageKeys = Object.entries(chain.messageKeys || {}); const messageKeys = Object.entries(chain.messageKeys || {});
proto.messageKeys = messageKeys.map(entry => { proto.messageKeys = messageKeys.map(entry => {
const protoMessageKey = new SessionStructure.Chain.MessageKey(); const protoMessageKey = new SessionStructure.Chain.MessageKey();
protoMessageKey.index = getInteger(entry, '0'); protoMessageKey.index = getInteger(entry, '0') + 1;
const key = binaryToUint8Array(entry, '1', 32); const key = binaryToUint8Array(entry, '1', 32);
const { cipherKey, macKey, iv } = translateMessageKey(key); const { cipherKey, macKey, iv } = translateMessageKey(key);
@ -319,25 +320,6 @@ function toProtobufChain(
const WHISPER_MESSAGE_KEYS = 'WhisperMessageKeys'; const WHISPER_MESSAGE_KEYS = 'WhisperMessageKeys';
function deriveSecrets(
input: ArrayBuffer,
salt: ArrayBuffer,
info: ArrayBuffer
): Array<ArrayBuffer> {
const hkdf = HKDF.new(3);
const output = hkdf.deriveSecrets(
3 * 32,
Buffer.from(input),
Buffer.from(info),
Buffer.from(salt)
);
return [
typedArrayToArrayBuffer(output.slice(0, 32)),
typedArrayToArrayBuffer(output.slice(32, 64)),
typedArrayToArrayBuffer(output.slice(64, 96)),
];
}
function translateMessageKey(key: Uint8Array) { function translateMessageKey(key: Uint8Array) {
const input = key.buffer; const input = key.buffer;
const salt = new ArrayBuffer(32); const salt = new ArrayBuffer(32);

45
ts/window.d.ts vendored
View file

@ -17,17 +17,13 @@ import {
MessageModelCollectionType, MessageModelCollectionType,
MessageAttributesType, MessageAttributesType,
} from './model-types.d'; } from './model-types.d';
import {
LibSignalType,
SignalProtocolAddressClass,
StorageType,
} from './libsignal.d';
import { ContactRecordIdentityState, TextSecureType } from './textsecure.d'; import { ContactRecordIdentityState, TextSecureType } from './textsecure.d';
import { WebAPIConnectType } from './textsecure/WebAPI'; import { WebAPIConnectType } from './textsecure/WebAPI';
import { uploadDebugLogs } from './logging/debuglogs'; import { uploadDebugLogs } from './logging/debuglogs';
import { CallingClass } from './services/calling'; import { CallingClass } from './services/calling';
import * as Groups from './groups'; import * as Groups from './groups';
import * as Crypto from './Crypto'; import * as Crypto from './Crypto';
import * as Curve from './Curve';
import * as RemoteConfig from './RemoteConfig'; import * as RemoteConfig from './RemoteConfig';
import * as OS from './OS'; import * as OS from './OS';
import { getEnvironment } from './environment'; import { getEnvironment } from './environment';
@ -96,7 +92,7 @@ import { Quote } from './components/conversation/Quote';
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview'; import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
import { MIMEType } from './types/MIME'; import { MIMEType } from './types/MIME';
import { ElectronLocaleType } from './util/mapToSupportLocale'; import { ElectronLocaleType } from './util/mapToSupportLocale';
import { SignalProtocolStore } from './LibSignalStore'; import { SignalProtocolStore } from './SignalProtocolStore';
import { StartupQueue } from './util/StartupQueue'; import { StartupQueue } from './util/StartupQueue';
import * as synchronousCrypto from './util/synchronousCrypto'; import * as synchronousCrypto from './util/synchronousCrypto';
import SyncRequest from './textsecure/SyncRequest'; import SyncRequest from './textsecure/SyncRequest';
@ -196,7 +192,6 @@ declare global {
getRegionCodeForNumber: (number: string) => string; getRegionCodeForNumber: (number: string) => string;
format: (number: string, format: PhoneNumberFormat) => string; format: (number: string, format: PhoneNumberFormat) => string;
}; };
libsignal: LibSignalType;
log: { log: {
fatal: LoggerType; fatal: LoggerType;
info: LoggerType; info: LoggerType;
@ -279,14 +274,9 @@ declare global {
stop: () => void; stop: () => void;
}; };
Crypto: typeof Crypto; Crypto: typeof Crypto;
Curve: typeof Curve;
Data: typeof Data; Data: typeof Data;
Groups: typeof Groups; Groups: typeof Groups;
Metadata: {
SecretSessionCipher: typeof SecretSessionCipherClass;
createCertificateValidator: (
trustRoot: ArrayBuffer
) => CertificateValidatorType;
};
RemoteConfig: typeof RemoteConfig; RemoteConfig: typeof RemoteConfig;
Services: { Services: {
calling: CallingClass; calling: CallingClass;
@ -599,35 +589,6 @@ export class CertificateValidatorType {
validate: (cerficate: any, certificateTime: number) => Promise<void>; validate: (cerficate: any, certificateTime: number) => Promise<void>;
} }
export class SecretSessionCipherClass {
constructor(
storage: StorageType,
options?: { messageKeysLimit?: number | boolean }
);
decrypt: (
validator: CertificateValidatorType,
ciphertext: ArrayBuffer,
serverTimestamp: number,
me: any
) => Promise<{
isMe: boolean;
sender: SignalProtocolAddressClass;
senderUuid: SignalProtocolAddressClass;
content: ArrayBuffer;
}>;
getRemoteRegistrationId: (
address: SignalProtocolAddressClass
) => Promise<number>;
closeOpenSessionForDevice: (
address: SignalProtocolAddressClass
) => Promise<void>;
encrypt: (
address: SignalProtocolAddressClass,
senderCertificate: any,
plaintext: ArrayBuffer | Uint8Array
) => Promise<ArrayBuffer>;
}
export class ByteBufferClass { export class ByteBufferClass {
constructor(value?: any, littleEndian?: number); constructor(value?: any, littleEndian?: number);
static wrap: ( static wrap: (

View file

@ -10928,9 +10928,9 @@ levn@~0.3.0:
prelude-ls "~1.1.2" prelude-ls "~1.1.2"
type-check "~0.3.2" type-check "~0.3.2"
"libsignal-client@https://github.com/signalapp/libsignal-client-node.git#f10fbd04eb6efb396eb12c25429761e8785dc9d0": "libsignal-client@https://github.com/signalapp/libsignal-client-node.git#afa8f40eaa218b7fff94278d9b2ab2e13a2ee04b":
version "0.3.0" version "0.3.3"
resolved "https://github.com/signalapp/libsignal-client-node.git#f10fbd04eb6efb396eb12c25429761e8785dc9d0" resolved "https://github.com/signalapp/libsignal-client-node.git#afa8f40eaa218b7fff94278d9b2ab2e13a2ee04b"
dependencies: dependencies:
bindings "^1.5.0" bindings "^1.5.0"