paint-brush
Cómo crear un chat cifrado de extremo a extremo con Seald y PubNubpor@seald
414 lecturas
414 lecturas

Cómo crear un chat cifrado de extremo a extremo con Seald y PubNub

por Seald16m2023/09/20
Read on Terminal Reader

Demasiado Largo; Para Leer

Este artículo le guiará en la creación de un chat cifrado de un extremo a otro utilizando PubNub y Seald SDK. Destaca la importancia del cifrado de extremo a extremo para la privacidad y la seguridad y explica por qué Seald.io es preferible al cifrado nativo de PubNub para la gestión de claves. El artículo cubre la implementación de salas de chat individuales y grupales, la autenticación de usuarios y la administración de acceso en tiempo real mediante PubNub. También describe el proceso de integración de Seald para un cifrado y descifrado sólido de mensajes y archivos dentro de la aplicación de chat. En general, proporciona una guía completa para crear una aplicación de chat segura.
featured image - Cómo crear un chat cifrado de extremo a extremo con Seald y PubNub
Seald HackerNoon profile picture
0-item
1-item

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í: https://github.com/seald/pubnub-example-project


¿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.


Descripción general del contenido

  • ¿Por qué utilizar cifrado de extremo a extremo?
  • ¿Por qué utilizar Seald.io en lugar del enlace de cifrado PubNub?👀
  • Metas 🏆
  • Implementación 🧠
  • Agregar cifrado de extremo a extremo con Seald 🔒💬
  • Conclusión ✅


¿Por qué utilizar cifrado de extremo a extremo? 🔒

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.


¿Por qué utilizar Seald.io en lugar del enlace de cifrado PubNub?👀

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.


Metas 🏆

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.


Implementación 🧠

Configura una cuenta PubNub 👤

Para comenzar, necesitará una cuenta PubNub. Puedes registrarte aquí . Una vez que haya iniciado sesión en su panel de control, debería ver que se ha creado una aplicación de demostración con un conjunto de claves de demostración:


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" }


Creando un chat básico usando PubNub 💬

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 } })


Obteniendo el estado inicial de la aplicación 🌱

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.


Recibir y enviar mensajes en una sala de chat 📩

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) } })


Administrar el chat grupal 👨‍👩‍👦‍👦

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!


Agregar cifrado de extremo a extremo con Seald 🔒💬

Configure una cuenta Seald 👤


Para comenzar con Seald, cree una cuenta de prueba gratuita aquí


Al acceder al panel de Seald, se muestran algunas URL y tokens API.


Obtenga los siguientes elementos:

  • ID de aplicación
  • apiURL
  • claveAlmacenamientoURL


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 nuestro artículo de documentación sobre JWT .


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

Creación de instancias del SDK de Seald 💡

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)] }) }


Creando y recuperando identidades Seald 🔑

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 el párrafo sobre autenticación de contraseña en nuestra documentación .

Comience a cifrar y descifrar mensajes 🔒🔓

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 */ } }


Administrar miembros del grupo 👨‍👩‍👦

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 }) } }

Conclusión ✅

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 Contáctenos si necesita alguna orientación adicional.


No puedo esperar a ver lo que has creado 🥳.




También publicado aquí.