diff --git a/mise.toml b/mise.toml index 7110df7..ea6265d 100644 --- a/mise.toml +++ b/mise.toml @@ -16,5 +16,5 @@ sources = ['protobufs/*.proto'] outputs = ['useragent/lib/proto/*'] run = ''' dart pub global activate protoc_plugin && \ -protoc --dart_out=useragent/lib/proto --proto_path=protobufs/ protobufs/*.proto +protoc --dart_out=grpc:useragent/lib/proto --proto_path=protobufs/ protobufs/*.proto ''' \ No newline at end of file diff --git a/useragent/lib/features/arbiter_url.dart b/useragent/lib/features/connection/arbiter_url.dart similarity index 100% rename from useragent/lib/features/arbiter_url.dart rename to useragent/lib/features/connection/arbiter_url.dart diff --git a/useragent/lib/features/connection/connection.dart b/useragent/lib/features/connection/connection.dart index 944711a..4afc46e 100644 --- a/useragent/lib/features/connection/connection.dart +++ b/useragent/lib/features/connection/connection.dart @@ -1,3 +1,132 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:arbiter/features/connection/server_info_storage.dart'; +import 'package:arbiter/features/identity/pk_manager.dart'; +import 'package:arbiter/proto/arbiter.pbgrpc.dart'; +import 'package:arbiter/proto/user_agent.pb.dart'; +import 'package:grpc/grpc.dart'; +import 'package:mtcore/markettakers.dart'; + +class Connection { + final ClientChannel channel; + final StreamController _tx; + final StreamIterator _rx; + + Connection({ + required this.channel, + required StreamController tx, + required ResponseStream rx, + }) : _tx = tx, + _rx = StreamIterator(rx); + + Future send(UserAgentRequest request) async { + _tx.add(request); + } + + Future receive() async { + await _rx.moveNext(); + return _rx.current; + + } + + Future close() async { + await _tx.close(); + await channel.shutdown(); + } +} + +Future _connect(StoredServerInfo serverInfo) async { + final channel = ClientChannel( + serverInfo.address, + port: serverInfo.port, + options: ChannelOptions( + connectTimeout: const Duration(seconds: 10), + credentials: ChannelCredentials.secure( + onBadCertificate: (cert, host) { + return true; + }, + ), + ), + ); + + final client = ArbiterServiceClient(channel); + final tx = StreamController(); + + final rx = client.userAgent(tx.stream); + + return Connection(channel: channel, tx: tx, rx: rx); +} + +List formatChallenge(AuthChallenge challenge, List pubkey) { + final encodedPubkey = base64Encode(pubkey); + final payload = "${challenge.nonce}:$encodedPubkey"; + return utf8.encode(payload); +} + +Future connectAndAuthorize( + StoredServerInfo serverInfo, + KeyHandle key, { + String? bootstrapToken, +}) async { + try { + final connection = await _connect(serverInfo); + talker.info( + 'Connected to server at ${serverInfo.address}:${serverInfo.port}', + ); + final pubkey = await key.getPublicKey(); + + final req = AuthChallengeRequest( + pubkey: pubkey, + bootstrapToken: bootstrapToken, + keyType: switch (key.alg) { + KeyAlgorithm.rsa => KeyType.KEY_TYPE_RSA, + KeyAlgorithm.ecdsa => KeyType.KEY_TYPE_ECDSA_SECP256K1, + KeyAlgorithm.ed25519 => KeyType.KEY_TYPE_ED25519, + }, + ); + await connection.send(UserAgentRequest(authChallengeRequest: req)); + talker.info( + "Sent auth challenge request with pubkey ${base64Encode(pubkey)}", + ); -class Connection {} \ No newline at end of file + final response = await connection.receive(); + + talker.info( + 'Received response from server, checking for auth challenge...', + ); + + if (!response.hasAuthChallenge()) { + throw Exception( + 'Expected AuthChallengeResponse, got ${response.whichPayload()}', + ); + } + + final challenge = formatChallenge(response.authChallenge, pubkey); + talker.info( + 'Received auth challenge, signing with key ${base64Encode(pubkey)}', + ); + + final signature = await key.sign(challenge); + + final solutionReq = AuthChallengeSolution(signature: signature); + await connection.send(UserAgentRequest(authChallengeSolution: solutionReq)); + + talker.info('Sent auth challenge solution, waiting for server response...'); + + final solutionResponse = await connection.receive(); + + if (!solutionResponse.hasAuthOk()) { + throw Exception( + 'Expected AuthChallengeSolutionResponse, got ${solutionResponse.whichPayload()}', + ); + } + + talker.info('Authentication successful, connection established'); + + return connection; + } catch (e) { + throw Exception('Failed to connect to server: $e'); + } +} diff --git a/useragent/lib/features/server_info_storage.dart b/useragent/lib/features/connection/server_info_storage.dart similarity index 76% rename from useragent/lib/features/server_info_storage.dart rename to useragent/lib/features/connection/server_info_storage.dart index 704939e..39bec59 100644 --- a/useragent/lib/features/server_info_storage.dart +++ b/useragent/lib/features/connection/server_info_storage.dart @@ -1,7 +1,11 @@ import 'dart:convert'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:json_annotation/json_annotation.dart'; +part 'server_info_storage.g.dart'; + +@JsonSerializable() class StoredServerInfo { const StoredServerInfo({ required this.address, @@ -13,19 +17,9 @@ class StoredServerInfo { final int port; final String caCertFingerprint; - Map toJson() => { - 'address': address, - 'port': port, - 'caCertFingerprint': caCertFingerprint, - }; - - factory StoredServerInfo.fromJson(Map json) { - return StoredServerInfo( - address: json['address'] as String, - port: json['port'] as int, - caCertFingerprint: json['caCertFingerprint'] as String, - ); - } + factory StoredServerInfo.fromJson(Map json) => _$StoredServerInfoFromJson(json); + Map toJson() => _$StoredServerInfoToJson(this); + } abstract class ServerInfoStorage { diff --git a/useragent/lib/features/connection/server_info_storage.g.dart b/useragent/lib/features/connection/server_info_storage.g.dart new file mode 100644 index 0000000..fcd0018 --- /dev/null +++ b/useragent/lib/features/connection/server_info_storage.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'server_info_storage.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +StoredServerInfo _$StoredServerInfoFromJson(Map json) => + StoredServerInfo( + address: json['address'] as String, + port: (json['port'] as num).toInt(), + caCertFingerprint: json['caCertFingerprint'] as String, + ); + +Map _$StoredServerInfoToJson(StoredServerInfo instance) => + { + 'address': instance.address, + 'port': instance.port, + 'caCertFingerprint': instance.caCertFingerprint, + }; diff --git a/useragent/lib/features/pk_manager.dart b/useragent/lib/features/identity/pk_manager.dart similarity index 100% rename from useragent/lib/features/pk_manager.dart rename to useragent/lib/features/identity/pk_manager.dart diff --git a/useragent/lib/features/simple_ed25519.dart b/useragent/lib/features/identity/simple_ed25519.dart similarity index 96% rename from useragent/lib/features/simple_ed25519.dart rename to useragent/lib/features/identity/simple_ed25519.dart index f5e2183..01ee24d 100644 --- a/useragent/lib/features/simple_ed25519.dart +++ b/useragent/lib/features/identity/simple_ed25519.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:cryptography/cryptography.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:arbiter/features/pk_manager.dart'; +import 'package:arbiter/features/identity/pk_manager.dart'; final storage = FlutterSecureStorage( aOptions: AndroidOptions.biometric( @@ -16,7 +16,6 @@ final storage = FlutterSecureStorage( synchronizable: false, accessControlFlags: [ AccessControlFlag.userPresence, - AccessControlFlag.privateKeyUsage, ], usesDataProtectionKeychain: true, ), diff --git a/useragent/lib/proto/arbiter.pb.dart b/useragent/lib/proto/arbiter.pb.dart index a0f96d7..58ca420 100644 --- a/useragent/lib/proto/arbiter.pb.dart +++ b/useragent/lib/proto/arbiter.pb.dart @@ -10,14 +10,10 @@ // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_relative_imports -import 'dart:async' as $async; import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; -import 'client.pb.dart' as $0; -import 'user_agent.pb.dart' as $1; - export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; class ServerInfo extends $pb.GeneratedMessage { @@ -86,21 +82,6 @@ class ServerInfo extends $pb.GeneratedMessage { void clearCertPublicKey() => $_clearField(2); } -class ArbiterServiceApi { - final $pb.RpcClient _client; - - ArbiterServiceApi(this._client); - - $async.Future<$0.ClientResponse> client( - $pb.ClientContext? ctx, $0.ClientRequest request) => - _client.invoke<$0.ClientResponse>( - ctx, 'ArbiterService', 'Client', request, $0.ClientResponse()); - $async.Future<$1.UserAgentResponse> userAgent( - $pb.ClientContext? ctx, $1.UserAgentRequest request) => - _client.invoke<$1.UserAgentResponse>( - ctx, 'ArbiterService', 'UserAgent', request, $1.UserAgentResponse()); -} - const $core.bool _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); const $core.bool _omitMessageNames = diff --git a/useragent/lib/proto/arbiter.pbgrpc.dart b/useragent/lib/proto/arbiter.pbgrpc.dart new file mode 100644 index 0000000..4cc66b9 --- /dev/null +++ b/useragent/lib/proto/arbiter.pbgrpc.dart @@ -0,0 +1,90 @@ +// This is a generated file - do not edit. +// +// Generated from arbiter.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports + +import 'dart:async' as $async; +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'client.pb.dart' as $0; +import 'user_agent.pb.dart' as $1; + +export 'arbiter.pb.dart'; + +@$pb.GrpcServiceName('arbiter.ArbiterService') +class ArbiterServiceClient extends $grpc.Client { + /// The hostname for this service. + static const $core.String defaultHost = ''; + + /// OAuth scopes needed for the client. + static const $core.List<$core.String> oauthScopes = [ + '', + ]; + + ArbiterServiceClient(super.channel, {super.options, super.interceptors}); + + $grpc.ResponseStream<$0.ClientResponse> client( + $async.Stream<$0.ClientRequest> request, { + $grpc.CallOptions? options, + }) { + return $createStreamingCall(_$client, request, options: options); + } + + $grpc.ResponseStream<$1.UserAgentResponse> userAgent( + $async.Stream<$1.UserAgentRequest> request, { + $grpc.CallOptions? options, + }) { + return $createStreamingCall(_$userAgent, request, options: options); + } + + // method descriptors + + static final _$client = + $grpc.ClientMethod<$0.ClientRequest, $0.ClientResponse>( + '/arbiter.ArbiterService/Client', + ($0.ClientRequest value) => value.writeToBuffer(), + $0.ClientResponse.fromBuffer); + static final _$userAgent = + $grpc.ClientMethod<$1.UserAgentRequest, $1.UserAgentResponse>( + '/arbiter.ArbiterService/UserAgent', + ($1.UserAgentRequest value) => value.writeToBuffer(), + $1.UserAgentResponse.fromBuffer); +} + +@$pb.GrpcServiceName('arbiter.ArbiterService') +abstract class ArbiterServiceBase extends $grpc.Service { + $core.String get $name => 'arbiter.ArbiterService'; + + ArbiterServiceBase() { + $addMethod($grpc.ServiceMethod<$0.ClientRequest, $0.ClientResponse>( + 'Client', + client, + true, + true, + ($core.List<$core.int> value) => $0.ClientRequest.fromBuffer(value), + ($0.ClientResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$1.UserAgentRequest, $1.UserAgentResponse>( + 'UserAgent', + userAgent, + true, + true, + ($core.List<$core.int> value) => $1.UserAgentRequest.fromBuffer(value), + ($1.UserAgentResponse value) => value.writeToBuffer())); + } + + $async.Stream<$0.ClientResponse> client( + $grpc.ServiceCall call, $async.Stream<$0.ClientRequest> request); + + $async.Stream<$1.UserAgentResponse> userAgent( + $grpc.ServiceCall call, $async.Stream<$1.UserAgentRequest> request); +} diff --git a/useragent/lib/proto/arbiter.pbjson.dart b/useragent/lib/proto/arbiter.pbjson.dart index 3300005..d93bf39 100644 --- a/useragent/lib/proto/arbiter.pbjson.dart +++ b/useragent/lib/proto/arbiter.pbjson.dart @@ -15,15 +15,6 @@ import 'dart:convert' as $convert; import 'dart:core' as $core; import 'dart:typed_data' as $typed_data; -import 'package:protobuf/well_known_types/google/protobuf/empty.pbjson.dart' - as $3; -import 'package:protobuf/well_known_types/google/protobuf/timestamp.pbjson.dart' - as $4; - -import 'client.pbjson.dart' as $0; -import 'evm.pbjson.dart' as $2; -import 'user_agent.pbjson.dart' as $1; - @$core.Deprecated('Use serverInfoDescriptor instead') const ServerInfo$json = { '1': 'ServerInfo', @@ -37,88 +28,3 @@ const ServerInfo$json = { final $typed_data.Uint8List serverInfoDescriptor = $convert.base64Decode( 'CgpTZXJ2ZXJJbmZvEhgKB3ZlcnNpb24YASABKAlSB3ZlcnNpb24SJgoPY2VydF9wdWJsaWNfa2' 'V5GAIgASgMUg1jZXJ0UHVibGljS2V5'); - -const $core.Map<$core.String, $core.dynamic> ArbiterServiceBase$json = { - '1': 'ArbiterService', - '2': [ - { - '1': 'Client', - '2': '.arbiter.client.ClientRequest', - '3': '.arbiter.client.ClientResponse', - '5': true, - '6': true - }, - { - '1': 'UserAgent', - '2': '.arbiter.user_agent.UserAgentRequest', - '3': '.arbiter.user_agent.UserAgentResponse', - '5': true, - '6': true - }, - ], -}; - -@$core.Deprecated('Use arbiterServiceDescriptor instead') -const $core.Map<$core.String, $core.Map<$core.String, $core.dynamic>> - ArbiterServiceBase$messageJson = { - '.arbiter.client.ClientRequest': $0.ClientRequest$json, - '.arbiter.client.AuthChallengeRequest': $0.AuthChallengeRequest$json, - '.arbiter.client.AuthChallengeSolution': $0.AuthChallengeSolution$json, - '.arbiter.client.ClientResponse': $0.ClientResponse$json, - '.arbiter.client.AuthChallenge': $0.AuthChallenge$json, - '.arbiter.client.AuthOk': $0.AuthOk$json, - '.arbiter.evm.EvmSignTransactionResponse': $2.EvmSignTransactionResponse$json, - '.arbiter.evm.TransactionEvalError': $2.TransactionEvalError$json, - '.google.protobuf.Empty': $3.Empty$json, - '.arbiter.evm.NoMatchingGrantError': $2.NoMatchingGrantError$json, - '.arbiter.evm.SpecificMeaning': $2.SpecificMeaning$json, - '.arbiter.evm.EtherTransferMeaning': $2.EtherTransferMeaning$json, - '.arbiter.evm.TokenTransferMeaning': $2.TokenTransferMeaning$json, - '.arbiter.evm.TokenInfo': $2.TokenInfo$json, - '.arbiter.evm.PolicyViolationsError': $2.PolicyViolationsError$json, - '.arbiter.evm.EvalViolation': $2.EvalViolation$json, - '.arbiter.evm.GasLimitExceededViolation': $2.GasLimitExceededViolation$json, - '.arbiter.evm.EvmAnalyzeTransactionResponse': - $2.EvmAnalyzeTransactionResponse$json, - '.arbiter.client.ClientConnectError': $0.ClientConnectError$json, - '.arbiter.user_agent.UserAgentRequest': $1.UserAgentRequest$json, - '.arbiter.user_agent.AuthChallengeRequest': $1.AuthChallengeRequest$json, - '.arbiter.user_agent.AuthChallengeSolution': $1.AuthChallengeSolution$json, - '.arbiter.user_agent.UnsealStart': $1.UnsealStart$json, - '.arbiter.user_agent.UnsealEncryptedKey': $1.UnsealEncryptedKey$json, - '.arbiter.evm.EvmGrantCreateRequest': $2.EvmGrantCreateRequest$json, - '.arbiter.evm.SharedSettings': $2.SharedSettings$json, - '.google.protobuf.Timestamp': $4.Timestamp$json, - '.arbiter.evm.TransactionRateLimit': $2.TransactionRateLimit$json, - '.arbiter.evm.SpecificGrant': $2.SpecificGrant$json, - '.arbiter.evm.EtherTransferSettings': $2.EtherTransferSettings$json, - '.arbiter.evm.VolumeRateLimit': $2.VolumeRateLimit$json, - '.arbiter.evm.TokenTransferSettings': $2.TokenTransferSettings$json, - '.arbiter.evm.EvmGrantDeleteRequest': $2.EvmGrantDeleteRequest$json, - '.arbiter.evm.EvmGrantListRequest': $2.EvmGrantListRequest$json, - '.arbiter.user_agent.ClientConnectionResponse': - $1.ClientConnectionResponse$json, - '.arbiter.user_agent.UserAgentResponse': $1.UserAgentResponse$json, - '.arbiter.user_agent.AuthChallenge': $1.AuthChallenge$json, - '.arbiter.user_agent.AuthOk': $1.AuthOk$json, - '.arbiter.user_agent.UnsealStartResponse': $1.UnsealStartResponse$json, - '.arbiter.evm.WalletCreateResponse': $2.WalletCreateResponse$json, - '.arbiter.evm.WalletEntry': $2.WalletEntry$json, - '.arbiter.evm.WalletListResponse': $2.WalletListResponse$json, - '.arbiter.evm.WalletList': $2.WalletList$json, - '.arbiter.evm.EvmGrantCreateResponse': $2.EvmGrantCreateResponse$json, - '.arbiter.evm.EvmGrantDeleteResponse': $2.EvmGrantDeleteResponse$json, - '.arbiter.evm.EvmGrantListResponse': $2.EvmGrantListResponse$json, - '.arbiter.evm.EvmGrantList': $2.EvmGrantList$json, - '.arbiter.evm.GrantEntry': $2.GrantEntry$json, - '.arbiter.user_agent.ClientConnectionRequest': - $1.ClientConnectionRequest$json, - '.arbiter.user_agent.ClientConnectionCancel': $1.ClientConnectionCancel$json, -}; - -/// Descriptor for `ArbiterService`. Decode as a `google.protobuf.ServiceDescriptorProto`. -final $typed_data.Uint8List arbiterServiceDescriptor = $convert.base64Decode( - 'Cg5BcmJpdGVyU2VydmljZRJLCgZDbGllbnQSHS5hcmJpdGVyLmNsaWVudC5DbGllbnRSZXF1ZX' - 'N0Gh4uYXJiaXRlci5jbGllbnQuQ2xpZW50UmVzcG9uc2UoATABElwKCVVzZXJBZ2VudBIkLmFy' - 'Yml0ZXIudXNlcl9hZ2VudC5Vc2VyQWdlbnRSZXF1ZXN0GiUuYXJiaXRlci51c2VyX2FnZW50Ll' - 'VzZXJBZ2VudFJlc3BvbnNlKAEwAQ=='); diff --git a/useragent/lib/proto/arbiter.pbserver.dart b/useragent/lib/proto/arbiter.pbserver.dart deleted file mode 100644 index 59ed28f..0000000 --- a/useragent/lib/proto/arbiter.pbserver.dart +++ /dev/null @@ -1,56 +0,0 @@ -// This is a generated file - do not edit. -// -// Generated from arbiter.proto. - -// @dart = 3.3 - -// ignore_for_file: annotate_overrides, camel_case_types, comment_references -// ignore_for_file: constant_identifier_names -// ignore_for_file: curly_braces_in_flow_control_structures -// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes -// ignore_for_file: non_constant_identifier_names, prefer_relative_imports - -import 'dart:async' as $async; -import 'dart:core' as $core; - -import 'package:protobuf/protobuf.dart' as $pb; - -import 'arbiter.pbjson.dart'; -import 'client.pb.dart' as $0; -import 'user_agent.pb.dart' as $1; - -export 'arbiter.pb.dart'; - -abstract class ArbiterServiceBase extends $pb.GeneratedService { - $async.Future<$0.ClientResponse> client( - $pb.ServerContext ctx, $0.ClientRequest request); - $async.Future<$1.UserAgentResponse> userAgent( - $pb.ServerContext ctx, $1.UserAgentRequest request); - - $pb.GeneratedMessage createRequest($core.String methodName) { - switch (methodName) { - case 'Client': - return $0.ClientRequest(); - case 'UserAgent': - return $1.UserAgentRequest(); - default: - throw $core.ArgumentError('Unknown method: $methodName'); - } - } - - $async.Future<$pb.GeneratedMessage> handleCall($pb.ServerContext ctx, - $core.String methodName, $pb.GeneratedMessage request) { - switch (methodName) { - case 'Client': - return client(ctx, request as $0.ClientRequest); - case 'UserAgent': - return userAgent(ctx, request as $1.UserAgentRequest); - default: - throw $core.ArgumentError('Unknown method: $methodName'); - } - } - - $core.Map<$core.String, $core.dynamic> get $json => ArbiterServiceBase$json; - $core.Map<$core.String, $core.Map<$core.String, $core.dynamic>> - get $messageJson => ArbiterServiceBase$messageJson; -} diff --git a/useragent/lib/proto/user_agent.pb.dart b/useragent/lib/proto/user_agent.pb.dart index 8ea3f4f..f73a61e 100644 --- a/useragent/lib/proto/user_agent.pb.dart +++ b/useragent/lib/proto/user_agent.pb.dart @@ -26,10 +26,12 @@ class AuthChallengeRequest extends $pb.GeneratedMessage { factory AuthChallengeRequest({ $core.List<$core.int>? pubkey, $core.String? bootstrapToken, + KeyType? keyType, }) { final result = create(); if (pubkey != null) result.pubkey = pubkey; if (bootstrapToken != null) result.bootstrapToken = bootstrapToken; + if (keyType != null) result.keyType = keyType; return result; } @@ -50,6 +52,8 @@ class AuthChallengeRequest extends $pb.GeneratedMessage { ..a<$core.List<$core.int>>( 1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY) ..aOS(2, _omitFieldNames ? '' : 'bootstrapToken') + ..aE(3, _omitFieldNames ? '' : 'keyType', + enumValues: KeyType.values) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -88,6 +92,15 @@ class AuthChallengeRequest extends $pb.GeneratedMessage { $core.bool hasBootstrapToken() => $_has(1); @$pb.TagNumber(2) void clearBootstrapToken() => $_clearField(2); + + @$pb.TagNumber(3) + KeyType get keyType => $_getN(2); + @$pb.TagNumber(3) + set keyType(KeyType value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasKeyType() => $_has(2); + @$pb.TagNumber(3) + void clearKeyType() => $_clearField(3); } class AuthChallenge extends $pb.GeneratedMessage { diff --git a/useragent/lib/proto/user_agent.pbenum.dart b/useragent/lib/proto/user_agent.pbenum.dart index b2b6484..9ceaae4 100644 --- a/useragent/lib/proto/user_agent.pbenum.dart +++ b/useragent/lib/proto/user_agent.pbenum.dart @@ -14,6 +14,31 @@ import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; +class KeyType extends $pb.ProtobufEnum { + static const KeyType KEY_TYPE_UNSPECIFIED = + KeyType._(0, _omitEnumNames ? '' : 'KEY_TYPE_UNSPECIFIED'); + static const KeyType KEY_TYPE_ED25519 = + KeyType._(1, _omitEnumNames ? '' : 'KEY_TYPE_ED25519'); + static const KeyType KEY_TYPE_ECDSA_SECP256K1 = + KeyType._(2, _omitEnumNames ? '' : 'KEY_TYPE_ECDSA_SECP256K1'); + static const KeyType KEY_TYPE_RSA = + KeyType._(3, _omitEnumNames ? '' : 'KEY_TYPE_RSA'); + + static const $core.List values = [ + KEY_TYPE_UNSPECIFIED, + KEY_TYPE_ED25519, + KEY_TYPE_ECDSA_SECP256K1, + KEY_TYPE_RSA, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 3); + static KeyType? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const KeyType._(super.value, super.name); +} + class UnsealResult extends $pb.ProtobufEnum { static const UnsealResult UNSEAL_RESULT_UNSPECIFIED = UnsealResult._(0, _omitEnumNames ? '' : 'UNSEAL_RESULT_UNSPECIFIED'); diff --git a/useragent/lib/proto/user_agent.pbjson.dart b/useragent/lib/proto/user_agent.pbjson.dart index 843ab59..e9a48c6 100644 --- a/useragent/lib/proto/user_agent.pbjson.dart +++ b/useragent/lib/proto/user_agent.pbjson.dart @@ -15,6 +15,22 @@ import 'dart:convert' as $convert; import 'dart:core' as $core; import 'dart:typed_data' as $typed_data; +@$core.Deprecated('Use keyTypeDescriptor instead') +const KeyType$json = { + '1': 'KeyType', + '2': [ + {'1': 'KEY_TYPE_UNSPECIFIED', '2': 0}, + {'1': 'KEY_TYPE_ED25519', '2': 1}, + {'1': 'KEY_TYPE_ECDSA_SECP256K1', '2': 2}, + {'1': 'KEY_TYPE_RSA', '2': 3}, + ], +}; + +/// Descriptor for `KeyType`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List keyTypeDescriptor = $convert.base64Decode( + 'CgdLZXlUeXBlEhgKFEtFWV9UWVBFX1VOU1BFQ0lGSUVEEAASFAoQS0VZX1RZUEVfRUQyNTUxOR' + 'ABEhwKGEtFWV9UWVBFX0VDRFNBX1NFQ1AyNTZLMRACEhAKDEtFWV9UWVBFX1JTQRAD'); + @$core.Deprecated('Use unsealResultDescriptor instead') const UnsealResult$json = { '1': 'UnsealResult', @@ -64,6 +80,14 @@ const AuthChallengeRequest$json = { '10': 'bootstrapToken', '17': true }, + { + '1': 'key_type', + '3': 3, + '4': 1, + '5': 14, + '6': '.arbiter.user_agent.KeyType', + '10': 'keyType' + }, ], '8': [ {'1': '_bootstrap_token'}, @@ -73,8 +97,9 @@ const AuthChallengeRequest$json = { /// Descriptor for `AuthChallengeRequest`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List authChallengeRequestDescriptor = $convert.base64Decode( 'ChRBdXRoQ2hhbGxlbmdlUmVxdWVzdBIWCgZwdWJrZXkYASABKAxSBnB1YmtleRIsCg9ib290c3' - 'RyYXBfdG9rZW4YAiABKAlIAFIOYm9vdHN0cmFwVG9rZW6IAQFCEgoQX2Jvb3RzdHJhcF90b2tl' - 'bg=='); + 'RyYXBfdG9rZW4YAiABKAlIAFIOYm9vdHN0cmFwVG9rZW6IAQESNgoIa2V5X3R5cGUYAyABKA4y' + 'Gy5hcmJpdGVyLnVzZXJfYWdlbnQuS2V5VHlwZVIHa2V5VHlwZUISChBfYm9vdHN0cmFwX3Rva2' + 'Vu'); @$core.Deprecated('Use authChallengeDescriptor instead') const AuthChallenge$json = { diff --git a/useragent/lib/providers/connection/bootstrap_token.dart b/useragent/lib/providers/connection/bootstrap_token.dart new file mode 100644 index 0000000..1062f31 --- /dev/null +++ b/useragent/lib/providers/connection/bootstrap_token.dart @@ -0,0 +1,22 @@ + + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'bootstrap_token.g.dart'; + +@Riverpod(keepAlive: true) +class BootstrapToken extends _$BootstrapToken { + String? build() { + return null; + } + + void set(String token) { + state = token; + } + + String? take() { + final token = state; + state = null; + return token; + } +} \ No newline at end of file diff --git a/useragent/lib/providers/connection/bootstrap_token.g.dart b/useragent/lib/providers/connection/bootstrap_token.g.dart new file mode 100644 index 0000000..4477998 --- /dev/null +++ b/useragent/lib/providers/connection/bootstrap_token.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'bootstrap_token.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(BootstrapToken) +final bootstrapTokenProvider = BootstrapTokenProvider._(); + +final class BootstrapTokenProvider + extends $NotifierProvider { + BootstrapTokenProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'bootstrapTokenProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$bootstrapTokenHash(); + + @$internal + @override + BootstrapToken create() => BootstrapToken(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(String? value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$bootstrapTokenHash() => r'a59e679ab0561ed2ab4148660499891571d439db'; + +abstract class _$BootstrapToken extends $Notifier { + String? build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + String?, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/useragent/lib/providers/connection/connection_manager.dart b/useragent/lib/providers/connection/connection_manager.dart new file mode 100644 index 0000000..c1e5266 --- /dev/null +++ b/useragent/lib/providers/connection/connection_manager.dart @@ -0,0 +1,39 @@ +import 'package:arbiter/features/connection/connection.dart'; +import 'package:arbiter/providers/connection/bootstrap_token.dart'; +import 'package:arbiter/providers/key.dart'; +import 'package:arbiter/providers/server_info.dart'; +import 'package:mtcore/markettakers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'connection_manager.g.dart'; + +@Riverpod(keepAlive: true) +class ConnectionManager extends _$ConnectionManager { + @override + Future build() async { + final serverInfo = await ref.watch(serverInfoProvider.future); + final key = await ref.watch(keyProvider.future); + final token = ref.watch(bootstrapTokenProvider); + + if (serverInfo == null || key == null) { + return null; + } + final Connection connection; + try { + connection = await connectAndAuthorize(serverInfo, key, bootstrapToken: token); + } catch (e) { + talker.handle(e); + rethrow; + } + + + ref.onDispose(() { + final connection = state.asData?.value; + if (connection != null) { + connection.close(); + } + }); + + return connection; + } +} diff --git a/useragent/lib/providers/connection/connection_manager.g.dart b/useragent/lib/providers/connection/connection_manager.g.dart new file mode 100644 index 0000000..3646316 --- /dev/null +++ b/useragent/lib/providers/connection/connection_manager.g.dart @@ -0,0 +1,54 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'connection_manager.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ConnectionManager) +final connectionManagerProvider = ConnectionManagerProvider._(); + +final class ConnectionManagerProvider + extends $AsyncNotifierProvider { + ConnectionManagerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'connectionManagerProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$connectionManagerHash(); + + @$internal + @override + ConnectionManager create() => ConnectionManager(); +} + +String _$connectionManagerHash() => r'8923346dff75a9a06127c71a0a39ca65d9733d8c'; + +abstract class _$ConnectionManager extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, Connection?>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, Connection?>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/useragent/lib/providers/key.dart b/useragent/lib/providers/key.dart index 196fc45..247a906 100644 --- a/useragent/lib/providers/key.dart +++ b/useragent/lib/providers/key.dart @@ -1,7 +1,7 @@ import 'package:mtcore/markettakers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:arbiter/features/pk_manager.dart'; -import 'package:arbiter/features/simple_ed25519.dart'; +import 'package:arbiter/features/identity/pk_manager.dart'; +import 'package:arbiter/features/identity/simple_ed25519.dart'; part 'key.g.dart'; diff --git a/useragent/lib/providers/server_info.dart b/useragent/lib/providers/server_info.dart index 651143e..27bee2e 100644 --- a/useragent/lib/providers/server_info.dart +++ b/useragent/lib/providers/server_info.dart @@ -1,4 +1,4 @@ -import 'package:arbiter/features/server_info_storage.dart'; +import 'package:arbiter/features/connection/server_info_storage.dart'; import 'package:cryptography/cryptography.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/useragent/lib/router.dart b/useragent/lib/router.dart index 5462fbd..b809a4b 100644 --- a/useragent/lib/router.dart +++ b/useragent/lib/router.dart @@ -8,6 +8,7 @@ class Router extends RootStackRouter { List get routes => [ AutoRoute(page: Bootstrap.page, path: '/bootstrap', initial: true), AutoRoute(page: ServerInfoSetupRoute.page, path: '/server-info'), + AutoRoute(page: ServerConnectionRoute.page, path: '/server-connection'), AutoRoute( page: DashboardRouter.page, diff --git a/useragent/lib/router.gr.dart b/useragent/lib/router.gr.dart index 704418e..e1b93c6 100644 --- a/useragent/lib/router.gr.dart +++ b/useragent/lib/router.gr.dart @@ -13,18 +13,20 @@ import 'package:arbiter/screens/bootstrap.dart' as _i2; import 'package:arbiter/screens/dashboard.dart' as _i4; import 'package:arbiter/screens/dashboard/about.dart' as _i1; import 'package:arbiter/screens/dashboard/calc.dart' as _i3; -import 'package:arbiter/screens/server_info_setup.dart' as _i5; -import 'package:auto_route/auto_route.dart' as _i6; +import 'package:arbiter/screens/server_connection.dart' as _i5; +import 'package:arbiter/screens/server_info_setup.dart' as _i6; +import 'package:auto_route/auto_route.dart' as _i7; +import 'package:flutter/material.dart' as _i8; /// generated route for /// [_i1.AboutScreen] -class AboutRoute extends _i6.PageRouteInfo { - const AboutRoute({List<_i6.PageRouteInfo>? children}) +class AboutRoute extends _i7.PageRouteInfo { + const AboutRoute({List<_i7.PageRouteInfo>? children}) : super(AboutRoute.name, initialChildren: children); static const String name = 'AboutRoute'; - static _i6.PageInfo page = _i6.PageInfo( + static _i7.PageInfo page = _i7.PageInfo( name, builder: (data) { return const _i1.AboutScreen(); @@ -34,13 +36,13 @@ class AboutRoute extends _i6.PageRouteInfo { /// generated route for /// [_i2.Bootstrap] -class Bootstrap extends _i6.PageRouteInfo { - const Bootstrap({List<_i6.PageRouteInfo>? children}) +class Bootstrap extends _i7.PageRouteInfo { + const Bootstrap({List<_i7.PageRouteInfo>? children}) : super(Bootstrap.name, initialChildren: children); static const String name = 'Bootstrap'; - static _i6.PageInfo page = _i6.PageInfo( + static _i7.PageInfo page = _i7.PageInfo( name, builder: (data) { return const _i2.Bootstrap(); @@ -50,13 +52,13 @@ class Bootstrap extends _i6.PageRouteInfo { /// generated route for /// [_i3.CalcScreen] -class CalcRoute extends _i6.PageRouteInfo { - const CalcRoute({List<_i6.PageRouteInfo>? children}) +class CalcRoute extends _i7.PageRouteInfo { + const CalcRoute({List<_i7.PageRouteInfo>? children}) : super(CalcRoute.name, initialChildren: children); static const String name = 'CalcRoute'; - static _i6.PageInfo page = _i6.PageInfo( + static _i7.PageInfo page = _i7.PageInfo( name, builder: (data) { return const _i3.CalcScreen(); @@ -66,13 +68,13 @@ class CalcRoute extends _i6.PageRouteInfo { /// generated route for /// [_i4.DashboardRouter] -class DashboardRouter extends _i6.PageRouteInfo { - const DashboardRouter({List<_i6.PageRouteInfo>? children}) +class DashboardRouter extends _i7.PageRouteInfo { + const DashboardRouter({List<_i7.PageRouteInfo>? children}) : super(DashboardRouter.name, initialChildren: children); static const String name = 'DashboardRouter'; - static _i6.PageInfo page = _i6.PageInfo( + static _i7.PageInfo page = _i7.PageInfo( name, builder: (data) { return const _i4.DashboardRouter(); @@ -81,17 +83,70 @@ class DashboardRouter extends _i6.PageRouteInfo { } /// generated route for -/// [_i5.ServerInfoSetupScreen] -class ServerInfoSetupRoute extends _i6.PageRouteInfo { - const ServerInfoSetupRoute({List<_i6.PageRouteInfo>? children}) +/// [_i5.ServerConnectionScreen] +class ServerConnectionRoute + extends _i7.PageRouteInfo { + ServerConnectionRoute({ + _i8.Key? key, + String? arbiterUrl, + List<_i7.PageRouteInfo>? children, + }) : super( + ServerConnectionRoute.name, + args: ServerConnectionRouteArgs(key: key, arbiterUrl: arbiterUrl), + initialChildren: children, + ); + + static const String name = 'ServerConnectionRoute'; + + static _i7.PageInfo page = _i7.PageInfo( + name, + builder: (data) { + final args = data.argsAs( + orElse: () => const ServerConnectionRouteArgs(), + ); + return _i5.ServerConnectionScreen( + key: args.key, + arbiterUrl: args.arbiterUrl, + ); + }, + ); +} + +class ServerConnectionRouteArgs { + const ServerConnectionRouteArgs({this.key, this.arbiterUrl}); + + final _i8.Key? key; + + final String? arbiterUrl; + + @override + String toString() { + return 'ServerConnectionRouteArgs{key: $key, arbiterUrl: $arbiterUrl}'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! ServerConnectionRouteArgs) return false; + return key == other.key && arbiterUrl == other.arbiterUrl; + } + + @override + int get hashCode => key.hashCode ^ arbiterUrl.hashCode; +} + +/// generated route for +/// [_i6.ServerInfoSetupScreen] +class ServerInfoSetupRoute extends _i7.PageRouteInfo { + const ServerInfoSetupRoute({List<_i7.PageRouteInfo>? children}) : super(ServerInfoSetupRoute.name, initialChildren: children); static const String name = 'ServerInfoSetupRoute'; - static _i6.PageInfo page = _i6.PageInfo( + static _i7.PageInfo page = _i7.PageInfo( name, builder: (data) { - return const _i5.ServerInfoSetupScreen(); + return const _i6.ServerInfoSetupScreen(); }, ); } diff --git a/useragent/lib/screens/server_connection.dart b/useragent/lib/screens/server_connection.dart new file mode 100644 index 0000000..8c1727f --- /dev/null +++ b/useragent/lib/screens/server_connection.dart @@ -0,0 +1,60 @@ +import 'package:arbiter/providers/connection/connection_manager.dart'; +import 'package:arbiter/router.gr.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 ServerConnectionScreen extends HookConsumerWidget { + const ServerConnectionScreen({super.key, this.arbiterUrl}); + + final String? arbiterUrl; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final connectionState = ref.watch(connectionManagerProvider); + + if (connectionState.value != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + context.router.replace(const DashboardRouter()); + }); + } + + final body = switch (connectionState) { + AsyncLoading() => const CircularProgressIndicator(), + AsyncError(:final error) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Connection failed', + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + SizedBox(height: 1.5.h), + Text('$error', textAlign: TextAlign.center), + SizedBox(height: 2.h), + + TextButton( + onPressed: () { + context.router.replace(const ServerInfoSetupRoute()); + }, + child: const Text('Back to server setup'), + ), + ], + ), + _ => const CircularProgressIndicator(), + }; + + return Scaffold( + appBar: AppBar(title: const Text('Connecting')), + body: Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 8.w), + child: body, + ), + ), + ); + } +} diff --git a/useragent/lib/screens/server_info_setup.dart b/useragent/lib/screens/server_info_setup.dart index 82873cf..99c319a 100644 --- a/useragent/lib/screens/server_info_setup.dart +++ b/useragent/lib/screens/server_info_setup.dart @@ -1,5 +1,6 @@ -import 'package:arbiter/features/arbiter_url.dart'; -import 'package:arbiter/features/server_info_storage.dart'; +import 'package:arbiter/features/connection/arbiter_url.dart'; +import 'package:arbiter/features/connection/server_info_storage.dart'; +import 'package:arbiter/providers/connection/bootstrap_token.dart'; import 'package:arbiter/providers/server_info.dart'; import 'package:arbiter/router.gr.dart'; import 'package:auto_route/auto_route.dart'; @@ -25,7 +26,7 @@ class ServerInfoSetupScreen extends HookConsumerWidget { if (serverInfo != null) { WidgetsBinding.instance.addPostFrameCallback((_) { if (context.mounted) { - context.router.replace(const DashboardRouter()); + context.router.replace(ServerConnectionRoute()); } }); } @@ -38,6 +39,14 @@ class ServerInfoSetupScreen extends HookConsumerWidget { try { final arbiterUrl = ArbiterUrl.parse(controller.text.trim()); + + // set token before triggering reconnection by updating server info + if (arbiterUrl.bootstrapToken != null) { + ref + .read(bootstrapTokenProvider.notifier) + .set(arbiterUrl.bootstrapToken!); + } + await ref .read(serverInfoProvider.notifier) .save( @@ -47,7 +56,9 @@ class ServerInfoSetupScreen extends HookConsumerWidget { ); if (context.mounted) { - context.router.replace(const DashboardRouter()); + context.router.replace( + ServerConnectionRoute(arbiterUrl: controller.text.trim()), + ); } } on FormatException catch (error) { errorText.value = error.message; @@ -94,13 +105,15 @@ class ServerInfoSetupScreen extends HookConsumerWidget { final options = [ const _OptionCard( title: 'Local', - subtitle: 'Will start and connect to a local service in a future update.', + subtitle: + 'Will start and connect to a local service in a future update.', enabled: false, child: SizedBox.shrink(), ), _OptionCard( title: 'Remote', - subtitle: 'Paste an Arbiter URL to store the server address, port, and CA fingerprint.', + subtitle: + 'Paste an Arbiter URL to store the server address, port, and CA fingerprint.', child: _RemoteConnectionForm( controller: controller, errorText: errorText.value, @@ -271,10 +284,7 @@ class _OptionCard extends StatelessWidget { ), SizedBox(height: 1.h), Text(subtitle, style: theme.textTheme.bodyMedium), - if (enabled) ...[ - SizedBox(height: 2.h), - child, - ], + if (enabled) ...[SizedBox(height: 2.h), child], ], ), ), diff --git a/useragent/macos/Runner/DebugProfile.entitlements b/useragent/macos/Runner/DebugProfile.entitlements index fbad023..da92e01 100644 --- a/useragent/macos/Runner/DebugProfile.entitlements +++ b/useragent/macos/Runner/DebugProfile.entitlements @@ -4,5 +4,7 @@ keychain-access-groups + com.apple.security.network.client + diff --git a/useragent/macos/Runner/Release.entitlements b/useragent/macos/Runner/Release.entitlements index 0833c1b..02b114d 100644 --- a/useragent/macos/Runner/Release.entitlements +++ b/useragent/macos/Runner/Release.entitlements @@ -6,5 +6,7 @@ keychain-access-groups + com.apple.security.network.client + diff --git a/useragent/pubspec.lock b/useragent/pubspec.lock index e6982a5..71f69d1 100644 --- a/useragent/pubspec.lock +++ b/useragent/pubspec.lock @@ -218,7 +218,7 @@ packages: source: hosted version: "0.3.5+2" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf @@ -401,8 +401,16 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" + url: "https://pub.dev" + source: hosted + version: "3.2.3" freezed_annotation: - dependency: transitive + dependency: "direct main" description: name: freezed_annotation sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" @@ -538,13 +546,21 @@ packages: source: hosted version: "0.7.2" json_annotation: - dependency: transitive + dependency: "direct main" description: name: json_annotation - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 + url: "https://pub.dev" + source: hosted + version: "6.11.2" leak_tracker: dependency: transitive description: @@ -918,6 +934,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.1" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" + url: "https://pub.dev" + source: hosted + version: "1.3.8" source_map_stack_trace: dependency: transitive description: diff --git a/useragent/pubspec.yaml b/useragent/pubspec.yaml index cc653e5..9775044 100644 --- a/useragent/pubspec.yaml +++ b/useragent/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: hosted: https://git.markettakers.org/api/packages/MarketTakers/pub/ version: ^1.0.6 cryptography: ^2.9.0 + crypto: ^3.0.6 flutter_secure_storage: ^10.0.0 cryptography_flutter: ^2.3.4 riverpod_annotation: ^4.0.0 @@ -30,6 +31,8 @@ dependencies: flutter_hooks: ^0.21.3+1 auto_route: ^11.1.0 protobuf: ^6.0.0 + freezed_annotation: ^3.1.0 + json_annotation: ^4.9.0 dev_dependencies: flutter_test: @@ -39,6 +42,8 @@ dev_dependencies: riverpod_generator: ^4.0.0+1 build_runner: ^2.12.2 auto_route_generator: ^10.4.0 + freezed: ^3.2.3 + json_serializable: ^6.11.2 flutter: uses-material-design: true