From ed8b3a8fff7333ba8c852641f8610935ca3c855f Mon Sep 17 00:00:00 2001 From: sneak Date: Sat, 9 May 2026 21:29:08 +0200 Subject: [PATCH] Reflow README to prettier 80-column prose wrap --- README.md | 141 +++++++++++++++++++++++++----------------------------- 1 file changed, 66 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index a589bbf..97538c3 100644 --- a/README.md +++ b/README.md @@ -43,29 +43,27 @@ await client.downloadFile(file, "./out/"); ## Rationale Ente is one of very few photo services with a credible end-to-end encryption -story. The shipping clients (mobile Flutter, web React, desktop Electron, and -Go CLI) work, but they are slow, buggy, and difficult to script against. The -Flutter app fails to sync reliably. The web app is heavy. The desktop app is -the web app inside a slow Electron wrapper. The Go CLI is the closest thing -to a usable tool, but it is awkward to integrate from anything that is not a -shell. +story. The shipping clients (mobile Flutter, web React, desktop Electron, and Go +CLI) work, but they are slow, buggy, and difficult to script against. The +Flutter app fails to sync reliably. The web app is heavy. The desktop app is the +web app inside a slow Electron wrapper. The Go CLI is the closest thing to a +usable tool, but it is awkward to integrate from anything that is not a shell. quack is the first step in fixing that. This repo ships a small, correct, well-tested implementation of Ente's cryptographic protocol and its read-only API surface, plus a CLI that proves the library is enough to do real work without a UI. -The longer-term goal of this project is a simple desktop client for Ente, -built on this library in Electron (or a comparable runtime), with two -priorities above everything else: correctness and stability. Performance and -simplicity follow from those. Features will be added only after the protocol -layer is correct, the local cache is reliable, and the UI is responsive on a -five-year-old laptop. +The longer-term goal of this project is a simple desktop client for Ente, built +on this library in Electron (or a comparable runtime), with two priorities above +everything else: correctness and stability. Performance and simplicity follow +from those. Features will be added only after the protocol layer is correct, the +local cache is reliable, and the UI is responsive on a five-year-old laptop. -This first release is deliberately scoped to read operations: log in, walk -the account, decrypt and save files. Upload, sharing, deletion, and -bidirectional sync are out of scope. Adding them later is straightforward; -doing them right requires the protocol layer to be correct first. +This first release is deliberately scoped to read operations: log in, walk the +account, decrypt and save files. Upload, sharing, deletion, and bidirectional +sync are out of scope. Adding them later is straightforward; doing them right +requires the protocol layer to be correct first. ## Design @@ -101,14 +99,14 @@ required for `crypto_pwhash` / Argon2id). No hand-rolled crypto. The key hierarchy, derived during login, is: 1. The user enters their password. -2. Argon2id (`crypto_pwhash`) over the password and a server-issued - `kekSalt`, with server-issued `memLimit` and `opsLimit`, produces a - 32-byte Key Encryption Key (KEK). +2. Argon2id (`crypto_pwhash`) over the password and a server-issued `kekSalt`, + with server-issued `memLimit` and `opsLimit`, produces a 32-byte Key + Encryption Key (KEK). 3. SRP login: a 16-byte SRP login subkey is derived from the KEK using `crypto_kdf_derive_from_key` (BLAKE2b) with subkey id 1 and context `loginctx`. That 16-byte value is the SRP password. -4. After SRP completes (or after email-OTP fallback), the server returns a - blob of "key attributes" plus an encrypted auth token. +4. After SRP completes (or after email-OTP fallback), the server returns a blob + of "key attributes" plus an encrypted auth token. 5. `crypto_secretbox_open_easy` over the encrypted master key with the KEK yields the 32-byte master key. 6. `crypto_secretbox_open_easy` over the encrypted secret key with the master @@ -118,12 +116,12 @@ The key hierarchy, derived during login, is: yields the URL-safe base64 auth token used in `X-Auth-Token` for all subsequent calls. -Per-collection keys are decrypted with `crypto_secretbox_open_easy` using -the master key (for owned collections). Per-file keys are decrypted with +Per-collection keys are decrypted with `crypto_secretbox_open_easy` using the +master key (for owned collections). Per-file keys are decrypted with `crypto_secretbox_open_easy` using the collection key. File metadata is a secretbox under the file key. File content is a chunked -`crypto_secretstream_xchacha20poly1305` stream under the file key, with a -4 MiB plaintext chunk size and a 17-byte authentication overhead per chunk. +`crypto_secretstream_xchacha20poly1305` stream under the file key, with a 4 MiB +plaintext chunk size and a 17-byte authentication overhead per chunk. ### HTTP API @@ -134,39 +132,37 @@ Production endpoints: - Thumbnail CDN: `https://thumbnails.ente.io/?fileID=` A custom API endpoint is configurable for self-hosted servers via the -`ENTE_API_ENDPOINT` environment variable. When set, file downloads route -through `/files/download/` instead of the dedicated CDN host. +`ENTE_API_ENDPOINT` environment variable. When set, file downloads route through +`/files/download/` instead of the dedicated CDN host. Required request headers on every authenticated call: - `X-Auth-Token`: the decrypted auth token from login. -- `X-Client-Package`: identifies the client. quack uses - `berlin.sneak.quack`. +- `X-Client-Package`: identifies the client. quack uses `berlin.sneak.quack`. Endpoints used: - `GET /users/srp/attributes?email=`: fetch SRP and KDF parameters. - `POST /users/srp/create-session`: begin SRP handshake. -- `POST /users/srp/verify-session`: complete SRP, receive 2FA challenge or - the encrypted token plus key attributes. +- `POST /users/srp/verify-session`: complete SRP, receive 2FA challenge or the + encrypted token plus key attributes. - `POST /users/ott` and `POST /users/verify-email`: email OTP fallback path. - `POST /users/two-factor/verify`: TOTP second factor. - `GET /collections/v2?sinceTime=`: list collections changed since microsecond timestamp; pass 0 for a full enumeration. -- `GET /collections/v2/diff?collectionID=&sinceTime=`: list - files in a collection; paginate while `hasMore` is true. +- `GET /collections/v2/diff?collectionID=&sinceTime=`: list files in a + collection; paginate while `hasMore` is true. - `GET https://files.ente.io/?fileID=`: download encrypted file bytes. ### Session persistence After login, quack writes an encrypted session blob to -`$XDG_CONFIG_HOME/quack/session.json` (default -`~/.config/quack/session.json`) containing the auth token, the user's master -key, the user's secret key, and the user's email. The session file is itself -encrypted with a key derived from a per-machine random value stored in the OS -keychain when available, falling back to a key file at mode `0600` in the -same config directory. The master key and secret key are never written to -disk in cleartext. +`$XDG_CONFIG_HOME/quack/session.json` (default `~/.config/quack/session.json`) +containing the auth token, the user's master key, the user's secret key, and the +user's email. The session file is itself encrypted with a key derived from a +per-machine random value stored in the OS keychain when available, falling back +to a key file at mode `0600` in the same config directory. The master key and +secret key are never written to disk in cleartext. ### CLI surface @@ -174,11 +170,10 @@ disk in cleartext. - `quack logout`: deletes the session. - `quack whoami`: prints the logged-in email. - `quack collections`: list collections (id, name, type, file count). -- `quack files --collection `: list files in a collection (id, name, - type, creation time, size). +- `quack files --collection `: list files in a collection (id, name, type, + creation time, size). - `quack get --out `: download and decrypt a file. -- `quack get-thumb --out `: download and decrypt a - thumbnail. +- `quack get-thumb --out `: download and decrypt a thumbnail. All commands accept `--json` for machine-readable output. @@ -552,10 +547,10 @@ Phase 1: scaffolding (this commit and the next) - [x] `git init`, write README - [ ] Create `initial-scaffolding` feature branch -- [ ] Add `LICENSE` (WTFPL), `REPO_POLICIES.md`, `.gitignore`, - `.editorconfig`, `.prettierrc`, `.prettierignore`, `.dockerignore` -- [ ] Add `Makefile` with `test`, `lint`, `fmt`, `fmt-check`, `check`, - `docker`, `hooks`, plus `build`, `dev`, `clean` +- [ ] Add `LICENSE` (WTFPL), `REPO_POLICIES.md`, `.gitignore`, `.editorconfig`, + `.prettierrc`, `.prettierignore`, `.dockerignore` +- [ ] Add `Makefile` with `test`, `lint`, `fmt`, `fmt-check`, `check`, `docker`, + `hooks`, plus `build`, `dev`, `clean` - [ ] Add `Dockerfile` running `make check` against pinned node image - [ ] Add `.gitea/workflows/check.yml` running `docker build .` - [ ] Add `package.json`, `tsconfig.json`, install pinned versions of @@ -567,23 +562,23 @@ Phase 2: crypto primitives - [ ] Wrap libsodium init as an awaitable singleton - [ ] `deriveKEK(password, kekSalt, memLimit, opsLimit)` (Argon2id) -- [ ] `deriveLoginSubkey(kek)` (KDF with subkey id 1, context `loginctx`, - 16 bytes) +- [ ] `deriveLoginSubkey(kek)` (KDF with subkey id 1, context `loginctx`, 16 + bytes) - [ ] `decryptBox(ciphertext, nonce, key)` for secretbox - [ ] `decryptSealed(ciphertext, publicKey, secretKey)` for sealed box -- [ ] `initStreamPull` and `pullStreamChunk` for chunked secretstream - (4 MiB plaintext chunks, 17-byte overhead) +- [ ] `initStreamPull` and `pullStreamChunk` for chunked secretstream (4 MiB + plaintext chunks, 17-byte overhead) - [ ] Round-trip tests against vectors generated by libsodium directly Phase 3: SRP + auth -- [ ] SRP-6a client using `secure-remote-password` with the same group as - the server +- [ ] SRP-6a client using `secure-remote-password` with the same group as the + server - [ ] `beginLogin(email, password)` returning a `LoginChallenge` - [ ] `requestEmailOTP` and `submitEmailOTP` for accounts without SRP - [ ] `submitTOTP(sessionID, code)` -- [ ] `unwrapAuth(response, password)` returning master key, secret key, - public key, and decrypted token +- [ ] `unwrapAuth(response, password)` returning master key, secret key, public + key, and decrypted token - [ ] Tests against recorded HTTP fixtures Phase 4: HTTP client + endpoints @@ -605,18 +600,16 @@ Phase 5: collections and files Phase 6: download -- [ ] `downloadFile(fileID, outPath)` streams the encrypted body, decrypts - it chunk by chunk, writes plaintext to `outPath`. Resolves the - filename from the decrypted metadata title when no `outPath` is - supplied. +- [ ] `downloadFile(fileID, outPath)` streams the encrypted body, decrypts it + chunk by chunk, writes plaintext to `outPath`. Resolves the filename from + the decrypted metadata title when no `outPath` is supplied. - [ ] `downloadThumbnail(fileID, outPath)` for the thumbnail CDN -- [ ] Live integration test against a throwaway Ente account if one is - available +- [ ] Live integration test against a throwaway Ente account if one is available Phase 7: session persistence -- [ ] `SessionStore` writing an encrypted session blob with a key from the - OS keychain (`keytar`) or a `0600` keyfile fallback +- [ ] `SessionStore` writing an encrypted session blob with a key from the OS + keychain (`keytar`) or a `0600` keyfile fallback - [ ] `Client.fromSavedSession()` and `Client.saveSession()` - [ ] `quack logout` deletes the session and the keychain entry @@ -624,8 +617,7 @@ Phase 8: CLI - [ ] `commander`-based CLI that matches the surface in the Design section - [ ] `--json` output for every command -- [ ] Reasonable progress output for long downloads (only when stdout is a - TTY) +- [ ] Reasonable progress output for long downloads (only when stdout is a TTY) Phase 9: docs and 1.0 @@ -639,18 +631,17 @@ Phase 10 and beyond: desktop client (separate repo) - [ ] Local cache (SQLite) keyed on `(collectionID, fileID, updationTime)` - [ ] Background sync worker that streams new files into the cache - [ ] Read-only gallery UI: thumbnails, full-image view, basic search -- [ ] Add upload, delete, and share back into the library before the - desktop UI exposes them +- [ ] Add upload, delete, and share back into the library before the desktop UI + exposes them ## Source attribution -The cryptographic protocol and wire format implemented here are Ente's, -taken from the Ente open source clients at -. No code is imported or vendored from -those projects; any reference code that is copied is rewritten in TypeScript -in this repository. Protocol fidelity is verified against the upstream -implementations in `web/packages/base/`, `mobile/apps/photos/lib/`, and -`cli/`. +The cryptographic protocol and wire format implemented here are Ente's, taken +from the Ente open source clients at . No code +is imported or vendored from those projects; any reference code that is copied +is rewritten in TypeScript in this repository. Protocol fidelity is verified +against the upstream implementations in `web/packages/base/`, +`mobile/apps/photos/lib/`, and `cli/`. ## License