Security whitepaper · v1

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

  1. 01The server never sees your plaintext messages. Not even briefly in memory — encryption and decryption happen in your browser only.
  2. 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.
  3. 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 exchangeECDH over NIST P-256 (secp256r1)
Symmetric cipherAES-256-GCM (96-bit IV, 128-bit tag)
Message key derivationHKDF (implicit via ECDH → AES-GCM)
Passphrase key derivationPBKDF2-HMAC-SHA256, 600,000 iterations
Fingerprint hashSHA-256, double-hashed, truncated to 60 digits
Randomnesscrypto.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:

  1. Fetches the recipient's current public key from the server (cached 2 min).
  2. Generates a fresh ephemeral keypair for this one message only.
  3. Performs ECDH between the ephemeral private key and the recipient's public key → derives a 256-bit AES-GCM key.
  4. Pads the plaintext to the next 256-byte boundary with random bytes (prevents length analysis).
  5. Encrypts with AES-256-GCM using a random 96-bit IV.
  6. Sends { ciphertext, iv, ephemeral_public_key } over WebSocket.
  7. 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:

  1. The ciphertext arrives over WebSocket and is displayed in locked form.
  2. The user taps Decrypt, triggering a WebAuthn biometric prompt.
  3. The device unlocks the private key from IndexedDB.
  4. ECDH runs between the private key and the message's ephemeral public key → derives the same AES-GCM key.
  5. AES-256-GCM decrypts and verifies the auth tag. Any tampering fails verification loudly.
  6. 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.

IronSeal Security Whitepaper · v1← Settings