encryption

package
v10.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2026 License: AGPL-3.0 Imports: 9 Imported by: 0

Documentation

Overview

Package encryption provides authenticated encryption over a rotatable set of keys.

The surface is three interfaces and one implementation. Cipher is single-key authenticated encryption, and it is what a provider implements — see the aes subpackage. Keyring composes Ciphers into an EncryptorDecryptor that writes under a current key and reads under any key it still holds. KeyWrapper is a separate seam for encrypting key material rather than data; see the kms subpackage.

Rotation

A ciphertext names the key that produced it, in the clear, at the front of the frame. That one fact is what makes rotation incremental: adding a key and naming it current changes what new writes use, and every existing ciphertext keeps opening under the key it already names. Nothing has to be re-encrypted at the moment of the change.

Moving old rows over is therefore a background concern rather than a flag day — re-encrypt on next write, and sweep whatever is never written again. The keyring counts decryptions per key ID so that sweep has a finish line: decryptions still attributed to a retired key are exactly the rows that have not been reached.

The dangerous operation is retiring a key, not adding one. A key dropped from the ring while ciphertexts still name it makes those rows unreadable, and permanently so once the material is gone.

Associated data

Encrypt and Decrypt take associated data alongside the payload: authenticated, not encrypted, and not recoverable from the ciphertext. Supplying the identity of the thing being encrypted — a row's primary key, a subject ID, a column name — binds the ciphertext to where it lives, so a value lifted out of one row and pasted into another fails to open instead of quietly decrypting. Passing nil is allowed and means no binding.

The frame header is authenticated too. Without that, rewriting the key ID on a stored ciphertext would steer decryption at a different key, and the only thing standing in the way would be that the wrong key happens to fail.

Errors

Everything that fails to authenticate reports ErrAuthenticationFailed, whether the cause was tampering, the wrong key, or associated data that does not match. That is deliberate: distinguishing them for a caller distinguishes them for an attacker. Bytes that cannot be parsed at all are ErrMalformedCiphertext, which is a different problem — not a ciphertext this package produced — and a ciphertext naming a key the ring does not hold is ErrUnknownKeyID, which is usually an operational problem worth alerting on rather than a security event.

Index

Constants

View Source
const MaxKeyIDLength = 255

MaxKeyIDLength bounds a KeyID.

The limit comes from the frame: a ciphertext stores its key ID length in one byte, because the alternative is a variable-length integer in a format that has to be parsed correctly by every future version of this package. 255 is far past any sane ID, and an ID approaching it is a sign that something other than an identifier is being stored.

Variables

View Source
var (
	ErrIncorrectKeyLength = errors.New("secret is not the right length")

	// ErrMalformedCiphertext is returned when ciphertext is too short or too
	// damaged to be parsed at all — a truncated frame, a missing nonce, a key
	// ID length that runs off the end. It means the bytes are not a ciphertext
	// this package produced, which is a different problem from a ciphertext
	// that fails to authenticate.
	ErrMalformedCiphertext = errors.New("malformed ciphertext")

	// ErrAuthenticationFailed is returned when ciphertext fails its
	// authentication check. It covers tampering, a wrong key, and associated
	// data that does not match what encryption was given, and it deliberately
	// does not distinguish between them: telling a caller which one it was
	// tells an attacker the same thing.
	ErrAuthenticationFailed = errors.New("ciphertext authentication failed")

	// ErrUnknownKeyID is returned when a ciphertext names a key the ring does
	// not hold. In a rotating system this is the expected shape of a real
	// operational problem — a key retired before everything it encrypted was
	// re-encrypted — so it is worth alerting on rather than swallowing.
	ErrUnknownKeyID = errors.New("ciphertext names a key that is not in the keyring")

	// ErrEmptyKeyring is returned when a Keyring is built with no keys.
	ErrEmptyKeyring = errors.New("keyring contains no keys")

	// ErrNilCipher is returned when a key is offered to a ring with no Cipher
	// to perform its encryption.
	ErrNilCipher = errors.New("key has no cipher")

	// ErrNoCurrentKey is returned when a Keyring's named current key is not
	// among the keys it was given. Encryption has to pick exactly one key and
	// there is no safe way to guess which.
	ErrNoCurrentKey = errors.New("keyring has no current key")

	// ErrEmptyKeyID is returned when a key is offered to a ring without an ID.
	// Every ciphertext has to name its key, so a key with no name cannot
	// participate.
	ErrEmptyKeyID = errors.New("key ID is empty")

	// ErrKeyIDTooLong is returned when a key ID exceeds MaxKeyIDLength.
	ErrKeyIDTooLong = errors.New("key ID is too long")

	// ErrDuplicateKeyID is returned when two keys in one ring share an ID.
	// Which one a ciphertext meant would be unanswerable.
	ErrDuplicateKeyID = errors.New("keyring contains duplicate key IDs")

	// ErrUnsupportedCiphertextVersion is returned when a ciphertext's leading
	// version byte is one this build does not know. It means the data was
	// written by a newer version of this package, and the safe response is to
	// refuse rather than to guess at the layout.
	ErrUnsupportedCiphertextVersion = errors.New("unsupported ciphertext version")
)

