mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 10:11:52 +03:00
Refactor FIDO PIN handling.
This commit is contained in:
parent
6b3bd585ba
commit
37883a1427
@ -46,17 +46,11 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
||||
final RpcNodeSession _session;
|
||||
_DesktopFidoStateNotifier(this._session) : super();
|
||||
|
||||
Future<void> refresh() async {
|
||||
try {
|
||||
Future<void> refresh() => updateState(() async {
|
||||
final result = await _session.command('get');
|
||||
_log.config('application status', jsonEncode(result));
|
||||
final fidoState = FidoState.fromJson(result['data']);
|
||||
setState(fidoState);
|
||||
} catch (error) {
|
||||
_log.severe('Unable to update FIDO state', jsonEncode(error));
|
||||
setFailure('Failed to update FIDO');
|
||||
}
|
||||
}
|
||||
return FidoState.fromJson(result['data']);
|
||||
});
|
||||
|
||||
@override
|
||||
Stream<InteractionEvent> reset() {
|
||||
@ -104,6 +98,39 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
final desktopFidoPinProvider = StateNotifierProvider.autoDispose
|
||||
.family<PinNotifier, bool, DevicePath>((ref, devicePath) {
|
||||
return _DesktopPinNotifier(ref.watch(_sessionProvider(devicePath)),
|
||||
ref.watch(_pinProvider(devicePath).notifier));
|
||||
});
|
||||
|
||||
class _DesktopPinNotifier extends PinNotifier {
|
||||
final RpcNodeSession _session;
|
||||
final StateController<String?> _pinController;
|
||||
|
||||
_DesktopPinNotifier(this._session, this._pinController)
|
||||
: super(_pinController.state != null);
|
||||
|
||||
@override
|
||||
Future<PinResult> unlock(String pin) async {
|
||||
try {
|
||||
await _session.command(
|
||||
'verify_pin',
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family<
|
||||
FidoFingerprintsNotifier,
|
||||
AsyncValue<List<Fingerprint>>,
|
||||
@ -116,7 +143,7 @@ final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family<
|
||||
session.setErrorHandler('auth-required', (_) async {
|
||||
final pin = ref.read(_pinProvider(devicePath));
|
||||
if (pin != null) {
|
||||
await notifier.unlock(pin, remember: false);
|
||||
await notifier._unlock(pin);
|
||||
}
|
||||
});
|
||||
ref.onDispose(() {
|
||||
@ -127,40 +154,33 @@ final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family<
|
||||
|
||||
class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
||||
final RpcNodeSession _session;
|
||||
final StateController<String?> _pin;
|
||||
bool locked = true;
|
||||
final StateController<String?> _pinNotifier;
|
||||
|
||||
_DesktopFidoFingerprintsNotifier(this._session, this._pin) {
|
||||
final pin = _pin.state;
|
||||
_DesktopFidoFingerprintsNotifier(this._session, this._pinNotifier) {
|
||||
final pin = _pinNotifier.state;
|
||||
if (pin != null) {
|
||||
unlock(pin, remember: false);
|
||||
_unlock(pin);
|
||||
} else {
|
||||
state = const AsyncValue.error('locked');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PinResult> unlock(String pin, {bool remember = true}) async {
|
||||
Future<void> _unlock(String pin) async {
|
||||
try {
|
||||
await _session.command(
|
||||
'unlock',
|
||||
target: ['fingerprints'],
|
||||
params: {'pin': pin},
|
||||
);
|
||||
locked = false;
|
||||
if (remember) {
|
||||
_pin.state = pin;
|
||||
}
|
||||
await _refresh();
|
||||
return PinResult.success();
|
||||
} on RpcError catch (e) {
|
||||
if (e.status == 'pin-validation') {
|
||||
_pin.state = null;
|
||||
return PinResult.failed(e.body['retries'], e.body['auth_blocked']);
|
||||
}
|
||||
_pinNotifier.state = null;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
final result = await _session.command('fingerprints');
|
||||
@ -242,7 +262,7 @@ final desktopCredentialProvider = StateNotifierProvider.autoDispose.family<
|
||||
session.setErrorHandler('auth-required', (_) async {
|
||||
final pin = ref.read(_pinProvider(devicePath));
|
||||
if (pin != null) {
|
||||
await notifier.unlock(pin, remember: false);
|
||||
await notifier._unlock(pin);
|
||||
}
|
||||
});
|
||||
ref.onDispose(() {
|
||||
@ -253,40 +273,33 @@ final desktopCredentialProvider = StateNotifierProvider.autoDispose.family<
|
||||
|
||||
class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier {
|
||||
final RpcNodeSession _session;
|
||||
final StateController<String?> _pin;
|
||||
bool locked = true;
|
||||
final StateController<String?> _pinNotifier;
|
||||
|
||||
_DesktopFidoCredentialsNotifier(this._session, this._pin) {
|
||||
final pin = _pin.state;
|
||||
_DesktopFidoCredentialsNotifier(this._session, this._pinNotifier) {
|
||||
final pin = _pinNotifier.state;
|
||||
if (pin != null) {
|
||||
unlock(pin, remember: false);
|
||||
_unlock(pin);
|
||||
} else {
|
||||
state = const AsyncValue.error('locked');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PinResult> unlock(String pin, {bool remember = true}) async {
|
||||
Future<void> _unlock(String pin) async {
|
||||
try {
|
||||
await _session.command(
|
||||
'unlock',
|
||||
target: ['credentials'],
|
||||
params: {'pin': pin},
|
||||
);
|
||||
locked = false;
|
||||
if (remember) {
|
||||
_pin.state = pin;
|
||||
}
|
||||
await _refresh();
|
||||
return PinResult.success();
|
||||
} on RpcError catch (e) {
|
||||
if (e.status == 'pin-validation') {
|
||||
_pin.state = null;
|
||||
return PinResult.failed(e.body['retries'], e.body['auth_blocked']);
|
||||
}
|
||||
_pinNotifier.state = null;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
final List<FidoCredential> creds = [];
|
||||
|
@ -100,6 +100,7 @@ Future<List<Override>> initializeAndGetOverrides(
|
||||
qrScannerProvider.overrideWithProvider(desktopQrScannerProvider),
|
||||
managementStateProvider.overrideWithProvider(desktopManagementState),
|
||||
fidoStateProvider.overrideWithProvider(desktopFidoState),
|
||||
fidoPinProvider.overrideWithProvider(desktopFidoPinProvider),
|
||||
fingerprintProvider.overrideWithProvider(desktopFingerprintProvider),
|
||||
credentialProvider.overrideWithProvider(desktopCredentialProvider),
|
||||
currentDeviceProvider.overrideWithProvider(desktopCurrentDeviceProvider)
|
||||
|
@ -15,10 +15,19 @@ abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState> {
|
||||
Future<PinResult> setPin(String newPin, {String? oldPin});
|
||||
}
|
||||
|
||||
final fidoPinProvider =
|
||||
StateNotifierProvider.autoDispose.family<PinNotifier, bool, DevicePath>(
|
||||
(ref, devicePath) => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
abstract class PinNotifier extends StateNotifier<bool> {
|
||||
PinNotifier(bool unlocked) : super(unlocked);
|
||||
Future<PinResult> unlock(String pin);
|
||||
}
|
||||
|
||||
abstract class LockedCollectionNotifier<T>
|
||||
extends StateNotifier<AsyncValue<List<T>>> {
|
||||
LockedCollectionNotifier() : super(const AsyncValue.loading());
|
||||
Future<PinResult> unlock(String pin);
|
||||
|
||||
@protected
|
||||
void setItems(List<T> items) {
|
||||
|
@ -41,12 +41,15 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Animation<Color?> _animateColor(Color color, {Function? atPeak}) {
|
||||
Animation<Color?> _animateColor(Color color,
|
||||
{Function? atPeak, bool reverse = true}) {
|
||||
final animation =
|
||||
ColorTween(begin: Colors.black, end: color).animate(_animator);
|
||||
_animator.forward().then((_) {
|
||||
if (reverse) {
|
||||
atPeak?.call();
|
||||
_animator.reverse();
|
||||
}
|
||||
});
|
||||
return animation;
|
||||
}
|
||||
@ -72,7 +75,7 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
_samples += 1;
|
||||
_remaining = remaining;
|
||||
});
|
||||
});
|
||||
}, reverse: remaining > 0);
|
||||
}, complete: (fingerprint) {
|
||||
_remaining = 0;
|
||||
_fingerprint = fingerprint;
|
||||
|
@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../app/views/app_failure_screen.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'delete_credential_dialog.dart';
|
||||
import 'rename_credential_dialog.dart';
|
||||
import 'unlock_view.dart';
|
||||
|
||||
class CredentialPage extends ConsumerWidget {
|
||||
final DeviceNode node;
|
||||
@ -18,13 +18,7 @@ class CredentialPage extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ref.watch(credentialProvider(node.path)).when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => UnlockView(
|
||||
onUnlock: (pin) async {
|
||||
return ref
|
||||
.read(credentialProvider(node.path).notifier)
|
||||
.unlock(pin);
|
||||
},
|
||||
),
|
||||
error: (error, _) => AppFailureScreen('$error'),
|
||||
data: (credentials) => ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
|
@ -2,12 +2,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../app/views/app_failure_screen.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'add_fingerprint_dialog.dart';
|
||||
import 'delete_fingerprint_dialog.dart';
|
||||
import 'rename_fingerprint_dialog.dart';
|
||||
import 'unlock_view.dart';
|
||||
|
||||
class FingerprintPage extends ConsumerWidget {
|
||||
final DeviceNode node;
|
||||
@ -19,13 +19,7 @@ class FingerprintPage extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ref.watch(fingerprintProvider(node.path)).when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => UnlockView(
|
||||
onUnlock: (pin) async {
|
||||
return ref
|
||||
.read(fingerprintProvider(node.path).notifier)
|
||||
.unlock(pin);
|
||||
},
|
||||
),
|
||||
error: (error, _) => AppFailureScreen('$error'),
|
||||
data: (fingerprints) => ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
|
@ -1,11 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'pin_dialog.dart';
|
||||
import 'pin_entry_dialog.dart';
|
||||
import 'reset_dialog.dart';
|
||||
|
||||
class FidoMainPage extends StatelessWidget {
|
||||
class FidoMainPage extends ConsumerWidget {
|
||||
final DeviceNode node;
|
||||
final FidoState state;
|
||||
final Function(SubPage page) setSubPage;
|
||||
@ -14,8 +17,21 @@ class FidoMainPage extends StatelessWidget {
|
||||
{required this.setSubPage, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
_openLockedPage(BuildContext context, WidgetRef ref, SubPage subPage) async {
|
||||
final unlocked = ref.read(fidoPinProvider(node.path));
|
||||
if (unlocked) {
|
||||
setSubPage(subPage);
|
||||
} else {
|
||||
final result = await showDialog(
|
||||
context: context, builder: (context) => PinEntryDialog(node.path));
|
||||
if (result == true) {
|
||||
setSubPage(subPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
@ -41,7 +57,7 @@ class FidoMainPage extends StatelessWidget {
|
||||
? 'Fingerprints have been registered'
|
||||
: 'No fingerprints registered'),
|
||||
onTap: () {
|
||||
setSubPage(SubPage.fingerprints);
|
||||
_openLockedPage(context, ref, SubPage.fingerprints);
|
||||
},
|
||||
),
|
||||
if (state.credMgmt)
|
||||
@ -50,14 +66,13 @@ class FidoMainPage extends StatelessWidget {
|
||||
child: Icon(Icons.account_box),
|
||||
),
|
||||
title: const Text('Credentials'),
|
||||
enabled: state.hasPin,
|
||||
subtitle: Text(state.hasPin
|
||||
? 'Manage stored credentials on key'
|
||||
: 'Set a PIN to manage credentials'),
|
||||
onTap: state.hasPin
|
||||
? () {
|
||||
setSubPage(SubPage.credentials);
|
||||
}
|
||||
: null,
|
||||
onTap: () {
|
||||
_openLockedPage(context, ref, SubPage.credentials);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CircleAvatar(
|
||||
|
93
lib/fido/views/pin_entry_dialog.dart
Executable file
93
lib/fido/views/pin_entry_dialog.dart
Executable file
@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../state.dart';
|
||||
|
||||
class PinEntryDialog extends ConsumerStatefulWidget {
|
||||
final DevicePath _devicePath;
|
||||
const PinEntryDialog(this._devicePath, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ConsumerState<PinEntryDialog> createState() => _PinEntryDialogState();
|
||||
}
|
||||
|
||||
class _PinEntryDialogState extends ConsumerState<PinEntryDialog> {
|
||||
final _pinController = TextEditingController();
|
||||
bool _blocked = false;
|
||||
int? _retries;
|
||||
|
||||
void _submit() async {
|
||||
final result = await ref
|
||||
.read(fidoPinProvider(widget._devicePath).notifier)
|
||||
.unlock(_pinController.text);
|
||||
result.when(success: () {
|
||||
Navigator.pop(context, true);
|
||||
}, failed: (retries, authBlocked) {
|
||||
setState(() {
|
||||
_pinController.clear();
|
||||
_retries = retries;
|
||||
_blocked = authBlocked;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
String? _getErrorText() {
|
||||
if (_retries == 0) {
|
||||
return 'PIN is blocked. Factory reset the FIDO application.';
|
||||
}
|
||||
if (_blocked) {
|
||||
return 'PIN temporarily blocked, remove and reinsert your YubiKey.';
|
||||
}
|
||||
if (_retries != null) {
|
||||
return 'Wrong PIN. $_retries attempts remaining.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@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 AlertDialog(
|
||||
scrollable: true,
|
||||
title: const Text('Enter PIN'),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Enter the FIDO PIN for your YubiKey.',
|
||||
),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
controller: _pinController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'PIN',
|
||||
errorText: _getErrorText(),
|
||||
),
|
||||
onChanged: (_) => setState(() {}), // Update state on change
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('Continue'),
|
||||
onPressed:
|
||||
_pinController.text.isNotEmpty && !_blocked ? _submit : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../models.dart';
|
||||
|
||||
class UnlockView extends StatelessWidget {
|
||||
final Future<PinResult> Function(String pin) onUnlock;
|
||||
|
||||
const UnlockView({required this.onUnlock, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||
child: Text(
|
||||
'Enter PIN',
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'Enter the FIDO PIN for your YubiKey. If you don\'t know your PIN, you\'ll need to reset the YubiKey.',
|
||||
),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(labelText: 'PIN'),
|
||||
onSubmitted: (pin) async {
|
||||
// TODO: Handle wrong PIN
|
||||
await onUnlock(pin);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user