Files
quak/README.md
sneak 16ea7b1f03 Rewrite README to match current implementation
Fixes accumulated drift from the original spec-first README:

- Intro: added paragraph describing backup, metadata decryption, and
  thumbnail repair capabilities
- Rationale: removed the 'deliberately scoped to read operations'
  claim (no longer true since thumbnail upload exists); called out
  the Go CLI's crash-on-failure bug as explicit motivation
- Getting Started: fixed library example to use actual Client.login()
  API, removed nonexistent fromSavedSession/getFile methods, added
  backup CLI example
- Layout: fixed to match actual directory structure (download/,
  backup.ts, thumbnails.ts; removed nonexistent session/)
- Session handling: replaced the fictional encrypted-keychain
  SessionStore description with the actual implementation (plain JSON
  via env-paths, consumer-managed persistence via toJSON/fromJSON)
- CLI surface: added backup, helper list-missing-thumbnails, and
  helper fix-missing-thumbnails commands
- Backup layout: new section documenting the originals/ + collections/
  symlink structure
- API reference: replaced the stale type declarations with pointers
  to the actual source files and a note that test/client/usage.test.ts
  is the authoritative API tutorial
- TODO: collapsed completed phases, kept only open items
- For LLMs: new section summarizing repo policies, TDD workflow,
  required checks, formatting rules, and pointers to REPO_POLICIES.md
  and LLM_PROSE_TELLS.md
2026-06-09 12:35:40 -04:00

359 lines
15 KiB
Markdown

