diff --git a/background.html b/background.html index 885c94ea1..ec11b1664 100644 --- a/background.html +++ b/background.html @@ -222,10 +222,6 @@ > -
diff --git a/js/rotate_signed_prekey_listener.js b/js/rotate_signed_prekey_listener.js deleted file mode 100644 index 4304a7946..000000000 --- a/js/rotate_signed_prekey_listener.js +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2017-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global Whisper, storage, getAccountManager */ - -// eslint-disable-next-line func-names -(function () { - window.Whisper = window.Whisper || {}; - const ROTATION_INTERVAL = 48 * 60 * 60 * 1000; - let timeout; - - function scheduleNextRotation() { - const now = Date.now(); - const nextTime = now + ROTATION_INTERVAL; - storage.put('nextSignedKeyRotationTime', nextTime); - } - - function scheduleRotationForNow() { - const now = Date.now(); - storage.put('nextSignedKeyRotationTime', now); - } - - async function run() { - window.SignalContext.log.info('Rotating signed prekey...'); - try { - await getAccountManager().rotateSignedPreKey(); - scheduleNextRotation(); - setTimeoutForNextRun(); - } catch (error) { - window.SignalContext.log.error( - 'rotateSignedPrekey() failed. Trying again in five minutes' - ); - setTimeout(setTimeoutForNextRun, 5 * 60 * 1000); - } - } - - function runWhenOnline() { - if (navigator.onLine) { - run(); - } else { - window.SignalContext.log.info( - 'We are offline; keys will be rotated when we are next online' - ); - const listener = () => { - window.removeEventListener('online', listener); - setTimeoutForNextRun(); - }; - window.addEventListener('online', listener); - } - } - - function setTimeoutForNextRun() { - const now = Date.now(); - const time = storage.get('nextSignedKeyRotationTime', now); - - window.SignalContext.log.info( - 'Next signed key rotation scheduled for', - new Date(time).toISOString() - ); - - let waitTime = time - now; - if (waitTime < 0) { - waitTime = 0; - } - - clearTimeout(timeout); - timeout = setTimeout(runWhenOnline, waitTime); - } - - let initComplete; - Whisper.RotateSignedPreKeyListener = { - init(events, newVersion) { - if (initComplete) { - window.SignalContext.log.info( - 'Rotate signed prekey listener: Already initialized' - ); - return; - } - initComplete = true; - - if (newVersion) { - scheduleRotationForNow(); - setTimeoutForNextRun(); - } else { - setTimeoutForNextRun(); - } - - events.on('timetravel', () => { - if (window.Signal.Util.Registration.isDone()) { - setTimeoutForNextRun(); - } - }); - }, - }; -})(); diff --git a/preload.js b/preload.js index e07cbdf5b..c82c5c919 100644 --- a/preload.js +++ b/preload.js @@ -14,6 +14,7 @@ try { const _ = require('lodash'); const { strictAssert } = require('./ts/util/assert'); const { parseIntWithFallback } = require('./ts/util/parseIntWithFallback'); + const { UUIDKind } = require('./ts/types/UUID'); // It is important to call this as early as possible const { SignalContext } = require('./ts/windows/context'); @@ -185,7 +186,7 @@ try { } const ourUuid = window.textsecure.storage.user.getUuid(); - const ourPni = window.textsecure.storage.user.getPni(); + const ourPni = window.textsecure.storage.user.getUuid(UUIDKind.PNI); event.sender.send('additional-log-data-response', { capabilities: ourCapabilities || {}, diff --git a/ts/SignalProtocolStore.ts b/ts/SignalProtocolStore.ts index 4feb05fda..02e7922e4 100644 --- a/ts/SignalProtocolStore.ts +++ b/ts/SignalProtocolStore.ts @@ -375,7 +375,7 @@ export class SignalProtocolStore extends EventsMixin { const id: PreKeyIdType = `${ourUuid.toString()}:${keyId}`; try { - this.trigger('removePreKey'); + this.trigger('removePreKey', ourUuid); } catch (error) { log.error( 'removePreKey error triggering removePreKey:', diff --git a/ts/background.ts b/ts/background.ts index 9649df36f..85a888ddb 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -83,6 +83,7 @@ import type { import { VerifiedEvent } from './textsecure/messageReceiverEvents'; import type { WebAPIType } from './textsecure/WebAPI'; import * as KeyChangeListener from './textsecure/KeyChangeListener'; +import { RotateSignedPreKeyListener } from './textsecure/RotateSignedPreKeyListener'; import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation'; import { getSendOptions } from './util/getSendOptions'; import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff'; @@ -116,6 +117,8 @@ import { onRetryRequest, onDecryptionError } from './util/handleRetry'; import { themeChanged } from './shims/themeChanged'; import { createIPCEvents } from './util/createIPCEvents'; import { RemoveAllConfiguration } from './types/RemoveAllConfiguration'; +import type { UUID } from './types/UUID'; +import { UUIDKind } from './types/UUID'; import * as log from './logging/log'; import { loadRecentEmojis, @@ -498,8 +501,9 @@ export async function startApp(): Promise { window.document.title = window.getTitle(); KeyChangeListener.init(window.textsecure.storage.protocol); - window.textsecure.storage.protocol.on('removePreKey', () => { - window.getAccountManager().refreshPreKeys(); + window.textsecure.storage.protocol.on('removePreKey', (ourUuid: UUID) => { + const uuidKind = window.textsecure.storage.user.getOurUuidKind(ourUuid); + window.getAccountManager().refreshPreKeys(uuidKind); }); window.getSocketStatus = () => { @@ -2153,6 +2157,18 @@ export async function startApp(): Promise { return unlinkAndDisconnect(RemoveAllConfiguration.Full); } + if (!window.textsecure.storage.user.getUuid(UUIDKind.PNI)) { + log.info('PNI not captured during registration, fetching'); + const { pni } = await server.whoami(); + if (!pni) { + log.error('No PNI found, unlinking'); + return unlinkAndDisconnect(RemoveAllConfiguration.Soft); + } + + log.info('Setting PNI to', pni); + await window.textsecure.storage.user.setPni(pni); + } + if (connectCount === 1) { try { // Note: we always have to register our capabilities all at once, so we do this @@ -2321,10 +2337,7 @@ export async function startApp(): Promise { window.readyForUpdates(); // Start listeners here, after we get through our queue. - window.Whisper.RotateSignedPreKeyListener.init( - window.Whisper.events, - newVersion - ); + RotateSignedPreKeyListener.init(window.Whisper.events, newVersion); // Go back to main process before processing delayed actions await window.Signal.Data.goBackToMainProcess(); diff --git a/ts/models/messages.ts b/ts/models/messages.ts index 96977e4bf..253c271d8 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -40,7 +40,7 @@ import { SendMessageProtoError } from '../textsecure/Errors'; import * as expirationTimer from '../util/expirationTimer'; import type { ReactionType } from '../types/Reactions'; -import { UUID } from '../types/UUID'; +import { UUID, UUIDKind } from '../types/UUID'; import type { UUIDStringType } from '../types/UUID'; import * as reactionUtil from '../reactions/util'; import { @@ -1470,7 +1470,9 @@ export class MessageModel extends window.Backbone.Model { }); if (hadSignedPreKeyRotationError) { - promises.push(window.getAccountManager().rotateSignedPreKey()); + promises.push( + window.getAccountManager().rotateSignedPreKey(UUIDKind.ACI) + ); } attributesToUpdate.sendStateByConversationId = sendStateByConversationId; diff --git a/ts/services/groupCredentialFetcher.ts b/ts/services/groupCredentialFetcher.ts index cb9e92fbd..1eb6deaa7 100644 --- a/ts/services/groupCredentialFetcher.ts +++ b/ts/services/groupCredentialFetcher.ts @@ -10,6 +10,7 @@ import type { GroupCredentialType } from '../textsecure/WebAPI'; import * as durations from '../util/durations'; import { BackOff } from '../util/BackOff'; import { sleep } from '../util/sleep'; +import { UUIDKind } from '../types/UUID'; import * as log from '../logging/log'; export const GROUP_CREDENTIALS_KEY = 'groupCredentials'; @@ -142,7 +143,7 @@ export async function maybeFetchNewCredentials(): Promise { serverPublicParamsBase64 ); const newCredentials = sortCredentials( - await accountManager.getGroupCredentials(startDay, endDay) + await accountManager.getGroupCredentials(startDay, endDay, UUIDKind.ACI) ).map((item: GroupCredentialType) => { const authCredential = clientZKAuthOperations.receiveAuthCredential( uuid, diff --git a/ts/state/ducks/accounts.ts b/ts/state/ducks/accounts.ts index c2550766a..d4a4171e9 100644 --- a/ts/state/ducks/accounts.ts +++ b/ts/state/ducks/accounts.ts @@ -3,7 +3,7 @@ import type { ThunkAction } from 'redux-thunk'; import type { StateType as RootStateType } from '../reducer'; -import { getUserLanguages } from '../../util/userLanguages'; +import { UUID } from '../../types/UUID'; import type { NoopActionType } from './noop'; @@ -51,13 +51,9 @@ function checkForAccount( let hasAccount = false; try { - await window.textsecure.messaging.getProfile(identifier, { - userLanguages: getUserLanguages( - navigator.languages, - window.getLocale() - ), - }); - hasAccount = true; + hasAccount = await window.textsecure.messaging.checkAccountExistence( + new UUID(identifier) + ); } catch (_error) { // Doing nothing with this failed fetch } diff --git a/ts/test-electron/models/conversations_test.ts b/ts/test-electron/models/conversations_test.ts index 58b9f5d6e..2eac06977 100644 --- a/ts/test-electron/models/conversations_test.ts +++ b/ts/test-electron/models/conversations_test.ts @@ -18,6 +18,7 @@ describe('Conversations', () => { it('updates lastMessage even in race conditions with db', async () => { const ourNumber = '+15550000000'; const ourUuid = UUID.generate().toString(); + const ourPni = UUID.generate().toString(); // Creating a fake conversation const conversation = new window.Whisper.Conversation({ @@ -39,6 +40,7 @@ describe('Conversations', () => { await window.textsecure.storage.user.setCredentials({ number: ourNumber, uuid: ourUuid, + pni: ourPni, deviceId: 2, deviceName: 'my device', password: 'password', diff --git a/ts/test-electron/textsecure/generate_keys_test.ts b/ts/test-electron/textsecure/generate_keys_test.ts index 3823bc7e7..214d5e9fc 100644 --- a/ts/test-electron/textsecure/generate_keys_test.ts +++ b/ts/test-electron/textsecure/generate_keys_test.ts @@ -9,7 +9,7 @@ import { generateKeyPair } from '../../Curve'; import type { GeneratedKeysType } from '../../textsecure/AccountManager'; import AccountManager from '../../textsecure/AccountManager'; import type { PreKeyType, SignedPreKeyType } from '../../textsecure/Types.d'; -import { UUID } from '../../types/UUID'; +import { UUID, UUIDKind } from '../../types/UUID'; const { textsecure } = window; @@ -91,7 +91,7 @@ describe('Key generation', function thisNeeded() { before(async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const accountManager = new AccountManager({} as any); - result = await accountManager.generateKeys(count); + result = await accountManager.generateKeys(count, UUIDKind.ACI); }); for (let i = 1; i <= count; i += 1) { @@ -125,7 +125,7 @@ describe('Key generation', function thisNeeded() { before(async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const accountManager = new AccountManager({} as any); - result = await accountManager.generateKeys(count); + result = await accountManager.generateKeys(count, UUIDKind.ACI); }); for (let i = 1; i <= 2 * count; i += 1) { @@ -159,7 +159,7 @@ describe('Key generation', function thisNeeded() { before(async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const accountManager = new AccountManager({} as any); - result = await accountManager.generateKeys(count); + result = await accountManager.generateKeys(count, UUIDKind.ACI); }); for (let i = 1; i <= 3 * count; i += 1) { diff --git a/ts/textsecure/AccountManager.ts b/ts/textsecure/AccountManager.ts index c87360f51..6b3fecc95 100644 --- a/ts/textsecure/AccountManager.ts +++ b/ts/textsecure/AccountManager.ts @@ -30,7 +30,8 @@ import { generateSignedPreKey, generatePreKey, } from '../Curve'; -import { UUID } from '../types/UUID'; +import type { UUIDStringType } from '../types/UUID'; +import { UUID, UUIDKind } from '../types/UUID'; import { isMoreRecentThan, isOlderThan } from '../util/timestamp'; import { ourProfileKeyService } from '../services/ourProfileKey'; import { assert, strictAssert } from '../util/assert'; @@ -170,8 +171,12 @@ export default class AccountManager extends EventTarget { ); await this.clearSessionsAndPreKeys(); - const keys = await this.generateKeys(SIGNED_KEY_GEN_BATCH_SIZE); - await this.server.registerKeys(keys); + // TODO: DESKTOP-2788 + const keys = await this.generateKeys( + SIGNED_KEY_GEN_BATCH_SIZE, + UUIDKind.ACI + ); + await this.server.registerKeys(keys, UUIDKind.ACI); await this.confirmKeys(keys); await this.registrationDone(); }); @@ -184,11 +189,6 @@ export default class AccountManager extends EventTarget { ) { const createAccount = this.createAccount.bind(this); const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); - const generateKeys = this.generateKeys.bind( - this, - SIGNED_KEY_GEN_BATCH_SIZE, - progressCallback - ); const provisioningCipher = new ProvisioningCipher(); const pubKey = await provisioningCipher.getPublicKey(); @@ -274,37 +274,40 @@ export default class AccountManager extends EventTarget { deviceName, provisionMessage.userAgent, provisionMessage.readReceipts, - { uuid: provisionMessage.uuid } + { + uuid: provisionMessage.uuid + ? UUID.cast(provisionMessage.uuid) + : undefined, + } ); await clearSessionsAndPreKeys(); - const keys = await generateKeys(); - await this.server.registerKeys(keys); + // TODO: DESKTOP-2794 + const keys = await this.generateKeys( + SIGNED_KEY_GEN_BATCH_SIZE, + UUIDKind.ACI, + progressCallback + ); + await this.server.registerKeys(keys, UUIDKind.ACI); await this.confirmKeys(keys); await this.registrationDone(); }); } - async refreshPreKeys() { - const generateKeys = this.generateKeys.bind( - this, - SIGNED_KEY_GEN_BATCH_SIZE - ); - const registerKeys = this.server.registerKeys.bind(this.server); - - return this.queueTask(async () => - this.server.getMyKeys().then(async preKeyCount => { - log.info(`prekey count ${preKeyCount}`); - if (preKeyCount < 10) { - return generateKeys().then(registerKeys); - } - return null; - }) - ); + async refreshPreKeys(uuidKind: UUIDKind) { + return this.queueTask(async () => { + const preKeyCount = await this.server.getMyKeys(uuidKind); + log.info(`prekey count ${preKeyCount}`); + if (preKeyCount >= 10) { + return; + } + const keys = await this.generateKeys(SIGNED_KEY_GEN_BATCH_SIZE, uuidKind); + await this.server.registerKeys(keys, uuidKind); + }); } - async rotateSignedPreKey() { + async rotateSignedPreKey(uuidKind: UUIDKind) { return this.queueTask(async () => { - const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind); const signedKeyId = window.textsecure.storage.get('signedKeyId', 1); if (typeof signedKeyId !== 'number') { throw new Error('Invalid signedKeyId'); @@ -350,11 +353,14 @@ export default class AccountManager extends EventTarget { return Promise.all([ window.textsecure.storage.put('signedKeyId', signedKeyId + 1), store.storeSignedPreKey(ourUuid, res.keyId, res.keyPair), - server.setSignedPreKey({ - keyId: res.keyId, - publicKey: res.keyPair.pubKey, - signature: res.signature, - }), + server.setSignedPreKey( + { + keyId: res.keyId, + publicKey: res.keyPair.pubKey, + signature: res.signature, + }, + uuidKind + ), ]) .then(async () => { const confirmed = true; @@ -456,7 +462,7 @@ export default class AccountManager extends EventTarget { deviceName: string | null, userAgent?: string | null, readReceipts?: boolean | null, - options: { accessKey?: Uint8Array; uuid?: string } = {} + options: { accessKey?: Uint8Array; uuid?: UUIDStringType } = {} ): Promise { const { storage } = window.textsecure; const { accessKey, uuid } = options; @@ -548,6 +554,7 @@ export default class AccountManager extends EventTarget { // information. await storage.user.setCredentials({ uuid: ourUuid, + pni: response.pni, number, deviceId: response.deviceId ?? 1, deviceName: deviceName ?? undefined, @@ -617,8 +624,12 @@ export default class AccountManager extends EventTarget { ]); } - async getGroupCredentials(startDay: number, endDay: number) { - return this.server.getGroupCredentials(startDay, endDay); + async getGroupCredentials( + startDay: number, + endDay: number, + uuidKind: UUIDKind + ) { + return this.server.getGroupCredentials(startDay, endDay, uuidKind); } // Takes the same object returned by generateKeys @@ -636,14 +647,18 @@ export default class AccountManager extends EventTarget { await store.storeSignedPreKey(ourUuid, key.keyId, key.keyPair, confirmed); } - async generateKeys(count: number, providedProgressCallback?: Function) { + async generateKeys( + count: number, + uuidKind: UUIDKind, + providedProgressCallback?: Function + ) { const progressCallback = typeof providedProgressCallback === 'function' ? providedProgressCallback : null; const startId = window.textsecure.storage.get('maxPreKeyId', 1); const signedKeyId = window.textsecure.storage.get('signedKeyId', 1); - const ourUuid = window.textsecure.storage.user.getCheckedUuid(); + const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind); if (typeof startId !== 'number') { throw new Error('Invalid maxPreKeyId'); diff --git a/ts/textsecure/RotateSignedPreKeyListener.ts b/ts/textsecure/RotateSignedPreKeyListener.ts new file mode 100644 index 000000000..55a2f065a --- /dev/null +++ b/ts/textsecure/RotateSignedPreKeyListener.ts @@ -0,0 +1,104 @@ +// Copyright 2017-2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import * as durations from '../util/durations'; +import { UUIDKind } from '../types/UUID'; +import * as log from '../logging/log'; + +const ROTATION_INTERVAL = 2 * durations.DAY; + +export type MinimalEventsType = { + on(event: 'timetravel', callback: () => void): void; +}; + +let initComplete = false; + +export class RotateSignedPreKeyListener { + public timeout: NodeJS.Timeout | undefined; + + protected scheduleRotationForNow(): void { + const now = Date.now(); + window.textsecure.storage.put('nextSignedKeyRotationTime', now); + } + + protected setTimeoutForNextRun(): void { + const now = Date.now(); + const time = window.textsecure.storage.get( + 'nextSignedKeyRotationTime', + now + ); + + log.info( + 'Next signed key rotation scheduled for', + new Date(time).toISOString() + ); + + let waitTime = time - now; + if (waitTime < 0) { + waitTime = 0; + } + + if (this.timeout !== undefined) { + clearTimeout(this.timeout); + } + this.timeout = setTimeout(() => this.runWhenOnline(), waitTime); + } + + private scheduleNextRotation(): void { + const now = Date.now(); + const nextTime = now + ROTATION_INTERVAL; + window.textsecure.storage.put('nextSignedKeyRotationTime', nextTime); + } + + private async run(): Promise { + log.info('Rotating signed prekey...'); + try { + const accountManager = window.getAccountManager(); + await Promise.all([ + accountManager.rotateSignedPreKey(UUIDKind.ACI), + accountManager.rotateSignedPreKey(UUIDKind.PNI), + ]); + this.scheduleNextRotation(); + this.setTimeoutForNextRun(); + } catch (error) { + log.error('rotateSignedPrekey() failed. Trying again in five minutes'); + setTimeout(() => this.setTimeoutForNextRun(), 5 * durations.MINUTE); + } + } + + private runWhenOnline() { + if (window.navigator.onLine) { + this.run(); + } else { + log.info('We are offline; keys will be rotated when we are next online'); + const listener = () => { + window.removeEventListener('online', listener); + this.setTimeoutForNextRun(); + }; + window.addEventListener('online', listener); + } + } + + public static init(events: MinimalEventsType, newVersion: boolean): void { + if (initComplete) { + window.SignalContext.log.info( + 'Rotate signed prekey listener: Already initialized' + ); + return; + } + initComplete = true; + + const listener = new RotateSignedPreKeyListener(); + + if (newVersion) { + listener.scheduleRotationForNow(); + } + listener.setTimeoutForNextRun(); + + events.on('timetravel', () => { + if (window.Signal.Util.Registration.isDone()) { + listener.setTimeoutForNextRun(); + } + }); + } +} diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index a0ae3180d..efc78b5b3 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -23,7 +23,7 @@ import { SenderKeys } from '../LibSignalStores'; import type { LinkPreviewType } from '../types/message/LinkPreviews'; import { MIMETypeToString } from '../types/MIME'; import type * as Attachment from '../types/Attachment'; -import type { UUIDStringType } from '../types/UUID'; +import type { UUID, UUIDStringType } from '../types/UUID'; import type { ChallengeType, GroupCredentialsType, @@ -2039,7 +2039,7 @@ export default class MessageSender { number: string, options: Readonly<{ accessKey?: string; - profileKeyVersion?: string; + profileKeyVersion: string; profileKeyCredentialRequest?: string; userLanguages: ReadonlyArray; }> @@ -2057,6 +2057,10 @@ export default class MessageSender { return this.server.getProfile(number, options); } + async checkAccountExistence(uuid: UUID): Promise { + return this.server.checkAccountExistence(uuid); + } + async getProfileForUsername( username: string ): ReturnType { diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index a50944121..d5fb0f4e3 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -35,7 +35,8 @@ import { toWebSafeBase64 } from '../util/webSafeBase64'; import type { SocketStatus } from '../types/SocketStatus'; import { toLogFormat } from '../types/errors'; import { isPackIdValid, redactPackId } from '../types/Stickers'; -import type { UUIDStringType } from '../types/UUID'; +import type { UUID, UUIDStringType } from '../types/UUID'; +import { UUIDKind } from '../types/UUID'; import * as Bytes from '../Bytes'; import { constantTimeEqual, @@ -164,7 +165,7 @@ function getContentType(response: Response) { type FetchHeaderListType = { [name: string]: string }; export type HeaderListType = { [name: string]: string | ReadonlyArray }; -type HTTPCodeType = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; +type HTTPCodeType = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'; type RedactUrl = (url: string) => string; @@ -519,6 +520,7 @@ function makeHTTPError( const URL_CALLS = { accounts: 'v1/accounts', + accountExistence: 'v1/accounts/account', attachmentId: 'v2/attachments/form/upload', attestation: 'v1/attestation', challenge: 'v1/challenge', @@ -745,10 +747,17 @@ export type MakeProxiedRequestResultType = }; export type WhoamiResultType = Readonly<{ - uuid?: string; + uuid?: UUIDStringType; + pni?: UUIDStringType; number?: string; }>; +export type ConfirmCodeResultType = Readonly<{ + uuid: UUIDStringType; + pni: UUIDStringType; + deviceId?: number; +}>; + export type WebAPIType = { confirmCode: ( number: string, @@ -756,8 +765,8 @@ export type WebAPIType = { newPassword: string, registrationId: number, deviceName?: string | null, - options?: { accessKey?: Uint8Array; uuid?: string } - ) => Promise<{ uuid?: string; deviceId?: number }>; + options?: { accessKey?: Uint8Array; uuid?: UUIDStringType } + ) => Promise; createGroup: ( group: Proto.IGroup, options: GroupCredentialsType @@ -775,7 +784,8 @@ export type WebAPIType = { getGroupAvatar: (key: string) => Promise; getGroupCredentials: ( startDay: number, - endDay: number + endDay: number, + uuidKind: UUIDKind ) => Promise>; getGroupExternalCredential: ( options: GroupCredentialsType @@ -794,13 +804,14 @@ export type WebAPIType = { deviceId?: number, options?: { accessKey?: string } ) => Promise; - getMyKeys: () => Promise; + getMyKeys: (uuidKind: UUIDKind) => Promise; getProfile: ( identifier: string, options: { - profileKeyVersion?: string; + profileKeyVersion: string; profileKeyCredentialRequest?: string; userLanguages: ReadonlyArray; + credentialType?: 'pni' | 'profileKey'; } ) => Promise; getProfileForUsername: (username: string) => Promise; @@ -808,7 +819,7 @@ export type WebAPIType = { identifier: string, options: { accessKey: string; - profileKeyVersion?: string; + profileKeyVersion: string; profileKeyCredentialRequest?: string; userLanguages: ReadonlyArray; } @@ -866,11 +877,12 @@ export type WebAPIType = { ) => Promise; putUsername: (newUsername: string) => Promise; registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise; - registerKeys: (genKeys: KeysType) => Promise; + registerKeys: (genKeys: KeysType, uuidKind: UUIDKind) => Promise; registerSupportForUnauthenticatedDelivery: () => Promise; reportMessage: (senderE164: string, serverGuid: string) => Promise; requestVerificationSMS: (number: string, token: string) => Promise; requestVerificationVoice: (number: string, token: string) => Promise; + checkAccountExistence: (uuid: UUID) => Promise; sendMessages: ( destination: string, messageArray: ReadonlyArray, @@ -890,7 +902,10 @@ export type WebAPIType = { timestamp: number, online?: boolean ) => Promise; - setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise; + setSignedPreKey: ( + signedPreKey: SignedPreKeyType, + uuidKind: UUIDKind + ) => Promise; updateDeviceName: (deviceName: string) => Promise; uploadAvatar: ( uploadAvatarRequestHeaders: UploadAvatarHeadersType, @@ -1091,6 +1106,7 @@ export function initialize({ unregisterRequestHandler, authenticate, logout, + checkAccountExistence, confirmCode, createGroup, deleteUsername, @@ -1180,13 +1196,13 @@ export function initialize({ (param.jsonData ? JSON.stringify(param.jsonData) : undefined), headers: param.headers, host: param.host || url, - password: param.password || password, + password: param.password ?? password, path: URL_CALLS[param.call] + param.urlParameters, proxyUrl, responseType: param.responseType, timeout: param.timeout, type: param.httpType, - user: param.username || username, + user: param.username ?? username, redactUrl: param.redactUrl, serverUrl: url, validateResponse: param.validateResponse, @@ -1209,6 +1225,18 @@ export function initialize({ } } + function uuidKindToQuery(kind: UUIDKind): string { + let value: string; + if (kind === UUIDKind.ACI) { + value = 'aci'; + } else if (kind === UUIDKind.PNI) { + value = 'pni'; + } else { + throw new Error(`Unsupported UUIDKind: ${kind}`); + } + return `identity=${value}`; + } + async function whoami() { return (await _ajax({ call: 'whoami', @@ -1378,16 +1406,14 @@ export function initialize({ function getProfileUrl( identifier: string, - profileKeyVersion?: string, - profileKeyCredentialRequest?: string + profileKeyVersion: string, + profileKeyCredentialRequest?: string, + credentialType: 'pni' | 'profileKey' = 'profileKey' ) { - let profileUrl = `/${identifier}`; + let profileUrl = `/${identifier}/${profileKeyVersion}`; - if (profileKeyVersion) { - profileUrl += `/${profileKeyVersion}`; - } - if (profileKeyVersion && profileKeyCredentialRequest) { - profileUrl += `/${profileKeyCredentialRequest}`; + if (profileKeyCredentialRequest) { + profileUrl += `/${profileKeyCredentialRequest}?credentialType=${credentialType}`; } return profileUrl; @@ -1396,13 +1422,18 @@ export function initialize({ async function getProfile( identifier: string, options: { - profileKeyVersion?: string; + profileKeyVersion: string; profileKeyCredentialRequest?: string; userLanguages: ReadonlyArray; + credentialType?: 'pni' | 'profileKey'; } ) { - const { profileKeyVersion, profileKeyCredentialRequest, userLanguages } = - options; + const { + profileKeyVersion, + profileKeyCredentialRequest, + userLanguages, + credentialType = 'profileKey', + } = options; return (await _ajax({ call: 'profile', @@ -1410,7 +1441,8 @@ export function initialize({ urlParameters: getProfileUrl( identifier, profileKeyVersion, - profileKeyCredentialRequest + profileKeyCredentialRequest, + credentialType ), headers: { 'Accept-Language': formatAcceptLanguageHeader(userLanguages), @@ -1425,9 +1457,13 @@ export function initialize({ } async function getProfileForUsername(usernameToFetch: string) { - return getProfile(`username/${usernameToFetch}`, { - userLanguages: [], - }); + return (await _ajax({ + call: 'profile', + httpType: 'GET', + urlParameters: `username/${usernameToFetch}`, + responseType: 'json', + redactUrl: _createRedactor(usernameToFetch), + })) as ProfileType; } async function putProfile( @@ -1451,7 +1487,7 @@ export function initialize({ identifier: string, options: { accessKey: string; - profileKeyVersion?: string; + profileKeyVersion: string; profileKeyCredentialRequest?: string; userLanguages: ReadonlyArray; } @@ -1573,13 +1609,32 @@ export function initialize({ }); } + async function checkAccountExistence(uuid: UUID) { + try { + await _ajax({ + httpType: 'HEAD', + call: 'accountExistence', + urlParameters: `/${uuid.toString()}`, + unauthenticated: true, + accessKey: undefined, + }); + return true; + } catch (error) { + if (error instanceof HTTPError && error.code === 404) { + return false; + } + + throw error; + } + } + async function confirmCode( number: string, code: string, newPassword: string, registrationId: number, deviceName?: string | null, - options: { accessKey?: Uint8Array; uuid?: string } = {} + options: { accessKey?: Uint8Array; uuid?: UUIDStringType } = {} ) { const capabilities: CapabilitiesUploadType = { announcementGroup: true, @@ -1620,7 +1675,7 @@ export function initialize({ responseType: 'json', urlParameters: urlPrefix + code, jsonData, - })) as { uuid?: string; deviceId?: number }; + })) as ConfirmCodeResultType; // Set final REST credentials to let `registerKeys` succeed. username = `${uuid || response.uuid || number}.${response.deviceId || 1}`; @@ -1670,7 +1725,7 @@ export function initialize({ }>; }; - async function registerKeys(genKeys: KeysType) { + async function registerKeys(genKeys: KeysType, uuidKind: UUIDKind) { const preKeys = genKeys.preKeys.map(key => ({ keyId: key.keyId, publicKey: Bytes.toBase64(key.publicKey), @@ -1688,14 +1743,19 @@ export function initialize({ await _ajax({ call: 'keys', + urlParameters: `?${uuidKindToQuery(uuidKind)}`, httpType: 'PUT', jsonData: keys, }); } - async function setSignedPreKey(signedPreKey: SignedPreKeyType) { + async function setSignedPreKey( + signedPreKey: SignedPreKeyType, + uuidKind: UUIDKind + ) { await _ajax({ call: 'signed', + urlParameters: `?${uuidKindToQuery(uuidKind)}`, httpType: 'PUT', jsonData: { keyId: signedPreKey.keyId, @@ -1709,9 +1769,10 @@ export function initialize({ count: number; }; - async function getMyKeys(): Promise { + async function getMyKeys(uuidKind: UUIDKind): Promise { const result = (await _ajax({ call: 'keys', + urlParameters: `?${uuidKindToQuery(uuidKind)}`, httpType: 'GET', responseType: 'json', validateResponse: { count: 'number' }, @@ -2233,11 +2294,12 @@ export function initialize({ async function getGroupCredentials( startDay: number, - endDay: number + endDay: number, + uuidKind: UUIDKind ): Promise> { const response = (await _ajax({ call: 'getGroupCredentials', - urlParameters: `/${startDay}/${endDay}`, + urlParameters: `/${startDay}/${endDay}?${uuidKindToQuery(uuidKind)}`, httpType: 'GET', responseType: 'json', })) as CredentialResponseType; diff --git a/ts/textsecure/storage/User.ts b/ts/textsecure/storage/User.ts index b24dc1ccf..a1050b7ed 100644 --- a/ts/textsecure/storage/User.ts +++ b/ts/textsecure/storage/User.ts @@ -11,7 +11,8 @@ import * as log from '../../logging/log'; import Helpers from '../Helpers'; export type SetCredentialsOptions = { - uuid?: string; + uuid: string; + pni: string; number: string; deviceId: number; deviceName?: string; @@ -58,22 +59,30 @@ export class User { return Helpers.unencodeNumber(numberId)[0]; } - public getUuid(): UUID | undefined { + public getUuid(uuidKind = UUIDKind.ACI): UUID | undefined { + if (uuidKind === UUIDKind.PNI) { + const pni = this.storage.get('pni'); + if (pni === undefined) return undefined; + return new UUID(pni); + } + + strictAssert( + uuidKind === UUIDKind.ACI, + `Unsupported uuid kind: ${uuidKind}` + ); const uuid = this.storage.get('uuid_id'); if (uuid === undefined) return undefined; return new UUID(Helpers.unencodeNumber(uuid.toLowerCase())[0]); } - public getCheckedUuid(): UUID { - const uuid = this.getUuid(); + public getCheckedUuid(uuidKind?: UUIDKind): UUID { + const uuid = this.getUuid(uuidKind); strictAssert(uuid !== undefined, 'Must have our own uuid'); return uuid; } - public getPni(): UUID | undefined { - const pni = this.storage.get('pni'); - if (pni === undefined) return undefined; - return new UUID(pni); + public async setPni(pni: string): Promise { + await this.storage.put('pni', UUID.cast(pni)); } public getOurUuidKind(uuid: UUID): UUIDKind { @@ -83,7 +92,7 @@ export class User { return UUIDKind.ACI; } - const pni = this.getPni(); + const pni = this.getUuid(UUIDKind.PNI); if (pni?.toString() === uuid.toString()) { return UUIDKind.PNI; } @@ -118,12 +127,13 @@ export class User { public async setCredentials( credentials: SetCredentialsOptions ): Promise { - const { uuid, number, deviceId, deviceName, password } = credentials; + const { uuid, pni, number, deviceId, deviceName, password } = credentials; await Promise.all([ this.storage.put('number_id', `${number}.${deviceId}`), this.storage.put('uuid_id', `${uuid}.${deviceId}`), this.storage.put('password', password), + this.setPni(pni), deviceName ? this.storage.put('device_name', deviceName) : Promise.resolve(), diff --git a/ts/types/Storage.d.ts b/ts/types/Storage.d.ts index fb0e13140..d05b50923 100644 --- a/ts/types/Storage.d.ts +++ b/ts/types/Storage.d.ts @@ -134,6 +134,7 @@ export type StorageAccessType = { paymentAddress: string; zoomFactor: ZoomFactorType; preferredLeftPaneWidth: number; + nextSignedKeyRotationTime: number; areWeASubscriber: boolean; subscriberId: Uint8Array; subscriberCurrencyCode: string; diff --git a/ts/window.d.ts b/ts/window.d.ts index c7bd4fe71..59e771bee 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -548,7 +548,6 @@ export type WhisperType = { MessageCollection: typeof MessageModelCollectionType; GroupMemberConversation: WhatIsThis; - RotateSignedPreKeyListener: WhatIsThis; WallClockListener: WhatIsThis; deliveryReceiptQueue: PQueue;