Documentation
¶
Overview ¶
Package keymgmt holds the safe header-write protocol shared by every operation that mutates the key-slot header. A clobbered or half-written header locks the user out of every blob under it, so writes go through SafePut: back up the current header, confirm it hasn't changed underneath us, write, then read back and verify the result still unlocks before trusting it. RestoreBackup is the recovery counterpart.
This is the one place that needs both the storage layer (backend) and the crypto layer (crypto): the backend handles backup and the raw read/write, crypto parses and unlocks the result. Keeping the orchestration here lets each of those layers stay unaware of the other.
Index ¶
- Variables
- func FollowRotations(ctx context.Context, store backend.Backend, h *crypto.Header, pinned string, ...) error
- func RestoreBackup(ctx context.Context, store backend.HeaderStore) error
- func RotateMaster(ctx context.Context, store Vault, hdr *crypto.Header, base []byte, ...) (*crypto.MasterKey, error)
- func SafePut(ctx context.Context, store backend.HeaderStore, h *crypto.Header, base []byte, ...) error
- func VerifyEpoch(ctx context.Context, store backend.HeaderStore, mk *crypto.MasterKey) error
- type Vault
Constants ¶
This section is empty.
Variables ¶
var ErrEpochChanged = errors.New("the vault's master key changed since this operation started")
ErrEpochChanged reports that the vault's header no longer wraps the master key the caller is holding: a rotation (or a substitution) landed since the caller unlocked. The caller's in-flight work was sealed under a superseded key and must not be left on storage.
var ErrNoPath = errors.New("no signed rotation path from the pinned master to the current one")
ErrNoPath reports that no chain of valid signed transitions connects the pinned master to the observed one.
Functions ¶
func FollowRotations ¶ added in v0.7.0
func FollowRotations(ctx context.Context, store backend.Backend, h *crypto.Header, pinned string, pinnedRevision int, mk *crypto.MasterKey) error
FollowRotations proves, from signed transitions alone, that the master a header now wraps descends legitimately from the master this machine pinned. It searches for a chain of valid transitions from the pinned signing key to the header's, each signed by its predecessor, all within this vault, with strictly increasing revisions that stay within the pinned revision's future and end no later than the observed header's. mk is the unlocked master the header yielded; the final hop must name exactly it, binding the chain to the key actually in hand rather than to header metadata.
A nil error means the pin may advance to the observed header silently. Any failure returns ErrNoPath: the caller falls back to the alarm it would have raised anyway — walking can clear an alarm, never create one.
func RestoreBackup ¶
func RestoreBackup(ctx context.Context, store backend.HeaderStore) error
RestoreBackup copies the header backup back over the header and confirms the result parses, so recovery from a bad write is one command instead of a raw rclone incantation. It reports a clear error when there is no backup to restore, which includes versioned remotes (recover a prior object version through the remote's version history there).
func RotateMaster ¶
func RotateMaster(ctx context.Context, store Vault, hdr *crypto.Header, base []byte, oldMK *crypto.MasterKey, verify func(*crypto.Header) (*crypto.MasterKey, error), onFlip func(*crypto.MasterKey)) (*crypto.MasterKey, error)
RotateMaster re-keys a vault: it mints a fresh master, re-encrypts every blob under it, and rewrites the header to wrap the new master under hdr's slots. hdr carries the desired slot set (unchanged for a precautionary rotate, minus a slot for offboarding). base is the current header bytes (freshness); oldMK the current master; verify re-unlocks the new header with the operator's credential. Returns the new master.
It never leaves a reader unable to decrypt, via a three-phase transition that keeps the invariant "the master the header yields is a recipient of every blob" at all times:
- widen — re-encrypt every blob to {old, new}, header still yields old.
- flip — write the new header (yields new); every blob already has new.
- narrow — re-encrypt every blob to {new} only; old can no longer decrypt.
Writes racing the rotation are handled from both sides. The narrow pass re-lists the namespace after the flip, so any object that landed before the flip — sealed under the old master by a writer that had not yet noticed the rotation — is found and re-keyed (the old-master fallback read below). Any object that lands after the flip is the writer's job: its post-write epoch check (VerifyEpoch) sees the new master and rolls the write back. An object escapes both only if the rotation crashes in the window between the flip and the end of the narrow pass; that residual is documented in the threat model.
Objects deleted while rotating (a concurrent compaction folding them away) are skipped: their content lives in that compaction's snapshot, which either the re-list sees or the compactor's own epoch check rolls back.
A crash loses the in-memory new master, but the operation is re-runnable from any state: a re-run re-keys from whatever the header currently yields.
onFlip, if non-nil, is called with the new master immediately after the header flip succeeds (before the narrow pass). The caller uses it to advance its local rollback pin to the new master at the moment it becomes authoritative, so an interrupted narrow doesn't leave the pin behind the header (which would make a re-run look like a rollback).
func SafePut ¶
func SafePut(ctx context.Context, store backend.HeaderStore, h *crypto.Header, base []byte, mk *crypto.MasterKey, verify func(*crypto.Header) (*crypto.MasterKey, error)) error
SafePut writes header h with the full safety protocol around it. It bumps h's revision, seals it (authentication tag) with mk, and writes the marshaled result.
- base is the exact header bytes read at the start of the operation (nil if the storage had no header yet). Immediately before writing, SafePut re-reads the header and aborts if it no longer matches base, so a concurrent key operation can't be silently overwritten. This is a freshness check, not a lock: a write landing in the gap between this read and the write below still races. For an operation a human runs occasionally that is an acceptable, documented residual.
- mk is the master the header should wrap; SafePut seals with it and, after writing, confirms the read-back header's authentication tag verifies under it (catches a corrupted/substituted write).
- verify re-unlocks the read-back header with the caller's own credential (a passphrase or an age identity) and must yield mk, an end-to-end check that the header is still usable by the caller before they walk away.
On any verification failure SafePut returns an error and leaves the backup in place; recover with RestoreBackup.
func VerifyEpoch ¶ added in v0.5.0
VerifyEpoch confirms mk is still the master the vault's header yields. It is the post-write half of the write-epoch protocol: a writer seals an object, then calls this; on ErrEpochChanged it removes its own object and retries under the new master. Together with rotation re-listing the namespace after its header flip (see RotateMaster), this closes the race where a write lands mid-rotation sealed under the old master and ends up unreadable by everyone once the old key is gone.
The header's recipient field is attacker-writable in principle (it is only authenticated by a tag keyed from the master it names), so a mismatch is treated as "redo the unlock ceremony" — which runs the real pin and authentication checks — never as a reason to trust anything new here. When the recipient does match mk, the tag must verify under it, so a tampered header cannot pass as "unchanged".