Merge: decrypt magic metadata + per-file JSON in backups

All three metadata layers (basic, magicMetadata, pubMagicMetadata) are
now decrypted from secretstream blobs and exposed on EnteFile. Backup
writes originals/<fileID>.json with the full decrypted metadata
including camera make/model, dimensions, datetime, and any face/keyword
data the Ente clients have added.
This commit is contained in:
2026-05-13 19:07:26 -07:00
4 changed files with 49 additions and 0 deletions

View File

@@ -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<string, unknown> = {
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)) { if (!existsSync(linkPath) && existsSync(origPath)) {
const target = relative(colDir, origPath); const target = relative(colDir, origPath);
symlinkSync(target, linkPath); symlinkSync(target, linkPath);

View File

@@ -7,6 +7,7 @@ import type {
FileType, FileType,
RawCollection, RawCollection,
RawEnteFile, RawEnteFile,
RawMagicMetadata,
} from "./types.js"; } from "./types.js";
const KNOWN_COLLECTION_TYPES = new Set([ const KNOWN_COLLECTION_TYPES = new Set([
@@ -88,14 +89,32 @@ export const decryptFile = (
hash: metadataJSON.hash, hash: metadataJSON.hash,
}; };
const magicMetadata = decryptMagicMetadata(raw.magicMetadata, key);
const pubMagicMetadata = decryptMagicMetadata(raw.pubMagicMetadata, key);
return { return {
id: raw.id, id: raw.id,
collectionID: raw.collectionID, collectionID: raw.collectionID,
ownerID: raw.ownerID, ownerID: raw.ownerID,
key, key,
metadata, metadata,
magicMetadata,
pubMagicMetadata,
file: { decryptionHeader: raw.file.decryptionHeader }, file: { decryptionHeader: raw.file.decryptionHeader },
thumbnail: { decryptionHeader: raw.thumbnail.decryptionHeader }, thumbnail: { decryptionHeader: raw.thumbnail.decryptionHeader },
updationTime: raw.updationTime, updationTime: raw.updationTime,
}; };
}; };
const decryptMagicMetadata = (
raw: RawMagicMetadata | undefined,
key: Uint8Array,
): Record<string, unknown> | 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));
};

View File

@@ -9,4 +9,5 @@ export type {
Microseconds, Microseconds,
RawCollection, RawCollection,
RawEnteFile, RawEnteFile,
RawMagicMetadata,
} from "./types.js"; } from "./types.js";

View File

@@ -40,6 +40,8 @@ export interface EnteFile {
ownerID: number; ownerID: number;
key: Uint8Array; key: Uint8Array;
metadata: FileMetadata; metadata: FileMetadata;
magicMetadata?: Record<string, unknown>;
pubMagicMetadata?: Record<string, unknown>;
file: FileBlob; file: FileBlob;
thumbnail: FileBlob; thumbnail: FileBlob;
updationTime: Microseconds; updationTime: Microseconds;
@@ -59,6 +61,13 @@ export interface RawCollection {
isDeleted?: boolean; isDeleted?: boolean;
} }
export interface RawMagicMetadata {
version: number;
count: number;
data: string;
header: string;
}
export interface RawEnteFile { export interface RawEnteFile {
id: number; id: number;
collectionID: number; collectionID: number;
@@ -66,6 +75,8 @@ export interface RawEnteFile {
encryptedKey: string; encryptedKey: string;
keyDecryptionNonce: string; keyDecryptionNonce: string;
metadata: { encryptedData: string; decryptionHeader: string }; metadata: { encryptedData: string; decryptionHeader: string };
magicMetadata?: RawMagicMetadata;
pubMagicMetadata?: RawMagicMetadata;
info?: { fileSize?: number; thumbSize?: number }; info?: { fileSize?: number; thumbSize?: number };
file: { decryptionHeader: string }; file: { decryptionHeader: string };
thumbnail: { decryptionHeader: string }; thumbnail: { decryptionHeader: string };