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

View file

@ -2036,7 +2036,7 @@ export default class MessageSender {
// Simple pass-throughs // Simple pass-throughs
async getProfile( async getProfile(
number: string, uuid: UUID,
options: Readonly<{ options: Readonly<{
accessKey?: string; accessKey?: string;
profileKeyVersion: string; profileKeyVersion: string;
@ -2051,10 +2051,10 @@ export default class MessageSender {
...options, ...options,
accessKey, 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> { async checkAccountExistence(uuid: UUID): Promise<boolean> {

View file

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

View file

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