You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

75 lines
2.2 KiB

package comm
import (
"crypto/aes"
"crypto/cipher"
cryptorand "crypto/rand"
"encoding/base64"
"fmt"
"io"
)
// AES-256-GCM 字段加密:console 后台与业务服务共用的单一实现。
//
// 「应用的服务配置」(app_service_config) 由 console 前端写、业务服务启动时读/回写,两端读写同一张加密表,
// 故加密算法必须完全一致——统一收敛到本文件(console 的 svcEncrypt/svcDecrypt、业务侧 seed 均委托这里),
// 避免各处复制导致密文格式漂移。密钥由 ${FIELD_ENCRYPT_KEY} 注入,console 与业务服务必须配同一值。
// EncMask 加密字段返回前端时的脱敏占位符;更新时前端回传该占位/空串则保留原密文。
const EncMask = "[encrypted]"
// normAESKey 把任意长度的密钥规整为 32 字节(AES-256):不足补零、超长截断。
func normAESKey(key string) []byte {
k := make([]byte, 32)
copy(k, []byte(key))
return k
}
// Encrypt 用 AES-256-GCM 加密明文,返回 base64(nonce‖ciphertext)。空明文返回空串。
func Encrypt(key, plaintext string) (string, error) {
if plaintext == "" {
return "", nil
}
block, err := aes.NewCipher(normAESKey(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(cryptorand.Reader, nonce); err != nil {
return "", err
}
ct := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ct), nil
}
// Decrypt 解密 Encrypt 产出的密文,返回明文。
func Decrypt(key, ciphertext string) (string, error) {
if ciphertext == "" {
return "", nil
}
raw, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", fmt.Errorf("base64 解码失败: %w", err)
}
block, err := aes.NewCipher(normAESKey(key))
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
ns := gcm.NonceSize()
if len(raw) < ns {
return "", fmt.Errorf("密文过短")
}
plain, err := gcm.Open(nil, raw[:ns], raw[ns:], nil)
if err != nil {
return "", fmt.Errorf("GCM 解密失败: %w", err)
}
return string(plain), nil
}