Skip to content

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)
  1. watchdog fires on_modified / on_created / on_moved events (the last is important — many editors save via write-temp-then-rename).
  2. SnapshotHandler filters by suffix (.md, .txt for now) and deduplicates by per-path last-HMAC — saving the same bytes twice only records one snapshot.
  3. capture_path(path, store, ciphertext_dir, keys, now) reads the file, extracts plaintext, and hands off to capture().
  4. capture() derives a nonce, encrypts the plaintext with AES-256-GCM, computes HMAC-SHA256(mac_key, plaintext), and writes the ciphertext to <store>/ciphertext/<uuid>.bin.
  5. A Snapshot row 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; writes backend.json.
  • sync — push unsynced snapshots.

Store location is configurable via BLINDPROOF_STORE_DIR; default ~/.blindproof.

What's deliberately not in the client

  • Auto-sync. watch captures; sync uploads. Decoupling them keeps capture resilient when the network is down.
  • Embedded dashboard. The GUI opens https://blindproof.co.uk/dashboard in the system browser instead of bundling a webview.
  • .docx / Scrivener / Google Docs extractors. On the roadmap (see blindproof_spec.md §9); the current client supports .md and .txt only. The POC environment has no local Word install to verify the .docx path end-to-end.
  • Key rotation. V1. Today, rotating a passphrase means creating a new store.

See also