Skip to main content

Module crypto

Module crypto 

Source
Expand description

End-to-end payload encryption boundary (slice B6 of docs/proposals/multi-device-sync.md, §“Transport: Parslee-hosted relay, E2E for personal scope”).

The proposal’s trust posture: the relay is a dumb, untrusted ordered-log store. Personal-scope payloads are encrypted end-to-end, always; the relay stores only ciphertext and “can route and dedup on op_id and hlc (which stay cleartext) but cannot read conversations, memory, or secrets.” This module is the encrypt/decrypt boundary that realizes it.

§The design that keeps the shipped oplog intact

An op’s op_id is the SHA-256 content address over device_id ‖ seq ‖ prev ‖ hlc ‖ scope ‖ surface ‖ canonical(payload) (see crate::oplog). To keep op_id/seq/prev/hlc/scope/surface cleartext metadata — exactly what B3’s relay chain-verification and dedup rely on — the encryption is applied to the payload only, at authoring time: a device that wants E2E authors its op with cipher.encrypt(plaintext) as the payload, so the canonical op the whole system carries is ciphertext-native. The chain hashes over ciphertext, crate::oplog::verify_log verifies it, and the relay sees only the Envelope. A peer holding the same key recovers the plaintext with PayloadCipher::decrypt. No change to the OpRecord shape, the journal, the relay, or the fold — the ciphertext is just a serde_json::Value like any other payload.

§Real crypto, not a placeholder

LocalKeyCipher is a genuine AEAD: ChaCha20-Poly1305 with a random 96-bit nonce per op (RustCrypto chacha20poly1305). Confidentiality AND integrity — a tampered ciphertext fails the Poly1305 tag and PayloadCipher::decrypt returns CryptoError::Decrypt, never silently wrong plaintext. The key is a user-held 256-bit secret (LocalKeyCipher::load_or_generate persists it 0600 under ~/.car/sync/), never transmitted — the proposal’s “the key is user-held, derived at Parslee login, never transmitted.”

§What’s wired, what remains

Shipped + tested here and in the session:

  • Decrypt-before-foldcrate::session::SyncSession::with_key_provider encrypts each payload at append (ciphertext-native, op_id over ciphertext) and decrypts at state() after the chain verifies and before the fold groups on payload["id"]/fold_key. Op identity stays the cleartext-metadata op_id.
  • Login-derived key distributionDerivedKeyProvider HKDF-derives per-audience keys from one master; DerivedKeyProvider::from_passphrase is the zero-knowledge cross-device source (same passphrase → same keys on every device, never transmitted). LocalKeyCipher remains the raw single-key reference. Per-audience isolation via encryption_audience.
  • Checkpoints under E2Ecrate::session::SyncSession::publish_checkpoint is guarded off under a key provider: a ciphertext-folded checkpoint would form an inconsistent decrypt base, and a cleartext one would leak. The encrypted op log is retained and cold bootstrap replays it.

Remaining: per-scope encrypted checkpoint push — a whole-chain checkpoint mixes Personal + Shared{org} audiences, so it must be split per scope key (or encrypted to the personal key with org state re-derived from the org stream) before it can be pushed to an untrusted relay to restore relay-side GC; and org-key distribution via the entitlements layer.

Structs§

DerivedKeyProvider
Derives per-audience LocalKeyCiphers from one login-derived master secret (HKDF-SHA256), caching by audience. The master comes from the Parslee login (per-user) / entitlements (per-org); it is NEVER sent to the relay.
Envelope
The ciphertext form of a payload — what the relay stores and sees. Cleartext op_id/seq/hlc/scope/surface metadata lives outside this, on the crate::oplog::OpRecord; the envelope hides only the payload body.
LocalKeyCipher
The single-user reference cipher: a local 256-bit ChaCha20-Poly1305 key.

Enums§

CryptoError
A crypto-boundary failure.

Constants§

ALG_CHACHA20POLY1305
The frozen algorithm tag written into every Envelope — lets a future cipher upgrade coexist (a decryptor rejects an unknown tag rather than mis-decoding).

Traits§

PayloadCipher
The encrypt/decrypt boundary. A device authors an E2E op with cipher.encrypt(plaintext) as its payload; a peer holding the key recovers it with cipher.decrypt(&op.payload). Object-safe so a daemon can hold an Arc<dyn PayloadCipher> (a null/local reference now, a login-derived key later) without a type change.
SyncKeyProvider
Supplies the PayloadCipher for a scope’s encryption audience. The daemon holds one and asks for a cipher per op-scope, so a remote relay only ever sees ciphertext under the right (personal / org) key.

Functions§

derive_key
Derive a 256-bit AEAD key for audience from a login/entitlement master secret via HKDF-SHA256. Deterministic: the same (master, audience) yields the same key on every device (a user’s Mac and phone decrypt each other’s ops); distinct audiences (“personal” vs “org:”) yield independent keys.
encryption_audience
The encryption audience a scope maps to — the set of principals whose key a payload under this scope is encrypted to. Personal → the user’s own key; Shared{org} → the org key. The B4-pinned rule (“scopes are encryption audiences”) uses this: a single ciphertext must have a single audience, so a whole-chain checkpoint mixing scopes cannot be one ciphertext.