package secret import ( "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "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, from a secret file // an orchestrator provisioned, 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 key []byte } // Environment variables that supply the master key. Both are read before // any configured path. const ( // MasterKeyEnv holds the base64 key itself. MasterKeyEnv = "KEEPORG_MASTER_KEY" // MasterKeyFileEnv holds the path to a file containing it, which is how // a container runtime or an orchestrator hands over a provisioned // secret without putting it in the process environment. MasterKeyFileEnv = "KEEPORG_MASTER_KEY_FILE" ) // keySize is the master key length in bytes. const keySize = 32 // LoadKeyring finds the master key, in this order: // // 1. $KEEPORG_MASTER_KEY, the base64 key itself. // 2. $KEEPORG_MASTER_KEY_FILE, a path to a provisioned secret. It must // already exist: something else owns that file, so a missing one is a // misconfiguration to report, not a reason to mint a new key that would // fail to open any existing vault. // 3. path, from -master-key-file or master_key_file in the configuration. // This one is created on first run if it is absent, which is what makes // a plain package install work with no setup. // // A file may hold the key as base64 text, with or without a trailing // newline, or as exactly 32 raw bytes, because secret stores produce both. func LoadKeyring(path string) (*Keyring, error) { if v := strings.TrimSpace(os.Getenv(MasterKeyEnv)); v != "" { key, err := decodeKey([]byte(v)) if err != nil { return nil, fmt.Errorf("secret: $%s: %w", MasterKeyEnv, err) } return NewKeyring(key) } if p := strings.TrimSpace(os.Getenv(MasterKeyFileEnv)); p != "" { b, err := os.ReadFile(p) if err != nil { return nil, fmt.Errorf("secret: $%s points at %s: %w", MasterKeyFileEnv, p, err) } key, err := decodeKey(b) if err != nil { return nil, fmt.Errorf("secret: $%s file %s: %w", MasterKeyFileEnv, p, err) } return NewKeyring(key) } if path == "" { return nil, errors.New("secret: no master key: set $" + MasterKeyEnv + ", $" + MasterKeyFileEnv + ", or configure master_key_file") } b, err := os.ReadFile(path) switch { case err == nil: key, derr := decodeKey(b) if derr != nil { return nil, fmt.Errorf("secret: master key file %s: %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(keySize) 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) } // decodeKey accepts the shapes a provisioned secret arrives in: exactly 32 // raw bytes, or base64 text with or without padding and trailing newline. func decodeKey(b []byte) ([]byte, error) { if len(b) == keySize { return b, nil } text := strings.TrimSpace(string(b)) if key, err := base64.StdEncoding.DecodeString(text); err == nil { return key, nil } if key, err := base64.RawStdEncoding.DecodeString(text); err == nil { return key, nil } return nil, fmt.Errorf("expected %d raw bytes or a base64 key, got %d bytes "+ "that are neither", keySize, len(b)) } // NewKeyring builds a keyring from a 32-byte key. func NewKeyring(key []byte) (*Keyring, error) { if len(key) != keySize { return nil, fmt.Errorf("secret: master key must be %d bytes, got %d", keySize, 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, key: append([]byte(nil), key...)}, nil } // Derive returns a keyring for one purpose, keyed by HMAC-SHA256 of the // master key and the purpose's name. Each use of the master key gets its own // key this way: a ciphertext made for one purpose does not open under // another, and no subkey reveals the master key. func (k *Keyring) Derive(purpose string) *Keyring { m := hmac.New(sha256.New, k.key) m.Write([]byte("keeporg/" + purpose)) sub, err := NewKeyring(m.Sum(nil)) if err != nil { panic("secret: derive: " + err.Error()) // a SHA-256 sum is always 32 bytes } return sub } // Seal encrypts data with AES-256-GCM under a fresh nonce, authenticating // aad with it. The output is the nonce followed by the ciphertext. func (k *Keyring) Seal(aad, data []byte) []byte { nonce := randBytes(k.aead.NonceSize()) return k.aead.Seal(nonce, nonce, data, aad) } // ErrSealed means sealed data did not open: a different key, different // associated data, or tampering. var ErrSealed = errors.New("secret: sealed data does not open with this key") // Open decrypts what Seal produced with the same aad. func (k *Keyring) Open(aad, sealed []byte) ([]byte, error) { n := k.aead.NonceSize() if len(sealed) < n { return nil, ErrSealed } plain, err := k.aead.Open(nil, sealed[:n], sealed[n:], aad) if err != nil { return nil, ErrSealed } return plain, nil } // MAC is a keyed, deterministic digest of value, in hex. It names a row by a // value without storing the value: equal inputs give equal names, and the // name says nothing about the input to anyone without the key. func (k *Keyring) MAC(value string) string { m := hmac.New(sha256.New, k.key) m.Write([]byte(value)) return hex.EncodeToString(m.Sum(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 }