docs(sdk): add comprehensive SDK documentation and wire format reference
Covers all official SDKs (Rust, Go, Python, TypeScript, C FFI), the v2 wire format with method ID tables, authentication flow, and a build-your-own-SDK guide with implementation checklist.
This commit is contained in:
166
docs/sdk/build-your-own.md
Normal file
166
docs/sdk/build-your-own.md
Normal file
@@ -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
|
||||
142
docs/sdk/c-ffi.md
Normal file
142
docs/sdk/c-ffi.md
Normal file
@@ -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 <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// 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
|
||||
95
docs/sdk/go.md
Normal file
95
docs/sdk/go.md
Normal file
@@ -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
|
||||
63
docs/sdk/index.md
Normal file
63
docs/sdk/index.md
Normal file
@@ -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.
|
||||
120
docs/sdk/python.md
Normal file
120
docs/sdk/python.md
Normal file
@@ -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 |
|
||||
63
docs/sdk/rust.md
Normal file
63
docs/sdk/rust.md
Normal file
@@ -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 |
|
||||
128
docs/sdk/typescript.md
Normal file
128
docs/sdk/typescript.md
Normal file
@@ -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 |
|
||||
208
docs/sdk/wire-format.md
Normal file
208
docs/sdk/wire-format.md
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user