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,
 | 
			
		||||
  getRandomBytes,
 | 
			
		||||
  getZeroes,
 | 
			
		||||
  sha256,
 | 
			
		||||
} from './Crypto';
 | 
			
		||||
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
 | 
			
		||||
//   too many things pull in Crypto.ts, so it broke storybook.
 | 
			
		||||
| 
						 | 
				
			
			@ -806,3 +809,57 @@ class AddMacTransform extends Transform {
 | 
			
		|||
    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 { DAY } from '../../util/durations';
 | 
			
		||||
import { migrateDataToFileSystem } from '../../util/attachments/migrateDataToFilesystem';
 | 
			
		||||
 | 
			
		||||
describe('Attachment', () => {
 | 
			
		||||
  describe('getFileExtension', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -420,6 +421,8 @@ describe('Attachment', () => {
 | 
			
		|||
        contentType: MIME.IMAGE_JPEG,
 | 
			
		||||
        path: 'abc/abcdefgh123456789',
 | 
			
		||||
        fileName: 'foo.jpg',
 | 
			
		||||
        plaintextHash:
 | 
			
		||||
          '9dac71e94805b04964a99011d74da584301362712570e98354d535c3cd3fdfca',
 | 
			
		||||
        size: 1111,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -429,7 +432,7 @@ describe('Attachment', () => {
 | 
			
		|||
        return 'abc/abcdefgh123456789';
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const actual = await Attachment.migrateDataToFileSystem(input, {
 | 
			
		||||
      const actual = await migrateDataToFileSystem(input, {
 | 
			
		||||
        writeNewAttachmentData,
 | 
			
		||||
        logger,
 | 
			
		||||
      });
 | 
			
		||||
| 
						 | 
				
			
			@ -451,7 +454,7 @@ describe('Attachment', () => {
 | 
			
		|||
 | 
			
		||||
      const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
 | 
			
		||||
 | 
			
		||||
      const actual = await Attachment.migrateDataToFileSystem(input, {
 | 
			
		||||
      const actual = await migrateDataToFileSystem(input, {
 | 
			
		||||
        writeNewAttachmentData,
 | 
			
		||||
        logger,
 | 
			
		||||
      });
 | 
			
		||||
| 
						 | 
				
			
			@ -469,7 +472,7 @@ describe('Attachment', () => {
 | 
			
		|||
 | 
			
		||||
      const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
 | 
			
		||||
 | 
			
		||||
      const actual = await Attachment.migrateDataToFileSystem(input, {
 | 
			
		||||
      const actual = await migrateDataToFileSystem(input, {
 | 
			
		||||
        writeNewAttachmentData,
 | 
			
		||||
        logger,
 | 
			
		||||
      });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -376,6 +376,8 @@ describe('Message', () => {
 | 
			
		|||
            path: 'abc/abcdefg',
 | 
			
		||||
            fileName: 'test\uFFFDfig.exe',
 | 
			
		||||
            size: 1111,
 | 
			
		||||
            plaintextHash:
 | 
			
		||||
              'f191b44995ef464dbf1943bc686008c08e95dab78cbdfe7bb5e257a8214d5b15',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        hasAttachments: 1,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,6 @@ import moment from 'moment';
 | 
			
		|||
import {
 | 
			
		||||
  isNumber,
 | 
			
		||||
  padStart,
 | 
			
		||||
  isTypedArray,
 | 
			
		||||
  isFunction,
 | 
			
		||||
  isUndefined,
 | 
			
		||||
  isString,
 | 
			
		||||
| 
						 | 
				
			
			@ -185,42 +184,6 @@ export type ThumbnailType = Pick<
 | 
			
		|||
  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
 | 
			
		||||
// {
 | 
			
		||||
//   id: string
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,11 +15,11 @@ import type {
 | 
			
		|||
  AttachmentType,
 | 
			
		||||
  AttachmentWithHydratedData,
 | 
			
		||||
  UploadedAttachmentType,
 | 
			
		||||
  migrateDataToFileSystem,
 | 
			
		||||
} from './Attachment';
 | 
			
		||||
import { toLogFormat } from './errors';
 | 
			
		||||
import type { LoggerType } from './Logging';
 | 
			
		||||
import type { ServiceIdString } from './ServiceId';
 | 
			
		||||
import type { migrateDataToFileSystem } from '../util/attachments/migrateDataToFilesystem';
 | 
			
		||||
 | 
			
		||||
type GenericEmbeddedContactType<AvatarType> = {
 | 
			
		||||
  name?: Name;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,6 @@ import { autoOrientJPEG } from '../util/attachments';
 | 
			
		|||
import {
 | 
			
		||||
  captureDimensionsAndScreenshot,
 | 
			
		||||
  hasData,
 | 
			
		||||
  migrateDataToFileSystem,
 | 
			
		||||
  removeSchemaVersion,
 | 
			
		||||
  replaceUnicodeOrderOverrides,
 | 
			
		||||
  replaceUnicodeV2,
 | 
			
		||||
| 
						 | 
				
			
			@ -34,6 +33,8 @@ import type {
 | 
			
		|||
  LinkPreviewWithHydratedData,
 | 
			
		||||
} from './message/LinkPreviews';
 | 
			
		||||
import type { StickerType, StickerWithHydratedData } from './Stickers';
 | 
			
		||||
import { addPlaintextHashToAttachment } from '../AttachmentCrypto';
 | 
			
		||||
import { migrateDataToFileSystem } from '../util/attachments/migrateDataToFilesystem';
 | 
			
		||||
 | 
			
		||||
export { hasExpiration } from './Message';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -118,6 +119,8 @@ export type ContextWithMessageType = ContextType & {
 | 
			
		|||
//     attachment filenames
 | 
			
		||||
// Version 10
 | 
			
		||||
//   - Preview: A new type of attachment can be included in a message.
 | 
			
		||||
// Version 11
 | 
			
		||||
//   - Attachments: add sha256 plaintextHash
 | 
			
		||||
 | 
			
		||||
const INITIAL_SCHEMA_VERSION = 0;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -438,6 +441,11 @@ const toVersion10 = _withSchemaVersion({
 | 
			
		|||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const toVersion11 = _withSchemaVersion({
 | 
			
		||||
  schemaVersion: 11,
 | 
			
		||||
  upgrade: _mapAttachments(addPlaintextHashToAttachment),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const VERSIONS = [
 | 
			
		||||
  toVersion0,
 | 
			
		||||
  toVersion1,
 | 
			
		||||
| 
						 | 
				
			
			@ -450,6 +458,7 @@ const VERSIONS = [
 | 
			
		|||
  toVersion8,
 | 
			
		||||
  toVersion9,
 | 
			
		||||
  toVersion10,
 | 
			
		||||
  toVersion11,
 | 
			
		||||
];
 | 
			
		||||
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
		Add a link
		
	
		Reference in a new issue