33 Commits

Author SHA1 Message Date
CleverWild
efb11d2271 refactor(arbiter-client): rewrite errors to terrros 2026-03-24 17:25:45 +01:00
2148faa376 Merge pull request 'SDK-client-UA-registration' (#34) from SDK-client-UA-registration into main
Some checks failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-lint Pipeline was successful
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
ci/woodpecker/push/useragent-analyze Pipeline failed
Reviewed-on: #34
2026-03-22 11:11:11 +00:00
hdbg
eb37ee0a0c refactor(client): redesign of wallet handle
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-22 12:05:48 +01:00
hdbg
1f07fd6a98 refactor(client): split into more modules 2026-03-22 11:57:55 +01:00
hdbg
e135519c06 chore(deps): update Rust dependencies and add cargo-edit
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-22 00:10:18 +01:00
CleverWild
f015d345f4 Merge remote-tracking branch 'origin/main' into SDK-client-UA-registration
Some checks failed
ci/woodpecker/pr/server-audit Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-21 21:14:41 +01:00
CleverWild
784261f4d8 perf(user-agent): use sqlite INSERT ... RETURNING for sdk client approve
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-19 19:07:28 +01:00
CleverWild
971db0e919 refactor(client-auth): introduce ClientId newtype to avoid client_id/nonce confusion
refactor(user-agent): replace manual terminality helper with fatality::Fatality
2026-03-19 19:07:19 +01:00
CleverWild
e1a8553142 feat(client-auth): emit and require AuthOk for SDK client challenge flow 2026-03-19 19:06:27 +01:00
CleverWild
ec70561c93 refactor(arbiter-client): split auth handshake into check/do steps and simplify TxSigner signing flow 2026-03-19 19:05:56 +01:00
CleverWild
3993d3a8cc refactor(client): decouple grpc connect from wallet address and add explicit wallet configuration 2026-03-19 18:21:09 +01:00
CleverWild
c87456ae2f feat(client): add file-backed signing key storage with transparent first-run key creation 2026-03-19 18:10:43 +01:00
CleverWild
e89983de3a refactor(proto): align remaining ClientConnection protobuf pairs with SdkClient* naming 2026-03-19 18:00:10 +01:00
CleverWild
f56668d9f6 chore: make const for buffer size 2026-03-19 17:54:31 +01:00
CleverWild
434738bae5 fix: return very important comment 2026-03-19 17:52:11 +01:00
hdbg
915540de32 housekeeping(server): fixed clippy warns
Some checks failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-lint Pipeline was successful
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
ci/woodpecker/push/useragent-analyze Pipeline failed
2026-03-19 07:53:55 +00:00
hdbg
5a5008080a docs: document explicit AuthResult enums and request multiplexing 2026-03-19 07:53:55 +00:00
hdbg
3bc423f9b2 feat(useragent): showing auth error when something went wrong 2026-03-19 07:53:55 +00:00
hdbg
f2c33a5bf4 refactor(useragent): using request/response for correct multiplexing behaviour 2026-03-19 07:53:55 +00:00
hdbg
3e8b26418a feat(proto): request / response pair tracking by assigning id 2026-03-19 07:53:55 +00:00
hdbg
60ce1cc110 test(user-agent): add test helpers and update actor integration tests 2026-03-19 07:53:55 +00:00
hdbg
2ff4d0961c refactor(server::client): migrated to new connection design 2026-03-19 07:53:55 +00:00
hdbg
d61dab3285 refactor(server::useragent): migrated to new connection design 2026-03-19 07:53:55 +00:00
hdbg
c439c9645d ci(useragent): added analyze step
Some checks failed
ci/woodpecker/push/useragent-analyze Pipeline failed
2026-03-19 00:38:59 +01:00
hdbg
c2883704e6 housekeeping: removed ide config from repo 2026-03-19 00:34:43 +01:00
CleverWild
77c3babec7 feat: compat migrations
Some checks failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-17 19:44:58 +01:00
CleverWild
6f03ce4d1d chore: remove invalidly committed PoC crate
Some checks failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-17 19:42:35 +01:00
CleverWild
c90af9c196 fix(server): restore online client approval UX with sdk management
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-16 18:46:50 +01:00
CleverWild
a5a9bc73b0 feat(poc): enhance SDK client error handling in user agent module
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-16 18:19:50 +01:00
CleverWild
099f76166e feat(PoC): terrors crate usage
Some checks failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-15 21:11:23 +01:00
CleverWild
66026e903a feat(poc): complete terrors PoC with main scenarios 2026-03-15 19:24:49 +01:00
CleverWild
3360d3c8c7 feat(poc): add db and auth modules with terrors error chains 2026-03-15 19:24:21 +01:00
CleverWild
02980468db feat(poc): add terrors PoC crate scaffold and error types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 19:21:55 +01:00
63 changed files with 3572 additions and 1912 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@ target/
scripts/__pycache__/ scripts/__pycache__/
.DS_Store .DS_Store
.cargo/config.toml .cargo/config.toml
.vscode/

View File

@@ -1,2 +0,0 @@
{
}

View File

@@ -22,4 +22,4 @@ steps:
- apt-get update && apt-get install -y pkg-config - apt-get update && apt-get install -y pkg-config
- mise install rust - mise install rust
- mise install protoc - mise install protoc
- mise exec rust -- cargo clippy --all-targets --all-features -- -D warnings - mise exec rust -- cargo clippy --all -- -D warnings

View File

@@ -0,0 +1,18 @@
when:
- event: pull_request
path:
include: ['.woodpecker/useragent-*.yaml', 'useragent/**']
- event: push
branch: main
path:
include: ['.woodpecker/useragent-*.yaml', 'useragent/**']
steps:
- name: analyze
image: jdxcode/mise:latest
commands:
- mise install flutter
- mise install protoc
# Reruns codegen to catch protocol drift
- mise codegen
- cd useragent/ && flutter analyze

View File

@@ -6,6 +6,20 @@ This document covers concrete technology choices and dependencies. For the archi
## Client Connection Flow ## Client Connection Flow
### Authentication Result Semantics
Authentication no longer uses an implicit success-only response shape. Both `client` and `user-agent` return explicit auth status enums over the wire.
- **Client:** `AuthResult` may return `SUCCESS`, `INVALID_KEY`, `INVALID_SIGNATURE`, `APPROVAL_DENIED`, `NO_USER_AGENTS_ONLINE`, or `INTERNAL`
- **User-agent:** `AuthResult` may return `SUCCESS`, `INVALID_KEY`, `INVALID_SIGNATURE`, `BOOTSTRAP_REQUIRED`, `TOKEN_INVALID`, or `INTERNAL`
This makes transport-level failures and actor/domain-level auth failures distinct:
- **Transport/protocol failures** are surfaced as stream/status errors
- **Authentication failures** are surfaced as successful protocol responses carrying an explicit auth status
Clients are expected to handle these status codes directly and present the concrete failure reason to the user.
### New Client Approval ### New Client Approval
When a client whose public key is not yet in the database connects, all connected user agents are asked to approve the connection. The first agent to respond determines the outcome; remaining requests are cancelled via a watch channel. When a client whose public key is not yet in the database connects, all connected user agents are asked to approve the connection. The first agent to respond determines the outcome; remaining requests are cancelled via a watch channel.
@@ -68,9 +82,21 @@ The `program_client.nonce` column stores the **next usable nonce** — i.e. it i
## Communication ## Communication
- **Protocol:** gRPC with Protocol Buffers - **Protocol:** gRPC with Protocol Buffers
- **Request/response matching:** multiplexed over a single bidirectional stream using per-connection request IDs
- **Server identity distribution:** `ServerInfo` protobuf struct containing the TLS public key fingerprint - **Server identity distribution:** `ServerInfo` protobuf struct containing the TLS public key fingerprint
- **Future consideration:** grpc-web lacks bidirectional stream support, so a browser-based wallet may require protojson over WebSocket - **Future consideration:** grpc-web lacks bidirectional stream support, so a browser-based wallet may require protojson over WebSocket
### Request Multiplexing
Both `client` and `user-agent` connections support multiple in-flight requests over one gRPC bidi stream.
- Every request carries a monotonically increasing request ID
- Every normal response echoes the request ID it corresponds to
- Out-of-band server messages omit the response ID entirely
- The server rejects already-seen request IDs at the transport adapter boundary before business logic sees the message
This keeps request correlation entirely in transport/client connection code while leaving actor and domain handlers unaware of request IDs.
--- ---
## EVM Policy Engine ## EVM Policy Engine

View File

@@ -1,16 +1,37 @@
# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html
[[tools.ast-grep]] [[tools.ast-grep]]
version = "0.42.0" version = "0.42.0"
backend = "aqua:ast-grep/ast-grep" 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"} [tools.ast-grep."platforms.linux-arm64"]
"platforms.macos-arm64" = { checksum = "sha256:fc300d5293b1c770a5aece03a8a193b92e71e87cec726c28096990691a582620", url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-apple-darwin.zip"} checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836"
"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"} url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.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.ast-grep."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"
[tools.ast-grep."platforms.macos-arm64"]
checksum = "sha256:fc300d5293b1c770a5aece03a8a193b92e71e87cec726c28096990691a582620"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-apple-darwin.zip"
[tools.ast-grep."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"
[tools.ast-grep."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"]] [[tools."cargo:cargo-audit"]]
version = "0.22.1" version = "0.22.1"
backend = "cargo:cargo-audit" backend = "cargo:cargo-audit"
[[tools."cargo:cargo-edit"]]
version = "0.13.9"
backend = "cargo:cargo-edit"
[[tools."cargo:cargo-features"]] [[tools."cargo:cargo-features"]]
version = "1.0.0" version = "1.0.0"
backend = "cargo:cargo-features" backend = "cargo:cargo-features"
@@ -62,20 +83,50 @@ backend = "asdf:flutter"
[[tools.protoc]] [[tools.protoc]]
version = "29.6" version = "29.6"
backend = "aqua:protocolbuffers/protobuf/protoc" backend = "aqua:protocolbuffers/protobuf/protoc"
"platforms.linux-arm64" = { checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79", url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip"}
"platforms.linux-x64" = { checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323", url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip"} [tools.protoc."platforms.linux-arm64"]
"platforms.macos-arm64" = { checksum = "sha256:b9576b5fa1a1ef3fe13a8c91d9d8204b46545759bea5ae155cd6ba2ea4cdaeed", url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-aarch_64.zip"} checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79"
"platforms.macos-x64" = { checksum = "sha256:312f04713946921cc0187ef34df80241ddca1bab6f564c636885fd2cc90d3f88", url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-x86_64.zip"} url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip"
"platforms.windows-x64" = { checksum = "sha256:1ebd7c87baffb9f1c47169b640872bf5fb1e4408079c691af527be9561d8f6f7", url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-win64.zip"}
[tools.protoc."platforms.linux-x64"]
checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323"
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip"
[tools.protoc."platforms.macos-arm64"]
checksum = "sha256:b9576b5fa1a1ef3fe13a8c91d9d8204b46545759bea5ae155cd6ba2ea4cdaeed"
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-aarch_64.zip"
[tools.protoc."platforms.macos-x64"]
checksum = "sha256:312f04713946921cc0187ef34df80241ddca1bab6f564c636885fd2cc90d3f88"
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-x86_64.zip"
[tools.protoc."platforms.windows-x64"]
checksum = "sha256:1ebd7c87baffb9f1c47169b640872bf5fb1e4408079c691af527be9561d8f6f7"
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-win64.zip"
[[tools.python]] [[tools.python]]
version = "3.14.3" version = "3.14.3"
backend = "core:python" backend = "core:python"
"platforms.linux-arm64" = { checksum = "sha256:be0f4dc2932f762292b27d46ea7d3e8e66ddf3969a5eb0254a229015ed402625", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"}
"platforms.linux-x64" = { checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"} [tools.python."platforms.linux-arm64"]
"platforms.macos-arm64" = { checksum = "sha256:4703cdf18b26798fde7b49b6b66149674c25f97127be6a10dbcf29309bdcdcdb", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-apple-darwin-install_only_stripped.tar.gz"} checksum = "sha256:be0f4dc2932f762292b27d46ea7d3e8e66ddf3969a5eb0254a229015ed402625"
"platforms.macos-x64" = { checksum = "sha256:76f1cc26e3d262eae8ca546a93e8bded10cf0323613f7e246fea2e10a8115eb7", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-apple-darwin-install_only_stripped.tar.gz"} url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
"platforms.windows-x64" = { checksum = "sha256:950c5f21a015c1bdd1337f233456df2470fab71e4d794407d27a84cb8b9909a0", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"}
[tools.python."platforms.linux-x64"]
checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
[tools.python."platforms.macos-arm64"]
checksum = "sha256:4703cdf18b26798fde7b49b6b66149674c25f97127be6a10dbcf29309bdcdcdb"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-apple-darwin-install_only_stripped.tar.gz"
[tools.python."platforms.macos-x64"]
checksum = "sha256:76f1cc26e3d262eae8ca546a93e8bded10cf0323613f7e246fea2e10a8115eb7"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-apple-darwin-install_only_stripped.tar.gz"
[tools.python."platforms.windows-x64"]
checksum = "sha256:950c5f21a015c1bdd1337f233456df2470fab71e4d794407d27a84cb8b9909a0"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
[[tools.rust]] [[tools.rust]]
version = "1.93.0" version = "1.93.0"

View File

@@ -11,6 +11,7 @@ protoc = "29.6"
"cargo:cargo-insta" = "1.46.3" "cargo:cargo-insta" = "1.46.3"
python = "3.14.3" python = "3.14.3"
ast-grep = "0.42.0" ast-grep = "0.42.0"
"cargo:cargo-edit" = "0.13.9"
[tasks.codegen] [tasks.codegen]
sources = ['protobufs/*.proto'] sources = ['protobufs/*.proto']

View File

@@ -3,6 +3,7 @@ syntax = "proto3";
package arbiter.client; package arbiter.client;
import "evm.proto"; import "evm.proto";
import "google/protobuf/empty.proto";
message AuthChallengeRequest { message AuthChallengeRequest {
bytes pubkey = 1; bytes pubkey = 1;
@@ -17,30 +18,40 @@ message AuthChallengeSolution {
bytes signature = 1; bytes signature = 1;
} }
message AuthOk {} enum AuthResult {
AUTH_RESULT_UNSPECIFIED = 0;
AUTH_RESULT_SUCCESS = 1;
AUTH_RESULT_INVALID_KEY = 2;
AUTH_RESULT_INVALID_SIGNATURE = 3;
AUTH_RESULT_APPROVAL_DENIED = 4;
AUTH_RESULT_NO_USER_AGENTS_ONLINE = 5;
AUTH_RESULT_INTERNAL = 6;
}
enum VaultState {
VAULT_STATE_UNSPECIFIED = 0;
VAULT_STATE_UNBOOTSTRAPPED = 1;
VAULT_STATE_SEALED = 2;
VAULT_STATE_UNSEALED = 3;
VAULT_STATE_ERROR = 4;
}
message ClientRequest { message ClientRequest {
int32 request_id = 4;
oneof payload { oneof payload {
AuthChallengeRequest auth_challenge_request = 1; AuthChallengeRequest auth_challenge_request = 1;
AuthChallengeSolution auth_challenge_solution = 2; AuthChallengeSolution auth_challenge_solution = 2;
google.protobuf.Empty query_vault_state = 3;
} }
} }
message ClientConnectError {
enum Code {
UNKNOWN = 0;
APPROVAL_DENIED = 1;
NO_USER_AGENTS_ONLINE = 2;
}
Code code = 1;
}
message ClientResponse { message ClientResponse {
optional int32 request_id = 7;
oneof payload { oneof payload {
AuthChallenge auth_challenge = 1; AuthChallenge auth_challenge = 1;
AuthOk auth_ok = 2; AuthResult auth_result = 2;
ClientConnectError client_connect_error = 5;
arbiter.evm.EvmSignTransactionResponse evm_sign_transaction = 3; arbiter.evm.EvmSignTransactionResponse evm_sign_transaction = 3;
arbiter.evm.EvmAnalyzeTransactionResponse evm_analyze_transaction = 4; arbiter.evm.EvmAnalyzeTransactionResponse evm_analyze_transaction = 4;
VaultState vault_state = 6;
} }
} }

View File

@@ -2,8 +2,8 @@ syntax = "proto3";
package arbiter.user_agent; package arbiter.user_agent;
import "google/protobuf/empty.proto";
import "evm.proto"; import "evm.proto";
import "google/protobuf/empty.proto";
enum KeyType { enum KeyType {
KEY_TYPE_UNSPECIFIED = 0; KEY_TYPE_UNSPECIFIED = 0;
@@ -12,6 +12,55 @@ enum KeyType {
KEY_TYPE_RSA = 3; KEY_TYPE_RSA = 3;
} }
// --- SDK client management ---
enum SdkClientError {
SDK_CLIENT_ERROR_UNSPECIFIED = 0;
SDK_CLIENT_ERROR_ALREADY_EXISTS = 1;
SDK_CLIENT_ERROR_NOT_FOUND = 2;
SDK_CLIENT_ERROR_HAS_RELATED_DATA = 3; // hard-delete blocked by FK (client has grants or transaction logs)
SDK_CLIENT_ERROR_INTERNAL = 4;
}
message SdkClientApproveRequest {
bytes pubkey = 1; // 32-byte ed25519 public key
}
message SdkClientRevokeRequest {
int32 client_id = 1;
}
message SdkClientEntry {
int32 id = 1;
bytes pubkey = 2;
int32 created_at = 3;
}
message SdkClientList {
repeated SdkClientEntry clients = 1;
}
message SdkClientApproveResponse {
oneof result {
SdkClientEntry client = 1;
SdkClientError error = 2;
}
}
message SdkClientRevokeResponse {
oneof result {
google.protobuf.Empty ok = 1;
SdkClientError error = 2;
}
}
message SdkClientListResponse {
oneof result {
SdkClientList clients = 1;
SdkClientError error = 2;
}
}
message AuthChallengeRequest { message AuthChallengeRequest {
bytes pubkey = 1; bytes pubkey = 1;
optional string bootstrap_token = 2; optional string bootstrap_token = 2;
@@ -19,15 +68,23 @@ message AuthChallengeRequest {
} }
message AuthChallenge { message AuthChallenge {
bytes pubkey = 1;
int32 nonce = 2; int32 nonce = 2;
reserved 1;
} }
message AuthChallengeSolution { message AuthChallengeSolution {
bytes signature = 1; bytes signature = 1;
} }
message AuthOk {} enum AuthResult {
AUTH_RESULT_UNSPECIFIED = 0;
AUTH_RESULT_SUCCESS = 1;
AUTH_RESULT_INVALID_KEY = 2;
AUTH_RESULT_INVALID_SIGNATURE = 3;
AUTH_RESULT_BOOTSTRAP_REQUIRED = 4;
AUTH_RESULT_TOKEN_INVALID = 5;
AUTH_RESULT_INTERNAL = 6;
}
message UnsealStart { message UnsealStart {
bytes client_pubkey = 1; bytes client_pubkey = 1;
@@ -70,17 +127,18 @@ enum VaultState {
VAULT_STATE_ERROR = 4; VAULT_STATE_ERROR = 4;
} }
message ClientConnectionRequest { message SdkClientConnectionRequest {
bytes pubkey = 1; bytes pubkey = 1;
} }
message ClientConnectionResponse { message SdkClientConnectionResponse {
bool approved = 1; bool approved = 1;
} }
message ClientConnectionCancel {} message SdkClientConnectionCancel {}
message UserAgentRequest { message UserAgentRequest {
int32 id = 16;
oneof payload { oneof payload {
AuthChallengeRequest auth_challenge_request = 1; AuthChallengeRequest auth_challenge_request = 1;
AuthChallengeSolution auth_challenge_solution = 2; AuthChallengeSolution auth_challenge_solution = 2;
@@ -92,14 +150,18 @@ message UserAgentRequest {
arbiter.evm.EvmGrantCreateRequest evm_grant_create = 8; arbiter.evm.EvmGrantCreateRequest evm_grant_create = 8;
arbiter.evm.EvmGrantDeleteRequest evm_grant_delete = 9; arbiter.evm.EvmGrantDeleteRequest evm_grant_delete = 9;
arbiter.evm.EvmGrantListRequest evm_grant_list = 10; arbiter.evm.EvmGrantListRequest evm_grant_list = 10;
ClientConnectionResponse client_connection_response = 11; SdkClientConnectionResponse sdk_client_connection_response = 11;
BootstrapEncryptedKey bootstrap_encrypted_key = 12; SdkClientApproveRequest sdk_client_approve = 12;
SdkClientRevokeRequest sdk_client_revoke = 13;
google.protobuf.Empty sdk_client_list = 14;
BootstrapEncryptedKey bootstrap_encrypted_key = 15;
} }
} }
message UserAgentResponse { message UserAgentResponse {
optional int32 id = 16;
oneof payload { oneof payload {
AuthChallenge auth_challenge = 1; AuthChallenge auth_challenge = 1;
AuthOk auth_ok = 2; AuthResult auth_result = 2;
UnsealStartResponse unseal_start_response = 3; UnsealStartResponse unseal_start_response = 3;
UnsealResult unseal_result = 4; UnsealResult unseal_result = 4;
VaultState vault_state = 5; VaultState vault_state = 5;
@@ -108,8 +170,10 @@ message UserAgentResponse {
arbiter.evm.EvmGrantCreateResponse evm_grant_create = 8; arbiter.evm.EvmGrantCreateResponse evm_grant_create = 8;
arbiter.evm.EvmGrantDeleteResponse evm_grant_delete = 9; arbiter.evm.EvmGrantDeleteResponse evm_grant_delete = 9;
arbiter.evm.EvmGrantListResponse evm_grant_list = 10; arbiter.evm.EvmGrantListResponse evm_grant_list = 10;
ClientConnectionRequest client_connection_request = 11; SdkClientConnectionResponse sdk_client_connection_response = 11;
ClientConnectionCancel client_connection_cancel = 12; SdkClientApproveResponse sdk_client_approve_response = 12;
BootstrapResult bootstrap_result = 13; SdkClientRevokeResponse sdk_client_revoke_response = 13;
SdkClientListResponse sdk_client_list_response = 14;
BootstrapResult bootstrap_result = 15;
} }
} }

349
server/Cargo.lock generated
View File

@@ -67,13 +67,13 @@ dependencies = [
[[package]] [[package]]
name = "alloy-chains" name = "alloy-chains"
version = "0.2.31" version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d9d22005bf31b018f31ef9ecadb5d2c39cf4f6acc8db0456f72c815f3d7f757" checksum = "9247f0a399ef71aeb68f497b2b8fb348014f742b50d3b83b1e00dfe1b7d64b3d"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"num_enum", "num_enum",
"strum", "strum 0.27.2",
] ]
[[package]] [[package]]
@@ -100,7 +100,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_with", "serde_with",
"thiserror", "thiserror 2.0.18",
] ]
[[package]] [[package]]
@@ -136,7 +136,7 @@ dependencies = [
"futures", "futures",
"futures-util", "futures-util",
"serde_json", "serde_json",
"thiserror", "thiserror 2.0.18",
] ]
[[package]] [[package]]
@@ -165,7 +165,7 @@ dependencies = [
"itoa", "itoa",
"serde", "serde",
"serde_json", "serde_json",
"winnow", "winnow 0.7.15",
] ]
[[package]] [[package]]
@@ -178,7 +178,7 @@ dependencies = [
"alloy-rlp", "alloy-rlp",
"crc", "crc",
"serde", "serde",
"thiserror", "thiserror 2.0.18",
] ]
[[package]] [[package]]
@@ -203,7 +203,7 @@ dependencies = [
"alloy-rlp", "alloy-rlp",
"borsh", "borsh",
"serde", "serde",
"thiserror", "thiserror 2.0.18",
] ]
[[package]] [[package]]
@@ -239,7 +239,7 @@ dependencies = [
"serde", "serde",
"serde_with", "serde_with",
"sha2 0.10.9", "sha2 0.10.9",
"thiserror", "thiserror 2.0.18",
] ]
[[package]] [[package]]
@@ -280,7 +280,7 @@ dependencies = [
"http", "http",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror 2.0.18",
"tracing", "tracing",
] ]
@@ -307,7 +307,7 @@ dependencies = [
"futures-utils-wasm", "futures-utils-wasm",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror 2.0.18",
] ]
[[package]] [[package]]
@@ -382,7 +382,7 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror 2.0.18",
"tokio", "tokio",
"tracing", "tracing",
"url", "url",
@@ -471,11 +471,11 @@ dependencies = [
"alloy-rlp", "alloy-rlp",
"alloy-serde", "alloy-serde",
"alloy-sol-types", "alloy-sol-types",
"itertools 0.14.0", "itertools 0.13.0",
"serde", "serde",
"serde_json", "serde_json",
"serde_with", "serde_with",
"thiserror", "thiserror 2.0.18",
] ]
[[package]] [[package]]
@@ -501,7 +501,7 @@ dependencies = [
"either", "either",
"elliptic-curve", "elliptic-curve",
"k256", "k256",
"thiserror", "thiserror 2.0.18",
] ]
[[package]] [[package]]
@@ -517,7 +517,7 @@ dependencies = [
"async-trait", "async-trait",
"k256", "k256",
"rand 0.8.5", "rand 0.8.5",
"thiserror", "thiserror 2.0.18",
] ]
[[package]] [[package]]
@@ -578,7 +578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249" checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249"
dependencies = [ dependencies = [
"serde", "serde",
"winnow", "winnow 0.7.15",
] ]
[[package]] [[package]]
@@ -608,7 +608,7 @@ dependencies = [
"parking_lot", "parking_lot",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror 2.0.18",
"tokio", "tokio",
"tower", "tower",
"tracing", "tracing",
@@ -624,7 +624,7 @@ checksum = "aa501ad58dd20acddbfebc65b52e60f05ebf97c52fa40d1b35e91f5e2da0ad0e"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"alloy-transport", "alloy-transport",
"itertools 0.14.0", "itertools 0.13.0",
"reqwest", "reqwest",
"serde_json", "serde_json",
"tower", "tower",
@@ -644,7 +644,7 @@ dependencies = [
"nybbles", "nybbles",
"serde", "serde",
"smallvec", "smallvec",
"thiserror", "thiserror 2.0.18",
"tracing", "tracing",
] ]
@@ -678,6 +678,20 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "arbiter-client" name = "arbiter-client"
version = "0.1.0" version = "0.1.0"
dependencies = [
"alloy",
"arbiter-proto",
"async-trait",
"ed25519-dalek",
"http",
"rand 0.10.0",
"rustls-webpki",
"terrors",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tonic",
]
[[package]] [[package]]
name = "arbiter-proto" name = "arbiter-proto"
@@ -691,12 +705,14 @@ dependencies = [
"miette", "miette",
"prost", "prost",
"prost-types", "prost-types",
"protoc-bin-vendored",
"rand 0.10.0", "rand 0.10.0",
"rcgen", "rcgen",
"rstest", "rstest",
"rustls-pki-types", "rustls-pki-types",
"thiserror", "thiserror 2.0.18",
"tokio", "tokio",
"tokio-stream",
"tonic", "tonic",
"tonic-prost", "tonic-prost",
"tonic-prost-build", "tonic-prost-build",
@@ -720,6 +736,7 @@ dependencies = [
"diesel-async", "diesel-async",
"diesel_migrations", "diesel_migrations",
"ed25519-dalek", "ed25519-dalek",
"fatality",
"futures", "futures",
"insta", "insta",
"k256", "k256",
@@ -737,9 +754,9 @@ dependencies = [
"sha2 0.10.9", "sha2 0.10.9",
"smlang", "smlang",
"spki", "spki",
"strum", "strum 0.28.0",
"test-log", "test-log",
"thiserror", "thiserror 2.0.18",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tonic", "tonic",
@@ -976,7 +993,7 @@ dependencies = [
"nom", "nom",
"num-traits", "num-traits",
"rusticata-macros", "rusticata-macros",
"thiserror", "thiserror 2.0.18",
"time", "time",
] ]
@@ -1061,9 +1078,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "aws-lc-rs" name = "aws-lc-rs"
version = "1.16.1" version = "1.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
dependencies = [ dependencies = [
"aws-lc-sys", "aws-lc-sys",
"untrusted 0.7.1", "untrusted 0.7.1",
@@ -1072,9 +1089,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-lc-sys" name = "aws-lc-sys"
version = "0.38.0" version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a"
dependencies = [ dependencies = [
"cc", "cc",
"cmake", "cmake",
@@ -1269,19 +1286,20 @@ dependencies = [
[[package]] [[package]]
name = "borsh" name = "borsh"
version = "1.6.0" version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
dependencies = [ dependencies = [
"borsh-derive", "borsh-derive",
"bytes",
"cfg_aliases", "cfg_aliases",
] ]
[[package]] [[package]]
name = "borsh-derive" name = "borsh-derive"
version = "1.6.0" version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"proc-macro-crate", "proc-macro-crate",
@@ -1795,15 +1813,16 @@ dependencies = [
[[package]] [[package]]
name = "diesel-async" name = "diesel-async"
version = "0.7.4" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13096fb8dae53f2d411c4b523bec85f45552ed3044a2ab4d85fb2092d9cb4f34" checksum = "b95864e58597509106f1fddfe0600de7e589e1fddddd87f54eee0a49fd111bbc"
dependencies = [ dependencies = [
"bb8", "bb8",
"diesel", "diesel",
"diesel_migrations", "diesel_migrations",
"futures-core", "futures-core",
"futures-util", "futures-util",
"pin-project-lite",
"scoped-futures", "scoped-futures",
"tokio", "tokio",
] ]
@@ -2033,7 +2052,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.52.0",
]
[[package]]
name = "expander"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2c470c71d91ecbd179935b24170459e926382eaaa86b590b78814e180d8a8e2"
dependencies = [
"blake2",
"file-guard",
"fs-err",
"prettyplease",
"proc-macro2",
"quote",
"syn 2.0.117",
] ]
[[package]] [[package]]
@@ -2064,6 +2098,30 @@ dependencies = [
"bytes", "bytes",
] ]
[[package]]
name = "fatality"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec6f82451ff7f0568c6181287189126d492b5654e30a788add08027b6363d019"
dependencies = [
"fatality-proc-macro",
"thiserror 1.0.69",
]
[[package]]
name = "fatality-proc-macro"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb42427514b063d97ce21d5199f36c0c307d981434a6be32582bc79fe5bd2303"
dependencies = [
"expander",
"indexmap 2.13.0",
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "ff" name = "ff"
version = "0.13.1" version = "0.13.1"
@@ -2086,6 +2144,16 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
[[package]]
name = "file-guard"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21ef72acf95ec3d7dbf61275be556299490a245f017cf084bd23b4f68cf9407c"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.9" version = "0.1.9"
@@ -2147,6 +2215,15 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fs-err"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "fs_extra" name = "fs_extra"
version = "1.3.0" version = "1.3.0"
@@ -2795,20 +2872,11 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.17" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "jobserver" name = "jobserver"
@@ -3119,7 +3187,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -3196,9 +3264,9 @@ dependencies = [
[[package]] [[package]]
name = "num_enum" name = "num_enum"
version = "0.7.5" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
dependencies = [ dependencies = [
"num_enum_derive", "num_enum_derive",
"rustversion", "rustversion",
@@ -3206,9 +3274,9 @@ dependencies = [
[[package]] [[package]]
name = "num_enum_derive" name = "num_enum_derive"
version = "0.7.5" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3600,7 +3668,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
dependencies = [ dependencies = [
"heck", "heck",
"itertools 0.14.0", "itertools 0.13.0",
"log", "log",
"multimap", "multimap",
"petgraph", "petgraph",
@@ -3621,7 +3689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools 0.14.0", "itertools 0.13.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn 2.0.117",
@@ -3638,10 +3706,74 @@ dependencies = [
] ]
[[package]] [[package]]
name = "pulldown-cmark" name = "protoc-bin-vendored"
version = "0.13.1" version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6" checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa"
dependencies = [
"protoc-bin-vendored-linux-aarch_64",
"protoc-bin-vendored-linux-ppcle_64",
"protoc-bin-vendored-linux-s390_64",
"protoc-bin-vendored-linux-x86_32",
"protoc-bin-vendored-linux-x86_64",
"protoc-bin-vendored-macos-aarch_64",
"protoc-bin-vendored-macos-x86_64",
"protoc-bin-vendored-win32",
]
[[package]]
name = "protoc-bin-vendored-linux-aarch_64"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c"
[[package]]
name = "protoc-bin-vendored-linux-ppcle_64"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c"
[[package]]
name = "protoc-bin-vendored-linux-s390_64"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0"
[[package]]
name = "protoc-bin-vendored-linux-x86_32"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5"
[[package]]
name = "protoc-bin-vendored-linux-x86_64"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78"
[[package]]
name = "protoc-bin-vendored-macos-aarch_64"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092"
[[package]]
name = "protoc-bin-vendored-macos-x86_64"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756"
[[package]]
name = "protoc-bin-vendored-win32"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3"
[[package]]
name = "pulldown-cmark"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14104c5a24d9bcf7eb2c24753e0f49fe14555d8bd565ea3d38e4b4303267259d"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"memchr", "memchr",
@@ -3677,7 +3809,7 @@ dependencies = [
"rustc-hash", "rustc-hash",
"rustls", "rustls",
"socket2", "socket2",
"thiserror", "thiserror 2.0.18",
"tokio", "tokio",
"tracing", "tracing",
"web-time", "web-time",
@@ -3698,7 +3830,7 @@ dependencies = [
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"slab", "slab",
"thiserror", "thiserror 2.0.18",
"tinyvec", "tinyvec",
"tracing", "tracing",
"web-time", "web-time",
@@ -4033,7 +4165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d"
dependencies = [ dependencies = [
"hashbrown 0.16.1", "hashbrown 0.16.1",
"thiserror", "thiserror 2.0.18",
] ]
[[package]] [[package]]
@@ -4154,7 +4286,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -4185,9 +4317,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.103.9" version = "0.103.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
dependencies = [ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"ring", "ring",
@@ -4570,7 +4702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -4631,7 +4763,16 @@ version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [ dependencies = [
"strum_macros", "strum_macros 0.27.2",
]
[[package]]
name = "strum"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
dependencies = [
"strum_macros 0.28.0",
] ]
[[package]] [[package]]
@@ -4646,6 +4787,18 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "strum_macros"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@@ -4743,7 +4896,7 @@ dependencies = [
"getrandom 0.4.2", "getrandom 0.4.2",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -4756,6 +4909,11 @@ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]]
name = "terrors"
version = "0.5.1"
source = "git+https://github.com/CleverWild/terrors#a0867fd9ca3fbb44c32e92113a917f1577b5716a"
[[package]] [[package]]
name = "test-log" name = "test-log"
version = "0.2.19" version = "0.2.19"
@@ -4787,13 +4945,33 @@ dependencies = [
"unicode-width 0.2.2", "unicode-width 0.2.2",
] ]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.18" version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl 2.0.18",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
] ]
[[package]] [[package]]
@@ -4955,7 +5133,7 @@ dependencies = [
"serde_spanned", "serde_spanned",
"toml_datetime 0.7.5+spec-1.1.0", "toml_datetime 0.7.5+spec-1.1.0",
"toml_parser", "toml_parser",
"winnow", "winnow 0.7.15",
] ]
[[package]] [[package]]
@@ -4969,32 +5147,32 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "1.0.0+spec-1.1.0" version = "1.0.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.25.4+spec-1.1.0" version = "0.25.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
dependencies = [ dependencies = [
"indexmap 2.13.0", "indexmap 2.13.0",
"toml_datetime 1.0.0+spec-1.1.0", "toml_datetime 1.0.1+spec-1.1.0",
"toml_parser", "toml_parser",
"winnow", "winnow 1.0.0",
] ]
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.0.9+spec-1.1.0" version = "1.0.10+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
dependencies = [ dependencies = [
"winnow", "winnow 1.0.0",
] ]
[[package]] [[package]]
@@ -5758,6 +5936,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winnow"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.51.0" version = "0.51.0"
@@ -5887,7 +6074,7 @@ dependencies = [
"nom", "nom",
"oid-registry", "oid-registry",
"rusticata-macros", "rusticata-macros",
"thiserror", "thiserror 2.0.18",
"time", "time",
] ]
@@ -5925,18 +6112,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.42" version = "0.8.47"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.42" version = "0.8.47"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@@ -1,7 +1,5 @@
[workspace] [workspace]
members = [ members = ["crates/*"]
"crates/*",
]
resolver = "3" resolver = "3"
[workspace.lints.clippy] [workspace.lints.clippy]
@@ -9,23 +7,23 @@ disallowed-methods = "deny"
[workspace.dependencies] [workspace.dependencies]
tonic = { version = "0.14.3", features = [ tonic = { version = "0.14.5", features = [
"deflate", "deflate",
"gzip", "gzip",
"tls-connect-info", "tls-connect-info",
"zstd", "zstd",
] } ] }
tracing = "0.1.44" tracing = "0.1.44"
tokio = { version = "1.49.0", features = ["full"] } tokio = { version = "1.50.0", features = ["full"] }
ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] } ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] }
chrono = { version = "0.4.43", features = ["serde"] } chrono = { version = "0.4.44", features = ["serde"] }
rand = "0.10.0" rand = "0.10.0"
rustls = { version = "0.23.36", features = ["aws-lc-rs"] } rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
smlang = "0.8.0" smlang = "0.8.0"
miette = { version = "7.6.0", features = ["fancy", "serde"] } miette = { version = "7.6.0", features = ["fancy", "serde"] }
thiserror = "2.0.18" thiserror = "2.0.18"
async-trait = "0.1.89" async-trait = "0.1.89"
futures = "0.3.31" futures = "0.3.32"
tokio-stream = { version = "0.1.18", features = ["full"] } tokio-stream = { version = "0.1.18", features = ["full"] }
kameo = "0.19.2" kameo = "0.19.2"
prost-types = { version = "0.14.3", features = ["chrono"] } prost-types = { version = "0.14.3", features = ["chrono"] }
@@ -43,3 +41,4 @@ k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] }
rsa = { version = "0.9", features = ["sha2"] } rsa = { version = "0.9", features = ["sha2"] }
sha2 = "0.10" sha2 = "0.10"
spki = "0.7" spki = "0.7"
terrors = { version = "0.5", git = "https://github.com/CleverWild/terrors" }

View File

@@ -5,4 +5,23 @@ edition = "2024"
repository = "https://git.markettakers.org/MarketTakers/arbiter" repository = "https://git.markettakers.org/MarketTakers/arbiter"
license = "Apache-2.0" license = "Apache-2.0"
[lints]
workspace = true
[features]
evm = ["dep:alloy"]
[dependencies] [dependencies]
arbiter-proto.path = "../arbiter-proto"
alloy = { workspace = true, optional = true }
tonic.workspace = true
tonic.features = ["tls-aws-lc"]
tokio.workspace = true
tokio-stream.workspace = true
ed25519-dalek.workspace = true
thiserror.workspace = true
http = "1.4.0"
rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] }
async-trait.workspace = true
rand.workspace = true
terrors.workspace = true

View File

@@ -0,0 +1,103 @@
use arbiter_proto::{
format_challenge,
proto::client::{
AuthChallengeRequest, AuthChallengeSolution, AuthResult, ClientRequest,
client_request::Payload as ClientRequestPayload,
client_response::Payload as ClientResponsePayload,
},
};
use ed25519_dalek::Signer as _;
use terrors::OneOf;
use crate::{
errors::{
ConnectError, MissingAuthChallengeError, UnexpectedAuthResponseError, map_auth_code_error,
},
transport::{ClientTransport, next_request_id},
};
async fn send_auth_challenge_request(
transport: &mut ClientTransport,
key: &ed25519_dalek::SigningKey,
) -> std::result::Result<(), ConnectError> {
transport
.send(ClientRequest {
request_id: next_request_id(),
payload: Some(ClientRequestPayload::AuthChallengeRequest(
AuthChallengeRequest {
pubkey: key.verifying_key().to_bytes().to_vec(),
},
)),
})
.await
.map_err(|_| OneOf::new(UnexpectedAuthResponseError))
}
async fn receive_auth_challenge(
transport: &mut ClientTransport,
) -> std::result::Result<arbiter_proto::proto::client::AuthChallenge, ConnectError> {
let response = transport
.recv()
.await
.map_err(|_| OneOf::new(MissingAuthChallengeError))?;
let payload = response
.payload
.ok_or_else(|| OneOf::new(MissingAuthChallengeError))?;
match payload {
ClientResponsePayload::AuthChallenge(challenge) => Ok(challenge),
ClientResponsePayload::AuthResult(result) => Err(map_auth_code_error(result)),
_ => Err(OneOf::new(UnexpectedAuthResponseError)),
}
}
async fn send_auth_challenge_solution(
transport: &mut ClientTransport,
key: &ed25519_dalek::SigningKey,
challenge: arbiter_proto::proto::client::AuthChallenge,
) -> std::result::Result<(), ConnectError> {
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
let signature = key.sign(&challenge_payload).to_bytes().to_vec();
transport
.send(ClientRequest {
request_id: next_request_id(),
payload: Some(ClientRequestPayload::AuthChallengeSolution(
AuthChallengeSolution { signature },
)),
})
.await
.map_err(|_| OneOf::new(UnexpectedAuthResponseError))
}
async fn receive_auth_confirmation(
transport: &mut ClientTransport,
) -> std::result::Result<(), ConnectError> {
let response = transport
.recv()
.await
.map_err(|_| OneOf::new(UnexpectedAuthResponseError))?;
let payload = response
.payload
.ok_or_else(|| OneOf::new(UnexpectedAuthResponseError))?;
match payload {
ClientResponsePayload::AuthResult(result)
if AuthResult::try_from(result).ok() == Some(AuthResult::Success) =>
{
Ok(())
}
ClientResponsePayload::AuthResult(result) => Err(map_auth_code_error(result)),
_ => Err(OneOf::new(UnexpectedAuthResponseError)),
}
}
pub(crate) async fn authenticate(
transport: &mut ClientTransport,
key: &ed25519_dalek::SigningKey,
) -> std::result::Result<(), ConnectError> {
send_auth_challenge_request(transport, key).await?;
let challenge = receive_auth_challenge(transport).await?;
send_auth_challenge_solution(transport, key, challenge).await?;
receive_auth_confirmation(transport).await
}

View File

@@ -0,0 +1,82 @@
use arbiter_proto::{proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl};
use std::sync::Arc;
use terrors::{Broaden as _, OneOf};
use tokio::sync::{Mutex, mpsc};
use tokio_stream::wrappers::ReceiverStream;
use tonic::transport::ClientTlsConfig;
use crate::{
auth::authenticate,
errors::ConnectError,
storage::{FileSigningKeyStorage, SigningKeyStorage},
transport::{BUFFER_LENGTH, ClientTransport},
};
#[cfg(feature = "evm")]
use crate::errors::{ClientConnectionClosedError, ClientError};
#[cfg(feature = "evm")]
use crate::wallets::evm::ArbiterEvmWallet;
pub struct ArbiterClient {
#[allow(dead_code)]
transport: Arc<Mutex<ClientTransport>>,
}
impl ArbiterClient {
pub async fn connect(url: ArbiterUrl) -> Result<Self, ConnectError> {
let storage = FileSigningKeyStorage::from_default_location().broaden()?;
Self::connect_with_storage(url, &storage).await
}
pub async fn connect_with_storage<S: SigningKeyStorage>(
url: ArbiterUrl,
storage: &S,
) -> Result<Self, ConnectError> {
let key = storage.load_or_create().broaden()?;
Self::connect_with_key(url, key).await
}
pub async fn connect_with_key(
url: ArbiterUrl,
key: ed25519_dalek::SigningKey,
) -> Result<Self, ConnectError> {
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)
.map_err(OneOf::new)?
.to_owned();
let tls = ClientTlsConfig::new().trust_anchor(anchor);
let channel = tonic::transport::Channel::from_shared(format!("{}:{}", url.host, url.port))
.map_err(OneOf::new)?
.tls_config(tls)
.map_err(OneOf::new)?
.connect()
.await
.map_err(OneOf::new)?;
let mut client = ArbiterServiceClient::new(channel);
let (tx, rx) = mpsc::channel(BUFFER_LENGTH);
let response_stream = client
.client(ReceiverStream::new(rx))
.await
.map_err(OneOf::new)?
.into_inner();
let mut transport = ClientTransport {
sender: tx,
receiver: response_stream,
};
authenticate(&mut transport, &key).await?;
Ok(Self {
transport: Arc::new(Mutex::new(transport)),
})
}
#[cfg(feature = "evm")]
pub async fn evm_wallets(&self) -> Result<Vec<ArbiterEvmWallet>, ClientError> {
let _ = &self.transport;
Err(OneOf::new(ClientConnectionClosedError))
}
}

View File

@@ -0,0 +1,127 @@
use terrors::OneOf;
use thiserror::Error;
#[cfg(feature = "evm")]
use alloy::{primitives::ChainId, signers::Error as AlloySignerError};
pub type StorageError = OneOf<(std::io::Error, InvalidKeyLengthError)>;
pub type ConnectError = OneOf<(
tonic::transport::Error,
http::uri::InvalidUri,
webpki::Error,
tonic::Status,
MissingAuthChallengeError,
ApprovalDeniedError,
NoUserAgentsOnlineError,
UnexpectedAuthResponseError,
std::io::Error,
InvalidKeyLengthError,
)>;
pub type ClientError = OneOf<(tonic::Status, ClientConnectionClosedError)>;
pub(crate) type ClientTransportError =
OneOf<(TransportChannelClosedError, TransportConnectionClosedError)>;
#[cfg(feature = "evm")]
pub(crate) type EvmWalletError = OneOf<(
EvmChainIdMismatchError,
EvmHashSigningUnsupportedError,
EvmTransactionSigningUnsupportedError,
)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Invalid signing key length in storage: expected {expected} bytes, got {actual} bytes")]
pub struct InvalidKeyLengthError {
pub expected: usize,
pub actual: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Auth challenge was not returned by server")]
pub struct MissingAuthChallengeError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Client approval denied by User Agent")]
pub struct ApprovalDeniedError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("No User Agents online to approve client")]
pub struct NoUserAgentsOnlineError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Unexpected auth response payload")]
pub struct UnexpectedAuthResponseError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Connection closed by server")]
pub struct ClientConnectionClosedError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Transport channel closed")]
pub struct TransportChannelClosedError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Connection closed by server")]
pub struct TransportConnectionClosedError;
#[cfg(feature = "evm")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Transaction chain id mismatch: signer {signer}, tx {tx}")]
pub struct EvmChainIdMismatchError {
pub signer: ChainId,
pub tx: ChainId,
}
#[cfg(feature = "evm")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("hash-only signing is not supported for ArbiterEvmWallet; use transaction signing")]
pub struct EvmHashSigningUnsupportedError;
#[cfg(feature = "evm")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("transaction signing is not supported by current arbiter.client protocol")]
pub struct EvmTransactionSigningUnsupportedError;
pub(crate) fn map_auth_code_error(code: i32) -> ConnectError {
use arbiter_proto::proto::client::AuthResult;
match AuthResult::try_from(code).unwrap_or(AuthResult::Unspecified) {
AuthResult::ApprovalDenied => OneOf::new(ApprovalDeniedError),
AuthResult::NoUserAgentsOnline => OneOf::new(NoUserAgentsOnlineError),
AuthResult::Unspecified
| AuthResult::Success
| AuthResult::InvalidKey
| AuthResult::InvalidSignature
| AuthResult::Internal => OneOf::new(UnexpectedAuthResponseError),
}
}
#[cfg(feature = "evm")]
impl From<EvmChainIdMismatchError> for AlloySignerError {
fn from(value: EvmChainIdMismatchError) -> Self {
AlloySignerError::TransactionChainIdMismatch {
signer: value.signer,
tx: value.tx,
}
}
}
#[cfg(feature = "evm")]
impl From<EvmHashSigningUnsupportedError> for AlloySignerError {
fn from(_value: EvmHashSigningUnsupportedError) -> Self {
AlloySignerError::other(
"hash-only signing is not supported for ArbiterEvmWallet; use transaction signing",
)
}
}
#[cfg(feature = "evm")]
impl From<EvmTransactionSigningUnsupportedError> for AlloySignerError {
fn from(_value: EvmTransactionSigningUnsupportedError) -> Self {
AlloySignerError::other(
"transaction signing is not supported by current arbiter.client protocol",
)
}
}

View File

@@ -1,14 +1,13 @@
pub fn add(left: u64, right: u64) -> u64 { mod auth;
left + right mod client;
} mod errors;
mod storage;
mod transport;
pub mod wallets;
#[cfg(test)] pub use client::ArbiterClient;
mod tests { pub use errors::{ClientError, ConnectError, StorageError};
use super::*; pub use storage::{FileSigningKeyStorage, SigningKeyStorage};
#[test] #[cfg(feature = "evm")]
fn it_works() { pub use wallets::evm::ArbiterEvmWallet;
let result = add(2, 2);
assert_eq!(result, 4);
}
}

View File

@@ -0,0 +1,130 @@
use arbiter_proto::home_path;
use std::path::{Path, PathBuf};
use terrors::OneOf;
use crate::errors::{InvalidKeyLengthError, StorageError};
pub trait SigningKeyStorage {
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError>;
}
#[derive(Debug, Clone)]
pub struct FileSigningKeyStorage {
path: PathBuf,
}
impl FileSigningKeyStorage {
pub const DEFAULT_FILE_NAME: &str = "sdk_client_ed25519.key";
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn from_default_location() -> std::result::Result<Self, StorageError> {
Ok(Self::new(
home_path()
.map_err(OneOf::new)?
.join(Self::DEFAULT_FILE_NAME),
))
}
fn read_key(path: &Path) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
let bytes = std::fs::read(path).map_err(OneOf::new)?;
let raw: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
OneOf::new(InvalidKeyLengthError {
expected: 32,
actual: v.len(),
})
})?;
Ok(ed25519_dalek::SigningKey::from_bytes(&raw))
}
}
impl SigningKeyStorage for FileSigningKeyStorage {
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent).map_err(OneOf::new)?;
}
if self.path.exists() {
return Self::read_key(&self.path);
}
let key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let raw_key = key.to_bytes();
// Use create_new to prevent accidental overwrite if another process creates the key first.
match std::fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&self.path)
{
Ok(mut file) => {
use std::io::Write as _;
file.write_all(&raw_key).map_err(OneOf::new)?;
Ok(key)
}
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
Self::read_key(&self.path)
}
Err(err) => Err(OneOf::new(err)),
}
}
}
#[cfg(test)]
mod tests {
use super::{FileSigningKeyStorage, SigningKeyStorage};
use crate::errors::InvalidKeyLengthError;
fn unique_temp_key_path() -> std::path::PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be after unix epoch")
.as_nanos();
std::env::temp_dir().join(format!(
"arbiter-client-key-{}-{}.bin",
std::process::id(),
nanos
))
}
#[test]
fn file_storage_creates_and_reuses_key() {
let path = unique_temp_key_path();
let storage = FileSigningKeyStorage::new(path.clone());
let key_a = storage
.load_or_create()
.expect("first load_or_create should create key");
let key_b = storage
.load_or_create()
.expect("second load_or_create should read same key");
assert_eq!(key_a.to_bytes(), key_b.to_bytes());
assert!(path.exists());
std::fs::remove_file(path).expect("temp key file should be removable");
}
#[test]
fn file_storage_rejects_invalid_key_length() {
let path = unique_temp_key_path();
std::fs::write(&path, [42u8; 31]).expect("should write invalid key file");
let storage = FileSigningKeyStorage::new(path.clone());
let err = storage
.load_or_create()
.expect_err("storage should reject non-32-byte key file");
match err.narrow::<InvalidKeyLengthError, _>() {
Ok(invalid_len) => {
assert_eq!(invalid_len.expected, 32);
assert_eq!(invalid_len.actual, 31);
}
Err(other) => panic!("unexpected io error: {other:?}"),
}
std::fs::remove_file(path).expect("temp key file should be removable");
}
}

