From d4098c711af3cb205f26fc37257e1e59964e813d Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 13 May 2026 19:07:16 -0700 Subject: [PATCH] Decrypt and persist all file metadata layers Extends RawEnteFile and EnteFile with optional magicMetadata and pubMagicMetadata fields. Both are secretstream blobs under the file key, decrypted to arbitrary JSON (Record). pubMagicMetadata carries ML-derived data from the Ente clients: camera make/model, image dimensions, datetime with timezone offset, and (when present) captions, editedName, face labels, keywords. magicMetadata carries private mutable fields like visibility. Backup now writes per-file JSON at originals/.json containing all three metadata layers (basic + magic + pubMagic). Live-tested: all 11 files in the dev account have pubMagicMetadata with SONY DSC-RX1RM3 camera info and 3000x2000 dimensions. --- src/backup.ts | 18 ++++++++++++++++++ src/model/decrypt.ts | 19 +++++++++++++++++++ src/model/index.ts | 1 + src/model/types.ts | 11 +++++++++++ 4 files changed, 49 insertions(+) diff --git a/src/backup.ts b/src/backup.ts index a0d6647..b6cece3 100644 --- a/src/backup.ts +++ b/src/backup.ts @@ -116,6 +116,24 @@ export const runBackup = async ( } } + // Write per-file metadata JSON alongside the original + const metaJsonPath = join(originalsDir, `${file.id}.json`); + if (!existsSync(metaJsonPath)) { + const fileMeta: Record = { + id: file.id, + collectionID: file.collectionID, + ownerID: file.ownerID, + metadata: file.metadata, + }; + if (file.magicMetadata) { + fileMeta.magicMetadata = file.magicMetadata; + } + if (file.pubMagicMetadata) { + fileMeta.pubMagicMetadata = file.pubMagicMetadata; + } + writeFileSync(metaJsonPath, JSON.stringify(fileMeta, null, 2)); + } + if (!existsSync(linkPath) && existsSync(origPath)) { const target = relative(colDir, origPath); symlinkSync(target, linkPath); diff --git a/src/model/decrypt.ts b/src/model/decrypt.ts index dd9f7b5..5154357 100644 --- a/src/model/decrypt.ts +++ b/src/model/decrypt.ts @@ -7,6 +7,7 @@ import type { FileType, RawCollection, RawEnteFile, + RawMagicMetadata, } from "./types.js"; const KNOWN_COLLECTION_TYPES = new Set([ @@ -88,14 +89,32 @@ export const decryptFile = ( hash: metadataJSON.hash, }; + const magicMetadata = decryptMagicMetadata(raw.magicMetadata, key); + const pubMagicMetadata = decryptMagicMetadata(raw.pubMagicMetadata, key); + return { id: raw.id, collectionID: raw.collectionID, ownerID: raw.ownerID, key, metadata, + magicMetadata, + pubMagicMetadata, file: { decryptionHeader: raw.file.decryptionHeader }, thumbnail: { decryptionHeader: raw.thumbnail.decryptionHeader }, updationTime: raw.updationTime, }; }; + +const decryptMagicMetadata = ( + raw: RawMagicMetadata | undefined, + key: Uint8Array, +): Record | undefined => { + if (!raw?.data || !raw?.header) return undefined; + const bytes = decryptBlob( + fromBase64(raw.data), + fromBase64(raw.header), + key, + ); + return JSON.parse(new TextDecoder().decode(bytes)); +}; diff --git a/src/model/index.ts b/src/model/index.ts index 7d337ed..e86d6b1 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -9,4 +9,5 @@ export type { Microseconds, RawCollection, RawEnteFile, + RawMagicMetadata, } from "./types.js"; diff --git a/src/model/types.ts b/src/model/types.ts index 79551a0..42cb5b5 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -40,6 +40,8 @@ export interface EnteFile { ownerID: number; key: Uint8Array; metadata: FileMetadata; + magicMetadata?: Record; + pubMagicMetadata?: Record; file: FileBlob; thumbnail: FileBlob; updationTime: Microseconds; @@ -59,6 +61,13 @@ export interface RawCollection { isDeleted?: boolean; } +export interface RawMagicMetadata { + version: number; + count: number; + data: string; + header: string; +} + export interface RawEnteFile { id: number; collectionID: number; @@ -66,6 +75,8 @@ export interface RawEnteFile { encryptedKey: string; keyDecryptionNonce: string; metadata: { encryptedData: string; decryptionHeader: string }; + magicMetadata?: RawMagicMetadata; + pubMagicMetadata?: RawMagicMetadata; info?: { fileSize?: number; thumbSize?: number }; file: { decryptionHeader: string }; thumbnail: { decryptionHeader: string };