Functions

This section is empty.

Types

type Cipher

type Cipher interface {
	// Seal encrypts plaintext, authenticating both it and associatedData.
	Seal(ctx context.Context, plaintext, associatedData []byte) ([]byte, error)
	// Open reverses Seal, and returns ErrAuthenticationFailed if the
	// ciphertext or the associated data has changed.
	Open(ctx context.Context, ciphertext, associatedData []byte) ([]byte, error)
}

Cipher is authenticated encryption under exactly one key, and it is the seam a provider implements. It deliberately knows nothing about key IDs, rotation, or framing — a Keyring composes Ciphers into a surface that has all three, so a provider only has to get the cryptography right.

Implementations must be AEADs. A Cipher that encrypts without authenticating cannot honor associatedData and cannot report ErrAuthenticationFailed, which are the two guarantees everything above this interface is built on.

type Decryptor

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

Decryptor decrypts ciphertext under whichever key produced it, provided that key is still in the ring.

associatedData has to match what Encrypt was given byte for byte. A mismatch is reported as ErrAuthenticationFailed and is indistinguishable from tampering, because it is not distinguishable from tampering.

type Encryptor

type Encryptor interface {
	Encrypt(ctx context.Context, plaintext, associatedData []byte) ([]byte, error)
}

Encryptor encrypts plaintext under the current key.

associatedData is authenticated but not encrypted: it is not recoverable from the ciphertext, and decryption fails unless the same value is supplied again. Passing the identity of the thing being encrypted — a row's primary key, a tenant ID, a column name — is what stops a ciphertext from being lifted out of one row and pasted into another, which is otherwise undetectable. nil means no binding.

type EncryptorDecryptor

type EncryptorDecryptor interface {
	Encryptor
	Decryptor
}

type KeyID

type KeyID string

KeyID names one key within a Keyring. It travels in the clear at the front of every ciphertext, so it identifies a key without revealing anything about it: use a short opaque label like "k1" or a date stamp, never key material and never something the key material can be derived from.

A key ID is permanent. It is how a ciphertext written years ago says which key opens it, so an ID that gets reused for different material does not rotate a keyring, it corrupts one.

type KeyWrapper

type KeyWrapper interface {
	// Wrap encrypts key material. associatedData binds the result to a
	// context the same way it does for a Cipher.
	Wrap(ctx context.Context, key, associatedData []byte) ([]byte, error)
	// Unwrap reverses Wrap.
	Unwrap(ctx context.Context, wrapped, associatedData []byte) ([]byte, error)
}

KeyWrapper encrypts and decrypts key material against a key it does not hand out. It is the seam for envelope encryption: a cloud KMS performs wrap and unwrap inside its own boundary, so the key doing the wrapping never enters this process and cannot leave in a heap dump.

This is why it is a separate interface from Cipher rather than a use of one. A Cipher is handed key material at construction; the entire value of a KeyWrapper is that nothing ever hands you the key.

Implementations that do hold the wrapping key locally are legitimate — there is nothing better available behind an environment variable — but they are a weaker thing wearing the same interface, and they should say so in their own documentation.

type Keyring

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

Keyring is an EncryptorDecryptor over several keys at once: it encrypts with the current one and decrypts with whichever key a ciphertext names, which is what makes rotation something other than a flag day.

Rotation is deliberately lazy and the ring does not perform it. Naming a new current key means new writes use it and old ciphertexts keep opening under the keys they name; moving the old rows over is a re-encrypt on next write plus a sweep for rows that are never written again. A ring that re-encrypted eagerly would turn a configuration change into an unbounded write amplification against the database.

