keyring

package
v0.29.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 27, 2026 License: MIT Imports: 29 Imported by: 0

Documentation

Overview

Package keyring provides envelope encryption and audit-log signing as a single deep module.

Callers exchange plaintext for an Envelope (DekID + Ciphertext + Nonce) and back. The KEK chain, DEK lifecycle, AEAD cipher selection, KMS-rooted master key chain, and HMAC-SHA256 signing all live behind this interface.

Two encryption shapes are supported:

  • Fresh-DEK envelope (Encrypt/Decrypt) — a new DEK is created for each call. Used by the secrets and tokenization features.
  • Persistent DEK (AllocateDek/EncryptWith/DecryptWith) — one DEK is allocated once and reused across many encrypt/decrypt calls. Used by the transit feature where a named key wraps user payloads repeatedly.

Signing (SignWithKey/VerifyWithKey) uses HKDF-SHA256 to derive a purpose- specific key from the active KEK, keeping raw key material inside the module.

All methods honor an ambient transaction propagated via context; persistence joins the caller's tx when one is present (see ADR-0005).

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnsupportedAlgorithm is returned when an unknown Algorithm value is used.
	ErrUnsupportedAlgorithm = apperrors.Wrap(apperrors.ErrInvalidInput, "unsupported algorithm")

	// ErrInvalidKeySize is returned when a key has an unexpected byte length.
	ErrInvalidKeySize = apperrors.Wrap(apperrors.ErrInvalidInput, "invalid key size")

	// ErrDecryptionFailed is returned when ciphertext cannot be authenticated or decrypted.
	ErrDecryptionFailed = apperrors.Wrap(apperrors.ErrInvalidInput, "decryption failed")

	// ErrMasterKeysNotSet is returned when the MASTER_KEYS environment variable is empty.
	ErrMasterKeysNotSet = apperrors.Wrap(apperrors.ErrInvalidInput, "MASTER_KEYS not set")

	// ErrActiveMasterKeyIDNotSet is returned when ACTIVE_MASTER_KEY_ID is not configured.
	ErrActiveMasterKeyIDNotSet = apperrors.Wrap(apperrors.ErrInvalidInput, "ACTIVE_MASTER_KEY_ID not set")

	// ErrInvalidMasterKeysFormat is returned when a MASTER_KEYS entry cannot be parsed.
	ErrInvalidMasterKeysFormat = apperrors.Wrap(apperrors.ErrInvalidInput, "invalid MASTER_KEYS format")

	// ErrInvalidMasterKeyBase64 is returned when a master key value is not valid base64.
	ErrInvalidMasterKeyBase64 = apperrors.Wrap(apperrors.ErrInvalidInput, "invalid master key base64")

	// ErrActiveMasterKeyNotFound is returned when ACTIVE_MASTER_KEY_ID is not present in the chain.
	ErrActiveMasterKeyNotFound = apperrors.Wrap(apperrors.ErrInvalidInput, "active master key not found")

	// ErrMasterKeyNotFound is returned when a KEK references a master key absent from the chain.
	ErrMasterKeyNotFound = apperrors.Wrap(apperrors.ErrNotFound, "master key not found")

	// ErrDekNotFound is returned when a DEK lookup by ID yields no row.
	ErrDekNotFound = apperrors.Wrap(apperrors.ErrNotFound, "dek not found")

	// ErrKekNotFound is returned when no KEK exists for the given ID, or no KEKs at all.
	ErrKekNotFound = apperrors.Wrap(apperrors.ErrNotFound, "kek not found")

	// ErrKMSProviderNotSet is returned when KMS_PROVIDER is empty.
	ErrKMSProviderNotSet = apperrors.Wrap(
		apperrors.ErrInvalidInput,
		"KMS_PROVIDER is required but not configured (use 'localsecrets' for local development)",
	)

	// ErrKMSKeyURINotSet is returned when KMS_KEY_URI is empty.
	ErrKMSKeyURINotSet = apperrors.Wrap(
		apperrors.ErrInvalidInput,
		"KMS_KEY_URI is required but not configured",
	)

	// ErrKMSDecryptionFailed is returned when the KMS cannot decrypt a master key.
	ErrKMSDecryptionFailed = apperrors.Wrap(apperrors.ErrInvalidInput, "KMS decryption failed")

	// ErrKMSOpenKeeperFailed is returned when the KMS keeper cannot be opened.
	ErrKMSOpenKeeperFailed = apperrors.Wrap(apperrors.ErrInvalidInput, "failed to open KMS keeper")

	// ErrSignatureInvalid is returned by VerifyWithKey when the HMAC does not match.
	ErrSignatureInvalid = errors.New("keyring: signature invalid")
)

