fixes for password and pin textinput handling and some changes for the password/pin management dialogs

This commit is contained in:
Rikard Braathen 2022-06-08 16:23:19 +02:00
parent 580e91d195
commit 751b972112
No known key found for this signature in database
GPG Key ID: BB568D1A81F6A965
4 changed files with 102 additions and 68 deletions

View File

@ -46,7 +46,6 @@ class FidoLockedPage extends ConsumerWidget {
actions: _buildActions(context),
child: Column(
children: [
const ListTile(title: Text('Unlock')),
_PinEntryForm(state, node),
],
),
@ -111,14 +110,21 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
final _pinController = TextEditingController();
bool _blocked = false;
int? _retries;
bool _pinIsWrong = false;
bool _isObscure = true;
void _submit() async {
setState(() {
_pinIsWrong = false;
_isObscure = true;
});
final result = await ref
.read(fidoStateProvider(widget._deviceNode.path).notifier)
.unlock(_pinController.text);
result.whenOrNull(failed: (retries, authBlocked) {
setState(() {
_pinController.clear();
_pinIsWrong = true;
_retries = retries;
_blocked = authBlocked;
});
@ -142,23 +148,39 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
Widget build(BuildContext context) {
final noFingerprints = widget._state.bioEnroll == false;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
padding: const EdgeInsets.only(left: 18.0, right: 18, top: 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Enter the FIDO2 PIN for your YubiKey'),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
padding: const EdgeInsets.only(top: 16.0, bottom: 16.0),
child: TextField(
autofocus: true,
obscureText: true,
obscureText: _isObscure,
controller: _pinController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'PIN',
errorText: _getErrorText(),
helperText: '', // Prevents dialog resizing
errorText: _pinIsWrong ? _getErrorText() : null,
errorMaxLines: 3,
suffixIcon: IconButton(
icon: Icon(
_isObscure ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
),
),
onChanged: (_) => setState(() {}), // Update state on change
onChanged: (value) {
setState(() {
_pinIsWrong = false;
});
}, // Update state on change
onSubmitted: (_) => _submit(),
),
),

View File

@ -23,6 +23,8 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
String _confirmPin = '';
String? _currentPinError;
String? _newPinError;
bool _currentIsWrong = false;
bool _newIsWrong = false;
@override
Widget build(BuildContext context) {
@ -48,10 +50,8 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (hasPin) ...[
Text(
'Current PIN',
style: Theme.of(context).textTheme.titleLarge,
),
const Text(
"Enter your current PIN. If you don't know your PIN, you'll need to reset the YubiKey."),
TextFormField(
initialValue: _currentPin,
autofocus: true,
@ -59,19 +59,19 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'Current PIN',
errorText: _currentPinError,
errorText: _currentIsWrong ? _currentPinError : null,
errorMaxLines: 3,
),
onChanged: (value) {
setState(() {
_currentIsWrong = false;
_currentPin = value;
});
},
),
],
Text(
'New PIN',
style: Theme.of(context).textTheme.titleLarge,
),
const Text(
'Enter your new PIN. A PIN must be at least 4 characters long and may contain letters, numbers and special characters.'),
TextFormField(
initialValue: _newPin,
autofocus: !hasPin,
@ -80,10 +80,12 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
border: const OutlineInputBorder(),
labelText: 'New PIN',
enabled: !hasPin || _currentPin.isNotEmpty,
errorText: _newPinError,
errorText: _newIsWrong ? _newPinError : null,
errorMaxLines: 3,
),
onChanged: (value) {
setState(() {
_newIsWrong = false;
_newPin = value;
});
},
@ -94,7 +96,8 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'Confirm PIN',
enabled: _newPin.isNotEmpty,
enabled:
(!hasPin || _currentPin.isNotEmpty) && _newPin.isNotEmpty,
),
onChanged: (value) {
setState(() {
@ -123,6 +126,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
if (_newPin.length < minPinLength) {
setState(() {
_newPinError = 'New PIN must be at least $minPinLength characters';
_newIsWrong = true;
});
return;
}
@ -137,8 +141,10 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
if (authBlocked) {
_currentPinError =
'PIN has been blocked until the YubiKey is removed and reinserted';
_currentIsWrong = true;
} else {
_currentPinError = 'Wrong PIN ($retries tries remaining)';
_currentIsWrong = true;
}
});
});

View File

@ -62,19 +62,19 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.state.hasKey) ...[
Text(
'Current password',
style: Theme.of(context).textTheme.titleLarge,
),
const Text(
"Enter your current password. If you don't know your password, you'll need to reset the YubiKey."),
TextField(
autofocus: true,
obscureText: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'Current password',
errorText: _currentIsWrong ? 'Wrong password' : null),
errorText: _currentIsWrong ? 'Wrong password' : null,
errorMaxLines: 3),
onChanged: (value) {
setState(() {
_currentIsWrong = false;
_currentPassword = value;
});
},
@ -116,12 +116,9 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
),
],
),
const Divider(),
],
Text(
'New password',
style: Theme.of(context).textTheme.titleLarge,
),
const Text(
'Enter your new password. A password may contain letters, numbers and special characters.'),
TextField(
autofocus: !widget.state.hasKey,
obscureText: true,
@ -141,7 +138,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'Confirm password',
enabled: _newPassword.isNotEmpty,
enabled: (!widget.state.hasKey || _currentPassword.isNotEmpty) &&
_newPassword.isNotEmpty,
),
onChanged: (value) {
setState(() {

View File

@ -84,7 +84,6 @@ class _LockedView extends ConsumerWidget {
],
child: Column(
children: [
const ListTile(title: Text('Unlock')),
_UnlockForm(
devicePath,
keystore: oathState.keystore,
@ -209,11 +208,13 @@ class _UnlockForm extends ConsumerStatefulWidget {
class _UnlockFormState extends ConsumerState<_UnlockForm> {
final _passwordController = TextEditingController();
bool _remember = false;
bool _wrong = false;
bool _passwordIsWrong = false;
bool _isObscure = true;
void _submit() async {
setState(() {
_wrong = false;
_passwordIsWrong = false;
_isObscure = false;
});
final result = await ref
.read(oathStateProvider(widget._devicePath).notifier)
@ -221,10 +222,9 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
if (!mounted) return;
if (!result.first) {
setState(() {
_wrong = true;
_passwordIsWrong = true;
_passwordController.clear();
});
showMessage(context, 'Wrong password');
} else if (_remember && !result.second) {
showMessage(context, 'Failed to remember password');
}
@ -236,7 +236,7 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.only(left: 18.0, right: 18, top: 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -247,51 +247,59 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
TextField(
controller: _passwordController,
autofocus: true,
obscureText: true,
obscureText: _isObscure,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'Password',
errorText: _wrong ? 'Wrong password' : null,
errorText: _passwordIsWrong ? 'Wrong password' : null,
helperText: '', // Prevents resizing when errorText shown
suffixIcon: IconButton(
icon: Icon(
_isObscure ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
),
),
onChanged: (_) => setState(() {}), // Update state on change
onChanged: (_) => setState(() {
_passwordIsWrong = false;
}), // Update state on change
onSubmitted: (_) => _submit(),
),
],
),
),
const SizedBox(height: 16.0),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: keystoreFailed
? const ListTile(
leading: Icon(Icons.warning_amber_rounded),
title: Text('OS Keystore unavailable'),
dense: true,
minLeadingWidth: 0,
)
: CheckboxListTile(
title: const Text('Remember password'),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
value: _remember,
onChanged: (value) {
setState(() {
_remember = value ?? false;
});
},
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ElevatedButton(
onPressed: _passwordController.text.isNotEmpty ? _submit : null,
child: const Text('Unlock'),
const SizedBox(height: 8.0),
keystoreFailed
? const ListTile(
leading: Icon(Icons.warning_amber_rounded),
title: Text('OS Keystore unavailable'),
dense: true,
minLeadingWidth: 0,
)
: CheckboxListTile(
title: const Text('Remember password'),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
value: _remember,
onChanged: (value) {
setState(() {
_remember = value ?? false;
});
},
),
)
],
Padding(
padding: const EdgeInsets.only(top: 12.0, right: 18.0, bottom: 4.0),
child: Align(
alignment: Alignment.centerRight,
child: ElevatedButton(
onPressed: _passwordController.text.isNotEmpty ? _submit : null,
child: const Text('Unlock'),
),
),
),
],
);