From d78108cf1bffb80a80f3d30696732789935b3dad Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Tue, 5 May 2026 09:16:40 -0400 Subject: [PATCH 01/17] ql-api: init --- Cargo.lock | 383 +++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + ql-api/Cargo.toml | 20 +++ ql-api/src/app_store.rs | 73 ++++++++ ql-api/src/benchmark.rs | 72 ++++++++ ql-api/src/bootstrap.rs | 22 +++ ql-api/src/codec.rs | 90 ++++++++++ ql-api/src/lib.rs | 38 ++++ 8 files changed, 694 insertions(+), 5 deletions(-) create mode 100644 ql-api/Cargo.toml create mode 100644 ql-api/src/app_store.rs create mode 100644 ql-api/src/benchmark.rs create mode 100644 ql-api/src/bootstrap.rs create mode 100644 ql-api/src/codec.rs create mode 100644 ql-api/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 38355b76..1e4bacb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,12 +26,40 @@ dependencies = [ "memchr", ] +[[package]] +name = "allo-isolate" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449e356a4864c017286dbbec0e12767ea07efba29e3b7d984194c2a7ff3c4550" +dependencies = [ + "anyhow", + "atomic", + "backtrace", +] + [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter 0.1.4", + "log", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -91,6 +119,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "async-channel" version = "2.5.0" @@ -103,6 +137,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + [[package]] name = "autocfg" version = "1.5.0" @@ -175,6 +215,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "build-target" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" + [[package]] name = "bumpalo" version = "3.19.0" @@ -224,6 +270,12 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.10.1" @@ -259,6 +311,33 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -287,6 +366,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "consts" version = "1.0.0" @@ -340,6 +429,28 @@ dependencies = [ "typenum", ] +[[package]] +name = "dart-sys" +version = "4.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57967e4b200d767d091b961d6ab42cc7d0cc14fe9e052e75d0d3cf9eb732d895" +dependencies = [ + "cc", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "dcbor" version = "0.23.3" @@ -354,6 +465,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "delegate-attr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "diatomic-waker" version = "0.2.3" @@ -376,6 +498,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_filter" version = "1.0.1" @@ -394,7 +526,7 @@ checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", - "env_filter", + "env_filter 1.0.1", "jiff", "log", ] @@ -443,23 +575,101 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "flutter_rust_bridge" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde126295b2acc5f0a712e265e91b6fdc0ed38767496483e592ae7134db83725" +dependencies = [ + "allo-isolate", + "android_logger", + "anyhow", + "build-target", + "bytemuck", + "byteorder", + "console_error_panic_hook", + "dart-sys", + "delegate-attr", + "flutter_rust_bridge_macros", + "futures", + "js-sys", + "lazy_static", + "log", + "oslog", + "portable-atomic", + "threadpool", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "flutter_rust_bridge_macros" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f0420326b13675321b194928bb7830043b68cf8b810e1c651285c747abb080" +dependencies = [ + "hex", + "md-5", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -474,6 +684,46 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + [[package]] name = "generator" version = "0.8.8" @@ -538,6 +788,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -587,6 +843,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -791,6 +1053,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -819,6 +1090,16 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.5" @@ -902,6 +1183,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.36.7" @@ -929,6 +1220,17 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea" +[[package]] +name = "oslog" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969" +dependencies = [ + "cc", + "dashmap", + "log", +] + [[package]] name = "parking" version = "2.2.1" @@ -938,6 +1240,19 @@ dependencies = [ "loom", ] +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + [[package]] name = "paste" version = "1.0.15" @@ -1050,6 +1365,17 @@ dependencies = [ "syn", ] +[[package]] +name = "ql-api" +version = "0.1.0" +dependencies = [ + "bytes", + "ciborium", + "flutter_rust_bridge", + "ql-rpc", + "serde", +] + [[package]] name = "ql-codec" version = "0.1.0" @@ -1188,6 +1514,15 @@ dependencies = [ "rand_core", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.3" @@ -1305,6 +1640,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.228" @@ -1450,6 +1791,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + [[package]] name = "tinyvec" version = "1.10.0" @@ -1678,6 +2028,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -1710,6 +2073,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "windows-core" version = "0.61.2" diff --git a/Cargo.toml b/Cargo.toml index 056fd430..a61e9d98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "btp", "ql-codec", "ql-common", + "ql-api", "ql-fsm", "ql-rpc", "ql-runtime", diff --git a/ql-api/Cargo.toml b/ql-api/Cargo.toml new file mode 100644 index 00000000..9188a3a8 --- /dev/null +++ b/ql-api/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ql-api" +version = "0.1.0" +edition = "2021" +description = "KeyOS QLv2 RPC protocol" +license = "Proprietary" + + +[dependencies] +ql-rpc = { workspace = true } +bytes = { workspace = true } +ciborium = "0.2.2" +flutter_rust_bridge = { version = "=2.11.1", optional = true } +serde = { version = "1.0", features = ["derive"] } + +[features] +frb = ["flutter_rust_bridge"] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] } diff --git a/ql-api/src/app_store.rs b/ql-api/src/app_store.rs new file mode 100644 index 00000000..92faf038 --- /dev/null +++ b/ql-api/src/app_store.rs @@ -0,0 +1,73 @@ +use ql_rpc::{Download, Request, RouteId}; + +use crate::{codec::rpc, Error, Route}; + +rpc! { + #[derive(Copy)] + pub struct AppVersion(pub u32, pub u32, pub u32); +} + +// +// LIST APPS +// + +pub struct ListApps; + +rpc! { + pub struct ListAppsRequest { + // + } +} + +rpc! { + pub struct ListAppsResponse { + pub apps: Vec, + } +} + +rpc! { + pub struct AppDetails { + pub name: String, + pub icon: String, // SVG? + pub developer_id: Vec, + pub app_id: Vec, + } +} + +impl Request for ListApps { + const ROUTE: RouteId = Route::ListApps.id(); + type Error = Error; + type Request = ListAppsRequest; + type Response = ListAppsResponse; +} + +// +// APP DOWNLOAD +// +// download a single tar file that contains: +// - app.elf +// - manifest.json + +pub struct AppDownload; + +rpc! { + pub struct AppDownloadRequest { + pub version: Option, + } +} + +rpc! { + pub struct AppDownloadHeader { + pub version: String, + } +} + +impl Download for AppDownload { + const ROUTE: RouteId = Route::AppDownload.id(); + + type Error = Error; + + type Request = AppDownloadRequest; + + type ResponseHeader = AppDownloadHeader; +} diff --git a/ql-api/src/benchmark.rs b/ql-api/src/benchmark.rs new file mode 100644 index 00000000..0b6504d4 --- /dev/null +++ b/ql-api/src/benchmark.rs @@ -0,0 +1,72 @@ +use ql_rpc::{request::Request, Download, RouteId, Subscription}; + +use crate::{codec::rpc, Error, Route}; + +// Echo request-response benchmark +pub struct Echo; + +impl Request for Echo { + type Error = Error; + type Request = EchoRequest; + type Response = EchoResponse; + + const ROUTE: RouteId = Route::Echo.id(); +} + +rpc! { + pub struct EchoRequest { + pub message: String, + } +} + +rpc! { + pub struct EchoResponse { + pub message: String, + } +} + +// Byte-stream subscription benchmark +pub struct BytesBenchmark; + +impl Subscription for BytesBenchmark { + type Error = Error; + type Event = BenchmarkEvent; + type Request = BenchmarkRequest; + + const ROUTE: ql_rpc::RouteId = Route::BytesBenchmark.id(); +} + +rpc! { + pub struct BenchmarkRequest { + pub length: u32, + } +} + +rpc! { + pub struct BenchmarkEvent { + pub bytes: Vec, + } +} + +// Raw-body download benchmark +pub struct DownloadBenchmark; + +impl Download for DownloadBenchmark { + type Error = Error; + type Request = DownloadBenchmarkRequest; + type ResponseHeader = DownloadBenchmarkHeader; + + const ROUTE: RouteId = Route::DownloadBenchmark.id(); +} + +rpc! { + pub struct DownloadBenchmarkRequest { + pub length: u64, + } +} + +rpc! { + pub struct DownloadBenchmarkHeader { + pub hash: Vec, + } +} diff --git a/ql-api/src/bootstrap.rs b/ql-api/src/bootstrap.rs new file mode 100644 index 00000000..9f050d4b --- /dev/null +++ b/ql-api/src/bootstrap.rs @@ -0,0 +1,22 @@ +use crate::{codec::rpc, Error, Route}; + +pub struct BootstrapQlv1; + +impl ql_rpc::Request for BootstrapQlv1 { + const ROUTE: ql_rpc::RouteId = Route::BootstrapQlv1.id(); + type Error = Error; + type Request = BootstrapRequest; + type Response = BootstrapResponse; +} + +rpc! { + pub struct BootstrapRequest { + pub xid_document: Vec, + } +} + +rpc! { + pub struct BootstrapResponse { + pub xid_document: Vec, + } +} diff --git a/ql-api/src/codec.rs b/ql-api/src/codec.rs new file mode 100644 index 00000000..7d5323f2 --- /dev/null +++ b/ql-api/src/codec.rs @@ -0,0 +1,90 @@ +use std::io; + +use bytes::{Buf, BufMut}; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::Error; + +macro_rules! impl_codec { + ($ty:ty) => { + impl ql_rpc::RpcCodec for $ty { + type Error = crate::Error; + + fn encode_value(&self, out: &mut B) { + $crate::codec::encode_cbor(self, out); + } + + fn decode_value(bytes: &mut B) -> Result { + $crate::codec::decode_cbor(bytes) + } + } + }; +} + +pub(crate) use impl_codec; + +macro_rules! rpc { + ($(#[$attr:meta])* $vis:vis struct $name:ident;) => { + compile_error!("rpc! does not support unit structs"); + }; + ($(#[$attr:meta])* $vis:vis struct $name:ident $($body:tt)+) => { + #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] + $(#[$attr])* + #[cfg_attr(feature = "frb", flutter_rust_bridge::frb(non_opaque))] + $vis struct $name $($body)+ + + $crate::codec::impl_codec!($name); + }; +} + +pub(crate) use rpc; + +pub fn encode_cbor(value: &T, out: &mut B) +where + T: Serialize, + B: BufMut + ?Sized, +{ + ciborium::ser::into_writer(value, BufMutWriter(out)) + .expect("CBOR serialization to BufMut failed"); +} + +pub fn decode_cbor(bytes: &mut B) -> Result +where + T: DeserializeOwned, + B: Buf, +{ + let value = ciborium::de::from_reader(BufReader(bytes))?; + bytes.advance(bytes.remaining()); + Ok(value) +} + +struct BufMutWriter<'a, B: BufMut + ?Sized>(&'a mut B); + +impl io::Write for BufMutWriter<'_, B> { + fn write(&mut self, data: &[u8]) -> io::Result { + if self.0.remaining_mut() < data.len() { + return Err(io::ErrorKind::WriteZero.into()); + } + + self.0.put_slice(data); + Ok(data.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +struct BufReader<'a, B: Buf>(&'a mut B); + +impl io::Read for BufReader<'_, B> { + fn read(&mut self, out: &mut [u8]) -> io::Result { + let len = out.len().min(self.0.remaining()); + if len == 0 { + return Ok(0); + } + + self.0.copy_to_slice(&mut out[..len]); + Ok(len) + } +} diff --git a/ql-api/src/lib.rs b/ql-api/src/lib.rs new file mode 100644 index 00000000..3899a2f7 --- /dev/null +++ b/ql-api/src/lib.rs @@ -0,0 +1,38 @@ +// 1. list catalog of apps (request/response) +// 2. get app by id +// 3. download version? + +mod codec; +mod app_store; +mod benchmark; +mod bootstrap; + +pub use app_store::*; +pub use benchmark::*; +pub use bootstrap::*; + +pub type Error = ciborium::de::Error; + +pub const SERVICE_ID: ql_rpc::ServiceId = ql_rpc::ServiceId([0; 16]); + +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Route { + // setup + BootstrapQlv1 = 100, + + // app store + ListApps = 200, + AppDownload = 201, + + // debug + Echo = 1000, + BytesBenchmark = 1001, + DownloadBenchmark = 1002, +} + +impl Route { + pub const fn id(self) -> ql_rpc::RouteId { + ql_rpc::RouteId::from_u32(self as u32) + } +} From 8714cc4b2aaebc0bf16a8d4b06f723bf72f5682b Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Thu, 14 May 2026 09:28:33 -0400 Subject: [PATCH 02/17] ql-api: migrate all legacy apis --- Cargo.lock | 1 + ql-api/Cargo.toml | 1 + ql-api/src/app_store.rs | 19 +++-- ql-api/src/backup.rs | 167 +++++++++++++++++++++++++++++++++++++++ ql-api/src/benchmark.rs | 27 +++---- ql-api/src/bitcoin.rs | 78 ++++++++++++++++++ ql-api/src/bootstrap.rs | 22 ------ ql-api/src/codec.rs | 34 -------- ql-api/src/firmware.rs | 87 ++++++++++++++++++++ ql-api/src/fx.rs | 84 ++++++++++++++++++++ ql-api/src/lib.rs | 80 +++++++++++++++---- ql-api/src/macros.rs | 82 +++++++++++++++++++ ql-api/src/onboarding.rs | 30 +++++++ ql-api/src/scv.rs | 44 +++++++++++ ql-api/src/status.rs | 52 ++++++++++++ ql-api/src/timezone.rs | 20 +++++ 16 files changed, 729 insertions(+), 99 deletions(-) create mode 100644 ql-api/src/backup.rs create mode 100644 ql-api/src/bitcoin.rs delete mode 100644 ql-api/src/bootstrap.rs create mode 100644 ql-api/src/firmware.rs create mode 100644 ql-api/src/fx.rs create mode 100644 ql-api/src/macros.rs create mode 100644 ql-api/src/onboarding.rs create mode 100644 ql-api/src/scv.rs create mode 100644 ql-api/src/status.rs create mode 100644 ql-api/src/timezone.rs diff --git a/Cargo.lock b/Cargo.lock index 1e4bacb1..33639331 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1372,6 +1372,7 @@ dependencies = [ "bytes", "ciborium", "flutter_rust_bridge", + "ql-common", "ql-rpc", "serde", ] diff --git a/ql-api/Cargo.toml b/ql-api/Cargo.toml index 9188a3a8..c81fbefb 100644 --- a/ql-api/Cargo.toml +++ b/ql-api/Cargo.toml @@ -7,6 +7,7 @@ license = "Proprietary" [dependencies] +ql-common = { workspace = true } ql-rpc = { workspace = true } bytes = { workspace = true } ciborium = "0.2.2" diff --git a/ql-api/src/app_store.rs b/ql-api/src/app_store.rs index 92faf038..629b6a80 100644 --- a/ql-api/src/app_store.rs +++ b/ql-api/src/app_store.rs @@ -1,6 +1,6 @@ -use ql_rpc::{Download, Request, RouteId}; +use ql_rpc::{Download, Request}; -use crate::{codec::rpc, Error, Route}; +use crate::{route, Error}; rpc! { #[derive(Copy)] @@ -11,8 +11,6 @@ rpc! { // LIST APPS // -pub struct ListApps; - rpc! { pub struct ListAppsRequest { // @@ -34,8 +32,7 @@ rpc! { } } -impl Request for ListApps { - const ROUTE: RouteId = Route::ListApps.id(); +impl Request for route::ListApps { type Error = Error; type Request = ListAppsRequest; type Response = ListAppsResponse; @@ -48,8 +45,6 @@ impl Request for ListApps { // - app.elf // - manifest.json -pub struct AppDownload; - rpc! { pub struct AppDownloadRequest { pub version: Option, @@ -62,12 +57,16 @@ rpc! { } } -impl Download for AppDownload { - const ROUTE: RouteId = Route::AppDownload.id(); +rpc! { + pub struct AppDownloadPartHeader {} +} +impl Download for route::AppDownload { type Error = Error; type Request = AppDownloadRequest; type ResponseHeader = AppDownloadHeader; + + type PartHeader = AppDownloadPartHeader; } diff --git a/ql-api/src/backup.rs b/ql-api/src/backup.rs new file mode 100644 index 00000000..e2c32a46 --- /dev/null +++ b/ql-api/src/backup.rs @@ -0,0 +1,167 @@ +use ql_rpc::{Download, Notification, Request, Upload}; + +use crate::{route, Error}; + +rpc! { + pub struct Shard(pub Vec); +} + +rpc! { + pub struct SeedFingerprint(pub [u8; 32]); +} + +rpc! { + pub struct BackupShardRequest { + pub shard: Shard, + } +} + +rpc! { + pub enum BackupShardResponse { + Success, + Error { error: String }, + } +} + +impl Request for route::BackupShard { + type Error = Error; + type Request = BackupShardRequest; + type Response = BackupShardResponse; +} + +rpc! { + pub struct RestoreShardRequest { + pub seed_fingerprint: SeedFingerprint, + pub timestamp: Option, + } +} + +rpc! { + pub enum RestoreShardResponse { + Success { shard: Shard }, + Error { error: String }, + NotFound, + } +} + +impl Request for route::RestoreShard { + type Error = Error; + type Request = RestoreShardRequest; + type Response = RestoreShardResponse; +} + +rpc! { + pub struct EnvoyMagicBackupEnabledRequest {} +} + +rpc! { + pub struct EnvoyMagicBackupEnabledResponse { + pub enabled: bool, + } +} + +impl Request for route::EnvoyMagicBackupEnabled { + type Error = Error; + type Request = EnvoyMagicBackupEnabledRequest; + type Response = EnvoyMagicBackupEnabledResponse; +} + +rpc! { + pub struct PassportMagicBackupEnabledPayload { + pub enabled: bool, + pub seed_fingerprint: SeedFingerprint, + } +} + +impl Notification for route::PassportMagicBackupEnabled { + type Error = Error; + type Payload = PassportMagicBackupEnabledPayload; +} + +rpc! { + pub struct PassportMagicBackupStatusRequest { + pub seed_fingerprint: SeedFingerprint, + pub timestamp: Option, + } +} + +rpc! { + pub struct PassportMagicBackupStatusResponse { + pub shard_backup_found: bool, + } +} + +impl Request for route::PassportMagicBackupStatus { + type Error = Error; + type Request = PassportMagicBackupStatusRequest; + type Response = PassportMagicBackupStatusResponse; +} + +rpc! { + pub struct UploadMagicBackupRequest { + pub seed_fingerprint: SeedFingerprint, + pub total_size: Option, + pub hash: [u8; 32], + } +} + +rpc! { + pub enum UploadMagicBackupResult { + Success, + Error { error: String }, + } +} + +rpc! { + pub struct UploadMagicBackupPartHeader {} +} + +impl Upload for route::UploadMagicBackup { + type Error = Error; + type Request = UploadMagicBackupRequest; + type PartHeader = UploadMagicBackupPartHeader; + type Response = UploadMagicBackupResult; +} + +rpc! { + pub struct DownloadMagicBackupRequest { + pub seed_fingerprint: SeedFingerprint, + } +} + +rpc! { + pub enum DownloadMagicBackupHeader { + Found(BackupMetadata), + NotFound, + Error { error: String }, + } +} + +rpc! { + pub struct BackupMetadata { + pub total_size: Option, + } +} + +rpc! { + pub struct DownloadMagicBackupPartHeader {} +} + +impl Download for route::DownloadMagicBackup { + type Error = Error; + type Request = DownloadMagicBackupRequest; + type ResponseHeader = DownloadMagicBackupHeader; + type PartHeader = DownloadMagicBackupPartHeader; +} + +rpc! { + pub enum RestoreMagicBackupResult { + Success, + Error { error: String }, + } +} + +impl Notification for route::RestoreMagicBackupComplete { + type Error = Error; + type Payload = RestoreMagicBackupResult; +} diff --git a/ql-api/src/benchmark.rs b/ql-api/src/benchmark.rs index 0b6504d4..3409899c 100644 --- a/ql-api/src/benchmark.rs +++ b/ql-api/src/benchmark.rs @@ -1,16 +1,12 @@ -use ql_rpc::{request::Request, Download, RouteId, Subscription}; +use ql_rpc::{request::Request, Download, Subscription}; -use crate::{codec::rpc, Error, Route}; +use crate::{route, Error}; // Echo request-response benchmark -pub struct Echo; - -impl Request for Echo { +impl Request for route::Echo { type Error = Error; type Request = EchoRequest; type Response = EchoResponse; - - const ROUTE: RouteId = Route::Echo.id(); } rpc! { @@ -26,14 +22,10 @@ rpc! { } // Byte-stream subscription benchmark -pub struct BytesBenchmark; - -impl Subscription for BytesBenchmark { +impl Subscription for route::BytesBenchmark { type Error = Error; type Event = BenchmarkEvent; type Request = BenchmarkRequest; - - const ROUTE: ql_rpc::RouteId = Route::BytesBenchmark.id(); } rpc! { @@ -49,14 +41,11 @@ rpc! { } // Raw-body download benchmark -pub struct DownloadBenchmark; - -impl Download for DownloadBenchmark { +impl Download for route::DownloadBenchmark { type Error = Error; type Request = DownloadBenchmarkRequest; type ResponseHeader = DownloadBenchmarkHeader; - - const ROUTE: RouteId = Route::DownloadBenchmark.id(); + type PartHeader = DownloadBenchmarkPartHeader; } rpc! { @@ -70,3 +59,7 @@ rpc! { pub hash: Vec, } } + +rpc! { + pub struct DownloadBenchmarkPartHeader {} +} diff --git a/ql-api/src/bitcoin.rs b/ql-api/src/bitcoin.rs new file mode 100644 index 00000000..9235247d --- /dev/null +++ b/ql-api/src/bitcoin.rs @@ -0,0 +1,78 @@ +use ql_rpc::{Notification, Request}; + +use crate::{route, Error}; + +rpc! { + pub struct SignPsbtRequest { + pub account_id: String, + pub psbt: Vec, + } +} + +rpc! { + pub enum SignPsbtResponse { + Signed { psbt: Vec }, + Rejected, + Error { error: String }, + } +} + +impl Request for route::SignPsbt { + type Error = Error; + type Request = SignPsbtRequest; + type Response = SignPsbtResponse; +} + +rpc! { + pub struct BroadcastTransactionRequest { + pub account_id: String, + pub psbt: Vec, + } +} + +rpc! { + pub enum BroadcastTransactionResponse { + Broadcast { txid: String }, + Error { error: String }, + } +} + +impl Request for route::BroadcastTransaction { + type Error = Error; + type Request = BroadcastTransactionRequest; + type Response = BroadcastTransactionResponse; +} + +rpc! { + pub struct AccountUpdatePayload { + pub account_id: String, + pub update: Vec, + } +} + +impl Notification for route::AccountUpdate { + type Error = Error; + type Payload = AccountUpdatePayload; +} + +rpc! { + pub struct ActiveSeedFingerprint { + pub fingerprint: String, + pub has_passphrase: bool, + } +} + +impl Notification for route::PassportActiveSeedFingerprint { + type Error = Error; + type Payload = ActiveSeedFingerprint; +} + +rpc! { + pub struct PassportActiveSeedFingerprintRequest {} +} + +impl Request for route::GetPassportActiveSeedFingerprint { + type Error = Error; + type Request = PassportActiveSeedFingerprintRequest; + type Response = ActiveSeedFingerprint; +} diff --git a/ql-api/src/bootstrap.rs b/ql-api/src/bootstrap.rs deleted file mode 100644 index 9f050d4b..00000000 --- a/ql-api/src/bootstrap.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::{codec::rpc, Error, Route}; - -pub struct BootstrapQlv1; - -impl ql_rpc::Request for BootstrapQlv1 { - const ROUTE: ql_rpc::RouteId = Route::BootstrapQlv1.id(); - type Error = Error; - type Request = BootstrapRequest; - type Response = BootstrapResponse; -} - -rpc! { - pub struct BootstrapRequest { - pub xid_document: Vec, - } -} - -rpc! { - pub struct BootstrapResponse { - pub xid_document: Vec, - } -} diff --git a/ql-api/src/codec.rs b/ql-api/src/codec.rs index 7d5323f2..eda25685 100644 --- a/ql-api/src/codec.rs +++ b/ql-api/src/codec.rs @@ -5,40 +5,6 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::Error; -macro_rules! impl_codec { - ($ty:ty) => { - impl ql_rpc::RpcCodec for $ty { - type Error = crate::Error; - - fn encode_value(&self, out: &mut B) { - $crate::codec::encode_cbor(self, out); - } - - fn decode_value(bytes: &mut B) -> Result { - $crate::codec::decode_cbor(bytes) - } - } - }; -} - -pub(crate) use impl_codec; - -macro_rules! rpc { - ($(#[$attr:meta])* $vis:vis struct $name:ident;) => { - compile_error!("rpc! does not support unit structs"); - }; - ($(#[$attr:meta])* $vis:vis struct $name:ident $($body:tt)+) => { - #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] - $(#[$attr])* - #[cfg_attr(feature = "frb", flutter_rust_bridge::frb(non_opaque))] - $vis struct $name $($body)+ - - $crate::codec::impl_codec!($name); - }; -} - -pub(crate) use rpc; - pub fn encode_cbor(value: &T, out: &mut B) where T: Serialize, diff --git a/ql-api/src/firmware.rs b/ql-api/src/firmware.rs new file mode 100644 index 00000000..bf0b49e0 --- /dev/null +++ b/ql-api/src/firmware.rs @@ -0,0 +1,87 @@ +use ql_rpc::{Download, Notification, Request}; + +use crate::{route, Error}; + +rpc! { + pub struct FirmwareUpdateCheckRequest { + pub current_version: String, + } +} + +rpc! { + pub enum FirmwareUpdateCheckResponse { + Available(FirmwareUpdateAvailable), + NotAvailable, + } +} + +rpc! { + pub struct FirmwareUpdateAvailable { + pub version: String, + pub changelog: String, + pub timestamp: u32, + pub total_size: u32, + pub patch_count: u8, + } +} + +impl Request for route::FirmwareUpdateCheck { + type Error = Error; + type Request = FirmwareUpdateCheckRequest; + type Response = FirmwareUpdateCheckResponse; +} + +rpc! { + pub struct FirmwareDownloadRequest { + pub current_version: String, + } +} + +rpc! { + pub enum FirmwareDownloadHeader { + Available(FirmwareUpdateAvailable), + NotAvailable, + Error { error: String }, + } +} + +rpc! { + pub struct FirmwareDownloadPartHeader { + pub patch_name: String, + pub size_bytes: u64, + } +} + +impl Download for route::FirmwareDownload { + type Error = Error; + type Request = FirmwareDownloadRequest; + type ResponseHeader = FirmwareDownloadHeader; + type PartHeader = FirmwareDownloadPartHeader; +} + +rpc! { + pub enum FirmwareInstallEvent { + Installing, + Rebooting, + Success { + installed_version: String, + }, + Error { + error: String, + stage: InstallErrorStage, + }, + } +} + +rpc! { + pub enum InstallErrorStage { + Download, + Verify, + Install, + } +} + +impl Notification for route::FirmwareInstallStatus { + type Error = Error; + type Payload = FirmwareInstallEvent; +} diff --git a/ql-api/src/fx.rs b/ql-api/src/fx.rs new file mode 100644 index 00000000..260cf3d4 --- /dev/null +++ b/ql-api/src/fx.rs @@ -0,0 +1,84 @@ +use ql_rpc::{Notification, Request, Subscription}; + +use crate::{route, Error}; + +rpc! { + #[PartialEq] + pub struct ExchangeRate { + pub currency_code: String, + pub rate: f32, + pub timestamp: u64, + } +} + +rpc! { + #[PartialEq] + pub struct PricePoint { + pub rate: f32, + pub timestamp: u64, + } +} + +rpc! { + pub struct ExchangeRateHistoryRequest { + pub currency_code: String, + } +} + +rpc! { + #[PartialEq] + pub struct ExchangeRateHistory { + pub history: Vec, + pub currency_code: String, + } +} + +impl Notification for route::ExchangeRateUpdate { + type Error = Error; + type Payload = ExchangeRate; +} + +rpc! { + pub struct ExchangeRateSubscriptionRequest { + pub currency_code: String, + } +} + +impl Subscription for route::ExchangeRateSubscription { + type Error = Error; + type Request = ExchangeRateSubscriptionRequest; + type Event = ExchangeRate; +} + +impl Request for route::ExchangeRateHistory { + type Error = Error; + type Request = ExchangeRateHistoryRequest; + type Response = ExchangeRateHistory; +} + +rpc! { + pub struct PassportFiatPreferencePayload { + pub currency_code: String, + } +} + +impl Notification for route::PassportFiatPreference { + type Error = Error; + type Payload = PassportFiatPreferencePayload; +} + +rpc! { + pub struct PassportFiatPreferenceRequest {} +} + +rpc! { + pub struct PassportFiatPreferenceResponse { + pub currency_code: String, + } +} + +impl Request for route::PassportFiatPreferenceRequest { + type Error = Error; + type Request = PassportFiatPreferenceRequest; + type Response = PassportFiatPreferenceResponse; +} diff --git a/ql-api/src/lib.rs b/ql-api/src/lib.rs index 3899a2f7..2f694dd8 100644 --- a/ql-api/src/lib.rs +++ b/ql-api/src/lib.rs @@ -2,37 +2,85 @@ // 2. get app by id // 3. download version? -mod codec; +#[macro_use] +mod macros; + mod app_store; +mod backup; mod benchmark; -mod bootstrap; +mod bitcoin; +mod codec; +mod firmware; +mod fx; +mod onboarding; +mod scv; +mod status; +mod timezone; pub use app_store::*; +pub use backup::*; pub use benchmark::*; -pub use bootstrap::*; +pub use bitcoin::*; +pub use firmware::*; +pub use fx::*; +pub use onboarding::*; +pub use scv::*; +pub use status::*; +pub use timezone::*; pub type Error = ciborium::de::Error; -pub const SERVICE_ID: ql_rpc::ServiceId = ql_rpc::ServiceId([0; 16]); - -#[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Route { - // setup - BootstrapQlv1 = 100, +pub const SERVICE_ID: ql_common::ServiceId = ql_common::ServiceId([0; 16]); +routes! { // app store ListApps = 200, AppDownload = 201, + // status and presence + DeviceStatus = 300, + EnvoyStatus = 301, + Timezone = 303, + + // status and presence + PassportDetails = 402, + PassportDetailsUpdated = 403, + + // security and onboarding + SecurityCheck = 500, + OnboardingStatus = 501, + + // firmware + FirmwareUpdateCheck = 600, + FirmwareDownload = 601, + FirmwareInstallStatus = 602, + + // market data + ExchangeRateUpdate = 700, + ExchangeRateSubscription = 701, + ExchangeRateHistory = 702, + PassportFiatPreference = 703, + PassportFiatPreferenceRequest = 704, + + // bitcoin and wallet + SignPsbt = 800, + BroadcastTransaction = 801, + AccountUpdate = 802, + PassportActiveSeedFingerprint = 803, + GetPassportActiveSeedFingerprint = 804, + + // backup + BackupShard = 900, + RestoreShard = 901, + EnvoyMagicBackupEnabled = 902, + PassportMagicBackupEnabled = 903, + PassportMagicBackupStatus = 904, + UploadMagicBackup = 905, + DownloadMagicBackup = 906, + RestoreMagicBackupComplete = 907, + // debug Echo = 1000, BytesBenchmark = 1001, DownloadBenchmark = 1002, } - -impl Route { - pub const fn id(self) -> ql_rpc::RouteId { - ql_rpc::RouteId::from_u32(self as u32) - } -} diff --git a/ql-api/src/macros.rs b/ql-api/src/macros.rs new file mode 100644 index 00000000..42d0a1f2 --- /dev/null +++ b/ql-api/src/macros.rs @@ -0,0 +1,82 @@ +macro_rules! impl_codec { + ($ty:ty) => { + impl ql_rpc::RpcCodec for $ty { + type Error = crate::Error; + + fn encode_value(&self, out: &mut B) { + $crate::codec::encode_cbor(self, out); + } + + fn decode_value( + bytes: &mut B, + ) -> Result::Error> { + $crate::codec::decode_cbor(bytes) + } + } + }; +} + +macro_rules! routes { + ($($route:ident = $id:literal,)*) => { + #[repr(u32)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub enum Route { + $($route = $id,)* + } + + impl Route { + pub const fn id(self) -> ql_common::RouteId { + ql_common::RouteId::from_u32(self as u32) + } + } + + pub mod route { + $( + #[derive(Debug)] + pub struct $route; + + impl ql_rpc::Route for $route { + const SERVICE: ql_common::ServiceId = crate::SERVICE_ID; + const ROUTE: ql_common::RouteId = super::Route::$route.id(); + } + )* + } + }; +} + +macro_rules! rpc { + ($(#[$attr:meta])* $vis:vis struct $name:ident;) => { + compile_error!("rpc! does not support unit structs"); + }; + (#[PartialEq] $(#[$attr:meta])* $vis:vis struct $name:ident $($body:tt)+) => { + rpc_item!(PartialEq; $(#[$attr])* $vis struct $name $($body)+); + }; + ($(#[$attr:meta])* $vis:vis struct $name:ident $($body:tt)+) => { + rpc_item!(Eq; $(#[$attr])* $vis struct $name $($body)+); + }; + (#[PartialEq] $(#[$attr:meta])* $vis:vis enum $name:ident $($body:tt)+) => { + rpc_item!(PartialEq; $(#[$attr])* $vis enum $name $($body)+); + }; + ($(#[$attr:meta])* $vis:vis enum $name:ident $($body:tt)+) => { + rpc_item!(Eq; $(#[$attr])* $vis enum $name $($body)+); + }; +} + +macro_rules! rpc_item { + (PartialEq; $(#[$attr:meta])* $vis:vis $kind:ident $name:ident $($body:tt)+) => { + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] + $(#[$attr])* + #[cfg_attr(feature = "frb", flutter_rust_bridge::frb(non_opaque))] + $vis $kind $name $($body)+ + + impl_codec!($name); + }; + (Eq; $(#[$attr:meta])* $vis:vis $kind:ident $name:ident $($body:tt)+) => { + #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] + $(#[$attr])* + #[cfg_attr(feature = "frb", flutter_rust_bridge::frb(non_opaque))] + $vis $kind $name $($body)+ + + impl_codec!($name); + }; +} diff --git a/ql-api/src/onboarding.rs b/ql-api/src/onboarding.rs new file mode 100644 index 00000000..24ce0263 --- /dev/null +++ b/ql-api/src/onboarding.rs @@ -0,0 +1,30 @@ +use ql_rpc::Notification; + +use crate::{route, Error}; + +rpc! { + pub enum OnboardingState { + SecurityChecked, + SecurityCheckFailed, + FirmwareUpdateScreen, + SecuringDevice, + DeviceSecured, + WalletCreationScreen, + CreatingWallet, + WalletCreated, + MagicBackupScreen, + CreatingMagicBackup, + MagicBackupCreated, + CreatingManualBackup, + CreatingKeycardBackup, + WritingDownSeedWords, + ConnectingWallet, + WalletConnected, + Completed, + } +} + +impl Notification for route::OnboardingStatus { + type Error = Error; + type Payload = OnboardingState; +} diff --git a/ql-api/src/scv.rs b/ql-api/src/scv.rs new file mode 100644 index 00000000..e4d60761 --- /dev/null +++ b/ql-api/src/scv.rs @@ -0,0 +1,44 @@ +use ql_rpc::Duplex; + +use crate::{route, Error}; + +rpc! { + pub struct ChallengeRequest { + pub data: Vec, + } +} + +rpc! { + pub enum ChallengeResponseResult { + Success { data: Vec }, + Error { error: String }, + } +} + +rpc! { + pub enum VerificationResult { + Success, + Error { error: String }, + Failure, + } +} + +rpc! { + pub enum SecurityCheckRemoteEvent { + ChallengeRequest(ChallengeRequest), + VerificationResult(VerificationResult), + } +} + +rpc! { + pub enum SecurityCheckPassportEvent { + Start, + ChallengeResponse(ChallengeResponseResult), + } +} + +impl Duplex for route::SecurityCheck { + type Error = Error; + type InitiatorEvent = SecurityCheckPassportEvent; + type ResponderEvent = SecurityCheckRemoteEvent; +} diff --git a/ql-api/src/status.rs b/ql-api/src/status.rs new file mode 100644 index 00000000..8a1ba269 --- /dev/null +++ b/ql-api/src/status.rs @@ -0,0 +1,52 @@ +use ql_rpc::{Notification, Request}; + +use crate::{route, Error}; + +rpc! { + pub enum PassportModel { + Gen1, + Gen2, + Prime, + } +} + +rpc! { + pub struct PassportFirmwareVersion(pub String); +} + +rpc! { + pub struct PassportSerial(pub String); +} + +rpc! { + pub enum PassportColor { + Light, + Dark, + } +} + +rpc! { + pub struct PassportDetailsRequest {} +} + +rpc! { + pub struct PassportDetailsResponse { + pub device_name: String, + pub model: PassportModel, + pub firmware_version: PassportFirmwareVersion, + pub serial: PassportSerial, + pub color: PassportColor, + pub onboarding_complete: bool, + } +} + +impl Request for route::PassportDetails { + type Error = Error; + type Request = PassportDetailsRequest; + type Response = PassportDetailsResponse; +} + +impl Notification for route::PassportDetailsUpdated { + type Error = Error; + type Payload = PassportDetailsResponse; +} diff --git a/ql-api/src/timezone.rs b/ql-api/src/timezone.rs new file mode 100644 index 00000000..4a44b924 --- /dev/null +++ b/ql-api/src/timezone.rs @@ -0,0 +1,20 @@ +use ql_rpc::Request; + +use crate::{route, Error}; + +rpc! { + pub struct TimezoneRequest {} +} + +rpc! { + pub struct TimezoneResponse { + pub offset_minutes: i32, + pub zone: String, + } +} + +impl Request for route::Timezone { + type Error = Error; + type Request = TimezoneRequest; + type Response = TimezoneResponse; +} From 7c8e2385985914a7eb214352c987648dce4398ef Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Wed, 20 May 2026 09:56:20 -0400 Subject: [PATCH 03/17] ql-api: current time --- ql-api/src/lib.rs | 5 +++-- ql-api/src/{timezone.rs => time.rs} | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) rename ql-api/src/{timezone.rs => time.rs} (54%) diff --git a/ql-api/src/lib.rs b/ql-api/src/lib.rs index 2f694dd8..065c9f9f 100644 --- a/ql-api/src/lib.rs +++ b/ql-api/src/lib.rs @@ -15,7 +15,7 @@ mod fx; mod onboarding; mod scv; mod status; -mod timezone; +mod time; pub use app_store::*; pub use backup::*; @@ -26,7 +26,7 @@ pub use fx::*; pub use onboarding::*; pub use scv::*; pub use status::*; -pub use timezone::*; +pub use time::*; pub type Error = ciborium::de::Error; @@ -41,6 +41,7 @@ routes! { DeviceStatus = 300, EnvoyStatus = 301, Timezone = 303, + CurrentTime = 304, // status and presence PassportDetails = 402, diff --git a/ql-api/src/timezone.rs b/ql-api/src/time.rs similarity index 54% rename from ql-api/src/timezone.rs rename to ql-api/src/time.rs index 4a44b924..112bd550 100644 --- a/ql-api/src/timezone.rs +++ b/ql-api/src/time.rs @@ -18,3 +18,19 @@ impl Request for route::Timezone { type Request = TimezoneRequest; type Response = TimezoneResponse; } + +rpc! { + pub struct CurrentTimeRequest {} +} + +rpc! { + pub struct CurrentTimeResponse { + pub server_time_millis: u64, + } +} + +impl Request for route::CurrentTime { + type Error = Error; + type Request = CurrentTimeRequest; + type Response = CurrentTimeResponse; +} From 8dd0007a894129ce3bacc28a0945ba42ff40f499 Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Mon, 29 Jun 2026 18:51:38 -0400 Subject: [PATCH 04/17] ql-api: separate service ids --- ql-api/src/bitcoin.rs | 5 ++ ql-api/src/lib.rs | 114 +++++++++++++++++++++++------------------- ql-api/src/macros.rs | 36 +++++++------ 3 files changed, 87 insertions(+), 68 deletions(-) diff --git a/ql-api/src/bitcoin.rs b/ql-api/src/bitcoin.rs index 9235247d..5443e9d8 100644 --- a/ql-api/src/bitcoin.rs +++ b/ql-api/src/bitcoin.rs @@ -55,6 +55,11 @@ impl Notification for route::AccountUpdate { type Payload = AccountUpdatePayload; } +impl Notification for route::PassportAccountUpdate { + type Error = Error; + type Payload = AccountUpdatePayload; +} + rpc! { pub struct ActiveSeedFingerprint { pub fingerprint: String, diff --git a/ql-api/src/lib.rs b/ql-api/src/lib.rs index 065c9f9f..d9225baa 100644 --- a/ql-api/src/lib.rs +++ b/ql-api/src/lib.rs @@ -30,58 +30,68 @@ pub use time::*; pub type Error = ciborium::de::Error; -pub const SERVICE_ID: ql_common::ServiceId = ql_common::ServiceId([0; 16]); +pub const FOUNDATION_APP: ql_common::ServiceId = ql_common::ServiceId([1; 16]); +pub const PASSPORT: ql_common::ServiceId = ql_common::ServiceId([2; 16]); routes! { - // app store - ListApps = 200, - AppDownload = 201, - - // status and presence - DeviceStatus = 300, - EnvoyStatus = 301, - Timezone = 303, - CurrentTime = 304, - - // status and presence - PassportDetails = 402, - PassportDetailsUpdated = 403, - - // security and onboarding - SecurityCheck = 500, - OnboardingStatus = 501, - - // firmware - FirmwareUpdateCheck = 600, - FirmwareDownload = 601, - FirmwareInstallStatus = 602, - - // market data - ExchangeRateUpdate = 700, - ExchangeRateSubscription = 701, - ExchangeRateHistory = 702, - PassportFiatPreference = 703, - PassportFiatPreferenceRequest = 704, - - // bitcoin and wallet - SignPsbt = 800, - BroadcastTransaction = 801, - AccountUpdate = 802, - PassportActiveSeedFingerprint = 803, - GetPassportActiveSeedFingerprint = 804, - - // backup - BackupShard = 900, - RestoreShard = 901, - EnvoyMagicBackupEnabled = 902, - PassportMagicBackupEnabled = 903, - PassportMagicBackupStatus = 904, - UploadMagicBackup = 905, - DownloadMagicBackup = 906, - RestoreMagicBackupComplete = 907, - - // debug - Echo = 1000, - BytesBenchmark = 1001, - DownloadBenchmark = 1002, + FOUNDATION_APP => FoundationAppRoute { + // app store + ListApps = 200, + AppDownload = 201, + + // time + Timezone = 303, + CurrentTime = 304, + + // status and presence + PassportDetailsUpdated = 403, + + // security and onboarding + SecurityCheck = 500, + OnboardingStatus = 501, + + // firmware + FirmwareUpdateCheck = 600, + FirmwareDownload = 601, + FirmwareInstallStatus = 602, + + // market data + ExchangeRateUpdate = 700, + ExchangeRateSubscription = 701, + ExchangeRateHistory = 702, + PassportFiatPreference = 703, + + // bitcoin and wallet + BroadcastTransaction = 801, + AccountUpdate = 802, + PassportActiveSeedFingerprint = 803, + + // backup + BackupShard = 900, + RestoreShard = 901, + EnvoyMagicBackupEnabled = 902, + PassportMagicBackupEnabled = 903, + PassportMagicBackupStatus = 904, + UploadMagicBackup = 905, + DownloadMagicBackup = 906, + RestoreMagicBackupComplete = 907, + + // debug + Echo = 1000, + BytesBenchmark = 1001, + DownloadBenchmark = 1002, + } + + PASSPORT => PassportRoute { + // status and presence + PassportDetails = 402, + + // market data + PassportFiatPreferenceRequest = 704, + + // bitcoin and wallet + SignPsbt = 800, + PassportAccountUpdate = 802, + GetPassportActiveSeedFingerprint = 804, + } } diff --git a/ql-api/src/macros.rs b/ql-api/src/macros.rs index 42d0a1f2..bc804f5c 100644 --- a/ql-api/src/macros.rs +++ b/ql-api/src/macros.rs @@ -17,28 +17,32 @@ macro_rules! impl_codec { } macro_rules! routes { - ($($route:ident = $id:literal,)*) => { - #[repr(u32)] - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub enum Route { - $($route = $id,)* - } + ($($service:ident => $route_enum:ident { $($route:ident = $id:literal,)* })*) => { + $( + #[repr(u32)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub enum $route_enum { + $($route = $id,)* + } - impl Route { - pub const fn id(self) -> ql_common::RouteId { - ql_common::RouteId::from_u32(self as u32) + impl $route_enum { + pub const fn id(self) -> ql_common::RouteId { + ql_common::RouteId::from_u32(self as u32) + } } - } + )* pub mod route { $( - #[derive(Debug)] - pub struct $route; + $( + #[derive(Debug)] + pub struct $route; - impl ql_rpc::Route for $route { - const SERVICE: ql_common::ServiceId = crate::SERVICE_ID; - const ROUTE: ql_common::RouteId = super::Route::$route.id(); - } + impl ql_rpc::Route for $route { + const SERVICE: ql_common::ServiceId = crate::$service; + const ROUTE: ql_common::RouteId = super::$route_enum::$route.id(); + } + )* )* } }; From 05e28a2b2831dc1c06568562b36fc6ba6319fec9 Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Mon, 29 Jun 2026 18:55:47 -0400 Subject: [PATCH 05/17] ql-api: revamp --- Cargo.lock | 10 +++ Cargo.toml | 2 + ql-api/Cargo.toml | 1 + ql-api/src/app_store.rs | 61 ++++++++--------- ql-api/src/backup.rs | 137 ++++++++++++++++++++------------------- ql-api/src/benchmark.rs | 65 ------------------- ql-api/src/bitcoin.rs | 90 ++++++++++++++----------- ql-api/src/codec.rs | 66 ++++++++++++++++++- ql-api/src/debug.rs | 101 +++++++++++++++++++++++++++++ ql-api/src/firmware.rs | 110 ++++++++++++++++++------------- ql-api/src/fx.rs | 61 ++++++----------- ql-api/src/key.rs | 85 ++++++++++++++++++++++++ ql-api/src/lib.rs | 131 ++++++++++++------------------------- ql-api/src/macros.rs | 84 ++++++++++++++++++------ ql-api/src/onboarding.rs | 23 ++++--- ql-api/src/scv.rs | 22 +++++-- ql-api/src/settings.rs | 103 +++++++++++++++++++++++++++++ ql-api/src/status.rs | 52 --------------- ql-api/src/time.rs | 25 +++---- ql-keyos/Cargo.toml | 14 ++++ ql-keyos/src/lib.rs | 122 ++++++++++++++++++++++++++++++++++ 21 files changed, 887 insertions(+), 478 deletions(-) delete mode 100644 ql-api/src/benchmark.rs create mode 100644 ql-api/src/debug.rs create mode 100644 ql-api/src/key.rs create mode 100644 ql-api/src/settings.rs delete mode 100644 ql-api/src/status.rs create mode 100644 ql-keyos/Cargo.toml create mode 100644 ql-keyos/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 33639331..80af5db8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1373,6 +1373,7 @@ dependencies = [ "ciborium", "flutter_rust_bridge", "ql-common", + "ql-keyos", "ql-rpc", "serde", ] @@ -1404,6 +1405,15 @@ dependencies = [ "ql-wire", ] +[[package]] +name = "ql-keyos" +version = "0.1.0" +dependencies = [ + "bytes", + "ql-common", + "serde", +] + [[package]] name = "ql-rpc" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index a61e9d98..ca1022e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "btp", "ql-codec", "ql-common", + "ql-keyos", "ql-api", "ql-fsm", "ql-rpc", @@ -28,5 +29,6 @@ btp = { path = "btp" } ql-codec = { path = "ql-codec" } ql-common = { path = "ql-common" } ql-fsm = { path = "ql-fsm" } +ql-keyos = { path = "ql-keyos" } ql-rpc = { path = "ql-rpc" } ql-wire = { path = "ql-wire" } diff --git a/ql-api/Cargo.toml b/ql-api/Cargo.toml index c81fbefb..3792167c 100644 --- a/ql-api/Cargo.toml +++ b/ql-api/Cargo.toml @@ -8,6 +8,7 @@ license = "Proprietary" [dependencies] ql-common = { workspace = true } +ql-keyos = { workspace = true, features = ["serde"] } ql-rpc = { workspace = true } bytes = { workspace = true } ciborium = "0.2.2" diff --git a/ql-api/src/app_store.rs b/ql-api/src/app_store.rs index 629b6a80..2463a3c0 100644 --- a/ql-api/src/app_store.rs +++ b/ql-api/src/app_store.rs @@ -1,72 +1,65 @@ use ql_rpc::{Download, Request}; -use crate::{route, Error}; +use crate::{Empty, Error}; + +// COMMON TYPES rpc! { #[derive(Copy)] pub struct AppVersion(pub u32, pub u32, pub u32); } -// -// LIST APPS -// - rpc! { - pub struct ListAppsRequest { - // + pub struct AppDetails { + pub name: String, + pub icon: String, // SVG? + pub developer_id: Vec, + pub app_id: Vec, } } -rpc! { - pub struct ListAppsResponse { - pub apps: Vec, +// APP ROUTES + +// SERVICE ROUTES + +service_routes! { + crate::service_id::APP_STORE => { + RequestListApps: Request = 1, + DownloadApp: Download = 2, } } rpc! { - pub struct AppDetails { - pub name: String, - pub icon: String, // SVG? - pub developer_id: Vec, - pub app_id: Vec, + pub struct ListAppsResponse { + pub apps: Vec, } } -impl Request for route::ListApps { +impl Request for RequestListApps { type Error = Error; - type Request = ListAppsRequest; + type Request = Empty; type Response = ListAppsResponse; } -// -// APP DOWNLOAD -// -// download a single tar file that contains: -// - app.elf -// - manifest.json - rpc! { - pub struct AppDownloadRequest { + pub struct DownloadAppParams { pub version: Option, } } rpc! { - pub struct AppDownloadHeader { + pub struct DownloadAppHeader { pub version: String, } } rpc! { - pub struct AppDownloadPartHeader {} + pub struct DownloadAppPartHeader {} } -impl Download for route::AppDownload { +impl Download for DownloadApp { type Error = Error; - - type Request = AppDownloadRequest; - - type ResponseHeader = AppDownloadHeader; - - type PartHeader = AppDownloadPartHeader; + type Request = DownloadAppParams; + type ResponseHeader = DownloadAppHeader; + type PartHeader = DownloadAppPartHeader; } diff --git a/ql-api/src/backup.rs b/ql-api/src/backup.rs index e2c32a46..f49a7996 100644 --- a/ql-api/src/backup.rs +++ b/ql-api/src/backup.rs @@ -1,6 +1,8 @@ -use ql_rpc::{Download, Notification, Request, Upload}; +use ql_rpc::{Download, Request, Subscription, Upload}; -use crate::{route, Error}; +use crate::{Empty, Error}; + +// COMMON TYPES rpc! { pub struct Shard(pub Vec); @@ -10,76 +12,95 @@ rpc! { pub struct SeedFingerprint(pub [u8; 32]); } -rpc! { - pub struct BackupShardRequest { - pub shard: Shard, +// APP ROUTES + +app_routes! { + crate::app_id::BACKUP => { + SubscribePassportMagicBackupEnabled: Subscription = 1, + SubscribeMagicBackupRestoreCompleted: Subscription = 2, } } rpc! { - pub enum BackupShardResponse { - Success, - Error { error: String }, + pub struct PassportMagicBackupEnabled { + pub enabled: bool, + pub seed_fingerprint: SeedFingerprint, } } -impl Request for route::BackupShard { +impl Subscription for SubscribePassportMagicBackupEnabled { type Error = Error; - type Request = BackupShardRequest; - type Response = BackupShardResponse; -} - -rpc! { - pub struct RestoreShardRequest { - pub seed_fingerprint: SeedFingerprint, - pub timestamp: Option, - } + type Request = Empty; + type Event = PassportMagicBackupEnabled; } rpc! { - pub enum RestoreShardResponse { - Success { shard: Shard }, + pub enum MagicBackupRestoreResult { + Success, Error { error: String }, - NotFound, } } -impl Request for route::RestoreShard { +impl Subscription for SubscribeMagicBackupRestoreCompleted { type Error = Error; - type Request = RestoreShardRequest; - type Response = RestoreShardResponse; + type Request = Empty; + type Event = MagicBackupRestoreResult; +} + +// SERVICE ROUTES + +service_routes! { + crate::service_id::BACKUP => { + RequestBackupShard: Request = 1, + RequestRestoreShard: Request = 2, + RequestPassportMagicBackupStatus: Request = 3, + UploadMagicBackup: Upload = 4, + DownloadMagicBackup: Download = 5, + } } rpc! { - pub struct EnvoyMagicBackupEnabledRequest {} + pub struct BackupShardParams { + pub shard: Shard, + } } rpc! { - pub struct EnvoyMagicBackupEnabledResponse { - pub enabled: bool, + pub enum BackupShardResponse { + Success, + Error { error: String }, } } -impl Request for route::EnvoyMagicBackupEnabled { +impl Request for RequestBackupShard { type Error = Error; - type Request = EnvoyMagicBackupEnabledRequest; - type Response = EnvoyMagicBackupEnabledResponse; + type Request = BackupShardParams; + type Response = BackupShardResponse; } rpc! { - pub struct PassportMagicBackupEnabledPayload { - pub enabled: bool, + pub struct RestoreShardParams { pub seed_fingerprint: SeedFingerprint, + pub timestamp: Option, + } +} + +rpc! { + pub enum RestoreShardResponse { + Success { shard: Shard }, + Error { error: String }, + NotFound, } } -impl Notification for route::PassportMagicBackupEnabled { +impl Request for RequestRestoreShard { type Error = Error; - type Payload = PassportMagicBackupEnabledPayload; + type Request = RestoreShardParams; + type Response = RestoreShardResponse; } rpc! { - pub struct PassportMagicBackupStatusRequest { + pub struct PassportMagicBackupStatusParams { pub seed_fingerprint: SeedFingerprint, pub timestamp: Option, } @@ -91,14 +112,14 @@ rpc! { } } -impl Request for route::PassportMagicBackupStatus { +impl Request for RequestPassportMagicBackupStatus { type Error = Error; - type Request = PassportMagicBackupStatusRequest; + type Request = PassportMagicBackupStatusParams; type Response = PassportMagicBackupStatusResponse; } rpc! { - pub struct UploadMagicBackupRequest { + pub struct UploadMagicBackupParams { pub seed_fingerprint: SeedFingerprint, pub total_size: Option, pub hash: [u8; 32], @@ -106,7 +127,7 @@ rpc! { } rpc! { - pub enum UploadMagicBackupResult { + pub enum UploadMagicBackupResponse { Success, Error { error: String }, } @@ -116,27 +137,19 @@ rpc! { pub struct UploadMagicBackupPartHeader {} } -impl Upload for route::UploadMagicBackup { +impl Upload for UploadMagicBackup { type Error = Error; - type Request = UploadMagicBackupRequest; + type Request = UploadMagicBackupParams; type PartHeader = UploadMagicBackupPartHeader; - type Response = UploadMagicBackupResult; + type Response = UploadMagicBackupResponse; } rpc! { - pub struct DownloadMagicBackupRequest { + pub struct DownloadMagicBackupParams { pub seed_fingerprint: SeedFingerprint, } } -rpc! { - pub enum DownloadMagicBackupHeader { - Found(BackupMetadata), - NotFound, - Error { error: String }, - } -} - rpc! { pub struct BackupMetadata { pub total_size: Option, @@ -144,24 +157,16 @@ rpc! { } rpc! { - pub struct DownloadMagicBackupPartHeader {} -} - -impl Download for route::DownloadMagicBackup { - type Error = Error; - type Request = DownloadMagicBackupRequest; - type ResponseHeader = DownloadMagicBackupHeader; - type PartHeader = DownloadMagicBackupPartHeader; -} - -rpc! { - pub enum RestoreMagicBackupResult { - Success, + pub enum DownloadMagicBackupHeader { + Found(BackupMetadata), + NotFound, Error { error: String }, } } -impl Notification for route::RestoreMagicBackupComplete { +impl Download for DownloadMagicBackup { type Error = Error; - type Payload = RestoreMagicBackupResult; + type Request = DownloadMagicBackupParams; + type ResponseHeader = DownloadMagicBackupHeader; + type PartHeader = Empty; } diff --git a/ql-api/src/benchmark.rs b/ql-api/src/benchmark.rs deleted file mode 100644 index 3409899c..00000000 --- a/ql-api/src/benchmark.rs +++ /dev/null @@ -1,65 +0,0 @@ -use ql_rpc::{request::Request, Download, Subscription}; - -use crate::{route, Error}; - -// Echo request-response benchmark -impl Request for route::Echo { - type Error = Error; - type Request = EchoRequest; - type Response = EchoResponse; -} - -rpc! { - pub struct EchoRequest { - pub message: String, - } -} - -rpc! { - pub struct EchoResponse { - pub message: String, - } -} - -// Byte-stream subscription benchmark -impl Subscription for route::BytesBenchmark { - type Error = Error; - type Event = BenchmarkEvent; - type Request = BenchmarkRequest; -} - -rpc! { - pub struct BenchmarkRequest { - pub length: u32, - } -} - -rpc! { - pub struct BenchmarkEvent { - pub bytes: Vec, - } -} - -// Raw-body download benchmark -impl Download for route::DownloadBenchmark { - type Error = Error; - type Request = DownloadBenchmarkRequest; - type ResponseHeader = DownloadBenchmarkHeader; - type PartHeader = DownloadBenchmarkPartHeader; -} - -rpc! { - pub struct DownloadBenchmarkRequest { - pub length: u64, - } -} - -rpc! { - pub struct DownloadBenchmarkHeader { - pub hash: Vec, - } -} - -rpc! { - pub struct DownloadBenchmarkPartHeader {} -} diff --git a/ql-api/src/bitcoin.rs b/ql-api/src/bitcoin.rs index 5443e9d8..8c2ab063 100644 --- a/ql-api/src/bitcoin.rs +++ b/ql-api/src/bitcoin.rs @@ -1,83 +1,97 @@ -use ql_rpc::{Notification, Request}; +use ql_rpc::{Request, Subscription}; -use crate::{route, Error}; +use crate::{Empty, Error}; -rpc! { - pub struct SignPsbtRequest { - pub account_id: String, - pub psbt: Vec, - } -} +// COMMON TYPES rpc! { - pub enum SignPsbtResponse { - Signed { psbt: Vec }, - Rejected, - Error { error: String }, + pub struct ActiveSeedFingerprint { + pub fingerprint: String, + pub has_passphrase: bool, } } -impl Request for route::SignPsbt { - type Error = Error; - type Request = SignPsbtRequest; - type Response = SignPsbtResponse; +// APP ROUTES + +app_routes! { + crate::app_id::BITCOIN => { + RequestSignPsbt: Request = 1, + SubscribeAccountUpdated: Subscription = 2, + RequestPassportActiveSeedFingerprint: Request = 3, + SubscribePassportActiveSeedFingerprint: Subscription = 4, + } } rpc! { - pub struct BroadcastTransactionRequest { + pub struct SignPsbtParams { pub account_id: String, pub psbt: Vec, } } rpc! { - pub enum BroadcastTransactionResponse { - Broadcast { txid: String }, + pub enum SignPsbtResponse { + Signed { psbt: Vec }, + Rejected, Error { error: String }, } } -impl Request for route::BroadcastTransaction { +impl Request for RequestSignPsbt { type Error = Error; - type Request = BroadcastTransactionRequest; - type Response = BroadcastTransactionResponse; + type Request = SignPsbtParams; + type Response = SignPsbtResponse; } rpc! { - pub struct AccountUpdatePayload { + pub struct AccountUpdatedEvent { pub account_id: String, pub update: Vec, } } -impl Notification for route::AccountUpdate { +impl Subscription for SubscribeAccountUpdated { type Error = Error; - type Payload = AccountUpdatePayload; + type Request = Empty; + type Event = AccountUpdatedEvent; } -impl Notification for route::PassportAccountUpdate { +impl Subscription for SubscribePassportActiveSeedFingerprint { type Error = Error; - type Payload = AccountUpdatePayload; + type Request = Empty; + type Event = ActiveSeedFingerprint; } -rpc! { - pub struct ActiveSeedFingerprint { - pub fingerprint: String, - pub has_passphrase: bool, +impl Request for RequestPassportActiveSeedFingerprint { + type Error = Error; + type Request = Empty; + type Response = ActiveSeedFingerprint; +} + +// SERVICE ROUTES + +service_routes! { + crate::service_id::BITCOIN => { + RequestBroadcastTransaction: Request = 1, } } -impl Notification for route::PassportActiveSeedFingerprint { - type Error = Error; - type Payload = ActiveSeedFingerprint; +rpc! { + pub struct BroadcastTransactionParams { + pub account_id: String, + pub psbt: Vec, + } } rpc! { - pub struct PassportActiveSeedFingerprintRequest {} + pub enum BroadcastTransactionResponse { + Broadcast { txid: String }, + Error { error: String }, + } } -impl Request for route::GetPassportActiveSeedFingerprint { +impl Request for RequestBroadcastTransaction { type Error = Error; - type Request = PassportActiveSeedFingerprintRequest; - type Response = ActiveSeedFingerprint; + type Request = BroadcastTransactionParams; + type Response = BroadcastTransactionResponse; } diff --git a/ql-api/src/codec.rs b/ql-api/src/codec.rs index eda25685..a90ce607 100644 --- a/ql-api/src/codec.rs +++ b/ql-api/src/codec.rs @@ -1,10 +1,27 @@ use std::io; use bytes::{Buf, BufMut}; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use crate::Error; +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "frb", flutter_rust_bridge::frb(non_opaque))] +pub struct Empty {} + +impl ql_rpc::RpcCodec for Empty { + type Error = Error; + + fn encode_value(&self, out: &mut B) { + encode_cbor(&self, out); + } + + fn decode_value(bytes: &mut B) -> Result { + decode_cbor::(bytes)?; + Ok(Self {}) + } +} + pub fn encode_cbor(value: &T, out: &mut B) where T: Serialize, @@ -54,3 +71,50 @@ impl io::Read for BufReader<'_, B> { Ok(len) } } + +#[cfg(test)] +mod tests { + use bytes::BytesMut; + + use super::*; + + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct OldHeader {} + + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct NewHeader { + field: Option, + } + + #[test] + fn cbor_empty_struct_can_evolve_with_optional_fields() { + let mut old_bytes = BytesMut::new(); + encode_cbor(&OldHeader {}, &mut old_bytes); + + let decoded_new: NewHeader = decode_cbor(&mut old_bytes.freeze()).unwrap(); + assert_eq!(decoded_new, NewHeader { field: None }); + + let mut new_bytes = BytesMut::new(); + encode_cbor(&NewHeader { field: Some(7) }, &mut new_bytes); + + let decoded_old: OldHeader = decode_cbor(&mut new_bytes.freeze()).unwrap(); + assert_eq!(decoded_old, OldHeader {}); + } + + #[test] + fn empty_codec_can_evolve_into_cbor_fields() { + let mut empty_bytes = BytesMut::new(); + ql_rpc::RpcCodec::encode_value(&Empty {}, &mut empty_bytes); + assert!(!empty_bytes.is_empty()); + + let decoded_new: NewHeader = decode_cbor(&mut empty_bytes.freeze()).unwrap(); + assert_eq!(decoded_new, NewHeader { field: None }); + + let mut new_bytes = BytesMut::new(); + encode_cbor(&NewHeader { field: Some(7) }, &mut new_bytes); + + let decoded_empty = + ::decode_value(&mut new_bytes.freeze()).unwrap(); + assert_eq!(decoded_empty, Empty {}); + } +} diff --git a/ql-api/src/debug.rs b/ql-api/src/debug.rs new file mode 100644 index 00000000..93e35eb9 --- /dev/null +++ b/ql-api/src/debug.rs @@ -0,0 +1,101 @@ +use ql_rpc::{request::Request, Download, Subscription}; + +use crate::Error; + +// APP ROUTES + +app_routes! { + crate::app_id::DEBUG => { + RequestPassportEcho: Request = 1, + SubscribePassportBytesBenchmark: Subscription = 2, + DownloadPassportBenchmark: Download = 3, + } +} + +rpc! { + pub struct EchoParams { + pub message: String, + } +} + +rpc! { + pub struct EchoResponse { + pub message: String, + } +} + +rpc! { + pub struct BytesBenchmarkParams { + pub length: u32, + } +} + +rpc! { + pub struct BytesBenchmarkEvent { + pub bytes: Vec, + } +} + +rpc! { + pub struct DownloadBenchmarkParams { + pub length: u64, + } +} + +rpc! { + pub struct DownloadBenchmarkHeader { + pub hash: Vec, + } +} + +rpc! { + pub struct DownloadBenchmarkPartHeader {} +} + +impl Request for RequestPassportEcho { + type Error = Error; + type Request = EchoParams; + type Response = EchoResponse; +} + +impl Subscription for SubscribePassportBytesBenchmark { + type Error = Error; + type Event = BytesBenchmarkEvent; + type Request = BytesBenchmarkParams; +} + +impl Download for DownloadPassportBenchmark { + type Error = Error; + type Request = DownloadBenchmarkParams; + type ResponseHeader = DownloadBenchmarkHeader; + type PartHeader = DownloadBenchmarkPartHeader; +} + +// SERVICE ROUTES + +service_routes! { + crate::service_id::DEBUG => { + RequestEcho: Request = 1, + SubscribeBytesBenchmark: Subscription = 2, + DownloadBenchmark: Download = 3, + } +} + +impl Request for RequestEcho { + type Error = Error; + type Request = EchoParams; + type Response = EchoResponse; +} + +impl Subscription for SubscribeBytesBenchmark { + type Error = Error; + type Event = BytesBenchmarkEvent; + type Request = BytesBenchmarkParams; +} + +impl Download for DownloadBenchmark { + type Error = Error; + type Request = DownloadBenchmarkParams; + type ResponseHeader = DownloadBenchmarkHeader; + type PartHeader = DownloadBenchmarkPartHeader; +} diff --git a/ql-api/src/firmware.rs b/ql-api/src/firmware.rs index bf0b49e0..c884d8f0 100644 --- a/ql-api/src/firmware.rs +++ b/ql-api/src/firmware.rs @@ -1,87 +1,107 @@ -use ql_rpc::{Download, Notification, Request}; +use ql_rpc::{Download, Request, Subscription}; -use crate::{route, Error}; +use crate::{Empty, Error}; + +// COMMON TYPES rpc! { - pub struct FirmwareUpdateCheckRequest { - pub current_version: String, + pub struct FirmwareUpdateAvailable { + pub version: String, + pub changelog: String, + pub timestamp: u32, + pub total_size: u32, + pub patch_count: u8, + } +} + +// APP ROUTES + +app_routes! { + crate::app_id::UPDATE => { + SubscribeFirmwareInstallStatus: Subscription = 1, } } rpc! { - pub enum FirmwareUpdateCheckResponse { - Available(FirmwareUpdateAvailable), - NotAvailable, + pub enum InstallErrorStage { + Download, + Verify, + Install, } } rpc! { - pub struct FirmwareUpdateAvailable { - pub version: String, - pub changelog: String, - pub timestamp: u32, - pub total_size: u32, - pub patch_count: u8, + pub enum FirmwareInstallEvent { + Installing, + Rebooting, + Success { + installed_version: String, + }, + Error { + error: String, + stage: InstallErrorStage, + }, } } -impl Request for route::FirmwareUpdateCheck { +impl Subscription for SubscribeFirmwareInstallStatus { type Error = Error; - type Request = FirmwareUpdateCheckRequest; - type Response = FirmwareUpdateCheckResponse; + type Request = Empty; + type Event = FirmwareInstallEvent; +} + +// SERVICE ROUTES + +service_routes! { + crate::service_id::FIRMWARE => { + RequestCheckFirmwareUpdate: Request = 1, + DownloadFirmware: Download = 2, + } } rpc! { - pub struct FirmwareDownloadRequest { + pub struct FirmwareUpdateCheckParams { pub current_version: String, } } rpc! { - pub enum FirmwareDownloadHeader { + pub enum FirmwareUpdateCheckResponse { Available(FirmwareUpdateAvailable), NotAvailable, - Error { error: String }, } } -rpc! { - pub struct FirmwareDownloadPartHeader { - pub patch_name: String, - pub size_bytes: u64, - } +impl Request for RequestCheckFirmwareUpdate { + type Error = Error; + type Request = FirmwareUpdateCheckParams; + type Response = FirmwareUpdateCheckResponse; } -impl Download for route::FirmwareDownload { - type Error = Error; - type Request = FirmwareDownloadRequest; - type ResponseHeader = FirmwareDownloadHeader; - type PartHeader = FirmwareDownloadPartHeader; +rpc! { + pub struct DownloadFirmwareParams { + pub current_version: String, + } } rpc! { - pub enum FirmwareInstallEvent { - Installing, - Rebooting, - Success { - installed_version: String, - }, - Error { - error: String, - stage: InstallErrorStage, - }, + pub enum DownloadFirmwareHeader { + Available(FirmwareUpdateAvailable), + NotAvailable, + Error { error: String }, } } rpc! { - pub enum InstallErrorStage { - Download, - Verify, - Install, + pub struct DownloadFirmwarePartHeader { + pub patch_name: String, + pub size_bytes: u64, } } -impl Notification for route::FirmwareInstallStatus { +impl Download for DownloadFirmware { type Error = Error; - type Payload = FirmwareInstallEvent; + type Request = DownloadFirmwareParams; + type ResponseHeader = DownloadFirmwareHeader; + type PartHeader = DownloadFirmwarePartHeader; } diff --git a/ql-api/src/fx.rs b/ql-api/src/fx.rs index 260cf3d4..aa561fdc 100644 --- a/ql-api/src/fx.rs +++ b/ql-api/src/fx.rs @@ -1,6 +1,8 @@ -use ql_rpc::{Notification, Request, Subscription}; +use ql_rpc::{Request, Subscription}; -use crate::{route, Error}; +use crate::Error; + +// COMMON TYPES rpc! { #[PartialEq] @@ -19,66 +21,45 @@ rpc! { } } -rpc! { - pub struct ExchangeRateHistoryRequest { - pub currency_code: String, - } -} +// APP ROUTES -rpc! { - #[PartialEq] - pub struct ExchangeRateHistory { - pub history: Vec, - pub currency_code: String, - } -} +// SERVICE ROUTES -impl Notification for route::ExchangeRateUpdate { - type Error = Error; - type Payload = ExchangeRate; +service_routes! { + crate::service_id::FX => { + SubscribeExchangeRate: Subscription = 1, + RequestExchangeRateHistory: Request = 2, + } } rpc! { - pub struct ExchangeRateSubscriptionRequest { + pub struct ExchangeRateParams { pub currency_code: String, } } -impl Subscription for route::ExchangeRateSubscription { +impl Subscription for SubscribeExchangeRate { type Error = Error; - type Request = ExchangeRateSubscriptionRequest; + type Request = ExchangeRateParams; type Event = ExchangeRate; } -impl Request for route::ExchangeRateHistory { - type Error = Error; - type Request = ExchangeRateHistoryRequest; - type Response = ExchangeRateHistory; -} - rpc! { - pub struct PassportFiatPreferencePayload { + pub struct ExchangeRateHistoryParams { pub currency_code: String, } } -impl Notification for route::PassportFiatPreference { - type Error = Error; - type Payload = PassportFiatPreferencePayload; -} - rpc! { - pub struct PassportFiatPreferenceRequest {} -} - -rpc! { - pub struct PassportFiatPreferenceResponse { + #[PartialEq] + pub struct ExchangeRateHistory { + pub history: Vec, pub currency_code: String, } } -impl Request for route::PassportFiatPreferenceRequest { +impl Request for RequestExchangeRateHistory { type Error = Error; - type Request = PassportFiatPreferenceRequest; - type Response = PassportFiatPreferenceResponse; + type Request = ExchangeRateHistoryParams; + type Response = ExchangeRateHistory; } diff --git a/ql-api/src/key.rs b/ql-api/src/key.rs new file mode 100644 index 00000000..2646dceb --- /dev/null +++ b/ql-api/src/key.rs @@ -0,0 +1,85 @@ +use ql_keyos::{AppId, RouteId, ServiceId}; + +pub mod app_id { + use ql_keyos::AppId; + + pub const SYSTEM: AppId = AppId::from_hex("0x6f732f716c0000000000000000000000"); + pub const ONBOARDING: AppId = AppId::from_hex("0xdac5321775d449c11bc9c90f38067f8f"); + pub const BACKUP: AppId = AppId::from_hex("0xe19f16fcd9610bca7d026b4673f1cb06"); + pub const UPDATE: AppId = AppId::from_hex("0x6F732F75706461746500000000000000"); + pub const BITCOIN: AppId = AppId::from_hex("0x426974636f696e2057616c6c65740000"); + pub const DEBUG: AppId = AppId::from_hex("0x8ec0b4ea99704f9f973f5d7b7a3294b2"); +} + +pub mod service_id { + use ql_keyos::ServiceId; + + pub const APP_STORE: ServiceId = ServiceId::from_u32(1); + pub const BACKUP: ServiceId = ServiceId::from_u32(2); + pub const BITCOIN: ServiceId = ServiceId::from_u32(3); + pub const DEBUG: ServiceId = ServiceId::from_u32(4); + pub const FIRMWARE: ServiceId = ServiceId::from_u32(5); + pub const FX: ServiceId = ServiceId::from_u32(6); + pub const SCV: ServiceId = ServiceId::from_u32(7); + pub const TIME: ServiceId = ServiceId::from_u32(8); +} + +/// Route key for a remote peer rpc +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ServiceRouteKey { + pub service_id: ServiceId, + pub route_id: RouteId, +} + +impl ql_rpc::RpcRouteKey for ServiceRouteKey { + fn encoded_len(&self) -> usize { + self.service_id.0.size() + self.route_id.0.size() + } + + fn encode(&self, out: &mut W) { + self.service_id.0.write_bytes(|bytes| out.put_slice(bytes)); + self.route_id.0.write_bytes(|bytes| out.put_slice(bytes)); + } + + fn decode(bytes: &[u8]) -> Option { + let (service_id, bytes) = ql_common::VarInt::decode_bytes(bytes)?; + let (route_id, rest) = ql_common::VarInt::decode_bytes(bytes)?; + rest.is_empty().then_some(Self { + service_id: ServiceId(service_id), + route_id: RouteId(route_id), + }) + } +} + +impl ql_keyos::ServiceTargetKey for ServiceRouteKey { + fn service_id(&self) -> ServiceId { + self.service_id + } +} + +/// Route key for an keyos handled rpc +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AppRouteKey { + pub app_id: AppId, + pub route_id: RouteId, +} + +impl ql_rpc::RpcRouteKey for AppRouteKey { + fn encoded_len(&self) -> usize { + AppId::SIZE + self.route_id.0.size() + } + + fn encode(&self, out: &mut W) { + out.put_slice(&self.app_id.0); + self.route_id.0.write_bytes(|bytes| out.put_slice(bytes)); + } + + fn decode(bytes: &[u8]) -> Option { + let app_id = AppId(bytes.get(..AppId::SIZE)?.try_into().ok()?); + let (route_id, rest) = ql_common::VarInt::decode_bytes(&bytes[AppId::SIZE..])?; + rest.is_empty().then_some(Self { + app_id, + route_id: RouteId(route_id), + }) + } +} diff --git a/ql-api/src/lib.rs b/ql-api/src/lib.rs index d9225baa..839676d9 100644 --- a/ql-api/src/lib.rs +++ b/ql-api/src/lib.rs @@ -1,97 +1,46 @@ -// 1. list catalog of apps (request/response) -// 2. get app by id -// 3. download version? - #[macro_use] mod macros; -mod app_store; -mod backup; -mod benchmark; -mod bitcoin; -mod codec; -mod firmware; -mod fx; -mod onboarding; -mod scv; -mod status; -mod time; - -pub use app_store::*; -pub use backup::*; -pub use benchmark::*; -pub use bitcoin::*; -pub use firmware::*; -pub use fx::*; -pub use onboarding::*; -pub use scv::*; -pub use status::*; -pub use time::*; +pub mod app_store; +pub mod backup; +pub mod bitcoin; +pub mod codec; +pub mod debug; +pub mod firmware; +pub mod fx; +pub mod key; +pub mod onboarding; +pub mod scv; +pub mod settings; +pub mod time; + +pub use self::{ + app_store::*, backup::*, bitcoin::*, codec::Empty, debug::*, firmware::*, fx::*, key::*, + onboarding::*, scv::*, settings::*, time::*, +}; pub type Error = ciborium::de::Error; -pub const FOUNDATION_APP: ql_common::ServiceId = ql_common::ServiceId([1; 16]); -pub const PASSPORT: ql_common::ServiceId = ql_common::ServiceId([2; 16]); - -routes! { - FOUNDATION_APP => FoundationAppRoute { - // app store - ListApps = 200, - AppDownload = 201, - - // time - Timezone = 303, - CurrentTime = 304, - - // status and presence - PassportDetailsUpdated = 403, - - // security and onboarding - SecurityCheck = 500, - OnboardingStatus = 501, - - // firmware - FirmwareUpdateCheck = 600, - FirmwareDownload = 601, - FirmwareInstallStatus = 602, - - // market data - ExchangeRateUpdate = 700, - ExchangeRateSubscription = 701, - ExchangeRateHistory = 702, - PassportFiatPreference = 703, - - // bitcoin and wallet - BroadcastTransaction = 801, - AccountUpdate = 802, - PassportActiveSeedFingerprint = 803, - - // backup - BackupShard = 900, - RestoreShard = 901, - EnvoyMagicBackupEnabled = 902, - PassportMagicBackupEnabled = 903, - PassportMagicBackupStatus = 904, - UploadMagicBackup = 905, - DownloadMagicBackup = 906, - RestoreMagicBackupComplete = 907, - - // debug - Echo = 1000, - BytesBenchmark = 1001, - DownloadBenchmark = 1002, - } - - PASSPORT => PassportRoute { - // status and presence - PassportDetails = 402, - - // market data - PassportFiatPreferenceRequest = 704, - - // bitcoin and wallet - SignPsbt = 800, - PassportAccountUpdate = 802, - GetPassportActiveSeedFingerprint = 804, - } -} +// naming scheme +// Request: +// - Request{Thing} +// - {Thing}Params +// - {Thing}Response +// Subscription: +// - Subscribe{Thing} +// - {Thing}Params +// - {Thing}Event +// Download: +// - Download{Thing} +// - Download{Thing}Params +// - Download{Thing}Header +// - Download{Thing}PartHeader +// Upload: +// - Upload{Thing} +// - Upload{Thing}Params +// - Upload{Thing}PartHeader +// - Upload{Thing}Response +// Duplex: +// - {Action}{Thing} +// - {Action}{Thing}InitiatorEvent +// - {Action}{Thing}ResponderEvent diff --git a/ql-api/src/macros.rs b/ql-api/src/macros.rs index bc804f5c..e700964f 100644 --- a/ql-api/src/macros.rs +++ b/ql-api/src/macros.rs @@ -16,35 +16,77 @@ macro_rules! impl_codec { }; } -macro_rules! routes { - ($($service:ident => $route_enum:ident { $($route:ident = $id:literal,)* })*) => { - $( - #[repr(u32)] - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub enum $route_enum { - $($route = $id,)* +macro_rules! duplicate_route_check { + ($($id:literal,)*) => { + const _: () = { + const IDS: &[u32] = &[$($id,)*]; + let mut i = 0; + while i < IDS.len() { + let mut j = i + 1; + while j < IDS.len() { + assert!(IDS[i] != IDS[j], "duplicate route id"); + j += 1; + } + i += 1; } + }; + }; +} + +macro_rules! assert_route_impl { + ($route:ident: $kind:ident) => { + const _: () = { + struct Assert(core::marker::PhantomData); + let _ = Assert::<$route>(core::marker::PhantomData); + }; + }; +} + +macro_rules! service_routes { + ($service_id:expr => { $($route:ident: $kind:ident = $id:literal,)* }) => { + duplicate_route_check!($($id,)*); + + $( + #[derive(Debug)] + pub struct $route; + + impl ql_rpc::Route for $route { + type Key = crate::ServiceRouteKey; - impl $route_enum { - pub const fn id(self) -> ql_common::RouteId { - ql_common::RouteId::from_u32(self as u32) + fn key() -> Self::Key { + crate::ServiceRouteKey { + service_id: $service_id, + route_id: ql_keyos::RouteId::from_u32($id), + } } } + + assert_route_impl!($route: $kind); )* + }; +} + +macro_rules! app_routes { + ($app_id:expr => { $($route:ident: $kind:ident = $id:literal,)* }) => { + duplicate_route_check!($($id,)*); + + $( + #[derive(Debug)] + pub struct $route; - pub mod route { - $( - $( - #[derive(Debug)] - pub struct $route; + impl ql_rpc::Route for $route { + type Key = crate::AppRouteKey; - impl ql_rpc::Route for $route { - const SERVICE: ql_common::ServiceId = crate::$service; - const ROUTE: ql_common::RouteId = super::$route_enum::$route.id(); + fn key() -> Self::Key { + crate::AppRouteKey { + app_id: $app_id, + route_id: ql_keyos::RouteId::from_u32($id), } - )* - )* - } + } + } + + assert_route_impl!($route: $kind); + )* }; } diff --git a/ql-api/src/onboarding.rs b/ql-api/src/onboarding.rs index 24ce0263..1a5ac9e6 100644 --- a/ql-api/src/onboarding.rs +++ b/ql-api/src/onboarding.rs @@ -1,11 +1,17 @@ -use ql_rpc::Notification; +use ql_rpc::Subscription; -use crate::{route, Error}; +use crate::{Empty, Error}; + +// APP ROUTES + +app_routes! { + crate::app_id::ONBOARDING => { + SubscribeOnboardingStatus: Subscription = 1, + } +} rpc! { pub enum OnboardingState { - SecurityChecked, - SecurityCheckFailed, FirmwareUpdateScreen, SecuringDevice, DeviceSecured, @@ -18,13 +24,14 @@ rpc! { CreatingManualBackup, CreatingKeycardBackup, WritingDownSeedWords, - ConnectingWallet, - WalletConnected, Completed, } } -impl Notification for route::OnboardingStatus { +impl Subscription for SubscribeOnboardingStatus { type Error = Error; - type Payload = OnboardingState; + type Request = Empty; + type Event = OnboardingState; } + +// SERVICE ROUTES diff --git a/ql-api/src/scv.rs b/ql-api/src/scv.rs index e4d60761..4c7e2a8d 100644 --- a/ql-api/src/scv.rs +++ b/ql-api/src/scv.rs @@ -1,6 +1,16 @@ use ql_rpc::Duplex; -use crate::{route, Error}; +use crate::Error; + +// APP ROUTES + +// SERVICE ROUTES + +service_routes! { + crate::service_id::SCV => { + RunSecurityCheck: Duplex = 1, + } +} rpc! { pub struct ChallengeRequest { @@ -24,21 +34,21 @@ rpc! { } rpc! { - pub enum SecurityCheckRemoteEvent { + pub enum RunSecurityCheckResponderEvent { ChallengeRequest(ChallengeRequest), VerificationResult(VerificationResult), } } rpc! { - pub enum SecurityCheckPassportEvent { + pub enum RunSecurityCheckInitiatorEvent { Start, ChallengeResponse(ChallengeResponseResult), } } -impl Duplex for route::SecurityCheck { +impl Duplex for RunSecurityCheck { type Error = Error; - type InitiatorEvent = SecurityCheckPassportEvent; - type ResponderEvent = SecurityCheckRemoteEvent; + type InitiatorEvent = RunSecurityCheckInitiatorEvent; + type ResponderEvent = RunSecurityCheckResponderEvent; } diff --git a/ql-api/src/settings.rs b/ql-api/src/settings.rs new file mode 100644 index 00000000..c09e452d --- /dev/null +++ b/ql-api/src/settings.rs @@ -0,0 +1,103 @@ +use ql_keyos::AppId; +use ql_rpc::{Request, Subscription}; + +use crate::{Empty, Error}; + +// COMMON TYPES + +rpc! { + pub enum PassportModel { + Gen1, + Gen2, + Prime, + } +} + +rpc! { + pub struct PassportFirmwareVersion(pub String); +} + +rpc! { + pub struct PassportSerial(pub String); +} + +rpc! { + pub enum PassportColor { + Light, + Dark, + } +} + +// APP ROUTES + +app_routes! { + crate::app_id::SYSTEM => { + RequestPassportDetails: Request = 1, + SubscribePassportDetails: Subscription = 2, + RequestPassportFiatPreference: Request = 3, + SubscribePassportFiatPreference: Subscription = 4, + SubscribeAppActivity: Subscription = 5, + } +} + +rpc! { + pub struct PassportDetailsResponse { + pub device_name: String, + pub model: PassportModel, + pub firmware_version: PassportFirmwareVersion, + pub serial: PassportSerial, + pub color: PassportColor, + pub onboarding_complete: bool, + } +} + +impl Request for RequestPassportDetails { + type Error = Error; + type Request = Empty; + type Response = PassportDetailsResponse; +} + +impl Subscription for SubscribePassportDetails { + type Error = Error; + type Request = Empty; + type Event = PassportDetailsResponse; +} + +rpc! { + pub struct PassportFiatPreferenceResponse { + pub currency_code: String, + } +} + +impl Request for RequestPassportFiatPreference { + type Error = Error; + type Request = Empty; + type Response = PassportFiatPreferenceResponse; +} + +rpc! { + pub struct PassportFiatPreferenceEvent { + pub currency_code: String, + } +} + +impl Subscription for SubscribePassportFiatPreference { + type Error = Error; + type Request = Empty; + type Event = PassportFiatPreferenceEvent; +} + +rpc! { + pub enum AppActivityEvent { + Opened(AppId), + Closed(AppId), + } +} + +impl Subscription for SubscribeAppActivity { + type Error = Error; + type Request = Empty; + type Event = AppActivityEvent; +} + +// SERVICE ROUTES diff --git a/ql-api/src/status.rs b/ql-api/src/status.rs deleted file mode 100644 index 8a1ba269..00000000 --- a/ql-api/src/status.rs +++ /dev/null @@ -1,52 +0,0 @@ -use ql_rpc::{Notification, Request}; - -use crate::{route, Error}; - -rpc! { - pub enum PassportModel { - Gen1, - Gen2, - Prime, - } -} - -rpc! { - pub struct PassportFirmwareVersion(pub String); -} - -rpc! { - pub struct PassportSerial(pub String); -} - -rpc! { - pub enum PassportColor { - Light, - Dark, - } -} - -rpc! { - pub struct PassportDetailsRequest {} -} - -rpc! { - pub struct PassportDetailsResponse { - pub device_name: String, - pub model: PassportModel, - pub firmware_version: PassportFirmwareVersion, - pub serial: PassportSerial, - pub color: PassportColor, - pub onboarding_complete: bool, - } -} - -impl Request for route::PassportDetails { - type Error = Error; - type Request = PassportDetailsRequest; - type Response = PassportDetailsResponse; -} - -impl Notification for route::PassportDetailsUpdated { - type Error = Error; - type Payload = PassportDetailsResponse; -} diff --git a/ql-api/src/time.rs b/ql-api/src/time.rs index 112bd550..3033cf67 100644 --- a/ql-api/src/time.rs +++ b/ql-api/src/time.rs @@ -1,9 +1,16 @@ use ql_rpc::Request; -use crate::{route, Error}; +use crate::{Empty, Error}; -rpc! { - pub struct TimezoneRequest {} +// APP ROUTES + +// SERVICE ROUTES + +service_routes! { + crate::service_id::TIME => { + RequestTimezone: Request = 1, + RequestCurrentTime: Request = 2, + } } rpc! { @@ -13,24 +20,20 @@ rpc! { } } -impl Request for route::Timezone { +impl Request for RequestTimezone { type Error = Error; - type Request = TimezoneRequest; + type Request = Empty; type Response = TimezoneResponse; } -rpc! { - pub struct CurrentTimeRequest {} -} - rpc! { pub struct CurrentTimeResponse { pub server_time_millis: u64, } } -impl Request for route::CurrentTime { +impl Request for RequestCurrentTime { type Error = Error; - type Request = CurrentTimeRequest; + type Request = Empty; type Response = CurrentTimeResponse; } diff --git a/ql-keyos/Cargo.toml b/ql-keyos/Cargo.toml new file mode 100644 index 00000000..aa193c89 --- /dev/null +++ b/ql-keyos/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ql-keyos" +version = "0.1.0" +edition = "2021" +description = "KeyOS QuantumLink addressing primitives" +license = "Proprietary" + +[dependencies] +bytes = { workspace = true } +ql-common = { workspace = true } +serde = { version = "1.0", features = ["derive"], optional = true } + +[features] +serde = ["dep:serde"] diff --git a/ql-keyos/src/lib.rs b/ql-keyos/src/lib.rs new file mode 100644 index 00000000..b1d8bb4c --- /dev/null +++ b/ql-keyos/src/lib.rs @@ -0,0 +1,122 @@ +//! KeyOS addressing primitives for QuantumLink + +use bytes::BufMut; +pub use ql_common::VarInt; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[repr(transparent)] +pub struct AppId(pub [u8; Self::SIZE]); + +impl AppId { + pub const SIZE: usize = 16; + + pub const fn from_hex(hex: &str) -> Self { + const fn hex_nibble(byte: u8) -> u8 { + match byte { + b'0'..=b'9' => byte - b'0', + b'a'..=b'f' => byte - b'a' + 10, + b'A'..=b'F' => byte - b'A' + 10, + _ => panic!("invalid hex digit"), + } + } + let bytes = hex.as_bytes(); + if bytes.len() != Self::SIZE * 2 + 2 || bytes[0] != b'0' || bytes[1] != b'x' { + panic!("app id must be 0x followed by 32 hex digits") + } + + let mut app_id = [0; Self::SIZE]; + let mut i = 0; + while i < Self::SIZE { + app_id[i] = (hex_nibble(bytes[i * 2 + 2]) << 4) | hex_nibble(bytes[i * 2 + 3]); + i += 1; + } + + Self(app_id) + } +} + +ql_common::varint_wrapper!( + /// Identifier for a route within a KeyOS app or service namespace + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "serde", serde(into = "u64", try_from = "u64"))] + RouteId +); + +ql_common::varint_wrapper!( + /// Identifier for a KeyOS service advertised by a peer + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "serde", serde(into = "u64", try_from = "u64"))] + ServiceId +); + +pub trait ServiceTargetKey { + fn service_id(&self) -> ServiceId; +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct PeerMetadata { + /// KeyOS app ids this peer is allowed to open streams toward + pub app_ids: Vec, + /// Service ids this peer advertises for KeyOS-opened streams + pub service_ids: Vec, +} + +impl PeerMetadata { + pub fn write_bytes(&self, out: &mut B) { + write_varint(len_varint(self.app_ids.len()), out); + for app_id in &self.app_ids { + out.put_slice(&app_id.0); + } + + write_varint(len_varint(self.service_ids.len()), out); + for service_id in &self.service_ids { + write_varint(service_id.0, out); + } + } + + pub fn from_bytes(mut bytes: &[u8]) -> Option { + let app_count = read_len(&mut bytes)?; + let app_bytes_len = app_count.checked_mul(AppId::SIZE)?; + if app_bytes_len > bytes.len() { + return None; + } + let mut app_ids = Vec::with_capacity(app_count); + for _ in 0..app_count { + let app_id = AppId(bytes.get(..AppId::SIZE)?.try_into().ok()?); + bytes = &bytes[AppId::SIZE..]; + app_ids.push(app_id); + } + + let service_count = read_len(&mut bytes)?; + if service_count > bytes.len() { + return None; + } + let mut service_ids = Vec::with_capacity(service_count); + for _ in 0..service_count { + let (service_id, rest) = VarInt::decode_bytes(bytes)?; + bytes = rest; + service_ids.push(ServiceId(service_id)); + } + + bytes.is_empty().then_some(Self { + app_ids, + service_ids, + }) + } +} + +fn len_varint(len: usize) -> VarInt { + VarInt::from_u64(len as u64).expect("metadata length exceeds varint bounds") +} + +fn read_len(bytes: &mut &[u8]) -> Option { + let (len, rest) = VarInt::decode_bytes(bytes)?; + *bytes = rest; + usize::try_from(len.into_inner()).ok() +} + +fn write_varint(value: VarInt, out: &mut B) { + value.write_bytes(|bytes| out.put_slice(bytes)); +} From 8ff03d5354a6076c5532831e4965a146c9004aa5 Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Wed, 8 Jul 2026 19:08:11 -0400 Subject: [PATCH 06/17] ql-keyos: simplify RouteKey encodings --- Cargo.lock | 3 -- ql-api/Cargo.toml | 1 - ql-api/src/key.rs | 53 +++++++++++++++---------- ql-api/src/macros.rs | 4 +- ql-keyos/Cargo.toml | 2 - ql-keyos/src/lib.rs | 94 +++++++++++++++++++++++++------------------- 6 files changed, 89 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80af5db8..8ed367cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1372,7 +1372,6 @@ dependencies = [ "bytes", "ciborium", "flutter_rust_bridge", - "ql-common", "ql-keyos", "ql-rpc", "serde", @@ -1409,8 +1408,6 @@ dependencies = [ name = "ql-keyos" version = "0.1.0" dependencies = [ - "bytes", - "ql-common", "serde", ] diff --git a/ql-api/Cargo.toml b/ql-api/Cargo.toml index 3792167c..1509b0f5 100644 --- a/ql-api/Cargo.toml +++ b/ql-api/Cargo.toml @@ -7,7 +7,6 @@ license = "Proprietary" [dependencies] -ql-common = { workspace = true } ql-keyos = { workspace = true, features = ["serde"] } ql-rpc = { workspace = true } bytes = { workspace = true } diff --git a/ql-api/src/key.rs b/ql-api/src/key.rs index 2646dceb..26d49e8d 100644 --- a/ql-api/src/key.rs +++ b/ql-api/src/key.rs @@ -14,14 +14,14 @@ pub mod app_id { pub mod service_id { use ql_keyos::ServiceId; - pub const APP_STORE: ServiceId = ServiceId::from_u32(1); - pub const BACKUP: ServiceId = ServiceId::from_u32(2); - pub const BITCOIN: ServiceId = ServiceId::from_u32(3); - pub const DEBUG: ServiceId = ServiceId::from_u32(4); - pub const FIRMWARE: ServiceId = ServiceId::from_u32(5); - pub const FX: ServiceId = ServiceId::from_u32(6); - pub const SCV: ServiceId = ServiceId::from_u32(7); - pub const TIME: ServiceId = ServiceId::from_u32(8); + pub const APP_STORE: ServiceId = ServiceId(1); + pub const BACKUP: ServiceId = ServiceId(2); + pub const BITCOIN: ServiceId = ServiceId(3); + pub const DEBUG: ServiceId = ServiceId(4); + pub const FIRMWARE: ServiceId = ServiceId(5); + pub const FX: ServiceId = ServiceId(6); + pub const SCV: ServiceId = ServiceId(7); + pub const TIME: ServiceId = ServiceId(8); } /// Route key for a remote peer rpc @@ -33,18 +33,18 @@ pub struct ServiceRouteKey { impl ql_rpc::RpcRouteKey for ServiceRouteKey { fn encoded_len(&self) -> usize { - self.service_id.0.size() + self.route_id.0.size() + size_of::() + size_of::() } fn encode(&self, out: &mut W) { - self.service_id.0.write_bytes(|bytes| out.put_slice(bytes)); - self.route_id.0.write_bytes(|bytes| out.put_slice(bytes)); + out.put_u64(self.service_id.0); + out.put_u32(self.route_id.0); } - fn decode(bytes: &[u8]) -> Option { - let (service_id, bytes) = ql_common::VarInt::decode_bytes(bytes)?; - let (route_id, rest) = ql_common::VarInt::decode_bytes(bytes)?; - rest.is_empty().then_some(Self { + fn decode(mut bytes: &[u8]) -> Option { + let service_id = read_u64(&mut bytes)?; + let route_id = read_u32(&mut bytes)?; + bytes.is_empty().then_some(Self { service_id: ServiceId(service_id), route_id: RouteId(route_id), }) @@ -66,20 +66,33 @@ pub struct AppRouteKey { impl ql_rpc::RpcRouteKey for AppRouteKey { fn encoded_len(&self) -> usize { - AppId::SIZE + self.route_id.0.size() + AppId::SIZE + size_of::() } fn encode(&self, out: &mut W) { out.put_slice(&self.app_id.0); - self.route_id.0.write_bytes(|bytes| out.put_slice(bytes)); + out.put_u32(self.route_id.0); } - fn decode(bytes: &[u8]) -> Option { + fn decode(mut bytes: &[u8]) -> Option { let app_id = AppId(bytes.get(..AppId::SIZE)?.try_into().ok()?); - let (route_id, rest) = ql_common::VarInt::decode_bytes(&bytes[AppId::SIZE..])?; - rest.is_empty().then_some(Self { + bytes = &bytes[AppId::SIZE..]; + let route_id = read_u32(&mut bytes)?; + bytes.is_empty().then_some(Self { app_id, route_id: RouteId(route_id), }) } } + +fn read_u64(bytes: &mut &[u8]) -> Option { + let value = u64::from_be_bytes(bytes.get(..8)?.try_into().ok()?); + *bytes = &bytes[8..]; + Some(value) +} + +fn read_u32(bytes: &mut &[u8]) -> Option { + let value = u32::from_be_bytes(bytes.get(..4)?.try_into().ok()?); + *bytes = &bytes[4..]; + Some(value) +} diff --git a/ql-api/src/macros.rs b/ql-api/src/macros.rs index e700964f..3cff6896 100644 --- a/ql-api/src/macros.rs +++ b/ql-api/src/macros.rs @@ -56,7 +56,7 @@ macro_rules! service_routes { fn key() -> Self::Key { crate::ServiceRouteKey { service_id: $service_id, - route_id: ql_keyos::RouteId::from_u32($id), + route_id: ql_keyos::RouteId($id), } } } @@ -80,7 +80,7 @@ macro_rules! app_routes { fn key() -> Self::Key { crate::AppRouteKey { app_id: $app_id, - route_id: ql_keyos::RouteId::from_u32($id), + route_id: ql_keyos::RouteId($id), } } } diff --git a/ql-keyos/Cargo.toml b/ql-keyos/Cargo.toml index aa193c89..3be66926 100644 --- a/ql-keyos/Cargo.toml +++ b/ql-keyos/Cargo.toml @@ -6,8 +6,6 @@ description = "KeyOS QuantumLink addressing primitives" license = "Proprietary" [dependencies] -bytes = { workspace = true } -ql-common = { workspace = true } serde = { version = "1.0", features = ["derive"], optional = true } [features] diff --git a/ql-keyos/src/lib.rs b/ql-keyos/src/lib.rs index b1d8bb4c..f7a1c83c 100644 --- a/ql-keyos/src/lib.rs +++ b/ql-keyos/src/lib.rs @@ -1,8 +1,5 @@ //! KeyOS addressing primitives for QuantumLink -use bytes::BufMut; -pub use ql_common::VarInt; - #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(transparent)] @@ -12,7 +9,7 @@ impl AppId { pub const SIZE: usize = 16; pub const fn from_hex(hex: &str) -> Self { - const fn hex_nibble(byte: u8) -> u8 { + const fn nibble(byte: u8) -> u8 { match byte { b'0'..=b'9' => byte - b'0', b'a'..=b'f' => byte - b'a' + 10, @@ -28,7 +25,7 @@ impl AppId { let mut app_id = [0; Self::SIZE]; let mut i = 0; while i < Self::SIZE { - app_id[i] = (hex_nibble(bytes[i * 2 + 2]) << 4) | hex_nibble(bytes[i * 2 + 3]); + app_id[i] = (nibble(bytes[i * 2 + 2]) << 4) | nibble(bytes[i * 2 + 3]); i += 1; } @@ -36,24 +33,7 @@ impl AppId { } } -ql_common::varint_wrapper!( - /// Identifier for a route within a KeyOS app or service namespace - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - #[cfg_attr(feature = "serde", serde(into = "u64", try_from = "u64"))] - RouteId -); - -ql_common::varint_wrapper!( - /// Identifier for a KeyOS service advertised by a peer - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - #[cfg_attr(feature = "serde", serde(into = "u64", try_from = "u64"))] - ServiceId -); - -pub trait ServiceTargetKey { - fn service_id(&self) -> ServiceId; -} - +// TODO: version #[derive(Debug, Clone, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct PeerMetadata { @@ -64,19 +44,32 @@ pub struct PeerMetadata { } impl PeerMetadata { - pub fn write_bytes(&self, out: &mut B) { - write_varint(len_varint(self.app_ids.len()), out); + pub fn write_bytes(&self, out: &mut Vec) { + fn len_u64(len: usize) -> u64 { + len.try_into().expect("metadata length exceeds u64") + } + + out.extend_from_slice(&len_u64(self.app_ids.len()).to_be_bytes()); for app_id in &self.app_ids { - out.put_slice(&app_id.0); + out.extend_from_slice(&app_id.0); } - write_varint(len_varint(self.service_ids.len()), out); + out.extend_from_slice(&len_u64(self.service_ids.len()).to_be_bytes()); for service_id in &self.service_ids { - write_varint(service_id.0, out); + out.extend_from_slice(&service_id.0.to_be_bytes()); } } pub fn from_bytes(mut bytes: &[u8]) -> Option { + fn read_len(bytes: &mut &[u8]) -> Option { + read_u64(bytes)?.try_into().ok() + } + fn read_u64(bytes: &mut &[u8]) -> Option { + let value = u64::from_be_bytes(bytes.get(..8)?.try_into().ok()?); + *bytes = &bytes[8..]; + Some(value) + } + let app_count = read_len(&mut bytes)?; let app_bytes_len = app_count.checked_mul(AppId::SIZE)?; if app_bytes_len > bytes.len() { @@ -95,9 +88,7 @@ impl PeerMetadata { } let mut service_ids = Vec::with_capacity(service_count); for _ in 0..service_count { - let (service_id, rest) = VarInt::decode_bytes(bytes)?; - bytes = rest; - service_ids.push(ServiceId(service_id)); + service_ids.push(ServiceId(read_u64(&mut bytes)?)); } bytes.is_empty().then_some(Self { @@ -105,18 +96,41 @@ impl PeerMetadata { service_ids, }) } -} -fn len_varint(len: usize) -> VarInt { - VarInt::from_u64(len as u64).expect("metadata length exceeds varint bounds") + pub fn to_vec(&self) -> Vec { + let mut bytes = Vec::default(); + self.write_bytes(&mut bytes); + bytes + } } -fn read_len(bytes: &mut &[u8]) -> Option { - let (len, rest) = VarInt::decode_bytes(bytes)?; - *bytes = rest; - usize::try_from(len.into_inner()).ok() +macro_rules! wrapper { + ($(#[$attr:meta])* $name:ident, $ty:ty) => { + $(#[$attr])* + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[repr(transparent)] + pub struct $name(pub $ty); + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } + } + }; } -fn write_varint(value: VarInt, out: &mut B) { - value.write_bytes(|bytes| out.put_slice(bytes)); +wrapper!( + /// Identifier for a route within a KeyOS app or service namespace + RouteId, + u32 +); +wrapper!( + /// Identifier for a KeyOS service advertised by a peer + ServiceId, + u64 +); + +pub trait ServiceTargetKey { + fn service_id(&self) -> ServiceId; } From 95e167de97255542445e08ec545c303d6d91b6f1 Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Thu, 9 Jul 2026 08:33:45 -0400 Subject: [PATCH 07/17] ql-api: remove subscription bytes benchmark --- ql-api/src/debug.rs | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/ql-api/src/debug.rs b/ql-api/src/debug.rs index 93e35eb9..276a6e96 100644 --- a/ql-api/src/debug.rs +++ b/ql-api/src/debug.rs @@ -1,4 +1,4 @@ -use ql_rpc::{request::Request, Download, Subscription}; +use ql_rpc::{request::Request, Download}; use crate::Error; @@ -7,8 +7,7 @@ use crate::Error; app_routes! { crate::app_id::DEBUG => { RequestPassportEcho: Request = 1, - SubscribePassportBytesBenchmark: Subscription = 2, - DownloadPassportBenchmark: Download = 3, + DownloadPassportBenchmark: Download = 2, } } @@ -24,18 +23,6 @@ rpc! { } } -rpc! { - pub struct BytesBenchmarkParams { - pub length: u32, - } -} - -rpc! { - pub struct BytesBenchmarkEvent { - pub bytes: Vec, - } -} - rpc! { pub struct DownloadBenchmarkParams { pub length: u64, @@ -58,12 +45,6 @@ impl Request for RequestPassportEcho { type Response = EchoResponse; } -impl Subscription for SubscribePassportBytesBenchmark { - type Error = Error; - type Event = BytesBenchmarkEvent; - type Request = BytesBenchmarkParams; -} - impl Download for DownloadPassportBenchmark { type Error = Error; type Request = DownloadBenchmarkParams; @@ -76,7 +57,6 @@ impl Download for DownloadPassportBenchmark { service_routes! { crate::service_id::DEBUG => { RequestEcho: Request = 1, - SubscribeBytesBenchmark: Subscription = 2, DownloadBenchmark: Download = 3, } } @@ -87,12 +67,6 @@ impl Request for RequestEcho { type Response = EchoResponse; } -impl Subscription for SubscribeBytesBenchmark { - type Error = Error; - type Event = BytesBenchmarkEvent; - type Request = BytesBenchmarkParams; -} - impl Download for DownloadBenchmark { type Error = Error; type Request = DownloadBenchmarkParams; From ae1bb40045865d64e874a52bf7d95c3a81d2925d Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Fri, 10 Jul 2026 09:03:18 -0400 Subject: [PATCH 08/17] ql-api/keyos: use codec + system permissioning --- Cargo.lock | 3 + ql-api/Cargo.toml | 1 + ql-api/src/key.rs | 56 +++++++---------- ql-api/src/lib.rs | 4 +- ql-api/src/{settings.rs => system.rs} | 20 ++++++- ql-keyos/Cargo.toml | 2 + ql-keyos/src/lib.rs | 86 +++++++-------------------- 7 files changed, 72 insertions(+), 100 deletions(-) rename ql-api/src/{settings.rs => system.rs} (82%) diff --git a/Cargo.lock b/Cargo.lock index 8ed367cb..7b08839e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1372,6 +1372,7 @@ dependencies = [ "bytes", "ciborium", "flutter_rust_bridge", + "ql-codec", "ql-keyos", "ql-rpc", "serde", @@ -1408,6 +1409,8 @@ dependencies = [ name = "ql-keyos" version = "0.1.0" dependencies = [ + "bytes", + "ql-codec", "serde", ] diff --git a/ql-api/Cargo.toml b/ql-api/Cargo.toml index 1509b0f5..27f93d57 100644 --- a/ql-api/Cargo.toml +++ b/ql-api/Cargo.toml @@ -7,6 +7,7 @@ license = "Proprietary" [dependencies] +ql-codec = { workspace = true } ql-keyos = { workspace = true, features = ["serde"] } ql-rpc = { workspace = true } bytes = { workspace = true } diff --git a/ql-api/src/key.rs b/ql-api/src/key.rs index 26d49e8d..46c750e9 100644 --- a/ql-api/src/key.rs +++ b/ql-api/src/key.rs @@ -1,9 +1,10 @@ +use ql_codec::{Encode, Reader}; use ql_keyos::{AppId, RouteId, ServiceId}; pub mod app_id { use ql_keyos::AppId; - pub const SYSTEM: AppId = AppId::from_hex("0x6f732f716c0000000000000000000000"); + pub const SYSTEM: AppId = AppId([0; AppId::SIZE]); pub const ONBOARDING: AppId = AppId::from_hex("0xdac5321775d449c11bc9c90f38067f8f"); pub const BACKUP: AppId = AppId::from_hex("0xe19f16fcd9610bca7d026b4673f1cb06"); pub const UPDATE: AppId = AppId::from_hex("0x6F732F75706461746500000000000000"); @@ -33,21 +34,21 @@ pub struct ServiceRouteKey { impl ql_rpc::RpcRouteKey for ServiceRouteKey { fn encoded_len(&self) -> usize { - size_of::() + size_of::() + self.service_id.encoded_len() + self.route_id.encoded_len() } fn encode(&self, out: &mut W) { - out.put_u64(self.service_id.0); - out.put_u32(self.route_id.0); + self.service_id.encode(out); + self.route_id.encode(out); } - fn decode(mut bytes: &[u8]) -> Option { - let service_id = read_u64(&mut bytes)?; - let route_id = read_u32(&mut bytes)?; - bytes.is_empty().then_some(Self { - service_id: ServiceId(service_id), - route_id: RouteId(route_id), - }) + fn decode(bytes: &[u8]) -> Option { + let mut reader = Reader::new(bytes); + let key = Self { + service_id: reader.decode().ok()?, + route_id: reader.decode().ok()?, + }; + reader.is_empty().then_some(key) } } @@ -66,33 +67,20 @@ pub struct AppRouteKey { impl ql_rpc::RpcRouteKey for AppRouteKey { fn encoded_len(&self) -> usize { - AppId::SIZE + size_of::() + self.app_id.encoded_len() + self.route_id.encoded_len() } fn encode(&self, out: &mut W) { - out.put_slice(&self.app_id.0); - out.put_u32(self.route_id.0); + self.app_id.encode(out); + self.route_id.encode(out); } - fn decode(mut bytes: &[u8]) -> Option { - let app_id = AppId(bytes.get(..AppId::SIZE)?.try_into().ok()?); - bytes = &bytes[AppId::SIZE..]; - let route_id = read_u32(&mut bytes)?; - bytes.is_empty().then_some(Self { - app_id, - route_id: RouteId(route_id), - }) + fn decode(bytes: &[u8]) -> Option { + let mut reader = Reader::new(bytes); + let key = Self { + app_id: reader.decode().ok()?, + route_id: reader.decode().ok()?, + }; + reader.is_empty().then_some(key) } } - -fn read_u64(bytes: &mut &[u8]) -> Option { - let value = u64::from_be_bytes(bytes.get(..8)?.try_into().ok()?); - *bytes = &bytes[8..]; - Some(value) -} - -fn read_u32(bytes: &mut &[u8]) -> Option { - let value = u32::from_be_bytes(bytes.get(..4)?.try_into().ok()?); - *bytes = &bytes[4..]; - Some(value) -} diff --git a/ql-api/src/lib.rs b/ql-api/src/lib.rs index 839676d9..47c1f1df 100644 --- a/ql-api/src/lib.rs +++ b/ql-api/src/lib.rs @@ -11,12 +11,12 @@ pub mod fx; pub mod key; pub mod onboarding; pub mod scv; -pub mod settings; +pub mod system; pub mod time; pub use self::{ app_store::*, backup::*, bitcoin::*, codec::Empty, debug::*, firmware::*, fx::*, key::*, - onboarding::*, scv::*, settings::*, time::*, + onboarding::*, scv::*, system::*, time::*, }; pub type Error = ciborium::de::Error; diff --git a/ql-api/src/settings.rs b/ql-api/src/system.rs similarity index 82% rename from ql-api/src/settings.rs rename to ql-api/src/system.rs index c09e452d..fc79e765 100644 --- a/ql-api/src/settings.rs +++ b/ql-api/src/system.rs @@ -1,4 +1,4 @@ -use ql_keyos::AppId; +use ql_keyos::{AppId, PeerPermissions}; use ql_rpc::{Request, Subscription}; use crate::{Empty, Error}; @@ -37,6 +37,7 @@ app_routes! { RequestPassportFiatPreference: Request = 3, SubscribePassportFiatPreference: Subscription = 4, SubscribeAppActivity: Subscription = 5, + RequestUpdatePeerPermissions: Request = 6, } } @@ -100,4 +101,21 @@ impl Subscription for SubscribeAppActivity { type Event = AppActivityEvent; } +rpc! { + pub struct UpdatePeerPermissionsParams(pub PeerPermissions); +} + +rpc! { + pub enum UpdatePeerPermissionsResponse { + Updated, + Denied, + } +} + +impl Request for RequestUpdatePeerPermissions { + type Error = Error; + type Request = UpdatePeerPermissionsParams; + type Response = UpdatePeerPermissionsResponse; +} + // SERVICE ROUTES diff --git a/ql-keyos/Cargo.toml b/ql-keyos/Cargo.toml index 3be66926..123d9c94 100644 --- a/ql-keyos/Cargo.toml +++ b/ql-keyos/Cargo.toml @@ -6,6 +6,8 @@ description = "KeyOS QuantumLink addressing primitives" license = "Proprietary" [dependencies] +bytes = { workspace = true } +ql-codec = { workspace = true } serde = { version = "1.0", features = ["derive"], optional = true } [features] diff --git a/ql-keyos/src/lib.rs b/ql-keyos/src/lib.rs index f7a1c83c..bb281c34 100644 --- a/ql-keyos/src/lib.rs +++ b/ql-keyos/src/lib.rs @@ -1,5 +1,7 @@ //! KeyOS addressing primitives for QuantumLink +use ql_codec::{ByteSlice, Decode, Encode, Reader}; + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(transparent)] @@ -33,77 +35,32 @@ impl AppId { } } -// TODO: version +impl Encode for AppId { + fn encoded_len(&self) -> usize { + Self::SIZE + } + + fn encode(&self, out: &mut W) { + self.0.encode(out); + } +} + +impl Decode for AppId { + fn decode(reader: &mut Reader) -> Result { + Ok(Self(reader.decode()?)) + } +} + +// todo: version #[derive(Debug, Clone, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct PeerMetadata { +pub struct PeerPermissions { /// KeyOS app ids this peer is allowed to open streams toward pub app_ids: Vec, /// Service ids this peer advertises for KeyOS-opened streams pub service_ids: Vec, } -impl PeerMetadata { - pub fn write_bytes(&self, out: &mut Vec) { - fn len_u64(len: usize) -> u64 { - len.try_into().expect("metadata length exceeds u64") - } - - out.extend_from_slice(&len_u64(self.app_ids.len()).to_be_bytes()); - for app_id in &self.app_ids { - out.extend_from_slice(&app_id.0); - } - - out.extend_from_slice(&len_u64(self.service_ids.len()).to_be_bytes()); - for service_id in &self.service_ids { - out.extend_from_slice(&service_id.0.to_be_bytes()); - } - } - - pub fn from_bytes(mut bytes: &[u8]) -> Option { - fn read_len(bytes: &mut &[u8]) -> Option { - read_u64(bytes)?.try_into().ok() - } - fn read_u64(bytes: &mut &[u8]) -> Option { - let value = u64::from_be_bytes(bytes.get(..8)?.try_into().ok()?); - *bytes = &bytes[8..]; - Some(value) - } - - let app_count = read_len(&mut bytes)?; - let app_bytes_len = app_count.checked_mul(AppId::SIZE)?; - if app_bytes_len > bytes.len() { - return None; - } - let mut app_ids = Vec::with_capacity(app_count); - for _ in 0..app_count { - let app_id = AppId(bytes.get(..AppId::SIZE)?.try_into().ok()?); - bytes = &bytes[AppId::SIZE..]; - app_ids.push(app_id); - } - - let service_count = read_len(&mut bytes)?; - if service_count > bytes.len() { - return None; - } - let mut service_ids = Vec::with_capacity(service_count); - for _ in 0..service_count { - service_ids.push(ServiceId(read_u64(&mut bytes)?)); - } - - bytes.is_empty().then_some(Self { - app_ids, - service_ids, - }) - } - - pub fn to_vec(&self) -> Vec { - let mut bytes = Vec::default(); - self.write_bytes(&mut bytes); - bytes - } -} - macro_rules! wrapper { ($(#[$attr:meta])* $name:ident, $ty:ty) => { $(#[$attr])* @@ -131,6 +88,9 @@ wrapper!( u64 ); +ql_codec::varint_wrapper!(RouteId, u32); +ql_codec::varint_wrapper!(ServiceId, u64); + pub trait ServiceTargetKey { fn service_id(&self) -> ServiceId; } From baaa53c404f4dc453d9a474d1c69936211f552da Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Fri, 10 Jul 2026 16:09:19 -0400 Subject: [PATCH 09/17] ql-keyos: move route keys --- Cargo.lock | 1 + ql-api/src/key.rs | 63 +------------------------------------------- ql-api/src/macros.rs | 8 +++--- ql-keyos/Cargo.toml | 1 + ql-keyos/src/lib.rs | 52 ++++++++++++++++++++++++++++++++++-- 5 files changed, 57 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b08839e..2681b91c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1411,6 +1411,7 @@ version = "0.1.0" dependencies = [ "bytes", "ql-codec", + "ql-rpc", "serde", ] diff --git a/ql-api/src/key.rs b/ql-api/src/key.rs index 46c750e9..822ce202 100644 --- a/ql-api/src/key.rs +++ b/ql-api/src/key.rs @@ -1,5 +1,4 @@ -use ql_codec::{Encode, Reader}; -use ql_keyos::{AppId, RouteId, ServiceId}; +pub use ql_keyos::{AppRouteKey, ServiceRouteKey}; pub mod app_id { use ql_keyos::AppId; @@ -24,63 +23,3 @@ pub mod service_id { pub const SCV: ServiceId = ServiceId(7); pub const TIME: ServiceId = ServiceId(8); } - -/// Route key for a remote peer rpc -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ServiceRouteKey { - pub service_id: ServiceId, - pub route_id: RouteId, -} - -impl ql_rpc::RpcRouteKey for ServiceRouteKey { - fn encoded_len(&self) -> usize { - self.service_id.encoded_len() + self.route_id.encoded_len() - } - - fn encode(&self, out: &mut W) { - self.service_id.encode(out); - self.route_id.encode(out); - } - - fn decode(bytes: &[u8]) -> Option { - let mut reader = Reader::new(bytes); - let key = Self { - service_id: reader.decode().ok()?, - route_id: reader.decode().ok()?, - }; - reader.is_empty().then_some(key) - } -} - -impl ql_keyos::ServiceTargetKey for ServiceRouteKey { - fn service_id(&self) -> ServiceId { - self.service_id - } -} - -/// Route key for an keyos handled rpc -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct AppRouteKey { - pub app_id: AppId, - pub route_id: RouteId, -} - -impl ql_rpc::RpcRouteKey for AppRouteKey { - fn encoded_len(&self) -> usize { - self.app_id.encoded_len() + self.route_id.encoded_len() - } - - fn encode(&self, out: &mut W) { - self.app_id.encode(out); - self.route_id.encode(out); - } - - fn decode(bytes: &[u8]) -> Option { - let mut reader = Reader::new(bytes); - let key = Self { - app_id: reader.decode().ok()?, - route_id: reader.decode().ok()?, - }; - reader.is_empty().then_some(key) - } -} diff --git a/ql-api/src/macros.rs b/ql-api/src/macros.rs index 3cff6896..539a4e35 100644 --- a/ql-api/src/macros.rs +++ b/ql-api/src/macros.rs @@ -51,10 +51,10 @@ macro_rules! service_routes { pub struct $route; impl ql_rpc::Route for $route { - type Key = crate::ServiceRouteKey; + type Key = ql_keyos::ServiceRouteKey; fn key() -> Self::Key { - crate::ServiceRouteKey { + ql_keyos::ServiceRouteKey { service_id: $service_id, route_id: ql_keyos::RouteId($id), } @@ -75,10 +75,10 @@ macro_rules! app_routes { pub struct $route; impl ql_rpc::Route for $route { - type Key = crate::AppRouteKey; + type Key = ql_keyos::AppRouteKey; fn key() -> Self::Key { - crate::AppRouteKey { + ql_keyos::AppRouteKey { app_id: $app_id, route_id: ql_keyos::RouteId($id), } diff --git a/ql-keyos/Cargo.toml b/ql-keyos/Cargo.toml index 123d9c94..caa16da1 100644 --- a/ql-keyos/Cargo.toml +++ b/ql-keyos/Cargo.toml @@ -8,6 +8,7 @@ license = "Proprietary" [dependencies] bytes = { workspace = true } ql-codec = { workspace = true } +ql-rpc = { workspace = true } serde = { version = "1.0", features = ["derive"], optional = true } [features] diff --git a/ql-keyos/src/lib.rs b/ql-keyos/src/lib.rs index bb281c34..665d31a6 100644 --- a/ql-keyos/src/lib.rs +++ b/ql-keyos/src/lib.rs @@ -91,6 +91,54 @@ wrapper!( ql_codec::varint_wrapper!(RouteId, u32); ql_codec::varint_wrapper!(ServiceId, u64); -pub trait ServiceTargetKey { - fn service_id(&self) -> ServiceId; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ServiceRouteKey { + pub service_id: ServiceId, + pub route_id: RouteId, +} + +impl ql_rpc::RpcRouteKey for ServiceRouteKey { + fn encoded_len(&self) -> usize { + self.service_id.encoded_len() + self.route_id.encoded_len() + } + + fn encode(&self, out: &mut W) { + self.service_id.encode(out); + self.route_id.encode(out); + } + + fn decode(bytes: &[u8]) -> Option { + let mut reader = Reader::new(bytes); + let key = Self { + service_id: reader.decode().ok()?, + route_id: reader.decode().ok()?, + }; + reader.is_empty().then_some(key) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AppRouteKey { + pub app_id: AppId, + pub route_id: RouteId, +} + +impl ql_rpc::RpcRouteKey for AppRouteKey { + fn encoded_len(&self) -> usize { + self.app_id.encoded_len() + self.route_id.encoded_len() + } + + fn encode(&self, out: &mut W) { + self.app_id.encode(out); + self.route_id.encode(out); + } + + fn decode(bytes: &[u8]) -> Option { + let mut reader = Reader::new(bytes); + let key = Self { + app_id: reader.decode().ok()?, + route_id: reader.decode().ok()?, + }; + reader.is_empty().then_some(key) + } } From 413960acb9074bee898ea55056ea490a6388d968 Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Tue, 14 Jul 2026 10:02:16 -0400 Subject: [PATCH 10/17] ql-api: RequestPeerPermissions --- ql-api/src/system.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ql-api/src/system.rs b/ql-api/src/system.rs index fc79e765..e327d7da 100644 --- a/ql-api/src/system.rs +++ b/ql-api/src/system.rs @@ -37,7 +37,7 @@ app_routes! { RequestPassportFiatPreference: Request = 3, SubscribePassportFiatPreference: Subscription = 4, SubscribeAppActivity: Subscription = 5, - RequestUpdatePeerPermissions: Request = 6, + RequestPeerPermissions: Request = 6, } } @@ -102,20 +102,20 @@ impl Subscription for SubscribeAppActivity { } rpc! { - pub struct UpdatePeerPermissionsParams(pub PeerPermissions); + pub struct PeerPermissionsParams(pub PeerPermissions); } rpc! { - pub enum UpdatePeerPermissionsResponse { + pub enum PeerPermissionsResponse { Updated, Denied, } } -impl Request for RequestUpdatePeerPermissions { +impl Request for RequestPeerPermissions { type Error = Error; - type Request = UpdatePeerPermissionsParams; - type Response = UpdatePeerPermissionsResponse; + type Request = PeerPermissionsParams; + type Response = PeerPermissionsResponse; } // SERVICE ROUTES From 55ee341894a2fcc1c3fdc08e3d5f487a3a70c917 Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Tue, 21 Jul 2026 11:05:33 -0400 Subject: [PATCH 11/17] remove service ids from peer permissions --- ql-keyos/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/ql-keyos/src/lib.rs b/ql-keyos/src/lib.rs index 665d31a6..4601d46b 100644 --- a/ql-keyos/src/lib.rs +++ b/ql-keyos/src/lib.rs @@ -57,8 +57,6 @@ impl Decode for AppId { pub struct PeerPermissions { /// KeyOS app ids this peer is allowed to open streams toward pub app_ids: Vec, - /// Service ids this peer advertises for KeyOS-opened streams - pub service_ids: Vec, } macro_rules! wrapper { From 00f74e6df83cc44642262a0089722b04cdcf963f Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Tue, 21 Jul 2026 09:46:16 -0400 Subject: [PATCH 12/17] ql-api: bootstrap peer bundles --- ql-api/src/system.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/ql-api/src/system.rs b/ql-api/src/system.rs index e327d7da..780751b9 100644 --- a/ql-api/src/system.rs +++ b/ql-api/src/system.rs @@ -38,6 +38,7 @@ app_routes! { SubscribePassportFiatPreference: Subscription = 4, SubscribeAppActivity: Subscription = 5, RequestPeerPermissions: Request = 6, + RequestInstallPeerBundles: Request = 7, } } @@ -118,4 +119,24 @@ impl Request for RequestPeerPermissions { type Response = PeerPermissionsResponse; } +rpc! { + pub struct InstallPeerBundlesParams { + pub router: Vec, + pub peers: Vec>, + } +} + +rpc! { + pub enum InstallPeerBundlesResponse { + Installed, + Rejected, + } +} + +impl Request for RequestInstallPeerBundles { + type Error = Error; + type Request = InstallPeerBundlesParams; + type Response = InstallPeerBundlesResponse; +} + // SERVICE ROUTES From e5cdab37380730866c71dce7a0a3d546aa579828 Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Wed, 2 Sep 2026 12:07:46 -0400 Subject: [PATCH 13/17] ql-relay: add --- Cargo.lock | 23 +++ Cargo.toml | 2 + ql-relay/Cargo.toml | 12 ++ ql-relay/src/lib.rs | 364 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 401 insertions(+) create mode 100644 ql-relay/Cargo.toml create mode 100644 ql-relay/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 2681b91c..06755eea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1415,6 +1415,16 @@ dependencies = [ "serde", ] +[[package]] +name = "ql-relay" +version = "0.1.0" +dependencies = [ + "bytes", + "ql-codec", + "ql-wire", + "tokio", +] + [[package]] name = "ql-rpc" version = "0.1.0" @@ -1750,6 +1760,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "syn" version = "2.0.106" @@ -1855,12 +1875,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", + "bytes", "io-uring", "libc", "mio", "pin-project-lite", "slab", + "socket2", "tokio-macros", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ca1022e3..b376fb2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "ql-codec", "ql-common", "ql-keyos", + "ql-relay", "ql-api", "ql-fsm", "ql-rpc", @@ -30,5 +31,6 @@ ql-codec = { path = "ql-codec" } ql-common = { path = "ql-common" } ql-fsm = { path = "ql-fsm" } ql-keyos = { path = "ql-keyos" } +ql-relay = { path = "ql-relay" } ql-rpc = { path = "ql-rpc" } ql-wire = { path = "ql-wire" } diff --git a/ql-relay/Cargo.toml b/ql-relay/Cargo.toml new file mode 100644 index 00000000..d33a594f --- /dev/null +++ b/ql-relay/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ql-relay" +version = "0.1.0" +edition = "2021" +description = "QuantumLink relay protocol and client" +license.workspace = true + +[dependencies] +bytes = { workspace = true } +ql-codec = { workspace = true } +ql-wire = { workspace = true, features = ["test-utils"] } +tokio = { version = "1", features = ["io-util", "net"] } diff --git a/ql-relay/src/lib.rs b/ql-relay/src/lib.rs new file mode 100644 index 00000000..6859f9fc --- /dev/null +++ b/ql-relay/src/lib.rs @@ -0,0 +1,364 @@ +use std::io; + +use ql_codec::{Decode, Encode}; +use ql_wire::{ + generate_identity, HandshakeId, IkHandshake, PeerBundle, QlHandshakeRecord, RecordHeader, + RecordType, RouteHeader, SessionKey, SoftwareCrypto, TransportParams, +}; +use tokio::net::{ + tcp::{OwnedReadHalf, OwnedWriteHalf}, + TcpStream, ToSocketAddrs, +}; + +use crate::protocol::PacketKind; + +pub const DEFAULT_ADDRESS: &str = "127.0.0.1:7447"; +pub const MAX_RECORD_SIZE: usize = 8 * 1024; + +pub struct Receiver { + tcp: OwnedReadHalf, + key: SessionKey, + counter: u64, +} + +pub struct Sender { + tcp: OwnedWriteHalf, + key: SessionKey, + counter: u64, + buffer: Vec, +} + +pub async fn connect( + address: impl ToSocketAddrs, + router: &PeerBundle, +) -> io::Result<(Receiver, Sender)> { + router.validate(&SoftwareCrypto).map_err(invalid_data)?; + let mut tcp = TcpStream::connect(address).await?; + tcp.set_nodelay(true)?; + + let identity = generate_identity(&SoftwareCrypto, "QL router transport"); + let route = RouteHeader { + sender: identity.qid, + recipient: router.qid, + }; + let mut handshake = IkHandshake::new_ik_initiator( + &SoftwareCrypto, + identity, + router.clone(), + TransportParams::default(), + ); + let mut random = [0; 4]; + ql_wire::QlRandom::fill_random_bytes(&SoftwareCrypto, &mut random); + let handshake_id = HandshakeId::decode_bytes(random.as_slice()).unwrap(); + let request = ql_wire::encode_record_vec( + RecordHeader::new(route, RecordType::Handshake), + &QlHandshakeRecord::Ik1( + handshake + .write_1(&SoftwareCrypto, handshake_id) + .map_err(invalid_data)?, + ), + ); + protocol::write_frame(&mut tcp, &request).await?; + + let response = protocol::read_frame(&mut tcp) + .await? + .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "router disconnected"))?; + let protocol::TransportResponse { + header, + handshake: response, + } = protocol::TransportResponse::decode_bytes(response.payload()).map_err(invalid_data)?; + let QlHandshakeRecord::Ik2(response) = response else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected transport handshake record", + )); + }; + handshake + .read_2(&SoftwareCrypto, header.route, &response) + .map_err(invalid_data)?; + let finalized = handshake.finalize(&SoftwareCrypto).map_err(invalid_data)?; + if finalized.remote_bundle != *router { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "transport peer does not match router", + )); + } + let tx_key = finalized.tx_key; + let rx_key = finalized.rx_key; + + let confirmation = protocol::seal_packet(&tx_key, PacketKind::Confirm, 0, &[])?; + protocol::write_packet(&mut tcp, &confirmation).await?; + + let (tcp, writer) = tcp.into_split(); + Ok(( + Receiver { + tcp, + key: rx_key, + counter: 0, + }, + Sender { + tcp: writer, + key: tx_key, + counter: 1, + buffer: Vec::new(), + }, + )) +} + +pub async fn receive(receiver: &mut Receiver) -> io::Result>> { + let Receiver { tcp, key, counter } = receiver; + let Some(frame) = protocol::read_frame(tcp).await? else { + return Ok(None); + }; + let nonce = protocol::take_counter(counter) + .ok_or_else(|| io::Error::other("nonce counter exhausted"))?; + let (kind, payload) = protocol::open_packet_owned(key, nonce, frame)?; + match kind { + PacketKind::Record => Ok(Some(payload)), + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected router packet", + )), + } +} + +pub async fn send(sender: &mut Sender, record: &[u8]) -> io::Result<()> { + send_packet(sender, PacketKind::Record, record).await +} + +pub async fn attach(sender: &mut Sender, bundle: &PeerBundle) -> io::Result<()> { + let payload = bundle.encode_vec(); + send_packet(sender, PacketKind::Attach, &payload).await +} + +async fn send_packet(sender: &mut Sender, kind: PacketKind, payload: &[u8]) -> io::Result<()> { + let nonce = protocol::take_counter(&mut sender.counter) + .ok_or_else(|| io::Error::other("nonce counter exhausted"))?; + protocol::seal_packet_into(&mut sender.buffer, &sender.key, kind, nonce, payload)?; + protocol::write_packet(&mut sender.tcp, &sender.buffer).await +} + +fn invalid_data(error: impl std::error::Error + Send + Sync + 'static) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, error) +} + +pub mod protocol { + use std::io; + + use ql_codec::{codec, Decode, Encode}; + use ql_wire::{ + Nonce, QlAead, QlHandshakeRecord, RecordHeader, SessionKey, SoftwareCrypto, + ENCRYPTED_MESSAGE_AUTH_SIZE, + }; + use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + + use crate::MAX_RECORD_SIZE; + + const FRAME_HEADER_SIZE: usize = 4; + const PACKET_HEADER_SIZE: usize = FRAME_HEADER_SIZE + 1; + const PACKET_OVERHEAD: usize = 1 + ENCRYPTED_MESSAGE_AUTH_SIZE; + const MAX_FRAME_SIZE: usize = MAX_RECORD_SIZE + PACKET_OVERHEAD; + + codec! { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum PacketKind { + Confirm = 1, + Attach = 2, + Record = 3, + } + } + + codec! { + pub struct TransportResponse { + pub header: RecordHeader, + pub handshake: QlHandshakeRecord, + } + } + + pub struct Frame(Vec); + + impl Frame { + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn payload(&self) -> &[u8] { + &self.0[FRAME_HEADER_SIZE..] + } + } + + pub struct Packet<'a> { + pub kind: PacketKind, + pub payload: &'a [u8], + } + + #[inline] + pub fn take_counter(next: &mut u64) -> Option { + let number = *next; + *next = number.checked_add(1)?; + Some(number) + } + + pub fn seal_packet( + key: &SessionKey, + kind: PacketKind, + nonce: u64, + payload: &[u8], + ) -> io::Result> { + let mut packet = Vec::with_capacity(FRAME_HEADER_SIZE + PACKET_OVERHEAD + payload.len()); + seal_packet_into(&mut packet, key, kind, nonce, payload)?; + Ok(packet) + } + + pub fn seal_packet_into( + packet: &mut Vec, + key: &SessionKey, + kind: PacketKind, + nonce: u64, + payload: &[u8], + ) -> io::Result<()> { + let length = PACKET_OVERHEAD + .checked_add(payload.len()) + .filter(|length| *length <= MAX_FRAME_SIZE) + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "frame exceeds the transport limit", + ) + })?; + packet.clear(); + packet.reserve(FRAME_HEADER_SIZE + length); + (length as u32).encode(packet); + kind.encode(packet); + packet.extend_from_slice(payload); + let tag = + SoftwareCrypto.aes256_gcm_encrypt(key, &Nonce::from_counter(nonce), packet, &mut []); + packet.extend_from_slice(&tag); + Ok(()) + } + + pub fn open_packet<'a>( + key: &SessionKey, + nonce: u64, + packet: &'a [u8], + ) -> io::Result> { + if packet.len() < FRAME_HEADER_SIZE + PACKET_OVERHEAD { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid authenticated frame", + )); + } + let length = + u32::decode_bytes(&packet[..FRAME_HEADER_SIZE]).map_err(super::invalid_data)?; + if length as usize != packet.len() - FRAME_HEADER_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid authenticated frame length", + )); + } + let kind = + PacketKind::decode_bytes(&packet[FRAME_HEADER_SIZE..]).map_err(super::invalid_data)?; + let tag_at = packet.len() - ENCRYPTED_MESSAGE_AUTH_SIZE; + let tag = <[u8; ENCRYPTED_MESSAGE_AUTH_SIZE]>::decode_bytes(&packet[tag_at..]) + .map_err(super::invalid_data)?; + if !SoftwareCrypto.aes256_gcm_decrypt( + key, + &Nonce::from_counter(nonce), + &packet[..tag_at], + &mut [], + &tag, + ) { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "authenticated packet tag mismatch", + )); + } + Ok(Packet { + kind, + payload: &packet[PACKET_HEADER_SIZE..tag_at], + }) + } + + pub fn open_packet_owned( + key: &SessionKey, + nonce: u64, + frame: Frame, + ) -> io::Result<(PacketKind, Vec)> { + let mut packet = frame.0; + let opened = open_packet(key, nonce, &packet)?; + let kind = opened.kind; + let payload_len = opened.payload.len(); + packet.copy_within(PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + payload_len, 0); + packet.truncate(payload_len); + Ok((kind, packet)) + } + + pub async fn read_frame(reader: &mut (impl AsyncRead + Unpin)) -> io::Result> { + let mut header = [0; FRAME_HEADER_SIZE]; + if reader.read(&mut header[..1]).await? == 0 { + return Ok(None); + } + reader.read_exact(&mut header[1..]).await?; + let length = u32::decode_bytes(header.as_slice()).map_err(super::invalid_data)? as usize; + if length > MAX_FRAME_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "frame exceeds the transport limit", + )); + } + let mut frame = Vec::with_capacity(FRAME_HEADER_SIZE + length); + frame.extend_from_slice(&header); + frame.resize(FRAME_HEADER_SIZE + length, 0); + reader.read_exact(&mut frame[FRAME_HEADER_SIZE..]).await?; + Ok(Some(Frame(frame))) + } + + pub async fn write_frame( + writer: &mut (impl AsyncWrite + Unpin), + payload: &[u8], + ) -> io::Result<()> { + if payload.len() > MAX_FRAME_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "frame exceeds the transport limit", + )); + } + let mut header = [0; FRAME_HEADER_SIZE]; + (payload.len() as u32).encode(&mut header.as_mut_slice()); + writer.write_all(&header).await?; + writer.write_all(payload).await + } + + pub async fn write_packet( + writer: &mut (impl AsyncWrite + Unpin), + packet: &[u8], + ) -> io::Result<()> { + writer.write_all(packet).await + } + + #[cfg(test)] + mod tests { + use ql_wire::SessionKey; + + use super::{ + open_packet, open_packet_owned, seal_packet, Frame, PacketKind, FRAME_HEADER_SIZE, + PACKET_HEADER_SIZE, + }; + + #[test] + fn packet_authentication_covers_the_entire_frame_and_counter() { + let key = SessionKey([7; SessionKey::SIZE]); + let packet = seal_packet(&key, PacketKind::Record, 4, b"record").unwrap(); + let (kind, payload) = open_packet_owned(&key, 4, Frame(packet.clone())).unwrap(); + assert_eq!(kind, PacketKind::Record); + assert_eq!(payload, b"record"); + assert!(open_packet(&key, 5, &packet).is_err()); + + for index in [0, FRAME_HEADER_SIZE, PACKET_HEADER_SIZE] { + let mut changed = packet.clone(); + changed[index] ^= 1; + assert!(open_packet(&key, 4, &changed).is_err()); + } + } + } +} From 7aa6bfb47145af6f19e2e94bb4a4f590eeade679 Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Wed, 2 Sep 2026 13:00:51 -0400 Subject: [PATCH 14/17] ql-relay: fix license metadata --- ql-relay/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ql-relay/Cargo.toml b/ql-relay/Cargo.toml index d33a594f..f8005cf5 100644 --- a/ql-relay/Cargo.toml +++ b/ql-relay/Cargo.toml @@ -3,7 +3,7 @@ name = "ql-relay" version = "0.1.0" edition = "2021" description = "QuantumLink relay protocol and client" -license.workspace = true +license = "Proprietary" [dependencies] bytes = { workspace = true } From 7b7bb40c302940193e05e57242d9ea3f9c75e30a Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Fri, 11 Sep 2026 17:44:27 -0400 Subject: [PATCH 15/17] ql-relay -> ql-router + simplify protocol --- Cargo.lock | 2 +- Cargo.toml | 4 +- ql-api/src/system.rs | 14 +++ {ql-relay => ql-router}/Cargo.toml | 4 +- {ql-relay => ql-router}/src/lib.rs | 154 ++++++++++++++++++++--------- ql-wire/src/handshake/challenge.rs | 142 -------------------------- ql-wire/src/handshake/mod.rs | 4 +- 7 files changed, 126 insertions(+), 198 deletions(-) rename {ql-relay => ql-router}/Cargo.toml (77%) rename {ql-relay => ql-router}/src/lib.rs (70%) delete mode 100644 ql-wire/src/handshake/challenge.rs diff --git a/Cargo.lock b/Cargo.lock index 06755eea..298d3822 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1416,7 +1416,7 @@ dependencies = [ ] [[package]] -name = "ql-relay" +name = "ql-router" version = "0.1.0" dependencies = [ "bytes", diff --git a/Cargo.toml b/Cargo.toml index b376fb2a..b1c846c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ "ql-codec", "ql-common", "ql-keyos", - "ql-relay", + "ql-router", "ql-api", "ql-fsm", "ql-rpc", @@ -31,6 +31,6 @@ ql-codec = { path = "ql-codec" } ql-common = { path = "ql-common" } ql-fsm = { path = "ql-fsm" } ql-keyos = { path = "ql-keyos" } -ql-relay = { path = "ql-relay" } +ql-router = { path = "ql-router" } ql-rpc = { path = "ql-rpc" } ql-wire = { path = "ql-wire" } diff --git a/ql-api/src/system.rs b/ql-api/src/system.rs index 780751b9..996390bf 100644 --- a/ql-api/src/system.rs +++ b/ql-api/src/system.rs @@ -39,6 +39,7 @@ app_routes! { SubscribeAppActivity: Subscription = 5, RequestPeerPermissions: Request = 6, RequestInstallPeerBundles: Request = 7, + RequestAttachRouter: Request = 8, } } @@ -139,4 +140,17 @@ impl Request for RequestInstallPeerBundles { type Response = InstallPeerBundlesResponse; } +rpc! { + pub enum AttachRouterResponse { + Started, + Rejected, + } +} + +impl Request for RequestAttachRouter { + type Error = Error; + type Request = Empty; + type Response = AttachRouterResponse; +} + // SERVICE ROUTES diff --git a/ql-relay/Cargo.toml b/ql-router/Cargo.toml similarity index 77% rename from ql-relay/Cargo.toml rename to ql-router/Cargo.toml index f8005cf5..faf92d85 100644 --- a/ql-relay/Cargo.toml +++ b/ql-router/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "ql-relay" +name = "ql-router" version = "0.1.0" edition = "2021" -description = "QuantumLink relay protocol and client" +description = "QuantumLink router protocol and client" license = "Proprietary" [dependencies] diff --git a/ql-relay/src/lib.rs b/ql-router/src/lib.rs similarity index 70% rename from ql-relay/src/lib.rs rename to ql-router/src/lib.rs index 6859f9fc..3a3f8c92 100644 --- a/ql-relay/src/lib.rs +++ b/ql-router/src/lib.rs @@ -1,9 +1,13 @@ +//! encrypted transport and routing client for QuantumLink +//! +//! one transport connection can attach multiple authenticated QIDs + use std::io; -use ql_codec::{Decode, Encode}; +use ql_codec::Decode; use ql_wire::{ - generate_identity, HandshakeId, IkHandshake, PeerBundle, QlHandshakeRecord, RecordHeader, - RecordType, RouteHeader, SessionKey, SoftwareCrypto, TransportParams, + generate_identity, HandshakeId, IkHandshake, PeerBundle, QlHandshakeRecord, QlIdentity, + RecordHeader, RecordType, RouteHeader, SessionKey, SoftwareCrypto, TransportParams, }; use tokio::net::{ tcp::{OwnedReadHalf, OwnedWriteHalf}, @@ -12,6 +16,12 @@ use tokio::net::{ use crate::protocol::PacketKind; +macro_rules! io_error { + ($kind:ident, $message:expr) => { + io::Error::new(io::ErrorKind::$kind, $message) + }; +} + pub const DEFAULT_ADDRESS: &str = "127.0.0.1:7447"; pub const MAX_RECORD_SIZE: usize = 8 * 1024; @@ -62,15 +72,15 @@ pub async fn connect( let response = protocol::read_frame(&mut tcp) .await? - .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "router disconnected"))?; + .ok_or_else(|| io_error!(UnexpectedEof, "router disconnected"))?; let protocol::TransportResponse { header, handshake: response, } = protocol::TransportResponse::decode_bytes(response.payload()).map_err(invalid_data)?; let QlHandshakeRecord::Ik2(response) = response else { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "unexpected transport handshake record", + return Err(io_error!( + InvalidData, + "unexpected transport handshake record" )); }; handshake @@ -78,9 +88,9 @@ pub async fn connect( .map_err(invalid_data)?; let finalized = handshake.finalize(&SoftwareCrypto).map_err(invalid_data)?; if finalized.remote_bundle != *router { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "transport peer does not match router", + return Err(io_error!( + PermissionDenied, + "transport peer does not match router" )); } let tx_key = finalized.tx_key; @@ -106,43 +116,106 @@ pub async fn connect( } pub async fn receive(receiver: &mut Receiver) -> io::Result>> { - let Receiver { tcp, key, counter } = receiver; - let Some(frame) = protocol::read_frame(tcp).await? else { + let Some((kind, payload)) = receive_packet(receiver).await? else { return Ok(None); }; - let nonce = protocol::take_counter(counter) - .ok_or_else(|| io::Error::other("nonce counter exhausted"))?; - let (kind, payload) = protocol::open_packet_owned(key, nonce, frame)?; match kind { PacketKind::Record => Ok(Some(payload)), - _ => Err(io::Error::new( - io::ErrorKind::InvalidData, - "unexpected router packet", - )), + _ => Err(io_error!(InvalidData, "unexpected router packet")), } } +pub async fn receive_packet(receiver: &mut Receiver) -> io::Result)>> { + let Receiver { tcp, key, counter } = receiver; + let Some(frame) = protocol::read_frame(tcp).await? else { + return Ok(None); + }; + let nonce = protocol::take_counter(counter) + .ok_or_else(|| io_error!(Other, "nonce counter exhausted"))?; + protocol::open_packet_owned(key, nonce, frame).map(Some) +} + pub async fn send(sender: &mut Sender, record: &[u8]) -> io::Result<()> { send_packet(sender, PacketKind::Record, record).await } -pub async fn attach(sender: &mut Sender, bundle: &PeerBundle) -> io::Result<()> { - let payload = bundle.encode_vec(); - send_packet(sender, PacketKind::Attach, &payload).await +pub async fn send_attach(sender: &mut Sender, handshake: &[u8]) -> io::Result<()> { + send_packet(sender, PacketKind::Attach, handshake).await +} + +pub async fn attach( + receiver: &mut Receiver, + sender: &mut Sender, + identity: &QlIdentity, + router: &PeerBundle, +) -> io::Result<()> { + let route = RouteHeader { + sender: identity.qid, + recipient: router.qid, + }; + let mut handshake = IkHandshake::new_ik_initiator( + &SoftwareCrypto, + identity, + router.clone(), + TransportParams::default(), + ); + let mut random = [0; 4]; + ql_wire::QlRandom::fill_random_bytes(&SoftwareCrypto, &mut random); + let handshake_id = HandshakeId::decode_bytes(random.as_slice()).unwrap(); + let request = ql_wire::encode_record_vec( + RecordHeader::new(route, RecordType::Handshake), + &QlHandshakeRecord::Ik1( + handshake + .write_1(&SoftwareCrypto, handshake_id) + .map_err(invalid_data)?, + ), + ); + send_attach(sender, &request).await?; + + let response = protocol::read_frame(&mut receiver.tcp) + .await? + .ok_or_else(|| io_error!(UnexpectedEof, "router disconnected"))?; + let nonce = protocol::take_counter(&mut receiver.counter) + .ok_or_else(|| io_error!(Other, "nonce counter exhausted"))?; + let (kind, response) = protocol::open_packet_owned(&receiver.key, nonce, response)?; + if kind != PacketKind::Attach { + return Err(io_error!(InvalidData, "unexpected router packet")); + } + let (header, response) = ql_wire::decode_record::(response.as_slice()) + .map_err(invalid_data)?; + let QlHandshakeRecord::Ik2(response) = response else { + return Err(io_error!( + InvalidData, + "unexpected attachment handshake record" + )); + }; + handshake + .read_2(&SoftwareCrypto, header.route, &response) + .map_err(invalid_data)?; + let finalized = handshake.finalize(&SoftwareCrypto).map_err(invalid_data)?; + if finalized.remote_bundle != *router { + return Err(io_error!( + PermissionDenied, + "attachment peer does not match router" + )); + } + Ok(()) } async fn send_packet(sender: &mut Sender, kind: PacketKind, payload: &[u8]) -> io::Result<()> { let nonce = protocol::take_counter(&mut sender.counter) - .ok_or_else(|| io::Error::other("nonce counter exhausted"))?; + .ok_or_else(|| io_error!(Other, "nonce counter exhausted"))?; protocol::seal_packet_into(&mut sender.buffer, &sender.key, kind, nonce, payload)?; protocol::write_packet(&mut sender.tcp, &sender.buffer).await } fn invalid_data(error: impl std::error::Error + Send + Sync + 'static) -> io::Error { - io::Error::new(io::ErrorKind::InvalidData, error) + io_error!(InvalidData, error) } pub mod protocol { + //! framing for the encrypted router transport + use std::io; use ql_codec::{codec, Decode, Encode}; @@ -220,12 +293,7 @@ pub mod protocol { let length = PACKET_OVERHEAD .checked_add(payload.len()) .filter(|length| *length <= MAX_FRAME_SIZE) - .ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidInput, - "frame exceeds the transport limit", - ) - })?; + .ok_or_else(|| io_error!(InvalidInput, "frame exceeds the transport limit"))?; packet.clear(); packet.reserve(FRAME_HEADER_SIZE + length); (length as u32).encode(packet); @@ -243,18 +311,12 @@ pub mod protocol { packet: &'a [u8], ) -> io::Result> { if packet.len() < FRAME_HEADER_SIZE + PACKET_OVERHEAD { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "invalid authenticated frame", - )); + return Err(io_error!(InvalidData, "invalid authenticated frame")); } let length = u32::decode_bytes(&packet[..FRAME_HEADER_SIZE]).map_err(super::invalid_data)?; if length as usize != packet.len() - FRAME_HEADER_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "invalid authenticated frame length", - )); + return Err(io_error!(InvalidData, "invalid authenticated frame length")); } let kind = PacketKind::decode_bytes(&packet[FRAME_HEADER_SIZE..]).map_err(super::invalid_data)?; @@ -268,9 +330,9 @@ pub mod protocol { &mut [], &tag, ) { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "authenticated packet tag mismatch", + return Err(io_error!( + PermissionDenied, + "authenticated packet tag mismatch" )); } Ok(Packet { @@ -301,10 +363,7 @@ pub mod protocol { reader.read_exact(&mut header[1..]).await?; let length = u32::decode_bytes(header.as_slice()).map_err(super::invalid_data)? as usize; if length > MAX_FRAME_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "frame exceeds the transport limit", - )); + return Err(io_error!(InvalidData, "frame exceeds the transport limit")); } let mut frame = Vec::with_capacity(FRAME_HEADER_SIZE + length); frame.extend_from_slice(&header); @@ -318,10 +377,7 @@ pub mod protocol { payload: &[u8], ) -> io::Result<()> { if payload.len() > MAX_FRAME_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "frame exceeds the transport limit", - )); + return Err(io_error!(InvalidInput, "frame exceeds the transport limit")); } let mut header = [0; FRAME_HEADER_SIZE]; (payload.len() as u32).encode(&mut header.as_mut_slice()); diff --git a/ql-wire/src/handshake/challenge.rs b/ql-wire/src/handshake/challenge.rs deleted file mode 100644 index 54f4c848..00000000 --- a/ql-wire/src/handshake/challenge.rs +++ /dev/null @@ -1,142 +0,0 @@ -use std::borrow::Borrow; - -use ql_codec::Reader; -use ql_common::QID; - -use super::{HandshakeId, IkHandshake, TransportParams}; -use crate::{ - decrypt_record, encode_record_vec, parse_session_frames, Error, PeerBundle, QlCrypto, - QlHandshakeRecord, QlIdentity, QlSessionRecord, RecordHeader, RecordSeq, RecordType, - RouteHeader, SessionFrame, SessionKey, SessionRecordBuilder, QL_WIRE_VERSION, -}; - -pub struct PeerChallenge { - handshake: IkHandshake, -} - -impl> PeerChallenge { - pub fn new( - crypto: &impl QlCrypto, - local: I, - remote: PeerBundle, - handshake_id: HandshakeId, - ) -> Result<(Self, Vec), Error> { - let route = RouteHeader { - sender: local.borrow().qid, - recipient: remote.qid, - }; - let mut handshake = - IkHandshake::new_kk_initiator(crypto, local, remote, TransportParams::default()); - let request = handshake.write_1(crypto, handshake_id)?; - let request = encode_record_vec( - RecordHeader::new(route, RecordType::Handshake), - &QlHandshakeRecord::Kk1(request), - ); - Ok((Self { handshake }, request)) - } - - pub fn verify( - mut self, - crypto: &impl QlCrypto, - response: &[u8], - ) -> Result<(QID, Vec), Error> { - let (header, response) = decode_handshake(response)?; - let QlHandshakeRecord::Kk2(response) = response else { - return Err(Error::InvalidPayload); - }; - self.handshake.read_2(crypto, header.route, &response)?; - let session = self.handshake.finalize(crypto)?; - let qid = session.remote_bundle.qid; - let route = RouteHeader { - sender: header.route.recipient, - recipient: qid, - }; - let mut confirmation = - SessionRecordBuilder::new(RecordSeq(0), SessionRecordBuilder::MIN_CAPACITY + 1); - confirmation.push_ping(); - Ok((qid, confirmation.encrypt(crypto, route, &session.tx_key))) - } -} - -pub struct PendingChallengeConfirmation { - route: RouteHeader, - rx_key: SessionKey, -} - -impl PendingChallengeConfirmation { - pub fn verify(self, crypto: &impl QlCrypto, confirmation: &mut [u8]) -> Result<(), Error> { - let mut reader = Reader::new(confirmation); - let header = reader.decode::()?; - if header.version != QL_WIRE_VERSION - || header.route != self.route - || header.record_type != RecordType::Session - { - return Err(Error::InvalidPayload); - } - let record = reader.decode::>()?; - if record.header.seq != RecordSeq(0) { - return Err(Error::InvalidPayload); - } - let payload = decrypt_record( - crypto, - &header, - &record.header, - record.payload, - &self.rx_key, - )?; - let mut frames = parse_session_frames(payload); - if !matches!(frames.next().transpose()?, Some(SessionFrame::Ping)) - || frames.next().is_some() - { - return Err(Error::InvalidPayload); - } - Ok(()) - } -} - -pub fn answer_peer_challenge>( - crypto: &impl QlCrypto, - local: I, - challenger: PeerBundle, - request: &[u8], -) -> Result<(Vec, PendingChallengeConfirmation), Error> { - let (header, request) = decode_handshake(request)?; - let QlHandshakeRecord::Kk1(request) = request else { - return Err(Error::InvalidPayload); - }; - let mut handshake = - IkHandshake::new_kk_responder(crypto, local, challenger, TransportParams::default()); - handshake.read_1(crypto, header.route, &request)?; - let response = handshake.write_2(crypto, request.handshake_id)?; - let session = handshake.finalize(crypto)?; - let response = encode_record_vec( - RecordHeader::new( - RouteHeader { - sender: header.route.recipient, - recipient: header.route.sender, - }, - RecordType::Handshake, - ), - &QlHandshakeRecord::Kk2(response), - ); - Ok(( - response, - PendingChallengeConfirmation { - route: header.route, - rx_key: session.rx_key, - }, - )) -} - -fn decode_handshake(bytes: &[u8]) -> Result<(RecordHeader, QlHandshakeRecord), Error> { - let mut reader = Reader::new(bytes); - let header = reader.decode::()?; - if header.version != QL_WIRE_VERSION || header.record_type != RecordType::Handshake { - return Err(Error::InvalidPayload); - } - let record = reader.decode()?; - if !reader.is_empty() { - return Err(Error::InvalidPayload); - } - Ok((header, record)) -} diff --git a/ql-wire/src/handshake/mod.rs b/ql-wire/src/handshake/mod.rs index 8e3c7c9e..4fb9934b 100644 --- a/ql-wire/src/handshake/mod.rs +++ b/ql-wire/src/handshake/mod.rs @@ -1,3 +1,5 @@ +//! authenticated QuantumLink handshake patterns + use ql_codec::{ByteSlice, Decode, Encode}; use crate::{ @@ -5,14 +7,12 @@ use crate::{ QlCrypto, RouteHeader, SessionKey, ENCRYPTED_MESSAGE_AUTH_SIZE, }; -mod challenge; mod id; mod ik; mod pairing; mod transport_params; mod xx; -pub use challenge::{answer_peer_challenge, PeerChallenge, PendingChallengeConfirmation}; pub use id::HandshakeId; pub use ik::{Ik1, Ik2, IkHandshake, IkPattern}; pub use pairing::{PairingId, PairingToken}; From 5c418e761df458c62c7a9bd543b8db069701676f Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Mon, 14 Sep 2026 10:22:56 -0400 Subject: [PATCH 16/17] ql-router: split deps --- ql-fsm/Cargo.toml | 2 +- ql-router/Cargo.toml | 8 +- ql-router/src/client.rs | 212 +++++++++++++++++++ ql-router/src/lib.rs | 416 +------------------------------------- ql-router/src/protocol.rs | 219 ++++++++++++++++++++ ql-runtime/Cargo.toml | 2 +- ql-wire/Cargo.toml | 2 +- ql-wire/src/lib.rs | 4 +- 8 files changed, 450 insertions(+), 415 deletions(-) create mode 100644 ql-router/src/client.rs create mode 100644 ql-router/src/protocol.rs diff --git a/ql-fsm/Cargo.toml b/ql-fsm/Cargo.toml index 4b5a48cc..cd975fca 100644 --- a/ql-fsm/Cargo.toml +++ b/ql-fsm/Cargo.toml @@ -14,4 +14,4 @@ ql-wire = { workspace = true } [dev-dependencies] proptest = "1.6" -ql-wire = { workspace = true, features = ["test-utils"] } +ql-wire = { workspace = true, features = ["software-crypto"] } diff --git a/ql-router/Cargo.toml b/ql-router/Cargo.toml index faf92d85..29bb6e12 100644 --- a/ql-router/Cargo.toml +++ b/ql-router/Cargo.toml @@ -5,8 +5,12 @@ edition = "2021" description = "QuantumLink router protocol and client" license = "Proprietary" +[features] +default = [] +tokio = ["dep:tokio", "ql-wire/software-crypto"] + [dependencies] bytes = { workspace = true } ql-codec = { workspace = true } -ql-wire = { workspace = true, features = ["test-utils"] } -tokio = { version = "1", features = ["io-util", "net"] } +ql-wire = { workspace = true } +tokio = { version = "1", features = ["io-util", "net"], optional = true } diff --git a/ql-router/src/client.rs b/ql-router/src/client.rs new file mode 100644 index 00000000..ece4f637 --- /dev/null +++ b/ql-router/src/client.rs @@ -0,0 +1,212 @@ +use std::io; + +use ql_codec::Decode; +use ql_wire::{ + generate_identity, HandshakeId, IkHandshake, PeerBundle, QlHandshakeRecord, QlIdentity, + RecordHeader, RecordType, RouteHeader, SessionKey, SoftwareCrypto, TransportParams, +}; +use tokio::net::{ + tcp::{OwnedReadHalf, OwnedWriteHalf}, + TcpStream, ToSocketAddrs, +}; + +use crate::{invalid_data, protocol, protocol::PacketKind}; + +pub struct Receiver { + tcp: OwnedReadHalf, + key: SessionKey, + counter: u64, +} + +pub struct Sender { + tcp: OwnedWriteHalf, + key: SessionKey, + counter: u64, + buffer: Vec, +} + +pub async fn connect( + address: impl ToSocketAddrs, + router: &PeerBundle, +) -> io::Result<(Receiver, Sender)> { + router.validate(&SoftwareCrypto).map_err(invalid_data)?; + let mut tcp = TcpStream::connect(address).await?; + tcp.set_nodelay(true)?; + + let identity = generate_identity(&SoftwareCrypto, "QL router transport"); + let route = RouteHeader { + sender: identity.qid, + recipient: router.qid, + }; + let mut handshake = IkHandshake::new_ik_initiator( + &SoftwareCrypto, + identity, + router.clone(), + TransportParams::default(), + ); + let mut random = [0; 4]; + ql_wire::QlRandom::fill_random_bytes(&SoftwareCrypto, &mut random); + let handshake_id = HandshakeId::decode_bytes(random.as_slice()).unwrap(); + let request = ql_wire::encode_record_vec( + RecordHeader::new(route, RecordType::Handshake), + &QlHandshakeRecord::Ik1( + handshake + .write_1(&SoftwareCrypto, handshake_id) + .map_err(invalid_data)?, + ), + ); + protocol::write_frame(&mut tcp, &request).await?; + + let response = protocol::read_frame(&mut tcp) + .await? + .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "router disconnected"))?; + let protocol::TransportResponse { + header, + handshake: response, + } = protocol::TransportResponse::decode_bytes(response.payload()).map_err(invalid_data)?; + let QlHandshakeRecord::Ik2(response) = response else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected transport handshake record", + )); + }; + handshake + .read_2(&SoftwareCrypto, header.route, &response) + .map_err(invalid_data)?; + let finalized = handshake.finalize(&SoftwareCrypto).map_err(invalid_data)?; + if finalized.remote_bundle != *router { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "transport peer does not match router", + )); + } + let tx_key = finalized.tx_key; + let rx_key = finalized.rx_key; + + let confirmation = + protocol::seal_packet(&SoftwareCrypto, &tx_key, PacketKind::Confirm, 0, &[])?; + protocol::write_packet(&mut tcp, &confirmation).await?; + + let (tcp, writer) = tcp.into_split(); + Ok(( + Receiver { + tcp, + key: rx_key, + counter: 0, + }, + Sender { + tcp: writer, + key: tx_key, + counter: 1, + buffer: Vec::new(), + }, + )) +} + +pub async fn receive(receiver: &mut Receiver) -> io::Result>> { + let Some((kind, payload)) = receive_packet(receiver).await? else { + return Ok(None); + }; + match kind { + PacketKind::Record => Ok(Some(payload)), + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected router packet", + )), + } +} + +pub async fn receive_packet(receiver: &mut Receiver) -> io::Result)>> { + let Receiver { tcp, key, counter } = receiver; + let Some(frame) = protocol::read_frame(tcp).await? else { + return Ok(None); + }; + let nonce = protocol::take_counter(counter) + .ok_or_else(|| io::Error::other("nonce counter exhausted"))?; + protocol::open_packet_owned(&SoftwareCrypto, key, nonce, frame).map(Some) +} + +pub async fn send(sender: &mut Sender, record: &[u8]) -> io::Result<()> { + send_packet(sender, PacketKind::Record, record).await +} + +pub async fn send_attach(sender: &mut Sender, handshake: &[u8]) -> io::Result<()> { + send_packet(sender, PacketKind::Attach, handshake).await +} + +pub async fn attach( + receiver: &mut Receiver, + sender: &mut Sender, + identity: &QlIdentity, + router: &PeerBundle, +) -> io::Result<()> { + let route = RouteHeader { + sender: identity.qid, + recipient: router.qid, + }; + let mut handshake = IkHandshake::new_ik_initiator( + &SoftwareCrypto, + identity, + router.clone(), + TransportParams::default(), + ); + let mut random = [0; 4]; + ql_wire::QlRandom::fill_random_bytes(&SoftwareCrypto, &mut random); + let handshake_id = HandshakeId::decode_bytes(random.as_slice()).unwrap(); + let request = ql_wire::encode_record_vec( + RecordHeader::new(route, RecordType::Handshake), + &QlHandshakeRecord::Ik1( + handshake + .write_1(&SoftwareCrypto, handshake_id) + .map_err(invalid_data)?, + ), + ); + send_attach(sender, &request).await?; + + let response = protocol::read_frame(&mut receiver.tcp) + .await? + .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "router disconnected"))?; + let nonce = protocol::take_counter(&mut receiver.counter) + .ok_or_else(|| io::Error::other("nonce counter exhausted"))?; + let (kind, response) = + protocol::open_packet_owned(&SoftwareCrypto, &receiver.key, nonce, response)?; + if kind != PacketKind::Attach { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected router packet", + )); + } + let (header, response) = ql_wire::decode_record::(response.as_slice()) + .map_err(invalid_data)?; + let QlHandshakeRecord::Ik2(response) = response else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected attachment handshake record", + )); + }; + handshake + .read_2(&SoftwareCrypto, header.route, &response) + .map_err(invalid_data)?; + let finalized = handshake.finalize(&SoftwareCrypto).map_err(invalid_data)?; + if finalized.remote_bundle != *router { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "attachment peer does not match router", + )); + } + Ok(()) +} + +async fn send_packet(sender: &mut Sender, kind: PacketKind, payload: &[u8]) -> io::Result<()> { + let nonce = protocol::take_counter(&mut sender.counter) + .ok_or_else(|| io::Error::other("nonce counter exhausted"))?; + protocol::seal_packet_into( + &SoftwareCrypto, + &mut sender.buffer, + &sender.key, + kind, + nonce, + payload, + )?; + protocol::write_packet(&mut sender.tcp, &sender.buffer).await +} diff --git a/ql-router/src/lib.rs b/ql-router/src/lib.rs index 3a3f8c92..82690fa3 100644 --- a/ql-router/src/lib.rs +++ b/ql-router/src/lib.rs @@ -1,420 +1,20 @@ -//! encrypted transport and routing client for QuantumLink +//! routing protocol for QuantumLink //! //! one transport connection can attach multiple authenticated QIDs +//! enable the `tokio` feature for the TCP client use std::io; -use ql_codec::Decode; -use ql_wire::{ - generate_identity, HandshakeId, IkHandshake, PeerBundle, QlHandshakeRecord, QlIdentity, - RecordHeader, RecordType, RouteHeader, SessionKey, SoftwareCrypto, TransportParams, -}; -use tokio::net::{ - tcp::{OwnedReadHalf, OwnedWriteHalf}, - TcpStream, ToSocketAddrs, -}; +#[cfg(feature = "tokio")] +mod client; +pub mod protocol; -use crate::protocol::PacketKind; - -macro_rules! io_error { - ($kind:ident, $message:expr) => { - io::Error::new(io::ErrorKind::$kind, $message) - }; -} +#[cfg(feature = "tokio")] +pub use client::*; pub const DEFAULT_ADDRESS: &str = "127.0.0.1:7447"; pub const MAX_RECORD_SIZE: usize = 8 * 1024; -pub struct Receiver { - tcp: OwnedReadHalf, - key: SessionKey, - counter: u64, -} - -pub struct Sender { - tcp: OwnedWriteHalf, - key: SessionKey, - counter: u64, - buffer: Vec, -} - -pub async fn connect( - address: impl ToSocketAddrs, - router: &PeerBundle, -) -> io::Result<(Receiver, Sender)> { - router.validate(&SoftwareCrypto).map_err(invalid_data)?; - let mut tcp = TcpStream::connect(address).await?; - tcp.set_nodelay(true)?; - - let identity = generate_identity(&SoftwareCrypto, "QL router transport"); - let route = RouteHeader { - sender: identity.qid, - recipient: router.qid, - }; - let mut handshake = IkHandshake::new_ik_initiator( - &SoftwareCrypto, - identity, - router.clone(), - TransportParams::default(), - ); - let mut random = [0; 4]; - ql_wire::QlRandom::fill_random_bytes(&SoftwareCrypto, &mut random); - let handshake_id = HandshakeId::decode_bytes(random.as_slice()).unwrap(); - let request = ql_wire::encode_record_vec( - RecordHeader::new(route, RecordType::Handshake), - &QlHandshakeRecord::Ik1( - handshake - .write_1(&SoftwareCrypto, handshake_id) - .map_err(invalid_data)?, - ), - ); - protocol::write_frame(&mut tcp, &request).await?; - - let response = protocol::read_frame(&mut tcp) - .await? - .ok_or_else(|| io_error!(UnexpectedEof, "router disconnected"))?; - let protocol::TransportResponse { - header, - handshake: response, - } = protocol::TransportResponse::decode_bytes(response.payload()).map_err(invalid_data)?; - let QlHandshakeRecord::Ik2(response) = response else { - return Err(io_error!( - InvalidData, - "unexpected transport handshake record" - )); - }; - handshake - .read_2(&SoftwareCrypto, header.route, &response) - .map_err(invalid_data)?; - let finalized = handshake.finalize(&SoftwareCrypto).map_err(invalid_data)?; - if finalized.remote_bundle != *router { - return Err(io_error!( - PermissionDenied, - "transport peer does not match router" - )); - } - let tx_key = finalized.tx_key; - let rx_key = finalized.rx_key; - - let confirmation = protocol::seal_packet(&tx_key, PacketKind::Confirm, 0, &[])?; - protocol::write_packet(&mut tcp, &confirmation).await?; - - let (tcp, writer) = tcp.into_split(); - Ok(( - Receiver { - tcp, - key: rx_key, - counter: 0, - }, - Sender { - tcp: writer, - key: tx_key, - counter: 1, - buffer: Vec::new(), - }, - )) -} - -pub async fn receive(receiver: &mut Receiver) -> io::Result>> { - let Some((kind, payload)) = receive_packet(receiver).await? else { - return Ok(None); - }; - match kind { - PacketKind::Record => Ok(Some(payload)), - _ => Err(io_error!(InvalidData, "unexpected router packet")), - } -} - -pub async fn receive_packet(receiver: &mut Receiver) -> io::Result)>> { - let Receiver { tcp, key, counter } = receiver; - let Some(frame) = protocol::read_frame(tcp).await? else { - return Ok(None); - }; - let nonce = protocol::take_counter(counter) - .ok_or_else(|| io_error!(Other, "nonce counter exhausted"))?; - protocol::open_packet_owned(key, nonce, frame).map(Some) -} - -pub async fn send(sender: &mut Sender, record: &[u8]) -> io::Result<()> { - send_packet(sender, PacketKind::Record, record).await -} - -pub async fn send_attach(sender: &mut Sender, handshake: &[u8]) -> io::Result<()> { - send_packet(sender, PacketKind::Attach, handshake).await -} - -pub async fn attach( - receiver: &mut Receiver, - sender: &mut Sender, - identity: &QlIdentity, - router: &PeerBundle, -) -> io::Result<()> { - let route = RouteHeader { - sender: identity.qid, - recipient: router.qid, - }; - let mut handshake = IkHandshake::new_ik_initiator( - &SoftwareCrypto, - identity, - router.clone(), - TransportParams::default(), - ); - let mut random = [0; 4]; - ql_wire::QlRandom::fill_random_bytes(&SoftwareCrypto, &mut random); - let handshake_id = HandshakeId::decode_bytes(random.as_slice()).unwrap(); - let request = ql_wire::encode_record_vec( - RecordHeader::new(route, RecordType::Handshake), - &QlHandshakeRecord::Ik1( - handshake - .write_1(&SoftwareCrypto, handshake_id) - .map_err(invalid_data)?, - ), - ); - send_attach(sender, &request).await?; - - let response = protocol::read_frame(&mut receiver.tcp) - .await? - .ok_or_else(|| io_error!(UnexpectedEof, "router disconnected"))?; - let nonce = protocol::take_counter(&mut receiver.counter) - .ok_or_else(|| io_error!(Other, "nonce counter exhausted"))?; - let (kind, response) = protocol::open_packet_owned(&receiver.key, nonce, response)?; - if kind != PacketKind::Attach { - return Err(io_error!(InvalidData, "unexpected router packet")); - } - let (header, response) = ql_wire::decode_record::(response.as_slice()) - .map_err(invalid_data)?; - let QlHandshakeRecord::Ik2(response) = response else { - return Err(io_error!( - InvalidData, - "unexpected attachment handshake record" - )); - }; - handshake - .read_2(&SoftwareCrypto, header.route, &response) - .map_err(invalid_data)?; - let finalized = handshake.finalize(&SoftwareCrypto).map_err(invalid_data)?; - if finalized.remote_bundle != *router { - return Err(io_error!( - PermissionDenied, - "attachment peer does not match router" - )); - } - Ok(()) -} - -async fn send_packet(sender: &mut Sender, kind: PacketKind, payload: &[u8]) -> io::Result<()> { - let nonce = protocol::take_counter(&mut sender.counter) - .ok_or_else(|| io_error!(Other, "nonce counter exhausted"))?; - protocol::seal_packet_into(&mut sender.buffer, &sender.key, kind, nonce, payload)?; - protocol::write_packet(&mut sender.tcp, &sender.buffer).await -} - fn invalid_data(error: impl std::error::Error + Send + Sync + 'static) -> io::Error { - io_error!(InvalidData, error) -} - -pub mod protocol { - //! framing for the encrypted router transport - - use std::io; - - use ql_codec::{codec, Decode, Encode}; - use ql_wire::{ - Nonce, QlAead, QlHandshakeRecord, RecordHeader, SessionKey, SoftwareCrypto, - ENCRYPTED_MESSAGE_AUTH_SIZE, - }; - use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; - - use crate::MAX_RECORD_SIZE; - - const FRAME_HEADER_SIZE: usize = 4; - const PACKET_HEADER_SIZE: usize = FRAME_HEADER_SIZE + 1; - const PACKET_OVERHEAD: usize = 1 + ENCRYPTED_MESSAGE_AUTH_SIZE; - const MAX_FRAME_SIZE: usize = MAX_RECORD_SIZE + PACKET_OVERHEAD; - - codec! { - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum PacketKind { - Confirm = 1, - Attach = 2, - Record = 3, - } - } - - codec! { - pub struct TransportResponse { - pub header: RecordHeader, - pub handshake: QlHandshakeRecord, - } - } - - pub struct Frame(Vec); - - impl Frame { - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } - - pub fn payload(&self) -> &[u8] { - &self.0[FRAME_HEADER_SIZE..] - } - } - - pub struct Packet<'a> { - pub kind: PacketKind, - pub payload: &'a [u8], - } - - #[inline] - pub fn take_counter(next: &mut u64) -> Option { - let number = *next; - *next = number.checked_add(1)?; - Some(number) - } - - pub fn seal_packet( - key: &SessionKey, - kind: PacketKind, - nonce: u64, - payload: &[u8], - ) -> io::Result> { - let mut packet = Vec::with_capacity(FRAME_HEADER_SIZE + PACKET_OVERHEAD + payload.len()); - seal_packet_into(&mut packet, key, kind, nonce, payload)?; - Ok(packet) - } - - pub fn seal_packet_into( - packet: &mut Vec, - key: &SessionKey, - kind: PacketKind, - nonce: u64, - payload: &[u8], - ) -> io::Result<()> { - let length = PACKET_OVERHEAD - .checked_add(payload.len()) - .filter(|length| *length <= MAX_FRAME_SIZE) - .ok_or_else(|| io_error!(InvalidInput, "frame exceeds the transport limit"))?; - packet.clear(); - packet.reserve(FRAME_HEADER_SIZE + length); - (length as u32).encode(packet); - kind.encode(packet); - packet.extend_from_slice(payload); - let tag = - SoftwareCrypto.aes256_gcm_encrypt(key, &Nonce::from_counter(nonce), packet, &mut []); - packet.extend_from_slice(&tag); - Ok(()) - } - - pub fn open_packet<'a>( - key: &SessionKey, - nonce: u64, - packet: &'a [u8], - ) -> io::Result> { - if packet.len() < FRAME_HEADER_SIZE + PACKET_OVERHEAD { - return Err(io_error!(InvalidData, "invalid authenticated frame")); - } - let length = - u32::decode_bytes(&packet[..FRAME_HEADER_SIZE]).map_err(super::invalid_data)?; - if length as usize != packet.len() - FRAME_HEADER_SIZE { - return Err(io_error!(InvalidData, "invalid authenticated frame length")); - } - let kind = - PacketKind::decode_bytes(&packet[FRAME_HEADER_SIZE..]).map_err(super::invalid_data)?; - let tag_at = packet.len() - ENCRYPTED_MESSAGE_AUTH_SIZE; - let tag = <[u8; ENCRYPTED_MESSAGE_AUTH_SIZE]>::decode_bytes(&packet[tag_at..]) - .map_err(super::invalid_data)?; - if !SoftwareCrypto.aes256_gcm_decrypt( - key, - &Nonce::from_counter(nonce), - &packet[..tag_at], - &mut [], - &tag, - ) { - return Err(io_error!( - PermissionDenied, - "authenticated packet tag mismatch" - )); - } - Ok(Packet { - kind, - payload: &packet[PACKET_HEADER_SIZE..tag_at], - }) - } - - pub fn open_packet_owned( - key: &SessionKey, - nonce: u64, - frame: Frame, - ) -> io::Result<(PacketKind, Vec)> { - let mut packet = frame.0; - let opened = open_packet(key, nonce, &packet)?; - let kind = opened.kind; - let payload_len = opened.payload.len(); - packet.copy_within(PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + payload_len, 0); - packet.truncate(payload_len); - Ok((kind, packet)) - } - - pub async fn read_frame(reader: &mut (impl AsyncRead + Unpin)) -> io::Result> { - let mut header = [0; FRAME_HEADER_SIZE]; - if reader.read(&mut header[..1]).await? == 0 { - return Ok(None); - } - reader.read_exact(&mut header[1..]).await?; - let length = u32::decode_bytes(header.as_slice()).map_err(super::invalid_data)? as usize; - if length > MAX_FRAME_SIZE { - return Err(io_error!(InvalidData, "frame exceeds the transport limit")); - } - let mut frame = Vec::with_capacity(FRAME_HEADER_SIZE + length); - frame.extend_from_slice(&header); - frame.resize(FRAME_HEADER_SIZE + length, 0); - reader.read_exact(&mut frame[FRAME_HEADER_SIZE..]).await?; - Ok(Some(Frame(frame))) - } - - pub async fn write_frame( - writer: &mut (impl AsyncWrite + Unpin), - payload: &[u8], - ) -> io::Result<()> { - if payload.len() > MAX_FRAME_SIZE { - return Err(io_error!(InvalidInput, "frame exceeds the transport limit")); - } - let mut header = [0; FRAME_HEADER_SIZE]; - (payload.len() as u32).encode(&mut header.as_mut_slice()); - writer.write_all(&header).await?; - writer.write_all(payload).await - } - - pub async fn write_packet( - writer: &mut (impl AsyncWrite + Unpin), - packet: &[u8], - ) -> io::Result<()> { - writer.write_all(packet).await - } - - #[cfg(test)] - mod tests { - use ql_wire::SessionKey; - - use super::{ - open_packet, open_packet_owned, seal_packet, Frame, PacketKind, FRAME_HEADER_SIZE, - PACKET_HEADER_SIZE, - }; - - #[test] - fn packet_authentication_covers_the_entire_frame_and_counter() { - let key = SessionKey([7; SessionKey::SIZE]); - let packet = seal_packet(&key, PacketKind::Record, 4, b"record").unwrap(); - let (kind, payload) = open_packet_owned(&key, 4, Frame(packet.clone())).unwrap(); - assert_eq!(kind, PacketKind::Record); - assert_eq!(payload, b"record"); - assert!(open_packet(&key, 5, &packet).is_err()); - - for index in [0, FRAME_HEADER_SIZE, PACKET_HEADER_SIZE] { - let mut changed = packet.clone(); - changed[index] ^= 1; - assert!(open_packet(&key, 4, &changed).is_err()); - } - } - } + io::Error::new(io::ErrorKind::InvalidData, error) } diff --git a/ql-router/src/protocol.rs b/ql-router/src/protocol.rs new file mode 100644 index 00000000..bf8004a7 --- /dev/null +++ b/ql-router/src/protocol.rs @@ -0,0 +1,219 @@ +//! framing for the encrypted router transport + +use std::io; + +use ql_codec::{codec, Decode, Encode}; +use ql_wire::{ + Nonce, QlAead, QlHandshakeRecord, RecordHeader, SessionKey, ENCRYPTED_MESSAGE_AUTH_SIZE, +}; +#[cfg(feature = "tokio")] +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +use crate::{invalid_data, MAX_RECORD_SIZE}; + +const FRAME_HEADER_SIZE: usize = 4; +const PACKET_HEADER_SIZE: usize = FRAME_HEADER_SIZE + 1; +const PACKET_OVERHEAD: usize = 1 + ENCRYPTED_MESSAGE_AUTH_SIZE; +const MAX_FRAME_SIZE: usize = MAX_RECORD_SIZE + PACKET_OVERHEAD; + +codec! { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum PacketKind { + Confirm = 1, + Attach = 2, + Record = 3, + } +} + +codec! { + pub struct TransportResponse { + pub header: RecordHeader, + pub handshake: QlHandshakeRecord, + } +} + +#[cfg(feature = "tokio")] +pub struct Frame(Vec); + +#[cfg(feature = "tokio")] +impl Frame { + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn payload(&self) -> &[u8] { + &self.0[FRAME_HEADER_SIZE..] + } +} + +pub struct Packet<'a> { + pub kind: PacketKind, + pub payload: &'a [u8], +} + +#[inline] +pub fn take_counter(next: &mut u64) -> Option { + let number = *next; + *next = number.checked_add(1)?; + Some(number) +} + +pub fn seal_packet( + crypto: &impl QlAead, + key: &SessionKey, + kind: PacketKind, + nonce: u64, + payload: &[u8], +) -> io::Result> { + let mut packet = Vec::with_capacity(FRAME_HEADER_SIZE + PACKET_OVERHEAD + payload.len()); + seal_packet_into(crypto, &mut packet, key, kind, nonce, payload)?; + Ok(packet) +} + +pub fn seal_packet_into( + crypto: &impl QlAead, + packet: &mut Vec, + key: &SessionKey, + kind: PacketKind, + nonce: u64, + payload: &[u8], +) -> io::Result<()> { + let length = PACKET_OVERHEAD + .checked_add(payload.len()) + .filter(|length| *length <= MAX_FRAME_SIZE) + .ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "frame exceeds transport limit") + })?; + packet.clear(); + packet.reserve(FRAME_HEADER_SIZE + length); + (length as u32).encode(packet); + kind.encode(packet); + packet.extend_from_slice(payload); + let tag = crypto.aes256_gcm_encrypt(key, &Nonce::from_counter(nonce), packet, &mut []); + packet.extend_from_slice(&tag); + Ok(()) +} + +pub fn open_packet<'a>( + crypto: &impl QlAead, + key: &SessionKey, + nonce: u64, + packet: &'a [u8], +) -> io::Result> { + if packet.len() < FRAME_HEADER_SIZE + PACKET_OVERHEAD { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid authenticated frame", + )); + } + let length = u32::decode_bytes(&packet[..FRAME_HEADER_SIZE]).map_err(invalid_data)?; + if length as usize != packet.len() - FRAME_HEADER_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid authenticated frame length", + )); + } + let kind = PacketKind::decode_bytes(&packet[FRAME_HEADER_SIZE..]).map_err(invalid_data)?; + let tag_at = packet.len() - ENCRYPTED_MESSAGE_AUTH_SIZE; + let tag = <[u8; ENCRYPTED_MESSAGE_AUTH_SIZE]>::decode_bytes(&packet[tag_at..]) + .map_err(invalid_data)?; + if !crypto.aes256_gcm_decrypt( + key, + &Nonce::from_counter(nonce), + &packet[..tag_at], + &mut [], + &tag, + ) { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "authenticated packet tag mismatch", + )); + } + Ok(Packet { + kind, + payload: &packet[PACKET_HEADER_SIZE..tag_at], + }) +} + +#[cfg(feature = "tokio")] +pub fn open_packet_owned( + crypto: &impl QlAead, + key: &SessionKey, + nonce: u64, + frame: Frame, +) -> io::Result<(PacketKind, Vec)> { + let mut packet = frame.0; + let opened = open_packet(crypto, key, nonce, &packet)?; + let kind = opened.kind; + let payload_len = opened.payload.len(); + packet.copy_within(PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + payload_len, 0); + packet.truncate(payload_len); + Ok((kind, packet)) +} + +#[cfg(feature = "tokio")] +pub async fn read_frame(reader: &mut (impl AsyncRead + Unpin)) -> io::Result> { + let mut header = [0; FRAME_HEADER_SIZE]; + if reader.read(&mut header[..1]).await? == 0 { + return Ok(None); + } + reader.read_exact(&mut header[1..]).await?; + let length = u32::decode_bytes(header.as_slice()).map_err(invalid_data)? as usize; + if length > MAX_FRAME_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "frame exceeds transport limit", + )); + } + let mut frame = Vec::with_capacity(FRAME_HEADER_SIZE + length); + frame.extend_from_slice(&header); + frame.resize(FRAME_HEADER_SIZE + length, 0); + reader.read_exact(&mut frame[FRAME_HEADER_SIZE..]).await?; + Ok(Some(Frame(frame))) +} + +#[cfg(feature = "tokio")] +pub async fn write_frame(writer: &mut (impl AsyncWrite + Unpin), payload: &[u8]) -> io::Result<()> { + if payload.len() > MAX_FRAME_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "frame exceeds transport limit", + )); + } + let mut header = [0; FRAME_HEADER_SIZE]; + (payload.len() as u32).encode(&mut header.as_mut_slice()); + writer.write_all(&header).await?; + writer.write_all(payload).await +} + +#[cfg(feature = "tokio")] +pub async fn write_packet(writer: &mut (impl AsyncWrite + Unpin), packet: &[u8]) -> io::Result<()> { + writer.write_all(packet).await +} + +#[cfg(all(test, feature = "tokio"))] +mod tests { + use ql_wire::{SessionKey, SoftwareCrypto}; + + use super::{ + open_packet, open_packet_owned, seal_packet, Frame, PacketKind, FRAME_HEADER_SIZE, + PACKET_HEADER_SIZE, + }; + + #[test] + fn packet_authentication_covers_the_entire_frame_and_counter() { + let key = SessionKey([7; SessionKey::SIZE]); + let packet = seal_packet(&SoftwareCrypto, &key, PacketKind::Record, 4, b"record").unwrap(); + let (kind, payload) = + open_packet_owned(&SoftwareCrypto, &key, 4, Frame(packet.clone())).unwrap(); + assert_eq!(kind, PacketKind::Record); + assert_eq!(payload, b"record"); + assert!(open_packet(&SoftwareCrypto, &key, 5, &packet).is_err()); + + for index in [0, FRAME_HEADER_SIZE, PACKET_HEADER_SIZE] { + let mut changed = packet.clone(); + changed[index] ^= 1; + assert!(open_packet(&SoftwareCrypto, &key, 4, &changed).is_err()); + } + } +} diff --git a/ql-runtime/Cargo.toml b/ql-runtime/Cargo.toml index fef1396c..4e504b07 100644 --- a/ql-runtime/Cargo.toml +++ b/ql-runtime/Cargo.toml @@ -26,7 +26,7 @@ ql-wire = { workspace = true } env_logger = "0.11" log = "0.4" ql-codec = { workspace = true } -ql-wire = { workspace = true, features = ["test-utils"] } +ql-wire = { workspace = true, features = ["software-crypto"] } tokio = { version = "1.44", features = ["macros", "rt", "time", "sync"] } [target.'cfg(loom)'.dev-dependencies] diff --git a/ql-wire/Cargo.toml b/ql-wire/Cargo.toml index f18f8a22..ffd5f064 100644 --- a/ql-wire/Cargo.toml +++ b/ql-wire/Cargo.toml @@ -6,7 +6,7 @@ description = "QuantumLink protocol wire format" license = "Proprietary" [features] -test-utils = [ +software-crypto = [ "dep:getrandom", "dep:libcrux-aesgcm", "dep:libcrux-ml-kem", diff --git a/ql-wire/src/lib.rs b/ql-wire/src/lib.rs index bfd68e4a..4858b512 100644 --- a/ql-wire/src/lib.rs +++ b/ql-wire/src/lib.rs @@ -14,7 +14,7 @@ mod identity; mod pq; mod qid; mod record; -#[cfg(any(feature = "test-utils", test))] +#[cfg(any(feature = "software-crypto", test))] mod testing; pub use crypto::*; @@ -27,7 +27,7 @@ pub use identity::*; pub use pq::*; pub use qid::*; pub use record::*; -#[cfg(any(feature = "test-utils", test))] +#[cfg(any(feature = "software-crypto", test))] pub use testing::*; pub const QL_WIRE_VERSION: u8 = 2; From c40ea7dc280b454f8a513ea8337726ac8df6f431 Mon Sep 17 00:00:00 2001 From: Nico Burniske Date: Mon, 14 Sep 2026 10:49:18 -0400 Subject: [PATCH 17/17] ql-router: cookie --- Cargo.lock | 1 + ql-router/Cargo.toml | 4 + ql-router/src/client.rs | 36 +++-- ql-router/src/lib.rs | 21 ++- ql-router/src/protocol.rs | 284 +++++++++++++++++++++++++++++++++----- 5 files changed, 295 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 298d3822..5b456676 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1421,6 +1421,7 @@ version = "0.1.0" dependencies = [ "bytes", "ql-codec", + "ql-common", "ql-wire", "tokio", ] diff --git a/ql-router/Cargo.toml b/ql-router/Cargo.toml index 29bb6e12..53915629 100644 --- a/ql-router/Cargo.toml +++ b/ql-router/Cargo.toml @@ -12,5 +12,9 @@ tokio = ["dep:tokio", "ql-wire/software-crypto"] [dependencies] bytes = { workspace = true } ql-codec = { workspace = true } +ql-common = { workspace = true } ql-wire = { workspace = true } tokio = { version = "1", features = ["io-util", "net"], optional = true } + +[dev-dependencies] +ql-wire = { workspace = true, features = ["software-crypto"] } diff --git a/ql-router/src/client.rs b/ql-router/src/client.rs index ece4f637..049fe6a5 100644 --- a/ql-router/src/client.rs +++ b/ql-router/src/client.rs @@ -10,7 +10,10 @@ use tokio::net::{ TcpStream, ToSocketAddrs, }; -use crate::{invalid_data, protocol, protocol::PacketKind}; +use crate::{ + invalid_data, + protocol::{self, AttachChallenge, AttachRecord, PacketKind}, +}; pub struct Receiver { tcp: OwnedReadHalf, @@ -123,7 +126,12 @@ pub async fn receive_packet(receiver: &mut Receiver) -> io::Result io::Result<()> { @@ -155,7 +163,7 @@ pub async fn attach( let handshake_id = HandshakeId::decode_bytes(random.as_slice()).unwrap(); let request = ql_wire::encode_record_vec( RecordHeader::new(route, RecordType::Handshake), - &QlHandshakeRecord::Ik1( + &AttachRecord::Initiate( handshake .write_1(&SoftwareCrypto, handshake_id) .map_err(invalid_data)?, @@ -163,22 +171,21 @@ pub async fn attach( ); send_attach(sender, &request).await?; - let response = protocol::read_frame(&mut receiver.tcp) + let (kind, response) = receive_packet(receiver) .await? .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "router disconnected"))?; - let nonce = protocol::take_counter(&mut receiver.counter) - .ok_or_else(|| io::Error::other("nonce counter exhausted"))?; - let (kind, response) = - protocol::open_packet_owned(&SoftwareCrypto, &receiver.key, nonce, response)?; if kind != PacketKind::Attach { return Err(io::Error::new( io::ErrorKind::InvalidData, "unexpected router packet", )); } - let (header, response) = ql_wire::decode_record::(response.as_slice()) - .map_err(invalid_data)?; - let QlHandshakeRecord::Ik2(response) = response else { + let (header, response) = protocol::decode_attach(&response)?; + let AttachRecord::Challenge(AttachChallenge { + handshake: response, + cookie, + }) = response + else { return Err(io::Error::new( io::ErrorKind::InvalidData, "unexpected attachment handshake record", @@ -194,7 +201,12 @@ pub async fn attach( "attachment peer does not match router", )); } - Ok(()) + let confirmation = protocol::confirm_cookie(&SoftwareCrypto, &finalized.tx_key, cookie); + let confirmation = ql_wire::encode_record_vec( + RecordHeader::new(route, RecordType::Handshake), + &AttachRecord::Confirm(confirmation), + ); + send_attach(sender, &confirmation).await } async fn send_packet(sender: &mut Sender, kind: PacketKind, payload: &[u8]) -> io::Result<()> { diff --git a/ql-router/src/lib.rs b/ql-router/src/lib.rs index 82690fa3..3e9494d9 100644 --- a/ql-router/src/lib.rs +++ b/ql-router/src/lib.rs @@ -1,8 +1,24 @@ //! routing protocol for QuantumLink //! -//! one transport connection can attach multiple authenticated QIDs -//! enable the `tokio` feature for the TCP client +//! ```text +//! +//! 1. encrypt the TCP connection +//! client -> router: IK1 using an ephemeral identity +//! router -> client: IK2 +//! client -> router: encrypted confirmation +//! +//! 2. attach identity B to the same connection +//! connection -> router: B's IK1 +//! router -> connection: B's IK2 + encrypted cookie +//! connection -> router: cookie + B's proof +//! +//! 3. proof valid: route records for B to the connection +//! ``` +//! +//! the cookie holds B's QID, attachment key, connection, and expiry. the router +//! keeps no pending attachment state. replaying IK1 causes work but cannot attach B +#[cfg(feature = "tokio")] use std::io; #[cfg(feature = "tokio")] @@ -15,6 +31,7 @@ pub use client::*; pub const DEFAULT_ADDRESS: &str = "127.0.0.1:7447"; pub const MAX_RECORD_SIZE: usize = 8 * 1024; +#[cfg(feature = "tokio")] fn invalid_data(error: impl std::error::Error + Send + Sync + 'static) -> io::Error { io::Error::new(io::ErrorKind::InvalidData, error) } diff --git a/ql-router/src/protocol.rs b/ql-router/src/protocol.rs index bf8004a7..e647cf40 100644 --- a/ql-router/src/protocol.rs +++ b/ql-router/src/protocol.rs @@ -1,20 +1,75 @@ -//! framing for the encrypted router transport +//! attachment and framing for the encrypted router transport -use std::io; +use std::{fmt, io}; -use ql_codec::{codec, Decode, Encode}; +use ql_codec::{codec, Decode, Encode, Reader}; +use ql_common::QID; use ql_wire::{ - Nonce, QlAead, QlHandshakeRecord, RecordHeader, SessionKey, ENCRYPTED_MESSAGE_AUTH_SIZE, + Ik1, Ik2, Nonce, QlAead, QlHandshakeRecord, QlHash, QlRandom, RecordHeader, SessionKey, + ENCRYPTED_MESSAGE_AUTH_SIZE, }; #[cfg(feature = "tokio")] use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use crate::{invalid_data, MAX_RECORD_SIZE}; +#[cfg(feature = "tokio")] +use crate::invalid_data; +use crate::MAX_RECORD_SIZE; const FRAME_HEADER_SIZE: usize = 4; const PACKET_HEADER_SIZE: usize = FRAME_HEADER_SIZE + 1; const PACKET_OVERHEAD: usize = 1 + ENCRYPTED_MESSAGE_AUTH_SIZE; const MAX_FRAME_SIZE: usize = MAX_RECORD_SIZE + PACKET_OVERHEAD; +const COOKIE_STATE_SIZE: usize = QID::SIZE + SessionKey::SIZE + size_of::() * 2; +const COOKIE_DOMAIN: &[u8] = b"ql-router-cookie-v1"; +const CONFIRM_DOMAIN: &[u8] = b"ql-router-confirm-v1"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + Codec(ql_codec::Error), + InvalidFrame, + FrameTooLarge, + AuthenticationFailed, + ExpiredCookie, + WrongConnection, + TrailingData, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Codec(error) => error.fmt(f), + Self::InvalidFrame => f.write_str("invalid frame"), + Self::FrameTooLarge => f.write_str("frame exceeds transport limit"), + Self::AuthenticationFailed => f.write_str("authentication failed"), + Self::ExpiredCookie => f.write_str("expired attachment cookie"), + Self::WrongConnection => f.write_str("attachment cookie belongs to another connection"), + Self::TrailingData => f.write_str("trailing attachment data"), + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(error: ql_codec::Error) -> Self { + Self::Codec(error) + } +} + +impl From for io::Error { + fn from(error: Error) -> Self { + let kind = match error { + Error::FrameTooLarge => io::ErrorKind::InvalidInput, + Error::AuthenticationFailed | Error::ExpiredCookie | Error::WrongConnection => { + io::ErrorKind::PermissionDenied + } + Error::Codec(_) | Error::InvalidFrame | Error::TrailingData => { + io::ErrorKind::InvalidData + } + }; + Self::new(kind, error) + } +} codec! { #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -32,6 +87,142 @@ codec! { } } +codec! { + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum AttachRecord as AttachKind { + Initiate(Ik1) = 1, + Challenge(AttachChallenge) = 2, + Confirm(AttachConfirm) = 3, + } +} + +codec! { + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct AttachChallenge { + pub handshake: Ik2, + pub cookie: AttachCookie, + } +} + +codec! { + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct AttachConfirm { + pub cookie: AttachCookie, + pub tag: [u8; ENCRYPTED_MESSAGE_AUTH_SIZE], + } +} + +codec! { + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct AttachCookie { + pub nonce: Nonce, + pub ciphertext: [u8; COOKIE_STATE_SIZE], + pub tag: [u8; ENCRYPTED_MESSAGE_AUTH_SIZE], + } +} + +ql_codec::codec! { + #[derive(Debug, Clone, PartialEq, Eq)] + struct CookieState { + qid: QID, + key: SessionKey, + connection_id: u64, + expires_at: u64, + } +} + +pub fn seal_cookie( + crypto: &(impl QlAead + QlRandom), + key: &SessionKey, + qid: QID, + confirmation_key: SessionKey, + connection_id: u64, + expires_at: u64, +) -> AttachCookie { + let mut nonce = Nonce([0; Nonce::SIZE]); + crypto.fill_random_bytes(&mut nonce.0); + let mut encoded = [0; COOKIE_STATE_SIZE]; + CookieState { + qid, + key: confirmation_key, + connection_id, + expires_at, + } + .encode(&mut encoded.as_mut_slice()); + let tag = crypto.aes256_gcm_encrypt(key, &nonce, COOKIE_DOMAIN, &mut encoded); + AttachCookie { + nonce, + ciphertext: encoded, + tag, + } +} + +pub fn confirm_cookie( + crypto: &(impl QlAead + QlHash), + key: &SessionKey, + cookie: AttachCookie, +) -> AttachConfirm { + let aad = crypto.sha256(&[ + CONFIRM_DOMAIN, + &cookie.nonce.0, + &cookie.ciphertext, + &cookie.tag, + ]); + let tag = crypto.aes256_gcm_encrypt(key, &Nonce::from_counter(0), &aad, &mut []); + AttachConfirm { cookie, tag } +} + +pub fn open_confirmation( + crypto: &(impl QlAead + QlHash), + key: &SessionKey, + connection_id: u64, + now: u64, + confirmation: &AttachConfirm, +) -> Result { + let mut state = confirmation.cookie.ciphertext; + if !crypto.aes256_gcm_decrypt( + key, + &confirmation.cookie.nonce, + COOKIE_DOMAIN, + &mut state, + &confirmation.cookie.tag, + ) { + return Err(Error::AuthenticationFailed); + } + let state = CookieState::decode_bytes(state.as_slice())?; + if state.connection_id != connection_id { + return Err(Error::WrongConnection); + } + if state.expires_at <= now { + return Err(Error::ExpiredCookie); + } + let aad = crypto.sha256(&[ + CONFIRM_DOMAIN, + &confirmation.cookie.nonce.0, + &confirmation.cookie.ciphertext, + &confirmation.cookie.tag, + ]); + if !crypto.aes256_gcm_decrypt( + &state.key, + &Nonce::from_counter(0), + &aad, + &mut [], + &confirmation.tag, + ) { + return Err(Error::AuthenticationFailed); + } + Ok(state.qid) +} + +pub fn decode_attach(bytes: &[u8]) -> Result<(RecordHeader, AttachRecord), Error> { + let mut reader = Reader::new(bytes); + let decoded = (reader.decode()?, reader.decode()?); + if !reader.is_empty() { + return Err(Error::TrailingData); + } + Ok(decoded) +} + #[cfg(feature = "tokio")] pub struct Frame(Vec); @@ -64,7 +255,7 @@ pub fn seal_packet( kind: PacketKind, nonce: u64, payload: &[u8], -) -> io::Result> { +) -> Result, Error> { let mut packet = Vec::with_capacity(FRAME_HEADER_SIZE + PACKET_OVERHEAD + payload.len()); seal_packet_into(crypto, &mut packet, key, kind, nonce, payload)?; Ok(packet) @@ -77,13 +268,11 @@ pub fn seal_packet_into( kind: PacketKind, nonce: u64, payload: &[u8], -) -> io::Result<()> { +) -> Result<(), Error> { let length = PACKET_OVERHEAD .checked_add(payload.len()) .filter(|length| *length <= MAX_FRAME_SIZE) - .ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidInput, "frame exceeds transport limit") - })?; + .ok_or(Error::FrameTooLarge)?; packet.clear(); packet.reserve(FRAME_HEADER_SIZE + length); (length as u32).encode(packet); @@ -99,24 +288,17 @@ pub fn open_packet<'a>( key: &SessionKey, nonce: u64, packet: &'a [u8], -) -> io::Result> { +) -> Result, Error> { if packet.len() < FRAME_HEADER_SIZE + PACKET_OVERHEAD { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "invalid authenticated frame", - )); + return Err(Error::InvalidFrame); } - let length = u32::decode_bytes(&packet[..FRAME_HEADER_SIZE]).map_err(invalid_data)?; + let length = u32::decode_bytes(&packet[..FRAME_HEADER_SIZE])?; if length as usize != packet.len() - FRAME_HEADER_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "invalid authenticated frame length", - )); + return Err(Error::InvalidFrame); } - let kind = PacketKind::decode_bytes(&packet[FRAME_HEADER_SIZE..]).map_err(invalid_data)?; + let kind = PacketKind::decode_bytes(&packet[FRAME_HEADER_SIZE..])?; let tag_at = packet.len() - ENCRYPTED_MESSAGE_AUTH_SIZE; - let tag = <[u8; ENCRYPTED_MESSAGE_AUTH_SIZE]>::decode_bytes(&packet[tag_at..]) - .map_err(invalid_data)?; + let tag = <[u8; ENCRYPTED_MESSAGE_AUTH_SIZE]>::decode_bytes(&packet[tag_at..])?; if !crypto.aes256_gcm_decrypt( key, &Nonce::from_counter(nonce), @@ -124,10 +306,7 @@ pub fn open_packet<'a>( &mut [], &tag, ) { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "authenticated packet tag mismatch", - )); + return Err(Error::AuthenticationFailed); } Ok(Packet { kind, @@ -141,7 +320,7 @@ pub fn open_packet_owned( key: &SessionKey, nonce: u64, frame: Frame, -) -> io::Result<(PacketKind, Vec)> { +) -> Result<(PacketKind, Vec), Error> { let mut packet = frame.0; let opened = open_packet(crypto, key, nonce, &packet)?; let kind = opened.kind; @@ -160,10 +339,7 @@ pub async fn read_frame(reader: &mut (impl AsyncRead + Unpin)) -> io::Result MAX_FRAME_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "frame exceeds transport limit", - )); + return Err(Error::FrameTooLarge.into()); } let mut frame = Vec::with_capacity(FRAME_HEADER_SIZE + length); frame.extend_from_slice(&header); @@ -175,10 +351,7 @@ pub async fn read_frame(reader: &mut (impl AsyncRead + Unpin)) -> io::Result io::Result<()> { if payload.len() > MAX_FRAME_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "frame exceeds transport limit", - )); + return Err(Error::FrameTooLarge.into()); } let mut header = [0; FRAME_HEADER_SIZE]; (payload.len() as u32).encode(&mut header.as_mut_slice()); @@ -191,15 +364,19 @@ pub async fn write_packet(writer: &mut (impl AsyncWrite + Unpin), packet: &[u8]) writer.write_all(packet).await } -#[cfg(all(test, feature = "tokio"))] +#[cfg(test)] mod tests { + use ql_common::QID; use ql_wire::{SessionKey, SoftwareCrypto}; + use super::{confirm_cookie, open_confirmation, seal_cookie}; + #[cfg(feature = "tokio")] use super::{ open_packet, open_packet_owned, seal_packet, Frame, PacketKind, FRAME_HEADER_SIZE, PACKET_HEADER_SIZE, }; + #[cfg(feature = "tokio")] #[test] fn packet_authentication_covers_the_entire_frame_and_counter() { let key = SessionKey([7; SessionKey::SIZE]); @@ -216,4 +393,37 @@ mod tests { assert!(open_packet(&SoftwareCrypto, &key, 4, &changed).is_err()); } } + + #[test] + fn attachment_cookie_confirms_private_key_possession() { + let cookie_key = SessionKey([1; SessionKey::SIZE]); + let qid = QID([2; QID::SIZE]); + let confirmation_key = SessionKey([3; SessionKey::SIZE]); + let cookie = seal_cookie( + &SoftwareCrypto, + &cookie_key, + qid, + confirmation_key.clone(), + 4, + 6, + ); + let forged = confirm_cookie( + &SoftwareCrypto, + &SessionKey([4; SessionKey::SIZE]), + cookie.clone(), + ); + assert!(open_confirmation(&SoftwareCrypto, &cookie_key, 4, 5, &forged).is_err()); + + let confirmation = confirm_cookie(&SoftwareCrypto, &confirmation_key, cookie); + assert_eq!( + open_confirmation(&SoftwareCrypto, &cookie_key, 4, 5, &confirmation).unwrap(), + qid + ); + assert!(open_confirmation(&SoftwareCrypto, &cookie_key, 5, 5, &confirmation).is_err()); + assert!(open_confirmation(&SoftwareCrypto, &cookie_key, 4, 6, &confirmation).is_err()); + + let mut changed = confirmation; + changed.tag[0] ^= 1; + assert!(open_confirmation(&SoftwareCrypto, &cookie_key, 4, 5, &changed).is_err()); + } }