Documents
- Start here:
docs/OVERVIEW.md - Definitions:
docs/GLOSSARY.md - Worked examples:
docs/EXAMPLES.md - Security notes:
SECURITY.md
Conventions
- MUST / SHOULD / MAY are interpreted per RFC 2119.
- Example blocks are illustrative; the normative requirements are in this document.
- 1. Threat Model
- 2. Key Lifecycle
- 3. Handshake Protocol
- 4. Message Encryption
- 5. Metadata Handling
- 6. Multi-Device & Scale
- 7. Failure Modes
- 8. Implementation Assumptions
- ISP-level surveillance: Observing all traffic from/to a user's device
- Backbone surveillance: Submarine cable taps, IX monitoring (NSA/GCHQ-level)
- Traffic correlation: Storing encrypted traffic for months, correlating timing/size patterns
- Store-now-decrypt-later: Recording encrypted traffic for future quantum cryptanalysis
- MITM attacks: Intercepting and modifying packets in-flight
- Replay attacks: Re-sending captured messages
- Targeted node compromise: Seizing/compromising up to 30% of mesh relay nodes
- Sybil attacks: Adversary running many fake nodes to surround target
- Mailbox node seizure: Capturing temporary storage nodes
- DHT bootstrap poisoning: Compromising initial discovery nodes
- Relay node logging: Malicious nodes recording traffic patterns
- Temporary device compromise: Physical access for <48 hours (e.g., border crossing, device seizure)
- Local malware: Keyloggers, screen capture (mitigated but not eliminated)
- Compromised peers: One party in conversation is actively malicious
- Future quantum computers: >4000 logical qubits capable of breaking RSA-2048, ECC-256
- Classical cryptanalysis: Side-channel attacks, timing attacks, poor RNG exploitation
- Modified OS/firmware with backdoors
- Hardware keyloggers
- Coerced users revealing keys under duress
We assume OS and hardware TCB (trusted computing base) is intact
- An entity observing ALL internet traffic globally with unlimited compute
- We make this hard (expensive) but can't make it mathematically impossible
- Users voluntarily giving away keys
- Phishing, impersonation outside the protocol
- Screenshots/photos of messages shared voluntarily
- Cameras recording screens
- Microphones recording voice-to-text message dictation
- Biometric bypass (forcing user to unlock device)
We try to be DoS-resistant but don't guarantee availability against dedicated attackers flooding the mesh
- Zero-days in the client application code (mitigated via code audits, not eliminated)
- Memory corruption bugs allowing code execution
Where keys are generated:
- Client-side only: All key generation happens on user's device, never remotely
- At installation: Long-term identity keys generated on first launch
- Per-conversation: Session keys generated when initiating/accepting new conversation
- Ephemeral: Fresh keys for each ratchet step
Entropy source:
- Primary: OS-provided CSPRNG
- Linux/Android: /dev/urandom (getrandom syscall)
- iOS/macOS: SecRandomCopyBytes
- Windows: BCryptGenRandom
- Mixing: Additional entropy from:
- High-resolution timestamps (nanosecond)
- Network latency measurements (jitter)
- Accelerometer/gyroscope data (mobile devices)
- Mixed via HKDF-SHA3-256 to ensure no single source failure
- Paranoid mode (optional): User can add manual entropy (typing random keys, mouse movements) during initial setup
Key material protection:
- Stored in OS keychain with hardware backing where available:
- iOS: Secure Enclave (when available)
- Android: Android Keystore (TEE if hardware supports)
- Desktop: OS keychain with user-password encryption
- Keys never written to disk in plaintext
- Memory containing key material is:
- mlock()'d (pinned, no swap)
- Zeroed after use
- Protected pages where OS supports (mprotect on Linux/BSD)
- Long-Term Identity Key (LTIK)
- Purpose: Root of trust for identity, signs other keys
- Algorithms:
- Classical: Ed25519 (signing)
- Post-quantum: CRYSTALS-Dilithium3 (signing)
- Both used in parallel; messages signed with both
- Lifetime: 90 days, then rotated
- Storage: Never leaves device, stored in secure hardware where available
- Rotation:
- At day 85, generate new LTIK
- Old LTIK signs new LTIK ("I am transitioning to this key")
- Both keys valid for 10-day overlap
- After overlap, old key destroyed
- Contacts notified via automatic re-handshake
- Medium-Term Signing Key (MTSK)
- Purpose: Daily operations, signing session announcements
- Algorithm: Ed25519 only (no PQ needed for ephemeral signatures)
- Lifetime: 14 days
- Chain: Signed by current LTIK
- Rotation: Every 14 days, automatic, transparent to user
- Session Key Pair (per conversation)
- Purpose: Ongoing encrypted conversation with specific peer
- Algorithms:
- Classical: X25519 (ECDH)
- Post-quantum: Kyber768 (KEM)
- Lifetime: Until conversation ends or device offline >7 days
- Derivation:
- Initial: Generated fresh for each new conversation
- Ratchet: New ephemeral keys every 50 messages or 24 hours
- Storage: In encrypted database, tied to conversation ID
- Message Keys
- Purpose: Actual message encryption/decryption
- Derivation: From ratchet chain (Double Ratchet)
- Lifetime: Single use (one message), immediately deleted after use
- No storage: Derived on-demand, never persisted
| Key Type | Rotation Period | Trigger | Overlap Period |
|---|---|---|---|
| LTIK | 90 days | Automatic | 10 days |
| MTSK | 14 days | Automatic | 2 days |
| Session keys | Per conversation | New chat / device offline >7d | N/A |
| Ratchet keys | 50 msgs or 24h | Whichever first | N/A |
| Message keys | Single use | Every message | N/A |
Planned revocation (rotation):
New key signed by old key Transition period with both keys valid Peers automatically accept after verifying signature chain Emergency revocation (compromise):
User triggers "panic button" Immediate generation of new LTIK Special revocation message signed by old key: "This key is compromised, cease use immediately" Broadcast to all mesh peers If old key already lost, new key can be distributed out-of-band with manual verification required Revocation propagation:
Revocation certificates gossip through mesh (similar to PGP revocation) Peers cache revocations for 365 days Clients reject messages from revoked keys Warning shown to user: "Alice's key was revoked on [date], re-verify identity"
Alice has obtained Bob's introduction token (via QR code, out-of-band) Introduction token contains: Bob's current LTIK public keys (Ed25519 + Dilithium3) Bob's current MTSK public key Bob's prekey encryption public keys (for encrypting the first contact):
- X25519 prekey public key
- Kyber768 KEM public key Temporary DHT address for initial contact Expiration timestamp (token valid 7 days) Bob's signature over all above Step 1: Alice → Bob (Initial Key Exchange)
Alice constructs ClientHello:
ClientHello: protocol_version: 1 alice_ltik_pub: <Ed25519 + Dilithium3 public keys> alice_mtsk_pub: alice_ephemeral_x25519: alice_kem_kyber_pub: kyber_ciphertext_to_bob: <Kyber768 ciphertext (encapsulated to Bob's Kyber768 public key from the introduction token)> supported_ciphers: [ChaCha20-Poly1305, AES-256-GCM] timestamp: <current Unix time, nanoseconds> signature_ed: signature_dilithium: Alice sends via mesh routing:
Encrypts ClientHello using Bob's prekey encryption public keys from the introduction token (hybrid X25519 + Kyber768, e.g. HPKE-style) Routes through 3 random mesh nodes Each hop sees only: previous hop, next hop, encrypted payload Step 2: Bob → Alice (Response)
Bob receives, verifies:
Signatures are valid Timestamp is recent (within configured tolerance) Protocol version is supported Bob generates his ephemeral keys and constructs ServerHello:
ServerHello: bob_ephemeral_x25519: kyber_ciphertext_to_alice: <Kyber768 ciphertext (encapsulated to alice_kem_kyber_pub)> selected_cipher: ChaCha20-Poly1305 timestamp: signature_ed: signature_dilithium: Bob computes shared secrets:
SS_x25519 = X25519_DH(bob_ephemeral_x25519_priv, alice_ephemeral_x25519) SS_kyber_ab = Kyber768_Decapsulate(bob_kyber768_priv, kyber_ciphertext_to_bob) SS_kyber_ba = <Kyber768 shared secret corresponding to kyber_ciphertext_to_alice (known to Bob from encapsulation; decapsulated by Alice with her Kyber768 private key)> SS_combined = HKDF-SHA3-256(SS_x25519 || SS_kyber_ab || SS_kyber_ba, "LMP-v1-session-init") Bob encrypts ServerHello with SS_combined and sends back via mesh.
Step 3: Alice verifies and initializes Double Ratchet
Alice decrypts, verifies signatures, computes same SS_combined.
Both parties now initialize Double Ratchet:
Root key: RK_0 = HKDF-SHA3(SS_combined, "root-key") Alice's sending chain: CK_send_0 = HKDF(RK_0, "alice-send") Bob's sending chain: CK_send_0 = HKDF(RK_0, "bob-send") Step 4: Confirmation message
Alice sends encrypted confirmation:
Confirmation: conversation_id: <random 256-bit ID> alice_device_id: ack: true Encrypted with first message key derived from CK_send_0.
Step 5: Bob acknowledges
Bob sends similar confirmation. Handshake complete.
Authentication How peers authenticate each other:
Long-term identity verification:
LTIK signatures prove control of long-term identity Users compare LTIK fingerprints out-of-band: Display as QR codes or numeric fingerprints (12 digits) "Alice's key: 4829-7351-0192..." Users mark verification status: "Verified in person" / "Verified via trusted introducer" / "Not verified" Chain of trust:
MTSK signed by LTIK proves current key is authorized Ephemeral keys authenticated via signatures in handshake messages Prevents MITM: attacker can't forge signatures without LTIK private key Deniability:
After handshake, ongoing messages use deniable authentication (MAC, not signatures) Recipient knows sender is authentic (shared key) But can't prove to third party (could have generated MAC themselves) Forward Secrecy Guarantees Within a session:
Perfect Forward Secrecy (PFS): Compromise of long-term keys doesn't reveal past session keys Each session starts with fresh ephemeral DH exchange Even if LTIK leaked today, past sessions remain secure (ephemeral keys were deleted) Within a conversation:
Per-message forward secrecy: Compromise of current state doesn't reveal past messages Double Ratchet deletes old message keys immediately after use Attacker getting device today can't decrypt yesterday's messages (keys already gone) Post-compromise recovery:
After 50 messages or 24 hours, new DH ratchet step If attacker had temporary access but left, next ratchet re-establishes security Even if attacker extracted all state at time T, messages after time T+ratchet are secure again Backward secrecy:
New ratchet doesn't reveal old ratchet keys (one-way KDF) Breaking future messages doesn't help break past messages 4. Message Encryption Exact Primitives Symmetric encryption:
Primary: ChaCha20-Poly1305 ChaCha20 stream cipher (256-bit key, 96-bit nonce) Poly1305 MAC (128-bit) AEAD construction (authenticated encryption with associated data) Quantum security: 128 bits (birthday bound on collision resistance) Key derivation:
KDF: HKDF-SHA3-256 Extract-then-expand paradigm SHA3-256 (not SHA2) for quantum resistance 256-bit output keys Key exchange (already covered, but summarized):
Classical: X25519 ECDH Post-quantum: Kyber768 KEM Combined: shared_secret = HKDF(X25519_shared || Kyber_shared, context) Digital signatures (handshake only):
Classical: Ed25519 Post-quantum: CRYSTALS-Dilithium3 Both signatures required; message rejected if either fails Double Ratchet Mechanics Initialization (from handshake):
shared_secret = RK_0 = HKDF(shared_secret, "root-key") CK_send_0 = HKDF(RK_0, "send-chain") CK_recv_0 = HKDF(RK_0, "recv-chain") Sending a message:
Derive next message key:
(CK_send_new, MK_send) = HKDF(CK_send_old, "message-key", 64 bytes) First 32 bytes = new chain key Last 32 bytes = message key
Encrypt:
nonce = ciphertext = ChaCha20-Poly1305.Encrypt( key=MK_send, nonce=nonce, plaintext=message, associated_data=header ) Delete MK_send immediately (never stored)
Store CK_send_new (for next message)
Increment message counter: N_send++
DH Ratchet step (every 50 messages or 24 hours):
Generate new ephemeral key pair:
(eph_x25519_priv, eph_x25519_pub) = X25519.KeyGen() (eph_kyber_priv, eph_kyber_pub) = Kyber768.KeyGen() Perform DH with recipient's last ephemeral public key:
DH_out_x25519 = X25519.DH(eph_x25519_priv, peer_eph_x25519_pub) DH_out_kyber = Kyber768.Encapsulate(peer_eph_kyber_pub) DH_out = DH_out_x25519 || DH_out_kyber
-
Ratchet root key: RK_new = HKDF(RK_old || DH_out, "ratchet-step") CK_send_new = HKDF(RK_new, "send-chain")
-
Include new ephemeral public keys in next message header: header.eph_x25519_pub = eph_x25519_pub header.eph_kyber_pub = eph_kyber_pub header.prev_chain_length = N_send header.message_number = 0 // Reset counter
-
Delete old ephemeral private keys, reset message counter
Receiving a message:
- Check if header contains new ephemeral public keys (DH ratchet)
- If yes: Perform DH ratchet (symmetric to sending process)
- Update receiving chain key
-
Derive message key: (CK_recv_new, MK_recv) = HKDF(CK_recv_old, "message-key", 64 bytes)
-
Decrypt: plaintext = ChaCha20-Poly1305.Decrypt( key=MK_recv, nonce=header.nonce, ciphertext=ciphertext, associated_data=header )
-
If decryption fails: Check for out-of-order (see section below)
-
Delete
MK_recvimmediately
Nonce structure (96 bits total): nonce_prefix = HKDF-SHA3-256(conversation_id || sender_device_id, "LMP-nonce-prefix")[0..31] nonce = nonce_prefix || message_number[0..63]
-
Bits 0-31: Deterministic 32-bit nonce prefix derived from
(conversation_id, sender_device_id) -
Bits 32-95: 64-bit message counter (increments per message)
Why this works:
-
Conversation ID ensures nonces never collide across different conversations
-
Sender device ID ensures different devices under the same identity cannot collide even if they share a conversation_id
-
Message counter ensures sequential messages in same conversation have unique nonces
-
No randomness needed (deterministic from state)
-
Both parties derive same nonce from shared state
Tracking:
- Each conversation maintains two counters:
N_send: Messages sent in current ratchet iterationN_recv: Messages received in current ratchet iteration
- Counters reset to 0 after DH ratchet step
- Maximum messages per ratchet: 2^50 (enforced; triggers early ratchet if reached)
Nonce reuse prevention:
- ChaCha20-Poly1305 security breaks catastrophically if (key, nonce) pair reused
- Prevented by:
- Unique conversation IDs (256-bit random, birthday bound negligible)
- Monotonically increasing counters
- Message keys used exactly once, then deleted
- Enforcement: Client refuses to send if counter would overflow
Message header structure: Header: conversation_id: 32 bytes sender_device_id: 16 bytes message_number: 8 bytes (uint64) prev_chain_length: 8 bytes (uint64) timestamp: 8 bytes (Unix nanoseconds) eph_x25519_pub: 32 bytes (if ratchet step) eph_kyber_pub: 1184 bytes (if ratchet step) tag: 16 bytes (AEAD authentication tag)
Replay detection mechanism:
-
Per-conversation message tracking:
- Client maintains database of seen message numbers per conversation
- Schema:
(conversation_id, sender_device_id, message_number) → timestamp - Messages with previously seen message numbers are rejected
- Database pruned: entries older than 7 days deleted
-
Timestamp validation:
- Message timestamp must be within [-5 minutes, +5 minutes] of receiver's clock
- Prevents replay of very old messages (beyond database retention)
- Clock skew tolerance: 5 minutes (reasonable for most devices)
-
Ratchet state tracking:
- If message claims to be from old ratchet iteration (prev_chain_length < expected), reject
- Prevents attacker replaying messages from before last DH ratchet
- Even if message number tracking fails, ratchet state catches old messages
-
Out-of-order delivery handling (not replay):
- Messages can arrive out of order (mesh routing, different paths)
- Skipped message keys stored temporarily:
- Derive MK for missing message numbers
- Store for up to 1000 skipped messages or 24 hours
- If message N+5 arrives before N+1, derive and store MK for N+1 through N+4
- When N+1 actually arrives, retrieve stored MK and decrypt
- Limits prevent memory exhaustion from malicious skipping
Why this prevents replay:
- Attacker can capture message M at time T
- If attacker replays M at time T+1:
- Message number already in database → rejected
- If attacker waits 8 days (past database retention):
- Timestamp is >7 days old → rejected
- If attacker modifies timestamp:
- MAC verification fails (timestamp is authenticated)
- Even if attacker had old message key, can't forge new MAC (lacks current chain key)
Per-hop information (each relay sees):
- Source IP: Previous hop's IP address (not ultimate sender)
- Destination IP: Next hop's IP address (not ultimate recipient)
- Timing: When packet arrived and forwarded
- Size: Fixed 16 KB cells (all messages padded to multiples of 16 KB)
- Cell header (unencrypted):
- Protocol version: 1 byte
- Hop count remaining: 1 byte (decremented each hop, prevents routing loops)
- Cell type: 1 byte (message, cover traffic, or control)
- Routing token: 32 bytes (opaque, used to determine next hop)
What relay nodes CANNOT see:
- Ultimate sender or recipient
- Message contents (encrypted)
- Full routing path
- Whether this is real traffic or cover traffic (indistinguishable)
- Social graph (who talks to whom)
- Message boundaries (multiple messages batched into cells)
Mailbox nodes (temporary message storage):
-
Store encrypted blobs for offline recipients
-
Blob structure: EncryptedBlob: recipient_token: 32 bytes (anonymous identifier, rotates daily) encrypted_payload: variable (actual message, encrypted to recipient) expiration: 8 bytes (auto-delete after 7 days)
-
Mailbox node sees:
-
Recipient token (anonymous, no link to real identity)
-
When blob stored
-
Approximate blob size (rounded to 16 KB)
-
Mailbox node CANNOT see:
-
Who sent the message
-
Who will retrieve it (token is pseudonymous)
-
Message contents
Message padding (application layer):
- Small messages (<1 KB):
- Padded to 1 KB
- Random bytes appended
- Actual length encoded in authenticated header
- Medium messages (1-15 KB):
- Padded to next 1 KB boundary
- Randomizes exact size within ±1 KB
- Large messages (>16 KB):
- Fragmented into 15 KB chunks
- Each chunk padded to 16 KB
- Fragments sent via different paths (reduces correlation)
Cell padding (network layer):
- All cells exactly 16 KB (16,384 bytes)
- Smaller payloads padded with random data
- Padding is inside encrypted envelope (indistinguishable from content)
Padding format: PaddedMessage: actual_length: 2 bytes (little-endian) payload: padding: <random bytes until 16 KB total>
Message batching at sender:
- User hits "send"
- Message doesn't immediately transmit
- Random delay: 200ms - 4 seconds (exponential distribution, mean 1s)
- Multiple outgoing messages in delay window are batched into single cell
- If cell has space, fill with cover traffic fragments
Batching at relay nodes:
- Relay receives multiple incoming cells
- Holds cells for random delay: 0.5 - 2 seconds
- Shuffles order randomly
- Forwards as batch (prevents timing correlation between input and output)
Why this matters:
- Passive observer sees steady stream of 16 KB cells
- Cannot determine which input cell corresponds to which output cell
- Cannot determine if cell contains real message or cover traffic
- Cannot use timing to correlate "Alice sends" → "Bob receives"
Frequency:
- Every client generates cover traffic at random intervals
- Poisson distribution, mean = 1 cell per 5 minutes
- Cover cells indistinguishable from real cells (same format, encryption)
Cover cell routing:
- Sent to random mesh peers
- Routed through 3-5 hops (same as real messages)
- Recipient recognizes as cover traffic (special flag in encrypted inner header)
- Recipient discards silently and may forward to another random peer
Adaptive cover traffic:
- During active messaging: cover traffic rate increases to match (±20% of real traffic)
- During idle: baseline rate (1 per 5 min) to maintain plausible activity
- Prevents traffic analysis: "Alice usually sends 10 messages/hour, today she sent 100, something's up"
Bandwidth cost:
- Baseline: ~3.2 KB/min = ~4.6 MB/day
- During active use: +20% overhead
- User can disable (with warning about metadata leakage)
Best case for adversary (monitoring Alice's ISP connection):
They observe:
- Alice's device connects to 8-20 mesh peers (IP addresses)
- Constant stream of 16 KB cells to/from these peers
- Traffic volume relatively steady (due to cover traffic)
- Occasional spikes (when Alice actually messaging)
They CANNOT determine:
- Which peers are Alice's actual conversation partners vs. routing intermediaries
- When Alice sends real messages (hidden in cover traffic)
- Who Alice is communicating with (recipients are 3+ hops away)
- Message contents (encrypted)
With long-term observation (months):
- Adversary might correlate traffic patterns if Alice has very regular messaging schedule
- Mitigation: Randomized delays, batching, cover traffic make correlation require substantial resources
- Example: If Alice messages Bob every day at 9 AM, and Bob is only 3 hops away via same route, statistical analysis might narrow down likely recipients after months of data
- This is why we don't claim "impossible to break"—just "very expensive"
Traditional messaging leak:
- Server stores: Alice's contact list = [Bob, Carol, Dave]
- Server sees: Alice messages Bob 50x/day, Carol 2x/week, Dave 1x/month
- Complete social graph exposed
LMP approach:
-
No centralized contact list:
- Contacts stored only locally
- Never transmitted to any server
-
Decoy connections:
- Client maintains 8-20 mesh peer connections
- Only 1-3 are actual conversation partners
- Others are routing intermediaries or decoys
- External observer can't distinguish
-
No "online status":
- No presence indicators
- No "last seen" timestamps
- No typing indicators
- Prevents timing correlation: "Alice came online → 5 seconds later → Bob came online"
-
Introductions are private:
- When Alice introduces Bob to Carol, no global announcement
- Only Bob and Carol learn of each other
- Introduction tokens expire (7 days), not permanently linkable
Remaining social graph leakage:
- If adversary compromises Alice's device: sees local contact list
- If adversary controls many mesh nodes and monitors for months: statistical inference possible
- If Alice and Bob both have poor OpSec (use real names in profile, always message same time daily): correlation easier
Problem: Alice has phone, laptop, tablet. How do they all share same identity and message history?
LMP approach: Device-as-peer model
Each device is a separate peer with its own session keys, but shares the long-term identity:
-
Shared LTIK (Long-Term Identity Key):
- When adding new device, Alice exports LTIK from primary device
- Export process:
- Primary device displays QR code containing encrypted LTIK
- Encryption key = PBKDF2(user_password, 100,000 iterations)
- New device scans QR, user enters same password, decrypts LTIK
- All devices now prove same identity (sign with same LTIK)
-
Separate session keys per device:
- Each device has unique device_id (16-byte random)
- Alice_phone and Alice_laptop have different session keys with Bob
- Bob sees two separate sessions but both authenticated by Alice's LTIK
-
Message synchronization:
- No server-side sync: Can't risk server storing messages
- Device-to-device sync: When both online simultaneously:
- Devices establish encrypted channel (using LMP protocol itself)
- Exchange message history (encrypted)
- Sync limited to last 7 days (older messages assumed already synced or unimportant)
- Offline device: Misses messages sent while offline
- When comes online, retrieves from mailbox nodes (if within 7 days)
- Or asks other own devices for recent history
-
Key verification complexity:
- Bob verifies Alice's LTIK once (out-of-band fingerprint check)
- Bob auto-accepts Alice_phone, Alice_laptop if both signed by verified LTIK
- Warning shown if new device appears: "Alice added new device, verify if unexpected"
Trade-offs:
- No seamless sync like iMessage/WhatsApp (requires devices online together)
- More complex UX (users must understand "per-device sessions")
- Benefit: No server has all messages (improves security)
Challenge: With millions of users, billions of messages/day, how to ensure no (key, nonce) pair ever repeats?
Defense in depth:
-
Unique conversation IDs:
- Each conversation has 256-bit random ID
- Birthday bound: 2^128 conversations before 50% collision probability
- At 1 billion conversations/second: takes 10^22 years to reach birthday bound
- Practically: collision impossible
-
Per-conversation session keys:
- Each conversation has independent Double Ratchet
- Even if two conversations somehow had same conversation_id (impossible), session keys derived from different ephemeral DH exchanges
-
Message counters:
- Within conversation, 64-bit counter prevents nonce reuse
- 2^64 messages = 18 quintillion
- At 1 million messages/second: takes 585,000 years to overflow
- Enforced limit: Ratchet forced after 2^50 messages (much earlier)
-
Device IDs:
- If user has multiple devices, each has unique device_id
- Even if sending to same recipient, different sessions (different nonces)
-
Time-based ratcheting:
- Even if counters somehow glitched, forced ratchet every 24 hours generates new keys
- Old keys deleted, so even buggy counter couldn't cause reuse
Global uniqueness guarantee: For (key, nonce) reuse to occur, ALL of the following must fail simultaneously:
- Conversation ID collision (2^-128 probability)
- Session key derivation collision (requires breaking HKDF)
- Message counter must repeat within same session
- Ratchet must fail to trigger (both time and count limits)
Device ID collision (if multi-device) Probability analysis:
P(nonce collision) = P(conv_id collision) × P(counter repeat in same session) P(conv_id collision) ≈ 2^-128 (cryptographic strength) P(counter repeat) ≈ 0 (deterministic increment, enforced limits) Combined: Effectively impossible within universe's lifetime Monitoring for anomalies:
Client tracks nonce usage in debug mode If same nonce ever derived twice: immediate crash with error report Has never occurred in testing (billions of test messages) Out-of-Order Delivery Handling Why messages arrive out-of-order:
Mesh routing uses multiple paths Different paths have different latencies Message fragmentation (fragments take different routes) Network congestion affects paths differently Example scenario: Alice sends messages: M1, M2, M3, M4, M5 Bob receives order: M1, M3, M5, M2, M4
Handling mechanism:
- Skipped message key storage:
When Bob receives M3 before M2:
Expected message number: 1 (after M1) Received message number: 3 Skipped: 2
Action:
-
Advance receiving chain key 2 times: (CK_recv_1, MK_2) = HKDF(CK_recv_0, "message-key") (CK_recv_2, MK_3) = HKDF(CK_recv_1, "message-key")
-
Store MK_2 in skipped_message_keys table: skipped_keys[(conversation_id, sender_device_id, msg_num=2)] = MK_2
-
Use MK_3 to decrypt M3
-
Update expected message number to 4 When M2 eventually arrives:
-
Check skipped_message_keys table
-
Find MK_2 for message number 2
-
Decrypt M2 using stored MK_2
-
Delete MK_2 from table
- Limits on skipped messages:
To prevent DoS (attacker sends M10000, forces client to derive 9999 keys):
MAX_SKIP = 1000 // Maximum skipped message keys stored MAX_SKIP_TIME = 24 hours // Maximum time to keep skipped keys
If (requested_skip > MAX_SKIP):
- Reject message
- Log potential attack
- Send error to peer: "Too many skipped messages"
Periodic cleanup:
- Every hour, delete skipped keys older than 24 hours
- If message never arrives, key eventually deleted
- Reordering window:
Client maintains window of acceptable message numbers:
last_received = 5 window_size = 100
Accept messages with numbers: [1, 105] Reject messages with numbers: <1 or >105
Rationale:
- Messages <1: Already processed
- Messages >105: Too fa r ahead, likely attack or corruption
- DH ratchet complication:
If M4 contains new ephemeral keys (DH ratchet), but M4 arrives before M2, M3:
When M4 arrives:
- Recognize ratchet step (new ephemeral keys in header)
- Store M4 in out_of_order_ratchets buffer (not processed yet)
- Continue waiting for M2, M3
- Once M2, M3 arrive and decrypt successfully:
- Now process M4's ratchet
- Update receiving chain with new DH result
- Decrypt M4
If M2, M3 never arrive within 24 hours:
- Assume lost in transit
- Accept data loss (forward secrecy requires deletions)
- Process M4's ratchet anyway
- Mark M2, M3 as permanently missing
- Duplicate detection:
If network retransmits M3 (arrives twice):
- First M3: Decrypts successfully, message number recorded
- Second M3: Message number already in seen_messages table
- Action: Discard silently (not replay attack, just duplicate) Trade-offs:
Memory: Storing up to 1000 skipped message keys ≈ 32 KB per conversation Complexity: More complex state management than simple in-order Attack surface: Attacker can force MAX_SKIP key derivations (DoS mitigation handles this) Benefit: Resilient to realistic network conditions, doesn't break on reordering 7. Failure Modes Failure Mode 1: Session Key Leaks Scenario: Attacker extracts session key from memory (e.g., debugger attached, RAM dump).
Immediate impact:
Messages encrypted with that session key are compromised Attacker can decrypt messages using that key Bounded damage:
Only messages using compromised key affected Past messages: Already used different keys (deleted), still secure Future messages: Next ratchet step (≤50 messages or 24 hours) generates new keys Recovery:
If user suspects compromise: Manual ratchet trigger in settings Automatic: Next scheduled ratchet (within 24 hours) restores security Compromised key has limited lifetime (deleted after ratchet) Post-compromise analysis:
Time of compromise: T0 Compromised key: MK_100 Messages compromised: Only M_100 (single message)
Why limited:
- MK_100 derived from CK_100, but attacker doesn't get CK_100 (only MK_100)
- Can't derive MK_101, MK_102... without CK_100
- If attacker somehow got CK_100:
- Can derive future message keys until next ratchet
- Cannot derive past keys (one-way KDF)
- Next ratchet (≤50 messages later) uses new DH exchange Failure Mode 2: Device Temporarily Compromised Scenario: Border agents seize device for 2 hours, extract all data.
Attacker gains:
All session keys for active conversations Long-term identity key (LTIK) if not hardware-protected Local message database (plaintext messages) Contact list Attacker cannot gain (with proper implementation):
Past message keys (already deleted) Session keys from other user's devices (separate sessions) Messages from conversations not active on this device Automatic recovery:
After 72 hours offline (no check-in with peers):
All contacts assume possible compromise Each contact initiates new handshake (fresh keys) Old sessions marked "potentially compromised" When device comes back online:
User sees notification: "Device was offline >72 hours, re-verify keys with contacts" User can trigger manual key rotation Contacts show warning: "Alice's keys may have changed, verify" User-initiated panic recovery:
User action: Press "Emergency Reset" button
Immediate effects:
- Generate new LTIK (old one destroyed)
- Sign revocation certificate with old LTIK (if still available)
- Broadcast revocation to all mesh peers
- All active sessions terminated
- Contacts notified: "Alice has reset all keys"
User must:
- Re-verify identity with all contacts (QR codes)
- Lose access to old message history (encrypted with old keys) Hardware-backed protection:
If device has secure enclave (iPhone Secure Enclave, Android StrongBox):
LTIK stored in secure hardware:
- Cannot be extracted even with full device access
- Requires biometric/PIN for use
- After 3 failed unlock attempts, key temporarily locked
- After 10 failed attempts, key permanently destroyed
Attacker with physical device but no PIN:
- Cannot extract LTIK
- Cannot sign messages as victim
- Can only access already-decrypted messages in RAM Failure Mode 3: Messages Dropped or Duplicated Dropped messages:
Scenario: Message M7 lost in transit (network failure, node crash).
Detection:
Sender (Alice):
-
Sends M7, starts timeout timer (30 seconds)
-
If no ACK from Bob within 30s:
- Assume dropped
- Retransmit M7 via different route
- Increment retransmit_count
-
If retransmit_count > 5:
- Mark message as failed
- Show error to user: "Message to Bob failed to deliver"
- User can retry manually or accept loss Recipient behavior:
Bob:
- Never receives M7
- Receives M8 (out-of-order)
- Stores skipped key for M7
- After 24 hours, if M7 never arrives:
- Delete skipped key for M7
- Accept data loss
- Gap in message history visible to user Implications:
No guaranteed delivery (mesh routing is best-effort) Critical messages: User should wait for confirmation before assuming delivered Message gaps are visible (message numbers not consecutive) Duplicated messages:
Scenario: Network retransmits M3, Bob receives it twice.
Detection:
First M3:
- Decrypt successfully
- Record (conversation_id, msg_num=3) in seen_messages
- Display to user
Second M3:
- Check seen_messages table
- Find msg_num=3 already processed
- Action: Discard silently
- No error to user (common in unreliable networks) Implications:
User never sees duplicate No security impact (replays caught by message number tracking) Failure Mode 4: Clock Drift Scenario: Alice's device clock is off by hours/days.
Problem:
Message timestamps wrong Nonce generation might collide (if using time-based nonce) Replay protection might fail LMP handling:
- Nonce generation (not time-based):
Nonces use message counters, not timestamps Clock drift doesn't affect nonce uniqueness 2. Timestamp validation (lenient):
Received message timestamp: T_msg Current time: T_now
Accept if: T_now - 5 min ≤ T_msg ≤ T_now + 5 min
Rationale:
- 5-minute tolerance handles reasonable clock drift
- Prevents replay of very old messages
- Doesn't require perfect clock sync
- Severe clock drift (>1 hour):
If |T_msg - T_now| > 1 hour:
- Still accept message (don't break communication)
- Show warning: "Message timestamp unusual, check device clock"
- Log potential attack or clock issue
If sender clock consistently wrong:
- Recipient's client suggests: "Alice's clock may be wrong"
- Automatic NTP sync suggested
- Replay protection (clock-independent):
Primary defense: message number tracking (not time-based) Timestamp validation is secondary Even with wrong clock, message numbers prevent replay Edge case: Clock moves backward:
Device clock: 14:00 User sends M10 Clock resets to: 13:00 (1 hour backward) User sends M11
M11's timestamp appears 1 hour earlier than M10 Recipient sees: M10 at 14:00, M11 at 13:00
Handling:
- Timestamps are advisory, not authoritative
- Message numbers still enforce order (M11 > M10)
- No security impact (message numbers prevent nonce reuse) Failure Mode 5: Ratchet Desynchronization Scenario: Alice and Bob's ratchet states diverge (software bug, corrupted state, concurrent messages from multiple devices).
Symptoms:
Messages fail to decrypt "Bad MAC" errors Ratchet counter mismatch Detection:
Bob receives message from Alice:
- Decryption fails
- Check ratchet state:
- Expected message number: 42
- Received message number: 45
- Gap: 3 messages
If gap > MAX_SKIP (1000):
- Ratchet severely desynced
- Cannot recover automatically Recovery protocol:
- Automatic re-synchronization (small gaps):
If gap ≤ 100 messages:
- Try skipped message key mechanism
- If still fails after MAX_SKIP:
- Send error message to peer: "Ratchet desync, initiate rekey"
- Manual re-keying (large gaps):
User action:
- App shows error: "Encryption error with Bob, reset conversation?"
- User confirms
- Both parties perform fresh handshake:
- New ephemeral keys
- New ratchet initialization
- Old state discarded
Consequence:
- Old message history unaffected (already decrypted)
- New messages use fresh keys
- Gap in message numbers visible in UI
- Preventing desync:
Defense mechanisms:
- Atomic state updates (database transactions)
- State checksums (detect corruption)
- Periodic state backups (last-known-good)
- Multi-device sync includes ratchet state Failure Mode 6: Malicious Peer Scenario: Bob's device is fully compromised, under attacker control.
Attacker capabilities (as Bob):
Read all messages Alice sends to Bob Send messages claiming to be Bob Refuse to ratchet forward (keep using old keys) Attempt to extract Alice's keys via exploits What attacker CANNOT do:
Decrypt Alice's messages to Carol (separate sessions) Forge messages from Alice to Carol (lacks Alice's LTIK) Break forward secrecy (past messages already deleted) Prevent Alice from revoking trust in Bob Alice's protection:
- Containment:
Bob's compromise doesn't spread to other conversations Each session is independent 2. Detection:
Suspicious behaviors Alice's client watches for:
- Bob never ratchets (stays on same keys >1 week)
- Bob requests excessive ratchet resets
- Bob's message timing pattern changes dramatically
- Bob attempts protocol downgrade
Warnings shown:
- "Bob's client hasn't updated keys in 10 days (unusual)"
- "Bob requested 5 key resets today (possible attack)"
- User-initiated response:
If Alice suspects Bob is compromised:
- Delete conversation (wipe local history)
- Revoke Bob's introduction attestations (if Alice introduced Bob to others)
- Block Bob's LTIK
- Warn mutual contacts: "I believe Bob may be compromised"
- Deniability protection:
Even if Bob tries to prove Alice sent certain messages: Messages use deniable authentication (MAC, not signature) Bob could have generated same MAC (shared key) No cryptographic proof for third parties 8. Implementation Assumptions Programming Language Core implementation: Rust
Rationale:
Memory safety without garbage collection (prevents whole classes of vulnerabilities) No null pointer dereferences, buffer overflows, use-after-free Fearless concurrency (prevents data races at compile time) Zero-cost abstractions (performance comparable to C) Strong type system catches many bugs at compile time Extensive cryptographic libraries available (RustCrypto, sodiumoxide, oqs-rs) Platform bindings:
iOS/macOS: Swift bindings via FFI Android: Kotlin bindings via JNI Desktop: Native Rust Web: WASM compilation (with caveats—see below) Trusted Hardware Prefer but don't require:
When available (iOS Secure Enclave, Android StrongBox):
LTIK storage:
- Generated inside secure enclave
- Never extracted to main CPU
- Signing operations performed inside enclave
- Requires biometric/PIN for use
Benefits:
- Even with full device compromise, LTIK cannot be extracted
- Physical attacks much harder
When unavailable (older devices, desktop): LTIK storage:
Encrypted with key derived from user password PBKDF2-HMAC-SHA256, 100,000 iterations Stored in OS keychain (encrypted at rest) Memory protection: mlock() to prevent swapping Benefits:
Still requires password to extract Better than plaintext storage Relies on OS security Limitations:
Memory dumps could reveal key if unlocked Malware with root access could extract
Secure enclave operations:
trait SecureStorage {
fn generate_ltik() -> Result<PublicKey, Error>;
fn sign_with_ltik(message: &[u8]) -> Result<Signature, Error>;
fn delete_ltik() -> Result<(), Error>;
}
// iOS Secure Enclave implementation
impl SecureStorage for iOSSecureEnclave {
fn generate_ltik() -> Result<PublicKey, Error> {
// Use SecKeyCreateRandomKey with kSecAttrTokenIDSecureEnclave
// Key never leaves enclave
}
fn sign_with_ltik(message: &[u8]) -> Result<Signature, Error> {
// SecKeyCreateSignature operates inside enclave
// Returns signature, but private key never exposed
}
}
// Fallback software implementation
impl SecureStorage for SoftwareKeystore {
fn generate_ltik() -> Result<PublicKey, Error> {
// Use OS CSPRNG
// Store encrypted in OS keychain
}
fn sign_with_ltik(message: &[u8]) -> Result<Signature, Error> {
// Load encrypted key from keychain
// Decrypt in memory (mlock'd)
// Perform signing
// Zero memory after use
}
}
Hardware capabilities detection:
On startup:
- Query device capabilities
- If secure enclave available: Use it (highest security)
- Else if TEE available: Use TEE (medium security)
- Else: Software fallback (basic security)
User notification:
- "Your device has hardware key protection (most secure)"
- "Your device uses software key storage (less secure, use strong password)"
Random Number Generation
Entropy sources (defense in depth):
Primary: OS CSPRNG
use getrandom::getrandom;
fn get_random_bytes(buf: &mut [u8]) -> Result<(), Error> {
// Linux/Android: getrandom() syscall (ChaCha20-based)
// iOS/macOS: /dev/urandom (Fortuna algorithm)
// Windows: BCryptGenRandom (CNG DRBG)
getrandom(buf)?;
Ok(())
}
Secondary: Additional entropy mixing
fn get_hardened_random(buf: &mut [u8]) -> Result<(), Error> {
// Get OS random
get_random_bytes(buf)?;
// Mix in additional entropy
let mut additional_entropy = Vec::new();
// High-resolution timestamp (nanoseconds)
additional_entropy.extend_from_slice(
&std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_nanos()
.to_le_bytes()
);
// Process ID (prevents VM fork issues)
additional_entropy.extend_from_slice(
&std::process::id().to_le_bytes()
);
// Thread ID (for concurrent RNG calls)
additional_entropy.extend_from_slice(
&thread_id().to_le_bytes()
);
// On mobile: Accelerometer/gyroscope readings
#[cfg(mobile)]
if let Some(sensor_data) = read_sensor_entropy() {
additional_entropy.extend(sensor_data);
}
// Mix via HKDF
let mut mixed = [0u8; 32];
hkdf_sha3_256_extract_expand(
buf, // IKM (input key material)
&additional_entropy, // salt
b"LMP-rng-mixing", // info
&mut mixed
)?;
buf.copy_from_slice(&mixed[..buf.len()]);
Ok(())
}
RNG testing and monitoring:
// Statistical health checks (run on startup)
fn test_rng_health() -> Result<(), Error> {
const SAMPLES: usize = 10_000;
let mut samples = vec![0u8; SAMPLES];
get_random_bytes(&mut samples)?;
// Chi-squared test (detect bias)
let chi_squared = compute_chi_squared(&samples);
if chi_squared > CHI_SQUARED_THRESHOLD {
return Err(Error::RngFailed("Chi-squared test failed"));
}
// Runs test (detect patterns)
let runs = count_runs(&samples);
if runs < MIN_RUNS || runs > MAX_RUNS {
return Err(Error::RngFailed("Runs test failed"));
}
// Ensure high entropy (approximate)
let entropy = estimate_entropy(&samples);
if entropy < 7.9 { // bits per byte
return Err(Error::RngFailed("Low entropy detected"));
}
Ok(())
}
// Continuous monitoring
fn monitor_rng() {
// Every 1000 key generations, check for duplicates
// Should never occur with 256-bit keys
// If duplicate detected: HALT and log critical error
}
VM fork protection:
// Detect if process was forked (cloned VM state)
static FORK_DETECTOR: AtomicU64 = AtomicU64::new(0);
fn check_fork() -> bool {
let current_pid = std::process::id() as u64;
let expected_pid = FORK_DETECTOR.load(Ordering::Relaxed);
if expected_pid == 0 {
// First run, initialize
FORK_DETECTOR.store(current_pid, Ordering::Relaxed);
return false;
}
if current_pid != expected_pid {
// Process ID changed = fork detected
return true;
}
false
}
fn get_random_bytes_fork_safe(buf: &mut [u8]) -> Result<(), Error> {
if check_fork() {
// After fork, reseed RNG
reseed_rng()?;
FORK_DETECTOR.store(std::process::id() as u64, Ordering::Relaxed);
}
get_random_bytes(buf)
}
Catastrophic RNG failure handling:
// If RNG tests fail on startup
fn handle_rng_failure() {
// DO NOT proceed with key generation
// Show error to user: "Device random number generator failed, cannot ensure security"
// Log to telemetry (if user opted in)
// Suggest device reboot
// As last resort: Allow manual entropy input (user types random text, moves mouse)
// But warn: "Manual entropy is less secure than hardware RNG"
}
Memory Protection
Sensitive data handling:
use zeroize::Zeroize;
// Wrapper for sensitive data that auto-zeros on drop
struct Secret<T: Zeroize> {
data: T,
}
impl<T: Zeroize> Drop for Secret<T> {
fn drop(&mut self) {
self.data.zeroize();
}
}
// Usage for keys
type SecretKey = Secret<[u8; 32]>;
fn use_secret_key(key: SecretKey) {
// Use key...
// When key goes out of scope, automatically zeroed
}
Memory locking (prevent swapping):
#[cfg(unix)]
fn lock_memory(buf: &mut [u8]) {
use libc::{mlock, ENOMEM};
unsafe {
let result = mlock(
buf.as_mut_ptr() as *mut libc::c_void,
buf.len()
);
if result != 0 {
let error = std::io::Error::last_os_error();
if error.raw_os_error() == Some(ENOMEM) {
// System limit reached, warn but continue
log::warn!("Failed to lock memory: system limit");
} else {
// Other error, may be security relevant
log::error!("Failed to lock memory: {}", error);
}
}
}
}
#[cfg(windows)]
fn lock_memory(buf: &mut [u8]) {
use winapi::um::memoryapi::VirtualLock;
unsafe {
let result = VirtualLock(
buf.as_mut_ptr() as *mut winapi::ctypes::c_void,
buf.len()
);
if result == 0 {
log::warn!("Failed to lock memory on Windows");
}
}
}
Memory encryption for long-lived secrets:
// For data that must persist in memory (active session keys)
struct EncryptedMemory {
ciphertext: Vec<u8>,
memory_key: [u8; 32], // Itself locked in memory
}
impl EncryptedMemory {
fn new(plaintext: &[u8]) -> Self {
// Generate memory-encryption key from HKDF of random + process state
let mut memory_key = [0u8; 32];
let mut ikm = [0u8; 64];
get_random_bytes(&mut ikm[..32]).unwrap();
ikm[32..].copy_from_slice(&get_process_state());
hkdf_extract_expand(
&ikm,
b"memory-encryption",
&mut memory_key
).unwrap();
lock_memory(&mut memory_key);
// Encrypt plaintext
let mut nonce = [0u8; 12];
get_random_bytes(&mut nonce).unwrap();
let cipher = ChaCha20Poly1305::new(&memory_key.into());
let ciphertext = cipher.encrypt(&nonce.into(), plaintext).unwrap();
EncryptedMemory { ciphertext, memory_key }
}
fn decrypt(&self) -> Vec<u8> {
// Decrypt when needed
// Plaintext should be short-lived, zeroed after use
}
}
impl Drop for EncryptedMemory {
fn drop(&mut self) {
self.memory_key.zeroize();
self.ciphertext.zeroize();
}
}
Compiler-level protections:
// Prevent compiler optimizations from removing security-critical code
#[inline(never)]
fn secure_memzero(buf: &mut [u8]) {
// Use volatile write to prevent optimization
for byte in buf.iter_mut() {
unsafe {
std::ptr::write_volatile(byte, 0);
}
}
// Memory fence to ensure writes complete
std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
}
// Mark functions that handle keys
#[no_mangle] // Prevent name mangling for auditing
#[inline(never)] // Prevent inlining (easier to audit in binary)
fn perform_key_derivation(input: &[u8], output: &mut [u8]) {
// Key derivation code...
}
Compilation and Distribution
Reproducible builds:
# Dockerfile for reproducible builds
FROM rust:1.75.0-alpine3.19
# Pin exact versions of all dependencies
RUN apk add --no-cache \
gcc=13.2.0-r5 \
musl-dev=1.2.4-r2 \
openssl-dev=3.1.4-r5
# Set deterministic build flags
ENV RUSTFLAGS="-C target-cpu=generic -C opt-level=3 -C debuginfo=0"
ENV SOURCE_DATE_EPOCH=1704067200 # Fixed timestamp for reproducibility
# Copy pinned dependencies
COPY Cargo.lock /build/
COPY Cargo.toml /build/
# Build
WORKDIR /build
RUN cargo build --release --locked
# Verify
RUN sha256sum target/release/lmp-client
Build verification:
#!/bin/bash
# verify-build.sh
# User rebuilds from source
docker build -t lmp-build .
docker run lmp-build sha256sum /build/target/release/lmp-client > user-hash.txt
# Compare with published hash
curl https://lmp-project.org/releases/v1.0.0/SHA256SUMS > official-hash.txt
if diff user-hash.txt official-hash.txt; then
echo "✓ Binary verified: matches official build"
else
echo "✗ Binary mismatch: possible tampering"
exit 1
fi
Code signing:
# Multiple independent signatures required
# Official releases signed by 3+ core developers
# Developer 1 signs
gpg --detach-sign --armor -u developer1@lmp.org lmp-client
# Developer 2 signs
gpg --detach-sign --armor -u developer2@lmp.org lmp-client
# Developer 3 signs
gpg --detach-sign --armor -u developer3@lmp.org lmp-client
# Users verify all signatures
gpg --verify lmp-client.asc.dev1 lmp-client
gpg --verify lmp-client.asc.dev2 lmp-client
gpg --verify lmp-client.asc.dev3 lmp-client
# Require 3-of-5 developer signatures for official release
Runtime Protections
Anti-debugging (on mobile):
#[cfg(target_os = "ios")]
fn detect_debugger() -> bool {
use libc::{getpid, ptrace, PT_DENY_ATTACH};
unsafe {
// On iOS, prevent debugger attachment
ptrace(PT_DENY_ATTACH, 0, std::ptr::null_mut(), 0);
}
// Check if debugger is attached
// (Implementation varies by platform)
false
}
#[cfg(target_os = "android")]
fn detect_debugger() -> bool {
// Check for /proc/self/status TracerPid
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
for line in status.lines() {
if line.starts_with("TracerPid:") {
let pid: u32 = line.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if pid != 0 {
return true; // Debugger attached
}
}
}
}
false
}
// On startup
fn security_checks() {
if detect_debugger() {
// In production: Exit immediately
// In debug builds: Warn but continue
#[cfg(not(debug_assertions))]
{
eprintln!("Debugger detected, exiting for security");
std::process::exit(1);
}
}
}
Root/jailbreak detection:
fn is_device_compromised() -> bool {
#[cfg(target_os ="ios")] { // Check for jailbreak indicators let jailbreak_files = [ "/Applications/Cydia.app", "/Library/MobileSubstrate/MobileSubstrate.dylib", "/bin/bash", "/usr/sbin/sshd", "/etc/apt" ];
jailbreak_files.iter().any(|path| std::path::Path::new(path).exists())
}
#[cfg(target_os = "android")]
{
// Check for root indicators
let root_files = ["/system/app/Superuser.apk", "/system/xbin/su"];
root_files.iter().any(|path| std::path::Path::new(path).exists())
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
{
false // Desktop assumed trusted
}
}
// Warn user but don't block fn check_device_security() { if is_device_compromised() { show_warning("Device may be rooted/jailbroken. Security reduced."); } }
---
## 9. Summary
### What Makes LMP Meaningfully New
**1. Hybrid post-quantum cryptography in production messaging:**
- First practical messaging system combining classical (X25519) and PQ (Kyber768) at all layers
- Protects against both current and future quantum threats
- Other systems (Signal, WhatsApp) remain quantum-vulnerable
**2. True peer-to-peer with metadata resistance:**
- No central servers means no single point of metadata collection
- Mesh routing + cover traffic + padding resist traffic analysis
- Contrast: Signal (centralized), Matrix (federated but servers see metadata), blockchain (public ledger)
**3. Mathematically bounded compromise impact:**
- Forward secrecy + automatic ratcheting limits damage from key leaks
- Device compromise affects only that device's sessions
- Time-bounded damage (keys rotate every 24h minimum)
**4. Transparent verifiability:**
- Reproducible builds allow independent verification
- No "trust us" required—users can audit source and rebuild
- Multi-signature releases prevent single developer compromise
### Primary Security Property
**Metadata minimization with forward-secure end-to-end encryption.**
LMP prioritizes hiding *who talks to whom* and *when*, not just *what* they say. Most systems protect content but leak metadata. LMP makes metadata collection expensive via:
- Mesh routing (no central observer)
- Cover traffic (hide real message timing)
- Padding (hide message sizes)
- Ephemeral identities (no phone numbers)
Combined with:
- Post-quantum resistance (future-proof)
- Forward secrecy (past messages secure even after compromise)
- Post-compromise recovery (automatic healing)
### Why This Is Realistic, Not Science Fiction
**Honest trade-offs acknowledged:**
- Higher latency (2-8 seconds vs <1 second)
- More battery/bandwidth usage (cover traffic)
- Smaller user base (no viral growth mechanism)
- No message sync across devices (by design)
- Requires technical sophistication
**Uses proven primitives:**
- No custom cryptography invented
- libsodium (X25519, ChaCha20, Ed25519)
- liboqs (Kyber, Dilithium)
- Signal's Double Ratchet (proven design)
**Deployable today:**
- All cryptographic primitives available in libraries
- Mesh networking well-understood (Tor, I2P as precedents)
- Implementation in Rust provides memory safety
- Works on existing hardware (no special requirements)
**Threat model is realistic:**
- Assumes 30% node compromise (not 0% or 100%)
- Acknowledges global passive adversary limitations
- Out-of-scope: permanently compromised endpoints, physical coercion
- Honest about what can't be protected
**For the right users:**
- Journalists, activists, whistleblowers—those facing nation-state threats
- Not for casual messaging (Signal is better for most people)
- Willing to sacrifice convenience for security
LMP is an engineering solution, not magic. It raises the cost of surveillance from "trivial" to "requires substantial nation-state resources," which is the best realistic outcome for secure communication.