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
|
|
|
);
|
|
|
|
|
2023-05-03 22:20:08 +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;
|
|
|
|
}
|
|
|
|
|
2023-05-03 22:20:08 +03:00
|
|
|
@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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2023-05-03 22:20:08 +03:00
|
|
|
_pinController = ref.watch(_pinProvider(devicePath).notifier);
|
|
|
|
_session.setErrorHandler('state-reset', (_) async {
|
2022-11-30 17:27:32 +03:00
|
|
|
ref.invalidate(_sessionProvider(devicePath));
|
2022-03-15 19:16:14 +03:00
|
|
|
});
|
2023-05-03 22:20:08 +03:00
|
|
|
_session.setErrorHandler('auth-required', (_) async {
|
2022-04-03 12:05:37 +03:00
|
|
|
final pin = ref.read(_pinProvider(devicePath));
|
|
|
|
if (pin != null) {
|
2023-05-03 22:20:08 +03:00
|
|
|
await unlock(pin);
|
2022-04-03 12:05:37 +03:00
|
|
|
}
|
|
|
|
});
|
|
|
|
ref.onDispose(() {
|
2023-05-03 22:20:08 +03:00
|
|
|
_session.unsetErrorHandler('auth-required');
|
2022-04-03 12:05:37 +03:00
|
|
|
});
|
2022-03-15 19:16:14 +03:00
|
|
|
ref.onDispose(() {
|
2023-05-03 22:20:08 +03:00
|
|
|
_session.unsetErrorHandler('state-reset');
|
2022-03-15 19:16:14 +03:00
|
|
|
});
|
|
|
|
|
2023-05-04 14:57:40 +03:00
|
|
|
return _build(devicePath);
|
2023-05-03 22:20:08 +03:00
|
|
|
}
|
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 controller.sink.close();
|
2023-05-03 22:20:08 +03:00
|
|
|
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,
|
|
|
|
});
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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);
|
2022-03-22 16:23:12 +03:00
|
|
|
}
|
|
|
|
|
2023-05-04 14:57:40 +03:00
|
|
|
FutureOr<List<Fingerprint>> _build(DevicePath devicePath) async {
|
2022-03-22 16:23:12 +03:00
|
|
|
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());
|
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]);
|
2023-05-04 14:57:40 +03:00
|
|
|
ref.invalidate(fidoStateProvider(_session.devicePath));
|
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) {
|
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
|
|
|
}
|
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);
|
2023-05-04 14:57:40 +03:00
|
|
|
ref.invalidate(fidoStateProvider(_session.devicePath));
|
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
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|