decrypt/encrypt with libsignal-client, remove libsignal-protocol-javascript
This commit is contained in:
parent
37ff4a1df4
commit
86d2a4b5dd
60 changed files with 2508 additions and 28714 deletions
|
@ -7,7 +7,6 @@ release/**
|
|||
# Generated files
|
||||
js/curve/*
|
||||
js/components.js
|
||||
js/libtextsecure.js
|
||||
js/util_worker.js
|
||||
libtextsecure/components.js
|
||||
libtextsecure/test/test.js
|
||||
|
@ -18,7 +17,6 @@ sticker-creator/dist/**
|
|||
js/Mp3LameEncoder.min.js
|
||||
js/WebAudioRecorderMp3.js
|
||||
js/libphonenumber-util.js
|
||||
libtextsecure/libsignal-protocol.js
|
||||
libtextsecure/test/blanket_mocha.js
|
||||
test/blanket_mocha.js
|
||||
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -20,7 +20,6 @@ tsconfig.tsbuildinfo
|
|||
# generated files
|
||||
js/components.js
|
||||
js/util_worker.js
|
||||
js/libtextsecure.js
|
||||
libtextsecure/components.js
|
||||
libtextsecure/test/test.js
|
||||
stylesheets/*.css
|
||||
|
|
|
@ -7,7 +7,6 @@ config/local.json
|
|||
dist/**
|
||||
js/components.js
|
||||
js/util_worker.js
|
||||
js/libtextsecure.js
|
||||
libtextsecure/components.js
|
||||
libtextsecure/test/test.js
|
||||
stylesheets/*.css
|
||||
|
@ -25,7 +24,6 @@ components/**
|
|||
js/curve/**
|
||||
js/Mp3LameEncoder.min.js
|
||||
js/WebAudioRecorderMp3.js
|
||||
libtextsecure/libsignal-protocol.js
|
||||
libtextsecure/test/blanket_mocha.js
|
||||
test/blanket_mocha.js
|
||||
|
||||
|
|
16
Gruntfile.js
16
Gruntfile.js
|
@ -49,22 +49,6 @@ module.exports = grunt => {
|
|||
],
|
||||
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: {
|
||||
src: [
|
||||
'node_modules/jquery/dist/jquery.js',
|
||||
|
|
|
@ -329,7 +329,11 @@
|
|||
<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/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/delivery_receipts.js'></script>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
const { bindActionCreators } = require('redux');
|
||||
const Backbone = require('../../ts/backbone');
|
||||
const Crypto = require('../../ts/Crypto');
|
||||
const Curve = require('../../ts/Curve');
|
||||
const {
|
||||
start: conversationControllerStart,
|
||||
} = require('../../ts/ConversationController');
|
||||
|
@ -430,6 +431,7 @@ exports.setup = (options = {}) => {
|
|||
Backbone,
|
||||
Components,
|
||||
Crypto,
|
||||
Curve,
|
||||
conversationControllerStart,
|
||||
Data,
|
||||
Emojis,
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2016-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global window, textsecure, SignalProtocolStore, libsignal */
|
||||
/* global window, textsecure, SignalProtocolStore */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
|
@ -9,8 +9,4 @@
|
|||
window.textsecure.storage = window.textsecure.storage || {};
|
||||
|
||||
textsecure.storage.protocol = new SignalProtocolStore();
|
||||
|
||||
textsecure.ProvisioningCipher = libsignal.ProvisioningCipher;
|
||||
textsecure.startWorker = libsignal.worker.startWorker;
|
||||
textsecure.stopWorker = libsignal.worker.stopWorker;
|
||||
})();
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
// Copyright 2017-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global libsignal */
|
||||
|
||||
describe('AccountManager', () => {
|
||||
let accountManager;
|
||||
|
||||
|
@ -16,7 +14,7 @@ describe('AccountManager', () => {
|
|||
const DAY = 1000 * 60 * 60 * 24;
|
||||
|
||||
beforeEach(async () => {
|
||||
const identityKey = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
const identityKey = window.Signal.Curve.generateKeyPair();
|
||||
|
||||
originalProtocolStorage = window.textsecure.storage.protocol;
|
||||
window.textsecure.storage.protocol = {
|
||||
|
|
|
@ -64,7 +64,9 @@ describe('GroupBuffer', () => {
|
|||
avatarBuffer.limit = avatarBuffer.offset;
|
||||
avatarBuffer.offset = 0;
|
||||
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',
|
||||
membersE164: ['cereal', 'burn', 'phreak', 'joey'],
|
||||
avatar: { contentType: 'image/jpeg', length: avatarLen },
|
||||
|
@ -92,7 +94,9 @@ describe('GroupBuffer', () => {
|
|||
assert.strictEqual(group.name, 'Hackers');
|
||||
assertEqualArrayBuffers(
|
||||
group.id.toArrayBuffer(),
|
||||
new Uint8Array([1, 3, 3, 7]).buffer
|
||||
window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 3, 3, 7])
|
||||
)
|
||||
);
|
||||
assert.sameMembers(group.membersE164, [
|
||||
'cereal',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2015-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global libsignal, textsecure */
|
||||
/* global textsecure */
|
||||
|
||||
describe('encrypting and decrypting profile data', () => {
|
||||
const NAME_PADDED_LENGTH = 53;
|
||||
|
@ -9,7 +9,7 @@ describe('encrypting and decrypting profile data', () => {
|
|||
it('pads, encrypts, decrypts, and unpads a short string', () => {
|
||||
const name = 'Alice';
|
||||
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
|
||||
const key = libsignal.crypto.getRandomBytes(32);
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto
|
||||
.encryptProfileName(buffer, key)
|
||||
|
@ -29,7 +29,7 @@ describe('encrypting and decrypting profile data', () => {
|
|||
it('handles a given name of the max, 53 characters', () => {
|
||||
const name = '01234567890123456789012345678901234567890123456789123';
|
||||
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
|
||||
const key = libsignal.crypto.getRandomBytes(32);
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto
|
||||
.encryptProfileName(buffer, key)
|
||||
|
@ -49,7 +49,7 @@ describe('encrypting and decrypting profile data', () => {
|
|||
it('handles family/given name of the max, 53 characters', () => {
|
||||
const name = '01234567890123456789\u000001234567890123456789012345678912';
|
||||
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
|
||||
const key = libsignal.crypto.getRandomBytes(32);
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto
|
||||
.encryptProfileName(buffer, key)
|
||||
|
@ -72,7 +72,7 @@ describe('encrypting and decrypting profile data', () => {
|
|||
it('handles a string with family/given name', () => {
|
||||
const name = 'Alice\0Jones';
|
||||
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
|
||||
const key = libsignal.crypto.getRandomBytes(32);
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto
|
||||
.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 key = libsignal.crypto.getRandomBytes(32);
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto
|
||||
.encryptProfileName(name.buffer, key)
|
||||
.then(encrypted => {
|
||||
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
|
||||
return textsecure.crypto
|
||||
.decryptProfileName(encrypted, key)
|
||||
.then(({ given, family }) => {
|
||||
assert.strictEqual(family, null);
|
||||
assert.strictEqual(given.byteLength, 0);
|
||||
assert.strictEqual(
|
||||
dcodeIO.ByteBuffer.wrap(given).toString('utf8'),
|
||||
''
|
||||
);
|
||||
});
|
||||
});
|
||||
const encrypted = await textsecure.crypto.encryptProfileName(name, key);
|
||||
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
|
||||
|
||||
const { given, family } = await textsecure.crypto.decryptProfileName(
|
||||
encrypted,
|
||||
key
|
||||
);
|
||||
assert.strictEqual(family, null);
|
||||
assert.strictEqual(given.byteLength, 0);
|
||||
assert.strictEqual(dcodeIO.ByteBuffer.wrap(given).toString('utf8'), '');
|
||||
});
|
||||
});
|
||||
describe('encrypting and decrypting profile avatars', () => {
|
||||
|
@ -118,7 +113,7 @@ describe('encrypting and decrypting profile data', () => {
|
|||
const buffer = dcodeIO.ByteBuffer.wrap(
|
||||
'This is an avatar'
|
||||
).toArrayBuffer();
|
||||
const key = libsignal.crypto.getRandomBytes(32);
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto.encryptProfile(buffer, key).then(encrypted => {
|
||||
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
|
||||
|
@ -133,8 +128,8 @@ describe('encrypting and decrypting profile data', () => {
|
|||
const buffer = dcodeIO.ByteBuffer.wrap(
|
||||
'This is an avatar'
|
||||
).toArrayBuffer();
|
||||
const key = libsignal.crypto.getRandomBytes(32);
|
||||
const badKey = libsignal.crypto.getRandomBytes(32);
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
const badKey = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto.encryptProfile(buffer, key).then(encrypted => {
|
||||
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2015-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global libsignal, textsecure */
|
||||
/* global textsecure */
|
||||
|
||||
describe('Key generation', function thisNeeded() {
|
||||
const count = 10;
|
||||
|
@ -43,11 +43,10 @@ describe('Key generation', function thisNeeded() {
|
|||
});
|
||||
}
|
||||
|
||||
before(() => {
|
||||
before(async () => {
|
||||
localStorage.clear();
|
||||
return libsignal.KeyHelper.generateIdentityKeyPair().then(keyPair =>
|
||||
textsecure.storage.protocol.put('identityKey', keyPair)
|
||||
);
|
||||
const keyPair = window.Signal.Curve.generateKeyPair();
|
||||
await textsecure.storage.protocol.put('identityKey', keyPair);
|
||||
});
|
||||
|
||||
describe('the first time', () => {
|
||||
|
|
|
@ -6,7 +6,6 @@ function SignalProtocolStore() {
|
|||
}
|
||||
|
||||
SignalProtocolStore.prototype = {
|
||||
Direction: { SENDING: 1, RECEIVING: 2 },
|
||||
VerifiedStatus: {
|
||||
DEFAULT: 0,
|
||||
VERIFIED: 1,
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
<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="../libsignal-protocol.js"></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/unprocessed.js" data-cover></script>
|
||||
|
@ -36,7 +35,6 @@
|
|||
<script type="text/javascript" src="helpers_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="protocol_wrapper_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="websocket-resources_test.js"></script>
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
// Copyright 2015-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global libsignal, textsecure */
|
||||
/* global textsecure */
|
||||
|
||||
describe('MessageReceiver', () => {
|
||||
const { WebSocket } = window;
|
||||
const number = '+19999999999';
|
||||
const uuid = 'AAAAAAAA-BBBB-4CCC-9DDD-EEEEEEEEEEEE';
|
||||
const deviceId = 1;
|
||||
const signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
|
||||
const signalingKey = window.Signal.Crypto.getRandomBytes(32 + 20);
|
||||
|
||||
before(() => {
|
||||
localStorage.clear();
|
||||
|
@ -34,7 +34,7 @@ describe('MessageReceiver', () => {
|
|||
sourceUuid: uuid,
|
||||
sourceDevice: deviceId,
|
||||
timestamp: Date.now(),
|
||||
content: libsignal.crypto.getRandomBytes(200),
|
||||
content: window.Signal.Crypto.getRandomBytes(200),
|
||||
};
|
||||
const body = new textsecure.protobuf.Envelope(attrs).toArrayBuffer();
|
||||
|
||||
|
@ -54,8 +54,8 @@ describe('MessageReceiver', () => {
|
|||
});
|
||||
|
||||
const messageReceiver = new textsecure.MessageReceiver(
|
||||
'oldUsername',
|
||||
'username',
|
||||
'oldUsername.2',
|
||||
'username.2',
|
||||
'password',
|
||||
'signalingKey',
|
||||
{
|
||||
|
@ -77,8 +77,8 @@ describe('MessageReceiver', () => {
|
|||
mockServer = new MockServer('ws://localhost:8081');
|
||||
|
||||
messageReceiver = new textsecure.MessageReceiver(
|
||||
'oldUsername',
|
||||
'username',
|
||||
'oldUsername.3',
|
||||
'username.3',
|
||||
'password',
|
||||
'signalingKey',
|
||||
{
|
||||
|
|
|
@ -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}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,18 +1,18 @@
|
|||
// Copyright 2015-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global libsignal, textsecure, storage, ConversationController */
|
||||
/* global textsecure, storage, ConversationController */
|
||||
|
||||
describe('SignalProtocolStore', () => {
|
||||
const store = textsecure.storage.protocol;
|
||||
const identifier = '+5558675309';
|
||||
const identityKey = {
|
||||
pubKey: libsignal.crypto.getRandomBytes(33),
|
||||
privKey: libsignal.crypto.getRandomBytes(32),
|
||||
pubKey: window.Signal.Crypto.getRandomBytes(33),
|
||||
privKey: window.Signal.Crypto.getRandomBytes(32),
|
||||
};
|
||||
const testKey = {
|
||||
pubKey: libsignal.crypto.getRandomBytes(33),
|
||||
privKey: libsignal.crypto.getRandomBytes(32),
|
||||
pubKey: window.Signal.Crypto.getRandomBytes(33),
|
||||
privKey: window.Signal.Crypto.getRandomBytes(32),
|
||||
};
|
||||
before(async () => {
|
||||
localStorage.clear();
|
||||
|
@ -40,7 +40,7 @@ describe('SignalProtocolStore', () => {
|
|||
assertEqualArrayBuffers(key, testKey.pubKey);
|
||||
});
|
||||
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);
|
||||
|
||||
const trusted = await store.isTrustedIdentity(identifier, newIdentity);
|
||||
|
|
|
@ -30,7 +30,9 @@ describe('WebSocket-Resource', () => {
|
|||
assert.strictEqual(request.path, '/some/path');
|
||||
assertEqualArrayBuffers(
|
||||
request.body.toArrayBuffer(),
|
||||
new Uint8Array([1, 2, 3]).buffer
|
||||
window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
)
|
||||
);
|
||||
request.respond(200, 'OK');
|
||||
},
|
||||
|
@ -45,7 +47,9 @@ describe('WebSocket-Resource', () => {
|
|||
id: requestId,
|
||||
verb: 'PUT',
|
||||
path: '/some/path',
|
||||
body: new Uint8Array([1, 2, 3]).buffer,
|
||||
body: window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
),
|
||||
},
|
||||
})
|
||||
.encode()
|
||||
|
@ -70,7 +74,9 @@ describe('WebSocket-Resource', () => {
|
|||
assert.strictEqual(message.request.path, '/some/path');
|
||||
assertEqualArrayBuffers(
|
||||
message.request.body.toArrayBuffer(),
|
||||
new Uint8Array([1, 2, 3]).buffer
|
||||
window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
)
|
||||
);
|
||||
requestId = message.request.id;
|
||||
},
|
||||
|
@ -82,7 +88,9 @@ describe('WebSocket-Resource', () => {
|
|||
resource.sendRequest({
|
||||
verb: 'PUT',
|
||||
path: '/some/path',
|
||||
body: new Uint8Array([1, 2, 3]).buffer,
|
||||
body: window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([1, 2, 3])
|
||||
),
|
||||
error: done,
|
||||
success(message, status) {
|
||||
assert.strictEqual(message, 'OK');
|
||||
|
|
|
@ -100,7 +100,7 @@
|
|||
"intl-tel-input": "12.1.15",
|
||||
"jquery": "3.5.0",
|
||||
"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",
|
||||
"lodash": "4.17.20",
|
||||
"lru-cache": "6.0.0",
|
||||
|
@ -386,6 +386,7 @@
|
|||
"_locales/**",
|
||||
"protos/*",
|
||||
"js/**",
|
||||
"libtextsecure/**",
|
||||
"ts/**/*.js",
|
||||
"ts/*.js",
|
||||
"stylesheets/*.css",
|
||||
|
|
124
preload.js
124
preload.js
|
@ -11,7 +11,6 @@ let preloadEndTime = 0;
|
|||
try {
|
||||
const electron = require('electron');
|
||||
const semver = require('semver');
|
||||
const client = require('libsignal-client');
|
||||
const _ = require('lodash');
|
||||
const { installGetter, installSetter } = require('./preload_utils');
|
||||
const {
|
||||
|
@ -426,6 +425,7 @@ try {
|
|||
|
||||
window.nodeSetImmediate = setImmediate;
|
||||
|
||||
window.Backbone = require('backbone');
|
||||
window.textsecure = require('./ts/textsecure').default;
|
||||
window.synchronousCrypto = require('./ts/util/synchronousCrypto');
|
||||
|
||||
|
@ -506,7 +506,6 @@ try {
|
|||
window.ReactDOM = require('react-dom');
|
||||
window.moment = require('moment');
|
||||
window.PQueue = require('p-queue').default;
|
||||
window.Backbone = require('backbone');
|
||||
|
||||
const Signal = require('./js/modules/signal');
|
||||
const i18n = require('./js/modules/i18n');
|
||||
|
@ -548,128 +547,9 @@ try {
|
|||
require('./ts/backbone/views/whisper_view');
|
||||
require('./ts/backbone/views/toast_view');
|
||||
require('./ts/views/conversation_view');
|
||||
require('./ts/LibSignalStore');
|
||||
require('./ts/SignalProtocolStore');
|
||||
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
|
||||
window.Signal.Backup = require('./js/modules/backup');
|
||||
window.Signal.Debug = require('./js/modules/debug');
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
<script type="text/javascript" src="../../js/components.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/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>
|
||||
</html>
|
||||
|
|
|
@ -43,7 +43,7 @@ window.localeMessages = ipc.sendSync('locale-data');
|
|||
|
||||
require('../ts/logging/set_up_renderer_logging').initialize();
|
||||
|
||||
require('../ts/LibSignalStore');
|
||||
require('../ts/SignalProtocolStore');
|
||||
|
||||
window.log.info('sticker-creator starting up...');
|
||||
|
||||
|
@ -202,9 +202,9 @@ window.encryptAndUpload = async (
|
|||
const { value: oldUsername } = oldUsernameItem;
|
||||
const { value: password } = passwordItem;
|
||||
|
||||
const packKey = window.libsignal.crypto.getRandomBytes(32);
|
||||
const packKey = window.Signal.Crypto.getRandomBytes(32);
|
||||
const encryptionKey = await deriveStickerPackKey(packKey);
|
||||
const iv = window.libsignal.crypto.getRandomBytes(16);
|
||||
const iv = window.Signal.Crypto.getRandomBytes(16);
|
||||
|
||||
const server = WebAPI.connect({
|
||||
username: username || oldUsername,
|
||||
|
@ -239,7 +239,7 @@ window.encryptAndUpload = async (
|
|||
);
|
||||
const encryptedStickers = await pMap(
|
||||
uniqueStickers,
|
||||
({ imageData }) => encrypt(imageData.buffer, encryptionKey, iv),
|
||||
({ imageData }) => encrypt(imageData, encryptionKey, iv),
|
||||
{
|
||||
concurrency: 3,
|
||||
timeout: 1000 * 60 * 2,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2017-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global Signal, Whisper, textsecure, _, libsignal */
|
||||
/* global Signal, Whisper, textsecure, _ */
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
|
@ -292,7 +292,7 @@ describe('Backup', () => {
|
|||
loadAttachmentData,
|
||||
} = window.Signal.Migrations;
|
||||
|
||||
const staticKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
const staticKeyPair = window.Signal.Curve.generateKeyPair();
|
||||
const attachmentsPattern = normalizePath(
|
||||
path.join(attachmentsPath, '**')
|
||||
);
|
||||
|
@ -303,13 +303,8 @@ describe('Backup', () => {
|
|||
|
||||
const CONVERSATION_ID = 'bdaa7f4f-e9bd-493e-ab0d-8331ad604269';
|
||||
|
||||
const toArrayBuffer = nodeBuffer =>
|
||||
nodeBuffer.buffer.slice(
|
||||
nodeBuffer.byteOffset,
|
||||
nodeBuffer.byteOffset + nodeBuffer.byteLength
|
||||
);
|
||||
|
||||
const getFixture = target => toArrayBuffer(fse.readFileSync(target));
|
||||
const getFixture = target =>
|
||||
window.Signal.Crypto.typedArrayToArrayBuffer(fse.readFileSync(target));
|
||||
|
||||
const FIXTURES = {
|
||||
gif: getFixture('fixtures/giphy-7GFfijngKbeNy.gif'),
|
||||
|
|
|
@ -1,19 +1,48 @@
|
|||
// Copyright 2014-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global Signal, textsecure, libsignal */
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('Crypto', () => {
|
||||
describe('generateRegistrationId', () => {
|
||||
it('generates an integer between 0 and 16383 (inclusive)', () => {
|
||||
for (let i = 0; i < 100; i += 1) {
|
||||
const id = window.Signal.Crypto.generateRegistrationId();
|
||||
assert.isAtLeast(id, 0);
|
||||
assert.isAtMost(id, 16383);
|
||||
assert(Number.isInteger(id));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveSecrets', () => {
|
||||
it('derives key parts via HKDF', () => {
|
||||
const input = window.Signal.Crypto.getRandomBytes(32);
|
||||
const salt = window.Signal.Crypto.getRandomBytes(32);
|
||||
const info = window.Signal.Crypto.bytesFromString('Hello world');
|
||||
const result = window.Signal.Crypto.deriveSecrets(input, salt, info);
|
||||
assert.lengthOf(result, 3);
|
||||
result.forEach(part => {
|
||||
// This is a smoke test; HKDF is tested as part of libsignal-client.
|
||||
assert.instanceOf(part, ArrayBuffer);
|
||||
assert.strictEqual(part.byteLength, 32);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('accessKey/profileKey', () => {
|
||||
it('verification roundtrips', async () => {
|
||||
const profileKey = await Signal.Crypto.getRandomBytes(32);
|
||||
const accessKey = await Signal.Crypto.deriveAccessKey(profileKey);
|
||||
const profileKey = await window.Signal.Crypto.getRandomBytes(32);
|
||||
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);
|
||||
});
|
||||
|
@ -45,11 +74,13 @@ describe('Crypto', () => {
|
|||
|
||||
vectors.forEach((vector, index) => {
|
||||
it(`vector ${index}`, async () => {
|
||||
const gv1 = Signal.Crypto.hexToArrayBuffer(vector.gv1);
|
||||
const gv1 = window.Signal.Crypto.hexToArrayBuffer(vector.gv1);
|
||||
const expectedHex = vector.masterKey;
|
||||
|
||||
const actual = await Signal.Crypto.deriveMasterKeyFromGroupV1(gv1);
|
||||
const actualHex = Signal.Crypto.arrayBufferToHex(actual);
|
||||
const actual = await window.Signal.Crypto.deriveMasterKeyFromGroupV1(
|
||||
gv1
|
||||
);
|
||||
const actualHex = window.Signal.Crypto.arrayBufferToHex(actual);
|
||||
|
||||
assert.strictEqual(actualHex, expectedHex);
|
||||
});
|
||||
|
@ -63,12 +94,21 @@ describe('Crypto', () => {
|
|||
message,
|
||||
'binary'
|
||||
).toArrayBuffer();
|
||||
const key = textsecure.crypto.getRandomBytes(32);
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||
const decrypted = await Signal.Crypto.decryptSymmetric(key, encrypted);
|
||||
const encrypted = await window.Signal.Crypto.encryptSymmetric(
|
||||
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) {
|
||||
throw new Error('The output and input did not match!');
|
||||
}
|
||||
|
@ -80,14 +120,20 @@ describe('Crypto', () => {
|
|||
message,
|
||||
'binary'
|
||||
).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);
|
||||
uintArray[2] += 2;
|
||||
|
||||
try {
|
||||
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
||||
await window.Signal.Crypto.decryptSymmetric(
|
||||
key,
|
||||
window.window.Signal.Crypto.typedArrayToArrayBuffer(uintArray)
|
||||
);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
|
@ -105,14 +151,20 @@ describe('Crypto', () => {
|
|||
message,
|
||||
'binary'
|
||||
).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);
|
||||
uintArray[uintArray.length - 3] += 2;
|
||||
|
||||
try {
|
||||
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
||||
await window.Signal.Crypto.decryptSymmetric(
|
||||
key,
|
||||
window.window.Signal.Crypto.typedArrayToArrayBuffer(uintArray)
|
||||
);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
|
@ -130,14 +182,20 @@ describe('Crypto', () => {
|
|||
message,
|
||||
'binary'
|
||||
).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);
|
||||
uintArray[35] += 9;
|
||||
|
||||
try {
|
||||
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
||||
await window.Signal.Crypto.decryptSymmetric(
|
||||
key,
|
||||
window.window.Signal.Crypto.typedArrayToArrayBuffer(uintArray)
|
||||
);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
|
@ -153,13 +211,13 @@ describe('Crypto', () => {
|
|||
describe('encrypted device name', () => {
|
||||
it('roundtrips', async () => {
|
||||
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,
|
||||
identityKey.pubKey
|
||||
);
|
||||
const decrypted = await Signal.Crypto.decryptDeviceName(
|
||||
const decrypted = await window.Signal.Crypto.decryptDeviceName(
|
||||
encrypted,
|
||||
identityKey.privKey
|
||||
);
|
||||
|
@ -169,15 +227,18 @@ describe('Crypto', () => {
|
|||
|
||||
it('fails if iv is changed', async () => {
|
||||
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,
|
||||
identityKey.pubKey
|
||||
);
|
||||
encrypted.syntheticIv = Signal.Crypto.getRandomBytes(16);
|
||||
encrypted.syntheticIv = window.Signal.Crypto.getRandomBytes(16);
|
||||
try {
|
||||
await Signal.Crypto.decryptDeviceName(encrypted, identityKey.privKey);
|
||||
await window.Signal.Crypto.decryptDeviceName(
|
||||
encrypted,
|
||||
identityKey.privKey
|
||||
);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
|
@ -189,52 +250,174 @@ describe('Crypto', () => {
|
|||
|
||||
describe('attachment encryption', () => {
|
||||
it('roundtrips', async () => {
|
||||
const staticKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
const staticKeyPair = window.Signal.Curve.generateKeyPair();
|
||||
const message = 'this is my message';
|
||||
const plaintext = Signal.Crypto.bytesFromString(message);
|
||||
const plaintext = window.Signal.Crypto.bytesFromString(message);
|
||||
const path =
|
||||
'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa';
|
||||
|
||||
const encrypted = await Signal.Crypto.encryptAttachment(
|
||||
const encrypted = await window.Signal.Crypto.encryptAttachment(
|
||||
staticKeyPair.pubKey.slice(1),
|
||||
path,
|
||||
plaintext
|
||||
);
|
||||
const decrypted = await Signal.Crypto.decryptAttachment(
|
||||
const decrypted = await window.Signal.Crypto.decryptAttachment(
|
||||
staticKeyPair.privKey,
|
||||
path,
|
||||
encrypted
|
||||
);
|
||||
|
||||
const equal = Signal.Crypto.constantTimeEqual(plaintext, decrypted);
|
||||
const equal = window.Signal.Crypto.constantTimeEqual(
|
||||
plaintext,
|
||||
decrypted
|
||||
);
|
||||
if (!equal) {
|
||||
throw new Error('The output and input did not match!');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyHmacSha256', () => {
|
||||
it('rejects if their MAC is too short', async () => {
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
|
||||
const ourMac = await window.Signal.Crypto.hmacSha256(key, plaintext);
|
||||
const theirMac = ourMac.slice(0, -1);
|
||||
let error;
|
||||
try {
|
||||
await window.Signal.Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
ourMac.byteLength
|
||||
);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert.instanceOf(error, Error);
|
||||
assert.strictEqual(error.message, 'Bad MAC length');
|
||||
});
|
||||
|
||||
it('rejects if their MAC is too long', async () => {
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
|
||||
const ourMac = await window.Signal.Crypto.hmacSha256(key, plaintext);
|
||||
const theirMac = window.Signal.Crypto.concatenateBytes(
|
||||
ourMac,
|
||||
new Uint8Array([0xff])
|
||||
);
|
||||
let error;
|
||||
try {
|
||||
await window.Signal.Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
ourMac.byteLength
|
||||
);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert.instanceOf(error, Error);
|
||||
assert.strictEqual(error.message, 'Bad MAC length');
|
||||
});
|
||||
|
||||
it('rejects if our MAC is shorter than the specified length', async () => {
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
|
||||
const ourMac = await window.Signal.Crypto.hmacSha256(key, plaintext);
|
||||
const theirMac = ourMac;
|
||||
let error;
|
||||
try {
|
||||
await window.Signal.Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
ourMac.byteLength + 1
|
||||
);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert.instanceOf(error, Error);
|
||||
assert.strictEqual(error.message, 'Bad MAC length');
|
||||
});
|
||||
|
||||
it("rejects if the MACs don't match", async () => {
|
||||
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
|
||||
const ourKey = window.Signal.Crypto.getRandomBytes(32);
|
||||
const ourMac = await window.Signal.Crypto.hmacSha256(ourKey, plaintext);
|
||||
const theirKey = window.Signal.Crypto.getRandomBytes(32);
|
||||
const theirMac = await window.Signal.Crypto.hmacSha256(
|
||||
theirKey,
|
||||
plaintext
|
||||
);
|
||||
let error;
|
||||
try {
|
||||
await window.Signal.Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
ourKey,
|
||||
theirMac,
|
||||
ourMac.byteLength
|
||||
);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert.instanceOf(error, Error);
|
||||
assert.strictEqual(error.message, 'Bad MAC');
|
||||
});
|
||||
|
||||
it('resolves with undefined if the MACs match exactly', async () => {
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
|
||||
const theirMac = await window.Signal.Crypto.hmacSha256(key, plaintext);
|
||||
const result = await window.Signal.Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
theirMac.byteLength
|
||||
);
|
||||
assert.isUndefined(result);
|
||||
});
|
||||
|
||||
it('resolves with undefined if the first `length` bytes of the MACs match', async () => {
|
||||
const key = window.Signal.Crypto.getRandomBytes(32);
|
||||
const plaintext = window.Signal.Crypto.bytesFromString('Hello world');
|
||||
const theirMac = (
|
||||
await window.Signal.Crypto.hmacSha256(key, plaintext)
|
||||
).slice(0, -5);
|
||||
const result = await window.Signal.Crypto.verifyHmacSha256(
|
||||
plaintext,
|
||||
key,
|
||||
theirMac,
|
||||
theirMac.byteLength
|
||||
);
|
||||
assert.isUndefined(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uuidToArrayBuffer', () => {
|
||||
const { uuidToArrayBuffer } = Signal.Crypto;
|
||||
const { uuidToArrayBuffer } = window.Signal.Crypto;
|
||||
|
||||
it('converts valid UUIDs to ArrayBuffers', () => {
|
||||
const expectedResult = new Uint8Array([
|
||||
0x22,
|
||||
0x6e,
|
||||
0x44,
|
||||
0x02,
|
||||
0x7f,
|
||||
0xfc,
|
||||
0x45,
|
||||
0x43,
|
||||
0x85,
|
||||
0xc9,
|
||||
0x46,
|
||||
0x22,
|
||||
0xc5,
|
||||
0x0a,
|
||||
0x5b,
|
||||
0x14,
|
||||
]).buffer;
|
||||
const expectedResult = window.window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([
|
||||
0x22,
|
||||
0x6e,
|
||||
0x44,
|
||||
0x02,
|
||||
0x7f,
|
||||
0xfc,
|
||||
0x45,
|
||||
0x43,
|
||||
0x85,
|
||||
0xc9,
|
||||
0x46,
|
||||
0x22,
|
||||
0xc5,
|
||||
0x0a,
|
||||
0x5b,
|
||||
0x14,
|
||||
])
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
uuidToArrayBuffer('226e4402-7ffc-4543-85c9-4622c50a5b14'),
|
||||
|
@ -261,27 +444,29 @@ describe('Crypto', () => {
|
|||
});
|
||||
|
||||
describe('arrayBufferToUuid', () => {
|
||||
const { arrayBufferToUuid } = Signal.Crypto;
|
||||
const { arrayBufferToUuid } = window.Signal.Crypto;
|
||||
|
||||
it('converts valid ArrayBuffers to UUID strings', () => {
|
||||
const buf = new Uint8Array([
|
||||
0x22,
|
||||
0x6e,
|
||||
0x44,
|
||||
0x02,
|
||||
0x7f,
|
||||
0xfc,
|
||||
0x45,
|
||||
0x43,
|
||||
0x85,
|
||||
0xc9,
|
||||
0x46,
|
||||
0x22,
|
||||
0xc5,
|
||||
0x0a,
|
||||
0x5b,
|
||||
0x14,
|
||||
]).buffer;
|
||||
const buf = window.window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
new Uint8Array([
|
||||
0x22,
|
||||
0x6e,
|
||||
0x44,
|
||||
0x02,
|
||||
0x7f,
|
||||
0xfc,
|
||||
0x45,
|
||||
0x43,
|
||||
0x85,
|
||||
0xc9,
|
||||
0x46,
|
||||
0x22,
|
||||
0xc5,
|
||||
0x0a,
|
||||
0x5b,
|
||||
0x14,
|
||||
])
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
arrayBufferToUuid(buf),
|
||||
|
@ -295,9 +480,19 @@ describe('Crypto', () => {
|
|||
|
||||
it('returns undefined if passed the wrong number of bytes', () => {
|
||||
assert.isUndefined(arrayBufferToUuid(new ArrayBuffer(0)));
|
||||
assert.isUndefined(arrayBufferToUuid(new Uint8Array([0x22]).buffer));
|
||||
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))
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -339,7 +339,12 @@
|
|||
|
||||
<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/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/models/blockedNumbers.js" data-cover></script>
|
||||
|
|
|
@ -1,23 +1,20 @@
|
|||
// Copyright 2017-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global ConversationController, libsignal, SignalProtocolStore, Whisper */
|
||||
/* global ConversationController, SignalProtocolStore, Whisper */
|
||||
|
||||
describe('KeyChangeListener', () => {
|
||||
const phoneNumberWithKeyChange = '+13016886524'; // nsa
|
||||
const address = new libsignal.SignalProtocolAddress(
|
||||
phoneNumberWithKeyChange,
|
||||
1
|
||||
);
|
||||
const oldKey = libsignal.crypto.getRandomBytes(33);
|
||||
const newKey = libsignal.crypto.getRandomBytes(33);
|
||||
const addressString = `${phoneNumberWithKeyChange}.1`;
|
||||
const oldKey = window.Signal.Crypto.getRandomBytes(33);
|
||||
const newKey = window.Signal.Crypto.getRandomBytes(33);
|
||||
let store;
|
||||
|
||||
beforeEach(async () => {
|
||||
store = new SignalProtocolStore();
|
||||
await store.hydrateCaches();
|
||||
Whisper.KeyChangeListener.init(store);
|
||||
return store.saveIdentity(address.toString(), oldKey);
|
||||
return store.saveIdentity(addressString, oldKey);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -31,9 +28,7 @@ describe('KeyChangeListener', () => {
|
|||
id: phoneNumberWithKeyChange,
|
||||
type: 'private',
|
||||
});
|
||||
await window.Signal.Data.saveConversation(convo.attributes, {
|
||||
Conversation: Whisper.Conversation,
|
||||
});
|
||||
await window.Signal.Data.saveConversation(convo.attributes);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -49,11 +44,11 @@ describe('KeyChangeListener', () => {
|
|||
it('generates a key change notice in the private conversation with this contact', done => {
|
||||
const original = convo.addKeyChange;
|
||||
convo.addKeyChange = keyChangedId => {
|
||||
assert.equal(address.getName(), keyChangedId);
|
||||
assert.equal(phoneNumberWithKeyChange, keyChangedId);
|
||||
convo.addKeyChange = original;
|
||||
done();
|
||||
};
|
||||
store.saveIdentity(address.toString(), newKey);
|
||||
store.saveIdentity(addressString, newKey);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -70,12 +65,8 @@ describe('KeyChangeListener', () => {
|
|||
type: 'group',
|
||||
members: [convo.id],
|
||||
});
|
||||
await window.Signal.Data.saveConversation(convo.attributes, {
|
||||
Conversation: Whisper.Conversation,
|
||||
});
|
||||
await window.Signal.Data.saveConversation(groupConvo.attributes, {
|
||||
Conversation: Whisper.Conversation,
|
||||
});
|
||||
await window.Signal.Data.saveConversation(convo.attributes);
|
||||
await window.Signal.Data.saveConversation(groupConvo.attributes);
|
||||
});
|
||||
after(async () => {
|
||||
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 => {
|
||||
const original = groupConvo.addKeyChange;
|
||||
groupConvo.addKeyChange = keyChangedId => {
|
||||
assert.equal(address.getName(), keyChangedId);
|
||||
assert.equal(phoneNumberWithKeyChange, keyChangedId);
|
||||
groupConvo.addKeyChange = original;
|
||||
done();
|
||||
};
|
||||
|
||||
store.saveIdentity(address.toString(), newKey);
|
||||
store.saveIdentity(addressString, newKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
131
ts/Crypto.ts
131
ts/Crypto.ts
|
@ -3,6 +3,8 @@
|
|||
|
||||
import pProps from 'p-props';
|
||||
import { chunk } from 'lodash';
|
||||
import { HKDF } from 'libsignal-client';
|
||||
import { calculateAgreement, generateKeyPair } from './Curve';
|
||||
|
||||
import {
|
||||
CipherType,
|
||||
|
@ -13,6 +15,14 @@ import {
|
|||
sign,
|
||||
} 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 {
|
||||
const ab = new ArrayBuffer(typedArray.length);
|
||||
// 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 info = bytesFromString('Sticker Pack');
|
||||
|
||||
const [part1, part2] = await window.libsignal.HKDF.deriveSecrets(
|
||||
packKey,
|
||||
salt,
|
||||
info
|
||||
);
|
||||
const [part1, part2] = await deriveSecrets(packKey, salt, info);
|
||||
|
||||
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(
|
||||
groupV1Id: ArrayBuffer
|
||||
): Promise<ArrayBuffer> {
|
||||
const salt = getZeroes(32);
|
||||
const info = bytesFromString('GV2 Migration');
|
||||
|
||||
const [part1] = await window.libsignal.HKDF.deriveSecrets(
|
||||
groupV1Id,
|
||||
salt,
|
||||
info
|
||||
);
|
||||
const [part1] = await deriveSecrets(groupV1Id, salt, info);
|
||||
|
||||
return part1;
|
||||
}
|
||||
|
@ -99,8 +120,8 @@ export async function encryptDeviceName(
|
|||
identityPublic: ArrayBuffer
|
||||
): Promise<Record<string, ArrayBuffer>> {
|
||||
const plaintext = bytesFromString(deviceName);
|
||||
const ephemeralKeyPair = await window.libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
const masterSecret = await window.libsignal.Curve.async.calculateAgreement(
|
||||
const ephemeralKeyPair = generateKeyPair();
|
||||
const masterSecret = calculateAgreement(
|
||||
identityPublic,
|
||||
ephemeralKeyPair.privKey
|
||||
);
|
||||
|
@ -133,10 +154,7 @@ export async function decryptDeviceName(
|
|||
},
|
||||
identityPrivate: ArrayBuffer
|
||||
): Promise<string> {
|
||||
const masterSecret = await window.libsignal.Curve.async.calculateAgreement(
|
||||
ephemeralPublic,
|
||||
identityPrivate
|
||||
);
|
||||
const masterSecret = calculateAgreement(ephemeralPublic, identityPrivate);
|
||||
|
||||
const key2 = await hmacSha256(masterSecret, bytesFromString('cipher'));
|
||||
const cipherKey = await hmacSha256(key2, syntheticIv);
|
||||
|
@ -187,8 +205,8 @@ export async function encryptFile(
|
|||
uniqueId: ArrayBuffer,
|
||||
plaintext: ArrayBuffer
|
||||
): Promise<ArrayBuffer> {
|
||||
const ephemeralKeyPair = await window.libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
const agreement = await window.libsignal.Curve.async.calculateAgreement(
|
||||
const ephemeralKeyPair = generateKeyPair();
|
||||
const agreement = calculateAgreement(
|
||||
staticPublicKey,
|
||||
ephemeralKeyPair.privKey
|
||||
);
|
||||
|
@ -206,10 +224,7 @@ export async function decryptFile(
|
|||
): Promise<ArrayBuffer> {
|
||||
const ephemeralPublicKey = getFirstBytes(data, PUB_KEY_LENGTH);
|
||||
const ciphertext = getBytes(data, PUB_KEY_LENGTH, data.byteLength);
|
||||
const agreement = await window.libsignal.Curve.async.calculateAgreement(
|
||||
ephemeralPublicKey,
|
||||
staticPrivateKey
|
||||
);
|
||||
const agreement = calculateAgreement(ephemeralPublicKey, staticPrivateKey);
|
||||
|
||||
const key = await hmacSha256(agreement, uniqueId);
|
||||
|
||||
|
@ -275,14 +290,14 @@ export async function encryptSymmetric(
|
|||
const cipherKey = await hmacSha256(key, nonce);
|
||||
const macKey = await hmacSha256(key, cipherKey);
|
||||
|
||||
const cipherText = await _encryptAes256CbcPkcsPadding(
|
||||
const ciphertext = await encryptAes256CbcPkcsPadding(
|
||||
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(
|
||||
|
@ -292,7 +307,7 @@ export async function decryptSymmetric(
|
|||
const iv = getZeroes(IV_LENGTH);
|
||||
|
||||
const nonce = getFirstBytes(data, NONCE_LENGTH);
|
||||
const cipherText = getBytes(
|
||||
const ciphertext = getBytes(
|
||||
data,
|
||||
NONCE_LENGTH,
|
||||
data.byteLength - NONCE_LENGTH - MAC_LENGTH
|
||||
|
@ -303,7 +318,7 @@ export async function decryptSymmetric(
|
|||
const macKey = await hmacSha256(key, cipherKey);
|
||||
|
||||
const ourMac = getFirstBytes(
|
||||
await hmacSha256(macKey, cipherText),
|
||||
await hmacSha256(macKey, ciphertext),
|
||||
MAC_LENGTH
|
||||
);
|
||||
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(
|
||||
|
@ -343,10 +358,37 @@ export async function hmacSha256(
|
|||
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,
|
||||
iv: ArrayBuffer,
|
||||
plaintext: ArrayBuffer
|
||||
theirMac: 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> {
|
||||
const algorithm = {
|
||||
name: 'AES-CBC',
|
||||
|
@ -369,10 +411,10 @@ export async function _encryptAes256CbcPkcsPadding(
|
|||
return window.crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
|
||||
}
|
||||
|
||||
export async function _decryptAes256CbcPkcsPadding(
|
||||
export async function decryptAes256CbcPkcsPadding(
|
||||
key: ArrayBuffer,
|
||||
iv: ArrayBuffer,
|
||||
plaintext: ArrayBuffer
|
||||
ciphertext: ArrayBuffer,
|
||||
iv: ArrayBuffer
|
||||
): Promise<ArrayBuffer> {
|
||||
const algorithm = {
|
||||
name: 'AES-CBC',
|
||||
|
@ -392,7 +434,7 @@ export async function _decryptAes256CbcPkcsPadding(
|
|||
['decrypt']
|
||||
);
|
||||
|
||||
return window.crypto.subtle.decrypt(algorithm, cryptoKey, plaintext);
|
||||
return window.crypto.subtle.decrypt(algorithm, cryptoKey, ciphertext);
|
||||
}
|
||||
|
||||
export async function encryptAesCtr(
|
||||
|
@ -531,7 +573,7 @@ export function getViewOfArrayBuffer(
|
|||
const source = new Uint8Array(buffer);
|
||||
const result = source.slice(start, finish);
|
||||
|
||||
return result.buffer;
|
||||
return window.Signal.Crypto.typedArrayToArrayBuffer(result);
|
||||
}
|
||||
|
||||
export function concatenateBytes(
|
||||
|
@ -627,7 +669,10 @@ export async function encryptCdsDiscoveryRequest(
|
|||
// Long.fromString handles numbers with or without a leading '+'
|
||||
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 commitment = sha256(queryDataPlaintext);
|
||||
const iv = getRandomBytes(12);
|
||||
|
@ -680,9 +725,11 @@ export function uuidToArrayBuffer(uuid: string): ArrayBuffer {
|
|||
return new ArrayBuffer(0);
|
||||
}
|
||||
|
||||
return Uint8Array.from(
|
||||
chunk(uuid.replace(/-/g, ''), 2).map(pair => parseInt(pair.join(''), 16))
|
||||
).buffer;
|
||||
return typedArrayToArrayBuffer(
|
||||
Uint8Array.from(
|
||||
chunk(uuid.replace(/-/g, ''), 2).map(pair => parseInt(pair.join(''), 16))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function arrayBufferToUuid(
|
||||
|
|
176
ts/Curve.ts
Normal file
176
ts/Curve.ts
Normal 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
157
ts/LibSignalStores.ts
Normal 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
229
ts/libsignal.d.ts
vendored
|
@ -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>;
|
||||
}
|
|
@ -11,6 +11,8 @@ import * as path from 'path';
|
|||
import pino from 'pino';
|
||||
import { createStream } from 'rotating-file-stream';
|
||||
|
||||
import { initLogger, LogLevel as SignalClientLogLevel } from 'libsignal-client';
|
||||
|
||||
import { uploadDebugLogs } from './debuglogs';
|
||||
import { redactAll } from '../../js/modules/privacy';
|
||||
import {
|
||||
|
@ -178,3 +180,36 @@ window.addEventListener('unhandledrejection', rejectionEvent => {
|
|||
error && error.stack ? error.stack : JSON.stringify(error);
|
||||
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})`);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -57,7 +57,7 @@ import {
|
|||
import {
|
||||
SenderCertificateMode,
|
||||
SerializedCertificateType,
|
||||
} from '../metadata/SecretSessionCipher';
|
||||
} from '../textsecure/OutgoingMessage';
|
||||
import { senderCertificateService } from '../services/senderCertificate';
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
@ -2056,8 +2056,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
keyChange = await window.textsecure.storage.protocol.processVerifiedMessage(
|
||||
this.id,
|
||||
verified,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
options.key!
|
||||
options.key || undefined
|
||||
);
|
||||
} else {
|
||||
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()) {
|
||||
throw new Error(
|
||||
'You cannot set a group conversation as trusted. ' +
|
||||
|
@ -4523,16 +4522,9 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
if (changed) {
|
||||
// save identity will close all sessions except for .1, so we
|
||||
// must close that one manually.
|
||||
const address = new window.libsignal.SignalProtocolAddress(
|
||||
identifier,
|
||||
1
|
||||
await window.textsecure.storage.protocol.archiveSession(
|
||||
`${identifier}.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');
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
SenderCertificateMode,
|
||||
serializedCertificateSchema,
|
||||
SerializedCertificateType,
|
||||
} from '../metadata/SecretSessionCipher';
|
||||
} from '../textsecure/OutgoingMessage';
|
||||
import { SenderCertificateClass } from '../textsecure';
|
||||
import { base64ToArrayBuffer } from '../Crypto';
|
||||
import { assert } from '../util/assert';
|
||||
|
|
|
@ -740,7 +740,12 @@ export async function mergeContactRecord(
|
|||
const verified = await conversation.safeGetVerified();
|
||||
const storageServiceVerified = contactRecord.identityState || 0;
|
||||
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;
|
||||
|
||||
switch (storageServiceVerified) {
|
||||
|
|
|
@ -53,6 +53,7 @@ import {
|
|||
StickerPackType,
|
||||
StickerType,
|
||||
UnprocessedType,
|
||||
UnprocessedUpdateType,
|
||||
} from './Interface';
|
||||
import Server from './Server';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
@ -1304,11 +1305,14 @@ async function saveUnprocesseds(
|
|||
async function updateUnprocessedAttempts(id: string, attempts: number) {
|
||||
await channels.updateUnprocessedAttempts(id, attempts);
|
||||
}
|
||||
async function updateUnprocessedWithData(id: string, data: UnprocessedType) {
|
||||
async function updateUnprocessedWithData(
|
||||
id: string,
|
||||
data: UnprocessedUpdateType
|
||||
) {
|
||||
await channels.updateUnprocessedWithData(id, data);
|
||||
}
|
||||
async function updateUnprocessedsWithData(
|
||||
array: Array<{ id: string; data: UnprocessedType }>
|
||||
array: Array<{ id: string; data: UnprocessedUpdateType }>
|
||||
) {
|
||||
await channels.updateUnprocessedsWithData(array);
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ export type SessionType = {
|
|||
conversationId: string;
|
||||
deviceId: number;
|
||||
record: string;
|
||||
version?: number;
|
||||
};
|
||||
export type SignedPreKeyType = {
|
||||
confirmed: boolean;
|
||||
|
@ -128,6 +129,14 @@ export type UnprocessedType = {
|
|||
decrypted?: string;
|
||||
};
|
||||
|
||||
export type UnprocessedUpdateType = {
|
||||
source?: string;
|
||||
sourceUuid?: string;
|
||||
sourceDevice?: string;
|
||||
serverTimestamp?: number;
|
||||
decrypted?: string;
|
||||
};
|
||||
|
||||
export type DataInterface = {
|
||||
close: () => Promise<void>;
|
||||
removeDB: () => Promise<void>;
|
||||
|
@ -210,10 +219,10 @@ export type DataInterface = {
|
|||
updateUnprocessedAttempts: (id: string, attempts: number) => Promise<void>;
|
||||
updateUnprocessedWithData: (
|
||||
id: string,
|
||||
data: UnprocessedType
|
||||
data: UnprocessedUpdateType
|
||||
) => Promise<void>;
|
||||
updateUnprocessedsWithData: (
|
||||
array: Array<{ id: string; data: UnprocessedType }>
|
||||
array: Array<{ id: string; data: UnprocessedUpdateType }>
|
||||
) => Promise<void>;
|
||||
getUnprocessedById: (id: string) => Promise<UnprocessedType | undefined>;
|
||||
saveUnprocesseds: (
|
||||
|
|
|
@ -53,6 +53,7 @@ import {
|
|||
StickerPackType,
|
||||
StickerType,
|
||||
UnprocessedType,
|
||||
UnprocessedUpdateType,
|
||||
} from './Interface';
|
||||
|
||||
declare global {
|
||||
|
@ -3517,7 +3518,7 @@ async function updateUnprocessedAttempts(
|
|||
}
|
||||
async function updateUnprocessedWithData(
|
||||
id: string,
|
||||
data: UnprocessedType
|
||||
data: UnprocessedUpdateType
|
||||
): Promise<void> {
|
||||
const db = getInstance();
|
||||
const { source, sourceUuid, sourceDevice, serverTimestamp, decrypted } = data;
|
||||
|
@ -3543,7 +3544,7 @@ async function updateUnprocessedWithData(
|
|||
});
|
||||
}
|
||||
async function updateUnprocessedsWithData(
|
||||
arrayOfUnprocessed: Array<{ id: string; data: UnprocessedType }>
|
||||
arrayOfUnprocessed: Array<{ id: string; data: UnprocessedUpdateType }>
|
||||
): Promise<void> {
|
||||
const db = getInstance();
|
||||
|
||||
|
|
|
@ -83,17 +83,17 @@ describe('sessionTranslation', () => {
|
|||
|
||||
const expected = {
|
||||
currentSession: {
|
||||
sessionVersion: 1,
|
||||
sessionVersion: 3,
|
||||
localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444',
|
||||
remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA',
|
||||
rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=',
|
||||
previousCounter: 2,
|
||||
previousCounter: 3,
|
||||
senderChain: {
|
||||
senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x',
|
||||
senderRatchetKeyPrivate:
|
||||
'5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=',
|
||||
chainKey: {
|
||||
index: -1,
|
||||
index: 0,
|
||||
key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=',
|
||||
},
|
||||
},
|
||||
|
@ -101,18 +101,18 @@ describe('sessionTranslation', () => {
|
|||
{
|
||||
senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz',
|
||||
chainKey: {
|
||||
index: 5,
|
||||
index: 6,
|
||||
key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=',
|
||||
},
|
||||
messageKeys: [
|
||||
{
|
||||
index: 0,
|
||||
index: 1,
|
||||
cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=',
|
||||
iv: 'TcRanSxZVWbuIq0xDRGnEw==',
|
||||
macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=',
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
index: 5,
|
||||
cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=',
|
||||
iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==',
|
||||
macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=',
|
||||
|
@ -320,17 +320,17 @@ describe('sessionTranslation', () => {
|
|||
|
||||
const expected = {
|
||||
currentSession: {
|
||||
sessionVersion: 1,
|
||||
sessionVersion: 3,
|
||||
localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444',
|
||||
remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA',
|
||||
rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=',
|
||||
previousCounter: 2,
|
||||
previousCounter: 3,
|
||||
senderChain: {
|
||||
senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x',
|
||||
senderRatchetKeyPrivate:
|
||||
'5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=',
|
||||
chainKey: {
|
||||
index: -1,
|
||||
index: 0,
|
||||
key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=',
|
||||
},
|
||||
},
|
||||
|
@ -338,18 +338,18 @@ describe('sessionTranslation', () => {
|
|||
{
|
||||
senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz',
|
||||
chainKey: {
|
||||
index: 5,
|
||||
index: 6,
|
||||
key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=',
|
||||
},
|
||||
messageKeys: [
|
||||
{
|
||||
index: 0,
|
||||
index: 1,
|
||||
cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=',
|
||||
iv: 'TcRanSxZVWbuIq0xDRGnEw==',
|
||||
macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=',
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
index: 5,
|
||||
cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=',
|
||||
iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==',
|
||||
macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=',
|
||||
|
@ -359,11 +359,11 @@ describe('sessionTranslation', () => {
|
|||
{
|
||||
senderRatchetKey: 'BTpb20+IlnBkryDC2ecQT96Hd3t9/Qh3ljnA3509kxRa',
|
||||
chainKey: {
|
||||
index: 1,
|
||||
index: 2,
|
||||
},
|
||||
messageKeys: [
|
||||
{
|
||||
index: 0,
|
||||
index: 1,
|
||||
cipherKey: 'aAbSz5jOagUTgQKo3aqExcl8hyZANrY+HvrLc/OgoQI=',
|
||||
iv: 'JcyLzw0fL67Kd4tfGJ2OUQ==',
|
||||
macKey: 'dt+RXeaeIx+ASrKSk7D4guwTE1IUYl3LiLG9aI4sZm8=',
|
||||
|
@ -373,29 +373,29 @@ describe('sessionTranslation', () => {
|
|||
{
|
||||
senderRatchetKey: 'Bd5nlMVr6YMBE5eh//tOWMgoOQakkneYri/YuVJpi0pJ',
|
||||
chainKey: {
|
||||
index: 11,
|
||||
index: 12,
|
||||
},
|
||||
messageKeys: [
|
||||
{
|
||||
index: 0,
|
||||
index: 1,
|
||||
cipherKey: 'pjcY/7MoRGtGHwNN/E8KqoKCx/5mdKp0VCmrmkBAj+M=',
|
||||
iv: 'eBpAEoDj94NsI0vsf+4Hrw==',
|
||||
macKey: 'P7Jz2KkOXC7B0mLkz7JaU/d0vdaYZjAfuKJ86xXB19U=',
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
index: 3,
|
||||
cipherKey: 'EGDj0sc/1TMtSycYDCrpZdl6UCzCzDuMwlAvVVAs2OQ=',
|
||||
iv: 'A+1OA9M2Z8gGlARtA231RA==',
|
||||
macKey: 'oQ/PQxJDD52qrkShSy6hD3fASEfhWnlmY3qsSPuOY/o=',
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
index: 4,
|
||||
cipherKey: 'WM3UUILGdECXjO8jZbBVYrPAnzRM8RdiU+PSAyHUT5U=',
|
||||
iv: 'CWuQIuIyGqApA6MQgnDR5Q==',
|
||||
macKey: 'hg+/xrOKFzn2eK1BnJ5C+ERsFgaWAOaBxQTc4q3b/g8=',
|
||||
},
|
||||
{
|
||||
index: 9,
|
||||
index: 10,
|
||||
cipherKey: 'T0cBaGAseFz+s2njVr4sqbFf1pUH5PoPvdMBoizIT+Y=',
|
||||
iv: 'hkT2kqgqhlORAjBI7ZDsig==',
|
||||
macKey: 'uE/Dd4WSQWkYNRgolcQtOd+HpaHP5wGogMzErkZj+AQ=',
|
||||
|
@ -405,29 +405,29 @@ describe('sessionTranslation', () => {
|
|||
{
|
||||
senderRatchetKey: 'BYSxQO1OIs0ZSFN7JI/vF5Rb0VwaKjs+UAAfDkhOYfkp',
|
||||
chainKey: {
|
||||
index: 5,
|
||||
index: 6,
|
||||
},
|
||||
messageKeys: [
|
||||
{
|
||||
index: 0,
|
||||
index: 1,
|
||||
cipherKey: 'ni6XhRCoLFud2Zk1zoel4he8znDG/t+TWVBASO35GlQ=',
|
||||
iv: 'rKy/sxLmQ4j2DSxbDZTO5A==',
|
||||
macKey: 'MKxs29AmNOnp6zZOsIbrmSqcVXYJL01kuvIaqwjRNvQ=',
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
index: 3,
|
||||
cipherKey: 'Pp7GOD72vfjvb3qx7qm1YVoZKPqnyXC2uqCt89ZA/yc=',
|
||||
iv: 'NuDf5iM0lD/o0YzjHZo4mA==',
|
||||
macKey: 'JkBZiaxmwFr1xh/zzTQE6mlUIVJmSIrqSIQVlaoTz7M=',
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
index: 4,
|
||||
cipherKey: 'zORWRvJEUe2F4UnBwe2YRqPS4GzUFE1lWptcqMzWf2U=',
|
||||
iv: 'Og7jF9JJhiLtPD8W2OgTnw==',
|
||||
macKey: 'Lxbcl9fL9x5Javtdz7tOV7Bbr8ar3rWxSIsi1Focv9w=',
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
index: 6,
|
||||
cipherKey: 'T/TZNw04+ZfB0s2ltOT9qbzRPnCFn7VvxqHHAvORFx0=',
|
||||
iv: 'DpOAK77ErIr2QFTsRnfOew==',
|
||||
macKey: 'k/fxafepBiA0dQOTpohL+EKm2+1jpFwRigVWt02U/Jg=',
|
||||
|
@ -437,23 +437,23 @@ describe('sessionTranslation', () => {
|
|||
{
|
||||
senderRatchetKey: 'BbXSFD/IoivRUvfnPzOaRLqDXEAwi4YEristfwiOj3IJ',
|
||||
chainKey: {
|
||||
index: 2,
|
||||
index: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
senderRatchetKey: 'BRRAnr1NhizgCPPzmYV9qGBpvwCpSQH0Rx+UOtl78wUg',
|
||||
chainKey: {
|
||||
index: 0,
|
||||
index: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
senderRatchetKey: 'BZvOKPA+kXiCg8TIP/52fu1reCDirC7wb5nyRGce3y4N',
|
||||
chainKey: {
|
||||
index: 6,
|
||||
index: 7,
|
||||
},
|
||||
messageKeys: [
|
||||
{
|
||||
index: 4,
|
||||
index: 5,
|
||||
cipherKey: 'PB44plPzHam/o2LZnyjo8HLRuAvp3uE6ixO5+GUCUsA=',
|
||||
iv: 'JBbgRb10X/dDsn0GKg69dA==',
|
||||
macKey: 'jKV1Rmlb0HATZHndLDIMONPgOXqT3kwE1QEstxXVe+o=',
|
||||
|
@ -463,23 +463,23 @@ describe('sessionTranslation', () => {
|
|||
{
|
||||
senderRatchetKey: 'Ba9q9bHjMHfbUNDCU8+0O7cmEcIluq+wk3/d2f7q+ThG',
|
||||
chainKey: {
|
||||
index: 3,
|
||||
index: 4,
|
||||
},
|
||||
messageKeys: [
|
||||
{
|
||||
index: 0,
|
||||
index: 1,
|
||||
cipherKey: '4buOJSqRFIpWwo4pXYwQTCTxas4+amBLpZ/CuEWXbPg=',
|
||||
iv: '9uD8ECO/fxtK28OvlCFXuQ==',
|
||||
macKey: 'LI0ZSdX7k+cd5bTgs6XEYYIWY+2cxhWI97vAGFpoZIc=',
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
index: 2,
|
||||
cipherKey: 'oNbFxcy2eebUQhoD+NLf12fgkXzhn4EU0Pgqn1bVKOs=',
|
||||
iv: 'o1mm4rCN6Q0J1hA7I5jjgA==',
|
||||
macKey: 'dfHB14sCIdun+RaKnAoyaQPC6qRDMewjqOIDZGmn3Es=',
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
index: 3,
|
||||
cipherKey: '/aU3zX2IdA91GAcB+7H57yzRe+6CgZ61tlW4M/rkCJI=',
|
||||
iv: 'v8VJF467QDD1ZCr1JD8pbQ==',
|
||||
macKey: 'MjK5iYjhZtQTJ4Eu3+qGOdYxn0G23EGRtTcusbzy9OA=',
|
||||
|
@ -489,19 +489,19 @@ describe('sessionTranslation', () => {
|
|||
{
|
||||
senderRatchetKey: 'BTwX5SmcUeBG7mwyOZ3YgxyXIN0ktzuEdWTfBUmPfGYG',
|
||||
chainKey: {
|
||||
index: 1,
|
||||
index: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
senderRatchetKey: 'BV7ECvKbwKIAD61BXDYr0xr3JtckuKzR1Hw8cVPWGtlo',
|
||||
chainKey: {
|
||||
index: 2,
|
||||
index: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
senderRatchetKey: 'BTC7rQqoykGR5Aaix7RkAhI5fSXufc6pVGN9OIC8EW5c',
|
||||
chainKey: {
|
||||
index: 0,
|
||||
index: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -576,17 +576,17 @@ describe('sessionTranslation', () => {
|
|||
|
||||
const expected = {
|
||||
currentSession: {
|
||||
sessionVersion: 1,
|
||||
sessionVersion: 3,
|
||||
localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444',
|
||||
remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA',
|
||||
rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=',
|
||||
previousCounter: 2,
|
||||
previousCounter: 3,
|
||||
senderChain: {
|
||||
senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x',
|
||||
senderRatchetKeyPrivate:
|
||||
'5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=',
|
||||
chainKey: {
|
||||
index: -1,
|
||||
index: 0,
|
||||
key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=',
|
||||
},
|
||||
},
|
||||
|
@ -594,18 +594,18 @@ describe('sessionTranslation', () => {
|
|||
{
|
||||
senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz',
|
||||
chainKey: {
|
||||
index: 5,
|
||||
index: 6,
|
||||
key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=',
|
||||
},
|
||||
messageKeys: [
|
||||
{
|
||||
index: 0,
|
||||
index: 1,
|
||||
cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=',
|
||||
iv: 'TcRanSxZVWbuIq0xDRGnEw==',
|
||||
macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=',
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
index: 5,
|
||||
cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=',
|
||||
iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==',
|
||||
macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=',
|
||||
|
@ -766,17 +766,17 @@ describe('sessionTranslation', () => {
|
|||
|
||||
const expected = {
|
||||
currentSession: {
|
||||
sessionVersion: 1,
|
||||
sessionVersion: 3,
|
||||
localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444',
|
||||
remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA',
|
||||
rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=',
|
||||
previousCounter: 2,
|
||||
previousCounter: 3,
|
||||
senderChain: {
|
||||
senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x',
|
||||
senderRatchetKeyPrivate:
|
||||
'5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=',
|
||||
chainKey: {
|
||||
index: -1,
|
||||
index: 0,
|
||||
key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=',
|
||||
},
|
||||
},
|
||||
|
@ -784,18 +784,18 @@ describe('sessionTranslation', () => {
|
|||
{
|
||||
senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz',
|
||||
chainKey: {
|
||||
index: 5,
|
||||
index: 6,
|
||||
key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=',
|
||||
},
|
||||
messageKeys: [
|
||||
{
|
||||
index: 0,
|
||||
index: 1,
|
||||
cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=',
|
||||
iv: 'TcRanSxZVWbuIq0xDRGnEw==',
|
||||
macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=',
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
index: 5,
|
||||
cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=',
|
||||
iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==',
|
||||
macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=',
|
||||
|
@ -809,17 +809,17 @@ describe('sessionTranslation', () => {
|
|||
},
|
||||
previousSessions: [
|
||||
{
|
||||
sessionVersion: 1,
|
||||
sessionVersion: 3,
|
||||
localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444',
|
||||
remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA',
|
||||
rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=',
|
||||
previousCounter: 2,
|
||||
previousCounter: 3,
|
||||
senderChain: {
|
||||
senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x',
|
||||
senderRatchetKeyPrivate:
|
||||
'5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=',
|
||||
chainKey: {
|
||||
index: -1,
|
||||
index: 0,
|
||||
key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=',
|
||||
},
|
||||
},
|
||||
|
@ -827,18 +827,18 @@ describe('sessionTranslation', () => {
|
|||
{
|
||||
senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz',
|
||||
chainKey: {
|
||||
index: 5,
|
||||
index: 6,
|
||||
key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=',
|
||||
},
|
||||
messageKeys: [
|
||||
{
|
||||
index: 1,
|
||||
index: 2,
|
||||
cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=',
|
||||
iv: 'TcRanSxZVWbuIq0xDRGnEw==',
|
||||
macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=',
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
index: 6,
|
||||
cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=',
|
||||
iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==',
|
||||
macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=',
|
||||
|
@ -851,17 +851,17 @@ describe('sessionTranslation', () => {
|
|||
aliceBaseKey: 'BUFOv0MAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N',
|
||||
},
|
||||
{
|
||||
sessionVersion: 1,
|
||||
sessionVersion: 3,
|
||||
localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444',
|
||||
remoteIdentityPublic: 'BaioqfzP5JD6b+GNqepPouf6eHK7xr9ynLJHnvlpgVRA',
|
||||
rootKey: 'ywM1L/yc2ppnA1hl+/oQlwD8Ara7bzUcg5etpQTQ/6s=',
|
||||
previousCounter: 2,
|
||||
previousCounter: 3,
|
||||
senderChain: {
|
||||
senderRatchetKey: 'BSsTNJarMQATbCAq40vnbrrW87PtVFOfJox7+SDNgj4x',
|
||||
senderRatchetKeyPrivate:
|
||||
'5JfjxauqiuD47SmI4QXBIoxzSk0uqKEScp0oTgw51Ag=',
|
||||
chainKey: {
|
||||
index: -1,
|
||||
index: 0,
|
||||
key: '6EI/Nw+vHhCoB499OpM/kLkQJFzrfqoAZ00w1ZgnowU=',
|
||||
},
|
||||
},
|
||||
|
@ -869,18 +869,18 @@ describe('sessionTranslation', () => {
|
|||
{
|
||||
senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz',
|
||||
chainKey: {
|
||||
index: 5,
|
||||
index: 6,
|
||||
key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=',
|
||||
},
|
||||
messageKeys: [
|
||||
{
|
||||
index: 2,
|
||||
index: 3,
|
||||
cipherKey: 'xVreEbT7Vtrxs85JyGBj6Y+UWftQz4H72F5kWV4cxqM=',
|
||||
iv: 'TcRanSxZVWbuIq0xDRGnEw==',
|
||||
macKey: '5fW9aIKXhtwWp/5alNJUIXInZbztf2ywzQSpYrXoQ3A=',
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
index: 4,
|
||||
cipherKey: 'A99HjM4pUugsQ5+2v48FGTGEhZPoW6wzW9MqSc11QQ4=',
|
||||
iv: 'bE8Ei2Rkaoz4SKRwdG4+tQ==',
|
||||
macKey: 'TOTdbAf0bCHOzcQ3lBaIm3yqmpEqvvldD0qTuDFmkAI=',
|
||||
|
@ -956,20 +956,20 @@ describe('sessionTranslation', () => {
|
|||
preKeyId: 386,
|
||||
signedPreKeyId: 2995,
|
||||
},
|
||||
previousCounter: 0,
|
||||
previousCounter: 1,
|
||||
remoteIdentityPublic: 'BRmB2uSNpwbXZJjisIh1p/VgRctUZSVIoiEm2ThjiHoq',
|
||||
remoteRegistrationId: 3188,
|
||||
rootKey: 'GzGfNozK5vDKqL4+fdqpiMRIuHNOndM6iMhGubNR1mk=',
|
||||
senderChain: {
|
||||
chainKey: {
|
||||
index: 0,
|
||||
index: 1,
|
||||
key: 'tl5Eby9q7n8PVeiriKoRjHhu9Y0RxvJ90PMq5MfKwgA=',
|
||||
},
|
||||
senderRatchetKey: 'BRSm55wC8hrG5Rp7l9gxtOhugp5ulcco20upOFCPyyJo',
|
||||
senderRatchetKeyPrivate:
|
||||
'IC0mCV0kFVAf+Q4cHid5hR7vy+5F0SvpYYaqsSA6d00=',
|
||||
},
|
||||
sessionVersion: 1,
|
||||
sessionVersion: 3,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
181
ts/test-electron/Curve_test.ts
Normal file
181
ts/test-electron/Curve_test.ts
Normal 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,34 +1,92 @@
|
|||
// Copyright 2015-2020 Signal Messenger, LLC
|
||||
// Copyright 2015-2021 Signal Messenger, LLC
|
||||
// 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', () => {
|
||||
const number = '+5558675309';
|
||||
let store;
|
||||
let identityKey;
|
||||
let testKey;
|
||||
let store: SignalProtocolStore;
|
||||
let identityKey: KeyPairType;
|
||||
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 () => {
|
||||
store = textsecure.storage.protocol;
|
||||
store = window.textsecure.storage.protocol;
|
||||
store.hydrateCaches();
|
||||
identityKey = {
|
||||
pubKey: libsignal.crypto.getRandomBytes(33),
|
||||
privKey: libsignal.crypto.getRandomBytes(32),
|
||||
pubKey: getPublicKey(),
|
||||
privKey: getPrivateKey(),
|
||||
};
|
||||
testKey = {
|
||||
pubKey: libsignal.crypto.getRandomBytes(33),
|
||||
privKey: libsignal.crypto.getRandomBytes(32),
|
||||
pubKey: getPublicKey(),
|
||||
privKey: getPrivateKey(),
|
||||
};
|
||||
|
||||
storage.put('registrationId', 1337);
|
||||
storage.put('identityKey', identityKey);
|
||||
await storage.fetch();
|
||||
ConversationController.reset();
|
||||
await ConversationController.load();
|
||||
await ConversationController.getOrCreateAndWait(number, 'private');
|
||||
setPublicKeyTypeByte(identityKey.pubKey);
|
||||
setPublicKeyTypeByte(testKey.pubKey);
|
||||
|
||||
clampPrivateKey(identityKey.privKey);
|
||||
clampPrivateKey(testKey.privKey);
|
||||
|
||||
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', () => {
|
||||
|
@ -42,23 +100,29 @@ describe('SignalProtocolStore', () => {
|
|||
it('retrieves my identity key', async () => {
|
||||
await store.hydrateCaches();
|
||||
const key = await store.getIdentityKeyPair();
|
||||
assertEqualArrayBuffers(key.pubKey, identityKey.pubKey);
|
||||
assertEqualArrayBuffers(key.privKey, identityKey.privKey);
|
||||
if (!key) {
|
||||
throw new Error('Missing key!');
|
||||
}
|
||||
|
||||
assert.isTrue(constantTimeEqual(key.pubKey, identityKey.pubKey));
|
||||
assert.isTrue(constantTimeEqual(key.privKey, identityKey.privKey));
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveIdentity', () => {
|
||||
const address = new libsignal.SignalProtocolAddress(number, 1);
|
||||
const identifier = address.toString();
|
||||
const identifier = `${number}.1`;
|
||||
|
||||
it('stores identity keys', async () => {
|
||||
await store.saveIdentity(identifier, testKey.pubKey);
|
||||
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 () => {
|
||||
const newIdentity = libsignal.crypto.getRandomBytes(33);
|
||||
const newIdentity = getPublicKey();
|
||||
await store.saveIdentity(identifier, testKey.pubKey);
|
||||
await store.saveIdentity(identifier, newIdentity);
|
||||
});
|
||||
|
@ -70,19 +134,28 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
it('marks the key firstUse', async () => {
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
assert(identity.firstUse);
|
||||
});
|
||||
it('sets the timestamp', async () => {
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
assert(identity.timestamp);
|
||||
});
|
||||
it('sets the verified status to DEFAULT', async () => {
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT);
|
||||
});
|
||||
});
|
||||
describe('When there is a different existing key (non first use)', () => {
|
||||
const newIdentity = libsignal.crypto.getRandomBytes(33);
|
||||
const newIdentity = getPublicKey();
|
||||
const oldTimestamp = Date.now();
|
||||
|
||||
before(async () => {
|
||||
|
@ -100,10 +173,16 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
it('marks the key not firstUse', async () => {
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
assert(!identity.firstUse);
|
||||
});
|
||||
it('updates the timestamp', async () => {
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
assert.notEqual(identity.timestamp, oldTimestamp);
|
||||
});
|
||||
|
||||
|
@ -123,6 +202,9 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
it('sets the new key to default', async () => {
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT);
|
||||
});
|
||||
});
|
||||
|
@ -142,7 +224,9 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
it('sets the new key to unverified', async () => {
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
assert.strictEqual(
|
||||
identity.verified,
|
||||
store.VerifiedStatus.UNVERIFIED
|
||||
|
@ -165,6 +249,9 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
it('sets the new key to unverified', async () => {
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
assert.strictEqual(
|
||||
identity.verified,
|
||||
store.VerifiedStatus.UNVERIFIED
|
||||
|
@ -180,6 +267,7 @@ describe('SignalProtocolStore', () => {
|
|||
publicKey: testKey.pubKey,
|
||||
timestamp: oldTimestamp,
|
||||
nonblockingApproval: false,
|
||||
firstUse: false,
|
||||
verified: store.VerifiedStatus.DEFAULT,
|
||||
});
|
||||
await store.hydrateCaches();
|
||||
|
@ -187,6 +275,9 @@ describe('SignalProtocolStore', () => {
|
|||
describe('If it is marked firstUse', () => {
|
||||
before(async () => {
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
identity.firstUse = true;
|
||||
await window.Signal.Data.createOrUpdateIdentityKey(identity);
|
||||
await store.hydrateCaches();
|
||||
|
@ -195,6 +286,9 @@ describe('SignalProtocolStore', () => {
|
|||
await store.saveIdentity(identifier, testKey.pubKey, true);
|
||||
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
assert(!identity.nonblockingApproval);
|
||||
assert.strictEqual(identity.timestamp, oldTimestamp);
|
||||
});
|
||||
|
@ -202,17 +296,23 @@ describe('SignalProtocolStore', () => {
|
|||
describe('If it is not marked firstUse', () => {
|
||||
before(async () => {
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
identity.firstUse = false;
|
||||
await window.Signal.Data.createOrUpdateIdentityKey(identity);
|
||||
await store.hydrateCaches();
|
||||
});
|
||||
describe('If nonblocking approval is required', () => {
|
||||
let now;
|
||||
let now: number;
|
||||
before(async () => {
|
||||
now = Date.now();
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(
|
||||
number
|
||||
);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
identity.timestamp = now;
|
||||
await window.Signal.Data.createOrUpdateIdentityKey(identity);
|
||||
await store.hydrateCaches();
|
||||
|
@ -223,6 +323,9 @@ describe('SignalProtocolStore', () => {
|
|||
const identity = await window.Signal.Data.getIdentityKeyById(
|
||||
number
|
||||
);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
|
||||
assert.strictEqual(identity.nonblockingApproval, true);
|
||||
assert.strictEqual(identity.timestamp, now);
|
||||
|
@ -233,12 +336,13 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
});
|
||||
describe('saveIdentityWithAttributes', () => {
|
||||
let now;
|
||||
let validAttributes;
|
||||
let now: number;
|
||||
let validAttributes: IdentityKeyType;
|
||||
|
||||
before(async () => {
|
||||
now = Date.now();
|
||||
validAttributes = {
|
||||
id: number,
|
||||
publicKey: testKey.pubKey,
|
||||
firstUse: true,
|
||||
timestamp: now,
|
||||
|
@ -255,29 +359,44 @@ describe('SignalProtocolStore', () => {
|
|||
|
||||
it('publicKey is saved', async () => {
|
||||
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 () => {
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
assert.strictEqual(identity.firstUse, true);
|
||||
});
|
||||
it('timestamp is saved', async () => {
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
assert.strictEqual(identity.timestamp, now);
|
||||
});
|
||||
it('verified is saved', async () => {
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED);
|
||||
});
|
||||
it('nonblockingApproval is saved', async () => {
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
assert.strictEqual(identity.nonblockingApproval, false);
|
||||
});
|
||||
});
|
||||
describe('with invalid attributes', () => {
|
||||
let attributes;
|
||||
let attributes: IdentityKeyType;
|
||||
beforeEach(() => {
|
||||
attributes = _.clone(validAttributes);
|
||||
attributes = window._.clone(validAttributes);
|
||||
});
|
||||
|
||||
async function testInvalidAttributes() {
|
||||
|
@ -290,23 +409,23 @@ describe('SignalProtocolStore', () => {
|
|||
}
|
||||
|
||||
it('rejects an invalid publicKey', async () => {
|
||||
attributes.publicKey = 'a string';
|
||||
attributes.publicKey = 'a string' as any;
|
||||
await testInvalidAttributes();
|
||||
});
|
||||
it('rejects invalid firstUse', async () => {
|
||||
attributes.firstUse = 0;
|
||||
attributes.firstUse = 0 as any;
|
||||
await testInvalidAttributes();
|
||||
});
|
||||
it('rejects invalid timestamp', async () => {
|
||||
attributes.timestamp = NaN;
|
||||
attributes.timestamp = NaN as any;
|
||||
await testInvalidAttributes();
|
||||
});
|
||||
it('rejects invalid verified', async () => {
|
||||
attributes.verified = null;
|
||||
attributes.verified = null as any;
|
||||
await testInvalidAttributes();
|
||||
});
|
||||
it('rejects invalid nonblockingApproval', async () => {
|
||||
attributes.nonblockingApproval = 0;
|
||||
attributes.nonblockingApproval = 0 as any;
|
||||
await testInvalidAttributes();
|
||||
});
|
||||
});
|
||||
|
@ -315,6 +434,9 @@ describe('SignalProtocolStore', () => {
|
|||
it('sets nonblockingApproval', async () => {
|
||||
await store.setApproval(number, true);
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
|
||||
assert.strictEqual(identity.nonblockingApproval, true);
|
||||
});
|
||||
|
@ -337,8 +459,12 @@ describe('SignalProtocolStore', () => {
|
|||
await store.setVerified(number, store.VerifiedStatus.VERIFIED);
|
||||
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
|
||||
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', () => {
|
||||
|
@ -351,12 +477,16 @@ describe('SignalProtocolStore', () => {
|
|||
);
|
||||
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
|
||||
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', () => {
|
||||
const newIdentity = libsignal.crypto.getRandomBytes(33);
|
||||
const newIdentity = getPublicKey();
|
||||
before(saveRecordDefault);
|
||||
it('does not change the record.', async () => {
|
||||
await store.setVerified(
|
||||
|
@ -366,14 +496,18 @@ describe('SignalProtocolStore', () => {
|
|||
);
|
||||
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
|
||||
assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT);
|
||||
assertEqualArrayBuffers(identity.publicKey, testKey.pubKey);
|
||||
assert.isTrue(constantTimeEqual(identity.publicKey, testKey.pubKey));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('processContactSyncVerificationState', () => {
|
||||
const newIdentity = libsignal.crypto.getRandomBytes(33);
|
||||
let keychangeTriggered;
|
||||
const newIdentity = getPublicKey();
|
||||
let keychangeTriggered: number;
|
||||
|
||||
beforeEach(() => {
|
||||
keychangeTriggered = 0;
|
||||
|
@ -436,12 +570,17 @@ describe('SignalProtocolStore', () => {
|
|||
const identity = await window.Signal.Data.getIdentityKeyById(
|
||||
number
|
||||
);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
identity.verified,
|
||||
store.VerifiedStatus.VERIFIED
|
||||
);
|
||||
assertEqualArrayBuffers(identity.publicKey, testKey.pubKey);
|
||||
assert.isTrue(
|
||||
constantTimeEqual(identity.publicKey, testKey.pubKey)
|
||||
);
|
||||
assert.strictEqual(keychangeTriggered, 0);
|
||||
});
|
||||
});
|
||||
|
@ -468,9 +607,14 @@ describe('SignalProtocolStore', () => {
|
|||
const identity = await window.Signal.Data.getIdentityKeyById(
|
||||
number
|
||||
);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
|
||||
assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT);
|
||||
assertEqualArrayBuffers(identity.publicKey, testKey.pubKey);
|
||||
assert.isTrue(
|
||||
constantTimeEqual(identity.publicKey, testKey.pubKey)
|
||||
);
|
||||
assert.strictEqual(keychangeTriggered, 0);
|
||||
});
|
||||
});
|
||||
|
@ -514,12 +658,15 @@ describe('SignalProtocolStore', () => {
|
|||
);
|
||||
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
identity.verified,
|
||||
store.VerifiedStatus.UNVERIFIED
|
||||
);
|
||||
assertEqualArrayBuffers(identity.publicKey, newIdentity);
|
||||
assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity));
|
||||
assert.strictEqual(keychangeTriggered, 0);
|
||||
});
|
||||
});
|
||||
|
@ -547,12 +694,15 @@ describe('SignalProtocolStore', () => {
|
|||
const identity = await window.Signal.Data.getIdentityKeyById(
|
||||
number
|
||||
);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
identity.verified,
|
||||
store.VerifiedStatus.UNVERIFIED
|
||||
);
|
||||
assertEqualArrayBuffers(identity.publicKey, newIdentity);
|
||||
assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity));
|
||||
assert.strictEqual(keychangeTriggered, 1);
|
||||
});
|
||||
});
|
||||
|
@ -578,12 +728,17 @@ describe('SignalProtocolStore', () => {
|
|||
const identity = await window.Signal.Data.getIdentityKeyById(
|
||||
number
|
||||
);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
identity.verified,
|
||||
store.VerifiedStatus.UNVERIFIED
|
||||
);
|
||||
assertEqualArrayBuffers(identity.publicKey, testKey.pubKey);
|
||||
assert.isTrue(
|
||||
constantTimeEqual(identity.publicKey, testKey.pubKey)
|
||||
);
|
||||
assert.strictEqual(keychangeTriggered, 0);
|
||||
});
|
||||
});
|
||||
|
@ -626,9 +781,12 @@ describe('SignalProtocolStore', () => {
|
|||
newIdentity
|
||||
);
|
||||
const identity = await window.Signal.Data.getIdentityKeyById(number);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
|
||||
assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED);
|
||||
assertEqualArrayBuffers(identity.publicKey, newIdentity);
|
||||
assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity));
|
||||
assert.strictEqual(keychangeTriggered, 0);
|
||||
});
|
||||
});
|
||||
|
@ -656,12 +814,15 @@ describe('SignalProtocolStore', () => {
|
|||
const identity = await window.Signal.Data.getIdentityKeyById(
|
||||
number
|
||||
);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
identity.verified,
|
||||
store.VerifiedStatus.VERIFIED
|
||||
);
|
||||
assertEqualArrayBuffers(identity.publicKey, newIdentity);
|
||||
assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity));
|
||||
assert.strictEqual(keychangeTriggered, 1);
|
||||
});
|
||||
});
|
||||
|
@ -687,12 +848,17 @@ describe('SignalProtocolStore', () => {
|
|||
const identity = await window.Signal.Data.getIdentityKeyById(
|
||||
number
|
||||
);
|
||||
if (!identity) {
|
||||
throw new Error('Missing identity!');
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
identity.verified,
|
||||
store.VerifiedStatus.VERIFIED
|
||||
);
|
||||
assertEqualArrayBuffers(identity.publicKey, testKey.pubKey);
|
||||
assert.isTrue(
|
||||
constantTimeEqual(identity.publicKey, testKey.pubKey)
|
||||
);
|
||||
assert.strictEqual(keychangeTriggered, 0);
|
||||
});
|
||||
});
|
||||
|
@ -795,13 +961,12 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
});
|
||||
describe('isTrustedIdentity', () => {
|
||||
const address = new libsignal.SignalProtocolAddress(number, 1);
|
||||
const identifier = address.toString();
|
||||
const identifier = `${number}.1`;
|
||||
|
||||
describe('When invalid direction is given', () => {
|
||||
it('should fail', async () => {
|
||||
try {
|
||||
await store.isTrustedIdentity(number, testKey.pubKey);
|
||||
await store.isTrustedIdentity(number, testKey.pubKey, 'dir' as any);
|
||||
throw new Error('isTrustedIdentity should have failed');
|
||||
} catch (error) {
|
||||
// good
|
||||
|
@ -810,13 +975,13 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
describe('When direction is RECEIVING', () => {
|
||||
it('always returns true', async () => {
|
||||
const newIdentity = libsignal.crypto.getRandomBytes(33);
|
||||
const newIdentity = getPublicKey();
|
||||
await store.saveIdentity(identifier, testKey.pubKey);
|
||||
|
||||
const trusted = await store.isTrustedIdentity(
|
||||
identifier,
|
||||
newIdentity,
|
||||
store.Direction.RECEIVING
|
||||
Direction.Receiving
|
||||
);
|
||||
|
||||
if (!trusted) {
|
||||
|
@ -830,11 +995,11 @@ describe('SignalProtocolStore', () => {
|
|||
await store.removeIdentityKey(number);
|
||||
});
|
||||
it('returns true', async () => {
|
||||
const newIdentity = libsignal.crypto.getRandomBytes(33);
|
||||
const newIdentity = getPublicKey();
|
||||
const trusted = await store.isTrustedIdentity(
|
||||
identifier,
|
||||
newIdentity,
|
||||
store.Direction.SENDING
|
||||
Direction.Sending
|
||||
);
|
||||
if (!trusted) {
|
||||
throw new Error('isTrusted returned false on first use');
|
||||
|
@ -847,11 +1012,11 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
describe('When the existing key is different', () => {
|
||||
it('returns false', async () => {
|
||||
const newIdentity = libsignal.crypto.getRandomBytes(33);
|
||||
const newIdentity = getPublicKey();
|
||||
const trusted = await store.isTrustedIdentity(
|
||||
identifier,
|
||||
newIdentity,
|
||||
store.Direction.SENDING
|
||||
Direction.Sending
|
||||
);
|
||||
if (trusted) {
|
||||
throw new Error('isTrusted returned true on untrusted key');
|
||||
|
@ -859,7 +1024,7 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
});
|
||||
describe('When the existing key matches the new key', () => {
|
||||
const newIdentity = libsignal.crypto.getRandomBytes(33);
|
||||
const newIdentity = getPublicKey();
|
||||
before(async () => {
|
||||
await store.saveIdentity(identifier, newIdentity);
|
||||
});
|
||||
|
@ -867,7 +1032,7 @@ describe('SignalProtocolStore', () => {
|
|||
const trusted = await store.isTrustedIdentity(
|
||||
identifier,
|
||||
newIdentity,
|
||||
store.Direction.SENDING
|
||||
Direction.Sending
|
||||
);
|
||||
|
||||
if (trusted) {
|
||||
|
@ -880,7 +1045,7 @@ describe('SignalProtocolStore', () => {
|
|||
const trusted = await store.isTrustedIdentity(
|
||||
identifier,
|
||||
newIdentity,
|
||||
store.Direction.SENDING
|
||||
Direction.Sending
|
||||
);
|
||||
if (!trusted) {
|
||||
throw new Error('isTrusted returned false on an approved key');
|
||||
|
@ -894,8 +1059,21 @@ describe('SignalProtocolStore', () => {
|
|||
it('stores prekeys', async () => {
|
||||
await store.storePreKey(1, testKey);
|
||||
const key = await store.loadPreKey(1);
|
||||
assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
|
||||
assertEqualArrayBuffers(key.privKey, testKey.privKey);
|
||||
if (!key) {
|
||||
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', () => {
|
||||
|
@ -903,7 +1081,7 @@ describe('SignalProtocolStore', () => {
|
|||
await store.storePreKey(2, testKey);
|
||||
});
|
||||
it('deletes prekeys', async () => {
|
||||
await store.removePreKey(2, testKey);
|
||||
await store.removePreKey(2);
|
||||
|
||||
const key = await store.loadPreKey(2);
|
||||
assert.isUndefined(key);
|
||||
|
@ -912,10 +1090,22 @@ describe('SignalProtocolStore', () => {
|
|||
describe('storeSignedPreKey', () => {
|
||||
it('stores signed prekeys', async () => {
|
||||
await store.storeSignedPreKey(3, testKey);
|
||||
|
||||
const key = await store.loadSignedPreKey(3);
|
||||
assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
|
||||
assertEqualArrayBuffers(key.privKey, testKey.privKey);
|
||||
if (!key) {
|
||||
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', () => {
|
||||
|
@ -923,7 +1113,7 @@ describe('SignalProtocolStore', () => {
|
|||
await store.storeSignedPreKey(4, testKey);
|
||||
});
|
||||
it('deletes signed prekeys', async () => {
|
||||
await store.removeSignedPreKey(4, testKey);
|
||||
await store.removeSignedPreKey(4);
|
||||
|
||||
const key = await store.loadSignedPreKey(4);
|
||||
assert.isUndefined(key);
|
||||
|
@ -931,24 +1121,25 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
describe('storeSession', () => {
|
||||
it('stores sessions', async () => {
|
||||
const testRecord = 'an opaque string';
|
||||
|
||||
const testRecord = getSessionRecord();
|
||||
await store.storeSession(`${number}.1`, testRecord);
|
||||
const record = await store.loadSession(`${number}.1`);
|
||||
if (!record) {
|
||||
throw new Error('Missing record!');
|
||||
}
|
||||
|
||||
assert.deepEqual(record, testRecord);
|
||||
assert.equal(record, testRecord);
|
||||
});
|
||||
});
|
||||
describe('removeAllSessions', () => {
|
||||
it('removes all sessions for a number', async () => {
|
||||
const testRecord = 'an opaque string';
|
||||
const devices = [1, 2, 3].map(deviceId => {
|
||||
return [number, deviceId].join('.');
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
devices.map(async encodedNumber => {
|
||||
await store.storeSession(encodedNumber, testRecord + encodedNumber);
|
||||
await store.storeSession(encodedNumber, getSessionRecord());
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -965,7 +1156,7 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
describe('clearSessionStore', () => {
|
||||
it('clears the session store', async () => {
|
||||
const testRecord = 'an opaque string';
|
||||
const testRecord = getSessionRecord();
|
||||
await store.storeSession(`${number}.1`, testRecord);
|
||||
await store.clearSessionStore();
|
||||
|
||||
|
@ -975,17 +1166,7 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
describe('getDeviceIds', () => {
|
||||
it('returns deviceIds for a number', async () => {
|
||||
const openRecord = JSON.stringify({
|
||||
version: 'v1',
|
||||
sessions: {
|
||||
ephemeralKey: {
|
||||
registrationId: 25,
|
||||
indexInfo: {
|
||||
closed: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const openRecord = getSessionRecord(true);
|
||||
const openDevices = [1, 2, 3, 10].map(deviceId => {
|
||||
return [number, deviceId].join('.');
|
||||
});
|
||||
|
@ -995,22 +1176,13 @@ describe('SignalProtocolStore', () => {
|
|||
})
|
||||
);
|
||||
|
||||
const closedRecord = JSON.stringify({
|
||||
version: 'v1',
|
||||
sessions: {
|
||||
ephemeralKey: {
|
||||
registrationId: 24,
|
||||
indexInfo: {
|
||||
closed: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const closedRecord = getSessionRecord(false);
|
||||
await store.storeSession([number, 11].join('.'), closedRecord);
|
||||
|
||||
const deviceIds = await store.getDeviceIds(number);
|
||||
assert.sameMembers(deviceIds, [1, 2, 3, 10]);
|
||||
});
|
||||
|
||||
it('returns empty array for a number with no device ids', async () => {
|
||||
const deviceIds = await store.getDeviceIds('foo');
|
||||
assert.sameMembers(deviceIds, []);
|
||||
|
@ -1026,9 +1198,27 @@ describe('SignalProtocolStore', () => {
|
|||
|
||||
it('adds three and gets them back', async () => {
|
||||
await Promise.all([
|
||||
store.addUnprocessed({ id: 2, envelope: 'second', timestamp: 2 }),
|
||||
store.addUnprocessed({ id: 3, envelope: 'third', timestamp: 3 }),
|
||||
store.addUnprocessed({ id: 1, envelope: 'first', timestamp: 1 }),
|
||||
store.addUnprocessed({
|
||||
id: '2-two',
|
||||
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();
|
||||
|
@ -1041,8 +1231,14 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
|
||||
it('saveUnprocessed successfully updates item', async () => {
|
||||
const id = 1;
|
||||
await store.addUnprocessed({ id, envelope: 'first', timestamp: 1 });
|
||||
const id = '1-one';
|
||||
await store.addUnprocessed({
|
||||
id,
|
||||
envelope: 'first',
|
||||
timestamp: 1,
|
||||
version: 2,
|
||||
attempts: 0,
|
||||
});
|
||||
await store.updateUnprocessedWithData(id, { decrypted: 'updated' });
|
||||
|
||||
const items = await store.getAllUnprocessed();
|
||||
|
@ -1052,8 +1248,14 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
|
||||
it('removeUnprocessed successfully deletes item', async () => {
|
||||
const id = 1;
|
||||
await store.addUnprocessed({ id, envelope: 'first', timestamp: 1 });
|
||||
const id = '1-one';
|
||||
await store.addUnprocessed({
|
||||
id,
|
||||
envelope: 'first',
|
||||
timestamp: 1,
|
||||
version: 2,
|
||||
attempts: 0,
|
||||
});
|
||||
await store.removeUnprocessed(id);
|
||||
|
||||
const items = await store.getAllUnprocessed();
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -11,7 +11,7 @@ import * as sinon from 'sinon';
|
|||
import { v4 as uuid } from 'uuid';
|
||||
import { arrayBufferToBase64 } from '../../Crypto';
|
||||
import { SenderCertificateClass } from '../../textsecure';
|
||||
import { SenderCertificateMode } from '../../metadata/SecretSessionCipher';
|
||||
import { SenderCertificateMode } from '../../textsecure/OutgoingMessage';
|
||||
|
||||
import { SenderCertificateService } from '../../services/senderCertificate';
|
||||
|
||||
|
|
70
ts/textsecure.d.ts
vendored
70
ts/textsecure.d.ts
vendored
|
@ -1,12 +1,6 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import {
|
||||
KeyPairType,
|
||||
SessionRecordType,
|
||||
SignedPreKeyType,
|
||||
StorageType,
|
||||
} from './libsignal.d';
|
||||
import Crypto from './textsecure/Crypto';
|
||||
import MessageReceiver from './textsecure/MessageReceiver';
|
||||
import MessageSender from './textsecure/SendMessage';
|
||||
|
@ -18,6 +12,7 @@ import { WebAPIType } from './textsecure/WebAPI';
|
|||
import utils from './textsecure/Helpers';
|
||||
import { CallingMessage as CallingMessageClass } from 'ringrtc';
|
||||
import { WhatIsThis } from './window.d';
|
||||
import { SignalProtocolStore } from './SignalProtocolStore';
|
||||
|
||||
export type UnprocessedType = {
|
||||
attempts: number;
|
||||
|
@ -79,7 +74,7 @@ export type TextSecureType = {
|
|||
get: (key: string, defaultValue?: any) => any;
|
||||
put: (key: string, value: any) => Promise<void>;
|
||||
remove: (key: string | Array<string>) => Promise<void>;
|
||||
protocol: StorageProtocolType;
|
||||
protocol: SignalProtocolStore;
|
||||
};
|
||||
messageReceiver: MessageReceiver;
|
||||
messageSender: MessageSender;
|
||||
|
@ -94,67 +89,6 @@ export type TextSecureType = {
|
|||
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
|
||||
|
||||
type DeviceMessagesProtobufTypes = {
|
||||
|
|
|
@ -11,16 +11,28 @@ import PQueue from 'p-queue';
|
|||
import EventTarget from './EventTarget';
|
||||
import { WebAPIType } from './WebAPI';
|
||||
import MessageReceiver from './MessageReceiver';
|
||||
import { KeyPairType, SignedPreKeyType } from '../libsignal.d';
|
||||
import { KeyPairType, CompatSignedPreKeyType } from './Types.d';
|
||||
import utils from './Helpers';
|
||||
import ProvisioningCipher from './ProvisioningCipher';
|
||||
import WebSocketResource, {
|
||||
IncomingWebSocketRequest,
|
||||
} from './WebsocketResources';
|
||||
import {
|
||||
deriveAccessKey,
|
||||
generateRegistrationId,
|
||||
getRandomBytes,
|
||||
} from '../Crypto';
|
||||
import {
|
||||
generateKeyPair,
|
||||
generateSignedPreKey,
|
||||
generatePreKey,
|
||||
} from '../Curve';
|
||||
import { isMoreRecentThan, isOlderThan } from '../util/timestamp';
|
||||
|
||||
const ARCHIVE_AGE = 30 * 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) {
|
||||
if (!id || !id.length) {
|
||||
|
@ -97,6 +109,9 @@ export default class AccountManager extends EventTarget {
|
|||
|
||||
async decryptDeviceName(base64: string) {
|
||||
const identityKey = await window.textsecure.storage.protocol.getIdentityKeyPair();
|
||||
if (!identityKey) {
|
||||
throw new Error('decryptDeviceName: No identity key pair!');
|
||||
}
|
||||
|
||||
const arrayBuffer = MessageReceiver.stringToArrayBufferBase64(base64);
|
||||
const proto = window.textsecure.protobuf.DeviceName.decode(arrayBuffer);
|
||||
|
@ -139,39 +154,28 @@ export default class AccountManager extends EventTarget {
|
|||
}
|
||||
|
||||
async registerSingleDevice(number: string, verificationCode: string) {
|
||||
const registerKeys = this.server.registerKeys.bind(this.server);
|
||||
const createAccount = this.createAccount.bind(this);
|
||||
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
|
||||
const generateKeys = this.generateKeys.bind(this, 100);
|
||||
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 this.queueTask(async () => {
|
||||
const identityKeyPair = generateKeyPair();
|
||||
const profileKey = getRandomBytes(PROFILE_KEY_LENGTH);
|
||||
const accessKey = await deriveAccessKey(profileKey);
|
||||
|
||||
return createAccount(
|
||||
number,
|
||||
verificationCode,
|
||||
identityKeyPair,
|
||||
profileKey,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{ accessKey }
|
||||
)
|
||||
.then(clearSessionsAndPreKeys)
|
||||
.then(async () => generateKeys())
|
||||
.then(async (keys: GeneratedKeysType) =>
|
||||
registerKeys(keys).then(async () => confirmKeys(keys))
|
||||
)
|
||||
.then(async () => registrationDone());
|
||||
}
|
||||
)
|
||||
);
|
||||
await this.createAccount(
|
||||
number,
|
||||
verificationCode,
|
||||
identityKeyPair,
|
||||
profileKey,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{ accessKey }
|
||||
);
|
||||
|
||||
await this.clearSessionsAndPreKeys();
|
||||
const keys = await this.generateKeys(SIGNED_KEY_GEN_BATCH_SIZE);
|
||||
await this.server.registerKeys(keys);
|
||||
await this.confirmKeys(keys);
|
||||
await this.registrationDone();
|
||||
});
|
||||
}
|
||||
|
||||
async registerSecondDevice(
|
||||
|
@ -181,7 +185,11 @@ export default class AccountManager extends EventTarget {
|
|||
) {
|
||||
const createAccount = this.createAccount.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 registrationDone = this.registrationDone.bind(this);
|
||||
const registerKeys = this.server.registerKeys.bind(this.server);
|
||||
|
@ -296,7 +304,10 @@ export default class AccountManager extends EventTarget {
|
|||
}
|
||||
|
||||
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);
|
||||
|
||||
return this.queueTask(async () =>
|
||||
|
@ -338,11 +349,13 @@ export default class AccountManager extends EventTarget {
|
|||
return store
|
||||
.getIdentityKeyPair()
|
||||
.then(
|
||||
async (identityKey: KeyPairType) =>
|
||||
window.libsignal.KeyHelper.generateSignedPreKey(
|
||||
identityKey,
|
||||
signedKeyId
|
||||
),
|
||||
async (identityKey: KeyPairType | undefined) => {
|
||||
if (!identityKey) {
|
||||
throw new Error('rotateSignedPreKey: No identity key pair!');
|
||||
}
|
||||
|
||||
return generateSignedPreKey(identityKey, signedKeyId);
|
||||
},
|
||||
() => {
|
||||
// We swallow any error here, because we don't want to get into
|
||||
// a loop of repeated retries.
|
||||
|
@ -352,7 +365,7 @@ export default class AccountManager extends EventTarget {
|
|||
return null;
|
||||
}
|
||||
)
|
||||
.then(async (res: SignedPreKeyType | null) => {
|
||||
.then(async (res: CompatSignedPreKeyType | null) => {
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
|
@ -489,11 +502,9 @@ export default class AccountManager extends EventTarget {
|
|||
options: { accessKey?: ArrayBuffer; uuid?: string } = {}
|
||||
): Promise<void> {
|
||||
const { accessKey, uuid } = options;
|
||||
let password = btoa(
|
||||
utils.getString(window.libsignal.crypto.getRandomBytes(16))
|
||||
);
|
||||
let password = btoa(utils.getString(getRandomBytes(16)));
|
||||
password = password.substring(0, password.length - 2);
|
||||
const registrationId = window.libsignal.KeyHelper.generateRegistrationId();
|
||||
const registrationId = generateRegistrationId();
|
||||
|
||||
const previousNumber = getIdentifier(
|
||||
window.textsecure.storage.get('number_id')
|
||||
|
@ -677,6 +688,10 @@ export default class AccountManager extends EventTarget {
|
|||
|
||||
const store = window.textsecure.storage.protocol;
|
||||
return store.getIdentityKeyPair().then(async identityKey => {
|
||||
if (!identityKey) {
|
||||
throw new Error('generateKeys: No identity key pair!');
|
||||
}
|
||||
|
||||
const result: any = {
|
||||
preKeys: [],
|
||||
identityKey: identityKey.pubKey,
|
||||
|
@ -685,7 +700,7 @@ export default class AccountManager extends EventTarget {
|
|||
|
||||
for (let keyId = startId; keyId < startId + count; keyId += 1) {
|
||||
promises.push(
|
||||
window.libsignal.KeyHelper.generatePreKey(keyId).then(async res => {
|
||||
Promise.resolve(generatePreKey(keyId)).then(async res => {
|
||||
await store.storePreKey(res.keyId, res.keyPair);
|
||||
result.preKeys.push({
|
||||
keyId: res.keyId,
|
||||
|
@ -699,19 +714,18 @@ export default class AccountManager extends EventTarget {
|
|||
}
|
||||
|
||||
promises.push(
|
||||
window.libsignal.KeyHelper.generateSignedPreKey(
|
||||
identityKey,
|
||||
signedKeyId
|
||||
).then(async res => {
|
||||
await store.storeSignedPreKey(res.keyId, res.keyPair);
|
||||
result.signedPreKey = {
|
||||
keyId: res.keyId,
|
||||
publicKey: res.keyPair.pubKey,
|
||||
signature: res.signature,
|
||||
// server.registerKeys doesn't use keyPair, confirmKeys does
|
||||
keyPair: res.keyPair,
|
||||
};
|
||||
})
|
||||
Promise.resolve(generateSignedPreKey(identityKey, signedKeyId)).then(
|
||||
async res => {
|
||||
await store.storeSignedPreKey(res.keyId, res.keyPair);
|
||||
result.signedPreKey = {
|
||||
keyId: res.keyId,
|
||||
publicKey: res.keyPair.pubKey,
|
||||
signature: res.signature,
|
||||
// server.registerKeys doesn't use keyPair, confirmKeys does
|
||||
keyPair: res.keyPair,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
promises.push(
|
||||
|
|
|
@ -5,6 +5,14 @@
|
|||
/* eslint-disable no-bitwise */
|
||||
/* eslint-disable more/no-then */
|
||||
import { ByteBufferClass } from '../window.d';
|
||||
import {
|
||||
decryptAes256CbcPkcsPadding,
|
||||
encryptAes256CbcPkcsPadding,
|
||||
getRandomBytes as outerGetRandomBytes,
|
||||
hmacSha256,
|
||||
sha256,
|
||||
verifyHmacSha256,
|
||||
} from '../Crypto';
|
||||
|
||||
declare global {
|
||||
// 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 = {
|
||||
// Decrypts message into a raw string
|
||||
async decryptWebsocketMessage(
|
||||
|
@ -175,11 +179,9 @@ const Crypto = {
|
|||
decodedMessage.byteLength
|
||||
);
|
||||
|
||||
return window.libsignal.crypto
|
||||
.verifyMAC(ivAndCiphertext, macKey, mac, 10)
|
||||
.then(async () =>
|
||||
window.libsignal.crypto.decrypt(aesKey, ciphertext, iv)
|
||||
);
|
||||
await verifyHmacSha256(ivAndCiphertext, macKey, mac, 10);
|
||||
|
||||
return decryptAes256CbcPkcsPadding(aesKey, ciphertext, iv);
|
||||
},
|
||||
|
||||
async decryptAttachment(
|
||||
|
@ -205,18 +207,13 @@ const Crypto = {
|
|||
encryptedBin.byteLength
|
||||
);
|
||||
|
||||
return window.libsignal.crypto
|
||||
.verifyMAC(ivAndCiphertext, macKey, mac, 32)
|
||||
.then(async () => {
|
||||
if (theirDigest) {
|
||||
return verifyDigest(encryptedBin, theirDigest);
|
||||
}
|
||||
await verifyHmacSha256(ivAndCiphertext, macKey, mac, 32);
|
||||
|
||||
return null;
|
||||
})
|
||||
.then(async () =>
|
||||
window.libsignal.crypto.decrypt(aesKey, ciphertext, iv)
|
||||
);
|
||||
if (theirDigest) {
|
||||
await verifyDigest(encryptedBin, theirDigest);
|
||||
}
|
||||
|
||||
return decryptAes256CbcPkcsPadding(aesKey, ciphertext, iv);
|
||||
},
|
||||
|
||||
async encryptAttachment(
|
||||
|
@ -239,36 +236,30 @@ const Crypto = {
|
|||
const aesKey = keys.slice(0, 32);
|
||||
const macKey = keys.slice(32, 64);
|
||||
|
||||
return window.libsignal.crypto
|
||||
.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);
|
||||
const ciphertext = await encryptAes256CbcPkcsPadding(aesKey, plaintext, iv);
|
||||
|
||||
return window.libsignal.crypto
|
||||
.calculateMAC(macKey, ivAndCiphertext.buffer as ArrayBuffer)
|
||||
.then(async mac => {
|
||||
const encryptedBin = new Uint8Array(
|
||||
16 + ciphertext.byteLength + 32
|
||||
);
|
||||
encryptedBin.set(ivAndCiphertext);
|
||||
encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength);
|
||||
return calculateDigest(encryptedBin.buffer as ArrayBuffer).then(
|
||||
digest => ({
|
||||
ciphertext: encryptedBin.buffer,
|
||||
digest,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
const ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength);
|
||||
ivAndCiphertext.set(new Uint8Array(iv));
|
||||
ivAndCiphertext.set(new Uint8Array(ciphertext), 16);
|
||||
|
||||
const mac = await hmacSha256(macKey, ivAndCiphertext.buffer as ArrayBuffer);
|
||||
|
||||
const encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32);
|
||||
encryptedBin.set(ivAndCiphertext);
|
||||
encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength);
|
||||
const digest = await sha256(encryptedBin.buffer as ArrayBuffer);
|
||||
|
||||
return {
|
||||
ciphertext: encryptedBin.buffer,
|
||||
digest,
|
||||
};
|
||||
},
|
||||
|
||||
async encryptProfile(
|
||||
data: ArrayBuffer,
|
||||
key: ArrayBuffer
|
||||
): Promise<ArrayBuffer> {
|
||||
const iv = window.libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH);
|
||||
const iv = outerGetRandomBytes(PROFILE_IV_LENGTH);
|
||||
if (key.byteLength !== PROFILE_KEY_LENGTH) {
|
||||
throw new Error('Got invalid length profile key');
|
||||
}
|
||||
|
@ -389,7 +380,7 @@ const Crypto = {
|
|||
},
|
||||
|
||||
getRandomBytes(size: number): ArrayBuffer {
|
||||
return window.libsignal.crypto.getRandomBytes(size);
|
||||
return outerGetRandomBytes(size);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
identifier: string;
|
||||
|
||||
|
|
|
@ -13,10 +13,27 @@ import { isNumber, map, omit, noop } from 'lodash';
|
|||
import PQueue from 'p-queue';
|
||||
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 { assert } from '../util/assert';
|
||||
|
||||
import { parseIntOrThrow } from '../util/parseIntOrThrow';
|
||||
import EventTarget from './EventTarget';
|
||||
import { WebAPIType } from './WebAPI';
|
||||
import utils from './Helpers';
|
||||
|
@ -24,13 +41,8 @@ import WebSocketResource, {
|
|||
IncomingWebSocketRequest,
|
||||
} from './WebsocketResources';
|
||||
import Crypto from './Crypto';
|
||||
import { deriveMasterKeyFromGroupV1 } from '../Crypto';
|
||||
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
|
||||
import { ContactBuffer, GroupBuffer } from './ContactsParser';
|
||||
import { IncomingIdentityKeyError } from './Errors';
|
||||
import {
|
||||
createCertificateValidator,
|
||||
SecretSessionCipher,
|
||||
} from '../metadata/SecretSessionCipher';
|
||||
|
||||
import {
|
||||
AttachmentPointerClass,
|
||||
|
@ -93,8 +105,6 @@ declare global {
|
|||
interface Error {
|
||||
reason?: any;
|
||||
stackForLog?: string;
|
||||
sender?: SignalProtocolAddressClass;
|
||||
senderUuid?: SignalProtocolAddressClass;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -196,7 +206,10 @@ class MessageReceiverInner extends EventTarget {
|
|||
this.uuid_id = username ? utils.unencodeNumber(username)[0] : undefined;
|
||||
this.deviceId =
|
||||
username || oldUsername
|
||||
? parseInt(utils.unencodeNumber(username || oldUsername)[1], 10)
|
||||
? parseIntOrThrow(
|
||||
utils.unencodeNumber(username || oldUsername)[1],
|
||||
'MessageReceiver.constructor: username || oldUsername'
|
||||
)
|
||||
: undefined;
|
||||
|
||||
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.sourceUuid = envelope.sourceUuid || item.sourceUuid;
|
||||
envelope.sourceDevice = envelope.sourceDevice || item.sourceDevice;
|
||||
envelope.serverTimestamp = envelope.serverTimestamp
|
||||
? envelope.serverTimestamp.toNumber()
|
||||
: item.serverTimestamp;
|
||||
envelope.serverTimestamp =
|
||||
item.serverTimestamp || envelope.serverTimestamp;
|
||||
|
||||
if (envelope.serverTimestamp && envelope.serverTimestamp.toNumber) {
|
||||
envelope.serverTimestamp = envelope.serverTimestamp.toNumber();
|
||||
}
|
||||
|
||||
const { decrypted } = item;
|
||||
if (decrypted) {
|
||||
|
@ -947,195 +963,214 @@ class MessageReceiverInner extends EventTarget {
|
|||
async decrypt(
|
||||
envelope: EnvelopeClass,
|
||||
ciphertext: any
|
||||
): Promise<ArrayBuffer> {
|
||||
): Promise<ArrayBuffer | null> {
|
||||
const { serverTrustRoot } = this;
|
||||
|
||||
let promise;
|
||||
const identifier = envelope.sourceUuid || envelope.source;
|
||||
const { sourceDevice } = envelope;
|
||||
|
||||
const address = new window.libsignal.SignalProtocolAddress(
|
||||
// Using source as opposed to sourceUuid allows us to get the existing
|
||||
// session if we haven't yet harvested the incoming uuid
|
||||
identifier as any,
|
||||
envelope.sourceDevice as any
|
||||
const localE164 = window.textsecure.storage.user.getNumber();
|
||||
const localUuid = window.textsecure.storage.user.getUuid();
|
||||
const localDeviceId = parseIntOrThrow(
|
||||
window.textsecure.storage.user.getDeviceId(),
|
||||
'MessageReceiver.decrypt: localDeviceId'
|
||||
);
|
||||
|
||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
||||
const ourUuid = window.textsecure.storage.user.getUuid();
|
||||
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;
|
||||
if (!localUuid) {
|
||||
throw new Error('MessageReceiver.decrypt: Failed to fetch local UUID');
|
||||
}
|
||||
|
||||
const sessionCipher = new window.libsignal.SessionCipher(
|
||||
window.textsecure.storage.protocol,
|
||||
address,
|
||||
options
|
||||
);
|
||||
const secretSessionCipher = new SecretSessionCipher(
|
||||
window.textsecure.storage.protocol,
|
||||
options
|
||||
);
|
||||
const sessionStore = new Sessions();
|
||||
const identityKeyStore = new IdentityKeys();
|
||||
const preKeyStore = new PreKeys();
|
||||
const signedPreKeyStore = new SignedPreKeys();
|
||||
|
||||
const me = {
|
||||
number: ourNumber,
|
||||
uuid: ourUuid,
|
||||
deviceId: parseInt(
|
||||
window.textsecure.storage.user.getDeviceId() as string,
|
||||
10
|
||||
),
|
||||
};
|
||||
let promise: Promise<
|
||||
ArrayBuffer | { isMe: boolean } | { isBlocked: boolean } | undefined
|
||||
>;
|
||||
|
||||
switch (envelope.type) {
|
||||
case window.textsecure.protobuf.Envelope.Type.CIPHERTEXT:
|
||||
window.log.info('message from', this.getEnvelopeId(envelope));
|
||||
promise = sessionCipher
|
||||
.decryptWhisperMessage(ciphertext)
|
||||
.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
|
||||
if (envelope.type === window.textsecure.protobuf.Envelope.Type.CIPHERTEXT) {
|
||||
window.log.info('message from', this.getEnvelopeId(envelope));
|
||||
if (!identifier) {
|
||||
throw new Error(
|
||||
'MessageReceiver.decrypt: No identifier for CIPHERTEXT message'
|
||||
);
|
||||
break;
|
||||
case window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER:
|
||||
window.log.info('received unidentified sender message');
|
||||
promise = secretSessionCipher
|
||||
.decrypt(
|
||||
createCertificateValidator(serverTrustRoot),
|
||||
ciphertext.toArrayBuffer(),
|
||||
Math.min(envelope.serverTimestamp || Date.now(), Date.now()),
|
||||
me
|
||||
)
|
||||
.then(
|
||||
result => {
|
||||
const { isMe, sender, senderUuid, content } = result;
|
||||
}
|
||||
if (!sourceDevice) {
|
||||
throw new Error(
|
||||
'MessageReceiver.decrypt: No sourceDevice for CIPHERTEXT message'
|
||||
);
|
||||
}
|
||||
const signalMessage = SignalMessage.deserialize(
|
||||
Buffer.from(ciphertext.toArrayBuffer())
|
||||
);
|
||||
|
||||
// We need to drop incoming messages from ourself since server can't
|
||||
// do it for us
|
||||
if (isMe) {
|
||||
return { isMe: true };
|
||||
}
|
||||
const address = `${identifier}.${sourceDevice}`;
|
||||
promise = window.textsecure.storage.protocol.enqueueSessionJob(
|
||||
address,
|
||||
() =>
|
||||
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 (
|
||||
(sender && this.isBlocked(sender.getName())) ||
|
||||
(senderUuid && this.isUuidBlocked(senderUuid.getName()))
|
||||
) {
|
||||
window.log.info(
|
||||
'Dropping blocked message after sealed sender decryption'
|
||||
);
|
||||
return { isBlocked: true };
|
||||
}
|
||||
const address = `${identifier}.${sourceDevice}`;
|
||||
promise = window.textsecure.storage.protocol.enqueueSessionJob(
|
||||
address,
|
||||
() =>
|
||||
signalDecryptPreKey(
|
||||
preKeySignalMessage,
|
||||
ProtocolAddress.new(identifier, sourceDevice),
|
||||
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
|
||||
// to make the rest of the app work properly.
|
||||
const decryptSealedSender = async (): Promise<
|
||||
SealedSenderDecryptionResult | null | { isBlocked: true }
|
||||
> => {
|
||||
const messageContent = await sealedSenderDecryptToUsmc(
|
||||
buffer,
|
||||
identityKeyStore
|
||||
);
|
||||
|
||||
const originalSource = envelope.source;
|
||||
const originalSourceUuid = envelope.sourceUuid;
|
||||
// Here we take this sender information and attach it back to the envelope
|
||||
// to make the rest of the app work properly.
|
||||
const certificate = messageContent.senderCertificate();
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
envelope.source = sender && sender.getName();
|
||||
// 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
|
||||
);
|
||||
const originalSource = envelope.source;
|
||||
const originalSourceUuid = envelope.sourceUuid;
|
||||
|
||||
if (!content) {
|
||||
throw new Error(
|
||||
'MessageReceiver.decrypt: Content returned was falsey!'
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
envelope.source = certificate.senderE164() || undefined;
|
||||
// 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
|
||||
// decrypt methods used above.
|
||||
return this.unpad(content);
|
||||
},
|
||||
(error: Error) => {
|
||||
const { sender, senderUuid } = error || {};
|
||||
|
||||
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;
|
||||
}
|
||||
if (
|
||||
(envelope.source && this.isBlocked(envelope.source)) ||
|
||||
(envelope.sourceUuid && this.isUuidBlocked(envelope.sourceUuid))
|
||||
) {
|
||||
window.log.info(
|
||||
'MessageReceiver.decrypt: Dropping blocked message after partial sealed sender decryption'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
promise = Promise.reject(new Error('Unknown message type'));
|
||||
return { isBlocked: true };
|
||||
}
|
||||
|
||||
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
|
||||
.then((plaintext: any) => {
|
||||
const { isMe, isBlocked } = plaintext || {};
|
||||
if (isMe || isBlocked) {
|
||||
this.removeFromCache(envelope);
|
||||
return null;
|
||||
}
|
||||
.then(
|
||||
(
|
||||
plaintext:
|
||||
| ArrayBuffer
|
||||
| { 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
|
||||
// cache has already been deleted by the time this runs. That's okay.
|
||||
try {
|
||||
this.updateCache(envelope, plaintext);
|
||||
} catch (error) {
|
||||
const errorString = error && error.stack ? error.stack : error;
|
||||
window.log.error(`decrypt: updateCache failed: ${errorString}`);
|
||||
}
|
||||
// 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.
|
||||
try {
|
||||
this.updateCache(envelope, plaintext);
|
||||
} catch (error) {
|
||||
const errorString = error && error.stack ? error.stack : error;
|
||||
window.log.error(`decrypt: updateCache failed: ${errorString}`);
|
||||
}
|
||||
|
||||
return plaintext;
|
||||
})
|
||||
return plaintext;
|
||||
}
|
||||
)
|
||||
.catch(async error => {
|
||||
this.removeFromCache(envelope);
|
||||
|
||||
|
@ -1143,7 +1178,10 @@ class MessageReceiverInner extends EventTarget {
|
|||
const deviceId = envelope.sourceDevice;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -1248,43 +1286,15 @@ class MessageReceiverInner extends EventTarget {
|
|||
window.log.warn(`lightSessionReset/${id}: Resetting session`);
|
||||
|
||||
// Archive open session with this device
|
||||
const address = new window.libsignal.SignalProtocolAddress(uuid, deviceId);
|
||||
const sessionCipher = new window.libsignal.SessionCipher(
|
||||
window.textsecure.storage.protocol,
|
||||
address
|
||||
await window.textsecure.storage.protocol.archiveSession(
|
||||
`${uuid}.${deviceId}`
|
||||
);
|
||||
|
||||
await sessionCipher.closeOpenSessionForDevice();
|
||||
|
||||
// Send a null message with newly-created session
|
||||
const sendOptions = await conversation.getSendOptions();
|
||||
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(
|
||||
envelope: EnvelopeClass,
|
||||
sentContainer: SyncMessageClass.Sent
|
||||
|
@ -1866,7 +1876,10 @@ class MessageReceiverInner extends EventTarget {
|
|||
}
|
||||
|
||||
this.removeFromCache(envelope);
|
||||
throw new Error('Got empty SyncMessage');
|
||||
window.log.warn(
|
||||
`handleSyncMessage/${this.getEnvelopeId(envelope)}: Got empty SyncMessage`
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async handleConfiguration(
|
||||
|
@ -2213,29 +2226,8 @@ class MessageReceiverInner extends EventTarget {
|
|||
}
|
||||
|
||||
async handleEndSession(identifier: string) {
|
||||
window.log.info('got end session');
|
||||
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
|
||||
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();
|
||||
})
|
||||
);
|
||||
window.log.info(`handleEndSession: closing sessions for ${identifier}`);
|
||||
await window.textsecure.storage.protocol.archiveAllSessions(identifier);
|
||||
}
|
||||
|
||||
async processDecrypted(envelope: EnvelopeClass, decrypted: DataMessageClass) {
|
||||
|
|
|
@ -9,9 +9,21 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
|
||||
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 { isEnabled as isRemoteFlagEnabled } from '../RemoteConfig';
|
||||
import { SignalProtocolAddressClass } from '../libsignal.d';
|
||||
import { ContentClass, DataMessageClass } from '../textsecure.d';
|
||||
import {
|
||||
CallbackResultType,
|
||||
|
@ -26,12 +38,40 @@ import {
|
|||
UnregisteredUserError,
|
||||
} from './Errors';
|
||||
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 & {
|
||||
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 {
|
||||
server: WebAPIType;
|
||||
|
||||
|
@ -142,7 +182,7 @@ export default class OutgoingMessage {
|
|||
if (deviceIds.length === 0) {
|
||||
this.registerError(
|
||||
identifier,
|
||||
'Got empty device list when loading device keys',
|
||||
'reloadDevicesAndSend: Got empty device list when loading device keys',
|
||||
undefined
|
||||
);
|
||||
return undefined;
|
||||
|
@ -153,44 +193,76 @@ export default class OutgoingMessage {
|
|||
|
||||
async getKeysForIdentifier(
|
||||
identifier: string,
|
||||
updateDevices: Array<number>
|
||||
updateDevices: Array<number> | undefined
|
||||
): Promise<void | Array<void | null>> {
|
||||
const handleResult = async (response: ServerKeysType) =>
|
||||
Promise.all(
|
||||
const handleResult = async (response: ServerKeysType) => {
|
||||
const sessionStore = new Sessions();
|
||||
const identityKeyStore = new IdentityKeys();
|
||||
|
||||
return Promise.all(
|
||||
response.devices.map(async device => {
|
||||
const { deviceId, registrationId, preKey, signedPreKey } = device;
|
||||
if (
|
||||
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) {
|
||||
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 = {
|
||||
...device,
|
||||
identityKey: response.identityKey,
|
||||
};
|
||||
return builder.processPreKey(deviceForProcess).catch(error => {
|
||||
if (error.message === 'Identity key changed') {
|
||||
error.timestamp = this.timestamp;
|
||||
error.originalMessage = this.message.toArrayBuffer();
|
||||
error.identityKey = response.identityKey;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
const preKeyBundle = PreKeyBundle.new(
|
||||
registrationId,
|
||||
deviceId,
|
||||
preKeyId,
|
||||
preKeyObject,
|
||||
signedPreKey.keyId,
|
||||
signedPreKeyObject,
|
||||
Buffer.from(signedPreKey.signature),
|
||||
identityKey
|
||||
);
|
||||
|
||||
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;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const { sendMetadata } = this;
|
||||
const info =
|
||||
|
@ -329,13 +401,6 @@ export default class OutgoingMessage {
|
|||
deviceIds: Array<number>,
|
||||
recurse?: boolean
|
||||
): Promise<void> {
|
||||
const ciphers: {
|
||||
[key: number]: {
|
||||
closeOpenSessionForDevice: (
|
||||
address: SignalProtocolAddressClass
|
||||
) => Promise<void>;
|
||||
};
|
||||
} = {};
|
||||
const plaintext = this.getPlaintext();
|
||||
|
||||
const { sendMetadata } = this;
|
||||
|
@ -364,54 +429,59 @@ export default class OutgoingMessage {
|
|||
);
|
||||
}
|
||||
|
||||
const sessionStore = new Sessions();
|
||||
const identityKeyStore = new IdentityKeys();
|
||||
|
||||
return Promise.all(
|
||||
deviceIds.map(async deviceId => {
|
||||
const address = new window.libsignal.SignalProtocolAddress(
|
||||
deviceIds.map(async destinationDeviceId => {
|
||||
const protocolAddress = ProtocolAddress.new(
|
||||
identifier,
|
||||
deviceId
|
||||
destinationDeviceId
|
||||
);
|
||||
|
||||
const options: any = {};
|
||||
|
||||
// No limit on message keys if we're communicating with our other devices
|
||||
if (ourNumber === identifier || ourUuid === identifier) {
|
||||
options.messageKeysLimit = false;
|
||||
const activeSession = await sessionStore.getSession(protocolAddress);
|
||||
if (!activeSession) {
|
||||
throw new Error('OutgoingMessage.doSendMessage: No active sesssion!');
|
||||
}
|
||||
|
||||
if (sealedSender && senderCertificate) {
|
||||
const secretSessionCipher = new SecretSessionCipher(
|
||||
window.textsecure.storage.protocol
|
||||
);
|
||||
ciphers[address.getDeviceId()] = secretSessionCipher;
|
||||
const destinationRegistrationId = activeSession.remoteRegistrationId();
|
||||
|
||||
const ciphertext = await secretSessionCipher.encrypt(
|
||||
address,
|
||||
senderCertificate,
|
||||
plaintext
|
||||
if (sealedSender && senderCertificate) {
|
||||
const certificate = SenderCertificate.deserialize(
|
||||
Buffer.from(senderCertificate.serialized)
|
||||
);
|
||||
|
||||
const buffer = await sealedSenderEncryptMessage(
|
||||
Buffer.from(plaintext),
|
||||
protocolAddress,
|
||||
certificate,
|
||||
sessionStore,
|
||||
identityKeyStore
|
||||
);
|
||||
|
||||
return {
|
||||
type: window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
|
||||
destinationDeviceId: address.getDeviceId(),
|
||||
destinationRegistrationId: await secretSessionCipher.getRemoteRegistrationId(
|
||||
address
|
||||
),
|
||||
content: window.Signal.Crypto.arrayBufferToBase64(ciphertext),
|
||||
destinationDeviceId,
|
||||
destinationRegistrationId,
|
||||
content: buffer.toString('base64'),
|
||||
};
|
||||
}
|
||||
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 {
|
||||
type: ciphertext.type,
|
||||
destinationDeviceId: address.getDeviceId(),
|
||||
destinationRegistrationId: ciphertext.registrationId,
|
||||
content: btoa(ciphertext.body),
|
||||
type,
|
||||
destinationDeviceId,
|
||||
destinationRegistrationId,
|
||||
content: ciphertextMessage.serialize().toString('base64'),
|
||||
};
|
||||
})
|
||||
)
|
||||
|
@ -474,14 +544,11 @@ export default class OutgoingMessage {
|
|||
);
|
||||
} else {
|
||||
p = Promise.all(
|
||||
error.response.staleDevices.map(async (deviceId: number) =>
|
||||
ciphers[deviceId].closeOpenSessionForDevice(
|
||||
new window.libsignal.SignalProtocolAddress(
|
||||
identifier,
|
||||
deviceId
|
||||
)
|
||||
)
|
||||
)
|
||||
error.response.staleDevices.map(async (deviceId: number) => {
|
||||
await window.textsecure.storage.protocol.archiveSession(
|
||||
`${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
|
||||
error.timestamp = this.timestamp;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
|
@ -509,34 +576,19 @@ export default class OutgoingMessage {
|
|||
);
|
||||
|
||||
window.log.info('closing all sessions for', identifier);
|
||||
const address = new window.libsignal.SignalProtocolAddress(
|
||||
identifier,
|
||||
1
|
||||
);
|
||||
|
||||
const sessionCipher = new window.libsignal.SessionCipher(
|
||||
window.textsecure.storage.protocol,
|
||||
address
|
||||
);
|
||||
window.log.info('closing session for', address.toString());
|
||||
return Promise.all([
|
||||
// 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;
|
||||
}
|
||||
);
|
||||
window.textsecure.storage.protocol
|
||||
.archiveAllSessions(identifier)
|
||||
.then(
|
||||
() => {
|
||||
throw error;
|
||||
},
|
||||
innerError => {
|
||||
window.log.error(
|
||||
`doSendMessage: Error closing sessions: ${innerError.stack}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this.registerError(
|
||||
|
@ -551,32 +603,30 @@ export default class OutgoingMessage {
|
|||
|
||||
async getStaleDeviceIdsForIdentifier(
|
||||
identifier: string
|
||||
): Promise<Array<number>> {
|
||||
return window.textsecure.storage.protocol
|
||||
.getDeviceIds(identifier)
|
||||
.then(async deviceIds => {
|
||||
if (deviceIds.length === 0) {
|
||||
return [1];
|
||||
): Promise<Array<number> | undefined> {
|
||||
const sessionStore = new Sessions();
|
||||
|
||||
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
|
||||
identifier
|
||||
);
|
||||
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(
|
||||
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);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return updateDevices;
|
||||
}
|
||||
|
||||
async removeDeviceIdsForIdentifier(
|
||||
|
@ -585,15 +635,9 @@ export default class OutgoingMessage {
|
|||
): Promise<void> {
|
||||
await Promise.all(
|
||||
deviceIdsToRemove.map(async deviceId => {
|
||||
const address = new window.libsignal.SignalProtocolAddress(
|
||||
identifier,
|
||||
deviceId
|
||||
await window.textsecure.storage.protocol.archiveSession(
|
||||
`${identifier}.${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.reloadDevicesAndSend(identifier, true)();
|
||||
} catch (error) {
|
||||
if (error.message === 'Identity key changed') {
|
||||
if (error?.message?.includes('untrusted identity for address')) {
|
||||
const newError = new OutgoingIdentityKeyError(
|
||||
identifier,
|
||||
error.originalMessage,
|
||||
error.timestamp,
|
||||
error.identityKey
|
||||
);
|
||||
this.registerError(identifier, 'Identity key changed', newError);
|
||||
this.registerError(identifier, 'Untrusted identity', newError);
|
||||
} else {
|
||||
this.registerError(
|
||||
identifier,
|
||||
|
|
|
@ -4,8 +4,15 @@
|
|||
/* eslint-disable more/no-then */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { KeyPairType } from '../libsignal.d';
|
||||
import { KeyPairType } from './Types.d';
|
||||
import { ProvisionEnvelopeClass } from '../textsecure.d';
|
||||
import {
|
||||
decryptAes256CbcPkcsPadding,
|
||||
deriveSecrets,
|
||||
bytesFromString,
|
||||
verifyHmacSha256,
|
||||
} from '../Crypto';
|
||||
import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve';
|
||||
|
||||
type ProvisionDecryptResult = {
|
||||
identityKeyPair: KeyPairType;
|
||||
|
@ -38,73 +45,55 @@ class ProvisioningCipherInner {
|
|||
throw new Error('ProvisioningCipher.decrypt: No keypair!');
|
||||
}
|
||||
|
||||
return window.libsignal.Curve.async
|
||||
.calculateAgreement(masterEphemeral, this.keyPair.privKey)
|
||||
.then(async ecRes =>
|
||||
window.libsignal.HKDF.deriveSecrets(
|
||||
ecRes,
|
||||
new ArrayBuffer(32),
|
||||
'TextSecure Provisioning Message'
|
||||
)
|
||||
)
|
||||
.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();
|
||||
const ecRes = calculateAgreement(masterEphemeral, this.keyPair.privKey);
|
||||
const keys = deriveSecrets(
|
||||
ecRes,
|
||||
new ArrayBuffer(32),
|
||||
bytesFromString('TextSecure Provisioning Message')
|
||||
);
|
||||
await verifyHmacSha256(ivAndCiphertext, keys[1], mac, 32);
|
||||
|
||||
return window.libsignal.Curve.async
|
||||
.createKeyPair(privKey)
|
||||
.then(keyPair => {
|
||||
window.normalizeUuids(
|
||||
provisionMessage,
|
||||
['uuid'],
|
||||
'ProvisioningCipher.decrypt'
|
||||
);
|
||||
const plaintext = await decryptAes256CbcPkcsPadding(
|
||||
keys[0],
|
||||
ciphertext,
|
||||
iv
|
||||
);
|
||||
const provisionMessage = window.textsecure.protobuf.ProvisionMessage.decode(
|
||||
plaintext
|
||||
);
|
||||
const privKey = provisionMessage.identityKeyPrivate.toArrayBuffer();
|
||||
|
||||
const ret: ProvisionDecryptResult = {
|
||||
identityKeyPair: keyPair,
|
||||
number: provisionMessage.number,
|
||||
uuid: provisionMessage.uuid,
|
||||
provisioningCode: provisionMessage.provisioningCode,
|
||||
userAgent: provisionMessage.userAgent,
|
||||
readReceipts: provisionMessage.readReceipts,
|
||||
};
|
||||
if (provisionMessage.profileKey) {
|
||||
ret.profileKey = provisionMessage.profileKey.toArrayBuffer();
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
});
|
||||
const keyPair = createKeyPair(privKey);
|
||||
window.normalizeUuids(
|
||||
provisionMessage,
|
||||
['uuid'],
|
||||
'ProvisioningCipher.decrypt'
|
||||
);
|
||||
|
||||
const ret: ProvisionDecryptResult = {
|
||||
identityKeyPair: keyPair,
|
||||
number: provisionMessage.number,
|
||||
uuid: provisionMessage.uuid,
|
||||
provisioningCode: provisionMessage.provisioningCode,
|
||||
userAgent: provisionMessage.userAgent,
|
||||
readReceipts: provisionMessage.readReceipts,
|
||||
};
|
||||
if (provisionMessage.profileKey) {
|
||||
ret.profileKey = provisionMessage.profileKey.toArrayBuffer();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
async getPublicKey(): Promise<ArrayBuffer> {
|
||||
return Promise.resolve()
|
||||
.then(async () => {
|
||||
if (!this.keyPair) {
|
||||
return window.libsignal.Curve.async
|
||||
.generateKeyPair()
|
||||
.then(keyPair => {
|
||||
this.keyPair = keyPair;
|
||||
});
|
||||
}
|
||||
if (!this.keyPair) {
|
||||
this.keyPair = generateKeyPair();
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.then(() => {
|
||||
if (!this.keyPair) {
|
||||
throw new Error('ProvisioningCipher.decrypt: No keypair!');
|
||||
}
|
||||
if (!this.keyPair) {
|
||||
throw new Error('ProvisioningCipher.decrypt: No keypair!');
|
||||
}
|
||||
|
||||
return this.keyPair.pubKey;
|
||||
});
|
||||
return this.keyPair.pubKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,11 +19,12 @@ import {
|
|||
WebAPIType,
|
||||
} from './WebAPI';
|
||||
import createTaskWithTimeout from './TaskWithTimeout';
|
||||
import OutgoingMessage from './OutgoingMessage';
|
||||
import OutgoingMessage, { SerializedCertificateType } from './OutgoingMessage';
|
||||
import Crypto from './Crypto';
|
||||
import {
|
||||
base64ToArrayBuffer,
|
||||
concatenateBytes,
|
||||
getRandomBytes,
|
||||
getZeroes,
|
||||
hexToArrayBuffer,
|
||||
} from '../Crypto';
|
||||
|
@ -46,7 +47,6 @@ import {
|
|||
LinkPreviewImage,
|
||||
LinkPreviewMetadata,
|
||||
} from '../linkPreviews/linkPreviewFetch';
|
||||
import { SerializedCertificateType } from '../metadata/SecretSessionCipher';
|
||||
|
||||
function stringToArrayBuffer(str: string): ArrayBuffer {
|
||||
if (typeof str !== 'string') {
|
||||
|
@ -473,8 +473,8 @@ export default class MessageSender {
|
|||
}
|
||||
|
||||
const padded = this.getPaddedAttachment(data);
|
||||
const key = window.libsignal.crypto.getRandomBytes(64);
|
||||
const iv = window.libsignal.crypto.getRandomBytes(16);
|
||||
const key = getRandomBytes(64);
|
||||
const iv = getRandomBytes(16);
|
||||
|
||||
const result = await Crypto.encryptAttachment(padded, key, iv);
|
||||
const id = await this.server.putAttachment(result.ciphertext);
|
||||
|
@ -1368,11 +1368,11 @@ export default class MessageSender {
|
|||
|
||||
getRandomPadding(): ArrayBuffer {
|
||||
// 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;
|
||||
|
||||
// Generate a random padding buffer of the chosen size
|
||||
return window.libsignal.crypto.getRandomBytes(paddingLength);
|
||||
return getRandomBytes(paddingLength);
|
||||
}
|
||||
|
||||
async sendNullMessage(
|
||||
|
@ -1607,31 +1607,10 @@ export default class MessageSender {
|
|||
window.log.error(prefix, error && error.stack ? error.stack : 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)
|
||||
.catch(logError('resetSession/closeAllSessions1 error:'))
|
||||
const sendToContactPromise = window.textsecure.storage.protocol
|
||||
.archiveAllSessions(identifier)
|
||||
.catch(logError('resetSession/archiveAllSessions1 error:'))
|
||||
.then(async () => {
|
||||
window.log.info(
|
||||
'resetSession: finished closing local sessions, now sending to contact'
|
||||
|
@ -1645,9 +1624,9 @@ export default class MessageSender {
|
|||
).catch(logError('resetSession/sendToContact error:'));
|
||||
})
|
||||
.then(async () =>
|
||||
closeAllSessions(identifier).catch(
|
||||
logError('resetSession/closeAllSessions2 error:')
|
||||
)
|
||||
window.textsecure.storage.protocol
|
||||
.archiveAllSessions(identifier)
|
||||
.catch(logError('resetSession/archiveAllSessions2 error:'))
|
||||
);
|
||||
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
|
|
40
ts/textsecure/Types.d.ts
vendored
Normal file
40
ts/textsecure/Types.d.ts
vendored
Normal 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;
|
||||
};
|
|
@ -39,11 +39,13 @@ import {
|
|||
concatenateBytes,
|
||||
constantTimeEqual,
|
||||
decryptAesGcm,
|
||||
deriveSecrets,
|
||||
encryptCdsDiscoveryRequest,
|
||||
getBytes,
|
||||
getRandomValue,
|
||||
splitUuids,
|
||||
} from '../Crypto';
|
||||
import { calculateAgreement, generateKeyPair } from '../Curve';
|
||||
import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch';
|
||||
|
||||
import {
|
||||
|
@ -2406,7 +2408,7 @@ export function initialize({
|
|||
username: string;
|
||||
password: string;
|
||||
}) {
|
||||
const keyPair = await window.libsignal.externalCurveAsync.generateKeyPair();
|
||||
const keyPair = generateKeyPair();
|
||||
const { privKey, pubKey } = keyPair;
|
||||
// Remove first "key type" byte from public key
|
||||
const slicedPubKey = pubKey.slice(1);
|
||||
|
@ -2476,11 +2478,11 @@ export function initialize({
|
|||
);
|
||||
|
||||
// Derive key
|
||||
const ephemeralToEphemeral = await window.libsignal.externalCurveAsync.calculateAgreement(
|
||||
const ephemeralToEphemeral = calculateAgreement(
|
||||
decoded.serverEphemeralPublic,
|
||||
privKey
|
||||
);
|
||||
const ephemeralToStatic = await window.libsignal.externalCurveAsync.calculateAgreement(
|
||||
const ephemeralToStatic = calculateAgreement(
|
||||
decoded.serverStaticPublic,
|
||||
privKey
|
||||
);
|
||||
|
@ -2493,10 +2495,7 @@ export function initialize({
|
|||
decoded.serverEphemeralPublic,
|
||||
decoded.serverStaticPublic
|
||||
);
|
||||
const [
|
||||
clientKey,
|
||||
serverKey,
|
||||
] = await window.libsignal.HKDF.deriveSecrets(
|
||||
const [clientKey, serverKey] = await deriveSecrets(
|
||||
masterSecret,
|
||||
publicKeys,
|
||||
new ArrayBuffer(0)
|
||||
|
|
|
@ -15940,22 +15940,6 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"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",
|
||||
"path": "ts/backbone/views/Lightbox.js",
|
||||
|
@ -16925,150 +16909,6 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"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",
|
||||
"path": "ts/util/hooks.js",
|
||||
|
|
|
@ -65,11 +65,14 @@ const excludedFilesRegexps = [
|
|||
'^ts/textsecure/MessageReceiver.ts',
|
||||
'^ts/ConversationController.js',
|
||||
'^ts/ConversationController.ts',
|
||||
'^ts/SignalProtocolStore.ts',
|
||||
'^ts/SignalProtocolStore.js',
|
||||
'^ts/textsecure/[^./]+.ts',
|
||||
'^ts/textsecure/[^./]+.js',
|
||||
|
||||
// Generated files
|
||||
'^js/components.js',
|
||||
'^js/curve/',
|
||||
'^js/libtextsecure.js',
|
||||
'^js/util_worker.js',
|
||||
'^libtextsecure/components.js',
|
||||
'^libtextsecure/test/test.js',
|
||||
|
@ -77,9 +80,6 @@ const excludedFilesRegexps = [
|
|||
'^test/test.js',
|
||||
'^ts/test[^/]*/.+',
|
||||
|
||||
// From libsignal-protocol-javascript project
|
||||
'^libtextsecure/libsignal-protocol.js',
|
||||
|
||||
// Copied from dependency
|
||||
'^js/Mp3LameEncoder.min.js',
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { get, isFinite, isInteger, isString } from 'lodash';
|
||||
import { HKDF } from 'libsignal-client';
|
||||
|
||||
import { signal } from '../protobuf/compiled';
|
||||
import {
|
||||
bytesFromString,
|
||||
deriveSecrets,
|
||||
fromEncodedBinaryToArrayBuffer,
|
||||
typedArrayToArrayBuffer,
|
||||
} from '../Crypto';
|
||||
|
@ -142,7 +142,8 @@ function toProtobufSession(
|
|||
proto.localIdentityPublic = new Uint8Array(ourData.identityKeyPublic);
|
||||
proto.localRegistrationId = ourData.registrationId;
|
||||
|
||||
proto.previousCounter = getInteger(session, 'currentRatchet.previousCounter');
|
||||
proto.previousCounter =
|
||||
getInteger(session, 'currentRatchet.previousCounter') + 1;
|
||||
proto.remoteIdentityPublic = binaryToUint8Array(
|
||||
session,
|
||||
'indexInfo.remoteIdentityKey',
|
||||
|
@ -150,7 +151,7 @@ function toProtobufSession(
|
|||
);
|
||||
proto.remoteRegistrationId = getInteger(session, 'registrationId');
|
||||
proto.rootKey = binaryToUint8Array(session, 'currentRatchet.rootKey', 32);
|
||||
proto.sessionVersion = 1;
|
||||
proto.sessionVersion = 3;
|
||||
|
||||
// Note: currently unused
|
||||
// proto.needsRefresh = null;
|
||||
|
@ -291,7 +292,7 @@ function toProtobufChain(
|
|||
const proto = new Chain();
|
||||
|
||||
const protoChainKey = new Chain.ChainKey();
|
||||
protoChainKey.index = getInteger(chain, 'chainKey.counter');
|
||||
protoChainKey.index = getInteger(chain, 'chainKey.counter') + 1;
|
||||
if (chain.chainKey?.key !== undefined) {
|
||||
protoChainKey.key = binaryToUint8Array(chain, 'chainKey.key', 32);
|
||||
}
|
||||
|
@ -300,7 +301,7 @@ function toProtobufChain(
|
|||
const messageKeys = Object.entries(chain.messageKeys || {});
|
||||
proto.messageKeys = messageKeys.map(entry => {
|
||||
const protoMessageKey = new SessionStructure.Chain.MessageKey();
|
||||
protoMessageKey.index = getInteger(entry, '0');
|
||||
protoMessageKey.index = getInteger(entry, '0') + 1;
|
||||
const key = binaryToUint8Array(entry, '1', 32);
|
||||
|
||||
const { cipherKey, macKey, iv } = translateMessageKey(key);
|
||||
|
@ -319,25 +320,6 @@ function toProtobufChain(
|
|||
|
||||
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) {
|
||||
const input = key.buffer;
|
||||
const salt = new ArrayBuffer(32);
|
||||
|
|
45
ts/window.d.ts
vendored
45
ts/window.d.ts
vendored
|
@ -17,17 +17,13 @@ import {
|
|||
MessageModelCollectionType,
|
||||
MessageAttributesType,
|
||||
} from './model-types.d';
|
||||
import {
|
||||
LibSignalType,
|
||||
SignalProtocolAddressClass,
|
||||
StorageType,
|
||||
} from './libsignal.d';
|
||||
import { ContactRecordIdentityState, TextSecureType } from './textsecure.d';
|
||||
import { WebAPIConnectType } from './textsecure/WebAPI';
|
||||
import { uploadDebugLogs } from './logging/debuglogs';
|
||||
import { CallingClass } from './services/calling';
|
||||
import * as Groups from './groups';
|
||||
import * as Crypto from './Crypto';
|
||||
import * as Curve from './Curve';
|
||||
import * as RemoteConfig from './RemoteConfig';
|
||||
import * as OS from './OS';
|
||||
import { getEnvironment } from './environment';
|
||||
|
@ -96,7 +92,7 @@ import { Quote } from './components/conversation/Quote';
|
|||
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
|
||||
import { MIMEType } from './types/MIME';
|
||||
import { ElectronLocaleType } from './util/mapToSupportLocale';
|
||||
import { SignalProtocolStore } from './LibSignalStore';
|
||||
import { SignalProtocolStore } from './SignalProtocolStore';
|
||||
import { StartupQueue } from './util/StartupQueue';
|
||||
import * as synchronousCrypto from './util/synchronousCrypto';
|
||||
import SyncRequest from './textsecure/SyncRequest';
|
||||
|
@ -196,7 +192,6 @@ declare global {
|
|||
getRegionCodeForNumber: (number: string) => string;
|
||||
format: (number: string, format: PhoneNumberFormat) => string;
|
||||
};
|
||||
libsignal: LibSignalType;
|
||||
log: {
|
||||
fatal: LoggerType;
|
||||
info: LoggerType;
|
||||
|
@ -279,14 +274,9 @@ declare global {
|
|||
stop: () => void;
|
||||
};
|
||||
Crypto: typeof Crypto;
|
||||
Curve: typeof Curve;
|
||||
Data: typeof Data;
|
||||
Groups: typeof Groups;
|
||||
Metadata: {
|
||||
SecretSessionCipher: typeof SecretSessionCipherClass;
|
||||
createCertificateValidator: (
|
||||
trustRoot: ArrayBuffer
|
||||
) => CertificateValidatorType;
|
||||
};
|
||||
RemoteConfig: typeof RemoteConfig;
|
||||
Services: {
|
||||
calling: CallingClass;
|
||||
|
@ -599,35 +589,6 @@ export class CertificateValidatorType {
|
|||
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 {
|
||||
constructor(value?: any, littleEndian?: number);
|
||||
static wrap: (
|
||||
|
|
|
@ -10928,9 +10928,9 @@ levn@~0.3.0:
|
|||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
|
||||
"libsignal-client@https://github.com/signalapp/libsignal-client-node.git#f10fbd04eb6efb396eb12c25429761e8785dc9d0":
|
||||
version "0.3.0"
|
||||
resolved "https://github.com/signalapp/libsignal-client-node.git#f10fbd04eb6efb396eb12c25429761e8785dc9d0"
|
||||
"libsignal-client@https://github.com/signalapp/libsignal-client-node.git#afa8f40eaa218b7fff94278d9b2ab2e13a2ee04b":
|
||||
version "0.3.3"
|
||||
resolved "https://github.com/signalapp/libsignal-client-node.git#afa8f40eaa218b7fff94278d9b2ab2e13a2ee04b"
|
||||
dependencies:
|
||||
bindings "^1.5.0"
|
||||
|
||||
|
|
Loading…
Reference in a new issue