2021-06-07 16:27:02 +00:00
|
|
|
// Copyright 2020-2021 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2020-09-24 21:53:21 +00:00
|
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
|
|
/* eslint-disable @typescript-eslint/ban-types */
|
|
|
|
/* eslint-disable more/no-then */
|
|
|
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
|
|
import PQueue from 'p-queue';
|
2021-09-10 02:38:11 +00:00
|
|
|
import { omit } from 'lodash';
|
2020-04-13 17:37:29 +00:00
|
|
|
|
|
|
|
import EventTarget from './EventTarget';
|
2021-09-22 00:58:03 +00:00
|
|
|
import type { WebAPIType } from './WebAPI';
|
|
|
|
import { HTTPError } from './Errors';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { KeyPairType, CompatSignedPreKeyType } from './Types.d';
|
2020-04-13 17:37:29 +00:00
|
|
|
import ProvisioningCipher from './ProvisioningCipher';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { IncomingWebSocketRequest } from './WebsocketResources';
|
2021-07-28 21:37:09 +00:00
|
|
|
import createTaskWithTimeout from './TaskWithTimeout';
|
2021-07-02 19:21:24 +00:00
|
|
|
import * as Bytes from '../Bytes';
|
2021-09-27 17:31:34 +00:00
|
|
|
import { RemoveAllConfiguration } from '../types/RemoveAllConfiguration';
|
|
|
|
import { senderCertificateService } from '../services/senderCertificate';
|
2021-04-16 23:13:13 +00:00
|
|
|
import {
|
|
|
|
deriveAccessKey,
|
|
|
|
generateRegistrationId,
|
|
|
|
getRandomBytes,
|
2021-09-24 00:49:05 +00:00
|
|
|
decryptDeviceName,
|
|
|
|
encryptDeviceName,
|
2021-04-16 23:13:13 +00:00
|
|
|
} from '../Crypto';
|
|
|
|
import {
|
|
|
|
generateKeyPair,
|
|
|
|
generateSignedPreKey,
|
|
|
|
generatePreKey,
|
|
|
|
} from '../Curve';
|
2021-09-22 00:58:03 +00:00
|
|
|
import { UUID } from '../types/UUID';
|
2021-03-22 21:08:52 +00:00
|
|
|
import { isMoreRecentThan, isOlderThan } from '../util/timestamp';
|
2021-05-05 16:39:16 +00:00
|
|
|
import { ourProfileKeyService } from '../services/ourProfileKey';
|
2021-09-10 02:38:11 +00:00
|
|
|
import { assert, strictAssert } from '../util/assert';
|
2021-06-07 16:27:02 +00:00
|
|
|
import { getProvisioningUrl } from '../util/getProvisioningUrl';
|
2021-07-02 19:21:24 +00:00
|
|
|
import { SignalService as Proto } from '../protobuf';
|
2021-09-17 18:27:53 +00:00
|
|
|
import * as log from '../logging/log';
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-08-03 22:26:00 +00:00
|
|
|
const DAY = 24 * 60 * 60 * 1000;
|
|
|
|
const MINIMUM_SIGNED_PREKEYS = 5;
|
|
|
|
const ARCHIVE_AGE = 30 * DAY;
|
2021-08-20 18:27:12 +00:00
|
|
|
const PREKEY_ROTATION_AGE = DAY * 1.5;
|
2021-04-16 23:13:13 +00:00
|
|
|
const PROFILE_KEY_LENGTH = 32;
|
|
|
|
const SIGNED_KEY_GEN_BATCH_SIZE = 100;
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-06-15 00:09:37 +00:00
|
|
|
function getIdentifier(id: string | undefined) {
|
2020-04-13 17:37:29 +00:00
|
|
|
if (!id || !id.length) {
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
|
|
|
const parts = id.split('.');
|
|
|
|
if (!parts.length) {
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
|
|
|
return parts[0];
|
|
|
|
}
|
|
|
|
|
2021-09-10 02:38:11 +00:00
|
|
|
export type GeneratedKeysType = {
|
2020-04-13 17:37:29 +00:00
|
|
|
preKeys: Array<{
|
|
|
|
keyId: number;
|
2021-09-24 00:49:05 +00:00
|
|
|
publicKey: Uint8Array;
|
2020-04-13 17:37:29 +00:00
|
|
|
}>;
|
|
|
|
signedPreKey: {
|
|
|
|
keyId: number;
|
2021-09-24 00:49:05 +00:00
|
|
|
publicKey: Uint8Array;
|
|
|
|
signature: Uint8Array;
|
2020-04-13 17:37:29 +00:00
|
|
|
keyPair: KeyPairType;
|
|
|
|
};
|
2021-09-24 00:49:05 +00:00
|
|
|
identityKey: Uint8Array;
|
2020-04-13 17:37:29 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
export default class AccountManager extends EventTarget {
|
|
|
|
pending: Promise<void>;
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
pendingQueue?: PQueue;
|
|
|
|
|
2021-07-23 17:23:50 +00:00
|
|
|
constructor(private readonly server: WebAPIType) {
|
2020-04-13 17:37:29 +00:00
|
|
|
super();
|
|
|
|
|
|
|
|
this.pending = Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
async requestVoiceVerification(number: string) {
|
|
|
|
return this.server.requestVerificationVoice(number);
|
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
async requestSMSVerification(number: string) {
|
|
|
|
return this.server.requestVerificationSMS(number);
|
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
encryptDeviceName(name: string, identityKey: KeyPairType) {
|
2020-04-13 17:37:29 +00:00
|
|
|
if (!name) {
|
|
|
|
return null;
|
|
|
|
}
|
2021-09-24 00:49:05 +00:00
|
|
|
const encrypted = encryptDeviceName(name, identityKey.pubKey);
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-07-02 19:21:24 +00:00
|
|
|
const proto = new Proto.DeviceName();
|
2021-09-24 00:49:05 +00:00
|
|
|
proto.ephemeralPublic = encrypted.ephemeralPublic;
|
|
|
|
proto.syntheticIv = encrypted.syntheticIv;
|
|
|
|
proto.ciphertext = encrypted.ciphertext;
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-07-02 19:21:24 +00:00
|
|
|
const bytes = Proto.DeviceName.encode(proto).finish();
|
|
|
|
return Bytes.toBase64(bytes);
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
async decryptDeviceName(base64: string) {
|
2021-09-10 02:38:11 +00:00
|
|
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
|
2021-11-11 22:43:05 +00:00
|
|
|
const identityKey =
|
|
|
|
await window.textsecure.storage.protocol.getIdentityKeyPair(ourUuid);
|
2021-04-16 23:13:13 +00:00
|
|
|
if (!identityKey) {
|
|
|
|
throw new Error('decryptDeviceName: No identity key pair!');
|
|
|
|
}
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-07-02 19:21:24 +00:00
|
|
|
const bytes = Bytes.fromBase64(base64);
|
|
|
|
const proto = Proto.DeviceName.decode(bytes);
|
|
|
|
assert(
|
|
|
|
proto.ephemeralPublic && proto.syntheticIv && proto.ciphertext,
|
|
|
|
'Missing required fields in DeviceName'
|
|
|
|
);
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
const name = decryptDeviceName(proto, identityKey.privKey);
|
2020-04-13 17:37:29 +00:00
|
|
|
|
|
|
|
return name;
|
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
async maybeUpdateDeviceName() {
|
2021-11-11 22:43:05 +00:00
|
|
|
const isNameEncrypted =
|
|
|
|
window.textsecure.storage.user.getDeviceNameEncrypted();
|
2020-04-13 17:37:29 +00:00
|
|
|
if (isNameEncrypted) {
|
|
|
|
return;
|
|
|
|
}
|
2021-09-10 02:38:11 +00:00
|
|
|
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"
|
|
|
|
);
|
2021-09-24 00:49:05 +00:00
|
|
|
const base64 = this.encryptDeviceName(deviceName || '', identityKeyPair);
|
2020-04-13 17:37:29 +00:00
|
|
|
|
|
|
|
if (base64) {
|
|
|
|
await this.server.updateDeviceName(base64);
|
|
|
|
}
|
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
async deviceNameIsEncrypted() {
|
|
|
|
await window.textsecure.storage.user.setDeviceNameEncrypted();
|
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
async registerSingleDevice(number: string, verificationCode: string) {
|
2021-04-16 23:13:13 +00:00
|
|
|
return this.queueTask(async () => {
|
|
|
|
const identityKeyPair = generateKeyPair();
|
|
|
|
const profileKey = getRandomBytes(PROFILE_KEY_LENGTH);
|
2021-09-24 00:49:05 +00:00
|
|
|
const accessKey = deriveAccessKey(profileKey);
|
2021-04-16 23:13:13 +00:00
|
|
|
|
|
|
|
await this.createAccount(
|
|
|
|
number,
|
|
|
|
verificationCode,
|
|
|
|
identityKeyPair,
|
|
|
|
profileKey,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
{ accessKey }
|
|
|
|
);
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-04-16 23:13:13 +00:00
|
|
|
await this.clearSessionsAndPreKeys();
|
|
|
|
const keys = await this.generateKeys(SIGNED_KEY_GEN_BATCH_SIZE);
|
|
|
|
await this.server.registerKeys(keys);
|
|
|
|
await this.confirmKeys(keys);
|
|
|
|
await this.registrationDone();
|
|
|
|
});
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async registerSecondDevice(
|
|
|
|
setProvisioningUrl: Function,
|
|
|
|
confirmNumber: (number?: string) => Promise<string>,
|
2021-09-17 18:27:53 +00:00
|
|
|
progressCallback?: Function
|
2020-04-13 17:37:29 +00:00
|
|
|
) {
|
|
|
|
const createAccount = this.createAccount.bind(this);
|
|
|
|
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
|
2021-04-16 23:13:13 +00:00
|
|
|
const generateKeys = this.generateKeys.bind(
|
|
|
|
this,
|
|
|
|
SIGNED_KEY_GEN_BATCH_SIZE,
|
|
|
|
progressCallback
|
|
|
|
);
|
2020-04-13 17:37:29 +00:00
|
|
|
const provisioningCipher = new ProvisioningCipher();
|
2021-06-09 22:28:54 +00:00
|
|
|
const pubKey = await provisioningCipher.getPublicKey();
|
|
|
|
|
2021-07-28 21:37:09 +00:00
|
|
|
let envelopeCallbacks:
|
|
|
|
| {
|
|
|
|
resolve(data: Proto.ProvisionEnvelope): void;
|
|
|
|
reject(error: Error): void;
|
|
|
|
}
|
|
|
|
| undefined;
|
|
|
|
const envelopePromise = new Promise<Proto.ProvisionEnvelope>(
|
|
|
|
(resolve, reject) => {
|
|
|
|
envelopeCallbacks = { resolve, reject };
|
|
|
|
}
|
|
|
|
);
|
2021-06-09 22:28:54 +00:00
|
|
|
|
2021-07-28 21:37:09 +00:00
|
|
|
const wsr = await this.server.getProvisioningResource({
|
|
|
|
handleRequest(request: IncomingWebSocketRequest) {
|
|
|
|
if (
|
|
|
|
request.path === '/v1/address' &&
|
|
|
|
request.verb === 'PUT' &&
|
|
|
|
request.body
|
|
|
|
) {
|
|
|
|
const proto = Proto.ProvisioningUuid.decode(request.body);
|
|
|
|
const { uuid } = proto;
|
|
|
|
if (!uuid) {
|
|
|
|
throw new Error('registerSecondDevice: expected a UUID');
|
|
|
|
}
|
|
|
|
const url = getProvisioningUrl(uuid, pubKey);
|
2021-06-09 22:28:54 +00:00
|
|
|
|
2021-08-11 19:29:07 +00:00
|
|
|
window.CI?.setProvisioningURL(url);
|
2021-07-28 21:37:09 +00:00
|
|
|
|
|
|
|
setProvisioningUrl(url);
|
|
|
|
request.respond(200, 'OK');
|
|
|
|
} else if (
|
|
|
|
request.path === '/v1/message' &&
|
|
|
|
request.verb === 'PUT' &&
|
|
|
|
request.body
|
|
|
|
) {
|
|
|
|
const envelope = Proto.ProvisionEnvelope.decode(request.body);
|
|
|
|
request.respond(200, 'OK');
|
|
|
|
wsr.close();
|
|
|
|
envelopeCallbacks?.resolve(envelope);
|
|
|
|
} else {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.error('Unknown websocket message', request.path);
|
2021-06-09 22:28:54 +00:00
|
|
|
}
|
2021-07-28 21:37:09 +00:00
|
|
|
},
|
|
|
|
});
|
2021-06-09 22:28:54 +00:00
|
|
|
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info('provisioning socket open');
|
2021-06-09 22:28:54 +00:00
|
|
|
|
2021-07-28 21:37:09 +00:00
|
|
|
wsr.addEventListener('close', ({ code, reason }) => {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info(`provisioning socket closed. Code: ${code} Reason: ${reason}`);
|
2021-06-09 22:28:54 +00:00
|
|
|
|
2021-07-28 21:37:09 +00:00
|
|
|
// Note: if we have resolved the envelope already - this has no effect
|
|
|
|
envelopeCallbacks?.reject(new Error('websocket closed'));
|
|
|
|
});
|
|
|
|
|
|
|
|
const envelope = await envelopePromise;
|
|
|
|
const provisionMessage = await provisioningCipher.decrypt(envelope);
|
|
|
|
|
|
|
|
await this.queueTask(async () => {
|
|
|
|
const deviceName = await confirmNumber(provisionMessage.number);
|
|
|
|
if (typeof deviceName !== 'string' || deviceName.length === 0) {
|
|
|
|
throw new Error(
|
|
|
|
'AccountManager.registerSecondDevice: Invalid device name'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
!provisionMessage.number ||
|
|
|
|
!provisionMessage.provisioningCode ||
|
|
|
|
!provisionMessage.identityKeyPair
|
|
|
|
) {
|
|
|
|
throw new Error(
|
|
|
|
'AccountManager.registerSecondDevice: Provision message was missing key data'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
await createAccount(
|
|
|
|
provisionMessage.number,
|
|
|
|
provisionMessage.provisioningCode,
|
|
|
|
provisionMessage.identityKeyPair,
|
|
|
|
provisionMessage.profileKey,
|
|
|
|
deviceName,
|
|
|
|
provisionMessage.userAgent,
|
|
|
|
provisionMessage.readReceipts,
|
|
|
|
{ uuid: provisionMessage.uuid }
|
|
|
|
);
|
|
|
|
await clearSessionsAndPreKeys();
|
|
|
|
const keys = await generateKeys();
|
|
|
|
await this.server.registerKeys(keys);
|
|
|
|
await this.confirmKeys(keys);
|
|
|
|
await this.registrationDone();
|
2021-06-09 22:28:54 +00:00
|
|
|
});
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
async refreshPreKeys() {
|
2021-04-16 23:13:13 +00:00
|
|
|
const generateKeys = this.generateKeys.bind(
|
|
|
|
this,
|
|
|
|
SIGNED_KEY_GEN_BATCH_SIZE
|
|
|
|
);
|
2020-04-13 17:37:29 +00:00
|
|
|
const registerKeys = this.server.registerKeys.bind(this.server);
|
|
|
|
|
|
|
|
return this.queueTask(async () =>
|
|
|
|
this.server.getMyKeys().then(async preKeyCount => {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info(`prekey count ${preKeyCount}`);
|
2020-04-13 17:37:29 +00:00
|
|
|
if (preKeyCount < 10) {
|
|
|
|
return generateKeys().then(registerKeys);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
async rotateSignedPreKey() {
|
|
|
|
return this.queueTask(async () => {
|
2021-09-10 02:38:11 +00:00
|
|
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
|
2020-04-13 17:37:29 +00:00
|
|
|
const signedKeyId = window.textsecure.storage.get('signedKeyId', 1);
|
|
|
|
if (typeof signedKeyId !== 'number') {
|
|
|
|
throw new Error('Invalid signedKeyId');
|
|
|
|
}
|
|
|
|
|
|
|
|
const store = window.textsecure.storage.protocol;
|
|
|
|
const { server, cleanSignedPreKeys } = this;
|
|
|
|
|
2021-09-10 02:38:11 +00:00
|
|
|
const existingKeys = await store.loadSignedPreKeys(ourUuid);
|
2020-06-11 20:29:14 +00:00
|
|
|
existingKeys.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
|
|
|
|
const confirmedKeys = existingKeys.filter(key => key.confirmed);
|
2021-08-03 22:26:00 +00:00
|
|
|
const mostRecent = confirmedKeys[0];
|
2020-06-11 20:29:14 +00:00
|
|
|
|
2021-08-20 18:27:12 +00:00
|
|
|
if (isMoreRecentThan(mostRecent?.created_at || 0, PREKEY_ROTATION_AGE)) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.warn(
|
2021-08-03 22:26:00 +00:00
|
|
|
`rotateSignedPreKey: ${confirmedKeys.length} confirmed keys, most recent was created ${mostRecent?.created_at}. Cancelling rotation.`
|
2020-06-11 20:29:14 +00:00
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
return store
|
2021-09-10 02:38:11 +00:00
|
|
|
.getIdentityKeyPair(ourUuid)
|
2020-04-13 17:37:29 +00:00
|
|
|
.then(
|
2021-04-16 23:13:13 +00:00
|
|
|
async (identityKey: KeyPairType | undefined) => {
|
|
|
|
if (!identityKey) {
|
|
|
|
throw new Error('rotateSignedPreKey: No identity key pair!');
|
|
|
|
}
|
|
|
|
|
|
|
|
return generateSignedPreKey(identityKey, signedKeyId);
|
|
|
|
},
|
2020-04-13 17:37:29 +00:00
|
|
|
() => {
|
|
|
|
// We swallow any error here, because we don't want to get into
|
|
|
|
// a loop of repeated retries.
|
2021-09-17 18:27:53 +00:00
|
|
|
log.error('Failed to get identity key. Canceling key rotation.');
|
2020-04-13 17:37:29 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
)
|
2021-04-16 23:13:13 +00:00
|
|
|
.then(async (res: CompatSignedPreKeyType | null) => {
|
2020-04-13 17:37:29 +00:00
|
|
|
if (!res) {
|
|
|
|
return null;
|
|
|
|
}
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info('Saving new signed prekey', res.keyId);
|
2020-04-13 17:37:29 +00:00
|
|
|
return Promise.all([
|
|
|
|
window.textsecure.storage.put('signedKeyId', signedKeyId + 1),
|
2021-09-10 02:38:11 +00:00
|
|
|
store.storeSignedPreKey(ourUuid, res.keyId, res.keyPair),
|
2020-04-13 17:37:29 +00:00
|
|
|
server.setSignedPreKey({
|
|
|
|
keyId: res.keyId,
|
|
|
|
publicKey: res.keyPair.pubKey,
|
|
|
|
signature: res.signature,
|
|
|
|
}),
|
|
|
|
])
|
|
|
|
.then(async () => {
|
|
|
|
const confirmed = true;
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info('Confirming new signed prekey', res.keyId);
|
2020-04-13 17:37:29 +00:00
|
|
|
return Promise.all([
|
|
|
|
window.textsecure.storage.remove('signedKeyRotationRejected'),
|
2021-09-10 02:38:11 +00:00
|
|
|
store.storeSignedPreKey(
|
|
|
|
ourUuid,
|
|
|
|
res.keyId,
|
|
|
|
res.keyPair,
|
|
|
|
confirmed
|
|
|
|
),
|
2020-04-13 17:37:29 +00:00
|
|
|
]);
|
|
|
|
})
|
|
|
|
.then(cleanSignedPreKeys);
|
|
|
|
})
|
|
|
|
.catch(async (e: Error) => {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.error('rotateSignedPrekey error:', e && e.stack ? e.stack : e);
|
2020-04-13 17:37:29 +00:00
|
|
|
|
|
|
|
if (
|
2021-09-22 00:58:03 +00:00
|
|
|
e instanceof HTTPError &&
|
2020-04-13 17:37:29 +00:00
|
|
|
e.code &&
|
|
|
|
e.code >= 400 &&
|
|
|
|
e.code <= 599
|
|
|
|
) {
|
|
|
|
const rejections =
|
|
|
|
1 + window.textsecure.storage.get('signedKeyRotationRejected', 0);
|
|
|
|
await window.textsecure.storage.put(
|
|
|
|
'signedKeyRotationRejected',
|
|
|
|
rejections
|
|
|
|
);
|
2021-09-17 18:27:53 +00:00
|
|
|
log.error('Signed key rotation rejected count:', rejections);
|
2020-04-13 17:37:29 +00:00
|
|
|
} else {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
async queueTask(task: () => Promise<any>) {
|
|
|
|
this.pendingQueue = this.pendingQueue || new PQueue({ concurrency: 1 });
|
2021-07-28 21:37:09 +00:00
|
|
|
const taskWithTimeout = createTaskWithTimeout(task, 'AccountManager task');
|
2020-04-13 17:37:29 +00:00
|
|
|
|
|
|
|
return this.pendingQueue.add(taskWithTimeout);
|
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
async cleanSignedPreKeys() {
|
2021-09-10 02:38:11 +00:00
|
|
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
|
2020-04-13 17:37:29 +00:00
|
|
|
const store = window.textsecure.storage.protocol;
|
|
|
|
|
2021-09-10 02:38:11 +00:00
|
|
|
const allKeys = await store.loadSignedPreKeys(ourUuid);
|
2021-08-03 22:26:00 +00:00
|
|
|
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);
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-08-03 22:26:00 +00:00
|
|
|
const recent = allKeys[0] ? allKeys[0].keyId : 'none';
|
|
|
|
const recentConfirmed = confirmed[0] ? confirmed[0].keyId : 'none';
|
|
|
|
const recentUnconfirmed = unconfirmed[0] ? unconfirmed[0].keyId : 'none';
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info(`cleanSignedPreKeys: Most recent signed key: ${recent}`);
|
|
|
|
log.info(
|
2021-08-03 22:26:00 +00:00
|
|
|
`cleanSignedPreKeys: Most recent confirmed signed key: ${recentConfirmed}`
|
|
|
|
);
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info(
|
2021-08-03 22:26:00 +00:00
|
|
|
`cleanSignedPreKeys: Most recent unconfirmed signed key: ${recentUnconfirmed}`
|
|
|
|
);
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info(
|
2021-08-03 22:26:00 +00:00
|
|
|
'cleanSignedPreKeys: Total signed key count:',
|
|
|
|
allKeys.length,
|
|
|
|
'-',
|
|
|
|
confirmed.length,
|
|
|
|
'confirmed'
|
|
|
|
);
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-08-03 22:26:00 +00:00
|
|
|
// Keep MINIMUM_SIGNED_PREKEYS keys, then drop if older than ARCHIVE_AGE
|
|
|
|
await Promise.all(
|
|
|
|
allKeys.map(async (key, index) => {
|
|
|
|
if (index < MINIMUM_SIGNED_PREKEYS) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const createdAt = key.created_at || 0;
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-08-03 22:26:00 +00:00
|
|
|
if (isOlderThan(createdAt, ARCHIVE_AGE)) {
|
|
|
|
const timestamp = new Date(createdAt).toJSON();
|
|
|
|
const confirmedText = key.confirmed ? ' (confirmed)' : '';
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info(
|
2021-08-03 22:26:00 +00:00
|
|
|
`Removing signed prekey: ${key.keyId} with timestamp ${timestamp}${confirmedText}`
|
|
|
|
);
|
2021-09-10 02:38:11 +00:00
|
|
|
await store.removeSignedPreKey(ourUuid, key.keyId);
|
2021-08-03 22:26:00 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
);
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async createAccount(
|
|
|
|
number: string,
|
|
|
|
verificationCode: string,
|
|
|
|
identityKeyPair: KeyPairType,
|
2021-09-24 00:49:05 +00:00
|
|
|
profileKey: Uint8Array | undefined,
|
2020-04-13 17:37:29 +00:00
|
|
|
deviceName: string | null,
|
|
|
|
userAgent?: string | null,
|
|
|
|
readReceipts?: boolean | null,
|
2021-09-24 00:49:05 +00:00
|
|
|
options: { accessKey?: Uint8Array; uuid?: string } = {}
|
2020-04-13 17:37:29 +00:00
|
|
|
): Promise<void> {
|
2021-09-10 02:38:11 +00:00
|
|
|
const { storage } = window.textsecure;
|
2021-01-11 21:59:46 +00:00
|
|
|
const { accessKey, uuid } = options;
|
2021-09-24 00:49:05 +00:00
|
|
|
let password = Bytes.toBase64(getRandomBytes(16));
|
2020-04-13 17:37:29 +00:00
|
|
|
password = password.substring(0, password.length - 2);
|
2021-04-16 23:13:13 +00:00
|
|
|
const registrationId = generateRegistrationId();
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-09-10 02:38:11 +00:00
|
|
|
const previousNumber = getIdentifier(storage.get('number_id'));
|
|
|
|
const previousUuid = getIdentifier(storage.get('uuid_id'));
|
2020-04-13 17:37:29 +00:00
|
|
|
|
|
|
|
let encryptedDeviceName;
|
|
|
|
if (deviceName) {
|
2021-09-24 00:49:05 +00:00
|
|
|
encryptedDeviceName = this.encryptDeviceName(deviceName, identityKeyPair);
|
2020-04-13 17:37:29 +00:00
|
|
|
await this.deviceNameIsEncrypted();
|
|
|
|
}
|
|
|
|
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info(
|
2020-04-13 17:37:29 +00:00
|
|
|
`createAccount: Number is ${number}, password has length: ${
|
|
|
|
password ? password.length : 'none'
|
|
|
|
}`
|
|
|
|
);
|
|
|
|
|
|
|
|
const response = await this.server.confirmCode(
|
|
|
|
number,
|
|
|
|
verificationCode,
|
|
|
|
password,
|
|
|
|
registrationId,
|
|
|
|
encryptedDeviceName,
|
2021-08-04 20:12:35 +00:00
|
|
|
{ accessKey, uuid }
|
2020-04-13 17:37:29 +00:00
|
|
|
);
|
|
|
|
|
2021-09-10 02:38:11 +00:00
|
|
|
const ourUuid = uuid || response.uuid;
|
|
|
|
strictAssert(ourUuid !== undefined, 'Should have UUID after registration');
|
|
|
|
|
|
|
|
const uuidChanged = previousUuid && ourUuid && previousUuid !== ourUuid;
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-06-08 18:54:20 +00:00
|
|
|
// We only consider the number changed if we didn't have a UUID before
|
|
|
|
const numberChanged =
|
|
|
|
!previousUuid && previousNumber && previousNumber !== number;
|
|
|
|
|
|
|
|
if (uuidChanged || numberChanged) {
|
|
|
|
if (uuidChanged) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.warn(
|
2021-09-27 17:31:34 +00:00
|
|
|
'createAccount: New uuid is different from old uuid; deleting all previous data'
|
2020-04-13 17:37:29 +00:00
|
|
|
);
|
|
|
|
}
|
2021-06-08 18:54:20 +00:00
|
|
|
if (numberChanged) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.warn(
|
2021-09-27 17:31:34 +00:00
|
|
|
'createAccount: New number is different from old number; deleting all previous data'
|
2020-04-13 17:37:29 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2021-09-10 02:38:11 +00:00
|
|
|
await storage.protocol.removeAllData();
|
2021-09-27 17:31:34 +00:00
|
|
|
log.info('createAccount: Successfully deleted previous data');
|
2020-04-13 17:37:29 +00:00
|
|
|
} catch (error) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.error(
|
2020-04-13 17:37:29 +00:00
|
|
|
'Something went wrong deleting data from previous number',
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
);
|
|
|
|
}
|
2021-09-27 17:31:34 +00:00
|
|
|
} else {
|
|
|
|
log.info('createAccount: Erasing configuration (soft)');
|
|
|
|
await storage.protocol.removeAllConfiguration(
|
|
|
|
RemoveAllConfiguration.Soft
|
|
|
|
);
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
|
|
|
|
2021-09-27 17:31:34 +00:00
|
|
|
await senderCertificateService.clear();
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-09-10 02:38:11 +00:00
|
|
|
if (previousUuid) {
|
|
|
|
await Promise.all([
|
|
|
|
storage.put(
|
|
|
|
'identityKeyMap',
|
|
|
|
omit(storage.get('identityKeyMap') || {}, previousUuid)
|
|
|
|
),
|
|
|
|
storage.put(
|
|
|
|
'registrationIdMap',
|
|
|
|
omit(storage.get('registrationIdMap') || {}, previousUuid)
|
|
|
|
),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2021-07-23 17:23:50 +00:00
|
|
|
// `setCredentials` needs to be called
|
2020-04-13 17:37:29 +00:00
|
|
|
// 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.
|
2021-09-10 02:38:11 +00:00
|
|
|
await storage.user.setCredentials({
|
|
|
|
uuid: ourUuid,
|
2020-04-13 17:37:29 +00:00
|
|
|
number,
|
2021-07-23 17:23:50 +00:00
|
|
|
deviceId: response.deviceId ?? 1,
|
|
|
|
deviceName: deviceName ?? undefined,
|
|
|
|
password,
|
|
|
|
});
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-01-15 16:57:09 +00:00
|
|
|
// This needs to be done very early, because it changes how things are saved in the
|
|
|
|
// database. Your identity, for example, in the saveIdentityWithAttributes call
|
|
|
|
// below.
|
|
|
|
const conversationId = window.ConversationController.ensureContactIds({
|
|
|
|
e164: number,
|
2021-09-10 02:38:11 +00:00
|
|
|
uuid: ourUuid,
|
2021-01-15 16:57:09 +00:00
|
|
|
highTrust: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!conversationId) {
|
|
|
|
throw new Error('registrationDone: no conversationId!');
|
|
|
|
}
|
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
// update our own identity key, which may have changed
|
|
|
|
// if we're relinking after a reinstall on the master device
|
2021-09-22 00:58:03 +00:00
|
|
|
await storage.protocol.saveIdentityWithAttributes(new UUID(ourUuid), {
|
2021-09-10 02:38:11 +00:00
|
|
|
publicKey: identityKeyPair.pubKey,
|
|
|
|
firstUse: true,
|
|
|
|
timestamp: Date.now(),
|
|
|
|
verified: storage.protocol.VerifiedStatus.VERIFIED,
|
|
|
|
nonblockingApproval: true,
|
|
|
|
});
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-09-10 02:38:11 +00:00
|
|
|
const identityKeyMap = {
|
|
|
|
...(storage.get('identityKeyMap') || {}),
|
|
|
|
[ourUuid]: {
|
2021-09-24 00:49:05 +00:00
|
|
|
pubKey: Bytes.toBase64(identityKeyPair.pubKey),
|
|
|
|
privKey: Bytes.toBase64(identityKeyPair.privKey),
|
2021-09-10 02:38:11 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
const registrationIdMap = {
|
|
|
|
...(storage.get('registrationIdMap') || {}),
|
|
|
|
[ourUuid]: registrationId,
|
|
|
|
};
|
|
|
|
|
|
|
|
await storage.put('identityKeyMap', identityKeyMap);
|
|
|
|
await storage.put('registrationIdMap', registrationIdMap);
|
2020-04-13 17:37:29 +00:00
|
|
|
if (profileKey) {
|
2021-05-05 16:39:16 +00:00
|
|
|
await ourProfileKeyService.set(profileKey);
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
|
|
|
if (userAgent) {
|
2021-09-10 02:38:11 +00:00
|
|
|
await storage.put('userAgent', userAgent);
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
|
|
|
|
2021-09-10 02:38:11 +00:00
|
|
|
await storage.put('read-receipt-setting', Boolean(readReceipts));
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-11-11 22:43:05 +00:00
|
|
|
const regionCode =
|
|
|
|
window.libphonenumber.util.getRegionCodeForNumber(number);
|
2021-09-10 02:38:11 +00:00
|
|
|
await storage.put('regionCode', regionCode);
|
|
|
|
await storage.protocol.hydrateCaches();
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
async clearSessionsAndPreKeys() {
|
|
|
|
const store = window.textsecure.storage.protocol;
|
|
|
|
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info('clearing all sessions, prekeys, and signed prekeys');
|
2020-04-13 17:37:29 +00:00
|
|
|
await Promise.all([
|
|
|
|
store.clearPreKeyStore(),
|
|
|
|
store.clearSignedPreKeysStore(),
|
|
|
|
store.clearSessionStore(),
|
|
|
|
]);
|
|
|
|
}
|
2020-09-09 02:25:05 +00:00
|
|
|
|
|
|
|
async getGroupCredentials(startDay: number, endDay: number) {
|
|
|
|
return this.server.getGroupCredentials(startDay, endDay);
|
|
|
|
}
|
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
// Takes the same object returned by generateKeys
|
|
|
|
async confirmKeys(keys: GeneratedKeysType) {
|
|
|
|
const store = window.textsecure.storage.protocol;
|
|
|
|
const key = keys.signedPreKey;
|
|
|
|
const confirmed = true;
|
|
|
|
|
|
|
|
if (!key) {
|
|
|
|
throw new Error('confirmKeys: signedPreKey is null');
|
|
|
|
}
|
|
|
|
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info('confirmKeys: confirming key', key.keyId);
|
2021-09-10 02:38:11 +00:00
|
|
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
|
|
|
|
await store.storeSignedPreKey(ourUuid, key.keyId, key.keyPair, confirmed);
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
async generateKeys(count: number, providedProgressCallback?: Function) {
|
|
|
|
const progressCallback =
|
|
|
|
typeof providedProgressCallback === 'function'
|
|
|
|
? providedProgressCallback
|
|
|
|
: null;
|
|
|
|
const startId = window.textsecure.storage.get('maxPreKeyId', 1);
|
|
|
|
const signedKeyId = window.textsecure.storage.get('signedKeyId', 1);
|
2021-09-10 02:38:11 +00:00
|
|
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
|
2020-04-13 17:37:29 +00:00
|
|
|
|
|
|
|
if (typeof startId !== 'number') {
|
|
|
|
throw new Error('Invalid maxPreKeyId');
|
|
|
|
}
|
|
|
|
if (typeof signedKeyId !== 'number') {
|
|
|
|
throw new Error('Invalid signedKeyId');
|
|
|
|
}
|
|
|
|
|
|
|
|
const store = window.textsecure.storage.protocol;
|
2021-09-10 02:38:11 +00:00
|
|
|
return store.getIdentityKeyPair(ourUuid).then(async identityKey => {
|
2021-04-16 23:13:13 +00:00
|
|
|
if (!identityKey) {
|
|
|
|
throw new Error('generateKeys: No identity key pair!');
|
|
|
|
}
|
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
const result: any = {
|
|
|
|
preKeys: [],
|
|
|
|
identityKey: identityKey.pubKey,
|
|
|
|
};
|
|
|
|
const promises = [];
|
|
|
|
|
|
|
|
for (let keyId = startId; keyId < startId + count; keyId += 1) {
|
|
|
|
promises.push(
|
2021-04-16 23:13:13 +00:00
|
|
|
Promise.resolve(generatePreKey(keyId)).then(async res => {
|
2021-09-10 02:38:11 +00:00
|
|
|
await store.storePreKey(ourUuid, res.keyId, res.keyPair);
|
2020-04-13 17:37:29 +00:00
|
|
|
result.preKeys.push({
|
|
|
|
keyId: res.keyId,
|
|
|
|
publicKey: res.keyPair.pubKey,
|
|
|
|
});
|
|
|
|
if (progressCallback) {
|
|
|
|
progressCallback();
|
|
|
|
}
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
promises.push(
|
2021-04-16 23:13:13 +00:00
|
|
|
Promise.resolve(generateSignedPreKey(identityKey, signedKeyId)).then(
|
|
|
|
async res => {
|
2021-09-10 02:38:11 +00:00
|
|
|
await store.storeSignedPreKey(ourUuid, res.keyId, res.keyPair);
|
2021-04-16 23:13:13 +00:00
|
|
|
result.signedPreKey = {
|
|
|
|
keyId: res.keyId,
|
|
|
|
publicKey: res.keyPair.pubKey,
|
|
|
|
signature: res.signature,
|
|
|
|
// server.registerKeys doesn't use keyPair, confirmKeys does
|
|
|
|
keyPair: res.keyPair,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
)
|
2020-04-13 17:37:29 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
promises.push(
|
|
|
|
window.textsecure.storage.put('maxPreKeyId', startId + count)
|
|
|
|
);
|
|
|
|
promises.push(
|
|
|
|
window.textsecure.storage.put('signedKeyId', signedKeyId + 1)
|
|
|
|
);
|
|
|
|
|
|
|
|
return Promise.all(promises).then(async () =>
|
|
|
|
// This is primarily for the signed prekey summary it logs out
|
|
|
|
this.cleanSignedPreKeys().then(() => result as GeneratedKeysType)
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2021-01-15 16:57:09 +00:00
|
|
|
async registrationDone() {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info('registration done');
|
2020-04-13 17:37:29 +00:00
|
|
|
this.dispatchEvent(new Event('registration'));
|
|
|
|
}
|
|
|
|
}
|