|
背景
在公司的业务需要中,遇到一个场景,用户需要从我们的系统导出报表,因为报表涉及的时间维度比较长,会导致报表的体积比较大(50M)。
在技术方案上,报表的生成是后端同学负责异步生成的。报表文件生成完成后,会上传到腾讯公有云 COS(一个文件存储服务)上。出于安全的考虑,在上传到腾讯 COS 前,后端同学会对文件进行加密,加密算法我们选用了 aes-256-gcm 算法。
腾讯公有云支持服务器端加密,为什么不使用?因为我们安全同学认为原文件传输到腾讯 COS 上不是特别的好,希望我们自行加密后再上传。 整个流程如下

流式解码的难题
这里存在一个问题,一个大文件,从 COS 下载到 Node 服务是需要时间的。经过测试,50M 大小的文件下载需要 30秒时间(网络 IO 有关)。如果 Node 服务等待文件下载完成再解密,浏览器将会有 30秒的时间接收不到数据,这会导致浏览器认为服务器超时而主动断开连接。并且用户也不可能等待长达10多秒的时候,用户在得不到响应,大于5秒时,就可能认为这是一个 bug 而重复点击。
所以我们需要在用户点击下载时,立即给用户返回数据,这要求数据流从 COS 到 Node 时,Node 立即就开始进行解密工作就往前端传输数据了。
如何解决 GCM 流式解密的问题
GCM 协议加密后数组包的构成
首先我们需要了解 GCM 数据包的构成。GCM 数据包分三部分,初始化向量 iv、密文和校验码 MAC。

初始化向量 IV
初始化向量用于加密开始时使用的初始值,部分文章也会将 IV 称为 nonce。初始化向量是可以明文传输或者保存的。在这次需求中,我们保留了将 IV 添加到头部保存到 COS 上的做法。IV 长度是可以定制的。但是通常默认是 12btyes。
密文
密文是通过通过密钥 Key 和初始化向量 IV,以及原文加密得到的。密文的加密解密是可以支持流式的。即使只有一个 bytes 也可以进行加解密,在流式加解密中密文的长度是未知的,与原文有关。
校验码 MAC
也称为消息认证码,是用于校验数据完整性的,如果数据被串改过,就可以在对比校验码时发现。在 NodeJs 中 gcm 加解码协议实现里,authTag 就是校验码。MAC 的长度是可以定义的,通常默认是 16bytes。
<hr/>流式加密
数据加密比较简单,先将 iv 输出到流,再流式的加密原文,一边得到密文一边输出到流,当所有的原文都加密结束后,拿到 MAC,再将 MAC 输出到流。即可完成流式加密。
流式解密
数据解密比加密复杂一些,首先持续的获取流式数据,当数据的长度达到 iv 的长度时,将 iv 获取出来,就可以开始解密了。之后仍然要流式的获取到数据,之后除尾部 mac length 之外的数据取出,用于解密。当流完成时,再将最后的 mac length 长度的数据取出,用于检验,完成全部的流式解密过程。
解决过程
我在 github 和 npm 上查找了解决方案,但未能如愿找到好的库。一个我尝试找到的库是 aes-gcm-stream。但我在阅读原码的时候发现,这个库存在两个问题。
- 不支持 typescript,这个库最近的更新时间已经是 8 年前了,当时 typescript 并不火,所以作者并未支持。
- 这个库并不是真正的流式的,我在阅读了它的源码时发现,它只是对外暴露了流式的处理接口,但是真正处理的逻辑并不是流式的。它会一起收集上游的数据,直到上游输出结束后才开始解密处理。这不仅让解密的输出置后,还会因为保留所有的数据,而让内存占用达到一个 O(n) 级别,其中 n 表示密文的长度。
<hr/>为此我特地写了这个库,实现了流式加密和解密。
- github: cexoso/gcm-stream
- npm: gcm-stream
|
|