MessageReceiver: Unwrap envelopes earlier in the processing chain
This commit is contained in:
parent
eceae64d41
commit
85004699f5
1 changed files with 275 additions and 339 deletions
|
@ -22,6 +22,7 @@ import {
|
||||||
SealedSenderDecryptionResult,
|
SealedSenderDecryptionResult,
|
||||||
sealedSenderDecryptMessage,
|
sealedSenderDecryptMessage,
|
||||||
sealedSenderDecryptToUsmc,
|
sealedSenderDecryptToUsmc,
|
||||||
|
SenderCertificate,
|
||||||
SenderKeyDistributionMessage,
|
SenderKeyDistributionMessage,
|
||||||
signalDecrypt,
|
signalDecrypt,
|
||||||
signalDecryptPreKey,
|
signalDecryptPreKey,
|
||||||
|
@ -38,7 +39,7 @@ import {
|
||||||
} from '../LibSignalStores';
|
} from '../LibSignalStores';
|
||||||
import { verifySignature } from '../Curve';
|
import { verifySignature } from '../Curve';
|
||||||
import { BackOff, FIBONACCI_TIMEOUTS } from '../util/BackOff';
|
import { BackOff, FIBONACCI_TIMEOUTS } from '../util/BackOff';
|
||||||
import { assert, strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
import { BatcherType, createBatcher } from '../util/batcher';
|
import { BatcherType, createBatcher } from '../util/batcher';
|
||||||
import { dropNull } from '../util/dropNull';
|
import { dropNull } from '../util/dropNull';
|
||||||
import { normalizeUuid } from '../util/normalizeUuid';
|
import { normalizeUuid } from '../util/normalizeUuid';
|
||||||
|
@ -60,6 +61,7 @@ import Crypto from './Crypto';
|
||||||
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
|
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
|
||||||
import { ContactBuffer, GroupBuffer } from './ContactsParser';
|
import { ContactBuffer, GroupBuffer } from './ContactsParser';
|
||||||
import { DownloadedAttachmentType } from '../types/Attachment';
|
import { DownloadedAttachmentType } from '../types/Attachment';
|
||||||
|
import * as Errors from '../types/errors';
|
||||||
import * as MIME from '../types/MIME';
|
import * as MIME from '../types/MIME';
|
||||||
import { SocketStatus } from '../types/SocketStatus';
|
import { SocketStatus } from '../types/SocketStatus';
|
||||||
|
|
||||||
|
@ -110,32 +112,26 @@ const GROUPV1_ID_LENGTH = 16;
|
||||||
const GROUPV2_ID_LENGTH = 32;
|
const GROUPV2_ID_LENGTH = 32;
|
||||||
const RETRY_TIMEOUT = 2 * 60 * 1000;
|
const RETRY_TIMEOUT = 2 * 60 * 1000;
|
||||||
|
|
||||||
type DecryptedEnvelope = Readonly<
|
type UnsealedEnvelope = Readonly<
|
||||||
ProcessedEnvelope & {
|
ProcessedEnvelope & {
|
||||||
unidentifiedDeliveryReceived?: boolean;
|
unidentifiedDeliveryReceived?: boolean;
|
||||||
contentHint?: number;
|
contentHint?: number;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
usmc?: UnidentifiedSenderMessageContent;
|
usmc?: UnidentifiedSenderMessageContent;
|
||||||
|
certificate?: SenderCertificate;
|
||||||
|
unsealedContent?: UnidentifiedSenderMessageContent;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type DecryptResult = Readonly<{
|
type DecryptResult = Readonly<{
|
||||||
envelope: DecryptedEnvelope;
|
envelope: UnsealedEnvelope;
|
||||||
plaintext?: Uint8Array;
|
plaintext?: Uint8Array;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type DecryptSealedSenderResult = Readonly<
|
type DecryptSealedSenderResult = Readonly<{
|
||||||
DecryptResult & {
|
plaintext?: Uint8Array;
|
||||||
unsealedPlaintext?: SealedSenderDecryptionResult;
|
unsealedPlaintext?: SealedSenderDecryptionResult;
|
||||||
isBlocked?: boolean;
|
}>;
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
type InnerDecryptResult = Readonly<
|
|
||||||
DecryptResult & {
|
|
||||||
isBlocked?: boolean;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
type CacheAddItemType = {
|
type CacheAddItemType = {
|
||||||
envelope: ProcessedEnvelope;
|
envelope: ProcessedEnvelope;
|
||||||
|
@ -694,7 +690,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
request.respond(500, 'Bad encrypted websocket message');
|
request.respond(500, 'Bad encrypted websocket message');
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'Error handling incoming message:',
|
'Error handling incoming message:',
|
||||||
e && e.stack ? e.stack : e
|
Errors.toLogFormat(e)
|
||||||
);
|
);
|
||||||
await this.dispatchAndWait(new ErrorEvent(e));
|
await this.dispatchAndWait(new ErrorEvent(e));
|
||||||
}
|
}
|
||||||
|
@ -889,7 +885,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
'queueCached error handling item',
|
'queueCached error handling item',
|
||||||
item.id,
|
item.id,
|
||||||
'removing it. Error:',
|
'removing it. Error:',
|
||||||
error && error.stack ? error.stack : error
|
Errors.toLogFormat(error)
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -900,7 +896,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
'queueCached error deleting item',
|
'queueCached error deleting item',
|
||||||
item.id,
|
item.id,
|
||||||
'Error:',
|
'Error:',
|
||||||
deleteError && deleteError.stack ? deleteError.stack : deleteError
|
Errors.toLogFormat(deleteError)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -968,7 +964,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'getAllFromCache error updating item after load:',
|
'getAllFromCache error updating item after load:',
|
||||||
error && error.stack ? error.stack : error
|
Errors.toLogFormat(error)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -986,7 +982,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
Readonly<{
|
Readonly<{
|
||||||
plaintext: Uint8Array;
|
plaintext: Uint8Array;
|
||||||
data: UnprocessedType;
|
data: UnprocessedType;
|
||||||
envelope: DecryptedEnvelope;
|
envelope: UnsealedEnvelope;
|
||||||
}>
|
}>
|
||||||
> = [];
|
> = [];
|
||||||
|
|
||||||
|
@ -1029,7 +1025,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
failed.push(data);
|
failed.push(data);
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'decryptAndCache error when processing the envelope',
|
'decryptAndCache error when processing the envelope',
|
||||||
error && error.stack ? error.stack : error
|
Errors.toLogFormat(error)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1079,7 +1075,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'decryptAndCache error trying to add messages to cache:',
|
'decryptAndCache error trying to add messages to cache:',
|
||||||
error && error.stack ? error.stack : error
|
Errors.toLogFormat(error)
|
||||||
);
|
);
|
||||||
|
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
|
@ -1095,7 +1091,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'decryptAndCache error when processing decrypted envelope',
|
'decryptAndCache error when processing decrypted envelope',
|
||||||
error && error.stack ? error.stack : error
|
Errors.toLogFormat(error)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1137,7 +1133,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async queueDecryptedEnvelope(
|
private async queueDecryptedEnvelope(
|
||||||
envelope: DecryptedEnvelope,
|
envelope: UnsealedEnvelope,
|
||||||
plaintext: Uint8Array
|
plaintext: Uint8Array
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const id = this.getEnvelopeId(envelope);
|
const id = this.getEnvelopeId(envelope);
|
||||||
|
@ -1154,8 +1150,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
`queueDecryptedEnvelope error handling envelope ${id}:`,
|
`queueDecryptedEnvelope error handling envelope ${id}:`,
|
||||||
error && error.extra ? JSON.stringify(error.extra) : '',
|
Errors.toLogFormat(error)
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1178,10 +1173,9 @@ export default class MessageReceiver extends EventTarget {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const args = [
|
const args = [
|
||||||
'queueEncryptedEnvelope error handling envelope',
|
'queueEncryptedEnvelope error handling envelope',
|
||||||
this.getEnvelopeId(error.envelope || envelope),
|
this.getEnvelopeId(envelope),
|
||||||
':',
|
':',
|
||||||
error && error.extra ? JSON.stringify(error.extra) : '',
|
Errors.toLogFormat(error),
|
||||||
error && error.stack ? error.stack : error,
|
|
||||||
];
|
];
|
||||||
if (error.warn) {
|
if (error.warn) {
|
||||||
window.log.warn(...args);
|
window.log.warn(...args);
|
||||||
|
@ -1215,7 +1209,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
|
|
||||||
// Called after `decryptEnvelope` decrypted the message.
|
// Called after `decryptEnvelope` decrypted the message.
|
||||||
private async handleDecryptedEnvelope(
|
private async handleDecryptedEnvelope(
|
||||||
envelope: DecryptedEnvelope,
|
envelope: UnsealedEnvelope,
|
||||||
plaintext: Uint8Array
|
plaintext: Uint8Array
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (this.stoppingProcessing) {
|
if (this.stoppingProcessing) {
|
||||||
|
@ -1241,9 +1235,13 @@ export default class MessageReceiver extends EventTarget {
|
||||||
|
|
||||||
private async decryptEnvelope(
|
private async decryptEnvelope(
|
||||||
stores: LockedStores,
|
stores: LockedStores,
|
||||||
envelope: ProcessedEnvelope
|
initialEnvelope: ProcessedEnvelope
|
||||||
): Promise<DecryptResult> {
|
): Promise<DecryptResult> {
|
||||||
|
let envelope: UnsealedEnvelope = initialEnvelope;
|
||||||
|
let logId = this.getEnvelopeId(envelope);
|
||||||
|
|
||||||
if (this.stoppingProcessing) {
|
if (this.stoppingProcessing) {
|
||||||
|
window.log.info(`MessageReceiver.decryptEnvelope(${logId}): dropping`);
|
||||||
return { plaintext: undefined, envelope };
|
return { plaintext: undefined, envelope };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1252,17 +1250,180 @@ export default class MessageReceiver extends EventTarget {
|
||||||
return { plaintext: undefined, envelope };
|
return { plaintext: undefined, envelope };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ciphertext: Uint8Array;
|
||||||
|
let isLegacy = false;
|
||||||
if (envelope.content) {
|
if (envelope.content) {
|
||||||
return this.decryptContentMessage(stores, envelope);
|
ciphertext = envelope.content;
|
||||||
}
|
} else if (envelope.legacyMessage) {
|
||||||
if (envelope.legacyMessage) {
|
ciphertext = envelope.legacyMessage;
|
||||||
return this.decryptLegacyMessage(stores, envelope);
|
isLegacy = true;
|
||||||
}
|
} else {
|
||||||
|
|
||||||
this.removeFromCache(envelope);
|
this.removeFromCache(envelope);
|
||||||
throw new Error('Received message with no content and no legacyMessage');
|
throw new Error('Received message with no content and no legacyMessage');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (envelope.type === Proto.Envelope.Type.UNIDENTIFIED_SENDER) {
|
||||||
|
window.log.info(
|
||||||
|
`MessageReceiver.decryptEnvelope(${logId}): unidentified message`
|
||||||
|
);
|
||||||
|
const messageContent = await sealedSenderDecryptToUsmc(
|
||||||
|
Buffer.from(ciphertext),
|
||||||
|
stores.identityKeyStore
|
||||||
|
);
|
||||||
|
|
||||||
|
// Here we take this sender information and attach it back to the envelope
|
||||||
|
// to make the rest of the app work properly.
|
||||||
|
const certificate = messageContent.senderCertificate();
|
||||||
|
|
||||||
|
const originalSource = envelope.source;
|
||||||
|
const originalSourceUuid = envelope.sourceUuid;
|
||||||
|
|
||||||
|
const newEnvelope: UnsealedEnvelope = {
|
||||||
|
...envelope,
|
||||||
|
|
||||||
|
// Overwrite Envelope fields
|
||||||
|
source: dropNull(certificate.senderE164()),
|
||||||
|
sourceUuid: normalizeUuid(
|
||||||
|
certificate.senderUuid(),
|
||||||
|
'MessageReceiver.decryptEnvelope.UNIDENTIFIED_SENDER.sourceUuid'
|
||||||
|
),
|
||||||
|
sourceDevice: certificate.senderDeviceId(),
|
||||||
|
|
||||||
|
// UnsealedEnvelope-only fields
|
||||||
|
unidentifiedDeliveryReceived: !(originalSource || originalSourceUuid),
|
||||||
|
contentHint: messageContent.contentHint(),
|
||||||
|
groupId: messageContent.groupId()?.toString('base64'),
|
||||||
|
usmc: messageContent,
|
||||||
|
certificate,
|
||||||
|
unsealedContent: messageContent,
|
||||||
|
};
|
||||||
|
const newLogId = this.getEnvelopeId(newEnvelope);
|
||||||
|
|
||||||
|
const validationResult = await this.validateUnsealedEnvelope(newEnvelope);
|
||||||
|
if (validationResult && validationResult.isBlocked) {
|
||||||
|
this.removeFromCache(envelope);
|
||||||
|
return { plaintext: undefined, envelope };
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.info(
|
||||||
|
`MessageReceiver.decryptEnvelope(${logId}): unwrapped into ${newLogId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
envelope = newEnvelope;
|
||||||
|
logId = newLogId;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.info(
|
||||||
|
`MessageReceiver.decryptEnvelope(${logId})${isLegacy ? ' (legacy)' : ''}`
|
||||||
|
);
|
||||||
|
const plaintext = await this.decrypt(stores, envelope, ciphertext);
|
||||||
|
|
||||||
|
if (!plaintext) {
|
||||||
|
window.log.warn('MessageReceiver.decrryptEnvelope: plaintext was falsey');
|
||||||
|
return { plaintext, envelope };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy envelopes do not carry senderKeyDistributionMessage
|
||||||
|
if (isLegacy) {
|
||||||
|
return { plaintext, envelope };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we need to process this as part of decryption, because we might need this
|
||||||
|
// sender key to decrypt the next message in the queue!
|
||||||
|
try {
|
||||||
|
const content = Proto.Content.decode(plaintext);
|
||||||
|
|
||||||
|
if (
|
||||||
|
content.senderKeyDistributionMessage &&
|
||||||
|
Bytes.isNotEmpty(content.senderKeyDistributionMessage)
|
||||||
|
) {
|
||||||
|
await this.handleSenderKeyDistributionMessage(
|
||||||
|
stores,
|
||||||
|
envelope,
|
||||||
|
content.senderKeyDistributionMessage
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'MessageReceiver.decrryptEnvelope: Failed to process sender ' +
|
||||||
|
`key distribution message: ${Errors.toLogFormat(error)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { plaintext, envelope };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async validateUnsealedEnvelope(
|
||||||
|
envelope: UnsealedEnvelope
|
||||||
|
): Promise<{ isBlocked: true } | void> {
|
||||||
|
const { unsealedContent: messageContent, certificate } = envelope;
|
||||||
|
strictAssert(
|
||||||
|
messageContent !== undefined,
|
||||||
|
'Missing message content for sealed sender message'
|
||||||
|
);
|
||||||
|
strictAssert(
|
||||||
|
certificate !== undefined,
|
||||||
|
'Missing sender certificate for sealed sender message'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(envelope.source && this.isBlocked(envelope.source)) ||
|
||||||
|
(envelope.sourceUuid && this.isUuidBlocked(envelope.sourceUuid))
|
||||||
|
) {
|
||||||
|
window.log.info(
|
||||||
|
'MessageReceiver.validateUnsealedEnvelope: Dropping blocked message ' +
|
||||||
|
'after partial sealed sender decryption'
|
||||||
|
);
|
||||||
|
return { isBlocked: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!envelope.serverTimestamp) {
|
||||||
|
throw new Error(
|
||||||
|
'MessageReceiver.decryptSealedSender: ' +
|
||||||
|
'Sealed sender message was missing serverTimestamp'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverCertificate = certificate.serverCertificate();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!verifySignature(
|
||||||
|
typedArrayToArrayBuffer(this.serverTrustRoot),
|
||||||
|
typedArrayToArrayBuffer(serverCertificate.certificateData()),
|
||||||
|
typedArrayToArrayBuffer(serverCertificate.signature())
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'MessageReceiver.validateUnsealedEnvelope: ' +
|
||||||
|
'Server certificate trust root validation failed'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!verifySignature(
|
||||||
|
typedArrayToArrayBuffer(serverCertificate.key().serialize()),
|
||||||
|
typedArrayToArrayBuffer(certificate.certificate()),
|
||||||
|
typedArrayToArrayBuffer(certificate.signature())
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'MessageReceiver.validateUnsealedEnvelope: ' +
|
||||||
|
'Server certificate server signature validation failed'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logId = this.getEnvelopeId(envelope);
|
||||||
|
|
||||||
|
if (envelope.serverTimestamp > certificate.expiration()) {
|
||||||
|
throw new Error(
|
||||||
|
`MessageReceiver.validateUnsealedEnvelope: ' +
|
||||||
|
'Sender certificate is expired for envelope ${logId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private async onDeliveryReceipt(envelope: ProcessedEnvelope): Promise<void> {
|
private async onDeliveryReceipt(envelope: ProcessedEnvelope): Promise<void> {
|
||||||
await this.dispatchAndWait(
|
await this.dispatchAndWait(
|
||||||
new DeliveryEvent(
|
new DeliveryEvent(
|
||||||
|
@ -1292,14 +1453,9 @@ export default class MessageReceiver extends EventTarget {
|
||||||
|
|
||||||
private async decryptSealedSender(
|
private async decryptSealedSender(
|
||||||
{ sessionStore, identityKeyStore, zone }: LockedStores,
|
{ sessionStore, identityKeyStore, zone }: LockedStores,
|
||||||
envelope: ProcessedEnvelope,
|
envelope: UnsealedEnvelope,
|
||||||
ciphertext: Uint8Array
|
ciphertext: Uint8Array
|
||||||
): Promise<{
|
): Promise<DecryptSealedSenderResult> {
|
||||||
error?: Error;
|
|
||||||
result: DecryptSealedSenderResult;
|
|
||||||
}> {
|
|
||||||
const buffer = Buffer.from(ciphertext);
|
|
||||||
|
|
||||||
const localE164 = window.textsecure.storage.user.getNumber();
|
const localE164 = window.textsecure.storage.user.getNumber();
|
||||||
const localUuid = window.textsecure.storage.user.getUuid();
|
const localUuid = window.textsecure.storage.user.getUuid();
|
||||||
const localDeviceId = parseIntOrThrow(
|
const localDeviceId = parseIntOrThrow(
|
||||||
|
@ -1313,86 +1469,17 @@ export default class MessageReceiver extends EventTarget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageContent: UnidentifiedSenderMessageContent = await sealedSenderDecryptToUsmc(
|
const logId = this.getEnvelopeId(envelope);
|
||||||
buffer,
|
|
||||||
identityKeyStore
|
const { unsealedContent: messageContent, certificate } = envelope;
|
||||||
|
strictAssert(
|
||||||
|
messageContent !== undefined,
|
||||||
|
'Missing message content for sealed sender message'
|
||||||
);
|
);
|
||||||
|
strictAssert(
|
||||||
// Here we take this sender information and attach it back to the envelope
|
certificate !== undefined,
|
||||||
// to make the rest of the app work properly.
|
'Missing sender certificate for sealed sender message'
|
||||||
const certificate = messageContent.senderCertificate();
|
|
||||||
|
|
||||||
const originalSource = envelope.source;
|
|
||||||
const originalSourceUuid = envelope.sourceUuid;
|
|
||||||
|
|
||||||
const newEnvelope: DecryptedEnvelope = {
|
|
||||||
...envelope,
|
|
||||||
|
|
||||||
source: dropNull(certificate.senderE164()),
|
|
||||||
sourceUuid: normalizeUuid(
|
|
||||||
certificate.senderUuid(),
|
|
||||||
'MessageReceiver.decryptSealedSender.UNIDENTIFIED_SENDER.sourceUuid'
|
|
||||||
),
|
|
||||||
sourceDevice: certificate.senderDeviceId(),
|
|
||||||
unidentifiedDeliveryReceived: !(originalSource || originalSourceUuid),
|
|
||||||
contentHint: messageContent.contentHint(),
|
|
||||||
groupId: messageContent.groupId()?.toString('base64'),
|
|
||||||
usmc: messageContent,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
(newEnvelope.source && this.isBlocked(newEnvelope.source)) ||
|
|
||||||
(newEnvelope.sourceUuid && this.isUuidBlocked(newEnvelope.sourceUuid))
|
|
||||||
) {
|
|
||||||
window.log.info(
|
|
||||||
'MessageReceiver.decryptSealedSender: Dropping blocked message after ' +
|
|
||||||
'partial sealed sender decryption'
|
|
||||||
);
|
);
|
||||||
return {
|
|
||||||
result: { isBlocked: true, envelope: newEnvelope },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newEnvelope.serverTimestamp) {
|
|
||||||
throw new Error(
|
|
||||||
'MessageReceiver.decryptSealedSender: ' +
|
|
||||||
'Sealed sender message was missing serverTimestamp'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverCertificate = certificate.serverCertificate();
|
|
||||||
|
|
||||||
if (
|
|
||||||
!verifySignature(
|
|
||||||
typedArrayToArrayBuffer(this.serverTrustRoot),
|
|
||||||
typedArrayToArrayBuffer(serverCertificate.certificateData()),
|
|
||||||
typedArrayToArrayBuffer(serverCertificate.signature())
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
'MessageReceiver.decryptSealedSender: Server certificate trust root validation failed'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!verifySignature(
|
|
||||||
typedArrayToArrayBuffer(serverCertificate.key().serialize()),
|
|
||||||
typedArrayToArrayBuffer(certificate.certificate()),
|
|
||||||
typedArrayToArrayBuffer(certificate.signature())
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
'MessageReceiver.decryptSealedSender: Server certificate server signature validation failed'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const unidentifiedLogId = this.getEnvelopeId(newEnvelope);
|
|
||||||
|
|
||||||
if (newEnvelope.serverTimestamp > certificate.expiration()) {
|
|
||||||
throw new Error(
|
|
||||||
`MessageReceiver.decryptSealedSender: Sender certificate is expired for envelope ${unidentifiedLogId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const unidentifiedSenderTypeEnum =
|
const unidentifiedSenderTypeEnum =
|
||||||
Proto.UnidentifiedSenderMessage.Message.Type;
|
Proto.UnidentifiedSenderMessage.Message.Type;
|
||||||
|
@ -1401,17 +1488,15 @@ export default class MessageReceiver extends EventTarget {
|
||||||
messageContent.msgType() === unidentifiedSenderTypeEnum.PLAINTEXT_CONTENT
|
messageContent.msgType() === unidentifiedSenderTypeEnum.PLAINTEXT_CONTENT
|
||||||
) {
|
) {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
`decrypt/${unidentifiedLogId}: unidentified message/plaintext contents`
|
`MessageReceiver.decryptSealedSender(${logId}): ` +
|
||||||
|
'unidentified message/plaintext contents'
|
||||||
);
|
);
|
||||||
const plaintextContent = PlaintextContent.deserialize(
|
const plaintextContent = PlaintextContent.deserialize(
|
||||||
messageContent.contents()
|
messageContent.contents()
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: {
|
|
||||||
plaintext: plaintextContent.body(),
|
plaintext: plaintextContent.body(),
|
||||||
envelope: newEnvelope,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1419,7 +1504,8 @@ export default class MessageReceiver extends EventTarget {
|
||||||
messageContent.msgType() === unidentifiedSenderTypeEnum.SENDERKEY_MESSAGE
|
messageContent.msgType() === unidentifiedSenderTypeEnum.SENDERKEY_MESSAGE
|
||||||
) {
|
) {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
`decrypt/${unidentifiedLogId}: unidentified message/sender key contents`
|
`MessageReceiver.decryptSealedSender(${logId}): ` +
|
||||||
|
'unidentified message/sender key contents'
|
||||||
);
|
);
|
||||||
const sealedSenderIdentifier = certificate.senderUuid();
|
const sealedSenderIdentifier = certificate.senderUuid();
|
||||||
const sealedSenderSourceDevice = certificate.senderDeviceId();
|
const sealedSenderSourceDevice = certificate.senderDeviceId();
|
||||||
|
@ -1427,7 +1513,6 @@ export default class MessageReceiver extends EventTarget {
|
||||||
|
|
||||||
const address = `${sealedSenderIdentifier}.${sealedSenderSourceDevice}`;
|
const address = `${sealedSenderIdentifier}.${sealedSenderSourceDevice}`;
|
||||||
|
|
||||||
try {
|
|
||||||
const plaintext = await window.textsecure.storage.protocol.enqueueSenderKeyJob(
|
const plaintext = await window.textsecure.storage.protocol.enqueueSenderKeyJob(
|
||||||
address,
|
address,
|
||||||
() =>
|
() =>
|
||||||
|
@ -1441,39 +1526,26 @@ export default class MessageReceiver extends EventTarget {
|
||||||
),
|
),
|
||||||
zone
|
zone
|
||||||
);
|
);
|
||||||
return {
|
return { plaintext };
|
||||||
result: {
|
|
||||||
plaintext,
|
|
||||||
envelope: newEnvelope,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
error,
|
|
||||||
result: {
|
|
||||||
envelope: newEnvelope,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.log.info(
|
window.log.info(
|
||||||
`decrypt/${unidentifiedLogId}: unidentified message/passing to sealedSenderDecryptMessage`
|
`MessageReceiver.decryptSealedSender(${logId}): ` +
|
||||||
|
'unidentified message/passing to sealedSenderDecryptMessage'
|
||||||
);
|
);
|
||||||
|
|
||||||
const preKeyStore = new PreKeys();
|
const preKeyStore = new PreKeys();
|
||||||
const signedPreKeyStore = new SignedPreKeys();
|
const signedPreKeyStore = new SignedPreKeys();
|
||||||
|
|
||||||
const sealedSenderIdentifier = newEnvelope.sourceUuid || newEnvelope.source;
|
const sealedSenderIdentifier = envelope.sourceUuid || envelope.source;
|
||||||
const address = `${sealedSenderIdentifier}.${newEnvelope.sourceDevice}`;
|
const address = `${sealedSenderIdentifier}.${envelope.sourceDevice}`;
|
||||||
try {
|
|
||||||
const unsealedPlaintext = await window.textsecure.storage.protocol.enqueueSessionJob(
|
const unsealedPlaintext = await window.textsecure.storage.protocol.enqueueSessionJob(
|
||||||
address,
|
address,
|
||||||
() =>
|
() =>
|
||||||
sealedSenderDecryptMessage(
|
sealedSenderDecryptMessage(
|
||||||
buffer,
|
Buffer.from(ciphertext),
|
||||||
PublicKey.deserialize(Buffer.from(this.serverTrustRoot)),
|
PublicKey.deserialize(Buffer.from(this.serverTrustRoot)),
|
||||||
newEnvelope.serverTimestamp,
|
envelope.serverTimestamp,
|
||||||
localE164 || null,
|
localE164 || null,
|
||||||
localUuid,
|
localUuid,
|
||||||
localDeviceId,
|
localDeviceId,
|
||||||
|
@ -1485,27 +1557,14 @@ export default class MessageReceiver extends EventTarget {
|
||||||
zone
|
zone
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return { unsealedPlaintext };
|
||||||
result: { unsealedPlaintext, envelope: newEnvelope },
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
error,
|
|
||||||
result: {
|
|
||||||
envelope: newEnvelope,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async innerDecrypt(
|
private async innerDecrypt(
|
||||||
stores: LockedStores,
|
stores: LockedStores,
|
||||||
envelope: ProcessedEnvelope,
|
envelope: ProcessedEnvelope,
|
||||||
ciphertext: Uint8Array
|
ciphertext: Uint8Array
|
||||||
): Promise<{
|
): Promise<Uint8Array> {
|
||||||
error?: Error;
|
|
||||||
result: InnerDecryptResult;
|
|
||||||
}> {
|
|
||||||
const { sessionStore, identityKeyStore, zone } = stores;
|
const { sessionStore, identityKeyStore, zone } = stores;
|
||||||
|
|
||||||
const logId = this.getEnvelopeId(envelope);
|
const logId = this.getEnvelopeId(envelope);
|
||||||
|
@ -1522,12 +1581,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
const buffer = Buffer.from(ciphertext);
|
const buffer = Buffer.from(ciphertext);
|
||||||
const plaintextContent = PlaintextContent.deserialize(buffer);
|
const plaintextContent = PlaintextContent.deserialize(buffer);
|
||||||
|
|
||||||
return {
|
return this.unpad(plaintextContent.body());
|
||||||
result: {
|
|
||||||
plaintext: this.unpad(plaintextContent.body()),
|
|
||||||
envelope,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (envelope.type === envelopeTypeEnum.CIPHERTEXT) {
|
if (envelope.type === envelopeTypeEnum.CIPHERTEXT) {
|
||||||
window.log.info(`decrypt/${logId}: ciphertext message`);
|
window.log.info(`decrypt/${logId}: ciphertext message`);
|
||||||
|
@ -1557,12 +1611,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
),
|
),
|
||||||
zone
|
zone
|
||||||
);
|
);
|
||||||
return {
|
return plaintext;
|
||||||
result: {
|
|
||||||
plaintext,
|
|
||||||
envelope,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (envelope.type === envelopeTypeEnum.PREKEY_BUNDLE) {
|
if (envelope.type === envelopeTypeEnum.PREKEY_BUNDLE) {
|
||||||
window.log.info(`decrypt/${logId}: prekey message`);
|
window.log.info(`decrypt/${logId}: prekey message`);
|
||||||
|
@ -1596,50 +1645,18 @@ export default class MessageReceiver extends EventTarget {
|
||||||
),
|
),
|
||||||
zone
|
zone
|
||||||
);
|
);
|
||||||
return {
|
return plaintext;
|
||||||
result: {
|
|
||||||
plaintext,
|
|
||||||
envelope,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (envelope.type === envelopeTypeEnum.UNIDENTIFIED_SENDER) {
|
if (envelope.type === envelopeTypeEnum.UNIDENTIFIED_SENDER) {
|
||||||
window.log.info(`decrypt/${logId}: unidentified message`);
|
window.log.info(`decrypt/${logId}: unidentified message`);
|
||||||
const { result, error } = await this.decryptSealedSender(
|
const { plaintext, unsealedPlaintext } = await this.decryptSealedSender(
|
||||||
stores,
|
stores,
|
||||||
envelope,
|
envelope,
|
||||||
ciphertext
|
ciphertext
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return {
|
|
||||||
error,
|
|
||||||
result,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
plaintext,
|
|
||||||
unsealedPlaintext,
|
|
||||||
isBlocked,
|
|
||||||
envelope: newEnvelope,
|
|
||||||
} = result;
|
|
||||||
if (isBlocked) {
|
|
||||||
return {
|
|
||||||
result: {
|
|
||||||
isBlocked: true,
|
|
||||||
envelope: newEnvelope,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plaintext) {
|
if (plaintext) {
|
||||||
return {
|
return this.unpad(plaintext);
|
||||||
result: {
|
|
||||||
plaintext: this.unpad(plaintext),
|
|
||||||
envelope: newEnvelope,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unsealedPlaintext) {
|
if (unsealedPlaintext) {
|
||||||
|
@ -1653,12 +1670,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
|
|
||||||
// Return just the content because that matches the signature of the other
|
// Return just the content because that matches the signature of the other
|
||||||
// decrypt methods used above.
|
// decrypt methods used above.
|
||||||
return {
|
return this.unpad(content);
|
||||||
result: {
|
|
||||||
plaintext: this.unpad(content),
|
|
||||||
envelope: newEnvelope,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Unexpected lack of plaintext from unidentified sender');
|
throw new Error('Unexpected lack of plaintext from unidentified sender');
|
||||||
|
@ -1668,35 +1680,17 @@ export default class MessageReceiver extends EventTarget {
|
||||||
|
|
||||||
private async decrypt(
|
private async decrypt(
|
||||||
stores: LockedStores,
|
stores: LockedStores,
|
||||||
envelope: ProcessedEnvelope,
|
envelope: UnsealedEnvelope,
|
||||||
ciphertext: Uint8Array
|
ciphertext: Uint8Array
|
||||||
): Promise<DecryptResult> {
|
): Promise<Uint8Array | undefined> {
|
||||||
let newEnvelope: DecryptedEnvelope = envelope;
|
|
||||||
try {
|
try {
|
||||||
const { result, error } = await this.innerDecrypt(
|
const plaintext = await this.innerDecrypt(stores, envelope, ciphertext);
|
||||||
stores,
|
|
||||||
envelope,
|
|
||||||
ciphertext
|
|
||||||
);
|
|
||||||
|
|
||||||
newEnvelope = result.envelope || envelope;
|
return new FIXMEU8(plaintext);
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { isBlocked, plaintext } = result;
|
|
||||||
|
|
||||||
if (isBlocked) {
|
|
||||||
this.removeFromCache(envelope);
|
|
||||||
return { plaintext: undefined, envelope: newEnvelope };
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(plaintext, 'Should have plaintext from innerDecrypt');
|
|
||||||
return { plaintext: new FIXMEU8(plaintext), envelope: newEnvelope };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.removeFromCache(newEnvelope);
|
this.removeFromCache(envelope);
|
||||||
const uuid = newEnvelope.sourceUuid;
|
const uuid = envelope.sourceUuid;
|
||||||
const deviceId = newEnvelope.sourceDevice;
|
const deviceId = envelope.sourceDevice;
|
||||||
|
|
||||||
// We don't do a light session reset if it's just a duplicated message
|
// We don't do a light session reset if it's just a duplicated message
|
||||||
if (
|
if (
|
||||||
|
@ -1716,19 +1710,19 @@ export default class MessageReceiver extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uuid && deviceId) {
|
if (uuid && deviceId) {
|
||||||
const { usmc } = newEnvelope;
|
const { usmc } = envelope;
|
||||||
const event = new DecryptionErrorEvent({
|
const event = new DecryptionErrorEvent({
|
||||||
cipherTextBytes: usmc
|
cipherTextBytes: usmc
|
||||||
? typedArrayToArrayBuffer(usmc.contents())
|
? typedArrayToArrayBuffer(usmc.contents())
|
||||||
: undefined,
|
: undefined,
|
||||||
cipherTextType: usmc ? usmc.msgType() : undefined,
|
cipherTextType: usmc ? usmc.msgType() : undefined,
|
||||||
contentHint: newEnvelope.contentHint,
|
contentHint: envelope.contentHint,
|
||||||
groupId: newEnvelope.groupId,
|
groupId: envelope.groupId,
|
||||||
receivedAtCounter: newEnvelope.receivedAtCounter,
|
receivedAtCounter: envelope.receivedAtCounter,
|
||||||
receivedAtDate: newEnvelope.receivedAtDate,
|
receivedAtDate: envelope.receivedAtDate,
|
||||||
senderDevice: deviceId,
|
senderDevice: deviceId,
|
||||||
senderUuid: uuid,
|
senderUuid: uuid,
|
||||||
timestamp: newEnvelope.timestamp,
|
timestamp: envelope.timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Avoid deadlocks by scheduling processing on decrypted queue
|
// Avoid deadlocks by scheduling processing on decrypted queue
|
||||||
|
@ -1737,7 +1731,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
TaskType.Decrypted
|
TaskType.Decrypted
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const envelopeId = this.getEnvelopeId(newEnvelope);
|
const envelopeId = this.getEnvelopeId(envelope);
|
||||||
window.log.error(
|
window.log.error(
|
||||||
`MessageReceiver.decrypt: Envelope ${envelopeId} missing uuid or deviceId`
|
`MessageReceiver.decrypt: Envelope ${envelopeId} missing uuid or deviceId`
|
||||||
);
|
);
|
||||||
|
@ -1829,7 +1823,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleDataMessage(
|
private async handleDataMessage(
|
||||||
envelope: DecryptedEnvelope,
|
envelope: UnsealedEnvelope,
|
||||||
msg: Proto.IDataMessage
|
msg: Proto.IDataMessage
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
|
@ -1916,23 +1910,6 @@ export default class MessageReceiver extends EventTarget {
|
||||||
return this.dispatchAndWait(ev);
|
return this.dispatchAndWait(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async decryptLegacyMessage(
|
|
||||||
stores: LockedStores,
|
|
||||||
envelope: ProcessedEnvelope
|
|
||||||
): Promise<DecryptResult> {
|
|
||||||
window.log.info(
|
|
||||||
'MessageReceiver.decryptLegacyMessage',
|
|
||||||
this.getEnvelopeId(envelope)
|
|
||||||
);
|
|
||||||
assert(envelope.legacyMessage, 'Should have `legacyMessage` field');
|
|
||||||
const result = await this.decrypt(stores, envelope, envelope.legacyMessage);
|
|
||||||
if (!result.plaintext) {
|
|
||||||
window.log.warn('decryptLegacyMessage: plaintext was falsey');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async innerHandleLegacyMessage(
|
private async innerHandleLegacyMessage(
|
||||||
envelope: ProcessedEnvelope,
|
envelope: ProcessedEnvelope,
|
||||||
plaintext: Uint8Array
|
plaintext: Uint8Array
|
||||||
|
@ -1941,47 +1918,6 @@ export default class MessageReceiver extends EventTarget {
|
||||||
return this.handleDataMessage(envelope, message);
|
return this.handleDataMessage(envelope, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async decryptContentMessage(
|
|
||||||
stores: LockedStores,
|
|
||||||
envelope: ProcessedEnvelope
|
|
||||||
): Promise<DecryptResult> {
|
|
||||||
window.log.info(
|
|
||||||
'MessageReceiver.decryptContentMessage',
|
|
||||||
this.getEnvelopeId(envelope)
|
|
||||||
);
|
|
||||||
assert(envelope.content, 'Should have `content` field');
|
|
||||||
const result = await this.decrypt(stores, envelope, envelope.content);
|
|
||||||
|
|
||||||
if (!result.plaintext) {
|
|
||||||
window.log.warn('decryptContentMessage: plaintext was falsey');
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: we need to process this as part of decryption, because we might need this
|
|
||||||
// sender key to decrypt the next message in the queue!
|
|
||||||
try {
|
|
||||||
const content = Proto.Content.decode(result.plaintext);
|
|
||||||
|
|
||||||
if (
|
|
||||||
content.senderKeyDistributionMessage &&
|
|
||||||
Bytes.isNotEmpty(content.senderKeyDistributionMessage)
|
|
||||||
) {
|
|
||||||
await this.handleSenderKeyDistributionMessage(
|
|
||||||
stores,
|
|
||||||
result.envelope,
|
|
||||||
content.senderKeyDistributionMessage
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const errorString = error && error.stack ? error.stack : error;
|
|
||||||
window.log.error(
|
|
||||||
`decryptContentMessage: Failed to process sender key distribution message: ${errorString}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async maybeUpdateTimestamp(
|
private async maybeUpdateTimestamp(
|
||||||
envelope: ProcessedEnvelope
|
envelope: ProcessedEnvelope
|
||||||
): Promise<ProcessedEnvelope> {
|
): Promise<ProcessedEnvelope> {
|
||||||
|
@ -2027,9 +1963,9 @@ export default class MessageReceiver extends EventTarget {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorString = error && error.stack ? error.stack : error;
|
|
||||||
window.log.error(
|
window.log.error(
|
||||||
`maybeUpdateTimestamp/${timestamp}: Failed to process sender key distribution message: ${errorString}`
|
`maybeUpdateTimestamp/${timestamp}: Failed to process sender key ` +
|
||||||
|
`distribution message: ${Errors.toLogFormat(error)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2089,7 +2025,7 @@ export default class MessageReceiver extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleDecryptionError(
|
private async handleDecryptionError(
|
||||||
envelope: DecryptedEnvelope,
|
envelope: UnsealedEnvelope,
|
||||||
decryptionError: Uint8Array
|
decryptionError: Uint8Array
|
||||||
) {
|
) {
|
||||||
const logId = this.getEnvelopeId(envelope);
|
const logId = this.getEnvelopeId(envelope);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue