package secret import ( "crypto/aes" "crypto/cipher" "encoding/base64" "errors" "fmt" "os" "path/filepath" "strings" ) // Keyring wraps the per-vault secrets the server must keep in order to merge // a vault: it can decrypt a vault, so it holds that vault's key. // // The wrapped secrets live in the server's state document; the key that // unwraps them does not. It comes from the environment or from a file // outside the data directory, so a copy of the data directory alone yields // ciphertext. Whoever holds both holds the vaults. type Keyring struct { aead cipher.AEAD } // MasterKeyEnv names the environment variable holding a base64 master key, // which takes precedence over the key file. const MasterKeyEnv = "KEEPORG_MASTER_KEY" // LoadKeyring reads the master key from the environment or from path, // creating one at path if neither exists. func LoadKeyring(path string) (*Keyring, error) { if v := strings.TrimSpace(os.Getenv(MasterKeyEnv)); v != "" { key, err := base64.StdEncoding.DecodeString(v) if err != nil { return nil, fmt.Errorf("secret: %s is not valid base64: %w", MasterKeyEnv, err) } return NewKeyring(key) } if path == "" { return nil, errors.New("secret: no master key: set " + MasterKeyEnv + " or configure master_key_file") } b, err := os.ReadFile(path) switch { case err == nil: key, derr := base64.StdEncoding.DecodeString(strings.TrimSpace(string(b))) if derr != nil { return nil, fmt.Errorf("secret: master key file %s is not valid base64: %w", path, derr) } return NewKeyring(key) case !os.IsNotExist(err): return nil, err } // First run: mint one. It is written with owner-only permissions and is // the one file an operator must back up separately from the data. key := randBytes(32) if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return nil, err } enc := base64.StdEncoding.EncodeToString(key) if err := os.WriteFile(path, []byte(enc+"\n"), 0o600); err != nil { return nil, fmt.Errorf("secret: write master key %s: %w", path, err) } return NewKeyring(key) } // NewKeyring builds a keyring from a 32-byte key. func NewKeyring(key []byte) (*Keyring, error) { if len(key) != 32 { return nil, fmt.Errorf("secret: master key must be 32 bytes, got %d", len(key)) } block, err := aes.NewCipher(key) if err != nil { return nil, err } aead, err := cipher.NewGCM(block) if err != nil { return nil, err } return &Keyring{aead: aead}, nil } // Wrap encrypts a vault secret. The vault's own id is authenticated with it, // so a wrapped secret moved to another vault's record fails to open rather // than unlocking the wrong database. func (k *Keyring) Wrap(vaultID, plaintext string) (string, error) { if vaultID == "" { return "", errors.New("secret: wrapping needs a vault id") } nonce := randBytes(k.aead.NonceSize()) out := k.aead.Seal(nonce, nonce, []byte(plaintext), []byte(vaultID)) return base64.StdEncoding.EncodeToString(out), nil } // Unwrap decrypts a vault secret wrapped for this vault. func (k *Keyring) Unwrap(vaultID, wrapped string) (string, error) { raw, err := base64.StdEncoding.DecodeString(wrapped) if err != nil { return "", fmt.Errorf("secret: wrapped value is not base64: %w", err) } n := k.aead.NonceSize() if len(raw) < n { return "", errors.New("secret: wrapped value is too short") } plain, err := k.aead.Open(nil, raw[:n], raw[n:], []byte(vaultID)) if err != nil { return "", errors.New("secret: wrapped value does not open with this master key") } return string(plain), nil }