mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-26 11:43:44 +03:00
Improve RPC device path stability.
This improves "remembering" the active YubiKey, and lets FIDO reset work with additional keys present.
This commit is contained in:
parent
69165a63c2
commit
89c868ecc6
@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../app/models.dart';
|
||||
import '../core/models.dart';
|
||||
import '../management/models.dart';
|
||||
|
||||
final _log = Logger('yubikeyDataCommandProvider');
|
||||
|
@ -98,6 +98,8 @@ class DevicePath {
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hashAll(segments);
|
||||
|
||||
String get key => segments.join('/');
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
@ -52,8 +52,13 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
||||
|
||||
@override
|
||||
Stream<InteractionEvent> reset() {
|
||||
final signaler = Signaler();
|
||||
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);
|
||||
|
||||
controller.onCancel = () {
|
||||
if (!controller.isClosed) {
|
||||
@ -69,10 +74,6 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
||||
controller.sink.addError(e);
|
||||
}
|
||||
};
|
||||
controller.sink.addStream(signaler.signals
|
||||
.where((s) => s.status == 'reset')
|
||||
.map((signal) => InteractionEvent.values
|
||||
.firstWhere((e) => e.name == signal.body['state'])));
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
|
@ -132,12 +132,7 @@ class _DesktopCurrentDeviceNotifier extends CurrentDeviceNotifier {
|
||||
if (!devices.contains(state)) {
|
||||
final lastDevice = _prefs.getString(_lastDevice) ?? '';
|
||||
try {
|
||||
state = devices.firstWhere(
|
||||
(dev) => dev.when(
|
||||
usbYubiKey: (path, name, pid, info) =>
|
||||
lastDevice == 'serial:${info?.serial}',
|
||||
nfcReader: (path, name) => lastDevice == 'name:$name',
|
||||
),
|
||||
state = devices.firstWhere((dev) => dev.path.key == lastDevice,
|
||||
orElse: () => devices.whereType<UsbYubiKeyNode>().first);
|
||||
} on StateError {
|
||||
state = null;
|
||||
@ -148,16 +143,6 @@ class _DesktopCurrentDeviceNotifier extends CurrentDeviceNotifier {
|
||||
@override
|
||||
setCurrentDevice(DeviceNode device) {
|
||||
state = device;
|
||||
device.when(
|
||||
usbYubiKey: (path, name, pid, info) {
|
||||
final serial = info?.serial;
|
||||
if (serial != null) {
|
||||
_prefs.setString(_lastDevice, 'serial:$serial');
|
||||
}
|
||||
},
|
||||
nfcReader: (path, name) {
|
||||
_prefs.setString(_lastDevice, 'name:$name');
|
||||
},
|
||||
);
|
||||
_prefs.setString(_lastDevice, device.path.key);
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Factory reset'),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Warning! This will irrevocably delete all U2F and FIDO2 accounts from your YubiKey.'),
|
||||
@ -57,7 +58,10 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
|
||||
'Your credentials, as well as any PIN set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.',
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
Text(_getMessage(), style: Theme.of(context).textTheme.headline6),
|
||||
Center(
|
||||
child: Text(_getMessage(),
|
||||
style: Theme.of(context).textTheme.headline6),
|
||||
),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
|
@ -51,6 +51,7 @@ from yubikit.logging import LOG_LEVEL
|
||||
|
||||
from ykman.pcsc import list_devices, YK_READER_NAME
|
||||
from smartcard.Exceptions import SmartcardException
|
||||
from hashlib import sha256
|
||||
from dataclasses import asdict
|
||||
from typing import Mapping, Tuple
|
||||
|
||||
@ -112,6 +113,12 @@ class RootNode(RpcNode):
|
||||
return dict(result=scan_qr())
|
||||
|
||||
|
||||
def _id_from_fingerprint(fp):
|
||||
if isinstance(fp, str):
|
||||
fp = fp.encode()
|
||||
return sha256(fp).hexdigest()[:16]
|
||||
|
||||
|
||||
class ReadersNode(RpcNode):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@ -132,7 +139,7 @@ class ReadersNode(RpcNode):
|
||||
self._readers = {}
|
||||
self._reader_mapping = {}
|
||||
for device in devices:
|
||||
dev_id = os.urandom(4).hex()
|
||||
dev_id = _id_from_fingerprint(device.fingerprint)
|
||||
self._reader_mapping[dev_id] = device
|
||||
self._readers[dev_id] = dict(name=device.reader.name)
|
||||
self._state = state
|
||||
@ -187,9 +194,10 @@ class DevicesNode(RpcNode):
|
||||
self._devices = {}
|
||||
self._device_mapping = {}
|
||||
for dev, info in list_all_devices():
|
||||
dev_id = str(info.serial) if info.serial else os.urandom(4).hex()
|
||||
while dev_id in self._device_mapping:
|
||||
dev_id = os.urandom(4).hex()
|
||||
if info.serial:
|
||||
dev_id = str(info.serial)
|
||||
else:
|
||||
dev_id = _id_from_fingerprint(dev.fingerprint)
|
||||
self._device_mapping[dev_id] = (dev, info)
|
||||
name = get_name(info, dev.pid.get_type() if dev.pid else None)
|
||||
self._devices[dev_id] = dict(pid=dev.pid, name=name, serial=info.serial)
|
||||
|
@ -117,25 +117,22 @@ class Ctap2Node(RpcNode):
|
||||
raise TimeoutException()
|
||||
|
||||
def _prepare_reset_usb(self, event, signal):
|
||||
target = _ctap_id(self.ctap)
|
||||
logger.debug(f"Reset over USB: {target}")
|
||||
|
||||
# TODO: Filter on target
|
||||
n_keys = len(list_ctap())
|
||||
if n_keys > 1:
|
||||
raise ValueError("Only one YubiKey can be connected to perform a reset.")
|
||||
dev_path = self.ctap.device.descriptor.path
|
||||
logger.debug(f"Reset over USB: {dev_path}")
|
||||
|
||||
signal("reset", dict(state="remove"))
|
||||
removed = False
|
||||
while not event.wait(0.5):
|
||||
sleep(0.5)
|
||||
keys = list_ctap()
|
||||
if not keys:
|
||||
present = {k.descriptor.path for k in keys}
|
||||
if dev_path not in present:
|
||||
if not removed:
|
||||
signal("reset", dict(state="insert"))
|
||||
removed = True
|
||||
elif removed and len(keys) == 1:
|
||||
connection = keys[0].open_connection(FidoConnection)
|
||||
elif removed:
|
||||
key = next(k for k in keys if k.descriptor.path == dev_path)
|
||||
connection = key.open_connection(FidoConnection)
|
||||
signal("reset", dict(state="touch"))
|
||||
return connection
|
||||
|
||||
@ -143,6 +140,7 @@ class Ctap2Node(RpcNode):
|
||||
|
||||
@action
|
||||
def reset(self, params, event, signal):
|
||||
target = _ctap_id(self.ctap)
|
||||
if isinstance(self.ctap.device, CtapPcscDevice):
|
||||
connection = self._prepare_reset_nfc(event, signal)
|
||||
else:
|
||||
@ -150,6 +148,8 @@ class Ctap2Node(RpcNode):
|
||||
|
||||
logger.debug("Performing reset...")
|
||||
self.ctap = Ctap2(connection)
|
||||
if target != _ctap_id(self.ctap):
|
||||
raise ValueError("Re-inserted YubiKey does not match initial device")
|
||||
self.ctap.reset(event)
|
||||
self._info = self.ctap.get_info()
|
||||
self._pin = None
|
||||
|
Loading…
Reference in New Issue
Block a user