密码存储的艺术

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加密时,会得到三个东西:

  1. 密文(Ciphertext):加密后的数据,看起来像乱码
  2. 随机数(Nonce):12字节的随机值,让同样的明文每次加密结果都不同
  3. 认证标签(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 什么是信封加密?

想象你要寄一封重要的信:

  1. 你把信放在信封里(用DEK加密数据)
  2. 你把信封的钥匙放在另一个保险箱里(用KEK加密DEK)
  3. 只有拥有保险箱钥匙的人才能拿到信封钥匙,进而打开信封

这就是信封加密(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. 总结:安全存储的终极奥义

让我们快速总结一下:

  1. AES-GCM:不只是加密,还能防篡改
  2. 信封加密:用KEK保护DEK,用DEK保护数据
  3. 密钥轮换:让黑客永远追不上你的安全策略
  4. 实战代码:从理论到实践的完整实现

记住,安全存储密码不是一蹴而就的,而是一个持续的过程。就像你家的门锁一样,需要定期更换,需要多层防护,需要时刻保持警惕。

现在,你可以告别"123456"时代,拥抱真正的安全存储了!


参考资料: - AES-GCM 标准 - 信封加密最佳实践 - 密钥管理最佳实践


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。