yubioath-flutter/lib/desktop/fido/state.dart

274 lines
7.9 KiB
Dart
Raw Normal View History

2022-10-04 13:12:54 +03:00
/*
* Copyright (C) 2022 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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
.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));
}
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));
});
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;
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>();
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,
});
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(
'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<
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;
_DesktopFidoFingerprintsNotifier(this._session) {
_refresh();
}
2022-03-23 19:50:49 +03:00
Future<void> _refresh() async {
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());
}
@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();
}
@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
}
@override
Future<Fingerprint> renameFingerprint(
2022-03-23 11:49:20 +03:00
Fingerprint fingerprint, String name) async {
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();
return renamed;
}
2022-03-15 19:16:14 +03:00
}
2022-03-23 19:50:49 +03:00
final desktopCredentialProvider = StateNotifierProvider.autoDispose.family<
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;
_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();
}
}