From ca1aef660f8c8b8c4d65939824ad4e2e47a178bd Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Fri, 3 Dec 2021 03:06:32 +0100 Subject: [PATCH] Generate PNI key on standalone registration --- ts/textsecure/AccountManager.ts | 132 ++++++++++++++++++++------------ ts/textsecure/SendMessage.ts | 6 +- ts/textsecure/WebAPI.ts | 8 +- ts/util/getProfile.ts | 36 ++++----- 4 files changed, 101 insertions(+), 81 deletions(-) diff --git a/ts/textsecure/AccountManager.ts b/ts/textsecure/AccountManager.ts index 6b3fecc959..32f8456a7c 100644 --- a/ts/textsecure/AccountManager.ts +++ b/ts/textsecure/AccountManager.ts @@ -30,7 +30,6 @@ import { generateSignedPreKey, generatePreKey, } from '../Curve'; -import type { UUIDStringType } from '../types/UUID'; import { UUID, UUIDKind } from '../types/UUID'; import { isMoreRecentThan, isOlderThan } from '../util/timestamp'; import { ourProfileKeyService } from '../services/ourProfileKey'; @@ -73,6 +72,18 @@ export type GeneratedKeysType = { identityKey: Uint8Array; }; +type CreateAccountOptionsType = Readonly<{ + number: string; + verificationCode: string; + identityKeyPair: KeyPairType; + pniKeyPair?: KeyPairType; + profileKey?: Uint8Array; + deviceName?: string; + userAgent?: string; + readReceipts?: boolean; + accessKey?: Uint8Array; +}>; + export default class AccountManager extends EventTarget { pending: Promise; @@ -156,28 +167,28 @@ export default class AccountManager extends EventTarget { async registerSingleDevice(number: string, verificationCode: string) { return this.queueTask(async () => { const identityKeyPair = generateKeyPair(); + const pniKeyPair = generateKeyPair(); const profileKey = getRandomBytes(PROFILE_KEY_LENGTH); const accessKey = deriveAccessKey(profileKey); - await this.createAccount( + await this.createAccount({ number, verificationCode, identityKeyPair, + pniKeyPair, profileKey, - null, - null, - null, - { accessKey } - ); + accessKey, + }); await this.clearSessionsAndPreKeys(); - // TODO: DESKTOP-2788 - const keys = await this.generateKeys( - SIGNED_KEY_GEN_BATCH_SIZE, - UUIDKind.ACI + + await Promise.all( + [UUIDKind.ACI, UUIDKind.PNI].map(async kind => { + const keys = await this.generateKeys(SIGNED_KEY_GEN_BATCH_SIZE, kind); + await this.server.registerKeys(keys, kind); + await this.confirmKeys(keys, kind); + }) ); - await this.server.registerKeys(keys, UUIDKind.ACI); - await this.confirmKeys(keys); await this.registrationDone(); }); } @@ -187,7 +198,6 @@ export default class AccountManager extends EventTarget { confirmNumber: (number?: string) => Promise, progressCallback?: Function ) { - const createAccount = this.createAccount.bind(this); const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); const provisioningCipher = new ProvisioningCipher(); const pubKey = await provisioningCipher.getPublicKey(); @@ -266,20 +276,15 @@ export default class AccountManager extends EventTarget { ); } - await createAccount( - provisionMessage.number, - provisionMessage.provisioningCode, - provisionMessage.identityKeyPair, - provisionMessage.profileKey, + await this.createAccount({ + number: provisionMessage.number, + verificationCode: provisionMessage.provisioningCode, + identityKeyPair: provisionMessage.identityKeyPair, + profileKey: provisionMessage.profileKey, deviceName, - provisionMessage.userAgent, - provisionMessage.readReceipts, - { - uuid: provisionMessage.uuid - ? UUID.cast(provisionMessage.uuid) - : undefined, - } - ); + userAgent: provisionMessage.userAgent, + readReceipts: provisionMessage.readReceipts, + }); await clearSessionsAndPreKeys(); // TODO: DESKTOP-2794 const keys = await this.generateKeys( @@ -288,7 +293,7 @@ export default class AccountManager extends EventTarget { progressCallback ); await this.server.registerKeys(keys, UUIDKind.ACI); - await this.confirmKeys(keys); + await this.confirmKeys(keys, UUIDKind.ACI); await this.registrationDone(); }); } @@ -454,18 +459,18 @@ export default class AccountManager extends EventTarget { ); } - async createAccount( - number: string, - verificationCode: string, - identityKeyPair: KeyPairType, - profileKey: Uint8Array | undefined, - deviceName: string | null, - userAgent?: string | null, - readReceipts?: boolean | null, - options: { accessKey?: Uint8Array; uuid?: UUIDStringType } = {} - ): Promise { + async createAccount({ + number, + verificationCode, + identityKeyPair, + pniKeyPair, + profileKey, + deviceName, + userAgent, + readReceipts, + accessKey, + }: CreateAccountOptionsType): Promise { const { storage } = window.textsecure; - const { accessKey, uuid } = options; let password = Bytes.toBase64(getRandomBytes(16)); password = password.substring(0, password.length - 2); const registrationId = generateRegistrationId(); @@ -491,11 +496,11 @@ export default class AccountManager extends EventTarget { password, registrationId, encryptedDeviceName, - { accessKey, uuid } + { accessKey } ); - const ourUuid = uuid || response.uuid; - strictAssert(ourUuid !== undefined, 'Should have UUID after registration'); + const ourUuid = UUID.cast(response.uuid); + const ourPni = UUID.cast(response.pni); const uuidChanged = previousUuid && ourUuid && previousUuid !== ourUuid; @@ -554,7 +559,7 @@ export default class AccountManager extends EventTarget { // information. await storage.user.setCredentials({ uuid: ourUuid, - pni: response.pni, + pni: ourPni, number, deviceId: response.deviceId ?? 1, deviceName: deviceName ?? undefined, @@ -574,15 +579,27 @@ export default class AccountManager extends EventTarget { throw new Error('registrationDone: no conversationId!'); } - // update our own identity key, which may have changed - // if we're relinking after a reinstall on the master device - await storage.protocol.saveIdentityWithAttributes(new UUID(ourUuid), { - publicKey: identityKeyPair.pubKey, + const identityAttrs = { firstUse: true, timestamp: Date.now(), verified: storage.protocol.VerifiedStatus.VERIFIED, nonblockingApproval: true, - }); + }; + + // update our own identity key, which may have changed + // if we're relinking after a reinstall on the master device + await Promise.all([ + storage.protocol.saveIdentityWithAttributes(new UUID(ourUuid), { + ...identityAttrs, + publicKey: identityKeyPair.pubKey, + }), + pniKeyPair + ? storage.protocol.saveIdentityWithAttributes(new UUID(ourPni), { + ...identityAttrs, + publicKey: pniKeyPair.pubKey, + }) + : Promise.resolve(), + ]); const identityKeyMap = { ...(storage.get('identityKeyMap') || {}), @@ -590,10 +607,20 @@ export default class AccountManager extends EventTarget { pubKey: Bytes.toBase64(identityKeyPair.pubKey), privKey: Bytes.toBase64(identityKeyPair.privKey), }, + ...(pniKeyPair + ? { + [ourPni]: { + pubKey: Bytes.toBase64(pniKeyPair.pubKey), + privKey: Bytes.toBase64(pniKeyPair.privKey), + }, + } + : {}), }; const registrationIdMap = { ...(storage.get('registrationIdMap') || {}), [ourUuid]: registrationId, + // TODO: DESKTOP-2825 + [ourPni]: registrationId, }; await storage.put('identityKeyMap', identityKeyMap); @@ -633,7 +660,7 @@ export default class AccountManager extends EventTarget { } // Takes the same object returned by generateKeys - async confirmKeys(keys: GeneratedKeysType) { + async confirmKeys(keys: GeneratedKeysType, uuidKind: UUIDKind) { const store = window.textsecure.storage.protocol; const key = keys.signedPreKey; const confirmed = true; @@ -642,8 +669,11 @@ export default class AccountManager extends EventTarget { throw new Error('confirmKeys: signedPreKey is null'); } - log.info('confirmKeys: confirming key', key.keyId); - const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + log.info( + `AccountManager.confirmKeys(${uuidKind}): confirming key`, + key.keyId + ); + const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind); await store.storeSignedPreKey(ourUuid, key.keyId, key.keyPair, confirmed); } diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index efc78b5b3a..ed6991d7a9 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -2036,7 +2036,7 @@ export default class MessageSender { // Simple pass-throughs async getProfile( - number: string, + uuid: UUID, options: Readonly<{ accessKey?: string; profileKeyVersion: string; @@ -2051,10 +2051,10 @@ export default class MessageSender { ...options, accessKey, }; - return this.server.getProfileUnauth(number, unauthOptions); + return this.server.getProfileUnauth(uuid.toString(), unauthOptions); } - return this.server.getProfile(number, options); + return this.server.getProfile(uuid.toString(), options); } async checkAccountExistence(uuid: UUID): Promise { diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index d5fb0f4e34..0e895a7894 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -765,7 +765,7 @@ export type WebAPIType = { newPassword: string, registrationId: number, deviceName?: string | null, - options?: { accessKey?: Uint8Array; uuid?: UUIDStringType } + options?: { accessKey?: Uint8Array } ) => Promise; createGroup: ( group: Proto.IGroup, @@ -1634,7 +1634,7 @@ export function initialize({ newPassword: string, registrationId: number, deviceName?: string | null, - options: { accessKey?: Uint8Array; uuid?: UUIDStringType } = {} + options: { accessKey?: Uint8Array } = {} ) { const capabilities: CapabilitiesUploadType = { announcementGroup: true, @@ -1644,7 +1644,7 @@ export function initialize({ changeNumber: true, }; - const { accessKey, uuid } = options; + const { accessKey } = options; const jsonData = { capabilities, fetchesMessages: true, @@ -1678,7 +1678,7 @@ export function initialize({ })) as ConfirmCodeResultType; // Set final REST credentials to let `registerKeys` succeed. - username = `${uuid || response.uuid || number}.${response.deviceId || 1}`; + username = `${response.uuid || number}.${response.deviceId || 1}`; password = newPassword; return response; diff --git a/ts/util/getProfile.ts b/ts/util/getProfile.ts index bb56338020..c2f3401ffa 100644 --- a/ts/util/getProfile.ts +++ b/ts/util/getProfile.ts @@ -5,7 +5,6 @@ import type { ProfileKeyCredentialRequestContext } from '@signalapp/signal-clien import { SEALED_SENDER } from '../types/SealedSender'; import { Address } from '../types/Address'; import { QualifiedAddress } from '../types/QualifiedAddress'; -import { UUID } from '../types/UUID'; import * as Bytes from '../Bytes'; import { trimForDisplay, verifyAccessKey, decryptProfile } from '../Crypto'; import { @@ -62,13 +61,11 @@ export async function getProfile( ]); const profileKey = c.get('profileKey'); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const uuid = c.get('uuid')!; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const identifier = c.getSendTarget()!; - const targetUuid = UUID.checkedLookup(identifier); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const profileKeyVersionHex = c.get('profileKeyVersion')!; + const uuid = c.getCheckedUuid('getProfile'); + const profileKeyVersionHex = c.get('profileKeyVersion'); + if (!profileKeyVersionHex) { + throw new Error('No profile key version available'); + } const existingProfileKeyCredential = c.get('profileKeyCredential'); let profileKeyCredentialRequestHex: undefined | string; @@ -76,31 +73,24 @@ export async function getProfile( | undefined | ProfileKeyCredentialRequestContext; - if ( - profileKey && - uuid && - profileKeyVersionHex && - !existingProfileKeyCredential - ) { + if (profileKey && profileKeyVersionHex && !existingProfileKeyCredential) { log.info('Generating request...'); ({ requestHex: profileKeyCredentialRequestHex, context: profileCredentialRequestContext, } = generateProfileKeyCredentialRequest( clientZkProfileCipher, - uuid, + uuid.toString(), profileKey )); } const { sendMetadata = {} } = await getSendOptions(c.attributes); - const getInfo = - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - sendMetadata[c.get('uuid')!] || sendMetadata[c.get('e164')!] || {}; + const getInfo = sendMetadata[uuid.toString()] || {}; if (getInfo.accessKey) { try { - profile = await window.textsecure.messaging.getProfile(identifier, { + profile = await window.textsecure.messaging.getProfile(uuid, { accessKey: getInfo.accessKey, profileKeyVersion: profileKeyVersionHex, profileKeyCredentialRequest: profileKeyCredentialRequestHex, @@ -112,7 +102,7 @@ export async function getProfile( `Setting sealedSender to DISABLED for conversation ${c.idForLogging()}` ); c.set({ sealedSender: SEALED_SENDER.DISABLED }); - profile = await window.textsecure.messaging.getProfile(identifier, { + profile = await window.textsecure.messaging.getProfile(uuid, { profileKeyVersion: profileKeyVersionHex, profileKeyCredentialRequest: profileKeyCredentialRequestHex, userLanguages, @@ -122,7 +112,7 @@ export async function getProfile( } } } else { - profile = await window.textsecure.messaging.getProfile(identifier, { + profile = await window.textsecure.messaging.getProfile(uuid, { profileKeyVersion: profileKeyVersionHex, profileKeyCredentialRequest: profileKeyCredentialRequestHex, userLanguages, @@ -132,7 +122,7 @@ export async function getProfile( if (profile.identityKey) { const identityKey = Bytes.fromBase64(profile.identityKey); const changed = await window.textsecure.storage.protocol.saveIdentity( - new Address(targetUuid, 1), + new Address(uuid, 1), identityKey, false ); @@ -141,7 +131,7 @@ export async function getProfile( // must close that one manually. const ourUuid = window.textsecure.storage.user.getCheckedUuid(); await window.textsecure.storage.protocol.archiveSession( - new QualifiedAddress(ourUuid, new Address(targetUuid, 1)) + new QualifiedAddress(ourUuid, new Address(uuid, 1)) ); } }