Documentation
¶
Overview ¶
Package workspacekeys implements the Workspace Content Key (WCK) epoch keyring for envelope encryption of the namespace-map event log (P4-SEC-02 / P4-SEC-07).
A WCK is a 32-byte symmetric key addressed by (epoch, kid), where kid = hex(sha256(wck)) (the full digest) names the specific key (P6-SEC-02). Keying by identity rather than bare epoch lets two keys minted independently at the same epoch — a legacy joiner's self-mint and the founder's fleet key — coexist instead of clobbering each other, which is what makes refusing key overwrites safe (P6-SEC-01b): nothing ever needs to displace an existing key, because a new key lands in its own (epoch, kid) slot. Events are envelope-encrypted under the push key (the fleet key at the highest epoch); the WCK is age-wrapped to each approved device's X25519 recipient and published as a device.key.granted event. Adding a device only re-wraps the small WCK to the new recipient (one grant per held epoch), never bulk content, so a newly-approved device ingests every epoch's WCK on its first pull and decrypts the entire history. Revoking a device mints a fresh WCK at epoch+1 and wraps it to the remaining approved recipients, giving go-forward forward secrecy without re-encrypting past events (a revoked device keeps its already-downloaded history — the residual risk age's no-native-revocation model accepts, bounded by secret rotation).
The secret WCK lives only in the OS keychain / 0600 file fallback (devicekeys.HybridStore). SQLite holds only non-secret metadata (which keys this device holds, each key's origin — 'self' mint, verified 'grant', or pre-kid 'legacy' backfill — and a membership audit of grants). Only three paths may write key metadata: founder bootstrap/rotate ('self'), verified grant ingestion ('grant'), and the one-time migration backfill ('legacy') (P6-SEC-01c). This package keeps keychain/platform/state deps out of internal/sync, which depends only on the WorkspaceKeyring interface defined in internal/sync/encryptedhub.go.
Index ¶
- type Keyring
- func (k *Keyring) CurrentEpoch(ctx context.Context) (int64, error)
- func (k *Keyring) EnsureBootstrap(ctx context.Context) (int64, error)
- func (k *Keyring) GrantAllEpochs(ctx context.Context, recipient string) ([]state.Event, error)
- func (k *Keyring) IngestGrant(ctx context.Context, grant dssync.DeviceKeyGrant) error
- func (k *Keyring) Prime(ctx context.Context) error
- func (k *Keyring) PushKey(ctx context.Context) (int64, string, []byte, error)
- func (k *Keyring) Rotate(ctx context.Context) (int64, []state.Event, error)
- func (k *Keyring) WCK(epoch int64) ([]byte, bool)
- func (k *Keyring) WCKCandidates(epoch int64, kid string) [][]byte
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Keyring ¶
type Keyring struct {
Store *state.Store
KeyStore devicekeys.HybridStore
// contains filtered or unexported fields
}
Keyring is the concrete WCK keyring. It implements dssync.WorkspaceKeyring for the EncryptedHub decorator and exposes CLI-facing lifecycle methods (EnsureBootstrap / GrantAllEpochs / Rotate) for init and devices.
func New ¶
func New(store *state.Store, keyStore devicekeys.HybridStore) *Keyring
New constructs a Keyring backed by the given store and key custody store.
func (*Keyring) CurrentEpoch ¶
CurrentEpoch returns the highest WCK epoch this device holds, or 0 if none has been bootstrapped (P4-SEC-07).
func (*Keyring) EnsureBootstrap ¶
EnsureBootstrap mints the first epoch's WCK if none exists, stores it in the keychain under its (epoch, kid), and records the metadata row with origin 'self'. It returns the active epoch (the minted epoch 1, or the existing highest epoch). Only the workspace founder may reach this path (the sync founding gate and devices approve); a joiner waits for a grant (P6-SEC-02).
func (*Keyring) GrantAllEpochs ¶
GrantAllEpochs wraps the active key of every held epoch to recipient and emits one device.key.granted event per epoch (P4-SEC-07). When several keys coexist at an epoch (P6-SEC-02 collision), only the fleet key — the same preference PushKey uses — is granted, so the recipient converges onto the key the fleet encrypts under. Used by `devices approve` so a newly-approved device can decrypt the entire namespace-map history on its first pull. Returns the emitted (stamped) events.
func (*Keyring) IngestGrant ¶
IngestGrant unwraps a device.key.grant payload addressed to the local device and persists the WCK under its (epoch, kid) with origin 'grant' (P4-SEC-07 / P6-SEC-02). Grants for other recipients are a no-op. The kid is computed from the unwrapped bytes; a grant whose carried kid disagrees is rejected (forged or corrupted). Because keys are addressed by (epoch, kid), ingesting never overwrites an existing key (P6-SEC-01b): the same key is idempotent, and a different key at the same epoch lands in its own slot, coexisting with any local self-mint. The EncryptedHub decorator calls this for every verified grant seen during a Pull, in HLC order, before decrypting the rest of the batch, so a newly-approved device obtains its WCKs before decrypting history.
func (*Keyring) Prime ¶
Prime loads every held key's WCK from the keychain into the in-memory cache so subsequent WCK lookups during a Pull are pure and context-free. Legacy kid-less rows (pre-migration-00014 keys) are lazily upgraded here: the key is loaded from its legacy custody slot, its kid is computed from the bytes, the key is re-stored under the kid-aware slot (the legacy slot is left in place — a harmless duplicate), and the metadata row is rewritten with the real kid. The EncryptedHub decorator calls Prime before ingesting in-batch grants and decrypting.
func (*Keyring) PushKey ¶
PushKey returns the key new outgoing events encrypt under: the highest held epoch, and within it the fleet key — origin 'grant' is preferred over a local 'self' mint, then 'legacy' — so a legacy joiner that later receives the founder's grant at the same epoch converges onto the fleet key (P6-SEC-02). epoch == 0 with a nil error means this device holds no workspace key yet.
func (*Keyring) Rotate ¶
Rotate mints a fresh WCK at epoch+1, stores it under its (epoch, kid) with origin 'self', and wraps it to every remaining approved recipient, emitting one device.key.granted event per recipient (P4-SEC-07). Used by `devices revoke`/`lost` for go-forward forward secrecy (the revoked device is excluded because its trust_state is already revoked when Rotate runs, so ApprovedRecipients no longer contains it) and by periodic rotation (`keys rotate` / the sync age trigger). Returns the new epoch and the emitted grant events.
Every grant is WRAPPED BEFORE any state is written (post-#56 Codex review, P1): the by-far-likeliest mid-rotation failure is a malformed recipient on an approved device row, and failing after StoreWCK/RecordKeyEpoch would leave a half-minted active epoch — the caller's next push would seal events under a key whose grants never published, and the fresh created_at would keep the age trigger from retrying. With wrap-first ordering a bad recipient aborts before the epoch exists at all. The residual window is a DB/custody failure between RecordKeyEpoch and the last grant insert; callers treat any Rotate error as fatal for their cycle so it is at least loud.
func (*Keyring) WCK ¶
WCK returns the preferred WCK at an epoch from the in-memory cache (fleet 'grant' keys beat local 'self' mints beat 'legacy'), or ok=false if this device holds none. It is a convenience for callers addressing a whole epoch; decrypt paths use WCKCandidates.
func (*Keyring) WCKCandidates ¶
WCKCandidates returns the candidate WCKs for decrypting an envelope addressed to (epoch, kid), from the in-memory cache. kid != "" selects the exact key; kid == "" (a legacy pre-kid envelope) returns every held key at the epoch — the caller tries each, and the AEAD authenticates so a wrong candidate just fails. Empty means no candidate is held (the grant has not arrived). Call Prime (and ingest any in-batch grants) first.