# Cryptographic Boundaries

This document catalogs every cryptographic primitive used in quicprochat, its purpose, the implementing crate, key lifecycle, and relevant security properties.

## Cryptographic Primitives

### 1. X25519 (Elliptic Curve Diffie-Hellman)

| Property | Value |
|----------|-------|
| **Purpose** | Key exchange in Noise_XX handshake, classical component of hybrid KEM, MLS HPKE baseline |
| **Crate** | `x25519-dalek 2.x` (features: `static_secrets`) |
| **Security level** | ~128-bit classical |
| **Source files** | `quicprochat-core/src/hybrid_kem.rs`, `quicprochat-core/src/pq_noise.rs` |

Uses `StaticSecret` for long-term keys and `EphemeralSecret` for one-time key exchange. In the hybrid KEM, the X25519 shared secret is combined with the ML-KEM-768 shared secret via HKDF.

### 2. Ed25519 (Digital Signatures)

| Property | Value |
|----------|-------|
| **Purpose** | Identity keys, MLS credential signing, sealed sender signatures, delivery proofs, Key Transparency leaf binding |
| **Crate** | `ed25519-dalek 2.x` (features: `rand_core`) |
| **Security level** | ~128-bit classical |
| **Source files** | `quicprochat-core/src/identity.rs`, `quicprochat-core/src/sealed_sender.rs` |

The `IdentityKeypair` stores the 32-byte private seed in `Zeroizing<[u8; 32]>`. Implements the `openmls_traits::signatures::Signer` trait for MLS integration.

### 3. ML-KEM-768 (Post-Quantum Key Encapsulation)

| Property | Value |
|----------|-------|
| **Purpose** | Post-quantum component of hybrid KEM (HPKE init keys in MLS), PQ Noise handshake |
| **Crate** | `ml-kem 0.2` (FIPS 203) |
| **Security level** | NIST Level 3 (~192-bit post-quantum) |
| **Key sizes** | Encapsulation key: 1184 bytes, Decapsulation key: 2400 bytes, Ciphertext: 1088 bytes |
| **Source files** | `quicprochat-core/src/hybrid_kem.rs`, `quicprochat-core/src/pq_noise.rs` |

ML-KEM-768 uses implicit rejection: decapsulation with a wrong ciphertext returns a pseudorandom shared secret rather than an error, which is detected downstream by AEAD failure.

### 4. ChaCha20-Poly1305 (AEAD Encryption)

| Property | Value |
|----------|-------|
| **Purpose** | Hybrid KEM envelope encryption, PQ Noise transport encryption, transcript archive encryption, recovery bundle encryption |
| **Crate** | `chacha20poly1305 0.10` |
| **Security level** | 256-bit key, 128-bit authentication tag |
| **Nonce** | 96-bit (12 bytes). Random in hybrid KEM; counter-based in PQ Noise transport |
| **Source files** | `quicprochat-core/src/hybrid_kem.rs`, `quicprochat-core/src/pq_noise.rs`, `quicprochat-core/src/transcript.rs`, `quicprochat-core/src/recovery.rs` |

In the hybrid KEM, the nonce is generated randomly per encryption (not derived from HKDF), preventing nonce reuse when the same shared secret is accidentally used more than once. In PQ Noise, nonces are monotonic counters (`u64` in the low 8 bytes of a 12-byte nonce).

### 5. HKDF-SHA256 (Key Derivation)

| Property | Value |
|----------|-------|
| **Purpose** | Deriving AEAD keys from combined X25519 + ML-KEM shared secrets, PQ Noise chaining key evolution, hybrid HPKE key schedule |
| **Crate** | `hkdf 0.12` with `sha2 0.10` |
| **Source files** | `quicprochat-core/src/hybrid_kem.rs`, `quicprochat-core/src/pq_noise.rs` |

Domain separation labels:
- Hybrid KEM: `info = "quicnprotochat-hybrid-v1"`, `salt = "quicnprotochat-hybrid-v1-salt"`
- Hybrid HPKE keypair derivation: `info = "quicnprotochat-hybrid-hpke-keypair-v1"`
- PQ Noise: `info = "quicprochat-pq-noise-v1"` (protocol name used as initial hash)
- PQ Noise split: `info = "initiator"` / `info = "responder"` for transport key derivation

### 6. Argon2id (Password Hashing / Key Stretching)

| Property | Value |
|----------|-------|
| **Purpose** | OPAQUE key stretching function (KSF), recovery code key derivation, SQLCipher key derivation (client-side) |
| **Crate** | `argon2 0.5` |
| **Parameters** | Memory: 19 MiB (`19 * 1024`), Iterations: 2, Parallelism: 1, Output: 32 bytes |
| **Source files** | `quicprochat-core/src/recovery.rs`, `quicprochat-core/src/opaque_auth.rs` |

Used as the KSF in the OPAQUE cipher suite (`OpaqueSuite`). For recovery codes, derives a 32-byte encryption key from a 6-character alphanumeric code with a random 16-byte salt.

