From f6f4f81acb9bb3e80e64b4d782434bdd76213ba1 Mon Sep 17 00:00:00 2001 From: hdbg Date: Sun, 29 Mar 2026 12:47:27 +0200 Subject: [PATCH] refactor(server, protocol): split big message files into smaller and domain-based --- protobufs/user_agent.proto | 197 +-------- protobufs/user_agent/auth.proto | 48 +++ protobufs/user_agent/evm.proto | 26 ++ protobufs/user_agent/sdk_client.proto | 100 +++++ protobufs/user_agent/vault/bootstrap.proto | 24 ++ protobufs/user_agent/vault/unseal.proto | 37 ++ protobufs/user_agent/vault/vault.proto | 31 ++ server/crates/arbiter-proto/src/lib.rs | 24 ++ .../arbiter-server/src/grpc/user_agent.rs | 399 +----------------- .../src/grpc/user_agent/auth.rs | 60 +-- .../arbiter-server/src/grpc/user_agent/evm.rs | 170 ++++++++ .../src/grpc/user_agent/inbound.rs | 6 +- .../src/grpc/user_agent/outbound.rs | 4 +- .../src/grpc/user_agent/sdk_client.rs | 190 +++++++++ .../src/grpc/user_agent/vault.rs | 180 ++++++++ useragent/macos/Podfile.lock | 4 +- 16 files changed, 902 insertions(+), 598 deletions(-) create mode 100644 protobufs/user_agent/auth.proto create mode 100644 protobufs/user_agent/evm.proto create mode 100644 protobufs/user_agent/sdk_client.proto create mode 100644 protobufs/user_agent/vault/bootstrap.proto create mode 100644 protobufs/user_agent/vault/unseal.proto create mode 100644 protobufs/user_agent/vault/vault.proto create mode 100644 server/crates/arbiter-server/src/grpc/user_agent/evm.rs create mode 100644 server/crates/arbiter-server/src/grpc/user_agent/sdk_client.rs create mode 100644 server/crates/arbiter-server/src/grpc/user_agent/vault.rs diff --git a/protobufs/user_agent.proto b/protobufs/user_agent.proto index 79e0f2c..20fe81c 100644 --- a/protobufs/user_agent.proto +++ b/protobufs/user_agent.proto @@ -2,198 +2,27 @@ syntax = "proto3"; package arbiter.user_agent; -import "client.proto"; -import "evm.proto"; -import "google/protobuf/empty.proto"; - -enum KeyType { - KEY_TYPE_UNSPECIFIED = 0; - KEY_TYPE_ED25519 = 1; - KEY_TYPE_ECDSA_SECP256K1 = 2; - KEY_TYPE_RSA = 3; -} - -// --- SDK client management --- - -enum SdkClientError { - SDK_CLIENT_ERROR_UNSPECIFIED = 0; - SDK_CLIENT_ERROR_ALREADY_EXISTS = 1; - SDK_CLIENT_ERROR_NOT_FOUND = 2; - SDK_CLIENT_ERROR_HAS_RELATED_DATA = 3; // hard-delete blocked by FK (client has grants or transaction logs) - SDK_CLIENT_ERROR_INTERNAL = 4; -} - -message SdkClientRevokeRequest { - int32 client_id = 1; -} - -message SdkClientEntry { - int32 id = 1; - bytes pubkey = 2; - arbiter.client.ClientInfo info = 3; - int32 created_at = 4; -} - -message SdkClientList { - repeated SdkClientEntry clients = 1; -} - -message SdkClientRevokeResponse { - oneof result { - google.protobuf.Empty ok = 1; - SdkClientError error = 2; - } -} - -message SdkClientListResponse { - oneof result { - SdkClientList clients = 1; - SdkClientError error = 2; - } -} - -message AuthChallengeRequest { - bytes pubkey = 1; - optional string bootstrap_token = 2; - KeyType key_type = 3; -} - -message AuthChallenge { - int32 nonce = 2; - reserved 1; -} - -message AuthChallengeSolution { - bytes signature = 1; -} - -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; -} - -message UnsealStartResponse { - bytes server_pubkey = 1; -} -message UnsealEncryptedKey { - bytes nonce = 1; - bytes ciphertext = 2; - bytes associated_data = 3; -} - -message BootstrapEncryptedKey { - bytes nonce = 1; - bytes ciphertext = 2; - bytes associated_data = 3; -} - -enum UnsealResult { - UNSEAL_RESULT_UNSPECIFIED = 0; - UNSEAL_RESULT_SUCCESS = 1; - UNSEAL_RESULT_INVALID_KEY = 2; - UNSEAL_RESULT_UNBOOTSTRAPPED = 3; -} - -enum BootstrapResult { - BOOTSTRAP_RESULT_UNSPECIFIED = 0; - BOOTSTRAP_RESULT_SUCCESS = 1; - BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED = 2; - BOOTSTRAP_RESULT_INVALID_KEY = 3; -} - -enum VaultState { - VAULT_STATE_UNSPECIFIED = 0; - VAULT_STATE_UNBOOTSTRAPPED = 1; - VAULT_STATE_SEALED = 2; - VAULT_STATE_UNSEALED = 3; - VAULT_STATE_ERROR = 4; -} - -message SdkClientConnectionRequest { - bytes pubkey = 1; - arbiter.client.ClientInfo info = 2; -} - -message SdkClientConnectionResponse { - bool approved = 1; - bytes pubkey = 2; -} - -message SdkClientConnectionCancel { - bytes pubkey = 1; -} - -message WalletAccess { - int32 wallet_id = 1; - int32 sdk_client_id = 2; -} - -message SdkClientWalletAccess { - int32 id = 1; - WalletAccess access = 2; -} - -message SdkClientGrantWalletAccess { - repeated WalletAccess accesses = 1; -} - -message SdkClientRevokeWalletAccess { - repeated int32 accesses = 1; -} - -message ListWalletAccessResponse { - repeated SdkClientWalletAccess accesses = 1; -} +import "user_agent/auth.proto"; +import "user_agent/evm.proto"; +import "user_agent/sdk_client.proto"; +import "user_agent/vault/vault.proto"; message UserAgentRequest { int32 id = 16; oneof payload { - AuthChallengeRequest auth_challenge_request = 1; - AuthChallengeSolution auth_challenge_solution = 2; - UnsealStart unseal_start = 3; - UnsealEncryptedKey unseal_encrypted_key = 4; - google.protobuf.Empty query_vault_state = 5; - google.protobuf.Empty evm_wallet_create = 6; - google.protobuf.Empty evm_wallet_list = 7; - arbiter.evm.EvmGrantCreateRequest evm_grant_create = 8; - arbiter.evm.EvmGrantDeleteRequest evm_grant_delete = 9; - arbiter.evm.EvmGrantListRequest evm_grant_list = 10; - SdkClientConnectionResponse sdk_client_connection_response = 11; - SdkClientRevokeRequest sdk_client_revoke = 12; - google.protobuf.Empty sdk_client_list = 13; - BootstrapEncryptedKey bootstrap_encrypted_key = 14; - SdkClientGrantWalletAccess grant_wallet_access = 15; - SdkClientRevokeWalletAccess revoke_wallet_access = 17; - google.protobuf.Empty list_wallet_access = 18; + auth.Request auth = 1; + vault.Request vault = 2; + evm.Request evm = 3; + sdk_client.Request sdk_client = 4; } } + message UserAgentResponse { optional int32 id = 16; oneof payload { - AuthChallenge auth_challenge = 1; - AuthResult auth_result = 2; - UnsealStartResponse unseal_start_response = 3; - UnsealResult unseal_result = 4; - VaultState vault_state = 5; - arbiter.evm.WalletCreateResponse evm_wallet_create = 6; - arbiter.evm.WalletListResponse evm_wallet_list = 7; - arbiter.evm.EvmGrantCreateResponse evm_grant_create = 8; - arbiter.evm.EvmGrantDeleteResponse evm_grant_delete = 9; - arbiter.evm.EvmGrantListResponse evm_grant_list = 10; - SdkClientConnectionRequest sdk_client_connection_request = 11; - SdkClientConnectionCancel sdk_client_connection_cancel = 12; - SdkClientRevokeResponse sdk_client_revoke_response = 13; - SdkClientListResponse sdk_client_list_response = 14; - BootstrapResult bootstrap_result = 15; - ListWalletAccessResponse list_wallet_access_response = 17; + auth.Response auth = 1; + vault.Response vault = 2; + evm.Response evm = 3; + sdk_client.Response sdk_client = 4; } } diff --git a/protobufs/user_agent/auth.proto b/protobufs/user_agent/auth.proto new file mode 100644 index 0000000..d2c5528 --- /dev/null +++ b/protobufs/user_agent/auth.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; + +package arbiter.user_agent.auth; + +enum KeyType { + KEY_TYPE_UNSPECIFIED = 0; + KEY_TYPE_ED25519 = 1; + KEY_TYPE_ECDSA_SECP256K1 = 2; + KEY_TYPE_RSA = 3; +} + +message AuthChallengeRequest { + bytes pubkey = 1; + optional string bootstrap_token = 2; + KeyType key_type = 3; +} + +message AuthChallenge { + int32 nonce = 1; +} + +message AuthChallengeSolution { + bytes signature = 1; +} + +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 Request { + oneof payload { + AuthChallengeRequest challenge_request = 1; + AuthChallengeSolution challenge_solution = 2; + } +} + +message Response { + oneof payload { + AuthChallenge challenge = 1; + AuthResult result = 2; + } +} diff --git a/protobufs/user_agent/evm.proto b/protobufs/user_agent/evm.proto new file mode 100644 index 0000000..5668d4d --- /dev/null +++ b/protobufs/user_agent/evm.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package arbiter.user_agent.evm; + +import "evm.proto"; +import "google/protobuf/empty.proto"; + +message Request { + oneof payload { + google.protobuf.Empty wallet_create = 1; + google.protobuf.Empty wallet_list = 2; + arbiter.evm.EvmGrantCreateRequest grant_create = 3; + arbiter.evm.EvmGrantDeleteRequest grant_delete = 4; + arbiter.evm.EvmGrantListRequest grant_list = 5; + } +} + +message Response { + oneof payload { + arbiter.evm.WalletCreateResponse wallet_create = 1; + arbiter.evm.WalletListResponse wallet_list = 2; + arbiter.evm.EvmGrantCreateResponse grant_create = 3; + arbiter.evm.EvmGrantDeleteResponse grant_delete = 4; + arbiter.evm.EvmGrantListResponse grant_list = 5; + } +} diff --git a/protobufs/user_agent/sdk_client.proto b/protobufs/user_agent/sdk_client.proto new file mode 100644 index 0000000..62f2a70 --- /dev/null +++ b/protobufs/user_agent/sdk_client.proto @@ -0,0 +1,100 @@ +syntax = "proto3"; + +package arbiter.user_agent.sdk_client; + +import "client.proto"; +import "google/protobuf/empty.proto"; + +enum Error { + ERROR_UNSPECIFIED = 0; + ERROR_ALREADY_EXISTS = 1; + ERROR_NOT_FOUND = 2; + ERROR_HAS_RELATED_DATA = 3; // hard-delete blocked by FK (client has grants or transaction logs) + ERROR_INTERNAL = 4; +} + +message RevokeRequest { + int32 client_id = 1; +} + +message Entry { + int32 id = 1; + bytes pubkey = 2; + arbiter.client.ClientInfo info = 3; + int32 created_at = 4; +} + +message List { + repeated Entry clients = 1; +} + +message RevokeResponse { + oneof result { + google.protobuf.Empty ok = 1; + Error error = 2; + } +} + +message ListResponse { + oneof result { + List clients = 1; + Error error = 2; + } +} + +message ConnectionRequest { + bytes pubkey = 1; + arbiter.client.ClientInfo info = 2; +} + +message ConnectionResponse { + bool approved = 1; + bytes pubkey = 2; +} + +message ConnectionCancel { + bytes pubkey = 1; +} + +message WalletAccess { + int32 wallet_id = 1; + int32 sdk_client_id = 2; +} + +message WalletAccessEntry { + int32 id = 1; + WalletAccess access = 2; +} + +message GrantWalletAccess { + repeated WalletAccess accesses = 1; +} + +message RevokeWalletAccess { + repeated int32 accesses = 1; +} + +message ListWalletAccessResponse { + repeated WalletAccessEntry accesses = 1; +} + +message Request { + oneof payload { + ConnectionResponse connection_response = 1; + RevokeRequest revoke = 2; + google.protobuf.Empty list = 3; + GrantWalletAccess grant_wallet_access = 4; + RevokeWalletAccess revoke_wallet_access = 5; + google.protobuf.Empty list_wallet_access = 6; + } +} + +message Response { + oneof payload { + ConnectionRequest connection_request = 1; + ConnectionCancel connection_cancel = 2; + RevokeResponse revoke = 3; + ListResponse list = 4; + ListWalletAccessResponse list_wallet_access = 5; + } +} diff --git a/protobufs/user_agent/vault/bootstrap.proto b/protobufs/user_agent/vault/bootstrap.proto new file mode 100644 index 0000000..8a009cf --- /dev/null +++ b/protobufs/user_agent/vault/bootstrap.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package arbiter.user_agent.vault.bootstrap; + +message BootstrapEncryptedKey { + bytes nonce = 1; + bytes ciphertext = 2; + bytes associated_data = 3; +} + +enum BootstrapResult { + BOOTSTRAP_RESULT_UNSPECIFIED = 0; + BOOTSTRAP_RESULT_SUCCESS = 1; + BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED = 2; + BOOTSTRAP_RESULT_INVALID_KEY = 3; +} + +message Request { + BootstrapEncryptedKey encrypted_key = 2; +} + +message Response { + BootstrapResult result = 1; +} diff --git a/protobufs/user_agent/vault/unseal.proto b/protobufs/user_agent/vault/unseal.proto new file mode 100644 index 0000000..8d0c378 --- /dev/null +++ b/protobufs/user_agent/vault/unseal.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package arbiter.user_agent.vault.unseal; + +message UnsealStart { + bytes client_pubkey = 1; +} + +message UnsealStartResponse { + bytes server_pubkey = 1; +} +message UnsealEncryptedKey { + bytes nonce = 1; + bytes ciphertext = 2; + bytes associated_data = 3; +} + +enum UnsealResult { + UNSEAL_RESULT_UNSPECIFIED = 0; + UNSEAL_RESULT_SUCCESS = 1; + UNSEAL_RESULT_INVALID_KEY = 2; + UNSEAL_RESULT_UNBOOTSTRAPPED = 3; +} + +message Request { + oneof payload { + UnsealStart start = 1; + UnsealEncryptedKey encrypted_key = 2; + } +} + +message Response { + oneof payload { + UnsealStartResponse start = 1; + UnsealResult result = 2; + } +} diff --git a/protobufs/user_agent/vault/vault.proto b/protobufs/user_agent/vault/vault.proto new file mode 100644 index 0000000..640b0d3 --- /dev/null +++ b/protobufs/user_agent/vault/vault.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package arbiter.user_agent.vault; + +import "google/protobuf/empty.proto"; +import "user_agent/vault/bootstrap.proto"; +import "user_agent/vault/unseal.proto"; + +enum VaultState { + VAULT_STATE_UNSPECIFIED = 0; + VAULT_STATE_UNBOOTSTRAPPED = 1; + VAULT_STATE_SEALED = 2; + VAULT_STATE_UNSEALED = 3; + VAULT_STATE_ERROR = 4; +} + +message Request { + oneof payload { + google.protobuf.Empty query_state = 1; + unseal.Request unseal = 2; + bootstrap.Request bootstrap = 3; + } +} + +message Response { + oneof payload { + VaultState state = 1; + unseal.Response unseal = 2; + bootstrap.Response bootstrap = 3; + } +} diff --git a/server/crates/arbiter-proto/src/lib.rs b/server/crates/arbiter-proto/src/lib.rs index 732ee65..323254a 100644 --- a/server/crates/arbiter-proto/src/lib.rs +++ b/server/crates/arbiter-proto/src/lib.rs @@ -8,6 +8,30 @@ pub mod proto { pub mod user_agent { tonic::include_proto!("arbiter.user_agent"); + + pub mod auth { + tonic::include_proto!("arbiter.user_agent.auth"); + } + + pub mod evm { + tonic::include_proto!("arbiter.user_agent.evm"); + } + + pub mod sdk_client { + tonic::include_proto!("arbiter.user_agent.sdk_client"); + } + + pub mod vault { + tonic::include_proto!("arbiter.user_agent.vault"); + + pub mod bootstrap { + tonic::include_proto!("arbiter.user_agent.vault.bootstrap"); + } + + pub mod unseal { + tonic::include_proto!("arbiter.user_agent.vault.unseal"); + } + } } pub mod client { diff --git a/server/crates/arbiter-server/src/grpc/user_agent.rs b/server/crates/arbiter-server/src/grpc/user_agent.rs index 3a8de53..4a32ab7 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent.rs @@ -2,29 +2,8 @@ use tokio::sync::mpsc; use arbiter_proto::{ proto::{ - client::ClientInfo as ProtoClientMetadata, - evm::{ - EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse, - EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, - GrantEntry, 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, - wallet_create_response::Result as WalletCreateResult, - wallet_list_response::Result as WalletListResult, - }, user_agent::{ - BootstrapEncryptedKey as ProtoBootstrapEncryptedKey, - BootstrapResult as ProtoBootstrapResult, ListWalletAccessResponse, - SdkClientConnectionCancel as ProtoSdkClientConnectionCancel, - SdkClientConnectionRequest as ProtoSdkClientConnectionRequest, - SdkClientEntry as ProtoSdkClientEntry, SdkClientError as ProtoSdkClientError, - SdkClientGrantWalletAccess, SdkClientList as ProtoSdkClientList, - SdkClientListResponse as ProtoSdkClientListResponse, SdkClientRevokeWalletAccess, - UnsealEncryptedKey as ProtoUnsealEncryptedKey, - UnsealResult as ProtoUnsealResult, UnsealStart, UserAgentRequest, UserAgentResponse, - VaultState as ProtoVaultState, - sdk_client_list_response::Result as ProtoSdkClientListResult, + UserAgentRequest, UserAgentResponse, user_agent_request::Payload as UserAgentRequestPayload, user_agent_response::Payload as UserAgentResponsePayload, }, @@ -32,33 +11,20 @@ use arbiter_proto::{ transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi}, }; use async_trait::async_trait; -use kameo::{ - actor::{ActorRef, Spawn as _}, - error::SendError, -}; +use kameo::actor::{ActorRef, Spawn as _}; use tonic::Status; use tracing::{error, info, warn}; use crate::{ - actors::{ - keyholder::KeyHolderState, - user_agent::{ - OutOfBand, UserAgentConnection, UserAgentSession, - session::connection::{ - BootstrapError, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, - HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, - HandleGrantEvmWalletAccess, HandleGrantList, HandleListWalletAccess, - HandleNewClientApprove, HandleQueryVaultState, HandleRevokeEvmWalletAccess, - HandleSdkClientList, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError, - }, - }, - }, - db::models::NewEvmWalletAccess, - grpc::{Convert, TryConvert, request_tracker::RequestTracker}, + actors::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession}, + grpc::request_tracker::RequestTracker, }; mod auth; +mod evm; mod inbound; mod outbound; +mod sdk_client; +mod vault; pub struct OutOfBandAdapter(mpsc::Sender); @@ -86,23 +52,7 @@ async fn dispatch_loop( return; }; - let payload = match oob { - OutOfBand::ClientConnectionRequest { profile } => { - UserAgentResponsePayload::SdkClientConnectionRequest(ProtoSdkClientConnectionRequest { - pubkey: profile.pubkey.to_bytes().to_vec(), - info: Some(ProtoClientMetadata { - name: profile.metadata.name, - description: profile.metadata.description, - version: profile.metadata.version, - }), - }) - } - OutOfBand::ClientConnectionCancel { pubkey } => { - UserAgentResponsePayload::SdkClientConnectionCancel(ProtoSdkClientConnectionCancel { - pubkey: pubkey.to_bytes().to_vec(), - }) - } - }; + let payload = sdk_client::out_of_band_payload(oob); if bi.send(Ok(UserAgentResponse { id: None, payload: Some(payload) })).await.is_err() { return; @@ -144,7 +94,7 @@ async fn dispatch_loop( } Ok(None) => {} Err(status) => { - error!(?status, "Failed to process user agent request"); + error!(?status, "Failed to process user agent request"); let _ = bi.send(Err(status)).await; return; } @@ -159,337 +109,16 @@ async fn dispatch_inner( payload: UserAgentRequestPayload, ) -> Result, Status> { match payload { - UserAgentRequestPayload::UnsealStart(req) => handle_unseal_start(actor, req).await, - UserAgentRequestPayload::UnsealEncryptedKey(req) => { - handle_unseal_encrypted_key(actor, req).await - } - UserAgentRequestPayload::BootstrapEncryptedKey(req) => { - handle_bootstrap_encrypted_key(actor, req).await - } - UserAgentRequestPayload::QueryVaultState(_) => handle_query_vault_state(actor).await, - UserAgentRequestPayload::EvmWalletCreate(_) => handle_evm_wallet_create(actor).await, - UserAgentRequestPayload::EvmWalletList(_) => handle_evm_wallet_list(actor).await, - UserAgentRequestPayload::EvmGrantList(_) => handle_evm_grant_list(actor).await, - UserAgentRequestPayload::EvmGrantCreate(req) => handle_evm_grant_create(actor, req).await, - UserAgentRequestPayload::EvmGrantDelete(req) => handle_evm_grant_delete(actor, req).await, - UserAgentRequestPayload::SdkClientConnectionResponse(resp) => { - handle_sdk_client_connection_response(actor, resp).await - } - UserAgentRequestPayload::SdkClientRevoke(_) => { - Err(Status::unimplemented("SdkClientRevoke is not yet implemented")) - } - UserAgentRequestPayload::SdkClientList(_) => handle_sdk_client_list(actor).await, - UserAgentRequestPayload::GrantWalletAccess(req) => { - handle_grant_wallet_access(actor, req).await - } - UserAgentRequestPayload::RevokeWalletAccess(req) => { - handle_revoke_wallet_access(actor, req).await - } - UserAgentRequestPayload::ListWalletAccess(_) => handle_list_wallet_access(actor).await, - UserAgentRequestPayload::AuthChallengeRequest(..) - | UserAgentRequestPayload::AuthChallengeSolution(..) => { - warn!(?payload, "Unsupported post-auth user agent request"); + UserAgentRequestPayload::Vault(req) => vault::dispatch(actor, req).await, + UserAgentRequestPayload::Evm(req) => evm::dispatch(actor, req).await, + UserAgentRequestPayload::SdkClient(req) => sdk_client::dispatch(actor, req).await, + UserAgentRequestPayload::Auth(..) => { + warn!("Unsupported post-auth user agent auth request"); Err(Status::invalid_argument("Unsupported user-agent request")) } } } -async fn handle_unseal_start( - actor: &ActorRef, - req: UnsealStart, -) -> Result, Status> { - let client_pubkey = <[u8; 32]>::try_from(req.client_pubkey) - .map(x25519_dalek::PublicKey::from) - .map_err(|_| Status::invalid_argument("Invalid X25519 public key"))?; - - let response = actor - .ask(HandleUnsealRequest { client_pubkey }) - .await - .map_err(|err| { - warn!(error = ?err, "Failed to handle unseal start request"); - Status::internal("Failed to start unseal flow") - })?; - - Ok(Some(UserAgentResponsePayload::UnsealStartResponse( - arbiter_proto::proto::user_agent::UnsealStartResponse { - server_pubkey: response.server_pubkey.as_bytes().to_vec(), - }, - ))) -} - -async fn handle_unseal_encrypted_key( - actor: &ActorRef, - req: ProtoUnsealEncryptedKey, -) -> Result, Status> { - let result = match actor - .ask(HandleUnsealEncryptedKey { - nonce: req.nonce, - ciphertext: req.ciphertext, - associated_data: req.associated_data, - }) - .await - { - Ok(()) => ProtoUnsealResult::Success, - Err(SendError::HandlerError(UnsealError::InvalidKey)) => ProtoUnsealResult::InvalidKey, - Err(err) => { - warn!(error = ?err, "Failed to handle unseal request"); - return Err(Status::internal("Failed to unseal vault")); - } - }; - Ok(Some(UserAgentResponsePayload::UnsealResult(result.into()))) -} - -async fn handle_bootstrap_encrypted_key( - actor: &ActorRef, - req: ProtoBootstrapEncryptedKey, -) -> Result, Status> { - let result = match actor - .ask(HandleBootstrapEncryptedKey { - nonce: req.nonce, - ciphertext: req.ciphertext, - associated_data: req.associated_data, - }) - .await - { - Ok(()) => ProtoBootstrapResult::Success, - Err(SendError::HandlerError(BootstrapError::InvalidKey)) => ProtoBootstrapResult::InvalidKey, - Err(SendError::HandlerError(BootstrapError::AlreadyBootstrapped)) => { - ProtoBootstrapResult::AlreadyBootstrapped - } - Err(err) => { - warn!(error = ?err, "Failed to handle bootstrap request"); - return Err(Status::internal("Failed to bootstrap vault")); - } - }; - Ok(Some(UserAgentResponsePayload::BootstrapResult(result.into()))) -} - -async fn handle_query_vault_state( - actor: &ActorRef, -) -> Result, Status> { - let state = match actor.ask(HandleQueryVaultState {}).await { - Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, - Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed, - Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed, - Err(err) => { - warn!(error = ?err, "Failed to query vault state"); - ProtoVaultState::Error - } - }; - Ok(Some(UserAgentResponsePayload::VaultState(state.into()))) -} - -async fn handle_evm_wallet_create( - actor: &ActorRef, -) -> Result, Status> { - let result = match actor.ask(HandleEvmWalletCreate {}).await { - Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry { - id: wallet_id, - address: address.to_vec(), - }), - Err(err) => { - warn!(error = ?err, "Failed to create EVM wallet"); - WalletCreateResult::Error(ProtoEvmError::Internal.into()) - } - }; - Ok(Some(UserAgentResponsePayload::EvmWalletCreate( - WalletCreateResponse { result: Some(result) }, - ))) -} - -async fn handle_evm_wallet_list( - actor: &ActorRef, -) -> Result, Status> { - let result = match actor.ask(HandleEvmWalletList {}).await { - Ok(wallets) => WalletListResult::Wallets(WalletList { - wallets: wallets - .into_iter() - .map(|(id, address)| WalletEntry { - address: address.to_vec(), - id, - }) - .collect(), - }), - Err(err) => { - warn!(error = ?err, "Failed to list EVM wallets"); - WalletListResult::Error(ProtoEvmError::Internal.into()) - } - }; - Ok(Some(UserAgentResponsePayload::EvmWalletList( - WalletListResponse { result: Some(result) }, - ))) -} - -async fn handle_evm_grant_list( - actor: &ActorRef, -) -> Result, Status> { - let result = match actor.ask(HandleGrantList {}).await { - Ok(grants) => EvmGrantListResult::Grants(EvmGrantList { - grants: grants - .into_iter() - .map(|grant| GrantEntry { - id: grant.id, - wallet_access_id: grant.shared.wallet_access_id, - shared: Some(grant.shared.convert()), - specific: Some(grant.settings.convert()), - }) - .collect(), - }), - Err(err) => { - warn!(error = ?err, "Failed to list EVM grants"); - EvmGrantListResult::Error(ProtoEvmError::Internal.into()) - } - }; - Ok(Some(UserAgentResponsePayload::EvmGrantList( - EvmGrantListResponse { result: Some(result) }, - ))) -} - -async fn handle_evm_grant_create( - actor: &ActorRef, - req: EvmGrantCreateRequest, -) -> Result, Status> { - let basic = req - .shared - .ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))? - .try_convert()?; - let grant = req - .specific - .ok_or_else(|| Status::invalid_argument("Missing specific grant settings"))? - .try_convert()?; - - let result = match actor.ask(HandleGrantCreate { basic, grant }).await { - Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id), - Err(err) => { - warn!(error = ?err, "Failed to create EVM grant"); - EvmGrantCreateResult::Error(ProtoEvmError::Internal.into()) - } - }; - Ok(Some(UserAgentResponsePayload::EvmGrantCreate( - EvmGrantCreateResponse { result: Some(result) }, - ))) -} - -async fn handle_evm_grant_delete( - actor: &ActorRef, - req: EvmGrantDeleteRequest, -) -> Result, Status> { - let result = match actor.ask(HandleGrantDelete { grant_id: req.grant_id }).await { - Ok(()) => EvmGrantDeleteResult::Ok(()), - Err(err) => { - warn!(error = ?err, "Failed to delete EVM grant"); - EvmGrantDeleteResult::Error(ProtoEvmError::Internal.into()) - } - }; - Ok(Some(UserAgentResponsePayload::EvmGrantDelete( - EvmGrantDeleteResponse { result: Some(result) }, - ))) -} - -async fn handle_sdk_client_connection_response( - actor: &ActorRef, - resp: arbiter_proto::proto::user_agent::SdkClientConnectionResponse, -) -> Result, Status> { - let pubkey_bytes = <[u8; 32]>::try_from(resp.pubkey) - .map_err(|_| Status::invalid_argument("Invalid Ed25519 public key length"))?; - let pubkey = ed25519_dalek::VerifyingKey::from_bytes(&pubkey_bytes) - .map_err(|_| Status::invalid_argument("Invalid Ed25519 public key"))?; - - actor - .ask(HandleNewClientApprove { - approved: resp.approved, - pubkey, - }) - .await - .map_err(|err| { - warn!(?err, "Failed to process client connection response"); - Status::internal("Failed to process response") - })?; - - Ok(None) -} - -async fn handle_sdk_client_list( - actor: &ActorRef, -) -> Result, Status> { - let result = match actor.ask(HandleSdkClientList {}).await { - Ok(clients) => ProtoSdkClientListResult::Clients(ProtoSdkClientList { - clients: clients - .into_iter() - .map(|(client, metadata)| ProtoSdkClientEntry { - id: client.id, - pubkey: client.public_key, - info: Some(ProtoClientMetadata { - name: metadata.name, - description: metadata.description, - version: metadata.version, - }), - created_at: client.created_at.0.timestamp() as i32, - }) - .collect(), - }), - Err(err) => { - warn!(error = ?err, "Failed to list SDK clients"); - ProtoSdkClientListResult::Error(ProtoSdkClientError::Internal.into()) - } - }; - Ok(Some(UserAgentResponsePayload::SdkClientListResponse( - ProtoSdkClientListResponse { result: Some(result) }, - ))) -} - -async fn handle_grant_wallet_access( - actor: &ActorRef, - req: SdkClientGrantWalletAccess, -) -> Result, Status> { - let entries: Vec = req.accesses.into_iter().map(|a| a.convert()).collect(); - match actor.ask(HandleGrantEvmWalletAccess { entries }).await { - Ok(()) => { - info!("Successfully granted wallet access"); - Ok(None) - } - Err(err) => { - warn!(error = ?err, "Failed to grant wallet access"); - Err(Status::internal("Failed to grant wallet access")) - } - } -} - -async fn handle_revoke_wallet_access( - actor: &ActorRef, - req: SdkClientRevokeWalletAccess, -) -> Result, Status> { - match actor - .ask(HandleRevokeEvmWalletAccess { entries: req.accesses }) - .await - { - Ok(()) => { - info!("Successfully revoked wallet access"); - Ok(None) - } - Err(err) => { - warn!(error = ?err, "Failed to revoke wallet access"); - Err(Status::internal("Failed to revoke wallet access")) - } - } -} - -async fn handle_list_wallet_access( - actor: &ActorRef, -) -> Result, Status> { - match actor.ask(HandleListWalletAccess {}).await { - Ok(accesses) => Ok(Some(UserAgentResponsePayload::ListWalletAccessResponse( - ListWalletAccessResponse { - accesses: accesses.into_iter().map(|a| a.convert()).collect(), - }, - ))), - Err(err) => { - warn!(error = ?err, "Failed to list wallet access"); - Err(Status::internal("Failed to list wallet access")) - } - } -} - pub async fn start( mut conn: UserAgentConnection, mut bi: GrpcBi, 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 578b849..15eeba6 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent/auth.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent/auth.rs @@ -1,9 +1,12 @@ use arbiter_proto::{ 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, + UserAgentRequest, UserAgentResponse, auth::{ + self as proto_auth, AuthChallenge as ProtoAuthChallenge, + AuthChallengeRequest as ProtoAuthChallengeRequest, + AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult, + KeyType as ProtoKeyType, request::Payload as AuthRequestPayload, + response::Payload as AuthResponsePayload, + }, user_agent_request::Payload as UserAgentRequestPayload, user_agent_response::Payload as UserAgentResponsePayload, }, transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi}, @@ -36,12 +39,14 @@ impl<'a> AuthTransportAdapter<'a> { async fn send_user_agent_response( &mut self, - payload: UserAgentResponsePayload, + payload: AuthResponsePayload, ) -> Result<(), TransportError> { self.bi .send(Ok(UserAgentResponse { id: Some(self.request_tracker.current_request_id()), - payload: Some(payload), + payload: Some(UserAgentResponsePayload::Auth(proto_auth::Response { + payload: Some(payload), + })), })) .await } @@ -56,19 +61,17 @@ impl Sender> for AuthTransportAdapter<'_> { use auth::{Error, Outbound}; let payload = match item { Ok(Outbound::AuthChallenge { nonce }) => { - UserAgentResponsePayload::AuthChallenge(ProtoAuthChallenge { nonce }) - } - Ok(Outbound::AuthSuccess) => { - UserAgentResponsePayload::AuthResult(ProtoAuthResult::Success.into()) + AuthResponsePayload::Challenge(ProtoAuthChallenge { nonce }) } + Ok(Outbound::AuthSuccess) => AuthResponsePayload::Result(ProtoAuthResult::Success.into()), Err(Error::UnregisteredPublicKey) => { - UserAgentResponsePayload::AuthResult(ProtoAuthResult::InvalidKey.into()) + AuthResponsePayload::Result(ProtoAuthResult::InvalidKey.into()) } Err(Error::InvalidChallengeSolution) => { - UserAgentResponsePayload::AuthResult(ProtoAuthResult::InvalidSignature.into()) + AuthResponsePayload::Result(ProtoAuthResult::InvalidSignature.into()) } Err(Error::InvalidBootstrapToken) => { - UserAgentResponsePayload::AuthResult(ProtoAuthResult::TokenInvalid.into()) + AuthResponsePayload::Result(ProtoAuthResult::TokenInvalid.into()) } Err(Error::Internal { details }) => { return self.bi.send(Err(Status::internal(details))).await; @@ -112,8 +115,26 @@ impl Receiver for AuthTransportAdapter<'_> { return None; }; + let UserAgentRequestPayload::Auth(auth_request) = payload else { + let _ = self + .bi + .send(Err(Status::invalid_argument( + "Unsupported user-agent auth request", + ))) + .await; + return None; + }; + + let Some(payload) = auth_request.payload else { + warn!( + event = "received auth request with empty payload", + "grpc.useragent.auth_adapter" + ); + return None; + }; + match payload { - UserAgentRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest { + AuthRequestPayload::ChallengeRequest(ProtoAuthChallengeRequest { pubkey, bootstrap_token, key_type, @@ -150,18 +171,9 @@ impl Receiver for AuthTransportAdapter<'_> { bootstrap_token, }) } - UserAgentRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution { + AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature, }) => Some(auth::Inbound::AuthChallengeSolution { signature }), - _ => { - let _ = self - .bi - .send(Err(Status::invalid_argument( - "Unsupported user-agent auth request", - ))) - .await; - None - } } } } diff --git a/server/crates/arbiter-server/src/grpc/user_agent/evm.rs b/server/crates/arbiter-server/src/grpc/user_agent/evm.rs new file mode 100644 index 0000000..e64a9ec --- /dev/null +++ b/server/crates/arbiter-server/src/grpc/user_agent/evm.rs @@ -0,0 +1,170 @@ +use arbiter_proto::proto::{ + evm::{ + EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse, + EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, + GrantEntry, 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, + wallet_create_response::Result as WalletCreateResult, + wallet_list_response::Result as WalletListResult, + }, + user_agent::{ + evm::{self as proto_evm, request::Payload as EvmRequestPayload, response::Payload as EvmResponsePayload}, + user_agent_response::Payload as UserAgentResponsePayload, + }, +}; +use kameo::actor::ActorRef; +use tonic::Status; +use tracing::warn; + +use crate::{ + actors::user_agent::{ + UserAgentSession, + session::connection::{ + HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, + HandleGrantList, + }, + }, + grpc::{Convert, TryConvert}, +}; + +fn wrap_evm_response(payload: EvmResponsePayload) -> UserAgentResponsePayload { + UserAgentResponsePayload::Evm(proto_evm::Response { + payload: Some(payload), + }) +} + +pub(super) async fn dispatch( + actor: &ActorRef, + req: proto_evm::Request, +) -> Result, Status> { + let Some(payload) = req.payload else { + return Err(Status::invalid_argument("Missing EVM request payload")); + }; + + match payload { + EvmRequestPayload::WalletCreate(_) => handle_wallet_create(actor).await, + EvmRequestPayload::WalletList(_) => handle_wallet_list(actor).await, + EvmRequestPayload::GrantCreate(req) => handle_grant_create(actor, req).await, + EvmRequestPayload::GrantDelete(req) => handle_grant_delete(actor, req).await, + EvmRequestPayload::GrantList(_) => handle_grant_list(actor).await, + } +} + +async fn handle_wallet_create( + actor: &ActorRef, +) -> Result, Status> { + let result = match actor.ask(HandleEvmWalletCreate {}).await { + Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry { + id: wallet_id, + address: address.to_vec(), + }), + Err(err) => { + warn!(error = ?err, "Failed to create EVM wallet"); + WalletCreateResult::Error(ProtoEvmError::Internal.into()) + } + }; + Ok(Some(wrap_evm_response(EvmResponsePayload::WalletCreate( + WalletCreateResponse { + result: Some(result), + }, + )))) +} + +async fn handle_wallet_list( + actor: &ActorRef, +) -> Result, Status> { + let result = match actor.ask(HandleEvmWalletList {}).await { + Ok(wallets) => WalletListResult::Wallets(WalletList { + wallets: wallets + .into_iter() + .map(|(id, address)| WalletEntry { + address: address.to_vec(), + id, + }) + .collect(), + }), + Err(err) => { + warn!(error = ?err, "Failed to list EVM wallets"); + WalletListResult::Error(ProtoEvmError::Internal.into()) + } + }; + Ok(Some(wrap_evm_response(EvmResponsePayload::WalletList( + WalletListResponse { + result: Some(result), + }, + )))) +} + +async fn handle_grant_list( + actor: &ActorRef, +) -> Result, Status> { + let result = match actor.ask(HandleGrantList {}).await { + Ok(grants) => EvmGrantListResult::Grants(EvmGrantList { + grants: grants + .into_iter() + .map(|grant| GrantEntry { + id: grant.id, + wallet_access_id: grant.shared.wallet_access_id, + shared: Some(grant.shared.convert()), + specific: Some(grant.settings.convert()), + }) + .collect(), + }), + Err(err) => { + warn!(error = ?err, "Failed to list EVM grants"); + EvmGrantListResult::Error(ProtoEvmError::Internal.into()) + } + }; + Ok(Some(wrap_evm_response(EvmResponsePayload::GrantList( + EvmGrantListResponse { + result: Some(result), + }, + )))) +} + +async fn handle_grant_create( + actor: &ActorRef, + req: EvmGrantCreateRequest, +) -> Result, Status> { + let basic = req + .shared + .ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))? + .try_convert()?; + let grant = req + .specific + .ok_or_else(|| Status::invalid_argument("Missing specific grant settings"))? + .try_convert()?; + + let result = match actor.ask(HandleGrantCreate { basic, grant }).await { + Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id), + Err(err) => { + warn!(error = ?err, "Failed to create EVM grant"); + EvmGrantCreateResult::Error(ProtoEvmError::Internal.into()) + } + }; + Ok(Some(wrap_evm_response(EvmResponsePayload::GrantCreate( + EvmGrantCreateResponse { + result: Some(result), + }, + )))) +} + +async fn handle_grant_delete( + actor: &ActorRef, + req: EvmGrantDeleteRequest, +) -> Result, Status> { + let result = match actor.ask(HandleGrantDelete { grant_id: req.grant_id }).await { + Ok(()) => EvmGrantDeleteResult::Ok(()), + Err(err) => { + warn!(error = ?err, "Failed to delete EVM grant"); + EvmGrantDeleteResult::Error(ProtoEvmError::Internal.into()) + } + }; + Ok(Some(wrap_evm_response(EvmResponsePayload::GrantDelete( + EvmGrantDeleteResponse { + result: Some(result), + }, + )))) +} diff --git a/server/crates/arbiter-server/src/grpc/user_agent/inbound.rs b/server/crates/arbiter-server/src/grpc/user_agent/inbound.rs index 769b7d8..6cfb2e5 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent/inbound.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent/inbound.rs @@ -5,12 +5,14 @@ use arbiter_proto::proto::evm::{ TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit, specific_grant::Grant as ProtoSpecificGrantType, }; -use arbiter_proto::proto::user_agent::{SdkClientWalletAccess, WalletAccess}; +use arbiter_proto::proto::user_agent::sdk_client::{ + WalletAccess, WalletAccessEntry as SdkClientWalletAccess, +}; use chrono::{DateTime, TimeZone, Utc}; use prost_types::Timestamp as ProtoTimestamp; use tonic::Status; -use crate::db::models::{CoreEvmWalletAccess, NewEvmWallet, NewEvmWalletAccess}; +use crate::db::models::{CoreEvmWalletAccess, NewEvmWalletAccess}; use crate::grpc::Convert; use crate::{ evm::policies::{ diff --git a/server/crates/arbiter-server/src/grpc/user_agent/outbound.rs b/server/crates/arbiter-server/src/grpc/user_agent/outbound.rs index 7d490b7..53ea729 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent/outbound.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent/outbound.rs @@ -5,7 +5,9 @@ use arbiter_proto::proto::{ TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit, specific_grant::Grant as ProtoSpecificGrantType, }, - user_agent::{SdkClientWalletAccess as ProtoSdkClientWalletAccess, WalletAccess}, + user_agent::sdk_client::{ + WalletAccess, WalletAccessEntry as ProtoSdkClientWalletAccess, + }, }; use chrono::{DateTime, Utc}; use prost_types::Timestamp as ProtoTimestamp; diff --git a/server/crates/arbiter-server/src/grpc/user_agent/sdk_client.rs b/server/crates/arbiter-server/src/grpc/user_agent/sdk_client.rs new file mode 100644 index 0000000..6e40514 --- /dev/null +++ b/server/crates/arbiter-server/src/grpc/user_agent/sdk_client.rs @@ -0,0 +1,190 @@ +use arbiter_proto::proto::{ + client::ClientInfo as ProtoClientMetadata, + user_agent::{ + sdk_client::{ + self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel, + ConnectionRequest as ProtoSdkClientConnectionRequest, + ConnectionResponse as ProtoSdkClientConnectionResponse, Entry as ProtoSdkClientEntry, + Error as ProtoSdkClientError, GrantWalletAccess as ProtoSdkClientGrantWalletAccess, + List as ProtoSdkClientList, ListResponse as ProtoSdkClientListResponse, + ListWalletAccessResponse, RevokeWalletAccess as ProtoSdkClientRevokeWalletAccess, + list_response::Result as ProtoSdkClientListResult, + request::Payload as SdkClientRequestPayload, + response::Payload as SdkClientResponsePayload, + }, + user_agent_response::Payload as UserAgentResponsePayload, + }, +}; +use kameo::actor::ActorRef; +use tonic::Status; +use tracing::{info, warn}; + +use crate::{ + actors::user_agent::{ + OutOfBand, UserAgentSession, + session::connection::{ + HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove, + HandleRevokeEvmWalletAccess, HandleSdkClientList, + }, + }, + db::models::NewEvmWalletAccess, + grpc::Convert, +}; + +fn wrap_sdk_client_response(payload: SdkClientResponsePayload) -> UserAgentResponsePayload { + UserAgentResponsePayload::SdkClient(proto_sdk_client::Response { + payload: Some(payload), + }) +} + +pub(super) fn out_of_band_payload(oob: OutOfBand) -> UserAgentResponsePayload { + match oob { + OutOfBand::ClientConnectionRequest { profile } => wrap_sdk_client_response( + SdkClientResponsePayload::ConnectionRequest(ProtoSdkClientConnectionRequest { + pubkey: profile.pubkey.to_bytes().to_vec(), + info: Some(ProtoClientMetadata { + name: profile.metadata.name, + description: profile.metadata.description, + version: profile.metadata.version, + }), + }), + ), + OutOfBand::ClientConnectionCancel { pubkey } => wrap_sdk_client_response( + SdkClientResponsePayload::ConnectionCancel(ProtoSdkClientConnectionCancel { + pubkey: pubkey.to_bytes().to_vec(), + }), + ), + } +} + +pub(super) async fn dispatch( + actor: &ActorRef, + req: proto_sdk_client::Request, +) -> Result, Status> { + let Some(payload) = req.payload else { + return Err(Status::invalid_argument("Missing SDK client request payload")); + }; + + match payload { + SdkClientRequestPayload::ConnectionResponse(resp) => { + handle_connection_response(actor, resp).await + } + SdkClientRequestPayload::Revoke(_) => { + Err(Status::unimplemented("SdkClientRevoke is not yet implemented")) + } + SdkClientRequestPayload::List(_) => handle_list(actor).await, + SdkClientRequestPayload::GrantWalletAccess(req) => handle_grant_wallet_access(actor, req).await, + SdkClientRequestPayload::RevokeWalletAccess(req) => { + handle_revoke_wallet_access(actor, req).await + } + SdkClientRequestPayload::ListWalletAccess(_) => handle_list_wallet_access(actor).await, + } +} + +async fn handle_connection_response( + actor: &ActorRef, + resp: ProtoSdkClientConnectionResponse, +) -> Result, Status> { + let pubkey_bytes = <[u8; 32]>::try_from(resp.pubkey) + .map_err(|_| Status::invalid_argument("Invalid Ed25519 public key length"))?; + let pubkey = ed25519_dalek::VerifyingKey::from_bytes(&pubkey_bytes) + .map_err(|_| Status::invalid_argument("Invalid Ed25519 public key"))?; + + actor + .ask(HandleNewClientApprove { + approved: resp.approved, + pubkey, + }) + .await + .map_err(|err| { + warn!(?err, "Failed to process client connection response"); + Status::internal("Failed to process response") + })?; + + Ok(None) +} + +async fn handle_list( + actor: &ActorRef, +) -> Result, Status> { + let result = match actor.ask(HandleSdkClientList {}).await { + Ok(clients) => ProtoSdkClientListResult::Clients(ProtoSdkClientList { + clients: clients + .into_iter() + .map(|(client, metadata)| ProtoSdkClientEntry { + id: client.id, + pubkey: client.public_key, + info: Some(ProtoClientMetadata { + name: metadata.name, + description: metadata.description, + version: metadata.version, + }), + created_at: client.created_at.0.timestamp() as i32, + }) + .collect(), + }), + Err(err) => { + warn!(error = ?err, "Failed to list SDK clients"); + ProtoSdkClientListResult::Error(ProtoSdkClientError::Internal.into()) + } + }; + Ok(Some(wrap_sdk_client_response(SdkClientResponsePayload::List( + ProtoSdkClientListResponse { + result: Some(result), + }, + )))) +} + +async fn handle_grant_wallet_access( + actor: &ActorRef, + req: ProtoSdkClientGrantWalletAccess, +) -> Result, Status> { + let entries: Vec = req.accesses.into_iter().map(|a| a.convert()).collect(); + match actor.ask(HandleGrantEvmWalletAccess { entries }).await { + Ok(()) => { + info!("Successfully granted wallet access"); + Ok(None) + } + Err(err) => { + warn!(error = ?err, "Failed to grant wallet access"); + Err(Status::internal("Failed to grant wallet access")) + } + } +} + +async fn handle_revoke_wallet_access( + actor: &ActorRef, + req: ProtoSdkClientRevokeWalletAccess, +) -> Result, Status> { + match actor + .ask(HandleRevokeEvmWalletAccess { + entries: req.accesses, + }) + .await + { + Ok(()) => { + info!("Successfully revoked wallet access"); + Ok(None) + } + Err(err) => { + warn!(error = ?err, "Failed to revoke wallet access"); + Err(Status::internal("Failed to revoke wallet access")) + } + } +} + +async fn handle_list_wallet_access( + actor: &ActorRef, +) -> Result, Status> { + match actor.ask(HandleListWalletAccess {}).await { + Ok(accesses) => Ok(Some(wrap_sdk_client_response( + SdkClientResponsePayload::ListWalletAccess(ListWalletAccessResponse { + accesses: accesses.into_iter().map(|a| a.convert()).collect(), + }), + ))), + Err(err) => { + warn!(error = ?err, "Failed to list wallet access"); + Err(Status::internal("Failed to list wallet access")) + } + } +} diff --git a/server/crates/arbiter-server/src/grpc/user_agent/vault.rs b/server/crates/arbiter-server/src/grpc/user_agent/vault.rs new file mode 100644 index 0000000..5aad751 --- /dev/null +++ b/server/crates/arbiter-server/src/grpc/user_agent/vault.rs @@ -0,0 +1,180 @@ +use arbiter_proto::proto::user_agent::{ + user_agent_response::Payload as UserAgentResponsePayload, + vault::{ + self as proto_vault, VaultState as ProtoVaultState, + bootstrap::{ + self as proto_bootstrap, BootstrapEncryptedKey as ProtoBootstrapEncryptedKey, + BootstrapResult as ProtoBootstrapResult, + }, + request::Payload as VaultRequestPayload, + response::Payload as VaultResponsePayload, + unseal::{ + self as proto_unseal, UnsealEncryptedKey as ProtoUnsealEncryptedKey, + UnsealResult as ProtoUnsealResult, UnsealStart, + request::Payload as UnsealRequestPayload, + response::Payload as UnsealResponsePayload, + }, + }, +}; +use kameo::{actor::ActorRef, error::SendError}; +use tonic::Status; +use tracing::warn; + +use crate::{ + actors::{ + keyholder::KeyHolderState, + user_agent::{ + UserAgentSession, + session::connection::{ + BootstrapError, HandleBootstrapEncryptedKey, HandleQueryVaultState, + HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError, + }, + }, + }, +}; + +fn wrap_vault_response(payload: VaultResponsePayload) -> UserAgentResponsePayload { + UserAgentResponsePayload::Vault(proto_vault::Response { + payload: Some(payload), + }) +} + +fn wrap_unseal_response(payload: UnsealResponsePayload) -> UserAgentResponsePayload { + wrap_vault_response(VaultResponsePayload::Unseal(proto_unseal::Response { + payload: Some(payload), + })) +} + +fn wrap_bootstrap_response(result: ProtoBootstrapResult) -> UserAgentResponsePayload { + wrap_vault_response(VaultResponsePayload::Bootstrap(proto_bootstrap::Response { + result: result.into(), + })) +} + +pub(super) async fn dispatch( + actor: &ActorRef, + req: proto_vault::Request, +) -> Result, Status> { + let Some(payload) = req.payload else { + return Err(Status::invalid_argument("Missing vault request payload")); + }; + + match payload { + VaultRequestPayload::QueryState(_) => handle_query_vault_state(actor).await, + VaultRequestPayload::Unseal(req) => dispatch_unseal_request(actor, req).await, + VaultRequestPayload::Bootstrap(req) => handle_bootstrap_request(actor, req).await, + } +} + +async fn dispatch_unseal_request( + actor: &ActorRef, + req: proto_unseal::Request, +) -> Result, Status> { + let Some(payload) = req.payload else { + return Err(Status::invalid_argument("Missing unseal request payload")); + }; + + match payload { + UnsealRequestPayload::Start(req) => handle_unseal_start(actor, req).await, + UnsealRequestPayload::EncryptedKey(req) => handle_unseal_encrypted_key(actor, req).await, + } +} + +async fn handle_unseal_start( + actor: &ActorRef, + req: UnsealStart, +) -> Result, Status> { + let client_pubkey = <[u8; 32]>::try_from(req.client_pubkey) + .map(x25519_dalek::PublicKey::from) + .map_err(|_| Status::invalid_argument("Invalid X25519 public key"))?; + + let response = actor + .ask(HandleUnsealRequest { client_pubkey }) + .await + .map_err(|err| { + warn!(error = ?err, "Failed to handle unseal start request"); + Status::internal("Failed to start unseal flow") + })?; + + Ok(Some(wrap_unseal_response(UnsealResponsePayload::Start( + proto_unseal::UnsealStartResponse { + server_pubkey: response.server_pubkey.as_bytes().to_vec(), + }, + )))) +} + +async fn handle_unseal_encrypted_key( + actor: &ActorRef, + req: ProtoUnsealEncryptedKey, +) -> Result, Status> { + let result = match actor + .ask(HandleUnsealEncryptedKey { + nonce: req.nonce, + ciphertext: req.ciphertext, + associated_data: req.associated_data, + }) + .await + { + Ok(()) => ProtoUnsealResult::Success, + Err(SendError::HandlerError(UnsealError::InvalidKey)) => ProtoUnsealResult::InvalidKey, + Err(err) => { + warn!(error = ?err, "Failed to handle unseal request"); + return Err(Status::internal("Failed to unseal vault")); + } + }; + Ok(Some(wrap_unseal_response(UnsealResponsePayload::Result( + result.into(), + )))) +} + +async fn handle_bootstrap_request( + actor: &ActorRef, + req: proto_bootstrap::Request, +) -> Result, Status> { + let encrypted_key = req + .encrypted_key + .ok_or_else(|| Status::invalid_argument("Missing bootstrap encrypted key"))?; + handle_bootstrap_encrypted_key(actor, encrypted_key).await +} + +async fn handle_bootstrap_encrypted_key( + actor: &ActorRef, + req: ProtoBootstrapEncryptedKey, +) -> Result, Status> { + let result = match actor + .ask(HandleBootstrapEncryptedKey { + nonce: req.nonce, + ciphertext: req.ciphertext, + associated_data: req.associated_data, + }) + .await + { + Ok(()) => ProtoBootstrapResult::Success, + Err(SendError::HandlerError(BootstrapError::InvalidKey)) => ProtoBootstrapResult::InvalidKey, + Err(SendError::HandlerError(BootstrapError::AlreadyBootstrapped)) => { + ProtoBootstrapResult::AlreadyBootstrapped + } + Err(err) => { + warn!(error = ?err, "Failed to handle bootstrap request"); + return Err(Status::internal("Failed to bootstrap vault")); + } + }; + Ok(Some(wrap_bootstrap_response(result))) +} + +async fn handle_query_vault_state( + actor: &ActorRef, +) -> Result, Status> { + let state = match actor.ask(HandleQueryVaultState {}).await { + Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, + Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed, + Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed, + Err(err) => { + warn!(error = ?err, "Failed to query vault state"); + ProtoVaultState::Error + } + }; + Ok(Some(wrap_vault_response(VaultResponsePayload::State( + state.into(), + )))) +} diff --git a/useragent/macos/Podfile.lock b/useragent/macos/Podfile.lock index c054fb2..2dad058 100644 --- a/useragent/macos/Podfile.lock +++ b/useragent/macos/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - biometric_signature (10.2.0): + - biometric_signature (11.0.1): - FlutterMacOS - cryptography_flutter (0.0.1): - FlutterMacOS @@ -35,7 +35,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos SPEC CHECKSUMS: - biometric_signature: 7ef6a703fcc2c3b0e3d937a8560507b1ba9d3414 + biometric_signature: bae0597fffbc51252959e78b56a2f5afb8d4e1f5 cryptography_flutter: be2b3e0e31603521b6a1c2bea232a88a2488a91c flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1