check / check (push) Successful in 25s
decryptFile now sets file.size/thumbnail.size from raw.info (undefined when absent) and carries an optional isDeleted on EnteFile. Plain EnteFile return, no caller changes; listFiles keeps filtering tombstones. Tests cover the three fields and the size-absent case. Model: opus-4-8
148 lines
4.2 KiB
TypeScript
148 lines
4.2 KiB
TypeScript
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<number, FileType> = {
|
|
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));
|
|
|
|
const metadata: FileMetadata = {
|
|
title: 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<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));
|
|
};
|