package payment import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "io" "log" "os" "strings" "sync" ) // 支付配置敏感字段的对称加密实现(AES-256-GCM)。 // // 设计要点: // 1. 主密钥来自环境变量 PAYMENT_SECRET_KEY(32 字节 hex / base64,或任意口令经 SHA-256 派生); // 2. 未设置时退化为用 JWT_SECRET 做域分离派生,保证开箱可用,但轮换 JWT_SECRET 会失去解密能力; // 3. 密文格式 enc:v1:,前缀便于未来平滑升级算法。 // Logan.Li const ( // EnvPaymentMasterKey 加密主密钥环境变量名 // Logan.Li EnvPaymentMasterKey = "PAYMENT_SECRET_KEY" // cipherPrefix 密文前缀 // Logan.Li cipherPrefix = "enc:v1:" // cipherAAD 附加认证数据,避免密文被跨用途搬运 // Logan.Li cipherAAD = "opsx-licence/payment-config/v1" ) var ( masterKeyOnce sync.Once masterKeyBytes []byte masterKeyErr error masterKeyOrigin string ) // PaymentMasterKeyOrigin 返回主密钥来源(env / jwt_secret),供页面提示 // Logan.Li func PaymentMasterKeyOrigin() string { _, _ = masterKey() return masterKeyOrigin } // masterKey 惰性解析主密钥,仅解析一次 // Logan.Li func masterKey() ([]byte, error) { masterKeyOnce.Do(func() { if raw := strings.TrimSpace(os.Getenv(EnvPaymentMasterKey)); raw != "" { key, err := parseKeyMaterial(raw) if err != nil { masterKeyErr = err return } masterKeyBytes = key masterKeyOrigin = "env" return } if jwt := strings.TrimSpace(os.Getenv("JWT_SECRET")); jwt != "" { sum := sha256.Sum256([]byte(cipherAAD + "|jwt:" + jwt)) masterKeyBytes = sum[:] masterKeyOrigin = "jwt_secret" log.Printf("[支付配置] 未设置 %s,已用 JWT_SECRET 派生加密主密钥;生产环境建议显式设置独立主密钥", EnvPaymentMasterKey) return } masterKeyOrigin = "none" masterKeyErr = errors.New("未配置加密主密钥:请设置环境变量 " + EnvPaymentMasterKey + "(32 字节 hex/base64,或任意口令)") }) return masterKeyBytes, masterKeyErr } // parseKeyMaterial 把环境变量值解析成 32 字节密钥 // Logan.Li func parseKeyMaterial(raw string) ([]byte, error) { // 1) 64 位 hex if len(raw) == 64 { if b, err := hex.DecodeString(raw); err == nil && len(b) == 32 { return b, nil } } // 2) base64(标准 / URL / 无填充) for _, enc := range []*base64.Encoding{ base64.StdEncoding, base64.RawStdEncoding, base64.URLEncoding, base64.RawURLEncoding, } { if b, err := enc.DecodeString(raw); err == nil && len(b) == 32 { return b, nil } } // 3) 任意口令:SHA-256 派生 if len(raw) >= 8 { sum := sha256.Sum256([]byte(raw)) return sum[:], nil } return nil, errors.New("PAYMENT_SECRET_KEY 过短(至少 8 个字符,推荐 openssl rand -hex 32)") } // EncryptSecret 加密敏感字段;空串直接返回空串 // Logan.Li func EncryptSecret(plain string) (string, error) { if strings.TrimSpace(plain) == "" { return "", nil } key, err := masterKey() 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 } sealed := gcm.Seal(nonce, nonce, []byte(plain), []byte(cipherAAD)) return cipherPrefix + base64.RawURLEncoding.EncodeToString(sealed), nil } // DecryptSecret 解密敏感字段;无前缀视为历史明文直接返回 // Logan.Li func DecryptSecret(stored string) (string, error) { raw := strings.TrimSpace(stored) if raw == "" { return "", nil } if !strings.HasPrefix(raw, cipherPrefix) { return raw, nil } key, err := masterKey() if err != nil { return "", err } payload, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(raw, cipherPrefix)) if err != nil { return "", errors.New("密文格式错误") } block, err := aes.NewCipher(key) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } if len(payload) < gcm.NonceSize() { return "", errors.New("密文长度异常") } nonce, cipherText := payload[:gcm.NonceSize()], payload[gcm.NonceSize():] plain, err := gcm.Open(nil, nonce, cipherText, []byte(cipherAAD)) if err != nil { return "", errors.New("解密失败:加密主密钥(PAYMENT_SECRET_KEY)与保存时不一致") } return string(plain), nil } // MaskTail 脱敏展示:仅保留末尾 keep 位,其余用掩码填充到 total 位 // Logan.Li func MaskTail(secret string, keep, total int) string { s := strings.TrimSpace(secret) if s == "" { return "" } if total <= 0 { total = len(s) } if total < keep { total = keep } if len(s) <= keep { return strings.Repeat("•", total-len(s)) + s } return strings.Repeat("•", total-keep) + s[len(s)-keep:] } // FingerprintSHA256 计算内容指纹,用于「已配置但不回显」字段 // Logan.Li func FingerprintSHA256(data []byte) string { if len(data) == 0 { return "" } sum := sha256.Sum256(data) return hex.EncodeToString(sum[:]) } // ShortFingerprint 指纹取前 16 位做展示 // Logan.Li func ShortFingerprint(fp string) string { if len(fp) <= 16 { return fp } return fp[:16] } // NormalizePEM 把可能是裸 base64 的密钥补全成 PEM 块 // Logan.Li func NormalizePEM(raw, begin, end string) string { s := strings.TrimSpace(raw) if s == "" { return "" } // 已含 PEM 头则原样(统一换行) if strings.Contains(s, "-----BEGIN") { return strings.ReplaceAll(s, "\r\n", "\n") } body := strings.Join(strings.Fields(s), "") var b strings.Builder b.WriteString(begin) b.WriteString("\n") for i := 0; i < len(body); i += 64 { j := i + 64 if j > len(body) { j = len(body) } b.WriteString(body[i:j]) b.WriteString("\n") } b.WriteString(end) b.WriteString("\n") return b.String() }