mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 02:01:36 +03:00
Add FIDO reset support.
This commit is contained in:
parent
b71d17386a
commit
69165a63c2
@ -4,9 +4,14 @@ class ResponsiveDialog extends StatelessWidget {
|
||||
final Widget? title;
|
||||
final Widget child;
|
||||
final List<Widget> actions;
|
||||
final Function()? onCancel;
|
||||
|
||||
const ResponsiveDialog(
|
||||
{Key? key, required this.child, this.title, this.actions = const []})
|
||||
{Key? key,
|
||||
required this.child,
|
||||
this.title,
|
||||
this.actions = const [],
|
||||
this.onCancel})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@ -20,6 +25,12 @@ class ResponsiveDialog extends StatelessWidget {
|
||||
appBar: AppBar(
|
||||
title: title,
|
||||
actions: actions,
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
onCancel?.call();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
@ -40,6 +51,7 @@ class ResponsiveDialog extends StatelessWidget {
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () {
|
||||
onCancel?.call();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
|
@ -1,12 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:yubico_authenticator/desktop/models.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../fido/models.dart';
|
||||
import '../../fido/state.dart';
|
||||
import '../models.dart';
|
||||
import '../rpc.dart';
|
||||
import '../state.dart';
|
||||
|
||||
@ -50,9 +51,30 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reset() async {
|
||||
// TODO: implement reset
|
||||
throw UnimplementedError();
|
||||
Stream<InteractionEvent> reset() {
|
||||
final signaler = Signaler();
|
||||
final controller = StreamController<InteractionEvent>();
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
controller.sink.addStream(signaler.signals
|
||||
.where((s) => s.status == 'reset')
|
||||
.map((signal) => InteractionEvent.values
|
||||
.firstWhere((e) => e.name == signal.body['state'])));
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -62,6 +84,7 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
||||
'pin': oldPin,
|
||||
'new_pin': newPin,
|
||||
});
|
||||
await refresh();
|
||||
return PinResult.success();
|
||||
} on RpcError catch (e) {
|
||||
if (e.status == 'pin-validation') {
|
||||
|
@ -139,7 +139,12 @@ class RpcSession {
|
||||
_log.fine('RECV', jsonEncode(response));
|
||||
response.map(
|
||||
signal: (signal) {
|
||||
request.signal?._recv.sink.add(signal);
|
||||
final signaler = request.signal;
|
||||
if (signaler != null) {
|
||||
signaler._recv.sink.add(signal);
|
||||
} else {
|
||||
_log.warning('Received unhandled signal: $signal');
|
||||
}
|
||||
},
|
||||
success: (success) {
|
||||
request.completer.complete(success.body);
|
||||
|
@ -3,6 +3,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'models.freezed.dart';
|
||||
part 'models.g.dart';
|
||||
|
||||
enum InteractionEvent { remove, insert, touch }
|
||||
|
||||
@freezed
|
||||
class FidoState with _$FidoState {
|
||||
const FidoState._();
|
||||
|
@ -10,7 +10,7 @@ final fidoStateProvider = StateNotifierProvider.autoDispose
|
||||
);
|
||||
|
||||
abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState> {
|
||||
Future<void> reset();
|
||||
Stream<InteractionEvent> reset();
|
||||
Future<PinResult> unlock(String pin);
|
||||
Future<PinResult> setPin(String newPin, {String? oldPin});
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import '../../app/models.dart';
|
||||
import '../../app/views/app_failure_screen.dart';
|
||||
import '../../app/views/app_loading_screen.dart';
|
||||
import '../state.dart';
|
||||
import 'reset_dialog.dart';
|
||||
|
||||
class FidoScreen extends ConsumerWidget {
|
||||
final YubiKeyData deviceData;
|
||||
@ -76,12 +77,18 @@ class FidoScreen extends ConsumerWidget {
|
||||
title: Text('Credentials'),
|
||||
subtitle: Text('Manage stored credentials on key'),
|
||||
),
|
||||
const ListTile(
|
||||
leading: CircleAvatar(
|
||||
ListTile(
|
||||
leading: const CircleAvatar(
|
||||
child: Icon(Icons.delete_forever),
|
||||
),
|
||||
title: Text('Factory reset'),
|
||||
subtitle: Text('Delete all data and remove PIN'),
|
||||
title: const Text('Factory reset'),
|
||||
subtitle: const Text('Delete all data and remove PIN'),
|
||||
onTap: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(deviceData.node),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
|
102
lib/fido/views/reset_dialog.dart
Executable file
102
lib/fido/views/reset_dialog.dart
Executable file
@ -0,0 +1,102 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:yubico_authenticator/core/models.dart';
|
||||
|
||||
import '../state.dart';
|
||||
import '../../app/views/responsive_dialog.dart';
|
||||
import '../../fido/models.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
|
||||
class ResetDialog extends ConsumerStatefulWidget {
|
||||
final DeviceNode node;
|
||||
const ResetDialog(this.node, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _ResetDialogState();
|
||||
}
|
||||
|
||||
class _ResetDialogState extends ConsumerState<ResetDialog> {
|
||||
StreamSubscription<InteractionEvent>? _subscription;
|
||||
InteractionEvent? _interaction;
|
||||
|
||||
String _getMessage() {
|
||||
final nfc = widget.node.transport == Transport.nfc;
|
||||
switch (_interaction) {
|
||||
case InteractionEvent.remove:
|
||||
return nfc
|
||||
? 'Remove your YubiKey from the NFC reader'
|
||||
: 'Unplug your YubiKey';
|
||||
case InteractionEvent.insert:
|
||||
return nfc
|
||||
? 'Place your YubiKey back on the reader'
|
||||
: 'Re-insert your YubiKey';
|
||||
case InteractionEvent.touch:
|
||||
return 'Touch your YubiKey now';
|
||||
case null:
|
||||
return 'Press reset to begin...';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If current device changes, we need to pop back to the main Page.
|
||||
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Factory reset'),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Warning! This will irrevocably delete all U2F and FIDO2 accounts from your YubiKey.'),
|
||||
Text(
|
||||
'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),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
onCancel: () {
|
||||
_subscription?.cancel();
|
||||
},
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _subscription == null
|
||||
? () async {
|
||||
_subscription = ref
|
||||
.read(fidoStateProvider(widget.node.path).notifier)
|
||||
.reset()
|
||||
.listen(
|
||||
(event) {
|
||||
setState(() {
|
||||
_interaction = event;
|
||||
});
|
||||
},
|
||||
onDone: () {
|
||||
_subscription = null;
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('FIDO application reset'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: const Text('Reset'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -245,7 +245,7 @@ class AbstractDeviceNode(RpcNode):
|
||||
info=asdict(self._info),
|
||||
)
|
||||
except Exception:
|
||||
logger.error(f"Unable to connect via {conn_type}", exc_info=True)
|
||||
logger.warning(f"Unable to connect via {conn_type}", exc_info=True)
|
||||
raise ValueError("No supported connections")
|
||||
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from .base import RpcNode, action, child, RpcException
|
||||
from .base import RpcNode, action, child, RpcException, TimeoutException
|
||||
from fido2.ctap import CtapError
|
||||
from fido2.ctap2 import (
|
||||
Ctap2,
|
||||
@ -35,7 +35,14 @@ from fido2.ctap2 import (
|
||||
FPBioEnrollment,
|
||||
CaptureError,
|
||||
)
|
||||
from fido2.pcsc import CtapPcscDevice
|
||||
from yubikit.core.fido import FidoConnection
|
||||
from ykman.hid import list_ctap_devices as list_ctap
|
||||
from ykman.pcsc import list_devices as list_ccid
|
||||
from smartcard.Exceptions import NoCardException, CardConnectionException
|
||||
|
||||
from dataclasses import asdict
|
||||
from time import sleep
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -50,6 +57,10 @@ class PinValidationException(RpcException):
|
||||
)
|
||||
|
||||
|
||||
def _ctap_id(ctap):
|
||||
return (ctap.info.aaguid, ctap.info.firmware_version)
|
||||
|
||||
|
||||
class Ctap2Node(RpcNode):
|
||||
def __init__(self, connection):
|
||||
super().__init__()
|
||||
@ -79,9 +90,68 @@ class Ctap2Node(RpcNode):
|
||||
data.update(uv_retries=uv_retries)
|
||||
return data
|
||||
|
||||
def _prepare_reset_nfc(self, event, signal):
|
||||
reader_name = self.ctap.device._name
|
||||
devices = list_ccid(reader_name)
|
||||
if not devices or devices[0].reader.name != reader_name:
|
||||
raise ValueError("Unable to isolate NFC reader")
|
||||
dev = devices[0]
|
||||
logger.debug(f"Reset over NFC using reader: {dev.reader.name}")
|
||||
|
||||
signal("reset", dict(state="remove"))
|
||||
removed = False
|
||||
while not event.wait(0.5):
|
||||
sleep(0.5)
|
||||
try:
|
||||
with dev.open_connection(FidoConnection):
|
||||
if removed:
|
||||
sleep(1.0) # Wait for the device to settle
|
||||
return dev.open_connection(FidoConnection)
|
||||
except CardConnectionException:
|
||||
pass # Expected, ignore
|
||||
except NoCardException:
|
||||
if not removed:
|
||||
signal("reset", dict(state="insert"))
|
||||
removed = True
|
||||
|
||||
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.")
|
||||
|
||||
signal("reset", dict(state="remove"))
|
||||
removed = False
|
||||
while not event.wait(0.5):
|
||||
sleep(0.5)
|
||||
keys = list_ctap()
|
||||
if not keys:
|
||||
if not removed:
|
||||
signal("reset", dict(state="insert"))
|
||||
removed = True
|
||||
elif removed and len(keys) == 1:
|
||||
connection = keys[0].open_connection(FidoConnection)
|
||||
signal("reset", dict(state="touch"))
|
||||
return connection
|
||||
|
||||
raise TimeoutException()
|
||||
|
||||
@action
|
||||
def reset(self, params, event, signal):
|
||||
if isinstance(self.ctap.device, CtapPcscDevice):
|
||||
connection = self._prepare_reset_nfc(event, signal)
|
||||
else:
|
||||
connection = self._prepare_reset_usb(event, signal)
|
||||
|
||||
logger.debug("Performing reset...")
|
||||
self.ctap = Ctap2(connection)
|
||||
self.ctap.reset(event)
|
||||
self._info = self.ctap.get_info()
|
||||
self._pin = None
|
||||
self._auth_blocked = False
|
||||
return dict()
|
||||
|
Loading…
Reference in New Issue
Block a user