diff --git a/background.html b/background.html index cab672b479..cbeb81d99f 100644 --- a/background.html +++ b/background.html @@ -309,7 +309,6 @@ type="text/javascript" src="js/rotate_signed_prekey_listener.js" > -
diff --git a/js/keychange_listener.js b/js/keychange_listener.js deleted file mode 100644 index 84053e6f95..0000000000 --- a/js/keychange_listener.js +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2017-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global Whisper, SignalProtocolStore, ConversationController, _ */ - -/* eslint-disable more/no-then */ - -// eslint-disable-next-line func-names -(function () { - window.Whisper = window.Whisper || {}; - - Whisper.KeyChangeListener = { - init(signalProtocolStore) { - if (!(signalProtocolStore instanceof SignalProtocolStore)) { - throw new Error('KeyChangeListener requires a SignalProtocolStore'); - } - - signalProtocolStore.on('keychange', async identifier => { - const conversation = await ConversationController.getOrCreateAndWait( - identifier, - 'private' - ); - conversation.addKeyChange(identifier); - - const groups = await ConversationController.getAllGroupsInvolvingId( - conversation.id - ); - _.forEach(groups, group => { - group.addKeyChange(identifier); - }); - }); - }, - }; -})(); diff --git a/js/modules/signal.js b/js/modules/signal.js index a08452f835..5ae22b8b16 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -142,6 +142,9 @@ const Errors = require('../../ts/types/errors'); const MessageType = require('./types/message'); const MIME = require('../../ts/types/MIME'); const SettingsType = require('../../ts/types/Settings'); +const { UUID } = require('../../ts/types/UUID'); +const { Address } = require('../../ts/types/Address'); +const { QualifiedAddress } = require('../../ts/types/QualifiedAddress'); // Views const Initialization = require('./views/initialization'); @@ -430,6 +433,9 @@ exports.setup = (options = {}) => { MIME, Settings: SettingsType, VisualAttachment, + UUID, + Address, + QualifiedAddress, }; const Views = { diff --git a/libtextsecure/test/generate_keys_test.js b/libtextsecure/test/generate_keys_test.js deleted file mode 100644 index 6ef771018c..0000000000 --- a/libtextsecure/test/generate_keys_test.js +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2015-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global textsecure */ - -describe('Key generation', function thisNeeded() { - const count = 10; - this.timeout(count * 2000); - - function validateStoredKeyPair(keyPair) { - /* Ensure the keypair matches the format used internally by libsignal-protocol */ - assert.isObject(keyPair, 'Stored keyPair is not an object'); - assert.instanceOf(keyPair.pubKey, ArrayBuffer); - assert.instanceOf(keyPair.privKey, ArrayBuffer); - assert.strictEqual(keyPair.pubKey.byteLength, 33); - assert.strictEqual(new Uint8Array(keyPair.pubKey)[0], 5); - assert.strictEqual(keyPair.privKey.byteLength, 32); - } - function itStoresPreKey(keyId) { - it(`prekey ${keyId} is valid`, () => - textsecure.storage.protocol.loadPreKey(keyId).then(keyPair => { - validateStoredKeyPair(keyPair); - })); - } - function itStoresSignedPreKey(keyId) { - it(`signed prekey ${keyId} is valid`, () => - textsecure.storage.protocol.loadSignedPreKey(keyId).then(keyPair => { - validateStoredKeyPair(keyPair); - })); - } - function validateResultKey(resultKey) { - return textsecure.storage.protocol - .loadPreKey(resultKey.keyId) - .then(keyPair => { - assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey); - }); - } - function validateResultSignedKey(resultSignedKey) { - return textsecure.storage.protocol - .loadSignedPreKey(resultSignedKey.keyId) - .then(keyPair => { - assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey); - }); - } - - before(async () => { - localStorage.clear(); - const keyPair = window.Signal.Curve.generateKeyPair(); - await textsecure.storage.protocol.put('identityKey', keyPair); - }); - - describe('the first time', () => { - let result; - /* result should have this format - * { - * preKeys: [ { keyId, publicKey }, ... ], - * signedPreKey: { keyId, publicKey, signature }, - * identityKey: - * } - */ - before(() => { - const accountManager = new textsecure.AccountManager(''); - return accountManager.generateKeys(count).then(res => { - result = res; - }); - }); - 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', () => - 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; - before(() => { - const accountManager = new textsecure.AccountManager(''); - return accountManager.generateKeys(count).then(res => { - result = res; - }); - }); - 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', () => - 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; - before(() => { - const accountManager = new textsecure.AccountManager(''); - return accountManager.generateKeys(count).then(res => { - result = res; - }); - }); - 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', () => - 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); - }); - }); -}); diff --git a/libtextsecure/test/in_memory_signal_protocol_store.js b/libtextsecure/test/in_memory_signal_protocol_store.js deleted file mode 100644 index e9b4ae5689..0000000000 --- a/libtextsecure/test/in_memory_signal_protocol_store.js +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2016-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -function SignalProtocolStore() { - this.store = {}; -} - -SignalProtocolStore.prototype = { - VerifiedStatus: { - DEFAULT: 0, - VERIFIED: 1, - UNVERIFIED: 2, - }, - - getIdentityKeyPair() { - return Promise.resolve(this.get('identityKey')); - }, - getLocalRegistrationId() { - return Promise.resolve(this.get('registrationId')); - }, - put(key, value) { - if ( - key === undefined || - value === undefined || - key === null || - value === null - ) { - throw new Error('Tried to store undefined/null'); - } - this.store[key] = value; - }, - get(key, defaultValue) { - 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) { - if (key === null || key === undefined) { - throw new Error('Tried to remove value for undefined/null key'); - } - delete this.store[key]; - }, - - isTrustedIdentity(identifier, identityKey) { - 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(identityKey === trusted); - }, - loadIdentityKey(identifier) { - if (identifier === null || identifier === undefined) { - throw new Error('Tried to get identity key for undefined/null key'); - } - return new Promise(resolve => { - resolve(this.get(`identityKey${identifier}`)); - }); - }, - saveIdentity(identifier, identityKey) { - if (identifier === null || identifier === undefined) { - throw new Error('Tried to put identity key for undefined/null key'); - } - return new Promise(resolve => { - const existing = this.get(`identityKey${identifier}`); - this.put(`identityKey${identifier}`, identityKey); - if (existing && existing !== identityKey) { - resolve(true); - } else { - resolve(false); - } - }); - }, - - /* Returns a prekeypair object or undefined */ - loadPreKey(keyId) { - return new Promise(resolve => { - const res = this.get(`25519KeypreKey${keyId}`); - resolve(res); - }); - }, - storePreKey(keyId, keyPair) { - return new Promise(resolve => { - resolve(this.put(`25519KeypreKey${keyId}`, keyPair)); - }); - }, - removePreKey(keyId) { - return new Promise(resolve => { - resolve(this.remove(`25519KeypreKey${keyId}`)); - }); - }, - - /* Returns a signed keypair object or undefined */ - loadSignedPreKey(keyId) { - return new Promise(resolve => { - const res = this.get(`25519KeysignedKey${keyId}`); - resolve(res); - }); - }, - loadSignedPreKeys() { - return new Promise(resolve => { - const res = []; - const keys = Object.keys(this.store); - for (let i = 0, max = keys.length; i < max; i += 1) { - const key = keys[i]; - if (key.startsWith('25519KeysignedKey')) { - res.push(this.store[key]); - } - } - resolve(res); - }); - }, - storeSignedPreKey(keyId, keyPair) { - return new Promise(resolve => { - resolve(this.put(`25519KeysignedKey${keyId}`, keyPair)); - }); - }, - removeSignedPreKey(keyId) { - return new Promise(resolve => { - resolve(this.remove(`25519KeysignedKey${keyId}`)); - }); - }, - - loadSession(identifier) { - return new Promise(resolve => { - resolve(this.get(`session${identifier}`)); - }); - }, - storeSession(identifier, record) { - return new Promise(resolve => { - resolve(this.put(`session${identifier}`, record)); - }); - }, - removeAllSessions(identifier) { - return new Promise(resolve => { - const keys = Object.keys(this.store); - for (let i = 0, max = keys.length; i < max; i += 1) { - const key = keys[i]; - if (key.match(RegExp(`^session${identifier.replace('+', '\\+')}.+`))) { - delete this.store[key]; - } - } - resolve(); - }); - }, - getDeviceIds(identifier) { - return new Promise(resolve => { - const deviceIds = []; - const keys = Object.keys(this.store); - for (let i = 0, max = keys.length; i < max; i += 1) { - const key = keys[i]; - if (key.match(RegExp(`^session${identifier.replace('+', '\\+')}.+`))) { - deviceIds.push(parseInt(key.split('.')[1], 10)); - } - } - resolve(deviceIds); - }); - }, - - getUnprocessedCount: () => Promise.resolve(0), - getAllUnprocessed: () => Promise.resolve([]), - getUnprocessedById: () => Promise.resolve(null), - addUnprocessed: () => Promise.resolve(), - addMultipleUnprocessed: () => Promise.resolve(), - updateUnprocessedAttempts: () => Promise.resolve(), - updateUnprocessedWithData: () => Promise.resolve(), - updateUnprocessedsWithData: () => Promise.resolve(), - removeUnprocessed: () => Promise.resolve(), - removeAllUnprocessed: () => Promise.resolve(), -}; diff --git a/libtextsecure/test/index.html b/libtextsecure/test/index.html index c0666ec67c..4e4decc364 100644 --- a/libtextsecure/test/index.html +++ b/libtextsecure/test/index.html @@ -14,10 +14,6 @@ - - diff --git a/libtextsecure/test/storage_test.js b/libtextsecure/test/storage_test.js deleted file mode 100644 index 09882403d0..0000000000 --- a/libtextsecure/test/storage_test.js +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2015-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global textsecure, storage, ConversationController */ - -describe('SignalProtocolStore', () => { - const store = textsecure.storage.protocol; - const identifier = '+5558675309'; - const identityKey = { - pubKey: window.Signal.Crypto.getRandomBytes(33), - privKey: window.Signal.Crypto.getRandomBytes(32), - }; - const testKey = { - pubKey: window.Signal.Crypto.getRandomBytes(33), - privKey: window.Signal.Crypto.getRandomBytes(32), - }; - before(async () => { - localStorage.clear(); - ConversationController.reset(); - // store.hydrateCaches(); - await storage.fetch(); - await ConversationController.load(); - await ConversationController.getOrCreateAndWait(identifier, 'private'); - }); - it('retrieves my registration id', async () => { - store.put('registrationId', 1337); - - const reg = await store.getLocalRegistrationId(); - assert.strictEqual(reg, 1337); - }); - it('retrieves my identity key', async () => { - store.put('identityKey', identityKey); - const key = await store.getIdentityKeyPair(); - assertEqualArrayBuffers(key.pubKey, identityKey.pubKey); - assertEqualArrayBuffers(key.privKey, identityKey.privKey); - }); - it('stores identity keys', async () => { - await store.saveIdentity(identifier, testKey.pubKey); - const key = await store.loadIdentityKey(identifier); - assertEqualArrayBuffers(key, testKey.pubKey); - }); - it('returns whether a key is trusted', async () => { - const newIdentity = window.Signal.Crypto.getRandomBytes(33); - await store.saveIdentity(identifier, testKey.pubKey); - - const trusted = await store.isTrustedIdentity(identifier, newIdentity); - if (trusted) { - throw new Error('Allowed to overwrite identity key'); - } - }); - it('returns whether a key is untrusted', async () => { - await store.saveIdentity(identifier, testKey.pubKey); - const trusted = await store.isTrustedIdentity(identifier, testKey.pubKey); - - if (!trusted) { - throw new Error('Allowed to overwrite identity key'); - } - }); - 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); - }); - it('deletes prekeys', async () => { - await store.storePreKey(2, testKey); - await store.removePreKey(2, testKey); - - const key = await store.loadPreKey(2); - assert.isUndefined(key); - }); - 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); - }); - it('deletes signed prekeys', async () => { - await store.storeSignedPreKey(4, testKey); - await store.removeSignedPreKey(4, testKey); - - const key = await store.loadSignedPreKey(4); - assert.isUndefined(key); - }); - it('stores sessions', async () => { - const testRecord = 'an opaque string'; - const devices = [1, 2, 3].map(deviceId => [identifier, deviceId].join('.')); - - await Promise.all( - devices.map(async encodedNumber => { - await store.storeSession(encodedNumber, testRecord + encodedNumber); - }) - ); - - const records = await Promise.all( - devices.map(store.loadSession.bind(store)) - ); - - for (let i = 0, max = records.length; i < max; i += 1) { - assert.strictEqual(records[i], testRecord + devices[i]); - } - }); - it('removes all sessions for a number', async () => { - const testRecord = 'an opaque string'; - const devices = [1, 2, 3].map(deviceId => [identifier, deviceId].join('.')); - - await Promise.all( - devices.map(async encodedNumber => { - await store.storeSession(encodedNumber, testRecord + encodedNumber); - }) - ); - - await store.removeAllSessions(identifier); - - const records = await Promise.all( - devices.map(store.loadSession.bind(store)) - ); - - for (let i = 0, max = records.length; i < max; i += 1) { - assert.isUndefined(records[i]); - } - }); - it('returns deviceIds for a number', async () => { - const testRecord = 'an opaque string'; - const devices = [1, 2, 3].map(deviceId => [identifier, deviceId].join('.')); - - await Promise.all( - devices.map(async encodedNumber => { - await store.storeSession(encodedNumber, testRecord + encodedNumber); - }) - ); - - const deviceIds = await store.getDeviceIds(identifier); - assert.sameMembers(deviceIds, [1, 2, 3]); - }); - it('returns empty array for a number with no device ids', async () => { - const deviceIds = await store.getDeviceIds('foo'); - assert.sameMembers(deviceIds, []); - }); -}); diff --git a/preload.js b/preload.js index f457c648f6..55819a7e41 100644 --- a/preload.js +++ b/preload.js @@ -189,6 +189,8 @@ try { statistics = {}; } + const ourUuid = window.textsecure.storage.user.getUuid(); + event.sender.send('additional-log-data-response', { capabilities: ourCapabilities || {}, remoteConfig: _.mapValues(remoteConfig, ({ value, enabled }) => { @@ -200,7 +202,7 @@ try { user: { deviceId: window.textsecure.storage.user.getDeviceId(), e164: window.textsecure.storage.user.getNumber(), - uuid: window.textsecure.storage.user.getUuid(), + uuid: ourUuid && ourUuid.toString(), conversationId: ourConversation && ourConversation.id, }, }); diff --git a/test/index.html b/test/index.html index ec39862435..6578502492 100644 --- a/test/index.html +++ b/test/index.html @@ -248,11 +248,6 @@ > - - diff --git a/ts/ConversationController.ts b/ts/ConversationController.ts index f2642fce11..2909c5cabd 100644 --- a/ts/ConversationController.ts +++ b/ts/ConversationController.ts @@ -7,6 +7,7 @@ import PQueue from 'p-queue'; import dataInterface from './sql/Client'; import { ConversationModelCollectionType, + ConversationAttributesType, ConversationAttributesTypeType, } from './model-types.d'; import { ConversationModel } from './models/conversations'; @@ -15,6 +16,9 @@ import { assert } from './util/assert'; import { isValidGuid } from './util/isValidGuid'; import { map, reduce } from './util/iterables'; import { isGroupV1, isGroupV2 } from './util/whatTypeOfConversation'; +import { UUID } from './types/UUID'; +import { Address } from './types/Address'; +import { QualifiedAddress } from './types/QualifiedAddress'; const MAX_MESSAGE_BODY_LENGTH = 64 * 1024; @@ -156,7 +160,7 @@ export class ConversationController { } dangerouslyCreateAndAdd( - attributes: Partial + attributes: Partial ): ConversationModel { return this._conversations.add(attributes); } @@ -295,7 +299,7 @@ export class ConversationController { getOurConversationId(): string | undefined { const e164 = window.textsecure.storage.user.getNumber(); - const uuid = window.textsecure.storage.user.getUuid(); + const uuid = window.textsecure.storage.user.getUuid()?.toString(); return this.ensureContactIds({ e164, uuid, highTrust: true }); } @@ -639,13 +643,14 @@ export class ConversationController { } const obsoleteId = obsolete.get('id'); + const obsoleteUuid = obsolete.get('uuid'); const currentId = current.get('id'); window.log.warn('combineConversations: Combining two conversations', { obsolete: obsoleteId, current: currentId, }); - if (conversationType === 'private') { + if (conversationType === 'private' && obsoleteUuid) { if (!current.get('profileKey') && obsolete.get('profileKey')) { window.log.warn( 'combineConversations: Copying profile key from old to new contact' @@ -661,21 +666,30 @@ export class ConversationController { window.log.warn( 'combineConversations: Delete all sessions tied to old conversationId' ); - const deviceIds = await window.textsecure.storage.protocol.getDeviceIds( - obsoleteId - ); + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({ + ourUuid, + identifier: obsoleteUuid, + }); await Promise.all( deviceIds.map(async deviceId => { - await window.textsecure.storage.protocol.removeSession( - `${obsoleteId}.${deviceId}` + const addr = new QualifiedAddress( + ourUuid, + Address.create(obsoleteUuid, deviceId) ); + await window.textsecure.storage.protocol.removeSession(addr); }) ); window.log.warn( 'combineConversations: Delete all identity information tied to old conversationId' ); - await window.textsecure.storage.protocol.removeIdentityKey(obsoleteId); + + if (obsoleteUuid) { + await window.textsecure.storage.protocol.removeIdentityKey( + new UUID(obsoleteUuid) + ); + } window.log.warn( 'combineConversations: Ensure that all V1 groups have new conversationId instead of old' diff --git a/ts/LibSignalStores.ts b/ts/LibSignalStores.ts index d69e767b2b..e21afc5b44 100644 --- a/ts/LibSignalStores.ts +++ b/ts/LibSignalStores.ts @@ -23,28 +23,42 @@ import { Uuid, } from '@signalapp/signal-client'; import { freezePreKey, freezeSignedPreKey } from './SignalProtocolStore'; +import { Address } from './types/Address'; +import { QualifiedAddress } from './types/QualifiedAddress'; +import type { UUID } from './types/UUID'; import { typedArrayToArrayBuffer } from './Crypto'; import { Zone } from './util/Zone'; -function encodedNameFromAddress(address: ProtocolAddress): string { +function encodeAddress(address: ProtocolAddress): Address { const name = address.name(); const deviceId = address.deviceId(); - const encodedName = `${name}.${deviceId}`; - return encodedName; + return Address.create(name, deviceId); } -export type SessionsOptions = { - readonly zone?: Zone; -}; +function toQualifiedAddress( + ourUuid: UUID, + address: ProtocolAddress +): QualifiedAddress { + return new QualifiedAddress(ourUuid, encodeAddress(address)); +} + +export type SessionsOptions = Readonly<{ + ourUuid: UUID; + zone?: Zone; +}>; export class Sessions extends SessionStore { + private readonly ourUuid: UUID; + private readonly zone: Zone | undefined; - constructor(options: SessionsOptions = {}) { + constructor({ ourUuid, zone }: SessionsOptions) { super(); - this.zone = options.zone; + + this.ourUuid = ourUuid; + this.zone = zone; } async saveSession( @@ -52,16 +66,16 @@ export class Sessions extends SessionStore { record: SessionRecord ): Promise { await window.textsecure.storage.protocol.storeSession( - encodedNameFromAddress(address), + toQualifiedAddress(this.ourUuid, address), record, { zone: this.zone } ); } async getSession(name: ProtocolAddress): Promise { - const encodedName = encodedNameFromAddress(name); + const encodedAddress = toQualifiedAddress(this.ourUuid, name); const record = await window.textsecure.storage.protocol.loadSession( - encodedName, + encodedAddress, { zone: this.zone } ); @@ -71,27 +85,36 @@ export class Sessions extends SessionStore { async getExistingSessions( addresses: Array ): Promise> { - const encodedAddresses = addresses.map(encodedNameFromAddress); + const encodedAddresses = addresses.map(addr => + toQualifiedAddress(this.ourUuid, addr) + ); return window.textsecure.storage.protocol.loadSessions(encodedAddresses, { zone: this.zone, }); } } -export type IdentityKeysOptions = { - readonly zone?: Zone; -}; +export type IdentityKeysOptions = Readonly<{ + ourUuid: UUID; + zone?: Zone; +}>; export class IdentityKeys extends IdentityKeyStore { + private readonly ourUuid: UUID; + private readonly zone: Zone | undefined; - constructor({ zone }: IdentityKeysOptions = {}) { + constructor({ ourUuid, zone }: IdentityKeysOptions) { super(); + + this.ourUuid = ourUuid; this.zone = zone; } async getIdentityKey(): Promise { - const keyPair = await window.textsecure.storage.protocol.getIdentityKeyPair(); + const keyPair = await window.textsecure.storage.protocol.getIdentityKeyPair( + this.ourUuid + ); if (!keyPair) { throw new Error('IdentityKeyStore/getIdentityKey: No identity key!'); } @@ -99,7 +122,9 @@ export class IdentityKeys extends IdentityKeyStore { } async getLocalRegistrationId(): Promise { - const id = await window.textsecure.storage.protocol.getLocalRegistrationId(); + const id = await window.textsecure.storage.protocol.getLocalRegistrationId( + this.ourUuid + ); if (!isNumber(id)) { throw new Error( 'IdentityKeyStore/getLocalRegistrationId: No registration id!' @@ -109,9 +134,9 @@ export class IdentityKeys extends IdentityKeyStore { } async getIdentity(address: ProtocolAddress): Promise { - const encodedName = encodedNameFromAddress(address); + const encodedAddress = encodeAddress(address); const key = await window.textsecure.storage.protocol.loadIdentityKey( - encodedName + encodedAddress.uuid ); if (!key) { @@ -122,13 +147,13 @@ export class IdentityKeys extends IdentityKeyStore { } async saveIdentity(name: ProtocolAddress, key: PublicKey): Promise { - const encodedName = encodedNameFromAddress(name); + const encodedAddress = encodeAddress(name); const publicKey = typedArrayToArrayBuffer(key.serialize()); // Pass `zone` to let `saveIdentity` archive sibling sessions when identity // key changes. return window.textsecure.storage.protocol.saveIdentity( - encodedName, + encodedAddress, publicKey, false, { zone: this.zone } @@ -140,27 +165,42 @@ export class IdentityKeys extends IdentityKeyStore { key: PublicKey, direction: Direction ): Promise { - const encodedName = encodedNameFromAddress(name); + const encodedAddress = encodeAddress(name); const publicKey = typedArrayToArrayBuffer(key.serialize()); return window.textsecure.storage.protocol.isTrustedIdentity( - encodedName, + encodedAddress, publicKey, direction ); } } +export type PreKeysOptions = Readonly<{ + ourUuid: UUID; +}>; + export class PreKeys extends PreKeyStore { + private readonly ourUuid: UUID; + + constructor({ ourUuid }: PreKeysOptions) { + super(); + this.ourUuid = ourUuid; + } + async savePreKey(id: number, record: PreKeyRecord): Promise { await window.textsecure.storage.protocol.storePreKey( + this.ourUuid, id, freezePreKey(record) ); } async getPreKey(id: number): Promise { - const preKey = await window.textsecure.storage.protocol.loadPreKey(id); + const preKey = await window.textsecure.storage.protocol.loadPreKey( + this.ourUuid, + id + ); if (preKey === undefined) { throw new Error(`getPreKey: PreKey ${id} not found`); @@ -170,17 +210,28 @@ export class PreKeys extends PreKeyStore { } async removePreKey(id: number): Promise { - await window.textsecure.storage.protocol.removePreKey(id); + await window.textsecure.storage.protocol.removePreKey(this.ourUuid, id); } } +export type SenderKeysOptions = Readonly<{ + ourUuid: UUID; +}>; + export class SenderKeys extends SenderKeyStore { + private readonly ourUuid: UUID; + + constructor({ ourUuid }: SenderKeysOptions) { + super(); + this.ourUuid = ourUuid; + } + async saveSenderKey( sender: ProtocolAddress, distributionId: Uuid, record: SenderKeyRecord ): Promise { - const encodedAddress = encodedNameFromAddress(sender); + const encodedAddress = toQualifiedAddress(this.ourUuid, sender); await window.textsecure.storage.protocol.saveSenderKey( encodedAddress, @@ -193,7 +244,7 @@ export class SenderKeys extends SenderKeyStore { sender: ProtocolAddress, distributionId: Uuid ): Promise { - const encodedAddress = encodedNameFromAddress(sender); + const encodedAddress = toQualifiedAddress(this.ourUuid, sender); const senderKey = await window.textsecure.storage.protocol.getSenderKey( encodedAddress, @@ -204,12 +255,24 @@ export class SenderKeys extends SenderKeyStore { } } +export type SignedPreKeysOptions = Readonly<{ + ourUuid: UUID; +}>; + export class SignedPreKeys extends SignedPreKeyStore { + private readonly ourUuid: UUID; + + constructor({ ourUuid }: SignedPreKeysOptions) { + super(); + this.ourUuid = ourUuid; + } + async saveSignedPreKey( id: number, record: SignedPreKeyRecord ): Promise { await window.textsecure.storage.protocol.storeSignedPreKey( + this.ourUuid, id, freezeSignedPreKey(record), true @@ -218,6 +281,7 @@ export class SignedPreKeys extends SignedPreKeyStore { async getSignedPreKey(id: number): Promise { const signedPreKey = await window.textsecure.storage.protocol.loadSignedPreKey( + this.ourUuid, id ); diff --git a/ts/SignalProtocolStore.ts b/ts/SignalProtocolStore.ts index b96f9281d6..7658603cc9 100644 --- a/ts/SignalProtocolStore.ts +++ b/ts/SignalProtocolStore.ts @@ -21,8 +21,9 @@ import { constantTimeEqual, fromEncodedBinaryToArrayBuffer, typedArrayToArrayBuffer, + base64ToArrayBuffer, } from './Crypto'; -import { assert } from './util/assert'; +import { assert, strictAssert } from './util/assert'; import { handleMessageSend } from './util/handleMessageSend'; import { isNotNil } from './util/isNotNil'; import { Zone } from './util/Zone'; @@ -33,19 +34,30 @@ import { } from './util/sessionTranslation'; import { DeviceType, - KeyPairType, IdentityKeyType, + IdentityKeyIdType, + KeyPairType, + OuterSignedPrekeyType, + PreKeyIdType, + PreKeyType, + SenderKeyIdType, SenderKeyType, + SessionIdType, SessionResetsType, SessionType, + SignedPreKeyIdType, SignedPreKeyType, - OuterSignedPrekeyType, - PreKeyType, UnprocessedType, UnprocessedUpdateType, } from './textsecure/Types.d'; import { getSendOptions } from './util/getSendOptions'; import type { RemoveAllConfiguration } from './types/RemoveAllConfiguration'; +import { UUID, UUIDStringType } from './types/UUID'; +import { Address } from './types/Address'; +import { + QualifiedAddress, + QualifiedAddressStringType, +} from './types/QualifiedAddress'; const TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds @@ -81,24 +93,6 @@ function validateIdentityKey(attrs: unknown): attrs is IdentityKeyType { return true; } -async function normalizeEncodedAddress( - encodedAddress: string -): Promise { - const [identifier, deviceId] = window.textsecure.utils.unencodeNumber( - encodedAddress - ); - try { - const conv = window.ConversationController.getOrCreate( - identifier, - 'private' - ); - return `${conv.get('id')}.${deviceId}`; - } catch (e) { - window.log.error(`could not get conversation for identifier ${identifier}`); - throw e; - } -} - type HasIdType = { id: T; }; @@ -154,7 +148,7 @@ export function hydratePublicKey(identityKey: IdentityKeyType): PublicKey { export function hydratePreKey(preKey: PreKeyType): PreKeyRecord { const publicKey = PublicKey.deserialize(Buffer.from(preKey.publicKey)); const privateKey = PrivateKey.deserialize(Buffer.from(preKey.privateKey)); - return PreKeyRecord.new(preKey.id, publicKey, privateKey); + return PreKeyRecord.new(preKey.keyId, publicKey, privateKey); } export function hydrateSignedPreKey( signedPreKey: SignedPreKeyType @@ -165,7 +159,7 @@ export function hydrateSignedPreKey( const signature = Buffer.from([]); return SignedPreKeyRecord.new( - signedPreKey.id, + signedPreKey.keyId, createdAt, pubKey, privKey, @@ -216,26 +210,32 @@ export class SignalProtocolStore extends EventsMixin { // Cached values - ourIdentityKey?: KeyPairType; + private ourIdentityKeys = new Map(); - ourRegistrationId?: number; + private ourRegistrationIds = new Map(); - identityKeys?: Map>; + identityKeys?: Map< + IdentityKeyIdType, + CacheEntryType + >; - senderKeys?: Map>; + senderKeys?: Map< + SenderKeyIdType, + CacheEntryType + >; - sessions?: Map; + sessions?: Map; - preKeys?: Map>; + preKeys?: Map>; signedPreKeys?: Map< - number, + SignedPreKeyIdType, CacheEntryType >; - senderKeyQueues: Map = new Map(); + senderKeyQueues = new Map(); - sessionQueues: Map = new Map(); + sessionQueues = new Map(); private currentZone?: Zone; @@ -243,19 +243,37 @@ export class SignalProtocolStore extends EventsMixin { private readonly zoneQueue: Array = []; - private pendingSessions = new Map(); + private pendingSessions = new Map(); private pendingUnprocessed = new Map(); async hydrateCaches(): Promise { await Promise.all([ (async () => { - const item = await window.Signal.Data.getItemById('identityKey'); - this.ourIdentityKey = item ? item.value : undefined; + this.ourIdentityKeys.clear(); + const map = await window.Signal.Data.getItemById('identityKeyMap'); + if (!map) { + return; + } + + for (const key of Object.keys(map.value)) { + const { privKey, pubKey } = map.value[key]; + this.ourIdentityKeys.set(new UUID(key).toString(), { + privKey: base64ToArrayBuffer(privKey), + pubKey: base64ToArrayBuffer(pubKey), + }); + } })(), (async () => { - const item = await window.Signal.Data.getItemById('registrationId'); - this.ourRegistrationId = item ? item.value : undefined; + this.ourRegistrationIds.clear(); + const map = await window.Signal.Data.getItemById('registrationIdMap'); + if (!map) { + return; + } + + for (const key of Object.keys(map.value)) { + this.ourRegistrationIds.set(new UUID(key).toString(), map.value[key]); + } })(), _fillCaches( this, @@ -267,7 +285,7 @@ export class SignalProtocolStore extends EventsMixin { 'sessions', window.Signal.Data.getAllSessions() ), - _fillCaches( + _fillCaches( this, 'preKeys', window.Signal.Data.getAllPreKeys() @@ -277,7 +295,7 @@ export class SignalProtocolStore extends EventsMixin { 'senderKeys', window.Signal.Data.getAllSenderKeys() ), - _fillCaches( + _fillCaches( this, 'signedPreKeys', window.Signal.Data.getAllSignedPreKeys() @@ -285,68 +303,83 @@ export class SignalProtocolStore extends EventsMixin { ]); } - async getIdentityKeyPair(): Promise { - return this.ourIdentityKey; + async getIdentityKeyPair(ourUuid: UUID): Promise { + return this.ourIdentityKeys.get(ourUuid.toString()); } - async getLocalRegistrationId(): Promise { - return this.ourRegistrationId; + async getLocalRegistrationId(ourUuid: UUID): Promise { + return this.ourRegistrationIds.get(ourUuid.toString()); } // PreKeys - async loadPreKey(keyId: number): Promise { + async loadPreKey( + ourUuid: UUID, + keyId: number + ): Promise { if (!this.preKeys) { throw new Error('loadPreKey: this.preKeys not yet cached!'); } - const entry = this.preKeys.get(keyId); + const id: PreKeyIdType = `${ourUuid.toString()}:${keyId}`; + + const entry = this.preKeys.get(id); if (!entry) { - window.log.error('Failed to fetch prekey:', keyId); + window.log.error('Failed to fetch prekey:', id); return undefined; } if (entry.hydrated) { - window.log.info('Successfully fetched prekey (cache hit):', keyId); + window.log.info('Successfully fetched prekey (cache hit):', id); return entry.item; } const item = hydratePreKey(entry.fromDB); - this.preKeys.set(keyId, { + this.preKeys.set(id, { hydrated: true, fromDB: entry.fromDB, item, }); - window.log.info('Successfully fetched prekey (cache miss):', keyId); + window.log.info('Successfully fetched prekey (cache miss):', id); return item; } - async storePreKey(keyId: number, keyPair: KeyPairType): Promise { + async storePreKey( + ourUuid: UUID, + keyId: number, + keyPair: KeyPairType + ): Promise { if (!this.preKeys) { throw new Error('storePreKey: this.preKeys not yet cached!'); } - if (this.preKeys.has(keyId)) { - throw new Error(`storePreKey: prekey ${keyId} already exists!`); + + const id: PreKeyIdType = `${ourUuid.toString()}:${keyId}`; + if (this.preKeys.has(id)) { + throw new Error(`storePreKey: prekey ${id} already exists!`); } const fromDB = { - id: keyId, + id, + keyId, + ourUuid: ourUuid.toString(), publicKey: keyPair.pubKey, privateKey: keyPair.privKey, }; await window.Signal.Data.createOrUpdatePreKey(fromDB); - this.preKeys.set(keyId, { + this.preKeys.set(id, { hydrated: false, fromDB, }); } - async removePreKey(keyId: number): Promise { + async removePreKey(ourUuid: UUID, keyId: number): Promise { if (!this.preKeys) { throw new Error('removePreKey: this.preKeys not yet cached!'); } + const id: PreKeyIdType = `${ourUuid.toString()}:${keyId}`; + try { this.trigger('removePreKey'); } catch (error) { @@ -356,8 +389,8 @@ export class SignalProtocolStore extends EventsMixin { ); } - this.preKeys.delete(keyId); - await window.Signal.Data.removePreKeyById(keyId); + this.preKeys.delete(id); + await window.Signal.Data.removePreKeyById(id); } async clearPreKeyStore(): Promise { @@ -370,58 +403,66 @@ export class SignalProtocolStore extends EventsMixin { // Signed PreKeys async loadSignedPreKey( + ourUuid: UUID, keyId: number ): Promise { if (!this.signedPreKeys) { throw new Error('loadSignedPreKey: this.signedPreKeys not yet cached!'); } - const entry = this.signedPreKeys.get(keyId); + const id: SignedPreKeyIdType = `${ourUuid.toString()}:${keyId}`; + + const entry = this.signedPreKeys.get(id); if (!entry) { - window.log.error('Failed to fetch signed prekey:', keyId); + window.log.error('Failed to fetch signed prekey:', id); return undefined; } if (entry.hydrated) { - window.log.info('Successfully fetched signed prekey (cache hit):', keyId); + window.log.info('Successfully fetched signed prekey (cache hit):', id); return entry.item; } const item = hydrateSignedPreKey(entry.fromDB); - this.signedPreKeys.set(keyId, { + this.signedPreKeys.set(id, { hydrated: true, item, fromDB: entry.fromDB, }); - window.log.info('Successfully fetched signed prekey (cache miss):', keyId); + window.log.info('Successfully fetched signed prekey (cache miss):', id); return item; } - async loadSignedPreKeys(): Promise> { + async loadSignedPreKeys( + ourUuid: UUID + ): Promise> { if (!this.signedPreKeys) { throw new Error('loadSignedPreKeys: this.signedPreKeys not yet cached!'); } - if (arguments.length > 0) { - throw new Error('loadSignedPreKeys takes no arguments'); + if (arguments.length > 1) { + throw new Error('loadSignedPreKeys takes one argument'); } const entries = Array.from(this.signedPreKeys.values()); - return entries.map(entry => { - const preKey = entry.fromDB; - return { - pubKey: preKey.publicKey, - privKey: preKey.privateKey, - created_at: preKey.created_at, - keyId: preKey.id, - confirmed: preKey.confirmed, - }; - }); + return entries + .filter(({ fromDB }) => fromDB.ourUuid === ourUuid.toString()) + .map(entry => { + const preKey = entry.fromDB; + return { + pubKey: preKey.publicKey, + privKey: preKey.privateKey, + created_at: preKey.created_at, + keyId: preKey.keyId, + confirmed: preKey.confirmed, + }; + }); } // Note that this is also called in update scenarios, for confirming that signed prekeys // have indeed been accepted by the server. async storeSignedPreKey( + ourUuid: UUID, keyId: number, keyPair: KeyPairType, confirmed?: boolean @@ -430,8 +471,12 @@ export class SignalProtocolStore extends EventsMixin { throw new Error('storeSignedPreKey: this.signedPreKeys not yet cached!'); } + const id: SignedPreKeyIdType = `${ourUuid.toString()}:${keyId}`; + const fromDB = { - id: keyId, + id, + ourUuid: ourUuid.toString(), + keyId, publicKey: keyPair.pubKey, privateKey: keyPair.privKey, created_at: Date.now(), @@ -439,19 +484,20 @@ export class SignalProtocolStore extends EventsMixin { }; await window.Signal.Data.createOrUpdateSignedPreKey(fromDB); - this.signedPreKeys.set(keyId, { + this.signedPreKeys.set(id, { hydrated: false, fromDB, }); } - async removeSignedPreKey(keyId: number): Promise { + async removeSignedPreKey(ourUuid: UUID, keyId: number): Promise { if (!this.signedPreKeys) { throw new Error('removeSignedPreKey: this.signedPreKeys not yet cached!'); } - this.signedPreKeys.delete(keyId); - await window.Signal.Data.removeSignedPreKeyById(keyId); + const id: SignedPreKeyIdType = `${ourUuid.toString()}:${keyId}`; + this.signedPreKeys.delete(id); + await window.Signal.Data.removeSignedPreKeyById(id); } async clearSignedPreKeysStore(): Promise { @@ -464,13 +510,12 @@ export class SignalProtocolStore extends EventsMixin { // Sender Key Queue async enqueueSenderKeyJob( - encodedAddress: string, + qualifiedAddress: QualifiedAddress, task: () => Promise, zone = GLOBAL_ZONE ): Promise { return this.withZone(zone, 'enqueueSenderKeyJob', async () => { - const senderId = await normalizeEncodedAddress(encodedAddress); - const queue = this._getSenderKeyQueue(senderId); + const queue = this._getSenderKeyQueue(qualifiedAddress); return queue.add(task); }); @@ -480,25 +525,28 @@ export class SignalProtocolStore extends EventsMixin { return new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 }); } - private _getSenderKeyQueue(senderId: string): PQueue { - const cachedQueue = this.senderKeyQueues.get(senderId); + private _getSenderKeyQueue(senderId: QualifiedAddress): PQueue { + const cachedQueue = this.senderKeyQueues.get(senderId.toString()); if (cachedQueue) { return cachedQueue; } const freshQueue = this._createSenderKeyQueue(); - this.senderKeyQueues.set(senderId, freshQueue); + this.senderKeyQueues.set(senderId.toString(), freshQueue); return freshQueue; } // Sender Keys - private getSenderKeyId(senderKeyId: string, distributionId: string): string { - return `${senderKeyId}--${distributionId}`; + private getSenderKeyId( + senderKeyId: QualifiedAddress, + distributionId: string + ): SenderKeyIdType { + return `${senderKeyId.toString()}--${distributionId}`; } async saveSenderKey( - encodedAddress: string, + qualifiedAddress: QualifiedAddress, distributionId: string, record: SenderKeyRecord ): Promise { @@ -506,9 +554,10 @@ export class SignalProtocolStore extends EventsMixin { throw new Error('saveSenderKey: this.senderKeys not yet cached!'); } + const senderId = qualifiedAddress.toString(); + try { - const senderId = await normalizeEncodedAddress(encodedAddress); - const id = this.getSenderKeyId(senderId, distributionId); + const id = this.getSenderKeyId(qualifiedAddress, distributionId); const fromDB: SenderKeyType = { id, @@ -528,22 +577,23 @@ export class SignalProtocolStore extends EventsMixin { } catch (error) { const errorString = error && error.stack ? error.stack : error; window.log.error( - `saveSenderKey: failed to save senderKey ${encodedAddress}/${distributionId}: ${errorString}` + `saveSenderKey: failed to save senderKey ${senderId}/${distributionId}: ${errorString}` ); } } async getSenderKey( - encodedAddress: string, + qualifiedAddress: QualifiedAddress, distributionId: string ): Promise { if (!this.senderKeys) { throw new Error('getSenderKey: this.senderKeys not yet cached!'); } + const senderId = qualifiedAddress.toString(); + try { - const senderId = await normalizeEncodedAddress(encodedAddress); - const id = this.getSenderKeyId(senderId, distributionId); + const id = this.getSenderKeyId(qualifiedAddress, distributionId); const entry = this.senderKeys.get(id); if (!entry) { @@ -567,23 +617,24 @@ export class SignalProtocolStore extends EventsMixin { } catch (error) { const errorString = error && error.stack ? error.stack : error; window.log.error( - `getSenderKey: failed to load sender key ${encodedAddress}/${distributionId}: ${errorString}` + `getSenderKey: failed to load sender key ${senderId}/${distributionId}: ${errorString}` ); return undefined; } } async removeSenderKey( - encodedAddress: string, + qualifiedAddress: QualifiedAddress, distributionId: string ): Promise { if (!this.senderKeys) { throw new Error('getSenderKey: this.senderKeys not yet cached!'); } + const senderId = qualifiedAddress.toString(); + try { - const senderId = await normalizeEncodedAddress(encodedAddress); - const id = this.getSenderKeyId(senderId, distributionId); + const id = this.getSenderKeyId(qualifiedAddress, distributionId); await window.Signal.Data.removeSenderKeyById(id); @@ -591,7 +642,7 @@ export class SignalProtocolStore extends EventsMixin { } catch (error) { const errorString = error && error.stack ? error.stack : error; window.log.error( - `removeSenderKey: failed to remove senderKey ${encodedAddress}/${distributionId}: ${errorString}` + `removeSenderKey: failed to remove senderKey ${senderId}/${distributionId}: ${errorString}` ); } } @@ -606,13 +657,12 @@ export class SignalProtocolStore extends EventsMixin { // Session Queue async enqueueSessionJob( - encodedAddress: string, + qualifiedAddress: QualifiedAddress, task: () => Promise, zone: Zone = GLOBAL_ZONE ): Promise { return this.withZone(zone, 'enqueueSessionJob', async () => { - const id = await normalizeEncodedAddress(encodedAddress); - const queue = this._getSessionQueue(id); + const queue = this._getSessionQueue(qualifiedAddress); return queue.add(task); }); @@ -622,14 +672,14 @@ export class SignalProtocolStore extends EventsMixin { return new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 }); } - private _getSessionQueue(id: string): PQueue { - const cachedQueue = this.sessionQueues.get(id); + private _getSessionQueue(id: QualifiedAddress): PQueue { + const cachedQueue = this.sessionQueues.get(id.toString()); if (cachedQueue) { return cachedQueue; } const freshQueue = this._createSessionQueue(); - this.sessionQueues.set(id, freshQueue); + this.sessionQueues.set(id.toString(), freshQueue); return freshQueue; } @@ -805,7 +855,7 @@ export class SignalProtocolStore extends EventsMixin { } async loadSession( - encodedAddress: string, + qualifiedAddress: QualifiedAddress, { zone = GLOBAL_ZONE }: SessionTransactionOptions = {} ): Promise { return this.withZone(zone, 'loadSession', async () => { @@ -813,12 +863,13 @@ export class SignalProtocolStore extends EventsMixin { throw new Error('loadSession: this.sessions not yet cached!'); } - if (encodedAddress === null || encodedAddress === undefined) { - throw new Error('loadSession: encodedAddress was undefined/null'); + if (qualifiedAddress === null || qualifiedAddress === undefined) { + throw new Error('loadSession: qualifiedAddress was undefined/null'); } + const id = qualifiedAddress.toString(); + try { - const id = await normalizeEncodedAddress(encodedAddress); const map = this.pendingSessions.has(id) ? this.pendingSessions : this.sessions; @@ -838,7 +889,7 @@ export class SignalProtocolStore extends EventsMixin { } catch (error) { const errorString = error && error.stack ? error.stack : error; window.log.error( - `loadSession: failed to load session ${encodedAddress}: ${errorString}` + `loadSession: failed to load session ${id}: ${errorString}` ); return undefined; } @@ -846,12 +897,12 @@ export class SignalProtocolStore extends EventsMixin { } async loadSessions( - encodedAddresses: Array, + qualifiedAddresses: Array, { zone = GLOBAL_ZONE }: SessionTransactionOptions = {} ): Promise> { return this.withZone(zone, 'loadSessions', async () => { const sessions = await Promise.all( - encodedAddresses.map(async address => + qualifiedAddresses.map(async address => this.loadSession(address, { zone }) ) ); @@ -889,12 +940,14 @@ export class SignalProtocolStore extends EventsMixin { throw new Error('_maybeMigrateSession: Unknown session version type!'); } - const keyPair = await this.getIdentityKeyPair(); + const ourUuid = new UUID(session.ourUuid); + + const keyPair = await this.getIdentityKeyPair(ourUuid); if (!keyPair) { throw new Error('_maybeMigrateSession: No identity key for ourself!'); } - const localRegistrationId = await this.getLocalRegistrationId(); + const localRegistrationId = await this.getLocalRegistrationId(ourUuid); if (!isNumber(localRegistrationId)) { throw new Error('_maybeMigrateSession: No registration id for ourself!'); } @@ -915,13 +968,15 @@ export class SignalProtocolStore extends EventsMixin { Buffer.from(sessionStructureToArrayBuffer(sessionProto)) ); - await this.storeSession(session.id, record, { zone }); + await this.storeSession(QualifiedAddress.parse(session.id), record, { + zone, + }); return record; } async storeSession( - encodedAddress: string, + qualifiedAddress: QualifiedAddress, record: SessionRecord, { zone = GLOBAL_ZONE }: SessionTransactionOptions = {} ): Promise { @@ -930,18 +985,25 @@ export class SignalProtocolStore extends EventsMixin { throw new Error('storeSession: this.sessions not yet cached!'); } - if (encodedAddress === null || encodedAddress === undefined) { - throw new Error('storeSession: encodedAddress was undefined/null'); + if (qualifiedAddress === null || qualifiedAddress === undefined) { + throw new Error('storeSession: qualifiedAddress was undefined/null'); } - const unencoded = window.textsecure.utils.unencodeNumber(encodedAddress); - const deviceId = parseInt(unencoded[1], 10); + const { uuid, deviceId } = qualifiedAddress; + + const conversation = window.ConversationController.get(uuid.toString()); + strictAssert( + conversation !== undefined, + `Conversation not found for uuid: ${uuid}` + ); + const id = qualifiedAddress.toString(); try { - const id = await normalizeEncodedAddress(encodedAddress); const fromDB = { id, version: 2, - conversationId: window.textsecure.utils.unencodeNumber(id)[0], + ourUuid: qualifiedAddress.ourUuid.toString(), + conversationId: conversation.id, + uuid: uuid.toString(), deviceId, record: record.serialize().toString('base64'), }; @@ -962,15 +1024,14 @@ export class SignalProtocolStore extends EventsMixin { } } catch (error) { const errorString = error && error.stack ? error.stack : error; - window.log.error( - `storeSession: Save failed fo ${encodedAddress}: ${errorString}` - ); + window.log.error(`storeSession: Save failed for ${id}: ${errorString}`); throw error; } }); } async getOpenDevices( + ourUuid: UUID, identifiers: Array, { zone = GLOBAL_ZONE }: SessionTransactionOptions = {} ): Promise<{ @@ -986,27 +1047,17 @@ export class SignalProtocolStore extends EventsMixin { } try { - const conversationIds = new Map(); - identifiers.forEach(identifier => { - if (identifier === null || identifier === undefined) { - throw new Error('getOpenDevices: identifier was undefined/null'); - } - - const conversation = window.ConversationController.getOrCreate( - identifier, - 'private' - ); - if (!conversation) { - throw new Error( - `getOpenDevices: No conversationId found for identifier ${identifier}` - ); - } - conversationIds.set(conversation.get('id'), identifier); - }); + const uuidsOrIdentifiers = new Set( + identifiers.map( + identifier => UUID.lookup(identifier)?.toString() || identifier + ) + ); const allSessions = this._getAllSessions(); - const entries = allSessions.filter(session => - conversationIds.has(session.fromDB.conversationId) + const entries = allSessions.filter( + ({ fromDB }) => + fromDB.ourUuid === ourUuid.toString() && + uuidsOrIdentifiers.has(fromDB.uuid) ); const openEntries: Array< | undefined @@ -1043,37 +1094,21 @@ export class SignalProtocolStore extends EventsMixin { } const { entry, record } = item; - const { conversationId } = entry.fromDB; - conversationIds.delete(conversationId); + const { uuid } = entry.fromDB; + uuidsOrIdentifiers.delete(uuid); const id = entry.fromDB.deviceId; - const conversation = window.ConversationController.get( - conversationId - ); - if (!conversation) { - throw new Error( - `getOpenDevices: Unable to find matching conversation for ${conversationId}` - ); - } - - const identifier = - conversation.get('uuid') || conversation.get('e164'); - if (!identifier) { - throw new Error( - `getOpenDevices: No identifier for conversation ${conversationId}` - ); - } const registrationId = record.remoteRegistrationId(); return { - identifier, + identifier: uuid, id, registrationId, }; }) .filter(isNotNil); - const emptyIdentifiers = Array.from(conversationIds.values()); + const emptyIdentifiers = Array.from(uuidsOrIdentifiers.values()); return { devices, @@ -1089,27 +1124,31 @@ export class SignalProtocolStore extends EventsMixin { }); } - async getDeviceIds(identifier: string): Promise> { - const { devices } = await this.getOpenDevices([identifier]); + async getDeviceIds({ + ourUuid, + identifier, + }: Readonly<{ + ourUuid: UUID; + identifier: string; + }>): Promise> { + const { devices } = await this.getOpenDevices(ourUuid, [identifier]); return devices.map((device: DeviceType) => device.id); } - async removeSession(encodedAddress: string): Promise { + async removeSession(qualifiedAddress: QualifiedAddress): Promise { return this.withZone(GLOBAL_ZONE, 'removeSession', async () => { if (!this.sessions) { throw new Error('removeSession: this.sessions not yet cached!'); } - window.log.info('removeSession: deleting session for', encodedAddress); + const id = qualifiedAddress.toString(); + window.log.info('removeSession: deleting session for', id); try { - const id = await normalizeEncodedAddress(encodedAddress); await window.Signal.Data.removeSessionById(id); this.sessions.delete(id); this.pendingSessions.delete(id); } catch (e) { - window.log.error( - `removeSession: Failed to delete session for ${encodedAddress}` - ); + window.log.error(`removeSession: Failed to delete session for ${id}`); } }); } @@ -1127,6 +1166,7 @@ export class SignalProtocolStore extends EventsMixin { window.log.info('removeAllSessions: deleting sessions for', identifier); const id = window.ConversationController.getConversationId(identifier); + strictAssert(id, `Conversation not found: ${identifier}`); const entries = Array.from(this.sessions.values()); @@ -1138,7 +1178,7 @@ export class SignalProtocolStore extends EventsMixin { } } - await window.Signal.Data.removeSessionsByConversation(identifier); + await window.Signal.Data.removeSessionsByConversation(id); }); } @@ -1147,8 +1187,10 @@ export class SignalProtocolStore extends EventsMixin { return; } + const addr = QualifiedAddress.parse(entry.fromDB.id); + await this.enqueueSessionJob( - entry.fromDB.id, + addr, async () => { const item = entry.hydrated ? entry.item @@ -1160,21 +1202,21 @@ export class SignalProtocolStore extends EventsMixin { item.archiveCurrentState(); - await this.storeSession(entry.fromDB.id, item, { zone }); + await this.storeSession(addr, item, { zone }); }, zone ); } - async archiveSession(encodedAddress: string): Promise { + async archiveSession(qualifiedAddress: QualifiedAddress): Promise { return this.withZone(GLOBAL_ZONE, 'archiveSession', async () => { if (!this.sessions) { throw new Error('archiveSession: this.sessions not yet cached!'); } - window.log.info(`archiveSession: session for ${encodedAddress}`); + const id = qualifiedAddress.toString(); - const id = await normalizeEncodedAddress(encodedAddress); + window.log.info(`archiveSession: session for ${id}`); const entry = this.pendingSessions.get(id) || this.sessions.get(id); @@ -1183,7 +1225,7 @@ export class SignalProtocolStore extends EventsMixin { } async archiveSiblingSessions( - encodedAddress: string, + encodedAddress: Address, { zone = GLOBAL_ZONE }: SessionTransactionOptions = {} ): Promise { return this.withZone(zone, 'archiveSiblingSessions', async () => { @@ -1195,18 +1237,16 @@ export class SignalProtocolStore extends EventsMixin { window.log.info( 'archiveSiblingSessions: archiving sibling sessions for', - encodedAddress + encodedAddress.toString() ); - const id = await normalizeEncodedAddress(encodedAddress); - const [identifier, deviceId] = window.textsecure.utils.unencodeNumber(id); - const deviceIdNumber = parseInt(deviceId, 10); + const { uuid, deviceId } = encodedAddress; const allEntries = this._getAllSessions(); const entries = allEntries.filter( entry => - entry.fromDB.conversationId === identifier && - entry.fromDB.deviceId !== deviceIdNumber + entry.fromDB.uuid === uuid.toString() && + entry.fromDB.deviceId !== deviceId ); await Promise.all( @@ -1217,7 +1257,7 @@ export class SignalProtocolStore extends EventsMixin { }); } - async archiveAllSessions(identifier: string): Promise { + async archiveAllSessions(uuid: UUID): Promise { return this.withZone(GLOBAL_ZONE, 'archiveAllSessions', async () => { if (!this.sessions) { throw new Error('archiveAllSessions: this.sessions not yet cached!'); @@ -1225,14 +1265,12 @@ export class SignalProtocolStore extends EventsMixin { window.log.info( 'archiveAllSessions: archiving all sessions for', - identifier + uuid.toString() ); - const id = window.ConversationController.getConversationId(identifier); - const allEntries = this._getAllSessions(); const entries = allEntries.filter( - entry => entry.fromDB.conversationId === id + entry => entry.fromDB.uuid === uuid.toString() ); await Promise.all( @@ -1253,8 +1291,8 @@ export class SignalProtocolStore extends EventsMixin { }); } - async lightSessionReset(uuid: string, deviceId: number): Promise { - const id = `${uuid}.${deviceId}`; + async lightSessionReset(qualifiedAddress: QualifiedAddress): Promise { + const id = qualifiedAddress.toString(); const sessionResets = window.storage.get( 'sessionResets', @@ -1275,9 +1313,11 @@ export class SignalProtocolStore extends EventsMixin { window.storage.put('sessionResets', sessionResets); try { + const { uuid } = qualifiedAddress; + // First, fetch this conversation const conversationId = window.ConversationController.ensureContactIds({ - uuid, + uuid: uuid.toString(), }); assert(conversationId, `lightSessionReset/${id}: missing conversationId`); @@ -1287,12 +1327,17 @@ export class SignalProtocolStore extends EventsMixin { window.log.warn(`lightSessionReset/${id}: Resetting session`); // Archive open session with this device - await this.archiveSession(id); + await this.archiveSession(qualifiedAddress); // Send a null message with newly-created session const sendOptions = await getSendOptions(conversation.attributes); const result = await handleMessageSend( - window.textsecure.messaging.sendNullMessage({ uuid }, sendOptions), + window.textsecure.messaging.sendNullMessage( + { + uuid: uuid.toString(), + }, + sendOptions + ), { messageIds: [], sendType: 'nullMessage' } ); @@ -1315,19 +1360,14 @@ export class SignalProtocolStore extends EventsMixin { // Identity Keys - getIdentityRecord(identifier: string): IdentityKeyType | undefined { + getIdentityRecord(uuid: UUID): IdentityKeyType | undefined { if (!this.identityKeys) { throw new Error('getIdentityRecord: this.identityKeys not yet cached!'); } - try { - const id = window.ConversationController.getConversationId(identifier); - if (!id) { - throw new Error( - `getIdentityRecord: No conversation id for identifier ${identifier}` - ); - } + const id = uuid.toString(); + try { const entry = this.identityKeys.get(id); if (!entry) { return undefined; @@ -1336,34 +1376,74 @@ export class SignalProtocolStore extends EventsMixin { return entry.fromDB; } catch (e) { window.log.error( - `getIdentityRecord: Failed to get identity record for identifier ${identifier}` + `getIdentityRecord: Failed to get identity record for identifier ${id}` ); return undefined; } } + async getOrMigrateIdentityRecord( + uuid: UUID + ): Promise { + if (!this.identityKeys) { + throw new Error( + 'getOrMigrateIdentityRecord: this.identityKeys not yet cached!' + ); + } + + const result = this.getIdentityRecord(uuid); + if (result) { + return result; + } + + const newId = uuid.toString(); + const conversation = window.ConversationController.get(newId); + if (!conversation) { + return undefined; + } + + const conversationId = new UUID(conversation.id).toString(); + const record = this.identityKeys.get(`conversation:${conversationId}`); + if (!record) { + return undefined; + } + + const newRecord = { + ...record.fromDB, + id: newId, + }; + + window.log.info( + `SignalProtocolStore: migrating identity key from ${record.fromDB.id} ` + + `to ${newRecord.id}` + ); + + await this._saveIdentityKey(newRecord); + + this.identityKeys.delete(record.fromDB.id); + await window.Signal.Data.removeIdentityKeyById(record.fromDB.id); + + return newRecord; + } + async isTrustedIdentity( - encodedAddress: string, + encodedAddress: Address, publicKey: ArrayBuffer, direction: number ): Promise { if (!this.identityKeys) { - throw new Error('getIdentityRecord: this.identityKeys not yet cached!'); + throw new Error('isTrustedIdentity: this.identityKeys not yet cached!'); } if (encodedAddress === null || encodedAddress === undefined) { throw new Error('isTrustedIdentity: encodedAddress was undefined/null'); } - const identifier = window.textsecure.utils.unencodeNumber( - encodedAddress - )[0]; - const ourNumber = window.textsecure.storage.user.getNumber(); - const ourUuid = window.textsecure.storage.user.getUuid(); - const isOurIdentifier = - (ourNumber && identifier === ourNumber) || - (ourUuid && identifier === ourUuid); + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + const isOurIdentifier = encodedAddress.uuid.isEqual(ourUuid); - const identityRecord = this.getIdentityRecord(identifier); + const identityRecord = await this.getOrMigrateIdentityRecord( + encodedAddress.uuid + ); if (isOurIdentifier) { if (identityRecord && identityRecord.publicKey) { @@ -1418,12 +1498,11 @@ export class SignalProtocolStore extends EventsMixin { return true; } - async loadIdentityKey(identifier: string): Promise { - if (identifier === null || identifier === undefined) { - throw new Error('loadIdentityKey: identifier was undefined/null'); + async loadIdentityKey(uuid: UUID): Promise { + if (uuid === null || uuid === undefined) { + throw new Error('loadIdentityKey: uuid was undefined/null'); } - const id = window.textsecure.utils.unencodeNumber(identifier)[0]; - const identityRecord = this.getIdentityRecord(id); + const identityRecord = await this.getOrMigrateIdentityRecord(uuid); if (identityRecord) { return identityRecord.publicKey; @@ -1447,7 +1526,7 @@ export class SignalProtocolStore extends EventsMixin { } async saveIdentity( - encodedAddress: string, + encodedAddress: Address, publicKey: ArrayBuffer, nonblockingApproval = false, { zone }: SessionTransactionOptions = {} @@ -1468,14 +1547,11 @@ export class SignalProtocolStore extends EventsMixin { nonblockingApproval = false; } - const identifier = window.textsecure.utils.unencodeNumber( - encodedAddress - )[0]; - const identityRecord = this.getIdentityRecord(identifier); - const id = window.ConversationController.getOrCreate( - identifier, - 'private' - ).get('id'); + const identityRecord = await this.getOrMigrateIdentityRecord( + encodedAddress.uuid + ); + + const id = encodedAddress.uuid.toString(); if (!identityRecord || !identityRecord.publicKey) { // Lookup failed, or the current key was removed, so save this one. @@ -1516,7 +1592,7 @@ export class SignalProtocolStore extends EventsMixin { }); try { - this.trigger('keychange', identifier); + this.trigger('keychange', encodedAddress.uuid); } catch (error) { window.log.error( 'saveIdentity: error triggering keychange:', @@ -1526,7 +1602,9 @@ export class SignalProtocolStore extends EventsMixin { // Pass the zone to facilitate transactional session use in // MessageReceiver.ts - await this.archiveSiblingSessions(encodedAddress, { zone }); + await this.archiveSiblingSessions(encodedAddress, { + zone, + }); return true; } @@ -1551,24 +1629,17 @@ export class SignalProtocolStore extends EventsMixin { } async saveIdentityWithAttributes( - encodedAddress: string, + uuid: UUID, attributes: Partial ): Promise { - if (encodedAddress === null || encodedAddress === undefined) { - throw new Error( - 'saveIdentityWithAttributes: encodedAddress was undefined/null' - ); + if (uuid === null || uuid === undefined) { + throw new Error('saveIdentityWithAttributes: uuid was undefined/null'); } - const identifier = window.textsecure.utils.unencodeNumber( - encodedAddress - )[0]; - const identityRecord = this.getIdentityRecord(identifier); - const conv = window.ConversationController.getOrCreate( - identifier, - 'private' - ); - const id = conv.get('id'); + const identityRecord = await this.getOrMigrateIdentityRecord(uuid); + const id = uuid.toString(); + + window.ConversationController.getOrCreate(id, 'private'); const updates: Partial = { ...identityRecord, @@ -1581,24 +1652,18 @@ export class SignalProtocolStore extends EventsMixin { } } - async setApproval( - encodedAddress: string, - nonblockingApproval: boolean - ): Promise { - if (encodedAddress === null || encodedAddress === undefined) { - throw new Error('setApproval: encodedAddress was undefined/null'); + async setApproval(uuid: UUID, nonblockingApproval: boolean): Promise { + if (uuid === null || uuid === undefined) { + throw new Error('setApproval: uuid was undefined/null'); } if (typeof nonblockingApproval !== 'boolean') { throw new Error('setApproval: Invalid approval status'); } - const identifier = window.textsecure.utils.unencodeNumber( - encodedAddress - )[0]; - const identityRecord = this.getIdentityRecord(identifier); + const identityRecord = await this.getOrMigrateIdentityRecord(uuid); if (!identityRecord) { - throw new Error(`setApproval: No identity record for ${identifier}`); + throw new Error(`setApproval: No identity record for ${uuid}`); } identityRecord.nonblockingApproval = nonblockingApproval; @@ -1606,12 +1671,12 @@ export class SignalProtocolStore extends EventsMixin { } async setVerified( - encodedAddress: string, + uuid: UUID, verifiedStatus: number, publicKey?: ArrayBuffer ): Promise { - if (encodedAddress === null || encodedAddress === undefined) { - throw new Error('setVerified: encodedAddress was undefined/null'); + if (uuid === null || uuid === undefined) { + throw new Error('setVerified: uuid was undefined/null'); } if (!validateVerifiedStatus(verifiedStatus)) { throw new Error('setVerified: Invalid verified status'); @@ -1620,10 +1685,10 @@ export class SignalProtocolStore extends EventsMixin { throw new Error('setVerified: Invalid public key'); } - const identityRecord = this.getIdentityRecord(encodedAddress); + const identityRecord = await this.getOrMigrateIdentityRecord(uuid); if (!identityRecord) { - throw new Error(`setVerified: No identity record for ${encodedAddress}`); + throw new Error(`setVerified: No identity record for ${uuid.toString()}`); } if (!publicKey || constantTimeEqual(identityRecord.publicKey, publicKey)) { @@ -1639,14 +1704,14 @@ export class SignalProtocolStore extends EventsMixin { } } - async getVerified(identifier: string): Promise { - if (identifier === null || identifier === undefined) { - throw new Error('getVerified: identifier was undefined/null'); + async getVerified(uuid: UUID): Promise { + if (uuid === null || uuid === undefined) { + throw new Error('getVerified: uuid was undefined/null'); } - const identityRecord = this.getIdentityRecord(identifier); + const identityRecord = await this.getOrMigrateIdentityRecord(uuid); if (!identityRecord) { - throw new Error(`getVerified: No identity record for ${identifier}`); + throw new Error(`getVerified: No identity record for ${uuid}`); } const verifiedStatus = identityRecord.verified; @@ -1659,38 +1724,32 @@ export class SignalProtocolStore extends EventsMixin { // Resolves to true if a new identity key was saved processContactSyncVerificationState( - identifier: string, + uuid: UUID, verifiedStatus: number, publicKey: ArrayBuffer ): Promise { if (verifiedStatus === VerifiedStatus.UNVERIFIED) { - return this.processUnverifiedMessage( - identifier, - verifiedStatus, - publicKey - ); + return this.processUnverifiedMessage(uuid, verifiedStatus, publicKey); } - return this.processVerifiedMessage(identifier, verifiedStatus, publicKey); + return this.processVerifiedMessage(uuid, verifiedStatus, publicKey); } // This function encapsulates the non-Java behavior, since the mobile apps don't // currently receive contact syncs and therefore will see a verify sync with // UNVERIFIED status async processUnverifiedMessage( - identifier: string, + uuid: UUID, verifiedStatus: number, publicKey?: ArrayBuffer ): Promise { - if (identifier === null || identifier === undefined) { - throw new Error( - 'processUnverifiedMessage: identifier was undefined/null' - ); + if (uuid === null || uuid === undefined) { + throw new Error('processUnverifiedMessage: uuid was undefined/null'); } if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) { throw new Error('processUnverifiedMessage: Invalid public key'); } - const identityRecord = this.getIdentityRecord(identifier); + const identityRecord = await this.getOrMigrateIdentityRecord(uuid); let isEqual = false; @@ -1703,12 +1762,12 @@ export class SignalProtocolStore extends EventsMixin { isEqual && identityRecord.verified !== VerifiedStatus.UNVERIFIED ) { - await this.setVerified(identifier, verifiedStatus, publicKey); + await this.setVerified(uuid, verifiedStatus, publicKey); return false; } if (!identityRecord || !isEqual) { - await this.saveIdentityWithAttributes(identifier, { + await this.saveIdentityWithAttributes(uuid, { publicKey, verified: verifiedStatus, firstUse: false, @@ -1718,7 +1777,7 @@ export class SignalProtocolStore extends EventsMixin { if (identityRecord && !isEqual) { try { - this.trigger('keychange', identifier); + this.trigger('keychange', uuid); } catch (error) { window.log.error( 'processUnverifiedMessage: error triggering keychange:', @@ -1726,7 +1785,7 @@ export class SignalProtocolStore extends EventsMixin { ); } - await this.archiveAllSessions(identifier); + await this.archiveAllSessions(uuid); return true; } @@ -1742,12 +1801,12 @@ export class SignalProtocolStore extends EventsMixin { // This matches the Java method as of // https://github.com/signalapp/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188 async processVerifiedMessage( - identifier: string, + uuid: UUID, verifiedStatus: number, publicKey?: ArrayBuffer ): Promise { - if (identifier === null || identifier === undefined) { - throw new Error('processVerifiedMessage: identifier was undefined/null'); + if (uuid === null || uuid === undefined) { + throw new Error('processVerifiedMessage: uuid was undefined/null'); } if (!validateVerifiedStatus(verifiedStatus)) { throw new Error('processVerifiedMessage: Invalid verified status'); @@ -1756,7 +1815,7 @@ export class SignalProtocolStore extends EventsMixin { throw new Error('processVerifiedMessage: Invalid public key'); } - const identityRecord = this.getIdentityRecord(identifier); + const identityRecord = await this.getOrMigrateIdentityRecord(uuid); let isEqual = false; @@ -1777,7 +1836,7 @@ export class SignalProtocolStore extends EventsMixin { identityRecord.verified !== VerifiedStatus.DEFAULT && verifiedStatus === VerifiedStatus.DEFAULT ) { - await this.setVerified(identifier, verifiedStatus, publicKey); + await this.setVerified(uuid, verifiedStatus, publicKey); return false; } @@ -1787,7 +1846,7 @@ export class SignalProtocolStore extends EventsMixin { (identityRecord && !isEqual) || (identityRecord && identityRecord.verified !== VerifiedStatus.VERIFIED)) ) { - await this.saveIdentityWithAttributes(identifier, { + await this.saveIdentityWithAttributes(uuid, { publicKey, verified: verifiedStatus, firstUse: false, @@ -1797,7 +1856,7 @@ export class SignalProtocolStore extends EventsMixin { if (identityRecord && !isEqual) { try { - this.trigger('keychange', identifier); + this.trigger('keychange', uuid); } catch (error) { window.log.error( 'processVerifiedMessage error triggering keychange:', @@ -1805,7 +1864,7 @@ export class SignalProtocolStore extends EventsMixin { ); } - await this.archiveAllSessions(identifier); + await this.archiveAllSessions(uuid); // true signifies that we overwrote a previous key with a new one return true; @@ -1818,14 +1877,14 @@ export class SignalProtocolStore extends EventsMixin { return false; } - isUntrusted(identifier: string): boolean { - if (identifier === null || identifier === undefined) { - throw new Error('isUntrusted: identifier was undefined/null'); + isUntrusted(uuid: UUID): boolean { + if (uuid === null || uuid === undefined) { + throw new Error('isUntrusted: uuid was undefined/null'); } - const identityRecord = this.getIdentityRecord(identifier); + const identityRecord = this.getIdentityRecord(uuid); if (!identityRecord) { - throw new Error(`isUntrusted: No identity record for ${identifier}`); + throw new Error(`isUntrusted: No identity record for ${uuid.toString()}`); } if ( @@ -1839,17 +1898,15 @@ export class SignalProtocolStore extends EventsMixin { return false; } - async removeIdentityKey(identifier: string): Promise { + async removeIdentityKey(uuid: UUID): Promise { if (!this.identityKeys) { throw new Error('removeIdentityKey: this.identityKeys not yet cached!'); } - const id = window.ConversationController.getConversationId(identifier); - if (id) { - this.identityKeys.delete(id); - await window.Signal.Data.removeIdentityKeyById(id); - await this.removeAllSessions(id); - } + const id = uuid.toString(); + this.identityKeys.delete(id); + await window.Signal.Data.removeIdentityKeyById(id); + await this.removeAllSessions(id); } // Not yet processed messages - for resiliency diff --git a/ts/background.ts b/ts/background.ts index a5da8d2c74..b8b7e6854e 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -66,6 +66,7 @@ import { EnvelopeEvent, } from './textsecure/messageReceiverEvents'; import type { WebAPIType } from './textsecure/WebAPI'; +import * as KeyChangeListener from './textsecure/KeyChangeListener'; import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation'; import { getSendOptions } from './util/getSendOptions'; import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff'; @@ -497,7 +498,7 @@ export async function startApp(): Promise { window.document.title = window.getTitle(); - window.Whisper.KeyChangeListener.init(window.textsecure.storage.protocol); + KeyChangeListener.init(window.textsecure.storage.protocol); window.textsecure.storage.protocol.on('removePreKey', () => { window.getAccountManager().refreshPreKeys(); }); @@ -921,7 +922,7 @@ export async function startApp(): Promise { conversation.format() ); const ourNumber = window.textsecure.storage.user.getNumber(); - const ourUuid = window.textsecure.storage.user.getUuid(); + const ourUuid = window.textsecure.storage.user.getUuid()?.toString(); const ourConversationId = window.ConversationController.getOurConversationId(); const themeSetting = window.Events.getThemeSetting(); @@ -1065,7 +1066,7 @@ export async function startApp(): Promise { window.Whisper.events.on('userChanged', (reconnect = false) => { const newDeviceId = window.textsecure.storage.user.getDeviceId(); const newNumber = window.textsecure.storage.user.getNumber(); - const newUuid = window.textsecure.storage.user.getUuid(); + const newUuid = window.textsecure.storage.user.getUuid()?.toString(); const ourConversation = window.ConversationController.getOurConversation(); if (ourConversation?.get('e164') !== newNumber) { @@ -2136,30 +2137,9 @@ export async function startApp(): Promise { const deviceId = window.textsecure.storage.user.getDeviceId(); - // If we didn't capture a UUID on registration, go get it from the server if (!window.textsecure.storage.user.getUuid()) { - try { - const { uuid } = await server.whoami(); - assert(deviceId, 'We should have device id'); - window.textsecure.storage.user.setUuidAndDeviceId(uuid, deviceId); - const ourNumber = window.textsecure.storage.user.getNumber(); - - assert(ourNumber, 'We should have number'); - const me = await window.ConversationController.getOrCreateAndWait( - ourNumber, - 'private' - ); - me.updateUuid(uuid); - - await server.authenticate( - window.textsecure.storage.user.getWebAPICredentials() - ); - } catch (error) { - window.log.error( - 'Error: Unable to retrieve UUID from service.', - error && error.stack ? error.stack : error - ); - } + window.log.error('UUID not captured during registration, unlinking'); + return unlinkAndDisconnect(RemoveAllConfiguration.Full); } if (connectCount === 1) { @@ -2587,7 +2567,7 @@ export async function startApp(): Promise { (details.number && details.number === window.textsecure.storage.user.getNumber()) || (details.uuid && - details.uuid === window.textsecure.storage.user.getUuid()) + details.uuid === window.textsecure.storage.user.getUuid()?.toString()) ) { // special case for syncing details about ourselves if (details.profileKey) { @@ -2845,7 +2825,7 @@ export async function startApp(): Promise { }); function onEnvelopeReceived({ envelope }: EnvelopeEvent) { - const ourUuid = window.textsecure.storage.user.getUuid(); + const ourUuid = window.textsecure.storage.user.getUuid()?.toString(); if (envelope.sourceUuid && envelope.sourceUuid !== ourUuid) { window.ConversationController.ensureContactIds({ e164: envelope.source, @@ -3083,7 +3063,7 @@ export async function startApp(): Promise { return new window.Whisper.Message(({ source: window.textsecure.storage.user.getNumber(), - sourceUuid: window.textsecure.storage.user.getUuid(), + sourceUuid: window.textsecure.storage.user.getUuid()?.toString(), sourceDevice: data.device, sent_at: timestamp, serverTimestamp: data.serverTimestamp, @@ -3216,7 +3196,7 @@ export async function startApp(): Promise { const { data, confirm } = event; const source = window.textsecure.storage.user.getNumber(); - const sourceUuid = window.textsecure.storage.user.getUuid(); + const sourceUuid = window.textsecure.storage.user.getUuid()?.toString(); strictAssert(source && sourceUuid, 'Missing user number and uuid'); const messageDescriptor = getMessageDescriptor({ @@ -3492,7 +3472,7 @@ export async function startApp(): Promise { switch (eventType) { case FETCH_LATEST_ENUM.LOCAL_PROFILE: { - const ourUuid = window.textsecure.storage.user.getUuid(); + const ourUuid = window.textsecure.storage.user.getUuid()?.toString(); const ourE164 = window.textsecure.storage.user.getNumber(); await getProfile(ourUuid, ourE164); break; diff --git a/ts/groups.ts b/ts/groups.ts index 63f70e949d..fbb21d12a0 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -550,19 +550,12 @@ function buildGroupProto( ); } - const me = window.ConversationController.get(ourConversationId); - if (!me) { - throw new Error( - `buildGroupProto/${logId}: unable to find our own conversation!` - ); - } + const ourUuid = window.storage.user.getCheckedUuid(); - const ourUuid = me.get('uuid'); - if (!ourUuid) { - throw new Error(`buildGroupProto/${logId}: unable to find our own uuid!`); - } - - const ourUuidCipherTextBuffer = encryptUuid(clientZkGroupCipher, ourUuid); + const ourUuidCipherTextBuffer = encryptUuid( + clientZkGroupCipher, + ourUuid.toString() + ); proto.membersPendingProfileKey = (attributes.pendingMembersV2 || []).map( item => { @@ -627,15 +620,11 @@ export async function buildAddMembersChange( ); const clientZkGroupCipher = getClientZkGroupCipher(secretParams); - const ourConversationId = window.ConversationController.getOurConversationIdOrThrow(); - const ourConversation = window.ConversationController.get(ourConversationId); - const ourUuid = ourConversation?.get('uuid'); - if (!ourUuid) { - throw new Error( - `buildAddMembersChange/${logId}: unable to find our own UUID!` - ); - } - const ourUuidCipherTextBuffer = encryptUuid(clientZkGroupCipher, ourUuid); + const ourUuid = window.storage.user.getCheckedUuid(); + const ourUuidCipherTextBuffer = encryptUuid( + clientZkGroupCipher, + ourUuid.toString() + ); const now = Date.now(); @@ -1727,10 +1716,12 @@ export async function createGroupV2({ timestamp, }); + const ourUuid = window.storage.user.getCheckedUuid(); + const createdTheGroupMessage: MessageAttributesType = { ...generateBasicMessage(), type: 'group-v2-change', - sourceUuid: conversation.ourUuid, + sourceUuid: ourUuid.toString(), conversationId: conversation.id, received_at: window.Signal.Util.incrementMessageCounter(), received_at_ms: timestamp, diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index b54635cd4d..3ced9c0969 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -37,6 +37,7 @@ import { missingCaseError } from '../util/missingCaseError'; import { sniffImageMimeType } from '../util/sniffImageMimeType'; import { isValidE164 } from '../util/isValidE164'; import { MIMEType, IMAGE_WEBP } from '../types/MIME'; +import { UUID } from '../types/UUID'; import { arrayBufferToBase64, base64ToArrayBuffer, @@ -157,8 +158,6 @@ export class ConversationModel extends window.Backbone jobQueue?: typeof window.PQueueType; - ourUuid?: string; - storeName?: string | null; throttledBumpTyping?: () => void; @@ -239,7 +238,6 @@ export class ConversationModel extends window.Backbone this.storeName = 'conversations'; - this.ourUuid = window.textsecure.storage.user.getUuid(); this.verifiedEnum = window.textsecure.storage.protocol.VerifiedStatus; // This may be overridden by window.ConversationController.getOrCreate, and signify @@ -2105,7 +2103,14 @@ export class ConversationModel extends window.Backbone } async safeGetVerified(): Promise { - const promise = window.textsecure.storage.protocol.getVerified(this.id); + const uuid = this.get('uuid'); + if (!uuid) { + return window.textsecure.storage.protocol.VerifiedStatus.DEFAULT; + } + + const promise = window.textsecure.storage.protocol.getVerified( + new UUID(uuid) + ); return promise.catch( () => window.textsecure.storage.protocol.VerifiedStatus.DEFAULT ); @@ -2182,21 +2187,31 @@ export class ConversationModel extends window.Backbone ); } + const uuid = this.get('uuid'); const beginningVerified = this.get('verified'); let keyChange; if (options.viaSyncMessage) { + strictAssert( + uuid, + `Sync message didn't update uuid for conversation: ${this.id}` + ); + // handle the incoming key from the sync messages - need different // behavior if that key doesn't match the current key keyChange = await window.textsecure.storage.protocol.processVerifiedMessage( - this.id, + new UUID(uuid), verified, options.key || undefined ); - } else { + } else if (uuid) { keyChange = await window.textsecure.storage.protocol.setVerified( - this.id, + new UUID(uuid), verified ); + } else { + window.log.warn( + `_setVerified(${this.id}): no uuid to update protocol storage` + ); } this.set({ verified }); @@ -2227,12 +2242,8 @@ export class ConversationModel extends window.Backbone local: !options.viaSyncMessage, }); } - if (!options.viaSyncMessage) { - await this.sendVerifySyncMessage( - this.get('e164'), - this.get('uuid'), - verified - ); + if (!options.viaSyncMessage && uuid) { + await this.sendVerifySyncMessage(this.get('e164'), uuid, verified); } return keyChange; @@ -2240,7 +2251,7 @@ export class ConversationModel extends window.Backbone async sendVerifySyncMessage( e164: string | undefined, - uuid: string | undefined, + uuid: string, state: number ): Promise { const identifier = uuid || e164; @@ -2268,7 +2279,7 @@ export class ConversationModel extends window.Backbone const options = { ...sendOptions, ...contactSendOptions }; const key = await window.textsecure.storage.protocol.loadIdentityKey( - identifier + UUID.checkedLookup(identifier) ); if (!key) { throw new Error( @@ -2353,12 +2364,20 @@ export class ConversationModel extends window.Backbone ); } - return window.textsecure.storage.protocol.setApproval(this.id, true); + const uuid = this.get('uuid'); + if (!uuid) { + window.log.warn(`setApproved(${this.id}): no uuid, ignoring`); + return; + } + + return window.textsecure.storage.protocol.setApproval(new UUID(uuid), true); } safeIsUntrusted(): boolean { + const uuid = this.get('uuid'); try { - return window.textsecure.storage.protocol.isUntrusted(this.id); + strictAssert(uuid, `No uuid for conversation: ${this.id}`); + return window.textsecure.storage.protocol.isUntrusted(new UUID(uuid)); } catch (err) { return false; } @@ -2526,11 +2545,11 @@ export class ConversationModel extends window.Backbone await this.notify(model); } - async addKeyChange(keyChangedId: string): Promise { + async addKeyChange(keyChangedId: UUID): Promise { window.log.info( 'adding key change advisory for', this.idForLogging(), - keyChangedId, + keyChangedId.toString(), this.get('timestamp') ); @@ -2541,7 +2560,7 @@ export class ConversationModel extends window.Backbone sent_at: this.get('timestamp'), received_at: window.Signal.Util.incrementMessageCounter(), received_at_ms: timestamp, - key_changed: keyChangedId, + key_changed: keyChangedId.toString(), readStatus: ReadStatus.Unread, schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY, // TODO: DESKTOP-722 @@ -4839,7 +4858,7 @@ export class ConversationModel extends window.Backbone return; } - const ourUuid = window.textsecure.storage.user.getUuid(); + const ourUuid = window.textsecure.storage.user.getUuid()?.toString(); const mentionsMe = (message.get('bodyRanges') || []).some( range => range.mentionUuid && range.mentionUuid === ourUuid ); diff --git a/ts/models/messages.ts b/ts/models/messages.ts index c24f145686..e39d62dd05 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -221,7 +221,7 @@ export class MessageModel extends window.Backbone.Model { this.CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT; this.INITIAL_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.INITIAL; - this.OUR_UUID = window.textsecure.storage.user.getUuid(); + this.OUR_UUID = window.textsecure.storage.user.getUuid()?.toString(); this.on('change', this.notifyRedux); } @@ -2881,7 +2881,8 @@ export class MessageModel extends window.Backbone.Model { const profileKey = dataMessage.profileKey.toString('base64'); if ( source === window.textsecure.storage.user.getNumber() || - sourceUuid === window.textsecure.storage.user.getUuid() + sourceUuid === + window.textsecure.storage.user.getUuid()?.toString() ) { conversation.set({ profileSharing: true }); } else if (isDirectConversation(conversation.attributes)) { diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 0a452ae237..6ecb97a51d 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -53,6 +53,7 @@ import { ProcessGroupCallRingRequestResult, } from '../types/Calling'; import { LocalizerType } from '../types/Util'; +import { UUID } from '../types/UUID'; import { ConversationModel } from '../models/conversations'; import * as Bytes from '../Bytes'; import { @@ -60,7 +61,6 @@ import { arrayBufferToUuid, typedArrayToArrayBuffer, } from '../Crypto'; -import { assert } from '../util/assert'; import { dropNull, shallowDropNull } from '../util/dropNull'; import { getOwn } from '../util/getOwn'; import * as durations from '../util/durations'; @@ -262,7 +262,7 @@ export class CallingClass { } private attemptToGiveOurUuidToRingRtc(): void { - const ourUuid = window.textsecure.storage.user.getUuid(); + const ourUuid = window.textsecure.storage.user.getUuid()?.toString(); if (!ourUuid) { // This can happen if we're not linked. It's okay if we hit this case. return; @@ -1392,15 +1392,17 @@ export class CallingClass { return; } - const remoteUserId = envelope.sourceUuid || envelope.source; + const remoteUserId = envelope.sourceUuid; const remoteDeviceId = this.parseDeviceId(envelope.sourceDevice); if (!remoteUserId || !remoteDeviceId || !this.localDeviceId) { window.log.error('Missing identifier, ignoring call message.'); return; } - const senderIdentityRecord = window.textsecure.storage.protocol.getIdentityRecord( - remoteUserId + const { storage } = window.textsecure; + + const senderIdentityRecord = await storage.protocol.getOrMigrateIdentityRecord( + new UUID(remoteUserId) ); if (!senderIdentityRecord) { window.log.error( @@ -1410,14 +1412,9 @@ export class CallingClass { } const senderIdentityKey = senderIdentityRecord.publicKey.slice(1); // Ignore the type header, it is not used. - const ourIdentifier = - window.textsecure.storage.user.getUuid() || - window.textsecure.storage.user.getNumber(); - assert(ourIdentifier, 'We should have either uuid or number'); + const ourUuid = storage.user.getCheckedUuid(); - const receiverIdentityRecord = window.textsecure.storage.protocol.getIdentityRecord( - ourIdentifier - ); + const receiverIdentityRecord = storage.protocol.getIdentityRecord(ourUuid); if (!receiverIdentityRecord) { window.log.error( 'Missing receiver identity record; ignoring call message.' diff --git a/ts/services/groupCredentialFetcher.ts b/ts/services/groupCredentialFetcher.ts index 5942cc8a41..6164bcbd36 100644 --- a/ts/services/groupCredentialFetcher.ts +++ b/ts/services/groupCredentialFetcher.ts @@ -115,7 +115,7 @@ export function getCredentialsForToday( } export async function maybeFetchNewCredentials(): Promise { - const uuid = window.textsecure.storage.user.getUuid(); + const uuid = window.textsecure.storage.user.getUuid()?.toString(); if (!uuid) { window.log.info('maybeFetchCredentials: no UUID, returning early'); return; diff --git a/ts/services/storage.ts b/ts/services/storage.ts index 53887466a1..b571267b72 100644 --- a/ts/services/storage.ts +++ b/ts/services/storage.ts @@ -166,6 +166,11 @@ async function generateManifest( storageRecord.account = await toAccountRecord(conversation); identifier.type = ITEM_TYPE.ACCOUNT; } else if (conversationType === ConversationTypes.Direct) { + // Contacts must have UUID + if (!conversation.get('uuid')) { + continue; + } + storageRecord = new Proto.StorageRecord(); // eslint-disable-next-line no-await-in-loop storageRecord.contact = await toContactRecord(conversation); diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 64d3e2f5fd..6771c0d99c 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -35,6 +35,8 @@ import { } from '../util/universalExpireTimer'; import { ourProfileKeyService } from './ourProfileKey'; import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation'; +import { UUID } from '../types/UUID'; +import * as Errors from '../types/errors'; import { SignalService as Proto } from '../protobuf'; const { updateConversation } = dataInterface; @@ -118,9 +120,19 @@ export async function toContactRecord( if (profileKey) { contactRecord.profileKey = Bytes.fromBase64(String(profileKey)); } - const identityKey = await window.textsecure.storage.protocol.loadIdentityKey( - conversation.id - ); + + let maybeUuid: UUID | undefined; + try { + maybeUuid = uuid ? new UUID(uuid) : undefined; + } catch (error) { + window.log.warn( + `Invalid uuid in contact record: ${Errors.toLogFormat(error)}` + ); + } + + const identityKey = maybeUuid + ? await window.textsecure.storage.protocol.loadIdentityKey(maybeUuid) + : undefined; if (identityKey) { contactRecord.identityKey = new FIXMEU8(identityKey); } @@ -723,6 +735,11 @@ export async function mergeContactRecord( const e164 = contactRecord.serviceE164 || undefined; const uuid = contactRecord.serviceUuid || undefined; + // All contacts must have UUID + if (!uuid) { + return false; + } + const id = window.ConversationController.ensureContactIds({ e164, uuid, diff --git a/ts/sql/Client.ts b/ts/sql/Client.ts index b31780366c..b0e2f60fe8 100644 --- a/ts/sql/Client.ts +++ b/ts/sql/Client.ts @@ -50,14 +50,17 @@ import { ConversationType, DeleteSentProtoRecipientOptionsType, IdentityKeyType, + IdentityKeyIdType, ItemKeyType, ItemType, LastConversationMessagesType, MessageType, MessageTypeUnhydrated, PreKeyType, + PreKeyIdType, SearchResultMessageType, SenderKeyType, + SenderKeyIdType, SentMessageDBType, SentMessagesType, SentProtoType, @@ -66,7 +69,9 @@ import { SentRecipientsType, ServerInterface, SessionType, + SessionIdType, SignedPreKeyType, + SignedPreKeyIdType, StickerPackStatusType, StickerPackType, StickerType, @@ -634,17 +639,10 @@ async function removeIndexedDBFiles() { const IDENTITY_KEY_KEYS = ['publicKey']; async function createOrUpdateIdentityKey(data: IdentityKeyType) { - const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, { - ...data, - id: window.ConversationController.getConversationId(data.id), - }); + const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, data); await channels.createOrUpdateIdentityKey(updated); } -async function getIdentityKeyById(identifier: string) { - const id = window.ConversationController.getConversationId(identifier); - if (!id) { - throw new Error('getIdentityKeyById: unable to find conversationId'); - } +async function getIdentityKeyById(id: IdentityKeyIdType) { const data = await channels.getIdentityKeyById(id); return keysToArrayBuffer(IDENTITY_KEY_KEYS, data); @@ -655,11 +653,7 @@ async function bulkAddIdentityKeys(array: Array) { ); await channels.bulkAddIdentityKeys(updated); } -async function removeIdentityKeyById(identifier: string) { - const id = window.ConversationController.getConversationId(identifier); - if (!id) { - throw new Error('removeIdentityKeyById: unable to find conversationId'); - } +async function removeIdentityKeyById(id: IdentityKeyIdType) { await channels.removeIdentityKeyById(id); } async function removeAllIdentityKeys() { @@ -677,7 +671,7 @@ async function createOrUpdatePreKey(data: PreKeyType) { const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data); await channels.createOrUpdatePreKey(updated); } -async function getPreKeyById(id: number) { +async function getPreKeyById(id: PreKeyIdType) { const data = await channels.getPreKeyById(id); return keysToArrayBuffer(PRE_KEY_KEYS, data); @@ -686,7 +680,7 @@ async function bulkAddPreKeys(array: Array) { const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); await channels.bulkAddPreKeys(updated); } -async function removePreKeyById(id: number) { +async function removePreKeyById(id: PreKeyIdType) { await channels.removePreKeyById(id); } async function removeAllPreKeys() { @@ -705,7 +699,7 @@ async function createOrUpdateSignedPreKey(data: SignedPreKeyType) { const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data); await channels.createOrUpdateSignedPreKey(updated); } -async function getSignedPreKeyById(id: number) { +async function getSignedPreKeyById(id: SignedPreKeyIdType) { const data = await channels.getSignedPreKeyById(id); return keysToArrayBuffer(PRE_KEY_KEYS, data); @@ -721,7 +715,7 @@ async function bulkAddSignedPreKeys(array: Array) { const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); await channels.bulkAddSignedPreKeys(updated); } -async function removeSignedPreKeyById(id: number) { +async function removeSignedPreKeyById(id: SignedPreKeyIdType) { await channels.removeSignedPreKeyById(id); } async function removeAllSignedPreKeys() { @@ -731,7 +725,6 @@ async function removeAllSignedPreKeys() { // Items const ITEM_KEYS: Partial>> = { - identityKey: ['value.pubKey', 'value.privKey'], senderCertificate: ['value.serialized'], senderCertificateNoE164: ['value.serialized'], profileKey: ['value'], @@ -749,7 +742,9 @@ async function createOrUpdateItem(data: ItemType) { await channels.createOrUpdateItem(updated); } -async function getItemById(id: K): Promise> { +async function getItemById( + id: K +): Promise | undefined> { const keys = ITEM_KEYS[id]; const data = await channels.getItemById(id); @@ -788,7 +783,7 @@ async function createOrUpdateSenderKey(key: SenderKeyType): Promise { await channels.createOrUpdateSenderKey(key); } async function getSenderKeyById( - id: string + id: SenderKeyIdType ): Promise { return channels.getSenderKeyById(id); } @@ -798,7 +793,7 @@ async function removeAllSenderKeys(): Promise { async function getAllSenderKeys(): Promise> { return channels.getAllSenderKeys(); } -async function removeSenderKeyById(id: string): Promise { +async function removeSenderKeyById(id: SenderKeyIdType): Promise { return channels.removeSenderKeyById(id); } @@ -879,7 +874,7 @@ async function commitSessionsAndUnprocessed(options: { async function bulkAddSessions(array: Array) { await channels.bulkAddSessions(array); } -async function removeSessionById(id: string) { +async function removeSessionById(id: SessionIdType) { await channels.removeSessionById(id); } diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 7343f9c3a3..3fa9dffe41 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -19,6 +19,8 @@ import type { ProcessGroupCallRingRequestResult } from '../types/Calling'; import { StorageAccessType } from '../types/Storage.d'; import type { AttachmentType } from '../types/Attachment'; import { BodyRangesType } from '../types/Util'; +import type { QualifiedAddressStringType } from '../types/QualifiedAddress'; +import type { UUIDStringType } from '../types/UUID'; import type { RemoveAllConfiguration } from '../types/RemoveAllConfiguration'; export type AttachmentDownloadJobTypeType = @@ -57,14 +59,17 @@ export type EmojiType = { shortName: string; lastUsage: number; }; + export type IdentityKeyType = { firstUse: boolean; - id: string; + id: UUIDStringType | `conversation:${UUIDStringType}`; nonblockingApproval: boolean; publicKey: ArrayBuffer; timestamp: number; verified: number; }; +export type IdentityKeyIdType = IdentityKeyType['id']; + export type ItemKeyType = keyof StorageAccessType; export type AllItemsType = Partial; export type ItemType = { @@ -76,10 +81,13 @@ export type MessageTypeUnhydrated = { json: string; }; export type PreKeyType = { - id: number; + id: `${UUIDStringType}:${number}`; + keyId: number; + ourUuid: UUIDStringType; privateKey: ArrayBuffer; publicKey: ArrayBuffer; }; +export type PreKeyIdType = PreKeyType['id']; export type SearchResultMessageType = { json: string; snippet: string; @@ -114,7 +122,7 @@ export type SentMessageDBType = { export type SenderKeyType = { // Primary key - id: string; + id: `${QualifiedAddressStringType}--${string}`; // These two are combined into one string to give us the final id senderId: string; distributionId: string; @@ -122,21 +130,28 @@ export type SenderKeyType = { data: Buffer; lastUpdatedDate: number; }; +export type SenderKeyIdType = SenderKeyType['id']; export type SessionType = { - id: string; + id: QualifiedAddressStringType; + ourUuid: UUIDStringType; + uuid: UUIDStringType; conversationId: string; deviceId: number; record: string; version?: number; }; +export type SessionIdType = SessionType['id']; export type SignedPreKeyType = { confirmed: boolean; // eslint-disable-next-line camelcase created_at: number; - id: number; + ourUuid: UUIDStringType; + id: `${UUIDStringType}:${number}`; + keyId: number; privateKey: ArrayBuffer; publicKey: ArrayBuffer; }; +export type SignedPreKeyIdType = SignedPreKeyType['id']; export type StickerType = Readonly<{ id: number; @@ -227,23 +242,27 @@ export type DataInterface = { removeIndexedDBFiles: () => Promise; createOrUpdateIdentityKey: (data: IdentityKeyType) => Promise; - getIdentityKeyById: (id: string) => Promise; + getIdentityKeyById: ( + id: IdentityKeyIdType + ) => Promise; bulkAddIdentityKeys: (array: Array) => Promise; - removeIdentityKeyById: (id: string) => Promise; + removeIdentityKeyById: (id: IdentityKeyIdType) => Promise; removeAllIdentityKeys: () => Promise; getAllIdentityKeys: () => Promise>; createOrUpdatePreKey: (data: PreKeyType) => Promise; - getPreKeyById: (id: number) => Promise; + getPreKeyById: (id: PreKeyIdType) => Promise; bulkAddPreKeys: (array: Array) => Promise; - removePreKeyById: (id: number) => Promise; + removePreKeyById: (id: PreKeyIdType) => Promise; removeAllPreKeys: () => Promise; getAllPreKeys: () => Promise>; createOrUpdateSignedPreKey: (data: SignedPreKeyType) => Promise; - getSignedPreKeyById: (id: number) => Promise; + getSignedPreKeyById: ( + id: SignedPreKeyIdType + ) => Promise; bulkAddSignedPreKeys: (array: Array) => Promise; - removeSignedPreKeyById: (id: number) => Promise; + removeSignedPreKeyById: (id: SignedPreKeyIdType) => Promise; removeAllSignedPreKeys: () => Promise; getAllSignedPreKeys: () => Promise>; @@ -254,10 +273,10 @@ export type DataInterface = { getAllItems: () => Promise; createOrUpdateSenderKey: (key: SenderKeyType) => Promise; - getSenderKeyById: (id: string) => Promise; + getSenderKeyById: (id: SenderKeyIdType) => Promise; removeAllSenderKeys: () => Promise; getAllSenderKeys: () => Promise>; - removeSenderKeyById: (id: string) => Promise; + removeSenderKeyById: (id: SenderKeyIdType) => Promise; insertSentProto: ( proto: SentProtoType, @@ -296,7 +315,7 @@ export type DataInterface = { unprocessed: Array; }): Promise; bulkAddSessions: (array: Array) => Promise; - removeSessionById: (id: string) => Promise; + removeSessionById: (id: SessionIdType) => Promise; removeSessionsByConversation: (conversationId: string) => Promise; removeAllSessions: () => Promise; getAllSessions: () => Promise>; diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 6e03ba0110..4e43f97f7e 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -30,6 +30,7 @@ import { } from 'lodash'; import { ReadStatus } from '../messages/MessageReadStatus'; +import Helpers from '../textsecure/Helpers'; import { GroupV2MemberType } from '../model-types.d'; import { ReactionType } from '../types/Reactions'; import { STORAGE_UI_KEYS } from '../types/StorageUIKeys'; @@ -40,6 +41,7 @@ import { dropNull } from '../util/dropNull'; import { isNormalNumber } from '../util/isNormalNumber'; import { isNotNil } from '../util/isNotNil'; import { missingCaseError } from '../util/missingCaseError'; +import { isValidGuid } from '../util/isValidGuid'; import { parseIntOrThrow } from '../util/parseIntOrThrow'; import * as durations from '../util/durations'; import { formatCountForLogging } from '../logging/formatCountForLogging'; @@ -55,6 +57,7 @@ import { DeleteSentProtoRecipientOptionsType, EmojiType, IdentityKeyType, + IdentityKeyIdType, ItemKeyType, ItemType, LastConversationMessagesServerType, @@ -62,8 +65,10 @@ import { MessageType, MessageTypeUnhydrated, PreKeyType, + PreKeyIdType, SearchResultMessageType, SenderKeyType, + SenderKeyIdType, SentMessageDBType, SentMessagesType, SentProtoType, @@ -72,7 +77,9 @@ import { SentRecipientsType, ServerInterface, SessionType, + SessionIdType, SignedPreKeyType, + SignedPreKeyIdType, StickerPackStatusType, StickerPackType, StickerType, @@ -2101,6 +2108,336 @@ function updateToSchemaVersion40(currentVersion: number, db: Database) { console.log('updateToSchemaVersion40: success!'); } +function updateToSchemaVersion41(currentVersion: number, db: Database) { + if (currentVersion >= 41) { + return; + } + + const getConversationUuid = db.prepare( + ` + SELECT uuid + FROM + conversations + WHERE + id = $conversationId + ` + ); + + const clearSessionsAndKeys = () => { + // ts/background.ts will ask user to relink so all that matters here is + // to maintain an invariant: + // + // After this migration all sessions and keys are prefixed by + // "uuid:". + db.exec( + ` + DELETE FROM senderKeys; + DELETE FROM sessions; + DELETE FROM signedPreKeys; + DELETE FROM preKeys; + ` + ); + + assertSync(removeById('items', 'identityKey', db)); + assertSync(removeById('items', 'registrationId', db)); + }; + + const moveIdentityKeyToMap = (ourUuid: string) => { + type IdentityKeyType = { + privKey: string; + publicKey: string; + }; + + const identityKey = assertSync( + getById('items', 'identityKey', db) + ); + + type RegistrationId = number; + + const registrationId = assertSync( + getById('items', 'registrationId', db) + ); + + if (identityKey) { + assertSync( + createOrUpdateSync( + 'items', + { + id: 'identityKeyMap', + value: { + [ourUuid]: identityKey.value, + }, + }, + db + ) + ); + } + + if (registrationId) { + assertSync( + createOrUpdateSync( + 'items', + { + id: 'registrationIdMap', + value: { + [ourUuid]: registrationId.value, + }, + }, + db + ) + ); + } + + assertSync(removeById('items', 'identityKey', db)); + assertSync(removeById('items', 'registrationId', db)); + }; + + const prefixKeys = (ourUuid: string) => { + for (const table of ['signedPreKeys', 'preKeys']) { + // Add numeric `keyId` field to keys + db.prepare( + ` + UPDATE ${table} + SET + json = json_insert( + json, + '$.keyId', + json_extract(json, '$.id') + ) + ` + ).run(); + + // Update id to include suffix and add `ourUuid` field + db.prepare( + ` + UPDATE ${table} + SET + id = $ourUuid || ':' || id, + json = json_set( + json, + '$.id', + $ourUuid || ':' || json_extract(json, '$.id'), + '$.ourUuid', + $ourUuid + ) + ` + ).run({ ourUuid }); + } + + const senderKeys: ReadonlyArray<{ + id: string; + senderId: string; + }> = db.prepare('SELECT id, senderId FROM senderKeys').all(); + + console.log(`Updating ${senderKeys.length} sender keys`); + + const updateSenderKey = db.prepare( + ` + UPDATE senderKeys + SET + id = $newId, + senderId = $newSenderId + WHERE + id = $id + ` + ); + + const deleteSenderKey = db.prepare( + 'DELETE FROM senderKeys WHERE id = $id' + ); + + let updated = 0; + let deleted = 0; + for (const { id, senderId } of senderKeys) { + const [conversationId] = Helpers.unencodeNumber(senderId); + const { uuid } = getConversationUuid.get({ conversationId }); + + if (!uuid) { + deleted += 1; + deleteSenderKey.run({ id }); + continue; + } + + updated += 1; + updateSenderKey.run({ + id, + newId: `${ourUuid}:${id.replace(conversationId, uuid)}`, + newSenderId: `${senderId.replace(conversationId, uuid)}`, + }); + } + + console.log( + `Updated ${senderKeys.length} sender keys: ` + + `updated: ${updated}, deleted: ${deleted}` + ); + }; + + const updateSessions = (ourUuid: string) => { + // Use uuid instead of conversation id in existing sesions and prefix id + // with ourUuid. + // + // Set ourUuid column and field in json + const allSessions = db + .prepare('SELECT id, conversationId FROM SESSIONS') + .all(); + + console.log(`Updating ${allSessions.length} sessions`); + + const updateSession = db.prepare( + ` + UPDATE sessions + SET + id = $newId, + ourUuid = $ourUuid, + uuid = $uuid, + json = json_set( + sessions.json, + '$.id', + $newId, + '$.uuid', + $uuid, + '$.ourUuid', + $ourUuid + ) + WHERE + id = $id + ` + ); + + const deleteSession = db.prepare( + 'DELETE FROM sessions WHERE id = $id' + ); + + let updated = 0; + let deleted = 0; + for (const { id, conversationId } of allSessions) { + const { uuid } = getConversationUuid.get({ conversationId }); + if (!uuid) { + deleted += 1; + deleteSession.run({ id }); + continue; + } + + const newId = `${ourUuid}:${id.replace(conversationId, uuid)}`; + + updated += 1; + updateSession.run({ + id, + newId, + uuid, + ourUuid, + }); + } + + console.log( + `Updated ${allSessions.length} sessions: ` + + `updated: ${updated}, deleted: ${deleted}` + ); + }; + + const updateIdentityKeys = () => { + const identityKeys: ReadonlyArray<{ + id: string; + }> = db.prepare('SELECT id FROM identityKeys').all(); + + console.log(`Updating ${identityKeys.length} identity keys`); + + const updateIdentityKey = db.prepare( + ` + UPDATE identityKeys + SET + id = $newId, + json = json_set( + identityKeys.json, + '$.id', + $newId + ) + WHERE + id = $id + ` + ); + + let migrated = 0; + for (const { id } of identityKeys) { + const { uuid } = getConversationUuid.get({ conversationId: id }); + + let newId: string; + if (uuid) { + migrated += 1; + newId = uuid; + } else { + newId = `conversation:${id}`; + } + + updateIdentityKey.run({ id, newId }); + } + + console.log(`Migrated ${migrated} identity keys`); + }; + + db.transaction(() => { + db.exec( + ` + -- Change type of 'id' column from INTEGER to STRING + + ALTER TABLE preKeys + RENAME TO old_preKeys; + + ALTER TABLE signedPreKeys + RENAME TO old_signedPreKeys; + + CREATE TABLE preKeys( + id STRING PRIMARY KEY ASC, + json TEXT + ); + CREATE TABLE signedPreKeys( + id STRING PRIMARY KEY ASC, + json TEXT + ); + + -- sqlite handles the type conversion + INSERT INTO preKeys SELECT * FROM old_preKeys; + INSERT INTO signedPreKeys SELECT * FROM old_signedPreKeys; + + DROP TABLE old_preKeys; + DROP TABLE old_signedPreKeys; + + -- Alter sessions + + ALTER TABLE sessions + ADD COLUMN ourUuid STRING; + + ALTER TABLE sessions + ADD COLUMN uuid STRING; + ` + ); + + const ourUuid = getOurUuid(db); + + if (!isValidGuid(ourUuid)) { + console.error( + 'updateToSchemaVersion41: no uuid is available clearing sessions' + ); + + clearSessionsAndKeys(); + + db.pragma('user_version = 41'); + return; + } + + prefixKeys(ourUuid); + + updateSessions(ourUuid); + + moveIdentityKeyToMap(ourUuid); + + updateIdentityKeys(); + + db.pragma('user_version = 41'); + })(); + console.log('updateToSchemaVersion41: success!'); +} + const SCHEMA_VERSIONS = [ updateToSchemaVersion1, updateToSchemaVersion2, @@ -2142,6 +2479,7 @@ const SCHEMA_VERSIONS = [ updateToSchemaVersion38, updateToSchemaVersion39, updateToSchemaVersion40, + updateToSchemaVersion41, ]; function updateSchema(db: Database): void { @@ -2173,6 +2511,23 @@ function updateSchema(db: Database): void { } } +function getOurUuid(db: Database): string | undefined { + const UUID_ID: ItemKeyType = 'uuid_id'; + + const row: { json: string } | undefined = db + .prepare('SELECT json FROM items WHERE id = $id;') + .get({ id: UUID_ID }); + + if (!row) { + return undefined; + } + + const { value } = JSON.parse(row.json); + + const [ourUuid] = Helpers.unencodeNumber(String(value).toLowerCase()); + return ourUuid; +} + let globalInstance: Database | undefined; let globalInstanceRenderer: Database | undefined; let databaseFilePath: string | undefined; @@ -2370,13 +2725,15 @@ const IDENTITY_KEYS_TABLE = 'identityKeys'; function createOrUpdateIdentityKey(data: IdentityKeyType): Promise { return createOrUpdate(IDENTITY_KEYS_TABLE, data); } -function getIdentityKeyById(id: string): Promise { +async function getIdentityKeyById( + id: IdentityKeyIdType +): Promise { return getById(IDENTITY_KEYS_TABLE, id); } function bulkAddIdentityKeys(array: Array): Promise { return bulkAdd(IDENTITY_KEYS_TABLE, array); } -function removeIdentityKeyById(id: string): Promise { +async function removeIdentityKeyById(id: IdentityKeyIdType): Promise { return removeById(IDENTITY_KEYS_TABLE, id); } function removeAllIdentityKeys(): Promise { @@ -2390,13 +2747,15 @@ const PRE_KEYS_TABLE = 'preKeys'; function createOrUpdatePreKey(data: PreKeyType): Promise { return createOrUpdate(PRE_KEYS_TABLE, data); } -function getPreKeyById(id: number): Promise { +async function getPreKeyById( + id: PreKeyIdType +): Promise { return getById(PRE_KEYS_TABLE, id); } function bulkAddPreKeys(array: Array): Promise { return bulkAdd(PRE_KEYS_TABLE, array); } -function removePreKeyById(id: number): Promise { +async function removePreKeyById(id: PreKeyIdType): Promise { return removeById(PRE_KEYS_TABLE, id); } function removeAllPreKeys(): Promise { @@ -2410,15 +2769,15 @@ const SIGNED_PRE_KEYS_TABLE = 'signedPreKeys'; function createOrUpdateSignedPreKey(data: SignedPreKeyType): Promise { return createOrUpdate(SIGNED_PRE_KEYS_TABLE, data); } -function getSignedPreKeyById( - id: number +async function getSignedPreKeyById( + id: SignedPreKeyIdType ): Promise { return getById(SIGNED_PRE_KEYS_TABLE, id); } function bulkAddSignedPreKeys(array: Array): Promise { return bulkAdd(SIGNED_PRE_KEYS_TABLE, array); } -function removeSignedPreKeyById(id: number): Promise { +async function removeSignedPreKeyById(id: SignedPreKeyIdType): Promise { return removeById(SIGNED_PRE_KEYS_TABLE, id); } function removeAllSignedPreKeys(): Promise { @@ -2445,7 +2804,7 @@ function createOrUpdateItem( ): Promise { return createOrUpdate(ITEMS_TABLE, data); } -function getItemById( +async function getItemById( id: K ): Promise | undefined> { return getById(ITEMS_TABLE, id); @@ -2467,7 +2826,7 @@ async function getAllItems(): Promise { return result; } -function removeItemById(id: ItemKeyType): Promise { +async function removeItemById(id: ItemKeyType): Promise { return removeById(ITEMS_TABLE, id); } function removeAllItems(): Promise { @@ -2497,7 +2856,7 @@ async function createOrUpdateSenderKey(key: SenderKeyType): Promise { ).run(key); } async function getSenderKeyById( - id: string + id: SenderKeyIdType ): Promise { const db = getInstance(); const row = prepare(db, 'SELECT * FROM senderKeys WHERE id = $id').get({ @@ -2516,7 +2875,7 @@ async function getAllSenderKeys(): Promise> { return rows; } -async function removeSenderKeyById(id: string): Promise { +async function removeSenderKeyById(id: SenderKeyIdType): Promise { const db = getInstance(); prepare(db, 'DELETE FROM senderKeys WHERE id = $id').run({ id }); } @@ -2840,7 +3199,7 @@ async function _getAllSentProtoMessageIds(): Promise> { const SESSIONS_TABLE = 'sessions'; function createOrUpdateSessionSync(data: SessionType): void { const db = getInstance(); - const { id, conversationId } = data; + const { id, conversationId, ourUuid, uuid } = data; if (!id) { throw new Error( 'createOrUpdateSession: Provided data did not have a truthy id' @@ -2858,16 +3217,22 @@ function createOrUpdateSessionSync(data: SessionType): void { INSERT OR REPLACE INTO sessions ( id, conversationId, + ourUuid, + uuid, json ) values ( $id, $conversationId, + $ourUuid, + $uuid, $json ) ` ).run({ id, conversationId, + ourUuid, + uuid, json: objectToJSON(data), }); } @@ -2910,7 +3275,7 @@ async function commitSessionsAndUnprocessed({ function bulkAddSessions(array: Array): Promise { return bulkAdd(SESSIONS_TABLE, array); } -function removeSessionById(id: string): Promise { +async function removeSessionById(id: SessionIdType): Promise { return removeById(SESSIONS_TABLE, id); } async function removeSessionsByConversation( @@ -2933,11 +3298,11 @@ function getAllSessions(): Promise> { return getAllFromTable(SESSIONS_TABLE); } -function createOrUpdateSync( +function createOrUpdateSync( table: string, - data: Record & { id: string | number } + data: Record & { id: Key }, + db = getInstance() ): void { - const db = getInstance(); const { id } = data; if (!id) { throw new Error('createOrUpdate: Provided data did not have a truthy id'); @@ -2979,11 +3344,11 @@ async function bulkAdd( })(); } -async function getById( +function getById( table: string, - id: string | number -): Promise { - const db = getInstance(); + id: Key, + db = getInstance() +): Result | undefined { const row = db .prepare( ` @@ -3003,12 +3368,11 @@ async function getById( return jsonToObject(row.json); } -async function removeById( +function removeById( table: string, - id: string | number | Array -): Promise { - const db = getInstance(); - + id: Key | Array, + db = getInstance() +): void { if (!Array.isArray(id)) { db.prepare( ` @@ -4922,7 +5286,7 @@ async function resetAttachmentDownloadPending(): Promise { ` ).run(); } -function removeAttachmentDownloadJob(id: string): Promise { +async function removeAttachmentDownloadJob(id: string): Promise { return removeById(ATTACHMENT_DOWNLOADS_TABLE, id); } function removeAllAttachmentDownloadJobs(): Promise { diff --git a/ts/test-electron/MessageReceiver_test.ts b/ts/test-electron/MessageReceiver_test.ts index 8b1d745b83..ee1b9d2887 100644 --- a/ts/test-electron/MessageReceiver_test.ts +++ b/ts/test-electron/MessageReceiver_test.ts @@ -6,6 +6,7 @@ */ import { assert } from 'chai'; +import { v4 as getGuid } from 'uuid'; import MessageReceiver from '../textsecure/MessageReceiver'; import { IncomingWebSocketRequest } from '../textsecure/WebsocketResources'; @@ -22,11 +23,20 @@ describe('MessageReceiver', () => { const uuid = 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee'; const deviceId = 1; + let oldUuid: string | undefined; + let oldDeviceId: number | undefined; + beforeEach(async () => { + oldUuid = window.storage.user.getUuid()?.toString(); + oldDeviceId = window.storage.user.getDeviceId(); + await window.storage.user.setUuidAndDeviceId(getGuid(), 2); await window.storage.protocol.hydrateCaches(); }); afterEach(async () => { + if (oldUuid !== undefined && oldDeviceId !== undefined) { + await window.storage.user.setUuidAndDeviceId(oldUuid, oldDeviceId); + } await window.storage.protocol.removeAllUnprocessed(); }); diff --git a/ts/test-electron/SignalProtocolStore_test.ts b/ts/test-electron/SignalProtocolStore_test.ts index a842c6f742..83defbf6bf 100644 --- a/ts/test-electron/SignalProtocolStore_test.ts +++ b/ts/test-electron/SignalProtocolStore_test.ts @@ -10,6 +10,7 @@ import { SenderKeyRecord, SessionRecord, } from '@signalapp/signal-client'; +import { v4 as getGuid } from 'uuid'; import { signal } from '../protobuf/compiled'; import { sessionStructureToArrayBuffer } from '../util/sessionTranslation'; @@ -19,9 +20,13 @@ import { getRandomBytes, constantTimeEqual, typedArrayToArrayBuffer as toArrayBuffer, + arrayBufferToBase64 as toBase64, } from '../Crypto'; import { clampPrivateKey, setPublicKeyTypeByte } from '../Curve'; -import { SignalProtocolStore, GLOBAL_ZONE } from '../SignalProtocolStore'; +import { GLOBAL_ZONE, SignalProtocolStore } from '../SignalProtocolStore'; +import { Address } from '../types/Address'; +import { QualifiedAddress } from '../types/QualifiedAddress'; +import { UUID } from '../types/UUID'; import { IdentityKeyType, KeyPairType } from '../textsecure/Types.d'; chai.use(chaiAsPromised); @@ -34,7 +39,8 @@ const { } = signal.proto.storage; describe('SignalProtocolStore', () => { - const number = '+5558675309'; + const ourUuid = new UUID(getGuid()); + const theirUuid = new UUID(getGuid()); let store: SignalProtocolStore; let identityKey: KeyPairType; let testKey: KeyPairType; @@ -132,26 +138,34 @@ describe('SignalProtocolStore', () => { clampPrivateKey(identityKey.privKey); clampPrivateKey(testKey.privKey); - window.storage.put('registrationId', 1337); - window.storage.put('identityKey', identityKey); + window.storage.put('registrationIdMap', { [ourUuid.toString()]: 1337 }); + window.storage.put('identityKeyMap', { + [ourUuid.toString()]: { + privKey: toBase64(identityKey.privKey), + pubKey: toBase64(identityKey.pubKey), + }, + }); await window.storage.fetch(); window.ConversationController.reset(); await window.ConversationController.load(); - await window.ConversationController.getOrCreateAndWait(number, 'private'); + await window.ConversationController.getOrCreateAndWait( + theirUuid.toString(), + 'private' + ); }); describe('getLocalRegistrationId', () => { it('retrieves my registration id', async () => { await store.hydrateCaches(); - const id = await store.getLocalRegistrationId(); + const id = await store.getLocalRegistrationId(ourUuid); assert.strictEqual(id, 1337); }); }); describe('getIdentityKeyPair', () => { it('retrieves my identity key', async () => { await store.hydrateCaches(); - const key = await store.getIdentityKeyPair(); + const key = await store.getIdentityKeyPair(ourUuid); if (!key) { throw new Error('Missing key!'); } @@ -167,11 +181,14 @@ describe('SignalProtocolStore', () => { const expected = getSenderKeyRecord(); const deviceId = 1; - const encodedAddress = `${number}.${deviceId}`; + const qualifiedAddress = new QualifiedAddress( + ourUuid, + new Address(theirUuid, deviceId) + ); - await store.saveSenderKey(encodedAddress, distributionId, expected); + await store.saveSenderKey(qualifiedAddress, distributionId, expected); - const actual = await store.getSenderKey(encodedAddress, distributionId); + const actual = await store.getSenderKey(qualifiedAddress, distributionId); if (!actual) { throw new Error('getSenderKey returned nothing!'); } @@ -183,10 +200,10 @@ describe('SignalProtocolStore', () => { ) ); - await store.removeSenderKey(encodedAddress, distributionId); + await store.removeSenderKey(qualifiedAddress, distributionId); const postDeleteGet = await store.getSenderKey( - encodedAddress, + qualifiedAddress, distributionId ); assert.isUndefined(postDeleteGet); @@ -197,14 +214,17 @@ describe('SignalProtocolStore', () => { const expected = getSenderKeyRecord(); const deviceId = 1; - const encodedAddress = `${number}.${deviceId}`; + const qualifiedAddress = new QualifiedAddress( + ourUuid, + new Address(theirUuid, deviceId) + ); - await store.saveSenderKey(encodedAddress, distributionId, expected); + await store.saveSenderKey(qualifiedAddress, distributionId, expected); // Re-fetch from the database to ensure we get the latest database value await store.hydrateCaches(); - const actual = await store.getSenderKey(encodedAddress, distributionId); + const actual = await store.getSenderKey(qualifiedAddress, distributionId); if (!actual) { throw new Error('getSenderKey returned nothing!'); } @@ -216,13 +236,13 @@ describe('SignalProtocolStore', () => { ) ); - await store.removeSenderKey(encodedAddress, distributionId); + await store.removeSenderKey(qualifiedAddress, distributionId); // Re-fetch from the database to ensure we get the latest database value await store.hydrateCaches(); const postDeleteGet = await store.getSenderKey( - encodedAddress, + qualifiedAddress, distributionId ); assert.isUndefined(postDeleteGet); @@ -230,11 +250,11 @@ describe('SignalProtocolStore', () => { }); describe('saveIdentity', () => { - const identifier = `${number}.1`; + const identifier = new Address(theirUuid, 1); it('stores identity keys', async () => { await store.saveIdentity(identifier, testKey.pubKey); - const key = await store.loadIdentityKey(number); + const key = await store.loadIdentityKey(theirUuid); if (!key) { throw new Error('Missing key!'); } @@ -249,25 +269,31 @@ describe('SignalProtocolStore', () => { describe('When there is no existing key (first use)', () => { before(async () => { - await store.removeIdentityKey(number); + await store.removeIdentityKey(theirUuid); await store.saveIdentity(identifier, testKey.pubKey); }); it('marks the key firstUse', async () => { - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } assert(identity.firstUse); }); it('sets the timestamp', async () => { - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); 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); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -280,7 +306,7 @@ describe('SignalProtocolStore', () => { before(async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: identifier, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: oldTimestamp, @@ -292,14 +318,18 @@ describe('SignalProtocolStore', () => { await store.saveIdentity(identifier, newIdentity); }); it('marks the key not firstUse', async () => { - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } assert(!identity.firstUse); }); it('updates the timestamp', async () => { - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -309,7 +339,7 @@ describe('SignalProtocolStore', () => { describe('The previous verified status was DEFAULT', () => { before(async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: oldTimestamp, @@ -321,7 +351,9 @@ describe('SignalProtocolStore', () => { await store.saveIdentity(identifier, newIdentity); }); it('sets the new key to default', async () => { - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -331,7 +363,7 @@ describe('SignalProtocolStore', () => { describe('The previous verified status was VERIFIED', () => { before(async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: oldTimestamp, @@ -343,7 +375,9 @@ describe('SignalProtocolStore', () => { await store.saveIdentity(identifier, newIdentity); }); it('sets the new key to unverified', async () => { - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -356,7 +390,7 @@ describe('SignalProtocolStore', () => { describe('The previous verified status was UNVERIFIED', () => { before(async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: oldTimestamp, @@ -368,7 +402,9 @@ describe('SignalProtocolStore', () => { await store.saveIdentity(identifier, newIdentity); }); it('sets the new key to unverified', async () => { - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -383,7 +419,7 @@ describe('SignalProtocolStore', () => { const oldTimestamp = Date.now(); before(async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, timestamp: oldTimestamp, nonblockingApproval: false, @@ -394,7 +430,9 @@ describe('SignalProtocolStore', () => { }); describe('If it is marked firstUse', () => { before(async () => { - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -405,7 +443,9 @@ describe('SignalProtocolStore', () => { it('nothing changes', async () => { await store.saveIdentity(identifier, testKey.pubKey, true); - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -415,7 +455,9 @@ describe('SignalProtocolStore', () => { }); describe('If it is not marked firstUse', () => { before(async () => { - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -428,7 +470,7 @@ describe('SignalProtocolStore', () => { before(async () => { now = Date.now(); const identity = await window.Signal.Data.getIdentityKeyById( - number + theirUuid.toString() ); if (!identity) { throw new Error('Missing identity!'); @@ -441,7 +483,7 @@ describe('SignalProtocolStore', () => { await store.saveIdentity(identifier, testKey.pubKey, true); const identity = await window.Signal.Data.getIdentityKeyById( - number + theirUuid.toString() ); if (!identity) { throw new Error('Missing identity!'); @@ -462,7 +504,7 @@ describe('SignalProtocolStore', () => { before(async () => { now = Date.now(); validAttributes = { - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: now, @@ -470,43 +512,53 @@ describe('SignalProtocolStore', () => { nonblockingApproval: false, }; - await store.removeIdentityKey(number); + await store.removeIdentityKey(theirUuid); }); describe('with valid attributes', () => { before(async () => { - await store.saveIdentityWithAttributes(number, validAttributes); + await store.saveIdentityWithAttributes(theirUuid, validAttributes); }); it('publicKey is saved', async () => { - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); 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); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); 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); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); 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); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); 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); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -521,7 +573,7 @@ describe('SignalProtocolStore', () => { async function testInvalidAttributes() { try { - await store.saveIdentityWithAttributes(number, attributes); + await store.saveIdentityWithAttributes(theirUuid, attributes); throw new Error('saveIdentityWithAttributes should have failed'); } catch (error) { // good. we expect to fail with invalid attributes. @@ -552,8 +604,10 @@ describe('SignalProtocolStore', () => { }); describe('setApproval', () => { it('sets nonblockingApproval', async () => { - await store.setApproval(number, true); - const identity = await window.Signal.Data.getIdentityKeyById(number); + await store.setApproval(theirUuid, true); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -564,7 +618,7 @@ describe('SignalProtocolStore', () => { describe('setVerified', () => { async function saveRecordDefault() { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), @@ -576,9 +630,11 @@ describe('SignalProtocolStore', () => { describe('with no public key argument', () => { before(saveRecordDefault); it('updates the verified status', async () => { - await store.setVerified(number, store.VerifiedStatus.VERIFIED); + await store.setVerified(theirUuid, store.VerifiedStatus.VERIFIED); - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -591,12 +647,14 @@ describe('SignalProtocolStore', () => { before(saveRecordDefault); it('updates the verified status', async () => { await store.setVerified( - number, + theirUuid, store.VerifiedStatus.VERIFIED, testKey.pubKey ); - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -610,12 +668,14 @@ describe('SignalProtocolStore', () => { before(saveRecordDefault); it('does not change the record.', async () => { await store.setVerified( - number, + theirUuid, store.VerifiedStatus.VERIFIED, newIdentity ); - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -642,18 +702,20 @@ describe('SignalProtocolStore', () => { describe('when the new verified status is DEFAULT', () => { describe('when there is no existing record', () => { before(async () => { - await window.Signal.Data.removeIdentityKeyById(number); + await window.Signal.Data.removeIdentityKeyById(theirUuid.toString()); await store.hydrateCaches(); }); it('does nothing', async () => { await store.processContactSyncVerificationState( - number, + theirUuid, store.VerifiedStatus.DEFAULT, newIdentity ); - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (identity) { // fetchRecord resolved so there is a record. @@ -670,7 +732,7 @@ describe('SignalProtocolStore', () => { describe('when the existing key is different', () => { before(async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), @@ -682,13 +744,13 @@ describe('SignalProtocolStore', () => { it('does not save the new identity (because this is a less secure state)', async () => { await store.processContactSyncVerificationState( - number, + theirUuid, store.VerifiedStatus.DEFAULT, newIdentity ); const identity = await window.Signal.Data.getIdentityKeyById( - number + theirUuid.toString() ); if (!identity) { throw new Error('Missing identity!'); @@ -707,7 +769,7 @@ describe('SignalProtocolStore', () => { describe('when the existing key is the same but VERIFIED', () => { before(async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), @@ -719,13 +781,13 @@ describe('SignalProtocolStore', () => { it('updates the verified status', async () => { await store.processContactSyncVerificationState( - number, + theirUuid, store.VerifiedStatus.DEFAULT, testKey.pubKey ); const identity = await window.Signal.Data.getIdentityKeyById( - number + theirUuid.toString() ); if (!identity) { throw new Error('Missing identity!'); @@ -741,7 +803,7 @@ describe('SignalProtocolStore', () => { describe('when the existing key is the same and already DEFAULT', () => { before(async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), @@ -753,7 +815,7 @@ describe('SignalProtocolStore', () => { it('does not hang', async () => { await store.processContactSyncVerificationState( - number, + theirUuid, store.VerifiedStatus.DEFAULT, testKey.pubKey ); @@ -766,18 +828,20 @@ describe('SignalProtocolStore', () => { describe('when the new verified status is UNVERIFIED', () => { describe('when there is no existing record', () => { before(async () => { - await window.Signal.Data.removeIdentityKeyById(number); + await window.Signal.Data.removeIdentityKeyById(theirUuid.toString()); await store.hydrateCaches(); }); it('saves the new identity and marks it verified', async () => { await store.processContactSyncVerificationState( - number, + theirUuid, store.VerifiedStatus.UNVERIFIED, newIdentity ); - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -794,7 +858,7 @@ describe('SignalProtocolStore', () => { describe('when the existing key is different', () => { before(async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), @@ -806,13 +870,13 @@ describe('SignalProtocolStore', () => { it('saves the new identity and marks it UNVERIFIED', async () => { await store.processContactSyncVerificationState( - number, + theirUuid, store.VerifiedStatus.UNVERIFIED, newIdentity ); const identity = await window.Signal.Data.getIdentityKeyById( - number + theirUuid.toString() ); if (!identity) { throw new Error('Missing identity!'); @@ -829,7 +893,7 @@ describe('SignalProtocolStore', () => { describe('when the key exists and is DEFAULT', () => { before(async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), @@ -841,12 +905,12 @@ describe('SignalProtocolStore', () => { it('updates the verified status', async () => { await store.processContactSyncVerificationState( - number, + theirUuid, store.VerifiedStatus.UNVERIFIED, testKey.pubKey ); const identity = await window.Signal.Data.getIdentityKeyById( - number + theirUuid.toString() ); if (!identity) { throw new Error('Missing identity!'); @@ -865,7 +929,7 @@ describe('SignalProtocolStore', () => { describe('when the key exists and is already UNVERIFIED', () => { before(async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), @@ -877,7 +941,7 @@ describe('SignalProtocolStore', () => { it('does not hang', async () => { await store.processContactSyncVerificationState( - number, + theirUuid, store.VerifiedStatus.UNVERIFIED, testKey.pubKey ); @@ -890,17 +954,19 @@ describe('SignalProtocolStore', () => { describe('when the new verified status is VERIFIED', () => { describe('when there is no existing record', () => { before(async () => { - await window.Signal.Data.removeIdentityKeyById(number); + await window.Signal.Data.removeIdentityKeyById(theirUuid.toString()); await store.hydrateCaches(); }); it('saves the new identity and marks it verified', async () => { await store.processContactSyncVerificationState( - number, + theirUuid, store.VerifiedStatus.VERIFIED, newIdentity ); - const identity = await window.Signal.Data.getIdentityKeyById(number); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); if (!identity) { throw new Error('Missing identity!'); } @@ -914,7 +980,7 @@ describe('SignalProtocolStore', () => { describe('when the existing key is different', () => { before(async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), @@ -926,13 +992,13 @@ describe('SignalProtocolStore', () => { it('saves the new identity and marks it VERIFIED', async () => { await store.processContactSyncVerificationState( - number, + theirUuid, store.VerifiedStatus.VERIFIED, newIdentity ); const identity = await window.Signal.Data.getIdentityKeyById( - number + theirUuid.toString() ); if (!identity) { throw new Error('Missing identity!'); @@ -949,7 +1015,7 @@ describe('SignalProtocolStore', () => { describe('when the existing key is the same but UNVERIFIED', () => { before(async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), @@ -961,12 +1027,12 @@ describe('SignalProtocolStore', () => { it('saves the identity and marks it verified', async () => { await store.processContactSyncVerificationState( - number, + theirUuid, store.VerifiedStatus.VERIFIED, testKey.pubKey ); const identity = await window.Signal.Data.getIdentityKeyById( - number + theirUuid.toString() ); if (!identity) { throw new Error('Missing identity!'); @@ -985,7 +1051,7 @@ describe('SignalProtocolStore', () => { describe('when the existing key is the same and already VERIFIED', () => { before(async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, firstUse: true, timestamp: Date.now(), @@ -997,7 +1063,7 @@ describe('SignalProtocolStore', () => { it('does not hang', async () => { await store.processContactSyncVerificationState( - number, + theirUuid, store.VerifiedStatus.VERIFIED, testKey.pubKey ); @@ -1012,7 +1078,7 @@ describe('SignalProtocolStore', () => { describe('isUntrusted', () => { it('returns false if identity key old enough', async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, timestamp: Date.now() - 10 * 1000 * 60, verified: store.VerifiedStatus.DEFAULT, @@ -1021,13 +1087,13 @@ describe('SignalProtocolStore', () => { }); await store.hydrateCaches(); - const untrusted = await store.isUntrusted(number); + const untrusted = await store.isUntrusted(theirUuid); assert.strictEqual(untrusted, false); }); it('returns false if new but nonblockingApproval is true', async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, timestamp: Date.now(), verified: store.VerifiedStatus.DEFAULT, @@ -1036,13 +1102,13 @@ describe('SignalProtocolStore', () => { }); await store.hydrateCaches(); - const untrusted = await store.isUntrusted(number); + const untrusted = await store.isUntrusted(theirUuid); assert.strictEqual(untrusted, false); }); it('returns false if new but firstUse is true', async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, timestamp: Date.now(), verified: store.VerifiedStatus.DEFAULT, @@ -1051,13 +1117,13 @@ describe('SignalProtocolStore', () => { }); await store.hydrateCaches(); - const untrusted = await store.isUntrusted(number); + const untrusted = await store.isUntrusted(theirUuid); assert.strictEqual(untrusted, false); }); it('returns true if new, and no flags are set', async () => { await window.Signal.Data.createOrUpdateIdentityKey({ - id: number, + id: theirUuid.toString(), publicKey: testKey.pubKey, timestamp: Date.now(), verified: store.VerifiedStatus.DEFAULT, @@ -1066,27 +1132,31 @@ describe('SignalProtocolStore', () => { }); await store.hydrateCaches(); - const untrusted = await store.isUntrusted(number); + const untrusted = await store.isUntrusted(theirUuid); assert.strictEqual(untrusted, true); }); }); describe('getVerified', () => { before(async () => { - await store.setVerified(number, store.VerifiedStatus.VERIFIED); + await store.setVerified(theirUuid, store.VerifiedStatus.VERIFIED); }); it('resolves to the verified status', async () => { - const result = await store.getVerified(number); + const result = await store.getVerified(theirUuid); assert.strictEqual(result, store.VerifiedStatus.VERIFIED); }); }); describe('isTrustedIdentity', () => { - const identifier = `${number}.1`; + const identifier = new Address(theirUuid, 1); describe('When invalid direction is given', () => { it('should fail', async () => { try { - await store.isTrustedIdentity(number, testKey.pubKey, 'dir' as any); + await store.isTrustedIdentity( + identifier, + testKey.pubKey, + 'dir' as any + ); throw new Error('isTrustedIdentity should have failed'); } catch (error) { // good @@ -1112,7 +1182,7 @@ describe('SignalProtocolStore', () => { describe('When direction is SENDING', () => { describe('When there is no existing key (first use)', () => { before(async () => { - await store.removeIdentityKey(number); + await store.removeIdentityKey(theirUuid); }); it('returns true', async () => { const newIdentity = getPublicKey(); @@ -1177,8 +1247,8 @@ describe('SignalProtocolStore', () => { }); describe('storePreKey', () => { it('stores prekeys', async () => { - await store.storePreKey(1, testKey); - const key = await store.loadPreKey(1); + await store.storePreKey(ourUuid, 1, testKey); + const key = await store.loadPreKey(ourUuid, 1); if (!key) { throw new Error('Missing key!'); } @@ -1198,19 +1268,19 @@ describe('SignalProtocolStore', () => { }); describe('removePreKey', () => { before(async () => { - await store.storePreKey(2, testKey); + await store.storePreKey(ourUuid, 2, testKey); }); it('deletes prekeys', async () => { - await store.removePreKey(2); + await store.removePreKey(ourUuid, 2); - const key = await store.loadPreKey(2); + const key = await store.loadPreKey(ourUuid, 2); assert.isUndefined(key); }); }); describe('storeSignedPreKey', () => { it('stores signed prekeys', async () => { - await store.storeSignedPreKey(3, testKey); - const key = await store.loadSignedPreKey(3); + await store.storeSignedPreKey(ourUuid, 3, testKey); + const key = await store.loadSignedPreKey(ourUuid, 3); if (!key) { throw new Error('Missing key!'); } @@ -1230,20 +1300,21 @@ describe('SignalProtocolStore', () => { }); describe('removeSignedPreKey', () => { before(async () => { - await store.storeSignedPreKey(4, testKey); + await store.storeSignedPreKey(ourUuid, 4, testKey); }); it('deletes signed prekeys', async () => { - await store.removeSignedPreKey(4); + await store.removeSignedPreKey(ourUuid, 4); - const key = await store.loadSignedPreKey(4); + const key = await store.loadSignedPreKey(ourUuid, 4); assert.isUndefined(key); }); }); describe('storeSession', () => { it('stores sessions', async () => { const testRecord = getSessionRecord(); - await store.storeSession(`${number}.1`, testRecord); - const record = await store.loadSession(`${number}.1`); + const id = new QualifiedAddress(ourUuid, new Address(theirUuid, 1)); + await store.storeSession(id, testRecord); + const record = await store.loadSession(id); if (!record) { throw new Error('Missing record!'); } @@ -1252,18 +1323,19 @@ describe('SignalProtocolStore', () => { }); }); describe('removeAllSessions', () => { - it('removes all sessions for a number', async () => { - const devices = [1, 2, 3].map(deviceId => { - return [number, deviceId].join('.'); - }); + it('removes all sessions for a uuid', async () => { + const devices = [1, 2, 3].map( + deviceId => + new QualifiedAddress(ourUuid, new Address(theirUuid, deviceId)) + ); await Promise.all( - devices.map(async encodedNumber => { - await store.storeSession(encodedNumber, getSessionRecord()); + devices.map(async encodedAddress => { + await store.storeSession(encodedAddress, getSessionRecord()); }) ); - await store.removeAllSessions(number); + await store.removeAllSessions(theirUuid.toString()); const records = await Promise.all( devices.map(device => store.loadSession(device)) @@ -1277,83 +1349,112 @@ describe('SignalProtocolStore', () => { describe('clearSessionStore', () => { it('clears the session store', async () => { const testRecord = getSessionRecord(); - await store.storeSession(`${number}.1`, testRecord); + const id = new QualifiedAddress(ourUuid, new Address(theirUuid, 1)); + await store.storeSession(id, testRecord); await store.clearSessionStore(); - const record = await store.loadSession(`${number}.1`); + const record = await store.loadSession(id); assert.isUndefined(record); }); }); describe('getDeviceIds', () => { - it('returns deviceIds for a number', async () => { + it('returns deviceIds for a uuid', async () => { const openRecord = getSessionRecord(true); - const openDevices = [1, 2, 3, 10].map(deviceId => { - return [number, deviceId].join('.'); - }); + const openDevices = [1, 2, 3, 10].map( + deviceId => + new QualifiedAddress(ourUuid, new Address(theirUuid, deviceId)) + ); await Promise.all( - openDevices.map(async encodedNumber => { - await store.storeSession(encodedNumber, openRecord); + openDevices.map(async address => { + await store.storeSession(address, openRecord); }) ); const closedRecord = getSessionRecord(false); - await store.storeSession([number, 11].join('.'), closedRecord); + await store.storeSession( + new QualifiedAddress(ourUuid, new Address(theirUuid, 11)), + closedRecord + ); - const deviceIds = await store.getDeviceIds(number); + const deviceIds = await store.getDeviceIds({ + ourUuid, + identifier: theirUuid.toString(), + }); 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'); + it('returns empty array for a uuid with no device ids', async () => { + const deviceIds = await store.getDeviceIds({ + ourUuid, + identifier: 'foo', + }); assert.sameMembers(deviceIds, []); }); }); describe('getOpenDevices', () => { - it('returns all open devices for a number', async () => { + it('returns all open devices for a uuid', async () => { const openRecord = getSessionRecord(true); - const openDevices = [1, 2, 3, 10].map(deviceId => { - return [number, deviceId].join('.'); - }); + const openDevices = [1, 2, 3, 10].map( + deviceId => + new QualifiedAddress(ourUuid, new Address(theirUuid, deviceId)) + ); await Promise.all( - openDevices.map(async encodedNumber => { - await store.storeSession(encodedNumber, openRecord); + openDevices.map(async address => { + await store.storeSession(address, openRecord); }) ); const closedRecord = getSessionRecord(false); - await store.storeSession([number, 11].join('.'), closedRecord); + await store.storeSession( + new QualifiedAddress(ourUuid, new Address(theirUuid, 11)), + closedRecord + ); - const result = await store.getOpenDevices([number, 'blah', 'blah2']); - assert.deepEqual(result, { - devices: [ - { - id: 1, - identifier: number, - registrationId: 243, - }, - { - id: 2, - identifier: number, - registrationId: 243, - }, - { - id: 3, - identifier: number, - registrationId: 243, - }, - { - id: 10, - identifier: number, - registrationId: 243, - }, - ], - emptyIdentifiers: ['blah', 'blah2'], - }); + const result = await store.getOpenDevices(ourUuid, [ + theirUuid.toString(), + 'blah', + 'blah2', + ]); + assert.deepStrictEqual( + { + ...result, + devices: result.devices.map(({ id, identifier, registrationId }) => ({ + id, + identifier: identifier.toString(), + registrationId, + })), + }, + { + devices: [ + { + id: 1, + identifier: theirUuid.toString(), + registrationId: 243, + }, + { + id: 2, + identifier: theirUuid.toString(), + registrationId: 243, + }, + { + id: 3, + identifier: theirUuid.toString(), + registrationId: 243, + }, + { + id: 10, + identifier: theirUuid.toString(), + registrationId: 243, + }, + ], + emptyIdentifiers: ['blah', 'blah2'], + } + ); }); - it('returns empty array for a number with no device ids', async () => { - const result = await store.getOpenDevices(['foo']); + it('returns empty array for a uuid with no device ids', async () => { + const result = await store.getOpenDevices(ourUuid, ['foo']); assert.deepEqual(result, { devices: [], emptyIdentifiers: ['foo'], @@ -1369,11 +1470,11 @@ describe('SignalProtocolStore', () => { beforeEach(async () => { await store.removeAllUnprocessed(); - await store.removeAllSessions(number); + await store.removeAllSessions(theirUuid.toString()); }); it('should not store pending sessions in global zone', async () => { - const id = `${number}.1`; + const id = new QualifiedAddress(ourUuid, new Address(theirUuid, 1)); const testRecord = getSessionRecord(); await assert.isRejected( @@ -1388,7 +1489,7 @@ describe('SignalProtocolStore', () => { }); it('commits session stores and unprocessed on success', async () => { - const id = `${number}.1`; + const id = new QualifiedAddress(ourUuid, new Address(theirUuid, 1)); const testRecord = getSessionRecord(); await store.withZone(zone, 'test', async () => { @@ -1417,7 +1518,7 @@ describe('SignalProtocolStore', () => { }); it('reverts session stores and unprocessed on error', async () => { - const id = `${number}.1`; + const id = new QualifiedAddress(ourUuid, new Address(theirUuid, 1)); const testRecord = getSessionRecord(); const failedRecord = getSessionRecord(); @@ -1450,7 +1551,7 @@ describe('SignalProtocolStore', () => { }); it('can be re-entered', async () => { - const id = `${number}.1`; + const id = new QualifiedAddress(ourUuid, new Address(theirUuid, 1)); const testRecord = getSessionRecord(); await store.withZone(zone, 'test', async () => { @@ -1493,13 +1594,13 @@ describe('SignalProtocolStore', () => { }); it('should not deadlock in archiveSiblingSessions', async () => { - const id = `${number}.1`; - const sibling = `${number}.2`; + const id = new QualifiedAddress(ourUuid, new Address(theirUuid, 1)); + const sibling = new QualifiedAddress(ourUuid, new Address(theirUuid, 2)); await store.storeSession(id, getSessionRecord(true)); await store.storeSession(sibling, getSessionRecord(true)); - await store.archiveSiblingSessions(id, { zone }); + await store.archiveSiblingSessions(id.address, { zone }); }); it('can be concurrently re-entered after waiting', async () => { diff --git a/ts/test-electron/textsecure/AccountManager_test.ts b/ts/test-electron/textsecure/AccountManager_test.ts index 6ad97e03c1..1e15d7f942 100644 --- a/ts/test-electron/textsecure/AccountManager_test.ts +++ b/ts/test-electron/textsecure/AccountManager_test.ts @@ -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; 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}`); } diff --git a/test/keychange_listener_test.js b/ts/test-electron/textsecure/KeyChangeListener_test.ts similarity index 52% rename from test/keychange_listener_test.js rename to ts/test-electron/textsecure/KeyChangeListener_test.ts index d5d30cfc70..638e14e520 100644 --- a/test/keychange_listener_test.js +++ b/ts/test-electron/textsecure/KeyChangeListener_test.ts @@ -1,43 +1,56 @@ // Copyright 2017-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -/* global ConversationController, SignalProtocolStore, Whisper */ +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', () => { - const STORAGE_KEYS_TO_RESTORE = ['number_id', 'uuid_id']; - const oldStorageValues = new Map(); + let oldNumberId: string | undefined; + let oldUuidId: string | undefined; - const phoneNumberWithKeyChange = '+13016886524'; // nsa - const addressString = `${phoneNumberWithKeyChange}.1`; - const oldKey = window.Signal.Crypto.getRandomBytes(33); - const newKey = window.Signal.Crypto.getRandomBytes(33); - let store; + 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(); - STORAGE_KEYS_TO_RESTORE.forEach(key => { - oldStorageValues.set(key, window.textsecure.storage.get(key)); - }); - window.textsecure.storage.put('number_id', '+14155555556.2'); - window.textsecure.storage.put('uuid_id', `${window.getGuid()}.2`); + 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(); - await window.storage.fetch(); - oldStorageValues.forEach((oldValue, key) => { - if (oldValue) { - window.textsecure.storage.put(key, oldValue); - } else { - window.textsecure.storage.remove(key); - } - }); + 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; + let convo: ConversationModel; beforeEach(async () => { window.ConversationController.reset(); @@ -45,46 +58,46 @@ describe('KeyChangeListener', () => { await window.ConversationController.loadPromise(); convo = window.ConversationController.dangerouslyCreateAndAdd({ - id: phoneNumberWithKeyChange, + id: uuidWithKeyChange, type: 'private', }); await window.Signal.Data.saveConversation(convo.attributes); store = new SignalProtocolStore(); await store.hydrateCaches(); - Whisper.KeyChangeListener.init(store); - return store.saveIdentity(addressString, oldKey); + KeyChangeListener.init(store); + return store.saveIdentity(address, oldKey); }); afterEach(async () => { await window.Signal.Data.removeAllMessagesInConversation(convo.id, { - logId: phoneNumberWithKeyChange, + logId: uuidWithKeyChange, MessageCollection: Whisper.MessageCollection, }); await window.Signal.Data.removeConversation(convo.id, { Conversation: Whisper.Conversation, }); - await store.removeIdentityKey(phoneNumberWithKeyChange); + 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 = keyChangedId => { - assert.equal(phoneNumberWithKeyChange, keyChangedId); + convo.addKeyChange = async keyChangedId => { + assert.equal(uuidWithKeyChange, keyChangedId.toString()); convo.addKeyChange = original; done(); }; - store.saveIdentity(addressString, newKey); + store.saveIdentity(address, newKey); }); }); describe('When we have a group with this contact', () => { - let groupConvo; + let groupConvo: ConversationModel; beforeEach(async () => { - groupConvo = ConversationController.dangerouslyCreateAndAdd({ + groupConvo = window.ConversationController.dangerouslyCreateAndAdd({ id: 'groupId', type: 'group', members: [convo.id], @@ -94,7 +107,7 @@ describe('KeyChangeListener', () => { afterEach(async () => { await window.Signal.Data.removeAllMessagesInConversation(groupConvo.id, { - logId: phoneNumberWithKeyChange, + logId: uuidWithKeyChange, MessageCollection: Whisper.MessageCollection, }); await window.Signal.Data.removeConversation(groupConvo.id, { @@ -104,13 +117,13 @@ 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(phoneNumberWithKeyChange, keyChangedId); + groupConvo.addKeyChange = async keyChangedId => { + assert.equal(uuidWithKeyChange, keyChangedId.toString()); groupConvo.addKeyChange = original; done(); }; - store.saveIdentity(addressString, newKey); + store.saveIdentity(address, newKey); }); }); }); diff --git a/ts/test-electron/textsecure/generate_keys_test.ts b/ts/test-electron/textsecure/generate_keys_test.ts new file mode 100644 index 0000000000..5823c2a5cd --- /dev/null +++ b/ts/test-electron/textsecure/generate_keys_test.ts @@ -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 + ): Promise { + 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 + ) { + 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); + }); + }); +}); diff --git a/ts/textsecure/AccountManager.ts b/ts/textsecure/AccountManager.ts index 971282c4e3..5640fcfebe 100644 --- a/ts/textsecure/AccountManager.ts +++ b/ts/textsecure/AccountManager.ts @@ -7,6 +7,7 @@ /* eslint-disable class-methods-use-this */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import PQueue from 'p-queue'; +import { omit } from 'lodash'; import EventTarget from './EventTarget'; import { WebAPIType } from './WebAPI'; @@ -21,6 +22,7 @@ import { generateRegistrationId, getRandomBytes, typedArrayToArrayBuffer, + arrayBufferToBase64, } from '../Crypto'; import { generateKeyPair, @@ -29,7 +31,7 @@ import { } from '../Curve'; import { isMoreRecentThan, isOlderThan } from '../util/timestamp'; import { ourProfileKeyService } from '../services/ourProfileKey'; -import { assert } from '../util/assert'; +import { assert, strictAssert } from '../util/assert'; import { getProvisioningUrl } from '../util/getProvisioningUrl'; import { SignalService as Proto } from '../protobuf'; @@ -56,7 +58,7 @@ function getIdentifier(id: string | undefined) { return parts[0]; } -type GeneratedKeysType = { +export type GeneratedKeysType = { preKeys: Array<{ keyId: number; publicKey: ArrayBuffer; @@ -89,16 +91,10 @@ export default class AccountManager extends EventTarget { return this.server.requestVerificationSMS(number); } - async encryptDeviceName(name: string, providedIdentityKey?: KeyPairType) { + async encryptDeviceName(name: string, identityKey: KeyPairType) { if (!name) { return null; } - const identityKey = - providedIdentityKey || - (await window.textsecure.storage.protocol.getIdentityKeyPair()); - if (!identityKey) { - throw new Error('Identity key was not provided and is not in database!'); - } const encrypted = await window.Signal.Crypto.encryptDeviceName( name, identityKey.pubKey @@ -114,7 +110,10 @@ export default class AccountManager extends EventTarget { } async decryptDeviceName(base64: string) { - const identityKey = await window.textsecure.storage.protocol.getIdentityKeyPair(); + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + const identityKey = await window.textsecure.storage.protocol.getIdentityKeyPair( + ourUuid + ); if (!identityKey) { throw new Error('decryptDeviceName: No identity key pair!'); } @@ -144,8 +143,19 @@ export default class AccountManager extends EventTarget { if (isNameEncrypted) { return; } - const deviceName = window.textsecure.storage.user.getDeviceName(); - const base64 = await this.encryptDeviceName(deviceName || ''); + const { storage } = window.textsecure; + const deviceName = storage.user.getDeviceName(); + const identityKeyPair = await storage.protocol.getIdentityKeyPair( + storage.user.getCheckedUuid() + ); + strictAssert( + identityKeyPair !== undefined, + "Can't encrypt device name without identity key pair" + ); + const base64 = await this.encryptDeviceName( + deviceName || '', + identityKeyPair + ); if (base64) { await this.server.updateDeviceName(base64); @@ -310,6 +320,7 @@ export default class AccountManager extends EventTarget { async rotateSignedPreKey() { return this.queueTask(async () => { + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); const signedKeyId = window.textsecure.storage.get('signedKeyId', 1); if (typeof signedKeyId !== 'number') { throw new Error('Invalid signedKeyId'); @@ -318,7 +329,7 @@ export default class AccountManager extends EventTarget { const store = window.textsecure.storage.protocol; const { server, cleanSignedPreKeys } = this; - const existingKeys = await store.loadSignedPreKeys(); + const existingKeys = await store.loadSignedPreKeys(ourUuid); existingKeys.sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); const confirmedKeys = existingKeys.filter(key => key.confirmed); const mostRecent = confirmedKeys[0]; @@ -332,7 +343,7 @@ export default class AccountManager extends EventTarget { // eslint-disable-next-line consistent-return return store - .getIdentityKeyPair() + .getIdentityKeyPair(ourUuid) .then( async (identityKey: KeyPairType | undefined) => { if (!identityKey) { @@ -357,7 +368,7 @@ export default class AccountManager extends EventTarget { window.log.info('Saving new signed prekey', res.keyId); return Promise.all([ window.textsecure.storage.put('signedKeyId', signedKeyId + 1), - store.storeSignedPreKey(res.keyId, res.keyPair), + store.storeSignedPreKey(ourUuid, res.keyId, res.keyPair), server.setSignedPreKey({ keyId: res.keyId, publicKey: res.keyPair.pubKey, @@ -369,7 +380,12 @@ export default class AccountManager extends EventTarget { window.log.info('Confirming new signed prekey', res.keyId); return Promise.all([ window.textsecure.storage.remove('signedKeyRotationRejected'), - store.storeSignedPreKey(res.keyId, res.keyPair, confirmed), + store.storeSignedPreKey( + ourUuid, + res.keyId, + res.keyPair, + confirmed + ), ]); }) .then(cleanSignedPreKeys); @@ -409,9 +425,10 @@ export default class AccountManager extends EventTarget { } async cleanSignedPreKeys() { + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); const store = window.textsecure.storage.protocol; - const allKeys = await store.loadSignedPreKeys(); + const allKeys = await store.loadSignedPreKeys(ourUuid); allKeys.sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); const confirmed = allKeys.filter(key => key.confirmed); const unconfirmed = allKeys.filter(key => !key.confirmed); @@ -448,7 +465,7 @@ export default class AccountManager extends EventTarget { window.log.info( `Removing signed prekey: ${key.keyId} with timestamp ${timestamp}${confirmedText}` ); - await store.removeSignedPreKey(key.keyId); + await store.removeSignedPreKey(ourUuid, key.keyId); } }) ); @@ -464,17 +481,14 @@ export default class AccountManager extends EventTarget { readReceipts?: boolean | null, options: { accessKey?: ArrayBuffer; uuid?: string } = {} ): Promise { + const { storage } = window.textsecure; const { accessKey, uuid } = options; let password = btoa(utils.getString(getRandomBytes(16))); password = password.substring(0, password.length - 2); const registrationId = generateRegistrationId(); - const previousNumber = getIdentifier( - window.textsecure.storage.get('number_id') - ); - const previousUuid = getIdentifier( - window.textsecure.storage.get('uuid_id') - ); + const previousNumber = getIdentifier(storage.get('number_id')); + const previousUuid = getIdentifier(storage.get('uuid_id')); let encryptedDeviceName; if (deviceName) { @@ -500,7 +514,10 @@ export default class AccountManager extends EventTarget { { accessKey, uuid } ); - const uuidChanged = previousUuid && uuid && previousUuid !== uuid; + const ourUuid = uuid || response.uuid; + strictAssert(ourUuid !== undefined, 'Should have UUID after registration'); + + const uuidChanged = previousUuid && ourUuid && previousUuid !== ourUuid; // We only consider the number changed if we didn't have a UUID before const numberChanged = @@ -519,7 +536,7 @@ export default class AccountManager extends EventTarget { } try { - await window.textsecure.storage.protocol.removeAllData(); + await storage.protocol.removeAllData(); window.log.info('Successfully deleted previous data'); } catch (error) { window.log.error( @@ -530,23 +547,34 @@ export default class AccountManager extends EventTarget { } await Promise.all([ - window.textsecure.storage.remove('identityKey'), - window.textsecure.storage.user.removeCredentials(), - window.textsecure.storage.remove('registrationId'), - window.textsecure.storage.remove('regionCode'), - window.textsecure.storage.remove('userAgent'), - window.textsecure.storage.remove('profileKey'), - window.textsecure.storage.remove('read-receipt-setting'), + storage.user.removeCredentials(), + storage.remove('regionCode'), + storage.remove('userAgent'), + storage.remove('profileKey'), + storage.remove('read-receipt-setting'), ]); + if (previousUuid) { + await Promise.all([ + storage.put( + 'identityKeyMap', + omit(storage.get('identityKeyMap') || {}, previousUuid) + ), + storage.put( + 'registrationIdMap', + omit(storage.get('registrationIdMap') || {}, previousUuid) + ), + ]); + } + // `setCredentials` needs to be called // before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes` // indirectly calls `ConversationController.getConverationId()` which // initializes the conversation for the given number (our number) which // calls out to the user storage API to get the stored UUID and number // information. - await window.textsecure.storage.user.setCredentials({ - uuid, + await storage.user.setCredentials({ + uuid: ourUuid, number, deviceId: response.deviceId ?? 1, deviceName: deviceName ?? undefined, @@ -558,7 +586,7 @@ export default class AccountManager extends EventTarget { // below. const conversationId = window.ConversationController.ensureContactIds({ e164: number, - uuid, + uuid: ourUuid, highTrust: true, }); @@ -568,36 +596,42 @@ export default class AccountManager extends EventTarget { // update our own identity key, which may have changed // if we're relinking after a reinstall on the master device - await window.textsecure.storage.protocol.saveIdentityWithAttributes( - uuid || number, - { - publicKey: identityKeyPair.pubKey, - firstUse: true, - timestamp: Date.now(), - verified: window.textsecure.storage.protocol.VerifiedStatus.VERIFIED, - nonblockingApproval: true, - } - ); + await storage.protocol.saveIdentityWithAttributes(ourUuid, { + publicKey: identityKeyPair.pubKey, + firstUse: true, + timestamp: Date.now(), + verified: storage.protocol.VerifiedStatus.VERIFIED, + nonblockingApproval: true, + }); - await window.textsecure.storage.put('identityKey', identityKeyPair); - await window.textsecure.storage.put('registrationId', registrationId); + const identityKeyMap = { + ...(storage.get('identityKeyMap') || {}), + [ourUuid]: { + pubKey: arrayBufferToBase64(identityKeyPair.pubKey), + privKey: arrayBufferToBase64(identityKeyPair.privKey), + }, + }; + const registrationIdMap = { + ...(storage.get('registrationIdMap') || {}), + [ourUuid]: registrationId, + }; + + await storage.put('identityKeyMap', identityKeyMap); + await storage.put('registrationIdMap', registrationIdMap); if (profileKey) { await ourProfileKeyService.set(profileKey); } if (userAgent) { - await window.textsecure.storage.put('userAgent', userAgent); + await storage.put('userAgent', userAgent); } - await window.textsecure.storage.put( - 'read-receipt-setting', - Boolean(readReceipts) - ); + await storage.put('read-receipt-setting', Boolean(readReceipts)); const regionCode = window.libphonenumber.util.getRegionCodeForNumber( number ); - await window.textsecure.storage.put('regionCode', regionCode); - await window.textsecure.storage.protocol.hydrateCaches(); + await storage.put('regionCode', regionCode); + await storage.protocol.hydrateCaches(); } async clearSessionsAndPreKeys() { @@ -626,7 +660,8 @@ export default class AccountManager extends EventTarget { } window.log.info('confirmKeys: confirming key', key.keyId); - await store.storeSignedPreKey(key.keyId, key.keyPair, confirmed); + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + await store.storeSignedPreKey(ourUuid, key.keyId, key.keyPair, confirmed); } async generateKeys(count: number, providedProgressCallback?: Function) { @@ -636,6 +671,7 @@ export default class AccountManager extends EventTarget { : null; const startId = window.textsecure.storage.get('maxPreKeyId', 1); const signedKeyId = window.textsecure.storage.get('signedKeyId', 1); + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); if (typeof startId !== 'number') { throw new Error('Invalid maxPreKeyId'); @@ -645,7 +681,7 @@ export default class AccountManager extends EventTarget { } const store = window.textsecure.storage.protocol; - return store.getIdentityKeyPair().then(async identityKey => { + return store.getIdentityKeyPair(ourUuid).then(async identityKey => { if (!identityKey) { throw new Error('generateKeys: No identity key pair!'); } @@ -659,7 +695,7 @@ export default class AccountManager extends EventTarget { for (let keyId = startId; keyId < startId + count; keyId += 1) { promises.push( Promise.resolve(generatePreKey(keyId)).then(async res => { - await store.storePreKey(res.keyId, res.keyPair); + await store.storePreKey(ourUuid, res.keyId, res.keyPair); result.preKeys.push({ keyId: res.keyId, publicKey: res.keyPair.pubKey, @@ -674,7 +710,7 @@ export default class AccountManager extends EventTarget { promises.push( Promise.resolve(generateSignedPreKey(identityKey, signedKeyId)).then( async res => { - await store.storeSignedPreKey(res.keyId, res.keyPair); + await store.storeSignedPreKey(ourUuid, res.keyId, res.keyPair); result.signedPreKey = { keyId: res.keyId, publicKey: res.keyPair.pubKey, diff --git a/ts/textsecure/KeyChangeListener.ts b/ts/textsecure/KeyChangeListener.ts new file mode 100644 index 0000000000..9e7996b05a --- /dev/null +++ b/ts/textsecure/KeyChangeListener.ts @@ -0,0 +1,25 @@ +// Copyright 2017-2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { UUID } from '../types/UUID'; +import type { SignalProtocolStore } from '../SignalProtocolStore'; + +export function init(signalProtocolStore: SignalProtocolStore): void { + signalProtocolStore.on( + 'keychange', + async (uuid: UUID): Promise => { + const conversation = await window.ConversationController.getOrCreateAndWait( + uuid.toString(), + 'private' + ); + conversation.addKeyChange(uuid); + + const groups = await window.ConversationController.getAllGroupsInvolvingId( + conversation.id + ); + for (const group of groups) { + group.addKeyChange(uuid); + } + } + ); +} diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index c9749206ad..16845acc71 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -45,6 +45,9 @@ import { parseIntOrThrow } from '../util/parseIntOrThrow'; import { Zone } from '../util/Zone'; import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto'; import { DownloadedAttachmentType } from '../types/Attachment'; +import { Address } from '../types/Address'; +import { QualifiedAddress } from '../types/QualifiedAddress'; +import { UUID } from '../types/UUID'; import * as Errors from '../types/errors'; import { SignalService as Proto } from '../protobuf'; @@ -754,8 +757,9 @@ export default class MessageReceiver pendingSessions: true, pendingUnprocessed: true, }); - const sessionStore = new Sessions({ zone }); - const identityKeyStore = new IdentityKeys({ zone }); + const ourUuid = this.storage.user.getCheckedUuid(); + const sessionStore = new Sessions({ zone, ourUuid }); + const identityKeyStore = new IdentityKeys({ zone, ourUuid }); const failed: Array = []; // Below we: @@ -1228,18 +1232,12 @@ export default class MessageReceiver ciphertext: Uint8Array ): Promise { const localE164 = this.storage.user.getNumber(); - const localUuid = this.storage.user.getUuid(); + const ourUuid = this.storage.user.getCheckedUuid(); const localDeviceId = parseIntOrThrow( this.storage.user.getDeviceId(), 'MessageReceiver.decryptSealedSender: localDeviceId' ); - if (!localUuid) { - throw new Error( - 'MessageReceiver.decryptSealedSender: Failed to fetch local UUID' - ); - } - const logId = this.getEnvelopeId(envelope); const { unsealedContent: messageContent, certificate } = envelope; @@ -1280,9 +1278,12 @@ export default class MessageReceiver ); const sealedSenderIdentifier = certificate.senderUuid(); const sealedSenderSourceDevice = certificate.senderDeviceId(); - const senderKeyStore = new SenderKeys(); + const senderKeyStore = new SenderKeys({ ourUuid }); - const address = `${sealedSenderIdentifier}.${sealedSenderSourceDevice}`; + const address = new QualifiedAddress( + ourUuid, + Address.create(sealedSenderIdentifier, sealedSenderSourceDevice) + ); const plaintext = await this.storage.protocol.enqueueSenderKeyJob( address, @@ -1305,11 +1306,22 @@ export default class MessageReceiver 'unidentified message/passing to sealedSenderDecryptMessage' ); - const preKeyStore = new PreKeys(); - const signedPreKeyStore = new SignedPreKeys(); + const preKeyStore = new PreKeys({ ourUuid }); + const signedPreKeyStore = new SignedPreKeys({ ourUuid }); - const sealedSenderIdentifier = envelope.sourceUuid || envelope.source; - const address = `${sealedSenderIdentifier}.${envelope.sourceDevice}`; + const sealedSenderIdentifier = envelope.sourceUuid; + strictAssert( + sealedSenderIdentifier !== undefined, + 'Empty sealed sender identifier' + ); + strictAssert( + envelope.sourceDevice !== undefined, + 'Empty sealed sender device' + ); + const address = new QualifiedAddress( + ourUuid, + Address.create(sealedSenderIdentifier, envelope.sourceDevice) + ); const unsealedPlaintext = await this.storage.protocol.enqueueSessionJob( address, () => @@ -1318,7 +1330,7 @@ export default class MessageReceiver PublicKey.deserialize(Buffer.from(this.serverTrustRoot)), envelope.serverTimestamp, localE164 || null, - localUuid, + ourUuid.toString(), localDeviceId, sessionStore, identityKeyStore, @@ -1341,11 +1353,20 @@ export default class MessageReceiver const logId = this.getEnvelopeId(envelope); const envelopeTypeEnum = Proto.Envelope.Type; - const identifier = envelope.sourceUuid || envelope.source; + const identifier = envelope.sourceUuid; const { sourceDevice } = envelope; - const preKeyStore = new PreKeys(); - const signedPreKeyStore = new SignedPreKeys(); + const ourUuid = this.storage.user.getCheckedUuid(); + const preKeyStore = new PreKeys({ ourUuid }); + const signedPreKeyStore = new SignedPreKeys({ ourUuid }); + + strictAssert(identifier !== undefined, 'Empty identifier'); + strictAssert(sourceDevice !== undefined, 'Empty source device'); + + const address = new QualifiedAddress( + ourUuid, + Address.create(identifier, sourceDevice) + ); if (envelope.type === envelopeTypeEnum.PLAINTEXT_CONTENT) { window.log.info(`decrypt/${logId}: plaintext message`); @@ -1368,7 +1389,6 @@ export default class MessageReceiver } const signalMessage = SignalMessage.deserialize(Buffer.from(ciphertext)); - const address = `${identifier}.${sourceDevice}`; const plaintext = await this.storage.protocol.enqueueSessionJob( address, async () => @@ -1400,7 +1420,6 @@ export default class MessageReceiver Buffer.from(ciphertext) ); - const address = `${identifier}.${sourceDevice}`; const plaintext = await this.storage.protocol.enqueueSessionJob( address, async () => @@ -1562,7 +1581,7 @@ export default class MessageReceiver const isBlocked = groupId ? this.isGroupBlocked(groupId) : false; const { source, sourceUuid } = envelope; const ourE164 = this.storage.user.getNumber(); - const ourUuid = this.storage.user.getUuid(); + const ourUuid = this.storage.user.getCheckedUuid().toString(); const isMe = (source && ourE164 && source === ourE164) || (sourceUuid && ourUuid && sourceUuid === ourUuid); @@ -1613,7 +1632,7 @@ export default class MessageReceiver ); let p: Promise = Promise.resolve(); // eslint-disable-next-line no-bitwise - const destination = envelope.sourceUuid || envelope.source; + const destination = envelope.sourceUuid; if (!destination) { throw new Error( 'MessageReceiver.handleDataMessage: source and sourceUuid were falsey' @@ -1651,7 +1670,7 @@ export default class MessageReceiver const isBlocked = groupId ? this.isGroupBlocked(groupId) : false; const { source, sourceUuid } = envelope; const ourE164 = this.storage.user.getNumber(); - const ourUuid = this.storage.user.getUuid(); + const ourUuid = this.storage.user.getCheckedUuid().toString(); const isMe = (source && ourE164 && source === ourE164) || (sourceUuid && ourUuid && sourceUuid === ourUuid); @@ -1711,8 +1730,7 @@ export default class MessageReceiver } const { timestamp } = envelope; - const identifier = - envelope.groupId || envelope.sourceUuid || envelope.source; + const identifier = envelope.groupId || envelope.sourceUuid; const conversation = window.ConversationController.get(identifier); try { @@ -1848,7 +1866,7 @@ export default class MessageReceiver // Note: we don't call removeFromCache here because this message can be combined // with a dataMessage, for example. That processing will dictate cache removal. - const identifier = envelope.sourceUuid || envelope.source; + const identifier = envelope.sourceUuid; const { sourceDevice } = envelope; if (!identifier) { throw new Error( @@ -1865,8 +1883,12 @@ export default class MessageReceiver const senderKeyDistributionMessage = SenderKeyDistributionMessage.deserialize( Buffer.from(distributionMessage) ); - const senderKeyStore = new SenderKeys(); - const address = `${identifier}.${sourceDevice}`; + const ourUuid = this.storage.user.getCheckedUuid(); + const senderKeyStore = new SenderKeys({ ourUuid }); + const address = new QualifiedAddress( + ourUuid, + Address.create(identifier, sourceDevice) + ); await this.storage.protocol.enqueueSenderKeyJob( address, @@ -2109,11 +2131,11 @@ export default class MessageReceiver syncMessage: ProcessedSyncMessage ): Promise { const ourNumber = this.storage.user.getNumber(); - const ourUuid = this.storage.user.getUuid(); + const ourUuid = this.storage.user.getCheckedUuid(); const fromSelfSource = envelope.source && envelope.source === ourNumber; const fromSelfSourceUuid = - envelope.sourceUuid && envelope.sourceUuid === ourUuid; + envelope.sourceUuid && envelope.sourceUuid === ourUuid.toString(); if (!fromSelfSource && !fromSelfSourceUuid) { throw new Error('Received sync message from another number'); } @@ -2536,8 +2558,14 @@ export default class MessageReceiver } private async handleEndSession(identifier: string): Promise { + const theirUuid = UUID.lookup(identifier); + if (!theirUuid) { + window.log.warn(`handleEndSession: uuid not found for ${identifier}`); + return; + } + window.log.info(`handleEndSession: closing sessions for ${identifier}`); - await this.storage.protocol.archiveAllSessions(identifier); + await this.storage.protocol.archiveAllSessions(theirUuid); } private async processDecrypted( diff --git a/ts/textsecure/OutgoingMessage.ts b/ts/textsecure/OutgoingMessage.ts index 7aeb1875ef..dc6a1fe0cf 100644 --- a/ts/textsecure/OutgoingMessage.ts +++ b/ts/textsecure/OutgoingMessage.ts @@ -32,6 +32,9 @@ import { } from './Errors'; import { CallbackResultType, CustomError } from './Types.d'; import { isValidNumber } from '../types/PhoneNumber'; +import { Address } from '../types/Address'; +import { QualifiedAddress } from '../types/QualifiedAddress'; +import { UUID } from '../types/UUID'; import { Sessions, IdentityKeys } from '../LibSignalStores'; import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto'; import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup'; @@ -237,9 +240,11 @@ export default class OutgoingMessage { recurse?: boolean ): () => Promise { return async () => { - const deviceIds = await window.textsecure.storage.protocol.getDeviceIds( - identifier - ); + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({ + ourUuid, + identifier, + }); if (deviceIds.length === 0) { this.registerError( identifier, @@ -386,9 +391,12 @@ export default class OutgoingMessage { // We don't send to ourselves unless sealedSender is enabled const ourNumber = window.textsecure.storage.user.getNumber(); - const ourUuid = window.textsecure.storage.user.getUuid(); + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); const ourDeviceId = window.textsecure.storage.user.getDeviceId(); - if ((identifier === ourNumber || identifier === ourUuid) && !sealedSender) { + if ( + (identifier === ourNumber || identifier === ourUuid.toString()) && + !sealedSender + ) { deviceIds = reject( deviceIds, deviceId => @@ -399,18 +407,22 @@ export default class OutgoingMessage { ); } - const sessionStore = new Sessions(); - const identityKeyStore = new IdentityKeys(); + const sessionStore = new Sessions({ ourUuid }); + const identityKeyStore = new IdentityKeys({ ourUuid }); return Promise.all( deviceIds.map(async destinationDeviceId => { - const address = `${identifier}.${destinationDeviceId}`; + const theirUuid = UUID.checkedLookup(identifier); + const address = new QualifiedAddress( + ourUuid, + new Address(theirUuid, destinationDeviceId) + ); return window.textsecure.storage.protocol.enqueueSessionJob( address, async () => { const protocolAddress = ProtocolAddress.new( - identifier, + theirUuid.toString(), destinationDeviceId ); @@ -566,7 +578,10 @@ export default class OutgoingMessage { p = Promise.all( error.response.staleDevices.map(async (deviceId: number) => { await window.textsecure.storage.protocol.archiveSession( - `${identifier}.${deviceId}` + new QualifiedAddress( + ourUuid, + new Address(UUID.checkedLookup(identifier), deviceId) + ) ); }) ); @@ -595,7 +610,7 @@ export default class OutgoingMessage { window.log.info('closing all sessions for', identifier); window.textsecure.storage.protocol - .archiveAllSessions(identifier) + .archiveAllSessions(UUID.checkedLookup(identifier)) .then( () => { throw error; @@ -623,10 +638,13 @@ export default class OutgoingMessage { identifier: string, deviceIdsToRemove: Array ): Promise { + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + const theirUuid = UUID.checkedLookup(identifier); + await Promise.all( deviceIdsToRemove.map(async deviceId => { await window.textsecure.storage.protocol.archiveSession( - `${identifier}.${deviceId}` + new QualifiedAddress(ourUuid, new Address(theirUuid, deviceId)) ); }) ); @@ -675,9 +693,11 @@ export default class OutgoingMessage { ); } - const deviceIds = await window.textsecure.storage.protocol.getDeviceIds( - identifier - ); + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({ + ourUuid, + identifier, + }); if (deviceIds.length === 0) { await this.getKeysForIdentifier(identifier); } diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index bcaf7d506d..e12a3a571b 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -18,6 +18,9 @@ import { import { assert } from '../util/assert'; import { parseIntOrThrow } from '../util/parseIntOrThrow'; +import { Address } from '../types/Address'; +import { QualifiedAddress } from '../types/QualifiedAddress'; +import { UUID } from '../types/UUID'; import { SenderKeys } from '../LibSignalStores'; import { ChallengeType, @@ -737,14 +740,14 @@ export default class MessageSender { } const myE164 = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getUuid()?.toString(); const groupMembers = groupV2?.members || groupV1?.members || []; // We should always have a UUID but have this check just in case we don't. let isNotMe: (recipient: string) => boolean; if (myUuid) { - isNotMe = r => r !== myE164 && r !== myUuid; + isNotMe = r => r !== myE164 && r !== myUuid.toString(); } else { isNotMe = r => r !== myE164; } @@ -1030,8 +1033,7 @@ export default class MessageSender { isUpdate?: boolean; options?: SendOptionsType; }>): Promise { - const myNumber = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getCheckedUuid(); const dataMessage = Proto.DataMessage.decode( new FIXMEU8(encodedDataMessage) @@ -1086,7 +1088,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ - identifier: myUuid || myNumber, + identifier: myUuid.toString(), proto: contentMessage, timestamp, contentHint: ContentHint.RESENDABLE, @@ -1097,8 +1099,7 @@ export default class MessageSender { async sendRequestBlockSyncMessage( options?: Readonly ): Promise { - const myNumber = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getCheckedUuid(); const request = new Proto.SyncMessage.Request(); request.type = Proto.SyncMessage.Request.Type.BLOCKED; @@ -1110,7 +1111,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ - identifier: myUuid || myNumber, + identifier: myUuid.toString(), proto: contentMessage, timestamp: Date.now(), contentHint: ContentHint.IMPLICIT, @@ -1121,8 +1122,7 @@ export default class MessageSender { async sendRequestConfigurationSyncMessage( options?: Readonly ): Promise { - const myNumber = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getCheckedUuid(); const request = new Proto.SyncMessage.Request(); request.type = Proto.SyncMessage.Request.Type.CONFIGURATION; @@ -1134,7 +1134,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ - identifier: myUuid || myNumber, + identifier: myUuid.toString(), proto: contentMessage, timestamp: Date.now(), contentHint: ContentHint.IMPLICIT, @@ -1145,8 +1145,7 @@ export default class MessageSender { async sendRequestGroupSyncMessage( options?: Readonly ): Promise { - const myNumber = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getCheckedUuid(); const request = new Proto.SyncMessage.Request(); request.type = Proto.SyncMessage.Request.Type.GROUPS; @@ -1158,7 +1157,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ - identifier: myUuid || myNumber, + identifier: myUuid.toString(), proto: contentMessage, timestamp: Date.now(), contentHint: ContentHint.IMPLICIT, @@ -1169,8 +1168,7 @@ export default class MessageSender { async sendRequestContactSyncMessage( options?: Readonly ): Promise { - const myNumber = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getCheckedUuid(); const request = new Proto.SyncMessage.Request(); request.type = Proto.SyncMessage.Request.Type.CONTACTS; @@ -1182,7 +1180,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ - identifier: myUuid || myNumber, + identifier: myUuid.toString(), proto: contentMessage, timestamp: Date.now(), contentHint: ContentHint.IMPLICIT, @@ -1193,8 +1191,7 @@ export default class MessageSender { async sendFetchManifestSyncMessage( options?: Readonly ): Promise { - const myUuid = window.textsecure.storage.user.getUuid(); - const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getCheckedUuid(); const fetchLatest = new Proto.SyncMessage.FetchLatest(); fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST; @@ -1207,7 +1204,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ - identifier: myUuid || myNumber, + identifier: myUuid.toString(), proto: contentMessage, timestamp: Date.now(), contentHint: ContentHint.IMPLICIT, @@ -1218,8 +1215,7 @@ export default class MessageSender { async sendFetchLocalProfileSyncMessage( options?: Readonly ): Promise { - const myUuid = window.textsecure.storage.user.getUuid(); - const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getCheckedUuid(); const fetchLatest = new Proto.SyncMessage.FetchLatest(); fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.LOCAL_PROFILE; @@ -1232,7 +1228,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ - identifier: myUuid || myNumber, + identifier: myUuid.toString(), proto: contentMessage, timestamp: Date.now(), contentHint: ContentHint.IMPLICIT, @@ -1243,8 +1239,7 @@ export default class MessageSender { async sendRequestKeySyncMessage( options?: Readonly ): Promise { - const myUuid = window.textsecure.storage.user.getUuid(); - const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getCheckedUuid(); const request = new Proto.SyncMessage.Request(); request.type = Proto.SyncMessage.Request.Type.KEYS; @@ -1257,7 +1252,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ - identifier: myUuid || myNumber, + identifier: myUuid.toString(), proto: contentMessage, timestamp: Date.now(), contentHint: ContentHint.IMPLICIT, @@ -1273,8 +1268,7 @@ export default class MessageSender { }>, options?: Readonly ): Promise { - const myNumber = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getCheckedUuid(); const syncMessage = this.createSyncMessage(); syncMessage.read = []; @@ -1289,7 +1283,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ - identifier: myUuid || myNumber, + identifier: myUuid.toString(), proto: contentMessage, timestamp: Date.now(), contentHint: ContentHint.RESENDABLE, @@ -1305,8 +1299,7 @@ export default class MessageSender { }>, options?: SendOptionsType ): Promise { - const myNumber = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getCheckedUuid(); const syncMessage = this.createSyncMessage(); syncMessage.viewed = views.map(view => new Proto.SyncMessage.Viewed(view)); @@ -1316,7 +1309,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ - identifier: myUuid || myNumber, + identifier: myUuid.toString(), proto: contentMessage, timestamp: Date.now(), contentHint: ContentHint.RESENDABLE, @@ -1330,8 +1323,7 @@ export default class MessageSender { timestamp: number, options?: Readonly ): Promise { - const myNumber = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getCheckedUuid(); const syncMessage = this.createSyncMessage(); @@ -1349,7 +1341,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ - identifier: myUuid || myNumber, + identifier: myUuid.toString(), proto: contentMessage, timestamp: Date.now(), contentHint: ContentHint.RESENDABLE, @@ -1366,8 +1358,7 @@ export default class MessageSender { }>, options?: Readonly ): Promise { - const myNumber = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getCheckedUuid(); const syncMessage = this.createSyncMessage(); @@ -1390,7 +1381,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ - identifier: myUuid || myNumber, + identifier: myUuid.toString(), proto: contentMessage, timestamp: Date.now(), contentHint: ContentHint.RESENDABLE, @@ -1406,8 +1397,7 @@ export default class MessageSender { }>, options?: Readonly ): Promise { - const myNumber = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getCheckedUuid(); const ENUM = Proto.SyncMessage.StickerPackOperation.Type; const packOperations = operations.map(item => { @@ -1430,7 +1420,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ - identifier: myUuid || myNumber, + identifier: myUuid.toString(), proto: contentMessage, timestamp: Date.now(), contentHint: ContentHint.IMPLICIT, @@ -1445,8 +1435,7 @@ export default class MessageSender { identityKey: Readonly, options?: Readonly ): Promise { - const myNumber = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getCheckedUuid(); const now = Date.now(); if (!destinationE164 && !destinationUuid) { @@ -1488,7 +1477,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ - identifier: myUuid || myNumber, + identifier: myUuid.toString(), proto: secondMessage, timestamp: now, contentHint: ContentHint.RESENDABLE, @@ -1675,6 +1664,7 @@ export default class MessageSender { proto.timestamp = timestamp; const identifier = uuid || e164; + const theirUuid = UUID.checkedLookup(identifier); const logError = (prefix: string) => (error: Error) => { window.log.error(prefix, error && error.stack ? error.stack : error); @@ -1684,7 +1674,7 @@ export default class MessageSender { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const sendToContactPromise = window.textsecure.storage.protocol - .archiveAllSessions(identifier) + .archiveAllSessions(theirUuid) .catch(logError('resetSession/archiveAllSessions1 error:')) .then(async () => { window.log.info( @@ -1706,14 +1696,14 @@ export default class MessageSender { }) .then(async result => { await window.textsecure.storage.protocol - .archiveAllSessions(identifier) + .archiveAllSessions(theirUuid) .catch(logError('resetSession/archiveAllSessions2 error:')); return result; }); const myNumber = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getUuid()?.toString(); // We already sent the reset session to our other devices in the code above! if ((e164 && e164 === myNumber) || (uuid && uuid === myUuid)) { return sendToContactPromise; @@ -1882,7 +1872,7 @@ export default class MessageSender { : undefined; const myE164 = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getUuid()?.toString(); const identifiers = recipients.filter(id => id !== myE164 && id !== myUuid); if (identifiers.length === 0) { @@ -1921,20 +1911,21 @@ export default class MessageSender { async getSenderKeyDistributionMessage( distributionId: string ): Promise { - const ourUuid = window.textsecure.storage.user.getUuid(); - if (!ourUuid) { - throw new Error( - 'getSenderKeyDistributionMessage: Failed to fetch our UUID!' - ); - } + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); const ourDeviceId = parseIntOrThrow( window.textsecure.storage.user.getDeviceId(), 'getSenderKeyDistributionMessage' ); - const protocolAddress = ProtocolAddress.new(ourUuid, ourDeviceId); - const address = `${ourUuid}.${ourDeviceId}`; - const senderKeyStore = new SenderKeys(); + const protocolAddress = ProtocolAddress.new( + ourUuid.toString(), + ourDeviceId + ); + const address = new QualifiedAddress( + ourUuid, + new Address(ourUuid, ourDeviceId) + ); + const senderKeyStore = new SenderKeys({ ourUuid }); return window.textsecure.storage.protocol.enqueueSenderKeyJob( address, @@ -2044,7 +2035,7 @@ export default class MessageSender { options?: Readonly ): Promise { const myNumber = window.textsecure.storage.user.getNumber(); - const myUuid = window.textsecure.storage.user.getUuid(); + const myUuid = window.textsecure.storage.user.getUuid()?.toString(); const recipients = groupIdentifiers.filter( identifier => identifier !== myNumber && identifier !== myUuid ); diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index 9c25fb9cac..b85063c3af 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -6,9 +6,14 @@ import type { IncomingWebSocketRequest } from './WebsocketResources'; export { IdentityKeyType, + IdentityKeyIdType, + PreKeyIdType, PreKeyType, + SenderKeyIdType, SenderKeyType, + SessionIdType, SessionType, + SignedPreKeyIdType, SignedPreKeyType, UnprocessedType, UnprocessedUpdateType, diff --git a/ts/textsecure/getKeysForIdentifier.ts b/ts/textsecure/getKeysForIdentifier.ts index 3887b32a23..d83780c20e 100644 --- a/ts/textsecure/getKeysForIdentifier.ts +++ b/ts/textsecure/getKeysForIdentifier.ts @@ -10,6 +10,9 @@ import { import { UnregisteredUserError } from './Errors'; import { Sessions, IdentityKeys } from '../LibSignalStores'; +import { Address } from '../types/Address'; +import { QualifiedAddress } from '../types/QualifiedAddress'; +import { UUID } from '../types/UUID'; import { ServerKeysType, WebAPIType } from './WebAPI'; export async function getKeysForIdentifier( @@ -32,7 +35,11 @@ export async function getKeysForIdentifier( }; } catch (error) { if (error.name === 'HTTPError' && error.code === 404) { - await window.textsecure.storage.protocol.archiveAllSessions(identifier); + const theirUuid = UUID.lookup(identifier); + + if (theirUuid) { + await window.textsecure.storage.protocol.archiveAllSessions(theirUuid); + } } throw new UnregisteredUserError(identifier, error); } @@ -72,8 +79,9 @@ async function handleServerKeys( response: ServerKeysType, devicesToUpdate?: Array ): Promise { - const sessionStore = new Sessions(); - const identityKeyStore = new IdentityKeys(); + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + const sessionStore = new Sessions({ ourUuid }); + const identityKeyStore = new IdentityKeys({ ourUuid }); await Promise.all( response.devices.map(async device => { @@ -95,7 +103,11 @@ async function handleServerKeys( `getKeysForIdentifier/${identifier}: Missing signed prekey for deviceId ${deviceId}` ); } - const protocolAddress = ProtocolAddress.new(identifier, deviceId); + const theirUuid = UUID.checkedLookup(identifier); + const protocolAddress = ProtocolAddress.new( + theirUuid.toString(), + deviceId + ); const preKeyId = preKey?.keyId || null; const preKeyObject = preKey ? PublicKey.deserialize(Buffer.from(preKey.publicKey)) @@ -118,7 +130,10 @@ async function handleServerKeys( identityKey ); - const address = `${identifier}.${deviceId}`; + const address = new QualifiedAddress( + ourUuid, + new Address(theirUuid, deviceId) + ); await window.textsecure.storage.protocol .enqueueSessionJob(address, () => processPreKeyBundle( diff --git a/ts/textsecure/storage/User.ts b/ts/textsecure/storage/User.ts index ca99eae2a0..224ed19a23 100644 --- a/ts/textsecure/storage/User.ts +++ b/ts/textsecure/storage/User.ts @@ -5,6 +5,7 @@ import { WebAPICredentials } from '../Types.d'; import { strictAssert } from '../../util/assert'; import { StorageInterface } from '../../types/Storage.d'; +import { UUID } from '../../types/UUID'; import Helpers from '../Helpers'; @@ -56,10 +57,16 @@ export class User { return Helpers.unencodeNumber(numberId)[0]; } - public getUuid(): string | undefined { + public getUuid(): UUID | undefined { const uuid = this.storage.get('uuid_id'); if (uuid === undefined) return undefined; - return Helpers.unencodeNumber(uuid.toLowerCase())[0]; + return new UUID(Helpers.unencodeNumber(uuid.toLowerCase())[0]); + } + + public getCheckedUuid(): UUID { + const uuid = this.getUuid(); + strictAssert(uuid !== undefined, 'Must have our own uuid'); + return uuid; } public getDeviceId(): number | undefined { diff --git a/ts/types/Address.ts b/ts/types/Address.ts new file mode 100644 index 0000000000..0497a24c79 --- /dev/null +++ b/ts/types/Address.ts @@ -0,0 +1,30 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { strictAssert } from '../util/assert'; + +import { UUID, UUIDStringType } from './UUID'; + +export type AddressStringType = `${UUIDStringType}.${number}`; + +const ADDRESS_REGEXP = /^([0-9a-f-]+).(\d+)$/i; + +export class Address { + constructor(public readonly uuid: UUID, public readonly deviceId: number) {} + + public toString(): AddressStringType { + return `${this.uuid.toString()}.${this.deviceId}`; + } + + public static parse(value: string): Address { + const match = value.match(ADDRESS_REGEXP); + strictAssert(match !== null, `Invalid Address: ${value}`); + const [whole, uuid, deviceId] = match; + strictAssert(whole === value, 'Integrity check'); + return Address.create(uuid, parseInt(deviceId, 10)); + } + + public static create(uuid: string, deviceId: number): Address { + return new Address(new UUID(uuid), deviceId); + } +} diff --git a/ts/types/QualifiedAddress.ts b/ts/types/QualifiedAddress.ts new file mode 100644 index 0000000000..e229e64406 --- /dev/null +++ b/ts/types/QualifiedAddress.ts @@ -0,0 +1,48 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { strictAssert } from '../util/assert'; + +import { UUID, UUIDStringType } from './UUID'; +import { Address, AddressStringType } from './Address'; + +const QUALIFIED_ADDRESS_REGEXP = /^([0-9a-f-]+):([0-9a-f-]+).(\d+)$/i; + +export type QualifiedAddressCreateOptionsType = Readonly<{ + ourUuid: string; + uuid: string; + deviceId: number; +}>; + +export type QualifiedAddressStringType = `${UUIDStringType}:${AddressStringType}`; + +export class QualifiedAddress { + constructor( + public readonly ourUuid: UUID, + public readonly address: Address + ) {} + + public get uuid(): UUID { + return this.address.uuid; + } + + public get deviceId(): number { + return this.address.deviceId; + } + + public toString(): QualifiedAddressStringType { + return `${this.ourUuid.toString()}:${this.address.toString()}`; + } + + public static parse(value: string): QualifiedAddress { + const match = value.match(QUALIFIED_ADDRESS_REGEXP); + strictAssert(match !== null, `Invalid QualifiedAddress: ${value}`); + const [whole, ourUuid, uuid, deviceId] = match; + strictAssert(whole === value, 'Integrity check'); + + return new QualifiedAddress( + new UUID(ourUuid), + Address.create(uuid, parseInt(deviceId, 10)) + ); + } +} diff --git a/ts/types/Storage.d.ts b/ts/types/Storage.d.ts index a356396051..4ea05a62e5 100644 --- a/ts/types/Storage.d.ts +++ b/ts/types/Storage.d.ts @@ -30,6 +30,14 @@ export type ThemeSettingType = 'system' | 'light' | 'dark'; export type NotificationSettingType = 'message' | 'name' | 'count' | 'off'; +export type IdentityKeyMap = Record< + string, + { + privKey: string; + pubKey: string; + } +>; + // This should be in sync with `STORAGE_UI_KEYS` in `ts/types/StorageUIKeys.ts`. export type StorageAccessType = { 'always-relay-calls': boolean; @@ -55,7 +63,7 @@ export type StorageAccessType = { customColors: CustomColorsItemType; device_name: string; hasRegisterSupportForUnauthenticatedDelivery: boolean; - identityKey: KeyPairType; + identityKeyMap: IdentityKeyMap; lastHeartbeat: number; lastStartup: number; lastAttemptedToRefreshProfilesAt: number; @@ -64,7 +72,7 @@ export type StorageAccessType = { password: string; profileKey: ArrayBuffer; regionCode: string; - registrationId: number; + registrationIdMap: Record; remoteBuildExpiration: number; sessionResets: SessionResetsType; showStickerPickerHint: boolean; diff --git a/ts/types/UUID.ts b/ts/types/UUID.ts new file mode 100644 index 0000000000..1f1efd3306 --- /dev/null +++ b/ts/types/UUID.ts @@ -0,0 +1,44 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { strictAssert } from '../util/assert'; +import { isValidGuid } from '../util/isValidGuid'; + +export type UUIDStringType = `${string}-${string}-${string}-${string}-${string}`; + +export class UUID { + constructor(protected readonly value: string) { + strictAssert(isValidGuid(value), `Invalid UUID: ${value}`); + } + + public toString(): UUIDStringType { + return (this.value as unknown) as UUIDStringType; + } + + public isEqual(other: UUID): boolean { + return this.value === other.value; + } + + public static parse(value: string): UUID { + return new UUID(value); + } + + public static lookup(identifier: string): UUID | undefined { + const conversation = window.ConversationController.get(identifier); + const uuid = conversation?.get('uuid'); + if (uuid === undefined) { + return undefined; + } + + return new UUID(uuid); + } + + public static checkedLookup(identifier: string): UUID { + const uuid = UUID.lookup(identifier); + strictAssert( + uuid !== undefined, + `Conversation ${identifier} not found or has no uuid` + ); + return uuid; + } +} diff --git a/ts/util/getProfile.ts b/ts/util/getProfile.ts index 7e91aea9db..521c8509c8 100644 --- a/ts/util/getProfile.ts +++ b/ts/util/getProfile.ts @@ -3,6 +3,9 @@ import { ProfileKeyCredentialRequestContext } from 'zkgroup'; import { SEALED_SENDER } from '../types/SealedSender'; +import { Address } from '../types/Address'; +import { QualifiedAddress } from '../types/QualifiedAddress'; +import { UUID } from '../types/UUID'; import { base64ToArrayBuffer, stringFromBytes, @@ -54,6 +57,7 @@ export async function getProfile( const uuid = c.get('uuid')!; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const identifier = c.getSendTarget()!; + const targetUuid = UUID.checkedLookup(identifier); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const profileKeyVersionHex = c.get('profileKeyVersion')!; const existingProfileKeyCredential = c.get('profileKeyCredential'); @@ -115,15 +119,16 @@ export async function getProfile( const identityKey = base64ToArrayBuffer(profile.identityKey); const changed = await window.textsecure.storage.protocol.saveIdentity( - `${identifier}.1`, + new Address(targetUuid, 1), identityKey, false ); if (changed) { // save identity will close all sessions except for .1, so we // must close that one manually. + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); await window.textsecure.storage.protocol.archiveSession( - `${identifier}.1` + new QualifiedAddress(ourUuid, new Address(targetUuid, 1)) ); } diff --git a/ts/util/handleRetry.ts b/ts/util/handleRetry.ts index e80e2d32b0..63a60a0a7f 100644 --- a/ts/util/handleRetry.ts +++ b/ts/util/handleRetry.ts @@ -15,6 +15,8 @@ import { isGroupV2 } from './whatTypeOfConversation'; import { isOlderThan } from './timestamp'; import { parseIntOrThrow } from './parseIntOrThrow'; import * as RemoteConfig from '../RemoteConfig'; +import { Address } from '../types/Address'; +import { QualifiedAddress } from '../types/QualifiedAddress'; import { ConversationModel } from '../models/conversations'; import { @@ -184,7 +186,11 @@ async function archiveSessionOnMatch({ return; } - const address = `${requesterUuid}.${requesterDevice}`; + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + const address = new QualifiedAddress( + ourUuid, + Address.create(requesterUuid, requesterDevice) + ); const session = await window.textsecure.storage.protocol.loadSession(address); if (session && session.currentRatchetKeyMatches(ratchetKey)) { @@ -500,9 +506,10 @@ function scheduleSessionReset(senderUuid: string, senderDevice: number) { } lightSessionResetQueue.add(() => { + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + window.textsecure.storage.protocol.lightSessionReset( - senderUuid, - senderDevice + new QualifiedAddress(ourUuid, Address.create(senderUuid, senderDevice)) ); }); } diff --git a/ts/util/safetyNumber.ts b/ts/util/safetyNumber.ts index 149e50de18..abb8c93d99 100644 --- a/ts/util/safetyNumber.ts +++ b/ts/util/safetyNumber.ts @@ -3,6 +3,7 @@ import { PublicKey, Fingerprint } from '@signalapp/signal-client'; import { ConversationType } from '../state/ducks/conversations'; +import { UUID } from '../types/UUID'; import { assert } from './assert'; @@ -33,16 +34,18 @@ export async function generateSecurityNumber( export async function generateSecurityNumberBlock( contact: ConversationType ): Promise> { - const ourNumber = window.textsecure.storage.user.getNumber(); - const ourUuid = window.textsecure.storage.user.getUuid(); + const { storage } = window.textsecure; + const ourNumber = storage.user.getNumber(); + const ourUuid = storage.user.getCheckedUuid(); - const us = window.textsecure.storage.protocol.getIdentityRecord( - ourUuid || ourNumber || '' - ); + const us = storage.protocol.getIdentityRecord(ourUuid); const ourKey = us ? us.publicKey : null; - const them = window.textsecure.storage.protocol.getIdentityRecord(contact.id); - const theirKey = them ? them.publicKey : null; + const theirUuid = UUID.lookup(contact.id); + const them = theirUuid + ? await storage.protocol.getOrMigrateIdentityRecord(theirUuid) + : undefined; + const theirKey = them?.publicKey; if (!ourKey) { throw new Error('Could not load our key'); diff --git a/ts/util/sendToGroup.ts b/ts/util/sendToGroup.ts index a744ec7828..bc0ff7b71a 100644 --- a/ts/util/sendToGroup.ts +++ b/ts/util/sendToGroup.ts @@ -20,6 +20,9 @@ import { SenderCertificateMode, SendLogCallbackType, } from '../textsecure/OutgoingMessage'; +import { Address } from '../types/Address'; +import { QualifiedAddress } from '../types/QualifiedAddress'; +import { UUID } from '../types/UUID'; import { isEnabled } from '../RemoteConfig'; import { isOlderThan } from './timestamp'; @@ -286,10 +289,14 @@ export async function sendToGroupViaSenderKey(options: { } // 2. Fetch all devices we believe we'll be sending to + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); const { devices: currentDevices, emptyIdentifiers, - } = await window.textsecure.storage.protocol.getOpenDevices(recipients); + } = await window.textsecure.storage.protocol.getOpenDevices( + ourUuid, + recipients + ); // 3. If we have no open sessions with people we believe we are sending to, and we // believe that any have signal accounts, fetch their prekey bundle and start @@ -669,7 +676,13 @@ async function markIdentifierUnregistered(identifier: string) { conversation.setUnregistered(); window.Signal.Data.updateConversation(conversation.attributes); - await window.textsecure.storage.protocol.archiveAllSessions(identifier); + const uuid = UUID.lookup(identifier); + if (!uuid) { + window.log.warn(`No uuid found for ${identifier}`); + return; + } + + await window.textsecure.storage.protocol.archiveAllSessions(uuid); } function isIdentifierRegistered(identifier: string) { @@ -695,10 +708,13 @@ async function handle409Response(logId: string, error: Error) { // Archive sessions with devices that have been removed if (devices.extraDevices && devices.extraDevices.length > 0) { + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + await _waitForAll({ tasks: devices.extraDevices.map(deviceId => async () => { - const address = `${uuid}.${deviceId}`; - await window.textsecure.storage.protocol.archiveSession(address); + await window.textsecure.storage.protocol.archiveSession( + new QualifiedAddress(ourUuid, Address.create(uuid, deviceId)) + ); }), }); } @@ -727,11 +743,14 @@ async function handle410Response( tasks: parsed.data.map(item => async () => { const { uuid, devices } = item; if (devices.staleDevices && devices.staleDevices.length > 0) { + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + // First, archive our existing sessions with these devices await _waitForAll({ tasks: devices.staleDevices.map(deviceId => async () => { - const address = `${uuid}.${deviceId}`; - await window.textsecure.storage.protocol.archiveSession(address); + await window.textsecure.storage.protocol.archiveSession( + new QualifiedAddress(ourUuid, Address.create(uuid, deviceId)) + ); }), }); @@ -822,24 +841,24 @@ async function encryptForSenderKey({ distributionId: string; groupId: string; }): Promise { - const ourUuid = window.textsecure.storage.user.getUuid(); + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); const ourDeviceId = window.textsecure.storage.user.getDeviceId(); - if (!ourUuid || !ourDeviceId) { + if (!ourDeviceId) { throw new Error( 'encryptForSenderKey: Unable to fetch our uuid or deviceId' ); } const sender = ProtocolAddress.new( - ourUuid, + ourUuid.toString(), parseIntOrThrow(ourDeviceId, 'encryptForSenderKey, ourDeviceId') ); const ourAddress = getOurAddress(); - const senderKeyStore = new SenderKeys(); + const senderKeyStore = new SenderKeys({ ourUuid }); const message = Buffer.from(padMessage(new FIXMEU8(contentMessage))); const ciphertextMessage = await window.textsecure.storage.protocol.enqueueSenderKeyJob( - ourAddress, + new QualifiedAddress(ourUuid, ourAddress), () => groupEncrypt(sender, distributionId, senderKeyStore, message) ); @@ -874,9 +893,14 @@ async function encryptForSenderKey({ return 1; }) - .map(device => ProtocolAddress.new(device.identifier, device.id)); - const identityKeyStore = new IdentityKeys(); - const sessionStore = new Sessions(); + .map(device => { + return ProtocolAddress.new( + UUID.checkedLookup(device.identifier).toString(), + device.id + ); + }); + const identityKeyStore = new IdentityKeys({ ourUuid }); + const sessionStore = new Sessions({ ourUuid }); return sealedSenderMultiRecipientEncrypt( content, recipients, @@ -998,13 +1022,13 @@ export function _analyzeSenderKeyDevices( }; } -function getOurAddress(): string { - const ourUuid = window.textsecure.storage.user.getUuid(); +function getOurAddress(): Address { + const ourUuid = window.textsecure.storage.user.getCheckedUuid(); const ourDeviceId = window.textsecure.storage.user.getDeviceId(); - if (!ourUuid || !ourDeviceId) { - throw new Error('getOurAddress: Unable to fetch our uuid or deviceId'); + if (!ourDeviceId) { + throw new Error('getOurAddress: Unable to fetch our deviceId'); } - return `${ourUuid}.${ourDeviceId}`; + return new Address(ourUuid, ourDeviceId); } async function resetSenderKey(conversation: ConversationModel): Promise { @@ -1023,7 +1047,7 @@ async function resetSenderKey(conversation: ConversationModel): Promise { } const { distributionId } = senderKeyInfo; - const address = getOurAddress(); + const ourAddress = getOurAddress(); // Note: We preserve existing distributionId to minimize space for sender key storage conversation.set({ @@ -1035,8 +1059,9 @@ async function resetSenderKey(conversation: ConversationModel): Promise { }); window.Signal.Data.updateConversation(conversation.attributes); + const ourUuid = window.storage.user.getCheckedUuid(); await window.textsecure.storage.protocol.removeSenderKey( - address, + new QualifiedAddress(ourUuid, ourAddress), distributionId ); } diff --git a/ts/util/whatTypeOfConversation.ts b/ts/util/whatTypeOfConversation.ts index 2049502056..d262336a87 100644 --- a/ts/util/whatTypeOfConversation.ts +++ b/ts/util/whatTypeOfConversation.ts @@ -25,7 +25,7 @@ export function isDirectConversation( export function isMe(conversationAttrs: ConversationAttributesType): boolean { const { e164, uuid } = conversationAttrs; const ourNumber = window.textsecure.storage.user.getNumber(); - const ourUuid = window.textsecure.storage.user.getUuid(); + const ourUuid = window.textsecure.storage.user.getUuid()?.toString(); return Boolean((e164 && e164 === ourNumber) || (uuid && uuid === ourUuid)); } diff --git a/ts/window.d.ts b/ts/window.d.ts index 27c735277a..5ab57a5f80 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -117,6 +117,9 @@ import { MessageController } from './util/MessageController'; import { isValidGuid } from './util/isValidGuid'; import { StateType } from './state/reducer'; import { SystemTraySetting } from './types/SystemTraySetting'; +import { UUID } from './types/UUID'; +import { Address } from './types/Address'; +import { QualifiedAddress } from './types/QualifiedAddress'; import { CI } from './CI'; import { IPCEventsType } from './util/createIPCEvents'; import { ConversationView } from './views/conversation_view'; @@ -396,6 +399,9 @@ declare global { path: string; }; VisualAttachment: any; + UUID: typeof UUID; + Address: typeof Address; + QualifiedAddress: typeof QualifiedAddress; }; Util: typeof Util; GroupChange: { @@ -595,7 +601,6 @@ export type WhisperType = { MessageCollection: typeof MessageModelCollectionType; GroupMemberConversation: WhatIsThis; - KeyChangeListener: WhatIsThis; RotateSignedPreKeyListener: WhatIsThis; WallClockListener: WhatIsThis;