View File

@@ -0,0 +1,42 @@
use arbiter_proto::proto::client::{ClientRequest, ClientResponse};
use std::sync::atomic::{AtomicI32, Ordering};
use terrors::OneOf;
use tokio::sync::mpsc;
use crate::errors::{
ClientTransportError, TransportChannelClosedError, TransportConnectionClosedError,
};
pub(crate) const BUFFER_LENGTH: usize = 16;
static NEXT_REQUEST_ID: AtomicI32 = AtomicI32::new(1);
pub(crate) fn next_request_id() -> i32 {
NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed)
}
pub(crate) struct ClientTransport {
pub(crate) sender: mpsc::Sender<ClientRequest>,
pub(crate) receiver: tonic::Streaming<ClientResponse>,
}
impl ClientTransport {
pub(crate) async fn send(
&mut self,
request: ClientRequest,
) -> std::result::Result<(), ClientTransportError> {
self.sender
.send(request)
.await
.map_err(|_| OneOf::new(TransportChannelClosedError))
}
pub(crate) async fn recv(
&mut self,
) -> std::result::Result<ClientResponse, ClientTransportError> {
match self.receiver.message().await {
Ok(Some(resp)) => Ok(resp),
Ok(None) => Err(OneOf::new(TransportConnectionClosedError)),
Err(_) => Err(OneOf::new(TransportConnectionClosedError)),
}
}
}

View File

@@ -0,0 +1,97 @@
use alloy::{
consensus::SignableTransaction,
network::TxSigner,
primitives::{Address, B256, ChainId, Signature},
signers::{Result, Signer},
};
use async_trait::async_trait;
use std::sync::Arc;
use terrors::OneOf;
use tokio::sync::Mutex;
use crate::{
errors::{
EvmChainIdMismatchError, EvmHashSigningUnsupportedError,
EvmTransactionSigningUnsupportedError, EvmWalletError,
},
transport::ClientTransport,
};
pub struct ArbiterEvmWallet {
transport: Arc<Mutex<ClientTransport>>,
address: Address,
chain_id: Option<ChainId>,
}
impl ArbiterEvmWallet {
#[allow(dead_code)]
pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self {
Self {
transport,
address,
chain_id: None,
}
}
pub fn address(&self) -> Address {
self.address
}
pub fn with_chain_id(mut self, chain_id: ChainId) -> Self {
self.chain_id = Some(chain_id);
self
}
fn validate_chain_id(
&self,
tx: &mut dyn SignableTransaction<Signature>,
) -> std::result::Result<(), EvmWalletError> {
if let Some(chain_id) = self.chain_id
&& !tx.set_chain_id_checked(chain_id)
{
return Err(OneOf::new(EvmChainIdMismatchError {
signer: chain_id,
tx: tx.chain_id().unwrap(),
}));
}
Ok(())
}
}
#[async_trait]
impl Signer for ArbiterEvmWallet {
async fn sign_hash(&self, _hash: &B256) -> Result<Signature> {
Err(EvmWalletError::new(EvmHashSigningUnsupportedError).into())
}
fn address(&self) -> Address {
self.address
}
fn chain_id(&self) -> Option<ChainId> {
self.chain_id
}
fn set_chain_id(&mut self, chain_id: Option<ChainId>) {
self.chain_id = chain_id;
}
}
#[async_trait]
impl TxSigner<Signature> for ArbiterEvmWallet {
fn address(&self) -> Address {
self.address
}
async fn sign_transaction(
&self,
tx: &mut dyn SignableTransaction<Signature>,
) -> Result<Signature> {
let _transport = self.transport.lock().await;
self.validate_chain_id(tx)
.map_err(OneOf::into::<alloy::signers::Error>)?;
Err(EvmWalletError::new(EvmTransactionSigningUnsupportedError).into())
}
}

View File

@@ -0,0 +1,2 @@
#[cfg(feature = "evm")]
pub mod evm;

View File

@@ -10,7 +10,7 @@ tonic.workspace = true
tokio.workspace = true tokio.workspace = true
futures.workspace = true futures.workspace = true
hex = "0.4.3" hex = "0.4.3"
tonic-prost = "0.14.3" tonic-prost = "0.14.5"
prost = "0.14.3" prost = "0.14.3"
kameo.workspace = true kameo.workspace = true
url = "2.5.8" url = "2.5.8"
@@ -21,9 +21,11 @@ base64 = "0.22.1"
prost-types.workspace = true prost-types.workspace = true
tracing.workspace = true tracing.workspace = true
async-trait.workspace = true async-trait.workspace = true
tokio-stream.workspace = true
[build-dependencies] [build-dependencies]
tonic-prost-build = "0.14.3" tonic-prost-build = "0.14.5"
protoc-bin-vendored = "3"
[dev-dependencies] [dev-dependencies]
rstest.workspace = true rstest.workspace = true
@@ -32,5 +34,3 @@ rcgen.workspace = true
[package.metadata.cargo-shear] [package.metadata.cargo-shear]
ignored = ["tonic-prost", "prost", "kameo"] ignored = ["tonic-prost", "prost", "kameo"]

View File

@@ -1,21 +1,32 @@
use std::path::PathBuf;
use tonic_prost_build::configure; use tonic_prost_build::configure;
static PROTOBUF_DIR: &str = "../../../protobufs"; static PROTOBUF_DIR: &str = "../../../protobufs";
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo::rerun-if-changed={PROTOBUF_DIR}"); let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
let protobuf_dir = manifest_dir.join(PROTOBUF_DIR);
let protoc_include = protoc_bin_vendored::include_path()?;
let protoc_path = protoc_bin_vendored::protoc_bin_path()?;
unsafe {
std::env::set_var("PROTOC", &protoc_path);
std::env::set_var("PROTOC_INCLUDE", &protoc_include);
}
println!("cargo::rerun-if-changed={}", protobuf_dir.display());
configure() configure()
.message_attribute(".", "#[derive(::kameo::Reply)]") .message_attribute(".", "#[derive(::kameo::Reply)]")
.compile_well_known_types(true)
.compile_protos( .compile_protos(
&[ &[
format!("{}/arbiter.proto", PROTOBUF_DIR), protobuf_dir.join("arbiter.proto"),
format!("{}/user_agent.proto", PROTOBUF_DIR), protobuf_dir.join("user_agent.proto"),
format!("{}/client.proto", PROTOBUF_DIR), protobuf_dir.join("client.proto"),
format!("{}/evm.proto", PROTOBUF_DIR), protobuf_dir.join("evm.proto"),
], ],
&[PROTOBUF_DIR.to_string()], &[protobuf_dir],
) )?;
.unwrap();
Ok(()) Ok(())
} }

View File