Retiring a key is therefore the dangerous operation, not adding one. Drop a key from the ring while ciphertexts still name it and those rows stop being readable — permanently, if the material is gone. The decryption metrics exist to make that backlog visible before it becomes that.

func NewKeyring

func NewKeyring(current KeyID, ringKeys []RingKey, opts ...Option) (*Keyring, error)

NewKeyring builds a Keyring that encrypts under current and decrypts under any key in keys.

current has to name one of keys. There is no default and no "first one wins": which key new data is written under is the single most consequential thing about this object, and inferring it from ordering would make a reordered config file silently change what encrypts production.

func (*Keyring) CurrentKeyID

func (r *Keyring) CurrentKeyID() KeyID

CurrentKeyID reports the key new ciphertexts are written under. A sweep that re-encrypts stale rows needs it to know what "stale" means.

func (*Keyring) Decrypt

func (r *Keyring) Decrypt(ctx context.Context, ciphertext, associatedData []byte) ([]byte, error)

func (*Keyring) Encrypt

func (r *Keyring) Encrypt(ctx context.Context, plaintext, associatedData []byte) ([]byte, error)

func (*Keyring) KeyIDs

func (r *Keyring) KeyIDs() []KeyID

KeyIDs reports every key the ring can decrypt with, current included, in no particular order.

type Keyset

type Keyset map[KeyID]MasterKey

Keyset is every key a Keyring should hold, by ID. It is the shape key material arrives in from configuration and from dependency injection, and it is a named type for the same reason MasterKey is.

A Keyset is not itself a ring: it carries no notion of which key is current, because that belongs with the configuration that names it rather than with the material.

type MasterKey

type MasterKey []byte

MasterKey is secret key material used to encrypt and decrypt. It is a named type over []byte so that dependency-injection lookups resolve it distinctly and cannot collide with an arbitrary []byte value registered in the same container.

type Option

type Option func(*options)

Option configures a Keyring. The zero configuration works: an absent logger logs nowhere, an absent tracer provider traces nowhere, and an absent metrics provider records nothing.

func WithLogger

func WithLogger(logger logging.Logger) Option

WithLogger attaches a logger.

func WithMetricsProvider

func WithMetricsProvider(metricsProvider metrics.Provider) Option

WithMetricsProvider attaches a metrics provider for the rotation counters.

func WithPillars

func WithPillars(pillars *observability.Pillars) Option

WithPillars supplies logger, tracer provider, and metrics provider at once.

Options apply in order, so a WithPillars followed by a narrower option wins for that component: WithPillars(p) then WithMetricsProvider(nil) leaves the keyring traced and logged but unmetered.

func WithTracerProvider

func WithTracerProvider(tracerProvider tracing.Provider) Option

WithTracerProvider attaches a tracer provider, enabling spans on every operation.

type RingKey

type RingKey struct {
	// Cipher performs the actual encryption under this key.
	Cipher Cipher
	// ID names the key, and is written into every ciphertext the Cipher
	// produces through the ring.
	ID KeyID
}

RingKey is one key's identity paired with the Cipher that uses it.

Directories

Path Synopsis
Package aes contains the interfaces and implementations for encrypting and decrypting data.
Package aes contains the interfaces and implementations for encrypting and decrypting data.
Package encryptioncfg builds an encryption keyring over a caller-supplied encryption.Keyset, with one cipher provider — AES-256-GCM today — governing every key in the ring.
Package encryptioncfg builds an encryption keyring over a caller-supplied encryption.Keyset, with one cipher provider — AES-256-GCM today — governing every key in the ring.
kms
Package kms groups the encryption.KeyWrapper implementations.
Package kms groups the encryption.KeyWrapper implementations.
aws
Package aws wraps key material with AWS KMS.
Package aws wraps key material with AWS KMS.
gcp
Package gcp wraps key material with Google Cloud KMS.
Package gcp wraps key material with Google Cloud KMS.
local
Package local wraps key material with an encryption.Cipher held in this process.
Package local wraps key material with an encryption.Cipher held in this process.
Package encryptionmock provides moq-generated mock implementations of the encryption package's interfaces.
Package encryptionmock provides moq-generated mock implementations of the encryption package's interfaces.

Jump to

Keyboard shortcuts

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