Guides

Cryptography Specification

This is the normative specification of Komms's cryptographic core (kult-crypto). Design rationale lives in the ADRs; this document says what is built. Threat mapping: 02: Threat Model.

Status: implemented and normative. Any cryptographic or wire-format deviation requires a new accepted ADR plus compatibility and migration review.

1. Primitives

Purpose Primitive Crate (RustCrypto unless noted) Rationale
AEAD (messages, storage, sealed envelopes) XChaCha20-Poly1305 chacha20poly1305 192-bit nonce → random nonces are safe (collision prob. negligible); constant-time in pure software, no AES-NI needed on phones and cheap mesh gateways; large security margin.
ECDH X25519 x25519-dalek Ubiquitous, misuse-resistant, small keys (32 B, matters on LoRa).
Signatures Ed25519 ed25519-dalek Identity keys and prekey signing only, never over message content (deniability).
Post-quantum KEM ML-KEM-768 (FIPS 203) ml-kem NIST-standardized; hybrid with X25519 so both must break.
KDF HKDF-SHA-256 hkdf, sha2 The Double Ratchet's specified KDF; interoperable with published test vectors.
Hashing / fingerprints SHA-256; BLAKE3 for bulk/file hashing sha2, blake3 SHA-256 where protocol-conservatism matters; BLAKE3 where speed does.
Password KDF (at-rest key) Argon2id argon2 Memory-hard; parameters in §8.
Secret hygiene zeroize on every secret type zeroize Keys never outlive their use.

All random generation uses the OS CSPRNG (OsRng); no userspace PRNG state.

2. Key inventory

Key Type Lifetime Purpose
Identity key IK Ed25519 (+ X25519 via separate keypair, cross-signed) Long-term The user's identity; signs prekey bundles.
Signed prekey SPK X25519 Rotated ~weekly Medium-term DH input; signed by IK.
PQ signed prekey PQSPK ML-KEM-768 Rotated ~weekly KEM input for hybrid handshake; signed by IK.
One-time prekeys OPK_i X25519 Single use Strengthens first-message forward secrecy.
Ephemeral key EK X25519 Single handshake Sender-side handshake freshness.
Ratchet keys X25519 Per DH ratchet step Double Ratchet public ratchet.
Chain/message keys 32-B symmetric Single message Derived; zeroized after use.
Storage master key SK 32-B symmetric Long-term (rotatable) At-rest encryption root, §8.

We deliberately use separate Ed25519 and X25519 identity keypairs (cross-signed at creation) instead of birational conversion: simpler to audit, no edge-case pitfalls.

3. Handshake: hybrid PQXDH

Establishes the initial shared secret between Alice (initiator) and Bob (recipient, who may be offline), following Signal's PQXDH construction adapted to our encoding.

Bob publishes a prekey bundle (via DHT or exchanged directly/QR; see 06: Identity & Trust):

Bundle_B = { IK_B, SPK_B, Sig(IK_B, SPK_B), PQSPK_B, Sig(IK_B, PQSPK_B), OPK_B? , relay hints,
             expiry, Sig(IK_B, canonical-bundle) }

The final signature covers a canonical serialization of every field, including the expiry, the OPK (or its absence), and the relay hints, so whoever serves the bundle (a DHT node, a courier) can withhold it but cannot extend its lifetime, strip its OPK, or redirect its relay hints (06: Identity & Trust §2).

An out-of-band KPB2 pairing wrapper additionally carries the current 32-byte Connect discovery capability and its non-zero generation. The active physical device signs the exact encoded authority-bound KDP2 bundle, capability, and generation under "Komms-connect-pairing-bundle-v2". Capability or generation substitution therefore fails before the first flight is queued. The capability derives only the separate introduction-mailbox token; it is not PQXDH or ratchet key material.

Alice verifies all three signatures, then computes:

DH1 = DH(IK_A_x25519, SPK_B)
DH2 = DH(EK_A,        IK_B_x25519)
DH3 = DH(EK_A,        SPK_B)
DH4 = DH(EK_A,        OPK_B)          # if an OPK was available
(KEM_ct, KEM_ss) = ML-KEM-768.Encaps(PQSPK_B)

SK_root = HKDF-SHA-256(
    ikm  = 0xFF*32 || DH1 || DH2 || DH3 || DH4 || KEM_ss,
    salt = 0*32,
    info = "Komms-PQXDH-v1"
)

