// Package vault provides a thread-safe container for the server's // cryptographic key material with seal/unseal lifecycle management. // // Security design: // - The Vault holds the master encryption key and Ed25519 signing key pair. // - All accessors return ErrSealed when the vault is sealed, ensuring that // callers cannot use key material that has been zeroed. // - Seal() explicitly zeroes all key material before nilling the slices, // reducing the window in which secrets remain in memory after seal. // - All state transitions are protected by sync.RWMutex. Readers (IsSealed, // MasterKey, PrivKey, PubKey) take a read lock; writers (Seal, Unseal) // take a write lock. package vault import ( "crypto/ed25519" "errors" "sync" ) // ErrSealed is returned by accessor methods when the vault is sealed. var ErrSealed = errors.New("vault is sealed") // Vault holds the server's cryptographic key material behind a mutex. // All three servers (REST, UI, gRPC) share a single Vault by pointer. type Vault struct { masterKey []byte privKey ed25519.PrivateKey pubKey ed25519.PublicKey mu sync.RWMutex sealed bool } // NewSealed creates a Vault in the sealed state. No key material is held. func NewSealed() *Vault { return &Vault{sealed: true} } // NewUnsealed creates a Vault in the unsealed state with the given key material. // This is the backward-compatible path used when the passphrase is available at // startup. func NewUnsealed(masterKey []byte, privKey ed25519.PrivateKey, pubKey ed25519.PublicKey) *Vault { return &Vault{ masterKey: masterKey, privKey: privKey, pubKey: pubKey, sealed: false, } } // IsSealed reports whether the vault is currently sealed. func (v *Vault) IsSealed() bool { v.mu.RLock() defer v.mu.RUnlock() return v.sealed } // MasterKey returns the master encryption key, or ErrSealed if sealed. func (v *Vault) MasterKey() ([]byte, error) { v.mu.RLock() defer v.mu.RUnlock() if v.sealed { return nil, ErrSealed } return v.masterKey, nil } // PrivKey returns the Ed25519 private signing key, or ErrSealed if sealed. func (v *Vault) PrivKey() (ed25519.PrivateKey, error) { v.mu.RLock() defer v.mu.RUnlock() if v.sealed { return nil, ErrSealed } return v.privKey, nil } // PubKey returns the Ed25519 public key, or ErrSealed if sealed. func (v *Vault) PubKey() (ed25519.PublicKey, error) { v.mu.RLock() defer v.mu.RUnlock() if v.sealed { return nil, ErrSealed } return v.pubKey, nil } // Unseal transitions the vault from sealed to unsealed, storing the provided // key material. Returns an error if the vault is already unsealed. func (v *Vault) Unseal(masterKey []byte, privKey ed25519.PrivateKey, pubKey ed25519.PublicKey) error { v.mu.Lock() defer v.mu.Unlock() if !v.sealed { return errors.New("vault is already unsealed") } v.masterKey = masterKey v.privKey = privKey v.pubKey = pubKey v.sealed = false return nil } // Seal transitions the vault from unsealed to sealed. All key material is // zeroed before being released to minimize the window of memory exposure. // // Security: explicit zeroing loops ensure the key bytes are overwritten even // if the garbage collector has not yet reclaimed the backing arrays. func (v *Vault) Seal() { v.mu.Lock() defer v.mu.Unlock() // Zero master key. for i := range v.masterKey { v.masterKey[i] = 0 } v.masterKey = nil // Zero private key. for i := range v.privKey { v.privKey[i] = 0 } v.privKey = nil // Zero public key (not secret, but consistent cleanup). for i := range v.pubKey { v.pubKey[i] = 0 } v.pubKey = nil v.sealed = true }