Olá 👋
Hoje, vamos descobrir como construir um bate- papo criptografado de ponta a ponta com Pubnub e o seald SDK.
👉 Um exemplo totalmente funcional de como usar PubNub e Seald pode ser encontrado aqui:
O que é Pubnub?
PubNub é um serviço de comunicação em tempo real que pode ser integrado à maioria dos aplicativos. Confiável e escalável, pode ser facilmente integrado aos frameworks mais comuns.
O que é selo?
Seald.io oferece um SDK que permite realizar criptografia ponta a ponta , com recursos avançados de gerenciamento, sem nenhum conhecimento criptográfico prévio. Este SDK pode ser integrado a aplicativos web, back-end, móveis ou desktop.
A criptografia ponta a ponta oferece o mais alto nível de privacidade e segurança. Permite criptografar dados confidenciais assim que são coletados. Uma criptografia antecipada reduzirá a superfície de ataque do seu aplicativo. Outra vantagem é que você pode gerenciar com precisão quem pode acessar os dados. Isso também protegerá quando não estiver no seu escopo, como serviços de terceiros.
A criptografia ponta a ponta permite que você mantenha o controle em todos os momentos, quando seus dados estão em trânsito, quando estão em repouso e até mesmo quando não estão em suas mãos. Assim, oferece uma proteção muito mais ampla do que outras tecnologias de criptografia (TLS, criptografia de disco completo, ...).
Sempre que você enfrentar um caso onde a conformidade é importante (GDPR, HIPAA, SOC-2,...) ou onde você tem dados confidenciais (médicos, defesa,...), a criptografia ponta a ponta é obrigatória. Mas mesmo para dados mais comuns, é uma boa prática ter. Uma violação de dados é um evento devastador que se torna cada vez mais frequente.
PubNub SDK oferece um gancho de criptografia simples, usando o argumento cipherKey
ao instanciar seu SDK. Isso garantirá que todas as mensagens enviadas sejam criptografadas antes de serem enviadas. No entanto, você mesmo deve fazer o gerenciamento das chaves, que é a parte mais difícil de um sistema criptografado de ponta a ponta.
As vulnerabilidades raramente vêm da criptografia em si, mas na maioria das vezes do vazamento de chaves por meio de falhas no modelo de segurança.
Usar um serviço de terceiros para sua segurança permite que você não tenha um único ponto de falha. Seald.io propõe um modelo de segurança robusto, certificado pela ANSSI, com controle de gerenciamento de acesso em tempo real, revogação e recuperação de usuários, 2FA e muito mais.
Este artigo explica como integrar o Seald ao PubNub passo a passo, para proteger seu bate-papo com criptografia ponta a ponta. Construiremos um exemplo de aplicativo de mensagens, com os seguintes recursos:
Salas de bate-papo individuais e em grupo.
Cada membro tem uma sala de chat dedicada com todos os outros usuários.
Qualquer pessoa pode criar uma sala de chat em grupo com vários outros usuários.
Use criptografia ponta a ponta para cada mensagem e arquivo enviado.
Permitir gerenciamento de acesso em tempo real aos chats.
Para começar, você precisará de uma conta PubNub. Você pode se inscrever
Selecione o conjunto de chaves de demonstração e vá até a guia de configuração. Para nossa demonstração, precisamos ativar as permissões Files
e Objects
. Para a permissão do Object
, usaremos os seguintes eventos: User Metadata Events
, Channel Metadata Events
e Membership Events
.
Depois que o conjunto de chaves for criado e configurado, precisamos copiá-lo para nosso frontend.
Vamos criar um arquivo JSON na pasta src/
, chamado settings.json
. Usaremos este arquivo para todas as chaves de API que precisaremos. Começando com o conjunto de chaves PubNub:
{ "PUBNUB_PUB_KEY": "pub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "PUBNUB_SUB_KEY": "sub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" }
Usaremos o PubNub para quase todas as tarefas de back-end. Nosso back-end lidará apenas com a inscrição/login do usuário e usará um modelo de usuário minimalista com apenas um ID, um nome e um e-mail.
Na parte frontal, precisamos de uma pequena interface de autenticação:
Assim que o usuário tiver uma conta, a primeira coisa que ele precisa é de uma instância do PubNub SDK.
Para identificar o usuário no Pubnub, precisamos fornecer um UUID.
Para simplificar, usaremos o mesmo ID do nosso 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 manter nosso backend o mais simples possível, usaremos os metadados de usuário do PubNub para trocar informações dos usuários.
Logo após a instanciação do SDK, simplesmente chamamos a função PubNub setUUIDMetadata
:
/* frontend/src/App.js */ await pubnub.objects.setUUIDMetadata({ uuid: currentUser.id, data: { email: currentUser.emailAddress, name: currentUser.name } })
A primeira coisa a fazer com o PubNub é recuperar todos os membros existentes e armazená-los em nosso armazenamento de dados 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 um canal PubNub. Também adicionaremos alguns metadados a cada canal:
ownerId
: o ID do usuário que criou a sala.
one2one
: Um booleano para diferenciar salas de mensagens diretas e salas de grupo.
archived
: um booleano, para ocultar uma sala de grupo excluída.
Os metadados ownerId
serão usados posteriormente ao adicionar o Seald SDK. O PubNub não tem um conceito de propriedade, mas o Seald tem. Ele definirá quem pode adicionar ou remover usuários de um canal. Basicamente define um administrador de grupo.
Começaremos recuperando salas de chat existentes. Também precisaremos dos metadados da sala, por isso precisamos incluir campos personalizados. Então, precisamos filtrar as salas arquivadas e enviar tudo para nosso armazenamento de dados.
Por fim, subscribe
o canal PubNub associado à sala, para recebermos novas mensagens:
/* 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) })
Agora buscamos todas as salas em que estamos. Precisamos de uma última coisa para finalizar a inicialização do aplicativo: garantir que tenhamos uma sala one2one
com todos os outros membros, incluindo os recém-registrados.
Para cada usuário recém-encontrado, criaremos uma nova sala e enviaremos uma mensagem de alô.
Em seguida, definiremos os metadados da sala e assinaremos:
/* 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] }) } }
Depois de tudo isso feito, nosso estado inicial do aplicativo está totalmente definido. No entanto, precisamos mantê-lo atualizado.
Isso pode ser feito simplesmente adicionando um ouvinte de evento 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
Agora podemos dar uma olhada na própria sala de bate-papo one2one
. Então cuidaremos das salas de grupo.
Em um arquivo chat.js
teremos toda a lógica para exibir as mensagens de uma sala de chat.
Para inicializar esta sala, a única coisa que precisamos fazer é buscar todas as mensagens pré-existentes.
Isso pode ser feito simplesmente conhecendo o ID do quarto:
/* frontend/src/components/Chat.jsx */ const fetchedMessages = (await pubnub.fetchMessages({ channels: [currentRoomId] })).channels[currentRoomId]
Podemos assinar o canal para receber novas mensagens e adicionar um ouvinte para exibi-las em tempo real:
/* frontend/src/components/Chat.jsx */ pubnub.addListener({ message: handleReceiveMessage }) pubnub.subscribe({ channels: [currentRoomId] })
Para enviar uma mensagem, basta publicá-la no 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 um arquivo, primeiro iremos carregá-lo no PubNub. Em seguida, obteremos o URI do arquivo enviado e o publicaremos como uma mensagem na 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 criar e gerenciar grupos, precisaremos de uma interface para seleção de usuários:
Depois que os membros do grupo forem selecionados, podemos criar um canal PubNub para nossa sala e, em seguida, definir metadados e associação para o canal. O código é muito semelhante ao que é feito para as salas one2one, por isso não vamos repeti-lo aqui.
Agora temos um aplicativo de chat completo. Vamos adicionar criptografia ponta a ponta para cada mensagem!
Para começar com Seald, crie uma conta de teste gratuita
Ao acessar o painel do Seald, alguns URLs e tokens de API são exibidos.
Obtenha os seguintes elementos:
Adicionaremos essas chaves ao nosso 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 usar o Seald SDK, cada usuário precisa de uma licença JWT ao se inscrever.
Esses JWT precisam ser gerados no back-end usando um segredo e um ID secreto.
Na página inicial do painel, copie o segredo JWT e seu ID associado em backend/settings.json
:
{ "SEALD_JWT_SECRET_ID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "SEALD_JWT_SECRET": "XXXXXXXXXXXXXXXXXXXXXXXX" }
Durante a chamada da API de inscrição, geraremos um JWT de licença Seald e o retornaremos:
/* 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 obter mais informações sobre isso, consulte
Então, precisamos instalar o Seald SDK.
Também precisamos instalar um plugin para identificar usuários no servidor Seald. Para fazer isso, usaremos o pacote sdk-plugin-ssks-password
.
Este plugin permite a autenticação simples por senha de nossos usuários:
npm i -S @seald-io/sdk @seald-io/sdk-plugin-ssks-password
Em seguida, criaremos um arquivo seald.js
. Neste arquivo, começaremos criando uma função para instanciar o 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)] }) }
Em seald.js
, também adicionaremos duas funções: uma para criar uma identidade e outra para recuperá-la. Para criar uma identidade, também precisamos do JWT de licença retornado na criação da conta:
/* 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 nossos fluxos de inscrição e login, só precisamos chamar essas funções depois que o usuário fizer login.
Neste ponto, toda vez que nosso usuário estiver conectado, ele terá um Seald SDK funcionando, pronto para criptografar e descriptografar!
Aviso: Para segurança adequada, a senha deve ser previamente criptografada antes de ser enviada ao servidor de autenticação. Para mais detalhes sobre isso, consulte
Cada sala de bate-papo será associada a uma encryptionSession
no Seald SDK.
Cada vez que criamos uma sala de chat, basta adicionar uma linha para criar a sessão de criptografia e usá-la:
/* 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 👋')) } })
Observe que nosso usuário é incluído por padrão como destinatário da encryptionSession.
Ao acessar uma sala, precisamos obter a encryptionSession
correspondente. Ele pode ser recuperado de qualquer mensagem ou arquivo criptografado. Assim que tivermos isso, iremos mantê-lo na referência do componente.
Então podemos simplesmente usar as funções session.encryptMessage
, session.encryptFile
, session.decryptMessage
e session.decryptFile
.
Vamos começar com as mensagens. Para enviar uma mensagem:
/* 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... */ }
E quando recebemos uma mensagem:
/* 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] }) }
Além disso, usamos esta função decryptMessage
para descriptografar todas as mensagens já na sessão ao abrir uma sala:
/* frontend/src/components/Chat.js */ const fetchedMessages = (await pubnub.fetchMessages({ channels: [currentRoomId] })).channels[currentRoomId] const clearMessages = fetchedMessages ? await Promise.all(fetchedMessages.map(decryptMessage)) : []
E agora para arquivos. Para fazer upload de um arquivo:
/* 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 })
E para descriptografar um arquivo:
/* 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 */ } }
Os bate-papos em grupo também terão encryptionSession
. Cada vez que um grupo é criado, precisamos criar um:
/* frontend/src/components/ManageDialogRoom.js.js */ // To create the encryptionSession const sealdSession = await getSealdSDKInstance().createEncryptionSession( { userIds: dialogRoom.selectedUsersId }, { metadata: newRoomId } )
Então, toda vez que modificarmos membros do grupo, precisaremos adicioná-los ou removê-los dele:
/* 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 }) } }
Feito isso, terminamos!
Conseguimos integrar o Seald ao PubNub com apenas algumas linhas de código.
Agora que o chat está criptografado de ponta a ponta, você pode garantir aos seus usuários que seus dados permanecerão confidenciais, mesmo em caso de violação de dados.
Como sempre, não hesite em
Mal posso esperar para ver o que você construiu 🥳.
Também publicado aqui.