mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 02:01:36 +03:00
Enhance error handling in OTP.
This commit is contained in:
parent
23ebcb6955
commit
63bb18b2be
@ -89,7 +89,7 @@ extension on Application {
|
||||
IconData get _icon => switch (this) {
|
||||
Application.oath => Icons.supervisor_account_outlined,
|
||||
Application.fido => Icons.security_outlined,
|
||||
Application.otp => Icons.password_outlined,
|
||||
Application.otp => Icons.touch_app_outlined,
|
||||
Application.piv => Icons.approval_outlined,
|
||||
Application.management => Icons.construction_outlined,
|
||||
Application.openpgp => Icons.key_outlined,
|
||||
@ -99,7 +99,7 @@ extension on Application {
|
||||
IconData get _filledIcon => switch (this) {
|
||||
Application.oath => Icons.supervisor_account,
|
||||
Application.fido => Icons.security,
|
||||
Application.otp => Icons.password,
|
||||
Application.otp => Icons.touch_app,
|
||||
Application.piv => Icons.approval,
|
||||
Application.management => Icons.construction,
|
||||
Application.openpgp => Icons.key,
|
||||
|
@ -155,3 +155,18 @@ class Version with _$Version implements Comparable<Version> {
|
||||
}
|
||||
|
||||
final DateFormat dateFormatter = DateFormat('yyyy-MM-dd');
|
||||
|
||||
enum Format {
|
||||
base32('a-z2-7'),
|
||||
hex('abcdef0123456789'),
|
||||
modhex('cbdefghijklnrtuv');
|
||||
|
||||
final String allowedCharacters;
|
||||
|
||||
const Format(this.allowedCharacters);
|
||||
|
||||
bool isValid(String input) {
|
||||
return RegExp('^[$allowedCharacters]+\$', caseSensitive: false)
|
||||
.hasMatch(input);
|
||||
}
|
||||
}
|
||||
|
@ -61,9 +61,9 @@
|
||||
"s_manage": "Manage",
|
||||
"s_setup": "Setup",
|
||||
"s_settings": "Settings",
|
||||
"s_piv": "PIV",
|
||||
"s_piv": "Certificates",
|
||||
"s_webauthn": "WebAuthn",
|
||||
"s_otp": "OTP",
|
||||
"s_otp": "Slots",
|
||||
"s_help_and_about": "Help and about",
|
||||
"s_help_and_feedback": "Help and feedback",
|
||||
"s_send_feedback": "Send us feedback",
|
||||
@ -80,6 +80,13 @@
|
||||
"s_private_key": "Private key",
|
||||
"s_invalid_length": "Invalid length",
|
||||
"s_invalid_format": "Invalid format",
|
||||
"l_invalid_format_allowed_chars": "Invalid format, allowed characters: {characters}",
|
||||
"@l_invalid_format_allowed_chars": {
|
||||
"placeholders": {
|
||||
"characters": {}
|
||||
}
|
||||
},
|
||||
"l_invalid_keyboard_character": "Invalid characters for selected keyboard",
|
||||
"s_require_touch": "Require touch",
|
||||
"q_have_account_info": "Have account info?",
|
||||
"s_run_diagnostics": "Run diagnostics",
|
||||
@ -508,27 +515,20 @@
|
||||
|
||||
"@_otp_slots": {},
|
||||
"s_otp_slots": "Slots",
|
||||
"s_otp_slot_display_name": "{name} (Slot {id})",
|
||||
"@s_otp_slot_display_name" : {
|
||||
"placeholders": {
|
||||
"name": {},
|
||||
"id": {}
|
||||
}
|
||||
},
|
||||
"s_otp_slot_one": "Short touch",
|
||||
"s_otp_slot_two": "Long touch",
|
||||
"l_otp_slot_not_programmed": "Slot is empty",
|
||||
"l_otp_slot_programmed": "Slot is programmed",
|
||||
"l_otp_slot_empty": "Slot is empty",
|
||||
"l_otp_slot_configured": "Slot is configured",
|
||||
|
||||
"@_otp_slot_configurations": {},
|
||||
"s_yubiotp": "Yubico OTP",
|
||||
"l_yubiotp_desc": "YubiCloud verified OTP's",
|
||||
"l_yubiotp_desc": "Program a Yubico OTP credential",
|
||||
"s_challenge_response": "Challenge-response",
|
||||
"l_challenge_response_desc": "Show me yours and I'll show you mine",
|
||||
"l_challenge_response_desc": "Program a challenge-response credential",
|
||||
"s_static_password": "Static password",
|
||||
"l_static_password_desc": "Use complex passwords with a touch",
|
||||
"l_static_password_desc": "Configure a static password",
|
||||
"s_hotp": "OATH-HOTP",
|
||||
"l_hotp_desc": "HMAC based OTP's",
|
||||
"l_hotp_desc": "Program a HMAC-SHA1 based credential",
|
||||
"s_public_id": "Public ID",
|
||||
"s_private_id": "Private ID",
|
||||
"s_allow_any_character": "Allow any character",
|
||||
@ -538,34 +538,34 @@
|
||||
"s_generate_passowrd": "Generate password",
|
||||
|
||||
"@_otp_slot_actions": {},
|
||||
"s_delete_slot": "Delete slot",
|
||||
"l_delete_slot_desc": "Delete configuration in slot",
|
||||
"p_warning_delete_slot_configuration": "Warning! This action will delete the configuration in OTP slot {slot}.",
|
||||
"s_delete_slot": "Delete credential",
|
||||
"l_delete_slot_desc": "Remove credential in slot",
|
||||
"p_warning_delete_slot_configuration": "Warning! This action will permanently remove the credential from slot {slot_id}.",
|
||||
"@p_warning_delete_slot_configuration": {
|
||||
"placeholders": {
|
||||
"slot": {}
|
||||
"slot_id": {}
|
||||
}
|
||||
},
|
||||
"l_slot_deleted": "Slot configuration deleted",
|
||||
"l_slot_deleted": "Credential deleted",
|
||||
"s_swap": "Swap",
|
||||
"s_swap_slots": "Swap slots",
|
||||
"l_swap_slots_desc": "Swap short/long touch",
|
||||
"p_swap_slots_desc": "This will swap the configuration of the two slots.",
|
||||
"l_slots_swapped": "Slot configurations swapped",
|
||||
"l_slot_configuration_programmed": "Configured {type} credential",
|
||||
"@l_slot_configuration_programmed": {
|
||||
"l_slot_credential_configured": "Configured {type} credential",
|
||||
"@l_slot_credential_configured": {
|
||||
"placeholders": {
|
||||
"type": {}
|
||||
}
|
||||
},
|
||||
"l_slot_configuration_programmed_and_exported": "Configured {type} credential and exported to {file}",
|
||||
"@l_slot_configuration_programmed_and_exported": {
|
||||
"l_slot_credential_configured_and_exported": "Configured {type} credential and exported to {file}",
|
||||
"@l_slot_credential_configured_and_exported": {
|
||||
"placeholders": {
|
||||
"type": {},
|
||||
"file": {}
|
||||
}
|
||||
},
|
||||
"s_append_enter": "Append enter",
|
||||
"s_append_enter": "Append ⏎",
|
||||
"l_append_enter_desc": "Append an Enter keystroke after emitting the OTP",
|
||||
|
||||
"@_otp_errors": {},
|
||||
@ -576,6 +576,7 @@
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
"@_permissions": {},
|
||||
"s_enable_nfc": "Enable NFC",
|
||||
"s_permission_denied": "Permission denied",
|
||||
|
@ -29,10 +29,9 @@ enum SlotId {
|
||||
const SlotId(this.id, this.numberId);
|
||||
|
||||
String getDisplayName(AppLocalizations l10n) {
|
||||
String nameFor(String name) => l10n.s_otp_slot_display_name(name, numberId);
|
||||
return switch (this) {
|
||||
SlotId.one => nameFor(l10n.s_otp_slot_one),
|
||||
SlotId.two => nameFor(l10n.s_otp_slot_two)
|
||||
SlotId.one => l10n.s_otp_slot_one,
|
||||
SlotId.two => l10n.s_otp_slot_two
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -17,10 +17,10 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:yubico_authenticator/app/logging.dart';
|
||||
import 'package:yubico_authenticator/core/models.dart';
|
||||
import 'package:yubico_authenticator/core/state.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
@ -49,6 +49,7 @@ class _ConfigureChalrespDialogState
|
||||
extends ConsumerState<ConfigureChalrespDialog> {
|
||||
final _secretController = TextEditingController();
|
||||
bool _validateSecretLength = false;
|
||||
bool _validateSecretFormat = false;
|
||||
bool _requireTouch = false;
|
||||
final int secretMaxLength = 40;
|
||||
|
||||
@ -62,9 +63,11 @@ class _ConfigureChalrespDialogState
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
final secret = _secretController.text.replaceAll(' ', '');
|
||||
final secretLengthValid =
|
||||
secret.isNotEmpty && secret.length <= secretMaxLength;
|
||||
final secret = _secretController.text;
|
||||
final secretLengthValid = secret.isNotEmpty &&
|
||||
secret.length % 2 == 0 &&
|
||||
secret.length <= secretMaxLength;
|
||||
final secretFormatValid = Format.hex.isValid(secret);
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(l10n.s_challenge_response),
|
||||
@ -79,6 +82,12 @@ class _ConfigureChalrespDialogState
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!secretFormatValid) {
|
||||
setState(() {
|
||||
_validateSecretFormat = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await confirmOverwrite(context, widget.otpSlot)) {
|
||||
return;
|
||||
@ -94,8 +103,10 @@ class _ConfigureChalrespDialogState
|
||||
requireTouch: _requireTouch)));
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context,
|
||||
l10n.l_slot_configuration_programmed(l10n.s_hotp));
|
||||
showMessage(
|
||||
context,
|
||||
l10n.l_slot_credential_configured(
|
||||
l10n.s_challenge_response));
|
||||
});
|
||||
} catch (e) {
|
||||
_log.error('Failed to program credential', e);
|
||||
@ -125,10 +136,13 @@ class _ConfigureChalrespDialogState
|
||||
autofillHints: isAndroid ? [] : const [AutofillHints.password],
|
||||
maxLength: secretMaxLength,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
tooltip: l10n.s_generate_secret_key,
|
||||
suffixIcon: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
final random = Random.secure();
|
||||
final key = List.generate(
|
||||
20,
|
||||
@ -139,22 +153,31 @@ class _ConfigureChalrespDialogState
|
||||
setState(() {
|
||||
_secretController.text = key;
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_validateSecretLength || _validateSecretFormat) ...[
|
||||
const Icon(Icons.error_outlined),
|
||||
const SizedBox(
|
||||
width: 8.0,
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.key_outlined),
|
||||
labelText: l10n.s_secret_key,
|
||||
errorText: _validateSecretLength && !secretLengthValid
|
||||
? l10n.s_invalid_length
|
||||
: _validateSecretFormat && !secretFormatValid
|
||||
? l10n.l_invalid_format_allowed_chars(
|
||||
Format.hex.allowedCharacters)
|
||||
: null),
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp('[a-f0-9]', caseSensitive: false))
|
||||
],
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_validateSecretLength = false;
|
||||
_validateSecretFormat = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
@ -15,10 +15,10 @@
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:yubico_authenticator/app/logging.dart';
|
||||
import 'package:yubico_authenticator/core/models.dart';
|
||||
import 'package:yubico_authenticator/core/state.dart';
|
||||
import 'package:yubico_authenticator/oath/models.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@ -48,6 +48,7 @@ class ConfigureHotpDialog extends ConsumerStatefulWidget {
|
||||
class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
|
||||
final _secretController = TextEditingController();
|
||||
bool _validateSecretLength = false;
|
||||
bool _validateSecretFormat = false;
|
||||
int _digits = defaultDigits;
|
||||
final List<int> _digitsValues = [6, 8];
|
||||
bool _appendEnter = true;
|
||||
@ -65,6 +66,7 @@ class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
|
||||
|
||||
final secret = _secretController.text.replaceAll(' ', '');
|
||||
final secretLengthValid = secret.isNotEmpty && secret.length * 5 % 8 < 5;
|
||||
final secretFormatValid = Format.base32.isValid(secret);
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(l10n.s_hotp),
|
||||
@ -79,6 +81,12 @@ class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!secretFormatValid) {
|
||||
setState(() {
|
||||
_validateSecretFormat = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await confirmOverwrite(context, widget.otpSlot)) {
|
||||
return;
|
||||
@ -96,7 +104,7 @@ class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context,
|
||||
l10n.l_slot_configuration_programmed(l10n.s_hotp));
|
||||
l10n.l_slot_credential_configured(l10n.s_hotp));
|
||||
});
|
||||
} catch (e) {
|
||||
_log.error('Failed to program credential', e);
|
||||
@ -124,42 +132,66 @@ class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
|
||||
controller: _secretController,
|
||||
obscureText: _isObscure,
|
||||
autofillHints: isAndroid ? [] : const [AutofillHints.password],
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.allow(RegExp(
|
||||
'[abcdefghijklmnopqrstuvwxyz234567 ]',
|
||||
caseSensitive: false))
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
suffixIcon: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_isObscure ? Icons.visibility : Icons.visibility_off,
|
||||
color: IconTheme.of(context).color,
|
||||
),
|
||||
_isObscure
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: !(_validateSecretLength ||
|
||||
_validateSecretFormat)
|
||||
? IconTheme.of(context).color
|
||||
: null),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isObscure = !_isObscure;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_validateSecretLength || _validateSecretFormat) ...[
|
||||
const Icon(Icons.error_outlined),
|
||||
const SizedBox(
|
||||
width: 8.0,
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.key_outlined),
|
||||
labelText: l10n.s_secret_key,
|
||||
helperText: '', // Prevents resizing when errorText shown
|
||||
errorText: _validateSecretLength && !secretLengthValid
|
||||
? l10n.s_invalid_length
|
||||
: _validateSecretFormat && !secretFormatValid
|
||||
? l10n.l_invalid_format_allowed_chars(
|
||||
Format.base32.allowedCharacters)
|
||||
: null),
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_validateSecretLength = false;
|
||||
_validateSecretFormat = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
FilterChip(
|
||||
label: Text(l10n.s_append_enter),
|
||||
tooltip: l10n.l_append_enter_desc,
|
||||
selected: _appendEnter,
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
_appendEnter = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
ChoiceFilterChip<int>(
|
||||
items: _digitsValues,
|
||||
value: _digits,
|
||||
@ -170,16 +202,6 @@ class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
|
||||
_digits = digits;
|
||||
});
|
||||
}),
|
||||
FilterChip(
|
||||
label: Text(l10n.s_append_enter),
|
||||
tooltip: l10n.l_append_enter_desc,
|
||||
selected: _appendEnter,
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
_appendEnter = value;
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
|
@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:yubico_authenticator/app/logging.dart';
|
||||
@ -69,13 +68,13 @@ class _ConfigureStaticDialogState extends ConsumerState<ConfigureStaticDialog> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String generateFormatterPattern(String layout) {
|
||||
RegExp generateFormatterPattern(String layout) {
|
||||
final allowedCharacters = widget.keyboardLayouts[layout] ?? [];
|
||||
|
||||
final pattern =
|
||||
allowedCharacters.map((char) => RegExp.escape(char)).join('');
|
||||
|
||||
return '[$pattern]';
|
||||
return RegExp('^[$pattern]+\$', caseSensitive: false);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -85,10 +84,8 @@ class _ConfigureStaticDialogState extends ConsumerState<ConfigureStaticDialog> {
|
||||
final password = _passwordController.text.replaceAll(' ', '');
|
||||
final passwordLengthValid =
|
||||
password.isNotEmpty && password.length <= passwordMaxLength;
|
||||
|
||||
final layoutPattern = generateFormatterPattern(_keyboardLayout);
|
||||
final regex = RegExp('^$layoutPattern', caseSensitive: false);
|
||||
final passwordFormatValid = regex.hasMatch(password);
|
||||
final passwordFormatValid =
|
||||
generateFormatterPattern(_keyboardLayout).hasMatch(password);
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(l10n.s_static_password),
|
||||
@ -121,7 +118,7 @@ class _ConfigureStaticDialogState extends ConsumerState<ConfigureStaticDialog> {
|
||||
Navigator.of(context).pop();
|
||||
showMessage(
|
||||
context,
|
||||
l10n.l_slot_configuration_programmed(
|
||||
l10n.l_slot_credential_configured(
|
||||
l10n.s_static_password));
|
||||
});
|
||||
} catch (e) {
|
||||
@ -152,19 +149,32 @@ class _ConfigureStaticDialogState extends ConsumerState<ConfigureStaticDialog> {
|
||||
autofillHints: isAndroid ? [] : const [AutofillHints.password],
|
||||
maxLength: passwordMaxLength,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
suffixIcon: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: l10n.s_generate_passowrd,
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () async {
|
||||
final password = await ref
|
||||
.read(otpStateProvider(widget.devicePath).notifier)
|
||||
.read(
|
||||
otpStateProvider(widget.devicePath).notifier)
|
||||
.generateStaticPassword(
|
||||
passwordMaxLength, _keyboardLayout);
|
||||
setState(() {
|
||||
_validatePassword = false;
|
||||
_passwordController.text = password;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_validatePassword) ...[
|
||||
const Icon(Icons.error_outlined),
|
||||
const SizedBox(
|
||||
width: 8.0,
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.key_outlined),
|
||||
labelText: l10n.s_password,
|
||||
@ -175,13 +185,8 @@ class _ConfigureStaticDialogState extends ConsumerState<ConfigureStaticDialog> {
|
||||
: _validatePassword &&
|
||||
passwordLengthValid &&
|
||||
!passwordFormatValid
|
||||
? l10n.s_invalid_format
|
||||
? l10n.l_invalid_keyboard_character
|
||||
: null),
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.allow(RegExp(
|
||||
generateFormatterPattern(_keyboardLayout),
|
||||
caseSensitive: false))
|
||||
],
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
@ -194,17 +199,6 @@ class _ConfigureStaticDialogState extends ConsumerState<ConfigureStaticDialog> {
|
||||
spacing: 4.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
ChoiceFilterChip(
|
||||
items: widget.keyboardLayouts.keys.toList(),
|
||||
value: _keyboardLayout,
|
||||
selected: _keyboardLayout != _defaultKeyboardLayout,
|
||||
itemBuilder: (value) => Text(value),
|
||||
onChanged: (layout) {
|
||||
setState(() {
|
||||
_keyboardLayout = layout;
|
||||
_validatePassword = false;
|
||||
});
|
||||
}),
|
||||
FilterChip(
|
||||
label: Text(l10n.s_append_enter),
|
||||
tooltip: l10n.l_append_enter_desc,
|
||||
@ -214,7 +208,19 @@ class _ConfigureStaticDialogState extends ConsumerState<ConfigureStaticDialog> {
|
||||
_appendEnter = value;
|
||||
});
|
||||
},
|
||||
)
|
||||
),
|
||||
ChoiceFilterChip(
|
||||
items: widget.keyboardLayouts.keys.toList(),
|
||||
value: _keyboardLayout,
|
||||
selected: _keyboardLayout != _defaultKeyboardLayout,
|
||||
labelBuilder: (value) => Text('Keyboard $value'),
|
||||
itemBuilder: (value) => Text(value),
|
||||
onChanged: (layout) {
|
||||
setState(() {
|
||||
_keyboardLayout = layout;
|
||||
_validatePassword = false;
|
||||
});
|
||||
}),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
@ -19,10 +19,10 @@ import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:yubico_authenticator/app/logging.dart';
|
||||
import 'package:yubico_authenticator/core/models.dart';
|
||||
import 'package:yubico_authenticator/core/state.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:yubico_authenticator/widgets/choice_filter_chip.dart';
|
||||
@ -38,8 +38,6 @@ import 'overwrite_confirm_dialog.dart';
|
||||
|
||||
final _log = Logger('otp.view.configure_yubiotp_dialog');
|
||||
|
||||
final _modhexPattern = RegExp('[cbdefghijklnrtuv]', caseSensitive: false);
|
||||
|
||||
enum OutputActions {
|
||||
selectFile,
|
||||
noOutput;
|
||||
@ -48,7 +46,7 @@ enum OutputActions {
|
||||
|
||||
String getDisplayName(AppLocalizations l10n) => switch (this) {
|
||||
OutputActions.selectFile => 'Select file',
|
||||
OutputActions.noOutput => 'No output'
|
||||
OutputActions.noOutput => 'No export file'
|
||||
};
|
||||
}
|
||||
|
||||
@ -67,11 +65,14 @@ class _ConfigureYubiOtpDialogState
|
||||
final _secretController = TextEditingController();
|
||||
final _publicIdController = TextEditingController();
|
||||
final _privateIdController = TextEditingController();
|
||||
OutputActions _action = OutputActions.noOutput;
|
||||
bool _appendEnter = true;
|
||||
bool _validateSecretFormat = false;
|
||||
bool _validatePublicIdFormat = false;
|
||||
bool _validatePrivateIdFormat = false;
|
||||
final secretLength = 32;
|
||||
final publicIdLength = 12;
|
||||
final privateIdLength = 12;
|
||||
OutputActions _action = OutputActions.selectFile;
|
||||
bool _appendEnter = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@ -87,16 +88,19 @@ class _ConfigureYubiOtpDialogState
|
||||
|
||||
final info = ref.watch(currentDeviceDataProvider).valueOrNull?.info;
|
||||
|
||||
final secret = _secretController.text.replaceAll(' ', '');
|
||||
final secret = _secretController.text;
|
||||
final secretLengthValid = secret.length == secretLength;
|
||||
final secretFormatValid = Format.hex.isValid(secret);
|
||||
|
||||
final privateId = _privateIdController.text;
|
||||
final privateIdLengthValid = privateId.length == privateIdLength;
|
||||
final privatedIdFormatValid = Format.hex.isValid(privateId);
|
||||
|
||||
final publicId = _publicIdController.text;
|
||||
final publicIdLengthValid = publicId.length == publicIdLength;
|
||||
final publicIdFormatValid = Format.modhex.isValid(publicId);
|
||||
|
||||
final isValid =
|
||||
final lengthsValid =
|
||||
secretLengthValid && privateIdLengthValid && publicIdLengthValid;
|
||||
|
||||
final outputFile = ref.read(yubiOtpOutputProvider);
|
||||
@ -121,8 +125,19 @@ class _ConfigureYubiOtpDialogState
|
||||
actions: [
|
||||
TextButton(
|
||||
key: keys.saveButton,
|
||||
onPressed: isValid
|
||||
onPressed: lengthsValid
|
||||
? () async {
|
||||
if (!secretFormatValid ||
|
||||
!publicIdFormatValid ||
|
||||
!privatedIdFormatValid) {
|
||||
setState(() {
|
||||
_validateSecretFormat = !secretFormatValid;
|
||||
_validatePublicIdFormat = !publicIdFormatValid;
|
||||
_validatePrivateIdFormat = !privatedIdFormatValid;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await confirmOverwrite(context, widget.otpSlot)) {
|
||||
return;
|
||||
}
|
||||
@ -150,11 +165,10 @@ class _ConfigureYubiOtpDialogState
|
||||
showMessage(
|
||||
context,
|
||||
outputFile != null
|
||||
? l10n
|
||||
.l_slot_configuration_programmed_and_exported(
|
||||
? l10n.l_slot_credential_configured_and_exported(
|
||||
l10n.s_yubiotp,
|
||||
outputFile.uri.pathSegments.last)
|
||||
: l10n.l_slot_configuration_programmed(
|
||||
: l10n.l_slot_credential_configured(
|
||||
l10n.s_yubiotp));
|
||||
});
|
||||
} catch (e) {
|
||||
@ -191,7 +205,10 @@ class _ConfigureYubiOtpDialogState
|
||||
autofillHints: isAndroid ? [] : const [AutofillHints.password],
|
||||
maxLength: publicIdLength,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
suffixIcon: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: l10n.s_use_serial,
|
||||
icon: const Icon(Icons.auto_awesome_outlined),
|
||||
onPressed: (info?.serial != null)
|
||||
@ -206,16 +223,25 @@ class _ConfigureYubiOtpDialogState
|
||||
}
|
||||
: null,
|
||||
),
|
||||
if (_validatePublicIdFormat) ...[
|
||||
const Icon(Icons.error_outlined),
|
||||
const SizedBox(
|
||||
width: 8.0,
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.public_outlined),
|
||||
errorText: _validatePublicIdFormat && !publicIdFormatValid
|
||||
? l10n.l_invalid_format_allowed_chars(
|
||||
Format.modhex.allowedCharacters)
|
||||
: null,
|
||||
labelText: l10n.s_public_id),
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.allow(_modhexPattern)
|
||||
],
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
// Update lengths
|
||||
_validatePublicIdFormat = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -225,7 +251,10 @@ class _ConfigureYubiOtpDialogState
|
||||
autofillHints: isAndroid ? [] : const [AutofillHints.password],
|
||||
maxLength: privateIdLength,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
suffixIcon: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: l10n.s_generate_private_id,
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
@ -241,17 +270,25 @@ class _ConfigureYubiOtpDialogState
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_validatePrivateIdFormat) ...[
|
||||
const Icon(Icons.error_outlined),
|
||||
const SizedBox(
|
||||
width: 8.0,
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.key_outlined),
|
||||
errorText: _validatePrivateIdFormat && !privatedIdFormatValid
|
||||
? l10n.l_invalid_format_allowed_chars(
|
||||
Format.hex.allowedCharacters)
|
||||
: null,
|
||||
labelText: l10n.s_private_id),
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp('[a-f0-9]', caseSensitive: false))
|
||||
],
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
// Update lengths
|
||||
_validatePrivateIdFormat = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -261,7 +298,10 @@ class _ConfigureYubiOtpDialogState
|
||||
autofillHints: isAndroid ? [] : const [AutofillHints.password],
|
||||
maxLength: secretLength,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
suffixIcon: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: l10n.s_generate_secret_key,
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
@ -277,17 +317,25 @@ class _ConfigureYubiOtpDialogState
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_validateSecretFormat) ...[
|
||||
const Icon(Icons.error_outlined),
|
||||
const SizedBox(
|
||||
width: 8.0,
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.key_outlined),
|
||||
errorText: _validateSecretFormat && !secretFormatValid
|
||||
? l10n.l_invalid_format_allowed_chars(
|
||||
Format.hex.allowedCharacters)
|
||||
: null,
|
||||
labelText: l10n.s_secret_key),
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp('[a-f0-9]', caseSensitive: false))
|
||||
],
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
// Update lengths
|
||||
_validateSecretFormat = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -321,7 +369,9 @@ class _ConfigureYubiOtpDialogState
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 140),
|
||||
child: Text(
|
||||
'Output ${fileName ?? 'No output'}',
|
||||
fileName != null
|
||||
? 'Export $fileName'
|
||||
: _action.getDisplayName(l10n),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
|
@ -35,7 +35,7 @@ class DeleteSlotDialog extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return ResponsiveDialog(
|
||||
title: Text(l10n.l_delete_certificate),
|
||||
title: Text(l10n.s_delete_slot),
|
||||
actions: [
|
||||
TextButton(
|
||||
key: keys.deleteButton,
|
||||
@ -68,8 +68,8 @@ class DeleteSlotDialog extends ConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.p_warning_delete_slot_configuration(
|
||||
otpSlot.slot.getDisplayName(l10n))),
|
||||
Text(l10n
|
||||
.p_warning_delete_slot_configuration(otpSlot.slot.numberId)),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
|
@ -97,11 +97,10 @@ class _SlotListItem extends ConsumerWidget {
|
||||
leading: CircleAvatar(
|
||||
foregroundColor: colorScheme.onSecondary,
|
||||
backgroundColor: colorScheme.secondary,
|
||||
child: const Icon(Icons.password_outlined)),
|
||||
child: Text(slot.numberId.toString())),
|
||||
title: slot.getDisplayName(l10n),
|
||||
subtitle: isConfigured
|
||||
? l10n.l_otp_slot_programmed
|
||||
: l10n.l_otp_slot_not_programmed,
|
||||
subtitle:
|
||||
isConfigured ? l10n.l_otp_slot_configured : l10n.l_otp_slot_empty,
|
||||
trailing: OutlinedButton(
|
||||
onPressed: Actions.handler(context, const OpenIntent()),
|
||||
child: const Icon(Icons.more_horiz),
|
||||
|
@ -72,13 +72,17 @@ class SlotDialog extends ConsumerWidget {
|
||||
const Icon(
|
||||
Icons.touch_app,
|
||||
size: 100.0,
|
||||
)
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(otpSlot.isConfigured
|
||||
? l10n.l_otp_slot_configured
|
||||
: l10n.l_otp_slot_empty)
|
||||
],
|
||||
),
|
||||
),
|
||||
ActionListSection.fromMenuActions(
|
||||
context,
|
||||
l10n.s_actions,
|
||||
l10n.s_setup,
|
||||
actions: buildSlotActions(otpSlot.isConfigured, l10n),
|
||||
)
|
||||
],
|
||||
|
Loading…
Reference in New Issue
Block a user