@@ -3,6 +3,12 @@ pub mod url;
use base64::{Engine, prelude::BASE64_STANDARD}; use base64::{Engine, prelude::BASE64_STANDARD};
pub mod google {
pub mod protobuf {
tonic::include_proto!("google.protobuf");
}
}
pub mod proto { pub mod proto {
tonic::include_proto!("arbiter"); tonic::include_proto!("arbiter");

View File

@@ -1,29 +1,49 @@
//! Transport-facing abstractions shared by protocol/session code. //! Transport-facing abstractions shared by protocol/session code.
//! //!
//! This module defines a small duplex interface, [`Bi`], that actors and other //! This module defines a small set of transport traits that actors and other
//! protocol code can depend on without knowing anything about the concrete //! protocol code can depend on without knowing anything about the concrete
//! transport underneath. //! transport underneath.
//! //!
//! [`Bi`] is intentionally minimal and transport-agnostic: //! The abstraction is split into:
//! - [`Bi::recv`] yields inbound messages //! - [`Sender`] for outbound delivery
//! - [`Bi::send`] accepts outbound messages //! - [`Receiver`] for inbound delivery
//! - [`Bi`] as the combined duplex form (`Sender + Receiver`)
//!
//! This split lets code depend only on the half it actually needs. For
//! example, some actor/session code only sends out-of-band messages, while
//! auth/state-machine code may need full duplex access.
//!
//! [`Bi`] remains intentionally minimal and transport-agnostic:
//! - [`Receiver::recv`] yields inbound messages
//! - [`Sender::send`] accepts outbound messages
//! //!
//! Transport-specific adapters, including protobuf or gRPC bridges, live in the //! Transport-specific adapters, including protobuf or gRPC bridges, live in the
//! crates that own those boundaries rather than in `arbiter-proto`. //! crates that own those boundaries rather than in `arbiter-proto`.
//! //!
//! [`Bi`] deliberately does not model request/response correlation. Some
//! transports may carry multiplexed request/response traffic, some may emit
//! out-of-band messages, and some may be one-message-at-a-time state machines.
//! Correlation concerns such as request IDs, pending response maps, and
//! out-of-band routing belong in the adapter or connection layer built on top
//! of [`Bi`], not in this abstraction itself.
//!
//! # Generic Ordering Rule //! # Generic Ordering Rule
//! //!
//! This module consistently uses `Inbound` first and `Outbound` second in //! This module consistently uses `Inbound` first and `Outbound` second in
//! generic parameter lists. //! generic parameter lists.
//! //!
//! For [`Bi`], that means `Bi<Inbound, Outbound>`: //! For [`Receiver`], [`Sender`], and [`Bi`], this means:
//! - `Receiver<Inbound>`
//! - `Sender<Outbound>`
//! - `Bi<Inbound, Outbound>`
//!
//! Concretely, for [`Bi`]:
//! - `recv() -> Option<Inbound>` //! - `recv() -> Option<Inbound>`
//! - `send(Outbound)` //! - `send(Outbound)`
//! //!
//! [`expect_message`] is a small helper for request/response style flows: it //! [`expect_message`] is a small helper for linear protocol steps: it reads one
//! reads one inbound message from a transport and extracts a typed value from //! inbound message from a transport and extracts a typed value from it, failing
//! it, failing if the channel closes or the message shape is not what the //! if the channel closes or the message shape is not what the caller expected.
//! caller expected.
//! //!
//! [`DummyTransport`] is a no-op implementation useful for tests and local //! [`DummyTransport`] is a no-op implementation useful for tests and local
//! actor execution where no real stream exists. //! actor execution where no real stream exists.
@@ -63,16 +83,35 @@ where
extractor(msg).ok_or(Error::UnexpectedMessage) extractor(msg).ok_or(Error::UnexpectedMessage)
} }
#[async_trait]
pub trait Sender<Outbound>: Send + Sync {
async fn send(&mut self, item: Outbound) -> Result<(), Error>;
}
#[async_trait]
pub trait Receiver<Inbound>: Send + Sync {
async fn recv(&mut self) -> Option<Inbound>;
}
/// Minimal bidirectional transport abstraction used by protocol code. /// Minimal bidirectional transport abstraction used by protocol code.
/// ///
/// `Bi<Inbound, Outbound>` models a duplex channel with: /// `Bi<Inbound, Outbound>` is the combined duplex form of [`Sender`] and
/// [`Receiver`].
///
/// It models a channel with:
/// - inbound items of type `Inbound` read via [`Bi::recv`] /// - inbound items of type `Inbound` read via [`Bi::recv`]
/// - outbound items of type `Outbound` written via [`Bi::send`] /// - outbound items of type `Outbound` written via [`Bi::send`]
#[async_trait] ///
pub trait Bi<Inbound, Outbound>: Send + Sync + 'static { /// It does not imply request/response sequencing, one-at-a-time exchange, or
async fn send(&mut self, item: Outbound) -> Result<(), Error>; /// any built-in correlation mechanism between inbound and outbound items.
pub trait Bi<Inbound, Outbound>: Sender<Outbound> + Receiver<Inbound> + Send + Sync {}
async fn recv(&mut self) -> Option<Inbound>; pub trait SplittableBi<Inbound, Outbound>: Bi<Inbound, Outbound> {
type Sender: Sender<Outbound>;
type Receiver: Receiver<Inbound>;
fn split(self) -> (Self::Sender, Self::Receiver);
fn from_parts(sender: Self::Sender, receiver: Self::Receiver) -> Self;
} }
/// No-op [`Bi`] transport for tests and manual actor usage. /// No-op [`Bi`] transport for tests and manual actor usage.
@@ -83,22 +122,16 @@ pub struct DummyTransport<Inbound, Outbound> {
_marker: PhantomData<(Inbound, Outbound)>, _marker: PhantomData<(Inbound, Outbound)>,
} }
impl<Inbound, Outbound> DummyTransport<Inbound, Outbound> { impl<Inbound, Outbound> Default for DummyTransport<Inbound, Outbound> {
pub fn new() -> Self { fn default() -> Self {
Self { Self {
_marker: PhantomData, _marker: PhantomData,
} }
} }
} }
impl<Inbound, Outbound> Default for DummyTransport<Inbound, Outbound> {
fn default() -> Self {
Self::new()
}
}
#[async_trait] #[async_trait]
impl<Inbound, Outbound> Bi<Inbound, Outbound> for DummyTransport<Inbound, Outbound> impl<Inbound, Outbound> Sender<Outbound> for DummyTransport<Inbound, Outbound>
where where
Inbound: Send + Sync + 'static, Inbound: Send + Sync + 'static,
Outbound: Send + Sync + 'static, Outbound: Send + Sync + 'static,
@@ -106,9 +139,25 @@ where
async fn send(&mut self, _item: Outbound) -> Result<(), Error> { async fn send(&mut self, _item: Outbound) -> Result<(), Error> {
Ok(()) Ok(())
} }
}
#[async_trait]
impl<Inbound, Outbound> Receiver<Inbound> for DummyTransport<Inbound, Outbound>
where
Inbound: Send + Sync + 'static,
Outbound: Send + Sync + 'static,
{
async fn recv(&mut self) -> Option<Inbound> { async fn recv(&mut self) -> Option<Inbound> {
std::future::pending::<()>().await; std::future::pending::<()>().await;
None None
} }
} }
impl<Inbound, Outbound> Bi<Inbound, Outbound> for DummyTransport<Inbound, Outbound>
where
Inbound: Send + Sync + 'static,
Outbound: Send + Sync + 'static,
{
}
pub mod grpc;

View File

@@ -0,0 +1,106 @@
use async_trait::async_trait;
use futures::StreamExt;
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use super::{Bi, Receiver, Sender};
pub struct GrpcSender<Outbound> {
tx: mpsc::Sender<Result<Outbound, tonic::Status>>,
}
#[async_trait]
impl<Outbound> Sender<Result<Outbound, tonic::Status>> for GrpcSender<Outbound>
where
Outbound: Send + Sync + 'static,
{
async fn send(&mut self, item: Result<Outbound, tonic::Status>) -> Result<(), super::Error> {
self.tx
.send(item)
.await
.map_err(|_| super::Error::ChannelClosed)
}
}
pub struct GrpcReceiver<Inbound> {
rx: tonic::Streaming<Inbound>,
}
#[async_trait]
impl<Inbound> Receiver<Result<Inbound, tonic::Status>> for GrpcReceiver<Inbound>
where
Inbound: Send + Sync + 'static,
{
async fn recv(&mut self) -> Option<Result<Inbound, tonic::Status>> {
self.rx.next().await
}
}
pub struct GrpcBi<Inbound, Outbound> {
sender: GrpcSender<Outbound>,
receiver: GrpcReceiver<Inbound>,
}
impl<Inbound, Outbound> GrpcBi<Inbound, Outbound>
where
Inbound: Send + Sync + 'static,
Outbound: Send + Sync + 'static,
{
pub fn from_bi_stream(
receiver: tonic::Streaming<Inbound>,
) -> (Self, ReceiverStream<Result<Outbound, tonic::Status>>) {
let (tx, rx) = mpsc::channel(10);
let sender = GrpcSender { tx };
let receiver = GrpcReceiver { rx: receiver };
let bi = GrpcBi { sender, receiver };
(bi, ReceiverStream::new(rx))
}
}
#[async_trait]
impl<Inbound, Outbound> Sender<Result<Outbound, tonic::Status>> for GrpcBi<Inbound, Outbound>
where
Inbound: Send + Sync + 'static,
Outbound: Send + Sync + 'static,
{
async fn send(&mut self, item: Result<Outbound, tonic::Status>) -> Result<(), super::Error> {
self.sender.send(item).await
}
}
#[async_trait]
impl<Inbound, Outbound> Receiver<Result<Inbound, tonic::Status>> for GrpcBi<Inbound, Outbound>
where
Inbound: Send + Sync + 'static,
Outbound: Send + Sync + 'static,
{
async fn recv(&mut self) -> Option<Result<Inbound, tonic::Status>> {
self.receiver.recv().await
}
}
impl<Inbound, Outbound> Bi<Result<Inbound, tonic::Status>, Result<Outbound, tonic::Status>>
for GrpcBi<Inbound, Outbound>
where
Inbound: Send + Sync + 'static,
Outbound: Send + Sync + 'static,
{
}
impl<Inbound, Outbound>
super::SplittableBi<Result<Inbound, tonic::Status>, Result<Outbound, tonic::Status>>
for GrpcBi<Inbound, Outbound>
where
Inbound: Send + Sync + 'static,
Outbound: Send + Sync + 'static,
{
type Sender = GrpcSender<Outbound>;
type Receiver = GrpcReceiver<Inbound>;
fn split(self) -> (Self::Sender, Self::Receiver) {
(self.sender, self.receiver)
}
fn from_parts(sender: Self::Sender, receiver: Self::Receiver) -> Self {
GrpcBi { sender, receiver }
}
}

View File

