必须先读16字节IV,再读全部剩余数据作为密文+tag,用aesgcm.Open解密时需传入正确长度的dst切片,认证失败会返回cipher.ErrAuthFailed而非panic。
Go 的 crypto/aes 和 crypto/cipher 不提供开箱即用的“文件加密函数”,所有安全实现都依赖你自己组合底层原语。这意味着:密钥管理、IV 生成、填充方式、认证机制(如 GCM)必须手动处理,漏掉任一环节都可能让加密形同虚设。
crypto/rand.Read 生成新密钥(或派生自密码的密钥)aes.NewGCM,否则攻击者可翻转密文导致解密出可控明文直接读整个文件进内存再加密会爆内存,必须流式处理。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 当作密文一部分传给 aesgcm.Open、没预留足够空间读取 tag、忽略 aesgcm.Open 返回的 error(它会在认证失败时返回 cipher.ErrAuthFailed 而不是 panic)。
aesgcm.Open 的第一个参数是 dst 切片,必须至少和密文等长(它会把解密结果 copy 进去)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
}
用户输入的密码通常太短、熵太低,不能直接当 AES 密钥用。必须用 crypto/rand 生成 salt,并通过 crypto/pbkdf2 派生出 32 字节密钥(AES-256)。
这意味着加密文件结构变成:salt(16) + iv(16) + ciphertext + tag(16),解密时先读 salt,再用它和用户密码重新派生密钥。