必须先读16字节IV,再读全部剩余数据作为密文+tag,用aesgcm.Open解密时需传入正确长度的dst切片,认证失败会返回cipher.ErrAuthFailed而非panic。

加密前必须确认的三个前提
Go 的 crypto/aes 和 crypto/cipher 不提供开箱即用的“文件加密函数”,所有安全实现都依赖你自己组合底层原语。这意味着:密钥管理、IV 生成、填充方式、认证机制(如 GCM)必须手动处理,漏掉任一环节都可能让加密形同虚设。
- 密钥不能硬编码或复用——每次加密应使用
crypto/rand.Read生成新密钥(或派生自密码的密钥) - IV(初始化向量)必须随机且唯一,长度需匹配块大小(AES 是 16 字节),且必须随密文一起保存(但无需保密)
- 单纯 CBC 模式不防篡改——若需完整性校验,必须用
aes.NewGCM,否则攻击者可翻转密文导致解密出可控明文
用 AES-GCM 安全加密大文件的正确姿势
直接读整个文件进内存再加密会爆内存,必须流式处理。GCM 模式下无法像 CBC 那样分块加密后拼接——GCM 的认证标签(tag)只能在全部加密完成后生成,所以得先写密文,最后追加 tag;解密时则需先读完整密文+tag,再一次性验证解密。
实际做法是:把 IV(16 字节)+ 密文 + tag(16 字节)按固定顺序写入文件。解密时先读前 16 字节为 IV,剩余末尾 16 字节为 tag,中间为密文。
func encryptFile(src, dst string, key []byte) error {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
block, _ := aes.NewCipher(key)
aesgcm, _ := cipher.NewGCM(block)
nonce := make([]byte, aesgcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return err
}
// 先写 IV
if _, err := out.Write(nonce); err != nil {
return err
}
// 加密并写入密文(流式)
writer := aesgcm.Seal(nonce[:0], nonce, nil, nil)
// 注意:writer 是一个切片,后续 Write 会追加到它后面
if _, err := io.Copy(writer, f); err != nil {
return err
}
// writer 现在包含:nonce(已覆盖)+ 密文 + tag
// 但我们只写了 nonce(16字节),密文+tag 还在 writer 底层 buffer 中
// 所以要跳过前 aesgcm.NonceSize() 字节,写剩余部分
if _, err := out.Write(writer[aesgcm.NonceSize():]); err != nil {
return err
}
return nil
}
解密时最容易踩的坑:IV 长度、tag 位置、错误处理
常见错误包括:把 IV 当作密文一部分传给 aesgcm.Open、没预留足够空间读取 tag、忽略 aesgcm.Open 返回的 error(它会在认证失败时返回 cipher.ErrAuthFailed 而不是 panic)。
立即学习“go语言免费学习笔记(深入)”;
-
aesgcm.Open的第一个参数是 dst 切片,必须至少和密文等长(它会把解密结果 copy 进去) - IV 必须和加密时完全一致(长度、内容),且不能从密文中间任意截取
- 如果密文被截断或 tag 损坏,
aesgcm.Open会返回cipher.ErrAuthFailed,此时绝不能继续使用解密结果
func decryptFile(src, dst string, key []byte) error {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
// 读 IV(前 16 字节)
nonce := make([]byte, 16)
if _, err := io.ReadFull(f, nonce); err != nil {
return err
}
// 读剩余全部内容(密文 + tag)
all, err := io.ReadAll(f)
if err != nil {
return err
}
if len(all) < 16 {
return errors.New("ciphertext too short")
}
ciphertext := all[:len(all)-16]
tag := all[len(all)-16:]
block, _ := aes.NewCipher(key)
aesgcm, _ := cipher.NewGCM(block)
plaintext, err := aesgcm.Open(nil, nonce, append(ciphertext, tag...), nil)
if err != nil {
return err // 注意:这里 err 可能是 cipher.ErrAuthFailed
}
_, err = out.Write(plaintext)
return err
}
密码派生密钥(PBKDF2)比直接用字符串更安全
用户输入的密码通常太短、熵太低,不能直接当 AES 密钥用。必须用 crypto/rand 生成 salt,并通过 crypto/pbkdf2 派生出 32 字节密钥(AES-256)。
- salt 必须随机且唯一,长度建议 ≥ 16 字节,和 IV 一样需随密文保存
- 迭代次数建议 ≥ 100000(Go 1.19+ 默认是 100000,旧版本需显式指定)
- 派生出的密钥长度必须匹配 AES 模式(256 位 → 32 字节)
这意味着加密文件结构变成:salt(16) + iv(16) + ciphertext + tag(16),解密时先读 salt,再用它和用户密码重新派生密钥。










