Dois Meses Depois: O Que Aprendi ao Construir um Mensageiro E2EE
Após dois meses de desenvolvimento de um mensageiro com criptografia de ponta a ponta (E2EE), o autor compartilha suas descobertas sobre a complexidade da arquitetura, a importância do papel do servidor e os desafios da implementação de protocolos como o Double Ratchet e o suporte a múltiplos dispositivos.
MundiX News·26 de junho de 2026·15 min de leitura·👁 1 views
Dois meses atrás, publiquei um artigo sobre meu projeto pessoal — um mensageiro com criptografia de ponta a ponta (E2EE). Para ser honesto, não esperava que ele gerasse tanto interesse. Recebi comentários, observações, perguntas e críticas bastante úteis. Em alguns pontos, fui corrigido de forma pertinente. Em outros, fui levado a reconsiderar decisões que, naquele momento, me pareciam adequadas. E, o mais gratificante, algumas das funcionalidades que estavam apenas no roadmap conseguiram ser implementadas nesse período.
A primeira parte do artigo pode ser encontrada em: [link para o artigo anterior]. O projeto está disponível em: [link para o GitHub]. De modo geral, nesses dois meses, percebi uma coisa simples: criar um chat não é tão difícil. Criar um mensageiro E2EE já é mais complexo. Mas entender por que o Signal levou tantos anos para ser desenvolvido foi uma história completamente diferente. À primeira vista, parece que a documentação existe, o X3DH, o Double Ratchet, o WebCrypto, artigos e especificações estão disponíveis. Basta juntar tudo com cuidado. É nesse momento que, em algum lugar distante, um engenheiro do Signal começa a rir.
Para esclarecer, o Chaos não é um substituto para o Signal nem um "mensageiro seguro pronto para uso", mas sim um projeto pessoal de código aberto no qual estou explorando como os sistemas E2EE funcionam internamente. Inicialmente, eu queria apenas entender como o X3DH e o Double Ratchet funcionavam. Depois, tornou-se interessante criar algo mais parecido com um sistema real do que um exemplo abstrato de 200 linhas: um backend, um frontend, comunicação em tempo real, banco de dados, entrega de mensagens, múltiplos dispositivos, observabilidade, um cliente desktop, deploy e segurança, além de casos de uso estranhos que surgem no momento mais inoportuno. Gradualmente, o projeto deixou de ser um "chatzinho no Spring Boot" e começou a se transformar em um sistema onde qualquer decisão sobre chaves afeta subitamente o banco de dados, o WebSocket, a experiência do usuário (UX), a pré-visualização de mensagens, a autenticação, a recuperação de sessões e até mesmo o que pode ser exibido na lista de chats. É por isso que ele ainda não se tornou entediante.
Atualmente, o projeto cresceu consideravelmente. O diagrama é, claro, uma grande simplificação. Na realidade, tudo é um pouco mais complexo, pois qualquer projeto vivo tem a propriedade de se transformar gradualmente em um pequeno data center no seu notebook. Atualmente, o projeto inclui: um frontend em React; uma aplicação desktop em Electron; um backend em Spring Boot; API REST; STOMP/WebSocket para comunicação em tempo real; PostgreSQL para usuários, dispositivos, mensagens, envelopes, prekeys e anexos; Redis para limites de taxa, sessões, status online e refresh tokens; Docker Compose; Prometheus; Grafana; Loki; Caddy; e manifestos Kubernetes. No entanto, a principal conclusão aqui não é a quantidade de tecnologias. A principal conclusão é que E2EE não é um crypto-service separado nem uma única função no código. É uma propriedade arquitetural de todo o sistema. Se o servidor não deve ver o texto plano, isso afeta quase tudo. E é nesse momento que você começa a entender que E2EE não é uma função encrypt(), mas uma restrição que gradualmente se espalha por todo o sistema.
Uma das coisas que mais mudou nesse período foi a minha compreensão do papel do servidor. Em um mensageiro comum, o servidor é frequentemente a fonte da verdade para quase tudo: o texto da mensagem, a pré-visualização da última mensagem, a busca, o histórico, os status, os anexos, a indexação. Em um mensageiro E2EE, o servidor deve ser muito mais "burro". E isso é um elogio. Ele deve aceitar envelopes criptografados, armazenar o texto cifrado e entregá-lo aos dispositivos corretos. Todo o resto deve passar por ele o mínimo possível. A entrega simplificada funciona assim: Alice criptografa a mensagem localmente. Para cada dispositivo de Bob, um envelope criptografado separado é criado. O servidor armazena o texto cifrado. O servidor entrega os envelopes a todos os dispositivos de Bob. Bob descriptografa a mensagem localmente. O recibo de leitura é enviado de volta como um evento separado. O servidor, nesse caso, não vê o texto da mensagem. Mas é importante notar: isso não significa que o servidor "não sabe nada". Ele ainda vê metadados: quem está se comunicando com quem, quando ocorreram os eventos, aproximadamente quantos dados foram transferidos, quais dispositivos estão ativos. E este é um grande tópico separado.
Um dos momentos engraçados foi com a pré-visualização da última mensagem. No servidor, o campo content aparece aproximadamente assim: [encrypted]. Inicialmente, isso é um pouco irritante. Você abre o banco de dados, olha para a mensagem e vê não o texto, mas um placeholder. O primeiro pensamento: "Pronto, algo quebrou de novo". E então vem a compreensão: não, meu amigo, é exatamente assim que está funcionando. O servidor não deve saber o que está dentro da mensagem. Em um aplicativo comum, a pré-visualização da última mensagem é uma simples consulta SQL. Em um mensageiro E2EE, tudo é mais interessante. A pré-visualização precisa ser armazenada localmente no cliente após a descriptografia. O servidor pode armazenar apenas um placeholder seguro, pois, caso contrário, ele volta a saber algo que não deveria. Em algum momento, me peguei pensando que, se você realmente quer resolver um problema no servidor, primeiro deve se perguntar: "O servidor deve mesmo saber a resposta?". Muitas vezes, a resposta correta é não. E depois disso, a arquitetura se torna menos conveniente. Mas é mais honesta.
A maior mudança foi que eu finalmente implementei o DH Ratchet. Nos comentários do primeiro artigo, foi corretamente apontado que ainda não havia um Double Ratchet completo. Havia a configuração inicial da sessão via X3DH e a atualização simétrica das chaves de mensagem, mas faltava uma parte importante — o DH Ratchet. E sem ele, a recuperação de break-in (break-in recovery) não é adequada. Nesses dois meses, cheguei a essa parte. Se explicarmos de forma muito grosseira, o Double Ratchet consiste em duas ideias. A primeira é uma cadeia simétrica de chaves. Para cada mensagem, uma nova chave de mensagem é usada, e as chaves antigas gradualmente se tornam inúteis. A segunda é o DH Ratchet. Quando o interlocutor responde com uma nova chave DH, as partes recalculam a chave raiz (root key) e obtêm novas cadeias de envio/recebimento (sending/receiving chains). O mais importante aqui é a recuperação de break-in. Se um invasor obtiver de alguma forma a chave da cadeia atual, isso é ruim. Mas após uma nova troca de chaves DH, o estado antigo deixa de ajudar a descriptografar novas mensagens. Não é mágica. Não é proteção absoluta contra tudo. Mas é uma propriedade muito importante do protocolo. Simplificadamente, um passo do DH Ratchet no código se parece com isto:
javascript
asyncfunctiondhRatchetStep(session, remotePublicKey){// Alternamos para a nova chave pública DH do interlocutor session.remoteDhPublicKey= remotePublicKey;// Atualizamos a cadeia de recebimentoconst receivingSecret =awaitderiveSharedSecret( session.ownDhKeyPair.privateKey, remotePublicKey
);const receivingKeys =awaitderiveRootAndChainKey( session.rootKey, receivingSecret
); session.rootKey= receivingKeys.rootKey; session.receivingChainKey= receivingKeys.chainKey;// Geramos um novo par de chaves DH para nossa cadeia de envio session.ownDhKeyPair=awaitgenerateDhKeyPair();const sendingSecret =awaitderiveSharedSecret( session.ownDhKeyPair.privateKey, remotePublicKey
);const sendingKeys =awaitderiveRootAndChainKey( session.rootKey, sendingSecret
); session.rootKey= sendingKeys.rootKey; session.sendingChainKey= sendingKeys.chainKey; session.sentCount=0; session.receivedCount=0;}
No papel, tudo parece bonito. E é aqui que me peguei pensando pela primeira vez que ler a especificação do Signal é muito mais fácil do que integrá-la a um sistema real. Na realidade, começam a surgir questões desagradáveis: o que fazer com mensagens fora de ordem? Quantas chaves de mensagem puladas podem ser armazenadas? O que acontece se um dispositivo ficou offline por muito tempo? Como não quebrar o suporte a múltiplos dispositivos (multi-device)? Como se recuperar após a reinstalação do cliente? Onde a conveniência termina e a redução da segurança começa? Quanto mais eu lia a especificação do Signal, mais eu passava a respeitar as pessoas que não apenas inventaram tudo isso, mas também o levaram ao estado de produto.
Quando um usuário tem apenas um dispositivo, você pode fingir que tudo é relativamente claro. Existe Alice. Existe Bob. Existe uma sessão entre eles. Mas então Bob ganha um telefone, um laptop e mais um navegador que ele abriu "por cinco minutos" e depois não fechou por meio ano. E a imagem simples acaba. Agora, a mensagem precisa ser enviada não "para o usuário Bob", mas para cada dispositivo de Bob separadamente. Cada dispositivo tem suas próprias chaves, prekeys, sessões e envelopes. O servidor, nesse caso, se transforma em um mensageiro bastante honesto: "Eu não sei o que está dentro da caixa, mas sei para quais endereços levá-la". E isso é normal. Mas o suporte a múltiplos dispositivos sem verificação de dispositivos é apenas metade do caminho. Tecnicamente, adicionar um novo dispositivo não é tão difícil. É mais difícil provar ao usuário que este é realmente o dispositivo dele, e não alguém que se conectou silenciosamente à conta. É por isso que os números de segurança (safety numbers) e a verificação de dispositivos (device verification) apareceram no roadmap.
Outra história que ficou bem marcada foi a de dois horas de depuração devido a dois padrões. O cliente assina os dados usando WebCrypto. O Java no servidor deve verificar a assinatura. As chaves estão corretas. Os dados estão corretos. O algoritmo está correto. Tudo parece que deveria funcionar. Mas a assinatura não passa. Após várias horas, descobriu-se que o problema não era "criptografia" no sentido assustador da palavra. O problema estava no formato da assinatura. O WebCrypto retorna a assinatura ECDSA no formato IEEE P1363. E o Java/BouncyCastle espera ASN.1 DER nesse ponto. Ambos os lados fazem seu trabalho honestamente. Apenas um diz "olá" em um idioma, o outro espera "hello" em outro, e você fica entre eles pensando em que momento a vida tomou um rumo errado. No final, foi necessário um conversor:
O código é pequeno. Consumiu muitos nervos. E este é provavelmente um dos tipos de bugs mais úteis. Porque depois dele, você entende melhor não apenas o seu código, mas também as fronteiras entre o navegador, o Java e as bibliotecas criptográficas.
É engraçado, mas Spring Boot, WebSocket, Docker e até mesmo Kubernetes não se mostraram a parte mais difícil. Sim, cada um deles tem suas peculiaridades. O Kubernetes, em geral, às vezes se comporta como se não fosse você quem está fazendo o deploy da aplicação, mas ele quem está conduzindo a entrevista. Mas os verdadeiros problemas não começaram aí. Os verdadeiros problemas começaram em torno de confiança, chaves e estado de sessões. Quanto mais o projeto avança, menos tempo é gasto em "apenas escrever código" e mais tempo é dedicado a entender qual comportamento deve ser considerado correto.
Resumindo, o projeto percorreu aproximadamente o seguinte caminho em dois meses:
Era: X3DH, Cliente Web, Dispositivo Único, Observabilidade Básica, Docker, Lógica de Chat do Servidor.
Tornou-se: X3DH + DH Ratchet, Web + Desktop Electron, Multi-device, Prometheus + Grafana + Loki, Docker + Kubernetes, Servidor como roteador de envelopes criptografados.
Mas para mim, a principal mudança não está nesta lista. A principal mudança é que o modelo de pensamento mudou. No início do projeto, eu pensava aproximadamente assim: "Como posso enviar uma mensagem?". Agora, a pergunta soa diferente: "Quais dados o sistema tem o direito de saber para entregar a mensagem?". Este é um nível de inconveniência completamente diferente. E, parece, é aí que começa a verdadeira engenharia de E2EE.
Sim, parte do código repetitivo (boilerplate), testes e trechos rotineiros foram ajudados por Claude e ChatGPT. Mas quanto mais o projeto se desenvolvia, mais ficava claro que as questões sobre confiança, recuperação de sessões, comprometimento de dispositivos, forward secrecy e compromissos arquiteturais ainda precisavam ser resolvidas por conta própria. Em E2EE, o entendimento é mais importante do que a geração de código.
Para não criar falsas ilusões, aqui está o que eu mesmo ainda considero incompleto. Entrega de código frontend via Web. Mesmo que o backend não veja o texto plano, o cliente web ainda é carregado do servidor. Com um determinado modelo de ameaças, o servidor ou a infraestrutura de entrega podem tentar substituir o código do cliente. A aplicação desktop reduz parcialmente esse risco, mas não o elimina completamente. Números de segurança e verificação de dispositivos. Ainda não há um mecanismo conveniente que permita aos usuários verificar as impressões digitais das chaves e garantir que um novo dispositivo realmente pertença ao interlocutor. Sem isso, o suporte a múltiplos dispositivos permanece incompleto em termos de UX de confiança. Vazamento de metadados. O servidor não vê o texto das mensagens, mas ainda vê parte dos metadados: quando ocorreram os eventos, quais dispositivos estão conectados, para quem entregar o envelope, quais sessões estão ativas. Este é um conjunto separado de problemas que não pode ser honestamente ignorado. Revisão de segurança. O projeto não passou por uma auditoria criptográfica externa. E quando se trata de E2EE, é importante falar sobre isso abertamente. Uma auto-revisão de segurança é aproximadamente como ser seu próprio dentista: teoricamente interessante, praticamente melhor não fazer.
Atualmente, os planos incluem: Números de Segurança; Verificação de Dispositivos; Notificações Push; Cliente Android; revisão de segurança externa; trabalho contínuo em multi-device; melhoria da recuperação de sessões. Nesses dois meses, o projeto cresceu. Mas a principal conclusão não foi sobre a quantidade de código, nem sobre Docker, nem sobre Kubernetes, nem mesmo sobre Double Ratchet. A principal conclusão é que a complexidade do E2EE não começa onde aparecem AES-GCM ou X25519. Ela começa onde é preciso decidir: em quem se pode confiar; quais dados o servidor tem o direito de saber; o que fazer com vários dispositivos; como viver após um comprometimento; onde a segurança entra em conflito com a conveniência; como não quebrar tudo isso com um belo UX. Provavelmente é por isso que os grandes mensageiros se desenvolvem por anos. E eu, por enquanto, apenas continuo achando interessante investigar. E, parece, quanto mais fundo você cava, mais você começa a entender por que o Signal ainda lança artigos, especificações e novas versões do protocolo. Porque em tais sistemas, as perguntas, por algum motivo, nunca acabam. E sim, obrigado a todos que criticaram o primeiro artigo. Muitas das coisas que agora existem no projeto surgiram graças aos comentários. Então, se você vir um erro criptográfico, uma falha arquitetural ou apenas uma decisão estranha, não hesite em escrever. Da última vez, isso já ajudou.
🛡️⚡
Pare de pesquisar. Comece a hackear.
O MundiX é seu copiloto de pentest com IA: comandos exatos, análise de outputs e próximo passo na kill chain — em segundos.
Sem cartão para começar · Planos a partir de R$49/mês
Dois meses atrás, publiquei um artigo sobre meu projeto pessoal — um mensageiro com criptografia de ponta a ponta (E2EE). Para ser honesto, não esperava que ele gerasse tanto interesse. Recebi comentários, observações, perguntas e críticas bastante úteis. Em alguns pontos, fui corrigido de forma pertinente. Em outros, fui levado a reconsiderar decisões que, naquele momento, me pareciam adequadas. E, o mais gratificante, algumas das funcionalidades que estavam apenas no roadmap conseguiram ser implementadas nesse período.
A primeira parte do artigo pode ser encontrada em: [link para o artigo anterior]. O projeto está disponível em: [link para o GitHub]. De modo geral, nesses dois meses, percebi uma coisa simples: criar um chat não é tão difícil. Criar um mensageiro E2EE já é mais complexo. Mas entender por que o Signal levou tantos anos para ser desenvolvido foi uma história completamente diferente. À primeira vista, parece que a documentação existe, o X3DH, o Double Ratchet, o WebCrypto, artigos e especificações estão disponíveis. Basta juntar tudo com cuidado. É nesse momento que, em algum lugar distante, um engenheiro do Signal começa a rir.
Para esclarecer, o Chaos não é um substituto para o Signal nem um "mensageiro seguro pronto para uso", mas sim um projeto pessoal de código aberto no qual estou explorando como os sistemas E2EE funcionam internamente. Inicialmente, eu queria apenas entender como o X3DH e o Double Ratchet funcionavam. Depois, tornou-se interessante criar algo mais parecido com um sistema real do que um exemplo abstrato de 200 linhas: um backend, um frontend, comunicação em tempo real, banco de dados, entrega de mensagens, múltiplos dispositivos, observabilidade, um cliente desktop, deploy e segurança, além de casos de uso estranhos que surgem no momento mais inoportuno. Gradualmente, o projeto deixou de ser um "chatzinho no Spring Boot" e começou a se transformar em um sistema onde qualquer decisão sobre chaves afeta subitamente o banco de dados, o WebSocket, a experiência do usuário (UX), a pré-visualização de mensagens, a autenticação, a recuperação de sessões e até mesmo o que pode ser exibido na lista de chats. É por isso que ele ainda não se tornou entediante.
Atualmente, o projeto cresceu consideravelmente. O diagrama é, claro, uma grande simplificação. Na realidade, tudo é um pouco mais complexo, pois qualquer projeto vivo tem a propriedade de se transformar gradualmente em um pequeno data center no seu notebook. Atualmente, o projeto inclui: um frontend em React; uma aplicação desktop em Electron; um backend em Spring Boot; API REST; STOMP/WebSocket para comunicação em tempo real; PostgreSQL para usuários, dispositivos, mensagens, envelopes, prekeys e anexos; Redis para limites de taxa, sessões, status online e refresh tokens; Docker Compose; Prometheus; Grafana; Loki; Caddy; e manifestos Kubernetes. No entanto, a principal conclusão aqui não é a quantidade de tecnologias. A principal conclusão é que E2EE não é um crypto-service separado nem uma única função no código. É uma propriedade arquitetural de todo o sistema. Se o servidor não deve ver o texto plano, isso afeta quase tudo. E é nesse momento que você começa a entender que E2EE não é uma função encrypt(), mas uma restrição que gradualmente se espalha por todo o sistema.
Uma das coisas que mais mudou nesse período foi a minha compreensão do papel do servidor. Em um mensageiro comum, o servidor é frequentemente a fonte da verdade para quase tudo: o texto da mensagem, a pré-visualização da última mensagem, a busca, o histórico, os status, os anexos, a indexação. Em um mensageiro E2EE, o servidor deve ser muito mais "burro". E isso é um elogio. Ele deve aceitar envelopes criptografados, armazenar o texto cifrado e entregá-lo aos dispositivos corretos. Todo o resto deve passar por ele o mínimo possível. A entrega simplificada funciona assim: Alice criptografa a mensagem localmente. Para cada dispositivo de Bob, um envelope criptografado separado é criado. O servidor armazena o texto cifrado. O servidor entrega os envelopes a todos os dispositivos de Bob. Bob descriptografa a mensagem localmente. O recibo de leitura é enviado de volta como um evento separado. O servidor, nesse caso, não vê o texto da mensagem. Mas é importante notar: isso não significa que o servidor "não sabe nada". Ele ainda vê metadados: quem está se comunicando com quem, quando ocorreram os eventos, aproximadamente quantos dados foram transferidos, quais dispositivos estão ativos. E este é um grande tópico separado.
Um dos momentos engraçados foi com a pré-visualização da última mensagem. No servidor, o campo content aparece aproximadamente assim: [encrypted]. Inicialmente, isso é um pouco irritante. Você abre o banco de dados, olha para a mensagem e vê não o texto, mas um placeholder. O primeiro pensamento: "Pronto, algo quebrou de novo". E então vem a compreensão: não, meu amigo, é exatamente assim que está funcionando. O servidor não deve saber o que está dentro da mensagem. Em um aplicativo comum, a pré-visualização da última mensagem é uma simples consulta SQL. Em um mensageiro E2EE, tudo é mais interessante. A pré-visualização precisa ser armazenada localmente no cliente após a descriptografia. O servidor pode armazenar apenas um placeholder seguro, pois, caso contrário, ele volta a saber algo que não deveria. Em algum momento, me peguei pensando que, se você realmente quer resolver um problema no servidor, primeiro deve se perguntar: "O servidor deve mesmo saber a resposta?". Muitas vezes, a resposta correta é não. E depois disso, a arquitetura se torna menos conveniente. Mas é mais honesta.
A maior mudança foi que eu finalmente implementei o DH Ratchet. Nos comentários do primeiro artigo, foi corretamente apontado que ainda não havia um Double Ratchet completo. Havia a configuração inicial da sessão via X3DH e a atualização simétrica das chaves de mensagem, mas faltava uma parte importante — o DH Ratchet. E sem ele, a recuperação de break-in (break-in recovery) não é adequada. Nesses dois meses, cheguei a essa parte. Se explicarmos de forma muito grosseira, o Double Ratchet consiste em duas ideias. A primeira é uma cadeia simétrica de chaves. Para cada mensagem, uma nova chave de mensagem é usada, e as chaves antigas gradualmente se tornam inúteis. A segunda é o DH Ratchet. Quando o interlocutor responde com uma nova chave DH, as partes recalculam a chave raiz (root key) e obtêm novas cadeias de envio/recebimento (sending/receiving chains). O mais importante aqui é a recuperação de break-in. Se um invasor obtiver de alguma forma a chave da cadeia atual, isso é ruim. Mas após uma nova troca de chaves DH, o estado antigo deixa de ajudar a descriptografar novas mensagens. Não é mágica. Não é proteção absoluta contra tudo. Mas é uma propriedade muito importante do protocolo. Simplificadamente, um passo do DH Ratchet no código se parece com isto:
async function dhRatchetStep(session, remotePublicKey) {
// Alternamos para a nova chave pública DH do interlocutor
session.remoteDhPublicKey = remotePublicKey;
// Atualizamos a cadeia de recebimento
const receivingSecret = await deriveSharedSecret(
session.ownDhKeyPair.privateKey,
remotePublicKey
);
const receivingKeys = await deriveRootAndChainKey(
session.rootKey,
receivingSecret
);
session.rootKey = receivingKeys.rootKey;
session.receivingChainKey = receivingKeys.chainKey;
// Geramos um novo par de chaves DH para nossa cadeia de envio
session.ownDhKeyPair = await generateDhKeyPair();
const sendingSecret = await deriveSharedSecret(
session.ownDhKeyPair.privateKey,
remotePublicKey
);
const sendingKeys = await deriveRootAndChainKey(
session.rootKey,
sendingSecret
);
session.rootKey = sendingKeys.rootKey;
session.sendingChainKey = sendingKeys.chainKey;
session.sentCount = 0;
session.receivedCount = 0;
}
No papel, tudo parece bonito. E é aqui que me peguei pensando pela primeira vez que ler a especificação do Signal é muito mais fácil do que integrá-la a um sistema real. Na realidade, começam a surgir questões desagradáveis: o que fazer com mensagens fora de ordem? Quantas chaves de mensagem puladas podem ser armazenadas? O que acontece se um dispositivo ficou offline por muito tempo? Como não quebrar o suporte a múltiplos dispositivos (multi-device)? Como se recuperar após a reinstalação do cliente? Onde a conveniência termina e a redução da segurança começa? Quanto mais eu lia a especificação do Signal, mais eu passava a respeitar as pessoas que não apenas inventaram tudo isso, mas também o levaram ao estado de produto.
Quando um usuário tem apenas um dispositivo, você pode fingir que tudo é relativamente claro. Existe Alice. Existe Bob. Existe uma sessão entre eles. Mas então Bob ganha um telefone, um laptop e mais um navegador que ele abriu "por cinco minutos" e depois não fechou por meio ano. E a imagem simples acaba. Agora, a mensagem precisa ser enviada não "para o usuário Bob", mas para cada dispositivo de Bob separadamente. Cada dispositivo tem suas próprias chaves, prekeys, sessões e envelopes. O servidor, nesse caso, se transforma em um mensageiro bastante honesto: "Eu não sei o que está dentro da caixa, mas sei para quais endereços levá-la". E isso é normal. Mas o suporte a múltiplos dispositivos sem verificação de dispositivos é apenas metade do caminho. Tecnicamente, adicionar um novo dispositivo não é tão difícil. É mais difícil provar ao usuário que este é realmente o dispositivo dele, e não alguém que se conectou silenciosamente à conta. É por isso que os números de segurança (safety numbers) e a verificação de dispositivos (device verification) apareceram no roadmap.
Outra história que ficou bem marcada foi a de dois horas de depuração devido a dois padrões. O cliente assina os dados usando WebCrypto. O Java no servidor deve verificar a assinatura. As chaves estão corretas. Os dados estão corretos. O algoritmo está correto. Tudo parece que deveria funcionar. Mas a assinatura não passa. Após várias horas, descobriu-se que o problema não era "criptografia" no sentido assustador da palavra. O problema estava no formato da assinatura. O WebCrypto retorna a assinatura ECDSA no formato IEEE P1363. E o Java/BouncyCastle espera ASN.1 DER nesse ponto. Ambos os lados fazem seu trabalho honestamente. Apenas um diz "olá" em um idioma, o outro espera "hello" em outro, e você fica entre eles pensando em que momento a vida tomou um rumo errado. No final, foi necessário um conversor:
O código é pequeno. Consumiu muitos nervos. E este é provavelmente um dos tipos de bugs mais úteis. Porque depois dele, você entende melhor não apenas o seu código, mas também as fronteiras entre o navegador, o Java e as bibliotecas criptográficas.
É engraçado, mas Spring Boot, WebSocket, Docker e até mesmo Kubernetes não se mostraram a parte mais difícil. Sim, cada um deles tem suas peculiaridades. O Kubernetes, em geral, às vezes se comporta como se não fosse você quem está fazendo o deploy da aplicação, mas ele quem está conduzindo a entrevista. Mas os verdadeiros problemas não começaram aí. Os verdadeiros problemas começaram em torno de confiança, chaves e estado de sessões. Quanto mais o projeto avança, menos tempo é gasto em "apenas escrever código" e mais tempo é dedicado a entender qual comportamento deve ser considerado correto.
Resumindo, o projeto percorreu aproximadamente o seguinte caminho em dois meses:
Era: X3DH, Cliente Web, Dispositivo Único, Observabilidade Básica, Docker, Lógica de Chat do Servidor.
Tornou-se: X3DH + DH Ratchet, Web + Desktop Electron, Multi-device, Prometheus + Grafana + Loki, Docker + Kubernetes, Servidor como roteador de envelopes criptografados.
Mas para mim, a principal mudança não está nesta lista. A principal mudança é que o modelo de pensamento mudou. No início do projeto, eu pensava aproximadamente assim: "Como posso enviar uma mensagem?". Agora, a pergunta soa diferente: "Quais dados o sistema tem o direito de saber para entregar a mensagem?". Este é um nível de inconveniência completamente diferente. E, parece, é aí que começa a verdadeira engenharia de E2EE.
Sim, parte do código repetitivo (boilerplate), testes e trechos rotineiros foram ajudados por Claude e ChatGPT. Mas quanto mais o projeto se desenvolvia, mais ficava claro que as questões sobre confiança, recuperação de sessões, comprometimento de dispositivos, forward secrecy e compromissos arquiteturais ainda precisavam ser resolvidas por conta própria. Em E2EE, o entendimento é mais importante do que a geração de código.
Para não criar falsas ilusões, aqui está o que eu mesmo ainda considero incompleto. Entrega de código frontend via Web. Mesmo que o backend não veja o texto plano, o cliente web ainda é carregado do servidor. Com um determinado modelo de ameaças, o servidor ou a infraestrutura de entrega podem tentar substituir o código do cliente. A aplicação desktop reduz parcialmente esse risco, mas não o elimina completamente. Números de segurança e verificação de dispositivos. Ainda não há um mecanismo conveniente que permita aos usuários verificar as impressões digitais das chaves e garantir que um novo dispositivo realmente pertença ao interlocutor. Sem isso, o suporte a múltiplos dispositivos permanece incompleto em termos de UX de confiança. Vazamento de metadados. O servidor não vê o texto das mensagens, mas ainda vê parte dos metadados: quando ocorreram os eventos, quais dispositivos estão conectados, para quem entregar o envelope, quais sessões estão ativas. Este é um conjunto separado de problemas que não pode ser honestamente ignorado. Revisão de segurança. O projeto não passou por uma auditoria criptográfica externa. E quando se trata de E2EE, é importante falar sobre isso abertamente. Uma auto-revisão de segurança é aproximadamente como ser seu próprio dentista: teoricamente interessante, praticamente melhor não fazer.
Atualmente, os planos incluem: Números de Segurança; Verificação de Dispositivos; Notificações Push; Cliente Android; revisão de segurança externa; trabalho contínuo em multi-device; melhoria da recuperação de sessões. Nesses dois meses, o projeto cresceu. Mas a principal conclusão não foi sobre a quantidade de código, nem sobre Docker, nem sobre Kubernetes, nem mesmo sobre Double Ratchet. A principal conclusão é que a complexidade do E2EE não começa onde aparecem AES-GCM ou X25519. Ela começa onde é preciso decidir: em quem se pode confiar; quais dados o servidor tem o direito de saber; o que fazer com vários dispositivos; como viver após um comprometimento; onde a segurança entra em conflito com a conveniência; como não quebrar tudo isso com um belo UX. Provavelmente é por isso que os grandes mensageiros se desenvolvem por anos. E eu, por enquanto, apenas continuo achando interessante investigar. E, parece, quanto mais fundo você cava, mais você começa a entender por que o Signal ainda lança artigos, especificações e novas versões do protocolo. Porque em tais sistemas, as perguntas, por algum motivo, nunca acabam. E sim, obrigado a todos que criticaram o primeiro artigo. Muitas das coisas que agora existem no projeto surgiram graças aos comentários. Então, se você vir um erro criptográfico, uma falha arquitetural ou apenas uma decisão estranha, não hesite em escrever. Da última vez, isso já ajudou.
📤 Compartilhar & Baixar
🧰 Ferramentas recomendadas
Divulgação: alguns links são patrocinados. Podemos receber comissão se você comprar — sem custo extra para você. Só indicamos o que faz sentido para a comunidade.