Add plaintext hash to existing message attachments
This commit is contained in:
parent
e28a07588e
commit
dcf52aa619
7 changed files with 122 additions and 42 deletions
|
@ -30,8 +30,11 @@ import {
|
||||||
getAttachmentSizeBucket,
|
getAttachmentSizeBucket,
|
||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
getZeroes,
|
getZeroes,
|
||||||
|
sha256,
|
||||||
} from './Crypto';
|
} from './Crypto';
|
||||||
import { Environment } from './environment';
|
import { Environment } from './environment';
|
||||||
|
import type { AttachmentType } from './types/Attachment';
|
||||||
|
import type { ContextType } from './types/Message2';
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -806,3 +809,57 @@ class AddMacTransform extends Transform {
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called during message schema migration. New messages downloaded should have
|
||||||
|
// plaintextHash added automatically during decryption / writing to file system.
|
||||||
|
export async function addPlaintextHashToAttachment(
|
||||||
|
attachment: AttachmentType,
|
||||||
|
{ getAbsoluteAttachmentPath }: ContextType
|
||||||
|
): Promise<AttachmentType> {
|
||||||
|
if (!attachment.path) {
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plaintextHash = await getPlaintextHashForAttachmentOnDisk(
|
||||||
|
getAbsoluteAttachmentPath(attachment.path)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!plaintextHash) {
|
||||||
|
log.error('addPlaintextHashToAttachment: Failed to generate hash');
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...attachment,
|
||||||
|
plaintextHash,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPlaintextHashForAttachmentOnDisk(
|
||||||
|
absolutePath: string
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const readStream = createReadStream(absolutePath);
|
||||||
|
const hash = createHash(HashType.size256);
|
||||||
|
try {
|
||||||
|
await pipeline(readStream, hash);
|
||||||
|
const plaintextHash = hash.digest();
|
||||||
|
if (!plaintextHash) {
|
||||||
|
log.error(
|
||||||
|
'addPlaintextHashToAttachment: no hash generated from file; is the file empty?'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return Buffer.from(plaintextHash).toString('hex');
|
||||||
|
} catch (error) {
|
||||||
|
log.error('addPlaintextHashToAttachment: error during file read', error);
|
||||||
|
return undefined;
|
||||||
|
} finally {
|
||||||
|
readStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlaintextHashForInMemoryAttachment(
|
||||||
|
data: Uint8Array
|
||||||
|
): string {
|
||||||
|
return Buffer.from(sha256(data)).toString('hex');
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import * as logger from '../../logging/log';
|
||||||
|
|
||||||
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
|
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
|
||||||
import { DAY } from '../../util/durations';
|
import { DAY } from '../../util/durations';
|
||||||
|
import { migrateDataToFileSystem } from '../../util/attachments/migrateDataToFilesystem';
|
||||||
|
|
||||||
describe('Attachment', () => {
|
describe('Attachment', () => {
|
||||||
describe('getFileExtension', () => {
|
describe('getFileExtension', () => {
|
||||||
|
@ -420,6 +421,8 @@ describe('Attachment', () => {
|
||||||
contentType: MIME.IMAGE_JPEG,
|
contentType: MIME.IMAGE_JPEG,
|
||||||
path: 'abc/abcdefgh123456789',
|
path: 'abc/abcdefgh123456789',
|
||||||
fileName: 'foo.jpg',
|
fileName: 'foo.jpg',
|
||||||
|
plaintextHash:
|
||||||
|
'9dac71e94805b04964a99011d74da584301362712570e98354d535c3cd3fdfca',
|
||||||
size: 1111,
|
size: 1111,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -429,7 +432,7 @@ describe('Attachment', () => {
|
||||||
return 'abc/abcdefgh123456789';
|
return 'abc/abcdefgh123456789';
|
||||||
};
|
};
|
||||||
|
|
||||||
const actual = await Attachment.migrateDataToFileSystem(input, {
|
const actual = await migrateDataToFileSystem(input, {
|
||||||
writeNewAttachmentData,
|
writeNewAttachmentData,
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
|
@ -451,7 +454,7 @@ describe('Attachment', () => {
|
||||||
|
|
||||||
const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
|
const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
|
||||||
|
|
||||||
const actual = await Attachment.migrateDataToFileSystem(input, {
|
const actual = await migrateDataToFileSystem(input, {
|
||||||
writeNewAttachmentData,
|
writeNewAttachmentData,
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
|
@ -469,7 +472,7 @@ describe('Attachment', () => {
|
||||||
|
|
||||||
const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
|
const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
|
||||||
|
|
||||||
const actual = await Attachment.migrateDataToFileSystem(input, {
|
const actual = await migrateDataToFileSystem(input, {
|
||||||
writeNewAttachmentData,
|
writeNewAttachmentData,
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
|
|
|
@ -376,6 +376,8 @@ describe('Message', () => {
|
||||||
path: 'abc/abcdefg',
|
path: 'abc/abcdefg',
|
||||||
fileName: 'test\uFFFDfig.exe',
|
fileName: 'test\uFFFDfig.exe',
|
||||||
size: 1111,
|
size: 1111,
|
||||||
|
plaintextHash:
|
||||||
|
'f191b44995ef464dbf1943bc686008c08e95dab78cbdfe7bb5e257a8214d5b15',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
hasAttachments: 1,
|
hasAttachments: 1,
|
||||||
|
|
|
@ -5,7 +5,6 @@ import moment from 'moment';
|
||||||
import {
|
import {
|
||||||
isNumber,
|
isNumber,
|
||||||
padStart,
|
padStart,
|
||||||
isTypedArray,
|
|
||||||
isFunction,
|
isFunction,
|
||||||
isUndefined,
|
isUndefined,
|
||||||
isString,
|
isString,
|
||||||
|
@ -185,42 +184,6 @@ export type ThumbnailType = Pick<
|
||||||
objectUrl?: string;
|
objectUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function migrateDataToFileSystem(
|
|
||||||
attachment: AttachmentType,
|
|
||||||
{
|
|
||||||
writeNewAttachmentData,
|
|
||||||
logger,
|
|
||||||
}: {
|
|
||||||
writeNewAttachmentData: (data: Uint8Array) => Promise<string>;
|
|
||||||
logger: LoggerType;
|
|
||||||
}
|
|
||||||
): Promise<AttachmentType> {
|
|
||||||
if (!isFunction(writeNewAttachmentData)) {
|
|
||||||
throw new TypeError("'writeNewAttachmentData' must be a function");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = attachment;
|
|
||||||
const attachmentHasData = !isUndefined(data);
|
|
||||||
const shouldSkipSchemaUpgrade = !attachmentHasData;
|
|
||||||
|
|
||||||
if (shouldSkipSchemaUpgrade) {
|
|
||||||
return attachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This attachment was already broken by a roundtrip to the database - repair it now
|
|
||||||
if (!isTypedArray(data)) {
|
|
||||||
logger.warn(
|
|
||||||
'migrateDataToFileSystem: Attachment had non-array `data` field; deleting.'
|
|
||||||
);
|
|
||||||
return omit({ ...attachment }, ['data']);
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = await writeNewAttachmentData(data);
|
|
||||||
|
|
||||||
const attachmentWithoutData = omit({ ...attachment, path }, ['data']);
|
|
||||||
return attachmentWithoutData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// // Incoming message attachment fields
|
// // Incoming message attachment fields
|
||||||
// {
|
// {
|
||||||
// id: string
|
// id: string
|
||||||
|
|
|
@ -15,11 +15,11 @@ import type {
|
||||||
AttachmentType,
|
AttachmentType,
|
||||||
AttachmentWithHydratedData,
|
AttachmentWithHydratedData,
|
||||||
UploadedAttachmentType,
|
UploadedAttachmentType,
|
||||||
migrateDataToFileSystem,
|
|
||||||
} from './Attachment';
|
} from './Attachment';
|
||||||
import { toLogFormat } from './errors';
|
import { toLogFormat } from './errors';
|
||||||
import type { LoggerType } from './Logging';
|
import type { LoggerType } from './Logging';
|
||||||
import type { ServiceIdString } from './ServiceId';
|
import type { ServiceIdString } from './ServiceId';
|
||||||
|
import type { migrateDataToFileSystem } from '../util/attachments/migrateDataToFilesystem';
|
||||||
|
|
||||||
type GenericEmbeddedContactType<AvatarType> = {
|
type GenericEmbeddedContactType<AvatarType> = {
|
||||||
name?: Name;
|
name?: Name;
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { autoOrientJPEG } from '../util/attachments';
|
||||||
import {
|
import {
|
||||||
captureDimensionsAndScreenshot,
|
captureDimensionsAndScreenshot,
|
||||||
hasData,
|
hasData,
|
||||||
migrateDataToFileSystem,
|
|
||||||
removeSchemaVersion,
|
removeSchemaVersion,
|
||||||
replaceUnicodeOrderOverrides,
|
replaceUnicodeOrderOverrides,
|
||||||
replaceUnicodeV2,
|
replaceUnicodeV2,
|
||||||
|
@ -34,6 +33,8 @@ import type {
|
||||||
LinkPreviewWithHydratedData,
|
LinkPreviewWithHydratedData,
|
||||||
} from './message/LinkPreviews';
|
} from './message/LinkPreviews';
|
||||||
import type { StickerType, StickerWithHydratedData } from './Stickers';
|
import type { StickerType, StickerWithHydratedData } from './Stickers';
|
||||||
|
import { addPlaintextHashToAttachment } from '../AttachmentCrypto';
|
||||||
|
import { migrateDataToFileSystem } from '../util/attachments/migrateDataToFilesystem';
|
||||||
|
|
||||||
export { hasExpiration } from './Message';
|
export { hasExpiration } from './Message';
|
||||||
|
|
||||||
|
@ -118,6 +119,8 @@ export type ContextWithMessageType = ContextType & {
|
||||||
// attachment filenames
|
// attachment filenames
|
||||||
// Version 10
|
// Version 10
|
||||||
// - Preview: A new type of attachment can be included in a message.
|
// - Preview: A new type of attachment can be included in a message.
|
||||||
|
// Version 11
|
||||||
|
// - Attachments: add sha256 plaintextHash
|
||||||
|
|
||||||
const INITIAL_SCHEMA_VERSION = 0;
|
const INITIAL_SCHEMA_VERSION = 0;
|
||||||
|
|
||||||
|
@ -438,6 +441,11 @@ const toVersion10 = _withSchemaVersion({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toVersion11 = _withSchemaVersion({
|
||||||
|
schemaVersion: 11,
|
||||||
|
upgrade: _mapAttachments(addPlaintextHashToAttachment),
|
||||||
|
});
|
||||||
|
|
||||||
const VERSIONS = [
|
const VERSIONS = [
|
||||||
toVersion0,
|
toVersion0,
|
||||||
toVersion1,
|
toVersion1,
|
||||||
|
@ -450,6 +458,7 @@ const VERSIONS = [
|
||||||
toVersion8,
|
toVersion8,
|
||||||
toVersion9,
|
toVersion9,
|
||||||
toVersion10,
|
toVersion10,
|
||||||
|
toVersion11,
|
||||||
];
|
];
|
||||||
export const CURRENT_SCHEMA_VERSION = VERSIONS.length - 1;
|
export const CURRENT_SCHEMA_VERSION = VERSIONS.length - 1;
|
||||||
|
|
||||||
|
|
46
ts/util/attachments/migrateDataToFilesystem.ts
Normal file
46
ts/util/attachments/migrateDataToFilesystem.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { isFunction, isTypedArray, isUndefined, omit } from 'lodash';
|
||||||
|
import type { AttachmentType } from '../../types/Attachment';
|
||||||
|
import type { LoggerType } from '../../types/Logging';
|
||||||
|
import { getPlaintextHashForInMemoryAttachment } from '../../AttachmentCrypto';
|
||||||
|
|
||||||
|
export async function migrateDataToFileSystem(
|
||||||
|
attachment: AttachmentType,
|
||||||
|
{
|
||||||
|
writeNewAttachmentData,
|
||||||
|
logger,
|
||||||
|
}: {
|
||||||
|
writeNewAttachmentData: (data: Uint8Array) => Promise<string>;
|
||||||
|
logger: LoggerType;
|
||||||
|
}
|
||||||
|
): Promise<AttachmentType> {
|
||||||
|
if (!isFunction(writeNewAttachmentData)) {
|
||||||
|
throw new TypeError("'writeNewAttachmentData' must be a function");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = attachment;
|
||||||
|
const attachmentHasData = !isUndefined(data);
|
||||||
|
const shouldSkipSchemaUpgrade = !attachmentHasData;
|
||||||
|
|
||||||
|
if (shouldSkipSchemaUpgrade) {
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This attachment was already broken by a roundtrip to the database - repair it now
|
||||||
|
if (!isTypedArray(data)) {
|
||||||
|
logger.warn(
|
||||||
|
'migrateDataToFileSystem: Attachment had non-array `data` field; deleting.'
|
||||||
|
);
|
||||||
|
return omit({ ...attachment }, ['data']);
|
||||||
|
}
|
||||||
|
|
||||||
|
const plaintextHash = getPlaintextHashForInMemoryAttachment(data);
|
||||||
|
const path = await writeNewAttachmentData(data);
|
||||||
|
|
||||||
|
const attachmentWithoutData = omit({ ...attachment, path, plaintextHash }, [
|
||||||
|
'data',
|
||||||
|
]);
|
||||||
|
return attachmentWithoutData;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue