Skip to main content

E2E Encryption — Requirements

Overview

All messages and direct data exchanged between peers are E2E encrypted using NaCl box (ECDH key exchange + XSalsa20-Poly1305). The server never has access to plaintext message content.


User Stories

IDAs a…I want to…So that…
EE-01userhave my messages encrypted on my device before being sentthe server cannot read them
EE-02usersend encrypted messages to peers over both the WebSocket relay and the direct LAN TCP channelencryption is consistent regardless of transport
EE-03userhave my encryption keys protected by my passwordlosing the device does not expose past messages
EE-04userrecover my encryption keys using any of my registered recovery methodsI don't lose message history if I lose access to my device
EE-05userhave my public key published to the serverother users can encrypt messages addressed to me
EE-06systemsign peer public keysclients can detect tampering with the key registry

Functional Requirements

FR-EE-01 — Key generation

  • On first login, the app generates a Curve25519 ECDH key pair (PeerKey): public key and private key.
  • The private key is never transmitted to the server in plaintext.
  • The public key is registered with the server via POST /keys/register.

FR-EE-02 — Key storage (at rest)

  • The private key is wrapped (encrypted) with a DeviceKey derived from the user's password (Argon2 + key derivation).
  • The wrapped private key is stored server-side as WrappedKey (POST /users/wrapped-key).
  • The DeviceKey is never stored — re-derived at login from the user's password.

FR-EE-03 — Key recovery

  • A copy of the WrappedKey encrypted under each recovery method is stored as WrappedKeyRecovery.
  • Recovery methods: email OTP, phone OTP, security question, recovery key file.
  • On recovery, the user obtains the appropriate WrappedKeyRecovery copy and decrypts it using the recovery credential.

FR-EE-04 — Message encryption

  • Sender looks up the recipient's Curve25519 public key via GET /keys/<peer_id>.
  • Sender verifies the server's Ed25519 signature on the public key (signed_credential).
  • Sender encrypts the message using NaCl box (ECDH shared secret + XSalsa20-Poly1305).
  • Encrypted ciphertext is stored in message.content and synced to the server.

FR-EE-05 — Contact key caching

  • After first contact, the peer's public key is cached locally as ContactKey to avoid repeated server lookups.

Non-Functional Requirements

IDRequirement
NFR-EE-01The server must never receive or store plaintext message content
NFR-EE-02Key derivation must use Argon2 with sufficient parameters to resist brute-force
NFR-EE-03The server's Ed25519 signing key (SERVER_ED25519_SEED) must be set — if unset, key signing is disabled
NFR-EE-04Encryption applies to both WebSocket-relayed and LAN TCP direct messages

Out of Scope

See design.md#non-goals for what this feature explicitly does not cover.