Security architecture · 12 layers · 20+ runtime probes · last review 2026-05-27

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.

12
Crypto layers
20+
Runtime probes
64mb
Argon2id memory
~1µs
Key zeroing
defense status
live
handshake
ok · dual-signed
ratchet
advancing
envelope
pin-bound
securestring
masked
integrity
18 / 18 probes
replay window
30 s · 0 hits
risk score · ema
0.08 / 1.00block 0.70 · panic 0.90
Pre-audit build · 24 CRITICAL + 38 HIGH closed internally · external audit pending Q3 2026
01twelve layers

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.

01
PIN-bound envelope
Argon2id KDF · 64 MiB · t = 3 · secretbox-wrapped
shipped
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.

02
Realm database encryption
AES-256 · 512-bit derived key · WAL/SHM protected
shipped
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.

03
X25519 key agreement
RFC 7748 ECDH · Curve25519 · low-order rejection
shipped
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.

04
ML-KEM-768 · post-quantum KEM
FIPS 203 · lattice · NIST Category III
shipped
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.

05
Ed25519 identity signature
RFC 8032 EdDSA · 4-layer pinning
shipped
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.

06
ML-DSA-65 · post-quantum signature
FIPS 204 · lattice · dual-signed transcripts
shipped
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.

07
Double Ratchet · 1:1 forward secrecy
Full Signal 3-step · DH + chain + per-message key
shipped
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.

08
Group sender keys
Per-sender chain · mk mixed via HKDF
shipped
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.

09
ChaCha20-Poly1305 AEAD
Streaming chunked 32 KB · TAG_FINAL mandatory
shipped
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.

10
SecureString in memory
XOR-masked · per-block HKDF · zero in < 1 µs
shipped
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.

11
Signed WebRTC signaling
Ed25519 per ICE candidate · 30 s replay window
shipped
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.

12
Forward-secure delete
Message key + prekey + group key zeroed · ordered wipe
shipped
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.

