How IronSeal encrypts your messages
A plain-English specification of IronSeal's cryptography. We publish this so you can verify our zero-knowledge claim — not take it on faith.
Three promises
- 01The server never sees your plaintext messages. Not even briefly in memory — encryption and decryption happen in your browser only.
- 02Your private key never leaves your device in readable form. If you enable a recovery passphrase, only a ciphertext copy is stored — unreadable to the server.
- 03Every message uses a throwaway key. Compromising your long-term key does not decrypt past messages (Perfect Forward Secrecy).
1. Cryptographic primitives
We use only well-known, NIST-standardised algorithms. No custom crypto. Everything runs on the browser's built-in SubtleCrypto API.
| Identity key exchange | ECDH over NIST P-256 (secp256r1) |
| Symmetric cipher | AES-256-GCM (96-bit IV, 128-bit tag) |
| Message key derivation | HKDF (implicit via ECDH → AES-GCM) |
| Passphrase key derivation | PBKDF2-HMAC-SHA256, 600,000 iterations |
| Fingerprint hash | SHA-256, double-hashed, truncated to 60 digits |
| Randomness | crypto.getRandomValues (CSPRNG) |
2. Keys
When you register, your browser generates an ECDH P-256 keypair— a public key and a private key. The public key is uploaded to the server so others can send you messages. The private key is encrypted with a device-bound passphrase and stored in your browser's IndexedDB.
The private key is never transmitted in plaintext.The only exception is the optional recovery-passphrase backup, which stores a PBKDF2-encrypted blob on our server. That blob is cryptographically indistinguishable from random noise to anyone without the passphrase — including us.
3. Sending a message
When you send a message, your browser:
- Fetches the recipient's current public key from the server (cached 2 min).
- Generates a fresh ephemeral keypair for this one message only.
- Performs ECDH between the ephemeral private key and the recipient's public key → derives a 256-bit AES-GCM key.
- Pads the plaintext to the next 256-byte boundary with random bytes (prevents length analysis).
- Encrypts with AES-256-GCM using a random 96-bit IV.
- Sends
{ ciphertext, iv, ephemeral_public_key }over WebSocket. - Discards the ephemeral private key. It is never stored, never reused.
This last step is what gives us Perfect Forward Secrecy: even if an attacker later compromises your long-term private key, they still cannot decrypt past messages because each message's ephemeral key was destroyed immediately after use.
4. Receiving a message
On the recipient side:
- The ciphertext arrives over WebSocket and is displayed in locked form.
- The user taps Decrypt, triggering a WebAuthn biometric prompt.
- The device unlocks the private key from IndexedDB.
- ECDH runs between the private key and the message's ephemeral public key → derives the same AES-GCM key.
- AES-256-GCM decrypts and verifies the auth tag. Any tampering fails verification loudly.
- The padded plaintext is unpadded, sanitized for XSS, and rendered.
After three failed biometric attempts, the app auto-locks for 30 minutes. A per-message re-lock timer re-encrypts plaintext out of memory after a short window.
5. Group messages
Each message in a group is encrypted separately for every recipientusing that recipient's current public key. There is no shared group key for anyone to steal. A group of five people produces five independently-encrypted envelopes of the same plaintext — each decryptable only by its intended reader.
6. What the server stores
The database schema for a message is:
EncryptedMessage {
id: UUID
conversation_id: UUID
sender_id: UUID
recipient_id: UUID (groups only)
ciphertext: base64 ← unreadable to us
iv: base64
ephemeral_public_key: JWK
timestamp: ISO8601
}No message body. No decryption keys. No plaintext logs. The WebSocket (ChatConsumer) is a pure relay — it validates that the encryption fields are present and broadcasts to the channel group. It never inspects content.
7. What we protect against
- ✓Server compromise — a full database leak reveals only ciphertext.
- ✓Malicious insider — staff cannot read messages, even with admin access.
- ✓Network observer — TLS + E2EE; attacker sees only that traffic exists.
- ✓Long-term key theft — past messages stay safe (PFS).
- ✓Replay attacks — each IV is single-use; AES-GCM rejects reuse.
- ✓Message tampering — AES-GCM auth tag fails verification.
- ✓Brute-force on lost device — 3-attempt biometric lockout + 30-min cooldown.
8. What we don't protect against
Honesty matters. These are the residual risks inherent to any E2EE system:
- •Device compromise. If your logged-in device is stolen unlocked, the thief can read messages. No cloud product can prevent this.
- •Screenshot / screen-record. Recipients can photograph their screen. No software stops this.
- •Metadata. The server knows who sent a message to whom and when. Only the content is private.
- •Lost recovery passphrase. Without it, losing your device means losing access to old messages. We cannot reset it — that would break zero-knowledge.
9. Verify for yourself
Everything described here is directly observable in the IronSeal client. Open your browser's DevTools → Network tab and send a message. You'll see the WebSocket payload is ciphertext only. Inspect the source — the crypto module is insrc/lib/crypto.tsand the server message model has no plaintext field.
"Trust us" is not a security model. "Read the code" is.