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='js/reliable_trigger.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/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='js/notifications.js'></script>
@ -348,7 +345,6 @@
<script type='text/javascript' src='js/reactions.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/models/blockedNumbers.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>

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
// SPDX-License-Identifier: AGPL-3.0-only
/* global window, textsecure, SignalProtocolStore */
/* global window, SignalProtocolStore */
// eslint-disable-next-line func-names
(function () {
window.textsecure = window.textsecure || {};
window.textsecure.storage = window.textsecure.storage || {};
textsecure.storage.protocol = new SignalProtocolStore();
window.textsecure.storage.protocol = new SignalProtocolStore();
})();

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="../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="../../js/libphonenumber-util.js"></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="storage_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="generate_keys_test.js"></script>

View File

@ -10,11 +10,8 @@
<div id="root"></div>
<script type="text/javascript" src="../../js/components.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/storage/user.js'></script>
<script type='text/javascript' src='../../libtextsecure/storage/unprocessed.js'></script>
<script type='text/javascript' src='../../libtextsecure/protobufs.js'></script>
</body>
</html>

View File

@ -342,16 +342,11 @@
<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/storage.js" data-cover></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="../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/keychange_listener.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="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="reliable_trigger_test.js"></script>
<script type="text/javascript" src="backup_test.js"></script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import { isNormalNumber } from './util/isNormalNumber';
import { take } from './util/iterables';
import { isOlderThan } from './util/timestamp';
import { ConversationModel } from './models/conversations';
import { StorageInterface } from './types/Storage.d';
const STORAGE_KEY = 'lastAttemptedToRefreshProfilesAt';
const MAX_AGE_TO_BE_CONSIDERED_ACTIVE = 30 * 24 * 60 * 60 * 1000;
@ -21,13 +22,6 @@ const MAX_AGE_TO_BE_CONSIDERED_RECENTLY_REFRESHED = 1 * 24 * 60 * 60 * 1000;
const MAX_CONVERSATIONS_TO_REFRESH = 50;
const MIN_ELAPSED_DURATION_TO_REFRESH_AGAIN = 12 * 3600 * 1000;
// This type is a little stricter than what's on `window.storage`, and only requires what
// we need for easier testing.
type StorageType = {
get: (key: string) => unknown;
put: (key: string, value: unknown) => Promise<void>;
};
export async function routineProfileRefresh({
allConversations,
ourConversationId,
@ -35,7 +29,7 @@ export async function routineProfileRefresh({
}: {
allConversations: Array<ConversationModel>;
ourConversationId: string;
storage: StorageType;
storage: Pick<StorageInterface, 'get' | 'put'>;
}): Promise<void> {
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);
if (isNil(storedValue)) {

View File

@ -55,6 +55,7 @@ import {
uuidToArrayBuffer,
arrayBufferToUuid,
} from '../Crypto';
import { assert } from '../util/assert';
import { getOwn } from '../util/getOwn';
import {
fetchMembershipProof,
@ -1257,9 +1258,13 @@ export class CallingClass {
}
const senderIdentityKey = senderIdentityRecord.publicKey.slice(1); // Ignore the type header, it is not used.
const receiverIdentityRecord = window.textsecure.storage.protocol.getIdentityRecord(
const ourIdentifier =
window.textsecure.storage.user.getUuid() ||
window.textsecure.storage.user.getNumber()
window.textsecure.storage.user.getNumber();
assert(ourIdentifier, 'We should have either uuid or number');
const receiverIdentityRecord = window.textsecure.storage.protocol.getIdentityRecord(
ourIdentifier
);
if (!receiverIdentityRecord) {
window.log.error(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

45
ts/textsecure.d.ts vendored
View File

@ -14,7 +14,11 @@ import { WebAPIType } from './textsecure/WebAPI';
import utils from './textsecure/Helpers';
import { CallingMessage as CallingMessageClass } from 'ringrtc';
import { WhatIsThis } from './window.d';
import { SignalProtocolStore } from './SignalProtocolStore';
import { Storage } from './textsecure/Storage';
import {
StorageServiceCallOptionsType,
StorageServiceCredentials,
} from './textsecure/Types.d';
export type UnprocessedType = {
attempts: number;
@ -30,15 +34,7 @@ export type UnprocessedType = {
version: number;
};
export type StorageServiceCallOptionsType = {
credentials?: StorageServiceCredentials;
greaterThanVersion?: string;
};
export type StorageServiceCredentials = {
username: string;
password: string;
};
export { StorageServiceCallOptionsType, StorageServiceCredentials };
export type TextSecureType = {
createTaskWithTimeout: (
@ -47,34 +43,7 @@ export type TextSecureType = {
options?: { timeout?: number }
) => () => Promise<any>;
crypto: typeof Crypto;
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;
};
storage: Storage;
messageReceiver: MessageReceiver;
messageSender: MessageSender;
messaging: SendMessage;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 = {
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
import type { WebAPIConnectType, WebAPIType } from '../textsecure/WebAPI';
// We define a stricter storage here that returns `unknown` instead of `any`.
type Storage = {
get(key: string): unknown;
};
import { StorageInterface } from '../types/Storage.d';
export function connectToServerWithStoredCredentials(
WebAPI: WebAPIConnectType,
storage: Storage
storage: Pick<StorageInterface, 'get'>
): WebAPIType {
const username = storage.get('uuid_id') || storage.get('number_id');
if (typeof username !== 'string') {

View File

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

View File

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

View File

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

View File

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

40
ts/window.d.ts vendored
View File

@ -20,6 +20,7 @@ import {
ReactionModelType,
} from './model-types.d';
import { ContactRecordIdentityState, TextSecureType } from './textsecure.d';
import { Storage } from './textsecure/Storage';
import {
ChallengeHandler,
IPCRequest as IPCChallengeRequest,
@ -248,30 +249,7 @@ declare global {
setMenuBarVisibility: (value: WhatIsThis) => void;
showConfirmationDialog: (options: ConfirmationDialogViewProps) => void;
showKeyboardShortcuts: () => void;
storage: {
addBlockedGroup: (group: string) => void;
addBlockedNumber: (number: string) => void;
addBlockedUuid: (uuid: string) => void;
fetch: () => void;
get: {
<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;
};
storage: Storage;
systemTheme: WhatIsThis;
textsecure: TextSecureType;
synchronousCrypto: typeof synchronousCrypto;
@ -595,6 +573,20 @@ declare global {
interface Error {
originalError?: Event;
}
// Uint8Array and ArrayBuffer are type-compatible in TypeScript's covariant
// type checker, but in reality they are not. Let's assert correct use!
interface Uint8Array {
__uint8array: never;
}
interface ArrayBuffer {
__array_buffer: never;
}
interface SharedArrayBuffer {
__array_buffer: never;
}
}
export type DCodeIOType = {