Spec rev 0x0e2a · last review 2026-05-27

Cryptographicspecification.

The full technical reference for Lumes's 12 cryptographic layers, threat model, and primitives. Every algorithm, every parameter, every assumption — written down so it can be verified, attacked, and broken in public.

Document meta
spec version
v0.9.4-rc
keyset rev
0x0e2a
last commit
2026-05-27
audit status
internal · pending external
page hash
a3f1.9e0c.4b21

Introduction

Lumes is an end-to-end encrypted messenger designed for an adversarial threat model — sophisticated attackers with budget, access to commercial forensic tools (UFED, GrayKey), the ability to mount active MITM attacks, and in some cases physical custody of the device. Standard messengers protect against passive network surveillance. Lumes protects against the rest.

This is a living document. Until external audit is complete, treat every claim here as internally verified, not independently confirmed. Where we are honest about a limitation, the limitation is the spec.

Design principles

The cryptographic design follows five rules. They are not negotiable.

01 · Defense in depth

No single primitive is allowed to be the only thing standing between an attacker and a user's secrets. Twelve independent layers, each with its own assumed threat.

02 · Verifiable primitives only

Every cryptographic operation uses audited, peer-reviewed code: libsodium, tweetnacl, the NIST PQC reference implementations via liboqs. No hand-rolled crypto.

03 · Post-quantum dual signing

Classical primitives (X25519, Ed25519) ship alongside their lattice-based counterparts (ML-KEM-768, ML-DSA-65). Both must verify; breaking one family does not break the other.

04 · Forward secrecy beyond plaintext

When a message is deleted, the message key and the prekey and the group symmetric key are zeroed. Past sessions cannot be reconstructed.

05 · Honesty over hype

If we cannot prove a claim, we mark it pending. External audit is pending. The comparison table in the landing page says so.

Assumptions

What the spec relies on, and what it does not.

What we assume

  • The user's PIN is at least 6 digits and is not entered under coercion (duress PIN is the answer to coercion).
  • The compiled binary is the one shipped from our controlled build pipeline; release artefacts are signed (SHA-256 + ML-DSA-65).
  • The operating system kernel is not actively compromised at install time.
  • The NIST FIPS 203 / 204 standards are sound for the security level we target (Category III).

What we do not assume

  • That the device will never be seized.
  • That the network is trustworthy.
  • That the peer is uncompromised.
  • That RAM cannot be dumped while the app is running.
  • That the OS Keychain is unreadable to a forensic adversary.

The 12 layers

Each layer below has its own section. Layers are independent — breaking layer N does not break layer N+1.

01 · PIN-bound envelope

Every long-term secret stored on disk — identity keys, prekey bundle, ratchet root key, group symmetric keys — is wrapped in an Argon2id-derived envelope before persistence.

fn wrap(secret: &[u8], pin: &SecureString) -> Envelope {
  let salt = random_bytes(16);
  let kek  = argon2id(pin, salt,
    m_cost = 64 MiB,
    t_cost = 3,
    parallelism = 4,
    output_len = 32
  );
  let nonce = random_bytes(12);
  let ct    = aes_gcm_256.encrypt(kek, nonce, secret);
  Envelope { salt, nonce, ct, tag: ct.tag }
}

The PIN never touches disk. The key encryption key (KEK) is computed on every unlock; if the PIN is wrong, decryption fails on the authentication tag.

Cost to brute-force

At Argon2id m=64 MiB / t=3 / p=4, a single guess takes ~250 ms on a modern phone. For a 6-digit PIN, the expected brute-force cost is 5 × 10⁵ × 0.25 s ≈ 35 hours — but the device's secure enclave throttles unlock attempts to ~10/min, raising the realistic cost to ~6 weeks per device.

02 · Realm DB encryption

The Realm database file is encrypted with a 512-bit key derived from the unwrapped identity material. The realm key is itself wrapped in the PIN envelope (layer 1) — so layer 2 is meaningful only after layer 1 is unwrapped.

Realm uses AES-256-CBC with HMAC-SHA-256 for authentication, applied per-page.

03 · X25519 handshake

Classical Diffie-Hellman over Curve25519, per RFC 7748. Used as one half of the dual handshake; the other half is layer 4.

let our_sk      = X25519.generate();
let our_pk      = X25519.pk_from_sk(&our_sk);
let shared      = X25519.scalar_mult(&our_sk, &their_pk);
// shared is fed into HKDF along with the PQC shared secret (layer 4)

04 · ML-KEM-768

Post-quantum key encapsulation, per NIST FIPS 203. ML-KEM-768 provides Category III security (≈ AES-192 against quantum and classical adversaries).

let (their_pq_pk, their_pq_sk) = ml_kem_768.keypair();
let (ct, pq_shared) = ml_kem_768.encap(&their_pq_pk);
// pq_shared is concatenated with the X25519 shared (layer 3)
// then fed through HKDF-SHA-512:
let root = hkdf_sha512("lumes-v1-rk", x25519_shared || pq_shared, 64);

