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:
151
docs/src/wire-format/envelope-schema.md
Normal file
151
docs/src/wire-format/envelope-schema.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Envelope Schema
|
||||
|
||||
**Schema file:** `schemas/envelope.capnp`
|
||||
**File ID:** `@0xe4a7f2c8b1d63509`
|
||||
|
||||
The Envelope is the legacy top-level wire message used in M1 for all quicnprotochat traffic over the Noise channel. Every frame exchanged between peers was serialised as an Envelope, with the Delivery Service routing by `(groupId, msgType)` without inspecting the payload.
|
||||
|
||||
> **Note:** The Envelope is the M1-era framing format. The current M3+ architecture uses Cap'n Proto RPC directly via the [NodeService](node-service-schema.md) interface. The Envelope schema remains in the codebase for backward compatibility and for use in integration tests that exercise the Noise transport path.
|
||||
|
||||
---
|
||||
|
||||
## Full schema listing
|
||||
|
||||
```capnp
|
||||
# envelope.capnp -- top-level wire message for all quicnprotochat traffic.
|
||||
#
|
||||
# Every frame exchanged over the Noise channel is serialised as an Envelope.
|
||||
# The Delivery Service routes by (groupId, msgType) without inspecting payload.
|
||||
#
|
||||
# Field sizing rationale:
|
||||
# groupId / senderId : 32 bytes -- SHA-256 digest
|
||||
# payload : opaque -- MLS blob or control data; size bounded by
|
||||
# the Noise transport max message size (65535 B)
|
||||
# timestampMs : UInt64 -- unix epoch milliseconds; sufficient until year 292M
|
||||
#
|
||||
# ID generated with: capnp id
|
||||
@0xe4a7f2c8b1d63509;
|
||||
|
||||
struct Envelope {
|
||||
# Message type discriminant -- determines how payload is interpreted.
|
||||
msgType @0 :MsgType;
|
||||
|
||||
# 32-byte SHA-256 digest of the group name.
|
||||
# The Delivery Service uses this as its routing key.
|
||||
# Zero-filled for point-to-point control messages (ping, keyPackageUpload, etc.).
|
||||
groupId @1 :Data;
|
||||
|
||||
# 32-byte SHA-256 digest of the sender's Ed25519 identity public key.
|
||||
senderId @2 :Data;
|
||||
|
||||
# Opaque payload. Interpretation is determined by msgType.
|
||||
payload @3 :Data;
|
||||
|
||||
# Unix timestamp in milliseconds at the time of send.
|
||||
timestampMs @4 :UInt64;
|
||||
|
||||
enum MsgType {
|
||||
ping @0;
|
||||
pong @1;
|
||||
keyPackageUpload @2;
|
||||
keyPackageFetch @3;
|
||||
keyPackageResponse @4;
|
||||
mlsWelcome @5;
|
||||
mlsCommit @6;
|
||||
mlsApplication @7;
|
||||
error @8;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field-by-field analysis
|
||||
|
||||
### `msgType @0 :MsgType`
|
||||
|
||||
A 16-bit enum discriminant (Cap'n Proto enums are encoded as UInt16). Determines how the `payload` field should be interpreted. The discriminant is the first field in the struct for efficient dispatch: a router can read the first two bytes of the struct section to decide how to handle the message without parsing any pointer fields.
|
||||
|
||||
### `groupId @1 :Data`
|
||||
|
||||
A 32-byte `Data` field containing the SHA-256 digest of the group name. The Delivery Service uses this as its primary routing key when the Envelope-based protocol is active.
|
||||
|
||||
**Sizing rationale:** SHA-256 produces a 32-byte (256-bit) digest. This is stored as a variable-length `Data` field rather than a fixed-size blob because Cap'n Proto does not have a fixed-size array type. Implementations must validate that the field contains exactly 32 bytes.
|
||||
|
||||
**Special case:** For point-to-point control messages (`ping`, `pong`, `keyPackageUpload`, `keyPackageFetch`), the `groupId` is zero-filled (32 zero bytes) because these messages are not associated with any group.
|
||||
|
||||
### `senderId @2 :Data`
|
||||
|
||||
A 32-byte `Data` field containing the SHA-256 digest of the sender's Ed25519 identity public key. This allows the receiver to identify the sender without inspecting the MLS-layer credentials.
|
||||
|
||||
**Sizing rationale:** Same as `groupId` -- SHA-256 digest, 32 bytes.
|
||||
|
||||
### `payload @3 :Data`
|
||||
|
||||
An opaque byte string whose interpretation depends on `msgType`. The payload is bounded by the Noise transport maximum message size of 65,535 bytes (see [Framing Codec](framing-codec.md)).
|
||||
|
||||
### `timestampMs @4 :UInt64`
|
||||
|
||||
Unix epoch timestamp in milliseconds, set by the sender at the time of send. Encoded as a `UInt64`, which provides sufficient range until approximately year 292,000,000 -- effectively unlimited for practical purposes.
|
||||
|
||||
The timestamp is sender-asserted and **not** authenticated by the server. Receivers should treat it as advisory (for display ordering) rather than authoritative.
|
||||
|
||||
---
|
||||
|
||||
## MsgType enum
|
||||
|
||||
The `MsgType` enum defines nine message types. Each variant determines how the `payload` field is interpreted:
|
||||
|
||||
| Ordinal | Variant | Payload Contents | Direction |
|
||||
|---|---|---|---|
|
||||
| 0 | `ping` | Empty | Client -> Server or Peer -> Peer |
|
||||
| 1 | `pong` | Empty | Server -> Client or Peer -> Peer |
|
||||
| 2 | `keyPackageUpload` | openmls-serialised KeyPackage blob (TLS encoding) | Client -> Server |
|
||||
| 3 | `keyPackageFetch` | Target identity key (32 bytes, raw Ed25519 public key) | Client -> Server |
|
||||
| 4 | `keyPackageResponse` | openmls-serialised KeyPackage blob, or empty if none stored | Server -> Client |
|
||||
| 5 | `mlsWelcome` | `MLSMessage` blob (Welcome variant) | Peer -> Peer (via DS) |
|
||||
| 6 | `mlsCommit` | `MLSMessage` blob (PublicMessage / Commit variant) | Peer -> Group (via DS) |
|
||||
| 7 | `mlsApplication` | `MLSMessage` blob (PrivateMessage / Application variant) | Peer -> Group (via DS) |
|
||||
| 8 | `error` | UTF-8 error description string | Any direction |
|
||||
|
||||
### Control messages (0-1)
|
||||
|
||||
`ping` and `pong` are keepalive probes with empty payloads. They serve as health checks over long-lived Noise connections.
|
||||
|
||||
### Authentication messages (2-4)
|
||||
|
||||
`keyPackageUpload`, `keyPackageFetch`, and `keyPackageResponse` implement the Authentication Service protocol over the Envelope format. In the current architecture, these operations are handled by the [NodeService RPC](node-service-schema.md) methods `uploadKeyPackage` and `fetchKeyPackage` instead.
|
||||
|
||||
### MLS messages (5-7)
|
||||
|
||||
`mlsWelcome`, `mlsCommit`, and `mlsApplication` carry MLS protocol messages as opaque blobs. The Envelope does not inspect or validate the MLS content; it simply transports the bytes between peers via the Delivery Service.
|
||||
|
||||
### Error messages (8)
|
||||
|
||||
`error` carries a UTF-8 string describing an error condition. Used for protocol-level error reporting (e.g., "no KeyPackage found for identity").
|
||||
|
||||
---
|
||||
|
||||
## Relationship to NodeService
|
||||
|
||||
The Envelope schema was the original M1 wire format, where all communication was multiplexed over a single Noise-encrypted TCP stream. With the transition to QUIC + TLS 1.3 and Cap'n Proto RPC in M3, the Envelope's role has been superseded by the [NodeService interface](node-service-schema.md), which provides typed RPC methods for each operation.
|
||||
|
||||
The key differences:
|
||||
|
||||
| Aspect | Envelope (M1) | NodeService RPC (M3+) |
|
||||
|---|---|---|
|
||||
| Dispatch | Manual, based on `msgType` enum | Automatic, Cap'n Proto RPC method dispatch |
|
||||
| Type safety | Payload is opaque `Data` | Each method has typed parameters and return values |
|
||||
| Transport | Noise\_XX over TCP | QUIC + TLS 1.3 |
|
||||
| Auth | Implicit (Noise handshake authenticates peers) | Explicit `Auth` struct per method call |
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Wire Format Overview](overview.md) -- serialisation pipeline context
|
||||
- [NodeService Schema](node-service-schema.md) -- the current RPC interface that replaced Envelope-based dispatch
|
||||
- [Auth Schema](auth-schema.md) -- standalone Authentication Service interface
|
||||
- [Delivery Schema](delivery-schema.md) -- standalone Delivery Service interface
|
||||
- [Framing Codec](framing-codec.md) -- length-prefixed framing that wraps serialised Envelopes
|
||||
- [ADR-002: Cap'n Proto over MessagePack](../design-rationale/adr-002-capnproto.md) -- why Cap'n Proto was chosen for the wire format
|
||||
Reference in New Issue
Block a user