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

ContenidoE2EE
Conversaciones de soporte (usuario ↔ agentes del socio)
Mensajes del hub marcados con encryption: "e2e"
Archivos multimedia incluidos en sobres E2E
Cuerpo de OTP, SMS o respaldo por WhatsAppNo (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)

ElementoAlgoritmo
Identidad del dispositivoX25519
Acuerdo de clavesECDH (X25519)
KDFHKDF-SHA-256
Cifrado del contenidoAES-256-GCM
Codificación de transporteBase64url
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ódigoSignificado
e2e_no_devicesEl usuario todavía no ha publicado claves de identidad
validationFalta 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

ClienteComportamiento
Aplicación cliente / móvilGenerar las claves al iniciar sesión; cifrar el chat; descifrar los sobres para mostrarlos
Partner HubPublicar las claves del agente; cifrar las respuestas y los envíos sellados al hub
ServidorEnrutar 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