Sentinel errors returned by the keyring package. Callers should use errors.Is to test them.

Functions

func Zero

func Zero(b []byte)

Zero overwrites every byte of b with zero, clearing sensitive key material.

Types

type Algorithm

type Algorithm string

Algorithm is the AEAD algorithm used to wrap a DEK and to encrypt under it.

const (
	// AESGCM selects AES-256-GCM. Prefer this on hardware with AES-NI acceleration.
	AESGCM Algorithm = "aes-gcm"

	// ChaCha20 selects ChaCha20-Poly1305. Prefer this on hardware without AES-NI.
	ChaCha20 Algorithm = "chacha20-poly1305"
)

type DekHandle

type DekHandle struct {
	DekID uuid.UUID
}

DekHandle is an opaque reference to a persistent DEK allocated via AllocateDek. Callers store only the DekID and reload the handle on demand.

type Envelope

type Envelope struct {
	DekID      uuid.UUID
	Ciphertext []byte
	Nonce      []byte
}

Envelope is the result of Keyring.Encrypt and the input to Keyring.Decrypt. Callers persist the three fields exactly as they would today; nothing about the KEK or DEK material is exposed.

type Fake

type Fake struct {

	// FailEncrypt, FailDecrypt, FailAllocate, FailRewrap, FailSign, when
	// non-nil, make the matching method return the stored error. Useful for
	// failure-path tests in callers.
	FailEncrypt  error
	FailDecrypt  error
	FailAllocate error
	FailRewrap   error
	FailSign     error
	// contains filtered or unexported fields
}

Fake is an in-memory Keyring suitable for feature unit tests.

It does not perform real cryptography: ciphertext is a deterministic transformation of the plaintext keyed by the DekID. The point is to give features a real second adapter so that Keyring becomes a real seam, and to let feature tests assert behaviour (a value can be encrypted, persisted, and decrypted back) without touching the database or KMS.

Concurrency-safe.

func NewFake

func NewFake() *Fake

NewFake constructs an empty Fake.

func (*Fake) ActiveKekID

func (f *Fake) ActiveKekID() uuid.UUID

ActiveKekID returns the zero UUID for the Fake; the value is only used by the rotation worker to verify the operator-provided KEK ID matches.

func (*Fake) AllocateDek

func (f *Fake) AllocateDek(_ context.Context, _ Algorithm) (DekHandle, error)

AllocateDek returns a fresh handle. Algorithm is ignored.

func (*Fake) Decrypt

func (f *Fake) Decrypt(_ context.Context, env Envelope) ([]byte, error)

Decrypt reverses Encrypt. Returns an error if the DekID was never allocated.

func (*Fake) DecryptWith

func (f *Fake) DecryptWith(
	_ context.Context,
	handle DekHandle,
	ciphertext, _ []byte,
	aad []byte,
) ([]byte, error)

DecryptWith reverses EncryptWith.

func (*Fake) Encrypt

func (f *Fake) Encrypt(_ context.Context, plaintext []byte) (Envelope, error)

Encrypt returns an Envelope whose Ciphertext is a reversible XOR of the plaintext with a DekID-derived stream. The DekID is allocated and tracked.

func (*Fake) EncryptWith

func (f *Fake) EncryptWith(
	_ context.Context,
	handle DekHandle,
	plaintext, aad []byte,
) (ciphertext, nonce []byte, err error)

