Documentation
¶
Overview ¶
Package keys implements the key service: the Data Key (DK), Recovery Key (RK), and Server Runtime Wrap (SRW) model from decisions.md ("Trust & key model").
Hard rules (decisions.md, AGENTS.md):
- Never log key material (DK, RK, SRW blobs) or expose it in the admin UI.
- The plaintext RK is generated/used client-side only and never crosses the network.
- The envelope format is a versioned, long-term compatibility promise; the standalone decrypt CLI must parse it forever (see envelope.go for the wire format specification).
We do not hand-roll cryptography: the envelope uses XChaCha20-Poly1305 and Argon2id from golang.org/x/crypto. Data crypto and dedup remain kopia's job (decisions.md #5).
Index ¶
- Constants
- Variables
- func DecodeRecoveryKey(s string) ([]byte, error)
- func EncodeRecoveryKey(entropy []byte) string
- func GenerateDK() ([]byte, error)
- func GenerateRecoveryKey() (display string, secret []byte, err error)
- func GenerateSRWKey() ([]byte, error)
- func UnwrapRK(w WrappedDK, rk []byte) ([]byte, error)
- func UnwrapSRW(w WrappedDK, srwKey []byte) ([]byte, error)
- func Zeroize(b []byte)
- type ArgonParams
- type Clock
- type EnvelopeInfo
- type MemoryStore
- func (m *MemoryStore) Delete(spaceID string)
- func (m *MemoryStore) GetRK(ctx context.Context, spaceID string) (WrappedDK, error)
- func (m *MemoryStore) GetSRW(ctx context.Context, spaceID string) (WrappedDK, error)
- func (m *MemoryStore) PutRK(ctx context.Context, spaceID string, w WrappedDK) error
- func (m *MemoryStore) PutSRW(ctx context.Context, spaceID string, w WrappedDK) error
- func (m *MemoryStore) Spaces(ctx context.Context) ([]string, error)
- func (m *MemoryStore) Status(ctx context.Context, spaceID string) (Status, error)
- type NotFoundError
- type SRWWrapper
- type SpaceKeys
- type Status
- type Store
- type TWSealer
- type WrapKind
- type WrappedDK
- type Wrapper
Constants ¶
const DKSize = 32
DKSize is the Data Key length in bytes (256-bit, decisions.md).
const EnvelopeVersion = 1
EnvelopeVersion is the current key-envelope format version. It is part of a long-term compatibility promise (decisions.md): bump only additively and keep the decrypt CLI able to parse every historical version.
const (
// RKPrefix is the versioned Recovery Key prefix.
RKPrefix = "ocbk1"
)
const SRWKeySize = kekSize
SRWKeySize is the required length of the Server Runtime Wrap key, and of the Target Wrap key (both are 256-bit random cluster/KMS secrets).
Variables ¶
var ( // ErrBadEnvelope indicates a malformed or unsupported envelope. ErrBadEnvelope = errors.New("keys: malformed or unsupported envelope") // ErrUnwrap indicates authentication failure: wrong key or tampered blob. ErrUnwrap = errors.New("keys: cannot unwrap (wrong key or tampered envelope)") )
Envelope errors. They are deliberately coarse: an unwrap failure must not reveal whether the key, the ciphertext, or the header was at fault, and must never echo key material.
var DefaultArgonParams = ArgonParams{
Time: 3,
MemoryKiB: 64 * 1024,
Lanes: 4,
SaltLen: 16,
}
DefaultArgonParams are the current Argon2id costs for RK-derived KEKs. Chosen for an interactive, browser-capable ceremony (Phase 8 runs the same KDF in WebAssembly): 64 MiB / 3 passes / 4 lanes is the widely recommended interactive baseline and stays feasible in-browser.
Raising these is safe: new envelopes record the new costs, old envelopes keep unwrapping with theirs.
var ErrBadRecoveryKey = errors.New("keys: invalid recovery key")
ErrBadRecoveryKey indicates a malformed, mistyped, or wrong-version RK.
var ErrWeakEnvelope = errors.New("keys: recovery envelope key derivation is below the accepted minimum")
ErrWeakEnvelope indicates an envelope whose key derivation is below MinArgonParams. It is distinct from ErrBadEnvelope because the two need different answers: a malformed blob is a bug, a weak one is a client that must redo the ceremony with stronger parameters.
var MinArgonParams = ArgonParams{
Time: 2,
MemoryKiB: 19 * 1024,
Lanes: 1,
SaltLen: 16,
}
MinArgonParams is the weakest Argon2id work factor the service accepts on a client-produced Recovery Key envelope.
19 MiB / 2 passes / 1 lane is OWASP's low-memory Argon2id baseline. It is deliberately below DefaultArgonParams: the floor exists to reject envelopes that are trivially crackable, not to force every client onto the server's preferred costs. A device too slow for the default may legitimately tune down; one that tunes below this is producing an envelope not worth storing.
Raising this is a compatibility decision, not a free one: envelopes already stored keep unwrapping (the floor is only checked on the way in), but a client that was accepted yesterday can be refused tomorrow.
Functions ¶
func DecodeRecoveryKey ¶
DecodeRecoveryKey parses a user-entered Recovery Key back into the raw secret used for wrapping. It is tolerant of case, spaces, and missing/extra dashes, and maps Crockford look-alike characters, because users retype these by hand.
It never includes the input in its error, so a mistyped key cannot leak into logs (AGENTS.md: never log key material).
func EncodeRecoveryKey ¶
EncodeRecoveryKey renders raw entropy as the display form.
func GenerateDK ¶
GenerateDK returns a fresh 256-bit Data Key from crypto/rand. The caller owns the buffer and should Zeroize it when done.
func GenerateRecoveryKey ¶
GenerateRecoveryKey returns a fresh Recovery Key: the human-transportable string to show the user once, plus the raw secret bytes used for wrapping.
The caller must show the string exactly once and never persist it server-side (decisions.md: no server-side RK escrow). Zeroize the returned secret when done.
func GenerateSRWKey ¶
GenerateSRWKey returns a fresh 256-bit key suitable for SRW or TW custody. Operators generate this once and store it in a K8s secret / KMS.
func UnwrapRK ¶
UnwrapRK recovers a Data Key from an RK envelope. Argon2id parameters come from the envelope itself, so envelopes written under older parameters keep working after a parameter bump.
Types ¶
type ArgonParams ¶
type ArgonParams struct {
// Time is the number of passes.
Time uint32
// MemoryKiB is the memory cost in KiB.
MemoryKiB uint32
// Lanes is the degree of parallelism.
Lanes uint8
// SaltLen is the salt length in bytes.
SaltLen uint8
}
ArgonParams are the Argon2id cost parameters recorded in an RK envelope. They are stored per envelope so parameters can be raised over time without breaking old envelopes (phase-3 testing requirement: "old envelopes still unwrap after a parameter bump").
type EnvelopeInfo ¶
type EnvelopeInfo struct {
Version int
Kind WrapKind
KDF string
Argon ArgonParams
}
EnvelopeInfo describes an envelope without revealing key material. It is safe to log or expose in a status response.
func CheckRecoveryEnvelope ¶
func CheckRecoveryEnvelope(blob []byte) (EnvelopeInfo, error)
CheckRecoveryEnvelope validates a client-produced RK envelope against service policy and returns its metadata. It performs no decryption and needs no key.
The checks, in the order a caller cares about them:
- the blob parses as an envelope of this format;
- it is an RK wrap, not an SRW or TW one — storing anything else as a Space's recovery envelope would strand the user's only way back in;
- it is Argon2id-derived, because an RK is a human-transportable secret and a direct-key envelope would mean the client used something else entirely;
- its work factor meets MinArgonParams.
What it deliberately cannot check is whether the envelope actually wraps the Data Key the caller claims: that would need the Recovery Key, which never reaches the server. See the client invariant in key-envelope-format.md §5.
func Inspect ¶
func Inspect(blob []byte) (EnvelopeInfo, error)
Inspect parses an envelope header and reports its metadata. It performs no decryption and needs no key — used by keystatus and by the decrypt CLI to show what a blob needs before prompting for the Recovery Key.
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is a concurrency-safe in-memory Store.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
NewMemoryStore constructs an empty MemoryStore using the system clock.
func NewMemoryStoreWithClock ¶
func NewMemoryStoreWithClock(c Clock) *MemoryStore
NewMemoryStoreWithClock constructs a MemoryStore with an injected clock.
func (*MemoryStore) Delete ¶
func (m *MemoryStore) Delete(spaceID string)
Delete removes a space's key record. Used by tests and teardown; it does not destroy snapshot data.
type NotFoundError ¶
type NotFoundError struct{ SpaceID string }
NotFoundError is returned by Store when no envelope exists for a space.
func (NotFoundError) Error ¶
func (e NotFoundError) Error() string
type SRWWrapper ¶
type SRWWrapper struct {
// contains filtered or unexported fields
}
SRWWrapper is the server-side Wrapper implementation: it holds the SRW key (from a K8s secret / KMS) and unwraps DKs for the unattended worker.
The key is held in memory only; it is never logged, never returned, and never exposed through the API (decisions.md #1).
func NewSRWWrapper ¶
func NewSRWWrapper(srwKey []byte) (*SRWWrapper, error)
NewSRWWrapper constructs a wrapper around a 32-byte SRW key. The wrapper copies the key so the caller may zeroize its own buffer.
type SpaceKeys ¶
type SpaceKeys struct {
SpaceID string `json:"space_id"`
RK WrappedDK `json:"rk,omitzero"`
SRW WrappedDK `json:"srw,omitzero"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
SpaceKeys is the pre-versioned per-Space key record: both envelopes in one document. Since the R1 remediation each envelope is stored on its own, under keyenvelopes/<space-id>/{rk,srw}/<nanos>; this type remains because those older documents are still read (and never rewritten).
It holds only wrapped material. The Argon2id parameters live inside the RK envelope itself, so the record stays simple and the envelope self-contained.
type Status ¶
type Status struct {
SpaceID string
// Configured reports whether both wraps exist for the space.
Configured bool
// HasRK / HasSRW allow the UI to detect a half-finished setup.
HasRK bool
HasSRW bool
// RKVersion / SRWVersion are the envelope format versions in use.
RKVersion int
SRWVersion int
CreatedAt time.Time
UpdatedAt time.Time
}
Status is the key-material-free summary of a space's backup key setup. It is what GET /keystatus serves (phase-3 deliverable 2): configured? created when? which wrap versions?
type Store ¶
type Store interface {
// GetSRW returns the SRW-wrapped DK for a space, or NotFoundError.
GetSRW(ctx context.Context, spaceID string) (WrappedDK, error)
// PutSRW stores the SRW-wrapped DK for a space.
PutSRW(ctx context.Context, spaceID string, w WrappedDK) error
// GetRK returns the RK-wrapped DK for a space (for Take-Out export), or
// NotFoundError.
GetRK(ctx context.Context, spaceID string) (WrappedDK, error)
// PutRK stores the RK-wrapped DK for a space.
PutRK(ctx context.Context, spaceID string, w WrappedDK) error
// Status reports whether a space is configured and with which envelope
// versions. It returns no key material.
Status(ctx context.Context, spaceID string) (Status, error)
// Spaces returns the ids of every space holding an envelope, in order. It
// exists for the operations that must visit all of them — rotating the SRW
// key, above all — and returns ids only, never envelopes.
Spaces(ctx context.Context) ([]string, error)
}
Store persists and retrieves wrapped Data Keys per Space. It never stores or returns plaintext key material. Every method honours ctx: a cancelled request or a stopping run stops the store operation it started.
type TWSealer ¶
type TWSealer struct {
// contains filtered or unexported fields
}
TWSealer seals and opens arbitrary secret payloads under the Target Wrap key. internal/targets adapts this to its CredSealer interface, keeping the crypto here.
func NewTWSealer ¶
NewTWSealer constructs a sealer around a 32-byte Target Wrap key. The key is copied so the caller may zeroize its own buffer. It is never logged or returned.
type WrapKind ¶
type WrapKind int
WrapKind records which key wrapped a DK.
const ( // WrapUnknown is the zero value and must not be persisted. WrapUnknown WrapKind = iota // WrapRK is a DK wrapped by the user's Recovery Key (client-side). WrapRK // WrapSRW is a DK wrapped by the Server Runtime Wrap key (cluster secret/KMS). WrapSRW // WrapTW is a credential blob wrapped by the Target Wrap key // (decisions.md #14). It shares this envelope primitive but never wraps a DK. WrapTW )
type WrappedDK ¶
type WrappedDK struct {
// Version is the envelope format version (see EnvelopeVersion).
Version int `json:"version"`
// Kind distinguishes an RK-wrapped envelope from an SRW-wrapped one.
Kind WrapKind `json:"kind"`
// Blob is the opaque wrapped key material. Never logged.
Blob []byte `json:"blob,omitempty"`
// CreatedAt records when this envelope was produced. Metadata only.
CreatedAt time.Time `json:"created_at,omitzero"`
}
WrappedDK is a Data Key wrapped for storage. The plaintext DK never appears in a persisted WrappedDK. Which KEK wrapped it is recorded by Kind.
Blob is the fully self-contained serialized envelope (magic, version, kind, KDF parameters, salt, nonce, ciphertext) — see envelope.go. Keeping the envelope self-describing is what lets the offline decrypt CLI (Phase 5) parse a blob with nothing but the RK.
func RotateRK ¶
func RotateRK(old WrappedDK, oldRK, newRK []byte, params ArgonParams) (WrappedDK, error)
RotateRK re-wraps the DK under a new Recovery Key. Client-side / CLI only.
func RotateSRW ¶
RotateSRW re-wraps the DK under a new SRW key without touching snapshot data. The old envelope is superseded: it no longer opens under the new key, while the DK — and therefore every existing snapshot — is unchanged.
func WrapWithRK ¶
func WrapWithRK(dk, rk []byte, params ArgonParams) (WrappedDK, error)
WrapWithRK wraps a Data Key under a Recovery Key using Argon2id + AEAD.
Runs client-side or in the offline CLI — the server never receives a plaintext RK. params allows raising Argon2id costs over time; pass DefaultArgonParams for the current baseline.
func WrapWithSRW ¶
WrapWithSRW wraps a Data Key under the server-held SRW key. The SRW key is already uniformly random, so no KDF stretching is applied.
type Wrapper ¶
type Wrapper interface {
// UnwrapSRW recovers the plaintext DK from an SRW envelope using the
// server-held SRW key. Used by the unattended worker.
UnwrapSRW(w WrappedDK) (dk []byte, err error)
}
Wrapper unwraps a Data Key for use inside a single run. The plaintext DK it returns lives only in memory and must never be logged or persisted.