encryption

package
v0.1.100 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package encryption encrypts dashboard-managed secrets at rest.

Secret fields (provider API keys, MCP headers, guardrail secrets) are sealed with AES-256-GCM under a data key (DEK) before a store writes them. Each database holds its own random DEKs in the encryption_keys table, wrapped by a key-encryption key (KEK): either one derived from GOMODEL_ENCRYPTION_KEY with Argon2id, or an extension's KeyWrapper (a KMS). See ADR-0014.

A sealed value reads enc:v1:<key-id>:<base64(nonce||ciphertext)>. The additional authenticated data binds it to one entity and field, so a ciphertext copied to another row or field fails to open instead of decrypting there.

Index

Constants

View Source
const LocalWrapperID = "local"

LocalWrapperID is the wrapper id recorded for data keys wrapped with the KEK derived from GOMODEL_ENCRYPTION_KEY.

Variables

View Source
var ErrKeyRequired = errors.New("value is encrypted but GOMODEL_ENCRYPTION_KEY is not set")

ErrKeyRequired is returned when a sealed value is read without an encryption key configured.

View Source
var ErrSealUnconfirmed = errors.New("the secret was saved, but its data key could not be confirmed; save it again or run `gomodel secrets reencrypt`")

ErrSealUnconfirmed reports a save that was written but whose data key could not be confirmed as current afterwards, so it may still be sealed with a key a rotation replaced. The stored value stays readable.

Functions

func AAD

func AAD(kind, id, field string) []byte

AAD builds the additional authenticated data for one secret field.

func IsSealed

func IsSealed(value string) bool

IsSealed reports whether value is a sealed value rather than plaintext.

Types

type Box

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

Box seals and opens secret field values. A Box without keys (Disabled, or a nil *Box) passes plaintext through unchanged, which is the behaviour of a deployment without GOMODEL_ENCRYPTION_KEY.

A Box is safe for concurrent use.

func Disabled

func Disabled() *Box

Disabled returns a Box that stores secrets in plaintext. It still logs one warning the first time it reads a plaintext secret.

func Open

func Open(ctx context.Context, store KeyStore, opts Options) (*Box, error)

Open loads the database's data keys and returns the Box that seals and opens secret fields with them.

Without a configured KEK it returns a disabled Box, unless the database already holds data keys: their secrets would be unreadable, so that is an error. With a KEK and no data key yet, it creates the first one. A data key wrapped by an older KEK (GOMODEL_ENCRYPTION_KEY_PREVIOUS, or the local KEK when an extension wrapper is now configured) is re-wrapped with the current one.

func RotateDataKey

func RotateDataKey(ctx context.Context, store KeyStore, opts Options) (*Box, error)

RotateDataKey creates a new data key, wraps it with the current KEK, and makes it active. Values sealed with older data keys stay readable; `gomodel secrets reencrypt` moves them to the new one.

func (*Box) ActiveKeyID

func (b *Box) ActiveKeyID() string

ActiveKeyID returns the id of the data key new values are sealed with, or "" when encryption is disabled.

func (*Box) ConfirmSeal

func (b *Box) ConfirmSeal(reseal func() error, fields ...Field) error

ConfirmSeal is what a store runs right after writing freshly sealed fields: when RotatedSinceSeal reports a rotation, it calls reseal to rewrite the row with the new key. A failed check is retried once after a forced key reload. Any remaining failure wraps ErrSealUnconfirmed: the row is written, so the caller must not treat the save as lost, but it must not report a clean save either.

func (*Box) Enabled

func (b *Box) Enabled() bool

Enabled reports whether the Box seals new values. It never changes over the life of a Box: a reload only adds keys or moves the active one.

func (*Box) IsCurrent

func (b *Box) IsCurrent(value string) bool

IsCurrent reports whether value is sealed with the active data key, so re-encryption can skip it.

func (*Box) NeedsReseal

func (b *Box) NeedsReseal(fields ...Field) bool

