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

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

View file

@ -7,7 +7,6 @@ release/**
# Generated files
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
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = {

View file

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

View file

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

View file

@ -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', () => {

View file

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

View file

@ -21,7 +21,6 @@
<script type="text/javascript" src="in_memory_signal_protocol_store.js"></script>
<script type="text/javascript" src="../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>

View file

@ -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',
{

View file

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

View file

@ -1,18 +1,18 @@
// Copyright 2015-2020 Signal Messenger, LLC
// 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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
View file

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

157
ts/LibSignalStores.ts Normal file
View file

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

File diff suppressed because it is too large Load diff

229
ts/libsignal.d.ts vendored
View file

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

View file

@ -11,6 +11,8 @@ import * as path from 'path';
import pino from 'pino';
import { 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})`);
}
}
);

View file

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

View file

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

View file

@ -57,7 +57,7 @@ import {
import {
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');

View file

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

View file

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

View file

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

View file

@ -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: (

View file

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

View file

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

View file

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

View file

@ -1,34 +1,92 @@
// Copyright 2015-2020 Signal Messenger, LLC
// Copyright 2015-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* 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();

View file

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

View file

@ -11,7 +11,7 @@ import * as sinon from 'sinon';
import { v4 as uuid } from 'uuid';
import { 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
View file

@ -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 = {

View file

@ -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(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
View file

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

View file

@ -39,11 +39,13 @@ import {
concatenateBytes,
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)

View file

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

View file

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

View file

@ -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
View file

@ -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: (

View file

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