Documentation
¶
Overview ¶
Package encryption implements the AES-256-GCM envelope encryption scheme described in docs/design/2026_04_29_proposed_data_at_rest_encryption.md §4.
Stage 0 (foundation) provides the primitive Encrypt/Decrypt operations, the wire format envelope encoder/decoder, and the in-memory keystore. Composition of AAD bytes for storage-layer envelopes (§4.1) and raft-layer envelopes (§4.2) is the responsibility of callers in store/ and internal/raftengine/etcd/, added in later stages.
Wire format (§4.1):
+--------+------+---------+----------+-----------+--------+ | 0x01 | flag | key_id | nonce | ciphertext| tag | | 1 byte | 1 B | 4 bytes | 12 bytes | N bytes | 16 B | +--------+------+---------+----------+-----------+--------+
Per-value overhead is 34 bytes (HeaderSize + TagSize).
Index ¶
- Constants
- Variables
- func AppendHeaderAADBytes(dst []byte, version, flag byte, keyID uint32) []byte
- func HeaderAADBytes(version, flag byte, keyID uint32) []byte
- type Cipher
- type Envelope
- type Keystore
- func (k *Keystore) AEAD(keyID uint32) (cipher.AEAD, bool)
- func (k *Keystore) DEK(keyID uint32) ([KeySize]byte, bool)
- func (k *Keystore) Delete(keyID uint32)
- func (k *Keystore) Has(keyID uint32) bool
- func (k *Keystore) IDs() []uint32
- func (k *Keystore) Len() int
- func (k *Keystore) Set(keyID uint32, dek []byte) error
Constants ¶
const ( // EnvelopeVersionV1 is the current envelope format version. §11.3 // reserves 0x02..0x0F for future authenticated formats. The current // build only understands 0x01; ANY other version byte (including the // 0x02..0x0F reserved range) causes DecodeEnvelope to return // ErrEnvelopeVersion. Future decoders that know how to handle the // reserved range will widen this check. EnvelopeVersionV1 byte = 0x01 // FlagCompressed (bit 0) is set when ciphertext encrypts a Snappy- // compressed plaintext (§6.4). The flag participates in the AAD so a // post-hoc bit-flip is rejected by GCM verification. FlagCompressed byte = 1 << 0 // KeySize is the AES-256 key length in bytes. KeySize = 32 // NonceSize is the AES-GCM standard nonce size in bytes. NonceSize = 12 // TagSize is the AES-GCM authentication tag size in bytes. TagSize = 16 // HeaderAADSize covers version + flag + key_id (the bytes that // participate in storage AAD, distinct from the full envelope header // which also carries nonce). Exposed as the input length of // HeaderAADBytes. HeaderAADSize = versionBytes + flagBytes + keyIDBytes // 6 // HeaderSize covers version + flag + key_id + nonce, in that order. HeaderSize = HeaderAADSize + NonceSize // 18 // EnvelopeOverhead is the per-value byte overhead introduced by the // envelope: HeaderSize + TagSize. EnvelopeOverhead = HeaderSize + TagSize // 34 // ReservedKeyID is the cluster-wide "not bootstrapped" sentinel // (§5.1). Implementations MUST refuse to install or look up this // key_id. ReservedKeyID uint32 = 0 )
Public constants for the §4.1 wire format.
Variables ¶
var ( // ErrUnknownKeyID is returned when a wrap/unwrap call references a key_id // that is not present in the Keystore. Surfaces as `unknown_key_id` on // the §9.2 elastickv_encryption_decrypt_failures_total counter. ErrUnknownKeyID = errors.New("encryption: unknown key_id") // ErrReservedKeyID is returned when a caller tries to install or use // key_id 0; that value is reserved cluster-wide as the // "not bootstrapped" sentinel per §5.1. ErrReservedKeyID = errors.New("encryption: key_id 0 is reserved as the not-bootstrapped sentinel") // ErrBadNonceSize indicates the nonce passed to Encrypt/Decrypt was not // exactly NonceSize bytes. ErrBadNonceSize = errors.New("encryption: nonce size invalid") // ErrBadKeySize indicates the DEK passed to Keystore.Set was not exactly // KeySize bytes (AES-256 requires 32). ErrBadKeySize = errors.New("encryption: DEK size invalid") // ErrIntegrity indicates a GCM tag mismatch on Decrypt — i.e., the // ciphertext was tampered with, the AAD does not match the one used at // Encrypt, or the wrong DEK is loaded. Per §4.1, callers MUST treat this // as a typed read error and never silently zero or retry. ErrIntegrity = errors.New("encryption: integrity check failed (GCM tag mismatch)") // ErrEnvelopeShort indicates DecodeEnvelope received fewer bytes than the // minimum envelope size (HeaderSize + TagSize). ErrEnvelopeShort = errors.New("encryption: envelope shorter than header+tag") // ErrEnvelopeVersion indicates DecodeEnvelope saw a version byte the // current build does not know how to parse. Reserved values per §11.3. ErrEnvelopeVersion = errors.New("encryption: unknown envelope version") // ErrNilKeystore indicates NewCipher was called with a nil Keystore. // Surfaced at construction time so a wiring mistake is caught // before the first Encrypt/Decrypt would otherwise nil-deref panic. ErrNilKeystore = errors.New("encryption: keystore is nil") // ErrKeyConflict indicates Keystore.Set was called with a keyID // already loaded under DIFFERENT key material. Replacing live key // bytes for an in-use key_id would render every envelope already // persisted under that id undecryptable, so Set fails closed // rather than silently overwriting. Set with the SAME bytes is // idempotent (returns nil) and does not raise this error. ErrKeyConflict = errors.New("encryption: key_id already loaded with different key material") )
Functions ¶
func AppendHeaderAADBytes ¶
AppendHeaderAADBytes appends the same 6-byte header prefix (version, flag, key_id) onto dst and returns the extended slice. Allocation-free when dst already has HeaderAADSize spare capacity, which lets storage callers in later stages write the AAD directly into a pooled buffer alongside the per-record context (e.g., pebble_key) without an intermediate make().
func HeaderAADBytes ¶
HeaderAADBytes returns the first 6 bytes of the envelope header (version, flag, key_id) in their on-disk order. These bytes participate in the §4.1 storage-layer AAD (storage AAD = HeaderAADBytes ‖ pebble_key) and in the §4.2 raft-layer AAD's middle slice (raft AAD = "R" ‖ version ‖ key_id, computed by raft-layer callers in a later stage).
Allocates HeaderAADSize bytes. Hot-path callers should prefer AppendHeaderAADBytes to reuse a buffer.
Types ¶
type Cipher ¶
type Cipher struct {
// contains filtered or unexported fields
}
Cipher is the AES-256-GCM primitive over a Keystore.
Cipher does NOT compose AAD — callers in store/ (§4.1 AAD) and internal/raftengine/etcd/ (§4.2 AAD) supply the full AAD bytes. This keeps the cipher narrow and lets each layer choose the right AAD without baking storage/raft assumptions into the foundation package.
AES key expansion and GCM initialization happen once per DEK at Keystore.Set time; the hot path only needs an atomic.Pointer load and a Seal/Open call.
The zero value is NOT safe to use: Encrypt/Decrypt return ErrNilKeystore for a zero-value or nil Cipher rather than nil-deref panicking. Always construct via NewCipher.
func NewCipher ¶
NewCipher returns a Cipher backed by ks.
Returns ErrNilKeystore if ks is nil. Catching this at construction time turns a wiring mistake into a typed error during process startup or DEK rotation, rather than a nil-deref panic on the first Encrypt/Decrypt — important for the dynamic dependency-wiring paths where the encryption stack may be re-initialised after a sidecar resync (§5.5) or rotation (§5.2).
func (*Cipher) Decrypt ¶
Decrypt verifies and decrypts (ciphertext ‖ tag) using the DEK identified by keyID, the supplied nonce, and the same aad bytes that were passed to Encrypt.
On GCM tag mismatch, Decrypt returns an error wrapping ErrIntegrity. Per §4.1, callers MUST treat this as a typed read error and never silently zero or retry. The original Open error is attached as a secondary cause for diagnostic logging.
func (*Cipher) Encrypt ¶
Encrypt produces (ciphertext ‖ tag) for plaintext under the DEK identified by keyID and the supplied nonce. aad is treated verbatim.
Constraints:
- keyID must not be ReservedKeyID; otherwise ErrReservedKeyID.
- nonce must be NonceSize bytes; otherwise ErrBadNonceSize.
- keyID must be present in the Keystore; otherwise ErrUnknownKeyID.
CRITICAL: callers MUST NOT reuse the same (keyID, nonce) pair with any two distinct plaintexts. Nonce reuse under AES-GCM is catastrophic: it leaks the XOR of the two plaintexts and enables authentication-key recovery. The §4.1 storage-layer integration uses the nonce construction (node_id ‖ local_epoch ‖ write_count) to guarantee uniqueness by construction; do not substitute a different nonce scheme in that layer without a corresponding uniqueness proof. (For tests / benchmarks, fresh crypto/rand nonces are perfectly safe.)
The returned slice has length len(plaintext) + TagSize. It is freshly allocated; the caller may retain it indefinitely.
type Envelope ¶
type Envelope struct {
Version byte
Flag byte
KeyID uint32
Nonce [NonceSize]byte
// Body is the concatenation of ciphertext and the GCM tag, as produced
// by AEAD.Seal. Length is plaintext_len + TagSize.
Body []byte
}
Envelope is the parsed form of the §4.1 wire format.
func DecodeEnvelope ¶
DecodeEnvelope parses an envelope. It does NOT verify the GCM tag — authentication happens at Cipher.Decrypt time once the AAD is known.
DecodeEnvelope copies Body so the returned Envelope does not alias src.
func (*Envelope) Encode ¶
Encode serialises the envelope into a single byte slice using the §4.1 wire format. The returned slice is freshly allocated.
Encode validates the envelope at build time so a programmer error (uninitialised Version, truncated Body) fails here with a clear stack trace, rather than surfacing later as a confusing DecodeEnvelope or Cipher.Decrypt failure on the read side. Returns:
- ErrEnvelopeVersion if Version is not EnvelopeVersionV1.
- ErrEnvelopeShort if Body is shorter than TagSize (every valid body must contain at least the GCM tag).
type Keystore ¶
type Keystore struct {
// contains filtered or unexported fields
}
Keystore is a copy-on-write map from key_id to (DEK, pre-init AEAD).
Reads on the hot path take a single atomic load and observe an immutable snapshot of the map. Writes (rotation, bootstrap, retire) allocate a new map and CAS it in via atomic.Pointer.Store.
Per §10 self-review lens 2: this avoids contending a mutex on the hot path while keeping rotation atomic with respect to readers.
Zero-value safety: a `var ks Keystore` (or a nil *Keystore) is degraded but does not panic — read methods (AEAD, DEK, Has, IDs, Len) treat it as the empty keystore, Delete is a no-op, and Set returns ErrNilKeystore for a nil receiver. Always prefer NewKeystore so an unwrap path that needs to install keys reports the wiring mistake immediately.
func (*Keystore) AEAD ¶
AEAD returns the pre-initialized cipher.AEAD for keyID, ready for Seal/Open. The returned value is safe for concurrent use by multiple goroutines (Go stdlib AEAD implementations are stateless after initialization).
Used by Cipher.Encrypt / Cipher.Decrypt on the hot path. Returns (nil, false) if keyID is not loaded, the receiver is nil, or the Keystore is zero-valued.
func (*Keystore) DEK ¶
DEK returns the raw 32-byte DEK for keyID. The returned array is a value copy — callers are free to mutate it without affecting the keystore. The bool reports whether keyID is loaded.
Most call sites should use AEAD instead; DEK is provided for the rotation / rewrap path that needs the raw key material to wrap it under a new KEK.
func (*Keystore) Delete ¶
Delete removes the DEK for keyID. No-op if absent, the receiver is nil, or the Keystore is zero-valued (no map ever Stored).
func (*Keystore) IDs ¶
IDs returns a sorted snapshot of all currently-loaded key_ids. Returns nil for a nil receiver or zero-value Keystore.
func (*Keystore) Len ¶
Len reports the number of currently-loaded keys. Returns 0 for a nil receiver or zero-value Keystore.
func (*Keystore) Set ¶
Set installs a DEK under keyID and pre-initializes the cipher.AEAD. dek must be exactly KeySize bytes; the reserved key_id 0 is rejected with ErrReservedKeyID. The DEK bytes are copied into the keystore so the caller is free to zero or reuse the source slice.
Set is set-once with idempotent-same semantics: re-Set under an existing keyID with byte-identical DEK is a no-op (returns nil), but Set with DIFFERENT bytes for an already-loaded keyID returns ErrKeyConflict. Replacing live key bytes for a keyID would render every envelope already persisted under that id undecryptable.
A nil receiver returns ErrNilKeystore; zero-value Keystores are rejected at the same boundary as Cipher.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package kek implements KEK (Key Encryption Key) providers that wrap and unwrap DEKs (Data Encryption Keys) per §5.1 of the data-at-rest encryption design.
|
Package kek implements KEK (Key Encryption Key) providers that wrap and unwrap DEKs (Data Encryption Keys) per §5.1 of the data-at-rest encryption design. |