Why hybrid? If a future quantum computer breaks X25519, the PQC half of the handshake still holds. If a classical or implementation flaw breaks ML-KEM, the X25519 half holds. Both must fail to compromise the session.

05 · Ed25519 signing

Classical signatures over the handshake transcript, per RFC 8032. Identity binding for the X25519 half.

06 · ML-DSA-65 signing

Post-quantum signatures over the handshake transcript, per NIST FIPS 204. Identity binding for the ML-KEM half. Both Ed25519 (layer 5) and ML-DSA-65 (layer 6) signatures must verify.

07 · Double Ratchet

Full Signal-style Double Ratchet, three-step: DH out, chain advance, message key derivation. Provides forward secrecy (past keys do not compromise future messages) and post-compromise security (future keys recover even if a past key leaks).

// per message
let dh_out  = X25519.scalar_mult(&our_ratchet_sk, &their_ratchet_pk);
let (rk', ck) = hkdf_sha512("lumes-v1-rk-chain", rk || dh_out, 64);
let mk      = hkdf_sha512("lumes-v1-mk", ck, 32);
ck = hkdf_sha512("lumes-v1-ck", ck, 32);
rk = rk';

08 · ChaCha20-Poly1305

IETF AEAD per RFC 8439. Symmetric encryption of every message body using the per-message key derived in layer 7.

09 · SecureString

In-memory representation of sensitive material. Wraps a Uint8Array; exposes only a use(fn) method that decodes into a string, runs the closure, and zeroes the buffer with sodium.memzero in a finally block.

class SecureString {
  #buf: Uint8Array;
  #released = false;

  use<T>(fn: (s: string) => T): T {
    if (this.#released) throw new Error("use-after-zero");
    const s = new TextDecoder().decode(this.#buf);
    try { return fn(s); }
    finally { sodium.memzero(this.#buf); this.#released = true; }
  }
}

10 · WebRTC ICE signing

Every signaling message and every ICE candidate is Ed25519-signed by the sender, timestamped, and sequence-numbered. The receiver maintains a sliding 30 s window; messages outside the window or with a duplicate sequence number are rejected.

11 · Duress PIN

A second PIN, indistinguishable from the primary at the entry screen. Entering it silently zeroes the identity material, the prekey bundle, and the ratchet state — bypassing any USB / MDM / custody hooks.

Four independent enforcement layers — pre-decryption check, post-decryption check, runtime watchdog, and Realm-side trigger — ensure no single bypass leaks plaintext.

12 · Forward-secure delete

When a disappearing message expires, four buffers are zeroed in this order: plaintext → mk (message key) → spk (signed prekey from the session) → gsk (group symmetric key, if any).

Past sessions cannot be reconstructed even if the device is later seized.

Threat model

What Lumes defends against, and what it does not.

In scope

  • Forensic extraction tools (UFED, GrayKey, XRY)
  • Active network MITM, including state-level adversaries
  • Compromised peer (one side of the conversation)
  • Live RAM acquisition / JTAG / kernel hooks
  • Coercion / custody (duress PIN)
  • Future quantum cryptanalysis (PQC dual signing)

Out of scope

  • The user voluntarily revealing the PIN.
  • A compromised OS kernel at install time (we cannot verify code we did not run).
  • Side-channel attacks on the device's CPU (we use constant-time primitives but cannot guarantee absence of µarch leaks).
  • Camera, microphone, or screen content captured by a separate hostile process.

Primitives reference

PrimitiveStandardSource
x25519rfc 7748libsodium
ed25519rfc 8032libsodium
ml-kem-768fips 203liboqs (nist ref)
ml-dsa-65fips 204liboqs (nist ref)
chacha20-poly1305rfc 8439libsodium
argon2idrfc 9106libsodium
hkdf-sha512rfc 5869libsodium
aes-256-gcmnist sp 800-38dplatform

FAQ

Q · Why hybrid post-quantum and not pure PQC?

PQC standards are new. Lattice-based KEMs survived NIST's 5-year scrutiny but have less battle-testing than X25519. Hybrid is the conservative choice for the next decade.

Q · Why Argon2id and not Argon2i or Argon2d?

Argon2id combines the side-channel resistance of Argon2i with the GPU resistance of Argon2d. It is the variant explicitly recommended by RFC 9106 for password hashing.

Q · Where is the external audit?

Pending. We will publish the auditor, the scope, and the full report when complete. Until then, every claim in this document should be read as internally verified.

Q · Why no telemetry?

Because metadata is the message. If Lumes's server could see which two users talk, when, and how often, the encryption would be irrelevant for the threat model we target.

Responsible disclosure

If you find a vulnerability, please report it to gonxaa@proton.me using our PGP key (fingerprint 4A2F · 0E2A · 7C8B · 19F2 · 06C1).

We commit to:

  • Acknowledging your report within 48 hours.
  • Publishing a fix and credit within 90 days (or earlier if the issue is critical).
  • Not pursuing legal action against good-faith researchers.