Generate PNI key on standalone registration

This commit is contained in:
Fedor Indutny 2021-12-03 03:06:32 +01:00 committed by GitHub
parent 13de35bea2
commit ca1aef660f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 101 additions and 81 deletions

View file

@ -30,7 +30,6 @@ import {
generateSignedPreKey,
generatePreKey,
} from '../Curve';
import type { UUIDStringType } from '../types/UUID';
import { UUID, UUIDKind } from '../types/UUID';
import { isMoreRecentThan, isOlderThan } from '../util/timestamp';
import { ourProfileKeyService } from '../services/ourProfileKey';
@ -73,6 +72,18 @@ export type GeneratedKeysType = {
identityKey: Uint8Array;
};
type CreateAccountOptionsType = Readonly<{
number: string;
verificationCode: string;
identityKeyPair: KeyPairType;
pniKeyPair?: KeyPairType;
profileKey?: Uint8Array;
deviceName?: string;
userAgent?: string;
readReceipts?: boolean;
accessKey?: Uint8Array;
}>;
export default class AccountManager extends EventTarget {
pending: Promise<void>;
@ -156,28 +167,28 @@ export default class AccountManager extends EventTarget {
async registerSingleDevice(number: string, verificationCode: string) {
return this.queueTask(async () => {
const identityKeyPair = generateKeyPair();
const pniKeyPair = generateKeyPair();
const profileKey = getRandomBytes(PROFILE_KEY_LENGTH);
const accessKey = deriveAccessKey(profileKey);
await this.createAccount(
await this.createAccount({
number,
verificationCode,
identityKeyPair,
pniKeyPair,
profileKey,
null,
null,
null,
{ accessKey }
);
accessKey,
});
await this.clearSessionsAndPreKeys();
// TODO: DESKTOP-2788
const keys = await this.generateKeys(
SIGNED_KEY_GEN_BATCH_SIZE,
UUIDKind.ACI
await Promise.all(
[UUIDKind.ACI, UUIDKind.PNI].map(async kind => {
const keys = await this.generateKeys(SIGNED_KEY_GEN_BATCH_SIZE, kind);
await this.server.registerKeys(keys, kind);
await this.confirmKeys(keys, kind);
})
);
await this.server.registerKeys(keys, UUIDKind.ACI);
await this.confirmKeys(keys);
await this.registrationDone();
});
}
@ -187,7 +198,6 @@ export default class AccountManager extends EventTarget {
confirmNumber: (number?: string) => Promise<string>,
progressCallback?: Function
) {
const createAccount = this.createAccount.bind(this);
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
const provisioningCipher = new ProvisioningCipher();
const pubKey = await provisioningCipher.getPublicKey();
@ -266,20 +276,15 @@ export default class AccountManager extends EventTarget {
);
}
await createAccount(
provisionMessage.number,
provisionMessage.provisioningCode,
provisionMessage.identityKeyPair,
provisionMessage.profileKey,
await this.createAccount({
number: provisionMessage.number,
verificationCode: provisionMessage.provisioningCode,
identityKeyPair: provisionMessage.identityKeyPair,
profileKey: provisionMessage.profileKey,
deviceName,
provisionMessage.userAgent,
provisionMessage.readReceipts,
{
uuid: provisionMessage.uuid
? UUID.cast(provisionMessage.uuid)
: undefined,
}
);
userAgent: provisionMessage.userAgent,
readReceipts: provisionMessage.readReceipts,
});
await clearSessionsAndPreKeys();
// TODO: DESKTOP-2794
const keys = await this.generateKeys(
@ -288,7 +293,7 @@ export default class AccountManager extends EventTarget {
progressCallback
);
await this.server.registerKeys(keys, UUIDKind.ACI);
await this.confirmKeys(keys);
await this.confirmKeys(keys, UUIDKind.ACI);
await this.registrationDone();
});
}
@ -454,18 +459,18 @@ export default class AccountManager extends EventTarget {
);
}
async createAccount(
number: string,
verificationCode: string,
identityKeyPair: KeyPairType,
profileKey: Uint8Array | undefined,
deviceName: string | null,
userAgent?: string | null,
readReceipts?: boolean | null,
options: { accessKey?: Uint8Array; uuid?: UUIDStringType } = {}
): Promise<void> {
async createAccount({
number,
verificationCode,
identityKeyPair,
pniKeyPair,
profileKey,
deviceName,
userAgent,
readReceipts,
accessKey,
}: CreateAccountOptionsType): Promise<void> {
const { storage } = window.textsecure;
const { accessKey, uuid } = options;
let password = Bytes.toBase64(getRandomBytes(16));
password = password.substring(0, password.length - 2);
const registrationId = generateRegistrationId();
@ -491,11 +496,11 @@ export default class AccountManager extends EventTarget {
password,
registrationId,
encryptedDeviceName,
{ accessKey, uuid }
{ accessKey }
);
const ourUuid = uuid || response.uuid;
strictAssert(ourUuid !== undefined, 'Should have UUID after registration');
const ourUuid = UUID.cast(response.uuid);
const ourPni = UUID.cast(response.pni);
const uuidChanged = previousUuid && ourUuid && previousUuid !== ourUuid;
@ -554,7 +559,7 @@ export default class AccountManager extends EventTarget {
// information.
await storage.user.setCredentials({
uuid: ourUuid,
pni: response.pni,
pni: ourPni,
number,
deviceId: response.deviceId ?? 1,
deviceName: deviceName ?? undefined,
@ -574,15 +579,27 @@ export default class AccountManager extends EventTarget {
throw new Error('registrationDone: no conversationId!');
}
// update our own identity key, which may have changed
// if we're relinking after a reinstall on the master device
await storage.protocol.saveIdentityWithAttributes(new UUID(ourUuid), {
publicKey: identityKeyPair.pubKey,
const identityAttrs = {
firstUse: true,
timestamp: Date.now(),
verified: storage.protocol.VerifiedStatus.VERIFIED,
nonblockingApproval: true,
});
};
// update our own identity key, which may have changed
// if we're relinking after a reinstall on the master device
await Promise.all([
storage.protocol.saveIdentityWithAttributes(new UUID(ourUuid), {
...identityAttrs,
publicKey: identityKeyPair.pubKey,
}),
pniKeyPair
? storage.protocol.saveIdentityWithAttributes(new UUID(ourPni), {
...identityAttrs,
publicKey: pniKeyPair.pubKey,
})
: Promise.resolve(),
]);
const identityKeyMap = {
...(storage.get('identityKeyMap') || {}),
@ -590,10 +607,20 @@ export default class AccountManager extends EventTarget {
pubKey: Bytes.toBase64(identityKeyPair.pubKey),
privKey: Bytes.toBase64(identityKeyPair.privKey),
},
...(pniKeyPair
? {
[ourPni]: {
pubKey: Bytes.toBase64(pniKeyPair.pubKey),
privKey: Bytes.toBase64(pniKeyPair.privKey),
},
}
: {}),
};
const registrationIdMap = {
...(storage.get('registrationIdMap') || {}),
[ourUuid]: registrationId,
// TODO: DESKTOP-2825
[ourPni]: registrationId,
};
await storage.put('identityKeyMap', identityKeyMap);
@ -633,7 +660,7 @@ export default class AccountManager extends EventTarget {
}
// Takes the same object returned by generateKeys
async confirmKeys(keys: GeneratedKeysType) {
async confirmKeys(keys: GeneratedKeysType, uuidKind: UUIDKind) {
const store = window.textsecure.storage.protocol;
const key = keys.signedPreKey;
const confirmed = true;
@ -642,8 +669,11 @@ export default class AccountManager extends EventTarget {
throw new Error('confirmKeys: signedPreKey is null');
}
log.info('confirmKeys: confirming key', key.keyId);
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
log.info(
`AccountManager.confirmKeys(${uuidKind}): confirming key`,
key.keyId
);
const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind);
await store.storeSignedPreKey(ourUuid, key.keyId, key.keyPair, confirmed);
}

View file

@ -2036,7 +2036,7 @@ export default class MessageSender {
// Simple pass-throughs
async getProfile(
number: string,
uuid: UUID,
options: Readonly<{
accessKey?: string;
profileKeyVersion: string;
@ -2051,10 +2051,10 @@ export default class MessageSender {
...options,
accessKey,
};
return this.server.getProfileUnauth(number, unauthOptions);
return this.server.getProfileUnauth(uuid.toString(), unauthOptions);
}
return this.server.getProfile(number, options);
return this.server.getProfile(uuid.toString(), options);
}
async checkAccountExistence(uuid: UUID): Promise<boolean> {

View file

@ -765,7 +765,7 @@ export type WebAPIType = {
newPassword: string,
registrationId: number,
deviceName?: string | null,
options?: { accessKey?: Uint8Array; uuid?: UUIDStringType }
options?: { accessKey?: Uint8Array }
) => Promise<ConfirmCodeResultType>;
createGroup: (
group: Proto.IGroup,
@ -1634,7 +1634,7 @@ export function initialize({
newPassword: string,
registrationId: number,
deviceName?: string | null,
options: { accessKey?: Uint8Array; uuid?: UUIDStringType } = {}
options: { accessKey?: Uint8Array } = {}
) {
const capabilities: CapabilitiesUploadType = {
announcementGroup: true,
@ -1644,7 +1644,7 @@ export function initialize({
changeNumber: true,
};
const { accessKey, uuid } = options;
const { accessKey } = options;
const jsonData = {
capabilities,
fetchesMessages: true,
@ -1678,7 +1678,7 @@ export function initialize({
})) as ConfirmCodeResultType;
// Set final REST credentials to let `registerKeys` succeed.
username = `${uuid || response.uuid || number}.${response.deviceId || 1}`;
username = `${response.uuid || number}.${response.deviceId || 1}`;
password = newPassword;
return response;

View file

@ -5,7 +5,6 @@ import type { ProfileKeyCredentialRequestContext } from '@signalapp/signal-clien
import { SEALED_SENDER } from '../types/SealedSender';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import * as Bytes from '../Bytes';
import { trimForDisplay, verifyAccessKey, decryptProfile } from '../Crypto';
import {
@ -62,13 +61,11 @@ export async function getProfile(
]);
const profileKey = c.get('profileKey');
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const uuid = c.get('uuid')!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const identifier = c.getSendTarget()!;
const targetUuid = UUID.checkedLookup(identifier);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const profileKeyVersionHex = c.get('profileKeyVersion')!;
const uuid = c.getCheckedUuid('getProfile');
const profileKeyVersionHex = c.get('profileKeyVersion');
if (!profileKeyVersionHex) {
throw new Error('No profile key version available');
}
const existingProfileKeyCredential = c.get('profileKeyCredential');
let profileKeyCredentialRequestHex: undefined | string;
@ -76,31 +73,24 @@ export async function getProfile(
| undefined
| ProfileKeyCredentialRequestContext;
if (
profileKey &&
uuid &&
profileKeyVersionHex &&
!existingProfileKeyCredential
) {
if (profileKey && profileKeyVersionHex && !existingProfileKeyCredential) {
log.info('Generating request...');
({
requestHex: profileKeyCredentialRequestHex,
context: profileCredentialRequestContext,
} = generateProfileKeyCredentialRequest(
clientZkProfileCipher,
uuid,
uuid.toString(),
profileKey
));
}
const { sendMetadata = {} } = await getSendOptions(c.attributes);
const getInfo =
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
sendMetadata[c.get('uuid')!] || sendMetadata[c.get('e164')!] || {};
const getInfo = sendMetadata[uuid.toString()] || {};
if (getInfo.accessKey) {
try {
profile = await window.textsecure.messaging.getProfile(identifier, {
profile = await window.textsecure.messaging.getProfile(uuid, {
accessKey: getInfo.accessKey,
profileKeyVersion: profileKeyVersionHex,
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
@ -112,7 +102,7 @@ export async function getProfile(
`Setting sealedSender to DISABLED for conversation ${c.idForLogging()}`
);
c.set({ sealedSender: SEALED_SENDER.DISABLED });
profile = await window.textsecure.messaging.getProfile(identifier, {
profile = await window.textsecure.messaging.getProfile(uuid, {
profileKeyVersion: profileKeyVersionHex,
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
userLanguages,
@ -122,7 +112,7 @@ export async function getProfile(
}
}
} else {
profile = await window.textsecure.messaging.getProfile(identifier, {
profile = await window.textsecure.messaging.getProfile(uuid, {
profileKeyVersion: profileKeyVersionHex,
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
userLanguages,
@ -132,7 +122,7 @@ export async function getProfile(
if (profile.identityKey) {
const identityKey = Bytes.fromBase64(profile.identityKey);
const changed = await window.textsecure.storage.protocol.saveIdentity(
new Address(targetUuid, 1),
new Address(uuid, 1),
identityKey,
false
);
@ -141,7 +131,7 @@ export async function getProfile(
// must close that one manually.
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
await window.textsecure.storage.protocol.archiveSession(
new QualifiedAddress(ourUuid, new Address(targetUuid, 1))
new QualifiedAddress(ourUuid, new Address(uuid, 1))
);
}
}