### 7. OPAQUE (Password-Authenticated Key Exchange)

| Property | Value |
|----------|-------|
| **Purpose** | User registration and login without exposing passwords to the server |
| **Crate** | `opaque-ke 4.x` (features: `ristretto255`, `argon2`) |
| **OPRF group** | Ristretto255 (~128-bit security) |
| **Key exchange** | Triple-DH (3DH) over Ristretto255 with SHA-512 |
| **KSF** | Argon2id (see above) |
| **Source files** | `quicprochat-core/src/opaque_auth.rs`, `quicprochat-server/src/domain/auth.rs` |

The `OpaqueSuite` struct defines the cipher suite used by both client and server, ensuring identical parameters.

### 8. MLS (Messaging Layer Security, RFC 9420)

| Property | Value |
|----------|-------|
| **Purpose** | Group key agreement, epoch rotation, forward secrecy, post-compromise security |
| **Crate** | `openmls 0.8`, `openmls_rust_crypto 0.5` |
| **Ciphersuite** | `MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519` |
| **HPKE backend** | `HybridCryptoProvider` (custom `OpenMlsProvider` with hybrid KEM for HPKE operations) |
| **Source files** | `quicprochat-core/src/group.rs`, `quicprochat-core/src/hybrid_crypto.rs`, `quicprochat-core/src/keypackage.rs` |

The `HybridCryptoProvider` detects hybrid keys by length (1216-byte public = hybrid, 32-byte = classical) and routes HPKE operations accordingly. All non-HPKE operations (AEAD, hash, signatures, KDF) delegate to `openmls_rust_crypto::RustCrypto`.

### 9. Noise_XX (Transport Handshake)

| Property | Value |
|----------|-------|
| **Purpose** | Mutual authentication and transport encryption (v1 transport layer) |
| **Crate** | `snow 0.9` (v1), custom implementation in `pq_noise` module (v2 hybrid) |
| **Pattern** | XX (mutual authentication with identity hiding for initiator) |
| **Source files** | `quicprochat-core/src/pq_noise.rs` |

The PQ Noise handshake extends Noise_XX with an ML-KEM-768 encapsulation in message 2. The ML-KEM shared secret is mixed into the chaining key via `mix_key()`, so the final transport keys incorporate both X25519 DH and ML-KEM shared secrets.

### 10. SHA-256 (Hashing)

| Property | Value |
|----------|-------|
| **Purpose** | Identity key fingerprints, Key Transparency Merkle log, HKDF extract, recovery token derivation, transcript hash chains |
| **Crate** | `sha2 0.10` |
| **Source files** | `quicprochat-core/src/identity.rs`, `quicprochat-kt/src/lib.rs`, `quicprochat-core/src/recovery.rs`, `quicprochat-core/src/transcript.rs` |

### 11. HMAC-SHA256

| Property | Value |
|----------|-------|
| **Purpose** | Safety number computation (Signal-style, 5200-iteration key stretching) |
| **Crate** | `hmac 0.12` with `sha2 0.10` |
| **Source files** | `quicprochat-core/src/safety_numbers.rs` |

## Key Lifecycle

### Identity Key (Ed25519)

| Phase | Mechanism |
|-------|-----------|
| **Generation** | `SigningKey::generate(&mut OsRng)` — 32-byte seed from OS CSPRNG |
| **Storage** | Seed stored in `Zeroizing<[u8; 32]>` in memory; persisted via SQLCipher on disk with Argon2id-derived key |
| **Distribution** | Public key (32 bytes) uploaded to server, stored in Key Transparency log |
| **Rotation** | MLS self-update proposal (`propose_self_update()`) |
| **Zeroization** | `Zeroizing<[u8; 32]>` zeroes seed bytes on `Drop` |

### Hybrid KEM Key (X25519 + ML-KEM-768)

| Phase | Mechanism |
|-------|-----------|
| **Generation** | `HybridKeypair::generate()` — X25519 via `StaticSecret::random_from_rng(OsRng)`, ML-KEM-768 via `MlKem768::generate(&mut OsRng)` |
| **Derivation** | `HybridKeypair::derive_from_ikm(ikm)` — deterministic from IKM via HKDF-SHA256 + seeded StdRng (for MLS HPKE key schedule) |
| **Storage** | `HybridKeypairBytes` with `Zeroizing<[u8; 32]>` for X25519 SK and `Zeroizing<Vec<u8>>` for ML-KEM DK; stored in `DiskKeyStore` |
| **Distribution** | `HybridPublicKey` (x25519_pk + mlkem_ek = 1216 bytes) embedded in MLS KeyPackages |
| **Consumption** | Single-use: each KeyPackage is consumed on fetch (ADR-005) |
| **Zeroization** | `Zeroizing` wrappers on all private key material |

### MLS Group Epoch Keys

