Referencia de implementación del cifrado
El cifrado que se ejecuta es más estrecho que el cifrado que está escrito, y esa diferencia es el motivo de esta página. Las sealed boxes de libsodium son el único cifrado de mensajes que Railgun ejecuta — y solo se ejecutan en la ruta de cliente en la que el módulo nativo libsignal está ausente. En la compilación de escritorio de Electron, donde libsignal sí se carga, el manejador que cifra los mensajes directos sigue siendo un marcador de posición que codifica el texto plano en base64. Los mensajes de comunidad y de canal no se cifran en ninguna ruta. El protocolo de estilo Signal descrito en la segunda mitad de esta página está escrito y revisado, y no está conectado a ningún cliente que se pueda descargar.
Esto es una referencia de arquitectura, no una auditoría de seguridad independiente. Las secciones 1 y 2 describen lo que se entrega. Las secciones 4 a 7 describen un diseño hacia el que Railgun está trabajando y están marcadas como Planificado en todo momento. Nada de lo aquí escrito debe leerse como una afirmación de que un cliente de Railgun aplique actualmente una propiedad marcada como planificada.
Lo que se entrega hoy
| Ruta | Mecanismo | Estado |
|---|---|---|
| Mensajes directos — compilaciones de navegador y de desarrollo | sealed box de libsodium — acuerdo de claves X25519, cifrado autenticado XSalsa20-Poly1305. Se selecciona cuando el módulo nativo libsignal está ausente. | Cifrado |
| Mensajes directos — compilación de escritorio de Electron | libsignal se carga, por lo que encryptDm se enruta al manejador IPC de Electron — un marcador de posición que codifica el texto plano en base64. El relé recibe texto legible. | No cifrado |
| Mensajes de comunidad y de canal | Se envían como texto plano envuelto en base64. El cifrado de canales está escrito, pero devuelve un marcador de texto plano. | No cifrado |
| X3DH + Double Ratchet | Implementado sobre libsignal, importado solo como tipo. Ninguna ruta en tiempo de ejecución lo instancia. | Planificado |
| Almacenamiento de claves privadas | Las claves se generan y se conservan en el dispositivo. Ninguna ruta de código transmite material de clave privada o de identidad. | Implementado |
1. Las sealed boxes — el cifrado que se ejecuta
Allí donde Railgun cifra realmente un mensaje directo, lo hace con una sealed box de libsodium — la construcción crypto_box_seal. Combina el acuerdo de claves X25519 con el cifrado autenticado XSalsa20-Poly1305. Es criptografía real y bien revisada. Es también una construcción deliberadamente sencilla, y sus límites importan. Qué clientes llegan a ella es el asunto de la sección 2: las compilaciones de navegador y de desarrollo sí llegan; la compilación de escritorio de Electron no.
Cómo se construye una sealed box
// 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)
El destinatario recupera la clave pública efímera del principio del mensaje, vuelve a calcular el mismo secreto compartido con su propia clave privada y abre la caja. La etiqueta Poly1305 hace que un texto cifrado modificado no llegue a abrirse, en lugar de descifrarse como basura.
Lo que esto le da
- • Confidencialidad frente al relé y frente a cualquiera que esté en la red.
- • Integridad — la manipulación se detecta, no se descifra en silencio.
- • Anonimato del remitente: la clave efímera no revela nada sobre quién lo envió.
- • No hay establecimiento de sesión, de modo que funciona cuando el destinatario está desconectado.
Lo que esto no le da
- • Ningún secreto hacia adelante. La clave privada de largo plazo del destinatario abre todos los mensajes que se le hayan enviado alguna vez. Si se filtra, con ella se filtra el texto cifrado capturado.
- • Ninguna autenticación del remitente. Una sealed box es anónima por construcción; no prueba nada sobre quién la escribió.
- • Ninguna recuperación tras un compromiso. No existe ningún ratchet que permita dejar atrás el compromiso de una clave.
Esas tres carencias son exactamente lo que X3DH y el Double Ratchet existen para cerrar, y por eso ese trabajo está en marcha. Hasta que llegue, el resumen honesto de un mensaje directo con sealed box es: fuerte frente a un observador pasivo de la red y frente al relé, débil frente a un adversario que acabe obteniendo la clave del dispositivo de un destinatario. Ese resumen solo se aplica allí donde este código realmente se ejecuta.
2. Lo que no está cifrado
Los mensajes de comunidad y de canal — la mayor parte de lo que la mayoría de los usuarios de Railgun escribe en realidad — no están cifrados hoy en ninguna ruta de cliente. La función de cifrado de canales existe, y es franca sobre sí misma: registra una advertencia y devuelve el texto plano envuelto en base64 tras una cadena marcadora, para que nadie más adelante lo confunda con texto cifrado.
// What encryptChannel actually returns
logger.warn("Channel encryption not yet implemented")
ciphertext = base64("[DEVCRYPTO:PLAINTEXT]" + plaintext)
La consecuencia práctica: para el contenido de comunidades y canales, el servidor de Railgun está en la misma posición que cualquier servidor de chat corriente. Puede leer lo que usted escribe allí. La distribución de claves de grupo — una clave por canal entregada a los miembros dentro de sealed boxes — es la solución planificada y no se ha entregado.
Mensajes directos en la compilación de escritorio de Electron
La segunda carencia son los mensajes directos en el cliente que la mayoría de la gente usa, y es fácil enunciarla al revés, así que aquí está la lógica de selección tal como la ejecuta el código. initCrypto() pregunta al proceso principal si libsignal se cargó. @signalapp/libsignal-client es una dependencia real y se requiere al arrancar, de modo que en una compilación de Electron entregada sí se carga — y esa respuesta es la que selecciona la implementación IPC de Electron en lugar de la de sealed box. Su encryptDm reenvía al manejador crypto:encryptDm, que es un marcador de posición:
// crypto:encryptDm, Electron main process
const plaintextBytes = new TextEncoder().encode(plaintext)
ciphertext: toBase64(plaintextBytes) // not encryption
Así pues, a las sealed boxes de la sección 1 se llega cuando libsignal está ausente — en el navegador y en desarrollo. En el cliente de escritorio, un mensaje directo llega al relé como texto plano codificado en base64. Allí libsignal se carga para generar una clave de identidad y un paquete de preclaves; no establece ninguna sesión y no hace avanzar ningún ratchet — crypto:ensureDmSession es un registro de depuración sin cuerpo.
3. Curve25519 — la curva subyacente
Tanto las sealed boxes que se entregan como el diseño X3DH que no se entrega descansan sobre la misma curva elíptica: Curve25519, diseñada por Daniel J. Bernstein para un intercambio de claves Diffie-Hellman de alta velocidad. Esta sección describe la curva en sí, y se aplica a ambos.
La ecuación de la curva
y² = x³ + 486662x² + x
over the prime field 𝔽ₚ where p = 2²⁵⁵ − 19
El primo 2²⁵⁵ − 19 se eligió porque permite una aritmética modular extremadamente rápida. El coeficiente 486662 se eligió por ser el valor más pequeño que produce una curva segura con las propiedades de seguridad requeridas.
Generación de claves
1. Clave privada: genere 32 bytes aleatorios con un CSPRNG (generador de números pseudoaleatorios criptográficamente seguro). Ajuste la clave borrando los 3 bits menos significativos y el bit más significativo, y estableciendo el segundo bit más significativo. Esto garantiza que la clave sea múltiplo de 8 y esté dentro del rango 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. Clave pública: calcule la multiplicación escalar de la clave privada por el punto base G = 9.
A = a · G // Scalar multiplication on Curve25519
La seguridad se apoya en el problema del logaritmo discreto en curvas elípticas (ECDLP): dados A y G, es computacionalmente inviable recuperar a. Curve25519 proporciona aproximadamente 128 bits de seguridad.
Intercambio de claves Diffie-Hellman
Dos partes (Alice y Bob) pueden calcular un secreto compartido sin transmitirlo nunca:
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
Quien escuche el tráfico y conozca A y B (ambas públicas) no puede calcular S sin resolver el ECDLP. Esta es la suposición Diffie-Hellman computacional (CDH).
Una sealed box es este intercambio con un lado convertido en efímero y desechado. X3DH, más abajo, es este mismo intercambio realizado cuatro veces sobre pares de claves distintos, de modo que el resultado autentica a ambas partes además de ocultar el contenido.
4. X3DH — Extended Triple Diffie-Hellman
PLANIFICADOX3DH resuelve un problema difícil: ¿cómo establecen dos personas una sesión cifrada cuando una de ellas está desconectada? El Diffie-Hellman tradicional exige que ambas partes estén conectadas. X3DH usa paquetes de claves subidos por adelantado para hacer posible un acuerdo de claves asíncrono.
Tipos de claves
Clave de identidad (IK)
Par de claves Curve25519 de largo plazo. Se genera una sola vez e identifica criptográficamente al usuario. No cambia nunca, salvo que el usuario vuelva a registrarse.
Preclave firmada (SPK)
Par de claves Curve25519 de plazo medio, firmado por la clave de identidad. Se rota periódicamente. La firma demuestra que pertenece al titular de la clave de identidad.
Preclave de un solo uso (OPK)
Pares de claves Curve25519 efímeros subidos por lotes. Cada uno se usa una vez y luego se elimina. Aporta una capa adicional de secreto hacia adelante para el mensaje inicial.
Clave efímera (EK)
Un par de claves Curve25519 nuevo, generado por el remitente para cada sesión nueva. Nunca se almacena en el servidor.
El handshake de X3DH
Cuando Alice quiere escribir a Bob (que puede estar desconectado), obtiene del servidor el paquete de preclaves de Bob y realiza cuatro 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 qué cuatro operaciones DH?
- DH1 aporta autenticación mutua (intervienen ambas claves de identidad)
- DH2 garantiza que la clave efímera queda ligada a la identidad de Bob
- DH3 aporta secreto hacia adelante mediante la clave efímera
- DH4 aporta secreto hacia adelante adicional. Si no hay ninguna OPK disponible, X3DH funciona igualmente solo con DH1–DH3
DH1 es la operación de la que una sealed box carece. Por eso la ruta que se entrega no puede decirle de quién procede un mensaje.
KDF — función de derivación de claves
Las salidas DH en bruto se combinan mediante HKDF-SHA-256 (función de derivación de claves basada en HMAC). HKDF extrae entropía de las salidas DH concatenadas y la expande hasta obtener un secreto compartido uniformemente aleatorio:
PRK = HKDF-Extract(salt="", input=DH1||DH2||DH3||DH4)
SK = HKDF-Expand(PRK, info="RailGunX3DH", length=32)
5. El algoritmo Double Ratchet
PLANIFICADOUna vez que X3DH establece un secreto compartido inicial, el Double Ratchet toma el relevo. Usa dos "ratchets" entrelazados para derivar una clave nueva y única para cada mensaje. El diseño protege los mensajes anteriores y pretende recuperar la protección de los mensajes futuros tras un nuevo paso del ratchet.
Ratchet DH (asimétrico)
Cada vez que cambia el sentido de la conversación (Alice → Bob, y después Bob → Alice), se produce un nuevo intercambio de claves Diffie-Hellman con claves efímeras nuevas. Esto hace avanzar la clave raíz un paso:
dh_out = DH(my_ratchet_key, their_ratchet_key)
root_key, chain_key = KDF(root_key, dh_out)
Esto es lo que aportaría seguridad tras el compromiso: un atacante que robe el estado actual pierde el acceso en cuanto se produce un nuevo paso del ratchet DH.
Ratchet simétrico (cadena de hash)
Entre los pasos del ratchet DH, un ratchet basado en hash deriva una clave de mensaje nueva a partir de la clave de cadena para cada mensaje:
message_key = HMAC-SHA256(chain_key, 0x01)
chain_key = HMAC-SHA256(chain_key, 0x02)
La clave de cadena antigua se elimina después de cada paso. La clave de mensaje cifra exactamente un mensaje y luego se elimina. Este es el mecanismo de secreto hacia adelante.
Progresión del ratchet
Visualizado, el Double Ratchet tiene este aspecto. Cada flecha es una función unidireccional — se puede avanzar, pero nunca retroceder:
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ₙ cifraría exactamente un mensaje y después se eliminaría, de modo que un atacante que obtenga MK₄ no podría derivar MK₃ ni MK₅. En la ruta de sealed box que se entrega hoy no hay ninguna clave por mensaje que robar ni ninguna cadena que recorrer — hay una única clave de largo plazo que lo abre todo.
6. Secreto hacia adelante — qué haría falta
PLANIFICADOEl secreto hacia adelante significa que comprometer las claves de largo plazo no compromete las claves de sesión pasadas. Railgun no lo tiene hoy. Dos mecanismos lo proporcionarían, y ambos pertenecen al protocolo planificado descrito arriba:
Claves efímeras en X3DH
El remitente genera un par de claves efímeras nuevo para cada sesión nueva y elimina la mitad privada después del handshake. Un atacante que más tarde robe las claves de identidad de ambas partes sigue sin poder reconstruir el secreto de sesión. Las sealed boxes sí usan una clave efímera nueva en el lado del remitente — pero la mitad del intercambio correspondiente al destinatario es su clave de largo plazo, que es precisamente la razón por la que la propiedad no se cumple.
Eliminación de las claves del ratchet
El Double Ratchet elimina las claves de cadena y las claves de mensaje antiguas después de usarlas. La cadena KDF es unidireccional — dada CKₙ, se puede calcular CKₙ₊₁ pero no CKₙ₋₁, de modo que un compromiso en el instante T no revela nada de lo enviado antes de T.
Seguridad tras el compromiso
El ratchet DH aporta además secreto futuro: un atacante que comprometa el estado actual de la sesión lo pierde en el siguiente paso del ratchet, que introduce aleatoriedad de la que él no dispone. Sin un ratchet, una clave comprometida le sigue siendo útil al atacante indefinidamente.
7. Arquitectura del relé y de la cola
Railgun retransmite sobres y puede ponerlos temporalmente en cola para la entrega a dispositivos desconectados. El relé está construido de forma que no necesita el texto plano de los mensajes, pero hoy recibe texto plano en dos casos: los mensajes de canal, que ningún cliente cifra, y los mensajes directos de la compilación de escritorio de Electron, cuyo manejador IPC codifica en base64 en lugar de cifrar. Un mensaje directo procedente de la ruta de sealed box llega como texto cifrado que el relé no puede abrir.
Lo que ve el servidor
El servidor conserva:
- • Paquetes de claves públicas
- • Información de registro de los usuarios
- • Metadatos y pertenencia de comunidades y canales
- • Texto cifrado de sealed box que no puede abrir (mensajes directos de la ruta de navegador/desarrollo)
- • Texto plano de mensajes directos codificado en base64, procedente de la compilación de escritorio de Electron
- • El contenido de los mensajes de canal, hasta que se entregue el cifrado de canales
- • Registros de auditoría de autenticación
El servidor nunca recibe:
- • Claves privadas o de identidad
- • El estado de sesión del cliente
- • Una copia duradera de su historial de mensajes
Los mensajes se retransmiten en tiempo real a los destinatarios conectados a través de WebSocket. Para los dispositivos desconectados, los sobres se ponen en cola en Redis con un TTL máximo de 30 días y se eliminan en cuanto el dispositivo los confirma. No hay ningún archivo de mensajes en el servidor — el historial del cliente reside en el almacenamiento local de su propio dispositivo, de modo que un dispositivo perdido es un historial perdido.
8. Resumen de las propiedades de seguridad
| Propiedad | Mecanismo | Estado |
|---|---|---|
| Confidencialidad de los mensajes directos | Sealed boxes solo en la ruta de navegador/desarrollo; el manejador de escritorio de Electron codifica en base64 | Parcial |
| Integridad de los mensajes directos | Etiqueta Poly1305 en la ruta de sealed box; la ruta de escritorio de Electron envía base64 sin autenticar | Parcial |
| Las claves permanecen en el dispositivo | Almacén de claves local; ninguna ruta de código transmite claves privadas | Implementado |
| Entrega sin conexión | Sobres en cola en Redis con un TTL de 30 días, eliminados al confirmarse | Implementado |
| Confidencialidad de los canales | La distribución de claves de grupo no está implementada; el contenido se envía legible | No implementado |
| Autenticación del remitente | Las sealed boxes son anónimas por construcción | No implementado |
| Secreto hacia adelante | Requiere el Double Ratchet, que ningún cliente instancia | Planificado |
| Recuperación tras un compromiso | Requiere el ratchet DH, que ningún cliente instancia | Planificado |
| Auditoría independiente | No se afirma haber completado ninguna auditoría criptográfica por parte de terceros | No completada |
| Garantía poscuántica | No se afirma ninguna garantía general de seguridad poscuántica | No se afirma |
De dónde sale esta página
Cada estado indicado arriba se ha leído del código fuente del cliente y no de un documento de diseño. El repositorio del cliente no es público en este momento, así que no podemos pedirle que lo compruebe usted mismo — lo cual es una razón para valorar esta página como una divulgación y no como una prueba. Las preguntas sobre alguna afirmación concreta son bienvenidas en security@railgun.chat.