跳到主要内容
Web 聊天室消息加解密方案详解 | 极客日志
JavaScript Node.js 大前端 算法
Web 聊天室消息加解密方案详解 Web 聊天室通信安全核心在于平衡加密强度与实时性能。本文对比了五种主流方案:对称加密(AES-256-GCM)适合高频传输但密钥分发难;非对称加密(ECC/RSA)解决密钥交换但速度慢;混合加密结合两者优势,是推荐实践;端到端加密(Signal Protocol)提供最高安全性但实现复杂;轻量级加密(ChaCha20)适配低性能设备。实际选型需根据场景权衡安全性、兼容性与开发成本。
嘘 发布于 2026/4/5 更新于 2026/4/25 1 浏览
一、Web 聊天室消息加解密需求与技术约束
Web 聊天室基于 WebSocket 或 Socket.IO 实现实时双向通信,消息类型涵盖文本、图片、文件,场景包括单聊、群聊、广播。其安全风险主要集中在消息监听(中间人攻击)、内容篡改、身份冒充、数据泄露(服务器存储未加密消息),需通过加密方案满足核心安全需求,同时兼顾实时性与兼容性。
1.1 核心安全需求
需求维度 定义与目标 机密性(Confidentiality) 仅收发方可解密消息内容,中间人无法窃取(如 WebSocket 流量被截获时无法解析) 完整性(Integrity) 消息传输过程中未被篡改,接收方可验证内容一致性(如防止攻击者修改消息文本) 身份认证(Authentication) 确认消息发送方身份,防止伪造用户发送消息(如冒充管理员发送指令) 前向安全性(Forward Secrecy) 即使当前密钥泄露,过去的历史消息仍无法被解密(避免'一次泄露,全量曝光') 抗重放攻击(Anti-Replay) 防止攻击者重复发送旧消息(如重复发送'转账'指令)
1.2 技术约束
实时性 :加密解密耗时需控制在毫秒级,避免 WebSocket 消息延迟(如群聊消息发送后需秒级展示);
浏览器兼容性 :前端需基于 JS 实现加密,依赖浏览器对加密 API 的支持(如 Web Crypto API、第三方库);
前后端协同 :前后端需统一加密算法、密钥格式、数据传输格式(如 IV / 密文 / 标签的拼接规则);
设备适配 :支持低性能设备(如旧手机 WebView),避免算法对硬件加速的强依赖;
密钥管理 :前端私钥存储需安全(避免 localStorage 泄露),群聊密钥分发需高效。
二、主流消息加解密方案详解
2.1 方案 1:对称加密(AES-256-GCM)
2.1.1 方案概述
对称加密使用同一密钥完成加密与解密,AES(Advanced Encryption Standard)是当前最主流的对称加密算法,256 位密钥长度满足金融级安全需求;GCM(Galois/Counter Mode)是认证加密模式,同时提供机密性与完整性(通过认证标签验证),适合 Web 聊天室实时传输场景。
2.1.2 核心原理
AES-256 基础 :分组密码,将明文按 128 位分组,用 256 位密钥通过多轮置换 / 混淆运算生成密文;
GCM 模式工作流程 :
生成 12 字节初始化向量(IV,需随机且不重复,每次加密不同);
用密钥 + IV 生成计数器(Counter),计数器与密钥通过 AES 运算生成密钥流,与明文异或得到密文;
对'IV + 密文 + 附加数据(如消息 ID)'计算 Galois 哈希,生成 16 字节认证标签(用于解密时验证完整性);
解密验证 :接收方用相同密钥 + IV 解密得到明文,重新计算认证标签并与发送方标签比对,不一致则密文被篡改。
2.1.3 实现步骤(分场景)
场景 1:单聊加密
密钥协商 :
用户 A 与 B 通过'安全渠道'交换 AES-256 密钥(如通过服务器转发,但需用非对称加密保护密钥,此步骤暂不展开,后续混合加密会优化);
密钥生成:使用密码学安全随机数生成器(如 Web Crypto 的 crypto.getRandomValues())生成 32 字节(256 位)密钥。
消息加密(发送方 A) :
生成 12 字节 IV(crypto.getRandomValues(new Uint8Array(12)));
调用 AES-GCM 加密 API,输入'明文 + 密钥 + IV + 附加数据(如 messageId)',输出密文与认证标签;
拼接'IV(12 字节)+ 密文(N 字节)+ 认证标签(16 字节)',转为 Base64 字符串通过 WebSocket 发送。
消息解密(接收方 B) :
解析 Base64 字符串,按长度拆分 IV(前 12 字节)、密文(中间 N 字节)、认证标签(后 16 字节);
调用 AES-GCM 解密 API,输入'密文 + 密钥 + IV + 认证标签 + 附加数据',验证标签通过后得到明文。
场景 2:群聊加密
群密钥生成与分发 :
群创建者生成 AES-256 群密钥,通过服务器将密钥分发给所有群成员(需用成员的非对称公钥加密群密钥,避免分发泄露);
消息传输 :
发送方用群密钥加密消息(流程同单聊),服务器转发密文给所有群成员;
所有成员用群密钥解密消息;
密钥更新 :
群成员变更(如踢人 / 加人)时,创建者重新生成群密钥,用新成员公钥加密后分发,旧成员通过现有加密通道接收新密钥。
2.1.4 代码实现(前端 + 后端)
前端(Vue3 + Web Crypto API)
async function generateAesKey ( ) {
const key = await crypto.subtle .generateKey (
{ name : "AES-GCM" , length : 256 },
false ,
["encrypt" , "decrypt" ]
);
return key;
}
async function aesGcmEncrypt (plaintext, aesKey, additionalData ) {
const iv = crypto.getRandomValues (new Uint8Array (12 ));
const plaintextUint8 = new TextEncoder ().encode (plaintext);
const adUint8 = new TextEncoder ().encode (additionalData);
const encrypted = await crypto.subtle .encrypt (
{ name : "AES-GCM" , iv : iv, additionalData : adUint8, tagLength : 128 },
aesKey,
plaintextUint8
);
const encryptedUint8 = new Uint8Array (encrypted);
const ciphertext = encryptedUint8.slice (0 , encryptedUint8.length - 16 );
const tag = encryptedUint8.slice (encryptedUint8.length - 16 );
const combined = new Uint8Array ([...iv, ...ciphertext, ...tag]);
return btoa (String .fromCharCode (...combined));
}
async function aesGcmDecrypt (base64Str, aesKey, additionalData ) {
const combined = new Uint8Array (
atob (base64Str).split ("" ).map (c => c.charCodeAt (0 ))
);
const iv = combined.slice (0 , 12 );
const tag = combined.slice (combined.length - 16 );
const ciphertext = combined.slice (12 , combined.length - 16 );
const adUint8 = new TextEncoder ().encode (additionalData);
try {
const decrypted = await crypto.subtle .decrypt (
{ name : "AES-GCM" , iv : iv, additionalData : adUint8, tagLength : 128 },
aesKey,
new Uint8Array ([...ciphertext, ...tag])
);
return new TextDecoder ().decode (decrypted);
} catch (err) {
throw new Error ("密文被篡改或密钥错误" );
}
}
async function sendPrivateMessage (toUserId, plaintext, aesKey ) {
const messageId = uuidv4 ();
const encryptedStr = await aesGcmEncrypt (plaintext, aesKey, messageId);
socket.emit ("privateMessage" , {
toUserId,
messageId,
encryptedStr,
timestamp : Date .now ()
});
}
后端(Node.js + crypto) 后端仅转发加密消息,不处理解密(避免存储密钥),若需验证消息完整性可添加签名校验:
const express = require ("express" );
const http = require ("http" );
const { Server } = require ("socket.io" );
const crypto = require ("crypto" );
const app = express ();
const server = http.createServer (app);
const io = new Server (server, {
cors : { origin : "http://localhost:8080" }
});
const userMap = new Map ();
io.on ("connection" , (socket ) => {
socket.on ("userRegister" , (userId, eccPublicKey ) => {
userMap.set (userId, { socketId : socket.id , eccPublicKey });
socket.userId = userId;
console .log (`用户${userId} 上线` );
});
socket.on ("privateMessage" , (data ) => {
const { toUserId, messageId, encryptedStr, timestamp } = data;
const targetUser = userMap.get (toUserId);
if (targetUser) {
io.to (targetUser.socketId ).emit ("privateMessage" , {
fromUserId : socket.userId ,
messageId,
encryptedStr,
timestamp
});
}
});
socket.on ("groupMessage" , (groupData ) => {
const { groupId, encryptedStr, messageId, timestamp } = groupData;
io.to (groupId).emit ("groupMessage" , {
fromUserId : socket.userId ,
messageId,
encryptedStr,
timestamp
});
});
});
server.listen (3000 , () => console .log ("后端服务启动:3000 端口" ));
2.1.5 优劣分析 优点 缺点 性能优异:加密解密速度快(纯软件实现可达 GB/s 级),适合实时聊天室 密钥分发困难:单聊需安全交换密钥,群聊密钥更新复杂 兼容性好:Web Crypto API/AES-GCM 支持所有现代浏览器(Chrome 37+、Firefox 34+) 无身份认证:无法确认发送方身份,易被冒充 安全性高:GCM 模式抗篡改,256 位密钥抗暴力破解 缺乏前向安全性:密钥泄露则所有历史消息可解密 消息体积小:仅附加 IV(12 字节)+ 标签(16 字节),带宽占用低 群聊扩展性差:成员增多时密钥分发效率下降
2.2 方案 2:非对称加密(RSA-2048/ECC secp256r1)
2.2.1 方案概述 非对称加密使用密钥对(公钥 + 私钥),公钥可公开(用于加密 / 验签),私钥需保密(用于解密 / 签名)。RSA 基于大数分解问题,ECC(椭圆曲线加密)基于椭圆曲线离散对数问题,ECC 在相同安全性下密钥长度更短(secp256r1 公钥 64 字节 vs RSA-2048 公钥 256 字节),性能更优,更适合 Web 场景。
2.2.2 核心原理
(1)ECC secp256r1 原理(推荐)
椭圆曲线参数 :使用 NIST P-256 曲线(secp256r1),定义有限域上的椭圆方程 y² = x³ - 3x + b;
密钥对生成 :
私钥:随机生成 256 位整数 d(32 字节);
公钥:椭圆曲线上的点 Q = d * G(G 为曲线基点),表示为 64 字节(x 坐标 32 字节 + y 坐标 32 字节);
加密流程 :
发送方用接收方公钥 Q 生成临时点 C1 = k * G(k 为随机数);
计算共享点 S = k * Q,从 S 的 x 坐标派生对称密钥 K;
用 K 加密明文(如 AES-128),输出 C1 + 密文 + 标签;
解密流程 :
接收方用私钥 d 计算共享点 S = d * C1;
从 S 派生密钥 K,解密得到明文。
(2)RSA-2048 原理(兼容旧系统)
密钥对生成 :
生成两个大素数 p、q,计算 n = p*q(公钥 modulus);
计算欧拉函数 φ(n) = (p-1)(q-1),选择公钥指数 e(通常为 65537);
计算私钥指数 d(满足 e*d ≡ 1 mod φ(n));
加密 :密文 c = m^e mod n(m 为明文,需小于 n,RSA-2048 最大加密 245 字节);
解密 :明文 m = c^d mod n。
2.2.3 实现步骤(分场景)
场景 1:单聊加密(ECC)
密钥对生成与分发 :
用户 A 生成 ECC 密钥对(私钥 dA,公钥 QA),将 QA 发送给服务器;
用户 B 生成密钥对(私钥 dB,公钥 QB),将 QB 发送给服务器;
A 向服务器请求 B 的公钥 QB,B 请求 A 的公钥 QA。
消息加密(A→B) :
A 生成随机数 k,计算临时点 C1 = kG、共享点 S = k QB;
从 S.x 派生 AES-128 密钥 K(用 SHA-256 哈希后取前 16 字节);
用 K 加密明文(AES-GCM),生成密文 C2;
发送'C1(64 字节)+ C2(IV + 密文 + 标签)'给 B。
消息解密(B→A) :
B 用私钥 dB 计算 S = dB*C1,派生密钥 K;
用 K 解密 C2 得到明文。
场景 2:群聊加密(ECC)
密钥分发问题 :
若用 ECC 直接加密,发送方需用每个群成员的公钥加密消息,成员数为 N 时需加密 N 次,性能极差;
优化方案:发送方生成临时 AES 群密钥,用每个成员的公钥加密 AES 密钥,再发送'加密的 AES 密钥 + AES 加密的消息',成员解密 AES 密钥后解密消息。
2.2.4 代码实现(前端 ECC 加密) 使用 libsodium-wrappers(ECC 支持更完善的 JS 库):
import sodium from "libsodium-wrappers" ;
await sodium.ready ;
function generateEccKeyPair ( ) {
const keyPair = sodium.crypto_box_keypair ();
return {
privateKey : sodium.to_base64 (keyPair.privateKey ),
publicKey : sodium.to_base64 (keyPair.publicKey )
};
}
function eccEncrypt (plaintext, receiverPkBase64, senderSkBase64 ) {
const receiverPk = sodium.from_base64 (receiverPkBase64);
const senderSk = sodium.from_base64 (senderSkBase64);
const nonce = sodium.randombytes_buf (sodium.crypto_box_NONCEBYTES );
const ciphertext = sodium.crypto_box_easy (
sodium.encode_utf8 (plaintext),
nonce,
receiverPk,
senderSk
);
const combined = sodium.concat ([nonce, ciphertext]);
return sodium.to_base64 (combined);
}
function eccDecrypt (encryptedBase64, senderPkBase64, receiverSkBase64 ) {
const senderPk = sodium.from_base64 (senderPkBase64);
const receiverSk = sodium.from_base64 (receiverSkBase64);
const combined = sodium.from_base64 (encryptedBase64);
const nonce = combined.slice (0 , sodium.crypto_box_NONCEBYTES );
const ciphertext = combined.slice (sodium.crypto_box_NONCEBYTES );
try {
const plaintext = sodium.crypto_box_open_easy (
ciphertext,
nonce,
senderPk,
receiverSk
);
return sodium.decode_utf8 (plaintext);
} catch (err) {
throw new Error ("解密失败:密钥错误或密文篡改" );
}
}
const userAKeyPair = generateEccKeyPair ();
const userBPublicKey = "xxx" ;
const plaintext = "Hello, 非对称加密单聊!" ;
const encryptedStr = eccEncrypt (plaintext, userBPublicKey, userAKeyPair.privateKey );
socket.emit ("privateMessage" , {
toUserId : "userB" ,
encryptedStr,
fromUserPublicKey : userAKeyPair.publicKey
});
2.2.5 优劣分析 优点 缺点 密钥分发安全:公钥可公开传输,无需保密 性能差:ECC 加密速度约为 AES 的 1/10,RSA 更慢(不适合高频消息) 支持身份认证:私钥签名 + 公钥验签,确认发送方身份 消息长度限制:RSA-2048 最大加密 245 字节,需分段加密大消息(如图片) 前向安全性:每次会话生成新密钥对,泄露不影响历史消息 群聊兼容性差:N 个成员需加密 N 次,成员增多时延迟高 密钥存储简单:私钥仅需存储在本地,无需服务器同步 兼容性局限:部分旧浏览器(如 IE11)不支持 ECC 抗中间人攻击:公钥可通过证书验证(如 SSL 证书) 密钥管理复杂:私钥泄露则所有消息可解密,需安全存储(如硬件密钥)
2.3 方案 3:混合加密(AES-256-GCM + ECC secp256r1)
2.3.1 方案概述 混合加密结合对称加密的高性能与非对称加密的密钥分发优势,是 Web 聊天室的最优解之一(类似 TLS 协议原理):用 ECC 实现对称密钥(AES 密钥)的安全交换,用 AES-GCM 加密实际消息内容,兼顾安全与实时性。
2.3.2 核心原理
密钥交换阶段(ECDH) :
ECDH(Elliptic Curve Diffie-Hellman)是密钥协商协议,双方无需传输密钥,通过各自密钥对派生相同的共享密钥;
流程:A 生成密钥对(dA, QA),B 生成(dB, QB);A 发送 QA 给 B,B 发送 QB 给 A;A 计算 S = dAQB,B 计算 S = dB QA,双方得到相同共享点 S;从 S.x 派生 AES-256 密钥。
消息传输阶段(AES-GCM) :
用派生的 AES 密钥加密消息(流程同方案 1),实现高速传输;
每次会话生成新的 ECC 密钥对,保证前向安全性。
2.3.3 实现步骤(单聊 + 群聊)
场景 1:单聊加密(完整流程)
密钥协商(ECDH) :
步骤 1:用户 A 生成临时 ECC 密钥对(tempSkA, tempPkA),发送 tempPkA 给服务器,请求 B 的公钥;
步骤 2:服务器转发 tempPkA 给 B,并返回 B 的长期公钥 longPkB(B 注册时生成并存储);
步骤 3:B 生成临时密钥对(tempSkB, tempPkB),用 tempSkB 与 tempPkA 派生共享密钥 sharedKey,发送 tempPkB 给 A;
步骤 4:A 用 tempSkA 与 tempPkB 派生相同的 sharedKey,通过 SHA-256 哈希 + 密钥拉伸生成 AES-256 密钥 aesKey。
消息加密(AES-GCM) :
A 用 aesKey 加密消息,发送'IV + 密文 + 标签'给 B;
B 用 aesKey 解密消息。
会话更新 :
每发送 100 条消息或 24 小时后,重新执行 ECDH 协商,生成新 aesKey,保证前向安全性。
场景 2:群聊加密(优化方案)
群密钥生成与分发 :
群创建者 C 生成 AES 群密钥 groupAesKey;
C 从服务器获取所有群成员的长期公钥(longPk1, longPk2, ..., longPkn);
C 用每个成员的公钥加密 groupAesKey(ECC 加密),生成 encryptedKey1, encryptedKey2, ..., encryptedKeyn;
服务器将 encryptedKeyi 分发给成员 i,成员 i 用私钥解密得到 groupAesKey。
消息传输 :
任何成员发送群消息时,用 groupAesKey 加密(AES-GCM),服务器转发密文;
成员接收后用 groupAesKey 解密。
群密钥更新 :
成员变更时,当前持有 groupAesKey 的成员(如 C)生成新 groupAesKey,用新成员公钥加密分发,旧成员通过现有加密通道接收新密钥。
2.3.4 代码实现(前端 ECDH 密钥协商 + AES 加密)
async function generateLongEccKeyPair ( ) {
const keyPair = await crypto.subtle .generateKey (
{ name : "ECDH" , namedCurve : "P-256" },
true ,
["deriveKey" ]
);
const publicKeyRaw = await crypto.subtle .exportKey ("spki" , keyPair.publicKey );
const publicKeyBase64 = btoa (String .fromCharCode (...new Uint8Array (publicKeyRaw)));
return {
privateKey : keyPair.privateKey ,
publicKey : publicKeyBase64
};
}
async function deriveAesKey (localPrivateKey, peerPublicKeyBase64 ) {
const peerPublicKeyRaw = new Uint8Array (
atob (peerPublicKeyBase64).split ("" ).map (c => c.charCodeAt (0 ))
);
const peerPublicKey = await crypto.subtle .importKey (
"spki" ,
peerPublicKeyRaw,
{ name : "ECDH" , namedCurve : "P-256" },
false ,
[]
);
const sharedSecret = await crypto.subtle .deriveKey (
{ name : "ECDH" , public : peerPublicKey },
localPrivateKey,
{ name : "AES-GCM" , length : 256 },
false ,
["encrypt" , "decrypt" ]
);
return sharedSecret;
}
async function initPrivateChat (withUserId ) {
const localLongKeyPair = await loadLocalLongKeyPair ();
const peerLongPublicKey = await axios.get (`/api/user/${withUserId} /publicKey` );
const localTempKeyPair = await crypto.subtle .generateKey (
{ name : "ECDH" , namedCurve : "P-256" },
true ,
["deriveKey" ]
);
const localTempPublicKeyRaw = await crypto.subtle .exportKey ("spki" , localTempKeyPair.publicKey );
const localTempPublicKey = btoa (String .fromCharCode (...new Uint8Array (localTempPublicKeyRaw)));
const peerTempPublicKey = await new Promise ((resolve ) => {
socket.emit ("requestTempPublicKey" , { toUserId : withUserId, localTempPublicKey });
socket.once ("responseTempPublicKey" , (data ) => resolve (data.peerTempPublicKey ));
});
const aesKey = await deriveAesKey (localTempKeyPair.privateKey , peerTempPublicKey);
const plaintext = "混合加密单聊消息:AES+ECC" ;
const messageId = uuidv4 ();
const encryptedStr = await aesGcmEncrypt (plaintext, aesKey, messageId);
socket.emit ("privateMessage" , {
toUserId,
messageId,
encryptedStr
});
socket.on ("privateMessage" , async (data) => {
if (data.fromUserId === withUserId) {
const decryptedText = await aesGcmDecrypt (data.encryptedStr , aesKey, data.messageId );
console .log ("解密消息:" , decryptedText);
}
});
}
2.3.5 优劣分析 优点 缺点 性能均衡:AES 加密消息(快)+ ECC 协商密钥(轻量),适合实时群聊 实现复杂度高:需处理 ECDH 密钥协商、AES 加密、密钥更新多流程 安全性强:兼顾机密性(AES)、完整性(GCM)、前向安全性(临时密钥对) 群密钥分发依赖服务器:需服务器存储成员公钥,协同分发加密密钥 密钥管理可控:私钥本地存储,公钥服务器托管,降低泄露风险 旧浏览器兼容差:IE11 不支持 ECDH/P-256,需降级方案(如 RSA) 扩展性好:群聊成员增多时,仅需加密 1 次 AES 密钥(而非 N 次消息) 密钥更新需同步:群成员离线时可能错过密钥更新,需重试机制 抗攻击能力强:结合 ECC 抗中间人、AES-GCM 抗篡改 前端私钥存储风险:若私钥存在 localStorage,可能被 XSS 攻击窃取
2.4 方案 4:端到端加密(基于 Signal Protocol)
2.4.1 方案概述 Signal Protocol 是专为即时通讯设计的端到端加密(E2EE)方案,被 WhatsApp、Signal、Facebook Messenger 采用,提供强安全性(符合 NIST 标准),支持单聊 / 群聊、前向安全性、抗重放攻击,是私密聊天室的终极选择。
2.4.2 核心原理 Signal Protocol 核心由四部分组成:
双棘轮算法(Double Ratchet Algorithm) :
结合'对称棘轮'与'非对称棘轮',每次消息交互后更新发送 / 接收密钥:
对称棘轮:用哈希链(SHA-256)更新密钥,每次发送消息后将发送密钥 SK 更新为 SHA-256(SK);
非对称棘轮:用 ECC 密钥对更新,接收方定期生成新预密钥,发送方用新预密钥更新会话密钥;
保证前向安全性:即使当前密钥泄露,过去的消息仍无法解密。
预密钥机制(PreKey) :
用户生成一批预密钥(包含预密钥公钥 PKp、预密钥 ID Idp)和签名密钥对(SKs, PKs),上传到服务器;
新用户发起会话时,从服务器获取对方的预密钥 + 签名公钥,无需等待对方在线即可建立加密通道。
椭圆曲线加密(ECC secp256r1 + X25519) :
身份密钥(长期):IK(secp256r1,用于签名);
预密钥(短期):PKp(X25519,用于密钥协商);
临时密钥(单次会话):EK(X25519,用于初始协商)。
Sender Key 机制(群聊加密) :
群内生成 Sender Key(对称密钥),发送方用 Sender Key 加密消息,生成消息密钥 MK;
群成员用 Sender Key 解密 MK,再用 MK 解密消息;
Sender Key 更新时,通过现有加密通道用成员身份公钥加密分发。
2.4.3 实现步骤(单聊场景)
用户初始化(注册阶段) :
生成身份密钥对 IK = (IKs, IKp)(长期,不更新);
生成签名密钥对 SK = (SKs, SKp)(中期,定期更新);
生成 100 个预密钥 PreKey = [(Idp1, PKp1), (Idp2, PKp2), ..., (Idp100, PKp100)](短期,用完即补);
用 SKs 对 PKp 签名,上传 IKp、PKp、PreKey 到 Signal 服务器(仅存储公钥,不存储私钥)。
会话建立(A→B 首次聊天) :
步骤 1:A 从服务器获取 B 的 IKp、PKp、一个未使用的 PreKey (Idp, PKp);
步骤 2:A 生成临时密钥对 EK = (EKs, EKp);
步骤 3:A 用 EKs、B 的 PKp、B 的 PreKey.PKp 派生初始会话密钥 RK(Root Key)和发送密钥 SK;
步骤 4:A 发送'EKp + PreKey.Idp + 消息密文'给 B,消息密文用 SK 加密;
步骤 5:B 用自己的 PreKey 私钥、EKp 派生相同的 RK 和接收密钥 RK,解密得到消息。
会话持续(双棘轮更新) :
A 发送消息后,用哈希链更新发送密钥 SK = SHA-256(SK);
B 接收消息后,用哈希链更新接收密钥 RK = SHA-256(RK);
每 10 条消息后,B 生成新预密钥,A 用新预密钥更新 RK,保证前向安全性。
2.4.4 代码实现(基于 libsignal-protocol-javascript) Signal Protocol 算法复杂,推荐使用官方维护的 libsignal-protocol-javascript 库:
import * as signal from "libsignal-protocol-javascript" ;
class SignalStore {
constructor ( ) {
this .identityKeyPair = null ;
this .preKeys = new Map ();
this .signedPreKey = null ;
this .sessions = new Map ();
}
putIdentityKeyPair (keyPair ) { this .identityKeyPair = keyPair; }
getIdentityKeyPair ( ) { return this .identityKeyPair ; }
storePreKey (id, preKey ) { this .preKeys .set (id, preKey); }
getPreKey (id ) { return this .preKeys .get (id); }
storeSession (addr, session ) { this .sessions .set (addr, session); }
loadSession (addr ) { return this .sessions .get (addr); }
}
async function registerSignalUser (userId ) {
const store = new SignalStore ();
const keyHelper = signal.KeyHelper ;
const identityKeyPair = await keyHelper.generateIdentityKeyPair ();
store.putIdentityKeyPair (identityKeyPair);
const signedPreKey = await keyHelper.generateSignedPreKey (
identityKeyPair,
Math .floor (Date .now () / 1000 )
);
store.signedPreKey = signedPreKey;
for (let i = 0 ; i < 100 ; i++) {
const preKey = await keyHelper.generatePreKey (i);
store.storePreKey (preKey.keyId , preKey);
}
await axios.post ("/api/signal/register" , {
userId,
identityPublicKey : Buffer .from (identityKeyPair.pubKey ).toString ("base64" ),
signedPreKey : {
keyId : signedPreKey.keyId ,
publicKey : Buffer .from (signedPreKey.pubKey ).toString ("base64" ),
signature : Buffer .from (signedPreKey.signature ).toString ("base64" )
},
preKeys : Array .from (store.preKeys .entries ()).map (([id, pk] ) => ({
keyId : id,
publicKey : Buffer .from (pk.pubKey ).toString ("base64" )
}))
});
return store;
}
async function initSignalChat (store, targetUserId ) {
const keyHelper = signal.KeyHelper ;
const address = new signal.SignalProtocolAddress (targetUserId, 1 );
const targetPubKeys = await axios.get (`/api/signal/user/${targetUserId} /keys` );
const ephemeralKeyPair = await keyHelper.generateEphemeralKeyPair ();
const sessionBuilder = new signal.SessionBuilder (store, address);
await sessionBuilder.processPreKey ({
registrationId : 1 ,
identityKey : Buffer .from (targetPubKeys.identityPublicKey , "base64" ),
signedPreKey : {
keyId : targetPubKeys.signedPreKey .keyId ,
publicKey : Buffer .from (targetPubKeys.signedPreKey .publicKey , "base64" ),
signature : Buffer .from (targetPubKeys.signedPreKey .signature , "base64" )
},
preKey : {
keyId : targetPubKeys.preKey .keyId ,
publicKey : Buffer .from (targetPubKeys.preKey .publicKey , "base64" )
}
});
const sessionCipher = new signal.SessionCipher (store, address);
const plaintext = "Signal Protocol 端到端加密消息" ;
const ciphertext = await sessionCipher.encrypt (
Buffer .from (plaintext, "utf8" )
);
socket.emit ("signalMessage" , {
toUserId : targetUserId,
ciphertext : {
type : ciphertext.type ,
ephemeralKeyId : ciphertext.ephemeralKeyId ,
ciphertext : Buffer .from (ciphertext.body ).toString ("base64" )
}
});
socket.on ("signalMessage" , async (data) => {
if (data.fromUserId === targetUserId) {
const decryptCiphertext = {
type : data.ciphertext .type ,
ephemeralKeyId : data.ciphertext .ephemeralKeyId ,
body : Buffer .from (data.ciphertext .ciphertext , "base64" )
};
const decrypted = await sessionCipher.decrypt (decryptCiphertext);
console .log ("解密消息:" , decrypted.toString ("utf8" ));
}
});
}
2.4.5 优劣分析 优点 缺点 安全性顶级:符合 E2EE 标准,抗中间人、重放、篡改攻击,前向安全性最优 实现复杂度极高:需理解双棘轮、预密钥、Sender Key 等复杂概念 场景覆盖全:支持单聊、群聊、文件传输,适配 Web / 移动端 学习成本高:API 文档少,需阅读官方协议规范(Signal Specification) 成熟稳定:被数十亿用户验证(WhatsApp),无已知安全漏洞 服务器依赖强:需搭建 Signal 兼容服务器,管理预密钥生命周期 密钥管理自动化:自动更新密钥,无需用户干预 前端库体积大:libsignal-protocol-javascript 约 500KB,影响加载速度 抗离线攻击:预密钥机制支持离线发起会话 调试困难:加密流程黑盒化,问题定位复杂
2.5 方案 5:轻量级加密(ChaCha20-Poly1305)
2.5.1 方案概述 ChaCha20 是 Google 设计的流密码,Poly1305 是高效消息认证码,两者组合提供轻量级认证加密,适合低性能设备(如旧手机 WebView、嵌入式设备)—— 无需 AES 硬件加速,纯软件实现速度比 AES-GCM 快 30%~50%,且安全性与 AES-256 相当。
2.5.2 核心原理
ChaCha20 流密码 :
输入:256 位密钥、96 位 nonce(随机且不重复)、32 位计数器(初始为 0);
运算:通过'四轮双混合函数'(Double Round)生成 64 字节密钥流块,计数器递增生成后续块;
加密:密钥流与明文异或得到密文(流密码特性:相同密钥流 + 明文 = 密文,密文 + 密钥流 = 明文)。
Poly1305 认证码 :
用 32 位密钥(从 ChaCha20 密钥派生)对'nonce + 密文 + 附加数据'计算 128 位认证标签,验证密文完整性。
2.5.3 实现步骤(单聊场景)
密钥生成 :用 crypto.getRandomValues() 生成 32 字节(256 位)ChaCha20 密钥;
加密流程 :
生成 96 位 nonce(crypto.getRandomValues(new Uint8Array(12)));
用 ChaCha20 生成密钥流,加密明文得到密文;
用 Poly1305 计算认证标签;
发送'nonce(12 字节)+ 密文 + 标签(16 字节)';
解密流程 :
拆分 nonce、密文、标签;
生成密钥流解密得到明文;
重新计算标签并验证,不一致则拒绝。
2.5.4 代码实现(前端 libsodium) import sodium from "libsodium-wrappers" ;
await sodium.ready ;
function generateChaChaKey ( ) {
return sodium.to_base64 (sodium.randombytes_buf (sodium.crypto_aead_chacha20poly1305_ietf_KEYBYTES ));
}
function chachaEncrypt (plaintext, keyBase64, additionalData ) {
const key = sodium.from_base64 (keyBase64);
const nonce = sodium.randombytes_buf (sodium.crypto_aead_chacha20poly1305_ietf_NPUBBYTES );
const ad = sodium.encode_utf8 (additionalData);
const ciphertext = sodium.crypto_aead_chacha20poly1305_ietf_encrypt (
sodium.encode_utf8 (plaintext),
ad,
null ,
nonce,
key
);
const combined = sodium.concat ([nonce, ciphertext]);
return sodium.to_base64 (combined);
}
function chachaDecrypt (encryptedBase64, keyBase64, additionalData ) {
const key = sodium.from_base64 (keyBase64);
const combined = sodium.from_base64 (encryptedBase64);
const nonce = combined.slice (0 , sodium.crypto_aead_chacha20poly1305_ietf_NPUBBYTES );
const ciphertext = combined.slice (sodium.crypto_aead_chacha20poly1305_ietf_NPUBBYTES );
const ad = sodium.encode_utf8 (additionalData);
try {
const plaintext = sodium.crypto_aead_chacha20poly1305_ietf_decrypt (
null ,
ciphertext,
ad,
nonce,
key
);
return sodium.decode_utf8 (plaintext);
} catch (err) {
throw new Error ("解密失败:标签不匹配" );
}
}
const chachaKey = generateChaChaKey ();
const plaintext = "低性能设备友好:ChaCha20 加密" ;
const encryptedStr = chachaEncrypt (plaintext, chachaKey, "messageId_123" );
socket.emit ("privateMessage" , {
toUserId : "userB" ,
encryptedStr,
keyId : "chacha_key_001"
});
2.5.5 优劣分析 优点 缺点 性能优异:纯软件实现速度快,比 AES-GCM 快 30%,适合低性能设备 密钥分发问题:同 AES,需非对称加密辅助分发 兼容性好:libsodium 支持所有浏览器 -
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online