不能直接对 openUploadStream() 流做 AES pipe 加密,否则解密必然失败;根本原因是 GridFS 的 chunk 切分机制与 AES 加密后的数据长度变化(padding、authTag)发生不可调和的错位。

不能直接对 openUploadStream() 流做 AES pipe 加密,否则解密必然失败。 这是绝大多数人踩坑的第一步——表面看代码跑通了,上传成功,但下载后解密报错、内容乱码或截断。根本原因在于 GridFS 的 chunk 切分机制与 AES 加密后的数据长度变化(padding、authTag)发生不可调和的错位。
为什么 fs.createReadStream().pipe(cipher).pipe(bucket.openUploadStream()) 必然出错
这种写法看似流式优雅,实则破坏 GridFS 最底层的数据对齐假设:
- GridFS 默认按
chunkSizeBytes: 255 * 1024(或自定义值)切块,每个 chunk 是独立可寻址的文档; - AES-CBC 需填充(padding),AES-GCM 附加 16 字节
authTag,导致密文长度 ≠ 明文长度; -
openUploadStream()是封装好的可写流,不暴露 chunk 边界,你无法控制“哪一段密文对应哪个 chunk”; - 下载时
openDownloadStream()按原始 chunk 序号顺序读取,但密文已错位 → IV 不匹配、authTag 找不到或错配 → 解密失败。
正确做法:用 openUploadStreamWithId() + 手动分块 + 块级加密
核心是绕过 GridFS 自动分块,自己掌控明文→密文→写入的全链路:
- 调用
bucket.openUploadStreamWithId(fileId, { chunkSizeBytes: 1024 * 1024 }),显式指定 chunk 大小,并确保后续每块加密输入严格等于该大小(最后一块除外); - 使用
crypto.createCipheriv('aes-256-gcm', key, iv),每块明文调用cipher.update(chunk)+cipher.final(); - GCM 模式下必须调用
cipher.getAuthTag(),并把 tag 追加到该块密文末尾(即写入的 chunk 内容 =encryptedChunk + authTag); - 在文件 metadata 中存:
{ encryption: "aes-256-gcm", iv: iv.toString('base64'), key_id: "k2026" }——iv必须每个文件唯一,绝不可复用; - CTR 模式可省去 tag 管理(长度不变),但需自行保障 nonce 唯一性,GCM 更推荐(带完整性校验)。
下载解密时必须按 chunk 重放加密逻辑
下载不是简单拼接密文再整体解密,而是逐块还原:
- 先从
fs.files读出metadata.encryption、metadata.iv、chunkSizeBytes; - 用相同
key和iv创建decipher; - 对每个 chunk 文档的
data字段,剥离最后 16 字节作为authTag,剩余部分传给decipher.update(); - 调用
decipher.setAuthTag(tag)后再decipher.final(),否则 GCM 解密会静默失败; - 注意:Python(PyCryptodome)中
AES.new(..., AES.MODE_GCM, nonce=iv)的nonce就是 Node.js 的iv,别混淆为key。
真正难的不是写加密逻辑,而是让每一块明文、密文、tag、chunk 文档四者严丝合缝地绑定。任何一处错位(比如 chunkSizeBytes 设为 255KB 但加密时按 256KB 分)、IV 复用、tag 丢弃或错位,都会导致整份文件不可恢复。生产环境务必用已知明文+固定 IV 做端到端加解密验证,别只测“能跑”。











