实时协作编辑已经成为网页应用的基础能力,但多人同时编辑同一份文档时,数据在浏览器与服务器之间频繁往返,如果仅依靠传输层加密,服务端仍然能够读取全部明文。HTML5提供的Web Crypto API让前端可以直接完成密钥生成、非对称协商与对称加密,从而实现真正的端到端保护。

一、Web Crypto API基础与密钥生成
Web Crypto API是现代浏览器内置的加密接口,不需要引入任何第三方库即可使用。它支持RSA、ECDH、AES-GCM等算法,所有运算都在浏览器安全上下文中完成,私钥不会离开客户端。在实时协作场景中,每个参与者首先需要在本地生成一对非对称密钥,用于协商后续通信所用的对称会话密钥。
下面代码展示了如何使用window.crypto.subtle.generateKey生成ECDH密钥对。ECDH相比RSA计算更快,更适合需要频繁建立会话的协作应用。注意extractable参数设为true,方便后续导出公钥分发给其他端。
async function createKeyPair() {
const keyPair = await window.crypto.subtle.generateKey(
{
name: 'ECDH',
namedCurve: 'P-256'
},
true,
['deriveKey', 'deriveBits']
);
const publicKey = await window.crypto.subtle.exportKey(
'spki',
keyPair.publicKey
);
return { privateKey: keyPair.privateKey, publicKeyBuffer: publicKey };
}
生成密钥后,公钥可以经由信令服务器或WebSocket广播给房间内其他协作者,而私钥始终保留在本地。这样即便信令通道被监听,攻击者也只能拿到公钥,无法推导出会话密钥。实际项目中建议为每次进入房间都生成全新密钥对,避免长期使用同一密钥带来的泄露风险。
二、基于ECDH的会话密钥协商
当两名协作者互相拿到对方公钥后,就可以利用自己的私钥与对方公钥执行deriveKey,得到相同的AES-GCM会话密钥。由于ECDH的数学特性,两端独立计算出的密钥字节完全一致,不需要在网络上传递密钥本身。
以下示例演示如何用本地私钥和远端公钥派生出共享密钥。这里指定派生算法为ECDH,再用HKDF做密钥扩展,最终输出128位AES-GCM密钥,可用于加密协作过程中的操作指令与文本内容。
async function deriveSessionKey(privateKey, remotePublicKeyBuffer) {
const remotePublicKey = await window.crypto.subtle.importKey(
'spki',
remotePublicKeyBuffer,
{ name: 'ECDH', namedCurve: 'P-256' },
true,
[]
);
const sessionKey = await window.crypto.subtle.deriveKey(
{
name: 'ECDH',
public: remotePublicKey
},
privateKey,
{ name: 'AES-GCM', length: 128 },
false,
['encrypt', 'decrypt']
);
return sessionKey;
}
在多端协作时,常见做法是选一个稳定节点或服务器作为密钥分发辅助,但核心派生仍在各端完成。如果房间内有三个人,A需要和B、C分别协商出独立会话密钥,或者采用群组密钥方案。简单实现中,可以为每对成员维护独立AES密钥,发送消息时分别加密后广播,虽然带宽稍高但逻辑清晰。
三、用AES-GCM加密协作消息
AES-GCM是一种带认证标签的对称加密模式,既能保证机密性也能防止篡改。协作编辑时,每一次按键、删除或光标移动都可以序列化为JSON,再用会话密钥加密后通过WebSocket发送。接收方解密后应用变更,实现实时同步。
下面代码展示加密与解密函数。加密时随机生成12字节IV,GCM会自动附加认证标签,解密时若数据被篡改会直接抛出异常,保障协作指令的完整性。
async function encryptMessage(sessionKey, obj) {
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const data = new TextEncoder().encode(JSON.stringify(obj));
const cipher = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
sessionKey,
data
);
return { iv: Array.from(iv), cipher: Array.from(new Uint8Array(cipher)) };
}
async function decryptMessage(sessionKey, msg) {
const iv = new Uint8Array(msg.iv);
const cipher = new Uint8Array(msg.cipher);
const plain = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
sessionKey,
cipher
);
return JSON.parse(new TextDecoder().decode(plain));
}
使用上述方式,浏览器端产生的协作数据在离开设备前就已经加密,服务端只能转发密文。即便数据库泄露或运维人员查看日志,也无法还原文档内容。不过需要注意,IV不能重复使用同一密钥加密多段数据,否则会破坏GCM安全性,因此每次加密都必须重新随机生成IV。
四、结合WebSocket实现加密同步通道
实时协作依赖全双工通信,WebSocket是最轻量的选择。前端建立WebSocket连接后,先交换公钥,再协商会话密钥,此后所有send的数据均为加密对象。服务端只负责路由,不解析业务明文。
以下片段说明消息循环如何处理公钥与加密协作事件。收到对方公钥就派生密钥,收到加密消息就解密并交给编辑器引擎,这样把传输层与加密层解耦,便于后期替换信令协议。
let sessionKey = null;
const ws = new WebSocket('wss://ipipp.com/collab');
ws.onmessage = async (event) => {
const data = JSON.parse(event.data);
if (data.type === 'public_key') {
sessionKey = await deriveSessionKey(localPrivateKey, base64ToBuffer(data.key));
} else if (data.type === 'edit') {
const op = await decryptMessage(sessionKey, data.payload);
applyOperation(op);
}
};
async function sendEdit(op) {
const encrypted = await encryptMessage(sessionKey, op);
ws.send(JSON.stringify({ type: 'edit', payload: encrypted }));
}
在弱网环境下,WebSocket可能重连,此时应重新交换公钥并协商密钥,避免复用旧会话密钥带来的重放风险。对于需要离线编辑的场景,可以将密文暂存在IndexedDB中,恢复连接后再加密上传,保证本地数据同样不会以明文落盘。
五、密钥管理与退出清理
协作安全的核心之一是密钥生命周期。用户关闭页面或主动退出房间时,应当调用clear或置空变量释放私钥引用,并从内存中删除会话密钥。浏览器虽会自动回收,但显式清理能降低扩展程序或内存dump导致泄露的可能。
如果协作房间支持成员动态进出,新加入者无法解密历史消息是合理的设计,可采用消息加密密钥定期轮换策略,由任一在线成员生成新AES密钥并用各成员公钥加密分发。这样既保证前向保密,又不会让新成员看到其加入前的敏感内容。
function cleanupKeys() {
localPrivateKey = null;
sessionKey = null;
currentRoom = null;
console.log('本地协作密钥已清理');
}
综合来看,HTML5借助Web Crypto与WebSocket即可搭建一套不依赖服务端的轻量端到端加密协作方案。开发者只需处理好密钥协商、IV随机化与退出清理三个环节,就能在主流浏览器中提供安全的实时协作体验,而不必引入繁重的原生插件或后端加解密服务。