Enable downloading attachments from backup CDN

This commit is contained in:
trevor-signal 2024-05-02 13:11:34 -04:00 committed by GitHub
parent 2964006b79
commit 1e8047cf73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 989 additions and 385 deletions

View file

@ -26,6 +26,7 @@ import type { AttachmentType } from './types/Attachment';
import type { ContextType } from './types/Message2';
import { strictAssert } from './util/assert';
import * as Errors from './types/errors';
import { isNotNil } from './util/isNotNil';
// This file was split from ts/Crypto.ts because it pulls things in from node, and
// too many things pull in Crypto.ts, so it broke storybook.
@ -58,10 +59,12 @@ export async function encryptAttachmentV2({
keys,
plaintextAbsolutePath,
dangerousTestOnlyIv,
dangerousTestOnlySkipPadding = false,
}: {
keys: Readonly<Uint8Array>;
plaintextAbsolutePath: string;
dangerousTestOnlyIv?: Readonly<Uint8Array>;
dangerousTestOnlySkipPadding?: boolean;
}): Promise<EncryptedAttachmentV2> {
const logId = 'encryptAttachmentV2';
@ -75,6 +78,14 @@ export async function encryptAttachmentV2({
if (dangerousTestOnlyIv && window.getEnvironment() !== Environment.Test) {
throw new Error(`${logId}: Used dangerousTestOnlyIv outside tests!`);
}
if (
dangerousTestOnlySkipPadding &&
window.getEnvironment() !== Environment.Test
) {
throw new Error(
`${logId}: Used dangerousTestOnlySkipPadding outside tests!`
);
}
const iv = dangerousTestOnlyIv || _generateAttachmentIv();
const plaintextHash = createHash(HashType.size256);
@ -96,14 +107,16 @@ export async function encryptAttachmentV2({
}
await pipeline(
readFd.createReadStream(),
peekAndUpdateHash(plaintextHash),
appendPaddingStream(),
createCipheriv(CipherType.AES256CBC, aesKey, iv),
prependIv(iv),
appendMacStream(macKey),
peekAndUpdateHash(digest),
writeFd.createWriteStream()
[
readFd.createReadStream(),
peekAndUpdateHash(plaintextHash),
dangerousTestOnlySkipPadding ? undefined : appendPaddingStream(),
createCipheriv(CipherType.AES256CBC, aesKey, iv),
prependIv(iv),
appendMacStream(macKey),
peekAndUpdateHash(digest),
writeFd.createWriteStream(),
].filter(isNotNil)
);
} catch (error) {
log.error(
@ -136,32 +149,59 @@ export async function encryptAttachmentV2({
};
}
export async function decryptAttachmentV2({
ciphertextPath,
id,
keys,
size,
theirDigest,
}: {
type DecryptAttachmentOptionsType = Readonly<{
ciphertextPath: string;
id: string;
keys: Readonly<Uint8Array>;
idForLogging: string;
aesKey: Readonly<Uint8Array>;
macKey: Readonly<Uint8Array>;
size: number;
theirDigest: Readonly<Uint8Array>;
}): Promise<DecryptedAttachmentV2> {
const logId = `decryptAttachmentV2(${id})`;
outerEncryption?: {
aesKey: Readonly<Uint8Array>;
macKey: Readonly<Uint8Array>;
};
}>;
export async function decryptAttachmentV2(
options: DecryptAttachmentOptionsType
): Promise<DecryptedAttachmentV2> {
const {
idForLogging,
macKey,
aesKey,
ciphertextPath,
theirDigest,
outerEncryption,
} = options;
const logId = `decryptAttachmentV2(${idForLogging})`;
// Create random output file
const relativeTargetPath = getRelativePath(createName());
const absoluteTargetPath =
window.Signal.Migrations.getAbsoluteAttachmentPath(relativeTargetPath);
const { aesKey, macKey } = splitKeys(keys);
const digest = createHash(HashType.size256);
const hmac = createHmac(HashType.size256, macKey);
const plaintextHash = createHash(HashType.size256);
let theirMac = null as Uint8Array | null; // TypeScript shenanigans
let theirMac: Uint8Array | undefined;
// When downloading from backup there is an outer encryption layer; in that case we
// need to decrypt the outer layer and check its MAC
let theirOuterMac: Uint8Array | undefined;
const outerHmac = outerEncryption
? createHmac(HashType.size256, outerEncryption.macKey)
: undefined;
const maybeOuterEncryptionGetIvAndDecipher = outerEncryption
? getIvAndDecipher(outerEncryption.aesKey)
: undefined;
const maybeOuterEncryptionGetMacAndUpdateMac = outerHmac
? getMacAndUpdateHmac(outerHmac, theirOuterMacValue => {
theirOuterMac = theirOuterMacValue;
})
: undefined;
let readFd;
let writeFd;
@ -179,15 +219,19 @@ export async function decryptAttachmentV2({
}
await pipeline(
readFd.createReadStream(),
peekAndUpdateHash(digest),
getMacAndUpdateHmac(hmac, theirMacValue => {
theirMac = theirMacValue;
}),
getIvAndDecipher(aesKey),
trimPadding(size),
peekAndUpdateHash(plaintextHash),
writeFd.createWriteStream()
[
readFd.createReadStream(),
maybeOuterEncryptionGetMacAndUpdateMac,
maybeOuterEncryptionGetIvAndDecipher,
peekAndUpdateHash(digest),
getMacAndUpdateHmac(hmac, theirMacValue => {
theirMac = theirMacValue;
}),
getIvAndDecipher(aesKey),
trimPadding(options.size),
peekAndUpdateHash(plaintextHash),
writeFd.createWriteStream(),
].filter(isNotNil)
);
} catch (error) {
log.error(
@ -224,11 +268,29 @@ export async function decryptAttachmentV2({
if (!constantTimeEqual(ourMac, theirMac)) {
throw new Error(`${logId}: Bad MAC`);
}
if (!constantTimeEqual(ourDigest, theirDigest)) {
throw new Error(`${logId}: Bad digest`);
}
if (outerEncryption) {
strictAssert(outerHmac, 'outerHmac must exist');
const ourOuterMac = outerHmac.digest();
strictAssert(
ourOuterMac.byteLength === ATTACHMENT_MAC_LENGTH,
`${logId}: Failed to generate ourOuterMac!`
);
strictAssert(
theirOuterMac != null &&
theirOuterMac.byteLength === ATTACHMENT_MAC_LENGTH,
`${logId}: Failed to find theirOuterMac!`
);
if (!constantTimeEqual(ourOuterMac, theirOuterMac)) {
throw new Error(`${logId}: Bad outer encryption MAC`);
}
}
return {
path: relativeTargetPath,
plaintextHash: ourPlaintextHash,
@ -238,7 +300,12 @@ export async function decryptAttachmentV2({
/**
* Splits the keys into aes and mac keys.
*/
function splitKeys(keys: Uint8Array) {
type AttachmentEncryptionKeysType = {
aesKey: Uint8Array;
macKey: Uint8Array;
};
export function splitKeys(keys: Uint8Array): AttachmentEncryptionKeysType {
strictAssert(
keys.byteLength === KEY_SET_LENGTH,
`attachment keys must be ${KEY_SET_LENGTH} bytes, got ${keys.byteLength}`
@ -376,10 +443,20 @@ function trimPadding(size: number) {
});
}
export function getAttachmentDownloadSize(size: number): number {
export function getAttachmentCiphertextLength(plaintextLength: number): number {
const paddedPlaintextSize = logPadSize(plaintextLength);
return (
// Multiply this by 1.05 to allow some variance
logPadSize(size) * 1.05 + IV_LENGTH + ATTACHMENT_MAC_LENGTH
IV_LENGTH +
getAesCbcCiphertextLength(paddedPlaintextSize) +
ATTACHMENT_MAC_LENGTH
);
}
export function getAesCbcCiphertextLength(plaintextLength: number): number {
const AES_CBC_BLOCK_SIZE = 16;
return (
(1 + Math.floor(plaintextLength / AES_CBC_BLOCK_SIZE)) * AES_CBC_BLOCK_SIZE
);
}