你好👋
今天,我们来了解如何使用Pubnub和 seald SDK 构建端到端加密聊天。
👉 可以在此处找到如何使用 PubNub 和 Seald 的完整示例:
酒吧是什么?
PubNub 是一种实时通信服务,可以集成到大多数应用程序中。可靠且可扩展,它可以轻松地与最常见的框架集成。
什么是密封?
Seald.io 提供了一个 SDK,允许您执行端到端加密,具有高级管理功能,而无需任何先前的加密知识。该 SDK 可以集成到 Web、后端、移动或桌面应用程序中。
端到端加密提供最高级别的隐私和安全性。它允许在收集敏感数据后立即对其进行加密。早期加密将减少应用程序的攻击面。另一个优点是您可以精确管理谁可以访问数据。当它不在您的范围内时(例如第三方服务),这也将提供保护。
端到端加密使您能够始终保持控制,无论数据在传输中、静止时,甚至不在您手中时。因此,它提供了比其他加密技术(TLS、全盘加密等)更广泛的保护。
每当您遇到合规性很重要的情况(GDPR、HIPAA、SOC-2...)或您拥有敏感数据(医疗、国防...)时,端到端加密都是必备的。但即使对于更常见的数据,拥有它也是一个很好的做法。数据泄露是一种破坏性事件,而且变得越来越频繁。
PubNub SDK 提供了一个简单的加密挂钩,在实例化其 SDK 时使用cipherKey
参数。这样做将确保所有上传的消息在发送之前都经过加密。但是,您必须自己进行密钥管理,这是端到端加密系统中最困难的部分。
漏洞很少来自加密本身,但最常见的是由于安全模型中的缺陷而泄露的密钥。
使用第三方服务来保证您的安全可以让您不会出现单点故障。 Seald.io 提出了一个经过 ANSSI 认证的强大安全模型,具有实时访问管理控制、用户撤销和恢复、2FA 等功能。
本文逐步介绍如何将 Seald 与 PubNub 集成,以便通过端到端加密保护您的聊天。我们将构建一个示例消息应用程序,具有以下功能:
一对一和群聊天室。
每个成员都有一个与其他用户专用的聊天室。
任何人都可以与多个其他用户创建群聊天室。
对发送的每条消息和文件使用端到端加密。
允许对聊天进行实时访问管理。
首先,您需要一个 PubNub 帐户。您可以报名
选择演示键集,然后滚动到配置选项卡。对于我们的演示,我们需要激活Files
和Objects
权限。对于Object
权限,我们将使用以下事件: User Metadata Events
、 Channel Metadata Events
和Membership Events
。
创建并配置密钥集后,我们需要将其复制到前端。
让我们在src/
文件夹中创建一个名为settings.json
的 JSON 文件。我们将使用此文件来获取我们需要的所有 API 密钥。从 PubNub 键集开始:
{ "PUBNUB_PUB_KEY": "pub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "PUBNUB_SUB_KEY": "sub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" }
我们将使用 PubNub 来执行几乎所有后端任务。我们的后端仅处理用户注册/登录,并使用仅包含 ID、姓名和电子邮件的极简用户模型。
在前端,我们需要一个小的身份验证接口:
用户拥有帐户后,他们首先需要的是 PubNub SDK 的实例。
为了识别 Pubnub 上的用户,我们需要提供 UUID。
为了简单起见,我们将使用与后端相同的 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 的用户元数据来交换用户信息。
在 SDK 实例化之后,我们只需调用 PubNub 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 频道。我们还将向每个频道添加一些元数据:
ownerId
:创建房间的用户的 ID。
one2one
:用于区分直接消息传递房间和群组房间的布尔值。
archived
:布尔值,用于隐藏已删除的群组房间。
稍后添加 Seald SDK 时将使用ownerId
数据。 PubNub 没有所有权概念,但 Seald 有。它将定义谁可以在频道中添加或删除用户。它基本上定义了一个组管理员。
我们将从检索现有的聊天室开始。我们还需要房间元数据,因此我们需要包含自定义字段。然后,我们需要过滤掉存档的房间并将所有内容发送到我们的数据存储。
最后,我们subscribe
与房间关联的 PubNub 频道,因此我们将收到新消息:
/* 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,请创建一个免费试用帐户
登陆 Seald 仪表板时,会显示一些 URL 和 API 令牌。
获取以下元素:
我们将这些键添加到我们的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" }
为了能够使用 Seald SDK,每个用户在注册时都需要一个许可证 JWT。
这些 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'))
有关这方面的更多信息,请参阅
然后,我们需要安装 Seald SDK。
我们还需要安装一个插件来识别 Seald 服务器上的用户。为此,我们将使用sdk-plugin-ssks-password
包。
该插件允许对我们的用户进行简单的密码验证:
npm i -S @seald-io/sdk @seald-io/sdk-plugin-ssks-password
然后,我们将创建一个seald.js
文件。在此文件中,我们将首先创建一个函数来实例化 Seald SDK:
/* 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.js
中,我们还将添加两个函数:一个用于创建身份,一个用于检索身份。要创建身份,我们还需要创建帐户时返回的许可证 JWT:
/* 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 中。
既然聊天是端到端加密的,您可以向用户保证,即使在数据泄露的情况下,他们的数据也将保持机密。
一如既往,不要犹豫
迫不及待地想看看您构建的内容 🥳。
也发布在这里。