package store import ( "crypto/aes" "crypto/cipher" "crypto/pbkdf2" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "errors" "fmt" "log" "os" "path/filepath" "strconv" "strings" ) // Box encrypts the cluster passwords at rest (AES-256-GCM, random nonce, version byte). type Box struct{ aead cipher.AEAD } const boxVersion = 1 // NewBox builds a Box from a 32 byte key. func NewBox(key []byte) (*Box, error) { if len(key) != 32 { return nil, fmt.Errorf("encryption key: %d bytes, 32 expected", 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 &Box{aead}, nil } // Seal encrypts a secret: version | nonce | ciphertext+tag. func (b *Box) Seal(plain []byte) ([]byte, error) { nonce := make([]byte, b.aead.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, err } out := append([]byte{boxVersion}, nonce...) return b.aead.Seal(out, nonce, plain, []byte("pve-confsync token")), nil } // Open decrypts a secret sealed by Seal. An empty blob gives an empty secret. func (b *Box) Open(blob []byte) ([]byte, error) { if len(blob) == 0 { return nil, nil } n := b.aead.NonceSize() if blob[0] != boxVersion || len(blob) < 1+n { return nil, errors.New("unknown secret format") } plain, err := b.aead.Open(nil, blob[1:1+n], blob[1+n:], []byte("pve-confsync token")) if err != nil { return nil, errors.New("cannot decrypt (wrong encryption key)") } return plain, nil } // LoadKey returns the encryption key of the cluster passwords, in this order: // CONFSYNC_SECRET_KEY (64 hex characters or 44 base64 characters), // CONFSYNC_SECRET_KEY_FILE (same content, docker secret), or /secret.key, // created on first start. The last case keeps the key next to the database: fine for // a lab, a docker secret is better in production (a stolen /data then holds no // usable secret). func LoadKey(dataDir string) ([]byte, string, error) { if v := strings.TrimSpace(os.Getenv("CONFSYNC_SECRET_KEY")); v != "" { k, err := parseKey(v) return k, "CONFSYNC_SECRET_KEY", err } if f := os.Getenv("CONFSYNC_SECRET_KEY_FILE"); f != "" { raw, err := os.ReadFile(f) if err != nil { return nil, "", fmt.Errorf("CONFSYNC_SECRET_KEY_FILE: %w", err) } k, err := parseKey(strings.TrimSpace(string(raw))) return k, f, err } path := filepath.Join(dataDir, "secret.key") if raw, err := os.ReadFile(path); err == nil { k, err := parseKey(strings.TrimSpace(string(raw))) return k, path, err } k := make([]byte, 32) if _, err := rand.Read(k); err != nil { return nil, "", err } if err := os.WriteFile(path, []byte(hex.EncodeToString(k)+"\n"), 0o600); err != nil { return nil, "", fmt.Errorf("cannot save the encryption key: %w", err) } log.Printf("encryption key created in %s: keep a copy, the cluster passwords cannot be read without it", path) return k, path, nil } func parseKey(v string) ([]byte, error) { if k, err := hex.DecodeString(v); err == nil && len(k) == 32 { return k, nil } if k, err := base64.StdEncoding.DecodeString(v); err == nil && len(k) == 32 { return k, nil } return nil, errors.New("encryption key: 32 bytes expected, as 64 hex or 44 base64 characters (openssl rand -hex 32)") } // ---- passwords ---- // PBKDF2-HMAC-SHA256, 600 000 iterations (OWASP 2023 recommendation); the // standard library only, crypto/pbkdf2 exists since Go 1.24. const pbkdf2Iter = 600000 // HashPassword returns "pbkdf2-sha256$$$". func HashPassword(pass string) (string, error) { salt := make([]byte, 16) if _, err := rand.Read(salt); err != nil { return "", err } h, err := pbkdf2.Key(sha256.New, pass, salt, pbkdf2Iter, 32) if err != nil { return "", err } enc := base64.RawStdEncoding return fmt.Sprintf("pbkdf2-sha256$%d$%s$%s", pbkdf2Iter, enc.EncodeToString(salt), enc.EncodeToString(h)), nil } // CheckPassword compares in constant time. func CheckPassword(hash, pass string) bool { p := strings.Split(hash, "$") if len(p) != 4 || p[0] != "pbkdf2-sha256" { return false } iter, err := strconv.Atoi(p[1]) if err != nil || iter < 1 { return false } enc := base64.RawStdEncoding salt, err1 := enc.DecodeString(p[2]) want, err2 := enc.DecodeString(p[3]) if err1 != nil || err2 != nil { return false } got, err := pbkdf2.Key(sha256.New, pass, salt, iter, len(want)) return err == nil && subtle.ConstantTimeCompare(got, want) == 1 }