EncryptWith XORs plaintext with a stream derived from the handle's DekID and the optional aad.

func (*Fake) Rewrap

func (f *Fake) Rewrap(_ context.Context, dekID uuid.UUID) error

Rewrap is a no-op for the Fake (there is no KEK chain) but honors FailRewrap and validates the DekID is known.

func (*Fake) RewrapAll

func (f *Fake) RewrapAll(_ context.Context, _ int) (int, error)

RewrapAll returns 0 by default for the Fake — there is no notion of a stale KEK in-memory. Tests that exercise the rotation worker should stub this behaviour by counting tracked DEKs.

func (*Fake) SignWithKey

func (f *Fake) SignWithKey(data []byte) ([]byte, uuid.UUID, error)

SignWithKey computes HMAC-SHA256 of data using a fixed zero key and returns uuid.Nil as the KEK ID. Intended for tests; does not use real key material.

func (*Fake) VerifyWithKey

func (f *Fake) VerifyWithKey(_ uuid.UUID, data, sig []byte) error

VerifyWithKey verifies sig against data. The kekID is ignored by the Fake.

type KMSKeeper

type KMSKeeper interface {
	Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error)
	Close() error
}

KMSKeeper wraps a gocloud.dev/secrets.Keeper to expose only the operations the keyring needs: decryption and resource cleanup.

type KMSService

type KMSService interface {
	// OpenKeeper opens the KMS keeper at keyURI and returns it.
	// The caller must call Close on the returned keeper when done.
	OpenKeeper(ctx context.Context, keyURI string) (KMSKeeper, error)
}

KMSService opens cloud KMS keepers by URI. Implementations register themselves via gocloud.dev URL openers (see blank imports below).

func NewKMSService

func NewKMSService() KMSService

NewKMSService returns a KMSService backed by the gocloud.dev URL opener registry.

type KekUseCase

type KekUseCase interface {
	// Create generates a new KEK wrapped under the active master key and persists it.
	Create(ctx context.Context, masterKeyChain *MasterKeyChain, alg Algorithm) error

	// Rotate creates a new KEK version wrapped under the active master key,
	// incrementing the version counter relative to the current highest KEK.
	Rotate(ctx context.Context, masterKeyChain *MasterKeyChain, alg Algorithm) error
}

KekUseCase manages KEK lifecycle operations. It is the only caller that writes KEK rows; all read access goes through Bootstrap.

func NewKekUseCase

func NewKekUseCase(txManager database.TxManager, db *sql.DB) KekUseCase

NewKekUseCase returns a KekUseCase backed by the given transaction manager and database.

type KeySigner

type KeySigner interface {
	// SignWithKey signs data using a key derived from the active KEK via HKDF-SHA256.
	// Returns the 32-byte HMAC-SHA256 signature and the ID of the KEK used.
	SignWithKey(data []byte) (sig []byte, kekID uuid.UUID, err error)

	// VerifyWithKey verifies data against sig using the KEK identified by kekID.
	// Returns ErrSignatureInvalid if the signature does not match.
	// Returns ErrKekNotFound if the KEK is not present in the chain.
	VerifyWithKey(kekID uuid.UUID, data, sig []byte) error
}

KeySigner signs and verifies arbitrary byte payloads using KEK-derived HMAC-SHA256 keys. Key material never leaves the keyring.

type Keyring