The first envelope to Bob carries IK_A, EK_A, the OPK id used, and KEM_ct, plus an initial Double-Ratchet message encrypted under SK_root with the handshake transcript hash as associated data (binds the ciphertext to the exact bundle used; a MITM who swaps any handshake element causes AEAD failure).

Security properties: mutual (deniable) authentication from DH1/DH2; forward secrecy from EK/OPK; post-quantum confidentiality because SK_root is not computable without breaking both X25519 and ML-KEM-768.

4. Sessions: Double Ratchet

Standard Double Ratchet (Signal specification) with these fixed parameters:

Parameter Value
Root/chain KDF HKDF-SHA-256, domain-separated info strings ("KK-root", "KK-chain", "KK-msg")
Message AEAD XChaCha20-Poly1305; nonce = 24 random bytes carried in the envelope
Header encryption Enabled (Double Ratchet HE variant): ratchet public keys and counters are not visible on the wire
Max skipped message keys per chain (MAX_SKIP) 1 000
Max stored skipped keys per session 2 000, LRU-evicted, each with 30-day TTL
AEAD associated data session id ‖ protocol version

Delay-tolerance rationale: off-grid links reorder and delay heavily; generous MAX_SKIP with bounded, TTL'd storage keeps weeks-stale fragments decryptable without enabling a memory-exhaustion attack. These bounds are normative: implementers must not raise them without an ADR.

Deniability: no content signatures anywhere. Authenticity comes from AEAD under keys that both parties (and only they) could derive; either could have forged the transcript, so it proves nothing to third parties.

5. Envelope format

Compact binary, little-endian, fixed field order (no self-describing serialization in the hot path: every byte counts on LoRa). One envelope per message or fragment:

byte    0      : version (0x01 ordinary | 0x02 retained)
byte    1      : type (0x01 msg | 0x02 handshake | 0x03 receipt | 0x04 fragment)
bytes   2..34  : delivery token (32 B, §7)
bytes  34..42  : v2 only: retention_until (u64 LE, hour-aligned Unix seconds)
bytes  34/42..N: body (type-specific, always ciphertext)

Envelope v2 is used only when sealed work carries an authenticated ephemeral retention bucket. The cleartext value lets an intermediary delete without keys; the recipient accepts it only when content-v1 kind 0x0005 contains the exact same canonical hour ceiling. A missing, extra, non-canonical, or mismatched hint is terminal and never enters history. The hint is advisory to a relay and does not weaken endpoint authentication.

Bodies by type. msg/receipt: an encoded ratchet message (version ‖ encrypted header(80) ‖ nonce(24) ‖ ciphertext+tag); handshake: an anonymous-boxed first flight (ephemeral X25519 pub(32) ‖ nonce(24) ‖ ciphertext+tag), so the initiator's identity travels only inside AEAD; fragment: an 8-byte fragment header (message id hash 4 B = truncated BLAKE3 of the whole payload, index 2 B LE, count 2 B LE) followed by the payload slice; reassembly precedes decryption and re-verifies the id hash over the assembled bytes. Fragmentation policy and MTU tables: 05: Transports §4.

Padding: plaintext is padded (ISO/IEC 7816-4) to size buckets {192 B, 512 B, 1 KiB, 4 KiB, 16 KiB, 64 KiB} before encryption; larger payloads (media) are chunked at 64 KiB. The 192 B bucket exists so a short text message plus overhead still fits typical LoRa payloads after fragmentation into ≤2 frames.

5.1 Authenticated edit content

C3 Edit is content-v1 kind 0x0004 inside the plaintext described above. Its exact author/content reference, revision, and replacement UTF-8 are protected by the same Double Ratchet or group sender-key AEAD as the original. Nothing in the outer envelope identifies an edit. Pairwise authorization uses the authenticated content sender and exact target bytes; visible names and local timestamps are excluded. A bare sender-key group would provide only membership-level authenticity because every member holds the content key. ADR-0029 therefore adds a distinct pairwise-distributed origin key and recipient tag around the shared ciphertext. The recipient verifies that tag against the certified sender device before advancing the sender chain or applying the edit. Resolution by maximum (revision, edit_content_id) is application convergence, not a new cryptographic primitive or signature. The normative encoding and compatibility contract are ADR-0020, ADR-0029, and 18: Authenticated Message Editing.

