mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 00:12:09 +03:00
Merge PR #1425
This commit is contained in:
commit
2d43e526b7
@ -32,14 +32,18 @@ import '../state.dart';
|
||||
|
||||
final _log = Logger('desktop.fido.state');
|
||||
|
||||
final _pinProvider = StateProvider.autoDispose.family<String?, DevicePath>(
|
||||
(ref, _) => null,
|
||||
final _pinProvider = StateProvider.family<String?, DevicePath>(
|
||||
(ref, _) {
|
||||
// Clear PIN if current device is changed
|
||||
ref.watch(currentDeviceProvider);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
final _sessionProvider =
|
||||
Provider.autoDispose.family<RpcNodeSession, DevicePath>(
|
||||
(ref, devicePath) {
|
||||
// Make sure the pinProvider is held for the duration of the session.
|
||||
// Refresh state when PIN is changed
|
||||
ref.watch(_pinProvider(devicePath));
|
||||
return RpcNodeSession(
|
||||
ref.watch(rpcProvider).requireValue, devicePath, ['fido', 'ctap2']);
|
||||
|
@ -256,7 +256,7 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
onFieldSubmitted: (_) {
|
||||
_submit();
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
)
|
||||
]
|
||||
],
|
||||
|
@ -44,7 +44,8 @@ class FidoPinDialog extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
String _currentPin = '';
|
||||
final _currentPinController = TextEditingController();
|
||||
final _currentPinFocus = FocusNode();
|
||||
String _newPin = '';
|
||||
String _confirmPin = '';
|
||||
String? _currentPinError;
|
||||
@ -54,15 +55,28 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
bool _isObscureCurrent = true;
|
||||
bool _isObscureNew = true;
|
||||
bool _isObscureConfirm = true;
|
||||
bool _isBlocked = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_currentPinController.dispose();
|
||||
_currentPinFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasPin = widget.state.hasPin;
|
||||
final isValid = _newPin.isNotEmpty &&
|
||||
_newPin == _confirmPin &&
|
||||
(!hasPin || _currentPin.isNotEmpty);
|
||||
final minPinLength = widget.state.minPinLength;
|
||||
final currentMinPinLen = !hasPin
|
||||
? 0
|
||||
// N.B. current PIN may be shorter than minimum if set before the minimum was increased
|
||||
: (widget.state.forcePinChange ? 4 : widget.state.minPinLength);
|
||||
final currentPinLenOk =
|
||||
_currentPinController.text.length >= currentMinPinLen;
|
||||
final newPinLenOk = _newPin.length >= minPinLength;
|
||||
final isValid = currentPinLenOk && newPinLenOk && _newPin == _confirmPin;
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(hasPin ? l10n.s_change_pin : l10n.s_set_pin),
|
||||
@ -82,11 +96,13 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
Text(l10n.p_enter_current_pin_or_reset_no_puk),
|
||||
AppTextFormField(
|
||||
key: currentPin,
|
||||
initialValue: _currentPin,
|
||||
controller: _currentPinController,
|
||||
focusNode: _currentPinFocus,
|
||||
autofocus: true,
|
||||
obscureText: _isObscureCurrent,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
decoration: AppInputDecoration(
|
||||
enabled: !_isBlocked,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_current_pin,
|
||||
errorText: _currentIsWrong ? _currentPinError : null,
|
||||
@ -108,10 +124,9 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_currentIsWrong = false;
|
||||
_currentPin = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
],
|
||||
Text(l10n.p_enter_new_fido2_pin(minPinLength)),
|
||||
// TODO: Set max characters based on UTF-8 bytes
|
||||
@ -124,7 +139,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_new_pin,
|
||||
enabled: !hasPin || _currentPin.isNotEmpty,
|
||||
enabled: !_isBlocked && currentPinLenOk,
|
||||
errorText: _newIsWrong ? _newPinError : null,
|
||||
errorMaxLines: 3,
|
||||
prefixIcon: const Icon(Symbols.pin),
|
||||
@ -146,7 +161,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
_newPin = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextFormField(
|
||||
key: confirmPin,
|
||||
initialValue: _confirmPin,
|
||||
@ -168,8 +183,12 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
tooltip:
|
||||
_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin,
|
||||
),
|
||||
enabled:
|
||||
(!hasPin || _currentPin.isNotEmpty) && _newPin.isNotEmpty,
|
||||
enabled: !_isBlocked && currentPinLenOk && newPinLenOk,
|
||||
errorText: _newPin.length == _confirmPin.length &&
|
||||
_newPin != _confirmPin
|
||||
? l10n.l_pin_mismatch
|
||||
: null,
|
||||
helperText: '', // Prevents resizing when errorText shown
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
@ -181,7 +200,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
@ -195,15 +214,9 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
|
||||
void _submit() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final minPinLength = widget.state.minPinLength;
|
||||
final oldPin = _currentPin.isNotEmpty ? _currentPin : null;
|
||||
if (_newPin.length < minPinLength) {
|
||||
setState(() {
|
||||
_newPinError = l10n.l_new_pin_len(minPinLength);
|
||||
_newIsWrong = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
final oldPin = _currentPinController.text.isNotEmpty
|
||||
? _currentPinController.text
|
||||
: null;
|
||||
try {
|
||||
final result = await ref
|
||||
.read(fidoStateProvider(widget.devicePath).notifier)
|
||||
@ -213,9 +226,13 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
showMessage(context, l10n.s_pin_set);
|
||||
}, failed: (retries, authBlocked) {
|
||||
setState(() {
|
||||
_currentPinController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _currentPinController.text.length);
|
||||
_currentPinFocus.requestFocus();
|
||||
if (authBlocked) {
|
||||
_currentPinError = l10n.l_pin_soft_locked;
|
||||
_currentIsWrong = true;
|
||||
_isBlocked = true;
|
||||
} else {
|
||||
_currentPinError = l10n.l_wrong_pin_attempts_remaining(retries);
|
||||
_currentIsWrong = true;
|
||||
|
@ -37,11 +37,19 @@ class PinEntryForm extends ConsumerStatefulWidget {
|
||||
|
||||
class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
||||
final _pinController = TextEditingController();
|
||||
final _pinFocus = FocusNode();
|
||||
bool _blocked = false;
|
||||
int? _retries;
|
||||
bool _pinIsWrong = false;
|
||||
bool _isObscure = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pinController.dispose();
|
||||
_pinFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submit() async {
|
||||
setState(() {
|
||||
_pinIsWrong = false;
|
||||
@ -51,8 +59,10 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
||||
.read(fidoStateProvider(widget._deviceNode.path).notifier)
|
||||
.unlock(_pinController.text);
|
||||
result.whenOrNull(failed: (retries, authBlocked) {
|
||||
_pinController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _pinController.text.length);
|
||||
_pinFocus.requestFocus();
|
||||
setState(() {
|
||||
_pinController.clear();
|
||||
_pinIsWrong = true;
|
||||
_retries = retries;
|
||||
_blocked = authBlocked;
|
||||
@ -92,6 +102,8 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
||||
obscureText: _isObscure,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
controller: _pinController,
|
||||
focusNode: _pinFocus,
|
||||
enabled: !_blocked && (_retries ?? 1) > 0,
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_pin,
|
||||
@ -116,7 +128,7 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
||||
});
|
||||
}, // Update state on change
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
).init(),
|
||||
),
|
||||
ListTile(
|
||||
leading: noFingerprints
|
||||
@ -136,8 +148,12 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
||||
key: unlockFido2WithPin,
|
||||
icon: const Icon(Symbols.lock_open),
|
||||
label: Text(l10n.s_unlock),
|
||||
onPressed:
|
||||
_pinController.text.isNotEmpty && !_blocked ? _submit : null,
|
||||
onPressed: !_pinIsWrong &&
|
||||
_pinController.text.length >=
|
||||
widget._state.minPinLength &&
|
||||
!_blocked
|
||||
? _submit
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -112,7 +112,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -87,7 +87,7 @@ class _ManageLabelDialogState extends ConsumerState<ManageLabelDialog> {
|
||||
onFieldSubmitted: (_) {
|
||||
_submit();
|
||||
},
|
||||
)
|
||||
).init()
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -233,6 +233,8 @@
|
||||
"s_confirm_pin": "PIN bestätigen",
|
||||
"s_confirm_puk": null,
|
||||
"s_unblock_pin": null,
|
||||
"l_pin_mismatch": null,
|
||||
"l_puk_mismatch": null,
|
||||
"l_new_pin_len": "Neue PIN muss mindestens {length} Zeichen lang sein",
|
||||
"@l_new_pin_len": {
|
||||
"placeholders": {
|
||||
@ -309,6 +311,7 @@
|
||||
"s_new_password": "Neues Passwort",
|
||||
"s_current_password": "Aktuelles Passwort",
|
||||
"s_confirm_password": "Passwort bestätigen",
|
||||
"l_password_mismatch": null,
|
||||
"s_wrong_password": "Falsches Passwort",
|
||||
"s_remove_password": "Passwort entfernen",
|
||||
"s_password_removed": "Passwort entfernt",
|
||||
|
@ -233,6 +233,8 @@
|
||||
"s_confirm_pin": "Confirm PIN",
|
||||
"s_confirm_puk": "Confirm PUK",
|
||||
"s_unblock_pin": "Unblock PIN",
|
||||
"l_pin_mismatch": "PINs do not match",
|
||||
"l_puk_mismatch": "PUKs do not match",
|
||||
"l_new_pin_len": "New PIN must be at least {length} characters",
|
||||
"@l_new_pin_len": {
|
||||
"placeholders": {
|
||||
@ -309,6 +311,7 @@
|
||||
"s_new_password": "New password",
|
||||
"s_current_password": "Current password",
|
||||
"s_confirm_password": "Confirm password",
|
||||
"l_password_mismatch": "Passwords do not match",
|
||||
"s_wrong_password": "Wrong password",
|
||||
"s_remove_password": "Remove password",
|
||||
"s_password_removed": "Password removed",
|
||||
|
@ -233,6 +233,8 @@
|
||||
"s_confirm_pin": "Confirmez le PIN",
|
||||
"s_confirm_puk": "Confirmez le PUK",
|
||||
"s_unblock_pin": "Débloquer le PIN",
|
||||
"l_pin_mismatch": null,
|
||||
"l_puk_mismatch": null,
|
||||
"l_new_pin_len": "Le nouveau PIN doit avoir au moins {length} caractères",
|
||||
"@l_new_pin_len": {
|
||||
"placeholders": {
|
||||
@ -309,6 +311,7 @@
|
||||
"s_new_password": "Nouveau mot de passe",
|
||||
"s_current_password": "Mot de passe actuel",
|
||||
"s_confirm_password": "Confirmez le mot de passe",
|
||||
"l_password_mismatch": null,
|
||||
"s_wrong_password": "Mauvais mot de passe",
|
||||
"s_remove_password": "Supprimer le mot de passe",
|
||||
"s_password_removed": "Mot de passe supprimé",
|
||||
|
@ -233,6 +233,8 @@
|
||||
"s_confirm_pin": "PINの確認",
|
||||
"s_confirm_puk": "PUKの確認",
|
||||
"s_unblock_pin": "ブロックを解除",
|
||||
"l_pin_mismatch": null,
|
||||
"l_puk_mismatch": null,
|
||||
"l_new_pin_len": "新しいPINは少なくとも{length}文字である必要があります",
|
||||
"@l_new_pin_len": {
|
||||
"placeholders": {
|
||||
@ -309,6 +311,7 @@
|
||||
"s_new_password": "新しいパスワード",
|
||||
"s_current_password": "現在のパスワード",
|
||||
"s_confirm_password": "パスワードを確認",
|
||||
"l_password_mismatch": null,
|
||||
"s_wrong_password": "間違ったパスワード",
|
||||
"s_remove_password": "パスワードの削除",
|
||||
"s_password_removed": "パスワードが削除されました",
|
||||
|
@ -233,6 +233,8 @@
|
||||
"s_confirm_pin": "Potwierdź PIN",
|
||||
"s_confirm_puk": "Potwierdź PUK",
|
||||
"s_unblock_pin": "Odblokuj PIN",
|
||||
"l_pin_mismatch": null,
|
||||
"l_puk_mismatch": null,
|
||||
"l_new_pin_len": "Nowy PIN musi mieć co najmniej {length} znaków",
|
||||
"@l_new_pin_len": {
|
||||
"placeholders": {
|
||||
@ -309,6 +311,7 @@
|
||||
"s_new_password": "Nowe hasło",
|
||||
"s_current_password": "Aktualne hasło",
|
||||
"s_confirm_password": "Potwierdź hasło",
|
||||
"l_password_mismatch": null,
|
||||
"s_wrong_password": "Błędne hasło",
|
||||
"s_remove_password": "Usuń hasło",
|
||||
"s_password_removed": "Hasło zostało usunięte",
|
||||
|
@ -390,7 +390,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
onSubmitted: (_) {
|
||||
if (isValid) submit();
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.nameField,
|
||||
controller: _accountController,
|
||||
@ -418,7 +418,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
onSubmitted: (_) {
|
||||
if (isValid) submit();
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.secretField,
|
||||
controller: _secretController,
|
||||
@ -460,7 +460,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
onSubmitted: (_) {
|
||||
if (isValid) submit();
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
|
@ -41,7 +41,8 @@ class ManagePasswordDialog extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
String _currentPassword = '';
|
||||
final _currentPasswordController = TextEditingController();
|
||||
final _currentPasswordFocus = FocusNode();
|
||||
String _newPassword = '';
|
||||
String _confirmPassword = '';
|
||||
bool _currentIsWrong = false;
|
||||
@ -49,12 +50,19 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
bool _isObscureNew = true;
|
||||
bool _isObscureConfirm = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_currentPasswordController.dispose();
|
||||
_currentPasswordFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_submit() async {
|
||||
FocusUtils.unfocus(context);
|
||||
|
||||
final result = await ref
|
||||
.read(oathStateProvider(widget.path).notifier)
|
||||
.setPassword(_currentPassword, _newPassword);
|
||||
.setPassword(_currentPasswordController.text, _newPassword);
|
||||
if (result) {
|
||||
if (mounted) {
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
@ -63,6 +71,9 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_currentPasswordController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _currentPasswordController.text.length);
|
||||
_currentPasswordFocus.requestFocus();
|
||||
setState(() {
|
||||
_currentIsWrong = true;
|
||||
});
|
||||
@ -72,9 +83,10 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final isValid = _newPassword.isNotEmpty &&
|
||||
final isValid = !_currentIsWrong &&
|
||||
_newPassword.isNotEmpty &&
|
||||
_newPassword == _confirmPassword &&
|
||||
(!widget.state.hasKey || _currentPassword.isNotEmpty);
|
||||
(!widget.state.hasKey || _currentPasswordController.text.isNotEmpty);
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(
|
||||
@ -98,6 +110,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
obscureText: _isObscureCurrent,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
key: keys.currentPasswordField,
|
||||
controller: _currentPasswordController,
|
||||
focusNode: _currentPasswordFocus,
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_current_password,
|
||||
@ -121,21 +135,21 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_currentIsWrong = false;
|
||||
_currentPassword = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
key: keys.removePasswordButton,
|
||||
onPressed: _currentPassword.isNotEmpty
|
||||
onPressed: _currentPasswordController.text.isNotEmpty &&
|
||||
!_currentIsWrong
|
||||
? () async {
|
||||
final result = await ref
|
||||
.read(oathStateProvider(widget.path).notifier)
|
||||
.unsetPassword(_currentPassword);
|
||||
.unsetPassword(_currentPasswordController.text);
|
||||
if (result) {
|
||||
if (mounted) {
|
||||
await ref.read(withContextProvider)(
|
||||
@ -145,6 +159,12 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_currentPasswordController.selection =
|
||||
TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: _currentPasswordController
|
||||
.text.length);
|
||||
_currentPasswordFocus.requestFocus();
|
||||
setState(() {
|
||||
_currentIsWrong = true;
|
||||
});
|
||||
@ -193,7 +213,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
tooltip: _isObscureNew
|
||||
? l10n.s_show_password
|
||||
: l10n.s_hide_password),
|
||||
enabled: !widget.state.hasKey || _currentPassword.isNotEmpty,
|
||||
enabled: !widget.state.hasKey ||
|
||||
_currentPasswordController.text.isNotEmpty,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
@ -206,7 +227,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.confirmPasswordField,
|
||||
obscureText: _isObscureConfirm,
|
||||
@ -227,9 +248,14 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
tooltip: _isObscureConfirm
|
||||
? l10n.s_show_password
|
||||
: l10n.s_hide_password),
|
||||
enabled:
|
||||
(!widget.state.hasKey || _currentPassword.isNotEmpty) &&
|
||||
_newPassword.isNotEmpty,
|
||||
enabled: (!widget.state.hasKey ||
|
||||
_currentPasswordController.text.isNotEmpty) &&
|
||||
_newPassword.isNotEmpty,
|
||||
errorText: _newPassword.length == _confirmPassword.length &&
|
||||
_newPassword != _confirmPassword
|
||||
? l10n.l_password_mismatch
|
||||
: null,
|
||||
helperText: '', // Prevents resizing when errorText shown
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
onChanged: (value) {
|
||||
@ -242,7 +268,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -439,7 +439,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
Focus.of(context)
|
||||
.focusInDirection(TraversalDirection.down);
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
@ -193,7 +193,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
_issuer = value.trim();
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextFormField(
|
||||
initialValue: _name,
|
||||
maxLength: nameRemaining,
|
||||
@ -222,7 +222,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -38,6 +38,7 @@ class UnlockForm extends ConsumerStatefulWidget {
|
||||
|
||||
class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
final _passwordController = TextEditingController();
|
||||
final _passwordFocus = FocusNode();
|
||||
bool _remember = false;
|
||||
bool _passwordIsWrong = false;
|
||||
bool _isObscure = true;
|
||||
@ -51,9 +52,11 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
.unlock(_passwordController.text, remember: _remember);
|
||||
if (!mounted) return;
|
||||
if (!success) {
|
||||
_passwordController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _passwordController.text.length);
|
||||
_passwordFocus.requestFocus();
|
||||
setState(() {
|
||||
_passwordIsWrong = true;
|
||||
_passwordController.clear();
|
||||
});
|
||||
} else if (_remember && !remembered) {
|
||||
showMessage(context, AppLocalizations.of(context)!.l_remember_pw_failed);
|
||||
@ -79,6 +82,7 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
child: AppTextField(
|
||||
key: keys.passwordField,
|
||||
controller: _passwordController,
|
||||
focusNode: _passwordFocus,
|
||||
autofocus: true,
|
||||
obscureText: _isObscure,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
@ -106,7 +110,7 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
_passwordIsWrong = false;
|
||||
}), // Update state on change
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
).init(),
|
||||
),
|
||||
const SizedBox(height: 3.0),
|
||||
Column(
|
||||
@ -143,7 +147,8 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
key: keys.unlockButton,
|
||||
label: Text(l10n.s_unlock),
|
||||
icon: const Icon(Symbols.lock_open),
|
||||
onPressed: _passwordController.text.isNotEmpty
|
||||
onPressed: _passwordController.text.isNotEmpty &&
|
||||
!_passwordIsWrong
|
||||
? _submit
|
||||
: null,
|
||||
),
|
||||
|
@ -166,7 +166,7 @@ class _ConfigureChalrespDialogState
|
||||
_validateSecret = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
FilterChip(
|
||||
label: Text(l10n.s_require_touch),
|
||||
selected: _requireTouch,
|
||||
|
@ -127,6 +127,7 @@ class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
|
||||
key: keys.secretField,
|
||||
controller: _secretController,
|
||||
obscureText: _isObscure,
|
||||
autofocus: true,
|
||||
autofillHints: isAndroid ? [] : const [AutofillHints.password],
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
@ -158,7 +159,7 @@ class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
|
||||
_validateSecret = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4.0,
|
||||
|
@ -181,7 +181,7 @@ class _ConfigureStaticDialogState extends ConsumerState<ConfigureStaticDialog> {
|
||||
_validatePassword = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4.0,
|
||||
|
@ -237,7 +237,7 @@ class _ConfigureYubiOtpDialogState
|
||||
_validatePublicIdFormat = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.privateIdField,
|
||||
controller: _privateIdController,
|
||||
@ -274,7 +274,7 @@ class _ConfigureYubiOtpDialogState
|
||||
_validatePrivateIdFormat = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.secretField,
|
||||
controller: _secretController,
|
||||
@ -311,7 +311,7 @@ class _ConfigureYubiOtpDialogState
|
||||
_validateSecretFormat = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4.0,
|
||||
|
@ -35,8 +35,7 @@ abstract class PivStateNotifier extends ApplicationStateNotifier<PivState> {
|
||||
bool storeKey = false,
|
||||
});
|
||||
|
||||
Future<PinVerificationStatus> verifyPin(
|
||||
String pin); //TODO: Maybe return authenticated?
|
||||
Future<PinVerificationStatus> verifyPin(String pin);
|
||||
Future<PinVerificationStatus> changePin(String pin, String newPin);
|
||||
Future<PinVerificationStatus> changePuk(String puk, String newPuk);
|
||||
Future<PinVerificationStatus> unblockPin(String puk, String newPin);
|
||||
|
@ -52,24 +52,26 @@ class ExportIntent extends Intent {
|
||||
const ExportIntent(this.slot);
|
||||
}
|
||||
|
||||
Future<bool> _authenticate(
|
||||
BuildContext context, DevicePath devicePath, PivState pivState) async {
|
||||
return await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => pivState.protectedKey
|
||||
? PinDialog(devicePath)
|
||||
: AuthenticationDialog(
|
||||
devicePath,
|
||||
pivState,
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
Future<bool> _authIfNeeded(
|
||||
BuildContext context, DevicePath devicePath, PivState pivState) async {
|
||||
Future<bool> _authIfNeeded(BuildContext context, WidgetRef ref,
|
||||
DevicePath devicePath, PivState pivState) async {
|
||||
if (pivState.needsAuth) {
|
||||
return await _authenticate(context, devicePath, pivState);
|
||||
if (pivState.protectedKey &&
|
||||
pivState.metadata?.pinMetadata.defaultValue == true) {
|
||||
final status = await ref
|
||||
.read(pivStateProvider(devicePath).notifier)
|
||||
.verifyPin(defaultPin);
|
||||
return status.when(success: () => true, failure: (_) => false);
|
||||
}
|
||||
return await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => pivState.protectedKey
|
||||
? PinDialog(devicePath)
|
||||
: AuthenticationDialog(
|
||||
devicePath,
|
||||
pivState,
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -96,21 +98,32 @@ class PivActions extends ConsumerWidget {
|
||||
if (hasFeature(features.slotsGenerate))
|
||||
GenerateIntent:
|
||||
CallbackAction<GenerateIntent>(onInvoke: (intent) async {
|
||||
if (!pivState.protectedKey &&
|
||||
!await withContext((context) =>
|
||||
_authIfNeeded(context, devicePath, pivState))) {
|
||||
//Verify management key and maybe PIN
|
||||
if (!await withContext((context) =>
|
||||
_authIfNeeded(context, ref, devicePath, pivState))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify PIN, unless already done above
|
||||
// TODO: Avoid asking for PIN if not needed?
|
||||
final verified = await withContext((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => PinDialog(devicePath))) ??
|
||||
false;
|
||||
if (!pivState.protectedKey) {
|
||||
bool verified;
|
||||
if (pivState.metadata?.pinMetadata.defaultValue == true) {
|
||||
final status = await ref
|
||||
.read(pivStateProvider(devicePath).notifier)
|
||||
.verifyPin(defaultPin);
|
||||
verified =
|
||||
status.when(success: () => true, failure: (_) => false);
|
||||
} else {
|
||||
verified = await withContext((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => PinDialog(devicePath))) ??
|
||||
false;
|
||||
}
|
||||
|
||||
if (!verified) {
|
||||
return false;
|
||||
if (!verified) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return await withContext((context) async {
|
||||
@ -158,8 +171,8 @@ class PivActions extends ConsumerWidget {
|
||||
}),
|
||||
if (hasFeature(features.slotsImport))
|
||||
ImportIntent: CallbackAction<ImportIntent>(onInvoke: (intent) async {
|
||||
if (!await withContext(
|
||||
(context) => _authIfNeeded(context, devicePath, pivState))) {
|
||||
if (!await withContext((context) =>
|
||||
_authIfNeeded(context, ref, devicePath, pivState))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -243,8 +256,8 @@ class PivActions extends ConsumerWidget {
|
||||
if (hasFeature(features.slotsDelete))
|
||||
DeleteIntent<PivSlot>:
|
||||
CallbackAction<DeleteIntent<PivSlot>>(onInvoke: (intent) async {
|
||||
if (!await withContext(
|
||||
(context) => _authIfNeeded(context, devicePath, pivState))) {
|
||||
if (!await withContext((context) =>
|
||||
_authIfNeeded(context, ref, devicePath, pivState))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -44,10 +44,12 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
||||
bool _keyIsWrong = false;
|
||||
bool _keyFormatInvalid = false;
|
||||
final _keyController = TextEditingController();
|
||||
final _keyFocus = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_keyController.dispose();
|
||||
_keyFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -65,7 +67,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
||||
actions: [
|
||||
TextButton(
|
||||
key: keys.unlockButton,
|
||||
onPressed: _keyController.text.length == keyLen
|
||||
onPressed: !_keyIsWrong && _keyController.text.length == keyLen
|
||||
? () async {
|
||||
if (keyFormatInvalid) {
|
||||
setState(() {
|
||||
@ -81,6 +83,10 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
||||
if (status) {
|
||||
navigator.pop(true);
|
||||
} else {
|
||||
_keyController.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: _keyController.text.length);
|
||||
_keyFocus.requestFocus();
|
||||
setState(() {
|
||||
_keyIsWrong = true;
|
||||
});
|
||||
@ -88,6 +94,10 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
||||
} on CancellationException catch (_) {
|
||||
navigator.pop(false);
|
||||
} catch (_) {
|
||||
_keyController.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: _keyController.text.length);
|
||||
_keyFocus.requestFocus();
|
||||
// TODO: More error cases
|
||||
setState(() {
|
||||
_keyIsWrong = true;
|
||||
@ -109,6 +119,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
||||
autofocus: true,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
controller: _keyController,
|
||||
focusNode: _keyFocus,
|
||||
readOnly: _defaultKeyUsed,
|
||||
maxLength: !_defaultKeyUsed ? keyLen : null,
|
||||
decoration: AppInputDecoration(
|
||||
@ -149,7 +160,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
||||
_keyFormatInvalid = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -174,7 +174,7 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
|
||||
_subject = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
Text(
|
||||
l10n.rfc4514_examples,
|
||||
style: subtitleStyle,
|
||||
|
@ -162,7 +162,7 @@ class _ImportFileDialogState extends ConsumerState<ImportFileDialog> {
|
||||
});
|
||||
},
|
||||
onSubmitted: (_) => _examine(),
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -60,7 +60,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
||||
ActionListItem(
|
||||
key: keys.managePinAction,
|
||||
feature: features.actionsPin,
|
||||
title: l10n.s_pin,
|
||||
title: l10n.s_change_pin,
|
||||
subtitle: pinBlocked
|
||||
? (pukAttempts != 0
|
||||
? l10n.l_piv_pin_blocked
|
||||
@ -88,7 +88,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
||||
ActionListItem(
|
||||
key: keys.managePukAction,
|
||||
feature: features.actionsPuk,
|
||||
title: l10n.s_puk,
|
||||
title: l10n.s_change_puk,
|
||||
subtitle: pukAttempts != null
|
||||
? (pukAttempts == 0
|
||||
? l10n.l_piv_pin_puk_blocked
|
||||
|
@ -30,6 +30,7 @@ import '../../widgets/app_text_field.dart';
|
||||
import '../../widgets/app_text_form_field.dart';
|
||||
import '../../widgets/choice_filter_chip.dart';
|
||||
import '../../widgets/responsive_dialog.dart';
|
||||
import '../../widgets/utf8_utils.dart';
|
||||
import '../keys.dart' as keys;
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
@ -48,6 +49,7 @@ class ManageKeyDialog extends ConsumerStatefulWidget {
|
||||
class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
late bool _hasMetadata;
|
||||
late bool _defaultKeyUsed;
|
||||
late bool _defaultPinUsed;
|
||||
late bool _usesStoredKey;
|
||||
late bool _storeKey;
|
||||
bool _currentIsWrong = false;
|
||||
@ -56,6 +58,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
int _attemptsRemaining = -1;
|
||||
late ManagementKeyType _keyType;
|
||||
final _currentController = TextEditingController();
|
||||
final _currentFocus = FocusNode();
|
||||
final _keyController = TextEditingController();
|
||||
bool _isObscure = true;
|
||||
|
||||
@ -68,9 +71,13 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
defaultManagementKeyType;
|
||||
_defaultKeyUsed =
|
||||
widget.pivState.metadata?.managementKeyMetadata.defaultValue ?? false;
|
||||
_defaultPinUsed =
|
||||
widget.pivState.metadata?.pinMetadata.defaultValue ?? false;
|
||||
_usesStoredKey = widget.pivState.protectedKey;
|
||||
if (!_usesStoredKey && _defaultKeyUsed) {
|
||||
_currentController.text = defaultManagementKey;
|
||||
} else if (_usesStoredKey && _defaultPinUsed) {
|
||||
_currentController.text = defaultPin;
|
||||
}
|
||||
_storeKey = _usesStoredKey;
|
||||
}
|
||||
@ -79,16 +86,18 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
void dispose() {
|
||||
_keyController.dispose();
|
||||
_currentController.dispose();
|
||||
_currentFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_submit() async {
|
||||
final currentInvalidFormat = Format.hex.isValid(_currentController.text);
|
||||
final newInvalidFormat = Format.hex.isValid(_keyController.text);
|
||||
if (!currentInvalidFormat || !newInvalidFormat) {
|
||||
final currentValidFormat =
|
||||
_usesStoredKey || Format.hex.isValid(_currentController.text);
|
||||
final newValidFormat = Format.hex.isValid(_keyController.text);
|
||||
if (!currentValidFormat || !newValidFormat) {
|
||||
setState(() {
|
||||
_currentInvalidFormat = !currentInvalidFormat;
|
||||
_newInvalidFormat = !newInvalidFormat;
|
||||
_currentInvalidFormat = !currentValidFormat;
|
||||
_newInvalidFormat = !newValidFormat;
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -98,6 +107,9 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
final status = (await notifier.verifyPin(_currentController.text)).when(
|
||||
success: () => true,
|
||||
failure: (attemptsRemaining) {
|
||||
_currentController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _currentController.text.length);
|
||||
_currentFocus.requestFocus();
|
||||
setState(() {
|
||||
_attemptsRemaining = attemptsRemaining;
|
||||
_currentIsWrong = true;
|
||||
@ -110,6 +122,9 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
}
|
||||
} else {
|
||||
if (!await notifier.authenticate(_currentController.text)) {
|
||||
_currentController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _currentController.text.length);
|
||||
_currentFocus.requestFocus();
|
||||
setState(() {
|
||||
_currentIsWrong = true;
|
||||
});
|
||||
@ -118,15 +133,19 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
}
|
||||
|
||||
if (_storeKey && !_usesStoredKey) {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final verified = await withContext((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => PinDialog(widget.path))) ??
|
||||
false;
|
||||
if (_defaultPinUsed) {
|
||||
await notifier.verifyPin(defaultPin);
|
||||
} else {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final verified = await withContext((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => PinDialog(widget.path))) ??
|
||||
false;
|
||||
|
||||
if (!verified) {
|
||||
return;
|
||||
if (!verified) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,9 +166,8 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
widget.pivState.metadata?.managementKeyMetadata.keyType ??
|
||||
defaultManagementKeyType;
|
||||
final hexLength = _keyType.keyLength * 2;
|
||||
final protected = widget.pivState.protectedKey;
|
||||
final currentKeyOrPin = _currentController.text;
|
||||
final currentLenOk = protected
|
||||
final currentLenOk = _usesStoredKey
|
||||
? currentKeyOrPin.length >= 4
|
||||
: currentKeyOrPin.length == currentType.keyLength * 2;
|
||||
final newLenOk = _keyController.text.length == hexLength;
|
||||
@ -158,7 +176,8 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
title: Text(l10n.l_change_management_key),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: currentLenOk && newLenOk ? _submit : null,
|
||||
onPressed:
|
||||
!_currentIsWrong && currentLenOk && newLenOk ? _submit : null,
|
||||
key: keys.saveButton,
|
||||
child: Text(l10n.s_save),
|
||||
)
|
||||
@ -169,23 +188,25 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.p_change_management_key_desc),
|
||||
if (protected)
|
||||
if (_usesStoredKey)
|
||||
AppTextField(
|
||||
autofocus: true,
|
||||
obscureText: _isObscure,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
key: keys.pinPukField,
|
||||
maxLength: 8,
|
||||
inputFormatters: [limitBytesLength(8)],
|
||||
buildCounter: buildByteCounterFor(_currentController.text),
|
||||
controller: _currentController,
|
||||
focusNode: _currentFocus,
|
||||
readOnly: _defaultPinUsed,
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_pin,
|
||||
helperText: _defaultPinUsed ? l10n.l_default_pin_used : null,
|
||||
errorText: _currentIsWrong
|
||||
? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining)
|
||||
: _currentInvalidFormat
|
||||
? l10n.l_invalid_format_allowed_chars(
|
||||
Format.hex.allowedCharacters)
|
||||
: null,
|
||||
: null,
|
||||
errorMaxLines: 3,
|
||||
prefixIcon: const Icon(Symbols.pin),
|
||||
suffixIcon: IconButton(
|
||||
@ -206,13 +227,14 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
_currentInvalidFormat = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!protected)
|
||||
).init(),
|
||||
if (!_usesStoredKey)
|
||||
AppTextFormField(
|
||||
key: keys.managementKeyField,
|
||||
autofocus: !_defaultKeyUsed,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
controller: _currentController,
|
||||
focusNode: _currentFocus,
|
||||
readOnly: _defaultKeyUsed,
|
||||
maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null,
|
||||
decoration: AppInputDecoration(
|
||||
@ -251,7 +273,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
_currentIsWrong = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.newPinPukField,
|
||||
autofocus: _defaultKeyUsed,
|
||||
@ -299,7 +321,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4.0,
|
||||
|
@ -24,6 +24,7 @@ import '../../app/models.dart';
|
||||
import '../../widgets/app_input_decoration.dart';
|
||||
import '../../widgets/app_text_field.dart';
|
||||
import '../../widgets/responsive_dialog.dart';
|
||||
import '../../widgets/utf8_utils.dart';
|
||||
import '../keys.dart' as keys;
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
@ -44,20 +45,25 @@ class ManagePinPukDialog extends ConsumerStatefulWidget {
|
||||
|
||||
class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
final _currentPinController = TextEditingController();
|
||||
final _currentPinFocus = FocusNode();
|
||||
String _newPin = '';
|
||||
String _confirmPin = '';
|
||||
bool _pinIsBlocked = false;
|
||||
bool _currentIsWrong = false;
|
||||
int _attemptsRemaining = -1;
|
||||
bool _isObscureCurrent = true;
|
||||
bool _isObscureNew = true;
|
||||
bool _isObscureConfirm = true;
|
||||
late bool _defaultPinUsed;
|
||||
late bool _defaultPukUsed;
|
||||
late final bool _defaultPinUsed;
|
||||
late final bool _defaultPukUsed;
|
||||
late final int _minPinLen;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Old YubiKeys allowed a 4 digit PIN
|
||||
_minPinLen = widget.pivState.version.isAtLeast(4, 3, 1) ? 6 : 4;
|
||||
_defaultPinUsed =
|
||||
widget.pivState.metadata?.pinMetadata.defaultValue ?? false;
|
||||
_defaultPukUsed =
|
||||
@ -73,6 +79,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
@override
|
||||
void dispose() {
|
||||
_currentPinController.dispose();
|
||||
_currentPinFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -98,11 +105,16 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
_ => l10n.s_pin_set,
|
||||
});
|
||||
}, failure: (attemptsRemaining) {
|
||||
_currentPinController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _currentPinController.text.length);
|
||||
_currentPinFocus.requestFocus();
|
||||
setState(() {
|
||||
_attemptsRemaining = attemptsRemaining;
|
||||
_currentIsWrong = true;
|
||||
if (_attemptsRemaining == 0) {
|
||||
_pinIsBlocked = true;
|
||||
}
|
||||
});
|
||||
_currentPinController.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@ -110,8 +122,12 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final currentPin = _currentPinController.text;
|
||||
final isValid =
|
||||
_newPin.isNotEmpty && _newPin == _confirmPin && currentPin.isNotEmpty;
|
||||
final currentPinLen = byteLength(currentPin);
|
||||
final newPinLen = byteLength(_newPin);
|
||||
final isValid = !_currentIsWrong &&
|
||||
_newPin.isNotEmpty &&
|
||||
_newPin == _confirmPin &&
|
||||
currentPin.isNotEmpty;
|
||||
|
||||
final titleText = switch (widget.target) {
|
||||
ManageTarget.pin => l10n.s_change_pin,
|
||||
@ -138,7 +154,6 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
//TODO fix string
|
||||
Text(widget.target == ManageTarget.pin
|
||||
? l10n.p_enter_current_pin_or_reset
|
||||
: l10n.p_enter_current_puk_or_reset),
|
||||
@ -146,10 +161,14 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
autofocus: !(showDefaultPinUsed || showDefaultPukUsed),
|
||||
obscureText: _isObscureCurrent,
|
||||
maxLength: 8,
|
||||
inputFormatters: [limitBytesLength(8)],
|
||||
buildCounter: buildByteCounterFor(currentPin),
|
||||
autofillHints: const [AutofillHints.password],
|
||||
key: keys.pinPukField,
|
||||
readOnly: showDefaultPinUsed || showDefaultPukUsed,
|
||||
controller: _currentPinController,
|
||||
focusNode: _currentPinFocus,
|
||||
enabled: !_pinIsBlocked,
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: showDefaultPinUsed
|
||||
@ -160,13 +179,17 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
labelText: widget.target == ManageTarget.pin
|
||||
? l10n.s_current_pin
|
||||
: l10n.s_current_puk,
|
||||
errorText: _currentIsWrong
|
||||
errorText: _pinIsBlocked
|
||||
? (widget.target == ManageTarget.pin
|
||||
? l10n
|
||||
.l_wrong_pin_attempts_remaining(_attemptsRemaining)
|
||||
: l10n
|
||||
.l_wrong_puk_attempts_remaining(_attemptsRemaining))
|
||||
: null,
|
||||
? l10n.l_piv_pin_blocked
|
||||
: l10n.l_piv_pin_puk_blocked)
|
||||
: (_currentIsWrong
|
||||
? (widget.target == ManageTarget.pin
|
||||
? l10n.l_wrong_pin_attempts_remaining(
|
||||
_attemptsRemaining)
|
||||
: l10n.l_wrong_puk_attempts_remaining(
|
||||
_attemptsRemaining))
|
||||
: null),
|
||||
errorMaxLines: 3,
|
||||
prefixIcon: const Icon(Symbols.password),
|
||||
suffixIcon: IconButton(
|
||||
@ -189,7 +212,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
_currentIsWrong = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
Text(l10n.p_enter_new_piv_pin_puk(
|
||||
widget.target == ManageTarget.puk ? l10n.s_puk : l10n.s_pin)),
|
||||
AppTextField(
|
||||
@ -197,6 +220,8 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
autofocus: showDefaultPinUsed || showDefaultPukUsed,
|
||||
obscureText: _isObscureNew,
|
||||
maxLength: 8,
|
||||
inputFormatters: [limitBytesLength(8)],
|
||||
buildCounter: buildByteCounterFor(_newPin),
|
||||
autofillHints: const [AutofillHints.newPassword],
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
@ -217,8 +242,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
? (_isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin)
|
||||
: (_isObscureNew ? l10n.s_show_puk : l10n.s_hide_puk),
|
||||
),
|
||||
// Old YubiKeys allowed a 4 digit PIN
|
||||
enabled: currentPin.length >= 4,
|
||||
enabled: currentPinLen >= _minPinLen,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
@ -231,11 +255,13 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.confirmPinPukField,
|
||||
obscureText: _isObscureConfirm,
|
||||
maxLength: 8,
|
||||
inputFormatters: [limitBytesLength(8)],
|
||||
buildCounter: buildByteCounterFor(_confirmPin),
|
||||
autofillHints: const [AutofillHints.newPassword],
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
@ -256,7 +282,14 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
? (_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin)
|
||||
: (_isObscureConfirm ? l10n.s_show_puk : l10n.s_hide_puk),
|
||||
),
|
||||
enabled: currentPin.length >= 4 && _newPin.length >= 6,
|
||||
enabled: currentPinLen >= _minPinLen && newPinLen >= 6,
|
||||
errorText:
|
||||
newPinLen == _confirmPin.length && _newPin != _confirmPin
|
||||
? (widget.target == ManageTarget.pin
|
||||
? l10n.l_pin_mismatch
|
||||
: l10n.l_puk_mismatch)
|
||||
: null,
|
||||
helperText: '', // Prevents resizing when errorText shown
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
onChanged: (value) {
|
||||
@ -269,7 +302,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
_submit();
|
||||
}
|
||||
},
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -24,6 +24,7 @@ import '../../exception/cancellation_exception.dart';
|
||||
import '../../widgets/app_input_decoration.dart';
|
||||
import '../../widgets/app_text_field.dart';
|
||||
import '../../widgets/responsive_dialog.dart';
|
||||
import '../../widgets/utf8_utils.dart';
|
||||
import '../keys.dart' as keys;
|
||||
import '../state.dart';
|
||||
|
||||
@ -37,6 +38,7 @@ class PinDialog extends ConsumerStatefulWidget {
|
||||
|
||||
class _PinDialogState extends ConsumerState<PinDialog> {
|
||||
final _pinController = TextEditingController();
|
||||
final _pinFocus = FocusNode();
|
||||
bool _pinIsWrong = false;
|
||||
int _attemptsRemaining = -1;
|
||||
bool _isObscure = true;
|
||||
@ -44,6 +46,7 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
||||
@override
|
||||
void dispose() {
|
||||
_pinController.dispose();
|
||||
_pinFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -58,8 +61,10 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
||||
navigator.pop(true);
|
||||
},
|
||||
failure: (attemptsRemaining) {
|
||||
_pinController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _pinController.text.length);
|
||||
_pinFocus.requestFocus();
|
||||
setState(() {
|
||||
_pinController.clear();
|
||||
_attemptsRemaining = attemptsRemaining;
|
||||
_pinIsWrong = true;
|
||||
});
|
||||
@ -73,12 +78,15 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final version = ref.watch(pivStateProvider(widget.devicePath)).valueOrNull;
|
||||
final minPinLen = version?.version.isAtLeast(4, 3, 1) == true ? 6 : 4;
|
||||
final currentPinLen = byteLength(_pinController.text);
|
||||
return ResponsiveDialog(
|
||||
title: Text(l10n.s_pin_required),
|
||||
actions: [
|
||||
TextButton(
|
||||
key: keys.unlockButton,
|
||||
onPressed: _pinController.text.length >= 4 ? _submit : null,
|
||||
onPressed: currentPinLen >= minPinLen ? _submit : null,
|
||||
child: Text(l10n.s_unlock),
|
||||
),
|
||||
],
|
||||
@ -92,9 +100,12 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
||||
autofocus: true,
|
||||
obscureText: _isObscure,
|
||||
maxLength: 8,
|
||||
inputFormatters: [limitBytesLength(8)],
|
||||
buildCounter: buildByteCounterFor(_pinController.text),
|
||||
autofillHints: const [AutofillHints.password],
|
||||
key: keys.managementKeyField,
|
||||
controller: _pinController,
|
||||
focusNode: _pinFocus,
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_pin,
|
||||
@ -121,7 +132,7 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
||||
});
|
||||
},
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
).init(),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'app_input_decoration.dart';
|
||||
@ -86,4 +85,13 @@ class AppTextField extends TextField {
|
||||
super.spellCheckConfiguration,
|
||||
super.magnifierConfiguration,
|
||||
}) : super(decoration: decoration);
|
||||
|
||||
Widget init() => Builder(
|
||||
builder: (context) => DefaultSelectionStyle(
|
||||
selectionColor: decoration?.errorText != null
|
||||
? Theme.of(context).colorScheme.error
|
||||
: null,
|
||||
child: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import 'app_input_decoration.dart';
|
||||
|
||||
/// TextFormField without autocorrect and suggestions
|
||||
class AppTextFormField extends TextFormField {
|
||||
final AppInputDecoration? decoration;
|
||||
AppTextFormField({
|
||||
// default settings to turn off autocorrect
|
||||
super.autocorrect = false,
|
||||
@ -30,7 +31,7 @@ class AppTextFormField extends TextFormField {
|
||||
super.controller,
|
||||
super.initialValue,
|
||||
super.focusNode,
|
||||
AppInputDecoration? decoration,
|
||||
this.decoration,
|
||||
super.textCapitalization,
|
||||
super.textInputAction,
|
||||
super.style,
|
||||
@ -90,4 +91,13 @@ class AppTextFormField extends TextFormField {
|
||||
super.scribbleEnabled,
|
||||
super.canRequestFocus,
|
||||
}) : super(decoration: decoration);
|
||||
|
||||
Widget init() => Builder(
|
||||
builder: (context) => DefaultSelectionStyle(
|
||||
selectionColor: decoration?.errorText != null
|
||||
? Theme.of(context).colorScheme.error
|
||||
: null,
|
||||
child: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'package:analyzer/dart/ast/token.dart';
|
||||
import 'package:analyzer/error/listener.dart';
|
||||
import 'package:custom_lint_builder/custom_lint_builder.dart';
|
||||
|
||||
@ -30,6 +31,8 @@ class _AppLinter extends PluginBase {
|
||||
discouraged: 'TextFormField',
|
||||
recommended: 'AppTextFormField',
|
||||
),
|
||||
const CallInitAfterCreation(className: 'AppTextField'),
|
||||
const CallInitAfterCreation(className: 'AppTextFormField'),
|
||||
];
|
||||
}
|
||||
|
||||
@ -59,3 +62,35 @@ class UseRecommendedWidget extends DartLintRule {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CallInitAfterCreation extends DartLintRule {
|
||||
final String className;
|
||||
|
||||
const CallInitAfterCreation({required this.className})
|
||||
: super(
|
||||
code: const LintCode(
|
||||
name: 'call_init_after_creation',
|
||||
problemMessage: 'Call init() after creation',
|
||||
));
|
||||
|
||||
@override
|
||||
void run(
|
||||
CustomLintResolver resolver,
|
||||
ErrorReporter reporter,
|
||||
CustomLintContext context,
|
||||
) {
|
||||
context.registry.addInstanceCreationExpression((node) {
|
||||
if (node.constructorName.toString() == className) {
|
||||
final dot = node.endToken.next;
|
||||
final next = dot?.next;
|
||||
if (dot?.type == TokenType.PERIOD) {
|
||||
if (next?.type == TokenType.IDENTIFIER &&
|
||||
next?.toString() == 'init') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
reporter.reportErrorForNode(code, node.constructorName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user