crypto

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package crypto implements envelope encryption.

Data is encrypted with a Data Encryption Key (DEK) using AES-256-GCM. Each DEK is in turn encrypted ("wrapped") by a Key Encryption Key (KEK), typically customer-managed and backed by a cloud KMS. The wrapped DEK and the ID of the KEK that wrapped it are carried together as DEKMaterial, allowing the DEK to be recovered and the data decrypted later.

A KEKRegistry manages the set of available KEKs, selecting the appropriate key by namespace for encryption and by key ID for decryption. KEKs themselves are opened from key URIs by a KeyFactory, which handles the cloud KMS schemes directly and can be extended with schemes of the caller's own.

A Vault reports what it does to an Observer as one of a closed set of Event types: a CacheEvent for a DEK cache hit or miss, one EnvelopeEvent per Seal and Open carrying both the end-to-end and the AES-only duration and distinguishing a failure inside AES from a failure elsewhere in the operation, and a RotationEvent whenever a namespace's DEK is replaced. Observers are always called outside the Vault's internal lock, so an implementation may call back into the Vault, but it must not block, and it must not re-enter the operation that produced the event: calling Seal from Observe, for example, recurses without bound and a stack overflow cannot be recovered from.

Index

Constants

This section is empty.

Variables

View Source
var ErrMalformedCipherText = errors.New("invalid ciphertext, not encrypted with a DEK")

ErrMalformedCipherText indicates that the ciphertext was not created by a call to Encrypt, or that it was otherwise tampered with.

Functions

func DefaultSchemes added in v0.3.0

func DefaultSchemes() []string

DefaultSchemes lists the URI schemes a KeyFactory serves with NewCloudKey before any option is applied, lowercased as KeyFactory.Create matches them. It is useful for validating a key URI ahead of opening it, so a typo in a scheme can be reported alongside the rest of a config rather than at the point the key is first needed.

Each call returns a fresh slice; the caller may sort or filter it freely without disturbing the factory.

Types

type CacheEvent

type CacheEvent struct {
	// Hit is true when the wrapped DEK was already cached.
	Hit bool
	// Size is the number of entries in the DEK cache after the access.
	Size int
}

CacheEvent describes a DEK cache access.

type CloudKey added in v0.3.0

type CloudKey struct {
	*secrets.Keeper
	// contains filtered or unexported fields
}

CloudKey is a KEK backed by a cloud KMS key. The embedded secrets.Keeper supplies Decrypt and Close; CloudKey adds the ID and the namespace-aware Encrypt that KEK requires.

A Keeper addresses a single fixed key, so one CloudKey wraps DEKs for every namespace it is handed. Open one CloudKey per KMS key and let a KEKRegistry decide which namespaces map onto which key.

func (*CloudKey) Encrypt added in v0.3.0

func (k *CloudKey) Encrypt(ctx context.Context, _ string, dek []byte) ([]byte, error)

Encrypt wraps the DEK using the underlying keeper. The namespace is unused: a gocloud keeper addresses a single fixed key, so namespace-based selection has already happened by the time this KEK is chosen.

func (*CloudKey) ID added in v0.3.0

func (k *CloudKey) ID() string

ID returns a unique ID for this KEK, e.g. a KMS ARN.

type DEK

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

DEK defines a Data Encryption Key used to encrypt/decrypt payloads.

func NewDEK

func NewDEK() (*DEK, error)

NewDEK generates a new random 256-bit Data Encryption Key.

func (*DEK) Decrypt

func (d *DEK) Decrypt(ctx context.Context, ct []byte) ([]byte, error)

Decrypt decrypts the ciphertext ct using AES-256-GCM. The ciphertext must be prefixed with the nonce, as produced by DEK.Encrypt.

func (*DEK) Encrypt

func (d *DEK) Encrypt(ctx context.Context, pt []byte) ([]byte, error)

Encrypt encrypts the plaintext pt using AES-256-GCM. The returned ciphertext is prefixed with the randomly generated nonce.

type DEKMaterial

type DEKMaterial struct {
	Version      byte
	KEKID        string // The ID/URI of the KEK the encrypted the DEK.
	EncryptedDEK string // The base64-encoded encrypted DEK.
}

DEKMaterial defines the material needed in order to decrypt a payload.

type EnvelopeEvent added in v0.3.0

