crypto

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package crypto is the adapter behind core/port/crypto: AES-256-GCM envelope encryption over a keyring the environment supplies, and Argon2id where a key has to come from a passphrase (backup-restore.md §4, security.md §8).

It is the only package in this system that names a cipher. Everything inwards of it sees the port, which is what makes open point S-2 - where the master key lives once somebody else operates the installation - a second adapter rather than a rewrite.

Index

Constants

View Source
const (
	DerivationPasses      = 3
	DerivationMemoryKiB   = 64 * 1024
	DerivationParallelism = 4
	DerivationKeyBytes    = 32
	// DerivationSaltBytes is RFC 9106's recommendation. A salt is not a secret; it exists so that
	// two targets protected by the same passphrase do not share a key, and so that a table
	// computed for one installation is worth nothing against another.
	DerivationSaltBytes = 16
)

The cost this build derives at, and why each number is the number it is.

RFC 9106 §4 gives two recommended parameter sets; these are the second - t=3, m=64 MiB, p=4 - which is the one meant for a machine that cannot spare two gibibytes per derivation. A server that derives a backup key while also answering requests is exactly that machine, and a parameter set an operator would be tempted to lower is worse than a slightly cheaper one they leave alone.

Memory is the parameter that matters. Passes and lanes can be bought with more silicon; 64 mebibytes per guess cannot, which is what makes Argon2id worth its cost over a hash that only counts iterations (backup-restore.md §4).

Raising them later is safe and is expected: every derivation stores the numbers it used, so an archive written under these opens under these even after this build has moved on.

View Source
const MinMaterialLength = 32

MinMaterialLength is the floor for configured key material, and it is the same floor HUBTASK_SECRET_KEY has for the same reason: a key an operator could have typed from memory is not a key. The material is stretched through HKDF rather than used raw, but stretching adds no entropy - it only spreads what was given.

Variables

This section is empty.

Functions

This section is empty.

Types

type Envelope

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

Envelope seals a value under a fresh data key and seals that key under the installation's current master key (security.md §8, backup-restore.md §4).

Two layers rather than one, and not for ceremony. AES-GCM is safe as long as a nonce is never reused under one key, and the risk of that grows with the number of values encrypted under it; with an envelope the master key only ever encrypts random 32-byte keys, one per value, while every value gets a key of its own. It is also what makes the stored key identifier worth having: rotating the master key re-seals nothing, because what it protects is one key per row rather than the rows.

func NewEnvelope

func NewEnvelope(ring Keyring, entropy clockport.Entropy) Envelope

func (Envelope) ActiveKeyID

func (e Envelope) ActiveKeyID() string

ActiveKeyID is the key new values are sealed under.

func (Envelope) DeriveFromMaster

func (e Envelope) DeriveFromMaster(
	_ context.Context, purpose port.Purpose, salt []byte, length int,
) (port.MasterDerived, error)

DeriveFromMaster answers a key made from the installation's current master key (backup-restore.md §4).

HKDF-SHA256 rather than a hash of the concatenation, because that is what a key derivation function is for: the extract step means the master key does not have to be uniformly random, and the expand step means two purposes cannot collide into one key however similar their names are.

The salt is the archive's own, drawn when it is written and carried in its manifest: it gives every archive a key of its own without the database having to remember anything, so an archive opens after the database is lost. A nil salt is HKDF without one - the derivation archives were written under before they carried a salt.

func (Envelope) KeyIDs

func (e Envelope) KeyIDs() []string

KeyIDs is the ring's order: current first.

func (Envelope) Open

func (e Envelope) Open(
	_ context.Context, sealed port.Sealed, purpose port.Purpose,
) (secret.Secret, error)

Open reverses it, and answers nothing at all when anything is wrong.

The order matters: the wrapped key is authenticated before the value is touched, and the value is authenticated by GCM before a single byte of it is returned. There is no code path here that produces a partial plaintext, which is the property the acceptance asks for.

func (Envelope) ReproduceFromMaster

func (e Envelope) ReproduceFromMaster(
	_ context.Context, keyID string, purpose port.Purpose, salt []byte, length int,
) (secret.Bytes, error)

ReproduceFromMaster answers the same key again, from the master key the identifier names.

func (Envelope) Rewrap

func (e Envelope) Rewrap(
	_ context.Context, sealed port.Sealed, purpose port.Purpose,
) (port.Sealed, error)

Rewrap re-keys the wrapping and nothing else.

The envelope's shape is what makes this cheap and what makes it safe: the master key only ever protected the 32-byte data key at the front of the ciphertext, so moving a value to a new master key means opening that one wrapped key under the old master and sealing it again under the current one. The value's own ciphertext - everything from the data nonce on - is copied over unchanged, and the plaintext is never reconstructed. A fresh wrapping nonce is drawn, because a nonce is only safe once per key and the new key has never seen this one.

func (Envelope) Seal

func (e Envelope) Seal(
	_ context.Context, plaintext secret.Secret, purpose port.Purpose,
) (port.Sealed, error)

Seal encrypts under a data key of its own and wraps that key under the current master key.

type KeyMaterial

type KeyMaterial struct {
	ID       string
	Material secret.Secret
}

KeyMaterial is one configured master key as the environment gives it.

type Keyring

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

Keyring is the installation's master keys: one current, and every predecessor it still holds.

