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