密码存储的艺术
Posted on Sun 14 September 2025 in Journal
Abstract | 密码存储的艺术:从"123456"到"信封加密"的进化史 |
---|---|
Authors | Walter Fan |
Category | learning note |
Status | v1.0 |
Updated | 2025-09-14 |
License | CC-BY-NC-ND 4.0 |
目录
密码存储的艺术:从"123456"到"信封加密"的进化史
1. 引言:那些年我们犯过的错
还记得你第一次写代码时是怎么存储密码的吗?我猜大概是这样的:
// 新手程序员的最爱
password := "123456"
user.Password = password // 直接存储,简单粗暴
然后稍微"进步"一点:
// 自以为很聪明的做法
password := "123456"
hashed := md5(password) // MD5?这玩意儿早就被破解了
user.Password = hashed
再后来,你学会了bcrypt:
// 终于有点安全意识了
password := "123456"
hashed, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
user.Password = string(hashed)
但是,如果你要存储的是服务密钥(比如GitHub Token、数据库密码、API密钥),bcrypt就不够用了。因为这些密钥需要能够解密出来使用,而不是像用户密码那样只需要验证。
这时候,你就需要真正的加密,而不是哈希。
2. AES-GCM:不只是加密,更是"防伪"
2.1 什么是AES-GCM?
AES-GCM就像是给数据加了一把智能锁,它不仅能加密,还能检测数据是否被篡改。想象一下:
- AES:就像一把坚固的锁
- GCM:就像锁上的防伪标签,如果有人撬锁,标签就会坏掉
2.2 AES-GCM的三个"法宝"
当你用AES-GCM加密时,会得到三个东西:
- 密文(Ciphertext):加密后的数据,看起来像乱码
- 随机数(Nonce):12字节的随机值,让同样的明文每次加密结果都不同
- 认证标签(Tag):16字节的"防伪码",确保数据没被篡改
// 加密过程
plaintext := "我的秘密"
key := []byte("32字节的密钥...") // 256位密钥
// AES-GCM加密
ciphertext, nonce, tag := encryptAESGCM(key, plaintext)
// 存储到数据库
db.Save(EncryptedData{
Ciphertext: ciphertext,
Nonce: nonce,
Tag: tag,
})
解密时,三个东西缺一不可:
// 解密过程
plaintext, err := decryptAESGCM(key, ciphertext, nonce, tag)
if err != nil {
// 如果数据被篡改,这里会报错
return fmt.Errorf("数据可能被篡改: %w", err)
}
2.3 为什么需要Nonce?
Nonce就像是盐,让同样的密码每次加密结果都不同。没有Nonce的话:
// 危险!同样的明文总是产生同样的密文
password1 := "123456"
password2 := "123456"
// 两个密文完全一样,容易被攻击
有了Nonce:
// 安全!同样的明文每次加密都不同
password1 := "123456" + randomNonce1
password2 := "123456" + randomNonce2
// 两个密文完全不同
3. 信封加密:给密钥套个"信封"
3.1 什么是信封加密?
想象你要寄一封重要的信:
- 你把信放在信封里(用DEK加密数据)
- 你把信封的钥匙放在另一个保险箱里(用KEK加密DEK)
- 只有拥有保险箱钥匙的人才能拿到信封钥匙,进而打开信封
这就是信封加密(Envelope Encryption)的精髓。
3.2 信封加密的流程
// 第一步:生成数据加密密钥(DEK)
dek := generateRandomBytes(32) // 32字节 = 256位
// 第二步:用DEK加密你的秘密
secret := "GitHub Token: ghp_xxxxxxxxxxxx"
ciphertext, nonce, tag := encryptAESGCM(dek, secret)
// 第三步:用主密钥(KEK)加密DEK
kek := getMasterKey() // 从安全的地方获取主密钥
wrappedDEK, wrapNonce, wrapTag := encryptAESGCM(kek, dek)
// 第四步:存储所有加密数据
db.Save(EncryptedSecret{
// 加密后的秘密
Ciphertext: ciphertext,
Nonce: nonce,
Tag: tag,
// 加密后的DEK
WrappedDEK: wrappedDEK,
WrapNonce: wrapNonce,
WrapTag: wrapTag,
KekVersion: 1, // 记录使用的KEK版本
})
3.3 解密过程
// 解密过程
func decryptSecret(record EncryptedSecret, kek []byte) (string, error) {
// 第一步:用KEK解密DEK
dek, err := decryptAESGCM(kek, record.WrappedDEK, record.WrapNonce, record.WrapTag)
if err != nil {
return "", fmt.Errorf("无法解密DEK: %w", err)
}
// 第二步:用DEK解密秘密
secret, err := decryptAESGCM(dek, record.Ciphertext, record.Nonce, record.Tag)
if err != nil {
return "", fmt.Errorf("无法解密秘密: %w", err)
}
// 第三步:清除DEK(安全最佳实践)
for i := range dek {
dek[i] = 0
}
return string(secret), nil
}
4. 密钥轮换:让黑客永远追不上
4.1 为什么要轮换密钥?
想象一下,如果你的家门钥匙用了10年都没换,一旦钥匙被复制,你的家就永远不安全了。密钥也是一样:
- 减少爆炸半径:如果KEK泄露,只有部分数据受影响
- 合规要求:很多标准要求6-12个月轮换一次
- 加密敏捷性:可以随时升级算法或密钥长度
4.2 密钥轮换策略
// 版本化KEK管理
type KeyManager struct {
currentKEK []byte
kekVersion int
oldKEKs map[int][]byte // 存储旧版本KEK
}
// 轮换过程
func (km *KeyManager) RotateKeys() error {
// 1. 生成新的KEK
newKEK := generateRandomBytes(32)
newVersion := km.kekVersion + 1
// 2. 更新配置
km.currentKEK = newKEK
km.kekVersion = newVersion
// 3. 启动后台重加密任务
go km.rewrapAllSecrets()
return nil
}
// 后台重加密任务
func (km *KeyManager) rewrapAllSecrets() {
// 找到所有需要重加密的记录
records := db.FindOldRecords(km.kekVersion - 1)
for _, record := range records {
// 用旧KEK解密DEK
oldKEK := km.oldKEKs[record.KekVersion]
dek, err := decryptAESGCM(oldKEK, record.WrappedDEK, record.WrapNonce, record.WrapTag)
if err != nil {
log.Printf("解密失败: %v", err)
continue
}
// 用新KEK重新加密DEK
newWrappedDEK, newWrapNonce, newWrapTag := encryptAESGCM(km.currentKEK, dek)
// 更新数据库
record.WrappedDEK = newWrappedDEK
record.WrapNonce = newWrapNonce
record.WrapTag = newWrapTag
record.KekVersion = km.kekVersion
db.Update(record)
// 清除DEK
for i := range dek {
dek[i] = 0
}
}
}
5. 实战代码:从理论到实践
5.1 完整的数据结构
type EncryptedSecret struct {
// 加密后的秘密
Ciphertext []byte `json:"ciphertext"`
Nonce []byte `json:"nonce"`
Tag []byte `json:"tag"`
// 加密后的DEK
WrappedDEK []byte `json:"wrapped_dek"`
WrapNonce []byte `json:"wrap_nonce"`
WrapTag []byte `json:"wrap_tag"`
KekVersion int `json:"kek_version"`
}
5.2 加密函数
func EncryptSecret(secret string, kek []byte, kekVersion int) (*EncryptedSecret, error) {
// 第一步:生成DEK
dek := make([]byte, 32)
if _, err := rand.Read(dek); err != nil {
return nil, fmt.Errorf("生成DEK失败: %w", err)
}
// 第二步:用DEK加密秘密
ciphertext, nonce, tag, err := encryptAESGCM(dek, []byte(secret))
if err != nil {
return nil, fmt.Errorf("加密秘密失败: %w", err)
}
// 第三步:用KEK加密DEK
wrappedDEK, wrapNonce, wrapTag, err := encryptAESGCM(kek, dek)
if err != nil {
return nil, fmt.Errorf("加密DEK失败: %w", err)
}
// 清除DEK
for i := range dek {
dek[i] = 0
}
return &EncryptedSecret{
Ciphertext: ciphertext,
Nonce: nonce,
Tag: tag,
WrappedDEK: wrappedDEK,
WrapNonce: wrapNonce,
WrapTag: wrapTag,
KekVersion: kekVersion,
}, nil
}
5.3 解密函数
func DecryptSecret(record *EncryptedSecret, kek []byte) (string, error) {
// 第一步:用KEK解密DEK
dek, err := decryptAESGCM(kek, record.WrappedDEK, record.WrapNonce, record.WrapTag)
if err != nil {
return "", fmt.Errorf("解密DEK失败: %w", err)
}
// 第二步:用DEK解密秘密
plaintext, err := decryptAESGCM(dek, record.Ciphertext, record.Nonce, record.Tag)
if err != nil {
return "", fmt.Errorf("解密秘密失败: %w", err)
}
// 清除DEK
for i := range dek {
dek[i] = 0
}
return string(plaintext), nil
}
5.4 AES-GCM实现
func encryptAESGCM(key, plaintext []byte) ([]byte, []byte, []byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, nil, nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, nil, nil, err
}
// 生成随机nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, nil, nil, err
}
// 加密
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
return ciphertext, nonce, nil, nil
}
func decryptAESGCM(key, ciphertext, nonce, tag []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// 解密
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
6. 总结:安全存储的终极奥义
让我们快速总结一下:
- AES-GCM:不只是加密,还能防篡改
- 信封加密:用KEK保护DEK,用DEK保护数据
- 密钥轮换:让黑客永远追不上你的安全策略
- 实战代码:从理论到实践的完整实现
记住,安全存储密码不是一蹴而就的,而是一个持续的过程。就像你家的门锁一样,需要定期更换,需要多层防护,需要时刻保持警惕。
现在,你可以告别"123456"时代,拥抱真正的安全存储了!
参考资料: - AES-GCM 标准 - 信封加密最佳实践 - 密钥管理最佳实践
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。