Make finalization part of the stream
This commit is contained in:
parent
c93a211595
commit
86b4da1ec2
3 changed files with 122 additions and 55 deletions
|
@ -21,6 +21,7 @@ import { createName, getRelativePath } from './util/attachmentPath';
|
||||||
import { appendPaddingStream, logPadSize } from './util/logPadding';
|
import { appendPaddingStream, logPadSize } from './util/logPadding';
|
||||||
import { prependStream } from './util/prependStream';
|
import { prependStream } from './util/prependStream';
|
||||||
import { appendMacStream } from './util/appendMacStream';
|
import { appendMacStream } from './util/appendMacStream';
|
||||||
|
import { finalStream } from './util/finalStream';
|
||||||
import { getIvAndDecipher } from './util/getIvAndDecipher';
|
import { getIvAndDecipher } from './util/getIvAndDecipher';
|
||||||
import { getMacAndUpdateHmac } from './util/getMacAndUpdateHmac';
|
import { getMacAndUpdateHmac } from './util/getMacAndUpdateHmac';
|
||||||
import { trimPadding } from './util/trimPadding';
|
import { trimPadding } from './util/trimPadding';
|
||||||
|
@ -378,28 +379,9 @@ export async function decryptAttachmentV2ToSink(
|
||||||
}),
|
}),
|
||||||
trimPadding(options.size),
|
trimPadding(options.size),
|
||||||
peekAndUpdateHash(plaintextHash),
|
peekAndUpdateHash(plaintextHash),
|
||||||
sink,
|
finalStream(() => {
|
||||||
].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 ourMac = hmac.digest();
|
const ourMac = hmac.digest();
|
||||||
const ourDigest = digest.digest();
|
const ourDigest = digest.digest();
|
||||||
const ourPlaintextHash = plaintextHash.digest('hex');
|
|
||||||
|
|
||||||
strictAssert(
|
strictAssert(
|
||||||
ourMac.byteLength === ATTACHMENT_MAC_LENGTH,
|
ourMac.byteLength === ATTACHMENT_MAC_LENGTH,
|
||||||
|
@ -413,10 +395,6 @@ export async function decryptAttachmentV2ToSink(
|
||||||
ourDigest.byteLength === DIGEST_LENGTH,
|
ourDigest.byteLength === DIGEST_LENGTH,
|
||||||
`${logId}: Failed to generate ourDigest!`
|
`${logId}: Failed to generate ourDigest!`
|
||||||
);
|
);
|
||||||
strictAssert(
|
|
||||||
ourPlaintextHash.length === HEX_DIGEST_LENGTH,
|
|
||||||
`${logId}: Failed to generate file hash!`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!constantTimeEqual(ourMac, theirMac)) {
|
if (!constantTimeEqual(ourMac, theirMac)) {
|
||||||
throw new Error(`${logId}: Bad MAC`);
|
throw new Error(`${logId}: Bad MAC`);
|
||||||
|
@ -439,12 +417,10 @@ export async function decryptAttachmentV2ToSink(
|
||||||
throw missingCaseError(type);
|
throw missingCaseError(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
strictAssert(
|
if (!outerEncryption) {
|
||||||
iv != null && iv.byteLength === IV_LENGTH,
|
return;
|
||||||
`${logId}: failed to find their iv`
|
}
|
||||||
);
|
|
||||||
|
|
||||||
if (outerEncryption) {
|
|
||||||
strictAssert(outerHmac, 'outerHmac must exist');
|
strictAssert(outerHmac, 'outerHmac must exist');
|
||||||
|
|
||||||
const ourOuterMac = outerHmac.digest();
|
const ourOuterMac = outerHmac.digest();
|
||||||
|
@ -461,8 +437,37 @@ export async function decryptAttachmentV2ToSink(
|
||||||
if (!constantTimeEqual(ourOuterMac, theirOuterMac)) {
|
if (!constantTimeEqual(ourOuterMac, theirOuterMac)) {
|
||||||
throw new Error(`${logId}: Bad outer encryption MAC`);
|
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 {
|
return {
|
||||||
iv,
|
iv,
|
||||||
plaintextHash: ourPlaintextHash,
|
plaintextHash: ourPlaintextHash,
|
||||||
|
|
40
ts/test-node/util/finalStream_test.ts
Normal file
40
ts/test-node/util/finalStream_test.ts
Normal 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
22
ts/util/finalStream.ts
Normal 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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue