Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d78aacac2 |
@@ -21,9 +21,10 @@ Update the README API reference section to match the current implementation.
|
|||||||
- 2026-09-22: Carried file size, thumbnail size, and the deletion flag through
|
- 2026-09-22: Carried file size, thumbnail size, and the deletion flag through
|
||||||
`decryptFile` (issue 37, foundation for the cache/API design). Live files now
|
`decryptFile` (issue 37, foundation for the cache/API design). Live files now
|
||||||
populate `file.size`/`thumbnail.size` from the server's `info` (left
|
populate `file.size`/`thumbnail.size` from the server's `info` (left
|
||||||
`undefined` when the server omits it), and a deleted file returns an
|
`undefined` when the server omits it), and `isDeleted` is carried from the
|
||||||
`EnteFileTombstone` — `id`, `collectionID`, `updationTime`, `isDeleted` —
|
diff row onto `EnteFile`. No caller change: `listFiles` still filters deleted
|
||||||
instead of decrypting ciphertext the server no longer holds.
|
rows before decrypting. Surfacing a tombstone through decryption belongs to
|
||||||
|
the enumeration unit (issue 38).
|
||||||
- 2026-08-10: Made `lint-once.test.ts` enforce what its header claims. It walked
|
- 2026-08-10: Made `lint-once.test.ts` enforce what its header claims. It walked
|
||||||
`make check` only, so it never read `Dockerfile` — the image CI builds through
|
`make check` only, so it never read `Dockerfile` — the image CI builds through
|
||||||
`script/cibuild` — and a second `prettier --check .` could be added there with
|
`script/cibuild` — and a second `prettier --check .` could be added there with
|
||||||
|
|||||||
+2
-5
@@ -186,11 +186,8 @@ export class Client {
|
|||||||
}>("/collections/v2/diff", { collectionID, sinceTime });
|
}>("/collections/v2/diff", { collectionID, sinceTime });
|
||||||
|
|
||||||
for (const raw of diff) {
|
for (const raw of diff) {
|
||||||
// decryptFile returns a tombstone for a deleted file; the
|
if (!raw.isDeleted) {
|
||||||
// diff keeps returning those, so drop them from the listing.
|
allFiles.push(decryptFile(raw, collectionKey));
|
||||||
const file = decryptFile(raw, collectionKey);
|
|
||||||
if (!file.isDeleted) {
|
|
||||||
allFiles.push(file);
|
|
||||||
}
|
}
|
||||||
if (raw.updationTime > sinceTime) {
|
if (raw.updationTime > sinceTime) {
|
||||||
sinceTime = raw.updationTime;
|
sinceTime = raw.updationTime;
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ export type {
|
|||||||
Collection,
|
Collection,
|
||||||
CollectionType,
|
CollectionType,
|
||||||
EnteFile,
|
EnteFile,
|
||||||
EnteFileTombstone,
|
|
||||||
FileBlob,
|
FileBlob,
|
||||||
FileMetadata,
|
FileMetadata,
|
||||||
FileType,
|
FileType,
|
||||||
|
|||||||
+2
-13
@@ -8,7 +8,6 @@ import type {
|
|||||||
Collection,
|
Collection,
|
||||||
CollectionType,
|
CollectionType,
|
||||||
EnteFile,
|
EnteFile,
|
||||||
EnteFileTombstone,
|
|
||||||
FileMetadata,
|
FileMetadata,
|
||||||
FileType,
|
FileType,
|
||||||
KeyMaterial,
|
KeyMaterial,
|
||||||
@@ -84,18 +83,7 @@ export const decryptCollection = (
|
|||||||
export const decryptFile = (
|
export const decryptFile = (
|
||||||
raw: RawEnteFile,
|
raw: RawEnteFile,
|
||||||
collectionKey: Uint8Array,
|
collectionKey: Uint8Array,
|
||||||
): EnteFile | EnteFileTombstone => {
|
): EnteFile => {
|
||||||
// A deleted file has no ciphertext to decrypt; return the identity and
|
|
||||||
// the flag, and touch none of the encrypted fields.
|
|
||||||
if (raw.isDeleted) {
|
|
||||||
return {
|
|
||||||
id: raw.id,
|
|
||||||
collectionID: raw.collectionID,
|
|
||||||
updationTime: raw.updationTime,
|
|
||||||
isDeleted: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = decryptBox(
|
const key = decryptBox(
|
||||||
fromBase64(raw.encryptedKey),
|
fromBase64(raw.encryptedKey),
|
||||||
fromBase64(raw.keyDecryptionNonce),
|
fromBase64(raw.keyDecryptionNonce),
|
||||||
@@ -141,6 +129,7 @@ export const decryptFile = (
|
|||||||
size: raw.info?.thumbSize,
|
size: raw.info?.thumbSize,
|
||||||
},
|
},
|
||||||
updationTime: raw.updationTime,
|
updationTime: raw.updationTime,
|
||||||
|
isDeleted: raw.isDeleted,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ export type {
|
|||||||
Collection,
|
Collection,
|
||||||
CollectionType,
|
CollectionType,
|
||||||
EnteFile,
|
EnteFile,
|
||||||
EnteFileTombstone,
|
|
||||||
FileBlob,
|
FileBlob,
|
||||||
FileMetadata,
|
FileMetadata,
|
||||||
FileType,
|
FileType,
|
||||||
|
|||||||
+3
-12
@@ -48,18 +48,9 @@ export interface EnteFile {
|
|||||||
file: FileBlob;
|
file: FileBlob;
|
||||||
thumbnail: FileBlob;
|
thumbnail: FileBlob;
|
||||||
updationTime: Microseconds;
|
updationTime: Microseconds;
|
||||||
isDeleted?: false;
|
// Set from the diff row's flag. Live files decode with it absent/false;
|
||||||
}
|
// deleted rows are filtered out before decryptFile, so it is not set here.
|
||||||
|
isDeleted?: boolean;
|
||||||
// A deleted file as the sync diff reports it. The server keeps returning the
|
|
||||||
// row so clients can drop it, but it carries no ciphertext to decrypt, so a
|
|
||||||
// tombstone is only an identity plus the flag. isDeleted discriminates it
|
|
||||||
// from EnteFile, so callers narrow on that field before reaching for content.
|
|
||||||
export interface EnteFileTombstone {
|
|
||||||
id: number;
|
|
||||||
collectionID: number;
|
|
||||||
updationTime: Microseconds;
|
|
||||||
isDeleted: true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The key material a logged-in client holds, everything needed to decrypt
|
// The key material a logged-in client holds, everything needed to decrypt
|
||||||
|
|||||||
@@ -196,24 +196,6 @@ const buildRawFile = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// A deleted file as the sync diff returns it: isDeleted is set and the
|
|
||||||
// encrypted payload fields are empty, because the server no longer holds
|
|
||||||
// the content to encrypt. Attempting to decrypt these would throw, which
|
|
||||||
// is exactly what the tombstone path must avoid. Do not "fix" this fixture
|
|
||||||
// by giving it real ciphertext: its emptiness is what the wire sends.
|
|
||||||
const buildTombstoneRawFile = (): RawEnteFile => ({
|
|
||||||
id: 201,
|
|
||||||
collectionID: 100,
|
|
||||||
ownerID: 1,
|
|
||||||
encryptedKey: "",
|
|
||||||
keyDecryptionNonce: "",
|
|
||||||
metadata: { encryptedData: "", decryptionHeader: "" },
|
|
||||||
file: { decryptionHeader: "" },
|
|
||||||
thumbnail: { decryptionHeader: "" },
|
|
||||||
updationTime: 1700000000000001,
|
|
||||||
isDeleted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Tests
|
// Tests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -393,7 +375,6 @@ describe("model.decryptFile", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const file = decryptFile(raw, collectionKey);
|
const file = decryptFile(raw, collectionKey);
|
||||||
if (file.isDeleted) throw new Error("expected a live file");
|
|
||||||
|
|
||||||
expect(file.file.size).toBe(4096);
|
expect(file.file.size).toBe(4096);
|
||||||
expect(file.thumbnail.size).toBe(512);
|
expect(file.thumbnail.size).toBe(512);
|
||||||
@@ -408,28 +389,21 @@ describe("model.decryptFile", () => {
|
|||||||
expect(raw.info).toBeUndefined();
|
expect(raw.info).toBeUndefined();
|
||||||
|
|
||||||
const file = decryptFile(raw, collectionKey);
|
const file = decryptFile(raw, collectionKey);
|
||||||
if (file.isDeleted) throw new Error("expected a live file");
|
|
||||||
|
|
||||||
expect(file.file.size).toBeUndefined();
|
expect(file.file.size).toBeUndefined();
|
||||||
expect(file.thumbnail.size).toBeUndefined();
|
expect(file.thumbnail.size).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a minimal record for a tombstone without decrypting", () => {
|
it("carries the deletion flag from the diff row", () => {
|
||||||
// A deleted file arrives with isDeleted set and no usable ciphertext.
|
// The diff marks a deleted row with isDeleted; decryptFile copies it
|
||||||
// decryptFile must skip decryption entirely and return just the
|
// onto the file so a caller can tell a deleted row from a live one.
|
||||||
// identity plus the flag — decrypting the empty payload would throw.
|
|
||||||
const masterKey = sodium.crypto_secretbox_keygen();
|
const masterKey = sodium.crypto_secretbox_keygen();
|
||||||
const { collectionKey } = buildRawCollection(masterKey);
|
const { collectionKey } = buildRawCollection(masterKey);
|
||||||
const raw = buildTombstoneRawFile();
|
const raw = buildRawFile(collectionKey);
|
||||||
|
raw.isDeleted = true;
|
||||||
|
|
||||||
const file = decryptFile(raw, collectionKey);
|
const file = decryptFile(raw, collectionKey);
|
||||||
|
|
||||||
expect(file.isDeleted).toBe(true);
|
expect(file.isDeleted).toBe(true);
|
||||||
expect(file.id).toBe(201);
|
|
||||||
expect(file.collectionID).toBe(100);
|
|
||||||
expect(file.updationTime).toBe(1700000000000001);
|
|
||||||
// No decrypted content is carried for a tombstone.
|
|
||||||
expect("metadata" in file).toBe(false);
|
|
||||||
expect("key" in file).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user