// Package crypto 提供可复用的安全原语:AES-256-GCM 字段加解密、argon2id 口令哈希。 // 密钥经 HKDF 从 config.EncryptionKey 派生(与 JWT 密钥分离)。 package crypto import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "errors" "io" "strings" "opsxloki/config" "golang.org/x/crypto/argon2" "golang.org/x/crypto/hkdf" ) const ( keySaltPrefix = "lokideploy-v2-enc:" // HKDF info prefix for key derivation ) // deriveEncryptionKey 使用HKDF从ENCRYPTION_KEY派生AES-256密钥 // 这样即使ENCRYPTION_KEY被复用,不同上下文的密钥也不同 func deriveEncryptionKey() ([]byte, error) { cfg := config.GetConfig() encKey := cfg.EncryptionKey // 支持hex或base64编码的密钥 var masterKey []byte var err error if len(encKey) == 64 { // 可能是hex编码的32字节 masterKey, err = hex.DecodeString(encKey) if err != nil { masterKey = []byte(encKey) // fallback: 直接使用原始字节 } } else if len(encKey) > 32 { // 可能是base64编码 masterKey, err = base64.StdEncoding.DecodeString(encKey) if err != nil { masterKey = []byte(encKey) // fallback: 直接使用原始字节 } } else { masterKey = []byte(encKey) } // 使用HKDF-SHA256派生固定长度的AES密钥 hkdfReader := hkdf.New(sha256.New, masterKey, nil, []byte(keySaltPrefix+"aes-gcm-256")) aesKey := make([]byte, 32) if _, err := io.ReadFull(hkdfReader, aesKey); err != nil { return nil, err } return aesKey, nil } func HashPassword(password string) (string, error) { salt := make([]byte, 16) if _, err := rand.Read(salt); err != nil { return "", err } hash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, 32) combined := append(salt, hash...) return base64.StdEncoding.EncodeToString(combined), nil } func VerifyPassword(password, hashStr string) bool { combined, err := base64.StdEncoding.DecodeString(hashStr) if err != nil || len(combined) < 16 { return false } salt := combined[:16] hash := combined[16:] newHash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, 32) return subtle.ConstantTimeCompare(hash, newHash) == 1 } func Encrypt(plaintext string) (string, error) { if plaintext == "" { return "", nil } key, err := deriveEncryptionKey() if err != nil { return "", err } block, err := aes.NewCipher(key) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err = io.ReadFull(rand.Reader, nonce); err != nil { return "", err } ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil) return base64.StdEncoding.EncodeToString(ciphertext), nil } func Decrypt(ciphertext string) (string, error) { if ciphertext == "" { return "", nil } key, err := deriveEncryptionKey() if err != nil { return "", err } data, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { return "", err } block, err := aes.NewCipher(key) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } if len(data) < gcm.NonceSize() { return "", errors.New("ciphertext too short") } nonce, ciphertextBytes := data[:gcm.NonceSize()], data[gcm.NonceSize():] plaintext, err := gcm.Open(nil, nonce, ciphertextBytes, nil) if err != nil { return "", err } return string(plaintext), nil } // EncryptField 加密敏感字段,返回 ENC: 前缀标记格式。 // 加密失败时返回错误(绝不返回明文或哨兵串),调用方必须处理该错误, // 否则可能把垃圾值当凭证落库。 func EncryptField(plaintext string) (string, error) { if plaintext == "" { return "", nil } enc, err := Encrypt(plaintext) if err != nil { return "", err } return "ENC:" + enc, nil } // DecryptField 解密 ENC: 前缀标记的字段 func DecryptField(encrypted string) string { if encrypted == "" { return "" } if strings.HasPrefix(encrypted, "ENC:") { plain, err := Decrypt(encrypted[4:]) if err != nil { return "" } return plain } // 未加密的数据直接返回(向后兼容) return encrypted } // IsEncryptedField 检查字段是否已加密 func IsEncryptedField(value string) bool { return strings.HasPrefix(value, "ENC:") }