// Package secretbox 为落库机密字段提供信封加密(AES-256-GCM)。 // 主密钥来自环境变量 OPSLIC_MASTER_KEY(64 位 hex = 32 字节),启动时装载, // 缺失或非法即 fail-fast。密文带 enc:v1: 前缀;无前缀的值视为历史明文, // Decrypt 原样返回——支持存量数据一次性迁移渐进完成。 // Logan.Li package secretbox import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "encoding/hex" "errors" "fmt" "os" "strings" "sync" ) // Prefix 密文前缀;同时用于迁移时识别未加密的历史明文。 // Logan.Li const Prefix = "enc:v1:" var ( mu sync.RWMutex aed cipher.AEAD ) // LoadMasterKey 从 OPSLIC_MASTER_KEY 装载主密钥;缺失/非法返回错误(调用方应 fail-fast)。 // Logan.Li func LoadMasterKey() error { raw := strings.TrimSpace(os.Getenv("OPSLIC_MASTER_KEY")) if raw == "" { return errors.New("未配置 OPSLIC_MASTER_KEY(64 位 hex,32 字节):机密字段加密不可用,拒绝启动") } key, err := hex.DecodeString(raw) if err != nil || len(key) != 32 { return errors.New("OPSLIC_MASTER_KEY 必须是 64 位 hex(32 字节)") } block, err := aes.NewCipher(key) if err != nil { return fmt.Errorf("初始化 AES 失败: %w", err) } gcm, err := cipher.NewGCM(block) if err != nil { return fmt.Errorf("初始化 GCM 失败: %w", err) } mu.Lock() aed = gcm mu.Unlock() return nil } // Ready 报告主密钥是否已装载。 // Logan.Li func Ready() bool { mu.RLock() defer mu.RUnlock() return aed != nil } // Encrypt 加密明文:空串与已是密文的值原样返回(幂等,可安全重复调用)。 // Logan.Li func Encrypt(plain string) (string, error) { if plain == "" || strings.HasPrefix(plain, Prefix) { return plain, nil } mu.RLock() g := aed mu.RUnlock() if g == nil { return "", errors.New("secretbox: 主密钥未装载") } nonce := make([]byte, g.NonceSize()) if _, err := rand.Read(nonce); err != nil { return "", fmt.Errorf("生成 nonce 失败: %w", err) } sealed := g.Seal(nonce, nonce, []byte(plain), nil) return Prefix + base64.StdEncoding.EncodeToString(sealed), nil } // Decrypt 解密:无前缀(空值或历史明文)原样返回;密文被篡改时返回错误。 // Logan.Li func Decrypt(s string) (string, error) { if !strings.HasPrefix(s, Prefix) { return s, nil } mu.RLock() g := aed mu.RUnlock() if g == nil { return "", errors.New("secretbox: 主密钥未装载,无法解密机密字段") } raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(s, Prefix)) if err != nil { return "", fmt.Errorf("机密字段 base64 解码失败: %w", err) } if len(raw) < g.NonceSize() { return "", errors.New("机密字段密文长度非法") } plain, err := g.Open(nil, raw[:g.NonceSize()], raw[g.NonceSize():], nil) if err != nil { return "", fmt.Errorf("机密字段解密失败(主密钥不一致或密文被篡改): %w", err) } return string(plain), nil } // MustDecrypt 供钩子使用:解密失败直接返回错误,让读写显式失败而非静默拿错值。 // Logan.Li func MustDecrypt(s string) (string, error) { return Decrypt(s) }