Use single WebAPI instance across the app

This commit is contained in:
Fedor Indutny 2021-07-23 10:23:50 -07:00 committed by GitHub
parent 79633a9e7b
commit fdec47d637
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 218 additions and 308 deletions

View file

@ -2,7 +2,8 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { get, throttle } from 'lodash'; import { get, throttle } from 'lodash';
import { connectToServerWithStoredCredentials } from './util/connectToServerWithStoredCredentials';
import type { WebAPIType } from './textsecure/WebAPI';
export type ConfigKeyType = export type ConfigKeyType =
| 'desktop.announcementGroup' | 'desktop.announcementGroup'
@ -38,9 +39,9 @@ type ConfigListenersMapType = {
let config: ConfigMapType = {}; let config: ConfigMapType = {};
const listeners: ConfigListenersMapType = {}; const listeners: ConfigListenersMapType = {};
export async function initRemoteConfig(): Promise<void> { export async function initRemoteConfig(server: WebAPIType): Promise<void> {
config = window.storage.get('remoteConfig') || {}; config = window.storage.get('remoteConfig') || {};
await maybeRefreshRemoteConfig(); await maybeRefreshRemoteConfig(server);
} }
export function onChange( export function onChange(
@ -56,12 +57,10 @@ export function onChange(
}; };
} }
export const refreshRemoteConfig = async (): Promise<void> => { export const refreshRemoteConfig = async (
server: WebAPIType
): Promise<void> => {
const now = Date.now(); const now = Date.now();
const server = connectToServerWithStoredCredentials(
window.WebAPI,
window.storage
);
const newConfig = await server.getConfig(); const newConfig = await server.getConfig();
// Process new configuration in light of the old configuration // Process new configuration in light of the old configuration

View file

@ -60,7 +60,7 @@ import {
ContactEvent, ContactEvent,
GroupEvent, GroupEvent,
} from './textsecure/messageReceiverEvents'; } from './textsecure/messageReceiverEvents';
import { connectToServerWithStoredCredentials } from './util/connectToServerWithStoredCredentials'; import type { WebAPIType } from './textsecure/WebAPI';
import * as universalExpireTimer from './util/universalExpireTimer'; import * as universalExpireTimer from './util/universalExpireTimer';
import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation'; import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
import { getSendOptions } from './util/getSendOptions'; import { getSendOptions } from './util/getSendOptions';
@ -132,7 +132,24 @@ export async function startApp(): Promise<void> {
); );
} }
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); ourProfileKeyService.initialize(window.storage);
@ -153,8 +170,10 @@ export async function startApp(): Promise<void> {
const reconnectBackOff = new BackOff(FIBONACCI_TIMEOUTS); const reconnectBackOff = new BackOff(FIBONACCI_TIMEOUTS);
window.storage.onready(() => { window.storage.onready(() => {
strictAssert(server, 'WebAPI not ready');
senderCertificateService.initialize({ senderCertificateService.initialize({
WebAPI: window.WebAPI, server,
navigator, navigator,
onlineEventTarget: window, onlineEventTarget: window,
storage: window.storage, storage: window.storage,
@ -355,8 +374,7 @@ export async function startApp(): Promise<void> {
window.Whisper.KeyChangeListener.init(window.textsecure.storage.protocol); window.Whisper.KeyChangeListener.init(window.textsecure.storage.protocol);
window.textsecure.storage.protocol.on('removePreKey', () => { 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; let messageReceiver: MessageReceiver | undefined;
@ -372,32 +390,28 @@ export async function startApp(): Promise<void> {
}; };
let accountManager: typeof window.textsecure.AccountManager; let accountManager: typeof window.textsecure.AccountManager;
window.getAccountManager = () => { window.getAccountManager = () => {
if (!accountManager) { if (accountManager) {
const OLD_USERNAME = window.storage.get('number_id', ''); return accountManager;
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');
});
} }
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; return accountManager;
}; };
@ -483,6 +497,8 @@ export async function startApp(): Promise<void> {
} }
first = false; first = false;
strictAssert(server !== undefined, 'WebAPI not ready');
cleanupSessionResets(); cleanupSessionResets();
// These make key operations available to IPC handlers created in preload.js // These make key operations available to IPC handlers created in preload.js
@ -888,7 +904,7 @@ export async function startApp(): Promise<void> {
// We start this up before window.ConversationController.load() to // We start this up before window.ConversationController.load() to
// ensure that our feature flags are represented in the cached props // ensure that our feature flags are represented in the cached props
// we generate on load of each convo. // we generate on load of each convo.
window.Signal.RemoteConfig.initRemoteConfig(); window.Signal.RemoteConfig.initRemoteConfig(server);
let retryReceiptLifespan: number | undefined; let retryReceiptLifespan: number | undefined;
try { try {
@ -1724,6 +1740,7 @@ export async function startApp(): Promise<void> {
window.reduxActions.network.setChallengeStatus(challengeStatus); window.reduxActions.network.setChallengeStatus(challengeStatus);
}, },
}); });
window.Whisper.events.on('challengeResponse', response => { window.Whisper.events.on('challengeResponse', response => {
if (!challengeHandler) { if (!challengeHandler) {
throw new Error('Expected challenge handler to be there'); throw new Error('Expected challenge handler to be there');
@ -1732,13 +1749,8 @@ export async function startApp(): Promise<void> {
challengeHandler.onResponse(response); challengeHandler.onResponse(response);
}); });
window.storage.onready(async () => { // Storage is ready because `start()` is called from `storage.onready()`
if (!challengeHandler) { await challengeHandler.load();
throw new Error('Expected challenge handler to be there');
}
await challengeHandler.load();
});
window.Signal.challengeHandler = challengeHandler; window.Signal.challengeHandler = challengeHandler;
@ -1828,8 +1840,10 @@ export async function startApp(): Promise<void> {
// Maybe refresh remote configuration when we become active // Maybe refresh remote configuration when we become active
window.registerForActive(async () => { window.registerForActive(async () => {
strictAssert(server !== undefined, 'WebAPI not ready');
try { try {
await window.Signal.RemoteConfig.maybeRefreshRemoteConfig(); await window.Signal.RemoteConfig.maybeRefreshRemoteConfig(server);
} catch (error) { } catch (error) {
if (error && window._.isNumber(error.code)) { if (error && window._.isNumber(error.code)) {
window.log.warn( window.log.warn(
@ -2010,6 +2024,9 @@ export async function startApp(): Promise<void> {
window.log.warn('connect already running', { connectCount }); window.log.warn('connect already running', { connectCount });
return; return;
} }
strictAssert(server !== undefined, 'WebAPI not connected');
try { try {
connecting = true; connecting = true;
@ -2050,20 +2067,13 @@ export async function startApp(): Promise<void> {
messageReceiver = undefined; messageReceiver = undefined;
} }
const OLD_USERNAME = window.storage.get('number_id', ''); window.textsecure.messaging = new window.textsecure.MessageSender(server);
const USERNAME = window.storage.get('uuid_id', '');
const PASSWORD = window.storage.get('password', '');
window.textsecure.messaging = new window.textsecure.MessageSender(
USERNAME || OLD_USERNAME,
PASSWORD
);
if (connectCount === 0) { if (connectCount === 0) {
try { try {
// Force a re-fetch before we process our queue. We may want to turn on // Force a re-fetch before we process our queue. We may want to turn on
// something which changes how we process incoming messages! // something which changes how we process incoming messages!
await window.Signal.RemoteConfig.refreshRemoteConfig(); await window.Signal.RemoteConfig.refreshRemoteConfig(server);
} catch (error) { } catch (error) {
window.log.error( window.log.error(
'connect: Error refreshing remote config:', 'connect: Error refreshing remote config:',
@ -2109,9 +2119,7 @@ export async function startApp(): Promise<void> {
serverTrustRoot: window.getServerTrustRoot(), serverTrustRoot: window.getServerTrustRoot(),
}; };
messageReceiver = new window.textsecure.MessageReceiver( messageReceiver = new window.textsecure.MessageReceiver(
OLD_USERNAME, server,
USERNAME,
PASSWORD,
messageReceiverOptions messageReceiverOptions
); );
window.textsecure.messageReceiver = messageReceiver; window.textsecure.messageReceiver = messageReceiver;
@ -2251,8 +2259,7 @@ export async function startApp(): Promise<void> {
runStorageService(); runStorageService();
try { try {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion const manager = window.getAccountManager();
const manager = window.getAccountManager()!;
await Promise.all([ await Promise.all([
manager.maybeUpdateDeviceName(), manager.maybeUpdateDeviceName(),
window.textsecure.storage.user.removeSignalingKey(), window.textsecure.storage.user.removeSignalingKey(),
@ -2267,10 +2274,6 @@ export async function startApp(): Promise<void> {
const udSupportKey = 'hasRegisterSupportForUnauthenticatedDelivery'; const udSupportKey = 'hasRegisterSupportForUnauthenticatedDelivery';
if (!window.storage.get(udSupportKey)) { if (!window.storage.get(udSupportKey)) {
const server = connectToServerWithStoredCredentials(
window.WebAPI,
window.storage
);
try { try {
await server.registerSupportForUnauthenticatedDelivery(); await server.registerSupportForUnauthenticatedDelivery();
window.storage.put(udSupportKey, true); window.storage.put(udSupportKey, true);
@ -2286,10 +2289,6 @@ export async function startApp(): Promise<void> {
// If we didn't capture a UUID on registration, go get it from the server // If we didn't capture a UUID on registration, go get it from the server
if (!window.textsecure.storage.user.getUuid()) { if (!window.textsecure.storage.user.getUuid()) {
const server = window.WebAPI.connect({
username: OLD_USERNAME,
password: PASSWORD,
});
try { try {
const { uuid } = await server.whoami(); const { uuid } = await server.whoami();
assert(deviceId, 'We should have device id'); assert(deviceId, 'We should have device id');
@ -2311,10 +2310,6 @@ export async function startApp(): Promise<void> {
} }
if (connectCount === 1) { if (connectCount === 1) {
const server = connectToServerWithStoredCredentials(
window.WebAPI,
window.storage
);
try { try {
// Note: we always have to register our capabilities all at once, so we do this // Note: we always have to register our capabilities all at once, so we do this
// after connect on every startup // after connect on every startup

View file

@ -1,13 +1,21 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { WebAPIType } from '../textsecure/WebAPI';
import { removeStorageKeyJobQueue } from './removeStorageKeyJobQueue'; import { removeStorageKeyJobQueue } from './removeStorageKeyJobQueue';
import { reportSpamJobQueue } from './reportSpamJobQueue'; import { reportSpamJobQueue } from './reportSpamJobQueue';
/** /**
* Start all of the job queues. Should be called when the database is ready. * 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(); removeStorageKeyJobQueue.streamJobs();
reportSpamJobQueue.streamJobs(); reportSpamJobQueue.streamJobs();
} }

View file

@ -4,16 +4,17 @@
import * as z from 'zod'; import * as z from 'zod';
import * as moment from 'moment'; import * as moment from 'moment';
import { strictAssert } from '../util/assert';
import { waitForOnline } from '../util/waitForOnline'; import { waitForOnline } from '../util/waitForOnline';
import { isDone as isDeviceLinked } from '../util/registration'; import { isDone as isDeviceLinked } from '../util/registration';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { connectToServerWithStoredCredentials } from '../util/connectToServerWithStoredCredentials';
import { map } from '../util/iterables'; import { map } from '../util/iterables';
import { sleep } from '../util/sleep'; import { sleep } from '../util/sleep';
import { JobQueue } from './JobQueue'; import { JobQueue } from './JobQueue';
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
import { parseIntWithFallback } from '../util/parseIntWithFallback'; import { parseIntWithFallback } from '../util/parseIntWithFallback';
import type { WebAPIType } from '../textsecure/WebAPI';
const RETRY_WAIT_TIME = moment.duration(1, 'minute').asMilliseconds(); const RETRY_WAIT_TIME = moment.duration(1, 'minute').asMilliseconds();
const RETRYABLE_4XX_FAILURE_STATUSES = new Set([ const RETRYABLE_4XX_FAILURE_STATUSES = new Set([
@ -47,6 +48,12 @@ const reportSpamJobDataSchema = z.object({
export type ReportSpamJobData = z.infer<typeof reportSpamJobDataSchema>; export type ReportSpamJobData = z.infer<typeof reportSpamJobDataSchema>;
export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> { export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
private server?: WebAPIType;
public initialize({ server }: { server: WebAPIType }): void {
this.server = server;
}
protected parseData(data: unknown): ReportSpamJobData { protected parseData(data: unknown): ReportSpamJobData {
return reportSpamJobDataSchema.parse(data); return reportSpamJobDataSchema.parse(data);
} }
@ -67,10 +74,8 @@ export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
await waitForOnline(window.navigator, window); await waitForOnline(window.navigator, window);
const server = connectToServerWithStoredCredentials( const { server } = this;
window.WebAPI, strictAssert(server !== undefined, 'ReportSpamJobQueue not initialized');
window.storage
);
try { try {
await Promise.all( await Promise.all(

View file

@ -1678,8 +1678,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}); });
if (hadSignedPreKeyRotationError) { 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; attributesToUpdate.sendStateByConversationId = sendStateByConversationId;

View file

@ -13,8 +13,8 @@ import { missingCaseError } from '../util/missingCaseError';
import { normalizeNumber } from '../util/normalizeNumber'; import { normalizeNumber } from '../util/normalizeNumber';
import { waitForOnline } from '../util/waitForOnline'; import { waitForOnline } from '../util/waitForOnline';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { connectToServerWithStoredCredentials } from '../util/connectToServerWithStoredCredentials';
import { StorageInterface } from '../types/Storage.d'; import { StorageInterface } from '../types/Storage.d';
import type { WebAPIType } from '../textsecure/WebAPI';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import SenderCertificate = Proto.SenderCertificate; import SenderCertificate = Proto.SenderCertificate;
@ -28,7 +28,7 @@ const CLOCK_SKEW_THRESHOLD = 15 * 60 * 1000;
// This is exported for testing. // This is exported for testing.
export class SenderCertificateService { export class SenderCertificateService {
private WebAPI?: typeof window.WebAPI; private server?: WebAPIType;
private fetchPromises: Map< private fetchPromises: Map<
SenderCertificateMode, SenderCertificateMode,
@ -42,19 +42,19 @@ export class SenderCertificateService {
private storage?: StorageInterface; private storage?: StorageInterface;
initialize({ initialize({
WebAPI, server,
navigator, navigator,
onlineEventTarget, onlineEventTarget,
storage, storage,
}: { }: {
WebAPI: typeof window.WebAPI; server: WebAPIType;
navigator: Readonly<{ onLine: boolean }>; navigator: Readonly<{ onLine: boolean }>;
onlineEventTarget: EventTarget; onlineEventTarget: EventTarget;
storage: StorageInterface; storage: StorageInterface;
}): void { }): void {
log.info('Sender certificate service initialized'); log.info('Sender certificate service initialized');
this.WebAPI = WebAPI; this.server = server;
this.navigator = navigator; this.navigator = navigator;
this.onlineEventTarget = onlineEventTarget; this.onlineEventTarget = onlineEventTarget;
this.storage = storage; this.storage = storage;
@ -188,13 +188,12 @@ export class SenderCertificateService {
private async requestSenderCertificate( private async requestSenderCertificate(
mode: SenderCertificateMode mode: SenderCertificateMode
): Promise<string> { ): Promise<string> {
const { storage, WebAPI } = this; const { server } = this;
assert( assert(
storage && WebAPI, server,
'Sender certificate service method was called before it was initialized' 'Sender certificate service method was called before it was initialized'
); );
const server = connectToServerWithStoredCredentials(WebAPI, storage);
const omitE164 = mode === SenderCertificateMode.WithoutE164; const omitE164 = mode === SenderCertificateMode.WithoutE164;
const { certificate } = await server.getSenderCertificate(omitE164); const { certificate } = await server.getSenderCertificate(omitE164);
return certificate; return certificate;

View file

@ -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
);
});
});

View file

@ -11,6 +11,7 @@ import { connection as WebSocket } from 'websocket';
import MessageReceiver from '../textsecure/MessageReceiver'; import MessageReceiver from '../textsecure/MessageReceiver';
import { DecryptionErrorEvent } from '../textsecure/messageReceiverEvents'; import { DecryptionErrorEvent } from '../textsecure/messageReceiverEvents';
import { WebAPIType } from '../textsecure/WebAPI';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import * as Crypto from '../Crypto'; import * as Crypto from '../Crypto';
@ -32,15 +33,10 @@ describe('MessageReceiver', () => {
it('generates decryption-error event when it cannot decrypt', done => { it('generates decryption-error event when it cannot decrypt', done => {
const socket = new FakeSocket(); const socket = new FakeSocket();
const messageReceiver = new MessageReceiver( const messageReceiver = new MessageReceiver({} as WebAPIType, {
'oldUsername.2', serverTrustRoot: 'AAAAAAAA',
'username.2', socket: socket as WebSocket,
'password', });
{
serverTrustRoot: 'AAAAAAAA',
socket: socket as WebSocket,
}
);
const body = Proto.Envelope.encode({ const body = Proto.Envelope.encode({
type: Proto.Envelope.Type.CIPHERTEXT, type: Proto.Envelope.Type.CIPHERTEXT,

View file

@ -34,12 +34,13 @@ describe('Conversations', () => {
version: 0, version: 0,
}); });
window.textsecure.storage.user.setNumberAndDeviceId( await window.textsecure.storage.user.setCredentials({
ourNumber, number: ourNumber,
2, uuid: ourUuid,
'my device' deviceId: 2,
); deviceName: 'my device',
window.textsecure.storage.user.setUuidAndDeviceId(ourUuid, 2); password: 'password',
});
await window.ConversationController.loadPromise(); await window.ConversationController.loadPromise();
await window.Signal.Data.saveConversation(conversation.attributes); await window.Signal.Data.saveConversation(conversation.attributes);

View file

@ -7,6 +7,7 @@ import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import { SendStatus } from '../../messages/MessageSendState'; import { SendStatus } from '../../messages/MessageSendState';
import MessageSender from '../../textsecure/SendMessage'; import MessageSender from '../../textsecure/SendMessage';
import { WebAPIType } from '../../textsecure/WebAPI';
import { CallbackResultType } from '../../textsecure/Types.d'; import { CallbackResultType } from '../../textsecure/Types.d';
import type { StorageAccessType } from '../../types/Storage.d'; import type { StorageAccessType } from '../../types/Storage.d';
import { SignalService as Proto } from '../../protobuf'; import { SignalService as Proto } from '../../protobuf';
@ -81,7 +82,7 @@ describe('Message', () => {
oldMessageSender = window.textsecure.messaging; oldMessageSender = window.textsecure.messaging;
window.textsecure.messaging = window.textsecure.messaging =
oldMessageSender ?? new MessageSender('username', 'password'); oldMessageSender ?? new MessageSender({} as WebAPIType);
this.sandbox this.sandbox
.stub(window.textsecure.messaging, 'sendSyncMessage') .stub(window.textsecure.messaging, 'sendSyncMessage')
.resolves({}); .resolves({});

View file

@ -23,7 +23,6 @@ describe('SenderCertificateService', () => {
let fakeValidCertificate: SenderCertificate; let fakeValidCertificate: SenderCertificate;
let fakeValidCertificateExpiry: number; let fakeValidCertificateExpiry: number;
let fakeServer: any; let fakeServer: any;
let fakeWebApi: typeof window.WebAPI;
let fakeNavigator: { onLine: boolean }; let fakeNavigator: { onLine: boolean };
let fakeWindow: EventTarget; let fakeWindow: EventTarget;
let fakeStorage: any; let fakeStorage: any;
@ -31,7 +30,7 @@ describe('SenderCertificateService', () => {
function initializeTestService(): SenderCertificateService { function initializeTestService(): SenderCertificateService {
const result = new SenderCertificateService(); const result = new SenderCertificateService();
result.initialize({ result.initialize({
WebAPI: fakeWebApi, server: fakeServer,
navigator: fakeNavigator, navigator: fakeNavigator,
onlineEventTarget: fakeWindow, onlineEventTarget: fakeWindow,
storage: fakeStorage, storage: fakeStorage,
@ -55,7 +54,6 @@ describe('SenderCertificateService', () => {
), ),
}), }),
}; };
fakeWebApi = { connect: sinon.stub().returns(fakeServer) };
fakeNavigator = { onLine: true }; fakeNavigator = { onLine: true };

View file

@ -70,16 +70,13 @@ type GeneratedKeysType = {
}; };
export default class AccountManager extends EventTarget { export default class AccountManager extends EventTarget {
server: WebAPIType;
pending: Promise<void>; pending: Promise<void>;
pendingQueue?: PQueue; pendingQueue?: PQueue;
constructor(username: string, password: string) { constructor(private readonly server: WebAPIType) {
super(); super();
this.server = window.WebAPI.connect({ username, password });
this.pending = Promise.resolve(); this.pending = Promise.resolve();
} }
@ -569,34 +566,27 @@ export default class AccountManager extends EventTarget {
await Promise.all([ await Promise.all([
window.textsecure.storage.remove('identityKey'), window.textsecure.storage.remove('identityKey'),
window.textsecure.storage.remove('password'), window.textsecure.storage.user.removeCredentials(),
window.textsecure.storage.remove('registrationId'), 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('regionCode'),
window.textsecure.storage.remove('userAgent'), window.textsecure.storage.remove('userAgent'),
window.textsecure.storage.remove('profileKey'), window.textsecure.storage.remove('profileKey'),
window.textsecure.storage.remove('read-receipt-setting'), window.textsecure.storage.remove('read-receipt-setting'),
]); ]);
// `setNumberAndDeviceId` and `setUuidAndDeviceId` need to be called // `setCredentials` needs to be called
// before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes` // before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes`
// indirectly calls `ConversationController.getConverationId()` which // indirectly calls `ConversationController.getConverationId()` which
// initializes the conversation for the given number (our number) 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 // calls out to the user storage API to get the stored UUID and number
// information. // information.
await window.textsecure.storage.user.setNumberAndDeviceId( await window.textsecure.storage.user.setCredentials({
uuid,
number, number,
response.deviceId || 1, deviceId: response.deviceId ?? 1,
deviceName || undefined deviceName: deviceName ?? undefined,
); password,
});
if (uuid) {
await window.textsecure.storage.user.setUuidAndDeviceId(
uuid,
response.deviceId || 1
);
}
// This needs to be done very early, because it changes how things are saved in the // 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 // 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('identityKey', identityKeyPair);
await window.textsecure.storage.put('password', password);
await window.textsecure.storage.put('registrationId', registrationId); await window.textsecure.storage.put('registrationId', registrationId);
if (profileKey) { if (profileKey) {
await ourProfileKeyService.set(profileKey); await ourProfileKeyService.set(profileKey);

View file

@ -53,7 +53,6 @@ import { processAttachment, processDataMessage } from './processDataMessage';
import { processSyncMessage } from './processSyncMessage'; import { processSyncMessage } from './processSyncMessage';
import EventTarget, { EventHandler } from './EventTarget'; import EventTarget, { EventHandler } from './EventTarget';
import { WebAPIType } from './WebAPI'; import { WebAPIType } from './WebAPI';
import utils from './Helpers';
import WebSocketResource, { import WebSocketResource, {
IncomingWebSocketRequest, IncomingWebSocketRequest,
CloseEvent, CloseEvent,
@ -186,16 +185,12 @@ class MessageReceiverInner extends EventTarget {
number_id?: string; number_id?: string;
password: string;
encryptedQueue: PQueue; encryptedQueue: PQueue;
decryptedQueue: PQueue; decryptedQueue: PQueue;
retryCachedTimeout: any; retryCachedTimeout: any;
server: WebAPIType;
serverTrustRoot: Uint8Array; serverTrustRoot: Uint8Array;
socket?: WebSocket; socket?: WebSocket;
@ -204,10 +199,6 @@ class MessageReceiverInner extends EventTarget {
stoppingProcessing?: boolean; stoppingProcessing?: boolean;
username: string;
uuid: string;
uuid_id?: string; uuid_id?: string;
wsr?: WebSocketResource; wsr?: WebSocketResource;
@ -215,9 +206,7 @@ class MessageReceiverInner extends EventTarget {
private readonly reconnectBackOff = new BackOff(FIBONACCI_TIMEOUTS); private readonly reconnectBackOff = new BackOff(FIBONACCI_TIMEOUTS);
constructor( constructor(
oldUsername: string, public readonly server: WebAPIType,
username: string,
password: string,
options: { options: {
serverTrustRoot: string; serverTrustRoot: string;
} }
@ -227,30 +216,14 @@ class MessageReceiverInner extends EventTarget {
this.count = 0; this.count = 0;
this.processedCount = 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) { if (!options.serverTrustRoot) {
throw new Error('Server trust root is required!'); throw new Error('Server trust root is required!');
} }
this.serverTrustRoot = Bytes.fromBase64(options.serverTrustRoot); this.serverTrustRoot = Bytes.fromBase64(options.serverTrustRoot);
this.number_id = oldUsername this.number_id = window.textsecure.storage.user.getNumber();
? utils.unencodeNumber(oldUsername)[0] this.uuid_id = window.textsecure.storage.user.getUuid();
: undefined; this.deviceId = window.textsecure.storage.user.getDeviceId();
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.incomingQueue = new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 }); this.incomingQueue = new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 });
this.appQueue = 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; private readonly inner: MessageReceiverInner;
constructor( constructor(
oldUsername: string, server: WebAPIType,
username: string,
password: string,
options: { options: {
serverTrustRoot: string; serverTrustRoot: string;
retryCached?: string; retryCached?: string;
socket?: WebSocket; socket?: WebSocket;
} }
) { ) {
const inner = new MessageReceiverInner( const inner = new MessageReceiverInner(server, options);
oldUsername,
username,
password,
options
);
this.inner = inner; this.inner = inner;
this.close = inner.close.bind(inner); this.close = inner.close.bind(inner);

View file

@ -439,14 +439,11 @@ class Message {
} }
export default class MessageSender { export default class MessageSender {
server: WebAPIType;
pendingMessages: { pendingMessages: {
[id: string]: PQueue; [id: string]: PQueue;
}; };
constructor(username: string, password: string) { constructor(public readonly server: WebAPIType) {
this.server = window.WebAPI.connect({ username, password });
this.pendingMessages = {}; this.pendingMessages = {};
} }

View file

@ -23,6 +23,11 @@ export type StorageServiceCredentials = {
password: string; password: string;
}; };
export type WebAPICredentials = {
username: string;
password: string;
};
export type DeviceType = { export type DeviceType = {
id: number; id: number;
identifier: string; identifier: string;

View file

@ -59,6 +59,7 @@ import { SignalService as Proto } from '../protobuf';
import { ConnectTimeoutError } from './Errors'; import { ConnectTimeoutError } from './Errors';
import MessageSender from './SendMessage'; import MessageSender from './SendMessage';
import { WebAPICredentials } from './Types.d';
// TODO: remove once we move away from ArrayBuffers // TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array; const FIXMEU8 = Uint8Array;
@ -859,11 +860,6 @@ type InitializeOptionsType = {
version: string; version: string;
}; };
type ConnectParametersType = {
username: string;
password: string;
};
type MessageType = any; type MessageType = any;
type AjaxOptionsType = { type AjaxOptionsType = {
@ -888,7 +884,7 @@ type AjaxOptionsType = {
}; };
export type WebAPIConnectType = { export type WebAPIConnectType = {
connect: (options: ConnectParametersType) => WebAPIType; connect: (options: WebAPICredentials) => WebAPIType;
}; };
export type CapabilitiesType = { export type CapabilitiesType = {
@ -1089,6 +1085,7 @@ export type WebAPIType = {
getConfig: () => Promise< getConfig: () => Promise<
Array<{ name: string; enabled: boolean; value: string | null }> Array<{ name: string; enabled: boolean; value: string | null }>
>; >;
authenticate: (credentials: WebAPICredentials) => Promise<void>;
}; };
export type SignedPreKeyType = { export type SignedPreKeyType = {
@ -1197,7 +1194,7 @@ export function initialize({
function connect({ function connect({
username: initialUsername, username: initialUsername,
password: initialPassword, password: initialPassword,
}: ConnectParametersType) { }: WebAPICredentials) {
let username = initialUsername; let username = initialUsername;
let password = initialPassword; let password = initialPassword;
const PARSE_RANGE_HEADER = /\/(\d+)$/; const PARSE_RANGE_HEADER = /\/(\d+)$/;
@ -1205,6 +1202,7 @@ export function initialize({
// Thanks, function hoisting! // Thanks, function hoisting!
return { return {
authenticate,
confirmCode, confirmCode,
createGroup, createGroup,
fetchLinkPreviewImage, fetchLinkPreviewImage,
@ -1307,6 +1305,14 @@ export function initialize({
}); });
} }
async function authenticate({
username: newUsername,
password: newPassword,
}: WebAPICredentials) {
username = newUsername;
password = newPassword;
}
async function getConfig() { async function getConfig() {
type ResType = { type ResType = {
config: Array<{ name: string; enabled: boolean; value: string | null }>; config: Array<{ name: string; enabled: boolean; value: string | null }>;

View file

@ -1,29 +1,35 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { EventEmitter } from 'events';
import { WebAPICredentials } from '../Types.d';
import { StorageInterface } from '../../types/Storage.d'; import { StorageInterface } from '../../types/Storage.d';
import Helpers from '../Helpers'; import Helpers from '../Helpers';
export class User { export type SetCredentialsOptions = {
constructor(private readonly storage: StorageInterface) {} uuid?: string;
number: string;
deviceId: number;
deviceName?: string;
password: string;
};
public async setNumberAndDeviceId( export class User extends EventEmitter {
number: string, constructor(private readonly storage: StorageInterface) {
deviceId: number, super();
deviceName?: string
): Promise<void> {
await this.storage.put('number_id', `${number}.${deviceId}`);
if (deviceName) {
await this.storage.put('device_name', deviceName);
}
} }
public async setUuidAndDeviceId( public async setUuidAndDeviceId(
uuid: string, uuid: string,
deviceId: number deviceId: number
): Promise<void> { ): Promise<void> {
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 { public getNumber(): string | undefined {
@ -62,6 +68,41 @@ export class User {
return this.storage.remove('signaling_key'); return this.storage.remove('signaling_key');
} }
public async setCredentials(
credentials: SetCredentialsOptions
): Promise<void> {
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<void> {
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 { private _getDeviceIdFromUuid(): string | undefined {
const uuid = this.storage.get('uuid_id'); const uuid = this.storage.get('uuid_id');
if (uuid === undefined) return undefined; if (uuid === undefined) return undefined;
@ -73,4 +114,25 @@ export class User {
if (numberId === undefined) return undefined; if (numberId === undefined) return undefined;
return Helpers.unencodeNumber(numberId)[1]; 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<any>) => 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<any>): boolean {
return super.emit(type, ...args);
}
} }

View file

@ -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<StorageInterface, 'get'>
): 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 });
}

2
ts/window.d.ts vendored
View file

@ -175,7 +175,7 @@ declare global {
receivedAtCounter: number; receivedAtCounter: number;
enterKeyboardMode: () => void; enterKeyboardMode: () => void;
enterMouseMode: () => void; enterMouseMode: () => void;
getAccountManager: () => AccountManager | undefined; getAccountManager: () => AccountManager;
getAlwaysRelayCalls: () => Promise<boolean>; getAlwaysRelayCalls: () => Promise<boolean>;
getBuiltInImages: () => Promise<Array<string>>; getBuiltInImages: () => Promise<Array<string>>;
getCallRingtoneNotification: () => Promise<boolean>; getCallRingtoneNotification: () => Promise<boolean>;