From 8f5086227a370ac2247d18a36afb7f5ce8682927 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Mon, 14 Jun 2021 17:09:37 -0700 Subject: [PATCH] Enforce stronger types for ArrayBuffers and storage --- background.html | 4 - js/models/blockedNumbers.js | 101 ----------- js/storage.js | 131 -------------- libtextsecure/protocol_wrapper.js | 7 +- libtextsecure/storage/unprocessed.js | 43 ----- libtextsecure/storage/user.js | 70 -------- libtextsecure/test/index.html | 6 - sticker-creator/index.html | 3 - test/index.html | 6 - ts/RemoteConfig.ts | 2 +- ts/SignalProtocolStore.ts | 7 +- ts/background.ts | 57 +++--- ts/challenge.ts | 6 +- ts/groups.ts | 8 +- ts/jobs/removeStorageKeyJobQueue.ts | 2 +- ts/models/conversations.ts | 25 +-- ts/models/messages.ts | 5 +- ts/routineProfileRefresh.ts | 14 +- ts/services/calling.ts | 9 +- ts/services/ourProfileKey.ts | 12 +- ts/services/senderCertificate.ts | 12 +- ts/services/storage.ts | 30 +++- ts/services/storageRecordOps.ts | 6 +- ts/shims/storage.ts | 11 +- ts/sql/Client.ts | 38 ++-- ts/sql/Interface.ts | 17 +- ts/sql/Server.ts | 29 ++- ts/state/ducks/items.ts | 14 +- ts/test-both/challenge_test.ts | 4 +- ts/test-both/util/getProvisioningUrl_test.ts | 4 +- ts/test-both/util/retryPlaceholders_test.ts | 4 +- ts/test-both/util/synchronousCrypto_test.ts | 6 +- ts/test-electron/SignalProtocolStore_test.ts | 16 +- ts/test-electron/WebsocketResources_test.ts | 12 +- ts/test-electron/background_test.ts | 7 +- ts/test-electron/models/messages_test.ts | 4 +- .../routineProfileRefresh_test.ts | 2 +- ts/textsecure.d.ts | 45 +---- ts/textsecure/AccountManager.ts | 8 +- ts/textsecure/MessageReceiver.ts | 28 +-- ts/textsecure/OutgoingMessage.ts | 3 +- ts/textsecure/SendMessage.ts | 34 ++-- ts/textsecure/Storage.ts | 170 ++++++++++++++---- ts/textsecure/Types.d.ts | 12 ++ ts/textsecure/WebsocketResources.ts | 3 +- ts/textsecure/index.ts | 4 +- ts/textsecure/storage/Blocked.ts | 98 ++++++++++ ts/textsecure/storage/User.ts | 76 ++++++++ ts/types/Colors.ts | 5 + ts/types/Storage.d.ts | 133 ++++++++++++++ .../connectToServerWithStoredCredentials.ts | 8 +- ts/util/retryPlaceholders.ts | 2 +- ts/util/safetyNumber.ts | 5 +- ts/util/sendToGroup.ts | 5 +- ts/views/conversation_view.ts | 10 +- ts/window.d.ts | 40 ++--- 56 files changed, 748 insertions(+), 675 deletions(-) delete mode 100644 js/models/blockedNumbers.js delete mode 100644 js/storage.js delete mode 100644 libtextsecure/storage/unprocessed.js delete mode 100644 libtextsecure/storage/user.js create mode 100644 ts/textsecure/storage/Blocked.ts create mode 100644 ts/textsecure/storage/User.ts create mode 100644 ts/types/Storage.d.ts diff --git a/background.html b/background.html index e5e3aad232..c01692a931 100644 --- a/background.html +++ b/background.html @@ -332,11 +332,8 @@ - - - @@ -348,7 +345,6 @@ - diff --git a/js/models/blockedNumbers.js b/js/models/blockedNumbers.js deleted file mode 100644 index 852564663e..0000000000 --- a/js/models/blockedNumbers.js +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2016-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global storage, _ */ - -// eslint-disable-next-line func-names -(function () { - const BLOCKED_NUMBERS_ID = 'blocked'; - const BLOCKED_UUIDS_ID = 'blocked-uuids'; - const BLOCKED_GROUPS_ID = 'blocked-groups'; - - function getArray(key) { - const result = storage.get(key, []); - - if (!Array.isArray(result)) { - window.log.error( - `Expected storage key ${JSON.stringify( - key - )} to contain an array or nothing` - ); - return []; - } - - return result; - } - - storage.getBlockedNumbers = () => getArray(BLOCKED_NUMBERS_ID); - storage.isBlocked = number => { - const numbers = storage.getBlockedNumbers(); - - return _.include(numbers, number); - }; - storage.addBlockedNumber = number => { - const numbers = storage.getBlockedNumbers(); - if (_.include(numbers, number)) { - return; - } - - window.log.info('adding', number, 'to blocked list'); - storage.put(BLOCKED_NUMBERS_ID, numbers.concat(number)); - }; - storage.removeBlockedNumber = number => { - const numbers = storage.getBlockedNumbers(); - if (!_.include(numbers, number)) { - return; - } - - window.log.info('removing', number, 'from blocked list'); - storage.put(BLOCKED_NUMBERS_ID, _.without(numbers, number)); - }; - - storage.getBlockedUuids = () => getArray(BLOCKED_UUIDS_ID); - storage.isUuidBlocked = uuid => { - const uuids = storage.getBlockedUuids(); - - return _.include(uuids, uuid); - }; - storage.addBlockedUuid = uuid => { - const uuids = storage.getBlockedUuids(); - if (_.include(uuids, uuid)) { - return; - } - - window.log.info('adding', uuid, 'to blocked list'); - storage.put(BLOCKED_UUIDS_ID, uuids.concat(uuid)); - }; - storage.removeBlockedUuid = uuid => { - const numbers = storage.getBlockedUuids(); - if (!_.include(numbers, uuid)) { - return; - } - - window.log.info('removing', uuid, 'from blocked list'); - storage.put(BLOCKED_UUIDS_ID, _.without(numbers, uuid)); - }; - - storage.getBlockedGroups = () => getArray(BLOCKED_GROUPS_ID); - storage.isGroupBlocked = groupId => { - const groupIds = storage.getBlockedGroups(); - - return _.include(groupIds, groupId); - }; - storage.addBlockedGroup = groupId => { - const groupIds = storage.getBlockedGroups(); - if (_.include(groupIds, groupId)) { - return; - } - - window.log.info(`adding group(${groupId}) to blocked list`); - storage.put(BLOCKED_GROUPS_ID, groupIds.concat(groupId)); - }; - storage.removeBlockedGroup = groupId => { - const groupIds = storage.getBlockedGroups(); - if (!_.include(groupIds, groupId)) { - return; - } - - window.log.info(`removing group(${groupId} from blocked list`); - storage.put(BLOCKED_GROUPS_ID, _.without(groupIds, groupId)); - }; -})(); diff --git a/js/storage.js b/js/storage.js deleted file mode 100644 index 47eb94b4b2..0000000000 --- a/js/storage.js +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2014-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global _ */ -/* eslint-disable more/no-then */ - -// eslint-disable-next-line func-names -(function () { - window.Whisper = window.Whisper || {}; - - let ready = false; - let items; - let callbacks = []; - - reset(); - - async function put(key, value) { - if (value === undefined) { - window.log.warn(`storage/put: undefined provided for key ${key}`); - } - if (!ready) { - window.log.warn('Called storage.put before storage is ready. key:', key); - } - - const data = { id: key, value }; - - items[key] = data; - await window.Signal.Data.createOrUpdateItem(data); - - if (_.has(window, ['reduxActions', 'items', 'putItemExternal'])) { - window.reduxActions.items.putItemExternal(key, value); - } - } - - function get(key, defaultValue) { - if (!ready) { - window.log.warn('Called storage.get before storage is ready. key:', key); - } - - const item = items[key]; - if (!item) { - return defaultValue; - } - - return item.value; - } - - async function remove(key) { - if (!ready) { - window.log.warn( - 'Called storage.remove before storage is ready. key:', - key - ); - } - - delete items[key]; - await window.Signal.Data.removeItemById(key); - - if (_.has(window, ['reduxActions', 'items', 'removeItemExternal'])) { - window.reduxActions.items.removeItemExternal(key); - } - } - - function onready(callback) { - if (ready) { - callback(); - } else { - callbacks.push(callback); - } - } - - function callListeners() { - if (ready) { - callbacks.forEach(callback => callback()); - callbacks = []; - } - } - - async function fetch() { - this.reset(); - const array = await window.Signal.Data.getAllItems(); - - for (let i = 0, max = array.length; i < max; i += 1) { - const item = array[i]; - const { id } = item; - items[id] = item; - } - - ready = true; - callListeners(); - } - - function getItemsState() { - const data = _.clone(items); - const ids = Object.keys(data); - ids.forEach(id => { - data[id] = data[id].value; - }); - - return data; - } - - function reset() { - ready = false; - items = Object.create(null); - } - - const storage = { - fetch, - put, - get, - getItemsState, - remove, - onready, - reset, - }; - - // Keep a reference to this storage system, since there are scenarios where - // we need to replace it with the legacy storage system for a while. - window.newStorage = storage; - - window.textsecure = window.textsecure || {}; - window.textsecure.storage = window.textsecure.storage || {}; - - window.installStorage = newStorage => { - window.storage = newStorage; - window.textsecure.storage.impl = newStorage; - }; - - window.installStorage(storage); -})(); diff --git a/libtextsecure/protocol_wrapper.js b/libtextsecure/protocol_wrapper.js index 5445e5f557..0f43e029e5 100644 --- a/libtextsecure/protocol_wrapper.js +++ b/libtextsecure/protocol_wrapper.js @@ -1,12 +1,9 @@ // Copyright 2016-2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -/* global window, textsecure, SignalProtocolStore */ +/* global window, SignalProtocolStore */ // eslint-disable-next-line func-names (function () { - window.textsecure = window.textsecure || {}; - window.textsecure.storage = window.textsecure.storage || {}; - - textsecure.storage.protocol = new SignalProtocolStore(); + window.textsecure.storage.protocol = new SignalProtocolStore(); })(); diff --git a/libtextsecure/storage/unprocessed.js b/libtextsecure/storage/unprocessed.js deleted file mode 100644 index 0ce444b4b9..0000000000 --- a/libtextsecure/storage/unprocessed.js +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2017-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global window, textsecure */ - -// eslint-disable-next-line func-names -(function () { - /** *************************************** - *** Not-yet-processed message storage *** - **************************************** */ - window.textsecure = window.textsecure || {}; - window.textsecure.storage = window.textsecure.storage || {}; - - window.textsecure.storage.unprocessed = { - getCount() { - return textsecure.storage.protocol.getUnprocessedCount(); - }, - getAll() { - return textsecure.storage.protocol.getAllUnprocessed(); - }, - get(id) { - return textsecure.storage.protocol.getUnprocessedById(id); - }, - updateAttempts(id, attempts) { - return textsecure.storage.protocol.updateUnprocessedAttempts( - id, - attempts - ); - }, - addDecryptedData(id, data) { - return textsecure.storage.protocol.updateUnprocessedWithData(id, data); - }, - addDecryptedDataToList(array) { - return textsecure.storage.protocol.updateUnprocessedsWithData(array); - }, - remove(idOrArray) { - return textsecure.storage.protocol.removeUnprocessed(idOrArray); - }, - removeAll() { - return textsecure.storage.protocol.removeAllUnprocessed(); - }, - }; -})(); diff --git a/libtextsecure/storage/user.js b/libtextsecure/storage/user.js deleted file mode 100644 index 6a1ea686da..0000000000 --- a/libtextsecure/storage/user.js +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2015-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global textsecure, window */ - -// eslint-disable-next-line func-names -(function () { - /** ******************************************* - *** Utilities to store data about the user *** - ********************************************* */ - window.textsecure = window.textsecure || {}; - window.textsecure.storage = window.textsecure.storage || {}; - - window.textsecure.storage.user = { - setNumberAndDeviceId(number, deviceId, deviceName) { - textsecure.storage.put('number_id', `${number}.${deviceId}`); - if (deviceName) { - textsecure.storage.put('device_name', deviceName); - } - }, - - setUuidAndDeviceId(uuid, deviceId) { - textsecure.storage.put('uuid_id', `${uuid}.${deviceId}`); - }, - - getNumber() { - const numberId = textsecure.storage.get('number_id'); - if (numberId === undefined) return undefined; - return textsecure.utils.unencodeNumber(numberId)[0]; - }, - - getUuid() { - const uuid = textsecure.storage.get('uuid_id'); - if (uuid === undefined) return undefined; - return textsecure.utils.unencodeNumber(uuid.toLowerCase())[0]; - }, - - getDeviceId() { - return this._getDeviceIdFromUuid() || this._getDeviceIdFromNumber(); - }, - - _getDeviceIdFromUuid() { - const uuid = textsecure.storage.get('uuid_id'); - if (uuid === undefined) return undefined; - return textsecure.utils.unencodeNumber(uuid)[1]; - }, - - _getDeviceIdFromNumber() { - const numberId = textsecure.storage.get('number_id'); - if (numberId === undefined) return undefined; - return textsecure.utils.unencodeNumber(numberId)[1]; - }, - - getDeviceName() { - return textsecure.storage.get('device_name'); - }, - - setDeviceNameEncrypted() { - return textsecure.storage.put('deviceNameEncrypted', true); - }, - - getDeviceNameEncrypted() { - return textsecure.storage.get('deviceNameEncrypted'); - }, - - getSignalingKey() { - return textsecure.storage.get('signaling_key'); - }, - }; -})(); diff --git a/libtextsecure/test/index.html b/libtextsecure/test/index.html index 9c59b3f2ba..bda21e922d 100644 --- a/libtextsecure/test/index.html +++ b/libtextsecure/test/index.html @@ -22,18 +22,12 @@ - - - - - - diff --git a/sticker-creator/index.html b/sticker-creator/index.html index 76eb698ab0..ef53cedcfc 100644 --- a/sticker-creator/index.html +++ b/sticker-creator/index.html @@ -10,11 +10,8 @@
- - - diff --git a/test/index.html b/test/index.html index b3db3f31d7..40f350b0f9 100644 --- a/test/index.html +++ b/test/index.html @@ -342,16 +342,11 @@ - - - - - @@ -375,7 +370,6 @@ - diff --git a/ts/RemoteConfig.ts b/ts/RemoteConfig.ts index 9d18cf1797..84ca90343c 100644 --- a/ts/RemoteConfig.ts +++ b/ts/RemoteConfig.ts @@ -26,7 +26,7 @@ type ConfigValueType = { enabledAt?: number; value?: unknown; }; -type ConfigMapType = { [key: string]: ConfigValueType }; +export type ConfigMapType = { [key: string]: ConfigValueType }; type ConfigListenerType = (value: ConfigValueType) => unknown; type ConfigListenersMapType = { [key: string]: Array; diff --git a/ts/SignalProtocolStore.ts b/ts/SignalProtocolStore.ts index dd89aca075..e69be54837 100644 --- a/ts/SignalProtocolStore.ts +++ b/ts/SignalProtocolStore.ts @@ -36,6 +36,7 @@ import { KeyPairType, IdentityKeyType, SenderKeyType, + SessionResetsType, SessionType, SignedPreKeyType, OuterSignedPrekeyType, @@ -114,8 +115,6 @@ type MapFields = | 'sessions' | 'signedPreKeys'; -type SessionResetsType = Record; - export type SessionTransactionOptions = { readonly zone?: Zone; }; @@ -1199,8 +1198,8 @@ export class SignalProtocolStore extends EventsMixin { const sessionResets = window.storage.get( 'sessionResets', - {} - ) as SessionResetsType; + {} + ); const lastReset = sessionResets[id]; diff --git a/ts/background.ts b/ts/background.ts index 49551cecf3..f1e924aa97 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -9,6 +9,7 @@ import { } from '@signalapp/signal-client'; import { DataMessageClass } from './textsecure.d'; +import { SessionResetsType } from './textsecure/Types.d'; import { MessageAttributesType } from './model-types.d'; import { WhatIsThis } from './window.d'; import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings'; @@ -49,11 +50,10 @@ export function isOverHourIntoPast(timestamp: number): boolean { return isNumber(timestamp) && isOlderThan(timestamp, HOUR); } -type SessionResetsType = Record; export async function cleanupSessionResets(): Promise { - const sessionResets = window.storage.get( + const sessionResets = window.storage.get( 'sessionResets', - {} + {} ); const keys = Object.keys(sessionResets); @@ -326,9 +326,9 @@ 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'); + 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 @@ -498,8 +498,7 @@ export async function startApp(): Promise { getAutoLaunch: () => window.getAutoLaunch(), setAutoLaunch: (value: boolean) => window.setAutoLaunch(value), - // eslint-disable-next-line eqeqeq - isPrimary: () => window.textsecure.storage.user.getDeviceId() == '1', + isPrimary: () => window.textsecure.storage.user.getDeviceId() === 1, getSyncRequest: () => new Promise((resolve, reject) => { const FIVE_MINUTES = 5 * 60 * 60 * 1000; @@ -680,7 +679,7 @@ export async function startApp(): Promise { }; // How long since we were last running? - const lastHeartbeat = window.storage.get('lastHeartbeat'); + const lastHeartbeat = window.storage.get('lastHeartbeat', 0); await window.storage.put('lastStartup', Date.now()); const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000; @@ -1962,10 +1961,13 @@ export async function startApp(): Promise { messageReceiver = null; } - const OLD_USERNAME = window.storage.get('number_id'); - const USERNAME = window.storage.get('uuid_id'); - const PASSWORD = window.storage.get('password'); - const mySignalingKey = window.storage.get('signaling_key'); + const OLD_USERNAME = window.storage.get('number_id', ''); + const USERNAME = window.storage.get('uuid_id', ''); + const PASSWORD = window.storage.get('password', ''); + const mySignalingKey = window.storage.get( + 'signaling_key', + new ArrayBuffer(0) + ); window.textsecure.messaging = new window.textsecure.MessageSender( USERNAME || OLD_USERNAME, @@ -2097,8 +2099,7 @@ export async function startApp(): Promise { !firstRun && connectCount === 1 && newVersion && - // eslint-disable-next-line eqeqeq - window.textsecure.storage.user.getDeviceId() != '1' + window.textsecure.storage.user.getDeviceId() !== 1 ) { window.log.info('Boot after upgrading. Requesting contact sync'); window.getSyncRequest(); @@ -2147,11 +2148,11 @@ export async function startApp(): Promise { }); try { const { uuid } = await server.whoami(); - window.textsecure.storage.user.setUuidAndDeviceId( - uuid, - deviceId as WhatIsThis - ); + assert(deviceId, 'We should have device id'); + window.textsecure.storage.user.setUuidAndDeviceId(uuid, deviceId); const ourNumber = window.textsecure.storage.user.getNumber(); + + assert(ourNumber, 'We should have number'); const me = await window.ConversationController.getOrCreateAndWait( ourNumber, 'private' @@ -2188,7 +2189,7 @@ export async function startApp(): Promise { } } - if (firstRun === true && deviceId !== '1') { + if (firstRun === true && deviceId !== 1) { const hasThemeSetting = Boolean(window.storage.get('theme-setting')); if ( !hasThemeSetting && @@ -3339,7 +3340,9 @@ export async function startApp(): Promise { // These two bits of data are important to ensure that the app loads up // the conversation list, instead of showing just the QR code screen. window.Signal.Util.Registration.markEverDone(); - await window.textsecure.storage.put(NUMBER_ID_KEY, previousNumberId); + if (previousNumberId !== undefined) { + await window.textsecure.storage.put(NUMBER_ID_KEY, previousNumberId); + } // These two are important to ensure we don't rip through every message // in the database attempting to upgrade it after starting up again. @@ -3347,10 +3350,14 @@ export async function startApp(): Promise { IS_MIGRATION_COMPLETE_KEY, isMigrationComplete || false ); - await window.textsecure.storage.put( - LAST_PROCESSED_INDEX_KEY, - lastProcessedIndex || null - ); + if (lastProcessedIndex !== undefined) { + await window.textsecure.storage.put( + LAST_PROCESSED_INDEX_KEY, + lastProcessedIndex + ); + } else { + await window.textsecure.storage.remove(LAST_PROCESSED_INDEX_KEY); + } await window.textsecure.storage.put(VERSION_KEY, window.getVersion()); window.log.info('Successfully cleared local configuration'); diff --git a/ts/challenge.ts b/ts/challenge.ts index 6edc0b6bb1..9437d0de6a 100644 --- a/ts/challenge.ts +++ b/ts/challenge.ts @@ -19,6 +19,7 @@ import { isNotNil } from './util/isNotNil'; import { isOlderThan } from './util/timestamp'; import { parseRetryAfter } from './util/parseRetryAfter'; import { getEnvironment, Environment } from './environment'; +import { StorageInterface } from './types/Storage.d'; export type ChallengeResponse = { readonly captcha: string; @@ -62,10 +63,7 @@ export type MinimalMessage = Pick< }; export type Options = { - readonly storage: { - get(key: string): ReadonlyArray; - put(key: string, value: ReadonlyArray): Promise; - }; + readonly storage: Pick; requestChallenge(request: IPCRequest): void; diff --git a/ts/groups.ts b/ts/groups.ts index 1b1bfc16b6..9124bd906c 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -2171,8 +2171,8 @@ export async function initiateMigrationToGroupV2( }, }); - if (window.storage.isGroupBlocked(previousGroupV1Id)) { - window.storage.addBlockedGroup(groupId); + if (window.storage.blocked.isGroupBlocked(previousGroupV1Id)) { + window.storage.blocked.addBlockedGroup(groupId); } // Save these most recent updates to conversation @@ -2646,8 +2646,8 @@ export async function respondToGroupV2Migration({ }, }); - if (window.storage.isGroupBlocked(previousGroupV1Id)) { - window.storage.addBlockedGroup(groupId); + if (window.storage.blocked.isGroupBlocked(previousGroupV1Id)) { + window.storage.blocked.addBlockedGroup(groupId); } // Save these most recent updates to conversation diff --git a/ts/jobs/removeStorageKeyJobQueue.ts b/ts/jobs/removeStorageKeyJobQueue.ts index d385c52c5f..a21a905c8d 100644 --- a/ts/jobs/removeStorageKeyJobQueue.ts +++ b/ts/jobs/removeStorageKeyJobQueue.ts @@ -7,7 +7,7 @@ import { JobQueue } from './JobQueue'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; const removeStorageKeyJobDataSchema = z.object({ - key: z.string().min(1), + key: z.enum(['senderCertificateWithUuid']), }); type RemoveStorageKeyJobData = z.infer; diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 370520862e..444858e7aa 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -816,17 +816,17 @@ export class ConversationModel extends window.Backbone isBlocked(): boolean { const uuid = this.get('uuid'); if (uuid) { - return window.storage.isUuidBlocked(uuid); + return window.storage.blocked.isUuidBlocked(uuid); } const e164 = this.get('e164'); if (e164) { - return window.storage.isBlocked(e164); + return window.storage.blocked.isBlocked(e164); } const groupId = this.get('groupId'); if (groupId) { - return window.storage.isGroupBlocked(groupId); + return window.storage.blocked.isGroupBlocked(groupId); } return false; @@ -838,19 +838,19 @@ export class ConversationModel extends window.Backbone const uuid = this.get('uuid'); if (uuid) { - window.storage.addBlockedUuid(uuid); + window.storage.blocked.addBlockedUuid(uuid); blocked = true; } const e164 = this.get('e164'); if (e164) { - window.storage.addBlockedNumber(e164); + window.storage.blocked.addBlockedNumber(e164); blocked = true; } const groupId = this.get('groupId'); if (groupId) { - window.storage.addBlockedGroup(groupId); + window.storage.blocked.addBlockedGroup(groupId); blocked = true; } @@ -865,19 +865,19 @@ export class ConversationModel extends window.Backbone const uuid = this.get('uuid'); if (uuid) { - window.storage.removeBlockedUuid(uuid); + window.storage.blocked.removeBlockedUuid(uuid); unblocked = true; } const e164 = this.get('e164'); if (e164) { - window.storage.removeBlockedNumber(e164); + window.storage.blocked.removeBlockedNumber(e164); unblocked = true; } const groupId = this.get('groupId'); if (groupId) { - window.storage.removeBlockedGroup(groupId); + window.storage.blocked.removeBlockedGroup(groupId); unblocked = true; } @@ -2913,6 +2913,9 @@ export class ConversationModel extends window.Backbone validateNumber(): string | null { if (isDirectConversation(this.attributes) && this.get('e164')) { const regionCode = window.storage.get('regionCode'); + if (!regionCode) { + throw new Error('No region code'); + } const number = window.libphonenumber.util.parseNumber( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.get('e164')!, @@ -5256,7 +5259,7 @@ export class ConversationModel extends window.Backbone window.log.info('pinning', this.idForLogging()); const pinnedConversationIds = new Set( - window.storage.get>('pinnedConversationIds', []) + window.storage.get('pinnedConversationIds', new Array()) ); pinnedConversationIds.add(this.id); @@ -5279,7 +5282,7 @@ export class ConversationModel extends window.Backbone window.log.info('un-pinning', this.idForLogging()); const pinnedConversationIds = new Set( - window.storage.get>('pinnedConversationIds', []) + window.storage.get('pinnedConversationIds', new Array()) ); pinnedConversationIds.delete(this.id); diff --git a/ts/models/messages.ts b/ts/models/messages.ts index 5357297e65..401173e57c 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -507,7 +507,7 @@ export class MessageModel extends window.Backbone.Model { _.find(errorsForContact, error => error.name === OUTGOING_KEY_ERROR) ); const isUnidentifiedDelivery = - window.storage.get('unidentifiedDeliveryIndicators') && + window.storage.get('unidentifiedDeliveryIndicators', false) && this.isUnidentifiedDelivery(id, unidentifiedLookup); return { @@ -1189,6 +1189,9 @@ export class MessageModel extends window.Backbone.Model { } const regionCode = window.storage.get('regionCode'); + if (!regionCode) { + throw new Error('No region code'); + } const { contactSelector } = Contact; const contact = contacts[0]; const firstNumber = diff --git a/ts/routineProfileRefresh.ts b/ts/routineProfileRefresh.ts index ebc8c2ae83..991461fa7b 100644 --- a/ts/routineProfileRefresh.ts +++ b/ts/routineProfileRefresh.ts @@ -14,6 +14,7 @@ import { isNormalNumber } from './util/isNormalNumber'; import { take } from './util/iterables'; import { isOlderThan } from './util/timestamp'; import { ConversationModel } from './models/conversations'; +import { StorageInterface } from './types/Storage.d'; const STORAGE_KEY = 'lastAttemptedToRefreshProfilesAt'; const MAX_AGE_TO_BE_CONSIDERED_ACTIVE = 30 * 24 * 60 * 60 * 1000; @@ -21,13 +22,6 @@ const MAX_AGE_TO_BE_CONSIDERED_RECENTLY_REFRESHED = 1 * 24 * 60 * 60 * 1000; const MAX_CONVERSATIONS_TO_REFRESH = 50; const MIN_ELAPSED_DURATION_TO_REFRESH_AGAIN = 12 * 3600 * 1000; -// This type is a little stricter than what's on `window.storage`, and only requires what -// we need for easier testing. -type StorageType = { - get: (key: string) => unknown; - put: (key: string, value: unknown) => Promise; -}; - export async function routineProfileRefresh({ allConversations, ourConversationId, @@ -35,7 +29,7 @@ export async function routineProfileRefresh({ }: { allConversations: Array; ourConversationId: string; - storage: StorageType; + storage: Pick; }): Promise { log.info('routineProfileRefresh: starting'); @@ -93,7 +87,9 @@ export async function routineProfileRefresh({ ); } -function hasEnoughTimeElapsedSinceLastRefresh(storage: StorageType): boolean { +function hasEnoughTimeElapsedSinceLastRefresh( + storage: Pick +): boolean { const storedValue = storage.get(STORAGE_KEY); if (isNil(storedValue)) { diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 7657478f2b..582e1819d9 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -55,6 +55,7 @@ import { uuidToArrayBuffer, arrayBufferToUuid, } from '../Crypto'; +import { assert } from '../util/assert'; import { getOwn } from '../util/getOwn'; import { fetchMembershipProof, @@ -1257,9 +1258,13 @@ export class CallingClass { } const senderIdentityKey = senderIdentityRecord.publicKey.slice(1); // Ignore the type header, it is not used. - const receiverIdentityRecord = window.textsecure.storage.protocol.getIdentityRecord( + const ourIdentifier = window.textsecure.storage.user.getUuid() || - window.textsecure.storage.user.getNumber() + window.textsecure.storage.user.getNumber(); + assert(ourIdentifier, 'We should have either uuid or number'); + + const receiverIdentityRecord = window.textsecure.storage.protocol.getIdentityRecord( + ourIdentifier ); if (!receiverIdentityRecord) { window.log.error( diff --git a/ts/services/ourProfileKey.ts b/ts/services/ourProfileKey.ts index f388dc5b6b..234ea9919a 100644 --- a/ts/services/ourProfileKey.ts +++ b/ts/services/ourProfileKey.ts @@ -4,22 +4,16 @@ import { assert } from '../util/assert'; import * as log from '../logging/log'; -// We define a stricter storage here that returns `unknown` instead of `any`. -type Storage = { - get(key: string): unknown; - put(key: string, value: unknown): Promise; - remove(key: string): Promise; - onready: (callback: () => unknown) => void; -}; +import { StorageInterface } from '../types/Storage.d'; export class OurProfileKeyService { private getPromise: undefined | Promise; private promisesBlockingGet: Array> = []; - private storage?: Storage; + private storage?: StorageInterface; - initialize(storage: Storage): void { + initialize(storage: StorageInterface): void { log.info('Our profile key service: initializing'); const storageReadyPromise = new Promise(resolve => { diff --git a/ts/services/senderCertificate.ts b/ts/services/senderCertificate.ts index 5f45dc2c72..d6b9686810 100644 --- a/ts/services/senderCertificate.ts +++ b/ts/services/senderCertificate.ts @@ -13,13 +13,7 @@ import { missingCaseError } from '../util/missingCaseError'; import { waitForOnline } from '../util/waitForOnline'; import * as log from '../logging/log'; import { connectToServerWithStoredCredentials } from '../util/connectToServerWithStoredCredentials'; - -// We define a stricter storage here that returns `unknown` instead of `any`. -type Storage = { - get(key: string): unknown; - put(key: string, value: unknown): Promise; - remove(key: string): Promise; -}; +import { StorageInterface } from '../types/Storage.d'; function isWellFormed(data: unknown): data is SerializedCertificateType { return serializedCertificateSchema.safeParse(data).success; @@ -43,7 +37,7 @@ export class SenderCertificateService { private onlineEventTarget?: EventTarget; - private storage?: Storage; + private storage?: StorageInterface; initialize({ SenderCertificate, @@ -56,7 +50,7 @@ export class SenderCertificateService { navigator: Readonly<{ onLine: boolean }>; onlineEventTarget: EventTarget; SenderCertificate: typeof SenderCertificateClass; - storage: Storage; + storage: StorageInterface; }): void { log.info('Sender certificate service initialized'); diff --git a/ts/services/storage.ts b/ts/services/storage.ts index e8d456c962..d128a0b08b 100644 --- a/ts/services/storage.ts +++ b/ts/services/storage.ts @@ -91,6 +91,9 @@ async function encryptRecord( : generateStorageID(); const storageKeyBase64 = window.storage.get('storageKey'); + if (!storageKeyBase64) { + throw new Error('No storage key'); + } const storageKey = base64ToArrayBuffer(storageKeyBase64); const storageItemKey = await deriveStorageItemKey( storageKey, @@ -260,8 +263,10 @@ async function generateManifest( manifestRecordKeys.add(identifier); }); - const recordsWithErrors: ReadonlyArray = - window.storage.get('storage-service-error-records') || []; + const recordsWithErrors: ReadonlyArray = window.storage.get( + 'storage-service-error-records', + new Array() + ); window.log.info( 'storageService.generateManifest: adding records that had errors in the previous merge', @@ -406,6 +411,9 @@ async function generateManifest( manifestRecord.keys = Array.from(manifestRecordKeys); const storageKeyBase64 = window.storage.get('storageKey'); + if (!storageKeyBase64) { + throw new Error('No storage key'); + } const storageKey = base64ToArrayBuffer(storageKeyBase64); const storageManifestKey = await deriveStorageManifestKey( storageKey, @@ -539,7 +547,7 @@ async function stopStorageServiceSync() { async function createNewManifest() { window.log.info('storageService.createNewManifest: creating new manifest'); - const version = window.storage.get('manifestVersion') || 0; + const version = window.storage.get('manifestVersion', 0); const { conversationsToUpdate, @@ -562,6 +570,9 @@ async function decryptManifest( const { version, value } = encryptedManifest; const storageKeyBase64 = window.storage.get('storageKey'); + if (!storageKeyBase64) { + throw new Error('No storage key'); + } const storageKey = base64ToArrayBuffer(storageKeyBase64); const storageManifestKey = await deriveStorageManifestKey( storageKey, @@ -577,7 +588,7 @@ async function decryptManifest( } async function fetchManifest( - manifestVersion: string + manifestVersion: number ): Promise { window.log.info('storageService.fetchManifest'); @@ -799,6 +810,9 @@ async function processRemoteRecords( remoteOnlyRecords: Map ): Promise { const storageKeyBase64 = window.storage.get('storageKey'); + if (!storageKeyBase64) { + throw new Error('No storage key'); + } const storageKey = base64ToArrayBuffer(storageKeyBase64); window.log.info( @@ -911,8 +925,10 @@ async function processRemoteRecords( // Collect full map of previously and currently unknown records const unknownRecords: Map = new Map(); - const unknownRecordsArray: ReadonlyArray = - window.storage.get('storage-service-unknown-records') || []; + const unknownRecordsArray: ReadonlyArray = window.storage.get( + 'storage-service-unknown-records', + new Array() + ); unknownRecordsArray.forEach((record: UnknownRecord) => { unknownRecords.set(record.storageID, record); }); @@ -1087,7 +1103,7 @@ async function upload(fromSync = false): Promise { previousManifest = await sync(); } - const localManifestVersion = window.storage.get('manifestVersion') || 0; + const localManifestVersion = window.storage.get('manifestVersion', 0); const version = Number(localManifestVersion) + 1; window.log.info( diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 901881f9bf..3df9d620eb 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -229,7 +229,7 @@ export async function toAccountRecord( } const pinnedConversations = window.storage - .get>('pinnedConversationIds', []) + .get('pinnedConversationIds', new Array()) .map(id => { const pinnedConversation = window.ConversationController.get(id); @@ -824,7 +824,7 @@ export async function mergeAccountRecord( universalExpireTimer, } = accountRecord; - window.storage.put('read-receipt-setting', readReceipts); + window.storage.put('read-receipt-setting', Boolean(readReceipts)); if (typeof sealedSenderIndicators === 'boolean') { window.storage.put('sealedSenderIndicators', sealedSenderIndicators); @@ -890,7 +890,7 @@ export async function mergeAccountRecord( ); const missingStoragePinnedConversationIds = window.storage - .get>('pinnedConversationIds', []) + .get('pinnedConversationIds', new Array()) .filter(id => !modelPinnedConversationIds.includes(id)); if (missingStoragePinnedConversationIds.length !== 0) { diff --git a/ts/shims/storage.ts b/ts/shims/storage.ts index 9b526c447e..cf575b6fea 100644 --- a/ts/shims/storage.ts +++ b/ts/shims/storage.ts @@ -1,13 +1,16 @@ // Copyright 2019-2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { StorageAccessType } from '../types/Storage.d'; + // Matching window.storage.put API -// eslint-disable-next-line max-len -// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types -export function put(key: string, value: any): void { +export function put( + key: K, + value: StorageAccessType[K] +): void { window.storage.put(key, value); } -export async function remove(key: string): Promise { +export async function remove(key: keyof StorageAccessType): Promise { await window.storage.remove(key); } diff --git a/ts/sql/Client.ts b/ts/sql/Client.ts index 668376799e..64c29e3e51 100644 --- a/ts/sql/Client.ts +++ b/ts/sql/Client.ts @@ -7,6 +7,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable no-restricted-syntax */ import { ipcRenderer } from 'electron'; import { @@ -44,6 +45,7 @@ import { ClientJobType, ConversationType, IdentityKeyType, + ItemKeyType, ItemType, MessageType, MessageTypeUnhydrated, @@ -132,7 +134,6 @@ const dataInterface: ClientInterface = { createOrUpdateItem, getItemById, getAllItems, - bulkAddItems, removeItemById, removeAllItems, @@ -692,14 +693,14 @@ async function removeAllSignedPreKeys() { // Items -const ITEM_KEYS: { [key: string]: Array | undefined } = { +const ITEM_KEYS: Partial>> = { identityKey: ['value.pubKey', 'value.privKey'], senderCertificate: ['value.serialized'], senderCertificateNoE164: ['value.serialized'], signaling_key: ['value'], profileKey: ['value'], }; -async function createOrUpdateItem(data: ItemType) { +async function createOrUpdateItem(data: ItemType) { const { id } = data; if (!id) { throw new Error( @@ -712,7 +713,7 @@ async function createOrUpdateItem(data: ItemType) { await channels.createOrUpdateItem(updated); } -async function getItemById(id: string) { +async function getItemById(id: K): Promise> { const keys = ITEM_KEYS[id]; const data = await channels.getItemById(id); @@ -721,23 +722,24 @@ async function getItemById(id: string) { async function getAllItems() { const items = await channels.getAllItems(); - return map(items, item => { - const { id } = item; - const keys = ITEM_KEYS[id]; + const result = Object.create(null); - return Array.isArray(keys) ? keysToArrayBuffer(keys, item) : item; - }); -} -async function bulkAddItems(array: Array) { - const updated = map(array, data => { - const { id } = data; - const keys = ITEM_KEYS[id]; + for (const id of Object.keys(items)) { + const key = id as ItemKeyType; + const value = items[key]; - return keys && Array.isArray(keys) ? keysFromArrayBuffer(keys, data) : data; - }); - await channels.bulkAddItems(updated); + const keys = ITEM_KEYS[key]; + + const deserializedValue = Array.isArray(keys) + ? keysToArrayBuffer(keys, { value }).value + : value; + + result[key] = deserializedValue; + } + + return result; } -async function removeItemById(id: string) { +async function removeItemById(id: ItemKeyType) { await channels.removeItemById(id); } async function removeAllItems() { diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index a0e2233a5b..5b168d2e5d 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -15,6 +15,7 @@ import { ConversationModel } from '../models/conversations'; import { StoredJob } from '../jobs/types'; import { ReactionType } from '../types/Reactions'; import { ConversationColorType, CustomColorType } from '../types/Colors'; +import { StorageAccessType } from '../types/Storage.d'; export type AttachmentDownloadJobType = { id: string; @@ -48,7 +49,12 @@ export type IdentityKeyType = { timestamp: number; verified: number; }; -export type ItemType = any; +export type ItemKeyType = keyof StorageAccessType; +export type AllItemsType = Partial; +export type ItemType = { + id: K; + value: StorageAccessType[K]; +}; export type MessageType = MessageAttributesType; export type MessageTypeUnhydrated = { json: string; @@ -177,12 +183,11 @@ export type DataInterface = { removeAllSignedPreKeys: () => Promise; getAllSignedPreKeys: () => Promise>; - createOrUpdateItem: (data: ItemType) => Promise; - getItemById: (id: string) => Promise; - bulkAddItems: (array: Array) => Promise; - removeItemById: (id: string) => Promise; + createOrUpdateItem(data: ItemType): Promise; + getItemById(id: K): Promise | undefined>; + removeItemById: (id: ItemKeyType) => Promise; removeAllItems: () => Promise; - getAllItems: () => Promise>; + getAllItems: () => Promise; createOrUpdateSenderKey: (key: SenderKeyType) => Promise; getSenderKeyById: (id: string) => Promise; diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 44f32c9d2f..d2acaa0a04 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -44,6 +44,8 @@ import { ConversationType, EmojiType, IdentityKeyType, + AllItemsType, + ItemKeyType, ItemType, MessageType, MessageTypeUnhydrated, @@ -123,7 +125,6 @@ const dataInterface: ServerInterface = { createOrUpdateItem, getItemById, getAllItems, - bulkAddItems, removeItemById, removeAllItems, @@ -2170,24 +2171,34 @@ async function getAllSignedPreKeys(): Promise> { } const ITEMS_TABLE = 'items'; -function createOrUpdateItem(data: ItemType): Promise { +function createOrUpdateItem( + data: ItemType +): Promise { return createOrUpdate(ITEMS_TABLE, data); } -function getItemById(id: string): Promise { +function getItemById( + id: K +): Promise | undefined> { return getById(ITEMS_TABLE, id); } -async function getAllItems(): Promise> { +async function getAllItems(): Promise { const db = getInstance(); const rows: JSONRows = db .prepare('SELECT json FROM items ORDER BY id ASC;') .all(); - return rows.map(row => jsonToObject(row.json)); + const items = rows.map(row => jsonToObject(row.json)); + + const result: AllItemsType = Object.create(null); + + for (const { id, value } of items) { + const key = id as ItemKeyType; + result[key] = value; + } + + return result; } -function bulkAddItems(array: Array): Promise { - return bulkAdd(ITEMS_TABLE, array); -} -function removeItemById(id: string): Promise { +function removeItemById(id: ItemKeyType): Promise { return removeById(ITEMS_TABLE, id); } function removeAllItems(): Promise { diff --git a/ts/state/ducks/items.ts b/ts/state/ducks/items.ts index e503b2e776..83f7916eb0 100644 --- a/ts/state/ducks/items.ts +++ b/ts/state/ducks/items.ts @@ -11,9 +11,11 @@ import { ConversationColors, ConversationColorType, CustomColorType, + CustomColorsItemType, DefaultConversationColorType, } from '../../types/Colors'; import { reloadSelectedConversation } from '../../shims/reloadSelectedConversation'; +import { StorageAccessType } from '../../types/Storage.d'; // State @@ -25,10 +27,7 @@ export type ItemsStateType = { // This property should always be set and this is ensured in background.ts readonly defaultConversationColor?: DefaultConversationColorType; - readonly customColors?: { - readonly colors: Record; - readonly version: number; - }; + readonly customColors?: CustomColorsItemType; }; // Actions @@ -85,7 +84,10 @@ export const actions = { export const useActions = (): typeof actions => useBoundActions(actions); -function putItem(key: string, value: unknown): ItemPutAction { +function putItem( + key: K, + value: StorageAccessType[K] +): ItemPutAction { storageShim.put(key, value); return { @@ -108,7 +110,7 @@ function putItemExternal(key: string, value: unknown): ItemPutExternalAction { }; } -function removeItem(key: string): ItemRemoveAction { +function removeItem(key: keyof StorageAccessType): ItemRemoveAction { storageShim.remove(key); return { diff --git a/ts/test-both/challenge_test.ts b/ts/test-both/challenge_test.ts index 4102dbf854..14b11cce8b 100644 --- a/ts/test-both/challenge_test.ts +++ b/ts/test-both/challenge_test.ts @@ -118,10 +118,10 @@ describe('ChallengeHandler', () => { expireAfter, storage: { - get(key) { + get(key: string) { return storage.get(key); }, - async put(key, value) { + async put(key: string, value: unknown) { storage.set(key, value); }, }, diff --git a/ts/test-both/util/getProvisioningUrl_test.ts b/ts/test-both/util/getProvisioningUrl_test.ts index 6c415ff659..8e9c5562a7 100644 --- a/ts/test-both/util/getProvisioningUrl_test.ts +++ b/ts/test-both/util/getProvisioningUrl_test.ts @@ -4,6 +4,8 @@ import { assert } from 'chai'; import { size } from '../../util/iterables'; +import { typedArrayToArrayBuffer } from '../../Crypto'; + import { getProvisioningUrl } from '../../util/getProvisioningUrl'; describe('getProvisioningUrl', () => { @@ -11,7 +13,7 @@ describe('getProvisioningUrl', () => { const uuid = 'a08bf1fd-1799-427f-a551-70af747e3956'; const publicKey = new Uint8Array([9, 8, 7, 6, 5, 4, 3]); - const result = getProvisioningUrl(uuid, publicKey); + const result = getProvisioningUrl(uuid, typedArrayToArrayBuffer(publicKey)); const resultUrl = new URL(result); assert(result.startsWith('tsdevice:/?')); diff --git a/ts/test-both/util/retryPlaceholders_test.ts b/ts/test-both/util/retryPlaceholders_test.ts index f39699225a..6232879903 100644 --- a/ts/test-both/util/retryPlaceholders_test.ts +++ b/ts/test-both/util/retryPlaceholders_test.ts @@ -18,7 +18,7 @@ describe('RetryPlaceholders', () => { let clock: any; beforeEach(() => { - window.storage.put(STORAGE_KEY, null); + window.storage.put(STORAGE_KEY, undefined as any); clock = sinon.useFakeTimers({ now: NOW, @@ -55,7 +55,7 @@ describe('RetryPlaceholders', () => { window.storage.put(STORAGE_KEY, [ { item: 'is wrong shape!' }, { bad: 'is not good!' }, - ]); + ] as any); const placeholders = new RetryPlaceholders(); diff --git a/ts/test-both/util/synchronousCrypto_test.ts b/ts/test-both/util/synchronousCrypto_test.ts index dbb50da2d5..91ace27734 100644 --- a/ts/test-both/util/synchronousCrypto_test.ts +++ b/ts/test-both/util/synchronousCrypto_test.ts @@ -44,9 +44,9 @@ describe('synchronousCrypto', () => { describe('encrypt+decrypt', () => { it('returns original input', () => { - const iv = crypto.randomBytes(16); - const key = crypto.randomBytes(32); - const input = Buffer.from('plaintext'); + const iv = toArrayBuffer(crypto.randomBytes(16)); + const key = toArrayBuffer(crypto.randomBytes(32)); + const input = toArrayBuffer(Buffer.from('plaintext')); const ciphertext = encrypt(key, input, iv); const plaintext = decrypt(key, ciphertext, iv); diff --git a/ts/test-electron/SignalProtocolStore_test.ts b/ts/test-electron/SignalProtocolStore_test.ts index c884bdeeca..81ea8dd2b2 100644 --- a/ts/test-electron/SignalProtocolStore_test.ts +++ b/ts/test-electron/SignalProtocolStore_test.ts @@ -15,7 +15,11 @@ import { signal } from '../protobuf/compiled'; import { sessionStructureToArrayBuffer } from '../util/sessionTranslation'; import { Zone } from '../util/Zone'; -import { getRandomBytes, constantTimeEqual } from '../Crypto'; +import { + getRandomBytes, + constantTimeEqual, + typedArrayToArrayBuffer as toArrayBuffer, +} from '../Crypto'; import { clampPrivateKey, setPublicKeyTypeByte } from '../Curve'; import { SignalProtocolStore, GLOBAL_ZONE } from '../SignalProtocolStore'; import { IdentityKeyType, KeyPairType } from '../textsecure/Types.d'; @@ -173,7 +177,10 @@ describe('SignalProtocolStore', () => { } assert.isTrue( - constantTimeEqual(expected.serialize(), actual.serialize()) + constantTimeEqual( + toArrayBuffer(expected.serialize()), + toArrayBuffer(actual.serialize()) + ) ); await store.removeSenderKey(encodedAddress, distributionId); @@ -203,7 +210,10 @@ describe('SignalProtocolStore', () => { } assert.isTrue( - constantTimeEqual(expected.serialize(), actual.serialize()) + constantTimeEqual( + toArrayBuffer(expected.serialize()), + toArrayBuffer(actual.serialize()) + ) ); await store.removeSenderKey(encodedAddress, distributionId); diff --git a/ts/test-electron/WebsocketResources_test.ts b/ts/test-electron/WebsocketResources_test.ts index 00c6aadd52..63a7e66c17 100644 --- a/ts/test-electron/WebsocketResources_test.ts +++ b/ts/test-electron/WebsocketResources_test.ts @@ -12,6 +12,8 @@ import * as sinon from 'sinon'; import EventEmitter from 'events'; import { connection as WebSocket } from 'websocket'; +import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto'; + import WebSocketResource from '../textsecure/WebsocketResources'; describe('WebSocket-Resource', () => { @@ -29,7 +31,7 @@ describe('WebSocket-Resource', () => { sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => { const message = window.textsecure.protobuf.WebSocketMessage.decode( - data + toArrayBuffer(data) ); assert.strictEqual( message.type, @@ -86,7 +88,7 @@ describe('WebSocket-Resource', () => { sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => { const message = window.textsecure.protobuf.WebSocketMessage.decode( - data + toArrayBuffer(data) ); assert.strictEqual( message.type, @@ -166,7 +168,7 @@ describe('WebSocket-Resource', () => { sinon.stub(socket, 'sendBytes').callsFake(data => { const message = window.textsecure.protobuf.WebSocketMessage.decode( - data + toArrayBuffer(data) ); assert.strictEqual( message.type, @@ -189,7 +191,7 @@ describe('WebSocket-Resource', () => { sinon.stub(socket, 'sendBytes').callsFake(data => { const message = window.textsecure.protobuf.WebSocketMessage.decode( - data + toArrayBuffer(data) ); assert.strictEqual( message.type, @@ -230,7 +232,7 @@ describe('WebSocket-Resource', () => { sinon.stub(socket, 'sendBytes').callsFake(data => { const message = window.textsecure.protobuf.WebSocketMessage.decode( - data + toArrayBuffer(data) ); assert.strictEqual( message.type, diff --git a/ts/test-electron/background_test.ts b/ts/test-electron/background_test.ts index 753997ca74..7e49afbd6f 100644 --- a/ts/test-electron/background_test.ts +++ b/ts/test-electron/background_test.ts @@ -44,14 +44,15 @@ describe('#cleanupSessionResets', () => { it('filters out falsey items', () => { const startValue = { one: 0, - two: false, - three: Date.now(), + two: Date.now(), }; window.storage.put('sessionResets', startValue); cleanupSessionResets(); const actual = window.storage.get('sessionResets'); - const expected = window._.pick(startValue, ['three']); + const expected = window._.pick(startValue, ['two']); assert.deepEqual(actual, expected); + + assert.deepEqual(Object.keys(startValue), ['two']); }); }); diff --git a/ts/test-electron/models/messages_test.ts b/ts/test-electron/models/messages_test.ts index b2d232cba0..8a6d57adb1 100644 --- a/ts/test-electron/models/messages_test.ts +++ b/ts/test-electron/models/messages_test.ts @@ -37,8 +37,8 @@ describe('Message', () => { }); after(async () => { - window.textsecure.storage.put('number_id', null); - window.textsecure.storage.put('uuid_id', null); + window.textsecure.storage.remove('number_id'); + window.textsecure.storage.remove('uuid_id'); await window.Signal.Data.removeAll(); await window.storage.fetch(); diff --git a/ts/test-electron/routineProfileRefresh_test.ts b/ts/test-electron/routineProfileRefresh_test.ts index 3c9db7cc4b..799570bfa4 100644 --- a/ts/test-electron/routineProfileRefresh_test.ts +++ b/ts/test-electron/routineProfileRefresh_test.ts @@ -66,7 +66,7 @@ describe('routineProfileRefresh', () => { return result; } - function makeStorage(lastAttemptAt: undefined | number = undefined) { + function makeStorage(lastAttemptAt?: number) { return { get: sinonSandbox .stub() diff --git a/ts/textsecure.d.ts b/ts/textsecure.d.ts index f7b2bd871a..9ab358aed5 100644 --- a/ts/textsecure.d.ts +++ b/ts/textsecure.d.ts @@ -14,7 +14,11 @@ import { WebAPIType } from './textsecure/WebAPI'; import utils from './textsecure/Helpers'; import { CallingMessage as CallingMessageClass } from 'ringrtc'; import { WhatIsThis } from './window.d'; -import { SignalProtocolStore } from './SignalProtocolStore'; +import { Storage } from './textsecure/Storage'; +import { + StorageServiceCallOptionsType, + StorageServiceCredentials, +} from './textsecure/Types.d'; export type UnprocessedType = { attempts: number; @@ -30,15 +34,7 @@ export type UnprocessedType = { version: number; }; -export type StorageServiceCallOptionsType = { - credentials?: StorageServiceCredentials; - greaterThanVersion?: string; -}; - -export type StorageServiceCredentials = { - username: string; - password: string; -}; +export { StorageServiceCallOptionsType, StorageServiceCredentials }; export type TextSecureType = { createTaskWithTimeout: ( @@ -47,34 +43,7 @@ export type TextSecureType = { options?: { timeout?: number } ) => () => Promise; crypto: typeof Crypto; - storage: { - user: { - getNumber: () => string; - getUuid: () => string | undefined; - getDeviceId: () => number | string; - getDeviceName: () => string; - getDeviceNameEncrypted: () => boolean; - setDeviceNameEncrypted: () => Promise; - getSignalingKey: () => ArrayBuffer; - setNumberAndDeviceId: ( - number: string, - deviceId: number, - deviceName?: string | null - ) => Promise; - setUuidAndDeviceId: (uuid: string, deviceId: number) => Promise; - }; - unprocessed: { - remove: (id: string | Array) => Promise; - getCount: () => Promise; - removeAll: () => Promise; - getAll: () => Promise>; - updateAttempts: (id: string, attempts: number) => Promise; - }; - get: (key: string, defaultValue?: any) => any; - put: (key: string, value: any) => Promise; - remove: (key: string | Array) => Promise; - protocol: SignalProtocolStore; - }; + storage: Storage; messageReceiver: MessageReceiver; messageSender: MessageSender; messaging: SendMessage; diff --git a/ts/textsecure/AccountManager.ts b/ts/textsecure/AccountManager.ts index 14814525f3..f4a8b9405b 100644 --- a/ts/textsecure/AccountManager.ts +++ b/ts/textsecure/AccountManager.ts @@ -36,7 +36,7 @@ const PREKEY_ROTATION_AGE = 24 * 60 * 60 * 1000; const PROFILE_KEY_LENGTH = 32; const SIGNED_KEY_GEN_BATCH_SIZE = 100; -function getIdentifier(id: string) { +function getIdentifier(id: string | undefined) { if (!id || !id.length) { return id; } @@ -137,7 +137,7 @@ export default class AccountManager extends EventTarget { return; } const deviceName = window.textsecure.storage.user.getDeviceName(); - const base64 = await this.encryptDeviceName(deviceName); + const base64 = await this.encryptDeviceName(deviceName || ''); if (base64) { await this.server.updateDeviceName(base64); @@ -578,7 +578,7 @@ export default class AccountManager extends EventTarget { window.textsecure.storage.remove('regionCode'), window.textsecure.storage.remove('userAgent'), window.textsecure.storage.remove('profileKey'), - window.textsecure.storage.remove('read-receipts-setting'), + window.textsecure.storage.remove('read-receipt-setting'), ]); // `setNumberAndDeviceId` and `setUuidAndDeviceId` need to be called @@ -590,7 +590,7 @@ export default class AccountManager extends EventTarget { await window.textsecure.storage.user.setNumberAndDeviceId( number, response.deviceId || 1, - deviceName + deviceName || undefined ); if (uuid) { diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index 071c98d893..6c6c95195e 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -756,7 +756,7 @@ class MessageReceiverInner extends EventTarget { try { const { id } = item; - await window.textsecure.storage.unprocessed.remove(id); + await window.textsecure.storage.protocol.removeUnprocessed(id); } catch (deleteError) { window.log.error( 'queueCached error deleting item', @@ -800,17 +800,17 @@ class MessageReceiverInner extends EventTarget { async getAllFromCache() { window.log.info('getAllFromCache'); - const count = await window.textsecure.storage.unprocessed.getCount(); + const count = await window.textsecure.storage.protocol.getUnprocessedCount(); if (count > 1500) { - await window.textsecure.storage.unprocessed.removeAll(); + await window.textsecure.storage.protocol.removeAllUnprocessed(); window.log.warn( `There were ${count} messages in cache. Deleted all instead of reprocessing` ); return []; } - const items = await window.textsecure.storage.unprocessed.getAll(); + const items = await window.textsecure.storage.protocol.getAllUnprocessed(); window.log.info('getAllFromCache loaded', items.length, 'saved envelopes'); return Promise.all( @@ -823,9 +823,9 @@ class MessageReceiverInner extends EventTarget { 'getAllFromCache final attempt for envelope', item.id ); - await window.textsecure.storage.unprocessed.remove(item.id); + await window.textsecure.storage.protocol.removeUnprocessed(item.id); } else { - await window.textsecure.storage.unprocessed.updateAttempts( + await window.textsecure.storage.protocol.updateUnprocessedAttempts( item.id, attempts ); @@ -981,7 +981,7 @@ class MessageReceiverInner extends EventTarget { } async cacheRemoveBatch(items: Array) { - await window.textsecure.storage.unprocessed.remove(items); + await window.textsecure.storage.protocol.removeUnprocessed(items); } removeFromCache(envelope: EnvelopeClass) { @@ -1361,7 +1361,7 @@ class MessageReceiverInner extends EventTarget { buffer, PublicKey.deserialize(Buffer.from(serverTrustRoot)), envelope.serverTimestamp, - localE164, + localE164 || null, localUuid, localDeviceId, sessionStore, @@ -2417,7 +2417,9 @@ class MessageReceiverInner extends EventTarget { blocked: SyncMessageClass.Blocked ) { window.log.info('Setting these numbers as blocked:', blocked.numbers); - await window.textsecure.storage.put('blocked', blocked.numbers); + if (blocked.numbers) { + await window.textsecure.storage.put('blocked', blocked.numbers); + } if (blocked.uuids) { window.normalizeUuids( blocked, @@ -2439,17 +2441,15 @@ class MessageReceiverInner extends EventTarget { } isBlocked(number: string) { - return window.textsecure.storage.get('blocked', []).includes(number); + return window.textsecure.storage.blocked.isBlocked(number); } isUuidBlocked(uuid: string) { - return window.textsecure.storage.get('blocked-uuids', []).includes(uuid); + return window.textsecure.storage.blocked.isUuidBlocked(uuid); } isGroupBlocked(groupId: string) { - return window.textsecure.storage - .get('blocked-groups', []) - .includes(groupId); + return window.textsecure.storage.blocked.isGroupBlocked(groupId); } cleanAttachment(attachment: AttachmentPointerClass) { diff --git a/ts/textsecure/OutgoingMessage.ts b/ts/textsecure/OutgoingMessage.ts index ea7cf88310..a60a97e703 100644 --- a/ts/textsecure/OutgoingMessage.ts +++ b/ts/textsecure/OutgoingMessage.ts @@ -39,6 +39,7 @@ import { } from './Errors'; import { isValidNumber } from '../types/PhoneNumber'; import { Sessions, IdentityKeys } from '../LibSignalStores'; +import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto'; import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup'; import { getKeysForIdentifier } from './getKeysForIdentifier'; @@ -309,7 +310,7 @@ export default class OutgoingMessage { this.plaintext = message.serialize(); } } - return this.plaintext; + return toArrayBuffer(this.plaintext); } async getCiphertextMessage({ diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index 55821850b5..db3671c509 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -16,6 +16,7 @@ import { SenderKeyDistributionMessage, } from '@signalapp/signal-client'; +import { assert } from '../util/assert'; import { parseIntOrThrow } from '../util/parseIntOrThrow'; import { SenderKeys } from '../LibSignalStores'; import { @@ -750,8 +751,8 @@ export default class MessageSender { const blockedIdentifiers = new Set( concat( - window.storage.getBlockedUuids(), - window.storage.getBlockedNumbers() + window.storage.blocked.getBlockedUuids(), + window.storage.blocked.getBlockedNumbers() ) ); @@ -895,12 +896,13 @@ export default class MessageSender { } async sendIndividualProto( - identifier: string, + identifier: string | undefined, proto: DataMessageClass | ContentClass | PlaintextContent, timestamp: number, contentHint: number, options?: SendOptionsType ): Promise { + assert(identifier, "Identifier can't be undefined"); return new Promise((resolve, reject) => { const callback = (res: CallbackResultType) => { if (res && res.errors && res.errors.length > 0) { @@ -976,7 +978,7 @@ export default class MessageSender { const myUuid = window.textsecure.storage.user.getUuid(); const myDevice = window.textsecure.storage.user.getDeviceId(); - if (myDevice === 1 || myDevice === '1') { + if (myDevice === 1) { return Promise.resolve(); } @@ -1050,7 +1052,7 @@ export default class MessageSender { const myNumber = window.textsecure.storage.user.getNumber(); const myUuid = window.textsecure.storage.user.getUuid(); const myDevice = window.textsecure.storage.user.getDeviceId(); - if (myDevice !== 1 && myDevice !== '1') { + if (myDevice !== 1) { const request = new window.textsecure.protobuf.SyncMessage.Request(); request.type = window.textsecure.protobuf.SyncMessage.Request.Type.BLOCKED; @@ -1081,7 +1083,7 @@ export default class MessageSender { const myNumber = window.textsecure.storage.user.getNumber(); const myUuid = window.textsecure.storage.user.getUuid(); const myDevice = window.textsecure.storage.user.getDeviceId(); - if (myDevice !== 1 && myDevice !== '1') { + if (myDevice !== 1) { const request = new window.textsecure.protobuf.SyncMessage.Request(); request.type = window.textsecure.protobuf.SyncMessage.Request.Type.CONFIGURATION; @@ -1112,7 +1114,7 @@ export default class MessageSender { const myNumber = window.textsecure.storage.user.getNumber(); const myUuid = window.textsecure.storage.user.getUuid(); const myDevice = window.textsecure.storage.user.getDeviceId(); - if (myDevice !== 1 && myDevice !== '1') { + if (myDevice !== 1) { const request = new window.textsecure.protobuf.SyncMessage.Request(); request.type = window.textsecure.protobuf.SyncMessage.Request.Type.GROUPS; const syncMessage = this.createSyncMessage(); @@ -1143,7 +1145,7 @@ export default class MessageSender { const myUuid = window.textsecure.storage.user.getUuid(); const myDevice = window.textsecure.storage.user.getDeviceId(); - if (myDevice !== 1 && myDevice !== '1') { + if (myDevice !== 1) { const request = new window.textsecure.protobuf.SyncMessage.Request(); request.type = window.textsecure.protobuf.SyncMessage.Request.Type.CONTACTS; @@ -1175,7 +1177,7 @@ export default class MessageSender { const myNumber = window.textsecure.storage.user.getNumber(); const myDevice = window.textsecure.storage.user.getDeviceId(); - if (myDevice === 1 || myDevice === '1') { + if (myDevice === 1) { return; } @@ -1208,7 +1210,7 @@ export default class MessageSender { const myNumber = window.textsecure.storage.user.getNumber(); const myDevice = window.textsecure.storage.user.getDeviceId(); - if (myDevice === 1 || myDevice === '1') { + if (myDevice === 1) { return; } @@ -1244,7 +1246,7 @@ export default class MessageSender { const myNumber = window.textsecure.storage.user.getNumber(); const myUuid = window.textsecure.storage.user.getUuid(); const myDevice = window.textsecure.storage.user.getDeviceId(); - if (myDevice !== 1 && myDevice !== '1') { + if (myDevice !== 1) { const syncMessage = this.createSyncMessage(); syncMessage.read = []; for (let i = 0; i < reads.length; i += 1) { @@ -1283,7 +1285,7 @@ export default class MessageSender { const myNumber = window.textsecure.storage.user.getNumber(); const myUuid = window.textsecure.storage.user.getUuid(); const myDevice = window.textsecure.storage.user.getDeviceId(); - if (myDevice === 1 || myDevice === '1') { + if (myDevice === 1) { return null; } @@ -1323,7 +1325,7 @@ export default class MessageSender { const myNumber = window.textsecure.storage.user.getNumber(); const myUuid = window.textsecure.storage.user.getUuid(); const myDevice = window.textsecure.storage.user.getDeviceId(); - if (myDevice === 1 || myDevice === '1') { + if (myDevice === 1) { return null; } @@ -1361,7 +1363,7 @@ export default class MessageSender { options?: SendOptionsType ): Promise { const myDevice = window.textsecure.storage.user.getDeviceId(); - if (myDevice === 1 || myDevice === '1') { + if (myDevice === 1) { return null; } @@ -1412,7 +1414,7 @@ export default class MessageSender { const myDevice = window.textsecure.storage.user.getDeviceId(); const now = Date.now(); - if (myDevice === 1 || myDevice === '1') { + if (myDevice === 1) { return Promise.resolve(); } @@ -1526,7 +1528,7 @@ export default class MessageSender { const myDevice = window.textsecure.storage.user.getDeviceId(); if ( (myNumber === recipientE164 || myUuid === recipientUuid) && - (myDevice === 1 || myDevice === '1') + myDevice === 1 ) { return Promise.resolve(); } diff --git a/ts/textsecure/Storage.ts b/ts/textsecure/Storage.ts index 76cbde5866..15d63f2fd1 100644 --- a/ts/textsecure/Storage.ts +++ b/ts/textsecure/Storage.ts @@ -1,51 +1,149 @@ // Copyright 2020-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable no-restricted-syntax */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import utils from './Helpers'; +import { + StorageAccessType as Access, + StorageInterface, +} from '../types/Storage.d'; +import { User } from './storage/User'; +import { Blocked } from './storage/Blocked'; -// Default implementation working with localStorage -const localStorageImpl: StorageInterface = { - put(key: string, value: any) { - if (value === undefined) { - throw new Error('Tried to store undefined'); +import { assert } from '../util/assert'; +import Data from '../sql/Client'; +import { SignalProtocolStore } from '../SignalProtocolStore'; + +export class Storage implements StorageInterface { + public readonly user: User; + + public readonly blocked: Blocked; + + private ready = false; + + private readyCallbacks: Array<() => void> = []; + + private items: Partial = Object.create(null); + + private privProtocol: SignalProtocolStore | undefined; + + constructor() { + this.user = new User(this); + this.blocked = new Blocked(this); + + window.storage = this; + } + + get protocol(): SignalProtocolStore { + assert( + this.privProtocol !== undefined, + 'SignalProtocolStore not initialized' + ); + return this.privProtocol; + } + + set protocol(value: SignalProtocolStore) { + this.privProtocol = value; + } + + // `StorageInterface` implementation + + public get( + key: K + ): V | undefined; + + public get( + key: K, + defaultValue: V + ): V; + + public get( + key: K, + defaultValue?: V + ): V | undefined { + if (!this.ready) { + window.log.warn('Called storage.get before storage is ready. key:', key); } - localStorage.setItem(`${key}`, utils.jsonThing(value)); - }, - get(key: string, defaultValue: any) { - const value = localStorage.getItem(`${key}`); - if (value === null) { + const item = this.items[key]; + if (item === undefined) { return defaultValue; } - return JSON.parse(value); - }, - remove(key: string) { - localStorage.removeItem(`${key}`); - }, -}; + return item as V; + } -export type StorageInterface = { - put(key: string, value: any): void | Promise; - get(key: string, defaultValue: any): any; - remove(key: string): void | Promise; -}; + public async put( + key: K, + value: Access[K] + ): Promise { + if (!this.ready) { + window.log.warn('Called storage.put before storage is ready. key:', key); + } -const Storage = { - impl: localStorageImpl, + this.items[key] = value; + await window.Signal.Data.createOrUpdateItem({ id: key, value }); - put(key: string, value: unknown): Promise | void { - return Storage.impl.put(key, value); - }, + window.reduxActions?.items.putItemExternal(key, value); + } - get(key: string, defaultValue: unknown): Promise { - return Storage.impl.get(key, defaultValue); - }, + public async remove(key: K): Promise { + if (!this.ready) { + window.log.warn( + 'Called storage.remove before storage is ready. key:', + key + ); + } - remove(key: string): Promise | void { - return Storage.impl.remove(key); - }, -}; + delete this.items[key]; + await Data.removeItemById(key); -export default Storage; + window.reduxActions?.items.removeItemExternal(key); + } + + // Regular methods + + public onready(callback: () => void): void { + if (this.ready) { + callback(); + } else { + this.readyCallbacks.push(callback); + } + } + + public async fetch(): Promise { + this.reset(); + + Object.assign(this.items, await Data.getAllItems()); + + this.ready = true; + this.callListeners(); + } + + public reset(): void { + this.ready = false; + this.items = Object.create(null); + } + + public getItemsState(): Partial { + const state = Object.create(null); + + // TypeScript isn't smart enough to figure out the types automatically. + const { items } = this; + const allKeys = Object.keys(items) as Array; + + for (const key of allKeys) { + state[key] = items[key]; + } + + return state; + } + + private callListeners(): void { + if (!this.ready) { + return; + } + const callbacks = this.readyCallbacks; + this.readyCallbacks = []; + callbacks.forEach(callback => callback()); + } +} diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index cff966f729..a13a9a1155 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -11,6 +11,16 @@ export { UnprocessedUpdateType, } from '../sql/Interface'; +export type StorageServiceCallOptionsType = { + credentials?: StorageServiceCredentials; + greaterThanVersion?: number; +}; + +export type StorageServiceCredentials = { + username: string; + password: string; +}; + export type DeviceType = { id: number; identifier: string; @@ -44,3 +54,5 @@ export type OuterSignedPrekeyType = { privKey: ArrayBuffer; pubKey: ArrayBuffer; }; + +export type SessionResetsType = Record; diff --git a/ts/textsecure/WebsocketResources.ts b/ts/textsecure/WebsocketResources.ts index 273ac08c11..d83524abd8 100644 --- a/ts/textsecure/WebsocketResources.ts +++ b/ts/textsecure/WebsocketResources.ts @@ -30,6 +30,7 @@ import { connection as WebSocket, IMessage } from 'websocket'; import { ByteBufferClass } from '../window.d'; +import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto'; import EventTarget from './EventTarget'; @@ -153,7 +154,7 @@ export default class WebSocketResource extends EventTarget { } const message = window.textsecure.protobuf.WebSocketMessage.decode( - binaryData + toArrayBuffer(binaryData) ); if ( message.type === diff --git a/ts/textsecure/index.ts b/ts/textsecure/index.ts index 1273e0f91e..09214fbadd 100644 --- a/ts/textsecure/index.ts +++ b/ts/textsecure/index.ts @@ -11,7 +11,7 @@ import createTaskWithTimeout from './TaskWithTimeout'; import SyncRequest from './SyncRequest'; import MessageSender from './SendMessage'; import StringView from './StringView'; -import Storage from './Storage'; +import { Storage } from './Storage'; import * as WebAPI from './WebAPI'; import WebSocketResource from './WebsocketResources'; @@ -19,7 +19,7 @@ export const textsecure = { createTaskWithTimeout, crypto: Crypto, utils, - storage: Storage, + storage: new Storage(), AccountManager, ContactBuffer, diff --git a/ts/textsecure/storage/Blocked.ts b/ts/textsecure/storage/Blocked.ts new file mode 100644 index 0000000000..9c3b6e081b --- /dev/null +++ b/ts/textsecure/storage/Blocked.ts @@ -0,0 +1,98 @@ +// Copyright 2016-2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { without } from 'lodash'; + +import { StorageInterface } from '../../types/Storage.d'; + +const BLOCKED_NUMBERS_ID = 'blocked'; +const BLOCKED_UUIDS_ID = 'blocked-uuids'; +const BLOCKED_GROUPS_ID = 'blocked-groups'; + +export class Blocked { + constructor(private readonly storage: StorageInterface) {} + + public getBlockedNumbers(): Array { + return this.storage.get(BLOCKED_NUMBERS_ID, new Array()); + } + + public isBlocked(number: string): boolean { + return this.getBlockedNumbers().includes(number); + } + + public async addBlockedNumber(number: string): Promise { + const numbers = this.getBlockedNumbers(); + if (numbers.includes(number)) { + return; + } + + window.log.info('adding', number, 'to blocked list'); + await this.storage.put(BLOCKED_NUMBERS_ID, numbers.concat(number)); + } + + public async removeBlockedNumber(number: string): Promise { + const numbers = this.getBlockedNumbers(); + if (!numbers.includes(number)) { + return; + } + + window.log.info('removing', number, 'from blocked list'); + await this.storage.put(BLOCKED_NUMBERS_ID, without(numbers, number)); + } + + public getBlockedUuids(): Array { + return this.storage.get(BLOCKED_UUIDS_ID, new Array()); + } + + public isUuidBlocked(uuid: string): boolean { + return this.getBlockedUuids().includes(uuid); + } + + public async addBlockedUuid(uuid: string): Promise { + const uuids = this.getBlockedUuids(); + if (uuids.includes(uuid)) { + return; + } + + window.log.info('adding', uuid, 'to blocked list'); + await this.storage.put(BLOCKED_UUIDS_ID, uuids.concat(uuid)); + } + + public async removeBlockedUuid(uuid: string): Promise { + const numbers = this.getBlockedUuids(); + if (!numbers.includes(uuid)) { + return; + } + + window.log.info('removing', uuid, 'from blocked list'); + await this.storage.put(BLOCKED_UUIDS_ID, without(numbers, uuid)); + } + + public getBlockedGroups(): Array { + return this.storage.get(BLOCKED_GROUPS_ID, new Array()); + } + + public isGroupBlocked(groupId: string): boolean { + return this.getBlockedGroups().includes(groupId); + } + + public async addBlockedGroup(groupId: string): Promise { + const groupIds = this.getBlockedGroups(); + if (groupIds.includes(groupId)) { + return; + } + + window.log.info(`adding group(${groupId}) to blocked list`); + await this.storage.put(BLOCKED_GROUPS_ID, groupIds.concat(groupId)); + } + + public async removeBlockedGroup(groupId: string): Promise { + const groupIds = this.getBlockedGroups(); + if (!groupIds.includes(groupId)) { + return; + } + + window.log.info(`removing group(${groupId} from blocked list`); + await this.storage.put(BLOCKED_GROUPS_ID, without(groupIds, groupId)); + } +} diff --git a/ts/textsecure/storage/User.ts b/ts/textsecure/storage/User.ts new file mode 100644 index 0000000000..e77d87dfc8 --- /dev/null +++ b/ts/textsecure/storage/User.ts @@ -0,0 +1,76 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { StorageInterface } from '../../types/Storage.d'; + +import Helpers from '../Helpers'; + +export class User { + constructor(private readonly storage: StorageInterface) {} + + 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); + } + } + + public async setUuidAndDeviceId( + uuid: string, + deviceId: number + ): Promise { + return this.storage.put('uuid_id', `${uuid}.${deviceId}`); + } + + public getNumber(): string | undefined { + const numberId = this.storage.get('number_id'); + if (numberId === undefined) return undefined; + return Helpers.unencodeNumber(numberId)[0]; + } + + public getUuid(): string | undefined { + const uuid = this.storage.get('uuid_id'); + if (uuid === undefined) return undefined; + return Helpers.unencodeNumber(uuid.toLowerCase())[0]; + } + + public getDeviceId(): number | undefined { + const value = this._getDeviceIdFromUuid() || this._getDeviceIdFromNumber(); + if (value === undefined) { + return undefined; + } + return parseInt(value, 10); + } + + public getDeviceName(): string | undefined { + return this.storage.get('device_name'); + } + + public async setDeviceNameEncrypted(): Promise { + return this.storage.put('deviceNameEncrypted', true); + } + + public getDeviceNameEncrypted(): boolean | undefined { + return this.storage.get('deviceNameEncrypted'); + } + + public getSignalingKey(): ArrayBuffer | undefined { + return this.storage.get('signaling_key'); + } + + private _getDeviceIdFromUuid(): string | undefined { + const uuid = this.storage.get('uuid_id'); + if (uuid === undefined) return undefined; + return Helpers.unencodeNumber(uuid)[1]; + } + + private _getDeviceIdFromNumber(): string | undefined { + const numberId = this.storage.get('number_id'); + if (numberId === undefined) return undefined; + return Helpers.unencodeNumber(numberId)[1]; + } +} diff --git a/ts/types/Colors.ts b/ts/types/Colors.ts index 7e14455edb..25a7fd8955 100644 --- a/ts/types/Colors.ts +++ b/ts/types/Colors.ts @@ -105,3 +105,8 @@ export type DefaultConversationColorType = { export const DEFAULT_CONVERSATION_COLOR: DefaultConversationColorType = { color: 'ultramarine', }; + +export type CustomColorsItemType = { + readonly colors: Record; + readonly version: number; +}; diff --git a/ts/types/Storage.d.ts b/ts/types/Storage.d.ts new file mode 100644 index 0000000000..fab84870ee --- /dev/null +++ b/ts/types/Storage.d.ts @@ -0,0 +1,133 @@ +// Copyright 2020-2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { + CustomColorsItemType, + DefaultConversationColorType, +} from './Colors'; +import type { AudioDevice } from './Calling'; +import type { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability'; +import type { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode'; +import type { RetryItemType } from '../util/retryPlaceholders'; +import type { ConfigMapType as RemoteConfigType } from '../RemoteConfig'; + +import type { GroupCredentialType } from '../textsecure/WebAPI'; +import type { + KeyPairType, + SessionResetsType, + StorageServiceCredentials, +} from '../textsecure/Types.d'; + +export type SerializedCertificateType = { + expires: number; + serialized: ArrayBuffer; +}; + +export type StorageAccessType = { + 'always-relay-calls': boolean; + 'audio-notification': boolean; + 'badge-count-muted-conversations': boolean; + 'blocked-groups': Array; + 'blocked-uuids': Array; + 'call-ringtone-notification': boolean; + 'call-system-notification': boolean; + 'hide-menu-bar': boolean; + 'incoming-call-notification': boolean; + 'notification-draw-attention': boolean; + 'notification-setting': 'message' | 'name' | 'count' | 'off'; + 'read-receipt-setting': boolean; + 'spell-check': boolean; + 'theme-setting': 'light' | 'dark' | 'system'; + attachmentMigration_isComplete: boolean; + attachmentMigration_lastProcessedIndex: number; + blocked: Array; + defaultConversationColor: DefaultConversationColorType; + customColors: CustomColorsItemType; + device_name: string; + hasRegisterSupportForUnauthenticatedDelivery: boolean; + identityKey: KeyPairType; + lastHeartbeat: number; + lastStartup: number; + lastAttemptedToRefreshProfilesAt: number; + maxPreKeyId: number; + number_id: string; + password: string; + profileKey: ArrayBuffer; + regionCode: string; + registrationId: number; + remoteBuildExpiration: number; + sessionResets: SessionResetsType; + showStickerPickerHint: boolean; + showStickersIntroduction: boolean; + signedKeyId: number; + signedKeyRotationRejected: number; + storageKey: string; + synced_at: number; + userAgent: string; + uuid_id: string; + version: string; + linkPreviews: boolean; + universalExpireTimer: number; + retryPlaceholders: Array; + chromiumRegistrationDoneEver: ''; + chromiumRegistrationDone: ''; + phoneNumberSharingMode: PhoneNumberSharingMode; + phoneNumberDiscoverability: PhoneNumberDiscoverability; + pinnedConversationIds: Array; + primarySendsSms: boolean; + typingIndicators: boolean; + sealedSenderIndicators: boolean; + storageFetchComplete: boolean; + avatarUrl: string; + manifestVersion: number; + storageCredentials: StorageServiceCredentials; + 'storage-service-error-records': Array<{ + itemType: number; + storageID: string; + }>; + 'storage-service-unknown-records': Array<{ + itemType: number; + storageID: string; + }>; + 'preferred-video-input-device': string; + 'preferred-audio-input-device': AudioDevice; + 'preferred-audio-output-device': AudioDevice; + remoteConfig: RemoteConfigType; + unidentifiedDeliveryIndicators: boolean; + groupCredentials: Array; + lastReceivedAtCounter: number; + signaling_key: ArrayBuffer; + skinTone: number; + unreadCount: number; + 'challenge:retry-message-ids': ReadonlyArray<{ + messageId: string; + createdAt: number; + }>; + deviceNameEncrypted: boolean; + 'indexeddb-delete-needed': boolean; + senderCertificate: SerializedCertificateType; + senderCertificateNoE164: SerializedCertificateType; + + // Deprecated + senderCertificateWithUuid: never; +}; + +export interface StorageInterface { + onready(callback: () => void): void; + + get( + key: K + ): V | undefined; + + get( + key: K, + defaultValue: V + ): V; + + put( + key: K, + value: StorageAccessType[K] + ): Promise; + + remove(key: K): Promise; +} diff --git a/ts/util/connectToServerWithStoredCredentials.ts b/ts/util/connectToServerWithStoredCredentials.ts index 8968646e52..9e77c43458 100644 --- a/ts/util/connectToServerWithStoredCredentials.ts +++ b/ts/util/connectToServerWithStoredCredentials.ts @@ -2,15 +2,11 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { WebAPIConnectType, WebAPIType } from '../textsecure/WebAPI'; - -// We define a stricter storage here that returns `unknown` instead of `any`. -type Storage = { - get(key: string): unknown; -}; +import { StorageInterface } from '../types/Storage.d'; export function connectToServerWithStoredCredentials( WebAPI: WebAPIConnectType, - storage: Storage + storage: Pick ): WebAPIType { const username = storage.get('uuid_id') || storage.get('number_id'); if (typeof username !== 'string') { diff --git a/ts/util/retryPlaceholders.ts b/ts/util/retryPlaceholders.ts index a37d4274f4..acab920322 100644 --- a/ts/util/retryPlaceholders.ts +++ b/ts/util/retryPlaceholders.ts @@ -52,7 +52,7 @@ export class RetryPlaceholders { } const parsed = retryItemListSchema.safeParse( - window.storage.get(STORAGE_KEY) || [] + window.storage.get(STORAGE_KEY, new Array()) ); if (!parsed.success) { window.log.warn( diff --git a/ts/util/safetyNumber.ts b/ts/util/safetyNumber.ts index e2e7486ee1..149e50de18 100644 --- a/ts/util/safetyNumber.ts +++ b/ts/util/safetyNumber.ts @@ -4,6 +4,8 @@ import { PublicKey, Fingerprint } from '@signalapp/signal-client'; import { ConversationType } from '../state/ducks/conversations'; +import { assert } from './assert'; + export async function generateSecurityNumber( ourNumber: string, ourKey: ArrayBuffer, @@ -35,7 +37,7 @@ export async function generateSecurityNumberBlock( const ourUuid = window.textsecure.storage.user.getUuid(); const us = window.textsecure.storage.protocol.getIdentityRecord( - ourUuid || ourNumber + ourUuid || ourNumber || '' ); const ourKey = us ? us.publicKey : null; @@ -57,6 +59,7 @@ export async function generateSecurityNumberBlock( return []; } + assert(ourNumber, 'Should have our number'); const securityNumber = await generateSecurityNumber( ourNumber, ourKey, diff --git a/ts/util/sendToGroup.ts b/ts/util/sendToGroup.ts index c8e0eb3cfb..127af8bc28 100644 --- a/ts/util/sendToGroup.ts +++ b/ts/util/sendToGroup.ts @@ -11,6 +11,7 @@ import { SenderCertificate, UnidentifiedSenderMessageContent, } from '@signalapp/signal-client'; +import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto'; import { senderCertificateService } from '../services/senderCertificate'; import { padMessage, @@ -371,8 +372,8 @@ export async function sendToGroupViaSenderKey(options: { const accessKeys = getXorOfAccessKeys(devicesForSenderKey); const result = await window.textsecure.messaging.sendWithSenderKey( - messageBuffer, - accessKeys, + toArrayBuffer(messageBuffer), + toArrayBuffer(accessKeys), timestamp, online ); diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index fa3bc49b47..f156fefa28 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -460,9 +460,9 @@ Whisper.ConversationView = Whisper.View.extend({ const { model }: { model: ConversationModel } = this; if (value) { - const pinnedConversationIds = window.storage.get>( + const pinnedConversationIds = window.storage.get( 'pinnedConversationIds', - [] + new Array() ); if (pinnedConversationIds.length >= 4) { @@ -3880,14 +3880,14 @@ Whisper.ConversationView = Whisper.View.extend({ } if ( isDirectConversation(this.model.attributes) && - (window.storage.isBlocked(this.model.get('e164')) || - window.storage.isUuidBlocked(this.model.get('uuid'))) + (window.storage.blocked.isBlocked(this.model.get('e164')) || + window.storage.blocked.isUuidBlocked(this.model.get('uuid'))) ) { ToastView = Whisper.BlockedToast; } if ( !isDirectConversation(this.model.attributes) && - window.storage.isGroupBlocked(this.model.get('groupId')) + window.storage.blocked.isGroupBlocked(this.model.get('groupId')) ) { ToastView = Whisper.BlockedGroupToast; } diff --git a/ts/window.d.ts b/ts/window.d.ts index 6e67245403..d485bc0641 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -20,6 +20,7 @@ import { ReactionModelType, } from './model-types.d'; import { ContactRecordIdentityState, TextSecureType } from './textsecure.d'; +import { Storage } from './textsecure/Storage'; import { ChallengeHandler, IPCRequest as IPCChallengeRequest, @@ -248,30 +249,7 @@ declare global { setMenuBarVisibility: (value: WhatIsThis) => void; showConfirmationDialog: (options: ConfirmationDialogViewProps) => void; showKeyboardShortcuts: () => void; - storage: { - addBlockedGroup: (group: string) => void; - addBlockedNumber: (number: string) => void; - addBlockedUuid: (uuid: string) => void; - fetch: () => void; - get: { - (key: string): T | undefined; - (key: string, defaultValue: T): T; - }; - getBlockedGroups: () => Array; - getBlockedNumbers: () => Array; - getBlockedUuids: () => Array; - getItemsState: () => WhatIsThis; - isBlocked: (number: string) => boolean; - isGroupBlocked: (group: unknown) => boolean; - isUuidBlocked: (uuid: string) => boolean; - onready: (callback: () => unknown) => void; - put: (key: string, value: any) => Promise; - remove: (key: string) => Promise; - removeBlockedGroup: (group: string) => void; - removeBlockedNumber: (number: string) => void; - removeBlockedUuid: (uuid: string) => void; - reset: () => void; - }; + storage: Storage; systemTheme: WhatIsThis; textsecure: TextSecureType; synchronousCrypto: typeof synchronousCrypto; @@ -595,6 +573,20 @@ declare global { interface Error { originalError?: Event; } + + // Uint8Array and ArrayBuffer are type-compatible in TypeScript's covariant + // type checker, but in reality they are not. Let's assert correct use! + interface Uint8Array { + __uint8array: never; + } + + interface ArrayBuffer { + __array_buffer: never; + } + + interface SharedArrayBuffer { + __array_buffer: never; + } } export type DCodeIOType = {