Request and handle PniIdentity sync message

This commit is contained in:
Fedor Indutny 2022-03-25 10:36:08 -07:00 committed by GitHub
parent 5a107e1bc3
commit a0ae7c1aa2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 245 additions and 74 deletions

View file

@ -407,6 +407,7 @@ message SyncMessage {
BLOCKED = 3; BLOCKED = 3;
CONFIGURATION = 4; CONFIGURATION = 4;
KEYS = 5; KEYS = 5;
PNI_IDENTITY = 6;
} }
optional Type type = 1; optional Type type = 1;
@ -416,6 +417,11 @@ message SyncMessage {
optional bytes storageService = 1; optional bytes storageService = 1;
} }
message PniIdentity {
optional bytes publicKey = 1;
optional bytes privateKey = 2;
}
message Read { message Read {
optional string sender = 1; optional string sender = 1;
optional string senderUuid = 3; optional string senderUuid = 3;
@ -496,6 +502,7 @@ message SyncMessage {
optional MessageRequestResponse messageRequestResponse = 14; optional MessageRequestResponse messageRequestResponse = 14;
reserved 15; // not yet added reserved 15; // not yet added
repeated Viewed viewed = 16; repeated Viewed viewed = 16;
optional PniIdentity pniIdentity = 17;
} }
message AttachmentPointer { message AttachmentPointer {

View file

@ -68,6 +68,7 @@ import type {
FetchLatestEvent, FetchLatestEvent,
GroupEvent, GroupEvent,
KeysEvent, KeysEvent,
PNIIdentityEvent,
MessageEvent, MessageEvent,
MessageEventData, MessageEventData,
MessageRequestResponseEvent, MessageRequestResponseEvent,
@ -369,6 +370,10 @@ export async function startApp(): Promise<void> {
queuedEventListener(onFetchLatestSync) queuedEventListener(onFetchLatestSync)
); );
messageReceiver.addEventListener('keys', queuedEventListener(onKeysSync)); messageReceiver.addEventListener('keys', queuedEventListener(onKeysSync));
messageReceiver.addEventListener(
'pniIdentity',
queuedEventListener(onPNIIdentitySync)
);
}); });
ourProfileKeyService.initialize(window.storage); ourProfileKeyService.initialize(window.storage);
@ -2259,6 +2264,8 @@ export async function startApp(): Promise<void> {
window.waitForEmptyEventQueue = waitForEmptyEventQueue; window.waitForEmptyEventQueue = waitForEmptyEventQueue;
async function onEmpty() { async function onEmpty() {
const { storage, messaging } = window.textsecure;
await Promise.all([ await Promise.all([
window.waitForAllBatchers(), window.waitForAllBatchers(),
window.flushAllWaitBatchers(), window.flushAllWaitBatchers(),
@ -2332,7 +2339,7 @@ export async function startApp(): Promise<void> {
} }
}); });
await window.Signal.Data.saveMessages(messagesToSave, { await window.Signal.Data.saveMessages(messagesToSave, {
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), ourUuid: storage.user.getCheckedUuid().toString(),
}); });
// Process crash reports if any // Process crash reports if any
@ -2348,7 +2355,7 @@ export async function startApp(): Promise<void> {
routineProfileRefresh({ routineProfileRefresh({
allConversations: window.ConversationController.getAll(), allConversations: window.ConversationController.getAll(),
ourConversationId, ourConversationId,
storage: window.storage, storage,
}); });
} else { } else {
assert( assert(
@ -2356,6 +2363,17 @@ export async function startApp(): Promise<void> {
'Failed to fetch our conversation ID. Skipping routine profile refresh' 'Failed to fetch our conversation ID. Skipping routine profile refresh'
); );
} }
// Make sure we have the PNI identity
const pni = storage.user.getCheckedUuid(UUIDKind.PNI);
const pniIdentity = await storage.protocol.getIdentityKeyPair(pni);
if (!pniIdentity) {
log.info('Requesting PNI identity sync');
await singleProtoJobQueue.add(
messaging.getRequestPniIdentitySyncMessage()
);
}
} }
let initialStartupCount = 0; let initialStartupCount = 0;
@ -3486,6 +3504,15 @@ export async function startApp(): Promise<void> {
} }
} }
async function onPNIIdentitySync(ev: PNIIdentityEvent) {
ev.confirm();
log.info('onPNIIdentitySync: updating PNI keys');
const manager = window.getAccountManager();
const { privateKey: privKey, publicKey: pubKey } = ev.data;
await manager.updatePNIIdentity({ privKey, pubKey });
}
async function onMessageRequestResponse(ev: MessageRequestResponseEvent) { async function onMessageRequestResponse(ev: MessageRequestResponseEvent) {
ev.confirm(); ev.confirm();

View file

@ -1,15 +1,11 @@
// Copyright 2020-2021 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* 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'; import PQueue from 'p-queue';
import { omit } from 'lodash'; import { omit } from 'lodash';
import EventTarget from './EventTarget'; import EventTarget from './EventTarget';
import type { WebAPIType } from './WebAPI'; import type { WebAPIType, GroupCredentialType } from './WebAPI';
import { HTTPError } from './Errors'; import { HTTPError } from './Errors';
import type { KeyPairType } from './Types.d'; import type { KeyPairType } from './Types.d';
import ProvisioningCipher from './ProvisioningCipher'; import ProvisioningCipher from './ProvisioningCipher';
@ -96,15 +92,15 @@ export default class AccountManager extends EventTarget {
this.pending = Promise.resolve(); this.pending = Promise.resolve();
} }
async requestVoiceVerification(number: string, token: string) { async requestVoiceVerification(number: string, token: string): Promise<void> {
return this.server.requestVerificationVoice(number, token); return this.server.requestVerificationVoice(number, token);
} }
async requestSMSVerification(number: string, token: string) { async requestSMSVerification(number: string, token: string): Promise<void> {
return this.server.requestVerificationSMS(number, token); return this.server.requestVerificationSMS(number, token);
} }
encryptDeviceName(name: string, identityKey: KeyPairType) { encryptDeviceName(name: string, identityKey: KeyPairType): string | null {
if (!name) { if (!name) {
return null; return null;
} }
@ -119,7 +115,7 @@ export default class AccountManager extends EventTarget {
return Bytes.toBase64(bytes); return Bytes.toBase64(bytes);
} }
async decryptDeviceName(base64: string) { async decryptDeviceName(base64: string): Promise<string> {
const ourUuid = window.textsecure.storage.user.getCheckedUuid(); const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const identityKey = const identityKey =
await window.textsecure.storage.protocol.getIdentityKeyPair(ourUuid); await window.textsecure.storage.protocol.getIdentityKeyPair(ourUuid);
@ -139,7 +135,7 @@ export default class AccountManager extends EventTarget {
return name; return name;
} }
async maybeUpdateDeviceName() { async maybeUpdateDeviceName(): Promise<void> {
const isNameEncrypted = const isNameEncrypted =
window.textsecure.storage.user.getDeviceNameEncrypted(); window.textsecure.storage.user.getDeviceNameEncrypted();
if (isNameEncrypted) { if (isNameEncrypted) {
@ -161,11 +157,14 @@ export default class AccountManager extends EventTarget {
} }
} }
async deviceNameIsEncrypted() { async deviceNameIsEncrypted(): Promise<void> {
await window.textsecure.storage.user.setDeviceNameEncrypted(); await window.textsecure.storage.user.setDeviceNameEncrypted();
} }
async registerSingleDevice(number: string, verificationCode: string) { async registerSingleDevice(
number: string,
verificationCode: string
): Promise<void> {
return this.queueTask(async () => { return this.queueTask(async () => {
const aciKeyPair = generateKeyPair(); const aciKeyPair = generateKeyPair();
const pniKeyPair = generateKeyPair(); const pniKeyPair = generateKeyPair();
@ -203,9 +202,9 @@ export default class AccountManager extends EventTarget {
} }
async registerSecondDevice( async registerSecondDevice(
setProvisioningUrl: (url: string) => unknown, setProvisioningUrl: (url: string) => void,
confirmNumber: (number?: string) => Promise<string> confirmNumber: (number?: string) => Promise<string>
) { ): Promise<void> {
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();
@ -323,7 +322,7 @@ export default class AccountManager extends EventTarget {
}); });
} }
async refreshPreKeys(uuidKind: UUIDKind) { async refreshPreKeys(uuidKind: UUIDKind): Promise<void> {
return this.queueTask(async () => { return this.queueTask(async () => {
const preKeyCount = await this.server.getMyKeys(uuidKind); const preKeyCount = await this.server.getMyKeys(uuidKind);
log.info(`prekey count ${preKeyCount}`); log.info(`prekey count ${preKeyCount}`);
@ -335,7 +334,7 @@ export default class AccountManager extends EventTarget {
}); });
} }
async rotateSignedPreKey(uuidKind: UUIDKind) { async rotateSignedPreKey(uuidKind: UUIDKind): Promise<void> {
return this.queueTask(async () => { return this.queueTask(async () => {
const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind); const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind);
const signedKeyId = window.textsecure.storage.get('signedKeyId', 1); const signedKeyId = window.textsecure.storage.get('signedKeyId', 1);
@ -447,14 +446,14 @@ export default class AccountManager extends EventTarget {
}); });
} }
async queueTask(task: () => Promise<any>) { async queueTask<T>(task: () => Promise<T>): Promise<T> {
this.pendingQueue = this.pendingQueue || new PQueue({ concurrency: 1 }); this.pendingQueue = this.pendingQueue || new PQueue({ concurrency: 1 });
const taskWithTimeout = createTaskWithTimeout(task, 'AccountManager task'); const taskWithTimeout = createTaskWithTimeout(task, 'AccountManager task');
return this.pendingQueue.add(taskWithTimeout); return this.pendingQueue.add(taskWithTimeout);
} }
async cleanSignedPreKeys() { async cleanSignedPreKeys(): Promise<void> {
const ourUuid = window.textsecure.storage.user.getCheckedUuid(); const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const store = window.textsecure.storage.protocol; const store = window.textsecure.storage.protocol;
@ -662,7 +661,8 @@ export default class AccountManager extends EventTarget {
const registrationIdMap = { const registrationIdMap = {
...(storage.get('registrationIdMap') || {}), ...(storage.get('registrationIdMap') || {}),
[ourUuid]: registrationId, [ourUuid]: registrationId,
// TODO: DESKTOP-2825
// TODO: DESKTOP-3318
[ourPni]: registrationId, [ourPni]: registrationId,
}; };
@ -683,7 +683,7 @@ export default class AccountManager extends EventTarget {
await storage.protocol.hydrateCaches(); await storage.protocol.hydrateCaches();
} }
async clearSessionsAndPreKeys() { async clearSessionsAndPreKeys(): Promise<void> {
const store = window.textsecure.storage.protocol; const store = window.textsecure.storage.protocol;
log.info('clearing all sessions, prekeys, and signed prekeys'); log.info('clearing all sessions, prekeys, and signed prekeys');
@ -694,16 +694,68 @@ export default class AccountManager extends EventTarget {
]); ]);
} }
async updatePNIIdentity(identityKeyPair: KeyPairType): Promise<void> {
const { storage } = window.textsecure;
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);
// 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),
},
};
const aci = storage.user.getCheckedUuid(UUIDKind.ACI);
const oldRegistrationIdMap = storage.get('registrationIdMap') || {};
const registrationIdMap = {
...oldRegistrationIdMap,
// TODO: DESKTOP-3318
[pni.toString()]: oldRegistrationIdMap[aci.toString()],
};
await Promise.all([
storage.put('identityKeyMap', identityKeyMap),
storage.put('registrationIdMap', registrationIdMap),
]);
await storage.protocol.hydrateCaches();
});
}
async getGroupCredentials( async getGroupCredentials(
startDay: number, startDay: number,
endDay: number, endDay: number,
uuidKind: UUIDKind uuidKind: UUIDKind
) { ): Promise<Array<GroupCredentialType>> {
return this.server.getGroupCredentials(startDay, endDay, uuidKind); return this.server.getGroupCredentials(startDay, endDay, uuidKind);
} }
// Takes the same object returned by generateKeys // Takes the same object returned by generateKeys
async confirmKeys(keys: GeneratedKeysType, uuidKind: UUIDKind) { async confirmKeys(
keys: GeneratedKeysType,
uuidKind: UUIDKind
): Promise<void> {
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;
@ -720,10 +772,16 @@ export default class AccountManager extends EventTarget {
await store.storeSignedPreKey(ourUuid, key.keyId, key.keyPair, confirmed); await store.storeSignedPreKey(ourUuid, key.keyId, key.keyPair, confirmed);
} }
async generateKeys(count: number, uuidKind: UUIDKind) { async generateKeys(
const startId = window.textsecure.storage.get('maxPreKeyId', 1); count: number,
const signedKeyId = window.textsecure.storage.get('signedKeyId', 1); uuidKind: UUIDKind,
const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind); maybeIdentityKey?: KeyPairType
): Promise<GeneratedKeysType> {
const { storage } = window.textsecure;
const startId = storage.get('maxPreKeyId', 1);
const signedKeyId = storage.get('signedKeyId', 1);
const ourUuid = storage.user.getCheckedUuid(uuidKind);
if (typeof startId !== 'number') { if (typeof startId !== 'number') {
throw new Error('Invalid maxPreKeyId'); throw new Error('Invalid maxPreKeyId');
@ -732,60 +790,58 @@ export default class AccountManager extends EventTarget {
throw new Error('Invalid signedKeyId'); throw new Error('Invalid signedKeyId');
} }
const store = window.textsecure.storage.protocol; const store = storage.protocol;
return store.getIdentityKeyPair(ourUuid).then(async identityKey => { const identityKey =
if (!identityKey) { maybeIdentityKey ?? (await store.getIdentityKeyPair(ourUuid));
throw new Error('generateKeys: No identity key pair!'); strictAssert(identityKey, 'generateKeys: No identity key pair!');
}
const result: any = { const result: Omit<GeneratedKeysType, 'signedPreKey'> = {
preKeys: [], preKeys: [],
identityKey: identityKey.pubKey, identityKey: identityKey.pubKey,
};
const promises = [];
for (let keyId = startId; keyId < startId + count; keyId += 1) {
promises.push(
(async () => {
const res = generatePreKey(keyId);
await store.storePreKey(ourUuid, res.keyId, res.keyPair);
result.preKeys.push({
keyId: res.keyId,
publicKey: res.keyPair.pubKey,
});
})()
);
}
const signedPreKey = (async () => {
const res = generateSignedPreKey(identityKey, signedKeyId);
await store.storeSignedPreKey(ourUuid, res.keyId, res.keyPair);
return {
keyId: res.keyId,
publicKey: res.keyPair.pubKey,
signature: res.signature,
// server.registerKeys doesn't use keyPair, confirmKeys does
keyPair: res.keyPair,
}; };
const promises = []; })();
for (let keyId = startId; keyId < startId + count; keyId += 1) { promises.push(signedPreKey);
promises.push( promises.push(storage.put('maxPreKeyId', startId + count));
Promise.resolve(generatePreKey(keyId)).then(async res => { promises.push(storage.put('signedKeyId', signedKeyId + 1));
await store.storePreKey(ourUuid, res.keyId, res.keyPair);
result.preKeys.push({
keyId: res.keyId,
publicKey: res.keyPair.pubKey,
});
})
);
}
promises.push( await Promise.all(promises);
Promise.resolve(generateSignedPreKey(identityKey, signedKeyId)).then(
async res => {
await store.storeSignedPreKey(ourUuid, res.keyId, res.keyPair);
result.signedPreKey = {
keyId: res.keyId,
publicKey: res.keyPair.pubKey,
signature: res.signature,
// server.registerKeys doesn't use keyPair, confirmKeys does
keyPair: res.keyPair,
};
}
)
);
promises.push( // This is primarily for the signed prekey summary it logs out
window.textsecure.storage.put('maxPreKeyId', startId + count) this.cleanSignedPreKeys();
);
promises.push(
window.textsecure.storage.put('signedKeyId', signedKeyId + 1)
);
return Promise.all(promises).then(async () => return {
// This is primarily for the signed prekey summary it logs out ...result,
this.cleanSignedPreKeys().then(() => result as GeneratedKeysType) signedPreKey: await signedPreKey,
); };
});
} }
async registrationDone() { async registrationDone(): Promise<void> {
log.info('registration done'); log.info('registration done');
this.dispatchEvent(new Event('registration')); this.dispatchEvent(new Event('registration'));
} }

View file

@ -98,6 +98,7 @@ import {
MessageRequestResponseEvent, MessageRequestResponseEvent,
FetchLatestEvent, FetchLatestEvent,
KeysEvent, KeysEvent,
PNIIdentityEvent,
StickerPackEvent, StickerPackEvent,
VerifiedEvent, VerifiedEvent,
ReadSyncEvent, ReadSyncEvent,
@ -193,6 +194,8 @@ export default class MessageReceiver
private stoppingProcessing?: boolean; private stoppingProcessing?: boolean;
private pendingPNIIdentityEvent?: PNIIdentityEvent;
constructor({ server, storage, serverTrustRoot }: MessageReceiverOptions) { constructor({ server, storage, serverTrustRoot }: MessageReceiverOptions) {
super(); super();
@ -467,6 +470,11 @@ export default class MessageReceiver
handler: (ev: KeysEvent) => void handler: (ev: KeysEvent) => void
): void; ): void;
public override addEventListener(
name: 'pniIdentity',
handler: (ev: PNIIdentityEvent) => void
): void;
public override addEventListener( public override addEventListener(
name: 'sticker-pack', name: 'sticker-pack',
handler: (ev: StickerPackEvent) => void handler: (ev: StickerPackEvent) => void
@ -598,6 +606,13 @@ export default class MessageReceiver
this.isEmptied = true; this.isEmptied = true;
this.maybeScheduleRetryTimeout(); 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 () => { const waitForDecryptedQueue = async () => {
@ -2474,6 +2489,9 @@ export default class MessageReceiver
if (syncMessage.keys) { if (syncMessage.keys) {
return this.handleKeys(envelope, syncMessage.keys); return this.handleKeys(envelope, syncMessage.keys);
} }
if (syncMessage.pniIdentity) {
return this.handlePNIIdentity(envelope, syncMessage.pniIdentity);
}
if (syncMessage.viewed && syncMessage.viewed.length) { if (syncMessage.viewed && syncMessage.viewed.length) {
return this.handleViewed(envelope, syncMessage.viewed); return this.handleViewed(envelope, syncMessage.viewed);
} }
@ -2591,6 +2609,31 @@ export default class MessageReceiver
return this.dispatchAndWait(ev); return this.dispatchAndWait(ev);
} }
private async handlePNIIdentity(
envelope: ProcessedEnvelope,
{ publicKey, privateKey }: Proto.SyncMessage.IPniIdentity
): Promise<void> {
log.info('MessageReceiver: got pni identity sync message');
if (!publicKey || !privateKey) {
log.warn('MessageReceiver: empty pni identity sync message');
return undefined;
}
const ev = new PNIIdentityEvent(
{ publicKey, privateKey },
this.removeFromCache.bind(this, envelope)
);
if (this.isEmptied) {
log.info('MessageReceiver: emitting pni identity sync message');
return this.dispatchAndWait(ev);
}
log.info('MessageReceiver: scheduling pni identity sync message');
this.pendingPNIIdentityEvent = ev;
}
private async handleStickerPackOperation( private async handleStickerPackOperation(
envelope: ProcessedEnvelope, envelope: ProcessedEnvelope,
operations: Array<Proto.SyncMessage.IStickerPackOperation> operations: Array<Proto.SyncMessage.IStickerPackOperation>

View file

@ -1266,6 +1266,29 @@ export default class MessageSender {
}; };
} }
getRequestPniIdentitySyncMessage(): SingleProtoJobData {
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const request = new Proto.SyncMessage.Request();
request.type = Proto.SyncMessage.Request.Type.PNI_IDENTITY;
const syncMessage = this.createSyncMessage();
syncMessage.request = request;
const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage;
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return {
contentHint: ContentHint.RESENDABLE,
identifier: myUuid.toString(),
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
),
type: 'pniIdentitySyncRequest',
};
}
getFetchManifestSyncMessage(): SingleProtoJobData { getFetchManifestSyncMessage(): SingleProtoJobData {
const myUuid = window.textsecure.storage.user.getCheckedUuid(); const myUuid = window.textsecure.storage.user.getCheckedUuid();

View file

@ -349,6 +349,20 @@ 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<{ export type StickerPackEventData = Readonly<{
id?: string; id?: string;
key?: string; key?: string;

View file

@ -18,6 +18,7 @@ const { insertSentProto, updateConversation } = dataInterface;
export const sendTypesEnum = z.enum([ export const sendTypesEnum = z.enum([
'blockSyncRequest', 'blockSyncRequest',
'pniIdentitySyncRequest',
'callingMessage', // excluded from send log 'callingMessage', // excluded from send log
'configurationSyncRequest', 'configurationSyncRequest',
'contactSyncRequest', 'contactSyncRequest',