Fix file metadata decryption: use secretstream blob, not secretbox

File metadata is encrypted as a single-chunk secretstream blob (the
'decryptionHeader' is the secretstream init header, not a secretbox
nonce). Collection keys and names correctly use secretbox.

Adds decryptBlob(ciphertext, header, key) to the crypto module as a
convenience wrapper for single-chunk secretstream decryption (init +
pull + verify TAG_FINAL).

Live-tested: collection names and file metadata (titles, types, dates)
decrypt correctly from the real Ente API.
This commit is contained in:
2026-05-13 17:38:18 -07:00
parent f81216333e
commit 44718a92a9
5 changed files with 170 additions and 41 deletions

View File

@@ -2,6 +2,8 @@ import { init } from "../../src/crypto/index.js";
import { ApiClient } from "../../src/api/client.js";
import { beginLogin } from "../../src/auth/login.js";
import { unwrapAuth } from "../../src/auth/unwrap.js";
import { decryptCollection, decryptFile } from "../../src/model/index.js";
import type { RawCollection, RawEnteFile } from "../../src/model/index.js";
// Dev account — not a secret, throwaway account for integration testing.
const EMAIL = "entedev2026jp@acidhou.se";
@@ -9,7 +11,7 @@ const PASSWORD = "loldongs";
const RECOVERY_KEY =
"deliver have behave collect void chicken boring embrace coast reflect squeeze cotton dish resemble license remain quick dwarf plastic ensure amused cry nasty equip";
void RECOVERY_KEY; // available if needed for account recovery tests
void RECOVERY_KEY;
const main = async () => {
await init();
@@ -17,45 +19,71 @@ const main = async () => {
console.log(`Logging in as ${EMAIL}...`);
const challenge = await beginLogin(api, EMAIL, PASSWORD);
console.log("beginLogin returned:", challenge.kind);
let response;
if (challenge.kind === "complete") {
response = challenge.response;
} else if (challenge.kind === "totp") {
console.error("Account requires TOTP — disable 2FA for dev testing");
process.exit(1);
} else if (challenge.kind === "emailOTP") {
console.error("Account uses email OTP — SRP not set up?");
process.exit(1);
} else {
console.error("Unexpected challenge:", challenge);
if (challenge.kind !== "complete") {
console.error("Expected complete login, got:", challenge.kind);
process.exit(1);
}
console.log("Got AuthorizationResponse, user ID:", response.id);
console.log("Unwrapping keys...");
const { masterKey, token } = await unwrapAuth(response, PASSWORD);
console.log("Master key length:", masterKey.length, "bytes");
console.log("Token length:", token.length, "chars");
const { masterKey, token } = await unwrapAuth(challenge.response, PASSWORD);
api.setAuthToken(token);
console.log("Logged in, user ID:", challenge.response.id);
// Fetch and decrypt collections
console.log("\nFetching collections...");
const { collections } = await api.getJSON<{ collections: unknown[] }>(
"/collections/v2",
{ sinceTime: 0 },
const { collections: rawCollections } = await api.getJSON<{
collections: RawCollection[];
}>("/collections/v2", { sinceTime: 0 });
const userID = challenge.response.id;
const collections = rawCollections.map((raw) =>
decryptCollection(raw, masterKey, userID),
);
console.log(`Got ${collections.length} collection(s)`);
for (const c of collections.slice(0, 5)) {
const col = c as Record<string, unknown>;
console.log(`${collections.length} collection(s):`);
for (const c of collections) {
console.log(
` id=${col.id} type=${col.type} updationTime=${col.updationTime}`,
` [${c.type}] "${c.name}" (id=${c.id}, shared=${c.isShared})`,
);
}
console.log("\nLive login test passed.");
// Fetch and decrypt files from the first non-empty collection
for (const col of collections) {
console.log(`\nFetching files from "${col.name}" (id=${col.id})...`);
const { diff: rawFiles } = await api.getJSON<{
diff: RawEnteFile[];
hasMore: boolean;
}>("/collections/v2/diff", {
collectionID: col.id,
sinceTime: 0,
});
if (rawFiles.length === 0) {
console.log(" (empty)");
continue;
}
const files = rawFiles
.filter((f) => !f.isDeleted)
.map((raw) => decryptFile(raw, col.key));
console.log(` ${files.length} file(s):`);
for (const f of files.slice(0, 10)) {
console.log(
` ${f.metadata.title} [${f.metadata.fileType}] id=${f.id}`,
);
if (f.metadata.latitude !== undefined) {
console.log(
` location: ${f.metadata.latitude}, ${f.metadata.longitude}`,
);
}
}
if (files.length > 10) {
console.log(` ... and ${files.length - 10} more`);
}
break;
}
console.log("\nLive integration test passed.");
};
main().catch((err) => {