内容提要
本文介绍如何用 TypeScript、WebRTC DataChannel 和 WebCrypto 构建浏览器内 P2P 加密文件流式传输引擎。朴素实现会无上限写入 SCTP 缓冲区,约 100 MB 即崩溃。方案采用 64 KB 分块、AES-GCM 逐块加密、SHA-256 完整性校验,并通过 bufferedAmountLowThreshold 实现背压流控,使内存保持有界。内容还涵盖信令、密钥交换、重组下载及生产加固建议。
延伸解读
为什么朴素实现会在约 100 MB 崩溃
文章指出,常见做法是在紧密循环中直接调用 dataChannel.send(),把数据无上限地灌入 SCTP 发送缓冲区。小文件尚可,但当文件超过可用系统内存余量(通常约 100 MB 及以上)时,bufferedAmount 会不受控攀升,内存压力可能导致标签页崩溃。这解释了为什么必须引入背压流控,而不是单纯依赖 DataChannel 本身。
64 KB 分块与 256 KB 阈值的取舍
文章选择 64 KB 块,是因为 RFC 8831 将 64 KB 视为 SCTP 消息大小的互操作性安全下限,且更细粒度便于背压响应,AES-GCM 每块仅增加 16 字节认证标签。阈值设为 256 KB,配合 64 KB 块约允许四个块在途。阈值过低会频繁暂停、饿死网络;过高则内存占用回升。高带宽局域网可翻倍至 128 KB 块与 512 KB 阈值,但再往上瓶颈会转为 SCTP 拥塞控制。
IV 唯一性与密钥轮换的硬性要求
AES-GCM 要求同一密钥下每次加密使用唯一的 12 字节 IV,复用 IV 会导致明文可被恢复。文章用 4 字节大端块索引加 8 个随机字节拼接成 IV,将生日界推到每密钥约 2^32 个块(按 64 KB 算约 274 TB),并明确要求每次传输后轮换密钥。此外,原始密钥经 DataChannel 传输只能防被动窃听,被攻陷的信令服务器仍可替换密钥,生产环境应改用 ECDH 密钥协商。
当前实现的内存与信令限制
文章坦承两处局限:接收端会把所有解密后的块缓存在内存中,超出可用内存的文件需改用 File System Access API 或 StreamSaver.js;清单方式会把文件读两遍,内存受限时可考虑传输中计算摘要、传输后校验。信令方面,BroadcastChannel 仅限同源同一浏览器,只能用于本地开发,生产需替换为 WebSocket 或 SSE,并为对称 NAT 后的对等方配置 TURN 回退。
Q&A
为什么朴素的 WebRTC 文件传输在传输大文件时会崩溃?
朴素实现会在紧密循环中调用 dataChannel.send(),把无上限的数据倾倒进 SCTP 发送缓冲区。对于超出可用系统内存余量的文件(通常约 100 MB 及以上),RTCDataChannel.bufferedAmount 会不受控制地攀升,内存压力可能导致标签页崩溃。
如何用 bufferedAmountLowThreshold 实现背压流控?
设置 dataChannel.bufferedAmountLowThreshold = 256 * 1024(256 KB),当 bufferedAmount 超过阈值时暂停发送,等待 bufferedamountlow 事件触发后恢复。配合 64 KB 的块,这大约允许任意时刻有四个块在途。
为什么选择 64 KB 作为分块大小?
按 RFC 8831,SCTP 消息大小的互操作性安全下限是 64 KB;浏览器会协商出更大的值,但 64 KB 块是安全且可移植的默认选择。更小的 64 KB 块能提供更细粒度的背压响应,而 AES-GCM 每 64 KB 块只增加 16 字节(0.02%)的认证标签开销。它们还能降低峰值内存占用,因为任意时刻在途的字节更少。
如何为每个块生成唯一的 AES-GCM IV?
AES-GCM 要求在同一密钥下的每一次加密操作都使用唯一的 12 字节初始化向量(IV)。这里的策略是把 4 字节大端块索引与 8 个随机字节拼接起来,降低跨传输的碰撞概率;有了 8 个随机字节,生日界约为每密钥 2^32 个块(按 64 KB 块算约 274 TB)。每次传输后请轮换密钥。IV 会被前置到密文上,以便接收端在解密前取出。
接收端如何校验每个块的完整性?
接收端用 crypto.subtle.digest('SHA-256', decryptedChunk) 计算每个块的 SHA-256 摘要。在传输开始之前,发送端会计算一份清单,其中包含每个明文块的十六进制 SHA-256 摘要,并作为控制消息传输出去。接收端把每个解密后的块与清单中对应条目比对,一旦不匹配立即中止。
生产环境需要做哪些加固?
把 BroadcastChannel 信令替换为基于 WebSocket 或 SSE 的信令服务器。为处于对称 NAT 之后的对等方配置 TURN 服务器回退,否则直连会失败。使用临时 ECDH(ECDHE):每次会话用 crypto.subtle.generateKey 生成一对全新的 P-256 密钥。静态 ECDH 不提供前向保密,只有临时密钥对才有。若要在接收端实现真正流式下载而不完整缓冲 Blob,在 Chromium 浏览器中优先使用 File System Access API(showSaveFilePicker)。StreamSaver.js 是一个兼容性更广的替代方案,但已不再活跃维护。设置 CSP 头以允许用于触发下载的 blob: URL。