Voltar ao início
Criptografia

Referência de implementação da criptografia

A criptografia que é executada é mais estreita do que a criptografia que está escrita, e esta página existe por causa dessa diferença. Os sealed boxes do libsodium são a única criptografia de mensagens que o Railgun executa — e só são executados no caminho de cliente em que o módulo nativo libsignal está ausente. Na build desktop Electron, onde o libsignal de fato é carregado, o handler que criptografa mensagens diretas ainda é um espaço reservado que codifica o texto simples em base64. Mensagens de comunidade e de canal não são criptografadas em nenhum caminho. O protocolo no estilo Signal descrito na segunda metade desta página está escrito e revisado, e não está ligado a nenhum cliente que você possa baixar.

Esta é uma referência de arquitetura, não uma auditoria de segurança independente. As seções 1 e 2 descrevem o que é entregue. As seções 4 a 7 descrevem um projeto que o Railgun está construindo e estão marcadas como Planejado em toda parte. Nada aqui deve ser lido como afirmação de que um cliente Railgun impõe atualmente uma propriedade marcada como planejada.

O que é entregue hoje

CaminhoMecanismoEstado
Mensagens diretas — builds de navegador e de desenvolvimentoSealed box do libsodium — acordo de chaves X25519, criptografia autenticada XSalsa20-Poly1305. Selecionado quando o módulo nativo libsignal está ausente.Criptografado
Mensagens diretas — build desktop ElectronO libsignal é carregado, então encryptDm é encaminhado para o handler IPC do Electron — um espaço reservado que codifica o texto simples em base64. O relay recebe texto legível.Não criptografado
Mensagens de comunidade e de canalEnviadas como texto simples embrulhado em base64. A criptografia de canais está escrita, mas retorna um marcador de texto simples.Não criptografado
X3DH + Double RatchetImplementado sobre o libsignal, importado apenas como tipo. Nenhum caminho de execução o instancia.Planejado
Armazenamento de chaves privadasAs chaves são geradas e mantidas no dispositivo. Nenhum caminho de código transmite material de chave privada ou de identidade.Implementado

1. Sealed boxes — a criptografia que é executada

Onde o Railgun de fato criptografa uma mensagem direta, ele o faz com um sealed box do libsodium — a construção crypto_box_seal. Ela combina o acordo de chaves X25519 com a criptografia autenticada XSalsa20-Poly1305. É criptografia real e bem revisada. É também uma construção deliberadamente simples, e seus limites importam. Quais clientes chegam a ela é o assunto da seção 2: as builds de navegador e de desenvolvimento chegam; a build desktop Electron não.

Como um sealed box é construído

// Sender has only the recipient's public key, PK_r

(pk_e, sk_e) = crypto_box_keypair() // fresh, single-use

nonce = BLAKE2b(pk_e || PK_r) // derived, not random

ct = crypto_box(plaintext, nonce, PK_r, sk_e)

sealed = pk_e || ct // ephemeral key is prepended

erase(sk_e)

O destinatário recupera a chave pública efêmera do início da mensagem, recalcula o mesmo segredo compartilhado com sua própria chave privada e abre o box. A tag Poly1305 faz com que um texto cifrado modificado falhe ao abrir, em vez de ser descriptografado em lixo.

O que isso lhe dá

  • Confidencialidade diante do relay e de qualquer um na rede.
  • Integridade — a adulteração é detectada, não descriptografada em silêncio.
  • Anonimato do remetente: a chave efêmera não revela nada sobre quem enviou.
  • Nenhuma configuração de sessão, então funciona quando o destinatário está offline.

O que isso não lhe dá

  • Nenhuma forward secrecy. A chave privada de longo prazo do destinatário abre todas as mensagens já enviadas a ele. Se ela vazar, o texto cifrado capturado vaza junto.
  • Nenhuma autenticação do remetente. Um sealed box é anônimo por construção; ele não prova nada sobre quem o escreveu.
  • Nenhuma recuperação pós-comprometimento. Não há ratchet para seguir adiante depois do comprometimento de uma chave.

Essas três lacunas são exatamente o que o X3DH e o Double Ratchet existem para fechar, e é por isso que esse trabalho está em andamento. Até que ele chegue, o resumo honesto de uma DM com sealed box é: forte contra um observador passivo da rede e contra o relay, fraco contra um adversário que eventualmente obtenha a chave do dispositivo do destinatário. Esse resumo só se aplica onde este código realmente é executado.

2. O que não é criptografado

Mensagens de comunidade e de canal — a maior parte do que os usuários do Railgun realmente escrevem — não são criptografadas hoje em nenhum caminho de cliente. A função de criptografia de canal existe, e é franca sobre si mesma: registra um aviso e retorna o texto simples embrulhado em base64 atrás de uma string marcadora, para que ninguém adiante o confunda com texto cifrado.