@@ -9,8 +9,8 @@ license = "Apache-2.0"
workspace = true workspace = true
[dependencies] [dependencies]
diesel = { version = "2.3.6", features = ["chrono", "returning_clauses_for_sqlite_3_35", "serde_json", "time", "uuid"] } diesel = { version = "2.3.7", features = ["chrono", "returning_clauses_for_sqlite_3_35", "serde_json", "time", "uuid"] }
diesel-async = { version = "0.7.4", features = [ diesel-async = { version = "0.8.0", features = [
"bb8", "bb8",
"migrations", "migrations",
"sqlite", "sqlite",
@@ -27,6 +27,7 @@ rustls.workspace = true
smlang.workspace = true smlang.workspace = true
miette.workspace = true miette.workspace = true
thiserror.workspace = true thiserror.workspace = true
fatality = "0.1.1"
diesel_migrations = { version = "2.3.1", features = ["sqlite"] } diesel_migrations = { version = "2.3.1", features = ["sqlite"] }
async-trait.workspace = true async-trait.workspace = true
secrecy = "0.10.3" secrecy = "0.10.3"
@@ -43,7 +44,7 @@ x25519-dalek.workspace = true
chacha20poly1305 = { version = "0.10.1", features = ["std"] } chacha20poly1305 = { version = "0.10.1", features = ["std"] }
argon2 = { version = "0.5.3", features = ["zeroize"] } argon2 = { version = "0.5.3", features = ["zeroize"] }
restructed = "0.2.2" restructed = "0.2.2"
strum = { version = "0.27.2", features = ["derive"] } strum = { version = "0.28.0", features = ["derive"] }
pem = "3.0.6" pem = "3.0.6"
k256.workspace = true k256.workspace = true
rsa.workspace = true rsa.workspace = true

View File

@@ -157,3 +157,5 @@ create table if not exists evm_ether_transfer_grant_target (
create unique index if not exists uniq_ether_transfer_target on evm_ether_transfer_grant_target(grant_id, address); create unique index if not exists uniq_ether_transfer_target on evm_ether_transfer_grant_target(grant_id, address);
CREATE UNIQUE INDEX program_client_public_key_unique
ON program_client (public_key);

View File

@@ -1,30 +1,25 @@
use arbiter_proto::{format_challenge, transport::expect_message}; use arbiter_proto::{
format_challenge,
transport::{Bi, expect_message},
};
use diesel::{ use diesel::{
ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, dsl::insert_into, update, ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, dsl::insert_into, update,
}; };
use diesel_async::RunQueryDsl as _; use diesel_async::RunQueryDsl as _;
use ed25519_dalek::VerifyingKey; use ed25519_dalek::{Signature, VerifyingKey};
use kameo::error::SendError; use kameo::error::SendError;
use tracing::error; use tracing::error;
use crate::{ use crate::{
actors::{ actors::{
client::{ClientConnection, ConnectErrorCode, Request, Response}, client::ClientConnection,
router::{self, RequestClientApproval}, router::{self, RequestClientApproval},
}, },
db::{self, schema::program_client}, db::{self, schema::program_client},
}; };
use super::session::ClientSession;
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum Error { pub enum Error {
#[error("Unexpected message payload")]
UnexpectedMessagePayload,
#[error("Invalid client public key length")]
InvalidClientPubkeyLength,
#[error("Invalid client public key encoding")]
InvalidAuthPubkeyEncoding,
#[error("Database pool unavailable")] #[error("Database pool unavailable")]
DatabasePoolUnavailable, DatabasePoolUnavailable,
#[error("Database operation failed")] #[error("Database operation failed")]
@@ -33,8 +28,6 @@ pub enum Error {
InvalidChallengeSolution, InvalidChallengeSolution,
#[error("Client approval request failed")] #[error("Client approval request failed")]
ApproveError(#[from] ApproveError), ApproveError(#[from] ApproveError),
#[error("Internal error")]
InternalError,
#[error("Transport error")] #[error("Transport error")]
Transport, Transport,
} }
@@ -49,6 +42,18 @@ pub enum ApproveError {
Upstream(router::ApprovalError), Upstream(router::ApprovalError),
} }
#[derive(Debug, Clone)]
pub enum Inbound {
AuthChallengeRequest { pubkey: VerifyingKey },
AuthChallengeSolution { signature: Signature },
}
#[derive(Debug, Clone)]
pub enum Outbound {
AuthChallenge { pubkey: VerifyingKey, nonce: i32 },
AuthSuccess,
}
/// Atomically reads and increments the nonce for a known client. /// Atomically reads and increments the nonce for a known client.
/// Returns `None` if the pubkey is not registered. /// Returns `None` if the pubkey is not registered.
async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Option<i32>, Error> { async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Option<i32>, Error> {
@@ -62,10 +67,10 @@ async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Optio
conn.exclusive_transaction(|conn| { conn.exclusive_transaction(|conn| {
let pubkey_bytes = pubkey_bytes.clone(); let pubkey_bytes = pubkey_bytes.clone();
Box::pin(async move { Box::pin(async move {
let Some(current_nonce) = program_client::table let Some((client_id, current_nonce)) = program_client::table
.filter(program_client::public_key.eq(&pubkey_bytes)) .filter(program_client::public_key.eq(&pubkey_bytes))
.select(program_client::nonce) .select((program_client::id, program_client::nonce))
.first::<i32>(conn) .first::<(i32, i32)>(conn)
.await .await
.optional()? .optional()?
else { else {
@@ -78,6 +83,7 @@ async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Optio
.execute(conn) .execute(conn)
.await?; .await?;
let _ = client_id;
Ok(Some(current_nonce)) Ok(Some(current_nonce))
}) })
}) })
@@ -113,7 +119,15 @@ async fn approve_new_client(
} }
} }
async fn insert_client(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<(), Error> { enum InsertClientResult {
Inserted,
AlreadyExists,
}
async fn insert_client(
db: &db::DatabasePool,
pubkey: &VerifyingKey,
) -> Result<InsertClientResult, Error> {
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default() .unwrap_or_default()
@@ -124,7 +138,7 @@ async fn insert_client(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<(
Error::DatabasePoolUnavailable Error::DatabasePoolUnavailable
})?; })?;
insert_into(program_client::table) match insert_into(program_client::table)
.values(( .values((
program_client::public_key.eq(pubkey.as_bytes().to_vec()), program_client::public_key.eq(pubkey.as_bytes().to_vec()),
program_client::nonce.eq(1), // pre-incremented; challenge uses 0 program_client::nonce.eq(1), // pre-incremented; challenge uses 0
@@ -133,35 +147,51 @@ async fn insert_client(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<(
)) ))
.execute(&mut conn) .execute(&mut conn)
.await .await
.map_err(|e| { {
Ok(_) => {}
Err(diesel::result::Error::DatabaseError(
diesel::result::DatabaseErrorKind::UniqueViolation,
_,
)) => return Ok(InsertClientResult::AlreadyExists),
Err(e) => {
error!(error = ?e, "Failed to insert new client"); error!(error = ?e, "Failed to insert new client");
return Err(Error::DatabaseOperationFailed);
}
}
let client_id = program_client::table
.filter(program_client::public_key.eq(pubkey.as_bytes().to_vec()))
.order(program_client::id.desc())
.select(program_client::id)
.first::<i32>(&mut conn)
.await
.map_err(|e| {
error!(error = ?e, "Failed to load inserted client id");
Error::DatabaseOperationFailed Error::DatabaseOperationFailed
})?; })?;
Ok(()) let _ = client_id;
Ok(InsertClientResult::Inserted)
} }
async fn challenge_client( async fn challenge_client<T>(
props: &mut ClientConnection, transport: &mut T,
pubkey: VerifyingKey, pubkey: VerifyingKey,
nonce: i32, nonce: i32,
) -> Result<(), Error> { ) -> Result<(), Error>
let challenge_pubkey = pubkey.as_bytes().to_vec(); where
T: Bi<Inbound, Result<Outbound, Error>> + ?Sized,
props {
.transport transport
.send(Ok(Response::AuthChallenge { .send(Ok(Outbound::AuthChallenge { pubkey, nonce }))
pubkey: challenge_pubkey.clone(),
nonce,
}))
.await .await
.map_err(|e| { .map_err(|e| {
error!(error = ?e, "Failed to send auth challenge"); error!(error = ?e, "Failed to send auth challenge");
Error::Transport Error::Transport
})?; })?;
let signature = expect_message(&mut *props.transport, |req: Request| match req { let signature = expect_message(transport, |req: Inbound| match req {
Request::AuthChallengeSolution { signature } => Some(signature), Inbound::AuthChallengeSolution { signature } => Some(signature),
_ => None, _ => None,
}) })
.await .await
@@ -170,13 +200,9 @@ async fn challenge_client(
Error::Transport Error::Transport
})?; })?;
let formatted = format_challenge(nonce, &challenge_pubkey); let formatted = format_challenge(nonce, pubkey.as_bytes());
let sig = signature.as_slice().try_into().map_err(|_| {
error!("Invalid signature length");
Error::InvalidChallengeSolution
})?;
pubkey.verify_strict(&formatted, &sig).map_err(|_| { pubkey.verify_strict(&formatted, &signature).map_err(|_| {
error!("Challenge solution verification failed"); error!("Challenge solution verification failed");
Error::InvalidChallengeSolution Error::InvalidChallengeSolution
})?; })?;
@@ -184,54 +210,40 @@ async fn challenge_client(
Ok(()) Ok(())
} }
fn connect_error_code(err: &Error) -> ConnectErrorCode { pub async fn authenticate<T>(
match err { props: &mut ClientConnection,
Error::ApproveError(ApproveError::Denied) => ConnectErrorCode::ApprovalDenied, transport: &mut T,
Error::ApproveError(ApproveError::Upstream( ) -> Result<VerifyingKey, Error>
router::ApprovalError::NoUserAgentsConnected, where
)) => ConnectErrorCode::NoUserAgentsOnline, T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
_ => ConnectErrorCode::Unknown, {
} let Some(Inbound::AuthChallengeRequest { pubkey }) = transport.recv().await
}
async fn authenticate(props: &mut ClientConnection) -> Result<VerifyingKey, Error> {
let Some(Request::AuthChallengeRequest {
pubkey: challenge_pubkey,
}) = props.transport.recv().await
else { else {
return Err(Error::Transport); return Err(Error::Transport);
}; };
let pubkey_bytes = challenge_pubkey
.as_array()
.ok_or(Error::InvalidClientPubkeyLength)?;
let pubkey =
VerifyingKey::from_bytes(pubkey_bytes).map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
let nonce = match get_nonce(&props.db, &pubkey).await? { let nonce = match get_nonce(&props.db, &pubkey).await? {
Some(nonce) => nonce, Some(nonce) => nonce,
None => { None => {
approve_new_client(&props.actors, pubkey).await?; approve_new_client(&props.actors, pubkey).await?;
insert_client(&props.db, &pubkey).await?; match insert_client(&props.db, &pubkey).await? {
0 InsertClientResult::Inserted => 0,
InsertClientResult::AlreadyExists => match get_nonce(&props.db, &pubkey).await? {
Some(nonce) => nonce,
None => return Err(Error::DatabaseOperationFailed),
},
}
} }
}; };
challenge_client(props, pubkey, nonce).await?; challenge_client(transport, pubkey, nonce).await?;
transport
.send(Ok(Outbound::AuthSuccess))
.await
.map_err(|e| {
error!(error = ?e, "Failed to send auth success");
Error::Transport
})?;
Ok(pubkey) Ok(pubkey)
} }
pub async fn authenticate_and_create(mut props: ClientConnection) -> Result<ClientSession, Error> {
match authenticate(&mut props).await {
Ok(_pubkey) => Ok(ClientSession::new(props)),
Err(err) => {
let code = connect_error_code(&err);
let _ = props
.transport
.send(Ok(Response::ClientConnectError { code }))
.await;
Err(err)
}
}
}

View File

@@ -7,68 +7,31 @@ use crate::{
db, db,
}; };
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ClientError {
#[error("Expected message with payload")]
MissingRequestPayload,
#[error("Unexpected request payload")]
UnexpectedRequestPayload,
#[error("State machine error")]
StateTransitionFailed,
#[error("Connection registration failed")]
ConnectionRegistrationFailed,
#[error(transparent)]
Auth(#[from] auth::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectErrorCode {
Unknown,
ApprovalDenied,
NoUserAgentsOnline,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Request {
AuthChallengeRequest { pubkey: Vec<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 struct ClientConnection {
pub(crate) db: db::DatabasePool, pub(crate) db: db::DatabasePool,
pub(crate) transport: Transport,
pub(crate) actors: GlobalActors, pub(crate) actors: GlobalActors,
} }
impl ClientConnection { impl ClientConnection {
pub fn new(db: db::DatabasePool, transport: Transport, actors: GlobalActors) -> Self { pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self {
Self { Self { db, actors }
db,
transport,
actors,
}
} }
} }
pub mod auth; pub mod auth;
pub mod session; pub mod session;
pub async fn connect_client(props: ClientConnection) { pub async fn connect_client<T>(mut props: ClientConnection, transport: &mut T)
match auth::authenticate_and_create(props).await { where
Ok(session) => { T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send + ?Sized,
ClientSession::spawn(session); {
match auth::authenticate(&mut props, transport).await {
Ok(_pubkey) => {
ClientSession::spawn(ClientSession::new(props));
info!("Client authenticated, session started"); info!("Client authenticated, session started");
} }
Err(err) => { Err(err) => {
let _ = transport.send(Err(err.clone())).await;
error!(?err, "Authentication failed, closing connection"); error!(?err, "Authentication failed, closing connection");
} }
} }

View File

@@ -1,12 +1,9 @@
use kameo::Actor; use kameo::{Actor, messages};
use tokio::select; use tracing::error;
use tracing::{error, info};
use crate::{ use crate::{
actors::{ actors::{
GlobalActors, GlobalActors, client::ClientConnection, keyholder::KeyHolderState, router::RegisterClient,
client::{ClientConnection, ClientError, Request, Response},
router::RegisterClient,
}, },
db, db,
}; };
@@ -19,19 +16,30 @@ impl ClientSession {
pub(crate) fn new(props: ClientConnection) -> Self { pub(crate) fn new(props: ClientConnection) -> Self {
Self { props } Self { props }
} }
pub async fn process_transport_inbound(&mut self, req: Request) -> Output {
let _ = req;
Err(ClientError::UnexpectedRequestPayload)
}
} }
type Output = Result<Response, ClientError>; #[messages]
impl ClientSession {
#[message]
pub(crate) async fn handle_query_vault_state(&mut self) -> Result<KeyHolderState, Error> {
use crate::actors::keyholder::GetState;
let vault_state = match self.props.actors.key_holder.ask(GetState {}).await {
Ok(state) => state,
Err(err) => {
error!(?err, actor = "client", "keyholder.query.failed");
return Err(Error::Internal);
}
};
Ok(vault_state)
}
}
impl Actor for ClientSession { impl Actor for ClientSession {
type Args = Self; type Args = Self;
type Error = ClientError; type Error = Error;
async fn on_start( async fn on_start(
args: Self::Args, args: Self::Args,
@@ -42,52 +50,22 @@ impl Actor for ClientSession {
.router .router
.ask(RegisterClient { actor: this }) .ask(RegisterClient { actor: this })
.await .await
.map_err(|_| ClientError::ConnectionRegistrationFailed)?; .map_err(|_| Error::ConnectionRegistrationFailed)?;
Ok(args) 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;
}
msg = self.props.transport.recv() => {
match msg {
Some(request) => {
match self.process_transport_inbound(request).await {
Ok(resp) => {
if self.props.transport.send(Ok(resp)).await.is_err() {
error!(actor = "client", reason = "channel closed", "send.failed");
return Some(kameo::mailbox::Signal::Stop);
}
}
Err(err) => {
let _ = self.props.transport.send(Err(err)).await;
return Some(kameo::mailbox::Signal::Stop);
}
}
}
None => {
info!(actor = "client", "transport.closed");
return Some(kameo::mailbox::Signal::Stop);
}
}
}
}
}
}
} }
impl ClientSession { impl ClientSession {
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self { pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
use arbiter_proto::transport::DummyTransport; let props = ClientConnection::new(db, actors);
let transport: super::Transport = Box::new(DummyTransport::new());
let props = ClientConnection::new(db, transport, actors);
Self { props } Self { props }
} }
} }
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Connection registration failed")]
ConnectionRegistrationFailed,
#[error("Internal error")]
Internal,
}

View File

@@ -22,7 +22,7 @@ use encryption::v1::{self, KeyCell, Nonce};
pub mod encryption; pub mod encryption;
#[derive(Default, EnumDiscriminants)] #[derive(Default, EnumDiscriminants)]
#[strum_discriminants(derive(Reply), vis(pub))] #[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
enum State { enum State {
#[default] #[default]
Unbootstrapped, Unbootstrapped,
@@ -325,7 +325,7 @@ impl KeyHolder {
} }
#[message] #[message]
pub fn get_state(&self) -> StateDiscriminants { pub fn get_state(&self) -> KeyHolderState {
self.state.discriminant() self.state.discriminant()
} }

View File

@@ -163,7 +163,6 @@ impl MessageRouter {
.map(|agent| agent.downgrade()) .map(|agent| agent.downgrade())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// handle in subtask to not to lock the actor
tokio::task::spawn(async move { tokio::task::spawn(async move {
let result = request_client_approval(&weak_refs, client_pubkey).await; let result = request_client_approval(&weak_refs, client_pubkey).await;
reply_sender.send(result); reply_sender.send(result);

View File

@@ -1,74 +1,82 @@
use arbiter_proto::transport::Bi;
use tracing::error; use tracing::error;
use crate::actors::user_agent::{ use crate::actors::user_agent::{
Request, UserAgentConnection, AuthPublicKey, UserAgentConnection,
auth::state::{AuthContext, AuthStateMachine}, auth::state::{AuthContext, AuthStateMachine},
AuthPublicKey,
session::UserAgentSession,
}; };
#[derive(thiserror::Error, Debug, PartialEq)]
pub enum Error {
#[error("Unexpected message payload")]
UnexpectedMessagePayload,
#[error("Invalid client public key length")]
InvalidClientPubkeyLength,
#[error("Invalid client public key encoding")]
InvalidAuthPubkeyEncoding,
#[error("Database pool unavailable")]
DatabasePoolUnavailable,
#[error("Database operation failed")]
DatabaseOperationFailed,
#[error("Public key not registered")]
PublicKeyNotRegistered,
#[error("Transport error")]
Transport,
#[error("Invalid bootstrap token")]
InvalidBootstrapToken,
#[error("Bootstrapper actor unreachable")]
BootstrapperActorUnreachable,
#[error("Invalid challenge solution")]
InvalidChallengeSolution,
}
mod state; mod state;
use state::*; use state::*;
fn parse_auth_event(payload: Request) -> Result<AuthEvents, Error> { #[derive(Debug, Clone)]
match payload { pub enum Inbound {
Request::AuthChallengeRequest { AuthChallengeRequest {
pubkey, pubkey: AuthPublicKey,
bootstrap_token: None, bootstrap_token: Option<String>,
} => Ok(AuthEvents::AuthRequest(ChallengeRequest { pubkey })), },
Request::AuthChallengeRequest { AuthChallengeSolution {
pubkey, signature: Vec<u8>,
bootstrap_token: Some(token), },
} => Ok(AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest { }
pubkey,
token, #[derive(Debug)]
})), pub enum Error {
Request::AuthChallengeSolution { signature } => { UnregisteredPublicKey,
Ok(AuthEvents::ReceivedSolution(ChallengeSolution { InvalidChallengeSolution,
solution: signature, InvalidBootstrapToken,
})) Internal { details: String },
Transport,
}
impl Error {
fn internal(details: impl Into<String>) -> Self {
Self::Internal {
details: details.into(),
} }
_ => Err(Error::UnexpectedMessagePayload),
} }
} }
pub async fn authenticate(props: &mut UserAgentConnection) -> Result<AuthPublicKey, Error> { #[derive(Debug, Clone)]
let mut state = AuthStateMachine::new(AuthContext::new(props)); pub enum Outbound {
AuthChallenge { nonce: i32 },
AuthSuccess,
}
fn parse_auth_event(payload: Inbound) -> AuthEvents {
match payload {
Inbound::AuthChallengeRequest {
pubkey,
bootstrap_token: None,
} => AuthEvents::AuthRequest(ChallengeRequest { pubkey }),
Inbound::AuthChallengeRequest {
pubkey,
bootstrap_token: Some(token),
} => AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest { pubkey, token }),
Inbound::AuthChallengeSolution { signature } => {
AuthEvents::ReceivedSolution(ChallengeSolution {
solution: signature,
})
}
}
}
pub async fn authenticate<T>(
props: &mut UserAgentConnection,
transport: T,
) -> Result<AuthPublicKey, Error>
where
T: Bi<Inbound, Result<Outbound, Error>> + Send,
{
let mut state = AuthStateMachine::new(AuthContext::new(props, transport));
loop { loop {
// `state` holds a mutable reference to `props` so we can't access it directly here // `state` holds a mutable reference to `props` so we can't access it directly here
let transport = state.context_mut().conn.transport.as_mut(); let Some(payload) = state.context_mut().transport.recv().await else {
let Some(payload) = transport.recv().await else {
return Err(Error::Transport); return Err(Error::Transport);
}; };
let event = parse_auth_event(payload)?; match state.process_event(parse_auth_event(payload)).await {
match state.process_event(event).await {
Ok(AuthStates::AuthOk(key)) => return Ok(key.clone()), Ok(AuthStates::AuthOk(key)) => return Ok(key.clone()),
Err(AuthError::ActionFailed(err)) => { Err(AuthError::ActionFailed(err)) => {
error!(?err, "State machine action failed"); error!(?err, "State machine action failed");
@@ -91,11 +99,3 @@ pub async fn authenticate(props: &mut UserAgentConnection) -> Result<AuthPublicK
} }
} }
} }
pub async fn authenticate_and_create(
mut props: UserAgentConnection,
) -> Result<UserAgentSession, Error> {
let _key = authenticate(&mut props).await?;
let session = UserAgentSession::new(props);
Ok(session)
}

View File

@@ -1,3 +1,4 @@
use arbiter_proto::transport::Bi;
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update}; use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use tracing::error; use tracing::error;
@@ -6,7 +7,7 @@ use super::Error;
use crate::{ use crate::{
actors::{ actors::{
bootstrap::ConsumeToken, bootstrap::ConsumeToken,
user_agent::{AuthPublicKey, Response, UserAgentConnection}, user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
}, },
db::schema, db::schema,
}; };
@@ -42,7 +43,7 @@ smlang::statemachine!(
async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result<i32, Error> { async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result<i32, Error> {
let mut db_conn = db.get().await.map_err(|e| { let mut db_conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error"); error!(error = ?e, "Database pool error");
Error::DatabasePoolUnavailable Error::internal("Database unavailable")
})?; })?;
db_conn db_conn
.exclusive_transaction(|conn| { .exclusive_transaction(|conn| {
@@ -66,11 +67,11 @@ async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Resu
.optional() .optional()
.map_err(|e| { .map_err(|e| {
error!(error = ?e, "Database error"); error!(error = ?e, "Database error");
Error::DatabaseOperationFailed Error::internal("Database operation failed")
})? })?
.ok_or_else(|| { .ok_or_else(|| {
error!(?pubkey_bytes, "Public key not found in database"); error!(?pubkey_bytes, "Public key not found in database");
Error::PublicKeyNotRegistered Error::UnregisteredPublicKey
}) })
} }
@@ -79,7 +80,7 @@ async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> R
let key_type = pubkey.key_type(); let key_type = pubkey.key_type();
let mut conn = db.get().await.map_err(|e| { let mut conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error"); error!(error = ?e, "Database pool error");
Error::DatabasePoolUnavailable Error::internal("Database unavailable")
})?; })?;
diesel::insert_into(schema::useragent_client::table) diesel::insert_into(schema::useragent_client::table)
@@ -92,23 +93,27 @@ async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> R
.await .await
.map_err(|e| { .map_err(|e| {
error!(error = ?e, "Database error"); error!(error = ?e, "Database error");
Error::DatabaseOperationFailed Error::internal("Database operation failed")
})?; })?;
Ok(()) Ok(())
} }
pub struct AuthContext<'a> { pub struct AuthContext<'a, T> {
pub(super) conn: &'a mut UserAgentConnection, pub(super) conn: &'a mut UserAgentConnection,
pub(super) transport: T,
} }
impl<'a> AuthContext<'a> { impl<'a, T> AuthContext<'a, T> {
pub fn new(conn: &'a mut UserAgentConnection) -> Self { pub fn new(conn: &'a mut UserAgentConnection, transport: T) -> Self {
Self { conn } Self { conn, transport }
} }
} }
impl AuthStateMachineContext for AuthContext<'_> { impl<T> AuthStateMachineContext for AuthContext<'_, T>
where
T: Bi<super::Inbound, Result<super::Outbound, Error>> + Send,
{
type Error = Error; type Error = Error;
async fn prepare_challenge( async fn prepare_challenge(
@@ -118,9 +123,8 @@ impl AuthStateMachineContext for AuthContext<'_> {
let stored_bytes = pubkey.to_stored_bytes(); let stored_bytes = pubkey.to_stored_bytes();
let nonce = create_nonce(&self.conn.db, &stored_bytes).await?; let nonce = create_nonce(&self.conn.db, &stored_bytes).await?;
self.conn self.transport
.transport .send(Ok(Outbound::AuthChallenge { nonce }))
.send(Ok(Response::AuthChallenge { nonce }))
.await .await
.map_err(|e| { .map_err(|e| {
error!(?e, "Failed to send auth challenge"); error!(?e, "Failed to send auth challenge");
@@ -149,7 +153,7 @@ impl AuthStateMachineContext for AuthContext<'_> {
.await .await
.map_err(|e| { .map_err(|e| {
error!(?e, "Failed to consume bootstrap token"); error!(?e, "Failed to consume bootstrap token");
Error::BootstrapperActorUnreachable Error::internal("Failed to consume bootstrap token")
})?; })?;
if !token_ok { if !token_ok {
@@ -159,9 +163,8 @@ impl AuthStateMachineContext for AuthContext<'_> {
register_key(&self.conn.db, &pubkey).await?; register_key(&self.conn.db, &pubkey).await?;
self.conn self.transport
.transport .send(Ok(Outbound::AuthSuccess))
.send(Ok(Response::AuthOk))
.await .await
.map_err(|_| Error::Transport)?; .map_err(|_| Error::Transport)?;
@@ -172,7 +175,10 @@ impl AuthStateMachineContext for AuthContext<'_> {
#[allow(clippy::unused_unit)] #[allow(clippy::unused_unit)]
async fn verify_solution( async fn verify_solution(
&mut self, &mut self,
ChallengeContext { challenge_nonce, key }: &ChallengeContext, ChallengeContext {
challenge_nonce,
key,
}: &ChallengeContext,
ChallengeSolution { solution }: ChallengeSolution, ChallengeSolution { solution }: ChallengeSolution,
) -> Result<AuthPublicKey, Self::Error> { ) -> Result<AuthPublicKey, Self::Error> {
let formatted = arbiter_proto::format_challenge(*challenge_nonce, &key.to_stored_bytes()); let formatted = arbiter_proto::format_challenge(*challenge_nonce, &key.to_stored_bytes());
@@ -205,9 +211,8 @@ impl AuthStateMachineContext for AuthContext<'_> {
}; };
if valid { if valid {
self.conn self.transport
.transport .send(Ok(Outbound::AuthSuccess))
.send(Ok(Response::AuthOk))
.await .await
.map_err(|_| Error::Transport)?; .map_err(|_| Error::Transport)?;
} }

View File

@@ -1,33 +1,8 @@
use alloy::primitives::Address;
use arbiter_proto::transport::Bi;
use kameo::actor::Spawn as _;
use tracing::{error, info};
use crate::{ use crate::{
actors::{GlobalActors, evm, user_agent::session::UserAgentSession}, actors::GlobalActors,
db::{self, models::KeyType}, db::{self, models::KeyType},
evm::policies::SharedGrantSettings,
evm::policies::{Grant, SpecificGrant},
}; };
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum TransportResponseError {
#[error("Unexpected request payload")]
UnexpectedRequestPayload,
#[error("Invalid state for unseal encrypted key")]
InvalidStateForUnsealEncryptedKey,
#[error("client_pubkey must be 32 bytes")]
InvalidClientPubkeyLength,
#[error("State machine error")]
StateTransitionFailed,
#[error("Vault is not available")]
KeyHolderActorUnreachable,
#[error(transparent)]
Auth(#[from] auth::Error),
#[error("Failed registering connection")]
ConnectionRegistrationFailed,
}
/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake. /// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum AuthPublicKey { pub enum AuthPublicKey {
@@ -65,119 +40,55 @@ impl AuthPublicKey {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] impl TryFrom<(KeyType, Vec<u8>)> for AuthPublicKey {
pub enum UnsealError { type Error = &'static str;
InvalidKey,
Unbootstrapped, fn try_from(value: (KeyType, Vec<u8>)) -> Result<Self, Self::Error> {
} let (key_type, bytes) = value;
match key_type {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] KeyType::Ed25519 => {
pub enum BootstrapError { let bytes: [u8; 32] = bytes.try_into().map_err(|_| "invalid Ed25519 key length")?;
AlreadyBootstrapped, let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes)
InvalidKey, .map_err(|_e| "invalid Ed25519 key")?;
} Ok(AuthPublicKey::Ed25519(key))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] KeyType::EcdsaSecp256k1 => {
pub enum VaultState { let point =
Unbootstrapped, k256::EncodedPoint::from_bytes(&bytes).map_err(|_e| "invalid ECDSA key")?;
Sealed, let key = k256::ecdsa::VerifyingKey::from_encoded_point(&point)
Unsealed, .map_err(|_e| "invalid ECDSA key")?;
} Ok(AuthPublicKey::EcdsaSecp256k1(key))
}
#[derive(Debug, Clone)] KeyType::Rsa => {
pub enum Request { use rsa::pkcs8::DecodePublicKey as _;
AuthChallengeRequest { let key = rsa::RsaPublicKey::from_public_key_der(&bytes)
pubkey: AuthPublicKey, .map_err(|_e| "invalid RSA key")?;
bootstrap_token: Option<String>, Ok(AuthPublicKey::Rsa(key))
}, }
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,
},
} }
// Messages, sent by user agent to connection client without having a request
#[derive(Debug)] #[derive(Debug)]
pub enum Response { pub enum OutOfBand {
AuthChallenge { ClientConnectionRequest { pubkey: ed25519_dalek::VerifyingKey },
nonce: i32,
},
AuthOk,
UnsealStartResponse {
server_pubkey: x25519_dalek::PublicKey,
},
UnsealResult(Result<(), UnsealError>),
BootstrapResult(Result<(), BootstrapError>),
VaultState(VaultState),
ClientConnectionRequest {
pubkey: ed25519_dalek::VerifyingKey,
},
ClientConnectionCancel, 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 { pub struct UserAgentConnection {
db: db::DatabasePool, pub(crate) db: db::DatabasePool,
actors: GlobalActors, pub(crate) actors: GlobalActors,
transport: Transport,
} }
impl UserAgentConnection { impl UserAgentConnection {
pub fn new(db: db::DatabasePool, actors: GlobalActors, transport: Transport) -> Self { pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self {
Self { Self { db, actors }
db,
actors,
transport,
}
} }
} }
pub mod auth; pub mod auth;
pub mod session; pub mod session;
#[tracing::instrument(skip(props))] pub use auth::authenticate;
pub async fn connect_user_agent(props: UserAgentConnection) { pub use session::UserAgentSession;
match auth::authenticate_and_create(props).await {
Ok(session) => {
UserAgentSession::spawn(session);
info!("User authenticated, session started");
}
Err(err) => {
error!(?err, "Authentication failed, closing connection");
}
}
}

View File

@@ -1,93 +1,81 @@
use std::borrow::Cow;
use arbiter_proto::transport::Sender;
use async_trait::async_trait;
use ed25519_dalek::VerifyingKey; use ed25519_dalek::VerifyingKey;
use kameo::{Actor, messages, prelude::Context}; use kameo::{Actor, messages};
use tokio::{select, sync::watch}; use thiserror::Error;
use tracing::{error, info}; use tokio::sync::watch;
use tracing::error;
use crate::actors::{ use crate::actors::{
router::RegisterUserAgent, router::RegisterUserAgent,
user_agent::{ user_agent::{OutOfBand, UserAgentConnection},
Request, Response, TransportResponseError,
UserAgentConnection,
},
}; };
mod state; mod state;
use state::{DummyContext, UserAgentEvents, UserAgentStateMachine}; use state::{DummyContext, UserAgentEvents, UserAgentStateMachine};
// Error for consumption by other actors #[derive(Debug, Error)]
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum Error { pub enum Error {
#[error("User agent session ended due to connection loss")] #[error("State transition failed")]
ConnectionLost, State,
#[error("User agent session ended due to unexpected message")] #[error("Internal error: {message}")]
UnexpectedMessage, Internal { message: Cow<'static, str> },
}
impl Error {
pub fn internal(message: impl Into<Cow<'static, str>>) -> Self {
Self::Internal {
message: message.into(),
}
}
} }
pub struct UserAgentSession { pub struct UserAgentSession {
props: UserAgentConnection, props: UserAgentConnection,
state: UserAgentStateMachine<DummyContext>, state: UserAgentStateMachine<DummyContext>,
#[allow(dead_code, reason = "The session keeps ownership of the outbound transport even before the state-machine flow starts using it directly")]
sender: Box<dyn Sender<OutOfBand>>,
} }
mod connection; mod connection;
pub(crate) use connection::{
BootstrapError, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, HandleEvmWalletList,
HandleGrantCreate, HandleGrantDelete, HandleGrantList, HandleQueryVaultState,
};
pub use connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError};
impl UserAgentSession { impl UserAgentSession {
pub(crate) fn new(props: UserAgentConnection) -> Self { pub(crate) fn new(props: UserAgentConnection, sender: Box<dyn Sender<OutOfBand>>) -> Self {
Self { Self {
props, props,
state: UserAgentStateMachine::new(DummyContext), state: UserAgentStateMachine::new(DummyContext),
sender,
} }
} }
pub(super) async fn send_msg<Reply: kameo::Reply>( pub fn new_test(db: crate::db::DatabasePool, actors: crate::actors::GlobalActors) -> Self {
struct DummySender;
#[async_trait]
impl Sender<OutOfBand> for DummySender {
async fn send(
&mut self, &mut self,
msg: Response, _item: OutOfBand,
_ctx: &mut Context<Self, Reply>, ) -> Result<(), arbiter_proto::transport::Error> {
) -> Result<(), Error> { Ok(())
self.props.transport.send(Ok(msg)).await.map_err(|_| { }
error!(
actor = "useragent",
reason = "channel closed",
"send.failed"
);
Error::ConnectionLost
})
} }
async fn expect_msg<Extractor, Msg, Reply>( Self::new(UserAgentConnection::new(db, actors), Box::new(DummySender))
&mut self,
extractor: Extractor,
ctx: &mut Context<Self, Reply>,
) -> Result<Msg, Error>
where
Extractor: FnOnce(Request) -> Option<Msg>,
Reply: kameo::Reply,
{
let msg = self.props.transport.recv().await.ok_or_else(|| {
error!(
actor = "useragent",
reason = "channel closed",
"recv.failed"
);
ctx.stop();
Error::ConnectionLost
})?;
extractor(msg).ok_or_else(|| {
error!(
actor = "useragent",
reason = "unexpected message",
"recv.failed"
);
ctx.stop();
Error::UnexpectedMessage
})
} }
fn transition(&mut self, event: UserAgentEvents) -> Result<(), TransportResponseError> { fn transition(&mut self, event: UserAgentEvents) -> Result<(), Error> {
self.state.process_event(event).map_err(|e| { self.state.process_event(event).map_err(|e| {
error!(?e, "State transition failed"); error!(?e, "State transition failed");
TransportResponseError::StateTransitionFailed Error::State
})?; })?;
Ok(()) Ok(())
} }
@@ -95,52 +83,34 @@ impl UserAgentSession {
#[messages] #[messages]
impl UserAgentSession { impl UserAgentSession {
// TODO: Think about refactoring it to state-machine based flow, as we already have one #[message]
#[message(ctx)]
pub async fn request_new_client_approval( pub async fn request_new_client_approval(
&mut self, &mut self,
client_pubkey: VerifyingKey, client_pubkey: VerifyingKey,
mut cancel_flag: watch::Receiver<()>, mut cancel_flag: watch::Receiver<()>,
ctx: &mut Context<Self, Result<bool, Error>>, ) -> Result<bool, ()> {
) -> Result<bool, Error> { if self
self.send_msg( .sender
Response::ClientConnectionRequest { .send(OutOfBand::ClientConnectionRequest {
pubkey: client_pubkey, pubkey: client_pubkey,
}, })
ctx, .await
) .is_err()
.await?; {
return Err(());
let extractor = |msg| {
if let Request::ClientConnectionResponse { approved } = msg {
Some(approved)
} else {
None
} }
};
tokio::select! { let _ = cancel_flag.changed().await;
_ = cancel_flag.changed() => {
info!(actor = "useragent", "client connection approval cancelled"); let _ = self.sender.send(OutOfBand::ClientConnectionCancel).await;
self.send_msg(
Response::ClientConnectionCancel,
ctx,
).await?;
Ok(false) Ok(false)
} }
result = self.expect_msg(extractor, ctx) => {
let result = result?;
info!(actor = "useragent", "received client connection approval result: approved={}", result);
Ok(result)
}
}
}
} }
impl Actor for UserAgentSession { impl Actor for UserAgentSession {
type Args = Self; type Args = Self;
type Error = TransportResponseError; type Error = Error;
async fn on_start( async fn on_start(
args: Self::Args, args: Self::Args,
@@ -155,56 +125,8 @@ impl Actor for UserAgentSession {
.await .await
.map_err(|err| { .map_err(|err| {
error!(?err, "Failed to register user agent connection with router"); error!(?err, "Failed to register user agent connection with router");
TransportResponseError::ConnectionRegistrationFailed Error::internal("Failed to register user agent connection with router")
})?; })?;
Ok(args) 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;
}
msg = self.props.transport.recv() => {
match msg {
Some(request) => {
match self.process_transport_inbound(request).await {
Ok(response) => {
if self.props.transport.send(Ok(response)).await.is_err() {
error!(actor = "useragent", reason = "channel closed", "send.failed");
return Some(kameo::mailbox::Signal::Stop);
}
}
Err(err) => {
let _ = self.props.transport.send(Err(err)).await;
return Some(kameo::mailbox::Signal::Stop);
}
}
}
None => {
info!(actor = "useragent", "transport.closed");
return Some(kameo::mailbox::Signal::Stop);
}
}
}
}
}
}
}
impl UserAgentSession {
pub fn new_test(db: crate::db::DatabasePool, actors: crate::actors::GlobalActors) -> Self {
use arbiter_proto::transport::DummyTransport;
let transport: super::Transport = Box::new(DummyTransport::new());
let props = UserAgentConnection::new(db, actors, transport);
Self {
props,
state: UserAgentStateMachine::new(DummyContext),
}
}
} }

View File

@@ -1,10 +1,15 @@
use std::sync::Mutex; use std::sync::Mutex;
use alloy::primitives::Address;
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use kameo::error::SendError; use kameo::error::SendError;
use kameo::messages;
use tracing::{error, info}; use tracing::{error, info};
use x25519_dalek::{EphemeralSecret, PublicKey}; use x25519_dalek::{EphemeralSecret, PublicKey};
use crate::actors::keyholder::KeyHolderState;
use crate::actors::user_agent::session::Error;
use crate::evm::policies::{Grant, SpecificGrant};
use crate::safe_cell::SafeCell; use crate::safe_cell::SafeCell;
use crate::{ use crate::{
actors::{ actors::{
@@ -12,67 +17,19 @@ use crate::{
Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants, Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
}, },
keyholder::{self, Bootstrap, TryUnseal}, keyholder::{self, Bootstrap, TryUnseal},
user_agent::{ user_agent::session::{
BootstrapError, Request, Response, TransportResponseError, UnsealError, VaultState,
session::{
UserAgentSession, UserAgentSession,
state::{UnsealContext, UserAgentEvents, UserAgentStates}, state::{UnsealContext, UserAgentEvents, UserAgentStates},
}, },
}, },
},
safe_cell::SafeCellHandle as _, safe_cell::SafeCellHandle as _,
}; };
impl UserAgentSession { impl UserAgentSession {
pub async fn process_transport_inbound(&mut self, req: Request) -> Output { fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> {
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 { let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
error!("Received encrypted key in invalid state"); error!("Received encrypted key in invalid state");
return Err(TransportResponseError::InvalidStateForUnsealEncryptedKey); return Err(Error::internal("Invalid state for unseal encrypted key"));
}; };
let ephemeral_secret = { let ephemeral_secret = {
@@ -87,7 +44,7 @@ impl UserAgentSession {
None => { None => {
drop(secret_lock); drop(secret_lock);
error!("Ephemeral secret already taken"); error!("Ephemeral secret already taken");
return Err(TransportResponseError::StateTransitionFailed); return Err(Error::internal("Ephemeral secret already taken"));
} }
} }
}; };
@@ -121,8 +78,38 @@ impl UserAgentSession {
} }
} }
} }
}
async fn handle_unseal_request(&mut self, client_pubkey: x25519_dalek::PublicKey) -> Output { pub struct UnsealStartResponse {
pub server_pubkey: PublicKey,
}
#[derive(Debug, Error)]
pub enum UnsealError {
#[error("Invalid key provided for unsealing")]
InvalidKey,
#[error("Internal error during unsealing process")]
General(#[from] super::Error),
}
#[derive(Debug, Error)]
pub enum BootstrapError {
#[error("Invalid key provided for bootstrapping")]
InvalidKey,
#[error("Vault is already bootstrapped")]
AlreadyBootstrapped,
#[error("Internal error during bootstrapping process")]
General(#[from] super::Error),
}
#[messages]
impl UserAgentSession {
#[message]
pub async fn handle_unseal_request(
&mut self,
client_pubkey: x25519_dalek::PublicKey,
) -> Result<UnsealStartResponse, Error> {
let secret = EphemeralSecret::random(); let secret = EphemeralSecret::random();
let public_key = PublicKey::from(&secret); let public_key = PublicKey::from(&secret);
@@ -131,24 +118,27 @@ impl UserAgentSession {
client_public_key: client_pubkey, client_public_key: client_pubkey,
}))?; }))?;
Ok(Response::UnsealStartResponse { Ok(UnsealStartResponse {
server_pubkey: public_key, server_pubkey: public_key,
}) })
} }
async fn handle_unseal_encrypted_key( #[message]
pub async fn handle_unseal_encrypted_key(
&mut self, &mut self,
nonce: Vec<u8>, nonce: Vec<u8>,
ciphertext: Vec<u8>, ciphertext: Vec<u8>,
associated_data: Vec<u8>, associated_data: Vec<u8>,
) -> Output { ) -> Result<(), UnsealError> {
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() { let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
Ok(values) => values, Ok(values) => values,
Err(TransportResponseError::StateTransitionFailed) => { Err(Error::State) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?; self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(Response::UnsealResult(Err(UnsealError::InvalidKey))); return Err(UnsealError::InvalidKey);
}
Err(_err) => {
return Err(Error::internal("Failed to take unseal secret").into());
} }
Err(err) => return Err(err),
}; };
let seal_key_buffer = match Self::decrypt_client_key_material( let seal_key_buffer = match Self::decrypt_client_key_material(
@@ -161,7 +151,7 @@ impl UserAgentSession {
Ok(buffer) => buffer, Ok(buffer) => buffer,
Err(()) => { Err(()) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?; self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(Response::UnsealResult(Err(UnsealError::InvalidKey))); return Err(UnsealError::InvalidKey);
} }
}; };
@@ -177,38 +167,39 @@ impl UserAgentSession {
Ok(_) => { Ok(_) => {
info!("Successfully unsealed key with client-provided key"); info!("Successfully unsealed key with client-provided key");
self.transition(UserAgentEvents::ReceivedValidKey)?; self.transition(UserAgentEvents::ReceivedValidKey)?;
Ok(Response::UnsealResult(Ok(()))) Ok(())
} }
Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => { Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?; self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(Response::UnsealResult(Err(UnsealError::InvalidKey))) Err(UnsealError::InvalidKey)
} }
Err(SendError::HandlerError(err)) => { Err(SendError::HandlerError(err)) => {
error!(?err, "Keyholder failed to unseal key"); error!(?err, "Keyholder failed to unseal key");
self.transition(UserAgentEvents::ReceivedInvalidKey)?; self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(Response::UnsealResult(Err(UnsealError::InvalidKey))) Err(UnsealError::InvalidKey)
} }
Err(err) => { Err(err) => {
error!(?err, "Failed to send unseal request to keyholder"); error!(?err, "Failed to send unseal request to keyholder");
self.transition(UserAgentEvents::ReceivedInvalidKey)?; self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(TransportResponseError::KeyHolderActorUnreachable) Err(Error::internal("Vault actor error").into())
} }
} }
} }
async fn handle_bootstrap_encrypted_key( #[message]
pub(crate) async fn handle_bootstrap_encrypted_key(
&mut self, &mut self,
nonce: Vec<u8>, nonce: Vec<u8>,
ciphertext: Vec<u8>, ciphertext: Vec<u8>,
associated_data: Vec<u8>, associated_data: Vec<u8>,
) -> Output { ) -> Result<(), BootstrapError> {
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() { let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
Ok(values) => values, Ok(values) => values,
Err(TransportResponseError::StateTransitionFailed) => { Err(Error::State) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?; self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey))); return Err(BootstrapError::InvalidKey);
} }
Err(err) => return Err(err), Err(err) => return Err(err.into()),
}; };
let seal_key_buffer = match Self::decrypt_client_key_material( let seal_key_buffer = match Self::decrypt_client_key_material(
@@ -221,7 +212,7 @@ impl UserAgentSession {
Ok(buffer) => buffer, Ok(buffer) => buffer,
Err(()) => { Err(()) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?; self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey))); return Err(BootstrapError::InvalidKey);
} }
}; };
@@ -237,87 +228,94 @@ impl UserAgentSession {
Ok(_) => { Ok(_) => {
info!("Successfully bootstrapped vault with client-provided key"); info!("Successfully bootstrapped vault with client-provided key");
self.transition(UserAgentEvents::ReceivedValidKey)?; self.transition(UserAgentEvents::ReceivedValidKey)?;
Ok(Response::BootstrapResult(Ok(()))) Ok(())
} }
Err(SendError::HandlerError(keyholder::Error::AlreadyBootstrapped)) => { Err(SendError::HandlerError(keyholder::Error::AlreadyBootstrapped)) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?; self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(Response::BootstrapResult(Err( Err(BootstrapError::AlreadyBootstrapped)
BootstrapError::AlreadyBootstrapped,
)))
} }
Err(SendError::HandlerError(err)) => { Err(SendError::HandlerError(err)) => {
error!(?err, "Keyholder failed to bootstrap vault"); error!(?err, "Keyholder failed to bootstrap vault");
self.transition(UserAgentEvents::ReceivedInvalidKey)?; self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey))) Err(BootstrapError::InvalidKey)
} }
Err(err) => { Err(err) => {
error!(?err, "Failed to send bootstrap request to keyholder"); error!(?err, "Failed to send bootstrap request to keyholder");
self.transition(UserAgentEvents::ReceivedInvalidKey)?; self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(TransportResponseError::KeyHolderActorUnreachable) Err(BootstrapError::General(Error::internal(
"Vault actor error",
)))
} }
} }
} }
} }
#[messages]
impl UserAgentSession { impl UserAgentSession {
async fn handle_query_vault_state(&mut self) -> Output { #[message]
use crate::actors::keyholder::{GetState, StateDiscriminants}; pub(crate) async fn handle_query_vault_state(&mut self) -> Result<KeyHolderState, Error> {
use crate::actors::keyholder::GetState;
let vault_state = match self.props.actors.key_holder.ask(GetState {}).await { let vault_state = match self.props.actors.key_holder.ask(GetState {}).await {
Ok(StateDiscriminants::Unbootstrapped) => VaultState::Unbootstrapped, Ok(state) => state,
Ok(StateDiscriminants::Sealed) => VaultState::Sealed,
Ok(StateDiscriminants::Unsealed) => VaultState::Unsealed,
Err(err) => { Err(err) => {
error!(?err, actor = "useragent", "keyholder.query.failed"); error!(?err, actor = "useragent", "keyholder.query.failed");
return Err(TransportResponseError::KeyHolderActorUnreachable); return Err(Error::internal("Vault is in broken state"));
} }
}; };
Ok(Response::VaultState(vault_state)) Ok(vault_state)
} }
} }
#[messages]
impl UserAgentSession { impl UserAgentSession {
async fn handle_evm_wallet_create(&mut self) -> Output { #[message]
let result = match self.props.actors.evm.ask(Generate {}).await { pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result<Address, Error> {
Ok(_address) => return Ok(Response::EvmWalletCreate(Ok(()))), match self.props.actors.evm.ask(Generate {}).await {
Err(SendError::HandlerError(err)) => Err(err), Ok(address) => Ok(address),
Err(SendError::HandlerError(err)) => Err(Error::internal(format!(
"EVM wallet generation failed: {err}"
))),
Err(err) => { Err(err) => {
error!(?err, "EVM actor unreachable during wallet create"); error!(?err, "EVM actor unreachable during wallet create");
return Err(TransportResponseError::KeyHolderActorUnreachable); Err(Error::internal("EVM actor unreachable"))
}
} }
};
Ok(Response::EvmWalletCreate(result))
} }
async fn handle_evm_wallet_list(&mut self) -> Output { #[message]
pub(crate) async fn handle_evm_wallet_list(&mut self) -> Result<Vec<Address>, Error> {
match self.props.actors.evm.ask(ListWallets {}).await { match self.props.actors.evm.ask(ListWallets {}).await {
Ok(wallets) => Ok(Response::EvmWalletList(wallets)), Ok(wallets) => Ok(wallets),
Err(err) => { Err(err) => {
error!(?err, "EVM wallet list failed"); error!(?err, "EVM wallet list failed");
Err(TransportResponseError::KeyHolderActorUnreachable) Err(Error::internal("Failed to list EVM wallets"))
} }
} }
} }
} }
#[messages]
impl UserAgentSession { impl UserAgentSession {
async fn handle_grant_list(&mut self) -> Output { #[message]
pub(crate) async fn handle_grant_list(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
match self.props.actors.evm.ask(UseragentListGrants {}).await { match self.props.actors.evm.ask(UseragentListGrants {}).await {
Ok(grants) => Ok(Response::ListGrants(grants)), Ok(grants) => Ok(grants),
Err(err) => { Err(err) => {
error!(?err, "EVM grant list failed"); error!(?err, "EVM grant list failed");
Err(TransportResponseError::KeyHolderActorUnreachable) Err(Error::internal("Failed to list EVM grants"))
} }
} }
} }
async fn handle_grant_create( #[message]
pub(crate) async fn handle_grant_create(
&mut self, &mut self,
client_id: i32, client_id: i32,
basic: crate::evm::policies::SharedGrantSettings, basic: crate::evm::policies::SharedGrantSettings,
grant: crate::evm::policies::SpecificGrant, grant: crate::evm::policies::SpecificGrant,
) -> Output { ) -> Result<i32, Error> {
match self match self
.props .props
.actors .actors
@@ -329,15 +327,16 @@ impl UserAgentSession {
}) })
.await .await
{ {
Ok(grant_id) => Ok(Response::EvmGrantCreate(Ok(grant_id))), Ok(grant_id) => Ok(grant_id),
Err(err) => { Err(err) => {
error!(?err, "EVM grant create failed"); error!(?err, "EVM grant create failed");
Err(TransportResponseError::KeyHolderActorUnreachable) Err(Error::internal("Failed to create EVM grant"))
} }
} }
} }
async fn handle_grant_delete(&mut self, grant_id: i32) -> Output { #[message]
pub(crate) async fn handle_grant_delete(&mut self, grant_id: i32) -> Result<(), Error> {
match self match self
.props .props
.actors .actors
@@ -345,10 +344,10 @@ impl UserAgentSession {
.ask(UseragentDeleteGrant { grant_id }) .ask(UseragentDeleteGrant { grant_id })
.await .await
{ {
Ok(()) => Ok(Response::EvmGrantDelete(Ok(()))), Ok(()) => Ok(()),
Err(err) => { Err(err) => {
error!(?err, "EVM grant delete failed"); error!(?err, "EVM grant delete failed");
Err(TransportResponseError::KeyHolderActorUnreachable) Err(Error::internal("Failed to delete EVM grant"))
} }
} }
} }

View File

@@ -44,6 +44,14 @@ pub enum DatabaseSetupError {
Pool(#[from] PoolInitError), Pool(#[from] PoolInitError),
} }
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("Database connection error")]
Pool(#[from] PoolError),
#[error("Database query error")]
Connection(#[from] diesel::result::Error),
}
#[tracing::instrument(level = "info")] #[tracing::instrument(level = "info")]
fn database_path() -> Result<std::path::PathBuf, DatabaseSetupError> { fn database_path() -> Result<std::path::PathBuf, DatabaseSetupError> {
let arbiter_home = arbiter_proto::home_path().map_err(DatabaseSetupError::HomeDir)?; let arbiter_home = arbiter_proto::home_path().map_err(DatabaseSetupError::HomeDir)?;

View File

@@ -1,137 +1,137 @@
use arbiter_proto::{ use arbiter_proto::{
proto::client::{ proto::client::{
AuthChallenge as ProtoAuthChallenge, ClientRequest, ClientResponse, VaultState as ProtoVaultState,
AuthChallengeRequest as ProtoAuthChallengeRequest,
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthOk as ProtoAuthOk,
ClientConnectError, ClientRequest, ClientResponse,
client_connect_error::Code as ProtoClientConnectErrorCode,
client_request::Payload as ClientRequestPayload, client_request::Payload as ClientRequestPayload,
client_response::Payload as ClientResponsePayload, client_response::Payload as ClientResponsePayload,
}, },
transport::{Bi, Error as TransportError}, transport::{Receiver, Sender, grpc::GrpcBi},
}; };
use async_trait::async_trait; use kameo::{
use futures::StreamExt as _; actor::{ActorRef, Spawn as _},
use tokio::sync::mpsc; error::SendError,
use tonic::{Status, Streaming}; };
use tonic::Status;
use tracing::{info, warn};
use crate::actors::client::{ use crate::{
self, ClientError, ConnectErrorCode, Request as DomainRequest, Response as DomainResponse, actors::{
client::{
self, ClientConnection,
session::{ClientSession, Error, HandleQueryVaultState},
},
keyholder::KeyHolderState,
},
grpc::request_tracker::RequestTracker,
utils::defer,
}; };
pub struct GrpcTransport { mod auth;
sender: mpsc::Sender<Result<ClientResponse, Status>>,
receiver: Streaming<ClientRequest>, async fn dispatch_loop(
mut bi: GrpcBi<ClientRequest, ClientResponse>,
actor: ActorRef<ClientSession>,
mut request_tracker: RequestTracker,
) {
loop {
let Some(conn) = bi.recv().await else {
return;
};
if dispatch_conn_message(&mut bi, &actor, &mut request_tracker, conn)
.await
.is_err()
{
return;
}
}
} }
impl GrpcTransport { async fn dispatch_conn_message(
pub fn new( bi: &mut GrpcBi<ClientRequest, ClientResponse>,
sender: mpsc::Sender<Result<ClientResponse, Status>>, actor: &ActorRef<ClientSession>,
receiver: Streaming<ClientRequest>, request_tracker: &mut RequestTracker,
) -> Self { conn: Result<ClientRequest, Status>,
Self { sender, receiver } ) -> Result<(), ()> {
let conn = match conn {
Ok(conn) => conn,
Err(err) => {
warn!(error = ?err, "Failed to receive client request");
return Err(());
} }
};
fn request_to_domain(request: ClientRequest) -> Result<DomainRequest, Status> { let request_id = match request_tracker.request(conn.request_id) {
match request.payload { Ok(request_id) => request_id,
Some(ClientRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest { Err(err) => {
pubkey, let _ = bi.send(Err(err)).await;
})) => Ok(DomainRequest::AuthChallengeRequest { pubkey }), return Err(());
Some(ClientRequestPayload::AuthChallengeSolution(
ProtoAuthChallengeSolution { signature },
)) => Ok(DomainRequest::AuthChallengeSolution { signature }),
None => Err(Status::invalid_argument("Missing client request payload")),
}
} }
};
let Some(payload) = conn.payload else {
let _ = bi
.send(Err(Status::invalid_argument(
"Missing client request payload",
)))
.await;
return Err(());
};
fn response_to_proto(response: DomainResponse) -> ClientResponse { let payload = match payload {
let payload = match response { ClientRequestPayload::QueryVaultState(_) => ClientResponsePayload::VaultState(
DomainResponse::AuthChallenge { pubkey, nonce } => { match actor.ask(HandleQueryVaultState {}).await {
ClientResponsePayload::AuthChallenge(ProtoAuthChallenge { pubkey, nonce }) Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
} Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed,
DomainResponse::AuthOk => ClientResponsePayload::AuthOk(ProtoAuthOk {}), Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed,
DomainResponse::ClientConnectError { code } => { Err(SendError::HandlerError(Error::Internal)) => ProtoVaultState::Error,
ClientResponsePayload::ClientConnectError(ClientConnectError { Err(err) => {
code: match code { warn!(error = ?err, "Failed to query vault state");
ConnectErrorCode::Unknown => ProtoClientConnectErrorCode::Unknown, ProtoVaultState::Error
ConnectErrorCode::ApprovalDenied => {
ProtoClientConnectErrorCode::ApprovalDenied
}
ConnectErrorCode::NoUserAgentsOnline => {
ProtoClientConnectErrorCode::NoUserAgentsOnline
} }
} }
.into(), .into(),
}) ),
payload => {
warn!(?payload, "Unsupported post-auth client request");
let _ = bi
.send(Err(Status::invalid_argument("Unsupported client request")))
.await;
return Err(());
} }
}; };
ClientResponse { bi.send(Ok(ClientResponse {
request_id: Some(request_id),
payload: Some(payload), 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 .await
.map_err(|_| TransportError::ChannelClosed) .map_err(|_| ())
} }
async fn recv(&mut self) -> Option<DomainRequest> { pub async fn start(conn: ClientConnection, mut bi: GrpcBi<ClientRequest, ClientResponse>) {
match self.receiver.next().await { let mut conn = conn;
Some(Ok(item)) => match Self::request_to_domain(item) { let mut request_tracker = RequestTracker::default();
Ok(request) => Some(request), let mut response_id = None;
Err(status) => {
let _ = self.sender.send(Err(status)).await; match auth::start(&mut conn, &mut bi, &mut request_tracker, &mut response_id).await {
None Ok(_) => {
let actor =
client::session::ClientSession::spawn(client::session::ClientSession::new(conn));
let actor_for_cleanup = actor.clone();
let _ = defer(move || {
actor_for_cleanup.kill();
});
info!("Client authenticated successfully");
dispatch_loop(bi, actor, request_tracker).await;
} }
}, Err(e) => {
Some(Err(error)) => { let mut transport = auth::AuthTransportAdapter::new(
tracing::error!(error = ?error, "grpc client recv failed; closing stream"); &mut bi,
None &mut request_tracker,
} &mut response_id,
None => None, );
let _ = transport.send(Err(e.clone())).await;
warn!(error = ?e, "Authentication failed");
} }
} }
} }
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,173 @@
use arbiter_proto::{
proto::client::{
AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest,
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
ClientRequest, ClientResponse, client_request::Payload as ClientRequestPayload,
client_response::Payload as ClientResponsePayload,
},
transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi},
};
use async_trait::async_trait;
use tonic::Status;
use tracing::warn;
use crate::{
actors::client::{self, ClientConnection, auth},
grpc::request_tracker::RequestTracker,
};
pub struct AuthTransportAdapter<'a> {
bi: &'a mut GrpcBi<ClientRequest, ClientResponse>,
request_tracker: &'a mut RequestTracker,
response_id: &'a mut Option<i32>,
}
impl<'a> AuthTransportAdapter<'a> {
pub fn new(
bi: &'a mut GrpcBi<ClientRequest, ClientResponse>,
request_tracker: &'a mut RequestTracker,
response_id: &'a mut Option<i32>,
) -> Self {
Self {
bi,
request_tracker,
response_id,
}
}
fn response_to_proto(response: auth::Outbound) -> ClientResponsePayload {
match response {
auth::Outbound::AuthChallenge { pubkey, nonce } => {
ClientResponsePayload::AuthChallenge(ProtoAuthChallenge {
pubkey: pubkey.to_bytes().to_vec(),
nonce,
})
}
auth::Outbound::AuthSuccess => {
ClientResponsePayload::AuthResult(ProtoAuthResult::Success.into())
}
}
}
fn error_to_proto(error: auth::Error) -> ClientResponsePayload {
ClientResponsePayload::AuthResult(
match error {
auth::Error::InvalidChallengeSolution => ProtoAuthResult::InvalidSignature,
auth::Error::ApproveError(auth::ApproveError::Denied) => {
ProtoAuthResult::ApprovalDenied
}
auth::Error::ApproveError(auth::ApproveError::Upstream(
crate::actors::router::ApprovalError::NoUserAgentsConnected,
)) => ProtoAuthResult::NoUserAgentsOnline,
auth::Error::ApproveError(auth::ApproveError::Internal)
| auth::Error::DatabasePoolUnavailable
| auth::Error::DatabaseOperationFailed
| auth::Error::Transport => ProtoAuthResult::Internal,
}
.into(),
)
}
async fn send_client_response(
&mut self,
payload: ClientResponsePayload,
) -> Result<(), TransportError> {
let request_id = self.response_id.take();
self.bi
.send(Ok(ClientResponse {
request_id,
payload: Some(payload),
}))
.await
}
async fn send_auth_result(&mut self, result: ProtoAuthResult) -> Result<(), TransportError> {
self.send_client_response(ClientResponsePayload::AuthResult(result.into()))
.await
}
}
#[async_trait]
impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
async fn send(
&mut self,
item: Result<auth::Outbound, auth::Error>,
) -> Result<(), TransportError> {
let payload = match item {
Ok(message) => AuthTransportAdapter::response_to_proto(message),
Err(err) => AuthTransportAdapter::error_to_proto(err),
};
self.send_client_response(payload).await
}
}
#[async_trait]
impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
async fn recv(&mut self) -> Option<auth::Inbound> {
let request = match self.bi.recv().await? {
Ok(request) => request,
Err(error) => {
warn!(error = ?error, "grpc client recv failed; closing stream");
return None;
}
};
let request_id = match self.request_tracker.request(request.request_id) {
Ok(request_id) => request_id,
Err(error) => {
let _ = self.bi.send(Err(error)).await;
return None;
}
};
*self.response_id = Some(request_id);
let payload = request.payload?;
match payload {
ClientRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest { pubkey }) => {
let Ok(pubkey) = <[u8; 32]>::try_from(pubkey) else {
let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await;
return None;
};
let Ok(pubkey) = ed25519_dalek::VerifyingKey::from_bytes(&pubkey) else {
let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await;
return None;
};
Some(auth::Inbound::AuthChallengeRequest { pubkey })
}
ClientRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution {
signature,
}) => {
let Ok(signature) = ed25519_dalek::Signature::try_from(signature.as_slice()) else {
let _ = self
.send_auth_result(ProtoAuthResult::InvalidSignature)
.await;
return None;
};
Some(auth::Inbound::AuthChallengeSolution { signature })
}
_ => {
let _ = self
.bi
.send(Err(Status::invalid_argument("Unsupported client auth request")))
.await;
None
}
}
}
}
impl Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {}
pub async fn start(
conn: &mut ClientConnection,
bi: &mut GrpcBi<ClientRequest, ClientResponse>,
request_tracker: &mut RequestTracker,
response_id: &mut Option<i32>,
) -> Result<(), auth::Error> {
let mut transport = AuthTransportAdapter::new(bi, request_tracker, response_id);
client::auth::authenticate(conn, &mut transport).await?;
Ok(())
}

