Documentation
¶
Overview ¶
Package crypto wraps age. Cipher is the encrypt/decrypt seam, satisfied by PassphraseCipher (symmetric, passphrase-derived, used to wrap key slots) and MasterKey (the random master that encrypts every blob).
Index ¶
- Constants
- Variables
- func EncryptToMasters(plaintext []byte, masters ...*MasterKey) ([]byte, error)
- func Fingerprint(vaultID, signPub string) string
- func KDFKnown(kdf string) bool
- func NewHeader(passphrase, slotName string) (*Header, *MasterKey, error)
- func NewRecipientHeader(recipient *age.X25519Recipient, slotName string) (*Header, *MasterKey, error)
- func NewVaultID() (string, error)
- func SuiteKnown(suite string) bool
- type Cipher
- type Header
- func (h *Header) AddPassphraseSlot(passphrase, name string, mk *MasterKey) error
- func (h *Header) AddRecipientSlot(recipient *age.X25519Recipient, name string, mk *MasterKey) error
- func (h *Header) Marshal() ([]byte, error)
- func (h *Header) NamespaceEntry(ns string) (ManifestEntry, bool)
- func (h *Header) PrimarySlot() int
- func (h *Header) RemoveNamespace(ns string)
- func (h *Header) RemoveSlot(i int, mk *MasterKey) error
- func (h *Header) RotateSlot(i int, newPassphrase string, slotKey *age.X25519Identity) error
- func (h *Header) Seal(mk *MasterKey) error
- func (h *Header) SetMaster(mk *MasterKey) error
- func (h *Header) SetNamespace(ns string, e ManifestEntry)
- func (h *Header) SetPrimary(i int) error
- func (h *Header) Unlock(passphrase string) (*MasterKey, int, *age.X25519Identity, error)
- func (h *Header) UnlockIdentity(id *age.X25519Identity) (*MasterKey, int, error)
- func (h *Header) Verify(mk *MasterKey) error
- type ManifestEntry
- type MasterKey
- func (m *MasterKey) BlobMAC(plaintext []byte) (string, error)
- func (m *MasterKey) CheckBlobMAC(plaintext []byte, want string) error
- func (m *MasterKey) Decrypt(ciphertext []byte) ([]byte, error)
- func (m *MasterKey) Encrypt(plaintext []byte) ([]byte, error)
- func (m *MasterKey) PublicKey() string
- func (m *MasterKey) SignPub() (string, error)
- func (m *MasterKey) String() string
- type PassphraseCipher
- type Slot
- type Transition
Constants ¶
const ( SlotPassphrase = "passphrase" SlotRecipient = "recipient" )
Slot type tags.
const ( // KDFAgeScrypt is age's scrypt passphrase recipient: the only KDF today. age // embeds its own work factor in the wrapped blob, so a parameter bump needs // no new identifier; this tag exists for the family jump (scrypt -> Argon2id) // that age cannot express. KDFAgeScrypt = "age-scrypt" )
KDF identifiers. The passphrase KDF is recorded per slot, not per suite, because slots are independently re-wrapped (RotateSlot), so a KDF can be upgraded one slot at a time as each owner re-enters their passphrase.
const ( // SuiteX25519 is notenv's only suite: an age X25519 master, age // (X25519 -> ChaCha20-Poly1305) encryption, HMAC-SHA256 header and manifest // MACs keyed via HKDF-SHA256, and Ed25519 rotation signatures. SuiteX25519 = "x25519-hmacsha256-ed25519" )
Suite identifiers. A suite bundles the master-key type, object/wrap encryption, the header and manifest MACs, and the rotation signature.
Variables ¶
var ErrNotRecipient = errors.New("this secret was encrypted under a different key")
ErrNotRecipient reports that a ciphertext is valid age but was not encrypted to the key that tried to open it: the key is wrong, not the data. Callers (rotation's fallback read, the stale-cache retry) branch on it with errors.Is.
var ErrWrongPassphrase = errors.New("wrong passphrase")
ErrWrongPassphrase is returned when a passphrase does not match the ciphertext (or, for Header.Unlock, any key slot).
Functions ¶
func EncryptToMasters ¶ added in v0.2.0
EncryptToMasters seals plaintext to every master's recipient, so any of their identities can decrypt it. Rotation uses this to keep a blob readable under both the old and new master at once during the transition.
func Fingerprint ¶ added in v0.14.0
Fingerprint digests a vault's identity and signing key into a short code the vault owner sends out-of-band during onboarding. A first contact that verifies the served header against it (directly, or through the signed rotation chain to an ancestor signing key) cannot be pointed at a substituted vault, which closes trust-on-first-use for onboarded humans.
func KDFKnown ¶ added in v0.16.0
KDFKnown reports whether this build can unwrap a slot wrapped under the given KDF.
func NewHeader ¶
NewHeader generates a master key and a header with one passphrase slot: the primary slot, owned by whoever initialized the storage.
func NewRecipientHeader ¶ added in v0.10.0
func NewRecipientHeader(recipient *age.X25519Recipient, slotName string) (*Header, *MasterKey, error)
NewRecipientHeader generates a master key and a header whose only slot is an age recipient: the promptless creation path (CI, agents). No passphrase exists; the identity holder is the vault's owner.
func NewVaultID ¶ added in v0.7.0
NewVaultID mints a vault's stable random identity.
func SuiteKnown ¶ added in v0.16.0
SuiteKnown reports whether this build can read a vault on the given suite.
Types ¶
type Header ¶
type Header struct {
Version int `json:"version"`
Suite string `json:"suite"` // cipher-suite identifier (see suite.go): the algorithm bundle this vault uses. notenv owns the choice; users never select one
VaultID string `json:"vault_id,omitempty"` // random, minted once at creation; the stable identity pins and transitions are scoped to, surviving relocation of the vault to another remote/base
Recipient string `json:"recipient"` // master public key (decorative)
SignPub string `json:"sign_pub,omitempty"` // the master's Ed25519 public key (derived from the master secret); pins store it, rotation transitions are verified against it
Revision int `json:"revision"` // monotonic; bumped on every write (anti-rollback)
Master []byte `json:"master"` // master identity, age-encrypted to every slot's public key
Slots []Slot `json:"slots"`
// Manifest binds every namespace's blob to this authenticated header:
// namespace name → blob pointer and its plaintext MAC (see manifest.go). Nil
// only on a vault that has never stored a secret.
Manifest map[string]ManifestEntry `json:"manifest,omitempty"`
// Transitions is the vault's rotation history: the signed record of every
// master change, each authorized by the master it replaced. A machine pinned
// at an older master walks this chain to follow a legitimate rotation without
// alarming (see internal/keymgmt). It rides in the header so a rotation's
// record and its header flip land in one compare-and-swap, never as two
// writes that race; nil until the first rotation.
Transitions []Transition `json:"transitions,omitempty"`
Auth []byte `json:"auth,omitempty"` // HMAC over the header keyed from the master (see auth.go)
}
Header is the parsed header object. Optional-shaped fields carry omitempty so a parsed header reproduces the exact canonical bytes it was sealed over (a vault that has never stored a secret has no manifest, and its tag was computed without the key present); changing a tag's presence rules breaks verification of every header already sealed under the old rules.
func ParseHeader ¶
func (*Header) AddPassphraseSlot ¶ added in v0.2.0
AddPassphraseSlot creates a slot keypair, wraps its private key under the passphrase, and re-encrypts the master to include the new slot.
func (*Header) AddRecipientSlot ¶ added in v0.2.0
AddRecipientSlot adds a teammate by their age public key, which is the slot key; they hold the private key and unlock with their own age identity.
func (*Header) NamespaceEntry ¶ added in v0.18.0
func (h *Header) NamespaceEntry(ns string) (ManifestEntry, bool)
NamespaceEntry returns the manifest entry for a namespace and whether one exists. A namespace that was never created has none; one that exists keeps its entry even when it holds no secrets (namespaces are persistent), so presence here means "the namespace exists", not "it holds secrets".
func (*Header) PrimarySlot ¶ added in v0.2.0
PrimarySlot returns the index of the primary slot, or -1 if none is marked.
func (*Header) RemoveNamespace ¶ added in v0.18.0
RemoveNamespace drops a namespace's entry from the manifest (for example, on `namespace delete`).
func (*Header) RemoveSlot ¶ added in v0.2.0
RemoveSlot deletes slot i and re-encrypts the master to the survivors, so the removed slot can no longer decrypt the master. It refuses to remove the last slot (which would brick the header). NOTE: this does not re-key blobs, so a holder who retained the master is not revoked; true revocation re-keys via SetMaster (rotate-master). mk is the current master.
func (*Header) RotateSlot ¶ added in v0.2.0
RotateSlot re-wraps a passphrase slot's private key under a new passphrase. The master, the slot keypair, and Master are untouched, so other slots and every blob are unaffected. slotKey is the slot's private key, obtained from Unlock. Rotation clears Provisional: setting an own passphrase is exactly what the flag waits for.
func (*Header) Seal ¶ added in v0.2.0
Seal sets the header's authentication tag over its current contents.
func (*Header) SetMaster ¶ added in v0.2.0
SetMaster installs a new master key, re-encrypting it to every slot's public key. Used by rotate-master after the blobs have been re-keyed.
func (*Header) SetNamespace ¶ added in v0.18.0
func (h *Header) SetNamespace(ns string, e ManifestEntry)
SetNamespace records (or replaces) a namespace's blob entry.
func (*Header) SetPrimary ¶ added in v0.2.0
SetPrimary makes slot i the sole primary slot.
func (*Header) Unlock ¶
Unlock opens the master via a passphrase. It tries each passphrase slot in turn and returns the first whose wrapped private key the passphrase decrypts AND which opens the master: the master, the matched slot index, and the slot private key (needed to rotate that passphrase). A slot whose blob does not decrypt, does not parse as a key, or whose key is not a recipient of the master is skipped, not fatal, so one corrupt, stale, or planted slot cannot shadow a valid later slot under the same passphrase. ErrWrongPassphrase if none opens, with a distinct hint when a slot's passphrase matched but its key could not open the master (a tampered or half-rotated header). Cost note: each trial is a full scrypt derivation, so unlock latency scales with the number of passphrase slots tried before a match (a wrong passphrase pays for all of them). Fine at the intended 2–3 slots; a vault with many will feel it.
func (*Header) UnlockIdentity ¶ added in v0.2.0
UnlockIdentity opens the master via a teammate's age identity (their recipient slot). Returns the master and the matched slot index (or -1 if the identity decrypts the master but matches no slot's public key). ErrWrongPassphrase if the identity is not a recipient of the master.
type ManifestEntry ¶ added in v0.8.0
type ManifestEntry struct {
Blob string `json:"blob"`
MAC string `json:"mac"`
Prev string `json:"prev,omitempty"`
PrevMAC string `json:"prev_mac,omitempty"`
}
ManifestEntry is one namespace's blob the header vouches for. Blob is the object key of the current blob and MAC is its plaintext MAC. Prev/PrevMAC name the one-generation backup (the blob the previous write superseded), kept so a corrupt current blob can fall back to the last good one; both are empty on a namespace's first write, before any generation has been superseded.
type MasterKey ¶
type MasterKey struct {
// contains filtered or unexported fields
}
MasterKey is the unwrapped master identity. It satisfies Cipher: Encrypt seals to the master recipient, Decrypt opens with the identity.
func GenerateMasterKey ¶ added in v0.2.0
GenerateMasterKey mints a fresh master key under the current suite (used by rotation, which always lands on current-best; see design/crypto-agility.md).
func ParseMasterKey ¶
ParseMasterKey parses the identity string form (used by the session cache, which stores the unwrapped master key, not the passphrase).
func (*MasterKey) BlobMAC ¶ added in v0.18.0
BlobMAC computes the manifest MAC for a blob's plaintext.
func (*MasterKey) CheckBlobMAC ¶ added in v0.18.0
CheckBlobMAC verifies a blob's plaintext against a recorded MAC in constant time.
func (*MasterKey) PublicKey ¶ added in v0.2.0
PublicKey returns the master's public key (its rollback-pin fingerprint).
type PassphraseCipher ¶
type PassphraseCipher struct {
// contains filtered or unexported fields
}
PassphraseCipher encrypts to an age scrypt recipient (symmetric, passphrase-derived). In the header model it wraps key-slot contents, not data blobs.
func NewPassphraseCipher ¶
func NewPassphraseCipher(passphrase string) *PassphraseCipher
type Slot ¶
type Slot struct {
Name string `json:"name,omitempty"`
Primary bool `json:"primary,omitempty"`
Type string `json:"type"` // "passphrase" | "recipient"
PublicKey string `json:"public_key"` // recipient of Master
Wrapped []byte `json:"wrapped,omitempty"` // passphrase slots: slot private key, scrypt-encrypted
KDF string `json:"kdf,omitempty"` // passphrase slots: the KDF its Wrapped blob uses (see suite.go); empty on recipient slots
// Provisional marks a passphrase slot still wrapped under the temporary
// onboarding passphrase its issuer generated (and therefore knows). Tooling
// refuses to operate under a provisional slot until its holder rotates it
// to a passphrase of their own, which clears the flag. Tooling-enforced,
// like Primary.
Provisional bool `json:"provisional,omitempty"`
// TS is the slot's creation time in Unix seconds. Advisory display data
// (clocks lie): it feeds `credential inspect` and the stale-provisional warning and
// is never load-bearing. Stamped by the command layer; this package reads
// no clock.
TS int64 `json:"ts,omitempty"`
}
Slot is one credential that can unlock the master. Name identifies its owner (user@host). Primary marks the slot whose owner may rotate/remove other slots; advisory until header signing exists, but tooling refuses to remove or demote it. PublicKey is the slot's age public key (a recipient of Master); for a recipient slot it is the teammate's public key.
type Transition ¶ added in v0.7.0
type Transition struct {
VaultID string `json:"vault_id"`
FromSignPub string `json:"from_sign_pub"`
ToSignPub string `json:"to_sign_pub"`
ToMasterPub string `json:"to_master_pub"`
ToRevision int `json:"to_revision"`
Sig []byte `json:"sig"`
}
A Transition is the outgoing master's signed statement that a successor is legitimate: "I, the master whose signing key is FromSignPub, was replaced by the master (ToSignPub, ToMasterPub) at header revision ToRevision, in vault VaultID." A machine whose local pin still names the old master verifies the signature with the pinned public key and follows the change without raising the master-changed alarm, so the alarm fires only for changes nobody with the old master authorized.
The slot set is deliberately not covered: the new header's own MAC authenticates it under the new master, and the old key's signature over it would stop no one it doesn't already stop. The vault ID prevents replaying a transition from one vault against another; the revision orders chains.
func NewTransition ¶ added in v0.7.0
func NewTransition(old, newKey *MasterKey, vaultID string, toRevision int) (*Transition, error)
NewTransition builds and signs the record for a master change: old is the master being replaced (it signs), newKey the successor, toRevision the header revision the successor is installed at.
func (*Transition) Verify ¶ added in v0.7.0
func (t *Transition) Verify() error
Verify checks the signature under the transition's own FromSignPub. The caller decides whether that key is one it trusts (its pin, or the end of a previously verified chain); Verify only proves the record is internally authentic.