From 65560e728daae0b464ea870333311ffc873bced9 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 8 Dec 2021 11:20:04 +0100 Subject: [PATCH] Add "Manage password" dialog. --- lib/oath/menu_actions.dart | 12 ++ lib/oath/state.dart | 62 +++++++++- lib/oath/views/oath_screen.dart | 17 ++- lib/oath/views/password_dialog.dart | 175 ++++++++++++++++++++++++++++ 4 files changed, 259 insertions(+), 7 deletions(-) create mode 100755 lib/oath/views/password_dialog.dart diff --git a/lib/oath/menu_actions.dart b/lib/oath/menu_actions.dart index d66dd2d9..67103d60 100755 --- a/lib/oath/menu_actions.dart +++ b/lib/oath/menu_actions.dart @@ -5,6 +5,7 @@ import '../app/models.dart'; import '../app/state.dart'; import 'state.dart'; import 'views/add_account_page.dart'; +import 'views/password_dialog.dart'; List buildOathMenuActions( BuildContext context, AutoDisposeProviderRef ref) { @@ -25,6 +26,17 @@ List buildOathMenuActions( ); }, ), + if (!state.locked) + MenuAction( + text: 'Manage password', + icon: const Icon(Icons.password), + action: () { + showDialog( + context: context, + builder: (context) => ManagePasswordDialog(device), + ); + }, + ), MenuAction( text: 'Factory reset', icon: const Icon(Icons.delete_forever), diff --git a/lib/oath/state.dart b/lib/oath/state.dart index bac71d39..1a91f978 100755 --- a/lib/oath/state.dart +++ b/lib/oath/state.dart @@ -29,6 +29,10 @@ class _LockKeyNotifier extends StateNotifier { setKey(String key) { state = key; } + + unsetKey() { + state = null; + } } final oathStateProvider = StateNotifierProvider.autoDispose @@ -63,8 +67,12 @@ class OathStateNotifier extends StateNotifier { var oathState = OathState.fromJson(result['data']); final key = _read(_lockKeyProvider(_session.devicePath)); if (oathState.locked && key != null) { - await _session.command('validate', params: {'key': key}); - oathState = oathState.copyWith(locked: false); + final result = await _session.command('validate', params: {'key': key}); + if (result['unlocked']) { + oathState = oathState.copyWith(locked: false); + } else { + _read(_lockKeyProvider(_session.devicePath).notifier).unsetKey(); + } } if (mounted) { state = oathState; @@ -75,12 +83,58 @@ class OathStateNotifier extends StateNotifier { var result = await _session.command('derive', params: {'password': password}); var key = result['key']; - await _session.command('validate', params: {'key': key}); - if (mounted) { + final status = await _session.command('validate', params: {'key': key}); + if (mounted && status['unlocked']) { log.config('applet unlocked'); _read(_lockKeyProvider(_session.devicePath).notifier).setKey(key); state = state?.copyWith(locked: false); } + return status['unlocked']; + } + + Future _checkPassword(String password) async { + log.info('Calling check password $password'); + var result = + await _session.command('derive', params: {'password': password}); + log.info( + 'Check ${_read(_lockKeyProvider(_session.devicePath))} == ${result['key']}'); + return _read(_lockKeyProvider(_session.devicePath)) == result['key']; + } + + Future setPassword(String? current, String password) async { + if (state?.hasKey ?? false) { + if (current != null) { + if (!await _checkPassword(current)) { + return false; + } + } else { + return false; + } + } + + var result = + await _session.command('derive', params: {'password': password}); + var key = result['key']; + await _session.command('set_key', params: {'key': key}); + log.config('OATH key set'); + _read(_lockKeyProvider(_session.devicePath).notifier).setKey(key); + if (mounted) { + state = state?.copyWith(hasKey: true); + } + return true; + } + + Future unsetPassword(String current) async { + if (state?.hasKey ?? false) { + if (!await _checkPassword(current)) { + return false; + } + } + await _session.command('unset_key'); + _read(_lockKeyProvider(_session.devicePath).notifier).unsetKey(); + if (mounted) { + state = state?.copyWith(hasKey: false, locked: false); + } return true; } } diff --git a/lib/oath/views/oath_screen.dart b/lib/oath/views/oath_screen.dart index bb2edba9..988a19a0 100755 --- a/lib/oath/views/oath_screen.dart +++ b/lib/oath/views/oath_screen.dart @@ -28,12 +28,23 @@ class OathScreen extends ConsumerWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('YubiKey locked'), + const Text('Password required'), TextField( + autofocus: true, obscureText: true, decoration: const InputDecoration(labelText: 'Password'), - onSubmitted: (value) { - ref.read(oathStateProvider(device.path).notifier).unlock(value); + onSubmitted: (value) async { + final result = await ref + .read(oathStateProvider(device.path).notifier) + .unlock(value); + if (!result) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Wrong password'), + duration: Duration(seconds: 1), + ), + ); + } }, ), ], diff --git a/lib/oath/views/password_dialog.dart b/lib/oath/views/password_dialog.dart new file mode 100755 index 00000000..4a443d80 --- /dev/null +++ b/lib/oath/views/password_dialog.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; + +import '../../app/models.dart'; +import '../../app/state.dart'; +import '../state.dart'; + +final log = Logger('oath.views.password_dialog'); + +class ManagePasswordDialog extends ConsumerStatefulWidget { + final DeviceNode device; + const ManagePasswordDialog(this.device, {Key? key}) : super(key: key); + + @override + ConsumerState createState() => + _ManagePasswordDialogState(); +} + +class _ManagePasswordDialogState extends ConsumerState { + String _currentPassword = ''; + String _newPassword = ''; + String _confirmPassword = ''; + bool _currentIsWrong = false; + + @override + Widget build(BuildContext context) { + // If current device changes, we need to pop back to the main Page. + ref.listen(currentDeviceProvider, (previous, next) { + Navigator.of(context).pop(); + }); + + final state = ref.watch(oathStateProvider(widget.device.path)); + final hasKey = state?.hasKey ?? false; + + return AlertDialog( + title: const Text('Manage password'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (hasKey) + Column( + children: [ + const Text( + 'Enter your current password to change it. If you don\'t know your password, you\'ll need to reset the YubiKey, thne create a new password.'), + Row( + children: [ + Expanded( + child: TextField( + autofocus: true, + obscureText: true, + decoration: InputDecoration( + labelText: 'Current', + errorText: + _currentIsWrong ? 'Wrong password' : null), + onChanged: (value) { + setState(() { + _currentPassword = value; + }); + }, + ), + ), + const SizedBox( + width: 8.0, + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + child: const Text('Remove'), + onPressed: _currentPassword.isNotEmpty + ? () async { + final result = await ref + .read(oathStateProvider( + widget.device.path) + .notifier) + .unsetPassword(_currentPassword); + if (result) { + Navigator.of(context).pop(); + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text('Password removed'), + duration: Duration(seconds: 2), + ), + ); + } else { + setState(() { + _currentIsWrong = true; + }); + } + } + : null, + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 16.0, + ), + ], + ), + const Text( + 'Enter your new password. A password may contain letters, numbers and other characters.'), + Row( + children: [ + Expanded( + child: TextField( + autofocus: !hasKey, + obscureText: true, + decoration: const InputDecoration(labelText: 'Password'), + onChanged: (value) { + setState(() { + _newPassword = value; + }); + }, + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: TextField( + obscureText: true, + decoration: const InputDecoration( + labelText: 'Confirm', + ), + onChanged: (value) { + setState(() { + _confirmPassword = value; + }); + }, + ), + ), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: _newPassword.isNotEmpty && _newPassword == _confirmPassword + ? () async { + final result = await ref + .read(oathStateProvider(widget.device.path).notifier) + .setPassword(_currentPassword, _newPassword); + if (result) { + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Password set'), + duration: Duration(seconds: 2), + ), + ); + } else { + setState(() { + _currentIsWrong = true; + }); + } + } + : null, + child: const Text('Save'), + ) + ], + ); + } +}