# quak
quak is a WTFPL-licensed TypeScript client library and CLI by
[@sneak](https://sneak.berlin) for the [Ente](https://ente.io) end-to-end
encrypted photo hosting service. It logs in, enumerates collections and files,
and downloads individual images while decrypting them on the way to disk.
quak also includes a resilient backup command that downloads every file in the
account into a deduplicated local directory tree, skipping files that already
exist on disk and continuing past individual download failures instead of
crashing. It decrypts and persists all three metadata layers (basic, private
magic, public magic) per file, including camera info, GPS coordinates, captions,
and any face/keyword labels the Ente clients have added. A helper subcommand can
detect and regenerate missing thumbnails, encrypting and uploading them back to
the server.
## Getting Started
```bash
git clone https://git.eeqj.de/sneak/quak.git
cd quak
yarn install
yarn build
# Log in (prompts for email, password, and OTP/TOTP if required).
yarn quak login
# List the user's collections (albums).
yarn quak collections
# List files in a collection.
yarn quak files --collection 12345
# Download and decrypt a single file.
yarn quak get 67890 --out ./photo.jpg
# Back up every file in the account.
yarn quak backup ./my-backup
```
For library use:
```ts
import { Client } from "quak";
const client = await Client.login({
email: "you@example.com",
password: "your-password",
});
for (const c of await client.listCollections()) {
console.log(c.id, c.name);
const files = await client.listFiles(c.id, c.key);
for (const f of files) {
console.log(` ${f.metadata.title} [${f.metadata.fileType}]`);
}
}
// Download a file
const files = await client.listFiles(collectionID, collectionKey);
await client.downloadFile(files[0], "./photo.jpg");
// Serialize session for later (consumer handles persistence)
const snapshot = client.toJSON();
// ... later:
const restored = Client.fromJSON(snapshot);
```
## 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.
The Go CLI's backup mode crashes entirely when a single file download fails,
which makes it useless as an actual backup tool.
quak fixes these problems. This repo ships a correct, well-tested implementation
of Ente's cryptographic protocol and API surface, plus a CLI that proves the
library is enough to do real work without a UI. The backup command is resilient
by design: per-file errors are logged and the run continues.
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.
## Development workflow
All work on quak is test-driven. No exceptions.
1. Every change starts on a feature branch off `main`.
2. The first commit on the branch is the test suite for what is being added or
changed. Those tests must fail at that commit; the branch is red until the
implementation lands.
3. Subsequent commits add the implementation and any refactors needed to make
the tests pass.
4. A feature branch can only be merged into `main` when `make check` is green.
`main` is always green. The Dockerfile runs `make check`, so a red branch
cannot pass CI.
5. Tests are the canonical API documentation for this library. Every test file
is commented thoroughly enough that a reader who has never seen quak can
learn how to use it from the tests alone. Comments explain why a behavior
matters, not just what the assertion checks.
6. Test fixtures (cryptographic vectors, recorded HTTP responses, sample files)
are committed alongside their tests. Where possible they are generated by
deterministic helpers in the `test/` tree so any reviewer can reproduce them
by running the helper.
7. `git rebase -i` is allowed on a feature branch before merge to clean up the
test-then-implementation sequence into reviewable commits, but the final
history must still show tests landing before (or with) the matching
implementation.
8. The pre-commit hook installed by `make hooks` runs
`make lint && make fmt-check`, not the full `make check`. This is deliberate
so the TDD red-phase commit (failing tests, no implementation yet) can land.
The full `make check` runs as part of `docker build .`, which is what CI
executes, so a red branch still cannot reach `main`.
## Design
quak is a TypeScript library with a thin CLI wrapper. The library does the work;
the CLI is for humans.
### Layout
```
quak/
src/
crypto/ libsodium primitives (boxes, secretstreams, KDF, SRP)
api/ HTTP client (ApiClient class)
auth/ login flow (SRP + email OTP + TOTP), key unwrap
model/ decrypted Collection, File, Metadata types + decrypt fns
download/ streaming file/thumbnail download + decryption
backup.ts resilient full-account backup with dedup
thumbnails.ts detect + regenerate missing thumbnails
client.ts high-level Client class assembled from the above
index.ts public library exports
bin/
quak.ts CLI entrypoint (commander.js)
test/ unit + integration tests (vitest)
Makefile
Dockerfile
package.json
tsconfig.json
```
### Cryptography
All cryptography is done by `libsodium-wrappers-sumo` (the "sumo" build is
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).
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.
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
key yields the user's X25519 private key. The matching public key is
delivered in cleartext.
7. `crypto_box_seal_open` over the encrypted token with the user's keypair
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
`crypto_secretbox_open_easy` using the collection key. File metadata is a
secretstream blob (single chunk, TAG_FINAL) 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. Thumbnails use the same secretstream blob format as metadata.
For upload (thumbnail repair), `encryptBlob` performs the push side: a single
secretstream chunk with TAG_FINAL, returning the header and ciphertext.
### HTTP API
Production endpoints:
- API: `https://api.ente.io`
- File download CDN: `https://files.ente.io/?fileID=<id>`
- Thumbnail CDN: `https://thumbnails.ente.io/?fileID=<id>`
A custom API endpoint is configurable for self-hosted servers via the
constructor option `apiOrigin`. When set, file downloads route through
`<apiOrigin>/files/download/<id>` 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. quak uses `berlin.sneak.quak`.
Endpoints used:
- `GET /users/srp/attributes?email=<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/ott` and `POST /users/verify-email`: email OTP fallback path.
- `POST /users/two-factor/verify`: TOTP second factor.
- `GET /collections/v2?sinceTime=<usec>`: list collections changed since
microsecond timestamp; pass 0 for a full enumeration.
- `GET /collections/v2/diff?collectionID=<id>&sinceTime=<usec>`: list files in a
collection; paginate while `hasMore` is true.
- `GET https://files.ente.io/?fileID=<id>`: download encrypted file bytes.
- `POST /files/upload-url`: mint a presigned upload URL (for thumbnail repair).
- `PUT /files/thumbnail`: register an uploaded thumbnail's object key.
### Session handling
The `Client` class holds the auth token, master key, secret key, and public key
in memory. There is no on-disk session store in the library; the consumer
decides how to persist sessions.
`client.toJSON()` returns a `ClientSnapshot` (a plain serializable object with
base64-encoded keys) that the consumer can write to disk, a database, or
whatever else fits their use case. `Client.fromJSON(snapshot)` restores a
working client from that snapshot without re-authenticating.
The CLI stores the snapshot at the platform-appropriate data directory via
`env-paths`: `~/Library/Application Support/quak/session.json` on macOS,
`$XDG_DATA_HOME/quak/session.json` on Linux. The file is written with mode
`0600`. The key material is stored in cleartext in the JSON; treat this file as
you would treat the password itself.
### CLI surface
```
quak login interactive or QUAK_EMAIL/QUAK_PASSWORD
quak whoami print logged-in account as JSON
quak logout delete saved session
quak collections [--json] list all collections
quak files --collection <id> [--json] list files in a collection
quak get <fileID> [--out path] [--collection] download and decrypt a file
quak get-thumb <fileID> [--out] [--collection] download and decrypt a thumbnail
quak backup <dir> [--json] full incremental backup
quak helper list-missing-thumbnails [--json] find files with missing thumbnails
quak helper fix-missing-thumbnails [--file ids] generate + upload missing thumbnails
```
`get` and `get-thumb` search all collections for the file ID when `--collection`
is not specified. All listing and backup commands support `--json` for
machine-readable output.
### Backup layout
`quak backup <dir>` produces:
```
<dir>/
originals/
<fileID>.<ext> actual file content (one per unique file)
<fileID>.json all decrypted metadata for that file
collections/
<name>/
<title> -> ../../originals/<fileID>.<ext> (symlink)
<name>.json collection metadata + file list
```
Each file is downloaded exactly once regardless of how many collections it
appears in. On subsequent runs, existing originals are skipped. If a download
fails, the error is logged and the backup continues with the next file. The exit
code is non-zero if any files failed.
## TODO
- [ ] Retry policy: no retry on 4xx, exponential backoff on 5xx and network
errors
- [ ] Update the API reference section below to match the current implementation
- [ ] `make docker` green
- [ ] Tag `v1.0.0`
Future (desktop client, separate repo):
- [ ] Electron app skeleton consuming this library
- [ ] Local cache (SQLite) keyed on `(collectionID, fileID, updationTime)`
- [ ] Background sync worker that streams new files into the cache
- [ ] Gallery UI: thumbnails, full-image view, basic search
- [ ] Upload, delete, and share operations in the library
## API reference
The API reference section below is from an earlier draft and does not fully
reflect the current implementation. The authoritative API documentation is in
the test files, particularly `test/client/usage.test.ts` which is a literate
tutorial walking through every operation. Run `yarn test` to verify the examples
are correct.
The key types and their actual signatures can be found in:
- `src/client.ts`: `Client`, `LoginOptions`, `ClientSnapshot`
- `src/api/client.ts`: `ApiClient`, `ApiClientOptions`, `ApiError`
- `src/auth/types.ts`: `KeyAttributes`, `SRPAttributes`,
`AuthorizationResponse`, `LoginChallenge`
- `src/model/types.ts`: `Collection`, `EnteFile`, `FileMetadata`, `FileBlob`,
`RawCollection`, `RawEnteFile`, `RawMagicMetadata`
- `src/download/index.ts`: `DownloadResult`
- `src/backup.ts`: `BackupResult`, `BackupError`
- `src/thumbnails.ts`: `MissingThumbnailInfo`, `ThumbnailFixResult`
## Source attribution
The cryptographic protocol and wire format implemented here are Ente's, taken
from the Ente open source clients at <https://github.com/ente-io/ente>. 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/`.
## For LLMs
If you are an LLM agent working on this repository, read and follow these
documents:
- **`REPO_POLICIES.md`** in the repo root. It is copied from
<https://git.eeqj.de/sneak/prompts> and covers repository structure, tooling,
Makefile targets, Dockerfile conventions, dependency pinning, and commit
hygiene. All external dependencies must be pinned by cryptographic hash in
`yarn.lock`. Never `git add -A`. Never force-push to main.
- **The "Development workflow" section above.** All changes go on feature
branches. Tests are written first and committed in a failing state before the
implementation. Tests are the canonical API documentation and must be
commented thoroughly. `main` is always green.
- **Required checks before every commit:** `make lint` (eslint + prettier check)
and `make fmt-check` must pass. The pre-commit hook enforces this.
`make check` (which also runs tests) must pass before merging to `main`.
- **Formatting:** prettier with 4-space indents and `proseWrap: always` for
markdown. Use `make fmt` to format. Use `yarn` not `npm`.
- **Testing:** vitest. Tests go in `test/` mirroring the `src/` structure.
`make test` must complete in under 20 seconds. Use `mkdtempSync` for temporary
directories, never manual timestamp paths.
- **Code style:** `const` for everything, `let` if reassignment is needed, never
`var`. Avoid unnecessary comments. No hand-rolled crypto. The
`LLM_PROSE_TELLS.md` document in the prompts repo applies to any prose written
in this repository (README, comments, commit messages).
## License
WTFPL. See [LICENSE](LICENSE).
## Author
[@sneak](https://sneak.berlin)