How it works
Encra uses two layers of cryptography. X25519 establishes a shared secret between two parties. Double Ratchet ensures every message uses a unique key that is deleted after use.
Alice's device generates key pair
crypto_box_keypair() — private key never leaves device
Alice registers public key with server
POST /v1/keys — only the public key is sent
Alice fetches Bob's public key
GET /v1/keys/bob
Alice derives shared secret (locally)
ECDH(alicePriv, bobPub) — same as ECDH(bobPriv, alicePub)
Alice initialises Double Ratchet
DoubleRatchet.initSender(shared, bobPub)
Alice encrypts message
ratchet.encrypt('Hello!') — unique key per message
Server relays ciphertext blob to Bob
Server sees: { ciphertext, nonce, header } — nothing readable
Bob decrypts with his ratchet
ratchet.decrypt(msg) — message key deleted after use
Key generation
When a user first connects, Encra generates an X25519 key pair entirely on their device using libsodium's cryptographically secure random number generator. The private key is stored locally (in memory for the session, or IndexedDB for persistence) and is never transmitted anywhere.
// What happens on device
const { publicKey, privateKey } = crypto_box_keypair()
// publicKey → registered with server
// privateKey → stays on device FOREVER
Key exchange (X25519 ECDH)
To chat with Bob, Alice fetches his public key from the server. She then runs Elliptic Curve Diffie-Hellman (X25519) locally:
Alice's side
ECDH(alicePriv, bobPub)
→ sharedSecret
Bob's side
ECDH(bobPriv, alicePub)
→ same sharedSecret
Both sides arrive at the same 32-byte shared secret without ever transmitting it. This is the mathematical property of Diffie-Hellman. The server cannot compute this secret because it never has both private keys.
ℹ Why X25519?
Message encryption
Messages are encrypted using XSalsa20-Poly1305 — a stream cipher combined with a message authentication code (MAC). Every message gets a randomly generated 24-byte nonce to ensure uniqueness.
encrypt(plaintext, key, nonce)
→ ciphertext (same length as plaintext + 16 byte MAC)
decrypt(ciphertext, key, nonce)
→ plaintext (or throws if MAC verification fails)
The MAC means any tampering with the ciphertext is detected and the decryption is rejected. The server cannot modify messages without Alice and Bob noticing.
Double Ratchet
The Double Ratchet is what makes Encra truly Signal-level. Without it, every message would use the same shared secret — compromise the key once and all messages are readable.
Symmetric Ratchet Chain
DH Ratchet (on direction change)
When the conversation direction flips (Alice→Bob becomes Bob→Alice), a new ephemeral key pair is exchanged and the Root Key is updated. This provides break-in recovery — even if an attacker compromises the current state, they lose access after the next DH step.
The two properties this gives you:
Forward secrecy
Message keys are deleted after use. Compromising state today cannot reveal past messages — their keys are gone forever.
Break-in recovery
After the next DH ratchet step, an attacker with compromised state loses access. Future messages are safe even after a breach.
What the server does (and doesn't do)
✓ Server stores
user_id → public_key mapping
Encrypted ciphertext blobs (offline delivery)
Timestamps and metadata
✗ Server never sees
Private keys
Shared secrets
Plaintext messages
Ratchet state
ℹ Zero trust