View File

@@ -1,19 +1,21 @@
use arbiter_proto::{
use arbiter_proto::proto::{ proto::{
client::{ClientRequest, ClientResponse}, client::{ClientRequest, ClientResponse},
user_agent::{UserAgentRequest, UserAgentResponse}, user_agent::{UserAgentRequest, UserAgentResponse},
},
transport::grpc::GrpcBi,
}; };
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use tonic::{Request, Response, Status, async_trait}; use tonic::{Request, Response, Status, async_trait};
use tracing::info; use tracing::info;
use crate::{ use crate::{
DEFAULT_CHANNEL_SIZE, actors::{client::ClientConnection, user_agent::UserAgentConnection},
actors::{client::{ClientConnection, connect_client}, user_agent::{UserAgentConnection, connect_user_agent}}, grpc::user_agent::start,
}; };
pub mod client; pub mod client;
mod request_tracker;
pub mod user_agent; pub mod user_agent;
#[async_trait] #[async_trait]
@@ -27,19 +29,13 @@ impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Ser
request: Request<tonic::Streaming<ClientRequest>>, request: Request<tonic::Streaming<ClientRequest>>,
) -> Result<Response<Self::ClientStream>, Status> { ) -> Result<Response<Self::ClientStream>, Status> {
let req_stream = request.into_inner(); let req_stream = request.into_inner();
let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE); let (bi, rx) = GrpcBi::from_bi_stream(req_stream);
let props = ClientConnection::new(self.context.db.clone(), self.context.actors.clone());
let transport = client::GrpcTransport::new(tx, req_stream); tokio::spawn(client::start(props, bi));
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"); info!(event = "connection established", "grpc.client");
Ok(Response::new(ReceiverStream::new(rx))) Ok(Response::new(rx))
} }
#[tracing::instrument(level = "debug", skip(self))] #[tracing::instrument(level = "debug", skip(self))]
@@ -48,18 +44,19 @@ impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Ser
request: Request<tonic::Streaming<UserAgentRequest>>, request: Request<tonic::Streaming<UserAgentRequest>>,
) -> Result<Response<Self::UserAgentStream>, Status> { ) -> Result<Response<Self::UserAgentStream>, Status> {
let req_stream = request.into_inner(); let req_stream = request.into_inner();
let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
let transport = user_agent::GrpcTransport::new(tx, req_stream); let (bi, rx) = GrpcBi::from_bi_stream(req_stream);
let props = UserAgentConnection::new(
self.context.db.clone(), tokio::spawn(start(
self.context.actors.clone(), UserAgentConnection {
Box::new(transport), db: self.context.db.clone(),
); actors: self.context.actors.clone(),
tokio::spawn(connect_user_agent(props)); },
bi,
));
info!(event = "connection established", "grpc.user_agent"); info!(event = "connection established", "grpc.user_agent");
Ok(Response::new(ReceiverStream::new(rx))) Ok(Response::new(rx))
} }
} }

View File

@@ -0,0 +1,20 @@
use tonic::Status;
#[derive(Default)]
pub struct RequestTracker {
next_request_id: i32,
}
impl RequestTracker {
pub fn request(&mut self, id: i32) -> Result<i32, Status> {
if id < self.next_request_id {
return Err(Status::invalid_argument("Duplicate request id"));
}
self.next_request_id = id
.checked_add(1)
.ok_or_else(|| Status::invalid_argument("Invalid request id"))?;
Ok(id)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,180 @@
use arbiter_proto::{
proto::user_agent::{
AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest,
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
KeyType as ProtoKeyType, UserAgentRequest, UserAgentResponse,
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
},
transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi},
};
use async_trait::async_trait;
use tonic::Status;
use tracing::warn;
use crate::{
actors::user_agent::{AuthPublicKey, UserAgentConnection, auth},
db::models::KeyType,
grpc::request_tracker::RequestTracker,
};
pub struct AuthTransportAdapter<'a> {
bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>,
request_tracker: &'a mut RequestTracker,
response_id: &'a mut Option<i32>,
}
impl<'a> AuthTransportAdapter<'a> {
pub fn new(
bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>,
request_tracker: &'a mut RequestTracker,
response_id: &'a mut Option<i32>,
) -> Self {
Self {
bi,
request_tracker,
response_id,
}
}
async fn send_user_agent_response(
&mut self,
payload: UserAgentResponsePayload,
) -> Result<(), TransportError> {
let id = self.response_id.take();
self.bi
.send(Ok(UserAgentResponse {
id,
payload: Some(payload),
}))
.await
}
}
#[async_trait]
impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
async fn send(
&mut self,
item: Result<auth::Outbound, auth::Error>,
) -> Result<(), TransportError> {
use auth::{Error, Outbound};
let payload = match item {
Ok(Outbound::AuthChallenge { nonce }) => {
UserAgentResponsePayload::AuthChallenge(ProtoAuthChallenge { nonce })
}
Ok(Outbound::AuthSuccess) => {
UserAgentResponsePayload::AuthResult(ProtoAuthResult::Success.into())
}
Err(Error::UnregisteredPublicKey) => {
UserAgentResponsePayload::AuthResult(ProtoAuthResult::InvalidKey.into())
}
Err(Error::InvalidChallengeSolution) => {
UserAgentResponsePayload::AuthResult(ProtoAuthResult::InvalidSignature.into())
}
Err(Error::InvalidBootstrapToken) => {
UserAgentResponsePayload::AuthResult(ProtoAuthResult::TokenInvalid.into())
}
Err(Error::Internal { details }) => return self.bi.send(Err(Status::internal(details))).await,
Err(Error::Transport) => {
return self.bi.send(Err(Status::unavailable("transport error"))).await;
}
};
self.send_user_agent_response(payload).await
}
}
#[async_trait]
impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
async fn recv(&mut self) -> Option<auth::Inbound> {
let request = match self.bi.recv().await? {
Ok(request) => request,
Err(error) => {
warn!(error = ?error, "Failed to receive user agent auth request");
return None;
}
};
let request_id = match self.request_tracker.request(request.id) {
Ok(request_id) => request_id,
Err(error) => {
let _ = self.bi.send(Err(error)).await;
return None;
}
};
*self.response_id = Some(request_id);
let Some(payload) = request.payload else {
warn!(
event = "received request with empty payload",
"grpc.useragent.auth_adapter"
);
return None;
};
match payload {
UserAgentRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest {
pubkey,
bootstrap_token,
key_type,
}) => {
let Ok(key_type) = ProtoKeyType::try_from(key_type) else {
warn!(
event = "received request with invalid key type",
"grpc.useragent.auth_adapter"
);
return None;
};
let key_type = match key_type {
ProtoKeyType::Ed25519 => KeyType::Ed25519,
ProtoKeyType::EcdsaSecp256k1 => KeyType::EcdsaSecp256k1,
ProtoKeyType::Rsa => KeyType::Rsa,
ProtoKeyType::Unspecified => {
warn!(
event = "received request with unspecified key type",
"grpc.useragent.auth_adapter"
);
return None;
}
};
let Ok(pubkey) = AuthPublicKey::try_from((key_type, pubkey)) else {
warn!(
event = "received request with invalid public key",
"grpc.useragent.auth_adapter"
);
return None;
};
Some(auth::Inbound::AuthChallengeRequest {
pubkey,
bootstrap_token,
})
}
UserAgentRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution {
signature,
}) => Some(auth::Inbound::AuthChallengeSolution { signature }),
_ => {
let _ = self
.bi
.send(Err(Status::invalid_argument(
"Unsupported user-agent auth request",
)))
.await;
None
}
}
}
}
impl Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {}
pub async fn start(
conn: &mut UserAgentConnection,
bi: &mut GrpcBi<UserAgentRequest, UserAgentResponse>,
request_tracker: &mut RequestTracker,
response_id: &mut Option<i32>,
) -> Result<AuthPublicKey, auth::Error> {
let transport = AuthTransportAdapter::new(bi, request_tracker, response_id);
auth::authenticate(conn, transport).await
}

View File

@@ -1,10 +1,4 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic
)]
use crate::context::ServerContext; use crate::context::ServerContext;
pub mod actors; pub mod actors;
@@ -13,8 +7,7 @@ pub mod db;
pub mod evm; pub mod evm;
pub mod grpc; pub mod grpc;
pub mod safe_cell; pub mod safe_cell;
pub mod utils;
const DEFAULT_CHANNEL_SIZE: usize = 1000;
pub struct Server { pub struct Server {
context: ServerContext, context: ServerContext,
@@ -25,4 +18,3 @@ impl Server {
Self { context } Self { context }
} }
} }

View File

@@ -0,0 +1,16 @@
struct DeferClosure<F: FnOnce()> {
f: Option<F>,
}
impl<F: FnOnce()> Drop for DeferClosure<F> {
fn drop(&mut self) {
if let Some(f) = self.f.take() {
f();
}
}
}
// Run some code when a scope is exited, similar to Go's defer statement
pub fn defer<F: FnOnce()>(f: F) -> impl Drop + Sized {
DeferClosure { f: Some(f) }
}

View File

@@ -1,7 +1,7 @@
use arbiter_proto::transport::Bi; use arbiter_proto::transport::{Receiver, Sender};
use arbiter_server::actors::GlobalActors; use arbiter_server::actors::GlobalActors;
use arbiter_server::{ use arbiter_server::{
actors::client::{ClientConnection, Request, Response, connect_client}, actors::client::{ClientConnection, auth, connect_client},
db::{self, schema}, db::{self, schema},
}; };
use diesel::{ExpressionMethods as _, insert_into}; use diesel::{ExpressionMethods as _, insert_into};
@@ -17,15 +17,17 @@ pub async fn test_unregistered_pubkey_rejected() {
let (server_transport, mut test_transport) = ChannelTransport::new(); let (server_transport, mut test_transport) = ChannelTransport::new();
let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let actors = GlobalActors::spawn(db.clone()).await.unwrap();
let props = ClientConnection::new(db.clone(), Box::new(server_transport), actors); let props = ClientConnection::new(db.clone(), actors);
let task = tokio::spawn(connect_client(props)); let task = tokio::spawn(async move {
let mut server_transport = server_transport;
connect_client(props, &mut server_transport).await;
});
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
test_transport test_transport
.send(Request::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: pubkey_bytes, pubkey: new_key.verifying_key(),
}) })
.await .await
.unwrap(); .unwrap();
@@ -54,13 +56,16 @@ pub async fn test_challenge_auth() {
let (server_transport, mut test_transport) = ChannelTransport::new(); let (server_transport, mut test_transport) = ChannelTransport::new();
let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let actors = GlobalActors::spawn(db.clone()).await.unwrap();
let props = ClientConnection::new(db.clone(), Box::new(server_transport), actors); let props = ClientConnection::new(db.clone(), actors);
let task = tokio::spawn(connect_client(props)); let task = tokio::spawn(async move {
let mut server_transport = server_transport;
connect_client(props, &mut server_transport).await;
});
// Send challenge request // Send challenge request
test_transport test_transport
.send(Request::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: pubkey_bytes, pubkey: new_key.verifying_key(),
}) })
.await .await
.unwrap(); .unwrap();
@@ -72,23 +77,32 @@ pub async fn test_challenge_auth() {
.expect("should receive challenge"); .expect("should receive challenge");
let challenge = match response { let challenge = match response {
Ok(resp) => match resp { Ok(resp) => match resp {
Response::AuthChallenge { pubkey, nonce } => (pubkey, nonce), auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
other => panic!("Expected AuthChallenge, got {other:?}"), other => panic!("Expected AuthChallenge, got {other:?}"),
}, },
Err(err) => panic!("Expected Ok response, got Err({err:?})"), Err(err) => panic!("Expected Ok response, got Err({err:?})"),
}; };
// Sign the challenge and send solution // Sign the challenge and send solution
let formatted_challenge = arbiter_proto::format_challenge(challenge.1, &challenge.0); let formatted_challenge = arbiter_proto::format_challenge(challenge.1, challenge.0.as_bytes());
let signature = new_key.sign(&formatted_challenge); let signature = new_key.sign(&formatted_challenge);
test_transport test_transport
.send(Request::AuthChallengeSolution { .send(auth::Inbound::AuthChallengeSolution { signature })
signature: signature.to_bytes().to_vec(),
})
.await .await
.unwrap(); .unwrap();
let response = test_transport
.recv()
.await
.expect("should receive auth success");
match response {
Ok(auth::Outbound::AuthSuccess) => {}
Ok(other) => panic!("Expected AuthSuccess, got {other:?}"),
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
}
// Auth completes, session spawned // Auth completes, session spawned
task.await.unwrap(); task.await.unwrap();
} }

View File

