Reflow README to prettier 80-column prose wrap
This commit is contained in:
141
README.md
141
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=<id>`
|
||||
|
||||
A custom API endpoint is configurable for self-hosted servers via the
|
||||
`ENTE_API_ENDPOINT` environment variable. When set, file downloads route
|
||||
through `<endpoint>/files/download/<id>` instead of the dedicated CDN host.
|
||||
`ENTE_API_ENDPOINT` environment variable. When set, file downloads route through
|
||||
`<endpoint>/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. quack uses
|
||||
`berlin.sneak.quack`.
|
||||
- `X-Client-Package`: identifies the client. quack uses `berlin.sneak.quack`.
|
||||
|
||||
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/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 /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.
|
||||
|
||||
### 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 <id>`: list files in a collection (id, name,
|
||||
type, creation time, size).
|
||||
- `quack files --collection <id>`: list files in a collection (id, name, type,
|
||||
creation time, size).
|
||||
- `quack get <fileID> --out <dir>`: download and decrypt a file.
|
||||
- `quack get-thumb <fileID> --out <dir>`: download and decrypt a
|
||||
thumbnail.
|
||||
- `quack get-thumb <fileID> --out <dir>`: 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
|
||||
<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/`.
|
||||
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/`.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
Reference in New Issue
Block a user