Make finalization part of the stream

This commit is contained in:
Fedor Indutny 2024-07-19 19:17:02 -07:00 committed by GitHub
parent c93a211595
commit 86b4da1ec2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 122 additions and 55 deletions

View file

@ -21,6 +21,7 @@ import { createName, getRelativePath } from './util/attachmentPath';
import { appendPaddingStream, logPadSize } from './util/logPadding';
import { prependStream } from './util/prependStream';
import { appendMacStream } from './util/appendMacStream';
import { finalStream } from './util/finalStream';
import { getIvAndDecipher } from './util/getIvAndDecipher';
import { getMacAndUpdateHmac } from './util/getMacAndUpdateHmac';
import { trimPadding } from './util/trimPadding';
@ -378,28 +379,9 @@ export async function decryptAttachmentV2ToSink(
}),
trimPadding(options.size),
peekAndUpdateHash(plaintextHash),
sink,
].filter(isNotNil)
);
} catch (error) {
// These errors happen when canceling fetch from `attachment://` urls,
// ignore them to avoid noise in the logs.
if (error.name === 'AbortError') {
throw error;
}
log.error(
`${logId}: Failed to decrypt attachment`,
Errors.toLogFormat(error)
);
throw error;
} finally {
await readFd?.close();
}
finalStream(() => {
const ourMac = hmac.digest();
const ourDigest = digest.digest();
const ourPlaintextHash = plaintextHash.digest('hex');
strictAssert(
ourMac.byteLength === ATTACHMENT_MAC_LENGTH,
@ -413,10 +395,6 @@ export async function decryptAttachmentV2ToSink(
ourDigest.byteLength === DIGEST_LENGTH,
`${logId}: Failed to generate ourDigest!`
);
strictAssert(
ourPlaintextHash.length === HEX_DIGEST_LENGTH,
`${logId}: Failed to generate file hash!`
);
if (!constantTimeEqual(ourMac, theirMac)) {
throw new Error(`${logId}: Bad MAC`);
@ -439,12 +417,10 @@ export async function decryptAttachmentV2ToSink(
throw missingCaseError(type);
}
strictAssert(
iv != null && iv.byteLength === IV_LENGTH,
`${logId}: failed to find their iv`
);
if (!outerEncryption) {
return;
}
if (outerEncryption) {
strictAssert(outerHmac, 'outerHmac must exist');
const ourOuterMac = outerHmac.digest();
@ -461,8 +437,37 @@ export async function decryptAttachmentV2ToSink(
if (!constantTimeEqual(ourOuterMac, theirOuterMac)) {
throw new Error(`${logId}: Bad outer encryption MAC`);
}
}),
sink,
].filter(isNotNil)
);
} catch (error) {
// These errors happen when canceling fetch from `attachment://` urls,
// ignore them to avoid noise in the logs.
if (error.name === 'AbortError') {
throw error;
}
log.error(
`${logId}: Failed to decrypt attachment`,
Errors.toLogFormat(error)
);
throw error;
} finally {
await readFd?.close();
}
const ourPlaintextHash = plaintextHash.digest('hex');
strictAssert(
ourPlaintextHash.length === HEX_DIGEST_LENGTH,
`${logId}: Failed to generate file hash!`
);
strictAssert(
iv != null && iv.byteLength === IV_LENGTH,
`${logId}: failed to find their iv`
);
return {
iv,
plaintextHash: ourPlaintextHash,

View file

@ -0,0 +1,40 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { finalStream } from '../../util/finalStream';
describe('finalStream', () => {
it('should invoke callback before pipeline resolves', async () => {
let called = false;
await pipeline(
Readable.from(['abc']),
finalStream(async () => {
// Forcing next tick
await Promise.resolve();
called = true;
})
);
assert.isTrue(called);
});
it('should propagate errors from callback', async () => {
await assert.isRejected(
pipeline(
Readable.from(['abc']),
finalStream(async () => {
// Forcing next tick
await Promise.resolve();
throw new Error('failure');
})
),
'failure'
);
});
});

22
ts/util/finalStream.ts Normal file
View file

@ -0,0 +1,22 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { Transform } from 'node:stream';
export function finalStream(finalizer: () => Promise<void> | void): Transform {
return new Transform({
transform(data, enc, callback) {
this.push(Buffer.from(data, enc));
callback();
},
async final(callback) {
try {
await finalizer();
} catch (error) {
callback(error);
return;
}
callback(null);
},
});
}