type EnvelopeEvent struct {
	// Op is OpEncrypt for Seal, OpDecrypt for Open.
	Op Operation
	// Namespace is set on OpEncrypt. It is always empty on OpDecrypt: Open
	// selects its KEK by ID from the message material and never learns a
	// namespace.
	Namespace string
	// Err is the error the operation returned, or nil.
	Err error
	// CryptoAttempted reports whether the AES-256-GCM step ran. It is the
	// only reliable signal for that, and Crypto is not: a small payload can
	// complete inside the clock's resolution, so a step that did run can
	// still report a zero duration. Crypto and CryptoErr are meaningful only
	// when this is true.
	CryptoAttempted bool
	// CryptoErr is the error from the AES-256-GCM step alone, or nil when
	// that step succeeded. Err, by contrast, is the whole operation's error,
	// which may come from a KEK wrap or a cache-miss unwrap rather than from
	// AES, so the two differ whenever a Seal encrypts successfully and then
	// fails to wrap its DEK.
	CryptoErr error
	// Total covers the whole operation, including any KEK wrap on the first
	// Seal after a rotation and any unwrap on a cache miss.
	Total time.Duration
	// Crypto covers only the AES-256-GCM step, and may be zero for a step
	// that ran. Total minus Crypto is the cost of everything around AES, and
	// is meaningful only when CryptoAttempted is true.
	Crypto time.Duration
}

EnvelopeEvent describes one completed envelope operation, successful or not. Exactly one is reported per Vault.Seal and per Vault.Open, on every path including early failures.

type Event added in v0.3.0

type Event interface {
	// contains filtered or unexported methods
}

Event is one thing a Vault reports to an Observer. The set is closed: only this package can define one, so an Observer's type switch can be written against a known set of cases.

type KEK

type KEK interface {
	io.Closer

	// ID returns a unique ID for this KEK, e.g. a KMS ARN.
	ID() string
	// Encrypt encrypts a DEK for the given namespace, returning the ciphertext.
	// The namespace lets a KEK select a per-namespace key; implementations backed
	// by a single fixed key ignore it.
	Encrypt(ctx context.Context, ns string, dek []byte) ([]byte, error)
	// Decrypt decrypts a DEK previously produced by Encrypt. No namespace is
	// required: the KEK is selected by ID from the DEK material.
	Decrypt(ctx context.Context, dek []byte) ([]byte, error)
}

KEK defines an interface for a Key Encryption Keys. These keys are used to encrypt/decrypt DEKs and are customer-managed (e.g. via AWS/GCP KMS).

func NewCloudKey added in v0.3.0

func NewCloudKey(ctx context.Context, uri string) (KEK, error)

NewCloudKey opens the cloud KMS key addressed by uri, which must use one of the schemes listed in NewKeyFactory. Close the returned key when it is no longer needed; a KEKRegistry does that for the keys it holds.

The "testing://" scheme is rewritten to gocloud's "base64key://" local keeper so tests and local runs need no cloud KMS at all. Everything after that scheme is the base64-encoded 32-byte key; pass a bare "testing://" to get a random one. Key material is kept out of the errors this function returns, but it still reaches the key's ID, and therefore every DEK the key wraps, which is one more reason to keep the scheme away from production.

For every other scheme the ID is just uri, so it is stable across processes and identifies the key again on the decrypt path. Schemes are matched without regard to case, so the ID of a testing key is the same however its scheme was spelled.

type KEKRegistry

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

KEKRegistry holds the set of KEKs available for encrypting and decrypting DEKs. It is keyed by namespace (for encryption) and by key ID (for decryption). Close must be called when the registry is no longer needed to release KEK resources.

func NewKEKRegistry

func NewKEKRegistry(opts ...KEKRegistryOption) (*KEKRegistry, error)

NewKEKRegistry constructs a KEKRegistry, applying opts in order. A default key is required (see WithDefaultKey); construction fails if one is not provided. The key-ID index used by Decrypt is built after all options are applied.

func (*KEKRegistry) Close

func (r *KEKRegistry) Close() error

Close closes all registered KEKs and releases their resources. Subsequent calls return the same error as the first call.

func (*KEKRegistry) Decrypt

func (r *KEKRegistry) Decrypt(ctx context.Context, m *DEKMaterial) (*DEK, error)

Decrypt decrypts the DEK described by m using the KEK identified by m.KEKID.

func (*KEKRegistry) Encrypt

func (r *KEKRegistry) Encrypt(ctx context.Context, ns string, dek *DEK) (*DEKMaterial, error)

Encrypt encrypts the given DEK using the KEK registered for the specified namespace. It returns DEKMaterial containing the KEK ID and the base64-encoded ciphertext.

type KEKRegistryOption

type KEKRegistryOption interface {
	// contains filtered or unexported methods
}

KEKRegistryOption configures a KEKRegistry during construction.

func WithDecryptOnlyKey

func WithDecryptOnlyKey(k KEK) KEKRegistryOption

WithDecryptOnlyKey registers k for decryption only. It is added to the key-ID index so that DEKs encrypted with k can still be opened, but k is never selected for new DEK encryption. This is typically used for keys that have been rotated out of active use.

func WithDefaultKey

