From 79b48115e63f1be4586ad5b506e53dd200c5a7e4 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Thu, 28 Jul 2022 09:35:29 -0700 Subject: [PATCH] Handle PniChangeNumber --- app/main.ts | 1 - config/default.json | 3 +- package.json | 4 +- protos/SignalService.proto | 10 +- ts/SignalProtocolStore.ts | 108 ++++- ts/background.ts | 14 - ts/protobuf/index.ts | 4 +- ts/services/groupCredentialFetcher.ts | 53 ++- ts/sql/Client.ts | 449 ++++++++++-------- ts/sql/Interface.ts | 173 ++++--- ts/sql/Server.ts | 88 ++-- .../migrations/64-uuid-column-for-pre-keys.ts | 38 ++ ts/sql/migrations/index.ts | 2 + ts/test-electron/SignalProtocolStore_test.ts | 90 +++- .../textsecure/generate_keys_test.ts | 6 +- ts/test-mock/bootstrap.ts | 2 - ts/test-mock/change-number/prekey_test.ts | 77 +++ ts/test-mock/gv2/create_test.ts | 4 +- ts/test-node/util/mapObjectWithSpec_test.ts | 64 +++ ts/textsecure/AccountManager.ts | 97 ++-- ts/textsecure/MessageReceiver.ts | 134 ++++-- ts/textsecure/Types.d.ts | 7 + ts/textsecure/WebAPI.ts | 17 +- ts/textsecure/cds/CDSI.ts | 5 - ts/textsecure/cds/CDSISocket.ts | 7 +- ts/textsecure/messageReceiverEvents.ts | 14 - ts/types/RendererConfig.ts | 2 - ts/types/Storage.d.ts | 4 +- ts/types/Util.ts | 4 + ts/util/mapObjectWithSpec.ts | 68 +++ ts/util/updateOurUsernameAndPni.ts | 4 +- yarn.lock | 18 +- 32 files changed, 1086 insertions(+), 485 deletions(-) create mode 100644 ts/sql/migrations/64-uuid-column-for-pre-keys.ts create mode 100644 ts/test-mock/change-number/prekey_test.ts create mode 100644 ts/test-node/util/mapObjectWithSpec_test.ts create mode 100644 ts/util/mapObjectWithSpec.ts diff --git a/app/main.ts b/app/main.ts index 08f01464c..a56b71bae 100644 --- a/app/main.ts +++ b/app/main.ts @@ -389,7 +389,6 @@ async function prepareUrl( directoryV3Url: config.get('directoryV3Url') || undefined, directoryV3MRENCLAVE: config.get('directoryV3MRENCLAVE') || undefined, - directoryV3Root: config.get('directoryV3Root') || undefined, }); if (!directoryConfig.success) { throw new Error( diff --git a/config/default.json b/config/default.json index 7b3fbad42..538315950 100644 --- a/config/default.json +++ b/config/default.json @@ -9,8 +9,7 @@ "directoryV2PublicKey": null, "directoryV2CodeHashes": null, "directoryV3Url": "https://cdsi.staging.signal.org", - "directoryV3MRENCLAVE": "e5eaa62da3514e8b37ccabddb87e52e7f319ccf5120a13f9e1b42b87ec9dd3dd", - "directoryV3Root": "-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n", + "directoryV3MRENCLAVE": "7b75dd6e862decef9b37132d54be082441917a7790e82fe44f9cf653de03a75f", "cdn": { "0": "https://cdn-staging.signal.org", "2": "https://cdn2-staging.signal.org" diff --git a/package.json b/package.json index 67ed6eb72..8b1282963 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "@indutny/frameless-titlebar": "2.3.5", "@popperjs/core": "2.9.2", "@react-spring/web": "9.4.5", - "@signalapp/libsignal-client": "0.18.1", + "@signalapp/libsignal-client": "0.19.1", "@sindresorhus/is": "0.8.0", "@types/fabric": "4.5.3", "abort-controller": "3.0.0", @@ -190,7 +190,7 @@ "@babel/preset-typescript": "7.17.12", "@electron/fuses": "1.5.0", "@mixer/parallel-prettier": "2.0.1", - "@signalapp/mock-server": "2.1.0", + "@signalapp/mock-server": "2.3.0", "@storybook/addon-a11y": "6.5.6", "@storybook/addon-actions": "6.5.6", "@storybook/addon-controls": "6.5.6", diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 9bd13ab1f..64c5d4363 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -35,7 +35,8 @@ message Envelope { optional uint64 serverTimestamp = 10; optional bool ephemeral = 12; // indicates that the message should not be persisted if the recipient is offline optional bool urgent = 14 [default=true]; // indicates that the content is considered timely by the sender; defaults to true so senders have to opt-out to say something isn't time critical - // next: 15 + optional string updated_pni = 15; + // next: 16 } message Content { @@ -509,6 +510,12 @@ message SyncMessage { optional Type type = 1; } + message PniChangeNumber { + optional bytes identityKeyPair = 1; // Serialized libsignal-client IdentityKeyPair + optional bytes signedPreKey = 2; // Serialized libsignal-client SignedPreKeyRecord + optional uint32 registrationId = 3; + } + optional Sent sent = 1; optional Contacts contacts = 2; optional Groups groups = 3; @@ -526,6 +533,7 @@ message SyncMessage { reserved 15; // not yet added repeated Viewed viewed = 16; optional PniIdentity pniIdentity = 17; + optional PniChangeNumber pniChangeNumber = 18; } message AttachmentPointer { diff --git a/ts/SignalProtocolStore.ts b/ts/SignalProtocolStore.ts index cf96c7230..db6bfb8d0 100644 --- a/ts/SignalProtocolStore.ts +++ b/ts/SignalProtocolStore.ts @@ -2,11 +2,12 @@ // SPDX-License-Identifier: AGPL-3.0-only import PQueue from 'p-queue'; -import { isNumber } from 'lodash'; +import { isNumber, omit } from 'lodash'; import { z } from 'zod'; import { Direction, + IdentityKeyPair, PreKeyRecord, PrivateKey, PublicKey, @@ -31,6 +32,7 @@ import type { IdentityKeyIdType, KeyPairType, OuterSignedPrekeyType, + PniKeyMaterialType, PreKeyIdType, PreKeyType, SenderKeyIdType, @@ -255,8 +257,8 @@ export class SignalProtocolStore extends EventsMixin { for (const key of Object.keys(map.value)) { const { privKey, pubKey } = map.value[key]; this.ourIdentityKeys.set(new UUID(key).toString(), { - privKey: Bytes.fromBase64(privKey), - pubKey: Bytes.fromBase64(pubKey), + privKey, + pubKey, }); } })(), @@ -461,7 +463,8 @@ export class SignalProtocolStore extends EventsMixin { ourUuid: UUID, keyId: number, keyPair: KeyPairType, - confirmed?: boolean + confirmed?: boolean, + createdAt = Date.now() ): Promise { if (!this.signedPreKeys) { throw new Error('storeSignedPreKey: this.signedPreKeys not yet cached!'); @@ -475,7 +478,7 @@ export class SignalProtocolStore extends EventsMixin { keyId, publicKey: keyPair.pubKey, privateKey: keyPair.privKey, - created_at: Date.now(), + created_at: createdAt, confirmed: Boolean(confirmed), }; @@ -1935,6 +1938,101 @@ export class SignalProtocolStore extends EventsMixin { }); } + async removeOurOldPni(oldPni: UUID): Promise { + const { storage } = window; + + log.info(`SignalProtocolStore.removeOurOldPni(${oldPni})`); + + // Update caches + this.ourIdentityKeys.delete(oldPni.toString()); + this.ourRegistrationIds.delete(oldPni.toString()); + + const preKeyPrefix = `${oldPni.toString()}:`; + if (this.preKeys) { + for (const key of this.preKeys.keys()) { + if (key.startsWith(preKeyPrefix)) { + this.preKeys.delete(key); + } + } + } + if (this.signedPreKeys) { + for (const key of this.signedPreKeys.keys()) { + if (key.startsWith(preKeyPrefix)) { + this.signedPreKeys.delete(key); + } + } + } + + // Update database + await Promise.all([ + storage.put( + 'identityKeyMap', + omit(storage.get('identityKeyMap') || {}, oldPni.toString()) + ), + storage.put( + 'registrationIdMap', + omit(storage.get('registrationIdMap') || {}, oldPni.toString()) + ), + window.Signal.Data.removePreKeysByUuid(oldPni.toString()), + window.Signal.Data.removeSignedPreKeysByUuid(oldPni.toString()), + ]); + } + + async updateOurPniKeyMaterial( + pni: UUID, + { + identityKeyPair: identityBytes, + signedPreKey: signedPreKeyBytes, + registrationId, + }: PniKeyMaterialType + ): Promise { + log.info(`SignalProtocolStore.updateOurPniKeyMaterial(${pni})`); + + const identityKeyPair = IdentityKeyPair.deserialize( + Buffer.from(identityBytes) + ); + const signedPreKey = SignedPreKeyRecord.deserialize( + Buffer.from(signedPreKeyBytes) + ); + + const { storage } = window; + + const pniPublicKey = identityKeyPair.publicKey.serialize(); + const pniPrivateKey = identityKeyPair.privateKey.serialize(); + + // Update caches + this.ourIdentityKeys.set(pni.toString(), { + pubKey: pniPublicKey, + privKey: pniPrivateKey, + }); + this.ourRegistrationIds.set(pni.toString(), registrationId); + + // Update database + await Promise.all([ + storage.put('identityKeyMap', { + ...(storage.get('identityKeyMap') || {}), + [pni.toString()]: { + pubKey: pniPublicKey, + privKey: pniPrivateKey, + }, + }), + storage.put('registrationIdMap', { + ...(storage.get('registrationIdMap') || {}), + [pni.toString()]: registrationId, + }), + this.storeSignedPreKey( + pni, + signedPreKey.id(), + { + privKey: signedPreKey.privateKey().serialize(), + pubKey: signedPreKey.publicKey().serialize(), + }, + true, + signedPreKey.timestamp() + ), + ]); + } + async removeAllData(): Promise { await window.Signal.Data.removeAll(); await this.hydrateCaches(); diff --git a/ts/background.ts b/ts/background.ts index e5f4ec272..3ce1cced6 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -79,7 +79,6 @@ import type { FetchLatestEvent, GroupEvent, KeysEvent, - PNIIdentityEvent, MessageEvent, MessageEventData, MessageRequestResponseEvent, @@ -395,10 +394,6 @@ export async function startApp(): Promise { queuedEventListener(onFetchLatestSync) ); messageReceiver.addEventListener('keys', queuedEventListener(onKeysSync)); - messageReceiver.addEventListener( - 'pniIdentity', - queuedEventListener(onPNIIdentitySync) - ); messageReceiver.addEventListener( 'storyRecipientUpdate', queuedEventListener(onStoryRecipientUpdate, false) @@ -3670,15 +3665,6 @@ export async function startApp(): Promise { } } - async function onPNIIdentitySync(ev: PNIIdentityEvent) { - ev.confirm(); - - log.info('onPNIIdentitySync: updating PNI keys'); - const manager = window.getAccountManager(); - const { privateKey: privKey, publicKey: pubKey } = ev.data; - await manager.updatePNIIdentity({ privKey, pubKey }); - } - async function onMessageRequestResponse(ev: MessageRequestResponseEvent) { ev.confirm(); diff --git a/ts/protobuf/index.ts b/ts/protobuf/index.ts index 8b8f02710..5c2465d1f 100644 --- a/ts/protobuf/index.ts +++ b/ts/protobuf/index.ts @@ -3,6 +3,6 @@ import './wrap'; -import { signalservice as SignalService } from './compiled'; +import { signalservice as SignalService, signal as Signal } from './compiled'; -export { SignalService }; +export { SignalService, Signal }; diff --git a/ts/services/groupCredentialFetcher.ts b/ts/services/groupCredentialFetcher.ts index 04267a0c1..80a40c845 100644 --- a/ts/services/groupCredentialFetcher.ts +++ b/ts/services/groupCredentialFetcher.ts @@ -17,7 +17,7 @@ import * as log from '../logging/log'; export const GROUP_CREDENTIALS_KEY = 'groupCredentials'; -type CredentialsDataType = Array; +type CredentialsDataType = ReadonlyArray; type RequestDatesType = { startDayInMs: number; endDayInMs: number; @@ -145,33 +145,40 @@ export async function maybeFetchNewCredentials(): Promise { `${logId}: fetching credentials for ${startDayInMs} through ${endDayInMs}` ); - // TODO(indutny): In the future we'd like to avoid this extra call all the time - const { pni } = await server.whoami(); - if (!pni) { - log.info(`${logId}: no PNI, returning early`); - return; - } - const serverPublicParamsBase64 = window.getServerPublicParams(); const clientZKAuthOperations = getClientZkAuthOperations( serverPublicParamsBase64 ); - const newCredentials = sortCredentials( - await server.getGroupCredentials({ startDayInMs, endDayInMs }) - ).map((item: GroupCredentialType) => { - const authCredential = clientZKAuthOperations.receiveAuthCredentialWithPni( - aci, - pni, - item.redemptionTime, - new AuthCredentialWithPniResponse(Buffer.from(item.credential, 'base64')) - ); - const credential = authCredential.serialize().toString('base64'); - return { - redemptionTime: item.redemptionTime * durations.SECOND, - credential, - }; - }); + const { pni, credentials: rawCredentials } = await server.getGroupCredentials( + { startDayInMs, endDayInMs } + ); + strictAssert(pni, 'Server must give pni along with group credentials'); + + const localPni = window.storage.user.getUuid(UUIDKind.PNI); + if (pni !== localPni?.toString()) { + log.error(`${logId}: local PNI ${localPni}, does not match remote ${pni}`); + } + + const newCredentials = sortCredentials(rawCredentials).map( + (item: GroupCredentialType) => { + const authCredential = + clientZKAuthOperations.receiveAuthCredentialWithPni( + aci, + pni, + item.redemptionTime, + new AuthCredentialWithPniResponse( + Buffer.from(item.credential, 'base64') + ) + ); + const credential = authCredential.serialize().toString('base64'); + + return { + redemptionTime: item.redemptionTime * durations.SECOND, + credential, + }; + } + ); const today = toDayMillis(Date.now()); const previousCleaned = previous diff --git a/ts/sql/Client.ts b/ts/sql/Client.ts index 1762bd10f..2b79ae7e2 100644 --- a/ts/sql/Client.ts +++ b/ts/sql/Client.ts @@ -3,26 +3,19 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable camelcase */ -/* eslint-disable no-param-reassign */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/ban-types */ import { ipcRenderer as ipc } from 'electron'; import fs from 'fs-extra'; import pify from 'pify'; import { - cloneDeep, compact, fromPairs, - get, groupBy, isFunction, isTypedArray, last, map, omit, - set, toPairs, uniq, } from 'lodash'; @@ -34,6 +27,8 @@ import * as Bytes from '../Bytes'; import { CURRENT_SCHEMA_VERSION } from '../types/Message2'; import { createBatcher } from '../util/batcher'; import { assert, strictAssert } from '../util/assert'; +import { mapObjectWithSpec } from '../util/mapObjectWithSpec'; +import type { ObjectMappingSpecType } from '../util/mapObjectWithSpec'; import { cleanDataForIpc } from './cleanDataForIpc'; import type { ReactionType } from '../types/Reactions'; import type { ConversationColorType, CustomColorType } from '../types/Colors'; @@ -49,21 +44,30 @@ import { formatJobForInsert } from '../jobs/formatJobForInsert'; import { cleanupMessage } from '../util/cleanup'; import type { + AllItemsType, AttachmentDownloadJobType, ClientInterface, ClientJobType, ClientSearchResultMessageType, ConversationType, + ConversationMetricsType, DeleteSentProtoRecipientOptionsType, + EmojiType, + GetUnreadByConversationAndMarkReadResultType, + GetConversationRangeCenteredOnMessageResultType, IdentityKeyIdType, IdentityKeyType, + StoredIdentityKeyType, ItemKeyType, ItemType, + StoredItemType, ConversationMessageStatsType, MessageType, MessageTypeUnhydrated, PreKeyIdType, PreKeyType, + ReactionResultType, + StoredPreKeyType, SenderKeyIdType, SenderKeyType, SentMessageDBType, @@ -78,6 +82,7 @@ import type { SessionType, SignedPreKeyIdType, SignedPreKeyType, + StoredSignedPreKeyType, StickerPackStatusType, StickerPackType, StickerType, @@ -115,9 +120,9 @@ const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments'; const ENSURE_FILE_PERMISSIONS = 'ensure-file-permissions'; type ClientJobUpdateType = { - resolve: Function; - reject: Function; - args?: Array; + resolve: (value: unknown) => void; + reject: (error: Error) => void; + args?: ReadonlyArray; }; enum RendererState { @@ -131,8 +136,8 @@ const _jobs: { [id: string]: ClientJobType } = Object.create(null); const _DEBUG = false; let _jobCounter = 0; let _shuttingDown = false; -let _shutdownCallback: Function | null = null; -let _shutdownPromise: Promise | null = null; +let _shutdownCallback: ((error?: Error) => void) | null = null; +let _shutdownPromise: Promise | null = null; let state = RendererState.InMain; const startupQueries = new Map(); @@ -156,6 +161,7 @@ const dataInterface: ClientInterface = { getPreKeyById, bulkAddPreKeys, removePreKeyById, + removePreKeysByUuid, removeAllPreKeys, getAllPreKeys, @@ -163,6 +169,7 @@ const dataInterface: ClientInterface = { getSignedPreKeyById, bulkAddSignedPreKeys, removeSignedPreKeyById, + removeSignedPreKeysByUuid, removeAllSignedPreKeys, getAllSignedPreKeys, @@ -409,7 +416,7 @@ async function goBackToMainProcess(): Promise { const channelsAsUnknown = fromPairs( compact( - map(toPairs(dataInterface), ([name, value]: [string, any]) => { + map(toPairs(dataInterface), ([name, value]: [string, unknown]) => { if (isFunction(value)) { return [name, makeChannel(name)]; } @@ -417,9 +424,9 @@ const channelsAsUnknown = fromPairs( return null; }) ) -) as any; +) as unknown; -const channels: ServerInterface = channelsAsUnknown; +const channels: ServerInterface = channelsAsUnknown as ServerInterface; function _cleanData( data: unknown @@ -436,14 +443,15 @@ function _cleanData( } export function _cleanMessageData(data: MessageType): MessageType { + const result = { ...data }; // Ensure that all messages have the received_at set properly if (!data.received_at) { assert(false, 'received_at was not set on the message'); - data.received_at = window.Signal.Util.incrementMessageCounter(); + result.received_at = window.Signal.Util.incrementMessageCounter(); } if (data.attachments) { const logId = getMessageIdForLogging(data); - data.attachments = data.attachments.map((attachment, index) => { + result.attachments = data.attachments.map((attachment, index) => { if (attachment.data && !isTypedArray(attachment.data)) { log.warn( `_cleanMessageData/${logId}: Attachment ${index} had non-array \`data\` field; deleting.` @@ -454,7 +462,7 @@ export function _cleanMessageData(data: MessageType): MessageType { return attachment; }); } - return _cleanData(omit(data, ['dataMessage'])); + return _cleanData(omit(result, ['dataMessage'])); } async function _shutdown() { @@ -478,7 +486,7 @@ async function _shutdown() { // Outstanding jobs; we need to wait until the last one is done _shutdownPromise = new Promise((resolve, reject) => { - _shutdownCallback = (error: Error) => { + _shutdownCallback = (error?: Error) => { log.info('data.shutdown: process complete'); if (error) { reject(error); @@ -521,7 +529,7 @@ function _updateJob(id: number, data: ClientJobUpdateType) { _jobs[id] = { ..._jobs[id], ...data, - resolve: (value: any) => { + resolve: (value: unknown) => { _removeJob(id); const end = Date.now(); if (_DEBUG) { @@ -595,18 +603,21 @@ if (ipc && ipc.on) { } function makeChannel(fnName: string) { - return async (...args: Array) => { + return async (...args: ReadonlyArray) => { // During startup we want to avoid the high overhead of IPC so we utilize // the db that exists in the renderer process to be able to boot up quickly // once the app is running we switch back to the main process to avoid the // UI from locking up whenever we do costly db operations. if (state === RendererState.InRenderer) { const serverFnName = fnName as keyof ServerInterface; + const serverFn = Server[serverFnName] as ( + ...fnArgs: ReadonlyArray + ) => unknown; const start = Date.now(); try { // Ignoring this error TS2556: Expected 3 arguments, but got 0 or more. - return await (Server[serverFnName] as Function)(...args); + return await serverFn(...args); } catch (error) { if (isCorruptionError(error)) { log.error( @@ -659,41 +670,27 @@ function makeChannel(fnName: string) { }; } -function keysToBytes(keys: Array, data: any) { - const updated = cloneDeep(data); - - const max = keys.length; - for (let i = 0; i < max; i += 1) { - const key = keys[i]; - const value = get(data, key); - - if (value) { - set(updated, key, Bytes.fromBase64(value)); - } - } - - return updated; +function specToBytes( + spec: ObjectMappingSpecType, + data: Input +): Output { + return mapObjectWithSpec(spec, data, x => + Bytes.fromBase64(x) + ); } -function keysFromBytes(keys: Array, data: any) { - const updated = cloneDeep(data); - - const max = keys.length; - for (let i = 0; i < max; i += 1) { - const key = keys[i]; - const value = get(data, key); - - if (value) { - set(updated, key, Bytes.toBase64(value)); - } - } - - return updated; +function specFromBytes( + spec: ObjectMappingSpecType, + data: Input +): Output { + return mapObjectWithSpec(spec, data, x => + Bytes.toBase64(x) + ); } // Top-level calls -async function shutdown() { +async function shutdown(): Promise { log.info('Client.shutdown'); // Stop accepting new SQL jobs, flush outstanding queue @@ -704,111 +701,144 @@ async function shutdown() { } // Note: will need to restart the app after calling this, to set up afresh -async function close() { +async function close(): Promise { await channels.close(); } // Note: will need to restart the app after calling this, to set up afresh -async function removeDB() { +async function removeDB(): Promise { await channels.removeDB(); } -async function removeIndexedDBFiles() { +async function removeIndexedDBFiles(): Promise { await channels.removeIndexedDBFiles(); } // Identity Keys -const IDENTITY_KEY_KEYS = ['publicKey']; -async function createOrUpdateIdentityKey(data: IdentityKeyType) { - const updated = keysFromBytes(IDENTITY_KEY_KEYS, data); +const IDENTITY_KEY_SPEC = ['publicKey']; +async function createOrUpdateIdentityKey(data: IdentityKeyType): Promise { + const updated: StoredIdentityKeyType = specFromBytes(IDENTITY_KEY_SPEC, data); await channels.createOrUpdateIdentityKey(updated); } -async function getIdentityKeyById(id: IdentityKeyIdType) { +async function getIdentityKeyById( + id: IdentityKeyIdType +): Promise { const data = await channels.getIdentityKeyById(id); - return keysToBytes(IDENTITY_KEY_KEYS, data); + return specToBytes(IDENTITY_KEY_SPEC, data); } -async function bulkAddIdentityKeys(array: Array) { - const updated = map(array, data => keysFromBytes(IDENTITY_KEY_KEYS, data)); +async function bulkAddIdentityKeys( + array: Array +): Promise { + const updated: Array = map(array, data => + specFromBytes(IDENTITY_KEY_SPEC, data) + ); await channels.bulkAddIdentityKeys(updated); } -async function removeIdentityKeyById(id: IdentityKeyIdType) { +async function removeIdentityKeyById(id: IdentityKeyIdType): Promise { await channels.removeIdentityKeyById(id); } -async function removeAllIdentityKeys() { +async function removeAllIdentityKeys(): Promise { await channels.removeAllIdentityKeys(); } -async function getAllIdentityKeys() { +async function getAllIdentityKeys(): Promise> { const keys = await channels.getAllIdentityKeys(); - return keys.map(key => keysToBytes(IDENTITY_KEY_KEYS, key)); + return keys.map(key => specToBytes(IDENTITY_KEY_SPEC, key)); } // Pre Keys -async function createOrUpdatePreKey(data: PreKeyType) { - const updated = keysFromBytes(PRE_KEY_KEYS, data); +async function createOrUpdatePreKey(data: PreKeyType): Promise { + const updated: StoredPreKeyType = specFromBytes(PRE_KEY_SPEC, data); await channels.createOrUpdatePreKey(updated); } -async function getPreKeyById(id: PreKeyIdType) { +async function getPreKeyById( + id: PreKeyIdType +): Promise { const data = await channels.getPreKeyById(id); - return keysToBytes(PRE_KEY_KEYS, data); + return specToBytes(PRE_KEY_SPEC, data); } -async function bulkAddPreKeys(array: Array) { - const updated = map(array, data => keysFromBytes(PRE_KEY_KEYS, data)); +async function bulkAddPreKeys(array: Array): Promise { + const updated: Array = map(array, data => + specFromBytes(PRE_KEY_SPEC, data) + ); await channels.bulkAddPreKeys(updated); } -async function removePreKeyById(id: PreKeyIdType) { +async function removePreKeyById(id: PreKeyIdType): Promise { await channels.removePreKeyById(id); } -async function removeAllPreKeys() { +async function removePreKeysByUuid(uuid: UUIDStringType): Promise { + await channels.removePreKeysByUuid(uuid); +} +async function removeAllPreKeys(): Promise { await channels.removeAllPreKeys(); } -async function getAllPreKeys() { +async function getAllPreKeys(): Promise> { const keys = await channels.getAllPreKeys(); - return keys.map(key => keysToBytes(PRE_KEY_KEYS, key)); + return keys.map(key => specToBytes(PRE_KEY_SPEC, key)); } // Signed Pre Keys -const PRE_KEY_KEYS = ['privateKey', 'publicKey']; -async function createOrUpdateSignedPreKey(data: SignedPreKeyType) { - const updated = keysFromBytes(PRE_KEY_KEYS, data); +const PRE_KEY_SPEC = ['privateKey', 'publicKey']; +async function createOrUpdateSignedPreKey( + data: SignedPreKeyType +): Promise { + const updated: StoredSignedPreKeyType = specFromBytes(PRE_KEY_SPEC, data); await channels.createOrUpdateSignedPreKey(updated); } -async function getSignedPreKeyById(id: SignedPreKeyIdType) { +async function getSignedPreKeyById( + id: SignedPreKeyIdType +): Promise { const data = await channels.getSignedPreKeyById(id); - return keysToBytes(PRE_KEY_KEYS, data); + return specToBytes(PRE_KEY_SPEC, data); } -async function getAllSignedPreKeys() { +async function getAllSignedPreKeys(): Promise> { const keys = await channels.getAllSignedPreKeys(); - return keys.map((key: SignedPreKeyType) => keysToBytes(PRE_KEY_KEYS, key)); + return keys.map(key => specToBytes(PRE_KEY_SPEC, key)); } -async function bulkAddSignedPreKeys(array: Array) { - const updated = map(array, data => keysFromBytes(PRE_KEY_KEYS, data)); +async function bulkAddSignedPreKeys( + array: Array +): Promise { + const updated: Array = map(array, data => + specFromBytes(PRE_KEY_SPEC, data) + ); await channels.bulkAddSignedPreKeys(updated); } -async function removeSignedPreKeyById(id: SignedPreKeyIdType) { +async function removeSignedPreKeyById(id: SignedPreKeyIdType): Promise { await channels.removeSignedPreKeyById(id); } -async function removeAllSignedPreKeys() { +async function removeSignedPreKeysByUuid(uuid: UUIDStringType): Promise { + await channels.removeSignedPreKeysByUuid(uuid); +} +async function removeAllSignedPreKeys(): Promise { await channels.removeAllSignedPreKeys(); } // Items -const ITEM_KEYS: Partial>> = { +const ITEM_SPECS: Partial> = { senderCertificate: ['value.serialized'], senderCertificateNoE164: ['value.serialized'], subscriberId: ['value'], profileKey: ['value'], + identityKeyMap: { + key: 'value', + valueSpec: { + isMap: true, + valueSpec: ['privKey', 'pubKey'], + }, + }, }; -async function createOrUpdateItem(data: ItemType) { +async function createOrUpdateItem( + data: ItemType +): Promise { const { id } = data; if (!id) { throw new Error( @@ -816,20 +846,22 @@ async function createOrUpdateItem(data: ItemType) { ); } - const keys = ITEM_KEYS[id]; - const updated = Array.isArray(keys) ? keysFromBytes(keys, data) : data; + const spec = ITEM_SPECS[id]; + const updated: StoredItemType = spec + ? specFromBytes(spec, data) + : (data as unknown as StoredItemType); await channels.createOrUpdateItem(updated); } async function getItemById( id: K ): Promise | undefined> { - const keys = ITEM_KEYS[id]; + const spec = ITEM_SPECS[id]; const data = await channels.getItemById(id); - return Array.isArray(keys) ? keysToBytes(keys, data) : data; + return spec ? specToBytes(spec, data) : (data as unknown as ItemType); } -async function getAllItems() { +async function getAllItems(): Promise { const items = await channels.getAllItems(); const result = Object.create(null); @@ -838,10 +870,10 @@ async function getAllItems() { const key = id as ItemKeyType; const value = items[key]; - const keys = ITEM_KEYS[key]; + const keys = ITEM_SPECS[key]; - const deserializedValue = Array.isArray(keys) - ? keysToBytes(keys, { value }).value + const deserializedValue = keys + ? (specToBytes(keys, { value }) as ItemType).value : value; result[key] = deserializedValue; @@ -849,10 +881,10 @@ async function getAllItems() { return result; } -async function removeItemById(id: ItemKeyType) { +async function removeItemById(id: ItemKeyType): Promise { await channels.removeItemById(id); } -async function removeAllItems() { +async function removeAllItems(): Promise { await channels.removeAllItems(); } @@ -938,33 +970,37 @@ async function _getAllSentProtoMessageIds(): Promise> { // Sessions -async function createOrUpdateSession(data: SessionType) { +async function createOrUpdateSession(data: SessionType): Promise { await channels.createOrUpdateSession(data); } -async function createOrUpdateSessions(array: Array) { +async function createOrUpdateSessions( + array: Array +): Promise { await channels.createOrUpdateSessions(array); } async function commitDecryptResult(options: { senderKeys: Array; sessions: Array; unprocessed: Array; -}) { +}): Promise { await channels.commitDecryptResult(options); } -async function bulkAddSessions(array: Array) { +async function bulkAddSessions(array: Array): Promise { await channels.bulkAddSessions(array); } -async function removeSessionById(id: SessionIdType) { +async function removeSessionById(id: SessionIdType): Promise { await channels.removeSessionById(id); } -async function removeSessionsByConversation(conversationId: string) { +async function removeSessionsByConversation( + conversationId: string +): Promise { await channels.removeSessionsByConversation(conversationId); } -async function removeAllSessions() { +async function removeAllSessions(): Promise { await channels.removeAllSessions(); } -async function getAllSessions() { +async function getAllSessions(): Promise> { const sessions = await channels.getAllSessions(); return sessions; @@ -972,19 +1008,23 @@ async function getAllSessions() { // Conversation -async function getConversationCount() { +async function getConversationCount(): Promise { return channels.getConversationCount(); } -async function saveConversation(data: ConversationType) { +async function saveConversation(data: ConversationType): Promise { await channels.saveConversation(data); } -async function saveConversations(array: Array) { +async function saveConversations( + array: Array +): Promise { await channels.saveConversations(array); } -async function getConversationById(id: string) { +async function getConversationById( + id: string +): Promise { return channels.getConversationById(id); } @@ -1006,11 +1046,13 @@ const updateConversationBatcher = createBatcher({ }, }); -function updateConversation(data: ConversationType) { +function updateConversation(data: ConversationType): void { updateConversationBatcher.add(data); } -async function updateConversations(array: Array) { +async function updateConversations( + array: Array +): Promise { const { cleaned, pathsChanged } = cleanDataForIpc(array); assert( !pathsChanged.length, @@ -1019,7 +1061,7 @@ async function updateConversations(array: Array) { await channels.updateConversations(cleaned); } -async function removeConversation(id: string) { +async function removeConversation(id: string): Promise { const existing = await getConversationById(id); // Note: It's important to have a fully database-hydrated model to delete here because @@ -1032,21 +1074,23 @@ async function removeConversation(id: string) { } } -async function eraseStorageServiceStateFromConversations() { +async function eraseStorageServiceStateFromConversations(): Promise { await channels.eraseStorageServiceStateFromConversations(); } -async function getAllConversations() { +async function getAllConversations(): Promise> { return channels.getAllConversations(); } -async function getAllConversationIds() { +async function getAllConversationIds(): Promise> { const ids = await channels.getAllConversationIds(); return ids; } -async function getAllGroupsInvolvingUuid(uuid: UUIDStringType) { +async function getAllGroupsInvolvingUuid( + uuid: UUIDStringType +): Promise> { return channels.getAllGroupsInvolvingUuid(uuid); } @@ -1067,7 +1111,7 @@ function handleSearchMessageJSON( async function searchMessages( query: string, { limit }: { limit?: number } = {} -) { +): Promise> { const messages = await channels.searchMessages(query, { limit }); return handleSearchMessageJSON(messages); @@ -1077,7 +1121,7 @@ async function searchMessagesInConversation( query: string, conversationId: string, { limit }: { limit?: number } = {} -) { +): Promise> { const messages = await channels.searchMessagesInConversation( query, conversationId, @@ -1089,11 +1133,11 @@ async function searchMessagesInConversation( // Message -async function getMessageCount(conversationId?: string) { +async function getMessageCount(conversationId?: string): Promise { return channels.getMessageCount(conversationId); } -async function getStoryCount(conversationId: string) { +async function getStoryCount(conversationId: string): Promise { return channels.getStoryCount(conversationId); } @@ -1104,7 +1148,7 @@ async function saveMessage( forceSave?: boolean; ourUuid: UUIDStringType; } -) { +): Promise { const id = await channels.saveMessage(_cleanMessageData(data), { ...options, jobToInsert: options.jobToInsert && formatJobForInsert(options.jobToInsert), @@ -1119,7 +1163,7 @@ async function saveMessage( async function saveMessages( arrayOfMessages: ReadonlyArray, options: { forceSave?: boolean; ourUuid: UUIDStringType } -) { +): Promise { await channels.saveMessages( arrayOfMessages.map(message => _cleanMessageData(message)), options @@ -1129,7 +1173,7 @@ async function saveMessages( tapToViewMessagesDeletionService.update(); } -async function removeMessage(id: string) { +async function removeMessage(id: string): Promise { const message = await getMessageById(id); // Note: It's important to have a fully database-hydrated model to delete here because @@ -1141,15 +1185,17 @@ async function removeMessage(id: string) { } // Note: this method will not clean up external files, just delete from SQL -async function removeMessages(ids: Array) { +async function removeMessages(ids: Array): Promise { await channels.removeMessages(ids); } -async function getMessageById(id: string) { +async function getMessageById(id: string): Promise { return channels.getMessageById(id); } -async function getMessagesById(messageIds: Array) { +async function getMessagesById( + messageIds: Array +): Promise> { if (!messageIds.length) { return []; } @@ -1157,14 +1203,14 @@ async function getMessagesById(messageIds: Array) { } // For testing only -async function _getAllMessages() { +async function _getAllMessages(): Promise> { return channels._getAllMessages(); } -async function _removeAllMessages() { +async function _removeAllMessages(): Promise { await channels._removeAllMessages(); } -async function getAllMessageIds() { +async function getAllMessageIds(): Promise> { const ids = await channels.getAllMessageIds(); return ids; @@ -1180,7 +1226,7 @@ async function getMessageBySender({ sourceUuid: UUIDStringType; sourceDevice: number; sent_at: number; -}) { +}): Promise { return channels.getMessageBySender({ source, sourceUuid, @@ -1195,7 +1241,7 @@ async function getTotalUnreadForConversation( storyId: UUIDStringType | undefined; isGroup: boolean; } -) { +): Promise { return channels.getTotalUnreadForConversation(conversationId, options); } @@ -1205,7 +1251,7 @@ async function getUnreadByConversationAndMarkRead(options: { newestUnreadAt: number; readAt?: number; storyId?: UUIDStringType; -}) { +}): Promise { return channels.getUnreadByConversationAndMarkRead(options); } @@ -1213,14 +1259,14 @@ async function getUnreadReactionsAndMarkRead(options: { conversationId: string; newestUnreadAt: number; storyId?: UUIDStringType; -}) { +}): Promise> { return channels.getUnreadReactionsAndMarkRead(options); } async function markReactionAsRead( targetAuthorUuid: string, targetTimestamp: number -) { +): Promise { return channels.markReactionAsRead(targetAuthorUuid, targetTimestamp); } @@ -1229,18 +1275,18 @@ async function removeReactionFromConversation(reaction: { fromId: string; targetAuthorUuid: string; targetTimestamp: number; -}) { +}): Promise { return channels.removeReactionFromConversation(reaction); } -async function addReaction(reactionObj: ReactionType) { +async function addReaction(reactionObj: ReactionType): Promise { return channels.addReaction(reactionObj); } -async function _getAllReactions() { +async function _getAllReactions(): Promise> { return channels._getAllReactions(); } -async function _removeAllReactions() { +async function _removeAllReactions(): Promise { await channels._removeAllReactions(); } @@ -1267,7 +1313,7 @@ async function getOlderMessagesByConversation( sentAt?: number; storyId: string | undefined; } -) { +): Promise> { const messages = await channels.getOlderMessagesByConversation( conversationId, { @@ -1307,7 +1353,7 @@ async function getNewerMessagesByConversation( sentAt?: number; storyId: UUIDStringType | undefined; } -) { +): Promise> { const messages = await channels.getNewerMessagesByConversation( conversationId, { @@ -1347,14 +1393,14 @@ async function getLastConversationMessage({ conversationId, }: { conversationId: string; -}) { +}): Promise { return channels.getLastConversationMessage({ conversationId }); } async function getMessageMetricsForConversation( conversationId: string, storyId?: UUIDStringType, isGroup?: boolean -) { +): Promise { const result = await channels.getMessageMetricsForConversation( conversationId, storyId, @@ -1371,7 +1417,7 @@ async function getConversationRangeCenteredOnMessage(options: { receivedAt: number; sentAt?: number; storyId: UUIDStringType | undefined; -}) { +}): Promise> { const result = await channels.getConversationRangeCenteredOnMessage(options); return { @@ -1390,7 +1436,7 @@ function hasGroupCallHistoryMessage( async function migrateConversationMessages( obsoleteId: string, currentId: string -) { +): Promise { await channels.migrateConversationMessages(obsoleteId, currentId); } @@ -1401,7 +1447,7 @@ async function removeAllMessagesInConversation( }: { logId: string; } -) { +): Promise { let messages; do { const chunkSize = 20; @@ -1438,151 +1484,174 @@ async function removeAllMessagesInConversation( } while (messages.length > 0); } -async function getMessagesBySentAt(sentAt: number) { +async function getMessagesBySentAt( + sentAt: number +): Promise> { return channels.getMessagesBySentAt(sentAt); } -async function getExpiredMessages() { +async function getExpiredMessages(): Promise> { return channels.getExpiredMessages(); } -function getMessagesUnexpectedlyMissingExpirationStartTimestamp() { +function getMessagesUnexpectedlyMissingExpirationStartTimestamp(): Promise< + Array +> { return channels.getMessagesUnexpectedlyMissingExpirationStartTimestamp(); } -function getSoonestMessageExpiry() { +function getSoonestMessageExpiry(): Promise { return channels.getSoonestMessageExpiry(); } -async function getNextTapToViewMessageTimestampToAgeOut() { +async function getNextTapToViewMessageTimestampToAgeOut(): Promise< + number | undefined +> { return channels.getNextTapToViewMessageTimestampToAgeOut(); } -async function getTapToViewMessagesNeedingErase() { +async function getTapToViewMessagesNeedingErase(): Promise> { return channels.getTapToViewMessagesNeedingErase(); } // Unprocessed -async function getUnprocessedCount() { +async function getUnprocessedCount(): Promise { return channels.getUnprocessedCount(); } -async function getAllUnprocessedAndIncrementAttempts() { +async function getAllUnprocessedAndIncrementAttempts(): Promise< + Array +> { return channels.getAllUnprocessedAndIncrementAttempts(); } -async function getUnprocessedById(id: string) { +async function getUnprocessedById( + id: string +): Promise { return channels.getUnprocessedById(id); } async function updateUnprocessedWithData( id: string, data: UnprocessedUpdateType -) { +): Promise { await channels.updateUnprocessedWithData(id, data); } async function updateUnprocessedsWithData( array: Array<{ id: string; data: UnprocessedUpdateType }> -) { +): Promise { await channels.updateUnprocessedsWithData(array); } -async function removeUnprocessed(id: string | Array) { +async function removeUnprocessed(id: string | Array): Promise { await channels.removeUnprocessed(id); } -async function removeAllUnprocessed() { +async function removeAllUnprocessed(): Promise { await channels.removeAllUnprocessed(); } // Attachment downloads -async function getAttachmentDownloadJobById(id: string) { +async function getAttachmentDownloadJobById( + id: string +): Promise { return channels.getAttachmentDownloadJobById(id); } async function getNextAttachmentDownloadJobs( limit?: number, options?: { timestamp?: number } -) { +): Promise> { return channels.getNextAttachmentDownloadJobs(limit, options); } -async function saveAttachmentDownloadJob(job: AttachmentDownloadJobType) { +async function saveAttachmentDownloadJob( + job: AttachmentDownloadJobType +): Promise { await channels.saveAttachmentDownloadJob(_cleanData(job)); } -async function setAttachmentDownloadJobPending(id: string, pending: boolean) { +async function setAttachmentDownloadJobPending( + id: string, + pending: boolean +): Promise { await channels.setAttachmentDownloadJobPending(id, pending); } -async function resetAttachmentDownloadPending() { +async function resetAttachmentDownloadPending(): Promise { await channels.resetAttachmentDownloadPending(); } -async function removeAttachmentDownloadJob(id: string) { +async function removeAttachmentDownloadJob(id: string): Promise { await channels.removeAttachmentDownloadJob(id); } -async function removeAllAttachmentDownloadJobs() { +async function removeAllAttachmentDownloadJobs(): Promise { await channels.removeAllAttachmentDownloadJobs(); } // Stickers -async function getStickerCount() { +async function getStickerCount(): Promise { return channels.getStickerCount(); } -async function createOrUpdateStickerPack(pack: StickerPackType) { +async function createOrUpdateStickerPack(pack: StickerPackType): Promise { await channels.createOrUpdateStickerPack(pack); } async function updateStickerPackStatus( packId: string, status: StickerPackStatusType, options?: { timestamp: number } -) { +): Promise { await channels.updateStickerPackStatus(packId, status, options); } -async function createOrUpdateSticker(sticker: StickerType) { +async function createOrUpdateSticker(sticker: StickerType): Promise { await channels.createOrUpdateSticker(sticker); } async function updateStickerLastUsed( packId: string, stickerId: number, timestamp: number -) { +): Promise { await channels.updateStickerLastUsed(packId, stickerId, timestamp); } -async function addStickerPackReference(messageId: string, packId: string) { +async function addStickerPackReference( + messageId: string, + packId: string +): Promise { await channels.addStickerPackReference(messageId, packId); } -async function deleteStickerPackReference(messageId: string, packId: string) { +async function deleteStickerPackReference( + messageId: string, + packId: string +): Promise | undefined> { return channels.deleteStickerPackReference(messageId, packId); } -async function deleteStickerPack(packId: string) { +async function deleteStickerPack(packId: string): Promise> { const paths = await channels.deleteStickerPack(packId); return paths; } -async function getAllStickerPacks() { +async function getAllStickerPacks(): Promise> { const packs = await channels.getAllStickerPacks(); return packs; } -async function getAllStickers() { +async function getAllStickers(): Promise> { const stickers = await channels.getAllStickers(); return stickers; } -async function getRecentStickers() { +async function getRecentStickers(): Promise> { const recentStickers = await channels.getRecentStickers(); return recentStickers; } -async function clearAllErrorStickerPackAttempts() { +async function clearAllErrorStickerPackAttempts(): Promise { await channels.clearAllErrorStickerPackAttempts(); } // Emojis -async function updateEmojiUsage(shortName: string) { +async function updateEmojiUsage(shortName: string): Promise { await channels.updateEmojiUsage(shortName); } -async function getRecentEmojis(limit = 32) { +async function getRecentEmojis(limit = 32): Promise> { return channels.getRecentEmojis(limit); } @@ -1690,24 +1759,26 @@ async function countStoryReadsByConversation( // Other -async function removeAll() { +async function removeAll(): Promise { await channels.removeAll(); } -async function removeAllConfiguration(type?: RemoveAllConfiguration) { +async function removeAllConfiguration( + type?: RemoveAllConfiguration +): Promise { await channels.removeAllConfiguration(type); } -async function cleanupOrphanedAttachments() { +async function cleanupOrphanedAttachments(): Promise { await callChannel(CLEANUP_ORPHANED_ATTACHMENTS_KEY); } -async function ensureFilePermissions() { +async function ensureFilePermissions(): Promise { await callChannel(ENSURE_FILE_PERMISSIONS); } // Note: will need to restart the app after calling this, to set up afresh -async function removeOtherData() { +async function removeOtherData(): Promise { await Promise.all([ callChannel(ERASE_SQL_KEY), callChannel(ERASE_ATTACHMENTS_KEY), @@ -1717,7 +1788,7 @@ async function removeOtherData() { ]); } -async function callChannel(name: string) { +async function callChannel(name: string): Promise { return createTaskWithTimeout( () => new Promise((resolve, reject) => { @@ -1739,7 +1810,7 @@ async function callChannel(name: string) { async function getMessagesNeedingUpgrade( limit: number, { maxVersion = CURRENT_SCHEMA_VERSION }: { maxVersion: number } -) { +): Promise> { const messages = await channels.getMessagesNeedingUpgrade(limit, { maxVersion, }); @@ -1750,7 +1821,7 @@ async function getMessagesNeedingUpgrade( async function getMessagesWithVisualMediaAttachments( conversationId: string, { limit }: { limit: number } -) { +): Promise> { return channels.getMessagesWithVisualMediaAttachments(conversationId, { limit, }); @@ -1759,7 +1830,7 @@ async function getMessagesWithVisualMediaAttachments( async function getMessagesWithFileAttachments( conversationId: string, { limit }: { limit: number } -) { +): Promise> { return channels.getMessagesWithFileAttachments(conversationId, { limit, }); diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index caa9ae67d..a8f2ad460 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -1,9 +1,7 @@ // Copyright 2020-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -/* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable camelcase */ -/* eslint-disable @typescript-eslint/no-explicit-any */ import type { ConversationAttributesType, MessageAttributesType, @@ -15,7 +13,7 @@ import type { ConversationColorType, CustomColorType } from '../types/Colors'; import type { ProcessGroupCallRingRequestResult } from '../types/Calling'; import type { StorageAccessType } from '../types/Storage.d'; import type { AttachmentType } from '../types/Attachment'; -import type { BodyRangesType } from '../types/Util'; +import type { BodyRangesType, BytesToStrings } from '../types/Util'; import type { QualifiedAddressStringType } from '../types/QualifiedAddress'; import type { UUIDStringType } from '../types/UUID'; import type { BadgeType } from '../badges/types'; @@ -66,14 +64,27 @@ export type IdentityKeyType = { timestamp: number; verified: number; }; +export type StoredIdentityKeyType = { + firstUse: boolean; + id: UUIDStringType | `conversation:${string}`; + nonblockingApproval: boolean; + publicKey: string; + timestamp: number; + verified: number; +}; export type IdentityKeyIdType = IdentityKeyType['id']; export type ItemKeyType = keyof StorageAccessType; export type AllItemsType = Partial; +export type StoredAllItemsType = Partial>; export type ItemType = { id: K; value: StorageAccessType[K]; }; +export type StoredItemType = { + id: K; + value: BytesToStrings; +}; export type MessageType = MessageAttributesType; export type MessageTypeUnhydrated = { json: string; @@ -85,6 +96,13 @@ export type PreKeyType = { privateKey: Uint8Array; publicKey: Uint8Array; }; +export type StoredPreKeyType = { + id: `${UUIDStringType}:${number}`; + keyId: number; + ourUuid: UUIDStringType; + privateKey: string; + publicKey: string; +}; export type PreKeyIdType = PreKeyType['id']; export type ServerSearchResultMessageType = { json: string; @@ -149,6 +167,15 @@ export type SignedPreKeyType = { privateKey: Uint8Array; publicKey: Uint8Array; }; +export type StoredSignedPreKeyType = { + confirmed: boolean; + created_at: number; + ourUuid: UUIDStringType; + id: `${UUIDStringType}:${number}`; + keyId: number; + privateKey: string; + publicKey: string; +}; export type SignedPreKeyIdType = SignedPreKeyType['id']; export type StickerType = Readonly<{ @@ -205,6 +232,7 @@ export type UnprocessedType = { sourceUuid?: UUIDStringType; sourceDevice?: number; destinationUuid?: string; + updatedPni?: string; serverGuid?: string; serverTimestamp?: number; decrypted?: string; @@ -262,41 +290,49 @@ export type StoryReadType = Readonly<{ storyReadDate: number; }>; +export type ReactionResultType = Pick< + ReactionType, + 'targetAuthorUuid' | 'targetTimestamp' | 'messageId' +> & { rowid: number }; + +export type GetUnreadByConversationAndMarkReadResultType = Array< + { originalReadStatus: ReadStatus | undefined } & Pick< + MessageType, + | 'id' + | 'source' + | 'sourceUuid' + | 'sent_at' + | 'type' + | 'readStatus' + | 'seenStatus' + > +>; + +export type GetConversationRangeCenteredOnMessageResultType = + Readonly<{ + older: Array; + newer: Array; + metrics: ConversationMetricsType; + }>; + export type DataInterface = { close: () => Promise; removeDB: () => Promise; removeIndexedDBFiles: () => Promise; - createOrUpdateIdentityKey: (data: IdentityKeyType) => Promise; - getIdentityKeyById: ( - id: IdentityKeyIdType - ) => Promise; - bulkAddIdentityKeys: (array: Array) => Promise; removeIdentityKeyById: (id: IdentityKeyIdType) => Promise; removeAllIdentityKeys: () => Promise; - getAllIdentityKeys: () => Promise>; - createOrUpdatePreKey: (data: PreKeyType) => Promise; - getPreKeyById: (id: PreKeyIdType) => Promise; - bulkAddPreKeys: (array: Array) => Promise; removePreKeyById: (id: PreKeyIdType) => Promise; + removePreKeysByUuid: (uuid: UUIDStringType) => Promise; removeAllPreKeys: () => Promise; - getAllPreKeys: () => Promise>; - createOrUpdateSignedPreKey: (data: SignedPreKeyType) => Promise; - getSignedPreKeyById: ( - id: SignedPreKeyIdType - ) => Promise; - bulkAddSignedPreKeys: (array: Array) => Promise; removeSignedPreKeyById: (id: SignedPreKeyIdType) => Promise; + removeSignedPreKeysByUuid: (uuid: UUIDStringType) => Promise; removeAllSignedPreKeys: () => Promise; - getAllSignedPreKeys: () => Promise>; - createOrUpdateItem(data: ItemType): Promise; - getItemById(id: K): Promise | undefined>; - removeItemById: (id: ItemKeyType) => Promise; removeAllItems: () => Promise; - getAllItems: () => Promise; + removeItemById: (id: ItemKeyType) => Promise; createOrUpdateSenderKey: (key: SenderKeyType) => Promise; getSenderKeyById: (id: SenderKeyIdType) => Promise; @@ -402,29 +438,12 @@ export type DataInterface = { newestUnreadAt: number; readAt?: number; storyId?: UUIDStringType; - }) => Promise< - Array< - { originalReadStatus: ReadStatus | undefined } & Pick< - MessageType, - | 'id' - | 'readStatus' - | 'seenStatus' - | 'sent_at' - | 'source' - | 'sourceUuid' - | 'type' - > - > - >; + }) => Promise; getUnreadReactionsAndMarkRead: (options: { conversationId: string; newestUnreadAt: number; storyId?: UUIDStringType; - }) => Promise< - Array< - Pick - > - >; + }) => Promise>; markReactionAsRead: ( targetAuthorUuid: string, targetTimestamp: number @@ -671,11 +690,36 @@ export type ServerInterface = DataInterface & { receivedAt: number; sentAt?: number; storyId: UUIDStringType | undefined; - }) => Promise<{ - older: Array; - newer: Array; - metrics: ConversationMetricsType; - }>; + }) => Promise< + GetConversationRangeCenteredOnMessageResultType + >; + + createOrUpdateIdentityKey: (data: StoredIdentityKeyType) => Promise; + getIdentityKeyById: ( + id: IdentityKeyIdType + ) => Promise; + bulkAddIdentityKeys: (array: Array) => Promise; + getAllIdentityKeys: () => Promise>; + + createOrUpdatePreKey: (data: StoredPreKeyType) => Promise; + getPreKeyById: (id: PreKeyIdType) => Promise; + bulkAddPreKeys: (array: Array) => Promise; + getAllPreKeys: () => Promise>; + + createOrUpdateSignedPreKey: (data: StoredSignedPreKeyType) => Promise; + getSignedPreKeyById: ( + id: SignedPreKeyIdType + ) => Promise; + bulkAddSignedPreKeys: (array: Array) => Promise; + getAllSignedPreKeys: () => Promise>; + + createOrUpdateItem( + data: StoredItemType + ): Promise; + getItemById( + id: K + ): Promise | undefined>; + getAllItems: () => Promise; // Server-only @@ -744,11 +788,30 @@ export type ClientInterface = DataInterface & { receivedAt: number; sentAt?: number; storyId: UUIDStringType | undefined; - }) => Promise<{ - older: Array; - newer: Array; - metrics: ConversationMetricsType; - }>; + }) => Promise>; + + createOrUpdateIdentityKey: (data: IdentityKeyType) => Promise; + getIdentityKeyById: ( + id: IdentityKeyIdType + ) => Promise; + bulkAddIdentityKeys: (array: Array) => Promise; + getAllIdentityKeys: () => Promise>; + + createOrUpdatePreKey: (data: PreKeyType) => Promise; + getPreKeyById: (id: PreKeyIdType) => Promise; + bulkAddPreKeys: (array: Array) => Promise; + getAllPreKeys: () => Promise>; + + createOrUpdateSignedPreKey: (data: SignedPreKeyType) => Promise; + getSignedPreKeyById: ( + id: SignedPreKeyIdType + ) => Promise; + bulkAddSignedPreKeys: (array: Array) => Promise; + getAllSignedPreKeys: () => Promise>; + + createOrUpdateItem(data: ItemType): Promise; + getItemById(id: K): Promise | undefined>; + getAllItems: () => Promise; // Client-side only @@ -774,10 +837,10 @@ export type ClientInterface = DataInterface & { export type ClientJobType = { fnName: string; start: number; - resolve?: Function; - reject?: Function; + resolve?: (value: unknown) => void; + reject?: (error: Error) => void; // Only in DEBUG mode complete?: boolean; - args?: Array; + args?: ReadonlyArray; }; diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 59f157bea..85ab7eba9 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -71,22 +71,25 @@ import { import { updateSchema } from './migrations'; import type { - AllItemsType, + StoredAllItemsType, AttachmentDownloadJobType, ConversationMetricsType, ConversationType, DeleteSentProtoRecipientOptionsType, EmojiType, + GetConversationRangeCenteredOnMessageResultType, + GetUnreadByConversationAndMarkReadResultType, IdentityKeyIdType, - IdentityKeyType, + StoredIdentityKeyType, ItemKeyType, - ItemType, + StoredItemType, ConversationMessageStatsType, MessageMetricsType, MessageType, MessageTypeUnhydrated, PreKeyIdType, - PreKeyType, + ReactionResultType, + StoredPreKeyType, ServerSearchResultMessageType, SenderKeyIdType, SenderKeyType, @@ -100,7 +103,7 @@ import type { SessionIdType, SessionType, SignedPreKeyIdType, - SignedPreKeyType, + StoredSignedPreKeyType, StickerPackStatusType, StickerPackType, StickerType, @@ -149,6 +152,7 @@ const dataInterface: ServerInterface = { getPreKeyById, bulkAddPreKeys, removePreKeyById, + removePreKeysByUuid, removeAllPreKeys, getAllPreKeys, @@ -156,6 +160,7 @@ const dataInterface: ServerInterface = { getSignedPreKeyById, bulkAddSignedPreKeys, removeSignedPreKeyById, + removeSignedPreKeysByUuid, removeAllSignedPreKeys, getAllSignedPreKeys, @@ -634,16 +639,18 @@ function getInstance(): Database { } const IDENTITY_KEYS_TABLE = 'identityKeys'; -async function createOrUpdateIdentityKey(data: IdentityKeyType): Promise { +async function createOrUpdateIdentityKey( + data: StoredIdentityKeyType +): Promise { return createOrUpdate(getInstance(), IDENTITY_KEYS_TABLE, data); } async function getIdentityKeyById( id: IdentityKeyIdType -): Promise { +): Promise { return getById(getInstance(), IDENTITY_KEYS_TABLE, id); } async function bulkAddIdentityKeys( - array: Array + array: Array ): Promise { return bulkAdd(getInstance(), IDENTITY_KEYS_TABLE, array); } @@ -653,55 +660,67 @@ async function removeIdentityKeyById(id: IdentityKeyIdType): Promise { async function removeAllIdentityKeys(): Promise { return removeAllFromTable(getInstance(), IDENTITY_KEYS_TABLE); } -async function getAllIdentityKeys(): Promise> { +async function getAllIdentityKeys(): Promise> { return getAllFromTable(getInstance(), IDENTITY_KEYS_TABLE); } const PRE_KEYS_TABLE = 'preKeys'; -async function createOrUpdatePreKey(data: PreKeyType): Promise { +async function createOrUpdatePreKey(data: StoredPreKeyType): Promise { return createOrUpdate(getInstance(), PRE_KEYS_TABLE, data); } async function getPreKeyById( id: PreKeyIdType -): Promise { +): Promise { return getById(getInstance(), PRE_KEYS_TABLE, id); } -async function bulkAddPreKeys(array: Array): Promise { +async function bulkAddPreKeys(array: Array): Promise { return bulkAdd(getInstance(), PRE_KEYS_TABLE, array); } async function removePreKeyById(id: PreKeyIdType): Promise { return removeById(getInstance(), PRE_KEYS_TABLE, id); } +async function removePreKeysByUuid(uuid: UUIDStringType): Promise { + const db = getInstance(); + db.prepare('DELETE FROM preKeys WHERE ourUuid IS $uuid;').run({ + uuid, + }); +} async function removeAllPreKeys(): Promise { return removeAllFromTable(getInstance(), PRE_KEYS_TABLE); } -async function getAllPreKeys(): Promise> { +async function getAllPreKeys(): Promise> { return getAllFromTable(getInstance(), PRE_KEYS_TABLE); } const SIGNED_PRE_KEYS_TABLE = 'signedPreKeys'; async function createOrUpdateSignedPreKey( - data: SignedPreKeyType + data: StoredSignedPreKeyType ): Promise { return createOrUpdate(getInstance(), SIGNED_PRE_KEYS_TABLE, data); } async function getSignedPreKeyById( id: SignedPreKeyIdType -): Promise { +): Promise { return getById(getInstance(), SIGNED_PRE_KEYS_TABLE, id); } async function bulkAddSignedPreKeys( - array: Array + array: Array ): Promise { return bulkAdd(getInstance(), SIGNED_PRE_KEYS_TABLE, array); } async function removeSignedPreKeyById(id: SignedPreKeyIdType): Promise { return removeById(getInstance(), SIGNED_PRE_KEYS_TABLE, id); } +async function removeSignedPreKeysByUuid(uuid: UUIDStringType): Promise { + const db = getInstance(); + db.prepare('DELETE FROM signedPreKeys WHERE ourUuid IS $uuid;').run({ + uuid, + }); +} async function removeAllSignedPreKeys(): Promise { return removeAllFromTable(getInstance(), SIGNED_PRE_KEYS_TABLE); } -async function getAllSignedPreKeys(): Promise> { +async function getAllSignedPreKeys(): Promise> { const db = getInstance(); const rows: JSONRows = db .prepare( @@ -718,16 +737,16 @@ async function getAllSignedPreKeys(): Promise> { const ITEMS_TABLE = 'items'; async function createOrUpdateItem( - data: ItemType + data: StoredItemType ): Promise { return createOrUpdate(getInstance(), ITEMS_TABLE, data); } async function getItemById( id: K -): Promise | undefined> { +): Promise | undefined> { return getById(getInstance(), ITEMS_TABLE, id); } -async function getAllItems(): Promise { +async function getAllItems(): Promise { const db = getInstance(); const rows: JSONRows = db .prepare('SELECT json FROM items ORDER BY id ASC;') @@ -743,7 +762,7 @@ async function getAllItems(): Promise { result[id] = value; } - return result as unknown as AllItemsType; + return result as unknown as StoredAllItemsType; } async function removeItemById(id: ItemKeyType): Promise { return removeById(getInstance(), ITEMS_TABLE, id); @@ -2097,20 +2116,7 @@ async function getUnreadByConversationAndMarkRead({ newestUnreadAt: number; storyId?: UUIDStringType; readAt?: number; -}): Promise< - Array< - { originalReadStatus: ReadStatus | undefined } & Pick< - MessageType, - | 'id' - | 'source' - | 'sourceUuid' - | 'sent_at' - | 'type' - | 'readStatus' - | 'seenStatus' - > - > -> { +}): Promise { const db = getInstance(); return db.transaction(() => { const expirationStartTimestamp = Math.min(Date.now(), readAt ?? Infinity); @@ -2203,10 +2209,6 @@ async function getUnreadByConversationAndMarkRead({ })(); } -type ReactionResultType = Pick< - ReactionType, - 'targetAuthorUuid' | 'targetTimestamp' | 'messageId' -> & { rowid: number }; async function getUnreadReactionsAndMarkRead({ conversationId, newestUnreadAt, @@ -2869,11 +2871,9 @@ async function getConversationRangeCenteredOnMessage({ receivedAt: number; sentAt?: number; storyId: UUIDStringType | undefined; -}): Promise<{ - older: Array; - newer: Array; - metrics: ConversationMetricsType; -}> { +}): Promise< + GetConversationRangeCenteredOnMessageResultType +> { const db = getInstance(); return db.transaction(() => { diff --git a/ts/sql/migrations/64-uuid-column-for-pre-keys.ts b/ts/sql/migrations/64-uuid-column-for-pre-keys.ts new file mode 100644 index 000000000..cd76c0cbf --- /dev/null +++ b/ts/sql/migrations/64-uuid-column-for-pre-keys.ts @@ -0,0 +1,38 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { Database } from 'better-sqlite3'; + +import type { LoggerType } from '../../types/Logging'; + +export default function updateToSchemaVersion64( + currentVersion: number, + db: Database, + logger: LoggerType +): void { + if (currentVersion >= 64) { + return; + } + + db.transaction(() => { + db.exec( + ` + ALTER TABLE preKeys + ADD COLUMN ourUuid STRING + GENERATED ALWAYS AS (json_extract(json, '$.ourUuid')); + + CREATE INDEX preKeys_ourUuid ON preKeys (ourUuid); + + ALTER TABLE signedPreKeys + ADD COLUMN ourUuid STRING + GENERATED ALWAYS AS (json_extract(json, '$.ourUuid')); + + CREATE INDEX signedPreKeys_ourUuid ON signedPreKeys (ourUuid); + ` + ); + + db.pragma('user_version = 64'); + })(); + + logger.info('updateToSchemaVersion64: success!'); +} diff --git a/ts/sql/migrations/index.ts b/ts/sql/migrations/index.ts index 25eeb7c11..0e6e5c7c0 100644 --- a/ts/sql/migrations/index.ts +++ b/ts/sql/migrations/index.ts @@ -39,6 +39,7 @@ import updateToSchemaVersion60 from './60-update-expiring-index'; import updateToSchemaVersion61 from './61-distribution-list-storage'; import updateToSchemaVersion62 from './62-add-urgent-to-send-log'; import updateToSchemaVersion63 from './63-add-urgent-to-unprocessed'; +import updateToSchemaVersion64 from './64-uuid-column-for-pre-keys'; function updateToSchemaVersion1( currentVersion: number, @@ -1941,6 +1942,7 @@ export const SCHEMA_VERSIONS = [ updateToSchemaVersion61, updateToSchemaVersion62, updateToSchemaVersion63, + updateToSchemaVersion64, ]; export function updateSchema(db: Database, logger: LoggerType): void { diff --git a/ts/test-electron/SignalProtocolStore_test.ts b/ts/test-electron/SignalProtocolStore_test.ts index 8dabdb88f..d1e3f7d84 100644 --- a/ts/test-electron/SignalProtocolStore_test.ts +++ b/ts/test-electron/SignalProtocolStore_test.ts @@ -7,8 +7,12 @@ import chai, { assert } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { Direction, + IdentityKeyPair, + PrivateKey, + PublicKey, SenderKeyRecord, SessionRecord, + SignedPreKeyRecord, } from '@signalapp/libsignal-client'; import { signal } from '../protobuf/compiled'; @@ -18,7 +22,11 @@ import { Zone } from '../util/Zone'; import * as Bytes from '../Bytes'; import { getRandomBytes, constantTimeEqual } from '../Crypto'; -import { clampPrivateKey, setPublicKeyTypeByte } from '../Curve'; +import { + clampPrivateKey, + setPublicKeyTypeByte, + generateSignedPreKey, +} from '../Curve'; import type { SignalProtocolStore } from '../SignalProtocolStore'; import { GLOBAL_ZONE } from '../SignalProtocolStore'; import { Address } from '../types/Address'; @@ -134,8 +142,8 @@ describe('SignalProtocolStore', () => { window.storage.put('registrationIdMap', { [ourUuid.toString()]: 1337 }); window.storage.put('identityKeyMap', { [ourUuid.toString()]: { - privKey: Bytes.toBase64(identityKey.privKey), - pubKey: Bytes.toBase64(identityKey.pubKey), + privKey: identityKey.privKey, + pubKey: identityKey.pubKey, }, }); await window.storage.fetch(); @@ -1766,4 +1774,80 @@ describe('SignalProtocolStore', () => { assert.strictEqual(items.length, 0); }); }); + describe('removeOurOldPni/updateOurPniKeyMaterial', () => { + beforeEach(async () => { + await store.storePreKey(ourUuid, 2, testKey); + await store.storeSignedPreKey(ourUuid, 3, testKey); + }); + + it('removes old data and sets new', async () => { + const oldPni = ourUuid; + const newPni = UUID.generate(); + + const newIdentity = IdentityKeyPair.generate(); + + const data = generateSignedPreKey( + { + pubKey: newIdentity.publicKey.serialize(), + privKey: newIdentity.privateKey.serialize(), + }, + 8201 + ); + const createdAt = Date.now() - 1241; + const signedPreKey = SignedPreKeyRecord.new( + data.keyId, + createdAt, + PublicKey.deserialize(Buffer.from(data.keyPair.pubKey)), + PrivateKey.deserialize(Buffer.from(data.keyPair.privKey)), + Buffer.from(data.signature) + ); + + await store.removeOurOldPni(oldPni); + await store.updateOurPniKeyMaterial(newPni, { + identityKeyPair: newIdentity.serialize(), + signedPreKey: signedPreKey.serialize(), + registrationId: 5231, + }); + + // Old data has to be removed + assert.isUndefined(await store.getIdentityKeyPair(oldPni)); + assert.isUndefined(await store.getLocalRegistrationId(oldPni)); + assert.isUndefined(await store.loadPreKey(oldPni, 2)); + assert.isUndefined(await store.loadSignedPreKey(oldPni, 3)); + + // New data has to be added + const storedIdentity = await store.getIdentityKeyPair(newPni); + if (!storedIdentity) { + throw new Error('New identity not found'); + } + assert.isTrue( + Bytes.areEqual( + storedIdentity.privKey, + newIdentity.privateKey.serialize() + ) + ); + assert.isTrue( + Bytes.areEqual(storedIdentity.pubKey, newIdentity.publicKey.serialize()) + ); + + const storedSignedPreKey = await store.loadSignedPreKey(newPni, 8201); + if (!storedSignedPreKey) { + throw new Error('New signed pre key not found'); + } + assert.isTrue( + Bytes.areEqual( + storedSignedPreKey.publicKey().serialize(), + data.keyPair.pubKey + ) + ); + assert.isTrue( + Bytes.areEqual( + storedSignedPreKey.privateKey().serialize(), + data.keyPair.privKey + ) + ); + assert.strictEqual(storedSignedPreKey.timestamp(), createdAt); + // Note: signature is ignored. + }); + }); }); diff --git a/ts/test-electron/textsecure/generate_keys_test.ts b/ts/test-electron/textsecure/generate_keys_test.ts index 214d5e9fc..9d887896e 100644 --- a/ts/test-electron/textsecure/generate_keys_test.ts +++ b/ts/test-electron/textsecure/generate_keys_test.ts @@ -3,7 +3,6 @@ import { assert } from 'chai'; -import { toBase64 } from '../../Bytes'; import { constantTimeEqual } from '../../Crypto'; import { generateKeyPair } from '../../Curve'; import type { GeneratedKeysType } from '../../textsecure/AccountManager'; @@ -71,10 +70,7 @@ describe('Key generation', function thisNeeded() { before(async () => { const keyPair = generateKeyPair(); await textsecure.storage.put('identityKeyMap', { - [ourUuid.toString()]: { - privKey: toBase64(keyPair.privKey), - pubKey: toBase64(keyPair.pubKey), - }, + [ourUuid.toString()]: keyPair, }); await textsecure.storage.user.setUuidAndDeviceId(ourUuid.toString(), 1); await textsecure.storage.protocol.hydrateCaches(); diff --git a/ts/test-mock/bootstrap.ts b/ts/test-mock/bootstrap.ts index 181e5590f..344270f3f 100644 --- a/ts/test-mock/bootstrap.ts +++ b/ts/test-mock/bootstrap.ts @@ -330,8 +330,6 @@ export class Bootstrap { directoryV3Url: url, directoryV3MRENCLAVE: '51133fecb3fa18aaf0c8f64cb763656d3272d9faaacdb26ae7df082e414fb142', - directoryV3Root: - '-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n', ...this.options.extraConfig, }); diff --git a/ts/test-mock/change-number/prekey_test.ts b/ts/test-mock/change-number/prekey_test.ts new file mode 100644 index 000000000..df478082b --- /dev/null +++ b/ts/test-mock/change-number/prekey_test.ts @@ -0,0 +1,77 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { UUIDKind } from '@signalapp/mock-server'; +import createDebug from 'debug'; + +import * as durations from '../../util/durations'; +import { Bootstrap } from '../bootstrap'; +import type { App } from '../bootstrap'; + +export const debug = createDebug('mock:test:change-number'); + +describe('change number', function needsName() { + this.timeout(durations.MINUTE); + + let bootstrap: Bootstrap; + let app: App; + + beforeEach(async () => { + bootstrap = new Bootstrap(); + await bootstrap.init(); + app = await bootstrap.link(); + }); + + afterEach(async function after() { + if (this.currentTest?.state !== 'passed') { + await bootstrap.saveLogs(); + } + + await app.close(); + await bootstrap.teardown(); + }); + + it('should accept sync message and update keys', async () => { + const { server, phone, desktop, contacts } = bootstrap; + + const [first] = contacts; + + const window = await app.getWindow(); + + const leftPane = window.locator('.left-pane-wrapper'); + + debug('prepare a message for original PNI'); + const messageBefore = await first.encryptText(desktop, 'Before', { + uuidKind: UUIDKind.PNI, + }); + + debug('preparing change number'); + const changeNumber = await phone.prepareChangeNumber(); + + const newKey = await desktop.popSingleUseKey(UUIDKind.PNI); + await first.addSingleUseKey(desktop, newKey, UUIDKind.PNI); + + debug('prepare a message for updated PNI'); + const messageAfter = await first.encryptText(desktop, 'After', { + uuidKind: UUIDKind.PNI, + }); + + debug('sending all messages'); + await Promise.all([ + server.send(desktop, messageBefore), + phone.sendChangeNumber(changeNumber), + server.send(desktop, messageAfter), + ]); + + debug('opening conversation with the first contact'); + await leftPane + .locator( + '_react=ConversationListItem' + + `[title = ${JSON.stringify(first.profileName)}] ` + + ' >> "After"' + ) + .click(); + + debug('done'); + }); +}); diff --git a/ts/test-mock/gv2/create_test.ts b/ts/test-mock/gv2/create_test.ts index 38c90185b..6bd60beaf 100644 --- a/ts/test-mock/gv2/create_test.ts +++ b/ts/test-mock/gv2/create_test.ts @@ -3,7 +3,7 @@ import { assert } from 'chai'; import type { PrimaryDevice, Group } from '@signalapp/mock-server'; -import { StorageState, Proto } from '@signalapp/mock-server'; +import { StorageState, Proto, UUIDKind } from '@signalapp/mock-server'; import createDebug from 'debug'; import * as durations from '../../util/durations'; @@ -55,7 +55,7 @@ describe('gv2', function needsName() { identityState: Proto.ContactRecord.IdentityState.VERIFIED, whitelisted: true, - identityKey: pniContact.pniPublicKey.serialize(), + identityKey: pniContact.getPublicKey(UUIDKind.PNI).serialize(), // Give PNI as the uuid! serviceUuid: pniContact.device.pni, diff --git a/ts/test-node/util/mapObjectWithSpec_test.ts b/ts/test-node/util/mapObjectWithSpec_test.ts new file mode 100644 index 000000000..7f0bbf5ce --- /dev/null +++ b/ts/test-node/util/mapObjectWithSpec_test.ts @@ -0,0 +1,64 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; + +import { mapObjectWithSpec } from '../../util/mapObjectWithSpec'; + +describe('mapObjectWithSpec', () => { + const increment = (value: number) => value + 1; + + it('maps a single key/value pair', () => { + assert.deepStrictEqual(mapObjectWithSpec('a', { a: 1 }, increment), { + a: 2, + }); + }); + + it('maps a multiple key/value pairs', () => { + assert.deepStrictEqual( + mapObjectWithSpec(['a', 'b'], { a: 1, b: 2 }, increment), + { a: 2, b: 3 } + ); + }); + + it('maps a key with a value spec', () => { + assert.deepStrictEqual( + mapObjectWithSpec( + { + key: 'a', + valueSpec: ['b', 'c'], + }, + { a: { b: 1, c: 2 } }, + increment + ), + { a: { b: 2, c: 3 } } + ); + }); + + it('maps a map with a value spec', () => { + assert.deepStrictEqual( + mapObjectWithSpec( + { + isMap: true, + valueSpec: ['b', 'c'], + }, + { + key1: { b: 1, c: 2 }, + key2: { b: 3, c: 4 }, + }, + increment + ), + { + key1: { b: 2, c: 3 }, + key2: { b: 4, c: 5 }, + } + ); + }); + + it('map undefined to undefined', () => { + assert.deepStrictEqual( + mapObjectWithSpec('a', undefined, increment), + undefined + ); + }); +}); diff --git a/ts/textsecure/AccountManager.ts b/ts/textsecure/AccountManager.ts index beb7234e3..5128b5e08 100644 --- a/ts/textsecure/AccountManager.ts +++ b/ts/textsecure/AccountManager.ts @@ -7,7 +7,7 @@ import { omit } from 'lodash'; import EventTarget from './EventTarget'; import type { WebAPIType } from './WebAPI'; import { HTTPError } from './Errors'; -import type { KeyPairType } from './Types.d'; +import type { KeyPairType, PniKeyMaterialType } from './Types.d'; import ProvisioningCipher from './ProvisioningCipher'; import type { IncomingWebSocketRequest } from './WebsocketResources'; import createTaskWithTimeout from './TaskWithTimeout'; @@ -332,6 +332,7 @@ export default class AccountManager extends EventTarget { } const keys = await this.generateKeys(SIGNED_KEY_GEN_BATCH_SIZE, uuidKind); await this.server.registerKeys(keys, uuidKind); + await this.confirmKeys(keys, uuidKind); }); } @@ -649,16 +650,10 @@ export default class AccountManager extends EventTarget { const identityKeyMap = { ...(storage.get('identityKeyMap') || {}), - [ourUuid]: { - pubKey: Bytes.toBase64(aciKeyPair.pubKey), - privKey: Bytes.toBase64(aciKeyPair.privKey), - }, + [ourUuid]: aciKeyPair, ...(pniKeyPair ? { - [ourPni]: { - pubKey: Bytes.toBase64(pniKeyPair.pubKey), - privKey: Bytes.toBase64(pniKeyPair.privKey), - }, + [ourPni]: pniKeyPair, } : {}), }; @@ -702,30 +697,18 @@ export default class AccountManager extends EventTarget { log.info('AccountManager.updatePNIIdentity: generating new keys'); - return this.queueTask(async () => { - const keys = await this.generateKeys( - SIGNED_KEY_GEN_BATCH_SIZE, - UUIDKind.PNI, - identityKeyPair - ); - await this.server.registerKeys(keys, UUIDKind.PNI); - await this.confirmKeys(keys, UUIDKind.PNI); - + await this.queueTask(async () => { // Server has accepted our keys which means we have the latest PNI identity // now that doesn't conflict the PNI identity of the primary device. log.info( 'AccountManager.updatePNIIdentity: updating identity key ' + 'and registration id' ); - const { pubKey, privKey } = identityKeyPair; const pni = storage.user.getCheckedUuid(UUIDKind.PNI); const identityKeyMap = { ...(storage.get('identityKeyMap') || {}), - [pni.toString()]: { - pubKey: Bytes.toBase64(pubKey), - privKey: Bytes.toBase64(privKey), - }, + [pni.toString()]: identityKeyPair, }; const aci = storage.user.getCheckedUuid(UUIDKind.ACI); @@ -744,6 +727,26 @@ export default class AccountManager extends EventTarget { await storage.protocol.hydrateCaches(); }); + + // Intentionally not awaiting becase `updatePNIIdentity` runs on an + // Encrypted queue of MessageReceiver and we don't want to await remote + // endpoints and block message processing. + this.queueTask(async () => { + try { + const keys = await this.generateKeys( + SIGNED_KEY_GEN_BATCH_SIZE, + UUIDKind.PNI, + identityKeyPair + ); + await this.server.registerKeys(keys, UUIDKind.PNI); + await this.confirmKeys(keys, UUIDKind.PNI); + } catch (error) { + log.error( + 'updatePNIIdentity: Failed to upload PNI prekeys. Moving on', + Errors.toLogFormat(error) + ); + } + }); } // Takes the same object returned by generateKeys @@ -841,30 +844,50 @@ export default class AccountManager extends EventTarget { this.dispatchEvent(new Event('registration')); } - async setPni(pni: string): Promise { + async setPni(pni: string, keyMaterial?: PniKeyMaterialType): Promise { const { storage } = window.textsecure; const oldPni = storage.user.getUuid(UUIDKind.PNI)?.toString(); - if (oldPni === pni) { + if (oldPni === pni && !keyMaterial) { return; } + log.info(`AccountManager.setPni(${pni}): updating from ${oldPni}`); + if (oldPni) { - await Promise.all([ - storage.put( - 'identityKeyMap', - omit(storage.get('identityKeyMap') || {}, oldPni) - ), - storage.put( - 'registrationIdMap', - omit(storage.get('registrationIdMap') || {}, oldPni) - ), - ]); + await storage.protocol.removeOurOldPni(new UUID(oldPni)); } - log.info(`AccountManager.setPni: updating pni from ${oldPni} to ${pni}`); await storage.user.setPni(pni); - await storage.protocol.hydrateCaches(); + if (keyMaterial) { + await storage.protocol.updateOurPniKeyMaterial( + new UUID(pni), + keyMaterial + ); + + // Intentionally not awaiting since this is processed on encrypted queue + // of MessageReceiver. + this.queueTask(async () => { + try { + const keys = await this.generateKeys( + SIGNED_KEY_GEN_BATCH_SIZE, + UUIDKind.PNI + ); + await this.server.registerKeys(keys, UUIDKind.PNI); + await this.confirmKeys(keys, UUIDKind.PNI); + } catch (error) { + log.error( + 'setPni: Failed to upload PNI prekeys. Moving on', + Errors.toLogFormat(error) + ); + } + }); + + // PNI has changed and credentials are no longer valid + await storage.put('groupCredentials', []); + } else { + log.warn(`AccountManager.setPni(${pni}): no key material`); + } } } diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index cbafa4094..366f78c23 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -101,7 +101,6 @@ import { MessageRequestResponseEvent, FetchLatestEvent, KeysEvent, - PNIIdentityEvent, StickerPackEvent, ReadSyncEvent, ViewSyncEvent, @@ -256,8 +255,6 @@ export default class MessageReceiver private stoppingProcessing?: boolean; - private pendingPNIIdentityEvent?: PNIIdentityEvent; - constructor({ server, storage, serverTrustRoot }: MessageReceiverOptions) { super(); @@ -376,6 +373,14 @@ export default class MessageReceiver ) ) : ourUuid, + updatedPni: decoded.updatedPni + ? new UUID( + normalizeUuid( + decoded.updatedPni, + 'MessageReceiver.handleRequest.updatedPni' + ) + ) + : undefined, timestamp: decoded.timestamp?.toNumber(), content: dropNull(decoded.content), serverGuid: decoded.serverGuid, @@ -535,11 +540,6 @@ export default class MessageReceiver handler: (ev: KeysEvent) => void ): void; - public override addEventListener( - name: 'pniIdentity', - handler: (ev: PNIIdentityEvent) => void - ): void; - public override addEventListener( name: 'sticker-pack', handler: (ev: StickerPackEvent) => void @@ -671,13 +671,6 @@ export default class MessageReceiver this.isEmptied = true; this.maybeScheduleRetryTimeout(); - - // Emit PNI identity event after processing the queue - const { pendingPNIIdentityEvent } = this; - this.pendingPNIIdentityEvent = undefined; - if (pendingPNIIdentityEvent) { - await this.dispatchAndWait(pendingPNIIdentityEvent); - } }; const waitForDecryptedQueue = async () => { @@ -772,6 +765,9 @@ export default class MessageReceiver destinationUuid: new UUID( decoded.destinationUuid || item.destinationUuid || ourUuid.toString() ), + updatedPni: decoded.updatedPni + ? new UUID(decoded.updatedPni) + : undefined, timestamp: decoded.timestamp?.toNumber(), content: dropNull(decoded.content), serverGuid: decoded.serverGuid, @@ -902,16 +898,6 @@ export default class MessageReceiver items.map(async ({ data, envelope }) => { try { const { destinationUuid } = envelope; - const uuidKind = - this.storage.user.getOurUuidKind(destinationUuid); - if (uuidKind === UUIDKind.Unknown) { - log.warn( - 'MessageReceiver.decryptAndCacheBatch: ' + - `Rejecting envelope ${getEnvelopeId(envelope)}, ` + - `unknown uuid: ${destinationUuid}` - ); - return; - } let stores = storesMap.get(destinationUuid.toString()); if (!stores) { @@ -935,8 +921,7 @@ export default class MessageReceiver const result = await this.queueEncryptedEnvelope( stores, - envelope, - uuidKind + envelope ); if (result.plaintext) { decrypted.push({ @@ -972,6 +957,7 @@ export default class MessageReceiver sourceUuid: envelope.sourceUuid, sourceDevice: envelope.sourceDevice, destinationUuid: envelope.destinationUuid.toString(), + updatedPni: envelope.updatedPni?.toString(), serverGuid: envelope.serverGuid, serverTimestamp: envelope.serverTimestamp, decrypted: Bytes.toBase64(plaintext), @@ -1089,13 +1075,23 @@ export default class MessageReceiver private async queueEncryptedEnvelope( stores: LockedStores, - envelope: ProcessedEnvelope, - uuidKind: UUIDKind + envelope: ProcessedEnvelope ): Promise { let logId = getEnvelopeId(envelope); - log.info(`queueing ${uuidKind} envelope`, logId); + log.info('queueing envelope', logId); const task = async (): Promise => { + const { destinationUuid } = envelope; + const uuidKind = this.storage.user.getOurUuidKind(destinationUuid); + if (uuidKind === UUIDKind.Unknown) { + log.warn( + 'MessageReceiver.decryptAndCacheBatch: ' + + `Rejecting envelope ${getEnvelopeId(envelope)}, ` + + `unknown uuid: ${destinationUuid}` + ); + return { plaintext: undefined, envelope }; + } + const unsealedEnvelope = await this.unsealEnvelope( stores, envelope, @@ -1311,6 +1307,19 @@ export default class MessageReceiver content.senderKeyDistributionMessage ); } + + // Some sync messages have to be fully processed in the middle of + // decryption queue since subsequent envelopes use their key material. + const { syncMessage } = content; + if (syncMessage?.pniIdentity) { + await this.handlePNIIdentity(envelope, syncMessage.pniIdentity); + return { plaintext: undefined, envelope }; + } + + if (syncMessage?.pniChangeNumber) { + await this.handlePNIChangeNumber(envelope, syncMessage.pniChangeNumber); + return { plaintext: undefined, envelope }; + } } catch (error) { log.error( 'MessageReceiver.decryptEnvelope: Failed to process sender ' + @@ -2575,6 +2584,9 @@ export default class MessageReceiver if (envelope.sourceDevice == ourDeviceId) { throw new Error('Received sync message from our own device'); } + if (syncMessage.pniIdentity) { + return; + } if (syncMessage.sent) { const sentMessage = syncMessage.sent; @@ -2615,7 +2627,7 @@ export default class MessageReceiver if (this.isInvalidGroupData(sentMessage.message, envelope)) { this.removeFromCache(envelope); - return undefined; + return; } await this.checkGroupV1Data(sentMessage.message); @@ -2633,11 +2645,11 @@ export default class MessageReceiver } if (syncMessage.contacts) { this.handleContacts(envelope, syncMessage.contacts); - return undefined; + return; } if (syncMessage.groups) { this.handleGroups(envelope, syncMessage.groups); - return undefined; + return; } if (syncMessage.blocked) { return this.handleBlocked(envelope, syncMessage.blocked); @@ -2645,7 +2657,7 @@ export default class MessageReceiver if (syncMessage.request) { log.info('Got SyncMessage Request'); this.removeFromCache(envelope); - return undefined; + return; } if (syncMessage.read && syncMessage.read.length) { return this.handleRead(envelope, syncMessage.read); @@ -2653,7 +2665,7 @@ export default class MessageReceiver if (syncMessage.verified) { log.info('Got verified sync message, dropping'); this.removeFromCache(envelope); - return undefined; + return; } if (syncMessage.configuration) { return this.handleConfiguration(envelope, syncMessage.configuration); @@ -2682,9 +2694,6 @@ export default class MessageReceiver if (syncMessage.keys) { return this.handleKeys(envelope, syncMessage.keys); } - if (syncMessage.pniIdentity) { - return this.handlePNIIdentity(envelope, syncMessage.pniIdentity); - } if (syncMessage.viewed && syncMessage.viewed.length) { return this.handleViewed(envelope, syncMessage.viewed); } @@ -2693,7 +2702,6 @@ export default class MessageReceiver log.warn( `handleSyncMessage/${getEnvelopeId(envelope)}: Got empty SyncMessage` ); - return Promise.resolve(); } private async handleConfiguration( @@ -2813,6 +2821,7 @@ export default class MessageReceiver return this.dispatchAndWait(ev); } + // Runs on TaskType.Encrypted queue private async handlePNIIdentity( envelope: ProcessedEnvelope, { publicKey, privateKey }: Proto.SyncMessage.IPniIdentity @@ -2823,22 +2832,47 @@ export default class MessageReceiver if (!publicKey || !privateKey) { log.warn('MessageReceiver: empty pni identity sync message'); - return undefined; + return; } - const ev = new PNIIdentityEvent( - { publicKey, privateKey }, - this.removeFromCache.bind(this, envelope) - ); + const manager = window.getAccountManager(); + await manager.updatePNIIdentity({ privKey: privateKey, pubKey: publicKey }); + } - if (this.isEmptied) { - log.info('MessageReceiver: emitting pni identity sync message'); - return this.dispatchAndWait(ev); + // Runs on TaskType.Encrypted queue + private async handlePNIChangeNumber( + envelope: ProcessedEnvelope, + { + identityKeyPair, + signedPreKey, + registrationId, + }: Proto.SyncMessage.IPniChangeNumber + ): Promise { + log.info('MessageReceiver: got pni change number sync message'); + + logUnexpectedUrgentValue(envelope, 'pniIdentitySync'); + + const { updatedPni } = envelope; + if (!updatedPni) { + log.warn('MessageReceiver: missing pni in change number sync message'); + return; } - log.info('MessageReceiver: scheduling pni identity sync message'); - this.pendingPNIIdentityEvent?.confirm(); - this.pendingPNIIdentityEvent = ev; + if ( + !Bytes.isNotEmpty(identityKeyPair) || + !Bytes.isNotEmpty(signedPreKey) || + !isNumber(registrationId) + ) { + log.warn('MessageReceiver: empty pni change number sync message'); + return; + } + + const manager = window.getAccountManager(); + await manager.setPni(updatedPni.toString(), { + identityKeyPair, + signedPreKey, + registrationId, + }); } private async handleStickerPackOperation( diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index 9428de186..904af6166 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -87,6 +87,7 @@ export type ProcessedEnvelope = Readonly<{ sourceUuid?: UUIDStringType; sourceDevice?: number; destinationUuid: UUID; + updatedPni?: UUID; timestamp: number; content?: Uint8Array; serverGuid: string; @@ -270,3 +271,9 @@ export interface CallbackResultType { export interface IRequestHandler { handleRequest(request: IncomingWebSocketRequest): void; } + +export type PniKeyMaterialType = Readonly<{ + identityKeyPair: Uint8Array; + signedPreKey: Uint8Array; + registrationId: number; +}>; diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index 36b5424f4..f0d091aa3 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -566,7 +566,6 @@ type DirectoryV3OptionsType = Readonly<{ directoryVersion: 3; directoryV3Url: string; directoryV3MRENCLAVE: string; - directoryV3Root: string; }>; type OptionalDirectoryFieldsType = { @@ -578,7 +577,6 @@ type OptionalDirectoryFieldsType = { directoryV2CodeHashes?: unknown; directoryV3Url?: unknown; directoryV3MRENCLAVE?: unknown; - directoryV3Root?: unknown; }; type DirectoryOptionsType = OptionalDirectoryFieldsType & @@ -803,6 +801,11 @@ export type GetGroupCredentialsOptionsType = Readonly<{ endDayInMs: number; }>; +export type GetGroupCredentialsResultType = Readonly<{ + pni?: string | null; + credentials: ReadonlyArray; +}>; + export type WebAPIType = { startRegistration(): unknown; finishRegistration(baton: unknown): void; @@ -831,7 +834,7 @@ export type WebAPIType = { getGroupAvatar: (key: string) => Promise; getGroupCredentials: ( options: GetGroupCredentialsOptionsType - ) => Promise>; + ) => Promise; getGroupExternalCredential: ( options: GroupCredentialsType ) => Promise; @@ -1205,8 +1208,7 @@ export function initialize({ }, }); } else if (directoryConfig.directoryVersion === 3) { - const { directoryV3Url, directoryV3MRENCLAVE, directoryV3Root } = - directoryConfig; + const { directoryV3Url, directoryV3MRENCLAVE } = directoryConfig; cds = new CDSI({ logger: log, @@ -1214,7 +1216,6 @@ export function initialize({ url: directoryV3Url, mrenclave: directoryV3MRENCLAVE, - root: directoryV3Root, certificateAuthority, version, @@ -2510,7 +2511,7 @@ export function initialize({ async function getGroupCredentials({ startDayInMs, endDayInMs, - }: GetGroupCredentialsOptionsType): Promise> { + }: GetGroupCredentialsOptionsType): Promise { const startDayInSeconds = startDayInMs / durations.SECOND; const endDayInSeconds = endDayInMs / durations.SECOND; const response = (await _ajax({ @@ -2522,7 +2523,7 @@ export function initialize({ responseType: 'json', })) as CredentialResponseType; - return response.credentials; + return response; } async function getGroupExternalCredential( diff --git a/ts/textsecure/cds/CDSI.ts b/ts/textsecure/cds/CDSI.ts index 85ce56980..2c471229b 100644 --- a/ts/textsecure/cds/CDSI.ts +++ b/ts/textsecure/cds/CDSI.ts @@ -10,20 +10,16 @@ import { CDSSocketManagerBase } from './CDSSocketManagerBase'; export type CDSIOptionsType = Readonly<{ mrenclave: string; - root: string; }> & CDSSocketManagerBaseOptionsType; export class CDSI extends CDSSocketManagerBase { private readonly mrenclave: Buffer; - private readonly trustedCaCert: Buffer; - constructor(options: CDSIOptionsType) { super(options); this.mrenclave = Buffer.from(Bytes.fromHex(options.mrenclave)); - this.trustedCaCert = Buffer.from(options.root); } protected override getSocketUrl(): string { @@ -37,7 +33,6 @@ export class CDSI extends CDSSocketManagerBase { logger: this.logger, socket, mrenclave: this.mrenclave, - trustedCaCert: this.trustedCaCert, }); } } diff --git a/ts/textsecure/cds/CDSISocket.ts b/ts/textsecure/cds/CDSISocket.ts index 48fe6a322..49bc87b69 100644 --- a/ts/textsecure/cds/CDSISocket.ts +++ b/ts/textsecure/cds/CDSISocket.ts @@ -4,14 +4,12 @@ import { Cds2Client } from '@signalapp/libsignal-client'; import { strictAssert } from '../../util/assert'; -import { DAY } from '../../util/durations'; import { SignalService as Proto } from '../../protobuf'; import { CDSSocketBase, CDSSocketState } from './CDSSocketBase'; import type { CDSSocketBaseOptionsType } from './CDSSocketBase'; export type CDSISocketOptionsType = Readonly<{ mrenclave: Buffer; - trustedCaCert: Buffer; }> & CDSSocketBaseOptionsType; @@ -30,15 +28,14 @@ export class CDSISocket extends CDSSocketBase { await this.socketIterator.next(); strictAssert(!done, 'CDSI socket closed before handshake'); - const earliestValidTimestamp = new Date(Date.now() - DAY); + const earliestValidTimestamp = new Date(); strictAssert( this.privCdsClient === undefined, 'CDSI handshake called twice' ); - this.privCdsClient = Cds2Client.new_NOT_FOR_PRODUCTION( + this.privCdsClient = Cds2Client.new( this.options.mrenclave, - this.options.trustedCaCert, attestationMessage, earliestValidTimestamp ); diff --git a/ts/textsecure/messageReceiverEvents.ts b/ts/textsecure/messageReceiverEvents.ts index 70ed3966c..9a6a65a2d 100644 --- a/ts/textsecure/messageReceiverEvents.ts +++ b/ts/textsecure/messageReceiverEvents.ts @@ -357,20 +357,6 @@ export class KeysEvent extends ConfirmableEvent { } } -export type PNIIdentityEventData = Readonly<{ - publicKey: Uint8Array; - privateKey: Uint8Array; -}>; - -export class PNIIdentityEvent extends ConfirmableEvent { - constructor( - public readonly data: PNIIdentityEventData, - confirm: ConfirmCallback - ) { - super('pniIdentity', confirm); - } -} - export type StickerPackEventData = Readonly<{ id?: string; key?: string; diff --git a/ts/types/RendererConfig.ts b/ts/types/RendererConfig.ts index 54e19edb5..edffd7d7f 100644 --- a/ts/types/RendererConfig.ts +++ b/ts/types/RendererConfig.ts @@ -36,7 +36,6 @@ const directoryV3ConfigSchema = z.object({ directoryVersion: z.literal(3), directoryV3Url: configRequiredStringSchema, directoryV3MRENCLAVE: configRequiredStringSchema, - directoryV3Root: configRequiredStringSchema, }); export const directoryConfigSchema = z @@ -50,7 +49,6 @@ export const directoryConfigSchema = z directoryV2Url: configOptionalUnknownSchema, directoryV3Url: configOptionalUnknownSchema, directoryV3MRENCLAVE: configOptionalUnknownSchema, - directoryV3Root: configOptionalUnknownSchema, }) .and( directoryV1ConfigSchema diff --git a/ts/types/Storage.d.ts b/ts/types/Storage.d.ts index 33a89e3f1..9822e72e0 100644 --- a/ts/types/Storage.d.ts +++ b/ts/types/Storage.d.ts @@ -35,8 +35,8 @@ export type NotificationSettingType = 'message' | 'name' | 'count' | 'off'; export type IdentityKeyMap = Record< string, { - privKey: string; - pubKey: string; + privKey: Uint8Array; + pubKey: Uint8Array; } >; diff --git a/ts/types/Util.ts b/ts/types/Util.ts index 018a19d5a..8ed15eb82 100644 --- a/ts/types/Util.ts +++ b/ts/types/Util.ts @@ -61,3 +61,7 @@ type InternalAssertProps< export type AssertProps = InternalAssertProps; export type UnwrapPromise = Value extends Promise ? T : Value; + +export type BytesToStrings = Value extends Uint8Array + ? string + : { [Key in keyof Value]: BytesToStrings }; diff --git a/ts/util/mapObjectWithSpec.ts b/ts/util/mapObjectWithSpec.ts new file mode 100644 index 000000000..9738115cd --- /dev/null +++ b/ts/util/mapObjectWithSpec.ts @@ -0,0 +1,68 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { cloneDeep, get, set } from 'lodash'; + +export type ObjectMappingSpecType = + | string + | ReadonlyArray + | Readonly<{ + key: string; + valueSpec: ObjectMappingSpecType; + }> + | Readonly<{ + isMap: true; + valueSpec: ObjectMappingSpecType; + }>; + +export function mapObjectWithSpec( + spec: ObjectMappingSpecType, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + data: any, + map: (value: Input) => Output, + target = cloneDeep(data) +): any { + if (!data) { + return target; + } + + if (typeof spec === 'string') { + const value = get(data, spec); + + if (value) { + set(target, spec, map(value)); + } + return target; + } + + if ('isMap' in spec) { + for (const key of Object.keys(data)) { + // eslint-disable-next-line no-param-reassign + target[key] = mapObjectWithSpec( + spec.valueSpec, + data[key], + map, + target[key] + ); + } + return target; + } + + if ('key' in spec) { + // eslint-disable-next-line no-param-reassign + target[spec.key] = mapObjectWithSpec( + spec.valueSpec, + data[spec.key], + map, + target[spec.key] + ); + return target; + } + + for (const key of spec) { + mapObjectWithSpec(key, data, map, target); + } + + return target; +} diff --git a/ts/util/updateOurUsernameAndPni.ts b/ts/util/updateOurUsernameAndPni.ts index 35dac034e..e0e2fcb2a 100644 --- a/ts/util/updateOurUsernameAndPni.ts +++ b/ts/util/updateOurUsernameAndPni.ts @@ -13,7 +13,7 @@ export async function updateOurUsernameAndPni(): Promise { ); const me = window.ConversationController.getOurConversationOrThrow(); - const { username, pni } = await server.whoami(); + const { username } = await server.whoami(); me.set({ username: dropNull(username) }); window.Signal.Data.updateConversation(me.attributes); @@ -23,6 +23,4 @@ export async function updateOurUsernameAndPni(): Promise { manager, 'updateOurUsernameAndPni: AccountManager not available' ); - - await manager.setPni(pni); } diff --git a/yarn.lock b/yarn.lock index 8bdf09e26..dd9a6effb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1745,20 +1745,20 @@ "@react-spring/shared" "~9.4.5" "@react-spring/types" "~9.4.5" -"@signalapp/libsignal-client@0.18.1", "@signalapp/libsignal-client@^0.18.1": - version "0.18.1" - resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.18.1.tgz#6b499cdcc952f1981c6367f68484cf3275be3b31" - integrity sha512-43NcTYpahImlWHBDaNFmn7QaeXZHkFkTtb4m+ZWgzU0mkS1M8V+orGen2XuDvNiu+9HQmW4Lg7FV1deXhWtIRA== +"@signalapp/libsignal-client@0.19.1", "@signalapp/libsignal-client@^0.19.1": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.19.1.tgz#ccc12f0f034fe522940ba176a4518b4a05162b6d" + integrity sha512-x6qMjLxoq39oXnoUI8vA1Pd+fitEuYdA828LwBZIY0gxdBVv4D2DNB2kmyiGH2KtqHucnsRSz216gBOWbI2Q/g== dependencies: node-gyp-build "^4.2.3" uuid "^8.3.0" -"@signalapp/mock-server@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.1.0.tgz#25e42aad9ec2bc76c92173e7894f1aec4c2bb719" - integrity sha512-AoeCRw8hOv4F+YQ6um/ZZiskaS1SsAXoQPgSMK69/xfDcPURJnVU6KB5Fy3chU2ZF0SZyWzS8vF3QguFKsIFWA== +"@signalapp/mock-server@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.3.0.tgz#96e75fbc8d8b5f62a6deec89f9fc1bb1a4210c89" + integrity sha512-BNvT9/FbEBOKBd+2T8ImqOfKygCDLHl8bzQImRDrh3Umy7fmqYhTiuZb7WslCojEpjwH6fvZ6KfXkZDzkahqjg== dependencies: - "@signalapp/libsignal-client" "^0.18.1" + "@signalapp/libsignal-client" "^0.19.1" debug "^4.3.2" long "^4.0.0" micro "^9.3.4"