crypto

package
v1.0.3 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

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

View Source
const (
	SlotPassphrase = "passphrase"
	SlotRecipient  = "recipient"
)

Slot type tags.

View Source
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.

View Source
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

View Source
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.

View Source
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

func EncryptToMasters(plaintext []byte, masters ...*MasterKey) ([]byte, error)

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

func Fingerprint(vaultID, signPub string) string

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

func KDFKnown(kdf string) bool

KDFKnown reports whether this build can unwrap a slot wrapped under the given KDF.

func NewHeader

func NewHeader(passphrase, slotName string) (*Header, *MasterKey, error)

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

func NewVaultID() (string, error)

NewVaultID mints a vault's stable random identity.

func SuiteKnown added in v0.16.0

func SuiteKnown(suite string) bool

SuiteKnown reports whether this build can read a vault on the given suite.

Types

type Cipher

type Cipher interface {
	Encrypt(plaintext []byte) ([]byte, error)
	Decrypt(ciphertext []byte) ([]byte, error)
}
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 ParseHeader(data []byte) (*Header, error)

func (*Header) AddPassphraseSlot added in v0.2.0

func (h *Header) AddPassphraseSlot(passphrase, name string, mk *MasterKey) error

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

func (h *Header) AddRecipientSlot(recipient *age.X25519Recipient, name string, mk *MasterKey) error

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) Marshal

func (h *Header) Marshal() ([]byte, error)

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

func (h *Header) PrimarySlot() int

PrimarySlot returns the index of the primary slot, or -1 if none is marked.

func (*Header) RemoveNamespace added in v0.18.0

func (h *Header) RemoveNamespace(ns string)

RemoveNamespace drops a namespace's entry from the manifest (for example, on `namespace delete`).

func (*Header) RemoveSlot added in v0.2.0

func (h *Header) RemoveSlot(i int, mk *MasterKey) error

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

func (h *Header) RotateSlot(i int, newPassphrase string, slotKey *age.X25519Identity) error

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

func (h *Header) Seal(mk *MasterKey) error

Seal sets the header's authentication tag over its current contents.

func (*Header) SetMaster added in v0.2.0

func (h *Header) SetMaster(mk *MasterKey) error

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

func (h *Header) SetPrimary(i int) error

SetPrimary makes slot i the sole primary slot.

func (*Header) Unlock

func (h *Header) Unlock(passphrase string) (*MasterKey, int, *age.X25519Identity, error)

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

func (h *Header) UnlockIdentity(id *age.X25519Identity) (*MasterKey, int, error)

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.

func (*Header) Verify added in v0.2.0

func (h *Header) Verify(mk *MasterKey) error

Verify recomputes the tag and constant-time compares it to the stored one.

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

func GenerateMasterKey() (*MasterKey, error)

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

func ParseMasterKey(s string) (*MasterKey, error)

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

func (m *MasterKey) BlobMAC(plaintext []byte) (string, error)

BlobMAC computes the manifest MAC for a blob's plaintext.

func (*MasterKey) CheckBlobMAC added in v0.18.0

func (m *MasterKey) CheckBlobMAC(plaintext []byte, want string) error

CheckBlobMAC verifies a blob's plaintext against a recorded MAC in constant time.

func (*MasterKey) Decrypt

func (m *MasterKey) Decrypt(ciphertext []byte) ([]byte, error)

func (*MasterKey) Encrypt

func (m *MasterKey) Encrypt(plaintext []byte) ([]byte, error)

func (*MasterKey) PublicKey added in v0.2.0

func (m *MasterKey) PublicKey() string

PublicKey returns the master's public key (its rollback-pin fingerprint).

func (*MasterKey) SignPub added in v0.7.0

func (m *MasterKey) SignPub() (string, error)

SignPub returns the master's Ed25519 public key, hex-encoded: the form the header carries and pins store.

func (*MasterKey) String

func (m *MasterKey) String() string

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

func (*PassphraseCipher) Decrypt

func (c *PassphraseCipher) Decrypt(ciphertext []byte) ([]byte, error)

func (*PassphraseCipher) Encrypt

func (c *PassphraseCipher) Encrypt(plaintext []byte) ([]byte, error)

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.

Jump to

Keyboard shortcuts

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