feat(useragent): bootstrap / unseal flow implementattion
This commit is contained in:
@@ -42,6 +42,12 @@ message UnsealEncryptedKey {
|
|||||||
bytes associated_data = 3;
|
bytes associated_data = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message BootstrapEncryptedKey {
|
||||||
|
bytes nonce = 1;
|
||||||
|
bytes ciphertext = 2;
|
||||||
|
bytes associated_data = 3;
|
||||||
|
}
|
||||||
|
|
||||||
enum UnsealResult {
|
enum UnsealResult {
|
||||||
UNSEAL_RESULT_UNSPECIFIED = 0;
|
UNSEAL_RESULT_UNSPECIFIED = 0;
|
||||||
UNSEAL_RESULT_SUCCESS = 1;
|
UNSEAL_RESULT_SUCCESS = 1;
|
||||||
@@ -49,6 +55,13 @@ enum UnsealResult {
|
|||||||
UNSEAL_RESULT_UNBOOTSTRAPPED = 3;
|
UNSEAL_RESULT_UNBOOTSTRAPPED = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum BootstrapResult {
|
||||||
|
BOOTSTRAP_RESULT_UNSPECIFIED = 0;
|
||||||
|
BOOTSTRAP_RESULT_SUCCESS = 1;
|
||||||
|
BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED = 2;
|
||||||
|
BOOTSTRAP_RESULT_INVALID_KEY = 3;
|
||||||
|
}
|
||||||
|
|
||||||
enum VaultState {
|
enum VaultState {
|
||||||
VAULT_STATE_UNSPECIFIED = 0;
|
VAULT_STATE_UNSPECIFIED = 0;
|
||||||
VAULT_STATE_UNBOOTSTRAPPED = 1;
|
VAULT_STATE_UNBOOTSTRAPPED = 1;
|
||||||
@@ -80,6 +93,7 @@ message UserAgentRequest {
|
|||||||
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;
|
ClientConnectionResponse client_connection_response = 11;
|
||||||
|
BootstrapEncryptedKey bootstrap_encrypted_key = 12;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
message UserAgentResponse {
|
message UserAgentResponse {
|
||||||
@@ -96,5 +110,6 @@ message UserAgentResponse {
|
|||||||
arbiter.evm.EvmGrantListResponse evm_grant_list = 10;
|
arbiter.evm.EvmGrantListResponse evm_grant_list = 10;
|
||||||
ClientConnectionRequest client_connection_request = 11;
|
ClientConnectionRequest client_connection_request = 11;
|
||||||
ClientConnectionCancel client_connection_cancel = 12;
|
ClientConnectionCancel client_connection_cancel = 12;
|
||||||
|
BootstrapResult bootstrap_result = 13;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use std::{ops::DerefMut, sync::Mutex};
|
|||||||
use arbiter_proto::proto::{
|
use arbiter_proto::proto::{
|
||||||
evm as evm_proto,
|
evm as evm_proto,
|
||||||
user_agent::{
|
user_agent::{
|
||||||
ClientConnectionCancel, ClientConnectionRequest, UnsealEncryptedKey, UnsealResult,
|
BootstrapEncryptedKey, BootstrapResult, ClientConnectionCancel, ClientConnectionRequest,
|
||||||
UnsealStart, UnsealStartResponse, UserAgentRequest, UserAgentResponse,
|
UnsealEncryptedKey, UnsealResult, UnsealStart, UnsealStartResponse, UserAgentRequest,
|
||||||
user_agent_request::Payload as UserAgentRequestPayload,
|
UserAgentResponse, user_agent_request::Payload as UserAgentRequestPayload,
|
||||||
user_agent_response::Payload as UserAgentResponsePayload,
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -19,7 +19,7 @@ use x25519_dalek::{EphemeralSecret, PublicKey};
|
|||||||
|
|
||||||
use crate::actors::{
|
use crate::actors::{
|
||||||
evm::{Generate, ListWallets},
|
evm::{Generate, ListWallets},
|
||||||
keyholder::{self, TryUnseal},
|
keyholder::{self, Bootstrap, TryUnseal},
|
||||||
router::RegisterUserAgent,
|
router::RegisterUserAgent,
|
||||||
user_agent::{TransportResponseError, UserAgentConnection},
|
user_agent::{TransportResponseError, UserAgentConnection},
|
||||||
};
|
};
|
||||||
@@ -168,9 +168,11 @@ impl UserAgentSession {
|
|||||||
UserAgentRequestPayload::UnsealEncryptedKey(unseal_encrypted_key) => {
|
UserAgentRequestPayload::UnsealEncryptedKey(unseal_encrypted_key) => {
|
||||||
self.handle_unseal_encrypted_key(unseal_encrypted_key).await
|
self.handle_unseal_encrypted_key(unseal_encrypted_key).await
|
||||||
}
|
}
|
||||||
UserAgentRequestPayload::QueryVaultState(_) => {
|
UserAgentRequestPayload::BootstrapEncryptedKey(bootstrap_encrypted_key) => {
|
||||||
self.handle_query_vault_state().await
|
self.handle_bootstrap_encrypted_key(bootstrap_encrypted_key)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
UserAgentRequestPayload::QueryVaultState(_) => self.handle_query_vault_state().await,
|
||||||
UserAgentRequestPayload::EvmWalletCreate(_) => self.handle_evm_wallet_create().await,
|
UserAgentRequestPayload::EvmWalletCreate(_) => self.handle_evm_wallet_create().await,
|
||||||
UserAgentRequestPayload::EvmWalletList(_) => self.handle_evm_wallet_list().await,
|
UserAgentRequestPayload::EvmWalletList(_) => self.handle_evm_wallet_list().await,
|
||||||
_ => Err(TransportResponseError::UnexpectedRequestPayload),
|
_ => Err(TransportResponseError::UnexpectedRequestPayload),
|
||||||
@@ -187,6 +189,59 @@ fn response(payload: UserAgentResponsePayload) -> UserAgentResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
|
fn take_unseal_secret(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<(EphemeralSecret, PublicKey), TransportResponseError> {
|
||||||
|
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
|
||||||
|
error!("Received encrypted key in invalid state");
|
||||||
|
return Err(TransportResponseError::InvalidStateForUnsealEncryptedKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
let ephemeral_secret = {
|
||||||
|
let mut secret_lock = unseal_context.secret.lock().unwrap();
|
||||||
|
let secret = secret_lock.take();
|
||||||
|
match secret {
|
||||||
|
Some(secret) => secret,
|
||||||
|
None => {
|
||||||
|
drop(secret_lock);
|
||||||
|
error!("Ephemeral secret already taken");
|
||||||
|
return Err(TransportResponseError::StateTransitionFailed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((ephemeral_secret, unseal_context.client_public_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_client_key_material(
|
||||||
|
ephemeral_secret: EphemeralSecret,
|
||||||
|
client_public_key: PublicKey,
|
||||||
|
nonce: &[u8],
|
||||||
|
ciphertext: &[u8],
|
||||||
|
associated_data: &[u8],
|
||||||
|
) -> Result<MemSafe<Vec<u8>>, ()> {
|
||||||
|
let nonce = XNonce::from_slice(nonce);
|
||||||
|
|
||||||
|
let shared_secret = ephemeral_secret.diffie_hellman(&client_public_key);
|
||||||
|
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
|
||||||
|
|
||||||
|
let mut key_buffer = MemSafe::new(ciphertext.to_vec()).unwrap();
|
||||||
|
|
||||||
|
let decryption_result = {
|
||||||
|
let mut write_handle = key_buffer.write().unwrap();
|
||||||
|
let write_handle = write_handle.deref_mut();
|
||||||
|
cipher.decrypt_in_place(nonce, associated_data, write_handle)
|
||||||
|
};
|
||||||
|
|
||||||
|
match decryption_result {
|
||||||
|
Ok(_) => Ok(key_buffer),
|
||||||
|
Err(err) => {
|
||||||
|
error!(?err, "Failed to decrypt encrypted key material");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_unseal_request(&mut self, req: UnsealStart) -> Output {
|
async fn handle_unseal_request(&mut self, req: UnsealStart) -> Output {
|
||||||
let secret = EphemeralSecret::random();
|
let secret = EphemeralSecret::random();
|
||||||
let public_key = PublicKey::from(&secret);
|
let public_key = PublicKey::from(&secret);
|
||||||
@@ -211,41 +266,33 @@ impl UserAgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_unseal_encrypted_key(&mut self, req: UnsealEncryptedKey) -> Output {
|
async fn handle_unseal_encrypted_key(&mut self, req: UnsealEncryptedKey) -> Output {
|
||||||
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
|
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
|
||||||
error!("Received unseal encrypted key in invalid state");
|
Ok(values) => values,
|
||||||
return Err(TransportResponseError::InvalidStateForUnsealEncryptedKey);
|
Err(TransportResponseError::StateTransitionFailed) => {
|
||||||
};
|
|
||||||
let ephemeral_secret = {
|
|
||||||
let mut secret_lock = unseal_context.secret.lock().unwrap();
|
|
||||||
let secret = secret_lock.take();
|
|
||||||
match secret {
|
|
||||||
Some(secret) => secret,
|
|
||||||
None => {
|
|
||||||
drop(secret_lock);
|
|
||||||
error!("Ephemeral secret already taken");
|
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
return Ok(response(UserAgentResponsePayload::UnsealResult(
|
return Ok(response(UserAgentResponsePayload::UnsealResult(
|
||||||
UnsealResult::InvalidKey.into(),
|
UnsealResult::InvalidKey.into(),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
};
|
||||||
|
|
||||||
|
let seal_key_buffer = match Self::decrypt_client_key_material(
|
||||||
|
ephemeral_secret,
|
||||||
|
client_public_key,
|
||||||
|
&req.nonce,
|
||||||
|
&req.ciphertext,
|
||||||
|
&req.associated_data,
|
||||||
|
) {
|
||||||
|
Ok(buffer) => buffer,
|
||||||
|
Err(()) => {
|
||||||
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
|
return Ok(response(UserAgentResponsePayload::UnsealResult(
|
||||||
|
UnsealResult::InvalidKey.into(),
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let nonce = XNonce::from_slice(&req.nonce);
|
|
||||||
|
|
||||||
let shared_secret = ephemeral_secret.diffie_hellman(&unseal_context.client_public_key);
|
|
||||||
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
|
|
||||||
|
|
||||||
let mut seal_key_buffer = MemSafe::new(req.ciphertext.clone()).unwrap();
|
|
||||||
|
|
||||||
let decryption_result = {
|
|
||||||
let mut write_handle = seal_key_buffer.write().unwrap();
|
|
||||||
let write_handle = write_handle.deref_mut();
|
|
||||||
cipher.decrypt_in_place(nonce, &req.associated_data, write_handle)
|
|
||||||
};
|
|
||||||
|
|
||||||
match decryption_result {
|
|
||||||
Ok(_) => {
|
|
||||||
match self
|
match self
|
||||||
.props
|
.props
|
||||||
.actors
|
.actors
|
||||||
@@ -282,21 +329,77 @@ impl UserAgentSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
|
||||||
error!(?err, "Failed to decrypt unseal key");
|
async fn handle_bootstrap_encrypted_key(&mut self, req: BootstrapEncryptedKey) -> Output {
|
||||||
|
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
|
||||||
|
Ok(values) => values,
|
||||||
|
Err(TransportResponseError::StateTransitionFailed) => {
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
Ok(response(UserAgentResponsePayload::UnsealResult(
|
return Ok(response(UserAgentResponsePayload::BootstrapResult(
|
||||||
UnsealResult::InvalidKey.into(),
|
BootstrapResult::InvalidKey.into(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
};
|
||||||
|
|
||||||
|
let seal_key_buffer = match Self::decrypt_client_key_material(
|
||||||
|
ephemeral_secret,
|
||||||
|
client_public_key,
|
||||||
|
&req.nonce,
|
||||||
|
&req.ciphertext,
|
||||||
|
&req.associated_data,
|
||||||
|
) {
|
||||||
|
Ok(buffer) => buffer,
|
||||||
|
Err(()) => {
|
||||||
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
|
return Ok(response(UserAgentResponsePayload::BootstrapResult(
|
||||||
|
BootstrapResult::InvalidKey.into(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match self
|
||||||
|
.props
|
||||||
|
.actors
|
||||||
|
.key_holder
|
||||||
|
.ask(Bootstrap {
|
||||||
|
seal_key_raw: seal_key_buffer,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
info!("Successfully bootstrapped vault with client-provided key");
|
||||||
|
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||||
|
Ok(response(UserAgentResponsePayload::BootstrapResult(
|
||||||
|
BootstrapResult::Success.into(),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
Err(SendError::HandlerError(keyholder::Error::AlreadyBootstrapped)) => {
|
||||||
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
|
Ok(response(UserAgentResponsePayload::BootstrapResult(
|
||||||
|
BootstrapResult::AlreadyBootstrapped.into(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
Err(SendError::HandlerError(err)) => {
|
||||||
|
error!(?err, "Keyholder failed to bootstrap vault");
|
||||||
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
|
Ok(response(UserAgentResponsePayload::BootstrapResult(
|
||||||
|
BootstrapResult::InvalidKey.into(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!(?err, "Failed to send bootstrap request to keyholder");
|
||||||
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
|
Err(TransportResponseError::KeyHolderActorUnreachable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
async fn handle_query_vault_state(&mut self) -> Output {
|
async fn handle_query_vault_state(&mut self) -> Output {
|
||||||
use arbiter_proto::proto::user_agent::VaultState;
|
|
||||||
use crate::actors::keyholder::{GetState, StateDiscriminants};
|
use crate::actors::keyholder::{GetState, StateDiscriminants};
|
||||||
|
use arbiter_proto::proto::user_agent::VaultState;
|
||||||
|
|
||||||
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(StateDiscriminants::Unbootstrapped) => VaultState::Unbootstrapped,
|
||||||
|
|||||||
@@ -254,12 +254,11 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod grpc;
|
mod grpc;
|
||||||
pub use grpc::{connect_grpc, ConnectError, UserAgentGrpc};
|
pub use grpc::{ConnectError, UserAgentGrpc, connect_grpc};
|
||||||
|
|
||||||
use arbiter_proto::proto::user_agent::{
|
use arbiter_proto::proto::user_agent::{
|
||||||
UnsealEncryptedKey, UnsealStart,
|
BootstrapEncryptedKey, UnsealEncryptedKey, UnsealStart,
|
||||||
user_agent_request::Payload as RequestPayload,
|
user_agent_request::Payload as RequestPayload, user_agent_response::Payload as ResponsePayload,
|
||||||
user_agent_response::Payload as ResponsePayload,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Send an `UnsealStart` request and await the server's `UnsealStartResponse`.
|
/// Send an `UnsealStart` request and await the server's `UnsealStartResponse`.
|
||||||
@@ -274,6 +273,13 @@ pub struct SendUnsealEncryptedKey {
|
|||||||
pub associated_data: Vec<u8>,
|
pub associated_data: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a `BootstrapEncryptedKey` request and await the server's `BootstrapResult`.
|
||||||
|
pub struct SendBootstrapEncryptedKey {
|
||||||
|
pub nonce: Vec<u8>,
|
||||||
|
pub ciphertext: Vec<u8>,
|
||||||
|
pub associated_data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Query the server for the current `VaultState`.
|
/// Query the server for the current `VaultState`.
|
||||||
pub struct QueryVaultState;
|
pub struct QueryVaultState;
|
||||||
|
|
||||||
@@ -350,6 +356,40 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<Transport> kameo::message::Message<SendBootstrapEncryptedKey> for UserAgentActor<Transport>
|
||||||
|
where
|
||||||
|
Transport: Bi<UserAgentResponse, UserAgentRequest>,
|
||||||
|
{
|
||||||
|
type Reply = Result<i32, SessionError>;
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
msg: SendBootstrapEncryptedKey,
|
||||||
|
_ctx: &mut kameo::message::Context<Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
|
self.transport
|
||||||
|
.send(UserAgentRequest {
|
||||||
|
payload: Some(RequestPayload::BootstrapEncryptedKey(
|
||||||
|
BootstrapEncryptedKey {
|
||||||
|
nonce: msg.nonce,
|
||||||
|
ciphertext: msg.ciphertext,
|
||||||
|
associated_data: msg.associated_data,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| SessionError::TransportSendFailed)?;
|
||||||
|
|
||||||
|
match self.transport.recv().await {
|
||||||
|
Some(resp) => match resp.payload {
|
||||||
|
Some(ResponsePayload::BootstrapResult(r)) => Ok(r),
|
||||||
|
_ => Err(SessionError::UnexpectedResponse),
|
||||||
|
},
|
||||||
|
None => Err(SessionError::TransportClosed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<Transport> kameo::message::Message<QueryVaultState> for UserAgentActor<Transport>
|
impl<Transport> kameo::message::Message<QueryVaultState> for UserAgentActor<Transport>
|
||||||
where
|
where
|
||||||
Transport: Bi<UserAgentResponse, UserAgentRequest>,
|
Transport: Bi<UserAgentResponse, UserAgentRequest>,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:arbiter/features/connection/server_info_storage.dart';
|
|||||||
import 'package:arbiter/features/identity/pk_manager.dart';
|
import 'package:arbiter/features/identity/pk_manager.dart';
|
||||||
import 'package:arbiter/proto/arbiter.pbgrpc.dart';
|
import 'package:arbiter/proto/arbiter.pbgrpc.dart';
|
||||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
|
import 'package:cryptography/cryptography.dart';
|
||||||
import 'package:grpc/grpc.dart';
|
import 'package:grpc/grpc.dart';
|
||||||
import 'package:mtcore/markettakers.dart';
|
import 'package:mtcore/markettakers.dart';
|
||||||
|
|
||||||
@@ -25,9 +26,11 @@ class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<UserAgentResponse> receive() async {
|
Future<UserAgentResponse> receive() async {
|
||||||
await _rx.moveNext();
|
final hasValue = await _rx.moveNext();
|
||||||
|
if (!hasValue) {
|
||||||
|
throw Exception('Connection closed while waiting for server response.');
|
||||||
|
}
|
||||||
return _rx.current;
|
return _rx.current;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
@@ -64,6 +67,112 @@ List<int> formatChallenge(AuthChallenge challenge, List<int> pubkey) {
|
|||||||
return utf8.encode(payload);
|
return utf8.encode(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _vaultKeyAssociatedData = 'arbiter.vault.password';
|
||||||
|
|
||||||
|
Future<BootstrapResult> bootstrapVault(
|
||||||
|
Connection connection,
|
||||||
|
String password,
|
||||||
|
) async {
|
||||||
|
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
|
||||||
|
|
||||||
|
await connection.send(
|
||||||
|
UserAgentRequest(
|
||||||
|
bootstrapEncryptedKey: BootstrapEncryptedKey(
|
||||||
|
nonce: encryptedKey.nonce,
|
||||||
|
ciphertext: encryptedKey.ciphertext,
|
||||||
|
associatedData: encryptedKey.associatedData,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final response = await connection.receive();
|
||||||
|
if (!response.hasBootstrapResult()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected bootstrap result, got ${response.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.bootstrapResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<UnsealResult> unsealVault(Connection connection, String password) async {
|
||||||
|
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
|
||||||
|
|
||||||
|
await connection.send(
|
||||||
|
UserAgentRequest(
|
||||||
|
unsealEncryptedKey: UnsealEncryptedKey(
|
||||||
|
nonce: encryptedKey.nonce,
|
||||||
|
ciphertext: encryptedKey.ciphertext,
|
||||||
|
associatedData: encryptedKey.associatedData,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final response = await connection.receive();
|
||||||
|
if (!response.hasUnsealResult()) {
|
||||||
|
throw Exception('Expected unseal result, got ${response.whichPayload()}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.unsealResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<_EncryptedVaultKey> _encryptVaultKeyMaterial(
|
||||||
|
Connection connection,
|
||||||
|
String password,
|
||||||
|
) async {
|
||||||
|
final keyExchange = X25519();
|
||||||
|
final cipher = Xchacha20.poly1305Aead();
|
||||||
|
final clientKeyPair = await keyExchange.newKeyPair();
|
||||||
|
final clientPublicKey = await clientKeyPair.extractPublicKey();
|
||||||
|
|
||||||
|
await connection.send(
|
||||||
|
UserAgentRequest(
|
||||||
|
unsealStart: UnsealStart(clientPubkey: clientPublicKey.bytes),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final handshakeResponse = await connection.receive();
|
||||||
|
if (!handshakeResponse.hasUnsealStartResponse()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected unseal handshake response, got ${handshakeResponse.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final serverPublicKey = SimplePublicKey(
|
||||||
|
handshakeResponse.unsealStartResponse.serverPubkey,
|
||||||
|
type: KeyPairType.x25519,
|
||||||
|
);
|
||||||
|
final sharedSecret = await keyExchange.sharedSecretKey(
|
||||||
|
keyPair: clientKeyPair,
|
||||||
|
remotePublicKey: serverPublicKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
final secretBox = await cipher.encrypt(
|
||||||
|
utf8.encode(password),
|
||||||
|
secretKey: sharedSecret,
|
||||||
|
nonce: cipher.newNonce(),
|
||||||
|
aad: utf8.encode(_vaultKeyAssociatedData),
|
||||||
|
);
|
||||||
|
|
||||||
|
return _EncryptedVaultKey(
|
||||||
|
nonce: secretBox.nonce,
|
||||||
|
ciphertext: [...secretBox.cipherText, ...secretBox.mac.bytes],
|
||||||
|
associatedData: utf8.encode(_vaultKeyAssociatedData),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EncryptedVaultKey {
|
||||||
|
const _EncryptedVaultKey({
|
||||||
|
required this.nonce,
|
||||||
|
required this.ciphertext,
|
||||||
|
required this.associatedData,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<int> nonce;
|
||||||
|
final List<int> ciphertext;
|
||||||
|
final List<int> associatedData;
|
||||||
|
}
|
||||||
|
|
||||||
Future<Connection> connectAndAuthorize(
|
Future<Connection> connectAndAuthorize(
|
||||||
StoredServerInfo serverInfo,
|
StoredServerInfo serverInfo,
|
||||||
KeyHandle key, {
|
KeyHandle key, {
|
||||||
@@ -90,12 +199,14 @@ Future<Connection> connectAndAuthorize(
|
|||||||
"Sent auth challenge request with pubkey ${base64Encode(pubkey)}",
|
"Sent auth challenge request with pubkey ${base64Encode(pubkey)}",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
final response = await connection.receive();
|
final response = await connection.receive();
|
||||||
|
|
||||||
talker.info(
|
talker.info('Received response from server, checking auth flow...');
|
||||||
'Received response from server, checking for auth challenge...',
|
|
||||||
);
|
if (response.hasAuthOk()) {
|
||||||
|
talker.info('Authentication successful, connection established');
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.hasAuthChallenge()) {
|
if (!response.hasAuthChallenge()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
|
|||||||
@@ -460,6 +460,89 @@ class UnsealEncryptedKey extends $pb.GeneratedMessage {
|
|||||||
void clearAssociatedData() => $_clearField(3);
|
void clearAssociatedData() => $_clearField(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BootstrapEncryptedKey extends $pb.GeneratedMessage {
|
||||||
|
factory BootstrapEncryptedKey({
|
||||||
|
$core.List<$core.int>? nonce,
|
||||||
|
$core.List<$core.int>? ciphertext,
|
||||||
|
$core.List<$core.int>? associatedData,
|
||||||
|
}) {
|
||||||
|
final result = create();
|
||||||
|
if (nonce != null) result.nonce = nonce;
|
||||||
|
if (ciphertext != null) result.ciphertext = ciphertext;
|
||||||
|
if (associatedData != null) result.associatedData = associatedData;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
BootstrapEncryptedKey._();
|
||||||
|
|
||||||
|
factory BootstrapEncryptedKey.fromBuffer($core.List<$core.int> data,
|
||||||
|
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||||
|
create()..mergeFromBuffer(data, registry);
|
||||||
|
factory BootstrapEncryptedKey.fromJson($core.String json,
|
||||||
|
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||||
|
create()..mergeFromJson(json, registry);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||||
|
_omitMessageNames ? '' : 'BootstrapEncryptedKey',
|
||||||
|
package:
|
||||||
|
const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'),
|
||||||
|
createEmptyInstance: create)
|
||||||
|
..a<$core.List<$core.int>>(
|
||||||
|
1, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OY)
|
||||||
|
..a<$core.List<$core.int>>(
|
||||||
|
2, _omitFieldNames ? '' : 'ciphertext', $pb.PbFieldType.OY)
|
||||||
|
..a<$core.List<$core.int>>(
|
||||||
|
3, _omitFieldNames ? '' : 'associatedData', $pb.PbFieldType.OY)
|
||||||
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
|
BootstrapEncryptedKey clone() => deepCopy();
|
||||||
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
|
BootstrapEncryptedKey copyWith(
|
||||||
|
void Function(BootstrapEncryptedKey) updates) =>
|
||||||
|
super.copyWith((message) => updates(message as BootstrapEncryptedKey))
|
||||||
|
as BootstrapEncryptedKey;
|
||||||
|
|
||||||
|
@$core.override
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static BootstrapEncryptedKey create() => BootstrapEncryptedKey._();
|
||||||
|
@$core.override
|
||||||
|
BootstrapEncryptedKey createEmptyInstance() => create();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static BootstrapEncryptedKey getDefault() => _defaultInstance ??=
|
||||||
|
$pb.GeneratedMessage.$_defaultFor<BootstrapEncryptedKey>(create);
|
||||||
|
static BootstrapEncryptedKey? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.List<$core.int> get nonce => $_getN(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set nonce($core.List<$core.int> value) => $_setBytes(0, value);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasNonce() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearNonce() => $_clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.List<$core.int> get ciphertext => $_getN(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set ciphertext($core.List<$core.int> value) => $_setBytes(1, value);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasCiphertext() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearCiphertext() => $_clearField(2);
|
||||||
|
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.List<$core.int> get associatedData => $_getN(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
set associatedData($core.List<$core.int> value) => $_setBytes(2, value);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.bool hasAssociatedData() => $_has(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
void clearAssociatedData() => $_clearField(3);
|
||||||
|
}
|
||||||
|
|
||||||
class ClientConnectionRequest extends $pb.GeneratedMessage {
|
class ClientConnectionRequest extends $pb.GeneratedMessage {
|
||||||
factory ClientConnectionRequest({
|
factory ClientConnectionRequest({
|
||||||
$core.List<$core.int>? pubkey,
|
$core.List<$core.int>? pubkey,
|
||||||
@@ -625,6 +708,7 @@ enum UserAgentRequest_Payload {
|
|||||||
evmGrantDelete,
|
evmGrantDelete,
|
||||||
evmGrantList,
|
evmGrantList,
|
||||||
clientConnectionResponse,
|
clientConnectionResponse,
|
||||||
|
bootstrapEncryptedKey,
|
||||||
notSet
|
notSet
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,6 +725,7 @@ class UserAgentRequest extends $pb.GeneratedMessage {
|
|||||||
$1.EvmGrantDeleteRequest? evmGrantDelete,
|
$1.EvmGrantDeleteRequest? evmGrantDelete,
|
||||||
$1.EvmGrantListRequest? evmGrantList,
|
$1.EvmGrantListRequest? evmGrantList,
|
||||||
ClientConnectionResponse? clientConnectionResponse,
|
ClientConnectionResponse? clientConnectionResponse,
|
||||||
|
BootstrapEncryptedKey? bootstrapEncryptedKey,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (authChallengeRequest != null)
|
if (authChallengeRequest != null)
|
||||||
@@ -658,6 +743,8 @@ class UserAgentRequest extends $pb.GeneratedMessage {
|
|||||||
if (evmGrantList != null) result.evmGrantList = evmGrantList;
|
if (evmGrantList != null) result.evmGrantList = evmGrantList;
|
||||||
if (clientConnectionResponse != null)
|
if (clientConnectionResponse != null)
|
||||||
result.clientConnectionResponse = clientConnectionResponse;
|
result.clientConnectionResponse = clientConnectionResponse;
|
||||||
|
if (bootstrapEncryptedKey != null)
|
||||||
|
result.bootstrapEncryptedKey = bootstrapEncryptedKey;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,6 +770,7 @@ class UserAgentRequest extends $pb.GeneratedMessage {
|
|||||||
9: UserAgentRequest_Payload.evmGrantDelete,
|
9: UserAgentRequest_Payload.evmGrantDelete,
|
||||||
10: UserAgentRequest_Payload.evmGrantList,
|
10: UserAgentRequest_Payload.evmGrantList,
|
||||||
11: UserAgentRequest_Payload.clientConnectionResponse,
|
11: UserAgentRequest_Payload.clientConnectionResponse,
|
||||||
|
12: UserAgentRequest_Payload.bootstrapEncryptedKey,
|
||||||
0: UserAgentRequest_Payload.notSet
|
0: UserAgentRequest_Payload.notSet
|
||||||
};
|
};
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||||
@@ -690,7 +778,7 @@ class UserAgentRequest extends $pb.GeneratedMessage {
|
|||||||
package:
|
package:
|
||||||
const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'),
|
const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'),
|
||||||
createEmptyInstance: create)
|
createEmptyInstance: create)
|
||||||
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
|
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
|
||||||
..aOM<AuthChallengeRequest>(
|
..aOM<AuthChallengeRequest>(
|
||||||
1, _omitFieldNames ? '' : 'authChallengeRequest',
|
1, _omitFieldNames ? '' : 'authChallengeRequest',
|
||||||
subBuilder: AuthChallengeRequest.create)
|
subBuilder: AuthChallengeRequest.create)
|
||||||
@@ -716,6 +804,9 @@ class UserAgentRequest extends $pb.GeneratedMessage {
|
|||||||
..aOM<ClientConnectionResponse>(
|
..aOM<ClientConnectionResponse>(
|
||||||
11, _omitFieldNames ? '' : 'clientConnectionResponse',
|
11, _omitFieldNames ? '' : 'clientConnectionResponse',
|
||||||
subBuilder: ClientConnectionResponse.create)
|
subBuilder: ClientConnectionResponse.create)
|
||||||
|
..aOM<BootstrapEncryptedKey>(
|
||||||
|
12, _omitFieldNames ? '' : 'bootstrapEncryptedKey',
|
||||||
|
subBuilder: BootstrapEncryptedKey.create)
|
||||||
..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.')
|
||||||
@@ -748,6 +839,7 @@ class UserAgentRequest extends $pb.GeneratedMessage {
|
|||||||
@$pb.TagNumber(9)
|
@$pb.TagNumber(9)
|
||||||
@$pb.TagNumber(10)
|
@$pb.TagNumber(10)
|
||||||
@$pb.TagNumber(11)
|
@$pb.TagNumber(11)
|
||||||
|
@$pb.TagNumber(12)
|
||||||
UserAgentRequest_Payload whichPayload() =>
|
UserAgentRequest_Payload whichPayload() =>
|
||||||
_UserAgentRequest_PayloadByTag[$_whichOneof(0)]!;
|
_UserAgentRequest_PayloadByTag[$_whichOneof(0)]!;
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
@@ -761,6 +853,7 @@ class UserAgentRequest extends $pb.GeneratedMessage {
|
|||||||
@$pb.TagNumber(9)
|
@$pb.TagNumber(9)
|
||||||
@$pb.TagNumber(10)
|
@$pb.TagNumber(10)
|
||||||
@$pb.TagNumber(11)
|
@$pb.TagNumber(11)
|
||||||
|
@$pb.TagNumber(12)
|
||||||
void clearPayload() => $_clearField($_whichOneof(0));
|
void clearPayload() => $_clearField($_whichOneof(0));
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
@@ -885,6 +978,18 @@ class UserAgentRequest extends $pb.GeneratedMessage {
|
|||||||
void clearClientConnectionResponse() => $_clearField(11);
|
void clearClientConnectionResponse() => $_clearField(11);
|
||||||
@$pb.TagNumber(11)
|
@$pb.TagNumber(11)
|
||||||
ClientConnectionResponse ensureClientConnectionResponse() => $_ensure(10);
|
ClientConnectionResponse ensureClientConnectionResponse() => $_ensure(10);
|
||||||
|
|
||||||
|
@$pb.TagNumber(12)
|
||||||
|
BootstrapEncryptedKey get bootstrapEncryptedKey => $_getN(11);
|
||||||
|
@$pb.TagNumber(12)
|
||||||
|
set bootstrapEncryptedKey(BootstrapEncryptedKey value) =>
|
||||||
|
$_setField(12, value);
|
||||||
|
@$pb.TagNumber(12)
|
||||||
|
$core.bool hasBootstrapEncryptedKey() => $_has(11);
|
||||||
|
@$pb.TagNumber(12)
|
||||||
|
void clearBootstrapEncryptedKey() => $_clearField(12);
|
||||||
|
@$pb.TagNumber(12)
|
||||||
|
BootstrapEncryptedKey ensureBootstrapEncryptedKey() => $_ensure(11);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserAgentResponse_Payload {
|
enum UserAgentResponse_Payload {
|
||||||
@@ -900,6 +1005,7 @@ enum UserAgentResponse_Payload {
|
|||||||
evmGrantList,
|
evmGrantList,
|
||||||
clientConnectionRequest,
|
clientConnectionRequest,
|
||||||
clientConnectionCancel,
|
clientConnectionCancel,
|
||||||
|
bootstrapResult,
|
||||||
notSet
|
notSet
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,6 +1023,7 @@ class UserAgentResponse extends $pb.GeneratedMessage {
|
|||||||
$1.EvmGrantListResponse? evmGrantList,
|
$1.EvmGrantListResponse? evmGrantList,
|
||||||
ClientConnectionRequest? clientConnectionRequest,
|
ClientConnectionRequest? clientConnectionRequest,
|
||||||
ClientConnectionCancel? clientConnectionCancel,
|
ClientConnectionCancel? clientConnectionCancel,
|
||||||
|
BootstrapResult? bootstrapResult,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (authChallenge != null) result.authChallenge = authChallenge;
|
if (authChallenge != null) result.authChallenge = authChallenge;
|
||||||
@@ -934,6 +1041,7 @@ class UserAgentResponse extends $pb.GeneratedMessage {
|
|||||||
result.clientConnectionRequest = clientConnectionRequest;
|
result.clientConnectionRequest = clientConnectionRequest;
|
||||||
if (clientConnectionCancel != null)
|
if (clientConnectionCancel != null)
|
||||||
result.clientConnectionCancel = clientConnectionCancel;
|
result.clientConnectionCancel = clientConnectionCancel;
|
||||||
|
if (bootstrapResult != null) result.bootstrapResult = bootstrapResult;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -960,6 +1068,7 @@ class UserAgentResponse extends $pb.GeneratedMessage {
|
|||||||
10: UserAgentResponse_Payload.evmGrantList,
|
10: UserAgentResponse_Payload.evmGrantList,
|
||||||
11: UserAgentResponse_Payload.clientConnectionRequest,
|
11: UserAgentResponse_Payload.clientConnectionRequest,
|
||||||
12: UserAgentResponse_Payload.clientConnectionCancel,
|
12: UserAgentResponse_Payload.clientConnectionCancel,
|
||||||
|
13: UserAgentResponse_Payload.bootstrapResult,
|
||||||
0: UserAgentResponse_Payload.notSet
|
0: UserAgentResponse_Payload.notSet
|
||||||
};
|
};
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||||
@@ -967,7 +1076,7 @@ class UserAgentResponse extends $pb.GeneratedMessage {
|
|||||||
package:
|
package:
|
||||||
const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'),
|
const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'),
|
||||||
createEmptyInstance: create)
|
createEmptyInstance: create)
|
||||||
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
|
..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)
|
..aOM<AuthOk>(2, _omitFieldNames ? '' : 'authOk', subBuilder: AuthOk.create)
|
||||||
@@ -993,6 +1102,8 @@ class UserAgentResponse extends $pb.GeneratedMessage {
|
|||||||
..aOM<ClientConnectionCancel>(
|
..aOM<ClientConnectionCancel>(
|
||||||
12, _omitFieldNames ? '' : 'clientConnectionCancel',
|
12, _omitFieldNames ? '' : 'clientConnectionCancel',
|
||||||
subBuilder: ClientConnectionCancel.create)
|
subBuilder: ClientConnectionCancel.create)
|
||||||
|
..aE<BootstrapResult>(13, _omitFieldNames ? '' : 'bootstrapResult',
|
||||||
|
enumValues: BootstrapResult.values)
|
||||||
..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.')
|
||||||
@@ -1026,6 +1137,7 @@ class UserAgentResponse extends $pb.GeneratedMessage {
|
|||||||
@$pb.TagNumber(10)
|
@$pb.TagNumber(10)
|
||||||
@$pb.TagNumber(11)
|
@$pb.TagNumber(11)
|
||||||
@$pb.TagNumber(12)
|
@$pb.TagNumber(12)
|
||||||
|
@$pb.TagNumber(13)
|
||||||
UserAgentResponse_Payload whichPayload() =>
|
UserAgentResponse_Payload whichPayload() =>
|
||||||
_UserAgentResponse_PayloadByTag[$_whichOneof(0)]!;
|
_UserAgentResponse_PayloadByTag[$_whichOneof(0)]!;
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
@@ -1040,6 +1152,7 @@ class UserAgentResponse extends $pb.GeneratedMessage {
|
|||||||
@$pb.TagNumber(10)
|
@$pb.TagNumber(10)
|
||||||
@$pb.TagNumber(11)
|
@$pb.TagNumber(11)
|
||||||
@$pb.TagNumber(12)
|
@$pb.TagNumber(12)
|
||||||
|
@$pb.TagNumber(13)
|
||||||
void clearPayload() => $_clearField($_whichOneof(0));
|
void clearPayload() => $_clearField($_whichOneof(0));
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
@@ -1171,6 +1284,15 @@ class UserAgentResponse extends $pb.GeneratedMessage {
|
|||||||
void clearClientConnectionCancel() => $_clearField(12);
|
void clearClientConnectionCancel() => $_clearField(12);
|
||||||
@$pb.TagNumber(12)
|
@$pb.TagNumber(12)
|
||||||
ClientConnectionCancel ensureClientConnectionCancel() => $_ensure(11);
|
ClientConnectionCancel ensureClientConnectionCancel() => $_ensure(11);
|
||||||
|
|
||||||
|
@$pb.TagNumber(13)
|
||||||
|
BootstrapResult get bootstrapResult => $_getN(12);
|
||||||
|
@$pb.TagNumber(13)
|
||||||
|
set bootstrapResult(BootstrapResult value) => $_setField(13, value);
|
||||||
|
@$pb.TagNumber(13)
|
||||||
|
$core.bool hasBootstrapResult() => $_has(12);
|
||||||
|
@$pb.TagNumber(13)
|
||||||
|
void clearBootstrapResult() => $_clearField(13);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $core.bool _omitFieldNames =
|
const $core.bool _omitFieldNames =
|
||||||
|
|||||||
@@ -64,6 +64,31 @@ class UnsealResult extends $pb.ProtobufEnum {
|
|||||||
const UnsealResult._(super.value, super.name);
|
const UnsealResult._(super.value, super.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BootstrapResult extends $pb.ProtobufEnum {
|
||||||
|
static const BootstrapResult BOOTSTRAP_RESULT_UNSPECIFIED =
|
||||||
|
BootstrapResult._(0, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_UNSPECIFIED');
|
||||||
|
static const BootstrapResult BOOTSTRAP_RESULT_SUCCESS =
|
||||||
|
BootstrapResult._(1, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_SUCCESS');
|
||||||
|
static const BootstrapResult BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED =
|
||||||
|
BootstrapResult._(2, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED');
|
||||||
|
static const BootstrapResult BOOTSTRAP_RESULT_INVALID_KEY =
|
||||||
|
BootstrapResult._(3, _omitEnumNames ? '' : 'BOOTSTRAP_RESULT_INVALID_KEY');
|
||||||
|
|
||||||
|
static const $core.List<BootstrapResult> values = <BootstrapResult>[
|
||||||
|
BOOTSTRAP_RESULT_UNSPECIFIED,
|
||||||
|
BOOTSTRAP_RESULT_SUCCESS,
|
||||||
|
BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED,
|
||||||
|
BOOTSTRAP_RESULT_INVALID_KEY,
|
||||||
|
];
|
||||||
|
|
||||||
|
static final $core.List<BootstrapResult?> _byValue =
|
||||||
|
$pb.ProtobufEnum.$_initByValueList(values, 3);
|
||||||
|
static BootstrapResult? valueOf($core.int value) =>
|
||||||
|
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
||||||
|
|
||||||
|
const BootstrapResult._(super.value, super.name);
|
||||||
|
}
|
||||||
|
|
||||||
class VaultState extends $pb.ProtobufEnum {
|
class VaultState extends $pb.ProtobufEnum {
|
||||||
static const VaultState VAULT_STATE_UNSPECIFIED =
|
static const VaultState VAULT_STATE_UNSPECIFIED =
|
||||||
VaultState._(0, _omitEnumNames ? '' : 'VAULT_STATE_UNSPECIFIED');
|
VaultState._(0, _omitEnumNames ? '' : 'VAULT_STATE_UNSPECIFIED');
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ part 'bootstrap_token.g.dart';
|
|||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class BootstrapToken extends _$BootstrapToken {
|
class BootstrapToken extends _$BootstrapToken {
|
||||||
|
@override
|
||||||
String? build() {
|
String? build() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -14,6 +15,10 @@ class BootstrapToken extends _$BootstrapToken {
|
|||||||
state = token;
|
state = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
state = null;
|
||||||
|
}
|
||||||
|
|
||||||
String? take() {
|
String? take() {
|
||||||
final token = state;
|
final token = state;
|
||||||
state = null;
|
state = null;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class BootstrapTokenProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$bootstrapTokenHash() => r'a59e679ab0561ed2ab4148660499891571d439db';
|
String _$bootstrapTokenHash() => r'5c09ea4480fc3a7fd0d0a0bced712912542cca5d';
|
||||||
|
|
||||||
abstract class _$BootstrapToken extends $Notifier<String?> {
|
abstract class _$BootstrapToken extends $Notifier<String?> {
|
||||||
String? build();
|
String? build();
|
||||||
|
|||||||
@@ -20,13 +20,19 @@ class ConnectionManager extends _$ConnectionManager {
|
|||||||
}
|
}
|
||||||
final Connection connection;
|
final Connection connection;
|
||||||
try {
|
try {
|
||||||
connection = await connectAndAuthorize(serverInfo, key, bootstrapToken: token);
|
connection = await connectAndAuthorize(
|
||||||
|
serverInfo,
|
||||||
|
key,
|
||||||
|
bootstrapToken: token,
|
||||||
|
);
|
||||||
|
if (token != null) {
|
||||||
|
ref.read(bootstrapTokenProvider.notifier).clear();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
talker.handle(e);
|
talker.handle(e);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
final connection = state.asData?.value;
|
final connection = state.asData?.value;
|
||||||
if (connection != null) {
|
if (connection != null) {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ final class ConnectionManagerProvider
|
|||||||
ConnectionManager create() => ConnectionManager();
|
ConnectionManager create() => ConnectionManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$connectionManagerHash() => r'8923346dff75a9a06127c71a0a39ca65d9733d8c';
|
String _$connectionManagerHash() => r'd01084e550f315bc6cadfe74413a7f959426a80e';
|
||||||
|
|
||||||
abstract class _$ConnectionManager extends $AsyncNotifier<Connection?> {
|
abstract class _$ConnectionManager extends $AsyncNotifier<Connection?> {
|
||||||
FutureOr<Connection?> build();
|
FutureOr<Connection?> build();
|
||||||
|
|||||||
23
useragent/lib/providers/evm.dart
Normal file
23
useragent/lib/providers/evm.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:arbiter/proto/evm.pb.dart';
|
||||||
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
|
import 'package:arbiter/providers/connection/connection_manager.dart';
|
||||||
|
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'evm.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class Evm extends _$Evm {
|
||||||
|
Future<List<WalletEntry>?> build() async {
|
||||||
|
final connection = await ref.watch(connectionManagerProvider.future);
|
||||||
|
if (connection == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await connection.send(UserAgentRequest(
|
||||||
|
evmWalletList: Empty()
|
||||||
|
));
|
||||||
|
|
||||||
|
final response = await connection.receive();
|
||||||
|
}
|
||||||
|
}
|
||||||
55
useragent/lib/providers/evm.g.dart
Normal file
55
useragent/lib/providers/evm.g.dart
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'evm.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(Evm)
|
||||||
|
final evmProvider = EvmProvider._();
|
||||||
|
|
||||||
|
final class EvmProvider
|
||||||
|
extends $AsyncNotifierProvider<Evm, List<WalletEntry>?> {
|
||||||
|
EvmProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'evmProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$evmHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
Evm create() => Evm();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$evmHash() => r'6d2e0baf7b78a0850d7b99b0be7abde206e088c7';
|
||||||
|
|
||||||
|
abstract class _$Evm extends $AsyncNotifier<List<WalletEntry>?> {
|
||||||
|
FutureOr<List<WalletEntry>?> build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref =
|
||||||
|
this.ref as $Ref<AsyncValue<List<WalletEntry>?>, List<WalletEntry>?>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<AsyncValue<List<WalletEntry>?>, List<WalletEntry>?>,
|
||||||
|
AsyncValue<List<WalletEntry>?>,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
useragent/lib/providers/vault_state.dart
Normal file
27
useragent/lib/providers/vault_state.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
|
import 'package:arbiter/providers/connection/connection_manager.dart';
|
||||||
|
import 'package:mtcore/markettakers.dart';
|
||||||
|
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'vault_state.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<VaultState?> vaultState(Ref ref) async {
|
||||||
|
final conn = await ref.watch(connectionManagerProvider.future);
|
||||||
|
if (conn == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn.send(UserAgentRequest(queryVaultState: Empty()));
|
||||||
|
|
||||||
|
final resp = await conn.receive();
|
||||||
|
if (resp.whichPayload() != UserAgentResponse_Payload.vaultState) {
|
||||||
|
talker.warning('Expected vault state response, got ${resp.whichPayload()}');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final vaultState = resp.vaultState;
|
||||||
|
|
||||||
|
return vaultState;
|
||||||
|
}
|
||||||
49
useragent/lib/providers/vault_state.g.dart
Normal file
49
useragent/lib/providers/vault_state.g.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'vault_state.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(vaultState)
|
||||||
|
final vaultStateProvider = VaultStateProvider._();
|
||||||
|
|
||||||
|
final class VaultStateProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<VaultState?>,
|
||||||
|
VaultState?,
|
||||||
|
FutureOr<VaultState?>
|
||||||
|
>
|
||||||
|
with $FutureModifier<VaultState?>, $FutureProvider<VaultState?> {
|
||||||
|
VaultStateProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'vaultStateProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$vaultStateHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$FutureProviderElement<VaultState?> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $FutureProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<VaultState?> create(Ref ref) {
|
||||||
|
return vaultState(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$vaultStateHash() => r'1fd975a9661de1f62beef9eb1c7c439f377a8b88';
|
||||||
@@ -9,6 +9,7 @@ class Router extends RootStackRouter {
|
|||||||
AutoRoute(page: Bootstrap.page, path: '/bootstrap', initial: true),
|
AutoRoute(page: Bootstrap.page, path: '/bootstrap', initial: true),
|
||||||
AutoRoute(page: ServerInfoSetupRoute.page, path: '/server-info'),
|
AutoRoute(page: ServerInfoSetupRoute.page, path: '/server-info'),
|
||||||
AutoRoute(page: ServerConnectionRoute.page, path: '/server-connection'),
|
AutoRoute(page: ServerConnectionRoute.page, path: '/server-connection'),
|
||||||
|
AutoRoute(page: VaultSetupRoute.page, path: '/vault'),
|
||||||
|
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: DashboardRouter.page,
|
page: DashboardRouter.page,
|
||||||
|
|||||||
@@ -15,18 +15,19 @@ import 'package:arbiter/screens/dashboard/about.dart' as _i1;
|
|||||||
import 'package:arbiter/screens/dashboard/calc.dart' as _i3;
|
import 'package:arbiter/screens/dashboard/calc.dart' as _i3;
|
||||||
import 'package:arbiter/screens/server_connection.dart' as _i5;
|
import 'package:arbiter/screens/server_connection.dart' as _i5;
|
||||||
import 'package:arbiter/screens/server_info_setup.dart' as _i6;
|
import 'package:arbiter/screens/server_info_setup.dart' as _i6;
|
||||||
import 'package:auto_route/auto_route.dart' as _i7;
|
import 'package:arbiter/screens/vault_setup.dart' as _i7;
|
||||||
import 'package:flutter/material.dart' as _i8;
|
import 'package:auto_route/auto_route.dart' as _i8;
|
||||||
|
import 'package:flutter/material.dart' as _i9;
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [_i1.AboutScreen]
|
/// [_i1.AboutScreen]
|
||||||
class AboutRoute extends _i7.PageRouteInfo<void> {
|
class AboutRoute extends _i8.PageRouteInfo<void> {
|
||||||
const AboutRoute({List<_i7.PageRouteInfo>? children})
|
const AboutRoute({List<_i8.PageRouteInfo>? children})
|
||||||
: super(AboutRoute.name, initialChildren: children);
|
: super(AboutRoute.name, initialChildren: children);
|
||||||
|
|
||||||
static const String name = 'AboutRoute';
|
static const String name = 'AboutRoute';
|
||||||
|
|
||||||
static _i7.PageInfo page = _i7.PageInfo(
|
static _i8.PageInfo page = _i8.PageInfo(
|
||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
return const _i1.AboutScreen();
|
return const _i1.AboutScreen();
|
||||||
@@ -36,13 +37,13 @@ class AboutRoute extends _i7.PageRouteInfo<void> {
|
|||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [_i2.Bootstrap]
|
/// [_i2.Bootstrap]
|
||||||
class Bootstrap extends _i7.PageRouteInfo<void> {
|
class Bootstrap extends _i8.PageRouteInfo<void> {
|
||||||
const Bootstrap({List<_i7.PageRouteInfo>? children})
|
const Bootstrap({List<_i8.PageRouteInfo>? children})
|
||||||
: super(Bootstrap.name, initialChildren: children);
|
: super(Bootstrap.name, initialChildren: children);
|
||||||
|
|
||||||
static const String name = 'Bootstrap';
|
static const String name = 'Bootstrap';
|
||||||
|
|
||||||
static _i7.PageInfo page = _i7.PageInfo(
|
static _i8.PageInfo page = _i8.PageInfo(
|
||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
return const _i2.Bootstrap();
|
return const _i2.Bootstrap();
|
||||||
@@ -52,13 +53,13 @@ class Bootstrap extends _i7.PageRouteInfo<void> {
|
|||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [_i3.CalcScreen]
|
/// [_i3.CalcScreen]
|
||||||
class CalcRoute extends _i7.PageRouteInfo<void> {
|
class CalcRoute extends _i8.PageRouteInfo<void> {
|
||||||
const CalcRoute({List<_i7.PageRouteInfo>? children})
|
const CalcRoute({List<_i8.PageRouteInfo>? children})
|
||||||
: super(CalcRoute.name, initialChildren: children);
|
: super(CalcRoute.name, initialChildren: children);
|
||||||
|
|
||||||
static const String name = 'CalcRoute';
|
static const String name = 'CalcRoute';
|
||||||
|
|
||||||
static _i7.PageInfo page = _i7.PageInfo(
|
static _i8.PageInfo page = _i8.PageInfo(
|
||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
return const _i3.CalcScreen();
|
return const _i3.CalcScreen();
|
||||||
@@ -68,13 +69,13 @@ class CalcRoute extends _i7.PageRouteInfo<void> {
|
|||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [_i4.DashboardRouter]
|
/// [_i4.DashboardRouter]
|
||||||
class DashboardRouter extends _i7.PageRouteInfo<void> {
|
class DashboardRouter extends _i8.PageRouteInfo<void> {
|
||||||
const DashboardRouter({List<_i7.PageRouteInfo>? children})
|
const DashboardRouter({List<_i8.PageRouteInfo>? children})
|
||||||
: super(DashboardRouter.name, initialChildren: children);
|
: super(DashboardRouter.name, initialChildren: children);
|
||||||
|
|
||||||
static const String name = 'DashboardRouter';
|
static const String name = 'DashboardRouter';
|
||||||
|
|
||||||
static _i7.PageInfo page = _i7.PageInfo(
|
static _i8.PageInfo page = _i8.PageInfo(
|
||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
return const _i4.DashboardRouter();
|
return const _i4.DashboardRouter();
|
||||||
@@ -85,11 +86,11 @@ class DashboardRouter extends _i7.PageRouteInfo<void> {
|
|||||||
/// generated route for
|
/// generated route for
|
||||||
/// [_i5.ServerConnectionScreen]
|
/// [_i5.ServerConnectionScreen]
|
||||||
class ServerConnectionRoute
|
class ServerConnectionRoute
|
||||||
extends _i7.PageRouteInfo<ServerConnectionRouteArgs> {
|
extends _i8.PageRouteInfo<ServerConnectionRouteArgs> {
|
||||||
ServerConnectionRoute({
|
ServerConnectionRoute({
|
||||||
_i8.Key? key,
|
_i9.Key? key,
|
||||||
String? arbiterUrl,
|
String? arbiterUrl,
|
||||||
List<_i7.PageRouteInfo>? children,
|
List<_i8.PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
ServerConnectionRoute.name,
|
ServerConnectionRoute.name,
|
||||||
args: ServerConnectionRouteArgs(key: key, arbiterUrl: arbiterUrl),
|
args: ServerConnectionRouteArgs(key: key, arbiterUrl: arbiterUrl),
|
||||||
@@ -98,7 +99,7 @@ class ServerConnectionRoute
|
|||||||
|
|
||||||
static const String name = 'ServerConnectionRoute';
|
static const String name = 'ServerConnectionRoute';
|
||||||
|
|
||||||
static _i7.PageInfo page = _i7.PageInfo(
|
static _i8.PageInfo page = _i8.PageInfo(
|
||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
final args = data.argsAs<ServerConnectionRouteArgs>(
|
final args = data.argsAs<ServerConnectionRouteArgs>(
|
||||||
@@ -115,7 +116,7 @@ class ServerConnectionRoute
|
|||||||
class ServerConnectionRouteArgs {
|
class ServerConnectionRouteArgs {
|
||||||
const ServerConnectionRouteArgs({this.key, this.arbiterUrl});
|
const ServerConnectionRouteArgs({this.key, this.arbiterUrl});
|
||||||
|
|
||||||
final _i8.Key? key;
|
final _i9.Key? key;
|
||||||
|
|
||||||
final String? arbiterUrl;
|
final String? arbiterUrl;
|
||||||
|
|
||||||
@@ -137,16 +138,32 @@ class ServerConnectionRouteArgs {
|
|||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [_i6.ServerInfoSetupScreen]
|
/// [_i6.ServerInfoSetupScreen]
|
||||||
class ServerInfoSetupRoute extends _i7.PageRouteInfo<void> {
|
class ServerInfoSetupRoute extends _i8.PageRouteInfo<void> {
|
||||||
const ServerInfoSetupRoute({List<_i7.PageRouteInfo>? children})
|
const ServerInfoSetupRoute({List<_i8.PageRouteInfo>? children})
|
||||||
: super(ServerInfoSetupRoute.name, initialChildren: children);
|
: super(ServerInfoSetupRoute.name, initialChildren: children);
|
||||||
|
|
||||||
static const String name = 'ServerInfoSetupRoute';
|
static const String name = 'ServerInfoSetupRoute';
|
||||||
|
|
||||||
static _i7.PageInfo page = _i7.PageInfo(
|
static _i8.PageInfo page = _i8.PageInfo(
|
||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
return const _i6.ServerInfoSetupScreen();
|
return const _i6.ServerInfoSetupScreen();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [_i7.VaultSetupScreen]
|
||||||
|
class VaultSetupRoute extends _i8.PageRouteInfo<void> {
|
||||||
|
const VaultSetupRoute({List<_i8.PageRouteInfo>? children})
|
||||||
|
: super(VaultSetupRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'VaultSetupRoute';
|
||||||
|
|
||||||
|
static _i8.PageInfo page = _i8.PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const _i7.VaultSetupScreen();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class ServerConnectionScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (connectionState.value != null) {
|
if (connectionState.value != null) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.router.replace(const DashboardRouter());
|
context.router.replace(const VaultSetupRoute());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
408
useragent/lib/screens/vault_setup.dart
Normal file
408
useragent/lib/screens/vault_setup.dart
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
import 'package:arbiter/features/connection/connection.dart';
|
||||||
|
import 'package:arbiter/proto/user_agent.pbenum.dart';
|
||||||
|
import 'package:arbiter/providers/connection/connection_manager.dart';
|
||||||
|
import 'package:arbiter/providers/vault_state.dart';
|
||||||
|
import 'package:arbiter/router.gr.dart';
|
||||||
|
import 'package:arbiter/widgets/bottom_popup.dart';
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class VaultSetupScreen extends HookConsumerWidget {
|
||||||
|
const VaultSetupScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final vaultState = ref.watch(vaultStateProvider);
|
||||||
|
final bootstrapPasswordController = useTextEditingController();
|
||||||
|
final bootstrapConfirmController = useTextEditingController();
|
||||||
|
final unsealPasswordController = useTextEditingController();
|
||||||
|
final errorText = useState<String?>(null);
|
||||||
|
final isSubmitting = useState(false);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (vaultState.asData?.value == VaultState.VAULT_STATE_UNSEALED) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (context.mounted) {
|
||||||
|
context.router.replace(const DashboardRouter());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [context, vaultState.asData?.value]);
|
||||||
|
|
||||||
|
Future<void> refreshVaultState() async {
|
||||||
|
ref.invalidate(vaultStateProvider);
|
||||||
|
await ref.read(vaultStateProvider.future);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> submitBootstrap() async {
|
||||||
|
final password = bootstrapPasswordController.text;
|
||||||
|
final confirmation = bootstrapConfirmController.text;
|
||||||
|
|
||||||
|
if (password.isEmpty || confirmation.isEmpty) {
|
||||||
|
errorText.value = 'Enter the password twice.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password != confirmation) {
|
||||||
|
errorText.value = 'Passwords do not match.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final confirmed = await showBottomPopup<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (popupContext) {
|
||||||
|
return _WarningPopup(
|
||||||
|
title: 'Bootstrap vault?',
|
||||||
|
body:
|
||||||
|
'This password cannot be recovered. If you lose it, the vault cannot be unsealed.',
|
||||||
|
confirmLabel: 'Bootstrap',
|
||||||
|
onCancel: () => Navigator.of(popupContext).pop(false),
|
||||||
|
onConfirm: () => Navigator.of(popupContext).pop(true),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed != true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorText.value = null;
|
||||||
|
isSubmitting.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final connection = await ref.read(connectionManagerProvider.future);
|
||||||
|
if (connection == null) {
|
||||||
|
throw Exception('Not connected to the server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await bootstrapVault(connection, password);
|
||||||
|
switch (result) {
|
||||||
|
case BootstrapResult.BOOTSTRAP_RESULT_SUCCESS:
|
||||||
|
bootstrapPasswordController.clear();
|
||||||
|
bootstrapConfirmController.clear();
|
||||||
|
await refreshVaultState();
|
||||||
|
break;
|
||||||
|
case BootstrapResult.BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED:
|
||||||
|
errorText.value =
|
||||||
|
'The vault was already bootstrapped. Refreshing vault state.';
|
||||||
|
await refreshVaultState();
|
||||||
|
break;
|
||||||
|
case BootstrapResult.BOOTSTRAP_RESULT_INVALID_KEY:
|
||||||
|
case BootstrapResult.BOOTSTRAP_RESULT_UNSPECIFIED:
|
||||||
|
errorText.value = 'Failed to bootstrap the vault.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errorText.value = _formatVaultError(error);
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> submitUnseal() async {
|
||||||
|
final password = unsealPasswordController.text;
|
||||||
|
if (password.isEmpty) {
|
||||||
|
errorText.value = 'Enter the vault password.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorText.value = null;
|
||||||
|
isSubmitting.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final connection = await ref.read(connectionManagerProvider.future);
|
||||||
|
if (connection == null) {
|
||||||
|
throw Exception('Not connected to the server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await unsealVault(connection, password);
|
||||||
|
switch (result) {
|
||||||
|
case UnsealResult.UNSEAL_RESULT_SUCCESS:
|
||||||
|
unsealPasswordController.clear();
|
||||||
|
await refreshVaultState();
|
||||||
|
break;
|
||||||
|
case UnsealResult.UNSEAL_RESULT_INVALID_KEY:
|
||||||
|
errorText.value = 'Incorrect password.';
|
||||||
|
break;
|
||||||
|
case UnsealResult.UNSEAL_RESULT_UNBOOTSTRAPPED:
|
||||||
|
errorText.value =
|
||||||
|
'The vault is not bootstrapped yet. Refreshing vault state.';
|
||||||
|
await refreshVaultState();
|
||||||
|
break;
|
||||||
|
case UnsealResult.UNSEAL_RESULT_UNSPECIFIED:
|
||||||
|
errorText.value = 'Failed to unseal the vault.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errorText.value = _formatVaultError(error);
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final body = switch (vaultState) {
|
||||||
|
AsyncLoading() => const Center(child: CircularProgressIndicator()),
|
||||||
|
AsyncError(:final error) => _VaultCard(
|
||||||
|
title: 'Vault unavailable',
|
||||||
|
subtitle: _formatVaultError(error),
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () => ref.invalidate(vaultStateProvider),
|
||||||
|
child: const Text('Retry'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AsyncData(:final value) => switch (value) {
|
||||||
|
VaultState.VAULT_STATE_UNBOOTSTRAPPED => _VaultCard(
|
||||||
|
title: 'Create vault password',
|
||||||
|
subtitle:
|
||||||
|
'Choose the password that will be required to unseal this vault.',
|
||||||
|
child: _PasswordForm(
|
||||||
|
errorText: errorText.value,
|
||||||
|
isSubmitting: isSubmitting.value,
|
||||||
|
submitLabel: 'Bootstrap vault',
|
||||||
|
onSubmit: submitBootstrap,
|
||||||
|
fields: [
|
||||||
|
_PasswordFieldConfig(
|
||||||
|
controller: bootstrapPasswordController,
|
||||||
|
label: 'Password',
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
),
|
||||||
|
_PasswordFieldConfig(
|
||||||
|
controller: bootstrapConfirmController,
|
||||||
|
label: 'Confirm password',
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
onSubmitted: (_) => submitBootstrap(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
VaultState.VAULT_STATE_SEALED => _VaultCard(
|
||||||
|
title: 'Unseal vault',
|
||||||
|
subtitle: 'Enter the vault password to continue.',
|
||||||
|
child: _PasswordForm(
|
||||||
|
errorText: errorText.value,
|
||||||
|
isSubmitting: isSubmitting.value,
|
||||||
|
submitLabel: 'Unseal vault',
|
||||||
|
onSubmit: submitUnseal,
|
||||||
|
fields: [
|
||||||
|
_PasswordFieldConfig(
|
||||||
|
controller: unsealPasswordController,
|
||||||
|
label: 'Password',
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
onSubmitted: (_) => submitUnseal(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
VaultState.VAULT_STATE_UNSEALED => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
VaultState.VAULT_STATE_ERROR => _VaultCard(
|
||||||
|
title: 'Vault state unavailable',
|
||||||
|
subtitle: 'Unable to determine the current vault state.',
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () => ref.invalidate(vaultStateProvider),
|
||||||
|
child: const Text('Retry'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
VaultState.VAULT_STATE_UNSPECIFIED => _VaultCard(
|
||||||
|
title: 'Vault state unavailable',
|
||||||
|
subtitle: 'Unable to determine the current vault state.',
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () => ref.invalidate(vaultStateProvider),
|
||||||
|
child: const Text('Retry'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
null => _VaultCard(
|
||||||
|
title: 'Vault state unavailable',
|
||||||
|
subtitle: 'Unable to determine the current vault state.',
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () => ref.invalidate(vaultStateProvider),
|
||||||
|
child: const Text('Retry'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_ => _VaultCard(
|
||||||
|
title: 'Vault state unavailable',
|
||||||
|
subtitle: 'Unable to determine the current vault state.',
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () => ref.invalidate(vaultStateProvider),
|
||||||
|
child: const Text('Retry'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Vault Setup')),
|
||||||
|
body: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 3.h),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 520),
|
||||||
|
child: body,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VaultCard extends StatelessWidget {
|
||||||
|
const _VaultCard({
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(title, style: Theme.of(context).textTheme.headlineSmall),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(subtitle, style: Theme.of(context).textTheme.bodyMedium),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PasswordForm extends StatelessWidget {
|
||||||
|
const _PasswordForm({
|
||||||
|
required this.fields,
|
||||||
|
required this.errorText,
|
||||||
|
required this.isSubmitting,
|
||||||
|
required this.submitLabel,
|
||||||
|
required this.onSubmit,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<_PasswordFieldConfig> fields;
|
||||||
|
final String? errorText;
|
||||||
|
final bool isSubmitting;
|
||||||
|
final String submitLabel;
|
||||||
|
final Future<void> Function() onSubmit;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (final field in fields) ...[
|
||||||
|
TextField(
|
||||||
|
controller: field.controller,
|
||||||
|
obscureText: true,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autocorrect: false,
|
||||||
|
textInputAction: field.textInputAction,
|
||||||
|
onSubmitted: field.onSubmitted,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: field.label,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
if (errorText != null) ...[
|
||||||
|
Text(
|
||||||
|
errorText!,
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: isSubmitting ? null : () => onSubmit(),
|
||||||
|
child: Text(isSubmitting ? 'Working...' : submitLabel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PasswordFieldConfig {
|
||||||
|
const _PasswordFieldConfig({
|
||||||
|
required this.controller,
|
||||||
|
required this.label,
|
||||||
|
required this.textInputAction,
|
||||||
|
this.onSubmitted,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String label;
|
||||||
|
final TextInputAction textInputAction;
|
||||||
|
final ValueChanged<String>? onSubmitted;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WarningPopup extends StatelessWidget {
|
||||||
|
const _WarningPopup({
|
||||||
|
required this.title,
|
||||||
|
required this.body,
|
||||||
|
required this.confirmLabel,
|
||||||
|
required this.onCancel,
|
||||||
|
required this.onConfirm,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String body;
|
||||||
|
final String confirmLabel;
|
||||||
|
final VoidCallback onCancel;
|
||||||
|
final VoidCallback onConfirm;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(title, style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(body, style: Theme.of(context).textTheme.bodyMedium),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(onPressed: onCancel, child: const Text('Cancel')),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
FilledButton(onPressed: onConfirm, child: Text(confirmLabel)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatVaultError(Object error) {
|
||||||
|
final message = error.toString();
|
||||||
|
|
||||||
|
if (message.contains('GrpcError')) {
|
||||||
|
return 'The server rejected the vault request. Check the password and try again.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.replaceFirst('Exception: ', '');
|
||||||
|
}
|
||||||
92
useragent/lib/widgets/bottom_popup.dart
Normal file
92
useragent/lib/widgets/bottom_popup.dart
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
Future<T?> showBottomPopup<T>({
|
||||||
|
required BuildContext context,
|
||||||
|
required WidgetBuilder builder,
|
||||||
|
bool barrierDismissible = true,
|
||||||
|
}) {
|
||||||
|
return showGeneralDialog<T>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: barrierDismissible,
|
||||||
|
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||||
|
barrierColor: Colors.transparent,
|
||||||
|
transitionDuration: const Duration(milliseconds: 320),
|
||||||
|
pageBuilder: (dialogContext, animation, secondaryAnimation) {
|
||||||
|
return _BottomPopupRoute(
|
||||||
|
animation: animation,
|
||||||
|
builder: builder,
|
||||||
|
barrierDismissible: barrierDismissible,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BottomPopupRoute extends StatelessWidget {
|
||||||
|
const _BottomPopupRoute({
|
||||||
|
required this.animation,
|
||||||
|
required this.builder,
|
||||||
|
required this.barrierDismissible,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Animation<double> animation;
|
||||||
|
final WidgetBuilder builder;
|
||||||
|
final bool barrierDismissible;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final barrierAnimation = CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: const Interval(0, 0.3125, curve: Curves.easeOut),
|
||||||
|
);
|
||||||
|
final popupAnimation = CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: const Interval(0.3125, 1, curve: Curves.easeOutCubic),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: barrierDismissible ? () => Navigator.of(context).pop() : null,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: barrierAnimation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return ColoredBox(
|
||||||
|
color: Colors.black.withValues(
|
||||||
|
alpha: 0.35 * barrierAnimation.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SafeArea(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: popupAnimation,
|
||||||
|
child: SlideTransition(
|
||||||
|
position:
|
||||||
|
Tween<Offset>(
|
||||||
|
begin: const Offset(0, 0.08),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(popupAnimation),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {},
|
||||||
|
child: Builder(builder: builder),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user