From 04bea299cb781dc03b95b2318a9d8c7ba1711caa Mon Sep 17 00:00:00 2001 From: hdbg Date: Tue, 17 Mar 2026 18:39:12 +0100 Subject: [PATCH 1/8] refactor(server::useragent): migrated to new connection design --- protobufs/user_agent.proto | 16 +- server/Cargo.lock | 1 + server/crates/arbiter-proto/Cargo.toml | 1 + server/crates/arbiter-proto/src/transport.rs | 49 +- .../arbiter-proto/src/transport/grpc.rs | 106 ++ .../src/actors/keyholder/mod.rs | 4 +- .../src/actors/user_agent/auth.rs | 120 +-- .../src/actors/user_agent/auth/state.rs | 53 +- .../src/actors/user_agent/mod.rs | 158 +-- .../src/actors/user_agent/session.rs | 175 +--- .../actors/user_agent/session/connection.rs | 202 ++-- server/crates/arbiter-server/src/db/mod.rs | 8 + .../crates/arbiter-server/src/grpc/client.rs | 21 +- server/crates/arbiter-server/src/grpc/mod.rs | 35 +- .../arbiter-server/src/grpc/user_agent.rs | 974 +++++++++--------- .../src/grpc/user_agent/auth.rs | 151 +++ server/crates/arbiter-server/src/lib.rs | 1 + server/crates/arbiter-server/src/utils.rs | 16 + .../arbiter-server/tests/user_agent/auth.rs | 4 +- .../arbiter-server/tests/user_agent/unseal.rs | 14 +- 20 files changed, 1151 insertions(+), 958 deletions(-) create mode 100644 server/crates/arbiter-proto/src/transport/grpc.rs create mode 100644 server/crates/arbiter-server/src/grpc/user_agent/auth.rs create mode 100644 server/crates/arbiter-server/src/utils.rs diff --git a/protobufs/user_agent.proto b/protobufs/user_agent.proto index 821575e..6fb77e4 100644 --- a/protobufs/user_agent.proto +++ b/protobufs/user_agent.proto @@ -2,8 +2,8 @@ syntax = "proto3"; package arbiter.user_agent; -import "google/protobuf/empty.proto"; import "evm.proto"; +import "google/protobuf/empty.proto"; enum KeyType { KEY_TYPE_UNSPECIFIED = 0; @@ -19,15 +19,23 @@ message AuthChallengeRequest { } message AuthChallenge { - bytes pubkey = 1; int32 nonce = 2; + reserved 1; } message AuthChallengeSolution { bytes signature = 1; } -message AuthOk {} +enum AuthResult { + AUTH_RESULT_UNSPECIFIED = 0; + AUTH_RESULT_SUCCESS = 1; + AUTH_RESULT_INVALID_KEY = 2; + AUTH_RESULT_INVALID_SIGNATURE = 3; + AUTH_RESULT_BOOTSTRAP_REQUIRED = 4; + AUTH_RESULT_TOKEN_INVALID = 5; + AUTH_RESULT_INTERNAL = 6; +} message UnsealStart { bytes client_pubkey = 1; @@ -99,7 +107,7 @@ message UserAgentRequest { message UserAgentResponse { oneof payload { AuthChallenge auth_challenge = 1; - AuthOk auth_ok = 2; + AuthResult auth_result = 2; UnsealStartResponse unseal_start_response = 3; UnsealResult unseal_result = 4; VaultState vault_state = 5; diff --git a/server/Cargo.lock b/server/Cargo.lock index 30ec3d7..057a88b 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -697,6 +697,7 @@ dependencies = [ "rustls-pki-types", "thiserror", "tokio", + "tokio-stream", "tonic", "tonic-prost", "tonic-prost-build", diff --git a/server/crates/arbiter-proto/Cargo.toml b/server/crates/arbiter-proto/Cargo.toml index 0673f8a..88676a0 100644 --- a/server/crates/arbiter-proto/Cargo.toml +++ b/server/crates/arbiter-proto/Cargo.toml @@ -21,6 +21,7 @@ base64 = "0.22.1" prost-types.workspace = true tracing.workspace = true async-trait.workspace = true +tokio-stream.workspace = true [build-dependencies] tonic-prost-build = "0.14.3" diff --git a/server/crates/arbiter-proto/src/transport.rs b/server/crates/arbiter-proto/src/transport.rs index 55415c8..b31aa61 100644 --- a/server/crates/arbiter-proto/src/transport.rs +++ b/server/crates/arbiter-proto/src/transport.rs @@ -63,16 +63,29 @@ where extractor(msg).ok_or(Error::UnexpectedMessage) } +#[async_trait] +pub trait Sender: Send + Sync { + async fn send(&mut self, item: Outbound) -> Result<(), Error>; +} + +#[async_trait] +pub trait Receiver: Send + Sync { + async fn recv(&mut self) -> Option; +} + /// Minimal bidirectional transport abstraction used by protocol code. /// /// `Bi` models a duplex channel with: /// - inbound items of type `Inbound` read via [`Bi::recv`] /// - outbound items of type `Outbound` written via [`Bi::send`] -#[async_trait] -pub trait Bi: Send + Sync + 'static { - async fn send(&mut self, item: Outbound) -> Result<(), Error>; +pub trait Bi: Sender + Receiver + Send + Sync {} - async fn recv(&mut self) -> Option; +pub trait SplittableBi: Bi { + type Sender: Sender; + type Receiver: Receiver; + + fn split(self) -> (Self::Sender, Self::Receiver); + fn from_parts(sender: Self::Sender, receiver: Self::Receiver) -> Self; } /// No-op [`Bi`] transport for tests and manual actor usage. @@ -83,22 +96,16 @@ pub struct DummyTransport { _marker: PhantomData<(Inbound, Outbound)>, } -impl DummyTransport { - pub fn new() -> Self { +impl Default for DummyTransport { + fn default() -> Self { Self { _marker: PhantomData, } } } -impl Default for DummyTransport { - fn default() -> Self { - Self::new() - } -} - #[async_trait] -impl Bi for DummyTransport +impl Sender for DummyTransport where Inbound: Send + Sync + 'static, Outbound: Send + Sync + 'static, @@ -106,9 +113,25 @@ where async fn send(&mut self, _item: Outbound) -> Result<(), Error> { Ok(()) } +} +#[async_trait] +impl Receiver for DummyTransport +where + Inbound: Send + Sync + 'static, + Outbound: Send + Sync + 'static, +{ async fn recv(&mut self) -> Option { std::future::pending::<()>().await; None } } + +impl Bi for DummyTransport +where + Inbound: Send + Sync + 'static, + Outbound: Send + Sync + 'static, +{ +} + +pub mod grpc; diff --git a/server/crates/arbiter-proto/src/transport/grpc.rs b/server/crates/arbiter-proto/src/transport/grpc.rs new file mode 100644 index 0000000..e0959e0 --- /dev/null +++ b/server/crates/arbiter-proto/src/transport/grpc.rs @@ -0,0 +1,106 @@ +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>, +} + +#[async_trait] +impl Sender> for GrpcSender +where + Outbound: Send + Sync + 'static, +{ + async fn send(&mut self, item: Result) -> Result<(), super::Error> { + self.tx + .send(item) + .await + .map_err(|_| super::Error::ChannelClosed) + } +} + +pub struct GrpcReceiver { + rx: tonic::Streaming, +} +#[async_trait] +impl Receiver> for GrpcReceiver +where + Inbound: Send + Sync + 'static, +{ + async fn recv(&mut self) -> Option> { + self.rx.next().await + } +} + +pub struct GrpcBi { + sender: GrpcSender, + receiver: GrpcReceiver, +} + +impl GrpcBi +where + Inbound: Send + Sync + 'static, + Outbound: Send + Sync + 'static, +{ + pub fn from_bi_stream( + receiver: tonic::Streaming, + ) -> (Self, ReceiverStream>) { + let (tx, rx) = mpsc::channel(10); + let sender = GrpcSender { tx }; + let receiver = GrpcReceiver { rx: receiver }; + let bi = GrpcBi { sender, receiver }; + (bi, ReceiverStream::new(rx)) + } +} + +#[async_trait] +impl Sender> for GrpcBi +where + Inbound: Send + Sync + 'static, + Outbound: Send + Sync + 'static, +{ + async fn send(&mut self, item: Result) -> Result<(), super::Error> { + self.sender.send(item).await + } +} + +#[async_trait] +impl Receiver> for GrpcBi +where + Inbound: Send + Sync + 'static, + Outbound: Send + Sync + 'static, +{ + async fn recv(&mut self) -> Option> { + self.receiver.recv().await + } +} + +impl Bi, Result> + for GrpcBi +where + Inbound: Send + Sync + 'static, + Outbound: Send + Sync + 'static, +{ +} + +impl + super::SplittableBi, Result> + for GrpcBi +where + Inbound: Send + Sync + 'static, + Outbound: Send + Sync + 'static, +{ + type Sender = GrpcSender; + type Receiver = GrpcReceiver; + + fn split(self) -> (Self::Sender, Self::Receiver) { + (self.sender, self.receiver) + } + + fn from_parts(sender: Self::Sender, receiver: Self::Receiver) -> Self { + GrpcBi { sender, receiver } + } +} diff --git a/server/crates/arbiter-server/src/actors/keyholder/mod.rs b/server/crates/arbiter-server/src/actors/keyholder/mod.rs index f37284a..3a245af 100644 --- a/server/crates/arbiter-server/src/actors/keyholder/mod.rs +++ b/server/crates/arbiter-server/src/actors/keyholder/mod.rs @@ -22,7 +22,7 @@ use encryption::v1::{self, KeyCell, Nonce}; pub mod encryption; #[derive(Default, EnumDiscriminants)] -#[strum_discriminants(derive(Reply), vis(pub))] +#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))] enum State { #[default] Unbootstrapped, @@ -325,7 +325,7 @@ impl KeyHolder { } #[message] - pub fn get_state(&self) -> StateDiscriminants { + pub fn get_state(&self) -> KeyHolderState { self.state.discriminant() } diff --git a/server/crates/arbiter-server/src/actors/user_agent/auth.rs b/server/crates/arbiter-server/src/actors/user_agent/auth.rs index eab7acf..7e2cf9c 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth.rs @@ -1,74 +1,82 @@ +use arbiter_proto::transport::Bi; use tracing::error; use crate::actors::user_agent::{ - Request, UserAgentConnection, + AuthPublicKey, UserAgentConnection, auth::state::{AuthContext, AuthStateMachine}, - AuthPublicKey, - session::UserAgentSession, }; -#[derive(thiserror::Error, Debug, PartialEq)] -pub enum Error { - #[error("Unexpected message payload")] - UnexpectedMessagePayload, - #[error("Invalid client public key length")] - InvalidClientPubkeyLength, - #[error("Invalid client public key encoding")] - InvalidAuthPubkeyEncoding, - #[error("Database pool unavailable")] - DatabasePoolUnavailable, - #[error("Database operation failed")] - DatabaseOperationFailed, - #[error("Public key not registered")] - PublicKeyNotRegistered, - #[error("Transport error")] - Transport, - #[error("Invalid bootstrap token")] - InvalidBootstrapToken, - #[error("Bootstrapper actor unreachable")] - BootstrapperActorUnreachable, - #[error("Invalid challenge solution")] - InvalidChallengeSolution, -} - mod state; use state::*; -fn parse_auth_event(payload: Request) -> Result { - match payload { - Request::AuthChallengeRequest { - pubkey, - bootstrap_token: None, - } => Ok(AuthEvents::AuthRequest(ChallengeRequest { pubkey })), - Request::AuthChallengeRequest { - pubkey, - bootstrap_token: Some(token), - } => Ok(AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest { - pubkey, - token, - })), - Request::AuthChallengeSolution { signature } => { - Ok(AuthEvents::ReceivedSolution(ChallengeSolution { - solution: signature, - })) +#[derive(Debug, Clone)] +pub enum Inbound { + AuthChallengeRequest { + pubkey: AuthPublicKey, + bootstrap_token: Option, + }, + AuthChallengeSolution { + signature: Vec, + }, +} + +#[derive(Debug)] +pub enum Error { + UnregisteredPublicKey, + InvalidChallengeSolution, + InvalidBootstrapToken, + Internal { details: String }, + Transport, +} + +impl Error { + fn internal(details: impl Into) -> Self { + Self::Internal { + details: details.into(), } - _ => Err(Error::UnexpectedMessagePayload), } } -pub async fn authenticate(props: &mut UserAgentConnection) -> Result { - let mut state = AuthStateMachine::new(AuthContext::new(props)); +#[derive(Debug, Clone)] +pub enum Outbound { + AuthChallenge { nonce: i32 }, + AuthSuccess, +} + +fn parse_auth_event(payload: Inbound) -> AuthEvents { + match payload { + Inbound::AuthChallengeRequest { + pubkey, + bootstrap_token: None, + } => AuthEvents::AuthRequest(ChallengeRequest { pubkey }), + Inbound::AuthChallengeRequest { + pubkey, + bootstrap_token: Some(token), + } => AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest { pubkey, token }), + Inbound::AuthChallengeSolution { signature } => { + AuthEvents::ReceivedSolution(ChallengeSolution { + solution: signature, + }) + } + } +} + +pub async fn authenticate( + props: &mut UserAgentConnection, + transport: T, +) -> Result +where + T: Bi> + Send, +{ + 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 transport = state.context_mut().conn.transport.as_mut(); - let Some(payload) = transport.recv().await else { + let Some(payload) = state.context_mut().transport.recv().await else { return Err(Error::Transport); }; - let event = parse_auth_event(payload)?; - - match state.process_event(event).await { + match state.process_event(parse_auth_event(payload)).await { Ok(AuthStates::AuthOk(key)) => return Ok(key.clone()), Err(AuthError::ActionFailed(err)) => { error!(?err, "State machine action failed"); @@ -91,11 +99,3 @@ pub async fn authenticate(props: &mut UserAgentConnection) -> Result Result { - let _key = authenticate(&mut props).await?; - let session = UserAgentSession::new(props); - Ok(session) -} 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 index 608f3a7..7a5991d 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs @@ -1,3 +1,5 @@ +use alloy::transports::Transport; +use arbiter_proto::transport::Bi; use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update}; use diesel_async::RunQueryDsl; use tracing::error; @@ -6,7 +8,7 @@ use super::Error; use crate::{ actors::{ bootstrap::ConsumeToken, - user_agent::{AuthPublicKey, Response, UserAgentConnection}, + user_agent::{AuthPublicKey, OutOfBand, UserAgentConnection, auth::Outbound}, }, db::schema, }; @@ -42,7 +44,7 @@ smlang::statemachine!( async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result { let mut db_conn = db.get().await.map_err(|e| { error!(error = ?e, "Database pool error"); - Error::DatabasePoolUnavailable + Error::internal("Database unavailable") })?; db_conn .exclusive_transaction(|conn| { @@ -66,11 +68,11 @@ async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Resu .optional() .map_err(|e| { error!(error = ?e, "Database error"); - Error::DatabaseOperationFailed + Error::internal("Database operation failed") })? .ok_or_else(|| { error!(?pubkey_bytes, "Public key not found in database"); - Error::PublicKeyNotRegistered + Error::UnregisteredPublicKey }) } @@ -79,7 +81,7 @@ async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> R let key_type = pubkey.key_type(); let mut conn = db.get().await.map_err(|e| { error!(error = ?e, "Database pool error"); - Error::DatabasePoolUnavailable + Error::internal("Database unavailable") })?; diesel::insert_into(schema::useragent_client::table) @@ -92,23 +94,27 @@ async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> R .await .map_err(|e| { error!(error = ?e, "Database error"); - Error::DatabaseOperationFailed + Error::internal("Database operation failed") })?; Ok(()) } -pub struct AuthContext<'a> { +pub struct AuthContext<'a, T> { pub(super) conn: &'a mut UserAgentConnection, + pub(super) transport: T, } -impl<'a> AuthContext<'a> { - pub fn new(conn: &'a mut UserAgentConnection) -> Self { - Self { conn } +impl<'a, T> AuthContext<'a, T> { + pub fn new(conn: &'a mut UserAgentConnection, transport: T) -> Self { + Self { conn, transport } } } -impl AuthStateMachineContext for AuthContext<'_> { +impl AuthStateMachineContext for AuthContext<'_, T> +where + T: Bi> + Send, +{ type Error = Error; async fn prepare_challenge( @@ -118,9 +124,9 @@ impl AuthStateMachineContext for AuthContext<'_> { let stored_bytes = pubkey.to_stored_bytes(); let nonce = create_nonce(&self.conn.db, &stored_bytes).await?; - self.conn + self .transport - .send(Ok(Response::AuthChallenge { nonce })) + .send(Ok(Outbound::AuthChallenge { nonce })) .await .map_err(|e| { error!(?e, "Failed to send auth challenge"); @@ -149,7 +155,7 @@ impl AuthStateMachineContext for AuthContext<'_> { .await .map_err(|e| { error!(?e, "Failed to consume bootstrap token"); - Error::BootstrapperActorUnreachable + Error::internal("Failed to consume bootstrap token") })?; if !token_ok { @@ -159,11 +165,11 @@ impl AuthStateMachineContext for AuthContext<'_> { register_key(&self.conn.db, &pubkey).await?; - self.conn - .transport - .send(Ok(Response::AuthOk)) - .await - .map_err(|_| Error::Transport)?; + self + .transport + .send(Ok(Outbound::AuthSuccess)) + .await + .map_err(|_| Error::Transport)?; Ok(pubkey) } @@ -172,7 +178,10 @@ impl AuthStateMachineContext for AuthContext<'_> { #[allow(clippy::unused_unit)] async fn verify_solution( &mut self, - ChallengeContext { challenge_nonce, key }: &ChallengeContext, + ChallengeContext { + challenge_nonce, + key, + }: &ChallengeContext, ChallengeSolution { solution }: ChallengeSolution, ) -> Result { let formatted = arbiter_proto::format_challenge(*challenge_nonce, &key.to_stored_bytes()); @@ -205,9 +214,9 @@ impl AuthStateMachineContext for AuthContext<'_> { }; if valid { - self.conn + self .transport - .send(Ok(Response::AuthOk)) + .send(Ok(Outbound::AuthSuccess)) .await .map_err(|_| Error::Transport)?; } diff --git a/server/crates/arbiter-server/src/actors/user_agent/mod.rs b/server/crates/arbiter-server/src/actors/user_agent/mod.rs index 6b4a7d6..7f980b8 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/mod.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/mod.rs @@ -1,33 +1,15 @@ use alloy::primitives::Address; -use arbiter_proto::transport::Bi; +use arbiter_proto::transport::{Bi, Sender}; use kameo::actor::Spawn as _; use tracing::{error, info}; use crate::{ - actors::{GlobalActors, evm, user_agent::session::UserAgentSession}, + actors::{GlobalActors, evm}, db::{self, models::KeyType}, evm::policies::SharedGrantSettings, evm::policies::{Grant, SpecificGrant}, }; -#[derive(Debug, thiserror::Error, PartialEq)] -pub enum TransportResponseError { - #[error("Unexpected request payload")] - UnexpectedRequestPayload, - #[error("Invalid state for unseal encrypted key")] - InvalidStateForUnsealEncryptedKey, - #[error("client_pubkey must be 32 bytes")] - InvalidClientPubkeyLength, - #[error("State machine error")] - StateTransitionFailed, - #[error("Vault is not available")] - KeyHolderActorUnreachable, - #[error(transparent)] - Auth(#[from] auth::Error), - #[error("Failed registering connection")] - ConnectionRegistrationFailed, -} - /// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake. #[derive(Clone, Debug)] pub enum AuthPublicKey { @@ -65,119 +47,55 @@ impl AuthPublicKey { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum UnsealError { - InvalidKey, - Unbootstrapped, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BootstrapError { - AlreadyBootstrapped, - InvalidKey, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VaultState { - Unbootstrapped, - Sealed, - Unsealed, -} - -#[derive(Debug, Clone)] -pub enum Request { - AuthChallengeRequest { - pubkey: AuthPublicKey, - bootstrap_token: Option, - }, - AuthChallengeSolution { - signature: Vec, - }, - UnsealStart { - client_pubkey: x25519_dalek::PublicKey, - }, - UnsealEncryptedKey { - nonce: Vec, - ciphertext: Vec, - associated_data: Vec, - }, - BootstrapEncryptedKey { - nonce: Vec, - ciphertext: Vec, - associated_data: Vec, - }, - QueryVaultState, - EvmWalletCreate, - EvmWalletList, - ClientConnectionResponse { - approved: bool, - }, - - ListGrants, - EvmGrantCreate { - client_id: i32, - shared: SharedGrantSettings, - specific: SpecificGrant, - }, - EvmGrantDelete { - grant_id: i32, - }, +impl TryFrom<(KeyType, Vec)> for AuthPublicKey { + type Error = &'static str; + + fn try_from(value: (KeyType, Vec)) -> Result { + let (key_type, bytes) = value; + match key_type { + KeyType::Ed25519 => { + let bytes: [u8; 32] = bytes.try_into().map_err(|_| "invalid Ed25519 key length")?; + let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes) + .map_err(|e| "invalid Ed25519 key")?; + Ok(AuthPublicKey::Ed25519(key)) + } + KeyType::EcdsaSecp256k1 => { + let point = + k256::EncodedPoint::from_bytes(&bytes).map_err(|e| "invalid ECDSA key")?; + let key = k256::ecdsa::VerifyingKey::from_encoded_point(&point) + .map_err(|e| "invalid ECDSA key")?; + Ok(AuthPublicKey::EcdsaSecp256k1(key)) + } + KeyType::Rsa => { + use rsa::pkcs8::DecodePublicKey as _; + let key = rsa::RsaPublicKey::from_public_key_der(&bytes) + .map_err(|e| "invalid RSA key")?; + Ok(AuthPublicKey::Rsa(key)) + } + } + } } +// Messages, sent by user agent to connection client without having a request #[derive(Debug)] -pub enum Response { - AuthChallenge { - nonce: i32, - }, - AuthOk, - UnsealStartResponse { - server_pubkey: x25519_dalek::PublicKey, - }, - UnsealResult(Result<(), UnsealError>), - BootstrapResult(Result<(), BootstrapError>), - VaultState(VaultState), - ClientConnectionRequest { - pubkey: ed25519_dalek::VerifyingKey, - }, +pub enum OutOfBand { + ClientConnectionRequest { pubkey: ed25519_dalek::VerifyingKey }, ClientConnectionCancel, - EvmWalletCreate(Result<(), evm::Error>), - EvmWalletList(Vec
), - - ListGrants(Vec>), - EvmGrantCreate(Result), - EvmGrantDelete(Result<(), evm::Error>), } -pub type Transport = Box> + Send>; - pub struct UserAgentConnection { - db: db::DatabasePool, - actors: GlobalActors, - transport: Transport, + pub(crate) db: db::DatabasePool, + pub(crate) actors: GlobalActors, } impl UserAgentConnection { - pub fn new(db: db::DatabasePool, actors: GlobalActors, transport: Transport) -> Self { - Self { - db, - actors, - transport, - } + pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self { + Self { db, actors } } } pub mod auth; pub mod session; -#[tracing::instrument(skip(props))] -pub async fn connect_user_agent(props: UserAgentConnection) { - match auth::authenticate_and_create(props).await { - Ok(session) => { - UserAgentSession::spawn(session); - info!("User authenticated, session started"); - } - Err(err) => { - error!(?err, "Authentication failed, closing connection"); - } - } -} +pub use auth::authenticate; +pub use session::UserAgentSession; diff --git a/server/crates/arbiter-server/src/actors/user_agent/session.rs b/server/crates/arbiter-server/src/actors/user_agent/session.rs index d568cc5..382165a 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session.rs @@ -1,93 +1,63 @@ +use std::{borrow::Cow, convert::Infallible}; + +use arbiter_proto::transport::Sender; use ed25519_dalek::VerifyingKey; use kameo::{Actor, messages, prelude::Context}; +use thiserror::Error; use tokio::{select, sync::watch}; use tracing::{error, info}; use crate::actors::{ router::RegisterUserAgent, - user_agent::{ - Request, Response, TransportResponseError, - UserAgentConnection, - }, + user_agent::{OutOfBand, UserAgentConnection}, }; mod state; use state::{DummyContext, UserAgentEvents, UserAgentStateMachine}; -// Error for consumption by other actors -#[derive(Debug, thiserror::Error, PartialEq)] +#[derive(Debug, Error)] pub enum Error { - #[error("User agent session ended due to connection loss")] - ConnectionLost, + #[error("State transition failed")] + State, - #[error("User agent session ended due to unexpected message")] - UnexpectedMessage, + #[error("Internal error: {message}")] + Internal { message: Cow<'static, str> }, +} + +impl Error { + pub fn internal(message: impl Into>) -> Self { + Self::Internal { + message: message.into(), + } + } } pub struct UserAgentSession { props: UserAgentConnection, state: UserAgentStateMachine, + sender: Box>, } mod connection; +pub(crate) use connection::{ + BootstrapError, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, HandleEvmWalletList, + HandleGrantCreate, HandleGrantDelete, HandleGrantList, HandleQueryVaultState, + HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError, +}; impl UserAgentSession { - pub(crate) fn new(props: UserAgentConnection) -> Self { + pub(crate) fn new(props: UserAgentConnection, sender: Box>) -> Self { Self { props, state: UserAgentStateMachine::new(DummyContext), + sender, } } - pub(super) async fn send_msg( - &mut self, - msg: Response, - _ctx: &mut Context, - ) -> Result<(), Error> { - self.props.transport.send(Ok(msg)).await.map_err(|_| { - error!( - actor = "useragent", - reason = "channel closed", - "send.failed" - ); - Error::ConnectionLost - }) - } - - async fn expect_msg( - &mut self, - extractor: Extractor, - ctx: &mut Context, - ) -> Result - where - Extractor: FnOnce(Request) -> Option, - Reply: kameo::Reply, - { - let msg = self.props.transport.recv().await.ok_or_else(|| { - error!( - actor = "useragent", - reason = "channel closed", - "recv.failed" - ); - ctx.stop(); - Error::ConnectionLost - })?; - - extractor(msg).ok_or_else(|| { - error!( - actor = "useragent", - reason = "unexpected message", - "recv.failed" - ); - ctx.stop(); - Error::UnexpectedMessage - }) - } - - fn transition(&mut self, event: UserAgentEvents) -> Result<(), TransportResponseError> { + fn transition(&mut self, event: UserAgentEvents) -> Result<(), Error> { self.state.process_event(event).map_err(|e| { error!(?e, "State transition failed"); - TransportResponseError::StateTransitionFailed + Error::State })?; Ok(()) } @@ -95,52 +65,21 @@ impl UserAgentSession { #[messages] impl UserAgentSession { - // TODO: Think about refactoring it to state-machine based flow, as we already have one #[message(ctx)] pub async fn request_new_client_approval( &mut self, client_pubkey: VerifyingKey, mut cancel_flag: watch::Receiver<()>, - ctx: &mut Context>, - ) -> Result { - self.send_msg( - Response::ClientConnectionRequest { - pubkey: client_pubkey, - }, - ctx, - ) - .await?; - - let extractor = |msg| { - if let Request::ClientConnectionResponse { approved } = msg { - Some(approved) - } else { - None - } - }; - - tokio::select! { - _ = cancel_flag.changed() => { - info!(actor = "useragent", "client connection approval cancelled"); - self.send_msg( - Response::ClientConnectionCancel, - ctx, - ).await?; - Ok(false) - } - result = self.expect_msg(extractor, ctx) => { - let result = result?; - info!(actor = "useragent", "received client connection approval result: approved={}", result); - Ok(result) - } - } + ctx: &mut Context>, + ) -> Result { + todo!("Think about refactoring it to state-machine based flow, as we already have one") } } impl Actor for UserAgentSession { type Args = Self; - type Error = TransportResponseError; + type Error = Error; async fn on_start( args: Self::Args, @@ -155,56 +94,8 @@ impl Actor for UserAgentSession { .await .map_err(|err| { error!(?err, "Failed to register user agent connection with router"); - TransportResponseError::ConnectionRegistrationFailed + Error::internal("Failed to register user agent connection with router") })?; Ok(args) } - - async fn next( - &mut self, - _actor_ref: kameo::prelude::WeakActorRef, - mailbox_rx: &mut kameo::prelude::MailboxReceiver, - ) -> Option> { - loop { - select! { - signal = mailbox_rx.recv() => { - return signal; - } - msg = self.props.transport.recv() => { - match msg { - Some(request) => { - match self.process_transport_inbound(request).await { - Ok(response) => { - if self.props.transport.send(Ok(response)).await.is_err() { - error!(actor = "useragent", reason = "channel closed", "send.failed"); - return Some(kameo::mailbox::Signal::Stop); - } - } - Err(err) => { - let _ = self.props.transport.send(Err(err)).await; - return Some(kameo::mailbox::Signal::Stop); - } - } - } - None => { - info!(actor = "useragent", "transport.closed"); - return Some(kameo::mailbox::Signal::Stop); - } - } - } - } - } - } -} - -impl UserAgentSession { - pub fn new_test(db: crate::db::DatabasePool, actors: crate::actors::GlobalActors) -> Self { - use arbiter_proto::transport::DummyTransport; - let transport: super::Transport = Box::new(DummyTransport::new()); - let props = UserAgentConnection::new(db, actors, transport); - Self { - props, - state: UserAgentStateMachine::new(DummyContext), - } - } } 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 index f7cf2be..ed9a107 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs @@ -1,10 +1,15 @@ use std::sync::Mutex; +use alloy::primitives::Address; use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; use kameo::error::SendError; +use kameo::messages; use tracing::{error, info}; use x25519_dalek::{EphemeralSecret, PublicKey}; +use crate::actors::keyholder::KeyHolderState; +use crate::actors::user_agent::session::Error; +use crate::evm::policies::{Grant, SpecificGrant}; use crate::safe_cell::SafeCell; use crate::{ actors::{ @@ -13,7 +18,7 @@ use crate::{ }, keyholder::{self, Bootstrap, TryUnseal}, user_agent::{ - BootstrapError, Request, Response, TransportResponseError, UnsealError, VaultState, + OutOfBand, session::{ UserAgentSession, state::{UnsealContext, UserAgentEvents, UserAgentStates}, @@ -24,55 +29,10 @@ use crate::{ }; impl UserAgentSession { - pub async fn process_transport_inbound(&mut self, req: Request) -> Output { - match req { - Request::UnsealStart { client_pubkey } => { - self.handle_unseal_request(client_pubkey).await - } - Request::UnsealEncryptedKey { - nonce, - ciphertext, - associated_data, - } => { - self.handle_unseal_encrypted_key(nonce, ciphertext, associated_data) - .await - } - Request::BootstrapEncryptedKey { - nonce, - ciphertext, - associated_data, - } => { - self.handle_bootstrap_encrypted_key(nonce, ciphertext, associated_data) - .await - } - Request::ListGrants => self.handle_grant_list().await, - Request::QueryVaultState => self.handle_query_vault_state().await, - Request::EvmWalletCreate => self.handle_evm_wallet_create().await, - Request::EvmWalletList => self.handle_evm_wallet_list().await, - Request::AuthChallengeRequest { .. } - | Request::AuthChallengeSolution { .. } - | Request::ClientConnectionResponse { .. } => { - Err(TransportResponseError::UnexpectedRequestPayload) - } - Request::EvmGrantCreate { - client_id, - shared, - specific, - } => self.handle_grant_create(client_id, shared, specific).await, - Request::EvmGrantDelete { grant_id } => self.handle_grant_delete(grant_id).await, - } - } -} - -type Output = Result; - -impl UserAgentSession { - fn take_unseal_secret( - &mut self, - ) -> Result<(EphemeralSecret, PublicKey), TransportResponseError> { + 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(TransportResponseError::InvalidStateForUnsealEncryptedKey); + return Err(Error::internal("Invalid state for unseal encrypted key")); }; let ephemeral_secret = { @@ -87,7 +47,7 @@ impl UserAgentSession { None => { drop(secret_lock); error!("Ephemeral secret already taken"); - return Err(TransportResponseError::StateTransitionFailed); + return Err(Error::internal("Ephemeral secret already taken")); } } }; @@ -121,8 +81,38 @@ impl UserAgentSession { } } } +} - async fn handle_unseal_request(&mut self, client_pubkey: x25519_dalek::PublicKey) -> Output { +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), +} + +#[messages] +impl UserAgentSession { + #[message] + pub(crate) async fn handle_unseal_request( + &mut self, + client_pubkey: x25519_dalek::PublicKey, + ) -> Result { let secret = EphemeralSecret::random(); let public_key = PublicKey::from(&secret); @@ -131,24 +121,27 @@ impl UserAgentSession { client_public_key: client_pubkey, }))?; - Ok(Response::UnsealStartResponse { + Ok(UnsealStartResponse { server_pubkey: public_key, }) } - async fn handle_unseal_encrypted_key( + #[message] + pub(crate) async fn handle_unseal_encrypted_key( &mut self, nonce: Vec, ciphertext: Vec, associated_data: Vec, - ) -> Output { + ) -> Result<(), UnsealError> { let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() { Ok(values) => values, - Err(TransportResponseError::StateTransitionFailed) => { + Err(Error::State) => { self.transition(UserAgentEvents::ReceivedInvalidKey)?; - return Ok(Response::UnsealResult(Err(UnsealError::InvalidKey))); + return Err(UnsealError::InvalidKey); + } + Err(err) => { + return Err(Error::internal("Failed to take unseal secret").into()); } - Err(err) => return Err(err), }; let seal_key_buffer = match Self::decrypt_client_key_material( @@ -161,7 +154,7 @@ impl UserAgentSession { Ok(buffer) => buffer, Err(()) => { self.transition(UserAgentEvents::ReceivedInvalidKey)?; - return Ok(Response::UnsealResult(Err(UnsealError::InvalidKey))); + return Err(UnsealError::InvalidKey); } }; @@ -177,38 +170,39 @@ impl UserAgentSession { Ok(_) => { info!("Successfully unsealed key with client-provided key"); self.transition(UserAgentEvents::ReceivedValidKey)?; - Ok(Response::UnsealResult(Ok(()))) + Ok(()) } Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => { self.transition(UserAgentEvents::ReceivedInvalidKey)?; - Ok(Response::UnsealResult(Err(UnsealError::InvalidKey))) + Err(UnsealError::InvalidKey) } Err(SendError::HandlerError(err)) => { error!(?err, "Keyholder failed to unseal key"); self.transition(UserAgentEvents::ReceivedInvalidKey)?; - Ok(Response::UnsealResult(Err(UnsealError::InvalidKey))) + Err(UnsealError::InvalidKey) } Err(err) => { error!(?err, "Failed to send unseal request to keyholder"); self.transition(UserAgentEvents::ReceivedInvalidKey)?; - Err(TransportResponseError::KeyHolderActorUnreachable) + Err(Error::internal("Vault actor error").into()) } } } - async fn handle_bootstrap_encrypted_key( + #[message] + pub(crate) async fn handle_bootstrap_encrypted_key( &mut self, nonce: Vec, ciphertext: Vec, associated_data: Vec, - ) -> Output { + ) -> Result<(), BootstrapError> { let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() { Ok(values) => values, - Err(TransportResponseError::StateTransitionFailed) => { + Err(Error::State) => { self.transition(UserAgentEvents::ReceivedInvalidKey)?; - return Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey))); + return Err(BootstrapError::InvalidKey); } - Err(err) => return Err(err), + Err(err) => return Err(err.into()), }; let seal_key_buffer = match Self::decrypt_client_key_material( @@ -221,7 +215,7 @@ impl UserAgentSession { Ok(buffer) => buffer, Err(()) => { self.transition(UserAgentEvents::ReceivedInvalidKey)?; - return Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey))); + return Err(BootstrapError::InvalidKey); } }; @@ -237,87 +231,94 @@ impl UserAgentSession { Ok(_) => { info!("Successfully bootstrapped vault with client-provided key"); self.transition(UserAgentEvents::ReceivedValidKey)?; - Ok(Response::BootstrapResult(Ok(()))) + Ok(()) } Err(SendError::HandlerError(keyholder::Error::AlreadyBootstrapped)) => { self.transition(UserAgentEvents::ReceivedInvalidKey)?; - Ok(Response::BootstrapResult(Err( - BootstrapError::AlreadyBootstrapped, - ))) + Err(BootstrapError::AlreadyBootstrapped) } Err(SendError::HandlerError(err)) => { error!(?err, "Keyholder failed to bootstrap vault"); self.transition(UserAgentEvents::ReceivedInvalidKey)?; - Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey))) + Err(BootstrapError::InvalidKey) } Err(err) => { error!(?err, "Failed to send bootstrap request to keyholder"); self.transition(UserAgentEvents::ReceivedInvalidKey)?; - Err(TransportResponseError::KeyHolderActorUnreachable) + Err(BootstrapError::General(Error::internal( + "Vault actor error", + ))) } } } } +#[messages] impl UserAgentSession { - async fn handle_query_vault_state(&mut self) -> Output { - use crate::actors::keyholder::{GetState, StateDiscriminants}; + #[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(StateDiscriminants::Unbootstrapped) => VaultState::Unbootstrapped, - Ok(StateDiscriminants::Sealed) => VaultState::Sealed, - Ok(StateDiscriminants::Unsealed) => VaultState::Unsealed, + Ok(state) => state, Err(err) => { error!(?err, actor = "useragent", "keyholder.query.failed"); - return Err(TransportResponseError::KeyHolderActorUnreachable); + return Err(Error::internal("Vault is in broken state").into()); } }; - Ok(Response::VaultState(vault_state)) + Ok(vault_state) } } +#[messages] impl UserAgentSession { - async fn handle_evm_wallet_create(&mut self) -> Output { - let result = match self.props.actors.evm.ask(Generate {}).await { - Ok(_address) => return Ok(Response::EvmWalletCreate(Ok(()))), - Err(SendError::HandlerError(err)) => Err(err), + #[message] + pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result { + match self.props.actors.evm.ask(Generate {}).await { + Ok(address) => return 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"); - return Err(TransportResponseError::KeyHolderActorUnreachable); + return Err(Error::internal("EVM actor unreachable")); } - }; - Ok(Response::EvmWalletCreate(result)) + } } - async fn handle_evm_wallet_list(&mut self) -> Output { + #[message] + pub(crate) async fn handle_evm_wallet_list(&mut self) -> Result, Error> { match self.props.actors.evm.ask(ListWallets {}).await { - Ok(wallets) => Ok(Response::EvmWalletList(wallets)), + Ok(wallets) => Ok(wallets), Err(err) => { error!(?err, "EVM wallet list failed"); - Err(TransportResponseError::KeyHolderActorUnreachable) + Err(Error::internal("Failed to list EVM wallets")) } } } } +#[messages] impl UserAgentSession { - async fn handle_grant_list(&mut self) -> Output { + #[message] + pub(crate) async fn handle_grant_list(&mut self) -> Result>, Error> { match self.props.actors.evm.ask(UseragentListGrants {}).await { - Ok(grants) => Ok(Response::ListGrants(grants)), + Ok(grants) => Ok(grants), Err(err) => { error!(?err, "EVM grant list failed"); - Err(TransportResponseError::KeyHolderActorUnreachable) + Err(Error::internal("Failed to list EVM grants")) } } } - async fn handle_grant_create( + #[message] + pub(crate) async fn handle_grant_create( &mut self, client_id: i32, basic: crate::evm::policies::SharedGrantSettings, grant: crate::evm::policies::SpecificGrant, - ) -> Output { + ) -> Result { match self .props .actors @@ -329,15 +330,16 @@ impl UserAgentSession { }) .await { - Ok(grant_id) => Ok(Response::EvmGrantCreate(Ok(grant_id))), + Ok(grant_id) => Ok(grant_id), Err(err) => { error!(?err, "EVM grant create failed"); - Err(TransportResponseError::KeyHolderActorUnreachable) + Err(Error::internal("Failed to create EVM grant")) } } } - async fn handle_grant_delete(&mut self, grant_id: i32) -> Output { + #[message] + pub(crate) async fn handle_grant_delete(&mut self, grant_id: i32) -> Result<(), Error> { match self .props .actors @@ -345,10 +347,10 @@ impl UserAgentSession { .ask(UseragentDeleteGrant { grant_id }) .await { - Ok(()) => Ok(Response::EvmGrantDelete(Ok(()))), + Ok(()) => Ok(()), Err(err) => { error!(?err, "EVM grant delete failed"); - Err(TransportResponseError::KeyHolderActorUnreachable) + Err(Error::internal("Failed to delete EVM grant")) } } } diff --git a/server/crates/arbiter-server/src/db/mod.rs b/server/crates/arbiter-server/src/db/mod.rs index 616bd92..ba7ef0e 100644 --- a/server/crates/arbiter-server/src/db/mod.rs +++ b/server/crates/arbiter-server/src/db/mod.rs @@ -44,6 +44,14 @@ pub enum DatabaseSetupError { Pool(#[from] PoolInitError), } +#[derive(Error, Debug)] +pub enum DatabaseError { + #[error("Database connection error")] + Pool(#[from] PoolError), + #[error("Database query error")] + Connection(#[from] diesel::result::Error), +} + #[tracing::instrument(level = "info")] fn database_path() -> Result { let arbiter_home = arbiter_proto::home_path().map_err(DatabaseSetupError::HomeDir)?; diff --git a/server/crates/arbiter-server/src/grpc/client.rs b/server/crates/arbiter-server/src/grpc/client.rs index 1e9e072..3d41785 100644 --- a/server/crates/arbiter-server/src/grpc/client.rs +++ b/server/crates/arbiter-server/src/grpc/client.rs @@ -1,14 +1,13 @@ use arbiter_proto::{ proto::client::{ - AuthChallenge as ProtoAuthChallenge, - AuthChallengeRequest as ProtoAuthChallengeRequest, + AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest, AuthChallengeSolution as ProtoAuthChallengeSolution, AuthOk as ProtoAuthOk, ClientConnectError, ClientRequest, ClientResponse, client_connect_error::Code as ProtoClientConnectErrorCode, client_request::Payload as ClientRequestPayload, client_response::Payload as ClientResponsePayload, }, - transport::{Bi, Error as TransportError}, + transport::{Bi, Error as TransportError, Sender}, }; use async_trait::async_trait; use futures::StreamExt as _; @@ -37,9 +36,9 @@ impl GrpcTransport { Some(ClientRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest { pubkey, })) => Ok(DomainRequest::AuthChallengeRequest { pubkey }), - Some(ClientRequestPayload::AuthChallengeSolution( - ProtoAuthChallengeSolution { signature }, - )) => Ok(DomainRequest::AuthChallengeSolution { signature }), + Some(ClientRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution { + signature, + })) => Ok(DomainRequest::AuthChallengeSolution { signature }), None => Err(Status::invalid_argument("Missing client request payload")), } } @@ -86,8 +85,11 @@ impl GrpcTransport { } #[async_trait] -impl Bi> for GrpcTransport { - async fn send(&mut self, item: Result) -> Result<(), TransportError> { +impl Sender> for GrpcTransport { + async fn send( + &mut self, + item: Result, + ) -> Result<(), TransportError> { let outbound = match item { Ok(message) => Ok(Self::response_to_proto(message)), Err(err) => Err(Self::error_to_status(err)), @@ -98,7 +100,10 @@ impl Bi> for GrpcTransport { .await .map_err(|_| TransportError::ChannelClosed) } +} +#[async_trait] +impl Bi> for GrpcTransport { async fn recv(&mut self) -> Option { match self.receiver.next().await { Some(Ok(item)) => match Self::request_to_domain(item) { diff --git a/server/crates/arbiter-server/src/grpc/mod.rs b/server/crates/arbiter-server/src/grpc/mod.rs index 18b9f70..204d6b1 100644 --- a/server/crates/arbiter-server/src/grpc/mod.rs +++ b/server/crates/arbiter-server/src/grpc/mod.rs @@ -1,7 +1,9 @@ - -use arbiter_proto::proto::{ - client::{ClientRequest, ClientResponse}, - user_agent::{UserAgentRequest, UserAgentResponse}, +use arbiter_proto::{ + proto::{ + client::{ClientRequest, ClientResponse}, + user_agent::{UserAgentRequest, UserAgentResponse}, + }, + transport::grpc::GrpcBi, }; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; @@ -10,7 +12,11 @@ use tracing::info; use crate::{ DEFAULT_CHANNEL_SIZE, - actors::{client::{ClientConnection, connect_client}, user_agent::{UserAgentConnection, connect_user_agent}}, + actors::{ + client::{ClientConnection, connect_client}, + user_agent::UserAgentConnection, + }, + grpc::{self, user_agent::start}, }; pub mod client; @@ -48,18 +54,19 @@ impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Ser request: Request>, ) -> Result, Status> { let req_stream = request.into_inner(); - let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE); - let transport = user_agent::GrpcTransport::new(tx, req_stream); - let props = UserAgentConnection::new( - self.context.db.clone(), - self.context.actors.clone(), - Box::new(transport), - ); - tokio::spawn(connect_user_agent(props)); + let (bi, rx) = GrpcBi::from_bi_stream(req_stream); + + tokio::spawn(start( + UserAgentConnection { + db: self.context.db.clone(), + actors: self.context.actors.clone(), + }, + bi, + )); info!(event = "connection established", "grpc.user_agent"); - Ok(Response::new(ReceiverStream::new(rx))) + Ok(Response::new(rx)) } } diff --git a/server/crates/arbiter-server/src/grpc/user_agent.rs b/server/crates/arbiter-server/src/grpc/user_agent.rs index 39c6afb..4df6317 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent.rs @@ -1,12 +1,14 @@ +use tokio::sync::mpsc; + use arbiter_proto::{ proto::{ - self, evm::{ EtherTransferSettings as ProtoEtherTransferSettings, EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse, EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, GrantEntry, SharedSettings as ProtoSharedSettings, SpecificGrant as ProtoSpecificGrant, TokenTransferSettings as ProtoTokenTransferSettings, + TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit, WalletCreateResponse, WalletEntry, WalletList, WalletListResponse, evm_grant_create_response::Result as EvmGrantCreateResult, evm_grant_delete_response::Result as EvmGrantDeleteResult, @@ -16,494 +18,538 @@ use arbiter_proto::{ wallet_list_response::Result as WalletListResult, }, user_agent::{ - AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest, - AuthChallengeSolution as ProtoAuthChallengeSolution, AuthOk as ProtoAuthOk, BootstrapEncryptedKey as ProtoBootstrapEncryptedKey, BootstrapResult as ProtoBootstrapResult, ClientConnectionCancel, - ClientConnectionRequest, ClientConnectionResponse, KeyType as ProtoKeyType, - UnsealEncryptedKey as ProtoUnsealEncryptedKey, UnsealResult as ProtoUnsealResult, - UnsealStart, UnsealStartResponse, UserAgentRequest, UserAgentResponse, + ClientConnectionRequest, UnsealEncryptedKey as ProtoUnsealEncryptedKey, + UnsealResult as ProtoUnsealResult, UnsealStart, UserAgentRequest, UserAgentResponse, VaultState as ProtoVaultState, user_agent_request::Payload as UserAgentRequestPayload, user_agent_response::Payload as UserAgentResponsePayload, }, }, - transport::{Bi, Error as TransportError}, + transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi}, }; use async_trait::async_trait; -use futures::StreamExt as _; -use prost_types::Timestamp; -use tokio::sync::mpsc; -use tonic::{Status, Streaming}; +use chrono::{TimeZone, Utc}; +use kameo::{actor::{ActorRef, Spawn as _}, error::SendError}; +use tonic::Status; +use tracing::{info, warn}; use crate::{ - actors::user_agent::{ - self, AuthPublicKey, BootstrapError, Request as DomainRequest, Response as DomainResponse, - TransportResponseError, UnsealError, VaultState, - }, - evm::{ - policies::{Grant, SpecificGrant}, - policies::{ - SharedGrantSettings, TransactionRateLimit, VolumeRateLimit, ether_transfer, - token_transfers, + actors::{ + keyholder::KeyHolderState, + user_agent::{ + OutOfBand, UserAgentConnection, UserAgentSession, + session::{ + BootstrapError, Error, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleGrantList, HandleQueryVaultState, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError + }, }, }, + evm::policies::{ + Grant, SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, + ether_transfer, token_transfers, + }, + utils::defer, }; use alloy::primitives::{Address, U256}; -use chrono::{DateTime, TimeZone, Utc}; +mod auth; -pub struct GrpcTransport { - sender: mpsc::Sender>, - receiver: Streaming, -} - -impl GrpcTransport { - pub fn new( - sender: mpsc::Sender>, - receiver: Streaming, - ) -> Self { - Self { sender, receiver } - } - - fn request_to_domain(request: UserAgentRequest) -> Result { - match request.payload { - Some(UserAgentRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest { - pubkey, - bootstrap_token, - key_type, - })) => Ok(DomainRequest::AuthChallengeRequest { - pubkey: parse_auth_pubkey(key_type, pubkey)?, - bootstrap_token, - }), - Some(UserAgentRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution { - signature, - })) => Ok(DomainRequest::AuthChallengeSolution { signature }), - Some(UserAgentRequestPayload::UnsealStart(UnsealStart { client_pubkey })) => { - let client_pubkey: [u8; 32] = client_pubkey - .as_slice() - .try_into() - .map_err(|_| Status::invalid_argument("client_pubkey must be 32 bytes"))?; - Ok(DomainRequest::UnsealStart { - client_pubkey: x25519_dalek::PublicKey::from(client_pubkey), - }) - } - Some(UserAgentRequestPayload::UnsealEncryptedKey(ProtoUnsealEncryptedKey { - nonce, - ciphertext, - associated_data, - })) => Ok(DomainRequest::UnsealEncryptedKey { - nonce, - ciphertext, - associated_data, - }), - Some(UserAgentRequestPayload::BootstrapEncryptedKey(ProtoBootstrapEncryptedKey { - nonce, - ciphertext, - associated_data, - })) => Ok(DomainRequest::BootstrapEncryptedKey { - nonce, - ciphertext, - associated_data, - }), - Some(UserAgentRequestPayload::QueryVaultState(_)) => Ok(DomainRequest::QueryVaultState), - Some(UserAgentRequestPayload::EvmWalletCreate(_)) => Ok(DomainRequest::EvmWalletCreate), - Some(UserAgentRequestPayload::EvmWalletList(_)) => Ok(DomainRequest::EvmWalletList), - Some(UserAgentRequestPayload::ClientConnectionResponse(ClientConnectionResponse { - approved, - })) => Ok(DomainRequest::ClientConnectionResponse { approved }), - - Some(UserAgentRequestPayload::EvmGrantList(_)) => Ok(DomainRequest::ListGrants), - Some(UserAgentRequestPayload::EvmGrantCreate(EvmGrantCreateRequest { - client_id, - shared, - specific, - })) => { - let shared = parse_shared_settings(client_id, shared)?; - let specific = parse_specific_grant(specific)?; - Ok(DomainRequest::EvmGrantCreate { - client_id, - shared, - specific, - }) - } - Some(UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id })) => { - Ok(DomainRequest::EvmGrantDelete { grant_id }) - } - None => Err(Status::invalid_argument( - "Missing user-agent request payload", - )), - } - } - - fn response_to_proto(response: DomainResponse) -> UserAgentResponse { - let payload = match response { - DomainResponse::AuthChallenge { nonce } => { - UserAgentResponsePayload::AuthChallenge(ProtoAuthChallenge { - pubkey: Vec::new(), - nonce, - }) - } - DomainResponse::AuthOk => UserAgentResponsePayload::AuthOk(ProtoAuthOk {}), - DomainResponse::UnsealStartResponse { server_pubkey } => { - UserAgentResponsePayload::UnsealStartResponse(UnsealStartResponse { - server_pubkey: server_pubkey.as_bytes().to_vec(), - }) - } - DomainResponse::UnsealResult(result) => UserAgentResponsePayload::UnsealResult( - match result { - Ok(()) => ProtoUnsealResult::Success, - Err(UnsealError::InvalidKey) => ProtoUnsealResult::InvalidKey, - Err(UnsealError::Unbootstrapped) => ProtoUnsealResult::Unbootstrapped, - } - .into(), - ), - DomainResponse::BootstrapResult(result) => UserAgentResponsePayload::BootstrapResult( - match result { - Ok(()) => ProtoBootstrapResult::Success, - Err(BootstrapError::AlreadyBootstrapped) => { - ProtoBootstrapResult::AlreadyBootstrapped - } - Err(BootstrapError::InvalidKey) => ProtoBootstrapResult::InvalidKey, - } - .into(), - ), - DomainResponse::VaultState(state) => UserAgentResponsePayload::VaultState( - match state { - VaultState::Unbootstrapped => ProtoVaultState::Unbootstrapped, - VaultState::Sealed => ProtoVaultState::Sealed, - VaultState::Unsealed => ProtoVaultState::Unsealed, - } - .into(), - ), - DomainResponse::ClientConnectionRequest { pubkey } => { - UserAgentResponsePayload::ClientConnectionRequest(ClientConnectionRequest { - pubkey: pubkey.to_bytes().to_vec(), - }) - } - DomainResponse::ClientConnectionCancel => { - UserAgentResponsePayload::ClientConnectionCancel(ClientConnectionCancel {}) - } - DomainResponse::EvmWalletCreate(result) => { - UserAgentResponsePayload::EvmWalletCreate(WalletCreateResponse { - result: Some(match result { - Ok(()) => WalletCreateResult::Wallet(WalletEntry { - address: Vec::new(), - }), - Err(_) => WalletCreateResult::Error(ProtoEvmError::Internal.into()), - }), - }) - } - DomainResponse::EvmWalletList(wallets) => { - UserAgentResponsePayload::EvmWalletList(WalletListResponse { - result: Some(WalletListResult::Wallets(WalletList { - wallets: wallets - .into_iter() - .map(|addr| WalletEntry { - address: addr.as_slice().to_vec(), - }) - .collect(), - })), - }) - } - DomainResponse::ListGrants(grants) => { - UserAgentResponsePayload::EvmGrantList(EvmGrantListResponse { - result: Some(EvmGrantListResult::Grants(EvmGrantList { - grants: grants.into_iter().map(grant_to_proto).collect(), - })), - }) - } - DomainResponse::EvmGrantCreate(result) => { - UserAgentResponsePayload::EvmGrantCreate(EvmGrantCreateResponse { - result: Some(match result { - Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id), - Err(_) => EvmGrantCreateResult::Error(ProtoEvmError::Internal.into()), - }), - }) - } - DomainResponse::EvmGrantDelete(result) => { - UserAgentResponsePayload::EvmGrantDelete(EvmGrantDeleteResponse { - result: Some(match result { - Ok(()) => EvmGrantDeleteResult::Ok(()), - Err(_) => EvmGrantDeleteResult::Error(ProtoEvmError::Internal.into()), - }), - }) - } - }; - - UserAgentResponse { - payload: Some(payload), - } - } - - fn error_to_status(value: TransportResponseError) -> Status { - match value { - TransportResponseError::UnexpectedRequestPayload => { - Status::invalid_argument("Expected message with payload") - } - TransportResponseError::InvalidStateForUnsealEncryptedKey => { - Status::failed_precondition("Invalid state for unseal encrypted key") - } - TransportResponseError::InvalidClientPubkeyLength => { - Status::invalid_argument("client_pubkey must be 32 bytes") - } - TransportResponseError::StateTransitionFailed => { - Status::internal("State machine error") - } - TransportResponseError::KeyHolderActorUnreachable => { - Status::internal("Vault is not available") - } - TransportResponseError::Auth(ref err) => auth_error_status(err), - TransportResponseError::ConnectionRegistrationFailed => { - Status::internal("Failed registering connection") - } - } - } -} +pub struct OutOfBandAdapter(mpsc::Sender); #[async_trait] -impl Bi> for GrpcTransport { - async fn send( - &mut self, - item: Result, - ) -> Result<(), TransportError> { - let outbound = match item { - Ok(message) => Ok(Self::response_to_proto(message)), - Err(err) => Err(Self::error_to_status(err)), +impl Sender for OutOfBandAdapter { + async fn send(&mut self, item: OutOfBand) -> Result<(), TransportError> { + self.0.send(item).await.map_err(|e| { + warn!(error = ?e, "Failed to send out-of-band message"); + TransportError::ChannelClosed + }) + } +} + +async fn dispatch_loop( + mut bi: GrpcBi, + actor: ActorRef, + mut receiver: mpsc::Receiver, +) { + loop { + tokio::select! { + oob = receiver.recv() => { + let Some(oob) = oob else { + return; + }; + + if send_out_of_band(&mut bi, oob).await.is_err() { + return; + } + } + + conn = bi.recv() => { + let Some(conn) = conn else { + return; + }; + + if dispatch_conn_message(&mut bi, &actor, conn).await.is_err() { + return; + } + } + } + } +} + +async fn dispatch_conn_message( + bi: &mut GrpcBi, + actor: &ActorRef, + conn: Result, +) -> Result<(), ()> { + let conn = match conn { + Ok(conn) => conn, + Err(err) => { + warn!(error = ?err, "Failed to receive user agent request"); + return Err(()); + } + }; + + let Some(payload) = conn.payload else { + let _ = bi.send(Err(Status::invalid_argument("Missing user-agent request payload"))).await; + return Err(()); + }; + + let payload = match payload { + UserAgentRequestPayload::UnsealStart(UnsealStart { client_pubkey }) => { + let client_pubkey = match <[u8; 32]>::try_from(client_pubkey) { + Ok(bytes) => x25519_dalek::PublicKey::from(bytes), + Err(_) => { + let _ = bi.send(Err(Status::invalid_argument("Invalid X25519 public key"))).await; + return Err(()); + } + }; + + match actor.ask(HandleUnsealRequest { client_pubkey }).await { + Ok(response) => UserAgentResponsePayload::UnsealStartResponse( + arbiter_proto::proto::user_agent::UnsealStartResponse { + server_pubkey: response.server_pubkey.as_bytes().to_vec(), + }, + ), + Err(err) => { + warn!(error = ?err, "Failed to handle unseal start request"); + let _ = bi.send(Err(Status::internal("Failed to start unseal flow"))).await; + return Err(()); + } + } + } + UserAgentRequestPayload::UnsealEncryptedKey(ProtoUnsealEncryptedKey { + nonce, + ciphertext, + associated_data, + }) => UserAgentResponsePayload::UnsealResult( + match actor + .ask(HandleUnsealEncryptedKey { + nonce, + ciphertext, + associated_data, + }) + .await + { + Ok(()) => ProtoUnsealResult::Success, + Err(SendError::HandlerError(UnsealError::InvalidKey)) => { + ProtoUnsealResult::InvalidKey + } + Err(err) => { + warn!(error = ?err, "Failed to handle unseal request"); + let _ = bi.send(Err(Status::internal("Failed to unseal vault"))).await; + return Err(()); + } + } + .into(), + ), + UserAgentRequestPayload::BootstrapEncryptedKey(ProtoBootstrapEncryptedKey { + nonce, + ciphertext, + associated_data, + }) => UserAgentResponsePayload::BootstrapResult( + match actor + .ask(HandleBootstrapEncryptedKey { + nonce, + ciphertext, + 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"); + let _ = bi.send(Err(Status::internal("Failed to bootstrap vault"))).await; + return Err(()); + } + } + .into(), + ), + UserAgentRequestPayload::QueryVaultState(_) => UserAgentResponsePayload::VaultState( + match actor.ask(HandleQueryVaultState {}).await { + Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, + Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed, + Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed, + Err(err) => { + warn!(error = ?err, "Failed to query vault state"); + ProtoVaultState::Error + } + } + .into(), + ), + UserAgentRequestPayload::EvmWalletCreate(_) => UserAgentResponsePayload::EvmWalletCreate( + EvmGrantOrWallet::wallet_create_response(actor.ask(HandleEvmWalletCreate {}).await), + ), + UserAgentRequestPayload::EvmWalletList(_) => UserAgentResponsePayload::EvmWalletList( + EvmGrantOrWallet::wallet_list_response(actor.ask(HandleEvmWalletList {}).await), + ), + UserAgentRequestPayload::EvmGrantList(_) => UserAgentResponsePayload::EvmGrantList( + EvmGrantOrWallet::grant_list_response(actor.ask(HandleGrantList {}).await), + ), + UserAgentRequestPayload::EvmGrantCreate(EvmGrantCreateRequest { + client_id, + shared, + specific, + }) => { + let (basic, grant) = match parse_grant_request(shared, specific) { + Ok(values) => values, + Err(status) => { + let _ = bi.send(Err(status)).await; + return Err(()); + } + }; + + UserAgentResponsePayload::EvmGrantCreate(EvmGrantOrWallet::grant_create_response( + actor.ask(HandleGrantCreate { + client_id, + basic, + grant, + }) + .await, + )) + } + UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id }) => { + UserAgentResponsePayload::EvmGrantDelete(EvmGrantOrWallet::grant_delete_response( + actor.ask(HandleGrantDelete { grant_id }).await, + )) + } + payload => { + warn!(?payload, "Unsupported post-auth user agent request"); + let _ = bi.send(Err(Status::invalid_argument("Unsupported user-agent request"))).await; + return Err(()); + } + }; + + bi.send(Ok(UserAgentResponse { + payload: Some(payload), + })) + .await + .map_err(|_| ()) +} + +async fn send_out_of_band( + bi: &mut GrpcBi, + oob: OutOfBand, +) -> Result<(), ()> { + let payload = match oob { + OutOfBand::ClientConnectionRequest { pubkey } => { + UserAgentResponsePayload::ClientConnectionRequest(ClientConnectionRequest { + pubkey: pubkey.to_bytes().to_vec(), + }) + } + OutOfBand::ClientConnectionCancel => { + UserAgentResponsePayload::ClientConnectionCancel(ClientConnectionCancel {}) + } + }; + + bi.send(Ok(UserAgentResponse { + payload: Some(payload), + })) + .await + .map_err(|_| ()) +} + +fn parse_grant_request( + shared: Option, + specific: Option, +) -> Result<(SharedGrantSettings, SpecificGrant), Status> { + let shared = shared.ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))?; + let specific = + specific.ok_or_else(|| Status::invalid_argument("Missing specific grant settings"))?; + + Ok((shared_settings_from_proto(shared)?, specific_grant_from_proto(specific)?)) +} + +fn shared_settings_from_proto(shared: ProtoSharedSettings) -> Result { + Ok(SharedGrantSettings { + wallet_id: shared.wallet_id, + client_id: 0, + chain: shared.chain_id, + valid_from: shared + .valid_from + .map(proto_timestamp_to_utc) + .transpose()?, + valid_until: shared + .valid_until + .map(proto_timestamp_to_utc) + .transpose()?, + max_gas_fee_per_gas: shared + .max_gas_fee_per_gas + .as_deref() + .map(u256_from_proto_bytes) + .transpose()?, + max_priority_fee_per_gas: shared + .max_priority_fee_per_gas + .as_deref() + .map(u256_from_proto_bytes) + .transpose()?, + rate_limit: shared + .rate_limit + .map(|limit| TransactionRateLimit { + count: limit.count, + window: chrono::Duration::seconds(limit.window_secs), + }), + }) +} + +fn specific_grant_from_proto(specific: ProtoSpecificGrant) -> Result { + match specific.grant { + Some(ProtoSpecificGrantType::EtherTransfer(ProtoEtherTransferSettings { + targets, + limit, + })) => Ok(SpecificGrant::EtherTransfer(ether_transfer::Settings { + target: targets + .into_iter() + .map(address_from_bytes) + .collect::>()?, + limit: volume_rate_limit_from_proto( + limit.ok_or_else(|| { + Status::invalid_argument("Missing ether transfer volume rate limit") + })?, + )?, + })), + Some(ProtoSpecificGrantType::TokenTransfer(ProtoTokenTransferSettings { + token_contract, + target, + volume_limits, + })) => Ok(SpecificGrant::TokenTransfer(token_transfers::Settings { + token_contract: address_from_bytes(token_contract)?, + target: target.map(address_from_bytes).transpose()?, + volume_limits: volume_limits + .into_iter() + .map(volume_rate_limit_from_proto) + .collect::>()?, + })), + None => Err(Status::invalid_argument("Missing specific grant kind")), + } +} + +fn volume_rate_limit_from_proto(limit: ProtoVolumeRateLimit) -> Result { + Ok(VolumeRateLimit { + max_volume: u256_from_proto_bytes(&limit.max_volume)?, + window: chrono::Duration::seconds(limit.window_secs), + }) +} + +fn address_from_bytes(bytes: Vec) -> Result { + if bytes.len() != 20 { + return Err(Status::invalid_argument("Invalid EVM address")); + } + + Ok(Address::from_slice(&bytes)) +} + +fn u256_from_proto_bytes(bytes: &[u8]) -> Result { + if bytes.len() > 32 { + return Err(Status::invalid_argument("Invalid U256 byte length")); + } + + Ok(U256::from_be_slice(bytes)) +} + +fn proto_timestamp_to_utc( + timestamp: prost_types::Timestamp, +) -> Result, Status> { + Utc.timestamp_opt(timestamp.seconds, timestamp.nanos as u32) + .single() + .ok_or_else(|| Status::invalid_argument("Invalid timestamp")) +} + +fn shared_settings_to_proto(shared: SharedGrantSettings) -> ProtoSharedSettings { + ProtoSharedSettings { + wallet_id: shared.wallet_id, + chain_id: shared.chain, + valid_from: shared.valid_from.map(|time| prost_types::Timestamp { + seconds: time.timestamp(), + nanos: time.timestamp_subsec_nanos() as i32, + }), + valid_until: shared.valid_until.map(|time| prost_types::Timestamp { + seconds: time.timestamp(), + nanos: time.timestamp_subsec_nanos() as i32, + }), + max_gas_fee_per_gas: shared.max_gas_fee_per_gas.map(|value| { + value.to_be_bytes::<32>().to_vec() + }), + max_priority_fee_per_gas: shared.max_priority_fee_per_gas.map(|value| { + value.to_be_bytes::<32>().to_vec() + }), + rate_limit: shared.rate_limit.map(|limit| ProtoTransactionRateLimit { + count: limit.count, + window_secs: limit.window.num_seconds(), + }), + } +} + +fn specific_grant_to_proto(grant: SpecificGrant) -> ProtoSpecificGrant { + let grant = match grant { + SpecificGrant::EtherTransfer(settings) => { + ProtoSpecificGrantType::EtherTransfer(ProtoEtherTransferSettings { + targets: settings.target.into_iter().map(|address| address.to_vec()).collect(), + limit: Some(ProtoVolumeRateLimit { + max_volume: settings.limit.max_volume.to_be_bytes::<32>().to_vec(), + window_secs: settings.limit.window.num_seconds(), + }), + }) + } + SpecificGrant::TokenTransfer(settings) => { + ProtoSpecificGrantType::TokenTransfer(ProtoTokenTransferSettings { + token_contract: settings.token_contract.to_vec(), + target: settings.target.map(|address| address.to_vec()), + volume_limits: settings + .volume_limits + .into_iter() + .map(|limit| ProtoVolumeRateLimit { + max_volume: limit.max_volume.to_be_bytes::<32>().to_vec(), + window_secs: limit.window.num_seconds(), + }) + .collect(), + }) + } + }; + + ProtoSpecificGrant { grant: Some(grant) } +} + +struct EvmGrantOrWallet; + +impl EvmGrantOrWallet { + fn wallet_create_response( + result: Result>, + ) -> WalletCreateResponse { + let result = match result { + Ok(wallet) => WalletCreateResult::Wallet(WalletEntry { + address: wallet.to_vec(), + }), + Err(err) => { + warn!(error = ?err, "Failed to create EVM wallet"); + WalletCreateResult::Error(ProtoEvmError::Internal.into()) + } }; - self.sender - .send(outbound) - .await - .map_err(|_| TransportError::ChannelClosed) + WalletCreateResponse { result: Some(result) } } - async fn recv(&mut self) -> Option { - match self.receiver.next().await { - Some(Ok(item)) => match Self::request_to_domain(item) { - Ok(request) => Some(request), - Err(status) => { - let _ = self.sender.send(Err(status)).await; - None - } - }, - Some(Err(error)) => { - tracing::error!(error = ?error, "grpc user-agent recv failed; closing stream"); - None + fn wallet_list_response( + result: Result, SendError>, + ) -> WalletListResponse { + let result = match result { + Ok(wallets) => WalletListResult::Wallets(WalletList { + wallets: wallets + .into_iter() + .map(|wallet| WalletEntry { + address: wallet.to_vec(), + }) + .collect(), + }), + Err(err) => { + warn!(error = ?err, "Failed to list EVM wallets"); + WalletListResult::Error(ProtoEvmError::Internal.into()) } - None => None, - } + }; + + WalletListResponse { result: Some(result) } + } + + fn grant_create_response( + result: Result>, + ) -> EvmGrantCreateResponse { + let result = match result { + Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id), + Err(err) => { + warn!(error = ?err, "Failed to create EVM grant"); + EvmGrantCreateResult::Error(ProtoEvmError::Internal.into()) + } + }; + + EvmGrantCreateResponse { result: Some(result) } + } + + fn grant_delete_response( + result: Result<(), SendError>, + ) -> EvmGrantDeleteResponse { + let result = match result { + Ok(()) => EvmGrantDeleteResult::Ok(()), + Err(err) => { + warn!(error = ?err, "Failed to delete EVM grant"); + EvmGrantDeleteResult::Error(ProtoEvmError::Internal.into()) + } + }; + + EvmGrantDeleteResponse { result: Some(result) } + } + + fn grant_list_response( + result: Result>, SendError>, + ) -> EvmGrantListResponse { + let result = match result { + Ok(grants) => EvmGrantListResult::Grants(EvmGrantList { + grants: grants + .into_iter() + .map(|grant| GrantEntry { + id: grant.id, + client_id: grant.shared.client_id, + shared: Some(shared_settings_to_proto(grant.shared)), + specific: Some(specific_grant_to_proto(grant.settings)), + }) + .collect(), + }), + Err(err) => { + warn!(error = ?err, "Failed to list EVM grants"); + EvmGrantListResult::Error(ProtoEvmError::Internal.into()) + } + }; + + EvmGrantListResponse { result: Some(result) } } } -fn grant_to_proto(grant: Grant) -> proto::evm::GrantEntry { - GrantEntry { - id: grant.id, - specific: Some(match grant.settings { - SpecificGrant::EtherTransfer(settings) => ProtoSpecificGrant { - grant: Some(ProtoSpecificGrantType::EtherTransfer( - ProtoEtherTransferSettings { - targets: settings - .target - .into_iter() - .map(|addr| addr.as_slice().to_vec()) - .collect(), - limit: Some(proto::evm::VolumeRateLimit { - max_volume: settings.limit.max_volume.to_be_bytes_vec(), - window_secs: settings.limit.window.num_seconds(), - }), - }, - )), - }, - SpecificGrant::TokenTransfer(settings) => ProtoSpecificGrant { - grant: Some(ProtoSpecificGrantType::TokenTransfer( - ProtoTokenTransferSettings { - token_contract: settings.token_contract.as_slice().to_vec(), - target: settings.target.map(|addr| addr.as_slice().to_vec()), - volume_limits: settings - .volume_limits - .into_iter() - .map(|vrl| proto::evm::VolumeRateLimit { - max_volume: vrl.max_volume.to_be_bytes_vec(), - window_secs: vrl.window.num_seconds(), - }) - .collect(), - }, - )), - }, - }), - client_id: grant.shared.client_id, - shared: Some(proto::evm::SharedSettings { - wallet_id: grant.shared.wallet_id, - chain_id: grant.shared.chain, - valid_from: grant.shared.valid_from.map(|dt| Timestamp { - seconds: dt.timestamp(), - nanos: 0, - }), - valid_until: grant.shared.valid_until.map(|dt| Timestamp { - seconds: dt.timestamp(), - nanos: 0, - }), - max_gas_fee_per_gas: grant - .shared - .max_gas_fee_per_gas - .map(|fee| fee.to_be_bytes_vec()), - max_priority_fee_per_gas: grant - .shared - .max_priority_fee_per_gas - .map(|fee| fee.to_be_bytes_vec()), - rate_limit: grant - .shared - .rate_limit - .map(|limit| proto::evm::TransactionRateLimit { - count: limit.count, - window_secs: limit.window.num_seconds(), - }), - }), - } -} - -fn parse_volume_rate_limit(vrl: ProtoVolumeRateLimit) -> Result { - Ok(VolumeRateLimit { - max_volume: U256::from_be_slice(&vrl.max_volume), - window: chrono::Duration::seconds(vrl.window_secs), - }) -} - -fn parse_shared_settings( - client_id: i32, - proto: Option, -) -> Result { - let s = proto.ok_or_else(|| Status::invalid_argument("missing shared settings"))?; - let parse_u256 = |b: Vec| -> Result { - if b.is_empty() { - Err(Status::invalid_argument("U256 bytes must not be empty")) - } else { - Ok(U256::from_be_slice(&b)) +pub async fn start( + mut conn: UserAgentConnection, + mut bi: GrpcBi, +) { + let pubkey = match auth::start(&mut conn, &mut bi).await { + Ok(pubkey) => pubkey, + Err(e) => { + warn!(error = ?e, "Authentication failed"); + return; } }; - let parse_ts = |ts: prost_types::Timestamp| -> Result, Status> { - Utc.timestamp_opt(ts.seconds, ts.nanos as u32) - .single() - .ok_or_else(|| Status::invalid_argument("invalid timestamp")) - }; - Ok(SharedGrantSettings { - wallet_id: s.wallet_id, - client_id, - chain: s.chain_id, - valid_from: s.valid_from.map(parse_ts).transpose()?, - valid_until: s.valid_until.map(parse_ts).transpose()?, - max_gas_fee_per_gas: s.max_gas_fee_per_gas.map(parse_u256).transpose()?, - max_priority_fee_per_gas: s.max_priority_fee_per_gas.map(parse_u256).transpose()?, - rate_limit: s.rate_limit.map(|rl| TransactionRateLimit { - count: rl.count, - window: chrono::Duration::seconds(rl.window_secs), - }), - }) -} - -fn parse_specific_grant(proto: Option) -> Result { - use proto::evm::specific_grant::Grant as ProtoGrant; - let g = proto - .and_then(|sg| sg.grant) - .ok_or_else(|| Status::invalid_argument("missing specific grant"))?; - match g { - ProtoGrant::EtherTransfer(s) => { - let limit = parse_volume_rate_limit( - s.limit - .ok_or_else(|| Status::invalid_argument("missing ether transfer limit"))?, - )?; - let target = s - .targets - .into_iter() - .map(|b| { - if b.len() == 20 { - Ok(Address::from_slice(&b)) - } else { - Err(Status::invalid_argument( - "ether transfer target must be 20 bytes", - )) - } - }) - .collect::, _>>()?; - Ok(SpecificGrant::EtherTransfer(ether_transfer::Settings { - target, - limit, - })) - } - ProtoGrant::TokenTransfer(s) => { - if s.token_contract.len() != 20 { - return Err(Status::invalid_argument("token_contract must be 20 bytes")); - } - let target = s - .target - .map(|b| { - if b.len() == 20 { - Ok(Address::from_slice(&b)) - } else { - Err(Status::invalid_argument( - "token transfer target must be 20 bytes", - )) - } - }) - .transpose()?; - let volume_limits = s - .volume_limits - .into_iter() - .map(parse_volume_rate_limit) - .collect::, _>>()?; - Ok(SpecificGrant::TokenTransfer(token_transfers::Settings { - token_contract: Address::from_slice(&s.token_contract), - target, - volume_limits, - })) - } - } -} - -fn parse_auth_pubkey(key_type: i32, pubkey: Vec) -> Result { - match ProtoKeyType::try_from(key_type).unwrap_or(ProtoKeyType::Unspecified) { - ProtoKeyType::Unspecified | ProtoKeyType::Ed25519 => { - let bytes: [u8; 32] = pubkey - .as_slice() - .try_into() - .map_err(|_| Status::invalid_argument("invalid Ed25519 public key length"))?; - let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes) - .map_err(|_| Status::invalid_argument("invalid Ed25519 public key encoding"))?; - Ok(AuthPublicKey::Ed25519(key)) - } - ProtoKeyType::EcdsaSecp256k1 => { - let key = k256::ecdsa::VerifyingKey::from_sec1_bytes(&pubkey) - .map_err(|_| Status::invalid_argument("invalid secp256k1 public key encoding"))?; - Ok(AuthPublicKey::EcdsaSecp256k1(key)) - } - ProtoKeyType::Rsa => { - use rsa::pkcs8::DecodePublicKey as _; - - let key = rsa::RsaPublicKey::from_public_key_der(&pubkey) - .map_err(|_| Status::invalid_argument("invalid RSA public key encoding"))?; - Ok(AuthPublicKey::Rsa(key)) - } - } -} - -fn auth_error_status(value: &user_agent::auth::Error) -> Status { - use user_agent::auth::Error; - - match value { - Error::UnexpectedMessagePayload | Error::InvalidClientPubkeyLength => { - Status::invalid_argument(value.to_string()) - } - Error::InvalidAuthPubkeyEncoding => { - Status::invalid_argument("Failed to convert pubkey to VerifyingKey") - } - Error::PublicKeyNotRegistered | Error::InvalidChallengeSolution => { - Status::unauthenticated(value.to_string()) - } - Error::InvalidBootstrapToken => Status::invalid_argument("Invalid bootstrap token"), - Error::Transport => Status::internal("Transport error"), - Error::BootstrapperActorUnreachable => { - Status::internal("Bootstrap token consumption failed") - } - Error::DatabasePoolUnavailable => Status::internal("Database pool error"), - Error::DatabaseOperationFailed => Status::internal("Database error"), - } + + 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(); + + // when connection closes + let _ = defer(move || { + actor_for_cleanup.kill(); + }); + + info!(?pubkey, "User authenticated successfully"); + dispatch_loop(bi, actor, oob_receiver).await; } diff --git a/server/crates/arbiter-server/src/grpc/user_agent/auth.rs b/server/crates/arbiter-server/src/grpc/user_agent/auth.rs new file mode 100644 index 0000000..0e648a8 --- /dev/null +++ b/server/crates/arbiter-server/src/grpc/user_agent/auth.rs @@ -0,0 +1,151 @@ +use arbiter_proto::{ + proto::{ + self, + evm::{ + EtherTransferSettings as ProtoEtherTransferSettings, EvmError as ProtoEvmError, + EvmGrantCreateRequest, EvmGrantCreateResponse, EvmGrantDeleteRequest, + EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, GrantEntry, + SharedSettings as ProtoSharedSettings, SpecificGrant as ProtoSpecificGrant, + TokenTransferSettings as ProtoTokenTransferSettings, + VolumeRateLimit as ProtoVolumeRateLimit, WalletCreateResponse, WalletEntry, WalletList, + WalletListResponse, evm_grant_create_response::Result as EvmGrantCreateResult, + evm_grant_delete_response::Result as EvmGrantDeleteResult, + evm_grant_list_response::Result as EvmGrantListResult, + specific_grant::Grant as ProtoSpecificGrantType, + wallet_create_response::Result as WalletCreateResult, + wallet_list_response::Result as WalletListResult, + }, + user_agent::{ + AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest, + AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult, + BootstrapEncryptedKey as ProtoBootstrapEncryptedKey, + BootstrapResult as ProtoBootstrapResult, ClientConnectionCancel, + ClientConnectionRequest, ClientConnectionResponse, KeyType as ProtoKeyType, + UnsealEncryptedKey as ProtoUnsealEncryptedKey, UnsealResult as ProtoUnsealResult, + UnsealStart, UnsealStartResponse, UserAgentRequest, UserAgentResponse, + VaultState as ProtoVaultState, user_agent_request::Payload as UserAgentRequestPayload, + user_agent_response::Payload as UserAgentResponsePayload, + }, + }, + transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi}, +}; +use async_trait::async_trait; +use tonic::{Status, Streaming}; +use tracing::{info, warn}; + +use crate::{ + actors::user_agent::{ + self, AuthPublicKey, OutOfBand as DomainResponse, UserAgentConnection, auth, + }, + db::models::KeyType, + evm::policies::{ + Grant, SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, + ether_transfer, token_transfers, + }, +}; +use alloy::primitives::{Address, U256}; +use chrono::{DateTime, TimeZone, Utc}; + +pub struct AuthTransportAdapter<'a>(&'a mut GrpcBi); + +#[async_trait] +impl Sender> for AuthTransportAdapter<'_> { + async fn send( + &mut self, + item: Result, + ) -> Result<(), TransportError> { + use auth::{Error, Outbound}; + let response = match item { + Ok(Outbound::AuthChallenge { nonce }) => Ok(UserAgentResponsePayload::AuthChallenge( + ProtoAuthChallenge { nonce }, + )), + Ok(Outbound::AuthSuccess) => Ok(UserAgentResponsePayload::AuthResult( + ProtoAuthResult::Success.into(), + )), + + Err(Error::UnregisteredPublicKey) => Ok(UserAgentResponsePayload::AuthResult( + ProtoAuthResult::InvalidKey.into(), + )), + Err(Error::InvalidChallengeSolution) => Ok(UserAgentResponsePayload::AuthResult( + ProtoAuthResult::InvalidSignature.into(), + )), + Err(Error::InvalidBootstrapToken) => Ok(UserAgentResponsePayload::BootstrapResult( + ProtoAuthResult::TokenInvalid.into(), + )), + Err(Error::Internal { details }) => Err(Status::internal(details)), + Err(Error::Transport) => Err(Status::unavailable("transport error")), + }; + self.0 + .send(response.map(|r| UserAgentResponse { payload: Some(r) })) + .await + } +} + +#[async_trait] +impl Receiver for AuthTransportAdapter<'_> { + async fn recv(&mut self) -> Option { + let Ok(UserAgentRequest { + payload: Some(payload), + }) = self.0.recv().await? + else { + warn!( + event = "received request with empty payload", + "grpc.useragent.auth_adapter" + ); + return None; + }; + + match payload { + UserAgentRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest { + pubkey, + bootstrap_token, + key_type, + }) => { + let Ok(key_type) = ProtoKeyType::try_from(key_type) else { + warn!( + event = "received request with invalid key type", + "grpc.useragent.auth_adapter" + ); + return None; + }; + let key_type = match key_type { + ProtoKeyType::Ed25519 => KeyType::Ed25519, + ProtoKeyType::EcdsaSecp256k1 => KeyType::EcdsaSecp256k1, + ProtoKeyType::Rsa => KeyType::Rsa, + ProtoKeyType::Unspecified => { + warn!( + event = "received request with unspecified key type", + "grpc.useragent.auth_adapter" + ); + return None; + } + }; + let Ok(pubkey) = AuthPublicKey::try_from((key_type, pubkey)) else { + warn!( + event = "received request with invalid public key", + "grpc.useragent.auth_adapter" + ); + return None; + }; + + Some(auth::Inbound::AuthChallengeRequest { + pubkey, + bootstrap_token, + }) + } + UserAgentRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution { + signature, + }) => Some(auth::Inbound::AuthChallengeSolution { signature }), + _ => None, // Ignore other request types for this adapter + } + } +} +impl Bi> for AuthTransportAdapter<'_> {} + +pub async fn start( + conn: &mut UserAgentConnection, + bi: &mut GrpcBi, +) -> Result { + let mut transport = AuthTransportAdapter(bi); + auth::authenticate(conn, transport).await +} diff --git a/server/crates/arbiter-server/src/lib.rs b/server/crates/arbiter-server/src/lib.rs index 410e499..0b255e5 100644 --- a/server/crates/arbiter-server/src/lib.rs +++ b/server/crates/arbiter-server/src/lib.rs @@ -13,6 +13,7 @@ pub mod db; pub mod evm; pub mod grpc; pub mod safe_cell; +pub mod utils; const DEFAULT_CHANNEL_SIZE: usize = 1000; diff --git a/server/crates/arbiter-server/src/utils.rs b/server/crates/arbiter-server/src/utils.rs new file mode 100644 index 0000000..d072aa7 --- /dev/null +++ b/server/crates/arbiter-server/src/utils.rs @@ -0,0 +1,16 @@ +struct DeferClosure { + f: Option, +} + +impl Drop for DeferClosure { + fn drop(&mut self) { + if let Some(f) = self.f.take() { + f(); + } + } +} + +// Run some code when a scope is exited, similar to Go's defer statement +pub fn defer(f: F) -> impl Drop + Sized { + DeferClosure { f: Some(f) } +} diff --git a/server/crates/arbiter-server/tests/user_agent/auth.rs b/server/crates/arbiter-server/tests/user_agent/auth.rs index 4f23e9d..1a7bbad 100644 --- a/server/crates/arbiter-server/tests/user_agent/auth.rs +++ b/server/crates/arbiter-server/tests/user_agent/auth.rs @@ -3,7 +3,7 @@ use arbiter_server::{ actors::{ GlobalActors, bootstrap::GetToken, - user_agent::{AuthPublicKey, Request, Response, UserAgentConnection, connect_user_agent}, + user_agent::{AuthPublicKey, Request, OutOfBand, UserAgentConnection, connect_user_agent}, }, db::{self, schema}, }; @@ -118,7 +118,7 @@ pub async fn test_challenge_auth() { .expect("should receive challenge"); let challenge = match response { Ok(resp) => match resp { - Response::AuthChallenge { nonce } => nonce, + OutOfBand::AuthChallenge { nonce } => nonce, other => panic!("Expected AuthChallenge, got {other:?}"), }, Err(err) => panic!("Expected Ok response, got Err({err:?})"), diff --git a/server/crates/arbiter-server/tests/user_agent/unseal.rs b/server/crates/arbiter-server/tests/user_agent/unseal.rs index ec5de37..4b6d7a3 100644 --- a/server/crates/arbiter-server/tests/user_agent/unseal.rs +++ b/server/crates/arbiter-server/tests/user_agent/unseal.rs @@ -2,7 +2,7 @@ use arbiter_server::{ actors::{ GlobalActors, keyholder::{Bootstrap, Seal}, - user_agent::{Request, Response, UnsealError, session::UserAgentSession}, + user_agent::{Request, OutOfBand, UnsealError, session::UserAgentSession}, }, db, safe_cell::{SafeCell, SafeCellHandle as _}, @@ -40,7 +40,7 @@ async fn client_dh_encrypt(user_agent: &mut UserAgentSession, key_to_send: &[u8] .unwrap(); let server_pubkey = match response { - Response::UnsealStartResponse { server_pubkey } => server_pubkey, + OutOfBand::UnsealStartResponse { server_pubkey } => server_pubkey, other => panic!("Expected UnsealStartResponse, got {other:?}"), }; @@ -73,7 +73,7 @@ pub async fn test_unseal_success() { .await .unwrap(); - assert!(matches!(response, Response::UnsealResult(Ok(())))); + assert!(matches!(response, OutOfBand::UnsealResult(Ok(())))); } #[tokio::test] @@ -90,7 +90,7 @@ pub async fn test_unseal_wrong_seal_key() { assert!(matches!( response, - Response::UnsealResult(Err(UnsealError::InvalidKey)) + OutOfBand::UnsealResult(Err(UnsealError::InvalidKey)) )); } @@ -120,7 +120,7 @@ pub async fn test_unseal_corrupted_ciphertext() { assert!(matches!( response, - Response::UnsealResult(Err(UnsealError::InvalidKey)) + OutOfBand::UnsealResult(Err(UnsealError::InvalidKey)) )); } @@ -140,7 +140,7 @@ pub async fn test_unseal_retry_after_invalid_key() { assert!(matches!( response, - Response::UnsealResult(Err(UnsealError::InvalidKey)) + OutOfBand::UnsealResult(Err(UnsealError::InvalidKey)) )); } @@ -152,6 +152,6 @@ pub async fn test_unseal_retry_after_invalid_key() { .await .unwrap(); - assert!(matches!(response, Response::UnsealResult(Ok(())))); + assert!(matches!(response, OutOfBand::UnsealResult(Ok(())))); } } -- 2.49.1 From a663363626b0b30a89ec9ac3b2118fd3d74858c2 Mon Sep 17 00:00:00 2001 From: hdbg Date: Wed, 18 Mar 2026 22:40:07 +0100 Subject: [PATCH 2/8] refactor(server::client): migrated to new connection design --- protobufs/client.proto | 33 ++- .../arbiter-server/src/actors/client/auth.rs | 114 ++++------ .../arbiter-server/src/actors/client/mod.rs | 57 +---- .../src/actors/client/session.rs | 84 +++---- .../src/actors/user_agent/auth/state.rs | 11 +- .../crates/arbiter-server/src/grpc/client.rs | 214 ++++++++---------- .../arbiter-server/src/grpc/client/auth.rs | 131 +++++++++++ server/crates/arbiter-server/src/grpc/mod.rs | 19 +- .../arbiter-server/src/grpc/user_agent.rs | 131 ++++++----- server/crates/arbiter-server/src/lib.rs | 7 +- .../arbiter-server/tests/client/auth.rs | 45 ++-- .../crates/arbiter-server/tests/common/mod.rs | 25 +- .../arbiter-server/tests/user_agent/auth.rs | 2 +- .../arbiter-server/tests/user_agent/unseal.rs | 2 +- 14 files changed, 474 insertions(+), 401 deletions(-) create mode 100644 server/crates/arbiter-server/src/grpc/client/auth.rs diff --git a/protobufs/client.proto b/protobufs/client.proto index 62761c3..dbe9708 100644 --- a/protobufs/client.proto +++ b/protobufs/client.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package arbiter.client; import "evm.proto"; +import "google/protobuf/empty.proto"; message AuthChallengeRequest { bytes pubkey = 1; @@ -17,30 +18,38 @@ message AuthChallengeSolution { bytes signature = 1; } -message AuthOk {} +enum AuthResult { + AUTH_RESULT_UNSPECIFIED = 0; + AUTH_RESULT_SUCCESS = 1; + AUTH_RESULT_INVALID_KEY = 2; + AUTH_RESULT_INVALID_SIGNATURE = 3; + AUTH_RESULT_APPROVAL_DENIED = 4; + AUTH_RESULT_NO_USER_AGENTS_ONLINE = 5; + AUTH_RESULT_INTERNAL = 6; +} + +enum VaultState { + VAULT_STATE_UNSPECIFIED = 0; + VAULT_STATE_UNBOOTSTRAPPED = 1; + VAULT_STATE_SEALED = 2; + VAULT_STATE_UNSEALED = 3; + VAULT_STATE_ERROR = 4; +} message ClientRequest { oneof payload { AuthChallengeRequest auth_challenge_request = 1; AuthChallengeSolution auth_challenge_solution = 2; + google.protobuf.Empty query_vault_state = 3; } } -message ClientConnectError { - enum Code { - UNKNOWN = 0; - APPROVAL_DENIED = 1; - NO_USER_AGENTS_ONLINE = 2; - } - Code code = 1; -} - message ClientResponse { oneof payload { AuthChallenge auth_challenge = 1; - AuthOk auth_ok = 2; - ClientConnectError client_connect_error = 5; + AuthResult auth_result = 2; arbiter.evm.EvmSignTransactionResponse evm_sign_transaction = 3; arbiter.evm.EvmAnalyzeTransactionResponse evm_analyze_transaction = 4; + VaultState vault_state = 6; } } diff --git a/server/crates/arbiter-server/src/actors/client/auth.rs b/server/crates/arbiter-server/src/actors/client/auth.rs index c69fb77..ffd425a 100644 --- a/server/crates/arbiter-server/src/actors/client/auth.rs +++ b/server/crates/arbiter-server/src/actors/client/auth.rs @@ -1,30 +1,25 @@ -use arbiter_proto::{format_challenge, transport::expect_message}; +use arbiter_proto::{ + format_challenge, + transport::{Bi, expect_message}, +}; use diesel::{ ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, dsl::insert_into, update, }; use diesel_async::RunQueryDsl as _; -use ed25519_dalek::VerifyingKey; +use ed25519_dalek::{Signature, VerifyingKey}; use kameo::error::SendError; use tracing::error; use crate::{ actors::{ - client::{ClientConnection, ConnectErrorCode, Request, Response}, + client::ClientConnection, router::{self, RequestClientApproval}, }, db::{self, schema::program_client}, }; -use super::session::ClientSession; - #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] pub enum Error { - #[error("Unexpected message payload")] - UnexpectedMessagePayload, - #[error("Invalid client public key length")] - InvalidClientPubkeyLength, - #[error("Invalid client public key encoding")] - InvalidAuthPubkeyEncoding, #[error("Database pool unavailable")] DatabasePoolUnavailable, #[error("Database operation failed")] @@ -33,8 +28,6 @@ pub enum Error { InvalidChallengeSolution, #[error("Client approval request failed")] ApproveError(#[from] ApproveError), - #[error("Internal error")] - InternalError, #[error("Transport error")] Transport, } @@ -49,6 +42,18 @@ pub enum ApproveError { Upstream(router::ApprovalError), } +#[derive(Debug, Clone)] +pub enum Inbound { + AuthChallengeRequest { pubkey: VerifyingKey }, + AuthChallengeSolution { signature: Signature }, +} + +#[derive(Debug, Clone)] +pub enum Outbound { + AuthChallenge { pubkey: VerifyingKey, nonce: i32 }, + AuthSuccess, +} + /// Atomically reads and increments the nonce for a known client. /// Returns `None` if the pubkey is not registered. async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result, Error> { @@ -141,27 +146,24 @@ async fn insert_client(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<( Ok(()) } -async fn challenge_client( - props: &mut ClientConnection, +async fn challenge_client( + transport: &mut T, pubkey: VerifyingKey, nonce: i32, -) -> Result<(), Error> { - let challenge_pubkey = pubkey.as_bytes().to_vec(); - - props - .transport - .send(Ok(Response::AuthChallenge { - pubkey: challenge_pubkey.clone(), - nonce, - })) +) -> Result<(), Error> +where + T: Bi> + ?Sized, +{ + transport + .send(Ok(Outbound::AuthChallenge { pubkey, nonce })) .await .map_err(|e| { error!(error = ?e, "Failed to send auth challenge"); Error::Transport })?; - let signature = expect_message(&mut *props.transport, |req: Request| match req { - Request::AuthChallengeSolution { signature } => Some(signature), + let signature = expect_message(transport, |req: Inbound| match req { + Inbound::AuthChallengeSolution { signature } => Some(signature), _ => None, }) .await @@ -170,13 +172,9 @@ async fn challenge_client( Error::Transport })?; - let formatted = format_challenge(nonce, &challenge_pubkey); - let sig = signature.as_slice().try_into().map_err(|_| { - error!("Invalid signature length"); - Error::InvalidChallengeSolution - })?; + let formatted = format_challenge(nonce, pubkey.as_bytes()); - pubkey.verify_strict(&formatted, &sig).map_err(|_| { + pubkey.verify_strict(&formatted, &signature).map_err(|_| { error!("Challenge solution verification failed"); Error::InvalidChallengeSolution })?; @@ -184,30 +182,17 @@ async fn challenge_client( Ok(()) } -fn connect_error_code(err: &Error) -> ConnectErrorCode { - match err { - Error::ApproveError(ApproveError::Denied) => ConnectErrorCode::ApprovalDenied, - Error::ApproveError(ApproveError::Upstream( - router::ApprovalError::NoUserAgentsConnected, - )) => ConnectErrorCode::NoUserAgentsOnline, - _ => ConnectErrorCode::Unknown, - } -} - -async fn authenticate(props: &mut ClientConnection) -> Result { - let Some(Request::AuthChallengeRequest { - pubkey: challenge_pubkey, - }) = props.transport.recv().await - else { +pub async fn authenticate( + props: &mut ClientConnection, + transport: &mut T, +) -> Result +where + T: Bi> + Send + ?Sized, +{ + let Some(Inbound::AuthChallengeRequest { pubkey }) = transport.recv().await else { return Err(Error::Transport); }; - let pubkey_bytes = challenge_pubkey - .as_array() - .ok_or(Error::InvalidClientPubkeyLength)?; - let pubkey = - VerifyingKey::from_bytes(pubkey_bytes).map_err(|_| Error::InvalidAuthPubkeyEncoding)?; - let nonce = match get_nonce(&props.db, &pubkey).await? { Some(nonce) => nonce, None => { @@ -217,21 +202,14 @@ async fn authenticate(props: &mut ClientConnection) -> Result Result { - match authenticate(&mut props).await { - Ok(_pubkey) => Ok(ClientSession::new(props)), - Err(err) => { - let code = connect_error_code(&err); - let _ = props - .transport - .send(Ok(Response::ClientConnectError { code })) - .await; - Err(err) - } - } -} diff --git a/server/crates/arbiter-server/src/actors/client/mod.rs b/server/crates/arbiter-server/src/actors/client/mod.rs index 55c0ed7..3fae866 100644 --- a/server/crates/arbiter-server/src/actors/client/mod.rs +++ b/server/crates/arbiter-server/src/actors/client/mod.rs @@ -7,68 +7,31 @@ use crate::{ db, }; -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] -pub enum ClientError { - #[error("Expected message with payload")] - MissingRequestPayload, - #[error("Unexpected request payload")] - UnexpectedRequestPayload, - #[error("State machine error")] - StateTransitionFailed, - #[error("Connection registration failed")] - ConnectionRegistrationFailed, - #[error(transparent)] - Auth(#[from] auth::Error), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ConnectErrorCode { - Unknown, - ApprovalDenied, - NoUserAgentsOnline, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Request { - AuthChallengeRequest { pubkey: Vec }, - AuthChallengeSolution { signature: Vec }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Response { - AuthChallenge { pubkey: Vec, nonce: i32 }, - AuthOk, - ClientConnectError { code: ConnectErrorCode }, -} - -pub type Transport = Box> + Send>; - pub struct ClientConnection { pub(crate) db: db::DatabasePool, - pub(crate) transport: Transport, pub(crate) actors: GlobalActors, } impl ClientConnection { - pub fn new(db: db::DatabasePool, transport: Transport, actors: GlobalActors) -> Self { - Self { - db, - transport, - actors, - } + pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self { + Self { db, actors } } } pub mod auth; pub mod session; -pub async fn connect_client(props: ClientConnection) { - match auth::authenticate_and_create(props).await { - Ok(session) => { - ClientSession::spawn(session); +pub async fn connect_client(mut props: ClientConnection, transport: &mut T) +where + T: Bi> + Send + ?Sized, +{ + match auth::authenticate(&mut props, transport).await { + Ok(_pubkey) => { + ClientSession::spawn(ClientSession::new(props)); info!("Client authenticated, session started"); } Err(err) => { + let _ = transport.send(Err(err.clone())).await; error!(?err, "Authentication failed, closing connection"); } } diff --git a/server/crates/arbiter-server/src/actors/client/session.rs b/server/crates/arbiter-server/src/actors/client/session.rs index fb18feb..93f2c6e 100644 --- a/server/crates/arbiter-server/src/actors/client/session.rs +++ b/server/crates/arbiter-server/src/actors/client/session.rs @@ -1,12 +1,9 @@ -use kameo::Actor; -use tokio::select; -use tracing::{error, info}; +use kameo::{Actor, messages}; +use tracing::error; use crate::{ actors::{ - GlobalActors, - client::{ClientConnection, ClientError, Request, Response}, - router::RegisterClient, + GlobalActors, client::ClientConnection, keyholder::KeyHolderState, router::RegisterClient, }, db, }; @@ -19,19 +16,30 @@ impl ClientSession { pub(crate) fn new(props: ClientConnection) -> Self { Self { props } } - - pub async fn process_transport_inbound(&mut self, req: Request) -> Output { - let _ = req; - Err(ClientError::UnexpectedRequestPayload) - } } -type Output = Result; +#[messages] +impl ClientSession { + #[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 = "client", "keyholder.query.failed"); + return Err(Error::Internal); + } + }; + + Ok(vault_state) + } +} impl Actor for ClientSession { type Args = Self; - type Error = ClientError; + type Error = Error; async fn on_start( args: Self::Args, @@ -42,52 +50,22 @@ impl Actor for ClientSession { .router .ask(RegisterClient { actor: this }) .await - .map_err(|_| ClientError::ConnectionRegistrationFailed)?; + .map_err(|_| Error::ConnectionRegistrationFailed)?; Ok(args) } - - async fn next( - &mut self, - _actor_ref: kameo::prelude::WeakActorRef, - mailbox_rx: &mut kameo::prelude::MailboxReceiver, - ) -> Option> { - loop { - select! { - signal = mailbox_rx.recv() => { - return signal; - } - msg = self.props.transport.recv() => { - match msg { - Some(request) => { - match self.process_transport_inbound(request).await { - Ok(resp) => { - if self.props.transport.send(Ok(resp)).await.is_err() { - error!(actor = "client", reason = "channel closed", "send.failed"); - return Some(kameo::mailbox::Signal::Stop); - } - } - Err(err) => { - let _ = self.props.transport.send(Err(err)).await; - return Some(kameo::mailbox::Signal::Stop); - } - } - } - None => { - info!(actor = "client", "transport.closed"); - return Some(kameo::mailbox::Signal::Stop); - } - } - } - } - } - } } impl ClientSession { pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self { - use arbiter_proto::transport::DummyTransport; - let transport: super::Transport = Box::new(DummyTransport::new()); - let props = ClientConnection::new(db, transport, actors); + let props = ClientConnection::new(db, actors); Self { props } } } + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Connection registration failed")] + ConnectionRegistrationFailed, + #[error("Internal error")] + Internal, +} 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 index 7a5991d..2fdd048 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs @@ -106,7 +106,7 @@ pub struct AuthContext<'a, T> { } impl<'a, T> AuthContext<'a, T> { - pub fn new(conn: &'a mut UserAgentConnection, transport: T) -> Self { + pub fn new(conn: &'a mut UserAgentConnection, transport: T) -> Self { Self { conn, transport } } } @@ -124,8 +124,7 @@ where let stored_bytes = pubkey.to_stored_bytes(); let nonce = create_nonce(&self.conn.db, &stored_bytes).await?; - self - .transport + self.transport .send(Ok(Outbound::AuthChallenge { nonce })) .await .map_err(|e| { @@ -165,8 +164,7 @@ where register_key(&self.conn.db, &pubkey).await?; - self - .transport + self.transport .send(Ok(Outbound::AuthSuccess)) .await .map_err(|_| Error::Transport)?; @@ -214,8 +212,7 @@ where }; if valid { - self - .transport + self.transport .send(Ok(Outbound::AuthSuccess)) .await .map_err(|_| Error::Transport)?; diff --git a/server/crates/arbiter-server/src/grpc/client.rs b/server/crates/arbiter-server/src/grpc/client.rs index 3d41785..17442a0 100644 --- a/server/crates/arbiter-server/src/grpc/client.rs +++ b/server/crates/arbiter-server/src/grpc/client.rs @@ -1,142 +1,118 @@ use arbiter_proto::{ proto::client::{ - AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest, - AuthChallengeSolution as ProtoAuthChallengeSolution, AuthOk as ProtoAuthOk, - ClientConnectError, ClientRequest, ClientResponse, - client_connect_error::Code as ProtoClientConnectErrorCode, + ClientRequest, ClientResponse, VaultState as ProtoVaultState, client_request::Payload as ClientRequestPayload, client_response::Payload as ClientResponsePayload, }, - transport::{Bi, Error as TransportError, Sender}, + transport::{Receiver, Sender, grpc::GrpcBi}, }; -use async_trait::async_trait; -use futures::StreamExt as _; -use tokio::sync::mpsc; -use tonic::{Status, Streaming}; +use kameo::{ + actor::{ActorRef, Spawn as _}, + error::SendError, +}; +use tracing::{info, warn}; -use crate::actors::client::{ - self, ClientError, ConnectErrorCode, Request as DomainRequest, Response as DomainResponse, +use crate::{ + actors::{ + client::{ + self, ClientConnection, + session::{ClientSession, Error, HandleQueryVaultState}, + }, + keyholder::KeyHolderState, + }, + utils::defer, }; -pub struct GrpcTransport { - sender: mpsc::Sender>, - receiver: Streaming, -} +mod auth; -impl GrpcTransport { - pub fn new( - sender: mpsc::Sender>, - receiver: Streaming, - ) -> Self { - Self { sender, receiver } - } - - fn request_to_domain(request: ClientRequest) -> Result { - match request.payload { - Some(ClientRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest { - pubkey, - })) => Ok(DomainRequest::AuthChallengeRequest { pubkey }), - Some(ClientRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution { - signature, - })) => Ok(DomainRequest::AuthChallengeSolution { signature }), - None => Err(Status::invalid_argument("Missing client request payload")), - } - } - - fn response_to_proto(response: DomainResponse) -> ClientResponse { - let payload = match response { - DomainResponse::AuthChallenge { pubkey, nonce } => { - ClientResponsePayload::AuthChallenge(ProtoAuthChallenge { pubkey, nonce }) - } - DomainResponse::AuthOk => ClientResponsePayload::AuthOk(ProtoAuthOk {}), - DomainResponse::ClientConnectError { code } => { - ClientResponsePayload::ClientConnectError(ClientConnectError { - code: match code { - ConnectErrorCode::Unknown => ProtoClientConnectErrorCode::Unknown, - ConnectErrorCode::ApprovalDenied => { - ProtoClientConnectErrorCode::ApprovalDenied - } - ConnectErrorCode::NoUserAgentsOnline => { - ProtoClientConnectErrorCode::NoUserAgentsOnline - } - } - .into(), - }) - } +async fn dispatch_loop( + mut bi: GrpcBi, + actor: ActorRef, +) { + loop { + let Some(conn) = bi.recv().await else { + return; }; - ClientResponse { - payload: Some(payload), - } - } - - fn error_to_status(value: ClientError) -> Status { - match value { - ClientError::MissingRequestPayload | ClientError::UnexpectedRequestPayload => { - Status::invalid_argument("Expected message with payload") - } - ClientError::StateTransitionFailed => Status::internal("State machine error"), - ClientError::Auth(ref err) => auth_error_status(err), - ClientError::ConnectionRegistrationFailed => { - Status::internal("Connection registration failed") - } + if dispatch_conn_message(&mut bi, &actor, conn).await.is_err() { + return; } } } -#[async_trait] -impl Sender> for GrpcTransport { - async fn send( - &mut self, - item: Result, - ) -> Result<(), TransportError> { - let outbound = match item { - Ok(message) => Ok(Self::response_to_proto(message)), - Err(err) => Err(Self::error_to_status(err)), - }; +async fn dispatch_conn_message( + bi: &mut GrpcBi, + actor: &ActorRef, + conn: Result, +) -> Result<(), ()> { + let conn = match conn { + Ok(conn) => conn, + Err(err) => { + warn!(error = ?err, "Failed to receive client request"); + return Err(()); + } + }; - self.sender - .send(outbound) - .await - .map_err(|_| TransportError::ChannelClosed) - } -} + let Some(payload) = conn.payload else { + let _ = bi + .send(Err(tonic::Status::invalid_argument( + "Missing client request payload", + ))) + .await; + return Err(()); + }; -#[async_trait] -impl Bi> for GrpcTransport { - async fn recv(&mut self) -> Option { - match self.receiver.next().await { - Some(Ok(item)) => match Self::request_to_domain(item) { - Ok(request) => Some(request), - Err(status) => { - let _ = self.sender.send(Err(status)).await; - None + let payload = match payload { + ClientRequestPayload::QueryVaultState(_) => ClientResponsePayload::VaultState( + match actor.ask(HandleQueryVaultState {}).await { + Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, + Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed, + Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed, + Err(SendError::HandlerError(Error::Internal)) => ProtoVaultState::Error, + Err(err) => { + warn!(error = ?err, "Failed to query vault state"); + ProtoVaultState::Error } - }, - Some(Err(error)) => { - tracing::error!(error = ?error, "grpc client recv failed; closing stream"); - None } - None => None, + .into(), + ), + payload => { + warn!(?payload, "Unsupported post-auth client request"); + let _ = bi + .send(Err(tonic::Status::invalid_argument( + "Unsupported client request", + ))) + .await; + return Err(()); + } + }; + + bi.send(Ok(ClientResponse { + payload: Some(payload), + })) + .await + .map_err(|_| ()) +} + +pub async fn start(conn: ClientConnection, mut bi: GrpcBi) { + let mut conn = conn; + match auth::start(&mut conn, &mut bi).await { + Ok(_) => { + let actor = + client::session::ClientSession::spawn(client::session::ClientSession::new(conn)); + let actor_for_cleanup = actor.clone(); + let _ = defer(move || { + actor_for_cleanup.kill(); + }); + + info!("Client authenticated successfully"); + dispatch_loop(bi, actor).await; + } + Err(e) => { + let mut transport = auth::AuthTransportAdapter(&mut bi); + let _ = transport.send(Err(e.clone())).await; + warn!(error = ?e, "Authentication failed"); + return; } } } - -fn auth_error_status(value: &client::auth::Error) -> Status { - use client::auth::Error; - - match value { - Error::UnexpectedMessagePayload | Error::InvalidClientPubkeyLength => { - Status::invalid_argument(value.to_string()) - } - Error::InvalidAuthPubkeyEncoding => { - Status::invalid_argument("Failed to convert pubkey to VerifyingKey") - } - Error::InvalidChallengeSolution => Status::unauthenticated(value.to_string()), - Error::ApproveError(_) => Status::permission_denied(value.to_string()), - Error::Transport => Status::internal("Transport error"), - Error::DatabasePoolUnavailable => Status::internal("Database pool error"), - Error::DatabaseOperationFailed => Status::internal("Database error"), - Error::InternalError => Status::internal("Internal error"), - } -} diff --git a/server/crates/arbiter-server/src/grpc/client/auth.rs b/server/crates/arbiter-server/src/grpc/client/auth.rs new file mode 100644 index 0000000..8374b36 --- /dev/null +++ b/server/crates/arbiter-server/src/grpc/client/auth.rs @@ -0,0 +1,131 @@ +use arbiter_proto::{ + proto::client::{ + AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest, + AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult, + ClientRequest, ClientResponse, client_request::Payload as ClientRequestPayload, + client_response::Payload as ClientResponsePayload, + }, + transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi}, +}; +use async_trait::async_trait; +use tracing::warn; + +use crate::actors::client::{self, ClientConnection, auth}; + +pub struct AuthTransportAdapter<'a>(pub(super) &'a mut GrpcBi); + +impl AuthTransportAdapter<'_> { + fn response_to_proto(response: auth::Outbound) -> ClientResponse { + let payload = match response { + auth::Outbound::AuthChallenge { pubkey, nonce } => { + ClientResponsePayload::AuthChallenge(ProtoAuthChallenge { + pubkey: pubkey.to_bytes().to_vec(), + nonce, + }) + } + auth::Outbound::AuthSuccess => { + ClientResponsePayload::AuthResult(ProtoAuthResult::Success.into()) + } + }; + + ClientResponse { + payload: Some(payload), + } + } + + fn error_to_proto(error: auth::Error) -> ClientResponse { + ClientResponse { + payload: Some(ClientResponsePayload::AuthResult( + match error { + auth::Error::InvalidChallengeSolution => ProtoAuthResult::InvalidSignature, + auth::Error::ApproveError(auth::ApproveError::Denied) => { + ProtoAuthResult::ApprovalDenied + } + auth::Error::ApproveError(auth::ApproveError::Upstream( + crate::actors::router::ApprovalError::NoUserAgentsConnected, + )) => ProtoAuthResult::NoUserAgentsOnline, + auth::Error::ApproveError(auth::ApproveError::Internal) + | auth::Error::DatabasePoolUnavailable + | auth::Error::DatabaseOperationFailed + | auth::Error::Transport => ProtoAuthResult::Internal, + } + .into(), + )), + } + } + + async fn send_auth_result(&mut self, result: ProtoAuthResult) -> Result<(), TransportError> { + self.0 + .send(Ok(ClientResponse { + payload: Some(ClientResponsePayload::AuthResult(result.into())), + })) + .await + } +} + +#[async_trait] +impl Sender> for AuthTransportAdapter<'_> { + async fn send( + &mut self, + item: Result, + ) -> Result<(), TransportError> { + let outbound = match item { + Ok(message) => Ok(AuthTransportAdapter::response_to_proto(message)), + Err(err) => Ok(AuthTransportAdapter::error_to_proto(err)), + }; + + self.0.send(outbound).await + } +} + +#[async_trait] +impl Receiver for AuthTransportAdapter<'_> { + async fn recv(&mut self) -> Option { + let request = match self.0.recv().await? { + Ok(request) => request, + Err(error) => { + warn!(error = ?error, "grpc client recv failed; closing stream"); + return None; + } + }; + + let payload = request.payload?; + + match payload { + ClientRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest { pubkey }) => { + let Ok(pubkey) = <[u8; 32]>::try_from(pubkey) else { + let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await; + return None; + }; + let Ok(pubkey) = ed25519_dalek::VerifyingKey::from_bytes(&pubkey) else { + let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await; + return None; + }; + Some(auth::Inbound::AuthChallengeRequest { pubkey }) + } + ClientRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution { + signature, + }) => { + let Ok(signature) = ed25519_dalek::Signature::try_from(signature.as_slice()) else { + let _ = self + .send_auth_result(ProtoAuthResult::InvalidSignature) + .await; + return None; + }; + Some(auth::Inbound::AuthChallengeSolution { signature }) + } + _ => None, + } + } +} + +impl Bi> for AuthTransportAdapter<'_> {} + +pub async fn start( + conn: &mut ClientConnection, + bi: &mut GrpcBi, +) -> Result<(), auth::Error> { + let mut transport = AuthTransportAdapter(bi); + client::auth::authenticate(conn, &mut transport).await?; + Ok(()) +} diff --git a/server/crates/arbiter-server/src/grpc/mod.rs b/server/crates/arbiter-server/src/grpc/mod.rs index 204d6b1..7bdedea 100644 --- a/server/crates/arbiter-server/src/grpc/mod.rs +++ b/server/crates/arbiter-server/src/grpc/mod.rs @@ -12,10 +12,7 @@ use tracing::info; use crate::{ DEFAULT_CHANNEL_SIZE, - actors::{ - client::{ClientConnection, connect_client}, - user_agent::UserAgentConnection, - }, + actors::{client::ClientConnection, user_agent::UserAgentConnection}, grpc::{self, user_agent::start}, }; @@ -33,19 +30,13 @@ impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Ser request: Request>, ) -> Result, Status> { let req_stream = request.into_inner(); - let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE); - - let transport = client::GrpcTransport::new(tx, req_stream); - let props = ClientConnection::new( - self.context.db.clone(), - Box::new(transport), - self.context.actors.clone(), - ); - tokio::spawn(connect_client(props)); + let (bi, rx) = GrpcBi::from_bi_stream(req_stream); + let props = ClientConnection::new(self.context.db.clone(), self.context.actors.clone()); + tokio::spawn(client::start(props, bi)); info!(event = "connection established", "grpc.client"); - Ok(Response::new(ReceiverStream::new(rx))) + Ok(Response::new(rx)) } #[tracing::instrument(level = "debug", skip(self))] diff --git a/server/crates/arbiter-server/src/grpc/user_agent.rs b/server/crates/arbiter-server/src/grpc/user_agent.rs index 4df6317..fe587dc 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent.rs @@ -30,7 +30,10 @@ use arbiter_proto::{ }; use async_trait::async_trait; use chrono::{TimeZone, Utc}; -use kameo::{actor::{ActorRef, Spawn as _}, error::SendError}; +use kameo::{ + actor::{ActorRef, Spawn as _}, + error::SendError, +}; use tonic::Status; use tracing::{info, warn}; @@ -40,7 +43,9 @@ use crate::{ user_agent::{ OutOfBand, UserAgentConnection, UserAgentSession, session::{ - BootstrapError, Error, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleGrantList, HandleQueryVaultState, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError + BootstrapError, Error, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, + HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleGrantList, + HandleQueryVaultState, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError, }, }, }, @@ -109,7 +114,11 @@ async fn dispatch_conn_message( }; let Some(payload) = conn.payload else { - let _ = bi.send(Err(Status::invalid_argument("Missing user-agent request payload"))).await; + let _ = bi + .send(Err(Status::invalid_argument( + "Missing user-agent request payload", + ))) + .await; return Err(()); }; @@ -118,7 +127,9 @@ async fn dispatch_conn_message( let client_pubkey = match <[u8; 32]>::try_from(client_pubkey) { Ok(bytes) => x25519_dalek::PublicKey::from(bytes), Err(_) => { - let _ = bi.send(Err(Status::invalid_argument("Invalid X25519 public key"))).await; + let _ = bi + .send(Err(Status::invalid_argument("Invalid X25519 public key"))) + .await; return Err(()); } }; @@ -131,7 +142,9 @@ async fn dispatch_conn_message( ), Err(err) => { warn!(error = ?err, "Failed to handle unseal start request"); - let _ = bi.send(Err(Status::internal("Failed to start unseal flow"))).await; + let _ = bi + .send(Err(Status::internal("Failed to start unseal flow"))) + .await; return Err(()); } } @@ -155,7 +168,9 @@ async fn dispatch_conn_message( } Err(err) => { warn!(error = ?err, "Failed to handle unseal request"); - let _ = bi.send(Err(Status::internal("Failed to unseal vault"))).await; + let _ = bi + .send(Err(Status::internal("Failed to unseal vault"))) + .await; return Err(()); } } @@ -178,12 +193,14 @@ async fn dispatch_conn_message( Err(SendError::HandlerError(BootstrapError::InvalidKey)) => { ProtoBootstrapResult::InvalidKey } - Err(SendError::HandlerError( - BootstrapError::AlreadyBootstrapped, - )) => ProtoBootstrapResult::AlreadyBootstrapped, + Err(SendError::HandlerError(BootstrapError::AlreadyBootstrapped)) => { + ProtoBootstrapResult::AlreadyBootstrapped + } Err(err) => { warn!(error = ?err, "Failed to handle bootstrap request"); - let _ = bi.send(Err(Status::internal("Failed to bootstrap vault"))).await; + let _ = bi + .send(Err(Status::internal("Failed to bootstrap vault"))) + .await; return Err(()); } } @@ -224,12 +241,13 @@ async fn dispatch_conn_message( }; UserAgentResponsePayload::EvmGrantCreate(EvmGrantOrWallet::grant_create_response( - actor.ask(HandleGrantCreate { - client_id, - basic, - grant, - }) - .await, + actor + .ask(HandleGrantCreate { + client_id, + basic, + grant, + }) + .await, )) } UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id }) => { @@ -239,7 +257,11 @@ async fn dispatch_conn_message( } payload => { warn!(?payload, "Unsupported post-auth user agent request"); - let _ = bi.send(Err(Status::invalid_argument("Unsupported user-agent request"))).await; + let _ = bi + .send(Err(Status::invalid_argument( + "Unsupported user-agent request", + ))) + .await; return Err(()); } }; @@ -281,7 +303,10 @@ fn parse_grant_request( let specific = specific.ok_or_else(|| Status::invalid_argument("Missing specific grant settings"))?; - Ok((shared_settings_from_proto(shared)?, specific_grant_from_proto(specific)?)) + Ok(( + shared_settings_from_proto(shared)?, + specific_grant_from_proto(specific)?, + )) } fn shared_settings_from_proto(shared: ProtoSharedSettings) -> Result { @@ -289,14 +314,8 @@ fn shared_settings_from_proto(shared: ProtoSharedSettings) -> Result Result Result>()?, - limit: volume_rate_limit_from_proto( - limit.ok_or_else(|| { - Status::invalid_argument("Missing ether transfer volume rate limit") - })?, - )?, + limit: volume_rate_limit_from_proto(limit.ok_or_else(|| { + Status::invalid_argument("Missing ether transfer volume rate limit") + })?)?, })), Some(ProtoSpecificGrantType::TokenTransfer(ProtoTokenTransferSettings { token_contract, @@ -391,12 +406,12 @@ fn shared_settings_to_proto(shared: SharedGrantSettings) -> ProtoSharedSettings seconds: time.timestamp(), nanos: time.timestamp_subsec_nanos() as i32, }), - max_gas_fee_per_gas: shared.max_gas_fee_per_gas.map(|value| { - value.to_be_bytes::<32>().to_vec() - }), - max_priority_fee_per_gas: shared.max_priority_fee_per_gas.map(|value| { - value.to_be_bytes::<32>().to_vec() - }), + max_gas_fee_per_gas: shared + .max_gas_fee_per_gas + .map(|value| value.to_be_bytes::<32>().to_vec()), + max_priority_fee_per_gas: shared + .max_priority_fee_per_gas + .map(|value| value.to_be_bytes::<32>().to_vec()), rate_limit: shared.rate_limit.map(|limit| ProtoTransactionRateLimit { count: limit.count, window_secs: limit.window.num_seconds(), @@ -408,7 +423,11 @@ fn specific_grant_to_proto(grant: SpecificGrant) -> ProtoSpecificGrant { let grant = match grant { SpecificGrant::EtherTransfer(settings) => { ProtoSpecificGrantType::EtherTransfer(ProtoEtherTransferSettings { - targets: settings.target.into_iter().map(|address| address.to_vec()).collect(), + targets: settings + .target + .into_iter() + .map(|address| address.to_vec()) + .collect(), limit: Some(ProtoVolumeRateLimit { max_volume: settings.limit.max_volume.to_be_bytes::<32>().to_vec(), window_secs: settings.limit.window.num_seconds(), @@ -450,7 +469,9 @@ impl EvmGrantOrWallet { } }; - WalletCreateResponse { result: Some(result) } + WalletCreateResponse { + result: Some(result), + } } fn wallet_list_response( @@ -471,7 +492,9 @@ impl EvmGrantOrWallet { } }; - WalletListResponse { result: Some(result) } + WalletListResponse { + result: Some(result), + } } fn grant_create_response( @@ -485,12 +508,12 @@ impl EvmGrantOrWallet { } }; - EvmGrantCreateResponse { result: Some(result) } + EvmGrantCreateResponse { + result: Some(result), + } } - fn grant_delete_response( - result: Result<(), SendError>, - ) -> EvmGrantDeleteResponse { + fn grant_delete_response(result: Result<(), SendError>) -> EvmGrantDeleteResponse { let result = match result { Ok(()) => EvmGrantDeleteResult::Ok(()), Err(err) => { @@ -499,7 +522,9 @@ impl EvmGrantOrWallet { } }; - EvmGrantDeleteResponse { result: Some(result) } + EvmGrantDeleteResponse { + result: Some(result), + } } fn grant_list_response( @@ -523,7 +548,9 @@ impl EvmGrantOrWallet { } }; - EvmGrantListResponse { result: Some(result) } + EvmGrantListResponse { + result: Some(result), + } } } diff --git a/server/crates/arbiter-server/src/lib.rs b/server/crates/arbiter-server/src/lib.rs index 0b255e5..6367164 100644 --- a/server/crates/arbiter-server/src/lib.rs +++ b/server/crates/arbiter-server/src/lib.rs @@ -1,9 +1,5 @@ #![forbid(unsafe_code)] -#![deny( - clippy::unwrap_used, - clippy::expect_used, - clippy::panic -)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use crate::context::ServerContext; @@ -26,4 +22,3 @@ impl Server { Self { context } } } - diff --git a/server/crates/arbiter-server/tests/client/auth.rs b/server/crates/arbiter-server/tests/client/auth.rs index ca4f38b..ca1d0d0 100644 --- a/server/crates/arbiter-server/tests/client/auth.rs +++ b/server/crates/arbiter-server/tests/client/auth.rs @@ -1,7 +1,7 @@ -use arbiter_proto::transport::Bi; +use arbiter_proto::transport::{Receiver, Sender}; use arbiter_server::actors::GlobalActors; use arbiter_server::{ - actors::client::{ClientConnection, Request, Response, connect_client}, + actors::client::{ClientConnection, auth, connect_client}, db::{self, schema}, }; use diesel::{ExpressionMethods as _, insert_into}; @@ -17,15 +17,17 @@ pub async fn test_unregistered_pubkey_rejected() { let (server_transport, mut test_transport) = ChannelTransport::new(); let actors = GlobalActors::spawn(db.clone()).await.unwrap(); - let props = ClientConnection::new(db.clone(), Box::new(server_transport), actors); - let task = tokio::spawn(connect_client(props)); + let props = ClientConnection::new(db.clone(), actors); + let task = tokio::spawn(async move { + let mut server_transport = server_transport; + connect_client(props, &mut server_transport).await; + }); let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); - let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); test_transport - .send(Request::AuthChallengeRequest { - pubkey: pubkey_bytes, + .send(auth::Inbound::AuthChallengeRequest { + pubkey: new_key.verifying_key(), }) .await .unwrap(); @@ -54,13 +56,16 @@ pub async fn test_challenge_auth() { let (server_transport, mut test_transport) = ChannelTransport::new(); let actors = GlobalActors::spawn(db.clone()).await.unwrap(); - let props = ClientConnection::new(db.clone(), Box::new(server_transport), actors); - let task = tokio::spawn(connect_client(props)); + let props = ClientConnection::new(db.clone(), actors); + let task = tokio::spawn(async move { + let mut server_transport = server_transport; + connect_client(props, &mut server_transport).await; + }); // Send challenge request test_transport - .send(Request::AuthChallengeRequest { - pubkey: pubkey_bytes, + .send(auth::Inbound::AuthChallengeRequest { + pubkey: new_key.verifying_key(), }) .await .unwrap(); @@ -72,23 +77,31 @@ pub async fn test_challenge_auth() { .expect("should receive challenge"); let challenge = match response { Ok(resp) => match resp { - Response::AuthChallenge { pubkey, nonce } => (pubkey, nonce), + auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce), other => panic!("Expected AuthChallenge, got {other:?}"), }, Err(err) => panic!("Expected Ok response, got Err({err:?})"), }; // Sign the challenge and send solution - let formatted_challenge = arbiter_proto::format_challenge(challenge.1, &challenge.0); + let formatted_challenge = arbiter_proto::format_challenge(challenge.1, challenge.0.as_bytes()); let signature = new_key.sign(&formatted_challenge); test_transport - .send(Request::AuthChallengeSolution { - signature: signature.to_bytes().to_vec(), - }) + .send(auth::Inbound::AuthChallengeSolution { signature }) .await .unwrap(); + let response = test_transport + .recv() + .await + .expect("should receive auth success"); + match response { + Ok(auth::Outbound::AuthSuccess) => {} + Ok(other) => panic!("Expected AuthSuccess, got {other:?}"), + Err(err) => panic!("Expected Ok response, got Err({err:?})"), + } + // Auth completes, session spawned task.await.unwrap(); } diff --git a/server/crates/arbiter-server/tests/common/mod.rs b/server/crates/arbiter-server/tests/common/mod.rs index 3bc3430..13ccd32 100644 --- a/server/crates/arbiter-server/tests/common/mod.rs +++ b/server/crates/arbiter-server/tests/common/mod.rs @@ -1,7 +1,8 @@ -use arbiter_proto::transport::{Bi, Error}; +use arbiter_proto::transport::{Bi, Error, Receiver, Sender}; use arbiter_server::{ actors::keyholder::KeyHolder, - db::{self, schema}, safe_cell::{SafeCell, SafeCellHandle as _}, + db::{self, schema}, + safe_cell::{SafeCell, SafeCellHandle as _}, }; use async_trait::async_trait; use diesel::QueryDsl; @@ -54,10 +55,10 @@ impl ChannelTransport { } #[async_trait] -impl Bi for ChannelTransport +impl Sender for ChannelTransport where - T: Send + 'static, - Y: Send + 'static, + T: Send + Sync + 'static, + Y: Send + Sync + 'static, { async fn send(&mut self, item: Y) -> Result<(), Error> { self.sender @@ -65,8 +66,22 @@ where .await .map_err(|_| Error::ChannelClosed) } +} +#[async_trait] +impl Receiver for ChannelTransport +where + T: Send + Sync + 'static, + Y: Send + Sync + 'static, +{ async fn recv(&mut self) -> Option { self.receiver.recv().await } } + +impl Bi for ChannelTransport +where + T: Send + Sync + 'static, + Y: Send + Sync + 'static, +{ +} diff --git a/server/crates/arbiter-server/tests/user_agent/auth.rs b/server/crates/arbiter-server/tests/user_agent/auth.rs index 1a7bbad..bfe308a 100644 --- a/server/crates/arbiter-server/tests/user_agent/auth.rs +++ b/server/crates/arbiter-server/tests/user_agent/auth.rs @@ -3,7 +3,7 @@ use arbiter_server::{ actors::{ GlobalActors, bootstrap::GetToken, - user_agent::{AuthPublicKey, Request, OutOfBand, UserAgentConnection, connect_user_agent}, + user_agent::{AuthPublicKey, OutOfBand, Request, UserAgentConnection, connect_user_agent}, }, db::{self, schema}, }; diff --git a/server/crates/arbiter-server/tests/user_agent/unseal.rs b/server/crates/arbiter-server/tests/user_agent/unseal.rs index 4b6d7a3..0b2eea6 100644 --- a/server/crates/arbiter-server/tests/user_agent/unseal.rs +++ b/server/crates/arbiter-server/tests/user_agent/unseal.rs @@ -2,7 +2,7 @@ use arbiter_server::{ actors::{ GlobalActors, keyholder::{Bootstrap, Seal}, - user_agent::{Request, OutOfBand, UnsealError, session::UserAgentSession}, + user_agent::{OutOfBand, Request, UnsealError, session::UserAgentSession}, }, db, safe_cell::{SafeCell, SafeCellHandle as _}, -- 2.49.1 From 58f7b649124a9aa59c7c12c216e4657754c038f3 Mon Sep 17 00:00:00 2001 From: hdbg Date: Wed, 18 Mar 2026 23:37:40 +0100 Subject: [PATCH 3/8] test(user-agent): add test helpers and update actor integration tests --- .../src/actors/user_agent/session.rs | 19 +++- .../actors/user_agent/session/connection.rs | 4 +- .../arbiter-server/tests/user_agent/auth.rs | 64 ++++++++++---- .../arbiter-server/tests/user_agent/unseal.rs | 88 +++++++++---------- 4 files changed, 107 insertions(+), 68 deletions(-) diff --git a/server/crates/arbiter-server/src/actors/user_agent/session.rs b/server/crates/arbiter-server/src/actors/user_agent/session.rs index 382165a..398b09f 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, convert::Infallible}; use arbiter_proto::transport::Sender; +use async_trait::async_trait; use ed25519_dalek::VerifyingKey; use kameo::{Actor, messages, prelude::Context}; use thiserror::Error; @@ -42,8 +43,8 @@ mod connection; pub(crate) use connection::{ BootstrapError, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleGrantList, HandleQueryVaultState, - HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError, }; +pub use connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError}; impl UserAgentSession { pub(crate) fn new(props: UserAgentConnection, sender: Box>) -> Self { @@ -54,6 +55,22 @@ impl UserAgentSession { } } + 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"); 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 index ed9a107..364dbf4 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs @@ -109,7 +109,7 @@ pub enum BootstrapError { #[messages] impl UserAgentSession { #[message] - pub(crate) async fn handle_unseal_request( + pub async fn handle_unseal_request( &mut self, client_pubkey: x25519_dalek::PublicKey, ) -> Result { @@ -127,7 +127,7 @@ impl UserAgentSession { } #[message] - pub(crate) async fn handle_unseal_encrypted_key( + pub async fn handle_unseal_encrypted_key( &mut self, nonce: Vec, ciphertext: Vec, diff --git a/server/crates/arbiter-server/tests/user_agent/auth.rs b/server/crates/arbiter-server/tests/user_agent/auth.rs index bfe308a..285ddcf 100644 --- a/server/crates/arbiter-server/tests/user_agent/auth.rs +++ b/server/crates/arbiter-server/tests/user_agent/auth.rs @@ -1,9 +1,9 @@ -use arbiter_proto::transport::Bi; +use arbiter_proto::transport::{Receiver, Sender}; use arbiter_server::{ actors::{ GlobalActors, bootstrap::GetToken, - user_agent::{AuthPublicKey, OutOfBand, Request, UserAgentConnection, connect_user_agent}, + user_agent::{AuthPublicKey, UserAgentConnection, auth}, }, db::{self, schema}, }; @@ -21,19 +21,31 @@ pub async fn test_bootstrap_token_auth() { let token = actors.bootstrapper.ask(GetToken).await.unwrap().unwrap(); let (server_transport, mut test_transport) = ChannelTransport::new(); - let props = UserAgentConnection::new(db.clone(), actors, Box::new(server_transport)); - let task = tokio::spawn(connect_user_agent(props)); + 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 + }); let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); test_transport - .send(Request::AuthChallengeRequest { + .send(auth::Inbound::AuthChallengeRequest { pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), bootstrap_token: Some(token), }) .await .unwrap(); - task.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:?}"), + } + + task.await.unwrap().unwrap(); let mut conn = db.get().await.unwrap(); let stored_pubkey: Vec = schema::useragent_client::table @@ -51,20 +63,25 @@ pub async fn test_bootstrap_invalid_token_auth() { let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let (server_transport, mut test_transport) = ChannelTransport::new(); - let props = UserAgentConnection::new(db.clone(), actors, Box::new(server_transport)); - let task = tokio::spawn(connect_user_agent(props)); + 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 + }); let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); test_transport - .send(Request::AuthChallengeRequest { + .send(auth::Inbound::AuthChallengeRequest { pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), bootstrap_token: Some("invalid_token".to_string()), }) .await .unwrap(); - // Auth fails, connect_user_agent returns, transport drops - task.await.unwrap(); + assert!(matches!( + task.await.unwrap(), + Err(auth::Error::InvalidBootstrapToken) + )); // Verify no key was registered let mut conn = db.get().await.unwrap(); @@ -99,12 +116,15 @@ pub async fn test_challenge_auth() { } let (server_transport, mut test_transport) = ChannelTransport::new(); - let props = UserAgentConnection::new(db.clone(), actors, Box::new(server_transport)); - let task = tokio::spawn(connect_user_agent(props)); + 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 + }); // Send challenge request test_transport - .send(Request::AuthChallengeRequest { + .send(auth::Inbound::AuthChallengeRequest { pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), bootstrap_token: None, }) @@ -118,7 +138,7 @@ pub async fn test_challenge_auth() { .expect("should receive challenge"); let challenge = match response { Ok(resp) => match resp { - OutOfBand::AuthChallenge { nonce } => nonce, + auth::Outbound::AuthChallenge { nonce } => nonce, other => panic!("Expected AuthChallenge, got {other:?}"), }, Err(err) => panic!("Expected Ok response, got Err({err:?})"), @@ -128,12 +148,20 @@ pub async fn test_challenge_auth() { let signature = new_key.sign(&formatted_challenge); test_transport - .send(Request::AuthChallengeSolution { + .send(auth::Inbound::AuthChallengeSolution { signature: signature.to_bytes().to_vec(), }) .await .unwrap(); - // Auth completes, session spawned - task.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:?}"), + } + + task.await.unwrap().unwrap(); } diff --git a/server/crates/arbiter-server/tests/user_agent/unseal.rs b/server/crates/arbiter-server/tests/user_agent/unseal.rs index 0b2eea6..cd59b01 100644 --- a/server/crates/arbiter-server/tests/user_agent/unseal.rs +++ b/server/crates/arbiter-server/tests/user_agent/unseal.rs @@ -2,15 +2,20 @@ use arbiter_server::{ actors::{ GlobalActors, keyholder::{Bootstrap, Seal}, - user_agent::{OutOfBand, Request, UnsealError, session::UserAgentSession}, + user_agent::session::{ + HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError, UserAgentSession, + }, }, db, safe_cell::{SafeCell, SafeCellHandle as _}, }; use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; +use kameo::actor::Spawn as _; use x25519_dalek::{EphemeralSecret, PublicKey}; -async fn setup_sealed_user_agent(seal_key: &[u8]) -> (db::DatabasePool, UserAgentSession) { +async fn setup_sealed_user_agent( + seal_key: &[u8], +) -> (db::DatabasePool, kameo::actor::ActorRef) { let db = db::create_test_pool().await; let actors = GlobalActors::spawn(db.clone()).await.unwrap(); @@ -23,26 +28,26 @@ async fn setup_sealed_user_agent(seal_key: &[u8]) -> (db::DatabasePool, UserAgen .unwrap(); actors.key_holder.ask(Seal).await.unwrap(); - let session = UserAgentSession::new_test(db.clone(), actors); + let session = UserAgentSession::spawn(UserAgentSession::new_test(db.clone(), actors)); (db, session) } -async fn client_dh_encrypt(user_agent: &mut UserAgentSession, key_to_send: &[u8]) -> Request { +async fn client_dh_encrypt( + user_agent: &kameo::actor::ActorRef, + key_to_send: &[u8], +) -> HandleUnsealEncryptedKey { let client_secret = EphemeralSecret::random(); let client_public = PublicKey::from(&client_secret); let response = user_agent - .process_transport_inbound(Request::UnsealStart { + .ask(HandleUnsealRequest { client_pubkey: client_public, }) .await .unwrap(); - let server_pubkey = match response { - OutOfBand::UnsealStartResponse { server_pubkey } => server_pubkey, - other => panic!("Expected UnsealStartResponse, got {other:?}"), - }; + let server_pubkey = response.server_pubkey; let shared_secret = client_secret.diffie_hellman(&server_pubkey); let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into()); @@ -53,7 +58,7 @@ async fn client_dh_encrypt(user_agent: &mut UserAgentSession, key_to_send: &[u8] .encrypt_in_place(&nonce, associated_data, &mut ciphertext) .unwrap(); - Request::UnsealEncryptedKey { + HandleUnsealEncryptedKey { nonce: nonce.to_vec(), ciphertext, associated_data: associated_data.to_vec(), @@ -64,63 +69,58 @@ async fn client_dh_encrypt(user_agent: &mut UserAgentSession, key_to_send: &[u8] #[test_log::test] pub async fn test_unseal_success() { let seal_key = b"test-seal-key"; - let (_db, mut user_agent) = setup_sealed_user_agent(seal_key).await; + let (_db, user_agent) = setup_sealed_user_agent(seal_key).await; - let encrypted_key = client_dh_encrypt(&mut user_agent, seal_key).await; + let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await; - let response = user_agent - .process_transport_inbound(encrypted_key) - .await - .unwrap(); - - assert!(matches!(response, OutOfBand::UnsealResult(Ok(())))); + let response = user_agent.ask(encrypted_key).await; + assert!(matches!(response, Ok(()))); } #[tokio::test] #[test_log::test] pub async fn test_unseal_wrong_seal_key() { - let (_db, mut user_agent) = setup_sealed_user_agent(b"correct-key").await; + let (_db, user_agent) = setup_sealed_user_agent(b"correct-key").await; - let encrypted_key = client_dh_encrypt(&mut user_agent, b"wrong-key").await; - - let response = user_agent - .process_transport_inbound(encrypted_key) - .await - .unwrap(); + let encrypted_key = client_dh_encrypt(&user_agent, b"wrong-key").await; + let response = user_agent.ask(encrypted_key).await; assert!(matches!( response, - OutOfBand::UnsealResult(Err(UnsealError::InvalidKey)) + Err(kameo::error::SendError::HandlerError( + UnsealError::InvalidKey + )) )); } #[tokio::test] #[test_log::test] pub async fn test_unseal_corrupted_ciphertext() { - let (_db, mut user_agent) = setup_sealed_user_agent(b"test-key").await; + let (_db, user_agent) = setup_sealed_user_agent(b"test-key").await; let client_secret = EphemeralSecret::random(); let client_public = PublicKey::from(&client_secret); user_agent - .process_transport_inbound(Request::UnsealStart { + .ask(HandleUnsealRequest { client_pubkey: client_public, }) .await .unwrap(); let response = user_agent - .process_transport_inbound(Request::UnsealEncryptedKey { + .ask(HandleUnsealEncryptedKey { nonce: vec![0u8; 24], ciphertext: vec![0u8; 32], associated_data: vec![], }) - .await - .unwrap(); + .await; assert!(matches!( response, - OutOfBand::UnsealResult(Err(UnsealError::InvalidKey)) + Err(kameo::error::SendError::HandlerError( + UnsealError::InvalidKey + )) )); } @@ -128,30 +128,24 @@ pub async fn test_unseal_corrupted_ciphertext() { #[test_log::test] pub async fn test_unseal_retry_after_invalid_key() { let seal_key = b"real-seal-key"; - let (_db, mut user_agent) = setup_sealed_user_agent(seal_key).await; + let (_db, user_agent) = setup_sealed_user_agent(seal_key).await; { - let encrypted_key = client_dh_encrypt(&mut user_agent, b"wrong-key").await; - - let response = user_agent - .process_transport_inbound(encrypted_key) - .await - .unwrap(); + let encrypted_key = client_dh_encrypt(&user_agent, b"wrong-key").await; + let response = user_agent.ask(encrypted_key).await; assert!(matches!( response, - OutOfBand::UnsealResult(Err(UnsealError::InvalidKey)) + Err(kameo::error::SendError::HandlerError( + UnsealError::InvalidKey + )) )); } { - let encrypted_key = client_dh_encrypt(&mut user_agent, seal_key).await; + let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await; - let response = user_agent - .process_transport_inbound(encrypted_key) - .await - .unwrap(); - - assert!(matches!(response, OutOfBand::UnsealResult(Ok(())))); + let response = user_agent.ask(encrypted_key).await; + assert!(matches!(response, Ok(()))); } } -- 2.49.1 From 78ad31dc40501652430ee78f5a891181ea0513e9 Mon Sep 17 00:00:00 2001 From: hdbg Date: Wed, 18 Mar 2026 23:43:44 +0100 Subject: [PATCH 4/8] feat(proto): request / response pair tracking by assigning id --- protobufs/client.proto | 2 + protobufs/user_agent.proto | 2 + .../crates/arbiter-server/src/grpc/client.rs | 38 +++- .../arbiter-server/src/grpc/client/auth.rs | 118 +++++++++---- server/crates/arbiter-server/src/grpc/mod.rs | 1 + .../src/grpc/request_tracker.rs | 20 +++ .../arbiter-server/src/grpc/user_agent.rs | 33 +++- .../src/grpc/user_agent/auth.rs | 163 +++++++++++------- 8 files changed, 259 insertions(+), 118 deletions(-) create mode 100644 server/crates/arbiter-server/src/grpc/request_tracker.rs diff --git a/protobufs/client.proto b/protobufs/client.proto index dbe9708..c090a0d 100644 --- a/protobufs/client.proto +++ b/protobufs/client.proto @@ -37,6 +37,7 @@ enum VaultState { } message ClientRequest { + int32 request_id = 4; oneof payload { AuthChallengeRequest auth_challenge_request = 1; AuthChallengeSolution auth_challenge_solution = 2; @@ -45,6 +46,7 @@ message ClientRequest { } message ClientResponse { + optional int32 request_id = 7; oneof payload { AuthChallenge auth_challenge = 1; AuthResult auth_result = 2; diff --git a/protobufs/user_agent.proto b/protobufs/user_agent.proto index 6fb77e4..f54f05a 100644 --- a/protobufs/user_agent.proto +++ b/protobufs/user_agent.proto @@ -89,6 +89,7 @@ message ClientConnectionResponse { message ClientConnectionCancel {} message UserAgentRequest { + int32 id = 14; oneof payload { AuthChallengeRequest auth_challenge_request = 1; AuthChallengeSolution auth_challenge_solution = 2; @@ -105,6 +106,7 @@ message UserAgentRequest { } } message UserAgentResponse { + optional int32 id = 14; oneof payload { AuthChallenge auth_challenge = 1; AuthResult auth_result = 2; diff --git a/server/crates/arbiter-server/src/grpc/client.rs b/server/crates/arbiter-server/src/grpc/client.rs index 17442a0..653c7a8 100644 --- a/server/crates/arbiter-server/src/grpc/client.rs +++ b/server/crates/arbiter-server/src/grpc/client.rs @@ -10,6 +10,7 @@ use kameo::{ actor::{ActorRef, Spawn as _}, error::SendError, }; +use tonic::Status; use tracing::{info, warn}; use crate::{ @@ -20,6 +21,7 @@ use crate::{ }, keyholder::KeyHolderState, }, + grpc::request_tracker::RequestTracker, utils::defer, }; @@ -28,13 +30,17 @@ mod auth; async fn dispatch_loop( mut bi: GrpcBi, actor: ActorRef, + mut request_tracker: RequestTracker, ) { loop { let Some(conn) = bi.recv().await else { return; }; - if dispatch_conn_message(&mut bi, &actor, conn).await.is_err() { + if dispatch_conn_message(&mut bi, &actor, &mut request_tracker, conn) + .await + .is_err() + { return; } } @@ -43,7 +49,8 @@ async fn dispatch_loop( async fn dispatch_conn_message( bi: &mut GrpcBi, actor: &ActorRef, - conn: Result, + request_tracker: &mut RequestTracker, + conn: Result, ) -> Result<(), ()> { let conn = match conn { Ok(conn) => conn, @@ -53,9 +60,16 @@ async fn dispatch_conn_message( } }; + let request_id = match request_tracker.request(conn.request_id) { + Ok(request_id) => request_id, + Err(err) => { + let _ = bi.send(Err(err)).await; + return Err(()); + } + }; let Some(payload) = conn.payload else { let _ = bi - .send(Err(tonic::Status::invalid_argument( + .send(Err(Status::invalid_argument( "Missing client request payload", ))) .await; @@ -79,15 +93,14 @@ async fn dispatch_conn_message( payload => { warn!(?payload, "Unsupported post-auth client request"); let _ = bi - .send(Err(tonic::Status::invalid_argument( - "Unsupported client request", - ))) + .send(Err(Status::invalid_argument("Unsupported client request"))) .await; return Err(()); } }; bi.send(Ok(ClientResponse { + request_id: Some(request_id), payload: Some(payload), })) .await @@ -96,7 +109,10 @@ async fn dispatch_conn_message( pub async fn start(conn: ClientConnection, mut bi: GrpcBi) { let mut conn = conn; - match auth::start(&mut conn, &mut bi).await { + let mut request_tracker = RequestTracker::default(); + let mut response_id = None; + + match auth::start(&mut conn, &mut bi, &mut request_tracker, &mut response_id).await { Ok(_) => { let actor = client::session::ClientSession::spawn(client::session::ClientSession::new(conn)); @@ -106,10 +122,14 @@ pub async fn start(conn: ClientConnection, mut bi: GrpcBi { - let mut transport = auth::AuthTransportAdapter(&mut bi); + let mut transport = auth::AuthTransportAdapter::new( + &mut bi, + &mut request_tracker, + &mut response_id, + ); let _ = transport.send(Err(e.clone())).await; warn!(error = ?e, "Authentication failed"); return; diff --git a/server/crates/arbiter-server/src/grpc/client/auth.rs b/server/crates/arbiter-server/src/grpc/client/auth.rs index 8374b36..49d8d55 100644 --- a/server/crates/arbiter-server/src/grpc/client/auth.rs +++ b/server/crates/arbiter-server/src/grpc/client/auth.rs @@ -8,15 +8,35 @@ 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}; +use crate::{ + actors::client::{self, ClientConnection, auth}, + grpc::request_tracker::RequestTracker, +}; -pub struct AuthTransportAdapter<'a>(pub(super) &'a mut GrpcBi); +pub struct AuthTransportAdapter<'a> { + bi: &'a mut GrpcBi, + request_tracker: &'a mut RequestTracker, + response_id: &'a mut Option, +} -impl AuthTransportAdapter<'_> { - fn response_to_proto(response: auth::Outbound) -> ClientResponse { - let payload = match response { +impl<'a> AuthTransportAdapter<'a> { + pub fn new( + bi: &'a mut GrpcBi, + request_tracker: &'a mut RequestTracker, + response_id: &'a mut Option, + ) -> Self { + Self { + bi, + request_tracker, + response_id, + } + } + + fn response_to_proto(response: auth::Outbound) -> ClientResponsePayload { + match response { auth::Outbound::AuthChallenge { pubkey, nonce } => { ClientResponsePayload::AuthChallenge(ProtoAuthChallenge { pubkey: pubkey.to_bytes().to_vec(), @@ -26,39 +46,44 @@ impl AuthTransportAdapter<'_> { auth::Outbound::AuthSuccess => { ClientResponsePayload::AuthResult(ProtoAuthResult::Success.into()) } - }; - - ClientResponse { - payload: Some(payload), } } - fn error_to_proto(error: auth::Error) -> ClientResponse { - ClientResponse { - payload: Some(ClientResponsePayload::AuthResult( - match error { - auth::Error::InvalidChallengeSolution => ProtoAuthResult::InvalidSignature, - auth::Error::ApproveError(auth::ApproveError::Denied) => { - ProtoAuthResult::ApprovalDenied - } - auth::Error::ApproveError(auth::ApproveError::Upstream( - crate::actors::router::ApprovalError::NoUserAgentsConnected, - )) => ProtoAuthResult::NoUserAgentsOnline, - auth::Error::ApproveError(auth::ApproveError::Internal) - | auth::Error::DatabasePoolUnavailable - | auth::Error::DatabaseOperationFailed - | auth::Error::Transport => ProtoAuthResult::Internal, + fn error_to_proto(error: auth::Error) -> ClientResponsePayload { + ClientResponsePayload::AuthResult( + match error { + auth::Error::InvalidChallengeSolution => ProtoAuthResult::InvalidSignature, + auth::Error::ApproveError(auth::ApproveError::Denied) => { + ProtoAuthResult::ApprovalDenied } - .into(), - )), - } + auth::Error::ApproveError(auth::ApproveError::Upstream( + crate::actors::router::ApprovalError::NoUserAgentsConnected, + )) => ProtoAuthResult::NoUserAgentsOnline, + auth::Error::ApproveError(auth::ApproveError::Internal) + | auth::Error::DatabasePoolUnavailable + | auth::Error::DatabaseOperationFailed + | auth::Error::Transport => ProtoAuthResult::Internal, + } + .into(), + ) + } + + async fn send_client_response( + &mut self, + payload: ClientResponsePayload, + ) -> Result<(), TransportError> { + let request_id = self.response_id.take(); + + self.bi + .send(Ok(ClientResponse { + request_id, + payload: Some(payload), + })) + .await } async fn send_auth_result(&mut self, result: ProtoAuthResult) -> Result<(), TransportError> { - self.0 - .send(Ok(ClientResponse { - payload: Some(ClientResponsePayload::AuthResult(result.into())), - })) + self.send_client_response(ClientResponsePayload::AuthResult(result.into())) .await } } @@ -69,19 +94,19 @@ impl Sender> for AuthTransportAdapter<'_> { &mut self, item: Result, ) -> Result<(), TransportError> { - let outbound = match item { - Ok(message) => Ok(AuthTransportAdapter::response_to_proto(message)), - Err(err) => Ok(AuthTransportAdapter::error_to_proto(err)), + let payload = match item { + Ok(message) => AuthTransportAdapter::response_to_proto(message), + Err(err) => AuthTransportAdapter::error_to_proto(err), }; - self.0.send(outbound).await + self.send_client_response(payload).await } } #[async_trait] impl Receiver for AuthTransportAdapter<'_> { async fn recv(&mut self) -> Option { - let request = match self.0.recv().await? { + let request = match self.bi.recv().await? { Ok(request) => request, Err(error) => { warn!(error = ?error, "grpc client recv failed; closing stream"); @@ -89,6 +114,15 @@ impl Receiver for AuthTransportAdapter<'_> { } }; + let request_id = match self.request_tracker.request(request.request_id) { + Ok(request_id) => request_id, + Err(error) => { + let _ = self.bi.send(Err(error)).await; + return None; + } + }; + *self.response_id = Some(request_id); + let payload = request.payload?; match payload { @@ -114,7 +148,13 @@ impl Receiver for AuthTransportAdapter<'_> { }; Some(auth::Inbound::AuthChallengeSolution { signature }) } - _ => None, + _ => { + let _ = self + .bi + .send(Err(Status::invalid_argument("Unsupported client auth request"))) + .await; + None + } } } } @@ -124,8 +164,10 @@ impl Bi> for AuthTransportAda pub async fn start( conn: &mut ClientConnection, bi: &mut GrpcBi, + request_tracker: &mut RequestTracker, + response_id: &mut Option, ) -> Result<(), auth::Error> { - let mut transport = AuthTransportAdapter(bi); + let mut transport = AuthTransportAdapter::new(bi, request_tracker, response_id); client::auth::authenticate(conn, &mut transport).await?; Ok(()) } diff --git a/server/crates/arbiter-server/src/grpc/mod.rs b/server/crates/arbiter-server/src/grpc/mod.rs index 7bdedea..709d24d 100644 --- a/server/crates/arbiter-server/src/grpc/mod.rs +++ b/server/crates/arbiter-server/src/grpc/mod.rs @@ -17,6 +17,7 @@ use crate::{ }; pub mod client; +mod request_tracker; pub mod user_agent; #[async_trait] diff --git a/server/crates/arbiter-server/src/grpc/request_tracker.rs b/server/crates/arbiter-server/src/grpc/request_tracker.rs new file mode 100644 index 0000000..e282343 --- /dev/null +++ b/server/crates/arbiter-server/src/grpc/request_tracker.rs @@ -0,0 +1,20 @@ +use tonic::Status; + +#[derive(Default)] +pub struct RequestTracker { + next_request_id: i32, +} + +impl RequestTracker { + pub fn request(&mut self, id: i32) -> Result { + if id < self.next_request_id { + return Err(Status::invalid_argument("Duplicate request id")); + } + + self.next_request_id = id + .checked_add(1) + .ok_or_else(|| Status::invalid_argument("Invalid request id"))?; + + Ok(id) + } +} diff --git a/server/crates/arbiter-server/src/grpc/user_agent.rs b/server/crates/arbiter-server/src/grpc/user_agent.rs index fe587dc..c3fb347 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent.rs @@ -53,6 +53,7 @@ use crate::{ Grant, SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, ether_transfer, token_transfers, }, + grpc::request_tracker::RequestTracker, utils::defer, }; use alloy::primitives::{Address, U256}; @@ -74,6 +75,7 @@ async fn dispatch_loop( mut bi: GrpcBi, actor: ActorRef, mut receiver: mpsc::Receiver, + mut request_tracker: RequestTracker, ) { loop { tokio::select! { @@ -92,7 +94,10 @@ async fn dispatch_loop( return; }; - if dispatch_conn_message(&mut bi, &actor, conn).await.is_err() { + if dispatch_conn_message(&mut bi, &actor, &mut request_tracker, conn) + .await + .is_err() + { return; } } @@ -103,6 +108,7 @@ async fn dispatch_loop( async fn dispatch_conn_message( bi: &mut GrpcBi, actor: &ActorRef, + request_tracker: &mut RequestTracker, conn: Result, ) -> Result<(), ()> { let conn = match conn { @@ -113,6 +119,14 @@ async fn dispatch_conn_message( } }; + let request_id = match request_tracker.request(conn.id) { + Ok(request_id) => request_id, + Err(err) => { + let _ = bi.send(Err(err)).await; + return Err(()); + } + }; + let Some(payload) = conn.payload else { let _ = bi .send(Err(Status::invalid_argument( @@ -267,6 +281,7 @@ async fn dispatch_conn_message( }; bi.send(Ok(UserAgentResponse { + id: Some(request_id), payload: Some(payload), })) .await @@ -289,6 +304,7 @@ async fn send_out_of_band( }; bi.send(Ok(UserAgentResponse { + id: None, payload: Some(payload), })) .await @@ -558,7 +574,17 @@ pub async fn start( mut conn: UserAgentConnection, mut bi: GrpcBi, ) { - let pubkey = match auth::start(&mut conn, &mut bi).await { + let mut request_tracker = RequestTracker::default(); + let mut response_id = None; + + let pubkey = match auth::start( + &mut conn, + &mut bi, + &mut request_tracker, + &mut response_id, + ) + .await + { Ok(pubkey) => pubkey, Err(e) => { warn!(error = ?e, "Authentication failed"); @@ -572,11 +598,10 @@ pub async fn start( let actor = UserAgentSession::spawn(UserAgentSession::new(conn, Box::new(oob_adapter))); let actor_for_cleanup = actor.clone(); - // when connection closes let _ = defer(move || { actor_for_cleanup.kill(); }); info!(?pubkey, "User authenticated successfully"); - dispatch_loop(bi, actor, oob_receiver).await; + dispatch_loop(bi, actor, oob_receiver, request_tracker).await; } 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 0e648a8..024190d 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent/auth.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent/auth.rs @@ -1,52 +1,56 @@ use arbiter_proto::{ - proto::{ - self, - evm::{ - EtherTransferSettings as ProtoEtherTransferSettings, EvmError as ProtoEvmError, - EvmGrantCreateRequest, EvmGrantCreateResponse, EvmGrantDeleteRequest, - EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, GrantEntry, - SharedSettings as ProtoSharedSettings, SpecificGrant as ProtoSpecificGrant, - TokenTransferSettings as ProtoTokenTransferSettings, - VolumeRateLimit as ProtoVolumeRateLimit, WalletCreateResponse, WalletEntry, WalletList, - WalletListResponse, evm_grant_create_response::Result as EvmGrantCreateResult, - evm_grant_delete_response::Result as EvmGrantDeleteResult, - evm_grant_list_response::Result as EvmGrantListResult, - specific_grant::Grant as ProtoSpecificGrantType, - wallet_create_response::Result as WalletCreateResult, - wallet_list_response::Result as WalletListResult, - }, - user_agent::{ - AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest, - AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult, - BootstrapEncryptedKey as ProtoBootstrapEncryptedKey, - BootstrapResult as ProtoBootstrapResult, ClientConnectionCancel, - ClientConnectionRequest, ClientConnectionResponse, KeyType as ProtoKeyType, - UnsealEncryptedKey as ProtoUnsealEncryptedKey, UnsealResult as ProtoUnsealResult, - UnsealStart, UnsealStartResponse, UserAgentRequest, UserAgentResponse, - VaultState as ProtoVaultState, user_agent_request::Payload as UserAgentRequestPayload, - user_agent_response::Payload as UserAgentResponsePayload, - }, + proto::user_agent::{ + AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest, + AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult, + KeyType as ProtoKeyType, UserAgentRequest, UserAgentResponse, + user_agent_request::Payload as UserAgentRequestPayload, + user_agent_response::Payload as UserAgentResponsePayload, }, transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi}, }; use async_trait::async_trait; -use tonic::{Status, Streaming}; -use tracing::{info, warn}; +use tonic::Status; +use tracing::warn; use crate::{ - actors::user_agent::{ - self, AuthPublicKey, OutOfBand as DomainResponse, UserAgentConnection, auth, - }, + actors::user_agent::{AuthPublicKey, UserAgentConnection, auth}, db::models::KeyType, - evm::policies::{ - Grant, SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, - ether_transfer, token_transfers, - }, + grpc::request_tracker::RequestTracker, }; -use alloy::primitives::{Address, U256}; -use chrono::{DateTime, TimeZone, Utc}; -pub struct AuthTransportAdapter<'a>(&'a mut GrpcBi); +pub struct AuthTransportAdapter<'a> { + bi: &'a mut GrpcBi, + request_tracker: &'a mut RequestTracker, + response_id: &'a mut Option, +} + +impl<'a> AuthTransportAdapter<'a> { + pub fn new( + bi: &'a mut GrpcBi, + request_tracker: &'a mut RequestTracker, + response_id: &'a mut Option, + ) -> Self { + Self { + bi, + request_tracker, + response_id, + } + } + + async fn send_user_agent_response( + &mut self, + payload: UserAgentResponsePayload, + ) -> Result<(), TransportError> { + let id = self.response_id.take(); + + self.bi + .send(Ok(UserAgentResponse { + id, + payload: Some(payload), + })) + .await + } +} #[async_trait] impl Sender> for AuthTransportAdapter<'_> { @@ -55,39 +59,53 @@ impl Sender> for AuthTransportAdapter<'_> { item: Result, ) -> Result<(), TransportError> { use auth::{Error, Outbound}; - let response = match item { - Ok(Outbound::AuthChallenge { nonce }) => Ok(UserAgentResponsePayload::AuthChallenge( - ProtoAuthChallenge { nonce }, - )), - Ok(Outbound::AuthSuccess) => Ok(UserAgentResponsePayload::AuthResult( - ProtoAuthResult::Success.into(), - )), - - Err(Error::UnregisteredPublicKey) => Ok(UserAgentResponsePayload::AuthResult( - ProtoAuthResult::InvalidKey.into(), - )), - Err(Error::InvalidChallengeSolution) => Ok(UserAgentResponsePayload::AuthResult( - ProtoAuthResult::InvalidSignature.into(), - )), - Err(Error::InvalidBootstrapToken) => Ok(UserAgentResponsePayload::BootstrapResult( - ProtoAuthResult::TokenInvalid.into(), - )), - Err(Error::Internal { details }) => Err(Status::internal(details)), - Err(Error::Transport) => Err(Status::unavailable("transport error")), + let payload = match item { + Ok(Outbound::AuthChallenge { nonce }) => { + UserAgentResponsePayload::AuthChallenge(ProtoAuthChallenge { nonce }) + } + Ok(Outbound::AuthSuccess) => { + UserAgentResponsePayload::AuthResult(ProtoAuthResult::Success.into()) + } + Err(Error::UnregisteredPublicKey) => { + UserAgentResponsePayload::AuthResult(ProtoAuthResult::InvalidKey.into()) + } + Err(Error::InvalidChallengeSolution) => { + UserAgentResponsePayload::AuthResult(ProtoAuthResult::InvalidSignature.into()) + } + Err(Error::InvalidBootstrapToken) => { + UserAgentResponsePayload::AuthResult(ProtoAuthResult::TokenInvalid.into()) + } + Err(Error::Internal { details }) => return self.bi.send(Err(Status::internal(details))).await, + Err(Error::Transport) => { + return self.bi.send(Err(Status::unavailable("transport error"))).await; + } }; - self.0 - .send(response.map(|r| UserAgentResponse { payload: Some(r) })) - .await + + self.send_user_agent_response(payload).await } } #[async_trait] impl Receiver for AuthTransportAdapter<'_> { async fn recv(&mut self) -> Option { - let Ok(UserAgentRequest { - payload: Some(payload), - }) = self.0.recv().await? - else { + let request = match self.bi.recv().await? { + Ok(request) => request, + Err(error) => { + warn!(error = ?error, "Failed to receive user agent auth request"); + return None; + } + }; + + let request_id = match self.request_tracker.request(request.id) { + Ok(request_id) => request_id, + Err(error) => { + let _ = self.bi.send(Err(error)).await; + return None; + } + }; + *self.response_id = Some(request_id); + + let Some(payload) = request.payload else { warn!( event = "received request with empty payload", "grpc.useragent.auth_adapter" @@ -136,16 +154,27 @@ impl Receiver for AuthTransportAdapter<'_> { UserAgentRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution { signature, }) => Some(auth::Inbound::AuthChallengeSolution { signature }), - _ => None, // Ignore other request types for this adapter + _ => { + let _ = self + .bi + .send(Err(Status::invalid_argument( + "Unsupported user-agent auth request", + ))) + .await; + None + } } } } + impl Bi> for AuthTransportAdapter<'_> {} pub async fn start( conn: &mut UserAgentConnection, bi: &mut GrpcBi, + request_tracker: &mut RequestTracker, + response_id: &mut Option, ) -> Result { - let mut transport = AuthTransportAdapter(bi); + let transport = AuthTransportAdapter::new(bi, request_tracker, response_id); auth::authenticate(conn, transport).await } -- 2.49.1 From d9b17b253a514c292d74881a3118bafb978721b6 Mon Sep 17 00:00:00 2001 From: hdbg Date: Thu, 19 Mar 2026 00:05:55 +0100 Subject: [PATCH 5/8] refactor(useragent): using request/response for correct multiplexing behaviour --- useragent/lib/features/connection/auth.dart | 19 +- .../lib/features/connection/connection.dart | 112 +++++++-- useragent/lib/features/connection/evm.dart | 12 +- .../lib/features/connection/evm/grants.dart | 14 +- useragent/lib/features/connection/vault.dart | 12 +- useragent/lib/proto/client.pb.dart | 218 +++++++----------- useragent/lib/proto/client.pbenum.dart | 72 ++++-- useragent/lib/proto/client.pbjson.dart | 154 +++++++------ useragent/lib/proto/user_agent.pb.dart | 102 +++----- useragent/lib/proto/user_agent.pbenum.dart | 45 +++- useragent/lib/proto/user_agent.pbjson.dart | 177 +++++++++----- useragent/lib/providers/vault_state.dart | 4 +- useragent/lib/providers/vault_state.g.dart | 2 +- 13 files changed, 552 insertions(+), 391 deletions(-) diff --git a/useragent/lib/features/connection/auth.dart b/useragent/lib/features/connection/auth.dart index 9e432b8..fa95e4e 100644 --- a/useragent/lib/features/connection/auth.dart +++ b/useragent/lib/features/connection/auth.dart @@ -30,15 +30,18 @@ Future connectAndAuthorize( KeyAlgorithm.ed25519 => KeyType.KEY_TYPE_ED25519, }, ); - await connection.send(UserAgentRequest(authChallengeRequest: req)); + final response = await connection.request( + UserAgentRequest(authChallengeRequest: req), + ); talker.info( "Sent auth challenge request with pubkey ${base64Encode(pubkey)}", ); - - final response = await connection.receive(); talker.info('Received response from server, checking auth flow...'); - if (response.hasAuthOk()) { + if (response.hasAuthResult()) { + if (response.authResult != AuthResult.AUTH_RESULT_SUCCESS) { + throw Exception('Authentication failed: ${response.authResult}'); + } talker.info('Authentication successful, connection established'); return connection; } @@ -55,18 +58,20 @@ Future connectAndAuthorize( ); final signature = await key.sign(challenge); - await connection.send( + final solutionResponse = await connection.request( UserAgentRequest(authChallengeSolution: AuthChallengeSolution(signature: signature)), ); talker.info('Sent auth challenge solution, waiting for server response...'); - final solutionResponse = await connection.receive(); - if (!solutionResponse.hasAuthOk()) { + if (!solutionResponse.hasAuthResult()) { throw Exception( 'Expected AuthChallengeSolutionResponse, got ${solutionResponse.whichPayload()}', ); } + if (solutionResponse.authResult != AuthResult.AUTH_RESULT_SUCCESS) { + throw Exception('Authentication failed: ${solutionResponse.authResult}'); + } talker.info('Authentication successful, connection established'); return connection; diff --git a/useragent/lib/features/connection/connection.dart b/useragent/lib/features/connection/connection.dart index 726a8d5..b5d4a38 100644 --- a/useragent/lib/features/connection/connection.dart +++ b/useragent/lib/features/connection/connection.dart @@ -5,33 +5,113 @@ import 'package:grpc/grpc.dart'; import 'package:mtcore/markettakers.dart'; class Connection { - final ClientChannel channel; - final StreamController _tx; - final StreamIterator _rx; - Connection({ required this.channel, required StreamController tx, required ResponseStream rx, - }) : _tx = tx, - _rx = StreamIterator(rx); - - Future send(UserAgentRequest request) async { - talker.debug('Sending request: ${request.toDebugString()}'); - _tx.add(request); + }) : _tx = tx { + _rxSubscription = rx.listen( + _handleResponse, + onError: _handleError, + onDone: _handleDone, + cancelOnError: true, + ); } - Future receive() async { - final hasValue = await _rx.moveNext(); - if (!hasValue) { - throw Exception('Connection closed while waiting for server response.'); + final ClientChannel channel; + final StreamController _tx; + final Map> _pendingRequests = {}; + final StreamController _outOfBandMessages = + StreamController.broadcast(); + + StreamSubscription? _rxSubscription; + int _nextRequestId = 0; + + Stream get outOfBandMessages => _outOfBandMessages.stream; + + Future request(UserAgentRequest message) async { + _ensureOpen(); + + final requestId = _nextRequestId++; + final completer = Completer(); + _pendingRequests[requestId] = completer; + + message.id = requestId; + talker.debug('Sending request: ${message.toDebugString()}'); + + try { + _tx.add(message); + } catch (error, stackTrace) { + _pendingRequests.remove(requestId); + completer.completeError(error, stackTrace); } - talker.debug('Received response: ${_rx.current.toDebugString()}'); - return _rx.current; + + return completer.future; } Future close() async { + final rxSubscription = _rxSubscription; + if (rxSubscription == null) { + return; + } + + _rxSubscription = null; + await rxSubscription.cancel(); + _failPendingRequests(Exception('Connection closed.')); + await _outOfBandMessages.close(); await _tx.close(); await channel.shutdown(); } + + void _handleResponse(UserAgentResponse response) { + talker.debug('Received response: ${response.toDebugString()}'); + + if (response.hasId()) { + final completer = _pendingRequests.remove(response.id); + if (completer == null) { + talker.warning('Received response for unknown request id ${response.id}'); + return; + } + completer.complete(response); + return; + } + + _outOfBandMessages.add(response); + } + + void _handleError(Object error, StackTrace stackTrace) { + _rxSubscription = null; + _failPendingRequests(error, stackTrace); + _outOfBandMessages.addError(error, stackTrace); + } + + void _handleDone() { + if (_rxSubscription == null) { + return; + } + + _rxSubscription = null; + final error = Exception( + 'Connection closed while waiting for server response.', + ); + _failPendingRequests(error); + _outOfBandMessages.close(); + } + + void _failPendingRequests(Object error, [StackTrace? stackTrace]) { + final pendingRequests = _pendingRequests.values.toList(growable: false); + _pendingRequests.clear(); + + for (final completer in pendingRequests) { + if (!completer.isCompleted) { + completer.completeError(error, stackTrace); + } + } + } + + void _ensureOpen() { + if (_rxSubscription == null) { + throw StateError('Connection is closed.'); + } + } } diff --git a/useragent/lib/features/connection/evm.dart b/useragent/lib/features/connection/evm.dart index 5ceb422..aae5a9d 100644 --- a/useragent/lib/features/connection/evm.dart +++ b/useragent/lib/features/connection/evm.dart @@ -4,9 +4,9 @@ import 'package:arbiter/proto/user_agent.pb.dart'; import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart'; Future> listEvmWallets(Connection connection) async { - await connection.send(UserAgentRequest(evmWalletList: Empty())); - - final response = await connection.receive(); + final response = await connection.request( + UserAgentRequest(evmWalletList: Empty()), + ); if (!response.hasEvmWalletList()) { throw Exception( 'Expected EVM wallet list response, got ${response.whichPayload()}', @@ -25,9 +25,9 @@ Future> listEvmWallets(Connection connection) async { } Future createEvmWallet(Connection connection) async { - await connection.send(UserAgentRequest(evmWalletCreate: Empty())); - - final response = await connection.receive(); + final response = await connection.request( + UserAgentRequest(evmWalletCreate: Empty()), + ); if (!response.hasEvmWalletCreate()) { throw Exception( 'Expected EVM wallet create response, got ${response.whichPayload()}', diff --git a/useragent/lib/features/connection/evm/grants.dart b/useragent/lib/features/connection/evm/grants.dart index f4bfd6f..08550e3 100644 --- a/useragent/lib/features/connection/evm/grants.dart +++ b/useragent/lib/features/connection/evm/grants.dart @@ -13,9 +13,9 @@ Future> listEvmGrants( request.walletId = walletId; } - await connection.send(UserAgentRequest(evmGrantList: request)); - - final response = await connection.receive(); + final response = await connection.request( + UserAgentRequest(evmGrantList: request), + ); if (!response.hasEvmGrantList()) { throw Exception( 'Expected EVM grant list response, got ${response.whichPayload()}', @@ -45,7 +45,7 @@ Future createEvmGrant( TransactionRateLimit? rateLimit, required SpecificGrant specific, }) async { - await connection.send( + final response = await connection.request( UserAgentRequest( evmGrantCreate: EvmGrantCreateRequest( clientId: clientId, @@ -62,8 +62,6 @@ Future createEvmGrant( ), ), ); - - final response = await connection.receive(); if (!response.hasEvmGrantCreate()) { throw Exception( 'Expected EVM grant create response, got ${response.whichPayload()}', @@ -82,11 +80,9 @@ Future createEvmGrant( } Future deleteEvmGrant(Connection connection, int grantId) async { - await connection.send( + final response = await connection.request( UserAgentRequest(evmGrantDelete: EvmGrantDeleteRequest(grantId: grantId)), ); - - final response = await connection.receive(); if (!response.hasEvmGrantDelete()) { throw Exception( 'Expected EVM grant delete response, got ${response.whichPayload()}', diff --git a/useragent/lib/features/connection/vault.dart b/useragent/lib/features/connection/vault.dart index 92f3048..ae57243 100644 --- a/useragent/lib/features/connection/vault.dart +++ b/useragent/lib/features/connection/vault.dart @@ -10,7 +10,7 @@ Future bootstrapVault( ) async { final encryptedKey = await _encryptVaultKeyMaterial(connection, password); - await connection.send( + final response = await connection.request( UserAgentRequest( bootstrapEncryptedKey: BootstrapEncryptedKey( nonce: encryptedKey.nonce, @@ -19,8 +19,6 @@ Future bootstrapVault( ), ), ); - - final response = await connection.receive(); if (!response.hasBootstrapResult()) { throw Exception( 'Expected bootstrap result, got ${response.whichPayload()}', @@ -33,7 +31,7 @@ Future bootstrapVault( Future unsealVault(Connection connection, String password) async { final encryptedKey = await _encryptVaultKeyMaterial(connection, password); - await connection.send( + final response = await connection.request( UserAgentRequest( unsealEncryptedKey: UnsealEncryptedKey( nonce: encryptedKey.nonce, @@ -42,8 +40,6 @@ Future unsealVault(Connection connection, String password) async { ), ), ); - - final response = await connection.receive(); if (!response.hasUnsealResult()) { throw Exception('Expected unseal result, got ${response.whichPayload()}'); } @@ -60,11 +56,9 @@ Future<_EncryptedVaultKey> _encryptVaultKeyMaterial( final clientKeyPair = await keyExchange.newKeyPair(); final clientPublicKey = await clientKeyPair.extractPublicKey(); - await connection.send( + final handshakeResponse = await connection.request( UserAgentRequest(unsealStart: UnsealStart(clientPubkey: clientPublicKey.bytes)), ); - - final handshakeResponse = await connection.receive(); if (!handshakeResponse.hasUnsealStartResponse()) { throw Exception( 'Expected unseal handshake response, got ${handshakeResponse.whichPayload()}', diff --git a/useragent/lib/proto/client.pb.dart b/useragent/lib/proto/client.pb.dart index 4a5e8f5..8d0a540 100644 --- a/useragent/lib/proto/client.pb.dart +++ b/useragent/lib/proto/client.pb.dart @@ -13,9 +13,10 @@ import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; +import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart' as $0; import 'client.pbenum.dart'; -import 'evm.pb.dart' as $0; +import 'evm.pb.dart' as $1; export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; @@ -199,46 +200,10 @@ class AuthChallengeSolution extends $pb.GeneratedMessage { void clearSignature() => $_clearField(1); } -class AuthOk extends $pb.GeneratedMessage { - factory AuthOk() => create(); - - AuthOk._(); - - factory AuthOk.fromBuffer($core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory AuthOk.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'AuthOk', - package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'), - createEmptyInstance: create) - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - AuthOk clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - AuthOk copyWith(void Function(AuthOk) updates) => - super.copyWith((message) => updates(message as AuthOk)) as AuthOk; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static AuthOk create() => AuthOk._(); - @$core.override - AuthOk createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static AuthOk getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static AuthOk? _defaultInstance; -} - enum ClientRequest_Payload { authChallengeRequest, authChallengeSolution, + queryVaultState, notSet } @@ -246,12 +211,16 @@ class ClientRequest extends $pb.GeneratedMessage { factory ClientRequest({ AuthChallengeRequest? authChallengeRequest, AuthChallengeSolution? authChallengeSolution, + $0.Empty? queryVaultState, + $core.int? requestId, }) { final result = create(); if (authChallengeRequest != null) result.authChallengeRequest = authChallengeRequest; if (authChallengeSolution != null) result.authChallengeSolution = authChallengeSolution; + if (queryVaultState != null) result.queryVaultState = queryVaultState; + if (requestId != null) result.requestId = requestId; return result; } @@ -268,19 +237,23 @@ class ClientRequest extends $pb.GeneratedMessage { _ClientRequest_PayloadByTag = { 1: ClientRequest_Payload.authChallengeRequest, 2: ClientRequest_Payload.authChallengeSolution, + 3: ClientRequest_Payload.queryVaultState, 0: ClientRequest_Payload.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'ClientRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'), createEmptyInstance: create) - ..oo(0, [1, 2]) + ..oo(0, [1, 2, 3]) ..aOM( 1, _omitFieldNames ? '' : 'authChallengeRequest', subBuilder: AuthChallengeRequest.create) ..aOM( 2, _omitFieldNames ? '' : 'authChallengeSolution', subBuilder: AuthChallengeSolution.create) + ..aOM<$0.Empty>(3, _omitFieldNames ? '' : 'queryVaultState', + subBuilder: $0.Empty.create) + ..aI(4, _omitFieldNames ? '' : 'requestId') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -304,10 +277,12 @@ class ClientRequest extends $pb.GeneratedMessage { @$pb.TagNumber(1) @$pb.TagNumber(2) + @$pb.TagNumber(3) ClientRequest_Payload whichPayload() => _ClientRequest_PayloadByTag[$_whichOneof(0)]!; @$pb.TagNumber(1) @$pb.TagNumber(2) + @$pb.TagNumber(3) void clearPayload() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -332,89 +307,55 @@ class ClientRequest extends $pb.GeneratedMessage { void clearAuthChallengeSolution() => $_clearField(2); @$pb.TagNumber(2) AuthChallengeSolution ensureAuthChallengeSolution() => $_ensure(1); -} -class ClientConnectError extends $pb.GeneratedMessage { - factory ClientConnectError({ - ClientConnectError_Code? code, - }) { - final result = create(); - if (code != null) result.code = code; - return result; - } + @$pb.TagNumber(3) + $0.Empty get queryVaultState => $_getN(2); + @$pb.TagNumber(3) + set queryVaultState($0.Empty value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasQueryVaultState() => $_has(2); + @$pb.TagNumber(3) + void clearQueryVaultState() => $_clearField(3); + @$pb.TagNumber(3) + $0.Empty ensureQueryVaultState() => $_ensure(2); - ClientConnectError._(); - - factory ClientConnectError.fromBuffer($core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory ClientConnectError.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ClientConnectError', - package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'), - createEmptyInstance: create) - ..aE(1, _omitFieldNames ? '' : 'code', - enumValues: ClientConnectError_Code.values) - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ClientConnectError clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ClientConnectError copyWith(void Function(ClientConnectError) updates) => - super.copyWith((message) => updates(message as ClientConnectError)) - as ClientConnectError; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static ClientConnectError create() => ClientConnectError._(); - @$core.override - ClientConnectError createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static ClientConnectError getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static ClientConnectError? _defaultInstance; - - @$pb.TagNumber(1) - ClientConnectError_Code get code => $_getN(0); - @$pb.TagNumber(1) - set code(ClientConnectError_Code value) => $_setField(1, value); - @$pb.TagNumber(1) - $core.bool hasCode() => $_has(0); - @$pb.TagNumber(1) - void clearCode() => $_clearField(1); + @$pb.TagNumber(4) + $core.int get requestId => $_getIZ(3); + @$pb.TagNumber(4) + set requestId($core.int value) => $_setSignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasRequestId() => $_has(3); + @$pb.TagNumber(4) + void clearRequestId() => $_clearField(4); } enum ClientResponse_Payload { authChallenge, - authOk, + authResult, evmSignTransaction, evmAnalyzeTransaction, - clientConnectError, + vaultState, notSet } class ClientResponse extends $pb.GeneratedMessage { factory ClientResponse({ AuthChallenge? authChallenge, - AuthOk? authOk, - $0.EvmSignTransactionResponse? evmSignTransaction, - $0.EvmAnalyzeTransactionResponse? evmAnalyzeTransaction, - ClientConnectError? clientConnectError, + AuthResult? authResult, + $1.EvmSignTransactionResponse? evmSignTransaction, + $1.EvmAnalyzeTransactionResponse? evmAnalyzeTransaction, + VaultState? vaultState, + $core.int? requestId, }) { final result = create(); if (authChallenge != null) result.authChallenge = authChallenge; - if (authOk != null) result.authOk = authOk; + if (authResult != null) result.authResult = authResult; if (evmSignTransaction != null) result.evmSignTransaction = evmSignTransaction; if (evmAnalyzeTransaction != null) result.evmAnalyzeTransaction = evmAnalyzeTransaction; - if (clientConnectError != null) - result.clientConnectError = clientConnectError; + if (vaultState != null) result.vaultState = vaultState; + if (requestId != null) result.requestId = requestId; return result; } @@ -430,28 +371,30 @@ class ClientResponse extends $pb.GeneratedMessage { static const $core.Map<$core.int, ClientResponse_Payload> _ClientResponse_PayloadByTag = { 1: ClientResponse_Payload.authChallenge, - 2: ClientResponse_Payload.authOk, + 2: ClientResponse_Payload.authResult, 3: ClientResponse_Payload.evmSignTransaction, 4: ClientResponse_Payload.evmAnalyzeTransaction, - 5: ClientResponse_Payload.clientConnectError, + 6: ClientResponse_Payload.vaultState, 0: ClientResponse_Payload.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'ClientResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4, 5]) + ..oo(0, [1, 2, 3, 4, 6]) ..aOM(1, _omitFieldNames ? '' : 'authChallenge', subBuilder: AuthChallenge.create) - ..aOM(2, _omitFieldNames ? '' : 'authOk', subBuilder: AuthOk.create) - ..aOM<$0.EvmSignTransactionResponse>( + ..aE(2, _omitFieldNames ? '' : 'authResult', + enumValues: AuthResult.values) + ..aOM<$1.EvmSignTransactionResponse>( 3, _omitFieldNames ? '' : 'evmSignTransaction', - subBuilder: $0.EvmSignTransactionResponse.create) - ..aOM<$0.EvmAnalyzeTransactionResponse>( + subBuilder: $1.EvmSignTransactionResponse.create) + ..aOM<$1.EvmAnalyzeTransactionResponse>( 4, _omitFieldNames ? '' : 'evmAnalyzeTransaction', - subBuilder: $0.EvmAnalyzeTransactionResponse.create) - ..aOM(5, _omitFieldNames ? '' : 'clientConnectError', - subBuilder: ClientConnectError.create) + subBuilder: $1.EvmAnalyzeTransactionResponse.create) + ..aE(6, _omitFieldNames ? '' : 'vaultState', + enumValues: VaultState.values) + ..aI(7, _omitFieldNames ? '' : 'requestId') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -477,14 +420,14 @@ class ClientResponse extends $pb.GeneratedMessage { @$pb.TagNumber(2) @$pb.TagNumber(3) @$pb.TagNumber(4) - @$pb.TagNumber(5) + @$pb.TagNumber(6) ClientResponse_Payload whichPayload() => _ClientResponse_PayloadByTag[$_whichOneof(0)]!; @$pb.TagNumber(1) @$pb.TagNumber(2) @$pb.TagNumber(3) @$pb.TagNumber(4) - @$pb.TagNumber(5) + @$pb.TagNumber(6) void clearPayload() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -499,50 +442,55 @@ class ClientResponse extends $pb.GeneratedMessage { AuthChallenge ensureAuthChallenge() => $_ensure(0); @$pb.TagNumber(2) - AuthOk get authOk => $_getN(1); + AuthResult get authResult => $_getN(1); @$pb.TagNumber(2) - set authOk(AuthOk value) => $_setField(2, value); + set authResult(AuthResult value) => $_setField(2, value); @$pb.TagNumber(2) - $core.bool hasAuthOk() => $_has(1); + $core.bool hasAuthResult() => $_has(1); @$pb.TagNumber(2) - void clearAuthOk() => $_clearField(2); - @$pb.TagNumber(2) - AuthOk ensureAuthOk() => $_ensure(1); + void clearAuthResult() => $_clearField(2); @$pb.TagNumber(3) - $0.EvmSignTransactionResponse get evmSignTransaction => $_getN(2); + $1.EvmSignTransactionResponse get evmSignTransaction => $_getN(2); @$pb.TagNumber(3) - set evmSignTransaction($0.EvmSignTransactionResponse value) => + set evmSignTransaction($1.EvmSignTransactionResponse value) => $_setField(3, value); @$pb.TagNumber(3) $core.bool hasEvmSignTransaction() => $_has(2); @$pb.TagNumber(3) void clearEvmSignTransaction() => $_clearField(3); @$pb.TagNumber(3) - $0.EvmSignTransactionResponse ensureEvmSignTransaction() => $_ensure(2); + $1.EvmSignTransactionResponse ensureEvmSignTransaction() => $_ensure(2); @$pb.TagNumber(4) - $0.EvmAnalyzeTransactionResponse get evmAnalyzeTransaction => $_getN(3); + $1.EvmAnalyzeTransactionResponse get evmAnalyzeTransaction => $_getN(3); @$pb.TagNumber(4) - set evmAnalyzeTransaction($0.EvmAnalyzeTransactionResponse value) => + set evmAnalyzeTransaction($1.EvmAnalyzeTransactionResponse value) => $_setField(4, value); @$pb.TagNumber(4) $core.bool hasEvmAnalyzeTransaction() => $_has(3); @$pb.TagNumber(4) void clearEvmAnalyzeTransaction() => $_clearField(4); @$pb.TagNumber(4) - $0.EvmAnalyzeTransactionResponse ensureEvmAnalyzeTransaction() => $_ensure(3); + $1.EvmAnalyzeTransactionResponse ensureEvmAnalyzeTransaction() => $_ensure(3); - @$pb.TagNumber(5) - ClientConnectError get clientConnectError => $_getN(4); - @$pb.TagNumber(5) - set clientConnectError(ClientConnectError value) => $_setField(5, value); - @$pb.TagNumber(5) - $core.bool hasClientConnectError() => $_has(4); - @$pb.TagNumber(5) - void clearClientConnectError() => $_clearField(5); - @$pb.TagNumber(5) - ClientConnectError ensureClientConnectError() => $_ensure(4); + @$pb.TagNumber(6) + VaultState get vaultState => $_getN(4); + @$pb.TagNumber(6) + set vaultState(VaultState value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasVaultState() => $_has(4); + @$pb.TagNumber(6) + void clearVaultState() => $_clearField(6); + + @$pb.TagNumber(7) + $core.int get requestId => $_getIZ(5); + @$pb.TagNumber(7) + set requestId($core.int value) => $_setSignedInt32(5, value); + @$pb.TagNumber(7) + $core.bool hasRequestId() => $_has(5); + @$pb.TagNumber(7) + void clearRequestId() => $_clearField(7); } const $core.bool _omitFieldNames = diff --git a/useragent/lib/proto/client.pbenum.dart b/useragent/lib/proto/client.pbenum.dart index f5d4b5e..ab30126 100644 --- a/useragent/lib/proto/client.pbenum.dart +++ b/useragent/lib/proto/client.pbenum.dart @@ -14,28 +14,66 @@ import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; -class ClientConnectError_Code extends $pb.ProtobufEnum { - static const ClientConnectError_Code UNKNOWN = - ClientConnectError_Code._(0, _omitEnumNames ? '' : 'UNKNOWN'); - static const ClientConnectError_Code APPROVAL_DENIED = - ClientConnectError_Code._(1, _omitEnumNames ? '' : 'APPROVAL_DENIED'); - static const ClientConnectError_Code NO_USER_AGENTS_ONLINE = - ClientConnectError_Code._( - 2, _omitEnumNames ? '' : 'NO_USER_AGENTS_ONLINE'); +class AuthResult extends $pb.ProtobufEnum { + static const AuthResult AUTH_RESULT_UNSPECIFIED = + AuthResult._(0, _omitEnumNames ? '' : 'AUTH_RESULT_UNSPECIFIED'); + static const AuthResult AUTH_RESULT_SUCCESS = + AuthResult._(1, _omitEnumNames ? '' : 'AUTH_RESULT_SUCCESS'); + static const AuthResult AUTH_RESULT_INVALID_KEY = + AuthResult._(2, _omitEnumNames ? '' : 'AUTH_RESULT_INVALID_KEY'); + static const AuthResult AUTH_RESULT_INVALID_SIGNATURE = + AuthResult._(3, _omitEnumNames ? '' : 'AUTH_RESULT_INVALID_SIGNATURE'); + static const AuthResult AUTH_RESULT_APPROVAL_DENIED = + AuthResult._(4, _omitEnumNames ? '' : 'AUTH_RESULT_APPROVAL_DENIED'); + static const AuthResult AUTH_RESULT_NO_USER_AGENTS_ONLINE = AuthResult._( + 5, _omitEnumNames ? '' : 'AUTH_RESULT_NO_USER_AGENTS_ONLINE'); + static const AuthResult AUTH_RESULT_INTERNAL = + AuthResult._(6, _omitEnumNames ? '' : 'AUTH_RESULT_INTERNAL'); - static const $core.List values = - [ - UNKNOWN, - APPROVAL_DENIED, - NO_USER_AGENTS_ONLINE, + static const $core.List values = [ + AUTH_RESULT_UNSPECIFIED, + AUTH_RESULT_SUCCESS, + AUTH_RESULT_INVALID_KEY, + AUTH_RESULT_INVALID_SIGNATURE, + AUTH_RESULT_APPROVAL_DENIED, + AUTH_RESULT_NO_USER_AGENTS_ONLINE, + AUTH_RESULT_INTERNAL, ]; - static final $core.List _byValue = - $pb.ProtobufEnum.$_initByValueList(values, 2); - static ClientConnectError_Code? valueOf($core.int value) => + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 6); + static AuthResult? valueOf($core.int value) => value < 0 || value >= _byValue.length ? null : _byValue[value]; - const ClientConnectError_Code._(super.value, super.name); + const AuthResult._(super.value, super.name); +} + +class VaultState extends $pb.ProtobufEnum { + static const VaultState VAULT_STATE_UNSPECIFIED = + VaultState._(0, _omitEnumNames ? '' : 'VAULT_STATE_UNSPECIFIED'); + static const VaultState VAULT_STATE_UNBOOTSTRAPPED = + VaultState._(1, _omitEnumNames ? '' : 'VAULT_STATE_UNBOOTSTRAPPED'); + static const VaultState VAULT_STATE_SEALED = + VaultState._(2, _omitEnumNames ? '' : 'VAULT_STATE_SEALED'); + static const VaultState VAULT_STATE_UNSEALED = + VaultState._(3, _omitEnumNames ? '' : 'VAULT_STATE_UNSEALED'); + static const VaultState VAULT_STATE_ERROR = + VaultState._(4, _omitEnumNames ? '' : 'VAULT_STATE_ERROR'); + + static const $core.List values = [ + VAULT_STATE_UNSPECIFIED, + VAULT_STATE_UNBOOTSTRAPPED, + VAULT_STATE_SEALED, + VAULT_STATE_UNSEALED, + VAULT_STATE_ERROR, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 4); + static VaultState? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const VaultState._(super.value, super.name); } const $core.bool _omitEnumNames = diff --git a/useragent/lib/proto/client.pbjson.dart b/useragent/lib/proto/client.pbjson.dart index d005846..97f914d 100644 --- a/useragent/lib/proto/client.pbjson.dart +++ b/useragent/lib/proto/client.pbjson.dart @@ -15,6 +15,46 @@ import 'dart:convert' as $convert; import 'dart:core' as $core; import 'dart:typed_data' as $typed_data; +@$core.Deprecated('Use authResultDescriptor instead') +const AuthResult$json = { + '1': 'AuthResult', + '2': [ + {'1': 'AUTH_RESULT_UNSPECIFIED', '2': 0}, + {'1': 'AUTH_RESULT_SUCCESS', '2': 1}, + {'1': 'AUTH_RESULT_INVALID_KEY', '2': 2}, + {'1': 'AUTH_RESULT_INVALID_SIGNATURE', '2': 3}, + {'1': 'AUTH_RESULT_APPROVAL_DENIED', '2': 4}, + {'1': 'AUTH_RESULT_NO_USER_AGENTS_ONLINE', '2': 5}, + {'1': 'AUTH_RESULT_INTERNAL', '2': 6}, + ], +}; + +/// Descriptor for `AuthResult`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List authResultDescriptor = $convert.base64Decode( + 'CgpBdXRoUmVzdWx0EhsKF0FVVEhfUkVTVUxUX1VOU1BFQ0lGSUVEEAASFwoTQVVUSF9SRVNVTF' + 'RfU1VDQ0VTUxABEhsKF0FVVEhfUkVTVUxUX0lOVkFMSURfS0VZEAISIQodQVVUSF9SRVNVTFRf' + 'SU5WQUxJRF9TSUdOQVRVUkUQAxIfChtBVVRIX1JFU1VMVF9BUFBST1ZBTF9ERU5JRUQQBBIlCi' + 'FBVVRIX1JFU1VMVF9OT19VU0VSX0FHRU5UU19PTkxJTkUQBRIYChRBVVRIX1JFU1VMVF9JTlRF' + 'Uk5BTBAG'); + +@$core.Deprecated('Use vaultStateDescriptor instead') +const VaultState$json = { + '1': 'VaultState', + '2': [ + {'1': 'VAULT_STATE_UNSPECIFIED', '2': 0}, + {'1': 'VAULT_STATE_UNBOOTSTRAPPED', '2': 1}, + {'1': 'VAULT_STATE_SEALED', '2': 2}, + {'1': 'VAULT_STATE_UNSEALED', '2': 3}, + {'1': 'VAULT_STATE_ERROR', '2': 4}, + ], +}; + +/// Descriptor for `VaultState`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List vaultStateDescriptor = $convert.base64Decode( + 'CgpWYXVsdFN0YXRlEhsKF1ZBVUxUX1NUQVRFX1VOU1BFQ0lGSUVEEAASHgoaVkFVTFRfU1RBVE' + 'VfVU5CT09UU1RSQVBQRUQQARIWChJWQVVMVF9TVEFURV9TRUFMRUQQAhIYChRWQVVMVF9TVEFU' + 'RV9VTlNFQUxFRBADEhUKEVZBVUxUX1NUQVRFX0VSUk9SEAQ='); + @$core.Deprecated('Use authChallengeRequestDescriptor instead') const AuthChallengeRequest$json = { '1': 'AuthChallengeRequest', @@ -54,19 +94,11 @@ const AuthChallengeSolution$json = { final $typed_data.Uint8List authChallengeSolutionDescriptor = $convert.base64Decode( 'ChVBdXRoQ2hhbGxlbmdlU29sdXRpb24SHAoJc2lnbmF0dXJlGAEgASgMUglzaWduYXR1cmU='); -@$core.Deprecated('Use authOkDescriptor instead') -const AuthOk$json = { - '1': 'AuthOk', -}; - -/// Descriptor for `AuthOk`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List authOkDescriptor = - $convert.base64Decode('CgZBdXRoT2s='); - @$core.Deprecated('Use clientRequestDescriptor instead') const ClientRequest$json = { '1': 'ClientRequest', '2': [ + {'1': 'request_id', '3': 4, '4': 1, '5': 5, '10': 'requestId'}, { '1': 'auth_challenge_request', '3': 1, @@ -85,6 +117,15 @@ const ClientRequest$json = { '9': 0, '10': 'authChallengeSolution' }, + { + '1': 'query_vault_state', + '3': 3, + '4': 1, + '5': 11, + '6': '.google.protobuf.Empty', + '9': 0, + '10': 'queryVaultState' + }, ], '8': [ {'1': 'payload'}, @@ -93,47 +134,26 @@ const ClientRequest$json = { /// Descriptor for `ClientRequest`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List clientRequestDescriptor = $convert.base64Decode( - 'Cg1DbGllbnRSZXF1ZXN0ElwKFmF1dGhfY2hhbGxlbmdlX3JlcXVlc3QYASABKAsyJC5hcmJpdG' - 'VyLmNsaWVudC5BdXRoQ2hhbGxlbmdlUmVxdWVzdEgAUhRhdXRoQ2hhbGxlbmdlUmVxdWVzdBJf' - 'ChdhdXRoX2NoYWxsZW5nZV9zb2x1dGlvbhgCIAEoCzIlLmFyYml0ZXIuY2xpZW50LkF1dGhDaG' - 'FsbGVuZ2VTb2x1dGlvbkgAUhVhdXRoQ2hhbGxlbmdlU29sdXRpb25CCQoHcGF5bG9hZA=='); - -@$core.Deprecated('Use clientConnectErrorDescriptor instead') -const ClientConnectError$json = { - '1': 'ClientConnectError', - '2': [ - { - '1': 'code', - '3': 1, - '4': 1, - '5': 14, - '6': '.arbiter.client.ClientConnectError.Code', - '10': 'code' - }, - ], - '4': [ClientConnectError_Code$json], -}; - -@$core.Deprecated('Use clientConnectErrorDescriptor instead') -const ClientConnectError_Code$json = { - '1': 'Code', - '2': [ - {'1': 'UNKNOWN', '2': 0}, - {'1': 'APPROVAL_DENIED', '2': 1}, - {'1': 'NO_USER_AGENTS_ONLINE', '2': 2}, - ], -}; - -/// Descriptor for `ClientConnectError`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List clientConnectErrorDescriptor = $convert.base64Decode( - 'ChJDbGllbnRDb25uZWN0RXJyb3ISOwoEY29kZRgBIAEoDjInLmFyYml0ZXIuY2xpZW50LkNsaW' - 'VudENvbm5lY3RFcnJvci5Db2RlUgRjb2RlIkMKBENvZGUSCwoHVU5LTk9XThAAEhMKD0FQUFJP' - 'VkFMX0RFTklFRBABEhkKFU5PX1VTRVJfQUdFTlRTX09OTElORRAC'); + 'Cg1DbGllbnRSZXF1ZXN0Eh0KCnJlcXVlc3RfaWQYBCABKAVSCXJlcXVlc3RJZBJcChZhdXRoX2' + 'NoYWxsZW5nZV9yZXF1ZXN0GAEgASgLMiQuYXJiaXRlci5jbGllbnQuQXV0aENoYWxsZW5nZVJl' + 'cXVlc3RIAFIUYXV0aENoYWxsZW5nZVJlcXVlc3QSXwoXYXV0aF9jaGFsbGVuZ2Vfc29sdXRpb2' + '4YAiABKAsyJS5hcmJpdGVyLmNsaWVudC5BdXRoQ2hhbGxlbmdlU29sdXRpb25IAFIVYXV0aENo' + 'YWxsZW5nZVNvbHV0aW9uEkQKEXF1ZXJ5X3ZhdWx0X3N0YXRlGAMgASgLMhYuZ29vZ2xlLnByb3' + 'RvYnVmLkVtcHR5SABSD3F1ZXJ5VmF1bHRTdGF0ZUIJCgdwYXlsb2Fk'); @$core.Deprecated('Use clientResponseDescriptor instead') const ClientResponse$json = { '1': 'ClientResponse', '2': [ + { + '1': 'request_id', + '3': 7, + '4': 1, + '5': 5, + '9': 1, + '10': 'requestId', + '17': true + }, { '1': 'auth_challenge', '3': 1, @@ -144,22 +164,13 @@ const ClientResponse$json = { '10': 'authChallenge' }, { - '1': 'auth_ok', + '1': 'auth_result', '3': 2, '4': 1, - '5': 11, - '6': '.arbiter.client.AuthOk', + '5': 14, + '6': '.arbiter.client.AuthResult', '9': 0, - '10': 'authOk' - }, - { - '1': 'client_connect_error', - '3': 5, - '4': 1, - '5': 11, - '6': '.arbiter.client.ClientConnectError', - '9': 0, - '10': 'clientConnectError' + '10': 'authResult' }, { '1': 'evm_sign_transaction', @@ -179,19 +190,30 @@ const ClientResponse$json = { '9': 0, '10': 'evmAnalyzeTransaction' }, + { + '1': 'vault_state', + '3': 6, + '4': 1, + '5': 14, + '6': '.arbiter.client.VaultState', + '9': 0, + '10': 'vaultState' + }, ], '8': [ {'1': 'payload'}, + {'1': '_request_id'}, ], }; /// Descriptor for `ClientResponse`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List clientResponseDescriptor = $convert.base64Decode( - 'Cg5DbGllbnRSZXNwb25zZRJGCg5hdXRoX2NoYWxsZW5nZRgBIAEoCzIdLmFyYml0ZXIuY2xpZW' - '50LkF1dGhDaGFsbGVuZ2VIAFINYXV0aENoYWxsZW5nZRIxCgdhdXRoX29rGAIgASgLMhYuYXJi' - 'aXRlci5jbGllbnQuQXV0aE9rSABSBmF1dGhPaxJWChRjbGllbnRfY29ubmVjdF9lcnJvchgFIA' - 'EoCzIiLmFyYml0ZXIuY2xpZW50LkNsaWVudENvbm5lY3RFcnJvckgAUhJjbGllbnRDb25uZWN0' - 'RXJyb3ISWwoUZXZtX3NpZ25fdHJhbnNhY3Rpb24YAyABKAsyJy5hcmJpdGVyLmV2bS5Fdm1TaW' - 'duVHJhbnNhY3Rpb25SZXNwb25zZUgAUhJldm1TaWduVHJhbnNhY3Rpb24SZAoXZXZtX2FuYWx5' - 'emVfdHJhbnNhY3Rpb24YBCABKAsyKi5hcmJpdGVyLmV2bS5Fdm1BbmFseXplVHJhbnNhY3Rpb2' - '5SZXNwb25zZUgAUhVldm1BbmFseXplVHJhbnNhY3Rpb25CCQoHcGF5bG9hZA=='); + 'Cg5DbGllbnRSZXNwb25zZRIiCgpyZXF1ZXN0X2lkGAcgASgFSAFSCXJlcXVlc3RJZIgBARJGCg' + '5hdXRoX2NoYWxsZW5nZRgBIAEoCzIdLmFyYml0ZXIuY2xpZW50LkF1dGhDaGFsbGVuZ2VIAFIN' + 'YXV0aENoYWxsZW5nZRI9CgthdXRoX3Jlc3VsdBgCIAEoDjIaLmFyYml0ZXIuY2xpZW50LkF1dG' + 'hSZXN1bHRIAFIKYXV0aFJlc3VsdBJbChRldm1fc2lnbl90cmFuc2FjdGlvbhgDIAEoCzInLmFy' + 'Yml0ZXIuZXZtLkV2bVNpZ25UcmFuc2FjdGlvblJlc3BvbnNlSABSEmV2bVNpZ25UcmFuc2FjdG' + 'lvbhJkChdldm1fYW5hbHl6ZV90cmFuc2FjdGlvbhgEIAEoCzIqLmFyYml0ZXIuZXZtLkV2bUFu' + 'YWx5emVUcmFuc2FjdGlvblJlc3BvbnNlSABSFWV2bUFuYWx5emVUcmFuc2FjdGlvbhI9Cgt2YX' + 'VsdF9zdGF0ZRgGIAEoDjIaLmFyYml0ZXIuY2xpZW50LlZhdWx0U3RhdGVIAFIKdmF1bHRTdGF0' + 'ZUIJCgdwYXlsb2FkQg0KC19yZXF1ZXN0X2lk'); diff --git a/useragent/lib/proto/user_agent.pb.dart b/useragent/lib/proto/user_agent.pb.dart index e7e96a1..3b85474 100644 --- a/useragent/lib/proto/user_agent.pb.dart +++ b/useragent/lib/proto/user_agent.pb.dart @@ -105,11 +105,9 @@ class AuthChallengeRequest extends $pb.GeneratedMessage { class AuthChallenge extends $pb.GeneratedMessage { factory AuthChallenge({ - $core.List<$core.int>? pubkey, $core.int? nonce, }) { final result = create(); - if (pubkey != null) result.pubkey = pubkey; if (nonce != null) result.nonce = nonce; return result; } @@ -128,8 +126,6 @@ class AuthChallenge extends $pb.GeneratedMessage { package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'), createEmptyInstance: create) - ..a<$core.List<$core.int>>( - 1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY) ..aI(2, _omitFieldNames ? '' : 'nonce') ..hasRequiredFields = false; @@ -152,21 +148,12 @@ class AuthChallenge extends $pb.GeneratedMessage { $pb.GeneratedMessage.$_defaultFor(create); static AuthChallenge? _defaultInstance; - @$pb.TagNumber(1) - $core.List<$core.int> get pubkey => $_getN(0); - @$pb.TagNumber(1) - set pubkey($core.List<$core.int> value) => $_setBytes(0, value); - @$pb.TagNumber(1) - $core.bool hasPubkey() => $_has(0); - @$pb.TagNumber(1) - void clearPubkey() => $_clearField(1); - @$pb.TagNumber(2) - $core.int get nonce => $_getIZ(1); + $core.int get nonce => $_getIZ(0); @$pb.TagNumber(2) - set nonce($core.int value) => $_setSignedInt32(1, value); + set nonce($core.int value) => $_setSignedInt32(0, value); @$pb.TagNumber(2) - $core.bool hasNonce() => $_has(1); + $core.bool hasNonce() => $_has(0); @$pb.TagNumber(2) void clearNonce() => $_clearField(2); } @@ -228,44 +215,6 @@ class AuthChallengeSolution extends $pb.GeneratedMessage { void clearSignature() => $_clearField(1); } -class AuthOk extends $pb.GeneratedMessage { - factory AuthOk() => create(); - - AuthOk._(); - - factory AuthOk.fromBuffer($core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory AuthOk.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'AuthOk', - package: - const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'), - createEmptyInstance: create) - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - AuthOk clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - AuthOk copyWith(void Function(AuthOk) updates) => - super.copyWith((message) => updates(message as AuthOk)) as AuthOk; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static AuthOk create() => AuthOk._(); - @$core.override - AuthOk createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static AuthOk getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static AuthOk? _defaultInstance; -} - class UnsealStart extends $pb.GeneratedMessage { factory UnsealStart({ $core.List<$core.int>? clientPubkey, @@ -726,6 +675,7 @@ class UserAgentRequest extends $pb.GeneratedMessage { $1.EvmGrantListRequest? evmGrantList, ClientConnectionResponse? clientConnectionResponse, BootstrapEncryptedKey? bootstrapEncryptedKey, + $core.int? id, }) { final result = create(); if (authChallengeRequest != null) @@ -745,6 +695,7 @@ class UserAgentRequest extends $pb.GeneratedMessage { result.clientConnectionResponse = clientConnectionResponse; if (bootstrapEncryptedKey != null) result.bootstrapEncryptedKey = bootstrapEncryptedKey; + if (id != null) result.id = id; return result; } @@ -807,6 +758,7 @@ class UserAgentRequest extends $pb.GeneratedMessage { ..aOM( 12, _omitFieldNames ? '' : 'bootstrapEncryptedKey', subBuilder: BootstrapEncryptedKey.create) + ..aI(14, _omitFieldNames ? '' : 'id') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -990,11 +942,20 @@ class UserAgentRequest extends $pb.GeneratedMessage { void clearBootstrapEncryptedKey() => $_clearField(12); @$pb.TagNumber(12) BootstrapEncryptedKey ensureBootstrapEncryptedKey() => $_ensure(11); + + @$pb.TagNumber(14) + $core.int get id => $_getIZ(12); + @$pb.TagNumber(14) + set id($core.int value) => $_setSignedInt32(12, value); + @$pb.TagNumber(14) + $core.bool hasId() => $_has(12); + @$pb.TagNumber(14) + void clearId() => $_clearField(14); } enum UserAgentResponse_Payload { authChallenge, - authOk, + authResult, unsealStartResponse, unsealResult, vaultState, @@ -1012,7 +973,7 @@ enum UserAgentResponse_Payload { class UserAgentResponse extends $pb.GeneratedMessage { factory UserAgentResponse({ AuthChallenge? authChallenge, - AuthOk? authOk, + AuthResult? authResult, UnsealStartResponse? unsealStartResponse, UnsealResult? unsealResult, VaultState? vaultState, @@ -1024,10 +985,11 @@ class UserAgentResponse extends $pb.GeneratedMessage { ClientConnectionRequest? clientConnectionRequest, ClientConnectionCancel? clientConnectionCancel, BootstrapResult? bootstrapResult, + $core.int? id, }) { final result = create(); if (authChallenge != null) result.authChallenge = authChallenge; - if (authOk != null) result.authOk = authOk; + if (authResult != null) result.authResult = authResult; if (unsealStartResponse != null) result.unsealStartResponse = unsealStartResponse; if (unsealResult != null) result.unsealResult = unsealResult; @@ -1042,6 +1004,7 @@ class UserAgentResponse extends $pb.GeneratedMessage { if (clientConnectionCancel != null) result.clientConnectionCancel = clientConnectionCancel; if (bootstrapResult != null) result.bootstrapResult = bootstrapResult; + if (id != null) result.id = id; return result; } @@ -1057,7 +1020,7 @@ class UserAgentResponse extends $pb.GeneratedMessage { static const $core.Map<$core.int, UserAgentResponse_Payload> _UserAgentResponse_PayloadByTag = { 1: UserAgentResponse_Payload.authChallenge, - 2: UserAgentResponse_Payload.authOk, + 2: UserAgentResponse_Payload.authResult, 3: UserAgentResponse_Payload.unsealStartResponse, 4: UserAgentResponse_Payload.unsealResult, 5: UserAgentResponse_Payload.vaultState, @@ -1079,7 +1042,8 @@ class UserAgentResponse extends $pb.GeneratedMessage { ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) ..aOM(1, _omitFieldNames ? '' : 'authChallenge', subBuilder: AuthChallenge.create) - ..aOM(2, _omitFieldNames ? '' : 'authOk', subBuilder: AuthOk.create) + ..aE(2, _omitFieldNames ? '' : 'authResult', + enumValues: AuthResult.values) ..aOM(3, _omitFieldNames ? '' : 'unsealStartResponse', subBuilder: UnsealStartResponse.create) ..aE(4, _omitFieldNames ? '' : 'unsealResult', @@ -1104,6 +1068,7 @@ class UserAgentResponse extends $pb.GeneratedMessage { subBuilder: ClientConnectionCancel.create) ..aE(13, _omitFieldNames ? '' : 'bootstrapResult', enumValues: BootstrapResult.values) + ..aI(14, _omitFieldNames ? '' : 'id') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -1167,15 +1132,13 @@ class UserAgentResponse extends $pb.GeneratedMessage { AuthChallenge ensureAuthChallenge() => $_ensure(0); @$pb.TagNumber(2) - AuthOk get authOk => $_getN(1); + AuthResult get authResult => $_getN(1); @$pb.TagNumber(2) - set authOk(AuthOk value) => $_setField(2, value); + set authResult(AuthResult value) => $_setField(2, value); @$pb.TagNumber(2) - $core.bool hasAuthOk() => $_has(1); + $core.bool hasAuthResult() => $_has(1); @$pb.TagNumber(2) - void clearAuthOk() => $_clearField(2); - @$pb.TagNumber(2) - AuthOk ensureAuthOk() => $_ensure(1); + void clearAuthResult() => $_clearField(2); @$pb.TagNumber(3) UnsealStartResponse get unsealStartResponse => $_getN(2); @@ -1293,6 +1256,15 @@ class UserAgentResponse extends $pb.GeneratedMessage { $core.bool hasBootstrapResult() => $_has(12); @$pb.TagNumber(13) void clearBootstrapResult() => $_clearField(13); + + @$pb.TagNumber(14) + $core.int get id => $_getIZ(13); + @$pb.TagNumber(14) + set id($core.int value) => $_setSignedInt32(13, value); + @$pb.TagNumber(14) + $core.bool hasId() => $_has(13); + @$pb.TagNumber(14) + void clearId() => $_clearField(14); } const $core.bool _omitFieldNames = diff --git a/useragent/lib/proto/user_agent.pbenum.dart b/useragent/lib/proto/user_agent.pbenum.dart index 41b6c7e..66ff3be 100644 --- a/useragent/lib/proto/user_agent.pbenum.dart +++ b/useragent/lib/proto/user_agent.pbenum.dart @@ -39,6 +39,40 @@ class KeyType extends $pb.ProtobufEnum { const KeyType._(super.value, super.name); } +class AuthResult extends $pb.ProtobufEnum { + static const AuthResult AUTH_RESULT_UNSPECIFIED = + AuthResult._(0, _omitEnumNames ? '' : 'AUTH_RESULT_UNSPECIFIED'); + static const AuthResult AUTH_RESULT_SUCCESS = + AuthResult._(1, _omitEnumNames ? '' : 'AUTH_RESULT_SUCCESS'); + static const AuthResult AUTH_RESULT_INVALID_KEY = + AuthResult._(2, _omitEnumNames ? '' : 'AUTH_RESULT_INVALID_KEY'); + static const AuthResult AUTH_RESULT_INVALID_SIGNATURE = + AuthResult._(3, _omitEnumNames ? '' : 'AUTH_RESULT_INVALID_SIGNATURE'); + static const AuthResult AUTH_RESULT_BOOTSTRAP_REQUIRED = + AuthResult._(4, _omitEnumNames ? '' : 'AUTH_RESULT_BOOTSTRAP_REQUIRED'); + static const AuthResult AUTH_RESULT_TOKEN_INVALID = + AuthResult._(5, _omitEnumNames ? '' : 'AUTH_RESULT_TOKEN_INVALID'); + static const AuthResult AUTH_RESULT_INTERNAL = + AuthResult._(6, _omitEnumNames ? '' : 'AUTH_RESULT_INTERNAL'); + + static const $core.List values = [ + AUTH_RESULT_UNSPECIFIED, + AUTH_RESULT_SUCCESS, + AUTH_RESULT_INVALID_KEY, + AUTH_RESULT_INVALID_SIGNATURE, + AUTH_RESULT_BOOTSTRAP_REQUIRED, + AUTH_RESULT_TOKEN_INVALID, + AUTH_RESULT_INTERNAL, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 6); + static AuthResult? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const AuthResult._(super.value, super.name); +} + class UnsealResult extends $pb.ProtobufEnum { static const UnsealResult UNSEAL_RESULT_UNSPECIFIED = UnsealResult._(0, _omitEnumNames ? '' : 'UNSEAL_RESULT_UNSPECIFIED'); @@ -65,14 +99,15 @@ class UnsealResult extends $pb.ProtobufEnum { } class BootstrapResult extends $pb.ProtobufEnum { - static const BootstrapResult BOOTSTRAP_RESULT_UNSPECIFIED = - BootstrapResult._(0, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_UNSPECIFIED'); + static const BootstrapResult BOOTSTRAP_RESULT_UNSPECIFIED = BootstrapResult._( + 0, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_UNSPECIFIED'); static const BootstrapResult BOOTSTRAP_RESULT_SUCCESS = BootstrapResult._(1, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_SUCCESS'); static const BootstrapResult BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED = - BootstrapResult._(2, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED'); - static const BootstrapResult BOOTSTRAP_RESULT_INVALID_KEY = - BootstrapResult._(3, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_INVALID_KEY'); + BootstrapResult._( + 2, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED'); + static const BootstrapResult BOOTSTRAP_RESULT_INVALID_KEY = BootstrapResult._( + 3, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_INVALID_KEY'); static const $core.List values = [ BOOTSTRAP_RESULT_UNSPECIFIED, diff --git a/useragent/lib/proto/user_agent.pbjson.dart b/useragent/lib/proto/user_agent.pbjson.dart index e9a48c6..b74a873 100644 --- a/useragent/lib/proto/user_agent.pbjson.dart +++ b/useragent/lib/proto/user_agent.pbjson.dart @@ -31,6 +31,28 @@ final $typed_data.Uint8List keyTypeDescriptor = $convert.base64Decode( 'CgdLZXlUeXBlEhgKFEtFWV9UWVBFX1VOU1BFQ0lGSUVEEAASFAoQS0VZX1RZUEVfRUQyNTUxOR' 'ABEhwKGEtFWV9UWVBFX0VDRFNBX1NFQ1AyNTZLMRACEhAKDEtFWV9UWVBFX1JTQRAD'); +@$core.Deprecated('Use authResultDescriptor instead') +const AuthResult$json = { + '1': 'AuthResult', + '2': [ + {'1': 'AUTH_RESULT_UNSPECIFIED', '2': 0}, + {'1': 'AUTH_RESULT_SUCCESS', '2': 1}, + {'1': 'AUTH_RESULT_INVALID_KEY', '2': 2}, + {'1': 'AUTH_RESULT_INVALID_SIGNATURE', '2': 3}, + {'1': 'AUTH_RESULT_BOOTSTRAP_REQUIRED', '2': 4}, + {'1': 'AUTH_RESULT_TOKEN_INVALID', '2': 5}, + {'1': 'AUTH_RESULT_INTERNAL', '2': 6}, + ], +}; + +/// Descriptor for `AuthResult`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List authResultDescriptor = $convert.base64Decode( + 'CgpBdXRoUmVzdWx0EhsKF0FVVEhfUkVTVUxUX1VOU1BFQ0lGSUVEEAASFwoTQVVUSF9SRVNVTF' + 'RfU1VDQ0VTUxABEhsKF0FVVEhfUkVTVUxUX0lOVkFMSURfS0VZEAISIQodQVVUSF9SRVNVTFRf' + 'SU5WQUxJRF9TSUdOQVRVUkUQAxIiCh5BVVRIX1JFU1VMVF9CT09UU1RSQVBfUkVRVUlSRUQQBB' + 'IdChlBVVRIX1JFU1VMVF9UT0tFTl9JTlZBTElEEAUSGAoUQVVUSF9SRVNVTFRfSU5URVJOQUwQ' + 'Bg=='); + @$core.Deprecated('Use unsealResultDescriptor instead') const UnsealResult$json = { '1': 'UnsealResult', @@ -48,6 +70,23 @@ final $typed_data.Uint8List unsealResultDescriptor = $convert.base64Decode( '9SRVNVTFRfU1VDQ0VTUxABEh0KGVVOU0VBTF9SRVNVTFRfSU5WQUxJRF9LRVkQAhIgChxVTlNF' 'QUxfUkVTVUxUX1VOQk9PVFNUUkFQUEVEEAM='); +@$core.Deprecated('Use bootstrapResultDescriptor instead') +const BootstrapResult$json = { + '1': 'BootstrapResult', + '2': [ + {'1': 'BOOTSTRAP_RESULT_UNSPECIFIED', '2': 0}, + {'1': 'BOOTSTRAP_RESULT_SUCCESS', '2': 1}, + {'1': 'BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED', '2': 2}, + {'1': 'BOOTSTRAP_RESULT_INVALID_KEY', '2': 3}, + ], +}; + +/// Descriptor for `BootstrapResult`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List bootstrapResultDescriptor = $convert.base64Decode( + 'Cg9Cb290c3RyYXBSZXN1bHQSIAocQk9PVFNUUkFQX1JFU1VMVF9VTlNQRUNJRklFRBAAEhwKGE' + 'JPT1RTVFJBUF9SRVNVTFRfU1VDQ0VTUxABEikKJUJPT1RTVFJBUF9SRVNVTFRfQUxSRUFEWV9C' + 'T09UU1RSQVBQRUQQAhIgChxCT09UU1RSQVBfUkVTVUxUX0lOVkFMSURfS0VZEAM='); + @$core.Deprecated('Use vaultStateDescriptor instead') const VaultState$json = { '1': 'VaultState', @@ -105,15 +144,16 @@ 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'}, ], + '9': [ + {'1': 1, '2': 2}, + ], }; /// Descriptor for `AuthChallenge`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List authChallengeDescriptor = $convert.base64Decode( - 'Cg1BdXRoQ2hhbGxlbmdlEhYKBnB1YmtleRgBIAEoDFIGcHVia2V5EhQKBW5vbmNlGAIgASgFUg' - 'Vub25jZQ=='); + 'Cg1BdXRoQ2hhbGxlbmdlEhQKBW5vbmNlGAIgASgFUgVub25jZUoECAEQAg=='); @$core.Deprecated('Use authChallengeSolutionDescriptor instead') const AuthChallengeSolution$json = { @@ -127,15 +167,6 @@ const AuthChallengeSolution$json = { final $typed_data.Uint8List authChallengeSolutionDescriptor = $convert.base64Decode( 'ChVBdXRoQ2hhbGxlbmdlU29sdXRpb24SHAoJc2lnbmF0dXJlGAEgASgMUglzaWduYXR1cmU='); -@$core.Deprecated('Use authOkDescriptor instead') -const AuthOk$json = { - '1': 'AuthOk', -}; - -/// Descriptor for `AuthOk`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List authOkDescriptor = - $convert.base64Decode('CgZBdXRoT2s='); - @$core.Deprecated('Use unsealStartDescriptor instead') const UnsealStart$json = { '1': 'UnsealStart', @@ -177,6 +208,22 @@ final $typed_data.Uint8List unsealEncryptedKeyDescriptor = $convert.base64Decode 'QYAiABKAxSCmNpcGhlcnRleHQSJwoPYXNzb2NpYXRlZF9kYXRhGAMgASgMUg5hc3NvY2lhdGVk' 'RGF0YQ=='); +@$core.Deprecated('Use bootstrapEncryptedKeyDescriptor instead') +const BootstrapEncryptedKey$json = { + '1': 'BootstrapEncryptedKey', + '2': [ + {'1': 'nonce', '3': 1, '4': 1, '5': 12, '10': 'nonce'}, + {'1': 'ciphertext', '3': 2, '4': 1, '5': 12, '10': 'ciphertext'}, + {'1': 'associated_data', '3': 3, '4': 1, '5': 12, '10': 'associatedData'}, + ], +}; + +/// Descriptor for `BootstrapEncryptedKey`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List bootstrapEncryptedKeyDescriptor = $convert.base64Decode( + 'ChVCb290c3RyYXBFbmNyeXB0ZWRLZXkSFAoFbm9uY2UYASABKAxSBW5vbmNlEh4KCmNpcGhlcn' + 'RleHQYAiABKAxSCmNpcGhlcnRleHQSJwoPYXNzb2NpYXRlZF9kYXRhGAMgASgMUg5hc3NvY2lh' + 'dGVkRGF0YQ=='); + @$core.Deprecated('Use clientConnectionRequestDescriptor instead') const ClientConnectionRequest$json = { '1': 'ClientConnectionRequest', @@ -216,6 +263,7 @@ final $typed_data.Uint8List clientConnectionCancelDescriptor = const UserAgentRequest$json = { '1': 'UserAgentRequest', '2': [ + {'1': 'id', '3': 14, '4': 1, '5': 5, '10': 'id'}, { '1': 'auth_challenge_request', '3': 1, @@ -315,6 +363,15 @@ const UserAgentRequest$json = { '9': 0, '10': 'clientConnectionResponse' }, + { + '1': 'bootstrap_encrypted_key', + '3': 12, + '4': 1, + '5': 11, + '6': '.arbiter.user_agent.BootstrapEncryptedKey', + '9': 0, + '10': 'bootstrapEncryptedKey' + }, ], '8': [ {'1': 'payload'}, @@ -323,28 +380,32 @@ const UserAgentRequest$json = { /// Descriptor for `UserAgentRequest`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List userAgentRequestDescriptor = $convert.base64Decode( - 'ChBVc2VyQWdlbnRSZXF1ZXN0EmAKFmF1dGhfY2hhbGxlbmdlX3JlcXVlc3QYASABKAsyKC5hcm' - 'JpdGVyLnVzZXJfYWdlbnQuQXV0aENoYWxsZW5nZVJlcXVlc3RIAFIUYXV0aENoYWxsZW5nZVJl' - 'cXVlc3QSYwoXYXV0aF9jaGFsbGVuZ2Vfc29sdXRpb24YAiABKAsyKS5hcmJpdGVyLnVzZXJfYW' - 'dlbnQuQXV0aENoYWxsZW5nZVNvbHV0aW9uSABSFWF1dGhDaGFsbGVuZ2VTb2x1dGlvbhJECgx1' - 'bnNlYWxfc3RhcnQYAyABKAsyHy5hcmJpdGVyLnVzZXJfYWdlbnQuVW5zZWFsU3RhcnRIAFILdW' - '5zZWFsU3RhcnQSWgoUdW5zZWFsX2VuY3J5cHRlZF9rZXkYBCABKAsyJi5hcmJpdGVyLnVzZXJf' - 'YWdlbnQuVW5zZWFsRW5jcnlwdGVkS2V5SABSEnVuc2VhbEVuY3J5cHRlZEtleRJEChFxdWVyeV' - '92YXVsdF9zdGF0ZRgFIAEoCzIWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eUgAUg9xdWVyeVZhdWx0' - 'U3RhdGUSRAoRZXZtX3dhbGxldF9jcmVhdGUYBiABKAsyFi5nb29nbGUucHJvdG9idWYuRW1wdH' - 'lIAFIPZXZtV2FsbGV0Q3JlYXRlEkAKD2V2bV93YWxsZXRfbGlzdBgHIAEoCzIWLmdvb2dsZS5w' - 'cm90b2J1Zi5FbXB0eUgAUg1ldm1XYWxsZXRMaXN0Ek4KEGV2bV9ncmFudF9jcmVhdGUYCCABKA' - 'syIi5hcmJpdGVyLmV2bS5Fdm1HcmFudENyZWF0ZVJlcXVlc3RIAFIOZXZtR3JhbnRDcmVhdGUS' - 'TgoQZXZtX2dyYW50X2RlbGV0ZRgJIAEoCzIiLmFyYml0ZXIuZXZtLkV2bUdyYW50RGVsZXRlUm' - 'VxdWVzdEgAUg5ldm1HcmFudERlbGV0ZRJICg5ldm1fZ3JhbnRfbGlzdBgKIAEoCzIgLmFyYml0' - 'ZXIuZXZtLkV2bUdyYW50TGlzdFJlcXVlc3RIAFIMZXZtR3JhbnRMaXN0EmwKGmNsaWVudF9jb2' - '5uZWN0aW9uX3Jlc3BvbnNlGAsgASgLMiwuYXJiaXRlci51c2VyX2FnZW50LkNsaWVudENvbm5l' - 'Y3Rpb25SZXNwb25zZUgAUhhjbGllbnRDb25uZWN0aW9uUmVzcG9uc2VCCQoHcGF5bG9hZA=='); + 'ChBVc2VyQWdlbnRSZXF1ZXN0Eg4KAmlkGA4gASgFUgJpZBJgChZhdXRoX2NoYWxsZW5nZV9yZX' + 'F1ZXN0GAEgASgLMiguYXJiaXRlci51c2VyX2FnZW50LkF1dGhDaGFsbGVuZ2VSZXF1ZXN0SABS' + 'FGF1dGhDaGFsbGVuZ2VSZXF1ZXN0EmMKF2F1dGhfY2hhbGxlbmdlX3NvbHV0aW9uGAIgASgLMi' + 'kuYXJiaXRlci51c2VyX2FnZW50LkF1dGhDaGFsbGVuZ2VTb2x1dGlvbkgAUhVhdXRoQ2hhbGxl' + 'bmdlU29sdXRpb24SRAoMdW5zZWFsX3N0YXJ0GAMgASgLMh8uYXJiaXRlci51c2VyX2FnZW50Ll' + 'Vuc2VhbFN0YXJ0SABSC3Vuc2VhbFN0YXJ0EloKFHVuc2VhbF9lbmNyeXB0ZWRfa2V5GAQgASgL' + 'MiYuYXJiaXRlci51c2VyX2FnZW50LlVuc2VhbEVuY3J5cHRlZEtleUgAUhJ1bnNlYWxFbmNyeX' + 'B0ZWRLZXkSRAoRcXVlcnlfdmF1bHRfc3RhdGUYBSABKAsyFi5nb29nbGUucHJvdG9idWYuRW1w' + 'dHlIAFIPcXVlcnlWYXVsdFN0YXRlEkQKEWV2bV93YWxsZXRfY3JlYXRlGAYgASgLMhYuZ29vZ2' + 'xlLnByb3RvYnVmLkVtcHR5SABSD2V2bVdhbGxldENyZWF0ZRJACg9ldm1fd2FsbGV0X2xpc3QY' + 'ByABKAsyFi5nb29nbGUucHJvdG9idWYuRW1wdHlIAFINZXZtV2FsbGV0TGlzdBJOChBldm1fZ3' + 'JhbnRfY3JlYXRlGAggASgLMiIuYXJiaXRlci5ldm0uRXZtR3JhbnRDcmVhdGVSZXF1ZXN0SABS' + 'DmV2bUdyYW50Q3JlYXRlEk4KEGV2bV9ncmFudF9kZWxldGUYCSABKAsyIi5hcmJpdGVyLmV2bS' + '5Fdm1HcmFudERlbGV0ZVJlcXVlc3RIAFIOZXZtR3JhbnREZWxldGUSSAoOZXZtX2dyYW50X2xp' + 'c3QYCiABKAsyIC5hcmJpdGVyLmV2bS5Fdm1HcmFudExpc3RSZXF1ZXN0SABSDGV2bUdyYW50TG' + 'lzdBJsChpjbGllbnRfY29ubmVjdGlvbl9yZXNwb25zZRgLIAEoCzIsLmFyYml0ZXIudXNlcl9h' + 'Z2VudC5DbGllbnRDb25uZWN0aW9uUmVzcG9uc2VIAFIYY2xpZW50Q29ubmVjdGlvblJlc3Bvbn' + 'NlEmMKF2Jvb3RzdHJhcF9lbmNyeXB0ZWRfa2V5GAwgASgLMikuYXJiaXRlci51c2VyX2FnZW50' + 'LkJvb3RzdHJhcEVuY3J5cHRlZEtleUgAUhVib290c3RyYXBFbmNyeXB0ZWRLZXlCCQoHcGF5bG' + '9hZA=='); @$core.Deprecated('Use userAgentResponseDescriptor instead') const UserAgentResponse$json = { '1': 'UserAgentResponse', '2': [ + {'1': 'id', '3': 14, '4': 1, '5': 5, '9': 1, '10': 'id', '17': true}, { '1': 'auth_challenge', '3': 1, @@ -355,13 +416,13 @@ const UserAgentResponse$json = { '10': 'authChallenge' }, { - '1': 'auth_ok', + '1': 'auth_result', '3': 2, '4': 1, - '5': 11, - '6': '.arbiter.user_agent.AuthOk', + '5': 14, + '6': '.arbiter.user_agent.AuthResult', '9': 0, - '10': 'authOk' + '10': 'authResult' }, { '1': 'unseal_start_response', @@ -453,30 +514,42 @@ const UserAgentResponse$json = { '9': 0, '10': 'clientConnectionCancel' }, + { + '1': 'bootstrap_result', + '3': 13, + '4': 1, + '5': 14, + '6': '.arbiter.user_agent.BootstrapResult', + '9': 0, + '10': 'bootstrapResult' + }, ], '8': [ {'1': 'payload'}, + {'1': '_id'}, ], }; /// Descriptor for `UserAgentResponse`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List userAgentResponseDescriptor = $convert.base64Decode( - 'ChFVc2VyQWdlbnRSZXNwb25zZRJKCg5hdXRoX2NoYWxsZW5nZRgBIAEoCzIhLmFyYml0ZXIudX' - 'Nlcl9hZ2VudC5BdXRoQ2hhbGxlbmdlSABSDWF1dGhDaGFsbGVuZ2USNQoHYXV0aF9vaxgCIAEo' - 'CzIaLmFyYml0ZXIudXNlcl9hZ2VudC5BdXRoT2tIAFIGYXV0aE9rEl0KFXVuc2VhbF9zdGFydF' - '9yZXNwb25zZRgDIAEoCzInLmFyYml0ZXIudXNlcl9hZ2VudC5VbnNlYWxTdGFydFJlc3BvbnNl' - 'SABSE3Vuc2VhbFN0YXJ0UmVzcG9uc2USRwoNdW5zZWFsX3Jlc3VsdBgEIAEoDjIgLmFyYml0ZX' - 'IudXNlcl9hZ2VudC5VbnNlYWxSZXN1bHRIAFIMdW5zZWFsUmVzdWx0EkEKC3ZhdWx0X3N0YXRl' - 'GAUgASgOMh4uYXJiaXRlci51c2VyX2FnZW50LlZhdWx0U3RhdGVIAFIKdmF1bHRTdGF0ZRJPCh' - 'Fldm1fd2FsbGV0X2NyZWF0ZRgGIAEoCzIhLmFyYml0ZXIuZXZtLldhbGxldENyZWF0ZVJlc3Bv' - 'bnNlSABSD2V2bVdhbGxldENyZWF0ZRJJCg9ldm1fd2FsbGV0X2xpc3QYByABKAsyHy5hcmJpdG' - 'VyLmV2bS5XYWxsZXRMaXN0UmVzcG9uc2VIAFINZXZtV2FsbGV0TGlzdBJPChBldm1fZ3JhbnRf' - 'Y3JlYXRlGAggASgLMiMuYXJiaXRlci5ldm0uRXZtR3JhbnRDcmVhdGVSZXNwb25zZUgAUg5ldm' - '1HcmFudENyZWF0ZRJPChBldm1fZ3JhbnRfZGVsZXRlGAkgASgLMiMuYXJiaXRlci5ldm0uRXZt' - 'R3JhbnREZWxldGVSZXNwb25zZUgAUg5ldm1HcmFudERlbGV0ZRJJCg5ldm1fZ3JhbnRfbGlzdB' - 'gKIAEoCzIhLmFyYml0ZXIuZXZtLkV2bUdyYW50TGlzdFJlc3BvbnNlSABSDGV2bUdyYW50TGlz' - 'dBJpChljbGllbnRfY29ubmVjdGlvbl9yZXF1ZXN0GAsgASgLMisuYXJiaXRlci51c2VyX2FnZW' - '50LkNsaWVudENvbm5lY3Rpb25SZXF1ZXN0SABSF2NsaWVudENvbm5lY3Rpb25SZXF1ZXN0EmYK' - 'GGNsaWVudF9jb25uZWN0aW9uX2NhbmNlbBgMIAEoCzIqLmFyYml0ZXIudXNlcl9hZ2VudC5DbG' - 'llbnRDb25uZWN0aW9uQ2FuY2VsSABSFmNsaWVudENvbm5lY3Rpb25DYW5jZWxCCQoHcGF5bG9h' - 'ZA=='); + 'ChFVc2VyQWdlbnRSZXNwb25zZRITCgJpZBgOIAEoBUgBUgJpZIgBARJKCg5hdXRoX2NoYWxsZW' + '5nZRgBIAEoCzIhLmFyYml0ZXIudXNlcl9hZ2VudC5BdXRoQ2hhbGxlbmdlSABSDWF1dGhDaGFs' + 'bGVuZ2USQQoLYXV0aF9yZXN1bHQYAiABKA4yHi5hcmJpdGVyLnVzZXJfYWdlbnQuQXV0aFJlc3' + 'VsdEgAUgphdXRoUmVzdWx0El0KFXVuc2VhbF9zdGFydF9yZXNwb25zZRgDIAEoCzInLmFyYml0' + 'ZXIudXNlcl9hZ2VudC5VbnNlYWxTdGFydFJlc3BvbnNlSABSE3Vuc2VhbFN0YXJ0UmVzcG9uc2' + 'USRwoNdW5zZWFsX3Jlc3VsdBgEIAEoDjIgLmFyYml0ZXIudXNlcl9hZ2VudC5VbnNlYWxSZXN1' + 'bHRIAFIMdW5zZWFsUmVzdWx0EkEKC3ZhdWx0X3N0YXRlGAUgASgOMh4uYXJiaXRlci51c2VyX2' + 'FnZW50LlZhdWx0U3RhdGVIAFIKdmF1bHRTdGF0ZRJPChFldm1fd2FsbGV0X2NyZWF0ZRgGIAEo' + 'CzIhLmFyYml0ZXIuZXZtLldhbGxldENyZWF0ZVJlc3BvbnNlSABSD2V2bVdhbGxldENyZWF0ZR' + 'JJCg9ldm1fd2FsbGV0X2xpc3QYByABKAsyHy5hcmJpdGVyLmV2bS5XYWxsZXRMaXN0UmVzcG9u' + 'c2VIAFINZXZtV2FsbGV0TGlzdBJPChBldm1fZ3JhbnRfY3JlYXRlGAggASgLMiMuYXJiaXRlci' + '5ldm0uRXZtR3JhbnRDcmVhdGVSZXNwb25zZUgAUg5ldm1HcmFudENyZWF0ZRJPChBldm1fZ3Jh' + 'bnRfZGVsZXRlGAkgASgLMiMuYXJiaXRlci5ldm0uRXZtR3JhbnREZWxldGVSZXNwb25zZUgAUg' + '5ldm1HcmFudERlbGV0ZRJJCg5ldm1fZ3JhbnRfbGlzdBgKIAEoCzIhLmFyYml0ZXIuZXZtLkV2' + 'bUdyYW50TGlzdFJlc3BvbnNlSABSDGV2bUdyYW50TGlzdBJpChljbGllbnRfY29ubmVjdGlvbl' + '9yZXF1ZXN0GAsgASgLMisuYXJiaXRlci51c2VyX2FnZW50LkNsaWVudENvbm5lY3Rpb25SZXF1' + 'ZXN0SABSF2NsaWVudENvbm5lY3Rpb25SZXF1ZXN0EmYKGGNsaWVudF9jb25uZWN0aW9uX2Nhbm' + 'NlbBgMIAEoCzIqLmFyYml0ZXIudXNlcl9hZ2VudC5DbGllbnRDb25uZWN0aW9uQ2FuY2VsSABS' + 'FmNsaWVudENvbm5lY3Rpb25DYW5jZWwSUAoQYm9vdHN0cmFwX3Jlc3VsdBgNIAEoDjIjLmFyYm' + 'l0ZXIudXNlcl9hZ2VudC5Cb290c3RyYXBSZXN1bHRIAFIPYm9vdHN0cmFwUmVzdWx0QgkKB3Bh' + 'eWxvYWRCBQoDX2lk'); diff --git a/useragent/lib/providers/vault_state.dart b/useragent/lib/providers/vault_state.dart index de02c5e..edb189e 100644 --- a/useragent/lib/providers/vault_state.dart +++ b/useragent/lib/providers/vault_state.dart @@ -13,9 +13,7 @@ Future vaultState(Ref ref) async { return null; } - await conn.send(UserAgentRequest(queryVaultState: Empty())); - - final resp = await conn.receive(); + final resp = await conn.request(UserAgentRequest(queryVaultState: Empty())); if (resp.whichPayload() != UserAgentResponse_Payload.vaultState) { talker.warning('Expected vault state response, got ${resp.whichPayload()}'); return null; diff --git a/useragent/lib/providers/vault_state.g.dart b/useragent/lib/providers/vault_state.g.dart index aa4f953..7d0bd98 100644 --- a/useragent/lib/providers/vault_state.g.dart +++ b/useragent/lib/providers/vault_state.g.dart @@ -46,4 +46,4 @@ final class VaultStateProvider } } -String _$vaultStateHash() => r'1fd975a9661de1f62beef9eb1c7c439f377a8b88'; +String _$vaultStateHash() => r'97085e49bc3a296e36fa6c04a8f4c9abafac0835'; -- 2.49.1 From 95be080acc25a2512c874cba709ba1cb7696e3c2 Mon Sep 17 00:00:00 2001 From: hdbg Date: Thu, 19 Mar 2026 00:19:55 +0100 Subject: [PATCH 6/8] feat(useragent): showing auth error when something went wrong --- useragent/lib/features/connection/auth.dart | 58 ++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/useragent/lib/features/connection/auth.dart b/useragent/lib/features/connection/auth.dart index fa95e4e..937285a 100644 --- a/useragent/lib/features/connection/auth.dart +++ b/useragent/lib/features/connection/auth.dart @@ -9,13 +9,49 @@ import 'package:arbiter/proto/user_agent.pb.dart'; import 'package:grpc/grpc.dart'; import 'package:mtcore/markettakers.dart'; +class AuthorizationException implements Exception { + const AuthorizationException(this.result); + + final AuthResult result; + + String get message => switch (result) { + AuthResult.AUTH_RESULT_INVALID_KEY => + 'Authentication failed: this device key is not registered on the server.', + AuthResult.AUTH_RESULT_INVALID_SIGNATURE => + 'Authentication failed: the server rejected the signature for this device key.', + AuthResult.AUTH_RESULT_BOOTSTRAP_REQUIRED => + 'Authentication failed: the server requires bootstrap before this device can connect.', + AuthResult.AUTH_RESULT_TOKEN_INVALID => + 'Authentication failed: the bootstrap token is invalid.', + AuthResult.AUTH_RESULT_INTERNAL => + 'Authentication failed: the server hit an internal error.', + AuthResult.AUTH_RESULT_UNSPECIFIED => + 'Authentication failed: the server returned an unspecified auth error.', + AuthResult.AUTH_RESULT_SUCCESS => 'Authentication succeeded.', + _ => 'Authentication failed: ${result.name}.', + }; + + @override + String toString() => message; +} + +class ConnectionException implements Exception { + const ConnectionException(this.message); + + final String message; + + @override + String toString() => message; +} + Future connectAndAuthorize( StoredServerInfo serverInfo, KeyHandle key, { String? bootstrapToken, }) async { + Connection? connection; try { - final connection = await _connect(serverInfo); + connection = await _connect(serverInfo); talker.info( 'Connected to server at ${serverInfo.address}:${serverInfo.port}', ); @@ -40,14 +76,14 @@ Future connectAndAuthorize( if (response.hasAuthResult()) { if (response.authResult != AuthResult.AUTH_RESULT_SUCCESS) { - throw Exception('Authentication failed: ${response.authResult}'); + throw AuthorizationException(response.authResult); } talker.info('Authentication successful, connection established'); return connection; } if (!response.hasAuthChallenge()) { - throw Exception( + throw ConnectionException( 'Expected AuthChallengeResponse, got ${response.whichPayload()}', ); } @@ -65,18 +101,28 @@ Future connectAndAuthorize( talker.info('Sent auth challenge solution, waiting for server response...'); if (!solutionResponse.hasAuthResult()) { - throw Exception( + throw ConnectionException( 'Expected AuthChallengeSolutionResponse, got ${solutionResponse.whichPayload()}', ); } if (solutionResponse.authResult != AuthResult.AUTH_RESULT_SUCCESS) { - throw Exception('Authentication failed: ${solutionResponse.authResult}'); + throw AuthorizationException(solutionResponse.authResult); } talker.info('Authentication successful, connection established'); return connection; + } on AuthorizationException { + await connection?.close(); + rethrow; + } on GrpcError catch (error) { + await connection?.close(); + throw ConnectionException('Failed to connect to server: ${error.message}'); } catch (e) { - throw Exception('Failed to connect to server: $e'); + await connection?.close(); + if (e is ConnectionException) { + rethrow; + } + throw ConnectionException('Failed to connect to server: $e'); } } -- 2.49.1 From 4d5ce9cefae8b06d3c4b4416b78a85752337b09d Mon Sep 17 00:00:00 2001 From: hdbg Date: Thu, 19 Mar 2026 00:29:06 +0100 Subject: [PATCH 7/8] docs: document explicit AuthResult enums and request multiplexing --- IMPLEMENTATION.md | 26 +++++++++++ server/crates/arbiter-proto/src/transport.rs | 46 +++++++++++++++----- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index f71de5a..4718707 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -6,6 +6,20 @@ This document covers concrete technology choices and dependencies. For the archi ## Client Connection Flow +### Authentication Result Semantics + +Authentication no longer uses an implicit success-only response shape. Both `client` and `user-agent` return explicit auth status enums over the wire. + +- **Client:** `AuthResult` may return `SUCCESS`, `INVALID_KEY`, `INVALID_SIGNATURE`, `APPROVAL_DENIED`, `NO_USER_AGENTS_ONLINE`, or `INTERNAL` +- **User-agent:** `AuthResult` may return `SUCCESS`, `INVALID_KEY`, `INVALID_SIGNATURE`, `BOOTSTRAP_REQUIRED`, `TOKEN_INVALID`, or `INTERNAL` + +This makes transport-level failures and actor/domain-level auth failures distinct: + +- **Transport/protocol failures** are surfaced as stream/status errors +- **Authentication failures** are surfaced as successful protocol responses carrying an explicit auth status + +Clients are expected to handle these status codes directly and present the concrete failure reason to the user. + ### New Client Approval When a client whose public key is not yet in the database connects, all connected user agents are asked to approve the connection. The first agent to respond determines the outcome; remaining requests are cancelled via a watch channel. @@ -68,9 +82,21 @@ The `program_client.nonce` column stores the **next usable nonce** — i.e. it i ## Communication - **Protocol:** gRPC with Protocol Buffers +- **Request/response matching:** multiplexed over a single bidirectional stream using per-connection request IDs - **Server identity distribution:** `ServerInfo` protobuf struct containing the TLS public key fingerprint - **Future consideration:** grpc-web lacks bidirectional stream support, so a browser-based wallet may require protojson over WebSocket +### Request Multiplexing + +Both `client` and `user-agent` connections support multiple in-flight requests over one gRPC bidi stream. + +- Every request carries a monotonically increasing request ID +- Every normal response echoes the request ID it corresponds to +- Out-of-band server messages omit the response ID entirely +- The server rejects already-seen request IDs at the transport adapter boundary before business logic sees the message + +This keeps request correlation entirely in transport/client connection code while leaving actor and domain handlers unaware of request IDs. + --- ## EVM Policy Engine diff --git a/server/crates/arbiter-proto/src/transport.rs b/server/crates/arbiter-proto/src/transport.rs index b31aa61..25259c3 100644 --- a/server/crates/arbiter-proto/src/transport.rs +++ b/server/crates/arbiter-proto/src/transport.rs @@ -1,29 +1,49 @@ //! Transport-facing abstractions shared by protocol/session code. //! -//! This module defines a small duplex interface, [`Bi`], that actors and other +//! This module defines a small set of transport traits that actors and other //! protocol code can depend on without knowing anything about the concrete //! transport underneath. //! -//! [`Bi`] is intentionally minimal and transport-agnostic: -//! - [`Bi::recv`] yields inbound messages -//! - [`Bi::send`] accepts outbound messages +//! The abstraction is split into: +//! - [`Sender`] for outbound delivery +//! - [`Receiver`] for inbound delivery +//! - [`Bi`] as the combined duplex form (`Sender + Receiver`) +//! +//! This split lets code depend only on the half it actually needs. For +//! example, some actor/session code only sends out-of-band messages, while +//! auth/state-machine code may need full duplex access. +//! +//! [`Bi`] remains intentionally minimal and transport-agnostic: +//! - [`Receiver::recv`] yields inbound messages +//! - [`Sender::send`] accepts outbound messages //! //! Transport-specific adapters, including protobuf or gRPC bridges, live in the //! crates that own those boundaries rather than in `arbiter-proto`. //! +//! [`Bi`] deliberately does not model request/response correlation. Some +//! transports may carry multiplexed request/response traffic, some may emit +//! out-of-band messages, and some may be one-message-at-a-time state machines. +//! Correlation concerns such as request IDs, pending response maps, and +//! out-of-band routing belong in the adapter or connection layer built on top +//! of [`Bi`], not in this abstraction itself. +//! //! # Generic Ordering Rule //! //! This module consistently uses `Inbound` first and `Outbound` second in //! generic parameter lists. //! -//! For [`Bi`], that means `Bi`: +//! For [`Receiver`], [`Sender`], and [`Bi`], this means: +//! - `Receiver` +//! - `Sender` +//! - `Bi` +//! +//! Concretely, for [`Bi`]: //! - `recv() -> Option` //! - `send(Outbound)` //! -//! [`expect_message`] is a small helper for request/response style flows: it -//! reads one inbound message from a transport and extracts a typed value from -//! it, failing if the channel closes or the message shape is not what the -//! caller expected. +//! [`expect_message`] is a small helper for linear protocol steps: it reads one +//! inbound message from a transport and extracts a typed value from it, failing +//! if the channel closes or the message shape is not what the caller expected. //! //! [`DummyTransport`] is a no-op implementation useful for tests and local //! actor execution where no real stream exists. @@ -75,9 +95,15 @@ pub trait Receiver: Send + Sync { /// Minimal bidirectional transport abstraction used by protocol code. /// -/// `Bi` models a duplex channel with: +/// `Bi` is the combined duplex form of [`Sender`] and +/// [`Receiver`]. +/// +/// It models a channel with: /// - inbound items of type `Inbound` read via [`Bi::recv`] /// - outbound items of type `Outbound` written via [`Bi::send`] +/// +/// It does not imply request/response sequencing, one-at-a-time exchange, or +/// any built-in correlation mechanism between inbound and outbound items. pub trait Bi: Sender + Receiver + Send + Sync {} pub trait SplittableBi: Bi { -- 2.49.1 From bb6879d6c2fe1250684fefcaea467b4503d0d814 Mon Sep 17 00:00:00 2001 From: hdbg Date: Thu, 19 Mar 2026 00:42:42 +0100 Subject: [PATCH 8/8] housekeeping(server): fixed clippy warns --- .woodpecker/server-lint.yaml | 2 +- .../src/actors/user_agent/auth/state.rs | 3 +-- .../arbiter-server/src/actors/user_agent/mod.rs | 16 +++++----------- .../src/actors/user_agent/session.rs | 17 ++++++++++------- .../src/actors/user_agent/session/connection.rs | 13 +++++-------- server/crates/arbiter-server/src/grpc/client.rs | 1 - server/crates/arbiter-server/src/grpc/mod.rs | 4 +--- server/crates/arbiter-server/src/lib.rs | 1 + 8 files changed, 24 insertions(+), 33 deletions(-) diff --git a/.woodpecker/server-lint.yaml b/.woodpecker/server-lint.yaml index 719f3a1..3b74047 100644 --- a/.woodpecker/server-lint.yaml +++ b/.woodpecker/server-lint.yaml @@ -22,4 +22,4 @@ steps: - apt-get update && apt-get install -y pkg-config - mise install rust - mise install protoc - - mise exec rust -- cargo clippy --all-targets --all-features -- -D warnings \ No newline at end of file + - mise exec rust -- cargo clippy --all -- -D warnings \ No newline at end of file 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 index 2fdd048..c422589 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs @@ -1,4 +1,3 @@ -use alloy::transports::Transport; use arbiter_proto::transport::Bi; use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update}; use diesel_async::RunQueryDsl; @@ -8,7 +7,7 @@ use super::Error; use crate::{ actors::{ bootstrap::ConsumeToken, - user_agent::{AuthPublicKey, OutOfBand, UserAgentConnection, auth::Outbound}, + user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound}, }, db::schema, }; diff --git a/server/crates/arbiter-server/src/actors/user_agent/mod.rs b/server/crates/arbiter-server/src/actors/user_agent/mod.rs index 7f980b8..7f454b7 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/mod.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/mod.rs @@ -1,13 +1,7 @@ -use alloy::primitives::Address; -use arbiter_proto::transport::{Bi, Sender}; -use kameo::actor::Spawn as _; -use tracing::{error, info}; use crate::{ - actors::{GlobalActors, evm}, + actors::GlobalActors, db::{self, models::KeyType}, - evm::policies::SharedGrantSettings, - evm::policies::{Grant, SpecificGrant}, }; /// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake. @@ -56,20 +50,20 @@ impl TryFrom<(KeyType, Vec)> for AuthPublicKey { KeyType::Ed25519 => { let bytes: [u8; 32] = bytes.try_into().map_err(|_| "invalid Ed25519 key length")?; let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes) - .map_err(|e| "invalid Ed25519 key")?; + .map_err(|_e| "invalid Ed25519 key")?; Ok(AuthPublicKey::Ed25519(key)) } KeyType::EcdsaSecp256k1 => { let point = - k256::EncodedPoint::from_bytes(&bytes).map_err(|e| "invalid ECDSA key")?; + k256::EncodedPoint::from_bytes(&bytes).map_err(|_e| "invalid ECDSA key")?; let key = k256::ecdsa::VerifyingKey::from_encoded_point(&point) - .map_err(|e| "invalid ECDSA key")?; + .map_err(|_e| "invalid ECDSA key")?; Ok(AuthPublicKey::EcdsaSecp256k1(key)) } KeyType::Rsa => { use rsa::pkcs8::DecodePublicKey as _; let key = rsa::RsaPublicKey::from_public_key_der(&bytes) - .map_err(|e| "invalid RSA key")?; + .map_err(|_e| "invalid RSA key")?; Ok(AuthPublicKey::Rsa(key)) } } diff --git a/server/crates/arbiter-server/src/actors/user_agent/session.rs b/server/crates/arbiter-server/src/actors/user_agent/session.rs index 398b09f..6463479 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session.rs @@ -1,12 +1,12 @@ -use std::{borrow::Cow, convert::Infallible}; +use std::borrow::Cow; use arbiter_proto::transport::Sender; use async_trait::async_trait; use ed25519_dalek::VerifyingKey; -use kameo::{Actor, messages, prelude::Context}; +use kameo::{Actor, messages}; use thiserror::Error; -use tokio::{select, sync::watch}; -use tracing::{error, info}; +use tokio::sync::watch; +use tracing::error; use crate::actors::{ router::RegisterUserAgent, @@ -36,6 +36,7 @@ impl Error { pub struct UserAgentSession { props: UserAgentConnection, state: UserAgentStateMachine, + #[allow(dead_code, reason = "The session keeps ownership of the outbound transport even before the state-machine flow starts using it directly")] sender: Box>, } @@ -82,13 +83,15 @@ impl UserAgentSession { #[messages] impl UserAgentSession { - #[message(ctx)] + #[message] pub async fn request_new_client_approval( &mut self, client_pubkey: VerifyingKey, - mut cancel_flag: watch::Receiver<()>, - ctx: &mut Context>, + cancel_flag: watch::Receiver<()>, ) -> Result { + // temporary use to make clippy happy while we refactor this flow + dbg!(client_pubkey); + dbg!(cancel_flag); todo!("Think about refactoring it to state-machine based flow, as we already have one") } } 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 index 364dbf4..44b47c3 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs @@ -17,13 +17,10 @@ use crate::{ Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants, }, keyholder::{self, Bootstrap, TryUnseal}, - user_agent::{ - OutOfBand, - session::{ + user_agent::session::{ UserAgentSession, state::{UnsealContext, UserAgentEvents, UserAgentStates}, }, - }, }, safe_cell::SafeCellHandle as _, }; @@ -139,7 +136,7 @@ impl UserAgentSession { self.transition(UserAgentEvents::ReceivedInvalidKey)?; return Err(UnsealError::InvalidKey); } - Err(err) => { + Err(_err) => { return Err(Error::internal("Failed to take unseal secret").into()); } }; @@ -263,7 +260,7 @@ impl UserAgentSession { Ok(state) => state, Err(err) => { error!(?err, actor = "useragent", "keyholder.query.failed"); - return Err(Error::internal("Vault is in broken state").into()); + return Err(Error::internal("Vault is in broken state")); } }; @@ -276,13 +273,13 @@ impl UserAgentSession { #[message] pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result { match self.props.actors.evm.ask(Generate {}).await { - Ok(address) => return Ok(address), + 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"); - return Err(Error::internal("EVM actor unreachable")); + Err(Error::internal("EVM actor unreachable")) } } } diff --git a/server/crates/arbiter-server/src/grpc/client.rs b/server/crates/arbiter-server/src/grpc/client.rs index 653c7a8..2fb1d24 100644 --- a/server/crates/arbiter-server/src/grpc/client.rs +++ b/server/crates/arbiter-server/src/grpc/client.rs @@ -132,7 +132,6 @@ pub async fn start(conn: ClientConnection, mut bi: GrpcBi