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.”

§Honest boundary — what lands here, what is a documented follow-up

This slice ships the boundary primitive + a local-key reference, tested (round-trip, relay-sees-ciphertext, tamper-rejected, wrong-key-rejected). What remains, called out so no one mistakes this for a finished E2E story:

  • Decrypt-before-fold wiring. The per-surface fold rules group on payload["id"]/fold_key (fact_id dedup, registry LWW-per-record), which are hidden under ciphertext. A live E2E device must therefore decrypt each op’s payload after the ciphertext chain verifies and before the fold groups it (op identity stays the cleartext-metadata op_id; only the payload is swapped). That decrypt-then-fold step in crate::session::SyncSession is the remaining engine wiring.
  • Key distribution. LocalKeyCipher is a single local key — the single-user multi-device case. Deriving it from the Parslee login secret, and the org-key distribution via the entitlements layer, is the key-management follow-up (proposal §“Open questions / Key recovery, Org-key rotation”).
  • Scopes are encryption audiences (B4 pin, binding on B6). A whole-chain checkpoint that mixes Personal + Shared{org} payloads mixes different key audiences; it must be split per scope key or encrypted to the personal key only with org-shared state re-derived from the org op-stream — it must NOT ship a single-key whole-chain ciphertext to an org audience. encryption_audience surfaces a scope’s audience tag so a caller can enforce single-audience-per-ciphertext; the per-scope checkpoint split is the follow-up (it lands with the per-scope relay streams B4 also deferred).

Structs§

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.

Functions§

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.