- omnivox::server::<T>()/client::<T>() builders; plain closures, enum in, match in the callback — the original C++ UX with the ceremony removed - serde + bincode replace the hand-rolled Cereal codec (vectors.rs still governs the crypto leaves via the C++ golden oracles) - v2.1 frames: u32 len | type | ct | tag; nonce derived from the sequence counter (no wire nonce), type byte authenticated as AAD; any frame-level failure is fatal, authenticated-but-undecodable payloads are skipped - stream lane: send_stream/on_stream, 1 MiB frames, shared sequence with messages (exact sender interleaving preserved); see dump-agent/viewer - per-connection bounded queues + writer tasks; reply/reply_stream are non-blocking try_send lanes for callbacks - benches: seal/open criterion group + transport harness (spray msg/s, stream GiB/s, raw loopback baseline) - profile.dev.package scrypt/pbkdf2 opt-level 3: golden-vector scrypt at N=2^17 was unusable in debug builds - PROTOCOL-V2.md and README rewritten for v2.1
100 lines
3.7 KiB
Markdown
100 lines
3.7 KiB
Markdown
# omnivox-rs
|
|
|
|
Encrypted async TCP client-server messaging — protocol **v2.1**. A Rust
|
|
library plus example binaries, descended from the OmniVox C++ library and
|
|
hardened past it after a full red-team review.
|
|
|
|
The API is the original C++ shape — your struct goes in, a match arms over
|
|
it in one callback — with all the boilerplate removed:
|
|
|
|
```rust
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
enum AppMsg { Hello, Note(String) }
|
|
|
|
// server
|
|
let server = omnivox::server::<AppMsg>()
|
|
.password("hunter2", b"static-salt") // or .key([u8; 32])
|
|
.on_data(|msg, ctx| match msg {
|
|
AppMsg::Hello => println!("hello from {}", ctx.ip()),
|
|
AppMsg::Note(n) => { ctx.reply(&AppMsg::Note(n.to_uppercase())); }
|
|
})
|
|
.serve(8080)
|
|
.await?;
|
|
|
|
// client
|
|
let client = omnivox::client::<AppMsg>()
|
|
.password("hunter2", b"static-salt")
|
|
.on_data(|msg, _ctx| println!("server said {msg:?}"))
|
|
.connect("127.0.0.1", 8080)
|
|
.await?;
|
|
|
|
client.send(&AppMsg::Hello).await;
|
|
```
|
|
|
|
Any `Serialize + DeserializeOwned` type is a message — serde+bincode
|
|
replaces the original's hand-written `Cereal`. For bulk data there is a
|
|
second lane: `client.send_stream(&bytes)` pairs with an `on_stream`
|
|
callback — no encoding, 1 MiB frames, same sequence numbers, so messages
|
|
and stream chunks arrive in exactly the order they were sent.
|
|
|
|
- X25519 handshake on every connection → forward secrecy, session-bound keys
|
|
- ChaCha20-Poly1305, nonce derived from per-direction sequence numbers →
|
|
replay-proof, no nonce on the wire
|
|
- Direction-separated traffic keys; frame type authenticated as AAD
|
|
- scrypt password KDF (blocking-thread), random-PSK support, panic-isolated
|
|
callbacks, connection caps, idle/stall/send timeouts, zero `unsafe`
|
|
|
|
**Not wire-compatible with the C++ original or v1** — by design; v1 peers
|
|
are refused at the handshake. See `PROTOCOL-V2.md` for the normative spec
|
|
and `PARITY-CONTRACT.md` (retired) for the port's history.
|
|
|
|
## Security posture
|
|
|
|
| v1 weakness | v2.1 answer |
|
|
|---|---|
|
|
| captured frames replay forever | sequence-number nonces; a replay kills the connection |
|
|
| frames portable across connections | keys bound to per-session nonces |
|
|
| 5 MiB pre-auth allocation from 8 bytes | fixed 69-byte handshake before any attacker-sized read |
|
|
| one symmetric key, either peer can forge as itself | direction-separated HKDF session keys |
|
|
| static key, no forward secrecy | ephemeral X25519 per connection |
|
|
| message/stream confusion impossible to detect | type byte authenticated as AAD |
|
|
| no client-side timeouts | full idle/stall discipline both roles |
|
|
| panicking user callback killed the read loop | callbacks panic-isolated |
|
|
| demo password in every repo checkout | release builds refuse the demo key |
|
|
|
|
Use random 32-byte PSKs where you can; an active attacker can grind scrypt
|
|
against human passwords (PAKE planned).
|
|
|
|
## Examples
|
|
|
|
```bash
|
|
cargo run --example omnivox-server # terminal 1 — enum echo server
|
|
cargo run --example omnivox-client # terminal 2
|
|
|
|
cargo run --example dump-agent # terminal 1 — streams a toy 16 MiB
|
|
cargo run --example dump-viewer # terminal 2 — receives, verifies, saves
|
|
```
|
|
|
|
## Benchmarks
|
|
|
|
`cargo bench --bench transport` — measured on the author's machine
|
|
(Intel Core Ultra 7 258V, loopback):
|
|
|
|
| What | Result |
|
|
|---|---|
|
|
| ChaCha20-Poly1305 seal, 1 MiB frame | ≈1.18 GiB/s single-threaded |
|
|
| 256 B messages, encrypted round trip | ≈120 000 msg/s |
|
|
| Stream lane, 1 MiB frames | ≈0.7 GiB/s (vs ≈2.0 raw loopback echo) |
|
|
|
|
## Build & test
|
|
|
|
```bash
|
|
cargo test # unit, attack regressions, golden vectors, doctests
|
|
cargo clippy --all-targets -- -D warnings
|
|
```
|
|
|
|
## License
|
|
|
|
GPL-3.0-or-later. |