27、WebRTC 安全:端到端加密(E2EE)、插入流(Insertable Streams)与自定义加密、安全最佳实践
聊到WebRTC安全,很多人第一反应就是“不是有DTLS-SRTP吗?”。没错,DTLS-SRTP确实给媒体流提供了传输层的加密。但这里有个关键问题——加密的钥匙在谁手里?
在标准架构里,信令服务器或者SFU(选择性转发单元)是知道密钥的。这意味着服务端理论上可以解密你的音视频。说白了,服务商能看到你在说什么、在做什么。对于很多场景,比如视频会议、远程医疗、金融交易,这显然不够。
我们需要的是真正的端到端加密(E2EE)。也就是只有通信的双方能解密,中间的任何节点——包括服务器——都看不到明文内容。
为什么标准WebRTC不够“端到端”
我刚开始接触WebRTC时,也以为它默认就是端到端加密的。直到有一次帮客户做金融合规审计,才发现问题所在。
标准WebRTC的加密链路是这样的:
- DTLS握手:在Peer Connection建立时,双方通过DTLS协商一个共享密钥。
- SRTP加密:媒体数据使用这个密钥进行AES加密传输。
- 密钥在哪儿:如果是P2P连接,密钥只有两端知道。但一旦引入SFU(服务端转发),SFU必须参与DTLS握手,所以它也知道密钥。
这就意味着,SFU运营商可以解密所有媒体流。你想想看,这在很多场景下是多大的安全隐患。
Insertable Streams:给开发者一把“钥匙”
那怎么实现真正的E2EE呢?WebRTC在2021年左右引入了一个新API——Insertable Streams(可插入流)。
这个API说白了,就是让你在编码后的视频帧(或者音频帧)进入RTP打包之前,或者在接收端解码之前,插入一段自定义的处理逻辑。你可以在这里做额外的加密、解密、甚至修改数据。
我个人的习惯是,把它想象成一个“管道中间的一个阀门”。数据流经这里时,你可以做任何操作。
核心接口:RTCRtpScriptTransform
实现方式其实不复杂。看个例子:
// 发送端:加密
const sender = pc.addTrack(track, stream);
const senderStreams = sender.createEncodedStreams();
const transform = new TransformStream({
transform: (encodedFrame, controller) => {
// 获取原始数据
const view = new DataView(encodedFrame.data);
const newData = new ArrayBuffer(encodedFrame.data.byteLength + 4);
const newView = new DataView(newData);
// 简单示例:在帧头添加一个4字节的随机密钥索引
newView.setUint32(0, keyIndex);
// 复制原始数据
for (let i = 0; i < view.byteLength; i++) {
newView.setUint8(i + 4, view.getUint8(i) ^ encryptionKey[keyIndex]);
}
encodedFrame.data = newData;
controller.enqueue(encodedFrame);
}
});
senderStreams.readable
.pipeThrough(transform)
.pipeTo(senderStreams.writable);
接收端做反向操作就行。这里我用的是XOR加密做演示,实际生产环境肯定要用AES-GCM这类标准算法。
RTCEncodedVideoFrame 或 RTCEncodedAudioFrame。注意,你操作的是已经编码过的数据(比如H.264 NAL单元),不是原始YUV/PCM。
自定义加密:别自己造轮子
说到自定义加密,我得提醒一句:千万不要自己设计加密算法。我在项目中见过有人用简单的XOR或者Base64“加密”视频流,那基本等于没加密。
正确的做法是:
- 使用标准算法:AES-GCM、ChaCha20-Poly1305 都是好选择。
- 处理好密钥分发:E2EE的难点不在加密本身,而在密钥怎么安全地传给对方。通常通过带外信道(比如单独的信令通道、QR码扫描、或者预共享密钥)来交换。
- 注意帧边界:Insertable Streams 给到你的是一帧一帧的数据。加密时要注意,不要把一帧的数据切碎,也不要跨帧加密。否则接收端无法正确解密。
我曾经踩过一个坑:在加密H.264视频时,没有考虑到NAL单元的分割。结果加密后的数据破坏了H.264的起始码,导致解码器完全崩溃。嗯,后来花了整整两天才定位到问题。
安全最佳实践:不止是加密
E2EE只是WebRTC安全的一部分。一个完整的WebRTC安全方案,还需要考虑以下几个方面:
1. 信令通道安全
信令交换(SDP、ICE候选)必须走HTTPS或WSS。如果信令被篡改,攻击者可以插入自己的密钥,实现中间人攻击。说白了,E2EE的前提是信令通道是可信的。
2. 身份认证与密钥绑定
你需要确保你加密时用的公钥,确实属于你要通信的那个人。通常的做法是:
- 使用数字签名(比如在SDP中嵌入公钥指纹)
- 或者通过身份提供商(IdP)进行认证
3. 前向安全性(Forward Secrecy)
如果长期密钥泄露,会不会导致历史通话被解密?标准WebRTC的DTLS-SRTP已经提供了前向安全性。但在自定义E2EE方案中,你需要自己保证这一点。建议每次会话生成临时密钥,会话结束后销毁。
4. 元数据保护
即使媒体内容加密了,攻击者仍然可以通过分析流量模式(比如包大小、时间间隔)推断出一些信息。比如,静音检测、说话人切换等。高级的安全方案会使用流量填充(Traffic Padding)来掩盖这些模式。
知识体系总览
下面这张图总结了本章的核心逻辑:从标准WebRTC的传输加密,到通过Insertable Streams实现E2EE,再到完整的安全最佳实践。
实际项目中的取舍
最后聊点实际的。E2EE虽然安全,但会带来一些副作用:
- 服务端无法做转码:比如SFU不能做SVC层级选择、不能做转码降分辨率。因为服务端看不到明文。
- 无法做服务端录制:录制必须在客户端完成,或者客户端把解密后的流再传给录制服务(那就不是E2EE了)。
- 调试困难:加密后的数据在抓包工具里全是乱码,排查问题很麻烦。
所以,要不要上E2EE,取决于你的业务场景。如果是内部会议、医疗、金融,那必须上。如果是直播、娱乐,可能标准加密就够了。
我个人建议:默认开启E2EE,但提供降级选项。这样既保证了安全,又保留了灵活性。