5.2 Authenticated ephemeral content

C4 uses content-v1 kind 0x0005. Its payload is version(1) ‖ mode(1) ‖ reserved(2) ‖ expires_at(8) ‖ retention_until(8) ‖ body_len(4) ‖ body. Mode 1 carries non-empty bounded UTF-8; mode 2 carries the existing canonical attachment manifest. retention_until must equal the one-hour ceiling of the exact expires_at; supported lifetimes are 60 seconds through 30 days. The whole payload remains inside pairwise Double Ratchet or group sender-key encryption.

Exact local expiry and first-open consumption are lifecycle rules, not new cryptographic erasure primitives. Terminal sealed tombstones prevent duplicate or reordered ciphertext from restoring plaintext. Encoding, compatibility, metadata leakage, and backup behavior are normative in ADR-0021 and summarized in 19: Disappearing Messages and View-Once Attachments.

5.3 Authenticated live-call control and media

C7 signaling is content-v1 kind 0x0008 inside the pairwise Double Ratchet. The offer carries a fresh random 32-byte call master secret and binds a random 16-byte call id, exact initiating physical device, and expiry. Answer/decline/ busy/hangup bind the exact responding physical device. No outer envelope field identifies a call, and call control never becomes durable history or backup content.

After one answer wins, the media context binds the call id, both stable account identities, and both exact physical-device ids. HKDF-SHA-256 derives separate initiator→responder and responder→initiator media/header bases. Each direction must authenticate an empty hello before audio; XChaCha20-Poly1305 protects the record kind, phase, sequence, timestamp, and at most 1,275 Opus bytes under the context hash. Keys rotate every 4,096 records and a 128-record replay window fails closed. Wrong context, role, direction, phase, call, replay, tamper, and post-terminal media are rejected without releasing plaintext. Master/derived keys and decoded packets zeroize on terminal/drop paths. The normative contract and limitations are ADR-0013 and 23: Live Audio Calls.

6. Group messaging (sender keys with recipient-authenticated origins)

Per group, each sending physical device generates a sender chain: (key_id: 16 random bytes, chain_key: 32, iteration: u32). The chain advances with ck' = HKDF(ck, "KK-group-chain") and derives mk = HKDF(ck, "KK-group-msg"). Receiving chains reuse the pairwise delay-tolerance bounds: at most 1,000 skipped keys per advance, 2,000 retained keys under LRU eviction, and a 30-day TTL.

The original v1 body is version(1) ‖ enc_header(60) ‖ nonce(24) ‖ ciphertext+tag. Its sealed header contains key_id ‖ iteration. That format authenticates membership, not an individual sender; it is accepted only as visibly labelled legacy history.

The stable v2 body remains one shared XChaCha20-Poly1305 ciphertext, preserving the encrypt-once mesh property. Its sealed 76-byte header additionally binds the 16-byte content id. For each sender account/device, sender chain, recipient account/device tuple, the sender distributes a separate random 32-byte origin_key over the authenticated pairwise device session. Every recipient wrapper adds:

HMAC-SHA-256(origin_key,
    "Komms-Group-Origin-v1" ||
    group_id || sender_account || sender_device ||
    recipient_account || recipient_device ||
    sender_chain_key_id || content_id ||
    u64_le(authenticated_retention_or_zero) ||
    SHA-256(shared_group_ciphertext))

The receiver verifies this tag in constant time before advancing or decrypting the sender chain. It derives the stored author only from the verified pairwise device certificate and accepted device-authority chain. Text, attachments, edits, votes, polls, expiry, roles, moderation, ownership, and device-sync imports use the same rule. Roster, device, session, authority, or group generation changes rotate sender chains and origin capabilities. Exact distribution, rotation, upgrade, replay, and deniability rules are in ADR-0029; the sender-key construction remains in ADR-0012.

C6 authority state adds identity/device signatures where durable authority requires third-party verification. Canonical full authority state uses Komms-group-authority-state-v1; ordered ownership certificates use Komms-group-owner-transfer-v1; generation-bound admin requests use Komms-group-admin-request-v1; and owner poll-moderation snapshots use Komms-group-poll-moderation-v1. Each domain signs its own bounded canonical bytes, never an ordinary chat message or vote. Moderation binds its exact group id, poll author/id, authority generation, and final vote heads. The prior owner signs the state that enacts transfer; the new owner signs later states. Every accepted authority transition also rotates the group secret and sender chains. Exact state, chain, fork, and verification rules are in ADR-0023.

