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

326 lines
9.6 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';
2023-05-04 14:57:40 +03:00
import '../../app/state.dart';
2022-03-15 19:16:14 +03:00
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(
2022-12-20 16:14:14 +03:00
ref.watch(rpcProvider).requireValue, devicePath, ['fido', 'ctap2']);
2022-03-23 19:50:49 +03:00
},
2022-03-15 19:16:14 +03:00
);
final desktopFidoState = AsyncNotifierProvider.autoDispose
.family<FidoStateNotifier, FidoState, DevicePath>(
_DesktopFidoStateNotifier.new);
class _DesktopFidoStateNotifier extends FidoStateNotifier {
late RpcNodeSession _session;
late StateController<String?> _pinController;
2023-05-04 14:57:40 +03:00
FutureOr<FidoState> _build(DevicePath devicePath) async {
var result = await _session.command('get');
FidoState fidoState = FidoState.fromJson(result['data']);
if (fidoState.hasPin && !fidoState.unlocked) {
final pin = ref.read(_pinProvider(devicePath));
if (pin != null) {
await unlock(pin);
result = await _session.command('get');
fidoState = FidoState.fromJson(result['data']);
}
}
_log.debug('application status', jsonEncode(fidoState));
return fidoState;
}
@override
FutureOr<FidoState> build(DevicePath devicePath) async {
_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));
}
2023-05-04 14:57:40 +03:00
ref.listen<WindowState>(
windowStateProvider,
(prev, next) async {
if (prev?.active == false && next.active) {
// Refresh state on active
final newState = await _build(devicePath);
if (state.valueOrNull != newState) {
state = AsyncValue.data(newState);
}
}
},
);
_pinController = ref.watch(_pinProvider(devicePath).notifier);
_session.setErrorHandler('state-reset', (_) async {
ref.invalidate(_sessionProvider(devicePath));
2022-03-15 19:16:14 +03:00
});
_session.setErrorHandler('auth-required', (_) async {
final pin = ref.read(_pinProvider(devicePath));
if (pin != null) {
await unlock(pin);
}
});
ref.onDispose(() {
_session.unsetErrorHandler('auth-required');
});
2022-03-15 19:16:14 +03:00
ref.onDispose(() {
_session.unsetErrorHandler('state-reset');
2022-03-15 19:16:14 +03:00
});
2023-05-04 14:57:40 +03:00
return _build(devicePath);
}
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 controller.sink.close();
ref.invalidateSelf();
2022-03-17 22:10:10 +03:00
} 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;
}
}
}
2023-05-04 14:57:40 +03:00
final desktopFingerprintProvider = AsyncNotifierProvider.autoDispose
.family<FidoFingerprintsNotifier, List<Fingerprint>, DevicePath>(
_DesktopFidoFingerprintsNotifier.new);
2022-03-23 19:50:49 +03:00
class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier {
2023-05-04 14:57:40 +03:00
late RpcNodeSession _session;
@override
FutureOr<List<Fingerprint>> build(DevicePath devicePath) async {
_session = ref.watch(_sessionProvider(devicePath));
ref.watch(fidoStateProvider(devicePath));
// Refresh on active
ref.listen<WindowState>(
windowStateProvider,
(prev, next) async {
if (prev?.active == false && next.active) {
// Refresh state on active
final newState = await _build(devicePath);
if (state.valueOrNull != newState) {
state = AsyncValue.data(newState);
}
}
},
);
2022-03-23 19:50:49 +03:00
2023-05-04 14:57:40 +03:00
return _build(devicePath);
}
2023-05-04 14:57:40 +03:00
FutureOr<List<Fingerprint>> _build(DevicePath devicePath) async {
final result = await _session.command('fingerprints');
2023-05-04 14:57:40 +03:00
return List.unmodifiable((result['children'] as Map<String, dynamic>)
2022-03-23 19:50:49 +03:00
.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]);
2023-05-04 14:57:40 +03:00
ref.invalidate(fidoStateProvider(_session.devicePath));
}
@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) {
2023-06-13 15:33:23 +03:00
controller.sink.add(switch (signal.status) {
'capture' => FingerprintEvent.capture(signal.body['remaining']),
'capture-error' => FingerprintEvent.error(signal.body['code']),
final other => throw UnimplementedError(other),
});
2022-03-23 11:49:20 +03:00
});
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)));
2023-05-04 14:57:40 +03:00
ref.invalidate(fidoStateProvider(_session.devicePath));
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);
2023-05-04 14:57:40 +03:00
ref.invalidate(fidoStateProvider(_session.devicePath));
return renamed;
}
2022-03-15 19:16:14 +03:00
}
2022-03-23 19:50:49 +03:00
2023-05-04 14:57:40 +03:00
final desktopCredentialProvider = AsyncNotifierProvider.autoDispose
.family<FidoCredentialsNotifier, List<FidoCredential>, DevicePath>(
_DesktopFidoCredentialsNotifier.new);
2022-03-23 19:50:49 +03:00
class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier {
2023-05-04 14:57:40 +03:00
late RpcNodeSession _session;
@override
FutureOr<List<FidoCredential>> build(DevicePath devicePath) async {
_session = ref.watch(_sessionProvider(devicePath));
ref.watch(fidoStateProvider(devicePath));
// Refresh on active
ref.listen<WindowState>(
windowStateProvider,
(prev, next) async {
if (prev?.active == false && next.active) {
// Refresh state on active
final newState = await _build(devicePath);
if (state.valueOrNull != newState) {
state = AsyncValue.data(newState);
}
}
},
);
2022-03-23 19:50:49 +03:00
2023-05-04 14:57:40 +03:00
return _build(devicePath);
2022-03-23 19:50:49 +03:00
}
2023-05-04 14:57:40 +03:00
FutureOr<List<FidoCredential>> _build(DevicePath devicePath) async {
2022-03-23 19:50:49 +03:00
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']));
}
}
2023-05-04 14:57:40 +03:00
return List.unmodifiable(creds);
2022-03-23 19:50:49 +03:00
}
@override
Future<void> deleteCredential(FidoCredential credential) async {
await _session.command('delete', target: [
'credentials',
credential.rpId,
credential.credentialId,
]);
2023-05-04 14:57:40 +03:00
ref.invalidate(fidoStateProvider(_session.devicePath));
2022-03-23 19:50:49 +03:00
}
}