Refactor provisioning flow

This commit is contained in:
Fedor Indutny 2024-08-29 17:02:48 -07:00 committed by GitHub
parent a80da0ad24
commit b2c3b1f43e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 373 additions and 225 deletions

View file

@ -21,9 +21,6 @@ import type {
KyberPreKeyType,
PniKeyMaterialType,
} from './Types.d';
import ProvisioningCipher from './ProvisioningCipher';
import type { IncomingWebSocketRequest } from './WebsocketResources';
import { ServerRequestType } from './WebsocketResources';
import createTaskWithTimeout from './TaskWithTimeout';
import * as Bytes from '../Bytes';
import * as Errors from '../types/errors';
@ -46,7 +43,6 @@ import {
import type { AciString, PniString, ServiceIdString } from '../types/ServiceId';
import {
isUntaggedPniString,
normalizePni,
ServiceIdKind,
toTaggedPni,
} from '../types/ServiceId';
@ -61,7 +57,6 @@ import { missingCaseError } from '../util/missingCaseError';
import { SignalService as Proto } from '../protobuf';
import * as log from '../logging/log';
import type { StorageAccessType } from '../types/Storage';
import { linkDeviceRoute } from '../util/signalRoutes';
import { getRelativePath, createName } from '../util/attachmentPath';
import { isBackupEnabled } from '../util/isBackupEnabled';
@ -116,7 +111,7 @@ const SIGNED_PRE_KEY_UPDATE_TIME_KEY: StorageKeyByServiceIdKind = {
[ServiceIdKind.PNI]: 'signedKeyUpdateTimePNI',
};
enum AccountType {
export enum AccountType {
Primary = 'Primary',
Linked = 'Linked',
}
@ -146,7 +141,7 @@ type CreatePrimaryDeviceOptionsType = Readonly<{
}> &
CreateAccountSharedOptionsType;
type CreateLinkedDeviceOptionsType = Readonly<{
export type CreateLinkedDeviceOptionsType = Readonly<{
type: AccountType.Linked;
deviceName: string;
@ -327,146 +322,26 @@ export default class AccountManager extends EventTarget {
const accessKey = deriveAccessKey(profileKey);
const masterKey = getRandomBytes(MASTER_KEY_LENGTH);
const registrationBaton = this.server.startRegistration();
try {
await this.createAccount({
type: AccountType.Primary,
number,
verificationCode,
sessionId,
aciKeyPair,
pniKeyPair,
profileKey,
accessKey,
masterKey,
readReceipts: true,
});
} finally {
this.server.finishRegistration(registrationBaton);
}
await this.registrationDone();
await this.createAccount({
type: AccountType.Primary,
number,
verificationCode,
sessionId,
aciKeyPair,
pniKeyPair,
profileKey,
accessKey,
masterKey,
readReceipts: true,
});
});
}
async registerSecondDevice(
setProvisioningUrl: (url: string) => void,
confirmNumber: (number?: string) => Promise<ConfirmNumberResultType>
options: CreateLinkedDeviceOptionsType
): Promise<void> {
const provisioningCipher = new ProvisioningCipher();
const pubKey = await provisioningCipher.getPublicKey();
let envelopeCallbacks:
| {
resolve(data: Proto.ProvisionEnvelope): void;
reject(error: Error): void;
}
| undefined;
const envelopePromise = new Promise<Proto.ProvisionEnvelope>(
(resolve, reject) => {
envelopeCallbacks = { resolve, reject };
}
);
const wsr = await this.server.getProvisioningResource({
handleRequest(request: IncomingWebSocketRequest) {
if (
request.requestType === ServerRequestType.ProvisioningAddress &&
request.body
) {
const proto = Proto.ProvisioningUuid.decode(request.body);
const { uuid } = proto;
if (!uuid) {
throw new Error('registerSecondDevice: expected a UUID');
}
const url = linkDeviceRoute
.toAppUrl({
uuid,
pubKey: Bytes.toBase64(pubKey),
})
.toString();
window.SignalCI?.setProvisioningURL(url);
setProvisioningUrl(url);
request.respond(200, 'OK');
} else if (
request.requestType === ServerRequestType.ProvisioningMessage &&
request.body
) {
const envelope = Proto.ProvisionEnvelope.decode(request.body);
request.respond(200, 'OK');
wsr.close();
envelopeCallbacks?.resolve(envelope);
} else {
log.error('Unknown websocket message', request.requestType);
}
},
});
log.info('provisioning socket open');
wsr.addEventListener('close', ({ code, reason }) => {
log.info(`provisioning socket closed. Code: ${code} Reason: ${reason}`);
// 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, backupFile } = 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.aciKeyPair ||
!provisionMessage.pniKeyPair ||
!provisionMessage.aci ||
!Bytes.isNotEmpty(provisionMessage.profileKey) ||
!Bytes.isNotEmpty(provisionMessage.masterKey) ||
!isUntaggedPniString(provisionMessage.untaggedPni)
) {
throw new Error(
'AccountManager.registerSecondDevice: Provision message was missing key data'
);
}
const ourAci = normalizeAci(provisionMessage.aci, 'provisionMessage.aci');
const ourPni = normalizePni(
toTaggedPni(provisionMessage.untaggedPni),
'provisionMessage.pni'
);
const registrationBaton = this.server.startRegistration();
try {
await this.createAccount({
type: AccountType.Linked,
number: provisionMessage.number,
verificationCode: provisionMessage.provisioningCode,
aciKeyPair: provisionMessage.aciKeyPair,
pniKeyPair: provisionMessage.pniKeyPair,
profileKey: provisionMessage.profileKey,
deviceName,
backupFile,
userAgent: provisionMessage.userAgent,
ourAci,
ourPni,
readReceipts: Boolean(provisionMessage.readReceipts),
masterKey: provisionMessage.masterKey,
});
} finally {
this.server.finishRegistration(registrationBaton);
}
await this.registrationDone();
await this.createAccount(options);
});
}
@ -1021,6 +896,18 @@ export default class AccountManager extends EventTarget {
private async createAccount(
options: CreateAccountOptionsType
): Promise<void> {
const registrationBaton = this.server.startRegistration();
try {
await this.doCreateAccount(options);
} finally {
this.server.finishRegistration(registrationBaton);
}
await this.registrationDone();
}
private async doCreateAccount(
options: CreateAccountOptionsType
): Promise<void> {
const {
number,