使用 Web Crypto API 进行端到端加密聊天

2025-05-24

使用 Web Crypto API 进行端到端加密聊天

在传输或存储用户数据(尤其是私人对话)时,必须考虑采用加密技术来确保隐私。

动画显示了聊天屏幕上的消息从加密文本到解密文本的交替

通过阅读本教程,您将了解如何仅使用 JavaScript 和Web Crypto API(一种本机浏览器 API)对 Web 应用程序中的数据进行端到端加密。

请注意,本教程非常基础,仅供参考,可能包含简化内容,不建议自行制定加密协议。如果在安全专业人员的帮助下使用不当,所使用的算法可能会存在某些“陷阱”。

如果你遇到问题,也可以在这个GitHub 仓库中找到完整的项目。如果你有任何疑问,欢迎随时在Twitter上联系我:)。

什么是端到端加密?

端到端加密是一种通信系统,只有通信双方才能读取消息。任何窃听者都无法获取解密对话所需的加密密钥,即使是运营消息服务的公司也无法获取。

什么是 Web 加密 API?

Web 加密 API 定义了一个低级接口,用于与用户代理管理或公开的加密密钥材料进行交互。该 API 本身与密钥存储的底层实现无关,但提供了一组通用接口,允许富 Web 应用程序执行签名生成和验证、哈希计算和验证、加密和解密等操作,而无需访问原始密钥材料。

基础知识

在以下步骤中,我们将声明端到端加密所涉及的基本函数。您可以将每个函数复制到文件.js夹下的专用文件中。请注意,由于 Web Crypto API 的异步特性,lib所有这些函数都是函数。async

注意:并非所有浏览器都实现了我们将要使用的算法。例如,Internet Explorer 和 Microsoft Edge。请查看MDN Web 文档中的兼容性表:Subtle Crypto - Web APIs

生成密钥对

加密密钥对对于端到端加密至关重要。密钥对由公钥私钥组成。应用程序中的每个用户都应该拥有一个密钥对来保护其数据,其中公钥部分可供其他用户访问,而私钥部分只有密钥对的所有者可以访问。您将在下一节中了解这些密钥如何发挥作用。

要生成密钥对,我们将使用方法,并使用JWK 格式window.crypto.subtle.generateKey导出私钥和公钥。后者用于保存或传输这些密钥。可以将其视为一种序列化密钥以便在 JavaScript 之外使用的方法。window.crypto.subtle.exportKey

generateKeyPair.jsPS:如果由于 dev.to 中的错误而导致您看不到以下内容,请刷新此页面。

export default async () => {
const keyPair = await window.crypto.subtle.generateKey(
{
name: "ECDH",
namedCurve: "P-256",
},
true,
["deriveKey", "deriveBits"]
);
const publicKeyJwk = await window.crypto.subtle.exportKey(
"jwk",
keyPair.publicKey
);
const privateKeyJwk = await window.crypto.subtle.exportKey(
"jwk",
keyPair.privateKey
);
return { publicKeyJwk, privateKeyJwk };
};

此外,我选择了基于P-256 椭圆曲线的ECDH 算法,因为它支持良好,并且在安全性和性能之间取得了良好的平衡。随着新算法的出现,这种偏好可能会随着时间而改变。

注意:导出私钥可能会导致安全问题,因此必须谨慎处理。本教程集成部分将介绍允许用户复制粘贴私钥的方法,这种方法并不好,仅用于教育目的。

导出密钥

我们将使用上一步生成的密钥对来派生对称加密密钥,该密钥用于加密和解密数据,并且对于任何两个通信用户而言都是唯一的。例如,用户 A 使用其私钥和用户 B 的公钥派生密钥,而用户 B 使用其私钥和用户 A 的公钥派生相同的密钥。任何人都无法在无法访问至少一个用户私钥的情况下生成派生密钥,因此确保其安全至关重要。

在上一步中,我们导出了 JWK 格式的密钥对。在派生密钥之前,我们需要使用 将它们导入到原始状态window.crypto.subtle.importKey。要派生密钥,我们将使用window.crypto.subtle.deriveKey

在这种情况下,我选择了AES-GCM 算法,因为它具有已知的安全性/性能平衡和浏览器可用性。

加密文本

现在我们可以使用派生密钥来加密文本,因此传输它是安全的。

在加密之前,我们将文本编码为Uint8Array,因为这是 encrypt 函数所需的格式。我们使用 加密该数组window.crypto.subtle.encrypt,然后将其ArrayBuffer输出转回Uint8Array,再将其转换为string,并将其编码为Base64。JavaScript 会使其变得稍微复杂一些,但这只是将加密数据转换为可传输文本的一种方法。

如您所见,AES-GCM 算法参数包含一个初始化向量(iv)。对于每个加密操作,它可以是随机的,但必须唯一,以确保加密的强度。它包含在消息中,以便用于解密过程,这是下一步。此外,虽然不太可能达到这个数字,但您应该在 2^32 次使用后丢弃密钥,因为随机 IV 可能会在此时重复出现。

解密文本

现在我们可以使用派生密钥来解密我们收到的任何加密文本,其操作与加密步骤完全相反。

在解密之前,我们检索初始化向量,将字符串从 Base64 转换回 ,并将其转换为Uint8Array,然后使用相同的算法定义进行解密。之后,我们解码ArrayBuffer并返回人类可读的字符串。

此解密过程也可能由于使用了错误的派生密钥或初始化向量而失败,这意味着用户没有正确的密钥对来解密收到的文本。在这种情况下,我们会返回一条错误消息。

