import 'dart:async'; import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import '../../app/models.dart'; import '../../fido/models.dart'; import '../../fido/state.dart'; import '../models.dart'; import '../rpc.dart'; import '../state.dart'; final _log = Logger('desktop.fido.state'); final _pinProvider = StateProvider.autoDispose.family( (ref, _) => null, ); final _sessionProvider = Provider.autoDispose.family( (ref, devicePath) { // Make sure the pinProvider is held for the duration of the session. ref.watch(_pinProvider(devicePath)); return RpcNodeSession( ref.watch(rpcProvider), devicePath, ['fido', 'ctap2']); }, ); final desktopFidoState = StateNotifierProvider.autoDispose .family, DevicePath>( (ref, devicePath) { final session = ref.watch(_sessionProvider(devicePath)); final notifier = _DesktopFidoStateNotifier(session); session.setErrorHandler('state-reset', (_) async { ref.refresh(_sessionProvider(devicePath)); }); ref.onDispose(() { session.unsetErrorHandler('state-reset'); }); return notifier..refresh(); }, ); class _DesktopFidoStateNotifier extends FidoStateNotifier { final RpcNodeSession _session; _DesktopFidoStateNotifier(this._session) : super(); Future refresh() => updateState(() async { final result = await _session.command('get'); _log.config('application status', jsonEncode(result)); return FidoState.fromJson(result['data']); }); @override Stream reset() { final controller = StreamController(); final signaler = Signaler(); signaler.signals .where((s) => s.status == 'reset') .map((signal) => InteractionEvent.values .firstWhere((e) => e.name == signal.body['state'])) .listen(controller.sink.add); controller.onCancel = () { if (!controller.isClosed) { signaler.cancel(); } }; controller.onListen = () async { try { await _session.command('reset', signal: signaler); await refresh(); await controller.sink.close(); } catch (e) { controller.sink.addError(e); } }; return controller.stream; } @override Future setPin(String newPin, {String? oldPin}) async { try { await _session.command('set_pin', params: { 'pin': oldPin, 'new_pin': newPin, }); await refresh(); return PinResult.success(); } on RpcError catch (e) { if (e.status == 'pin-validation') { return PinResult.failed(e.body['retries'], e.body['auth_blocked']); } rethrow; } } } final desktopFidoPinProvider = StateNotifierProvider.autoDispose .family((ref, devicePath) { return _DesktopPinNotifier(ref.watch(_sessionProvider(devicePath)), ref.watch(_pinProvider(devicePath).notifier)); }); class _DesktopPinNotifier extends PinNotifier { final RpcNodeSession _session; final StateController _pinController; _DesktopPinNotifier(this._session, this._pinController) : super(_pinController.state != null); @override Future unlock(String pin) async { try { await _session.command( 'verify_pin', params: {'pin': pin}, ); _pinController.state = pin; return PinResult.success(); } on RpcError catch (e) { if (e.status == 'pin-validation') { _pinController.state = null; return PinResult.failed(e.body['retries'], e.body['auth_blocked']); } rethrow; } } } final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family< FidoFingerprintsNotifier, AsyncValue>, DevicePath>((ref, devicePath) { final session = ref.watch(_sessionProvider(devicePath)); final notifier = _DesktopFidoFingerprintsNotifier( session, ref.watch(_pinProvider(devicePath).notifier), ); session.setErrorHandler('auth-required', (_) async { final pin = ref.read(_pinProvider(devicePath)); if (pin != null) { await notifier._unlock(pin); } }); ref.onDispose(() { session.unsetErrorHandler('auth-required'); }); return notifier; }); class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier { final RpcNodeSession _session; final StateController _pinNotifier; _DesktopFidoFingerprintsNotifier(this._session, this._pinNotifier) { final pin = _pinNotifier.state; if (pin != null) { _unlock(pin); } else { state = const AsyncValue.error('locked'); } } Future _unlock(String pin) async { try { await _session.command( 'unlock', target: ['fingerprints'], params: {'pin': pin}, ); await _refresh(); } on RpcError catch (e) { if (e.status == 'pin-validation') { _pinNotifier.state = null; } else { rethrow; } } } Future _refresh() async { final result = await _session.command('fingerprints'); setItems((result['children'] as Map) .entries .map((e) => Fingerprint(e.key, e.value['name'])) .toList()); } @override Future deleteFingerprint(Fingerprint fingerprint) async { await _session .command('delete', target: ['fingerprints', fingerprint.templateId]); await _refresh(); } @override Stream registerFingerprint({String? name}) { final controller = StreamController(); final signaler = Signaler(); signaler.signals.listen((signal) { switch (signal.status) { case 'capture': controller.sink .add(FingerprintEvent.capture(signal.body['remaining'])); break; case 'capture-error': controller.sink.add(FingerprintEvent.error(signal.body['code'])); break; } }); controller.onCancel = () { if (!controller.isClosed) { signaler.cancel(); } }; controller.onListen = () async { try { final result = await _session.command( 'add', target: ['fingerprints'], params: {'name': name}, signal: signaler, ); controller.sink .add(FingerprintEvent.complete(Fingerprint.fromJson(result))); await _refresh(); await controller.sink.close(); } catch (e) { controller.sink.addError(e); } }; return controller.stream; } @override Future renameFingerprint( Fingerprint fingerprint, String name) async { await _session.command('rename', target: ['fingerprints', fingerprint.templateId], params: {'name': name}); final renamed = fingerprint.copyWith(name: name); await _refresh(); return renamed; } } final desktopCredentialProvider = StateNotifierProvider.autoDispose.family< FidoCredentialsNotifier, AsyncValue>, DevicePath>((ref, devicePath) { final session = ref.watch(_sessionProvider(devicePath)); final notifier = _DesktopFidoCredentialsNotifier( session, ref.watch(_pinProvider(devicePath).notifier), ); session.setErrorHandler('auth-required', (_) async { final pin = ref.read(_pinProvider(devicePath)); if (pin != null) { await notifier._unlock(pin); } }); ref.onDispose(() { session.unsetErrorHandler('auth-required'); }); return notifier; }); class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier { final RpcNodeSession _session; final StateController _pinNotifier; _DesktopFidoCredentialsNotifier(this._session, this._pinNotifier) { final pin = _pinNotifier.state; if (pin != null) { _unlock(pin); } else { state = const AsyncValue.error('locked'); } } Future _unlock(String pin) async { try { await _session.command( 'unlock', target: ['credentials'], params: {'pin': pin}, ); await _refresh(); } on RpcError catch (e) { if (e.status == 'pin-validation') { _pinNotifier.state = null; } else { rethrow; } } } Future _refresh() async { final List creds = []; final rps = await _session.command('credentials'); for (final rpId in (rps['children'] as Map).keys) { final result = await _session.command(rpId, target: ['credentials']); for (final e in (result['children'] as Map).entries) { creds.add(FidoCredential( rpId: rpId, credentialId: e.key, userId: e.value['user_id'], userName: e.value['user_name'])); } } setItems(creds); } @override Future deleteCredential(FidoCredential credential) async { await _session.command('delete', target: [ 'credentials', credential.rpId, credential.credentialId, ]); await _refresh(); } }