Message cryptographyTechnical referenceBrowse
Reference

Cryptography

Standard primitives, composed carefully, with the composition's evidence status stated rather than assumed. Auris invents no primitives. The claims about how they compose are tracked one by one in the claims ledger; this page describes the mechanisms.

Versioned reference
On this page

Primitives and parameters

RoleInstantiationNotes
Diffie-HellmanX2551932-byte public keys
KEM, handshakeML-KEM-1024 (FIPS 203)1568-byte keys and ciphertexts; signed and one-time prekeys, since build 146
KEM, ratchetML-KEM-768 (FIPS 203)per-run material inside the protected header
KEM, inner transportML-KEM-768 (FIPS 203)Build 147 hybrid path: an ephemeral key per socket plus encapsulation to the backend's pinned static key
SignatureEd2551964-byte signatures
PQ signatureML-DSA-65 (FIPS 204)3309-byte signatures; required whenever advertised
Hash / KDF / MACSHA-256 · HKDF-SHA256 · HMAC-SHA256chain-key ratchet on HMAC; all derivations domain-separated
AEAD, messagesChaCha20-Poly1305 (IETF)32-byte key, 12-byte nonce, 16-byte tag
AEAD, attachments and call framesAES-256-GCMCryptoKit; disjoint nonce domains per media stream
Password hashingArgon2id v1.3 (RFC 9106)m=64 MiB, t=3, p=1; reference source compiled in-tree

ML-KEM and ML-DSA are Apple's CryptoKit implementations on the iOS 26 floor, verified with known-answer and conformance checks in the repository. Coverage and execution are specific to a test run; a standard algorithm is not a FIPS certification of Auris. A future backend needs explicit byte and error-semantics conformance.

Parameter sources: NIST FIPS 203 and FIPS 204. Both publication pages list errata notices; the original publication dates alone are not a complete update check.

Identity and prekeys

Each user holds three long-term keys in the Keychain, device-bound and never synced: an X25519 identity key, an Ed25519 signing key, and an ML-DSA-65 signing key. Handshake material is a weekly-rotating signed prekey (X25519) and an ML-KEM-1024 signed prekey, each signed by both signing keys; the previous generation is kept for a grace window so a handshake begun against a just-rotated key still completes. A pool of one-time prekeys (each an X25519 key plus an ML-KEM-1024 key, individually signed) is dispensed one per new session by the server, which deletes the public half on dispense.

The responder deletes a consumed one-time private key only after it has durably committed the accepted session together with a retirement intent. The intent supports retrying retirement after a crash; it does not prove every storage interleaving. The intended post-compromise benefit of one-time keys still depends on the platform actually making deleted storage unrecoverable; that premise, and a full proof, remain open (ledger row C1-OTP). When the pool is empty the handshake falls back to signed reusable last-resort prekeys, which do not carry the one-time erasure premise.

First contact: PQXDH v4

First-contact sequence
  1. Initiator → directoryRequest the peer’s key bundle

    Look up the Auris ID, with the invitation token when required.

  2. InitiatorVerify the bundle

    Check the Ed25519 and ML-DSA-65 signatures before using the peer’s advertised keys.

  3. Initiator → responderConstruct and send the sealed initial

    Combine the X25519 and ML-KEM-1024 contributions. The envelope contains the identities, capabilities and signed transcript.

  4. ResponderOpen the envelope

    Perform the preliminary DH/KEM work needed to decrypt the enclosed transcript.

  5. ResponderAuthenticate the transcript

    Verify the reconstructed transcript and its contained signatures before accepting the session.

  6. ResponderCommit the accepted session

    Only the authenticated result may become accepted session state.

This shows the released pv4 initial. Opening comes before transcript verification; acceptance comes after it. Error, retry and persistence paths are omitted. The frozen v5 research construction is separate.

Session setup runs X3DH (three Diffie-Hellman exchanges, or four when a one-time prekey was available) and mixes an ML-KEM-1024 shared secret into the root key. The complete initiator transcript, binding both identities, both negotiated capability statements, the chosen prekeys and the KEM ciphertext, is signed with Ed25519 and ML-DSA-65. Since build 128 a responder verifies the initial's signatures before accepting the handshake. The sealed envelope adds a preliminary opening step in build 146. A bundle without the required signed capability suite (current first-contact floor: capability 8) is refused rather than normalised to an older format.

