feat(useragent): initial connection impl

This commit is contained in:
hdbg
2026-03-15 16:53:49 +01:00
parent 27836beb75
commit c61a9e30ac
28 changed files with 688 additions and 225 deletions

View File

@@ -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<UserAgentRequest> _tx;
final StreamIterator<UserAgentResponse> _rx;
Connection({
required this.channel,
required StreamController<UserAgentRequest> tx,
required ResponseStream<UserAgentResponse> rx,
}) : _tx = tx,
_rx = StreamIterator(rx);
Future<void> send(UserAgentRequest request) async {
_tx.add(request);
}
Future<UserAgentResponse> receive() async {
await _rx.moveNext();
return _rx.current;
}
Future<void> close() async {
await _tx.close();
await channel.shutdown();
}
}
Future<Connection> _connect(StoredServerInfo serverInfo) async {
final channel = ClientChannel(
serverInfo.address,
port: serverInfo.port,
options: ChannelOptions(
connectTimeout: const Duration(seconds: 10),
credentials: ChannelCredentials.secure(
onBadCertificate: (cert, host) {
return true;
},
),
),
);
final client = ArbiterServiceClient(channel);
final tx = StreamController<UserAgentRequest>();
final rx = client.userAgent(tx.stream);
return Connection(channel: channel, tx: tx, rx: rx);
}
List<int> formatChallenge(AuthChallenge challenge, List<int> pubkey) {
final encodedPubkey = base64Encode(pubkey);
final payload = "${challenge.nonce}:$encodedPubkey";
return utf8.encode(payload);
}
Future<Connection> connectAndAuthorize(
StoredServerInfo serverInfo,
KeyHandle key, {
String? bootstrapToken,
}) async {
try {
final connection = await _connect(serverInfo);
talker.info(
'Connected to server at ${serverInfo.address}:${serverInfo.port}',
);
final pubkey = await key.getPublicKey();
final req = AuthChallengeRequest(
pubkey: pubkey,
bootstrapToken: bootstrapToken,
keyType: switch (key.alg) {
KeyAlgorithm.rsa => KeyType.KEY_TYPE_RSA,
KeyAlgorithm.ecdsa => KeyType.KEY_TYPE_ECDSA_SECP256K1,
KeyAlgorithm.ed25519 => KeyType.KEY_TYPE_ED25519,
},
);
await connection.send(UserAgentRequest(authChallengeRequest: req));
talker.info(
"Sent auth challenge request with pubkey ${base64Encode(pubkey)}",
);
class Connection {}
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');
}
}

View File

@@ -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<String, dynamic> toJson() => {
'address': address,
'port': port,
'caCertFingerprint': caCertFingerprint,
};
factory StoredServerInfo.fromJson(Map<String, dynamic> json) {
return StoredServerInfo(
address: json['address'] as String,
port: json['port'] as int,
caCertFingerprint: json['caCertFingerprint'] as String,
);
}
factory StoredServerInfo.fromJson(Map<String, dynamic> json) => _$StoredServerInfoFromJson(json);
Map<String, dynamic> toJson() => _$StoredServerInfoToJson(this);
}
abstract class ServerInfoStorage {

View File

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

View File

@@ -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,
),