diff --git a/Cargo.lock b/Cargo.lock index 38355b76..2681b91c 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 = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +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,19 @@ dependencies = [ "syn", ] +[[package]] +name = "ql-api" +version = "0.1.0" +dependencies = [ + "bytes", + "ciborium", + "flutter_rust_bridge", + "ql-codec", + "ql-keyos", + "ql-rpc", + "serde", +] + [[package]] name = "ql-codec" version = "0.1.0" @@ -1077,6 +1405,16 @@ dependencies = [ "ql-wire", ] +[[package]] +name = "ql-keyos" +version = "0.1.0" +dependencies = [ + "bytes", + "ql-codec", + "ql-rpc", + "serde", +] + [[package]] name = "ql-rpc" version = "0.1.0" @@ -1188,6 +1526,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 +1652,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 +1803,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 +2040,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 +2085,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..ca1022e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,8 @@ members = [ "btp", "ql-codec", "ql-common", + "ql-keyos", + "ql-api", "ql-fsm", "ql-rpc", "ql-runtime", @@ -27,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 new file mode 100644 index 00000000..27f93d57 --- /dev/null +++ b/ql-api/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ql-api" +version = "0.1.0" +edition = "2021" +description = "KeyOS QLv2 RPC protocol" +license = "Proprietary" + + +[dependencies] +ql-codec = { workspace = true } +ql-keyos = { workspace = true, features = ["serde"] } +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..2463a3c0 --- /dev/null +++ b/ql-api/src/app_store.rs @@ -0,0 +1,65 @@ +use ql_rpc::{Download, Request}; + +use crate::{Empty, Error}; + +// COMMON TYPES + +rpc! { + #[derive(Copy)] + pub struct AppVersion(pub u32, pub u32, pub u32); +} + +rpc! { + pub struct AppDetails { + pub name: String, + pub icon: String, // SVG? + pub developer_id: Vec, + pub app_id: Vec, + } +} + +// APP ROUTES + +// SERVICE ROUTES + +service_routes! { + crate::service_id::APP_STORE => { + RequestListApps: Request = 1, + DownloadApp: Download = 2, + } +} + +rpc! { + pub struct ListAppsResponse { + pub apps: Vec, + } +} + +impl Request for RequestListApps { + type Error = Error; + type Request = Empty; + type Response = ListAppsResponse; +} + +rpc! { + pub struct DownloadAppParams { + pub version: Option, + } +} + +rpc! { + pub struct DownloadAppHeader { + pub version: String, + } +} + +rpc! { + pub struct DownloadAppPartHeader {} +} + +impl Download for DownloadApp { + type Error = Error; + type Request = DownloadAppParams; + type ResponseHeader = DownloadAppHeader; + type PartHeader = DownloadAppPartHeader; +} diff --git a/ql-api/src/backup.rs b/ql-api/src/backup.rs new file mode 100644 index 00000000..f49a7996 --- /dev/null +++ b/ql-api/src/backup.rs @@ -0,0 +1,172 @@ +use ql_rpc::{Download, Request, Subscription, Upload}; + +use crate::{Empty, Error}; + +// COMMON TYPES + +rpc! { + pub struct Shard(pub Vec); +} + +rpc! { + pub struct SeedFingerprint(pub [u8; 32]); +} + +// APP ROUTES + +app_routes! { + crate::app_id::BACKUP => { + SubscribePassportMagicBackupEnabled: Subscription = 1, + SubscribeMagicBackupRestoreCompleted: Subscription = 2, + } +} + +rpc! { + pub struct PassportMagicBackupEnabled { + pub enabled: bool, + pub seed_fingerprint: SeedFingerprint, + } +} + +impl Subscription for SubscribePassportMagicBackupEnabled { + type Error = Error; + type Request = Empty; + type Event = PassportMagicBackupEnabled; +} + +rpc! { + pub enum MagicBackupRestoreResult { + Success, + Error { error: String }, + } +} + +impl Subscription for SubscribeMagicBackupRestoreCompleted { + type Error = Error; + 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 BackupShardParams { + pub shard: Shard, + } +} + +rpc! { + pub enum BackupShardResponse { + Success, + Error { error: String }, + } +} + +impl Request for RequestBackupShard { + type Error = Error; + type Request = BackupShardParams; + type Response = BackupShardResponse; +} + +rpc! { + pub struct RestoreShardParams { + pub seed_fingerprint: SeedFingerprint, + pub timestamp: Option, + } +} + +rpc! { + pub enum RestoreShardResponse { + Success { shard: Shard }, + Error { error: String }, + NotFound, + } +} + +impl Request for RequestRestoreShard { + type Error = Error; + type Request = RestoreShardParams; + type Response = RestoreShardResponse; +} + +rpc! { + pub struct PassportMagicBackupStatusParams { + pub seed_fingerprint: SeedFingerprint, + pub timestamp: Option, + } +} + +rpc! { + pub struct PassportMagicBackupStatusResponse { + pub shard_backup_found: bool, + } +} + +impl Request for RequestPassportMagicBackupStatus { + type Error = Error; + type Request = PassportMagicBackupStatusParams; + type Response = PassportMagicBackupStatusResponse; +} + +rpc! { + pub struct UploadMagicBackupParams { + pub seed_fingerprint: SeedFingerprint, + pub total_size: Option, + pub hash: [u8; 32], + } +} + +rpc! { + pub enum UploadMagicBackupResponse { + Success, + Error { error: String }, + } +} + +rpc! { + pub struct UploadMagicBackupPartHeader {} +} + +impl Upload for UploadMagicBackup { + type Error = Error; + type Request = UploadMagicBackupParams; + type PartHeader = UploadMagicBackupPartHeader; + type Response = UploadMagicBackupResponse; +} + +rpc! { + pub struct DownloadMagicBackupParams { + pub seed_fingerprint: SeedFingerprint, + } +} + +rpc! { + pub struct BackupMetadata { + pub total_size: Option, + } +} + +rpc! { + pub enum DownloadMagicBackupHeader { + Found(BackupMetadata), + NotFound, + Error { error: String }, + } +} + +impl Download for DownloadMagicBackup { + type Error = Error; + type Request = DownloadMagicBackupParams; + type ResponseHeader = DownloadMagicBackupHeader; + type PartHeader = Empty; +} diff --git a/ql-api/src/bitcoin.rs b/ql-api/src/bitcoin.rs new file mode 100644 index 00000000..8c2ab063 --- /dev/null +++ b/ql-api/src/bitcoin.rs @@ -0,0 +1,97 @@ +use ql_rpc::{Request, Subscription}; + +use crate::{Empty, Error}; + +// COMMON TYPES + +rpc! { + pub struct ActiveSeedFingerprint { + pub fingerprint: String, + pub has_passphrase: bool, + } +} + +// APP ROUTES + +app_routes! { + crate::app_id::BITCOIN => { + RequestSignPsbt: Request = 1, + SubscribeAccountUpdated: Subscription = 2, + RequestPassportActiveSeedFingerprint: Request = 3, + SubscribePassportActiveSeedFingerprint: Subscription = 4, + } +} + +rpc! { + pub struct SignPsbtParams { + pub account_id: String, + pub psbt: Vec, + } +} + +rpc! { + pub enum SignPsbtResponse { + Signed { psbt: Vec }, + Rejected, + Error { error: String }, + } +} + +impl Request for RequestSignPsbt { + type Error = Error; + type Request = SignPsbtParams; + type Response = SignPsbtResponse; +} + +rpc! { + pub struct AccountUpdatedEvent { + pub account_id: String, + pub update: Vec, + } +} + +impl Subscription for SubscribeAccountUpdated { + type Error = Error; + type Request = Empty; + type Event = AccountUpdatedEvent; +} + +impl Subscription for SubscribePassportActiveSeedFingerprint { + type Error = Error; + type Request = Empty; + type Event = ActiveSeedFingerprint; +} + +impl Request for RequestPassportActiveSeedFingerprint { + type Error = Error; + type Request = Empty; + type Response = ActiveSeedFingerprint; +} + +// SERVICE ROUTES + +service_routes! { + crate::service_id::BITCOIN => { + RequestBroadcastTransaction: Request = 1, + } +} + +rpc! { + pub struct BroadcastTransactionParams { + pub account_id: String, + pub psbt: Vec, + } +} + +rpc! { + pub enum BroadcastTransactionResponse { + Broadcast { txid: String }, + Error { error: String }, + } +} + +impl Request for RequestBroadcastTransaction { + type Error = Error; + type Request = BroadcastTransactionParams; + type Response = BroadcastTransactionResponse; +} diff --git a/ql-api/src/codec.rs b/ql-api/src/codec.rs new file mode 100644 index 00000000..a90ce607 --- /dev/null +++ b/ql-api/src/codec.rs @@ -0,0 +1,120 @@ +use std::io; + +use bytes::{Buf, BufMut}; +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, + 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) + } +} + +#[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..276a6e96 --- /dev/null +++ b/ql-api/src/debug.rs @@ -0,0 +1,75 @@ +use ql_rpc::{request::Request, Download}; + +use crate::Error; + +// APP ROUTES + +app_routes! { + crate::app_id::DEBUG => { + RequestPassportEcho: Request = 1, + DownloadPassportBenchmark: Download = 2, + } +} + +rpc! { + pub struct EchoParams { + pub message: String, + } +} + +rpc! { + pub struct EchoResponse { + pub message: String, + } +} + +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 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, + DownloadBenchmark: Download = 3, + } +} + +impl Request for RequestEcho { + type Error = Error; + type Request = EchoParams; + type Response = EchoResponse; +} + +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 new file mode 100644 index 00000000..c884d8f0 --- /dev/null +++ b/ql-api/src/firmware.rs @@ -0,0 +1,107 @@ +use ql_rpc::{Download, Request, Subscription}; + +use crate::{Empty, Error}; + +// COMMON TYPES + +rpc! { + 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 InstallErrorStage { + Download, + Verify, + Install, + } +} + +rpc! { + pub enum FirmwareInstallEvent { + Installing, + Rebooting, + Success { + installed_version: String, + }, + Error { + error: String, + stage: InstallErrorStage, + }, + } +} + +impl Subscription for SubscribeFirmwareInstallStatus { + type Error = Error; + type Request = Empty; + type Event = FirmwareInstallEvent; +} + +// SERVICE ROUTES + +service_routes! { + crate::service_id::FIRMWARE => { + RequestCheckFirmwareUpdate: Request = 1, + DownloadFirmware: Download = 2, + } +} + +rpc! { + pub struct FirmwareUpdateCheckParams { + pub current_version: String, + } +} + +rpc! { + pub enum FirmwareUpdateCheckResponse { + Available(FirmwareUpdateAvailable), + NotAvailable, + } +} + +impl Request for RequestCheckFirmwareUpdate { + type Error = Error; + type Request = FirmwareUpdateCheckParams; + type Response = FirmwareUpdateCheckResponse; +} + +rpc! { + pub struct DownloadFirmwareParams { + pub current_version: String, + } +} + +rpc! { + pub enum DownloadFirmwareHeader { + Available(FirmwareUpdateAvailable), + NotAvailable, + Error { error: String }, + } +} + +rpc! { + pub struct DownloadFirmwarePartHeader { + pub patch_name: String, + pub size_bytes: u64, + } +} + +impl Download for DownloadFirmware { + type Error = Error; + type Request = DownloadFirmwareParams; + type ResponseHeader = DownloadFirmwareHeader; + type PartHeader = DownloadFirmwarePartHeader; +} diff --git a/ql-api/src/fx.rs b/ql-api/src/fx.rs new file mode 100644 index 00000000..aa561fdc --- /dev/null +++ b/ql-api/src/fx.rs @@ -0,0 +1,65 @@ +use ql_rpc::{Request, Subscription}; + +use crate::Error; + +// COMMON TYPES + +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, + } +} + +// APP ROUTES + +// SERVICE ROUTES + +service_routes! { + crate::service_id::FX => { + SubscribeExchangeRate: Subscription = 1, + RequestExchangeRateHistory: Request = 2, + } +} + +rpc! { + pub struct ExchangeRateParams { + pub currency_code: String, + } +} + +impl Subscription for SubscribeExchangeRate { + type Error = Error; + type Request = ExchangeRateParams; + type Event = ExchangeRate; +} + +rpc! { + pub struct ExchangeRateHistoryParams { + pub currency_code: String, + } +} + +rpc! { + #[PartialEq] + pub struct ExchangeRateHistory { + pub history: Vec, + pub currency_code: String, + } +} + +impl Request for RequestExchangeRateHistory { + type Error = Error; + 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..822ce202 --- /dev/null +++ b/ql-api/src/key.rs @@ -0,0 +1,25 @@ +pub use ql_keyos::{AppRouteKey, ServiceRouteKey}; + +pub mod app_id { + use ql_keyos::AppId; + + 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"); + 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(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); +} diff --git a/ql-api/src/lib.rs b/ql-api/src/lib.rs new file mode 100644 index 00000000..47c1f1df --- /dev/null +++ b/ql-api/src/lib.rs @@ -0,0 +1,46 @@ +#[macro_use] +mod macros; + +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 system; +pub mod time; + +pub use self::{ + app_store::*, backup::*, bitcoin::*, codec::Empty, debug::*, firmware::*, fx::*, key::*, + onboarding::*, scv::*, system::*, time::*, +}; + +pub type Error = ciborium::de::Error; + +// 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 new file mode 100644 index 00000000..539a4e35 --- /dev/null +++ b/ql-api/src/macros.rs @@ -0,0 +1,128 @@ +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! 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 = ql_keyos::ServiceRouteKey; + + fn key() -> Self::Key { + ql_keyos::ServiceRouteKey { + service_id: $service_id, + route_id: ql_keyos::RouteId($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; + + impl ql_rpc::Route for $route { + type Key = ql_keyos::AppRouteKey; + + fn key() -> Self::Key { + ql_keyos::AppRouteKey { + app_id: $app_id, + route_id: ql_keyos::RouteId($id), + } + } + } + + assert_route_impl!($route: $kind); + )* + }; +} + +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..1a5ac9e6 --- /dev/null +++ b/ql-api/src/onboarding.rs @@ -0,0 +1,37 @@ +use ql_rpc::Subscription; + +use crate::{Empty, Error}; + +// APP ROUTES + +app_routes! { + crate::app_id::ONBOARDING => { + SubscribeOnboardingStatus: Subscription = 1, + } +} + +rpc! { + pub enum OnboardingState { + FirmwareUpdateScreen, + SecuringDevice, + DeviceSecured, + WalletCreationScreen, + CreatingWallet, + WalletCreated, + MagicBackupScreen, + CreatingMagicBackup, + MagicBackupCreated, + CreatingManualBackup, + CreatingKeycardBackup, + WritingDownSeedWords, + Completed, + } +} + +impl Subscription for SubscribeOnboardingStatus { + type Error = Error; + type Request = Empty; + type Event = OnboardingState; +} + +// SERVICE ROUTES diff --git a/ql-api/src/scv.rs b/ql-api/src/scv.rs new file mode 100644 index 00000000..4c7e2a8d --- /dev/null +++ b/ql-api/src/scv.rs @@ -0,0 +1,54 @@ +use ql_rpc::Duplex; + +use crate::Error; + +// APP ROUTES + +// SERVICE ROUTES + +service_routes! { + crate::service_id::SCV => { + RunSecurityCheck: Duplex = 1, + } +} + +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 RunSecurityCheckResponderEvent { + ChallengeRequest(ChallengeRequest), + VerificationResult(VerificationResult), + } +} + +rpc! { + pub enum RunSecurityCheckInitiatorEvent { + Start, + ChallengeResponse(ChallengeResponseResult), + } +} + +impl Duplex for RunSecurityCheck { + type Error = Error; + type InitiatorEvent = RunSecurityCheckInitiatorEvent; + type ResponderEvent = RunSecurityCheckResponderEvent; +} diff --git a/ql-api/src/system.rs b/ql-api/src/system.rs new file mode 100644 index 00000000..780751b9 --- /dev/null +++ b/ql-api/src/system.rs @@ -0,0 +1,142 @@ +use ql_keyos::{AppId, PeerPermissions}; +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, + RequestPeerPermissions: Request = 6, + RequestInstallPeerBundles: Request = 7, + } +} + +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; +} + +rpc! { + pub struct PeerPermissionsParams(pub PeerPermissions); +} + +rpc! { + pub enum PeerPermissionsResponse { + Updated, + Denied, + } +} + +impl Request for RequestPeerPermissions { + type Error = Error; + type Request = PeerPermissionsParams; + 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 diff --git a/ql-api/src/time.rs b/ql-api/src/time.rs new file mode 100644 index 00000000..3033cf67 --- /dev/null +++ b/ql-api/src/time.rs @@ -0,0 +1,39 @@ +use ql_rpc::Request; + +use crate::{Empty, Error}; + +// APP ROUTES + +// SERVICE ROUTES + +service_routes! { + crate::service_id::TIME => { + RequestTimezone: Request = 1, + RequestCurrentTime: Request = 2, + } +} + +rpc! { + pub struct TimezoneResponse { + pub offset_minutes: i32, + pub zone: String, + } +} + +impl Request for RequestTimezone { + type Error = Error; + type Request = Empty; + type Response = TimezoneResponse; +} + +rpc! { + pub struct CurrentTimeResponse { + pub server_time_millis: u64, + } +} + +impl Request for RequestCurrentTime { + type Error = Error; + type Request = Empty; + type Response = CurrentTimeResponse; +} diff --git a/ql-fsm/src/pairing.rs b/ql-fsm/src/pairing.rs index fb9b0532..12eec3e8 100644 --- a/ql-fsm/src/pairing.rs +++ b/ql-fsm/src/pairing.rs @@ -1,9 +1,16 @@ use ql_common::QID; use ql_wire::PairingToken; -/// Out-of-band invite consumed by the initiator of an XX pairing -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PairingInvite { - pub qid: QID, - pub token: PairingToken, +ql_codec::codec! { + /// Out-of-band invite consumed by the initiator of an XX pairing + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct PairingInvite { + pub version: u8, + pub qid: QID, + pub token: PairingToken, + } +} + +impl PairingInvite { + pub const VERSION: u8 = 1; } diff --git a/ql-fsm/src/tests/handshake.rs b/ql-fsm/src/tests/handshake.rs index b517dcc1..3ffe083a 100644 --- a/ql-fsm/src/tests/handshake.rs +++ b/ql-fsm/src/tests/handshake.rs @@ -106,6 +106,7 @@ fn connect_methods_require_bound_peer() { fsm.connect_xx( time, PairingInvite { + version: PairingInvite::VERSION, qid: ql_common::QID([2; ql_common::QID::SIZE]), token: pairing_token(2), }, diff --git a/ql-fsm/src/tests/mod.rs b/ql-fsm/src/tests/mod.rs index d7003faa..60986ae3 100644 --- a/ql-fsm/src/tests/mod.rs +++ b/ql-fsm/src/tests/mod.rs @@ -203,6 +203,7 @@ impl Harness { fsm.connect_xx( time, PairingInvite { + version: PairingInvite::VERSION, qid: remote_qid, token, }, diff --git a/ql-keyos/Cargo.toml b/ql-keyos/Cargo.toml new file mode 100644 index 00000000..caa16da1 --- /dev/null +++ b/ql-keyos/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ql-keyos" +version = "0.1.0" +edition = "2021" +description = "KeyOS QuantumLink addressing primitives" +license = "Proprietary" + +[dependencies] +bytes = { workspace = true } +ql-codec = { workspace = true } +ql-rpc = { 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..4601d46b --- /dev/null +++ b/ql-keyos/src/lib.rs @@ -0,0 +1,142 @@ +//! 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)] +pub struct AppId(pub [u8; Self::SIZE]); + +impl AppId { + pub const SIZE: usize = 16; + + pub const fn from_hex(hex: &str) -> Self { + const fn 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] = (nibble(bytes[i * 2 + 2]) << 4) | nibble(bytes[i * 2 + 3]); + i += 1; + } + + Self(app_id) + } +} + +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 PeerPermissions { + /// KeyOS app ids this peer is allowed to open streams toward + pub app_ids: Vec, +} + +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) + } + } + }; +} + +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 +); + +ql_codec::varint_wrapper!(RouteId, u32); +ql_codec::varint_wrapper!(ServiceId, u64); + +#[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) + } +} diff --git a/ql-runtime/src/tests/handshake.rs b/ql-runtime/src/tests/handshake.rs index fb915d4d..805371e0 100644 --- a/ql-runtime/src/tests/handshake.rs +++ b/ql-runtime/src/tests/handshake.rs @@ -139,6 +139,7 @@ async fn start_pairing_round_trip_connects_when_armed() { handle_b.arm_pairing(token); handle_a.start_pairing(PairingInvite { + version: PairingInvite::VERSION, qid: identity_b.qid, token, }); @@ -168,6 +169,7 @@ async fn start_pairing_does_not_connect_when_unarmed() { spawn_forwarder(outbound_b, inbound_a_tx); handle_a.start_pairing(PairingInvite { + version: PairingInvite::VERSION, qid: identity_b.qid, token, });