
Go 里用 AES-GCM 加密文件元数据比加密文件内容更关键
文件内容加密了,但 os.Stat() 返回的 ModTime、Size、Name 这些信息如果明文存,攻击者照样能推断出文件类型、修改频率、甚至业务逻辑。元数据加密不是锦上添花,是防止侧信道泄露的第一道防线。
实操上别碰 AES-CBC 或裸 AES-ECB:前者需要自己管理 IV 和填充,后者完全不防重放;AES-GCM 是 Go 标准库唯一开箱即用、带认证的模式,必须用它。
-
AES-GCM要求密钥长度严格为16(AES-128)、24(AES-192)或32(AES-256)字节,少一个字节就 panic,别用md5.Sum直接截取 - IV(nonce)必须每次加密唯一,且不能重复——推荐用
crypto/rand.Read()生成 12 字节 nonce,写进加密后数据头部 - 不要把文件名哈希值当密钥,哈希不可逆但可爆破;密钥必须来自密码派生(如
scrypt.Key)或硬件安全模块
加密前先序列化元数据,别直接加密 os.FileInfo 结构体
os.FileInfo 是接口,底层实现随 OS 变(比如 Windows 的 syscall.Win32FileAttributeData),直接 gob.Encoder 或 json.Marshal 可能跨平台失效,且包含无关字段(如未导出的 sys 字段)。
只保留真正需要加密的字段:路径(相对)、大小、修改时间、权限(Mode().Perm()),封装成自定义结构体再加密。
立即学习“go语言免费学习笔记(深入)”;
- 示例结构体:
type EncryptedMeta struct { Path string `json:"path"` Size int64 `json:"size"` ModTime time.Time `json:"mod_time"` Perm os.FileMode `json:"perm"` } - 用
json.Marshal序列化,体积小、跨语言、无反射风险;避免gob(Go 专属、版本敏感) - 注意
time.Time默认序列化为 RFC3339 字符串,解密后需用time.Parse还原,别依赖本地时区
io.Copy 加密流式写入时,别在循环里反复调用 cipher.AEAD.Seal
常见错误:读一块数据 → 加密一块 → 写一块 → 再读下一块。这会导致每个块都生成独立的 GCM tag(16 字节),大幅膨胀体积,且破坏完整性验证范围——tag 只保护当前块,无法防中间块篡改。
正确做法是把整个元数据当作单个消息加密,而不是分块流式加密。GCM 本身支持大消息(GB 级别),性能无瓶颈。
- 完整元数据序列化后,一次性调用
aead.Seal(nil, nonce, data, additionalData) -
additionalData参数别留空:填入文件路径的 SHA256 前 16 字节,绑定元数据与路径,防挪用到其他文件 - 加密后数据格式建议:
[nonce(12)][ciphertext][tag(16)],固定头部便于解密时切片
解密失败时,crypto/aes 不报具体错误,只返回 EOF 或空切片
这是 Go 标准库的故意设计:避免通过错误类型泄露加密状态(比如区分“密钥错”和“tag 验证失败”)。但对调试极不友好,容易卡在“为什么解不出来”。
真实场景中,失败原因集中在三处:nonce 错位、密钥不匹配、additionalData 不一致。必须靠日志+断点定位,不能依赖错误提示。
- 解密前先校验输入长度:至少
12 + 16 = 28字节(nonce + tag),否则直接 return,不进Open - 用
hex.EncodeToString打印 nonce 和密钥前 8 字节做快速比对,别只看变量名 - 测试时用固定 nonce(如全 0)和固定密钥,确保加解密通路闭环;上线后再切回随机 nonce
元数据加密真正的复杂点不在算法调用,而在密钥生命周期管理和附加数据的设计。比如路径变更后,旧元数据要不要自动迁移?权限字段是否该脱敏(如只存 0644 而非完整 Mode())?这些决策比选 AES-GCM 重要得多。










