Signal-Desktop/ts/SignalProtocolStore.ts

1355 lines
38 KiB
TypeScript
Raw Normal View History

2021-02-26 23:42:45 +00:00
// Copyright 2016-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable class-methods-use-this */
import PQueue from 'p-queue';
import { isNumber } from 'lodash';
import * as z from 'zod';
import {
SessionRecord,
PreKeyRecord,
PrivateKey,
PublicKey,
SignedPreKeyRecord,
Direction,
} from 'libsignal-client';
import {
constantTimeEqual,
fromEncodedBinaryToArrayBuffer,
typedArrayToArrayBuffer,
} from './Crypto';
2021-02-26 23:42:45 +00:00
import { isNotNil } from './util/isNotNil';
import { isMoreRecentThan } from './util/timestamp';
import {
sessionRecordToProtobuf,
sessionStructureToArrayBuffer,
} from './util/sessionTranslation';
import {
KeyPairType,
IdentityKeyType,
SessionType,
SignedPreKeyType,
OuterSignedPrekeyType,
PreKeyType,
UnprocessedType,
UnprocessedUpdateType,
} from './textsecure/Types.d';
2021-02-26 23:42:45 +00:00
const TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds
const VerifiedStatus = {
DEFAULT: 0,
VERIFIED: 1,
UNVERIFIED: 2,
};
function validateVerifiedStatus(status: number): boolean {
if (
status === VerifiedStatus.DEFAULT ||
status === VerifiedStatus.VERIFIED ||
status === VerifiedStatus.UNVERIFIED
) {
return true;
}
return false;
}
const identityKeySchema = z.object({
id: z.string(),
publicKey: z.instanceof(ArrayBuffer),
firstUse: z.boolean(),
timestamp: z.number().refine((value: number) => value % 1 === 0 && value > 0),
verified: z.number().refine(validateVerifiedStatus),
nonblockingApproval: z.boolean(),
2021-02-26 23:42:45 +00:00
});
function validateIdentityKey(attrs: unknown): attrs is IdentityKeyType {
// We'll throw if this doesn't match
identityKeySchema.parse(attrs);
return true;
}
2021-02-26 23:42:45 +00:00
async function normalizeEncodedAddress(
encodedAddress: string
): Promise<string> {
const [identifier, deviceId] = window.textsecure.utils.unencodeNumber(
encodedAddress
);
try {
const conv = window.ConversationController.getOrCreate(
identifier,
'private'
);
return `${conv.get('id')}.${deviceId}`;
} catch (e) {
window.log.error(`could not get conversation for identifier ${identifier}`);
throw e;
}
}
type HasIdType = {
id: string | number;
};
type CacheEntryType<DBType, HydratedType> =
| {
hydrated: false;
fromDB: DBType;
}
| { hydrated: true; fromDB: DBType; item: HydratedType };
2021-02-26 23:42:45 +00:00
async function _fillCaches<T extends HasIdType, HydratedType>(
2021-02-26 23:42:45 +00:00
object: SignalProtocolStore,
field: keyof SignalProtocolStore,
itemsPromise: Promise<Array<T>>
): Promise<void> {
const items = await itemsPromise;
const cache: Record<string, CacheEntryType<T, HydratedType>> = Object.create(
null
);
2021-02-26 23:42:45 +00:00
for (let i = 0, max = items.length; i < max; i += 1) {
const fromDB = items[i];
const { id } = fromDB;
2021-02-26 23:42:45 +00:00
cache[id] = {
fromDB,
hydrated: false,
};
2021-02-26 23:42:45 +00:00
}
window.log.info(`SignalProtocolStore: Finished caching ${field} data`);
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
object[field] = cache as any;
}
export function hydrateSession(session: SessionType): SessionRecord {
return SessionRecord.deserialize(Buffer.from(session.record, 'base64'));
}
export function hydratePublicKey(identityKey: IdentityKeyType): PublicKey {
return PublicKey.deserialize(Buffer.from(identityKey.publicKey));
}
export function hydratePreKey(preKey: PreKeyType): PreKeyRecord {
const publicKey = PublicKey.deserialize(Buffer.from(preKey.publicKey));
const privateKey = PrivateKey.deserialize(Buffer.from(preKey.privateKey));
return PreKeyRecord.new(preKey.id, publicKey, privateKey);
}
export function hydrateSignedPreKey(
signedPreKey: SignedPreKeyType
): SignedPreKeyRecord {
const createdAt = signedPreKey.created_at;
const pubKey = PublicKey.deserialize(Buffer.from(signedPreKey.publicKey));
const privKey = PrivateKey.deserialize(Buffer.from(signedPreKey.privateKey));
const signature = Buffer.from([]);
return SignedPreKeyRecord.new(
signedPreKey.id,
createdAt,
pubKey,
privKey,
signature
);
}
2021-02-26 23:42:45 +00:00
export function freezeSession(session: SessionRecord): string {
return session.serialize().toString('base64');
}
export function freezePublicKey(publicKey: PublicKey): ArrayBuffer {
return typedArrayToArrayBuffer(publicKey.serialize());
}
export function freezePreKey(preKey: PreKeyRecord): KeyPairType {
const keyPair = {
pubKey: typedArrayToArrayBuffer(preKey.publicKey().serialize()),
privKey: typedArrayToArrayBuffer(preKey.privateKey().serialize()),
};
return keyPair;
}
export function freezeSignedPreKey(
signedPreKey: SignedPreKeyRecord
): KeyPairType {
const keyPair = {
pubKey: typedArrayToArrayBuffer(signedPreKey.publicKey().serialize()),
privKey: typedArrayToArrayBuffer(signedPreKey.privateKey().serialize()),
};
return keyPair;
}
2021-02-26 23:42:45 +00:00
// We add a this parameter to avoid an 'implicit any' error on the next line
const EventsMixin = (function EventsMixin(this: unknown) {
window._.assign(this, window.Backbone.Events);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any) as typeof window.Backbone.EventsMixin;
export class SignalProtocolStore extends EventsMixin {
// Enums used across the app
VerifiedStatus = VerifiedStatus;
// Cached values
ourIdentityKey?: KeyPairType;
ourRegistrationId?: number;
identityKeys?: Record<string, CacheEntryType<IdentityKeyType, PublicKey>>;
sessions?: Record<string, CacheEntryType<SessionType, SessionRecord>>;
2021-02-26 23:42:45 +00:00
preKeys?: Record<string, CacheEntryType<PreKeyType, PreKeyRecord>>;
2021-02-26 23:42:45 +00:00
signedPreKeys?: Record<
string,
CacheEntryType<SignedPreKeyType, SignedPreKeyRecord>
>;
2021-02-26 23:42:45 +00:00
sessionQueues: Map<string, PQueue> = new Map<string, PQueue>();
2021-02-26 23:42:45 +00:00
async hydrateCaches(): Promise<void> {
await Promise.all([
(async () => {
const item = await window.Signal.Data.getItemById('identityKey');
this.ourIdentityKey = item ? item.value : undefined;
})(),
(async () => {
const item = await window.Signal.Data.getItemById('registrationId');
this.ourRegistrationId = item ? item.value : undefined;
})(),
_fillCaches<IdentityKeyType, PublicKey>(
2021-02-26 23:42:45 +00:00
this,
'identityKeys',
window.Signal.Data.getAllIdentityKeys()
),
_fillCaches<SessionType, SessionRecord>(
2021-02-26 23:42:45 +00:00
this,
'sessions',
window.Signal.Data.getAllSessions()
),
_fillCaches<PreKeyType, PreKeyRecord>(
2021-02-26 23:42:45 +00:00
this,
'preKeys',
window.Signal.Data.getAllPreKeys()
),
_fillCaches<SignedPreKeyType, SignedPreKeyRecord>(
2021-02-26 23:42:45 +00:00
this,
'signedPreKeys',
window.Signal.Data.getAllSignedPreKeys()
),
]);
}
async getIdentityKeyPair(): Promise<KeyPairType | undefined> {
return this.ourIdentityKey;
}
async getLocalRegistrationId(): Promise<number | undefined> {
return this.ourRegistrationId;
}
// PreKeys
async loadPreKey(keyId: string | number): Promise<PreKeyRecord | undefined> {
2021-02-26 23:42:45 +00:00
if (!this.preKeys) {
throw new Error('loadPreKey: this.preKeys not yet cached!');
}
const entry = this.preKeys[keyId];
if (!entry) {
window.log.error('Failed to fetch prekey:', keyId);
return undefined;
2021-02-26 23:42:45 +00:00
}
if (entry.hydrated) {
window.log.info('Successfully fetched prekey (cache hit):', keyId);
return entry.item;
}
const item = hydratePreKey(entry.fromDB);
this.preKeys[keyId] = {
hydrated: true,
fromDB: entry.fromDB,
item,
};
window.log.info('Successfully fetched prekey (cache miss):', keyId);
return item;
2021-02-26 23:42:45 +00:00
}
async storePreKey(keyId: number, keyPair: KeyPairType): Promise<void> {
if (!this.preKeys) {
throw new Error('storePreKey: this.preKeys not yet cached!');
}
if (this.preKeys[keyId]) {
throw new Error(`storePreKey: prekey ${keyId} already exists!`);
}
2021-02-26 23:42:45 +00:00
const fromDB = {
2021-02-26 23:42:45 +00:00
id: keyId,
publicKey: keyPair.pubKey,
privateKey: keyPair.privKey,
};
await window.Signal.Data.createOrUpdatePreKey(fromDB);
this.preKeys[keyId] = {
hydrated: false,
fromDB,
};
2021-02-26 23:42:45 +00:00
}
async removePreKey(keyId: number): Promise<void> {
if (!this.preKeys) {
throw new Error('removePreKey: this.preKeys not yet cached!');
}
try {
this.trigger('removePreKey');
} catch (error) {
window.log.error(
'removePreKey error triggering removePreKey:',
error && error.stack ? error.stack : error
);
}
delete this.preKeys[keyId];
await window.Signal.Data.removePreKeyById(keyId);
}
async clearPreKeyStore(): Promise<void> {
this.preKeys = Object.create(null);
await window.Signal.Data.removeAllPreKeys();
}
// Signed PreKeys
async loadSignedPreKey(
keyId: number
): Promise<SignedPreKeyRecord | undefined> {
2021-02-26 23:42:45 +00:00
if (!this.signedPreKeys) {
throw new Error('loadSignedPreKey: this.signedPreKeys not yet cached!');
}
const entry = this.signedPreKeys[keyId];
if (!entry) {
window.log.error('Failed to fetch signed prekey:', keyId);
return undefined;
2021-02-26 23:42:45 +00:00
}
if (entry.hydrated) {
window.log.info('Successfully fetched signed prekey (cache hit):', keyId);
return entry.item;
}
const item = hydrateSignedPreKey(entry.fromDB);
this.signedPreKeys[keyId] = {
hydrated: true,
item,
fromDB: entry.fromDB,
};
window.log.info('Successfully fetched signed prekey (cache miss):', keyId);
return item;
2021-02-26 23:42:45 +00:00
}
async loadSignedPreKeys(): Promise<Array<OuterSignedPrekeyType>> {
if (!this.signedPreKeys) {
throw new Error('loadSignedPreKeys: this.signedPreKeys not yet cached!');
}
if (arguments.length > 0) {
throw new Error('loadSignedPreKeys takes no arguments');
}
const entries = Object.values(this.signedPreKeys);
return entries.map(entry => {
const preKey = entry.fromDB;
return {
pubKey: preKey.publicKey,
privKey: preKey.privateKey,
created_at: preKey.created_at,
keyId: preKey.id,
confirmed: preKey.confirmed,
};
});
2021-02-26 23:42:45 +00:00
}
// Note that this is also called in update scenarios, for confirming that signed prekeys
// have indeed been accepted by the server.
2021-02-26 23:42:45 +00:00
async storeSignedPreKey(
keyId: number,
keyPair: KeyPairType,
confirmed?: boolean
): Promise<void> {
if (!this.signedPreKeys) {
throw new Error('storeSignedPreKey: this.signedPreKeys not yet cached!');
}
const fromDB = {
2021-02-26 23:42:45 +00:00
id: keyId,
publicKey: keyPair.pubKey,
privateKey: keyPair.privKey,
created_at: Date.now(),
confirmed: Boolean(confirmed),
};
await window.Signal.Data.createOrUpdateSignedPreKey(fromDB);
this.signedPreKeys[keyId] = {
hydrated: false,
fromDB,
};
2021-02-26 23:42:45 +00:00
}
async removeSignedPreKey(keyId: number): Promise<void> {
if (!this.signedPreKeys) {
throw new Error('removeSignedPreKey: this.signedPreKeys not yet cached!');
}
delete this.signedPreKeys[keyId];
await window.Signal.Data.removeSignedPreKeyById(keyId);
}
async clearSignedPreKeysStore(): Promise<void> {
this.signedPreKeys = Object.create(null);
await window.Signal.Data.removeAllSignedPreKeys();
}
// Session Queue
async enqueueSessionJob<T>(
encodedAddress: string,
task: () => Promise<T>
): Promise<T> {
const id = await normalizeEncodedAddress(encodedAddress);
const queue = this._getSessionQueue(id);
return queue.add<T>(task);
}
private _createSessionQueue(): PQueue {
return new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 });
}
private _getSessionQueue(id: string): PQueue {
const cachedQueue = this.sessionQueues.get(id);
if (cachedQueue) {
return cachedQueue;
}
const freshQueue = this._createSessionQueue();
this.sessionQueues.set(id, freshQueue);
return freshQueue;
}
2021-02-26 23:42:45 +00:00
// Sessions
async loadSession(
encodedAddress: string
): Promise<SessionRecord | undefined> {
2021-02-26 23:42:45 +00:00
if (!this.sessions) {
throw new Error('loadSession: this.sessions not yet cached!');
}
if (encodedAddress === null || encodedAddress === undefined) {
throw new Error('loadSession: encodedAddress was undefined/null');
2021-02-26 23:42:45 +00:00
}
try {
const id = await normalizeEncodedAddress(encodedAddress);
const entry = this.sessions[id];
if (!entry) {
return undefined;
}
2021-02-26 23:42:45 +00:00
if (entry.hydrated) {
return entry.item;
2021-02-26 23:42:45 +00:00
}
const item = await this._maybeMigrateSession(entry.fromDB);
this.sessions[id] = {
hydrated: true,
item,
fromDB: entry.fromDB,
};
return item;
2021-02-26 23:42:45 +00:00
} catch (error) {
const errorString = error && error.stack ? error.stack : error;
window.log.error(
`loadSession: failed to load session ${encodedAddress}: ${errorString}`
2021-02-26 23:42:45 +00:00
);
return undefined;
2021-02-26 23:42:45 +00:00
}
}
2021-02-26 23:42:45 +00:00
private async _maybeMigrateSession(
session: SessionType
): Promise<SessionRecord> {
// Already migrated, return record directly
if (session.version === 2) {
return hydrateSession(session);
}
// Not yet converted, need to translate to new format
if (session.version !== undefined) {
throw new Error('_maybeMigrateSession: Unknown session version type!');
}
const keyPair = await this.getIdentityKeyPair();
if (!keyPair) {
throw new Error('_maybeMigrateSession: No identity key for ourself!');
}
const localRegistrationId = await this.getLocalRegistrationId();
if (!isNumber(localRegistrationId)) {
throw new Error('_maybeMigrateSession: No registration id for ourself!');
}
const localUserData = {
identityKeyPublic: keyPair.pubKey,
registrationId: localRegistrationId,
};
window.log.info(
`_maybeMigrateSession: Migrating session with id ${session.id}`
);
const sessionProto = sessionRecordToProtobuf(
JSON.parse(session.record),
localUserData
);
return SessionRecord.deserialize(
Buffer.from(sessionStructureToArrayBuffer(sessionProto))
);
2021-02-26 23:42:45 +00:00
}
async storeSession(
encodedAddress: string,
record: SessionRecord
): Promise<void> {
2021-02-26 23:42:45 +00:00
if (!this.sessions) {
throw new Error('storeSession: this.sessions not yet cached!');
}
if (encodedAddress === null || encodedAddress === undefined) {
throw new Error('storeSession: encodedAddress was undefined/null');
2021-02-26 23:42:45 +00:00
}
const unencoded = window.textsecure.utils.unencodeNumber(encodedAddress);
const deviceId = parseInt(unencoded[1], 10);
try {
const id = await normalizeEncodedAddress(encodedAddress);
const fromDB = {
2021-02-26 23:42:45 +00:00
id,
version: 2,
2021-02-26 23:42:45 +00:00
conversationId: window.textsecure.utils.unencodeNumber(id)[0],
deviceId,
record: record.serialize().toString('base64'),
2021-02-26 23:42:45 +00:00
};
await window.Signal.Data.createOrUpdateSession(fromDB);
this.sessions[id] = {
hydrated: true,
fromDB,
item: record,
};
2021-02-26 23:42:45 +00:00
} catch (error) {
const errorString = error && error.stack ? error.stack : error;
window.log.error(
`storeSession: Save failed fo ${encodedAddress}: ${errorString}`
2021-02-26 23:42:45 +00:00
);
throw error;
2021-02-26 23:42:45 +00:00
}
}
async getDeviceIds(identifier: string): Promise<Array<number>> {
if (!this.sessions) {
throw new Error('getDeviceIds: this.sessions not yet cached!');
}
if (identifier === null || identifier === undefined) {
throw new Error('getDeviceIds: identifier was undefined/null');
2021-02-26 23:42:45 +00:00
}
try {
const id = window.ConversationController.getConversationId(identifier);
if (!id) {
throw new Error(
`getDeviceIds: No conversationId found for identifier ${identifier}`
);
}
2021-02-26 23:42:45 +00:00
const allSessions = Object.values(this.sessions);
const entries = allSessions.filter(
session => session.fromDB.conversationId === id
2021-02-26 23:42:45 +00:00
);
const openIds = await Promise.all(
entries.map(async entry => {
if (entry.hydrated) {
const record = entry.item;
if (record.hasCurrentState()) {
return entry.fromDB.deviceId;
}
return undefined;
}
2021-02-26 23:42:45 +00:00
const record = await this._maybeMigrateSession(entry.fromDB);
if (record.hasCurrentState()) {
return entry.fromDB.deviceId;
2021-02-26 23:42:45 +00:00
}
return undefined;
})
);
return openIds.filter(isNotNil);
2021-02-26 23:42:45 +00:00
} catch (error) {
window.log.error(
`getDeviceIds: Failed to get device ids for identifier ${identifier}`,
2021-02-26 23:42:45 +00:00
error && error.stack ? error.stack : error
);
}
return [];
}
async removeSession(encodedAddress: string): Promise<void> {
if (!this.sessions) {
throw new Error('removeSession: this.sessions not yet cached!');
}
window.log.info('removeSession: deleting session for', encodedAddress);
try {
const id = await normalizeEncodedAddress(encodedAddress);
await window.Signal.Data.removeSessionById(id);
delete this.sessions[id];
2021-02-26 23:42:45 +00:00
} catch (e) {
window.log.error(
`removeSession: Failed to delete session for ${encodedAddress}`
);
2021-02-26 23:42:45 +00:00
}
}
async removeAllSessions(identifier: string): Promise<void> {
if (!this.sessions) {
throw new Error('removeAllSessions: this.sessions not yet cached!');
}
if (identifier === null || identifier === undefined) {
throw new Error('removeAllSessions: identifier was undefined/null');
2021-02-26 23:42:45 +00:00
}
window.log.info('removeAllSessions: deleting sessions for', identifier);
const id = window.ConversationController.getConversationId(identifier);
const entries = Object.values(this.sessions);
2021-02-26 23:42:45 +00:00
for (let i = 0, max = entries.length; i < max; i += 1) {
const entry = entries[i];
if (entry.fromDB.conversationId === id) {
delete this.sessions[entry.fromDB.id];
2021-02-26 23:42:45 +00:00
}
}
await window.Signal.Data.removeSessionsByConversation(identifier);
}
private async _archiveSession(
entry?: CacheEntryType<SessionType, SessionRecord>
) {
if (!entry) {
return;
}
await this.enqueueSessionJob(entry.fromDB.id, async () => {
const item = entry.hydrated
? entry.item
: await this._maybeMigrateSession(entry.fromDB);
if (!item.hasCurrentState()) {
return;
}
item.archiveCurrentState();
await this.storeSession(entry.fromDB.id, item);
});
}
async archiveSession(encodedAddress: string): Promise<void> {
if (!this.sessions) {
throw new Error('archiveSession: this.sessions not yet cached!');
}
window.log.info(`archiveSession: session for ${encodedAddress}`);
const id = await normalizeEncodedAddress(encodedAddress);
const entry = this.sessions[id];
await this._archiveSession(entry);
}
async archiveSiblingSessions(encodedAddress: string): Promise<void> {
2021-02-26 23:42:45 +00:00
if (!this.sessions) {
throw new Error('archiveSiblingSessions: this.sessions not yet cached!');
}
window.log.info(
'archiveSiblingSessions: archiving sibling sessions for',
encodedAddress
2021-02-26 23:42:45 +00:00
);
const id = await normalizeEncodedAddress(encodedAddress);
const [identifier, deviceId] = window.textsecure.utils.unencodeNumber(id);
const deviceIdNumber = parseInt(deviceId, 10);
2021-02-26 23:42:45 +00:00
const allEntries = Object.values(this.sessions);
const entries = allEntries.filter(
entry =>
entry.fromDB.conversationId === identifier &&
entry.fromDB.deviceId !== deviceIdNumber
);
2021-02-26 23:42:45 +00:00
await Promise.all(
entries.map(async entry => {
await this._archiveSession(entry);
2021-02-26 23:42:45 +00:00
})
);
}
async archiveAllSessions(identifier: string): Promise<void> {
if (!this.sessions) {
throw new Error('archiveAllSessions: this.sessions not yet cached!');
}
window.log.info(
'archiveAllSessions: archiving all sessions for',
identifier
);
const id = window.ConversationController.getConversationId(identifier);
const allEntries = Object.values(this.sessions);
const entries = allEntries.filter(
entry => entry.fromDB.conversationId === id
);
2021-02-26 23:42:45 +00:00
await Promise.all(
entries.map(async entry => {
await this._archiveSession(entry);
2021-02-26 23:42:45 +00:00
})
);
}
async clearSessionStore(): Promise<void> {
this.sessions = Object.create(null);
window.Signal.Data.removeAllSessions();
}
// Identity Keys
getIdentityRecord(identifier: string): IdentityKeyType | undefined {
if (!this.identityKeys) {
throw new Error('getIdentityRecord: this.identityKeys not yet cached!');
}
try {
const id = window.ConversationController.getConversationId(identifier);
if (!id) {
throw new Error(
`getIdentityRecord: No conversation id for identifier ${identifier}`
);
}
const entry = this.identityKeys[id];
if (!entry) {
return undefined;
2021-02-26 23:42:45 +00:00
}
return entry.fromDB;
2021-02-26 23:42:45 +00:00
} catch (e) {
window.log.error(
`getIdentityRecord: Failed to get identity record for identifier ${identifier}`
2021-02-26 23:42:45 +00:00
);
return undefined;
2021-02-26 23:42:45 +00:00
}
}
async isTrustedIdentity(
encodedAddress: string,
publicKey: ArrayBuffer,
direction: number
): Promise<boolean> {
if (!this.identityKeys) {
throw new Error('getIdentityRecord: this.identityKeys not yet cached!');
}
if (encodedAddress === null || encodedAddress === undefined) {
throw new Error('isTrustedIdentity: encodedAddress was undefined/null');
2021-02-26 23:42:45 +00:00
}
const identifier = window.textsecure.utils.unencodeNumber(
encodedAddress
)[0];
const ourNumber = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid();
const isOurIdentifier =
(ourNumber && identifier === ourNumber) ||
(ourUuid && identifier === ourUuid);
const identityRecord = this.getIdentityRecord(identifier);
if (isOurIdentifier) {
if (identityRecord && identityRecord.publicKey) {
return constantTimeEqual(identityRecord.publicKey, publicKey);
}
window.log.warn(
'isTrustedIdentity: No local record for our own identifier. Returning true.'
);
return true;
}
switch (direction) {
case Direction.Sending:
2021-02-26 23:42:45 +00:00
return this.isTrustedForSending(publicKey, identityRecord);
case Direction.Receiving:
2021-02-26 23:42:45 +00:00
return true;
default:
throw new Error(`isTrustedIdentity: Unknown direction: ${direction}`);
2021-02-26 23:42:45 +00:00
}
}
isTrustedForSending(
publicKey: ArrayBuffer,
identityRecord?: IdentityKeyType
): boolean {
if (!identityRecord) {
window.log.info(
'isTrustedForSending: No previous record, returning true...'
);
return true;
}
const existing = identityRecord.publicKey;
if (!existing) {
window.log.info('isTrustedForSending: Nothing here, returning true...');
return true;
}
if (!constantTimeEqual(existing, publicKey)) {
window.log.info("isTrustedForSending: Identity keys don't match...");
return false;
}
if (identityRecord.verified === VerifiedStatus.UNVERIFIED) {
window.log.error('isTrustedIdentity: Needs unverified approval!');
2021-02-26 23:42:45 +00:00
return false;
}
if (this.isNonBlockingApprovalRequired(identityRecord)) {
window.log.error('isTrustedForSending: Needs non-blocking approval!');
return false;
}
return true;
}
async loadIdentityKey(identifier: string): Promise<ArrayBuffer | undefined> {
if (identifier === null || identifier === undefined) {
throw new Error('loadIdentityKey: identifier was undefined/null');
2021-02-26 23:42:45 +00:00
}
const id = window.textsecure.utils.unencodeNumber(identifier)[0];
const identityRecord = this.getIdentityRecord(id);
if (identityRecord) {
return identityRecord.publicKey;
}
return undefined;
}
private async _saveIdentityKey(data: IdentityKeyType): Promise<void> {
if (!this.identityKeys) {
throw new Error('_saveIdentityKey: this.identityKeys not yet cached!');
}
const { id } = data;
await window.Signal.Data.createOrUpdateIdentityKey(data);
this.identityKeys[id] = {
hydrated: false,
fromDB: data,
};
2021-02-26 23:42:45 +00:00
}
async saveIdentity(
encodedAddress: string,
publicKey: ArrayBuffer,
nonblockingApproval = false
2021-02-26 23:42:45 +00:00
): Promise<boolean> {
if (!this.identityKeys) {
throw new Error('saveIdentity: this.identityKeys not yet cached!');
}
if (encodedAddress === null || encodedAddress === undefined) {
throw new Error('saveIdentity: encodedAddress was undefined/null');
2021-02-26 23:42:45 +00:00
}
if (!(publicKey instanceof ArrayBuffer)) {
// eslint-disable-next-line no-param-reassign
publicKey = fromEncodedBinaryToArrayBuffer(publicKey);
}
if (typeof nonblockingApproval !== 'boolean') {
// eslint-disable-next-line no-param-reassign
nonblockingApproval = false;
}
const identifier = window.textsecure.utils.unencodeNumber(
encodedAddress
)[0];
const identityRecord = this.getIdentityRecord(identifier);
const id = window.ConversationController.getOrCreate(
identifier,
'private'
).get('id');
if (!identityRecord || !identityRecord.publicKey) {
// Lookup failed, or the current key was removed, so save this one.
window.log.info('saveIdentity: Saving new identity...');
2021-02-26 23:42:45 +00:00
await this._saveIdentityKey({
id,
publicKey,
firstUse: true,
timestamp: Date.now(),
verified: VerifiedStatus.DEFAULT,
nonblockingApproval,
});
return false;
}
const oldpublicKey = identityRecord.publicKey;
if (!constantTimeEqual(oldpublicKey, publicKey)) {
window.log.info('saveIdentity: Replacing existing identity...');
2021-02-26 23:42:45 +00:00
const previousStatus = identityRecord.verified;
let verifiedStatus;
if (
previousStatus === VerifiedStatus.VERIFIED ||
previousStatus === VerifiedStatus.UNVERIFIED
) {
verifiedStatus = VerifiedStatus.UNVERIFIED;
} else {
verifiedStatus = VerifiedStatus.DEFAULT;
}
await this._saveIdentityKey({
id,
publicKey,
firstUse: false,
timestamp: Date.now(),
verified: verifiedStatus,
nonblockingApproval,
});
try {
this.trigger('keychange', identifier);
} catch (error) {
window.log.error(
'saveIdentity: error triggering keychange:',
2021-02-26 23:42:45 +00:00
error && error.stack ? error.stack : error
);
}
await this.archiveSiblingSessions(encodedAddress);
return true;
}
if (this.isNonBlockingApprovalRequired(identityRecord)) {
window.log.info('saveIdentity: Setting approval status...');
2021-02-26 23:42:45 +00:00
identityRecord.nonblockingApproval = nonblockingApproval;
await this._saveIdentityKey(identityRecord);
return false;
}
return false;
}
isNonBlockingApprovalRequired(identityRecord: IdentityKeyType): boolean {
return (
!identityRecord.firstUse &&
isMoreRecentThan(identityRecord.timestamp, TIMESTAMP_THRESHOLD) &&
2021-02-26 23:42:45 +00:00
!identityRecord.nonblockingApproval
);
}
async saveIdentityWithAttributes(
encodedAddress: string,
attributes: Partial<IdentityKeyType>
2021-02-26 23:42:45 +00:00
): Promise<void> {
if (encodedAddress === null || encodedAddress === undefined) {
throw new Error(
'saveIdentityWithAttributes: encodedAddress was undefined/null'
);
2021-02-26 23:42:45 +00:00
}
const identifier = window.textsecure.utils.unencodeNumber(
encodedAddress
)[0];
const identityRecord = this.getIdentityRecord(identifier);
const conv = window.ConversationController.getOrCreate(
identifier,
'private'
);
const id = conv.get('id');
const updates: Partial<IdentityKeyType> = {
2021-02-26 23:42:45 +00:00
...identityRecord,
...attributes,
id,
};
if (validateIdentityKey(updates)) {
2021-02-26 23:42:45 +00:00
await this._saveIdentityKey(updates);
}
}
async setApproval(
encodedAddress: string,
nonblockingApproval: boolean
): Promise<void> {
if (encodedAddress === null || encodedAddress === undefined) {
throw new Error('setApproval: encodedAddress was undefined/null');
2021-02-26 23:42:45 +00:00
}
if (typeof nonblockingApproval !== 'boolean') {
throw new Error('setApproval: Invalid approval status');
2021-02-26 23:42:45 +00:00
}
const identifier = window.textsecure.utils.unencodeNumber(
encodedAddress
)[0];
const identityRecord = this.getIdentityRecord(identifier);
if (!identityRecord) {
throw new Error(`setApproval: No identity record for ${identifier}`);
2021-02-26 23:42:45 +00:00
}
identityRecord.nonblockingApproval = nonblockingApproval;
await this._saveIdentityKey(identityRecord);
}
async setVerified(
encodedAddress: string,
verifiedStatus: number,
publicKey?: ArrayBuffer
2021-02-26 23:42:45 +00:00
): Promise<void> {
if (encodedAddress === null || encodedAddress === undefined) {
throw new Error('setVerified: encodedAddress was undefined/null');
2021-02-26 23:42:45 +00:00
}
if (!validateVerifiedStatus(verifiedStatus)) {
throw new Error('setVerified: Invalid verified status');
2021-02-26 23:42:45 +00:00
}
if (arguments.length > 2 && !(publicKey instanceof ArrayBuffer)) {
throw new Error('setVerified: Invalid public key');
2021-02-26 23:42:45 +00:00
}
const identityRecord = this.getIdentityRecord(encodedAddress);
if (!identityRecord) {
throw new Error(`setVerified: No identity record for ${encodedAddress}`);
2021-02-26 23:42:45 +00:00
}
if (!publicKey || constantTimeEqual(identityRecord.publicKey, publicKey)) {
identityRecord.verified = verifiedStatus;
if (validateIdentityKey(identityRecord)) {
2021-02-26 23:42:45 +00:00
await this._saveIdentityKey(identityRecord);
}
} else {
window.log.info(
'setVerified: No identity record for specified publicKey'
);
2021-02-26 23:42:45 +00:00
}
}
async getVerified(identifier: string): Promise<number> {
if (identifier === null || identifier === undefined) {
throw new Error('getVerified: identifier was undefined/null');
2021-02-26 23:42:45 +00:00
}
const identityRecord = this.getIdentityRecord(identifier);
if (!identityRecord) {
throw new Error(`getVerified: No identity record for ${identifier}`);
2021-02-26 23:42:45 +00:00
}
const verifiedStatus = identityRecord.verified;
if (validateVerifiedStatus(verifiedStatus)) {
return verifiedStatus;
}
return VerifiedStatus.DEFAULT;
}
// Resolves to true if a new identity key was saved
processContactSyncVerificationState(
identifier: string,
verifiedStatus: number,
publicKey: ArrayBuffer
): Promise<boolean> {
if (verifiedStatus === VerifiedStatus.UNVERIFIED) {
return this.processUnverifiedMessage(
identifier,
verifiedStatus,
publicKey
);
}
return this.processVerifiedMessage(identifier, verifiedStatus, publicKey);
}
// This function encapsulates the non-Java behavior, since the mobile apps don't
// currently receive contact syncs and therefore will see a verify sync with
// UNVERIFIED status
async processUnverifiedMessage(
identifier: string,
verifiedStatus: number,
publicKey?: ArrayBuffer
): Promise<boolean> {
if (identifier === null || identifier === undefined) {
throw new Error(
'processUnverifiedMessage: identifier was undefined/null'
);
2021-02-26 23:42:45 +00:00
}
if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) {
throw new Error('processUnverifiedMessage: Invalid public key');
2021-02-26 23:42:45 +00:00
}
const identityRecord = this.getIdentityRecord(identifier);
let isEqual = false;
if (identityRecord && publicKey) {
isEqual = constantTimeEqual(publicKey, identityRecord.publicKey);
}
if (
identityRecord &&
isEqual &&
identityRecord.verified !== VerifiedStatus.UNVERIFIED
) {
await this.setVerified(identifier, verifiedStatus, publicKey);
2021-02-26 23:42:45 +00:00
return false;
}
if (!identityRecord || !isEqual) {
await this.saveIdentityWithAttributes(identifier, {
publicKey,
verified: verifiedStatus,
firstUse: false,
timestamp: Date.now(),
nonblockingApproval: true,
});
2021-02-26 23:42:45 +00:00
if (identityRecord && !isEqual) {
try {
this.trigger('keychange', identifier);
} catch (error) {
window.log.error(
'processUnverifiedMessage: error triggering keychange:',
2021-02-26 23:42:45 +00:00
error && error.stack ? error.stack : error
);
}
await this.archiveAllSessions(identifier);
return true;
}
}
// The situation which could get us here is:
// 1. had a previous key
// 2. new key is the same
// 3. desired new status is same as what we had before
return false;
}
// This matches the Java method as of
// https://github.com/signalapp/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188
async processVerifiedMessage(
identifier: string,
verifiedStatus: number,
publicKey?: ArrayBuffer
2021-02-26 23:42:45 +00:00
): Promise<boolean> {
if (identifier === null || identifier === undefined) {
throw new Error('processVerifiedMessage: identifier was undefined/null');
2021-02-26 23:42:45 +00:00
}
if (!validateVerifiedStatus(verifiedStatus)) {
throw new Error('processVerifiedMessage: Invalid verified status');
2021-02-26 23:42:45 +00:00
}
if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) {
throw new Error('processVerifiedMessage: Invalid public key');
2021-02-26 23:42:45 +00:00
}
const identityRecord = this.getIdentityRecord(identifier);
let isEqual = false;
if (identityRecord && publicKey) {
isEqual = constantTimeEqual(publicKey, identityRecord.publicKey);
}
if (!identityRecord && verifiedStatus === VerifiedStatus.DEFAULT) {
window.log.info(
'processVerifiedMessage: No existing record for default status'
);
2021-02-26 23:42:45 +00:00
return false;
}
if (
identityRecord &&
isEqual &&
identityRecord.verified !== VerifiedStatus.DEFAULT &&
verifiedStatus === VerifiedStatus.DEFAULT
) {
await this.setVerified(identifier, verifiedStatus, publicKey);
2021-02-26 23:42:45 +00:00
return false;
}
if (
verifiedStatus === VerifiedStatus.VERIFIED &&
(!identityRecord ||
(identityRecord && !isEqual) ||
(identityRecord && identityRecord.verified !== VerifiedStatus.VERIFIED))
) {
await this.saveIdentityWithAttributes(identifier, {
publicKey,
verified: verifiedStatus,
firstUse: false,
timestamp: Date.now(),
nonblockingApproval: true,
});
2021-02-26 23:42:45 +00:00
if (identityRecord && !isEqual) {
try {
this.trigger('keychange', identifier);
} catch (error) {
window.log.error(
'processVerifiedMessage error triggering keychange:',
error && error.stack ? error.stack : error
);
}
await this.archiveAllSessions(identifier);
// true signifies that we overwrote a previous key with a new one
return true;
}
}
// We get here if we got a new key and the status is DEFAULT. If the
// message is out of date, we don't want to lose whatever more-secure
// state we had before.
return false;
}
isUntrusted(identifier: string): boolean {
if (identifier === null || identifier === undefined) {
throw new Error('isUntrusted: identifier was undefined/null');
2021-02-26 23:42:45 +00:00
}
const identityRecord = this.getIdentityRecord(identifier);
if (!identityRecord) {
throw new Error(`isUntrusted: No identity record for ${identifier}`);
2021-02-26 23:42:45 +00:00
}
if (
isMoreRecentThan(identityRecord.timestamp, TIMESTAMP_THRESHOLD) &&
2021-02-26 23:42:45 +00:00
!identityRecord.nonblockingApproval &&
!identityRecord.firstUse
) {
return true;
}
return false;
}
async removeIdentityKey(identifier: string): Promise<void> {
if (!this.identityKeys) {
throw new Error('removeIdentityKey: this.identityKeys not yet cached!');
}
const id = window.ConversationController.getConversationId(identifier);
if (id) {
delete this.identityKeys[id];
await window.Signal.Data.removeIdentityKeyById(id);
await this.removeAllSessions(id);
2021-02-26 23:42:45 +00:00
}
}
// Not yet processed messages - for resiliency
getUnprocessedCount(): Promise<number> {
return window.Signal.Data.getUnprocessedCount();
}
getAllUnprocessed(): Promise<Array<UnprocessedType>> {
return window.Signal.Data.getAllUnprocessed();
}
getUnprocessedById(id: string): Promise<UnprocessedType | undefined> {
return window.Signal.Data.getUnprocessedById(id);
}
addUnprocessed(data: UnprocessedType): Promise<string> {
2021-02-26 23:42:45 +00:00
// We need to pass forceSave because the data has an id already, which will cause
// an update instead of an insert.
return window.Signal.Data.saveUnprocessed(data, {
forceSave: true,
});
}
addMultipleUnprocessed(array: Array<UnprocessedType>): Promise<void> {
// We need to pass forceSave because the data has an id already, which will cause
// an update instead of an insert.
return window.Signal.Data.saveUnprocesseds(array, {
forceSave: true,
});
}
updateUnprocessedAttempts(id: string, attempts: number): Promise<void> {
return window.Signal.Data.updateUnprocessedAttempts(id, attempts);
}
updateUnprocessedWithData(
id: string,
data: UnprocessedUpdateType
): Promise<void> {
2021-02-26 23:42:45 +00:00
return window.Signal.Data.updateUnprocessedWithData(id, data);
}
updateUnprocessedsWithData(
items: Array<{ id: string; data: UnprocessedUpdateType }>
): Promise<void> {
2021-02-26 23:42:45 +00:00
return window.Signal.Data.updateUnprocessedsWithData(items);
}
removeUnprocessed(idOrArray: string | Array<string>): Promise<void> {
return window.Signal.Data.removeUnprocessed(idOrArray);
}
removeAllUnprocessed(): Promise<void> {
return window.Signal.Data.removeAllUnprocessed();
}
async removeAllData(): Promise<void> {
await window.Signal.Data.removeAll();
await this.hydrateCaches();
window.storage.reset();
await window.storage.fetch();
window.ConversationController.reset();
await window.ConversationController.load();
}
async removeAllConfiguration(): Promise<void> {
await window.Signal.Data.removeAllConfiguration();
await this.hydrateCaches();
window.storage.reset();
await window.storage.fetch();
}
}
window.SignalProtocolStore = SignalProtocolStore;