// Copyright 2015 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; import { constantTimeEqual } from '../../Crypto'; import { generateKeyPair } from '../../Curve'; import type { UploadKeysType } from '../../textsecure/WebAPI'; import AccountManager from '../../textsecure/AccountManager'; import type { PreKeyType, SignedPreKeyType } from '../../textsecure/Types.d'; import { ServiceIdKind, normalizeAci } from '../../types/ServiceId'; const { textsecure } = window; const assertEqualBuffers = (a: Uint8Array, b: Uint8Array) => { assert.isTrue(constantTimeEqual(a, b)); }; describe('Key generation', function thisNeeded() { const count = 10; const ourServiceId = normalizeAci( 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee', 'test' ); let result: UploadKeysType; this.timeout(count * 2000); function itStoresPreKey(keyId: number): void { it(`prekey ${keyId} is valid`, async () => { const keyPair = await textsecure.storage.protocol.loadPreKey( ourServiceId, keyId ); assert(keyPair, `PreKey ${keyId} not found`); }); } function itStoresKyberPreKey(keyId: number): void { it(`kyber pre key ${keyId} is valid`, async () => { const key = await textsecure.storage.protocol.loadKyberPreKey( ourServiceId, keyId ); assert(key, `kyber pre key ${keyId} not found`); }); } function itStoresSignedPreKey(keyId: number): void { it(`signed prekey ${keyId} is valid`, async () => { const keyPair = await textsecure.storage.protocol.loadSignedPreKey( ourServiceId, keyId ); assert(keyPair, `SignedPreKey ${keyId} not found`); }); } async function validateResultPreKey( resultKey: Pick ): Promise { const keyPair = await textsecure.storage.protocol.loadPreKey( ourServiceId, resultKey.keyId ); if (!keyPair) { throw new Error(`PreKey ${resultKey.keyId} not found`); } assertEqualBuffers(resultKey.publicKey, keyPair.publicKey().serialize()); } async function validateResultSignedKey( resultSignedKey?: Pick ) { if (!resultSignedKey) { throw new Error('validateResultSignedKey: No signed prekey provided!'); } const keyPair = await textsecure.storage.protocol.loadSignedPreKey( ourServiceId, resultSignedKey.keyId ); if (!keyPair) { throw new Error(`SignedPreKey ${resultSignedKey.keyId} not found`); } assertEqualBuffers( resultSignedKey.publicKey, keyPair.publicKey().serialize() ); } before(async () => { await textsecure.storage.protocol.clearPreKeyStore(); await textsecure.storage.protocol.clearKyberPreKeyStore(); await textsecure.storage.protocol.clearSignedPreKeysStore(); const keyPair = generateKeyPair(); await textsecure.storage.put('identityKeyMap', { [ourServiceId]: keyPair, }); await textsecure.storage.user.setUuidAndDeviceId(ourServiceId, 1); await textsecure.storage.protocol.hydrateCaches(); }); after(async () => { await textsecure.storage.protocol.clearPreKeyStore(); await textsecure.storage.protocol.clearKyberPreKeyStore(); await textsecure.storage.protocol.clearSignedPreKeysStore(); }); describe('the first time', () => { before(async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const accountManager = new AccountManager({} as any); result = await accountManager._generateKeys(count, ServiceIdKind.ACI); }); describe('generates the basics', () => { for (let i = 1; i <= count; i += 1) { itStoresPreKey(i); } for (let i = 1; i <= count + 1; i += 1) { itStoresKyberPreKey(i); } itStoresSignedPreKey(1); }); it(`result contains ${count} preKeys`, () => { const preKeys = result.preKeys || []; assert.isArray(preKeys); assert.lengthOf(preKeys, count); for (let i = 0; i < count; i += 1) { assert.isObject(preKeys[i]); } }); it('result contains the correct keyIds', () => { const preKeys = result.preKeys || []; for (let i = 0; i < count; i += 1) { assert.strictEqual(preKeys[i].keyId, i + 1); } }); it('result contains the correct public keys', async () => { const preKeys = result.preKeys || []; await Promise.all(preKeys.map(validateResultPreKey)); }); it('returns a signed prekey', () => { assert.strictEqual(result.signedPreKey?.keyId, 1); assert.instanceOf(result.signedPreKey?.signature, Uint8Array); return validateResultSignedKey(result.signedPreKey); }); }); describe('the second time', () => { before(async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const accountManager = new AccountManager({} as any); result = await accountManager._generateKeys(count, ServiceIdKind.ACI); }); describe('generates the basics', () => { for (let i = 1; i <= 2 * count; i += 1) { itStoresPreKey(i); } for (let i = 1; i <= 2 * count + 2; i += 1) { itStoresKyberPreKey(i); } itStoresSignedPreKey(1); itStoresSignedPreKey(2); }); it(`result contains ${count} preKeys`, () => { const preKeys = result.preKeys || []; assert.isArray(preKeys); assert.lengthOf(preKeys, count); for (let i = 0; i < count; i += 1) { assert.isObject(preKeys[i]); } }); it('result contains the correct keyIds', () => { const preKeys = result.preKeys || []; for (let i = 1; i <= count; i += 1) { assert.strictEqual(preKeys[i - 1].keyId, i + count); } }); it('result contains the correct public keys', async () => { const preKeys = result.preKeys || []; await Promise.all(preKeys.map(validateResultPreKey)); }); it('returns a signed prekey', () => { assert.strictEqual(result.signedPreKey?.keyId, 2); assert.instanceOf(result.signedPreKey?.signature, Uint8Array); return validateResultSignedKey(result.signedPreKey); }); }); describe('the third time, after keys are confirmed', () => { before(async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const accountManager = new AccountManager({} as any); await accountManager._confirmKeys(result, ServiceIdKind.ACI); result = await accountManager._generateKeys(count, ServiceIdKind.ACI); }); describe('generates the basics', () => { for (let i = 1; i <= 3 * count; i += 1) { itStoresPreKey(i); } // Note: no new last resort kyber key generated for (let i = 1; i <= 3 * count + 2; i += 1) { itStoresKyberPreKey(i); } itStoresSignedPreKey(1); itStoresSignedPreKey(2); }); it(`result contains ${count} preKeys`, () => { const preKeys = result.preKeys || []; assert.isArray(preKeys); assert.lengthOf(preKeys, count); for (let i = 0; i < count; i += 1) { assert.isObject(preKeys[i]); } }); it('result contains the correct keyIds', () => { const preKeys = result.preKeys || []; for (let i = 1; i <= count; i += 1) { assert.strictEqual(preKeys[i - 1].keyId, i + 2 * count); } }); it('result contains the correct public keys', async () => { const preKeys = result.preKeys || []; await Promise.all(preKeys.map(validateResultPreKey)); }); it('does not generate a third last resort prekey', async () => { const keyId = 3 * count + 3; const key = await textsecure.storage.protocol.loadKyberPreKey( ourServiceId, keyId ); assert.isUndefined(key, `kyber pre key ${keyId} was unexpectedly found`); }); it('does not generate a third signed prekey', async () => { const keyId = 3; const keyPair = await textsecure.storage.protocol.loadSignedPreKey( ourServiceId, keyId ); assert.isUndefined( keyPair, `SignedPreKey ${keyId} was unexpectedly found` ); }); }); });