diff --git a/server/crates/arbiter-server/src/grpc/user_agent.rs b/server/crates/arbiter-server/src/grpc/user_agent.rs index 832c468..3a8de53 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent.rs @@ -21,7 +21,7 @@ use arbiter_proto::{ SdkClientEntry as ProtoSdkClientEntry, SdkClientError as ProtoSdkClientError, SdkClientGrantWalletAccess, SdkClientList as ProtoSdkClientList, SdkClientListResponse as ProtoSdkClientListResponse, SdkClientRevokeWalletAccess, - SdkClientWalletAccess, UnsealEncryptedKey as ProtoUnsealEncryptedKey, + UnsealEncryptedKey as ProtoUnsealEncryptedKey, UnsealResult as ProtoUnsealResult, UnsealStart, UserAgentRequest, UserAgentResponse, VaultState as ProtoVaultState, sdk_client_list_response::Result as ProtoSdkClientListResult, @@ -53,7 +53,7 @@ use crate::{ }, }, }, - db::models::{CoreEvmWalletAccess, NewEvmWalletAccess}, + db::models::NewEvmWalletAccess, grpc::{Convert, TryConvert, request_tracker::RequestTracker}, }; mod auth; @@ -158,285 +158,336 @@ async fn dispatch_inner( actor: &ActorRef, payload: UserAgentRequestPayload, ) -> Result, Status> { - let response = match payload { - UserAgentRequestPayload::UnsealStart(UnsealStart { client_pubkey }) => { - let client_pubkey = <[u8; 32]>::try_from(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") - })?; - - UserAgentResponsePayload::UnsealStartResponse( - arbiter_proto::proto::user_agent::UnsealStartResponse { - server_pubkey: response.server_pubkey.as_bytes().to_vec(), - }, - ) + match payload { + UserAgentRequestPayload::UnsealStart(req) => handle_unseal_start(actor, req).await, + UserAgentRequestPayload::UnsealEncryptedKey(req) => { + handle_unseal_encrypted_key(actor, req).await } - - UserAgentRequestPayload::UnsealEncryptedKey(ProtoUnsealEncryptedKey { - nonce, - ciphertext, - associated_data, - }) => { - let result = 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"); - return Err(Status::internal("Failed to unseal vault")); - } - }; - UserAgentResponsePayload::UnsealResult(result.into()) + UserAgentRequestPayload::BootstrapEncryptedKey(req) => { + handle_bootstrap_encrypted_key(actor, req).await } - - UserAgentRequestPayload::BootstrapEncryptedKey(ProtoBootstrapEncryptedKey { - nonce, - ciphertext, - associated_data, - }) => { - let result = 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"); - return Err(Status::internal("Failed to bootstrap vault")); - } - }; - UserAgentResponsePayload::BootstrapResult(result.into()) - } - - UserAgentRequestPayload::QueryVaultState(_) => { - 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 - } - }; - UserAgentResponsePayload::VaultState(state.into()) - } - - UserAgentRequestPayload::EvmWalletCreate(_) => { - 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()) - } - }; - UserAgentResponsePayload::EvmWalletCreate(WalletCreateResponse { - result: Some(result), - }) - } - - UserAgentRequestPayload::EvmWalletList(_) => { - 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()) - } - }; - UserAgentResponsePayload::EvmWalletList(WalletListResponse { - result: Some(result), - }) - } - - UserAgentRequestPayload::EvmGrantList(_) => { - 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()) - } - }; - UserAgentResponsePayload::EvmGrantList(EvmGrantListResponse { - result: Some(result), - }) - } - - UserAgentRequestPayload::EvmGrantCreate(EvmGrantCreateRequest { shared, specific }) => { - let basic = shared - .ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))? - .try_convert()?; - let grant = 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()) - } - }; - UserAgentResponsePayload::EvmGrantCreate(EvmGrantCreateResponse { - result: Some(result), - }) - } - - UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id }) => { - let result = match actor.ask(HandleGrantDelete { grant_id }).await { - Ok(()) => EvmGrantDeleteResult::Ok(()), - Err(err) => { - warn!(error = ?err, "Failed to delete EVM grant"); - EvmGrantDeleteResult::Error(ProtoEvmError::Internal.into()) - } - }; - UserAgentResponsePayload::EvmGrantDelete(EvmGrantDeleteResponse { - result: Some(result), - }) - } - + 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) => { - 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") - })?; - - return Ok(None); + handle_sdk_client_connection_response(actor, resp).await } - - UserAgentRequestPayload::SdkClientRevoke(_) => todo!(), - - UserAgentRequestPayload::SdkClientList(_) => { - 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()) - } - }; - UserAgentResponsePayload::SdkClientListResponse(ProtoSdkClientListResponse { - result: Some(result), - }) + UserAgentRequestPayload::SdkClientRevoke(_) => { + Err(Status::unimplemented("SdkClientRevoke is not yet implemented")) } - - UserAgentRequestPayload::GrantWalletAccess(SdkClientGrantWalletAccess { accesses }) => { - let entries: Vec = - accesses.into_iter().map(|a| a.convert()).collect(); - - match actor.ask(HandleGrantEvmWalletAccess { entries }).await { - Ok(()) => { - info!("Successfully granted wallet access"); - return Ok(None); - } - Err(err) => { - warn!(error = ?err, "Failed to grant wallet access"); - return Err(Status::internal("Failed to grant wallet access")); - } - } + UserAgentRequestPayload::SdkClientList(_) => handle_sdk_client_list(actor).await, + UserAgentRequestPayload::GrantWalletAccess(req) => { + handle_grant_wallet_access(actor, req).await } - - UserAgentRequestPayload::RevokeWalletAccess(SdkClientRevokeWalletAccess { accesses }) => { - match actor.ask(HandleRevokeEvmWalletAccess { entries: accesses }).await { - Ok(()) => { - info!("Successfully revoked wallet access"); - return Ok(None); - } - Err(err) => { - warn!(error = ?err, "Failed to revoke wallet access"); - return Err(Status::internal("Failed to revoke wallet access")); - } - } + UserAgentRequestPayload::RevokeWalletAccess(req) => { + handle_revoke_wallet_access(actor, req).await } - - UserAgentRequestPayload::ListWalletAccess(_) => { - let result = match actor.ask(HandleListWalletAccess {}).await { - Ok(accesses) => ListWalletAccessResponse { - accesses: accesses.into_iter().map(|a| a.convert()).collect(), - }, - Err(err) => { - warn!(error = ?err, "Failed to list wallet access"); - return Err(Status::internal("Failed to list wallet access")); - } - }; - UserAgentResponsePayload::ListWalletAccessResponse(result) - } - + UserAgentRequestPayload::ListWalletAccess(_) => handle_list_wallet_access(actor).await, UserAgentRequestPayload::AuthChallengeRequest(..) | UserAgentRequestPayload::AuthChallengeSolution(..) => { warn!(?payload, "Unsupported post-auth user agent request"); - return Err(Status::invalid_argument("Unsupported user-agent 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()))) +} - Ok(Some(response)) +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(