UUID-keyed lookups in SignalProtocolStore

This commit is contained in:
Fedor Indutny 2021-09-09 19:38:11 -07:00 committed by GitHub
parent 6323aedd9b
commit c7e7d55af4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 2094 additions and 1447 deletions

View file

@ -2,10 +2,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { v4 as getGuid } from 'uuid';
import { getRandomBytes } from '../../Crypto';
import AccountManager from '../../textsecure/AccountManager';
import { OuterSignedPrekeyType } from '../../textsecure/Types.d';
import { UUID } from '../../types/UUID';
/* eslint-disable @typescript-eslint/no-explicit-any */
@ -21,15 +23,18 @@ describe('AccountManager', () => {
let originalGetIdentityKeyPair: any;
let originalLoadSignedPreKeys: any;
let originalRemoveSignedPreKey: any;
let originalGetUuid: any;
let signedPreKeys: Array<OuterSignedPrekeyType>;
const DAY = 1000 * 60 * 60 * 24;
const pubKey = getRandomBytes(33);
const privKey = getRandomBytes(32);
const identityKey = window.Signal.Curve.generateKeyPair();
beforeEach(async () => {
const identityKey = window.Signal.Curve.generateKeyPair();
const ourUuid = new UUID(getGuid());
originalGetUuid = window.textsecure.storage.user.getUuid;
originalGetIdentityKeyPair =
window.textsecure.storage.protocol.getIdentityKeyPair;
originalLoadSignedPreKeys =
@ -37,12 +42,15 @@ describe('AccountManager', () => {
originalRemoveSignedPreKey =
window.textsecure.storage.protocol.removeSignedPreKey;
window.textsecure.storage.user.getUuid = () => ourUuid;
window.textsecure.storage.protocol.getIdentityKeyPair = async () =>
identityKey;
window.textsecure.storage.protocol.loadSignedPreKeys = async () =>
signedPreKeys;
});
afterEach(() => {
window.textsecure.storage.user.getUuid = originalGetUuid;
window.textsecure.storage.protocol.getIdentityKeyPair = originalGetIdentityKeyPair;
window.textsecure.storage.protocol.loadSignedPreKeys = originalLoadSignedPreKeys;
window.textsecure.storage.protocol.removeSignedPreKey = originalRemoveSignedPreKey;
@ -51,7 +59,10 @@ describe('AccountManager', () => {
describe('encrypted device name', () => {
it('roundtrips', async () => {
const deviceName = 'v2.5.0 on Ubunto 20.04';
const encrypted = await accountManager.encryptDeviceName(deviceName);
const encrypted = await accountManager.encryptDeviceName(
deviceName,
identityKey
);
if (!encrypted) {
throw new Error('failed to encrypt!');
}
@ -62,7 +73,10 @@ describe('AccountManager', () => {
});
it('handles falsey deviceName', async () => {
const encrypted = await accountManager.encryptDeviceName('');
const encrypted = await accountManager.encryptDeviceName(
'',
identityKey
);
assert.strictEqual(encrypted, null);
});
});
@ -146,7 +160,10 @@ describe('AccountManager', () => {
];
let count = 0;
window.textsecure.storage.protocol.removeSignedPreKey = async keyId => {
window.textsecure.storage.protocol.removeSignedPreKey = async (
_,
keyId
) => {
if (keyId !== 4) {
throw new Error(`Wrong keys were eliminated! ${keyId}`);
}

View file

@ -0,0 +1,129 @@
// Copyright 2017-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { v4 as getGuid } from 'uuid';
import { getRandomBytes } from '../../Crypto';
import { Address } from '../../types/Address';
import { UUID } from '../../types/UUID';
import { SignalProtocolStore } from '../../SignalProtocolStore';
import type { ConversationModel } from '../../models/conversations';
import * as KeyChangeListener from '../../textsecure/KeyChangeListener';
const { Whisper } = window;
describe('KeyChangeListener', () => {
let oldNumberId: string | undefined;
let oldUuidId: string | undefined;
const ourUuid = getGuid();
const uuidWithKeyChange = getGuid();
const address = Address.create(uuidWithKeyChange, 1);
const oldKey = getRandomBytes(33);
const newKey = getRandomBytes(33);
let store: SignalProtocolStore;
before(async () => {
window.ConversationController.reset();
await window.ConversationController.load();
const { storage } = window.textsecure;
oldNumberId = storage.get('number_id');
oldUuidId = storage.get('uuid_id');
await storage.put('number_id', '+14155555556.2');
await storage.put('uuid_id', `${ourUuid}.2`);
});
after(async () => {
await window.Signal.Data.removeAll();
const { storage } = window.textsecure;
await storage.fetch();
if (oldNumberId) {
await storage.put('number_id', oldNumberId);
}
if (oldUuidId) {
await storage.put('uuid_id', oldUuidId);
}
});
let convo: ConversationModel;
beforeEach(async () => {
window.ConversationController.reset();
await window.ConversationController.load();
await window.ConversationController.loadPromise();
convo = window.ConversationController.dangerouslyCreateAndAdd({
id: uuidWithKeyChange,
type: 'private',
});
await window.Signal.Data.saveConversation(convo.attributes);
store = new SignalProtocolStore();
await store.hydrateCaches();
KeyChangeListener.init(store);
return store.saveIdentity(address, oldKey);
});
afterEach(async () => {
await window.Signal.Data.removeAllMessagesInConversation(convo.id, {
logId: uuidWithKeyChange,
MessageCollection: Whisper.MessageCollection,
});
await window.Signal.Data.removeConversation(convo.id, {
Conversation: Whisper.Conversation,
});
await store.removeIdentityKey(new UUID(uuidWithKeyChange));
});
describe('When we have a conversation with this contact', () => {
it('generates a key change notice in the private conversation with this contact', done => {
const original = convo.addKeyChange;
convo.addKeyChange = async keyChangedId => {
assert.equal(uuidWithKeyChange, keyChangedId.toString());
convo.addKeyChange = original;
done();
};
store.saveIdentity(address, newKey);
});
});
describe('When we have a group with this contact', () => {
let groupConvo: ConversationModel;
beforeEach(async () => {
groupConvo = window.ConversationController.dangerouslyCreateAndAdd({
id: 'groupId',
type: 'group',
members: [convo.id],
});
await window.Signal.Data.saveConversation(groupConvo.attributes);
});
afterEach(async () => {
await window.Signal.Data.removeAllMessagesInConversation(groupConvo.id, {
logId: uuidWithKeyChange,
MessageCollection: Whisper.MessageCollection,
});
await window.Signal.Data.removeConversation(groupConvo.id, {
Conversation: Whisper.Conversation,
});
});
it('generates a key change notice in the group conversation with this contact', done => {
const original = groupConvo.addKeyChange;
groupConvo.addKeyChange = async keyChangedId => {
assert.equal(uuidWithKeyChange, keyChangedId.toString());
groupConvo.addKeyChange = original;
done();
};
store.saveIdentity(address, newKey);
});
});
});

View file

@ -0,0 +1,198 @@
// Copyright 2015-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import {
typedArrayToArrayBuffer as toArrayBuffer,
arrayBufferToBase64 as toBase64,
constantTimeEqual,
} from '../../Crypto';
import { generateKeyPair } from '../../Curve';
import AccountManager, {
GeneratedKeysType,
} from '../../textsecure/AccountManager';
import { PreKeyType, SignedPreKeyType } from '../../textsecure/Types.d';
import { UUID } from '../../types/UUID';
const { textsecure } = window;
const assertEqualArrayBuffers = (a: ArrayBuffer, b: ArrayBuffer) => {
assert.isTrue(constantTimeEqual(a, b));
};
describe('Key generation', function thisNeeded() {
const count = 10;
const ourUuid = new UUID('aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee');
this.timeout(count * 2000);
function itStoresPreKey(keyId: number): void {
it(`prekey ${keyId} is valid`, async () => {
const keyPair = await textsecure.storage.protocol.loadPreKey(
ourUuid,
keyId
);
assert(keyPair, `PreKey ${keyId} not found`);
});
}
function itStoresSignedPreKey(keyId: number): void {
it(`signed prekey ${keyId} is valid`, async () => {
const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
ourUuid,
keyId
);
assert(keyPair, `SignedPreKey ${keyId} not found`);
});
}
async function validateResultKey(
resultKey: Pick<PreKeyType, 'keyId' | 'publicKey'>
): Promise<void> {
const keyPair = await textsecure.storage.protocol.loadPreKey(
ourUuid,
resultKey.keyId
);
if (!keyPair) {
throw new Error(`PreKey ${resultKey.keyId} not found`);
}
assertEqualArrayBuffers(
resultKey.publicKey,
toArrayBuffer(keyPair.publicKey().serialize())
);
}
async function validateResultSignedKey(
resultSignedKey: Pick<SignedPreKeyType, 'keyId' | 'publicKey'>
) {
const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
ourUuid,
resultSignedKey.keyId
);
if (!keyPair) {
throw new Error(`SignedPreKey ${resultSignedKey.keyId} not found`);
}
assertEqualArrayBuffers(
resultSignedKey.publicKey,
toArrayBuffer(keyPair.publicKey().serialize())
);
}
before(async () => {
const keyPair = generateKeyPair();
await textsecure.storage.put('identityKeyMap', {
[ourUuid.toString()]: {
privKey: toBase64(keyPair.privKey),
pubKey: toBase64(keyPair.pubKey),
},
});
await textsecure.storage.user.setUuidAndDeviceId(ourUuid.toString(), 1);
await textsecure.storage.protocol.hydrateCaches();
});
after(async () => {
await textsecure.storage.protocol.clearPreKeyStore();
await textsecure.storage.protocol.clearSignedPreKeysStore();
});
describe('the first time', () => {
let result: GeneratedKeysType;
before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountManager = new AccountManager({} as any);
result = await accountManager.generateKeys(count);
});
for (let i = 1; i <= count; i += 1) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', () => {
for (let i = 0; i < count; i += 1) {
assert.strictEqual(result.preKeys[i].keyId, i + 1);
}
});
it('result contains the correct public keys', async () => {
await Promise.all(result.preKeys.map(validateResultKey));
});
it('returns a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 1);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
return validateResultSignedKey(result.signedPreKey);
});
});
describe('the second time', () => {
let result: GeneratedKeysType;
before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountManager = new AccountManager({} as any);
result = await accountManager.generateKeys(count);
});
for (let i = 1; i <= 2 * count; i += 1) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
itStoresSignedPreKey(2);
it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', () => {
for (let i = 1; i <= count; i += 1) {
assert.strictEqual(result.preKeys[i - 1].keyId, i + count);
}
});
it('result contains the correct public keys', async () => {
await Promise.all(result.preKeys.map(validateResultKey));
});
it('returns a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 2);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
return validateResultSignedKey(result.signedPreKey);
});
});
describe('the third time', () => {
let result: GeneratedKeysType;
before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountManager = new AccountManager({} as any);
result = await accountManager.generateKeys(count);
});
for (let i = 1; i <= 3 * count; i += 1) {
itStoresPreKey(i);
}
itStoresSignedPreKey(2);
itStoresSignedPreKey(3);
it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', () => {
for (let i = 1; i <= count; i += 1) {
assert.strictEqual(result.preKeys[i - 1].keyId, i + 2 * count);
}
});
it('result contains the correct public keys', async () => {
await Promise.all(result.preKeys.map(validateResultKey));
});
it('result contains a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 3);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
return validateResultSignedKey(result.signedPreKey);
});
});
});