diff --git a/app/attachment_channel.main.ts b/app/attachment_channel.main.ts index cb5643f9aa..cfb73946d3 100644 --- a/app/attachment_channel.main.ts +++ b/app/attachment_channel.main.ts @@ -25,6 +25,7 @@ import { access } from 'node:fs/promises'; import { type DecryptAttachmentToSinkOptionsType, decryptAttachmentV2ToSink, + type IntegrityCheckType, } from '../ts/AttachmentCrypto.node.js'; import * as Bytes from '../ts/Bytes.std.js'; import type { MessageAttachmentsCursorType } from '../ts/sql/Interface.std.js'; @@ -68,6 +69,7 @@ import { getStickersPath, getTempPath, } from './attachments.node.js'; +import { isValidDigest, isValidPlaintextHash } from '../ts/types/Crypto.std.js'; const { isNumber } = lodash; @@ -98,7 +100,7 @@ type RangeFinderContextType = Readonly< } | { type: 'incremental'; - digest: Uint8Array; + integrityCheck: IntegrityCheckType; incrementalMac: Uint8Array; chunkSize: number; keysBase64: string; @@ -154,10 +156,7 @@ async function safeDecryptToSink( theirChunkSize: ctx.chunkSize, theirIncrementalMac: ctx.incrementalMac, type: 'standard', - integrityCheck: { - type: 'encrypted', - digest: ctx.digest, - }, + integrityCheck: ctx.integrityCheck, }; const controller = new AbortController(); @@ -703,9 +702,26 @@ export async function handleAttachmentRequest(req: Request): Promise { // When trying to view in-progress downloads, we need more information // to validate the file before returning data. - const digestBase64 = url.searchParams.get('digest'); - if (digestBase64 == null) { - return new Response('Missing digest', { status: 400 }); + const digestBase64 = url.searchParams.get('digest') ?? undefined; + const plaintextHashHex = + url.searchParams.get('plaintextHash') ?? undefined; + + let integrityCheck: IntegrityCheckType; + + if (isValidPlaintextHash(plaintextHashHex)) { + integrityCheck = { + type: 'plaintext', + plaintextHash: Bytes.fromHex(plaintextHashHex), + }; + } else if (isValidDigest(digestBase64)) { + integrityCheck = { + type: 'encrypted', + digest: Bytes.fromBase64(digestBase64), + }; + } else { + return new Response('Missing/invalid plaintextHash or digest', { + status: 400, + }); } const incrementalMacBase64 = url.searchParams.get('incrementalMac'); @@ -724,7 +740,7 @@ export async function handleAttachmentRequest(req: Request): Promise { context = { type: 'incremental', chunkSize, - digest: Bytes.fromBase64(digestBase64), + integrityCheck, incrementalMac: Bytes.fromBase64(incrementalMacBase64), keysBase64, path, diff --git a/ts/util/getLocalAttachmentUrl.std.ts b/ts/util/getLocalAttachmentUrl.std.ts index 84642a1fe3..a853a12c5c 100644 --- a/ts/util/getLocalAttachmentUrl.std.ts +++ b/ts/util/getLocalAttachmentUrl.std.ts @@ -27,6 +27,7 @@ export function getLocalAttachmentUrl( AttachmentType, | 'contentType' | 'digest' + | 'plaintextHash' | 'downloadPath' | 'incrementalMac' | 'chunkSize' @@ -87,10 +88,17 @@ export function getLocalAttachmentUrl( } url.searchParams.set('key', attachment.key); - if (!attachment.digest) { - throw new Error('getLocalAttachmentUrl: Missing attachment digest!'); + // Attachments restored from backup / link & sync may have plaintextHash but not + // digest + if (attachment.plaintextHash) { + url.searchParams.set('plaintextHash', attachment.plaintextHash); + } else if (attachment.digest) { + url.searchParams.set('digest', attachment.digest); + } else { + throw new Error( + 'getLocalAttachmentUrl: Missing attachment plaintextHash and digest!' + ); } - url.searchParams.set('digest', attachment.digest); if (!attachment.incrementalMac) { throw new Error(