diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 3e4a1f35cf30..5fbab42cebb1 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -32,6 +32,7 @@ message Envelope { optional bytes content = 8; // Contains an encrypted Content optional string serverGuid = 9; optional uint64 serverTimestamp = 10; + optional string destinationUuid = 13; } message Content { diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index f832696d0559..d8e1df2173f9 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -206,6 +206,7 @@ export type UnprocessedType = { source?: string; sourceUuid?: string; sourceDevice?: number; + destinationUuid?: string; serverGuid?: string; serverTimestamp?: number; decrypted?: string; diff --git a/ts/textsecure.d.ts b/ts/textsecure.d.ts index b717256ec351..0433441b0964 100644 --- a/ts/textsecure.d.ts +++ b/ts/textsecure.d.ts @@ -29,6 +29,7 @@ export type UnprocessedType = { source?: string; sourceDevice?: number; sourceUuid?: string; + destinationUuid?: string; messageAgeSec?: number; version: number; }; diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index 67bdf001384d..0248d8748cbf 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -49,7 +49,8 @@ import { deriveMasterKeyFromGroupV1 } from '../Crypto'; import type { DownloadedAttachmentType } from '../types/Attachment'; import { Address } from '../types/Address'; import { QualifiedAddress } from '../types/QualifiedAddress'; -import { UUID } from '../types/UUID'; +import type { UUIDStringType } from '../types/UUID'; +import { UUID, UUIDKind } from '../types/UUID'; import * as Errors from '../types/errors'; import { SignalService as Proto } from '../protobuf'; @@ -264,6 +265,8 @@ export default class MessageReceiver const decoded = Proto.Envelope.decode(plaintext); const serverTimestamp = normalizeNumber(decoded.serverTimestamp); + const ourUuid = this.storage.user.getCheckedUuid(); + const envelope: ProcessedEnvelope = { // Make non-private envelope IDs dashless so they don't get redacted // from logs @@ -283,6 +286,14 @@ export default class MessageReceiver ) : undefined, sourceDevice: decoded.sourceDevice, + destinationUuid: decoded.destinationUuid + ? new UUID( + normalizeUuid( + decoded.destinationUuid, + 'MessageReceiver.handleRequest.destinationUuid' + ) + ) + : ourUuid, timestamp: normalizeNumber(decoded.timestamp), legacyMessage: dropNull(decoded.legacyMessage), content: dropNull(decoded.content), @@ -604,6 +615,8 @@ export default class MessageReceiver const decoded = Proto.Envelope.decode(envelopePlaintext); + const ourUuid = this.storage.user.getCheckedUuid(); + const envelope: ProcessedEnvelope = { id: item.id, receivedAtCounter: item.timestamp, @@ -615,6 +628,9 @@ export default class MessageReceiver source: decoded.source || item.source, sourceUuid: decoded.sourceUuid || item.sourceUuid, sourceDevice: decoded.sourceDevice || item.sourceDevice, + destinationUuid: new UUID( + decoded.destinationUuid || item.destinationUuid || ourUuid.toString() + ), timestamp: normalizeNumber(decoded.timestamp), legacyMessage: dropNull(decoded.legacyMessage), content: dropNull(decoded.content), @@ -668,12 +684,16 @@ export default class MessageReceiver private getEnvelopeId(envelope: ProcessedEnvelope): string { const { timestamp } = envelope; + let prefix = ''; + if (envelope.sourceUuid || envelope.source) { const sender = envelope.sourceUuid || envelope.source; - return `${sender}.${envelope.sourceDevice} ${timestamp} (${envelope.id})`; + prefix += `${sender}.${envelope.sourceDevice} `; } - return `${timestamp} (${envelope.id})`; + prefix += `> ${envelope.destinationUuid.toString()}`; + + return `${prefix}${timestamp} (${envelope.id})`; } private clearRetryTimeout(): void { @@ -737,9 +757,8 @@ export default class MessageReceiver pendingSessions: true, pendingUnprocessed: true, }); - const ourUuid = this.storage.user.getCheckedUuid(); - const sessionStore = new Sessions({ zone, ourUuid }); - const identityKeyStore = new IdentityKeys({ zone, ourUuid }); + + const storesMap = new Map(); const failed: Array = []; // Below we: @@ -755,9 +774,38 @@ export default class MessageReceiver await Promise.all( 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 ${this.getEnvelopeId(envelope)}, ` + + `unknown uuid: ${destinationUuid}` + ); + return; + } + + let stores = storesMap.get(destinationUuid.toString()); + if (!stores) { + stores = { + sessionStore: new Sessions({ + zone, + ourUuid: destinationUuid, + }), + identityKeyStore: new IdentityKeys({ + zone, + ourUuid: destinationUuid, + }), + zone, + }; + storesMap.set(destinationUuid.toString(), stores); + } + const result = await this.queueEncryptedEnvelope( - { sessionStore, identityKeyStore, zone }, - envelope + stores, + envelope, + uuidKind ); if (result.plaintext) { decrypted.push({ @@ -769,7 +817,8 @@ export default class MessageReceiver } catch (error) { failed.push(data); log.error( - 'decryptAndCache error when processing the envelope', + 'MessageReceiver.decryptAndCacheBatch error when ' + + 'processing the envelope', Errors.toLogFormat(error) ); } @@ -791,6 +840,7 @@ export default class MessageReceiver source: envelope.source, sourceUuid: envelope.sourceUuid, sourceDevice: envelope.sourceDevice, + destinationUuid: envelope.destinationUuid.toString(), serverGuid: envelope.serverGuid, serverTimestamp: envelope.serverTimestamp, decrypted: Bytes.toBase64(plaintext), @@ -901,17 +951,27 @@ export default class MessageReceiver private async queueEncryptedEnvelope( stores: LockedStores, - envelope: ProcessedEnvelope + envelope: ProcessedEnvelope, + uuidKind: UUIDKind ): Promise { let logId = this.getEnvelopeId(envelope); - log.info('queueing envelope', logId); + log.info(`queueing ${uuidKind} envelope`, logId); const task = createTaskWithTimeout(async (): Promise => { - const unsealedEnvelope = await this.unsealEnvelope(stores, envelope); + const unsealedEnvelope = await this.unsealEnvelope( + stores, + envelope, + uuidKind + ); + + // Dropped early + if (!unsealedEnvelope) { + return { plaintext: undefined, envelope }; + } logId = this.getEnvelopeId(unsealedEnvelope); - return this.decryptEnvelope(stores, unsealedEnvelope); + return this.decryptEnvelope(stores, unsealedEnvelope, uuidKind); }, `MessageReceiver: unseal and decrypt ${logId}`); try { @@ -976,8 +1036,9 @@ export default class MessageReceiver private async unsealEnvelope( stores: LockedStores, - envelope: ProcessedEnvelope - ): Promise { + envelope: ProcessedEnvelope, + uuidKind: UUIDKind + ): Promise { const logId = this.getEnvelopeId(envelope); if (this.stoppingProcessing) { @@ -989,6 +1050,11 @@ export default class MessageReceiver return envelope; } + if (uuidKind === UUIDKind.PNI) { + log.warn(`MessageReceiver.unsealEnvelope(${logId}): dropping for PNI`); + return undefined; + } + const ciphertext = envelope.content || envelope.legacyMessage; if (!ciphertext) { this.removeFromCache(envelope); @@ -1036,7 +1102,8 @@ export default class MessageReceiver private async decryptEnvelope( stores: LockedStores, - envelope: UnsealedEnvelope + envelope: UnsealedEnvelope, + uuidKind: UUIDKind ): Promise { const logId = this.getEnvelopeId(envelope); @@ -1068,7 +1135,12 @@ export default class MessageReceiver log.info( `MessageReceiver.decryptEnvelope(${logId})${isLegacy ? ' (legacy)' : ''}` ); - const plaintext = await this.decrypt(stores, envelope, ciphertext); + const plaintext = await this.decrypt( + stores, + envelope, + ciphertext, + uuidKind + ); if (!plaintext) { log.warn('MessageReceiver.decryptEnvelope: plaintext was falsey'); @@ -1206,7 +1278,7 @@ export default class MessageReceiver ciphertext: Uint8Array ): Promise { const localE164 = this.storage.user.getNumber(); - const ourUuid = this.storage.user.getCheckedUuid(); + const { destinationUuid } = envelope; const localDeviceId = parseIntOrThrow( this.storage.user.getDeviceId(), 'MessageReceiver.decryptSealedSender: localDeviceId' @@ -1252,10 +1324,10 @@ export default class MessageReceiver ); const sealedSenderIdentifier = certificate.senderUuid(); const sealedSenderSourceDevice = certificate.senderDeviceId(); - const senderKeyStore = new SenderKeys({ ourUuid }); + const senderKeyStore = new SenderKeys({ ourUuid: destinationUuid }); const address = new QualifiedAddress( - ourUuid, + destinationUuid, Address.create(sealedSenderIdentifier, sealedSenderSourceDevice) ); @@ -1280,8 +1352,8 @@ export default class MessageReceiver 'unidentified message/passing to sealedSenderDecryptMessage' ); - const preKeyStore = new PreKeys({ ourUuid }); - const signedPreKeyStore = new SignedPreKeys({ ourUuid }); + const preKeyStore = new PreKeys({ ourUuid: destinationUuid }); + const signedPreKeyStore = new SignedPreKeys({ ourUuid: destinationUuid }); const sealedSenderIdentifier = envelope.sourceUuid; strictAssert( @@ -1293,7 +1365,7 @@ export default class MessageReceiver 'Empty sealed sender device' ); const address = new QualifiedAddress( - ourUuid, + destinationUuid, Address.create(sealedSenderIdentifier, envelope.sourceDevice) ); const unsealedPlaintext = await this.storage.protocol.enqueueSessionJob( @@ -1304,7 +1376,7 @@ export default class MessageReceiver PublicKey.deserialize(Buffer.from(this.serverTrustRoot)), envelope.serverTimestamp, localE164 || null, - ourUuid.toString(), + destinationUuid.toString(), localDeviceId, sessionStore, identityKeyStore, @@ -1320,8 +1392,9 @@ export default class MessageReceiver private async innerDecrypt( stores: LockedStores, envelope: ProcessedEnvelope, - ciphertext: Uint8Array - ): Promise { + ciphertext: Uint8Array, + uuidKind: UUIDKind + ): Promise { const { sessionStore, identityKeyStore, zone } = stores; const logId = this.getEnvelopeId(envelope); @@ -1330,18 +1403,29 @@ export default class MessageReceiver const identifier = envelope.sourceUuid; const { sourceDevice } = envelope; - const ourUuid = this.storage.user.getCheckedUuid(); - const preKeyStore = new PreKeys({ ourUuid }); - const signedPreKeyStore = new SignedPreKeys({ ourUuid }); + const { destinationUuid } = envelope; + const preKeyStore = new PreKeys({ ourUuid: destinationUuid }); + const signedPreKeyStore = new SignedPreKeys({ ourUuid: destinationUuid }); strictAssert(identifier !== undefined, 'Empty identifier'); strictAssert(sourceDevice !== undefined, 'Empty source device'); const address = new QualifiedAddress( - ourUuid, + destinationUuid, Address.create(identifier, sourceDevice) ); + if ( + uuidKind === UUIDKind.PNI && + envelope.type !== envelopeTypeEnum.PREKEY_BUNDLE + ) { + log.warn( + `MessageReceiver.innerDecrypt(${logId}): ` + + 'non-PreKey envelope on PNI' + ); + return undefined; + } + if (envelope.type === envelopeTypeEnum.PLAINTEXT_CONTENT) { log.info(`decrypt/${logId}: plaintext message`); const buffer = Buffer.from(ciphertext); @@ -1445,10 +1529,11 @@ export default class MessageReceiver private async decrypt( stores: LockedStores, envelope: UnsealedEnvelope, - ciphertext: Uint8Array + ciphertext: Uint8Array, + uuidKind: UUIDKind ): Promise { try { - return await this.innerDecrypt(stores, envelope, ciphertext); + return await this.innerDecrypt(stores, envelope, ciphertext, uuidKind); } catch (error) { const uuid = envelope.sourceUuid; const deviceId = envelope.sourceDevice; @@ -1860,10 +1945,10 @@ export default class MessageReceiver SenderKeyDistributionMessage.deserialize( Buffer.from(distributionMessage) ); - const ourUuid = this.storage.user.getCheckedUuid(); - const senderKeyStore = new SenderKeys({ ourUuid }); + const { destinationUuid } = envelope; + const senderKeyStore = new SenderKeys({ ourUuid: destinationUuid }); const address = new QualifiedAddress( - ourUuid, + destinationUuid, Address.create(identifier, sourceDevice) ); diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index 8393c458f456..1d06e9f40982 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -3,6 +3,7 @@ import type { SignalService as Proto } from '../protobuf'; import type { IncomingWebSocketRequest } from './WebsocketResources'; +import type { UUID } from '../types/UUID'; export { IdentityKeyType, @@ -82,6 +83,7 @@ export type ProcessedEnvelope = Readonly<{ source?: string; sourceUuid?: string; sourceDevice?: number; + destinationUuid: UUID; timestamp: number; legacyMessage?: Uint8Array; content?: Uint8Array; diff --git a/ts/textsecure/storage/User.ts b/ts/textsecure/storage/User.ts index 6f490a9e65d8..b24dc1ccf37a 100644 --- a/ts/textsecure/storage/User.ts +++ b/ts/textsecure/storage/User.ts @@ -5,7 +5,7 @@ import type { WebAPICredentials } from '../Types.d'; import { strictAssert } from '../../util/assert'; import type { StorageInterface } from '../../types/Storage.d'; -import { UUID } from '../../types/UUID'; +import { UUID, UUIDKind } from '../../types/UUID'; import * as log from '../../logging/log'; import Helpers from '../Helpers'; @@ -70,6 +70,27 @@ export class User { return uuid; } + public getPni(): UUID | undefined { + const pni = this.storage.get('pni'); + if (pni === undefined) return undefined; + return new UUID(pni); + } + + public getOurUuidKind(uuid: UUID): UUIDKind { + const ourUuid = this.getUuid(); + + if (ourUuid?.toString() === uuid.toString()) { + return UUIDKind.ACI; + } + + const pni = this.getPni(); + if (pni?.toString() === uuid.toString()) { + return UUIDKind.PNI; + } + + return UUIDKind.Unknown; + } + public getDeviceId(): number | undefined { const value = this._getDeviceIdFromUuid() || this._getDeviceIdFromNumber(); if (value === undefined) { diff --git a/ts/types/Storage.d.ts b/ts/types/Storage.d.ts index bd49042d99fc..a3c1a7a1c792 100644 --- a/ts/types/Storage.d.ts +++ b/ts/types/Storage.d.ts @@ -84,6 +84,7 @@ export type StorageAccessType = { synced_at: number; userAgent: string; uuid_id: string; + pni: string; version: string; linkPreviews: boolean; universalExpireTimer: number; diff --git a/ts/types/UUID.ts b/ts/types/UUID.ts index b9aed0a3caf9..2060a3aa91ce 100644 --- a/ts/types/UUID.ts +++ b/ts/types/UUID.ts @@ -8,6 +8,12 @@ import { strictAssert } from '../util/assert'; export type UUIDStringType = `${string}-${string}-${string}-${string}-${string}`; +export enum UUIDKind { + ACI = 'ACI', + PNI = 'PNI', + Unknown = 'Unknown', +} + export const isValidUuid = (value: unknown): value is UUIDStringType => typeof value === 'string' && /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(