你好👋 今天,我们来了解 和 seald SDK 构建 如何使用 Pubnub 端到端 聊天 加密 。 👉 可以在此处找到如何使用 PubNub 和 Seald 的完整示例: https://github.com/seald/pubnub-example-project 酒吧是什么? PubNub 是一种实时通信服务,可以集成到大多数应用程序中。可靠且可扩展,它可以轻松地与最常见的框架集成。 什么是密封? Seald.io 提供了一个 SDK,允许您执行 ,具有高级管理功能, 该 SDK 可以集成到 Web、后端、移动或桌面应用程序中。 端到端加密 而无需任何先前的加密知识。 内容概述 为什么要使用端到端加密? 为什么使用 Seald.io 而不是 PubNub 加密钩子?👀 目标🏆 实施🧠 使用 Seald 添加端到端加密 🔒💬 结论✅ 为什么要使用端到端加密? 🔒 端到端加密提供 它允许在收集敏感数据后立即对其进行加密。早期加密将减少应用程序的攻击面。另一个优点是您可以精确 当它不在您的范围内时( 最高级别的隐私和安全性。 管理谁可以访问数据。 例如第三方服务),这也将提供保护。 端到端加密使您能够 无论数据在传输中、静止时,甚至不在您手中时。因此,它提供了比其他加密技术(TLS、全盘加密等)更广泛的保护。 始终保持控制, 每当您遇到 情况(GDPR、HIPAA、SOC-2...)或您拥有 (医疗、国防...)时,端到端加密都是必备的。但即使对于更常见的数据,拥有它也是一个很好的做法。数据泄露是一种破坏性事件, 很重要的 合规性 敏感数据 而且变得越来越频繁。 为什么使用 Seald.io 而不是 PubNub 加密钩子?👀 PubNub SDK 提供了一个简单的加密挂钩,在实例化其 SDK 时使用 参数。这样做将确保所有上传的消息 但是,您必须自己进行密钥管理,这是端到端加密系统中 。 cipherKey 在发送之前都经过加密。 最困难的部分 漏洞很少来自加密本身,但最常见的 。 是由于安全模型中的缺陷而泄露的密钥 使用第三方服务来保证您的安全可以让您 Seald.io 提出了一个 具有实时访问管理控制、用户撤销和恢复、2FA 等功能。 不会出现单点故障。 经过 ANSSI 认证的强大安全模型, 目标🏆 本文 以便通过端到端加密保护您的聊天。我们将构建 具有以下功能: 逐步介绍如何将 Seald 与 PubNub 集成, 一个示例消息应用程序, 一对一和群聊天室。 每个成员都有一个与其他用户专用的聊天室。 任何人都可以与多个其他用户创建群聊天室。 对发送的每条消息和文件使用端到端加密。 允许对聊天进行实时访问管理。 实施🧠 设置 PubNub 帐户 👤 首先,您需要 您可以报名 。登录仪表板后,您应该会看到带有演示密钥集的 已创建: 一个 PubNub 帐户。 这里 演示应用程序 然后滚动到配置选项卡。对于我们的演示,我们需要激活 和 权限。对于 权限,我们将使用以下事件: 、 和 。 选择演示键集, Files Objects Object User Metadata Events Channel Metadata Events Membership Events 创建并配置密钥集后, 我们需要将其复制到前端。 让我们在 文件夹中创建一个名为 的 JSON 文件。我们将使用此文件来获取我们需要的所有 API 密钥。从 PubNub 键集开始: src/ settings.json { "PUBNUB_PUB_KEY": "pub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "PUBNUB_SUB_KEY": "sub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" } 使用 PubNub 构建基本聊天 💬 我们将使用 PubNub 来执行几乎 我们的后端仅处理用户注册/登录,并使用仅包含 ID、姓名和电子邮件的极简用户模型。 所有后端任务。 在前端,我们需要一个小的身份验证接口: 用户拥有帐户后,他们首先需要的是 PubNub SDK 的实例。 我们需要提供 UUID。 为了识别 Pubnub 上的用户, 为了简单起见,我们将使用与后端相同的 ID: /* frontend/src/App.js */ import settings from './settings.json' // our settings file for API keys /* ... */ const pubnub = new PubNub({ publishKey: settings.PUBNUB_PUB_KEY, subscribeKey: settings.PUBNUB_SUB_KEY, uuid: currentUser.id }) 为了使我们的后端尽可能简单,我们将使用 PubNub 的用户元数据来交换用户信息。 我们只需调用 PubNub 函数: 在 SDK 实例化之后, setUUIDMetadata /* frontend/src/App.js */ await pubnub.objects.setUUIDMetadata({ uuid: currentUser.id, data: { email: currentUser.emailAddress, name: currentUser.name } }) 获取初始应用状态🌱 使用 PubNub 要做的 是检索所有现有成员并将它们存储在我们的本地数据存储中: 第一件事 /* frontend/src/App.js */ const existingMembers = await pubnub.objects.getAllUUIDMetadata() dispatch({ type: SET_USERS, payload: { users: existingMembers.data.map(u => new User({ id: u.id, name: u.name, emailAddress: u.email })) } }) 每个聊天室都会对应一个 PubNub 频道。我们还将向每个频道添加一些元数据: :创建房间的用户的 ID。 ownerId :用于区分直接消息传递房间和群组房间的布尔值。 one2one :布尔值,用于隐藏已删除的群组房间。 archived 稍后添加 Seald SDK 时将使用 数据。 PubNub 没有所有权概念,但 Seald 有。它将定义谁可以在频道中添加或删除用户。它基本上定义了一个组管理员。 ownerId 我们将从 我们还需要房间元数据,因此我们需要包含自定义字段。然后,我们需要过滤掉存档的房间并将所有内容发送到我们的数据存储。 检索现有的聊天室开始。 最后,我们 与房间关联的 PubNub 频道,因此我们将收到新消息: subscribe /* frontend/src/App.js */ // Retrieve rooms of which we are members const memberships = await pubnub.objects.getMemberships({ include: { customChannelFields: true } }) const knownRooms = [] // For each room, retrieve room members for (const room of memberships.data.filter(r => !r.channel.custom.archived)) { const roomMembers = await pubnub.objects.getChannelMembers({ channel: room.channel.id }) knownRooms.push(new Room({ id: room.channel.id, name: room.channel.name, users: roomMembers.data.map(u => u.uuid.id), ownerId: room.channel.custom.ownerId, one2one: room.channel.custom.one2one })) } // Store rooms in our data store dispatch({ type: SET_ROOMS, payload: { rooms: knownRooms } }) // Subscribe to channels to get new messages pubnub.subscribe({ channels: knownRooms.map(r => r.id) }) 现在我们已经获取了我们所在的所有房间。我们需要最后一件事来完成应用程序初始化:确保我们与所有其他成员(包括新注册的成员)都有一个 房间。 one2one 对于每个新找到的用户, 一个新房间并发送一条问候消息。 我们将创建 然后,我们将设置房间的元数据,并订阅它: /* frontend/src/App.js */ // Ensure that we have a one2one room with everyone const one2oneRooms = knownRooms.filter(r => r.one2one) for (const m of existingMembers.data.filter(u => u.id!== currentUser.id)) { if (!one2oneRooms.find(r => r.users.includes(m.id))) { // New user found: generating a new one2one room const newRoomId = PubNub.generateUUID() const newRoom = new Room({ id: newRoomId, users: [currentUser.id, m.id], one2one: true, name: m.name, ownerId: currentUser.id }) // Add the new room to our local list dispatch({ type: EDIT_OR_ADD_ROOM, payload: { room: new Room({ id: newRoomId, users: [currentUser.id, m.id], one2one: true, name: m.name, ownerId: currentUser.id }) } }) // Publish a "Hello" message in the room await pubnub.publish({ channel: newRoomId, message: { type: 'message', data: (await sealdSession.encryptMessage('Hello 👋')) } }) // Subscribe to the new room pubnub.subscribe({ channels: [newRoomId] }) await pubnub.objects.setChannelMetadata({ channel: newRoomId, data: { name: 'one2one', custom: { one2one: true, ownerId: currentUser.id, }, } }) await pubnub.objects.setChannelMembers({ channel: newRoomId, uuids: [currentUser.id, m.id] }) } } 一旦完成所有这些,我们的初始应用程序状态 但是,我们需要保持最新状态。 就已完全定义。 只需添加 事件的事件侦听器即可完成: membership /* frontend/src/App.js */ pubnub.addListener({ objects: async function(objectEvent) { if (objectEvent.message.type === 'membership') { if (objectEvent.message.event === 'delete') { // User is removed from a room /* Removing the room from store... */ } if (objectEvent.message.event === 'set') { // User is added to a room const metadata = await pubnub.objects.getChannelMetadata({ channel: objectEvent.message.data.channel.id }) const roomMembers = (await pubnub.objects.getChannelMembers({ channel: objectEvent.message.data.channel.id })).data.map(u => u.uuid.id) /* Adding new room to store + subscribing to new room channel... */ } } } }) pubnub.subscribe({ channels: [currentUser.id] }) // channel on which events concerning the current user are published 我们现在可以看看 聊天室本身。然后我们将处理团体房间。 one2one 在聊天室中接收和发送消息 📩 在 文件中,我们将拥有显示聊天室消息的所有逻辑。 chat.js 我们唯一需要做的就是获取所有预先存在的消息。 要初始化这个房间, 只需知道房间 ID 即可完成: /* frontend/src/components/Chat.jsx */ const fetchedMessages = (await pubnub.fetchMessages({ channels: [currentRoomId] })).channels[currentRoomId] 我们可以订阅频道来获取新消息,并添加监听器来实时显示它们: /* frontend/src/components/Chat.jsx */ pubnub.addListener({ message: handleReceiveMessage }) pubnub.subscribe({ channels: [currentRoomId] }) 要发送消息,我们只需将其发布到频道上即可: /* frontend/src/components/Chat.jsx */ const handleSubmitMessage = async e => { /* Some checks that the room is in a correct state... */ await pubnub.publish({ channel: state.room.id, message: { type: 'message', data: state.message } }) } 我们首先将其上传到 PubNub。然后我们将获取上传的文件URI,并将其作为消息发布在聊天室中: 要发送文件, /* frontend/src/components/UploadButton.jsx */ // Upload Encrypted file const uploadData = await pubnub.sendFile({ channel: room.id, file: myFile, storeInHistory: false }) const fileURL = await pubnub.getFileUrl({ id: uploadData.id, name: uploadData.name, channel: room.id }) await pubnub.publish({ channel: state.room.id, message: { type: 'file', url: fileURL, fileName: await sealdSession.encryptMessage(selectedFiles[0].name) } }) 管理群聊👨👩👦👦 要创建和管理组,我们需要一个用于选择用户的界面: 我们可以为我们的房间创建一个 PubNub 频道,然后为该频道设置元数据和成员资格。该代码与 one2one rooms 的代码非常相似,因此我们在此不再重复。 选择群组成员后, 我们现在有了一个完整的聊天应用程序。 让我们为每条消息添加端到端加密! 使用 Seald 添加端到端加密 🔒💬 设立一个 Seal 账户 👤 要开始使用 Seald, 请创建一个免费试用帐户 这里 登陆 Seald 仪表板时,会显示一些 URL 和 API 令牌。 获取以下元素: 应用程序ID 接口地址 密钥存储URL 我们将这些键添加到我们的 中: settings.json { "PUBNUB_PUB_KEY": "pub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "PUBNUB_SUB_KEY": "sub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "SEALD_APP_ID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "SEALD_API_URL": "https://api.staging-0.seald.io", "SEALD_KEYSTORAGE_URL": "https://ssks.staging-0.seald.io" } 每个用户在注册时都需要一个许可证 JWT。 为了能够使用 Seald SDK, 这些 JWT 需要在后端使用秘密和秘密 ID 生成。 从仪表板登录页面,将 JWT 密钥及其关联 ID 复制到 中: backend/settings.json { "SEALD_JWT_SECRET_ID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "SEALD_JWT_SECRET": "XXXXXXXXXXXXXXXXXXXXXXXX" } 在注册 API 调用期间,我们将生成 Seald 许可证 JWT,并将其返回: /* backend/routes/account.js */ const token = new SignJWT({ iss: settings.SEALD_JWT_SECRET_ID, jti: uuidv4(), /// Random string with enough entropy to never repeat. iat: Math.floor(Date.now() / 1000), // JWT valid only for 10 minutes. `Date.now()` returns the in milliseconds, this needs it in seconds. scopes: [3], // PERMISSION_JOIN_TEAM join_team: true }) .setProtectedHeader({ alg: 'HS256' }) const signupJWT = await token.sign(Buffer.from(settings.SEALD_JWT_SECRET, 'ascii')) 有关这方面的更多信息,请参阅 。 我们关于 JWT 的文档文章 然后,我们需要 安装 Seald SDK。 来识别 Seald 服务器上的用户。为此,我们将使用 包。 我们还需要安装一个插件 sdk-plugin-ssks-password 该插件允许对我们的用户进行简单的密码验证: npm i -S @seald-io/sdk @seald-io/sdk-plugin-ssks-password 实例化 Seald SDK 💡 然后,我们将创建一个 文件。在此文件中,我们将首先创建一个函数来实例化 Seald SDK: seald.js /* frontend/src/services/seald.js */ import SealdSDK from '@seald-io/sdk-web' import SealdSDKPluginSSKSPassword from '@seald-io/sdk-plugin-ssks-password' import settings from './settings.json' let sealdSDKInstance = null const instantiateSealdSDK = async () => { sealdSDKInstance = SealdSDK({ appId: settings.SEALD_APP_ID, apiURL: settings.SEALD_API_URL, plugins: [SealdSDKPluginSSKSPassword(settings.SEALD_KEYSTORAGE_URL)] }) } 创建和检索 Seald 身份 🔑 在 中,我们还将添加两个函数:一个用于创建身份,一个用于检索身份。 我们还需要创建帐户时返回的许可证 JWT: seald.js 要创建身份, /* frontend/src/services/seald.js */ export const createIdentity = async ({ userId, password, signupJWT }) => { await instantiateSealdSDK() await sealdSDKInstance.initiateIdentity({ signupJWT }) await sealdSDKInstance.ssksPassword.saveIdentity({ userId, password }) } export const retrieveIdentity = async ({ userId, password }) => { await instantiateSealdSDK() await sealdSDKInstance.ssksPassword.retrieveIdentity({ userId, password }) } 在我们的注册和登录流程中,我们只需要在用户登录后调用这些函数。 此时,每次我们的用户连接时,他都有一个工作的 Seald SDK, 准备加密和解密! 为了获得适当的安全性,应在将密码发送到身份验证服务器之前对其进行预哈希处理。有关这方面的更多详细信息,请参阅 。 警告: 我们文档中有关密码验证的段落 开始加密和解密消息🔒🔓 每个聊天室都将与 Seald SDK 上的一个 相关联。 encryptionSession 每次我们创建聊天室时, 然后使用它: 我们只需添加一行来创建加密会话, /* frontend/src/App.js */ // Create a Seald session const sealdSession = await getSealdSDKInstance().createEncryptionSession( { userIds: [currentUser.id, m.id] }, { metadata: newRoomId } ) // Publish a "Hello" message in the room await pubnub.publish({ channel: newRoomId, message: { type: 'message', data: (await sealdSession.encryptMessage('Hello 👋')) } }) 请注意,默认情况下,我们的用户作为 encryptionSession. 当访问房间时,我们需要获取对应的 。可以从任何加密的消息或文件中检索它。一旦我们有了它,我们将把它保存在组件引用中。 encryptionSession 然后我们可以简单地使用 、 、 和 函数。 session.encryptMessage session.encryptFile session.decryptMessage session.decryptFile 让我们从消息开始。发送消息: /* frontend/src/components/Chat.js */ const handleSubmitMessage = async m => { /* Some validation that we are in a valid room... */ // if there is no encryption session set in cache yet, create one // (should never happen, as a "Hello" is sent on room creation) if (!sealdSessionRef.current) { sealdSessionRef.current = await getSealdSDKInstance().createEncryptionSession( { userIds: state.room.users }, { metadata: state.room.id } ) } // use the session to encrypt the message we are trying to send const encryptedMessage = await sealdSessionRef.current.encryptMessage(state.message) // publish the encrypted message to pubnub await pubnub.publish({ channel: state.room.id, message: { type: 'message', data: encryptedMessage } }) /* Some cleanup... */ } 当我们收到一条消息时: /* frontend/src/components/Chat.js */ const decryptMessage = async m => { /* Filter out files... */ let encryptedData = m.message.data if (!sealdSessionRef.current) { // no encryption session set in cache yet // we try to get it by parsing the current message sealdSessionRef.current = await getSealdSDKInstance().retrieveEncryptionSession({ encryptedMessage: encryptedData }) // now that we have a session loaded, let's decrypt } const decryptedData = await sealdSessionRef.current.decryptMessage(encryptedData) // we have successfully decrypted the message return { ...m, uuid: m.uuid || m.publisher, value: decryptedData } /* Some error handling... */ } /* Other stuff... */ const handleReceiveMessage = async m => { const decryptedMessage = await decryptMessage(m) setState(draft => { draft.messages = [...draft.messages, decryptedMessage] }) } 此外,我们在打开房间时使用此 函数来解密会话中已有的所有消息: decryptMessage /* frontend/src/components/Chat.js */ const fetchedMessages = (await pubnub.fetchMessages({ channels: [currentRoomId] })).channels[currentRoomId] const clearMessages = fetchedMessages ? await Promise.all(fetchedMessages.map(decryptMessage)) : [] 现在是文件。上传文件: /* frontend/src/components/UploadButton.js */ // Encrypt file const encryptedBlob = await sealdSession.encryptFile( selectedFiles[0], selectedFiles[0].name, { fileSize: selectedFiles[0].size } ) const encryptedFile = new File([encryptedBlob], selectedFiles[0].name) // Upload Encrypted file const uploadData = await pubnub.sendFile({ channel: room.id, file: encryptedFile, storeInHistory: false }) 并解密文件: /* frontend/src/components/Message.js */ const onClick = async () => { if (state.data.type === 'file') { const response = await fetch(state.data.url) const encryptedBlob = await response.blob() const { data: clearBlob, filename } = await sealdSession.decryptFile(encryptedBlob) const href = window.URL.createObjectURL(clearBlob) /* Create an <a> element and simulate a click on it to download the created objectURL */ } } 管理群组成员👨👩👦 群聊也将有其 。每次创建一个组时,我们都需要创建一个: encryptionSession /* frontend/src/components/ManageDialogRoom.js.js */ // To create the encryptionSession const sealdSession = await getSealdSDKInstance().createEncryptionSession( { userIds: dialogRoom.selectedUsersId }, { metadata: newRoomId } ) 然后, 我们都需要添加或删除组成员: 每次修改组成员时, /* frontend/src/components/ManageDialogRoom.js.js */ // we compare old and new members to figure out which ones were just added or removed const usersToRemove = dialogRoom.room.users.filter(id => !dialogRoom.selectedUsersId.includes(id)) const usersToAdd = dialogRoom.selectedUsersId.filter(id => !dialogRoom.room.users.includes(id)) if (usersToAdd.length > 0) { // for every added user, add them to the Seald session await dialogRoom.sealdSession.addRecipients({ userIds: usersToAdd }) // then add them to the pubnub channel await pubnub.objects.setChannelMembers({ channel: dialogRoom.room.id, uuids: usersToAdd }) } if (usersToRemove.length > 0) { // for every removed user, revoke them from the Seald session await dialogRoom.sealdSession.revokeRecipients({ userIds: usersToRemove }) // then remove them from the pubnub channel for (const u of usersToRemove) { await pubnub.objects.removeMemberships({ channels: [dialogRoom.room.id], uuid: u }) } } 结论✅ 一旦完成,我们就完成了! 我们仅用几行代码就成功地 。 将 Seald 集成到 PubNub 中 既然 您可以向用户保证, 聊天是端到端加密的, 即使在数据泄露的情况下,他们的数据也将保持机密。 一如既往, 如果您需要任何额外的指导。 不要犹豫 联系我们 迫不及待地想看看您构建的内容 🥳。 也发布 在这里。