/** * Live photo fixtures for the download, content cache, backup and CLI tests. * * Ente stores a live photo as one ZIP holding its image and its video, which * Ente's clients name `image.` and `video.`. The ZIP is built here * with fflate from small fixed bytes and encrypted the way the server serves a * file under 4 MiB, as one secretstream chunk. `cdnSource` serves it to the * real download layer, so a test checks what quak stores for a real one. */ import { createHash } from "node:crypto"; import { zipSync } from "fflate"; import { ApiClient } from "../src/api/client.js"; import { encryptBlob, init, toBase64 } from "../src/crypto/index.js"; import { makeDownloadContentSource, type ContentSource, } from "../src/library/content.js"; import type { EnteFile } from "../src/model/types.js"; export const IMAGE = new TextEncoder().encode("the still image"); export const VIDEO = new TextEncoder().encode("the few seconds of video"); // A live photo's ZIP; by default the entries an iPhone's live photo gets. export const livePhotoZip = ( entries: Record = { "image.heic": IMAGE, "video.mov": VIDEO, }, ): Uint8Array => zipSync(entries); const blake2b = (bytes: Uint8Array): string => createHash("blake2b512").update(bytes).digest("base64"); // The hash Ente's clients record for a live photo: the unkeyed BLAKE2b-512 of // the image and of the video, each in standard base64, joined by a colon. export const livePhotoHash = (image = IMAGE, video = VIDEO): string => `${blake2b(image)}:${blake2b(video)}`; // `file` as a live photo whose original is `zip` and whose recorded hash is // `hash`, and `body`, what the server serves for it: `zip` encrypted under the // file's key and header. export const asLivePhoto = async ( file: EnteFile, zip = livePhotoZip(), hash = livePhotoHash(), ): Promise<{ file: EnteFile; body: Uint8Array }> => { await init(); const key = new Uint8Array(32).fill(file.id & 0xff); const { header, ciphertext } = encryptBlob(zip, key); return { file: { ...file, key, metadata: { ...file.metadata, fileType: "livePhoto", hash }, file: { decryptionHeader: toBase64(header) }, }, body: ciphertext, }; }; // A content source that downloads through the real download layer from a // stand-in server, which serves `bodies` by file ID and a 404 for any other. export const cdnSource = (bodies: Map): ContentSource => makeDownloadContentSource( new ApiClient({ fetch: (async (url: string | URL) => { const fileID = new URL(String(url)).searchParams.get("fileID"); const body = bodies.get(Number(fileID)); return body === undefined ? new Response("not found", { status: 404 }) : new Response(body); }) as typeof globalThis.fetch, retry: { attempts: 1 }, }), );