func WithDefaultKey(k KEK) KEKRegistryOption

WithDefaultKey sets the fallback KEK used when no namespace-specific key is registered. A default key is required: NewKEKRegistry returns an error if one is not provided.

func WithKeyForNamespace

func WithKeyForNamespace(ns string, k KEK) KEKRegistryOption

WithKeyForNamespace registers k for ns, used when encrypting or decrypting DEKs for that namespace.

type KeyConfig

type KeyConfig struct {
	// Duration is how long a DEK is valid before it must be rotated.
	Duration time.Duration
	// RenewBefore causes a DEK to be treated as expired this long before
	// Duration elapses, so it can be rotated ahead of its actual expiry.
	RenewBefore time.Duration
}

KeyConfig controls the lifetime of a namespace's DEK.

type KeyFactory added in v0.3.0

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

KeyFactory opens [KEK]s from key URIs, choosing an opener by URI scheme. It handles the cloud KMS schemes out of the box (see NewKeyFactory) and takes additional or replacement schemes through WithKeyFactoryFuncForScheme, so a caller can serve keys from its own key store without the code consuming those KEKs knowing where they come from.

Schemes are registered during construction only, so a KeyFactory never changes afterwards and one may be shared by any number of goroutines opening keys at once.

func NewKeyFactory added in v0.3.0

func NewKeyFactory(opts ...KeyFactoryOption) *KeyFactory

NewKeyFactory returns a KeyFactory that opens cloud KMS keys, then applies opts in order. The schemes registered up front are DefaultSchemes, all served by NewCloudKey:

awskms://         AWS KMS
azurekeyvault://  Azure Key Vault
gcpkms://         Google Cloud KMS
testing://        a local in-process key, for tests and local runs only

The driver behind each scheme is linked in by importing this package, so no further imports are needed to use them.

opts are applied after those defaults, which means WithKeyFactoryFuncForScheme can replace any of them as well as add schemes of its own.

func (*KeyFactory) Create added in v0.3.0

func (f *KeyFactory) Create(ctx context.Context, uri string) (KEK, error)

Create opens the key addressed by uri with the KeyFactoryFunc registered for its scheme. It fails if uri does not parse or if no opener is registered for the scheme. Beyond that the opener decides: an unreachable or misconfigured key surfaces as whatever error that opener returns.

type KeyFactoryFunc added in v0.3.0

type KeyFactoryFunc func(context.Context, string) (KEK, error)

KeyFactoryFunc opens the key addressed by a URI. It is called only for URIs whose scheme it was registered under, and receives the URI verbatim as it was passed to KeyFactory.Create, scheme included.

type KeyFactoryOption added in v0.3.0

type KeyFactoryOption interface {
	// contains filtered or unexported methods
}

KeyFactoryOption configures a KeyFactory during construction.

func WithKeyFactoryFuncForScheme added in v0.3.0

func WithKeyFactoryFuncForScheme(scheme string, fn KeyFactoryFunc) KeyFactoryOption

WithKeyFactoryFuncForScheme registers fn as the opener for scheme, replacing whatever was registered for it before, including the built-in cloud KMS schemes. URI schemes are case-insensitive, so scheme is lowercased on the way in and matches however a caller spells it in a URI.

type Message

type Message struct {
	Ciphertext  []byte
	KeyMaterial *DEKMaterial
}

Message is the result of sealing plaintext: the AES-256-GCM ciphertext together with the wrapped DEK (DEKMaterial) required to open it.

type NamespacedVault

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

NamespacedVault is a Vault bound to a single namespace so callers can Seal and Open without passing the namespace on every call. Obtain one via Vault.ForNamespace.

func (*NamespacedVault) Open

func (v *NamespacedVault) Open(ctx context.Context, msg *Message) ([]byte, error)

Open decrypts msg within the bound namespace. See Vault.Open.

func (*NamespacedVault) Seal

func (v *NamespacedVault) Seal(ctx context.Context, data []byte) (*Message, error)

Seal encrypts data within the bound namespace. See Vault.Seal.

type Observer

type Observer interface {
	Observe(Event)
}

Observer receives notifications about Vault-internal events for telemetry. It is called with a CacheEvent from Open only, an EnvelopeEvent from both Seal and Open, and a RotationEvent from both Seal and Refresh. Implementations must be safe for concurrent use, must not block, and must not re-enter the operation that produced the event. A nil Observer is never used; the Vault substitutes a no-op (see WithObserver).

type Operation added in v0.3.0

type Operation uint8

Operation names the envelope operation an EnvelopeEvent describes.

const (
	// OpEncrypt is a Seal: the DEK encrypting a payload.
	OpEncrypt Operation = iota
	// OpDecrypt is an Open: the DEK decrypting a payload.
	OpDecrypt
)

