Handle PniChangeNumber

This commit is contained in:
Fedor Indutny 2022-07-28 09:35:29 -07:00 committed by Josh Perez
parent 412f07d2a2
commit 79b48115e6
32 changed files with 1086 additions and 485 deletions

View file

@ -7,7 +7,7 @@ import { omit } from 'lodash';
import EventTarget from './EventTarget';
import type { WebAPIType } from './WebAPI';
import { HTTPError } from './Errors';
import type { KeyPairType } from './Types.d';
import type { KeyPairType, PniKeyMaterialType } from './Types.d';
import ProvisioningCipher from './ProvisioningCipher';
import type { IncomingWebSocketRequest } from './WebsocketResources';
import createTaskWithTimeout from './TaskWithTimeout';
@ -332,6 +332,7 @@ export default class AccountManager extends EventTarget {
}
const keys = await this.generateKeys(SIGNED_KEY_GEN_BATCH_SIZE, uuidKind);
await this.server.registerKeys(keys, uuidKind);
await this.confirmKeys(keys, uuidKind);
});
}
@ -649,16 +650,10 @@ export default class AccountManager extends EventTarget {
const identityKeyMap = {
...(storage.get('identityKeyMap') || {}),
[ourUuid]: {
pubKey: Bytes.toBase64(aciKeyPair.pubKey),
privKey: Bytes.toBase64(aciKeyPair.privKey),
},
[ourUuid]: aciKeyPair,
...(pniKeyPair
? {
[ourPni]: {
pubKey: Bytes.toBase64(pniKeyPair.pubKey),
privKey: Bytes.toBase64(pniKeyPair.privKey),
},
[ourPni]: pniKeyPair,
}
: {}),
};
@ -702,30 +697,18 @@ export default class AccountManager extends EventTarget {
log.info('AccountManager.updatePNIIdentity: generating new keys');
return this.queueTask(async () => {
const keys = await this.generateKeys(
SIGNED_KEY_GEN_BATCH_SIZE,
UUIDKind.PNI,
identityKeyPair
);
await this.server.registerKeys(keys, UUIDKind.PNI);
await this.confirmKeys(keys, UUIDKind.PNI);
await this.queueTask(async () => {
// Server has accepted our keys which means we have the latest PNI identity
// now that doesn't conflict the PNI identity of the primary device.
log.info(
'AccountManager.updatePNIIdentity: updating identity key ' +
'and registration id'
);
const { pubKey, privKey } = identityKeyPair;
const pni = storage.user.getCheckedUuid(UUIDKind.PNI);
const identityKeyMap = {
...(storage.get('identityKeyMap') || {}),
[pni.toString()]: {
pubKey: Bytes.toBase64(pubKey),
privKey: Bytes.toBase64(privKey),
},
[pni.toString()]: identityKeyPair,
};
const aci = storage.user.getCheckedUuid(UUIDKind.ACI);
@ -744,6 +727,26 @@ export default class AccountManager extends EventTarget {
await storage.protocol.hydrateCaches();
});
// Intentionally not awaiting becase `updatePNIIdentity` runs on an
// Encrypted queue of MessageReceiver and we don't want to await remote
// endpoints and block message processing.
this.queueTask(async () => {
try {
const keys = await this.generateKeys(
SIGNED_KEY_GEN_BATCH_SIZE,
UUIDKind.PNI,
identityKeyPair
);
await this.server.registerKeys(keys, UUIDKind.PNI);
await this.confirmKeys(keys, UUIDKind.PNI);
} catch (error) {
log.error(
'updatePNIIdentity: Failed to upload PNI prekeys. Moving on',
Errors.toLogFormat(error)
);
}
});
}
// Takes the same object returned by generateKeys
@ -841,30 +844,50 @@ export default class AccountManager extends EventTarget {
this.dispatchEvent(new Event('registration'));
}
async setPni(pni: string): Promise<void> {
async setPni(pni: string, keyMaterial?: PniKeyMaterialType): Promise<void> {
const { storage } = window.textsecure;
const oldPni = storage.user.getUuid(UUIDKind.PNI)?.toString();
if (oldPni === pni) {
if (oldPni === pni && !keyMaterial) {
return;
}
log.info(`AccountManager.setPni(${pni}): updating from ${oldPni}`);
if (oldPni) {
await Promise.all([
storage.put(
'identityKeyMap',
omit(storage.get('identityKeyMap') || {}, oldPni)
),
storage.put(
'registrationIdMap',
omit(storage.get('registrationIdMap') || {}, oldPni)
),
]);
await storage.protocol.removeOurOldPni(new UUID(oldPni));
}
log.info(`AccountManager.setPni: updating pni from ${oldPni} to ${pni}`);
await storage.user.setPni(pni);
await storage.protocol.hydrateCaches();
if (keyMaterial) {
await storage.protocol.updateOurPniKeyMaterial(
new UUID(pni),
keyMaterial
);
// Intentionally not awaiting since this is processed on encrypted queue
// of MessageReceiver.
this.queueTask(async () => {
try {
const keys = await this.generateKeys(
SIGNED_KEY_GEN_BATCH_SIZE,
UUIDKind.PNI
);
await this.server.registerKeys(keys, UUIDKind.PNI);
await this.confirmKeys(keys, UUIDKind.PNI);
} catch (error) {
log.error(
'setPni: Failed to upload PNI prekeys. Moving on',
Errors.toLogFormat(error)
);
}
});
// PNI has changed and credentials are no longer valid
await storage.put('groupCredentials', []);
} else {
log.warn(`AccountManager.setPni(${pni}): no key material`);
}
}
}

View file

@ -101,7 +101,6 @@ import {
MessageRequestResponseEvent,
FetchLatestEvent,
KeysEvent,
PNIIdentityEvent,
StickerPackEvent,
ReadSyncEvent,
ViewSyncEvent,
@ -256,8 +255,6 @@ export default class MessageReceiver
private stoppingProcessing?: boolean;
private pendingPNIIdentityEvent?: PNIIdentityEvent;
constructor({ server, storage, serverTrustRoot }: MessageReceiverOptions) {
super();
@ -376,6 +373,14 @@ export default class MessageReceiver
)
)
: ourUuid,
updatedPni: decoded.updatedPni
? new UUID(
normalizeUuid(
decoded.updatedPni,
'MessageReceiver.handleRequest.updatedPni'
)
)
: undefined,
timestamp: decoded.timestamp?.toNumber(),
content: dropNull(decoded.content),
serverGuid: decoded.serverGuid,
@ -535,11 +540,6 @@ export default class MessageReceiver
handler: (ev: KeysEvent) => void
): void;
public override addEventListener(
name: 'pniIdentity',
handler: (ev: PNIIdentityEvent) => void
): void;
public override addEventListener(
name: 'sticker-pack',
handler: (ev: StickerPackEvent) => void
@ -671,13 +671,6 @@ export default class MessageReceiver
this.isEmptied = true;
this.maybeScheduleRetryTimeout();
// Emit PNI identity event after processing the queue
const { pendingPNIIdentityEvent } = this;
this.pendingPNIIdentityEvent = undefined;
if (pendingPNIIdentityEvent) {
await this.dispatchAndWait(pendingPNIIdentityEvent);
}
};
const waitForDecryptedQueue = async () => {
@ -772,6 +765,9 @@ export default class MessageReceiver
destinationUuid: new UUID(
decoded.destinationUuid || item.destinationUuid || ourUuid.toString()
),
updatedPni: decoded.updatedPni
? new UUID(decoded.updatedPni)
: undefined,
timestamp: decoded.timestamp?.toNumber(),
content: dropNull(decoded.content),
serverGuid: decoded.serverGuid,
@ -902,16 +898,6 @@ export default class MessageReceiver
items.map(async ({ data, envelope }) => {
try {
const { destinationUuid } = envelope;
const uuidKind =
this.storage.user.getOurUuidKind(destinationUuid);
if (uuidKind === UUIDKind.Unknown) {
log.warn(
'MessageReceiver.decryptAndCacheBatch: ' +
`Rejecting envelope ${getEnvelopeId(envelope)}, ` +
`unknown uuid: ${destinationUuid}`
);
return;
}
let stores = storesMap.get(destinationUuid.toString());
if (!stores) {
@ -935,8 +921,7 @@ export default class MessageReceiver
const result = await this.queueEncryptedEnvelope(
stores,
envelope,
uuidKind
envelope
);
if (result.plaintext) {
decrypted.push({
@ -972,6 +957,7 @@ export default class MessageReceiver
sourceUuid: envelope.sourceUuid,
sourceDevice: envelope.sourceDevice,
destinationUuid: envelope.destinationUuid.toString(),
updatedPni: envelope.updatedPni?.toString(),
serverGuid: envelope.serverGuid,
serverTimestamp: envelope.serverTimestamp,
decrypted: Bytes.toBase64(plaintext),
@ -1089,13 +1075,23 @@ export default class MessageReceiver
private async queueEncryptedEnvelope(
stores: LockedStores,
envelope: ProcessedEnvelope,
uuidKind: UUIDKind
envelope: ProcessedEnvelope
): Promise<DecryptResult> {
let logId = getEnvelopeId(envelope);
log.info(`queueing ${uuidKind} envelope`, logId);
log.info('queueing envelope', logId);
const task = async (): Promise<DecryptResult> => {
const { destinationUuid } = envelope;
const uuidKind = this.storage.user.getOurUuidKind(destinationUuid);
if (uuidKind === UUIDKind.Unknown) {
log.warn(
'MessageReceiver.decryptAndCacheBatch: ' +
`Rejecting envelope ${getEnvelopeId(envelope)}, ` +
`unknown uuid: ${destinationUuid}`
);
return { plaintext: undefined, envelope };
}
const unsealedEnvelope = await this.unsealEnvelope(
stores,
envelope,
@ -1311,6 +1307,19 @@ export default class MessageReceiver
content.senderKeyDistributionMessage
);
}
// Some sync messages have to be fully processed in the middle of
// decryption queue since subsequent envelopes use their key material.
const { syncMessage } = content;
if (syncMessage?.pniIdentity) {
await this.handlePNIIdentity(envelope, syncMessage.pniIdentity);
return { plaintext: undefined, envelope };
}
if (syncMessage?.pniChangeNumber) {
await this.handlePNIChangeNumber(envelope, syncMessage.pniChangeNumber);
return { plaintext: undefined, envelope };
}
} catch (error) {
log.error(
'MessageReceiver.decryptEnvelope: Failed to process sender ' +
@ -2575,6 +2584,9 @@ export default class MessageReceiver
if (envelope.sourceDevice == ourDeviceId) {
throw new Error('Received sync message from our own device');
}
if (syncMessage.pniIdentity) {
return;
}
if (syncMessage.sent) {
const sentMessage = syncMessage.sent;
@ -2615,7 +2627,7 @@ export default class MessageReceiver
if (this.isInvalidGroupData(sentMessage.message, envelope)) {
this.removeFromCache(envelope);
return undefined;
return;
}
await this.checkGroupV1Data(sentMessage.message);
@ -2633,11 +2645,11 @@ export default class MessageReceiver
}
if (syncMessage.contacts) {
this.handleContacts(envelope, syncMessage.contacts);
return undefined;
return;
}
if (syncMessage.groups) {
this.handleGroups(envelope, syncMessage.groups);
return undefined;
return;
}
if (syncMessage.blocked) {
return this.handleBlocked(envelope, syncMessage.blocked);
@ -2645,7 +2657,7 @@ export default class MessageReceiver
if (syncMessage.request) {
log.info('Got SyncMessage Request');
this.removeFromCache(envelope);
return undefined;
return;
}
if (syncMessage.read && syncMessage.read.length) {
return this.handleRead(envelope, syncMessage.read);
@ -2653,7 +2665,7 @@ export default class MessageReceiver
if (syncMessage.verified) {
log.info('Got verified sync message, dropping');
this.removeFromCache(envelope);
return undefined;
return;
}
if (syncMessage.configuration) {
return this.handleConfiguration(envelope, syncMessage.configuration);
@ -2682,9 +2694,6 @@ export default class MessageReceiver
if (syncMessage.keys) {
return this.handleKeys(envelope, syncMessage.keys);
}
if (syncMessage.pniIdentity) {
return this.handlePNIIdentity(envelope, syncMessage.pniIdentity);
}
if (syncMessage.viewed && syncMessage.viewed.length) {
return this.handleViewed(envelope, syncMessage.viewed);
}
@ -2693,7 +2702,6 @@ export default class MessageReceiver
log.warn(
`handleSyncMessage/${getEnvelopeId(envelope)}: Got empty SyncMessage`
);
return Promise.resolve();
}
private async handleConfiguration(
@ -2813,6 +2821,7 @@ export default class MessageReceiver
return this.dispatchAndWait(ev);
}
// Runs on TaskType.Encrypted queue
private async handlePNIIdentity(
envelope: ProcessedEnvelope,
{ publicKey, privateKey }: Proto.SyncMessage.IPniIdentity
@ -2823,22 +2832,47 @@ export default class MessageReceiver
if (!publicKey || !privateKey) {
log.warn('MessageReceiver: empty pni identity sync message');
return undefined;
return;
}
const ev = new PNIIdentityEvent(
{ publicKey, privateKey },
this.removeFromCache.bind(this, envelope)
);
const manager = window.getAccountManager();
await manager.updatePNIIdentity({ privKey: privateKey, pubKey: publicKey });
}
if (this.isEmptied) {
log.info('MessageReceiver: emitting pni identity sync message');
return this.dispatchAndWait(ev);
// Runs on TaskType.Encrypted queue
private async handlePNIChangeNumber(
envelope: ProcessedEnvelope,
{
identityKeyPair,
signedPreKey,
registrationId,
}: Proto.SyncMessage.IPniChangeNumber
): Promise<void> {
log.info('MessageReceiver: got pni change number sync message');
logUnexpectedUrgentValue(envelope, 'pniIdentitySync');
const { updatedPni } = envelope;
if (!updatedPni) {
log.warn('MessageReceiver: missing pni in change number sync message');
return;
}
log.info('MessageReceiver: scheduling pni identity sync message');
this.pendingPNIIdentityEvent?.confirm();
this.pendingPNIIdentityEvent = ev;
if (
!Bytes.isNotEmpty(identityKeyPair) ||
!Bytes.isNotEmpty(signedPreKey) ||
!isNumber(registrationId)
) {
log.warn('MessageReceiver: empty pni change number sync message');
return;
}
const manager = window.getAccountManager();
await manager.setPni(updatedPni.toString(), {
identityKeyPair,
signedPreKey,
registrationId,
});
}
private async handleStickerPackOperation(

View file

@ -87,6 +87,7 @@ export type ProcessedEnvelope = Readonly<{
sourceUuid?: UUIDStringType;
sourceDevice?: number;
destinationUuid: UUID;
updatedPni?: UUID;
timestamp: number;
content?: Uint8Array;
serverGuid: string;
@ -270,3 +271,9 @@ export interface CallbackResultType {
export interface IRequestHandler {
handleRequest(request: IncomingWebSocketRequest): void;
}
export type PniKeyMaterialType = Readonly<{
identityKeyPair: Uint8Array;
signedPreKey: Uint8Array;
registrationId: number;
}>;

View file

@ -566,7 +566,6 @@ type DirectoryV3OptionsType = Readonly<{
directoryVersion: 3;
directoryV3Url: string;
directoryV3MRENCLAVE: string;
directoryV3Root: string;
}>;
type OptionalDirectoryFieldsType = {
@ -578,7 +577,6 @@ type OptionalDirectoryFieldsType = {
directoryV2CodeHashes?: unknown;
directoryV3Url?: unknown;
directoryV3MRENCLAVE?: unknown;
directoryV3Root?: unknown;
};
type DirectoryOptionsType = OptionalDirectoryFieldsType &
@ -803,6 +801,11 @@ export type GetGroupCredentialsOptionsType = Readonly<{
endDayInMs: number;
}>;
export type GetGroupCredentialsResultType = Readonly<{
pni?: string | null;
credentials: ReadonlyArray<GroupCredentialType>;
}>;
export type WebAPIType = {
startRegistration(): unknown;
finishRegistration(baton: unknown): void;
@ -831,7 +834,7 @@ export type WebAPIType = {
getGroupAvatar: (key: string) => Promise<Uint8Array>;
getGroupCredentials: (
options: GetGroupCredentialsOptionsType
) => Promise<Array<GroupCredentialType>>;
) => Promise<GetGroupCredentialsResultType>;
getGroupExternalCredential: (
options: GroupCredentialsType
) => Promise<Proto.GroupExternalCredential>;
@ -1205,8 +1208,7 @@ export function initialize({
},
});
} else if (directoryConfig.directoryVersion === 3) {
const { directoryV3Url, directoryV3MRENCLAVE, directoryV3Root } =
directoryConfig;
const { directoryV3Url, directoryV3MRENCLAVE } = directoryConfig;
cds = new CDSI({
logger: log,
@ -1214,7 +1216,6 @@ export function initialize({
url: directoryV3Url,
mrenclave: directoryV3MRENCLAVE,
root: directoryV3Root,
certificateAuthority,
version,
@ -2510,7 +2511,7 @@ export function initialize({
async function getGroupCredentials({
startDayInMs,
endDayInMs,
}: GetGroupCredentialsOptionsType): Promise<Array<GroupCredentialType>> {
}: GetGroupCredentialsOptionsType): Promise<GetGroupCredentialsResultType> {
const startDayInSeconds = startDayInMs / durations.SECOND;
const endDayInSeconds = endDayInMs / durations.SECOND;
const response = (await _ajax({
@ -2522,7 +2523,7 @@ export function initialize({
responseType: 'json',
})) as CredentialResponseType;
return response.credentials;
return response;
}
async function getGroupExternalCredential(

View file

@ -10,20 +10,16 @@ import { CDSSocketManagerBase } from './CDSSocketManagerBase';
export type CDSIOptionsType = Readonly<{
mrenclave: string;
root: string;
}> &
CDSSocketManagerBaseOptionsType;
export class CDSI extends CDSSocketManagerBase<CDSISocket, CDSIOptionsType> {
private readonly mrenclave: Buffer;
private readonly trustedCaCert: Buffer;
constructor(options: CDSIOptionsType) {
super(options);
this.mrenclave = Buffer.from(Bytes.fromHex(options.mrenclave));
this.trustedCaCert = Buffer.from(options.root);
}
protected override getSocketUrl(): string {
@ -37,7 +33,6 @@ export class CDSI extends CDSSocketManagerBase<CDSISocket, CDSIOptionsType> {
logger: this.logger,
socket,
mrenclave: this.mrenclave,
trustedCaCert: this.trustedCaCert,
});
}
}

View file

@ -4,14 +4,12 @@
import { Cds2Client } from '@signalapp/libsignal-client';
import { strictAssert } from '../../util/assert';
import { DAY } from '../../util/durations';
import { SignalService as Proto } from '../../protobuf';
import { CDSSocketBase, CDSSocketState } from './CDSSocketBase';
import type { CDSSocketBaseOptionsType } from './CDSSocketBase';
export type CDSISocketOptionsType = Readonly<{
mrenclave: Buffer;
trustedCaCert: Buffer;
}> &
CDSSocketBaseOptionsType;
@ -30,15 +28,14 @@ export class CDSISocket extends CDSSocketBase<CDSISocketOptionsType> {
await this.socketIterator.next();
strictAssert(!done, 'CDSI socket closed before handshake');
const earliestValidTimestamp = new Date(Date.now() - DAY);
const earliestValidTimestamp = new Date();
strictAssert(
this.privCdsClient === undefined,
'CDSI handshake called twice'
);
this.privCdsClient = Cds2Client.new_NOT_FOR_PRODUCTION(
this.privCdsClient = Cds2Client.new(
this.options.mrenclave,
this.options.trustedCaCert,
attestationMessage,
earliestValidTimestamp
);

View file

@ -357,20 +357,6 @@ export class KeysEvent extends ConfirmableEvent {
}
}
export type PNIIdentityEventData = Readonly<{
publicKey: Uint8Array;
privateKey: Uint8Array;
}>;
export class PNIIdentityEvent extends ConfirmableEvent {
constructor(
public readonly data: PNIIdentityEventData,
confirm: ConfirmCallback
) {
super('pniIdentity', confirm);
}
}
export type StickerPackEventData = Readonly<{
id?: string;
key?: string;