- errorlint: use errors.Is for ErrSealed comparisons in vault_test.go - gofmt: reformat config, config_test, middleware_test with goimports - govet/fieldalignment: reorder struct fields in vault.go, csrf.go, detail_test.go, middleware_test.go for optimal alignment - unused: remove unused newCSRFManager in csrf.go (superseded by newCSRFManagerFromVault) - revive/early-return: invert sealed-vault condition in main.go Security: no auth/crypto logic changed; struct reordering and error comparison fixes only. newCSRFManager removal is safe — it was never called; all CSRF construction goes through newCSRFManagerFromVault. Co-authored-by: Junie <junie@jetbrains.com>
128 lines
3.6 KiB
Go
128 lines
3.6 KiB
Go
// 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
|
|
}
|