Cifrado de extremo a extremo (E2EE)
MorisBox cifra el contenido in-app para que los servidores almacenen únicamente el texto cifrado y los metadatos. Las claves privadas nunca salen de los dispositivos del usuario o del agente.
Qué se cifra
| Contenido | E2EE |
|---|---|
| Conversaciones de soporte (usuario ↔ agentes del socio) | Sí |
Mensajes del hub marcados con encryption: "e2e" | Sí |
| Archivos multimedia incluidos en sobres E2E | Sí |
| Cuerpo de OTP, SMS o respaldo por WhatsApp | No (debe ser legible en esos canales) |
| Metadatos (socio, marcas de tiempo, estado, tamaño) | No |
Los mensajes E2E almacenan el contenido únicamente in-app. Si el usuario no está conectado en la aplicación (ninguna actividad del dispositivo durante unos 5 minutos), MorisBox también envía una alerta opaca por WhatsApp (y después por SMS):
Nuevo mensaje de {partner} en su bandeja de MorisBox. Abra la aplicación para leerlo (mensaje seguro).
Esta alerta nunca incluye el cuerpo cifrado. Los códigos OTP y de inicio de sesión no usan E2EE, de modo que los usuarios siempre puedan acceder a su cuenta.
Protocolo (v1)
| Elemento | Algoritmo |
|---|---|
| Identidad del dispositivo | X25519 |
| Acuerdo de claves | ECDH (X25519) |
| KDF | HKDF-SHA-256 |
| Cifrado del contenido | AES-256-GCM |
| Codificación de transporte | Base64url |
content_key → AES-GCM encrypt(plaintext JSON)
wrap(content_key) per recipient device via ECDH + AES-GCM
El doble ratchet al estilo de Signal no forma parte de la v1 (se prevé reforzarlo).
Directorio de claves de dispositivos
Dispositivos de usuario
Después de iniciar sesión, el cliente genera un par de claves de identidad y publica la mitad pública:
POST /api/v1/me/devices/keys
Authorization: Bearer <user_jwt>
Content-Type: application/json
{
"identity_key_pub": "<b64url x25519 public key>"
}
Dispositivos de agentes del socio (Partner Hub)
POST /api/v1/agent/devices/keys
Authorization: Bearer <partner_api_key>
{
"device_uuid": "<browser session uuid>",
"identity_key_pub": "<b64url>"
}
Obtener las claves de un teléfono
GET /api/v1/keys/+228XXXXXXXX
Authorization: Bearer <partner_api_key>
{
"phone": "+228XXXXXXXX",
"devices": [
{
"device_uuid": "...",
"identity_key_pub": "...",
"platform": "web",
"has_keys": true
}
],
"agent_devices": [ ... ]
}
Enviar un mensaje cifrado al hub
POST /api/v1/messages
Authorization: Bearer <partner_api_key>
Content-Type: application/json
{
"phone": "+228XXXXXXXX",
"encryption": "e2e",
"e2e_envelope": {
"v": 1,
"alg": "x25519-hkdf-sha256-aes-256-gcm",
"mode": "sealed",
"key_version": 0,
"sender_device_uuid": "partner-agent-…",
"ciphertext": "<b64url>",
"nonce": "<b64url>",
"aad": "seed360|msg|<ref>|v1",
"key_wraps": [
{
"device_uuid": "user-device-…",
"wrap": "<b64url>",
"wrap_nonce": "<b64url>",
"sender_eph_pub": "<b64url>"
}
]
},
"content_type": "text"
}
El servidor almacena únicamente el sobre. body nunca se conserva como texto claro.
Errores:
| Código | Significado |
|---|---|
e2e_no_devices | El usuario todavía no ha publicado claves de identidad |
validation | Falta e2e_envelope.ciphertext o nonce |
Mensajes de conversación (cliente)
POST /api/v1/me/conversations/<ref>/messages
Authorization: Bearer <user_jwt>
{
"encrypted": true,
"e2e_mode": "conversation",
"e2e_key_version": 3,
"e2e_envelope": { ... }
}
Cargue los wraps de la clave de contenido:
POST /api/v1/me/conversations/<ref>/keys
{
"key_version": 3,
"wraps": [
{
"device_uuid": "...",
"device_kind": "user",
"wrap": "...",
"wrap_nonce": "...",
"sender_eph_pub": "..."
}
]
}
Directorio de participantes:
GET /api/v1/me/conversations/<ref>/devices
Comportamiento del cliente
| Cliente | Comportamiento |
|---|---|
| Aplicación cliente / móvil | Generar las claves al iniciar sesión; cifrar el chat; descifrar los sobres para mostrarlos |
| Partner Hub | Publicar las claves del agente; cifrar las respuestas y los envíos sellados al hub |
| Servidor | Enrutar el texto cifrado; nunca descifrarlo |
La interfaz muestra un indicador de candado en los mensajes cifrados. Si el dispositivo no tiene un wrap, el usuario ve «no se puede descifrar».
Notas de seguridad
- Las claves privadas permanecen en el dispositivo (SecureStore / almacenamiento local; evolucione hacia claves respaldadas por hardware cuando estén disponibles).
- El AAD vincula el sobre con una cadena de contexto del mensaje para reducir los ataques de sustitución.
- Los dispositivos revocados quedan excluidos de los wraps nuevos.
- No registre cuerpos descifrados en servidores ni pasarelas.
Véase también
- Plataformas: dónde se aplica E2EE
- Mensajes: tipos de contenido
- Conversaciones: hilos de soporte