Generate mediaName for backed-up attachments

This commit is contained in:
trevor-signal 2024-03-06 13:15:10 -05:00 committed by GitHub
parent db623d13b2
commit cf381cd46c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 171 additions and 2 deletions

View file

@ -525,7 +525,7 @@ export async function addPlaintextHashToAttachment(
}; };
} }
async function getPlaintextHashForAttachmentOnDisk( export async function getPlaintextHashForAttachmentOnDisk(
absolutePath: string absolutePath: string
): Promise<string | undefined> { ): Promise<string | undefined> {
let readFd; let readFd;

View file

@ -0,0 +1,102 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import * as sinon from 'sinon';
import { getMediaNameForBackup } from '../../util/attachments/getMediaNameForBackup';
import { IMAGE_PNG } from '../../types/MIME';
import { sha256 } from '../../Crypto';
import { DAY } from '../../util/durations';
describe('getMediaNameForBackup', () => {
const TEST_HASH = sha256(Buffer.from('testattachmentdata'));
const TEST_HASH_BASE_64 =
// calculated as Buffer.from(TEST_HASH).toString('base64')
'ds5/U14lB2ziO90B7MldFTJUQdyw4qQ9y6Gnt9fmHL0=';
afterEach(function (this: Mocha.Context) {
sinon.restore();
});
it("should return base64 encoded plaintextHash if it's already been calculated", async () => {
assert.strictEqual(
await getMediaNameForBackup(
{
contentType: IMAGE_PNG,
size: 100,
plaintextHash: Buffer.from(TEST_HASH).toString('hex'),
},
'senderAci',
Date.now()
),
TEST_HASH_BASE_64
);
});
it('should calculate hash from file on disk if plaintextHash has not yet been calculated', async () => {
const stubbedGetHashFromDisk = sinon
.stub()
.callsFake(async (_path: string) =>
Buffer.from(TEST_HASH).toString('hex')
);
const mediaName = await getMediaNameForBackup(
{
contentType: IMAGE_PNG,
size: 100,
path: 'path/to/file',
},
'senderAci',
Date.now(),
{ getPlaintextHashForAttachmentOnDisk: stubbedGetHashFromDisk }
);
assert.strictEqual(stubbedGetHashFromDisk.callCount, 1);
assert.strictEqual(mediaName, TEST_HASH_BASE_64);
});
it('should return temporary identifier if attachment is undownloaded but in attachment tier', async () => {
const mediaName = await getMediaNameForBackup(
{
contentType: IMAGE_PNG,
size: 100,
cdnKey: 'cdnKey',
},
'senderAci',
Date.now()
);
assert.strictEqual(mediaName, 'senderAci_cdnKey');
});
it('should return temporary identifier if undownloaded attachment has temporary error', async () => {
const mediaName = await getMediaNameForBackup(
{
contentType: IMAGE_PNG,
size: 100,
cdnKey: 'cdnKey',
error: true,
key: 'attachmentkey',
},
'senderAci',
Date.now()
);
assert.strictEqual(mediaName, 'senderAci_cdnKey');
});
it('should return undefined if attachment is too old to be in attachment tier', async () => {
const mediaName = await getMediaNameForBackup(
{
contentType: IMAGE_PNG,
size: 100,
cdnKey: 'cdnKey',
},
'senderAci',
Date.now() - 31 * DAY
);
assert.strictEqual(mediaName, undefined);
});
});

View file

@ -28,7 +28,7 @@ import type { ProcessedAttachment } from './Types.d';
import type { WebAPIType } from './WebAPI'; import type { WebAPIType } from './WebAPI';
import { createName, getRelativePath } from '../windows/attachments'; import { createName, getRelativePath } from '../windows/attachments';
function getCdn(attachment: ProcessedAttachment) { export function getCdn(attachment: ProcessedAttachment): string {
const { cdnId, cdnKey } = attachment; const { cdnId, cdnKey } = attachment;
const cdn = cdnId || cdnKey; const cdn = cdnId || cdnKey;
strictAssert(cdn, 'Attachment was missing cdnId or cdnKey'); strictAssert(cdn, 'Attachment was missing cdnId or cdnKey');

View file

@ -0,0 +1,67 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { getPlaintextHashForAttachmentOnDisk } from '../../AttachmentCrypto';
import type { AttachmentType } from '../../types/Attachment';
import { DAY } from '../durations';
import * as log from '../../logging/log';
import { isOlderThan } from '../timestamp';
import { getCdn } from '../../textsecure/downloadAttachment';
import * as Bytes from '../../Bytes';
const TIME_IN_ATTACHMENT_TIER = 30 * DAY;
// We store the plaintext hash as a hex string, but the mediaName should be
// the base64 encoded version.
function convertHexStringToBase64(hexString: string): string {
return Bytes.toBase64(Bytes.fromHex(hexString));
}
type GetMediaNameDependenciesType = {
getPlaintextHashForAttachmentOnDisk: (
path: string
) => Promise<string | undefined>;
};
export async function getMediaNameForBackup(
attachment: AttachmentType,
senderAci: string,
messageTimestamp: number,
// allow optional dependency injection for testing
dependencies: GetMediaNameDependenciesType = {
getPlaintextHashForAttachmentOnDisk,
}
): Promise<string | undefined> {
if (attachment.plaintextHash) {
return convertHexStringToBase64(attachment.plaintextHash);
}
if (attachment.path) {
const hashFromFileOnDisk =
await dependencies.getPlaintextHashForAttachmentOnDisk(
window.Signal.Migrations.getAbsoluteAttachmentPath(attachment.path)
);
if (!hashFromFileOnDisk) {
log.error(
'getMediaNameForBackup: no hash from attachment on disk (maybe it is empty?)'
);
return;
}
return convertHexStringToBase64(hashFromFileOnDisk);
}
const cdnKey = getCdn(attachment);
if (!cdnKey) {
log.error('getMediaNameForBackup: attachment has no cdnKey');
return;
}
if (isOlderThan(messageTimestamp, TIME_IN_ATTACHMENT_TIER)) {
log.error(
"getMediaNameForBackup: attachment is not downloaded but is too old; it's no longer in attachment tier."
);
return;
}
return `${senderAci}_${cdnKey}`;
}