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:
Dain Nilsson 2022-03-18 11:22:24 +01:00
parent 69165a63c2
commit 89c868ecc6
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
7 changed files with 38 additions and 37 deletions

View File

@ -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');

View File

@ -98,6 +98,8 @@ class DevicePath {
@override
int get hashCode => Object.hashAll(segments);
String get key => segments.join('/');
}
@freezed

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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)

View File

@ -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