集成到您的聊天应用中

这就是所有需要的加密工作!在接下来的章节中,我将解释如何使用上面实现的方法,对使用Stream Chat 强大的 React 聊天组件构建的聊天应用程序进行端到端加密

克隆项目

在本地文件夹中克隆加密的 Web 聊天存储库,安装依赖项并运行它。

之后,应该会打开一个浏览器标签页。但首先,我们需要使用自己的 Stream Chat API 密钥配置项目。

配置流聊天仪表板

在GetStream.io创建您的帐户,创建一个应用程序,然后选择开发而不是生产。

用户在 GetStream.io 创建开发应用程序的屏幕截图

为了简化操作,我们先禁用授权检查和权限检查。请务必点击“保存”。当您的应用投入生产时,您应该保持这些检查处于启用状态,并设置一个后端来为用户提供令牌。

在 Stream App 仪表板中启用跳过身份验证检查和权限的屏幕截图

为了将来的参考,请参阅有关身份验证的文档有关权限的文档

请记下 Stream 凭据,因为我们将在下一步中使用它们在应用中初始化聊天客户端。由于我们已禁用身份验证和权限,因此目前我们实际上只需要密钥。不过,将来您将在后端使用密钥来实现身份验证,以便为 Stream Chat 颁发用户令牌,从而使您的聊天应用能够拥有适当的访问控制。

流仪表板上的凭据屏幕截图

如您所见,我已删除了我的密钥。请您妥善保管这些凭证。

更改凭证

在 中src/lib/chatClient.js,将密钥更改为您的密钥。我们将使用此对象进行 API 调用并配置聊天组件。

之后,你应该可以测试该应用程序了。在接下来的步骤中,你将了解我们定义的函数的作用。

设置用户

在 中src/lib/setUser.js,我们定义了一个函数,用于设置聊天客户端的用户,并使用给定密钥对的公钥对其进行更新。发送公钥对于其他用户派生与我们用户进行加密和解密通信所需的密钥是必要的。

在这个函数中,我们导入了chatClient上一步中定义的 。它接受一个用户 ID 和一个密钥对,然后调用它chatClient.setUser来设置用户。之后,它会检查该用户是否已经拥有公钥,以及它是否与给定的密钥对中的公钥匹配。如果公钥匹配或不存在,我们会使用给定的公钥更新该用户;否则,我们会断开连接并显示错误。

发送方组件

在中src/components/Sender.js,我们定义了第一个屏幕,我们在其中选择我们的用户 ID,并且可以使用我们在中描述的函数生成密钥对generateKey.js,或者,如果这是一个现有用户,则粘贴在创建用户时生成的密钥对。

该图显示了一个登录表单,其中包含一个 ID 字段、一个密钥对字段、一个生成按钮和一个提交按钮

接收方组件

在 中src/components/Recipient.js,我们定义了第二个屏幕,在其中选择要与之通信的用户的 ID。组件将使用 来获取此用户chatClient.queryUsers。该调用的结果将包含用户的公钥,我们将使用该公钥来派生加密/解密密钥。

图片显示了一个表单,其中包含您想要与之通信的用户 ID 字段以及一个提交按钮

KeyDeriver 组件

在 中src/components/KeyDeriver.js,我们定义了第三个屏幕,其中密钥是使用我们在 中实现的方法,deriveKey.js通过发送者(我们)的私钥和接收者的公钥派生出来的。此组件只是一个被动加载屏幕,因为所需的信息已在前两个屏幕中收集。但是,如果密钥出现问题,它会显示错误。

EncryptedMessage组件

在中src/components/EncryptedMessage.js,我们自定义 Stream Chat 的消息组件,以使用我们在中定义的方法decrypt.js以及加密数据和派生密钥来解密消息。

该图显示一条聊天消息,内容为“这是一条加密消息”

如果没有对 Message 组件进行这种定制,它将显示如下:

图片显示聊天消息中包含难以理解的文字

定制是通过包装 Stream Chat 的MessageSimple组件并使用useEffect钩子通过解密方法修改消息道具来完成的。

EncryptedMessageInput 组件

在中src/components/EncryptedMessageInput.js,我们自定义了 Stream Chat 的 MessageInput 组件,使用我们在中定义的方法对发送前写入的消息encrypt.js与原始文本进行加密。

自定义是通过包装 Stream Chat 的MessageInputLarge组件并将overrideSubmitHandlerprop 设置为在发送到频道之前加密文本的函数来完成的。

聊天组件

最后,在中src/components/Chat.js,我们使用 Stream Chat 的组件以及我们自定义的 Message 和 EncryptedMessageInput 组件构建整个聊天屏幕。

MessageList组件有一个Messageprop,设置为自定义EncryptedMessage组件,并且EncryptedMessageInput可以在层次结构中将其放置在其正下方。

Web Crypto API 的后续步骤

恭喜!您刚刚学习了如何在 Web 应用中实现基本的端到端加密。重要的是要知道,这只是最基本的端到端加密形式。它缺少一些额外的调整,这些调整可以使其在现实世界中更加安全,例如随机填充数字签名前向保密等。此外,在实际应用中,寻求应用程序安全专业人员的帮助至关重要。

PS:特别感谢评论里的Junxiao纠正我的错误:-)

文章来源:https://dev.to/cardoso/end-to-end-encrypted-chat-with-the-web-crypto-api-3d02
PREV
当编程不再有趣时
NEXT
Socket.io 教程不是聊天应用程序(使用 React.js) Socket.io 教程不是聊天应用程序(使用 React.js)