A ring rather than a single key, because that is what rotation means. A single key with an identifier would let an operator change the key and would make every value sealed under the old one unreadable at the same moment - which is not a rotation but an outage. The predecessors are readable and never written to; removing one is how an operator finally retires it, and a value still naming it then says so rather than failing quietly.

func NewKeyring

func NewKeyring(entries []KeyMaterial) (Keyring, error)

NewKeyring builds the ring. The first entry is the current key: the order is the configuration's statement of which key is which, and a separate "which one is active" setting would be a second place for the two to disagree.

func (Keyring) ActiveKeyID

func (r Keyring) ActiveKeyID() string

ActiveKeyID is the key new values are sealed under, empty on a ring with no keys.

func (Keyring) IsEmpty

func (r Keyring) IsEmpty() bool

IsEmpty reports an installation that has configured no key at all. It is not an error at startup - an installation that never encrypts anything needs none - and it is the reason sealing refuses rather than silently writing a plaintext.

func (Keyring) KeyIDs

func (r Keyring) KeyIDs() []string

KeyIDs lists the keys the installation holds, current first. Safe to log: an identifier says which key and nothing about it, and "which keys does this process have" is the first question asked when something will not open.

type Passphrase

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

Passphrase derives a key from something a person typed (backup-restore.md §4).

The passphrase is stored nowhere - not here, not in the archive, not in the row that describes the target. That is the property the whole feature rests on and the reason `BackupTargetCreate.encryption_passphrase` is documented in the contract as not stored: without it the backup is useless, which is stated as an unmissable notice at setup rather than softened.

func NewPassphrase

func NewPassphrase(entropy clockport.Entropy) Passphrase

func (Passphrase) Derive

func (p Passphrase) Derive(
	passphrase secret.Secret, from port.Derivation,
) (secret.Bytes, error)

Derive computes the key. Same passphrase and same derivation, same key - on any machine, in any version, which is what lets an archive written last year open today.

func (Passphrase) NewDerivation

func (p Passphrase) NewDerivation() (port.Derivation, error)

NewDerivation draws a fresh salt and states this build's cost.

A new salt every time, never a reused one and never one derived from the target's name: two targets protected by the same passphrase must not arrive at the same key, or cracking one cracks both.

type Passwords

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

Passwords hashes and verifies the local accounts' credential (ADR-0005, T-02).

The stored form is the PHC string the reference implementation defines - $argon2id$v=19$m=…,t=…,p=…$salt$hash - because it carries its own parameters: verification reads the cost from the hash, so raising this build's numbers leaves every stored password verifiable.

func NewPasswords

func NewPasswords(entropy clockport.Entropy) (Passwords, error)

NewPasswords draws the decoy. The error path exists because construction draws randomness, and a process that cannot draw randomness must not start pretending it can verify credentials.

func (Passwords) Hash

func (p Passwords) Hash(password secret.Secret) (string, error)

Hash computes the stored form under this build's cost, with a fresh salt.

func (Passwords) NeedsRehash

func (p Passwords) NeedsRehash(stored string) bool

NeedsRehash answers whether a stored hash was computed under a lower cost than this build's: any of memory, passes, lanes or key length below the constants above. Lower, not different: during a rolling update an older build serves beside a newer one, and "different" would have the two rewrite each other's hashes on every sign-in. A cost lowered on purpose therefore reaches a hash only when the password is next set. An unreadable hash answers false - Verify reports it.

func (Passwords) Verify

func (p Passwords) Verify(stored string, password secret.Secret) (bool, error)

Verify compares a presented password against a stored hash, in constant time over the derived key. It answers false rather than an error for a wrong password, because "wrong" is an answer and not a failure; the error path is for a stored value this process cannot read.

func (Passwords) VerifyDecoy

func (p Passwords) VerifyDecoy(password secret.Secret)

VerifyDecoy burns the same work a real verification would, against the construction-time decoy. The answer is always false; the point is that "no such account" takes as long as "wrong password" and exercises the same code (T-02).

type Stream

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

Stream is the archive's cipher: AES-256-GCM applied chunk by chunk, so that a member of any size is encrypted on its way to a target without ever being held (backup-restore.md §4).

It shares a package with Envelope rather than living beside its caller because of the seam TestCryptographyStaysInItsAdapter enforces: one place in this repository names a cipher, has one nonce discipline, and is the one file that changes when open point S-2 moves the key material somewhere else.

func NewStream

func NewStream(entropy clockport.Entropy) Stream

NewStream builds the cipher. The entropy source is a port for the reason every source of non-determinism in this system is: a test that cannot fix the nonce cannot assert the bytes.

func (Stream) KeyBytes

func (s Stream) KeyBytes() int

KeyBytes is 32 - AES-256, as §4 requires.

func (Stream) Open

func (s Stream) Open(r io.Reader, key secret.Bytes, purpose port.Purpose) (io.Reader, error)

Open unwraps a reader. Nothing reaches the caller before its chunk has been authenticated.

func (Stream) Seal

func (s Stream) Seal(w io.Writer, key secret.Bytes, purpose port.Purpose) (io.WriteCloser, error)

Seal wraps a writer. Every chunk is encrypted as it fills; nothing is held beyond one chunk.

Jump to

Keyboard shortcuts

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