Revert "Store IV when encrypting or decrypting attachments"
Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
parent
fe42ba093f
commit
20adc4c74e
6 changed files with 11 additions and 136 deletions
|
@ -27,7 +27,6 @@ import type { ContextType } from './types/Message2';
|
||||||
import { strictAssert } from './util/assert';
|
import { strictAssert } from './util/assert';
|
||||||
import * as Errors from './types/errors';
|
import * as Errors from './types/errors';
|
||||||
import { isNotNil } from './util/isNotNil';
|
import { isNotNil } from './util/isNotNil';
|
||||||
import { missingCaseError } from './util/missingCaseError';
|
|
||||||
|
|
||||||
// This file was split from ts/Crypto.ts because it pulls things in from node, and
|
// 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.
|
// too many things pull in Crypto.ts, so it broke storybook.
|
||||||
|
@ -47,14 +46,12 @@ export function _generateAttachmentIv(): Uint8Array {
|
||||||
|
|
||||||
export type EncryptedAttachmentV2 = {
|
export type EncryptedAttachmentV2 = {
|
||||||
digest: Uint8Array;
|
digest: Uint8Array;
|
||||||
iv: Uint8Array;
|
|
||||||
plaintextHash: string;
|
plaintextHash: string;
|
||||||
ciphertextSize: number;
|
ciphertextSize: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DecryptedAttachmentV2 = {
|
export type DecryptedAttachmentV2 = {
|
||||||
path: string;
|
path: string;
|
||||||
iv: Uint8Array;
|
|
||||||
plaintextHash: string;
|
plaintextHash: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -62,21 +59,10 @@ export type PlaintextSourceType =
|
||||||
| { data: Uint8Array }
|
| { data: Uint8Array }
|
||||||
| { absolutePath: string };
|
| { absolutePath: string };
|
||||||
|
|
||||||
export type HardcodedIVForEncryptionType =
|
|
||||||
| {
|
|
||||||
reason: 'test';
|
|
||||||
iv: Uint8Array;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
reason: 'reencrypting-for-backup';
|
|
||||||
iv: Uint8Array;
|
|
||||||
digestToMatch: Uint8Array;
|
|
||||||
};
|
|
||||||
|
|
||||||
type EncryptAttachmentV2PropsType = {
|
type EncryptAttachmentV2PropsType = {
|
||||||
plaintext: PlaintextSourceType;
|
plaintext: PlaintextSourceType;
|
||||||
keys: Readonly<Uint8Array>;
|
keys: Readonly<Uint8Array>;
|
||||||
dangerousIv?: HardcodedIVForEncryptionType;
|
dangerousTestOnlyIv?: Readonly<Uint8Array>;
|
||||||
dangerousTestOnlySkipPadding?: boolean;
|
dangerousTestOnlySkipPadding?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -110,7 +96,7 @@ export async function encryptAttachmentV2ToDisk(
|
||||||
export async function encryptAttachmentV2({
|
export async function encryptAttachmentV2({
|
||||||
keys,
|
keys,
|
||||||
plaintext,
|
plaintext,
|
||||||
dangerousIv,
|
dangerousTestOnlyIv,
|
||||||
dangerousTestOnlySkipPadding,
|
dangerousTestOnlySkipPadding,
|
||||||
sink,
|
sink,
|
||||||
}: EncryptAttachmentV2PropsType & {
|
}: EncryptAttachmentV2PropsType & {
|
||||||
|
@ -120,26 +106,9 @@ export async function encryptAttachmentV2({
|
||||||
|
|
||||||
const { aesKey, macKey } = splitKeys(keys);
|
const { aesKey, macKey } = splitKeys(keys);
|
||||||
|
|
||||||
if (dangerousIv) {
|
if (dangerousTestOnlyIv && window.getEnvironment() !== Environment.Test) {
|
||||||
if (dangerousIv.reason === 'test') {
|
throw new Error(`${logId}: Used dangerousTestOnlyIv outside tests!`);
|
||||||
if (window.getEnvironment() !== Environment.Test) {
|
|
||||||
throw new Error(
|
|
||||||
`${logId}: Used dangerousIv with reason test outside tests!`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (dangerousIv.reason === 'reencrypting-for-backup') {
|
|
||||||
strictAssert(
|
|
||||||
dangerousIv.digestToMatch.byteLength === DIGEST_LENGTH,
|
|
||||||
`${logId}: Must provide valid digest to match if providing iv for re-encryption`
|
|
||||||
);
|
|
||||||
log.info(
|
|
||||||
`${logId}: using hardcoded iv because we are re-encrypting for backup`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw missingCaseError(dangerousIv);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
dangerousTestOnlySkipPadding &&
|
dangerousTestOnlySkipPadding &&
|
||||||
window.getEnvironment() !== Environment.Test
|
window.getEnvironment() !== Environment.Test
|
||||||
|
@ -148,8 +117,7 @@ export async function encryptAttachmentV2({
|
||||||
`${logId}: Used dangerousTestOnlySkipPadding outside tests!`
|
`${logId}: Used dangerousTestOnlySkipPadding outside tests!`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const iv = dangerousTestOnlyIv || _generateAttachmentIv();
|
||||||
const iv = dangerousIv?.iv || _generateAttachmentIv();
|
|
||||||
const plaintextHash = createHash(HashType.size256);
|
const plaintextHash = createHash(HashType.size256);
|
||||||
const digest = createHash(HashType.size256);
|
const digest = createHash(HashType.size256);
|
||||||
|
|
||||||
|
@ -199,16 +167,8 @@ export async function encryptAttachmentV2({
|
||||||
|
|
||||||
strictAssert(ciphertextSize != null, 'Failed to measure ciphertext size!');
|
strictAssert(ciphertextSize != null, 'Failed to measure ciphertext size!');
|
||||||
|
|
||||||
if (dangerousIv?.reason === 'reencrypting-for-backup') {
|
|
||||||
if (!constantTimeEqual(ourDigest, dangerousIv.digestToMatch)) {
|
|
||||||
throw new Error(
|
|
||||||
`${logId}: iv was hardcoded for backup re-encryption, but digest does not match`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
digest: ourDigest,
|
digest: ourDigest,
|
||||||
iv,
|
|
||||||
plaintextHash: ourPlaintextHash,
|
plaintextHash: ourPlaintextHash,
|
||||||
ciphertextSize,
|
ciphertextSize,
|
||||||
};
|
};
|
||||||
|
@ -270,7 +230,6 @@ export async function decryptAttachmentV2(
|
||||||
|
|
||||||
let readFd;
|
let readFd;
|
||||||
let writeFd;
|
let writeFd;
|
||||||
let iv: Uint8Array | undefined;
|
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
readFd = await open(ciphertextPath, 'r');
|
readFd = await open(ciphertextPath, 'r');
|
||||||
|
@ -293,9 +252,7 @@ export async function decryptAttachmentV2(
|
||||||
getMacAndUpdateHmac(hmac, theirMacValue => {
|
getMacAndUpdateHmac(hmac, theirMacValue => {
|
||||||
theirMac = theirMacValue;
|
theirMac = theirMacValue;
|
||||||
}),
|
}),
|
||||||
getIvAndDecipher(aesKey, theirIv => {
|
getIvAndDecipher(aesKey),
|
||||||
iv = theirIv;
|
|
||||||
}),
|
|
||||||
trimPadding(options.size),
|
trimPadding(options.size),
|
||||||
peekAndUpdateHash(plaintextHash),
|
peekAndUpdateHash(plaintextHash),
|
||||||
writeFd.createWriteStream(),
|
writeFd.createWriteStream(),
|
||||||
|
@ -340,11 +297,6 @@ export async function decryptAttachmentV2(
|
||||||
throw new Error(`${logId}: Bad digest`);
|
throw new Error(`${logId}: Bad digest`);
|
||||||
}
|
}
|
||||||
|
|
||||||
strictAssert(
|
|
||||||
iv != null && iv.byteLength === IV_LENGTH,
|
|
||||||
`${logId}: failed to find their iv`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (outerEncryption) {
|
if (outerEncryption) {
|
||||||
strictAssert(outerHmac, 'outerHmac must exist');
|
strictAssert(outerHmac, 'outerHmac must exist');
|
||||||
|
|
||||||
|
@ -366,7 +318,6 @@ export async function decryptAttachmentV2(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: relativeTargetPath,
|
path: relativeTargetPath,
|
||||||
iv,
|
|
||||||
plaintextHash: ourPlaintextHash,
|
plaintextHash: ourPlaintextHash,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -453,10 +404,7 @@ export function getMacAndUpdateHmac(
|
||||||
* Gets the IV from the start of the stream and creates a decipher.
|
* Gets the IV from the start of the stream and creates a decipher.
|
||||||
* Then deciphers the rest of the stream.
|
* Then deciphers the rest of the stream.
|
||||||
*/
|
*/
|
||||||
export function getIvAndDecipher(
|
export function getIvAndDecipher(aesKey: Uint8Array): Transform {
|
||||||
aesKey: Uint8Array,
|
|
||||||
onFoundIv?: (iv: Buffer) => void
|
|
||||||
): Transform {
|
|
||||||
let maybeIvBytes: Buffer | null = Buffer.alloc(0);
|
let maybeIvBytes: Buffer | null = Buffer.alloc(0);
|
||||||
let decipher: Decipher | null = null;
|
let decipher: Decipher | null = null;
|
||||||
return new Transform({
|
return new Transform({
|
||||||
|
@ -480,7 +428,6 @@ export function getIvAndDecipher(
|
||||||
// remainder of the bytes through.
|
// remainder of the bytes through.
|
||||||
const iv = maybeIvBytes.subarray(0, IV_LENGTH);
|
const iv = maybeIvBytes.subarray(0, IV_LENGTH);
|
||||||
const remainder = maybeIvBytes.subarray(IV_LENGTH);
|
const remainder = maybeIvBytes.subarray(IV_LENGTH);
|
||||||
onFoundIv?.(iv);
|
|
||||||
maybeIvBytes = null; // free memory
|
maybeIvBytes = null; // free memory
|
||||||
decipher = createDecipheriv(CipherType.AES256CBC, aesKey, iv);
|
decipher = createDecipheriv(CipherType.AES256CBC, aesKey, iv);
|
||||||
callback(null, decipher.update(remainder));
|
callback(null, decipher.update(remainder));
|
||||||
|
|
|
@ -37,7 +37,6 @@ import {
|
||||||
CipherType,
|
CipherType,
|
||||||
} from '../Crypto';
|
} from '../Crypto';
|
||||||
import {
|
import {
|
||||||
type HardcodedIVForEncryptionType,
|
|
||||||
KEY_SET_LENGTH,
|
KEY_SET_LENGTH,
|
||||||
_generateAttachmentIv,
|
_generateAttachmentIv,
|
||||||
decryptAttachmentV2,
|
decryptAttachmentV2,
|
||||||
|
@ -609,24 +608,19 @@ describe('Crypto', () => {
|
||||||
path,
|
path,
|
||||||
data,
|
data,
|
||||||
plaintextHash,
|
plaintextHash,
|
||||||
encryptionKeys,
|
|
||||||
dangerousIv,
|
|
||||||
}: {
|
}: {
|
||||||
path?: string;
|
path?: string;
|
||||||
data: Uint8Array;
|
data: Uint8Array;
|
||||||
plaintextHash: Uint8Array;
|
plaintextHash: Uint8Array;
|
||||||
encryptionKeys?: Uint8Array;
|
|
||||||
dangerousIv?: HardcodedIVForEncryptionType;
|
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
let plaintextPath;
|
let plaintextPath;
|
||||||
let ciphertextPath;
|
let ciphertextPath;
|
||||||
const keys = encryptionKeys ?? generateAttachmentKeys();
|
const keys = generateAttachmentKeys();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const encryptedAttachment = await encryptAttachmentV2ToDisk({
|
const encryptedAttachment = await encryptAttachmentV2ToDisk({
|
||||||
keys,
|
keys,
|
||||||
plaintext: path ? { absolutePath: path } : { data },
|
plaintext: path ? { absolutePath: path } : { data },
|
||||||
dangerousIv,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ciphertextPath = window.Signal.Migrations.getAbsoluteAttachmentPath(
|
ciphertextPath = window.Signal.Migrations.getAbsoluteAttachmentPath(
|
||||||
|
@ -645,21 +639,6 @@ describe('Crypto', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const plaintext = readFileSync(plaintextPath);
|
const plaintext = readFileSync(plaintextPath);
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
|
||||||
encryptedAttachment.iv,
|
|
||||||
decryptedAttachment.iv
|
|
||||||
);
|
|
||||||
if (dangerousIv) {
|
|
||||||
assert.deepStrictEqual(encryptedAttachment.iv, dangerousIv.iv);
|
|
||||||
if (dangerousIv.reason === 'reencrypting-for-backup') {
|
|
||||||
assert.deepStrictEqual(
|
|
||||||
encryptedAttachment.digest,
|
|
||||||
dangerousIv.digestToMatch
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.isTrue(constantTimeEqual(data, plaintext));
|
assert.isTrue(constantTimeEqual(data, plaintext));
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
encryptedAttachment.ciphertextSize,
|
encryptedAttachment.ciphertextSize,
|
||||||
|
@ -732,52 +711,6 @@ describe('Crypto', () => {
|
||||||
plaintextHash,
|
plaintextHash,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('dangerousIv', () => {
|
|
||||||
it('uses hardcodedIv in tests', async () => {
|
|
||||||
await testV2RoundTripData({
|
|
||||||
data: FILE_CONTENTS,
|
|
||||||
plaintextHash: FILE_HASH,
|
|
||||||
dangerousIv: {
|
|
||||||
reason: 'test',
|
|
||||||
iv: _generateAttachmentIv(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses hardcodedIv when re-encrypting for backup', async () => {
|
|
||||||
const keys = generateAttachmentKeys();
|
|
||||||
const previouslyEncrypted = await encryptAttachmentV2ToDisk({
|
|
||||||
keys,
|
|
||||||
plaintext: { data: FILE_CONTENTS },
|
|
||||||
});
|
|
||||||
|
|
||||||
await testV2RoundTripData({
|
|
||||||
data: FILE_CONTENTS,
|
|
||||||
plaintextHash: FILE_HASH,
|
|
||||||
encryptionKeys: keys,
|
|
||||||
dangerousIv: {
|
|
||||||
reason: 'reencrypting-for-backup',
|
|
||||||
iv: previouslyEncrypted.iv,
|
|
||||||
digestToMatch: previouslyEncrypted.digest,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// If the digest is wrong, it should throw
|
|
||||||
await assert.isRejected(
|
|
||||||
testV2RoundTripData({
|
|
||||||
data: FILE_CONTENTS,
|
|
||||||
plaintextHash: FILE_HASH,
|
|
||||||
encryptionKeys: keys,
|
|
||||||
dangerousIv: {
|
|
||||||
reason: 'reencrypting-for-backup',
|
|
||||||
iv: previouslyEncrypted.iv,
|
|
||||||
digestToMatch: getRandomBytes(32),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
'iv was hardcoded for backup re-encryption, but digest does not match'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('v2 -> v1 (disk -> memory)', async () => {
|
it('v2 -> v1 (disk -> memory)', async () => {
|
||||||
|
@ -841,7 +774,7 @@ describe('Crypto', () => {
|
||||||
const encryptedAttachmentV2 = await encryptAttachmentV2ToDisk({
|
const encryptedAttachmentV2 = await encryptAttachmentV2ToDisk({
|
||||||
keys,
|
keys,
|
||||||
plaintext: { absolutePath: FILE_PATH },
|
plaintext: { absolutePath: FILE_PATH },
|
||||||
dangerousIv: { iv: dangerousTestOnlyIv, reason: 'test' },
|
dangerousTestOnlyIv,
|
||||||
});
|
});
|
||||||
ciphertextPath = window.Signal.Migrations.getAbsoluteAttachmentPath(
|
ciphertextPath = window.Signal.Migrations.getAbsoluteAttachmentPath(
|
||||||
encryptedAttachmentV2.path
|
encryptedAttachmentV2.path
|
||||||
|
|
|
@ -132,7 +132,7 @@ export async function downloadAttachment(
|
||||||
window.Signal.Migrations.getAbsoluteAttachmentPath(downloadedPath);
|
window.Signal.Migrations.getAbsoluteAttachmentPath(downloadedPath);
|
||||||
|
|
||||||
const { aesKey, macKey } = splitKeys(Bytes.fromBase64(key));
|
const { aesKey, macKey } = splitKeys(Bytes.fromBase64(key));
|
||||||
const { path, plaintextHash, iv } = await decryptAttachmentV2({
|
const { path, plaintextHash } = await decryptAttachmentV2({
|
||||||
ciphertextPath: cipherTextAbsolutePath,
|
ciphertextPath: cipherTextAbsolutePath,
|
||||||
idForLogging: logId,
|
idForLogging: logId,
|
||||||
aesKey,
|
aesKey,
|
||||||
|
@ -155,7 +155,6 @@ export async function downloadAttachment(
|
||||||
? MIME.stringToMIMEType(contentType)
|
? MIME.stringToMIMEType(contentType)
|
||||||
: MIME.APPLICATION_OCTET_STREAM,
|
: MIME.APPLICATION_OCTET_STREAM,
|
||||||
plaintextHash,
|
plaintextHash,
|
||||||
iv: Bytes.toBase64(iv),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,6 @@ export type AttachmentType = {
|
||||||
cdnId?: string;
|
cdnId?: string;
|
||||||
cdnKey?: string;
|
cdnKey?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
iv?: string;
|
|
||||||
data?: Uint8Array;
|
data?: Uint8Array;
|
||||||
textAttachment?: TextAttachmentType;
|
textAttachment?: TextAttachmentType;
|
||||||
wasTooBig?: boolean;
|
wasTooBig?: boolean;
|
||||||
|
@ -98,7 +97,6 @@ export type UploadedAttachmentType = Proto.IAttachmentPointer &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
// Required fields
|
// Required fields
|
||||||
cdnKey: string;
|
cdnKey: string;
|
||||||
iv: Uint8Array;
|
|
||||||
key: Uint8Array;
|
key: Uint8Array;
|
||||||
size: number;
|
size: number;
|
||||||
digest: Uint8Array;
|
digest: Uint8Array;
|
||||||
|
|
|
@ -77,7 +77,7 @@ export const downscaleOutgoingAttachment = async (
|
||||||
|
|
||||||
export type CdnFieldsType = Pick<
|
export type CdnFieldsType = Pick<
|
||||||
AttachmentType,
|
AttachmentType,
|
||||||
'cdnId' | 'cdnKey' | 'cdnNumber' | 'key' | 'digest' | 'iv' | 'plaintextHash'
|
'cdnId' | 'cdnKey' | 'cdnNumber' | 'key' | 'digest' | 'plaintextHash'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export function copyCdnFields(
|
export function copyCdnFields(
|
||||||
|
@ -91,7 +91,6 @@ export function copyCdnFields(
|
||||||
cdnKey: uploaded.cdnKey,
|
cdnKey: uploaded.cdnKey,
|
||||||
cdnNumber: dropNull(uploaded.cdnNumber),
|
cdnNumber: dropNull(uploaded.cdnNumber),
|
||||||
key: Bytes.toBase64(uploaded.key),
|
key: Bytes.toBase64(uploaded.key),
|
||||||
iv: Bytes.toBase64(uploaded.iv),
|
|
||||||
digest: Bytes.toBase64(uploaded.digest),
|
digest: Bytes.toBase64(uploaded.digest),
|
||||||
plaintextHash: uploaded.plaintextHash,
|
plaintextHash: uploaded.plaintextHash,
|
||||||
};
|
};
|
||||||
|
|
|
@ -40,7 +40,6 @@ export async function uploadAttachment(
|
||||||
cdnKey,
|
cdnKey,
|
||||||
cdnNumber,
|
cdnNumber,
|
||||||
key: keys,
|
key: keys,
|
||||||
iv: encrypted.iv,
|
|
||||||
size: attachment.data.byteLength,
|
size: attachment.data.byteLength,
|
||||||
digest: encrypted.digest,
|
digest: encrypted.digest,
|
||||||
plaintextHash: encrypted.plaintextHash,
|
plaintextHash: encrypted.plaintextHash,
|
||||||
|
|
Loading…
Reference in a new issue