Enforce stronger types for ArrayBuffers and storage

This commit is contained in:
Fedor Indutny 2021-06-14 17:09:37 -07:00 committed by GitHub
parent 61ac79e9ae
commit 8f5086227a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 748 additions and 675 deletions

View file

@ -332,11 +332,8 @@
<script type='text/javascript' src='ts/backboneJquery.js'></script> <script type='text/javascript' src='ts/backboneJquery.js'></script>
<script type='text/javascript' src='js/reliable_trigger.js'></script> <script type='text/javascript' src='js/reliable_trigger.js'></script>
<script type='text/javascript' src='js/database.js'></script> <script type='text/javascript' src='js/database.js'></script>
<script type='text/javascript' src='js/storage.js'></script>
<script type='text/javascript' src='libtextsecure/protocol_wrapper.js'></script> <script type='text/javascript' src='libtextsecure/protocol_wrapper.js'></script>
<script type='text/javascript' src='libtextsecure/storage/user.js'></script>
<script type='text/javascript' src='libtextsecure/storage/unprocessed.js'></script>
<script type='text/javascript' src='libtextsecure/protobufs.js'></script> <script type='text/javascript' src='libtextsecure/protobufs.js'></script>
<script type='text/javascript' src='js/notifications.js'></script> <script type='text/javascript' src='js/notifications.js'></script>
@ -348,7 +345,6 @@
<script type='text/javascript' src='js/reactions.js'></script> <script type='text/javascript' src='js/reactions.js'></script>
<script type='text/javascript' src='js/deletes.js'></script> <script type='text/javascript' src='js/deletes.js'></script>
<script type='text/javascript' src='js/libphonenumber-util.js'></script> <script type='text/javascript' src='js/libphonenumber-util.js'></script>
<script type='text/javascript' src='js/models/blockedNumbers.js'></script>
<script type='text/javascript' src='js/expiring_messages.js'></script> <script type='text/javascript' src='js/expiring_messages.js'></script>
<script type='text/javascript' src='js/expiring_tap_to_view_messages.js'></script> <script type='text/javascript' src='js/expiring_tap_to_view_messages.js'></script>

View file

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

View file

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

View file

@ -1,12 +1,9 @@
// Copyright 2016-2020 Signal Messenger, LLC // Copyright 2016-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global window, textsecure, SignalProtocolStore */ /* global window, SignalProtocolStore */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function () {
window.textsecure = window.textsecure || {}; window.textsecure.storage.protocol = new SignalProtocolStore();
window.textsecure.storage = window.textsecure.storage || {};
textsecure.storage.protocol = new SignalProtocolStore();
})(); })();

View file

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

View file

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

View file

@ -22,18 +22,12 @@
<script type="text/javascript" src="../components.js"></script> <script type="text/javascript" src="../components.js"></script>
<script type="text/javascript" src="../protobufs.js" data-cover></script> <script type="text/javascript" src="../protobufs.js" data-cover></script>
<script type="text/javascript" src="../storage/user.js" data-cover></script>
<script type="text/javascript" src="../storage/unprocessed.js" data-cover></script>
<script type="text/javascript" src="../protocol_wrapper.js" data-cover></script> <script type="text/javascript" src="../protocol_wrapper.js" data-cover></script>
<script type="text/javascript" src="../../js/libphonenumber-util.js"></script> <script type="text/javascript" src="../../js/libphonenumber-util.js"></script>
<script type="text/javascript" src="../../js/components.js" data-cover></script> <script type="text/javascript" src="../../js/components.js" data-cover></script>
<script type="text/javascript" src="../../js/signal_protocol_store.js" data-cover></script>
<script type="text/javascript" src="../../js/storage.js" data-cover></script>
<script type="text/javascript" src="../../js/models/blockedNumbers.js" data-cover></script>
<script type="text/javascript" src="helpers_test.js"></script> <script type="text/javascript" src="helpers_test.js"></script>
<script type="text/javascript" src="storage_test.js"></script>
<script type="text/javascript" src="crypto_test.js"></script> <script type="text/javascript" src="crypto_test.js"></script>
<script type="text/javascript" src="contacts_parser_test.js"></script> <script type="text/javascript" src="contacts_parser_test.js"></script>
<script type="text/javascript" src="generate_keys_test.js"></script> <script type="text/javascript" src="generate_keys_test.js"></script>

View file

@ -10,11 +10,8 @@
<div id="root"></div> <div id="root"></div>
<script type="text/javascript" src="../../js/components.js"></script> <script type="text/javascript" src="../../js/components.js"></script>
<script type="text/javascript" src="../../ts/backbonejQuery.js"></script> <script type="text/javascript" src="../../ts/backbonejQuery.js"></script>
<script type="text/javascript" src="../../js/storage.js"></script>
<script type='text/javascript' src='../../libtextsecure/protocol_wrapper.js'></script> <script type='text/javascript' src='../../libtextsecure/protocol_wrapper.js'></script>
<script type='text/javascript' src='../../libtextsecure/storage/user.js'></script>
<script type='text/javascript' src='../../libtextsecure/storage/unprocessed.js'></script>
<script type='text/javascript' src='../../libtextsecure/protobufs.js'></script> <script type='text/javascript' src='../../libtextsecure/protobufs.js'></script>
</body> </body>
</html> </html>

View file

@ -342,16 +342,11 @@
<script type="text/javascript" src="test.js"></script> <script type="text/javascript" src="test.js"></script>
<script type="text/javascript" src="../js/database.js" data-cover></script> <script type="text/javascript" src="../js/database.js" data-cover></script>
<script type="text/javascript" src="../js/storage.js" data-cover></script>
<script type="text/javascript" src="../libtextsecure/protocol_wrapper.js"></script> <script type="text/javascript" src="../libtextsecure/protocol_wrapper.js"></script>
<script type="text/javascript" src="../libtextsecure/storage/user.js"></script>
<script type="text/javascript" src="../libtextsecure/storage/unprocessed.js"></script>
<script type="text/javascript" src="../libtextsecure/protobufs.js"></script> <script type="text/javascript" src="../libtextsecure/protobufs.js"></script>
<script type="text/javascript" src="../js/libphonenumber-util.js"></script> <script type="text/javascript" src="../js/libphonenumber-util.js"></script>
<script type="text/javascript" src="../js/models/blockedNumbers.js" data-cover></script>
<script type="text/javascript" src="../js/message_controller.js" data-cover></script> <script type="text/javascript" src="../js/message_controller.js" data-cover></script>
<script type="text/javascript" src="../js/keychange_listener.js" data-cover></script> <script type="text/javascript" src="../js/keychange_listener.js" data-cover></script>
<script type='text/javascript' src='../js/expiring_messages.js' data-cover></script> <script type='text/javascript' src='../js/expiring_messages.js' data-cover></script>
@ -375,7 +370,6 @@
<script type="text/javascript" src="models/conversations_test.js"></script> <script type="text/javascript" src="models/conversations_test.js"></script>
<script type="text/javascript" src="libphonenumber_util_test.js"></script> <script type="text/javascript" src="libphonenumber_util_test.js"></script>
<script type="text/javascript" src="storage_test.js"></script>
<script type="text/javascript" src="keychange_listener_test.js"></script> <script type="text/javascript" src="keychange_listener_test.js"></script>
<script type="text/javascript" src="reliable_trigger_test.js"></script> <script type="text/javascript" src="reliable_trigger_test.js"></script>
<script type="text/javascript" src="backup_test.js"></script> <script type="text/javascript" src="backup_test.js"></script>

View file

