Hola 👋
Hoy, descubramos cómo crear un chat cifrado de extremo a extremo con Pubnub y el SDK sellado.
👉 Puede encontrar un ejemplo completamente funcional de cómo usar PubNub y Seald aquí:
¿Qué es Pubnub?
PubNub es un servicio de comunicación en tiempo real que se puede integrar en la mayoría de las aplicaciones. Fiable y escalable, se puede integrar fácilmente con los marcos más comunes.
¿Qué es Seald?
Seald.io ofrece un SDK que permite realizar cifrado de extremo a extremo , con funciones de gestión avanzadas, sin ningún conocimiento criptográfico previo. Este SDK se puede integrar en aplicaciones web, backend, móviles o de escritorio.
El cifrado de extremo a extremo ofrece el más alto nivel de privacidad y seguridad. Permite cifrar datos confidenciales tan pronto como se recopilan. Un cifrado temprano reducirá la superficie de ataque de su aplicación. Otra ventaja es que puedes gestionar con precisión quién puede acceder a los datos. Esto también protegerá cuando no esté dentro de su alcance, como servicios de terceros.
El cifrado de extremo a extremo le permite mantener el control en todo momento, cuando sus datos están en tránsito, cuando están en reposo e incluso cuando no están en su mano. Por tanto, ofrece una protección mucho más amplia que otras tecnologías de cifrado (TLS, cifrado de disco completo,...).
Siempre que se enfrente a un caso en el que el cumplimiento es importante (GDPR, HIPAA, SOC-2,...) o en el que tenga datos confidenciales (médicos, de defensa,...), el cifrado de extremo a extremo es imprescindible. Pero incluso para datos más comunes, es una buena práctica tenerlos. Una violación de datos es un evento devastador que cada vez es más frecuente.
PubNub SDK ofrece un enlace de cifrado simple, utilizando el argumento cipherKey
al crear una instancia de su SDK. Al hacerlo, se garantizará que todos los mensajes cargados estén cifrados antes de enviarse. Sin embargo, usted mismo debe realizar la administración de claves, que es la parte más difícil de un sistema cifrado de extremo a extremo.
Las vulnerabilidades rara vez provienen del cifrado en sí, sino más bien de la filtración de claves a través de fallas en el modelo de seguridad.
El uso de un servicio de terceros para su seguridad le permite no tener un único punto de falla. Seald.io propone un modelo de seguridad robusto, certificado por ANSSI, con control de gestión de acceso en tiempo real, revocación y recuperación de usuarios, 2FA y más.
Este artículo explica cómo integrar Seald con PubNub paso a paso para proteger su chat con cifrado de extremo a extremo. Crearemos una aplicación de mensajería de ejemplo, con las siguientes características:
Salas de chat individuales y grupales.
Cada miembro tiene una sala de chat dedicada con todos los demás usuarios.
Cualquiera puede crear una sala de chat grupal con varios otros usuarios.
Utilice cifrado de extremo a extremo para cada mensaje y archivo enviado.
Permitir la gestión de acceso en tiempo real a los chats.
Para comenzar, necesitará una cuenta PubNub. Puedes registrarte
Seleccione el conjunto de claves de demostración y desplácese hasta la pestaña de configuración. Para nuestra demostración, necesitamos activar los permisos Files
y Objects
. Para el permiso Object
, usaremos los siguientes eventos: User Metadata Events
, Channel Metadata Events
y Membership Events
.
Una vez creado y configurado el conjunto de claves, debemos copiarlo a nuestra interfaz.
Creemos un archivo JSON en la carpeta src/
, llamado settings.json
. Usaremos este archivo para todas las claves API que necesitemos. Comenzando con el conjunto de claves de PubNub:
{ "PUBNUB_PUB_KEY": "pub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "PUBNUB_SUB_KEY": "sub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" }
Usaremos PubNub para casi todas las tareas de backend. Nuestro backend solo manejará el registro/inicio de sesión de usuarios y utilizará un modelo de usuario minimalista con solo una identificación, un nombre y un correo electrónico.
En el frente, necesitamos una pequeña interfaz de autenticación:
Una vez que el usuario tiene una cuenta, lo primero que necesita es una instancia del SDK de PubNub.
Para identificar al usuario en Pubnub, debemos proporcionar un UUID.
Para simplificar las cosas, usaremos la misma ID que en nuestro 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 })
Para mantener nuestro backend lo más simple posible, usaremos los metadatos de usuario de PubNub para intercambiar información de los usuarios.
Justo después de la creación de instancias del SDK, simplemente llamamos a la función PubNub setUUIDMetadata
:
/* frontend/src/App.js */ await pubnub.objects.setUUIDMetadata({ uuid: currentUser.id, data: { email: currentUser.emailAddress, name: currentUser.name } })
Lo primero que debe hacer con PubNub es recuperar todos los miembros existentes y almacenarlos en nuestro almacén de datos local:
/* 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 })) } })
Cada sala de chat corresponderá a un canal de PubNub. También agregaremos algunos metadatos a cada canal:
ownerId
: el ID del usuario que creó la sala.
one2one
: Un booleano para diferenciar salas de mensajería directa y salas de grupos.
archived
: un booleano, para ocultar una sala de grupo eliminada.
Los metadatos ownerId
se utilizarán más adelante al agregar el SDK de Seald. PubNub no tiene un concepto de propiedad, pero Seald sí. Definirá quién puede agregar o eliminar usuarios de un canal. Básicamente define un administrador de grupo.
Comenzaremos recuperando las salas de chat existentes. También necesitaremos los metadatos de la sala, por lo que debemos incluir campos personalizados. Luego, debemos filtrar las salas archivadas y enviar todo a nuestro almacén de datos.
Finalmente, nos subscribe
al canal PubNub asociado a la sala, por lo que recibiremos nuevos mensajes:
/* 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) })
Ahora hemos recuperado todas las salas en las que nos encontramos. Necesitamos una última cosa para finalizar la inicialización de la aplicación: asegurarnos de tener una sala one2one
con todos los demás miembros, incluidos los recién registrados.
Para cada usuario recién encontrado, crearemos una nueva sala y le enviaremos un mensaje de saludo.
Luego, configuraremos los metadatos de la sala y nos suscribiremos a ella:
/* 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] }) } }
Una vez hecho todo esto, el estado inicial de nuestra aplicación estará completamente definido. Sin embargo, debemos mantenerlo actualizado.
Se puede hacer simplemente agregando un detector de eventos para eventos 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
Ahora podemos echar un vistazo a la propia sala de chat one2one
. Luego nos ocuparemos de las salas de grupos.
En un archivo chat.js
tendremos toda la lógica para mostrar los mensajes de una sala de chat.
Para inicializar esta sala, lo único que debemos hacer es recuperar todos los mensajes preexistentes.
Se puede hacer simplemente conociendo el ID de la habitación:
/* frontend/src/components/Chat.jsx */ const fetchedMessages = (await pubnub.fetchMessages({ channels: [currentRoomId] })).channels[currentRoomId]
Podemos suscribirnos al canal para recibir nuevos mensajes y agregar un oyente para mostrarlos en tiempo real:
/* frontend/src/components/Chat.jsx */ pubnub.addListener({ message: handleReceiveMessage }) pubnub.subscribe({ channels: [currentRoomId] })
Para enviar un mensaje simplemente debemos publicarlo en el canal:
/* 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 } }) }
Para enviar un archivo, primero lo subiremos a PubNub. Luego obtendremos el URI del archivo cargado y lo publicaremos como un mensaje en la sala de chat:
/* 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) } })
Para crear y administrar grupos, necesitaremos una interfaz para seleccionar usuarios:
Una vez seleccionados los miembros del grupo, podemos crear un canal PubNub para nuestra sala y luego configurar los metadatos y la membresía del canal. El código es muy similar al que se hace para las habitaciones one2one, por lo que no lo repetiremos aquí.
Ahora tenemos una aplicación de chat completa. ¡Agreguemos cifrado de extremo a extremo para cada mensaje!
Para comenzar con Seald, cree una cuenta de prueba gratuita
Al acceder al panel de Seald, se muestran algunas URL y tokens API.
Obtenga los siguientes elementos:
Agregaremos estas claves a nuestro 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" }
Para poder utilizar Seald SDK, cada usuario necesita una licencia JWT al registrarse.
Estos JWT deben generarse en el backend utilizando un secreto y una ID secreta.
Desde la página de inicio del panel, copie el secreto JWT y su ID asociado en backend/settings.json
:
{ "SEALD_JWT_SECRET_ID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "SEALD_JWT_SECRET": "XXXXXXXXXXXXXXXXXXXXXXXX" }
Durante la llamada API de registro, generaremos una licencia JWT de Seald y la devolveremos:
/* 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'))
Para obtener más información sobre esto, consulte
Luego, necesitamos instalar el SDK de Seald.
También necesitamos instalar un complemento para identificar a los usuarios en el servidor Seald. Para hacerlo, usaremos el paquete sdk-plugin-ssks-password
.
Este complemento permite una autenticación de contraseña simple de nuestros usuarios:
npm i -S @seald-io/sdk @seald-io/sdk-plugin-ssks-password
Luego, crearemos un archivo seald.js
. En este archivo, comenzaremos creando una función para crear una instancia del SDK de Seald:
/* 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)] }) }
En seald.js
, también agregaremos dos funciones: una para crear una identidad y otra para recuperar una. Para crear una identidad, también necesitamos la licencia JWT devuelta al crear la cuenta:
/* 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 }) }
Durante nuestros flujos de registro e inicio de sesión, solo necesitamos llamar a estas funciones después de que el usuario haya iniciado sesión.
En este punto, cada vez que nuestro usuario se conecta, tiene un SDK de Seald en funcionamiento, ¡listo para cifrar y descifrar!
Advertencia: Para una seguridad adecuada, la contraseña debe tener un hash previo antes de enviarse al servidor de autenticación. Para obtener más detalles sobre esto, consulte
Cada sala de chat se asociará con una encryptionSession
en el SDK de Seald.
Cada vez que creamos una sala de chat, simplemente debemos agregar una línea para crear la sesión de cifrado y luego usarla:
/* 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 👋')) } })
Tenga en cuenta que nuestro usuario está incluido de forma predeterminada como destinatario de la encryptionSession.
Al acceder a una sala, necesitamos obtener la encryptionSession
correspondiente. Se puede recuperar de cualquier mensaje o archivo cifrado. Una vez que lo tengamos, lo mantendremos como referencia de componentes.
Entonces podemos simplemente usar las funciones session.encryptMessage
, session.encryptFile
, session.decryptMessage
y session.decryptFile
.
Empecemos por los mensajes. Para enviar un mensaje:
/* 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... */ }
Y cuando recibimos un mensaje:
/* 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] }) }
Además, utilizamos esta función decryptMessage
para descifrar todos los mensajes que ya están en la sesión al abrir una sala:
/* frontend/src/components/Chat.js */ const fetchedMessages = (await pubnub.fetchMessages({ channels: [currentRoomId] })).channels[currentRoomId] const clearMessages = fetchedMessages ? await Promise.all(fetchedMessages.map(decryptMessage)) : []
Y ahora los archivos. Para cargar un archivo:
/* 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 })
Y para descifrar un archivo:
/* 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 */ } }
Los chats grupales también tendrán su encryptionSession
. Cada vez que se crea un grupo, debemos crear uno:
/* frontend/src/components/ManageDialogRoom.js.js */ // To create the encryptionSession const sealdSession = await getSealdSDKInstance().createEncryptionSession( { userIds: dialogRoom.selectedUsersId }, { metadata: newRoomId } )
Luego, cada vez que modifiquemos miembros del grupo, necesitaremos agregarlos o eliminarlos del mismo:
/* 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 }) } }
Una vez hecho esto, ¡habremos terminado!
Logramos integrar Seald en PubNub con solo unas pocas líneas de código.
Ahora que el chat está cifrado de extremo a extremo, puede asegurar a sus usuarios que sus datos seguirán siendo confidenciales, incluso en caso de vulneración de datos.
Como siempre, no dudes en
No puedo esperar a ver lo que has creado 🥳.
También publicado aquí.