2022-03-17 22:10:10 +03:00
|
|
|
import 'dart:async';
|
2022-03-15 19:16:14 +03:00
|
|
|
import 'dart:convert';
|
2022-03-30 17:45:47 +03:00
|
|
|
import 'dart:io';
|
2022-03-15 19:16:14 +03:00
|
|
|
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import 'package:logging/logging.dart';
|
2022-05-03 12:24:25 +03:00
|
|
|
import 'package:yubico_authenticator/app/logging.dart';
|
2022-03-15 19:16:14 +03:00
|
|
|
|
|
|
|
import '../../app/models.dart';
|
|
|
|
import '../../fido/models.dart';
|
|
|
|
import '../../fido/state.dart';
|
2022-03-17 22:10:10 +03:00
|
|
|
import '../models.dart';
|
2022-03-15 19:16:14 +03:00
|
|
|
import '../rpc.dart';
|
|
|
|
import '../state.dart';
|
|
|
|
|
|
|
|
final _log = Logger('desktop.fido.state');
|
|
|
|
|
2022-03-23 19:50:49 +03:00
|
|
|
final _pinProvider = StateProvider.autoDispose.family<String?, DevicePath>(
|
|
|
|
(ref, _) => null,
|
|
|
|
);
|
|
|
|
|
2022-03-15 19:16:14 +03:00
|
|
|
final _sessionProvider =
|
|
|
|
Provider.autoDispose.family<RpcNodeSession, DevicePath>(
|
2022-03-23 19:50:49 +03:00
|
|
|
(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']);
|
|
|
|
},
|
2022-03-15 19:16:14 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
final desktopFidoState = StateNotifierProvider.autoDispose
|
2022-03-24 00:55:18 +03:00
|
|
|
.family<FidoStateNotifier, AsyncValue<FidoState>, DevicePath>(
|
2022-03-15 19:16:14 +03:00
|
|
|
(ref, devicePath) {
|
|
|
|
final session = ref.watch(_sessionProvider(devicePath));
|
2022-03-30 17:45:47 +03:00
|
|
|
if (Platform.isWindows) {
|
|
|
|
// Make sure to rebuild if isAdmin changes
|
|
|
|
ref.watch(rpcStateProvider.select((state) => state.isAdmin));
|
|
|
|
}
|
2022-04-03 12:05:37 +03:00
|
|
|
final notifier = _DesktopFidoStateNotifier(
|
|
|
|
session,
|
|
|
|
ref.watch(_pinProvider(devicePath).notifier),
|
|
|
|
);
|
2022-03-15 19:16:14 +03:00
|
|
|
session.setErrorHandler('state-reset', (_) async {
|
|
|
|
ref.refresh(_sessionProvider(devicePath));
|
|
|
|
});
|
2022-04-03 12:05:37 +03:00
|
|
|
session.setErrorHandler('auth-required', (_) async {
|
|
|
|
final pin = ref.read(_pinProvider(devicePath));
|
|
|
|
if (pin != null) {
|
|
|
|
await notifier.unlock(pin);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
ref.onDispose(() {
|
|
|
|
session.unsetErrorHandler('auth-required');
|
|
|
|
});
|
2022-03-15 19:16:14 +03:00
|
|
|
ref.onDispose(() {
|
|
|
|
session.unsetErrorHandler('state-reset');
|
|
|
|
});
|
|
|
|
return notifier..refresh();
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
|
|
|
final RpcNodeSession _session;
|
2022-04-03 12:05:37 +03:00
|
|
|
final StateController<String?> _pinController;
|
|
|
|
_DesktopFidoStateNotifier(this._session, this._pinController) : super();
|
2022-03-15 19:16:14 +03:00
|
|
|
|
2022-03-24 14:39:49 +03:00
|
|
|
Future<void> refresh() => updateState(() async {
|
|
|
|
final result = await _session.command('get');
|
2022-05-03 12:24:25 +03:00
|
|
|
_log.debug('application status', jsonEncode(result));
|
2022-03-24 14:39:49 +03:00
|
|
|
return FidoState.fromJson(result['data']);
|
|
|
|
});
|
2022-03-15 19:16:14 +03:00
|
|
|
|
|
|
|
@override
|
2022-03-17 22:10:10 +03:00
|
|
|
Stream<InteractionEvent> reset() {
|
|
|
|
final controller = StreamController<InteractionEvent>();
|
2022-03-18 13:22:24 +03:00
|
|
|
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);
|
2022-03-17 22:10:10 +03:00
|
|
|
|
|
|
|
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;
|
2022-03-15 19:16:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2022-03-17 15:06:48 +03:00
|
|
|
Future<PinResult> setPin(String newPin, {String? oldPin}) async {
|
|
|
|
try {
|
|
|
|
await _session.command('set_pin', params: {
|
|
|
|
'pin': oldPin,
|
|
|
|
'new_pin': newPin,
|
|
|
|
});
|
2022-04-03 12:05:37 +03:00
|
|
|
return unlock(newPin);
|
2022-03-17 15:06:48 +03:00
|
|
|
} on RpcError catch (e) {
|
|
|
|
if (e.status == 'pin-validation') {
|
|
|
|
return PinResult.failed(e.body['retries'], e.body['auth_blocked']);
|
|
|
|
}
|
|
|
|
rethrow;
|
|
|
|
}
|
2022-03-23 19:50:49 +03:00
|
|
|
}
|
2022-03-24 14:39:49 +03:00
|
|
|
|
|
|
|
@override
|
|
|
|
Future<PinResult> unlock(String pin) async {
|
|
|
|
try {
|
|
|
|
await _session.command(
|
2022-04-03 12:05:37 +03:00
|
|
|
'unlock',
|
2022-03-24 14:39:49 +03:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-23 19:50:49 +03:00
|
|
|
final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family<
|
2022-04-03 12:05:37 +03:00
|
|
|
FidoFingerprintsNotifier, AsyncValue<List<Fingerprint>>, DevicePath>(
|
|
|
|
(ref, devicePath) => _DesktopFidoFingerprintsNotifier(
|
|
|
|
ref.watch(_sessionProvider(devicePath)),
|
|
|
|
));
|
2022-03-23 19:50:49 +03:00
|
|
|
|
|
|
|
class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
|
|
|
final RpcNodeSession _session;
|
|
|
|
|
2022-04-03 12:05:37 +03:00
|
|
|
_DesktopFidoFingerprintsNotifier(this._session) {
|
|
|
|
_refresh();
|
2022-03-22 16:23:12 +03:00
|
|
|
}
|
|
|
|
|
2022-03-23 19:50:49 +03:00
|
|
|
Future<void> _refresh() async {
|
2022-03-22 16:23:12 +03:00
|
|
|
final result = await _session.command('fingerprints');
|
2022-03-23 19:50:49 +03:00
|
|
|
setItems((result['children'] as Map<String, dynamic>)
|
|
|
|
.entries
|
|
|
|
.map((e) => Fingerprint(e.key, e.value['name']))
|
|
|
|
.toList());
|
2022-03-22 16:23:12 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> deleteFingerprint(Fingerprint fingerprint) async {
|
2022-03-23 11:49:20 +03:00
|
|
|
await _session
|
|
|
|
.command('delete', target: ['fingerprints', fingerprint.templateId]);
|
2022-03-23 19:50:49 +03:00
|
|
|
await _refresh();
|
2022-03-22 16:23:12 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2022-03-23 11:49:20 +03:00
|
|
|
Stream<FingerprintEvent> registerFingerprint({String? name}) {
|
|
|
|
final controller = StreamController<FingerprintEvent>();
|
|
|
|
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)));
|
2022-03-23 19:50:49 +03:00
|
|
|
await _refresh();
|
2022-03-23 11:49:20 +03:00
|
|
|
await controller.sink.close();
|
|
|
|
} catch (e) {
|
|
|
|
controller.sink.addError(e);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return controller.stream;
|
2022-03-15 19:16:14 +03:00
|
|
|
}
|
2022-03-22 16:23:12 +03:00
|
|
|
|
|
|
|
@override
|
|
|
|
Future<Fingerprint> renameFingerprint(
|
2022-03-23 11:49:20 +03:00
|
|
|
Fingerprint fingerprint, String name) async {
|
2022-03-22 16:23:12 +03:00
|
|
|
await _session.command('rename',
|
2022-03-23 11:49:20 +03:00
|
|
|
target: ['fingerprints', fingerprint.templateId],
|
|
|
|
params: {'name': name});
|
|
|
|
final renamed = fingerprint.copyWith(name: name);
|
2022-03-23 19:50:49 +03:00
|
|
|
await _refresh();
|
2022-03-22 16:23:12 +03:00
|
|
|
return renamed;
|
|
|
|
}
|
2022-03-15 19:16:14 +03:00
|
|
|
}
|
2022-03-23 19:50:49 +03:00
|
|
|
|
|
|
|
final desktopCredentialProvider = StateNotifierProvider.autoDispose.family<
|
2022-04-03 12:05:37 +03:00
|
|
|
FidoCredentialsNotifier, AsyncValue<List<FidoCredential>>, DevicePath>(
|
|
|
|
(ref, devicePath) => _DesktopFidoCredentialsNotifier(
|
|
|
|
ref.watch(_sessionProvider(devicePath)),
|
|
|
|
));
|
2022-03-23 19:50:49 +03:00
|
|
|
|
|
|
|
class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier {
|
|
|
|
final RpcNodeSession _session;
|
|
|
|
|
2022-04-03 12:05:37 +03:00
|
|
|
_DesktopFidoCredentialsNotifier(this._session) {
|
|
|
|
_refresh();
|
2022-03-23 19:50:49 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _refresh() async {
|
|
|
|
final List<FidoCredential> creds = [];
|
|
|
|
final rps = await _session.command('credentials');
|
|
|
|
for (final rpId in (rps['children'] as Map<String, dynamic>).keys) {
|
|
|
|
final result = await _session.command(rpId, target: ['credentials']);
|
|
|
|
for (final e in (result['children'] as Map<String, dynamic>).entries) {
|
|
|
|
creds.add(FidoCredential(
|
|
|
|
rpId: rpId,
|
|
|
|
credentialId: e.key,
|
|
|
|
userId: e.value['user_id'],
|
|
|
|
userName: e.value['user_name']));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setItems(creds);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> deleteCredential(FidoCredential credential) async {
|
|
|
|
await _session.command('delete', target: [
|
|
|
|
'credentials',
|
|
|
|
credential.rpId,
|
|
|
|
credential.credentialId,
|
|
|
|
]);
|
|
|
|
await _refresh();
|
|
|
|
}
|
|
|
|
}
|