type Keyring interface {
	KeySigner

	// Encrypt creates a fresh DEK, persists it, and uses it to encrypt
	// plaintext exactly once. The returned Envelope contains everything a
	// future Decrypt call needs.
	Encrypt(ctx context.Context, plaintext []byte) (Envelope, error)

	// Decrypt reverses Encrypt for an Envelope produced by this Keyring.
	Decrypt(ctx context.Context, env Envelope) ([]byte, error)

	// AllocateDek creates and persists a new DEK and returns a handle to it.
	// The DEK is wrapped under the active KEK with the given algorithm.
	AllocateDek(ctx context.Context, alg Algorithm) (DekHandle, error)

	// EncryptWith encrypts plaintext under the DEK identified by handle and
	// authenticates the optional aad. Reuses the DEK across many calls.
	EncryptWith(
		ctx context.Context,
		handle DekHandle,
		plaintext, aad []byte,
	) (ciphertext, nonce []byte, err error)

	// DecryptWith reverses EncryptWith for ciphertext produced under the
	// same DekHandle with the same aad.
	DecryptWith(
		ctx context.Context,
		handle DekHandle,
		ciphertext, nonce, aad []byte,
	) ([]byte, error)

	// Rewrap re-encrypts an existing DEK under the active KEK without
	// changing the underlying key material. Used by KEK rotation.
	Rewrap(ctx context.Context, dekID uuid.UUID) error

	// RewrapAll re-encrypts, in batches, every DEK not already under the
	// active KEK. Returns the total number of DEKs rewrapped across all
	// batches. Used by the KEK rotation worker.
	RewrapAll(ctx context.Context, batchSize int) (int, error)

	// ActiveKekID returns the ID of the KEK currently used for new
	// encryption. Exposed so the rotation worker can confirm it is
	// targeting the same KEK the application is using.
	ActiveKekID() uuid.UUID
}

Keyring is the single seam features use for encryption and signing. It embeds KeySigner so any Keyring implementation also satisfies signing without a separate interface or runtime type assertion.

Implementations must be safe for concurrent use.

func Bootstrap

func Bootstrap(
	ctx context.Context,
	masterKeyChain *MasterKeyChain,
	db *sql.DB,
	alg Algorithm,
) (Keyring, error)

Bootstrap loads all KEKs from the database, decrypts them using masterKeyChain, and returns a ready-to-use Keyring. alg is the default Algorithm used for new DEKs. Returns ErrKekNotFound if no KEKs exist or ErrMasterKeyNotFound if a KEK references a master key that is absent from the chain.

type MasterKey

type MasterKey struct {
	ID  string
	Key []byte
}

MasterKey is a plaintext root key used to wrap KEKs. Key material must be zeroed via Zero when no longer needed.

type MasterKeyChain

type MasterKeyChain struct {
	// contains filtered or unexported fields
}

MasterKeyChain is an in-memory store of one or more MasterKeys, one of which is designated active. The active key is used for new KEK operations; all keys are available for decryption of existing KEKs. Concurrency-safe.

func LoadMasterKeyChain

func LoadMasterKeyChain(
	ctx context.Context,
	cfg *config.Config,
	kmsService KMSService,
	logger *slog.Logger,
) (*MasterKeyChain, error)

LoadMasterKeyChain reads MASTER_KEYS and ACTIVE_MASTER_KEY_ID from the environment, decrypts each key via the KMS, and returns a populated chain. KMS_PROVIDER and KMS_KEY_URI must be set in cfg.

func NewMasterKeyChain

func NewMasterKeyChain(activeID string) *MasterKeyChain

NewMasterKeyChain returns an empty MasterKeyChain whose active key ID is activeID. Keys must be added via direct store calls before passing the chain to Bootstrap.

func (*MasterKeyChain) ActiveMasterKeyID

func (m *MasterKeyChain) ActiveMasterKeyID() string

ActiveMasterKeyID returns the ID of the master key used for new operations.

func (*MasterKeyChain) Close

func (m *MasterKeyChain) Close()

Close zeroes all key material in the chain and removes every entry. After Close the chain must not be used.

func (*MasterKeyChain) Get

func (m *MasterKeyChain) Get(id string) (*MasterKey, bool)

Get returns the MasterKey with the given ID, or false if it is not in the chain.

type NullSigner

type NullSigner struct{}

NullSigner is a no-op KeySigner for tests that do not exercise signing behaviour. SignWithKey returns a 32-byte zero signature and uuid.Nil; VerifyWithKey always returns nil.

func (NullSigner) SignWithKey

func (NullSigner) SignWithKey(_ []byte) ([]byte, uuid.UUID, error)

func (NullSigner) VerifyWithKey

func (NullSigner) VerifyWithKey(_ uuid.UUID, _, _ []byte) error

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL