Hello 👋 Today, let's discover with and the how to build an end-to-end chat encrypted Pubnub seald SDK. 👉 A fully working example of how to use PubNub and Seald can be found here: https://github.com/seald/pubnub-example-project What's Pubnub? PubNub is a real-time communication service that can be integrated into most applications. Reliable and scalable, it can easily be integrated with the most common frameworks. What's Seald? Seald.io offers an SDK that allows you to carry out , with advanced management features, This SDK can be integrated into web, backend, mobile, or desktop applications. end-to-end encryption without any prior cryptographic knowledge. Content Overview Why use end-to-end encryption? Why use Seald.io instead of PubNub encryption hook ?👀 Goals 🏆 Implementation 🧠 Adding end-to-end encryption with Seald 🔒💬 Conclusion ✅ Why use end-to-end encryption? 🔒 End-to-end encryption offers It allows encrypting sensitive data as soon as they are collected. An early encryption will reduce the attack surface of your app. Another advantage is that you can precisely This will also protect when it is not in your scope, the highest level of privacy and security. manage who can access the data. like third-party services. End-to-end encryption allows you when your data is in transit, when it is at rest, and even when it is not in your hand. Thus, it offers far wider protection than other encryption technologies (TLS, full-disk encryption, ...). to keep control at all times, Whenever you face a case where (GDPR, HIPAA, SOC-2,...) or where you have (medical, defense,...), end-to-end encryption is a must-have. But even for more commonplace data, it's good practice to have. A data breach is a devastating event that is becoming is important compliance sensitive data more and more frequent. Why use Seald.io instead of PubNub encryption hook ?👀 PubNub SDK offers a simple encryption hook, using the argument when instantiating its SDK. Doing so will ensure that all uploaded messages are encrypted However, you have to do the key management yourself, which is of an end-to-end encrypted system. cipherKey before being sent. the hardest part Vulnerabilities rarely come from the encryption itself, but most often through flaws in the security model. from keys being leaked Using a third-party service for your security allows you Seald.io proposes a robust security model, with real-time access-management control, user revocation and recovery, 2FA, and more. to not have a single point of failure. certified by the ANSSI, Goals 🏆 This article explains how to integrate Seald with PubNub in order to secure your chat with end-to-end encryption. We will build with the following features: step-by-step, an example messaging app, One-to-one and group chat rooms. Each member has a dedicated chat room with every other user. Anyone can create a group chat room with multiple other users. Use end-to-end encryption for every message and file sent. Allow real-time access management to chats. Implementation 🧠 Set up a PubNub account 👤 To get started, you will need You can sign up . Once logged in on your dashboard, you should see that with a demo keyset has been created: a PubNub account. here a demo app and scroll to the configuration tab. For our demo, we need to activate and permissions. For the permission, we will use the following events: , and . Select the demo keyset, Files Objects Object User Metadata Events Channel Metadata Events Membership Events Once the keyset is created and configured, we need to copy it to our frontend. Let's create a JSON file on the folder, called . We will use this file for all the API keys we will need. Starting with the PubNub keyset: src/ settings.json { "PUBNUB_PUB_KEY": "pub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "PUBNUB_SUB_KEY": "sub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" } Building a basic chat using PubNub 💬 We will use PubNub for almost Our backend will only handle user sign-up/sign-in, and use a minimalist user model with only an ID, a name, and an email. every backend task. On the front side, we need a small authentication interface: Once the user has an account, the first thing they need is an instance of the PubNub SDK. we need to provide a UUID. To identify the user on Pubnub, To keep things simple, we will use the same ID as on our backend: /* 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 }) To keep our backend as simple as possible, we will use PubNub's user metadata to exchange users' info. we simply call PubNub function: Just after the SDK instantiation, setUUIDMetadata /* frontend/src/App.js */ await pubnub.objects.setUUIDMetadata({ uuid: currentUser.id, data: { email: currentUser.emailAddress, name: currentUser.name } }) Getting initial app state 🌱 to do with PubNub is to retrieve all existing members and store them in our local data store: The first thing /* 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 })) } }) Each chat room will correspond to a PubNub channel. We will also add some metadata to each channel: : The ID of the user who created the room. ownerId : A boolean to differentiate direct messaging rooms and group rooms. one2one : A boolean, to hide a deleted group room. archived The metadata will be used later when adding the Seald SDK. PubNub doesn't have an ownership concept, but Seald does. It will define who can add or remove users from a channel. It basically defines a group administrator. ownerId We will start by We will also need the room metadata, so we need to include custom fields. Then, we need to filter out archived rooms and send everything to our data store. retrieving existing chat rooms. Finally, we to the PubNub channel associated with the room, so we will receive new messages: 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) }) Now we have fetched all the rooms we are in. We need one last thing to finish app initialization: ensuring we have a room with every other member, including newly registered ones. one2one For every newly found user, a new room and send a hello message. we will create Then, we will set the room's metadata, and subscribe to it: /* 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] }) } } Once all that is done, our initial app state However, we need to keep it up to date. is fully defined. It can be done simply by adding an event listener for events: 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 We can now have a look at a chat room itself. Then we will handle group rooms. one2one Receiving and sending messages in a chat room 📩 In a file, we will have all the logic to display the messages of a chat room. chat.js the only thing we need to do is to fetch all pre-existing messages. To initialize this room, It can be done simply by knowing the room ID: /* frontend/src/components/Chat.jsx */ const fetchedMessages = (await pubnub.fetchMessages({ channels: [currentRoomId] })).channels[currentRoomId] We can subscribe to the channel to get new messages, and add a listener to display them in real-time: /* frontend/src/components/Chat.jsx */ pubnub.addListener({ message: handleReceiveMessage }) pubnub.subscribe({ channels: [currentRoomId] }) To send a message, we simply need to publish it on the channel: /* 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 } }) } we will first upload it to PubNub. Then we will get the uploaded file URI, and publish it as a message in the chat room: To send a file, /* 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) } }) Managing group chat 👨👩👦👦 To create and manage groups, we will need an interface for selecting users: we can create a PubNub channel for our room, and then set metadata and membership for the channel. The code is very similar to what is done for one2one rooms, so we will not repeat it here. Once the group members have been selected, We now have a full chat app. Let's add end-to-end encryption for every message! Adding end-to-end encryption with Seald 🔒💬 Set up a Seald account 👤 To start with Seald, create a free trial account here When landing on the Seald dashboard, a few URLs and API tokens are displayed. Get the following elements: appId apiURL keyStorageURL We will add these keys to our : 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" } every user needs a licence JWT when signing-up. To be able to use the Seald SDK, These JWT needs to be generated on the backend using a secret and a secret ID. From the dashboard landing page, copy the JWT secret and its associated ID in : backend/settings.json { "SEALD_JWT_SECRET_ID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "SEALD_JWT_SECRET": "XXXXXXXXXXXXXXXXXXXXXXXX" } During the signup API call, we will generate a Seald licence JWT, and return it back: /* 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')) For more information about this, see . our documentation article about JWT Then, we need to install the Seald SDK. to identify users on the Seald server. To do so, we will use the package. We also need to install a plugin sdk-plugin-ssks-password This plugin allows for simple password authentication of our users: npm i -S @seald-io/sdk @seald-io/sdk-plugin-ssks-password Instantiating the Seald SDK 💡 Then, we will create a file. In this file, we will start by creating a function to instantiate the 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)] }) } Creating and retrieving Seald identities 🔑 In , we will also add two function: one to create an identity and one to retrieve one. we also need the licence JWT returned at account creation: seald.js To create an identity, /* 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 }) } During our signup and sign-in flows, we just need to call these functions after the user has logged in. At this point, every time our user is connected, he has a working Seald SDK, ready to encrypt and decrypt! For proper security, the password should be pre-hashed before being sent to the authentication server. For more details on this, please see . Warning: the paragraph about password authentication in our documentation Start encrypting and decrypting messages 🔒🔓 Each chat room will be associated with an on the Seald SDK. encryptionSession Every time we create a chat room, then use it: we simply have to add one line to create the encryption session, /* 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 👋')) } }) Note that our user is included by default as a recipient of the encryptionSession. When accessing a room, we need to get the corresponding . It can be retrieved from any encrypted messages or file. Once we have it, we'll keep it in component reference. encryptionSession Then we can simply use , , and functions. session.encryptMessage session.encryptFile session.decryptMessage session.decryptFile Let's start with the messages. To send a message: /* 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... */ } And when we receive a message: /* 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] }) } Also, we use this function to decrypt all messages already in the session when opening a room: decryptMessage /* frontend/src/components/Chat.js */ const fetchedMessages = (await pubnub.fetchMessages({ channels: [currentRoomId] })).channels[currentRoomId] const clearMessages = fetchedMessages ? await Promise.all(fetchedMessages.map(decryptMessage)) : [] And now for files. To upload a file: /* 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 }) And to decrypt a file: /* 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 */ } } Managing group members 👨👩👦 Group chats will also have their . Every time a group is created, we need to create one: encryptionSession /* frontend/src/components/ManageDialogRoom.js.js */ // To create the encryptionSession const sealdSession = await getSealdSDKInstance().createEncryptionSession( { userIds: dialogRoom.selectedUsersId }, { metadata: newRoomId } ) Then, we will need to add or remove them from it: every time we modify group members, /* 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 }) } } Conclusion ✅ Once that's done, we're finished! We managed with only a few lines of code. to integrate Seald into PubNub Now that you can assure your users that their data will remain confidential, the chat is end-to-end encrypted, even in case of data breaches. As always, if you need any additional guidance. don’t hesitate to contact us Can’t wait to see what you've built 🥳. Also published here.