Skip to content

Design principles

Four commitments shape every decision in BlindProof. They are load-bearing — violating any of them breaks the value proposition. They are architectural, not stylistic.

1. Plaintext never leaves the client

The backend holds only ciphertext and non-sensitive metadata. Paths, filenames, and project titles are encrypted too — they can be as revealing as the manuscript itself.

In practice: the encrypt boundary is the capture() function in client/blindproof.py. Nothing that passes through that function in the plaintext form is ever written to disk outside the user's own editor, and nothing that leaves the client is ever unencrypted. New endpoints, logs, or telemetry that could receive plaintext, paths, filenames, or titles are a bug by design.

Metadata deliberately unencrypted (needed for the dashboard and proof bundle): timestamps, sizes, word counts, session groupings, HMAC commitment hashes, Merkle roots, OTS receipts.

Encrypted: plaintext snapshots, original file bytes, paths, filenames, project names/titles.

When adding a new field to the data model, default to encrypting it unless there is a clear reason it must be queryable.

2. No novel cryptography

Boring, well-understood primitives, used in boring, well-understood ways:

Purpose Primitive
Passphrase → master key argon2id (Argon2id, server-stored salt, passphrase never leaves the client)
Master key → subkeys HKDF-SHA256enc_key + mac_key
Content encryption AES-256-GCM with a 12-byte random nonce per snapshot
Per-save commitment HMAC-SHA256(mac_key, plaintext)
Daily aggregation SHA-256 Merkle tree (Bitcoin-style, last node duplicated on odd levels)
External anchor OpenTimestamps → Bitcoin
Proof bundle signature Ed25519 over the canonical bundle bytes

Note the HMAC. A raw SHA-256 commitment would let an attacker who gained database access test hypotheses like "does this contain the sentence X?" by hashing candidate sentences and comparing. The HMAC is keyed with mac_key, derived from the author's passphrase, which the server never sees — so no offline hypothesis testing is possible.

3. Independent verifiability

verify.py — the checking tool that ships inside every proof bundle — must work with only the Python standard library plus opentimestamps-client. It must not depend on BlindProof's servers, authentication, or APIs.

A proof bundle generated today must still verify in ten or twenty years. That is the durability promise, and it is the single property that makes BlindProof legally credible: the evidence outlives the company.

In practice: the bundle is self-contained (manuscript digests, Merkle leaves and roots, OTS receipts, Ed25519 signature, the verifier script itself), and the verifier talks only to the public OpenTimestamps calendars and Bitcoin — not to us.

4. Daily Merkle granularity, not per-save

The backend aggregates each user's HMAC commitments into one Merkle root per day, and submits only that root to the public calendars.

Why not per-save?

  • Cost to the public calendars. OpenTimestamps is free and public; flooding it with per-save submissions is antisocial.
  • Session-timing leak. Per-save submissions would publish, in a fixed public record, the precise times of every save. Daily aggregation publishes only that some work happened on a given day, at roughly the scale indicated by the word-count metadata.
  • Fidelity is unchanged. The Merkle root binds every leaf; a single OTS anchor on the daily root is cryptographically equivalent to anchoring every leaf individually.

A fifth, quieter principle: no key recovery by us

The author's passphrase never reaches the backend. We cannot read their encrypted snapshots, and we cannot recover their account if they forget the passphrase.

Optional mitigation: a user-held recovery kit (the master key encrypted under a high-entropy recovery code, printed as a QR at enrolment). Losing the passphrase and the recovery sheet means losing access to the captured content — the same trade-off 1Password, Signal, and similar tools make explicit to their users.

This is communicated clearly in the author-facing docs — see Setting up and If things go wrong.