import { decryptBlob, decryptBox, decryptSealed, fromBase64, } from "../crypto/index.js"; import type { Collection, CollectionType, EnteFile, FileMetadata, FileType, KeyMaterial, RawCollection, RawEnteFile, RawMagicMetadata, } from "./types.js"; const KNOWN_COLLECTION_TYPES = new Set([ "album", "folder", "favorites", "uncategorized", ]); const parseCollectionType = (s: string): CollectionType => KNOWN_COLLECTION_TYPES.has(s) ? (s as CollectionType) : "unknown"; const FILE_TYPE_MAP: Record = { 0: "image", 1: "video", 2: "livePhoto", }; const parseFileType = (n: number): FileType => FILE_TYPE_MAP[n] ?? "unknown"; export const decryptCollection = ( raw: RawCollection, keys: KeyMaterial, currentUserID?: number, ): Collection => { // Owned collections carry their key as a secretbox under our master // key, with the nonce in keyDecryptionNonce. Collections shared with // us carry it as an anonymous sealed box to our public key and have // no keyDecryptionNonce at all (sealed boxes embed an ephemeral // public key instead). const key = raw.keyDecryptionNonce ? decryptBox( fromBase64(raw.encryptedKey), fromBase64(raw.keyDecryptionNonce), keys.masterKey, ) : decryptSealed( fromBase64(raw.encryptedKey), keys.publicKey, keys.secretKey, ); let name = ""; if (raw.encryptedName && raw.nameDecryptionNonce) { const nameBytes = decryptBox( fromBase64(raw.encryptedName), fromBase64(raw.nameDecryptionNonce), key, ); name = new TextDecoder().decode(nameBytes); } return { id: raw.id, ownerID: raw.owner.id, key, name, type: parseCollectionType(raw.type), updationTime: raw.updationTime, isShared: currentUserID !== undefined && raw.owner.id !== currentUserID, magicMetadata: decryptMagicMetadata(raw.magicMetadata, key), pubMagicMetadata: decryptMagicMetadata(raw.pubMagicMetadata, key), sharedMagicMetadata: decryptMagicMetadata(raw.sharedMagicMetadata, key), }; }; export const decryptFile = ( raw: RawEnteFile, collectionKey: Uint8Array, ): EnteFile => { const key = decryptBox( fromBase64(raw.encryptedKey), fromBase64(raw.keyDecryptionNonce), collectionKey, ); // File metadata is a single-chunk secretstream blob (not a secretbox). // The decryptionHeader field is the secretstream init header, not a nonce. const metadataBytes = decryptBlob( fromBase64(raw.metadata.encryptedData), fromBase64(raw.metadata.decryptionHeader), key, ); const metadataJSON = JSON.parse(new TextDecoder().decode(metadataBytes)); if ( typeof metadataJSON !== "object" || metadataJSON === null || Array.isArray(metadataJSON) ) { throw new Error(`file ${raw.id}: metadata is not a JSON object`); } const metadata: FileMetadata = { // The server controls this JSON: a title that is missing or not a // string becomes "", never an arbitrary value. title: typeof metadataJSON.title === "string" ? metadataJSON.title : "", fileType: parseFileType(metadataJSON.fileType ?? -1), creationTime: metadataJSON.creationTime ?? 0, modificationTime: metadataJSON.modificationTime ?? 0, latitude: metadataJSON.latitude, longitude: metadataJSON.longitude, 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, size: raw.info?.fileSize, }, thumbnail: { decryptionHeader: raw.thumbnail.decryptionHeader, size: raw.info?.thumbSize, }, updationTime: raw.updationTime, isDeleted: raw.isDeleted, }; }; 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)); };