不能用io.Copy直接套cipher.StreamWriter,因为cipher.Stream仅提供机密性、不校验完整性,且io.Copy不保证块对齐,易导致末尾填充错误或静默篡改;必须用cipher.AEAD分块加密,每块生成唯一nonce并校验auth tag。

流式加密不是“把文件读完再加密”,而是边读边加密、内存恒定——否则大文件直接 OOM。核心是用 cipher.AEAD 分块处理,而非 cipher.Stream 或一次性 Seal。
为什么不能用 io.Copy 直接套 cipher.StreamWriter
因为 cipher.Stream(如 CFB、OFB)只提供机密性,不校验完整性;攻击者翻转密文任意比特,解密后得到乱码但不报错,属于静默损坏。而 io.Copy 也不保证每次读写对齐 AES 块边界(16 字节),最后一块填充易错乱,解密后 pkcs7.Unpad 会 panic 或返回错误数据。
-
cipher.StreamWriter是纯异或流,无认证标签(auth tag),无法检测篡改 - 即使加了 HMAC,手写 CBC+HMAC 容易顺序错(先解密再验证 → 篡改已生效)
-
io.ReadFull和io.Write的字节数不等于缓冲区长度时,cipher.Stream不自动重试或对齐
必须用 cipher.AEAD 分块加密,且每块独立 nonce
cipher.AEAD(如 cipher.NewGCM 或 chacha20poly1305.NewX)强制输出密文 + auth tag,并在解密时自动校验。但它的 Seal 方法不支持无限流,所以得手动分块:每块生成唯一 nonce,加密后拼接 nonce + ciphertext + tag。
- 块大小建议设为
64 * 1024(64KB):太小增加 nonce 开销,太大则内存压力上升 - nonce 必须每次调用
crypto/rand.Read生成,长度严格等于aead.NonceSize()(GCM 通常为 12 字节) - 不要用递增计数器当 nonce——GCM 不接受非随机 nonce;ChaCha20-Poly1305 更宽容,但仍推荐随机
- 每块的
additionalData可填入该块哈希(如hmac.Sum(nil)),防重放或块移位
加密前必须预检 io.Reader 是否可读
Go 的 io.Reader 接口不暴露关闭状态,传入已关闭的 *os.File 或耗尽的 bytes.Reader,Read 可能返回 0, io.EOF 或 io.ErrUnexpectedEOF,导致空密文或截断。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
立即学习“go语言免费学习笔记(深入)”;
- 对
*os.File:调用file.Stat(),若返回os.PathError(如 "bad file descriptor")说明已关闭 - 对通用
io.Reader:用io.ReadFull(reader, make([]byte, 1))尝试读 1 字节,捕获错误;若返回io.EOF且 n == 0,说明流为空或已耗尽 - 切勿在加密函数内反复
Seek(0, io.SeekStart)——不是所有 Reader 支持 Seek(如http.Response.Body)
密钥和 salt/nonce 的存储结构必须协议化
密文不是裸数据,而是带元信息的二进制 blob。解密端靠固定偏移读取 salt、nonce、密文段,错一位就全盘失败。
- 推荐头部结构:
[salt(16B)][nonce(12B)][ciphertext+tag]—— salt 用于 PBKDF2 派生密钥,nonce 用于本次 AEAD 加密 - salt 和 nonce 都必须每次加密新生成,不能复用;写入顺序必须严格,读取时按硬偏移切片(如
buf[0:16]、buf[16:28]) - 别用
os.Create(dst)覆盖原文件;应写临时文件(dst + ".tmp"),成功后os.Rename() - 密钥派生必须用
pbkdf2.Key或scrypt.Key,不能用sha256.Sum256等快速哈希——抗暴力能力差
最易被跳过的点是:分块时每块的 additionalData 是否包含上下文(如块序号或前一块 hash),以及解密端是否严格校验每个 Open 调用的 error 而非只看返回长度——cipher.ErrDecrypt 和 I/O 错误必须区分处理。

















