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:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user