From fdec47d637cb5bb30503bb403ca857dec2040d97 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Fri, 23 Jul 2021 10:23:50 -0700 Subject: [PATCH] Use single WebAPI instance across the app --- ts/RemoteConfig.ts | 15 +-- ts/background.ts | 125 +++++++++--------- ts/jobs/initializeAllJobQueues.ts | 10 +- ts/jobs/reportSpamJobQueue.ts | 15 ++- ts/models/messages.ts | 3 +- ts/services/senderCertificate.ts | 15 +-- ...nnectToServerWithStoredCredentials_test.ts | 90 ------------- ts/test-electron/MessageReceiver_test.ts | 14 +- ts/test-electron/models/conversations_test.ts | 13 +- ts/test-electron/models/messages_test.ts | 3 +- .../services/senderCertificate_test.ts | 4 +- ts/textsecure/AccountManager.ts | 29 ++-- ts/textsecure/MessageReceiver.ts | 46 +------ ts/textsecure/SendMessage.ts | 5 +- ts/textsecure/Types.d.ts | 5 + ts/textsecure/WebAPI.ts | 20 ++- ts/textsecure/storage/User.ts | 86 ++++++++++-- .../connectToServerWithStoredCredentials.ts | 26 ---- ts/window.d.ts | 2 +- 19 files changed, 218 insertions(+), 308 deletions(-) delete mode 100644 ts/test-both/util/connectToServerWithStoredCredentials_test.ts delete mode 100644 ts/util/connectToServerWithStoredCredentials.ts diff --git a/ts/RemoteConfig.ts b/ts/RemoteConfig.ts index ca33441e048b..e2f0dc272a2d 100644 --- a/ts/RemoteConfig.ts +++ b/ts/RemoteConfig.ts @@ -2,7 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-only import { get, throttle } from 'lodash'; -import { connectToServerWithStoredCredentials } from './util/connectToServerWithStoredCredentials'; + +import type { WebAPIType } from './textsecure/WebAPI'; export type ConfigKeyType = | 'desktop.announcementGroup' @@ -38,9 +39,9 @@ type ConfigListenersMapType = { let config: ConfigMapType = {}; const listeners: ConfigListenersMapType = {}; -export async function initRemoteConfig(): Promise { +export async function initRemoteConfig(server: WebAPIType): Promise { config = window.storage.get('remoteConfig') || {}; - await maybeRefreshRemoteConfig(); + await maybeRefreshRemoteConfig(server); } export function onChange( @@ -56,12 +57,10 @@ export function onChange( }; } -export const refreshRemoteConfig = async (): Promise => { +export const refreshRemoteConfig = async ( + server: WebAPIType +): Promise => { const now = Date.now(); - const server = connectToServerWithStoredCredentials( - window.WebAPI, - window.storage - ); const newConfig = await server.getConfig(); // Process new configuration in light of the old configuration diff --git a/ts/background.ts b/ts/background.ts index 79716da7ca93..724851bfb05f 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -60,7 +60,7 @@ import { ContactEvent, GroupEvent, } from './textsecure/messageReceiverEvents'; -import { connectToServerWithStoredCredentials } from './util/connectToServerWithStoredCredentials'; +import type { WebAPIType } from './textsecure/WebAPI'; import * as universalExpireTimer from './util/universalExpireTimer'; import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation'; import { getSendOptions } from './util/getSendOptions'; @@ -132,7 +132,24 @@ export async function startApp(): Promise { ); } - initializeAllJobQueues(); + // Initialize WebAPI as early as possible + let server: WebAPIType | undefined; + window.storage.onready(() => { + server = window.WebAPI.connect( + window.textsecure.storage.user.getWebAPICredentials() + ); + + window.textsecure.storage.user.on('credentialsChange', async () => { + strictAssert(server !== undefined, 'WebAPI not ready'); + await server.authenticate( + window.textsecure.storage.user.getWebAPICredentials() + ); + }); + + initializeAllJobQueues({ + server, + }); + }); ourProfileKeyService.initialize(window.storage); @@ -153,8 +170,10 @@ export async function startApp(): Promise { const reconnectBackOff = new BackOff(FIBONACCI_TIMEOUTS); window.storage.onready(() => { + strictAssert(server, 'WebAPI not ready'); + senderCertificateService.initialize({ - WebAPI: window.WebAPI, + server, navigator, onlineEventTarget: window, storage: window.storage, @@ -355,8 +374,7 @@ export async function startApp(): Promise { window.Whisper.KeyChangeListener.init(window.textsecure.storage.protocol); window.textsecure.storage.protocol.on('removePreKey', () => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - window.getAccountManager()!.refreshPreKeys(); + window.getAccountManager().refreshPreKeys(); }); let messageReceiver: MessageReceiver | undefined; @@ -372,32 +390,28 @@ export async function startApp(): Promise { }; let accountManager: typeof window.textsecure.AccountManager; window.getAccountManager = () => { - if (!accountManager) { - const OLD_USERNAME = window.storage.get('number_id', ''); - const USERNAME = window.storage.get('uuid_id', ''); - const PASSWORD = window.storage.get('password', ''); - accountManager = new window.textsecure.AccountManager( - USERNAME || OLD_USERNAME, - PASSWORD - ); - accountManager.addEventListener('registration', () => { - const ourDeviceId = window.textsecure.storage.user.getDeviceId(); - const ourNumber = window.textsecure.storage.user.getNumber(); - const ourUuid = window.textsecure.storage.user.getUuid(); - const user = { - ourConversationId: window.ConversationController.getOurConversationId(), - ourDeviceId, - ourNumber, - ourUuid, - regionCode: window.storage.get('regionCode'), - }; - window.Whisper.events.trigger('userChanged', user); - - window.Signal.Util.Registration.markDone(); - window.log.info('dispatching registration event'); - window.Whisper.events.trigger('registration_done'); - }); + if (accountManager) { + return accountManager; } + + accountManager = new window.textsecure.AccountManager(server); + accountManager.addEventListener('registration', () => { + const ourDeviceId = window.textsecure.storage.user.getDeviceId(); + const ourNumber = window.textsecure.storage.user.getNumber(); + const ourUuid = window.textsecure.storage.user.getUuid(); + const user = { + ourConversationId: window.ConversationController.getOurConversationId(), + ourDeviceId, + ourNumber, + ourUuid, + regionCode: window.storage.get('regionCode'), + }; + window.Whisper.events.trigger('userChanged', user); + + window.Signal.Util.Registration.markDone(); + window.log.info('dispatching registration event'); + window.Whisper.events.trigger('registration_done'); + }); return accountManager; }; @@ -483,6 +497,8 @@ export async function startApp(): Promise { } first = false; + strictAssert(server !== undefined, 'WebAPI not ready'); + cleanupSessionResets(); // These make key operations available to IPC handlers created in preload.js @@ -888,7 +904,7 @@ export async function startApp(): Promise { // We start this up before window.ConversationController.load() to // ensure that our feature flags are represented in the cached props // we generate on load of each convo. - window.Signal.RemoteConfig.initRemoteConfig(); + window.Signal.RemoteConfig.initRemoteConfig(server); let retryReceiptLifespan: number | undefined; try { @@ -1724,6 +1740,7 @@ export async function startApp(): Promise { window.reduxActions.network.setChallengeStatus(challengeStatus); }, }); + window.Whisper.events.on('challengeResponse', response => { if (!challengeHandler) { throw new Error('Expected challenge handler to be there'); @@ -1732,13 +1749,8 @@ export async function startApp(): Promise { challengeHandler.onResponse(response); }); - window.storage.onready(async () => { - if (!challengeHandler) { - throw new Error('Expected challenge handler to be there'); - } - - await challengeHandler.load(); - }); + // Storage is ready because `start()` is called from `storage.onready()` + await challengeHandler.load(); window.Signal.challengeHandler = challengeHandler; @@ -1828,8 +1840,10 @@ export async function startApp(): Promise { // Maybe refresh remote configuration when we become active window.registerForActive(async () => { + strictAssert(server !== undefined, 'WebAPI not ready'); + try { - await window.Signal.RemoteConfig.maybeRefreshRemoteConfig(); + await window.Signal.RemoteConfig.maybeRefreshRemoteConfig(server); } catch (error) { if (error && window._.isNumber(error.code)) { window.log.warn( @@ -2010,6 +2024,9 @@ export async function startApp(): Promise { window.log.warn('connect already running', { connectCount }); return; } + + strictAssert(server !== undefined, 'WebAPI not connected'); + try { connecting = true; @@ -2050,20 +2067,13 @@ export async function startApp(): Promise { messageReceiver = undefined; } - const OLD_USERNAME = window.storage.get('number_id', ''); - const USERNAME = window.storage.get('uuid_id', ''); - const PASSWORD = window.storage.get('password', ''); - - window.textsecure.messaging = new window.textsecure.MessageSender( - USERNAME || OLD_USERNAME, - PASSWORD - ); + window.textsecure.messaging = new window.textsecure.MessageSender(server); if (connectCount === 0) { try { // Force a re-fetch before we process our queue. We may want to turn on // something which changes how we process incoming messages! - await window.Signal.RemoteConfig.refreshRemoteConfig(); + await window.Signal.RemoteConfig.refreshRemoteConfig(server); } catch (error) { window.log.error( 'connect: Error refreshing remote config:', @@ -2109,9 +2119,7 @@ export async function startApp(): Promise { serverTrustRoot: window.getServerTrustRoot(), }; messageReceiver = new window.textsecure.MessageReceiver( - OLD_USERNAME, - USERNAME, - PASSWORD, + server, messageReceiverOptions ); window.textsecure.messageReceiver = messageReceiver; @@ -2251,8 +2259,7 @@ export async function startApp(): Promise { runStorageService(); try { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const manager = window.getAccountManager()!; + const manager = window.getAccountManager(); await Promise.all([ manager.maybeUpdateDeviceName(), window.textsecure.storage.user.removeSignalingKey(), @@ -2267,10 +2274,6 @@ export async function startApp(): Promise { const udSupportKey = 'hasRegisterSupportForUnauthenticatedDelivery'; if (!window.storage.get(udSupportKey)) { - const server = connectToServerWithStoredCredentials( - window.WebAPI, - window.storage - ); try { await server.registerSupportForUnauthenticatedDelivery(); window.storage.put(udSupportKey, true); @@ -2286,10 +2289,6 @@ export async function startApp(): Promise { // If we didn't capture a UUID on registration, go get it from the server if (!window.textsecure.storage.user.getUuid()) { - const server = window.WebAPI.connect({ - username: OLD_USERNAME, - password: PASSWORD, - }); try { const { uuid } = await server.whoami(); assert(deviceId, 'We should have device id'); @@ -2311,10 +2310,6 @@ export async function startApp(): Promise { } if (connectCount === 1) { - const server = connectToServerWithStoredCredentials( - window.WebAPI, - window.storage - ); try { // Note: we always have to register our capabilities all at once, so we do this // after connect on every startup diff --git a/ts/jobs/initializeAllJobQueues.ts b/ts/jobs/initializeAllJobQueues.ts index 4e25bb1104e8..17cc338adb0e 100644 --- a/ts/jobs/initializeAllJobQueues.ts +++ b/ts/jobs/initializeAllJobQueues.ts @@ -1,13 +1,21 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import type { WebAPIType } from '../textsecure/WebAPI'; + import { removeStorageKeyJobQueue } from './removeStorageKeyJobQueue'; import { reportSpamJobQueue } from './reportSpamJobQueue'; /** * Start all of the job queues. Should be called when the database is ready. */ -export function initializeAllJobQueues(): void { +export function initializeAllJobQueues({ + server, +}: { + server: WebAPIType; +}): void { + reportSpamJobQueue.initialize({ server }); + removeStorageKeyJobQueue.streamJobs(); reportSpamJobQueue.streamJobs(); } diff --git a/ts/jobs/reportSpamJobQueue.ts b/ts/jobs/reportSpamJobQueue.ts index 351416ded232..9972b909f31a 100644 --- a/ts/jobs/reportSpamJobQueue.ts +++ b/ts/jobs/reportSpamJobQueue.ts @@ -4,16 +4,17 @@ import * as z from 'zod'; import * as moment from 'moment'; +import { strictAssert } from '../util/assert'; import { waitForOnline } from '../util/waitForOnline'; import { isDone as isDeviceLinked } from '../util/registration'; import * as log from '../logging/log'; -import { connectToServerWithStoredCredentials } from '../util/connectToServerWithStoredCredentials'; import { map } from '../util/iterables'; import { sleep } from '../util/sleep'; import { JobQueue } from './JobQueue'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; import { parseIntWithFallback } from '../util/parseIntWithFallback'; +import type { WebAPIType } from '../textsecure/WebAPI'; const RETRY_WAIT_TIME = moment.duration(1, 'minute').asMilliseconds(); const RETRYABLE_4XX_FAILURE_STATUSES = new Set([ @@ -47,6 +48,12 @@ const reportSpamJobDataSchema = z.object({ export type ReportSpamJobData = z.infer; export class ReportSpamJobQueue extends JobQueue { + private server?: WebAPIType; + + public initialize({ server }: { server: WebAPIType }): void { + this.server = server; + } + protected parseData(data: unknown): ReportSpamJobData { return reportSpamJobDataSchema.parse(data); } @@ -67,10 +74,8 @@ export class ReportSpamJobQueue extends JobQueue { await waitForOnline(window.navigator, window); - const server = connectToServerWithStoredCredentials( - window.WebAPI, - window.storage - ); + const { server } = this; + strictAssert(server !== undefined, 'ReportSpamJobQueue not initialized'); try { await Promise.all( diff --git a/ts/models/messages.ts b/ts/models/messages.ts index 1fc347f7d539..84460acd4299 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -1678,8 +1678,7 @@ export class MessageModel extends window.Backbone.Model { }); if (hadSignedPreKeyRotationError) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - promises.push(window.getAccountManager()!.rotateSignedPreKey()); + promises.push(window.getAccountManager().rotateSignedPreKey()); } attributesToUpdate.sendStateByConversationId = sendStateByConversationId; diff --git a/ts/services/senderCertificate.ts b/ts/services/senderCertificate.ts index 70032c2c7d07..b87ed8812e52 100644 --- a/ts/services/senderCertificate.ts +++ b/ts/services/senderCertificate.ts @@ -13,8 +13,8 @@ import { missingCaseError } from '../util/missingCaseError'; import { normalizeNumber } from '../util/normalizeNumber'; import { waitForOnline } from '../util/waitForOnline'; import * as log from '../logging/log'; -import { connectToServerWithStoredCredentials } from '../util/connectToServerWithStoredCredentials'; import { StorageInterface } from '../types/Storage.d'; +import type { WebAPIType } from '../textsecure/WebAPI'; import { SignalService as Proto } from '../protobuf'; import SenderCertificate = Proto.SenderCertificate; @@ -28,7 +28,7 @@ const CLOCK_SKEW_THRESHOLD = 15 * 60 * 1000; // This is exported for testing. export class SenderCertificateService { - private WebAPI?: typeof window.WebAPI; + private server?: WebAPIType; private fetchPromises: Map< SenderCertificateMode, @@ -42,19 +42,19 @@ export class SenderCertificateService { private storage?: StorageInterface; initialize({ - WebAPI, + server, navigator, onlineEventTarget, storage, }: { - WebAPI: typeof window.WebAPI; + server: WebAPIType; navigator: Readonly<{ onLine: boolean }>; onlineEventTarget: EventTarget; storage: StorageInterface; }): void { log.info('Sender certificate service initialized'); - this.WebAPI = WebAPI; + this.server = server; this.navigator = navigator; this.onlineEventTarget = onlineEventTarget; this.storage = storage; @@ -188,13 +188,12 @@ export class SenderCertificateService { private async requestSenderCertificate( mode: SenderCertificateMode ): Promise { - const { storage, WebAPI } = this; + const { server } = this; assert( - storage && WebAPI, + server, 'Sender certificate service method was called before it was initialized' ); - const server = connectToServerWithStoredCredentials(WebAPI, storage); const omitE164 = mode === SenderCertificateMode.WithoutE164; const { certificate } = await server.getSenderCertificate(omitE164); return certificate; diff --git a/ts/test-both/util/connectToServerWithStoredCredentials_test.ts b/ts/test-both/util/connectToServerWithStoredCredentials_test.ts deleted file mode 100644 index b6b8ffca2ebd..000000000000 --- a/ts/test-both/util/connectToServerWithStoredCredentials_test.ts +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { assert } from 'chai'; -import * as sinon from 'sinon'; - -import { connectToServerWithStoredCredentials } from '../../util/connectToServerWithStoredCredentials'; - -describe('connectToServerWithStoredCredentials', () => { - let fakeWebApi: any; - let fakeStorage: { get: sinon.SinonStub }; - let fakeWebApiConnect: { connect: sinon.SinonStub }; - - beforeEach(() => { - fakeWebApi = {}; - fakeStorage = { get: sinon.stub() }; - fakeWebApiConnect = { connect: sinon.stub().returns(fakeWebApi) }; - }); - - it('throws if no ID is in storage', () => { - fakeStorage.get.withArgs('password').returns('swordfish'); - - assert.throws(() => { - connectToServerWithStoredCredentials(fakeWebApiConnect, fakeStorage); - }); - }); - - it('throws if the ID in storage is not a string', () => { - fakeStorage.get.withArgs('uuid_id').returns(1234); - fakeStorage.get.withArgs('password').returns('swordfish'); - - assert.throws(() => { - connectToServerWithStoredCredentials(fakeWebApiConnect, fakeStorage); - }); - }); - - it('throws if no password is in storage', () => { - fakeStorage.get.withArgs('uuid_id').returns('foo'); - - assert.throws(() => { - connectToServerWithStoredCredentials(fakeWebApiConnect, fakeStorage); - }); - }); - - it('throws if the password in storage is not a string', () => { - fakeStorage.get.withArgs('uuid_id').returns('foo'); - fakeStorage.get.withArgs('password').returns(1234); - - assert.throws(() => { - connectToServerWithStoredCredentials(fakeWebApiConnect, fakeStorage); - }); - }); - - it('connects with the UUID ID (if available) and password', () => { - fakeStorage.get.withArgs('uuid_id').returns('foo'); - fakeStorage.get.withArgs('number_id').returns('should not be used'); - fakeStorage.get.withArgs('password').returns('swordfish'); - - connectToServerWithStoredCredentials(fakeWebApiConnect, fakeStorage); - - sinon.assert.calledWith(fakeWebApiConnect.connect, { - username: 'foo', - password: 'swordfish', - }); - }); - - it('connects with the number ID (if UUID ID not available) and password', () => { - fakeStorage.get.withArgs('number_id').returns('bar'); - fakeStorage.get.withArgs('password').returns('swordfish'); - - connectToServerWithStoredCredentials(fakeWebApiConnect, fakeStorage); - - sinon.assert.calledWith(fakeWebApiConnect.connect, { - username: 'bar', - password: 'swordfish', - }); - }); - - it('returns the connected WebAPI', () => { - fakeStorage.get.withArgs('uuid_id').returns('foo'); - fakeStorage.get.withArgs('password').returns('swordfish'); - - assert.strictEqual( - connectToServerWithStoredCredentials(fakeWebApiConnect, fakeStorage), - fakeWebApi - ); - }); -}); diff --git a/ts/test-electron/MessageReceiver_test.ts b/ts/test-electron/MessageReceiver_test.ts index 79c7eedd1ed5..1b30a2282013 100644 --- a/ts/test-electron/MessageReceiver_test.ts +++ b/ts/test-electron/MessageReceiver_test.ts @@ -11,6 +11,7 @@ import { connection as WebSocket } from 'websocket'; import MessageReceiver from '../textsecure/MessageReceiver'; import { DecryptionErrorEvent } from '../textsecure/messageReceiverEvents'; +import { WebAPIType } from '../textsecure/WebAPI'; import { SignalService as Proto } from '../protobuf'; import * as Crypto from '../Crypto'; @@ -32,15 +33,10 @@ describe('MessageReceiver', () => { it('generates decryption-error event when it cannot decrypt', done => { const socket = new FakeSocket(); - const messageReceiver = new MessageReceiver( - 'oldUsername.2', - 'username.2', - 'password', - { - serverTrustRoot: 'AAAAAAAA', - socket: socket as WebSocket, - } - ); + const messageReceiver = new MessageReceiver({} as WebAPIType, { + serverTrustRoot: 'AAAAAAAA', + socket: socket as WebSocket, + }); const body = Proto.Envelope.encode({ type: Proto.Envelope.Type.CIPHERTEXT, diff --git a/ts/test-electron/models/conversations_test.ts b/ts/test-electron/models/conversations_test.ts index 789151b304f1..0a5fe19ddf28 100644 --- a/ts/test-electron/models/conversations_test.ts +++ b/ts/test-electron/models/conversations_test.ts @@ -34,12 +34,13 @@ describe('Conversations', () => { version: 0, }); - window.textsecure.storage.user.setNumberAndDeviceId( - ourNumber, - 2, - 'my device' - ); - window.textsecure.storage.user.setUuidAndDeviceId(ourUuid, 2); + await window.textsecure.storage.user.setCredentials({ + number: ourNumber, + uuid: ourUuid, + deviceId: 2, + deviceName: 'my device', + password: 'password', + }); await window.ConversationController.loadPromise(); await window.Signal.Data.saveConversation(conversation.attributes); diff --git a/ts/test-electron/models/messages_test.ts b/ts/test-electron/models/messages_test.ts index 2e08bf36b4a7..fc05cb5e2a4f 100644 --- a/ts/test-electron/models/messages_test.ts +++ b/ts/test-electron/models/messages_test.ts @@ -7,6 +7,7 @@ import { setup as setupI18n } from '../../../js/modules/i18n'; import enMessages from '../../../_locales/en/messages.json'; import { SendStatus } from '../../messages/MessageSendState'; import MessageSender from '../../textsecure/SendMessage'; +import { WebAPIType } from '../../textsecure/WebAPI'; import { CallbackResultType } from '../../textsecure/Types.d'; import type { StorageAccessType } from '../../types/Storage.d'; import { SignalService as Proto } from '../../protobuf'; @@ -81,7 +82,7 @@ describe('Message', () => { oldMessageSender = window.textsecure.messaging; window.textsecure.messaging = - oldMessageSender ?? new MessageSender('username', 'password'); + oldMessageSender ?? new MessageSender({} as WebAPIType); this.sandbox .stub(window.textsecure.messaging, 'sendSyncMessage') .resolves({}); diff --git a/ts/test-electron/services/senderCertificate_test.ts b/ts/test-electron/services/senderCertificate_test.ts index 1ff9f9271088..967caed31df1 100644 --- a/ts/test-electron/services/senderCertificate_test.ts +++ b/ts/test-electron/services/senderCertificate_test.ts @@ -23,7 +23,6 @@ describe('SenderCertificateService', () => { let fakeValidCertificate: SenderCertificate; let fakeValidCertificateExpiry: number; let fakeServer: any; - let fakeWebApi: typeof window.WebAPI; let fakeNavigator: { onLine: boolean }; let fakeWindow: EventTarget; let fakeStorage: any; @@ -31,7 +30,7 @@ describe('SenderCertificateService', () => { function initializeTestService(): SenderCertificateService { const result = new SenderCertificateService(); result.initialize({ - WebAPI: fakeWebApi, + server: fakeServer, navigator: fakeNavigator, onlineEventTarget: fakeWindow, storage: fakeStorage, @@ -55,7 +54,6 @@ describe('SenderCertificateService', () => { ), }), }; - fakeWebApi = { connect: sinon.stub().returns(fakeServer) }; fakeNavigator = { onLine: true }; diff --git a/ts/textsecure/AccountManager.ts b/ts/textsecure/AccountManager.ts index cc18fea03c45..0922d31c8079 100644 --- a/ts/textsecure/AccountManager.ts +++ b/ts/textsecure/AccountManager.ts @@ -70,16 +70,13 @@ type GeneratedKeysType = { }; export default class AccountManager extends EventTarget { - server: WebAPIType; - pending: Promise; pendingQueue?: PQueue; - constructor(username: string, password: string) { + constructor(private readonly server: WebAPIType) { super(); - this.server = window.WebAPI.connect({ username, password }); this.pending = Promise.resolve(); } @@ -569,34 +566,27 @@ export default class AccountManager extends EventTarget { await Promise.all([ window.textsecure.storage.remove('identityKey'), - window.textsecure.storage.remove('password'), + window.textsecure.storage.user.removeCredentials(), window.textsecure.storage.remove('registrationId'), - window.textsecure.storage.remove('number_id'), - window.textsecure.storage.remove('device_name'), window.textsecure.storage.remove('regionCode'), window.textsecure.storage.remove('userAgent'), window.textsecure.storage.remove('profileKey'), window.textsecure.storage.remove('read-receipt-setting'), ]); - // `setNumberAndDeviceId` and `setUuidAndDeviceId` need to be called + // `setCredentials` needs to be called // before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes` // indirectly calls `ConversationController.getConverationId()` which // initializes the conversation for the given number (our number) which // calls out to the user storage API to get the stored UUID and number // information. - await window.textsecure.storage.user.setNumberAndDeviceId( + await window.textsecure.storage.user.setCredentials({ + uuid, number, - response.deviceId || 1, - deviceName || undefined - ); - - if (uuid) { - await window.textsecure.storage.user.setUuidAndDeviceId( - uuid, - response.deviceId || 1 - ); - } + deviceId: response.deviceId ?? 1, + deviceName: deviceName ?? undefined, + password, + }); // This needs to be done very early, because it changes how things are saved in the // database. Your identity, for example, in the saveIdentityWithAttributes call @@ -625,7 +615,6 @@ export default class AccountManager extends EventTarget { ); await window.textsecure.storage.put('identityKey', identityKeyPair); - await window.textsecure.storage.put('password', password); await window.textsecure.storage.put('registrationId', registrationId); if (profileKey) { await ourProfileKeyService.set(profileKey); diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index 4bc439c914c0..812af3d4142c 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -53,7 +53,6 @@ import { processAttachment, processDataMessage } from './processDataMessage'; import { processSyncMessage } from './processSyncMessage'; import EventTarget, { EventHandler } from './EventTarget'; import { WebAPIType } from './WebAPI'; -import utils from './Helpers'; import WebSocketResource, { IncomingWebSocketRequest, CloseEvent, @@ -186,16 +185,12 @@ class MessageReceiverInner extends EventTarget { number_id?: string; - password: string; - encryptedQueue: PQueue; decryptedQueue: PQueue; retryCachedTimeout: any; - server: WebAPIType; - serverTrustRoot: Uint8Array; socket?: WebSocket; @@ -204,10 +199,6 @@ class MessageReceiverInner extends EventTarget { stoppingProcessing?: boolean; - username: string; - - uuid: string; - uuid_id?: string; wsr?: WebSocketResource; @@ -215,9 +206,7 @@ class MessageReceiverInner extends EventTarget { private readonly reconnectBackOff = new BackOff(FIBONACCI_TIMEOUTS); constructor( - oldUsername: string, - username: string, - password: string, + public readonly server: WebAPIType, options: { serverTrustRoot: string; } @@ -227,30 +216,14 @@ class MessageReceiverInner extends EventTarget { this.count = 0; this.processedCount = 0; - this.username = oldUsername; - this.uuid = username; - this.password = password; - this.server = window.WebAPI.connect({ - username: username || oldUsername, - password, - }); - if (!options.serverTrustRoot) { throw new Error('Server trust root is required!'); } this.serverTrustRoot = Bytes.fromBase64(options.serverTrustRoot); - this.number_id = oldUsername - ? utils.unencodeNumber(oldUsername)[0] - : undefined; - this.uuid_id = username ? utils.unencodeNumber(username)[0] : undefined; - this.deviceId = - username || oldUsername - ? parseIntOrThrow( - utils.unencodeNumber(username || oldUsername)[1], - 'MessageReceiver.constructor: username || oldUsername' - ) - : undefined; + this.number_id = window.textsecure.storage.user.getNumber(); + this.uuid_id = window.textsecure.storage.user.getUuid(); + this.deviceId = window.textsecure.storage.user.getDeviceId(); this.incomingQueue = new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 }); this.appQueue = new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 }); @@ -2666,21 +2639,14 @@ export default class MessageReceiver { private readonly inner: MessageReceiverInner; constructor( - oldUsername: string, - username: string, - password: string, + server: WebAPIType, options: { serverTrustRoot: string; retryCached?: string; socket?: WebSocket; } ) { - const inner = new MessageReceiverInner( - oldUsername, - username, - password, - options - ); + const inner = new MessageReceiverInner(server, options); this.inner = inner; this.close = inner.close.bind(inner); diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index ab4606dfcec9..604c73e05b49 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -439,14 +439,11 @@ class Message { } export default class MessageSender { - server: WebAPIType; - pendingMessages: { [id: string]: PQueue; }; - constructor(username: string, password: string) { - this.server = window.WebAPI.connect({ username, password }); + constructor(public readonly server: WebAPIType) { this.pendingMessages = {}; } diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index 334008c6714e..74f46f31db32 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -23,6 +23,11 @@ export type StorageServiceCredentials = { password: string; }; +export type WebAPICredentials = { + username: string; + password: string; +}; + export type DeviceType = { id: number; identifier: string; diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index ca5c51ce0870..8f01cc79dcef 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -59,6 +59,7 @@ import { SignalService as Proto } from '../protobuf'; import { ConnectTimeoutError } from './Errors'; import MessageSender from './SendMessage'; +import { WebAPICredentials } from './Types.d'; // TODO: remove once we move away from ArrayBuffers const FIXMEU8 = Uint8Array; @@ -859,11 +860,6 @@ type InitializeOptionsType = { version: string; }; -type ConnectParametersType = { - username: string; - password: string; -}; - type MessageType = any; type AjaxOptionsType = { @@ -888,7 +884,7 @@ type AjaxOptionsType = { }; export type WebAPIConnectType = { - connect: (options: ConnectParametersType) => WebAPIType; + connect: (options: WebAPICredentials) => WebAPIType; }; export type CapabilitiesType = { @@ -1089,6 +1085,7 @@ export type WebAPIType = { getConfig: () => Promise< Array<{ name: string; enabled: boolean; value: string | null }> >; + authenticate: (credentials: WebAPICredentials) => Promise; }; export type SignedPreKeyType = { @@ -1197,7 +1194,7 @@ export function initialize({ function connect({ username: initialUsername, password: initialPassword, - }: ConnectParametersType) { + }: WebAPICredentials) { let username = initialUsername; let password = initialPassword; const PARSE_RANGE_HEADER = /\/(\d+)$/; @@ -1205,6 +1202,7 @@ export function initialize({ // Thanks, function hoisting! return { + authenticate, confirmCode, createGroup, fetchLinkPreviewImage, @@ -1307,6 +1305,14 @@ export function initialize({ }); } + async function authenticate({ + username: newUsername, + password: newPassword, + }: WebAPICredentials) { + username = newUsername; + password = newPassword; + } + async function getConfig() { type ResType = { config: Array<{ name: string; enabled: boolean; value: string | null }>; diff --git a/ts/textsecure/storage/User.ts b/ts/textsecure/storage/User.ts index e377effe4f65..7258e2252383 100644 --- a/ts/textsecure/storage/User.ts +++ b/ts/textsecure/storage/User.ts @@ -1,29 +1,35 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { EventEmitter } from 'events'; + +import { WebAPICredentials } from '../Types.d'; + import { StorageInterface } from '../../types/Storage.d'; import Helpers from '../Helpers'; -export class User { - constructor(private readonly storage: StorageInterface) {} +export type SetCredentialsOptions = { + uuid?: string; + number: string; + deviceId: number; + deviceName?: string; + password: string; +}; - public async setNumberAndDeviceId( - number: string, - deviceId: number, - deviceName?: string - ): Promise { - await this.storage.put('number_id', `${number}.${deviceId}`); - if (deviceName) { - await this.storage.put('device_name', deviceName); - } +export class User extends EventEmitter { + constructor(private readonly storage: StorageInterface) { + super(); } public async setUuidAndDeviceId( uuid: string, deviceId: number ): Promise { - return this.storage.put('uuid_id', `${uuid}.${deviceId}`); + await this.storage.put('uuid_id', `${uuid}.${deviceId}`); + + window.log.info('storage.user: uuid and device id changed'); + this.emit('credentialsChange'); } public getNumber(): string | undefined { @@ -62,6 +68,41 @@ export class User { return this.storage.remove('signaling_key'); } + public async setCredentials( + credentials: SetCredentialsOptions + ): Promise { + const { uuid, 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), + deviceName + ? this.storage.put('device_name', deviceName) + : Promise.resolve(), + ]); + + window.log.info('storage.user: credentials changed'); + this.emit('credentialsChange'); + } + + public async removeCredentials(): Promise { + await Promise.all([ + this.storage.remove('number_id'), + this.storage.remove('uuid_id'), + this.storage.remove('password'), + this.storage.remove('device_name'), + ]); + } + + public getWebAPICredentials(): WebAPICredentials { + return { + username: + this.storage.get('uuid_id') || this.storage.get('number_id') || '', + password: this.storage.get('password', ''), + }; + } + private _getDeviceIdFromUuid(): string | undefined { const uuid = this.storage.get('uuid_id'); if (uuid === undefined) return undefined; @@ -73,4 +114,25 @@ export class User { if (numberId === undefined) return undefined; return Helpers.unencodeNumber(numberId)[1]; } + + // + // EventEmitter typing + // + + public on(type: 'credentialsChange', callback: () => void): this; + + public on( + type: string | symbol, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + listener: (...args: Array) => void + ): this { + return super.on(type, listener); + } + + public emit(type: 'credentialsChange'): boolean; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public emit(type: string | symbol, ...args: Array): boolean { + return super.emit(type, ...args); + } } diff --git a/ts/util/connectToServerWithStoredCredentials.ts b/ts/util/connectToServerWithStoredCredentials.ts deleted file mode 100644 index 9e77c43458a3..000000000000 --- a/ts/util/connectToServerWithStoredCredentials.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import type { WebAPIConnectType, WebAPIType } from '../textsecure/WebAPI'; -import { StorageInterface } from '../types/Storage.d'; - -export function connectToServerWithStoredCredentials( - WebAPI: WebAPIConnectType, - storage: Pick -): WebAPIType { - const username = storage.get('uuid_id') || storage.get('number_id'); - if (typeof username !== 'string') { - throw new Error( - 'Username in storage was not a string. Cannot connect to WebAPI' - ); - } - - const password = storage.get('password'); - if (typeof password !== 'string') { - throw new Error( - 'Password in storage was not a string. Cannot connect to WebAPI' - ); - } - - return WebAPI.connect({ username, password }); -} diff --git a/ts/window.d.ts b/ts/window.d.ts index 5128e997e09c..6a0e0c154bdf 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -175,7 +175,7 @@ declare global { receivedAtCounter: number; enterKeyboardMode: () => void; enterMouseMode: () => void; - getAccountManager: () => AccountManager | undefined; + getAccountManager: () => AccountManager; getAlwaysRelayCalls: () => Promise; getBuiltInImages: () => Promise>; getCallRingtoneNotification: () => Promise;