func (Operation) String added in v0.3.0

func (o Operation) String() string

String returns the metric label value for o. An unrecognized value returns "unknown" so a label is never blank if a value is added without updating this method.

type RotationEvent added in v0.3.0

type RotationEvent struct {
	Namespace string
	Reason    RotationReason
}

RotationEvent reports that a namespace's DEK was replaced.

type RotationReason added in v0.3.0

type RotationReason uint8

RotationReason says why a namespace's DEK was replaced.

const (
	// RotationScheduled is a rotation performed by [Vault.Refresh], off the
	// request path.
	RotationScheduled RotationReason = iota
	// RotationOnDemand is a rotation [Vault.Seal] performed because it found the
	// DEK already expired, which means Refresh has fallen behind.
	RotationOnDemand
	// RotationInitial is the first DEK for a namespace that had no explicit
	// [WithKeyConfig], created on its first Seal.
	RotationInitial
)

func (RotationReason) String added in v0.3.0

func (r RotationReason) String() string

String returns the metric label value for r. An unrecognized value returns "unknown" so a label is never blank if a value is added without updating this method.

type Vault

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

Vault provides envelope encryption scoped by namespace. It keeps a sliding Data Encryption Key (DEK) per namespace, wrapping each DEK with the KEK selected for that namespace by a KEKRegistry. DEKs are rotated on a sliding schedule (see KeyConfig) and decrypted DEKs are cached to avoid repeated KMS calls on Open. A Vault is safe for concurrent use.

func NewVault

func NewVault(r *KEKRegistry, opts ...VaultOption) (*Vault, error)

NewVault constructs a Vault backed by r, applying opts in order. A DEK is pre-generated for every namespace registered via WithKeyConfig. NewVault returns an error if any option is invalid (for example, a duplicate namespace config) or if key/cache setup fails.

func (*Vault) ForNamespace

func (v *Vault) ForNamespace(ns string) *NamespacedVault

ForNamespace returns a NamespacedVault that seals and opens within ns.

func (*Vault) Open

func (v *Vault) Open(ctx context.Context, msg *Message) ([]byte, error)

Open decrypts msg, which must have been produced by Vault.Seal (or NamespacedVault.Seal). The wrapped DEK is unwrapped via the KEKRegistry using the KEK identified by the material carried in msg, served from the decrypted-DEK cache when it is enabled.

Exactly one EnvelopeEvent is reported to the Observer, on every path including failures. It carries no namespace: the KEK is selected by ID from the material, so Open never learns one.

func (*Vault) Refresh

func (v *Vault) Refresh() error

Refresh rotates every namespace DEK that has reached its renewal threshold. It is meant to be called periodically. Seal also rotates an expired DEK on demand, so Refresh is an optimization that keeps rotation off the request path rather than a correctness requirement.

One RotationEvent with RotationScheduled is reported per key rotated.

func (*Vault) Seal

func (v *Vault) Seal(ctx context.Context, ns string, data []byte) (*Message, error)

Seal encrypts data for ns, returning the ciphertext together with the wrapped DEK required to Open it. The active DEK for ns is created or rotated on demand. Concurrent first-time seals holding the same DEK are coalesced into a single KEK (KMS) call.

Exactly one EnvelopeEvent is reported to the Observer, on every path including failures.

type VaultOption

type VaultOption func(*vaultOptions)

VaultOption configures a Vault during construction.

func WithCacheSize

func WithCacheSize(n int) VaultOption

WithCacheSize sets the maximum number of decrypted DEKs retained in the Open cache. A value of zero or less disables the cache, so every Open unwraps its DEK via the KEKRegistry.

func WithDefaultKeyConfig

func WithDefaultKeyConfig(cfg KeyConfig) VaultOption

WithDefaultKeyConfig sets the KeyConfig used for namespaces that have no explicit WithKeyConfig. Without a default, sealing an unconfigured namespace fails; with one, a DEK is created for such namespaces on first use.

func WithKeyConfig

func WithKeyConfig(ns string, cfg KeyConfig) VaultOption

WithKeyConfig sets the KeyConfig for a specific namespace. Registering the same namespace more than once is an error surfaced by NewVault.

func WithNowFunc

func WithNowFunc(fn func() time.Time) VaultOption

WithNowFunc overrides the clock used to evaluate DEK expiry. It is primarily useful in tests. A nil function is rejected by NewVault.

func WithObserver

func WithObserver(o Observer) VaultOption

WithObserver sets the Observer notified of Vault-internal events: a CacheEvent from Open, an EnvelopeEvent from both Seal and Open, and a RotationEvent from Seal and Refresh. A nil Observer is replaced with a no-op, so no Vault call site needs to nil-check. Without this option no events are emitted.

Jump to

Keyboard shortcuts

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