AES-GCM加密前必须确认文件流支持io.Seeker,因其需预写认证标签(tag)且无法回溯不可seek流(如HTTP Body);推荐对不可seek流改用cipher.Stream模式或先缓存至内存/临时文件。

加密前必须确认文件流是否支持 io.Seeker
Go 的 crypto/aes 本身不处理流式加解密逻辑,真正关键的是你用的加密模式——比如 gcm 或 cbc,它们对输入流有不同要求。如果直接对 os.Stdin 或网络 http.Request.Body 这类不可回溯的流做 AES-GCM 加密,会失败,因为 GCM 需要先写认证标签(tag),而 tag 必须放在密文末尾或开头,但流式写入时无法预知长度,也无法倒回去填 tag。常见报错是 crypto/aes: invalid key size(其实是底层调用失败后错误掩盖)或更隐蔽的 write failed: bad message authentication code。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 对不可 seek 的流(如管道、HTTP body),优先选
crypto/cipher.Stream模式(如 CFB、OFB),它们可边读边加密,无需 tag 回写 - 若必须用 GCM(推荐场景),先将流完整读入内存或临时文件,确保能 seek 到开头再加密;或改用
io.Pipe+ goroutine 预读缓冲,但需自行管理 nonce 和 tag 位置 - 检查流类型:
_, ok := reader.(io.Seeker),不 ok 就别硬套cipher.NewGCM的标准封装
AES-GCM 加密时 nonce 不能复用且必须唯一
nonce 复用是 GCM 最致命的漏洞——相同 key + 相同 nonce 会导致密钥部分泄露,攻击者可能直接恢复明文。Go 标准库不会帮你校验 nonce 是否重复,它只信你给的值。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 永远用
crypto/rand.Read生成随机 nonce,长度严格为aead.NonceSize()(通常是 12 字节) - 不要用时间戳、计数器或文件名哈希当 nonce——除非你有强同步机制保证全局唯一
- 把 nonce 和密文一起存储:常见做法是前置 12 字节 nonce,再写密文+tag;读取时先读 nonce,再用它初始化 cipher
- 示例片段:
nonce := make([]byte, aesgcm.NonceSize())<br>if _, err := rand.Read(nonce); err != nil {<br> return err<br>}<br>ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)<br>// 写入:nonce + ciphertext
文件写入时避免明文残留和权限泄露
加密不是“把原文件读出来加密再覆盖”,而是生成新文件后原子替换——否则断电或 panic 会导致原文件被清空或只剩加密垃圾。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 永远写到临时路径(如
filepath.Join(filepath.Dir(src), "."+filepath.Base(src)+".tmp")),成功后再os.Rename - 设置新文件权限为
0600:os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0600),尤其在多用户系统上 - 加密完成后,显式覆写原文件(如果业务允许删除):
os.WriteFile(src, []byte{}, 0600),再os.Remove(src);但注意 SSD 可能无法真正覆写,敏感场景应依赖磁盘级擦除 - 别忽略
defer f.Close()—— 文件句柄泄漏会导致后续加密失败(too many open files)
解密失败时的错误处理不能只看 io.EOF
Go 的 aesgcm.Open 在 tag 验证失败时返回 cipher.AEADAuthError,但它常被上层 io.Read 包装成模糊的 EOF 或 invalid argument,导致误判为“文件读完了”。实际可能是密钥错、nonce 错、或文件被篡改。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 解密后立即验证返回的
error类型:if errors.Is(err, cipher.ErrInvalidTag) { ... } - 不要用
if err != nil && err != io.EOF来跳过错误——ErrInvalidTag是独立 error,不是 EOF 子类 - 日志中至少记录错误类型和密文前 8 字节(hex):
log.Printf("decrypt fail: %v, head=%x", err, ciphertext[:min(8, len(ciphertext))]) - 测试时故意改一个字节密文,确认你能捕获并区分
ErrInvalidTag和其他 I/O 错误


















