crypto

package
v0.12.1 Latest Latest
Warning

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

Go to latest
Published: Jun 12, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package crypto wraps age. Cipher is satisfied by PassphraseCipher (MVP) and, post-MVP, a recipients-based cipher.

Index

Constants

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

Slot type tags.

Variables

View Source
var ErrNotRecipient = errors.New("blob was not encrypted under the current master 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 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.

Types

type Cipher

type Cipher interface {
	Encrypt(plaintext []byte) ([]byte, error)
	Decrypt(ciphertext []byte) ([]byte, error)
}
type Header struct {
	Version   int    `json:"version"`
	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 segment/snapshot object to this authenticated
	// header: full object key → plaintext MAC (see manifest.go). Nil only on a
	// vault that has never stored a secret.
	Manifest map[string]ManifestEntry `json:"manifest,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) ApplyManifest added in v0.8.0

func (h *Header) ApplyManifest(d ManifestDelta)

ApplyManifest applies a delta to the header's manifest.

func (*Header) Marshal

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

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

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) 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 finds the passphrase slot whose wrapped private key the passphrase decrypts, then decrypts the master with that slot key. Returns the master, the matched slot index, and the slot private key (needed to rotate that passphrase). ErrWrongPassphrase if none opens. Cost note: each trial is a full scrypt derivation, so unlock latency scales with the number of passphrase slots tried before a match (and a wrong passphrase always pays for all of them). Fine at the intended 2–3 slots; a vault with many passphrase slots 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 ManifestDelta added in v0.8.0

type ManifestDelta struct {
	Add   map[string]ManifestEntry
	Fold  []string
	Prune []string
}

ManifestDelta is one writer's change to the manifest, applied atomically under the header compare-and-swap. Add lands new or adopted entries, Fold marks existing entries as subsumed by a snapshot, Prune drops entries whose objects are confirmed gone. Applied in that order.

func (ManifestDelta) Empty added in v0.8.0

func (d ManifestDelta) Empty() bool

Empty reports whether applying the delta would change nothing.

type ManifestEntry added in v0.8.0

type ManifestEntry struct {
	MAC    string `json:"mac"`
	Folded bool   `json:"folded,omitempty"`
}

ManifestEntry is one object the header vouches for. Folded marks an object a compaction has subsumed but possibly not yet deleted: readers skip it, and a later manifest write prunes the entry once the object is confirmed gone.

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 with no header (used by rotation).

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) CheckObjectMAC added in v0.8.0

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

CheckObjectMAC verifies an object's plaintext against a manifest entry's 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) ObjectMAC added in v0.8.0

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

ObjectMAC computes the manifest MAC for an object's plaintext.

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
}

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, new *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), new 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