feat: add post-quantum hybrid KEM + SQLCipher persistence
Feature 1 — Post-Quantum Hybrid KEM (X25519 + ML-KEM-768): - Create hybrid_kem.rs with keygen, encrypt, decrypt + 11 unit tests - Wire format: version(1) | x25519_eph_pk(32) | mlkem_ct(1088) | nonce(12) | ct - Add uploadHybridKey/fetchHybridKey RPCs to node.capnp schema - Server: hybrid key storage in FileBackedStore + RPC handlers - Client: hybrid keypair in StoredState, auto-wrap/unwrap in send/recv/invite/join - demo-group runs full hybrid PQ envelope round-trip Feature 2 — SQLCipher Persistence: - Extract Store trait from FileBackedStore API - Create SqlStore (rusqlite + bundled-sqlcipher) with encrypted-at-rest SQLite - Schema: key_packages, deliveries, hybrid_keys tables with indexes - Server CLI: --store-backend=sql, --db-path, --db-key flags - 5 unit tests for SqlStore (FIFO, round-trip, upsert, channel isolation) Also includes: client lib.rs refactor, auth config, TOML config file support, mdBook documentation, and various cleanups by user. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
232
docs/src/architecture/crate-responsibilities.md
Normal file
232
docs/src/architecture/crate-responsibilities.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# Crate Responsibilities
|
||||
|
||||
The quicnprotochat workspace is split into four crates with strict layering
|
||||
rules. Each crate owns one concern and depends only on the crates below it.
|
||||
This page documents what each crate provides, what it explicitly avoids, and
|
||||
how the crates relate to one another.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Flow Diagram
|
||||
|
||||
```text
|
||||
┌──────────────────────────┐
|
||||
│ quicnprotochat-client │
|
||||
│ (CLI, QUIC client, │
|
||||
│ GroupMember orchestr.) │
|
||||
└─────────┬───────┬────────┘
|
||||
│ │
|
||||
┌───────┘ └────────┐
|
||||
▼ ▼
|
||||
┌────────────────────────┐ ┌────────────────────────┐
|
||||
│ quicnprotochat-core │ │ quicnprotochat-server │
|
||||
│ (crypto, Noise, │ │ (QUIC listener, │
|
||||
│ MLS, hybrid KEM) │ │ NodeService RPC, │
|
||||
│ │ │ storage) │
|
||||
└──────────┬─────────────┘ └─────────┬──────────────┘
|
||||
│ │
|
||||
│ ┌───────────────────┘
|
||||
▼ ▼
|
||||
┌────────────────────────┐
|
||||
│ quicnprotochat-proto │
|
||||
│ (Cap'n Proto schemas, │
|
||||
│ codegen, helpers) │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
**Arrows point from dependant to dependency.** The proto crate sits at the base
|
||||
of the dependency graph. The core crate depends on proto for envelope
|
||||
serialisation. The server and client crates both depend on core and proto.
|
||||
|
||||
---
|
||||
|
||||
## quicnprotochat-core
|
||||
|
||||
**Role:** Pure cryptographic logic and transport primitives. No network I/O
|
||||
(except for the Noise handshake helpers that take an existing `TcpStream`). No
|
||||
async runtime dependency beyond what Noise transport needs.
|
||||
|
||||
### Modules
|
||||
|
||||
| Module | Public API | Description |
|
||||
|---------------|-----------------------------------------------------------------------------|-------------|
|
||||
| `keypair` | `NoiseKeypair` | Static X25519 keypair for Noise_XX. `StaticSecret` is `ZeroizeOnDrop`. `private_bytes()` returns `Zeroizing<[u8; 32]>`. |
|
||||
| `identity` | `IdentityKeypair` | Ed25519 signing keypair for MLS credentials. Seed stored as `Zeroizing<[u8; 32]>`. Implements `openmls_traits::Signer`. |
|
||||
| `noise` | `handshake_initiator`, `handshake_responder`, `NoiseTransport` | Noise_XX_25519_ChaChaPoly_BLAKE2s handshake over TCP. `NoiseTransport` provides `send_frame`/`recv_frame`, envelope helpers, and `into_capnp_io()` bridge. |
|
||||
| `codec` | `LengthPrefixedCodec`, `NOISE_MAX_MSG` | Tokio `Encoder<Bytes>` + `Decoder`. 4-byte LE length prefix. Max frame 65,535 bytes. |
|
||||
| `group` | `GroupMember` | MLS group state machine wrapping `openmls::MlsGroup`. Lifecycle: `new` -> `generate_key_package` -> `create_group` / `join_group` -> `send_message` / `receive_message`. |
|
||||
| `keypackage` | `generate_key_package` | Standalone KeyPackage generation (returns TLS-encoded bytes + SHA-256 fingerprint). |
|
||||
| `keystore` | `DiskKeyStore`, `StoreCrypto` | `OpenMlsKeyStore` implementation backed by an in-memory `HashMap` with optional bincode flush to disk. `StoreCrypto` couples `RustCrypto` + `DiskKeyStore` into an `OpenMlsCryptoProvider`. |
|
||||
| `hybrid_kem` | `HybridKeypair`, `HybridPublicKey`, `hybrid_encrypt`, `hybrid_decrypt` | X25519 + ML-KEM-768 hybrid KEM. HKDF-SHA256 key derivation, ChaCha20-Poly1305 AEAD. Versioned envelope wire format. |
|
||||
| `error` | `CoreError`, `CodecError`, `MAX_PLAINTEXT_LEN` | Unified error types. `CoreError` covers Noise, Codec, Cap'n Proto, MLS, and hybrid KEM failures. |
|
||||
|
||||
### What this crate does NOT do
|
||||
|
||||
- No network I/O beyond the Noise helpers (which take a pre-connected `TcpStream`).
|
||||
- No QUIC or TLS -- that is the server and client crates' concern.
|
||||
- No async runtime setup (it uses Tokio types internally but does not spawn or
|
||||
manage a runtime).
|
||||
- No CLI parsing.
|
||||
|
||||
### Key dependencies
|
||||
|
||||
`snow`, `x25519-dalek`, `ed25519-dalek`, `openmls`, `openmls_rust_crypto`,
|
||||
`openmls_traits`, `tls_codec`, `ml-kem`, `chacha20poly1305`, `hkdf`, `sha2`,
|
||||
`zeroize`, `capnp`, `quicnprotochat-proto`, `tokio`, `tokio-util`, `futures`,
|
||||
`bytes`, `serde`, `bincode`, `serde_json`, `thiserror`.
|
||||
|
||||
---
|
||||
|
||||
## quicnprotochat-proto
|
||||
|
||||
**Role:** Cap'n Proto schema definitions, compile-time code generation, and
|
||||
pure-synchronous serialisation helpers. This crate is the single source of truth
|
||||
for the wire format.
|
||||
|
||||
### Contents
|
||||
|
||||
| Item | Description |
|
||||
|---------------------------|-------------|
|
||||
| `schemas/envelope.capnp` | `Envelope` struct and `MsgType` enum -- top-level wire message for Noise-channel traffic. |
|
||||
| `schemas/auth.capnp` | `AuthenticationService` interface -- `uploadKeyPackage`, `fetchKeyPackage`. |
|
||||
| `schemas/delivery.capnp` | `DeliveryService` interface -- `enqueue`, `fetch`. |
|
||||
| `schemas/node.capnp` | `NodeService` interface (unified AS+DS) -- all RPC methods plus `Auth` struct. |
|
||||
| `build.rs` | Invokes `capnpc` to generate Rust types from the four `.capnp` files. |
|
||||
| `lib.rs` | `pub mod envelope_capnp`, `auth_capnp`, `delivery_capnp`, `node_capnp` -- re-exports generated modules. |
|
||||
| `MsgType` | Re-exported enum from `envelope_capnp::envelope::MsgType`. |
|
||||
| `ParsedEnvelope` | Owned, `Send + 'static` representation of a decoded `Envelope`. All byte fields are eagerly copied out of the Cap'n Proto reader. |
|
||||
| `build_envelope` | Serialise a `ParsedEnvelope` to unpacked Cap'n Proto wire bytes. |
|
||||
| `parse_envelope` | Deserialise wire bytes into a `ParsedEnvelope`. |
|
||||
| `to_bytes` / `from_bytes` | Low-level Cap'n Proto message <-> byte conversions. |
|
||||
|
||||
### What this crate does NOT do
|
||||
|
||||
- **No crypto** -- key material never enters this crate.
|
||||
- **No I/O** -- callers own the transport; this crate only converts bytes to
|
||||
types and back.
|
||||
- **No async** -- pure synchronous data-layer code.
|
||||
|
||||
### Key dependencies
|
||||
|
||||
`capnp` (runtime), `capnpc` (build-time only).
|
||||
|
||||
---
|
||||
|
||||
## quicnprotochat-server
|
||||
|
||||
**Role:** Network-facing server binary. Accepts QUIC + TLS 1.3 connections,
|
||||
dispatches Cap'n Proto RPC calls to `NodeServiceImpl`, and persists state to
|
||||
disk via `FileBackedStore`.
|
||||
|
||||
### Components
|
||||
|
||||
| Component | Description |
|
||||
|----------------------|-------------|
|
||||
| `NodeServiceImpl` | Implements `node_service::Server` (Cap'n Proto generated trait). Handles all eight RPC methods: `uploadKeyPackage`, `fetchKeyPackage`, `enqueue`, `fetch`, `fetchWait`, `health`, `uploadHybridKey`, `fetchHybridKey`. |
|
||||
| `FileBackedStore` | Mutex-guarded `HashMap`s for KeyPackages (keyed by Ed25519 public key), delivery queues (keyed by `ChannelKey = (channelId, recipientKey)`), and hybrid public keys. Each mutation flushes the full map to a bincode file on disk. |
|
||||
| `DashMap` waiters | `DashMap<Vec<u8>, Arc<Notify>>` -- per-recipient `tokio::sync::Notify` instances for `fetchWait` long-polling. `enqueue` calls `notify_waiters()` after appending. |
|
||||
| TLS config | Self-signed certificate auto-generated on first run (`rcgen`). TLS 1.3 only, ALPN `capnp`. |
|
||||
| CLI (`clap`) | `--listen` (default `0.0.0.0:7000`), `--data-dir`, `--tls-cert`, `--tls-key`. |
|
||||
|
||||
### Connection lifecycle
|
||||
|
||||
```text
|
||||
QUIC accept
|
||||
└─ TLS 1.3 handshake (self-signed cert, ALPN "capnp")
|
||||
└─ accept_bi() -> bidirectional QUIC stream
|
||||
└─ tokio_util::compat adapters (AsyncRead/AsyncWrite)
|
||||
└─ capnp-rpc twoparty::VatNetwork (Side::Server)
|
||||
└─ RpcSystem drives NodeServiceImpl
|
||||
```
|
||||
|
||||
Because `capnp-rpc` uses `Rc<RefCell<>>` internally and is therefore `!Send`,
|
||||
the entire RPC stack runs on a `tokio::task::LocalSet`. Each incoming connection
|
||||
is handled by `spawn_local`.
|
||||
|
||||
### What this crate does NOT do
|
||||
|
||||
- No direct crypto operations (it delegates to `quicnprotochat-core` types
|
||||
for fingerprinting and storage only).
|
||||
- No MLS processing -- all payloads are opaque byte strings.
|
||||
- No Noise transport (QUIC/TLS only).
|
||||
|
||||
### Key dependencies
|
||||
|
||||
`quicnprotochat-core`, `quicnprotochat-proto`, `quinn`, `quinn-proto`,
|
||||
`rustls`, `rcgen`, `capnp`, `capnp-rpc`, `tokio`, `tokio-util`, `dashmap`,
|
||||
`sha2`, `clap`, `tracing`, `anyhow`, `thiserror`, `bincode`, `serde`.
|
||||
|
||||
---
|
||||
|
||||
## quicnprotochat-client
|
||||
|
||||
**Role:** CLI client binary. Connects to the server over QUIC + TLS 1.3,
|
||||
orchestrates MLS group operations via `GroupMember`, and persists identity and
|
||||
group state to disk.
|
||||
|
||||
### Components
|
||||
|
||||
| Component | Description |
|
||||
|-------------------------|-------------|
|
||||
| `connect_node` | Establishes a QUIC/TLS connection, opens a bidirectional stream, and bootstraps a `capnp-rpc` `RpcSystem` to obtain a `node_service::Client`. |
|
||||
| CLI subcommands (`clap`)| `ping`, `register`, `fetch-key`, `demo-group`, `register-state`, `create-group`, `invite`, `join`, `send`, `recv`. |
|
||||
| `GroupMember` usage | The client creates a `GroupMember` (from `quicnprotochat-core`), calls `generate_key_package` / `create_group` / `add_member` / `join_group` / `send_message` / `receive_message`. |
|
||||
| State persistence | `StoredState` holds `identity_seed` (32 bytes) and optional serialised `MlsGroup`. A companion `.ks` file stores the `DiskKeyStore` with HPKE init private keys. |
|
||||
| Auth context | `ClientAuth` bundles an optional bearer token and device ID. Passed to every RPC via the `Auth` struct in `node.capnp`. |
|
||||
|
||||
### CLI subcommand summary
|
||||
|
||||
| Subcommand | What it does |
|
||||
|-------------------|--------------|
|
||||
| `ping` | Call `health()` and print RTT. |
|
||||
| `register` | Generate a fresh identity + KeyPackage, upload to AS, print identity key. |
|
||||
| `register-state` | Same as `register` but uses/creates persistent state file. |
|
||||
| `fetch-key` | Fetch a peer's KeyPackage by hex identity key. |
|
||||
| `create-group` | Create a new MLS group and save state. |
|
||||
| `invite` | Fetch peer's KeyPackage, add to group, enqueue Welcome via DS. |
|
||||
| `join` | Fetch Welcome from DS, join the MLS group. |
|
||||
| `send` | Encrypt a message with MLS, enqueue via DS. |
|
||||
| `recv` | Fetch pending payloads from DS, decrypt with MLS. Supports `--stream` for continuous long-polling. |
|
||||
| `demo-group` | End-to-end Alice+Bob round-trip (ephemeral identities). |
|
||||
|
||||
### What this crate does NOT do
|
||||
|
||||
- No server-side logic.
|
||||
- No Noise transport (QUIC/TLS only for server communication).
|
||||
- No direct crypto beyond calling `GroupMember` and verifying SHA-256
|
||||
fingerprints.
|
||||
|
||||
### Key dependencies
|
||||
|
||||
`quicnprotochat-core`, `quicnprotochat-proto`, `quinn`, `quinn-proto`,
|
||||
`rustls`, `capnp`, `capnp-rpc`, `tokio`, `tokio-util`, `clap`, `sha2`,
|
||||
`serde`, `bincode`, `anyhow`, `thiserror`, `tracing`.
|
||||
|
||||
---
|
||||
|
||||
## Layering Rules
|
||||
|
||||
1. **proto** depends on nothing in-workspace. It is pure data definition.
|
||||
2. **core** depends on **proto** (for `ParsedEnvelope` and envelope helpers).
|
||||
It does not depend on server or client.
|
||||
3. **server** depends on **core** and **proto**. It does not depend on client.
|
||||
4. **client** depends on **core** and **proto**. It does not depend on server.
|
||||
5. **server** and **client** never depend on each other. They communicate
|
||||
exclusively via the Cap'n Proto RPC wire protocol.
|
||||
|
||||
This layering ensures that:
|
||||
|
||||
- Crypto code can be tested in isolation (`cargo test -p quicnprotochat-core`).
|
||||
- Schema changes propagate automatically through codegen.
|
||||
- The server binary contains no client-side MLS orchestration logic.
|
||||
- The client binary contains no server-side storage or listener logic.
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Architecture Overview](overview.md) -- high-level system diagram
|
||||
- [Service Architecture](service-architecture.md) -- NodeService RPC details
|
||||
- [Wire Format Overview](../wire-format/overview.md) -- Cap'n Proto schema reference
|
||||
- [GroupMember Lifecycle](../internals/group-member-lifecycle.md) -- MLS state machine details
|
||||
- [Storage Backend](../internals/storage-backend.md) -- FileBackedStore internals
|
||||
Reference in New Issue
Block a user