End-to-End Encryption
Signal Protocol
Relay implements the Signal Protocol for end-to-end encrypted messaging. This is the same protocol used by Signal, WhatsApp, and others — widely regarded as the gold standard for private messaging.
The implementation is built from scratch in TypeScript using audited @noble/* cryptographic libraries, with no dependency on Signal's C/Java libraries. This makes it fully compatible with React Native and avoids native module complexity.
In plain English: Before a message leaves your phone, it gets scrambled with a key that only the recipient's phone has. The server carries the scrambled message but cannot unscramble it. Even if someone recorded every encrypted message you ever sent and later stole your current key, they still could not read old messages. That property is called forward secrecy, and it is baked into every message Relay sends.
How It Works: The Two-Phase Approach
Signal Protocol has two major phases:
- X3DH (Extended Triple Diffie-Hellman) — establishes a shared secret between two people who have never communicated before.
- Double Ratchet — uses that shared secret to encrypt every subsequent message, generating a new key for each one.
Phase 1: X3DH Key Agreement
When Alice wants to message Bob for the first time, she needs to establish a shared secret — even though Bob might be offline. X3DH solves this using pre-uploaded key bundles.
Pre-Key Bundles
When a user registers, their device generates and uploads a pre-key bundle to the server:
| Key | Type | Purpose |
|---|---|---|
| Identity key | Ed25519 | Long-term identity (converted to X25519 for DH) |
| Signed pre-key | X25519 | Medium-term DH key, signed by identity key |
| One-time pre-keys (100) | X25519 | Single-use DH keys for forward secrecy |
The server stores only public keys. Private keys never leave the device.
The Key Agreement
The four DH operations provide:
- DH1 + DH3: Mutual authentication (both identity keys are involved)
- DH2: Forward secrecy against compromise of Bob's signed pre-key
- DH4: Per-session uniqueness (one-time pre-key is consumed and never reused)
The shared secret is derived using HKDF-SHA256 with the info string "X3DHSharedSecret".
Ed25519 to X25519 Conversion
Identity keys are Ed25519 (for signing), but Diffie-Hellman requires X25519. The protocol converts between the two curve representations using the standard birational map (Montgomery to Edwards and back). This is handled by @noble/curves via edwardsToMontgomery.
Phase 2: Double Ratchet
Once X3DH establishes a shared secret, the Double Ratchet takes over for all subsequent messages. It provides two critical properties:
- Forward secrecy: Compromising a current key does not reveal past messages.
- Post-compromise security: If a key is compromised and later recovered, future messages become secure again.
How the Ratchet Works
The Double Ratchet maintains three pieces of state:
- Root key — long-lived, advances on each DH ratchet step
- Sending chain key — advances for each outgoing message
- Receiving chain key — advances for each incoming message
DH Ratchet: Each time the conversation "turns" (Alice sends, then Bob sends), a new Diffie-Hellman exchange occurs. This generates a new root key, which means even if an attacker compromises the current state, they lose access as soon as the ratchet advances.
Symmetric Ratchet (KDF Chain): Within a single turn, each message gets its own key derived from the chain key via HKDF. The chain key is updated so the previous message key cannot be re-derived.
Message Encryption
Each message key is expanded via HKDF into three components:
- AES key (32 bytes) — for AES-256-CBC encryption
- HMAC key (32 bytes) — for HMAC-SHA256 authentication
- IV (16 bytes) — initialization vector for CBC mode
The plaintext is PKCS7-padded, encrypted with AES-256-CBC, then authenticated with HMAC-SHA256. The format is: ciphertext || MAC.
Skipped Messages
If messages arrive out of order (e.g., message 5 arrives before message 4), the ratchet can "skip ahead" by advancing the chain key and caching the intermediate message keys. Up to 200 skipped message keys are stored per session to handle reordering.
Wire Format
Messages sent over the network have one of two types:
| Type | When | Contains |
|---|---|---|
prekey | First message to a new recipient | X3DH header (identity key, ephemeral key, used pre-key ID) + encrypted message |
whisper | All subsequent messages | Ratchet header (current DH public key, counters) + encrypted message |
Both are serialized as JSON, base64-encoded, and sent as an opaque body string. The server never parses the contents.
Group Messages
Group messages use pairwise encryption — each message is individually encrypted for every member of the group using their respective Signal sessions.
Alice sends "Hello" to a group of 3:
→ Encrypt with Alice↔Bob session → ciphertext_bob
→ Encrypt with Alice↔Carol session → ciphertext_carol
→ Encrypt with Alice↔Dave session → ciphertext_daveEach recipient receives their own ciphertext and decrypts it with their own session state. This means:
- The server cannot read any version of the message.
- Each member has an independent ratchet state with Alice.
- Removing a member immediately cuts off their access (no re-keying needed for existing sessions).
In plain English: Group messages are not encrypted once for the group — they are encrypted separately for each person in the group, using a unique key that only you and that person share. This means the server sees multiple encrypted blobs per group message, one for each member, and cannot read any of them.
Storage Encryption Layers
Signal Protocol keys and session state need to persist on-device between app launches. Relay uses a three-tier encryption model to protect this data:
Tier 1: OS Keychain (Most Sensitive)
Technology: expo-secure-store
Stores the Signal identity key pair, signed pre-key, and registration ID. These are the most sensitive Signal Protocol keys — the identity key is long-lived and losing it would require re-establishing all sessions.
Hardware-backed, biometric-gated on supported devices.
Tier 2: EncryptedStorage (Session Data)
Technology: AES-256-GCM wrapping AsyncStorage
Stores pre-key private keys and ratchet session state (root keys, chain keys, skipped message keys).
The encryption key is derived from the Signal identity private key via HKDF-SHA256 with the info string "RelayEncryptedStorage". This means:
- If an attacker gets filesystem access but not the OS keychain, session data is encrypted.
- The encryption key is deterministic — derived from the identity key, not stored separately.
Tier 3: Wallet Keychain (Financial Keys)
Technology: react-native-keychain
Stores the wallet private key, auth private key, and mnemonic. Uses the strongest available protection:
BIOMETRY_ANY_OR_DEVICE_PASSCODEaccess controlWHEN_UNLOCKED_THIS_DEVICE_ONLYaccessibilitySECURE_HARDWAREsecurity level
In plain English: Your keys are locked behind three layers. The most important keys (identity and wallet) live in your phone's secure hardware — the same vault that protects Face ID and Touch ID data. Session data is encrypted with a key that itself is locked in the vault. An attacker would need to break through the hardware security module to get at anything useful.