// What encryptChannel actually returns

logger.warn("Channel encryption not yet implemented")

ciphertext = base64("[DEVCRYPTO:PLAINTEXT]" + plaintext)

A consequência prática: para conteúdo de comunidade e de canal, o servidor do Railgun está na mesma posição de qualquer servidor de chat comum. Ele pode ler o que você escreve ali. A distribuição de chave de grupo — uma chave por canal entregue aos membros dentro de sealed boxes — é a correção planejada e não foi entregue.

Mensagens diretas na build desktop Electron

A segunda lacuna são as mensagens diretas no cliente que a maioria das pessoas usa, e é fácil enunciá-la ao contrário, então aqui está a lógica de seleção tal como o código a executa. initCrypto() pergunta ao processo principal se o libsignal foi carregado. @signalapp/libsignal-client é uma dependência real e é exigida na inicialização, então em uma build Electron entregue ele de fato carrega — e essa resposta é o que seleciona a implementação IPC do Electron em vez da do sealed box. Seu encryptDm encaminha para o handler crypto:encryptDm, que é um espaço reservado:

// crypto:encryptDm, Electron main process

const plaintextBytes = new TextEncoder().encode(plaintext)

ciphertext: toBase64(plaintextBytes) // not encryption

Portanto, os sealed boxes da seção 1 são alcançados quando o libsignal está ausente — no navegador e em desenvolvimento. No cliente desktop, uma mensagem direta chega ao relay como texto simples codificado em base64. Ali o libsignal é carregado para gerar uma chave de identidade e um pacote de prekeys; ele não estabelece sessão alguma e não faz ratchet de nada — crypto:ensureDmSession é um log de depuração sem corpo.

3. Curve25519 — a curva subjacente

Tanto os sealed boxes que são entregues quanto o projeto X3DH que não é se apoiam na mesma curva elíptica: Curve25519, projetada por Daniel J. Bernstein para troca de chaves Diffie-Hellman de alta velocidade. Esta seção descreve a curva em si e se aplica a ambos.

A equação da curva

y² = x³ + 486662x² + x

over the prime field 𝔽ₚ where p = 2²⁵⁵ − 19

O primo 2²⁵⁵ − 19 foi escolhido porque permite aritmética modular extremamente rápida. O coeficiente 486662 foi escolhido por ser o menor valor que produz uma curva segura com as propriedades de segurança exigidas.

Geração de chaves

1. Chave privada: Gere 32 bytes aleatórios usando um CSPRNG (gerador de números pseudoaleatórios criptograficamente seguro). Faça o clamping da chave zerando os 3 bits mais baixos e o bit mais alto, e definindo o segundo bit mais alto. Isso garante que a chave seja múltipla de 8 e esteja no intervalo válido.

a = random(32 bytes)

a[0] &= 248 // Clear low 3 bits

a[31] &= 127 // Clear high bit

a[31] |= 64 // Set second-highest bit

2. Chave pública: Calcule a multiplicação escalar da chave privada pelo ponto base G = 9.

A = a · G // Scalar multiplication on Curve25519

A segurança se apoia no Problema do Logaritmo Discreto em Curvas Elípticas (ECDLP): dados A e G, é computacionalmente inviável recuperar a. A Curve25519 oferece aproximadamente 128 bits de segurança.

Troca de chaves Diffie-Hellman

Duas partes (Alice e Bob) podem calcular um segredo compartilhado sem nunca transmiti-lo:

Alice: a (private), A = a·G (public)

Bob: b (private), B = b·G (public)

Alice computes: S = a · B = a · (b·G)

Bob computes: S = b · A = b · (a·G)

Both arrive at S = ab·G — the same shared secret

Um bisbilhoteiro que conhece A e B (ambas públicas) não consegue calcular S sem resolver o ECDLP. Essa é a suposição Diffie-Hellman Computacional (CDH).

Um sealed box é essa troca com um dos lados tornado efêmero e descartado. O X3DH, abaixo, é essa troca realizada quatro vezes sobre pares de chaves diferentes, de modo que o resultado autentique ambas as partes além de ocultar o conteúdo.

4. X3DH — Diffie-Hellman Triplo Estendido

PLANEJADO
Escrito sobre o libsignal e verificável no código-fonte. É importado apenas como tipo — nenhum cliente entregue o constrói. Trate tudo nesta seção como o projeto pretendido, não como o comportamento atual.

O X3DH resolve um problema difícil: como duas pessoas estabelecem uma sessão criptografada quando uma delas está offline? O Diffie-Hellman tradicional exige que ambas as partes estejam online. O X3DH usa pacotes de chaves enviados previamente para permitir o acordo de chaves assíncrono.

Tipos de chave

Chave de identidade (IK)

Par de chaves Curve25519 de longo prazo. Gerado uma única vez, identifica o usuário criptograficamente. Nunca muda, a menos que o usuário se registre novamente.

