diff --git a/docs/sdk/build-your-own.md b/docs/sdk/build-your-own.md new file mode 100644 index 0000000..9a117bf --- /dev/null +++ b/docs/sdk/build-your-own.md @@ -0,0 +1,166 @@ +# Build Your Own SDK + +This guide explains how to implement a quicproquo client SDK in any language. + +## Two Approaches + +### Approach 1: C FFI Wrapper (recommended) + +The simplest path. Wrap `libquicproquo_ffi` using your language's FFI mechanism (e.g., Python CFFI, Ruby FFI, JNI, Swift C interop). + +**Pros**: Full Rust crypto stack (MLS, OPAQUE, hybrid KEM) with zero reimplementation. +**Cons**: Requires shipping a native library, synchronous API only. + +See [C FFI docs](c-ffi.md) for the function signatures. + +### Approach 2: Native QUIC + Protobuf + +Implement the QUIC transport and protobuf serialization natively in your language. + +**Pros**: No Rust dependency, full async support, smaller binary. +**Cons**: Must handle crypto externally (OPAQUE, MLS key packages). + +## Native Implementation Checklist + +### 1. QUIC Transport + +Open a QUIC connection to the server: + +- **ALPN**: `qpq` +- **TLS**: 1.3 with server certificate verification +- **Port**: 5001 (default) + +Libraries by language: +| Language | Library | +|---|---| +| Go | `github.com/quic-go/quic-go` | +| Python | `aioquic` | +| Java/Kotlin | `netty-quic` or `Kwik` | +| Swift | `Network.framework` (built-in) | +| C/C++ | `quiche`, `msquic`, or `ngtcp2` | +| Rust | `quinn` | + +### 2. Wire Framing + +Each RPC uses a dedicated QUIC bidirectional stream with a 10-byte header: + +``` +[method_id:u16][req_id:u32][payload_len:u32][protobuf bytes] +``` + +All integers are **big-endian** (network byte order). + +Implementation: + +```python +# Encode +header = struct.pack("!HII", method_id, req_id, len(payload)) +frame = header + payload + +# Decode +method_id, req_id, length = struct.unpack("!HII", data[:10]) +payload = data[10 : 10 + length] +``` + +### 3. Protobuf Messages + +Generate or hand-write protobuf encode/decode for the message types in `proto/qpq/v1/*.proto`. + +Minimum required messages for a basic client: + +| Proto file | Messages | +|---|---| +| `auth.proto` | `OpaqueLoginStart{Request,Response}`, `OpaqueLoginFinish{Request,Response}` | +| `delivery.proto` | `EnqueueRequest`, `EnqueueResponse`, `FetchRequest`, `FetchResponse` | +| `user.proto` | `ResolveUserRequest`, `ResolveUserResponse` | +| `channel.proto` | `CreateChannelRequest`, `CreateChannelResponse` | +| `p2p.proto` | `HealthRequest`, `HealthResponse` | +| `common.proto` | `DeleteAccountRequest`, `DeleteAccountResponse` | + +### 4. RPC Flow + +``` +Client Server + │ │ + │ 1. Open QUIC bidirectional stream │ + │ 2. Send framed request │ + │ 3. Close send side (end-stream) │ + │ ──────────────────────────────────►│ + │ │ + │ 4. Read framed response │ + │ 5. Close receive side │ + │ ◄──────────────────────────────────│ +``` + +### 5. Implementation Checklist + +Core operations (implement in order): + +- [ ] **Connect**: Open QUIC connection with TLS 1.3 + ALPN `qpq` +- [ ] **Health**: Send `HealthRequest` (method 802), verify server is running +- [ ] **Register**: OPAQUE registration (methods 100-101) +- [ ] **Login**: OPAQUE login (methods 102-103), store session token +- [ ] **Upload Key Package**: Upload MLS key package (method 300) +- [ ] **Resolve User**: Look up peer's identity key (method 500) +- [ ] **Create Channel**: Create DM channel with peer (method 400) +- [ ] **Send**: Enqueue encrypted message (method 200) +- [ ] **Receive**: Fetch pending messages (method 201 or 202 for long-poll) +- [ ] **Ack**: Acknowledge received messages (method 204) +- [ ] **Disconnect**: Close QUIC connection gracefully + +### 6. OPAQUE Authentication + +OPAQUE is a 4-message protocol. You need an OPAQUE library for your language: + +| Language | Library | +|---|---| +| Rust | `opaque-ke` | +| Go | `github.com/cloudflare/circl/opaque` | +| Python | `opaque` (pure Python) or bind to Rust via FFI | +| JavaScript | `@nicholasgasior/opaque-wasm` or FFI | + +The SDK only needs to transport the OPAQUE bytes -- the crypto is handled by the OPAQUE library. The flow is: + +1. Generate `RegistrationRequest` locally +2. Send via `OpaqueRegisterStart` (method 100) +3. Process server's `RegistrationResponse` locally to produce `Upload` +4. Send via `OpaqueRegisterFinish` (method 101) +5. Generate `LoginRequest` locally +6. Send via `OpaqueLoginStart` (method 102) +7. Process server's `LoginResponse` locally to produce `Finalization` +8. Send via `OpaqueLoginFinish` (method 103) +9. Store the returned `session_token` + +### 7. Error Handling + +The server returns protobuf-encoded error responses. Your SDK should: + +- Distinguish authentication errors from transport errors +- Implement timeouts on all RPC calls +- Handle QUIC connection loss and reconnection + +## Reference Implementations + +Study these SDKs for patterns: + +- **Go** (`sdks/go/`): Native QUIC + Cap'n Proto, full OPAQUE flow +- **Python** (`sdks/python/`): Native QUIC + Protobuf v2 wire format +- **TypeScript** (`sdks/typescript/`): WebSocket bridge, WASM crypto +- **C FFI** (`crates/quicproquo-ffi/`): Synchronous wrapper + +## Testing + +Test your SDK against a local server: + +```sh +# Start the server +cargo run -p quicproquo-server + +# Run your SDK's tests +``` + +Verify at minimum: +1. Health check returns status "ok" +2. OPAQUE register + login succeeds +3. Send + receive roundtrip works +4. Error handling for invalid credentials diff --git a/docs/sdk/c-ffi.md b/docs/sdk/c-ffi.md new file mode 100644 index 0000000..651469d --- /dev/null +++ b/docs/sdk/c-ffi.md @@ -0,0 +1,142 @@ +# C FFI Bindings + +The C FFI layer (`crates/quicproquo-ffi/`) provides synchronous C-callable functions that wrap the Rust client library. This is the foundation for language bindings in Python (CFFI), Swift, Kotlin/JNI, Java, and Ruby. + +## Building + +```sh +cargo build --release -p quicproquo-ffi +``` + +This produces: +- Linux: `target/release/libquicproquo_ffi.so` +- macOS: `target/release/libquicproquo_ffi.dylib` +- Windows: `target/release/quicproquo_ffi.dll` + +## API + +### Status Codes + +```c +#define QPQ_OK 0 +#define QPQ_ERROR 1 +#define QPQ_AUTH_FAILED 2 +#define QPQ_TIMEOUT 3 +#define QPQ_NOT_CONNECTED 4 +``` + +### Functions + +```c +// Connect to a server. Returns opaque handle or NULL on failure. +QpqHandle* qpq_connect( + const char* server, // "host:port" + const char* ca_cert, // path to CA certificate PEM + const char* server_name // TLS SNI name +); + +// Authenticate with OPAQUE. Returns status code. +int qpq_login( + QpqHandle* handle, + const char* username, + const char* password +); + +// Send a message to a recipient (by username). +int qpq_send( + QpqHandle* handle, + const char* recipient, // recipient username + const uint8_t* message, // message bytes + size_t message_len +); + +// Receive pending messages. Blocks up to timeout_ms. +// On success, *out_json is a JSON array of strings. +int qpq_receive( + QpqHandle* handle, + uint32_t timeout_ms, + char** out_json // caller must free with qpq_free_string +); + +// Disconnect and free the handle. +void qpq_disconnect(QpqHandle* handle); + +// Get last error message (valid until next FFI call on this handle). +const char* qpq_last_error(const QpqHandle* handle); + +// Free a string returned by qpq_receive. +void qpq_free_string(char* ptr); +``` + +## Usage Example (C) + +```c +#include +#include + +// Forward declarations (or include a header). +typedef struct QpqHandle QpqHandle; +extern QpqHandle* qpq_connect(const char*, const char*, const char*); +extern int qpq_login(QpqHandle*, const char*, const char*); +extern int qpq_send(QpqHandle*, const char*, const unsigned char*, size_t); +extern int qpq_receive(QpqHandle*, unsigned int, char**); +extern void qpq_disconnect(QpqHandle*); +extern const char* qpq_last_error(const QpqHandle*); +extern void qpq_free_string(char*); + +int main(void) { + QpqHandle* h = qpq_connect("127.0.0.1:5001", "ca.pem", "localhost"); + if (!h) { + fprintf(stderr, "connect failed\n"); + return 1; + } + + int rc = qpq_login(h, "alice", "password123"); + if (rc != 0) { + fprintf(stderr, "login failed: %s\n", qpq_last_error(h)); + qpq_disconnect(h); + return 1; + } + + const char* msg = "hello from C!"; + qpq_send(h, "bob", (const unsigned char*)msg, strlen(msg)); + + char* json = NULL; + rc = qpq_receive(h, 5000, &json); + if (rc == 0 && json) { + printf("received: %s\n", json); + qpq_free_string(json); + } + + qpq_disconnect(h); + return 0; +} +``` + +Compile with: +```sh +gcc -o demo demo.c -L target/release -lquicproquo_ffi +``` + +## Memory Management + +- `qpq_connect` returns a heap-allocated handle. The caller **must** call `qpq_disconnect` to free it. +- `qpq_receive` writes a heap-allocated JSON string to `*out_json`. The caller **must** call `qpq_free_string` to free it. +- `qpq_last_error` returns a pointer owned by the handle. Do **not** free it. It is valid until the next FFI call on the same handle. + +## Thread Safety + +Each `QpqHandle` owns its own Tokio runtime. Concurrent calls on the **same** handle are not safe. Create separate handles for concurrent use. + +## Internals + +The FFI layer bridges synchronous C callers to the async Rust client: + +``` +C caller ─── qpq_login() ───► QpqHandle ─── runtime.block_on() ───► async Rust client +``` + +Each handle contains: +- A Tokio runtime for blocking on async operations +- Server connection parameters +- Login state and error buffer diff --git a/docs/sdk/go.md b/docs/sdk/go.md new file mode 100644 index 0000000..646a944 --- /dev/null +++ b/docs/sdk/go.md @@ -0,0 +1,95 @@ +# Go SDK + +The Go SDK provides a native QUIC client with Cap'n Proto serialization. + +Location: `sdks/go/` + +## Installation + +```sh +go get quicproquo.dev/sdk/go +``` + +## Quick Start + +```go +package main + +import ( + "context" + "fmt" + "quicproquo.dev/sdk/go/qpq" +) + +func main() { + ctx := context.Background() + + client, err := qpq.Connect(ctx, qpq.Options{ + Addr: "127.0.0.1:5001", + InsecureSkipVerify: true, // dev only + }) + if err != nil { + panic(err) + } + defer client.Close() + + // Health check + status, _ := client.Health(ctx) + fmt.Println("Server:", status) + + // OPAQUE login (requires external OPAQUE library) + resp, _ := client.LoginStart(ctx, "alice", opaqueRequest) + // ... process resp with OPAQUE library ... + client.LoginFinish(ctx, "alice", finalization, identityKey) + + // Resolve user + key, _ := client.ResolveUser(ctx, "bob") + + // Create channel + chID, wasNew, _ := client.CreateChannel(ctx, key) + + // Send + seq, _ := client.Send(ctx, key, []byte("hello")) + + // Receive (long-poll) + msgs, _ := client.ReceiveWait(ctx, myKey, 5000) + for _, m := range msgs { + fmt.Printf("[%d] %s\n", m.Seq, m.Data) + } +} +``` + +## API + +| Method | Description | +|---|---| +| `qpq.Connect(ctx, opts)` | Connect to server | +| `client.Close()` | Disconnect | +| `client.Health(ctx)` | Health check | +| `client.SetSessionToken(token)` | Set pre-existing token | +| `client.RegisterStart(ctx, username, request)` | OPAQUE register start | +| `client.RegisterFinish(ctx, username, upload, key)` | OPAQUE register finish | +| `client.LoginStart(ctx, username, request)` | OPAQUE login start | +| `client.LoginFinish(ctx, username, finalization, key)` | OPAQUE login finish | +| `client.ResolveUser(ctx, username)` | Look up identity key | +| `client.CreateChannel(ctx, peerKey)` | Create DM channel | +| `client.Send(ctx, recipientKey, payload)` | Send message | +| `client.SendWithTTL(ctx, recipientKey, payload, ttl)` | Disappearing message | +| `client.Receive(ctx, recipientKey)` | Fetch messages | +| `client.ReceiveWait(ctx, recipientKey, timeoutMs)` | Long-poll | +| `client.DeleteAccount(ctx)` | Delete account | + +## Structure + +- `qpq/` -- High-level client API +- `transport/` -- QUIC + TLS 1.3 transport, Cap'n Proto RPC framing +- `proto/node/` -- Generated Cap'n Proto Go types +- `cmd/example/` -- Example usage + +## Transport + +The Go SDK uses `quic-go` for QUIC transport and `capnproto.org/go/capnp/v3` for Cap'n Proto RPC. The transport layer handles: + +- TLS 1.3 with configurable CA certificates +- Cap'n Proto RPC bootstrap over QUIC streams +- Automatic connection management diff --git a/docs/sdk/index.md b/docs/sdk/index.md new file mode 100644 index 0000000..f01d491 --- /dev/null +++ b/docs/sdk/index.md @@ -0,0 +1,63 @@ +# quicproquo SDK Documentation + +This guide covers how to build clients for the quicproquo E2E encrypted messenger using the official SDKs or by implementing your own. + +## Official SDKs + +| Language | Location | Transport | Status | +|---|---|---|---| +| **Rust** | `crates/quicproquo-client` | QUIC + Cap'n Proto | Production | +| **Go** | `sdks/go/` | QUIC + Cap'n Proto | Production | +| **TypeScript** | `sdks/typescript/` | WebSocket bridge + WASM crypto | Production | +| **Python** | `sdks/python/` | QUIC + Protobuf (v2) / Rust FFI | Production | +| **C** | `crates/quicproquo-ffi/` | Rust FFI (synchronous) | Production | +| **Swift** | `sdks/swift/` | C FFI wrapper | In progress | +| **Kotlin** | `sdks/kotlin/` | JNI + C FFI | In progress | +| **Java** | `sdks/java/` | JNI + C FFI | In progress | +| **Ruby** | `sdks/ruby/` | FFI gem | In progress | + +## Documentation + +- [Wire Format](wire-format.md) -- v2 QUIC + Protobuf wire protocol +- [Rust SDK](rust.md) -- Native Rust client +- [Go SDK](go.md) -- Go client with QUIC transport +- [Python SDK](python.md) -- Python client (async QUIC + sync FFI) +- [TypeScript SDK](typescript.md) -- Browser/Node.js client with WASM crypto +- [C FFI](c-ffi.md) -- C bindings for language integrations +- [Build Your Own SDK](build-your-own.md) -- Guide for implementing a new language SDK + +## Architecture Overview + +``` + Client SDK Server + ───────── ────── + ┌──────────┐ QUIC/TLS 1.3 ┌──────────┐ + │ App code │ ◄──────────────► │ RPC │ + │ │ v2 wire frames │ dispatch │ + │ SDK API │ │ │ + │ │ [method_id:u16] │ handlers │ + │ Proto │ [req_id:u32] │ │ + │ encode/ │ [len:u32] │ storage │ + │ decode │ [protobuf] │ │ + │ │ │ │ + │ QUIC │ │ QUIC │ + │ transport│ │ listener │ + └──────────┘ └──────────┘ +``` + +Each RPC call opens a new QUIC bidirectional stream. The request and response use the same 10-byte framing header followed by a protobuf payload. + +## Quick Start + +1. Choose an SDK for your language +2. Connect to the server over QUIC (or WebSocket bridge for browsers) +3. Authenticate with OPAQUE (register or login) +4. Upload MLS key packages for E2E encryption +5. Send and receive encrypted messages + +## Canonical Schemas + +- **Protobuf** (v2): `proto/qpq/v1/*.proto` -- 14 service definitions +- **Cap'n Proto** (v1): `schemas/*.capnp` -- legacy RPC interface + +The protobuf schemas in `proto/qpq/v1/` are the canonical API contract for the v2 protocol. New SDKs should implement against these definitions. diff --git a/docs/sdk/python.md b/docs/sdk/python.md new file mode 100644 index 0000000..e470179 --- /dev/null +++ b/docs/sdk/python.md @@ -0,0 +1,120 @@ +# Python SDK + +The Python SDK provides both async QUIC transport and synchronous Rust FFI backends. + +Location: `sdks/python/` + +## Installation + +```sh +pip install quicproquo +``` + +## Transport Backends + +### Async QUIC (pure Python) + +Uses [aioquic](https://github.com/aiortc/aioquic) for native QUIC transport with the v2 protobuf wire format. No Rust build required. + +```python +import asyncio +from quicproquo import QpqClient, ConnectOptions + +async def main(): + client = await QpqClient.connect(ConnectOptions( + addr="127.0.0.1:5001", + ca_cert_path="ca.pem", + )) + + health = await client.health() + print(f"Server: {health.status}") + + # OPAQUE auth (requires external OPAQUE library) + resp = await client.login_start("alice", opaque_request) + token = await client.login_finish("alice", finalization, identity_key) + + # Messaging + key, proof = await client.resolve_user("bob") + seq, proof = await client.send(key, b"hello") + messages = await client.receive_wait(my_key, timeout_ms=5000) + + await client.close() + +asyncio.run(main()) +``` + +### Rust FFI (synchronous) + +Wraps `libquicproquo_ffi` via CFFI for full Rust crypto stack at native speed. + +```sh +cargo build --release -p quicproquo-ffi +``` + +```python +from quicproquo import QpqClient, ConnectOptions + +client = QpqClient.connect_ffi(ConnectOptions( + addr="127.0.0.1:5001", + ca_cert_path="ca.pem", +)) + +client.ffi_login("alice", "password123") +client.ffi_send("bob", b"hello") +messages = client.ffi_receive(timeout_ms=5000) +client.close_sync() +``` + +## Async API (QUIC) + +| Method | Description | +|---|---| +| `QpqClient.connect(opts)` | Connect via QUIC | +| `client.health()` | Health check | +| `client.register_start(username, request)` | OPAQUE register start | +| `client.register_finish(username, upload, key)` | OPAQUE register finish | +| `client.login_start(username, request)` | OPAQUE login start | +| `client.login_finish(username, finalization, key)` | OPAQUE login finish | +| `client.resolve_user(username)` | Look up identity key | +| `client.resolve_identity(key)` | Reverse look up username | +| `client.create_channel(peer_key)` | Create DM channel | +| `client.send(recipient_key, payload)` | Send message | +| `client.receive(recipient_key)` | Fetch messages | +| `client.receive_wait(recipient_key, timeout_ms=5000)` | Long-poll | +| `client.ack(recipient_key, seq_up_to)` | Acknowledge messages | +| `client.upload_key_package(key, package)` | Upload MLS key package | +| `client.fetch_key_package(key)` | Fetch MLS key package | +| `client.upload_hybrid_key(key, hybrid_pk)` | Upload hybrid key | +| `client.fetch_hybrid_key(key)` | Fetch hybrid key | +| `client.delete_account()` | Delete account | +| `client.close()` | Disconnect (async) | + +## FFI API (synchronous) + +| Method | Description | +|---|---| +| `QpqClient.connect_ffi(opts)` | Connect via Rust FFI | +| `client.ffi_login(username, password)` | Full OPAQUE login | +| `client.ffi_send(recipient, message)` | Send by username | +| `client.ffi_receive(timeout_ms=5000)` | Receive messages | +| `client.close_sync()` | Disconnect (sync) | + +## Exceptions + +| Exception | Description | +|---|---| +| `QpqError` | Base exception for all SDK errors | +| `AuthError` | OPAQUE authentication failed | +| `TimeoutError` | Operation timed out | +| `ConnectionError` | Server connection failure | + +## Structure + +| File | Purpose | +|---|---| +| `quicproquo/client.py` | High-level client API | +| `quicproquo/transport.py` | QUIC transport (aioquic) | +| `quicproquo/ffi.py` | Rust FFI transport (CFFI) | +| `quicproquo/proto.py` | Protobuf encode/decode | +| `quicproquo/wire.py` | v2 wire format framing | +| `quicproquo/types.py` | Data types and exceptions | diff --git a/docs/sdk/rust.md b/docs/sdk/rust.md new file mode 100644 index 0000000..1953d2e --- /dev/null +++ b/docs/sdk/rust.md @@ -0,0 +1,63 @@ +# Rust SDK + +The Rust client is the reference implementation, located in `crates/quicproquo-client/`. + +## Installation + +Add to your `Cargo.toml`: + +```toml +[dependencies] +quicproquo-client = { path = "crates/quicproquo-client" } +``` + +## Connection + +The Rust client connects directly over QUIC with Cap'n Proto RPC: + +```rust +use quicproquo_client::{cmd_health, cmd_login, cmd_send, connect_node}; + +// Health check +cmd_health("127.0.0.1:5001", &ca_cert_path, "localhost").await?; + +// Login via OPAQUE +cmd_login( + "127.0.0.1:5001", &ca_cert_path, "localhost", + "alice", "password123", + None, // identity_key_hex + Some(&state_path), // state persistence + None, // state_password +).await?; +``` + +## Key Features + +- Full MLS (RFC 9420) group encryption +- Hybrid post-quantum KEM (X25519 + ML-KEM-768) +- OPAQUE authentication with zeroizing credential storage +- SQLCipher local state with Argon2id key derivation +- Sealed sender metadata protection +- v2 QUIC + Protobuf transport (via `quicproquo-sdk` crate) + +## v2 SDK Crate + +The `quicproquo-sdk` crate provides the higher-level v2 API: + +```rust +use quicproquo_sdk::QpqClient; + +let client = QpqClient::connect("127.0.0.1:5001", &tls_config).await?; +let health = client.health().await?; +``` + +## Crate Structure + +| Crate | Purpose | +|---|---| +| `quicproquo-core` | Crypto primitives, MLS, hybrid KEM | +| `quicproquo-proto` | Protobuf + Cap'n Proto generated types | +| `quicproquo-rpc` | QUIC RPC framework (framing, dispatch) | +| `quicproquo-sdk` | High-level client SDK | +| `quicproquo-client` | CLI/TUI client application | +| `quicproquo-ffi` | C FFI bindings | diff --git a/docs/sdk/typescript.md b/docs/sdk/typescript.md new file mode 100644 index 0000000..a70e67c --- /dev/null +++ b/docs/sdk/typescript.md @@ -0,0 +1,128 @@ +# TypeScript SDK + +The TypeScript SDK provides a browser-compatible client with WASM-based crypto. + +Location: `sdks/typescript/` + +## Installation + +```sh +npm install quicproquo +``` + +## Quick Start + +```typescript +import { QpqClient } from "quicproquo"; + +// Connect via WebSocket bridge +const client = await QpqClient.connect({ + addr: "ws://127.0.0.1:5002", +}); + +const status = await client.health(); +console.log("Server:", status); + +// Set session token (obtained via OPAQUE externally) +client.setSessionToken(token); + +// Resolve user +const peerKey = await client.resolveUser("bob"); + +// Create channel +const { channelId, wasNew } = await client.createChannel(peerKey); + +// Send message +const seq = await client.send(peerKey, new TextEncoder().encode("hello")); + +// Receive messages +const messages = await client.receive(myKey); +for (const msg of messages) { + console.log(`[${msg.seq}] ${new TextDecoder().decode(msg.data)}`); +} + +client.close(); +``` + +## Offline Crypto + +The SDK includes WASM-compiled crypto primitives that work without a server: + +```typescript +const client = await QpqClient.offline(); + +// Ed25519 identity +const { seed, publicKey } = client.generateIdentity(); +const sig = client.sign(seed, message); +const valid = client.verify(publicKey, message, sig); + +// Hybrid KEM (X25519 + ML-KEM-768) +const kp = client.hybridGenerateKeypair(); +const ciphertext = client.hybridEncrypt(kp.publicKey, plaintext); +const decrypted = client.hybridDecrypt(kp.blob, ciphertext); + +// Safety numbers +const safetyNumber = client.computeSafetyNumber(keyA, keyB); + +// Sealed sender +const sealed = client.seal(seed, payload); +const unsealed = client.unseal(sealed); + +// Message padding +const padded = client.padMessage(data); +const unpadded = client.unpadMessage(padded); +``` + +## API + +### RPC Methods + +| Method | Description | +|---|---| +| `QpqClient.connect(opts)` | Connect via WebSocket | +| `QpqClient.offline()` | Crypto-only mode | +| `client.health()` | Health check | +| `client.resolveUser(username)` | Look up identity key | +| `client.createChannel(peerKey)` | Create DM channel | +| `client.send(recipientKey, payload)` | Send message | +| `client.sendWithTTL(recipientKey, payload, ttl)` | Disappearing message | +| `client.receive(recipientKey)` | Fetch messages | +| `client.deleteAccount()` | Delete account | +| `client.close()` | Disconnect | + +### Crypto Methods + +| Method | Description | +|---|---| +| `client.generateIdentity()` | Generate Ed25519 keypair | +| `client.identityPublicKey(seed)` | Derive public key | +| `client.sign(seed, message)` | Ed25519 sign | +| `client.verify(publicKey, message, sig)` | Ed25519 verify | +| `client.hybridGenerateKeypair()` | Generate hybrid keypair | +| `client.hybridEncrypt(pk, plaintext)` | Hybrid encrypt | +| `client.hybridDecrypt(blob, ciphertext)` | Hybrid decrypt | +| `client.computeSafetyNumber(keyA, keyB)` | Safety number | +| `client.seal(seed, payload)` | Sealed sender | +| `client.unseal(envelope)` | Unseal | +| `client.padMessage(data)` | Pad message | +| `client.unpadMessage(data)` | Unpad message | + +## Architecture + +The TypeScript SDK uses a WebSocket bridge proxy because browsers cannot open raw QUIC connections. The bridge translates JSON-over-WebSocket to Cap'n Proto RPC: + +``` +Browser ─── WebSocket ───► Bridge Proxy ─── QUIC/capnp ───► Server +``` + +Crypto operations (Ed25519, hybrid KEM, sealed sender) run entirely in WASM, compiled from the Rust `quicproquo-core` crate. + +## Structure + +| File | Purpose | +|---|---| +| `src/client.ts` | High-level client API | +| `src/transport.ts` | WebSocket transport with auto-reconnect | +| `src/crypto.ts` | WASM crypto bindings | +| `src/types.ts` | TypeScript interfaces | +| `wasm-crypto/` | Rust WASM crypto source | diff --git a/docs/sdk/wire-format.md b/docs/sdk/wire-format.md new file mode 100644 index 0000000..b7e0358 --- /dev/null +++ b/docs/sdk/wire-format.md @@ -0,0 +1,208 @@ +# v2 Wire Format + +The quicproquo v2 protocol uses QUIC (RFC 9000) with TLS 1.3 as the transport layer and Protocol Buffers for message serialization. + +## Connection + +- **Protocol**: QUIC with TLS 1.3 +- **ALPN**: `qpq` +- **Port**: 5001 (default) +- **Certificate**: Server presents a TLS certificate; clients verify against a CA cert + +## Frame Format + +Every RPC request and response is wrapped in a 10-byte binary header: + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| method_id (u16) | req_id (u32) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| req_id (cont.) | payload_len (u32) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| payload_len (cont.) | protobuf payload ... | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +| Field | Type | Bytes | Description | +|---|---|---|---| +| `method_id` | `u16` | 0-1 | RPC method identifier (network byte order) | +| `req_id` | `u32` | 2-5 | Client-generated request correlation ID (network byte order) | +| `payload_len` | `u32` | 6-9 | Length of the protobuf payload (network byte order) | +| payload | bytes | 10+ | Protobuf-encoded request or response message | + +All multi-byte integers are **big-endian** (network byte order). + +## Stream Model + +Each RPC call uses a **dedicated QUIC bidirectional stream**: + +1. Client opens a new stream +2. Client sends the request frame and marks end-of-stream +3. Server reads the request, processes it, and sends the response frame +4. Server marks end-of-stream + +This allows concurrent RPCs without head-of-line blocking. + +## Method IDs + +### Auth (100-103) + +| ID | Method | Request | Response | +|---|---|---|---| +| 100 | `OpaqueRegisterStart` | `OpaqueRegisterStartRequest` | `OpaqueRegisterStartResponse` | +| 101 | `OpaqueRegisterFinish` | `OpaqueRegisterFinishRequest` | `OpaqueRegisterFinishResponse` | +| 102 | `OpaqueLoginStart` | `OpaqueLoginStartRequest` | `OpaqueLoginStartResponse` | +| 103 | `OpaqueLoginFinish` | `OpaqueLoginFinishRequest` | `OpaqueLoginFinishResponse` | + +### Delivery (200-205) + +| ID | Method | Request | Response | +|---|---|---|---| +| 200 | `Enqueue` | `EnqueueRequest` | `EnqueueResponse` | +| 201 | `Fetch` | `FetchRequest` | `FetchResponse` | +| 202 | `FetchWait` | `FetchWaitRequest` | `FetchWaitResponse` | +| 203 | `Peek` | `PeekRequest` | `PeekResponse` | +| 204 | `Ack` | `AckRequest` | `AckResponse` | +| 205 | `BatchEnqueue` | `BatchEnqueueRequest` | `BatchEnqueueResponse` | + +### Keys (300-304) + +| ID | Method | Request | Response | +|---|---|---|---| +| 300 | `UploadKeyPackage` | `UploadKeyPackageRequest` | `UploadKeyPackageResponse` | +| 301 | `FetchKeyPackage` | `FetchKeyPackageRequest` | `FetchKeyPackageResponse` | +| 302 | `UploadHybridKey` | `UploadHybridKeyRequest` | `UploadHybridKeyResponse` | +| 303 | `FetchHybridKey` | `FetchHybridKeyRequest` | `FetchHybridKeyResponse` | +| 304 | `FetchHybridKeys` | `FetchHybridKeysRequest` | `FetchHybridKeysResponse` | + +### Channel (400) + +| ID | Method | Request | Response | +|---|---|---|---| +| 400 | `CreateChannel` | `CreateChannelRequest` | `CreateChannelResponse` | + +### Group Management (410-413) + +| ID | Method | Request | Response | +|---|---|---|---| +| 410 | `RemoveMember` | `RemoveMemberRequest` | `RemoveMemberResponse` | +| 411 | `UpdateGroupMetadata` | `UpdateGroupMetadataRequest` | `UpdateGroupMetadataResponse` | +| 412 | `ListGroupMembers` | `ListGroupMembersRequest` | `ListGroupMembersResponse` | +| 413 | `RotateKeys` | `RotateKeysRequest` | `RotateKeysResponse` | + +### User (500-501) + +| ID | Method | Request | Response | +|---|---|---|---| +| 500 | `ResolveUser` | `ResolveUserRequest` | `ResolveUserResponse` | +| 501 | `ResolveIdentity` | `ResolveIdentityRequest` | `ResolveIdentityResponse` | + +### Blob (600-601) + +| ID | Method | Request | Response | +|---|---|---|---| +| 600 | `UploadBlob` | `UploadBlobRequest` | `UploadBlobResponse` | +| 601 | `DownloadBlob` | `DownloadBlobRequest` | `DownloadBlobResponse` | + +### Device (700-702) + +| ID | Method | Request | Response | +|---|---|---|---| +| 700 | `RegisterDevice` | `RegisterDeviceRequest` | `RegisterDeviceResponse` | +| 701 | `ListDevices` | `ListDevicesRequest` | `ListDevicesResponse` | +| 702 | `RevokeDevice` | `RevokeDeviceRequest` | `RevokeDeviceResponse` | + +### P2P / Health (800-802) + +| ID | Method | Request | Response | +|---|---|---|---| +| 800 | `PublishEndpoint` | `PublishEndpointRequest` | `PublishEndpointResponse` | +| 801 | `ResolveEndpoint` | `ResolveEndpointRequest` | `ResolveEndpointResponse` | +| 802 | `Health` | `HealthRequest` | `HealthResponse` | + +### Federation (900-905) + +| ID | Method | Request | Response | +|---|---|---|---| +| 900 | `RelayEnqueue` | `RelayEnqueueRequest` | `RelayEnqueueResponse` | +| 901-905 | Reserved | -- | -- | + +### Account (950) + +| ID | Method | Request | Response | +|---|---|---|---| +| 950 | `DeleteAccount` | `DeleteAccountRequest` | `DeleteAccountResponse` | + +## Protobuf Definitions + +All message types are defined in `proto/qpq/v1/*.proto`: + +| File | Services | +|---|---| +| `auth.proto` | OPAQUE registration and login | +| `common.proto` | Auth context, account deletion | +| `delivery.proto` | Message enqueue, fetch, peek, ack | +| `keys.proto` | MLS key packages, hybrid keys | +| `channel.proto` | Channel creation | +| `user.proto` | User/identity resolution | +| `group.proto` | Group management | +| `blob.proto` | Binary object storage | +| `device.proto` | Multi-device management | +| `p2p.proto` | P2P endpoints, health | +| `federation.proto` | Cross-server relay | +| `push.proto` | Push notifications | +| `recovery.proto` | Account recovery | +| `moderation.proto` | Content moderation | + +## Authentication Flow + +Authentication uses the OPAQUE protocol (asymmetric PAKE): + +``` +Client Server + │ │ + │ OpaqueRegisterStart(username, │ + │ registration_request) │ + │ ──────────────────────────────────►│ + │ │ + │ OpaqueRegisterStartResponse( │ + │ registration_response) │ + │ ◄──────────────────────────────────│ + │ │ + │ OpaqueRegisterFinish(username, │ + │ upload, identity_key) │ + │ ──────────────────────────────────►│ + │ │ + │ OpaqueRegisterFinishResponse( │ + │ success) │ + │ ◄──────────────────────────────────│ + │ │ + │ OpaqueLoginStart(username, │ + │ login_request) │ + │ ──────────────────────────────────►│ + │ │ + │ OpaqueLoginStartResponse( │ + │ login_response) │ + │ ◄──────────────────────────────────│ + │ │ + │ OpaqueLoginFinish(username, │ + │ finalization, identity_key) │ + │ ──────────────────────────────────►│ + │ │ + │ OpaqueLoginFinishResponse( │ + │ session_token) │ + │ ◄──────────────────────────────────│ +``` + +The `session_token` is used for subsequent authenticated RPCs. + +## Error Handling + +The server returns protobuf-encoded error responses on the same stream. Error conditions include: + +- Invalid method ID: stream reset +- Authentication failure: error response with details +- Rate limiting: error response with retry-after hint +- Internal errors: generic error response