
存储手机号、证件号或处理支付回调时,常见做法是使用 AES 对敏感数据进行加解密。AES 属于对称加密:持有密钥方可还原明文。它不能替代 HTTPS/TLS 提供的传输保护,也不适合用于登录密码存储(密码应使用单向哈希)。本文说明 AES 的基本概念、模式选择,以及基于 Go 标准库的 AES-GCM 实现方式。
AES 与相近机制的边界
- AES(对称加密):加密与解密使用同一密钥。适用于数据库敏感字段加密、双方约定的报文加解密。
- HTTPS/TLS:保护客户端与服务器之间的传输通道。不宜在浏览器端自行实现 AES 并以此作为唯一安全措施。
- 密码哈希(bcrypt / argon2 等):用于登录口令的单向存储。AES 可逆,一旦密钥泄露即可还原全部口令,因此不可用于密码落库。
建议分工:静态敏感字段使用 AES;用户登录密码使用哈希;公网传输依赖 TLS。
AES 基本概念
AES(Advanced Encryption Standard)是 NIST 公布的对称分组密码标准,算法前身为 Rijndael,主流语言标准库均已提供实现。使用同一密钥将明文变换为密文,再用同一密钥恢复明文。
分组长度与密钥规格
AES 为分组密码(block cipher),每次处理固定 128 位(16 字节)数据块,并输出等长密文块。明文超过一块时按块处理;不足一块时,部分模式需要填充(padding)。GCM 等模式可按字节流处理,业务侧通常无需自行实现 PKCS#7 填充。
密钥长度仅允许以下三种:
| 名称 | 密钥长度 | 轮数 | 选用说明 |
|---|---|---|---|
| AES-128 | 16 字节 | 10 | 计算开销较低,一般业务场景可用 |
| AES-192 | 24 字节 | 12 | 实际使用较少,多见于对接方明确指定 |
| AES-256 | 32 字节 | 14 | 敏感数据落库的常用选择;部分合规要求会写明 AES-256 |
密钥应使用密码学安全随机数生成(Go 中为 crypto/rand),经 Base64 编码后存入环境变量或密钥管理服务(KMS)。不应使用可猜测字符串作为密钥;人类可记忆口令需先经密钥派生函数(如 Argon2、scrypt)处理,不可直接作为 AES 密钥。
工作模式
AES 算法本身只定义「16 字节明文 + 密钥 → 16 字节密文」。实际业务还需选择工作模式(mode of operation),以处理任意长度数据、引入 IV/Nonce,并决定是否具备完整性校验能力。
| 模式 | 特性 | 建议 |
|---|---|---|
| ECB | 相同明文块映射为相同密文块,易泄露数据模式 | 生产环境不应使用 |
| CBC | 需随机 IV;主要提供机密性,不提供完整性;需处理填充 | 常见于遗留系统;新建系统优先改用 GCM,或采用 CBC 并配合 HMAC |
| GCM | AEAD:同时提供加密与认证;使用随机 Nonce;密文被篡改时解密失败 | 新建系统的默认推荐 |
GCM(Galois/Counter Mode)在加密时生成认证标签(tag)并与密文关联。解密时若标签校验失败则返回错误,不会输出被篡改后的数据。相较仅使用 AES-CBC 且不做完整性校验的方案,GCM 可同时覆盖机密性与防篡改。
Nonce 与 IV
CBC 使用 IV,GCM 使用 Nonce,目的均为:在相同密钥与相同明文下,使两次加密结果不同,降低密文对比分析的风险。使用要求如下:
- 每次加密使用密码学安全随机数生成新的 IV/Nonce(GCM 常用 12 字节)。
- 与密文一并存储,常见格式为 Base64(
nonce ‖ ciphertext ‖ tag)。Nonce 通常可公开,保密对象是密钥。 - 在同一密钥下不得复用 Nonce。GCM 在 Nonce 复用时安全性会显著下降。
可选参数 AAD(Additional Authenticated Data)不参与加密,但参与认证。可将 user_id、记录主键等绑定到密文,防止密文在不同记录间被替换。Go 中 Seal/Open 的最后一个参数即为 AAD;无需使用时传入 nil。
推荐配置
- 密钥:AES-256(32 字节随机密钥),由环境变量或 KMS 提供。
- 模式:AES-GCM。
- Nonce:每次加密重新生成,并以明文前缀形式写入存储字符串。
Go 实现示例(AES-GCM)
以下示例仅依赖标准库。约定:密钥从环境变量读取(长度须为 16 或 32 字节);持久化字符串为 Base64(nonce ‖ ciphertext ‖ tag)。gcm.Seal 会将 tag 追加在密文之后;示例将 nonce 置于最前,解密时再分离。
package cryptoutil
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"io"
"os"
)
func LoadKey() ([]byte, error) {
key, err := base64.StdEncoding.DecodeString(os.Getenv("AES_KEY_B64"))
if err != nil {
return nil, err
}
if n := len(key); n != 16 && n != 32 {
return nil, errors.New("AES key must be 16 or 32 bytes")
}
return key, nil
}
func Encrypt(plain, key []byte) (string, error) {
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
}
// Seal 将认证标签追加在密文后;nonce 置于前部以便持久化
out := gcm.Seal(nonce, nonce, plain, nil)
return base64.StdEncoding.EncodeToString(out), nil
}
func Decrypt(encoded string, key []byte) ([]byte, error) {
raw, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
ns := gcm.NonceSize()
if len(raw) < ns {
return nil, errors.New("ciphertext too short")
}
nonce, ct := raw[:ns], raw[ns:]
return gcm.Open(nil, nonce, ct, nil)
}
生成 32 字节密钥示例:
b := make([]byte, 32)
_, _ = rand.Read(b)
fmt.Println(base64.StdEncoding.EncodeToString(b))
将输出配置到部署环境的 AES_KEY_B64。应用程序仅读取环境变量,密钥不得提交至版本库。
常见错误配置
- 使用 ECB:相同明文块产生相同密文块,数据模式易被观察。应改用 GCM。
- IV/Nonce 固定或复用:同一密钥下复用会削弱安全性。每次加密应通过
rand.Reader生成新值。 - 密钥写入代码或前端资源:出现在仓库、镜像或浏览器脚本中的密钥可视作已泄露。应使用环境变量或 KMS。
- 以 AES 存储登录密码:应使用 bcrypt、argon2 等单向哈希。AES 可逆,哈希不可逆。
- 仅在前端加密:浏览器侧密钥可被提取。传输层应依赖 HTTPS;敏感字段应在服务端加密后入库。
- 将加密等同于脱敏:持有密钥即可还原明文。日志与客服展示仍需按需掩码,密文不代表不可逆。
业务落地要点
- 存储:手机号、证件号等字段保存 GCM 密文。若需等值查询,可另建盲索引(如 HMAC),不宜为支持模糊查询而保存明文。
- 传输:对外 API 继续使用 HTTPS。仅在对接协议要求报文级加密时,再按约定使用 AES 或其他指定算法。若对方指定 AES-CBC,应严格遵循其 IV、填充约定,并确认是否另有签名或 MAC。
- 密钥轮换:密文旁记录
key_version;启用新密钥后,读取时按版本解密,写回时使用新版本加密,分批迁移数据。
实践上优先采用 AES-GCM、每次随机 Nonce、服务端保管密钥。在实现前应明确分组长度、工作模式与 Nonce 的作用;模式选择不当,再完善的封装也无法提供预期的安全属性。