不能将文件流直接 pipe 给 aes 再 pipe 给 openuploadstream,因为加密后数据长度变化(如 cbc 填充、gcm 的 authtag)会破坏 gridfs 默认 chunk 对齐,导致 downloadstream 读取错位、iv/tag 错配而解密失败。

GridFS 本身不加密,必须在应用层完成 AES 加密后再写入;直接对 openUploadStream() 流做 pipe 加密会破坏 chunk 对齐,导致后续解密失败。
为什么不能把文件流 pipe 给 AES 再 pipe 给 uploadStream
常见错误是这样写:
fs.createReadStream('sensitive.pdf')
.pipe(cipher) // AES 加密流
.pipe(bucket.openUploadStream('sensitive.pdf'))
这会导致三个问题:
- 加密后数据长度变化(尤其 CBC 模式需 padding),而 GridFS 默认按
chunkSizeBytes: 255 * 1024切块,密文长度错位 →downloadStream读出的 chunk 不再是完整加密块,解密时 IV/Tag 错配 -
openUploadStream()是封装好的可写流,不暴露 chunk 边界,你无法控制“哪一段明文对应哪一块密文” - GCM 模式生成的
authTag必须和对应密文块绑定存储,但自动 pipe 无法插入 tag,只能丢弃或错位
Node.js 中正确实现流式 AES-GCM 加密上传
核心是绕过自动分块,改用 openUploadStreamWithId() + 手动分块 + 块级加密。关键点:
- 用
crypto.createCipheriv('aes-256-gcm', key, iv)创建加密器,每次加密一块时调用cipher.setAAD()(可选)并保存cipher.getAuthTag() - 分块大小必须和
chunkSizeBytes一致(如1024 * 1024),且使用 CTR 或 GCM 模式——CTR 保持长度不变,GCM 需把authTag追加到密文块尾(总长 = 密文 + 16) - 调用
bucket.openUploadStreamWithId(fileId, { chunkSizeBytes: 1024 * 1024 }),确保驱动不重分块 - 每块明文调用
cipher.update(chunk)+cipher.final(),拼上authTag后写入uploadStream.write(encryptedChunkWithTag) - 在
metadata中存:{ encryption: "aes-256-gcm", iv: iv.toString('base64'), key_id: "k2026" }—— 注意iv必须是每个文件唯一,不能复用
Python(PyMongo + pycryptodome)中上传加密文件的要点
不要用 gridfs.GridFS(self.db).put() 直接传原始 bytes,它不给你插加密逻辑的机会。正确路径:
- 用
Random.new().read(AES.block_size)生成随机iv,用os.urandom(32)生成key(别硬编码!) - 读文件时用
with open(path, 'rb') as f:+f.read(1024*1024)分块,每块调用AES.new(key, AES.MODE_GCM, iv)加密,获取ciphertext和tag - 拼接密文块与 tag:
encrypted_chunk = ciphertext + tag,确保总长 ≤chunkSizeBytes(GCM tag 固定 16 字节,所以明文块要预留空间) - 调用
fs.upload_from_stream(filename, io.BytesIO(encrypted_chunk), metadata={...})之前,先创建GridFSBucket并显式设chunk_size_bytes=1024*1024 -
metadata中必须存iv(Binary(iv))、encryption、key_id,别存key或password
最容易被忽略的是:GCM 模式下,每个文件的 iv 必须唯一且不可预测,且 authTag 必须和密文块一一对应存储;如果上传时用了固定 iv 或丢了 tag,下载后解密必然失败,而且错误表现常是“decryption failed”或“authentication tag mismatch”,不会提示缺参数。