NeedsReseal reports whether any field holds plaintext or a value sealed with a data key other than the active one. It is always false for a disabled Box.

func (*Box) Open

func (b *Box) Open(aad []byte, value string) (string, error)

Open decrypts a sealed value. Plaintext is returned as-is: rows written before encryption was enabled stay readable and are sealed on their next write.

func (*Box) OpenFields

func (b *Box) OpenFields(kind, id string, fields ...Field) error

OpenFields opens every field of one entity in place.

func (*Box) RotatedSinceSeal

func (b *Box) RotatedSinceSeal(fields ...Field) (bool, error)

RotatedSinceSeal reports whether the data key that sealed fields is no longer active, according to the key store. Stores call it right after writing freshly sealed values, which closes the race with a data key rotation: if the key is still active, any rotation activates after the write, so the rotation's re-encryption pass will read the row; if it is not, the store re-seals the row itself. A Box without a key store, or fields with nothing sealed, never report a rotation. It fails when the key store cannot say which key is active.

func (*Box) Seal

func (b *Box) Seal(aad []byte, plaintext string) (string, error)

Seal encrypts plaintext under the active data key. Empty values and every value of a disabled Box are returned unchanged.

A Box backed by a key store first checks which data key is active, so a gateway that outlived a data key rotation never seals with the replaced key. Seals only happen on admin writes, so the extra read is cheap.

func (*Box) SealFields

func (b *Box) SealFields(kind, id string, fields ...Field) error

SealFields seals every field of one entity in place, all with the same data key: the active one is looked up once per entity, not per field.

type Field

type Field struct {
	Name  string
	Value *string
}

Field points at one secret value of an entity. Name is the field name bound into the value's additional authenticated data.

type Key

type Key struct {
	// ID is what sealed values name. Ids are decimal sequence numbers: the
	// first key is "1", so instances starting together against an empty
	// database race on the primary key rather than creating two keys.
	ID string
	// Wrapped is the data key encrypted by the wrapper named in WrapperID.
	Wrapped   []byte
	WrapperID string
	// KDF, KDFParams and Salt describe how the local KEK is derived from
	// GOMODEL_ENCRYPTION_KEY. They are empty for an extension wrapper.
	KDF       string
	KDFParams string
	Salt      []byte
	// Active marks the key new values are sealed with.
	Active    bool
	CreatedAt time.Time
}

Key is one row of the encryption_keys table: a data key wrapped by a key-encryption key. The plaintext data key is never stored.

type KeyStore

type KeyStore interface {
	// List returns every key.
	List(ctx context.Context) ([]Key, error)
	// Insert adds a key and reports false when the id is already taken.
	Insert(ctx context.Context, key Key) (bool, error)
	// UpdateWrapping replaces how a key is wrapped (after a KEK rotation).
	UpdateWrapping(ctx context.Context, key Key) error
	// Activate marks id, the newest key, active and clears the flag of
	// older keys.
	Activate(ctx context.Context, id string) error
}

KeyStore persists wrapped data keys.

func NewKeyStore

func NewKeyStore(ctx context.Context, shared storage.Storage) (KeyStore, error)

NewKeyStore opens the encryption_keys table or collection on the shared storage connection, creating it when needed.

type MongoDBKeyStore

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

MongoDBKeyStore stores wrapped data keys in MongoDB.

func NewMongoDBKeyStore

func NewMongoDBKeyStore(_ context.Context, database *mongo.Database) (*MongoDBKeyStore, error)

NewMongoDBKeyStore uses the encryption_keys collection. It needs no indexes: the collection holds a handful of documents keyed by _id.

func (*MongoDBKeyStore) Activate

func (s *MongoDBKeyStore) Activate(ctx context.Context, id string) error

Activate sets the new key active, then clears the flag of every older key. MongoDB transactions need a replica set, which a standalone server lacks, so the two steps are not atomic. Clearing only lower ids keeps concurrent rotations from clearing each other's key: whatever interleaving, the newest activated key stays active, and readers pick the newest of several.