| Phase | Mechanism |
|-------|-----------|
| **Derivation** | Derived by OpenMLS key schedule from group secrets + commit transcript |
| **Storage** | Held in `DiskKeyStore` (implements `StorageProvider`); flushed to disk on every write |
| **Rotation** | Advanced on every MLS Commit (epoch increment) |
| **Deletion** | Previous epoch keys are deleted by OpenMLS after processing; `DiskKeyStore` flushes deletions to disk |
| **Persistence** | `DiskKeyStore::persistent(path)` stores to file with 0o600 permissions (Unix) |

### OPAQUE Server Record

| Phase | Mechanism |
|-------|-----------|
| **Generation** | `ServerSetup::new(&mut OsRng)` on server startup |
| **Registration** | `ServerRegistration::start()` + `ServerRegistration::finish()` — server never sees plaintext password |
| **Storage** | Serialized OPAQUE record stored via `Store::store_user_record()` |
| **Login** | `ServerLogin::start()` + `ServerLogin::finish()` — session token issued on success |

### Session Tokens

| Phase | Mechanism |
|-------|-----------|
| **Generation** | Random bytes from OsRng on successful OPAQUE login |
| **Storage** | Server-side `DashMap<Vec<u8>, SessionInfo>` (in memory) |
| **Validation** | O(1) lookup; expired tokens are removed on access |
| **Expiry** | Time-bounded (`expires_at` field in `SessionInfo`) |

## Zeroization Audit

The following types implement zeroization of secret material:

| Type | Secret Field | Zeroization Mechanism |
|------|-------------|----------------------|
| `IdentityKeypair` | `seed: Zeroizing<[u8; 32]>` | `Zeroizing` zeroes on `Drop` |
| `HybridKeypairBytes` | `x25519_sk: Zeroizing<[u8; 32]>`, `mlkem_dk: Zeroizing<Vec<u8>>` | `Zeroizing` zeroes on `Drop` |
| `HybridKeypair::private_to_bytes()` | Return value | Returns `Zeroizing<Vec<u8>>` |
| `GroupMember::identity_seed()` | Return value | Returns `Zeroizing<[u8; 32]>` |
| `derive_aead_key()` (hybrid_kem) | `ikm`, `key_bytes` | Both wrapped in `Zeroizing` |
| `HandshakeState` (pq_noise) | `ck`, `k` | `Zeroizing<[u8; 32]>` for chaining key and encryption key |
| `TransportKey` (pq_noise) | `key` | `Zeroizing<[u8; 32]>` |
| `derive_recovery_key()` | Return value | Returns `Zeroizing<[u8; 32]>` |

**Known gap**: `x25519_dalek::StaticSecret` does not implement `Zeroize` in the public API. The `HybridKeypair` struct holds `StaticSecret` directly; its internal representation may not be zeroed on drop. This is mitigated by the `HybridKeypairBytes` serialization layer using `Zeroizing` for persistence, and by the short lifetime of `HybridKeypair` instances.

## Constant-Time Operations

| Location | Operation | Mechanism |
|----------|-----------|-----------|
| `quicprochat-core/src/recovery.rs` | Recovery code token comparison | `constant_time_eq()` — XOR-accumulate, branch-free |
| `ed25519-dalek` internals | Signature verification | Constant-time via `curve25519-dalek` |
| `x25519-dalek` internals | Diffie-Hellman computation | Constant-time via `curve25519-dalek` |
| `opaque-ke` internals | OPRF evaluation, 3DH | Constant-time via Ristretto255 |

## RNG Sources

| Usage | RNG | Crate |
|-------|-----|-------|
| Identity key generation | `OsRng` | `rand::rngs::OsRng` |
| Hybrid KEM key generation | `OsRng` | `rand::rngs::OsRng` |
| Hybrid KEM ephemeral DH | `OsRng` via `EphemeralSecret::random_from_rng(OsRng)` | `x25519-dalek` |
| ML-KEM encapsulation | `OsRng` | `ml-kem` |
| AEAD nonce generation (hybrid KEM) | `OsRng.fill_bytes()` | `rand` |
| PQ Noise ephemeral keys | `OsRng` via `StaticSecret::random_from_rng(OsRng)` | `x25519-dalek` |
| Message padding | `OsRng.fill_bytes()` | `rand` |
| Decoy traffic padding | `OsRng.fill_bytes()` | `rand` |
| Recovery code generation | `OsRng.next_u32()` | `rand` |
| OPAQUE server setup | `OsRng` | `rand` |
| Deterministic key derivation (MLS HPKE) | `StdRng::from_seed(hkdf_output)` | `rand` |

All cryptographic randomness is sourced from `OsRng` (backed by `getrandom`), which reads from the OS CSPRNG (`/dev/urandom` on Linux, `BCryptGenRandom` on Windows). The only exception is deterministic derivation in `HybridKeypair::derive_from_ikm()`, which uses a seeded `StdRng` for reproducible key generation as required by the MLS HPKE key schedule.
