Skip to main content

Module org_key_provider

Module org_key_provider 

Source
Expand description

OrgAwareKeyProvider — the SyncKeyProvider that encrypts org-scoped ops under the SHARED org key K_org (mutually readable across an org’s members) while personal ops keep the per-user key. This is the activation core of the org shared brain: swapping it in is what makes Scope::Shared { org } ops readable by every member instead of only their author.

§How it works (and why the wire form doesn’t change)

For Scope::Personal it delegates to an inner per-user provider (e.g. crate::crypto::DerivedKeyProvider). For Scope::Shared { org } it takes the org’s K_org root and derives the audience key through the SAME HKDF the per-user path uses — derive_key(K_org, "org:<org>")LocalKeyCipher. The only thing that differs from the per-user provider is the master: a shared K_org instead of a per-user secret. Same audience string, same crate::crypto::Envelope shape — so the ciphertext becomes mutually readable without any wire change. That is exactly the bug being fixed: today two members derive DIFFERENT org keys from DIFFERENT per-user masters and cannot read each other; a shared K_org master makes them converge.

§No directory, no identity secret on the hot path (option C)

SyncKeyProvider::cipher_for is called per-op on both the write path (SyncSession::append) and the read path (the fold’s decrypted_tail). So this provider holds ONLY pre-resolved K_org roots — no crate::org_key_directory::OrgKeyDirectory and no X25519 identity secret. Fetching wraps, unwrapping with the identity secret, and picking the newest epoch all happen OUT OF BAND (a builder that runs at open time, in the post-audit activation slice), never inside cipher_for. HKDF for a given org runs once and is cached, so the per-op cost is a map lookup.

§Fail-closed

A member with no K_org for an org (not yet granted the key, or org-scope not activated) gets a [DenyCipher] for that scope — encrypt AND decrypt error. Never a personal cipher, never a different org’s cipher, never a freshly minted key. On write that rolls the op back (a non-member cannot author an org op); on read the ciphertext stays an opaque blob (fail-closed, never plaintext, never wrong-key plaintext).

§Single-newest epoch (structural limit)

An crate::oplog::OpRecord’s envelope carries no key-id/epoch, and cipher_for sees only &Scope, so per-op multi-epoch key selection is UNEXPRESSIBLE through this trait today. This slice therefore resolves ONE K_org per org (the newest the member can unwrap): correct for writes (always the current key) and inert for reads (nothing rotates yet). Multi-epoch decryption is a future ADDITIVE change (an authenticated key-id on the envelope) — do NOT trial-decrypt across a keyring as a stand-in (guessing keys until an AEAD opens is a wrong-key-acceptance surface). The internal map is keyed by org so it can grow to (org, epoch) without a public-API change.

§Migration seam (for the activation slice)

Because the audience string is identical in both providers, the envelope carries NO signal of which master produced it. Org ops written under the OLD per-user-derived org key will not open under K_org after activation. That is a one-time migration concern for whoever flips the switch — named here, not silently ignored.

§INERT — no wiring, no flag

Org-scope E2E must not be enabled in prod before a cryptographer audit (the login_secret → derive_x25519_identity entropy dependency). So this type is BUILT, exported, and unit-tested, but referenced from ZERO call sites in the sync-subsystem construction. Inertness is grep-provable: search the type name and only tests answer. A default-OFF flag is deliberately NOT used — a flag is still an activation path (env drift, a copied config), which is precisely what the audit exists to gate. Absence is the proof a flag cannot give.

Structs§

OrgAwareKeyProvider
A SyncKeyProvider that keys Scope::Shared { org } ops on a shared K_org and delegates Scope::Personal to an inner per-user provider. See the module docs for the hot-path, fail-closed, epoch, and inertness contracts.