Core Concepts

Key exchange

Before Alice can send Bob an encrypted message, both sides need a shared secret — without transmitting that secret across the network. X25519 ECDH is the mathematical trick that makes this possible.

The problem

Symmetric encryption (like XSalsa20-Poly1305) requires both Alice and Bob to know the same key to encrypt and decrypt. But how do they agree on that key over a public network, where an eavesdropper can read every message they exchange?

1

Can't send the key

If Alice sends the secret key to Bob, anyone intercepting the message learns the key and can decrypt every future message.

2

Can't meet in person

Two strangers on the internet can't safely exchange a secret over a telephone or in person before they start chatting.

3

ECDH solves it

Elliptic Curve Diffie-Hellman lets two parties each contribute public values. Only someone with a private key can derive the shared secret.

How X25519 ECDH works

X25519 is a Diffie-Hellman function over the elliptic curve Curve25519. Each party generates a key pair — a random 32-byte private key and a corresponding 32-byte public key. The public key can be shared freely; the private key never leaves the device.

The core property of the DH function is commutative:

Alice computes:X25519(alice_priv, bob_pub)→ S
Bob computes:X25519(bob_priv, alice_pub)→ S
Both S values are identical — this is the mathematical guarantee of Diffie-Hellman.

Why is it secure?

Given only alice_pub and bob_pub (which the server has), computing S requires solving the Elliptic Curve Discrete Logarithm Problem — believed to require ~2128 operations on current hardware. There is no known classical algorithm that can do this in practical time.

Visual walkthrough

X25519 ECDH — both sides derive the same secret independently
Alice
generates once:
alice_priv← stays on device
alice_pub← shared openly
computes locally:
X25519(alice_priv, bob_pub)
→ shared_secret ✓
alice_pub →
server
relay
← bob_pub
Bob
generates once:
bob_priv← stays on device
bob_pub← shared openly
computes locally:
X25519(bob_priv, alice_pub)
→ shared_secret ✓
Both arrive at the identical 32-byte secret — without ever transmitting it.The server only ever saw the two public keys.

What the server sees

Encra's key server acts as a public directory — nothing more. It stores user IDs mapped to their public keys, so Alice can look up Bob's key before sending her first message. The server relays encrypted blobs and is mathematically incapable of reading them.

✓ What the server stores

user_id"alice"plain
device_id"device-uuid"plain
public_key"base64..."safe to store

✗ What the server never has

private_keynever transmitted
shared_secretnever transmitted
plaintextnever transmitted
ratchet_statenever transmitted

Multi-device

Each device generates its own key pair and registers its own public key under the same userId. When Bob sends Alice a message, Encra fetches all of Alice's registered public keys and encrypts one independent copy per device. Every device can decrypt its own copy; no device can decrypt another device's copy.

Multi-device — each device gets its own key pair
Alice — Phonepriv_A1 stays localpub_A1 → registered
Alice — Laptoppriv_A2 stays localpub_A2 → registered
Alice — Tabletpriv_A3 stays localpub_A3 → registered
Bob sends a message → 3 independent ciphertext copies (one per pub key) — all delivered simultaneously

Code

The key exchange primitives are exposed directly from @encra/core for advanced use. In practice, useE2EChat and EncraClient handle key generation, registration, and exchange automatically.

typescript
import {
  generateKeyPair,
  exportKey,
  importKey,
  deriveSharedSecret,
} from '@encra/core'

// --- Alice's device: generate once, store in IndexedDB ---
const aliceKeys = await generateKeyPair()
// aliceKeys.publicKey  → Uint8Array (32 bytes) — share this
// aliceKeys.privateKey → Uint8Array (32 bytes) — never share this

// Serialise for storage / transmission (URL-safe base64)
const alicePubB64 = await exportKey(aliceKeys.publicKey)

// --- Restore from storage ---
const alicePubRestored = await importKey(alicePubB64)

// --- Both sides: derive the same shared secret ---
// Alice
const sharedAlice = await deriveSharedSecret(
  aliceKeys.privateKey,
  bobPublicKey,            // fetched from GET /v1/keys/bob
)

// Bob (independently — same result)
const sharedBob = await deriveSharedSecret(
  bobKeys.privateKey,
  alicePublicKey,
)

// sharedAlice === sharedBob — they never exchanged this value

💡 Key fingerprints for verification

Use generateFingerprint(alicePub, bobPub) to produce a Signal-style safety number (BLAKE2b-256). Show it to both parties out-of-band (voice call, QR code) to confirm no MITM is in the path. See Cryptographic primitives.

Security properties

PropertyValueNotes
Key size256 bits (X25519)128-bit security level — equivalent to 3072-bit RSA
AlgorithmX25519 / Curve25519Constant-time; immune to timing side-channel attacks
Server knowledgePublic keys onlyServer cannot derive shared secret from public keys alone
MITM resistanceKey fingerprintsgenerateFingerprint() for out-of-band verification
Post-quantumSoon (ML-KEM-768)PQXDH hybrid planned — classical + quantum-resistant
Key storageIndexedDB (device-only)Private keys never leave the device; not synced to server

MITM window

The X25519 exchange is only secure if you trust that the public key you fetched belongs to the real Bob. Encra's server is a trusted relay — if it were compromised and substituted a different key, you would encrypt to the attacker. Key fingerprint verification closes this window. Post-quantum key encapsulation (ML-KEM-768 + PQXDH) is on the roadmap.

Encra AI

Ask me anything · docs, code, troubleshooting

Hi, I'm Encra AI

I can explain concepts, generate starter code, troubleshoot errors, and guide your setup.

May make mistakes · verify critical crypto details