Use streams to download attachments directly to disk

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
Scott Nonnenberg 2023-10-30 09:24:28 -07:00 committed by GitHub
parent 2da49456c6
commit 99b2bc304e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 2297 additions and 356 deletions

View file

@ -1,8 +1,13 @@
// Copyright 2015 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { assert } from 'chai';
import { readFileSync, unlinkSync, writeFileSync } from 'fs';
import { join } from 'path';
import * as log from '../logging/log';
import * as Bytes from '../Bytes';
import * as Curve from '../Curve';
import {
@ -27,7 +32,12 @@ import {
hmacSha256,
verifyHmacSha256,
randomInt,
encryptAttachment,
decryptAttachmentV1,
padAndEncryptAttachment,
} from '../Crypto';
import { decryptAttachmentV2, encryptAttachmentV2 } from '../AttachmentCrypto';
import { createTempDir, deleteTempDir } from '../updater/common';
import { uuidToBytes, bytesToUuid } from '../util/uuidToBytes';
const BUCKET_SIZES = [
@ -586,4 +596,188 @@ describe('Crypto', () => {
assert.strictEqual(count, 0, failures.join('\n'));
});
});
describe('attachments', () => {
const FILE_PATH = join(__dirname, '../../fixtures/ghost-kitty.mp4');
const FILE_CONTENTS = readFileSync(FILE_PATH);
let tempDir: string | undefined;
beforeEach(async () => {
tempDir = await createTempDir();
});
afterEach(async () => {
if (tempDir) {
await deleteTempDir(log, tempDir);
}
});
it('v1 roundtrips (memory only)', () => {
const keys = getRandomBytes(64);
// Note: support for padding is not in decryptAttachmentV1, so we don't pad here
const encryptedAttachment = encryptAttachment({
plaintext: FILE_CONTENTS,
keys,
});
const plaintext = decryptAttachmentV1(
encryptedAttachment.ciphertext,
keys,
encryptedAttachment.digest
);
assert.isTrue(constantTimeEqual(FILE_CONTENTS, plaintext));
});
it('v1 -> v2 (memory -> disk)', async () => {
const keys = getRandomBytes(64);
const ciphertextPath = join(tempDir!, 'file');
let plaintextPath;
try {
const encryptedAttachment = padAndEncryptAttachment({
plaintext: FILE_CONTENTS,
keys,
});
writeFileSync(ciphertextPath, encryptedAttachment.ciphertext);
const plaintextRelativePath = await decryptAttachmentV2({
ciphertextPath,
id: 'test',
keys,
size: FILE_CONTENTS.byteLength,
theirDigest: encryptedAttachment.digest,
});
plaintextPath = window.Signal.Migrations.getAbsoluteAttachmentPath(
plaintextRelativePath
);
const plaintext = readFileSync(plaintextPath);
assert.isTrue(constantTimeEqual(FILE_CONTENTS, plaintext));
} finally {
if (plaintextPath) {
unlinkSync(plaintextPath);
}
}
});
it('v2 roundtrips (all on disk)', async () => {
const keys = getRandomBytes(64);
let plaintextPath;
let ciphertextPath;
try {
const encryptedAttachment = await encryptAttachmentV2({
keys,
plaintextAbsolutePath: FILE_PATH,
size: FILE_CONTENTS.byteLength,
});
ciphertextPath = window.Signal.Migrations.getAbsoluteAttachmentPath(
encryptedAttachment.path
);
const plaintextRelativePath = await decryptAttachmentV2({
ciphertextPath,
id: 'test',
keys,
size: FILE_CONTENTS.byteLength,
theirDigest: encryptedAttachment.digest,
});
plaintextPath = window.Signal.Migrations.getAbsoluteAttachmentPath(
plaintextRelativePath
);
const plaintext = readFileSync(plaintextPath);
assert.isTrue(constantTimeEqual(FILE_CONTENTS, plaintext));
} finally {
if (plaintextPath) {
unlinkSync(plaintextPath);
}
if (ciphertextPath) {
unlinkSync(ciphertextPath);
}
}
});
it('v2 -> v1 (disk -> memory)', async () => {
const keys = getRandomBytes(64);
let ciphertextPath;
try {
const encryptedAttachment = await encryptAttachmentV2({
keys,
plaintextAbsolutePath: FILE_PATH,
size: FILE_CONTENTS.byteLength,
});
ciphertextPath = window.Signal.Migrations.getAbsoluteAttachmentPath(
encryptedAttachment.path
);
const ciphertext = readFileSync(ciphertextPath);
const plaintext = decryptAttachmentV1(
ciphertext,
keys,
encryptedAttachment.digest
);
const IV = 16;
const MAC = 32;
const PADDING_FOR_GHOST_KITTY = 126_066; // delta between file size and next bucket
assert.strictEqual(
plaintext.byteLength,
FILE_CONTENTS.byteLength + IV + MAC + PADDING_FOR_GHOST_KITTY,
'verify padding'
);
// Note: support for padding is not in decryptAttachmentV1, so we manually unpad
const plaintextWithoutPadding = plaintext.subarray(
0,
FILE_CONTENTS.byteLength
);
assert.isTrue(
constantTimeEqual(FILE_CONTENTS, plaintextWithoutPadding)
);
} finally {
if (ciphertextPath) {
unlinkSync(ciphertextPath);
}
}
});
it('v1 and v2 produce the same ciphertext, given same iv', async () => {
const keys = getRandomBytes(64);
let ciphertextPath;
const dangerousTestOnlyIv = getRandomBytes(16);
try {
const encryptedAttachmentV1 = padAndEncryptAttachment({
plaintext: FILE_CONTENTS,
keys,
dangerousTestOnlyIv,
});
const ciphertextV1 = encryptedAttachmentV1.ciphertext;
const encryptedAttachmentV2 = await encryptAttachmentV2({
keys,
plaintextAbsolutePath: FILE_PATH,
size: FILE_CONTENTS.byteLength,
dangerousTestOnlyIv,
});
ciphertextPath = window.Signal.Migrations.getAbsoluteAttachmentPath(
encryptedAttachmentV2.path
);
const ciphertextV2 = readFileSync(ciphertextPath);
assert.strictEqual(ciphertextV1.byteLength, ciphertextV2.byteLength);
assert.isTrue(constantTimeEqual(ciphertextV1, ciphertextV2));
} finally {
if (ciphertextPath) {
unlinkSync(ciphertextPath);
}
}
});
});
});