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
- type Envelope
- func (e Envelope) ActiveKeyID() string
- func (e Envelope) DeriveFromMaster(_ context.Context, purpose port.Purpose, salt []byte, length int) (port.MasterDerived, error)
- func (e Envelope) KeyIDs() []string
- func (e Envelope) Open(_ context.Context, sealed port.Sealed, purpose port.Purpose) (secret.Secret, error)
- func (e Envelope) ReproduceFromMaster(_ context.Context, keyID string, purpose port.Purpose, salt []byte, length int) (secret.Bytes, error)
- func (e Envelope) Rewrap(_ context.Context, sealed port.Sealed, purpose port.Purpose) (port.Sealed, error)
- func (e Envelope) Seal(_ context.Context, plaintext secret.Secret, purpose port.Purpose) (port.Sealed, error)
- type KeyMaterial
- type Keyring
- type Passphrase
- type Passwords
- type Stream
Constants ¶
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.
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 (Envelope) ActiveKeyID ¶
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) 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.
type KeyMaterial ¶
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 ¶
ActiveKeyID is the key new values are sealed under, empty on a ring with no keys.
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 ¶
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) NeedsRehash ¶
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 ¶
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 ¶
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 ¶
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.