Pré-chave assinada (SPK)

Par de chaves Curve25519 de médio prazo, assinado pela chave de identidade. Rotacionado periodicamente. A assinatura prova que ele pertence ao titular da chave de identidade.

Pré-chave de uso único (OPK)

Pares de chaves Curve25519 efêmeros enviados em lotes. Cada um é usado uma vez e depois excluído. Fornece uma camada extra de forward secrecy para a mensagem inicial.

Chave efêmera (EK)

Um novo par de chaves Curve25519 gerado pelo remetente para cada nova sessão. Nunca armazenado no servidor.

O handshake X3DH

Quando Alice quer enviar mensagem a Bob (que pode estar offline), ela busca no servidor o pacote de pré-chaves de Bob e realiza quatro cálculos Diffie-Hellman:

// Alice has: IKₐ (her identity key), EKₐ (fresh ephemeral)

// Bob published: IKᵦ, SPKᵦ, OPKᵦ

DH1 = DH(IKₐ, SPKᵦ) // her identity ↔ his signed pre-key

DH2 = DH(EKₐ, IKᵦ) // her ephemeral ↔ his identity

DH3 = DH(EKₐ, SPKᵦ) // her ephemeral ↔ his signed pre-key

DH4 = DH(EKₐ, OPKᵦ) // her ephemeral ↔ his one-time pre-key

SK = KDF(DH1 || DH2 || DH3 || DH4)

// SK is the shared secret, derived via HKDF-SHA-256

Por que quatro operações DH?

  • DH1 fornece autenticação mútua (ambas as chaves de identidade estão envolvidas)
  • DH2 garante que a chave efêmera esteja vinculada à identidade de Bob
  • DH3 fornece forward secrecy por meio da chave efêmera
  • DH4 fornece forward secrecy adicional. Se nenhuma OPK estiver disponível, o X3DH ainda funciona apenas com DH1–DH3

DH1 é a operação que um sealed box não tem. É por isso que o caminho entregue não consegue dizer de quem veio uma mensagem.

KDF — função de derivação de chaves

As saídas DH brutas são combinadas usando HKDF-SHA-256 (função de derivação de chaves baseada em HMAC). O HKDF extrai entropia das saídas DH concatenadas e a expande em um segredo compartilhado uniformemente aleatório:

PRK = HKDF-Extract(salt="", input=DH1||DH2||DH3||DH4)

SK = HKDF-Expand(PRK, info="RailGunX3DH", length=32)

5. O algoritmo Double Ratchet

PLANEJADO
Nenhuma versão do Railgun faz ratchet de chaves hoje. No caminho do sealed box, toda mensagem direta é criptografada para a mesma chave de longo prazo do destinatário; no caminho desktop Electron, ela não é criptografada de forma alguma.

Assim que o X3DH estabelece um segredo compartilhado inicial, o Double Ratchet assume. Ele usa dois "ratchets" interligados para derivar uma nova chave única para cada mensagem. O projeto protege as mensagens anteriores e pretende recuperar a proteção das mensagens futuras após um novo passo de ratchet.

Ratchet DH (assimétrico)

Cada vez que a direção da conversa muda (Alice → Bob, depois Bob → Alice), ocorre uma nova troca de chaves Diffie-Hellman usando chaves efêmeras novas. Isso avança a chave raiz um passo de "ratchet":

dh_out = DH(my_ratchet_key, their_ratchet_key)

root_key, chain_key = KDF(root_key, dh_out)

É isso que forneceria a segurança pós-comprometimento: um atacante que rouba o estado atual perde o acesso assim que ocorre um novo passo de ratchet DH.

Ratchet simétrico (cadeia de hash)

Entre os passos do ratchet DH, um ratchet baseado em hash deriva uma nova chave de mensagem a partir da chave de cadeia para cada mensagem:

message_key = HMAC-SHA256(chain_key, 0x01)

chain_key = HMAC-SHA256(chain_key, 0x02)

A chave de cadeia antiga é excluída após cada passo. A chave de mensagem criptografa exatamente uma mensagem e depois é excluída. Esse é o mecanismo de forward secrecy.

Progressão do ratchet

Visualizado, o Double Ratchet se parece com isto. Cada seta é uma função de mão única — você pode avançar, mas nunca voltar:

Root Key Chain:

RK₀ ──DH──→ RK₁ ──DH──→ RK₂ ──DH──→ RK₃ ...

Each RK step produces a Chain Key:

↓ ↓ ↓

CK₀ CK₁ CK₂

↓ ↓ ↓

Each CK step produces Message Keys:

CK₀→MK₀ CK₁→MK₃ CK₂→MK₅

CK₀→MK₁ CK₁→MK₄ CK₂→MK₆

CK₀→MK₂ CK₂→MK₇

