UUID-keyed lookups in SignalProtocolStore

This commit is contained in:
Fedor Indutny 2021-09-09 19:38:11 -07:00 committed by GitHub
parent 6323aedd9b
commit c7e7d55af4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 2094 additions and 1447 deletions

View file

@ -7,6 +7,7 @@
/* eslint-disable class-methods-use-this */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import PQueue from 'p-queue';
import { omit } from 'lodash';
import EventTarget from './EventTarget';
import { WebAPIType } from './WebAPI';
@ -21,6 +22,7 @@ import {
generateRegistrationId,
getRandomBytes,
typedArrayToArrayBuffer,
arrayBufferToBase64,
} from '../Crypto';
import {
generateKeyPair,
@ -29,7 +31,7 @@ import {
} from '../Curve';
import { isMoreRecentThan, isOlderThan } from '../util/timestamp';
import { ourProfileKeyService } from '../services/ourProfileKey';
import { assert } from '../util/assert';
import { assert, strictAssert } from '../util/assert';
import { getProvisioningUrl } from '../util/getProvisioningUrl';
import { SignalService as Proto } from '../protobuf';
@ -56,7 +58,7 @@ function getIdentifier(id: string | undefined) {
return parts[0];
}
type GeneratedKeysType = {
export type GeneratedKeysType = {
preKeys: Array<{
keyId: number;
publicKey: ArrayBuffer;
@ -89,16 +91,10 @@ export default class AccountManager extends EventTarget {
return this.server.requestVerificationSMS(number);
}
async encryptDeviceName(name: string, providedIdentityKey?: KeyPairType) {
async encryptDeviceName(name: string, identityKey: KeyPairType) {
if (!name) {
return null;
}
const identityKey =
providedIdentityKey ||
(await window.textsecure.storage.protocol.getIdentityKeyPair());
if (!identityKey) {
throw new Error('Identity key was not provided and is not in database!');
}
const encrypted = await window.Signal.Crypto.encryptDeviceName(
name,
identityKey.pubKey
@ -114,7 +110,10 @@ export default class AccountManager extends EventTarget {
}
async decryptDeviceName(base64: string) {
const identityKey = await window.textsecure.storage.protocol.getIdentityKeyPair();
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const identityKey = await window.textsecure.storage.protocol.getIdentityKeyPair(
ourUuid
);
if (!identityKey) {
throw new Error('decryptDeviceName: No identity key pair!');
}
@ -144,8 +143,19 @@ export default class AccountManager extends EventTarget {
if (isNameEncrypted) {
return;
}
const deviceName = window.textsecure.storage.user.getDeviceName();
const base64 = await this.encryptDeviceName(deviceName || '');
const { storage } = window.textsecure;
const deviceName = storage.user.getDeviceName();
const identityKeyPair = await storage.protocol.getIdentityKeyPair(
storage.user.getCheckedUuid()
);
strictAssert(
identityKeyPair !== undefined,
"Can't encrypt device name without identity key pair"
);
const base64 = await this.encryptDeviceName(
deviceName || '',
identityKeyPair
);
if (base64) {
await this.server.updateDeviceName(base64);
@ -310,6 +320,7 @@ export default class AccountManager extends EventTarget {
async rotateSignedPreKey() {
return this.queueTask(async () => {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const signedKeyId = window.textsecure.storage.get('signedKeyId', 1);
if (typeof signedKeyId !== 'number') {
throw new Error('Invalid signedKeyId');
@ -318,7 +329,7 @@ export default class AccountManager extends EventTarget {
const store = window.textsecure.storage.protocol;
const { server, cleanSignedPreKeys } = this;
const existingKeys = await store.loadSignedPreKeys();
const existingKeys = await store.loadSignedPreKeys(ourUuid);
existingKeys.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
const confirmedKeys = existingKeys.filter(key => key.confirmed);
const mostRecent = confirmedKeys[0];
@ -332,7 +343,7 @@ export default class AccountManager extends EventTarget {
// eslint-disable-next-line consistent-return
return store
.getIdentityKeyPair()
.getIdentityKeyPair(ourUuid)
.then(
async (identityKey: KeyPairType | undefined) => {
if (!identityKey) {
@ -357,7 +368,7 @@ export default class AccountManager extends EventTarget {
window.log.info('Saving new signed prekey', res.keyId);
return Promise.all([
window.textsecure.storage.put('signedKeyId', signedKeyId + 1),
store.storeSignedPreKey(res.keyId, res.keyPair),
store.storeSignedPreKey(ourUuid, res.keyId, res.keyPair),
server.setSignedPreKey({
keyId: res.keyId,
publicKey: res.keyPair.pubKey,
@ -369,7 +380,12 @@ export default class AccountManager extends EventTarget {
window.log.info('Confirming new signed prekey', res.keyId);
return Promise.all([
window.textsecure.storage.remove('signedKeyRotationRejected'),
store.storeSignedPreKey(res.keyId, res.keyPair, confirmed),
store.storeSignedPreKey(
ourUuid,
res.keyId,
res.keyPair,
confirmed
),
]);
})
.then(cleanSignedPreKeys);
@ -409,9 +425,10 @@ export default class AccountManager extends EventTarget {
}
async cleanSignedPreKeys() {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const store = window.textsecure.storage.protocol;
const allKeys = await store.loadSignedPreKeys();
const allKeys = await store.loadSignedPreKeys(ourUuid);
allKeys.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
const confirmed = allKeys.filter(key => key.confirmed);
const unconfirmed = allKeys.filter(key => !key.confirmed);
@ -448,7 +465,7 @@ export default class AccountManager extends EventTarget {
window.log.info(
`Removing signed prekey: ${key.keyId} with timestamp ${timestamp}${confirmedText}`
);
await store.removeSignedPreKey(key.keyId);
await store.removeSignedPreKey(ourUuid, key.keyId);
}
})
);
@ -464,17 +481,14 @@ export default class AccountManager extends EventTarget {
readReceipts?: boolean | null,
options: { accessKey?: ArrayBuffer; uuid?: string } = {}
): Promise<void> {
const { storage } = window.textsecure;
const { accessKey, uuid } = options;
let password = btoa(utils.getString(getRandomBytes(16)));
password = password.substring(0, password.length - 2);
const registrationId = generateRegistrationId();
const previousNumber = getIdentifier(
window.textsecure.storage.get('number_id')
);
const previousUuid = getIdentifier(
window.textsecure.storage.get('uuid_id')
);
const previousNumber = getIdentifier(storage.get('number_id'));
const previousUuid = getIdentifier(storage.get('uuid_id'));
let encryptedDeviceName;
if (deviceName) {
@ -500,7 +514,10 @@ export default class AccountManager extends EventTarget {
{ accessKey, uuid }
);
const uuidChanged = previousUuid && uuid && previousUuid !== uuid;
const ourUuid = uuid || response.uuid;
strictAssert(ourUuid !== undefined, 'Should have UUID after registration');
const uuidChanged = previousUuid && ourUuid && previousUuid !== ourUuid;
// We only consider the number changed if we didn't have a UUID before
const numberChanged =
@ -519,7 +536,7 @@ export default class AccountManager extends EventTarget {
}
try {
await window.textsecure.storage.protocol.removeAllData();
await storage.protocol.removeAllData();
window.log.info('Successfully deleted previous data');
} catch (error) {
window.log.error(
@ -530,23 +547,34 @@ export default class AccountManager extends EventTarget {
}
await Promise.all([
window.textsecure.storage.remove('identityKey'),
window.textsecure.storage.user.removeCredentials(),
window.textsecure.storage.remove('registrationId'),
window.textsecure.storage.remove('regionCode'),
window.textsecure.storage.remove('userAgent'),
window.textsecure.storage.remove('profileKey'),
window.textsecure.storage.remove('read-receipt-setting'),
storage.user.removeCredentials(),
storage.remove('regionCode'),
storage.remove('userAgent'),
storage.remove('profileKey'),
storage.remove('read-receipt-setting'),
]);
if (previousUuid) {
await Promise.all([
storage.put(
'identityKeyMap',
omit(storage.get('identityKeyMap') || {}, previousUuid)
),
storage.put(
'registrationIdMap',
omit(storage.get('registrationIdMap') || {}, previousUuid)
),
]);
}
// `setCredentials` needs to be called
// before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes`
// indirectly calls `ConversationController.getConverationId()` which
// initializes the conversation for the given number (our number) which
// calls out to the user storage API to get the stored UUID and number
// information.
await window.textsecure.storage.user.setCredentials({
uuid,
await storage.user.setCredentials({
uuid: ourUuid,
number,
deviceId: response.deviceId ?? 1,
deviceName: deviceName ?? undefined,
@ -558,7 +586,7 @@ export default class AccountManager extends EventTarget {
// below.
const conversationId = window.ConversationController.ensureContactIds({
e164: number,
uuid,
uuid: ourUuid,
highTrust: true,
});
@ -568,36 +596,42 @@ export default class AccountManager extends EventTarget {
// update our own identity key, which may have changed
// if we're relinking after a reinstall on the master device
await window.textsecure.storage.protocol.saveIdentityWithAttributes(
uuid || number,
{
publicKey: identityKeyPair.pubKey,
firstUse: true,
timestamp: Date.now(),
verified: window.textsecure.storage.protocol.VerifiedStatus.VERIFIED,
nonblockingApproval: true,
}
);
await storage.protocol.saveIdentityWithAttributes(ourUuid, {
publicKey: identityKeyPair.pubKey,
firstUse: true,
timestamp: Date.now(),
verified: storage.protocol.VerifiedStatus.VERIFIED,
nonblockingApproval: true,
});
await window.textsecure.storage.put('identityKey', identityKeyPair);
await window.textsecure.storage.put('registrationId', registrationId);
const identityKeyMap = {
...(storage.get('identityKeyMap') || {}),
[ourUuid]: {
pubKey: arrayBufferToBase64(identityKeyPair.pubKey),
privKey: arrayBufferToBase64(identityKeyPair.privKey),
},
};
const registrationIdMap = {
...(storage.get('registrationIdMap') || {}),
[ourUuid]: registrationId,
};
await storage.put('identityKeyMap', identityKeyMap);
await storage.put('registrationIdMap', registrationIdMap);
if (profileKey) {
await ourProfileKeyService.set(profileKey);
}
if (userAgent) {
await window.textsecure.storage.put('userAgent', userAgent);
await storage.put('userAgent', userAgent);
}
await window.textsecure.storage.put(
'read-receipt-setting',
Boolean(readReceipts)
);
await storage.put('read-receipt-setting', Boolean(readReceipts));
const regionCode = window.libphonenumber.util.getRegionCodeForNumber(
number
);
await window.textsecure.storage.put('regionCode', regionCode);
await window.textsecure.storage.protocol.hydrateCaches();
await storage.put('regionCode', regionCode);
await storage.protocol.hydrateCaches();
}
async clearSessionsAndPreKeys() {
@ -626,7 +660,8 @@ export default class AccountManager extends EventTarget {
}
window.log.info('confirmKeys: confirming key', key.keyId);
await store.storeSignedPreKey(key.keyId, key.keyPair, confirmed);
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
await store.storeSignedPreKey(ourUuid, key.keyId, key.keyPair, confirmed);
}
async generateKeys(count: number, providedProgressCallback?: Function) {
@ -636,6 +671,7 @@ export default class AccountManager extends EventTarget {
: null;
const startId = window.textsecure.storage.get('maxPreKeyId', 1);
const signedKeyId = window.textsecure.storage.get('signedKeyId', 1);
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
if (typeof startId !== 'number') {
throw new Error('Invalid maxPreKeyId');
@ -645,7 +681,7 @@ export default class AccountManager extends EventTarget {
}
const store = window.textsecure.storage.protocol;
return store.getIdentityKeyPair().then(async identityKey => {
return store.getIdentityKeyPair(ourUuid).then(async identityKey => {
if (!identityKey) {
throw new Error('generateKeys: No identity key pair!');
}
@ -659,7 +695,7 @@ export default class AccountManager extends EventTarget {
for (let keyId = startId; keyId < startId + count; keyId += 1) {
promises.push(
Promise.resolve(generatePreKey(keyId)).then(async res => {
await store.storePreKey(res.keyId, res.keyPair);
await store.storePreKey(ourUuid, res.keyId, res.keyPair);
result.preKeys.push({
keyId: res.keyId,
publicKey: res.keyPair.pubKey,
@ -674,7 +710,7 @@ export default class AccountManager extends EventTarget {
promises.push(
Promise.resolve(generateSignedPreKey(identityKey, signedKeyId)).then(
async res => {
await store.storeSignedPreKey(res.keyId, res.keyPair);
await store.storeSignedPreKey(ourUuid, res.keyId, res.keyPair);
result.signedPreKey = {
keyId: res.keyId,
publicKey: res.keyPair.pubKey,