func (*MongoDBKeyStore) Insert

func (s *MongoDBKeyStore) Insert(ctx context.Context, key Key) (bool, error)

func (*MongoDBKeyStore) List

func (s *MongoDBKeyStore) List(ctx context.Context) ([]Key, error)

func (*MongoDBKeyStore) UpdateWrapping

func (s *MongoDBKeyStore) UpdateWrapping(ctx context.Context, key Key) error

type Options

type Options struct {
	// Key is GOMODEL_ENCRYPTION_KEY. It derives the local KEK.
	Key string
	// PreviousKey is GOMODEL_ENCRYPTION_KEY_PREVIOUS: a local KEK that is
	// only used to unwrap data keys so they can be re-wrapped with the
	// current one.
	PreviousKey string
	// Wrapper is an extension's KMS-backed wrapper. When set it replaces the
	// local KEK; Key and PreviousKey then only unwrap data keys that still
	// need to be moved to it.
	Wrapper Wrapper
	// PreviousWrappers unwrap data keys held by wrappers Wrapper replaces,
	// so they can be re-wrapped with it.
	PreviousWrappers []Wrapper
}

Options selects the key-encryption key.

type Report

type Report struct {
	// Entity names the table or collection.
	Entity string
	// Rows is how many rows were examined.
	Rows int
	// Reencrypted is how many rows were rewritten.
	Reencrypted int
	// Skipped is how many rows kept changing under the pass and were left
	// for the next run.
	Skipped int
}

Report counts what a re-encryption pass did to one entity kind. It never carries values.

func ReencryptRows

func ReencryptRows(entity string, names []string, rewrite func(name string) (RowOutcome, error)) (Report, error)

ReencryptRows runs rewrite for each row and tallies the outcomes. A row in conflict is re-read and retried a few times, then counted as skipped.

type RowOutcome

type RowOutcome int

RowOutcome is what re-encrypting one row did.

const (
	// RowUnchanged: the row needed no rewrite, or no longer exists.
	RowUnchanged RowOutcome = iota
	// RowRewritten: the row's secrets were re-sealed.
	RowRewritten
	// RowConflict: the row's secrets changed between the read and the
	// conditional write, so nothing was written.
	RowConflict
)

type SQLKeyStore

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

SQLKeyStore stores wrapped data keys in SQLite or PostgreSQL. Binary values are stored as base64 text so one schema serves both engines.

func NewSQLKeyStore

func NewSQLKeyStore(ctx context.Context, db sqlx.DB) (*SQLKeyStore, error)

NewSQLKeyStore creates the encryption_keys table if needed.

func (*SQLKeyStore) Activate

func (s *SQLKeyStore) Activate(ctx context.Context, id string) error

Activate flips every row in one statement, so readers never see two active keys or none.

func (*SQLKeyStore) Insert

func (s *SQLKeyStore) Insert(ctx context.Context, key Key) (bool, error)

func (*SQLKeyStore) List

func (s *SQLKeyStore) List(ctx context.Context) ([]Key, error)

func (*SQLKeyStore) UpdateWrapping

func (s *SQLKeyStore) UpdateWrapping(ctx context.Context, key Key) error

type Wrapper

type Wrapper interface {
	ID() string
	WrapKey(ctx context.Context, dek []byte) ([]byte, error)
	UnwrapKey(ctx context.Context, wrapped []byte) ([]byte, error)
}

Wrapper wraps and unwraps data keys with a key-encryption key held elsewhere, such as a KMS. It has the method set of config.KeyWrapper, so an extension's wrapper is passed straight through.

Directories

Path Synopsis
Package encryptiontest provides an in-memory key store and ready Boxes for tests of stores that seal secrets.
Package encryptiontest provides an in-memory key store and ready Boxes for tests of stores that seal secrets.

Jump to

Keyboard shortcuts

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