@ -26,7 +26,7 @@ type ConfigValueType = {
enabledAt?: number; enabledAt?: number;
value?: unknown; value?: unknown;
}; };
type ConfigMapType = { [key: string]: ConfigValueType }; export type ConfigMapType = { [key: string]: ConfigValueType };
type ConfigListenerType = (value: ConfigValueType) => unknown; type ConfigListenerType = (value: ConfigValueType) => unknown;
type ConfigListenersMapType = { type ConfigListenersMapType = {
[key: string]: Array<ConfigListenerType>; [key: string]: Array<ConfigListenerType>;

View file

@ -36,6 +36,7 @@ import {
KeyPairType, KeyPairType,
IdentityKeyType, IdentityKeyType,
SenderKeyType, SenderKeyType,
SessionResetsType,
SessionType, SessionType,
SignedPreKeyType, SignedPreKeyType,
OuterSignedPrekeyType, OuterSignedPrekeyType,
@ -114,8 +115,6 @@ type MapFields =
| 'sessions' | 'sessions'
| 'signedPreKeys'; | 'signedPreKeys';
type SessionResetsType = Record<string, number>;
export type SessionTransactionOptions = { export type SessionTransactionOptions = {
readonly zone?: Zone; readonly zone?: Zone;
}; };
@ -1199,8 +1198,8 @@ export class SignalProtocolStore extends EventsMixin {
const sessionResets = window.storage.get( const sessionResets = window.storage.get(
'sessionResets', 'sessionResets',
{} <SessionResetsType>{}
) as SessionResetsType; );
const lastReset = sessionResets[id]; const lastReset = sessionResets[id];

View file

@ -9,6 +9,7 @@ import {
} from '@signalapp/signal-client'; } from '@signalapp/signal-client';
import { DataMessageClass } from './textsecure.d'; import { DataMessageClass } from './textsecure.d';
import { SessionResetsType } from './textsecure/Types.d';
import { MessageAttributesType } from './model-types.d'; import { MessageAttributesType } from './model-types.d';
import { WhatIsThis } from './window.d'; import { WhatIsThis } from './window.d';
import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings'; import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings';
@ -49,11 +50,10 @@ export function isOverHourIntoPast(timestamp: number): boolean {
return isNumber(timestamp) && isOlderThan(timestamp, HOUR); return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
} }
type SessionResetsType = Record<string, number>;
export async function cleanupSessionResets(): Promise<void> { export async function cleanupSessionResets(): Promise<void> {
const sessionResets = window.storage.get<SessionResetsType>( const sessionResets = window.storage.get(
'sessionResets', 'sessionResets',
{} <SessionResetsType>{}
); );
const keys = Object.keys(sessionResets); const keys = Object.keys(sessionResets);
@ -326,9 +326,9 @@ 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'); const OLD_USERNAME = window.storage.get('number_id', '');
const USERNAME = window.storage.get('uuid_id'); const USERNAME = window.storage.get('uuid_id', '');
const PASSWORD = window.storage.get('password'); const PASSWORD = window.storage.get('password', '');
accountManager = new window.textsecure.AccountManager( accountManager = new window.textsecure.AccountManager(
USERNAME || OLD_USERNAME, USERNAME || OLD_USERNAME,
PASSWORD PASSWORD
@ -498,8 +498,7 @@ export async function startApp(): Promise<void> {
getAutoLaunch: () => window.getAutoLaunch(), getAutoLaunch: () => window.getAutoLaunch(),
setAutoLaunch: (value: boolean) => window.setAutoLaunch(value), 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: () => getSyncRequest: () =>
new Promise<void>((resolve, reject) => { new Promise<void>((resolve, reject) => {
const FIVE_MINUTES = 5 * 60 * 60 * 1000; const FIVE_MINUTES = 5 * 60 * 60 * 1000;
@ -680,7 +679,7 @@ export async function startApp(): Promise<void> {
}; };
// How long since we were last running? // 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()); await window.storage.put('lastStartup', Date.now());
const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000; const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;
@ -1962,10 +1961,13 @@ export async function startApp(): Promise<void> {
messageReceiver = null; messageReceiver = null;
} }
const OLD_USERNAME = window.storage.get('number_id'); const OLD_USERNAME = window.storage.get('number_id', '');
const USERNAME = window.storage.get('uuid_id'); const USERNAME = window.storage.get('uuid_id', '');
const PASSWORD = window.storage.get('password'); const PASSWORD = window.storage.get('password', '');
const mySignalingKey = window.storage.get('signaling_key'); const mySignalingKey = window.storage.get(
'signaling_key',
new ArrayBuffer(0)
);
window.textsecure.messaging = new window.textsecure.MessageSender( window.textsecure.messaging = new window.textsecure.MessageSender(
USERNAME || OLD_USERNAME, USERNAME || OLD_USERNAME,
@ -2097,8 +2099,7 @@ export async function startApp(): Promise<void> {
!firstRun && !firstRun &&
connectCount === 1 && connectCount === 1 &&
newVersion && 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.log.info('Boot after upgrading. Requesting contact sync');
window.getSyncRequest(); window.getSyncRequest();
@ -2147,11 +2148,11 @@ export async function startApp(): Promise<void> {
}); });
try { try {
const { uuid } = await server.whoami(); const { uuid } = await server.whoami();
window.textsecure.storage.user.setUuidAndDeviceId( assert(deviceId, 'We should have device id');
uuid, window.textsecure.storage.user.setUuidAndDeviceId(uuid, deviceId);
deviceId as WhatIsThis
);
const ourNumber = window.textsecure.storage.user.getNumber(); const ourNumber = window.textsecure.storage.user.getNumber();
assert(ourNumber, 'We should have number');
const me = await window.ConversationController.getOrCreateAndWait( const me = await window.ConversationController.getOrCreateAndWait(
ourNumber, ourNumber,
'private' 'private'
@ -2188,7 +2189,7 @@ export async function startApp(): Promise<void> {
} }
} }
if (firstRun === true && deviceId !== '1') { if (firstRun === true && deviceId !== 1) {
const hasThemeSetting = Boolean(window.storage.get('theme-setting')); const hasThemeSetting = Boolean(window.storage.get('theme-setting'));
if ( if (
!hasThemeSetting && !hasThemeSetting &&
@ -3339,7 +3340,9 @@ export async function startApp(): Promise<void> {
// These two bits of data are important to ensure that the app loads up // 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. // the conversation list, instead of showing just the QR code screen.
window.Signal.Util.Registration.markEverDone(); window.Signal.Util.Registration.markEverDone();
if (previousNumberId !== undefined) {
await window.textsecure.storage.put(NUMBER_ID_KEY, previousNumberId); await window.textsecure.storage.put(NUMBER_ID_KEY, previousNumberId);
}
// These two are important to ensure we don't rip through every message // These two are important to ensure we don't rip through every message
// in the database attempting to upgrade it after starting up again. // in the database attempting to upgrade it after starting up again.
@ -3347,10 +3350,14 @@ export async function startApp(): Promise<void> {
IS_MIGRATION_COMPLETE_KEY, IS_MIGRATION_COMPLETE_KEY,
isMigrationComplete || false isMigrationComplete || false
); );
if (lastProcessedIndex !== undefined) {
await window.textsecure.storage.put( await window.textsecure.storage.put(
LAST_PROCESSED_INDEX_KEY, LAST_PROCESSED_INDEX_KEY,
lastProcessedIndex || null lastProcessedIndex
); );
} else {
await window.textsecure.storage.remove(LAST_PROCESSED_INDEX_KEY);
}
await window.textsecure.storage.put(VERSION_KEY, window.getVersion()); await window.textsecure.storage.put(VERSION_KEY, window.getVersion());
window.log.info('Successfully cleared local configuration'); window.log.info('Successfully cleared local configuration');

View file

@ -19,6 +19,7 @@ import { isNotNil } from './util/isNotNil';
import { isOlderThan } from './util/timestamp'; import { isOlderThan } from './util/timestamp';
import { parseRetryAfter } from './util/parseRetryAfter'; import { parseRetryAfter } from './util/parseRetryAfter';
import { getEnvironment, Environment } from './environment'; import { getEnvironment, Environment } from './environment';
import { StorageInterface } from './types/Storage.d';
export type ChallengeResponse = { export type ChallengeResponse = {
readonly captcha: string; readonly captcha: string;
@ -62,10 +63,7 @@ export type MinimalMessage = Pick<
}; };
export type Options = { export type Options = {
readonly storage: { readonly storage: Pick<StorageInterface, 'get' | 'put'>;
get(key: string): ReadonlyArray<StoredEntity>;
put(key: string, value: ReadonlyArray<StoredEntity>): Promise<void>;
};
requestChallenge(request: IPCRequest): void; requestChallenge(request: IPCRequest): void;

View file

@ -2171,8 +2171,8 @@ export async function initiateMigrationToGroupV2(
}, },
}); });
if (window.storage.isGroupBlocked(previousGroupV1Id)) { if (window.storage.blocked.isGroupBlocked(previousGroupV1Id)) {
window.storage.addBlockedGroup(groupId); window.storage.blocked.addBlockedGroup(groupId);
} }
// Save these most recent updates to conversation // Save these most recent updates to conversation
@ -2646,8 +2646,8 @@ export async function respondToGroupV2Migration({
}, },
}); });
if (window.storage.isGroupBlocked(previousGroupV1Id)) { if (window.storage.blocked.isGroupBlocked(previousGroupV1Id)) {
window.storage.addBlockedGroup(groupId); window.storage.blocked.addBlockedGroup(groupId);
} }
// Save these most recent updates to conversation // Save these most recent updates to conversation

View file

@ -7,7 +7,7 @@ import { JobQueue } from './JobQueue';
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
const removeStorageKeyJobDataSchema = z.object({ const removeStorageKeyJobDataSchema = z.object({
key: z.string().min(1), key: z.enum(['senderCertificateWithUuid']),
}); });
type RemoveStorageKeyJobData = z.infer<typeof removeStorageKeyJobDataSchema>; type RemoveStorageKeyJobData = z.infer<typeof removeStorageKeyJobDataSchema>;

View file

@ -816,17 +816,17 @@ export class ConversationModel extends window.Backbone
isBlocked(): boolean { isBlocked(): boolean {
const uuid = this.get('uuid'); const uuid = this.get('uuid');
if (uuid) { if (uuid) {
return window.storage.isUuidBlocked(uuid); return window.storage.blocked.isUuidBlocked(uuid);
} }
const e164 = this.get('e164'); const e164 = this.get('e164');
if (e164) { if (e164) {
return window.storage.isBlocked(e164); return window.storage.blocked.isBlocked(e164);
} }
const groupId = this.get('groupId'); const groupId = this.get('groupId');
if (groupId) { if (groupId) {
return window.storage.isGroupBlocked(groupId); return window.storage.blocked.isGroupBlocked(groupId);
} }
return false; return false;
@ -838,19 +838,19 @@ export class ConversationModel extends window.Backbone
const uuid = this.get('uuid'); const uuid = this.get('uuid');
if (uuid) { if (uuid) {
window.storage.addBlockedUuid(uuid); window.storage.blocked.addBlockedUuid(uuid);
blocked = true; blocked = true;
} }
const e164 = this.get('e164'); const e164 = this.get('e164');
if (e164) { if (e164) {
window.storage.addBlockedNumber(e164); window.storage.blocked.addBlockedNumber(e164);
blocked = true; blocked = true;
} }
const groupId = this.get('groupId'); const groupId = this.get('groupId');
if (groupId) { if (groupId) {
window.storage.addBlockedGroup(groupId); window.storage.blocked.addBlockedGroup(groupId);
blocked = true; blocked = true;
} }
@ -865,19 +865,19 @@ export class ConversationModel extends window.Backbone
const uuid = this.get('uuid'); const uuid = this.get('uuid');
if (uuid) { if (uuid) {
window.storage.removeBlockedUuid(uuid); window.storage.blocked.removeBlockedUuid(uuid);
unblocked = true; unblocked = true;
} }
const e164 = this.get('e164'); const e164 = this.get('e164');
if (e164) { if (e164) {
window.storage.removeBlockedNumber(e164); window.storage.blocked.removeBlockedNumber(e164);
unblocked = true; unblocked = true;
} }
const groupId = this.get('groupId'); const groupId = this.get('groupId');
if (groupId) { if (groupId) {
window.storage.removeBlockedGroup(groupId); window.storage.blocked.removeBlockedGroup(groupId);
unblocked = true; unblocked = true;
} }
@ -2913,6 +2913,9 @@ export class ConversationModel extends window.Backbone
validateNumber(): string | null { validateNumber(): string | null {
if (isDirectConversation(this.attributes) && this.get('e164')) { if (isDirectConversation(this.attributes) && this.get('e164')) {
const regionCode = window.storage.get('regionCode'); const regionCode = window.storage.get('regionCode');
if (!regionCode) {
throw new Error('No region code');
}
const number = window.libphonenumber.util.parseNumber( const number = window.libphonenumber.util.parseNumber(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.get('e164')!, this.get('e164')!,
@ -5256,7 +5259,7 @@ export class ConversationModel extends window.Backbone
window.log.info('pinning', this.idForLogging()); window.log.info('pinning', this.idForLogging());
const pinnedConversationIds = new Set( const pinnedConversationIds = new Set(
window.storage.get<Array<string>>('pinnedConversationIds', []) window.storage.get('pinnedConversationIds', new Array<string>())
); );
pinnedConversationIds.add(this.id); pinnedConversationIds.add(this.id);
@ -5279,7 +5282,7 @@ export class ConversationModel extends window.Backbone
window.log.info('un-pinning', this.idForLogging()); window.log.info('un-pinning', this.idForLogging());
const pinnedConversationIds = new Set( const pinnedConversationIds = new Set(
window.storage.get<Array<string>>('pinnedConversationIds', []) window.storage.get('pinnedConversationIds', new Array<string>())
); );
pinnedConversationIds.delete(this.id); pinnedConversationIds.delete(this.id);

View file

@ -507,7 +507,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
_.find(errorsForContact, error => error.name === OUTGOING_KEY_ERROR) _.find(errorsForContact, error => error.name === OUTGOING_KEY_ERROR)
); );
const isUnidentifiedDelivery = const isUnidentifiedDelivery =
window.storage.get('unidentifiedDeliveryIndicators') && window.storage.get('unidentifiedDeliveryIndicators', false) &&
this.isUnidentifiedDelivery(id, unidentifiedLookup); this.isUnidentifiedDelivery(id, unidentifiedLookup);
return { return {
@ -1189,6 +1189,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
} }
const regionCode = window.storage.get('regionCode'); const regionCode = window.storage.get('regionCode');
if (!regionCode) {
throw new Error('No region code');
}
const { contactSelector } = Contact; const { contactSelector } = Contact;
const contact = contacts[0]; const contact = contacts[0];
const firstNumber = const firstNumber =

View file

@ -14,6 +14,7 @@ import { isNormalNumber } from './util/isNormalNumber';
import { take } from './util/iterables'; import { take } from './util/iterables';
import { isOlderThan } from './util/timestamp'; import { isOlderThan } from './util/timestamp';
import { ConversationModel } from './models/conversations'; import { ConversationModel } from './models/conversations';
import { StorageInterface } from './types/Storage.d';
const STORAGE_KEY = 'lastAttemptedToRefreshProfilesAt'; const STORAGE_KEY = 'lastAttemptedToRefreshProfilesAt';
const MAX_AGE_TO_BE_CONSIDERED_ACTIVE = 30 * 24 * 60 * 60 * 1000; 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 MAX_CONVERSATIONS_TO_REFRESH = 50;
const MIN_ELAPSED_DURATION_TO_REFRESH_AGAIN = 12 * 3600 * 1000; 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<void>;
};
export async function routineProfileRefresh({ export async function routineProfileRefresh({
allConversations, allConversations,
ourConversationId, ourConversationId,
@ -35,7 +29,7 @@ export async function routineProfileRefresh({
}: { }: {
allConversations: Array<ConversationModel>; allConversations: Array<ConversationModel>;
ourConversationId: string; ourConversationId: string;
storage: StorageType; storage: Pick<StorageInterface, 'get' | 'put'>;
}): Promise<void> { }): Promise<void> {
log.info('routineProfileRefresh: starting'); log.info('routineProfileRefresh: starting');
@ -93,7 +87,9 @@ export async function routineProfileRefresh({
); );
} }
function hasEnoughTimeElapsedSinceLastRefresh(storage: StorageType): boolean { function hasEnoughTimeElapsedSinceLastRefresh(
storage: Pick<StorageInterface, 'get'>
): boolean {
const storedValue = storage.get(STORAGE_KEY); const storedValue = storage.get(STORAGE_KEY);
if (isNil(storedValue)) { if (isNil(storedValue)) {

View file

@ -55,6 +55,7 @@ import {
uuidToArrayBuffer, uuidToArrayBuffer,
arrayBufferToUuid, arrayBufferToUuid,
} from '../Crypto'; } from '../Crypto';
import { assert } from '../util/assert';
import { getOwn } from '../util/getOwn'; import { getOwn } from '../util/getOwn';
import { import {
fetchMembershipProof, fetchMembershipProof,
@ -1257,9 +1258,13 @@ export class CallingClass {
} }
const senderIdentityKey = senderIdentityRecord.publicKey.slice(1); // Ignore the type header, it is not used. 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.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) { if (!receiverIdentityRecord) {
window.log.error( window.log.error(

View file

@ -4,22 +4,16 @@
import { assert } from '../util/assert'; import { assert } from '../util/assert';
import * as log from '../logging/log'; import * as log from '../logging/log';
// We define a stricter storage here that returns `unknown` instead of `any`. import { StorageInterface } from '../types/Storage.d';
type Storage = {
get(key: string): unknown;
put(key: string, value: unknown): Promise<void>;
remove(key: string): Promise<void>;
onready: (callback: () => unknown) => void;
};
export class OurProfileKeyService { export class OurProfileKeyService {
private getPromise: undefined | Promise<undefined | ArrayBuffer>; private getPromise: undefined | Promise<undefined | ArrayBuffer>;
private promisesBlockingGet: Array<Promise<unknown>> = []; private promisesBlockingGet: Array<Promise<unknown>> = [];
private storage?: Storage; private storage?: StorageInterface;
initialize(storage: Storage): void { initialize(storage: StorageInterface): void {
log.info('Our profile key service: initializing'); log.info('Our profile key service: initializing');
const storageReadyPromise = new Promise<void>(resolve => { const storageReadyPromise = new Promise<void>(resolve => {

View file

@ -13,13 +13,7 @@ import { missingCaseError } from '../util/missingCaseError';
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 { connectToServerWithStoredCredentials } from '../util/connectToServerWithStoredCredentials';
import { StorageInterface } from '../types/Storage.d';
// We define a stricter storage here that returns `unknown` instead of `any`.
type Storage = {
get(key: string): unknown;
put(key: string, value: unknown): Promise<void>;
remove(key: string): Promise<void>;
};
function isWellFormed(data: unknown): data is SerializedCertificateType { function isWellFormed(data: unknown): data is SerializedCertificateType {
return serializedCertificateSchema.safeParse(data).success; return serializedCertificateSchema.safeParse(data).success;
@ -43,7 +37,7 @@ export class SenderCertificateService {
private onlineEventTarget?: EventTarget; private onlineEventTarget?: EventTarget;
private storage?: Storage; private storage?: StorageInterface;
initialize({ initialize({
SenderCertificate, SenderCertificate,
@ -56,7 +50,7 @@ export class SenderCertificateService {
navigator: Readonly<{ onLine: boolean }>; navigator: Readonly<{ onLine: boolean }>;
onlineEventTarget: EventTarget; onlineEventTarget: EventTarget;
SenderCertificate: typeof SenderCertificateClass; SenderCertificate: typeof SenderCertificateClass;
storage: Storage; storage: StorageInterface;
}): void { }): void {
log.info('Sender certificate service initialized'); log.info('Sender certificate service initialized');

View file

@ -91,6 +91,9 @@ async function encryptRecord(
: generateStorageID(); : generateStorageID();
const storageKeyBase64 = window.storage.get('storageKey'); const storageKeyBase64 = window.storage.get('storageKey');
if (!storageKeyBase64) {
throw new Error('No storage key');
}
const storageKey = base64ToArrayBuffer(storageKeyBase64); const storageKey = base64ToArrayBuffer(storageKeyBase64);
const storageItemKey = await deriveStorageItemKey( const storageItemKey = await deriveStorageItemKey(
storageKey, storageKey,
@ -260,8 +263,10 @@ async function generateManifest(
manifestRecordKeys.add(identifier); manifestRecordKeys.add(identifier);
}); });
const recordsWithErrors: ReadonlyArray<UnknownRecord> = const recordsWithErrors: ReadonlyArray<UnknownRecord> = window.storage.get(
window.storage.get('storage-service-error-records') || []; 'storage-service-error-records',
new Array<UnknownRecord>()
);
window.log.info( window.log.info(
'storageService.generateManifest: adding records that had errors in the previous merge', 'storageService.generateManifest: adding records that had errors in the previous merge',
@ -406,6 +411,9 @@ async function generateManifest(
manifestRecord.keys = Array.from(manifestRecordKeys); manifestRecord.keys = Array.from(manifestRecordKeys);
const storageKeyBase64 = window.storage.get('storageKey'); const storageKeyBase64 = window.storage.get('storageKey');
if (!storageKeyBase64) {
throw new Error('No storage key');
}
const storageKey = base64ToArrayBuffer(storageKeyBase64); const storageKey = base64ToArrayBuffer(storageKeyBase64);
const storageManifestKey = await deriveStorageManifestKey( const storageManifestKey = await deriveStorageManifestKey(
storageKey, storageKey,
@ -539,7 +547,7 @@ async function stopStorageServiceSync() {
async function createNewManifest() { async function createNewManifest() {
window.log.info('storageService.createNewManifest: creating new manifest'); window.log.info('storageService.createNewManifest: creating new manifest');
const version = window.storage.get('manifestVersion') || 0; const version = window.storage.get('manifestVersion', 0);
const { const {
conversationsToUpdate, conversationsToUpdate,
@ -562,6 +570,9 @@ async function decryptManifest(
const { version, value } = encryptedManifest; const { version, value } = encryptedManifest;
const storageKeyBase64 = window.storage.get('storageKey'); const storageKeyBase64 = window.storage.get('storageKey');
if (!storageKeyBase64) {
throw new Error('No storage key');
}
const storageKey = base64ToArrayBuffer(storageKeyBase64); const storageKey = base64ToArrayBuffer(storageKeyBase64);
const storageManifestKey = await deriveStorageManifestKey( const storageManifestKey = await deriveStorageManifestKey(
storageKey, storageKey,
@ -577,7 +588,7 @@ async function decryptManifest(
} }
async function fetchManifest( async function fetchManifest(
manifestVersion: string manifestVersion: number
): Promise<ManifestRecordClass | undefined> { ): Promise<ManifestRecordClass | undefined> {
window.log.info('storageService.fetchManifest'); window.log.info('storageService.fetchManifest');
@ -799,6 +810,9 @@ async function processRemoteRecords(
remoteOnlyRecords: Map<string, RemoteRecord> remoteOnlyRecords: Map<string, RemoteRecord>
): Promise<number> { ): Promise<number> {
const storageKeyBase64 = window.storage.get('storageKey'); const storageKeyBase64 = window.storage.get('storageKey');
if (!storageKeyBase64) {
throw new Error('No storage key');
}
const storageKey = base64ToArrayBuffer(storageKeyBase64); const storageKey = base64ToArrayBuffer(storageKeyBase64);
window.log.info( window.log.info(
@ -911,8 +925,10 @@ async function processRemoteRecords(
// Collect full map of previously and currently unknown records // Collect full map of previously and currently unknown records
const unknownRecords: Map<string, UnknownRecord> = new Map(); const unknownRecords: Map<string, UnknownRecord> = new Map();
const unknownRecordsArray: ReadonlyArray<UnknownRecord> = const unknownRecordsArray: ReadonlyArray<UnknownRecord> = window.storage.get(
window.storage.get('storage-service-unknown-records') || []; 'storage-service-unknown-records',
new Array<UnknownRecord>()
);
unknownRecordsArray.forEach((record: UnknownRecord) => { unknownRecordsArray.forEach((record: UnknownRecord) => {
unknownRecords.set(record.storageID, record); unknownRecords.set(record.storageID, record);
}); });
@ -1087,7 +1103,7 @@ async function upload(fromSync = false): Promise<void> {
previousManifest = await sync(); previousManifest = await sync();
} }
const localManifestVersion = window.storage.get('manifestVersion') || 0; const localManifestVersion = window.storage.get('manifestVersion', 0);
const version = Number(localManifestVersion) + 1; const version = Number(localManifestVersion) + 1;
window.log.info( window.log.info(

View file

@ -229,7 +229,7 @@ export async function toAccountRecord(
} }
const pinnedConversations = window.storage const pinnedConversations = window.storage
.get<Array<string>>('pinnedConversationIds', []) .get('pinnedConversationIds', new Array<string>())
.map(id => { .map(id => {
const pinnedConversation = window.ConversationController.get(id); const pinnedConversation = window.ConversationController.get(id);
@ -824,7 +824,7 @@ export async function mergeAccountRecord(
universalExpireTimer, universalExpireTimer,
} = accountRecord; } = accountRecord;
window.storage.put('read-receipt-setting', readReceipts); window.storage.put('read-receipt-setting', Boolean(readReceipts));
if (typeof sealedSenderIndicators === 'boolean') { if (typeof sealedSenderIndicators === 'boolean') {
window.storage.put('sealedSenderIndicators', sealedSenderIndicators); window.storage.put('sealedSenderIndicators', sealedSenderIndicators);
@ -890,7 +890,7 @@ export async function mergeAccountRecord(
); );
const missingStoragePinnedConversationIds = window.storage const missingStoragePinnedConversationIds = window.storage
.get<Array<string>>('pinnedConversationIds', []) .get('pinnedConversationIds', new Array<string>())
.filter(id => !modelPinnedConversationIds.includes(id)); .filter(id => !modelPinnedConversationIds.includes(id));
if (missingStoragePinnedConversationIds.length !== 0) { if (missingStoragePinnedConversationIds.length !== 0) {

View file

@ -1,13 +1,16 @@
// Copyright 2019-2020 Signal Messenger, LLC // Copyright 2019-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { StorageAccessType } from '../types/Storage.d';
// Matching window.storage.put API // Matching window.storage.put API
// eslint-disable-next-line max-len export function put<K extends keyof StorageAccessType>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types key: K,
export function put(key: string, value: any): void { value: StorageAccessType[K]
): void {
window.storage.put(key, value); window.storage.put(key, value);
} }
export async function remove(key: string): Promise<void> { export async function remove(key: keyof StorageAccessType): Promise<void> {
await window.storage.remove(key); await window.storage.remove(key);
} }

View file

@ -7,6 +7,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable no-restricted-syntax */
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { import {
@ -44,6 +45,7 @@ import {
ClientJobType, ClientJobType,
ConversationType, ConversationType,
IdentityKeyType, IdentityKeyType,
ItemKeyType,
ItemType, ItemType,
MessageType, MessageType,
MessageTypeUnhydrated, MessageTypeUnhydrated,
@ -132,7 +134,6 @@ const dataInterface: ClientInterface = {
createOrUpdateItem, createOrUpdateItem,
getItemById, getItemById,
getAllItems, getAllItems,
bulkAddItems,
removeItemById, removeItemById,
removeAllItems, removeAllItems,
@ -692,14 +693,14 @@ async function removeAllSignedPreKeys() {
// Items // Items
const ITEM_KEYS: { [key: string]: Array<string> | undefined } = { const ITEM_KEYS: Partial<Record<ItemKeyType, Array<string>>> = {
identityKey: ['value.pubKey', 'value.privKey'], identityKey: ['value.pubKey', 'value.privKey'],
senderCertificate: ['value.serialized'], senderCertificate: ['value.serialized'],
senderCertificateNoE164: ['value.serialized'], senderCertificateNoE164: ['value.serialized'],
signaling_key: ['value'], signaling_key: ['value'],
profileKey: ['value'], profileKey: ['value'],
}; };
async function createOrUpdateItem(data: ItemType) { async function createOrUpdateItem<K extends ItemKeyType>(data: ItemType<K>) {
const { id } = data; const { id } = data;
if (!id) { if (!id) {
throw new Error( throw new Error(
@ -712,7 +713,7 @@ async function createOrUpdateItem(data: ItemType) {
await channels.createOrUpdateItem(updated); await channels.createOrUpdateItem(updated);
} }
async function getItemById(id: string) { async function getItemById<K extends ItemKeyType>(id: K): Promise<ItemType<K>> {
const keys = ITEM_KEYS[id]; const keys = ITEM_KEYS[id];
const data = await channels.getItemById(id); const data = await channels.getItemById(id);
@ -721,23 +722,24 @@ async function getItemById(id: string) {
async function getAllItems() { async function getAllItems() {
const items = await channels.getAllItems(); const items = await channels.getAllItems();
return map(items, item => { const result = Object.create(null);
const { id } = item;
const keys = ITEM_KEYS[id];
return Array.isArray(keys) ? keysToArrayBuffer(keys, item) : item; for (const id of Object.keys(items)) {
}); const key = id as ItemKeyType;
} const value = items[key];
async function bulkAddItems(array: Array<ItemType>) {
const updated = map(array, data => {
const { id } = data;
const keys = ITEM_KEYS[id];
return keys && Array.isArray(keys) ? keysFromArrayBuffer(keys, data) : data; const keys = ITEM_KEYS[key];
});
await channels.bulkAddItems(updated); 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); await channels.removeItemById(id);
} }
async function removeAllItems() { async function removeAllItems() {

View file

@ -15,6 +15,7 @@ import { ConversationModel } from '../models/conversations';
import { StoredJob } from '../jobs/types'; import { StoredJob } from '../jobs/types';
import { ReactionType } from '../types/Reactions'; import { ReactionType } from '../types/Reactions';
import { ConversationColorType, CustomColorType } from '../types/Colors'; import { ConversationColorType, CustomColorType } from '../types/Colors';
import { StorageAccessType } from '../types/Storage.d';
export type AttachmentDownloadJobType = { export type AttachmentDownloadJobType = {
id: string; id: string;
@ -48,7 +49,12 @@ export type IdentityKeyType = {
timestamp: number; timestamp: number;
verified: number; verified: number;
}; };
export type ItemType = any; export type ItemKeyType = keyof StorageAccessType;
export type AllItemsType = Partial<StorageAccessType>;
export type ItemType<K extends ItemKeyType> = {
id: K;
value: StorageAccessType[K];
};
export type MessageType = MessageAttributesType; export type MessageType = MessageAttributesType;
export type MessageTypeUnhydrated = { export type MessageTypeUnhydrated = {
json: string; json: string;
@ -177,12 +183,11 @@ export type DataInterface = {
removeAllSignedPreKeys: () => Promise<void>; removeAllSignedPreKeys: () => Promise<void>;
getAllSignedPreKeys: () => Promise<Array<SignedPreKeyType>>; getAllSignedPreKeys: () => Promise<Array<SignedPreKeyType>>;
createOrUpdateItem: (data: ItemType) => Promise<void>; createOrUpdateItem<K extends ItemKeyType>(data: ItemType<K>): Promise<void>;
getItemById: (id: string) => Promise<ItemType | undefined>; getItemById<K extends ItemKeyType>(id: K): Promise<ItemType<K> | undefined>;
bulkAddItems: (array: Array<ItemType>) => Promise<void>; removeItemById: (id: ItemKeyType) => Promise<void>;
removeItemById: (id: string) => Promise<void>;
removeAllItems: () => Promise<void>; removeAllItems: () => Promise<void>;
getAllItems: () => Promise<Array<ItemType>>; getAllItems: () => Promise<AllItemsType>;
createOrUpdateSenderKey: (key: SenderKeyType) => Promise<void>; createOrUpdateSenderKey: (key: SenderKeyType) => Promise<void>;
getSenderKeyById: (id: string) => Promise<SenderKeyType | undefined>; getSenderKeyById: (id: string) => Promise<SenderKeyType | undefined>;

View file

@ -44,6 +44,8 @@ import {
ConversationType, ConversationType,
EmojiType, EmojiType,
IdentityKeyType, IdentityKeyType,
AllItemsType,
ItemKeyType,
ItemType, ItemType,
MessageType, MessageType,
MessageTypeUnhydrated, MessageTypeUnhydrated,
@ -123,7 +125,6 @@ const dataInterface: ServerInterface = {
createOrUpdateItem, createOrUpdateItem,
getItemById, getItemById,
getAllItems, getAllItems,
bulkAddItems,
removeItemById, removeItemById,
removeAllItems, removeAllItems,
@ -2170,24 +2171,34 @@ async function getAllSignedPreKeys(): Promise<Array<SignedPreKeyType>> {
} }
const ITEMS_TABLE = 'items'; const ITEMS_TABLE = 'items';
function createOrUpdateItem(data: ItemType): Promise<void> { function createOrUpdateItem<K extends ItemKeyType>(
data: ItemType<K>
): Promise<void> {
return createOrUpdate(ITEMS_TABLE, data); return createOrUpdate(ITEMS_TABLE, data);
} }
function getItemById(id: string): Promise<ItemType> { function getItemById<K extends ItemKeyType>(
id: K
): Promise<ItemType<K> | undefined> {
return getById(ITEMS_TABLE, id); return getById(ITEMS_TABLE, id);
} }
async function getAllItems(): Promise<Array<ItemType>> { async function getAllItems(): Promise<AllItemsType> {
const db = getInstance(); const db = getInstance();
const rows: JSONRows = db const rows: JSONRows = db
.prepare<EmptyQuery>('SELECT json FROM items ORDER BY id ASC;') .prepare<EmptyQuery>('SELECT json FROM items ORDER BY id ASC;')
.all(); .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<ItemType>): Promise<void> { function removeItemById(id: ItemKeyType): Promise<void> {
return bulkAdd(ITEMS_TABLE, array);
}
function removeItemById(id: string): Promise<void> {
return removeById(ITEMS_TABLE, id); return removeById(ITEMS_TABLE, id);
} }
function removeAllItems(): Promise<void> { function removeAllItems(): Promise<void> {

View file

@ -11,9 +11,11 @@ import {
ConversationColors, ConversationColors,
ConversationColorType, ConversationColorType,
CustomColorType, CustomColorType,
CustomColorsItemType,
DefaultConversationColorType, DefaultConversationColorType,
} from '../../types/Colors'; } from '../../types/Colors';
import { reloadSelectedConversation } from '../../shims/reloadSelectedConversation'; import { reloadSelectedConversation } from '../../shims/reloadSelectedConversation';
import { StorageAccessType } from '../../types/Storage.d';
// State // State
@ -25,10 +27,7 @@ export type ItemsStateType = {
// This property should always be set and this is ensured in background.ts // This property should always be set and this is ensured in background.ts
readonly defaultConversationColor?: DefaultConversationColorType; readonly defaultConversationColor?: DefaultConversationColorType;
readonly customColors?: { readonly customColors?: CustomColorsItemType;
readonly colors: Record<string, CustomColorType>;
readonly version: number;
};
}; };
// Actions // Actions
@ -85,7 +84,10 @@ export const actions = {
export const useActions = (): typeof actions => useBoundActions(actions); export const useActions = (): typeof actions => useBoundActions(actions);
function putItem(key: string, value: unknown): ItemPutAction { function putItem<K extends keyof StorageAccessType>(
key: K,
value: StorageAccessType[K]
): ItemPutAction {
storageShim.put(key, value); storageShim.put(key, value);
return { 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); storageShim.remove(key);
return { return {

View file

@ -118,10 +118,10 @@ describe('ChallengeHandler', () => {
expireAfter, expireAfter,
storage: { storage: {
get(key) { get(key: string) {
return storage.get(key); return storage.get(key);
}, },
async put(key, value) { async put(key: string, value: unknown) {
storage.set(key, value); storage.set(key, value);
}, },
}, },

View file

@ -4,6 +4,8 @@
import { assert } from 'chai'; import { assert } from 'chai';
import { size } from '../../util/iterables'; import { size } from '../../util/iterables';
import { typedArrayToArrayBuffer } from '../../Crypto';
import { getProvisioningUrl } from '../../util/getProvisioningUrl'; import { getProvisioningUrl } from '../../util/getProvisioningUrl';
describe('getProvisioningUrl', () => { describe('getProvisioningUrl', () => {
@ -11,7 +13,7 @@ describe('getProvisioningUrl', () => {
const uuid = 'a08bf1fd-1799-427f-a551-70af747e3956'; const uuid = 'a08bf1fd-1799-427f-a551-70af747e3956';
const publicKey = new Uint8Array([9, 8, 7, 6, 5, 4, 3]); 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); const resultUrl = new URL(result);
assert(result.startsWith('tsdevice:/?')); assert(result.startsWith('tsdevice:/?'));

View file

@ -18,7 +18,7 @@ describe('RetryPlaceholders', () => {
let clock: any; let clock: any;
beforeEach(() => { beforeEach(() => {
window.storage.put(STORAGE_KEY, null); window.storage.put(STORAGE_KEY, undefined as any);
clock = sinon.useFakeTimers({ clock = sinon.useFakeTimers({
now: NOW, now: NOW,
@ -55,7 +55,7 @@ describe('RetryPlaceholders', () => {
window.storage.put(STORAGE_KEY, [ window.storage.put(STORAGE_KEY, [
{ item: 'is wrong shape!' }, { item: 'is wrong shape!' },
{ bad: 'is not good!' }, { bad: 'is not good!' },
]); ] as any);
const placeholders = new RetryPlaceholders(); const placeholders = new RetryPlaceholders();

View file

@ -44,9 +44,9 @@ describe('synchronousCrypto', () => {
describe('encrypt+decrypt', () => { describe('encrypt+decrypt', () => {
it('returns original input', () => { it('returns original input', () => {
const iv = crypto.randomBytes(16); const iv = toArrayBuffer(crypto.randomBytes(16));
const key = crypto.randomBytes(32); const key = toArrayBuffer(crypto.randomBytes(32));
const input = Buffer.from('plaintext'); const input = toArrayBuffer(Buffer.from('plaintext'));
const ciphertext = encrypt(key, input, iv); const ciphertext = encrypt(key, input, iv);
const plaintext = decrypt(key, ciphertext, iv); const plaintext = decrypt(key, ciphertext, iv);

View file

@ -15,7 +15,11 @@ import { signal } from '../protobuf/compiled';
import { sessionStructureToArrayBuffer } from '../util/sessionTranslation'; import { sessionStructureToArrayBuffer } from '../util/sessionTranslation';
import { Zone } from '../util/Zone'; import { Zone } from '../util/Zone';
import { getRandomBytes, constantTimeEqual } from '../Crypto'; import {
getRandomBytes,
constantTimeEqual,
typedArrayToArrayBuffer as toArrayBuffer,
} from '../Crypto';
import { clampPrivateKey, setPublicKeyTypeByte } from '../Curve'; import { clampPrivateKey, setPublicKeyTypeByte } from '../Curve';
import { SignalProtocolStore, GLOBAL_ZONE } from '../SignalProtocolStore'; import { SignalProtocolStore, GLOBAL_ZONE } from '../SignalProtocolStore';
import { IdentityKeyType, KeyPairType } from '../textsecure/Types.d'; import { IdentityKeyType, KeyPairType } from '../textsecure/Types.d';
@ -173,7 +177,10 @@ describe('SignalProtocolStore', () => {
} }
assert.isTrue( assert.isTrue(
constantTimeEqual(expected.serialize(), actual.serialize()) constantTimeEqual(
toArrayBuffer(expected.serialize()),
toArrayBuffer(actual.serialize())
)
); );
await store.removeSenderKey(encodedAddress, distributionId); await store.removeSenderKey(encodedAddress, distributionId);
@ -203,7 +210,10 @@ describe('SignalProtocolStore', () => {
} }
assert.isTrue( assert.isTrue(
constantTimeEqual(expected.serialize(), actual.serialize()) constantTimeEqual(
toArrayBuffer(expected.serialize()),
toArrayBuffer(actual.serialize())
)
); );
await store.removeSenderKey(encodedAddress, distributionId); await store.removeSenderKey(encodedAddress, distributionId);

View file

@ -12,6 +12,8 @@ import * as sinon from 'sinon';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { connection as WebSocket } from 'websocket'; import { connection as WebSocket } from 'websocket';
import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto';
import WebSocketResource from '../textsecure/WebsocketResources'; import WebSocketResource from '../textsecure/WebsocketResources';
describe('WebSocket-Resource', () => { describe('WebSocket-Resource', () => {
@ -29,7 +31,7 @@ describe('WebSocket-Resource', () => {
sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => { sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => {
const message = window.textsecure.protobuf.WebSocketMessage.decode( const message = window.textsecure.protobuf.WebSocketMessage.decode(
data toArrayBuffer(data)
); );
assert.strictEqual( assert.strictEqual(
message.type, message.type,
@ -86,7 +88,7 @@ describe('WebSocket-Resource', () => {
sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => { sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => {
const message = window.textsecure.protobuf.WebSocketMessage.decode( const message = window.textsecure.protobuf.WebSocketMessage.decode(
data toArrayBuffer(data)
); );
assert.strictEqual( assert.strictEqual(
message.type, message.type,
@ -166,7 +168,7 @@ describe('WebSocket-Resource', () => {
sinon.stub(socket, 'sendBytes').callsFake(data => { sinon.stub(socket, 'sendBytes').callsFake(data => {
const message = window.textsecure.protobuf.WebSocketMessage.decode( const message = window.textsecure.protobuf.WebSocketMessage.decode(
data toArrayBuffer(data)
); );
assert.strictEqual( assert.strictEqual(
message.type, message.type,
@ -189,7 +191,7 @@ describe('WebSocket-Resource', () => {
sinon.stub(socket, 'sendBytes').callsFake(data => { sinon.stub(socket, 'sendBytes').callsFake(data => {
const message = window.textsecure.protobuf.WebSocketMessage.decode( const message = window.textsecure.protobuf.WebSocketMessage.decode(
data toArrayBuffer(data)
); );
assert.strictEqual( assert.strictEqual(
message.type, message.type,
@ -230,7 +232,7 @@ describe('WebSocket-Resource', () => {
sinon.stub(socket, 'sendBytes').callsFake(data => { sinon.stub(socket, 'sendBytes').callsFake(data => {
const message = window.textsecure.protobuf.WebSocketMessage.decode( const message = window.textsecure.protobuf.WebSocketMessage.decode(
data toArrayBuffer(data)
); );
assert.strictEqual( assert.strictEqual(
message.type, message.type,

View file

@ -44,14 +44,15 @@ describe('#cleanupSessionResets', () => {
it('filters out falsey items', () => { it('filters out falsey items', () => {
const startValue = { const startValue = {
one: 0, one: 0,
two: false, two: Date.now(),
three: Date.now(),
}; };
window.storage.put('sessionResets', startValue); window.storage.put('sessionResets', startValue);
cleanupSessionResets(); cleanupSessionResets();
const actual = window.storage.get('sessionResets'); const actual = window.storage.get('sessionResets');
const expected = window._.pick(startValue, ['three']); const expected = window._.pick(startValue, ['two']);
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
assert.deepEqual(Object.keys(startValue), ['two']);
}); });
}); });

View file

@ -37,8 +37,8 @@ describe('Message', () => {
}); });
after(async () => { after(async () => {
window.textsecure.storage.put('number_id', null); window.textsecure.storage.remove('number_id');
window.textsecure.storage.put('uuid_id', null); window.textsecure.storage.remove('uuid_id');
await window.Signal.Data.removeAll(); await window.Signal.Data.removeAll();
await window.storage.fetch(); await window.storage.fetch();

View file

@ -66,7 +66,7 @@ describe('routineProfileRefresh', () => {
return result; return result;
} }
function makeStorage(lastAttemptAt: undefined | number = undefined) { function makeStorage(lastAttemptAt?: number) {
return { return {
get: sinonSandbox get: sinonSandbox
.stub() .stub()

45
ts/textsecure.d.ts vendored
View file

@ -14,7 +14,11 @@ import { WebAPIType } from './textsecure/WebAPI';
import utils from './textsecure/Helpers'; import utils from './textsecure/Helpers';
import { CallingMessage as CallingMessageClass } from 'ringrtc'; import { CallingMessage as CallingMessageClass } from 'ringrtc';
import { WhatIsThis } from './window.d'; import { WhatIsThis } from './window.d';
import { SignalProtocolStore } from './SignalProtocolStore'; import { Storage } from './textsecure/Storage';
import {
StorageServiceCallOptionsType,
StorageServiceCredentials,
} from './textsecure/Types.d';
export type UnprocessedType = { export type UnprocessedType = {
attempts: number; attempts: number;
@ -30,15 +34,7 @@ export type UnprocessedType = {
version: number; version: number;
}; };
export type StorageServiceCallOptionsType = { export { StorageServiceCallOptionsType, StorageServiceCredentials };
credentials?: StorageServiceCredentials;
greaterThanVersion?: string;
};
export type StorageServiceCredentials = {
username: string;
password: string;
};
export type TextSecureType = { export type TextSecureType = {
createTaskWithTimeout: ( createTaskWithTimeout: (
@ -47,34 +43,7 @@ export type TextSecureType = {
options?: { timeout?: number } options?: { timeout?: number }
) => () => Promise<any>; ) => () => Promise<any>;
crypto: typeof Crypto; crypto: typeof Crypto;
storage: { storage: Storage;
user: {
getNumber: () => string;
getUuid: () => string | undefined;
getDeviceId: () => number | string;
getDeviceName: () => string;
getDeviceNameEncrypted: () => boolean;
setDeviceNameEncrypted: () => Promise<void>;
getSignalingKey: () => ArrayBuffer;
setNumberAndDeviceId: (
number: string,
deviceId: number,
deviceName?: string | null
) => Promise<void>;
setUuidAndDeviceId: (uuid: string, deviceId: number) => Promise<void>;
};
unprocessed: {
remove: (id: string | Array<string>) => Promise<void>;
getCount: () => Promise<number>;
removeAll: () => Promise<void>;
getAll: () => Promise<Array<UnprocessedType>>;
updateAttempts: (id: string, attempts: number) => Promise<void>;
};
get: (key: string, defaultValue?: any) => any;
put: (key: string, value: any) => Promise<void>;
remove: (key: string | Array<string>) => Promise<void>;
protocol: SignalProtocolStore;
};
messageReceiver: MessageReceiver; messageReceiver: MessageReceiver;
messageSender: MessageSender; messageSender: MessageSender;
messaging: SendMessage; messaging: SendMessage;

View file

@ -36,7 +36,7 @@ const PREKEY_ROTATION_AGE = 24 * 60 * 60 * 1000;
const PROFILE_KEY_LENGTH = 32; const PROFILE_KEY_LENGTH = 32;
const SIGNED_KEY_GEN_BATCH_SIZE = 100; const SIGNED_KEY_GEN_BATCH_SIZE = 100;
function getIdentifier(id: string) { function getIdentifier(id: string | undefined) {
if (!id || !id.length) { if (!id || !id.length) {
return id; return id;
} }
@ -137,7 +137,7 @@ export default class AccountManager extends EventTarget {
return; return;
} }
const deviceName = window.textsecure.storage.user.getDeviceName(); const deviceName = window.textsecure.storage.user.getDeviceName();
const base64 = await this.encryptDeviceName(deviceName); const base64 = await this.encryptDeviceName(deviceName || '');
if (base64) { if (base64) {
await this.server.updateDeviceName(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('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-receipts-setting'), window.textsecure.storage.remove('read-receipt-setting'),
]); ]);
// `setNumberAndDeviceId` and `setUuidAndDeviceId` need to be called // `setNumberAndDeviceId` and `setUuidAndDeviceId` need to be called
@ -590,7 +590,7 @@ export default class AccountManager extends EventTarget {
await window.textsecure.storage.user.setNumberAndDeviceId( await window.textsecure.storage.user.setNumberAndDeviceId(
number, number,
response.deviceId || 1, response.deviceId || 1,
deviceName deviceName || undefined
); );
if (uuid) { if (uuid) {

View file

@ -756,7 +756,7 @@ class MessageReceiverInner extends EventTarget {
try { try {
const { id } = item; const { id } = item;
await window.textsecure.storage.unprocessed.remove(id); await window.textsecure.storage.protocol.removeUnprocessed(id);
} catch (deleteError) { } catch (deleteError) {
window.log.error( window.log.error(
'queueCached error deleting item', 'queueCached error deleting item',
@ -800,17 +800,17 @@ class MessageReceiverInner extends EventTarget {
async getAllFromCache() { async getAllFromCache() {
window.log.info('getAllFromCache'); window.log.info('getAllFromCache');
const count = await window.textsecure.storage.unprocessed.getCount(); const count = await window.textsecure.storage.protocol.getUnprocessedCount();
if (count > 1500) { if (count > 1500) {
await window.textsecure.storage.unprocessed.removeAll(); await window.textsecure.storage.protocol.removeAllUnprocessed();
window.log.warn( window.log.warn(
`There were ${count} messages in cache. Deleted all instead of reprocessing` `There were ${count} messages in cache. Deleted all instead of reprocessing`
); );
return []; 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'); window.log.info('getAllFromCache loaded', items.length, 'saved envelopes');
return Promise.all( return Promise.all(
@ -823,9 +823,9 @@ class MessageReceiverInner extends EventTarget {
'getAllFromCache final attempt for envelope', 'getAllFromCache final attempt for envelope',
item.id item.id
); );
await window.textsecure.storage.unprocessed.remove(item.id); await window.textsecure.storage.protocol.removeUnprocessed(item.id);
} else { } else {
await window.textsecure.storage.unprocessed.updateAttempts( await window.textsecure.storage.protocol.updateUnprocessedAttempts(
item.id, item.id,
attempts attempts
); );
@ -981,7 +981,7 @@ class MessageReceiverInner extends EventTarget {
} }
async cacheRemoveBatch(items: Array<string>) { async cacheRemoveBatch(items: Array<string>) {
await window.textsecure.storage.unprocessed.remove(items); await window.textsecure.storage.protocol.removeUnprocessed(items);
} }
removeFromCache(envelope: EnvelopeClass) { removeFromCache(envelope: EnvelopeClass) {
@ -1361,7 +1361,7 @@ class MessageReceiverInner extends EventTarget {
buffer, buffer,
PublicKey.deserialize(Buffer.from(serverTrustRoot)), PublicKey.deserialize(Buffer.from(serverTrustRoot)),
envelope.serverTimestamp, envelope.serverTimestamp,
localE164, localE164 || null,
localUuid, localUuid,
localDeviceId, localDeviceId,
sessionStore, sessionStore,
@ -2417,7 +2417,9 @@ class MessageReceiverInner extends EventTarget {
blocked: SyncMessageClass.Blocked blocked: SyncMessageClass.Blocked
) { ) {
window.log.info('Setting these numbers as blocked:', blocked.numbers); window.log.info('Setting these numbers as blocked:', blocked.numbers);
if (blocked.numbers) {
await window.textsecure.storage.put('blocked', blocked.numbers); await window.textsecure.storage.put('blocked', blocked.numbers);
}
if (blocked.uuids) { if (blocked.uuids) {
window.normalizeUuids( window.normalizeUuids(
blocked, blocked,
@ -2439,17 +2441,15 @@ class MessageReceiverInner extends EventTarget {
} }
isBlocked(number: string) { isBlocked(number: string) {
return window.textsecure.storage.get('blocked', []).includes(number); return window.textsecure.storage.blocked.isBlocked(number);
} }
isUuidBlocked(uuid: string) { isUuidBlocked(uuid: string) {
return window.textsecure.storage.get('blocked-uuids', []).includes(uuid); return window.textsecure.storage.blocked.isUuidBlocked(uuid);
} }
isGroupBlocked(groupId: string) { isGroupBlocked(groupId: string) {
return window.textsecure.storage return window.textsecure.storage.blocked.isGroupBlocked(groupId);
.get('blocked-groups', [])
.includes(groupId);
} }
cleanAttachment(attachment: AttachmentPointerClass) { cleanAttachment(attachment: AttachmentPointerClass) {

View file

@ -39,6 +39,7 @@ import {
} from './Errors'; } from './Errors';
import { isValidNumber } from '../types/PhoneNumber'; import { isValidNumber } from '../types/PhoneNumber';
import { Sessions, IdentityKeys } from '../LibSignalStores'; import { Sessions, IdentityKeys } from '../LibSignalStores';
import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup'; import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
import { getKeysForIdentifier } from './getKeysForIdentifier'; import { getKeysForIdentifier } from './getKeysForIdentifier';
@ -309,7 +310,7 @@ export default class OutgoingMessage {
this.plaintext = message.serialize(); this.plaintext = message.serialize();
} }
} }
return this.plaintext; return toArrayBuffer(this.plaintext);
} }
async getCiphertextMessage({ async getCiphertextMessage({

View file

@ -16,6 +16,7 @@ import {
SenderKeyDistributionMessage, SenderKeyDistributionMessage,
} from '@signalapp/signal-client'; } from '@signalapp/signal-client';
import { assert } from '../util/assert';
import { parseIntOrThrow } from '../util/parseIntOrThrow'; import { parseIntOrThrow } from '../util/parseIntOrThrow';
import { SenderKeys } from '../LibSignalStores'; import { SenderKeys } from '../LibSignalStores';
import { import {
@ -750,8 +751,8 @@ export default class MessageSender {
const blockedIdentifiers = new Set( const blockedIdentifiers = new Set(
concat( concat(
window.storage.getBlockedUuids(), window.storage.blocked.getBlockedUuids(),
window.storage.getBlockedNumbers() window.storage.blocked.getBlockedNumbers()
) )
); );
@ -895,12 +896,13 @@ export default class MessageSender {
} }
async sendIndividualProto( async sendIndividualProto(
identifier: string, identifier: string | undefined,
proto: DataMessageClass | ContentClass | PlaintextContent, proto: DataMessageClass | ContentClass | PlaintextContent,
timestamp: number, timestamp: number,
contentHint: number, contentHint: number,
options?: SendOptionsType options?: SendOptionsType
): Promise<CallbackResultType> { ): Promise<CallbackResultType> {
assert(identifier, "Identifier can't be undefined");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const callback = (res: CallbackResultType) => { const callback = (res: CallbackResultType) => {
if (res && res.errors && res.errors.length > 0) { if (res && res.errors && res.errors.length > 0) {
@ -976,7 +978,7 @@ export default class MessageSender {
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice === 1 || myDevice === '1') { if (myDevice === 1) {
return Promise.resolve(); return Promise.resolve();
} }
@ -1050,7 +1052,7 @@ export default class MessageSender {
const myNumber = window.textsecure.storage.user.getNumber(); const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') { if (myDevice !== 1) {
const request = new window.textsecure.protobuf.SyncMessage.Request(); const request = new window.textsecure.protobuf.SyncMessage.Request();
request.type = request.type =
window.textsecure.protobuf.SyncMessage.Request.Type.BLOCKED; window.textsecure.protobuf.SyncMessage.Request.Type.BLOCKED;
@ -1081,7 +1083,7 @@ export default class MessageSender {
const myNumber = window.textsecure.storage.user.getNumber(); const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') { if (myDevice !== 1) {
const request = new window.textsecure.protobuf.SyncMessage.Request(); const request = new window.textsecure.protobuf.SyncMessage.Request();
request.type = request.type =
window.textsecure.protobuf.SyncMessage.Request.Type.CONFIGURATION; window.textsecure.protobuf.SyncMessage.Request.Type.CONFIGURATION;
@ -1112,7 +1114,7 @@ export default class MessageSender {
const myNumber = window.textsecure.storage.user.getNumber(); const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') { if (myDevice !== 1) {
const request = new window.textsecure.protobuf.SyncMessage.Request(); const request = new window.textsecure.protobuf.SyncMessage.Request();
request.type = window.textsecure.protobuf.SyncMessage.Request.Type.GROUPS; request.type = window.textsecure.protobuf.SyncMessage.Request.Type.GROUPS;
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
@ -1143,7 +1145,7 @@ export default class MessageSender {
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') { if (myDevice !== 1) {
const request = new window.textsecure.protobuf.SyncMessage.Request(); const request = new window.textsecure.protobuf.SyncMessage.Request();
request.type = request.type =
window.textsecure.protobuf.SyncMessage.Request.Type.CONTACTS; window.textsecure.protobuf.SyncMessage.Request.Type.CONTACTS;
@ -1175,7 +1177,7 @@ export default class MessageSender {
const myNumber = window.textsecure.storage.user.getNumber(); const myNumber = window.textsecure.storage.user.getNumber();
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice === 1 || myDevice === '1') { if (myDevice === 1) {
return; return;
} }
@ -1208,7 +1210,7 @@ export default class MessageSender {
const myNumber = window.textsecure.storage.user.getNumber(); const myNumber = window.textsecure.storage.user.getNumber();
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice === 1 || myDevice === '1') { if (myDevice === 1) {
return; return;
} }
@ -1244,7 +1246,7 @@ export default class MessageSender {
const myNumber = window.textsecure.storage.user.getNumber(); const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') { if (myDevice !== 1) {
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
syncMessage.read = []; syncMessage.read = [];
for (let i = 0; i < reads.length; i += 1) { 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 myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice === 1 || myDevice === '1') { if (myDevice === 1) {
return null; return null;
} }
@ -1323,7 +1325,7 @@ export default class MessageSender {
const myNumber = window.textsecure.storage.user.getNumber(); const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice === 1 || myDevice === '1') { if (myDevice === 1) {
return null; return null;
} }
@ -1361,7 +1363,7 @@ export default class MessageSender {
options?: SendOptionsType options?: SendOptionsType
): Promise<CallbackResultType | null> { ): Promise<CallbackResultType | null> {
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice === 1 || myDevice === '1') { if (myDevice === 1) {
return null; return null;
} }
@ -1412,7 +1414,7 @@ export default class MessageSender {
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
const now = Date.now(); const now = Date.now();
if (myDevice === 1 || myDevice === '1') { if (myDevice === 1) {
return Promise.resolve(); return Promise.resolve();
} }
@ -1526,7 +1528,7 @@ export default class MessageSender {
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if ( if (
(myNumber === recipientE164 || myUuid === recipientUuid) && (myNumber === recipientE164 || myUuid === recipientUuid) &&
(myDevice === 1 || myDevice === '1') myDevice === 1
) { ) {
return Promise.resolve(); return Promise.resolve();
} }

View file

@ -1,51 +1,149 @@
// Copyright 2020-2021 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/no-explicit-any */ import {
import utils from './Helpers'; StorageAccessType as Access,
StorageInterface,
} from '../types/Storage.d';
import { User } from './storage/User';
import { Blocked } from './storage/Blocked';
// Default implementation working with localStorage import { assert } from '../util/assert';
const localStorageImpl: StorageInterface = { import Data from '../sql/Client';
put(key: string, value: any) { import { SignalProtocolStore } from '../SignalProtocolStore';
if (value === undefined) {
throw new Error('Tried to store undefined'); export class Storage implements StorageInterface {
public readonly user: User;
public readonly blocked: Blocked;
private ready = false;
private readyCallbacks: Array<() => void> = [];
private items: Partial<Access> = Object.create(null);
private privProtocol: SignalProtocolStore | undefined;
constructor() {
this.user = new User(this);
this.blocked = new Blocked(this);
window.storage = this;
} }
localStorage.setItem(`${key}`, utils.jsonThing(value));
},
get(key: string, defaultValue: any) { get protocol(): SignalProtocolStore {
const value = localStorage.getItem(`${key}`); assert(
if (value === null) { this.privProtocol !== undefined,
'SignalProtocolStore not initialized'
);
return this.privProtocol;
}
set protocol(value: SignalProtocolStore) {
this.privProtocol = value;
}
// `StorageInterface` implementation
public get<K extends keyof Access, V extends Access[K]>(
key: K
): V | undefined;
public get<K extends keyof Access, V extends Access[K]>(
key: K,
defaultValue: V
): V;
public get<K extends keyof Access, V extends Access[K]>(
key: K,
defaultValue?: V
): V | undefined {
if (!this.ready) {
window.log.warn('Called storage.get before storage is ready. key:', key);
}
const item = this.items[key];
if (item === undefined) {
return defaultValue; return defaultValue;
} }
return JSON.parse(value);
},
remove(key: string) { return item as V;
localStorage.removeItem(`${key}`); }
},
};
export type StorageInterface = { public async put<K extends keyof Access>(
put(key: string, value: any): void | Promise<void>; key: K,
get(key: string, defaultValue: any): any; value: Access[K]
remove(key: string): void | Promise<void>; ): Promise<void> {
}; if (!this.ready) {
window.log.warn('Called storage.put before storage is ready. key:', key);
}
const Storage = { this.items[key] = value;
impl: localStorageImpl, await window.Signal.Data.createOrUpdateItem({ id: key, value });
put(key: string, value: unknown): Promise<void> | void { window.reduxActions?.items.putItemExternal(key, value);
return Storage.impl.put(key, value); }
},
get(key: string, defaultValue: unknown): Promise<unknown> { public async remove<K extends keyof Access>(key: K): Promise<void> {
return Storage.impl.get(key, defaultValue); if (!this.ready) {
}, window.log.warn(
'Called storage.remove before storage is ready. key:',
key
);
}
remove(key: string): Promise<void> | void { delete this.items[key];
return Storage.impl.remove(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<void> {
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<Access> {
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<keyof typeof items>;
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());
}
}

View file

@ -11,6 +11,16 @@ export {
UnprocessedUpdateType, UnprocessedUpdateType,
} from '../sql/Interface'; } from '../sql/Interface';
export type StorageServiceCallOptionsType = {
credentials?: StorageServiceCredentials;
greaterThanVersion?: number;
};
export type StorageServiceCredentials = {
username: string;
password: string;
};
export type DeviceType = { export type DeviceType = {
id: number; id: number;
identifier: string; identifier: string;
@ -44,3 +54,5 @@ export type OuterSignedPrekeyType = {
privKey: ArrayBuffer; privKey: ArrayBuffer;
pubKey: ArrayBuffer; pubKey: ArrayBuffer;
}; };
export type SessionResetsType = Record<string, number>;

View file

@ -30,6 +30,7 @@
import { connection as WebSocket, IMessage } from 'websocket'; import { connection as WebSocket, IMessage } from 'websocket';
import { ByteBufferClass } from '../window.d'; import { ByteBufferClass } from '../window.d';
import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto';
import EventTarget from './EventTarget'; import EventTarget from './EventTarget';
@ -153,7 +154,7 @@ export default class WebSocketResource extends EventTarget {
} }
const message = window.textsecure.protobuf.WebSocketMessage.decode( const message = window.textsecure.protobuf.WebSocketMessage.decode(
binaryData toArrayBuffer(binaryData)
); );
if ( if (
message.type === message.type ===

View file

@ -11,7 +11,7 @@ import createTaskWithTimeout from './TaskWithTimeout';
import SyncRequest from './SyncRequest'; import SyncRequest from './SyncRequest';
import MessageSender from './SendMessage'; import MessageSender from './SendMessage';
import StringView from './StringView'; import StringView from './StringView';
import Storage from './Storage'; import { Storage } from './Storage';
import * as WebAPI from './WebAPI'; import * as WebAPI from './WebAPI';
import WebSocketResource from './WebsocketResources'; import WebSocketResource from './WebsocketResources';
@ -19,7 +19,7 @@ export const textsecure = {
createTaskWithTimeout, createTaskWithTimeout,
crypto: Crypto, crypto: Crypto,
utils, utils,
storage: Storage, storage: new Storage(),
AccountManager, AccountManager,
ContactBuffer, ContactBuffer,

View file

@ -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<string> {
return this.storage.get(BLOCKED_NUMBERS_ID, new Array<string>());
}
public isBlocked(number: string): boolean {
return this.getBlockedNumbers().includes(number);
}
public async addBlockedNumber(number: string): Promise<void> {
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<void> {
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<string> {
return this.storage.get(BLOCKED_UUIDS_ID, new Array<string>());
}
public isUuidBlocked(uuid: string): boolean {
return this.getBlockedUuids().includes(uuid);
}
public async addBlockedUuid(uuid: string): Promise<void> {
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<void> {
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<string> {
return this.storage.get(BLOCKED_GROUPS_ID, new Array<string>());
}
public isGroupBlocked(groupId: string): boolean {
return this.getBlockedGroups().includes(groupId);
}
public async addBlockedGroup(groupId: string): Promise<void> {
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<void> {
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));
}
}

View file

@ -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<void> {
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<void> {
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<void> {
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];
}
}

View file

@ -105,3 +105,8 @@ export type DefaultConversationColorType = {
export const DEFAULT_CONVERSATION_COLOR: DefaultConversationColorType = { export const DEFAULT_CONVERSATION_COLOR: DefaultConversationColorType = {
color: 'ultramarine', color: 'ultramarine',
}; };
export type CustomColorsItemType = {
readonly colors: Record<string, CustomColorType>;
readonly version: number;
};

133
ts/types/Storage.d.ts vendored Normal file
View file

@ -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<string>;
'blocked-uuids': Array<string>;
'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<string>;
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<RetryItemType>;
chromiumRegistrationDoneEver: '';
chromiumRegistrationDone: '';
phoneNumberSharingMode: PhoneNumberSharingMode;
phoneNumberDiscoverability: PhoneNumberDiscoverability;
pinnedConversationIds: Array<string>;
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<GroupCredentialType>;
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<K extends keyof StorageAccessType, V extends StorageAccessType[K]>(
key: K
): V | undefined;
get<K extends keyof StorageAccessType, V extends StorageAccessType[K]>(
key: K,
defaultValue: V
): V;
put<K extends keyof StorageAccessType>(
key: K,
value: StorageAccessType[K]
): Promise<void>;
remove<K extends keyof StorageAccessType>(key: K): Promise<void>;
}

View file

@ -2,15 +2,11 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { WebAPIConnectType, WebAPIType } from '../textsecure/WebAPI'; import type { WebAPIConnectType, WebAPIType } from '../textsecure/WebAPI';
import { StorageInterface } from '../types/Storage.d';
// We define a stricter storage here that returns `unknown` instead of `any`.
type Storage = {
get(key: string): unknown;
};
export function connectToServerWithStoredCredentials( export function connectToServerWithStoredCredentials(
WebAPI: WebAPIConnectType, WebAPI: WebAPIConnectType,
storage: Storage storage: Pick<StorageInterface, 'get'>
): WebAPIType { ): WebAPIType {
const username = storage.get('uuid_id') || storage.get('number_id'); const username = storage.get('uuid_id') || storage.get('number_id');
if (typeof username !== 'string') { if (typeof username !== 'string') {

View file

@ -52,7 +52,7 @@ export class RetryPlaceholders {
} }
const parsed = retryItemListSchema.safeParse( const parsed = retryItemListSchema.safeParse(
window.storage.get(STORAGE_KEY) || [] window.storage.get(STORAGE_KEY, new Array<RetryItemType>())
); );
if (!parsed.success) { if (!parsed.success) {
window.log.warn( window.log.warn(

View file

@ -4,6 +4,8 @@
import { PublicKey, Fingerprint } from '@signalapp/signal-client'; import { PublicKey, Fingerprint } from '@signalapp/signal-client';
import { ConversationType } from '../state/ducks/conversations'; import { ConversationType } from '../state/ducks/conversations';
import { assert } from './assert';
export async function generateSecurityNumber( export async function generateSecurityNumber(
ourNumber: string, ourNumber: string,
ourKey: ArrayBuffer, ourKey: ArrayBuffer,
@ -35,7 +37,7 @@ export async function generateSecurityNumberBlock(
const ourUuid = window.textsecure.storage.user.getUuid(); const ourUuid = window.textsecure.storage.user.getUuid();
const us = window.textsecure.storage.protocol.getIdentityRecord( const us = window.textsecure.storage.protocol.getIdentityRecord(
ourUuid || ourNumber ourUuid || ourNumber || ''
); );
const ourKey = us ? us.publicKey : null; const ourKey = us ? us.publicKey : null;
@ -57,6 +59,7 @@ export async function generateSecurityNumberBlock(
return []; return [];
} }
assert(ourNumber, 'Should have our number');
const securityNumber = await generateSecurityNumber( const securityNumber = await generateSecurityNumber(
ourNumber, ourNumber,
ourKey, ourKey,

View file

@ -11,6 +11,7 @@ import {
SenderCertificate, SenderCertificate,
UnidentifiedSenderMessageContent, UnidentifiedSenderMessageContent,
} from '@signalapp/signal-client'; } from '@signalapp/signal-client';
import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto';
import { senderCertificateService } from '../services/senderCertificate'; import { senderCertificateService } from '../services/senderCertificate';
import { import {
padMessage, padMessage,
@ -371,8 +372,8 @@ export async function sendToGroupViaSenderKey(options: {
const accessKeys = getXorOfAccessKeys(devicesForSenderKey); const accessKeys = getXorOfAccessKeys(devicesForSenderKey);
const result = await window.textsecure.messaging.sendWithSenderKey( const result = await window.textsecure.messaging.sendWithSenderKey(
messageBuffer, toArrayBuffer(messageBuffer),
accessKeys, toArrayBuffer(accessKeys),
timestamp, timestamp,
online online
); );

View file

@ -460,9 +460,9 @@ Whisper.ConversationView = Whisper.View.extend({
const { model }: { model: ConversationModel } = this; const { model }: { model: ConversationModel } = this;
if (value) { if (value) {
const pinnedConversationIds = window.storage.get<Array<string>>( const pinnedConversationIds = window.storage.get(
'pinnedConversationIds', 'pinnedConversationIds',
[] new Array<string>()
); );
if (pinnedConversationIds.length >= 4) { if (pinnedConversationIds.length >= 4) {
@ -3880,14 +3880,14 @@ Whisper.ConversationView = Whisper.View.extend({
} }
if ( if (
isDirectConversation(this.model.attributes) && isDirectConversation(this.model.attributes) &&
(window.storage.isBlocked(this.model.get('e164')) || (window.storage.blocked.isBlocked(this.model.get('e164')) ||
window.storage.isUuidBlocked(this.model.get('uuid'))) window.storage.blocked.isUuidBlocked(this.model.get('uuid')))
) { ) {
ToastView = Whisper.BlockedToast; ToastView = Whisper.BlockedToast;
} }
if ( if (
!isDirectConversation(this.model.attributes) && !isDirectConversation(this.model.attributes) &&
window.storage.isGroupBlocked(this.model.get('groupId')) window.storage.blocked.isGroupBlocked(this.model.get('groupId'))
) { ) {
ToastView = Whisper.BlockedGroupToast; ToastView = Whisper.BlockedGroupToast;
} }

40
ts/window.d.ts vendored
View file

@ -20,6 +20,7 @@ import {
ReactionModelType, ReactionModelType,
} from './model-types.d'; } from './model-types.d';
import { ContactRecordIdentityState, TextSecureType } from './textsecure.d'; import { ContactRecordIdentityState, TextSecureType } from './textsecure.d';
import { Storage } from './textsecure/Storage';
import { import {
ChallengeHandler, ChallengeHandler,
IPCRequest as IPCChallengeRequest, IPCRequest as IPCChallengeRequest,
@ -248,30 +249,7 @@ declare global {
setMenuBarVisibility: (value: WhatIsThis) => void; setMenuBarVisibility: (value: WhatIsThis) => void;
showConfirmationDialog: (options: ConfirmationDialogViewProps) => void; showConfirmationDialog: (options: ConfirmationDialogViewProps) => void;
showKeyboardShortcuts: () => void; showKeyboardShortcuts: () => void;
storage: { storage: Storage;
addBlockedGroup: (group: string) => void;
addBlockedNumber: (number: string) => void;
addBlockedUuid: (uuid: string) => void;
fetch: () => void;
get: {
<T = any>(key: string): T | undefined;
<T>(key: string, defaultValue: T): T;
};
getBlockedGroups: () => Array<string>;
getBlockedNumbers: () => Array<string>;
getBlockedUuids: () => Array<string>;
getItemsState: () => WhatIsThis;
isBlocked: (number: string) => boolean;
isGroupBlocked: (group: unknown) => boolean;
isUuidBlocked: (uuid: string) => boolean;
onready: (callback: () => unknown) => void;
put: (key: string, value: any) => Promise<void>;
remove: (key: string) => Promise<void>;
removeBlockedGroup: (group: string) => void;
removeBlockedNumber: (number: string) => void;
removeBlockedUuid: (uuid: string) => void;
reset: () => void;
};
systemTheme: WhatIsThis; systemTheme: WhatIsThis;
textsecure: TextSecureType; textsecure: TextSecureType;
synchronousCrypto: typeof synchronousCrypto; synchronousCrypto: typeof synchronousCrypto;
@ -595,6 +573,20 @@ declare global {
interface Error { interface Error {
originalError?: Event; 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 = { export type DCodeIOType = {