Since build 146 the initial is sealed: everything identity-bearing, and both signatures, ride inside an AEAD box under a key derived from the initiator's ephemeral key, the responder's identity key and the KEM shared secret. These inputs are derivable by the responder without knowing who the initiator is. The server relays an ephemeral key, a KEM ciphertext and prekey selectors. Before that, the signed initial was a third-party-verifiable proof that A had contacted B, held by the server for the store-and-forward window; that was the accepted price of post-quantum initiator authentication, and the sealed form removes it for every current build. The server still sees the from/to edge of a first contact. The sealed initial hides the proof, not the fact that two mailboxes met.

✓Evidence
  • C1-A, post-quantum initiator authentication: implemented and tested; the formal proof of the current construction is open.
  • C1-M, matching-honest-session confidentiality: conditional on named assumptions (a hidden-input KEM-combiner premise), with a corrected symbolic model; the concrete reduction is incomplete.
  • C8, downgrade boundaries: implemented and tested; a transparency mechanism is still needed to prove the broader claim.
✕Refuted and open
  • The original "hybrid handshake is secure if either component holds against an active attacker" claim was refuted by a reproduced trace and is withdrawn.
  • On a very first lookup a malicious directory can serve a complete, internally valid attacker bundle. Safety-number comparison detects it; there is no key transparency.

The Double Ratchet and its post-quantum lanes

Messages ride a Double Ratchet: every message has its own derived key, out-of-order messages can be recovered within bounded skipped-key storage (maxSkip=256, ≤768 keys in total, oldest evicted first), and every decrypt is an atomic trial-commit: the message is decrypted against a copy of the session and the copy is swapped in only on AEAD success, so a unauthenticated input does not commit that trial state. This does not prove every recovery, authenticated-peer or persistence path. Plaintext is padded to 512-byte buckets before encryption.

Beyond setup, the ratchet keeps mixing fresh post-quantum secrets into the root. On current (av=4) sessions every DH sending run carries a fresh ML-KEM-768 public key and an encapsulation to the peer's latest advertised key, inside a protected header box of fixed size (2,627 bytes) that looks identical whether it carries a key, a ciphertext, a chunk, an acknowledgement, or nothing. The box's contents are encrypted and bound into the message's authenticated data, so the server and any wire observer see the same shape on every message. This conceals those fields from an observer without the relevant keys; timing, public ratchet position, compromise and cross-path correlation still matter. A second lane (chunked epochs streamed at ~160 bytes per message) remains for sessions persisted by older builds and remains in current code. It did not provide earlier healing in the examined simulations; its removal is deferred, not a shipped change.

Healing is directional. When a sender folds a fresh KEM secret into the root, its own sending direction heals first; the reverse direction heals only after the peer accepts a message from the mixed run and derives its next sending chain from the new root. Suitable fresh contributions and delivery are necessary; no universal turn count or wall-clock recovery deadline is established. The argument is written up in the directional post-quantum healing note.

Anatomy of a message on the wire

Message envelope
Visible

Public header

Ratchet key, counters, version, salt and commitment.

dhPub · pn · n · av · s · c

Encrypted

Protected header

Post-quantum fields, acknowledgements and dummy content.

2,627 bytes in this format.

Encrypted

Message body

ChaCha20-Poly1305 ciphertext.

Plaintext padded to 512-byte buckets.

Public fields and the protected-header box are bound through authenticated data; the message body is authenticated ciphertext. Fixed header length hides which protected fields are present, subject to the cryptographic assumptions. Timing, public counters and overall size classes remain observable. Box widths are not byte proportions.

Key commitment and fresh nonces

In a textbook Double Ratchet the message key and nonce are deterministic functions of the chain position, so nonce uniqueness is a persistence invariant: if a crash ever restores an earlier ratchet position, the next send re-derives an already-used key and nonce; a two-time pad. A randomized simulation of Auris's own persistence found an ordering in which exactly that could happen, and the fix landed twice over. The retirement rule for durable send marks was tightened so a landed state snapshot can only retire positions it actually covers. And since build 146, between peers on capability 7 or later, every message carries a fresh 16-byte random salt s and a 16-byte commitment tag c:

AEAD-KDF7(msgKey, s):
    okm = HKDF-SHA256(salt = s, IKM = msgKey, info = "AurisMsg/v7", L = 60)
    aeadKey = okm[0..31]   nonce = okm[32..43]   commit = okm[44..59]

