End-to-End Encryption (E2EE)

MorisBox encrypts in-app content so its servers store only ciphertext and metadata. Private keys never leave user or agent devices.

What is encrypted

ContentE2EE
Support conversations (user ↔ partner agents)Yes
Hub messages marked encryption: "e2e"Yes
Media inside E2E envelopesYes
OTP / SMS / WhatsApp fallback bodiesNo (those channels must be able to read them)
Metadata (partner, timestamps, state, and size)No

E2E messages store their content in-app only. If the user is not online in the app (no device activity for about five minutes), MorisBox also sends an opaque WhatsApp alert, followed by SMS:

New message from {partner} in your MorisBox inbox. Open the app to read it (secure message).

This alert never includes the encrypted body. OTP and sign-in codes are not E2EE, so users can always access their accounts.

Protocol (v1)

ElementAlgorithm
Device identityX25519
Key agreementECDH (X25519)
KDFHKDF-SHA-256
Content encryptionAES-256-GCM
Wire encodingBase64url
content_key  →  AES-GCM encrypt(plaintext JSON)
wrap(content_key) per recipient device via ECDH + AES-GCM

A Signal-style double ratchet is not part of v1; this hardening is planned.

Device key directory

User devices

After sign-in, the client generates an identity key pair and publishes its public half:

POST /api/v1/me/devices/keys
Authorization: Bearer <user_jwt>
Content-Type: application/json

{
  "identity_key_pub": "<b64url x25519 public key>"
}

Partner agent devices (Partner Hub)

POST /api/v1/agent/devices/keys
Authorization: Bearer <partner_api_key>

{
  "device_uuid": "<browser session uuid>",
  "identity_key_pub": "<b64url>"
}

Retrieve keys for a phone number

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": [ ... ]
}

Send an encrypted hub message

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"
}

The server stores only the envelope. body is never persisted in plaintext.

Errors:

CodeMeaning
e2e_no_devicesThe user has not published identity keys yet
validationMissing e2e_envelope.ciphertext / nonce

Conversation messages (client)

POST /api/v1/me/conversations/<ref>/messages
Authorization: Bearer <user_jwt>

{
  "encrypted": true,
  "e2e_mode": "conversation",
  "e2e_key_version": 3,
  "e2e_envelope": { ... }
}

Upload content-key wraps:

POST /api/v1/me/conversations/<ref>/keys
{
  "key_version": 3,
  "wraps": [
    {
      "device_uuid": "...",
      "device_kind": "user",
      "wrap": "...",
      "wrap_nonce": "...",
      "sender_eph_pub": "..."
    }
  ]
}

Peer directory:

GET /api/v1/me/conversations/<ref>/devices

Client behavior

ClientBehavior
Client Application / MobileGenerate keys at sign-in; encrypt chat; decrypt envelopes for display
Partner HubPublish agent keys; encrypt replies / sealed hub sends
ServerRoute ciphertext; never decrypt

The UI displays a padlock label on encrypted messages. If the device has no wrap, the user sees “unable to decrypt.”

Security notes

  • Private keys remain on the device (SecureStore / local storage; harden toward hardware-backed keys where available).
  • AAD binds the envelope to a message-context string to reduce substitution attacks.
  • Revoked devices are excluded from new wraps.
  • Never log decrypted bodies on servers or gateways.

See also