diff --git a/AGENTS.md b/AGENTS.md index fb2d230..d935f50 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,7 +66,7 @@ cargo insta review The server is actor-based using the **kameo** crate. All long-lived state lives in `GlobalActors`: - **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run. -- **`KeyHolder`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell. +- **`Vault`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell. - **`FlowCoordinator`** — Coordinates cross-connection flow between user agents and SDK clients. - **`EvmActor`** — Handles EVM transaction policy enforcement and signing. diff --git a/CLAUDE.md b/CLAUDE.md index c3c3357..8305248 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,7 +66,7 @@ cargo insta review The server is actor-based using the **kameo** crate. All long-lived state lives in `GlobalActors`: - **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run. -- **`KeyHolder`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell. +- **`Vault`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell. - **`FlowCoordinator`** — Coordinates cross-connection flow between user agents and SDK clients. - **`EvmActor`** — Handles EVM transaction policy enforcement and signing. diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index a1816b0..3a0bfd9 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -29,38 +29,23 @@ flowchart TD A([Client connects]) --> B[Receive AuthChallengeRequest] B --> C{pubkey in DB?} - C -- yes --> D[Read nonce\nIncrement nonce in DB] - D --> G + C -- yes --> G[Generate AuthChallenge] C -- no --> E[Ask all UserAgents:\nClientConnectionRequest] E --> F{First response} F -- denied --> Z([Reject connection]) F -- approved --> F2[Cancel remaining\nUserAgent requests] - F2 --> F3[INSERT client\nnonce = 1] - F3 --> G[Send AuthChallenge\nwith nonce] + F2 --> F3[INSERT client] + F3 --> G - G --> H[Receive AuthChallengeSolution] - H --> I{Signature valid?} - I -- no --> Z - I -- yes --> J([Session started]) + G --> H[Send AuthChallenge\ntimestamp + random bytes] + H --> I[Receive AuthChallengeSolution] + I --> K{Signature valid?} + K -- no --> Z + K -- yes --> J([Session started]) ``` -### Known Issue: Concurrent Registration Race (TOCTOU) - -Two connections presenting the same previously-unknown public key can race through the approval flow simultaneously: - -1. Both check the DB → neither is registered. -2. Both request approval from user agents → both receive approval. -3. Both `INSERT` the client record → the second insert silently overwrites the first, resetting the nonce. - -This means the first connection's nonce is invalidated by the second, causing its challenge verification to fail. A fix requires either serialising new-client registration (e.g. an in-memory lock keyed on pubkey) or replacing the separate check + insert with an `INSERT OR IGNORE` / upsert guarded by a unique constraint on `public_key`. - -### Nonce Semantics - -The `program_client.nonce` column stores the **next usable nonce** — i.e. it is always one ahead of the nonce last issued in a challenge. - -- **New client:** inserted with `nonce = 1`; the first challenge is issued with `nonce = 0`. -- **Existing client:** the current DB value is read and used as the challenge nonce, then immediately incremented within the same exclusive transaction, preventing replay. +Auth challenges are generated from fresh random bytes plus a timestamp. They are signed as the canonical challenge payload and are not persisted in `program_client`. --- diff --git a/mise.lock b/mise.lock index 56e1cdd..08d8cb1 100644 --- a/mise.lock +++ b/mise.lock @@ -72,6 +72,10 @@ backend = "cargo:diesel_cli" default-features = "false" features = "sqlite,sqlite-bundled" +[[tools."cargo:flutter_rust_bridge_codegen"]] +version = "2.12.0" +backend = "cargo:flutter_rust_bridge_codegen" + [[tools.flutter]] version = "3.38.9-stable" backend = "asdf:flutter" diff --git a/mise.toml b/mise.toml index b45baa0..4ca43db 100644 --- a/mise.toml +++ b/mise.toml @@ -13,6 +13,7 @@ python = "3.14.3" ast-grep = "0.42.0" "cargo:cargo-edit" = "0.13.9" "cargo:cargo-mutants" = "27.0.0" +"cargo:flutter_rust_bridge_codegen" = "2.12.0" [tasks.codegen] sources = ['protobufs/*.proto', 'protobufs/**/*.proto'] diff --git a/protobufs/client/auth.proto b/protobufs/client/auth.proto index f3d7d2d..382cd23 100644 --- a/protobufs/client/auth.proto +++ b/protobufs/client/auth.proto @@ -10,8 +10,8 @@ message AuthChallengeRequest { } message AuthChallenge { - bytes pubkey = 1; - int32 nonce = 2; + uint64 timestamp_nanos = 1; + bytes random = 2; } message AuthChallengeSolution { diff --git a/protobufs/user_agent/auth.proto b/protobufs/user_agent/auth.proto index d2c5528..291084e 100644 --- a/protobufs/user_agent/auth.proto +++ b/protobufs/user_agent/auth.proto @@ -2,21 +2,14 @@ syntax = "proto3"; package arbiter.user_agent.auth; -enum KeyType { - KEY_TYPE_UNSPECIFIED = 0; - KEY_TYPE_ED25519 = 1; - KEY_TYPE_ECDSA_SECP256K1 = 2; - KEY_TYPE_RSA = 3; -} - message AuthChallengeRequest { bytes pubkey = 1; optional string bootstrap_token = 2; - KeyType key_type = 3; } message AuthChallenge { - int32 nonce = 1; + uint64 timestamp_nanos = 1; + bytes random = 2; } message AuthChallengeSolution { diff --git a/server/Cargo.lock b/server/Cargo.lock index b728c28..836a999 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -95,7 +95,7 @@ dependencies = [ "either", "k256", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "secp256k1", "serde", "serde_json", @@ -520,7 +520,7 @@ dependencies = [ "alloy-signer", "async-trait", "k256", - "rand 0.8.5", + "rand 0.8.6", "thiserror 2.0.18", ] @@ -687,6 +687,7 @@ dependencies = [ "arbiter-crypto", "arbiter-proto", "async-trait", + "chrono", "http", "rand 0.10.1", "rustls-webpki", @@ -701,12 +702,13 @@ name = "arbiter-crypto" version = "0.1.0" dependencies = [ "alloy", - "base64", "chrono", "hmac 0.13.0", "memsafe", "ml-dsa", "rand 0.10.1", + "thiserror 2.0.18", + "x-wing", ] [[package]] @@ -771,6 +773,7 @@ dependencies = [ "insta", "k256", "kameo", + "kameo_actors", "ml-dsa", "mutants", "pem", @@ -796,7 +799,7 @@ dependencies = [ "tonic", "tracing", "tracing-subscriber", - "x25519-dalek", + "x25519-dalek 2.0.1", "zeroize", ] @@ -986,7 +989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -996,7 +999,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -1006,7 +1009,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -1112,9 +1115,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -1123,9 +1126,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", @@ -1536,11 +1539,12 @@ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", + "konst", ] [[package]] @@ -1663,6 +1667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ "hybrid-array", + "rand_core 0.10.1", ] [[package]] @@ -2258,7 +2263,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand 0.8.5", + "rand 0.8.6", "rustc-hex", "static_assertions", ] @@ -2657,6 +2662,7 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" dependencies = [ + "ctutils", "typenum", "zeroize", ] @@ -3076,8 +3082,7 @@ dependencies = [ [[package]] name = "kameo" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1dfd134d7a2c6ec05ee696dcbf3f7a034bdb97ecc623e981014652dcd124d77" +source = "git+https://github.com/hdbg/kameo.git?rev=805b417#805b41783fe90b54827ecad142b422c7a9b69b9a" dependencies = [ "downcast-rs", "dyn-clone", @@ -3088,12 +3093,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "kameo_actors" +version = "0.5.0" +source = "git+https://github.com/hdbg/kameo.git?rev=805b417#805b41783fe90b54827ecad142b422c7a9b69b9a" +dependencies = [ + "futures", + "glob", + "kameo", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "kameo_macros" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c9002c9ecd16e1636f566c0bf62db48e70d86ed0d0a91b398955e883217a23" +source = "git+https://github.com/hdbg/kameo.git?rev=805b417#805b41783fe90b54827ecad142b422c7a9b69b9a" dependencies = [ + "darling 0.23.0", "heck", "proc-macro2", "quote", @@ -3129,6 +3146,31 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.1", + "rand_core 0.10.1", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "lazy_static" version = "1.5.0" @@ -3349,12 +3391,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ml-kem" +version = "0.3.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04437cb1a66c0b78740927b76cc61f218344b9f6ef3dd430e283274a718ef0e9" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "rand_core 0.10.1", + "sha3 0.11.0", + "zeroize", +] + [[package]] name = "module-lattice" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "164eb3faeaecbd14b0b2a917c1b4d0c035097a9c559b0bed85c2cdd032bc8faa" dependencies = [ + "ctutils", "hybrid-array", "num-traits", "zeroize", @@ -4050,9 +4107,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -4365,7 +4422,7 @@ dependencies = [ "parity-scale-codec", "primitive-types", "proptest", - "rand 0.8.5", + "rand 0.8.6", "rand 0.9.4", "rlp", "ruint-macro", @@ -4611,7 +4668,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ "bitcoin_hashes", - "rand 0.8.5", + "rand 0.8.6", "secp256k1-sys", "serde", ] @@ -5284,9 +5341,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.51.1" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -5700,9 +5757,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "js-sys", "wasm-bindgen", @@ -5762,11 +5819,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -5775,7 +5832,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -5903,9 +5960,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -6249,6 +6306,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -6343,6 +6406,20 @@ dependencies = [ "tap", ] +[[package]] +name = "x-wing" +version = "0.1.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d0d5f4d1f26b9b9e7477af1d3bef960e1d1fb64edab7912fde472a8a8432e" +dependencies = [ + "kem", + "ml-kem", + "rand_core 0.10.1", + "sha3 0.11.0", + "x25519-dalek 3.0.0-pre.6", + "zeroize", +] + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -6355,6 +6432,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x25519-dalek" +version = "3.0.0-pre.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d5d6ff67acd3945b933e592bfa7143db4fcbb2f871754b6b9fbd7847fc5aea" +dependencies = [ + "curve25519-dalek 5.0.0-pre.6", + "rand_core 0.10.1", + "zeroize", +] + [[package]] name = "x509-parser" version = "0.18.1" diff --git a/server/Cargo.toml b/server/Cargo.toml index 3e61bea..0438e2a 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -16,7 +16,7 @@ tonic = { version = "0.14.5", features = [ "zstd", ] } tracing = "0.1.44" -tokio = { version = "1.51.1", features = ["full"] } +tokio = { version = "1.52.1", features = ["full"] } ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] } chrono = { version = "0.4.44", features = ["serde"] } rand = "0.10.1" @@ -26,7 +26,6 @@ thiserror = "2.0.18" async-trait = "0.1.89" futures = "0.3.32" tokio-stream = { version = "0.1.18", features = ["full"] } -kameo = "0.20.0" prost-types = { version = "0.14.3", features = ["chrono"] } x25519-dalek = { version = "2.0.1", features = ["getrandom"] } rstest = "0.26.1" @@ -47,4 +46,6 @@ miette = { version = "7.6.0", features = ["fancy", "serde"] } mutants = "0.0.4" ml-dsa = { version = "0.1.0-rc.8", features = ["zeroize"] } base64 = "0.22.1" +kameo = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"} +kameo_actors = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"} hmac = "0.13.0" diff --git a/server/crates/arbiter-client/Cargo.toml b/server/crates/arbiter-client/Cargo.toml index ea0b621..828501e 100644 --- a/server/crates/arbiter-client/Cargo.toml +++ b/server/crates/arbiter-client/Cargo.toml @@ -24,3 +24,4 @@ http = "1.4.0" rustls-webpki = { version = "0.103.12", features = ["aws-lc-rs"] } async-trait.workspace = true rand.workspace = true +chrono.workspace = true diff --git a/server/crates/arbiter-client/src/auth.rs b/server/crates/arbiter-client/src/auth.rs index e6068e5..150f8cf 100644 --- a/server/crates/arbiter-client/src/auth.rs +++ b/server/crates/arbiter-client/src/auth.rs @@ -1,4 +1,8 @@ -use arbiter_crypto::authn::{CLIENT_CONTEXT, SigningKey, format_challenge}; +use crate::{ + storage::StorageError, + transport::{ClientTransport, next_request_id}, +}; +use arbiter_crypto::authn::{self, CLIENT_CONTEXT, SigningKey}; use arbiter_proto::{ ClientMetadata, proto::{ @@ -16,13 +20,12 @@ use arbiter_proto::{ }, }; -use crate::{ - storage::StorageError, - transport::{ClientTransport, next_request_id}, -}; +use chrono::DateTime; #[derive(Debug, thiserror::Error)] pub enum AuthError { + #[error("Server sent invalid auth challenge")] + InvalidChallenge, #[error("Auth challenge was not returned by server")] MissingAuthChallenge, @@ -98,7 +101,15 @@ async fn send_auth_challenge_solution( key: &SigningKey, challenge: AuthChallenge, ) -> std::result::Result<(), AuthError> { - let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey); + let timestamp = DateTime::from_timestamp_nanos(challenge.timestamp_nanos as i64); + let challenge = authn::AuthChallenge { + nonce: *challenge + .random + .as_array() + .ok_or(AuthError::InvalidChallenge)?, + timestamp, + }; + let challenge_payload: Vec = challenge.format(); let signature = key .sign_message(&challenge_payload, CLIENT_CONTEXT) .map_err(|_| AuthError::UnexpectedAuthResponse)? diff --git a/server/crates/arbiter-client/src/bin/test_connect.rs b/server/crates/arbiter-client/src/bin/test_connect.rs index 311d333..fad1f6b 100644 --- a/server/crates/arbiter-client/src/bin/test_connect.rs +++ b/server/crates/arbiter-client/src/bin/test_connect.rs @@ -1,8 +1,8 @@ -use std::io::{self, Write}; - use arbiter_client::ArbiterClient; use arbiter_proto::{ClientMetadata, url::ArbiterUrl}; +use std::io::{self, Write}; + #[tokio::main] async fn main() { println!("Testing connection to Arbiter server..."); diff --git a/server/crates/arbiter-client/src/client.rs b/server/crates/arbiter-client/src/client.rs index b540647..dd6be98 100644 --- a/server/crates/arbiter-client/src/client.rs +++ b/server/crates/arbiter-client/src/client.rs @@ -1,21 +1,20 @@ -use arbiter_crypto::authn::SigningKey; -use arbiter_proto::{ - ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl, -}; -use std::sync::Arc; -use tokio::sync::{Mutex, mpsc}; -use tokio_stream::wrappers::ReceiverStream; -use tonic::transport::ClientTlsConfig; - +#[cfg(feature = "evm")] +use crate::wallets::evm::ArbiterEvmWallet; use crate::{ StorageError, auth::{AuthError, authenticate}, storage::{FileSigningKeyStorage, SigningKeyStorage}, transport::{BUFFER_LENGTH, ClientTransport}, }; +use arbiter_crypto::authn::SigningKey; +use arbiter_proto::{ + ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl, +}; -#[cfg(feature = "evm")] -use crate::wallets::evm::ArbiterEvmWallet; +use std::sync::Arc; +use tokio::sync::{Mutex, mpsc}; +use tokio_stream::wrappers::ReceiverStream; +use tonic::transport::ClientTlsConfig; #[derive(Debug, thiserror::Error)] pub enum Error { diff --git a/server/crates/arbiter-client/src/storage.rs b/server/crates/arbiter-client/src/storage.rs index 55d4a46..ca7450a 100644 --- a/server/crates/arbiter-client/src/storage.rs +++ b/server/crates/arbiter-client/src/storage.rs @@ -1,5 +1,6 @@ use arbiter_crypto::authn::SigningKey; use arbiter_proto::home_path; + use std::path::{Path, PathBuf}; #[derive(Debug, thiserror::Error)] diff --git a/server/crates/arbiter-client/src/transport.rs b/server/crates/arbiter-client/src/transport.rs index 7332e89..26e3ba3 100644 --- a/server/crates/arbiter-client/src/transport.rs +++ b/server/crates/arbiter-client/src/transport.rs @@ -1,4 +1,5 @@ use arbiter_proto::proto::client::{ClientRequest, ClientResponse}; + use std::sync::atomic::{AtomicI32, Ordering}; use tokio::sync::mpsc; diff --git a/server/crates/arbiter-client/src/wallets/evm.rs b/server/crates/arbiter-client/src/wallets/evm.rs index 5c975c9..f26e969 100644 --- a/server/crates/arbiter-client/src/wallets/evm.rs +++ b/server/crates/arbiter-client/src/wallets/evm.rs @@ -1,13 +1,4 @@ -use alloy::{ - consensus::SignableTransaction, - network::TxSigner, - primitives::{Address, B256, ChainId, Signature}, - signers::{Error, Result, Signer}, -}; -use async_trait::async_trait; -use std::sync::Arc; -use tokio::sync::Mutex; - +use crate::transport::{ClientTransport, next_request_id}; use arbiter_proto::proto::{ client::{ ClientRequest, @@ -25,7 +16,15 @@ use arbiter_proto::proto::{ shared::evm::TransactionEvalError, }; -use crate::transport::{ClientTransport, next_request_id}; +use alloy::{ + consensus::SignableTransaction, + network::TxSigner, + primitives::{Address, B256, ChainId, Signature}, + signers::{Error, Result, Signer}, +}; +use async_trait::async_trait; +use std::sync::Arc; +use tokio::sync::Mutex; /// A typed error payload returned by [`ArbiterEvmWallet`] transaction signing. /// diff --git a/server/crates/arbiter-crypto/Cargo.toml b/server/crates/arbiter-crypto/Cargo.toml index be3b873..b544b33 100644 --- a/server/crates/arbiter-crypto/Cargo.toml +++ b/server/crates/arbiter-crypto/Cargo.toml @@ -6,16 +6,17 @@ edition = "2024" [dependencies] ml-dsa = {workspace = true, optional = true } rand = {workspace = true, optional = true} -base64 = {workspace = true, optional = true } memsafe = {version = "0.4.0", optional = true} hmac.workspace = true alloy.workspace = true +x-wing = { version = "0.1.0-rc.0", features = ["zeroize"] } chrono.workspace = true +thiserror.workspace = true [lints] workspace = true [features] default = ["authn", "safecell"] -authn = ["dep:ml-dsa", "dep:rand", "dep:base64"] +authn = ["dep:ml-dsa", "dep:rand"] safecell = ["dep:memsafe"] diff --git a/server/crates/arbiter-crypto/src/authn/v1.rs b/server/crates/arbiter-crypto/src/authn/v1.rs index c33de49..94aa9d2 100644 --- a/server/crates/arbiter-crypto/src/authn/v1.rs +++ b/server/crates/arbiter-crypto/src/authn/v1.rs @@ -1,16 +1,65 @@ -use base64::{Engine as _, prelude::BASE64_STANDARD}; +use chrono::{DateTime, Utc}; use hmac::digest::Digest; use ml_dsa::{ EncodedVerifyingKey, Error, KeyGen, MlDsa87, Seed, Signature as MlDsaSignature, SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey, signature::Keypair as _, }; +use rand::RngExt; pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client"; pub static USERAGENT_CONTEXT: &[u8] = b"arbiter_user_agent"; -pub fn format_challenge(nonce: i32, pubkey: &[u8]) -> Vec { - let concat_form = format!("{}:{}", nonce, BASE64_STANDARD.encode(pubkey)); - concat_form.into_bytes() +const NONCE_SIZE: usize = 32; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +#[error("invalid length: expected {expected} bytes, got {actual} bytes")] +pub struct InvalidLength { + pub expected: usize, + pub actual: usize, +} + +#[derive(Debug, Clone)] +pub struct AuthChallenge { + pub nonce: [u8; NONCE_SIZE], + pub timestamp: DateTime, +} + +impl AuthChallenge { + pub fn generate(rng: &mut impl rand::CryptoRng) -> Self { + let timestamp = Utc::now(); + let nonce = { + let mut array = [0; NONCE_SIZE]; + rng.fill(&mut array); + array + }; + + Self { nonce, timestamp } + } + + pub fn format(&self) -> Vec { + { + let mut buffer = Vec::from(self.nonce); + + let stamp = self + .timestamp + .timestamp_nanos_opt() + .expect("We would be long dead by the time this triggers :)"); + buffer.extend_from_slice(stamp.to_be_bytes().as_slice()); + + buffer + } + } + + pub fn from_parts(nonce: &[u8], timestamp: i64) -> Result { + let random_nonce = nonce.as_array().ok_or(InvalidLength { + expected: NONCE_SIZE, + actual: nonce.len(), + })?; + Ok(AuthChallenge { + nonce: *random_nonce, + timestamp: DateTime::from_timestamp_nanos(timestamp), + }) + } } pub type KeyParams = MlDsa87; @@ -35,12 +84,10 @@ impl PublicKey { self.0.encode().0.to_vec() } - pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool { - self.0.verify_with_context( - &format_challenge(nonce, &self.to_bytes()), - context, - &signature.0, - ) + pub fn verify(&self, challenge: &AuthChallenge, context: &[u8], signature: &Signature) -> bool { + let challenge = challenge.format(); + self.0 + .verify_with_context(&challenge, context, &signature.0) } } @@ -74,11 +121,14 @@ impl SigningKey { .map(Into::into) } - pub fn sign_challenge(&self, nonce: i32, context: &[u8]) -> Result { - self.sign_message( - &format_challenge(nonce, &self.public_key().to_bytes()), - context, - ) + pub fn sign_challenge( + &self, + challenge: &AuthChallenge, + context: &[u8], + ) -> Result { + let challenge = challenge.format(); + + self.sign_message(&challenge, context) } } @@ -139,6 +189,8 @@ impl TryFrom<&'_ [u8]> for Signature { mod tests { use ml_dsa::{KeyGen, MlDsa87, signature::Keypair as _}; + use crate::authn::AuthChallenge; + use super::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, USERAGENT_CONTEXT}; #[test] @@ -168,13 +220,13 @@ mod tests { fn challenge_verification_uses_context_and_canonical_key_bytes() { let key = SigningKey::generate(); let public_key = key.public_key(); - let nonce = 17; + let challenge = AuthChallenge::generate(&mut rand::rng()); let signature = key - .sign_challenge(nonce, CLIENT_CONTEXT) + .sign_challenge(&challenge, CLIENT_CONTEXT) .expect("signature should be created"); - assert!(public_key.verify(nonce, CLIENT_CONTEXT, &signature)); - assert!(!public_key.verify(nonce, USERAGENT_CONTEXT, &signature)); + assert!(public_key.verify(&challenge, CLIENT_CONTEXT, &signature)); + assert!(!public_key.verify(&challenge, USERAGENT_CONTEXT, &signature)); } #[test] @@ -184,10 +236,16 @@ mod tests { assert_eq!(restored.public_key(), original.public_key()); + let challenge = AuthChallenge::generate(&mut rand::rng()); + let signature = restored - .sign_challenge(9, CLIENT_CONTEXT) + .sign_challenge(&challenge, CLIENT_CONTEXT) .expect("signature should be created"); - assert!(restored.public_key().verify(9, CLIENT_CONTEXT, &signature)); + assert!( + restored + .public_key() + .verify(&challenge, CLIENT_CONTEXT, &signature) + ); } } diff --git a/server/crates/arbiter-crypto/src/hashing.rs b/server/crates/arbiter-crypto/src/hashing.rs index 48b54ee..5f3de8d 100644 --- a/server/crates/arbiter-crypto/src/hashing.rs +++ b/server/crates/arbiter-crypto/src/hashing.rs @@ -1,6 +1,7 @@ -pub use hmac::digest::Digest; use std::collections::HashSet; +pub use hmac::digest::Digest; + /// Deterministically hash a value by feeding its fields into the hasher in a consistent order. #[diagnostic::on_unimplemented( note = "for local types consider adding `#[derive(arbiter_macros::Hashable)]` to your `{Self}` type", diff --git a/server/crates/arbiter-crypto/src/lib.rs b/server/crates/arbiter-crypto/src/lib.rs index b9c7503..8e1c9ce 100644 --- a/server/crates/arbiter-crypto/src/lib.rs +++ b/server/crates/arbiter-crypto/src/lib.rs @@ -3,3 +3,5 @@ pub mod authn; pub mod hashing; #[cfg(feature = "safecell")] pub mod safecell; + +pub use x_wing; diff --git a/server/crates/arbiter-crypto/src/safecell.rs b/server/crates/arbiter-crypto/src/safecell.rs index 80dc57e..391730d 100644 --- a/server/crates/arbiter-crypto/src/safecell.rs +++ b/server/crates/arbiter-crypto/src/safecell.rs @@ -1,7 +1,9 @@ -use std::ops::{Deref, DerefMut}; -use std::{any::type_name, fmt}; - use memsafe::MemSafe; +use std::{ + any::type_name, + fmt, + ops::{Deref, DerefMut}, +}; pub trait SafeCellHandle { type CellRead<'a>: Deref diff --git a/server/crates/arbiter-macros/src/hashable.rs b/server/crates/arbiter-macros/src/hashable.rs index 7473497..a175987 100644 --- a/server/crates/arbiter-macros/src/hashable.rs +++ b/server/crates/arbiter-macros/src/hashable.rs @@ -1,10 +1,8 @@ +use crate::utils::{HASHABLE_TRAIT_PATH, HMAC_DIGEST_PATH}; + use proc_macro2::{Span, TokenStream, TokenTree}; use quote::quote; -use syn::parse_quote; -use syn::spanned::Spanned; -use syn::{DataStruct, DeriveInput, Fields, Generics, Index}; - -use crate::utils::{HASHABLE_TRAIT_PATH, HMAC_DIGEST_PATH}; +use syn::{DataStruct, DeriveInput, Fields, Generics, Index, parse_quote, spanned::Spanned}; pub(crate) fn derive(input: &DeriveInput) -> TokenStream { match &input.data { diff --git a/server/crates/arbiter-macros/src/utils.rs b/server/crates/arbiter-macros/src/utils.rs index 460aa94..d4f792b 100644 --- a/server/crates/arbiter-macros/src/utils.rs +++ b/server/crates/arbiter-macros/src/utils.rs @@ -1,7 +1,7 @@ -pub struct ToPath(pub &'static str); +pub(crate) struct ToPath(pub(crate) &'static str); impl ToPath { - pub fn to_path(&self) -> syn::Path { + pub(crate) fn to_path(&self) -> syn::Path { syn::parse_str(self.0).expect("Invalid path") } } @@ -15,5 +15,5 @@ macro_rules! ensure_path { }}; } -pub const HASHABLE_TRAIT_PATH: ToPath = ensure_path!(::arbiter_crypto::hashing::Hashable); -pub const HMAC_DIGEST_PATH: ToPath = ensure_path!(::arbiter_crypto::hashing::Digest); +pub(crate) const HASHABLE_TRAIT_PATH: ToPath = ensure_path!(::arbiter_crypto::hashing::Hashable); +pub(crate) const HMAC_DIGEST_PATH: ToPath = ensure_path!(::arbiter_crypto::hashing::Digest); diff --git a/server/crates/arbiter-proto/src/transport.rs b/server/crates/arbiter-proto/src/transport.rs index 5f8b5b4..afa641b 100644 --- a/server/crates/arbiter-proto/src/transport.rs +++ b/server/crates/arbiter-proto/src/transport.rs @@ -54,10 +54,9 @@ //! as a closed outbound channel. //! - [`Bi::recv`] returns `None` when the underlying transport closes. //! - Message translation is intentionally out of scope for this module. - -use std::marker::PhantomData; - use async_trait::async_trait; +use kameo::{error::Infallible, prelude::*}; +use std::marker::PhantomData; /// Errors returned by transport adapters implementing [`Bi`]. #[derive(thiserror::Error, Debug)] @@ -106,6 +105,36 @@ pub trait Receiver: Send + Sync { /// any built-in correlation mechanism between inbound and outbound items. pub trait Bi: Sender + Receiver + Send + Sync {} +#[async_trait] +impl Sender for &mut T +where + T: Sender + ?Sized, + Outbound: Send + 'static, +{ + async fn send(&mut self, item: Outbound) -> Result<(), Error> { + (**self).send(item).await + } +} + +#[async_trait] +impl Receiver for &mut T +where + T: Receiver + ?Sized, + Inbound: Send + 'static, +{ + async fn recv(&mut self) -> Option { + (**self).recv().await + } +} + +impl Bi for &mut T +where + T: Bi + ?Sized, + Inbound: Send + 'static, + Outbound: Send + 'static, +{ +} + pub trait SplittableBi: Bi { type Sender: Sender; type Receiver: Receiver; @@ -161,3 +190,29 @@ where } pub mod grpc; + +#[derive(thiserror::Error, Debug)] +pub enum ForwardError { + #[error("Transport error: {0}")] + Transport(#[from] Error), + #[error("Actor delivery error: {0}")] + Actor(SendError), +} + +pub async fn forward_to_actor( + transport: &mut Transport, + actor: &ActorRef, +) -> Result<(), ForwardError> +where + Transport: Bi::Ok>, + Handler: Actor + Message, + Inbound: Send + 'static, + Outbound: Send + 'static + Reply, // `Infallible` to enforce contract that `Outbound` carries handler-level error +{ + while let Some(request) = transport.recv().await { + let resp = actor.ask(request).await.map_err(ForwardError::Actor)?; + transport.send(resp).await? + } + + Err(Error::ChannelClosed.into()) +} diff --git a/server/crates/arbiter-proto/src/transport/grpc.rs b/server/crates/arbiter-proto/src/transport/grpc.rs index e0959e0..17b3c27 100644 --- a/server/crates/arbiter-proto/src/transport/grpc.rs +++ b/server/crates/arbiter-proto/src/transport/grpc.rs @@ -1,10 +1,10 @@ +use super::{Bi, Receiver, Sender}; + use async_trait::async_trait; use futures::StreamExt; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; -use super::{Bi, Receiver, Sender}; - pub struct GrpcSender { tx: mpsc::Sender>, } diff --git a/server/crates/arbiter-proto/src/url.rs b/server/crates/arbiter-proto/src/url.rs index 321df0b..7e1ee52 100644 --- a/server/crates/arbiter-proto/src/url.rs +++ b/server/crates/arbiter-proto/src/url.rs @@ -1,7 +1,6 @@ -use std::fmt::Display; - use base64::{Engine as _, prelude::BASE64_URL_SAFE}; use rustls_pki_types::CertificateDer; +use std::fmt::Display; const ARBITER_URL_SCHEME: &str = "arbiter"; const CERT_QUERY_KEY: &str = "cert"; diff --git a/server/crates/arbiter-server/Cargo.toml b/server/crates/arbiter-server/Cargo.toml index 0bf40e2..0626f2e 100644 --- a/server/crates/arbiter-server/Cargo.toml +++ b/server/crates/arbiter-server/Cargo.toml @@ -59,6 +59,7 @@ ml-dsa.workspace = true ed25519-dalek.workspace = true x25519-dalek.workspace = true k256.workspace = true +kameo_actors.workspace = true [dev-dependencies] insta = "1.47.2" diff --git a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql index bb33278..ac40bce 100644 --- a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql +++ b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql @@ -45,13 +45,11 @@ insert into arbiter_settings (id) values (1) on conflict do nothing; create table if not exists useragent_client ( id integer not null primary key, - nonce integer not null default(1), -- used for auth challenge public_key blob not null, - key_type integer not null default(1), created_at integer not null default(unixepoch ('now')), updated_at integer not null default(unixepoch ('now')) ) STRICT; -create unique index if not exists uniq_useragent_client_public_key on useragent_client (public_key, key_type); +create unique index if not exists uniq_useragent_client_public_key on useragent_client (public_key); create table if not exists client_metadata ( id integer not null primary key, @@ -73,7 +71,6 @@ create unique index if not exists uniq_metadata_binding_client on client_metadat create table if not exists program_client ( id integer not null primary key, - nonce integer not null default(1), -- used for auth challenge public_key blob not null, metadata_id integer not null references client_metadata (id) on delete cascade, created_at integer not null default(unixepoch ('now')), diff --git a/server/crates/arbiter-server/src/actors/bootstrap.rs b/server/crates/arbiter-server/src/actors/bootstrap.rs index cef154a..01c246d 100644 --- a/server/crates/arbiter-server/src/actors/bootstrap.rs +++ b/server/crates/arbiter-server/src/actors/bootstrap.rs @@ -1,13 +1,13 @@ +use crate::db::{self, DatabasePool, schema}; use arbiter_proto::{BOOTSTRAP_PATH, home_path}; + use diesel::QueryDsl; use diesel_async::RunQueryDsl; use kameo::{Actor, messages}; - use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng}; use subtle::ConstantTimeEq as _; use thiserror::Error; -use crate::db::{self, DatabasePool, schema}; const TOKEN_LENGTH: usize = 64; pub async fn generate_token() -> Result { diff --git a/server/crates/arbiter-server/src/actors/evm/mod.rs b/server/crates/arbiter-server/src/actors/evm/mod.rs index c31cdd0..7623afd 100644 --- a/server/crates/arbiter-server/src/actors/evm/mod.rs +++ b/server/crates/arbiter-server/src/actors/evm/mod.rs @@ -1,13 +1,5 @@ -use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature}; -use diesel::{ - ExpressionMethods, OptionalExtension as _, QueryDsl, SelectableHelper as _, dsl::insert_into, -}; -use diesel_async::RunQueryDsl; -use kameo::{Actor, actor::ActorRef, messages}; -use rand::{SeedableRng, rng, rngs::StdRng}; - use crate::{ - actors::keyholder::{CreateNew, Decrypt, KeyHolder}, + actors::vault::{CreateNew, Decrypt, Vault}, crypto::integrity, db::{ DatabaseError, DatabasePool, @@ -24,6 +16,14 @@ use crate::{ }; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; +use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature}; +use diesel::{ + ExpressionMethods, OptionalExtension as _, QueryDsl, SelectableHelper as _, dsl::insert_into, +}; +use diesel_async::RunQueryDsl; +use kameo::{Actor, actor::ActorRef, messages}; +use rand::{SeedableRng, rng, rngs::StdRng}; + pub use crate::evm::safe_signer; #[derive(Debug, thiserror::Error)] @@ -34,11 +34,11 @@ pub enum SignTransactionError { #[error("Database error: {0}")] Database(#[from] DatabaseError), - #[error("Keyholder error: {0}")] - Keyholder(#[from] crate::actors::keyholder::Error), + #[error("Vault error: {0}")] + Vault(#[from] crate::actors::vault::Error), - #[error("Keyholder mailbox error")] - KeyholderSend, + #[error("Vault mailbox error")] + VaultSend, #[error("Signing error: {0}")] Signing(#[from] alloy::signers::Error), @@ -49,11 +49,11 @@ pub enum SignTransactionError { #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Keyholder error: {0}")] - Keyholder(#[from] crate::actors::keyholder::Error), + #[error("Vault error: {0}")] + Vault(#[from] crate::actors::vault::Error), - #[error("Keyholder mailbox error")] - KeyholderSend, + #[error("Vault mailbox error")] + VaultSend, #[error("Database error: {0}")] Database(#[from] DatabaseError), @@ -64,20 +64,20 @@ pub enum Error { #[derive(Actor)] pub struct EvmActor { - pub keyholder: ActorRef, + pub vault: ActorRef, pub db: DatabasePool, pub rng: StdRng, pub engine: evm::Engine, } impl EvmActor { - pub fn new(keyholder: ActorRef, db: DatabasePool) -> Self { + pub fn new(vault: ActorRef, db: DatabasePool) -> Self { // is it safe to seed rng from system once? // todo: audit let rng = StdRng::from_rng(&mut rng()); - let engine = evm::Engine::new(db.clone(), keyholder.clone()); + let engine = evm::Engine::new(db.clone(), vault.clone()); Self { - keyholder, + vault, db, rng, engine, @@ -94,10 +94,10 @@ impl EvmActor { let plaintext = key_cell.read_inline(|reader| SafeCell::new(reader.to_vec())); let aead_id: i32 = self - .keyholder + .vault .ask(CreateNew { plaintext }) .await - .map_err(|_| Error::KeyholderSend)?; + .map_err(|_| Error::VaultSend)?; let mut conn = self.db.get().await.map_err(DatabaseError::from)?; let wallet_id = insert_into(schema::evm_wallet::table) @@ -160,7 +160,7 @@ impl EvmActor { #[message] pub async fn useragent_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> { // let mut conn = self.db.get().await.map_err(DatabaseError::from)?; - // let keyholder = self.keyholder.clone(); + // let vault = self.vault.clone(); // diesel_async::AsyncConnection::transaction(&mut conn, |conn| { // Box::pin(async move { @@ -254,12 +254,12 @@ impl EvmActor { drop(conn); let raw_key: SafeCell> = self - .keyholder + .vault .ask(Decrypt { aead_id: wallet.aead_encrypted_id, }) .await - .map_err(|_| SignTransactionError::KeyholderSend)?; + .map_err(|_| SignTransactionError::VaultSend)?; let signer = safe_signer::SafeSigner::from_cell(raw_key)?; diff --git a/server/crates/arbiter-server/src/actors/flow_coordinator/client_connect_approval.rs b/server/crates/arbiter-server/src/actors/flow_coordinator/client_connect_approval.rs index c5b20c3..9249ce2 100644 --- a/server/crates/arbiter-server/src/actors/flow_coordinator/client_connect_approval.rs +++ b/server/crates/arbiter-server/src/actors/flow_coordinator/client_connect_approval.rs @@ -1,16 +1,17 @@ -use std::ops::ControlFlow; +use crate::{ + actors::flow_coordinator::ApprovalError, + peers::{ + client::ClientProfile, + user_agent::{UserAgentSession, session::BeginNewClientApproval}, + }, +}; use kameo::{ Actor, messages, prelude::{ActorId, ActorRef, ActorStopReason, Context, WeakActorRef}, reply::ReplySender, }; - -use crate::actors::{ - client::ClientProfile, - flow_coordinator::ApprovalError, - user_agent::{UserAgentSession, session::BeginNewClientApproval}, -}; +use std::ops::ControlFlow; pub struct Args { pub client: ClientProfile, diff --git a/server/crates/arbiter-server/src/actors/flow_coordinator/mod.rs b/server/crates/arbiter-server/src/actors/flow_coordinator/mod.rs index 2e0aa9a..64a1999 100644 --- a/server/crates/arbiter-server/src/actors/flow_coordinator/mod.rs +++ b/server/crates/arbiter-server/src/actors/flow_coordinator/mod.rs @@ -1,4 +1,10 @@ -use std::{collections::HashMap, ops::ControlFlow}; +use crate::{ + actors::{ + flow_coordinator::client_connect_approval::ClientApprovalController, + useragent_registry::{GetConnected, UserAgentRegistry}, + }, + peers::client::{ClientProfile, session::ClientSession}, +}; use kameo::{ Actor, @@ -7,20 +13,23 @@ use kameo::{ prelude::{ActorStopReason, Context, WeakActorRef}, reply::DelegatedReply, }; +use std::{collections::HashMap, ops::ControlFlow}; use tracing::info; -use crate::actors::{ - client::{ClientProfile, session::ClientSession}, - flow_coordinator::client_connect_approval::ClientApprovalController, - user_agent::session::UserAgentSession, -}; - pub mod client_connect_approval; -#[derive(Default)] pub struct FlowCoordinator { - pub user_agents: HashMap>, pub clients: HashMap>, + useragent_registry: ActorRef, +} + +impl FlowCoordinator { + pub fn new(useragent_registry: ActorRef) -> Self { + Self { + clients: HashMap::default(), + useragent_registry, + } + } } impl Actor for FlowCoordinator { @@ -38,13 +47,7 @@ impl Actor for FlowCoordinator { id: ActorId, _: ActorStopReason, ) -> Result, Self::Error> { - if self.user_agents.remove(&id).is_some() { - info!( - ?id, - actor = "FlowCoordinator", - event = "useragent.disconnected" - ); - } else if self.clients.remove(&id).is_some() { + if self.clients.remove(&id).is_some() { info!( ?id, actor = "FlowCoordinator", @@ -69,17 +72,6 @@ pub enum ApprovalError { #[messages] impl FlowCoordinator { - #[message(ctx)] - pub async fn register_user_agent( - &mut self, - actor: ActorRef, - ctx: &mut Context, - ) { - info!(id = %actor.id(), actor = "FlowCoordinator", event = "useragent.connected"); - ctx.actor_ref().link(&actor).await; - self.user_agents.insert(actor.id(), actor); - } - #[message(ctx)] pub async fn register_client( &mut self, @@ -101,7 +93,14 @@ impl FlowCoordinator { unreachable!("Expected `request_client_approval` to have callback channel"); }; - let refs: Vec<_> = self.user_agents.values().cloned().collect(); + let refs = match self.useragent_registry.ask(GetConnected).await { + Ok(refs) => refs, + Err(_) => { + reply_sender.send(Err(ApprovalError::NoUserAgentsConnected)); + return reply; + } + }; + if refs.is_empty() { reply_sender.send(Err(ApprovalError::NoUserAgentsConnected)); return reply; diff --git a/server/crates/arbiter-server/src/actors/mod.rs b/server/crates/arbiter-server/src/actors/mod.rs index 8ff1fce..e28f12c 100644 --- a/server/crates/arbiter-server/src/actors/mod.rs +++ b/server/crates/arbiter-server/src/actors/mod.rs @@ -1,47 +1,59 @@ -use kameo::actor::{ActorRef, Spawn}; -use thiserror::Error; - use crate::{ actors::{ bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator, - keyholder::KeyHolder, + useragent_registry::UserAgentRegistry, vault::Vault, }, db, }; +use kameo::actor::{ActorRef, Spawn}; +use kameo_actors::{DeliveryStrategy, message_bus::MessageBus}; +use thiserror::Error; + pub mod bootstrap; -pub mod client; -mod evm; +pub mod evm; pub mod flow_coordinator; -pub mod keyholder; -pub mod user_agent; +pub mod useragent_registry; +pub mod vault; #[derive(Error, Debug)] pub enum SpawnError { #[error("Failed to spawn Bootstrapper actor")] Bootstrapper(#[from] bootstrap::Error), - #[error("Failed to spawn KeyHolder actor")] - KeyHolder(#[from] keyholder::Error), + #[error("Failed to spawn Vault actor")] + Vault(#[from] vault::Error), } /// Long-lived actors that are shared across all connections and handle global state and operations #[derive(Clone)] pub struct GlobalActors { - pub key_holder: ActorRef, + pub vault: ActorRef, pub bootstrapper: ActorRef, pub flow_coordinator: ActorRef, + pub useragent_registry: ActorRef, pub evm: ActorRef, + pub events: ActorRef, } impl GlobalActors { + pub fn spawn_message_bus() -> ActorRef { + MessageBus::spawn(MessageBus::new(DeliveryStrategy::Guaranteed)) + } + pub async fn spawn(db: db::DatabasePool) -> Result { - let key_holder = KeyHolder::spawn(KeyHolder::new(db.clone()).await?); + let message_bus = Self::spawn_message_bus(); + let key_holder = Vault::spawn(Vault::new(db.clone(), message_bus.clone()).await?); + let useragent_registry = UserAgentRegistry::spawn(UserAgentRegistry::default()); Ok(Self { bootstrapper: Bootstrapper::spawn(Bootstrapper::new(&db).await?), evm: EvmActor::spawn(EvmActor::new(key_holder.clone(), db)), - key_holder, - flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::default()), + vault: key_holder, + flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::new( + useragent_registry.clone(), + )), + useragent_registry, + events: message_bus, }) } } diff --git a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs deleted file mode 100644 index 60bcf6f..0000000 --- a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs +++ /dev/null @@ -1,318 +0,0 @@ -use arbiter_crypto::authn::{self, USERAGENT_CONTEXT}; -use arbiter_proto::transport::Bi; -use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update}; -use diesel_async::{AsyncConnection, RunQueryDsl}; -use kameo::actor::ActorRef; -use tracing::error; - -use super::Error; -use crate::{ - actors::{ - bootstrap::ConsumeToken, - keyholder::KeyHolder, - user_agent::{UserAgentConnection, UserAgentCredentials, auth::Outbound}, - }, - crypto::integrity, - db::{DatabasePool, schema::useragent_client}, -}; - -pub struct ChallengeRequest { - pub pubkey: authn::PublicKey, -} - -pub struct BootstrapAuthRequest { - pub pubkey: authn::PublicKey, - pub token: String, -} - -pub struct ChallengeContext { - pub challenge_nonce: i32, - pub key: authn::PublicKey, -} - -pub struct ChallengeSolution { - pub solution: Vec, -} - -smlang::statemachine!( - name: Auth, - custom_error: true, - transitions: { - *Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext), - Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(authn::PublicKey), - SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(authn::PublicKey), - } -); - -/// Returns the current nonce, ready to use for the challenge nonce. -async fn get_current_nonce_and_id( - db: &DatabasePool, - key: &authn::PublicKey, -) -> Result<(i32, i32), Error> { - let mut db_conn = db.get().await.map_err(|e| { - error!(error = ?e, "Database pool error"); - Error::internal("Database unavailable") - })?; - db_conn - .exclusive_transaction(|conn| { - Box::pin(async move { - useragent_client::table - .filter(useragent_client::public_key.eq(key.to_bytes())) - .select((useragent_client::id, useragent_client::nonce)) - .first::<(i32, i32)>(conn) - .await - }) - }) - .await - .optional() - .map_err(|e| { - error!(error = ?e, "Database error"); - Error::internal("Database operation failed") - })? - .ok_or_else(|| { - error!(?key, "Public key not found in database"); - Error::UnregisteredPublicKey - }) -} - -async fn verify_integrity( - db: &DatabasePool, - keyholder: &ActorRef, - pubkey: &authn::PublicKey, -) -> Result<(), Error> { - let mut db_conn = db.get().await.map_err(|e| { - error!(error = ?e, "Database pool error"); - Error::internal("Database unavailable") - })?; - - let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?; - - let _result = integrity::verify_entity( - &mut db_conn, - keyholder, - &UserAgentCredentials { - pubkey: pubkey.clone(), - nonce, - }, - id, - ) - .await - .map_err(|e| { - error!(?e, "Integrity verification failed"); - Error::internal("Integrity verification failed") - })?; - - Ok(()) -} - -async fn create_nonce( - db: &DatabasePool, - keyholder: &ActorRef, - pubkey: &authn::PublicKey, -) -> Result { - let mut db_conn = db.get().await.map_err(|e| { - error!(error = ?e, "Database pool error"); - Error::internal("Database unavailable") - })?; - let new_nonce = db_conn - .exclusive_transaction(|conn| { - Box::pin(async move { - let (id, new_nonce): (i32, i32) = update(useragent_client::table) - .filter(useragent_client::public_key.eq(pubkey.to_bytes())) - .set(useragent_client::nonce.eq(useragent_client::nonce + 1)) - .returning((useragent_client::id, useragent_client::nonce)) - .get_result(conn) - .await - .map_err(|e| { - error!(error = ?e, "Database error"); - Error::internal("Database operation failed") - })?; - - integrity::sign_entity( - conn, - keyholder, - &UserAgentCredentials { - pubkey: pubkey.clone(), - nonce: new_nonce, - }, - id, - ) - .await - .map_err(|e| { - error!(?e, "Integrity signature update failed"); - Error::internal("Database error") - })?; - - Result::<_, Error>::Ok(new_nonce) - }) - }) - .await?; - Ok(new_nonce) -} - -async fn register_key( - db: &DatabasePool, - keyholder: &ActorRef, - pubkey: &authn::PublicKey, -) -> Result<(), Error> { - let pubkey_bytes = pubkey.to_bytes(); - let mut conn = db.get().await.map_err(|e| { - error!(error = ?e, "Database pool error"); - Error::internal("Database unavailable") - })?; - - conn.transaction(|conn| { - Box::pin(async move { - const NONCE_START: i32 = 1; - - let id: i32 = diesel::insert_into(useragent_client::table) - .values(( - useragent_client::public_key.eq(pubkey_bytes), - useragent_client::nonce.eq(NONCE_START), - )) - .returning(useragent_client::id) - .get_result(conn) - .await - .map_err(|e| { - error!(error = ?e, "Database error"); - Error::internal("Database operation failed") - })?; - - let entity = UserAgentCredentials { - pubkey: pubkey.clone(), - nonce: NONCE_START, - }; - - integrity::sign_entity(conn, keyholder, &entity, id) - .await - .map_err(|e| { - error!(error = ?e, "Failed to sign integrity tag for new user-agent key"); - Error::internal("Failed to register public key") - })?; - - Result::<_, Error>::Ok(()) - }) - }) - .await?; - - Ok(()) -} - -pub struct AuthContext<'a, T> { - pub(super) conn: &'a mut UserAgentConnection, - pub(super) transport: T, -} - -impl<'a, T> AuthContext<'a, T> { - pub fn new(conn: &'a mut UserAgentConnection, transport: T) -> Self { - Self { conn, transport } - } -} - -impl AuthStateMachineContext for AuthContext<'_, T> -where - T: Bi> + Send, -{ - type Error = Error; - - async fn prepare_challenge( - &mut self, - ChallengeRequest { pubkey }: ChallengeRequest, - ) -> Result { - verify_integrity(&self.conn.db, &self.conn.actors.key_holder, &pubkey).await?; - - let nonce = create_nonce(&self.conn.db, &self.conn.actors.key_holder, &pubkey).await?; - - self.transport - .send(Ok(Outbound::AuthChallenge { nonce })) - .await - .map_err(|e| { - error!(?e, "Failed to send auth challenge"); - Error::Transport - })?; - - Ok(ChallengeContext { - challenge_nonce: nonce, - key: pubkey, - }) - } - - #[allow(missing_docs)] - #[allow(clippy::result_unit_err)] - async fn verify_bootstrap_token( - &mut self, - BootstrapAuthRequest { pubkey, token }: BootstrapAuthRequest, - ) -> Result { - let token_ok: bool = self - .conn - .actors - .bootstrapper - .ask(ConsumeToken { - token: token.clone(), - }) - .await - .map_err(|e| { - error!(?e, "Failed to consume bootstrap token"); - Error::internal("Failed to consume bootstrap token") - })?; - - if !token_ok { - error!("Invalid bootstrap token provided"); - return Err(Error::InvalidBootstrapToken); - } - - match token_ok { - true => { - register_key(&self.conn.db, &self.conn.actors.key_holder, &pubkey).await?; - self.transport - .send(Ok(Outbound::AuthSuccess)) - .await - .map_err(|_| Error::Transport)?; - Ok(pubkey) - } - false => { - error!("Invalid bootstrap token provided"); - self.transport - .send(Err(Error::InvalidBootstrapToken)) - .await - .map_err(|_| Error::Transport)?; - Err(Error::InvalidBootstrapToken) - } - } - } - - #[allow(missing_docs)] - #[allow(clippy::unused_unit)] - async fn verify_solution( - &mut self, - ChallengeContext { - challenge_nonce, - key, - }: &ChallengeContext, - ChallengeSolution { solution }: ChallengeSolution, - ) -> Result { - let signature = authn::Signature::try_from(solution.as_slice()).map_err(|_| { - error!("Failed to decode signature in challenge solution"); - Error::InvalidChallengeSolution - })?; - - let valid = key.verify(*challenge_nonce, USERAGENT_CONTEXT, &signature); - - match valid { - true => { - self.transport - .send(Ok(Outbound::AuthSuccess)) - .await - .map_err(|_| Error::Transport)?; - Ok(key.clone()) - } - false => { - self.transport - .send(Err(Error::InvalidChallengeSolution)) - .await - .map_err(|_| Error::Transport)?; - Err(Error::InvalidChallengeSolution) - } - } - } -} diff --git a/server/crates/arbiter-server/src/actors/user_agent/mod.rs b/server/crates/arbiter-server/src/actors/user_agent/mod.rs deleted file mode 100644 index 5e87d23..0000000 --- a/server/crates/arbiter-server/src/actors/user_agent/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::{ - actors::{GlobalActors, client::ClientProfile}, - crypto::integrity::Integrable, - db, -}; -use arbiter_crypto::authn; - -#[derive(Debug, arbiter_macros::Hashable)] -pub struct UserAgentCredentials { - pub pubkey: authn::PublicKey, - pub nonce: i32, -} - -impl Integrable for UserAgentCredentials { - const KIND: &'static str = "useragent_credentials"; -} - -// Messages, sent by user agent to connection client without having a request -#[derive(Debug)] -pub enum OutOfBand { - ClientConnectionRequest { profile: ClientProfile }, - ClientConnectionCancel { pubkey: authn::PublicKey }, -} - -pub struct UserAgentConnection { - pub(crate) db: db::DatabasePool, - pub(crate) actors: GlobalActors, -} - -impl UserAgentConnection { - pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self { - Self { db, actors } - } -} - -pub mod auth; -pub mod session; - -pub use auth::authenticate; -pub use session::UserAgentSession; diff --git a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs b/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs deleted file mode 100644 index 71f4067..0000000 --- a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs +++ /dev/null @@ -1,524 +0,0 @@ -use std::sync::Mutex; - -use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature}; -use arbiter_crypto::{ - authn, - safecell::{SafeCell, SafeCellHandle as _}, -}; -use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; -use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper}; -use diesel_async::{AsyncConnection, RunQueryDsl}; -use kameo::error::SendError; -use kameo::messages; -use kameo::prelude::Context; -use tracing::{error, info}; -use x25519_dalek::{EphemeralSecret, PublicKey}; - -use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer; -use crate::actors::keyholder::KeyHolderState; -use crate::actors::user_agent::session::Error; -use crate::actors::{ - evm::{ - ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError, - UseragentCreateGrant, UseragentListGrants, - }, - keyholder::{self, Bootstrap, TryUnseal}, - user_agent::session::{ - UserAgentSession, - state::{UnsealContext, UserAgentEvents, UserAgentStates}, - }, -}; -use crate::db::models::{ - EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata, -}; -use crate::evm::policies::{Grant, SpecificGrant}; - -impl UserAgentSession { - fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> { - let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else { - error!("Received encrypted key in invalid state"); - return Err(Error::internal("Invalid state for unseal encrypted key")); - }; - - let ephemeral_secret = { - #[allow( - clippy::unwrap_used, - reason = "Mutex poison is unrecoverable and should panic" - )] - let mut secret_lock = unseal_context.secret.lock().unwrap(); - let secret = secret_lock.take(); - match secret { - Some(secret) => secret, - None => { - drop(secret_lock); - error!("Ephemeral secret already taken"); - return Err(Error::internal("Ephemeral secret already taken")); - } - } - }; - - Ok((ephemeral_secret, unseal_context.client_public_key)) - } - - fn decrypt_client_key_material( - ephemeral_secret: EphemeralSecret, - client_public_key: PublicKey, - nonce: &[u8], - ciphertext: &[u8], - associated_data: &[u8], - ) -> Result>, ()> { - let nonce = XNonce::from_slice(nonce); - - let shared_secret = ephemeral_secret.diffie_hellman(&client_public_key); - let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into()); - - let mut key_buffer = SafeCell::new(ciphertext.to_vec()); - - let decryption_result = key_buffer.write_inline(|write_handle| { - cipher.decrypt_in_place(nonce, associated_data, write_handle) - }); - - match decryption_result { - Ok(_) => Ok(key_buffer), - Err(err) => { - error!(?err, "Failed to decrypt encrypted key material"); - Err(()) - } - } - } -} - -pub struct UnsealStartResponse { - pub server_pubkey: PublicKey, -} - -#[derive(Debug, Error)] -pub enum UnsealError { - #[error("Invalid key provided for unsealing")] - InvalidKey, - #[error("Internal error during unsealing process")] - General(#[from] super::Error), -} - -#[derive(Debug, Error)] -pub enum BootstrapError { - #[error("Invalid key provided for bootstrapping")] - InvalidKey, - #[error("Vault is already bootstrapped")] - AlreadyBootstrapped, - - #[error("Internal error during bootstrapping process")] - General(#[from] super::Error), -} - -#[derive(Debug, Error)] -pub enum SignTransactionError { - #[error("Policy evaluation failed")] - Vet(#[from] crate::evm::VetError), - - #[error("Internal signing error")] - Internal, -} - -#[derive(Debug, Error)] -pub enum GrantMutationError { - #[error("Vault is sealed")] - VaultSealed, - - #[error("Internal grant mutation error")] - Internal, -} - -#[messages] -impl UserAgentSession { - #[message] - pub async fn handle_unseal_request( - &mut self, - client_pubkey: x25519_dalek::PublicKey, - ) -> Result { - let secret = EphemeralSecret::random(); - let public_key = PublicKey::from(&secret); - - self.transition(UserAgentEvents::UnsealRequest(UnsealContext { - secret: Mutex::new(Some(secret)), - client_public_key: client_pubkey, - }))?; - - Ok(UnsealStartResponse { - server_pubkey: public_key, - }) - } - - #[message] - pub async fn handle_unseal_encrypted_key( - &mut self, - nonce: Vec, - ciphertext: Vec, - associated_data: Vec, - ) -> Result<(), UnsealError> { - let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() { - Ok(values) => values, - Err(Error::State) => { - self.transition(UserAgentEvents::ReceivedInvalidKey)?; - return Err(UnsealError::InvalidKey); - } - Err(_err) => { - return Err(Error::internal("Failed to take unseal secret").into()); - } - }; - - let seal_key_buffer = match Self::decrypt_client_key_material( - ephemeral_secret, - client_public_key, - &nonce, - &ciphertext, - &associated_data, - ) { - Ok(buffer) => buffer, - Err(()) => { - self.transition(UserAgentEvents::ReceivedInvalidKey)?; - return Err(UnsealError::InvalidKey); - } - }; - - match self - .props - .actors - .key_holder - .ask(TryUnseal { - seal_key_raw: seal_key_buffer, - }) - .await - { - Ok(_) => { - info!("Successfully unsealed key with client-provided key"); - self.transition(UserAgentEvents::ReceivedValidKey)?; - Ok(()) - } - Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => { - self.transition(UserAgentEvents::ReceivedInvalidKey)?; - Err(UnsealError::InvalidKey) - } - Err(SendError::HandlerError(err)) => { - error!(?err, "Keyholder failed to unseal key"); - self.transition(UserAgentEvents::ReceivedInvalidKey)?; - Err(UnsealError::InvalidKey) - } - Err(err) => { - error!(?err, "Failed to send unseal request to keyholder"); - self.transition(UserAgentEvents::ReceivedInvalidKey)?; - Err(Error::internal("Vault actor error").into()) - } - } - } - - #[message] - pub(crate) async fn handle_bootstrap_encrypted_key( - &mut self, - nonce: Vec, - ciphertext: Vec, - associated_data: Vec, - ) -> Result<(), BootstrapError> { - let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() { - Ok(values) => values, - Err(Error::State) => { - self.transition(UserAgentEvents::ReceivedInvalidKey)?; - return Err(BootstrapError::InvalidKey); - } - Err(err) => return Err(err.into()), - }; - - let seal_key_buffer = match Self::decrypt_client_key_material( - ephemeral_secret, - client_public_key, - &nonce, - &ciphertext, - &associated_data, - ) { - Ok(buffer) => buffer, - Err(()) => { - self.transition(UserAgentEvents::ReceivedInvalidKey)?; - return Err(BootstrapError::InvalidKey); - } - }; - - match self - .props - .actors - .key_holder - .ask(Bootstrap { - seal_key_raw: seal_key_buffer, - }) - .await - { - Ok(_) => { - info!("Successfully bootstrapped vault with client-provided key"); - self.transition(UserAgentEvents::ReceivedValidKey)?; - Ok(()) - } - Err(SendError::HandlerError(keyholder::Error::AlreadyBootstrapped)) => { - self.transition(UserAgentEvents::ReceivedInvalidKey)?; - Err(BootstrapError::AlreadyBootstrapped) - } - Err(SendError::HandlerError(err)) => { - error!(?err, "Keyholder failed to bootstrap vault"); - self.transition(UserAgentEvents::ReceivedInvalidKey)?; - Err(BootstrapError::InvalidKey) - } - Err(err) => { - error!(?err, "Failed to send bootstrap request to keyholder"); - self.transition(UserAgentEvents::ReceivedInvalidKey)?; - Err(BootstrapError::General(Error::internal( - "Vault actor error", - ))) - } - } - } -} - -#[messages] -impl UserAgentSession { - #[message] - pub(crate) async fn handle_query_vault_state(&mut self) -> Result { - use crate::actors::keyholder::GetState; - - let vault_state = match self.props.actors.key_holder.ask(GetState {}).await { - Ok(state) => state, - Err(err) => { - error!(?err, actor = "useragent", "keyholder.query.failed"); - return Err(Error::internal("Vault is in broken state")); - } - }; - - Ok(vault_state) - } -} - -#[messages] -impl UserAgentSession { - #[message] - pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result<(i32, Address), Error> { - match self.props.actors.evm.ask(Generate {}).await { - Ok(address) => Ok(address), - Err(SendError::HandlerError(err)) => Err(Error::internal(format!( - "EVM wallet generation failed: {err}" - ))), - Err(err) => { - error!(?err, "EVM actor unreachable during wallet create"); - Err(Error::internal("EVM actor unreachable")) - } - } - } - - #[message] - pub(crate) async fn handle_evm_wallet_list(&mut self) -> Result, Error> { - match self.props.actors.evm.ask(ListWallets {}).await { - Ok(wallets) => Ok(wallets), - Err(err) => { - error!(?err, "EVM wallet list failed"); - Err(Error::internal("Failed to list EVM wallets")) - } - } - } -} - -#[messages] -impl UserAgentSession { - #[message] - pub(crate) async fn handle_grant_list(&mut self) -> Result>, Error> { - match self.props.actors.evm.ask(UseragentListGrants {}).await { - Ok(grants) => Ok(grants), - Err(err) => { - error!(?err, "EVM grant list failed"); - Err(Error::internal("Failed to list EVM grants")) - } - } - } - - #[message] - pub(crate) async fn handle_grant_create( - &mut self, - basic: crate::evm::policies::SharedGrantSettings, - grant: crate::evm::policies::SpecificGrant, - ) -> Result { - match self - .props - .actors - .evm - .ask(UseragentCreateGrant { basic, grant }) - .await - { - Ok(grant_id) => Ok(grant_id), - Err(err) => { - error!(?err, "EVM grant create failed"); - Err(GrantMutationError::Internal) - } - } - } - - #[message] - pub(crate) async fn handle_grant_delete( - &mut self, - grant_id: i32, - ) -> Result<(), GrantMutationError> { - // match self - // .props - // .actors - // .evm - // .ask(UseragentDeleteGrant { grant_id }) - // .await - // { - // Ok(()) => Ok(()), - // Err(err) => { - // error!(?err, "EVM grant delete failed"); - // Err(GrantMutationError::Internal) - // } - // } - let _ = grant_id; - todo!() - } - - #[message] - pub(crate) async fn handle_sign_transaction( - &mut self, - client_id: i32, - wallet_address: Address, - transaction: TxEip1559, - ) -> Result { - match self - .props - .actors - .evm - .ask(ClientSignTransaction { - client_id, - wallet_address, - transaction, - }) - .await - { - Ok(signature) => Ok(signature), - Err(SendError::HandlerError(EvmSignError::Vet(vet_error))) => { - Err(SignTransactionError::Vet(vet_error)) - } - Err(err) => { - error!(?err, "EVM sign transaction failed in user-agent session"); - Err(SignTransactionError::Internal) - } - } - } - - #[message] - pub(crate) async fn handle_grant_evm_wallet_access( - &mut self, - entries: Vec, - ) -> Result<(), Error> { - let mut conn = self.props.db.get().await?; - conn.transaction(|conn| { - Box::pin(async move { - use crate::db::schema::evm_wallet_access; - - for entry in entries { - diesel::insert_into(evm_wallet_access::table) - .values(&entry) - .on_conflict_do_nothing() - .execute(conn) - .await?; - } - - Result::<_, Error>::Ok(()) - }) - }) - .await?; - Ok(()) - } - - #[message] - pub(crate) async fn handle_revoke_evm_wallet_access( - &mut self, - entries: Vec, - ) -> Result<(), Error> { - let mut conn = self.props.db.get().await?; - conn.transaction(|conn| { - Box::pin(async move { - use crate::db::schema::evm_wallet_access; - for entry in entries { - diesel::delete(evm_wallet_access::table) - .filter(evm_wallet_access::wallet_id.eq(entry)) - .execute(conn) - .await?; - } - - Result::<_, Error>::Ok(()) - }) - }) - .await?; - Ok(()) - } - - #[message] - pub(crate) async fn handle_list_wallet_access( - &mut self, - ) -> Result, Error> { - let mut conn = self.props.db.get().await?; - use crate::db::schema::evm_wallet_access; - let access_entries = evm_wallet_access::table - .select(EvmWalletAccess::as_select()) - .load::<_>(&mut conn) - .await?; - Ok(access_entries) - } -} - -#[messages] -impl UserAgentSession { - #[message(ctx)] - pub(crate) async fn handle_new_client_approve( - &mut self, - approved: bool, - pubkey: authn::PublicKey, - ctx: &mut Context>, - ) -> Result<(), Error> { - let pending_approval = match self.pending_client_approvals.remove(&pubkey.to_bytes()) { - Some(approval) => approval, - None => { - error!("Received client connection response for unknown client"); - return Err(Error::internal("Unknown client in connection response")); - } - }; - - pending_approval - .controller - .tell(ClientApprovalAnswer { approved }) - .await - .map_err(|err| { - error!( - ?err, - "Failed to send client approval response to controller" - ); - Error::internal("Failed to send client approval response to controller") - })?; - - ctx.actor_ref().unlink(&pending_approval.controller).await; - - Ok(()) - } - - #[message] - pub(crate) async fn handle_sdk_client_list( - &mut self, - ) -> Result, Error> { - use crate::db::schema::{client_metadata, program_client}; - let mut conn = self.props.db.get().await?; - - let clients = program_client::table - .inner_join(client_metadata::table) - .select(( - ProgramClient::as_select(), - ProgramClientMetadata::as_select(), - )) - .load::<(ProgramClient, ProgramClientMetadata)>(&mut conn) - .await?; - - Ok(clients) - } -} diff --git a/server/crates/arbiter-server/src/actors/user_agent/session/state.rs b/server/crates/arbiter-server/src/actors/user_agent/session/state.rs deleted file mode 100644 index 23ab674..0000000 --- a/server/crates/arbiter-server/src/actors/user_agent/session/state.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::sync::Mutex; - -use x25519_dalek::{EphemeralSecret, PublicKey}; - -pub struct UnsealContext { - pub client_public_key: PublicKey, - pub secret: Mutex>, -} - -smlang::statemachine!( - name: UserAgent, - custom_error: false, - transitions: { - *Idle + UnsealRequest(UnsealContext) / generate_temp_keypair = WaitingForUnsealKey(UnsealContext), - WaitingForUnsealKey(UnsealContext) + ReceivedValidKey = Unsealed, - WaitingForUnsealKey(UnsealContext) + ReceivedInvalidKey = Idle, - } -); - -pub struct DummyContext; -impl UserAgentStateMachineContext for DummyContext { - #[allow(missing_docs)] - #[allow(clippy::unused_unit)] - fn generate_temp_keypair(&mut self, event_data: UnsealContext) -> Result { - Ok(event_data) - } -} diff --git a/server/crates/arbiter-server/src/actors/useragent_registry.rs b/server/crates/arbiter-server/src/actors/useragent_registry.rs new file mode 100644 index 0000000..15e6080 --- /dev/null +++ b/server/crates/arbiter-server/src/actors/useragent_registry.rs @@ -0,0 +1,61 @@ +use crate::peers::user_agent::UserAgentSession; + +use kameo::{ + Actor, + actor::{ActorId, ActorRef}, + error::Infallible, + messages, + prelude::{ActorStopReason, Context, WeakActorRef}, +}; +use std::{collections::HashMap, ops::ControlFlow}; +use tracing::info; + +#[derive(Default)] +pub struct UserAgentRegistry { + connected: HashMap>, +} + +impl Actor for UserAgentRegistry { + type Args = Self; + + type Error = Infallible; + + async fn on_start(args: Self::Args, _: ActorRef) -> Result { + Ok(args) + } + + async fn on_link_died( + &mut self, + _: WeakActorRef, + id: ActorId, + _: ActorStopReason, + ) -> Result, Self::Error> { + if self.connected.remove(&id).is_some() { + info!( + ?id, + actor = "UserAgentRegistry", + event = "useragent.disconnected" + ); + } + Ok(ControlFlow::Continue(())) + } +} + +#[messages] +impl UserAgentRegistry { + #[message(ctx)] + pub async fn connect_useragent( + &mut self, + actor: ActorRef, + ctx: &mut Context, + ) { + info!(id = %actor.id(), actor = "UserAgentRegistry", event = "useragent.connected"); + ctx.actor_ref().link(&actor).await; + self.connected.insert(actor.id(), actor); + } + + #[message] + pub fn get_connected(&self) -> Vec> { + self.connected.values().cloned().collect() + } +} diff --git a/server/crates/arbiter-server/src/actors/keyholder/mod.rs b/server/crates/arbiter-server/src/actors/vault/mod.rs similarity index 83% rename from server/crates/arbiter-server/src/actors/keyholder/mod.rs rename to server/crates/arbiter-server/src/actors/vault/mod.rs index 8c8aa2a..8ddfe08 100644 --- a/server/crates/arbiter-server/src/actors/keyholder/mod.rs +++ b/server/crates/arbiter-server/src/actors/vault/mod.rs @@ -1,3 +1,17 @@ +use crate::{ + crypto::{ + KeyCell, derive_key, + encryption::v1::{self, Nonce}, + integrity::v1::HmacSha256, + }, + db::{ + self, + models::{self, RootKeyHistory}, + schema::{self}, + }, +}; +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; + use chrono::Utc; use diesel::{ ExpressionMethods as _, OptionalExtension, QueryDsl, SelectableHelper, @@ -5,42 +19,31 @@ use diesel::{ }; use diesel_async::{AsyncConnection, RunQueryDsl}; use hmac::{KeyInit as _, Mac as _}; -use kameo::{Actor, Reply, messages}; +use kameo::{Actor, Reply, actor::ActorRef, messages}; +use kameo_actors::message_bus::{MessageBus, Publish}; use strum::{EnumDiscriminants, IntoDiscriminant}; use tracing::{error, info}; -use crate::crypto::{ - KeyCell, derive_key, - encryption::v1::{self, Nonce}, - integrity::v1::HmacSha256, -}; -use crate::db::{ - self, - models::{self, RootKeyHistory}, - schema::{self}, -}; -use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; +pub mod events { -#[derive(Default, EnumDiscriminants)] -#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))] -enum State { - #[default] - Unbootstrapped, - Sealed { - root_key_history_id: i32, - }, - Unsealed { - root_key_history_id: i32, - root_key: KeyCell, - }, + #[derive(Clone, Copy)] + pub struct Bootstrapped; + + #[derive(Clone, Copy)] + pub struct Unsealed; + + #[derive(Clone, Copy)] + pub struct VaultResealed; } #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Keyholder is already bootstrapped")] + #[error("Vault is already bootstrapped")] AlreadyBootstrapped, - #[error("Keyholder is not bootstrapped")] + #[error("Vault is not bootstrapped")] NotBootstrapped, + #[error("Vault is sealed")] + Sealed, #[error("Invalid key provided")] InvalidKey, @@ -60,18 +63,35 @@ pub enum Error { BrokenDatabase, } +struct Unsealed { + root_key_history_id: i32, + root_key: KeyCell, +} + +#[derive(Default, EnumDiscriminants)] +#[strum_discriminants(derive(Reply), vis(pub), name(VaultState))] +enum State { + #[default] + Unbootstrapped, + Sealed { + root_key_history_id: i32, + }, + Unsealed(Unsealed), +} + /// Manages vault root key and tracks current state of the vault (bootstrapped/unbootstrapped, sealed/unsealed). /// Provides API for encrypting and decrypting data using the vault root key. /// Abstraction over database to make sure nonces are never reused and encryption keys are never exposed in plaintext outside of this actor. #[derive(Actor)] -pub struct KeyHolder { +pub struct Vault { db: db::DatabasePool, state: State, + events: ActorRef, } #[messages] -impl KeyHolder { - pub async fn new(db: db::DatabasePool) -> Result { +impl Vault { + pub async fn new(db: db::DatabasePool, events: ActorRef) -> Result { let state = { let mut conn = db.get().await?; @@ -89,10 +109,10 @@ impl KeyHolder { } }; - Ok(Self { db, state }) + Ok(Self { db, state, events }) } - // Exclusive transaction to avoid race condtions if multiple keyholders write + // Exclusive transaction to avoid race condtions if multiple vaults write // additional layer of protection against nonce-reuse async fn get_new_nonce(pool: &db::DatabasePool, root_key_id: i32) -> Result { let mut conn = pool.get().await?; @@ -129,6 +149,14 @@ impl KeyHolder { Ok(nonce) } + fn expect_unsealed(state: &mut State) -> Result<&mut Unsealed, Error> { + match state { + State::Unsealed(unsealed) => Ok(unsealed), + State::Unbootstrapped => Err(Error::NotBootstrapped), + State::Sealed { .. } => Err(Error::Sealed), + } + } + #[message] pub async fn bootstrap(&mut self, seal_key_raw: SafeCell>) -> Result<(), Error> { if !matches!(self.state, State::Unbootstrapped) { @@ -181,12 +209,13 @@ impl KeyHolder { }) .await?; - self.state = State::Unsealed { + self.state = State::Unsealed(Unsealed { root_key, root_key_history_id, - }; + }); - info!("Keyholder bootstrapped successfully"); + info!("Vault bootstrapped successfully"); + let _ = self.events.tell(Publish(events::Bootstrapped)).await; Ok(()) } @@ -233,24 +262,23 @@ impl KeyHolder { Error::InvalidKey })?; - self.state = State::Unsealed { + self.state = State::Unsealed(Unsealed { root_key_history_id: current_key.id, root_key: KeyCell::try_from(root_key).map_err(|err| { error!(?err, "Broken database: invalid encryption key size"); Error::BrokenDatabase })?, - }; + }); - info!("Keyholder unsealed successfully"); + info!("Vault unsealed successfully"); + let _ = self.events.tell(Publish(events::Unsealed)).await; Ok(()) } #[message] pub async fn decrypt(&mut self, aead_id: i32) -> Result>, Error> { - let State::Unsealed { root_key, .. } = &mut self.state else { - return Err(Error::NotBootstrapped); - }; + let Unsealed { root_key, .. } = Self::expect_unsealed(&mut self.state)?; let row: models::AeadEncrypted = { let mut conn = self.db.get().await?; @@ -278,14 +306,10 @@ impl KeyHolder { // Creates new `aead_encrypted` entry in the database and returns it's ID #[message] pub async fn create_new(&mut self, mut plaintext: SafeCell>) -> Result { - let State::Unsealed { + let Unsealed { root_key, root_key_history_id, - .. - } = &mut self.state - else { - return Err(Error::NotBootstrapped); - }; + } = Self::expect_unsealed(&mut self.state)?; // Order matters here - `get_new_nonce` acquires connection, so we need to call it before next acquire // Borrow checker note: &mut borrow a few lines above is disjoint from this field @@ -315,19 +339,16 @@ impl KeyHolder { } #[message] - pub fn get_state(&self) -> KeyHolderState { + pub fn get_state(&self) -> VaultState { self.state.discriminant() } #[message] pub fn sign_integrity(&mut self, mac_input: Vec) -> Result<(i32, Vec), Error> { - let State::Unsealed { + let Unsealed { root_key, root_key_history_id, - } = &mut self.state - else { - return Err(Error::NotBootstrapped); - }; + } = Self::expect_unsealed(&mut self.state)?; let mut hmac = root_key .0 @@ -349,13 +370,10 @@ impl KeyHolder { expected_mac: Vec, key_version: i32, ) -> Result { - let State::Unsealed { + let Unsealed { root_key, root_key_history_id, - } = &mut self.state - else { - return Err(Error::NotBootstrapped); - }; + } = Self::expect_unsealed(&mut self.state)?; if *root_key_history_id != key_version { return Ok(false); @@ -374,17 +392,16 @@ impl KeyHolder { } #[message] - pub fn seal(&mut self) -> Result<(), Error> { - let State::Unsealed { + pub async fn seal(&mut self) -> Result<(), Error> { + let Unsealed { root_key_history_id, .. - } = &self.state - else { - return Err(Error::NotBootstrapped); - }; + } = Self::expect_unsealed(&mut self.state)?; + self.state = State::Sealed { root_key_history_id: *root_key_history_id, }; + let _ = self.events.tell(Publish(events::VaultResealed)).await; Ok(()) } } @@ -395,13 +412,18 @@ mod tests { use diesel_async::RunQueryDsl; - use crate::db::{self}; + use crate::{ + actors::GlobalActors, + db::{self}, + }; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use super::*; - async fn bootstrapped_actor(db: &db::DatabasePool) -> KeyHolder { - let mut actor = KeyHolder::new(db.clone()).await.unwrap(); + async fn bootstrapped_actor(db: &db::DatabasePool) -> Vault { + let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()) + .await + .unwrap(); let seal_key = SafeCell::new(b"test-seal-key".to_vec()); actor.bootstrap(seal_key).await.unwrap(); actor @@ -413,17 +435,17 @@ mod tests { let db = db::create_test_pool().await; let mut actor = bootstrapped_actor(&db).await; let root_key_history_id = match actor.state { - State::Unsealed { + State::Unsealed(Unsealed { root_key_history_id, .. - } => root_key_history_id, + }) => root_key_history_id, _ => panic!("expected unsealed state"), }; - let n1 = KeyHolder::get_new_nonce(&db, root_key_history_id) + let n1 = Vault::get_new_nonce(&db, root_key_history_id) .await .unwrap(); - let n2 = KeyHolder::get_new_nonce(&db, root_key_history_id) + let n2 = Vault::get_new_nonce(&db, root_key_history_id) .await .unwrap(); assert!(n2.to_vec() > n1.to_vec(), "nonce must increase"); diff --git a/server/crates/arbiter-server/src/context/mod.rs b/server/crates/arbiter-server/src/context/mod.rs index dd44655..aeea460 100644 --- a/server/crates/arbiter-server/src/context/mod.rs +++ b/server/crates/arbiter-server/src/context/mod.rs @@ -1,13 +1,12 @@ -use std::sync::Arc; - -use thiserror::Error; - use crate::{ actors::GlobalActors, context::tls::TlsManager, db::{self}, }; +use std::sync::Arc; +use thiserror::Error; + pub mod tls; #[derive(Error, Debug)] diff --git a/server/crates/arbiter-server/src/context/tls.rs b/server/crates/arbiter-server/src/context/tls.rs index 786b68f..4c9d0fc 100644 --- a/server/crates/arbiter-server/src/context/tls.rs +++ b/server/crates/arbiter-server/src/context/tls.rs @@ -1,17 +1,3 @@ -use std::{net::Ipv4Addr, string::FromUtf8Error}; - -use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper as _}; -use diesel_async::{AsyncConnection, RunQueryDsl}; - -use pem::Pem; -use rcgen::{ - BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType, - IsCa, Issuer, KeyPair, KeyUsagePurpose, SanType, -}; -use rustls::pki_types::pem::PemObject; -use thiserror::Error; -use tonic::transport::CertificateDer; - use crate::db::{ self, models::{NewTlsHistory, TlsHistory}, @@ -21,6 +7,18 @@ use crate::db::{ }, }; +use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper as _}; +use diesel_async::{AsyncConnection, RunQueryDsl}; +use pem::Pem; +use rcgen::{ + BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType, + IsCa, Issuer, KeyPair, KeyUsagePurpose, SanType, +}; +use rustls::pki_types::pem::PemObject; +use std::{net::Ipv4Addr, string::FromUtf8Error}; +use thiserror::Error; +use tonic::transport::CertificateDer; + const ENCODE_CONFIG: pem::EncodeConfig = { let line_ending = match cfg!(target_family = "windows") { true => pem::LineEnding::CRLF, diff --git a/server/crates/arbiter-server/src/crypto/encryption/v1.rs b/server/crates/arbiter-server/src/crypto/encryption/v1.rs index e2b7c04..d57c481 100644 --- a/server/crates/arbiter-server/src/crypto/encryption/v1.rs +++ b/server/crates/arbiter-server/src/crypto/encryption/v1.rs @@ -1,5 +1,4 @@ use argon2::password_hash::Salt as ArgonSalt; - use rand::{ Rng as _, SeedableRng, rngs::{StdRng, SysRng}, @@ -63,7 +62,7 @@ mod tests { use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; #[test] - pub fn derive_seal_key_deterministic() { + fn derive_seal_key_deterministic() { static PASSWORD: &[u8] = b"password"; let password = SafeCell::new(PASSWORD.to_vec()); let password2 = SafeCell::new(PASSWORD.to_vec()); @@ -79,7 +78,7 @@ mod tests { } #[test] - pub fn successful_derive() { + fn successful_derive() { static PASSWORD: &[u8] = b"password"; let password = SafeCell::new(PASSWORD.to_vec()); let salt = generate_salt(); @@ -93,7 +92,7 @@ mod tests { #[test] // We should fuzz this - pub fn test_nonce_increment() { + fn test_nonce_increment() { let mut nonce = Nonce([0u8; NONCE_LENGTH]); nonce.increment(); diff --git a/server/crates/arbiter-server/src/crypto/integrity/v1.rs b/server/crates/arbiter-server/src/crypto/integrity/v1.rs index 1b89df8..378687e 100644 --- a/server/crates/arbiter-server/src/crypto/integrity/v1.rs +++ b/server/crates/arbiter-server/src/crypto/integrity/v1.rs @@ -1,32 +1,29 @@ -use crate::actors::keyholder; -use arbiter_crypto::hashing::Hashable; -use hmac::Hmac; -use sha2::Sha256; - -use diesel::{ExpressionMethods as _, QueryDsl, dsl::insert_into, sqlite::Sqlite}; -use diesel_async::{AsyncConnection, RunQueryDsl}; -use kameo::{actor::ActorRef, error::SendError}; -use sha2::Digest as _; - use crate::{ - actors::keyholder::{KeyHolder, SignIntegrity, VerifyIntegrity}, + actors::vault::{self, GetState, SignIntegrity, Vault, VerifyIntegrity}, db::{ self, models::{IntegrityEnvelope, NewIntegrityEnvelope}, schema::integrity_envelope, }, }; +use arbiter_crypto::hashing::Hashable; + +use diesel::{ExpressionMethods as _, QueryDsl, dsl::insert_into, sqlite::Sqlite}; +use diesel_async::{AsyncConnection, RunQueryDsl}; +use hmac::Hmac; +use kameo::{actor::ActorRef, error::SendError}; +use sha2::{Digest as _, Sha256}; #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Database error: {0}")] Database(#[from] db::DatabaseError), - #[error("KeyHolder error: {0}")] - Keyholder(#[from] keyholder::Error), + #[error("Vault error: {0}")] + Vault(#[from] vault::Error), - #[error("KeyHolder mailbox error")] - KeyholderSend, + #[error("Vault mailbox error")] + VaultSend, #[error("Integrity envelope is missing for entity {entity_kind}")] MissingEnvelope { entity_kind: &'static str }, @@ -103,7 +100,7 @@ impl IntoId for &'_ [u8] { pub async fn sign_entity( conn: &mut impl AsyncConnection, - keyholder: &ActorRef, + vault: &ActorRef, entity: &E, entity_id: impl IntoId, ) -> Result<(), Error> { @@ -113,13 +110,14 @@ pub async fn sign_entity( let mac_input = build_mac_input(E::KIND, &entity_id, E::VERSION, &payload_hash); - let (key_version, mac) = keyholder - .ask(SignIntegrity { mac_input }) - .await - .map_err(|err| match err { - kameo::error::SendError::HandlerError(inner) => Error::Keyholder(inner), - _ => Error::KeyholderSend, - })?; + let (key_version, mac) = + vault + .ask(SignIntegrity { mac_input }) + .await + .map_err(|err| match err { + kameo::error::SendError::HandlerError(inner) => Error::Vault(inner), + _ => Error::VaultSend, + })?; insert_into(integrity_envelope::table) .values(NewIntegrityEnvelope { @@ -148,7 +146,7 @@ pub async fn sign_entity( pub async fn verify_entity( conn: &mut impl AsyncConnection, - keyholder: &ActorRef, + vault: &ActorRef, entity: &E, entity_id: impl IntoId, ) -> Result { @@ -176,7 +174,7 @@ pub async fn verify_entity( let payload_hash = payload_hash(&entity); let mac_input = build_mac_input(E::KIND, &entity_id, envelope.payload_version, &payload_hash); - let result = keyholder + let result = vault .ask(VerifyIntegrity { mac_input, expected_mac: envelope.mac, @@ -189,13 +187,16 @@ pub async fn verify_entity( Ok(false) => Err(Error::MacMismatch { entity_kind: E::KIND, }), - Err(SendError::HandlerError(keyholder::Error::NotBootstrapped)) => { - Ok(AttestationStatus::Unavailable) - } - Err(_) => Err(Error::KeyholderSend), + Err(SendError::HandlerError(vault::Error::Sealed)) => Ok(AttestationStatus::Unavailable), + Err(_) => Err(Error::VaultSend), } } +pub async fn is_signing_available(vault: &ActorRef) -> Result { + let state = vault.ask(GetState).await.map_err(|_| Error::VaultSend)?; + Ok(matches!(state, vault::VaultState::Unsealed)) +} + #[cfg(test)] mod tests { use diesel::{ExpressionMethods as _, QueryDsl}; @@ -203,7 +204,10 @@ mod tests { use kameo::{actor::ActorRef, prelude::Spawn}; use crate::{ - actors::keyholder::{Bootstrap, KeyHolder}, + actors::{ + GlobalActors, + vault::{Bootstrap, Vault}, + }, db::{self, schema}, }; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; @@ -218,8 +222,12 @@ mod tests { const KIND: &'static str = "dummy_entity"; } - async fn bootstrapped_keyholder(db: &db::DatabasePool) -> ActorRef { - let actor = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap()); + async fn bootstrapped_vault(db: &db::DatabasePool) -> ActorRef { + let actor = Vault::spawn( + Vault::new(db.clone(), GlobalActors::spawn_message_bus()) + .await + .unwrap(), + ); actor .ask(Bootstrap { seal_key_raw: SafeCell::new(b"integrity-test-seal-key".to_vec()), @@ -232,7 +240,7 @@ mod tests { #[tokio::test] async fn sign_writes_envelope_and_verify_passes() { let db = db::create_test_pool().await; - let keyholder = bootstrapped_keyholder(&db).await; + let vault = bootstrapped_vault(&db).await; let mut conn = db.get().await.unwrap(); const ENTITY_ID: &[u8] = b"entity-id-7"; @@ -242,7 +250,7 @@ mod tests { payload: b"payload-v1".to_vec(), }; - sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID) + sign_entity(&mut conn, &vault, &entity, ENTITY_ID) .await .unwrap(); @@ -255,7 +263,7 @@ mod tests { .unwrap(); assert_eq!(count, 1, "envelope row must be created exactly once"); - verify_entity(&mut conn, &keyholder, &entity, ENTITY_ID) + verify_entity(&mut conn, &vault, &entity, ENTITY_ID) .await .unwrap(); } @@ -263,7 +271,7 @@ mod tests { #[tokio::test] async fn tampered_mac_fails_verification() { let db = db::create_test_pool().await; - let keyholder = bootstrapped_keyholder(&db).await; + let vault = bootstrapped_vault(&db).await; let mut conn = db.get().await.unwrap(); const ENTITY_ID: &[u8] = b"entity-id-11"; @@ -273,7 +281,7 @@ mod tests { payload: b"payload-v1".to_vec(), }; - sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID) + sign_entity(&mut conn, &vault, &entity, ENTITY_ID) .await .unwrap(); @@ -285,7 +293,7 @@ mod tests { .await .unwrap(); - let err = verify_entity(&mut conn, &keyholder, &entity, ENTITY_ID) + let err = verify_entity(&mut conn, &vault, &entity, ENTITY_ID) .await .unwrap_err(); assert!(matches!(err, Error::MacMismatch { .. })); @@ -294,7 +302,7 @@ mod tests { #[tokio::test] async fn changed_payload_fails_verification() { let db = db::create_test_pool().await; - let keyholder = bootstrapped_keyholder(&db).await; + let vault = bootstrapped_vault(&db).await; let mut conn = db.get().await.unwrap(); const ENTITY_ID: &[u8] = b"entity-id-21"; @@ -304,7 +312,7 @@ mod tests { payload: b"payload-v1".to_vec(), }; - sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID) + sign_entity(&mut conn, &vault, &entity, ENTITY_ID) .await .unwrap(); @@ -313,7 +321,7 @@ mod tests { ..entity }; - let err = verify_entity(&mut conn, &keyholder, &tampered, ENTITY_ID) + let err = verify_entity(&mut conn, &vault, &tampered, ENTITY_ID) .await .unwrap_err(); assert!(matches!(err, Error::MacMismatch { .. })); diff --git a/server/crates/arbiter-server/src/crypto/mod.rs b/server/crates/arbiter-server/src/crypto/mod.rs index 5a11898..b331209 100644 --- a/server/crates/arbiter-server/src/crypto/mod.rs +++ b/server/crates/arbiter-server/src/crypto/mod.rs @@ -1,4 +1,5 @@ -use std::ops::Deref as _; +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; +use encryption::v1::{Nonce, Salt}; use argon2::{Algorithm, Argon2}; use chacha20poly1305::{ @@ -9,14 +10,11 @@ use rand::{ Rng as _, SeedableRng as _, rngs::{StdRng, SysRng}, }; - -use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; +use std::ops::Deref as _; pub mod encryption; pub mod integrity; -use encryption::v1::{Nonce, Salt}; - pub struct KeyCell(pub SafeCell); impl From> for KeyCell { fn from(value: SafeCell) -> Self { @@ -144,7 +142,7 @@ mod tests { use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; #[test] - pub fn encrypt_decrypt() { + fn encrypt_decrypt() { static PASSWORD: &[u8] = b"password"; let password = SafeCell::new(PASSWORD.to_vec()); let salt = generate_salt(); diff --git a/server/crates/arbiter-server/src/db/mod.rs b/server/crates/arbiter-server/src/db/mod.rs index 9971ad2..7e67663 100644 --- a/server/crates/arbiter-server/src/db/mod.rs +++ b/server/crates/arbiter-server/src/db/mod.rs @@ -5,7 +5,6 @@ use diesel_async::{ sync_connection_wrapper::SyncConnectionWrapper, }; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; - use thiserror::Error; use tracing::info; diff --git a/server/crates/arbiter-server/src/db/models.rs b/server/crates/arbiter-server/src/db/models.rs index dba14dc..1de6b1d 100644 --- a/server/crates/arbiter-server/src/db/models.rs +++ b/server/crates/arbiter-server/src/db/models.rs @@ -1,12 +1,12 @@ #![allow(unused)] #![allow(clippy::all)] - use crate::db::schema::{ self, aead_encrypted, arbiter_settings, evm_basic_grant, evm_ether_transfer_grant, evm_ether_transfer_grant_target, evm_ether_transfer_limit, evm_token_transfer_grant, evm_token_transfer_log, evm_token_transfer_volume_limit, evm_transaction_log, evm_wallet, integrity_envelope, root_key_history, tls_history, }; + use chrono::{DateTime, Utc}; use diesel::{prelude::*, sqlite::Sqlite}; use restructed::Models; @@ -195,7 +195,6 @@ pub struct ProgramClientMetadataHistory { #[diesel(table_name = schema::program_client, check_for_backend(Sqlite))] pub struct ProgramClient { pub id: i32, - pub nonce: i32, pub public_key: Vec, pub metadata_id: i32, pub created_at: SqliteTimestamp, @@ -206,7 +205,6 @@ pub struct ProgramClient { #[diesel(table_name = schema::useragent_client, check_for_backend(Sqlite))] pub struct UseragentClient { pub id: i32, - pub nonce: i32, pub public_key: Vec, pub created_at: SqliteTimestamp, pub updated_at: SqliteTimestamp, diff --git a/server/crates/arbiter-server/src/db/schema.rs b/server/crates/arbiter-server/src/db/schema.rs index c9b980c..6d1c6b2 100644 --- a/server/crates/arbiter-server/src/db/schema.rs +++ b/server/crates/arbiter-server/src/db/schema.rs @@ -155,7 +155,6 @@ diesel::table! { diesel::table! { program_client (id) { id -> Integer, - nonce -> Integer, public_key -> Binary, metadata_id -> Integer, created_at -> Integer, @@ -189,9 +188,7 @@ diesel::table! { diesel::table! { useragent_client (id) { id -> Integer, - nonce -> Integer, public_key -> Binary, - key_type -> Integer, created_at -> Integer, updated_at -> Integer, } diff --git a/server/crates/arbiter-server/src/evm/mod.rs b/server/crates/arbiter-server/src/evm/mod.rs index 15ac999..c8629de 100644 --- a/server/crates/arbiter-server/src/evm/mod.rs +++ b/server/crates/arbiter-server/src/evm/mod.rs @@ -1,17 +1,5 @@ -pub mod abi; -pub mod safe_signer; - -use alloy::{ - consensus::TxEip1559, - primitives::{TxKind, U256}, -}; -use chrono::Utc; -use diesel::{ExpressionMethods as _, QueryDsl as _, QueryResult, insert_into, sqlite::Sqlite}; -use diesel_async::{AsyncConnection, RunQueryDsl}; -use kameo::actor::ActorRef; - use crate::{ - actors::keyholder::KeyHolder, + actors::vault::Vault, crypto::integrity, db::{ self, DatabaseError, @@ -27,6 +15,18 @@ use crate::{ }, }; +use alloy::{ + consensus::TxEip1559, + primitives::{TxKind, U256}, +}; +use chrono::Utc; +use diesel::{ExpressionMethods as _, QueryDsl as _, QueryResult, insert_into, sqlite::Sqlite}; +use diesel_async::{AsyncConnection, RunQueryDsl}; +use kameo::actor::ActorRef; + +pub mod abi; +pub mod safe_signer; + pub mod policies; mod utils; @@ -138,7 +138,7 @@ async fn check_shared_constraints( // Supporting only EIP-1559 transactions for now, but we can easily extend this to support legacy transactions if needed pub struct Engine { db: db::DatabasePool, - keyholder: ActorRef, + vault: ActorRef, } impl Engine { @@ -158,7 +158,7 @@ impl Engine { .map_err(DatabaseError::from)? .ok_or(PolicyError::NoMatchingGrant)?; - integrity::verify_entity(&mut conn, &self.keyholder, &grant.settings, grant.id).await?; + integrity::verify_entity(&mut conn, &self.vault, &grant.settings, grant.id).await?; let mut violations = check_shared_constraints( &context, @@ -207,8 +207,8 @@ impl Engine { } impl Engine { - pub fn new(db: db::DatabasePool, keyholder: ActorRef) -> Self { - Self { db, keyholder } + pub fn new(db: db::DatabasePool, vault: ActorRef) -> Self { + Self { db, vault } } pub async fn create_grant( @@ -219,7 +219,7 @@ impl Engine { P::Settings: Clone, { let mut conn = self.db.get().await?; - let keyholder = self.keyholder.clone(); + let vault = self.vault.clone(); let id = conn .transaction(|conn| { @@ -258,7 +258,7 @@ impl Engine { P::create_grant(&basic_grant, &full_grant.specific, conn).await?; - integrity::sign_entity(conn, &keyholder, &full_grant, basic_grant.id) + integrity::sign_entity(conn, &vault, &full_grant, basic_grant.id) .await .map_err(|_| diesel::result::Error::RollbackTransaction)?; @@ -283,7 +283,7 @@ impl Engine { // Verify integrity of all grants before returning any results for grant in &all_grants { - integrity::verify_entity(conn, &self.keyholder, &grant.settings, grant.id).await?; + integrity::verify_entity(conn, &self.vault, &grant.settings, grant.id).await?; } Ok(all_grants.into_iter().map(|g| Grant { diff --git a/server/crates/arbiter-server/src/evm/policies.rs b/server/crates/arbiter-server/src/evm/policies.rs index 828c52e..33773a3 100644 --- a/server/crates/arbiter-server/src/evm/policies.rs +++ b/server/crates/arbiter-server/src/evm/policies.rs @@ -1,4 +1,8 @@ -use std::fmt::Display; +use crate::{ + crypto::integrity::v1::Integrable, + db::models::{self, EvmBasicGrant, EvmWalletAccess}, + evm::utils, +}; use alloy::primitives::{Address, Bytes, ChainId, U256}; use chrono::{DateTime, Duration, Utc}; @@ -6,15 +10,9 @@ use diesel::{ ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, sqlite::Sqlite, }; use diesel_async::{AsyncConnection, RunQueryDsl}; - +use std::fmt::Display; use thiserror::Error; -use crate::{ - crypto::integrity::v1::Integrable, - db::models::{self, EvmBasicGrant, EvmWalletAccess}, - evm::utils, -}; - pub mod ether_transfer; pub mod token_transfers; diff --git a/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs b/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs index 3fa507b..d337a0b 100644 --- a/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs +++ b/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs @@ -1,30 +1,33 @@ -use std::collections::HashMap; -use std::fmt::Display; - -use alloy::primitives::{Address, U256}; -use chrono::{DateTime, Duration, Utc}; -use diesel::dsl::{auto_type, insert_into}; -use diesel::sqlite::Sqlite; -use diesel::{ExpressionMethods, JoinOnDsl, prelude::*}; -use diesel_async::{AsyncConnection, RunQueryDsl}; - -use crate::crypto::integrity::v1::Integrable; -use crate::db::models::{ - EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit, - NewEvmEtherTransferLimit, SqliteTimestamp, -}; -use crate::db::schema::{evm_basic_grant, evm_ether_transfer_limit, evm_transaction_log}; -use crate::evm::policies::{ - CombinedSettings, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit, -}; +use super::{DatabaseID, EvalContext, EvalViolation}; use crate::{ + crypto::integrity::v1::Integrable, + db::models::{ + EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit, + NewEvmEtherTransferLimit, SqliteTimestamp, + }, + db::schema::{evm_basic_grant, evm_ether_transfer_limit, evm_transaction_log}, db::{ models::{self, NewEvmEtherTransferGrant, NewEvmEtherTransferGrantTarget}, schema::{evm_ether_transfer_grant, evm_ether_transfer_grant_target}, }, + evm::policies::{ + CombinedSettings, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning, + VolumeRateLimit, + }, evm::{policies::Policy, utils}, }; +use alloy::primitives::{Address, U256}; +use chrono::{DateTime, Duration, Utc}; +use diesel::{ + ExpressionMethods, JoinOnDsl, + dsl::{auto_type, insert_into}, + prelude::*, + sqlite::Sqlite, +}; +use diesel_async::{AsyncConnection, RunQueryDsl}; +use std::{collections::HashMap, fmt::Display}; + #[auto_type] fn grant_join() -> _ { evm_ether_transfer_grant::table.inner_join( @@ -32,8 +35,6 @@ fn grant_join() -> _ { ) } -use super::{DatabaseID, EvalContext, EvalViolation}; - // Plain ether transfer #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Meaning { diff --git a/server/crates/arbiter-server/src/evm/policies/ether_transfer/tests.rs b/server/crates/arbiter-server/src/evm/policies/ether_transfer/tests.rs index 5253a25..519579b 100644 --- a/server/crates/arbiter-server/src/evm/policies/ether_transfer/tests.rs +++ b/server/crates/arbiter-server/src/evm/policies/ether_transfer/tests.rs @@ -1,25 +1,26 @@ +use super::{EtherTransfer, Settings}; +use crate::{ + db::{ + self, DatabaseConnection, + models::{ + EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp, + }, + schema::{evm_basic_grant, evm_transaction_log}, + }, + evm::{ + policies::{ + CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, + VolumeRateLimit, + }, + utils, + }, +}; + use alloy::primitives::{Address, Bytes, U256, address}; use chrono::{Duration, Utc}; use diesel::{SelectableHelper, insert_into}; use diesel_async::RunQueryDsl; -use crate::db::{ - self, DatabaseConnection, - models::{ - EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp, - }, - schema::{evm_basic_grant, evm_transaction_log}, -}; -use crate::evm::{ - policies::{ - CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, - VolumeRateLimit, - }, - utils, -}; - -use super::{EtherTransfer, Settings}; - const WALLET_ACCESS_ID: i32 = 1; const CHAIN_ID: u64 = 1; diff --git a/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs b/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs index f540c82..86f879c 100644 --- a/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs +++ b/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs @@ -1,16 +1,4 @@ -use std::collections::HashMap; - -use crate::db::schema::{ - evm_basic_grant, evm_token_transfer_grant, evm_token_transfer_log, - evm_token_transfer_volume_limit, -}; -use crate::evm::{ - abi::IERC20::transferCall, - policies::{ - Grant, Policy, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit, - }, - utils, -}; +use super::{DatabaseID, EvalContext, EvalViolation}; use crate::{ crypto::integrity::Integrable, db::models::{ @@ -18,20 +6,34 @@ use crate::{ NewEvmTokenTransferGrant, NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit, SqliteTimestamp, }, + db::schema::{ + evm_basic_grant, evm_token_transfer_grant, evm_token_transfer_log, + evm_token_transfer_volume_limit, + }, evm::policies::CombinedSettings, + evm::{ + abi::IERC20::transferCall, + policies::{ + Grant, Policy, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit, + }, + utils, + }, }; +use arbiter_tokens_registry::evm::nonfungible::{self, TokenInfo}; + use alloy::{ primitives::{Address, U256}, sol_types::SolCall, }; -use arbiter_tokens_registry::evm::nonfungible::{self, TokenInfo}; use chrono::{DateTime, Duration, Utc}; -use diesel::dsl::{auto_type, insert_into}; -use diesel::sqlite::Sqlite; -use diesel::{ExpressionMethods, prelude::*}; +use diesel::{ + ExpressionMethods, + dsl::{auto_type, insert_into}, + prelude::*, + sqlite::Sqlite, +}; use diesel_async::{AsyncConnection, RunQueryDsl}; - -use super::{DatabaseID, EvalContext, EvalViolation}; +use std::collections::HashMap; #[auto_type] fn grant_join() -> _ { diff --git a/server/crates/arbiter-server/src/evm/policies/token_transfers/tests.rs b/server/crates/arbiter-server/src/evm/policies/token_transfers/tests.rs index c059b0b..77dd226 100644 --- a/server/crates/arbiter-server/src/evm/policies/token_transfers/tests.rs +++ b/server/crates/arbiter-server/src/evm/policies/token_transfers/tests.rs @@ -1,25 +1,28 @@ -use alloy::primitives::{Address, Bytes, U256, address}; -use alloy::sol_types::SolCall; +use super::{Settings, TokenTransfer}; +use crate::{ + db::{ + self, DatabaseConnection, + models::{EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, SqliteTimestamp}, + schema::evm_basic_grant, + }, + evm::{ + abi::IERC20::transferCall, + policies::{ + CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, + VolumeRateLimit, + }, + utils, + }, +}; + +use alloy::{ + primitives::{Address, Bytes, U256, address}, + sol_types::SolCall, +}; use chrono::{Duration, Utc}; use diesel::{SelectableHelper, insert_into}; use diesel_async::RunQueryDsl; -use crate::db::{ - self, DatabaseConnection, - models::{EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, SqliteTimestamp}, - schema::evm_basic_grant, -}; -use crate::evm::{ - abi::IERC20::transferCall, - policies::{ - CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, - VolumeRateLimit, - }, - utils, -}; - -use super::{Settings, TokenTransfer}; - // DAI on Ethereum mainnet — present in the static token registry const CHAIN_ID: u64 = 1; const DAI: Address = address!("6B175474E89094C44Da98b954EedeAC495271d0F"); diff --git a/server/crates/arbiter-server/src/evm/safe_signer.rs b/server/crates/arbiter-server/src/evm/safe_signer.rs index e2f8100..dc9c1ed 100644 --- a/server/crates/arbiter-server/src/evm/safe_signer.rs +++ b/server/crates/arbiter-server/src/evm/safe_signer.rs @@ -1,4 +1,4 @@ -use std::sync::Mutex; +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use alloy::{ consensus::SignableTransaction, @@ -6,9 +6,9 @@ use alloy::{ primitives::{Address, B256, ChainId, Signature}, signers::{Error, Result, Signer, SignerSync, utils::secret_key_to_address}, }; -use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use async_trait::async_trait; use k256::ecdsa::{self, RecoveryId, SigningKey, signature::hazmat::PrehashSigner}; +use std::sync::Mutex; /// An Ethereum signer that stores its secp256k1 secret key inside a /// hardware-protected [`MemSafe`] cell. diff --git a/server/crates/arbiter-server/src/evm/utils.rs b/server/crates/arbiter-server/src/evm/utils.rs index 7358173..dd75434 100644 --- a/server/crates/arbiter-server/src/evm/utils.rs +++ b/server/crates/arbiter-server/src/evm/utils.rs @@ -2,20 +2,20 @@ use alloy::primitives::U256; #[derive(thiserror::Error, Debug)] #[error("Expected {expected} bytes but got {actual} bytes")] -pub struct LengthError { - pub expected: usize, - pub actual: usize, +pub(super) struct LengthError { + pub(super) expected: usize, + pub(super) actual: usize, } -pub fn u256_to_bytes(value: U256) -> [u8; 32] { +pub(super) fn u256_to_bytes(value: U256) -> [u8; 32] { value.to_le_bytes() } -pub fn bytes_to_u256(bytes: &[u8]) -> Option { +pub(super) fn bytes_to_u256(bytes: &[u8]) -> Option { let bytes: [u8; 32] = bytes.try_into().ok()?; Some(U256::from_le_bytes(bytes)) } -pub fn try_bytes_to_u256(bytes: &[u8]) -> diesel::result::QueryResult { +pub(super) fn try_bytes_to_u256(bytes: &[u8]) -> diesel::result::QueryResult { let bytes: [u8; 32] = bytes.try_into().map_err(|_| { diesel::result::Error::DeserializationError(Box::new(LengthError { expected: 32, diff --git a/server/crates/arbiter-server/src/grpc/client.rs b/server/crates/arbiter-server/src/grpc/client.rs index 52fa1ea..ac34e77 100644 --- a/server/crates/arbiter-server/src/grpc/client.rs +++ b/server/crates/arbiter-server/src/grpc/client.rs @@ -1,3 +1,7 @@ +use crate::{ + grpc::request_tracker::RequestTracker, + peers::client::{ClientConnection, session::ClientSession}, +}; use arbiter_proto::{ proto::client::{ ClientRequest, ClientResponse, client_request::Payload as ClientRequestPayload, @@ -5,15 +9,11 @@ use arbiter_proto::{ }, transport::{Receiver, Sender, grpc::GrpcBi}, }; + use kameo::actor::{ActorRef, Spawn as _}; use tonic::Status; use tracing::{info, warn}; -use crate::{ - actors::client::{ClientConnection, session::ClientSession}, - grpc::request_tracker::RequestTracker, -}; - mod auth; mod evm; mod inbound; diff --git a/server/crates/arbiter-server/src/grpc/client/auth.rs b/server/crates/arbiter-server/src/grpc/client/auth.rs index 4a7b944..11f1361 100644 --- a/server/crates/arbiter-server/src/grpc/client/auth.rs +++ b/server/crates/arbiter-server/src/grpc/client/auth.rs @@ -1,3 +1,7 @@ +use crate::{ + grpc::request_tracker::RequestTracker, + peers::client::{self, ClientConnection, auth}, +}; use arbiter_crypto::authn; use arbiter_proto::{ ClientMetadata, @@ -17,22 +21,18 @@ use arbiter_proto::{ }, transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi}, }; + use async_trait::async_trait; use tonic::Status; use tracing::warn; -use crate::{ - actors::client::{self, ClientConnection, auth}, - grpc::request_tracker::RequestTracker, -}; - -pub struct AuthTransportAdapter<'a> { +pub(super) struct AuthTransportAdapter<'a> { bi: &'a mut GrpcBi, request_tracker: &'a mut RequestTracker, } impl<'a> AuthTransportAdapter<'a> { - pub fn new( + pub(super) fn new( bi: &'a mut GrpcBi, request_tracker: &'a mut RequestTracker, ) -> Self { @@ -44,10 +44,14 @@ impl<'a> AuthTransportAdapter<'a> { fn response_to_proto(response: auth::Outbound) -> AuthResponsePayload { match response { - auth::Outbound::AuthChallenge { pubkey, nonce } => { + auth::Outbound::AuthChallenge { challenge } => { AuthResponsePayload::Challenge(ProtoAuthChallenge { - pubkey: pubkey.to_bytes(), - nonce, + timestamp_nanos: challenge + .timestamp + .timestamp_nanos_opt() + .expect("timestamp within range") + as u64, + random: challenge.nonce.to_vec(), }) } auth::Outbound::AuthSuccess => { @@ -193,7 +197,7 @@ fn client_metadata_from_proto(metadata: ProtoClientInfo) -> ClientMetadata { } } -pub async fn start( +pub(super) async fn start( conn: &mut ClientConnection, bi: &mut GrpcBi, request_tracker: &mut RequestTracker, diff --git a/server/crates/arbiter-server/src/grpc/client/evm.rs b/server/crates/arbiter-server/src/grpc/client/evm.rs index 5b5ba2e..7cbd02b 100644 --- a/server/crates/arbiter-server/src/grpc/client/evm.rs +++ b/server/crates/arbiter-server/src/grpc/client/evm.rs @@ -1,3 +1,10 @@ +use crate::{ + grpc::{ + Convert, TryConvert, + common::inbound::{RawEvmAddress, RawEvmTransaction}, + }, + peers::client::session::{ClientSession, HandleSignTransaction, SignTransactionRpcError}, +}; use arbiter_proto::proto::{ client::{ client_response::Payload as ClientResponsePayload, @@ -11,18 +18,11 @@ use arbiter_proto::proto::{ evm_sign_transaction_response::Result as EvmSignTransactionResult, }, }; + use kameo::actor::ActorRef; use tonic::Status; use tracing::warn; -use crate::{ - actors::client::session::{ClientSession, HandleSignTransaction, SignTransactionRpcError}, - grpc::{ - Convert, TryConvert, - common::inbound::{RawEvmAddress, RawEvmTransaction}, - }, -}; - fn wrap_response(payload: EvmResponsePayload) -> ClientResponsePayload { ClientResponsePayload::Evm(proto_evm::Response { payload: Some(payload), diff --git a/server/crates/arbiter-server/src/grpc/client/vault.rs b/server/crates/arbiter-server/src/grpc/client/vault.rs index b5e98e7..3416e33 100644 --- a/server/crates/arbiter-server/src/grpc/client/vault.rs +++ b/server/crates/arbiter-server/src/grpc/client/vault.rs @@ -1,3 +1,7 @@ +use crate::{ + actors::vault::VaultState, + peers::client::session::{ClientSession, Error, HandleQueryVaultState}, +}; use arbiter_proto::proto::{ client::{ client_response::Payload as ClientResponsePayload, @@ -8,15 +12,11 @@ use arbiter_proto::proto::{ }, shared::VaultState as ProtoVaultState, }; + use kameo::{actor::ActorRef, error::SendError}; use tonic::Status; use tracing::warn; -use crate::actors::{ - client::session::{ClientSession, Error, HandleQueryVaultState}, - keyholder::KeyHolderState, -}; - pub(super) async fn dispatch( actor: &ActorRef, req: proto_vault::Request, @@ -30,9 +30,9 @@ pub(super) async fn dispatch( match payload { VaultRequestPayload::QueryState(_) => { let state = match actor.ask(HandleQueryVaultState {}).await { - Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, - Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed, - Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed, + Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, + Ok(VaultState::Sealed) => ProtoVaultState::Sealed, + Ok(VaultState::Unsealed) => ProtoVaultState::Unsealed, Err(SendError::HandlerError(Error::Internal)) => ProtoVaultState::Error, Err(err) => { warn!(error = ?err, "Failed to query vault state"); diff --git a/server/crates/arbiter-server/src/grpc/common.rs b/server/crates/arbiter-server/src/grpc/common.rs index 5756441..b638928 100644 --- a/server/crates/arbiter-server/src/grpc/common.rs +++ b/server/crates/arbiter-server/src/grpc/common.rs @@ -1,2 +1,2 @@ -pub mod inbound; -pub mod outbound; +pub(super) mod inbound; +pub(super) mod outbound; diff --git a/server/crates/arbiter-server/src/grpc/common/inbound.rs b/server/crates/arbiter-server/src/grpc/common/inbound.rs index d9e4d9a..5c71aed 100644 --- a/server/crates/arbiter-server/src/grpc/common/inbound.rs +++ b/server/crates/arbiter-server/src/grpc/common/inbound.rs @@ -1,8 +1,8 @@ -use alloy::{consensus::TxEip1559, primitives::Address, rlp::Decodable as _}; - use crate::grpc::TryConvert; -pub struct RawEvmAddress(pub Vec); +use alloy::{consensus::TxEip1559, primitives::Address, rlp::Decodable as _}; + +pub(in crate::grpc) struct RawEvmAddress(pub(in crate::grpc) Vec); impl TryConvert for RawEvmAddress { type Output = Address; @@ -21,7 +21,7 @@ impl TryConvert for RawEvmAddress { } } -pub struct RawEvmTransaction(pub Vec); +pub(in crate::grpc) struct RawEvmTransaction(pub(in crate::grpc) Vec); impl TryConvert for RawEvmTransaction { type Output = TxEip1559; diff --git a/server/crates/arbiter-server/src/grpc/common/outbound.rs b/server/crates/arbiter-server/src/grpc/common/outbound.rs index c2a2045..8b8447c 100644 --- a/server/crates/arbiter-server/src/grpc/common/outbound.rs +++ b/server/crates/arbiter-server/src/grpc/common/outbound.rs @@ -1,4 +1,10 @@ -use alloy::primitives::U256; +use crate::{ + evm::{ + PolicyError, VetError, + policies::{EvalViolation, SpecificMeaning}, + }, + grpc::Convert, +}; use arbiter_proto::proto::{ evm::{ EvmError as ProtoEvmError, @@ -14,13 +20,7 @@ use arbiter_proto::proto::{ }, }; -use crate::{ - evm::{ - PolicyError, VetError, - policies::{EvalViolation, SpecificMeaning}, - }, - grpc::Convert, -}; +use alloy::primitives::U256; fn u256_to_proto_bytes(value: U256) -> Vec { value.to_be_bytes::<32>().to_vec() diff --git a/server/crates/arbiter-server/src/grpc/mod.rs b/server/crates/arbiter-server/src/grpc/mod.rs index 7181594..4e0ed8f 100644 --- a/server/crates/arbiter-server/src/grpc/mod.rs +++ b/server/crates/arbiter-server/src/grpc/mod.rs @@ -1,3 +1,4 @@ +use crate::peers::{client::ClientConnection, user_agent::UserAgentConnection}; use arbiter_proto::{ proto::{ client::{ClientRequest, ClientResponse}, @@ -5,15 +6,11 @@ use arbiter_proto::{ }, transport::grpc::GrpcBi, }; + use tokio_stream::wrappers::ReceiverStream; use tonic::{Request, Response, Status, async_trait}; use tracing::info; -use crate::{ - actors::{client::ClientConnection, user_agent::UserAgentConnection}, - grpc::user_agent::start, -}; - mod request_tracker; pub mod client; @@ -63,7 +60,7 @@ impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Ser let (bi, rx) = GrpcBi::from_bi_stream(req_stream); - tokio::spawn(start( + tokio::spawn(user_agent::start( UserAgentConnection { db: self.context.db.clone(), actors: self.context.actors.clone(), diff --git a/server/crates/arbiter-server/src/grpc/request_tracker.rs b/server/crates/arbiter-server/src/grpc/request_tracker.rs index 7ab6254..715f838 100644 --- a/server/crates/arbiter-server/src/grpc/request_tracker.rs +++ b/server/crates/arbiter-server/src/grpc/request_tracker.rs @@ -1,12 +1,12 @@ use tonic::Status; #[derive(Default)] -pub struct RequestTracker { +pub(super) struct RequestTracker { next_request_id: i32, } impl RequestTracker { - pub fn request(&mut self, id: i32) -> Result { + pub(super) fn request(&mut self, id: i32) -> Result { if id < self.next_request_id { return Err(Status::invalid_argument("Duplicate request id")); } @@ -20,7 +20,7 @@ impl RequestTracker { // This is used to set the response id for auth responses, which need to match the request id of the auth challenge request. // -1 offset is needed because request() increments the next_request_id after returning the current request id. - pub fn current_request_id(&self) -> i32 { + pub(super) fn current_request_id(&self) -> i32 { self.next_request_id - 1 } } diff --git a/server/crates/arbiter-server/src/grpc/user_agent.rs b/server/crates/arbiter-server/src/grpc/user_agent.rs index bc00687..fc64017 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent.rs @@ -1,5 +1,7 @@ -use tokio::sync::mpsc; - +use crate::{ + grpc::request_tracker::RequestTracker, + peers::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession}, +}; use arbiter_proto::{ proto::user_agent::{ UserAgentRequest, UserAgentResponse, @@ -8,22 +10,20 @@ use arbiter_proto::{ }, transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi}, }; + use async_trait::async_trait; -use kameo::actor::{ActorRef, Spawn as _}; +use kameo::actor::ActorRef; +use tokio::sync::mpsc; use tonic::Status; use tracing::{error, info, warn}; -use crate::{ - actors::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession}, - grpc::request_tracker::RequestTracker, -}; - mod auth; mod evm; mod inbound; mod outbound; mod sdk_client; mod vault; +mod vault_gate; pub struct OutOfBandAdapter(mpsc::Sender); @@ -124,21 +124,22 @@ pub async fn start( ) { let mut request_tracker = RequestTracker::default(); - let pubkey = match auth::start(&mut conn, &mut bi, &mut request_tracker).await { - Ok(pubkey) => pubkey, - Err(e) => { - warn!(error = ?e, "Authentication failed"); - return; - } - }; - let (oob_sender, oob_receiver) = mpsc::channel(16); let oob_adapter = OutOfBandAdapter(oob_sender); - let actor = UserAgentSession::spawn(UserAgentSession::new(conn, Box::new(oob_adapter))); - let actor_for_cleanup = actor.clone(); + let actor = { + let transport = auth::AuthTransportAdapter::new(&mut bi, &mut request_tracker); + match crate::peers::user_agent::start(&mut conn, transport, Box::new(oob_adapter)).await { + Ok(actor) => actor, + Err(e) => { + warn!(error = ?e, "User agent connection failed"); + return; + } + } + }; - info!(?pubkey, "User authenticated successfully"); - dispatch_loop(bi, actor, oob_receiver, request_tracker).await; - actor_for_cleanup.kill(); + info!("User agent session established"); + + dispatch_loop(bi, actor.clone(), oob_receiver, request_tracker).await; + actor.kill(); } diff --git a/server/crates/arbiter-server/src/grpc/user_agent/auth.rs b/server/crates/arbiter-server/src/grpc/user_agent/auth.rs index e2625e0..10a20a0 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent/auth.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent/auth.rs @@ -1,3 +1,4 @@ +use crate::{grpc::request_tracker::RequestTracker, peers::user_agent::auth}; use arbiter_crypto::authn; use arbiter_proto::{ proto::user_agent::{ @@ -13,22 +14,18 @@ use arbiter_proto::{ }, transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi}, }; + use async_trait::async_trait; use tonic::Status; use tracing::warn; -use crate::{ - actors::user_agent::{UserAgentConnection, auth}, - grpc::request_tracker::RequestTracker, -}; - -pub struct AuthTransportAdapter<'a> { - bi: &'a mut GrpcBi, - request_tracker: &'a mut RequestTracker, +pub(super) struct AuthTransportAdapter<'a> { + pub(super) bi: &'a mut GrpcBi, + pub(super) request_tracker: &'a mut RequestTracker, } impl<'a> AuthTransportAdapter<'a> { - pub fn new( + pub(super) fn new( bi: &'a mut GrpcBi, request_tracker: &'a mut RequestTracker, ) -> Self { @@ -38,19 +35,35 @@ impl<'a> AuthTransportAdapter<'a> { } } - async fn send_user_agent_response( + pub(super) fn bi_mut(&mut self) -> &mut GrpcBi { + self.bi + } + + pub(super) fn tracker_mut(&mut self) -> &mut RequestTracker { + self.request_tracker + } + + pub(super) async fn send_response_payload( &mut self, - payload: AuthResponsePayload, + payload: UserAgentResponsePayload, ) -> Result<(), TransportError> { self.bi .send(Ok(UserAgentResponse { id: Some(self.request_tracker.current_request_id()), - payload: Some(UserAgentResponsePayload::Auth(proto_auth::Response { - payload: Some(payload), - })), + payload: Some(payload), })) .await } + + async fn send_user_agent_response( + &mut self, + payload: AuthResponsePayload, + ) -> Result<(), TransportError> { + self.send_response_payload(UserAgentResponsePayload::Auth(proto_auth::Response { + payload: Some(payload), + })) + .await + } } #[async_trait] @@ -61,8 +74,15 @@ impl Sender> for AuthTransportAdapter<'_> { ) -> Result<(), TransportError> { use auth::{Error, Outbound}; let payload = match item { - Ok(Outbound::AuthChallenge { nonce }) => { - AuthResponsePayload::Challenge(ProtoAuthChallenge { nonce }) + Ok(Outbound::AuthChallenge { challenge }) => { + AuthResponsePayload::Challenge(ProtoAuthChallenge { + timestamp_nanos: challenge + .timestamp + .timestamp_nanos_opt() + .expect("timestamp within range") + as u64, + random: challenge.nonce.to_vec(), + }) } Ok(Outbound::AuthSuccess) => { AuthResponsePayload::Result(ProtoAuthResult::Success.into()) @@ -140,7 +160,6 @@ impl Receiver for AuthTransportAdapter<'_> { AuthRequestPayload::ChallengeRequest(ProtoAuthChallengeRequest { pubkey, bootstrap_token, - key_type: _, }) => { let Ok(pubkey) = authn::PublicKey::try_from(pubkey.as_slice()) else { warn!( @@ -163,12 +182,3 @@ impl Receiver for AuthTransportAdapter<'_> { } impl Bi> for AuthTransportAdapter<'_> {} - -pub async fn start( - conn: &mut UserAgentConnection, - bi: &mut GrpcBi, - request_tracker: &mut RequestTracker, -) -> Result { - let transport = AuthTransportAdapter::new(bi, request_tracker); - auth::authenticate(conn, transport).await -} diff --git a/server/crates/arbiter-server/src/grpc/user_agent/evm.rs b/server/crates/arbiter-server/src/grpc/user_agent/evm.rs index 28725c2..b2dbe96 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent/evm.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent/evm.rs @@ -1,3 +1,17 @@ +use crate::{ + grpc::{ + Convert, TryConvert, + common::inbound::{RawEvmAddress, RawEvmTransaction}, + }, + peers::user_agent::{ + UserAgentSession, + session::handlers::{ + GrantMutationError, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate, + HandleGrantDelete, HandleGrantList, HandleSignTransaction, + SignTransactionError as SessionSignTransactionError, + }, + }, +}; use arbiter_proto::proto::{ evm::{ EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse, @@ -18,25 +32,11 @@ use arbiter_proto::proto::{ user_agent_response::Payload as UserAgentResponsePayload, }, }; + use kameo::actor::ActorRef; use tonic::Status; use tracing::warn; -use crate::{ - actors::user_agent::{ - UserAgentSession, - session::connection::{ - GrantMutationError, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate, - HandleGrantDelete, HandleGrantList, HandleSignTransaction, - SignTransactionError as SessionSignTransactionError, - }, - }, - grpc::{ - Convert, TryConvert, - common::inbound::{RawEvmAddress, RawEvmTransaction}, - }, -}; - fn wrap_evm_response(payload: EvmResponsePayload) -> UserAgentResponsePayload { UserAgentResponsePayload::Evm(proto_evm::Response { payload: Some(payload), diff --git a/server/crates/arbiter-server/src/grpc/user_agent/inbound.rs b/server/crates/arbiter-server/src/grpc/user_agent/inbound.rs index 6cfb2e5..c42f661 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent/inbound.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent/inbound.rs @@ -1,26 +1,26 @@ -use alloy::primitives::{Address, U256}; -use arbiter_proto::proto::evm::{ - EtherTransferSettings as ProtoEtherTransferSettings, SharedSettings as ProtoSharedSettings, - SpecificGrant as ProtoSpecificGrant, TokenTransferSettings as ProtoTokenTransferSettings, - TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit, - specific_grant::Grant as ProtoSpecificGrantType, -}; -use arbiter_proto::proto::user_agent::sdk_client::{ - WalletAccess, WalletAccessEntry as SdkClientWalletAccess, -}; -use chrono::{DateTime, TimeZone, Utc}; -use prost_types::Timestamp as ProtoTimestamp; -use tonic::Status; - -use crate::db::models::{CoreEvmWalletAccess, NewEvmWalletAccess}; -use crate::grpc::Convert; use crate::{ + db::models::{CoreEvmWalletAccess, NewEvmWalletAccess}, evm::policies::{ SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, ether_transfer, token_transfers, }, + grpc::Convert, grpc::TryConvert, }; +use arbiter_proto::{ + proto::evm::{ + EtherTransferSettings as ProtoEtherTransferSettings, SharedSettings as ProtoSharedSettings, + SpecificGrant as ProtoSpecificGrant, TokenTransferSettings as ProtoTokenTransferSettings, + TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit, + specific_grant::Grant as ProtoSpecificGrantType, + }, + proto::user_agent::sdk_client::{WalletAccess, WalletAccessEntry as SdkClientWalletAccess}, +}; + +use alloy::primitives::{Address, U256}; +use chrono::{DateTime, TimeZone, Utc}; +use prost_types::Timestamp as ProtoTimestamp; +use tonic::Status; fn address_from_bytes(bytes: Vec) -> Result { if bytes.len() != 20 { diff --git a/server/crates/arbiter-server/src/grpc/user_agent/outbound.rs b/server/crates/arbiter-server/src/grpc/user_agent/outbound.rs index 805386e..5b778b4 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent/outbound.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent/outbound.rs @@ -1,3 +1,8 @@ +use crate::{ + db::models::EvmWalletAccess, + evm::policies::{SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit}, + grpc::Convert, +}; use arbiter_proto::proto::{ evm::{ EtherTransferSettings as ProtoEtherTransferSettings, SharedSettings as ProtoSharedSettings, @@ -7,15 +12,10 @@ use arbiter_proto::proto::{ }, user_agent::sdk_client::{WalletAccess, WalletAccessEntry as ProtoSdkClientWalletAccess}, }; + use chrono::{DateTime, Utc}; use prost_types::Timestamp as ProtoTimestamp; -use crate::{ - db::models::EvmWalletAccess, - evm::policies::{SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit}, - grpc::Convert, -}; - impl Convert for DateTime { type Output = ProtoTimestamp; diff --git a/server/crates/arbiter-server/src/grpc/user_agent/sdk_client.rs b/server/crates/arbiter-server/src/grpc/user_agent/sdk_client.rs index b0d832f..4420439 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent/sdk_client.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent/sdk_client.rs @@ -1,3 +1,14 @@ +use crate::{ + db::models::NewEvmWalletAccess, + grpc::Convert, + peers::user_agent::{ + OutOfBand, UserAgentSession, + session::handlers::{ + HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove, + HandleRevokeEvmWalletAccess, HandleSdkClientList, + }, + }, +}; use arbiter_crypto::authn; use arbiter_proto::proto::{ shared::ClientInfo as ProtoClientMetadata, @@ -16,22 +27,11 @@ use arbiter_proto::proto::{ user_agent_response::Payload as UserAgentResponsePayload, }, }; + use kameo::actor::ActorRef; use tonic::Status; use tracing::{info, warn}; -use crate::{ - actors::user_agent::{ - OutOfBand, UserAgentSession, - session::connection::{ - HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove, - HandleRevokeEvmWalletAccess, HandleSdkClientList, - }, - }, - db::models::NewEvmWalletAccess, - grpc::Convert, -}; - fn wrap_sdk_client_response(payload: SdkClientResponsePayload) -> UserAgentResponsePayload { UserAgentResponsePayload::SdkClient(proto_sdk_client::Response { payload: Some(payload), diff --git a/server/crates/arbiter-server/src/grpc/user_agent/vault.rs b/server/crates/arbiter-server/src/grpc/user_agent/vault.rs index 8a2940f..6ccb8bb 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent/vault.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent/vault.rs @@ -1,54 +1,28 @@ -use arbiter_proto::proto::shared::VaultState as ProtoVaultState; -use arbiter_proto::proto::user_agent::{ - user_agent_response::Payload as UserAgentResponsePayload, - vault::{ - self as proto_vault, - bootstrap::{ - self as proto_bootstrap, BootstrapEncryptedKey as ProtoBootstrapEncryptedKey, - BootstrapResult as ProtoBootstrapResult, - }, - request::Payload as VaultRequestPayload, - response::Payload as VaultResponsePayload, - unseal::{ - self as proto_unseal, UnsealEncryptedKey as ProtoUnsealEncryptedKey, - UnsealResult as ProtoUnsealResult, UnsealStart, - request::Payload as UnsealRequestPayload, response::Payload as UnsealResponsePayload, +use crate::{ + actors::vault::VaultState, + peers::user_agent::{UserAgentSession, session::handlers::HandleQueryVaultState}, +}; +use arbiter_proto::{ + proto::shared::VaultState as ProtoVaultState, + proto::user_agent::{ + user_agent_response::Payload as UserAgentResponsePayload, + vault::{ + self as proto_vault, request::Payload as VaultRequestPayload, + response::Payload as VaultResponsePayload, }, }, }; -use kameo::{actor::ActorRef, error::SendError}; + +use kameo::actor::ActorRef; use tonic::Status; use tracing::warn; -use crate::actors::{ - keyholder::KeyHolderState, - user_agent::{ - UserAgentSession, - session::connection::{ - BootstrapError, HandleBootstrapEncryptedKey, HandleQueryVaultState, - HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError, - }, - }, -}; - fn wrap_vault_response(payload: VaultResponsePayload) -> UserAgentResponsePayload { UserAgentResponsePayload::Vault(proto_vault::Response { payload: Some(payload), }) } -fn wrap_unseal_response(payload: UnsealResponsePayload) -> UserAgentResponsePayload { - wrap_vault_response(VaultResponsePayload::Unseal(proto_unseal::Response { - payload: Some(payload), - })) -} - -fn wrap_bootstrap_response(result: ProtoBootstrapResult) -> UserAgentResponsePayload { - wrap_vault_response(VaultResponsePayload::Bootstrap(proto_bootstrap::Response { - result: result.into(), - })) -} - pub(super) async fn dispatch( actor: &ActorRef, req: proto_vault::Request, @@ -59,116 +33,21 @@ pub(super) async fn dispatch( match payload { VaultRequestPayload::QueryState(_) => handle_query_vault_state(actor).await, - VaultRequestPayload::Unseal(req) => dispatch_unseal_request(actor, req).await, - VaultRequestPayload::Bootstrap(req) => handle_bootstrap_request(actor, req).await, + VaultRequestPayload::Unseal(_) | VaultRequestPayload::Bootstrap(_) => { + Err(Status::permission_denied( + "Vault is already unsealed; unseal/bootstrap not permitted in session", + )) + } } } -async fn dispatch_unseal_request( - actor: &ActorRef, - req: proto_unseal::Request, -) -> Result, Status> { - let Some(payload) = req.payload else { - return Err(Status::invalid_argument("Missing unseal request payload")); - }; - - match payload { - UnsealRequestPayload::Start(req) => handle_unseal_start(actor, req).await, - UnsealRequestPayload::EncryptedKey(req) => handle_unseal_encrypted_key(actor, req).await, - } -} - -async fn handle_unseal_start( - actor: &ActorRef, - req: UnsealStart, -) -> Result, Status> { - let client_pubkey = <[u8; 32]>::try_from(req.client_pubkey) - .map(x25519_dalek::PublicKey::from) - .map_err(|_| Status::invalid_argument("Invalid X25519 public key"))?; - - let response = actor - .ask(HandleUnsealRequest { client_pubkey }) - .await - .map_err(|err| { - warn!(error = ?err, "Failed to handle unseal start request"); - Status::internal("Failed to start unseal flow") - })?; - - Ok(Some(wrap_unseal_response(UnsealResponsePayload::Start( - proto_unseal::UnsealStartResponse { - server_pubkey: response.server_pubkey.as_bytes().to_vec(), - }, - )))) -} - -async fn handle_unseal_encrypted_key( - actor: &ActorRef, - req: ProtoUnsealEncryptedKey, -) -> Result, Status> { - let result = match actor - .ask(HandleUnsealEncryptedKey { - nonce: req.nonce, - ciphertext: req.ciphertext, - associated_data: req.associated_data, - }) - .await - { - Ok(()) => ProtoUnsealResult::Success, - Err(SendError::HandlerError(UnsealError::InvalidKey)) => ProtoUnsealResult::InvalidKey, - Err(err) => { - warn!(error = ?err, "Failed to handle unseal request"); - return Err(Status::internal("Failed to unseal vault")); - } - }; - Ok(Some(wrap_unseal_response(UnsealResponsePayload::Result( - result.into(), - )))) -} - -async fn handle_bootstrap_request( - actor: &ActorRef, - req: proto_bootstrap::Request, -) -> Result, Status> { - let encrypted_key = req - .encrypted_key - .ok_or_else(|| Status::invalid_argument("Missing bootstrap encrypted key"))?; - handle_bootstrap_encrypted_key(actor, encrypted_key).await -} - -async fn handle_bootstrap_encrypted_key( - actor: &ActorRef, - req: ProtoBootstrapEncryptedKey, -) -> Result, Status> { - let result = match actor - .ask(HandleBootstrapEncryptedKey { - nonce: req.nonce, - ciphertext: req.ciphertext, - associated_data: req.associated_data, - }) - .await - { - Ok(()) => ProtoBootstrapResult::Success, - Err(SendError::HandlerError(BootstrapError::InvalidKey)) => { - ProtoBootstrapResult::InvalidKey - } - Err(SendError::HandlerError(BootstrapError::AlreadyBootstrapped)) => { - ProtoBootstrapResult::AlreadyBootstrapped - } - Err(err) => { - warn!(error = ?err, "Failed to handle bootstrap request"); - return Err(Status::internal("Failed to bootstrap vault")); - } - }; - Ok(Some(wrap_bootstrap_response(result))) -} - async fn handle_query_vault_state( actor: &ActorRef, ) -> Result, Status> { let state = match actor.ask(HandleQueryVaultState {}).await { - Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, - Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed, - Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed, + Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, + Ok(VaultState::Sealed) => ProtoVaultState::Sealed, + Ok(VaultState::Unsealed) => ProtoVaultState::Unsealed, Err(err) => { warn!(error = ?err, "Failed to query vault state"); ProtoVaultState::Error diff --git a/server/crates/arbiter-server/src/grpc/user_agent/vault_gate.rs b/server/crates/arbiter-server/src/grpc/user_agent/vault_gate.rs new file mode 100644 index 0000000..bffa5cc --- /dev/null +++ b/server/crates/arbiter-server/src/grpc/user_agent/vault_gate.rs @@ -0,0 +1,79 @@ +use super::auth::AuthTransportAdapter; +use crate::{ + grpc::TryConvert, + peers::user_agent::vault_gate::{self as vault_gate}, +}; +use arbiter_proto::transport::{Bi, Error as TransportError, Receiver, Sender}; + +use async_trait::async_trait; +use tonic::Status; +use tracing::warn; + +mod inbound; +mod outbound; + +#[async_trait] +impl Receiver for AuthTransportAdapter<'_> { + async fn recv(&mut self) -> Option { + let request = match self.bi_mut().recv().await? { + Ok(request) => request, + Err(error) => { + warn!( + ?error, + "Failed to receive user agent request during vault gate" + ); + return None; + } + }; + + if let Err(err) = self.tracker_mut().request(request.id) { + let _ = self.bi_mut().send(Err(err)).await; + return None; + } + + let Some(payload) = request.payload else { + let _ = self + .bi_mut() + .send(Err(Status::invalid_argument("Missing request payload"))) + .await; + return None; + }; + + match payload.try_convert() { + Ok(inbound) => Some(inbound), + Err(status) => { + let _ = self.bi_mut().send(Err(status)).await; + None + } + } + } +} + +#[async_trait] +impl Sender> for AuthTransportAdapter<'_> { + async fn send( + &mut self, + item: Result, + ) -> Result<(), TransportError> { + let outbound = match item { + Ok(outbound) => outbound, + Err(err) => { + warn!(?err, "vault gate produced transport-level error"); + return self + .bi_mut() + .send(Err(Status::internal(err.to_string()))) + .await; + } + }; + + match outbound.try_convert() { + Ok(payload) => self.send_response_payload(payload).await, + Err(status) => self.bi_mut().send(Err(status)).await, + } + } +} + +impl Bi> + for AuthTransportAdapter<'_> +{ +} diff --git a/server/crates/arbiter-server/src/grpc/user_agent/vault_gate/inbound.rs b/server/crates/arbiter-server/src/grpc/user_agent/vault_gate/inbound.rs new file mode 100644 index 0000000..8dbb671 --- /dev/null +++ b/server/crates/arbiter-server/src/grpc/user_agent/vault_gate/inbound.rs @@ -0,0 +1,129 @@ +use crate::{ + grpc::{Convert, TryConvert}, + peers::user_agent::vault_gate::{ + self as vault_gate, HandleBootstrapEncryptedKey, HandleHandshake, HandleUnsealEncryptedKey, + }, +}; +use arbiter_proto::proto::user_agent::{ + user_agent_request::Payload as UserAgentRequestPayload, + vault::{ + self as proto_vault, + bootstrap::{self as proto_bootstrap}, + request::Payload as VaultRequestPayload, + unseal::{self as proto_unseal, request::Payload as UnsealRequestPayload}, + }, +}; + +use tonic::Status; + +impl TryConvert for UserAgentRequestPayload { + type Output = vault_gate::Inbound; + type Error = Status; + + fn try_convert(self) -> Result { + match self { + UserAgentRequestPayload::Vault(req) => req.try_convert(), + _ => Err(Status::permission_denied( + "Only vault operations are permitted before unsealing", + )), + } + } +} + +impl TryConvert for proto_vault::Request { + type Output = vault_gate::Inbound; + type Error = Status; + + fn try_convert(self) -> Result { + self.payload + .ok_or_else(|| Status::invalid_argument("Missing vault request payload"))? + .try_convert() + } +} + +impl TryConvert for VaultRequestPayload { + type Output = vault_gate::Inbound; + type Error = Status; + + fn try_convert(self) -> Result { + match self { + VaultRequestPayload::QueryState(_) => Ok(vault_gate::Inbound::HandleVaultState), + VaultRequestPayload::Unseal(req) => req.try_convert(), + VaultRequestPayload::Bootstrap(req) => req.try_convert(), + } + } +} + +impl TryConvert for proto_unseal::Request { + type Output = vault_gate::Inbound; + type Error = Status; + + fn try_convert(self) -> Result { + self.payload + .ok_or_else(|| Status::invalid_argument("Missing unseal request payload"))? + .try_convert() + } +} + +impl TryConvert for UnsealRequestPayload { + type Output = vault_gate::Inbound; + type Error = Status; + + fn try_convert(self) -> Result { + match self { + UnsealRequestPayload::Start(start) => start.try_convert(), + UnsealRequestPayload::EncryptedKey(key) => Ok(key.convert()), + } + } +} + +impl TryConvert for proto_unseal::UnsealStart { + type Output = vault_gate::Inbound; + type Error = Status; + + fn try_convert(self) -> Result { + let bytes = <[u8; 32]>::try_from(self.client_pubkey) + .map_err(|_| Status::invalid_argument("Invalid X25519 public key"))?; + Ok(vault_gate::Inbound::HandleHandshake(HandleHandshake { + client_pubkey: x25519_dalek::PublicKey::from(bytes), + })) + } +} + +impl Convert for proto_unseal::UnsealEncryptedKey { + type Output = vault_gate::Inbound; + + fn convert(self) -> vault_gate::Inbound { + vault_gate::Inbound::HandleUnsealEncryptedKey(HandleUnsealEncryptedKey { + nonce: self.nonce, + ciphertext: self.ciphertext, + associated_data: self.associated_data, + }) + } +} + +impl TryConvert for proto_bootstrap::Request { + type Output = vault_gate::Inbound; + type Error = Status; + + fn try_convert(self) -> Result { + self.encrypted_key + .ok_or_else(|| Status::invalid_argument("Missing bootstrap encrypted key"))? + .try_convert() + } +} + +impl TryConvert for proto_bootstrap::BootstrapEncryptedKey { + type Output = vault_gate::Inbound; + type Error = Status; + + fn try_convert(self) -> Result { + Ok(vault_gate::Inbound::HandleBootstrapEncryptedKey( + HandleBootstrapEncryptedKey { + nonce: self.nonce, + ciphertext: self.ciphertext, + associated_data: self.associated_data, + }, + )) + } +} diff --git a/server/crates/arbiter-server/src/grpc/user_agent/vault_gate/outbound.rs b/server/crates/arbiter-server/src/grpc/user_agent/vault_gate/outbound.rs new file mode 100644 index 0000000..14ff947 --- /dev/null +++ b/server/crates/arbiter-server/src/grpc/user_agent/vault_gate/outbound.rs @@ -0,0 +1,115 @@ +use crate::{ + actors::vault::VaultState, + grpc::{Convert, TryConvert}, + peers::user_agent::vault_gate::{self as vault_gate}, +}; +use arbiter_proto::proto::{ + shared::VaultState as ProtoVaultState, + user_agent::{ + user_agent_response::Payload as UserAgentResponsePayload, + vault::{ + self as proto_vault, + bootstrap::{self as proto_bootstrap, BootstrapResult as ProtoBootstrapResult}, + response::Payload as VaultResponsePayload, + unseal::{ + self as proto_unseal, UnsealResult as ProtoUnsealResult, + response::Payload as UnsealResponsePayload, + }, + }, + }, +}; + +use tonic::Status; +use tracing::warn; + +fn wrap_vault_response(payload: VaultResponsePayload) -> UserAgentResponsePayload { + UserAgentResponsePayload::Vault(proto_vault::Response { + payload: Some(payload), + }) +} + +fn wrap_unseal_response(payload: UnsealResponsePayload) -> UserAgentResponsePayload { + wrap_vault_response(VaultResponsePayload::Unseal(proto_unseal::Response { + payload: Some(payload), + })) +} + +fn wrap_bootstrap_response(result: ProtoBootstrapResult) -> UserAgentResponsePayload { + wrap_vault_response(VaultResponsePayload::Bootstrap(proto_bootstrap::Response { + result: result.into(), + })) +} + +impl Convert for VaultState { + type Output = UserAgentResponsePayload; + + fn convert(self) -> UserAgentResponsePayload { + let proto_state = match self { + VaultState::Unbootstrapped => ProtoVaultState::Unbootstrapped, + VaultState::Sealed => ProtoVaultState::Sealed, + VaultState::Unsealed => ProtoVaultState::Unsealed, + }; + wrap_vault_response(VaultResponsePayload::State(proto_state.into())) + } +} + +impl Convert for vault_gate::HandshakeResponse { + type Output = UserAgentResponsePayload; + + fn convert(self) -> UserAgentResponsePayload { + wrap_unseal_response(UnsealResponsePayload::Start( + proto_unseal::UnsealStartResponse { + server_pubkey: self.server_pubkey.as_bytes().to_vec(), + }, + )) + } +} + +impl TryConvert for vault_gate::Outbound { + type Output = UserAgentResponsePayload; + type Error = Status; + + fn try_convert(self) -> Result { + match self { + vault_gate::Outbound::HandleVaultState(result) => result + .map_err(|err| { + warn!(?err, "vault state query failed"); + Status::internal("Failed to query vault state") + }) + .map(VaultState::convert), + vault_gate::Outbound::HandleHandshake(result) => result + .map_err(|err| { + warn!(?err, "handshake failed"); + Status::internal("Failed to start unseal flow") + }) + .map(vault_gate::HandshakeResponse::convert), + vault_gate::Outbound::HandleUnsealEncryptedKey(result) => { + let proto_result = match result { + Ok(()) => ProtoUnsealResult::Success, + Err(vault_gate::Error::InvalidKey) => ProtoUnsealResult::InvalidKey, + Err(err) => { + warn!(?err, "unseal failed"); + return Err(Status::internal("Failed to unseal vault")); + } + }; + Ok(wrap_unseal_response(UnsealResponsePayload::Result( + proto_result.into(), + ))) + } + vault_gate::Outbound::HandleBootstrapEncryptedKey(result) => { + let proto_result = match result { + Ok(()) => ProtoBootstrapResult::Success, + Err(vault_gate::Error::InvalidKey) => ProtoBootstrapResult::InvalidKey, + Err(vault_gate::Error::AlreadyBootstrapped) => { + ProtoBootstrapResult::AlreadyBootstrapped + } + Err(err) => { + warn!(?err, "bootstrap failed"); + return Err(Status::internal("Failed to bootstrap vault")); + } + }; + Ok(wrap_bootstrap_response(proto_result)) + } + } + } +} diff --git a/server/crates/arbiter-server/src/lib.rs b/server/crates/arbiter-server/src/lib.rs index 8bb07c8..466e639 100644 --- a/server/crates/arbiter-server/src/lib.rs +++ b/server/crates/arbiter-server/src/lib.rs @@ -7,6 +7,7 @@ pub mod crypto; pub mod db; pub mod evm; pub mod grpc; +pub mod peers; pub mod utils; pub struct Server { diff --git a/server/crates/arbiter-server/src/main.rs b/server/crates/arbiter-server/src/main.rs index a63644b..0c2edde 100644 --- a/server/crates/arbiter-server/src/main.rs +++ b/server/crates/arbiter-server/src/main.rs @@ -1,9 +1,9 @@ -use std::net::SocketAddr; - -use anyhow::anyhow; use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl}; use arbiter_server::{Server, actors::bootstrap::GetToken, context::ServerContext, db}; + +use anyhow::anyhow; use rustls::crypto::aws_lc_rs; +use std::net::SocketAddr; use tonic::transport::{Identity, ServerTlsConfig}; use tracing::info; diff --git a/server/crates/arbiter-server/src/actors/client/auth.rs b/server/crates/arbiter-server/src/peers/client/auth.rs similarity index 75% rename from server/crates/arbiter-server/src/actors/client/auth.rs rename to server/crates/arbiter-server/src/peers/client/auth.rs index a9ff7c2..5c224d7 100644 --- a/server/crates/arbiter-server/src/actors/client/auth.rs +++ b/server/crates/arbiter-server/src/peers/client/auth.rs @@ -1,22 +1,9 @@ -use arbiter_crypto::authn::{self, CLIENT_CONTEXT}; -use arbiter_proto::{ - ClientMetadata, - transport::{Bi, expect_message}, -}; -use chrono::Utc; -use diesel::{ - ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, SelectableHelper as _, - dsl::insert_into, update, -}; -use diesel_async::RunQueryDsl as _; -use kameo::{actor::ActorRef, error::SendError}; -use tracing::error; - +use super::{ClientConnection, ClientCredentials, ClientProfile}; use crate::{ actors::{ - client::{ClientConnection, ClientCredentials, ClientProfile}, + GlobalActors, flow_coordinator::{self, RequestClientApproval}, - keyholder::KeyHolder, + vault::Vault, }, crypto::integrity::{self, AttestationStatus}, db::{ @@ -25,6 +12,20 @@ use crate::{ schema::program_client, }, }; +use arbiter_crypto::authn::{self, AuthChallenge, CLIENT_CONTEXT}; +use arbiter_proto::{ + ClientMetadata, + transport::{Bi, expect_message}, +}; + +use chrono::Utc; +use diesel::{ + ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, SelectableHelper as _, + dsl::insert_into, update, +}; +use diesel_async::RunQueryDsl as _; +use kameo::{actor::ActorRef, error::SendError}; +use tracing::error; #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] pub enum Error { @@ -72,19 +73,14 @@ pub enum Inbound { #[derive(Debug, Clone)] pub enum Outbound { - AuthChallenge { - pubkey: authn::PublicKey, - nonce: i32, - }, + AuthChallenge { challenge: AuthChallenge }, AuthSuccess, } -/// Returns the current nonce and client ID for a registered client. -/// Returns `None` if the pubkey is not registered. -async fn get_current_nonce_and_id( +async fn get_client_id( db: &db::DatabasePool, pubkey: &authn::PublicKey, -) -> Result, Error> { +) -> Result, Error> { let pubkey_bytes = pubkey.to_bytes(); let mut conn = db.get().await.map_err(|e| { error!(error = ?e, "Database pool error"); @@ -92,8 +88,8 @@ async fn get_current_nonce_and_id( })?; program_client::table .filter(program_client::public_key.eq(&pubkey_bytes)) - .select((program_client::id, program_client::nonce)) - .first::<(i32, i32)>(&mut conn) + .select(program_client::id) + .first::(&mut conn) .await .optional() .map_err(|e| { @@ -104,7 +100,7 @@ async fn get_current_nonce_and_id( async fn verify_integrity( db: &db::DatabasePool, - keyholder: &ActorRef, + vault: &ActorRef, pubkey: &authn::PublicKey, ) -> Result<(), Error> { let mut db_conn = db.get().await.map_err(|e| { @@ -112,17 +108,16 @@ async fn verify_integrity( Error::DatabasePoolUnavailable })?; - let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?.ok_or_else(|| { + let id = get_client_id(db, pubkey).await?.ok_or_else(|| { error!("Client not found during integrity verification"); Error::DatabaseOperationFailed })?; let attestation = integrity::verify_entity( &mut db_conn, - keyholder, + vault, &ClientCredentials { pubkey: pubkey.clone(), - nonce, }, id, ) @@ -140,57 +135,7 @@ async fn verify_integrity( Ok(()) } -/// Atomically increments the nonce and re-signs the integrity envelope. -/// Returns the new nonce, which is used as the challenge nonce. -async fn create_nonce( - db: &db::DatabasePool, - keyholder: &ActorRef, - pubkey: &authn::PublicKey, -) -> Result { - let pubkey_bytes = pubkey.to_bytes(); - let pubkey = pubkey.clone(); - - let mut conn = db.get().await.map_err(|e| { - error!(error = ?e, "Database pool error"); - Error::DatabasePoolUnavailable - })?; - - conn.exclusive_transaction(|conn| { - let keyholder = keyholder.clone(); - let pubkey = pubkey.clone(); - Box::pin(async move { - let (id, new_nonce): (i32, i32) = update(program_client::table) - .filter(program_client::public_key.eq(&pubkey_bytes)) - .set(program_client::nonce.eq(program_client::nonce + 1)) - .returning((program_client::id, program_client::nonce)) - .get_result(conn) - .await?; - - integrity::sign_entity( - conn, - &keyholder, - &ClientCredentials { - pubkey: pubkey.clone(), - nonce: new_nonce, - }, - id, - ) - .await - .map_err(|e| { - error!(?e, "Integrity sign failed after nonce update"); - Error::DatabaseOperationFailed - })?; - - Ok(new_nonce) - }) - }) - .await -} - -async fn approve_new_client( - actors: &crate::actors::GlobalActors, - profile: ClientProfile, -) -> Result<(), Error> { +async fn approve_new_client(actors: &GlobalActors, profile: ClientProfile) -> Result<(), Error> { let result = actors .flow_coordinator .ask(RequestClientApproval { client: profile }) @@ -212,7 +157,7 @@ async fn approve_new_client( async fn insert_client( db: &db::DatabasePool, - keyholder: &ActorRef, + vault: &ActorRef, pubkey: &authn::PublicKey, metadata: &ClientMetadata, ) -> Result { @@ -226,11 +171,9 @@ async fn insert_client( })?; conn.exclusive_transaction(|conn| { - let keyholder = keyholder.clone(); + let vault = vault.clone(); let pubkey = pubkey.clone(); Box::pin(async move { - const NONCE_START: i32 = 1; - let metadata_id = insert_into(client_metadata::table) .values(( client_metadata::name.eq(&metadata.name), @@ -245,7 +188,6 @@ async fn insert_client( .values(( program_client::public_key.eq(pubkey.to_bytes()), program_client::metadata_id.eq(metadata_id), - program_client::nonce.eq(NONCE_START), )) .on_conflict_do_nothing() .returning(program_client::id) @@ -254,10 +196,9 @@ async fn insert_client( integrity::sign_entity( conn, - &keyholder, + &vault, &ClientCredentials { pubkey: pubkey.clone(), - nonce: NONCE_START, }, client_id, ) @@ -347,15 +288,14 @@ async fn sync_client_metadata( async fn challenge_client( transport: &mut T, pubkey: authn::PublicKey, - nonce: i32, + challenge: AuthChallenge, ) -> Result<(), Error> where T: Bi> + ?Sized, { transport .send(Ok(Outbound::AuthChallenge { - pubkey: pubkey.clone(), - nonce, + challenge: challenge.clone(), })) .await .map_err(|e| { @@ -373,7 +313,7 @@ where Error::Transport })?; - if !pubkey.verify(nonce, CLIENT_CONTEXT, &signature) { + if !pubkey.verify(&challenge, CLIENT_CONTEXT, &signature) { error!("Challenge solution verification failed"); return Err(Error::InvalidChallengeSolution); } @@ -389,9 +329,9 @@ where return Err(Error::Transport); }; - let client_id = match get_current_nonce_and_id(&props.db, &pubkey).await? { - Some((id, _)) => { - verify_integrity(&props.db, &props.actors.key_holder, &pubkey).await?; + let client_id = match get_client_id(&props.db, &pubkey).await? { + Some(id) => { + verify_integrity(&props.db, &props.actors.vault, &pubkey).await?; id } None => { @@ -403,13 +343,14 @@ where }, ) .await?; - insert_client(&props.db, &props.actors.key_holder, &pubkey, &metadata).await? + insert_client(&props.db, &props.actors.vault, &pubkey, &metadata).await? } }; sync_client_metadata(&props.db, client_id, &metadata).await?; - let challenge_nonce = create_nonce(&props.db, &props.actors.key_holder, &pubkey).await?; - challenge_client(transport, pubkey, challenge_nonce).await?; + + let challenge = AuthChallenge::generate(&mut rand::rng()); + challenge_client(transport, pubkey, challenge).await?; transport .send(Ok(Outbound::AuthSuccess)) diff --git a/server/crates/arbiter-server/src/actors/client/mod.rs b/server/crates/arbiter-server/src/peers/client/mod.rs similarity index 89% rename from server/crates/arbiter-server/src/actors/client/mod.rs rename to server/crates/arbiter-server/src/peers/client/mod.rs index 13e5733..a6c0b77 100644 --- a/server/crates/arbiter-server/src/actors/client/mod.rs +++ b/server/crates/arbiter-server/src/peers/client/mod.rs @@ -1,24 +1,22 @@ +use crate::{ + actors::GlobalActors, crypto::integrity::Integrable, db, peers::client::session::ClientSession, +}; use arbiter_crypto::authn; +use arbiter_macros::Hashable; use arbiter_proto::{ClientMetadata, transport::Bi}; + use kameo::actor::Spawn; use tracing::{error, info}; -use crate::{ - actors::{GlobalActors, client::session::ClientSession}, - crypto::integrity::Integrable, - db, -}; - #[derive(Debug, Clone)] pub struct ClientProfile { pub pubkey: authn::PublicKey, pub metadata: ClientMetadata, } -#[derive(arbiter_macros::Hashable)] +#[derive(Hashable)] pub struct ClientCredentials { pub pubkey: authn::PublicKey, - pub nonce: i32, } impl Integrable for ClientCredentials { diff --git a/server/crates/arbiter-server/src/actors/client/session.rs b/server/crates/arbiter-server/src/peers/client/session.rs similarity index 89% rename from server/crates/arbiter-server/src/actors/client/session.rs rename to server/crates/arbiter-server/src/peers/client/session.rs index 184c650..760d8a9 100644 --- a/server/crates/arbiter-server/src/actors/client/session.rs +++ b/server/crates/arbiter-server/src/peers/client/session.rs @@ -1,20 +1,19 @@ -use kameo::{Actor, messages}; -use tracing::error; - -use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature}; - +use super::ClientConnection; use crate::{ actors::{ GlobalActors, - client::ClientConnection, evm::{ClientSignTransaction, SignTransactionError}, flow_coordinator::RegisterClient, - keyholder::KeyHolderState, + vault::VaultState, }, db, evm::VetError, }; +use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature}; +use kameo::{Actor, messages}; +use tracing::error; + pub struct ClientSession { props: ClientConnection, client_id: i32, @@ -29,13 +28,13 @@ impl ClientSession { #[messages] impl ClientSession { #[message] - pub(crate) async fn handle_query_vault_state(&mut self) -> Result { - use crate::actors::keyholder::GetState; + pub(crate) async fn handle_query_vault_state(&mut self) -> Result { + use crate::actors::vault::GetState; - let vault_state = match self.props.actors.key_holder.ask(GetState {}).await { + let vault_state = match self.props.actors.vault.ask(GetState {}).await { Ok(state) => state, Err(err) => { - error!(?err, actor = "client", "keyholder.query.failed"); + error!(?err, actor = "client", "vault.query.failed"); return Err(Error::Internal); } }; diff --git a/server/crates/arbiter-server/src/peers/mod.rs b/server/crates/arbiter-server/src/peers/mod.rs new file mode 100644 index 0000000..02b992f --- /dev/null +++ b/server/crates/arbiter-server/src/peers/mod.rs @@ -0,0 +1,2 @@ +pub mod client; +pub mod user_agent; diff --git a/server/crates/arbiter-server/src/actors/user_agent/auth.rs b/server/crates/arbiter-server/src/peers/user_agent/auth/mod.rs similarity index 76% rename from server/crates/arbiter-server/src/actors/user_agent/auth.rs rename to server/crates/arbiter-server/src/peers/user_agent/auth/mod.rs index 00d2d55..51caeb8 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth.rs +++ b/server/crates/arbiter-server/src/peers/user_agent/auth/mod.rs @@ -1,13 +1,11 @@ -use arbiter_crypto::authn; +use super::{Credentials, UserAgentConnection}; +use arbiter_crypto::authn::{self, AuthChallenge}; use arbiter_proto::transport::Bi; + +use state::*; use tracing::error; -use crate::actors::user_agent::{ - UserAgentConnection, - auth::state::{AuthContext, AuthStateMachine}, -}; mod state; -use state::*; #[derive(Debug, Clone)] pub enum Inbound { @@ -46,7 +44,7 @@ impl From for Error { #[derive(Debug, Clone)] pub enum Outbound { - AuthChallenge { nonce: i32 }, + AuthChallenge { challenge: AuthChallenge }, AuthSuccess, } @@ -54,12 +52,11 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents { match payload { Inbound::AuthChallengeRequest { pubkey, - bootstrap_token: None, - } => AuthEvents::AuthRequest(ChallengeRequest { pubkey }), - Inbound::AuthChallengeRequest { + bootstrap_token, + } => AuthEvents::AuthRequest(ChallengeRequest { pubkey, - bootstrap_token: Some(token), - } => AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest { pubkey, token }), + bootstrap_token, + }), Inbound::AuthChallengeSolution { signature } => { AuthEvents::ReceivedSolution(ChallengeSolution { solution: signature, @@ -70,21 +67,20 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents { pub async fn authenticate( props: &mut UserAgentConnection, - transport: T, -) -> Result + transport: &mut T, +) -> Result where - T: Bi> + Send, + T: Bi> + Send + ?Sized, { let mut state = AuthStateMachine::new(AuthContext::new(props, transport)); loop { - // `state` holds a mutable reference to `props` so we can't access it directly here let Some(payload) = state.context_mut().transport.recv().await else { return Err(Error::Transport); }; match state.process_event(parse_auth_event(payload)).await { - Ok(AuthStates::AuthOk(key)) => return Ok(key.clone()), + Ok(AuthStates::AuthOk(result)) => return Ok(result.clone()), Err(AuthError::ActionFailed(err)) => { error!(?err, "State machine action failed"); return Err(err); diff --git a/server/crates/arbiter-server/src/peers/user_agent/auth/state.rs b/server/crates/arbiter-server/src/peers/user_agent/auth/state.rs new file mode 100644 index 0000000..93f7b80 --- /dev/null +++ b/server/crates/arbiter-server/src/peers/user_agent/auth/state.rs @@ -0,0 +1,198 @@ +use super::{ + super::{Credentials, UserAgentConnection}, + Error, +}; +use crate::{ + actors::bootstrap::ConsumeToken, + db::{DatabasePool, schema::useragent_client}, + peers::user_agent::auth::Outbound, +}; +use arbiter_crypto::authn::{self, AuthChallenge, USERAGENT_CONTEXT}; +use arbiter_proto::transport::Bi; + +use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl}; +use diesel_async::RunQueryDsl; +use tracing::error; + +pub(super) struct ChallengeRequest { + pub(super) pubkey: authn::PublicKey, + pub(super) bootstrap_token: Option, +} + +pub(super) struct ChallengeContext { + pub(super) challenge: AuthChallenge, + pub(super) pubkey: authn::PublicKey, + pub(super) bootstrap_token: Option, +} + +pub(super) struct ChallengeSolution { + pub(super) solution: Vec, +} + +smlang::statemachine!( + name: Auth, + custom_error: true, + transitions: { + *Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext), + SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(Credentials), + } +); + +async fn get_client_id(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result, Error> { + let mut conn = db.get().await.map_err(|e| { + error!(error = ?e, "Database pool error"); + Error::internal("Database unavailable") + })?; + + useragent_client::table + .filter(useragent_client::public_key.eq(pubkey.to_bytes())) + .select(useragent_client::id) + .first::(&mut conn) + .await + .optional() + .map_err(|e| { + error!(error = ?e, "Database error"); + Error::internal("Database operation failed") + }) +} + +async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result { + let pubkey_bytes = pubkey.to_bytes(); + let mut conn = db.get().await.map_err(|e| { + error!(error = ?e, "Database pool error"); + Error::internal("Database unavailable") + })?; + + let id: i32 = diesel::insert_into(useragent_client::table) + .values((useragent_client::public_key.eq(pubkey_bytes),)) + .returning(useragent_client::id) + .get_result(&mut conn) + .await + .map_err(|e| { + error!(error = ?e, "Database error"); + Error::internal("Database operation failed") + })?; + + Ok(id) +} + +pub(super) struct AuthContext<'a, T: ?Sized> { + pub(super) conn: &'a mut UserAgentConnection, + pub(super) transport: &'a mut T, +} + +impl<'a, T: ?Sized> AuthContext<'a, T> { + pub(super) fn new(conn: &'a mut UserAgentConnection, transport: &'a mut T) -> Self { + Self { conn, transport } + } +} + +impl AuthStateMachineContext for AuthContext<'_, T> +where + T: Bi> + Send + ?Sized, +{ + type Error = Error; + + async fn prepare_challenge( + &mut self, + ChallengeRequest { + pubkey, + bootstrap_token, + }: ChallengeRequest, + ) -> Result { + // Verify pubkey is registered (unless bootstrapping) + if bootstrap_token.is_none() { + let id = get_client_id(&self.conn.db, &pubkey).await?; + if id.is_none() { + return Err(Error::UnregisteredPublicKey); + } + } + + let challenge = AuthChallenge::generate(&mut rand::rng()); + + self.transport + .send(Ok(Outbound::AuthChallenge { + challenge: challenge.clone(), + })) + .await + .map_err(|e| { + error!(?e, "Failed to send auth challenge"); + Error::Transport + })?; + + Ok(ChallengeContext { + challenge, + pubkey, + bootstrap_token, + }) + } + + #[allow(missing_docs)] + #[allow(clippy::unused_unit)] + async fn verify_solution( + &mut self, + ChallengeContext { + challenge, + pubkey, + bootstrap_token, + }: &ChallengeContext, + ChallengeSolution { solution }: ChallengeSolution, + ) -> Result { + let signature = authn::Signature::try_from(solution.as_slice()).map_err(|_| { + error!("Failed to decode signature in challenge solution"); + Error::InvalidChallengeSolution + })?; + + let valid = pubkey.verify(challenge, USERAGENT_CONTEXT, &signature); + + if !valid { + self.transport + .send(Err(Error::InvalidChallengeSolution)) + .await + .map_err(|_| Error::Transport)?; + return Err(Error::InvalidChallengeSolution); + } + + // Resolve client id: bootstrap (consume token + register) or lookup + let id = match bootstrap_token { + Some(token) => { + let token_ok: bool = self + .conn + .actors + .bootstrapper + .ask(ConsumeToken { + token: token.clone(), + }) + .await + .map_err(|e| { + error!(?e, "Failed to consume bootstrap token"); + Error::internal("Failed to consume bootstrap token") + })?; + + if !token_ok { + error!("Invalid bootstrap token provided"); + self.transport + .send(Err(Error::InvalidBootstrapToken)) + .await + .map_err(|_| Error::Transport)?; + return Err(Error::InvalidBootstrapToken); + } + + register_key(&self.conn.db, pubkey).await? + } + None => get_client_id(&self.conn.db, pubkey) + .await? + .ok_or(Error::UnregisteredPublicKey)?, + }; + + self.transport + .send(Ok(Outbound::AuthSuccess)) + .await + .map_err(|_| Error::Transport)?; + + Ok(Credentials { + id, + pubkey: pubkey.clone(), + }) + } +} diff --git a/server/crates/arbiter-server/src/peers/user_agent/mod.rs b/server/crates/arbiter-server/src/peers/user_agent/mod.rs new file mode 100644 index 0000000..055b53f --- /dev/null +++ b/server/crates/arbiter-server/src/peers/user_agent/mod.rs @@ -0,0 +1,185 @@ +use crate::{ + actors::{ + GlobalActors, + vault::{GetState, Vault}, + }, + crypto::integrity::{self, AttestationStatus, Integrable}, + db::{self, DatabaseError, DatabasePool}, + peers::client::ClientProfile, +}; +use arbiter_crypto::authn; +use arbiter_macros::Hashable; +use arbiter_proto::transport::{Bi, Sender}; +use vault_gate::VaultGate; + +use kameo::actor::{ActorRef, Spawn as _}; +use tokio::sync::oneshot; +use tracing::{error, warn}; + +pub use auth::authenticate; +pub use session::UserAgentSession; + +pub mod auth; +pub mod session; +pub mod vault_gate; + +#[derive(Debug, Clone, Hashable)] +pub struct Credentials { + pub id: i32, + pub pubkey: authn::PublicKey, +} + +impl Integrable for Credentials { + const KIND: &'static str = "useragent_credentials"; +} + +// Messages, sent by user agent to connection client without having a request +#[derive(Debug)] +pub enum OutOfBand { + ClientConnectionRequest { profile: ClientProfile }, + ClientConnectionCancel { pubkey: authn::PublicKey }, +} + +#[derive(Clone)] +pub struct UserAgentConnection { + pub(crate) db: db::DatabasePool, + pub(crate) actors: GlobalActors, +} + +impl UserAgentConnection { + pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self { + Self { db, actors } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("authentication failed: {0:?}")] + Auth(auth::Error), + #[error("vault gate failed: {0}")] + VaultGate(#[from] vault_gate::Error), + #[error("transport closed unexpectedly")] + Transport, + #[error("database error: {0}")] + Database(DatabaseError), + #[error("internal: {0}")] + Internal(String), +} + +impl From for Error { + fn from(err: auth::Error) -> Self { + Self::Auth(err) + } +} + +async fn verify_integrity( + db: &DatabasePool, + vault: &ActorRef, + credentials: &Credentials, +) -> Result<(), Error> { + let mut conn = db + .get() + .await + .map_err(|_| Error::Internal("DB unavailable".into()))?; + match integrity::verify_entity(&mut conn, vault, credentials, credentials.id).await { + Ok(AttestationStatus::Attested) => Ok(()), + Ok(AttestationStatus::Unavailable) => { + Err(Error::Internal("Vault sealed during promotion".into())) + } + Err(e) => { + error!(?e, "Integrity verification failed during unseal promotion"); + Err(Error::Internal("Integrity check failed".into())) + } + } +} + +async fn should_run_gate(vault: &ActorRef) -> Result { + let vault_state = vault + .ask(GetState {}) + .await + .map_err(|_| Error::Internal("Failed to contact the vault".into()))?; + + Ok(!matches!( + vault_state, + crate::actors::vault::VaultState::Unsealed + )) +} + +async fn run_vault_gate( + props: &UserAgentConnection, + transport: &mut T, + auth_creds: Credentials, +) -> Result<(), Error> +where + T: Bi> + Send + ?Sized, +{ + let (promotion_tx, mut promotion_rx) = oneshot::channel(); + let gate = VaultGate::spawn(VaultGate::new( + auth_creds, + props.actors.clone(), + props.db.clone(), + promotion_tx, + )); + + let result = loop { + tokio::select! { + promotion = &mut promotion_rx => { + break match promotion { + Ok(Ok(creds)) => Ok(creds), + Ok(Err(err)) => Err(Error::VaultGate(err)), + Err(_) => Err(Error::Internal( + "vault gate promotion channel closed".into(), + )), + }; + } + + inbound = transport.recv() => { + let Some(inbound) = inbound else { + break Err(Error::Transport); + }; + + match gate.ask(inbound).await { + Ok(outbound) => { + if transport.send(Ok(outbound)).await.is_err() { + break Err(Error::Transport); + } + } + Err(err) => { + warn!(?err, "VaultGate failed to handle message"); + break Err(Error::Internal(format!( + "vault gate ask failed: {err:?}" + ))); + } + } + } + } + }; + + gate.kill(); + result +} + +pub async fn start( + props: &mut UserAgentConnection, + mut transport: T, + oob_sender: Box>, +) -> Result, Error> +where + T: Bi> + Send, + T: Bi> + Send, +{ + let creds = authenticate(props, &mut transport).await?; + + // should run vault gate only if sealed / unbootstrapped + if should_run_gate(&props.actors.vault).await? { + run_vault_gate(props, &mut transport, creds.clone()).await?; + } + + // checking the integrity + verify_integrity(&props.db, &props.actors.vault, &creds).await?; + + Ok(UserAgentSession::spawn(UserAgentSession::new( + props.clone(), + oob_sender, + ))) +} diff --git a/server/crates/arbiter-server/src/peers/user_agent/session/handlers.rs b/server/crates/arbiter-server/src/peers/user_agent/session/handlers.rs new file mode 100644 index 0000000..d0c9d73 --- /dev/null +++ b/server/crates/arbiter-server/src/peers/user_agent/session/handlers.rs @@ -0,0 +1,283 @@ +use super::{Error, UserAgentSession}; +use crate::{ + actors::evm::{ + ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError, + UseragentCreateGrant, UseragentListGrants, + }, + actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer, + actors::vault::VaultState, + db::models::{EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata}, + evm::policies::{Grant, SpecificGrant}, +}; +use arbiter_crypto::authn; + +use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature}; +use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper}; +use diesel_async::{AsyncConnection, RunQueryDsl}; +use kameo::{error::SendError, messages, prelude::Context}; +use tracing::error; + +#[derive(Debug, Error)] +pub enum SignTransactionError { + #[error("Policy evaluation failed")] + Vet(#[from] crate::evm::VetError), + + #[error("Internal signing error")] + Internal, +} + +#[derive(Debug, Error)] +pub enum GrantMutationError { + #[error("Vault is sealed")] + VaultSealed, + + #[error("Internal grant mutation error")] + Internal, +} + +#[messages] +impl UserAgentSession { + #[message] + pub(crate) async fn handle_query_vault_state(&mut self) -> Result { + use crate::actors::vault::GetState; + + let vault_state = match self.props.actors.vault.ask(GetState {}).await { + Ok(state) => state, + Err(err) => { + error!(?err, actor = "useragent", "vault.query.failed"); + return Err(Error::internal("Vault is in broken state")); + } + }; + + Ok(vault_state) + } +} + +#[messages] +impl UserAgentSession { + #[message] + pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result<(i32, Address), Error> { + match self.props.actors.evm.ask(Generate {}).await { + Ok(address) => Ok(address), + Err(SendError::HandlerError(err)) => Err(Error::internal(format!( + "EVM wallet generation failed: {err}" + ))), + Err(err) => { + error!(?err, "EVM actor unreachable during wallet create"); + Err(Error::internal("EVM actor unreachable")) + } + } + } + + #[message] + pub(crate) async fn handle_evm_wallet_list(&mut self) -> Result, Error> { + match self.props.actors.evm.ask(ListWallets {}).await { + Ok(wallets) => Ok(wallets), + Err(err) => { + error!(?err, "EVM wallet list failed"); + Err(Error::internal("Failed to list EVM wallets")) + } + } + } +} + +#[messages] +impl UserAgentSession { + #[message] + pub(crate) async fn handle_grant_list(&mut self) -> Result>, Error> { + match self.props.actors.evm.ask(UseragentListGrants {}).await { + Ok(grants) => Ok(grants), + Err(err) => { + error!(?err, "EVM grant list failed"); + Err(Error::internal("Failed to list EVM grants")) + } + } + } + + #[message] + pub(crate) async fn handle_grant_create( + &mut self, + basic: crate::evm::policies::SharedGrantSettings, + grant: crate::evm::policies::SpecificGrant, + ) -> Result { + match self + .props + .actors + .evm + .ask(UseragentCreateGrant { basic, grant }) + .await + { + Ok(grant_id) => Ok(grant_id), + Err(err) => { + error!(?err, "EVM grant create failed"); + Err(GrantMutationError::Internal) + } + } + } + + #[message] + pub(crate) async fn handle_grant_delete( + &mut self, + grant_id: i32, + ) -> Result<(), GrantMutationError> { + // match self + // .props + // .actors + // .evm + // .ask(UseragentDeleteGrant { grant_id }) + // .await + // { + // Ok(()) => Ok(()), + // Err(err) => { + // error!(?err, "EVM grant delete failed"); + // Err(GrantMutationError::Internal) + // } + // } + let _ = grant_id; + todo!() + } + + #[message] + pub(crate) async fn handle_sign_transaction( + &mut self, + client_id: i32, + wallet_address: Address, + transaction: TxEip1559, + ) -> Result { + match self + .props + .actors + .evm + .ask(ClientSignTransaction { + client_id, + wallet_address, + transaction, + }) + .await + { + Ok(signature) => Ok(signature), + Err(SendError::HandlerError(EvmSignError::Vet(vet_error))) => { + Err(SignTransactionError::Vet(vet_error)) + } + Err(err) => { + error!(?err, "EVM sign transaction failed in user-agent session"); + Err(SignTransactionError::Internal) + } + } + } + + #[message] + pub(crate) async fn handle_grant_evm_wallet_access( + &mut self, + entries: Vec, + ) -> Result<(), Error> { + let mut conn = self.props.db.get().await?; + conn.transaction(|conn| { + Box::pin(async move { + use crate::db::schema::evm_wallet_access; + + for entry in entries { + diesel::insert_into(evm_wallet_access::table) + .values(&entry) + .on_conflict_do_nothing() + .execute(conn) + .await?; + } + + Result::<_, Error>::Ok(()) + }) + }) + .await?; + Ok(()) + } + + #[message] + pub(crate) async fn handle_revoke_evm_wallet_access( + &mut self, + entries: Vec, + ) -> Result<(), Error> { + let mut conn = self.props.db.get().await?; + conn.transaction(|conn| { + Box::pin(async move { + use crate::db::schema::evm_wallet_access; + for entry in entries { + diesel::delete(evm_wallet_access::table) + .filter(evm_wallet_access::wallet_id.eq(entry)) + .execute(conn) + .await?; + } + + Result::<_, Error>::Ok(()) + }) + }) + .await?; + Ok(()) + } + + #[message] + pub(crate) async fn handle_list_wallet_access( + &mut self, + ) -> Result, Error> { + let mut conn = self.props.db.get().await?; + use crate::db::schema::evm_wallet_access; + let access_entries = evm_wallet_access::table + .select(EvmWalletAccess::as_select()) + .load::<_>(&mut conn) + .await?; + Ok(access_entries) + } +} + +#[messages] +impl UserAgentSession { + #[message(ctx)] + pub(crate) async fn handle_new_client_approve( + &mut self, + approved: bool, + pubkey: authn::PublicKey, + ctx: &mut Context>, + ) -> Result<(), Error> { + let pending_approval = match self.pending_client_approvals.remove(&pubkey.to_bytes()) { + Some(approval) => approval, + None => { + error!("Received client connection response for unknown client"); + return Err(Error::internal("Unknown client in connection response")); + } + }; + + pending_approval + .controller + .tell(ClientApprovalAnswer { approved }) + .await + .map_err(|err| { + error!( + ?err, + "Failed to send client approval response to controller" + ); + Error::internal("Failed to send client approval response to controller") + })?; + + ctx.actor_ref().unlink(&pending_approval.controller).await; + + Ok(()) + } + + #[message] + pub(crate) async fn handle_sdk_client_list( + &mut self, + ) -> Result, Error> { + use crate::db::schema::{client_metadata, program_client}; + let mut conn = self.props.db.get().await?; + + let clients = program_client::table + .inner_join(client_metadata::table) + .select(( + ProgramClient::as_select(), + ProgramClientMetadata::as_select(), + )) + .load::<(ProgramClient, ProgramClientMetadata)>(&mut conn) + .await?; + + Ok(clients) + } +} diff --git a/server/crates/arbiter-server/src/actors/user_agent/session.rs b/server/crates/arbiter-server/src/peers/user_agent/session/mod.rs similarity index 75% rename from server/crates/arbiter-server/src/actors/user_agent/session.rs rename to server/crates/arbiter-server/src/peers/user_agent/session/mod.rs index d3410bd..989303c 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session.rs +++ b/server/crates/arbiter-server/src/peers/user_agent/session/mod.rs @@ -1,21 +1,19 @@ +use super::{OutOfBand, UserAgentConnection}; +use crate::{ + actors::{ + flow_coordinator::client_connect_approval::ClientApprovalController, + useragent_registry::ConnectUseragent, + }, + peers::client::ClientProfile, +}; use arbiter_crypto::authn; - -use std::{borrow::Cow, collections::HashMap}; - use arbiter_proto::transport::Sender; -use async_trait::async_trait; + use kameo::{Actor, actor::ActorRef, messages}; +use std::{borrow::Cow, collections::HashMap}; use thiserror::Error; use tracing::error; -use crate::actors::{ - client::ClientProfile, - flow_coordinator::{RegisterUserAgent, client_connect_approval::ClientApprovalController}, - user_agent::{OutOfBand, UserAgentConnection}, -}; -mod state; -use state::{DummyContext, UserAgentEvents, UserAgentStateMachine}; - #[derive(Debug, Error)] pub enum Error { #[error("State transition failed")] @@ -53,47 +51,21 @@ pub struct PendingClientApproval { pub struct UserAgentSession { props: UserAgentConnection, - state: UserAgentStateMachine, sender: Box>, pending_client_approvals: HashMap, PendingClientApproval>, } -pub mod connection; +pub mod handlers; impl UserAgentSession { pub(crate) fn new(props: UserAgentConnection, sender: Box>) -> Self { Self { props, - state: UserAgentStateMachine::new(DummyContext), sender, pending_client_approvals: Default::default(), } } - - pub fn new_test(db: crate::db::DatabasePool, actors: crate::actors::GlobalActors) -> Self { - struct DummySender; - - #[async_trait] - impl Sender for DummySender { - async fn send( - &mut self, - _item: OutOfBand, - ) -> Result<(), arbiter_proto::transport::Error> { - Ok(()) - } - } - - Self::new(UserAgentConnection::new(db, actors), Box::new(DummySender)) - } - - fn transition(&mut self, event: UserAgentEvents) -> Result<(), Error> { - self.state.process_event(event).map_err(|e| { - error!(?e, "State transition failed"); - Error::State - })?; - Ok(()) - } } #[messages] @@ -140,17 +112,17 @@ impl Actor for UserAgentSession { ) -> Result { args.props .actors - .flow_coordinator - .ask(RegisterUserAgent { + .useragent_registry + .ask(ConnectUseragent { actor: this.clone(), }) .await .map_err(|err| { error!( ?err, - "Failed to register user agent connection with flow coordinator" + "Failed to register user agent connection with user agent registry" ); - Error::internal("Failed to register user agent connection with flow coordinator") + Error::internal("Failed to register user agent connection with user agent registry") })?; Ok(args) } diff --git a/server/crates/arbiter-server/src/peers/user_agent/vault_gate/mod.rs b/server/crates/arbiter-server/src/peers/user_agent/vault_gate/mod.rs new file mode 100644 index 0000000..1c0f4b9 --- /dev/null +++ b/server/crates/arbiter-server/src/peers/user_agent/vault_gate/mod.rs @@ -0,0 +1,294 @@ +use super::Credentials; +use crate::{ + actors::{ + GlobalActors, + vault::{self, Bootstrap, GetState, TryUnseal, VaultState, events}, + }, + crypto::integrity::{self}, + db::DatabasePool, +}; +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; +use state::*; + +use chacha20poly1305::{AeadInPlace, KeyInit as _, XChaCha20Poly1305, XNonce}; +use kameo::{Actor, error::SendError, messages, prelude::Message}; +use kameo_actors::message_bus::Register; +use tokio::sync::oneshot; +use tracing::{error, info}; +use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret}; + +pub mod state; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Vault is already bootstrapped")] + AlreadyBootstrapped, + #[error("Invalid key provided")] + InvalidKey, + + #[error("State transition failed")] + State, + + #[error("Internal error: {0}")] + Internal(String), +} +impl Error { + fn internal(message: impl Into) -> Self { + Self::Internal(message.into()) + } +} + +pub struct HandshakeResponse { + pub server_pubkey: PublicKey, +} + +pub struct VaultGate { + pub auth_creds: Credentials, + pub promotion_tx: Option>>, + pub state: State, + pub actors: GlobalActors, + pub db: DatabasePool, +} + +impl VaultGate { + pub fn new( + auth_creds: Credentials, + actors: GlobalActors, + db: DatabasePool, + promotion_tx: oneshot::Sender>, + ) -> Self { + Self { + auth_creds, + state: State::default(), + actors, + db, + promotion_tx: Some(promotion_tx), + } + } +} + +impl Actor for VaultGate { + type Args = Self; + + type Error = (); + + async fn on_start( + args: Self::Args, + actor_ref: kameo::prelude::ActorRef, + ) -> Result { + let _ = args + .actors + .events + .tell(Register( + actor_ref.clone().recipient::(), + )) + .await; + let _ = args + .actors + .events + .tell(Register(actor_ref.recipient::())) + .await; + Ok(args) + } +} + +impl VaultGate { + fn decrypt_key( + secret: &SharedSecret, + nonce: &[u8], + ciphertext: &[u8], + associated_data: &[u8], + ) -> Result>, ()> { + let nonce = XNonce::from_slice(nonce); + + let cipher = XChaCha20Poly1305::new(secret.as_bytes().into()); + + let mut key_buffer = SafeCell::new(ciphertext.to_vec()); + + let decryption_result = key_buffer.write_inline(|write_handle| { + cipher.decrypt_in_place(nonce, associated_data, write_handle) + }); + + match decryption_result { + Ok(_) => Ok(key_buffer), + Err(err) => { + error!(?err, "Failed to decrypt encrypted key material"); + Err(()) + } + } + } +} + +#[messages(messages = Inbound, replies = Outbound)] +impl VaultGate { + #[message] + pub async fn handle_handshake( + &mut self, + client_pubkey: x25519_dalek::PublicKey, + ) -> Result { + let ephemeral_secret = EphemeralSecret::random(); + let public_key = PublicKey::from(&ephemeral_secret); + + let secret = ephemeral_secret.diffie_hellman(&client_pubkey); + + self.state = State::ReadyForExchange { + server_key: public_key, + secret, + }; + + Ok(HandshakeResponse { + server_pubkey: public_key, + }) + } + + #[message] + pub async fn handle_unseal_encrypted_key( + &mut self, + nonce: Vec, + ciphertext: Vec, + associated_data: Vec, + ) -> Result<(), Error> { + let State::ReadyForExchange { secret, .. } = &self.state else { + return Err(Error::State); + }; + + let seal_key_buffer = match Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data) + { + Ok(buffer) => buffer, + Err(()) => { + return Err(Error::InvalidKey); + } + }; + + match self + .actors + .vault + .ask(TryUnseal { + seal_key_raw: seal_key_buffer, + }) + .await + { + Ok(_) => { + info!("Successfully unsealed key with client-provided key"); + Ok(()) + } + Err(SendError::HandlerError(vault::Error::InvalidKey)) => Err(Error::InvalidKey), + Err(SendError::HandlerError(err)) => { + error!(?err, "Vault failed to unseal key"); + Err(Error::InvalidKey) + } + Err(err) => { + error!(?err, "Failed to send unseal request to vault"); + Err(Error::internal("Vault actor error")) + } + } + } + + #[message] + pub async fn handle_bootstrap_encrypted_key( + &mut self, + nonce: Vec, + ciphertext: Vec, + associated_data: Vec, + ) -> Result<(), Error> { + let State::ReadyForExchange { secret, .. } = &self.state else { + return Err(Error::State); + }; + + let seal_key_buffer = match Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data) + { + Ok(buffer) => buffer, + Err(()) => { + return Err(Error::InvalidKey); + } + }; + + match self + .actors + .vault + .ask(Bootstrap { + seal_key_raw: seal_key_buffer, + }) + .await + { + Ok(_) => { + info!("Successfully bootstrapped vault with client-provided key"); + Ok(()) + } + Err(SendError::HandlerError(vault::Error::AlreadyBootstrapped)) => { + Err(Error::AlreadyBootstrapped) + } + Err(SendError::HandlerError(err)) => { + error!(?err, "Vault failed to bootstrap vault"); + Err(Error::InvalidKey) + } + Err(err) => { + error!(?err, "Failed to send bootstrap request to vault"); + Err(Error::internal("Vault error")) + } + } + } + + #[message] + pub async fn handle_vault_state(&mut self) -> Result { + let answer = self + .actors + .vault + .ask(GetState {}) + .await + .map_err(|_| Error::internal("failed to query vault"))?; + + Ok(answer) + } +} + +impl Message for VaultGate { + type Reply = (); + + async fn handle( + &mut self, + _: events::Bootstrapped, + ctx: &mut kameo::prelude::Context, + ) -> Self::Reply { + let result = async { + let mut conn = self + .db + .get() + .await + .map_err(|_| Error::internal("DB unavailable"))?; + integrity::sign_entity( + &mut conn, + &self.actors.vault, + &self.auth_creds, + self.auth_creds.id, + ) + .await + .map_err(|e| { + error!(?e, "Failed to sign integrity envelope on bootstrap"); + Error::internal("Integrity sign failed") + })?; + Ok(()) + } + .await; + + if let Some(tx) = self.promotion_tx.take() { + let _ = tx.send(result); + } + ctx.stop(); + } +} + +impl Message for VaultGate { + type Reply = (); + + async fn handle( + &mut self, + _: events::Unsealed, + ctx: &mut kameo::prelude::Context, + ) -> Self::Reply { + if let Some(tx) = self.promotion_tx.take() { + let _ = tx.send(Ok(())); + } + ctx.stop(); + } +} diff --git a/server/crates/arbiter-server/src/peers/user_agent/vault_gate/state.rs b/server/crates/arbiter-server/src/peers/user_agent/vault_gate/state.rs new file mode 100644 index 0000000..2f4baff --- /dev/null +++ b/server/crates/arbiter-server/src/peers/user_agent/vault_gate/state.rs @@ -0,0 +1,11 @@ +use x25519_dalek::{PublicKey, SharedSecret}; + +#[derive(Default)] +pub enum State { + #[default] + Idle, + ReadyForExchange { + server_key: PublicKey, + secret: SharedSecret, + }, +} diff --git a/server/crates/arbiter-server/tests/client/auth.rs b/server/crates/arbiter-server/tests/client/auth.rs index a7137e3..e0b866d 100644 --- a/server/crates/arbiter-server/tests/client/auth.rs +++ b/server/crates/arbiter-server/tests/client/auth.rs @@ -1,24 +1,23 @@ +use super::common::ChannelTransport; use arbiter_crypto::{ - authn::{self, CLIENT_CONTEXT, format_challenge}, + authn::{self, AuthChallenge, CLIENT_CONTEXT}, safecell::{SafeCell, SafeCellHandle as _}, }; -use arbiter_proto::ClientMetadata; -use arbiter_proto::transport::{Receiver, Sender}; +use arbiter_proto::{ + ClientMetadata, + transport::{Receiver, Sender}, +}; use arbiter_server::{ - actors::{ - GlobalActors, - client::{ClientConnection, ClientCredentials, auth, connect_client}, - keyholder::Bootstrap, - }, + actors::{GlobalActors, vault::Bootstrap}, crypto::integrity, db::{self, schema}, + peers::client::{ClientConnection, ClientCredentials, auth, connect_client}, }; + use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into}; use diesel_async::RunQueryDsl; use ml_dsa::{KeyGen, MlDsa87, SigningKey, VerifyingKey, signature::Keypair as _}; -use super::common::ChannelTransport; - fn metadata(name: &str, description: Option<&str>, version: Option<&str>) -> ClientMetadata { ClientMetadata { name: name.to_owned(), @@ -58,10 +57,9 @@ async fn insert_registered_client( integrity::sign_entity( &mut conn, - &actors.key_holder, + &actors.vault, &ClientCredentials { pubkey: pubkey.into(), - nonce: 1, }, client_id, ) @@ -69,12 +67,8 @@ async fn insert_registered_client( .unwrap(); } -fn sign_client_challenge( - key: &SigningKey, - nonce: i32, - pubkey: &authn::PublicKey, -) -> authn::Signature { - let challenge = format_challenge(nonce, &pubkey.to_bytes()); +fn sign_client_challenge(key: &SigningKey, challenge: &AuthChallenge) -> authn::Signature { + let challenge = challenge.format(); key.signing_key() .sign_deterministic(&challenge, CLIENT_CONTEXT) .unwrap() @@ -89,10 +83,7 @@ async fn insert_bootstrap_sentinel_useragent(db: &db::DatabasePool) { .to_vec(); insert_into(schema::useragent_client::table) - .values(( - schema::useragent_client::public_key.eq(sentinel_key), - schema::useragent_client::key_type.eq(1i32), - )) + .values((schema::useragent_client::public_key.eq(sentinel_key),)) .execute(&mut conn) .await .unwrap(); @@ -103,7 +94,7 @@ async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors { let actors = GlobalActors::spawn(db.clone()).await.unwrap(); actors - .key_holder + .vault .ask(Bootstrap { seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()), }) @@ -114,7 +105,7 @@ async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors { #[tokio::test] #[test_log::test] -pub async fn test_unregistered_pubkey_rejected() { +async fn test_unregistered_pubkey_rejected() { let db = db::create_test_pool().await; let (server_transport, mut test_transport) = ChannelTransport::new(); @@ -141,7 +132,7 @@ pub async fn test_unregistered_pubkey_rejected() { #[tokio::test] #[test_log::test] -pub async fn test_challenge_auth() { +async fn test_challenge_auth() { let db = db::create_test_pool().await; let actors = spawn_test_actors(&db).await; @@ -178,14 +169,14 @@ pub async fn test_challenge_auth() { .expect("should receive challenge"); let challenge = match response { Ok(resp) => match resp { - auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce), + auth::Outbound::AuthChallenge { challenge } => challenge, other => panic!("Expected AuthChallenge, got {other:?}"), }, Err(err) => panic!("Expected Ok response, got Err({err:?})"), }; // Sign the challenge and send solution - let signature = sign_client_challenge(&new_key, challenge.1, &challenge.0); + let signature = sign_client_challenge(&new_key, &challenge); test_transport .send(auth::Inbound::AuthChallengeSolution { signature }) @@ -208,7 +199,7 @@ pub async fn test_challenge_auth() { #[tokio::test] #[test_log::test] -pub async fn test_metadata_unchanged_does_not_append_history() { +async fn test_metadata_unchanged_does_not_append_history() { let db = db::create_test_pool().await; let actors = spawn_test_actors(&db).await; let new_key = MlDsa87::key_gen(&mut rand::rng()); @@ -233,11 +224,11 @@ pub async fn test_metadata_unchanged_does_not_append_history() { .unwrap(); let response = test_transport.recv().await.unwrap().unwrap(); - let (pubkey, nonce) = match response { - auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce), + let challenge = match response { + auth::Outbound::AuthChallenge { challenge } => challenge, other => panic!("Expected AuthChallenge, got {other:?}"), }; - let signature = sign_client_challenge(&new_key, nonce, &pubkey); + let signature = sign_client_challenge(&new_key, &challenge); test_transport .send(auth::Inbound::AuthChallengeSolution { signature }) .await @@ -265,7 +256,7 @@ pub async fn test_metadata_unchanged_does_not_append_history() { #[tokio::test] #[test_log::test] -pub async fn test_metadata_change_appends_history_and_repoints_binding() { +async fn test_metadata_change_appends_history_and_repoints_binding() { let db = db::create_test_pool().await; let actors = spawn_test_actors(&db).await; let new_key = MlDsa87::key_gen(&mut rand::rng()); @@ -295,11 +286,11 @@ pub async fn test_metadata_change_appends_history_and_repoints_binding() { .unwrap(); let response = test_transport.recv().await.unwrap().unwrap(); - let (pubkey, nonce) = match response { - auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce), + let challenge = match response { + auth::Outbound::AuthChallenge { challenge } => challenge, other => panic!("Expected AuthChallenge, got {other:?}"), }; - let signature = sign_client_challenge(&new_key, nonce, &pubkey); + let signature = sign_client_challenge(&new_key, &challenge); test_transport .send(auth::Inbound::AuthChallengeSolution { signature }) .await @@ -352,7 +343,7 @@ pub async fn test_metadata_change_appends_history_and_repoints_binding() { #[tokio::test] #[test_log::test] -pub async fn test_challenge_auth_rejects_integrity_tag_mismatch() { +async fn test_challenge_auth_rejects_integrity_tag_mismatch() { let db = db::create_test_pool().await; let actors = spawn_test_actors(&db).await; diff --git a/server/crates/arbiter-server/tests/common/mod.rs b/server/crates/arbiter-server/tests/common/mod.rs index c4e6878..4fe0465 100644 --- a/server/crates/arbiter-server/tests/common/mod.rs +++ b/server/crates/arbiter-server/tests/common/mod.rs @@ -1,7 +1,7 @@ use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_proto::transport::{Bi, Error, Receiver, Sender}; use arbiter_server::{ - actors::keyholder::KeyHolder, + actors::{GlobalActors, vault::Vault}, db::{self, schema}, }; @@ -11,8 +11,10 @@ use diesel_async::RunQueryDsl; use tokio::sync::mpsc; #[allow(dead_code)] -pub async fn bootstrapped_keyholder(db: &db::DatabasePool) -> KeyHolder { - let mut actor = KeyHolder::new(db.clone()).await.unwrap(); +pub(crate) async fn bootstrapped_vault(db: &db::DatabasePool) -> Vault { + let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()) + .await + .unwrap(); actor .bootstrap(SafeCell::new(b"test-seal-key".to_vec())) .await @@ -21,7 +23,7 @@ pub async fn bootstrapped_keyholder(db: &db::DatabasePool) -> KeyHolder { } #[allow(dead_code)] -pub async fn root_key_history_id(db: &db::DatabasePool) -> i32 { +pub(crate) async fn root_key_history_id(db: &db::DatabasePool) -> i32 { let mut conn = db.get().await.unwrap(); let id = schema::arbiter_settings::table .select(schema::arbiter_settings::root_key_id) @@ -32,14 +34,14 @@ pub async fn root_key_history_id(db: &db::DatabasePool) -> i32 { } #[allow(dead_code)] -pub struct ChannelTransport { +pub(crate) struct ChannelTransport { receiver: mpsc::Receiver, sender: mpsc::Sender, } impl ChannelTransport { #[allow(dead_code)] - pub fn new() -> (Self, ChannelTransport) { + pub(crate) fn new() -> (Self, ChannelTransport) { let (tx1, rx1) = mpsc::channel(10); let (tx2, rx2) = mpsc::channel(10); ( diff --git a/server/crates/arbiter-server/tests/keyholder.rs b/server/crates/arbiter-server/tests/keyholder.rs deleted file mode 100644 index 0fa5692..0000000 --- a/server/crates/arbiter-server/tests/keyholder.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod common; - -#[path = "keyholder/concurrency.rs"] -mod concurrency; -#[path = "keyholder/lifecycle.rs"] -mod lifecycle; -#[path = "keyholder/storage.rs"] -mod storage; diff --git a/server/crates/arbiter-server/tests/user_agent/auth.rs b/server/crates/arbiter-server/tests/user_agent/auth.rs index aeccc8a..d215bba 100644 --- a/server/crates/arbiter-server/tests/user_agent/auth.rs +++ b/server/crates/arbiter-server/tests/user_agent/auth.rs @@ -1,44 +1,157 @@ +use super::common::ChannelTransport; use arbiter_crypto::{ - authn::{self, USERAGENT_CONTEXT, format_challenge}, + authn::{self, AuthChallenge, USERAGENT_CONTEXT}, safecell::{SafeCell, SafeCellHandle as _}, }; - -use arbiter_proto::transport::{Receiver, Sender}; +use arbiter_proto::transport::{Error as TransportError, Receiver, Sender}; use arbiter_server::{ - actors::{ - GlobalActors, - bootstrap::GetToken, - keyholder::Bootstrap, - user_agent::{UserAgentConnection, UserAgentCredentials, auth}, - }, + actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap}, crypto::integrity, db::{self, schema}, + peers::user_agent::{self, Credentials, UserAgentConnection, auth, vault_gate}, }; + +use async_trait::async_trait; use diesel::{ExpressionMethods as _, QueryDsl, insert_into}; use diesel_async::RunQueryDsl; use ml_dsa::{KeyGen, MlDsa87, SigningKey, signature::Keypair as _}; - -use super::common::ChannelTransport; +use tokio::sync::mpsc; fn sign_useragent_challenge( key: &SigningKey, - nonce: i32, - pubkey_bytes: &[u8], + challenge: &AuthChallenge, ) -> authn::Signature { - let challenge = format_challenge(nonce, pubkey_bytes); + let challenge = challenge.format(); key.signing_key() .sign_deterministic(&challenge, USERAGENT_CONTEXT) .unwrap() .into() } +fn tamper_challenge(challenge: &AuthChallenge) -> AuthChallenge { + let mut challenge = challenge.clone(); + challenge.nonce[0] ^= 1; + challenge +} + +struct NullOobSender; + +#[async_trait] +impl Sender for NullOobSender { + async fn send(&mut self, _item: user_agent::OutOfBand) -> Result<(), TransportError> { + Ok(()) + } +} + +struct StartServerTransport { + auth_rx: mpsc::Receiver, + auth_tx: mpsc::Sender>, + vault_rx: mpsc::Receiver, + vault_tx: mpsc::Sender>, +} + +struct StartTestTransport { + auth_rx: mpsc::Receiver>, + auth_tx: mpsc::Sender, +} + +fn start_transport_pair() -> (StartServerTransport, StartTestTransport) { + let (auth_in_tx, auth_in_rx) = mpsc::channel(10); + let (auth_out_tx, auth_out_rx) = mpsc::channel(10); + let (_vault_in_tx, vault_in_rx) = mpsc::channel(10); + let (vault_out_tx, _vault_out_rx) = mpsc::channel(10); + + ( + StartServerTransport { + auth_rx: auth_in_rx, + auth_tx: auth_out_tx, + vault_rx: vault_in_rx, + vault_tx: vault_out_tx, + }, + StartTestTransport { + auth_rx: auth_out_rx, + auth_tx: auth_in_tx, + }, + ) +} + +#[async_trait] +impl Receiver for StartServerTransport { + async fn recv(&mut self) -> Option { + self.auth_rx.recv().await + } +} + +#[async_trait] +impl Sender> for StartServerTransport { + async fn send( + &mut self, + item: Result, + ) -> Result<(), TransportError> { + self.auth_tx + .send(item) + .await + .map_err(|_| TransportError::ChannelClosed) + } +} + +impl arbiter_proto::transport::Bi> + for StartServerTransport +{ +} + +#[async_trait] +impl Receiver for StartServerTransport { + async fn recv(&mut self) -> Option { + self.vault_rx.recv().await + } +} + +#[async_trait] +impl Sender> for StartServerTransport { + async fn send( + &mut self, + item: Result, + ) -> Result<(), TransportError> { + self.vault_tx + .send(item) + .await + .map_err(|_| TransportError::ChannelClosed) + } +} + +impl + arbiter_proto::transport::Bi< + vault_gate::Inbound, + Result, + > for StartServerTransport +{ +} + +#[async_trait] +impl Receiver> for StartTestTransport { + async fn recv(&mut self) -> Option> { + self.auth_rx.recv().await + } +} + +#[async_trait] +impl Sender for StartTestTransport { + async fn send(&mut self, item: auth::Inbound) -> Result<(), TransportError> { + self.auth_tx + .send(item) + .await + .map_err(|_| TransportError::ChannelClosed) + } +} + #[tokio::test] #[test_log::test] -pub async fn test_bootstrap_token_auth() { +async fn test_bootstrap_token_auth() { let db = db::create_test_pool().await; let actors = GlobalActors::spawn(db.clone()).await.unwrap(); actors - .key_holder + .vault .ask(Bootstrap { seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()), }) @@ -46,11 +159,11 @@ pub async fn test_bootstrap_token_auth() { .unwrap(); let token = actors.bootstrapper.ask(GetToken).await.unwrap().unwrap(); - let (server_transport, mut test_transport) = ChannelTransport::new(); + let (mut server_transport, mut test_transport) = ChannelTransport::new(); let db_for_task = db.clone(); let task = tokio::spawn(async move { let mut props = UserAgentConnection::new(db_for_task, actors); - auth::authenticate(&mut props, server_transport).await + auth::authenticate(&mut props, &mut server_transport).await }); let new_key = MlDsa87::key_gen(&mut rand::rng()); @@ -62,14 +175,29 @@ pub async fn test_bootstrap_token_auth() { .await .unwrap(); + let response = test_transport + .recv() + .await + .expect("should receive challenge"); + let challenge = match response { + Ok(auth::Outbound::AuthChallenge { challenge }) => challenge, + other => panic!("Expected AuthChallenge, got {other:?}"), + }; + + let signature = sign_useragent_challenge(&new_key, &challenge); + + test_transport + .send(auth::Inbound::AuthChallengeSolution { + signature: signature.to_bytes(), + }) + .await + .unwrap(); + let response = test_transport .recv() .await .expect("should receive auth result"); - match response { - Ok(auth::Outbound::AuthSuccess) => {} - other => panic!("Expected AuthSuccess, got {other:?}"), - } + assert!(matches!(response, Ok(auth::Outbound::AuthSuccess))); task.await.unwrap().unwrap(); @@ -84,15 +212,15 @@ pub async fn test_bootstrap_token_auth() { #[tokio::test] #[test_log::test] -pub async fn test_bootstrap_invalid_token_auth() { +async fn test_bootstrap_invalid_token_auth() { let db = db::create_test_pool().await; let actors = GlobalActors::spawn(db.clone()).await.unwrap(); - let (server_transport, mut test_transport) = ChannelTransport::new(); + let (mut server_transport, mut test_transport) = ChannelTransport::new(); let db_for_task = db.clone(); let task = tokio::spawn(async move { let mut props = UserAgentConnection::new(db_for_task, actors); - auth::authenticate(&mut props, server_transport).await + auth::authenticate(&mut props, &mut server_transport).await }); let new_key = MlDsa87::key_gen(&mut rand::rng()); @@ -104,6 +232,23 @@ pub async fn test_bootstrap_invalid_token_auth() { .await .unwrap(); + let response = test_transport + .recv() + .await + .expect("should receive challenge"); + let challenge = match response { + Ok(auth::Outbound::AuthChallenge { challenge }) => challenge, + other => panic!("Expected AuthChallenge, got {other:?}"), + }; + + let signature = sign_useragent_challenge(&new_key, &challenge); + test_transport + .send(auth::Inbound::AuthChallengeSolution { + signature: signature.to_bytes(), + }) + .await + .unwrap(); + assert!(matches!( task.await.unwrap(), Err(auth::Error::InvalidBootstrapToken) @@ -120,11 +265,11 @@ pub async fn test_bootstrap_invalid_token_auth() { #[tokio::test] #[test_log::test] -pub async fn test_challenge_auth() { +async fn test_challenge_auth() { let db = db::create_test_pool().await; let actors = GlobalActors::spawn(db.clone()).await.unwrap(); actors - .key_holder + .vault .ask(Bootstrap { seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()), }) @@ -137,20 +282,17 @@ pub async fn test_challenge_auth() { { let mut conn = db.get().await.unwrap(); let id: i32 = insert_into(schema::useragent_client::table) - .values(( - schema::useragent_client::public_key.eq(pubkey_bytes.clone()), - schema::useragent_client::key_type.eq(1i32), - )) + .values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),)) .returning(schema::useragent_client::id) .get_result(&mut conn) .await .unwrap(); integrity::sign_entity( &mut conn, - &actors.key_holder, - &UserAgentCredentials { + &actors.vault, + &Credentials { + id, pubkey: new_key.verifying_key().into(), - nonce: 1, }, id, ) @@ -158,11 +300,11 @@ pub async fn test_challenge_auth() { .unwrap(); } - let (server_transport, mut test_transport) = ChannelTransport::new(); + let (mut server_transport, mut test_transport) = ChannelTransport::new(); let db_for_task = db.clone(); let task = tokio::spawn(async move { let mut props = UserAgentConnection::new(db_for_task, actors); - auth::authenticate(&mut props, server_transport).await + auth::authenticate(&mut props, &mut server_transport).await }); test_transport @@ -179,13 +321,13 @@ pub async fn test_challenge_auth() { .expect("should receive challenge"); let challenge = match response { Ok(resp) => match resp { - auth::Outbound::AuthChallenge { nonce } => nonce, + auth::Outbound::AuthChallenge { challenge } => challenge, other => panic!("Expected AuthChallenge, got {other:?}"), }, Err(err) => panic!("Expected Ok response, got Err({err:?})"), }; - let signature = sign_useragent_challenge(&new_key, challenge, &pubkey_bytes); + let signature = sign_useragent_challenge(&new_key, &challenge); test_transport .send(auth::Inbound::AuthChallengeSolution { @@ -208,12 +350,12 @@ pub async fn test_challenge_auth() { #[tokio::test] #[test_log::test] -pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() { +async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() { let db = db::create_test_pool().await; let actors = GlobalActors::spawn(db.clone()).await.unwrap(); actors - .key_holder + .vault .ask(Bootstrap { seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()), }) @@ -226,81 +368,17 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() { let mut conn = db.get().await.unwrap(); insert_into(schema::useragent_client::table) - .values(( - schema::useragent_client::public_key.eq(pubkey_bytes.clone()), - schema::useragent_client::key_type.eq(1i32), - )) + .values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),)) .execute(&mut conn) .await .unwrap(); } - let (server_transport, mut test_transport) = ChannelTransport::new(); + let (server_transport, mut test_transport) = start_transport_pair(); let db_for_task = db.clone(); let task = tokio::spawn(async move { let mut props = UserAgentConnection::new(db_for_task, actors); - auth::authenticate(&mut props, server_transport).await - }); - - test_transport - .send(auth::Inbound::AuthChallengeRequest { - pubkey: new_key.verifying_key().into(), - bootstrap_token: None, - }) - .await - .unwrap(); - - assert!(matches!( - task.await.unwrap(), - Err(auth::Error::Internal { .. }) - )); -} - -#[tokio::test] -#[test_log::test] -pub async fn test_challenge_auth_rejects_invalid_signature() { - let db = db::create_test_pool().await; - let actors = GlobalActors::spawn(db.clone()).await.unwrap(); - actors - .key_holder - .ask(Bootstrap { - seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()), - }) - .await - .unwrap(); - - let new_key = MlDsa87::key_gen(&mut rand::rng()); - let pubkey_bytes = new_key.verifying_key().encode().to_vec(); - - { - let mut conn = db.get().await.unwrap(); - let id: i32 = insert_into(schema::useragent_client::table) - .values(( - schema::useragent_client::public_key.eq(pubkey_bytes.clone()), - schema::useragent_client::key_type.eq(1i32), - )) - .returning(schema::useragent_client::id) - .get_result(&mut conn) - .await - .unwrap(); - integrity::sign_entity( - &mut conn, - &actors.key_holder, - &UserAgentCredentials { - pubkey: new_key.verifying_key().into(), - nonce: 1, - }, - id, - ) - .await - .unwrap(); - } - - let (server_transport, mut test_transport) = ChannelTransport::new(); - let db_for_task = db.clone(); - let task = tokio::spawn(async move { - let mut props = UserAgentConnection::new(db_for_task, actors); - auth::authenticate(&mut props, server_transport).await + user_agent::start(&mut props, server_transport, Box::new(NullOobSender)).await }); test_transport @@ -317,13 +395,98 @@ pub async fn test_challenge_auth_rejects_invalid_signature() { .expect("should receive challenge"); let challenge = match response { Ok(resp) => match resp { - auth::Outbound::AuthChallenge { nonce } => nonce, + auth::Outbound::AuthChallenge { challenge } => challenge, other => panic!("Expected AuthChallenge, got {other:?}"), }, Err(err) => panic!("Expected Ok response, got Err({err:?})"), }; - let signature = sign_useragent_challenge(&new_key, challenge + 1, &pubkey_bytes); + let signature = sign_useragent_challenge(&new_key, &challenge); + + test_transport + .send(auth::Inbound::AuthChallengeSolution { + signature: signature.to_bytes(), + }) + .await + .unwrap(); + + let response = test_transport + .recv() + .await + .expect("should receive auth result"); + assert!(matches!(response, Ok(auth::Outbound::AuthSuccess))); + + assert!(matches!( + task.await.unwrap(), + Err(user_agent::Error::Internal(_)) + )); +} + +#[tokio::test] +#[test_log::test] +async fn test_challenge_auth_rejects_invalid_signature() { + let db = db::create_test_pool().await; + let actors = GlobalActors::spawn(db.clone()).await.unwrap(); + actors + .vault + .ask(Bootstrap { + seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()), + }) + .await + .unwrap(); + + let new_key = MlDsa87::key_gen(&mut rand::rng()); + let pubkey_bytes = new_key.verifying_key().encode().to_vec(); + + { + let mut conn = db.get().await.unwrap(); + let id: i32 = insert_into(schema::useragent_client::table) + .values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),)) + .returning(schema::useragent_client::id) + .get_result(&mut conn) + .await + .unwrap(); + integrity::sign_entity( + &mut conn, + &actors.vault, + &Credentials { + id, + pubkey: new_key.verifying_key().into(), + }, + id, + ) + .await + .unwrap(); + } + + let (mut server_transport, mut test_transport) = ChannelTransport::new(); + let db_for_task = db.clone(); + let task = tokio::spawn(async move { + let mut props = UserAgentConnection::new(db_for_task, actors); + auth::authenticate(&mut props, &mut server_transport).await + }); + + test_transport + .send(auth::Inbound::AuthChallengeRequest { + pubkey: new_key.verifying_key().into(), + bootstrap_token: None, + }) + .await + .unwrap(); + + let response = test_transport + .recv() + .await + .expect("should receive challenge"); + let challenge = match response { + Ok(resp) => match resp { + auth::Outbound::AuthChallenge { challenge } => challenge, + other => panic!("Expected AuthChallenge, got {other:?}"), + }, + Err(err) => panic!("Expected Ok response, got Err({err:?})"), + }; + + let signature = sign_useragent_challenge(&new_key, &tamper_challenge(&challenge)); test_transport .send(auth::Inbound::AuthChallengeSolution { diff --git a/server/crates/arbiter-server/tests/user_agent/unseal.rs b/server/crates/arbiter-server/tests/user_agent/unseal.rs index 15cf475..bae308a 100644 --- a/server/crates/arbiter-server/tests/user_agent/unseal.rs +++ b/server/crates/arbiter-server/tests/user_agent/unseal.rs @@ -1,49 +1,62 @@ -use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; +use arbiter_crypto::{ + authn, + safecell::{SafeCell, SafeCellHandle as _}, +}; use arbiter_server::{ actors::{ GlobalActors, - keyholder::{Bootstrap, Seal}, - user_agent::{ - UserAgentSession, - session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError}, - }, + vault::{Bootstrap, Seal}, }, db, + peers::user_agent::{ + Credentials, + vault_gate::{ + Error as VaultGateError, HandleHandshake, HandleUnsealEncryptedKey, VaultGate, + }, + }, }; use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; use kameo::actor::Spawn as _; +use tokio::sync::oneshot; use x25519_dalek::{EphemeralSecret, PublicKey}; -async fn setup_sealed_user_agent( +async fn setup_sealed_gate( seal_key: &[u8], -) -> (db::DatabasePool, kameo::actor::ActorRef) { +) -> ( + db::DatabasePool, + kameo::actor::ActorRef, + oneshot::Receiver>, +) { let db = db::create_test_pool().await; let actors = GlobalActors::spawn(db.clone()).await.unwrap(); actors - .key_holder + .vault .ask(Bootstrap { seal_key_raw: SafeCell::new(seal_key.to_vec()), }) .await .unwrap(); - actors.key_holder.ask(Seal).await.unwrap(); + actors.vault.ask(Seal).await.unwrap(); - let session = UserAgentSession::spawn(UserAgentSession::new_test(db.clone(), actors)); + let (promotion_tx, promotion_rx) = oneshot::channel(); + let pubkey = authn::SigningKey::generate().public_key(); + let auth_creds = Credentials { id: 1, pubkey }; + let gate = VaultGate::spawn(VaultGate::new(auth_creds, actors, db.clone(), promotion_tx)); - (db, session) + (db, gate, promotion_rx) } async fn client_dh_encrypt( - user_agent: &kameo::actor::ActorRef, + gate: &kameo::actor::ActorRef, key_to_send: &[u8], ) -> HandleUnsealEncryptedKey { let client_secret = EphemeralSecret::random(); let client_public = PublicKey::from(&client_secret); - let response = user_agent - .ask(HandleUnsealRequest { + let response = gate + .ask(HandleHandshake { client_pubkey: client_public, }) .await @@ -69,48 +82,47 @@ async fn client_dh_encrypt( #[tokio::test] #[test_log::test] -pub async fn test_unseal_success() { +async fn test_unseal_success() { let seal_key = b"test-seal-key"; - let (_db, user_agent) = setup_sealed_user_agent(seal_key).await; + let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await; - let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await; + let encrypted_key = client_dh_encrypt(&gate, seal_key).await; - let response = user_agent.ask(encrypted_key).await; + let response = gate.ask(encrypted_key).await; assert!(matches!(response, Ok(()))); } #[tokio::test] #[test_log::test] -pub async fn test_unseal_wrong_seal_key() { - let (_db, user_agent) = setup_sealed_user_agent(b"correct-key").await; +async fn test_unseal_wrong_seal_key() { + let (_db, gate, _promotion_rx) = setup_sealed_gate(b"correct-key").await; - let encrypted_key = client_dh_encrypt(&user_agent, b"wrong-key").await; + let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await; - let response = user_agent.ask(encrypted_key).await; + let response = gate.ask(encrypted_key).await; assert!(matches!( response, Err(kameo::error::SendError::HandlerError( - UnsealError::InvalidKey + VaultGateError::InvalidKey )) )); } #[tokio::test] #[test_log::test] -pub async fn test_unseal_corrupted_ciphertext() { - let (_db, user_agent) = setup_sealed_user_agent(b"test-key").await; +async fn test_unseal_corrupted_ciphertext() { + let (_db, gate, _promotion_rx) = setup_sealed_gate(b"test-key").await; let client_secret = EphemeralSecret::random(); let client_public = PublicKey::from(&client_secret); - user_agent - .ask(HandleUnsealRequest { - client_pubkey: client_public, - }) - .await - .unwrap(); + gate.ask(HandleHandshake { + client_pubkey: client_public, + }) + .await + .unwrap(); - let response = user_agent + let response = gate .ask(HandleUnsealEncryptedKey { nonce: vec![0u8; 24], ciphertext: vec![0u8; 32], @@ -121,33 +133,33 @@ pub async fn test_unseal_corrupted_ciphertext() { assert!(matches!( response, Err(kameo::error::SendError::HandlerError( - UnsealError::InvalidKey + VaultGateError::InvalidKey )) )); } #[tokio::test] #[test_log::test] -pub async fn test_unseal_retry_after_invalid_key() { +async fn test_unseal_retry_after_invalid_key() { let seal_key = b"real-seal-key"; - let (_db, user_agent) = setup_sealed_user_agent(seal_key).await; + let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await; { - let encrypted_key = client_dh_encrypt(&user_agent, b"wrong-key").await; + let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await; - let response = user_agent.ask(encrypted_key).await; + let response = gate.ask(encrypted_key).await; assert!(matches!( response, Err(kameo::error::SendError::HandlerError( - UnsealError::InvalidKey + VaultGateError::InvalidKey )) )); } { - let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await; + let encrypted_key = client_dh_encrypt(&gate, seal_key).await; - let response = user_agent.ask(encrypted_key).await; + let response = gate.ask(encrypted_key).await; assert!(matches!(response, Ok(()))); } } diff --git a/server/crates/arbiter-server/tests/vault.rs b/server/crates/arbiter-server/tests/vault.rs new file mode 100644 index 0000000..c7640a8 --- /dev/null +++ b/server/crates/arbiter-server/tests/vault.rs @@ -0,0 +1,8 @@ +mod common; + +#[path = "vault/concurrency.rs"] +mod concurrency; +#[path = "vault/lifecycle.rs"] +mod lifecycle; +#[path = "vault/storage.rs"] +mod storage; diff --git a/server/crates/arbiter-server/tests/keyholder/concurrency.rs b/server/crates/arbiter-server/tests/vault/concurrency.rs similarity index 90% rename from server/crates/arbiter-server/tests/keyholder/concurrency.rs rename to server/crates/arbiter-server/tests/vault/concurrency.rs index f128beb..ee84f4a 100644 --- a/server/crates/arbiter-server/tests/keyholder/concurrency.rs +++ b/server/crates/arbiter-server/tests/vault/concurrency.rs @@ -1,20 +1,21 @@ -use std::collections::{HashMap, HashSet}; - +use crate::common; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_server::{ - actors::keyholder::{CreateNew, Error, KeyHolder}, + actors::{ + GlobalActors, + vault::{CreateNew, Error, Vault}, + }, db::{self, models, schema}, }; use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::sql_query}; use diesel_async::RunQueryDsl; use kameo::actor::{ActorRef, Spawn as _}; +use std::collections::{HashMap, HashSet}; use tokio::task::JoinSet; -use crate::common; - async fn write_concurrently( - actor: ActorRef, + actor: ActorRef, prefix: &'static str, count: usize, ) -> Vec<(i32, Vec)> { @@ -44,7 +45,7 @@ async fn write_concurrently( #[test_log::test] async fn concurrent_create_new_no_duplicate_nonces_() { let db = db::create_test_pool().await; - let actor = KeyHolder::spawn(common::bootstrapped_keyholder(&db).await); + let actor = Vault::spawn(common::bootstrapped_vault(&db).await); let writes = write_concurrently(actor, "nonce-unique", 32).await; assert_eq!(writes.len(), 32); @@ -66,7 +67,7 @@ async fn concurrent_create_new_no_duplicate_nonces_() { #[test_log::test] async fn concurrent_create_new_root_nonce_never_moves_backward() { let db = db::create_test_pool().await; - let actor = KeyHolder::spawn(common::bootstrapped_keyholder(&db).await); + let actor = Vault::spawn(common::bootstrapped_vault(&db).await); write_concurrently(actor, "root-max", 24).await; @@ -94,7 +95,7 @@ async fn concurrent_create_new_root_nonce_never_moves_backward() { #[test_log::test] async fn insert_failure_does_not_create_partial_row() { let db = db::create_test_pool().await; - let mut actor = common::bootstrapped_keyholder(&db).await; + let mut actor = common::bootstrapped_vault(&db).await; let root_key_history_id = common::root_key_history_id(&db).await; let mut conn = db.get().await.unwrap(); @@ -156,12 +157,14 @@ async fn insert_failure_does_not_create_partial_row() { #[test_log::test] async fn decrypt_roundtrip_after_high_concurrency() { let db = db::create_test_pool().await; - let actor = KeyHolder::spawn(common::bootstrapped_keyholder(&db).await); + let actor = Vault::spawn(common::bootstrapped_vault(&db).await); let writes = write_concurrently(actor, "roundtrip", 40).await; let expected: HashMap> = writes.into_iter().collect(); - let mut decryptor = KeyHolder::new(db.clone()).await.unwrap(); + let mut decryptor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()) + .await + .unwrap(); decryptor .try_unseal(SafeCell::new(b"test-seal-key".to_vec())) .await diff --git a/server/crates/arbiter-server/tests/keyholder/lifecycle.rs b/server/crates/arbiter-server/tests/vault/lifecycle.rs similarity index 75% rename from server/crates/arbiter-server/tests/keyholder/lifecycle.rs rename to server/crates/arbiter-server/tests/vault/lifecycle.rs index bd50b6f..25017c4 100644 --- a/server/crates/arbiter-server/tests/keyholder/lifecycle.rs +++ b/server/crates/arbiter-server/tests/vault/lifecycle.rs @@ -1,6 +1,10 @@ +use crate::common; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_server::{ - actors::keyholder::{Error, KeyHolder}, + actors::{ + GlobalActors, + vault::{Error, Vault}, + }, crypto::encryption::v1::{Nonce, ROOT_KEY_TAG}, db::{self, models, schema}, }; @@ -8,13 +12,13 @@ use arbiter_server::{ use diesel::{QueryDsl, SelectableHelper}; use diesel_async::RunQueryDsl; -use crate::common; - #[tokio::test] #[test_log::test] async fn test_bootstrap() { let db = db::create_test_pool().await; - let mut actor = KeyHolder::new(db.clone()).await.unwrap(); + let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()) + .await + .unwrap(); let seal_key = SafeCell::new(b"test-seal-key".to_vec()); actor.bootstrap(seal_key).await.unwrap(); @@ -37,7 +41,7 @@ async fn test_bootstrap() { #[test_log::test] async fn test_bootstrap_rejects_double() { let db = db::create_test_pool().await; - let mut actor = common::bootstrapped_keyholder(&db).await; + let mut actor = common::bootstrapped_vault(&db).await; let seal_key2 = SafeCell::new(b"test-seal-key".to_vec()); let err = actor.bootstrap(seal_key2).await.unwrap_err(); @@ -48,7 +52,9 @@ async fn test_bootstrap_rejects_double() { #[test_log::test] async fn test_create_new_before_bootstrap_fails() { let db = db::create_test_pool().await; - let mut actor = KeyHolder::new(db).await.unwrap(); + let mut actor = Vault::new(db, GlobalActors::spawn_message_bus()) + .await + .unwrap(); let err = actor .create_new(SafeCell::new(b"data".to_vec())) @@ -61,7 +67,9 @@ async fn test_create_new_before_bootstrap_fails() { #[test_log::test] async fn test_decrypt_before_bootstrap_fails() { let db = db::create_test_pool().await; - let mut actor = KeyHolder::new(db).await.unwrap(); + let mut actor = Vault::new(db, GlobalActors::spawn_message_bus()) + .await + .unwrap(); let err = actor.decrypt(1).await.unwrap_err(); assert!(matches!(err, Error::NotBootstrapped)); @@ -71,19 +79,21 @@ async fn test_decrypt_before_bootstrap_fails() { #[test_log::test] async fn test_new_restores_sealed_state() { let db = db::create_test_pool().await; - let actor = common::bootstrapped_keyholder(&db).await; + let actor = common::bootstrapped_vault(&db).await; drop(actor); - let mut actor2 = KeyHolder::new(db).await.unwrap(); + let mut actor2 = Vault::new(db, GlobalActors::spawn_message_bus()) + .await + .unwrap(); let err = actor2.decrypt(1).await.unwrap_err(); - assert!(matches!(err, Error::NotBootstrapped)); + assert!(matches!(err, Error::Sealed)); } #[tokio::test] #[test_log::test] async fn test_unseal_correct_password() { let db = db::create_test_pool().await; - let mut actor = common::bootstrapped_keyholder(&db).await; + let mut actor = common::bootstrapped_vault(&db).await; let plaintext = b"survive a restart"; let aead_id = actor @@ -92,7 +102,9 @@ async fn test_unseal_correct_password() { .unwrap(); drop(actor); - let mut actor = KeyHolder::new(db.clone()).await.unwrap(); + let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()) + .await + .unwrap(); let seal_key = SafeCell::new(b"test-seal-key".to_vec()); actor.try_unseal(seal_key).await.unwrap(); @@ -104,7 +116,7 @@ async fn test_unseal_correct_password() { #[test_log::test] async fn test_unseal_wrong_then_correct_password() { let db = db::create_test_pool().await; - let mut actor = common::bootstrapped_keyholder(&db).await; + let mut actor = common::bootstrapped_vault(&db).await; let plaintext = b"important data"; let aead_id = actor @@ -113,7 +125,9 @@ async fn test_unseal_wrong_then_correct_password() { .unwrap(); drop(actor); - let mut actor = KeyHolder::new(db.clone()).await.unwrap(); + let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()) + .await + .unwrap(); let bad_key = SafeCell::new(b"wrong-password".to_vec()); let err = actor.try_unseal(bad_key).await.unwrap_err(); diff --git a/server/crates/arbiter-server/tests/keyholder/storage.rs b/server/crates/arbiter-server/tests/vault/storage.rs similarity index 91% rename from server/crates/arbiter-server/tests/keyholder/storage.rs rename to server/crates/arbiter-server/tests/vault/storage.rs index 71ebccf..391080f 100644 --- a/server/crates/arbiter-server/tests/keyholder/storage.rs +++ b/server/crates/arbiter-server/tests/vault/storage.rs @@ -1,22 +1,20 @@ -use std::collections::HashSet; - +use crate::common; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_server::{ - actors::keyholder::Error, + actors::vault::Error, crypto::encryption::v1::Nonce, db::{self, models, schema}, }; use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update}; use diesel_async::RunQueryDsl; - -use crate::common; +use std::collections::HashSet; #[tokio::test] #[test_log::test] async fn test_create_decrypt_roundtrip() { let db = db::create_test_pool().await; - let mut actor = common::bootstrapped_keyholder(&db).await; + let mut actor = common::bootstrapped_vault(&db).await; let plaintext = b"hello arbiter"; let aead_id = actor @@ -32,7 +30,7 @@ async fn test_create_decrypt_roundtrip() { #[test_log::test] async fn test_decrypt_nonexistent_returns_not_found() { let db = db::create_test_pool().await; - let mut actor = common::bootstrapped_keyholder(&db).await; + let mut actor = common::bootstrapped_vault(&db).await; let err = actor.decrypt(9999).await.unwrap_err(); assert!(matches!(err, Error::NotFound)); @@ -42,7 +40,7 @@ async fn test_decrypt_nonexistent_returns_not_found() { #[test_log::test] async fn test_ciphertext_differs_across_entries() { let db = db::create_test_pool().await; - let mut actor = common::bootstrapped_keyholder(&db).await; + let mut actor = common::bootstrapped_vault(&db).await; let plaintext = b"same content"; let id1 = actor @@ -80,7 +78,7 @@ async fn test_ciphertext_differs_across_entries() { #[test_log::test] async fn test_nonce_never_reused() { let db = db::create_test_pool().await; - let mut actor = common::bootstrapped_keyholder(&db).await; + let mut actor = common::bootstrapped_vault(&db).await; let n = 5; for i in 0..n { @@ -124,7 +122,7 @@ async fn test_nonce_never_reused() { #[test_log::test] async fn broken_db_nonce_format_fails_closed() { let db = db::create_test_pool().await; - let mut actor = common::bootstrapped_keyholder(&db).await; + let mut actor = common::bootstrapped_vault(&db).await; let root_key_history_id = common::root_key_history_id(&db).await; let mut conn = db.get().await.unwrap(); @@ -145,7 +143,7 @@ async fn broken_db_nonce_format_fails_closed() { assert!(matches!(err, Error::BrokenDatabase)); let db = db::create_test_pool().await; - let mut actor = common::bootstrapped_keyholder(&db).await; + let mut actor = common::bootstrapped_vault(&db).await; let id = actor .create_new(SafeCell::new(b"decrypt target".to_vec())) .await diff --git a/useragent/flutter_rust_bridge.yaml b/useragent/flutter_rust_bridge.yaml new file mode 100644 index 0000000..e15ed91 --- /dev/null +++ b/useragent/flutter_rust_bridge.yaml @@ -0,0 +1,3 @@ +rust_input: crate::api +rust_root: rust/ +dart_output: lib/src/rust \ No newline at end of file diff --git a/useragent/lib/features/callouts/callout_event.dart b/useragent/lib/features/callouts/callout_event.dart index 32b90dc..99e943b 100644 --- a/useragent/lib/features/callouts/callout_event.dart +++ b/useragent/lib/features/callouts/callout_event.dart @@ -18,7 +18,6 @@ sealed class CalloutEvent with _$CalloutEvent { required CalloutData data, }) = CalloutEventAdded; - const factory CalloutEvent.cancelled({ - required String id, - }) = CalloutEventCancelled; + const factory CalloutEvent.cancelled({required String id}) = + CalloutEventCancelled; } diff --git a/useragent/lib/features/callouts/show_callout.dart b/useragent/lib/features/callouts/show_callout.dart index 5fd8ac0..77e3a48 100644 --- a/useragent/lib/features/callouts/show_callout.dart +++ b/useragent/lib/features/callouts/show_callout.dart @@ -14,11 +14,8 @@ Future showCallout(BuildContext context, WidgetRef ref, String id) async { barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, barrierColor: Colors.transparent, transitionDuration: const Duration(milliseconds: 320), - pageBuilder: (_, animation, _) => _CalloutOverlay( - id: id, - data: data, - animation: animation, - ), + pageBuilder: (_, animation, _) => + _CalloutOverlay(id: id, data: data, animation: animation), ); } @@ -35,22 +32,25 @@ class _CalloutOverlay extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - ref.listen( - calloutManagerProvider.select((map) => map.containsKey(id)), - (wasPresent, isPresent) { - if (wasPresent == true && !isPresent && context.mounted) { - Navigator.of(context).pop(); - } - }, - ); + ref.listen(calloutManagerProvider.select((map) => map.containsKey(id)), ( + wasPresent, + isPresent, + ) { + if (wasPresent == true && !isPresent && context.mounted) { + Navigator.of(context).pop(); + } + }); final content = switch (data) { - ConnectApprovalData(:final pubkey, :final clientInfo) => SdkConnectCallout( - pubkey: pubkey, - clientInfo: clientInfo, - onAccept: () => ref.read(calloutManagerProvider.notifier).sendDecision(id, true), - onDecline: () => ref.read(calloutManagerProvider.notifier).sendDecision(id, false), - ), + ConnectApprovalData(:final pubkey, :final clientInfo) => + SdkConnectCallout( + pubkey: pubkey, + clientInfo: clientInfo, + onAccept: () => + ref.read(calloutManagerProvider.notifier).sendDecision(id, true), + onDecline: () => + ref.read(calloutManagerProvider.notifier).sendDecision(id, false), + ), }; final barrierAnim = CurvedAnimation( diff --git a/useragent/lib/features/callouts/show_callout_list.dart b/useragent/lib/features/callouts/show_callout_list.dart index de235b8..2e72491 100644 --- a/useragent/lib/features/callouts/show_callout_list.dart +++ b/useragent/lib/features/callouts/show_callout_list.dart @@ -14,7 +14,8 @@ Future showCalloutList(BuildContext context, WidgetRef ref) async { barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, barrierColor: Colors.transparent, transitionDuration: const Duration(milliseconds: 280), - pageBuilder: (_, animation, __) => _CalloutListOverlay(animation: animation), + pageBuilder: (_, animation, __) => + _CalloutListOverlay(animation: animation), ); if (selectedId != null && context.mounted) { @@ -51,7 +52,9 @@ class _CalloutListOverlay extends ConsumerWidget { child: AnimatedBuilder( animation: barrierAnim, builder: (_, __) => ColoredBox( - color: Colors.black.withValues(alpha: 0.35 * barrierAnim.value), + color: Colors.black.withValues( + alpha: 0.35 * barrierAnim.value, + ), ), ), ), diff --git a/useragent/lib/features/connection/arbiter_url.dart b/useragent/lib/features/connection/arbiter_url.dart index d71d236..b6edbdf 100644 --- a/useragent/lib/features/connection/arbiter_url.dart +++ b/useragent/lib/features/connection/arbiter_url.dart @@ -50,7 +50,9 @@ class ArbiterUrl { try { return base64Url.decode(base64Url.normalize(cert)); } on FormatException catch (error) { - throw FormatException("Invalid base64 in 'cert' query parameter: ${error.message}"); + throw FormatException( + "Invalid base64 in 'cert' query parameter: ${error.message}", + ); } } } diff --git a/useragent/lib/features/connection/auth.dart b/useragent/lib/features/connection/auth.dart index cd512d7..84cb3d9 100644 --- a/useragent/lib/features/connection/auth.dart +++ b/useragent/lib/features/connection/auth.dart @@ -7,6 +7,7 @@ import 'package:arbiter/features/identity/pk_manager.dart'; import 'package:arbiter/proto/arbiter.pbgrpc.dart'; import 'package:arbiter/proto/user_agent/auth.pb.dart' as ua_auth; import 'package:arbiter/proto/user_agent.pb.dart'; +import 'package:arbiter/src/rust/api.dart'; import 'package:grpc/grpc.dart'; import 'package:mtcore/markettakers.dart'; @@ -61,11 +62,6 @@ Future connectAndAuthorize( final req = ua_auth.AuthChallengeRequest( pubkey: pubkey, bootstrapToken: bootstrapToken, - keyType: switch (key.alg) { - KeyAlgorithm.rsa => ua_auth.KeyType.KEY_TYPE_RSA, - KeyAlgorithm.ecdsa => ua_auth.KeyType.KEY_TYPE_ECDSA_SECP256K1, - KeyAlgorithm.ed25519 => ua_auth.KeyType.KEY_TYPE_ED25519, - }, ); final response = await connection.ask( UserAgentRequest(auth: ua_auth.Request(challengeRequest: req)), @@ -97,7 +93,10 @@ Future connectAndAuthorize( ); } - final challenge = _formatChallenge(authResponse.challenge, pubkey); + final challenge = await formatChallenge( + random: authResponse.challenge.random, + timestamp: authResponse.challenge.timestampNanos.toInt(), + ); talker.info( 'Received auth challenge, signing with key ${base64Encode(pubkey)}', ); @@ -106,7 +105,9 @@ Future connectAndAuthorize( final solutionResponse = await connection.ask( UserAgentRequest( auth: ua_auth.Request( - challengeSolution: ua_auth.AuthChallengeSolution(signature: signature), + challengeSolution: ua_auth.AuthChallengeSolution( + signature: signature, + ), ), ), ); @@ -167,9 +168,3 @@ Future _connect(StoredServerInfo serverInfo) async { return Connection(channel: channel, tx: tx, rx: rx); } - -List _formatChallenge(ua_auth.AuthChallenge challenge, List pubkey) { - final encodedPubkey = base64Encode(pubkey); - final payload = "${challenge.nonce}:$encodedPubkey"; - return utf8.encode(payload); -} diff --git a/useragent/lib/features/connection/connection.dart b/useragent/lib/features/connection/connection.dart index 9427c83..245bb30 100644 --- a/useragent/lib/features/connection/connection.dart +++ b/useragent/lib/features/connection/connection.dart @@ -85,7 +85,9 @@ class Connection { if (response.hasId()) { final completer = _pendingRequests.remove(response.id); if (completer == null) { - talker.warning('Received response for unknown request id ${response.id}'); + talker.warning( + 'Received response for unknown request id ${response.id}', + ); return; } completer.complete(response); diff --git a/useragent/lib/features/connection/evm.dart b/useragent/lib/features/connection/evm.dart index f08fc90..0b645c0 100644 --- a/useragent/lib/features/connection/evm.dart +++ b/useragent/lib/features/connection/evm.dart @@ -9,9 +9,7 @@ Future> listEvmWallets(Connection connection) async { UserAgentRequest(evm: ua_evm.Request(walletList: Empty())), ); if (!response.hasEvm()) { - throw Exception( - 'Expected EVM response, got ${response.whichPayload()}', - ); + throw Exception('Expected EVM response, got ${response.whichPayload()}'); } final evmResponse = response.evm; @@ -37,9 +35,7 @@ Future createEvmWallet(Connection connection) async { UserAgentRequest(evm: ua_evm.Request(walletCreate: Empty())), ); if (!response.hasEvm()) { - throw Exception( - 'Expected EVM response, got ${response.whichPayload()}', - ); + throw Exception('Expected EVM response, got ${response.whichPayload()}'); } final evmResponse = response.evm; diff --git a/useragent/lib/features/connection/evm/grants.dart b/useragent/lib/features/connection/evm/grants.dart index 4e51a58..7790e83 100644 --- a/useragent/lib/features/connection/evm/grants.dart +++ b/useragent/lib/features/connection/evm/grants.dart @@ -10,9 +10,7 @@ Future> listEvmGrants(Connection connection) async { UserAgentRequest(evm: ua_evm.Request(grantList: request)), ); if (!response.hasEvm()) { - throw Exception( - 'Expected EVM response, got ${response.whichPayload()}', - ); + throw Exception('Expected EVM response, got ${response.whichPayload()}'); } final evmResponse = response.evm; @@ -50,9 +48,7 @@ Future createEvmGrant( final resp = await connection.ask(request); if (!resp.hasEvm()) { - throw Exception( - 'Expected EVM response, got ${resp.whichPayload()}', - ); + throw Exception('Expected EVM response, got ${resp.whichPayload()}'); } final evmResponse = resp.evm; @@ -70,15 +66,11 @@ Future createEvmGrant( Future deleteEvmGrant(Connection connection, int grantId) async { final response = await connection.ask( UserAgentRequest( - evm: ua_evm.Request( - grantDelete: EvmGrantDeleteRequest(grantId: grantId), - ), + evm: ua_evm.Request(grantDelete: EvmGrantDeleteRequest(grantId: grantId)), ), ); if (!response.hasEvm()) { - throw Exception( - 'Expected EVM response, got ${response.whichPayload()}', - ); + throw Exception('Expected EVM response, got ${response.whichPayload()}'); } final evmResponse = response.evm; diff --git a/useragent/lib/features/connection/evm/wallet_access.dart b/useragent/lib/features/connection/evm/wallet_access.dart index 2dcdea0..235948a 100644 --- a/useragent/lib/features/connection/evm/wallet_access.dart +++ b/useragent/lib/features/connection/evm/wallet_access.dart @@ -8,9 +8,7 @@ Future> readClientWalletAccess( required int clientId, }) async { final response = await connection.ask( - UserAgentRequest( - sdkClient: ua_sdk.Request(listWalletAccess: Empty()), - ), + UserAgentRequest(sdkClient: ua_sdk.Request(listWalletAccess: Empty())), ); if (!response.hasSdkClient()) { throw Exception( @@ -33,9 +31,7 @@ Future> listAllWalletAccesses( Connection connection, ) async { final response = await connection.ask( - UserAgentRequest( - sdkClient: ua_sdk.Request(listWalletAccess: Empty()), - ), + UserAgentRequest(sdkClient: ua_sdk.Request(listWalletAccess: Empty())), ); if (!response.hasSdkClient()) { throw Exception( @@ -81,9 +77,7 @@ Future writeClientWalletAccess( UserAgentRequest( sdkClient: ua_sdk.Request( revokeWalletAccess: ua_sdk.RevokeWalletAccess( - accesses: [ - for (final walletId in toRevoke) walletId, - ], + accesses: [for (final walletId in toRevoke) walletId], ), ), ), diff --git a/useragent/lib/features/connection/server_info_storage.dart b/useragent/lib/features/connection/server_info_storage.dart index d84ca52..27ed5ad 100644 --- a/useragent/lib/features/connection/server_info_storage.dart +++ b/useragent/lib/features/connection/server_info_storage.dart @@ -17,9 +17,9 @@ class StoredServerInfo { final int port; final String caCertFingerprint; - factory StoredServerInfo.fromJson(Map json) => _$StoredServerInfoFromJson(json); + factory StoredServerInfo.fromJson(Map json) => + _$StoredServerInfoFromJson(json); Map toJson() => _$StoredServerInfoToJson(this); - } abstract class ServerInfoStorage { diff --git a/useragent/lib/features/connection/vault.dart b/useragent/lib/features/connection/vault.dart index 4f02f9f..79bc6a1 100644 --- a/useragent/lib/features/connection/vault.dart +++ b/useragent/lib/features/connection/vault.dart @@ -1,5 +1,6 @@ import 'package:arbiter/features/connection/connection.dart'; -import 'package:arbiter/proto/user_agent/vault/bootstrap.pb.dart' as ua_bootstrap; +import 'package:arbiter/proto/user_agent/vault/bootstrap.pb.dart' + as ua_bootstrap; import 'package:arbiter/proto/user_agent/vault/unseal.pb.dart' as ua_unseal; import 'package:arbiter/proto/user_agent/vault/vault.pb.dart' as ua_vault; import 'package:arbiter/proto/user_agent.pb.dart'; @@ -27,9 +28,7 @@ Future bootstrapVault( ), ); if (!response.hasVault()) { - throw Exception( - 'Expected vault response, got ${response.whichPayload()}', - ); + throw Exception('Expected vault response, got ${response.whichPayload()}'); } final vaultResponse = response.vault; diff --git a/useragent/lib/features/identity/hazmat_mldsa.dart b/useragent/lib/features/identity/hazmat_mldsa.dart new file mode 100644 index 0000000..682a69b --- /dev/null +++ b/useragent/lib/features/identity/hazmat_mldsa.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; + +import 'package:arbiter/src/rust/api.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:arbiter/features/identity/pk_manager.dart'; + +final storage = FlutterSecureStorage( + aOptions: AndroidOptions.biometric( + enforceBiometrics: true, + biometricPromptTitle: 'Authentication Required', + ), + mOptions: MacOsOptions( + accessibility: KeychainAccessibility.unlocked_this_device, + label: "Arbiter", + description: "Confirm your identity to access vault", + synchronizable: false, + accessControlFlags: [AccessControlFlag.userPresence], + usesDataProtectionKeychain: true, + ), +); + +class HazmatMldsa extends KeyHandle { + final MldsaKey _key; + + HazmatMldsa({required MldsaKey key}) : _key = key; + + @override + Future> getPublicKey() async { + final publicKey = await _key.getPublicKey(); + return publicKey; + } + + @override + Future> sign(List data) async { + final signature = await _key.sign(message: data); + return signature; + } +} + +class HazmatMLDSAManager extends KeyManager { + static const _storageKey = "ed25519_identity"; + + @override + Future create() async { + final storedKey = await get(); + if (storedKey != null) { + return storedKey; + } + + final newKeypair = await MldsaKey.generate(); + final keyBytes = await newKeypair.toBytes(); + + await storage.write(key: _storageKey, value: base64Encode(keyBytes)); + + return HazmatMldsa(key: newKeypair); + } + + @override + Future get() async { + final storedKeyPair = await storage.read(key: _storageKey); + if (storedKeyPair == null) { + return null; + } + + final keyBytes = base64Decode(storedKeyPair); + final key = await MldsaKey.fromBytes(bytes: keyBytes); + + return HazmatMldsa(key: key); + } +} diff --git a/useragent/lib/features/identity/pk_manager.dart b/useragent/lib/features/identity/pk_manager.dart index ff6db23..d0ecb9f 100644 --- a/useragent/lib/features/identity/pk_manager.dart +++ b/useragent/lib/features/identity/pk_manager.dart @@ -1,11 +1,6 @@ -enum KeyAlgorithm { - rsa, ecdsa, ed25519 -} - -// The API to handle without storing the private key in memory. +// The API to handle without storing the private key in memory. //The implementation will use platform-specific secure storage and signing capabilities. abstract class KeyHandle { - KeyAlgorithm get alg; Future> sign(List data); Future> getPublicKey(); } diff --git a/useragent/lib/features/identity/simple_ed25519.dart b/useragent/lib/features/identity/simple_ed25519.dart deleted file mode 100644 index 01ee24d..0000000 --- a/useragent/lib/features/identity/simple_ed25519.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'dart:convert'; - -import 'package:cryptography/cryptography.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:arbiter/features/identity/pk_manager.dart'; - -final storage = FlutterSecureStorage( - aOptions: AndroidOptions.biometric( - enforceBiometrics: true, - biometricPromptTitle: 'Authentication Required', - ), - mOptions: MacOsOptions( - accessibility: KeychainAccessibility.unlocked_this_device, - label: "Arbiter", - description: "Confirm your identity to access vault", - synchronizable: false, - accessControlFlags: [ - AccessControlFlag.userPresence, - ], - usesDataProtectionKeychain: true, - ), -); - -final processor = Ed25519(); - -class SimpleEd25519 extends KeyHandle { - final SimpleKeyPair _keyPair; - - SimpleEd25519({required SimpleKeyPair keyPair}) : _keyPair = keyPair; - - @override - KeyAlgorithm get alg => KeyAlgorithm.ed25519; - - @override - Future> getPublicKey() async { - final publicKey = await _keyPair.extractPublicKey(); - return publicKey.bytes; - } - - @override - Future> sign(List data) async { - final signature = await processor.sign(data, keyPair: _keyPair); - return signature.bytes; - } -} - -class SimpleEd25519Manager extends KeyManager { - static const _storageKey = "ed25519_identity"; - static const _storagePublicKey = "ed25519_public_key"; - - @override - Future create() async { - final storedKey = await get(); - if (storedKey != null) { - return storedKey; - } - - final newKey = await processor.newKeyPair(); - final rawKey = await newKey.extract(); - - final keyData = base64Encode(rawKey.bytes); - await storage.write(key: _storageKey, value: keyData); - - final publicKeyData = base64Encode(rawKey.publicKey.bytes); - await storage.write(key: _storagePublicKey, value: publicKeyData); - - return SimpleEd25519(keyPair: newKey); - } - - @override - Future get() async { - final storedKeyPair = await storage.read(key: _storageKey); - if (storedKeyPair == null) { - return null; - } - - final publicKeyData = await storage.read(key: _storagePublicKey); - final publicKeyRaw = base64Decode(publicKeyData!); - final publicKey = SimplePublicKey( - publicKeyRaw, - type: processor.keyPairType, - ); - - final keyBytes = base64Decode(storedKeyPair); - final keypair = SimpleKeyPairData( - keyBytes, - publicKey: publicKey, - type: processor.keyPairType, - ); - - return SimpleEd25519(keyPair: keypair); - } -} diff --git a/useragent/lib/main.dart b/useragent/lib/main.dart index 5fac968..d361cdb 100644 --- a/useragent/lib/main.dart +++ b/useragent/lib/main.dart @@ -1,10 +1,12 @@ import 'package:arbiter/router.dart'; +import 'package:arbiter/src/rust/frb_generated.dart'; import 'package:flutter/material.dart' hide Router; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:sizer/sizer.dart'; -void main() { +Future main() async { WidgetsFlutterBinding.ensureInitialized(); + await RustLib.init(); runApp(const ProviderScope(child: App())); } @@ -33,3 +35,5 @@ class _AppState extends State { ); } } + + diff --git a/useragent/lib/proto/client/auth.pb.dart b/useragent/lib/proto/client/auth.pb.dart index 98eaab1..ae19314 100644 --- a/useragent/lib/proto/client/auth.pb.dart +++ b/useragent/lib/proto/client/auth.pb.dart @@ -12,6 +12,7 @@ import 'dart:core' as $core; +import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:protobuf/protobuf.dart' as $pb; import '../shared/client.pb.dart' as $0; @@ -94,12 +95,12 @@ class AuthChallengeRequest extends $pb.GeneratedMessage { class AuthChallenge extends $pb.GeneratedMessage { factory AuthChallenge({ - $core.List<$core.int>? pubkey, - $core.int? nonce, + $fixnum.Int64? timestampNanos, + $core.List<$core.int>? random, }) { final result = create(); - if (pubkey != null) result.pubkey = pubkey; - if (nonce != null) result.nonce = nonce; + if (timestampNanos != null) result.timestampNanos = timestampNanos; + if (random != null) result.random = random; return result; } @@ -117,9 +118,11 @@ class AuthChallenge extends $pb.GeneratedMessage { package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client.auth'), createEmptyInstance: create) + ..a<$fixnum.Int64>( + 1, _omitFieldNames ? '' : 'timestampNanos', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) ..a<$core.List<$core.int>>( - 1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY) - ..aI(2, _omitFieldNames ? '' : 'nonce') + 2, _omitFieldNames ? '' : 'random', $pb.PbFieldType.OY) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -142,22 +145,22 @@ class AuthChallenge extends $pb.GeneratedMessage { static AuthChallenge? _defaultInstance; @$pb.TagNumber(1) - $core.List<$core.int> get pubkey => $_getN(0); + $fixnum.Int64 get timestampNanos => $_getI64(0); @$pb.TagNumber(1) - set pubkey($core.List<$core.int> value) => $_setBytes(0, value); + set timestampNanos($fixnum.Int64 value) => $_setInt64(0, value); @$pb.TagNumber(1) - $core.bool hasPubkey() => $_has(0); + $core.bool hasTimestampNanos() => $_has(0); @$pb.TagNumber(1) - void clearPubkey() => $_clearField(1); + void clearTimestampNanos() => $_clearField(1); @$pb.TagNumber(2) - $core.int get nonce => $_getIZ(1); + $core.List<$core.int> get random => $_getN(1); @$pb.TagNumber(2) - set nonce($core.int value) => $_setSignedInt32(1, value); + set random($core.List<$core.int> value) => $_setBytes(1, value); @$pb.TagNumber(2) - $core.bool hasNonce() => $_has(1); + $core.bool hasRandom() => $_has(1); @$pb.TagNumber(2) - void clearNonce() => $_clearField(2); + void clearRandom() => $_clearField(2); } class AuthChallengeSolution extends $pb.GeneratedMessage { diff --git a/useragent/lib/proto/client/auth.pbjson.dart b/useragent/lib/proto/client/auth.pbjson.dart index c9cf99e..c7b42b7 100644 --- a/useragent/lib/proto/client/auth.pbjson.dart +++ b/useragent/lib/proto/client/auth.pbjson.dart @@ -62,15 +62,15 @@ final $typed_data.Uint8List authChallengeRequestDescriptor = $convert.base64Deco const AuthChallenge$json = { '1': 'AuthChallenge', '2': [ - {'1': 'pubkey', '3': 1, '4': 1, '5': 12, '10': 'pubkey'}, - {'1': 'nonce', '3': 2, '4': 1, '5': 5, '10': 'nonce'}, + {'1': 'timestamp_nanos', '3': 1, '4': 1, '5': 4, '10': 'timestampNanos'}, + {'1': 'random', '3': 2, '4': 1, '5': 12, '10': 'random'}, ], }; /// Descriptor for `AuthChallenge`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List authChallengeDescriptor = $convert.base64Decode( - 'Cg1BdXRoQ2hhbGxlbmdlEhYKBnB1YmtleRgBIAEoDFIGcHVia2V5EhQKBW5vbmNlGAIgASgFUg' - 'Vub25jZQ=='); + 'Cg1BdXRoQ2hhbGxlbmdlEicKD3RpbWVzdGFtcF9uYW5vcxgBIAEoBFIOdGltZXN0YW1wTmFub3' + 'MSFgoGcmFuZG9tGAIgASgMUgZyYW5kb20='); @$core.Deprecated('Use authChallengeSolutionDescriptor instead') const AuthChallengeSolution$json = { diff --git a/useragent/lib/proto/shared/evm.pb.dart b/useragent/lib/proto/shared/evm.pb.dart index 4857f1b..cf2b7f9 100644 --- a/useragent/lib/proto/shared/evm.pb.dart +++ b/useragent/lib/proto/shared/evm.pb.dart @@ -414,6 +414,79 @@ class GasLimitExceededViolation extends $pb.GeneratedMessage { void clearMaxPriorityFeePerGas() => $_clearField(2); } +class EvalViolation_ChainIdMismatch extends $pb.GeneratedMessage { + factory EvalViolation_ChainIdMismatch({ + $fixnum.Int64? expected, + $fixnum.Int64? actual, + }) { + final result = create(); + if (expected != null) result.expected = expected; + if (actual != null) result.actual = actual; + return result; + } + + EvalViolation_ChainIdMismatch._(); + + factory EvalViolation_ChainIdMismatch.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory EvalViolation_ChainIdMismatch.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'EvalViolation.ChainIdMismatch', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.shared.evm'), + createEmptyInstance: create) + ..a<$fixnum.Int64>( + 1, _omitFieldNames ? '' : 'expected', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'actual', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + EvalViolation_ChainIdMismatch clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + EvalViolation_ChainIdMismatch copyWith( + void Function(EvalViolation_ChainIdMismatch) updates) => + super.copyWith( + (message) => updates(message as EvalViolation_ChainIdMismatch)) + as EvalViolation_ChainIdMismatch; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EvalViolation_ChainIdMismatch create() => + EvalViolation_ChainIdMismatch._(); + @$core.override + EvalViolation_ChainIdMismatch createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static EvalViolation_ChainIdMismatch getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static EvalViolation_ChainIdMismatch? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get expected => $_getI64(0); + @$pb.TagNumber(1) + set expected($fixnum.Int64 value) => $_setInt64(0, value); + @$pb.TagNumber(1) + $core.bool hasExpected() => $_has(0); + @$pb.TagNumber(1) + void clearExpected() => $_clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get actual => $_getI64(1); + @$pb.TagNumber(2) + set actual($fixnum.Int64 value) => $_setInt64(1, value); + @$pb.TagNumber(2) + $core.bool hasActual() => $_has(1); + @$pb.TagNumber(2) + void clearActual() => $_clearField(2); +} + enum EvalViolation_Kind { invalidTarget, gasLimitExceeded, @@ -421,6 +494,7 @@ enum EvalViolation_Kind { volumetricLimitExceeded, invalidTime, invalidTransactionType, + chainIdMismatch, notSet } @@ -432,6 +506,7 @@ class EvalViolation extends $pb.GeneratedMessage { $0.Empty? volumetricLimitExceeded, $0.Empty? invalidTime, $0.Empty? invalidTransactionType, + EvalViolation_ChainIdMismatch? chainIdMismatch, }) { final result = create(); if (invalidTarget != null) result.invalidTarget = invalidTarget; @@ -442,6 +517,7 @@ class EvalViolation extends $pb.GeneratedMessage { if (invalidTime != null) result.invalidTime = invalidTime; if (invalidTransactionType != null) result.invalidTransactionType = invalidTransactionType; + if (chainIdMismatch != null) result.chainIdMismatch = chainIdMismatch; return result; } @@ -462,6 +538,7 @@ class EvalViolation extends $pb.GeneratedMessage { 4: EvalViolation_Kind.volumetricLimitExceeded, 5: EvalViolation_Kind.invalidTime, 6: EvalViolation_Kind.invalidTransactionType, + 7: EvalViolation_Kind.chainIdMismatch, 0: EvalViolation_Kind.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( @@ -469,7 +546,7 @@ class EvalViolation extends $pb.GeneratedMessage { package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.shared.evm'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4, 5, 6]) + ..oo(0, [1, 2, 3, 4, 5, 6, 7]) ..a<$core.List<$core.int>>( 1, _omitFieldNames ? '' : 'invalidTarget', $pb.PbFieldType.OY) ..aOM( @@ -483,6 +560,9 @@ class EvalViolation extends $pb.GeneratedMessage { subBuilder: $0.Empty.create) ..aOM<$0.Empty>(6, _omitFieldNames ? '' : 'invalidTransactionType', subBuilder: $0.Empty.create) + ..aOM( + 7, _omitFieldNames ? '' : 'chainIdMismatch', + subBuilder: EvalViolation_ChainIdMismatch.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -510,6 +590,7 @@ class EvalViolation extends $pb.GeneratedMessage { @$pb.TagNumber(4) @$pb.TagNumber(5) @$pb.TagNumber(6) + @$pb.TagNumber(7) EvalViolation_Kind whichKind() => _EvalViolation_KindByTag[$_whichOneof(0)]!; @$pb.TagNumber(1) @$pb.TagNumber(2) @@ -517,6 +598,7 @@ class EvalViolation extends $pb.GeneratedMessage { @$pb.TagNumber(4) @$pb.TagNumber(5) @$pb.TagNumber(6) + @$pb.TagNumber(7) void clearKind() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -582,6 +664,18 @@ class EvalViolation extends $pb.GeneratedMessage { void clearInvalidTransactionType() => $_clearField(6); @$pb.TagNumber(6) $0.Empty ensureInvalidTransactionType() => $_ensure(5); + + @$pb.TagNumber(7) + EvalViolation_ChainIdMismatch get chainIdMismatch => $_getN(6); + @$pb.TagNumber(7) + set chainIdMismatch(EvalViolation_ChainIdMismatch value) => + $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasChainIdMismatch() => $_has(6); + @$pb.TagNumber(7) + void clearChainIdMismatch() => $_clearField(7); + @$pb.TagNumber(7) + EvalViolation_ChainIdMismatch ensureChainIdMismatch() => $_ensure(6); } /// Transaction was classified but no grant covers it diff --git a/useragent/lib/proto/shared/evm.pbjson.dart b/useragent/lib/proto/shared/evm.pbjson.dart index 3ae3545..77c31ec 100644 --- a/useragent/lib/proto/shared/evm.pbjson.dart +++ b/useragent/lib/proto/shared/evm.pbjson.dart @@ -195,12 +195,31 @@ const EvalViolation$json = { '9': 0, '10': 'invalidTransactionType' }, + { + '1': 'chain_id_mismatch', + '3': 7, + '4': 1, + '5': 11, + '6': '.arbiter.shared.evm.EvalViolation.ChainIdMismatch', + '9': 0, + '10': 'chainIdMismatch' + }, ], + '3': [EvalViolation_ChainIdMismatch$json], '8': [ {'1': 'kind'}, ], }; +@$core.Deprecated('Use evalViolationDescriptor instead') +const EvalViolation_ChainIdMismatch$json = { + '1': 'ChainIdMismatch', + '2': [ + {'1': 'expected', '3': 1, '4': 1, '5': 4, '10': 'expected'}, + {'1': 'actual', '3': 2, '4': 1, '5': 4, '10': 'actual'}, + ], +}; + /// Descriptor for `EvalViolation`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List evalViolationDescriptor = $convert.base64Decode( 'Cg1FdmFsVmlvbGF0aW9uEicKDmludmFsaWRfdGFyZ2V0GAEgASgMSABSDWludmFsaWRUYXJnZX' @@ -211,7 +230,10 @@ final $typed_data.Uint8List evalViolationDescriptor = $convert.base64Decode( 'YuRW1wdHlIAFIXdm9sdW1ldHJpY0xpbWl0RXhjZWVkZWQSOwoMaW52YWxpZF90aW1lGAUgASgL' 'MhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5SABSC2ludmFsaWRUaW1lElIKGGludmFsaWRfdHJhbn' 'NhY3Rpb25fdHlwZRgGIAEoCzIWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eUgAUhZpbnZhbGlkVHJh' - 'bnNhY3Rpb25UeXBlQgYKBGtpbmQ='); + 'bnNhY3Rpb25UeXBlEl8KEWNoYWluX2lkX21pc21hdGNoGAcgASgLMjEuYXJiaXRlci5zaGFyZW' + 'QuZXZtLkV2YWxWaW9sYXRpb24uQ2hhaW5JZE1pc21hdGNoSABSD2NoYWluSWRNaXNtYXRjaBpF' + 'Cg9DaGFpbklkTWlzbWF0Y2gSGgoIZXhwZWN0ZWQYASABKARSCGV4cGVjdGVkEhYKBmFjdHVhbB' + 'gCIAEoBFIGYWN0dWFsQgYKBGtpbmQ='); @$core.Deprecated('Use noMatchingGrantErrorDescriptor instead') const NoMatchingGrantError$json = { diff --git a/useragent/lib/proto/user_agent/auth.pb.dart b/useragent/lib/proto/user_agent/auth.pb.dart index 8bfea1e..c89b8c5 100644 --- a/useragent/lib/proto/user_agent/auth.pb.dart +++ b/useragent/lib/proto/user_agent/auth.pb.dart @@ -12,6 +12,7 @@ import 'dart:core' as $core; +import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:protobuf/protobuf.dart' as $pb; import 'auth.pbenum.dart'; @@ -24,12 +25,10 @@ class AuthChallengeRequest extends $pb.GeneratedMessage { factory AuthChallengeRequest({ $core.List<$core.int>? pubkey, $core.String? bootstrapToken, - KeyType? keyType, }) { final result = create(); if (pubkey != null) result.pubkey = pubkey; if (bootstrapToken != null) result.bootstrapToken = bootstrapToken; - if (keyType != null) result.keyType = keyType; return result; } @@ -50,8 +49,6 @@ class AuthChallengeRequest extends $pb.GeneratedMessage { ..a<$core.List<$core.int>>( 1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY) ..aOS(2, _omitFieldNames ? '' : 'bootstrapToken') - ..aE(3, _omitFieldNames ? '' : 'keyType', - enumValues: KeyType.values) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -90,23 +87,16 @@ class AuthChallengeRequest extends $pb.GeneratedMessage { $core.bool hasBootstrapToken() => $_has(1); @$pb.TagNumber(2) void clearBootstrapToken() => $_clearField(2); - - @$pb.TagNumber(3) - KeyType get keyType => $_getN(2); - @$pb.TagNumber(3) - set keyType(KeyType value) => $_setField(3, value); - @$pb.TagNumber(3) - $core.bool hasKeyType() => $_has(2); - @$pb.TagNumber(3) - void clearKeyType() => $_clearField(3); } class AuthChallenge extends $pb.GeneratedMessage { factory AuthChallenge({ - $core.int? nonce, + $fixnum.Int64? timestampNanos, + $core.List<$core.int>? random, }) { final result = create(); - if (nonce != null) result.nonce = nonce; + if (timestampNanos != null) result.timestampNanos = timestampNanos; + if (random != null) result.random = random; return result; } @@ -124,7 +114,11 @@ class AuthChallenge extends $pb.GeneratedMessage { package: const $pb.PackageName( _omitMessageNames ? '' : 'arbiter.user_agent.auth'), createEmptyInstance: create) - ..aI(1, _omitFieldNames ? '' : 'nonce') + ..a<$fixnum.Int64>( + 1, _omitFieldNames ? '' : 'timestampNanos', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'random', $pb.PbFieldType.OY) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -147,13 +141,22 @@ class AuthChallenge extends $pb.GeneratedMessage { static AuthChallenge? _defaultInstance; @$pb.TagNumber(1) - $core.int get nonce => $_getIZ(0); + $fixnum.Int64 get timestampNanos => $_getI64(0); @$pb.TagNumber(1) - set nonce($core.int value) => $_setSignedInt32(0, value); + set timestampNanos($fixnum.Int64 value) => $_setInt64(0, value); @$pb.TagNumber(1) - $core.bool hasNonce() => $_has(0); + $core.bool hasTimestampNanos() => $_has(0); @$pb.TagNumber(1) - void clearNonce() => $_clearField(1); + void clearTimestampNanos() => $_clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get random => $_getN(1); + @$pb.TagNumber(2) + set random($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasRandom() => $_has(1); + @$pb.TagNumber(2) + void clearRandom() => $_clearField(2); } class AuthChallengeSolution extends $pb.GeneratedMessage { diff --git a/useragent/lib/proto/user_agent/auth.pbenum.dart b/useragent/lib/proto/user_agent/auth.pbenum.dart index 853cbed..835ae9c 100644 --- a/useragent/lib/proto/user_agent/auth.pbenum.dart +++ b/useragent/lib/proto/user_agent/auth.pbenum.dart @@ -14,31 +14,6 @@ import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; -class KeyType extends $pb.ProtobufEnum { - static const KeyType KEY_TYPE_UNSPECIFIED = - KeyType._(0, _omitEnumNames ? '' : 'KEY_TYPE_UNSPECIFIED'); - static const KeyType KEY_TYPE_ED25519 = - KeyType._(1, _omitEnumNames ? '' : 'KEY_TYPE_ED25519'); - static const KeyType KEY_TYPE_ECDSA_SECP256K1 = - KeyType._(2, _omitEnumNames ? '' : 'KEY_TYPE_ECDSA_SECP256K1'); - static const KeyType KEY_TYPE_RSA = - KeyType._(3, _omitEnumNames ? '' : 'KEY_TYPE_RSA'); - - static const $core.List values = [ - KEY_TYPE_UNSPECIFIED, - KEY_TYPE_ED25519, - KEY_TYPE_ECDSA_SECP256K1, - KEY_TYPE_RSA, - ]; - - static final $core.List _byValue = - $pb.ProtobufEnum.$_initByValueList(values, 3); - static KeyType? valueOf($core.int value) => - value < 0 || value >= _byValue.length ? null : _byValue[value]; - - const KeyType._(super.value, super.name); -} - class AuthResult extends $pb.ProtobufEnum { static const AuthResult AUTH_RESULT_UNSPECIFIED = AuthResult._(0, _omitEnumNames ? '' : 'AUTH_RESULT_UNSPECIFIED'); diff --git a/useragent/lib/proto/user_agent/auth.pbjson.dart b/useragent/lib/proto/user_agent/auth.pbjson.dart index 87fc1c6..87d0979 100644 --- a/useragent/lib/proto/user_agent/auth.pbjson.dart +++ b/useragent/lib/proto/user_agent/auth.pbjson.dart @@ -15,22 +15,6 @@ import 'dart:convert' as $convert; import 'dart:core' as $core; import 'dart:typed_data' as $typed_data; -@$core.Deprecated('Use keyTypeDescriptor instead') -const KeyType$json = { - '1': 'KeyType', - '2': [ - {'1': 'KEY_TYPE_UNSPECIFIED', '2': 0}, - {'1': 'KEY_TYPE_ED25519', '2': 1}, - {'1': 'KEY_TYPE_ECDSA_SECP256K1', '2': 2}, - {'1': 'KEY_TYPE_RSA', '2': 3}, - ], -}; - -/// Descriptor for `KeyType`. Decode as a `google.protobuf.EnumDescriptorProto`. -final $typed_data.Uint8List keyTypeDescriptor = $convert.base64Decode( - 'CgdLZXlUeXBlEhgKFEtFWV9UWVBFX1VOU1BFQ0lGSUVEEAASFAoQS0VZX1RZUEVfRUQyNTUxOR' - 'ABEhwKGEtFWV9UWVBFX0VDRFNBX1NFQ1AyNTZLMRACEhAKDEtFWV9UWVBFX1JTQRAD'); - @$core.Deprecated('Use authResultDescriptor instead') const AuthResult$json = { '1': 'AuthResult', @@ -67,14 +51,6 @@ const AuthChallengeRequest$json = { '10': 'bootstrapToken', '17': true }, - { - '1': 'key_type', - '3': 3, - '4': 1, - '5': 14, - '6': '.arbiter.user_agent.auth.KeyType', - '10': 'keyType' - }, ], '8': [ {'1': '_bootstrap_token'}, @@ -84,21 +60,22 @@ const AuthChallengeRequest$json = { /// Descriptor for `AuthChallengeRequest`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List authChallengeRequestDescriptor = $convert.base64Decode( 'ChRBdXRoQ2hhbGxlbmdlUmVxdWVzdBIWCgZwdWJrZXkYASABKAxSBnB1YmtleRIsCg9ib290c3' - 'RyYXBfdG9rZW4YAiABKAlIAFIOYm9vdHN0cmFwVG9rZW6IAQESOwoIa2V5X3R5cGUYAyABKA4y' - 'IC5hcmJpdGVyLnVzZXJfYWdlbnQuYXV0aC5LZXlUeXBlUgdrZXlUeXBlQhIKEF9ib290c3RyYX' - 'BfdG9rZW4='); + 'RyYXBfdG9rZW4YAiABKAlIAFIOYm9vdHN0cmFwVG9rZW6IAQFCEgoQX2Jvb3RzdHJhcF90b2tl' + 'bg=='); @$core.Deprecated('Use authChallengeDescriptor instead') const AuthChallenge$json = { '1': 'AuthChallenge', '2': [ - {'1': 'nonce', '3': 1, '4': 1, '5': 5, '10': 'nonce'}, + {'1': 'timestamp_nanos', '3': 1, '4': 1, '5': 4, '10': 'timestampNanos'}, + {'1': 'random', '3': 2, '4': 1, '5': 12, '10': 'random'}, ], }; /// Descriptor for `AuthChallenge`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List authChallengeDescriptor = $convert - .base64Decode('Cg1BdXRoQ2hhbGxlbmdlEhQKBW5vbmNlGAEgASgFUgVub25jZQ=='); +final $typed_data.Uint8List authChallengeDescriptor = $convert.base64Decode( + 'Cg1BdXRoQ2hhbGxlbmdlEicKD3RpbWVzdGFtcF9uYW5vcxgBIAEoBFIOdGltZXN0YW1wTmFub3' + 'MSFgoGcmFuZG9tGAIgASgMUgZyYW5kb20='); @$core.Deprecated('Use authChallengeSolutionDescriptor instead') const AuthChallengeSolution$json = { diff --git a/useragent/lib/providers/connection/bootstrap_token.dart b/useragent/lib/providers/connection/bootstrap_token.dart index 48b8291..fef2bc8 100644 --- a/useragent/lib/providers/connection/bootstrap_token.dart +++ b/useragent/lib/providers/connection/bootstrap_token.dart @@ -1,5 +1,3 @@ - - import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'bootstrap_token.g.dart'; diff --git a/useragent/lib/providers/evm/evm.dart b/useragent/lib/providers/evm/evm.dart index f32386f..9e73f70 100644 --- a/useragent/lib/providers/evm/evm.dart +++ b/useragent/lib/providers/evm/evm.dart @@ -2,7 +2,6 @@ import 'package:arbiter/features/connection/evm.dart' as evm; import 'package:arbiter/proto/evm.pb.dart'; import 'package:arbiter/providers/connection/connection_manager.dart'; import 'package:hooks_riverpod/experimental/mutation.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'evm.g.dart'; @@ -35,7 +34,7 @@ final createEvmWallet = Mutation(); Future executeCreateEvmWallet(MutationTarget target) async { return await createEvmWallet.run(target, (tsx) async { - final connection = await tsx.get(connectionManagerProvider.future); + final connection = await tsx.get(connectionManagerProvider.future); if (connection == null) { throw Exception('Not connected to the server.'); } @@ -44,4 +43,4 @@ Future executeCreateEvmWallet(MutationTarget target) async { await tsx.get(evmProvider.notifier).refreshWallets(); }); -} \ No newline at end of file +} diff --git a/useragent/lib/providers/key.dart b/useragent/lib/providers/key.dart index 247a906..4ad9662 100644 --- a/useragent/lib/providers/key.dart +++ b/useragent/lib/providers/key.dart @@ -1,13 +1,13 @@ import 'package:mtcore/markettakers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:arbiter/features/identity/pk_manager.dart'; -import 'package:arbiter/features/identity/simple_ed25519.dart'; +import 'package:arbiter/features/identity/hazmat_mldsa.dart'; part 'key.g.dart'; @riverpod KeyManager keyManager(Ref ref) { - return SimpleEd25519Manager(); + return HazmatMLDSAManager(); } @Riverpod(keepAlive: true) diff --git a/useragent/lib/providers/sdk_clients/list.dart b/useragent/lib/providers/sdk_clients/list.dart index a6ade66..4a52818 100644 --- a/useragent/lib/providers/sdk_clients/list.dart +++ b/useragent/lib/providers/sdk_clients/list.dart @@ -18,9 +18,7 @@ Future?> sdkClients(Ref ref) async { ); if (!resp.hasSdkClient()) { - throw Exception( - 'Expected SDK client response, got ${resp.whichPayload()}', - ); + throw Exception('Expected SDK client response, got ${resp.whichPayload()}'); } final sdkClientResponse = resp.sdkClient; if (!sdkClientResponse.hasList()) { diff --git a/useragent/lib/providers/server_info.dart b/useragent/lib/providers/server_info.dart index 27bee2e..54344c8 100644 --- a/useragent/lib/providers/server_info.dart +++ b/useragent/lib/providers/server_info.dart @@ -46,6 +46,8 @@ class ServerInfo extends _$ServerInfo { Future _fingerprint(List caCert) async { final digest = await Sha256().hash(caCert); - return digest.bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); + return digest.bytes + .map((byte) => byte.toRadixString(16).padLeft(2, '0')) + .join(); } } diff --git a/useragent/lib/screens/callouts/sdk_connect.dart b/useragent/lib/screens/callouts/sdk_connect.dart index 372261f..aa7c194 100644 --- a/useragent/lib/screens/callouts/sdk_connect.dart +++ b/useragent/lib/screens/callouts/sdk_connect.dart @@ -28,8 +28,7 @@ class SdkConnectCallout extends StatelessWidget { final hasDescription = clientInfo.hasDescription() && clientInfo.description.isNotEmpty; - final hasVersion = - clientInfo.hasVersion() && clientInfo.version.isNotEmpty; + final hasVersion = clientInfo.hasVersion() && clientInfo.version.isNotEmpty; final showInfoCard = hasDescription || hasVersion; return CreamFrame( @@ -74,10 +73,7 @@ class SdkConnectCallout extends StatelessWidget { borderRadius: BorderRadius.circular(14), border: Border.all(color: Palette.line), ), - padding: EdgeInsets.symmetric( - horizontal: 1.6.w, - vertical: 1.2.h, - ), + padding: EdgeInsets.symmetric(horizontal: 1.6.w, vertical: 1.2.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, spacing: 0.6.h, diff --git a/useragent/lib/screens/dashboard.dart b/useragent/lib/screens/dashboard.dart index 4ad5390..9a8c396 100644 --- a/useragent/lib/screens/dashboard.dart +++ b/useragent/lib/screens/dashboard.dart @@ -78,7 +78,7 @@ class DashboardRouter extends StatelessWidget { } class _CalloutBell extends ConsumerWidget { - const _CalloutBell({super.key}); + const _CalloutBell(); @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/useragent/lib/screens/dashboard/clients/details.dart b/useragent/lib/screens/dashboard/clients/details.dart index 3613401..25c700d 100644 --- a/useragent/lib/screens/dashboard/clients/details.dart +++ b/useragent/lib/screens/dashboard/clients/details.dart @@ -1,4 +1,3 @@ - import 'package:arbiter/proto/user_agent/sdk_client.pb.dart' as ua_sdk; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; @@ -13,5 +12,4 @@ class ClientDetails extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { throw UnimplementedError(); } - } diff --git a/useragent/lib/screens/dashboard/clients/details/widgets/client_summary_card.dart b/useragent/lib/screens/dashboard/clients/details/widgets/client_summary_card.dart index 81cc528..4de0b5d 100644 --- a/useragent/lib/screens/dashboard/clients/details/widgets/client_summary_card.dart +++ b/useragent/lib/screens/dashboard/clients/details/widgets/client_summary_card.dart @@ -12,30 +12,24 @@ class ClientSummaryCard extends StatelessWidget { return CreamFrame( padding: const EdgeInsets.all(20), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - client.info.name, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 8), - Text(client.info.description), - const SizedBox(height: 16), - Wrap( - runSpacing: 8, - spacing: 16, - children: [ - _Fact(label: 'Client ID', value: '${client.id}'), - _Fact(label: 'Version', value: client.info.version), - _Fact( - label: 'Registered', - value: _formatDate(client.createdAt), - ), - _Fact(label: 'Pubkey', value: _shortPubkey(client.pubkey)), - ], - ), - ], - ), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(client.info.name, style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 8), + Text(client.info.description), + const SizedBox(height: 16), + Wrap( + runSpacing: 8, + spacing: 16, + children: [ + _Fact(label: 'Client ID', value: '${client.id}'), + _Fact(label: 'Version', value: client.info.version), + _Fact(label: 'Registered', value: _formatDate(client.createdAt)), + _Fact(label: 'Pubkey', value: _shortPubkey(client.pubkey)), + ], + ), + ], + ), ); } } diff --git a/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_save_bar.dart b/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_save_bar.dart index b96f2f6..d52828a 100644 --- a/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_save_bar.dart +++ b/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_save_bar.dart @@ -28,27 +28,27 @@ class WalletAccessSaveBar extends StatelessWidget { return CreamFrame( padding: const EdgeInsets.all(16), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (errorText != null) ...[ - Text(errorText, style: TextStyle(color: Palette.coral)), - const SizedBox(height: 12), - ], - Row( - children: [ - TextButton( - onPressed: state.hasChanges && !isPending ? onDiscard : null, - child: const Text('Reset'), - ), - const Spacer(), - FilledButton( - onPressed: state.hasChanges && !isPending ? onSave : null, - child: Text(isPending ? 'Saving...' : 'Save changes'), - ), - ], - ), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (errorText != null) ...[ + Text(errorText, style: TextStyle(color: Palette.coral)), + const SizedBox(height: 12), ], - ), + Row( + children: [ + TextButton( + onPressed: state.hasChanges && !isPending ? onDiscard : null, + child: const Text('Reset'), + ), + const Spacer(), + FilledButton( + onPressed: state.hasChanges && !isPending ? onSave : null, + child: Text(isPending ? 'Saving...' : 'Save changes'), + ), + ], + ), + ], + ), ); } } diff --git a/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_section.dart b/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_section.dart index cf55b29..54f348e 100644 --- a/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_section.dart +++ b/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_section.dart @@ -30,26 +30,23 @@ class WalletAccessSection extends ConsumerWidget { return CreamFrame( padding: const EdgeInsets.all(20), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Wallet access', - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 8), - Text('Choose which managed wallets this client can see.'), - const SizedBox(height: 16), - _WalletAccessBody( - clientId: clientId, - state: state, - accessSelectionAsync: accessSelectionAsync, - isSavePending: isSavePending, - optionsAsync: optionsAsync, - onSearchChanged: onSearchChanged, - onToggleWallet: onToggleWallet, - ), - ], - ), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Wallet access', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 8), + Text('Choose which managed wallets this client can see.'), + const SizedBox(height: 16), + _WalletAccessBody( + clientId: clientId, + state: state, + accessSelectionAsync: accessSelectionAsync, + isSavePending: isSavePending, + optionsAsync: optionsAsync, + onSearchChanged: onSearchChanged, + onToggleWallet: onToggleWallet, + ), + ], + ), ); } } diff --git a/useragent/lib/screens/dashboard/clients/table.dart b/useragent/lib/screens/dashboard/clients/table.dart index 9ccbcda..3d9a452 100644 --- a/useragent/lib/screens/dashboard/clients/table.dart +++ b/useragent/lib/screens/dashboard/clients/table.dart @@ -378,48 +378,48 @@ class _ClientTable extends StatelessWidget { builder: (context, constraints) { final tableWidth = math.max(_tableMinWidth, constraints.maxWidth); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Registered clients', - style: theme.textTheme.titleLarge?.copyWith( - color: Palette.ink, - fontWeight: FontWeight.w800, - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Registered clients', + style: theme.textTheme.titleLarge?.copyWith( + color: Palette.ink, + fontWeight: FontWeight.w800, ), - SizedBox(height: 0.6.h), - Text( - 'Every entry here has authenticated with Arbiter at least once.', - style: theme.textTheme.bodyMedium?.copyWith( - color: Palette.ink.withValues(alpha: 0.70), - height: 1.4, - ), + ), + SizedBox(height: 0.6.h), + Text( + 'Every entry here has authenticated with Arbiter at least once.', + style: theme.textTheme.bodyMedium?.copyWith( + color: Palette.ink.withValues(alpha: 0.70), + height: 1.4, ), - SizedBox(height: 1.6.h), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SizedBox( - width: tableWidth, - child: Column( - children: [ - const _ClientTableHeader(), - SizedBox(height: 1.h), - for (var i = 0; i < clients.length; i++) - Padding( - padding: EdgeInsets.only( - bottom: i == clients.length - 1 ? 0 : 1.h, - ), - child: _ClientTableRow(client: clients[i]), + ), + SizedBox(height: 1.6.h), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: tableWidth, + child: Column( + children: [ + const _ClientTableHeader(), + SizedBox(height: 1.h), + for (var i = 0; i < clients.length; i++) + Padding( + padding: EdgeInsets.only( + bottom: i == clients.length - 1 ? 0 : 1.h, ), - ], - ), + child: _ClientTableRow(client: clients[i]), + ), + ], ), ), - ], - ); - }, - ), + ), + ], + ); + }, + ), ); } } diff --git a/useragent/lib/screens/dashboard/evm/grants/create/fields/client_picker_field.dart b/useragent/lib/screens/dashboard/evm/grants/create/fields/client_picker_field.dart index cfee2fe..6be82e5 100644 --- a/useragent/lib/screens/dashboard/evm/grants/create/fields/client_picker_field.dart +++ b/useragent/lib/screens/dashboard/evm/grants/create/fields/client_picker_field.dart @@ -31,7 +31,9 @@ class ClientPickerField extends ConsumerWidget { ? null : (value) { ref.read(grantCreationProvider.notifier).setClientId(value); - FormBuilder.of(context)?.fields['walletAccessId']?.didChange(null); + FormBuilder.of( + context, + )?.fields['walletAccessId']?.didChange(null); }, ); } diff --git a/useragent/lib/screens/dashboard/evm/grants/create/fields/date_time_field.dart b/useragent/lib/screens/dashboard/evm/grants/create/fields/date_time_field.dart index 166359c..62d3ddb 100644 --- a/useragent/lib/screens/dashboard/evm/grants/create/fields/date_time_field.dart +++ b/useragent/lib/screens/dashboard/evm/grants/create/fields/date_time_field.dart @@ -16,46 +16,48 @@ class FormBuilderDateTimeField extends FormBuilderField { super.onChanged, super.validator, }) : super( - builder: (FormFieldState field) { - final value = field.value; - return OutlinedButton( - onPressed: () async { - final ctx = field.context; - final now = DateTime.now(); - final date = await showDatePicker( - context: ctx, - firstDate: DateTime(now.year - 5), - lastDate: DateTime(now.year + 10), - initialDate: value ?? now, - ); - if (date == null) return; - if (!ctx.mounted) return; - final time = await showTimePicker( - context: ctx, - initialTime: TimeOfDay.fromDateTime(value ?? now), - ); - if (time == null) return; - field.didChange(DateTime( - date.year, - date.month, - date.day, - time.hour, - time.minute, - )); - }, - onLongPress: value == null ? null : () => field.didChange(null), - child: Padding( - padding: EdgeInsets.symmetric(vertical: 1.8.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label), - SizedBox(height: 0.6.h), - Text(value?.toLocal().toString() ?? 'Not set'), - ], - ), - ), - ); - }, - ); + builder: (FormFieldState field) { + final value = field.value; + return OutlinedButton( + onPressed: () async { + final ctx = field.context; + final now = DateTime.now(); + final date = await showDatePicker( + context: ctx, + firstDate: DateTime(now.year - 5), + lastDate: DateTime(now.year + 10), + initialDate: value ?? now, + ); + if (date == null) return; + if (!ctx.mounted) return; + final time = await showTimePicker( + context: ctx, + initialTime: TimeOfDay.fromDateTime(value ?? now), + ); + if (time == null) return; + field.didChange( + DateTime( + date.year, + date.month, + date.day, + time.hour, + time.minute, + ), + ); + }, + onLongPress: value == null ? null : () => field.didChange(null), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 1.8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label), + SizedBox(height: 0.6.h), + Text(value?.toLocal().toString() ?? 'Not set'), + ], + ), + ), + ); + }, + ); } diff --git a/useragent/lib/screens/dashboard/evm/grants/create/fields/wallet_access_picker_field.dart b/useragent/lib/screens/dashboard/evm/grants/create/fields/wallet_access_picker_field.dart index 72a760a..cde8d17 100644 --- a/useragent/lib/screens/dashboard/evm/grants/create/fields/wallet_access_picker_field.dart +++ b/useragent/lib/screens/dashboard/evm/grants/create/fields/wallet_access_picker_field.dart @@ -36,8 +36,8 @@ class WalletAccessPickerField extends ConsumerWidget { helperText: state.selectedClientId == null ? 'Select a client first' : accesses.isEmpty - ? 'No wallet accesses for this client' - : null, + ? 'No wallet accesses for this client' + : null, border: const OutlineInputBorder(), ), items: [ diff --git a/useragent/lib/screens/dashboard/evm/grants/create/grants/ether_transfer_grant.dart b/useragent/lib/screens/dashboard/evm/grants/create/grants/ether_transfer_grant.dart index 547e247..bc7bda6 100644 --- a/useragent/lib/screens/dashboard/evm/grants/create/grants/ether_transfer_grant.dart +++ b/useragent/lib/screens/dashboard/evm/grants/create/grants/ether_transfer_grant.dart @@ -93,9 +93,9 @@ class _EtherTransferForm extends ConsumerWidget { SizedBox(height: 1.6.h), Text( 'Ether volume limit', - style: Theme.of(context).textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w800, - ), + style: Theme.of( + context, + ).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w800), ), SizedBox(height: 0.8.h), Row( @@ -157,9 +157,9 @@ class _EtherTargetsField extends StatelessWidget { Expanded( child: Text( 'Ether targets', - style: Theme.of(context).textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w800, - ), + style: Theme.of( + context, + ).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w800), ), ), TextButton.icon( diff --git a/useragent/lib/screens/dashboard/evm/grants/create/grants/token_transfer_grant.dart b/useragent/lib/screens/dashboard/evm/grants/create/grants/token_transfer_grant.dart index 9352b94..58e27de 100644 --- a/useragent/lib/screens/dashboard/evm/grants/create/grants/token_transfer_grant.dart +++ b/useragent/lib/screens/dashboard/evm/grants/create/grants/token_transfer_grant.dart @@ -13,7 +13,11 @@ import 'package:sizer/sizer.dart'; part 'token_transfer_grant.g.dart'; class VolumeLimitEntry { - VolumeLimitEntry({required this.id, this.amount = '', this.windowSeconds = ''}); + VolumeLimitEntry({ + required this.id, + this.amount = '', + this.windowSeconds = '', + }); final int id; final String amount; @@ -27,7 +31,6 @@ class VolumeLimitEntry { ); } - @riverpod class TokenGrantLimits extends _$TokenGrantLimits { int _nextId = 0; @@ -47,7 +50,6 @@ class TokenGrantLimits extends _$TokenGrantLimits { void remove(int index) => state = [...state]..removeAt(index); } - class TokenTransferGrantHandler implements GrantFormHandler { const TokenTransferGrantHandler(); @@ -65,11 +67,16 @@ class TokenTransferGrantHandler implements GrantFormHandler { return SpecificGrant( tokenTransfer: TokenTransferSettings( - tokenContract: - parseHexAddress(formValues['tokenContract'] as String? ?? ''), + tokenContract: parseHexAddress( + formValues['tokenContract'] as String? ?? '', + ), target: targetText.trim().isEmpty ? null : parseHexAddress(targetText), volumeLimits: limits - .where((e) => e.amount.trim().isNotEmpty && e.windowSeconds.trim().isNotEmpty) + .where( + (e) => + e.amount.trim().isNotEmpty && + e.windowSeconds.trim().isNotEmpty, + ) .map( (e) => VolumeRateLimit( maxVolume: parseBigIntBytes(e.amount), @@ -153,9 +160,9 @@ class _TokenVolumeLimitsField extends StatelessWidget { Expanded( child: Text( 'Token volume limits', - style: Theme.of(context).textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w800, - ), + style: Theme.of( + context, + ).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w800), ), ), TextButton.icon( @@ -196,7 +203,9 @@ class _TokenVolumeLimitRow extends HookWidget { @override Widget build(BuildContext context) { final amountController = useTextEditingController(text: value.amount); - final windowController = useTextEditingController(text: value.windowSeconds); + final windowController = useTextEditingController( + text: value.windowSeconds, + ); return Row( children: [ @@ -214,8 +223,7 @@ class _TokenVolumeLimitRow extends HookWidget { Expanded( child: TextField( controller: windowController, - onChanged: (next) => - onChanged(value.copyWith(windowSeconds: next)), + onChanged: (next) => onChanged(value.copyWith(windowSeconds: next)), decoration: const InputDecoration( labelText: 'Window (seconds)', border: OutlineInputBorder(), diff --git a/useragent/lib/screens/dashboard/evm/grants/create/screen.dart b/useragent/lib/screens/dashboard/evm/grants/create/screen.dart index b813c14..8808e3e 100644 --- a/useragent/lib/screens/dashboard/evm/grants/create/screen.dart +++ b/useragent/lib/screens/dashboard/evm/grants/create/screen.dart @@ -25,10 +25,10 @@ const _etherHandler = EtherTransferGrantHandler(); const _tokenHandler = TokenTransferGrantHandler(); GrantFormHandler _handlerFor(SpecificGrant_Grant type) => switch (type) { - SpecificGrant_Grant.etherTransfer => _etherHandler, - SpecificGrant_Grant.tokenTransfer => _tokenHandler, - _ => throw ArgumentError('Unsupported grant type: $type'), - }; + SpecificGrant_Grant.etherTransfer => _etherHandler, + SpecificGrant_Grant.tokenTransfer => _tokenHandler, + _ => throw ArgumentError('Unsupported grant type: $type'), +}; @RoutePage() class CreateEvmGrantScreen extends HookConsumerWidget { @@ -62,12 +62,14 @@ class CreateEvmGrantScreen extends HookConsumerWidget { ); final validFrom = formValues['validFrom'] as DateTime?; final validUntil = formValues['validUntil'] as DateTime?; - if (validFrom != null) sharedSettings.validFrom = toTimestamp(validFrom); + if (validFrom != null) + sharedSettings.validFrom = toTimestamp(validFrom); if (validUntil != null) { sharedSettings.validUntil = toTimestamp(validUntil); } - final gasBytes = - optionalBigIntBytes(formValues['maxGasFeePerGas'] as String? ?? ''); + final gasBytes = optionalBigIntBytes( + formValues['maxGasFeePerGas'] as String? ?? '', + ); if (gasBytes != null) sharedSettings.maxGasFeePerGas = gasBytes; final priorityBytes = optionalBigIntBytes( formValues['maxPriorityFeePerGas'] as String? ?? '', @@ -106,7 +108,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget { SizedBox(height: 1.8.h), const _Section( title: 'Authorization', - tooltip: 'Select which SDK client receives this grant and ' + tooltip: + 'Select which SDK client receives this grant and ' 'which of its wallet accesses it applies to.', child: AuthorizationFields(), ), @@ -118,7 +121,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget { const Expanded( child: _Section( title: 'Chain', - tooltip: 'Restrict this grant to a specific EVM chain ID. ' + tooltip: + 'Restrict this grant to a specific EVM chain ID. ' 'Leave empty to allow any chain.', optional: true, child: ChainIdField(), @@ -128,7 +132,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget { const Expanded( child: _Section( title: 'Timing', - tooltip: 'Set an optional validity window. ' + tooltip: + 'Set an optional validity window. ' 'Signing requests outside this period will be rejected.', optional: true, child: ValidityWindowField(), @@ -145,7 +150,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget { const Expanded( child: _Section( title: 'Gas limits', - tooltip: 'Cap the gas fees this grant may authorize. ' + tooltip: + 'Cap the gas fees this grant may authorize. ' 'Transactions exceeding these values will be rejected.', optional: true, child: GasFeeOptionsField(), @@ -155,7 +161,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget { const Expanded( child: _Section( title: 'Transaction limits', - tooltip: 'Limit how many transactions can be signed ' + tooltip: + 'Limit how many transactions can be signed ' 'within a rolling time window.', optional: true, child: TransactionRateLimitField(), @@ -172,7 +179,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget { SizedBox(height: 1.8.h), _Section( title: 'Grant-specific options', - tooltip: 'Rules specific to the selected transfer type. ' + tooltip: + 'Rules specific to the selected transfer type. ' 'Switch between Ether and token above to change these fields.', child: handler.buildForm(context, ref), ), @@ -180,8 +188,7 @@ class CreateEvmGrantScreen extends HookConsumerWidget { Align( alignment: Alignment.centerRight, child: FilledButton.icon( - onPressed: - createMutation is MutationPending ? null : submit, + onPressed: createMutation is MutationPending ? null : submit, icon: createMutation is MutationPending ? SizedBox( width: 1.8.h, @@ -266,9 +273,9 @@ class _Section extends StatelessWidget { children: [ Text( title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w800, - ), + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w800), ), SizedBox(width: 0.4.w), Tooltip( @@ -283,9 +290,9 @@ class _Section extends StatelessWidget { SizedBox(width: 0.6.w), Text( '(optional)', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: subtleColor, - ), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: subtleColor), ), ], ], diff --git a/useragent/lib/screens/dashboard/evm/grants/create/shared_grant_fields.dart b/useragent/lib/screens/dashboard/evm/grants/create/shared_grant_fields.dart index 9722d05..65aba62 100644 --- a/useragent/lib/screens/dashboard/evm/grants/create/shared_grant_fields.dart +++ b/useragent/lib/screens/dashboard/evm/grants/create/shared_grant_fields.dart @@ -19,4 +19,3 @@ class AuthorizationFields extends StatelessWidget { ); } } - diff --git a/useragent/lib/screens/dashboard/evm/grants/widgets/grant_card.dart b/useragent/lib/screens/dashboard/evm/grants/widgets/grant_card.dart index c04e4d3..76857fd 100644 --- a/useragent/lib/screens/dashboard/evm/grants/widgets/grant_card.dart +++ b/useragent/lib/screens/dashboard/evm/grants/widgets/grant_card.dart @@ -46,9 +46,7 @@ class GrantCard extends ConsumerWidget { final accessById = { for (final a in walletAccesses) a.id: a, }; - final walletById = { - for (final w in wallets) w.id: w, - }; + final walletById = {for (final w in wallets) w.id: w}; final clientNameById = { for (final c in clients) c.id: c.info.name, }; @@ -192,8 +190,9 @@ class GrantCard extends ConsumerWidget { padding: EdgeInsets.symmetric(horizontal: 0.8.w), child: Text( '·', - style: theme.textTheme.bodySmall - ?.copyWith(color: muted), + style: theme.textTheme.bodySmall?.copyWith( + color: muted, + ), ), ), Expanded( @@ -201,8 +200,9 @@ class GrantCard extends ConsumerWidget { clientLabel, maxLines: 1, overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall - ?.copyWith(color: muted), + style: theme.textTheme.bodySmall?.copyWith( + color: muted, + ), ), ), ], diff --git a/useragent/lib/screens/dashboard/evm/wallets/header.dart b/useragent/lib/screens/dashboard/evm/wallets/header.dart index 646d5ea..b74a50a 100644 --- a/useragent/lib/screens/dashboard/evm/wallets/header.dart +++ b/useragent/lib/screens/dashboard/evm/wallets/header.dart @@ -5,7 +5,6 @@ import 'package:hooks_riverpod/experimental/mutation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:sizer/sizer.dart'; - class CreateWalletButton extends ConsumerWidget { const CreateWalletButton({super.key}); @@ -88,7 +87,6 @@ class RefreshWalletButton extends ConsumerWidget { } } - String _formatError(Object error) { final message = error.toString(); if (message.startsWith('Exception: ')) { diff --git a/useragent/lib/screens/dashboard/evm/wallets/table.dart b/useragent/lib/screens/dashboard/evm/wallets/table.dart index a364d72..6d850e1 100644 --- a/useragent/lib/screens/dashboard/evm/wallets/table.dart +++ b/useragent/lib/screens/dashboard/evm/wallets/table.dart @@ -36,54 +36,51 @@ class WalletTable extends StatelessWidget { return CreamFrame( padding: EdgeInsets.all(2.h), child: LayoutBuilder( - builder: (context, constraints) { - final tableWidth = math.max(_tableMinWidth, constraints.maxWidth); + builder: (context, constraints) { + final tableWidth = math.max(_tableMinWidth, constraints.maxWidth); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Managed wallets', - style: theme.textTheme.titleLarge?.copyWith( - color: Palette.ink, - fontWeight: FontWeight.w800, - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Managed wallets', + style: theme.textTheme.titleLarge?.copyWith( + color: Palette.ink, + fontWeight: FontWeight.w800, ), - SizedBox(height: 0.6.h), - Text( - 'Every address here is generated and held by Arbiter.', - style: theme.textTheme.bodyMedium?.copyWith( - color: Palette.ink.withValues(alpha: 0.70), - height: 1.4, - ), + ), + SizedBox(height: 0.6.h), + Text( + 'Every address here is generated and held by Arbiter.', + style: theme.textTheme.bodyMedium?.copyWith( + color: Palette.ink.withValues(alpha: 0.70), + height: 1.4, ), - SizedBox(height: 1.6.h), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SizedBox( - width: tableWidth, - child: Column( - children: [ - const _WalletTableHeader(), - SizedBox(height: 1.h), - for (var i = 0; i < wallets.length; i++) - Padding( - padding: EdgeInsets.only( - bottom: i == wallets.length - 1 ? 0 : 1.h, - ), - child: _WalletTableRow( - wallet: wallets[i], - index: i, - ), + ), + SizedBox(height: 1.6.h), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: tableWidth, + child: Column( + children: [ + const _WalletTableHeader(), + SizedBox(height: 1.h), + for (var i = 0; i < wallets.length; i++) + Padding( + padding: EdgeInsets.only( + bottom: i == wallets.length - 1 ? 0 : 1.h, ), - ], - ), + child: _WalletTableRow(wallet: wallets[i], index: i), + ), + ], ), ), - ], - ); - }, - ), + ), + ], + ); + }, + ), ); } } diff --git a/useragent/lib/src/rust/api.dart b/useragent/lib/src/rust/api.dart new file mode 100644 index 0000000..8648c0e --- /dev/null +++ b/useragent/lib/src/rust/api.dart @@ -0,0 +1,30 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.12.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import 'frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +Future formatChallenge({ + required List random, + required PlatformInt64 timestamp, +}) => RustLib.instance.api.crateApiFormatChallenge( + random: random, + timestamp: timestamp, +); + +// Rust type: RustOpaqueMoi> +abstract class MldsaKey implements RustOpaqueInterface { + static Future fromBytes({required List bytes}) => + RustLib.instance.api.crateApiMldsaKeyFromBytes(bytes: bytes); + + static Future generate() => + RustLib.instance.api.crateApiMldsaKeyGenerate(); + + Future getPublicKey(); + + Future sign({required List message}); + + Future toBytes(); +} diff --git a/useragent/lib/src/rust/frb_generated.dart b/useragent/lib/src/rust/frb_generated.dart new file mode 100644 index 0000000..fa57ada --- /dev/null +++ b/useragent/lib/src/rust/frb_generated.dart @@ -0,0 +1,630 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.12.0. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +import 'api.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'frb_generated.dart'; +import 'frb_generated.io.dart' + if (dart.library.js_interop) 'frb_generated.web.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +/// Main entrypoint of the Rust API +class RustLib extends BaseEntrypoint { + @internal + static final instance = RustLib._(); + + RustLib._(); + + /// Initialize flutter_rust_bridge + static Future init({ + RustLibApi? api, + BaseHandler? handler, + ExternalLibrary? externalLibrary, + bool forceSameCodegenVersion = true, + }) async { + await instance.initImpl( + api: api, + handler: handler, + externalLibrary: externalLibrary, + forceSameCodegenVersion: forceSameCodegenVersion, + ); + } + + /// Initialize flutter_rust_bridge in mock mode. + /// No libraries for FFI are loaded. + static void initMock({required RustLibApi api}) { + instance.initMockImpl(api: api); + } + + /// Dispose flutter_rust_bridge + /// + /// The call to this function is optional, since flutter_rust_bridge (and everything else) + /// is automatically disposed when the app stops. + static void dispose() => instance.disposeImpl(); + + @override + ApiImplConstructor get apiImplConstructor => + RustLibApiImpl.new; + + @override + WireConstructor get wireConstructor => + RustLibWire.fromExternalLibrary; + + @override + Future executeRustInitializers() async {} + + @override + ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig => + kDefaultExternalLibraryLoaderConfig; + + @override + String get codegenVersion => '2.12.0'; + + @override + int get rustContentHash => 1247923898; + + static const kDefaultExternalLibraryLoaderConfig = + ExternalLibraryLoaderConfig( + stem: 'rust_lib_arbiter', + ioDirectory: 'rust/target/release/', + webPrefix: 'pkg/', + wasmBindgenName: 'wasm_bindgen', + ); +} + +abstract class RustLibApi extends BaseApi { + Future crateApiMldsaKeyFromBytes({required List bytes}); + + Future crateApiMldsaKeyGenerate(); + + Future crateApiMldsaKeyGetPublicKey({required MldsaKey that}); + + Future crateApiMldsaKeySign({ + required MldsaKey that, + required List message, + }); + + Future crateApiMldsaKeyToBytes({required MldsaKey that}); + + Future crateApiFormatChallenge({ + required List random, + required PlatformInt64 timestamp, + }); + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_MldsaKey; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_MldsaKey; + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_MldsaKeyPtr; +} + +class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { + RustLibApiImpl({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + @override + Future crateApiMldsaKeyFromBytes({required List bytes}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_list_prim_u_8_loose(bytes, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 1, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiMldsaKeyFromBytesConstMeta, + argValues: [bytes], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiMldsaKeyFromBytesConstMeta => const TaskConstMeta( + debugName: "MldsaKey_from_bytes", + argNames: ["bytes"], + ); + + @override + Future crateApiMldsaKeyGenerate() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 2, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey, + decodeErrorData: null, + ), + constMeta: kCrateApiMldsaKeyGenerateConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiMldsaKeyGenerateConstMeta => + const TaskConstMeta(debugName: "MldsaKey_generate", argNames: []); + + @override + Future crateApiMldsaKeyGetPublicKey({required MldsaKey that}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + that, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 3, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_prim_u_8_strict, + decodeErrorData: null, + ), + constMeta: kCrateApiMldsaKeyGetPublicKeyConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiMldsaKeyGetPublicKeyConstMeta => + const TaskConstMeta( + debugName: "MldsaKey_get_public_key", + argNames: ["that"], + ); + + @override + Future crateApiMldsaKeySign({ + required MldsaKey that, + required List message, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(message, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 4, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_prim_u_8_strict, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiMldsaKeySignConstMeta, + argValues: [that, message], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiMldsaKeySignConstMeta => const TaskConstMeta( + debugName: "MldsaKey_sign", + argNames: ["that", "message"], + ); + + @override + Future crateApiMldsaKeyToBytes({required MldsaKey that}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + that, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 5, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_prim_u_8_strict, + decodeErrorData: null, + ), + constMeta: kCrateApiMldsaKeyToBytesConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiMldsaKeyToBytesConstMeta => + const TaskConstMeta(debugName: "MldsaKey_to_bytes", argNames: ["that"]); + + @override + Future crateApiFormatChallenge({ + required List random, + required PlatformInt64 timestamp, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_list_prim_u_8_loose(random, serializer); + sse_encode_i_64(timestamp, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 6, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_prim_u_8_strict, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiFormatChallengeConstMeta, + argValues: [random, timestamp], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiFormatChallengeConstMeta => const TaskConstMeta( + debugName: "format_challenge", + argNames: ["random", "timestamp"], + ); + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_MldsaKey => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_MldsaKey => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey; + + @protected + AnyhowException dco_decode_AnyhowException(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return AnyhowException(raw as String); + } + + @protected + MldsaKey + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return MldsaKeyImpl.frbInternalDcoDecode(raw as List); + } + + @protected + MldsaKey + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return MldsaKeyImpl.frbInternalDcoDecode(raw as List); + } + + @protected + MldsaKey + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return MldsaKeyImpl.frbInternalDcoDecode(raw as List); + } + + @protected + String dco_decode_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as String; + } + + @protected + PlatformInt64 dco_decode_i_64(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dcoDecodeI64(raw); + } + + @protected + List dco_decode_list_prim_u_8_loose(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as List; + } + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as Uint8List; + } + + @protected + int dco_decode_u_8(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + void dco_decode_unit(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return; + } + + @protected + BigInt dco_decode_usize(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dcoDecodeU64(raw); + } + + @protected + AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_String(deserializer); + return AnyhowException(inner); + } + + @protected + MldsaKey + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return MldsaKeyImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + MldsaKey + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return MldsaKeyImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + MldsaKey + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return MldsaKeyImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + String sse_decode_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_list_prim_u_8_strict(deserializer); + return utf8.decoder.convert(inner); + } + + @protected + PlatformInt64 sse_decode_i_64(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getPlatformInt64(); + } + + @protected + List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); + return deserializer.buffer.getUint8List(len_); + } + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); + return deserializer.buffer.getUint8List(len_); + } + + @protected + int sse_decode_u_8(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint8(); + } + + @protected + void sse_decode_unit(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + } + + @protected + BigInt sse_decode_usize(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getBigUint64(); + } + + @protected + int sse_decode_i_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getInt32(); + } + + @protected + bool sse_decode_bool(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint8() != 0; + } + + @protected + void sse_encode_AnyhowException( + AnyhowException self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.message, serializer); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + MldsaKey self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as MldsaKeyImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + MldsaKey self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as MldsaKeyImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + MldsaKey self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as MldsaKeyImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void sse_encode_String(String self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer); + } + + @protected + void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putPlatformInt64(self); + } + + @protected + void sse_encode_list_prim_u_8_loose( + List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + serializer.buffer.putUint8List( + self is Uint8List ? self : Uint8List.fromList(self), + ); + } + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + serializer.buffer.putUint8List(self); + } + + @protected + void sse_encode_u_8(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint8(self); + } + + @protected + void sse_encode_unit(void self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + } + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putBigUint64(self); + } + + @protected + void sse_encode_i_32(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putInt32(self); + } + + @protected + void sse_encode_bool(bool self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint8(self ? 1 : 0); + } +} + +@sealed +class MldsaKeyImpl extends RustOpaque implements MldsaKey { + // Not to be used by end users + MldsaKeyImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + MldsaKeyImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_MldsaKey, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_MldsaKey, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_MldsaKeyPtr, + ); + + Future getPublicKey() => + RustLib.instance.api.crateApiMldsaKeyGetPublicKey(that: this); + + Future sign({required List message}) => + RustLib.instance.api.crateApiMldsaKeySign(that: this, message: message); + + Future toBytes() => + RustLib.instance.api.crateApiMldsaKeyToBytes(that: this); +} diff --git a/useragent/lib/src/rust/frb_generated.io.dart b/useragent/lib/src/rust/frb_generated.io.dart new file mode 100644 index 0000000..3780c4e --- /dev/null +++ b/useragent/lib/src/rust/frb_generated.io.dart @@ -0,0 +1,220 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.12.0. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +import 'api.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi' as ffi; +import 'frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; + +abstract class RustLibApiImplPlatform extends BaseApiImpl { + RustLibApiImplPlatform({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_MldsaKeyPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKeyPtr; + + @protected + AnyhowException dco_decode_AnyhowException(dynamic raw); + + @protected + MldsaKey + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + dynamic raw, + ); + + @protected + MldsaKey + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + dynamic raw, + ); + + @protected + MldsaKey + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + dynamic raw, + ); + + @protected + String dco_decode_String(dynamic raw); + + @protected + PlatformInt64 dco_decode_i_64(dynamic raw); + + @protected + List dco_decode_list_prim_u_8_loose(dynamic raw); + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + + @protected + int dco_decode_u_8(dynamic raw); + + @protected + void dco_decode_unit(dynamic raw); + + @protected + BigInt dco_decode_usize(dynamic raw); + + @protected + AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); + + @protected + MldsaKey + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + SseDeserializer deserializer, + ); + + @protected + MldsaKey + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + SseDeserializer deserializer, + ); + + @protected + MldsaKey + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + SseDeserializer deserializer, + ); + + @protected + String sse_decode_String(SseDeserializer deserializer); + + @protected + PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); + + @protected + List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer); + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + int sse_decode_u_8(SseDeserializer deserializer); + + @protected + void sse_decode_unit(SseDeserializer deserializer); + + @protected + BigInt sse_decode_usize(SseDeserializer deserializer); + + @protected + int sse_decode_i_32(SseDeserializer deserializer); + + @protected + bool sse_decode_bool(SseDeserializer deserializer); + + @protected + void sse_encode_AnyhowException( + AnyhowException self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + MldsaKey self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + MldsaKey self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + MldsaKey self, + SseSerializer serializer, + ); + + @protected + void sse_encode_String(String self, SseSerializer serializer); + + @protected + void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); + + @protected + void sse_encode_list_prim_u_8_loose(List self, SseSerializer serializer); + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_u_8(int self, SseSerializer serializer); + + @protected + void sse_encode_unit(void self, SseSerializer serializer); + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_i_32(int self, SseSerializer serializer); + + @protected + void sse_encode_bool(bool self, SseSerializer serializer); +} + +// Section: wire_class + +class RustLibWire implements BaseWire { + factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) => + RustLibWire(lib.ffiDynamicLibrary); + + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + RustLibWire(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKeyPtr = + _lookup)>>( + 'frbgen_arbiter_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKeyPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKeyPtr = + _lookup)>>( + 'frbgen_arbiter_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKeyPtr + .asFunction)>(); +} diff --git a/useragent/lib/src/rust/frb_generated.web.dart b/useragent/lib/src/rust/frb_generated.web.dart new file mode 100644 index 0000000..e8a41cb --- /dev/null +++ b/useragent/lib/src/rust/frb_generated.web.dart @@ -0,0 +1,212 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.12.0. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +// Static analysis wrongly picks the IO variant, thus ignore this +// ignore_for_file: argument_type_not_assignable + +import 'api.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; + +abstract class RustLibApiImplPlatform extends BaseApiImpl { + RustLibApiImplPlatform({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_MldsaKeyPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey; + + @protected + AnyhowException dco_decode_AnyhowException(dynamic raw); + + @protected + MldsaKey + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + dynamic raw, + ); + + @protected + MldsaKey + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + dynamic raw, + ); + + @protected + MldsaKey + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + dynamic raw, + ); + + @protected + String dco_decode_String(dynamic raw); + + @protected + PlatformInt64 dco_decode_i_64(dynamic raw); + + @protected + List dco_decode_list_prim_u_8_loose(dynamic raw); + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + + @protected + int dco_decode_u_8(dynamic raw); + + @protected + void dco_decode_unit(dynamic raw); + + @protected + BigInt dco_decode_usize(dynamic raw); + + @protected + AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); + + @protected + MldsaKey + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + SseDeserializer deserializer, + ); + + @protected + MldsaKey + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + SseDeserializer deserializer, + ); + + @protected + MldsaKey + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + SseDeserializer deserializer, + ); + + @protected + String sse_decode_String(SseDeserializer deserializer); + + @protected + PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); + + @protected + List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer); + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + int sse_decode_u_8(SseDeserializer deserializer); + + @protected + void sse_decode_unit(SseDeserializer deserializer); + + @protected + BigInt sse_decode_usize(SseDeserializer deserializer); + + @protected + int sse_decode_i_32(SseDeserializer deserializer); + + @protected + bool sse_decode_bool(SseDeserializer deserializer); + + @protected + void sse_encode_AnyhowException( + AnyhowException self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + MldsaKey self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + MldsaKey self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + MldsaKey self, + SseSerializer serializer, + ); + + @protected + void sse_encode_String(String self, SseSerializer serializer); + + @protected + void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); + + @protected + void sse_encode_list_prim_u_8_loose(List self, SseSerializer serializer); + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_u_8(int self, SseSerializer serializer); + + @protected + void sse_encode_unit(void self, SseSerializer serializer); + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_i_32(int self, SseSerializer serializer); + + @protected + void sse_encode_bool(bool self, SseSerializer serializer); +} + +// Section: wire_class + +class RustLibWire implements BaseWire { + RustLibWire.fromExternalLibrary(ExternalLibrary lib); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + ptr, + ); +} + +@JS('wasm_bindgen') +external RustLibWasmModule get wasmModule; + +@JS() +@anonymous +extension type RustLibWasmModule._(JSObject _) implements JSObject { + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + int ptr, + ); +} diff --git a/useragent/lib/widgets/bottom_popup.dart b/useragent/lib/widgets/bottom_popup.dart index 875ef08..3e95db0 100644 --- a/useragent/lib/widgets/bottom_popup.dart +++ b/useragent/lib/widgets/bottom_popup.dart @@ -50,7 +50,9 @@ class _BottomPopupRoute extends StatelessWidget { Positioned.fill( child: GestureDetector( behavior: HitTestBehavior.opaque, - onTap: barrierDismissible ? () => Navigator.of(context).pop() : null, + onTap: barrierDismissible + ? () => Navigator.of(context).pop() + : null, child: AnimatedBuilder( animation: barrierAnimation, builder: (context, child) { @@ -71,11 +73,10 @@ class _BottomPopupRoute extends StatelessWidget { child: FadeTransition( opacity: popupAnimation, child: SlideTransition( - position: - Tween( - begin: const Offset(0, 0.08), - end: Offset.zero, - ).animate(popupAnimation), + position: Tween( + begin: const Offset(0, 0.08), + end: Offset.zero, + ).animate(popupAnimation), child: GestureDetector( onTap: () {}, child: Builder(builder: builder), diff --git a/useragent/lib/widgets/state_panel.dart b/useragent/lib/widgets/state_panel.dart index 4c73875..ca00b06 100644 --- a/useragent/lib/widgets/state_panel.dart +++ b/useragent/lib/widgets/state_panel.dart @@ -28,42 +28,42 @@ class StatePanel extends StatelessWidget { return CreamFrame( padding: EdgeInsets.all(2.8.h), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (busy) - SizedBox( - width: 2.8.h, - height: 2.8.h, - child: const CircularProgressIndicator(strokeWidth: 2.5), - ) - else - Icon(icon, size: 34, color: Palette.coral), - SizedBox(height: 1.8.h), - Text( - title, - style: theme.textTheme.headlineSmall?.copyWith( - color: Palette.ink, - fontWeight: FontWeight.w800, - ), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (busy) + SizedBox( + width: 2.8.h, + height: 2.8.h, + child: const CircularProgressIndicator(strokeWidth: 2.5), + ) + else + Icon(icon, size: 34, color: Palette.coral), + SizedBox(height: 1.8.h), + Text( + title, + style: theme.textTheme.headlineSmall?.copyWith( + color: Palette.ink, + fontWeight: FontWeight.w800, ), - SizedBox(height: 1.h), - Text( - body, - style: theme.textTheme.bodyLarge?.copyWith( - color: Palette.ink.withValues(alpha: 0.72), - height: 1.5, - ), + ), + SizedBox(height: 1.h), + Text( + body, + style: theme.textTheme.bodyLarge?.copyWith( + color: Palette.ink.withValues(alpha: 0.72), + height: 1.5, + ), + ), + if (actionLabel != null && onAction != null) ...[ + SizedBox(height: 2.h), + OutlinedButton.icon( + onPressed: () => onAction!(), + icon: const Icon(Icons.refresh), + label: Text(actionLabel!), ), - if (actionLabel != null && onAction != null) ...[ - SizedBox(height: 2.h), - OutlinedButton.icon( - onPressed: () => onAction!(), - icon: const Icon(Icons.refresh), - label: Text(actionLabel!), - ), - ], ], - ), + ], + ), ); } } diff --git a/useragent/macos/Podfile.lock b/useragent/macos/Podfile.lock index 2dad058..26d37b4 100644 --- a/useragent/macos/Podfile.lock +++ b/useragent/macos/Podfile.lock @@ -9,6 +9,8 @@ PODS: - FlutterMacOS (1.0.0) - rive_native (0.0.1): - FlutterMacOS + - rust_lib_arbiter (0.0.1): + - FlutterMacOS - share_plus (0.0.1): - FlutterMacOS @@ -18,6 +20,7 @@ DEPENDENCIES: - flutter_secure_storage_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_darwin/darwin`) - FlutterMacOS (from `Flutter/ephemeral`) - rive_native (from `Flutter/ephemeral/.symlinks/plugins/rive_native/macos`) + - rust_lib_arbiter (from `Flutter/ephemeral/.symlinks/plugins/rust_lib_arbiter/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) EXTERNAL SOURCES: @@ -31,6 +34,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral rive_native: :path: Flutter/ephemeral/.symlinks/plugins/rive_native/macos + rust_lib_arbiter: + :path: Flutter/ephemeral/.symlinks/plugins/rust_lib_arbiter/macos share_plus: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos @@ -40,6 +45,7 @@ SPEC CHECKSUMS: flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 rive_native: 1c53d33e44c2b54424810effea4590671dd220c7 + rust_lib_arbiter: 78dcf27cf17e741c6f4f0b12b64a40980746698a share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc PODFILE CHECKSUM: 224cb1c0d6f5312abfc2477bcb5c7f1fca2574fb diff --git a/useragent/mise.toml b/useragent/mise.toml new file mode 100644 index 0000000..540e6c5 --- /dev/null +++ b/useragent/mise.toml @@ -0,0 +1,4 @@ +[tasks.codegen] +run = ''' + flutter_rust_bridge_codegen generate +''' diff --git a/useragent/pubspec.lock b/useragent/pubspec.lock index a051ec2..ba8a18d 100644 --- a/useragent/pubspec.lock +++ b/useragent/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.5" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95 + url: "https://pub.dev" + source: hosted + version: "2.1.1" build_config: dependency: transitive description: @@ -218,7 +226,7 @@ packages: source: hosted version: "0.3.5+2" crypto: - dependency: "direct main" + dependency: transitive description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf @@ -226,7 +234,7 @@ packages: source: hosted version: "3.0.7" cryptography: - dependency: "direct main" + dependency: transitive description: name: cryptography sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0" @@ -311,6 +319,11 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.1" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_form_builder: dependency: "direct main" description: @@ -343,6 +356,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + flutter_rust_bridge: + dependency: "direct main" + description: + name: flutter_rust_bridge + sha256: e87d6b9ee934dcd24a128ccb2bd91905d2d5fe5c06245d6a8f5477d4907a437a + url: "https://pub.dev" + source: hosted + version: "2.12.0" flutter_secure_storage: dependency: "direct main" description: @@ -433,6 +454,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -537,6 +563,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" intl: dependency: transitive description: @@ -801,6 +832,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.2" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" protobuf: dependency: "direct main" description: @@ -881,6 +920,13 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0+1" + rust_lib_arbiter: + dependency: "direct main" + description: + path: rust_builder + relative: true + source: path + version: "0.0.1" share_plus: dependency: transitive description: @@ -1022,6 +1068,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" talker: dependency: "direct main" description: @@ -1182,6 +1236,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" webkit_inspection_protocol: dependency: transitive description: diff --git a/useragent/pubspec.yaml b/useragent/pubspec.yaml index 9a77966..eb26c37 100644 --- a/useragent/pubspec.yaml +++ b/useragent/pubspec.yaml @@ -21,8 +21,6 @@ dependencies: mtcore: hosted: https://git.markettakers.org/api/packages/MarketTakers/pub/ version: ^1.0.6 - cryptography: ^2.9.0 - crypto: ^3.0.6 flutter_secure_storage: ^10.0.0 cryptography_flutter: ^2.3.4 riverpod_annotation: ^4.0.0 @@ -35,6 +33,9 @@ dependencies: json_annotation: ^4.9.0 timeago: ^3.7.1 flutter_form_builder: ^10.3.0+2 + rust_lib_arbiter: + path: rust_builder + flutter_rust_bridge: 2.12.0 dev_dependencies: flutter_test: @@ -46,6 +47,8 @@ dev_dependencies: auto_route_generator: ^10.4.0 freezed: ^3.2.3 json_serializable: ^6.11.2 + integration_test: + sdk: flutter flutter: uses-material-design: true diff --git a/useragent/rust/.gitignore b/useragent/rust/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/useragent/rust/.gitignore @@ -0,0 +1 @@ +/target diff --git a/useragent/rust/Cargo.lock b/useragent/rust/Cargo.lock new file mode 100644 index 0000000..5442523 --- /dev/null +++ b/useragent/rust/Cargo.lock @@ -0,0 +1,5218 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "accesskit" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5351dcebb14b579ccab05f288596b2ae097005be7ee50a7c3d4ca9d0d5a66f6a" +dependencies = [ + "uuid", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842fd8203e6dfcf531d24f5bac792088edfba7d6b35844fead191603fb32a260" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "phf", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534bc3fdc89a64a1db3c46b33c198fde2b7c3c7d094e5809c8c8bf2970c18243" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e549dd7c6562b6a2ea807b44726e6241707db054a817dc4c7e2b8d3b39bfac" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "static_assertions", + "windows", + "windows-core", +] + +[[package]] +name = "accesskit_winit" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +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 = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.11.0", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.18", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[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", + "log", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbiter-crypto" +version = "0.1.0" +dependencies = [ + "chrono", + "memsafe", + "ml-dsa", + "rand", + "x-wing", +] + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.7.1", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.11.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.11.0", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[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 = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", + "rand_core", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "curve25519-dalek" +version = "5.0.0-pre.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335f1947f241137a14106b6f5acc5918a5ede29c9d71d3f2cb1678d5075d9fc3" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "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 = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.6", +] + +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.1", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "ecolor" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "137c0ce4ce4152ff7e223a7ce22ee1057cdff61fce0a45c32459c3ccec64868d" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "eframe" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6e995b8e434d65aefd12c4519221be3e8f38efd77804ef39ca10553f4ad7063" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "pollster", + "profiling", + "raw-window-handle", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "wgpu", + "windows-sys 0.61.2", + "winit", +] + +[[package]] +name = "egui" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f34aaf627da598dfadd64b0fee6101d22e9c451d1e5348157312720b7f459f0f" +dependencies = [ + "accesskit", + "ahash", + "bitflags 2.11.0", + "emath", + "epaint", + "log", + "nohash-hasher", + "profiling", + "smallvec", + "unicode-segmentation", +] + +[[package]] +name = "egui-wgpu" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71033ff78b041c9c363450f4498ff95468ef3ecbcc71a62f67036a6207d98fa4" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "profiling", + "thiserror 2.0.18", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11a2881b2bf1a305e413e644af63f836737a33d85077705ff808e88f902ff742" +dependencies = [ + "accesskit_winit", + "arboard", + "bytemuck", + "egui", + "log", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", + "profiling", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3b28d39ab6c0cac238190e6cb1e8c9047d02cb470ab942a7a3302e4cb3a8e74" +dependencies = [ + "bytemuck", + "egui", + "glow", + "log", + "memoffset", + "profiling", + "wasm-bindgen", + "web-sys", + "winit", +] + +[[package]] +name = "emath" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a05cd8bdf3b598488c627ca97c7fe8909448ffa26278dd3c7e535cdb554d721" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "epaint" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3017dd67f147a697ee0c8484fb568fd9553e2a0c114be5020dbbc11962841" +dependencies = [ + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "font-types", + "log", + "nohash-hasher", + "parking_lot", + "profiling", + "self_cell", + "skrifa", + "smallvec", + "vello_cpu", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3b85a2bb775a3ab02d077a65cc31575c11b2584581913253cc11ce49f48bba" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fearless_simd" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb2907d1f08b2b316b9223ced5b0e89d87028ba8deae9764741dba8ff7f3903" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "flutter_rust_bridge" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0884853aae8a6517b5b58cf36f55da487f2fe110e1686938eb29b6640aae4a5" +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.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b5ce32f35f710ced8c5aa557f023f1a624e737b5460cee2b70fcd3a8df09e1b" +dependencies = [ + "hex", + "md-5", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9237c6d82152100c691fb77ea18037b402bcc7257d2c876a4ffac81bc22a1c" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "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 = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core", + "wasip2", + "wasip3", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.11.0", + "cfg_aliases", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.18", + "windows", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.11.0", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "ctutils", + "typenum", + "zeroize", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.1", + "rand_core", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memsafe" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f3e199d5e8adf073900f95b635f1192c394a442ed406c16dc7991b74501645" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "ml-dsa" +version = "0.1.0-rc.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5b2bb0ad6fa2b40396775bd56f51345171490fef993f46f91a876ecdbdaea55" +dependencies = [ + "const-oid", + "ctutils", + "hybrid-array", + "module-lattice", + "pkcs8", + "rand_core", + "sha3", + "signature", + "zeroize", +] + +[[package]] +name = "ml-kem" +version = "0.3.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04437cb1a66c0b78740927b76cc61f218344b9f6ef3dd430e283274a718ef0e9" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "rand_core", + "sha3", + "zeroize", +] + +[[package]] +name = "module-lattice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "164eb3faeaecbd14b0b2a917c1b4d0c035097a9c559b0bed85c2cdd032bc8faa" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", + "zeroize", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "naga" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2630921705b9b01dcdd0b6864b9562ca3c1951eecd0f0c4f5f04f61e412647" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.18", + "unicode-ident", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.3", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.11.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "orbclient" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59aed3b33578edcfa1bc96a321d590d31832b6ad55a26f0313362ce687e9abd6" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[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 = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[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 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "peniko" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2b6aadb221872732e87d465213e9be5af2849b0e8cc5300a8ba98fffa2e00a" +dependencies = [ + "bytemuck", + "color", + "kurbo", + "linebender_resource_handle", + "smallvec", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs8" +version = "0.11.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" + +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "font-types", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "rust_lib_arbiter" +version = "0.1.0" +dependencies = [ + "anyhow", + "arbiter-crypto", + "eframe", + "egui", + "flutter_rust_bridge", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +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 = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit 0.19.2", + "tiny-skia", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.2", + "keccak", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "3.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" +dependencies = [ + "digest 0.11.2", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.11.0", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.11.0", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.4", + "thiserror 2.0.18", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +dependencies = [ + "backtrace", + "num_cpus", + "pin-project-lite", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.10+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow 1.0.1", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.1", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.2", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vello_common" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd1a4c633ce09e7d713df1a6e036644a125e15e0c169cfb5180ddf5836ca04b" +dependencies = [ + "bytemuck", + "fearless_simd", + "hashbrown 0.16.1", + "log", + "peniko", + "skrifa", + "smallvec", +] + +[[package]] +name = "vello_cpu" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162bfe48aabf6a9fdcd401b628c7d9f260c2cbabb343c70a65feba6f7849edc" +dependencies = [ + "bytemuck", + "hashbrown 0.16.1", + "vello_common", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.0", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.11.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml 0.39.2", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16" +dependencies = [ + "core-foundation 0.10.1", + "jni", + "log", + "ndk-context", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "url", + "web-sys", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c239a9a747bbd379590985bac952c2e53cb19873f7072b3370c6a6a8e06837" +dependencies = [ + "arrayvec", + "bitflags 2.11.0", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e80ac6cf1895df6342f87d975162108f9d98772a0d74bc404ab7304ac29469e" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.11.0", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.18", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43acd053312501689cd92a01a9638d37f3e41a5fd9534875efa8917ee2d11ac0" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef043bf135cc68b6f667c55ff4e345ce2b5924d75bad36a47921b0287ca4b24a" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-wasm" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7b75e72f49035f000dd5262e4126242e92a090a4fd75931ecfe7e60784e6fa" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "725d5c006a8c02967b6d93ef04f6537ec4593313e330cfe86d9d3f946eb90f28" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a47aef47636562f3937285af4c44b4b5b404b46577471411cc5313a921da7e" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.11.0", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga", + "ndk-sys", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.18", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows", + "windows-core", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4684f4410da0cf95a4cb63bb5edaac022461dedb6adf0b64d0d9b5f6890d51" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec2675540fb1a5cfa5ef122d3d5f390e2c75711a0b946410f2d6ac3a0f77d1f6" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "js-sys", + "log", + "raw-window-handle", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.11.0", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit 0.19.2", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x-wing" +version = "0.1.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d0d5f4d1f26b9b9e7477af1d3bef960e1d1fb64edab7912fde472a8a8432e" +dependencies = [ + "kem", + "ml-kem", + "rand_core", + "sha3", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "x25519-dalek" +version = "3.0.0-pre.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d5d6ff67acd3945b933e592bfa7143db4fcbb2f871754b6b9fbd7847fc5aea" +dependencies = [ + "curve25519-dalek", + "rand_core", + "zeroize", +] + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.11.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.15", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "441a0064125265655bccc3a6af6bef56814d9277ac83fce48b1cd7e160b80eac" +dependencies = [ + "quick-xml 0.38.4", + "serde", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.15", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "winnow 0.7.15", +] diff --git a/useragent/rust/Cargo.toml b/useragent/rust/Cargo.toml new file mode 100644 index 0000000..4cbf5e2 --- /dev/null +++ b/useragent/rust/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rust_lib_arbiter" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "staticlib"] + +[dependencies] +eframe = "0.34.1" +egui = "0.34.1" +flutter_rust_bridge = "=2.12.0" +arbiter-crypto = {path = "../../server/crates/arbiter-crypto"} +anyhow = "1.0.102" + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] } diff --git a/useragent/rust/src/api/mod.rs b/useragent/rust/src/api/mod.rs new file mode 100644 index 0000000..0590ab0 --- /dev/null +++ b/useragent/rust/src/api/mod.rs @@ -0,0 +1,38 @@ +use anyhow::anyhow; +use arbiter_crypto::authn::{self, AuthChallenge, USERAGENT_CONTEXT}; +use flutter_rust_bridge::frb; + +#[frb(opaque)] +pub struct MldsaKey(authn::SigningKey); + +impl MldsaKey { + pub fn from_bytes(bytes: &[u8]) -> anyhow::Result { + let bytes: [u8; 32] = bytes + .try_into() + .map_err(|_| anyhow!("Invalid key length"))?; + Ok(Self(authn::SigningKey::from_seed(bytes))) + } + + pub fn to_bytes(&self) -> Vec { + self.0.to_seed().to_vec() + } + + pub fn sign(&self, message: &[u8]) -> anyhow::Result> { + Ok(self.0.sign_message(message, USERAGENT_CONTEXT)?.to_bytes()) + } + + pub fn generate() -> Self { + Self(authn::SigningKey::generate()) + } + + pub fn get_public_key(&self) -> Vec { + self.0.public_key().to_bytes().to_vec() + } +} + +pub fn format_challenge(random: Vec, timestamp: i64) -> Result, String> { + let challenge = AuthChallenge::from_parts(&random, timestamp) + .map_err(|_| "Invalid nonce length".to_string())?; + + Ok(challenge.format()) +} diff --git a/useragent/rust/src/frb_generated.rs b/useragent/rust/src/frb_generated.rs new file mode 100644 index 0000000..46647d0 --- /dev/null +++ b/useragent/rust/src/frb_generated.rs @@ -0,0 +1,607 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.12.0. + +#![allow( + non_camel_case_types, + unused, + non_snake_case, + clippy::needless_return, + clippy::redundant_closure_call, + clippy::redundant_closure, + clippy::useless_conversion, + clippy::unit_arg, + clippy::unused_unit, + clippy::double_parens, + clippy::let_and_return, + clippy::too_many_arguments, + clippy::match_single_binding, + clippy::clone_on_copy, + clippy::let_unit_value, + clippy::deref_addrof, + clippy::explicit_auto_deref, + clippy::borrow_deref_ref, + clippy::uninlined_format_args, + clippy::needless_borrow +)] + +// Section: imports + +use crate::api::*; +use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; +use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; +use flutter_rust_bridge::{Handler, IntoIntoDart}; + +// Section: boilerplate + +flutter_rust_bridge::frb_generated_boilerplate!( + default_stream_sink_codec = SseCodec, + default_rust_opaque = RustOpaqueMoi, + default_rust_auto_opaque = RustAutoOpaqueMoi, +); +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1247923898; + +// Section: executor + +flutter_rust_bridge::frb_generated_default_handler!(); + +// Section: wire_funcs + +fn wire__crate__api__MldsaKey_from_bytes_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "MldsaKey_from_bytes", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_bytes = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let output_ok = crate::api::MldsaKey::from_bytes(&api_bytes)?; + Ok(output_ok) + })(), + ) + } + }, + ) +} +fn wire__crate__api__MldsaKey_generate_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "MldsaKey_generate", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok(crate::api::MldsaKey::generate())?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__MldsaKey_get_public_key_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "MldsaKey_get_public_key", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok(crate::api::MldsaKey::get_public_key( + &*api_that_guard, + ))?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__MldsaKey_sign_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "MldsaKey_sign", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_message = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = crate::api::MldsaKey::sign(&*api_that_guard, &api_message)?; + Ok(output_ok) + })(), + ) + } + }, + ) +} +fn wire__crate__api__MldsaKey_to_bytes_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "MldsaKey_to_bytes", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Result::<_, ()>::Ok(crate::api::MldsaKey::to_bytes(&*api_that_guard))?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__format_challenge_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "format_challenge", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_random = >::sse_decode(&mut deserializer); + let api_timestamp = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::format_challenge(api_random, api_timestamp)?; + Ok(output_ok) + })()) + } + }, + ) +} + +// Section: related_funcs + +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); + +// Section: dart2rust + +impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::anyhow::anyhow!("{}", inner); + } +} + +impl SseDecode for MldsaKey { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = >::sse_decode(deserializer); + return String::from_utf8(inner).unwrap(); + } +} + +impl SseDecode for i64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_i64::().unwrap() + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = Vec::with_capacity(len_ as usize); + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() + } +} + +impl SseDecode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} +} + +impl SseDecode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u64::().unwrap() as _ + } +} + +impl SseDecode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_i32::().unwrap() + } +} + +impl SseDecode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() != 0 + } +} + +fn pde_ffi_dispatcher_primary_impl( + func_id: i32, + port: flutter_rust_bridge::for_generated::MessagePort, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + 1 => wire__crate__api__MldsaKey_from_bytes_impl(port, ptr, rust_vec_len, data_len), + 2 => wire__crate__api__MldsaKey_generate_impl(port, ptr, rust_vec_len, data_len), + 3 => wire__crate__api__MldsaKey_get_public_key_impl(port, ptr, rust_vec_len, data_len), + 4 => wire__crate__api__MldsaKey_sign_impl(port, ptr, rust_vec_len, data_len), + 5 => wire__crate__api__MldsaKey_to_bytes_impl(port, ptr, rust_vec_len, data_len), + 6 => wire__crate__api__format_challenge_impl(port, ptr, rust_vec_len, data_len), + _ => unreachable!(), + } +} + +fn pde_ffi_dispatcher_sync_impl( + func_id: i32, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + _ => unreachable!(), + } +} + +// Section: rust2dart + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for MldsaKey { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(format!("{:?}", self), serializer); + } +} + +impl SseEncode for MldsaKey { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.into_bytes(), serializer); + } +} + +impl SseEncode for i64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_i64::(self).unwrap(); + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self).unwrap(); + } +} + +impl SseEncode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} +} + +impl SseEncode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer + .cursor + .write_u64::(self as _) + .unwrap(); + } +} + +impl SseEncode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_i32::(self).unwrap(); + } +} + +impl SseEncode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self as _).unwrap(); + } +} + +#[cfg(not(target_family = "wasm"))] +mod io { + // This file is automatically generated, so please do not edit it. + // @generated by `flutter_rust_bridge`@ 2.12.0. + + // Section: imports + + use super::*; + use crate::api::*; + use flutter_rust_bridge::for_generated::byteorder::{ + NativeEndian, ReadBytesExt, WriteBytesExt, + }; + use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::{Handler, IntoIntoDart}; + + // Section: boilerplate + + flutter_rust_bridge::frb_generated_boilerplate_io!(); + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_arbiter_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_arbiter_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } +} +#[cfg(not(target_family = "wasm"))] +pub use io::*; + +/// cbindgen:ignore +#[cfg(target_family = "wasm")] +mod web { + // This file is automatically generated, so please do not edit it. + // @generated by `flutter_rust_bridge`@ 2.12.0. + + // Section: imports + + use super::*; + use crate::api::*; + use flutter_rust_bridge::for_generated::byteorder::{ + NativeEndian, ReadBytesExt, WriteBytesExt, + }; + use flutter_rust_bridge::for_generated::wasm_bindgen; + use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*; + use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::{Handler, IntoIntoDart}; + + // Section: boilerplate + + flutter_rust_bridge::frb_generated_boilerplate_web!(); + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } +} +#[cfg(target_family = "wasm")] +pub use web::*; diff --git a/useragent/rust/src/lib.rs b/useragent/rust/src/lib.rs new file mode 100644 index 0000000..cbb071f --- /dev/null +++ b/useragent/rust/src/lib.rs @@ -0,0 +1,2 @@ +pub mod api; +mod frb_generated; diff --git a/useragent/rust_builder/.gitignore b/useragent/rust_builder/.gitignore new file mode 100644 index 0000000..ac5aa98 --- /dev/null +++ b/useragent/rust_builder/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/useragent/rust_builder/README.md b/useragent/rust_builder/README.md new file mode 100644 index 0000000..922615f --- /dev/null +++ b/useragent/rust_builder/README.md @@ -0,0 +1 @@ +Please ignore this folder, which is just glue to build Rust with Flutter. \ No newline at end of file diff --git a/useragent/rust_builder/android/.gitignore b/useragent/rust_builder/android/.gitignore new file mode 100644 index 0000000..161bdcd --- /dev/null +++ b/useragent/rust_builder/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/useragent/rust_builder/android/build.gradle b/useragent/rust_builder/android/build.gradle new file mode 100644 index 0000000..222ff73 --- /dev/null +++ b/useragent/rust_builder/android/build.gradle @@ -0,0 +1,56 @@ +// The Android Gradle Plugin builds the native code with the Android NDK. + +group 'com.flutter_rust_bridge.rust_lib_arbiter' +version '1.0' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + // The Android Gradle Plugin knows how to build native code with the NDK. + classpath 'com.android.tools.build:gradle:7.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + if (project.android.hasProperty("namespace")) { + namespace 'com.flutter_rust_bridge.rust_lib_arbiter' + } + + // Bumping the plugin compileSdkVersion requires all clients of this plugin + // to bump the version in their app. + compileSdkVersion 33 + + // Use the NDK version + // declared in /android/app/build.gradle file of the Flutter project. + // Replace it with a version number if this plugin requires a specfic NDK version. + // (e.g. ndkVersion "23.1.7779620") + ndkVersion android.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion 19 + } +} + +apply from: "../cargokit/gradle/plugin.gradle" +cargokit { + manifestDir = "../../rust" + libname = "rust_lib_arbiter" +} diff --git a/useragent/rust_builder/android/settings.gradle b/useragent/rust_builder/android/settings.gradle new file mode 100644 index 0000000..cb25a60 --- /dev/null +++ b/useragent/rust_builder/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'rust_lib_arbiter' diff --git a/useragent/rust_builder/android/src/main/AndroidManifest.xml b/useragent/rust_builder/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d22bc1b --- /dev/null +++ b/useragent/rust_builder/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/useragent/rust_builder/cargokit/.gitignore b/useragent/rust_builder/cargokit/.gitignore new file mode 100644 index 0000000..cf7bb86 --- /dev/null +++ b/useragent/rust_builder/cargokit/.gitignore @@ -0,0 +1,4 @@ +target +.dart_tool +*.iml +!pubspec.lock diff --git a/useragent/rust_builder/cargokit/LICENSE b/useragent/rust_builder/cargokit/LICENSE new file mode 100644 index 0000000..d33a5fe --- /dev/null +++ b/useragent/rust_builder/cargokit/LICENSE @@ -0,0 +1,42 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +Copyright 2022 Matej Knopp + +================================================================================ + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +================================================================================ + +APACHE LICENSE, VERSION 2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/useragent/rust_builder/cargokit/README b/useragent/rust_builder/cargokit/README new file mode 100644 index 0000000..398474d --- /dev/null +++ b/useragent/rust_builder/cargokit/README @@ -0,0 +1,11 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +Experimental repository to provide glue for seamlessly integrating cargo build +with flutter plugins and packages. + +See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/ +for a tutorial on how to use Cargokit. + +Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin. + diff --git a/useragent/rust_builder/cargokit/build_pod.sh b/useragent/rust_builder/cargokit/build_pod.sh new file mode 100755 index 0000000..ed0e0d9 --- /dev/null +++ b/useragent/rust_builder/cargokit/build_pod.sh @@ -0,0 +1,58 @@ +#!/bin/sh +set -e + +BASEDIR=$(dirname "$0") + +# Workaround for https://github.com/dart-lang/pub/issues/4010 +BASEDIR=$(cd "$BASEDIR" ; pwd -P) + +# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project +NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"` + +export PATH=${NEW_PATH%?} # remove trailing : + +env + +# Platform name (macosx, iphoneos, iphonesimulator) +export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME + +# Arctive architectures (arm64, armv7, x86_64), space separated. +export CARGOKIT_DARWIN_ARCHS=$ARCHS + +# Current build configuration (Debug, Release) +export CARGOKIT_CONFIGURATION=$CONFIGURATION + +# Path to directory containing Cargo.toml. +export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1 + +# Temporary directory for build artifacts. +export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR + +# Output directory for final artifacts. +export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME + +# Directory to store built tool artifacts. +export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool + +# Directory inside root project. Not necessarily the top level directory of root project. +export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT + +FLUTTER_EXPORT_BUILD_ENVIRONMENT=( + "$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS + "$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS +) + +for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}" +do + if [[ -f "$path" ]]; then + source "$path" + fi +done + +sh "$BASEDIR/run_build_tool.sh" build-pod "$@" + +# Make a symlink from built framework to phony file, which will be used as input to +# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate +# attribute on custom build phase) +ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony" +ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out" diff --git a/useragent/rust_builder/cargokit/build_tool/README.md b/useragent/rust_builder/cargokit/build_tool/README.md new file mode 100644 index 0000000..a878c27 --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/README.md @@ -0,0 +1,5 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +A sample command-line application with an entrypoint in `bin/`, library code +in `lib/`, and example unit test in `test/`. diff --git a/useragent/rust_builder/cargokit/build_tool/analysis_options.yaml b/useragent/rust_builder/cargokit/build_tool/analysis_options.yaml new file mode 100644 index 0000000..0e16a8b --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/analysis_options.yaml @@ -0,0 +1,34 @@ +# This is copied from Cargokit (which is the official way to use it currently) +# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +linter: + rules: + - prefer_relative_imports + - directives_ordering + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/useragent/rust_builder/cargokit/build_tool/bin/build_tool.dart b/useragent/rust_builder/cargokit/build_tool/bin/build_tool.dart new file mode 100644 index 0000000..268eb52 --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/bin/build_tool.dart @@ -0,0 +1,8 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'package:build_tool/build_tool.dart' as build_tool; + +void main(List arguments) { + build_tool.runMain(arguments); +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/build_tool.dart b/useragent/rust_builder/cargokit/build_tool/lib/build_tool.dart new file mode 100644 index 0000000..7c1bb75 --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/build_tool.dart @@ -0,0 +1,8 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'src/build_tool.dart' as build_tool; + +Future runMain(List args) async { + return build_tool.runMain(args); +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/android_environment.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/android_environment.dart new file mode 100644 index 0000000..15fc9ee --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/android_environment.dart @@ -0,0 +1,195 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; +import 'dart:isolate'; +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as path; +import 'package:version/version.dart'; + +import 'target.dart'; +import 'util.dart'; + +class AndroidEnvironment { + AndroidEnvironment({ + required this.sdkPath, + required this.ndkVersion, + required this.minSdkVersion, + required this.targetTempDir, + required this.target, + }); + + static void clangLinkerWrapper(List args) { + final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG']; + if (clang == null) { + throw Exception( + "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var"); + } + final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET']; + if (target == null) { + throw Exception( + "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var"); + } + + runCommand(clang, [ + target, + ...args, + ]); + } + + /// Full path to Android SDK. + final String sdkPath; + + /// Full version of Android NDK. + final String ndkVersion; + + /// Minimum supported SDK version. + final int minSdkVersion; + + /// Target directory for build artifacts. + final String targetTempDir; + + /// Target being built. + final Target target; + + bool ndkIsInstalled() { + final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); + final ndkPackageXml = File(path.join(ndkPath, 'package.xml')); + return ndkPackageXml.existsSync(); + } + + void installNdk({ + required String javaHome, + }) { + final sdkManagerExtension = Platform.isWindows ? '.bat' : ''; + final sdkManager = path.join( + sdkPath, + 'cmdline-tools', + 'latest', + 'bin', + 'sdkmanager$sdkManagerExtension', + ); + + log.info('Installing NDK $ndkVersion'); + runCommand(sdkManager, [ + '--install', + 'ndk;$ndkVersion', + ], environment: { + 'JAVA_HOME': javaHome, + }); + } + + Future> buildEnvironment() async { + final hostArch = Platform.isMacOS + ? "darwin-x86_64" + : (Platform.isLinux ? "linux-x86_64" : "windows-x86_64"); + + final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); + final toolchainPath = path.join( + ndkPath, + 'toolchains', + 'llvm', + 'prebuilt', + hostArch, + 'bin', + ); + + final minSdkVersion = + math.max(target.androidMinSdkVersion!, this.minSdkVersion); + + final exe = Platform.isWindows ? '.exe' : ''; + + final arKey = 'AR_${target.rust}'; + final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe'] + .map((e) => path.join(toolchainPath, e)) + .firstWhereOrNull((element) => File(element).existsSync()); + if (arValue == null) { + throw Exception('Failed to find ar for $target in $toolchainPath'); + } + + final targetArg = '--target=${target.rust}$minSdkVersion'; + + final ccKey = 'CC_${target.rust}'; + final ccValue = path.join(toolchainPath, 'clang$exe'); + final cfFlagsKey = 'CFLAGS_${target.rust}'; + final cFlagsValue = targetArg; + + final cxxKey = 'CXX_${target.rust}'; + final cxxValue = path.join(toolchainPath, 'clang++$exe'); + final cxxFlagsKey = 'CXXFLAGS_${target.rust}'; + final cxxFlagsValue = targetArg; + + final linkerKey = + 'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase(); + + final ranlibKey = 'RANLIB_${target.rust}'; + final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe'); + + final ndkVersionParsed = Version.parse(ndkVersion); + final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS'; + final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed); + + final runRustTool = + Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh'; + + final packagePath = (await Isolate.resolvePackageUri( + Uri.parse('package:build_tool/buildtool.dart')))! + .toFilePath(); + final selfPath = path.canonicalize(path.join( + packagePath, + '..', + '..', + '..', + runRustTool, + )); + + // Make sure that run_build_tool is working properly even initially launched directly + // through dart run. + final toolTempDir = + Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir; + + return { + arKey: arValue, + ccKey: ccValue, + cfFlagsKey: cFlagsValue, + cxxKey: cxxValue, + cxxFlagsKey: cxxFlagsValue, + ranlibKey: ranlibValue, + rustFlagsKey: rustFlagsValue, + linkerKey: selfPath, + // Recognized by main() so we know when we're acting as a wrapper + '_CARGOKIT_NDK_LINK_TARGET': targetArg, + '_CARGOKIT_NDK_LINK_CLANG': ccValue, + 'CARGOKIT_TOOL_TEMP_DIR': toolTempDir, + }; + } + + // Workaround for libgcc missing in NDK23, inspired by cargo-ndk + String _libGccWorkaround(String buildDir, Version ndkVersion) { + final workaroundDir = path.join( + buildDir, + 'cargokit', + 'libgcc_workaround', + '${ndkVersion.major}', + ); + Directory(workaroundDir).createSync(recursive: true); + if (ndkVersion.major >= 23) { + File(path.join(workaroundDir, 'libgcc.a')) + .writeAsStringSync('INPUT(-lunwind)'); + } else { + // Other way around, untested, forward libgcc.a from libunwind once Rust + // gets updated for NDK23+. + File(path.join(workaroundDir, 'libunwind.a')) + .writeAsStringSync('INPUT(-lgcc)'); + } + + var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? ''; + if (rustFlags.isNotEmpty) { + rustFlags = '$rustFlags\x1f'; + } + rustFlags = '$rustFlags-L\x1f$workaroundDir'; + return rustFlags; + } +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart new file mode 100644 index 0000000..e608cec --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart @@ -0,0 +1,266 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:http/http.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'builder.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'rustup.dart'; +import 'target.dart'; + +class Artifact { + /// File system location of the artifact. + final String path; + + /// Actual file name that the artifact should have in destination folder. + final String finalFileName; + + AritifactType get type { + if (finalFileName.endsWith('.dll') || + finalFileName.endsWith('.dll.lib') || + finalFileName.endsWith('.pdb') || + finalFileName.endsWith('.so') || + finalFileName.endsWith('.dylib')) { + return AritifactType.dylib; + } else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) { + return AritifactType.staticlib; + } else { + throw Exception('Unknown artifact type for $finalFileName'); + } + } + + Artifact({ + required this.path, + required this.finalFileName, + }); +} + +final _log = Logger('artifacts_provider'); + +class ArtifactProvider { + ArtifactProvider({ + required this.environment, + required this.userOptions, + }); + + final BuildEnvironment environment; + final CargokitUserOptions userOptions; + + Future>> getArtifacts(List targets) async { + final result = await _getPrecompiledArtifacts(targets); + + final pendingTargets = List.of(targets); + pendingTargets.removeWhere((element) => result.containsKey(element)); + + if (pendingTargets.isEmpty) { + return result; + } + + final rustup = Rustup(); + for (final target in targets) { + final builder = RustBuilder(target: target, environment: environment); + builder.prepare(rustup); + _log.info('Building ${environment.crateInfo.packageName} for $target'); + final targetDir = await builder.build(); + // For local build accept both static and dynamic libraries. + final artifactNames = { + ...getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + aritifactType: AritifactType.dylib, + remote: false, + ), + ...getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + aritifactType: AritifactType.staticlib, + remote: false, + ) + }; + final artifacts = artifactNames + .map((artifactName) => Artifact( + path: path.join(targetDir, artifactName), + finalFileName: artifactName, + )) + .where((element) => File(element.path).existsSync()) + .toList(); + result[target] = artifacts; + } + return result; + } + + Future>> _getPrecompiledArtifacts( + List targets) async { + if (userOptions.usePrecompiledBinaries == false) { + _log.info('Precompiled binaries are disabled'); + return {}; + } + if (environment.crateOptions.precompiledBinaries == null) { + _log.fine('Precompiled binaries not enabled for this crate'); + return {}; + } + + final start = Stopwatch()..start(); + final crateHash = CrateHash.compute(environment.manifestDir, + tempStorage: environment.targetTempDir); + _log.fine( + 'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms'); + + final downloadedArtifactsDir = + path.join(environment.targetTempDir, 'precompiled', crateHash); + Directory(downloadedArtifactsDir).createSync(recursive: true); + + final res = >{}; + + for (final target in targets) { + final requiredArtifacts = getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + remote: true, + ); + final artifactsForTarget = []; + + for (final artifact in requiredArtifacts) { + final fileName = PrecompileBinaries.fileName(target, artifact); + final downloadedPath = path.join(downloadedArtifactsDir, fileName); + if (!File(downloadedPath).existsSync()) { + final signatureFileName = + PrecompileBinaries.signatureFileName(target, artifact); + await _tryDownloadArtifacts( + crateHash: crateHash, + fileName: fileName, + signatureFileName: signatureFileName, + finalPath: downloadedPath, + ); + } + if (File(downloadedPath).existsSync()) { + artifactsForTarget.add(Artifact( + path: downloadedPath, + finalFileName: artifact, + )); + } else { + break; + } + } + + // Only provide complete set of artifacts. + if (artifactsForTarget.length == requiredArtifacts.length) { + _log.fine('Found precompiled artifacts for $target'); + res[target] = artifactsForTarget; + } + } + + return res; + } + + static Future _get(Uri url, {Map? headers}) async { + int attempt = 0; + const maxAttempts = 10; + while (true) { + try { + return await get(url, headers: headers); + } on SocketException catch (e) { + // Try to detect reset by peer error and retry. + if (attempt++ < maxAttempts && + (e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) { + _log.severe( + 'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...'); + await Future.delayed(Duration(seconds: 1)); + continue; + } else { + rethrow; + } + } + } + } + + Future _tryDownloadArtifacts({ + required String crateHash, + required String fileName, + required String signatureFileName, + required String finalPath, + }) async { + final precompiledBinaries = environment.crateOptions.precompiledBinaries!; + final prefix = precompiledBinaries.uriPrefix; + final url = Uri.parse('$prefix$crateHash/$fileName'); + final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName'); + _log.fine('Downloading signature from $signatureUrl'); + final signature = await _get(signatureUrl); + if (signature.statusCode == 404) { + _log.warning( + 'Precompiled binaries not available for crate hash $crateHash ($fileName)'); + return; + } + if (signature.statusCode != 200) { + _log.severe( + 'Failed to download signature $signatureUrl: status ${signature.statusCode}'); + return; + } + _log.fine('Downloading binary from $url'); + final res = await _get(url); + if (res.statusCode != 200) { + _log.severe('Failed to download binary $url: status ${res.statusCode}'); + return; + } + if (verify( + precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) { + File(finalPath).writeAsBytesSync(res.bodyBytes); + } else { + _log.shout('Signature verification failed! Ignoring binary.'); + } + } +} + +enum AritifactType { + staticlib, + dylib, +} + +AritifactType artifactTypeForTarget(Target target) { + if (target.darwinPlatform != null) { + return AritifactType.staticlib; + } else { + return AritifactType.dylib; + } +} + +List getArtifactNames({ + required Target target, + required String libraryName, + required bool remote, + AritifactType? aritifactType, +}) { + aritifactType ??= artifactTypeForTarget(target); + if (target.darwinArch != null) { + if (aritifactType == AritifactType.staticlib) { + return ['lib$libraryName.a']; + } else { + return ['lib$libraryName.dylib']; + } + } else if (target.rust.contains('-windows-')) { + if (aritifactType == AritifactType.staticlib) { + return ['$libraryName.lib']; + } else { + return [ + '$libraryName.dll', + '$libraryName.dll.lib', + if (!remote) '$libraryName.pdb' + ]; + } + } else if (target.rust.contains('-linux-')) { + if (aritifactType == AritifactType.staticlib) { + return ['lib$libraryName.a']; + } else { + return ['lib$libraryName.so']; + } + } else { + throw Exception("Unsupported target: ${target.rust}"); + } +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart new file mode 100644 index 0000000..6f3b2a4 --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart @@ -0,0 +1,40 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; + +class BuildCMake { + final CargokitUserOptions userOptions; + + BuildCMake({required this.userOptions}); + + Future build() async { + final targetPlatform = Environment.targetPlatform; + final target = Target.forFlutterName(Environment.targetPlatform); + if (target == null) { + throw Exception("Unknown target platform: $targetPlatform"); + } + + final environment = BuildEnvironment.fromEnvironment(isAndroid: false); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts([target]); + + final libs = artifacts[target]!; + + for (final lib in libs) { + if (lib.type == AritifactType.dylib) { + File(lib.path) + .copySync(path.join(Environment.outputDir, lib.finalFileName)); + } + } + } +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart new file mode 100644 index 0000000..7e61fcb --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart @@ -0,0 +1,49 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; + +final log = Logger('build_gradle'); + +class BuildGradle { + BuildGradle({required this.userOptions}); + + final CargokitUserOptions userOptions; + + Future build() async { + final targets = Environment.targetPlatforms.map((arch) { + final target = Target.forFlutterName(arch); + if (target == null) { + throw Exception( + "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); + } + return target; + }).toList(); + + final environment = BuildEnvironment.fromEnvironment(isAndroid: true); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts(targets); + + for (final target in targets) { + final libs = artifacts[target]!; + final outputDir = path.join(Environment.outputDir, target.android!); + Directory(outputDir).createSync(recursive: true); + + for (final lib in libs) { + if (lib.type == AritifactType.dylib) { + File(lib.path).copySync(path.join(outputDir, lib.finalFileName)); + } + } + } + } +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/build_pod.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/build_pod.dart new file mode 100644 index 0000000..8a9c0db --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/build_pod.dart @@ -0,0 +1,89 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; +import 'util.dart'; + +class BuildPod { + BuildPod({required this.userOptions}); + + final CargokitUserOptions userOptions; + + Future build() async { + final targets = Environment.darwinArchs.map((arch) { + final target = Target.forDarwin( + platformName: Environment.darwinPlatformName, darwinAarch: arch); + if (target == null) { + throw Exception( + "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); + } + return target; + }).toList(); + + final environment = BuildEnvironment.fromEnvironment(isAndroid: false); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts(targets); + + void performLipo(String targetFile, Iterable sourceFiles) { + runCommand("lipo", [ + '-create', + ...sourceFiles, + '-output', + targetFile, + ]); + } + + final outputDir = Environment.outputDir; + + Directory(outputDir).createSync(recursive: true); + + final staticLibs = artifacts.values + .expand((element) => element) + .where((element) => element.type == AritifactType.staticlib) + .toList(); + final dynamicLibs = artifacts.values + .expand((element) => element) + .where((element) => element.type == AritifactType.dylib) + .toList(); + + final libName = environment.crateInfo.packageName; + + // If there is static lib, use it and link it with pod + if (staticLibs.isNotEmpty) { + final finalTargetFile = path.join(outputDir, "lib$libName.a"); + performLipo(finalTargetFile, staticLibs.map((e) => e.path)); + } else { + // Otherwise try to replace bundle dylib with our dylib + final bundlePaths = [ + '$libName.framework/Versions/A/$libName', + '$libName.framework/$libName', + ]; + + for (final bundlePath in bundlePaths) { + final targetFile = path.join(outputDir, bundlePath); + if (File(targetFile).existsSync()) { + performLipo(targetFile, dynamicLibs.map((e) => e.path)); + + // Replace absolute id with @rpath one so that it works properly + // when moved to Frameworks. + runCommand("install_name_tool", [ + '-id', + '@rpath/$bundlePath', + targetFile, + ]); + return; + } + } + throw Exception('Unable to find bundle for dynamic library'); + } + } +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/build_tool.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/build_tool.dart new file mode 100644 index 0000000..70dfe0e --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/build_tool.dart @@ -0,0 +1,276 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:github/github.dart'; +import 'package:hex/hex.dart'; +import 'package:logging/logging.dart'; + +import 'android_environment.dart'; +import 'build_cmake.dart'; +import 'build_gradle.dart'; +import 'build_pod.dart'; +import 'logging.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'target.dart'; +import 'util.dart'; +import 'verify_binaries.dart'; + +final log = Logger('build_tool'); + +abstract class BuildCommand extends Command { + Future runBuildCommand(CargokitUserOptions options); + + @override + Future run() async { + final options = CargokitUserOptions.load(); + + if (options.verboseLogging || + Platform.environment['CARGOKIT_VERBOSE'] == '1') { + enableVerboseLogging(); + } + + await runBuildCommand(options); + } +} + +class BuildPodCommand extends BuildCommand { + @override + final name = 'build-pod'; + + @override + final description = 'Build cocoa pod library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildPod(userOptions: options); + await build.build(); + } +} + +class BuildGradleCommand extends BuildCommand { + @override + final name = 'build-gradle'; + + @override + final description = 'Build android library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildGradle(userOptions: options); + await build.build(); + } +} + +class BuildCMakeCommand extends BuildCommand { + @override + final name = 'build-cmake'; + + @override + final description = 'Build CMake library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildCMake(userOptions: options); + await build.build(); + } +} + +class GenKeyCommand extends Command { + @override + final name = 'gen-key'; + + @override + final description = 'Generate key pair for signing precompiled binaries'; + + @override + void run() { + final kp = generateKey(); + final private = HEX.encode(kp.privateKey.bytes); + final public = HEX.encode(kp.publicKey.bytes); + print("Private Key: $private"); + print("Public Key: $public"); + } +} + +class PrecompileBinariesCommand extends Command { + PrecompileBinariesCommand() { + argParser + ..addOption( + 'repository', + mandatory: true, + help: 'Github repository slug in format owner/name', + ) + ..addOption( + 'manifest-dir', + mandatory: true, + help: 'Directory containing Cargo.toml', + ) + ..addMultiOption('target', + help: 'Rust target triple of artifact to build.\n' + 'Can be specified multiple times or omitted in which case\n' + 'all targets for current platform will be built.') + ..addOption( + 'android-sdk-location', + help: 'Location of Android SDK (if available)', + ) + ..addOption( + 'android-ndk-version', + help: 'Android NDK version (if available)', + ) + ..addOption( + 'android-min-sdk-version', + help: 'Android minimum rquired version (if available)', + ) + ..addOption( + 'temp-dir', + help: 'Directory to store temporary build artifacts', + ) + ..addOption( + 'glibc-version', + help: 'GLIBC version to use for linux builds', + ) + ..addFlag( + "verbose", + abbr: "v", + defaultsTo: false, + help: "Enable verbose logging", + ); + } + + @override + final name = 'precompile-binaries'; + + @override + final description = 'Prebuild and upload binaries\n' + 'Private key must be passed through PRIVATE_KEY environment variable. ' + 'Use gen_key through generate priave key.\n' + 'Github token must be passed as GITHUB_TOKEN environment variable.\n'; + + @override + Future run() async { + final verbose = argResults!['verbose'] as bool; + if (verbose) { + enableVerboseLogging(); + } + + final privateKeyString = Platform.environment['PRIVATE_KEY']; + if (privateKeyString == null) { + throw ArgumentError('Missing PRIVATE_KEY environment variable'); + } + final githubToken = Platform.environment['GITHUB_TOKEN']; + if (githubToken == null) { + throw ArgumentError('Missing GITHUB_TOKEN environment variable'); + } + final privateKey = HEX.decode(privateKeyString); + if (privateKey.length != 64) { + throw ArgumentError('Private key must be 64 bytes long'); + } + final manifestDir = argResults!['manifest-dir'] as String; + if (!Directory(manifestDir).existsSync()) { + throw ArgumentError('Manifest directory does not exist: $manifestDir'); + } + String? androidMinSdkVersionString = + argResults!['android-min-sdk-version'] as String?; + int? androidMinSdkVersion; + if (androidMinSdkVersionString != null) { + androidMinSdkVersion = int.tryParse(androidMinSdkVersionString); + if (androidMinSdkVersion == null) { + throw ArgumentError( + 'Invalid android-min-sdk-version: $androidMinSdkVersionString'); + } + } + final targetStrigns = argResults!['target'] as List; + final targets = targetStrigns.map((target) { + final res = Target.forRustTriple(target); + if (res == null) { + throw ArgumentError('Invalid target: $target'); + } + return res; + }).toList(growable: false); + final precompileBinaries = PrecompileBinaries( + privateKey: PrivateKey(privateKey), + githubToken: githubToken, + manifestDir: manifestDir, + repositorySlug: RepositorySlug.full(argResults!['repository'] as String), + targets: targets, + androidSdkLocation: argResults!['android-sdk-location'] as String?, + androidNdkVersion: argResults!['android-ndk-version'] as String?, + androidMinSdkVersion: androidMinSdkVersion, + tempDir: argResults!['temp-dir'] as String?, + glibcVersion: argResults!['glibc-version'] as String?, + ); + + await precompileBinaries.run(); + } +} + +class VerifyBinariesCommand extends Command { + VerifyBinariesCommand() { + argParser.addOption( + 'manifest-dir', + mandatory: true, + help: 'Directory containing Cargo.toml', + ); + } + + @override + final name = "verify-binaries"; + + @override + final description = 'Verifies published binaries\n' + 'Checks whether there is a binary published for each targets\n' + 'and checks the signature.'; + + @override + Future run() async { + final manifestDir = argResults!['manifest-dir'] as String; + final verifyBinaries = VerifyBinaries( + manifestDir: manifestDir, + ); + await verifyBinaries.run(); + } +} + +Future runMain(List args) async { + try { + // Init logging before options are loaded + initLogging(); + + if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) { + return AndroidEnvironment.clangLinkerWrapper(args); + } + + final runner = CommandRunner('build_tool', 'Cargokit built_tool') + ..addCommand(BuildPodCommand()) + ..addCommand(BuildGradleCommand()) + ..addCommand(BuildCMakeCommand()) + ..addCommand(GenKeyCommand()) + ..addCommand(PrecompileBinariesCommand()) + ..addCommand(VerifyBinariesCommand()); + + await runner.run(args); + } on ArgumentError catch (e) { + stderr.writeln(e.toString()); + exit(1); + } catch (e, s) { + log.severe(kDoubleSeparator); + log.severe('Cargokit BuildTool failed with error:'); + log.severe(kSeparator); + log.severe(e); + // This tells user to install Rust, there's no need to pollute the log with + // stack trace. + if (e is! RustupNotFoundException) { + log.severe(kSeparator); + log.severe(s); + log.severe(kSeparator); + log.severe('BuildTool arguments: $args'); + } + log.severe(kDoubleSeparator); + exit(1); + } +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/builder.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/builder.dart new file mode 100644 index 0000000..cd5269f --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/builder.dart @@ -0,0 +1,209 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'package:collection/collection.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'android_environment.dart'; +import 'cargo.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'rustup.dart'; +import 'target.dart'; +import 'util.dart'; + +final _log = Logger('builder'); + +enum BuildConfiguration { + debug, + release, + profile, +} + +extension on BuildConfiguration { + bool get isDebug => this == BuildConfiguration.debug; + String get rustName => switch (this) { + BuildConfiguration.debug => 'debug', + BuildConfiguration.release => 'release', + BuildConfiguration.profile => 'release', + }; +} + +class BuildException implements Exception { + final String message; + + BuildException(this.message); + + @override + String toString() { + return 'BuildException: $message'; + } +} + +class BuildEnvironment { + final BuildConfiguration configuration; + final CargokitCrateOptions crateOptions; + final String targetTempDir; + final String manifestDir; + final CrateInfo crateInfo; + + final bool isAndroid; + final String? androidSdkPath; + final String? androidNdkVersion; + final int? androidMinSdkVersion; + final String? javaHome; + + final String? glibcVersion; + + BuildEnvironment({ + required this.configuration, + required this.crateOptions, + required this.targetTempDir, + required this.manifestDir, + required this.crateInfo, + required this.isAndroid, + this.androidSdkPath, + this.androidNdkVersion, + this.androidMinSdkVersion, + this.javaHome, + this.glibcVersion, + }); + + static BuildConfiguration parseBuildConfiguration(String value) { + // XCode configuration adds the flavor to configuration name. + final firstSegment = value.split('-').first; + final buildConfiguration = BuildConfiguration.values.firstWhereOrNull( + (e) => e.name == firstSegment, + ); + if (buildConfiguration == null) { + _log.warning('Unknown build configuraiton $value, will assume release'); + return BuildConfiguration.release; + } + return buildConfiguration; + } + + static BuildEnvironment fromEnvironment({ + required bool isAndroid, + }) { + final buildConfiguration = + parseBuildConfiguration(Environment.configuration); + final manifestDir = Environment.manifestDir; + final crateOptions = CargokitCrateOptions.load( + manifestDir: manifestDir, + ); + final crateInfo = CrateInfo.load(manifestDir); + return BuildEnvironment( + configuration: buildConfiguration, + crateOptions: crateOptions, + targetTempDir: Environment.targetTempDir, + manifestDir: manifestDir, + crateInfo: crateInfo, + isAndroid: isAndroid, + androidSdkPath: isAndroid ? Environment.sdkPath : null, + androidNdkVersion: isAndroid ? Environment.ndkVersion : null, + androidMinSdkVersion: + isAndroid ? int.parse(Environment.minSdkVersion) : null, + javaHome: isAndroid ? Environment.javaHome : null, + ); + } +} + +class RustBuilder { + final Target target; + final BuildEnvironment environment; + + RustBuilder({ + required this.target, + required this.environment, + }); + + void prepare( + Rustup rustup, + ) { + final toolchain = _toolchain; + if (rustup.installedTargets(toolchain) == null) { + rustup.installToolchain(toolchain); + } + if (toolchain == 'nightly') { + rustup.installRustSrcForNightly(); + } + if (!rustup.installedTargets(toolchain)!.contains(target.rust)) { + rustup.installTarget(target.rust, toolchain: toolchain); + } + if (environment.glibcVersion != null) { + rustup.installZigBuild(toolchain); + } + } + + CargoBuildOptions? get _buildOptions => + environment.crateOptions.cargo[environment.configuration]; + + String get _toolchain => _buildOptions?.toolchain.name ?? 'stable'; + + /// Returns the path of directory containing build artifacts. + Future build() async { + final extraArgs = _buildOptions?.flags ?? []; + final manifestPath = path.join(environment.manifestDir, 'Cargo.toml'); + runCommand( + 'rustup', + [ + 'run', + _toolchain, + 'cargo', + (target.android == null && environment.glibcVersion != null) + ? 'zigbuild' + : 'build', + ...extraArgs, + '--manifest-path', + manifestPath, + '-p', + environment.crateInfo.packageName, + if (!environment.configuration.isDebug) '--release', + '--target', + target.rust + + ((target.android == null && environment.glibcVersion != null) + ? '.${environment.glibcVersion!}' + : ""), + '--target-dir', + environment.targetTempDir, + ], + environment: await _buildEnvironment(), + ); + return path.join( + environment.targetTempDir, + target.rust, + environment.configuration.rustName, + ); + } + + Future> _buildEnvironment() async { + if (target.android == null) { + return {}; + } else { + final sdkPath = environment.androidSdkPath; + final ndkVersion = environment.androidNdkVersion; + final minSdkVersion = environment.androidMinSdkVersion; + if (sdkPath == null) { + throw BuildException('androidSdkPath is not set'); + } + if (ndkVersion == null) { + throw BuildException('androidNdkVersion is not set'); + } + if (minSdkVersion == null) { + throw BuildException('androidMinSdkVersion is not set'); + } + final env = AndroidEnvironment( + sdkPath: sdkPath, + ndkVersion: ndkVersion, + minSdkVersion: minSdkVersion, + targetTempDir: environment.targetTempDir, + target: target, + ); + if (!env.ndkIsInstalled() && environment.javaHome != null) { + env.installNdk(javaHome: environment.javaHome!); + } + return env.buildEnvironment(); + } + } +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/cargo.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/cargo.dart new file mode 100644 index 0000000..0d8958f --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/cargo.dart @@ -0,0 +1,48 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:toml/toml.dart'; + +class ManifestException { + ManifestException(this.message, {required this.fileName}); + + final String? fileName; + final String message; + + @override + String toString() { + if (fileName != null) { + return 'Failed to parse package manifest at $fileName: $message'; + } else { + return 'Failed to parse package manifest: $message'; + } + } +} + +class CrateInfo { + CrateInfo({required this.packageName}); + + final String packageName; + + static CrateInfo parseManifest(String manifest, {final String? fileName}) { + final toml = TomlDocument.parse(manifest); + final package = toml.toMap()['package']; + if (package == null) { + throw ManifestException('Missing package section', fileName: fileName); + } + final name = package['name']; + if (name == null) { + throw ManifestException('Missing package name', fileName: fileName); + } + return CrateInfo(packageName: name); + } + + static CrateInfo load(String manifestDir) { + final manifestFile = File(path.join(manifestDir, 'Cargo.toml')); + final manifest = manifestFile.readAsStringSync(); + return parseManifest(manifest, fileName: manifestFile.path); + } +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart new file mode 100644 index 0000000..0c4d88d --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart @@ -0,0 +1,124 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; +import 'package:path/path.dart' as path; + +class CrateHash { + /// Computes a hash uniquely identifying crate content. This takes into account + /// content all all .rs files inside the src directory, as well as Cargo.toml, + /// Cargo.lock, build.rs and cargokit.yaml. + /// + /// If [tempStorage] is provided, computed hash is stored in a file in that directory + /// and reused on subsequent calls if the crate content hasn't changed. + static String compute(String manifestDir, {String? tempStorage}) { + return CrateHash._( + manifestDir: manifestDir, + tempStorage: tempStorage, + )._compute(); + } + + CrateHash._({ + required this.manifestDir, + required this.tempStorage, + }); + + String _compute() { + final files = getFiles(); + final tempStorage = this.tempStorage; + if (tempStorage != null) { + final quickHash = _computeQuickHash(files); + final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash')); + quickHashFolder.createSync(recursive: true); + final quickHashFile = File(path.join(quickHashFolder.path, quickHash)); + if (quickHashFile.existsSync()) { + return quickHashFile.readAsStringSync(); + } + final hash = _computeHash(files); + quickHashFile.writeAsStringSync(hash); + return hash; + } else { + return _computeHash(files); + } + } + + /// Computes a quick hash based on files stat (without reading contents). This + /// is used to cache the real hash, which is slower to compute since it involves + /// reading every single file. + String _computeQuickHash(List files) { + final output = AccumulatorSink(); + final input = sha256.startChunkedConversion(output); + + final data = ByteData(8); + for (final file in files) { + input.add(utf8.encode(file.path)); + final stat = file.statSync(); + data.setUint64(0, stat.size); + input.add(data.buffer.asUint8List()); + data.setUint64(0, stat.modified.millisecondsSinceEpoch); + input.add(data.buffer.asUint8List()); + } + + input.close(); + return base64Url.encode(output.events.single.bytes); + } + + String _computeHash(List files) { + final output = AccumulatorSink(); + final input = sha256.startChunkedConversion(output); + + void addTextFile(File file) { + // text Files are hashed by lines in case we're dealing with github checkout + // that auto-converts line endings. + final splitter = LineSplitter(); + if (file.existsSync()) { + final data = file.readAsStringSync(); + final lines = splitter.convert(data); + for (final line in lines) { + input.add(utf8.encode(line)); + } + } + } + + for (final file in files) { + addTextFile(file); + } + + input.close(); + final res = output.events.single; + + // Truncate to 128bits. + final hash = res.bytes.sublist(0, 16); + return hex.encode(hash); + } + + List getFiles() { + final src = Directory(path.join(manifestDir, 'src')); + final files = src + .listSync(recursive: true, followLinks: false) + .whereType() + .toList(); + files.sortBy((element) => element.path); + void addFile(String relative) { + final file = File(path.join(manifestDir, relative)); + if (file.existsSync()) { + files.add(file); + } + } + + addFile('Cargo.toml'); + addFile('Cargo.lock'); + addFile('build.rs'); + addFile('cargokit.yaml'); + return files; + } + + final String manifestDir; + final String? tempStorage; +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/environment.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/environment.dart new file mode 100644 index 0000000..996483a --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/environment.dart @@ -0,0 +1,68 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +extension on String { + String resolveSymlink() => File(this).resolveSymbolicLinksSync(); +} + +class Environment { + /// Current build configuration (debug or release). + static String get configuration => + _getEnv("CARGOKIT_CONFIGURATION").toLowerCase(); + + static bool get isDebug => configuration == 'debug'; + static bool get isRelease => configuration == 'release'; + + /// Temporary directory where Rust build artifacts are placed. + static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR"); + + /// Final output directory where the build artifacts are placed. + static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR'); + + /// Path to the crate manifest (containing Cargo.toml). + static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR'); + + /// Directory inside root project. Not necessarily root folder. Symlinks are + /// not resolved on purpose. + static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR'); + + // Pod + + /// Platform name (macosx, iphoneos, iphonesimulator). + static String get darwinPlatformName => + _getEnv("CARGOKIT_DARWIN_PLATFORM_NAME"); + + /// List of architectures to build for (arm64, armv7, x86_64). + static List get darwinArchs => + _getEnv("CARGOKIT_DARWIN_ARCHS").split(' '); + + // Gradle + static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION"); + static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION"); + static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR"); + static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME"); + static List get targetPlatforms => + _getEnv("CARGOKIT_TARGET_PLATFORMS").split(','); + + // CMAKE + static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM"); + + static String _getEnv(String key) { + final res = Platform.environment[key]; + if (res == null) { + throw Exception("Missing environment variable $key"); + } + return res; + } + + static String _getEnvPath(String key) { + final res = _getEnv(key); + if (Directory(res).existsSync()) { + return res.resolveSymlink(); + } else { + return res; + } + } +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/logging.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/logging.dart new file mode 100644 index 0000000..5edd4fd --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/logging.dart @@ -0,0 +1,52 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:logging/logging.dart'; + +const String kSeparator = "--"; +const String kDoubleSeparator = "=="; + +bool _lastMessageWasSeparator = false; + +void _log(LogRecord rec) { + final prefix = '${rec.level.name}: '; + final out = rec.level == Level.SEVERE ? stderr : stdout; + if (rec.message == kSeparator) { + if (!_lastMessageWasSeparator) { + out.write(prefix); + out.writeln('-' * 80); + _lastMessageWasSeparator = true; + } + return; + } else if (rec.message == kDoubleSeparator) { + out.write(prefix); + out.writeln('=' * 80); + _lastMessageWasSeparator = true; + return; + } + out.write(prefix); + out.writeln(rec.message); + _lastMessageWasSeparator = false; +} + +void initLogging() { + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen((LogRecord rec) { + final lines = rec.message.split('\n'); + for (final line in lines) { + if (line.isNotEmpty || lines.length == 1 || line != lines.last) { + _log(LogRecord( + rec.level, + line, + rec.loggerName, + )); + } + } + }); +} + +void enableVerboseLogging() { + Logger.root.level = Level.ALL; +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/options.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/options.dart new file mode 100644 index 0000000..22aef1d --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/options.dart @@ -0,0 +1,309 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:hex/hex.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; +import 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +import 'builder.dart'; +import 'environment.dart'; +import 'rustup.dart'; + +final _log = Logger('options'); + +/// A class for exceptions that have source span information attached. +class SourceSpanException implements Exception { + // This is a getter so that subclasses can override it. + /// A message describing the exception. + String get message => _message; + final String _message; + + // This is a getter so that subclasses can override it. + /// The span associated with this exception. + /// + /// This may be `null` if the source location can't be determined. + SourceSpan? get span => _span; + final SourceSpan? _span; + + SourceSpanException(this._message, this._span); + + /// Returns a string representation of `this`. + /// + /// [color] may either be a [String], a [bool], or `null`. If it's a string, + /// it indicates an ANSI terminal color escape that should be used to + /// highlight the span's text. If it's `true`, it indicates that the text + /// should be highlighted using the default color. If it's `false` or `null`, + /// it indicates that the text shouldn't be highlighted. + @override + String toString({Object? color}) { + if (span == null) return message; + return 'Error on ${span!.message(message, color: color)}'; + } +} + +enum Toolchain { + stable, + beta, + nightly, +} + +class CargoBuildOptions { + final Toolchain toolchain; + final List flags; + + CargoBuildOptions({ + required this.toolchain, + required this.flags, + }); + + static Toolchain _toolchainFromNode(YamlNode node) { + if (node case YamlScalar(value: String name)) { + final toolchain = + Toolchain.values.firstWhereOrNull((element) => element.name == name); + if (toolchain != null) { + return toolchain; + } + } + throw SourceSpanException( + 'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.', + node.span); + } + + static CargoBuildOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargo options must be a map', node.span); + } + Toolchain toolchain = Toolchain.stable; + List flags = []; + for (final MapEntry(:key, :value) in node.nodes.entries) { + if (key case YamlScalar(value: 'toolchain')) { + toolchain = _toolchainFromNode(value); + } else if (key case YamlScalar(value: 'extra_flags')) { + if (value case YamlList(nodes: List list)) { + if (list.every((element) { + if (element case YamlScalar(value: String _)) { + return true; + } + return false; + })) { + flags = list.map((e) => e.value as String).toList(); + continue; + } + } + throw SourceSpanException( + 'Extra flags must be a list of strings', value.span); + } else { + throw SourceSpanException( + 'Unknown cargo option type. Must be "toolchain" or "extra_flags".', + key.span); + } + } + return CargoBuildOptions(toolchain: toolchain, flags: flags); + } +} + +extension on YamlMap { + /// Map that extracts keys so that we can do map case check on them. + Map get valueMap => + nodes.map((key, value) => MapEntry(key.value, value)); +} + +class PrecompiledBinaries { + final String uriPrefix; + final PublicKey publicKey; + + PrecompiledBinaries({ + required this.uriPrefix, + required this.publicKey, + }); + + static PublicKey _publicKeyFromHex(String key, SourceSpan? span) { + final bytes = HEX.decode(key); + if (bytes.length != 32) { + throw SourceSpanException( + 'Invalid public key. Must be 32 bytes long.', span); + } + return PublicKey(bytes); + } + + static PrecompiledBinaries parse(YamlNode node) { + if (node case YamlMap(valueMap: Map map)) { + if (map + case { + 'url_prefix': YamlNode urlPrefixNode, + 'public_key': YamlNode publicKeyNode, + }) { + final urlPrefix = switch (urlPrefixNode) { + YamlScalar(value: String urlPrefix) => urlPrefix, + _ => throw SourceSpanException( + 'Invalid URL prefix value.', urlPrefixNode.span), + }; + final publicKey = switch (publicKeyNode) { + YamlScalar(value: String publicKey) => + _publicKeyFromHex(publicKey, publicKeyNode.span), + _ => throw SourceSpanException( + 'Invalid public key value.', publicKeyNode.span), + }; + return PrecompiledBinaries( + uriPrefix: urlPrefix, + publicKey: publicKey, + ); + } + } + throw SourceSpanException( + 'Invalid precompiled binaries value. ' + 'Expected Map with "url_prefix" and "public_key".', + node.span); + } +} + +/// Cargokit options specified for Rust crate. +class CargokitCrateOptions { + CargokitCrateOptions({ + this.cargo = const {}, + this.precompiledBinaries, + }); + + final Map cargo; + final PrecompiledBinaries? precompiledBinaries; + + static CargokitCrateOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargokit options must be a map', node.span); + } + final options = {}; + PrecompiledBinaries? precompiledBinaries; + + for (final entry in node.nodes.entries) { + if (entry + case MapEntry( + key: YamlScalar(value: 'cargo'), + value: YamlNode node, + )) { + if (node is! YamlMap) { + throw SourceSpanException('Cargo options must be a map', node.span); + } + for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) { + if (key case YamlScalar(value: String name)) { + final configuration = BuildConfiguration.values + .firstWhereOrNull((element) => element.name == name); + if (configuration != null) { + options[configuration] = CargoBuildOptions.parse(value); + continue; + } + } + throw SourceSpanException( + 'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.', + key.span); + } + } else if (entry.key case YamlScalar(value: 'precompiled_binaries')) { + precompiledBinaries = PrecompiledBinaries.parse(entry.value); + } else { + throw SourceSpanException( + 'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".', + entry.key.span); + } + } + return CargokitCrateOptions( + cargo: options, + precompiledBinaries: precompiledBinaries, + ); + } + + static CargokitCrateOptions load({ + required String manifestDir, + }) { + final uri = Uri.file(path.join(manifestDir, "cargokit.yaml")); + final file = File.fromUri(uri); + if (file.existsSync()) { + final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri); + return parse(contents); + } else { + return CargokitCrateOptions(); + } + } +} + +class CargokitUserOptions { + // When Rustup is installed always build locally unless user opts into + // using precompiled binaries. + static bool defaultUsePrecompiledBinaries() { + return Rustup.executablePath() == null; + } + + CargokitUserOptions({ + required this.usePrecompiledBinaries, + required this.verboseLogging, + }); + + CargokitUserOptions._() + : usePrecompiledBinaries = defaultUsePrecompiledBinaries(), + verboseLogging = false; + + static CargokitUserOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargokit options must be a map', node.span); + } + bool usePrecompiledBinaries = defaultUsePrecompiledBinaries(); + bool verboseLogging = false; + + for (final entry in node.nodes.entries) { + if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) { + if (entry.value case YamlScalar(value: bool value)) { + usePrecompiledBinaries = value; + continue; + } + throw SourceSpanException( + 'Invalid value for "use_precompiled_binaries". Must be a boolean.', + entry.value.span); + } else if (entry.key case YamlScalar(value: 'verbose_logging')) { + if (entry.value case YamlScalar(value: bool value)) { + verboseLogging = value; + continue; + } + throw SourceSpanException( + 'Invalid value for "verbose_logging". Must be a boolean.', + entry.value.span); + } else { + throw SourceSpanException( + 'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".', + entry.key.span); + } + } + return CargokitUserOptions( + usePrecompiledBinaries: usePrecompiledBinaries, + verboseLogging: verboseLogging, + ); + } + + static CargokitUserOptions load() { + String fileName = "cargokit_options.yaml"; + var userProjectDir = Directory(Environment.rootProjectDir); + + while (userProjectDir.parent.path != userProjectDir.path) { + final configFile = File(path.join(userProjectDir.path, fileName)); + if (configFile.existsSync()) { + final contents = loadYamlNode( + configFile.readAsStringSync(), + sourceUrl: configFile.uri, + ); + final res = parse(contents); + if (res.verboseLogging) { + _log.info('Found user options file at ${configFile.path}'); + } + return res; + } + userProjectDir = userProjectDir.parent; + } + return CargokitUserOptions._(); + } + + final bool usePrecompiledBinaries; + final bool verboseLogging; +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart new file mode 100644 index 0000000..019859c --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart @@ -0,0 +1,205 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:github/github.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'cargo.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'rustup.dart'; +import 'target.dart'; + +final _log = Logger('precompile_binaries'); + +class PrecompileBinaries { + PrecompileBinaries({ + required this.privateKey, + required this.githubToken, + required this.repositorySlug, + required this.manifestDir, + required this.targets, + this.androidSdkLocation, + this.androidNdkVersion, + this.androidMinSdkVersion, + this.tempDir, + this.glibcVersion, + }); + + final PrivateKey privateKey; + final String githubToken; + final RepositorySlug repositorySlug; + final String manifestDir; + final List targets; + final String? androidSdkLocation; + final String? androidNdkVersion; + final int? androidMinSdkVersion; + final String? tempDir; + final String? glibcVersion; + + static String fileName(Target target, String name) { + return '${target.rust}_$name'; + } + + static String signatureFileName(Target target, String name) { + return '${target.rust}_$name.sig'; + } + + Future run() async { + final crateInfo = CrateInfo.load(manifestDir); + + final targets = List.of(this.targets); + if (targets.isEmpty) { + targets.addAll([ + ...Target.buildableTargets(), + if (androidSdkLocation != null) ...Target.androidTargets(), + ]); + } + + _log.info('Precompiling binaries for $targets'); + + final hash = CrateHash.compute(manifestDir); + _log.info('Computed crate hash: $hash'); + + final String tagName = 'precompiled_$hash'; + + final github = GitHub(auth: Authentication.withToken(githubToken)); + final repo = github.repositories; + final release = await _getOrCreateRelease( + repo: repo, + tagName: tagName, + packageName: crateInfo.packageName, + hash: hash, + ); + + final tempDir = this.tempDir != null + ? Directory(this.tempDir!) + : Directory.systemTemp.createTempSync('precompiled_'); + + tempDir.createSync(recursive: true); + + final crateOptions = CargokitCrateOptions.load( + manifestDir: manifestDir, + ); + + final buildEnvironment = BuildEnvironment( + configuration: BuildConfiguration.release, + crateOptions: crateOptions, + targetTempDir: tempDir.path, + manifestDir: manifestDir, + crateInfo: crateInfo, + isAndroid: androidSdkLocation != null, + androidSdkPath: androidSdkLocation, + androidNdkVersion: androidNdkVersion, + androidMinSdkVersion: androidMinSdkVersion, + glibcVersion: glibcVersion, + ); + + final rustup = Rustup(); + + for (final target in targets) { + final artifactNames = getArtifactNames( + target: target, + libraryName: crateInfo.packageName, + remote: true, + ); + + if (artifactNames.every((name) { + final fileName = PrecompileBinaries.fileName(target, name); + return (release.assets ?? []).any((e) => e.name == fileName); + })) { + _log.info("All artifacts for $target already exist - skipping"); + continue; + } + + _log.info('Building for $target'); + + final builder = + RustBuilder(target: target, environment: buildEnvironment); + builder.prepare(rustup); + final res = await builder.build(); + + final assets = []; + for (final name in artifactNames) { + final file = File(path.join(res, name)); + if (!file.existsSync()) { + throw Exception('Missing artifact: ${file.path}'); + } + + final data = file.readAsBytesSync(); + final create = CreateReleaseAsset( + name: PrecompileBinaries.fileName(target, name), + contentType: "application/octet-stream", + assetData: data, + ); + final signature = sign(privateKey, data); + final signatureCreate = CreateReleaseAsset( + name: signatureFileName(target, name), + contentType: "application/octet-stream", + assetData: signature, + ); + bool verified = verify(public(privateKey), data, signature); + if (!verified) { + throw Exception('Signature verification failed'); + } + assets.add(create); + assets.add(signatureCreate); + } + _log.info('Uploading assets: ${assets.map((e) => e.name)}'); + for (final asset in assets) { + // This seems to be failing on CI so do it one by one + int retryCount = 0; + while (true) { + try { + await repo.uploadReleaseAssets(release, [asset]); + break; + } on Exception catch (e) { + if (retryCount == 10) { + rethrow; + } + ++retryCount; + _log.shout( + 'Upload failed (attempt $retryCount, will retry): ${e.toString()}'); + await Future.delayed(Duration(seconds: 2)); + } + } + } + } + + _log.info('Cleaning up'); + tempDir.deleteSync(recursive: true); + } + + Future _getOrCreateRelease({ + required RepositoriesService repo, + required String tagName, + required String packageName, + required String hash, + }) async { + Release release; + try { + _log.info('Fetching release $tagName'); + release = await repo.getReleaseByTagName(repositorySlug, tagName); + } on ReleaseNotFound { + _log.info('Release not found - creating release $tagName'); + release = await repo.createRelease( + repositorySlug, + CreateRelease.from( + tagName: tagName, + name: 'Precompiled binaries ${hash.substring(0, 8)}', + targetCommitish: null, + isDraft: false, + isPrerelease: false, + body: 'Precompiled binaries for crate $packageName, ' + 'crate hash $hash.', + )); + } + return release; + } +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/rustup.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/rustup.dart new file mode 100644 index 0000000..e46722b --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/rustup.dart @@ -0,0 +1,149 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as path; + +import 'util.dart'; + +class _Toolchain { + _Toolchain( + this.name, + this.targets, + ); + + final String name; + final List targets; +} + +class Rustup { + List? installedTargets(String toolchain) { + final targets = _installedTargets(toolchain); + return targets != null ? List.unmodifiable(targets) : null; + } + + void installToolchain(String toolchain) { + log.info("Installing Rust toolchain: $toolchain"); + runCommand("rustup", ['toolchain', 'install', toolchain]); + _installedToolchains + .add(_Toolchain(toolchain, _getInstalledTargets(toolchain))); + } + + void installTarget( + String target, { + required String toolchain, + }) { + log.info("Installing Rust target: $target"); + runCommand("rustup", ['target', 'add', '--toolchain', toolchain, target]); + _installedTargets(toolchain)?.add(target); + } + + bool _didInstallZigBuild = false; + + void installZigBuild(String toolchain) { + if (_didInstallZigBuild) { + return; + } + + log.info("Installing Zig build"); + runCommand("rustup", [ + 'run', + toolchain, + 'cargo', + 'install', + '--locked', + 'cargo-zigbuild', + ]); + _didInstallZigBuild = true; + } + + final List<_Toolchain> _installedToolchains; + + Rustup() : _installedToolchains = _getInstalledToolchains(); + + List? _installedTargets(String toolchain) => _installedToolchains + .firstWhereOrNull( + (e) => e.name == toolchain || e.name.startsWith('$toolchain-')) + ?.targets; + + static List<_Toolchain> _getInstalledToolchains() { + String extractToolchainName(String line) { + // ignore (default) after toolchain name + final parts = line.split(' '); + return parts[0]; + } + + final res = runCommand("rustup", ['toolchain', 'list']); + + // To list all non-custom toolchains, we need to filter out lines that + // don't start with "stable", "beta", or "nightly". + Pattern nonCustom = RegExp(r"^(stable|beta|nightly)"); + final lines = res.stdout + .toString() + .split('\n') + .where((e) => e.isNotEmpty && e.startsWith(nonCustom)) + .map(extractToolchainName) + .toList(growable: true); + + return lines + .map( + (name) => _Toolchain( + name, + _getInstalledTargets(name), + ), + ) + .toList(growable: true); + } + + static List _getInstalledTargets(String toolchain) { + final res = runCommand("rustup", [ + 'target', + 'list', + '--toolchain', + toolchain, + '--installed', + ]); + final lines = res.stdout + .toString() + .split('\n') + .where((e) => e.isNotEmpty) + .toList(growable: true); + return lines; + } + + bool _didInstallRustSrcForNightly = false; + + void installRustSrcForNightly() { + if (_didInstallRustSrcForNightly) { + return; + } + // Useful for -Z build-std + runCommand( + "rustup", + ['component', 'add', 'rust-src', '--toolchain', 'nightly'], + ); + _didInstallRustSrcForNightly = true; + } + + static String? executablePath() { + final envPath = Platform.environment['PATH']; + final envPathSeparator = Platform.isWindows ? ';' : ':'; + final home = Platform.isWindows + ? Platform.environment['USERPROFILE'] + : Platform.environment['HOME']; + final paths = [ + if (home != null) path.join(home, '.cargo', 'bin'), + if (envPath != null) ...envPath.split(envPathSeparator), + ]; + for (final p in paths) { + final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup'; + final rustupPath = path.join(p, rustup); + if (File(rustupPath).existsSync()) { + return rustupPath; + } + } + return null; + } +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/target.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/target.dart new file mode 100644 index 0000000..624504e --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/target.dart @@ -0,0 +1,147 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; + +import 'util.dart'; + +class Target { + Target({ + required this.rust, + this.flutter, + this.android, + this.androidMinSdkVersion, + this.darwinPlatform, + this.darwinArch, + }); + + static final all = [ + Target( + rust: 'armv7-linux-androideabi', + flutter: 'android-arm', + android: 'armeabi-v7a', + androidMinSdkVersion: 16, + ), + Target( + rust: 'aarch64-linux-android', + flutter: 'android-arm64', + android: 'arm64-v8a', + androidMinSdkVersion: 21, + ), + Target( + rust: 'i686-linux-android', + flutter: 'android-x86', + android: 'x86', + androidMinSdkVersion: 16, + ), + Target( + rust: 'x86_64-linux-android', + flutter: 'android-x64', + android: 'x86_64', + androidMinSdkVersion: 21, + ), + Target( + rust: 'x86_64-pc-windows-msvc', + flutter: 'windows-x64', + ), + Target( + rust: 'aarch64-pc-windows-msvc', + flutter: 'windows-arm64', + ), + Target( + rust: 'x86_64-unknown-linux-gnu', + flutter: 'linux-x64', + ), + Target( + rust: 'aarch64-unknown-linux-gnu', + flutter: 'linux-arm64', + ), + Target(rust: 'riscv64gc-unknown-linux-gnu', flutter: 'linux-riscv64'), + Target( + rust: 'x86_64-apple-darwin', + darwinPlatform: 'macosx', + darwinArch: 'x86_64', + ), + Target( + rust: 'aarch64-apple-darwin', + darwinPlatform: 'macosx', + darwinArch: 'arm64', + ), + Target( + rust: 'aarch64-apple-ios', + darwinPlatform: 'iphoneos', + darwinArch: 'arm64', + ), + Target( + rust: 'aarch64-apple-ios-sim', + darwinPlatform: 'iphonesimulator', + darwinArch: 'arm64', + ), + Target( + rust: 'x86_64-apple-ios', + darwinPlatform: 'iphonesimulator', + darwinArch: 'x86_64', + ), + ]; + + static Target? forFlutterName(String flutterName) { + return all.firstWhereOrNull((element) => element.flutter == flutterName); + } + + static Target? forDarwin({ + required String platformName, + required String darwinAarch, + }) { + return all.firstWhereOrNull((element) => // + element.darwinPlatform == platformName && + element.darwinArch == darwinAarch); + } + + static Target? forRustTriple(String triple) { + return all.firstWhereOrNull((element) => element.rust == triple); + } + + static List androidTargets() { + return all + .where((element) => element.android != null) + .toList(growable: false); + } + + /// Returns buildable targets on current host platform ignoring Android targets. + static List buildableTargets() { + if (Platform.isLinux) { + // Right now we don't support cross-compiling on Linux. So we just return + // the host target. + final arch = (runCommand('arch', []).stdout as String).trim(); + if (arch == 'aarch64') { + return [Target.forRustTriple('aarch64-unknown-linux-gnu')!]; + } else if (arch == 'riscv64') { + return [Target.forRustTriple('riscv64gc-unknown-linux-gnu')!]; + } else { + return [Target.forRustTriple('x86_64-unknown-linux-gnu')!]; + } + } + return all.where((target) { + if (Platform.isWindows) { + return target.rust.contains('-windows-'); + } else if (Platform.isMacOS) { + return target.darwinPlatform != null; + } + return false; + }).toList(growable: false); + } + + @override + String toString() { + return rust; + } + + final String? flutter; + final String rust; + final String? android; + final int? androidMinSdkVersion; + final String? darwinPlatform; + final String? darwinArch; +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/util.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/util.dart new file mode 100644 index 0000000..8bb6a87 --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/util.dart @@ -0,0 +1,172 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:convert'; +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'logging.dart'; +import 'rustup.dart'; + +final log = Logger("process"); + +class CommandFailedException implements Exception { + final String executable; + final List arguments; + final ProcessResult result; + + CommandFailedException({ + required this.executable, + required this.arguments, + required this.result, + }); + + @override + String toString() { + final stdout = result.stdout.toString().trim(); + final stderr = result.stderr.toString().trim(); + return [ + "External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}", + "Returned Exit Code: ${result.exitCode}", + kSeparator, + "STDOUT:", + if (stdout.isNotEmpty) stdout, + kSeparator, + "STDERR:", + if (stderr.isNotEmpty) stderr, + ].join('\n'); + } +} + +class TestRunCommandArgs { + final String executable; + final List arguments; + final String? workingDirectory; + final Map? environment; + final bool includeParentEnvironment; + final bool runInShell; + final Encoding? stdoutEncoding; + final Encoding? stderrEncoding; + + TestRunCommandArgs({ + required this.executable, + required this.arguments, + this.workingDirectory, + this.environment, + this.includeParentEnvironment = true, + this.runInShell = false, + this.stdoutEncoding, + this.stderrEncoding, + }); +} + +class TestRunCommandResult { + TestRunCommandResult({ + this.pid = 1, + this.exitCode = 0, + this.stdout = '', + this.stderr = '', + }); + + final int pid; + final int exitCode; + final String stdout; + final String stderr; +} + +TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride; + +ProcessResult runCommand( + String executable, + List arguments, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + Encoding? stdoutEncoding = systemEncoding, + Encoding? stderrEncoding = systemEncoding, +}) { + if (testRunCommandOverride != null) { + final result = testRunCommandOverride!(TestRunCommandArgs( + executable: executable, + arguments: arguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding, + )); + return ProcessResult( + result.pid, + result.exitCode, + result.stdout, + result.stderr, + ); + } + log.finer('Running command $executable ${arguments.join(' ')}'); + final res = Process.runSync( + _resolveExecutable(executable), + arguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stderrEncoding: stderrEncoding, + stdoutEncoding: stdoutEncoding, + ); + if (res.exitCode != 0) { + throw CommandFailedException( + executable: executable, + arguments: arguments, + result: res, + ); + } else { + return res; + } +} + +class RustupNotFoundException implements Exception { + @override + String toString() { + return [ + ' ', + 'rustup not found in PATH.', + ' ', + 'Maybe you need to install Rust? It only takes a minute:', + ' ', + if (Platform.isWindows) 'https://www.rust-lang.org/tools/install', + if (hasHomebrewRustInPath()) ...[ + '\$ brew unlink rust # Unlink homebrew Rust from PATH', + ], + if (!Platform.isWindows) + "\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh", + ' ', + ].join('\n'); + } + + static bool hasHomebrewRustInPath() { + if (!Platform.isMacOS) { + return false; + } + final envPath = Platform.environment['PATH'] ?? ''; + final paths = envPath.split(':'); + return paths.any((p) { + return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync(); + }); + } +} + +String _resolveExecutable(String executable) { + if (executable == 'rustup') { + final resolved = Rustup.executablePath(); + if (resolved != null) { + return resolved; + } + throw RustupNotFoundException(); + } else { + return executable; + } +} diff --git a/useragent/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart b/useragent/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart new file mode 100644 index 0000000..2366b57 --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart @@ -0,0 +1,84 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:http/http.dart'; + +import 'artifacts_provider.dart'; +import 'cargo.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'target.dart'; + +class VerifyBinaries { + VerifyBinaries({ + required this.manifestDir, + }); + + final String manifestDir; + + Future run() async { + final crateInfo = CrateInfo.load(manifestDir); + + final config = CargokitCrateOptions.load(manifestDir: manifestDir); + final precompiledBinaries = config.precompiledBinaries; + if (precompiledBinaries == null) { + stdout.writeln('Crate does not support precompiled binaries.'); + } else { + final crateHash = CrateHash.compute(manifestDir); + stdout.writeln('Crate hash: $crateHash'); + + for (final target in Target.all) { + final message = 'Checking ${target.rust}...'; + stdout.write(message.padRight(40)); + stdout.flush(); + + final artifacts = getArtifactNames( + target: target, + libraryName: crateInfo.packageName, + remote: true, + ); + + final prefix = precompiledBinaries.uriPrefix; + + bool ok = true; + + for (final artifact in artifacts) { + final fileName = PrecompileBinaries.fileName(target, artifact); + final signatureFileName = + PrecompileBinaries.signatureFileName(target, artifact); + + final url = Uri.parse('$prefix$crateHash/$fileName'); + final signatureUrl = + Uri.parse('$prefix$crateHash/$signatureFileName'); + + final signature = await get(signatureUrl); + if (signature.statusCode != 200) { + stdout.writeln('MISSING'); + ok = false; + break; + } + final asset = await get(url); + if (asset.statusCode != 200) { + stdout.writeln('MISSING'); + ok = false; + break; + } + + if (!verify(precompiledBinaries.publicKey, asset.bodyBytes, + signature.bodyBytes)) { + stdout.writeln('INVALID SIGNATURE'); + ok = false; + } + } + + if (ok) { + stdout.writeln('OK'); + } + } + } + } +} diff --git a/useragent/rust_builder/cargokit/build_tool/pubspec.lock b/useragent/rust_builder/cargokit/build_tool/pubspec.lock new file mode 100644 index 0000000..343bdd3 --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/pubspec.lock @@ -0,0 +1,453 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + url: "https://pub.dev" + source: hosted + version: "64.0.0" + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + args: + dependency: "direct main" + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + collection: + dependency: "direct main" + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: "direct main" + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + url: "https://pub.dev" + source: hosted + version: "1.6.3" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + ed25519_edwards: + dependency: "direct main" + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + github: + dependency: "direct main" + description: + name: github + sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411" + url: "https://pub.dev" + source: hosted + version: "9.17.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + hex: + dependency: "direct main" + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + http: + dependency: "direct main" + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: "direct main" + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: "direct main" + description: + name: path + sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb" + url: "https://pub.dev" + source: hosted + version: "1.8.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: "direct main" + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9" + url: "https://pub.dev" + source: hosted + version: "1.24.6" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265" + url: "https://pub.dev" + source: hosted + version: "0.5.6" + toml: + dependency: "direct main" + description: + name: toml + sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44" + url: "https://pub.dev" + source: hosted + version: "0.14.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + version: + dependency: "direct main" + description: + name: version + sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d" + url: "https://pub.dev" + source: hosted + version: "11.9.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + yaml: + dependency: "direct main" + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.0.0 <4.0.0" diff --git a/useragent/rust_builder/cargokit/build_tool/pubspec.yaml b/useragent/rust_builder/cargokit/build_tool/pubspec.yaml new file mode 100644 index 0000000..18c61e3 --- /dev/null +++ b/useragent/rust_builder/cargokit/build_tool/pubspec.yaml @@ -0,0 +1,33 @@ +# This is copied from Cargokit (which is the official way to use it currently) +# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +name: build_tool +description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build. +publish_to: none +version: 1.0.0 + +environment: + sdk: ">=3.0.0 <4.0.0" + +# Add regular dependencies here. +dependencies: + # these are pinned on purpose because the bundle_tool_runner doesn't have + # pubspec.lock. See run_build_tool.sh + logging: 1.2.0 + path: 1.8.0 + version: 3.0.0 + collection: 1.18.0 + ed25519_edwards: 0.3.1 + hex: 0.2.0 + yaml: 3.1.2 + source_span: 1.10.0 + github: 9.17.0 + args: 2.4.2 + crypto: 3.0.3 + convert: 3.1.1 + http: 1.1.0 + toml: 0.14.0 + +dev_dependencies: + lints: ^2.1.0 + test: ^1.24.0 diff --git a/useragent/rust_builder/cargokit/cmake/cargokit.cmake b/useragent/rust_builder/cargokit/cmake/cargokit.cmake new file mode 100644 index 0000000..ddd05df --- /dev/null +++ b/useragent/rust_builder/cargokit/cmake/cargokit.cmake @@ -0,0 +1,99 @@ +SET(cargokit_cmake_root "${CMAKE_CURRENT_LIST_DIR}/..") + +# Workaround for https://github.com/dart-lang/pub/issues/4010 +get_filename_component(cargokit_cmake_root "${cargokit_cmake_root}" REALPATH) + +if(WIN32) + # REALPATH does not properly resolve symlinks on windows :-/ + execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_LIST_DIR}/resolve_symlinks.ps1" "${cargokit_cmake_root}" OUTPUT_VARIABLE cargokit_cmake_root OUTPUT_STRIP_TRAILING_WHITESPACE) +endif() + +# Arguments +# - target: CMAKE target to which rust library is linked +# - manifest_dir: relative path from current folder to directory containing cargo manifest +# - lib_name: cargo package name +# - any_symbol_name: name of any exported symbol from the library. +# used on windows to force linking with library. +function(apply_cargokit target manifest_dir lib_name any_symbol_name) + + set(CARGOKIT_LIB_NAME "${lib_name}") + set(CARGOKIT_LIB_FULL_NAME "${CMAKE_SHARED_MODULE_PREFIX}${CARGOKIT_LIB_NAME}${CMAKE_SHARED_MODULE_SUFFIX}") + if (CMAKE_CONFIGURATION_TYPES) + set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/$") + set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/$/${CARGOKIT_LIB_FULL_NAME}") + else() + set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}") + set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/${CARGOKIT_LIB_FULL_NAME}") + endif() + set(CARGOKIT_TEMP_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargokit_build") + + if (FLUTTER_TARGET_PLATFORM) + set(CARGOKIT_TARGET_PLATFORM "${FLUTTER_TARGET_PLATFORM}") + else() + set(CARGOKIT_TARGET_PLATFORM "windows-x64") + endif() + + set(CARGOKIT_ENV + "CARGOKIT_CMAKE=${CMAKE_COMMAND}" + "CARGOKIT_CONFIGURATION=$" + "CARGOKIT_MANIFEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${manifest_dir}" + "CARGOKIT_TARGET_TEMP_DIR=${CARGOKIT_TEMP_DIR}" + "CARGOKIT_OUTPUT_DIR=${CARGOKIT_OUTPUT_DIR}" + "CARGOKIT_TARGET_PLATFORM=${CARGOKIT_TARGET_PLATFORM}" + "CARGOKIT_TOOL_TEMP_DIR=${CARGOKIT_TEMP_DIR}/tool" + "CARGOKIT_ROOT_PROJECT_DIR=${CMAKE_SOURCE_DIR}" + ) + + if (WIN32) + set(SCRIPT_EXTENSION ".cmd") + set(IMPORT_LIB_EXTENSION ".lib") + else() + set(SCRIPT_EXTENSION ".sh") + set(IMPORT_LIB_EXTENSION "") + execute_process(COMMAND chmod +x "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}") + endif() + + # Using generators in custom command is only supported in CMake 3.20+ + if (CMAKE_CONFIGURATION_TYPES AND ${CMAKE_VERSION} VERSION_LESS "3.20.0") + foreach(CONFIG IN LISTS CMAKE_CONFIGURATION_TYPES) + add_custom_command( + OUTPUT + "${CMAKE_CURRENT_BINARY_DIR}/${CONFIG}/${CARGOKIT_LIB_FULL_NAME}" + "${CMAKE_CURRENT_BINARY_DIR}/_phony_" + COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} + "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake + VERBATIM + ) + endforeach() + else() + add_custom_command( + OUTPUT + ${OUTPUT_LIB} + "${CMAKE_CURRENT_BINARY_DIR}/_phony_" + COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} + "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake + VERBATIM + ) + endif() + + + set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/_phony_" PROPERTIES SYMBOLIC TRUE) + + if (TARGET ${target}) + # If we have actual cmake target provided create target and make existing + # target depend on it + add_custom_target("${target}_cargokit" DEPENDS ${OUTPUT_LIB}) + add_dependencies("${target}" "${target}_cargokit") + target_link_libraries("${target}" PRIVATE "${OUTPUT_LIB}${IMPORT_LIB_EXTENSION}") + if(WIN32) + target_link_options(${target} PRIVATE "/INCLUDE:${any_symbol_name}") + endif() + else() + # Otherwise (FFI) just use ALL to force building always + add_custom_target("${target}_cargokit" ALL DEPENDS ${OUTPUT_LIB}) + endif() + + # Allow adding the output library to plugin bundled libraries + set("${target}_cargokit_lib" ${OUTPUT_LIB} PARENT_SCOPE) + +endfunction() diff --git a/useragent/rust_builder/cargokit/cmake/resolve_symlinks.ps1 b/useragent/rust_builder/cargokit/cmake/resolve_symlinks.ps1 new file mode 100644 index 0000000..2ac593a --- /dev/null +++ b/useragent/rust_builder/cargokit/cmake/resolve_symlinks.ps1 @@ -0,0 +1,34 @@ +function Resolve-Symlinks { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [string] $Path + ) + + [string] $separator = '/' + [string[]] $parts = $Path.Split($separator) + + [string] $realPath = '' + foreach ($part in $parts) { + if ($realPath -and !$realPath.EndsWith($separator)) { + $realPath += $separator + } + + $realPath += $part.Replace('\', '/') + + # The slash is important when using Get-Item on Drive letters in pwsh. + if (-not($realPath.Contains($separator)) -and $realPath.EndsWith(':')) { + $realPath += '/' + } + + $item = Get-Item $realPath + if ($item.LinkTarget) { + $realPath = $item.LinkTarget.Replace('\', '/') + } + } + $realPath +} + +$path = Resolve-Symlinks -Path $args[0] +Write-Host $path diff --git a/useragent/rust_builder/cargokit/gradle/plugin.gradle b/useragent/rust_builder/cargokit/gradle/plugin.gradle new file mode 100644 index 0000000..68ff649 --- /dev/null +++ b/useragent/rust_builder/cargokit/gradle/plugin.gradle @@ -0,0 +1,184 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import java.nio.file.Paths +import org.apache.tools.ant.taskdefs.condition.Os + +CargoKitPlugin.file = buildscript.sourceFile + +apply plugin: CargoKitPlugin + +class CargoKitExtension { + String manifestDir; // Relative path to folder containing Cargo.toml + String libname; // Library name within Cargo.toml. Must be a cdylib +} + +abstract class CargoKitBuildTask extends DefaultTask { + + @Input + String buildMode + + @Input + String buildDir + + @Input + String outputDir + + @Input + String ndkVersion + + @Input + String sdkDirectory + + @Input + int compileSdkVersion; + + @Input + int minSdkVersion; + + @Input + String pluginFile + + @Input + List targetPlatforms + + @TaskAction + def build() { + if (project.cargokit.manifestDir == null) { + throw new GradleException("Property 'manifestDir' must be set on cargokit extension"); + } + + if (project.cargokit.libname == null) { + throw new GradleException("Property 'libname' must be set on cargokit extension"); + } + + def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh" + def path = Paths.get(new File(pluginFile).parent, "..", executableName); + + def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir) + + def rootProjectDir = project.rootProject.projectDir + + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + project.exec { + commandLine 'chmod', '+x', path + } + } + + project.exec { + executable path + args "build-gradle" + environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir + environment "CARGOKIT_TOOL_TEMP_DIR", "${buildDir}/build_tool" + environment "CARGOKIT_MANIFEST_DIR", manifestDir + environment "CARGOKIT_CONFIGURATION", buildMode + environment "CARGOKIT_TARGET_TEMP_DIR", buildDir + environment "CARGOKIT_OUTPUT_DIR", outputDir + environment "CARGOKIT_NDK_VERSION", ndkVersion + environment "CARGOKIT_SDK_DIR", sdkDirectory + environment "CARGOKIT_COMPILE_SDK_VERSION", compileSdkVersion + environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion + environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",") + environment "CARGOKIT_JAVA_HOME", System.properties['java.home'] + } + } +} + +class CargoKitPlugin implements Plugin { + + static String file; + + private Plugin findFlutterPlugin(Project rootProject) { + _findFlutterPlugin(rootProject.childProjects) + } + + private Plugin _findFlutterPlugin(Map projects) { + for (project in projects) { + for (plugin in project.value.getPlugins()) { + if (plugin.class.name == "com.flutter.gradle.FlutterPlugin" || plugin.class.name == "FlutterPlugin") { + return plugin; + } + } + def plugin = _findFlutterPlugin(project.value.childProjects); + if (plugin != null) { + return plugin; + } + } + return null; + } + + @Override + void apply(Project project) { + def plugin = findFlutterPlugin(project.rootProject); + + project.extensions.create("cargokit", CargoKitExtension) + + if (plugin == null) { + print("Flutter plugin not found, CargoKit plugin will not be applied.") + return; + } + + def cargoBuildDir = "${project.buildDir}/build" + + // Determine if the project is an application or library + def isApplication = plugin.project.plugins.hasPlugin('com.android.application') + def variants = isApplication ? plugin.project.android.applicationVariants : plugin.project.android.libraryVariants + + variants.all { variant -> + + final buildType = variant.buildType.name + + def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}"; + def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs; + jniLibs.srcDir(new File(cargoOutputDir)) + + def List platforms + try { + platforms = com.flutter.gradle.FlutterPluginUtils.getTargetPlatforms(project).collect() + } catch (Exception ignored) { + platforms = plugin.getTargetPlatforms().collect() + } + + // Same thing addFlutterDependencies does in flutter.gradle + if (buildType == "debug") { + platforms.add("android-x86") + platforms.add("android-x64") + } + + // The task name depends on plugin properties, which are not available + // at this point + project.getGradle().afterProject { + def taskName = "cargokitCargoBuild${project.cargokit.libname.capitalize()}${buildType.capitalize()}"; + + if (project.tasks.findByName(taskName)) { + return + } + + if (plugin.project.android.ndkVersion == null) { + throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.") + } + + def task = project.tasks.create(taskName, CargoKitBuildTask.class) { + buildMode = variant.buildType.name + buildDir = cargoBuildDir + outputDir = cargoOutputDir + ndkVersion = plugin.project.android.ndkVersion + sdkDirectory = plugin.project.android.sdkDirectory + minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int + compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int + targetPlatforms = platforms + pluginFile = CargoKitPlugin.file + } + def onTask = { newTask -> + if (newTask.name == "merge${buildType.capitalize()}NativeLibs") { + newTask.dependsOn task + // Fix gradle 7.4.2 not picking up JNI library changes + newTask.outputs.upToDateWhen { false } + } + } + project.tasks.each onTask + project.tasks.whenTaskAdded onTask + } + } + } +} diff --git a/useragent/rust_builder/cargokit/run_build_tool.cmd b/useragent/rust_builder/cargokit/run_build_tool.cmd new file mode 100755 index 0000000..c45d0aa --- /dev/null +++ b/useragent/rust_builder/cargokit/run_build_tool.cmd @@ -0,0 +1,91 @@ +@echo off +setlocal + +setlocal ENABLEDELAYEDEXPANSION + +SET BASEDIR=%~dp0 + +if not exist "%CARGOKIT_TOOL_TEMP_DIR%" ( + mkdir "%CARGOKIT_TOOL_TEMP_DIR%" +) +cd /D "%CARGOKIT_TOOL_TEMP_DIR%" + +SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool +SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart + +set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/% + +( + echo name: build_tool_runner + echo version: 1.0.0 + echo publish_to: none + echo. + echo environment: + echo sdk: '^>=3.0.0 ^<4.0.0' + echo. + echo dependencies: + echo build_tool: + echo path: %BUILD_TOOL_PKG_DIR_POSIX% +) >pubspec.yaml + +if not exist bin ( + mkdir bin +) + +( + echo import 'package:build_tool/build_tool.dart' as build_tool; + echo void main^(List^ args^) ^{ + echo build_tool.runMain^(args^); + echo ^} +) >bin\build_tool_runner.dart + +SET PRECOMPILED=bin\build_tool_runner.dill + +REM To detect changes in package we compare output of DIR /s (recursive) +set PREV_PACKAGE_INFO=.dart_tool\package_info.prev +set CUR_PACKAGE_INFO=.dart_tool\package_info.cur + +DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig" + +REM Last line in dir output is free space on harddrive. That is bound to +REM change between invocation so we need to remove it +( + Set "Line=" + For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do ( + SetLocal EnableDelayedExpansion + If Defined Line Echo !Line! + EndLocal + Set "Line=%%A") +) >"%CUR_PACKAGE_INFO%" +DEL "%CUR_PACKAGE_INFO%_orig" + +REM Compare current directory listing with previous +FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1 + +If %ERRORLEVEL% neq 0 ( + REM Changed - copy current to previous and remove precompiled kernel + if exist "%PREV_PACKAGE_INFO%" ( + DEL "%PREV_PACKAGE_INFO%" + ) + MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" + if exist "%PRECOMPILED%" ( + DEL "%PRECOMPILED%" + ) +) + +REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO% +REM which means we need to do pub get and precompile +if not exist "%PRECOMPILED%" ( + echo Running pub get in "%cd%" + "%DART%" pub get --no-precompile + "%DART%" compile kernel bin/build_tool_runner.dart +) + +"%DART%" "%PRECOMPILED%" %* + +REM 253 means invalid snapshot version. +If %ERRORLEVEL% equ 253 ( + "%DART%" pub get --no-precompile + "%DART%" compile kernel bin/build_tool_runner.dart + "%DART%" "%PRECOMPILED%" %* +) diff --git a/useragent/rust_builder/cargokit/run_build_tool.sh b/useragent/rust_builder/cargokit/run_build_tool.sh new file mode 100755 index 0000000..24b0ed8 --- /dev/null +++ b/useragent/rust_builder/cargokit/run_build_tool.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +set -e + +BASEDIR=$(dirname "$0") + +mkdir -p "$CARGOKIT_TOOL_TEMP_DIR" + +cd "$CARGOKIT_TOOL_TEMP_DIR" + +# Write a very simple bin package in temp folder that depends on build_tool package +# from Cargokit. This is done to ensure that we don't pollute Cargokit folder +# with .dart_tool contents. + +BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool" + +if [[ -z $FLUTTER_ROOT ]]; then # not defined + DART=dart +else + DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart" +fi + +cat << EOF > "pubspec.yaml" +name: build_tool_runner +version: 1.0.0 +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + build_tool: + path: "$BUILD_TOOL_PKG_DIR" +EOF + +mkdir -p "bin" + +cat << EOF > "bin/build_tool_runner.dart" +import 'package:build_tool/build_tool.dart' as build_tool; +void main(List args) { + build_tool.runMain(args); +} +EOF + +# Create alias for `shasum` if it does not exist and `sha1sum` exists +if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then + shopt -s expand_aliases + alias shasum="sha1sum" +fi + +# Dart run will not cache any package that has a path dependency, which +# is the case for our build_tool_runner. So instead we precompile the package +# ourselves. +# To invalidate the cached kernel we use the hash of ls -LR of the build_tool +# package directory. This should be good enough, as the build_tool package +# itself is not meant to have any path dependencies. + +if [[ "$OSTYPE" == "darwin"* ]]; then + PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum) +else + PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum) +fi + +PACKAGE_HASH_FILE=".package_hash" + +if [ -f "$PACKAGE_HASH_FILE" ]; then + EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE") + if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then + rm "$PACKAGE_HASH_FILE" + fi +fi + +# Run pub get if needed. +if [ ! -f "$PACKAGE_HASH_FILE" ]; then + "$DART" pub get --no-precompile + "$DART" compile kernel bin/build_tool_runner.dart + echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE" +fi + +# Rebuild the tool if it was deleted by Android Studio +if [ ! -f "bin/build_tool_runner.dill" ]; then + "$DART" compile kernel bin/build_tool_runner.dart +fi + +set +e + +"$DART" bin/build_tool_runner.dill "$@" + +exit_code=$? + +# 253 means invalid snapshot version. +if [ $exit_code == 253 ]; then + "$DART" pub get --no-precompile + "$DART" compile kernel bin/build_tool_runner.dart + "$DART" bin/build_tool_runner.dill "$@" + exit_code=$? +fi + +exit $exit_code diff --git a/useragent/rust_builder/ios/Classes/dummy_file.c b/useragent/rust_builder/ios/Classes/dummy_file.c new file mode 100644 index 0000000..e06dab9 --- /dev/null +++ b/useragent/rust_builder/ios/Classes/dummy_file.c @@ -0,0 +1 @@ +// This is an empty file to force CocoaPods to create a framework. diff --git a/useragent/rust_builder/ios/rust_lib_arbiter.podspec b/useragent/rust_builder/ios/rust_lib_arbiter.podspec new file mode 100644 index 0000000..2e27d0b --- /dev/null +++ b/useragent/rust_builder/ios/rust_lib_arbiter.podspec @@ -0,0 +1,45 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint rust_lib_arbiter.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'rust_lib_arbiter' + s.version = '0.0.1' + s.summary = 'A new Flutter FFI plugin project.' + s.description = <<-DESC +A new Flutter FFI plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + # This will ensure the source files in Classes/ are included in the native + # builds of apps using this FFI plugin. Podspec does not support relative + # paths, so Classes contains a forwarder C file that relatively imports + # `../src/*` so that the C sources can be shared among all target platforms. + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + + s.script_phase = { + :name => 'Build Rust library', + # First argument is relative path to the `rust` folder, second is name of rust library + :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../rust rust_lib_arbiter', + :execution_position => :before_compile, + :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], + # Let XCode know that the static library referenced in -force_load below is + # created by this build step. + :output_files => ["${BUILT_PRODUCTS_DIR}/librust_lib_arbiter.a"], + } + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + # Flutter.framework does not contain a i386 slice. + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/librust_lib_arbiter.a', + } +end \ No newline at end of file diff --git a/useragent/rust_builder/linux/CMakeLists.txt b/useragent/rust_builder/linux/CMakeLists.txt new file mode 100644 index 0000000..15b5dd9 --- /dev/null +++ b/useragent/rust_builder/linux/CMakeLists.txt @@ -0,0 +1,19 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +# Project-level configuration. +set(PROJECT_NAME "rust_lib_arbiter") +project(${PROJECT_NAME} LANGUAGES CXX) + +include("../cargokit/cmake/cargokit.cmake") +apply_cargokit(${PROJECT_NAME} ../../rust rust_lib_arbiter "") + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(rust_lib_arbiter_bundled_libraries + "${${PROJECT_NAME}_cargokit_lib}" + PARENT_SCOPE +) diff --git a/useragent/rust_builder/macos/Classes/dummy_file.c b/useragent/rust_builder/macos/Classes/dummy_file.c new file mode 100644 index 0000000..e06dab9 --- /dev/null +++ b/useragent/rust_builder/macos/Classes/dummy_file.c @@ -0,0 +1 @@ +// This is an empty file to force CocoaPods to create a framework. diff --git a/useragent/rust_builder/macos/rust_lib_arbiter.podspec b/useragent/rust_builder/macos/rust_lib_arbiter.podspec new file mode 100644 index 0000000..a623c95 --- /dev/null +++ b/useragent/rust_builder/macos/rust_lib_arbiter.podspec @@ -0,0 +1,44 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint rust_lib_arbiter.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'rust_lib_arbiter' + s.version = '0.0.1' + s.summary = 'A new Flutter FFI plugin project.' + s.description = <<-DESC +A new Flutter FFI plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + # This will ensure the source files in Classes/ are included in the native + # builds of apps using this FFI plugin. Podspec does not support relative + # paths, so Classes contains a forwarder C file that relatively imports + # `../src/*` so that the C sources can be shared among all target platforms. + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' + + s.script_phase = { + :name => 'Build Rust library', + # First argument is relative path to the `rust` folder, second is name of rust library + :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../rust rust_lib_arbiter', + :execution_position => :before_compile, + :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], + # Let XCode know that the static library referenced in -force_load below is + # created by this build step. + :output_files => ["${BUILT_PRODUCTS_DIR}/librust_lib_arbiter.a"], + } + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + # Flutter.framework does not contain a i386 slice. + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/librust_lib_arbiter.a', + } +end \ No newline at end of file diff --git a/useragent/rust_builder/pubspec.yaml b/useragent/rust_builder/pubspec.yaml new file mode 100644 index 0000000..98ebd2c --- /dev/null +++ b/useragent/rust_builder/pubspec.yaml @@ -0,0 +1,34 @@ +name: rust_lib_arbiter +description: "Utility to build Rust code" +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=3.3.0 <4.0.0' + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + ffi: ^2.0.2 + ffigen: ^11.0.0 + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + plugin: + platforms: + android: + ffiPlugin: true + ios: + ffiPlugin: true + linux: + ffiPlugin: true + macos: + ffiPlugin: true + windows: + ffiPlugin: true diff --git a/useragent/rust_builder/windows/.gitignore b/useragent/rust_builder/windows/.gitignore new file mode 100644 index 0000000..b3eb2be --- /dev/null +++ b/useragent/rust_builder/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/useragent/rust_builder/windows/CMakeLists.txt b/useragent/rust_builder/windows/CMakeLists.txt new file mode 100644 index 0000000..7743fd1 --- /dev/null +++ b/useragent/rust_builder/windows/CMakeLists.txt @@ -0,0 +1,20 @@ +# The Flutter tooling requires that developers have a version of Visual Studio +# installed that includes CMake 3.14 or later. You should not increase this +# version, as doing so will cause the plugin to fail to compile for some +# customers of the plugin. +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. +set(PROJECT_NAME "rust_lib_arbiter") +project(${PROJECT_NAME} LANGUAGES CXX) + +include("../cargokit/cmake/cargokit.cmake") +apply_cargokit(${PROJECT_NAME} ../../../../../../rust rust_lib_arbiter "") + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(rust_lib_arbiter_bundled_libraries + "${${PROJECT_NAME}_cargokit_lib}" + PARENT_SCOPE +) diff --git a/useragent/test_driver/integration_test.dart b/useragent/test_driver/integration_test.dart new file mode 100644 index 0000000..b38629c --- /dev/null +++ b/useragent/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/useragent/windows/flutter/generated_plugins.cmake b/useragent/windows/flutter/generated_plugins.cmake index d3c6423..834220c 100644 --- a/useragent/windows/flutter/generated_plugins.cmake +++ b/useragent/windows/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + rust_lib_arbiter ) set(PLUGIN_BUNDLED_LIBRARIES)