Unseal envelope in a separate step for better logs

This commit is contained in:
Fedor Indutny 2021-08-02 14:11:18 -07:00 committed by Scott Nonnenberg
parent 907e1d32ec
commit 21ffb7c054
3 changed files with 110 additions and 73 deletions

View file

@ -44,4 +44,12 @@ describe('createTaskWithTimeout', () => {
}); });
await assert.isRejected(taskWithTimeout(), 'Task is throwing!'); await assert.isRejected(taskWithTimeout(), 'Task is throwing!');
}); });
it('passes arguments to the underlying function', async () => {
const task = (arg: string) => Promise.resolve(arg);
const taskWithTimeout = createTaskWithTimeout(task, 'test');
const result = await taskWithTimeout('hi!');
assert.strictEqual(result, 'hi!');
});
}); });

View file

@ -919,21 +919,27 @@ export default class MessageReceiver
stores: LockedStores, stores: LockedStores,
envelope: ProcessedEnvelope envelope: ProcessedEnvelope
): Promise<DecryptResult> { ): Promise<DecryptResult> {
const id = this.getEnvelopeId(envelope); let logId = this.getEnvelopeId(envelope);
window.log.info('queueing envelope', id); window.log.info('queueing envelope', logId);
const task = this.decryptEnvelope.bind(this, stores, envelope); const task = createTaskWithTimeout(async (): Promise<DecryptResult> => {
const taskWithTimeout = createTaskWithTimeout( const unsealedEnvelope = await this.unsealEnvelope(stores, envelope);
task, if (!unsealedEnvelope) {
`queueEncryptedEnvelope ${id}` // Envelope was dropped or sender is blocked
); return { envelope, plaintext: undefined };
}
logId = this.getEnvelopeId(unsealedEnvelope);
return this.decryptEnvelope(stores, unsealedEnvelope);
}, `MessageReceiver: unseal and decrypt ${logId}`);
try { try {
return await this.addToQueue(taskWithTimeout, TaskType.Encrypted); return await this.addToQueue(task, TaskType.Encrypted);
} catch (error) { } catch (error) {
const args = [ const args = [
'queueEncryptedEnvelope error handling envelope', 'queueEncryptedEnvelope error handling envelope',
this.getEnvelopeId(envelope), logId,
':', ':',
Errors.toLogFormat(error), Errors.toLogFormat(error),
]; ];
@ -993,15 +999,86 @@ export default class MessageReceiver
throw new Error('Received message with no content and no legacyMessage'); throw new Error('Received message with no content and no legacyMessage');
} }
private async decryptEnvelope( private async unsealEnvelope(
stores: LockedStores, stores: LockedStores,
initialEnvelope: ProcessedEnvelope envelope: ProcessedEnvelope
): Promise<DecryptResult> { ): Promise<UnsealedEnvelope | undefined> {
let envelope: UnsealedEnvelope = initialEnvelope; const logId = this.getEnvelopeId(envelope);
let logId = this.getEnvelopeId(envelope);
if (this.stoppingProcessing) { if (this.stoppingProcessing) {
window.log.info(`MessageReceiver.decryptEnvelope(${logId}): dropping`); window.log.info(`MessageReceiver.unsealEnvelope(${logId}): dropping`);
return undefined;
}
if (envelope.type !== Proto.Envelope.Type.UNIDENTIFIED_SENDER) {
return envelope;
}
const ciphertext = envelope.content || envelope.legacyMessage;
if (!ciphertext) {
this.removeFromCache(envelope);
throw new Error('Received message with no content and no legacyMessage');
}
window.log.info(
`MessageReceiver.unsealEnvelope(${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.unsealEnvelope.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 undefined;
}
window.log.info(
`MessageReceiver.unsealEnvelope(${logId}): unwrapped into ${newLogId}`
);
return newEnvelope;
}
private async decryptEnvelope(
stores: LockedStores,
envelope: UnsealedEnvelope
): Promise<DecryptResult> {
const logId = this.getEnvelopeId(envelope);
if (this.stoppingProcessing) {
window.log.info(
`MessageReceiver.decryptEnvelope(${logId}): dropping unsealed`
);
return { plaintext: undefined, envelope }; return { plaintext: undefined, envelope };
} }
@ -1019,58 +1096,10 @@ export default class MessageReceiver
isLegacy = true; isLegacy = true;
} else { } else {
this.removeFromCache(envelope); this.removeFromCache(envelope);
throw new Error('Received message with no content and no legacyMessage'); strictAssert(
} false,
'Contentless envelope should be handled by unsealEnvelope'
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( window.log.info(
@ -1079,7 +1108,7 @@ export default class MessageReceiver
const plaintext = await this.decrypt(stores, envelope, ciphertext); const plaintext = await this.decrypt(stores, envelope, ciphertext);
if (!plaintext) { if (!plaintext) {
window.log.warn('MessageReceiver.decrryptEnvelope: plaintext was falsey'); window.log.warn('MessageReceiver.decryptEnvelope: plaintext was falsey');
return { plaintext, envelope }; return { plaintext, envelope };
} }
@ -1105,7 +1134,7 @@ export default class MessageReceiver
} }
} catch (error) { } catch (error) {
window.log.error( window.log.error(
'MessageReceiver.decrryptEnvelope: Failed to process sender ' + 'MessageReceiver.decryptEnvelope: Failed to process sender ' +
`key distribution message: ${Errors.toLogFormat(error)}` `key distribution message: ${Errors.toLogFormat(error)}`
); );
} }

View file

@ -1,16 +1,16 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
export default function createTaskWithTimeout<T>( export default function createTaskWithTimeout<T, Args extends Array<unknown>>(
task: () => Promise<T>, task: (...args: Args) => Promise<T>,
id: string, id: string,
options: { timeout?: number } = {} options: { timeout?: number } = {}
): () => Promise<T> { ): (...args: Args) => Promise<T> {
const timeout = options.timeout || 1000 * 60 * 2; // two minutes const timeout = options.timeout || 1000 * 60 * 2; // two minutes
const errorForStack = new Error('for stack'); const errorForStack = new Error('for stack');
return async () => return async (...args: Args) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
let complete = false; let complete = false;
let timer: NodeJS.Timeout | null = setTimeout(() => { let timer: NodeJS.Timeout | null = setTimeout(() => {
@ -58,7 +58,7 @@ export default function createTaskWithTimeout<T>(
let promise; let promise;
try { try {
promise = task(); promise = task(...args);
} catch (error) { } catch (error) {
clearTimer(); clearTimer();
throw error; throw error;