Cada MKₙ criptografaria exatamente uma mensagem e depois seria excluída, de modo que um atacante que obtivesse MK₄ não poderia derivar MK₃ nem MK₅. No caminho do sealed box que é entregue hoje, não há chave por mensagem para roubar nem cadeia para percorrer — há uma única chave de longo prazo que abre tudo.

6. Forward secrecy — o que seria necessário

PLANEJADO

Forward secrecy significa que comprometer chaves de longo prazo não compromete chaves de sessão passadas. O Railgun não a tem hoje. Dois mecanismos a forneceriam, e ambos pertencem ao protocolo planejado descrito acima:

Chaves efêmeras no X3DH

O remetente gera um novo par de chaves efêmeras para cada nova sessão e exclui a metade privada após o handshake. Um atacante que mais tarde roube as chaves de identidade de ambas as partes ainda assim não consegue reconstruir o segredo da sessão. Os sealed boxes de fato usam uma chave efêmera nova do lado do remetente — mas a metade da troca correspondente ao destinatário é a chave de longo prazo dele, que é precisamente a razão pela qual a propriedade não se sustenta.

Exclusão das chaves do ratchet

O Double Ratchet exclui as chaves de cadeia e as chaves de mensagem antigas após o uso. A cadeia KDF é de mão única — dada CKₙ, você pode calcular CKₙ₊₁, mas não CKₙ₋₁, de modo que um comprometimento no instante T não revela nada enviado antes de T.

Segurança pós-comprometimento

O ratchet DH também fornece future secrecy: um atacante que compromete o estado atual da sessão o perde no passo de ratchet seguinte, que introduz uma aleatoriedade que ele não possui. Sem um ratchet, uma chave comprometida continua útil ao atacante por tempo indeterminado.

7. Arquitetura de relay e fila

O Railgun retransmite envelopes e pode enfileirá-los temporariamente para entrega offline. O relay é construído de modo a não precisar do texto simples das mensagens, mas hoje ele recebe texto simples em dois casos: mensagens de canal, que nenhum cliente criptografa, e mensagens diretas da build desktop Electron, cujo handler IPC codifica em base64 em vez de criptografar. Uma mensagem direta vinda do caminho do sealed box chega como texto cifrado que o relay não pode abrir.

O que o servidor vê

O servidor guarda:

  • Pacotes de chaves públicas
  • Informações de registro do usuário
  • Metadados e participação de comunidades/canais
  • Texto cifrado em sealed box que ele não pode abrir (DMs do caminho navegador/desenvolvimento)
  • Texto simples de DM codificado em base64 vindo da build desktop Electron
  • Conteúdo das mensagens de canal, até que a criptografia de canais seja entregue
  • Registros de auditoria de autenticação

O servidor nunca recebe:

  • Chaves privadas ou de identidade
  • Estado de sessão do cliente
  • Uma cópia durável do seu histórico de mensagens

As mensagens são retransmitidas em tempo real para destinatários online por WebSocket. Para dispositivos offline, os envelopes são enfileirados no Redis com um TTL máximo de 30 dias e excluídos assim que o dispositivo os confirma. Não há arquivo de mensagens no servidor — o histórico do cliente fica no armazenamento local do seu próprio dispositivo, então um dispositivo perdido é um histórico perdido.

8. Resumo das propriedades de segurança

PropriedadeMecanismoEstado
Confidencialidade das DMsSealed boxes apenas no caminho navegador/desenvolvimento; o handler desktop Electron codifica em base64Parcial
Integridade das DMsTag Poly1305 no caminho do sealed box; o caminho desktop Electron envia base64 não autenticadoParcial
As chaves permanecem no dispositivoArmazenamento local de chaves; nenhum caminho de código transmite chaves privadasImplementado
Entrega offlineEnvelopes enfileirados no Redis com TTL de 30 dias, excluídos na confirmaçãoImplementado
Confidencialidade dos canaisA distribuição de chave de grupo não está implementada; o conteúdo é enviado legívelNão implementado
Autenticação do remetenteOs sealed boxes são anônimos por construçãoNão implementado
Forward secrecyRequer o Double Ratchet, que nenhum cliente instanciaPlanejado
Recuperação pós-comprometimentoRequer o ratchet DH, que nenhum cliente instanciaPlanejado
Auditoria independenteNenhuma auditoria criptográfica de terceiros concluída é alegadaNão concluído
Garantia pós-quânticaNenhuma garantia geral de segurança pós-quântica é alegadaNão alegado

De onde vem esta página

Todo estado acima foi lido do código-fonte do cliente, e não de um documento de projeto. O repositório do cliente não é público no momento, então não podemos pedir que você mesmo verifique — o que é razão para pesar esta página como uma divulgação, não como prova. Perguntas sobre uma afirmação específica são bem-vindas em security@railgun.chat.