02anti-forensic seizure

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
unwrap flow · user-side
PIN
in user memory
Argon2id KDF
64 MiB · t = 3
device key
unwraps Realm
what UFED sees without the PIN
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 plaintext
securestring · per-message lifecycle
01 · allocate
XOR
masked at rest
02 · reveal
"meet 21:00"
only at render
03 · use
fn(s)
closure scope
04 · zero
0x00…
memzero · 1 µs
try {
  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
03anti-memory-dump

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
04ordered wipe

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.

wipe sequence
  1. 01 · memory
    Zero every SecureString · destroy mask key
    All decrypted bodies, message keys, and the unlocked PIN are zeroed before anything else touches disk.
  2. 02 · enumerate
    First pass over secure storage
    With the database still active, enumerate prekeys and group symmetric keys for explicit removal.
  3. 03 · close
    Close the database cleanly
    Critical — without this, write-back caches reintroduce plaintext after the physical wipe in step 05.
  4. 04 · crypto-erase
    Reset hardware-backed keystore
    Device key, Realm key, PIN hash, JWT — all destroyed in the hardware keystore.
  5. 05 · overwrite
    3-pass random + 1-pass zero on storage files
    Database file, WAL, SHM, lock files, media vault, cache directory — overwritten then deleted.
  6. 06 · sweep
    Second pass over secure storage
    Idempotent sweep — anything left behind by step 02 is removed.
  7. 07 · UI
    Show a generic error
    The user under coercion must not see evidence of the wipe. UI shows a routine "incorrect PIN" message.
bypasses
USB · custody · dev-safe
total time
~4-6 s
recoverable
nothing
05runtime device integrity

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.

Root detection
Android · Magisk / SuperSU / RootCloak
Jailbreak detection
iOS · Cydia / Substrate / jb paths
Frida port scan
Both · 27042 · 27043 · 4444 · 1337
Frida named pipes
Both · /tmp/frida- · /data/local/tmp
Frida proc/maps signature
Android · /proc/self/maps
Cycript / Substrate dylibs
iOS · injected dylibs
ADB enabled
Android · developer flag
ptrace attached
Both · /proc/self/status TracerPid
GrayKey paths
iOS · /Library/* signature
AFC2 service
iOS · jailbreak indicator
UFED artifacts
Both · known forensic paths
Magnet AXIOM artifacts
Both · forensic suite
Debugger port scan
Both · 5037 · 9229
Crypto library tamper
Both · sodium + nacl hash check
Hook detection
Both · function source fingerprint
Sandbox breakout test
Both · random + secure-delete probe
Timing anomaly
Both · debugger heuristic
Custody mode
Both · USB + ADB combined
DYLD insert detection
iOS · DYLD_INSERT_LIBRARIES
Magisk Hide / Zygisk
Android · denylist evasion
shipped scaffolded · native bridge pending
06local database

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
three layers of storage protection
outer · PIN envelope
argon2id
User PIN derives the KEK that unwraps the database key. Without the PIN, the wrapped key is indistinguishable from random.
middle · realm key
512-bit aes
Decrypts the Realm database file at runtime. Held only in memory as a zerizable buffer; zeroed on background.
inner · message rows
per-message mk
Each message body is independently AEAD-encrypted with a Double-Ratchet-derived per-message key.
size when locked
opaque blob
attack required
3 simultaneous breaks
what the relay sees · per packet
recipient hasha3f1.9e0c.4b21.7d88
ciphertextpadded to 4 KiB
sender— sealed —
timestamp±5 min jitter
sequencemonotonic · anti-replay
tlstls 1.3 · hostname pinned
call signaling
ed25519 signed30 s replay window
07network & transport

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.
08identity & key transparency

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
merkle log · verification flow
STH · ed25519 signedleaf 0your keyleaf 2leaf N
algorithm
SHA-256 · RFC 6962
leaf framing
v3 byte-tagged
STH refresh
every 10 min
pendingProduction log key is currently TOFU — pinned hardcode lands with the public release. Gossip across mirrors deploys alongside.
epoch advancement on membership change
epoch 11
3 members
root key K₁₁
member joins
+1
trigger epoch++
epoch 12
4 members
root key K₁₂
distribute
sealed-box per member
no broadcast
per-message derivation
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
09group messaging

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
10authentication & session

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.

01 · client
Request challenge
POST /auth/challenge
{ userId }
02 · server
Issue 32 B nonce
nonce = random(32)
return { nonce, ttl: 60s }
03 · client
Sign challenge
sig = ed25519.sign(
  nonce + "|" + userId,
  identitySK
)
04 · server
Verify & issue JWT
verify(sig, pinnedPK)
  → ok
issue jwt(HS256)
05 · session
Sequence-bound WS
wsSeq monotonic
guarded types
replay = drop
no password

Server never receives a password or reusable secret. Identity proof is a fresh signature each time.

atomic TOFU

First-time-seen key bind is atomic at the DB level (INSERT OR IGNORE + re-read). Race-safe under concurrency.

algorithm whitelist

JWT verifier explicitly whitelists HS256. No alg-none, no algorithm confusion.

26 guarded types

Every state-changing WS message requires a valid monotonic sequence. Replay or skew drops at the transport layer.

honest pre-audit disclosure

Three defenses are still scaffolded.

Internal review across nine sprints closed 24 critical and 38 high-severity findings. External audit is scoped for Q3 2026 and the full report will be published the day it lands. Until then, the items below ship as planned but unverified — we mark them honestly so you can decide what risk you accept.

scaffolded
Certificate pinning — SPKI
Hostname-only allowlist active today; SPKI bridge ships in the next native build.
scaffolded
Tor / SOCKS5 transport
Detection only at the moment; native routing follows the same build.
pending
Key Transparency public log key
TOFU until the production log key is hardcoded post-deploy.
pending
Independent external audit
Scoped Q3 2026 · report public on close.
11what's next

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.