Client¶
The client is a single-file Python script at client/blindproof.py. It uses PEP 723 inline metadata for its runtime dependencies, so a fresh machine with only uv installed can run it with no setup:
uv run client/blindproof.py init ~/.blindproof
uv run client/blindproof.py watch ~/Documents/MyNovel
uv run client/blindproof.py enrol
uv run client/blindproof.py sync
Per-save data flow¶
sequenceDiagram
participant Editor
participant Watcher as SnapshotHandler
participant Capture as capture_path()
participant Crypto as AES-GCM + HMAC
participant Disk as Local store
Editor->>Watcher: file saved (.md, .txt)
Watcher->>Watcher: suffix filter + per-path HMAC dedup
Watcher->>Capture: read + extract plaintext
Capture->>Crypto: AES-256-GCM encrypt<br/>HMAC-SHA256 commit
Crypto->>Disk: ciphertext/<uuid>.bin
Crypto->>Disk: Snapshot row (SQLite)
watchdogfireson_modified/on_created/on_movedevents (the last is important — many editors save via write-temp-then-rename).SnapshotHandlerfilters by suffix (.md,.txtfor now) and deduplicates by per-path last-HMAC — saving the same bytes twice only records one snapshot.capture_path(path, store, ciphertext_dir, keys, now)reads the file, extracts plaintext, and hands off tocapture().capture()derives a nonce, encrypts the plaintext withAES-256-GCM, computesHMAC-SHA256(mac_key, plaintext), and writes the ciphertext to<store>/ciphertext/<uuid>.bin.- A
Snapshotrow lands in SQLite.
Plaintext lives only in RAM and only for the duration of the capture call. It is never written to disk outside the user's own editor.
Public API¶
All in client/blindproof.py:
Text and crypto¶
| Symbol | Purpose |
|---|---|
extract(suffix, raw) |
Plain text extraction for .md / .txt (UTF-8, BOM and CRLF handled). |
word_count(text), char_count(text) |
Metadata. |
derive_master_key(passphrase, salt) |
argon2id — server-stored salt, passphrase never leaves client. |
derive_subkeys(master_key) → Keys |
HKDF-SHA256 → enc_key + mac_key. |
hmac_commit(mac_key, plaintext) |
Per-save commitment. |
encrypt(enc_key, plaintext) → (nonce, ciphertext) |
AES-256-GCM, 12-byte random nonce. |
decrypt(enc_key, nonce, ciphertext) |
Inverse; raises on auth-tag failure. |
Capture and store¶
| Symbol | Purpose |
|---|---|
Snapshot |
Immutable record: captured_at, path, file_type, plaintext_hmac, nonce, ciphertext_ref, ciphertext_size, word_count, char_count, synced_at. |
capture(path, raw, keys, now) → CapturedData |
Pure encryption + metadata path, no I/O. |
capture_path(path, store, ciphertext_dir, keys, now) |
Full snapshot from disk: read, capture, persist. |
restore_snapshot(snapshot, ciphertext_dir, keys) |
Decrypt back to plaintext bytes. |
SnapshotStore |
SQLite persistence: record / list / count / get / latest_for_path / list_unsynced / mark_synced. Thread-safe. ISO-8601 timestamps. Additive on-open migrations (no migration framework). |
SnapshotHandler |
watchdog handler with suffix and directory filters, per-path HMAC dedup, on_moved routed through capture. |
Backend sync¶
| Symbol | Purpose |
|---|---|
BackendClient |
urllib-based HTTP client with injectable transport for tests. Methods: enrol, login, upload_snapshot, request_proof_bundle. |
sync_snapshots(store, ciphertext_dir, keys, client, now) |
For each unsynced snapshot: encrypt the path with enc_key (fresh nonce per upload), read ciphertext from disk, POST to /api/snapshots, stamp synced_at. |
load_backend_config, save_backend_config |
backend.json under the store dir: token + argon2 salt + server URL. |
Passphrase caching¶
| Symbol | Purpose |
|---|---|
PassphraseCache |
Protocol. |
InMemoryPassphraseCache |
Tests. |
KeyringPassphraseCache |
Real use; delegates to the OS keyring. |
BLINDPROOF_PASSPHRASE_INSECURE env var |
Smoke-test backdoor; never use in production. |
CLI¶
parse_args() + main() dispatch to these subcommands:
init <store_dir>— create a store and derive the master key.watch <path>— start the file watcher.restore <snapshot_id> <out_path>— decrypt a captured snapshot.enrol— register with the backend; writesbackend.json.sync— push unsynced snapshots.
Store location is configurable via BLINDPROOF_STORE_DIR; default ~/.blindproof.
What's deliberately not in the client¶
- Auto-sync.
watchcaptures;syncuploads. Decoupling them keeps capture resilient when the network is down. - Embedded dashboard. The GUI opens
https://blindproof.tomd.org/dashboardin the system browser instead of bundling a webview. .docx/ Scrivener / Google Docs extractors. On the roadmap (seeblindproof_spec.md§9); the current client supports.mdand.txtonly. The POC environment has no local Word install to verify the.docxpath end-to-end.- Key rotation. V1. Today, rotating a passphrase means creating a new store.
See also¶
- Running locally — exact commands.
- Testing — client test suite, fakes, and TDD workflow.
- Backend API — what the client posts to and how authentication works.