The receiver derives its own triple from the message key and the received salt and checks the commitment in constant time before attempting the AEAD open. A re-derived chain position can yield different key/nonce material with a fresh random salt. This reduces deterministic reuse risk after rollback; it does not establish complete rollback security or protect an already stolen message key. The derived commitment is checked and authenticated, but a full committing-AEAD theorem for this composition remains open. The fields add 32 raw bytes before serialization. See the salt-collision calculation.

Per-run keys for the protected header

Through build 146 the protected header box was sealed under two session-stable keys, chosen so a receiver could open the post-quantum material before deriving a new receiving chain. The price was that a later full state compromise opened every recorded box of that session; the ratchet's forward secrecy protected the content but not the post-quantum metadata. Build 147 seals each run's box under a key derived from the root that precedes the run and the run's public ratchet key, which the receiver can compute at the moment the run's first message arrives, and retires stable keys during migration. New capability-9 pairs do not derive stable keys; migrated sessions have a receive grace window. The implementation retains current run material and two superseded receive-run keys. This reduces retained-key exposure; old snapshots, compromise timing and implementation conformance remain separate obligations. Build 147 was archived; later migration fixes in build-148 source must not be assumed present in every client. The box keeps its fixed size.

The capability ladder

Every bundle carries a signed capability statement; a verified version is pinned so a later lookup cannot roll it back, and a bundle advertising a version must carry that version's fields with valid signatures or it is rejected as a downgrade.

CapabilityBuildWhat it addsStatus
6128Historical floor: signed capabilities and initiator transcript (pv4)Historical release
7146Sealed initial; per-message salt and key commitmentShipped
8146Handshake KEM raised to ML-KEM-1024; the current floorShipped
9147Per-run protected-header keysReleased beta; capability-9 path required

Emission of a new shape is gated on the peer's authenticated capability; seeded from the verified bundle or latched after a successful decrypt. Reception validates supported shapes and their authenticated fields. Compatibility handling is not acceptance of arbitrary versions or a proof against desynchronization.

Man-in-the-middle defence

The safety number covers both parties' identity, Ed25519 and ML-DSA keys: 5,200 rounds of SHA-512 over a domain-separated canonical encoding, then rejection-sampled unbiased digits. A change to any long-term key changes the number. Once a peer has advertised a post-quantum signing key, a later bundle without one is flagged like an identity change. Stripping the key also breaks the Ed25519 prekey signature that covers it, so the strip is caught either way. A "hybrid PQ session" badge states that ML-KEM contributed to the session; it deliberately says nothing about who is on the other end. Calls additionally verify the peer's DTLS fingerprint over the ratchet.

Session recovery, without weakening the ratchet

If one side loses its half of a session, the pair heals by re-handshaking. Only a fully valid initial (X3DH completes, the first message's AEAD verifies, the identity pin holds) can archive and replace a live session, and all of it is checked on a copy before any state changes. Every session-establishing initial is remembered by its ephemeral key, so a captured initial can never be replayed to reset a session to old key material. A superseded session stays decrypt-only for at most ten minutes (two, once the replacement carries traffic) so late messages still open; archived traffic can carry ordinary content but cannot change retention, replace routing capabilities, or touch call-security state.

Accepted weaknesses, so nobody has to discover them

  • The initiator's first chain has no post-quantum injection (no peer ratchet key exists yet) so PQ healing of that direction starts one round trip in. The handshake KEM secret is already in the root.
  • Retained keys are bounded, not zero. Up to 768 skipped message keys, five superseded KEM decapsulation keys and four decapsulated epoch secrets persist encrypted at rest; a device-state compromise decrypts exactly the out-of-order messages they cover.
  • No deniability claim. Prekey signatures are non-repudiable statements about prekeys, and no offline-deniability argument is made for message authorship.
  • Older sessions keep older properties. A session persisted by an earlier build stays on its format until the pair re-handshakes; the server can still read the post-quantum fields of such a session and could suppress its continuous healing while leaving the periodic floor.
  • The signed initial is non-repudiable toward the server for responders below capability 7. the sealed form closes this for every current build.
  • The fixed protected header costs bandwidth: about 4.7 KB per message after encoding, roughly 34 MB per hour per direction at two messages a second. Accepted for the metadata it hides.
  • Swift cannot promise deterministic erasure of every secret copy in process memory; a live memory compromise is outside the claims.

Every claim on this page has a row in the claims ledger with its evidence status. The full specification, written for external analysis, is the project's CRYPTO_SPEC.md.

Reviewed · Review scope and versions