Preliminary support for destinationUuid field

This commit is contained in:
Fedor Indutny 2021-11-12 22:26:52 +01:00 committed by GitHub
parent bb15cfc622
commit 066a23a6a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 154 additions and 36 deletions

View file

@ -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 {

View file

@ -206,6 +206,7 @@ export type UnprocessedType = {
source?: string;
sourceUuid?: string;
sourceDevice?: number;
destinationUuid?: string;
serverGuid?: string;
serverTimestamp?: number;
decrypted?: string;

1
ts/textsecure.d.ts vendored
View file

@ -29,6 +29,7 @@ export type UnprocessedType = {
source?: string;
sourceDevice?: number;
sourceUuid?: string;
destinationUuid?: string;
messageAgeSec?: number;
version: number;
};

View file

@ -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<UUIDStringType, LockedStores>();
const failed: Array<UnprocessedType> = [];
// Below we:
@ -755,9 +774,38 @@ export default class MessageReceiver
await Promise.all<void>(
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<DecryptResult> {
let logId = this.getEnvelopeId(envelope);
log.info('queueing envelope', logId);
log.info(`queueing ${uuidKind} envelope`, logId);
const task = createTaskWithTimeout(async (): Promise<DecryptResult> => {
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<UnsealedEnvelope> {
envelope: ProcessedEnvelope,
uuidKind: UUIDKind
): Promise<UnsealedEnvelope | undefined> {
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<DecryptResult> {
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<DecryptSealedSenderResult> {
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<Uint8Array> {
ciphertext: Uint8Array,
uuidKind: UUIDKind
): Promise<Uint8Array | undefined> {
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<Uint8Array | undefined> {
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)
);

View file

@ -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;

View file

@ -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) {

View file

@ -84,6 +84,7 @@ export type StorageAccessType = {
synced_at: number;
userAgent: string;
uuid_id: string;
pni: string;
version: string;
linkPreviews: boolean;
universalExpireTimer: number;

View file

@ -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(