7. Sealed sender & delivery tokens

Goal: intermediaries learn neither sender nor recipient identity (02: Threat Model §5, adversary A5).

  • Delivery token: token_i = HMAC-SHA-256(K_mailbox, "KK-token-v1" ‖ u64_le(epoch_i) ‖ IK_recipient). The complete 32-byte HMAC is used. K_mailbox is a per-contact-pair secret derived from the session root, epoch_i rotates daily, and IK_recipient (the addressee's Ed25519 identity key) splits each pair's tokens into two disjoint per-direction sequences, so when both parties use the same collect-and-delete relay, neither's check-in can drain mail addressed to the other (ADR-0007). Only sender and recipient can compute or recognize the sequence; a relay sees uncorrelatable 32-byte values. The recipient hands current tokens to its chosen relays as "accept mail for these" filters.
  • Sender anonymity: the envelope contains no sender field at all; sender identity is established inside the AEAD (encrypted header / handshake data). Transport-level anonymity is bounded per the threat model's residual-risk table.

8. Encryption at rest

passphrase/biometric-unlocked keystore
        │ Argon2id (m=256 MiB, t=3, p=4; mobile profile m=64 MiB)
        ▼
   KEK (32 B)
        │ unwraps (XChaCha20-Poly1305)
        ▼
   SK: storage master key
        │ HKDF-SHA-256, purpose-separated info strings
        ▼
   metadata / index root / row root / cursor root / media
                    │
                    └─ database + table + index-slot separation
  • Database: SQLite; every stored row is individually AEAD-sealed with a random 24-byte nonce and bound to its database id, schema, table domain, and final locator. Guessable equality keys become database/table/slot-separated keyed indexes rather than plaintext SQLite columns. There is no reliance on whole-file encryption alone.
  • Encrypted backups serialize validated logical records under their independent mnemonic-derived outer AEAD. They do not copy source database ids, opaque indexes, SQLite pages, or wrapped database-row ciphertext.
  • Ratchet session state is the most sensitive record class; serialized state is additionally wrapped and zeroized in memory after each persist.
  • Key rotation: re-wrap SK under a new KEK (passphrase change is O(1)); full SK rotation re-encrypts lazily.

9. Fingerprints & verification

Safety number = SHA-256 iterated 5 200× over (version ‖ IK_min ‖ IK_max), identity keys sorted bytewise so both parties compute the identical value. The 30 decimal digits (6 groups of 5, about 100 bits) are taken from the first 24 bytes of HKDF-SHA-256(digest, info = "KK-fingerprint") expanded to 48 bytes, read as 6 big-endian u32 words each reduced mod 100 000; the full raw 32-byte digest is the QR comparison value. Rationale and UX: 06: Identity & Trust.

10. Explicit exclusions

  • No custom primitives, ever. Constructions may be composed here; primitives come from published, externally maintained crates pinned by exact version and checksum. Dependency provenance is not a substitute for independent review of Komms's composition and implementation.
  • No compression before encryption (compression-oracle class attacks).
  • No protocol-level plaintext timestamps; time lives inside the AEAD.
  • kult-crypto is no_std-compatible (alloc-only) to keep the door open for microcontroller-class ports.

11. Test obligations (normative for M1)

  1. Known-answer tests against published X25519, Ed25519, ML-KEM-768, XChaCha20-Poly1305, HKDF, and Argon2id vectors.
  2. Double Ratchet interop vectors generated against a reference implementation, committed to the repo.
  3. Property tests: ratchet under arbitrary message loss/reorder within MAX_SKIP always decrypts; beyond bounds always fails closed.
  4. Fuzzing (cargo-fuzz) on envelope, handshake, backup/mnemonic, attachment, device-prekey, and call-media parsing/opening.
  5. cargo-deny + pinned lockfile; no git dependencies in kult-crypto.
  6. Call media KAT/property coverage for both directions, mandatory hello, tamper/replay/wrong-context failure, key-phase rotation, and exact bounds.
Edit this page on GitHub ↗