Howprotectionworks.
Twelve independent layers, anti-forensic envelopes that resist seizure, live-memory defenses that survive a debugger, and a wipe sequence that runs in the right order under coercion. Every defense is on this page — with the math, not the marketing.
Defense in depth,
named one by one.
Each layer has a specific job and a specific assumed attacker. Break one, the others hold. The brightening row below is the Double Ratchet advancing — a new per-message key every send.
Why
Forensic tools (UFED, GrayKey, AXIOM) extract the keychain byte-for-byte. Without this layer, that dump is your secrets.
How
The PIN derives a wrapping key through a memory-hard KDF. The wrapping key opens a secretbox containing the real device key.
Params
memory = 64 MiB
iterations = 3
parallelism = 4
output = 32 B
Lifecycle
Re-wrapped atomically on every PIN change. Brute-force floor ~6 weeks per device under enclave throttling.
Why
Message history is the highest-value target on a captured device. An unencrypted database is a full leak.
How
The whole database file plus its WAL, SHM and lock files are AES-encrypted with a 512-bit key held only in memory.
Params
cipher = AES-256-CBC
auth = HMAC-SHA-256
key = 64 B zerizable
compact = > 10 MB
Lifecycle
Excluded from iOS iCloud backup. Android allowBackup=false. Key zeroed on app backgrounding.
Why
Two devices need a private shared secret to encrypt. An eavesdropper must not be able to derive it.
How
Elliptic-curve Diffie-Hellman over Curve25519. Both sides compute the same 32-byte secret from their own private key and the other's public key.
Params
curve = Curve25519
private / public = 32 B
shared = 32 B
RFC = 7748
Lifecycle
Rejects all low-order points; rejects zero outputs. Combined with the post-quantum KEM into the root key via HKDF.
Why
An adversary recording traffic today bets a quantum computer will break X25519 within a decade. ML-KEM survives that bet.
How
Lattice-based key encapsulation. The peer encapsulates a 32-byte secret to your public key; you decapsulate with the matching private key.
Params
public key = 1184 B
ciphertext = 1088 B
shared = 32 B
level = NIST III
Lifecycle
Static keypair plus one-time pre-keys. Sticky per-contact session. Ciphertext hash bound into HKDF to prevent substitution.
Why
An active attacker who substitutes a public key in transit can impersonate. Pinning a signed identity prevents it.
How
The identity key signs every handshake transcript. Receivers verify against the pinned public key in four independent enforcement layers.
Params
private = 32 B
public = 32 B
signature = 64 B
compare = constant-time
Lifecycle
Rotation only via signed key-update with attestation. HMAC integrity check at rest. Mismatch is fail-closed, no silent TOFU.
Why
Quantum forgery of Ed25519 would unlock retroactive impersonation. The PQ signature survives that.
How
Lattice signature alongside Ed25519. Receivers require both to verify; one fail-closed for each.
Params
public key = ~1952 B
signature = ~3293 B
level = NIST III
fallback = Dilithium3
Lifecycle
Send is gated by peer acknowledgement. Receive fails closed if the signature is present but the pinned public key is absent.
Why
A future device compromise must not retroactively decrypt past messages, and past compromise must not break future ones.
How
Three-step ratchet: new DH on send, chain advance, message key derive. Each message gets its own key; old keys are destroyed.
Params
skip bound = 64
header = AEAD-enforced
mutex = per-contact
state = HMAC + 2x enc
Lifecycle
Skip keys keyed by DH-pubkey prefix and namespace. State persisted to secure storage with integrity, encrypted twice.
Why
Group messaging must not regress to a shared static key. Compromise of one member must not unlock the entire history.
How
Each sender maintains their own chain. The message key is mixed with a per-member encKey via HKDF, never replaced.
Params
skip TTL = 1 h
derive bound = 64
replay LRU = 2048 × 24 h
AAD = context-bound
Lifecycle
Epoch advances on every membership change. Root key rotated via sealed-box delivery to each remaining member.
Why
Without authenticated encryption, an attacker can flip a bit in the ciphertext and silently change the plaintext.
How
ChaCha20 stream cipher paired with a Poly1305 authentication tag on every chunk; any tampering causes decryption to fail.
Params
nonce = 192 bit
auth tag = 128 bit
chunk = 32 KB
terminator = TAG_FINAL
Lifecycle
Receiver requires TAG_FINAL on the last chunk or throws truncated stream — anti-truncation defense.
Why
A live RAM dump (JTAG, kernel hook) should never recover plaintext keys or message bodies.
How
Buffers are XOR-masked with a rotating key. Plaintext is revealed only inside a closure, then zeroed in a finally block.
Params
mask = 32 B random
derivation = HKDF per-block
rotation = every 5 min
zero = sodium.memzero
Lifecycle
All buffers zeroed on app background. use-after-zero throws synchronously. Bulk-clear on session expiry.
Why
WebRTC signaling is the soft underbelly of most messengers. Unauthenticated ICE candidates enable call hijack.
How
Every signaling message and every ICE candidate is individually Ed25519-signed and timestamped; receiver enforces a sliding window.
Params
window = 30 s
sequence = monotonic
transport = STUN-only
verify = double layer
Lifecycle
Short Authentication String (SAS) lets users verify the call out-of-band. Replay or duplicate-sequence drops at the protocol layer.
Why
Deleting plaintext alone does not prevent reconstruction. Old key material can decrypt yesterday's messages tomorrow.
How
On expiration, four buffers are zeroed in order: plaintext, message key, signed prekey, group symmetric key.
Params
keys zeroed = mk + spk + gsk
passes = 3 random + 1 zero
order = memory → db → disk
race-safe = yes
Lifecycle
Race-free against active writers. Forward-secrecy guarantee restored within seconds of expiration.
Without the PIN,
extraction yields noise.
Every long-term secret on the device is wrapped in an Argon2id-derived envelope before it ever touches storage. Forensic tools (UFED, GrayKey, AXIOM) can dump the underlying bytes — they cannot unwrap them. The PIN is never written to disk, never sent over the network, and never derivable from anything outside the user's head.
- → Argon2id parameters: m = 64 MiB · t = 3 · p = 4
- → Brute-force floor: ~6 weeks per device under enclave throttling
- → Plaintext keys purged from Keychain after first successful unlock
- → PIN change re-wraps 5 envelopes atomically; abort on any failure
7f 3a c1 88 e2 04 9b 14 d6 71 0a fe c9 22 b3 5c
b8 0c d3 41 27 ee a9 6d 4e 80 1b 97 22 c4 31 ff
2a 9f 67 03 11 d8 50 ee 63 a4 7c 8b 19 f2 06 c1
entropy = 7.998 bits/byte · indistinguishable from random
no PIN → no KEK → no plaintexttry { use(secret => chacha.encrypt(secret, nonce, plaintext)); } finally { sodium.memzero(buffer); // guaranteed on throw or return } // per-block HKDF — no repeating XOR pattern // mask key rotates every 5 minutes
Plaintext keys live
for microseconds.
Every sensitive buffer — message keys, identity material, decrypted bodies, the unlocked PIN — is wrapped in SecureString. Memory is XOR-masked at rest, revealed inside a closure for the operation that needs it, and zeroed in a finally block before control returns. The window an attacker has to dump RAM is measured in microseconds, not seconds.
- → Per-block HKDF keys — no repeating XOR pattern
- → Mask key rotated every 5 minutes
- → All SecureStrings zeroed on app backgrounding
- → use-after-zero throws synchronously
Wipe runs
in the right order.
A panic wipe is not a single call. Done wrong, write-back caches reintroduce plaintext after the secure delete completes. Lumes's wipe sequence runs in the order below — memory first, then the database closed cleanly, then a 3-pass random + zero overwrite of the storage files.
Triggers: duress PIN (silent), explicit panic button, automatic when the device risk score crosses 0.90, custody mode detection, or a remote notification.
- 01 · memoryZero every SecureString · destroy mask keyAll decrypted bodies, message keys, and the unlocked PIN are zeroed before anything else touches disk.
- 02 · enumerateFirst pass over secure storageWith the database still active, enumerate prekeys and group symmetric keys for explicit removal.
- 03 · closeClose the database cleanlyCritical — without this, write-back caches reintroduce plaintext after the physical wipe in step 05.
- 04 · crypto-eraseReset hardware-backed keystoreDevice key, Realm key, PIN hash, JWT — all destroyed in the hardware keystore.
- 05 · overwrite3-pass random + 1-pass zero on storage filesDatabase file, WAL, SHM, lock files, media vault, cache directory — overwritten then deleted.
- 06 · sweepSecond pass over secure storageIdempotent sweep — anything left behind by step 02 is removed.
- 07 · UIShow a generic errorThe user under coercion must not see evidence of the wipe. UI shows a routine "incorrect PIN" message.
Twenty probes,
sampled every 30 seconds.
A risk score, not a binary check. Each probe contributes weighted evidence; the score is an exponential moving average. Cross 0.70 and the app forces re-authentication. Cross 0.90 and it wipes.
Three locks
on the same door.
Messages, contacts, groups, and call history live in an encrypted local database. The database is itself protected by a key that is itself wrapped in a PIN-bound envelope. Each lock has to fail for plaintext to leak — and they fail independently.
- → 64-byte database key as zerizable buffer (not an immutable string)
- → Write-ahead-log, shared-memory, and lock files all protected
- → Excluded from iOS iCloud backup and Android allowBackup
- → Auto-compaction above 10 MB to limit forensic surface
The relay sees
a blob and a hash.
Sealed-sender envelopes hide the sender from the relay. Ciphertexts are padded to a fixed boundary so size doesn't leak. Timestamps carry up to five minutes of jitter. Every message body is delivered authenticated, and every WebRTC ICE candidate carries an Ed25519 signature with a 30-second anti-replay window.
- → Recipient identity = rotating HKDF hash, not a phone number
- → Monotonic WebSocket sequence — replay drops at the protocol layer
- → Optional Tor / SOCKS5 transport for network metadata
- → No analytics. No telemetry. No crash reports without opt-in.
Identity changes are
public events.
A messenger that lets the server silently swap a public key has already lost. Lumes publishes every identity change into an append-only Merkle log. Clients pull a signed tree head, verify it matches every other client's view, and refuse to accept any key update that didn't make it into the log.
- → Append-only Merkle tree · RFC 6962 consistency proofs
- → Signed Tree Head verified Ed25519 · fail-closed on mismatch
- → Each user can audit their own key history at any time
- → Gossip protocol between mirrors detects log forks
mk' = HKDF(encKey ∥ mk, "group-dr-mix", groupId ∥ memberId) // mk is mixed, never replaced — ensures per-member entropy // AAD context-bound: senderId ∥ groupId ∥ messageId ∥ epoch
One member out,
new keys for all.
A group is only as secure as its membership transitions. Each sender maintains their own forward-secret chain; the group root key rotates on every join or leave; new keys are distributed via sealed-box messages directly to remaining members. A removed member retains nothing.
- → Per-sender forward-secret chain — not a shared static key
- → Skip-key TTL 1 hour · auto-drop on expiry
- → Anti-replay LRU · 2 048 entries × 24 h per message ID
- → Derivation bound 64 — anti-DoS hardcap
Sign the nonce.
No password ever leaves.
Authentication is a signature on a server-issued challenge, not a password posted to an endpoint. The user identity is bound to a long-term Ed25519 key. The server never learns anything reusable.
POST /auth/challenge
{ userId }nonce = random(32)
return { nonce, ttl: 60s }sig = ed25519.sign( nonce + "|" + userId, identitySK )
verify(sig, pinnedPK) → ok issue jwt(HS256)
wsSeq monotonic guarded types replay = drop
Server never receives a password or reusable secret. Identity proof is a fresh signature each time.
First-time-seen key bind is atomic at the DB level (INSERT OR IGNORE + re-read). Race-safe under concurrency.
JWT verifier explicitly whitelists HS256. No alg-none, no algorithm confusion.
Every state-changing WS message requires a valid monotonic sequence. Replay or skew drops at the transport layer.
Read the spec.Or wait for the audit.
Everything described on this page is in the live build today. The full cryptographic specification — every algorithm, every parameter, every assumption — lives one click away in the Docs.