@@ -1,7 +1,8 @@
use arbiter_proto::transport::{Bi, Error}; use arbiter_proto::transport::{Bi, Error, Receiver, Sender};
use arbiter_server::{ use arbiter_server::{
actors::keyholder::KeyHolder, actors::keyholder::KeyHolder,
db::{self, schema}, safe_cell::{SafeCell, SafeCellHandle as _}, db::{self, schema},
safe_cell::{SafeCell, SafeCellHandle as _},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use diesel::QueryDsl; use diesel::QueryDsl;
@@ -54,10 +55,10 @@ impl<T, Y> ChannelTransport<T, Y> {
} }
#[async_trait] #[async_trait]
impl<T, Y> Bi<T, Y> for ChannelTransport<T, Y> impl<T, Y> Sender<Y> for ChannelTransport<T, Y>
where where
T: Send + 'static, T: Send + Sync + 'static,
Y: Send + 'static, Y: Send + Sync + 'static,
{ {
async fn send(&mut self, item: Y) -> Result<(), Error> { async fn send(&mut self, item: Y) -> Result<(), Error> {
self.sender self.sender
@@ -65,8 +66,22 @@ where
.await .await
.map_err(|_| Error::ChannelClosed) .map_err(|_| Error::ChannelClosed)
} }
}
#[async_trait]
impl<T, Y> Receiver<T> for ChannelTransport<T, Y>
where
T: Send + Sync + 'static,
Y: Send + Sync + 'static,
{
async fn recv(&mut self) -> Option<T> { async fn recv(&mut self) -> Option<T> {
self.receiver.recv().await self.receiver.recv().await
} }
} }
impl<T, Y> Bi<T, Y> for ChannelTransport<T, Y>
where
T: Send + Sync + 'static,
Y: Send + Sync + 'static,
{
}

View File

@@ -1,9 +1,9 @@
use arbiter_proto::transport::Bi; use arbiter_proto::transport::{Receiver, Sender};
use arbiter_server::{ use arbiter_server::{
actors::{ actors::{
GlobalActors, GlobalActors,
bootstrap::GetToken, bootstrap::GetToken,
user_agent::{AuthPublicKey, Request, Response, UserAgentConnection, connect_user_agent}, user_agent::{AuthPublicKey, UserAgentConnection, auth},
}, },
db::{self, schema}, db::{self, schema},
}; };
@@ -21,19 +21,31 @@ pub async fn test_bootstrap_token_auth() {
let token = actors.bootstrapper.ask(GetToken).await.unwrap().unwrap(); let token = actors.bootstrapper.ask(GetToken).await.unwrap().unwrap();
let (server_transport, mut test_transport) = ChannelTransport::new(); let (server_transport, mut test_transport) = ChannelTransport::new();
let props = UserAgentConnection::new(db.clone(), actors, Box::new(server_transport)); let db_for_task = db.clone();
let task = tokio::spawn(connect_user_agent(props)); let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors);
auth::authenticate(&mut props, server_transport).await
});
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
test_transport test_transport
.send(Request::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
bootstrap_token: Some(token), bootstrap_token: Some(token),
}) })
.await .await
.unwrap(); .unwrap();
task.await.unwrap(); let response = test_transport
.recv()
.await
.expect("should receive auth result");
match response {
Ok(auth::Outbound::AuthSuccess) => {}
other => panic!("Expected AuthSuccess, got {other:?}"),
}
task.await.unwrap().unwrap();
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let stored_pubkey: Vec<u8> = schema::useragent_client::table let stored_pubkey: Vec<u8> = schema::useragent_client::table
@@ -51,20 +63,25 @@ pub async fn test_bootstrap_invalid_token_auth() {
let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let actors = GlobalActors::spawn(db.clone()).await.unwrap();
let (server_transport, mut test_transport) = ChannelTransport::new(); let (server_transport, mut test_transport) = ChannelTransport::new();
let props = UserAgentConnection::new(db.clone(), actors, Box::new(server_transport)); let db_for_task = db.clone();
let task = tokio::spawn(connect_user_agent(props)); let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors);
auth::authenticate(&mut props, server_transport).await
});
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
test_transport test_transport
.send(Request::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
bootstrap_token: Some("invalid_token".to_string()), bootstrap_token: Some("invalid_token".to_string()),
}) })
.await .await
.unwrap(); .unwrap();
// Auth fails, connect_user_agent returns, transport drops assert!(matches!(
task.await.unwrap(); task.await.unwrap(),
Err(auth::Error::InvalidBootstrapToken)
));
// Verify no key was registered // Verify no key was registered
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
@@ -99,12 +116,15 @@ pub async fn test_challenge_auth() {
} }
let (server_transport, mut test_transport) = ChannelTransport::new(); let (server_transport, mut test_transport) = ChannelTransport::new();
let props = UserAgentConnection::new(db.clone(), actors, Box::new(server_transport)); let db_for_task = db.clone();
let task = tokio::spawn(connect_user_agent(props)); let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors);
auth::authenticate(&mut props, server_transport).await
});
// Send challenge request // Send challenge request
test_transport test_transport
.send(Request::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
bootstrap_token: None, bootstrap_token: None,
}) })
@@ -118,7 +138,7 @@ pub async fn test_challenge_auth() {
.expect("should receive challenge"); .expect("should receive challenge");
let challenge = match response { let challenge = match response {
Ok(resp) => match resp { Ok(resp) => match resp {
Response::AuthChallenge { nonce } => nonce, auth::Outbound::AuthChallenge { nonce } => nonce,
other => panic!("Expected AuthChallenge, got {other:?}"), other => panic!("Expected AuthChallenge, got {other:?}"),
}, },
Err(err) => panic!("Expected Ok response, got Err({err:?})"), Err(err) => panic!("Expected Ok response, got Err({err:?})"),
@@ -128,12 +148,20 @@ pub async fn test_challenge_auth() {
let signature = new_key.sign(&formatted_challenge); let signature = new_key.sign(&formatted_challenge);
test_transport test_transport
.send(Request::AuthChallengeSolution { .send(auth::Inbound::AuthChallengeSolution {
signature: signature.to_bytes().to_vec(), signature: signature.to_bytes().to_vec(),
}) })
.await .await
.unwrap(); .unwrap();
// Auth completes, session spawned let response = test_transport
task.await.unwrap(); .recv()
.await
.expect("should receive auth result");
match response {
Ok(auth::Outbound::AuthSuccess) => {}
other => panic!("Expected AuthSuccess, got {other:?}"),
}
task.await.unwrap().unwrap();
} }

View File

@@ -2,15 +2,20 @@ use arbiter_server::{
actors::{ actors::{
GlobalActors, GlobalActors,
keyholder::{Bootstrap, Seal}, keyholder::{Bootstrap, Seal},
user_agent::{Request, Response, UnsealError, session::UserAgentSession}, user_agent::session::{
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError, UserAgentSession,
},
}, },
db, db,
safe_cell::{SafeCell, SafeCellHandle as _}, safe_cell::{SafeCell, SafeCellHandle as _},
}; };
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use kameo::actor::Spawn as _;
use x25519_dalek::{EphemeralSecret, PublicKey}; use x25519_dalek::{EphemeralSecret, PublicKey};
async fn setup_sealed_user_agent(seal_key: &[u8]) -> (db::DatabasePool, UserAgentSession) { async fn setup_sealed_user_agent(
seal_key: &[u8],
) -> (db::DatabasePool, kameo::actor::ActorRef<UserAgentSession>) {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let actors = GlobalActors::spawn(db.clone()).await.unwrap();
@@ -23,26 +28,26 @@ async fn setup_sealed_user_agent(seal_key: &[u8]) -> (db::DatabasePool, UserAgen
.unwrap(); .unwrap();
actors.key_holder.ask(Seal).await.unwrap(); actors.key_holder.ask(Seal).await.unwrap();
let session = UserAgentSession::new_test(db.clone(), actors); let session = UserAgentSession::spawn(UserAgentSession::new_test(db.clone(), actors));
(db, session) (db, session)
} }
async fn client_dh_encrypt(user_agent: &mut UserAgentSession, key_to_send: &[u8]) -> Request { async fn client_dh_encrypt(
user_agent: &kameo::actor::ActorRef<UserAgentSession>,
key_to_send: &[u8],
) -> HandleUnsealEncryptedKey {
let client_secret = EphemeralSecret::random(); let client_secret = EphemeralSecret::random();
let client_public = PublicKey::from(&client_secret); let client_public = PublicKey::from(&client_secret);
let response = user_agent let response = user_agent
.process_transport_inbound(Request::UnsealStart { .ask(HandleUnsealRequest {
client_pubkey: client_public, client_pubkey: client_public,
}) })
.await .await
.unwrap(); .unwrap();
let server_pubkey = match response { let server_pubkey = response.server_pubkey;
Response::UnsealStartResponse { server_pubkey } => server_pubkey,
other => panic!("Expected UnsealStartResponse, got {other:?}"),
};
let shared_secret = client_secret.diffie_hellman(&server_pubkey); let shared_secret = client_secret.diffie_hellman(&server_pubkey);
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into()); let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
@@ -53,7 +58,7 @@ async fn client_dh_encrypt(user_agent: &mut UserAgentSession, key_to_send: &[u8]
.encrypt_in_place(&nonce, associated_data, &mut ciphertext) .encrypt_in_place(&nonce, associated_data, &mut ciphertext)
.unwrap(); .unwrap();
Request::UnsealEncryptedKey { HandleUnsealEncryptedKey {
nonce: nonce.to_vec(), nonce: nonce.to_vec(),
ciphertext, ciphertext,
associated_data: associated_data.to_vec(), associated_data: associated_data.to_vec(),
@@ -64,63 +69,58 @@ async fn client_dh_encrypt(user_agent: &mut UserAgentSession, key_to_send: &[u8]
#[test_log::test] #[test_log::test]
pub async fn test_unseal_success() { pub async fn test_unseal_success() {
let seal_key = b"test-seal-key"; let seal_key = b"test-seal-key";
let (_db, mut user_agent) = setup_sealed_user_agent(seal_key).await; let (_db, user_agent) = setup_sealed_user_agent(seal_key).await;
let encrypted_key = client_dh_encrypt(&mut user_agent, seal_key).await; let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await;
let response = user_agent let response = user_agent.ask(encrypted_key).await;
.process_transport_inbound(encrypted_key) assert!(matches!(response, Ok(())));
.await
.unwrap();
assert!(matches!(response, Response::UnsealResult(Ok(()))));
} }
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn test_unseal_wrong_seal_key() { pub async fn test_unseal_wrong_seal_key() {
let (_db, mut user_agent) = setup_sealed_user_agent(b"correct-key").await; let (_db, user_agent) = setup_sealed_user_agent(b"correct-key").await;
let encrypted_key = client_dh_encrypt(&mut user_agent, b"wrong-key").await; let encrypted_key = client_dh_encrypt(&user_agent, b"wrong-key").await;
let response = user_agent
.process_transport_inbound(encrypted_key)
.await
.unwrap();
let response = user_agent.ask(encrypted_key).await;
assert!(matches!( assert!(matches!(
response, response,
Response::UnsealResult(Err(UnsealError::InvalidKey)) Err(kameo::error::SendError::HandlerError(
UnsealError::InvalidKey
))
)); ));
} }
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn test_unseal_corrupted_ciphertext() { pub async fn test_unseal_corrupted_ciphertext() {
let (_db, mut user_agent) = setup_sealed_user_agent(b"test-key").await; let (_db, user_agent) = setup_sealed_user_agent(b"test-key").await;
let client_secret = EphemeralSecret::random(); let client_secret = EphemeralSecret::random();
let client_public = PublicKey::from(&client_secret); let client_public = PublicKey::from(&client_secret);
user_agent user_agent
.process_transport_inbound(Request::UnsealStart { .ask(HandleUnsealRequest {
client_pubkey: client_public, client_pubkey: client_public,
}) })
.await .await
.unwrap(); .unwrap();
let response = user_agent let response = user_agent
.process_transport_inbound(Request::UnsealEncryptedKey { .ask(HandleUnsealEncryptedKey {
nonce: vec![0u8; 24], nonce: vec![0u8; 24],
ciphertext: vec![0u8; 32], ciphertext: vec![0u8; 32],
associated_data: vec![], associated_data: vec![],
}) })
.await .await;
.unwrap();
assert!(matches!( assert!(matches!(
response, response,
Response::UnsealResult(Err(UnsealError::InvalidKey)) Err(kameo::error::SendError::HandlerError(
UnsealError::InvalidKey
))
)); ));
} }
@@ -128,30 +128,24 @@ pub async fn test_unseal_corrupted_ciphertext() {
#[test_log::test] #[test_log::test]
pub async fn test_unseal_retry_after_invalid_key() { pub async fn test_unseal_retry_after_invalid_key() {
let seal_key = b"real-seal-key"; let seal_key = b"real-seal-key";
let (_db, mut user_agent) = setup_sealed_user_agent(seal_key).await; let (_db, user_agent) = setup_sealed_user_agent(seal_key).await;
{ {
let encrypted_key = client_dh_encrypt(&mut user_agent, b"wrong-key").await; let encrypted_key = client_dh_encrypt(&user_agent, b"wrong-key").await;
let response = user_agent
.process_transport_inbound(encrypted_key)
.await
.unwrap();
let response = user_agent.ask(encrypted_key).await;
assert!(matches!( assert!(matches!(
response, response,
Response::UnsealResult(Err(UnsealError::InvalidKey)) Err(kameo::error::SendError::HandlerError(
UnsealError::InvalidKey
))
)); ));
} }
{ {
let encrypted_key = client_dh_encrypt(&mut user_agent, seal_key).await; let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await;
let response = user_agent let response = user_agent.ask(encrypted_key).await;
.process_transport_inbound(encrypted_key) assert!(matches!(response, Ok(())));
.await
.unwrap();
assert!(matches!(response, Response::UnsealResult(Ok(()))));
} }
} }

View File

@@ -9,13 +9,49 @@ import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:grpc/grpc.dart'; import 'package:grpc/grpc.dart';
import 'package:mtcore/markettakers.dart'; import 'package:mtcore/markettakers.dart';
class AuthorizationException implements Exception {
const AuthorizationException(this.result);
final AuthResult result;
String get message => switch (result) {
AuthResult.AUTH_RESULT_INVALID_KEY =>
'Authentication failed: this device key is not registered on the server.',
AuthResult.AUTH_RESULT_INVALID_SIGNATURE =>
'Authentication failed: the server rejected the signature for this device key.',
AuthResult.AUTH_RESULT_BOOTSTRAP_REQUIRED =>
'Authentication failed: the server requires bootstrap before this device can connect.',
AuthResult.AUTH_RESULT_TOKEN_INVALID =>
'Authentication failed: the bootstrap token is invalid.',
AuthResult.AUTH_RESULT_INTERNAL =>
'Authentication failed: the server hit an internal error.',
AuthResult.AUTH_RESULT_UNSPECIFIED =>
'Authentication failed: the server returned an unspecified auth error.',
AuthResult.AUTH_RESULT_SUCCESS => 'Authentication succeeded.',
_ => 'Authentication failed: ${result.name}.',
};
@override
String toString() => message;
}
class ConnectionException implements Exception {
const ConnectionException(this.message);
final String message;
@override
String toString() => message;
}
Future<Connection> connectAndAuthorize( Future<Connection> connectAndAuthorize(
StoredServerInfo serverInfo, StoredServerInfo serverInfo,
KeyHandle key, { KeyHandle key, {
String? bootstrapToken, String? bootstrapToken,
}) async { }) async {
Connection? connection;
try { try {
final connection = await _connect(serverInfo); connection = await _connect(serverInfo);
talker.info( talker.info(
'Connected to server at ${serverInfo.address}:${serverInfo.port}', 'Connected to server at ${serverInfo.address}:${serverInfo.port}',
); );
@@ -30,21 +66,24 @@ Future<Connection> connectAndAuthorize(
KeyAlgorithm.ed25519 => KeyType.KEY_TYPE_ED25519, KeyAlgorithm.ed25519 => KeyType.KEY_TYPE_ED25519,
}, },
); );
await connection.send(UserAgentRequest(authChallengeRequest: req)); final response = await connection.request(
UserAgentRequest(authChallengeRequest: req),
);
talker.info( talker.info(
"Sent auth challenge request with pubkey ${base64Encode(pubkey)}", "Sent auth challenge request with pubkey ${base64Encode(pubkey)}",
); );
final response = await connection.receive();
talker.info('Received response from server, checking auth flow...'); talker.info('Received response from server, checking auth flow...');
if (response.hasAuthOk()) { if (response.hasAuthResult()) {
if (response.authResult != AuthResult.AUTH_RESULT_SUCCESS) {
throw AuthorizationException(response.authResult);
}
talker.info('Authentication successful, connection established'); talker.info('Authentication successful, connection established');
return connection; return connection;
} }
if (!response.hasAuthChallenge()) { if (!response.hasAuthChallenge()) {
throw Exception( throw ConnectionException(
'Expected AuthChallengeResponse, got ${response.whichPayload()}', 'Expected AuthChallengeResponse, got ${response.whichPayload()}',
); );
} }
@@ -55,23 +94,35 @@ Future<Connection> connectAndAuthorize(
); );
final signature = await key.sign(challenge); final signature = await key.sign(challenge);
await connection.send( final solutionResponse = await connection.request(
UserAgentRequest(authChallengeSolution: AuthChallengeSolution(signature: signature)), UserAgentRequest(authChallengeSolution: AuthChallengeSolution(signature: signature)),
); );
talker.info('Sent auth challenge solution, waiting for server response...'); talker.info('Sent auth challenge solution, waiting for server response...');
final solutionResponse = await connection.receive(); if (!solutionResponse.hasAuthResult()) {
if (!solutionResponse.hasAuthOk()) { throw ConnectionException(
throw Exception(
'Expected AuthChallengeSolutionResponse, got ${solutionResponse.whichPayload()}', 'Expected AuthChallengeSolutionResponse, got ${solutionResponse.whichPayload()}',
); );
} }
if (solutionResponse.authResult != AuthResult.AUTH_RESULT_SUCCESS) {
throw AuthorizationException(solutionResponse.authResult);
}
talker.info('Authentication successful, connection established'); talker.info('Authentication successful, connection established');
return connection; return connection;
} on AuthorizationException {
await connection?.close();
rethrow;
} on GrpcError catch (error) {
await connection?.close();
throw ConnectionException('Failed to connect to server: ${error.message}');
} catch (e) { } catch (e) {
throw Exception('Failed to connect to server: $e'); await connection?.close();
if (e is ConnectionException) {
rethrow;
}
throw ConnectionException('Failed to connect to server: $e');
} }
} }

View File

@@ -5,33 +5,113 @@ import 'package:grpc/grpc.dart';
import 'package:mtcore/markettakers.dart'; import 'package:mtcore/markettakers.dart';
class Connection { class Connection {
final ClientChannel channel;
final StreamController<UserAgentRequest> _tx;
final StreamIterator<UserAgentResponse> _rx;
Connection({ Connection({
required this.channel, required this.channel,
required StreamController<UserAgentRequest> tx, required StreamController<UserAgentRequest> tx,
required ResponseStream<UserAgentResponse> rx, required ResponseStream<UserAgentResponse> rx,
}) : _tx = tx, }) : _tx = tx {
_rx = StreamIterator(rx); _rxSubscription = rx.listen(
_handleResponse,
Future<void> send(UserAgentRequest request) async { onError: _handleError,
talker.debug('Sending request: ${request.toDebugString()}'); onDone: _handleDone,
_tx.add(request); cancelOnError: true,
);
} }
Future<UserAgentResponse> receive() async { final ClientChannel channel;
final hasValue = await _rx.moveNext(); final StreamController<UserAgentRequest> _tx;
if (!hasValue) { final Map<int, Completer<UserAgentResponse>> _pendingRequests = {};
throw Exception('Connection closed while waiting for server response.'); final StreamController<UserAgentResponse> _outOfBandMessages =
StreamController<UserAgentResponse>.broadcast();
StreamSubscription<UserAgentResponse>? _rxSubscription;
int _nextRequestId = 0;
Stream<UserAgentResponse> get outOfBandMessages => _outOfBandMessages.stream;
Future<UserAgentResponse> request(UserAgentRequest message) async {
_ensureOpen();
final requestId = _nextRequestId++;
final completer = Completer<UserAgentResponse>();
_pendingRequests[requestId] = completer;
message.id = requestId;
talker.debug('Sending request: ${message.toDebugString()}');
try {
_tx.add(message);
} catch (error, stackTrace) {
_pendingRequests.remove(requestId);
completer.completeError(error, stackTrace);
} }
talker.debug('Received response: ${_rx.current.toDebugString()}');
return _rx.current; return completer.future;
} }
Future<void> close() async { Future<void> close() async {
final rxSubscription = _rxSubscription;
if (rxSubscription == null) {
return;
}
_rxSubscription = null;
await rxSubscription.cancel();
_failPendingRequests(Exception('Connection closed.'));
await _outOfBandMessages.close();
await _tx.close(); await _tx.close();
await channel.shutdown(); await channel.shutdown();
} }
void _handleResponse(UserAgentResponse response) {
talker.debug('Received response: ${response.toDebugString()}');
if (response.hasId()) {
final completer = _pendingRequests.remove(response.id);
if (completer == null) {
talker.warning('Received response for unknown request id ${response.id}');
return;
}
completer.complete(response);
return;
}
_outOfBandMessages.add(response);
}
void _handleError(Object error, StackTrace stackTrace) {
_rxSubscription = null;
_failPendingRequests(error, stackTrace);
_outOfBandMessages.addError(error, stackTrace);
}
void _handleDone() {
if (_rxSubscription == null) {
return;
}
_rxSubscription = null;
final error = Exception(
'Connection closed while waiting for server response.',
);
_failPendingRequests(error);
_outOfBandMessages.close();
}
void _failPendingRequests(Object error, [StackTrace? stackTrace]) {
final pendingRequests = _pendingRequests.values.toList(growable: false);
_pendingRequests.clear();
for (final completer in pendingRequests) {
if (!completer.isCompleted) {
completer.completeError(error, stackTrace);
}
}
}
void _ensureOpen() {
if (_rxSubscription == null) {
throw StateError('Connection is closed.');
}
}
} }

View File

@@ -4,9 +4,9 @@ import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart'; import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
Future<List<WalletEntry>> listEvmWallets(Connection connection) async { Future<List<WalletEntry>> listEvmWallets(Connection connection) async {
await connection.send(UserAgentRequest(evmWalletList: Empty())); final response = await connection.request(
UserAgentRequest(evmWalletList: Empty()),
final response = await connection.receive(); );
if (!response.hasEvmWalletList()) { if (!response.hasEvmWalletList()) {
throw Exception( throw Exception(
'Expected EVM wallet list response, got ${response.whichPayload()}', 'Expected EVM wallet list response, got ${response.whichPayload()}',
@@ -25,9 +25,9 @@ Future<List<WalletEntry>> listEvmWallets(Connection connection) async {
} }
Future<void> createEvmWallet(Connection connection) async { Future<void> createEvmWallet(Connection connection) async {
await connection.send(UserAgentRequest(evmWalletCreate: Empty())); final response = await connection.request(
UserAgentRequest(evmWalletCreate: Empty()),
final response = await connection.receive(); );
if (!response.hasEvmWalletCreate()) { if (!response.hasEvmWalletCreate()) {
throw Exception( throw Exception(
'Expected EVM wallet create response, got ${response.whichPayload()}', 'Expected EVM wallet create response, got ${response.whichPayload()}',

View File

@@ -13,9 +13,9 @@ Future<List<GrantEntry>> listEvmGrants(
request.walletId = walletId; request.walletId = walletId;
} }
await connection.send(UserAgentRequest(evmGrantList: request)); final response = await connection.request(
UserAgentRequest(evmGrantList: request),
final response = await connection.receive(); );
if (!response.hasEvmGrantList()) { if (!response.hasEvmGrantList()) {
throw Exception( throw Exception(
'Expected EVM grant list response, got ${response.whichPayload()}', 'Expected EVM grant list response, got ${response.whichPayload()}',
@@ -45,7 +45,7 @@ Future<int> createEvmGrant(
TransactionRateLimit? rateLimit, TransactionRateLimit? rateLimit,
required SpecificGrant specific, required SpecificGrant specific,
}) async { }) async {
await connection.send( final response = await connection.request(
UserAgentRequest( UserAgentRequest(
evmGrantCreate: EvmGrantCreateRequest( evmGrantCreate: EvmGrantCreateRequest(
clientId: clientId, clientId: clientId,
@@ -62,8 +62,6 @@ Future<int> createEvmGrant(
), ),
), ),
); );
final response = await connection.receive();
if (!response.hasEvmGrantCreate()) { if (!response.hasEvmGrantCreate()) {
throw Exception( throw Exception(
'Expected EVM grant create response, got ${response.whichPayload()}', 'Expected EVM grant create response, got ${response.whichPayload()}',
@@ -82,11 +80,9 @@ Future<int> createEvmGrant(
} }
Future<void> deleteEvmGrant(Connection connection, int grantId) async { Future<void> deleteEvmGrant(Connection connection, int grantId) async {
await connection.send( final response = await connection.request(
UserAgentRequest(evmGrantDelete: EvmGrantDeleteRequest(grantId: grantId)), UserAgentRequest(evmGrantDelete: EvmGrantDeleteRequest(grantId: grantId)),
); );
final response = await connection.receive();
if (!response.hasEvmGrantDelete()) { if (!response.hasEvmGrantDelete()) {
throw Exception( throw Exception(
'Expected EVM grant delete response, got ${response.whichPayload()}', 'Expected EVM grant delete response, got ${response.whichPayload()}',

View File

@@ -10,7 +10,7 @@ Future<BootstrapResult> bootstrapVault(
) async { ) async {
final encryptedKey = await _encryptVaultKeyMaterial(connection, password); final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
await connection.send( final response = await connection.request(
UserAgentRequest( UserAgentRequest(
bootstrapEncryptedKey: BootstrapEncryptedKey( bootstrapEncryptedKey: BootstrapEncryptedKey(
nonce: encryptedKey.nonce, nonce: encryptedKey.nonce,
@@ -19,8 +19,6 @@ Future<BootstrapResult> bootstrapVault(
), ),
), ),
); );
final response = await connection.receive();
if (!response.hasBootstrapResult()) { if (!response.hasBootstrapResult()) {
throw Exception( throw Exception(
'Expected bootstrap result, got ${response.whichPayload()}', 'Expected bootstrap result, got ${response.whichPayload()}',
@@ -33,7 +31,7 @@ Future<BootstrapResult> bootstrapVault(
Future<UnsealResult> unsealVault(Connection connection, String password) async { Future<UnsealResult> unsealVault(Connection connection, String password) async {
final encryptedKey = await _encryptVaultKeyMaterial(connection, password); final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
await connection.send( final response = await connection.request(
UserAgentRequest( UserAgentRequest(
unsealEncryptedKey: UnsealEncryptedKey( unsealEncryptedKey: UnsealEncryptedKey(
nonce: encryptedKey.nonce, nonce: encryptedKey.nonce,
@@ -42,8 +40,6 @@ Future<UnsealResult> unsealVault(Connection connection, String password) async {
), ),
), ),
); );
final response = await connection.receive();
if (!response.hasUnsealResult()) { if (!response.hasUnsealResult()) {
throw Exception('Expected unseal result, got ${response.whichPayload()}'); throw Exception('Expected unseal result, got ${response.whichPayload()}');
} }
@@ -60,11 +56,9 @@ Future<_EncryptedVaultKey> _encryptVaultKeyMaterial(
final clientKeyPair = await keyExchange.newKeyPair(); final clientKeyPair = await keyExchange.newKeyPair();
final clientPublicKey = await clientKeyPair.extractPublicKey(); final clientPublicKey = await clientKeyPair.extractPublicKey();
await connection.send( final handshakeResponse = await connection.request(
UserAgentRequest(unsealStart: UnsealStart(clientPubkey: clientPublicKey.bytes)), UserAgentRequest(unsealStart: UnsealStart(clientPubkey: clientPublicKey.bytes)),
); );
final handshakeResponse = await connection.receive();
if (!handshakeResponse.hasUnsealStartResponse()) { if (!handshakeResponse.hasUnsealStartResponse()) {
throw Exception( throw Exception(
'Expected unseal handshake response, got ${handshakeResponse.whichPayload()}', 'Expected unseal handshake response, got ${handshakeResponse.whichPayload()}',

View File

@@ -13,9 +13,10 @@
import 'dart:core' as $core; import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb; import 'package:protobuf/protobuf.dart' as $pb;
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart' as $0;
import 'client.pbenum.dart'; import 'client.pbenum.dart';
import 'evm.pb.dart' as $0; import 'evm.pb.dart' as $1;
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
@@ -199,46 +200,10 @@ class AuthChallengeSolution extends $pb.GeneratedMessage {
void clearSignature() => $_clearField(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 { enum ClientRequest_Payload {
authChallengeRequest, authChallengeRequest,
authChallengeSolution, authChallengeSolution,
queryVaultState,
notSet notSet
} }
@@ -246,12 +211,16 @@ class ClientRequest extends $pb.GeneratedMessage {
factory ClientRequest({ factory ClientRequest({
AuthChallengeRequest? authChallengeRequest, AuthChallengeRequest? authChallengeRequest,
AuthChallengeSolution? authChallengeSolution, AuthChallengeSolution? authChallengeSolution,
$0.Empty? queryVaultState,
$core.int? requestId,
}) { }) {
final result = create(); final result = create();
if (authChallengeRequest != null) if (authChallengeRequest != null)
result.authChallengeRequest = authChallengeRequest; result.authChallengeRequest = authChallengeRequest;
if (authChallengeSolution != null) if (authChallengeSolution != null)
result.authChallengeSolution = authChallengeSolution; result.authChallengeSolution = authChallengeSolution;
if (queryVaultState != null) result.queryVaultState = queryVaultState;
if (requestId != null) result.requestId = requestId;
return result; return result;
} }
@@ -268,19 +237,23 @@ class ClientRequest extends $pb.GeneratedMessage {
_ClientRequest_PayloadByTag = { _ClientRequest_PayloadByTag = {
1: ClientRequest_Payload.authChallengeRequest, 1: ClientRequest_Payload.authChallengeRequest,
2: ClientRequest_Payload.authChallengeSolution, 2: ClientRequest_Payload.authChallengeSolution,
3: ClientRequest_Payload.queryVaultState,
0: ClientRequest_Payload.notSet 0: ClientRequest_Payload.notSet
}; };
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ClientRequest', _omitMessageNames ? '' : 'ClientRequest',
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'), package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
createEmptyInstance: create) createEmptyInstance: create)
..oo(0, [1, 2]) ..oo(0, [1, 2, 3])
..aOM<AuthChallengeRequest>( ..aOM<AuthChallengeRequest>(
1, _omitFieldNames ? '' : 'authChallengeRequest', 1, _omitFieldNames ? '' : 'authChallengeRequest',
subBuilder: AuthChallengeRequest.create) subBuilder: AuthChallengeRequest.create)
..aOM<AuthChallengeSolution>( ..aOM<AuthChallengeSolution>(
2, _omitFieldNames ? '' : 'authChallengeSolution', 2, _omitFieldNames ? '' : 'authChallengeSolution',
subBuilder: AuthChallengeSolution.create) subBuilder: AuthChallengeSolution.create)
..aOM<$0.Empty>(3, _omitFieldNames ? '' : 'queryVaultState',
subBuilder: $0.Empty.create)
..aI(4, _omitFieldNames ? '' : 'requestId')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@@ -304,10 +277,12 @@ class ClientRequest extends $pb.GeneratedMessage {
@$pb.TagNumber(1) @$pb.TagNumber(1)
@$pb.TagNumber(2) @$pb.TagNumber(2)
@$pb.TagNumber(3)
ClientRequest_Payload whichPayload() => ClientRequest_Payload whichPayload() =>
_ClientRequest_PayloadByTag[$_whichOneof(0)]!; _ClientRequest_PayloadByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1) @$pb.TagNumber(1)
@$pb.TagNumber(2) @$pb.TagNumber(2)
@$pb.TagNumber(3)
void clearPayload() => $_clearField($_whichOneof(0)); void clearPayload() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1) @$pb.TagNumber(1)
@@ -332,89 +307,55 @@ class ClientRequest extends $pb.GeneratedMessage {
void clearAuthChallengeSolution() => $_clearField(2); void clearAuthChallengeSolution() => $_clearField(2);
@$pb.TagNumber(2) @$pb.TagNumber(2)
AuthChallengeSolution ensureAuthChallengeSolution() => $_ensure(1); AuthChallengeSolution ensureAuthChallengeSolution() => $_ensure(1);
}
class ClientConnectError extends $pb.GeneratedMessage { @$pb.TagNumber(3)
factory ClientConnectError({ $0.Empty get queryVaultState => $_getN(2);
ClientConnectError_Code? code, @$pb.TagNumber(3)
}) { set queryVaultState($0.Empty value) => $_setField(3, value);
final result = create(); @$pb.TagNumber(3)
if (code != null) result.code = code; $core.bool hasQueryVaultState() => $_has(2);
return result; @$pb.TagNumber(3)
} void clearQueryVaultState() => $_clearField(3);
@$pb.TagNumber(3)
$0.Empty ensureQueryVaultState() => $_ensure(2);
ClientConnectError._(); @$pb.TagNumber(4)
$core.int get requestId => $_getIZ(3);
factory ClientConnectError.fromBuffer($core.List<$core.int> data, @$pb.TagNumber(4)
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => set requestId($core.int value) => $_setSignedInt32(3, value);
create()..mergeFromBuffer(data, registry); @$pb.TagNumber(4)
factory ClientConnectError.fromJson($core.String json, $core.bool hasRequestId() => $_has(3);
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => @$pb.TagNumber(4)
create()..mergeFromJson(json, registry); void clearRequestId() => $_clearField(4);
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 { enum ClientResponse_Payload {
authChallenge, authChallenge,
authOk, authResult,
evmSignTransaction, evmSignTransaction,
evmAnalyzeTransaction, evmAnalyzeTransaction,
clientConnectError, vaultState,
notSet notSet
} }
class ClientResponse extends $pb.GeneratedMessage { class ClientResponse extends $pb.GeneratedMessage {
factory ClientResponse({ factory ClientResponse({
AuthChallenge? authChallenge, AuthChallenge? authChallenge,
AuthOk? authOk, AuthResult? authResult,
$0.EvmSignTransactionResponse? evmSignTransaction, $1.EvmSignTransactionResponse? evmSignTransaction,
$0.EvmAnalyzeTransactionResponse? evmAnalyzeTransaction, $1.EvmAnalyzeTransactionResponse? evmAnalyzeTransaction,
ClientConnectError? clientConnectError, VaultState? vaultState,
$core.int? requestId,
}) { }) {
final result = create(); final result = create();
if (authChallenge != null) result.authChallenge = authChallenge; if (authChallenge != null) result.authChallenge = authChallenge;
if (authOk != null) result.authOk = authOk; if (authResult != null) result.authResult = authResult;
if (evmSignTransaction != null) if (evmSignTransaction != null)
result.evmSignTransaction = evmSignTransaction; result.evmSignTransaction = evmSignTransaction;
if (evmAnalyzeTransaction != null) if (evmAnalyzeTransaction != null)
result.evmAnalyzeTransaction = evmAnalyzeTransaction; result.evmAnalyzeTransaction = evmAnalyzeTransaction;
if (clientConnectError != null) if (vaultState != null) result.vaultState = vaultState;
result.clientConnectError = clientConnectError; if (requestId != null) result.requestId = requestId;
return result; return result;
} }
@@ -430,28 +371,30 @@ class ClientResponse extends $pb.GeneratedMessage {
static const $core.Map<$core.int, ClientResponse_Payload> static const $core.Map<$core.int, ClientResponse_Payload>
_ClientResponse_PayloadByTag = { _ClientResponse_PayloadByTag = {
1: ClientResponse_Payload.authChallenge, 1: ClientResponse_Payload.authChallenge,
2: ClientResponse_Payload.authOk, 2: ClientResponse_Payload.authResult,
3: ClientResponse_Payload.evmSignTransaction, 3: ClientResponse_Payload.evmSignTransaction,
4: ClientResponse_Payload.evmAnalyzeTransaction, 4: ClientResponse_Payload.evmAnalyzeTransaction,
5: ClientResponse_Payload.clientConnectError, 6: ClientResponse_Payload.vaultState,
0: ClientResponse_Payload.notSet 0: ClientResponse_Payload.notSet
}; };
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ClientResponse', _omitMessageNames ? '' : 'ClientResponse',
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'), package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
createEmptyInstance: create) createEmptyInstance: create)
..oo(0, [1, 2, 3, 4, 5]) ..oo(0, [1, 2, 3, 4, 6])
..aOM<AuthChallenge>(1, _omitFieldNames ? '' : 'authChallenge', ..aOM<AuthChallenge>(1, _omitFieldNames ? '' : 'authChallenge',
subBuilder: AuthChallenge.create) subBuilder: AuthChallenge.create)
..aOM<AuthOk>(2, _omitFieldNames ? '' : 'authOk', subBuilder: AuthOk.create) ..aE<AuthResult>(2, _omitFieldNames ? '' : 'authResult',
..aOM<$0.EvmSignTransactionResponse>( enumValues: AuthResult.values)
..aOM<$1.EvmSignTransactionResponse>(
3, _omitFieldNames ? '' : 'evmSignTransaction', 3, _omitFieldNames ? '' : 'evmSignTransaction',
subBuilder: $0.EvmSignTransactionResponse.create) subBuilder: $1.EvmSignTransactionResponse.create)
..aOM<$0.EvmAnalyzeTransactionResponse>( ..aOM<$1.EvmAnalyzeTransactionResponse>(
4, _omitFieldNames ? '' : 'evmAnalyzeTransaction', 4, _omitFieldNames ? '' : 'evmAnalyzeTransaction',
subBuilder: $0.EvmAnalyzeTransactionResponse.create) subBuilder: $1.EvmAnalyzeTransactionResponse.create)
..aOM<ClientConnectError>(5, _omitFieldNames ? '' : 'clientConnectError', ..aE<VaultState>(6, _omitFieldNames ? '' : 'vaultState',
subBuilder: ClientConnectError.create) enumValues: VaultState.values)
..aI(7, _omitFieldNames ? '' : 'requestId')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@@ -477,14 +420,14 @@ class ClientResponse extends $pb.GeneratedMessage {
@$pb.TagNumber(2) @$pb.TagNumber(2)
@$pb.TagNumber(3) @$pb.TagNumber(3)
@$pb.TagNumber(4) @$pb.TagNumber(4)
@$pb.TagNumber(5) @$pb.TagNumber(6)
ClientResponse_Payload whichPayload() => ClientResponse_Payload whichPayload() =>
_ClientResponse_PayloadByTag[$_whichOneof(0)]!; _ClientResponse_PayloadByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1) @$pb.TagNumber(1)
@$pb.TagNumber(2) @$pb.TagNumber(2)
@$pb.TagNumber(3) @$pb.TagNumber(3)
@$pb.TagNumber(4) @$pb.TagNumber(4)
@$pb.TagNumber(5) @$pb.TagNumber(6)
void clearPayload() => $_clearField($_whichOneof(0)); void clearPayload() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1) @$pb.TagNumber(1)
@@ -499,50 +442,55 @@ class ClientResponse extends $pb.GeneratedMessage {
AuthChallenge ensureAuthChallenge() => $_ensure(0); AuthChallenge ensureAuthChallenge() => $_ensure(0);
@$pb.TagNumber(2) @$pb.TagNumber(2)
AuthOk get authOk => $_getN(1); AuthResult get authResult => $_getN(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set authOk(AuthOk value) => $_setField(2, value); set authResult(AuthResult value) => $_setField(2, value);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasAuthOk() => $_has(1); $core.bool hasAuthResult() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearAuthOk() => $_clearField(2); void clearAuthResult() => $_clearField(2);
@$pb.TagNumber(2)
AuthOk ensureAuthOk() => $_ensure(1);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$0.EvmSignTransactionResponse get evmSignTransaction => $_getN(2); $1.EvmSignTransactionResponse get evmSignTransaction => $_getN(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
set evmSignTransaction($0.EvmSignTransactionResponse value) => set evmSignTransaction($1.EvmSignTransactionResponse value) =>
$_setField(3, value); $_setField(3, value);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.bool hasEvmSignTransaction() => $_has(2); $core.bool hasEvmSignTransaction() => $_has(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
void clearEvmSignTransaction() => $_clearField(3); void clearEvmSignTransaction() => $_clearField(3);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$0.EvmSignTransactionResponse ensureEvmSignTransaction() => $_ensure(2); $1.EvmSignTransactionResponse ensureEvmSignTransaction() => $_ensure(2);
@$pb.TagNumber(4) @$pb.TagNumber(4)
$0.EvmAnalyzeTransactionResponse get evmAnalyzeTransaction => $_getN(3); $1.EvmAnalyzeTransactionResponse get evmAnalyzeTransaction => $_getN(3);
@$pb.TagNumber(4) @$pb.TagNumber(4)
set evmAnalyzeTransaction($0.EvmAnalyzeTransactionResponse value) => set evmAnalyzeTransaction($1.EvmAnalyzeTransactionResponse value) =>
$_setField(4, value); $_setField(4, value);
@$pb.TagNumber(4) @$pb.TagNumber(4)
$core.bool hasEvmAnalyzeTransaction() => $_has(3); $core.bool hasEvmAnalyzeTransaction() => $_has(3);
@$pb.TagNumber(4) @$pb.TagNumber(4)
void clearEvmAnalyzeTransaction() => $_clearField(4); void clearEvmAnalyzeTransaction() => $_clearField(4);
@$pb.TagNumber(4) @$pb.TagNumber(4)
$0.EvmAnalyzeTransactionResponse ensureEvmAnalyzeTransaction() => $_ensure(3); $1.EvmAnalyzeTransactionResponse ensureEvmAnalyzeTransaction() => $_ensure(3);
@$pb.TagNumber(5) @$pb.TagNumber(6)
ClientConnectError get clientConnectError => $_getN(4); VaultState get vaultState => $_getN(4);
@$pb.TagNumber(5) @$pb.TagNumber(6)
set clientConnectError(ClientConnectError value) => $_setField(5, value); set vaultState(VaultState value) => $_setField(6, value);
@$pb.TagNumber(5) @$pb.TagNumber(6)
$core.bool hasClientConnectError() => $_has(4); $core.bool hasVaultState() => $_has(4);
@$pb.TagNumber(5) @$pb.TagNumber(6)
void clearClientConnectError() => $_clearField(5); void clearVaultState() => $_clearField(6);
@$pb.TagNumber(5)
ClientConnectError ensureClientConnectError() => $_ensure(4); @$pb.TagNumber(7)
$core.int get requestId => $_getIZ(5);
@$pb.TagNumber(7)
set requestId($core.int value) => $_setSignedInt32(5, value);
@$pb.TagNumber(7)
$core.bool hasRequestId() => $_has(5);
@$pb.TagNumber(7)
void clearRequestId() => $_clearField(7);
} }
const $core.bool _omitFieldNames = const $core.bool _omitFieldNames =

View File

@@ -14,28 +14,66 @@ import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb; import 'package:protobuf/protobuf.dart' as $pb;
class ClientConnectError_Code extends $pb.ProtobufEnum { class AuthResult extends $pb.ProtobufEnum {
static const ClientConnectError_Code UNKNOWN = static const AuthResult AUTH_RESULT_UNSPECIFIED =
ClientConnectError_Code._(0, _omitEnumNames ? '' : 'UNKNOWN'); AuthResult._(0, _omitEnumNames ? '' : 'AUTH_RESULT_UNSPECIFIED');
static const ClientConnectError_Code APPROVAL_DENIED = static const AuthResult AUTH_RESULT_SUCCESS =
ClientConnectError_Code._(1, _omitEnumNames ? '' : 'APPROVAL_DENIED'); AuthResult._(1, _omitEnumNames ? '' : 'AUTH_RESULT_SUCCESS');
static const ClientConnectError_Code NO_USER_AGENTS_ONLINE = static const AuthResult AUTH_RESULT_INVALID_KEY =
ClientConnectError_Code._( AuthResult._(2, _omitEnumNames ? '' : 'AUTH_RESULT_INVALID_KEY');
2, _omitEnumNames ? '' : 'NO_USER_AGENTS_ONLINE'); static const AuthResult AUTH_RESULT_INVALID_SIGNATURE =
AuthResult._(3, _omitEnumNames ? '' : 'AUTH_RESULT_INVALID_SIGNATURE');
static const AuthResult AUTH_RESULT_APPROVAL_DENIED =
AuthResult._(4, _omitEnumNames ? '' : 'AUTH_RESULT_APPROVAL_DENIED');
static const AuthResult AUTH_RESULT_NO_USER_AGENTS_ONLINE = AuthResult._(
5, _omitEnumNames ? '' : 'AUTH_RESULT_NO_USER_AGENTS_ONLINE');
static const AuthResult AUTH_RESULT_INTERNAL =
AuthResult._(6, _omitEnumNames ? '' : 'AUTH_RESULT_INTERNAL');
static const $core.List<ClientConnectError_Code> values = static const $core.List<AuthResult> values = <AuthResult>[
<ClientConnectError_Code>[ AUTH_RESULT_UNSPECIFIED,
UNKNOWN, AUTH_RESULT_SUCCESS,
APPROVAL_DENIED, AUTH_RESULT_INVALID_KEY,
NO_USER_AGENTS_ONLINE, AUTH_RESULT_INVALID_SIGNATURE,
AUTH_RESULT_APPROVAL_DENIED,
AUTH_RESULT_NO_USER_AGENTS_ONLINE,
AUTH_RESULT_INTERNAL,
]; ];
static final $core.List<ClientConnectError_Code?> _byValue = static final $core.List<AuthResult?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 2); $pb.ProtobufEnum.$_initByValueList(values, 6);
static ClientConnectError_Code? valueOf($core.int value) => static AuthResult? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value]; value < 0 || value >= _byValue.length ? null : _byValue[value];
const ClientConnectError_Code._(super.value, super.name); const AuthResult._(super.value, super.name);
}
class VaultState extends $pb.ProtobufEnum {
static const VaultState VAULT_STATE_UNSPECIFIED =
VaultState._(0, _omitEnumNames ? '' : 'VAULT_STATE_UNSPECIFIED');
static const VaultState VAULT_STATE_UNBOOTSTRAPPED =
VaultState._(1, _omitEnumNames ? '' : 'VAULT_STATE_UNBOOTSTRAPPED');
static const VaultState VAULT_STATE_SEALED =
VaultState._(2, _omitEnumNames ? '' : 'VAULT_STATE_SEALED');
static const VaultState VAULT_STATE_UNSEALED =
VaultState._(3, _omitEnumNames ? '' : 'VAULT_STATE_UNSEALED');
static const VaultState VAULT_STATE_ERROR =
VaultState._(4, _omitEnumNames ? '' : 'VAULT_STATE_ERROR');
static const $core.List<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 = const $core.bool _omitEnumNames =

View File

@@ -15,6 +15,46 @@ import 'dart:convert' as $convert;
import 'dart:core' as $core; import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data; import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use authResultDescriptor instead')
const AuthResult$json = {
'1': 'AuthResult',
'2': [
{'1': 'AUTH_RESULT_UNSPECIFIED', '2': 0},
{'1': 'AUTH_RESULT_SUCCESS', '2': 1},
{'1': 'AUTH_RESULT_INVALID_KEY', '2': 2},
{'1': 'AUTH_RESULT_INVALID_SIGNATURE', '2': 3},
{'1': 'AUTH_RESULT_APPROVAL_DENIED', '2': 4},
{'1': 'AUTH_RESULT_NO_USER_AGENTS_ONLINE', '2': 5},
{'1': 'AUTH_RESULT_INTERNAL', '2': 6},
],
};
/// Descriptor for `AuthResult`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List authResultDescriptor = $convert.base64Decode(
'CgpBdXRoUmVzdWx0EhsKF0FVVEhfUkVTVUxUX1VOU1BFQ0lGSUVEEAASFwoTQVVUSF9SRVNVTF'
'RfU1VDQ0VTUxABEhsKF0FVVEhfUkVTVUxUX0lOVkFMSURfS0VZEAISIQodQVVUSF9SRVNVTFRf'
'SU5WQUxJRF9TSUdOQVRVUkUQAxIfChtBVVRIX1JFU1VMVF9BUFBST1ZBTF9ERU5JRUQQBBIlCi'
'FBVVRIX1JFU1VMVF9OT19VU0VSX0FHRU5UU19PTkxJTkUQBRIYChRBVVRIX1JFU1VMVF9JTlRF'
'Uk5BTBAG');
@$core.Deprecated('Use vaultStateDescriptor instead')
const VaultState$json = {
'1': 'VaultState',
'2': [
{'1': 'VAULT_STATE_UNSPECIFIED', '2': 0},
{'1': 'VAULT_STATE_UNBOOTSTRAPPED', '2': 1},
{'1': 'VAULT_STATE_SEALED', '2': 2},
{'1': 'VAULT_STATE_UNSEALED', '2': 3},
{'1': 'VAULT_STATE_ERROR', '2': 4},
],
};
/// Descriptor for `VaultState`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List vaultStateDescriptor = $convert.base64Decode(
'CgpWYXVsdFN0YXRlEhsKF1ZBVUxUX1NUQVRFX1VOU1BFQ0lGSUVEEAASHgoaVkFVTFRfU1RBVE'
'VfVU5CT09UU1RSQVBQRUQQARIWChJWQVVMVF9TVEFURV9TRUFMRUQQAhIYChRWQVVMVF9TVEFU'
'RV9VTlNFQUxFRBADEhUKEVZBVUxUX1NUQVRFX0VSUk9SEAQ=');
@$core.Deprecated('Use authChallengeRequestDescriptor instead') @$core.Deprecated('Use authChallengeRequestDescriptor instead')
const AuthChallengeRequest$json = { const AuthChallengeRequest$json = {
'1': 'AuthChallengeRequest', '1': 'AuthChallengeRequest',
@@ -54,19 +94,11 @@ const AuthChallengeSolution$json = {
final $typed_data.Uint8List authChallengeSolutionDescriptor = $convert.base64Decode( final $typed_data.Uint8List authChallengeSolutionDescriptor = $convert.base64Decode(
'ChVBdXRoQ2hhbGxlbmdlU29sdXRpb24SHAoJc2lnbmF0dXJlGAEgASgMUglzaWduYXR1cmU='); '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') @$core.Deprecated('Use clientRequestDescriptor instead')
const ClientRequest$json = { const ClientRequest$json = {
'1': 'ClientRequest', '1': 'ClientRequest',
'2': [ '2': [
{'1': 'request_id', '3': 4, '4': 1, '5': 5, '10': 'requestId'},
{ {
'1': 'auth_challenge_request', '1': 'auth_challenge_request',
'3': 1, '3': 1,
@@ -85,6 +117,15 @@ const ClientRequest$json = {
'9': 0, '9': 0,
'10': 'authChallengeSolution' '10': 'authChallengeSolution'
}, },
{
'1': 'query_vault_state',
'3': 3,
'4': 1,
'5': 11,
'6': '.google.protobuf.Empty',
'9': 0,
'10': 'queryVaultState'
},
], ],
'8': [ '8': [
{'1': 'payload'}, {'1': 'payload'},
@@ -93,47 +134,26 @@ const ClientRequest$json = {
/// Descriptor for `ClientRequest`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ClientRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List clientRequestDescriptor = $convert.base64Decode( final $typed_data.Uint8List clientRequestDescriptor = $convert.base64Decode(
'Cg1DbGllbnRSZXF1ZXN0ElwKFmF1dGhfY2hhbGxlbmdlX3JlcXVlc3QYASABKAsyJC5hcmJpdG' 'Cg1DbGllbnRSZXF1ZXN0Eh0KCnJlcXVlc3RfaWQYBCABKAVSCXJlcXVlc3RJZBJcChZhdXRoX2'
'VyLmNsaWVudC5BdXRoQ2hhbGxlbmdlUmVxdWVzdEgAUhRhdXRoQ2hhbGxlbmdlUmVxdWVzdBJf' 'NoYWxsZW5nZV9yZXF1ZXN0GAEgASgLMiQuYXJiaXRlci5jbGllbnQuQXV0aENoYWxsZW5nZVJl'
'ChdhdXRoX2NoYWxsZW5nZV9zb2x1dGlvbhgCIAEoCzIlLmFyYml0ZXIuY2xpZW50LkF1dGhDaG' 'cXVlc3RIAFIUYXV0aENoYWxsZW5nZVJlcXVlc3QSXwoXYXV0aF9jaGFsbGVuZ2Vfc29sdXRpb2'
'FsbGVuZ2VTb2x1dGlvbkgAUhVhdXRoQ2hhbGxlbmdlU29sdXRpb25CCQoHcGF5bG9hZA=='); '4YAiABKAsyJS5hcmJpdGVyLmNsaWVudC5BdXRoQ2hhbGxlbmdlU29sdXRpb25IAFIVYXV0aENo'
'YWxsZW5nZVNvbHV0aW9uEkQKEXF1ZXJ5X3ZhdWx0X3N0YXRlGAMgASgLMhYuZ29vZ2xlLnByb3'
@$core.Deprecated('Use clientConnectErrorDescriptor instead') 'RvYnVmLkVtcHR5SABSD3F1ZXJ5VmF1bHRTdGF0ZUIJCgdwYXlsb2Fk');
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') @$core.Deprecated('Use clientResponseDescriptor instead')
const ClientResponse$json = { const ClientResponse$json = {
'1': 'ClientResponse', '1': 'ClientResponse',
'2': [ '2': [
{
'1': 'request_id',
'3': 7,
'4': 1,
'5': 5,
'9': 1,
'10': 'requestId',
'17': true
},
{ {
'1': 'auth_challenge', '1': 'auth_challenge',
'3': 1, '3': 1,
@@ -144,22 +164,13 @@ const ClientResponse$json = {
'10': 'authChallenge' '10': 'authChallenge'
}, },
{ {
'1': 'auth_ok', '1': 'auth_result',
'3': 2, '3': 2,
'4': 1, '4': 1,
'5': 11, '5': 14,
'6': '.arbiter.client.AuthOk', '6': '.arbiter.client.AuthResult',
'9': 0, '9': 0,
'10': 'authOk' '10': 'authResult'
},
{
'1': 'client_connect_error',
'3': 5,
'4': 1,
'5': 11,
'6': '.arbiter.client.ClientConnectError',
'9': 0,
'10': 'clientConnectError'
}, },
{ {
'1': 'evm_sign_transaction', '1': 'evm_sign_transaction',
@@ -179,19 +190,30 @@ const ClientResponse$json = {
'9': 0, '9': 0,
'10': 'evmAnalyzeTransaction' '10': 'evmAnalyzeTransaction'
}, },
{
'1': 'vault_state',
'3': 6,
'4': 1,
'5': 14,
'6': '.arbiter.client.VaultState',
'9': 0,
'10': 'vaultState'
},
], ],
'8': [ '8': [
{'1': 'payload'}, {'1': 'payload'},
{'1': '_request_id'},
], ],
}; };
/// Descriptor for `ClientResponse`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ClientResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List clientResponseDescriptor = $convert.base64Decode( final $typed_data.Uint8List clientResponseDescriptor = $convert.base64Decode(
'Cg5DbGllbnRSZXNwb25zZRJGCg5hdXRoX2NoYWxsZW5nZRgBIAEoCzIdLmFyYml0ZXIuY2xpZW' 'Cg5DbGllbnRSZXNwb25zZRIiCgpyZXF1ZXN0X2lkGAcgASgFSAFSCXJlcXVlc3RJZIgBARJGCg'
'50LkF1dGhDaGFsbGVuZ2VIAFINYXV0aENoYWxsZW5nZRIxCgdhdXRoX29rGAIgASgLMhYuYXJi' '5hdXRoX2NoYWxsZW5nZRgBIAEoCzIdLmFyYml0ZXIuY2xpZW50LkF1dGhDaGFsbGVuZ2VIAFIN'
'aXRlci5jbGllbnQuQXV0aE9rSABSBmF1dGhPaxJWChRjbGllbnRfY29ubmVjdF9lcnJvchgFIA' 'YXV0aENoYWxsZW5nZRI9CgthdXRoX3Jlc3VsdBgCIAEoDjIaLmFyYml0ZXIuY2xpZW50LkF1dG'
'EoCzIiLmFyYml0ZXIuY2xpZW50LkNsaWVudENvbm5lY3RFcnJvckgAUhJjbGllbnRDb25uZWN0' 'hSZXN1bHRIAFIKYXV0aFJlc3VsdBJbChRldm1fc2lnbl90cmFuc2FjdGlvbhgDIAEoCzInLmFy'
'RXJyb3ISWwoUZXZtX3NpZ25fdHJhbnNhY3Rpb24YAyABKAsyJy5hcmJpdGVyLmV2bS5Fdm1TaW' 'Yml0ZXIuZXZtLkV2bVNpZ25UcmFuc2FjdGlvblJlc3BvbnNlSABSEmV2bVNpZ25UcmFuc2FjdG'
'duVHJhbnNhY3Rpb25SZXNwb25zZUgAUhJldm1TaWduVHJhbnNhY3Rpb24SZAoXZXZtX2FuYWx5' 'lvbhJkChdldm1fYW5hbHl6ZV90cmFuc2FjdGlvbhgEIAEoCzIqLmFyYml0ZXIuZXZtLkV2bUFu'
'emVfdHJhbnNhY3Rpb24YBCABKAsyKi5hcmJpdGVyLmV2bS5Fdm1BbmFseXplVHJhbnNhY3Rpb2' 'YWx5emVUcmFuc2FjdGlvblJlc3BvbnNlSABSFWV2bUFuYWx5emVUcmFuc2FjdGlvbhI9Cgt2YX'
'5SZXNwb25zZUgAUhVldm1BbmFseXplVHJhbnNhY3Rpb25CCQoHcGF5bG9hZA=='); 'VsdF9zdGF0ZRgGIAEoDjIaLmFyYml0ZXIuY2xpZW50LlZhdWx0U3RhdGVIAFIKdmF1bHRTdGF0'
'ZUIJCgdwYXlsb2FkQg0KC19yZXF1ZXN0X2lk');

View File

@@ -105,11 +105,9 @@ class AuthChallengeRequest extends $pb.GeneratedMessage {
class AuthChallenge extends $pb.GeneratedMessage { class AuthChallenge extends $pb.GeneratedMessage {
factory AuthChallenge({ factory AuthChallenge({
$core.List<$core.int>? pubkey,
$core.int? nonce, $core.int? nonce,
}) { }) {
final result = create(); final result = create();
if (pubkey != null) result.pubkey = pubkey;
if (nonce != null) result.nonce = nonce; if (nonce != null) result.nonce = nonce;
return result; return result;
} }
@@ -128,8 +126,6 @@ class AuthChallenge extends $pb.GeneratedMessage {
package: package:
const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'), const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'),
createEmptyInstance: create) createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY)
..aI(2, _omitFieldNames ? '' : 'nonce') ..aI(2, _omitFieldNames ? '' : 'nonce')
..hasRequiredFields = false; ..hasRequiredFields = false;
@@ -152,21 +148,12 @@ class AuthChallenge extends $pb.GeneratedMessage {
$pb.GeneratedMessage.$_defaultFor<AuthChallenge>(create); $pb.GeneratedMessage.$_defaultFor<AuthChallenge>(create);
static AuthChallenge? _defaultInstance; 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) @$pb.TagNumber(2)
$core.int get nonce => $_getIZ(1); $core.int get nonce => $_getIZ(0);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set nonce($core.int value) => $_setSignedInt32(1, value); set nonce($core.int value) => $_setSignedInt32(0, value);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasNonce() => $_has(1); $core.bool hasNonce() => $_has(0);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearNonce() => $_clearField(2); void clearNonce() => $_clearField(2);
} }
@@ -228,44 +215,6 @@ class AuthChallengeSolution extends $pb.GeneratedMessage {
void clearSignature() => $_clearField(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.user_agent'),
createEmptyInstance: create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthOk clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthOk copyWith(void Function(AuthOk) updates) =>
super.copyWith((message) => updates(message as AuthOk)) as AuthOk;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static AuthOk create() => AuthOk._();
@$core.override
AuthOk createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static AuthOk getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<AuthOk>(create);
static AuthOk? _defaultInstance;
}
class UnsealStart extends $pb.GeneratedMessage { class UnsealStart extends $pb.GeneratedMessage {
factory UnsealStart({ factory UnsealStart({
$core.List<$core.int>? clientPubkey, $core.List<$core.int>? clientPubkey,
@@ -726,6 +675,7 @@ class UserAgentRequest extends $pb.GeneratedMessage {
$1.EvmGrantListRequest? evmGrantList, $1.EvmGrantListRequest? evmGrantList,
ClientConnectionResponse? clientConnectionResponse, ClientConnectionResponse? clientConnectionResponse,
BootstrapEncryptedKey? bootstrapEncryptedKey, BootstrapEncryptedKey? bootstrapEncryptedKey,
$core.int? id,
}) { }) {
final result = create(); final result = create();
if (authChallengeRequest != null) if (authChallengeRequest != null)
@@ -745,6 +695,7 @@ class UserAgentRequest extends $pb.GeneratedMessage {
result.clientConnectionResponse = clientConnectionResponse; result.clientConnectionResponse = clientConnectionResponse;
if (bootstrapEncryptedKey != null) if (bootstrapEncryptedKey != null)
result.bootstrapEncryptedKey = bootstrapEncryptedKey; result.bootstrapEncryptedKey = bootstrapEncryptedKey;
if (id != null) result.id = id;
return result; return result;
} }
@@ -807,6 +758,7 @@ class UserAgentRequest extends $pb.GeneratedMessage {
..aOM<BootstrapEncryptedKey>( ..aOM<BootstrapEncryptedKey>(
12, _omitFieldNames ? '' : 'bootstrapEncryptedKey', 12, _omitFieldNames ? '' : 'bootstrapEncryptedKey',
subBuilder: BootstrapEncryptedKey.create) subBuilder: BootstrapEncryptedKey.create)
..aI(14, _omitFieldNames ? '' : 'id')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@@ -990,11 +942,20 @@ class UserAgentRequest extends $pb.GeneratedMessage {
void clearBootstrapEncryptedKey() => $_clearField(12); void clearBootstrapEncryptedKey() => $_clearField(12);
@$pb.TagNumber(12) @$pb.TagNumber(12)
BootstrapEncryptedKey ensureBootstrapEncryptedKey() => $_ensure(11); BootstrapEncryptedKey ensureBootstrapEncryptedKey() => $_ensure(11);
@$pb.TagNumber(14)
$core.int get id => $_getIZ(12);
@$pb.TagNumber(14)
set id($core.int value) => $_setSignedInt32(12, value);
@$pb.TagNumber(14)
$core.bool hasId() => $_has(12);
@$pb.TagNumber(14)
void clearId() => $_clearField(14);
} }
enum UserAgentResponse_Payload { enum UserAgentResponse_Payload {
authChallenge, authChallenge,
authOk, authResult,
unsealStartResponse, unsealStartResponse,
unsealResult, unsealResult,
vaultState, vaultState,
@@ -1012,7 +973,7 @@ enum UserAgentResponse_Payload {
class UserAgentResponse extends $pb.GeneratedMessage { class UserAgentResponse extends $pb.GeneratedMessage {
factory UserAgentResponse({ factory UserAgentResponse({
AuthChallenge? authChallenge, AuthChallenge? authChallenge,
AuthOk? authOk, AuthResult? authResult,
UnsealStartResponse? unsealStartResponse, UnsealStartResponse? unsealStartResponse,
UnsealResult? unsealResult, UnsealResult? unsealResult,
VaultState? vaultState, VaultState? vaultState,
@@ -1024,10 +985,11 @@ class UserAgentResponse extends $pb.GeneratedMessage {
ClientConnectionRequest? clientConnectionRequest, ClientConnectionRequest? clientConnectionRequest,
ClientConnectionCancel? clientConnectionCancel, ClientConnectionCancel? clientConnectionCancel,
BootstrapResult? bootstrapResult, BootstrapResult? bootstrapResult,
$core.int? id,
}) { }) {
final result = create(); final result = create();
if (authChallenge != null) result.authChallenge = authChallenge; if (authChallenge != null) result.authChallenge = authChallenge;
if (authOk != null) result.authOk = authOk; if (authResult != null) result.authResult = authResult;
if (unsealStartResponse != null) if (unsealStartResponse != null)
result.unsealStartResponse = unsealStartResponse; result.unsealStartResponse = unsealStartResponse;
if (unsealResult != null) result.unsealResult = unsealResult; if (unsealResult != null) result.unsealResult = unsealResult;
@@ -1042,6 +1004,7 @@ class UserAgentResponse extends $pb.GeneratedMessage {
if (clientConnectionCancel != null) if (clientConnectionCancel != null)
result.clientConnectionCancel = clientConnectionCancel; result.clientConnectionCancel = clientConnectionCancel;
if (bootstrapResult != null) result.bootstrapResult = bootstrapResult; if (bootstrapResult != null) result.bootstrapResult = bootstrapResult;
if (id != null) result.id = id;
return result; return result;
} }
@@ -1057,7 +1020,7 @@ class UserAgentResponse extends $pb.GeneratedMessage {
static const $core.Map<$core.int, UserAgentResponse_Payload> static const $core.Map<$core.int, UserAgentResponse_Payload>
_UserAgentResponse_PayloadByTag = { _UserAgentResponse_PayloadByTag = {
1: UserAgentResponse_Payload.authChallenge, 1: UserAgentResponse_Payload.authChallenge,
2: UserAgentResponse_Payload.authOk, 2: UserAgentResponse_Payload.authResult,
3: UserAgentResponse_Payload.unsealStartResponse, 3: UserAgentResponse_Payload.unsealStartResponse,
4: UserAgentResponse_Payload.unsealResult, 4: UserAgentResponse_Payload.unsealResult,
5: UserAgentResponse_Payload.vaultState, 5: UserAgentResponse_Payload.vaultState,
@@ -1079,7 +1042,8 @@ class UserAgentResponse extends $pb.GeneratedMessage {
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13])
..aOM<AuthChallenge>(1, _omitFieldNames ? '' : 'authChallenge', ..aOM<AuthChallenge>(1, _omitFieldNames ? '' : 'authChallenge',
subBuilder: AuthChallenge.create) subBuilder: AuthChallenge.create)
..aOM<AuthOk>(2, _omitFieldNames ? '' : 'authOk', subBuilder: AuthOk.create) ..aE<AuthResult>(2, _omitFieldNames ? '' : 'authResult',
enumValues: AuthResult.values)
..aOM<UnsealStartResponse>(3, _omitFieldNames ? '' : 'unsealStartResponse', ..aOM<UnsealStartResponse>(3, _omitFieldNames ? '' : 'unsealStartResponse',
subBuilder: UnsealStartResponse.create) subBuilder: UnsealStartResponse.create)
..aE<UnsealResult>(4, _omitFieldNames ? '' : 'unsealResult', ..aE<UnsealResult>(4, _omitFieldNames ? '' : 'unsealResult',
@@ -1104,6 +1068,7 @@ class UserAgentResponse extends $pb.GeneratedMessage {
subBuilder: ClientConnectionCancel.create) subBuilder: ClientConnectionCancel.create)
..aE<BootstrapResult>(13, _omitFieldNames ? '' : 'bootstrapResult', ..aE<BootstrapResult>(13, _omitFieldNames ? '' : 'bootstrapResult',
enumValues: BootstrapResult.values) enumValues: BootstrapResult.values)
..aI(14, _omitFieldNames ? '' : 'id')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@@ -1167,15 +1132,13 @@ class UserAgentResponse extends $pb.GeneratedMessage {
AuthChallenge ensureAuthChallenge() => $_ensure(0); AuthChallenge ensureAuthChallenge() => $_ensure(0);
@$pb.TagNumber(2) @$pb.TagNumber(2)
AuthOk get authOk => $_getN(1); AuthResult get authResult => $_getN(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set authOk(AuthOk value) => $_setField(2, value); set authResult(AuthResult value) => $_setField(2, value);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasAuthOk() => $_has(1); $core.bool hasAuthResult() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearAuthOk() => $_clearField(2); void clearAuthResult() => $_clearField(2);
@$pb.TagNumber(2)
AuthOk ensureAuthOk() => $_ensure(1);
@$pb.TagNumber(3) @$pb.TagNumber(3)
UnsealStartResponse get unsealStartResponse => $_getN(2); UnsealStartResponse get unsealStartResponse => $_getN(2);
@@ -1293,6 +1256,15 @@ class UserAgentResponse extends $pb.GeneratedMessage {
$core.bool hasBootstrapResult() => $_has(12); $core.bool hasBootstrapResult() => $_has(12);
@$pb.TagNumber(13) @$pb.TagNumber(13)
void clearBootstrapResult() => $_clearField(13); void clearBootstrapResult() => $_clearField(13);
@$pb.TagNumber(14)
$core.int get id => $_getIZ(13);
@$pb.TagNumber(14)
set id($core.int value) => $_setSignedInt32(13, value);
@$pb.TagNumber(14)
$core.bool hasId() => $_has(13);
@$pb.TagNumber(14)
void clearId() => $_clearField(14);
} }
const $core.bool _omitFieldNames = const $core.bool _omitFieldNames =

View File

@@ -39,6 +39,40 @@ class KeyType extends $pb.ProtobufEnum {
const KeyType._(super.value, super.name); const KeyType._(super.value, super.name);
} }
class AuthResult extends $pb.ProtobufEnum {
static const AuthResult AUTH_RESULT_UNSPECIFIED =
AuthResult._(0, _omitEnumNames ? '' : 'AUTH_RESULT_UNSPECIFIED');
static const AuthResult AUTH_RESULT_SUCCESS =
AuthResult._(1, _omitEnumNames ? '' : 'AUTH_RESULT_SUCCESS');
static const AuthResult AUTH_RESULT_INVALID_KEY =
AuthResult._(2, _omitEnumNames ? '' : 'AUTH_RESULT_INVALID_KEY');
static const AuthResult AUTH_RESULT_INVALID_SIGNATURE =
AuthResult._(3, _omitEnumNames ? '' : 'AUTH_RESULT_INVALID_SIGNATURE');
static const AuthResult AUTH_RESULT_BOOTSTRAP_REQUIRED =
AuthResult._(4, _omitEnumNames ? '' : 'AUTH_RESULT_BOOTSTRAP_REQUIRED');
static const AuthResult AUTH_RESULT_TOKEN_INVALID =
AuthResult._(5, _omitEnumNames ? '' : 'AUTH_RESULT_TOKEN_INVALID');
static const AuthResult AUTH_RESULT_INTERNAL =
AuthResult._(6, _omitEnumNames ? '' : 'AUTH_RESULT_INTERNAL');
static const $core.List<AuthResult> values = <AuthResult>[
AUTH_RESULT_UNSPECIFIED,
AUTH_RESULT_SUCCESS,
AUTH_RESULT_INVALID_KEY,
AUTH_RESULT_INVALID_SIGNATURE,
AUTH_RESULT_BOOTSTRAP_REQUIRED,
AUTH_RESULT_TOKEN_INVALID,
AUTH_RESULT_INTERNAL,
];
static final $core.List<AuthResult?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 6);
static AuthResult? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const AuthResult._(super.value, super.name);
}
class UnsealResult extends $pb.ProtobufEnum { class UnsealResult extends $pb.ProtobufEnum {
static const UnsealResult UNSEAL_RESULT_UNSPECIFIED = static const UnsealResult UNSEAL_RESULT_UNSPECIFIED =
UnsealResult._(0, _omitEnumNames ? '' : 'UNSEAL_RESULT_UNSPECIFIED'); UnsealResult._(0, _omitEnumNames ? '' : 'UNSEAL_RESULT_UNSPECIFIED');
@@ -65,14 +99,15 @@ class UnsealResult extends $pb.ProtobufEnum {
} }
class BootstrapResult extends $pb.ProtobufEnum { class BootstrapResult extends $pb.ProtobufEnum {
static const BootstrapResult BOOTSTRAP_RESULT_UNSPECIFIED = static const BootstrapResult BOOTSTRAP_RESULT_UNSPECIFIED = BootstrapResult._(
BootstrapResult._(0, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_UNSPECIFIED'); 0, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_UNSPECIFIED');
static const BootstrapResult BOOTSTRAP_RESULT_SUCCESS = static const BootstrapResult BOOTSTRAP_RESULT_SUCCESS =
BootstrapResult._(1, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_SUCCESS'); BootstrapResult._(1, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_SUCCESS');
static const BootstrapResult BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED = static const BootstrapResult BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED =
BootstrapResult._(2, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED'); BootstrapResult._(
static const BootstrapResult BOOTSTRAP_RESULT_INVALID_KEY = 2, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED');
BootstrapResult._(3, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_INVALID_KEY'); static const BootstrapResult BOOTSTRAP_RESULT_INVALID_KEY = BootstrapResult._(
3, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_INVALID_KEY');
static const $core.List<BootstrapResult> values = <BootstrapResult>[ static const $core.List<BootstrapResult> values = <BootstrapResult>[
BOOTSTRAP_RESULT_UNSPECIFIED, BOOTSTRAP_RESULT_UNSPECIFIED,

View File

@@ -31,6 +31,28 @@ final $typed_data.Uint8List keyTypeDescriptor = $convert.base64Decode(
'CgdLZXlUeXBlEhgKFEtFWV9UWVBFX1VOU1BFQ0lGSUVEEAASFAoQS0VZX1RZUEVfRUQyNTUxOR' 'CgdLZXlUeXBlEhgKFEtFWV9UWVBFX1VOU1BFQ0lGSUVEEAASFAoQS0VZX1RZUEVfRUQyNTUxOR'
'ABEhwKGEtFWV9UWVBFX0VDRFNBX1NFQ1AyNTZLMRACEhAKDEtFWV9UWVBFX1JTQRAD'); 'ABEhwKGEtFWV9UWVBFX0VDRFNBX1NFQ1AyNTZLMRACEhAKDEtFWV9UWVBFX1JTQRAD');
@$core.Deprecated('Use authResultDescriptor instead')
const AuthResult$json = {
'1': 'AuthResult',
'2': [
{'1': 'AUTH_RESULT_UNSPECIFIED', '2': 0},
{'1': 'AUTH_RESULT_SUCCESS', '2': 1},
{'1': 'AUTH_RESULT_INVALID_KEY', '2': 2},
{'1': 'AUTH_RESULT_INVALID_SIGNATURE', '2': 3},
{'1': 'AUTH_RESULT_BOOTSTRAP_REQUIRED', '2': 4},
{'1': 'AUTH_RESULT_TOKEN_INVALID', '2': 5},
{'1': 'AUTH_RESULT_INTERNAL', '2': 6},
],
};
/// Descriptor for `AuthResult`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List authResultDescriptor = $convert.base64Decode(
'CgpBdXRoUmVzdWx0EhsKF0FVVEhfUkVTVUxUX1VOU1BFQ0lGSUVEEAASFwoTQVVUSF9SRVNVTF'
'RfU1VDQ0VTUxABEhsKF0FVVEhfUkVTVUxUX0lOVkFMSURfS0VZEAISIQodQVVUSF9SRVNVTFRf'
'SU5WQUxJRF9TSUdOQVRVUkUQAxIiCh5BVVRIX1JFU1VMVF9CT09UU1RSQVBfUkVRVUlSRUQQBB'
'IdChlBVVRIX1JFU1VMVF9UT0tFTl9JTlZBTElEEAUSGAoUQVVUSF9SRVNVTFRfSU5URVJOQUwQ'
'Bg==');
@$core.Deprecated('Use unsealResultDescriptor instead') @$core.Deprecated('Use unsealResultDescriptor instead')
const UnsealResult$json = { const UnsealResult$json = {
'1': 'UnsealResult', '1': 'UnsealResult',
@@ -48,6 +70,23 @@ final $typed_data.Uint8List unsealResultDescriptor = $convert.base64Decode(
'9SRVNVTFRfU1VDQ0VTUxABEh0KGVVOU0VBTF9SRVNVTFRfSU5WQUxJRF9LRVkQAhIgChxVTlNF' '9SRVNVTFRfU1VDQ0VTUxABEh0KGVVOU0VBTF9SRVNVTFRfSU5WQUxJRF9LRVkQAhIgChxVTlNF'
'QUxfUkVTVUxUX1VOQk9PVFNUUkFQUEVEEAM='); 'QUxfUkVTVUxUX1VOQk9PVFNUUkFQUEVEEAM=');
@$core.Deprecated('Use bootstrapResultDescriptor instead')
const BootstrapResult$json = {
'1': 'BootstrapResult',
'2': [
{'1': 'BOOTSTRAP_RESULT_UNSPECIFIED', '2': 0},
{'1': 'BOOTSTRAP_RESULT_SUCCESS', '2': 1},
{'1': 'BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED', '2': 2},
{'1': 'BOOTSTRAP_RESULT_INVALID_KEY', '2': 3},
],
};
/// Descriptor for `BootstrapResult`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List bootstrapResultDescriptor = $convert.base64Decode(
'Cg9Cb290c3RyYXBSZXN1bHQSIAocQk9PVFNUUkFQX1JFU1VMVF9VTlNQRUNJRklFRBAAEhwKGE'
'JPT1RTVFJBUF9SRVNVTFRfU1VDQ0VTUxABEikKJUJPT1RTVFJBUF9SRVNVTFRfQUxSRUFEWV9C'
'T09UU1RSQVBQRUQQAhIgChxCT09UU1RSQVBfUkVTVUxUX0lOVkFMSURfS0VZEAM=');
@$core.Deprecated('Use vaultStateDescriptor instead') @$core.Deprecated('Use vaultStateDescriptor instead')
const VaultState$json = { const VaultState$json = {
'1': 'VaultState', '1': 'VaultState',
@@ -105,15 +144,16 @@ final $typed_data.Uint8List authChallengeRequestDescriptor = $convert.base64Deco
const AuthChallenge$json = { const AuthChallenge$json = {
'1': 'AuthChallenge', '1': 'AuthChallenge',
'2': [ '2': [
{'1': 'pubkey', '3': 1, '4': 1, '5': 12, '10': 'pubkey'},
{'1': 'nonce', '3': 2, '4': 1, '5': 5, '10': 'nonce'}, {'1': 'nonce', '3': 2, '4': 1, '5': 5, '10': 'nonce'},
], ],
'9': [
{'1': 1, '2': 2},
],
}; };
/// Descriptor for `AuthChallenge`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `AuthChallenge`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List authChallengeDescriptor = $convert.base64Decode( final $typed_data.Uint8List authChallengeDescriptor = $convert.base64Decode(
'Cg1BdXRoQ2hhbGxlbmdlEhYKBnB1YmtleRgBIAEoDFIGcHVia2V5EhQKBW5vbmNlGAIgASgFUg' 'Cg1BdXRoQ2hhbGxlbmdlEhQKBW5vbmNlGAIgASgFUgVub25jZUoECAEQAg==');
'Vub25jZQ==');
@$core.Deprecated('Use authChallengeSolutionDescriptor instead') @$core.Deprecated('Use authChallengeSolutionDescriptor instead')
const AuthChallengeSolution$json = { const AuthChallengeSolution$json = {
@@ -127,15 +167,6 @@ const AuthChallengeSolution$json = {
final $typed_data.Uint8List authChallengeSolutionDescriptor = $convert.base64Decode( final $typed_data.Uint8List authChallengeSolutionDescriptor = $convert.base64Decode(
'ChVBdXRoQ2hhbGxlbmdlU29sdXRpb24SHAoJc2lnbmF0dXJlGAEgASgMUglzaWduYXR1cmU='); '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') @$core.Deprecated('Use unsealStartDescriptor instead')
const UnsealStart$json = { const UnsealStart$json = {
'1': 'UnsealStart', '1': 'UnsealStart',
@@ -177,6 +208,22 @@ final $typed_data.Uint8List unsealEncryptedKeyDescriptor = $convert.base64Decode
'QYAiABKAxSCmNpcGhlcnRleHQSJwoPYXNzb2NpYXRlZF9kYXRhGAMgASgMUg5hc3NvY2lhdGVk' 'QYAiABKAxSCmNpcGhlcnRleHQSJwoPYXNzb2NpYXRlZF9kYXRhGAMgASgMUg5hc3NvY2lhdGVk'
'RGF0YQ=='); 'RGF0YQ==');
@$core.Deprecated('Use bootstrapEncryptedKeyDescriptor instead')
const BootstrapEncryptedKey$json = {
'1': 'BootstrapEncryptedKey',
'2': [
{'1': 'nonce', '3': 1, '4': 1, '5': 12, '10': 'nonce'},
{'1': 'ciphertext', '3': 2, '4': 1, '5': 12, '10': 'ciphertext'},
{'1': 'associated_data', '3': 3, '4': 1, '5': 12, '10': 'associatedData'},
],
};
/// Descriptor for `BootstrapEncryptedKey`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List bootstrapEncryptedKeyDescriptor = $convert.base64Decode(
'ChVCb290c3RyYXBFbmNyeXB0ZWRLZXkSFAoFbm9uY2UYASABKAxSBW5vbmNlEh4KCmNpcGhlcn'
'RleHQYAiABKAxSCmNpcGhlcnRleHQSJwoPYXNzb2NpYXRlZF9kYXRhGAMgASgMUg5hc3NvY2lh'
'dGVkRGF0YQ==');
@$core.Deprecated('Use clientConnectionRequestDescriptor instead') @$core.Deprecated('Use clientConnectionRequestDescriptor instead')
const ClientConnectionRequest$json = { const ClientConnectionRequest$json = {
'1': 'ClientConnectionRequest', '1': 'ClientConnectionRequest',
@@ -216,6 +263,7 @@ final $typed_data.Uint8List clientConnectionCancelDescriptor =
const UserAgentRequest$json = { const UserAgentRequest$json = {
'1': 'UserAgentRequest', '1': 'UserAgentRequest',
'2': [ '2': [
{'1': 'id', '3': 14, '4': 1, '5': 5, '10': 'id'},
{ {
'1': 'auth_challenge_request', '1': 'auth_challenge_request',
'3': 1, '3': 1,
@@ -315,6 +363,15 @@ const UserAgentRequest$json = {
'9': 0, '9': 0,
'10': 'clientConnectionResponse' '10': 'clientConnectionResponse'
}, },
{
'1': 'bootstrap_encrypted_key',
'3': 12,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.BootstrapEncryptedKey',
'9': 0,
'10': 'bootstrapEncryptedKey'
},
], ],
'8': [ '8': [
{'1': 'payload'}, {'1': 'payload'},
@@ -323,28 +380,32 @@ const UserAgentRequest$json = {
/// Descriptor for `UserAgentRequest`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `UserAgentRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List userAgentRequestDescriptor = $convert.base64Decode( final $typed_data.Uint8List userAgentRequestDescriptor = $convert.base64Decode(
'ChBVc2VyQWdlbnRSZXF1ZXN0EmAKFmF1dGhfY2hhbGxlbmdlX3JlcXVlc3QYASABKAsyKC5hcm' 'ChBVc2VyQWdlbnRSZXF1ZXN0Eg4KAmlkGA4gASgFUgJpZBJgChZhdXRoX2NoYWxsZW5nZV9yZX'
'JpdGVyLnVzZXJfYWdlbnQuQXV0aENoYWxsZW5nZVJlcXVlc3RIAFIUYXV0aENoYWxsZW5nZVJl' 'F1ZXN0GAEgASgLMiguYXJiaXRlci51c2VyX2FnZW50LkF1dGhDaGFsbGVuZ2VSZXF1ZXN0SABS'
'cXVlc3QSYwoXYXV0aF9jaGFsbGVuZ2Vfc29sdXRpb24YAiABKAsyKS5hcmJpdGVyLnVzZXJfYW' 'FGF1dGhDaGFsbGVuZ2VSZXF1ZXN0EmMKF2F1dGhfY2hhbGxlbmdlX3NvbHV0aW9uGAIgASgLMi'
'dlbnQuQXV0aENoYWxsZW5nZVNvbHV0aW9uSABSFWF1dGhDaGFsbGVuZ2VTb2x1dGlvbhJECgx1' 'kuYXJiaXRlci51c2VyX2FnZW50LkF1dGhDaGFsbGVuZ2VTb2x1dGlvbkgAUhVhdXRoQ2hhbGxl'
'bnNlYWxfc3RhcnQYAyABKAsyHy5hcmJpdGVyLnVzZXJfYWdlbnQuVW5zZWFsU3RhcnRIAFILdW' 'bmdlU29sdXRpb24SRAoMdW5zZWFsX3N0YXJ0GAMgASgLMh8uYXJiaXRlci51c2VyX2FnZW50Ll'
'5zZWFsU3RhcnQSWgoUdW5zZWFsX2VuY3J5cHRlZF9rZXkYBCABKAsyJi5hcmJpdGVyLnVzZXJf' 'Vuc2VhbFN0YXJ0SABSC3Vuc2VhbFN0YXJ0EloKFHVuc2VhbF9lbmNyeXB0ZWRfa2V5GAQgASgL'
'YWdlbnQuVW5zZWFsRW5jcnlwdGVkS2V5SABSEnVuc2VhbEVuY3J5cHRlZEtleRJEChFxdWVyeV' 'MiYuYXJiaXRlci51c2VyX2FnZW50LlVuc2VhbEVuY3J5cHRlZEtleUgAUhJ1bnNlYWxFbmNyeX'
'92YXVsdF9zdGF0ZRgFIAEoCzIWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eUgAUg9xdWVyeVZhdWx0' 'B0ZWRLZXkSRAoRcXVlcnlfdmF1bHRfc3RhdGUYBSABKAsyFi5nb29nbGUucHJvdG9idWYuRW1w'
'U3RhdGUSRAoRZXZtX3dhbGxldF9jcmVhdGUYBiABKAsyFi5nb29nbGUucHJvdG9idWYuRW1wdH' 'dHlIAFIPcXVlcnlWYXVsdFN0YXRlEkQKEWV2bV93YWxsZXRfY3JlYXRlGAYgASgLMhYuZ29vZ2'
'lIAFIPZXZtV2FsbGV0Q3JlYXRlEkAKD2V2bV93YWxsZXRfbGlzdBgHIAEoCzIWLmdvb2dsZS5w' 'xlLnByb3RvYnVmLkVtcHR5SABSD2V2bVdhbGxldENyZWF0ZRJACg9ldm1fd2FsbGV0X2xpc3QY'
'cm90b2J1Zi5FbXB0eUgAUg1ldm1XYWxsZXRMaXN0Ek4KEGV2bV9ncmFudF9jcmVhdGUYCCABKA' 'ByABKAsyFi5nb29nbGUucHJvdG9idWYuRW1wdHlIAFINZXZtV2FsbGV0TGlzdBJOChBldm1fZ3'
'syIi5hcmJpdGVyLmV2bS5Fdm1HcmFudENyZWF0ZVJlcXVlc3RIAFIOZXZtR3JhbnRDcmVhdGUS' 'JhbnRfY3JlYXRlGAggASgLMiIuYXJiaXRlci5ldm0uRXZtR3JhbnRDcmVhdGVSZXF1ZXN0SABS'
'TgoQZXZtX2dyYW50X2RlbGV0ZRgJIAEoCzIiLmFyYml0ZXIuZXZtLkV2bUdyYW50RGVsZXRlUm' 'DmV2bUdyYW50Q3JlYXRlEk4KEGV2bV9ncmFudF9kZWxldGUYCSABKAsyIi5hcmJpdGVyLmV2bS'
'VxdWVzdEgAUg5ldm1HcmFudERlbGV0ZRJICg5ldm1fZ3JhbnRfbGlzdBgKIAEoCzIgLmFyYml0' '5Fdm1HcmFudERlbGV0ZVJlcXVlc3RIAFIOZXZtR3JhbnREZWxldGUSSAoOZXZtX2dyYW50X2xp'
'ZXIuZXZtLkV2bUdyYW50TGlzdFJlcXVlc3RIAFIMZXZtR3JhbnRMaXN0EmwKGmNsaWVudF9jb2' 'c3QYCiABKAsyIC5hcmJpdGVyLmV2bS5Fdm1HcmFudExpc3RSZXF1ZXN0SABSDGV2bUdyYW50TG'
'5uZWN0aW9uX3Jlc3BvbnNlGAsgASgLMiwuYXJiaXRlci51c2VyX2FnZW50LkNsaWVudENvbm5l' 'lzdBJsChpjbGllbnRfY29ubmVjdGlvbl9yZXNwb25zZRgLIAEoCzIsLmFyYml0ZXIudXNlcl9h'
'Y3Rpb25SZXNwb25zZUgAUhhjbGllbnRDb25uZWN0aW9uUmVzcG9uc2VCCQoHcGF5bG9hZA=='); 'Z2VudC5DbGllbnRDb25uZWN0aW9uUmVzcG9uc2VIAFIYY2xpZW50Q29ubmVjdGlvblJlc3Bvbn'
'NlEmMKF2Jvb3RzdHJhcF9lbmNyeXB0ZWRfa2V5GAwgASgLMikuYXJiaXRlci51c2VyX2FnZW50'
'LkJvb3RzdHJhcEVuY3J5cHRlZEtleUgAUhVib290c3RyYXBFbmNyeXB0ZWRLZXlCCQoHcGF5bG'
'9hZA==');
@$core.Deprecated('Use userAgentResponseDescriptor instead') @$core.Deprecated('Use userAgentResponseDescriptor instead')
const UserAgentResponse$json = { const UserAgentResponse$json = {
'1': 'UserAgentResponse', '1': 'UserAgentResponse',
'2': [ '2': [
{'1': 'id', '3': 14, '4': 1, '5': 5, '9': 1, '10': 'id', '17': true},
{ {
'1': 'auth_challenge', '1': 'auth_challenge',
'3': 1, '3': 1,
@@ -355,13 +416,13 @@ const UserAgentResponse$json = {
'10': 'authChallenge' '10': 'authChallenge'
}, },
{ {
'1': 'auth_ok', '1': 'auth_result',
'3': 2, '3': 2,
'4': 1, '4': 1,
'5': 11, '5': 14,
'6': '.arbiter.user_agent.AuthOk', '6': '.arbiter.user_agent.AuthResult',
'9': 0, '9': 0,
'10': 'authOk' '10': 'authResult'
}, },
{ {
'1': 'unseal_start_response', '1': 'unseal_start_response',
@@ -453,30 +514,42 @@ const UserAgentResponse$json = {
'9': 0, '9': 0,
'10': 'clientConnectionCancel' '10': 'clientConnectionCancel'
}, },
{
'1': 'bootstrap_result',
'3': 13,
'4': 1,
'5': 14,
'6': '.arbiter.user_agent.BootstrapResult',
'9': 0,
'10': 'bootstrapResult'
},
], ],
'8': [ '8': [
{'1': 'payload'}, {'1': 'payload'},
{'1': '_id'},
], ],
}; };
/// Descriptor for `UserAgentResponse`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `UserAgentResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List userAgentResponseDescriptor = $convert.base64Decode( final $typed_data.Uint8List userAgentResponseDescriptor = $convert.base64Decode(
'ChFVc2VyQWdlbnRSZXNwb25zZRJKCg5hdXRoX2NoYWxsZW5nZRgBIAEoCzIhLmFyYml0ZXIudX' 'ChFVc2VyQWdlbnRSZXNwb25zZRITCgJpZBgOIAEoBUgBUgJpZIgBARJKCg5hdXRoX2NoYWxsZW'
'Nlcl9hZ2VudC5BdXRoQ2hhbGxlbmdlSABSDWF1dGhDaGFsbGVuZ2USNQoHYXV0aF9vaxgCIAEo' '5nZRgBIAEoCzIhLmFyYml0ZXIudXNlcl9hZ2VudC5BdXRoQ2hhbGxlbmdlSABSDWF1dGhDaGFs'
'CzIaLmFyYml0ZXIudXNlcl9hZ2VudC5BdXRoT2tIAFIGYXV0aE9rEl0KFXVuc2VhbF9zdGFydF' 'bGVuZ2USQQoLYXV0aF9yZXN1bHQYAiABKA4yHi5hcmJpdGVyLnVzZXJfYWdlbnQuQXV0aFJlc3'
'9yZXNwb25zZRgDIAEoCzInLmFyYml0ZXIudXNlcl9hZ2VudC5VbnNlYWxTdGFydFJlc3BvbnNl' 'VsdEgAUgphdXRoUmVzdWx0El0KFXVuc2VhbF9zdGFydF9yZXNwb25zZRgDIAEoCzInLmFyYml0'
'SABSE3Vuc2VhbFN0YXJ0UmVzcG9uc2USRwoNdW5zZWFsX3Jlc3VsdBgEIAEoDjIgLmFyYml0ZX' 'ZXIudXNlcl9hZ2VudC5VbnNlYWxTdGFydFJlc3BvbnNlSABSE3Vuc2VhbFN0YXJ0UmVzcG9uc2'
'IudXNlcl9hZ2VudC5VbnNlYWxSZXN1bHRIAFIMdW5zZWFsUmVzdWx0EkEKC3ZhdWx0X3N0YXRl' 'USRwoNdW5zZWFsX3Jlc3VsdBgEIAEoDjIgLmFyYml0ZXIudXNlcl9hZ2VudC5VbnNlYWxSZXN1'
'GAUgASgOMh4uYXJiaXRlci51c2VyX2FnZW50LlZhdWx0U3RhdGVIAFIKdmF1bHRTdGF0ZRJPCh' 'bHRIAFIMdW5zZWFsUmVzdWx0EkEKC3ZhdWx0X3N0YXRlGAUgASgOMh4uYXJiaXRlci51c2VyX2'
'Fldm1fd2FsbGV0X2NyZWF0ZRgGIAEoCzIhLmFyYml0ZXIuZXZtLldhbGxldENyZWF0ZVJlc3Bv' 'FnZW50LlZhdWx0U3RhdGVIAFIKdmF1bHRTdGF0ZRJPChFldm1fd2FsbGV0X2NyZWF0ZRgGIAEo'
'bnNlSABSD2V2bVdhbGxldENyZWF0ZRJJCg9ldm1fd2FsbGV0X2xpc3QYByABKAsyHy5hcmJpdG' 'CzIhLmFyYml0ZXIuZXZtLldhbGxldENyZWF0ZVJlc3BvbnNlSABSD2V2bVdhbGxldENyZWF0ZR'
'VyLmV2bS5XYWxsZXRMaXN0UmVzcG9uc2VIAFINZXZtV2FsbGV0TGlzdBJPChBldm1fZ3JhbnRf' 'JJCg9ldm1fd2FsbGV0X2xpc3QYByABKAsyHy5hcmJpdGVyLmV2bS5XYWxsZXRMaXN0UmVzcG9u'
'Y3JlYXRlGAggASgLMiMuYXJiaXRlci5ldm0uRXZtR3JhbnRDcmVhdGVSZXNwb25zZUgAUg5ldm' 'c2VIAFINZXZtV2FsbGV0TGlzdBJPChBldm1fZ3JhbnRfY3JlYXRlGAggASgLMiMuYXJiaXRlci'
'1HcmFudENyZWF0ZRJPChBldm1fZ3JhbnRfZGVsZXRlGAkgASgLMiMuYXJiaXRlci5ldm0uRXZt' '5ldm0uRXZtR3JhbnRDcmVhdGVSZXNwb25zZUgAUg5ldm1HcmFudENyZWF0ZRJPChBldm1fZ3Jh'
'R3JhbnREZWxldGVSZXNwb25zZUgAUg5ldm1HcmFudERlbGV0ZRJJCg5ldm1fZ3JhbnRfbGlzdB' 'bnRfZGVsZXRlGAkgASgLMiMuYXJiaXRlci5ldm0uRXZtR3JhbnREZWxldGVSZXNwb25zZUgAUg'
'gKIAEoCzIhLmFyYml0ZXIuZXZtLkV2bUdyYW50TGlzdFJlc3BvbnNlSABSDGV2bUdyYW50TGlz' '5ldm1HcmFudERlbGV0ZRJJCg5ldm1fZ3JhbnRfbGlzdBgKIAEoCzIhLmFyYml0ZXIuZXZtLkV2'
'dBJpChljbGllbnRfY29ubmVjdGlvbl9yZXF1ZXN0GAsgASgLMisuYXJiaXRlci51c2VyX2FnZW' 'bUdyYW50TGlzdFJlc3BvbnNlSABSDGV2bUdyYW50TGlzdBJpChljbGllbnRfY29ubmVjdGlvbl'
'50LkNsaWVudENvbm5lY3Rpb25SZXF1ZXN0SABSF2NsaWVudENvbm5lY3Rpb25SZXF1ZXN0EmYK' '9yZXF1ZXN0GAsgASgLMisuYXJiaXRlci51c2VyX2FnZW50LkNsaWVudENvbm5lY3Rpb25SZXF1'
'GGNsaWVudF9jb25uZWN0aW9uX2NhbmNlbBgMIAEoCzIqLmFyYml0ZXIudXNlcl9hZ2VudC5DbG' 'ZXN0SABSF2NsaWVudENvbm5lY3Rpb25SZXF1ZXN0EmYKGGNsaWVudF9jb25uZWN0aW9uX2Nhbm'
'llbnRDb25uZWN0aW9uQ2FuY2VsSABSFmNsaWVudENvbm5lY3Rpb25DYW5jZWxCCQoHcGF5bG9h' 'NlbBgMIAEoCzIqLmFyYml0ZXIudXNlcl9hZ2VudC5DbGllbnRDb25uZWN0aW9uQ2FuY2VsSABS'
'ZA=='); 'FmNsaWVudENvbm5lY3Rpb25DYW5jZWwSUAoQYm9vdHN0cmFwX3Jlc3VsdBgNIAEoDjIjLmFyYm'
'l0ZXIudXNlcl9hZ2VudC5Cb290c3RyYXBSZXN1bHRIAFIPYm9vdHN0cmFwUmVzdWx0QgkKB3Bh'
'eWxvYWRCBQoDX2lk');

View File

@@ -13,9 +13,7 @@ Future<VaultState?> vaultState(Ref ref) async {
return null; return null;
} }
await conn.send(UserAgentRequest(queryVaultState: Empty())); final resp = await conn.request(UserAgentRequest(queryVaultState: Empty()));
final resp = await conn.receive();
if (resp.whichPayload() != UserAgentResponse_Payload.vaultState) { if (resp.whichPayload() != UserAgentResponse_Payload.vaultState) {
talker.warning('Expected vault state response, got ${resp.whichPayload()}'); talker.warning('Expected vault state response, got ${resp.whichPayload()}');
return null; return null;

View File

@@ -46,4 +46,4 @@ final class VaultStateProvider
} }
} }
String _$vaultStateHash() => r'1fd975a9661de1f62beef9eb1c7c439f377a8b88'; String _$vaultStateHash() => r'97085e49bc3a296e36fa6c04a8f4c9abafac0835';