Merge pull request 'Grant management and vault UI' (#35) from push-zpvzkqpmzrur into main
Some checks failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-lint Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful

Had to merge this because in process of refactoring and would pollute this PR.

Reviewed-on: #35
This commit was merged in pull request #35.
This commit is contained in:
2026-03-18 21:23:22 +00:00
154 changed files with 15641 additions and 2373 deletions

128
AGENTS.md Normal file
View File

@@ -0,0 +1,128 @@
# AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
## Project Overview
Arbiter is a **permissioned signing service** for cryptocurrency wallets. It consists of:
- **`server/`** — Rust gRPC daemon that holds encrypted keys and enforces policies
- **`useragent/`** — Flutter desktop app (macOS/Windows) with a Rust backend via Rinf
- **`protobufs/`** — Protocol Buffer definitions shared between server and client
The vault never exposes key material; it only produces signatures when requests satisfy configured policies.
## Toolchain Setup
Tools are managed via [mise](https://mise.jdx.dev/). Install all required tools:
```sh
mise install
```
Key versions: Rust 1.93.0 (with clippy), Flutter 3.38.9-stable, protoc 29.6, diesel_cli 2.3.6 (sqlite).
## Server (Rust workspace at `server/`)
### Crates
| Crate | Purpose |
|---|---|
| `arbiter-proto` | Generated gRPC stubs + protobuf types; compiled from `protobufs/*.proto` via `tonic-prost-build` |
| `arbiter-server` | Main daemon — actors, DB, EVM policy engine, gRPC service implementation |
| `arbiter-useragent` | Rust client library for the user agent side of the gRPC protocol |
| `arbiter-client` | Rust client library for SDK clients |
### Common Commands
```sh
cd server
# Build
cargo build
# Run the server daemon
cargo run -p arbiter-server
# Run all tests (preferred over cargo test)
cargo nextest run
# Run a single test
cargo nextest run <test_name>
# Lint
cargo clippy
# Security audit
cargo audit
# Check unused dependencies
cargo shear
# Run snapshot tests and update snapshots
cargo insta review
```
### Architecture
The server is actor-based using the **kameo** crate. All long-lived state lives in `GlobalActors`:
- **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run.
- **`KeyHolder`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell.
- **`MessageRouter`** — Coordinates streaming messages between user agents and SDK clients.
- **`EvmActor`** — Handles EVM transaction policy enforcement and signing.
Per-connection actors live under `actors/user_agent/` and `actors/client/`, each with `auth` (challenge-response authentication) and `session` (post-auth operations) sub-modules.
**Database:** SQLite via `diesel-async` + `bb8` connection pool. Schema managed by embedded Diesel migrations in `crates/arbiter-server/migrations/`. DB file lives at `~/.arbiter/arbiter.sqlite`. Tests use a temp-file DB via `db::create_test_pool()`.
**Cryptography:**
- Authentication: ed25519 (challenge-response, nonce-tracked per peer)
- Encryption at rest: XChaCha20-Poly1305 (versioned via `scheme` field for transparent migration on unseal)
- Password KDF: Argon2
- Unseal transport: X25519 ephemeral key exchange
- TLS: self-signed certificate (aws-lc-rs backend), fingerprint distributed via `ArbiterUrl`
**Protocol:** gRPC with Protocol Buffers. The `ArbiterUrl` type encodes host, port, CA cert, and bootstrap token into a single shareable string (printed to console on first run).
### Proto Regeneration
When `.proto` files in `protobufs/` change, rebuild to regenerate:
```sh
cd server && cargo build -p arbiter-proto
```
### Database Migrations
```sh
# Create a new migration
diesel migration generate <name> --migration-dir crates/arbiter-server/migrations
# Run migrations manually (server also runs them on startup)
diesel migration run --migration-dir crates/arbiter-server/migrations
```
## User Agent (Flutter + Rinf at `useragent/`)
The Flutter app uses [Rinf](https://rinf.cunarist.org) to call Rust code. The Rust logic lives in `useragent/native/hub/` as a separate crate that uses `arbiter-useragent` for the gRPC client.
Communication between Dart and Rust uses typed **signals** defined in `useragent/native/hub/src/signals/`. After modifying signal structs, regenerate Dart bindings:
```sh
cd useragent && rinf gen
```
### Common Commands
```sh
cd useragent
# Run the app (macOS or Windows)
flutter run
# Regenerate Rust↔Dart signal bindings
rinf gen
# Analyze Dart code
flutter analyze
```
The Rinf Rust entry point is `useragent/native/hub/src/lib.rs`. It spawns actors defined in `useragent/native/hub/src/actors/` which handle Dart↔server communication via signals.

128
CLAUDE.md Normal file
View File

@@ -0,0 +1,128 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Arbiter is a **permissioned signing service** for cryptocurrency wallets. It consists of:
- **`server/`** — Rust gRPC daemon that holds encrypted keys and enforces policies
- **`useragent/`** — Flutter desktop app (macOS/Windows) with a Rust backend via Rinf
- **`protobufs/`** — Protocol Buffer definitions shared between server and client
The vault never exposes key material; it only produces signatures when requests satisfy configured policies.
## Toolchain Setup
Tools are managed via [mise](https://mise.jdx.dev/). Install all required tools:
```sh
mise install
```
Key versions: Rust 1.93.0 (with clippy), Flutter 3.38.9-stable, protoc 29.6, diesel_cli 2.3.6 (sqlite).
## Server (Rust workspace at `server/`)
### Crates
| Crate | Purpose |
|---|---|
| `arbiter-proto` | Generated gRPC stubs + protobuf types; compiled from `protobufs/*.proto` via `tonic-prost-build` |
| `arbiter-server` | Main daemon — actors, DB, EVM policy engine, gRPC service implementation |
| `arbiter-useragent` | Rust client library for the user agent side of the gRPC protocol |
| `arbiter-client` | Rust client library for SDK clients |
### Common Commands
```sh
cd server
# Build
cargo build
# Run the server daemon
cargo run -p arbiter-server
# Run all tests (preferred over cargo test)
cargo nextest run
# Run a single test
cargo nextest run <test_name>
# Lint
cargo clippy
# Security audit
cargo audit
# Check unused dependencies
cargo shear
# Run snapshot tests and update snapshots
cargo insta review
```
### Architecture
The server is actor-based using the **kameo** crate. All long-lived state lives in `GlobalActors`:
- **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run.
- **`KeyHolder`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell.
- **`MessageRouter`** — Coordinates streaming messages between user agents and SDK clients.
- **`EvmActor`** — Handles EVM transaction policy enforcement and signing.
Per-connection actors live under `actors/user_agent/` and `actors/client/`, each with `auth` (challenge-response authentication) and `session` (post-auth operations) sub-modules.
**Database:** SQLite via `diesel-async` + `bb8` connection pool. Schema managed by embedded Diesel migrations in `crates/arbiter-server/migrations/`. DB file lives at `~/.arbiter/arbiter.sqlite`. Tests use a temp-file DB via `db::create_test_pool()`.
**Cryptography:**
- Authentication: ed25519 (challenge-response, nonce-tracked per peer)
- Encryption at rest: XChaCha20-Poly1305 (versioned via `scheme` field for transparent migration on unseal)
- Password KDF: Argon2
- Unseal transport: X25519 ephemeral key exchange
- TLS: self-signed certificate (aws-lc-rs backend), fingerprint distributed via `ArbiterUrl`
**Protocol:** gRPC with Protocol Buffers. The `ArbiterUrl` type encodes host, port, CA cert, and bootstrap token into a single shareable string (printed to console on first run).
### Proto Regeneration
When `.proto` files in `protobufs/` change, rebuild to regenerate:
```sh
cd server && cargo build -p arbiter-proto
```
### Database Migrations
```sh
# Create a new migration
diesel migration generate <name> --migration-dir crates/arbiter-server/migrations
# Run migrations manually (server also runs them on startup)
diesel migration run --migration-dir crates/arbiter-server/migrations
```
## User Agent (Flutter + Rinf at `useragent/`)
The Flutter app uses [Rinf](https://rinf.cunarist.org) to call Rust code. The Rust logic lives in `useragent/native/hub/` as a separate crate that uses `arbiter-useragent` for the gRPC client.
Communication between Dart and Rust uses typed **signals** defined in `useragent/native/hub/src/signals/`. After modifying signal structs, regenerate Dart bindings:
```sh
cd useragent && rinf gen
```
### Common Commands
```sh
cd useragent
# Run the app (macOS or Windows)
flutter run
# Regenerate Rust↔Dart signal bindings
rinf gen
# Analyze Dart code
flutter analyze
```
The Rinf Rust entry point is `useragent/native/hub/src/lib.rs`. It spawns actors defined in `useragent/native/hub/src/actors/` which handle Dart↔server communication via signals.

View File

@@ -1,3 +1,12 @@
[[tools.ast-grep]]
version = "0.42.0"
backend = "aqua:ast-grep/ast-grep"
"platforms.linux-arm64" = { checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836", url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip"}
"platforms.linux-x64" = { checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651", url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip"}
"platforms.macos-arm64" = { checksum = "sha256:fc300d5293b1c770a5aece03a8a193b92e71e87cec726c28096990691a582620", url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-apple-darwin.zip"}
"platforms.macos-x64" = { checksum = "sha256:979ffe611327056f4730a1ae71b0209b3b830f58b22c6ed194cda34f55400db2", url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-apple-darwin.zip"}
"platforms.windows-x64" = { checksum = "sha256:55836fa1b2c65dc7d61615a4d9368622a0d2371a76d28b9a165e5a3ab6ae32a4", url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-pc-windows-msvc.zip"}
[[tools."cargo:cargo-audit"]]
version = "0.22.1"
backend = "cargo:cargo-audit"
@@ -42,6 +51,10 @@ backend = "cargo:diesel_cli"
default-features = "false"
features = "sqlite,sqlite-bundled"
[[tools."cargo:rinf_cli"]]
version = "8.9.1"
backend = "cargo:rinf_cli"
[[tools.flutter]]
version = "3.38.9-stable"
backend = "asdf:flutter"

View File

@@ -10,3 +10,12 @@ protoc = "29.6"
"cargo:cargo-shear" = "latest"
"cargo:cargo-insta" = "1.46.3"
python = "3.14.3"
ast-grep = "0.42.0"
[tasks.codegen]
sources = ['protobufs/*.proto']
outputs = ['useragent/lib/proto/*']
run = '''
dart pub global activate protoc_plugin && \
protoc --dart_out=grpc:useragent/lib/proto --proto_path=protobufs/ protobufs/*.proto
'''

View File

@@ -42,6 +42,12 @@ message UnsealEncryptedKey {
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;
@@ -49,6 +55,13 @@ enum UnsealResult {
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;
@@ -80,6 +93,7 @@ message UserAgentRequest {
arbiter.evm.EvmGrantDeleteRequest evm_grant_delete = 9;
arbiter.evm.EvmGrantListRequest evm_grant_list = 10;
ClientConnectionResponse client_connection_response = 11;
BootstrapEncryptedKey bootstrap_encrypted_key = 12;
}
}
message UserAgentResponse {
@@ -96,5 +110,6 @@ message UserAgentResponse {
arbiter.evm.EvmGrantListResponse evm_grant_list = 10;
ClientConnectionRequest client_connection_request = 11;
ClientConnectionCancel client_connection_cancel = 12;
BootstrapResult bootstrap_result = 13;
}
}

25
server/Cargo.lock generated
View File

@@ -727,6 +727,7 @@ dependencies = [
"memsafe",
"miette",
"pem",
"prost-types",
"rand 0.10.0",
"rcgen",
"restructed",
@@ -755,30 +756,6 @@ dependencies = [
"alloy",
]
[[package]]
name = "arbiter-useragent"
version = "0.1.0"
dependencies = [
"arbiter-proto",
"async-trait",
"ed25519-dalek",
"http",
"k256",
"kameo",
"rand 0.10.0",
"rsa",
"rustls-webpki",
"sha2 0.10.9",
"smlang",
"spki",
"thiserror",
"tokio",
"tokio-stream",
"tonic",
"tracing",
"x25519-dalek",
]
[[package]]
name = "argon2"
version = "0.5.3"

View File

@@ -1,78 +1,39 @@
//! Transport-facing abstractions for protocol/session code.
//! Transport-facing abstractions shared by protocol/session code.
//!
//! This module separates three concerns:
//!
//! - protocol/session logic wants a small duplex interface ([`Bi`])
//! - transport adapters push concrete stream items to an underlying IO layer
//! - transport boundaries translate between protocol-facing and transport-facing
//! item types via direction-specific converters
//! This module defines a small duplex interface, [`Bi`], 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 protocol messages
//! - [`Bi::send`] accepts outbound protocol/domain items
//! - [`Bi::recv`] yields inbound messages
//! - [`Bi::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`.
//!
//! # Generic Ordering Rule
//!
//! This module uses a single convention consistently: when a type or trait is
//! parameterized by protocol message directions, the generic parameters are
//! declared as `Inbound` first, then `Outbound`.
//! This module consistently uses `Inbound` first and `Outbound` second in
//! generic parameter lists.
//!
//! For [`Bi`], that means `Bi<Inbound, Outbound>`:
//! - `recv() -> Option<Inbound>`
//! - `send(Outbound)`
//!
//! For adapter types that are parameterized by direction-specific converters,
//! inbound-related converter parameters are declared before outbound-related
//! converter parameters.
//! [`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.
//!
//! [`RecvConverter`] and [`SendConverter`] are infallible conversion traits used
//! by adapters to map between protocol-facing and transport-facing item types.
//! The traits themselves are not result-aware; adapters decide how transport
//! errors are handled before (or instead of) conversion.
//!
//! [`grpc::GrpcAdapter`] combines:
//! - a tonic inbound stream
//! - a Tokio sender for outbound transport items
//! - a [`RecvConverter`] for the receive path
//! - a [`SendConverter`] for the send path
//!
//! [`DummyTransport`] is a no-op implementation useful for tests and local actor
//! execution where no real network stream exists.
//!
//! # Component Interaction
//!
//! ```text
//! inbound (network -> protocol)
//! ============================
//!
//! tonic::Streaming<RecvTransport>
//! -> grpc::GrpcAdapter::recv()
//! |
//! +--> on `Ok(item)`: RecvConverter::convert(RecvTransport) -> Inbound
//! +--> on `Err(status)`: log error and close stream (`None`)
//! -> Bi::recv()
//! -> protocol/session actor
//!
//! outbound (protocol -> network)
//! ==============================
//!
//! protocol/session actor
//! -> Bi::send(Outbound)
//! -> grpc::GrpcAdapter::send()
//! |
//! +--> SendConverter::convert(Outbound) -> SendTransport
//! -> Tokio mpsc::Sender<SendTransport>
//! -> tonic response stream
//! ```
//! [`DummyTransport`] is a no-op implementation useful for tests and local
//! actor execution where no real stream exists.
//!
//! # Design Notes
//!
//! - `send()` returns [`Error`] only for transport delivery failures (for
//! example, when the outbound channel is closed).
//! - [`grpc::GrpcAdapter`] logs tonic receive errors and treats them as stream
//! closure (`None`).
//! - When protocol-facing and transport-facing types are identical, use
//! [`IdentityRecvConverter`] / [`IdentitySendConverter`].
//! - [`Bi::send`] returns [`Error`] only for transport delivery failures, such
//! as a closed outbound channel.
//! - [`Bi::recv`] returns `None` when the underlying transport closes.
//! - Message translation is intentionally out of scope for this module.
use std::marker::PhantomData;
@@ -114,162 +75,6 @@ pub trait Bi<Inbound, Outbound>: Send + Sync + 'static {
async fn recv(&mut self) -> Option<Inbound>;
}
/// Converts transport-facing inbound items into protocol-facing inbound items.
pub trait RecvConverter: Send + Sync + 'static {
type Input;
type Output;
fn convert(&self, item: Self::Input) -> Self::Output;
}
/// Converts protocol/domain outbound items into transport-facing outbound items.
pub trait SendConverter: Send + Sync + 'static {
type Input;
type Output;
fn convert(&self, item: Self::Input) -> Self::Output;
}
/// A [`RecvConverter`] that forwards values unchanged.
pub struct IdentityRecvConverter<T> {
_marker: PhantomData<T>,
}
impl<T> IdentityRecvConverter<T> {
pub fn new() -> Self {
Self {
_marker: PhantomData,
}
}
}
impl<T> Default for IdentityRecvConverter<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> RecvConverter for IdentityRecvConverter<T>
where
T: Send + Sync + 'static,
{
type Input = T;
type Output = T;
fn convert(&self, item: Self::Input) -> Self::Output {
item
}
}
/// A [`SendConverter`] that forwards values unchanged.
pub struct IdentitySendConverter<T> {
_marker: PhantomData<T>,
}
impl<T> IdentitySendConverter<T> {
pub fn new() -> Self {
Self {
_marker: PhantomData,
}
}
}
impl<T> Default for IdentitySendConverter<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> SendConverter for IdentitySendConverter<T>
where
T: Send + Sync + 'static,
{
type Input = T;
type Output = T;
fn convert(&self, item: Self::Input) -> Self::Output {
item
}
}
/// gRPC-specific transport adapters and helpers.
pub mod grpc {
use async_trait::async_trait;
use futures::StreamExt;
use tokio::sync::mpsc;
use tonic::Streaming;
use super::{Bi, Error, RecvConverter, SendConverter};
/// [`Bi`] adapter backed by a tonic gRPC bidirectional stream.
///
/// Tonic receive errors are logged and treated as stream closure (`None`).
/// The receive converter is only invoked for successful inbound transport
/// items.
pub struct GrpcAdapter<InboundConverter, OutboundConverter>
where
InboundConverter: RecvConverter,
OutboundConverter: SendConverter,
{
sender: mpsc::Sender<OutboundConverter::Output>,
receiver: Streaming<InboundConverter::Input>,
inbound_converter: InboundConverter,
outbound_converter: OutboundConverter,
}
impl<InboundTransport, Inbound, InboundConverter, OutboundConverter>
GrpcAdapter<InboundConverter, OutboundConverter>
where
InboundConverter: RecvConverter<Input = InboundTransport, Output = Inbound>,
OutboundConverter: SendConverter,
{
pub fn new(
sender: mpsc::Sender<OutboundConverter::Output>,
receiver: Streaming<InboundTransport>,
inbound_converter: InboundConverter,
outbound_converter: OutboundConverter,
) -> Self {
Self {
sender,
receiver,
inbound_converter,
outbound_converter,
}
}
}
#[async_trait]
impl<InboundConverter, OutboundConverter> Bi<InboundConverter::Output, OutboundConverter::Input>
for GrpcAdapter<InboundConverter, OutboundConverter>
where
InboundConverter: RecvConverter,
OutboundConverter: SendConverter,
OutboundConverter::Input: Send + 'static,
OutboundConverter::Output: Send + 'static,
{
#[tracing::instrument(level = "trace", skip(self, item))]
async fn send(&mut self, item: OutboundConverter::Input) -> Result<(), Error> {
let outbound = self.outbound_converter.convert(item);
self.sender
.send(outbound)
.await
.map_err(|_| Error::ChannelClosed)
}
#[tracing::instrument(level = "trace", skip(self))]
async fn recv(&mut self) -> Option<InboundConverter::Output> {
match self.receiver.next().await {
Some(Ok(item)) => Some(self.inbound_converter.convert(item)),
Some(Err(error)) => {
tracing::error!(error = ?error, "grpc transport recv failed; closing stream");
None
}
None => None,
}
}
}
}
/// No-op [`Bi`] transport for tests and manual actor usage.
///
/// `send` drops all items and succeeds. [`Bi::recv`] never resolves and therefore

View File

@@ -50,6 +50,7 @@ rsa.workspace = true
sha2.workspace = true
spki.workspace = true
alloy.workspace = true
prost-types.workspace = true
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
[dev-dependencies]

View File

@@ -3,12 +3,7 @@ use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use kameo::{Actor, messages};
use miette::Diagnostic;
use rand::{
RngExt,
distr::{Alphanumeric},
make_rng,
rngs::StdRng,
};
use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng};
use thiserror::Error;
use crate::db::{self, DatabasePool, schema};
@@ -61,7 +56,6 @@ impl Bootstrapper {
drop(conn);
let token = if row_count == 0 {
let token = generate_token().await?;
Some(token)

View File

@@ -1,13 +1,4 @@
use arbiter_proto::{
format_challenge,
proto::client::{
AuthChallenge, AuthChallengeSolution, ClientConnectError, ClientRequest, ClientResponse,
client_connect_error::Code as ConnectErrorCode,
client_request::Payload as ClientRequestPayload,
client_response::Payload as ClientResponsePayload,
},
transport::expect_message,
};
use arbiter_proto::{format_challenge, transport::expect_message};
use diesel::{
ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, dsl::insert_into, update,
};
@@ -18,7 +9,7 @@ use tracing::error;
use crate::{
actors::{
client::ClientConnection,
client::{ClientConnection, ConnectErrorCode, Request, Response},
router::{self, RequestClientApproval},
},
db::{self, schema::program_client},
@@ -155,15 +146,13 @@ async fn challenge_client(
pubkey: VerifyingKey,
nonce: i32,
) -> Result<(), Error> {
let challenge = AuthChallenge {
pubkey: pubkey.as_bytes().to_vec(),
nonce,
};
let challenge_pubkey = pubkey.as_bytes().to_vec();
props
.transport
.send(Ok(ClientResponse {
payload: Some(ClientResponsePayload::AuthChallenge(challenge.clone())),
.send(Ok(Response::AuthChallenge {
pubkey: challenge_pubkey.clone(),
nonce,
}))
.await
.map_err(|e| {
@@ -171,12 +160,9 @@ async fn challenge_client(
Error::Transport
})?;
let AuthChallengeSolution { signature } =
expect_message(&mut *props.transport, |req: ClientRequest| {
match req.payload? {
ClientRequestPayload::AuthChallengeSolution(s) => Some(s),
let signature = expect_message(&mut *props.transport, |req: Request| match req {
Request::AuthChallengeSolution { signature } => Some(signature),
_ => None,
}
})
.await
.map_err(|e| {
@@ -184,7 +170,7 @@ async fn challenge_client(
Error::Transport
})?;
let formatted = format_challenge(nonce, &challenge.pubkey);
let formatted = format_challenge(nonce, &challenge_pubkey);
let sig = signature.as_slice().try_into().map_err(|_| {
error!("Invalid signature length");
Error::InvalidChallengeSolution
@@ -209,15 +195,14 @@ fn connect_error_code(err: &Error) -> ConnectErrorCode {
}
async fn authenticate(props: &mut ClientConnection) -> Result<VerifyingKey, Error> {
let Some(ClientRequest {
payload: Some(ClientRequestPayload::AuthChallengeRequest(challenge)),
let Some(Request::AuthChallengeRequest {
pubkey: challenge_pubkey,
}) = props.transport.recv().await
else {
return Err(Error::Transport);
};
let pubkey_bytes = challenge
.pubkey
let pubkey_bytes = challenge_pubkey
.as_array()
.ok_or(Error::InvalidClientPubkeyLength)?;
let pubkey =
@@ -244,11 +229,7 @@ pub async fn authenticate_and_create(mut props: ClientConnection) -> Result<Clie
let code = connect_error_code(&err);
let _ = props
.transport
.send(Ok(ClientResponse {
payload: Some(ClientResponsePayload::ClientConnectError(
ClientConnectError { code: code.into() },
)),
}))
.send(Ok(Response::ClientConnectError { code }))
.await;
Err(err)
}

View File

@@ -1,7 +1,4 @@
use arbiter_proto::{
proto::client::{ClientRequest, ClientResponse},
transport::Bi,
};
use arbiter_proto::transport::Bi;
use kameo::actor::Spawn;
use tracing::{error, info};
@@ -24,7 +21,27 @@ pub enum ClientError {
Auth(#[from] auth::Error),
}
pub type Transport = Box<dyn Bi<ClientRequest, Result<ClientResponse, ClientError>> + Send>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectErrorCode {
Unknown,
ApprovalDenied,
NoUserAgentsOnline,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Request {
AuthChallengeRequest { pubkey: Vec<u8> },
AuthChallengeSolution { signature: Vec<u8> },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Response {
AuthChallenge { pubkey: Vec<u8>, nonce: i32 },
AuthOk,
ClientConnectError { code: ConnectErrorCode },
}
pub type Transport = Box<dyn Bi<Request, Result<Response, ClientError>> + Send>;
pub struct ClientConnection {
pub(crate) db: db::DatabasePool,

View File

@@ -1,11 +1,15 @@
use arbiter_proto::proto::client::{ClientRequest, ClientResponse};
use kameo::Actor;
use tokio::select;
use tracing::{error, info};
use crate::{actors::{
GlobalActors, client::{ClientError, ClientConnection}, router::RegisterClient
}, db};
use crate::{
actors::{
GlobalActors,
client::{ClientConnection, ClientError, Request, Response},
router::RegisterClient,
},
db,
};
pub struct ClientSession {
props: ClientConnection,
@@ -16,18 +20,13 @@ impl ClientSession {
Self { props }
}
pub async fn process_transport_inbound(&mut self, req: ClientRequest) -> Output {
let msg = req.payload.ok_or_else(|| {
error!(actor = "client", "Received message with no payload");
ClientError::MissingRequestPayload
})?;
let _ = msg;
pub async fn process_transport_inbound(&mut self, req: Request) -> Output {
let _ = req;
Err(ClientError::UnexpectedRequestPayload)
}
}
type Output = Result<ClientResponse, ClientError>;
type Output = Result<Response, ClientError>;
impl Actor for ClientSession {
type Args = Self;

View File

@@ -1,21 +1,26 @@
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
use diesel::{ExpressionMethods, OptionalExtension as _, QueryDsl, SelectableHelper as _, dsl::insert_into};
use diesel::{
ExpressionMethods, OptionalExtension as _, QueryDsl, SelectableHelper as _, dsl::insert_into,
};
use diesel_async::RunQueryDsl;
use kameo::{Actor, actor::ActorRef, messages};
use memsafe::MemSafe;
use rand::{SeedableRng, rng, rngs::StdRng};
use crate::{
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
db::{self, DatabasePool, models::{self, EvmBasicGrant, SqliteTimestamp}, schema},
db::{
self, DatabasePool,
models::{self, SqliteTimestamp},
schema,
},
evm::{
self, RunKind,
self, ListGrantsError, RunKind,
policies::{
FullGrant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
ether_transfer::EtherTransfer,
token_transfers::TokenTransfer,
FullGrant, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
},
},
safe_cell::{SafeCell, SafeCellHandle as _},
};
pub use crate::evm::safe_signer;
@@ -88,7 +93,12 @@ impl EvmActor {
// todo: audit
let rng = StdRng::from_rng(&mut rng());
let engine = evm::Engine::new(db.clone());
Self { keyholder, db, rng, engine }
Self {
keyholder,
db,
rng,
engine,
}
}
}
@@ -98,11 +108,7 @@ impl EvmActor {
pub async fn generate(&mut self) -> Result<Address, Error> {
let (mut key_cell, address) = safe_signer::generate(&mut self.rng);
// Move raw key bytes into a Vec<u8> MemSafe for KeyHolder
let plaintext = {
let reader = key_cell.read().expect("MemSafe read");
MemSafe::new(reader.to_vec()).expect("MemSafe allocation")
};
let plaintext = key_cell.read_inline(|reader| SafeCell::new(reader.to_vec()));
let aead_id: i32 = self
.keyholder
@@ -149,12 +155,24 @@ impl EvmActor {
match grant {
SpecificGrant::EtherTransfer(settings) => {
self.engine
.create_grant::<EtherTransfer>(client_id, FullGrant { basic, specific: settings })
.create_grant::<EtherTransfer>(
client_id,
FullGrant {
basic,
specific: settings,
},
)
.await
}
SpecificGrant::TokenTransfer(settings) => {
self.engine
.create_grant::<TokenTransfer>(client_id, FullGrant { basic, specific: settings })
.create_grant::<TokenTransfer>(
client_id,
FullGrant {
basic,
specific: settings,
},
)
.await
}
}
@@ -172,19 +190,12 @@ impl EvmActor {
}
#[message]
pub async fn useragent_list_grants(
&mut self,
wallet_id: Option<i32>,
) -> Result<Vec<EvmBasicGrant>, Error> {
let mut conn = self.db.get().await?;
let mut query = schema::evm_basic_grant::table
.select(EvmBasicGrant::as_select())
.filter(schema::evm_basic_grant::revoked_at.is_null())
.into_boxed();
if let Some(wid) = wallet_id {
query = query.filter(schema::evm_basic_grant::wallet_id.eq(wid));
pub async fn useragent_list_grants(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
match self.engine.list_all_grants().await {
Ok(grants) => Ok(grants),
Err(ListGrantsError::Database(db)) => Err(Error::Database(db)),
Err(ListGrantsError::Pool(pool)) => Err(Error::DatabasePool(pool)),
}
Ok(query.load(&mut conn).await?)
}
#[message]
@@ -204,8 +215,14 @@ impl EvmActor {
.ok_or(SignTransactionError::WalletNotFound)?;
drop(conn);
let meaning = self.engine
.evaluate_transaction(wallet.id, client_id, transaction.clone(), RunKind::Execution)
let meaning = self
.engine
.evaluate_transaction(
wallet.id,
client_id,
transaction.clone(),
RunKind::Execution,
)
.await?;
Ok(meaning)
@@ -228,16 +245,23 @@ impl EvmActor {
.ok_or(SignTransactionError::WalletNotFound)?;
drop(conn);
let raw_key: MemSafe<Vec<u8>> = self
let raw_key: SafeCell<Vec<u8>> = self
.keyholder
.ask(Decrypt { aead_id: wallet.aead_encrypted_id })
.ask(Decrypt {
aead_id: wallet.aead_encrypted_id,
})
.await
.map_err(|_| SignTransactionError::KeyholderSend)?;
let signer = safe_signer::SafeSigner::from_memsafe(raw_key)?;
let signer = safe_signer::SafeSigner::from_cell(raw_key)?;
self.engine
.evaluate_transaction(wallet.id, client_id, transaction.clone(), RunKind::Execution)
.evaluate_transaction(
wallet.id,
client_id,
transaction.clone(),
RunKind::Execution,
)
.await?;
use alloy::network::TxSignerSync as _;

View File

@@ -5,12 +5,13 @@ use chacha20poly1305::{
AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce,
aead::{AeadMut, Error, Payload},
};
use memsafe::MemSafe;
use rand::{
Rng as _, SeedableRng,
rngs::{StdRng, SysRng},
};
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes();
pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes();
@@ -47,40 +48,37 @@ impl<'a> TryFrom<&'a [u8]> for Nonce {
}
}
pub struct KeyCell(pub MemSafe<Key>);
impl From<MemSafe<Key>> for KeyCell {
fn from(value: MemSafe<Key>) -> Self {
pub struct KeyCell(pub SafeCell<Key>);
impl From<SafeCell<Key>> for KeyCell {
fn from(value: SafeCell<Key>) -> Self {
Self(value)
}
}
impl TryFrom<MemSafe<Vec<u8>>> for KeyCell {
impl TryFrom<SafeCell<Vec<u8>>> for KeyCell {
type Error = ();
fn try_from(mut value: MemSafe<Vec<u8>>) -> Result<Self, Self::Error> {
let value = value.read().unwrap();
fn try_from(mut value: SafeCell<Vec<u8>>) -> Result<Self, Self::Error> {
let value = value.read();
if value.len() != size_of::<Key>() {
return Err(());
}
let mut cell = MemSafe::new(Key::default()).unwrap();
{
let mut cell_write = cell.write().unwrap();
let cell_slice: &mut [u8] = cell_write.as_mut();
cell_slice.copy_from_slice(&value);
}
let cell = SafeCell::new_inline(|cell_write: &mut Key| {
cell_write.copy_from_slice(&value);
});
Ok(Self(cell))
}
}
impl KeyCell {
pub fn new_secure_random() -> Self {
let mut key = MemSafe::new(Key::default()).unwrap();
{
let mut key_buffer = key.write().unwrap();
let key_buffer: &mut [u8] = key_buffer.as_mut();
let key = SafeCell::new_inline(|key_buffer: &mut Key| {
#[allow(
clippy::unwrap_used,
reason = "Rng failure is unrecoverable and should panic"
)]
let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap();
rng.fill_bytes(key_buffer);
}
});
key.into()
}
@@ -91,7 +89,7 @@ impl KeyCell {
associated_data: &[u8],
mut buffer: impl AsMut<Vec<u8>>,
) -> Result<(), Error> {
let key_reader = self.0.read().unwrap();
let key_reader = self.0.read();
let key_ref = key_reader.deref();
let cipher = XChaCha20Poly1305::new(key_ref);
let nonce = XNonce::from_slice(nonce.0.as_ref());
@@ -102,13 +100,13 @@ impl KeyCell {
&mut self,
nonce: &Nonce,
associated_data: &[u8],
buffer: &mut MemSafe<Vec<u8>>,
buffer: &mut SafeCell<Vec<u8>>,
) -> Result<(), Error> {
let key_reader = self.0.read().unwrap();
let key_reader = self.0.read();
let key_ref = key_reader.deref();
let cipher = XChaCha20Poly1305::new(key_ref);
let nonce = XNonce::from_slice(nonce.0.as_ref());
let mut buffer = buffer.write().unwrap();
let mut buffer = buffer.write();
let buffer: &mut Vec<u8> = buffer.as_mut();
cipher.decrypt_in_place(nonce, associated_data, buffer)
}
@@ -119,7 +117,7 @@ impl KeyCell {
associated_data: &[u8],
plaintext: impl AsRef<[u8]>,
) -> Result<Vec<u8>, Error> {
let key_reader = self.0.read().unwrap();
let key_reader = self.0.read();
let key_ref = key_reader.deref();
let mut cipher = XChaCha20Poly1305::new(key_ref);
let nonce = XNonce::from_slice(nonce.0.as_ref());
@@ -139,6 +137,10 @@ pub type Salt = [u8; ArgonSalt::RECOMMENDED_LENGTH];
pub fn generate_salt() -> Salt {
let mut salt = Salt::default();
#[allow(
clippy::unwrap_used,
reason = "Rng failure is unrecoverable and should panic"
)]
let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap();
rng.fill_bytes(&mut salt);
salt
@@ -146,19 +148,23 @@ pub fn generate_salt() -> Salt {
/// User password might be of different length, have not enough entropy, etc...
/// Derive a fixed-length key from the password using Argon2id, which is designed for password hashing and key derivation.
pub fn derive_seal_key(mut password: MemSafe<Vec<u8>>, salt: &Salt) -> KeyCell {
pub fn derive_seal_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell {
#[allow(clippy::unwrap_used)]
let params = argon2::Params::new(262_144, 3, 4, None).unwrap();
let hasher = Argon2::new(Algorithm::Argon2id, argon2::Version::V0x13, params);
let mut key = MemSafe::new(Key::default()).unwrap();
{
let password_source = password.read().unwrap();
let mut key_buffer = key.write().unwrap();
let mut key = SafeCell::new(Key::default());
password.read_inline(|password_source| {
let mut key_buffer = key.write();
let key_buffer: &mut [u8] = key_buffer.as_mut();
#[allow(
clippy::unwrap_used,
reason = "Better fail completely than return a weak key"
)]
hasher
.hash_password_into(password_source.deref(), salt, key_buffer)
.unwrap();
}
});
key.into()
}
@@ -166,20 +172,20 @@ pub fn derive_seal_key(mut password: MemSafe<Vec<u8>>, salt: &Salt) -> KeyCell {
#[cfg(test)]
mod tests {
use super::*;
use memsafe::MemSafe;
use crate::safe_cell::SafeCell;
#[test]
pub fn derive_seal_key_deterministic() {
static PASSWORD: &[u8] = b"password";
let password = MemSafe::new(PASSWORD.to_vec()).unwrap();
let password2 = MemSafe::new(PASSWORD.to_vec()).unwrap();
let password = SafeCell::new(PASSWORD.to_vec());
let password2 = SafeCell::new(PASSWORD.to_vec());
let salt = generate_salt();
let mut key1 = derive_seal_key(password, &salt);
let mut key2 = derive_seal_key(password2, &salt);
let key1_reader = key1.0.read().unwrap();
let key2_reader = key2.0.read().unwrap();
let key1_reader = key1.0.read();
let key2_reader = key2.0.read();
assert_eq!(key1_reader.deref(), key2_reader.deref());
}
@@ -187,11 +193,11 @@ mod tests {
#[test]
pub fn successful_derive() {
static PASSWORD: &[u8] = b"password";
let password = MemSafe::new(PASSWORD.to_vec()).unwrap();
let password = SafeCell::new(PASSWORD.to_vec());
let salt = generate_salt();
let mut key = derive_seal_key(password, &salt);
let key_reader = key.0.read().unwrap();
let key_reader = key.0.read();
let key_ref = key_reader.deref();
assert_ne!(key_ref.as_slice(), &[0u8; 32][..]);
@@ -200,7 +206,7 @@ mod tests {
#[test]
pub fn encrypt_decrypt() {
static PASSWORD: &[u8] = b"password";
let password = MemSafe::new(PASSWORD.to_vec()).unwrap();
let password = SafeCell::new(PASSWORD.to_vec());
let salt = generate_salt();
let mut key = derive_seal_key(password, &salt);
@@ -212,12 +218,12 @@ mod tests {
.unwrap();
assert_ne!(buffer, b"secret data");
let mut buffer = MemSafe::new(buffer).unwrap();
let mut buffer = SafeCell::new(buffer);
key.decrypt_in_place(&nonce, associated_data, &mut buffer)
.unwrap();
let buffer = buffer.read().unwrap();
let buffer = buffer.read();
assert_eq!(*buffer, b"secret data");
}

View File

@@ -5,14 +5,17 @@ use diesel::{
};
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::{Actor, Reply, messages};
use memsafe::MemSafe;
use strum::{EnumDiscriminants, IntoDiscriminant};
use tracing::{error, info};
use crate::db::{
use crate::safe_cell::SafeCell;
use crate::{
db::{
self,
models::{self, RootKeyHistory},
schema::{self},
},
safe_cell::SafeCellHandle as _,
};
use encryption::v1::{self, KeyCell, Nonce};
@@ -136,7 +139,7 @@ impl KeyHolder {
}
#[message]
pub async fn bootstrap(&mut self, seal_key_raw: MemSafe<Vec<u8>>) -> Result<(), Error> {
pub async fn bootstrap(&mut self, seal_key_raw: SafeCell<Vec<u8>>) -> Result<(), Error> {
if !matches!(self.state, State::Unbootstrapped) {
return Err(Error::AlreadyBootstrapped);
}
@@ -148,16 +151,15 @@ impl KeyHolder {
let root_key_nonce = v1::Nonce::default();
let data_encryption_nonce = v1::Nonce::default();
let root_key_ciphertext: Vec<u8> = {
let root_key_reader = root_key.0.read().unwrap();
let root_key_reader = root_key_reader.as_slice();
let root_key_ciphertext: Vec<u8> = root_key.0.read_inline(|reader| {
let root_key_reader = reader.as_slice();
seal_key
.encrypt(&root_key_nonce, v1::ROOT_KEY_TAG, root_key_reader)
.map_err(|err| {
error!(?err, "Fatal bootstrap error");
Error::Encryption(err)
})?
};
})
})?;
let mut conn = self.db.get().await?;
@@ -199,7 +201,7 @@ impl KeyHolder {
}
#[message]
pub async fn try_unseal(&mut self, seal_key_raw: MemSafe<Vec<u8>>) -> Result<(), Error> {
pub async fn try_unseal(&mut self, seal_key_raw: SafeCell<Vec<u8>>) -> Result<(), Error> {
let State::Sealed {
root_key_history_id,
} = &self.state
@@ -225,7 +227,7 @@ impl KeyHolder {
})?;
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
let mut root_key = MemSafe::new(current_key.ciphertext.clone()).unwrap();
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
let nonce = v1::Nonce::try_from(current_key.root_key_encryption_nonce.as_slice()).map_err(
|_| {
@@ -256,7 +258,7 @@ impl KeyHolder {
// Decrypts the `aead_encrypted` entry with the given ID and returns the plaintext
#[message]
pub async fn decrypt(&mut self, aead_id: i32) -> Result<MemSafe<Vec<u8>>, Error> {
pub async fn decrypt(&mut self, aead_id: i32) -> Result<SafeCell<Vec<u8>>, Error> {
let State::Unsealed { root_key, .. } = &mut self.state else {
return Err(Error::NotBootstrapped);
};
@@ -279,14 +281,14 @@ impl KeyHolder {
);
Error::BrokenDatabase
})?;
let mut output = MemSafe::new(row.ciphertext).unwrap();
let mut output = SafeCell::new(row.ciphertext);
root_key.decrypt_in_place(&nonce, v1::TAG, &mut output)?;
Ok(output)
}
// Creates new `aead_encrypted` entry in the database and returns it's ID
#[message]
pub async fn create_new(&mut self, mut plaintext: MemSafe<Vec<u8>>) -> Result<i32, Error> {
pub async fn create_new(&mut self, mut plaintext: SafeCell<Vec<u8>>) -> Result<i32, Error> {
let State::Unsealed {
root_key,
root_key_history_id,
@@ -299,7 +301,7 @@ impl KeyHolder {
// Borrow checker note: &mut borrow a few lines above is disjoint from this field
let nonce = Self::get_new_nonce(&self.db, *root_key_history_id).await?;
let mut ciphertext_buffer = plaintext.write().unwrap();
let mut ciphertext_buffer = plaintext.write();
let ciphertext_buffer: &mut Vec<u8> = ciphertext_buffer.as_mut();
root_key.encrypt_in_place(&nonce, v1::TAG, &mut *ciphertext_buffer)?;
@@ -313,7 +315,7 @@ impl KeyHolder {
current_nonce: nonce.to_vec(),
schema_version: 1,
associated_root_key_id: *root_key_history_id,
created_at: Utc::now().into()
created_at: Utc::now().into(),
})
.returning(schema::aead_encrypted::id)
.get_result(&mut conn)
@@ -348,15 +350,17 @@ mod tests {
use diesel::SelectableHelper;
use diesel_async::RunQueryDsl;
use memsafe::MemSafe;
use crate::db::{self};
use crate::{
db::{self},
safe_cell::SafeCell,
};
use super::*;
async fn bootstrapped_actor(db: &db::DatabasePool) -> KeyHolder {
let mut actor = KeyHolder::new(db.clone()).await.unwrap();
let seal_key = MemSafe::new(b"test-seal-key".to_vec()).unwrap();
let seal_key = SafeCell::new(b"test-seal-key".to_vec());
actor.bootstrap(seal_key).await.unwrap();
actor
}
@@ -391,7 +395,7 @@ mod tests {
assert_eq!(root_row.data_encryption_nonce, n2.to_vec());
let id = actor
.create_new(MemSafe::new(b"post-interleave".to_vec()).unwrap())
.create_new(SafeCell::new(b"post-interleave".to_vec()))
.await
.unwrap();
let row: models::AeadEncrypted = schema::aead_encrypted::table

View File

@@ -154,7 +154,7 @@ impl MessageRouter {
ctx: &mut Context<Self, DelegatedReply<Result<bool, ApprovalError>>>,
) -> DelegatedReply<Result<bool, ApprovalError>> {
let (reply, Some(reply_sender)) = ctx.reply_sender() else {
panic!("Exptected `request_client_approval` to have callback channel");
unreachable!("Expected `request_client_approval` to have callback channel");
};
let weak_refs = self

View File

@@ -1,12 +1,9 @@
use arbiter_proto::proto::user_agent::{
AuthChallengeRequest, AuthChallengeSolution, KeyType as ProtoKeyType, UserAgentRequest,
user_agent_request::Payload as UserAgentRequestPayload,
};
use tracing::error;
use crate::actors::user_agent::{
UserAgentConnection,
auth::state::{AuthContext, AuthPublicKey, AuthStateMachine},
Request, UserAgentConnection,
auth::state::{AuthContext, AuthStateMachine},
AuthPublicKey,
session::UserAgentSession,
};
@@ -37,54 +34,20 @@ pub enum Error {
mod state;
use state::*;
fn parse_pubkey(key_type: ProtoKeyType, pubkey: Vec<u8>) -> Result<AuthPublicKey, Error> {
match key_type {
// UNSPECIFIED treated as Ed25519 for backward compatibility
ProtoKeyType::Unspecified | ProtoKeyType::Ed25519 => {
let pubkey_bytes = pubkey.as_array().ok_or(Error::InvalidClientPubkeyLength)?;
let key = ed25519_dalek::VerifyingKey::from_bytes(pubkey_bytes)
.map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
Ok(AuthPublicKey::Ed25519(key))
}
ProtoKeyType::EcdsaSecp256k1 => {
// Public key is sent as 33-byte SEC1 compressed point
let key = k256::ecdsa::VerifyingKey::from_sec1_bytes(&pubkey)
.map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
Ok(AuthPublicKey::EcdsaSecp256k1(key))
}
ProtoKeyType::Rsa => {
use rsa::pkcs8::DecodePublicKey as _;
let key = rsa::RsaPublicKey::from_public_key_der(&pubkey)
.map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
Ok(AuthPublicKey::Rsa(key))
}
}
}
fn parse_auth_event(payload: UserAgentRequestPayload) -> Result<AuthEvents, Error> {
fn parse_auth_event(payload: Request) -> Result<AuthEvents, Error> {
match payload {
UserAgentRequestPayload::AuthChallengeRequest(AuthChallengeRequest {
Request::AuthChallengeRequest {
pubkey,
bootstrap_token: None,
key_type,
}) => {
let kt = ProtoKeyType::try_from(key_type).unwrap_or(ProtoKeyType::Unspecified);
Ok(AuthEvents::AuthRequest(ChallengeRequest {
pubkey: parse_pubkey(kt, pubkey)?,
}))
}
UserAgentRequestPayload::AuthChallengeRequest(AuthChallengeRequest {
} => Ok(AuthEvents::AuthRequest(ChallengeRequest { pubkey })),
Request::AuthChallengeRequest {
pubkey,
bootstrap_token: Some(token),
key_type,
}) => {
let kt = ProtoKeyType::try_from(key_type).unwrap_or(ProtoKeyType::Unspecified);
Ok(AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest {
pubkey: parse_pubkey(kt, pubkey)?,
} => Ok(AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest {
pubkey,
token,
}))
}
UserAgentRequestPayload::AuthChallengeSolution(AuthChallengeSolution { signature }) => {
})),
Request::AuthChallengeSolution { signature } => {
Ok(AuthEvents::ReceivedSolution(ChallengeSolution {
solution: signature,
}))
@@ -99,10 +62,7 @@ pub async fn authenticate(props: &mut UserAgentConnection) -> Result<AuthPublicK
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(UserAgentRequest {
payload: Some(payload),
}) = transport.recv().await
else {
let Some(payload) = transport.recv().await else {
return Err(Error::Transport);
};

View File

@@ -1,52 +1,16 @@
use arbiter_proto::proto::user_agent::{
AuthChallenge, UserAgentResponse, user_agent_response::Payload as UserAgentResponsePayload,
};
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
use diesel_async::RunQueryDsl;
use tracing::error;
use super::Error;
use crate::{
actors::{bootstrap::ConsumeToken, user_agent::UserAgentConnection},
db::{models::KeyType, schema},
actors::{
bootstrap::ConsumeToken,
user_agent::{AuthPublicKey, Response, UserAgentConnection},
},
db::schema,
};
/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake.
#[derive(Clone)]
pub enum AuthPublicKey {
Ed25519(ed25519_dalek::VerifyingKey),
/// Compressed SEC1 public key; signature bytes are raw 64-byte (r||s).
EcdsaSecp256k1(k256::ecdsa::VerifyingKey),
/// RSA-2048+ public key (Windows Hello / KeyCredentialManager); signature bytes are PSS+SHA-256.
Rsa(rsa::RsaPublicKey),
}
impl AuthPublicKey {
/// Canonical bytes stored in DB and echoed back in the challenge.
/// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. RSA: DER-encoded SPKI.
pub fn to_stored_bytes(&self) -> Vec<u8> {
match self {
AuthPublicKey::Ed25519(k) => k.to_bytes().to_vec(),
// SEC1 compressed (33 bytes) is the natural compact format for secp256k1
AuthPublicKey::EcdsaSecp256k1(k) => k.to_encoded_point(true).as_bytes().to_vec(),
AuthPublicKey::Rsa(k) => {
use rsa::pkcs8::EncodePublicKey as _;
k.to_public_key_der()
.expect("rsa SPKI encoding is infallible")
.to_vec()
}
}
}
pub fn key_type(&self) -> KeyType {
match self {
AuthPublicKey::Ed25519(_) => KeyType::Ed25519,
AuthPublicKey::EcdsaSecp256k1(_) => KeyType::EcdsaSecp256k1,
AuthPublicKey::Rsa(_) => KeyType::Rsa,
}
}
}
pub struct ChallengeRequest {
pub pubkey: AuthPublicKey,
}
@@ -57,7 +21,7 @@ pub struct BootstrapAuthRequest {
}
pub struct ChallengeContext {
pub challenge: AuthChallenge,
pub challenge_nonce: i32,
pub key: AuthPublicKey,
}
@@ -70,8 +34,8 @@ smlang::statemachine!(
custom_error: true,
transitions: {
*Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext),
Init + BootstrapAuthRequest(BootstrapAuthRequest) [async verify_bootstrap_token] / provide_key_bootstrap = AuthOk(AuthPublicKey),
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) [async verify_solution] / provide_key = AuthOk(AuthPublicKey),
Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(AuthPublicKey),
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(AuthPublicKey),
}
);
@@ -147,12 +111,71 @@ impl<'a> AuthContext<'a> {
impl AuthStateMachineContext for AuthContext<'_> {
type Error = Error;
async fn prepare_challenge(
&mut self,
ChallengeRequest { pubkey }: ChallengeRequest,
) -> Result<ChallengeContext, Self::Error> {
let stored_bytes = pubkey.to_stored_bytes();
let nonce = create_nonce(&self.conn.db, &stored_bytes).await?;
self.conn
.transport
.send(Ok(Response::AuthChallenge { nonce }))
.await
.map_err(|e| {
error!(?e, "Failed to send auth challenge");
Error::Transport
})?;
Ok(ChallengeContext {
challenge_nonce: nonce,
key: pubkey,
})
}
#[allow(missing_docs)]
#[allow(clippy::result_unit_err)]
async fn verify_bootstrap_token(
&mut self,
BootstrapAuthRequest { pubkey, token }: BootstrapAuthRequest,
) -> Result<AuthPublicKey, Self::Error> {
let token_ok: bool = self
.conn
.actors
.bootstrapper
.ask(ConsumeToken {
token: token.clone(),
})
.await
.map_err(|e| {
error!(?e, "Failed to consume bootstrap token");
Error::BootstrapperActorUnreachable
})?;
if !token_ok {
error!("Invalid bootstrap token provided");
return Err(Error::InvalidBootstrapToken);
}
register_key(&self.conn.db, &pubkey).await?;
self.conn
.transport
.send(Ok(Response::AuthOk))
.await
.map_err(|_| Error::Transport)?;
Ok(pubkey)
}
#[allow(missing_docs)]
#[allow(clippy::unused_unit)]
async fn verify_solution(
&self,
ChallengeContext { challenge, key }: &ChallengeContext,
ChallengeSolution { solution }: &ChallengeSolution,
) -> Result<bool, Self::Error> {
let formatted = arbiter_proto::format_challenge(challenge.nonce, &challenge.pubkey);
&mut self,
ChallengeContext { challenge_nonce, key }: &ChallengeContext,
ChallengeSolution { solution }: ChallengeSolution,
) -> Result<AuthPublicKey, Self::Error> {
let formatted = arbiter_proto::format_challenge(*challenge_nonce, &key.to_stored_bytes());
let valid = match key {
AuthPublicKey::Ed25519(vk) => {
@@ -181,117 +204,14 @@ impl AuthStateMachineContext for AuthContext<'_> {
}
};
Ok(valid)
}
async fn prepare_challenge(
&mut self,
ChallengeRequest { pubkey }: ChallengeRequest,
) -> Result<ChallengeContext, Self::Error> {
let stored_bytes = pubkey.to_stored_bytes();
let nonce = create_nonce(&self.conn.db, &stored_bytes).await?;
let challenge = AuthChallenge {
pubkey: stored_bytes,
nonce,
};
if valid {
self.conn
.transport
.send(Ok(UserAgentResponse {
payload: Some(UserAgentResponsePayload::AuthChallenge(challenge.clone())),
}))
.send(Ok(Response::AuthOk))
.await
.map_err(|e| {
error!(?e, "Failed to send auth challenge");
Error::Transport
})?;
Ok(ChallengeContext {
challenge,
key: pubkey,
})
.map_err(|_| Error::Transport)?;
}
#[allow(missing_docs)]
#[allow(clippy::result_unit_err)]
async fn verify_bootstrap_token(
&self,
BootstrapAuthRequest { pubkey, token }: &BootstrapAuthRequest,
) -> Result<bool, Self::Error> {
let token_ok: bool = self
.conn
.actors
.bootstrapper
.ask(ConsumeToken {
token: token.clone(),
})
.await
.map_err(|e| {
error!(?e, "Failed to consume bootstrap token");
Error::BootstrapperActorUnreachable
})?;
if !token_ok {
error!("Invalid bootstrap token provided");
return Err(Error::InvalidBootstrapToken);
}
register_key(&self.conn.db, pubkey).await?;
Ok(true)
}
fn provide_key_bootstrap(
&mut self,
event_data: BootstrapAuthRequest,
) -> Result<AuthPublicKey, Self::Error> {
Ok(event_data.pubkey)
}
fn provide_key(
&mut self,
state_data: &ChallengeContext,
_: ChallengeSolution,
) -> Result<AuthPublicKey, Self::Error> {
// ChallengeContext.key cannot be taken by value because smlang passes it by ref;
// we reconstruct stored bytes and return them wrapped in Ed25519 placeholder.
// Session uses only the raw bytes, so we carry them via a Vec<u8>.
// IMPORTANT: do NOT simplify this by storing the key type separately — the
// `AuthPublicKey` enum IS the source of truth for key bytes and type.
//
// smlang state-machine trait requires returning an owned value from `provide_key`,
// but `state_data` is only available by shared reference here. We extract the
// stored bytes and re-wrap as the correct variant so the caller can call
// `to_stored_bytes()` / `key_type()` without losing information.
let bytes = state_data.challenge.pubkey.clone();
let key_type = state_data.key.key_type();
let rebuilt = match key_type {
crate::db::models::KeyType::Ed25519 => {
let arr: &[u8; 32] = bytes
.as_slice()
.try_into()
.expect("ed25519 pubkey must be 32 bytes in challenge");
AuthPublicKey::Ed25519(
ed25519_dalek::VerifyingKey::from_bytes(arr)
.expect("key was already validated in parse_auth_event"),
)
}
crate::db::models::KeyType::EcdsaSecp256k1 => {
// bytes are SEC1 compressed (33 bytes produced by to_encoded_point(true))
AuthPublicKey::EcdsaSecp256k1(
k256::ecdsa::VerifyingKey::from_sec1_bytes(&bytes)
.expect("ecdsa key was already validated in parse_auth_event"),
)
}
crate::db::models::KeyType::Rsa => {
use rsa::pkcs8::DecodePublicKey as _;
AuthPublicKey::Rsa(
rsa::RsaPublicKey::from_public_key_der(&bytes)
.expect("rsa key was already validated in parse_auth_event"),
)
}
};
Ok(rebuilt)
Ok(key.clone())
}
}

View File

@@ -1,19 +1,17 @@
use arbiter_proto::{
proto::user_agent::{UserAgentRequest, UserAgentResponse},
transport::Bi,
};
use alloy::primitives::Address;
use arbiter_proto::transport::Bi;
use kameo::actor::Spawn as _;
use tracing::{error, info};
use crate::{
actors::{GlobalActors, user_agent::session::UserAgentSession},
db::{self},
actors::{GlobalActors, evm, user_agent::session::UserAgentSession},
db::{self, models::KeyType},
evm::policies::SharedGrantSettings,
evm::policies::{Grant, SpecificGrant},
};
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum TransportResponseError {
#[error("Expected message with payload")]
MissingRequestPayload,
#[error("Unexpected request payload")]
UnexpectedRequestPayload,
#[error("Invalid state for unseal encrypted key")]
@@ -30,8 +28,127 @@ pub enum TransportResponseError {
ConnectionRegistrationFailed,
}
pub type Transport =
Box<dyn Bi<UserAgentRequest, Result<UserAgentResponse, TransportResponseError>> + Send>;
/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake.
#[derive(Clone, Debug)]
pub enum AuthPublicKey {
Ed25519(ed25519_dalek::VerifyingKey),
/// Compressed SEC1 public key; signature bytes are raw 64-byte (r||s).
EcdsaSecp256k1(k256::ecdsa::VerifyingKey),
/// RSA-2048+ public key (Windows Hello / KeyCredentialManager); signature bytes are PSS+SHA-256.
Rsa(rsa::RsaPublicKey),
}
impl AuthPublicKey {
/// Canonical bytes stored in DB and echoed back in the challenge.
/// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. RSA: DER-encoded SPKI.
pub fn to_stored_bytes(&self) -> Vec<u8> {
match self {
AuthPublicKey::Ed25519(k) => k.to_bytes().to_vec(),
// SEC1 compressed (33 bytes) is the natural compact format for secp256k1
AuthPublicKey::EcdsaSecp256k1(k) => k.to_encoded_point(true).as_bytes().to_vec(),
AuthPublicKey::Rsa(k) => {
use rsa::pkcs8::EncodePublicKey as _;
#[allow(clippy::expect_used)]
k.to_public_key_der()
.expect("rsa SPKI encoding is infallible")
.to_vec()
}
}
}
pub fn key_type(&self) -> KeyType {
match self {
AuthPublicKey::Ed25519(_) => KeyType::Ed25519,
AuthPublicKey::EcdsaSecp256k1(_) => KeyType::EcdsaSecp256k1,
AuthPublicKey::Rsa(_) => KeyType::Rsa,
}
}
}
#[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<String>,
},
AuthChallengeSolution {
signature: Vec<u8>,
},
UnsealStart {
client_pubkey: x25519_dalek::PublicKey,
},
UnsealEncryptedKey {
nonce: Vec<u8>,
ciphertext: Vec<u8>,
associated_data: Vec<u8>,
},
BootstrapEncryptedKey {
nonce: Vec<u8>,
ciphertext: Vec<u8>,
associated_data: Vec<u8>,
},
QueryVaultState,
EvmWalletCreate,
EvmWalletList,
ClientConnectionResponse {
approved: bool,
},
ListGrants,
EvmGrantCreate {
client_id: i32,
shared: SharedGrantSettings,
specific: SpecificGrant,
},
EvmGrantDelete {
grant_id: i32,
},
}
#[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,
},
ClientConnectionCancel,
EvmWalletCreate(Result<(), evm::Error>),
EvmWalletList(Vec<Address>),
ListGrants(Vec<Grant<SpecificGrant>>),
EvmGrantCreate(Result<i32, evm::Error>),
EvmGrantDelete(Result<(), evm::Error>),
}
pub type Transport = Box<dyn Bi<Request, Result<Response, TransportResponseError>> + Send>;
pub struct UserAgentConnection {
db: db::DatabasePool,
@@ -52,6 +169,7 @@ impl UserAgentConnection {
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) => {

View File

@@ -1,31 +1,18 @@
use std::{ops::DerefMut, sync::Mutex};
use arbiter_proto::proto::{
evm as evm_proto,
user_agent::{
ClientConnectionCancel, ClientConnectionRequest, UnsealEncryptedKey, UnsealResult,
UnsealStart, UnsealStartResponse, UserAgentRequest, UserAgentResponse,
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
},
};
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use ed25519_dalek::VerifyingKey;
use kameo::{Actor, error::SendError, messages, prelude::Context};
use memsafe::MemSafe;
use kameo::{Actor, messages, prelude::Context};
use tokio::{select, sync::watch};
use tracing::{error, info};
use x25519_dalek::{EphemeralSecret, PublicKey};
use crate::actors::{
evm::{Generate, ListWallets},
keyholder::{self, TryUnseal},
router::RegisterUserAgent,
user_agent::{TransportResponseError, UserAgentConnection},
user_agent::{
Request, Response, TransportResponseError,
UserAgentConnection,
},
};
mod state;
use state::{DummyContext, UnsealContext, UserAgentEvents, UserAgentStateMachine, UserAgentStates};
use state::{DummyContext, UserAgentEvents, UserAgentStateMachine};
// Error for consumption by other actors
#[derive(Debug, thiserror::Error, PartialEq)]
@@ -42,6 +29,8 @@ pub struct UserAgentSession {
state: UserAgentStateMachine<DummyContext>,
}
mod connection;
impl UserAgentSession {
pub(crate) fn new(props: UserAgentConnection) -> Self {
Self {
@@ -50,24 +39,12 @@ impl UserAgentSession {
}
}
fn transition(&mut self, event: UserAgentEvents) -> Result<(), TransportResponseError> {
self.state.process_event(event).map_err(|e| {
error!(?e, "State transition failed");
TransportResponseError::StateTransitionFailed
})?;
Ok(())
}
async fn send_msg<Reply: kameo::Reply>(
pub(super) async fn send_msg<Reply: kameo::Reply>(
&mut self,
msg: UserAgentResponsePayload,
msg: Response,
_ctx: &mut Context<Self, Reply>,
) -> Result<(), Error> {
self.props
.transport
.send(Ok(response(msg)))
.await
.map_err(|_| {
self.props.transport.send(Ok(msg)).await.map_err(|_| {
error!(
actor = "useragent",
reason = "channel closed",
@@ -83,7 +60,7 @@ impl UserAgentSession {
ctx: &mut Context<Self, Reply>,
) -> Result<Msg, Error>
where
Extractor: FnOnce(UserAgentRequestPayload) -> Option<Msg>,
Extractor: FnOnce(Request) -> Option<Msg>,
Reply: kameo::Reply,
{
let msg = self.props.transport.recv().await.ok_or_else(|| {
@@ -96,7 +73,7 @@ impl UserAgentSession {
Error::ConnectionLost
})?;
msg.payload.and_then(extractor).ok_or_else(|| {
extractor(msg).ok_or_else(|| {
error!(
actor = "useragent",
reason = "unexpected message",
@@ -106,6 +83,14 @@ impl UserAgentSession {
Error::UnexpectedMessage
})
}
fn transition(&mut self, event: UserAgentEvents) -> Result<(), TransportResponseError> {
self.state.process_event(event).map_err(|e| {
error!(?e, "State transition failed");
TransportResponseError::StateTransitionFailed
})?;
Ok(())
}
}
#[messages]
@@ -119,18 +104,16 @@ impl UserAgentSession {
ctx: &mut Context<Self, Result<bool, Error>>,
) -> Result<bool, Error> {
self.send_msg(
UserAgentResponsePayload::ClientConnectionRequest(ClientConnectionRequest {
pubkey: client_pubkey.as_bytes().to_vec(),
}),
Response::ClientConnectionRequest {
pubkey: client_pubkey,
},
ctx,
)
.await?;
let extractor = |msg| {
if let UserAgentRequestPayload::ClientConnectionResponse(client_connection_response) =
msg
{
Some(client_connection_response)
if let Request::ClientConnectionResponse { approved } = msg {
Some(approved)
} else {
None
}
@@ -140,214 +123,20 @@ impl UserAgentSession {
_ = cancel_flag.changed() => {
info!(actor = "useragent", "client connection approval cancelled");
self.send_msg(
UserAgentResponsePayload::ClientConnectionCancel(ClientConnectionCancel {}),
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.approved);
Ok(result.approved)
info!(actor = "useragent", "received client connection approval result: approved={}", result);
Ok(result)
}
}
}
}
impl UserAgentSession {
pub async fn process_transport_inbound(&mut self, req: UserAgentRequest) -> Output {
let msg = req.payload.ok_or_else(|| {
error!(actor = "useragent", "Received message with no payload");
TransportResponseError::MissingRequestPayload
})?;
match msg {
UserAgentRequestPayload::UnsealStart(unseal_start) => {
self.handle_unseal_request(unseal_start).await
}
UserAgentRequestPayload::UnsealEncryptedKey(unseal_encrypted_key) => {
self.handle_unseal_encrypted_key(unseal_encrypted_key).await
}
UserAgentRequestPayload::EvmWalletCreate(_) => self.handle_evm_wallet_create().await,
UserAgentRequestPayload::EvmWalletList(_) => self.handle_evm_wallet_list().await,
_ => Err(TransportResponseError::UnexpectedRequestPayload),
}
}
}
type Output = Result<UserAgentResponse, TransportResponseError>;
fn response(payload: UserAgentResponsePayload) -> UserAgentResponse {
UserAgentResponse {
payload: Some(payload),
}
}
impl UserAgentSession {
async fn handle_unseal_request(&mut self, req: UnsealStart) -> Output {
let secret = EphemeralSecret::random();
let public_key = PublicKey::from(&secret);
let client_pubkey_bytes: [u8; 32] = req
.client_pubkey
.try_into()
.map_err(|_| TransportResponseError::InvalidClientPubkeyLength)?;
let client_public_key = PublicKey::from(client_pubkey_bytes);
self.transition(UserAgentEvents::UnsealRequest(UnsealContext {
secret: Mutex::new(Some(secret)),
client_public_key,
}))?;
Ok(response(UserAgentResponsePayload::UnsealStartResponse(
UnsealStartResponse {
server_pubkey: public_key.as_bytes().to_vec(),
},
)))
}
async fn handle_unseal_encrypted_key(&mut self, req: UnsealEncryptedKey) -> Output {
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
error!("Received unseal encrypted key in invalid state");
return Err(TransportResponseError::InvalidStateForUnsealEncryptedKey);
};
let ephemeral_secret = {
let mut secret_lock = unseal_context.secret.lock().unwrap();
let secret = secret_lock.take();
match secret {
Some(secret) => secret,
None => {
drop(secret_lock);
error!("Ephemeral secret already taken");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(response(UserAgentResponsePayload::UnsealResult(
UnsealResult::InvalidKey.into(),
)));
}
}
};
let nonce = XNonce::from_slice(&req.nonce);
let shared_secret = ephemeral_secret.diffie_hellman(&unseal_context.client_public_key);
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
let mut seal_key_buffer = MemSafe::new(req.ciphertext.clone()).unwrap();
let decryption_result = {
let mut write_handle = seal_key_buffer.write().unwrap();
let write_handle = write_handle.deref_mut();
cipher.decrypt_in_place(nonce, &req.associated_data, write_handle)
};
match decryption_result {
Ok(_) => {
match self
.props
.actors
.key_holder
.ask(TryUnseal {
seal_key_raw: seal_key_buffer,
})
.await
{
Ok(_) => {
info!("Successfully unsealed key with client-provided key");
self.transition(UserAgentEvents::ReceivedValidKey)?;
Ok(response(UserAgentResponsePayload::UnsealResult(
UnsealResult::Success.into(),
)))
}
Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(response(UserAgentResponsePayload::UnsealResult(
UnsealResult::InvalidKey.into(),
)))
}
Err(SendError::HandlerError(err)) => {
error!(?err, "Keyholder failed to unseal key");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(response(UserAgentResponsePayload::UnsealResult(
UnsealResult::InvalidKey.into(),
)))
}
Err(err) => {
error!(?err, "Failed to send unseal request to keyholder");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(TransportResponseError::KeyHolderActorUnreachable)
}
}
}
Err(err) => {
error!(?err, "Failed to decrypt unseal key");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(response(UserAgentResponsePayload::UnsealResult(
UnsealResult::InvalidKey.into(),
)))
}
}
}
}
impl UserAgentSession {
async fn handle_evm_wallet_create(&mut self) -> Output {
use evm_proto::wallet_create_response::Result as CreateResult;
let result = match self.props.actors.evm.ask(Generate {}).await {
Ok(address) => CreateResult::Wallet(evm_proto::WalletEntry {
address: address.as_slice().to_vec(),
}),
Err(err) => CreateResult::Error(map_evm_error("wallet create", err).into()),
};
Ok(response(UserAgentResponsePayload::EvmWalletCreate(
evm_proto::WalletCreateResponse {
result: Some(result),
},
)))
}
async fn handle_evm_wallet_list(&mut self) -> Output {
use evm_proto::wallet_list_response::Result as ListResult;
let result = match self.props.actors.evm.ask(ListWallets {}).await {
Ok(wallets) => ListResult::Wallets(evm_proto::WalletList {
wallets: wallets
.into_iter()
.map(|addr| evm_proto::WalletEntry {
address: addr.as_slice().to_vec(),
})
.collect(),
}),
Err(err) => ListResult::Error(map_evm_error("wallet list", err).into()),
};
Ok(response(UserAgentResponsePayload::EvmWalletList(
evm_proto::WalletListResponse {
result: Some(result),
},
)))
}
}
fn map_evm_error<M>(op: &str, err: SendError<M, crate::actors::evm::Error>) -> evm_proto::EvmError {
use crate::actors::{evm::Error as EvmError, keyholder::Error as KhError};
match err {
SendError::HandlerError(EvmError::Keyholder(KhError::NotBootstrapped)) => {
evm_proto::EvmError::VaultSealed
}
SendError::HandlerError(err) => {
error!(?err, "EVM {op} failed");
evm_proto::EvmError::Internal
}
_ => {
error!("EVM actor unreachable during {op}");
evm_proto::EvmError::Internal
}
}
}
impl Actor for UserAgentSession {
type Args = Self;

View File

@@ -0,0 +1,355 @@
use std::sync::Mutex;
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use kameo::error::SendError;
use tracing::{error, info};
use x25519_dalek::{EphemeralSecret, PublicKey};
use crate::safe_cell::SafeCell;
use crate::{
actors::{
evm::{
Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
},
keyholder::{self, Bootstrap, TryUnseal},
user_agent::{
BootstrapError, Request, Response, TransportResponseError, UnsealError, VaultState,
session::{
UserAgentSession,
state::{UnsealContext, UserAgentEvents, UserAgentStates},
},
},
},
safe_cell::SafeCellHandle as _,
};
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<Response, TransportResponseError>;
impl UserAgentSession {
fn take_unseal_secret(
&mut self,
) -> Result<(EphemeralSecret, PublicKey), TransportResponseError> {
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
error!("Received encrypted key in invalid state");
return Err(TransportResponseError::InvalidStateForUnsealEncryptedKey);
};
let ephemeral_secret = {
#[allow(
clippy::unwrap_used,
reason = "Mutex poison is unrecoverable and should panic"
)]
let mut secret_lock = unseal_context.secret.lock().unwrap();
let secret = secret_lock.take();
match secret {
Some(secret) => secret,
None => {
drop(secret_lock);
error!("Ephemeral secret already taken");
return Err(TransportResponseError::StateTransitionFailed);
}
}
};
Ok((ephemeral_secret, unseal_context.client_public_key))
}
fn decrypt_client_key_material(
ephemeral_secret: EphemeralSecret,
client_public_key: PublicKey,
nonce: &[u8],
ciphertext: &[u8],
associated_data: &[u8],
) -> Result<SafeCell<Vec<u8>>, ()> {
let nonce = XNonce::from_slice(nonce);
let shared_secret = ephemeral_secret.diffie_hellman(&client_public_key);
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
let mut key_buffer = SafeCell::new(ciphertext.to_vec());
let decryption_result = key_buffer.write_inline(|write_handle| {
cipher.decrypt_in_place(nonce, associated_data, write_handle)
});
match decryption_result {
Ok(_) => Ok(key_buffer),
Err(err) => {
error!(?err, "Failed to decrypt encrypted key material");
Err(())
}
}
}
async fn handle_unseal_request(&mut self, client_pubkey: x25519_dalek::PublicKey) -> Output {
let secret = EphemeralSecret::random();
let public_key = PublicKey::from(&secret);
self.transition(UserAgentEvents::UnsealRequest(UnsealContext {
secret: Mutex::new(Some(secret)),
client_public_key: client_pubkey,
}))?;
Ok(Response::UnsealStartResponse {
server_pubkey: public_key,
})
}
async fn handle_unseal_encrypted_key(
&mut self,
nonce: Vec<u8>,
ciphertext: Vec<u8>,
associated_data: Vec<u8>,
) -> Output {
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
Ok(values) => values,
Err(TransportResponseError::StateTransitionFailed) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)));
}
Err(err) => return Err(err),
};
let seal_key_buffer = match Self::decrypt_client_key_material(
ephemeral_secret,
client_public_key,
&nonce,
&ciphertext,
&associated_data,
) {
Ok(buffer) => buffer,
Err(()) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)));
}
};
match self
.props
.actors
.key_holder
.ask(TryUnseal {
seal_key_raw: seal_key_buffer,
})
.await
{
Ok(_) => {
info!("Successfully unsealed key with client-provided key");
self.transition(UserAgentEvents::ReceivedValidKey)?;
Ok(Response::UnsealResult(Ok(())))
}
Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(Response::UnsealResult(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(err) => {
error!(?err, "Failed to send unseal request to keyholder");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(TransportResponseError::KeyHolderActorUnreachable)
}
}
}
async fn handle_bootstrap_encrypted_key(
&mut self,
nonce: Vec<u8>,
ciphertext: Vec<u8>,
associated_data: Vec<u8>,
) -> Output {
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
Ok(values) => values,
Err(TransportResponseError::StateTransitionFailed) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey)));
}
Err(err) => return Err(err),
};
let seal_key_buffer = match Self::decrypt_client_key_material(
ephemeral_secret,
client_public_key,
&nonce,
&ciphertext,
&associated_data,
) {
Ok(buffer) => buffer,
Err(()) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey)));
}
};
match self
.props
.actors
.key_holder
.ask(Bootstrap {
seal_key_raw: seal_key_buffer,
})
.await
{
Ok(_) => {
info!("Successfully bootstrapped vault with client-provided key");
self.transition(UserAgentEvents::ReceivedValidKey)?;
Ok(Response::BootstrapResult(Ok(())))
}
Err(SendError::HandlerError(keyholder::Error::AlreadyBootstrapped)) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(Response::BootstrapResult(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(err) => {
error!(?err, "Failed to send bootstrap request to keyholder");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(TransportResponseError::KeyHolderActorUnreachable)
}
}
}
}
impl UserAgentSession {
async fn handle_query_vault_state(&mut self) -> Output {
use crate::actors::keyholder::{GetState, StateDiscriminants};
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,
Err(err) => {
error!(?err, actor = "useragent", "keyholder.query.failed");
return Err(TransportResponseError::KeyHolderActorUnreachable);
}
};
Ok(Response::VaultState(vault_state))
}
}
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),
Err(err) => {
error!(?err, "EVM actor unreachable during wallet create");
return Err(TransportResponseError::KeyHolderActorUnreachable);
}
};
Ok(Response::EvmWalletCreate(result))
}
async fn handle_evm_wallet_list(&mut self) -> Output {
match self.props.actors.evm.ask(ListWallets {}).await {
Ok(wallets) => Ok(Response::EvmWalletList(wallets)),
Err(err) => {
error!(?err, "EVM wallet list failed");
Err(TransportResponseError::KeyHolderActorUnreachable)
}
}
}
}
impl UserAgentSession {
async fn handle_grant_list(&mut self) -> Output {
match self.props.actors.evm.ask(UseragentListGrants {}).await {
Ok(grants) => Ok(Response::ListGrants(grants)),
Err(err) => {
error!(?err, "EVM grant list failed");
Err(TransportResponseError::KeyHolderActorUnreachable)
}
}
}
async fn handle_grant_create(
&mut self,
client_id: i32,
basic: crate::evm::policies::SharedGrantSettings,
grant: crate::evm::policies::SpecificGrant,
) -> Output {
match self
.props
.actors
.evm
.ask(UseragentCreateGrant {
client_id,
basic,
grant,
})
.await
{
Ok(grant_id) => Ok(Response::EvmGrantCreate(Ok(grant_id))),
Err(err) => {
error!(?err, "EVM grant create failed");
Err(TransportResponseError::KeyHolderActorUnreachable)
}
}
}
async fn handle_grant_delete(&mut self, grant_id: i32) -> Output {
match self
.props
.actors
.evm
.ask(UseragentDeleteGrant { grant_id })
.await
{
Ok(()) => Ok(Response::EvmGrantDelete(Ok(()))),
Err(err) => {
error!(?err, "EVM grant delete failed");
Err(TransportResponseError::KeyHolderActorUnreachable)
}
}
}
}

View File

@@ -8,7 +8,7 @@ use rcgen::{
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
IsCa, Issuer, KeyPair, KeyUsagePurpose,
};
use rustls::pki_types::{pem::PemObject};
use rustls::pki_types::pem::PemObject;
use thiserror::Error;
use tonic::transport::CertificateDer;
@@ -59,10 +59,7 @@ pub enum InitError {
pub type PemCert = String;
pub fn encode_cert_to_pem(cert: &CertificateDer) -> PemCert {
pem::encode_config(
&Pem::new("CERTIFICATE", cert.to_vec()),
ENCODE_CONFIG,
)
pem::encode_config(&Pem::new("CERTIFICATE", cert.to_vec()), ENCODE_CONFIG)
}
#[allow(unused)]
@@ -94,6 +91,10 @@ impl TlsCa {
let cert_key_pem = certified_issuer.key().serialize_pem();
#[allow(
clippy::unwrap_used,
reason = "Broken cert couldn't bootstrap server anyway"
)]
let issuer = Issuer::from_ca_cert_pem(
&certified_issuer.pem(),
KeyPair::from_pem(cert_key_pem.as_ref()).unwrap(),

View File

@@ -92,6 +92,7 @@ fn initialize_database(url: &str) -> Result<(), DatabaseSetupError> {
#[tracing::instrument(level = "info")]
pub async fn create_pool(url: Option<&str>) -> Result<DatabasePool, DatabaseSetupError> {
let database_url = url.map(String::from).unwrap_or(
#[allow(clippy::expect_used)]
database_path()?
.to_str()
.expect("database path is not valid UTF-8")
@@ -135,11 +136,13 @@ pub async fn create_test_pool() -> DatabasePool {
let tempfile_name = Alphanumeric.sample_string(&mut rand::rng(), 16);
let file = std::env::temp_dir().join(tempfile_name);
#[allow(clippy::expect_used)]
let url = file
.to_str()
.expect("temp file path is not valid UTF-8")
.to_string();
#[allow(clippy::expect_used)]
create_pool(Some(&url))
.await
.expect("Failed to create test database pool")

View File

@@ -117,9 +117,7 @@ async fn check_shared_constraints(
let now = Utc::now();
// Validity window
if shared.valid_from.is_some_and(|t| now < t)
|| shared.valid_until.is_some_and(|t| now > t)
{
if shared.valid_from.is_some_and(|t| now < t) || shared.valid_until.is_some_and(|t| now > t) {
violations.push(EvalViolation::InvalidTime);
}
@@ -127,9 +125,9 @@ async fn check_shared_constraints(
let fee_exceeded = shared
.max_gas_fee_per_gas
.is_some_and(|cap| U256::from(context.max_fee_per_gas) > cap);
let priority_exceeded = shared.max_priority_fee_per_gas.is_some_and(|cap| {
U256::from(context.max_priority_fee_per_gas) > cap
});
let priority_exceeded = shared
.max_priority_fee_per_gas
.is_some_and(|cap| U256::from(context.max_priority_fee_per_gas) > cap);
if fee_exceeded || priority_exceeded {
violations.push(EvalViolation::GasLimitExceeded {
max_gas_fee_per_gas: shared.max_gas_fee_per_gas,

View File

@@ -66,6 +66,7 @@ pub enum EvalViolation {
pub type DatabaseID = i32;
#[derive(Debug)]
pub struct Grant<PolicySettings> {
pub id: DatabaseID,
pub shared_grant_id: DatabaseID, // ID of the basic grant for shared-logic checks like rate limits and validity periods
@@ -73,7 +74,6 @@ pub struct Grant<PolicySettings> {
pub settings: PolicySettings,
}
pub trait Policy: Sized {
type Settings: Send + Sync + 'static + Into<SpecificGrant>;
type Meaning: Display + std::fmt::Debug + Send + Sync + 'static + Into<SpecificMeaning>;
@@ -146,6 +146,7 @@ pub struct VolumeRateLimit {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SharedGrantSettings {
pub wallet_id: i32,
pub client_id: i32,
pub chain: ChainId,
pub valid_from: Option<DateTime<Utc>>,
@@ -161,6 +162,7 @@ impl SharedGrantSettings {
fn try_from_model(model: EvmBasicGrant) -> QueryResult<Self> {
Ok(Self {
wallet_id: model.wallet_id,
client_id: model.client_id,
chain: model.chain_id as u64, // safe because chain_id is stored as i32 but is guaranteed to be a valid ChainId by the API when creating grants
valid_from: model.valid_from.map(Into::into),
valid_until: model.valid_until.map(Into::into),
@@ -198,6 +200,7 @@ impl SharedGrantSettings {
}
}
#[derive(Debug, Clone)]
pub enum SpecificGrant {
EtherTransfer(ether_transfer::Settings),
TokenTransfer(token_transfers::Settings),

View File

@@ -51,9 +51,10 @@ impl From<Meaning> for SpecificMeaning {
}
// A grant for ether transfers, which can be scoped to specific target addresses and volume limits
#[derive(Debug, Clone)]
pub struct Settings {
target: Vec<Address>,
limit: VolumeRateLimit,
pub target: Vec<Address>,
pub limit: VolumeRateLimit,
}
impl From<Settings> for SpecificGrant {

View File

@@ -9,9 +9,7 @@ use crate::db::{
schema::{evm_basic_grant, evm_transaction_log},
};
use crate::evm::{
policies::{
EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, VolumeRateLimit,
},
policies::{EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, VolumeRateLimit},
utils,
};
@@ -76,6 +74,7 @@ fn shared() -> SharedGrantSettings {
max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None,
rate_limit: None,
client_id: CLIENT_ID,
}
}

View File

@@ -58,10 +58,11 @@ impl From<Meaning> for SpecificMeaning {
}
// A grant for token transfers, which can be scoped to specific target addresses and volume limits
#[derive(Debug, Clone)]
pub struct Settings {
token_contract: Address,
target: Option<Address>,
volume_limits: Vec<VolumeRateLimit>,
pub token_contract: Address,
pub target: Option<Address>,
pub volume_limits: Vec<VolumeRateLimit>,
}
impl From<Settings> for SpecificGrant {
fn from(val: Settings) -> SpecificGrant {

View File

@@ -93,6 +93,7 @@ fn shared() -> SharedGrantSettings {
max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None,
rate_limit: None,
client_id: CLIENT_ID,
}
}
@@ -140,10 +141,18 @@ async fn evaluate_rejects_nonzero_eth_value() {
let mut context = ctx(DAI, calldata);
context.value = U256::from(1u64); // ETH attached to an ERC-20 call
let m = TokenTransfer::analyze(&EvalContext { value: U256::ZERO, ..context.clone() })
let m = TokenTransfer::analyze(&EvalContext {
value: U256::ZERO,
..context.clone()
})
.unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
assert!(v.iter().any(|e| matches!(e, EvalViolation::InvalidTransactionType)));
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
.await
.unwrap();
assert!(
v.iter()
.any(|e| matches!(e, EvalViolation::InvalidTransactionType))
);
}
#[tokio::test]
@@ -160,7 +169,9 @@ async fn evaluate_passes_any_recipient_when_no_restriction() {
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
let context = ctx(DAI, calldata);
let m = TokenTransfer::analyze(&context).unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
.await
.unwrap();
assert!(v.is_empty());
}
@@ -178,7 +189,9 @@ async fn evaluate_passes_matching_restricted_recipient() {
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
let context = ctx(DAI, calldata);
let m = TokenTransfer::analyze(&context).unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
.await
.unwrap();
assert!(v.is_empty());
}
@@ -196,8 +209,13 @@ async fn evaluate_rejects_wrong_restricted_recipient() {
let calldata = transfer_calldata(OTHER, U256::from(100u64));
let context = ctx(DAI, calldata);
let m = TokenTransfer::analyze(&context).unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
assert!(v.iter().any(|e| matches!(e, EvalViolation::InvalidTarget { .. })));
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
.await
.unwrap();
assert!(
v.iter()
.any(|e| matches!(e, EvalViolation::InvalidTarget { .. }))
);
}
#[tokio::test]
@@ -207,7 +225,9 @@ async fn evaluate_passes_volume_within_limit() {
let basic = insert_basic(&mut conn, false).await;
let settings = make_settings(None, Some(1_000));
let grant_id = TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
let grant_id = TokenTransfer::create_grant(&basic, &settings, &mut *conn)
.await
.unwrap();
// Record a past transfer of 500 (within 1000 limit)
use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
@@ -224,12 +244,22 @@ async fn evaluate_passes_volume_within_limit() {
.await
.unwrap();
let grant = Grant { id: grant_id, shared_grant_id: basic.id, shared: shared(), settings };
let grant = Grant {
id: grant_id,
shared_grant_id: basic.id,
shared: shared(),
settings,
};
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
let context = ctx(DAI, calldata);
let m = TokenTransfer::analyze(&context).unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
assert!(!v.iter().any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded)));
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
.await
.unwrap();
assert!(
!v.iter()
.any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded))
);
}
#[tokio::test]
@@ -239,7 +269,9 @@ async fn evaluate_rejects_volume_over_limit() {
let basic = insert_basic(&mut conn, false).await;
let settings = make_settings(None, Some(1_000));
let grant_id = TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
let grant_id = TokenTransfer::create_grant(&basic, &settings, &mut *conn)
.await
.unwrap();
use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
insert_into(evm_token_transfer_log::table)
@@ -255,12 +287,22 @@ async fn evaluate_rejects_volume_over_limit() {
.await
.unwrap();
let grant = Grant { id: grant_id, shared_grant_id: basic.id, shared: shared(), settings };
let grant = Grant {
id: grant_id,
shared_grant_id: basic.id,
shared: shared(),
settings,
};
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
let context = ctx(DAI, calldata);
let m = TokenTransfer::analyze(&context).unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
assert!(v.iter().any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded)));
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
.await
.unwrap();
assert!(
v.iter()
.any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded))
);
}
#[tokio::test]
@@ -277,8 +319,13 @@ async fn evaluate_no_volume_limits_always_passes() {
let calldata = transfer_calldata(RECIPIENT, U256::from(u64::MAX));
let context = ctx(DAI, calldata);
let m = TokenTransfer::analyze(&context).unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
assert!(!v.iter().any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded)));
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
.await
.unwrap();
assert!(
!v.iter()
.any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded))
);
}
// ── try_find_grant ───────────────────────────────────────────────────────
@@ -290,7 +337,9 @@ async fn try_find_grant_roundtrip() {
let basic = insert_basic(&mut conn, false).await;
let settings = make_settings(Some(RECIPIENT), Some(5_000));
TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
TokenTransfer::create_grant(&basic, &settings, &mut *conn)
.await
.unwrap();
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
let found = TokenTransfer::try_find_grant(&ctx(DAI, calldata), &mut *conn)
@@ -312,7 +361,9 @@ async fn try_find_grant_revoked_returns_none() {
let basic = insert_basic(&mut conn, true).await;
let settings = make_settings(None, None);
TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
TokenTransfer::create_grant(&basic, &settings, &mut *conn)
.await
.unwrap();
let calldata = transfer_calldata(RECIPIENT, U256::from(1u64));
let found = TokenTransfer::try_find_grant(&ctx(DAI, calldata), &mut *conn)
@@ -328,7 +379,9 @@ async fn try_find_grant_unknown_token_returns_none() {
let basic = insert_basic(&mut conn, false).await;
let settings = make_settings(None, None);
TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
TokenTransfer::create_grant(&basic, &settings, &mut *conn)
.await
.unwrap();
// Query with a different token contract
let calldata = transfer_calldata(RECIPIENT, U256::from(1u64));
@@ -355,9 +408,13 @@ async fn find_all_grants_excludes_revoked() {
let settings = make_settings(None, Some(1_000));
let active = insert_basic(&mut conn, false).await;
TokenTransfer::create_grant(&active, &settings, &mut *conn).await.unwrap();
TokenTransfer::create_grant(&active, &settings, &mut *conn)
.await
.unwrap();
let revoked = insert_basic(&mut conn, true).await;
TokenTransfer::create_grant(&revoked, &settings, &mut *conn).await.unwrap();
TokenTransfer::create_grant(&revoked, &settings, &mut *conn)
.await
.unwrap();
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
assert_eq!(all.len(), 1);
@@ -370,12 +427,17 @@ async fn find_all_grants_loads_volume_limits() {
let basic = insert_basic(&mut conn, false).await;
let settings = make_settings(None, Some(9_999));
TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
TokenTransfer::create_grant(&basic, &settings, &mut *conn)
.await
.unwrap();
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
assert_eq!(all.len(), 1);
assert_eq!(all[0].settings.volume_limits.len(), 1);
assert_eq!(all[0].settings.volume_limits[0].max_volume, U256::from(9_999u64));
assert_eq!(
all[0].settings.volume_limits[0].max_volume,
U256::from(9_999u64)
);
}
#[tokio::test]
@@ -388,7 +450,11 @@ async fn find_all_grants_multiple_grants_batch_loaded() {
.await
.unwrap();
let b2 = insert_basic(&mut conn, false).await;
TokenTransfer::create_grant(&b2, &make_settings(Some(RECIPIENT), Some(2_000)), &mut *conn)
TokenTransfer::create_grant(
&b2,
&make_settings(Some(RECIPIENT), Some(2_000)),
&mut *conn,
)
.await
.unwrap();

View File

@@ -1,14 +1,14 @@
use std::sync::Mutex;
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
use alloy::{
consensus::SignableTransaction,
network::{TxSigner, TxSignerSync},
primitives::{Address, ChainId, Signature, B256},
primitives::{Address, B256, ChainId, Signature},
signers::{Error, Result, Signer, SignerSync, utils::secret_key_to_address},
};
use async_trait::async_trait;
use k256::ecdsa::{self, signature::hazmat::PrehashSigner, RecoveryId, SigningKey};
use memsafe::MemSafe;
use k256::ecdsa::{self, RecoveryId, SigningKey, signature::hazmat::PrehashSigner};
/// An Ethereum signer that stores its secp256k1 secret key inside a
/// hardware-protected [`MemSafe`] cell.
@@ -20,7 +20,7 @@ use memsafe::MemSafe;
/// Because [`MemSafe::read`] requires `&mut self` while the [`Signer`] trait
/// requires `&self`, the cell is wrapped in a [`Mutex`].
pub struct SafeSigner {
key: Mutex<MemSafe<SigningKey>>,
key: Mutex<SafeCell<SigningKey>>,
address: Address,
chain_id: Option<ChainId>,
}
@@ -42,14 +42,13 @@ impl std::fmt::Debug for SafeSigner {
/// rejection, but we retry to be correct).
///
/// Returns the protected key bytes and the derived Ethereum address.
pub fn generate(rng: &mut impl rand::Rng) -> (MemSafe<[u8; 32]>, Address) {
pub fn generate(rng: &mut impl rand::Rng) -> (SafeCell<[u8; 32]>, Address) {
loop {
let mut cell = MemSafe::new([0u8; 32]).expect("MemSafe allocation");
{
let mut w = cell.write().expect("MemSafe write");
rng.fill_bytes(w.as_mut());
}
let reader = cell.read().expect("MemSafe read");
let mut cell = SafeCell::new_inline(|w: &mut [u8; 32]| {
rng.fill_bytes(w);
});
let reader = cell.read();
if let Ok(sk) = SigningKey::from_slice(reader.as_ref()) {
let address = secret_key_to_address(&sk);
drop(reader);
@@ -64,8 +63,8 @@ impl SafeSigner {
/// The key bytes are read from protected memory, parsed as a secp256k1
/// scalar, and immediately moved into a new [`MemSafe`] cell. The raw
/// bytes are never exposed outside this function.
pub fn from_memsafe(mut cell: MemSafe<Vec<u8>>) -> Result<Self> {
let reader = cell.read().map_err(Error::other)?;
pub fn from_cell(mut cell: SafeCell<Vec<u8>>) -> Result<Self> {
let reader = cell.read();
let sk = SigningKey::from_slice(reader.as_slice()).map_err(Error::other)?;
drop(reader);
Self::new(sk)
@@ -75,7 +74,7 @@ impl SafeSigner {
/// memory region.
pub fn new(key: SigningKey) -> Result<Self> {
let address = secret_key_to_address(&key);
let cell = MemSafe::new(key).map_err(Error::other)?;
let cell = SafeCell::new(key);
Ok(Self {
key: Mutex::new(cell),
address,
@@ -84,25 +83,25 @@ impl SafeSigner {
}
fn sign_hash_inner(&self, hash: &B256) -> Result<Signature> {
#[allow(clippy::expect_used)]
let mut cell = self.key.lock().expect("SafeSigner mutex poisoned");
let reader = cell.read().map_err(Error::other)?;
let reader = cell.read();
let sig: (ecdsa::Signature, RecoveryId) = reader.sign_prehash(hash.as_ref())?;
Ok(sig.into())
}
fn sign_tx_inner(
&self,
tx: &mut dyn SignableTransaction<Signature>,
) -> Result<Signature> {
fn sign_tx_inner(&self, tx: &mut dyn SignableTransaction<Signature>) -> Result<Signature> {
if let Some(chain_id) = self.chain_id
&& !tx.set_chain_id_checked(chain_id)
{
return Err(Error::TransactionChainIdMismatch {
signer: chain_id,
tx: tx.chain_id().unwrap(),
#[allow(clippy::expect_used)]
tx: tx.chain_id().expect("Chain ID is guaranteed to be set"),
});
}
self.sign_hash_inner(&tx.signature_hash()).map_err(Error::other)
self.sign_hash_inner(&tx.signature_hash())
.map_err(Error::other)
}
}

View File

@@ -0,0 +1,137 @@
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,
client_request::Payload as ClientRequestPayload,
client_response::Payload as ClientResponsePayload,
},
transport::{Bi, Error as TransportError},
};
use async_trait::async_trait;
use futures::StreamExt as _;
use tokio::sync::mpsc;
use tonic::{Status, Streaming};
use crate::actors::client::{
self, ClientError, ConnectErrorCode, Request as DomainRequest, Response as DomainResponse,
};
pub struct GrpcTransport {
sender: mpsc::Sender<Result<ClientResponse, Status>>,
receiver: Streaming<ClientRequest>,
}
impl GrpcTransport {
pub fn new(
sender: mpsc::Sender<Result<ClientResponse, Status>>,
receiver: Streaming<ClientRequest>,
) -> Self {
Self { sender, receiver }
}
fn request_to_domain(request: ClientRequest) -> Result<DomainRequest, Status> {
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(),
})
}
};
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")
}
}
}
}
#[async_trait]
impl Bi<DomainRequest, Result<DomainResponse, ClientError>> for GrpcTransport {
async fn send(&mut self, item: Result<DomainResponse, ClientError>) -> Result<(), TransportError> {
let outbound = match item {
Ok(message) => Ok(Self::response_to_proto(message)),
Err(err) => Err(Self::error_to_status(err)),
};
self.sender
.send(outbound)
.await
.map_err(|_| TransportError::ChannelClosed)
}
async fn recv(&mut self) -> Option<DomainRequest> {
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 client recv failed; closing stream");
None
}
None => None,
}
}
}
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"),
}
}

View File

@@ -0,0 +1,65 @@
use arbiter_proto::proto::{
client::{ClientRequest, ClientResponse},
user_agent::{UserAgentRequest, UserAgentResponse},
};
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use tonic::{Request, Response, Status, async_trait};
use tracing::info;
use crate::{
DEFAULT_CHANNEL_SIZE,
actors::{client::{ClientConnection, connect_client}, user_agent::{UserAgentConnection, connect_user_agent}},
};
pub mod client;
pub mod user_agent;
#[async_trait]
impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Server {
type UserAgentStream = ReceiverStream<Result<UserAgentResponse, Status>>;
type ClientStream = ReceiverStream<Result<ClientResponse, Status>>;
#[tracing::instrument(level = "debug", skip(self))]
async fn client(
&self,
request: Request<tonic::Streaming<ClientRequest>>,
) -> Result<Response<Self::ClientStream>, 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));
info!(event = "connection established", "grpc.client");
Ok(Response::new(ReceiverStream::new(rx)))
}
#[tracing::instrument(level = "debug", skip(self))]
async fn user_agent(
&self,
request: Request<tonic::Streaming<UserAgentRequest>>,
) -> Result<Response<Self::UserAgentStream>, 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));
info!(event = "connection established", "grpc.user_agent");
Ok(Response::new(ReceiverStream::new(rx)))
}
}

View File

@@ -0,0 +1,509 @@
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, 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,
VaultState as ProtoVaultState, user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
},
},
transport::{Bi, Error as TransportError},
};
use async_trait::async_trait;
use futures::StreamExt as _;
use prost_types::Timestamp;
use tokio::sync::mpsc;
use tonic::{Status, Streaming};
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,
},
},
};
use alloy::primitives::{Address, U256};
use chrono::{DateTime, TimeZone, Utc};
pub struct GrpcTransport {
sender: mpsc::Sender<Result<UserAgentResponse, Status>>,
receiver: Streaming<UserAgentRequest>,
}
impl GrpcTransport {
pub fn new(
sender: mpsc::Sender<Result<UserAgentResponse, Status>>,
receiver: Streaming<UserAgentRequest>,
) -> Self {
Self { sender, receiver }
}
fn request_to_domain(request: UserAgentRequest) -> Result<DomainRequest, Status> {
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")
}
}
}
}
#[async_trait]
impl Bi<DomainRequest, Result<DomainResponse, TransportResponseError>> for GrpcTransport {
async fn send(
&mut self,
item: Result<DomainResponse, TransportResponseError>,
) -> Result<(), TransportError> {
let outbound = match item {
Ok(message) => Ok(Self::response_to_proto(message)),
Err(err) => Err(Self::error_to_status(err)),
};
self.sender
.send(outbound)
.await
.map_err(|_| TransportError::ChannelClosed)
}
async fn recv(&mut self) -> Option<DomainRequest> {
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
}
None => None,
}
}
}
fn grant_to_proto(grant: Grant<SpecificGrant>) -> 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<VolumeRateLimit, Status> {
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<ProtoSharedSettings>,
) -> Result<SharedGrantSettings, Status> {
let s = proto.ok_or_else(|| Status::invalid_argument("missing shared settings"))?;
let parse_u256 = |b: Vec<u8>| -> Result<U256, Status> {
if b.is_empty() {
Err(Status::invalid_argument("U256 bytes must not be empty"))
} else {
Ok(U256::from_be_slice(&b))
}
};
let parse_ts = |ts: prost_types::Timestamp| -> Result<DateTime<Utc>, 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<proto::evm::SpecificGrant>) -> Result<SpecificGrant, Status> {
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::<Result<Vec<_>, _>>()?;
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::<Result<Vec<_>, _>>()?;
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<u8>) -> Result<AuthPublicKey, Status> {
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"),
}
}

View File

@@ -1,137 +1,21 @@
#![forbid(unsafe_code)]
use arbiter_proto::{
proto::{
client::{ClientRequest, ClientResponse},
user_agent::{UserAgentRequest, UserAgentResponse},
},
transport::{IdentityRecvConverter, SendConverter, grpc},
};
use async_trait::async_trait;
use tokio_stream::wrappers::ReceiverStream;
#![deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic
)]
use tokio::sync::mpsc;
use tonic::{Request, Response, Status};
use tracing::info;
use crate::{
actors::{
client::{self, ClientConnection as ClientConnectionProps, ClientError, connect_client},
user_agent::{self, TransportResponseError, UserAgentConnection, connect_user_agent},
},
context::ServerContext,
};
use crate::context::ServerContext;
pub mod actors;
pub mod context;
pub mod db;
pub mod evm;
pub mod grpc;
pub mod safe_cell;
const DEFAULT_CHANNEL_SIZE: usize = 1000;
struct UserAgentGrpcSender;
impl SendConverter for UserAgentGrpcSender {
type Input = Result<UserAgentResponse, TransportResponseError>;
type Output = Result<UserAgentResponse, Status>;
fn convert(&self, item: Self::Input) -> Self::Output {
match item {
Ok(message) => Ok(message),
Err(err) => Err(user_agent_error_status(err)),
}
}
}
struct ClientGrpcSender;
impl SendConverter for ClientGrpcSender {
type Input = Result<ClientResponse, ClientError>;
type Output = Result<ClientResponse, Status>;
fn convert(&self, item: Self::Input) -> Self::Output {
match item {
Ok(message) => Ok(message),
Err(err) => Err(client_error_status(err)),
}
}
}
fn client_error_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) => client_auth_error_status(err),
ClientError::ConnectionRegistrationFailed => {
Status::internal("Connection registration failed")
}
}
}
fn client_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"),
}
}
fn user_agent_error_status(value: TransportResponseError) -> Status {
match value {
TransportResponseError::MissingRequestPayload
| 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")
}
}
}
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"),
}
}
pub struct Server {
context: ServerContext,
}
@@ -142,60 +26,3 @@ impl Server {
}
}
#[async_trait]
impl arbiter_proto::proto::arbiter_service_server::ArbiterService for Server {
type UserAgentStream = ReceiverStream<Result<UserAgentResponse, Status>>;
type ClientStream = ReceiverStream<Result<ClientResponse, Status>>;
#[tracing::instrument(level = "debug", skip(self))]
async fn client(
&self,
request: Request<tonic::Streaming<ClientRequest>>,
) -> Result<Response<Self::ClientStream>, Status> {
let req_stream = request.into_inner();
let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
let transport = grpc::GrpcAdapter::new(
tx,
req_stream,
IdentityRecvConverter::<ClientRequest>::new(),
ClientGrpcSender,
);
let props = ClientConnectionProps::new(
self.context.db.clone(),
Box::new(transport),
self.context.actors.clone(),
);
tokio::spawn(connect_client(props));
info!(event = "connection established", "grpc.client");
Ok(Response::new(ReceiverStream::new(rx)))
}
#[tracing::instrument(level = "debug", skip(self))]
async fn user_agent(
&self,
request: Request<tonic::Streaming<UserAgentRequest>>,
) -> Result<Response<Self::UserAgentStream>, Status> {
let req_stream = request.into_inner();
let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
let transport = grpc::GrpcAdapter::new(
tx,
req_stream,
IdentityRecvConverter::<UserAgentRequest>::new(),
UserAgentGrpcSender,
);
let props = UserAgentConnection::new(
self.context.db.clone(),
self.context.actors.clone(),
Box::new(transport),
);
tokio::spawn(connect_user_agent(props));
info!(event = "connection established", "grpc.user_agent");
Ok(Response::new(ReceiverStream::new(rx)))
}
}

View File

@@ -0,0 +1,111 @@
use std::ops::{Deref, DerefMut};
use std::{any::type_name, fmt};
use memsafe::MemSafe;
pub trait SafeCellHandle<T> {
type CellRead<'a>: Deref<Target = T>
where
Self: 'a,
T: 'a;
type CellWrite<'a>: Deref<Target = T> + DerefMut<Target = T>
where
Self: 'a,
T: 'a;
fn new(value: T) -> Self
where
Self: Sized;
fn read(&mut self) -> Self::CellRead<'_>;
fn write(&mut self) -> Self::CellWrite<'_>;
fn new_inline<F>(f: F) -> Self
where
Self: Sized,
T: Default,
F: for<'a> FnOnce(&'a mut T),
{
let mut cell = Self::new(T::default());
{
let mut handle = cell.write();
f(handle.deref_mut());
}
cell
}
#[inline(always)]
fn read_inline<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&T) -> R,
{
f(&*self.read())
}
#[inline(always)]
fn write_inline<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&mut T) -> R,
{
f(&mut *self.write())
}
}
pub struct MemSafeCell<T>(MemSafe<T>);
impl<T> fmt::Debug for MemSafeCell<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MemSafeCell")
.field("inner", &format_args!("<protected {}>", type_name::<T>()))
.finish()
}
}
impl<T> SafeCellHandle<T> for MemSafeCell<T> {
type CellRead<'a>
= memsafe::MemSafeRead<'a, T>
where
Self: 'a,
T: 'a;
type CellWrite<'a>
= memsafe::MemSafeWrite<'a, T>
where
Self: 'a,
T: 'a;
fn new(value: T) -> Self {
match MemSafe::new(value) {
Ok(inner) => Self(inner),
Err(err) => {
// If protected memory cannot be allocated, process integrity is compromised.
abort_memory_breach("safe cell allocation", &err)
}
}
}
#[inline(always)]
fn read(&mut self) -> Self::CellRead<'_> {
match self.0.read() {
Ok(inner) => inner,
Err(err) => abort_memory_breach("safe cell read", &err),
}
}
#[inline(always)]
fn write(&mut self) -> Self::CellWrite<'_> {
match self.0.write() {
Ok(inner) => inner,
Err(err) => {
// If protected memory becomes unwritable here, treat it as a fatal memory breach.
abort_memory_breach("safe cell write", &err)
}
}
}
}
fn abort_memory_breach(action: &str, err: &memsafe::error::MemoryError) -> ! {
eprintln!("fatal {action}: {err}");
std::process::abort();
}
pub type SafeCell<T> = MemSafeCell<T>;

View File

@@ -1,12 +1,7 @@
use arbiter_proto::proto::client::{
AuthChallengeRequest, AuthChallengeSolution, ClientRequest,
client_request::Payload as ClientRequestPayload,
client_response::Payload as ClientResponsePayload,
};
use arbiter_proto::transport::Bi;
use arbiter_server::actors::GlobalActors;
use arbiter_server::{
actors::client::{ClientConnection, connect_client},
actors::client::{ClientConnection, Request, Response, connect_client},
db::{self, schema},
};
use diesel::{ExpressionMethods as _, insert_into};
@@ -29,12 +24,8 @@ pub async fn test_unregistered_pubkey_rejected() {
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
test_transport
.send(ClientRequest {
payload: Some(ClientRequestPayload::AuthChallengeRequest(
AuthChallengeRequest {
.send(Request::AuthChallengeRequest {
pubkey: pubkey_bytes,
},
)),
})
.await
.unwrap();
@@ -68,12 +59,8 @@ pub async fn test_challenge_auth() {
// Send challenge request
test_transport
.send(ClientRequest {
payload: Some(ClientRequestPayload::AuthChallengeRequest(
AuthChallengeRequest {
.send(Request::AuthChallengeRequest {
pubkey: pubkey_bytes,
},
)),
})
.await
.unwrap();
@@ -84,24 +71,20 @@ pub async fn test_challenge_auth() {
.await
.expect("should receive challenge");
let challenge = match response {
Ok(resp) => match resp.payload {
Some(ClientResponsePayload::AuthChallenge(c)) => c,
Ok(resp) => match resp {
Response::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.nonce, &challenge.pubkey);
let formatted_challenge = arbiter_proto::format_challenge(challenge.1, &challenge.0);
let signature = new_key.sign(&formatted_challenge);
test_transport
.send(ClientRequest {
payload: Some(ClientRequestPayload::AuthChallengeSolution(
AuthChallengeSolution {
.send(Request::AuthChallengeSolution {
signature: signature.to_bytes().to_vec(),
},
)),
})
.await
.unwrap();

View File

@@ -1,19 +1,18 @@
use arbiter_proto::transport::{Bi, Error};
use arbiter_server::{
actors::keyholder::KeyHolder,
db::{self, schema},
db::{self, schema}, safe_cell::{SafeCell, SafeCellHandle as _},
};
use async_trait::async_trait;
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use memsafe::MemSafe;
use tokio::sync::mpsc;
#[allow(dead_code)]
pub async fn bootstrapped_keyholder(db: &db::DatabasePool) -> KeyHolder {
let mut actor = KeyHolder::new(db.clone()).await.unwrap();
actor
.bootstrap(MemSafe::new(b"test-seal-key".to_vec()).unwrap())
.bootstrap(SafeCell::new(b"test-seal-key".to_vec()))
.await
.unwrap();
actor

View File

@@ -3,11 +3,11 @@ use std::collections::{HashMap, HashSet};
use arbiter_server::{
actors::keyholder::{CreateNew, Error, KeyHolder},
db::{self, models, schema},
safe_cell::{SafeCell, SafeCellHandle as _},
};
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::sql_query};
use diesel_async::RunQueryDsl;
use kameo::actor::{ActorRef, Spawn as _};
use memsafe::MemSafe;
use tokio::task::JoinSet;
use crate::common;
@@ -24,7 +24,7 @@ async fn write_concurrently(
let plaintext = format!("{prefix}-{i}").into_bytes();
let id = actor
.ask(CreateNew {
plaintext: MemSafe::new(plaintext.clone()).unwrap(),
plaintext: SafeCell::new(plaintext.clone()),
})
.await
.unwrap();
@@ -118,7 +118,7 @@ async fn insert_failure_does_not_create_partial_row() {
drop(conn);
let err = actor
.create_new(MemSafe::new(b"should fail".to_vec()).unwrap())
.create_new(SafeCell::new(b"should fail".to_vec()))
.await
.unwrap_err();
assert!(matches!(err, Error::DatabaseTransaction(_)));
@@ -162,12 +162,12 @@ async fn decrypt_roundtrip_after_high_concurrency() {
let mut decryptor = KeyHolder::new(db.clone()).await.unwrap();
decryptor
.try_unseal(MemSafe::new(b"test-seal-key".to_vec()).unwrap())
.try_unseal(SafeCell::new(b"test-seal-key".to_vec()))
.await
.unwrap();
for (id, plaintext) in expected {
let mut decrypted = decryptor.decrypt(id).await.unwrap();
assert_eq!(*decrypted.read().unwrap(), plaintext);
assert_eq!(*decrypted.read(), plaintext);
}
}

View File

@@ -1,10 +1,10 @@
use arbiter_server::{
actors::keyholder::{Error, KeyHolder},
db::{self, models, schema},
safe_cell::{SafeCell, SafeCellHandle as _},
};
use diesel::{QueryDsl, SelectableHelper};
use diesel_async::RunQueryDsl;
use memsafe::MemSafe;
use crate::common;
@@ -14,7 +14,7 @@ async fn test_bootstrap() {
let db = db::create_test_pool().await;
let mut actor = KeyHolder::new(db.clone()).await.unwrap();
let seal_key = MemSafe::new(b"test-seal-key".to_vec()).unwrap();
let seal_key = SafeCell::new(b"test-seal-key".to_vec());
actor.bootstrap(seal_key).await.unwrap();
let mut conn = db.get().await.unwrap();
@@ -43,7 +43,7 @@ async fn test_bootstrap_rejects_double() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_keyholder(&db).await;
let seal_key2 = MemSafe::new(b"test-seal-key".to_vec()).unwrap();
let seal_key2 = SafeCell::new(b"test-seal-key".to_vec());
let err = actor.bootstrap(seal_key2).await.unwrap_err();
assert!(matches!(err, Error::AlreadyBootstrapped));
}
@@ -55,7 +55,7 @@ async fn test_create_new_before_bootstrap_fails() {
let mut actor = KeyHolder::new(db).await.unwrap();
let err = actor
.create_new(MemSafe::new(b"data".to_vec()).unwrap())
.create_new(SafeCell::new(b"data".to_vec()))
.await
.unwrap_err();
assert!(matches!(err, Error::NotBootstrapped));
@@ -91,17 +91,17 @@ async fn test_unseal_correct_password() {
let plaintext = b"survive a restart";
let aead_id = actor
.create_new(MemSafe::new(plaintext.to_vec()).unwrap())
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
drop(actor);
let mut actor = KeyHolder::new(db.clone()).await.unwrap();
let seal_key = MemSafe::new(b"test-seal-key".to_vec()).unwrap();
let seal_key = SafeCell::new(b"test-seal-key".to_vec());
actor.try_unseal(seal_key).await.unwrap();
let mut decrypted = actor.decrypt(aead_id).await.unwrap();
assert_eq!(*decrypted.read().unwrap(), plaintext);
assert_eq!(*decrypted.read(), plaintext);
}
#[tokio::test]
@@ -112,20 +112,20 @@ async fn test_unseal_wrong_then_correct_password() {
let plaintext = b"important data";
let aead_id = actor
.create_new(MemSafe::new(plaintext.to_vec()).unwrap())
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
drop(actor);
let mut actor = KeyHolder::new(db.clone()).await.unwrap();
let bad_key = MemSafe::new(b"wrong-password".to_vec()).unwrap();
let bad_key = SafeCell::new(b"wrong-password".to_vec());
let err = actor.try_unseal(bad_key).await.unwrap_err();
assert!(matches!(err, Error::InvalidKey));
let good_key = MemSafe::new(b"test-seal-key".to_vec()).unwrap();
let good_key = SafeCell::new(b"test-seal-key".to_vec());
actor.try_unseal(good_key).await.unwrap();
let mut decrypted = actor.decrypt(aead_id).await.unwrap();
assert_eq!(*decrypted.read().unwrap(), plaintext);
assert_eq!(*decrypted.read(), plaintext);
}

View File

@@ -3,10 +3,10 @@ use std::collections::HashSet;
use arbiter_server::{
actors::keyholder::{Error, encryption::v1},
db::{self, models, schema},
safe_cell::{SafeCell, SafeCellHandle as _},
};
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update};
use diesel_async::RunQueryDsl;
use memsafe::MemSafe;
use crate::common;
@@ -18,12 +18,12 @@ async fn test_create_decrypt_roundtrip() {
let plaintext = b"hello arbiter";
let aead_id = actor
.create_new(MemSafe::new(plaintext.to_vec()).unwrap())
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
let mut decrypted = actor.decrypt(aead_id).await.unwrap();
assert_eq!(*decrypted.read().unwrap(), plaintext);
assert_eq!(*decrypted.read(), plaintext);
}
#[tokio::test]
@@ -44,11 +44,11 @@ async fn test_ciphertext_differs_across_entries() {
let plaintext = b"same content";
let id1 = actor
.create_new(MemSafe::new(plaintext.to_vec()).unwrap())
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
let id2 = actor
.create_new(MemSafe::new(plaintext.to_vec()).unwrap())
.create_new(SafeCell::new(plaintext.to_vec()))
.await
.unwrap();
@@ -70,8 +70,8 @@ async fn test_ciphertext_differs_across_entries() {
let mut d1 = actor.decrypt(id1).await.unwrap();
let mut d2 = actor.decrypt(id2).await.unwrap();
assert_eq!(*d1.read().unwrap(), plaintext);
assert_eq!(*d2.read().unwrap(), plaintext);
assert_eq!(*d1.read(), plaintext);
assert_eq!(*d2.read(), plaintext);
}
#[tokio::test]
@@ -83,7 +83,7 @@ async fn test_nonce_never_reused() {
let n = 5;
for i in 0..n {
actor
.create_new(MemSafe::new(format!("secret {i}").into_bytes()).unwrap())
.create_new(SafeCell::new(format!("secret {i}").into_bytes()))
.await
.unwrap();
}
@@ -137,7 +137,7 @@ async fn broken_db_nonce_format_fails_closed() {
drop(conn);
let err = actor
.create_new(MemSafe::new(b"must fail".to_vec()).unwrap())
.create_new(SafeCell::new(b"must fail".to_vec()))
.await
.unwrap_err();
assert!(matches!(err, Error::BrokenDatabase));
@@ -145,7 +145,7 @@ async fn broken_db_nonce_format_fails_closed() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_keyholder(&db).await;
let id = actor
.create_new(MemSafe::new(b"decrypt target".to_vec()).unwrap())
.create_new(SafeCell::new(b"decrypt target".to_vec()))
.await
.unwrap();
let mut conn = db.get().await.unwrap();

View File

@@ -1,14 +1,9 @@
use arbiter_proto::proto::user_agent::{
AuthChallengeRequest, AuthChallengeSolution, KeyType as ProtoKeyType, UserAgentRequest,
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
};
use arbiter_proto::transport::Bi;
use arbiter_server::{
actors::{
GlobalActors,
bootstrap::GetToken,
user_agent::{UserAgentConnection, connect_user_agent},
user_agent::{AuthPublicKey, Request, Response, UserAgentConnection, connect_user_agent},
},
db::{self, schema},
};
@@ -30,17 +25,10 @@ pub async fn test_bootstrap_token_auth() {
let task = tokio::spawn(connect_user_agent(props));
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
test_transport
.send(UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(
AuthChallengeRequest {
pubkey: pubkey_bytes,
.send(Request::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
bootstrap_token: Some(token),
key_type: ProtoKeyType::Ed25519.into(),
},
)),
})
.await
.unwrap();
@@ -67,17 +55,10 @@ pub async fn test_bootstrap_invalid_token_auth() {
let task = tokio::spawn(connect_user_agent(props));
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
test_transport
.send(UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(
AuthChallengeRequest {
pubkey: pubkey_bytes,
.send(Request::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
bootstrap_token: Some("invalid_token".to_string()),
key_type: ProtoKeyType::Ed25519.into(),
},
)),
})
.await
.unwrap();
@@ -123,14 +104,9 @@ pub async fn test_challenge_auth() {
// Send challenge request
test_transport
.send(UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(
AuthChallengeRequest {
pubkey: pubkey_bytes,
.send(Request::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
bootstrap_token: None,
key_type: ProtoKeyType::Ed25519.into(),
},
)),
})
.await
.unwrap();
@@ -141,24 +117,19 @@ pub async fn test_challenge_auth() {
.await
.expect("should receive challenge");
let challenge = match response {
Ok(resp) => match resp.payload {
Some(UserAgentResponsePayload::AuthChallenge(c)) => c,
Ok(resp) => match resp {
Response::AuthChallenge { nonce } => 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.nonce, &challenge.pubkey);
let formatted_challenge = arbiter_proto::format_challenge(challenge, &pubkey_bytes);
let signature = new_key.sign(&formatted_challenge);
test_transport
.send(UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeSolution(
AuthChallengeSolution {
.send(Request::AuthChallengeSolution {
signature: signature.to_bytes().to_vec(),
},
)),
})
.await
.unwrap();

View File

@@ -1,30 +1,23 @@
use arbiter_proto::proto::user_agent::{
UnsealEncryptedKey, UnsealResult, UnsealStart, UserAgentRequest,
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
};
use arbiter_server::{
actors::{
GlobalActors,
keyholder::{Bootstrap, Seal},
user_agent::session::UserAgentSession,
user_agent::{Request, Response, UnsealError, session::UserAgentSession},
},
db,
safe_cell::{SafeCell, SafeCellHandle as _},
};
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use memsafe::MemSafe;
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, UserAgentSession) {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
.key_holder
.ask(Bootstrap {
seal_key_raw: MemSafe::new(seal_key.to_vec()).unwrap(),
seal_key_raw: SafeCell::new(seal_key.to_vec()),
})
.await
.unwrap();
@@ -35,29 +28,23 @@ async fn setup_sealed_user_agent(
(db, session)
}
async fn client_dh_encrypt(
user_agent: &mut UserAgentSession,
key_to_send: &[u8],
) -> UnsealEncryptedKey {
async fn client_dh_encrypt(user_agent: &mut UserAgentSession, key_to_send: &[u8]) -> Request {
let client_secret = EphemeralSecret::random();
let client_public = PublicKey::from(&client_secret);
let response = user_agent
.process_transport_inbound(UserAgentRequest {
payload: Some(UserAgentRequestPayload::UnsealStart(UnsealStart {
client_pubkey: client_public.as_bytes().to_vec(),
})),
.process_transport_inbound(Request::UnsealStart {
client_pubkey: client_public,
})
.await
.unwrap();
let server_pubkey = match response.payload.unwrap() {
UserAgentResponsePayload::UnsealStartResponse(resp) => resp.server_pubkey,
let server_pubkey = match response {
Response::UnsealStartResponse { server_pubkey } => server_pubkey,
other => panic!("Expected UnsealStartResponse, got {other:?}"),
};
let server_public = PublicKey::from(<[u8; 32]>::try_from(server_pubkey.as_slice()).unwrap());
let shared_secret = client_secret.diffie_hellman(&server_public);
let shared_secret = client_secret.diffie_hellman(&server_pubkey);
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
let nonce = XNonce::from([0u8; 24]);
let associated_data = b"unseal";
@@ -66,19 +53,13 @@ async fn client_dh_encrypt(
.encrypt_in_place(&nonce, associated_data, &mut ciphertext)
.unwrap();
UnsealEncryptedKey {
Request::UnsealEncryptedKey {
nonce: nonce.to_vec(),
ciphertext,
associated_data: associated_data.to_vec(),
}
}
fn unseal_key_request(req: UnsealEncryptedKey) -> UserAgentRequest {
UserAgentRequest {
payload: Some(UserAgentRequestPayload::UnsealEncryptedKey(req)),
}
}
#[tokio::test]
#[test_log::test]
pub async fn test_unseal_success() {
@@ -88,14 +69,11 @@ pub async fn test_unseal_success() {
let encrypted_key = client_dh_encrypt(&mut user_agent, seal_key).await;
let response = user_agent
.process_transport_inbound(unseal_key_request(encrypted_key))
.process_transport_inbound(encrypted_key)
.await
.unwrap();
assert_eq!(
response.payload.unwrap(),
UserAgentResponsePayload::UnsealResult(UnsealResult::Success.into()),
);
assert!(matches!(response, Response::UnsealResult(Ok(()))));
}
#[tokio::test]
@@ -106,14 +84,14 @@ pub async fn test_unseal_wrong_seal_key() {
let encrypted_key = client_dh_encrypt(&mut user_agent, b"wrong-key").await;
let response = user_agent
.process_transport_inbound(unseal_key_request(encrypted_key))
.process_transport_inbound(encrypted_key)
.await
.unwrap();
assert_eq!(
response.payload.unwrap(),
UserAgentResponsePayload::UnsealResult(UnsealResult::InvalidKey.into()),
);
assert!(matches!(
response,
Response::UnsealResult(Err(UnsealError::InvalidKey))
));
}
#[tokio::test]
@@ -125,27 +103,25 @@ pub async fn test_unseal_corrupted_ciphertext() {
let client_public = PublicKey::from(&client_secret);
user_agent
.process_transport_inbound(UserAgentRequest {
payload: Some(UserAgentRequestPayload::UnsealStart(UnsealStart {
client_pubkey: client_public.as_bytes().to_vec(),
})),
.process_transport_inbound(Request::UnsealStart {
client_pubkey: client_public,
})
.await
.unwrap();
let response = user_agent
.process_transport_inbound(unseal_key_request(UnsealEncryptedKey {
.process_transport_inbound(Request::UnsealEncryptedKey {
nonce: vec![0u8; 24],
ciphertext: vec![0u8; 32],
associated_data: vec![],
}))
})
.await
.unwrap();
assert_eq!(
response.payload.unwrap(),
UserAgentResponsePayload::UnsealResult(UnsealResult::InvalidKey.into()),
);
assert!(matches!(
response,
Response::UnsealResult(Err(UnsealError::InvalidKey))
));
}
#[tokio::test]
@@ -158,27 +134,24 @@ pub async fn test_unseal_retry_after_invalid_key() {
let encrypted_key = client_dh_encrypt(&mut user_agent, b"wrong-key").await;
let response = user_agent
.process_transport_inbound(unseal_key_request(encrypted_key))
.process_transport_inbound(encrypted_key)
.await
.unwrap();
assert_eq!(
response.payload.unwrap(),
UserAgentResponsePayload::UnsealResult(UnsealResult::InvalidKey.into()),
);
assert!(matches!(
response,
Response::UnsealResult(Err(UnsealError::InvalidKey))
));
}
{
let encrypted_key = client_dh_encrypt(&mut user_agent, seal_key).await;
let response = user_agent
.process_transport_inbound(unseal_key_request(encrypted_key))
.process_transport_inbound(encrypted_key)
.await
.unwrap();
assert_eq!(
response.payload.unwrap(),
UserAgentResponsePayload::UnsealResult(UnsealResult::Success.into()),
);
assert!(matches!(response, Response::UnsealResult(Ok(()))));
}
}

View File

@@ -1,29 +0,0 @@
[package]
name = "arbiter-useragent"
version = "0.1.0"
edition = "2024"
license = "Apache-2.0"
[lints]
workspace = true
[dependencies]
arbiter-proto.path = "../arbiter-proto"
kameo.workspace = true
tokio = {workspace = true, features = ["net"]}
tonic.workspace = true
tonic.features = ["tls-aws-lc"]
tracing.workspace = true
ed25519-dalek.workspace = true
smlang.workspace = true
x25519-dalek.workspace = true
k256.workspace = true
rsa.workspace = true
sha2.workspace = true
spki.workspace = true
rand.workspace = true
thiserror.workspace = true
tokio-stream.workspace = true
http = "1.4.0"
rustls-webpki = { version = "0.103.9", features = ["aws-lc-rs"] }
async-trait.workspace = true

View File

@@ -1,70 +0,0 @@
use arbiter_proto::{
proto::{
arbiter_service_client::ArbiterServiceClient,
user_agent::{UserAgentRequest, UserAgentResponse},
},
transport::{IdentityRecvConverter, IdentitySendConverter, grpc},
url::ArbiterUrl,
};
use kameo::actor::{ActorRef, Spawn};
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use tonic::transport::ClientTlsConfig;
use super::{SigningKeyEnum, UserAgentActor};
#[derive(Debug, thiserror::Error)]
pub enum ConnectError {
#[error("Could establish connection")]
Connection(#[from] tonic::transport::Error),
#[error("Invalid server URI")]
InvalidUri(#[from] http::uri::InvalidUri),
#[error("Invalid CA certificate")]
InvalidCaCert(#[from] webpki::Error),
#[error("gRPC error")]
Grpc(#[from] tonic::Status),
}
pub type UserAgentGrpc = ActorRef<
UserAgentActor<
grpc::GrpcAdapter<
IdentityRecvConverter<UserAgentResponse>,
IdentitySendConverter<UserAgentRequest>,
>,
>,
>;
pub async fn connect_grpc(
url: ArbiterUrl,
key: SigningKeyEnum,
) -> Result<UserAgentGrpc, ConnectError> {
let bootstrap_token = url.bootstrap_token.clone();
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
let tls = ClientTlsConfig::new().trust_anchor(anchor);
// TODO: if `host` is localhost, we need to verify server's process authenticity
let channel = tonic::transport::Channel::from_shared(format!("{}:{}", url.host, url.port))?
.tls_config(tls)?
.connect()
.await?;
let mut client = ArbiterServiceClient::new(channel);
let (tx, rx) = mpsc::channel(16);
let bistream = client.user_agent(ReceiverStream::new(rx)).await?;
let bistream = bistream.into_inner();
let adapter = grpc::GrpcAdapter::new(
tx,
bistream,
IdentityRecvConverter::new(),
IdentitySendConverter::new(),
);
let actor = UserAgentActor::spawn(UserAgentActor::new(key, bootstrap_token, adapter));
Ok(actor)
}

View File

@@ -1,257 +0,0 @@
use arbiter_proto::{
format_challenge,
proto::user_agent::{
AuthChallengeRequest, AuthChallengeSolution, AuthOk, KeyType as ProtoKeyType,
UserAgentRequest, UserAgentResponse,
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
},
transport::Bi,
};
use kameo::{Actor, actor::ActorRef};
use smlang::statemachine;
use tokio::select;
use tracing::{error, info};
/// Signing key variants supported by the user-agent auth protocol.
pub enum SigningKeyEnum {
Ed25519(ed25519_dalek::SigningKey),
/// secp256k1 ECDSA; public key is sent as SEC1 compressed 33 bytes; signature is raw 64-byte (r||s).
EcdsaSecp256k1(k256::ecdsa::SigningKey),
/// RSA for Windows Hello (KeyCredentialManager); public key is DER SPKI; signature is PSS+SHA-256.
Rsa(rsa::RsaPrivateKey),
}
impl SigningKeyEnum {
/// Returns the canonical public key bytes to include in `AuthChallengeRequest.pubkey`.
pub fn pubkey_bytes(&self) -> Vec<u8> {
match self {
SigningKeyEnum::Ed25519(k) => k.verifying_key().to_bytes().to_vec(),
// 33-byte SEC1 compressed point — compact and natively supported by secp256k1 tooling
SigningKeyEnum::EcdsaSecp256k1(k) => {
k.verifying_key().to_encoded_point(true).as_bytes().to_vec()
}
SigningKeyEnum::Rsa(k) => {
use rsa::pkcs8::EncodePublicKey as _;
k.to_public_key()
.to_public_key_der()
.expect("rsa SPKI encoding is infallible")
.to_vec()
}
}
}
/// Returns the proto `KeyType` discriminant to send in `AuthChallengeRequest.key_type`.
pub fn proto_key_type(&self) -> ProtoKeyType {
match self {
SigningKeyEnum::Ed25519(_) => ProtoKeyType::Ed25519,
SigningKeyEnum::EcdsaSecp256k1(_) => ProtoKeyType::EcdsaSecp256k1,
SigningKeyEnum::Rsa(_) => ProtoKeyType::Rsa,
}
}
/// Signs `msg` and returns raw signature bytes matching the server-side verification.
pub fn sign(&self, msg: &[u8]) -> Vec<u8> {
match self {
SigningKeyEnum::Ed25519(k) => {
use ed25519_dalek::Signer as _;
k.sign(msg).to_bytes().to_vec()
}
SigningKeyEnum::EcdsaSecp256k1(k) => {
use k256::ecdsa::signature::Signer as _;
let sig: k256::ecdsa::Signature = k.sign(msg);
sig.to_bytes().to_vec()
}
SigningKeyEnum::Rsa(k) => {
use rsa::signature::RandomizedSigner as _;
let signing_key = rsa::pss::BlindedSigningKey::<sha2::Sha256>::new(k.clone());
// Use rand_core OsRng from the rsa crate's re-exported rand_core (0.6.x),
// which is the version rsa's signature API expects.
let sig = signing_key.sign_with_rng(&mut rsa::rand_core::OsRng, msg);
use rsa::signature::SignatureEncoding as _;
sig.to_vec()
}
}
}
}
statemachine! {
name: UserAgent,
custom_error: false,
transitions: {
*Init + SentAuthChallengeRequest = WaitingForServerAuth,
WaitingForServerAuth + ReceivedAuthChallenge = WaitingForAuthOk,
WaitingForServerAuth + ReceivedAuthOk = Authenticated,
WaitingForAuthOk + ReceivedAuthOk = Authenticated,
}
}
pub struct DummyContext;
impl UserAgentStateMachineContext for DummyContext {}
#[derive(Debug, thiserror::Error)]
pub enum InboundError {
#[error("Invalid user agent response")]
InvalidResponse,
#[error("Expected response payload")]
MissingResponsePayload,
#[error("Unexpected response payload")]
UnexpectedResponsePayload,
#[error("Invalid state for auth challenge")]
InvalidStateForAuthChallenge,
#[error("Invalid state for auth ok")]
InvalidStateForAuthOk,
#[error("State machine error")]
StateTransitionFailed,
#[error("Transport send failed")]
TransportSendFailed,
}
pub struct UserAgentActor<Transport>
where
Transport: Bi<UserAgentResponse, UserAgentRequest>,
{
key: SigningKeyEnum,
bootstrap_token: Option<String>,
state: UserAgentStateMachine<DummyContext>,
transport: Transport,
}
impl<Transport> UserAgentActor<Transport>
where
Transport: Bi<UserAgentResponse, UserAgentRequest>,
{
pub fn new(key: SigningKeyEnum, bootstrap_token: Option<String>, transport: Transport) -> Self {
Self {
key,
bootstrap_token,
state: UserAgentStateMachine::new(DummyContext),
transport,
}
}
fn transition(&mut self, event: UserAgentEvents) -> Result<(), InboundError> {
self.state.process_event(event).map_err(|e| {
error!(?e, "useragent state transition failed");
InboundError::StateTransitionFailed
})?;
Ok(())
}
async fn send_auth_challenge_request(&mut self) -> Result<(), InboundError> {
let req = AuthChallengeRequest {
pubkey: self.key.pubkey_bytes(),
bootstrap_token: self.bootstrap_token.take(),
key_type: self.key.proto_key_type().into(),
};
self.transition(UserAgentEvents::SentAuthChallengeRequest)?;
self.transport
.send(UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(req)),
})
.await
.map_err(|_| InboundError::TransportSendFailed)?;
info!(actor = "useragent", "auth.request.sent");
Ok(())
}
async fn handle_auth_challenge(
&mut self,
challenge: arbiter_proto::proto::user_agent::AuthChallenge,
) -> Result<(), InboundError> {
self.transition(UserAgentEvents::ReceivedAuthChallenge)?;
let formatted = format_challenge(challenge.nonce, &challenge.pubkey);
let signature_bytes = self.key.sign(&formatted);
let solution = AuthChallengeSolution {
signature: signature_bytes,
};
self.transport
.send(UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeSolution(solution)),
})
.await
.map_err(|_| InboundError::TransportSendFailed)?;
info!(actor = "useragent", "auth.solution.sent");
Ok(())
}
fn handle_auth_ok(&mut self, _ok: AuthOk) -> Result<(), InboundError> {
self.transition(UserAgentEvents::ReceivedAuthOk)?;
info!(actor = "useragent", "auth.ok");
Ok(())
}
pub async fn process_inbound_transport(
&mut self,
inbound: UserAgentResponse,
) -> Result<(), InboundError> {
let payload = inbound
.payload
.ok_or(InboundError::MissingResponsePayload)?;
match payload {
UserAgentResponsePayload::AuthChallenge(challenge) => {
self.handle_auth_challenge(challenge).await
}
UserAgentResponsePayload::AuthOk(ok) => self.handle_auth_ok(ok),
_ => Err(InboundError::UnexpectedResponsePayload),
}
}
}
impl<Transport> Actor for UserAgentActor<Transport>
where
Transport: Bi<UserAgentResponse, UserAgentRequest>,
{
type Args = Self;
type Error = ();
async fn on_start(
mut args: Self::Args,
_actor_ref: ActorRef<Self>,
) -> Result<Self, Self::Error> {
if let Err(err) = args.send_auth_challenge_request().await {
error!(?err, actor = "useragent", "auth.start.failed");
return Err(());
}
Ok(args)
}
async fn next(
&mut self,
_actor_ref: kameo::prelude::WeakActorRef<Self>,
mailbox_rx: &mut kameo::prelude::MailboxReceiver<Self>,
) -> Option<kameo::mailbox::Signal<Self>> {
loop {
select! {
signal = mailbox_rx.recv() => {
return signal;
}
inbound = self.transport.recv() => {
match inbound {
Some(inbound) => {
if let Err(err) = self.process_inbound_transport(inbound).await {
error!(?err, actor = "useragent", "transport.inbound.failed");
return Some(kameo::mailbox::Signal::Stop);
}
}
None => {
info!(actor = "useragent", "transport.closed");
return Some(kameo::mailbox::Signal::Stop);
}
}
}
}
}
}
}
mod grpc;
pub use grpc::{ConnectError, connect_grpc};

View File

@@ -1,146 +0,0 @@
use arbiter_proto::{
format_challenge,
proto::user_agent::{
AuthChallenge, AuthOk, UserAgentRequest, UserAgentResponse,
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
},
transport::Bi,
};
use arbiter_useragent::{SigningKeyEnum, UserAgentActor};
use async_trait::async_trait;
use ed25519_dalek::SigningKey;
use kameo::actor::Spawn;
use tokio::sync::mpsc;
use tokio::time::{Duration, timeout};
struct TestTransport {
inbound_rx: mpsc::Receiver<UserAgentResponse>,
outbound_tx: mpsc::Sender<UserAgentRequest>,
}
#[async_trait]
impl Bi<UserAgentResponse, UserAgentRequest> for TestTransport {
async fn send(
&mut self,
item: UserAgentRequest,
) -> Result<(), arbiter_proto::transport::Error> {
self.outbound_tx
.send(item)
.await
.map_err(|_| arbiter_proto::transport::Error::ChannelClosed)
}
async fn recv(&mut self) -> Option<UserAgentResponse> {
self.inbound_rx.recv().await
}
}
fn make_transport() -> (
TestTransport,
mpsc::Sender<UserAgentResponse>,
mpsc::Receiver<UserAgentRequest>,
) {
let (inbound_tx, inbound_rx) = mpsc::channel(8);
let (outbound_tx, outbound_rx) = mpsc::channel(8);
(
TestTransport {
inbound_rx,
outbound_tx,
},
inbound_tx,
outbound_rx,
)
}
fn test_key() -> SigningKeyEnum {
SigningKeyEnum::Ed25519(SigningKey::from_bytes(&[7u8; 32]))
}
#[tokio::test]
async fn sends_auth_request_on_start_with_bootstrap_token() {
let key = test_key();
let pubkey = key.pubkey_bytes();
let bootstrap_token = Some("bootstrap-123".to_string());
let (transport, inbound_tx, mut outbound_rx) = make_transport();
let actor = UserAgentActor::spawn(UserAgentActor::new(key, bootstrap_token.clone(), transport));
let outbound = timeout(Duration::from_secs(1), outbound_rx.recv())
.await
.expect("timed out waiting for auth request")
.expect("channel closed before auth request");
let UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(req)),
} = outbound
else {
panic!("expected auth challenge request");
};
assert_eq!(req.pubkey, pubkey);
assert_eq!(req.bootstrap_token, bootstrap_token);
drop(inbound_tx);
drop(actor);
}
#[tokio::test]
async fn challenge_flow_sends_solution_from_transport_inbound() {
let key = test_key();
let pubkey_bytes = key.pubkey_bytes();
let (transport, inbound_tx, mut outbound_rx) = make_transport();
let actor = UserAgentActor::spawn(UserAgentActor::new(key, None, transport));
let _initial_auth_request = timeout(Duration::from_secs(1), outbound_rx.recv())
.await
.expect("timed out waiting for initial auth request")
.expect("missing initial auth request");
let challenge = AuthChallenge {
pubkey: pubkey_bytes.clone(),
nonce: 42,
};
inbound_tx
.send(UserAgentResponse {
payload: Some(UserAgentResponsePayload::AuthChallenge(challenge.clone())),
})
.await
.unwrap();
let outbound = timeout(Duration::from_secs(1), outbound_rx.recv())
.await
.expect("timed out waiting for challenge solution")
.expect("missing challenge solution");
let UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeSolution(solution)),
} = outbound
else {
panic!("expected auth challenge solution");
};
// Verify the signature using the Ed25519 verifying key
let formatted = format_challenge(challenge.nonce, &challenge.pubkey);
let raw_key = SigningKey::from_bytes(&[7u8; 32]);
let sig: ed25519_dalek::Signature = solution
.signature
.as_slice()
.try_into()
.expect("signature bytes length");
raw_key
.verifying_key()
.verify_strict(&formatted, &sig)
.expect("solution signature should verify");
inbound_tx
.send(UserAgentResponse {
payload: Some(UserAgentResponsePayload::AuthOk(AuthOk {})),
})
.await
.unwrap();
drop(inbound_tx);
drop(actor);
}

0
server/rules/.gitkeep Normal file
View File

View File

@@ -0,0 +1,10 @@
id: safecell-new-inline
language: Rust
rule:
pattern: $CELL.write_inline(|$W| $BODY);
follows:
pattern: let mut $CELL = SafeCell::new($INIT);
fix:
template: let mut $CELL = SafeCell::new_inline(|$W| $BODY);
expandStart:
pattern: let mut $CELL = SafeCell::new($INIT)

View File

@@ -0,0 +1,17 @@
id: safecell-read-inline
language: Rust
rule:
pattern:
context: |
{
let $READ = $CELL.read();
$$$BODY
}
selector: block
inside:
kind: block
fix:
template: |
$CELL.read_inline(|$READ| {
$$$BODY
});

View File

@@ -0,0 +1,13 @@
id: safecell-write-inline
language: Rust
rule:
pattern: |
{
let mut $WRITE = $CELL.write();
$$$BODY
}
fix:
template: |
$CELL.write_inline(|$WRITE| {
$$$BODY
});

2
server/sgconfig.yml Normal file
View File

@@ -0,0 +1,2 @@
ruleDirs:
- ./rules

View File

@@ -1,31 +0,0 @@
Extension Discovery Cache
=========================
This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.
DO NOT USE THIS FOLDER
----------------------
* Do not read (or rely) the contents of this folder.
* Do write to this folder.
If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.
If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.
Use package `extension_discovery`
---------------------------------
If you want to access information from this folder.
Feel free to delete this folder
-------------------------------
Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.
Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.

View File

@@ -1 +0,0 @@
{"version":2,"entries":[{"package":"arbiter","rootUri":"../","packageUri":"lib/"}]}

View File

@@ -1,172 +0,0 @@
{
"configVersion": 2,
"packages": [
{
"name": "async",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/async-2.13.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "boolean_selector",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "characters",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/characters-1.4.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "clock",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/clock-1.1.2",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "collection",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/collection-1.19.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "fake_async",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/fake_async-1.3.3",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "flutter",
"rootUri": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable/packages/flutter",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "flutter_lints",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/flutter_lints-6.0.0",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "flutter_test",
"rootUri": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable/packages/flutter_test",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "leak_tracker",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/leak_tracker-11.0.2",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "leak_tracker_flutter_testing",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.10",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "leak_tracker_testing",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.2",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "lints",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/lints-6.1.0",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "matcher",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/matcher-0.12.17",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "material_color_utilities",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1",
"packageUri": "lib/",
"languageVersion": "2.17"
},
{
"name": "meta",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/meta-1.17.0",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "path",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/path-1.9.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "sky_engine",
"rootUri": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable/bin/cache/pkg/sky_engine",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "source_span",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/source_span-1.10.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "stack_trace",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/stack_trace-1.12.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "stream_channel",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/stream_channel-2.1.4",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "string_scanner",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/string_scanner-1.4.1",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "term_glyph",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/term_glyph-1.2.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "test_api",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/test_api-0.7.7",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "vector_math",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/vector_math-2.2.0",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "vm_service",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/vm_service-15.0.2",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "arbiter",
"rootUri": "../",
"packageUri": "lib/",
"languageVersion": "3.10"
}
],
"generator": "pub",
"generatorVersion": "3.10.8",
"flutterRoot": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable",
"flutterVersion": "3.38.9",
"pubCache": "file:///Users/kaska/.pub-cache"
}

View File

@@ -1,224 +0,0 @@
{
"roots": [
"arbiter"
],
"packages": [
{
"name": "arbiter",
"version": "0.1.0",
"dependencies": [
"flutter"
],
"devDependencies": [
"flutter_lints",
"flutter_test"
]
},
{
"name": "flutter_lints",
"version": "6.0.0",
"dependencies": [
"lints"
]
},
{
"name": "flutter_test",
"version": "0.0.0",
"dependencies": [
"clock",
"collection",
"fake_async",
"flutter",
"leak_tracker_flutter_testing",
"matcher",
"meta",
"path",
"stack_trace",
"stream_channel",
"test_api",
"vector_math"
]
},
{
"name": "flutter",
"version": "0.0.0",
"dependencies": [
"characters",
"collection",
"material_color_utilities",
"meta",
"sky_engine",
"vector_math"
]
},
{
"name": "lints",
"version": "6.1.0",
"dependencies": []
},
{
"name": "stream_channel",
"version": "2.1.4",
"dependencies": [
"async"
]
},
{
"name": "meta",
"version": "1.17.0",
"dependencies": []
},
{
"name": "collection",
"version": "1.19.1",
"dependencies": []
},
{
"name": "leak_tracker_flutter_testing",
"version": "3.0.10",
"dependencies": [
"flutter",
"leak_tracker",
"leak_tracker_testing",
"matcher",
"meta"
]
},
{
"name": "vector_math",
"version": "2.2.0",
"dependencies": []
},
{
"name": "stack_trace",
"version": "1.12.1",
"dependencies": [
"path"
]
},
{
"name": "clock",
"version": "1.1.2",
"dependencies": []
},
{
"name": "fake_async",
"version": "1.3.3",
"dependencies": [
"clock",
"collection"
]
},
{
"name": "path",
"version": "1.9.1",
"dependencies": []
},
{
"name": "matcher",
"version": "0.12.17",
"dependencies": [
"async",
"meta",
"stack_trace",
"term_glyph",
"test_api"
]
},
{
"name": "test_api",
"version": "0.7.7",
"dependencies": [
"async",
"boolean_selector",
"collection",
"meta",
"source_span",
"stack_trace",
"stream_channel",
"string_scanner",
"term_glyph"
]
},
{
"name": "sky_engine",
"version": "0.0.0",
"dependencies": []
},
{
"name": "material_color_utilities",
"version": "0.11.1",
"dependencies": [
"collection"
]
},
{
"name": "characters",
"version": "1.4.0",
"dependencies": []
},
{
"name": "async",
"version": "2.13.0",
"dependencies": [
"collection",
"meta"
]
},
{
"name": "leak_tracker_testing",
"version": "3.0.2",
"dependencies": [
"leak_tracker",
"matcher",
"meta"
]
},
{
"name": "leak_tracker",
"version": "11.0.2",
"dependencies": [
"clock",
"collection",
"meta",
"path",
"vm_service"
]
},
{
"name": "term_glyph",
"version": "1.2.2",
"dependencies": []
},
{
"name": "string_scanner",
"version": "1.4.1",
"dependencies": [
"source_span"
]
},
{
"name": "source_span",
"version": "1.10.2",
"dependencies": [
"collection",
"path",
"term_glyph"
]
},
{
"name": "boolean_selector",
"version": "2.1.2",
"dependencies": [
"source_span",
"string_scanner"
]
},
{
"name": "vm_service",
"version": "15.0.2",
"dependencies": []
}
],
"configVersion": 1
}

View File

@@ -1 +0,0 @@
3.38.9

View File

@@ -1,4 +1,4 @@
# app
# useragent
A new Flutter project.

14
useragent/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.useragent"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.useragent"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="useragent"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.example.useragent
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

View File

@@ -0,0 +1,56 @@
import 'dart:convert';
class ArbiterUrl {
const ArbiterUrl({
required this.host,
required this.port,
required this.caCert,
this.bootstrapToken,
});
final String host;
final int port;
final List<int> caCert;
final String? bootstrapToken;
static const _scheme = 'arbiter';
static const _certQueryKey = 'cert';
static const _bootstrapTokenQueryKey = 'bootstrap_token';
static ArbiterUrl parse(String value) {
final uri = Uri.tryParse(value);
if (uri == null || uri.scheme != _scheme) {
throw const FormatException("Invalid URL scheme, expected 'arbiter://'");
}
if (uri.host.isEmpty) {
throw const FormatException('Missing host in URL');
}
if (!uri.hasPort) {
throw const FormatException('Missing port in URL');
}
final cert = uri.queryParameters[_certQueryKey];
if (cert == null || cert.isEmpty) {
throw const FormatException("Missing 'cert' query parameter in URL");
}
final decodedCert = _decodeCert(cert);
return ArbiterUrl(
host: uri.host,
port: uri.port,
caCert: decodedCert,
bootstrapToken: uri.queryParameters[_bootstrapTokenQueryKey],
);
}
static List<int> _decodeCert(String cert) {
try {
return base64Url.decode(base64Url.normalize(cert));
} on FormatException catch (error) {
throw FormatException("Invalid base64 in 'cert' query parameter: ${error.message}");
}
}
}

View File

@@ -0,0 +1,103 @@
import 'dart:async';
import 'dart:convert';
import 'package:arbiter/features/connection/connection.dart';
import 'package:arbiter/features/connection/server_info_storage.dart';
import 'package:arbiter/features/identity/pk_manager.dart';
import 'package:arbiter/proto/arbiter.pbgrpc.dart';
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:grpc/grpc.dart';
import 'package:mtcore/markettakers.dart';
Future<Connection> connectAndAuthorize(
StoredServerInfo serverInfo,
KeyHandle key, {
String? bootstrapToken,
}) async {
try {
final connection = await _connect(serverInfo);
talker.info(
'Connected to server at ${serverInfo.address}:${serverInfo.port}',
);
final pubkey = await key.getPublicKey();
final req = AuthChallengeRequest(
pubkey: pubkey,
bootstrapToken: bootstrapToken,
keyType: switch (key.alg) {
KeyAlgorithm.rsa => KeyType.KEY_TYPE_RSA,
KeyAlgorithm.ecdsa => KeyType.KEY_TYPE_ECDSA_SECP256K1,
KeyAlgorithm.ed25519 => KeyType.KEY_TYPE_ED25519,
},
);
await connection.send(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()) {
talker.info('Authentication successful, connection established');
return connection;
}
if (!response.hasAuthChallenge()) {
throw Exception(
'Expected AuthChallengeResponse, got ${response.whichPayload()}',
);
}
final challenge = _formatChallenge(response.authChallenge, pubkey);
talker.info(
'Received auth challenge, signing with key ${base64Encode(pubkey)}',
);
final signature = await key.sign(challenge);
await connection.send(
UserAgentRequest(authChallengeSolution: AuthChallengeSolution(signature: signature)),
);
talker.info('Sent auth challenge solution, waiting for server response...');
final solutionResponse = await connection.receive();
if (!solutionResponse.hasAuthOk()) {
throw Exception(
'Expected AuthChallengeSolutionResponse, got ${solutionResponse.whichPayload()}',
);
}
talker.info('Authentication successful, connection established');
return connection;
} catch (e) {
throw Exception('Failed to connect to server: $e');
}
}
Future<Connection> _connect(StoredServerInfo serverInfo) async {
final channel = ClientChannel(
serverInfo.address,
port: serverInfo.port,
options: ChannelOptions(
connectTimeout: const Duration(seconds: 10),
credentials: ChannelCredentials.secure(
onBadCertificate: (cert, host) {
return true;
},
),
),
);
final client = ArbiterServiceClient(channel);
final tx = StreamController<UserAgentRequest>();
final rx = client.userAgent(tx.stream);
return Connection(channel: channel, tx: tx, rx: rx);
}
List<int> _formatChallenge(AuthChallenge challenge, List<int> pubkey) {
final encodedPubkey = base64Encode(pubkey);
final payload = "${challenge.nonce}:$encodedPubkey";
return utf8.encode(payload);
}

View File

@@ -0,0 +1,37 @@
import 'dart:async';
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:grpc/grpc.dart';
import 'package:mtcore/markettakers.dart';
class Connection {
final ClientChannel channel;
final StreamController<UserAgentRequest> _tx;
final StreamIterator<UserAgentResponse> _rx;
Connection({
required this.channel,
required StreamController<UserAgentRequest> tx,
required ResponseStream<UserAgentResponse> rx,
}) : _tx = tx,
_rx = StreamIterator(rx);
Future<void> send(UserAgentRequest request) async {
talker.debug('Sending request: ${request.toDebugString()}');
_tx.add(request);
}
Future<UserAgentResponse> receive() async {
final hasValue = await _rx.moveNext();
if (!hasValue) {
throw Exception('Connection closed while waiting for server response.');
}
talker.debug('Received response: ${_rx.current.toDebugString()}');
return _rx.current;
}
Future<void> close() async {
await _tx.close();
await channel.shutdown();
}
}

View File

@@ -0,0 +1,56 @@
import 'package:arbiter/features/connection/connection.dart';
import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
Future<List<WalletEntry>> listEvmWallets(Connection connection) async {
await connection.send(UserAgentRequest(evmWalletList: Empty()));
final response = await connection.receive();
if (!response.hasEvmWalletList()) {
throw Exception(
'Expected EVM wallet list response, got ${response.whichPayload()}',
);
}
final result = response.evmWalletList;
switch (result.whichResult()) {
case WalletListResponse_Result.wallets:
return result.wallets.wallets.toList(growable: false);
case WalletListResponse_Result.error:
throw Exception(_describeEvmError(result.error));
case WalletListResponse_Result.notSet:
throw Exception('EVM wallet list response was empty.');
}
}
Future<void> createEvmWallet(Connection connection) async {
await connection.send(UserAgentRequest(evmWalletCreate: Empty()));
final response = await connection.receive();
if (!response.hasEvmWalletCreate()) {
throw Exception(
'Expected EVM wallet create response, got ${response.whichPayload()}',
);
}
final result = response.evmWalletCreate;
switch (result.whichResult()) {
case WalletCreateResponse_Result.wallet:
return;
case WalletCreateResponse_Result.error:
throw Exception(_describeEvmError(result.error));
case WalletCreateResponse_Result.notSet:
throw Exception('Wallet creation returned no result.');
}
}
String _describeEvmError(EvmError error) {
return switch (error) {
EvmError.EVM_ERROR_VAULT_SEALED =>
'The vault is sealed. Unseal it before using EVM wallets.',
EvmError.EVM_ERROR_INTERNAL || EvmError.EVM_ERROR_UNSPECIFIED =>
'The server failed to process the EVM request.',
_ => 'The server failed to process the EVM request.',
};
}

View File

@@ -0,0 +1,122 @@
import 'package:arbiter/features/connection/connection.dart';
import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:fixnum/fixnum.dart';
import 'package:protobuf/well_known_types/google/protobuf/timestamp.pb.dart';
Future<List<GrantEntry>> listEvmGrants(
Connection connection, {
int? walletId,
}) async {
final request = EvmGrantListRequest();
if (walletId != null) {
request.walletId = walletId;
}
await connection.send(UserAgentRequest(evmGrantList: request));
final response = await connection.receive();
if (!response.hasEvmGrantList()) {
throw Exception(
'Expected EVM grant list response, got ${response.whichPayload()}',
);
}
final result = response.evmGrantList;
switch (result.whichResult()) {
case EvmGrantListResponse_Result.grants:
return result.grants.grants.toList(growable: false);
case EvmGrantListResponse_Result.error:
throw Exception(_describeGrantError(result.error));
case EvmGrantListResponse_Result.notSet:
throw Exception('EVM grant list response was empty.');
}
}
Future<int> createEvmGrant(
Connection connection, {
required int clientId,
required int walletId,
required Int64 chainId,
DateTime? validFrom,
DateTime? validUntil,
List<int>? maxGasFeePerGas,
List<int>? maxPriorityFeePerGas,
TransactionRateLimit? rateLimit,
required SpecificGrant specific,
}) async {
await connection.send(
UserAgentRequest(
evmGrantCreate: EvmGrantCreateRequest(
clientId: clientId,
shared: SharedSettings(
walletId: walletId,
chainId: chainId,
validFrom: validFrom == null ? null : _toTimestamp(validFrom),
validUntil: validUntil == null ? null : _toTimestamp(validUntil),
maxGasFeePerGas: maxGasFeePerGas,
maxPriorityFeePerGas: maxPriorityFeePerGas,
rateLimit: rateLimit,
),
specific: specific,
),
),
);
final response = await connection.receive();
if (!response.hasEvmGrantCreate()) {
throw Exception(
'Expected EVM grant create response, got ${response.whichPayload()}',
);
}
final result = response.evmGrantCreate;
switch (result.whichResult()) {
case EvmGrantCreateResponse_Result.grantId:
return result.grantId;
case EvmGrantCreateResponse_Result.error:
throw Exception(_describeGrantError(result.error));
case EvmGrantCreateResponse_Result.notSet:
throw Exception('Grant creation returned no result.');
}
}
Future<void> deleteEvmGrant(Connection connection, int grantId) async {
await connection.send(
UserAgentRequest(evmGrantDelete: EvmGrantDeleteRequest(grantId: grantId)),
);
final response = await connection.receive();
if (!response.hasEvmGrantDelete()) {
throw Exception(
'Expected EVM grant delete response, got ${response.whichPayload()}',
);
}
final result = response.evmGrantDelete;
switch (result.whichResult()) {
case EvmGrantDeleteResponse_Result.ok:
return;
case EvmGrantDeleteResponse_Result.error:
throw Exception(_describeGrantError(result.error));
case EvmGrantDeleteResponse_Result.notSet:
throw Exception('Grant revoke returned no result.');
}
}
Timestamp _toTimestamp(DateTime value) {
final utc = value.toUtc();
return Timestamp()
..seconds = Int64(utc.millisecondsSinceEpoch ~/ 1000)
..nanos = (utc.microsecondsSinceEpoch % 1000000) * 1000;
}
String _describeGrantError(EvmError error) {
return switch (error) {
EvmError.EVM_ERROR_VAULT_SEALED =>
'The vault is sealed. Unseal it before using EVM grants.',
EvmError.EVM_ERROR_INTERNAL || EvmError.EVM_ERROR_UNSPECIFIED =>
'The server failed to process the EVM grant request.',
_ => 'The server failed to process the EVM grant request.',
};
}

View File

@@ -0,0 +1,62 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:json_annotation/json_annotation.dart';
part 'server_info_storage.g.dart';
@JsonSerializable()
class StoredServerInfo {
const StoredServerInfo({
required this.address,
required this.port,
required this.caCertFingerprint,
});
final String address;
final int port;
final String caCertFingerprint;
factory StoredServerInfo.fromJson(Map<String, dynamic> json) => _$StoredServerInfoFromJson(json);
Map<String, dynamic> toJson() => _$StoredServerInfoToJson(this);
}
abstract class ServerInfoStorage {
Future<StoredServerInfo?> load();
Future<void> save(StoredServerInfo serverInfo);
Future<void> clear();
}
class SecureServerInfoStorage implements ServerInfoStorage {
static const _storageKey = 'server_info';
const SecureServerInfoStorage();
static const _storage = FlutterSecureStorage();
@override
Future<StoredServerInfo?> load() async {
return null;
final rawValue = await _storage.read(key: _storageKey);
if (rawValue == null) {
return null;
}
final decoded = jsonDecode(rawValue) as Map<String, dynamic>;
return StoredServerInfo.fromJson(decoded);
}
@override
Future<void> save(StoredServerInfo serverInfo) {
return _storage.write(
key: _storageKey,
value: jsonEncode(serverInfo.toJson()),
);
}
@override
Future<void> clear() {
return _storage.delete(key: _storageKey);
}
}

View File

@@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'server_info_storage.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
StoredServerInfo _$StoredServerInfoFromJson(Map<String, dynamic> json) =>
StoredServerInfo(
address: json['address'] as String,
port: (json['port'] as num).toInt(),
caCertFingerprint: json['caCertFingerprint'] as String,
);
Map<String, dynamic> _$StoredServerInfoToJson(StoredServerInfo instance) =>
<String, dynamic>{
'address': instance.address,
'port': instance.port,
'caCertFingerprint': instance.caCertFingerprint,
};

View File

@@ -0,0 +1,107 @@
import 'package:arbiter/features/connection/connection.dart';
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:cryptography/cryptography.dart';
const _vaultKeyAssociatedData = 'arbiter.vault.password';
Future<BootstrapResult> bootstrapVault(
Connection connection,
String password,
) async {
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
await connection.send(
UserAgentRequest(
bootstrapEncryptedKey: BootstrapEncryptedKey(
nonce: encryptedKey.nonce,
ciphertext: encryptedKey.ciphertext,
associatedData: encryptedKey.associatedData,
),
),
);
final response = await connection.receive();
if (!response.hasBootstrapResult()) {
throw Exception(
'Expected bootstrap result, got ${response.whichPayload()}',
);
}
return response.bootstrapResult;
}
Future<UnsealResult> unsealVault(Connection connection, String password) async {
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
await connection.send(
UserAgentRequest(
unsealEncryptedKey: UnsealEncryptedKey(
nonce: encryptedKey.nonce,
ciphertext: encryptedKey.ciphertext,
associatedData: encryptedKey.associatedData,
),
),
);
final response = await connection.receive();
if (!response.hasUnsealResult()) {
throw Exception('Expected unseal result, got ${response.whichPayload()}');
}
return response.unsealResult;
}
Future<_EncryptedVaultKey> _encryptVaultKeyMaterial(
Connection connection,
String password,
) async {
final keyExchange = X25519();
final cipher = Xchacha20.poly1305Aead();
final clientKeyPair = await keyExchange.newKeyPair();
final clientPublicKey = await clientKeyPair.extractPublicKey();
await connection.send(
UserAgentRequest(unsealStart: UnsealStart(clientPubkey: clientPublicKey.bytes)),
);
final handshakeResponse = await connection.receive();
if (!handshakeResponse.hasUnsealStartResponse()) {
throw Exception(
'Expected unseal handshake response, got ${handshakeResponse.whichPayload()}',
);
}
final serverPublicKey = SimplePublicKey(
handshakeResponse.unsealStartResponse.serverPubkey,
type: KeyPairType.x25519,
);
final sharedSecret = await keyExchange.sharedSecretKey(
keyPair: clientKeyPair,
remotePublicKey: serverPublicKey,
);
final secretBox = await cipher.encrypt(
password.codeUnits,
secretKey: sharedSecret,
nonce: cipher.newNonce(),
aad: _vaultKeyAssociatedData.codeUnits,
);
return _EncryptedVaultKey(
nonce: secretBox.nonce,
ciphertext: [...secretBox.cipherText, ...secretBox.mac.bytes],
associatedData: _vaultKeyAssociatedData.codeUnits,
);
}
class _EncryptedVaultKey {
const _EncryptedVaultKey({
required this.nonce,
required this.ciphertext,
required this.associatedData,
});
final List<int> nonce;
final List<int> ciphertext;
final List<int> associatedData;
}

View File

@@ -0,0 +1,16 @@
enum KeyAlgorithm {
rsa, ecdsa, ed25519
}
// The API to handle without storing the private key in memory.
//The implementation will use platform-specific secure storage and signing capabilities.
abstract class KeyHandle {
KeyAlgorithm get alg;
Future<List<int>> sign(List<int> data);
Future<List<int>> getPublicKey();
}
abstract class KeyManager {
Future<KeyHandle?> get();
Future<KeyHandle> create();
}

View File

@@ -0,0 +1,93 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:arbiter/features/identity/pk_manager.dart';
final storage = FlutterSecureStorage(
aOptions: AndroidOptions.biometric(
enforceBiometrics: true,
biometricPromptTitle: 'Authentication Required',
),
mOptions: MacOsOptions(
accessibility: KeychainAccessibility.unlocked_this_device,
label: "Arbiter",
description: "Confirm your identity to access vault",
synchronizable: false,
accessControlFlags: [
AccessControlFlag.userPresence,
],
usesDataProtectionKeychain: true,
),
);
final processor = Ed25519();
class SimpleEd25519 extends KeyHandle {
final SimpleKeyPair _keyPair;
SimpleEd25519({required SimpleKeyPair keyPair}) : _keyPair = keyPair;
@override
KeyAlgorithm get alg => KeyAlgorithm.ed25519;
@override
Future<List<int>> getPublicKey() async {
final publicKey = await _keyPair.extractPublicKey();
return publicKey.bytes;
}
@override
Future<List<int>> sign(List<int> data) async {
final signature = await processor.sign(data, keyPair: _keyPair);
return signature.bytes;
}
}
class SimpleEd25519Manager extends KeyManager {
static const _storageKey = "ed25519_identity";
static const _storagePublicKey = "ed25519_public_key";
@override
Future<KeyHandle> create() async {
final storedKey = await get();
if (storedKey != null) {
return storedKey;
}
final newKey = await processor.newKeyPair();
final rawKey = await newKey.extract();
final keyData = base64Encode(rawKey.bytes);
await storage.write(key: _storageKey, value: keyData);
final publicKeyData = base64Encode(rawKey.publicKey.bytes);
await storage.write(key: _storagePublicKey, value: publicKeyData);
return SimpleEd25519(keyPair: newKey);
}
@override
Future<KeyHandle?> get() async {
final storedKeyPair = await storage.read(key: _storageKey);
if (storedKeyPair == null) {
return null;
}
final publicKeyData = await storage.read(key: _storagePublicKey);
final publicKeyRaw = base64Decode(publicKeyData!);
final publicKey = SimplePublicKey(
publicKeyRaw,
type: processor.keyPairType,
);
final keyBytes = base64Decode(storedKeyPair);
final keypair = SimpleKeyPairData(
keyBytes,
publicKey: publicKey,
type: processor.keyPairType,
);
return SimpleEd25519(keyPair: keypair);
}
}

View File

@@ -1,122 +1,35 @@
import 'package:flutter/material.dart';
import 'package:arbiter/router.dart';
import 'package:flutter/material.dart' hide Router;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sizer/sizer.dart';
void main() {
runApp(const MyApp());
WidgetsFlutterBinding.ensureInitialized();
runApp(const ProviderScope(child: App()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
class App extends StatefulWidget {
const App({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
State<App> createState() => _AppState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
class _AppState extends State<App> {
late final Router _router;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
@override
void initState() {
super.initState();
_router = Router();
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: .center,
children: [
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
return Sizer(
builder: (context, orientation, deviceType) {
return MaterialApp.router(routerConfig: _router.config());
},
);
}
}

View File

@@ -0,0 +1,88 @@
// This is a generated file - do not edit.
//
// Generated from arbiter.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
class ServerInfo extends $pb.GeneratedMessage {
factory ServerInfo({
$core.String? version,
$core.List<$core.int>? certPublicKey,
}) {
final result = create();
if (version != null) result.version = version;
if (certPublicKey != null) result.certPublicKey = certPublicKey;
return result;
}
ServerInfo._();
factory ServerInfo.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ServerInfo.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ServerInfo',
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'version')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'certPublicKey', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ServerInfo clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ServerInfo copyWith(void Function(ServerInfo) updates) =>
super.copyWith((message) => updates(message as ServerInfo)) as ServerInfo;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ServerInfo create() => ServerInfo._();
@$core.override
ServerInfo createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ServerInfo getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ServerInfo>(create);
static ServerInfo? _defaultInstance;
@$pb.TagNumber(1)
$core.String get version => $_getSZ(0);
@$pb.TagNumber(1)
set version($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasVersion() => $_has(0);
@$pb.TagNumber(1)
void clearVersion() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get certPublicKey => $_getN(1);
@$pb.TagNumber(2)
set certPublicKey($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasCertPublicKey() => $_has(1);
@$pb.TagNumber(2)
void clearCertPublicKey() => $_clearField(2);
}
const $core.bool _omitFieldNames =
$core.bool.fromEnvironment('protobuf.omit_field_names');
const $core.bool _omitMessageNames =
$core.bool.fromEnvironment('protobuf.omit_message_names');

View File

@@ -0,0 +1,11 @@
// This is a generated file - do not edit.
//
// Generated from arbiter.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports

View File

@@ -0,0 +1,90 @@
// This is a generated file - do not edit.
//
// Generated from arbiter.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:async' as $async;
import 'dart:core' as $core;
import 'package:grpc/service_api.dart' as $grpc;
import 'package:protobuf/protobuf.dart' as $pb;
import 'client.pb.dart' as $0;
import 'user_agent.pb.dart' as $1;
export 'arbiter.pb.dart';
@$pb.GrpcServiceName('arbiter.ArbiterService')
class ArbiterServiceClient extends $grpc.Client {
/// The hostname for this service.
static const $core.String defaultHost = '';
/// OAuth scopes needed for the client.
static const $core.List<$core.String> oauthScopes = [
'',
];
ArbiterServiceClient(super.channel, {super.options, super.interceptors});
$grpc.ResponseStream<$0.ClientResponse> client(
$async.Stream<$0.ClientRequest> request, {
$grpc.CallOptions? options,
}) {
return $createStreamingCall(_$client, request, options: options);
}
$grpc.ResponseStream<$1.UserAgentResponse> userAgent(
$async.Stream<$1.UserAgentRequest> request, {
$grpc.CallOptions? options,
}) {
return $createStreamingCall(_$userAgent, request, options: options);
}
// method descriptors
static final _$client =
$grpc.ClientMethod<$0.ClientRequest, $0.ClientResponse>(
'/arbiter.ArbiterService/Client',
($0.ClientRequest value) => value.writeToBuffer(),
$0.ClientResponse.fromBuffer);
static final _$userAgent =
$grpc.ClientMethod<$1.UserAgentRequest, $1.UserAgentResponse>(
'/arbiter.ArbiterService/UserAgent',
($1.UserAgentRequest value) => value.writeToBuffer(),
$1.UserAgentResponse.fromBuffer);
}
@$pb.GrpcServiceName('arbiter.ArbiterService')
abstract class ArbiterServiceBase extends $grpc.Service {
$core.String get $name => 'arbiter.ArbiterService';
ArbiterServiceBase() {
$addMethod($grpc.ServiceMethod<$0.ClientRequest, $0.ClientResponse>(
'Client',
client,
true,
true,
($core.List<$core.int> value) => $0.ClientRequest.fromBuffer(value),
($0.ClientResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$1.UserAgentRequest, $1.UserAgentResponse>(
'UserAgent',
userAgent,
true,
true,
($core.List<$core.int> value) => $1.UserAgentRequest.fromBuffer(value),
($1.UserAgentResponse value) => value.writeToBuffer()));
}
$async.Stream<$0.ClientResponse> client(
$grpc.ServiceCall call, $async.Stream<$0.ClientRequest> request);
$async.Stream<$1.UserAgentResponse> userAgent(
$grpc.ServiceCall call, $async.Stream<$1.UserAgentRequest> request);
}

View File

@@ -0,0 +1,30 @@
// This is a generated file - do not edit.
//
// Generated from arbiter.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
// ignore_for_file: unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use serverInfoDescriptor instead')
const ServerInfo$json = {
'1': 'ServerInfo',
'2': [
{'1': 'version', '3': 1, '4': 1, '5': 9, '10': 'version'},
{'1': 'cert_public_key', '3': 2, '4': 1, '5': 12, '10': 'certPublicKey'},
],
};
/// Descriptor for `ServerInfo`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List serverInfoDescriptor = $convert.base64Decode(
'CgpTZXJ2ZXJJbmZvEhgKB3ZlcnNpb24YASABKAlSB3ZlcnNpb24SJgoPY2VydF9wdWJsaWNfa2'
'V5GAIgASgMUg1jZXJ0UHVibGljS2V5');

View File

@@ -0,0 +1,551 @@
// This is a generated file - do not edit.
//
// Generated from client.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
import 'client.pbenum.dart';
import 'evm.pb.dart' as $0;
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
export 'client.pbenum.dart';
class AuthChallengeRequest extends $pb.GeneratedMessage {
factory AuthChallengeRequest({
$core.List<$core.int>? pubkey,
}) {
final result = create();
if (pubkey != null) result.pubkey = pubkey;
return result;
}
AuthChallengeRequest._();
factory AuthChallengeRequest.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory AuthChallengeRequest.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'AuthChallengeRequest',
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthChallengeRequest clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthChallengeRequest copyWith(void Function(AuthChallengeRequest) updates) =>
super.copyWith((message) => updates(message as AuthChallengeRequest))
as AuthChallengeRequest;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static AuthChallengeRequest create() => AuthChallengeRequest._();
@$core.override
AuthChallengeRequest createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static AuthChallengeRequest getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<AuthChallengeRequest>(create);
static AuthChallengeRequest? _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);
}
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;
}
AuthChallenge._();
factory AuthChallenge.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory AuthChallenge.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'AuthChallenge',
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY)
..aI(2, _omitFieldNames ? '' : 'nonce')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthChallenge clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthChallenge copyWith(void Function(AuthChallenge) updates) =>
super.copyWith((message) => updates(message as AuthChallenge))
as AuthChallenge;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static AuthChallenge create() => AuthChallenge._();
@$core.override
AuthChallenge createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static AuthChallenge getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<AuthChallenge>(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);
@$pb.TagNumber(2)
set nonce($core.int value) => $_setSignedInt32(1, value);
@$pb.TagNumber(2)
$core.bool hasNonce() => $_has(1);
@$pb.TagNumber(2)
void clearNonce() => $_clearField(2);
}
class AuthChallengeSolution extends $pb.GeneratedMessage {
factory AuthChallengeSolution({
$core.List<$core.int>? signature,
}) {
final result = create();
if (signature != null) result.signature = signature;
return result;
}
AuthChallengeSolution._();
factory AuthChallengeSolution.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory AuthChallengeSolution.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'AuthChallengeSolution',
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'signature', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthChallengeSolution clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthChallengeSolution copyWith(
void Function(AuthChallengeSolution) updates) =>
super.copyWith((message) => updates(message as AuthChallengeSolution))
as AuthChallengeSolution;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static AuthChallengeSolution create() => AuthChallengeSolution._();
@$core.override
AuthChallengeSolution createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static AuthChallengeSolution getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<AuthChallengeSolution>(create);
static AuthChallengeSolution? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get signature => $_getN(0);
@$pb.TagNumber(1)
set signature($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1)
$core.bool hasSignature() => $_has(0);
@$pb.TagNumber(1)
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<AuthOk>(create);
static AuthOk? _defaultInstance;
}
enum ClientRequest_Payload {
authChallengeRequest,
authChallengeSolution,
notSet
}
class ClientRequest extends $pb.GeneratedMessage {
factory ClientRequest({
AuthChallengeRequest? authChallengeRequest,
AuthChallengeSolution? authChallengeSolution,
}) {
final result = create();
if (authChallengeRequest != null)
result.authChallengeRequest = authChallengeRequest;
if (authChallengeSolution != null)
result.authChallengeSolution = authChallengeSolution;
return result;
}
ClientRequest._();
factory ClientRequest.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ClientRequest.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static const $core.Map<$core.int, ClientRequest_Payload>
_ClientRequest_PayloadByTag = {
1: ClientRequest_Payload.authChallengeRequest,
2: ClientRequest_Payload.authChallengeSolution,
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])
..aOM<AuthChallengeRequest>(
1, _omitFieldNames ? '' : 'authChallengeRequest',
subBuilder: AuthChallengeRequest.create)
..aOM<AuthChallengeSolution>(
2, _omitFieldNames ? '' : 'authChallengeSolution',
subBuilder: AuthChallengeSolution.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ClientRequest clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ClientRequest copyWith(void Function(ClientRequest) updates) =>
super.copyWith((message) => updates(message as ClientRequest))
as ClientRequest;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ClientRequest create() => ClientRequest._();
@$core.override
ClientRequest createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ClientRequest getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ClientRequest>(create);
static ClientRequest? _defaultInstance;
@$pb.TagNumber(1)
@$pb.TagNumber(2)
ClientRequest_Payload whichPayload() =>
_ClientRequest_PayloadByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1)
@$pb.TagNumber(2)
void clearPayload() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1)
AuthChallengeRequest get authChallengeRequest => $_getN(0);
@$pb.TagNumber(1)
set authChallengeRequest(AuthChallengeRequest value) => $_setField(1, value);
@$pb.TagNumber(1)
$core.bool hasAuthChallengeRequest() => $_has(0);
@$pb.TagNumber(1)
void clearAuthChallengeRequest() => $_clearField(1);
@$pb.TagNumber(1)
AuthChallengeRequest ensureAuthChallengeRequest() => $_ensure(0);
@$pb.TagNumber(2)
AuthChallengeSolution get authChallengeSolution => $_getN(1);
@$pb.TagNumber(2)
set authChallengeSolution(AuthChallengeSolution value) =>
$_setField(2, value);
@$pb.TagNumber(2)
$core.bool hasAuthChallengeSolution() => $_has(1);
@$pb.TagNumber(2)
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;
}
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<ClientConnectError_Code>(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<ClientConnectError>(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);
}
enum ClientResponse_Payload {
authChallenge,
authOk,
evmSignTransaction,
evmAnalyzeTransaction,
clientConnectError,
notSet
}
class ClientResponse extends $pb.GeneratedMessage {
factory ClientResponse({
AuthChallenge? authChallenge,
AuthOk? authOk,
$0.EvmSignTransactionResponse? evmSignTransaction,
$0.EvmAnalyzeTransactionResponse? evmAnalyzeTransaction,
ClientConnectError? clientConnectError,
}) {
final result = create();
if (authChallenge != null) result.authChallenge = authChallenge;
if (authOk != null) result.authOk = authOk;
if (evmSignTransaction != null)
result.evmSignTransaction = evmSignTransaction;
if (evmAnalyzeTransaction != null)
result.evmAnalyzeTransaction = evmAnalyzeTransaction;
if (clientConnectError != null)
result.clientConnectError = clientConnectError;
return result;
}
ClientResponse._();
factory ClientResponse.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ClientResponse.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static const $core.Map<$core.int, ClientResponse_Payload>
_ClientResponse_PayloadByTag = {
1: ClientResponse_Payload.authChallenge,
2: ClientResponse_Payload.authOk,
3: ClientResponse_Payload.evmSignTransaction,
4: ClientResponse_Payload.evmAnalyzeTransaction,
5: ClientResponse_Payload.clientConnectError,
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])
..aOM<AuthChallenge>(1, _omitFieldNames ? '' : 'authChallenge',
subBuilder: AuthChallenge.create)
..aOM<AuthOk>(2, _omitFieldNames ? '' : 'authOk', subBuilder: AuthOk.create)
..aOM<$0.EvmSignTransactionResponse>(
3, _omitFieldNames ? '' : 'evmSignTransaction',
subBuilder: $0.EvmSignTransactionResponse.create)
..aOM<$0.EvmAnalyzeTransactionResponse>(
4, _omitFieldNames ? '' : 'evmAnalyzeTransaction',
subBuilder: $0.EvmAnalyzeTransactionResponse.create)
..aOM<ClientConnectError>(5, _omitFieldNames ? '' : 'clientConnectError',
subBuilder: ClientConnectError.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ClientResponse clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ClientResponse copyWith(void Function(ClientResponse) updates) =>
super.copyWith((message) => updates(message as ClientResponse))
as ClientResponse;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ClientResponse create() => ClientResponse._();
@$core.override
ClientResponse createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ClientResponse getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ClientResponse>(create);
static ClientResponse? _defaultInstance;
@$pb.TagNumber(1)
@$pb.TagNumber(2)
@$pb.TagNumber(3)
@$pb.TagNumber(4)
@$pb.TagNumber(5)
ClientResponse_Payload whichPayload() =>
_ClientResponse_PayloadByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1)
@$pb.TagNumber(2)
@$pb.TagNumber(3)
@$pb.TagNumber(4)
@$pb.TagNumber(5)
void clearPayload() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1)
AuthChallenge get authChallenge => $_getN(0);
@$pb.TagNumber(1)
set authChallenge(AuthChallenge value) => $_setField(1, value);
@$pb.TagNumber(1)
$core.bool hasAuthChallenge() => $_has(0);
@$pb.TagNumber(1)
void clearAuthChallenge() => $_clearField(1);
@$pb.TagNumber(1)
AuthChallenge ensureAuthChallenge() => $_ensure(0);
@$pb.TagNumber(2)
AuthOk get authOk => $_getN(1);
@$pb.TagNumber(2)
set authOk(AuthOk value) => $_setField(2, value);
@$pb.TagNumber(2)
$core.bool hasAuthOk() => $_has(1);
@$pb.TagNumber(2)
void clearAuthOk() => $_clearField(2);
@$pb.TagNumber(2)
AuthOk ensureAuthOk() => $_ensure(1);
@$pb.TagNumber(3)
$0.EvmSignTransactionResponse get evmSignTransaction => $_getN(2);
@$pb.TagNumber(3)
set evmSignTransaction($0.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);
@$pb.TagNumber(4)
$0.EvmAnalyzeTransactionResponse get evmAnalyzeTransaction => $_getN(3);
@$pb.TagNumber(4)
set evmAnalyzeTransaction($0.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);
@$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);
}
const $core.bool _omitFieldNames =
$core.bool.fromEnvironment('protobuf.omit_field_names');
const $core.bool _omitMessageNames =
$core.bool.fromEnvironment('protobuf.omit_message_names');

View File

@@ -0,0 +1,42 @@
// This is a generated file - do not edit.
//
// Generated from client.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
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');
static const $core.List<ClientConnectError_Code> values =
<ClientConnectError_Code>[
UNKNOWN,
APPROVAL_DENIED,
NO_USER_AGENTS_ONLINE,
];
static final $core.List<ClientConnectError_Code?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 2);
static ClientConnectError_Code? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const ClientConnectError_Code._(super.value, super.name);
}
const $core.bool _omitEnumNames =
$core.bool.fromEnvironment('protobuf.omit_enum_names');

View File

@@ -0,0 +1,197 @@
// This is a generated file - do not edit.
//
// Generated from client.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
// ignore_for_file: unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use authChallengeRequestDescriptor instead')
const AuthChallengeRequest$json = {
'1': 'AuthChallengeRequest',
'2': [
{'1': 'pubkey', '3': 1, '4': 1, '5': 12, '10': 'pubkey'},
],
};
/// Descriptor for `AuthChallengeRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List authChallengeRequestDescriptor =
$convert.base64Decode(
'ChRBdXRoQ2hhbGxlbmdlUmVxdWVzdBIWCgZwdWJrZXkYASABKAxSBnB1YmtleQ==');
@$core.Deprecated('Use authChallengeDescriptor instead')
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'},
],
};
/// Descriptor for `AuthChallenge`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List authChallengeDescriptor = $convert.base64Decode(
'Cg1BdXRoQ2hhbGxlbmdlEhYKBnB1YmtleRgBIAEoDFIGcHVia2V5EhQKBW5vbmNlGAIgASgFUg'
'Vub25jZQ==');
@$core.Deprecated('Use authChallengeSolutionDescriptor instead')
const AuthChallengeSolution$json = {
'1': 'AuthChallengeSolution',
'2': [
{'1': 'signature', '3': 1, '4': 1, '5': 12, '10': 'signature'},
],
};
/// Descriptor for `AuthChallengeSolution`. Decode as a `google.protobuf.DescriptorProto`.
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': 'auth_challenge_request',
'3': 1,
'4': 1,
'5': 11,
'6': '.arbiter.client.AuthChallengeRequest',
'9': 0,
'10': 'authChallengeRequest'
},
{
'1': 'auth_challenge_solution',
'3': 2,
'4': 1,
'5': 11,
'6': '.arbiter.client.AuthChallengeSolution',
'9': 0,
'10': 'authChallengeSolution'
},
],
'8': [
{'1': 'payload'},
],
};
/// 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');
@$core.Deprecated('Use clientResponseDescriptor instead')
const ClientResponse$json = {
'1': 'ClientResponse',
'2': [
{
'1': 'auth_challenge',
'3': 1,
'4': 1,
'5': 11,
'6': '.arbiter.client.AuthChallenge',
'9': 0,
'10': 'authChallenge'
},
{
'1': 'auth_ok',
'3': 2,
'4': 1,
'5': 11,
'6': '.arbiter.client.AuthOk',
'9': 0,
'10': 'authOk'
},
{
'1': 'client_connect_error',
'3': 5,
'4': 1,
'5': 11,
'6': '.arbiter.client.ClientConnectError',
'9': 0,
'10': 'clientConnectError'
},
{
'1': 'evm_sign_transaction',
'3': 3,
'4': 1,
'5': 11,
'6': '.arbiter.evm.EvmSignTransactionResponse',
'9': 0,
'10': 'evmSignTransaction'
},
{
'1': 'evm_analyze_transaction',
'3': 4,
'4': 1,
'5': 11,
'6': '.arbiter.evm.EvmAnalyzeTransactionResponse',
'9': 0,
'10': 'evmAnalyzeTransaction'
},
],
'8': [
{'1': 'payload'},
],
};
/// Descriptor for `ClientResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List clientResponseDescriptor = $convert.base64Decode(
'Cg5DbGllbnRSZXNwb25zZRJGCg5hdXRoX2NoYWxsZW5nZRgBIAEoCzIdLmFyYml0ZXIuY2xpZW'
'50LkF1dGhDaGFsbGVuZ2VIAFINYXV0aENoYWxsZW5nZRIxCgdhdXRoX29rGAIgASgLMhYuYXJi'
'aXRlci5jbGllbnQuQXV0aE9rSABSBmF1dGhPaxJWChRjbGllbnRfY29ubmVjdF9lcnJvchgFIA'
'EoCzIiLmFyYml0ZXIuY2xpZW50LkNsaWVudENvbm5lY3RFcnJvckgAUhJjbGllbnRDb25uZWN0'
'RXJyb3ISWwoUZXZtX3NpZ25fdHJhbnNhY3Rpb24YAyABKAsyJy5hcmJpdGVyLmV2bS5Fdm1TaW'
'duVHJhbnNhY3Rpb25SZXNwb25zZUgAUhJldm1TaWduVHJhbnNhY3Rpb24SZAoXZXZtX2FuYWx5'
'emVfdHJhbnNhY3Rpb24YBCABKAsyKi5hcmJpdGVyLmV2bS5Fdm1BbmFseXplVHJhbnNhY3Rpb2'
'5SZXNwb25zZUgAUhVldm1BbmFseXplVHJhbnNhY3Rpb25CCQoHcGF5bG9hZA==');

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
// This is a generated file - do not edit.
//
// Generated from evm.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class EvmError extends $pb.ProtobufEnum {
static const EvmError EVM_ERROR_UNSPECIFIED =
EvmError._(0, _omitEnumNames ? '' : 'EVM_ERROR_UNSPECIFIED');
static const EvmError EVM_ERROR_VAULT_SEALED =
EvmError._(1, _omitEnumNames ? '' : 'EVM_ERROR_VAULT_SEALED');
static const EvmError EVM_ERROR_INTERNAL =
EvmError._(2, _omitEnumNames ? '' : 'EVM_ERROR_INTERNAL');
static const $core.List<EvmError> values = <EvmError>[
EVM_ERROR_UNSPECIFIED,
EVM_ERROR_VAULT_SEALED,
EVM_ERROR_INTERNAL,
];
static final $core.List<EvmError?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 2);
static EvmError? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const EvmError._(super.value, super.name);
}
const $core.bool _omitEnumNames =
$core.bool.fromEnvironment('protobuf.omit_enum_names');

View File

@@ -0,0 +1,950 @@
// This is a generated file - do not edit.
//
// Generated from evm.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
// ignore_for_file: unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use evmErrorDescriptor instead')
const EvmError$json = {
'1': 'EvmError',
'2': [
{'1': 'EVM_ERROR_UNSPECIFIED', '2': 0},
{'1': 'EVM_ERROR_VAULT_SEALED', '2': 1},
{'1': 'EVM_ERROR_INTERNAL', '2': 2},
],
};
/// Descriptor for `EvmError`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List evmErrorDescriptor = $convert.base64Decode(
'CghFdm1FcnJvchIZChVFVk1fRVJST1JfVU5TUEVDSUZJRUQQABIaChZFVk1fRVJST1JfVkFVTF'
'RfU0VBTEVEEAESFgoSRVZNX0VSUk9SX0lOVEVSTkFMEAI=');
@$core.Deprecated('Use walletEntryDescriptor instead')
const WalletEntry$json = {
'1': 'WalletEntry',
'2': [
{'1': 'address', '3': 1, '4': 1, '5': 12, '10': 'address'},
],
};
/// Descriptor for `WalletEntry`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List walletEntryDescriptor = $convert
.base64Decode('CgtXYWxsZXRFbnRyeRIYCgdhZGRyZXNzGAEgASgMUgdhZGRyZXNz');
@$core.Deprecated('Use walletListDescriptor instead')
const WalletList$json = {
'1': 'WalletList',
'2': [
{
'1': 'wallets',
'3': 1,
'4': 3,
'5': 11,
'6': '.arbiter.evm.WalletEntry',
'10': 'wallets'
},
],
};
/// Descriptor for `WalletList`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List walletListDescriptor = $convert.base64Decode(
'CgpXYWxsZXRMaXN0EjIKB3dhbGxldHMYASADKAsyGC5hcmJpdGVyLmV2bS5XYWxsZXRFbnRyeV'
'IHd2FsbGV0cw==');
@$core.Deprecated('Use walletCreateResponseDescriptor instead')
const WalletCreateResponse$json = {
'1': 'WalletCreateResponse',
'2': [
{
'1': 'wallet',
'3': 1,
'4': 1,
'5': 11,
'6': '.arbiter.evm.WalletEntry',
'9': 0,
'10': 'wallet'
},
{
'1': 'error',
'3': 2,
'4': 1,
'5': 14,
'6': '.arbiter.evm.EvmError',
'9': 0,
'10': 'error'
},
],
'8': [
{'1': 'result'},
],
};
/// Descriptor for `WalletCreateResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List walletCreateResponseDescriptor = $convert.base64Decode(
'ChRXYWxsZXRDcmVhdGVSZXNwb25zZRIyCgZ3YWxsZXQYASABKAsyGC5hcmJpdGVyLmV2bS5XYW'
'xsZXRFbnRyeUgAUgZ3YWxsZXQSLQoFZXJyb3IYAiABKA4yFS5hcmJpdGVyLmV2bS5Fdm1FcnJv'
'ckgAUgVlcnJvckIICgZyZXN1bHQ=');
@$core.Deprecated('Use walletListResponseDescriptor instead')
const WalletListResponse$json = {
'1': 'WalletListResponse',
'2': [
{
'1': 'wallets',
'3': 1,
'4': 1,
'5': 11,
'6': '.arbiter.evm.WalletList',
'9': 0,
'10': 'wallets'
},
{
'1': 'error',
'3': 2,
'4': 1,
'5': 14,
'6': '.arbiter.evm.EvmError',
'9': 0,
'10': 'error'
},
],
'8': [
{'1': 'result'},
],
};
/// Descriptor for `WalletListResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List walletListResponseDescriptor = $convert.base64Decode(
'ChJXYWxsZXRMaXN0UmVzcG9uc2USMwoHd2FsbGV0cxgBIAEoCzIXLmFyYml0ZXIuZXZtLldhbG'
'xldExpc3RIAFIHd2FsbGV0cxItCgVlcnJvchgCIAEoDjIVLmFyYml0ZXIuZXZtLkV2bUVycm9y'
'SABSBWVycm9yQggKBnJlc3VsdA==');
@$core.Deprecated('Use transactionRateLimitDescriptor instead')
const TransactionRateLimit$json = {
'1': 'TransactionRateLimit',
'2': [
{'1': 'count', '3': 1, '4': 1, '5': 13, '10': 'count'},
{'1': 'window_secs', '3': 2, '4': 1, '5': 3, '10': 'windowSecs'},
],
};
/// Descriptor for `TransactionRateLimit`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List transactionRateLimitDescriptor = $convert.base64Decode(
'ChRUcmFuc2FjdGlvblJhdGVMaW1pdBIUCgVjb3VudBgBIAEoDVIFY291bnQSHwoLd2luZG93X3'
'NlY3MYAiABKANSCndpbmRvd1NlY3M=');
@$core.Deprecated('Use volumeRateLimitDescriptor instead')
const VolumeRateLimit$json = {
'1': 'VolumeRateLimit',
'2': [
{'1': 'max_volume', '3': 1, '4': 1, '5': 12, '10': 'maxVolume'},
{'1': 'window_secs', '3': 2, '4': 1, '5': 3, '10': 'windowSecs'},
],
};
/// Descriptor for `VolumeRateLimit`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List volumeRateLimitDescriptor = $convert.base64Decode(
'Cg9Wb2x1bWVSYXRlTGltaXQSHQoKbWF4X3ZvbHVtZRgBIAEoDFIJbWF4Vm9sdW1lEh8KC3dpbm'
'Rvd19zZWNzGAIgASgDUgp3aW5kb3dTZWNz');
@$core.Deprecated('Use sharedSettingsDescriptor instead')
const SharedSettings$json = {
'1': 'SharedSettings',
'2': [
{'1': 'wallet_id', '3': 1, '4': 1, '5': 5, '10': 'walletId'},
{'1': 'chain_id', '3': 2, '4': 1, '5': 4, '10': 'chainId'},
{
'1': 'valid_from',
'3': 3,
'4': 1,
'5': 11,
'6': '.google.protobuf.Timestamp',
'9': 0,
'10': 'validFrom',
'17': true
},
{
'1': 'valid_until',
'3': 4,
'4': 1,
'5': 11,
'6': '.google.protobuf.Timestamp',
'9': 1,
'10': 'validUntil',
'17': true
},
{
'1': 'max_gas_fee_per_gas',
'3': 5,
'4': 1,
'5': 12,
'9': 2,
'10': 'maxGasFeePerGas',
'17': true
},
{
'1': 'max_priority_fee_per_gas',
'3': 6,
'4': 1,
'5': 12,
'9': 3,
'10': 'maxPriorityFeePerGas',
'17': true
},
{
'1': 'rate_limit',
'3': 7,
'4': 1,
'5': 11,
'6': '.arbiter.evm.TransactionRateLimit',
'9': 4,
'10': 'rateLimit',
'17': true
},
],
'8': [
{'1': '_valid_from'},
{'1': '_valid_until'},
{'1': '_max_gas_fee_per_gas'},
{'1': '_max_priority_fee_per_gas'},
{'1': '_rate_limit'},
],
};
/// Descriptor for `SharedSettings`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sharedSettingsDescriptor = $convert.base64Decode(
'Cg5TaGFyZWRTZXR0aW5ncxIbCgl3YWxsZXRfaWQYASABKAVSCHdhbGxldElkEhkKCGNoYWluX2'
'lkGAIgASgEUgdjaGFpbklkEj4KCnZhbGlkX2Zyb20YAyABKAsyGi5nb29nbGUucHJvdG9idWYu'
'VGltZXN0YW1wSABSCXZhbGlkRnJvbYgBARJACgt2YWxpZF91bnRpbBgEIAEoCzIaLmdvb2dsZS'
'5wcm90b2J1Zi5UaW1lc3RhbXBIAVIKdmFsaWRVbnRpbIgBARIxChNtYXhfZ2FzX2ZlZV9wZXJf'
'Z2FzGAUgASgMSAJSD21heEdhc0ZlZVBlckdhc4gBARI7ChhtYXhfcHJpb3JpdHlfZmVlX3Blcl'
'9nYXMYBiABKAxIA1IUbWF4UHJpb3JpdHlGZWVQZXJHYXOIAQESRQoKcmF0ZV9saW1pdBgHIAEo'
'CzIhLmFyYml0ZXIuZXZtLlRyYW5zYWN0aW9uUmF0ZUxpbWl0SARSCXJhdGVMaW1pdIgBAUINCg'
'tfdmFsaWRfZnJvbUIOCgxfdmFsaWRfdW50aWxCFgoUX21heF9nYXNfZmVlX3Blcl9nYXNCGwoZ'
'X21heF9wcmlvcml0eV9mZWVfcGVyX2dhc0INCgtfcmF0ZV9saW1pdA==');
@$core.Deprecated('Use etherTransferSettingsDescriptor instead')
const EtherTransferSettings$json = {
'1': 'EtherTransferSettings',
'2': [
{'1': 'targets', '3': 1, '4': 3, '5': 12, '10': 'targets'},
{
'1': 'limit',
'3': 2,
'4': 1,
'5': 11,
'6': '.arbiter.evm.VolumeRateLimit',
'10': 'limit'
},
],
};
/// Descriptor for `EtherTransferSettings`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List etherTransferSettingsDescriptor = $convert.base64Decode(
'ChVFdGhlclRyYW5zZmVyU2V0dGluZ3MSGAoHdGFyZ2V0cxgBIAMoDFIHdGFyZ2V0cxIyCgVsaW'
'1pdBgCIAEoCzIcLmFyYml0ZXIuZXZtLlZvbHVtZVJhdGVMaW1pdFIFbGltaXQ=');
@$core.Deprecated('Use tokenTransferSettingsDescriptor instead')
const TokenTransferSettings$json = {
'1': 'TokenTransferSettings',
'2': [
{'1': 'token_contract', '3': 1, '4': 1, '5': 12, '10': 'tokenContract'},
{
'1': 'target',
'3': 2,
'4': 1,
'5': 12,
'9': 0,
'10': 'target',
'17': true
},
{
'1': 'volume_limits',
'3': 3,
'4': 3,
'5': 11,
'6': '.arbiter.evm.VolumeRateLimit',
'10': 'volumeLimits'
},
],
'8': [
{'1': '_target'},
],
};
/// Descriptor for `TokenTransferSettings`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List tokenTransferSettingsDescriptor = $convert.base64Decode(
'ChVUb2tlblRyYW5zZmVyU2V0dGluZ3MSJQoOdG9rZW5fY29udHJhY3QYASABKAxSDXRva2VuQ2'
'9udHJhY3QSGwoGdGFyZ2V0GAIgASgMSABSBnRhcmdldIgBARJBCg12b2x1bWVfbGltaXRzGAMg'
'AygLMhwuYXJiaXRlci5ldm0uVm9sdW1lUmF0ZUxpbWl0Ugx2b2x1bWVMaW1pdHNCCQoHX3Rhcm'
'dldA==');
@$core.Deprecated('Use specificGrantDescriptor instead')
const SpecificGrant$json = {
'1': 'SpecificGrant',
'2': [
{
'1': 'ether_transfer',
'3': 1,
'4': 1,
'5': 11,
'6': '.arbiter.evm.EtherTransferSettings',
'9': 0,
'10': 'etherTransfer'
},
{
'1': 'token_transfer',
'3': 2,
'4': 1,
'5': 11,
'6': '.arbiter.evm.TokenTransferSettings',
'9': 0,
'10': 'tokenTransfer'
},
],
'8': [
{'1': 'grant'},
],
};
/// Descriptor for `SpecificGrant`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List specificGrantDescriptor = $convert.base64Decode(
'Cg1TcGVjaWZpY0dyYW50EksKDmV0aGVyX3RyYW5zZmVyGAEgASgLMiIuYXJiaXRlci5ldm0uRX'
'RoZXJUcmFuc2ZlclNldHRpbmdzSABSDWV0aGVyVHJhbnNmZXISSwoOdG9rZW5fdHJhbnNmZXIY'
'AiABKAsyIi5hcmJpdGVyLmV2bS5Ub2tlblRyYW5zZmVyU2V0dGluZ3NIAFINdG9rZW5UcmFuc2'
'ZlckIHCgVncmFudA==');
@$core.Deprecated('Use etherTransferMeaningDescriptor instead')
const EtherTransferMeaning$json = {
'1': 'EtherTransferMeaning',
'2': [
{'1': 'to', '3': 1, '4': 1, '5': 12, '10': 'to'},
{'1': 'value', '3': 2, '4': 1, '5': 12, '10': 'value'},
],
};
/// Descriptor for `EtherTransferMeaning`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List etherTransferMeaningDescriptor = $convert.base64Decode(
'ChRFdGhlclRyYW5zZmVyTWVhbmluZxIOCgJ0bxgBIAEoDFICdG8SFAoFdmFsdWUYAiABKAxSBX'
'ZhbHVl');
@$core.Deprecated('Use tokenInfoDescriptor instead')
const TokenInfo$json = {
'1': 'TokenInfo',
'2': [
{'1': 'symbol', '3': 1, '4': 1, '5': 9, '10': 'symbol'},
{'1': 'address', '3': 2, '4': 1, '5': 12, '10': 'address'},
{'1': 'chain_id', '3': 3, '4': 1, '5': 4, '10': 'chainId'},
],
};
/// Descriptor for `TokenInfo`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List tokenInfoDescriptor = $convert.base64Decode(
'CglUb2tlbkluZm8SFgoGc3ltYm9sGAEgASgJUgZzeW1ib2wSGAoHYWRkcmVzcxgCIAEoDFIHYW'
'RkcmVzcxIZCghjaGFpbl9pZBgDIAEoBFIHY2hhaW5JZA==');
@$core.Deprecated('Use tokenTransferMeaningDescriptor instead')
const TokenTransferMeaning$json = {
'1': 'TokenTransferMeaning',
'2': [
{
'1': 'token',
'3': 1,
'4': 1,
'5': 11,
'6': '.arbiter.evm.TokenInfo',
'10': 'token'
},
{'1': 'to', '3': 2, '4': 1, '5': 12, '10': 'to'},
{'1': 'value', '3': 3, '4': 1, '5': 12, '10': 'value'},
],
};
/// Descriptor for `TokenTransferMeaning`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List tokenTransferMeaningDescriptor = $convert.base64Decode(
'ChRUb2tlblRyYW5zZmVyTWVhbmluZxIsCgV0b2tlbhgBIAEoCzIWLmFyYml0ZXIuZXZtLlRva2'
'VuSW5mb1IFdG9rZW4SDgoCdG8YAiABKAxSAnRvEhQKBXZhbHVlGAMgASgMUgV2YWx1ZQ==');
@$core.Deprecated('Use specificMeaningDescriptor instead')
const SpecificMeaning$json = {
'1': 'SpecificMeaning',
'2': [
{
'1': 'ether_transfer',
'3': 1,
'4': 1,
'5': 11,
'6': '.arbiter.evm.EtherTransferMeaning',
'9': 0,
'10': 'etherTransfer'
},
{
'1': 'token_transfer',
'3': 2,
'4': 1,
'5': 11,
'6': '.arbiter.evm.TokenTransferMeaning',
'9': 0,
'10': 'tokenTransfer'
},
],
'8': [
{'1': 'meaning'},
],
};
/// Descriptor for `SpecificMeaning`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List specificMeaningDescriptor = $convert.base64Decode(
'Cg9TcGVjaWZpY01lYW5pbmcSSgoOZXRoZXJfdHJhbnNmZXIYASABKAsyIS5hcmJpdGVyLmV2bS'
'5FdGhlclRyYW5zZmVyTWVhbmluZ0gAUg1ldGhlclRyYW5zZmVyEkoKDnRva2VuX3RyYW5zZmVy'
'GAIgASgLMiEuYXJiaXRlci5ldm0uVG9rZW5UcmFuc2Zlck1lYW5pbmdIAFINdG9rZW5UcmFuc2'
'ZlckIJCgdtZWFuaW5n');
@$core.Deprecated('Use gasLimitExceededViolationDescriptor instead')
const GasLimitExceededViolation$json = {
'1': 'GasLimitExceededViolation',
'2': [
{
'1': 'max_gas_fee_per_gas',
'3': 1,
'4': 1,
'5': 12,
'9': 0,
'10': 'maxGasFeePerGas',
'17': true
},
{
'1': 'max_priority_fee_per_gas',
'3': 2,
'4': 1,
'5': 12,
'9': 1,
'10': 'maxPriorityFeePerGas',
'17': true
},
],
'8': [
{'1': '_max_gas_fee_per_gas'},
{'1': '_max_priority_fee_per_gas'},
],
};
/// Descriptor for `GasLimitExceededViolation`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List gasLimitExceededViolationDescriptor = $convert.base64Decode(
'ChlHYXNMaW1pdEV4Y2VlZGVkVmlvbGF0aW9uEjEKE21heF9nYXNfZmVlX3Blcl9nYXMYASABKA'
'xIAFIPbWF4R2FzRmVlUGVyR2FziAEBEjsKGG1heF9wcmlvcml0eV9mZWVfcGVyX2dhcxgCIAEo'
'DEgBUhRtYXhQcmlvcml0eUZlZVBlckdhc4gBAUIWChRfbWF4X2dhc19mZWVfcGVyX2dhc0IbCh'
'lfbWF4X3ByaW9yaXR5X2ZlZV9wZXJfZ2Fz');
@$core.Deprecated('Use evalViolationDescriptor instead')
const EvalViolation$json = {
'1': 'EvalViolation',
'2': [
{
'1': 'invalid_target',
'3': 1,
'4': 1,
'5': 12,
'9': 0,
'10': 'invalidTarget'
},
{
'1': 'gas_limit_exceeded',
'3': 2,
'4': 1,
'5': 11,
'6': '.arbiter.evm.GasLimitExceededViolation',
'9': 0,
'10': 'gasLimitExceeded'
},
{
'1': 'rate_limit_exceeded',
'3': 3,
'4': 1,
'5': 11,
'6': '.google.protobuf.Empty',
'9': 0,
'10': 'rateLimitExceeded'
},
{
'1': 'volumetric_limit_exceeded',
'3': 4,
'4': 1,
'5': 11,
'6': '.google.protobuf.Empty',
'9': 0,
'10': 'volumetricLimitExceeded'
},
{
'1': 'invalid_time',
'3': 5,
'4': 1,
'5': 11,
'6': '.google.protobuf.Empty',
'9': 0,
'10': 'invalidTime'
},
{
'1': 'invalid_transaction_type',
'3': 6,
'4': 1,
'5': 11,
'6': '.google.protobuf.Empty',
'9': 0,
'10': 'invalidTransactionType'
},
],
'8': [
{'1': 'kind'},
],
};
/// Descriptor for `EvalViolation`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List evalViolationDescriptor = $convert.base64Decode(
'Cg1FdmFsVmlvbGF0aW9uEicKDmludmFsaWRfdGFyZ2V0GAEgASgMSABSDWludmFsaWRUYXJnZX'
'QSVgoSZ2FzX2xpbWl0X2V4Y2VlZGVkGAIgASgLMiYuYXJiaXRlci5ldm0uR2FzTGltaXRFeGNl'
'ZWRlZFZpb2xhdGlvbkgAUhBnYXNMaW1pdEV4Y2VlZGVkEkgKE3JhdGVfbGltaXRfZXhjZWVkZW'
'QYAyABKAsyFi5nb29nbGUucHJvdG9idWYuRW1wdHlIAFIRcmF0ZUxpbWl0RXhjZWVkZWQSVAoZ'
'dm9sdW1ldHJpY19saW1pdF9leGNlZWRlZBgEIAEoCzIWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eU'
'gAUhd2b2x1bWV0cmljTGltaXRFeGNlZWRlZBI7CgxpbnZhbGlkX3RpbWUYBSABKAsyFi5nb29n'
'bGUucHJvdG9idWYuRW1wdHlIAFILaW52YWxpZFRpbWUSUgoYaW52YWxpZF90cmFuc2FjdGlvbl'
'90eXBlGAYgASgLMhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5SABSFmludmFsaWRUcmFuc2FjdGlv'
'blR5cGVCBgoEa2luZA==');
@$core.Deprecated('Use noMatchingGrantErrorDescriptor instead')
const NoMatchingGrantError$json = {
'1': 'NoMatchingGrantError',
'2': [
{
'1': 'meaning',
'3': 1,
'4': 1,
'5': 11,
'6': '.arbiter.evm.SpecificMeaning',
'10': 'meaning'
},
],
};
/// Descriptor for `NoMatchingGrantError`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List noMatchingGrantErrorDescriptor = $convert.base64Decode(
'ChROb01hdGNoaW5nR3JhbnRFcnJvchI2CgdtZWFuaW5nGAEgASgLMhwuYXJiaXRlci5ldm0uU3'
'BlY2lmaWNNZWFuaW5nUgdtZWFuaW5n');
@$core.Deprecated('Use policyViolationsErrorDescriptor instead')
const PolicyViolationsError$json = {
'1': 'PolicyViolationsError',
'2': [
{
'1': 'meaning',
'3': 1,
'4': 1,
'5': 11,
'6': '.arbiter.evm.SpecificMeaning',
'10': 'meaning'
},
{
'1': 'violations',
'3': 2,
'4': 3,
'5': 11,
'6': '.arbiter.evm.EvalViolation',
'10': 'violations'
},
],
};
/// Descriptor for `PolicyViolationsError`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List policyViolationsErrorDescriptor = $convert.base64Decode(
'ChVQb2xpY3lWaW9sYXRpb25zRXJyb3ISNgoHbWVhbmluZxgBIAEoCzIcLmFyYml0ZXIuZXZtLl'
'NwZWNpZmljTWVhbmluZ1IHbWVhbmluZxI6Cgp2aW9sYXRpb25zGAIgAygLMhouYXJiaXRlci5l'
'dm0uRXZhbFZpb2xhdGlvblIKdmlvbGF0aW9ucw==');
@$core.Deprecated('Use transactionEvalErrorDescriptor instead')
const TransactionEvalError$json = {
'1': 'TransactionEvalError',
'2': [
{
'1': 'contract_creation_not_supported',
'3': 1,
'4': 1,
'5': 11,
'6': '.google.protobuf.Empty',
'9': 0,
'10': 'contractCreationNotSupported'
},
{
'1': 'unsupported_transaction_type',
'3': 2,
'4': 1,
'5': 11,
'6': '.google.protobuf.Empty',
'9': 0,
'10': 'unsupportedTransactionType'
},
{
'1': 'no_matching_grant',
'3': 3,
'4': 1,
'5': 11,
'6': '.arbiter.evm.NoMatchingGrantError',
'9': 0,
'10': 'noMatchingGrant'
},
{
'1': 'policy_violations',
'3': 4,
'4': 1,
'5': 11,
'6': '.arbiter.evm.PolicyViolationsError',
'9': 0,
'10': 'policyViolations'
},
],
'8': [
{'1': 'kind'},
],
};
/// Descriptor for `TransactionEvalError`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List transactionEvalErrorDescriptor = $convert.base64Decode(
'ChRUcmFuc2FjdGlvbkV2YWxFcnJvchJfCh9jb250cmFjdF9jcmVhdGlvbl9ub3Rfc3VwcG9ydG'
'VkGAEgASgLMhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5SABSHGNvbnRyYWN0Q3JlYXRpb25Ob3RT'
'dXBwb3J0ZWQSWgocdW5zdXBwb3J0ZWRfdHJhbnNhY3Rpb25fdHlwZRgCIAEoCzIWLmdvb2dsZS'
'5wcm90b2J1Zi5FbXB0eUgAUhp1bnN1cHBvcnRlZFRyYW5zYWN0aW9uVHlwZRJPChFub19tYXRj'
'aGluZ19ncmFudBgDIAEoCzIhLmFyYml0ZXIuZXZtLk5vTWF0Y2hpbmdHcmFudEVycm9ySABSD2'
'5vTWF0Y2hpbmdHcmFudBJRChFwb2xpY3lfdmlvbGF0aW9ucxgEIAEoCzIiLmFyYml0ZXIuZXZt'
'LlBvbGljeVZpb2xhdGlvbnNFcnJvckgAUhBwb2xpY3lWaW9sYXRpb25zQgYKBGtpbmQ=');
@$core.Deprecated('Use evmGrantCreateRequestDescriptor instead')
const EvmGrantCreateRequest$json = {
'1': 'EvmGrantCreateRequest',
'2': [
{'1': 'client_id', '3': 1, '4': 1, '5': 5, '10': 'clientId'},
{
'1': 'shared',
'3': 2,
'4': 1,
'5': 11,
'6': '.arbiter.evm.SharedSettings',
'10': 'shared'
},
{
'1': 'specific',
'3': 3,
'4': 1,
'5': 11,
'6': '.arbiter.evm.SpecificGrant',
'10': 'specific'
},
],
};
/// Descriptor for `EvmGrantCreateRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List evmGrantCreateRequestDescriptor = $convert.base64Decode(
'ChVFdm1HcmFudENyZWF0ZVJlcXVlc3QSGwoJY2xpZW50X2lkGAEgASgFUghjbGllbnRJZBIzCg'
'ZzaGFyZWQYAiABKAsyGy5hcmJpdGVyLmV2bS5TaGFyZWRTZXR0aW5nc1IGc2hhcmVkEjYKCHNw'
'ZWNpZmljGAMgASgLMhouYXJiaXRlci5ldm0uU3BlY2lmaWNHcmFudFIIc3BlY2lmaWM=');
@$core.Deprecated('Use evmGrantCreateResponseDescriptor instead')
const EvmGrantCreateResponse$json = {
'1': 'EvmGrantCreateResponse',
'2': [
{'1': 'grant_id', '3': 1, '4': 1, '5': 5, '9': 0, '10': 'grantId'},
{
'1': 'error',
'3': 2,
'4': 1,
'5': 14,
'6': '.arbiter.evm.EvmError',
'9': 0,
'10': 'error'
},
],
'8': [
{'1': 'result'},
],
};
/// Descriptor for `EvmGrantCreateResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List evmGrantCreateResponseDescriptor = $convert.base64Decode(
'ChZFdm1HcmFudENyZWF0ZVJlc3BvbnNlEhsKCGdyYW50X2lkGAEgASgFSABSB2dyYW50SWQSLQ'
'oFZXJyb3IYAiABKA4yFS5hcmJpdGVyLmV2bS5Fdm1FcnJvckgAUgVlcnJvckIICgZyZXN1bHQ=');
@$core.Deprecated('Use evmGrantDeleteRequestDescriptor instead')
const EvmGrantDeleteRequest$json = {
'1': 'EvmGrantDeleteRequest',
'2': [
{'1': 'grant_id', '3': 1, '4': 1, '5': 5, '10': 'grantId'},
],
};
/// Descriptor for `EvmGrantDeleteRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List evmGrantDeleteRequestDescriptor =
$convert.base64Decode(
'ChVFdm1HcmFudERlbGV0ZVJlcXVlc3QSGQoIZ3JhbnRfaWQYASABKAVSB2dyYW50SWQ=');
@$core.Deprecated('Use evmGrantDeleteResponseDescriptor instead')
const EvmGrantDeleteResponse$json = {
'1': 'EvmGrantDeleteResponse',
'2': [
{
'1': 'ok',
'3': 1,
'4': 1,
'5': 11,
'6': '.google.protobuf.Empty',
'9': 0,
'10': 'ok'
},
{
'1': 'error',
'3': 2,
'4': 1,
'5': 14,
'6': '.arbiter.evm.EvmError',
'9': 0,
'10': 'error'
},
],
'8': [
{'1': 'result'},
],
};
/// Descriptor for `EvmGrantDeleteResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List evmGrantDeleteResponseDescriptor = $convert.base64Decode(
'ChZFdm1HcmFudERlbGV0ZVJlc3BvbnNlEigKAm9rGAEgASgLMhYuZ29vZ2xlLnByb3RvYnVmLk'
'VtcHR5SABSAm9rEi0KBWVycm9yGAIgASgOMhUuYXJiaXRlci5ldm0uRXZtRXJyb3JIAFIFZXJy'
'b3JCCAoGcmVzdWx0');
@$core.Deprecated('Use grantEntryDescriptor instead')
const GrantEntry$json = {
'1': 'GrantEntry',
'2': [
{'1': 'id', '3': 1, '4': 1, '5': 5, '10': 'id'},
{'1': 'client_id', '3': 2, '4': 1, '5': 5, '10': 'clientId'},
{
'1': 'shared',
'3': 3,
'4': 1,
'5': 11,
'6': '.arbiter.evm.SharedSettings',
'10': 'shared'
},
{
'1': 'specific',
'3': 4,
'4': 1,
'5': 11,
'6': '.arbiter.evm.SpecificGrant',
'10': 'specific'
},
],
};
/// Descriptor for `GrantEntry`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List grantEntryDescriptor = $convert.base64Decode(
'CgpHcmFudEVudHJ5Eg4KAmlkGAEgASgFUgJpZBIbCgljbGllbnRfaWQYAiABKAVSCGNsaWVudE'
'lkEjMKBnNoYXJlZBgDIAEoCzIbLmFyYml0ZXIuZXZtLlNoYXJlZFNldHRpbmdzUgZzaGFyZWQS'
'NgoIc3BlY2lmaWMYBCABKAsyGi5hcmJpdGVyLmV2bS5TcGVjaWZpY0dyYW50UghzcGVjaWZpYw'
'==');
@$core.Deprecated('Use evmGrantListRequestDescriptor instead')
const EvmGrantListRequest$json = {
'1': 'EvmGrantListRequest',
'2': [
{
'1': 'wallet_id',
'3': 1,
'4': 1,
'5': 5,
'9': 0,
'10': 'walletId',
'17': true
},
],
'8': [
{'1': '_wallet_id'},
],
};
/// Descriptor for `EvmGrantListRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List evmGrantListRequestDescriptor = $convert.base64Decode(
'ChNFdm1HcmFudExpc3RSZXF1ZXN0EiAKCXdhbGxldF9pZBgBIAEoBUgAUgh3YWxsZXRJZIgBAU'
'IMCgpfd2FsbGV0X2lk');
@$core.Deprecated('Use evmGrantListResponseDescriptor instead')
const EvmGrantListResponse$json = {
'1': 'EvmGrantListResponse',
'2': [
{
'1': 'grants',
'3': 1,
'4': 1,
'5': 11,
'6': '.arbiter.evm.EvmGrantList',
'9': 0,
'10': 'grants'
},
{
'1': 'error',
'3': 2,
'4': 1,
'5': 14,
'6': '.arbiter.evm.EvmError',
'9': 0,
'10': 'error'
},
],
'8': [
{'1': 'result'},
],
};
/// Descriptor for `EvmGrantListResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List evmGrantListResponseDescriptor = $convert.base64Decode(
'ChRFdm1HcmFudExpc3RSZXNwb25zZRIzCgZncmFudHMYASABKAsyGS5hcmJpdGVyLmV2bS5Fdm'
'1HcmFudExpc3RIAFIGZ3JhbnRzEi0KBWVycm9yGAIgASgOMhUuYXJiaXRlci5ldm0uRXZtRXJy'
'b3JIAFIFZXJyb3JCCAoGcmVzdWx0');
@$core.Deprecated('Use evmGrantListDescriptor instead')
const EvmGrantList$json = {
'1': 'EvmGrantList',
'2': [
{
'1': 'grants',
'3': 1,
'4': 3,
'5': 11,
'6': '.arbiter.evm.GrantEntry',
'10': 'grants'
},
],
};
/// Descriptor for `EvmGrantList`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List evmGrantListDescriptor = $convert.base64Decode(
'CgxFdm1HcmFudExpc3QSLwoGZ3JhbnRzGAEgAygLMhcuYXJiaXRlci5ldm0uR3JhbnRFbnRyeV'
'IGZ3JhbnRz');
@$core.Deprecated('Use evmSignTransactionRequestDescriptor instead')
const EvmSignTransactionRequest$json = {
'1': 'EvmSignTransactionRequest',
'2': [
{'1': 'wallet_address', '3': 1, '4': 1, '5': 12, '10': 'walletAddress'},
{'1': 'rlp_transaction', '3': 2, '4': 1, '5': 12, '10': 'rlpTransaction'},
],
};
/// Descriptor for `EvmSignTransactionRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List evmSignTransactionRequestDescriptor =
$convert.base64Decode(
'ChlFdm1TaWduVHJhbnNhY3Rpb25SZXF1ZXN0EiUKDndhbGxldF9hZGRyZXNzGAEgASgMUg13YW'
'xsZXRBZGRyZXNzEicKD3JscF90cmFuc2FjdGlvbhgCIAEoDFIOcmxwVHJhbnNhY3Rpb24=');
@$core.Deprecated('Use evmSignTransactionResponseDescriptor instead')
const EvmSignTransactionResponse$json = {
'1': 'EvmSignTransactionResponse',
'2': [
{'1': 'signature', '3': 1, '4': 1, '5': 12, '9': 0, '10': 'signature'},
{
'1': 'eval_error',
'3': 2,
'4': 1,
'5': 11,
'6': '.arbiter.evm.TransactionEvalError',
'9': 0,
'10': 'evalError'
},
{
'1': 'error',
'3': 3,
'4': 1,
'5': 14,
'6': '.arbiter.evm.EvmError',
'9': 0,
'10': 'error'
},
],
'8': [
{'1': 'result'},
],
};
/// Descriptor for `EvmSignTransactionResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List evmSignTransactionResponseDescriptor = $convert.base64Decode(
'ChpFdm1TaWduVHJhbnNhY3Rpb25SZXNwb25zZRIeCglzaWduYXR1cmUYASABKAxIAFIJc2lnbm'
'F0dXJlEkIKCmV2YWxfZXJyb3IYAiABKAsyIS5hcmJpdGVyLmV2bS5UcmFuc2FjdGlvbkV2YWxF'
'cnJvckgAUglldmFsRXJyb3ISLQoFZXJyb3IYAyABKA4yFS5hcmJpdGVyLmV2bS5Fdm1FcnJvck'
'gAUgVlcnJvckIICgZyZXN1bHQ=');
@$core.Deprecated('Use evmAnalyzeTransactionRequestDescriptor instead')
const EvmAnalyzeTransactionRequest$json = {
'1': 'EvmAnalyzeTransactionRequest',
'2': [
{'1': 'wallet_address', '3': 1, '4': 1, '5': 12, '10': 'walletAddress'},
{'1': 'rlp_transaction', '3': 2, '4': 1, '5': 12, '10': 'rlpTransaction'},
],
};
/// Descriptor for `EvmAnalyzeTransactionRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List evmAnalyzeTransactionRequestDescriptor =
$convert.base64Decode(
'ChxFdm1BbmFseXplVHJhbnNhY3Rpb25SZXF1ZXN0EiUKDndhbGxldF9hZGRyZXNzGAEgASgMUg'
'13YWxsZXRBZGRyZXNzEicKD3JscF90cmFuc2FjdGlvbhgCIAEoDFIOcmxwVHJhbnNhY3Rpb24=');
@$core.Deprecated('Use evmAnalyzeTransactionResponseDescriptor instead')
const EvmAnalyzeTransactionResponse$json = {
'1': 'EvmAnalyzeTransactionResponse',
'2': [
{
'1': 'meaning',
'3': 1,
'4': 1,
'5': 11,
'6': '.arbiter.evm.SpecificMeaning',
'9': 0,
'10': 'meaning'
},
{
'1': 'eval_error',
'3': 2,
'4': 1,
'5': 11,
'6': '.arbiter.evm.TransactionEvalError',
'9': 0,
'10': 'evalError'
},
{
'1': 'error',
'3': 3,
'4': 1,
'5': 14,
'6': '.arbiter.evm.EvmError',
'9': 0,
'10': 'error'
},
],
'8': [
{'1': 'result'},
],
};
/// Descriptor for `EvmAnalyzeTransactionResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List evmAnalyzeTransactionResponseDescriptor = $convert.base64Decode(
'Ch1Fdm1BbmFseXplVHJhbnNhY3Rpb25SZXNwb25zZRI4CgdtZWFuaW5nGAEgASgLMhwuYXJiaX'
'Rlci5ldm0uU3BlY2lmaWNNZWFuaW5nSABSB21lYW5pbmcSQgoKZXZhbF9lcnJvchgCIAEoCzIh'
'LmFyYml0ZXIuZXZtLlRyYW5zYWN0aW9uRXZhbEVycm9ySABSCWV2YWxFcnJvchItCgVlcnJvch'
'gDIAEoDjIVLmFyYml0ZXIuZXZtLkV2bUVycm9ySABSBWVycm9yQggKBnJlc3VsdA==');

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,121 @@
// This is a generated file - do not edit.
//
// Generated from user_agent.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class KeyType extends $pb.ProtobufEnum {
static const KeyType KEY_TYPE_UNSPECIFIED =
KeyType._(0, _omitEnumNames ? '' : 'KEY_TYPE_UNSPECIFIED');
static const KeyType KEY_TYPE_ED25519 =
KeyType._(1, _omitEnumNames ? '' : 'KEY_TYPE_ED25519');
static const KeyType KEY_TYPE_ECDSA_SECP256K1 =
KeyType._(2, _omitEnumNames ? '' : 'KEY_TYPE_ECDSA_SECP256K1');
static const KeyType KEY_TYPE_RSA =
KeyType._(3, _omitEnumNames ? '' : 'KEY_TYPE_RSA');
static const $core.List<KeyType> values = <KeyType>[
KEY_TYPE_UNSPECIFIED,
KEY_TYPE_ED25519,
KEY_TYPE_ECDSA_SECP256K1,
KEY_TYPE_RSA,
];
static final $core.List<KeyType?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 3);
static KeyType? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const KeyType._(super.value, super.name);
}
class UnsealResult extends $pb.ProtobufEnum {
static const UnsealResult UNSEAL_RESULT_UNSPECIFIED =
UnsealResult._(0, _omitEnumNames ? '' : 'UNSEAL_RESULT_UNSPECIFIED');
static const UnsealResult UNSEAL_RESULT_SUCCESS =
UnsealResult._(1, _omitEnumNames ? '' : 'UNSEAL_RESULT_SUCCESS');
static const UnsealResult UNSEAL_RESULT_INVALID_KEY =
UnsealResult._(2, _omitEnumNames ? '' : 'UNSEAL_RESULT_INVALID_KEY');
static const UnsealResult UNSEAL_RESULT_UNBOOTSTRAPPED =
UnsealResult._(3, _omitEnumNames ? '' : 'UNSEAL_RESULT_UNBOOTSTRAPPED');
static const $core.List<UnsealResult> values = <UnsealResult>[
UNSEAL_RESULT_UNSPECIFIED,
UNSEAL_RESULT_SUCCESS,
UNSEAL_RESULT_INVALID_KEY,
UNSEAL_RESULT_UNBOOTSTRAPPED,
];
static final $core.List<UnsealResult?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 3);
static UnsealResult? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const UnsealResult._(super.value, super.name);
}
class BootstrapResult extends $pb.ProtobufEnum {
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');
static const $core.List<BootstrapResult> values = <BootstrapResult>[
BOOTSTRAP_RESULT_UNSPECIFIED,
BOOTSTRAP_RESULT_SUCCESS,
BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED,
BOOTSTRAP_RESULT_INVALID_KEY,
];
static final $core.List<BootstrapResult?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 3);
static BootstrapResult? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const BootstrapResult._(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<VaultState> values = <VaultState>[
VAULT_STATE_UNSPECIFIED,
VAULT_STATE_UNBOOTSTRAPPED,
VAULT_STATE_SEALED,
VAULT_STATE_UNSEALED,
VAULT_STATE_ERROR,
];
static final $core.List<VaultState?> _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 =
$core.bool.fromEnvironment('protobuf.omit_enum_names');

View File

@@ -0,0 +1,482 @@
// This is a generated file - do not edit.
//
// Generated from user_agent.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
// ignore_for_file: unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use keyTypeDescriptor instead')
const KeyType$json = {
'1': 'KeyType',
'2': [
{'1': 'KEY_TYPE_UNSPECIFIED', '2': 0},
{'1': 'KEY_TYPE_ED25519', '2': 1},
{'1': 'KEY_TYPE_ECDSA_SECP256K1', '2': 2},
{'1': 'KEY_TYPE_RSA', '2': 3},
],
};
/// Descriptor for `KeyType`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List keyTypeDescriptor = $convert.base64Decode(
'CgdLZXlUeXBlEhgKFEtFWV9UWVBFX1VOU1BFQ0lGSUVEEAASFAoQS0VZX1RZUEVfRUQyNTUxOR'
'ABEhwKGEtFWV9UWVBFX0VDRFNBX1NFQ1AyNTZLMRACEhAKDEtFWV9UWVBFX1JTQRAD');
@$core.Deprecated('Use unsealResultDescriptor instead')
const UnsealResult$json = {
'1': 'UnsealResult',
'2': [
{'1': 'UNSEAL_RESULT_UNSPECIFIED', '2': 0},
{'1': 'UNSEAL_RESULT_SUCCESS', '2': 1},
{'1': 'UNSEAL_RESULT_INVALID_KEY', '2': 2},
{'1': 'UNSEAL_RESULT_UNBOOTSTRAPPED', '2': 3},
],
};
/// Descriptor for `UnsealResult`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List unsealResultDescriptor = $convert.base64Decode(
'CgxVbnNlYWxSZXN1bHQSHQoZVU5TRUFMX1JFU1VMVF9VTlNQRUNJRklFRBAAEhkKFVVOU0VBTF'
'9SRVNVTFRfU1VDQ0VTUxABEh0KGVVOU0VBTF9SRVNVTFRfSU5WQUxJRF9LRVkQAhIgChxVTlNF'
'QUxfUkVTVUxUX1VOQk9PVFNUUkFQUEVEEAM=');
@$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',
'2': [
{'1': 'pubkey', '3': 1, '4': 1, '5': 12, '10': 'pubkey'},
{
'1': 'bootstrap_token',
'3': 2,
'4': 1,
'5': 9,
'9': 0,
'10': 'bootstrapToken',
'17': true
},
{
'1': 'key_type',
'3': 3,
'4': 1,
'5': 14,
'6': '.arbiter.user_agent.KeyType',
'10': 'keyType'
},
],
'8': [
{'1': '_bootstrap_token'},
],
};
/// Descriptor for `AuthChallengeRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List authChallengeRequestDescriptor = $convert.base64Decode(
'ChRBdXRoQ2hhbGxlbmdlUmVxdWVzdBIWCgZwdWJrZXkYASABKAxSBnB1YmtleRIsCg9ib290c3'
'RyYXBfdG9rZW4YAiABKAlIAFIOYm9vdHN0cmFwVG9rZW6IAQESNgoIa2V5X3R5cGUYAyABKA4y'
'Gy5hcmJpdGVyLnVzZXJfYWdlbnQuS2V5VHlwZVIHa2V5VHlwZUISChBfYm9vdHN0cmFwX3Rva2'
'Vu');
@$core.Deprecated('Use authChallengeDescriptor instead')
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'},
],
};
/// Descriptor for `AuthChallenge`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List authChallengeDescriptor = $convert.base64Decode(
'Cg1BdXRoQ2hhbGxlbmdlEhYKBnB1YmtleRgBIAEoDFIGcHVia2V5EhQKBW5vbmNlGAIgASgFUg'
'Vub25jZQ==');
@$core.Deprecated('Use authChallengeSolutionDescriptor instead')
const AuthChallengeSolution$json = {
'1': 'AuthChallengeSolution',
'2': [
{'1': 'signature', '3': 1, '4': 1, '5': 12, '10': 'signature'},
],
};
/// Descriptor for `AuthChallengeSolution`. Decode as a `google.protobuf.DescriptorProto`.
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',
'2': [
{'1': 'client_pubkey', '3': 1, '4': 1, '5': 12, '10': 'clientPubkey'},
],
};
/// Descriptor for `UnsealStart`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List unsealStartDescriptor = $convert.base64Decode(
'CgtVbnNlYWxTdGFydBIjCg1jbGllbnRfcHVia2V5GAEgASgMUgxjbGllbnRQdWJrZXk=');
@$core.Deprecated('Use unsealStartResponseDescriptor instead')
const UnsealStartResponse$json = {
'1': 'UnsealStartResponse',
'2': [
{'1': 'server_pubkey', '3': 1, '4': 1, '5': 12, '10': 'serverPubkey'},
],
};
/// Descriptor for `UnsealStartResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List unsealStartResponseDescriptor = $convert.base64Decode(
'ChNVbnNlYWxTdGFydFJlc3BvbnNlEiMKDXNlcnZlcl9wdWJrZXkYASABKAxSDHNlcnZlclB1Ym'
'tleQ==');
@$core.Deprecated('Use unsealEncryptedKeyDescriptor instead')
const UnsealEncryptedKey$json = {
'1': 'UnsealEncryptedKey',
'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 `UnsealEncryptedKey`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List unsealEncryptedKeyDescriptor = $convert.base64Decode(
'ChJVbnNlYWxFbmNyeXB0ZWRLZXkSFAoFbm9uY2UYASABKAxSBW5vbmNlEh4KCmNpcGhlcnRleH'
'QYAiABKAxSCmNpcGhlcnRleHQSJwoPYXNzb2NpYXRlZF9kYXRhGAMgASgMUg5hc3NvY2lhdGVk'
'RGF0YQ==');
@$core.Deprecated('Use clientConnectionRequestDescriptor instead')
const ClientConnectionRequest$json = {
'1': 'ClientConnectionRequest',
'2': [
{'1': 'pubkey', '3': 1, '4': 1, '5': 12, '10': 'pubkey'},
],
};
/// Descriptor for `ClientConnectionRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List clientConnectionRequestDescriptor =
$convert.base64Decode(
'ChdDbGllbnRDb25uZWN0aW9uUmVxdWVzdBIWCgZwdWJrZXkYASABKAxSBnB1YmtleQ==');
@$core.Deprecated('Use clientConnectionResponseDescriptor instead')
const ClientConnectionResponse$json = {
'1': 'ClientConnectionResponse',
'2': [
{'1': 'approved', '3': 1, '4': 1, '5': 8, '10': 'approved'},
],
};
/// Descriptor for `ClientConnectionResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List clientConnectionResponseDescriptor =
$convert.base64Decode(
'ChhDbGllbnRDb25uZWN0aW9uUmVzcG9uc2USGgoIYXBwcm92ZWQYASABKAhSCGFwcHJvdmVk');
@$core.Deprecated('Use clientConnectionCancelDescriptor instead')
const ClientConnectionCancel$json = {
'1': 'ClientConnectionCancel',
};
/// Descriptor for `ClientConnectionCancel`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List clientConnectionCancelDescriptor =
$convert.base64Decode('ChZDbGllbnRDb25uZWN0aW9uQ2FuY2Vs');
@$core.Deprecated('Use userAgentRequestDescriptor instead')
const UserAgentRequest$json = {
'1': 'UserAgentRequest',
'2': [
{
'1': 'auth_challenge_request',
'3': 1,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.AuthChallengeRequest',
'9': 0,
'10': 'authChallengeRequest'
},
{
'1': 'auth_challenge_solution',
'3': 2,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.AuthChallengeSolution',
'9': 0,
'10': 'authChallengeSolution'
},
{
'1': 'unseal_start',
'3': 3,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.UnsealStart',
'9': 0,
'10': 'unsealStart'
},
{
'1': 'unseal_encrypted_key',
'3': 4,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.UnsealEncryptedKey',
'9': 0,
'10': 'unsealEncryptedKey'
},
{
'1': 'query_vault_state',
'3': 5,
'4': 1,
'5': 11,
'6': '.google.protobuf.Empty',
'9': 0,
'10': 'queryVaultState'
},
{
'1': 'evm_wallet_create',
'3': 6,
'4': 1,
'5': 11,
'6': '.google.protobuf.Empty',
'9': 0,
'10': 'evmWalletCreate'
},
{
'1': 'evm_wallet_list',
'3': 7,
'4': 1,
'5': 11,
'6': '.google.protobuf.Empty',
'9': 0,
'10': 'evmWalletList'
},
{
'1': 'evm_grant_create',
'3': 8,
'4': 1,
'5': 11,
'6': '.arbiter.evm.EvmGrantCreateRequest',
'9': 0,
'10': 'evmGrantCreate'
},
{
'1': 'evm_grant_delete',
'3': 9,
'4': 1,
'5': 11,
'6': '.arbiter.evm.EvmGrantDeleteRequest',
'9': 0,
'10': 'evmGrantDelete'
},
{
'1': 'evm_grant_list',
'3': 10,
'4': 1,
'5': 11,
'6': '.arbiter.evm.EvmGrantListRequest',
'9': 0,
'10': 'evmGrantList'
},
{
'1': 'client_connection_response',
'3': 11,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.ClientConnectionResponse',
'9': 0,
'10': 'clientConnectionResponse'
},
],
'8': [
{'1': 'payload'},
],
};
/// 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==');
@$core.Deprecated('Use userAgentResponseDescriptor instead')
const UserAgentResponse$json = {
'1': 'UserAgentResponse',
'2': [
{
'1': 'auth_challenge',
'3': 1,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.AuthChallenge',
'9': 0,
'10': 'authChallenge'
},
{
'1': 'auth_ok',
'3': 2,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.AuthOk',
'9': 0,
'10': 'authOk'
},
{
'1': 'unseal_start_response',
'3': 3,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.UnsealStartResponse',
'9': 0,
'10': 'unsealStartResponse'
},
{
'1': 'unseal_result',
'3': 4,
'4': 1,
'5': 14,
'6': '.arbiter.user_agent.UnsealResult',
'9': 0,
'10': 'unsealResult'
},
{
'1': 'vault_state',
'3': 5,
'4': 1,
'5': 14,
'6': '.arbiter.user_agent.VaultState',
'9': 0,
'10': 'vaultState'
},
{
'1': 'evm_wallet_create',
'3': 6,
'4': 1,
'5': 11,
'6': '.arbiter.evm.WalletCreateResponse',
'9': 0,
'10': 'evmWalletCreate'
},
{
'1': 'evm_wallet_list',
'3': 7,
'4': 1,
'5': 11,
'6': '.arbiter.evm.WalletListResponse',
'9': 0,
'10': 'evmWalletList'
},
{
'1': 'evm_grant_create',
'3': 8,
'4': 1,
'5': 11,
'6': '.arbiter.evm.EvmGrantCreateResponse',
'9': 0,
'10': 'evmGrantCreate'
},
{
'1': 'evm_grant_delete',
'3': 9,
'4': 1,
'5': 11,
'6': '.arbiter.evm.EvmGrantDeleteResponse',
'9': 0,
'10': 'evmGrantDelete'
},
{
'1': 'evm_grant_list',
'3': 10,
'4': 1,
'5': 11,
'6': '.arbiter.evm.EvmGrantListResponse',
'9': 0,
'10': 'evmGrantList'
},
{
'1': 'client_connection_request',
'3': 11,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.ClientConnectionRequest',
'9': 0,
'10': 'clientConnectionRequest'
},
{
'1': 'client_connection_cancel',
'3': 12,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.ClientConnectionCancel',
'9': 0,
'10': 'clientConnectionCancel'
},
],
'8': [
{'1': 'payload'},
],
};
/// 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==');

Some files were not shown because too many files have changed in this diff Show More