From 06372442dbfd2404d3dee9900c68fec063954052 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Thu, 9 Nov 2023 13:39:58 +0100 Subject: [PATCH 01/27] Add helper utility function for OTP. --- helper/helper/yubiotp.py | 113 +++++++++++++++++++++++++++++---------- 1 file changed, 84 insertions(+), 29 deletions(-) diff --git a/helper/helper/yubiotp.py b/helper/helper/yubiotp.py index ffd0fd2b..1df0526c 100644 --- a/helper/helper/yubiotp.py +++ b/helper/helper/yubiotp.py @@ -14,7 +14,8 @@ from .base import RpcNode, action, child -from yubikit.core import NotSupportedError +from yubikit.core import NotSupportedError, CommandError +from yubikit.core.otp import modhex_encode, modhex_decode from yubikit.yubiotp import ( YubiOtpSession, SLOT, @@ -25,8 +26,17 @@ from yubikit.yubiotp import ( YubiOtpSlotConfiguration, StaticTicketSlotConfiguration, ) -from typing import Dict +from ykman.otp import generate_static_pw +from yubikit.oath import parse_b32_key +from ykman.scancodes import KEYBOARD_LAYOUT, encode +from typing import Dict +import struct + +_FAIL_MSG = ( + "Failed to write to the YubiKey. Make sure the device does not " + "have restricted access" +) class YubiOtpNode(RpcNode): def __init__(self, connection): @@ -65,6 +75,23 @@ class YubiOtpNode(RpcNode): def two(self): return SlotNode(self.session, SLOT.TWO) + @action(closes_child=False) + def serial_modhex(self, params, event, signal): + serial = params["serial"] + return dict( + encoded=modhex_encode(b"\xff\x00" + struct.pack(b">I", serial)) + ) + + @action(closes_child=False) + def generate_static(self, params, event, signal): + layout, length = params["layout"], int(params["length"]) + return dict( + password=generate_static_pw(length, KEYBOARD_LAYOUT[layout]) + ) + + @action(closes_child=False) + def keyboard_layouts(self, params, event, signal): + return {layout.name:[sc for sc in layout.value] for layout in KEYBOARD_LAYOUT} _CONFIG_TYPES = dict( hmac_sha1=HmacSha1SlotConfiguration, @@ -113,7 +140,10 @@ class SlotNode(RpcNode): @action(condition=lambda self: self._maybe_configured(self.slot)) def delete(self, params, event, signal): - self.session.delete_slot(self.slot, params.pop("cur_acc_code", None)) + try: + self.session.delete_slot(self.slot, params.pop("cur_acc_code", None)) + except CommandError: + raise ValueError(_FAIL_MSG) @action(condition=lambda self: self._can_calculate(self.slot)) def calculate(self, params, event, signal): @@ -121,7 +151,7 @@ class SlotNode(RpcNode): response = self.session.calculate_hmac_sha1(self.slot, challenge, event) return dict(response=response) - def _apply_config(self, config, params): + def _apply_options(self, config, options): for option in ( "serial_api_visible", "serial_usb_visible", @@ -140,39 +170,63 @@ class SlotNode(RpcNode): "short_ticket", "manual_update", ): - if option in params: - getattr(config, option)(params.pop(option)) + if option in options: + getattr(config, option)(options.pop(option)) for option in ("tabs", "delay", "pacing", "strong_password"): - if option in params: - getattr(config, option)(*params.pop(option)) + if option in options: + getattr(config, option)(*options.pop(option)) - if "token_id" in params: - token_id, *args = params.pop("token_id") + if "token_id" in options: + token_id, *args = options.pop("token_id") config.token_id(bytes.fromhex(token_id), *args) return config + def _get_config(self, type, **kwargs): + config = None + + if type in _CONFIG_TYPES: + if type == "hmac_sha1": + config = _CONFIG_TYPES[type]( + bytes.fromhex(kwargs["key"]) + ) + elif type == "hotp": + config = _CONFIG_TYPES[type]( + parse_b32_key(kwargs["key"]) + ) + elif type == "static_password": + config = _CONFIG_TYPES[type]( + encode(kwargs["password"], KEYBOARD_LAYOUT[kwargs["keyboard_layout"]]) + ) + elif type == "yubiotp": + config = _CONFIG_TYPES[type]( + fixed=modhex_decode(kwargs["public_id"]), + uid=bytes.fromhex(kwargs["private_id"]), + key=bytes.fromhex(kwargs["key"]) + ) + else: + raise ValueError("No supported configuration type provided.") + return config + @action def put(self, params, event, signal): - config = None - for key in _CONFIG_TYPES: - if key in params: - if config is not None: - raise ValueError("Only one configuration type can be provided.") - config = _CONFIG_TYPES[key]( - *(bytes.fromhex(arg) for arg in params.pop(key)) - ) - if config is None: - raise ValueError("No supported configuration type provided.") - self._apply_config(config, params) - self.session.put_configuration( - self.slot, - config, - params.pop("acc_code", None), - params.pop("cur_acc_code", None), - ) - return dict() + type = params.pop("type") + options = params.pop("options", {}) + args = params + + config = self._get_config(type, **args) + self._apply_options(config, options) + try: + self.session.put_configuration( + self.slot, + config, + params.pop("acc_code", None), + params.pop("cur_acc_code", None), + ) + return dict() + except CommandError: + raise ValueError(_FAIL_MSG) @action( condition=lambda self: self._state.version >= (2, 2, 0) @@ -180,7 +234,7 @@ class SlotNode(RpcNode): ) def update(self, params, event, signal): config = UpdateConfiguration() - self._apply_config(config, params) + self._apply_options(config, params) self.session.update_configuration( self.slot, config, @@ -188,3 +242,4 @@ class SlotNode(RpcNode): params.pop("cur_acc_code", None), ) return dict() + From 2e20c2db4444d1300c59c7ae2a018a5f8cf1bd3c Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Thu, 9 Nov 2023 14:09:59 +0100 Subject: [PATCH 02/27] Add OTP application. --- lib/app/models.dart | 1 + lib/app/views/main_page.dart | 2 + lib/desktop/init.dart | 5 + lib/desktop/otp/state.dart | 100 ++ lib/l10n/app_en.arb | 64 + lib/otp/features.dart | 26 + lib/otp/keys.dart | 38 + lib/otp/models.dart | 116 ++ lib/otp/models.freezed.dart | 1480 ++++++++++++++++++ lib/otp/models.g.dart | 160 ++ lib/otp/state.dart | 36 + lib/otp/views/actions.dart | 158 ++ lib/otp/views/configure_chalresp_dialog.dart | 183 +++ lib/otp/views/configure_hotp_dialog.dart | 199 +++ lib/otp/views/configure_static_dialog.dart | 217 +++ lib/otp/views/configure_yubiotp_dialog.dart | 255 +++ lib/otp/views/delete_slot_dialog.dart | 83 + lib/otp/views/key_actions.dart | 57 + lib/otp/views/otp_screen.dart | 114 ++ lib/otp/views/overwrite_confirm_dialog.dart | 66 + lib/otp/views/slot_dialog.dart | 89 ++ lib/otp/views/swap_slots_dialog.dart | 63 + 22 files changed, 3512 insertions(+) create mode 100644 lib/desktop/otp/state.dart create mode 100644 lib/otp/features.dart create mode 100644 lib/otp/keys.dart create mode 100644 lib/otp/models.dart create mode 100644 lib/otp/models.freezed.dart create mode 100644 lib/otp/models.g.dart create mode 100644 lib/otp/state.dart create mode 100644 lib/otp/views/actions.dart create mode 100644 lib/otp/views/configure_chalresp_dialog.dart create mode 100644 lib/otp/views/configure_hotp_dialog.dart create mode 100644 lib/otp/views/configure_static_dialog.dart create mode 100644 lib/otp/views/configure_yubiotp_dialog.dart create mode 100644 lib/otp/views/delete_slot_dialog.dart create mode 100644 lib/otp/views/key_actions.dart create mode 100644 lib/otp/views/otp_screen.dart create mode 100644 lib/otp/views/overwrite_confirm_dialog.dart create mode 100644 lib/otp/views/slot_dialog.dart create mode 100644 lib/otp/views/swap_slots_dialog.dart diff --git a/lib/app/models.dart b/lib/app/models.dart index a69f568d..9d87b6ab 100755 --- a/lib/app/models.dart +++ b/lib/app/models.dart @@ -55,6 +55,7 @@ enum Application { Application.oath => l10n.s_authenticator, Application.fido => l10n.s_webauthn, Application.piv => l10n.s_piv, + Application.otp => l10n.s_otp, _ => name.substring(0, 1).toUpperCase() + name.substring(1), }; diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index 4faa9e9c..b3d44c98 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -17,6 +17,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:yubico_authenticator/otp/views/otp_screen.dart'; import '../../android/app_methods.dart'; import '../../android/qr_scanner/qr_scanner_provider.dart'; @@ -150,6 +151,7 @@ class MainPage extends ConsumerWidget { Application.oath => OathScreen(data.node.path), Application.fido => FidoScreen(data), Application.piv => PivScreen(data.node.path), + Application.otp => OtpScreen(data.node.path), _ => MessagePage( header: l10n.s_app_not_supported, message: l10n.l_app_not_supported_desc, diff --git a/lib/desktop/init.dart b/lib/desktop/init.dart index 6a6001d8..ff673696 100755 --- a/lib/desktop/init.dart +++ b/lib/desktop/init.dart @@ -43,12 +43,14 @@ import '../fido/state.dart'; import '../management/state.dart'; import '../oath/state.dart'; import '../piv/state.dart'; +import '../otp/state.dart'; import '../version.dart'; import 'devices.dart'; import 'fido/state.dart'; import 'management/state.dart'; import 'oath/state.dart'; import 'piv/state.dart'; +import 'otp/state.dart'; import 'qr_scanner.dart'; import 'rpc.dart'; import 'state.dart'; @@ -189,6 +191,7 @@ Future initialize(List argv) async { Application.fido, Application.piv, Application.management, + Application.otp ])), prefProvider.overrideWithValue(prefs), rpcProvider.overrideWith((_) => rpcFuture), @@ -226,6 +229,8 @@ Future initialize(List argv) async { // PIV pivStateProvider.overrideWithProvider(desktopPivState.call), pivSlotsProvider.overrideWithProvider(desktopPivSlots.call), + // OTP + otpStateProvider.overrideWithProvider(desktopOtpState.call) ], child: YubicoAuthenticatorApp( page: Consumer( diff --git a/lib/desktop/otp/state.dart b/lib/desktop/otp/state.dart new file mode 100644 index 00000000..0109d76d --- /dev/null +++ b/lib/desktop/otp/state.dart @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; + +import '../../app/logging.dart'; +import '../../app/models.dart'; +import '../../otp/models.dart'; +import '../../otp/state.dart'; +import '../rpc.dart'; +import '../state.dart'; + +final _log = Logger('desktop.otp.state'); + +final _sessionProvider = + Provider.autoDispose.family( + (ref, devicePath) => RpcNodeSession( + ref.watch(rpcProvider).requireValue, devicePath, ['ccid', 'yubiotp']), +); + +final desktopOtpState = AsyncNotifierProvider.autoDispose + .family( + _DesktopOtpStateNotifier.new); + +class _DesktopOtpStateNotifier extends OtpStateNotifier { + late RpcNodeSession _session; + + @override + FutureOr build(DevicePath devicePath) async { + _session = ref.watch(_sessionProvider(devicePath)); + _session.setErrorHandler('state-reset', (_) async { + ref.invalidate(_sessionProvider(devicePath)); + }); + ref.onDispose(() { + _session.unsetErrorHandler('state-reset'); + }); + + final result = await _session.command('get'); + _log.debug('application status', jsonEncode(result)); + return OtpState.fromJson(result['data']); + } + + @override + Future swapSlots() async { + await _session.command('swap'); + ref.invalidate(_sessionProvider(_session.devicePath)); + } + + @override + Future generateStaticPassword(int length, String layout) async { + final result = await _session.command('generate_static', + params: {'length': length, 'layout': layout}); + return result['password']; + } + + @override + Future modhexEncodeSerial(int serial) async { + final result = + await _session.command('serial_modhex', params: {'serial': serial}); + return result['encoded']; + } + + @override + Future>> getKeyboardLayouts() async { + final result = await _session.command('keyboard_layouts'); + return Map>.from(result.map((key, value) => + MapEntry(key, (value as List).cast().toList()))); + } + + @override + Future deleteSlot(SlotId slot) async { + await _session.command('delete', target: [slot.id]); + ref.invalidateSelf(); + } + + @override + Future configureSlot(SlotId slot, + {required SlotConfiguration configuration}) async { + await _session.command('put', + target: [slot.id], params: configuration.toJson()); + ref.invalidateSelf(); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8c0acadd..f7c1094e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -63,6 +63,7 @@ "s_settings": "Settings", "s_piv": "PIV", "s_webauthn": "WebAuthn", + "s_otp": "OTP", "s_help_and_about": "Help and about", "s_help_and_feedback": "Help and feedback", "s_send_feedback": "Send us feedback", @@ -504,6 +505,69 @@ "s_slot_9d": "Key Management", "s_slot_9e": "Card Authentication", + "@_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", + + "@_otp_slot_configurations": {}, + "s_yubiotp": "Yubico OTP", + "l_yubiotp_desc": "YubiCloud verified OTP's", + "s_challenge_response": "Challenge-response", + "l_challenge_response_desc": "Show me yours and I'll show you mine", + "s_static_password": "Static password", + "l_static_password_desc": "Use complex passwords with a touch", + "s_hotp": "OATH-HOTP", + "l_hotp_desc": "HMAC based OTP's", + "s_public_id": "Public ID", + "s_private_id": "Private ID", + "s_allow_any_character": "Allow any character", + "s_use_serial": "Use serial", + "s_generate_private_id": "Generate private ID", + "s_generate_secret_key": "Generate secret key", + "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}.", + "@p_warning_delete_slot_configuration": { + "placeholders": { + "slot": {} + } + }, + "l_slot_deleted": "Slot configuration 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": { + "placeholders": { + "type": {} + } + }, + "s_append_enter": "Append enter", + "l_append_enter_desc": "Append an Enter keystroke after emitting the OTP", + + "@_otp_errors": {}, + "p_otp_slot_configuration_error": "Failed to modify {slot}! Make sure the YubiKey does not have restrictive access.", + "@p_otp_slot_configuration_error": { + "placeholders": { + "slot": {} + } + }, + "@_permissions": {}, "s_enable_nfc": "Enable NFC", "s_permission_denied": "Permission denied", diff --git a/lib/otp/features.dart b/lib/otp/features.dart new file mode 100644 index 00000000..b63fa3bd --- /dev/null +++ b/lib/otp/features.dart @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '../app/features.dart'; + +final actions = otp.feature('actions'); + +final actionsSwap = actions.feature('swap'); + +final slots = otp.feature('slots'); + +final slotsConfigure = slots.feature('configure'); +final slotsDelete = slots.feature('delete'); diff --git a/lib/otp/keys.dart b/lib/otp/keys.dart new file mode 100644 index 00000000..e8b05647 --- /dev/null +++ b/lib/otp/keys.dart @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022-2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; + +const _prefix = 'otp.keys'; +const _keyAction = '$_prefix.actions'; +const _slotAction = '$_prefix.slot.actions'; + +// Key actions +const swapSlots = Key('$_keyAction.swap_slots'); + +// Slot actions +const configureYubiOtp = Key('$_slotAction.configure_yubiotp'); +const configureHotp = Key('$_slotAction.configure_hotp'); +const configureStatic = Key('$_slotAction.configure_static'); +const configureChalResp = Key('$_slotAction.configure_chal_resp'); +const deleteAction = Key('$_slotAction.delete'); + +const saveButton = Key('$_prefix.save'); +const deleteButton = Key('$_prefix.delete'); + +const secretField = Key('$_prefix.secret'); +const publicIdField = Key('$_prefix.public_id'); +const privateIdField = Key('$_prefix.private_id'); diff --git a/lib/otp/models.dart b/lib/otp/models.dart new file mode 100644 index 00000000..30d05899 --- /dev/null +++ b/lib/otp/models.dart @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +part 'models.freezed.dart'; +part 'models.g.dart'; + +enum SlotId { + one('one', 1), + two('two', 2); + + final String id; + final int numberId; + 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) + }; + } + + factory SlotId.fromJson(String value) => + SlotId.values.firstWhere((e) => e.id == value); +} + +enum SlotConfigurationType { yubiotp, static, hotp, chalresp } + +@freezed +class OtpState with _$OtpState { + const OtpState._(); + factory OtpState({ + required bool slot1Configured, + required bool slot2Configured, + }) = _OtpState; + + factory OtpState.fromJson(Map json) => + _$OtpStateFromJson(json); + + List get slots => [ + OtpSlot(slot: SlotId.one, isConfigured: slot1Configured), + OtpSlot(slot: SlotId.two, isConfigured: slot2Configured), + ]; +} + +@freezed +class OtpSlot with _$OtpSlot { + factory OtpSlot({required SlotId slot, required bool isConfigured}) = + _OtpSlot; +} + +@freezed +class SlotConfigurationOptions with _$SlotConfigurationOptions { + // ignore: invalid_annotation_target + @JsonSerializable(includeIfNull: false) + factory SlotConfigurationOptions( + {bool? digits8, + bool? requireTouch, + bool? appendCr}) = _SlotConfigurationOptions; + + factory SlotConfigurationOptions.fromJson(Map json) => + _$SlotConfigurationOptionsFromJson(json); +} + +@Freezed(unionKey: 'type', unionValueCase: FreezedUnionCase.snake) +class SlotConfiguration with _$SlotConfiguration { + const SlotConfiguration._(); + + // ignore: invalid_annotation_target + @JsonSerializable(explicitToJson: true, includeIfNull: false) + const factory SlotConfiguration.hotp( + {required String key, + SlotConfigurationOptions? options}) = _SlotConfigurationHotp; + + @FreezedUnionValue('hmac_sha1') + // ignore: invalid_annotation_target + @JsonSerializable(explicitToJson: true, includeIfNull: false) + const factory SlotConfiguration.chalresp( + {required String key, + SlotConfigurationOptions? options}) = _SlotConfigurationHmacSha1; + + @FreezedUnionValue('static_password') + // ignore: invalid_annotation_target + @JsonSerializable(explicitToJson: true, includeIfNull: false) + const factory SlotConfiguration.static( + {required String password, + required String keyboardLayout, + SlotConfigurationOptions? options}) = _SlotConfigurationStaticPassword; + + // ignore: invalid_annotation_target + @JsonSerializable(explicitToJson: true, includeIfNull: false) + const factory SlotConfiguration.yubiotp( + {required String publicId, + required String privateId, + required String key, + SlotConfigurationOptions? options}) = _SlotConfigurationYubiOtp; + + factory SlotConfiguration.fromJson(Map json) => + _$SlotConfigurationFromJson(json); +} diff --git a/lib/otp/models.freezed.dart b/lib/otp/models.freezed.dart new file mode 100644 index 00000000..5db587e2 --- /dev/null +++ b/lib/otp/models.freezed.dart @@ -0,0 +1,1480 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'models.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +OtpState _$OtpStateFromJson(Map json) { + return _OtpState.fromJson(json); +} + +/// @nodoc +mixin _$OtpState { + bool get slot1Configured => throw _privateConstructorUsedError; + bool get slot2Configured => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $OtpStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OtpStateCopyWith<$Res> { + factory $OtpStateCopyWith(OtpState value, $Res Function(OtpState) then) = + _$OtpStateCopyWithImpl<$Res, OtpState>; + @useResult + $Res call({bool slot1Configured, bool slot2Configured}); +} + +/// @nodoc +class _$OtpStateCopyWithImpl<$Res, $Val extends OtpState> + implements $OtpStateCopyWith<$Res> { + _$OtpStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? slot1Configured = null, + Object? slot2Configured = null, + }) { + return _then(_value.copyWith( + slot1Configured: null == slot1Configured + ? _value.slot1Configured + : slot1Configured // ignore: cast_nullable_to_non_nullable + as bool, + slot2Configured: null == slot2Configured + ? _value.slot2Configured + : slot2Configured // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_OtpStateCopyWith<$Res> implements $OtpStateCopyWith<$Res> { + factory _$$_OtpStateCopyWith( + _$_OtpState value, $Res Function(_$_OtpState) then) = + __$$_OtpStateCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool slot1Configured, bool slot2Configured}); +} + +/// @nodoc +class __$$_OtpStateCopyWithImpl<$Res> + extends _$OtpStateCopyWithImpl<$Res, _$_OtpState> + implements _$$_OtpStateCopyWith<$Res> { + __$$_OtpStateCopyWithImpl( + _$_OtpState _value, $Res Function(_$_OtpState) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? slot1Configured = null, + Object? slot2Configured = null, + }) { + return _then(_$_OtpState( + slot1Configured: null == slot1Configured + ? _value.slot1Configured + : slot1Configured // ignore: cast_nullable_to_non_nullable + as bool, + slot2Configured: null == slot2Configured + ? _value.slot2Configured + : slot2Configured // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_OtpState extends _OtpState { + _$_OtpState({required this.slot1Configured, required this.slot2Configured}) + : super._(); + + factory _$_OtpState.fromJson(Map json) => + _$$_OtpStateFromJson(json); + + @override + final bool slot1Configured; + @override + final bool slot2Configured; + + @override + String toString() { + return 'OtpState(slot1Configured: $slot1Configured, slot2Configured: $slot2Configured)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_OtpState && + (identical(other.slot1Configured, slot1Configured) || + other.slot1Configured == slot1Configured) && + (identical(other.slot2Configured, slot2Configured) || + other.slot2Configured == slot2Configured)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, slot1Configured, slot2Configured); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_OtpStateCopyWith<_$_OtpState> get copyWith => + __$$_OtpStateCopyWithImpl<_$_OtpState>(this, _$identity); + + @override + Map toJson() { + return _$$_OtpStateToJson( + this, + ); + } +} + +abstract class _OtpState extends OtpState { + factory _OtpState( + {required final bool slot1Configured, + required final bool slot2Configured}) = _$_OtpState; + _OtpState._() : super._(); + + factory _OtpState.fromJson(Map json) = _$_OtpState.fromJson; + + @override + bool get slot1Configured; + @override + bool get slot2Configured; + @override + @JsonKey(ignore: true) + _$$_OtpStateCopyWith<_$_OtpState> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$OtpSlot { + SlotId get slot => throw _privateConstructorUsedError; + bool get isConfigured => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $OtpSlotCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OtpSlotCopyWith<$Res> { + factory $OtpSlotCopyWith(OtpSlot value, $Res Function(OtpSlot) then) = + _$OtpSlotCopyWithImpl<$Res, OtpSlot>; + @useResult + $Res call({SlotId slot, bool isConfigured}); +} + +/// @nodoc +class _$OtpSlotCopyWithImpl<$Res, $Val extends OtpSlot> + implements $OtpSlotCopyWith<$Res> { + _$OtpSlotCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? slot = null, + Object? isConfigured = null, + }) { + return _then(_value.copyWith( + slot: null == slot + ? _value.slot + : slot // ignore: cast_nullable_to_non_nullable + as SlotId, + isConfigured: null == isConfigured + ? _value.isConfigured + : isConfigured // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_OtpSlotCopyWith<$Res> implements $OtpSlotCopyWith<$Res> { + factory _$$_OtpSlotCopyWith( + _$_OtpSlot value, $Res Function(_$_OtpSlot) then) = + __$$_OtpSlotCopyWithImpl<$Res>; + @override + @useResult + $Res call({SlotId slot, bool isConfigured}); +} + +/// @nodoc +class __$$_OtpSlotCopyWithImpl<$Res> + extends _$OtpSlotCopyWithImpl<$Res, _$_OtpSlot> + implements _$$_OtpSlotCopyWith<$Res> { + __$$_OtpSlotCopyWithImpl(_$_OtpSlot _value, $Res Function(_$_OtpSlot) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? slot = null, + Object? isConfigured = null, + }) { + return _then(_$_OtpSlot( + slot: null == slot + ? _value.slot + : slot // ignore: cast_nullable_to_non_nullable + as SlotId, + isConfigured: null == isConfigured + ? _value.isConfigured + : isConfigured // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$_OtpSlot implements _OtpSlot { + _$_OtpSlot({required this.slot, required this.isConfigured}); + + @override + final SlotId slot; + @override + final bool isConfigured; + + @override + String toString() { + return 'OtpSlot(slot: $slot, isConfigured: $isConfigured)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_OtpSlot && + (identical(other.slot, slot) || other.slot == slot) && + (identical(other.isConfigured, isConfigured) || + other.isConfigured == isConfigured)); + } + + @override + int get hashCode => Object.hash(runtimeType, slot, isConfigured); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_OtpSlotCopyWith<_$_OtpSlot> get copyWith => + __$$_OtpSlotCopyWithImpl<_$_OtpSlot>(this, _$identity); +} + +abstract class _OtpSlot implements OtpSlot { + factory _OtpSlot( + {required final SlotId slot, + required final bool isConfigured}) = _$_OtpSlot; + + @override + SlotId get slot; + @override + bool get isConfigured; + @override + @JsonKey(ignore: true) + _$$_OtpSlotCopyWith<_$_OtpSlot> get copyWith => + throw _privateConstructorUsedError; +} + +SlotConfigurationOptions _$SlotConfigurationOptionsFromJson( + Map json) { + return _SlotConfigurationOptions.fromJson(json); +} + +/// @nodoc +mixin _$SlotConfigurationOptions { + bool? get digits8 => throw _privateConstructorUsedError; + bool? get requireTouch => throw _privateConstructorUsedError; + bool? get appendCr => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $SlotConfigurationOptionsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SlotConfigurationOptionsCopyWith<$Res> { + factory $SlotConfigurationOptionsCopyWith(SlotConfigurationOptions value, + $Res Function(SlotConfigurationOptions) then) = + _$SlotConfigurationOptionsCopyWithImpl<$Res, SlotConfigurationOptions>; + @useResult + $Res call({bool? digits8, bool? requireTouch, bool? appendCr}); +} + +/// @nodoc +class _$SlotConfigurationOptionsCopyWithImpl<$Res, + $Val extends SlotConfigurationOptions> + implements $SlotConfigurationOptionsCopyWith<$Res> { + _$SlotConfigurationOptionsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? digits8 = freezed, + Object? requireTouch = freezed, + Object? appendCr = freezed, + }) { + return _then(_value.copyWith( + digits8: freezed == digits8 + ? _value.digits8 + : digits8 // ignore: cast_nullable_to_non_nullable + as bool?, + requireTouch: freezed == requireTouch + ? _value.requireTouch + : requireTouch // ignore: cast_nullable_to_non_nullable + as bool?, + appendCr: freezed == appendCr + ? _value.appendCr + : appendCr // ignore: cast_nullable_to_non_nullable + as bool?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_SlotConfigurationOptionsCopyWith<$Res> + implements $SlotConfigurationOptionsCopyWith<$Res> { + factory _$$_SlotConfigurationOptionsCopyWith( + _$_SlotConfigurationOptions value, + $Res Function(_$_SlotConfigurationOptions) then) = + __$$_SlotConfigurationOptionsCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool? digits8, bool? requireTouch, bool? appendCr}); +} + +/// @nodoc +class __$$_SlotConfigurationOptionsCopyWithImpl<$Res> + extends _$SlotConfigurationOptionsCopyWithImpl<$Res, + _$_SlotConfigurationOptions> + implements _$$_SlotConfigurationOptionsCopyWith<$Res> { + __$$_SlotConfigurationOptionsCopyWithImpl(_$_SlotConfigurationOptions _value, + $Res Function(_$_SlotConfigurationOptions) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? digits8 = freezed, + Object? requireTouch = freezed, + Object? appendCr = freezed, + }) { + return _then(_$_SlotConfigurationOptions( + digits8: freezed == digits8 + ? _value.digits8 + : digits8 // ignore: cast_nullable_to_non_nullable + as bool?, + requireTouch: freezed == requireTouch + ? _value.requireTouch + : requireTouch // ignore: cast_nullable_to_non_nullable + as bool?, + appendCr: freezed == appendCr + ? _value.appendCr + : appendCr // ignore: cast_nullable_to_non_nullable + as bool?, + )); + } +} + +/// @nodoc + +@JsonSerializable(includeIfNull: false) +class _$_SlotConfigurationOptions implements _SlotConfigurationOptions { + _$_SlotConfigurationOptions({this.digits8, this.requireTouch, this.appendCr}); + + factory _$_SlotConfigurationOptions.fromJson(Map json) => + _$$_SlotConfigurationOptionsFromJson(json); + + @override + final bool? digits8; + @override + final bool? requireTouch; + @override + final bool? appendCr; + + @override + String toString() { + return 'SlotConfigurationOptions(digits8: $digits8, requireTouch: $requireTouch, appendCr: $appendCr)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_SlotConfigurationOptions && + (identical(other.digits8, digits8) || other.digits8 == digits8) && + (identical(other.requireTouch, requireTouch) || + other.requireTouch == requireTouch) && + (identical(other.appendCr, appendCr) || + other.appendCr == appendCr)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, digits8, requireTouch, appendCr); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_SlotConfigurationOptionsCopyWith<_$_SlotConfigurationOptions> + get copyWith => __$$_SlotConfigurationOptionsCopyWithImpl< + _$_SlotConfigurationOptions>(this, _$identity); + + @override + Map toJson() { + return _$$_SlotConfigurationOptionsToJson( + this, + ); + } +} + +abstract class _SlotConfigurationOptions implements SlotConfigurationOptions { + factory _SlotConfigurationOptions( + {final bool? digits8, + final bool? requireTouch, + final bool? appendCr}) = _$_SlotConfigurationOptions; + + factory _SlotConfigurationOptions.fromJson(Map json) = + _$_SlotConfigurationOptions.fromJson; + + @override + bool? get digits8; + @override + bool? get requireTouch; + @override + bool? get appendCr; + @override + @JsonKey(ignore: true) + _$$_SlotConfigurationOptionsCopyWith<_$_SlotConfigurationOptions> + get copyWith => throw _privateConstructorUsedError; +} + +SlotConfiguration _$SlotConfigurationFromJson(Map json) { + switch (json['type']) { + case 'hotp': + return _SlotConfigurationHotp.fromJson(json); + case 'hmac_sha1': + return _SlotConfigurationHmacSha1.fromJson(json); + case 'static_password': + return _SlotConfigurationStaticPassword.fromJson(json); + case 'yubiotp': + return _SlotConfigurationYubiOtp.fromJson(json); + + default: + throw CheckedFromJsonException(json, 'type', 'SlotConfiguration', + 'Invalid union type "${json['type']}"!'); + } +} + +/// @nodoc +mixin _$SlotConfiguration { + SlotConfigurationOptions? get options => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(String key, SlotConfigurationOptions? options) + hotp, + required TResult Function(String key, SlotConfigurationOptions? options) + chalresp, + required TResult Function(String password, String keyboardLayout, + SlotConfigurationOptions? options) + static, + required TResult Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options) + yubiotp, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String key, SlotConfigurationOptions? options)? hotp, + TResult? Function(String key, SlotConfigurationOptions? options)? chalresp, + TResult? Function(String password, String keyboardLayout, + SlotConfigurationOptions? options)? + static, + TResult? Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options)? + yubiotp, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String key, SlotConfigurationOptions? options)? hotp, + TResult Function(String key, SlotConfigurationOptions? options)? chalresp, + TResult Function(String password, String keyboardLayout, + SlotConfigurationOptions? options)? + static, + TResult Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options)? + yubiotp, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_SlotConfigurationHotp value) hotp, + required TResult Function(_SlotConfigurationHmacSha1 value) chalresp, + required TResult Function(_SlotConfigurationStaticPassword value) static, + required TResult Function(_SlotConfigurationYubiOtp value) yubiotp, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SlotConfigurationHotp value)? hotp, + TResult? Function(_SlotConfigurationHmacSha1 value)? chalresp, + TResult? Function(_SlotConfigurationStaticPassword value)? static, + TResult? Function(_SlotConfigurationYubiOtp value)? yubiotp, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SlotConfigurationHotp value)? hotp, + TResult Function(_SlotConfigurationHmacSha1 value)? chalresp, + TResult Function(_SlotConfigurationStaticPassword value)? static, + TResult Function(_SlotConfigurationYubiOtp value)? yubiotp, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $SlotConfigurationCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SlotConfigurationCopyWith<$Res> { + factory $SlotConfigurationCopyWith( + SlotConfiguration value, $Res Function(SlotConfiguration) then) = + _$SlotConfigurationCopyWithImpl<$Res, SlotConfiguration>; + @useResult + $Res call({SlotConfigurationOptions? options}); + + $SlotConfigurationOptionsCopyWith<$Res>? get options; +} + +/// @nodoc +class _$SlotConfigurationCopyWithImpl<$Res, $Val extends SlotConfiguration> + implements $SlotConfigurationCopyWith<$Res> { + _$SlotConfigurationCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? options = freezed, + }) { + return _then(_value.copyWith( + options: freezed == options + ? _value.options + : options // ignore: cast_nullable_to_non_nullable + as SlotConfigurationOptions?, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $SlotConfigurationOptionsCopyWith<$Res>? get options { + if (_value.options == null) { + return null; + } + + return $SlotConfigurationOptionsCopyWith<$Res>(_value.options!, (value) { + return _then(_value.copyWith(options: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_SlotConfigurationHotpCopyWith<$Res> + implements $SlotConfigurationCopyWith<$Res> { + factory _$$_SlotConfigurationHotpCopyWith(_$_SlotConfigurationHotp value, + $Res Function(_$_SlotConfigurationHotp) then) = + __$$_SlotConfigurationHotpCopyWithImpl<$Res>; + @override + @useResult + $Res call({String key, SlotConfigurationOptions? options}); + + @override + $SlotConfigurationOptionsCopyWith<$Res>? get options; +} + +/// @nodoc +class __$$_SlotConfigurationHotpCopyWithImpl<$Res> + extends _$SlotConfigurationCopyWithImpl<$Res, _$_SlotConfigurationHotp> + implements _$$_SlotConfigurationHotpCopyWith<$Res> { + __$$_SlotConfigurationHotpCopyWithImpl(_$_SlotConfigurationHotp _value, + $Res Function(_$_SlotConfigurationHotp) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? key = null, + Object? options = freezed, + }) { + return _then(_$_SlotConfigurationHotp( + key: null == key + ? _value.key + : key // ignore: cast_nullable_to_non_nullable + as String, + options: freezed == options + ? _value.options + : options // ignore: cast_nullable_to_non_nullable + as SlotConfigurationOptions?, + )); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class _$_SlotConfigurationHotp extends _SlotConfigurationHotp { + const _$_SlotConfigurationHotp( + {required this.key, this.options, final String? $type}) + : $type = $type ?? 'hotp', + super._(); + + factory _$_SlotConfigurationHotp.fromJson(Map json) => + _$$_SlotConfigurationHotpFromJson(json); + + @override + final String key; + @override + final SlotConfigurationOptions? options; + + @JsonKey(name: 'type') + final String $type; + + @override + String toString() { + return 'SlotConfiguration.hotp(key: $key, options: $options)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_SlotConfigurationHotp && + (identical(other.key, key) || other.key == key) && + (identical(other.options, options) || other.options == options)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, key, options); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_SlotConfigurationHotpCopyWith<_$_SlotConfigurationHotp> get copyWith => + __$$_SlotConfigurationHotpCopyWithImpl<_$_SlotConfigurationHotp>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String key, SlotConfigurationOptions? options) + hotp, + required TResult Function(String key, SlotConfigurationOptions? options) + chalresp, + required TResult Function(String password, String keyboardLayout, + SlotConfigurationOptions? options) + static, + required TResult Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options) + yubiotp, + }) { + return hotp(key, options); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String key, SlotConfigurationOptions? options)? hotp, + TResult? Function(String key, SlotConfigurationOptions? options)? chalresp, + TResult? Function(String password, String keyboardLayout, + SlotConfigurationOptions? options)? + static, + TResult? Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options)? + yubiotp, + }) { + return hotp?.call(key, options); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String key, SlotConfigurationOptions? options)? hotp, + TResult Function(String key, SlotConfigurationOptions? options)? chalresp, + TResult Function(String password, String keyboardLayout, + SlotConfigurationOptions? options)? + static, + TResult Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options)? + yubiotp, + required TResult orElse(), + }) { + if (hotp != null) { + return hotp(key, options); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_SlotConfigurationHotp value) hotp, + required TResult Function(_SlotConfigurationHmacSha1 value) chalresp, + required TResult Function(_SlotConfigurationStaticPassword value) static, + required TResult Function(_SlotConfigurationYubiOtp value) yubiotp, + }) { + return hotp(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SlotConfigurationHotp value)? hotp, + TResult? Function(_SlotConfigurationHmacSha1 value)? chalresp, + TResult? Function(_SlotConfigurationStaticPassword value)? static, + TResult? Function(_SlotConfigurationYubiOtp value)? yubiotp, + }) { + return hotp?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SlotConfigurationHotp value)? hotp, + TResult Function(_SlotConfigurationHmacSha1 value)? chalresp, + TResult Function(_SlotConfigurationStaticPassword value)? static, + TResult Function(_SlotConfigurationYubiOtp value)? yubiotp, + required TResult orElse(), + }) { + if (hotp != null) { + return hotp(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$_SlotConfigurationHotpToJson( + this, + ); + } +} + +abstract class _SlotConfigurationHotp extends SlotConfiguration { + const factory _SlotConfigurationHotp( + {required final String key, + final SlotConfigurationOptions? options}) = _$_SlotConfigurationHotp; + const _SlotConfigurationHotp._() : super._(); + + factory _SlotConfigurationHotp.fromJson(Map json) = + _$_SlotConfigurationHotp.fromJson; + + String get key; + @override + SlotConfigurationOptions? get options; + @override + @JsonKey(ignore: true) + _$$_SlotConfigurationHotpCopyWith<_$_SlotConfigurationHotp> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$_SlotConfigurationHmacSha1CopyWith<$Res> + implements $SlotConfigurationCopyWith<$Res> { + factory _$$_SlotConfigurationHmacSha1CopyWith( + _$_SlotConfigurationHmacSha1 value, + $Res Function(_$_SlotConfigurationHmacSha1) then) = + __$$_SlotConfigurationHmacSha1CopyWithImpl<$Res>; + @override + @useResult + $Res call({String key, SlotConfigurationOptions? options}); + + @override + $SlotConfigurationOptionsCopyWith<$Res>? get options; +} + +/// @nodoc +class __$$_SlotConfigurationHmacSha1CopyWithImpl<$Res> + extends _$SlotConfigurationCopyWithImpl<$Res, _$_SlotConfigurationHmacSha1> + implements _$$_SlotConfigurationHmacSha1CopyWith<$Res> { + __$$_SlotConfigurationHmacSha1CopyWithImpl( + _$_SlotConfigurationHmacSha1 _value, + $Res Function(_$_SlotConfigurationHmacSha1) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? key = null, + Object? options = freezed, + }) { + return _then(_$_SlotConfigurationHmacSha1( + key: null == key + ? _value.key + : key // ignore: cast_nullable_to_non_nullable + as String, + options: freezed == options + ? _value.options + : options // ignore: cast_nullable_to_non_nullable + as SlotConfigurationOptions?, + )); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class _$_SlotConfigurationHmacSha1 extends _SlotConfigurationHmacSha1 { + const _$_SlotConfigurationHmacSha1( + {required this.key, this.options, final String? $type}) + : $type = $type ?? 'hmac_sha1', + super._(); + + factory _$_SlotConfigurationHmacSha1.fromJson(Map json) => + _$$_SlotConfigurationHmacSha1FromJson(json); + + @override + final String key; + @override + final SlotConfigurationOptions? options; + + @JsonKey(name: 'type') + final String $type; + + @override + String toString() { + return 'SlotConfiguration.chalresp(key: $key, options: $options)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_SlotConfigurationHmacSha1 && + (identical(other.key, key) || other.key == key) && + (identical(other.options, options) || other.options == options)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, key, options); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_SlotConfigurationHmacSha1CopyWith<_$_SlotConfigurationHmacSha1> + get copyWith => __$$_SlotConfigurationHmacSha1CopyWithImpl< + _$_SlotConfigurationHmacSha1>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String key, SlotConfigurationOptions? options) + hotp, + required TResult Function(String key, SlotConfigurationOptions? options) + chalresp, + required TResult Function(String password, String keyboardLayout, + SlotConfigurationOptions? options) + static, + required TResult Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options) + yubiotp, + }) { + return chalresp(key, options); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String key, SlotConfigurationOptions? options)? hotp, + TResult? Function(String key, SlotConfigurationOptions? options)? chalresp, + TResult? Function(String password, String keyboardLayout, + SlotConfigurationOptions? options)? + static, + TResult? Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options)? + yubiotp, + }) { + return chalresp?.call(key, options); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String key, SlotConfigurationOptions? options)? hotp, + TResult Function(String key, SlotConfigurationOptions? options)? chalresp, + TResult Function(String password, String keyboardLayout, + SlotConfigurationOptions? options)? + static, + TResult Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options)? + yubiotp, + required TResult orElse(), + }) { + if (chalresp != null) { + return chalresp(key, options); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_SlotConfigurationHotp value) hotp, + required TResult Function(_SlotConfigurationHmacSha1 value) chalresp, + required TResult Function(_SlotConfigurationStaticPassword value) static, + required TResult Function(_SlotConfigurationYubiOtp value) yubiotp, + }) { + return chalresp(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SlotConfigurationHotp value)? hotp, + TResult? Function(_SlotConfigurationHmacSha1 value)? chalresp, + TResult? Function(_SlotConfigurationStaticPassword value)? static, + TResult? Function(_SlotConfigurationYubiOtp value)? yubiotp, + }) { + return chalresp?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SlotConfigurationHotp value)? hotp, + TResult Function(_SlotConfigurationHmacSha1 value)? chalresp, + TResult Function(_SlotConfigurationStaticPassword value)? static, + TResult Function(_SlotConfigurationYubiOtp value)? yubiotp, + required TResult orElse(), + }) { + if (chalresp != null) { + return chalresp(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$_SlotConfigurationHmacSha1ToJson( + this, + ); + } +} + +abstract class _SlotConfigurationHmacSha1 extends SlotConfiguration { + const factory _SlotConfigurationHmacSha1( + {required final String key, + final SlotConfigurationOptions? options}) = _$_SlotConfigurationHmacSha1; + const _SlotConfigurationHmacSha1._() : super._(); + + factory _SlotConfigurationHmacSha1.fromJson(Map json) = + _$_SlotConfigurationHmacSha1.fromJson; + + String get key; + @override + SlotConfigurationOptions? get options; + @override + @JsonKey(ignore: true) + _$$_SlotConfigurationHmacSha1CopyWith<_$_SlotConfigurationHmacSha1> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$_SlotConfigurationStaticPasswordCopyWith<$Res> + implements $SlotConfigurationCopyWith<$Res> { + factory _$$_SlotConfigurationStaticPasswordCopyWith( + _$_SlotConfigurationStaticPassword value, + $Res Function(_$_SlotConfigurationStaticPassword) then) = + __$$_SlotConfigurationStaticPasswordCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String password, + String keyboardLayout, + SlotConfigurationOptions? options}); + + @override + $SlotConfigurationOptionsCopyWith<$Res>? get options; +} + +/// @nodoc +class __$$_SlotConfigurationStaticPasswordCopyWithImpl<$Res> + extends _$SlotConfigurationCopyWithImpl<$Res, + _$_SlotConfigurationStaticPassword> + implements _$$_SlotConfigurationStaticPasswordCopyWith<$Res> { + __$$_SlotConfigurationStaticPasswordCopyWithImpl( + _$_SlotConfigurationStaticPassword _value, + $Res Function(_$_SlotConfigurationStaticPassword) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? password = null, + Object? keyboardLayout = null, + Object? options = freezed, + }) { + return _then(_$_SlotConfigurationStaticPassword( + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + keyboardLayout: null == keyboardLayout + ? _value.keyboardLayout + : keyboardLayout // ignore: cast_nullable_to_non_nullable + as String, + options: freezed == options + ? _value.options + : options // ignore: cast_nullable_to_non_nullable + as SlotConfigurationOptions?, + )); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class _$_SlotConfigurationStaticPassword + extends _SlotConfigurationStaticPassword { + const _$_SlotConfigurationStaticPassword( + {required this.password, + required this.keyboardLayout, + this.options, + final String? $type}) + : $type = $type ?? 'static_password', + super._(); + + factory _$_SlotConfigurationStaticPassword.fromJson( + Map json) => + _$$_SlotConfigurationStaticPasswordFromJson(json); + + @override + final String password; + @override + final String keyboardLayout; + @override + final SlotConfigurationOptions? options; + + @JsonKey(name: 'type') + final String $type; + + @override + String toString() { + return 'SlotConfiguration.static(password: $password, keyboardLayout: $keyboardLayout, options: $options)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_SlotConfigurationStaticPassword && + (identical(other.password, password) || + other.password == password) && + (identical(other.keyboardLayout, keyboardLayout) || + other.keyboardLayout == keyboardLayout) && + (identical(other.options, options) || other.options == options)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, password, keyboardLayout, options); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_SlotConfigurationStaticPasswordCopyWith< + _$_SlotConfigurationStaticPassword> + get copyWith => __$$_SlotConfigurationStaticPasswordCopyWithImpl< + _$_SlotConfigurationStaticPassword>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String key, SlotConfigurationOptions? options) + hotp, + required TResult Function(String key, SlotConfigurationOptions? options) + chalresp, + required TResult Function(String password, String keyboardLayout, + SlotConfigurationOptions? options) + static, + required TResult Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options) + yubiotp, + }) { + return static(password, keyboardLayout, options); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String key, SlotConfigurationOptions? options)? hotp, + TResult? Function(String key, SlotConfigurationOptions? options)? chalresp, + TResult? Function(String password, String keyboardLayout, + SlotConfigurationOptions? options)? + static, + TResult? Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options)? + yubiotp, + }) { + return static?.call(password, keyboardLayout, options); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String key, SlotConfigurationOptions? options)? hotp, + TResult Function(String key, SlotConfigurationOptions? options)? chalresp, + TResult Function(String password, String keyboardLayout, + SlotConfigurationOptions? options)? + static, + TResult Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options)? + yubiotp, + required TResult orElse(), + }) { + if (static != null) { + return static(password, keyboardLayout, options); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_SlotConfigurationHotp value) hotp, + required TResult Function(_SlotConfigurationHmacSha1 value) chalresp, + required TResult Function(_SlotConfigurationStaticPassword value) static, + required TResult Function(_SlotConfigurationYubiOtp value) yubiotp, + }) { + return static(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SlotConfigurationHotp value)? hotp, + TResult? Function(_SlotConfigurationHmacSha1 value)? chalresp, + TResult? Function(_SlotConfigurationStaticPassword value)? static, + TResult? Function(_SlotConfigurationYubiOtp value)? yubiotp, + }) { + return static?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SlotConfigurationHotp value)? hotp, + TResult Function(_SlotConfigurationHmacSha1 value)? chalresp, + TResult Function(_SlotConfigurationStaticPassword value)? static, + TResult Function(_SlotConfigurationYubiOtp value)? yubiotp, + required TResult orElse(), + }) { + if (static != null) { + return static(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$_SlotConfigurationStaticPasswordToJson( + this, + ); + } +} + +abstract class _SlotConfigurationStaticPassword extends SlotConfiguration { + const factory _SlotConfigurationStaticPassword( + {required final String password, + required final String keyboardLayout, + final SlotConfigurationOptions? options}) = + _$_SlotConfigurationStaticPassword; + const _SlotConfigurationStaticPassword._() : super._(); + + factory _SlotConfigurationStaticPassword.fromJson(Map json) = + _$_SlotConfigurationStaticPassword.fromJson; + + String get password; + String get keyboardLayout; + @override + SlotConfigurationOptions? get options; + @override + @JsonKey(ignore: true) + _$$_SlotConfigurationStaticPasswordCopyWith< + _$_SlotConfigurationStaticPassword> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$_SlotConfigurationYubiOtpCopyWith<$Res> + implements $SlotConfigurationCopyWith<$Res> { + factory _$$_SlotConfigurationYubiOtpCopyWith( + _$_SlotConfigurationYubiOtp value, + $Res Function(_$_SlotConfigurationYubiOtp) then) = + __$$_SlotConfigurationYubiOtpCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String publicId, + String privateId, + String key, + SlotConfigurationOptions? options}); + + @override + $SlotConfigurationOptionsCopyWith<$Res>? get options; +} + +/// @nodoc +class __$$_SlotConfigurationYubiOtpCopyWithImpl<$Res> + extends _$SlotConfigurationCopyWithImpl<$Res, _$_SlotConfigurationYubiOtp> + implements _$$_SlotConfigurationYubiOtpCopyWith<$Res> { + __$$_SlotConfigurationYubiOtpCopyWithImpl(_$_SlotConfigurationYubiOtp _value, + $Res Function(_$_SlotConfigurationYubiOtp) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? publicId = null, + Object? privateId = null, + Object? key = null, + Object? options = freezed, + }) { + return _then(_$_SlotConfigurationYubiOtp( + publicId: null == publicId + ? _value.publicId + : publicId // ignore: cast_nullable_to_non_nullable + as String, + privateId: null == privateId + ? _value.privateId + : privateId // ignore: cast_nullable_to_non_nullable + as String, + key: null == key + ? _value.key + : key // ignore: cast_nullable_to_non_nullable + as String, + options: freezed == options + ? _value.options + : options // ignore: cast_nullable_to_non_nullable + as SlotConfigurationOptions?, + )); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class _$_SlotConfigurationYubiOtp extends _SlotConfigurationYubiOtp { + const _$_SlotConfigurationYubiOtp( + {required this.publicId, + required this.privateId, + required this.key, + this.options, + final String? $type}) + : $type = $type ?? 'yubiotp', + super._(); + + factory _$_SlotConfigurationYubiOtp.fromJson(Map json) => + _$$_SlotConfigurationYubiOtpFromJson(json); + + @override + final String publicId; + @override + final String privateId; + @override + final String key; + @override + final SlotConfigurationOptions? options; + + @JsonKey(name: 'type') + final String $type; + + @override + String toString() { + return 'SlotConfiguration.yubiotp(publicId: $publicId, privateId: $privateId, key: $key, options: $options)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_SlotConfigurationYubiOtp && + (identical(other.publicId, publicId) || + other.publicId == publicId) && + (identical(other.privateId, privateId) || + other.privateId == privateId) && + (identical(other.key, key) || other.key == key) && + (identical(other.options, options) || other.options == options)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, publicId, privateId, key, options); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_SlotConfigurationYubiOtpCopyWith<_$_SlotConfigurationYubiOtp> + get copyWith => __$$_SlotConfigurationYubiOtpCopyWithImpl< + _$_SlotConfigurationYubiOtp>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String key, SlotConfigurationOptions? options) + hotp, + required TResult Function(String key, SlotConfigurationOptions? options) + chalresp, + required TResult Function(String password, String keyboardLayout, + SlotConfigurationOptions? options) + static, + required TResult Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options) + yubiotp, + }) { + return yubiotp(publicId, privateId, key, options); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String key, SlotConfigurationOptions? options)? hotp, + TResult? Function(String key, SlotConfigurationOptions? options)? chalresp, + TResult? Function(String password, String keyboardLayout, + SlotConfigurationOptions? options)? + static, + TResult? Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options)? + yubiotp, + }) { + return yubiotp?.call(publicId, privateId, key, options); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String key, SlotConfigurationOptions? options)? hotp, + TResult Function(String key, SlotConfigurationOptions? options)? chalresp, + TResult Function(String password, String keyboardLayout, + SlotConfigurationOptions? options)? + static, + TResult Function(String publicId, String privateId, String key, + SlotConfigurationOptions? options)? + yubiotp, + required TResult orElse(), + }) { + if (yubiotp != null) { + return yubiotp(publicId, privateId, key, options); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_SlotConfigurationHotp value) hotp, + required TResult Function(_SlotConfigurationHmacSha1 value) chalresp, + required TResult Function(_SlotConfigurationStaticPassword value) static, + required TResult Function(_SlotConfigurationYubiOtp value) yubiotp, + }) { + return yubiotp(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SlotConfigurationHotp value)? hotp, + TResult? Function(_SlotConfigurationHmacSha1 value)? chalresp, + TResult? Function(_SlotConfigurationStaticPassword value)? static, + TResult? Function(_SlotConfigurationYubiOtp value)? yubiotp, + }) { + return yubiotp?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SlotConfigurationHotp value)? hotp, + TResult Function(_SlotConfigurationHmacSha1 value)? chalresp, + TResult Function(_SlotConfigurationStaticPassword value)? static, + TResult Function(_SlotConfigurationYubiOtp value)? yubiotp, + required TResult orElse(), + }) { + if (yubiotp != null) { + return yubiotp(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$_SlotConfigurationYubiOtpToJson( + this, + ); + } +} + +abstract class _SlotConfigurationYubiOtp extends SlotConfiguration { + const factory _SlotConfigurationYubiOtp( + {required final String publicId, + required final String privateId, + required final String key, + final SlotConfigurationOptions? options}) = _$_SlotConfigurationYubiOtp; + const _SlotConfigurationYubiOtp._() : super._(); + + factory _SlotConfigurationYubiOtp.fromJson(Map json) = + _$_SlotConfigurationYubiOtp.fromJson; + + String get publicId; + String get privateId; + String get key; + @override + SlotConfigurationOptions? get options; + @override + @JsonKey(ignore: true) + _$$_SlotConfigurationYubiOtpCopyWith<_$_SlotConfigurationYubiOtp> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/otp/models.g.dart b/lib/otp/models.g.dart new file mode 100644 index 00000000..876aa236 --- /dev/null +++ b/lib/otp/models.g.dart @@ -0,0 +1,160 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$_OtpState _$$_OtpStateFromJson(Map json) => _$_OtpState( + slot1Configured: json['slot1_configured'] as bool, + slot2Configured: json['slot2_configured'] as bool, + ); + +Map _$$_OtpStateToJson(_$_OtpState instance) => + { + 'slot1_configured': instance.slot1Configured, + 'slot2_configured': instance.slot2Configured, + }; + +_$_SlotConfigurationOptions _$$_SlotConfigurationOptionsFromJson( + Map json) => + _$_SlotConfigurationOptions( + digits8: json['digits8'] as bool?, + requireTouch: json['require_touch'] as bool?, + appendCr: json['append_cr'] as bool?, + ); + +Map _$$_SlotConfigurationOptionsToJson( + _$_SlotConfigurationOptions instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('digits8', instance.digits8); + writeNotNull('require_touch', instance.requireTouch); + writeNotNull('append_cr', instance.appendCr); + return val; +} + +_$_SlotConfigurationHotp _$$_SlotConfigurationHotpFromJson( + Map json) => + _$_SlotConfigurationHotp( + key: json['key'] as String, + options: json['options'] == null + ? null + : SlotConfigurationOptions.fromJson( + json['options'] as Map), + $type: json['type'] as String?, + ); + +Map _$$_SlotConfigurationHotpToJson( + _$_SlotConfigurationHotp instance) { + final val = { + 'key': instance.key, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('options', instance.options?.toJson()); + val['type'] = instance.$type; + return val; +} + +_$_SlotConfigurationHmacSha1 _$$_SlotConfigurationHmacSha1FromJson( + Map json) => + _$_SlotConfigurationHmacSha1( + key: json['key'] as String, + options: json['options'] == null + ? null + : SlotConfigurationOptions.fromJson( + json['options'] as Map), + $type: json['type'] as String?, + ); + +Map _$$_SlotConfigurationHmacSha1ToJson( + _$_SlotConfigurationHmacSha1 instance) { + final val = { + 'key': instance.key, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('options', instance.options?.toJson()); + val['type'] = instance.$type; + return val; +} + +_$_SlotConfigurationStaticPassword _$$_SlotConfigurationStaticPasswordFromJson( + Map json) => + _$_SlotConfigurationStaticPassword( + password: json['password'] as String, + keyboardLayout: json['keyboard_layout'] as String, + options: json['options'] == null + ? null + : SlotConfigurationOptions.fromJson( + json['options'] as Map), + $type: json['type'] as String?, + ); + +Map _$$_SlotConfigurationStaticPasswordToJson( + _$_SlotConfigurationStaticPassword instance) { + final val = { + 'password': instance.password, + 'keyboard_layout': instance.keyboardLayout, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('options', instance.options?.toJson()); + val['type'] = instance.$type; + return val; +} + +_$_SlotConfigurationYubiOtp _$$_SlotConfigurationYubiOtpFromJson( + Map json) => + _$_SlotConfigurationYubiOtp( + publicId: json['public_id'] as String, + privateId: json['private_id'] as String, + key: json['key'] as String, + options: json['options'] == null + ? null + : SlotConfigurationOptions.fromJson( + json['options'] as Map), + $type: json['type'] as String?, + ); + +Map _$$_SlotConfigurationYubiOtpToJson( + _$_SlotConfigurationYubiOtp instance) { + final val = { + 'public_id': instance.publicId, + 'private_id': instance.privateId, + 'key': instance.key, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('options', instance.options?.toJson()); + val['type'] = instance.$type; + return val; +} diff --git a/lib/otp/state.dart b/lib/otp/state.dart new file mode 100644 index 00000000..0772b455 --- /dev/null +++ b/lib/otp/state.dart @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../app/models.dart'; +import '../core/state.dart'; +import 'models.dart'; + +final otpStateProvider = AsyncNotifierProvider.autoDispose + .family( + () => throw UnimplementedError(), +); + +abstract class OtpStateNotifier extends ApplicationStateNotifier { + Future generateStaticPassword(int length, String layout); + Future modhexEncodeSerial(int serial); + Future>> getKeyboardLayouts(); + Future swapSlots(); + Future configureSlot(SlotId slot, + {required SlotConfiguration configuration}); + Future deleteSlot(SlotId slot); +} diff --git a/lib/otp/views/actions.dart b/lib/otp/views/actions.dart new file mode 100644 index 00000000..d4fa8a2a --- /dev/null +++ b/lib/otp/views/actions.dart @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:yubico_authenticator/otp/state.dart'; +import 'package:yubico_authenticator/otp/views/configure_chalresp_dialog.dart'; +import 'package:yubico_authenticator/otp/views/configure_hotp_dialog.dart'; +import 'package:yubico_authenticator/otp/views/configure_static_dialog.dart'; +import 'package:yubico_authenticator/otp/views/configure_yubiotp_dialog.dart'; +import 'package:yubico_authenticator/otp/views/delete_slot_dialog.dart'; + +import '../../app/message.dart'; +import '../../app/shortcuts.dart'; +import '../../app/state.dart'; +import '../../app/models.dart'; +import '../../core/state.dart'; +import '../models.dart'; +import '../keys.dart' as keys; +import '../features.dart' as features; + +class ConfigureIntent extends Intent { + const ConfigureIntent({required this.configurationType}); + + final SlotConfigurationType configurationType; +} + +Widget registerOtpActions( + DevicePath devicePath, + OtpSlot otpSlot, { + required WidgetRef ref, + required Widget Function(BuildContext context) builder, + Map> actions = const {}, +}) { + final hasFeature = ref.watch(featureProvider); + return Actions( + actions: { + if (hasFeature(features.slotsConfigure)) + ConfigureIntent: + CallbackAction(onInvoke: (intent) async { + final withContext = ref.read(withContextProvider); + final configurationType = intent.configurationType; + + switch (configurationType) { + case SlotConfigurationType.chalresp: + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => + ConfigureChalrespDialog(devicePath, otpSlot)); + }); + case SlotConfigurationType.hotp: + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => + ConfigureHotpDialog(devicePath, otpSlot)); + }); + case SlotConfigurationType.static: + final keyboardLayouts = await ref + .read(otpStateProvider(devicePath).notifier) + .getKeyboardLayouts(); + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => ConfigureStaticDialog( + devicePath, otpSlot, keyboardLayouts)); + }); + case SlotConfigurationType.yubiotp: + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => + ConfigureYubiOtpDialog(devicePath, otpSlot)); + }); + default: + break; + } + + return null; + }), + if (hasFeature(features.slotsDelete)) + DeleteIntent: CallbackAction(onInvoke: (_) async { + final withContext = ref.read(withContextProvider); + + final bool? deleted = await withContext((context) async => + await showBlurDialog( + context: context, + builder: (context) => + DeleteSlotDialog(devicePath, otpSlot)) ?? + false); + return deleted; + }), + ...actions, + }, + child: Builder(builder: builder), + ); +} + +List buildSlotActions(bool isConfigured, AppLocalizations l10n) { + return [ + ActionItem( + key: keys.configureYubiOtp, + feature: features.slotsConfigure, + icon: const Icon(Icons.shuffle_outlined), + title: l10n.s_yubiotp, + subtitle: l10n.l_yubiotp_desc, + intent: const ConfigureIntent( + configurationType: SlotConfigurationType.yubiotp)), + ActionItem( + key: keys.configureChalResp, + feature: features.slotsConfigure, + icon: const Icon(Icons.key_outlined), + title: l10n.s_challenge_response, + subtitle: l10n.l_challenge_response_desc, + intent: const ConfigureIntent( + configurationType: SlotConfigurationType.chalresp)), + ActionItem( + key: keys.configureStatic, + feature: features.slotsConfigure, + icon: const Icon(Icons.password_outlined), + title: l10n.s_static_password, + subtitle: l10n.l_static_password_desc, + intent: const ConfigureIntent( + configurationType: SlotConfigurationType.static)), + ActionItem( + key: keys.configureHotp, + feature: features.slotsConfigure, + icon: const Icon(Icons.tag_outlined), + title: l10n.s_hotp, + subtitle: l10n.l_hotp_desc, + intent: const ConfigureIntent( + configurationType: SlotConfigurationType.hotp)), + ActionItem( + key: keys.deleteAction, + feature: features.slotsDelete, + actionStyle: ActionStyle.error, + icon: const Icon(Icons.delete_outline), + title: l10n.s_delete_slot, + subtitle: l10n.l_delete_slot_desc, + intent: isConfigured ? const DeleteIntent() : null, + ) + ]; +} diff --git a/lib/otp/views/configure_chalresp_dialog.dart b/lib/otp/views/configure_chalresp_dialog.dart new file mode 100644 index 00000000..72115a05 --- /dev/null +++ b/lib/otp/views/configure_chalresp_dialog.dart @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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/state.dart'; +import 'package:logging/logging.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../app/state.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; +import 'overwrite_confirm_dialog.dart'; + +final _log = Logger('otp.view.configure_Chalresp_dialog'); + +class ConfigureChalrespDialog extends ConsumerStatefulWidget { + final DevicePath devicePath; + final OtpSlot otpSlot; + const ConfigureChalrespDialog(this.devicePath, this.otpSlot, {super.key}); + + @override + ConsumerState createState() => + _ConfigureChalrespDialogState(); +} + +class _ConfigureChalrespDialogState + extends ConsumerState { + final _keyController = TextEditingController(); + bool _invalidKeyLength = false; + bool _configuring = false; + bool _requireTouch = false; + final int maxLength = 40; + + @override + void dispose() { + _keyController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + final secret = _keyController.text.replaceAll(' ', ''); + + return ResponsiveDialog( + allowCancel: !_configuring, + title: Text(l10n.s_challenge_response), + actions: [ + TextButton( + key: keys.saveButton, + onPressed: _configuring || _invalidKeyLength + ? null + : () async { + if (!(secret.isNotEmpty && secret.length <= maxLength)) { + setState(() { + _configuring = false; + _invalidKeyLength = true; + }); + return; + } + + if (!await confirmOverwrite(context, widget.otpSlot)) { + return; + } + + setState(() { + _configuring = true; + }); + + final otpNotifier = + ref.read(otpStateProvider(widget.devicePath).notifier); + try { + await otpNotifier.configureSlot(widget.otpSlot.slot, + configuration: SlotConfiguration.chalresp( + key: secret, + options: SlotConfigurationOptions( + requireTouch: _requireTouch))); + await ref.read(withContextProvider)((context) async { + Navigator.of(context).pop(); + showMessage(context, + l10n.l_slot_configuration_programmed(l10n.s_hotp)); + }); + } catch (e) { + _log.error('Failed to program credential', e); + await ref.read(withContextProvider)((context) async { + showMessage( + context, + l10n.p_otp_slot_configuration_error( + widget.otpSlot.slot.getDisplayName(l10n)), + duration: const Duration(seconds: 4), + ); + }); + } + }, + child: Text(l10n.s_save), + ) + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + key: keys.secretField, + autofocus: true, + controller: _keyController, + autofillHints: isAndroid ? [] : const [AutofillHints.password], + maxLength: maxLength, + decoration: InputDecoration( + suffixIcon: IconButton( + tooltip: l10n.s_generate_secret_key, + icon: const Icon(Icons.refresh), + onPressed: () { + final random = Random.secure(); + final key = List.generate( + 20, + (_) => random + .nextInt(256) + .toRadixString(16) + .padLeft(2, '0')).join(); + setState(() { + _keyController.text = key; + }); + }, + ), + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.key_outlined), + labelText: l10n.s_secret_key, + errorText: _invalidKeyLength ? l10n.s_invalid_length : null), + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp('[a-f0-9]', caseSensitive: false)) + ], + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _invalidKeyLength = false; + }); + }, + ), + FilterChip( + label: Text(l10n.s_require_touch), + selected: _requireTouch, + onSelected: (value) { + setState(() { + _requireTouch = value; + }); + }, + ) + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/otp/views/configure_hotp_dialog.dart b/lib/otp/views/configure_hotp_dialog.dart new file mode 100644 index 00000000..490fe2cd --- /dev/null +++ b/lib/otp/views/configure_hotp_dialog.dart @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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/state.dart'; +import 'package:yubico_authenticator/oath/models.dart'; +import 'package:logging/logging.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../app/state.dart'; +import '../../widgets/choice_filter_chip.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; +import 'overwrite_confirm_dialog.dart'; + +final _log = Logger('otp.view.configure_hotp_dialog'); + +class ConfigureHotpDialog extends ConsumerStatefulWidget { + final DevicePath devicePath; + final OtpSlot otpSlot; + const ConfigureHotpDialog(this.devicePath, this.otpSlot, {super.key}); + + @override + ConsumerState createState() => + _ConfigureHotpDialogState(); +} + +class _ConfigureHotpDialogState extends ConsumerState { + final _keyController = TextEditingController(); + bool _invalidKeyLength = false; + int _digits = defaultDigits; + final List _digitsValues = [6, 8]; + bool _appendEnter = true; + bool _configuring = false; + bool _isObscure = true; + + @override + void dispose() { + _keyController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + final secret = _keyController.text.replaceAll(' ', ''); + + return ResponsiveDialog( + allowCancel: !_configuring, + title: Text(l10n.s_hotp), + actions: [ + TextButton( + key: keys.saveButton, + onPressed: _configuring || _invalidKeyLength + ? null + : () async { + if (!(secret.isNotEmpty && secret.length * 5 % 8 < 5)) { + setState(() { + _configuring = false; + _invalidKeyLength = true; + }); + return; + } + + if (!await confirmOverwrite(context, widget.otpSlot)) { + return; + } + + setState(() { + _configuring = true; + }); + + final otpNotifier = + ref.read(otpStateProvider(widget.devicePath).notifier); + try { + await otpNotifier.configureSlot(widget.otpSlot.slot, + configuration: SlotConfiguration.hotp( + key: secret, + options: SlotConfigurationOptions( + digits8: _digits == 8, + appendCr: _appendEnter))); + await ref.read(withContextProvider)((context) async { + Navigator.of(context).pop(); + showMessage(context, + l10n.l_slot_configuration_programmed(l10n.s_hotp)); + }); + } catch (e) { + _log.error('Failed to program credential', e); + await ref.read(withContextProvider)((context) async { + showMessage( + context, + l10n.p_otp_slot_configuration_error( + widget.otpSlot.slot.getDisplayName(l10n)), + duration: const Duration(seconds: 4), + ); + }); + } + }, + child: Text(l10n.s_save), + ) + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + key: keys.secretField, + controller: _keyController, + obscureText: _isObscure, + autofillHints: isAndroid ? [] : const [AutofillHints.password], + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + '[abcdefghijklmnopqrstuvwxyz234567 ]', + caseSensitive: false)) + ], + decoration: InputDecoration( + suffixIcon: IconButton( + icon: Icon( + _isObscure ? Icons.visibility : Icons.visibility_off, + color: IconTheme.of(context).color, + ), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + ), + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.key_outlined), + labelText: l10n.s_secret_key, + errorText: _invalidKeyLength ? l10n.s_invalid_length : null), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _invalidKeyLength = false; + }); + }, + ), + const SizedBox(height: 8), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4.0, + runSpacing: 8.0, + children: [ + ChoiceFilterChip( + items: _digitsValues, + value: _digits, + selected: _digits != defaultDigits, + itemBuilder: (value) => Text(l10n.s_num_digits(value)), + onChanged: (digits) { + setState(() { + _digits = digits; + }); + }), + FilterChip( + label: Text(l10n.s_append_enter), + tooltip: l10n.l_append_enter_desc, + selected: _appendEnter, + onSelected: (value) { + setState(() { + _appendEnter = value; + }); + }, + ) + ], + ) + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/otp/views/configure_static_dialog.dart b/lib/otp/views/configure_static_dialog.dart new file mode 100644 index 00000000..7ade92bd --- /dev/null +++ b/lib/otp/views/configure_static_dialog.dart @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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/state.dart'; +import 'package:logging/logging.dart'; +import 'package:yubico_authenticator/widgets/choice_filter_chip.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../app/state.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; +import 'overwrite_confirm_dialog.dart'; + +final _log = Logger('otp.view.configure_Chalresp_dialog'); + +class ConfigureStaticDialog extends ConsumerStatefulWidget { + final DevicePath devicePath; + final OtpSlot otpSlot; + final Map> keyboardLayouts; + const ConfigureStaticDialog( + this.devicePath, this.otpSlot, this.keyboardLayouts, + {super.key}); + + @override + ConsumerState createState() => + _ConfigureStaticDialogState(); +} + +class _ConfigureStaticDialogState extends ConsumerState { + final _passwordController = TextEditingController(); + bool _invalidPasswordLength = false; + bool _configuring = false; + bool _appendEnter = true; + String _keyboardLayout = ''; + final maxLength = 38; + + @override + void initState() { + super.initState(); + _keyboardLayout = widget.keyboardLayouts.keys.toList()[0]; + } + + @override + void dispose() { + _passwordController.dispose(); + super.dispose(); + } + + RegExp generateFormatterPattern(String layout) { + final allowedCharacters = widget.keyboardLayouts[layout] ?? []; + + final pattern = + allowedCharacters.map((char) => RegExp.escape(char)).join(''); + + return RegExp('[$pattern]', caseSensitive: false); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + final secret = _passwordController.text.replaceAll(' ', ''); + + return ResponsiveDialog( + allowCancel: !_configuring, + title: Text(l10n.s_static_password), + actions: [ + TextButton( + key: keys.saveButton, + onPressed: _configuring || _invalidPasswordLength + ? null + : () async { + if (!(secret.isNotEmpty && secret.length <= maxLength)) { + setState(() { + _configuring = false; + _invalidPasswordLength = true; + }); + return; + } + + if (!await confirmOverwrite(context, widget.otpSlot)) { + return; + } + + setState(() { + _configuring = true; + }); + + final otpNotifier = + ref.read(otpStateProvider(widget.devicePath).notifier); + try { + await otpNotifier.configureSlot(widget.otpSlot.slot, + configuration: SlotConfiguration.static( + password: secret, + keyboardLayout: _keyboardLayout, + options: SlotConfigurationOptions( + appendCr: _appendEnter))); + await ref.read(withContextProvider)((context) async { + Navigator.of(context).pop(); + showMessage( + context, + l10n.l_slot_configuration_programmed( + l10n.s_static_password)); + }); + } catch (e) { + _log.error('Failed to program credential', e); + await ref.read(withContextProvider)((context) async { + showMessage( + context, + l10n.p_otp_slot_configuration_error( + widget.otpSlot.slot.getDisplayName(l10n)), + duration: const Duration(seconds: 4), + ); + }); + } + }, + child: Text(l10n.s_save), + ) + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + key: keys.secretField, + autofocus: true, + controller: _passwordController, + autofillHints: isAndroid ? [] : const [AutofillHints.password], + maxLength: maxLength, + decoration: InputDecoration( + suffixIcon: IconButton( + tooltip: l10n.s_generate_passowrd, + icon: const Icon(Icons.refresh), + onPressed: () async { + final password = await ref + .read(otpStateProvider(widget.devicePath).notifier) + .generateStaticPassword(maxLength, _keyboardLayout); + setState(() { + _passwordController.text = password; + }); + }, + ), + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.key_outlined), + labelText: l10n.s_password, + errorText: + _invalidPasswordLength ? l10n.s_invalid_length : null), + inputFormatters: [ + FilteringTextInputFormatter.allow( + generateFormatterPattern(_keyboardLayout)) + ], + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _invalidPasswordLength = false; + }); + }, + ), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4.0, + runSpacing: 8.0, + children: [ + ChoiceFilterChip( + items: widget.keyboardLayouts.keys.toList(), + value: _keyboardLayout, + itemBuilder: (value) => Text(value), + onChanged: (layout) { + setState(() { + _keyboardLayout = layout; + }); + }), + FilterChip( + label: Text(l10n.s_append_enter), + tooltip: l10n.l_append_enter_desc, + selected: _appendEnter, + onSelected: (value) { + setState(() { + _appendEnter = value; + }); + }, + ) + ], + ) + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/otp/views/configure_yubiotp_dialog.dart b/lib/otp/views/configure_yubiotp_dialog.dart new file mode 100644 index 00000000..a2fe9df0 --- /dev/null +++ b/lib/otp/views/configure_yubiotp_dialog.dart @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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/state.dart'; +import 'package:logging/logging.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../app/state.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; +import 'overwrite_confirm_dialog.dart'; + +final _log = Logger('otp.view.configure_yubiotp_dialog'); + +final _modhexPattern = RegExp('[cbdefghijklnrtuv]', caseSensitive: false); + +class ConfigureYubiOtpDialog extends ConsumerStatefulWidget { + final DevicePath devicePath; + final OtpSlot otpSlot; + const ConfigureYubiOtpDialog(this.devicePath, this.otpSlot, {super.key}); + + @override + ConsumerState createState() => + _ConfigureYubiOtpDialogState(); +} + +class _ConfigureYubiOtpDialogState + extends ConsumerState { + final _keyController = TextEditingController(); + final _publicIdController = TextEditingController(); + final _privateIdController = TextEditingController(); + final maxLengthKey = 32; + final maxLengthPublicId = 12; + final maxLengthPrivateId = 12; + bool _appendEnter = true; + bool _configuring = false; + + @override + void dispose() { + _keyController.dispose(); + _publicIdController.dispose(); + _privateIdController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + final info = ref.watch(currentDeviceDataProvider).valueOrNull?.info; + + final secret = _keyController.text.replaceAll(' ', ''); + final privateId = _privateIdController.text; + String publicId = _publicIdController.text; + + return ResponsiveDialog( + allowCancel: !_configuring, + title: Text(l10n.s_yubiotp), + actions: [ + TextButton( + key: keys.saveButton, + onPressed: _configuring + ? null + : () async { + if (!(secret.isNotEmpty && secret.length * 5 % 8 < 5)) { + setState(() { + _configuring = false; + }); + return; + } + + if (!await confirmOverwrite(context, widget.otpSlot)) { + return; + } + + setState(() { + _configuring = true; + }); + + final otpNotifier = + ref.read(otpStateProvider(widget.devicePath).notifier); + try { + if (info != null && publicId == info.serial.toString()) { + publicId = + await otpNotifier.modhexEncodeSerial(info.serial!); + } + await otpNotifier.configureSlot(widget.otpSlot.slot, + configuration: SlotConfiguration.yubiotp( + publicId: publicId, + privateId: privateId, + key: secret, + options: SlotConfigurationOptions( + appendCr: _appendEnter))); + await ref.read(withContextProvider)((context) async { + Navigator.of(context).pop(); + showMessage(context, + l10n.l_slot_configuration_programmed(l10n.s_yubiotp)); + }); + } catch (e) { + _log.error('Failed to program credential', e); + await ref.read(withContextProvider)((context) async { + showMessage( + context, + l10n.p_otp_slot_configuration_error( + widget.otpSlot.slot.getDisplayName(l10n)), + duration: const Duration(seconds: 4), + ); + }); + } + }, + child: Text(l10n.s_save), + ) + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + key: keys.publicIdField, + autofocus: true, + controller: _publicIdController, + autofillHints: isAndroid ? [] : const [AutofillHints.password], + maxLength: maxLengthPublicId, + decoration: InputDecoration( + suffixIcon: IconButton( + tooltip: l10n.s_use_serial, + icon: const Icon(Icons.auto_awesome_outlined), + onPressed: () { + setState(() { + _publicIdController.text = info!.serial.toString(); + }); + }, + ), + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.public_outlined), + labelText: l10n.s_public_id), + inputFormatters: [ + FilteringTextInputFormatter.allow(_modhexPattern) + ], + textInputAction: TextInputAction.next, + ), + TextField( + key: keys.privateIdField, + controller: _privateIdController, + autofillHints: isAndroid ? [] : const [AutofillHints.password], + maxLength: maxLengthPrivateId, + decoration: InputDecoration( + suffixIcon: IconButton( + tooltip: l10n.s_generate_private_id, + icon: const Icon(Icons.refresh), + onPressed: () { + final random = Random.secure(); + final key = List.generate( + 6, + (_) => random + .nextInt(256) + .toRadixString(16) + .padLeft(2, '0')).join(); + setState(() { + _privateIdController.text = key; + }); + }, + ), + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.key_outlined), + labelText: l10n.s_private_id), + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp('[a-f0-9]', caseSensitive: false)) + ], + textInputAction: TextInputAction.next, + ), + TextField( + key: keys.secretField, + controller: _keyController, + autofillHints: isAndroid ? [] : const [AutofillHints.password], + maxLength: maxLengthKey, + decoration: InputDecoration( + suffixIcon: IconButton( + tooltip: l10n.s_generate_secret_key, + icon: const Icon(Icons.refresh), + onPressed: () { + final random = Random.secure(); + final key = List.generate( + 16, + (_) => random + .nextInt(256) + .toRadixString(16) + .padLeft(2, '0')).join(); + setState(() { + _keyController.text = key; + }); + }, + ), + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.key_outlined), + labelText: l10n.s_secret_key), + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp('[a-f0-9]', caseSensitive: false)) + ], + textInputAction: TextInputAction.next, + ), + 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; + }); + }, + ) + ], + ) + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/otp/views/delete_slot_dialog.dart b/lib/otp/views/delete_slot_dialog.dart new file mode 100644 index 00000000..73ee0653 --- /dev/null +++ b/lib/otp/views/delete_slot_dialog.dart @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../app/state.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; + +class DeleteSlotDialog extends ConsumerWidget { + final DevicePath devicePath; + final OtpSlot otpSlot; + const DeleteSlotDialog(this.devicePath, this.otpSlot, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + return ResponsiveDialog( + title: Text(l10n.l_delete_certificate), + actions: [ + TextButton( + key: keys.deleteButton, + onPressed: () async { + try { + await ref + .read(otpStateProvider(devicePath).notifier) + .deleteSlot(otpSlot.slot); + await ref.read(withContextProvider)((context) async { + Navigator.of(context).pop(true); + showMessage(context, l10n.l_slot_deleted); + }); + } catch (e) { + await ref.read(withContextProvider)((context) async { + Navigator.of(context).pop(true); + showMessage( + context, + l10n.p_otp_slot_configuration_error( + otpSlot.slot.getDisplayName(l10n)), + duration: const Duration(seconds: 4), + ); + }); + } + }, + child: Text(l10n.s_delete), + ) + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.p_warning_delete_slot_configuration( + otpSlot.slot.getDisplayName(l10n))), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/otp/views/key_actions.dart b/lib/otp/views/key_actions.dart new file mode 100644 index 00000000..b70882a3 --- /dev/null +++ b/lib/otp/views/key_actions.dart @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../app/views/fs_dialog.dart'; +import '../../app/views/action_list.dart'; +import '../models.dart'; +import '../keys.dart' as keys; +import '../features.dart' as features; +import 'swap_slots_dialog.dart'; + +Widget otpBuildActions(BuildContext context, DevicePath devicePath, + OtpState otpState, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + + return FsDialog( + child: Column( + children: [ + ActionListSection(l10n.s_manage, children: [ + ActionListItem( + key: keys.swapSlots, + feature: features.actionsSwap, + title: l10n.s_swap_slots, + subtitle: l10n.l_swap_slots_desc, + icon: const Icon(Icons.swap_vert_outlined), + onTap: (otpState.slot1Configured || otpState.slot2Configured) + ? (context) { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => SwapSlotsDialog(devicePath)); + } + : null, + ) + ]) + ], + ), + ); +} diff --git a/lib/otp/views/otp_screen.dart b/lib/otp/views/otp_screen.dart new file mode 100644 index 00000000..0193b5a0 --- /dev/null +++ b/lib/otp/views/otp_screen.dart @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2022 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../app/shortcuts.dart'; +import '../../app/views/app_failure_page.dart'; +import '../../app/views/app_list_item.dart'; +import '../../app/views/app_page.dart'; +import '../../app/views/message_page.dart'; +import '../../core/state.dart'; +import '../../widgets/list_title.dart'; +import '../models.dart'; +import '../state.dart'; +import '../features.dart' as features; +import 'actions.dart'; +import 'key_actions.dart'; +import 'slot_dialog.dart'; + +class OtpScreen extends ConsumerWidget { + final DevicePath devicePath; + + const OtpScreen(this.devicePath, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + final hasFeature = ref.watch(featureProvider); + return ref.watch(otpStateProvider(devicePath)).when( + loading: () => MessagePage( + title: Text(l10n.s_otp), + graphic: const CircularProgressIndicator(), + delayedContent: true, + ), + error: (error, _) => + AppFailurePage(title: Text(l10n.s_otp), cause: error), + data: (otpState) { + return AppPage( + title: Text(l10n.s_otp), + keyActionsBuilder: hasFeature(features.actions) + ? (context) => + otpBuildActions(context, devicePath, otpState, ref) + : null, + child: Column(children: [ + ListTitle(l10n.s_otp_slots), + ...otpState.slots.map((e) => registerOtpActions(devicePath, e, + ref: ref, + actions: { + OpenIntent: CallbackAction(onInvoke: (_) async { + await showBlurDialog( + context: context, + barrierColor: Colors.transparent, + builder: (context) => SlotDialog(e.slot), + ); + return null; + }), + }, + builder: (context) => _SlotListItem(e))) + ]), + ); + }); + } +} + +class _SlotListItem extends ConsumerWidget { + final OtpSlot otpSlot; + const _SlotListItem(this.otpSlot); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final slot = otpSlot.slot; + final l10n = AppLocalizations.of(context)!; + final colorScheme = Theme.of(context).colorScheme; + final isConfigured = otpSlot.isConfigured; + final hasFeature = ref.watch(featureProvider); + + return Semantics( + label: slot.getDisplayName(l10n), + child: AppListItem( + leading: CircleAvatar( + foregroundColor: colorScheme.onSecondary, + backgroundColor: colorScheme.secondary, + child: const Icon(Icons.password_outlined)), + title: slot.getDisplayName(l10n), + subtitle: isConfigured + ? l10n.l_otp_slot_programmed + : l10n.l_otp_slot_not_programmed, + trailing: OutlinedButton( + onPressed: Actions.handler(context, const OpenIntent()), + child: const Icon(Icons.more_horiz), + ), + buildPopupActions: hasFeature(features.slots) + ? (context) => buildSlotActions(isConfigured, l10n) + : null, + )); + } +} diff --git a/lib/otp/views/overwrite_confirm_dialog.dart b/lib/otp/views/overwrite_confirm_dialog.dart new file mode 100644 index 00000000..54de5436 --- /dev/null +++ b/lib/otp/views/overwrite_confirm_dialog.dart @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../../app/message.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; + +class _OverwriteConfirmDialog extends StatelessWidget { + final OtpSlot otpSlot; + + const _OverwriteConfirmDialog({ + required this.otpSlot, + }); + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return ResponsiveDialog( + title: Text(l10n.s_overwrite_slot), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text(l10n.s_overwrite)), + ], + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.p_overwrite_slot_desc(otpSlot.slot.getDisplayName(l10n))), + const SizedBox(height: 12), + ], + ), + ), + ); + } +} + +Future confirmOverwrite(BuildContext context, OtpSlot otpSlot) async { + if (otpSlot.isConfigured) { + return await showBlurDialog( + context: context, + builder: (context) => _OverwriteConfirmDialog( + otpSlot: otpSlot, + )) ?? + false; + } + return true; +} diff --git a/lib/otp/views/slot_dialog.dart b/lib/otp/views/slot_dialog.dart new file mode 100644 index 00000000..e716f9f2 --- /dev/null +++ b/lib/otp/views/slot_dialog.dart @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:yubico_authenticator/otp/state.dart'; + +import '../../app/state.dart'; +import '../../app/views/fs_dialog.dart'; +import '../../app/views/action_list.dart'; +import '../models.dart'; +import 'actions.dart'; + +class SlotDialog extends ConsumerWidget { + final SlotId slot; + const SlotDialog(this.slot, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // TODO: Solve this in a cleaner way + final node = ref.watch(currentDeviceDataProvider).valueOrNull?.node; + if (node == null) { + // The rest of this method assumes there is a device, and will throw an exception if not. + // This will never be shown, as the dialog will be immediately closed + return const SizedBox(); + } + + final l10n = AppLocalizations.of(context)!; + final textTheme = Theme.of(context).textTheme; + + final otpState = ref.watch(otpStateProvider(node.path)).valueOrNull; + final otpSlot = + otpState!.slots.firstWhereOrNull((element) => element.slot == slot); + + if (otpSlot == null) { + return const FsDialog(child: CircularProgressIndicator()); + } + + return registerOtpActions(node.path, otpSlot, + ref: ref, + builder: (context) => FocusScope( + autofocus: true, + child: FsDialog( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 48, bottom: 16), + child: Column( + children: [ + Text( + otpSlot.slot.getDisplayName(l10n), + style: textTheme.headlineSmall, + softWrap: true, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + const Icon( + Icons.touch_app, + size: 100.0, + ) + ], + ), + ), + ActionListSection.fromMenuActions( + context, + l10n.s_actions, + actions: buildSlotActions(otpSlot.isConfigured, l10n), + ) + ], + ), + ), + )); + } +} diff --git a/lib/otp/views/swap_slots_dialog.dart b/lib/otp/views/swap_slots_dialog.dart new file mode 100644 index 00000000..7a7eddbc --- /dev/null +++ b/lib/otp/views/swap_slots_dialog.dart @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/message.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../state.dart'; +import '../../app/models.dart'; +import '../../app/state.dart'; + +class SwapSlotsDialog extends ConsumerWidget { + final DevicePath devicePath; + const SwapSlotsDialog(this.devicePath, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + return ResponsiveDialog( + title: Text(l10n.s_swap_slots), + actions: [ + TextButton( + onPressed: () async { + await ref.read(otpStateProvider(devicePath).notifier).swapSlots(); + await ref.read(withContextProvider)((context) async { + Navigator.of(context).pop(); + showMessage(context, l10n.l_slots_swapped); + }); + }, + child: Text(l10n.s_swap)) + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.p_swap_slots_desc), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} From 607ff95e310b82eae4aebf5741afdc8ec6ec5eb5 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Fri, 17 Nov 2023 13:02:51 +0100 Subject: [PATCH 03/27] Add export of YubiOTP credential. --- helper/helper/yubiotp.py | 13 +- lib/desktop/otp/state.dart | 12 ++ lib/l10n/app_en.arb | 8 + lib/otp/state.dart | 2 + lib/otp/views/configure_chalresp_dialog.dart | 43 ++--- lib/otp/views/configure_hotp_dialog.dart | 36 ++-- lib/otp/views/configure_static_dialog.dart | 67 ++++--- lib/otp/views/configure_yubiotp_dialog.dart | 187 ++++++++++++++----- lib/widgets/choice_filter_chip.dart | 4 +- 9 files changed, 256 insertions(+), 116 deletions(-) diff --git a/helper/helper/yubiotp.py b/helper/helper/yubiotp.py index 1df0526c..e8a37981 100644 --- a/helper/helper/yubiotp.py +++ b/helper/helper/yubiotp.py @@ -26,7 +26,7 @@ from yubikit.yubiotp import ( YubiOtpSlotConfiguration, StaticTicketSlotConfiguration, ) -from ykman.otp import generate_static_pw +from ykman.otp import generate_static_pw, format_csv from yubikit.oath import parse_b32_key from ykman.scancodes import KEYBOARD_LAYOUT, encode @@ -93,6 +93,16 @@ class YubiOtpNode(RpcNode): def keyboard_layouts(self, params, event, signal): return {layout.name:[sc for sc in layout.value] for layout in KEYBOARD_LAYOUT} + @action(closes_child=False) + def format_yubiotp_csv(self, params, even, signal): + serial = params["serial"] + public_id = modhex_decode(params["public_id"]) + private_id = bytes.fromhex(params["private_id"]) + key = bytes.fromhex(params["key"]) + + return dict(csv=format_csv(serial, public_id, private_id, key)) + + _CONFIG_TYPES = dict( hmac_sha1=HmacSha1SlotConfiguration, hotp=HotpSlotConfiguration, @@ -101,7 +111,6 @@ _CONFIG_TYPES = dict( static_ticket=StaticTicketSlotConfiguration, ) - class SlotNode(RpcNode): def __init__(self, session, slot): super().__init__() diff --git a/lib/desktop/otp/state.dart b/lib/desktop/otp/state.dart index 0109d76d..586e070c 100644 --- a/lib/desktop/otp/state.dart +++ b/lib/desktop/otp/state.dart @@ -84,6 +84,18 @@ class _DesktopOtpStateNotifier extends OtpStateNotifier { MapEntry(key, (value as List).cast().toList()))); } + @override + Future formatYubiOtpCsv( + int serial, String publicId, String privateId, String key) async { + final result = await _session.command('format_yubiotp_csv', params: { + 'serial': serial, + 'public_id': publicId, + 'private_id': privateId, + 'key': key + }); + return result['csv']; + } + @override Future deleteSlot(SlotId slot) async { await _session.command('delete', target: [slot.id]); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f7c1094e..f818a870 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -79,6 +79,7 @@ "s_hide_secret_key": "Hide secret key", "s_private_key": "Private key", "s_invalid_length": "Invalid length", + "s_invalid_format": "Invalid format", "s_require_touch": "Require touch", "q_have_account_info": "Have account info?", "s_run_diagnostics": "Run diagnostics", @@ -557,6 +558,13 @@ "type": {} } }, + "l_slot_configuration_programmed_and_exported": "Configured {type} credential and exported to {file}", + "@l_slot_configuration_programmed_and_exported": { + "placeholders": { + "type": {}, + "file": {} + } + }, "s_append_enter": "Append enter", "l_append_enter_desc": "Append an Enter keystroke after emitting the OTP", diff --git a/lib/otp/state.dart b/lib/otp/state.dart index 0772b455..0b6d7b0d 100644 --- a/lib/otp/state.dart +++ b/lib/otp/state.dart @@ -29,6 +29,8 @@ abstract class OtpStateNotifier extends ApplicationStateNotifier { Future generateStaticPassword(int length, String layout); Future modhexEncodeSerial(int serial); Future>> getKeyboardLayouts(); + Future formatYubiOtpCsv( + int serial, String publicId, String privateId, String key); Future swapSlots(); Future configureSlot(SlotId slot, {required SlotConfiguration configuration}); diff --git a/lib/otp/views/configure_chalresp_dialog.dart b/lib/otp/views/configure_chalresp_dialog.dart index 72115a05..239e1f4a 100644 --- a/lib/otp/views/configure_chalresp_dialog.dart +++ b/lib/otp/views/configure_chalresp_dialog.dart @@ -47,15 +47,14 @@ class ConfigureChalrespDialog extends ConsumerStatefulWidget { class _ConfigureChalrespDialogState extends ConsumerState { - final _keyController = TextEditingController(); - bool _invalidKeyLength = false; - bool _configuring = false; + final _secretController = TextEditingController(); + bool _validateSecretLength = false; bool _requireTouch = false; - final int maxLength = 40; + final int secretMaxLength = 40; @override void dispose() { - _keyController.dispose(); + _secretController.dispose(); super.dispose(); } @@ -63,21 +62,20 @@ class _ConfigureChalrespDialogState Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final secret = _keyController.text.replaceAll(' ', ''); + final secret = _secretController.text.replaceAll(' ', ''); + final secretLengthValid = + secret.isNotEmpty && secret.length <= secretMaxLength; return ResponsiveDialog( - allowCancel: !_configuring, title: Text(l10n.s_challenge_response), actions: [ TextButton( key: keys.saveButton, - onPressed: _configuring || _invalidKeyLength - ? null - : () async { - if (!(secret.isNotEmpty && secret.length <= maxLength)) { + onPressed: !_validateSecretLength + ? () async { + if (!secretLengthValid) { setState(() { - _configuring = false; - _invalidKeyLength = true; + _validateSecretLength = true; }); return; } @@ -86,10 +84,6 @@ class _ConfigureChalrespDialogState return; } - setState(() { - _configuring = true; - }); - final otpNotifier = ref.read(otpStateProvider(widget.devicePath).notifier); try { @@ -114,7 +108,8 @@ class _ConfigureChalrespDialogState ); }); } - }, + } + : null, child: Text(l10n.s_save), ) ], @@ -126,9 +121,9 @@ class _ConfigureChalrespDialogState TextField( key: keys.secretField, autofocus: true, - controller: _keyController, + controller: _secretController, autofillHints: isAndroid ? [] : const [AutofillHints.password], - maxLength: maxLength, + maxLength: secretMaxLength, decoration: InputDecoration( suffixIcon: IconButton( tooltip: l10n.s_generate_secret_key, @@ -142,14 +137,16 @@ class _ConfigureChalrespDialogState .toRadixString(16) .padLeft(2, '0')).join(); setState(() { - _keyController.text = key; + _secretController.text = key; }); }, ), border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.key_outlined), labelText: l10n.s_secret_key, - errorText: _invalidKeyLength ? l10n.s_invalid_length : null), + errorText: _validateSecretLength && !secretLengthValid + ? l10n.s_invalid_length + : null), inputFormatters: [ FilteringTextInputFormatter.allow( RegExp('[a-f0-9]', caseSensitive: false)) @@ -157,7 +154,7 @@ class _ConfigureChalrespDialogState textInputAction: TextInputAction.next, onChanged: (value) { setState(() { - _invalidKeyLength = false; + _validateSecretLength = false; }); }, ), diff --git a/lib/otp/views/configure_hotp_dialog.dart b/lib/otp/views/configure_hotp_dialog.dart index 490fe2cd..a6dce0fb 100644 --- a/lib/otp/views/configure_hotp_dialog.dart +++ b/lib/otp/views/configure_hotp_dialog.dart @@ -46,17 +46,16 @@ class ConfigureHotpDialog extends ConsumerStatefulWidget { } class _ConfigureHotpDialogState extends ConsumerState { - final _keyController = TextEditingController(); - bool _invalidKeyLength = false; + final _secretController = TextEditingController(); + bool _validateSecretLength = false; int _digits = defaultDigits; final List _digitsValues = [6, 8]; bool _appendEnter = true; - bool _configuring = false; bool _isObscure = true; @override void dispose() { - _keyController.dispose(); + _secretController.dispose(); super.dispose(); } @@ -64,21 +63,19 @@ class _ConfigureHotpDialogState extends ConsumerState { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final secret = _keyController.text.replaceAll(' ', ''); + final secret = _secretController.text.replaceAll(' ', ''); + final secretLengthValid = secret.isNotEmpty && secret.length * 5 % 8 < 5; return ResponsiveDialog( - allowCancel: !_configuring, title: Text(l10n.s_hotp), actions: [ TextButton( key: keys.saveButton, - onPressed: _configuring || _invalidKeyLength - ? null - : () async { - if (!(secret.isNotEmpty && secret.length * 5 % 8 < 5)) { + onPressed: !_validateSecretLength + ? () async { + if (!secretLengthValid) { setState(() { - _configuring = false; - _invalidKeyLength = true; + _validateSecretLength = true; }); return; } @@ -87,10 +84,6 @@ class _ConfigureHotpDialogState extends ConsumerState { return; } - setState(() { - _configuring = true; - }); - final otpNotifier = ref.read(otpStateProvider(widget.devicePath).notifier); try { @@ -116,7 +109,8 @@ class _ConfigureHotpDialogState extends ConsumerState { ); }); } - }, + } + : null, child: Text(l10n.s_save), ) ], @@ -127,7 +121,7 @@ class _ConfigureHotpDialogState extends ConsumerState { children: [ TextField( key: keys.secretField, - controller: _keyController, + controller: _secretController, obscureText: _isObscure, autofillHints: isAndroid ? [] : const [AutofillHints.password], inputFormatters: [ @@ -150,11 +144,13 @@ class _ConfigureHotpDialogState extends ConsumerState { border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.key_outlined), labelText: l10n.s_secret_key, - errorText: _invalidKeyLength ? l10n.s_invalid_length : null), + errorText: _validateSecretLength && !secretLengthValid + ? l10n.s_invalid_length + : null), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { - _invalidKeyLength = false; + _validateSecretLength = false; }); }, ), diff --git a/lib/otp/views/configure_static_dialog.dart b/lib/otp/views/configure_static_dialog.dart index 7ade92bd..a6427077 100644 --- a/lib/otp/views/configure_static_dialog.dart +++ b/lib/otp/views/configure_static_dialog.dart @@ -49,16 +49,18 @@ class ConfigureStaticDialog extends ConsumerStatefulWidget { class _ConfigureStaticDialogState extends ConsumerState { final _passwordController = TextEditingController(); - bool _invalidPasswordLength = false; - bool _configuring = false; + final passwordMaxLength = 38; + bool _validatePassword = false; bool _appendEnter = true; String _keyboardLayout = ''; - final maxLength = 38; + String _defaultKeyboardLayout = ''; @override void initState() { super.initState(); - _keyboardLayout = widget.keyboardLayouts.keys.toList()[0]; + final modhexLayout = widget.keyboardLayouts.keys.toList()[0]; + _keyboardLayout = modhexLayout; + _defaultKeyboardLayout = modhexLayout; } @override @@ -67,34 +69,37 @@ class _ConfigureStaticDialogState extends ConsumerState { super.dispose(); } - RegExp generateFormatterPattern(String layout) { + String generateFormatterPattern(String layout) { final allowedCharacters = widget.keyboardLayouts[layout] ?? []; final pattern = allowedCharacters.map((char) => RegExp.escape(char)).join(''); - return RegExp('[$pattern]', caseSensitive: false); + return '[$pattern]'; } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final secret = _passwordController.text.replaceAll(' ', ''); + 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); return ResponsiveDialog( - allowCancel: !_configuring, title: Text(l10n.s_static_password), actions: [ TextButton( key: keys.saveButton, - onPressed: _configuring || _invalidPasswordLength - ? null - : () async { - if (!(secret.isNotEmpty && secret.length <= maxLength)) { + onPressed: !_validatePassword + ? () async { + if (!passwordLengthValid || !passwordFormatValid) { setState(() { - _configuring = false; - _invalidPasswordLength = true; + _validatePassword = true; }); return; } @@ -103,16 +108,12 @@ class _ConfigureStaticDialogState extends ConsumerState { return; } - setState(() { - _configuring = true; - }); - final otpNotifier = ref.read(otpStateProvider(widget.devicePath).notifier); try { await otpNotifier.configureSlot(widget.otpSlot.slot, configuration: SlotConfiguration.static( - password: secret, + password: password, keyboardLayout: _keyboardLayout, options: SlotConfigurationOptions( appendCr: _appendEnter))); @@ -134,7 +135,8 @@ class _ConfigureStaticDialogState extends ConsumerState { ); }); } - }, + } + : null, child: Text(l10n.s_save), ) ], @@ -148,7 +150,7 @@ class _ConfigureStaticDialogState extends ConsumerState { autofocus: true, controller: _passwordController, autofillHints: isAndroid ? [] : const [AutofillHints.password], - maxLength: maxLength, + maxLength: passwordMaxLength, decoration: InputDecoration( suffixIcon: IconButton( tooltip: l10n.s_generate_passowrd, @@ -156,7 +158,8 @@ class _ConfigureStaticDialogState extends ConsumerState { onPressed: () async { final password = await ref .read(otpStateProvider(widget.devicePath).notifier) - .generateStaticPassword(maxLength, _keyboardLayout); + .generateStaticPassword( + passwordMaxLength, _keyboardLayout); setState(() { _passwordController.text = password; }); @@ -165,16 +168,24 @@ class _ConfigureStaticDialogState extends ConsumerState { border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.key_outlined), labelText: l10n.s_password, - errorText: - _invalidPasswordLength ? l10n.s_invalid_length : null), + errorText: _validatePassword && + !passwordLengthValid && + passwordFormatValid + ? l10n.s_invalid_length + : _validatePassword && + passwordLengthValid && + !passwordFormatValid + ? l10n.s_invalid_format + : null), inputFormatters: [ - FilteringTextInputFormatter.allow( - generateFormatterPattern(_keyboardLayout)) + FilteringTextInputFormatter.allow(RegExp( + generateFormatterPattern(_keyboardLayout), + caseSensitive: false)) ], textInputAction: TextInputAction.next, onChanged: (value) { setState(() { - _invalidPasswordLength = false; + _validatePassword = false; }); }, ), @@ -186,10 +197,12 @@ class _ConfigureStaticDialogState extends ConsumerState { ChoiceFilterChip( items: widget.keyboardLayouts.keys.toList(), value: _keyboardLayout, + selected: _keyboardLayout != _defaultKeyboardLayout, itemBuilder: (value) => Text(value), onChanged: (layout) { setState(() { _keyboardLayout = layout; + _validatePassword = false; }); }), FilterChip( diff --git a/lib/otp/views/configure_yubiotp_dialog.dart b/lib/otp/views/configure_yubiotp_dialog.dart index a2fe9df0..120765d0 100644 --- a/lib/otp/views/configure_yubiotp_dialog.dart +++ b/lib/otp/views/configure_yubiotp_dialog.dart @@ -15,7 +15,9 @@ */ import 'dart:math'; +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'; @@ -23,6 +25,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:yubico_authenticator/app/logging.dart'; import 'package:yubico_authenticator/core/state.dart'; import 'package:logging/logging.dart'; +import 'package:yubico_authenticator/widgets/choice_filter_chip.dart'; import '../../app/message.dart'; import '../../app/models.dart'; @@ -37,6 +40,18 @@ final _log = Logger('otp.view.configure_yubiotp_dialog'); final _modhexPattern = RegExp('[cbdefghijklnrtuv]', caseSensitive: false); +enum OutputActions { + selectFile, + noOutput; + + const OutputActions(); + + String getDisplayName(AppLocalizations l10n) => switch (this) { + OutputActions.selectFile => 'Select file', + OutputActions.noOutput => 'No output' + }; +} + class ConfigureYubiOtpDialog extends ConsumerStatefulWidget { final DevicePath devicePath; final OtpSlot otpSlot; @@ -49,18 +64,19 @@ class ConfigureYubiOtpDialog extends ConsumerStatefulWidget { class _ConfigureYubiOtpDialogState extends ConsumerState { - final _keyController = TextEditingController(); + final _secretController = TextEditingController(); final _publicIdController = TextEditingController(); final _privateIdController = TextEditingController(); - final maxLengthKey = 32; - final maxLengthPublicId = 12; - final maxLengthPrivateId = 12; + final secretLength = 32; + final publicIdLength = 12; + final privateIdLength = 12; + OutputActions _action = OutputActions.selectFile; + File? _outputFile = File('~/secrets.csv'); bool _appendEnter = true; - bool _configuring = false; @override void dispose() { - _keyController.dispose(); + _secretController.dispose(); _publicIdController.dispose(); _privateIdController.dispose(); super.dispose(); @@ -72,41 +88,46 @@ class _ConfigureYubiOtpDialogState final info = ref.watch(currentDeviceDataProvider).valueOrNull?.info; - final secret = _keyController.text.replaceAll(' ', ''); + final secret = _secretController.text.replaceAll(' ', ''); + final secretLengthValid = secret.length == secretLength; + final privateId = _privateIdController.text; - String publicId = _publicIdController.text; + final privateIdLengthValid = privateId.length == privateIdLength; + + final publicId = _publicIdController.text; + final publicIdLengthValid = publicId.length == publicIdLength; + + final isValid = + secretLengthValid && privateIdLengthValid && publicIdLengthValid; + + Future selectFile() async { + final filePath = await FilePicker.platform.saveFile( + dialogTitle: 'Export configuration to file', + allowedExtensions: ['csv'], + type: FileType.custom, + lockParentWindow: true); + + if (filePath == null) { + return null; + } + + return File(filePath); + } return ResponsiveDialog( - allowCancel: !_configuring, title: Text(l10n.s_yubiotp), actions: [ TextButton( key: keys.saveButton, - onPressed: _configuring - ? null - : () async { - if (!(secret.isNotEmpty && secret.length * 5 % 8 < 5)) { - setState(() { - _configuring = false; - }); - return; - } - + onPressed: isValid + ? () async { if (!await confirmOverwrite(context, widget.otpSlot)) { return; } - setState(() { - _configuring = true; - }); - final otpNotifier = ref.read(otpStateProvider(widget.devicePath).notifier); try { - if (info != null && publicId == info.serial.toString()) { - publicId = - await otpNotifier.modhexEncodeSerial(info.serial!); - } await otpNotifier.configureSlot(widget.otpSlot.slot, configuration: SlotConfiguration.yubiotp( publicId: publicId, @@ -114,23 +135,45 @@ class _ConfigureYubiOtpDialogState key: secret, options: SlotConfigurationOptions( appendCr: _appendEnter))); + if (_outputFile != null) { + final csv = await otpNotifier.formatYubiOtpCsv( + info!.serial!, publicId, privateId, secret); + + await _outputFile!.writeAsString( + '$csv${Platform.lineTerminator}', + mode: FileMode.append); + } await ref.read(withContextProvider)((context) async { Navigator.of(context).pop(); - showMessage(context, - l10n.l_slot_configuration_programmed(l10n.s_yubiotp)); + showMessage( + context, + _outputFile != null + ? l10n + .l_slot_configuration_programmed_and_exported( + l10n.s_yubiotp, + _outputFile!.uri.pathSegments.last) + : l10n.l_slot_configuration_programmed( + l10n.s_yubiotp)); }); } catch (e) { _log.error('Failed to program credential', e); await ref.read(withContextProvider)((context) async { + final String errorMessage; + if (e is PathNotFoundException) { + errorMessage = '${e.message} ${e.path.toString()}'; + } else { + errorMessage = l10n.p_otp_slot_configuration_error( + widget.otpSlot.slot.getDisplayName(l10n)); + } showMessage( context, - l10n.p_otp_slot_configuration_error( - widget.otpSlot.slot.getDisplayName(l10n)), + errorMessage, duration: const Duration(seconds: 4), ); }); } - }, + } + : null, child: Text(l10n.s_save), ) ], @@ -144,16 +187,22 @@ class _ConfigureYubiOtpDialogState autofocus: true, controller: _publicIdController, autofillHints: isAndroid ? [] : const [AutofillHints.password], - maxLength: maxLengthPublicId, + maxLength: publicIdLength, decoration: InputDecoration( suffixIcon: IconButton( tooltip: l10n.s_use_serial, icon: const Icon(Icons.auto_awesome_outlined), - onPressed: () { - setState(() { - _publicIdController.text = info!.serial.toString(); - }); - }, + onPressed: (info?.serial != null) + ? () async { + final publicId = await ref + .read(otpStateProvider(widget.devicePath) + .notifier) + .modhexEncodeSerial(info!.serial!); + setState(() { + _publicIdController.text = publicId; + }); + } + : null, ), border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.public_outlined), @@ -162,12 +211,17 @@ class _ConfigureYubiOtpDialogState FilteringTextInputFormatter.allow(_modhexPattern) ], textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + // Update lengths + }); + }, ), TextField( key: keys.privateIdField, controller: _privateIdController, autofillHints: isAndroid ? [] : const [AutofillHints.password], - maxLength: maxLengthPrivateId, + maxLength: privateIdLength, decoration: InputDecoration( suffixIcon: IconButton( tooltip: l10n.s_generate_private_id, @@ -193,12 +247,17 @@ class _ConfigureYubiOtpDialogState RegExp('[a-f0-9]', caseSensitive: false)) ], textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + // Update lengths + }); + }, ), TextField( key: keys.secretField, - controller: _keyController, + controller: _secretController, autofillHints: isAndroid ? [] : const [AutofillHints.password], - maxLength: maxLengthKey, + maxLength: secretLength, decoration: InputDecoration( suffixIcon: IconButton( tooltip: l10n.s_generate_secret_key, @@ -212,7 +271,7 @@ class _ConfigureYubiOtpDialogState .toRadixString(16) .padLeft(2, '0')).join(); setState(() { - _keyController.text = key; + _secretController.text = key; }); }, ), @@ -224,6 +283,11 @@ class _ConfigureYubiOtpDialogState RegExp('[a-f0-9]', caseSensitive: false)) ], textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + // Update lengths + }); + }, ), Wrap( crossAxisAlignment: WrapCrossAlignment.center, @@ -239,7 +303,44 @@ class _ConfigureYubiOtpDialogState _appendEnter = value; }); }, - ) + ), + ChoiceFilterChip( + tooltip: _outputFile?.path ?? 'No export', + selected: _outputFile != null, + avatar: _outputFile != null + ? Icon(Icons.check, + color: Theme.of(context).colorScheme.secondary) + : null, + value: _action, + items: OutputActions.values, + itemBuilder: (value) => Text(value.getDisplayName(l10n)), + labelBuilder: (_) { + String? fileName = _outputFile?.uri.pathSegments.last; + return Container( + constraints: const BoxConstraints(maxWidth: 140), + child: Text( + 'Output ${fileName ?? 'No output'}', + overflow: TextOverflow.ellipsis, + ), + ); + }, + onChanged: (value) async { + if (value == OutputActions.noOutput) { + setState(() { + _action = value; + _outputFile = null; + }); + } else if (value == OutputActions.selectFile) { + final file = await selectFile(); + if (file != null) { + setState(() { + _action = value; + _outputFile = file; + }); + } + } + }, + ), ], ) ] diff --git a/lib/widgets/choice_filter_chip.dart b/lib/widgets/choice_filter_chip.dart index 0b8c14d5..253785e0 100755 --- a/lib/widgets/choice_filter_chip.dart +++ b/lib/widgets/choice_filter_chip.dart @@ -21,6 +21,7 @@ import 'package:flutter/material.dart'; class ChoiceFilterChip extends StatefulWidget { final T value; final List items; + final String? tooltip; final Widget Function(T value) itemBuilder; final Widget Function(T value)? labelBuilder; final void Function(T value)? onChanged; @@ -32,6 +33,7 @@ class ChoiceFilterChip extends StatefulWidget { required this.items, required this.itemBuilder, required this.onChanged, + this.tooltip, this.avatar, this.selected = false, this.labelBuilder, @@ -57,7 +59,6 @@ class _ChoiceFilterChipState extends State> { ), Offset.zero & overlay.size, ); - return await showMenu( context: context, position: position, @@ -79,6 +80,7 @@ class _ChoiceFilterChipState extends State> { @override Widget build(BuildContext context) { return FilterChip( + tooltip: widget.tooltip, avatar: widget.avatar, labelPadding: const EdgeInsets.only(left: 4), label: Row( From 23ebcb695541087b9681d1b67b2c584362b0a346 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Wed, 22 Nov 2023 10:26:20 +0100 Subject: [PATCH 04/27] Persist last selected YubiOTP Output file. --- lib/otp/state.dart | 14 +++++++++ lib/otp/views/configure_yubiotp_dialog.dart | 32 ++++++++++----------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/lib/otp/state.dart b/lib/otp/state.dart index 0b6d7b0d..ccdfb15d 100644 --- a/lib/otp/state.dart +++ b/lib/otp/state.dart @@ -14,12 +14,26 @@ * limitations under the License. */ +import 'dart:io'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/models.dart'; import '../core/state.dart'; import 'models.dart'; +final yubiOtpOutputProvider = + StateNotifierProvider( + (ref) => YubiOtpOutputNotifier()); + +class YubiOtpOutputNotifier extends StateNotifier { + YubiOtpOutputNotifier() : super(null); + + void setOutput(File? file) { + state = file; + } +} + final otpStateProvider = AsyncNotifierProvider.autoDispose .family( () => throw UnimplementedError(), diff --git a/lib/otp/views/configure_yubiotp_dialog.dart b/lib/otp/views/configure_yubiotp_dialog.dart index 120765d0..8a6235f2 100644 --- a/lib/otp/views/configure_yubiotp_dialog.dart +++ b/lib/otp/views/configure_yubiotp_dialog.dart @@ -71,7 +71,6 @@ class _ConfigureYubiOtpDialogState final publicIdLength = 12; final privateIdLength = 12; OutputActions _action = OutputActions.selectFile; - File? _outputFile = File('~/secrets.csv'); bool _appendEnter = true; @override @@ -100,7 +99,9 @@ class _ConfigureYubiOtpDialogState final isValid = secretLengthValid && privateIdLengthValid && publicIdLengthValid; - Future selectFile() async { + final outputFile = ref.read(yubiOtpOutputProvider); + + Future selectFile() async { final filePath = await FilePicker.platform.saveFile( dialogTitle: 'Export configuration to file', allowedExtensions: ['csv'], @@ -108,10 +109,11 @@ class _ConfigureYubiOtpDialogState lockParentWindow: true); if (filePath == null) { - return null; + return false; } - return File(filePath); + ref.read(yubiOtpOutputProvider.notifier).setOutput(File(filePath)); + return true; } return ResponsiveDialog( @@ -135,11 +137,11 @@ class _ConfigureYubiOtpDialogState key: secret, options: SlotConfigurationOptions( appendCr: _appendEnter))); - if (_outputFile != null) { + if (outputFile != null) { final csv = await otpNotifier.formatYubiOtpCsv( info!.serial!, publicId, privateId, secret); - await _outputFile!.writeAsString( + await outputFile.writeAsString( '$csv${Platform.lineTerminator}', mode: FileMode.append); } @@ -147,11 +149,11 @@ class _ConfigureYubiOtpDialogState Navigator.of(context).pop(); showMessage( context, - _outputFile != null + outputFile != null ? l10n .l_slot_configuration_programmed_and_exported( l10n.s_yubiotp, - _outputFile!.uri.pathSegments.last) + outputFile.uri.pathSegments.last) : l10n.l_slot_configuration_programmed( l10n.s_yubiotp)); }); @@ -305,9 +307,9 @@ class _ConfigureYubiOtpDialogState }, ), ChoiceFilterChip( - tooltip: _outputFile?.path ?? 'No export', - selected: _outputFile != null, - avatar: _outputFile != null + tooltip: outputFile?.path ?? 'No export', + selected: outputFile != null, + avatar: outputFile != null ? Icon(Icons.check, color: Theme.of(context).colorScheme.secondary) : null, @@ -315,7 +317,7 @@ class _ConfigureYubiOtpDialogState items: OutputActions.values, itemBuilder: (value) => Text(value.getDisplayName(l10n)), labelBuilder: (_) { - String? fileName = _outputFile?.uri.pathSegments.last; + String? fileName = outputFile?.uri.pathSegments.last; return Container( constraints: const BoxConstraints(maxWidth: 140), child: Text( @@ -326,16 +328,14 @@ class _ConfigureYubiOtpDialogState }, onChanged: (value) async { if (value == OutputActions.noOutput) { + ref.read(yubiOtpOutputProvider.notifier).setOutput(null); setState(() { _action = value; - _outputFile = null; }); } else if (value == OutputActions.selectFile) { - final file = await selectFile(); - if (file != null) { + if (await selectFile()) { setState(() { _action = value; - _outputFile = file; }); } } From 63bb18b2be826c9d0311daaa2963c2cb78794f9b Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Thu, 23 Nov 2023 16:25:11 +0100 Subject: [PATCH 05/27] Enhance error handling in OTP. --- lib/app/views/navigation.dart | 4 +- lib/core/models.dart | 15 ++ lib/l10n/app_en.arb | 51 ++--- lib/otp/models.dart | 5 +- lib/otp/views/configure_chalresp_dialog.dart | 75 ++++--- lib/otp/views/configure_hotp_dialog.dart | 80 +++++--- lib/otp/views/configure_static_dialog.dart | 82 ++++---- lib/otp/views/configure_yubiotp_dialog.dart | 196 ++++++++++++------- lib/otp/views/delete_slot_dialog.dart | 6 +- lib/otp/views/otp_screen.dart | 7 +- lib/otp/views/slot_dialog.dart | 8 +- 11 files changed, 324 insertions(+), 205 deletions(-) diff --git a/lib/app/views/navigation.dart b/lib/app/views/navigation.dart index 2315da16..c3fa0191 100644 --- a/lib/app/views/navigation.dart +++ b/lib/app/views/navigation.dart @@ -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, diff --git a/lib/core/models.dart b/lib/core/models.dart index 4187431f..b24d5dde 100644 --- a/lib/core/models.dart +++ b/lib/core/models.dart @@ -155,3 +155,18 @@ class Version with _$Version implements Comparable { } 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); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f818a870..786ae7cb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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", diff --git a/lib/otp/models.dart b/lib/otp/models.dart index 30d05899..526419f8 100644 --- a/lib/otp/models.dart +++ b/lib/otp/models.dart @@ -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 }; } diff --git a/lib/otp/views/configure_chalresp_dialog.dart b/lib/otp/views/configure_chalresp_dialog.dart index 239e1f4a..8cf38f21 100644 --- a/lib/otp/views/configure_chalresp_dialog.dart +++ b/lib/otp/views/configure_chalresp_dialog.dart @@ -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 { 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,36 +136,48 @@ class _ConfigureChalrespDialogState autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: secretMaxLength, decoration: InputDecoration( - suffixIcon: IconButton( - tooltip: l10n.s_generate_secret_key, - icon: const Icon(Icons.refresh), - onPressed: () { - final random = Random.secure(); - final key = List.generate( - 20, - (_) => random - .nextInt(256) - .toRadixString(16) - .padLeft(2, '0')).join(); - setState(() { - _secretController.text = key; - }); - }, + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + setState(() { + final random = Random.secure(); + final key = List.generate( + 20, + (_) => random + .nextInt(256) + .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(), prefixIcon: const Icon(Icons.key_outlined), labelText: l10n.s_secret_key, errorText: _validateSecretLength && !secretLengthValid ? l10n.s_invalid_length - : null), - inputFormatters: [ - FilteringTextInputFormatter.allow( - RegExp('[a-f0-9]', caseSensitive: false)) - ], + : _validateSecretFormat && !secretFormatValid + ? l10n.l_invalid_format_allowed_chars( + Format.hex.allowedCharacters) + : null), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { _validateSecretLength = false; + _validateSecretFormat = false; }); }, ), diff --git a/lib/otp/views/configure_hotp_dialog.dart b/lib/otp/views/configure_hotp_dialog.dart index a6dce0fb..ad856af2 100644 --- a/lib/otp/views/configure_hotp_dialog.dart +++ b/lib/otp/views/configure_hotp_dialog.dart @@ -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 { final _secretController = TextEditingController(); bool _validateSecretLength = false; + bool _validateSecretFormat = false; int _digits = defaultDigits; final List _digitsValues = [6, 8]; bool _appendEnter = true; @@ -65,6 +66,7 @@ class _ConfigureHotpDialogState extends ConsumerState { 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 { }); return; } + if (!secretFormatValid) { + setState(() { + _validateSecretFormat = true; + }); + return; + } if (!await confirmOverwrite(context, widget.otpSlot)) { return; @@ -96,7 +104,7 @@ class _ConfigureHotpDialogState extends ConsumerState { 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 { controller: _secretController, obscureText: _isObscure, autofillHints: isAndroid ? [] : const [AutofillHints.password], - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp( - '[abcdefghijklmnopqrstuvwxyz234567 ]', - caseSensitive: false)) - ], decoration: InputDecoration( - suffixIcon: IconButton( - icon: Icon( - _isObscure ? Icons.visibility : Icons.visibility_off, - color: IconTheme.of(context).color, - ), - onPressed: () { - setState(() { - _isObscure = !_isObscure; - }); - }, + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon( + _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 - : null), + : _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( items: _digitsValues, value: _digits, @@ -170,16 +202,6 @@ class _ConfigureHotpDialogState extends ConsumerState { _digits = digits; }); }), - FilterChip( - label: Text(l10n.s_append_enter), - tooltip: l10n.l_append_enter_desc, - selected: _appendEnter, - onSelected: (value) { - setState(() { - _appendEnter = value; - }); - }, - ) ], ) ] diff --git a/lib/otp/views/configure_static_dialog.dart b/lib/otp/views/configure_static_dialog.dart index a6427077..f3831560 100644 --- a/lib/otp/views/configure_static_dialog.dart +++ b/lib/otp/views/configure_static_dialog.dart @@ -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 { 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 { 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 { Navigator.of(context).pop(); showMessage( context, - l10n.l_slot_configuration_programmed( + l10n.l_slot_credential_configured( l10n.s_static_password)); }); } catch (e) { @@ -152,18 +149,31 @@ class _ConfigureStaticDialogState extends ConsumerState { autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: passwordMaxLength, decoration: InputDecoration( - suffixIcon: IconButton( - tooltip: l10n.s_generate_passowrd, - icon: const Icon(Icons.refresh), - onPressed: () async { - final password = await ref - .read(otpStateProvider(widget.devicePath).notifier) - .generateStaticPassword( - passwordMaxLength, _keyboardLayout); - setState(() { - _passwordController.text = password; - }); - }, + 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) + .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), @@ -175,13 +185,8 @@ class _ConfigureStaticDialogState extends ConsumerState { : _validatePassword && passwordLengthValid && !passwordFormatValid - ? l10n.s_invalid_format + ? l10n.l_invalid_keyboard_character : null), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp( - generateFormatterPattern(_keyboardLayout), - caseSensitive: false)) - ], textInputAction: TextInputAction.next, onChanged: (value) { setState(() { @@ -194,17 +199,6 @@ class _ConfigureStaticDialogState extends ConsumerState { 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 { _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; + }); + }), ], ) ] diff --git a/lib/otp/views/configure_yubiotp_dialog.dart b/lib/otp/views/configure_yubiotp_dialog.dart index 8a6235f2..ec3b5681 100644 --- a/lib/otp/views/configure_yubiotp_dialog.dart +++ b/lib/otp/views/configure_yubiotp_dialog.dart @@ -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.s_yubiotp, - outputFile.uri.pathSegments.last) - : l10n.l_slot_configuration_programmed( + ? l10n.l_slot_credential_configured_and_exported( + l10n.s_yubiotp, + outputFile.uri.pathSegments.last) + : l10n.l_slot_credential_configured( l10n.s_yubiotp)); }); } catch (e) { @@ -191,31 +205,43 @@ class _ConfigureYubiOtpDialogState autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: publicIdLength, decoration: InputDecoration( - suffixIcon: IconButton( - tooltip: l10n.s_use_serial, - icon: const Icon(Icons.auto_awesome_outlined), - onPressed: (info?.serial != null) - ? () async { - final publicId = await ref - .read(otpStateProvider(widget.devicePath) - .notifier) - .modhexEncodeSerial(info!.serial!); - setState(() { - _publicIdController.text = publicId; - }); - } - : null, + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + tooltip: l10n.s_use_serial, + icon: const Icon(Icons.auto_awesome_outlined), + onPressed: (info?.serial != null) + ? () async { + final publicId = await ref + .read(otpStateProvider(widget.devicePath) + .notifier) + .modhexEncodeSerial(info!.serial!); + setState(() { + _publicIdController.text = publicId; + }); + } + : 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: [ - FilteringTextInputFormatter.allow(_modhexPattern) - ], textInputAction: TextInputAction.next, onChanged: (value) { setState(() { - // Update lengths + _validatePublicIdFormat = false; }); }, ), @@ -225,33 +251,44 @@ class _ConfigureYubiOtpDialogState autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: privateIdLength, decoration: InputDecoration( - suffixIcon: IconButton( - tooltip: l10n.s_generate_private_id, - icon: const Icon(Icons.refresh), - onPressed: () { - final random = Random.secure(); - final key = List.generate( - 6, - (_) => random - .nextInt(256) - .toRadixString(16) - .padLeft(2, '0')).join(); - setState(() { - _privateIdController.text = key; - }); - }, + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + tooltip: l10n.s_generate_private_id, + icon: const Icon(Icons.refresh), + onPressed: () { + final random = Random.secure(); + final key = List.generate( + 6, + (_) => random + .nextInt(256) + .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(), 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: [ - FilteringTextInputFormatter.allow( - RegExp('[a-f0-9]', caseSensitive: false)) - ], textInputAction: TextInputAction.next, onChanged: (value) { setState(() { - // Update lengths + _validatePrivateIdFormat = false; }); }, ), @@ -261,33 +298,44 @@ class _ConfigureYubiOtpDialogState autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: secretLength, decoration: InputDecoration( - suffixIcon: IconButton( - tooltip: l10n.s_generate_secret_key, - icon: const Icon(Icons.refresh), - onPressed: () { - final random = Random.secure(); - final key = List.generate( - 16, - (_) => random - .nextInt(256) - .toRadixString(16) - .padLeft(2, '0')).join(); - setState(() { - _secretController.text = key; - }); - }, + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + tooltip: l10n.s_generate_secret_key, + icon: const Icon(Icons.refresh), + onPressed: () { + final random = Random.secure(); + final key = List.generate( + 16, + (_) => random + .nextInt(256) + .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(), 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: [ - 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, ), ); diff --git a/lib/otp/views/delete_slot_dialog.dart b/lib/otp/views/delete_slot_dialog.dart index 73ee0653..1a88e1b2 100644 --- a/lib/otp/views/delete_slot_dialog.dart +++ b/lib/otp/views/delete_slot_dialog.dart @@ -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), diff --git a/lib/otp/views/otp_screen.dart b/lib/otp/views/otp_screen.dart index 0193b5a0..0c145022 100644 --- a/lib/otp/views/otp_screen.dart +++ b/lib/otp/views/otp_screen.dart @@ -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), diff --git a/lib/otp/views/slot_dialog.dart b/lib/otp/views/slot_dialog.dart index e716f9f2..73cee688 100644 --- a/lib/otp/views/slot_dialog.dart +++ b/lib/otp/views/slot_dialog.dart @@ -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), ) ], From fa92927f139fc45027511a00f520866ca7162c62 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Fri, 24 Nov 2023 14:37:37 +0100 Subject: [PATCH 06/27] Add error suffixIcon. --- lib/fido/views/locked_page.dart | 34 ++- lib/fido/views/pin_dialog.dart | 2 + lib/oath/views/add_account_page.dart | 92 +++++---- lib/oath/views/manage_password_dialog.dart | 12 +- lib/oath/views/rename_account_dialog.dart | 2 + lib/oath/views/unlock_form.dart | 40 ++-- lib/otp/views/configure_chalresp_dialog.dart | 90 ++++---- lib/otp/views/configure_hotp_dialog.dart | 91 ++++---- lib/otp/views/configure_static_dialog.dart | 72 +++---- lib/otp/views/configure_yubiotp_dialog.dart | 205 ++++++++++--------- lib/piv/views/authentication_dialog.dart | 75 ++++--- lib/piv/views/generate_key_dialog.dart | 14 +- lib/piv/views/import_file_dialog.dart | 12 +- lib/piv/views/manage_key_dialog.dart | 161 ++++++++++----- lib/piv/views/manage_pin_puk_dialog.dart | 28 +-- lib/piv/views/pin_dialog.dart | 36 ++-- 16 files changed, 548 insertions(+), 418 deletions(-) diff --git a/lib/fido/views/locked_page.dart b/lib/fido/views/locked_page.dart index 7698df61..85e9c06a 100755 --- a/lib/fido/views/locked_page.dart +++ b/lib/fido/views/locked_page.dart @@ -173,17 +173,29 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> { errorText: _pinIsWrong ? _getErrorText() : null, errorMaxLines: 3, prefixIcon: const Icon(Icons.pin_outlined), - suffixIcon: IconButton( - icon: Icon( - _isObscure ? Icons.visibility : Icons.visibility_off, - color: IconTheme.of(context).color, - ), - onPressed: () { - setState(() { - _isObscure = !_isObscure; - }); - }, - tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin, + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon( + _isObscure ? Icons.visibility : Icons.visibility_off, + color: !_pinIsWrong + ? IconTheme.of(context).color + : null), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin, + ), + if (_pinIsWrong) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], ), ), onChanged: (value) { diff --git a/lib/fido/views/pin_dialog.dart b/lib/fido/views/pin_dialog.dart index 8bc7b732..8064227f 100755 --- a/lib/fido/views/pin_dialog.dart +++ b/lib/fido/views/pin_dialog.dart @@ -84,6 +84,7 @@ class _FidoPinDialogState extends ConsumerState { errorText: _currentIsWrong ? _currentPinError : null, errorMaxLines: 3, prefixIcon: const Icon(Icons.pin_outlined), + suffixIcon: _currentIsWrong ? const Icon(Icons.error) : null, ), onChanged: (value) { setState(() { @@ -107,6 +108,7 @@ class _FidoPinDialogState extends ConsumerState { errorText: _newIsWrong ? _newPinError : null, errorMaxLines: 3, prefixIcon: const Icon(Icons.pin_outlined), + suffixIcon: _newIsWrong ? const Icon(Icons.error) : null, ), onChanged: (value) { setState(() { diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index efa39f48..aa9fbb33 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -19,10 +19,10 @@ import 'dart:convert'; 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:logging/logging.dart'; +import 'package:yubico_authenticator/core/models.dart'; import '../../android/oath/state.dart'; import '../../app/logging.dart'; @@ -49,9 +49,6 @@ import 'utils.dart'; final _log = Logger('oath.view.add_account_page'); -final _secretFormatterPattern = - RegExp('[abcdefghijklmnopqrstuvwxyz234567 ]', caseSensitive: false); - class OathAddAccountPage extends ConsumerStatefulWidget { final DevicePath? devicePath; final OathState? state; @@ -83,7 +80,7 @@ class _OathAddAccountPageState extends ConsumerState { HashAlgorithm _hashAlgorithm = defaultHashAlgorithm; int _digits = defaultDigits; int _counter = defaultCounter; - bool _validateSecretLength = false; + bool _validateSecret = false; bool _dataLoaded = false; bool _isObscure = true; List _periodValues = [20, 30, 45, 60]; @@ -235,6 +232,7 @@ class _OathAddAccountPageState extends ConsumerState { final secret = _secretController.text.replaceAll(' ', ''); final secretLengthValid = secret.length * 5 % 8 < 5; + final secretFormatValid = Format.base32.isValid(secret); // is this credentials name/issuer pair different from all other? final isUnique = _credentials @@ -271,7 +269,7 @@ class _OathAddAccountPageState extends ConsumerState { } void submit() async { - if (secretLengthValid) { + if (secretLengthValid && secretFormatValid) { final cred = CredentialData( issuer: issuerText.isEmpty ? null : issuerText, name: nameText, @@ -304,7 +302,7 @@ class _OathAddAccountPageState extends ConsumerState { } } else { setState(() { - _validateSecretLength = true; + _validateSecret = true; }); } } @@ -368,14 +366,18 @@ class _OathAddAccountPageState extends ConsumerState { decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_issuer_optional, - helperText: '', - // Prevents dialog resizing when disabled - prefixIcon: const Icon(Icons.business_outlined), + helperText: + '', // Prevents dialog resizing when disabled errorText: (byteLength(issuerText) > issuerMaxLength) ? '' // needs empty string to render as error : issuerNoColon ? null : l10n.l_invalid_character_issuer, + prefixIcon: const Icon(Icons.business_outlined), + suffixIcon: (!issuerNoColon || + byteLength(issuerText) > issuerMaxLength) + ? const Icon(Icons.error) + : null, ), textInputAction: TextInputAction.next, onChanged: (value) { @@ -395,7 +397,6 @@ class _OathAddAccountPageState extends ConsumerState { inputFormatters: [limitBytesLength(nameRemaining)], decoration: InputDecoration( border: const OutlineInputBorder(), - prefixIcon: const Icon(Icons.person_outline), labelText: l10n.s_account_name, helperText: '', // Prevents dialog resizing when disabled @@ -404,6 +405,11 @@ class _OathAddAccountPageState extends ConsumerState { : isUnique ? null : l10n.l_name_already_exists, + prefixIcon: const Icon(Icons.person_outline), + suffixIcon: + (!isUnique || byteLength(nameText) > nameMaxLength) + ? const Icon(Icons.error) + : null, ), textInputAction: TextInputAction.next, onChanged: (value) { @@ -423,38 +429,50 @@ class _OathAddAccountPageState extends ConsumerState { // would hint to use saved passwords for this field autofillHints: isAndroid ? [] : const [AutofillHints.password], - inputFormatters: [ - FilteringTextInputFormatter.allow( - _secretFormatterPattern) - ], decoration: InputDecoration( - suffixIcon: IconButton( - icon: Icon( - _isObscure - ? Icons.visibility - : Icons.visibility_off, - color: IconTheme.of(context).color, + border: const OutlineInputBorder(), + labelText: l10n.s_secret_key, + errorText: _validateSecret && !secretLengthValid + ? l10n.s_invalid_length + : _validateSecret && !secretFormatValid + ? l10n.l_invalid_format_allowed_chars( + Format.base32.allowedCharacters) + : null, + prefixIcon: const Icon(Icons.key_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon( + _isObscure + ? Icons.visibility + : Icons.visibility_off, + color: !_validateSecret + ? IconTheme.of(context).color + : null), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + tooltip: _isObscure + ? l10n.s_show_secret_key + : l10n.s_hide_secret_key, ), - onPressed: () { - setState(() { - _isObscure = !_isObscure; - }); - }, - tooltip: _isObscure - ? l10n.s_show_secret_key - : l10n.s_hide_secret_key, - ), - border: const OutlineInputBorder(), - prefixIcon: const Icon(Icons.key_outlined), - labelText: l10n.s_secret_key, - errorText: _validateSecretLength && !secretLengthValid - ? l10n.s_invalid_length - : null), + if (_validateSecret) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], + ), + ), readOnly: _dataLoaded, textInputAction: TextInputAction.done, onChanged: (value) { setState(() { - _validateSecretLength = false; + _validateSecret = false; }); }, onSubmitted: (_) { diff --git a/lib/oath/views/manage_password_dialog.dart b/lib/oath/views/manage_password_dialog.dart index 5bd55c88..d6d7adf7 100755 --- a/lib/oath/views/manage_password_dialog.dart +++ b/lib/oath/views/manage_password_dialog.dart @@ -89,11 +89,13 @@ class _ManagePasswordDialogState extends ConsumerState { autofillHints: const [AutofillHints.password], key: keys.currentPasswordField, decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_current_password, - prefixIcon: const Icon(Icons.password_outlined), - errorText: _currentIsWrong ? l10n.s_wrong_password : null, - errorMaxLines: 3), + border: const OutlineInputBorder(), + labelText: l10n.s_current_password, + errorText: _currentIsWrong ? l10n.s_wrong_password : null, + errorMaxLines: 3, + prefixIcon: const Icon(Icons.password_outlined), + suffixIcon: _currentIsWrong ? const Icon(Icons.error) : null, + ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { diff --git a/lib/oath/views/rename_account_dialog.dart b/lib/oath/views/rename_account_dialog.dart index 992a6ab9..3dfc236c 100755 --- a/lib/oath/views/rename_account_dialog.dart +++ b/lib/oath/views/rename_account_dialog.dart @@ -208,6 +208,8 @@ class _RenameAccountDialogState extends ConsumerState { ? l10n.l_name_already_exists : null, prefixIcon: const Icon(Icons.people_alt_outlined), + suffixIcon: + !nameNotEmpty || !isUnique ? const Icon(Icons.error) : null, ), textInputAction: TextInputAction.done, onChanged: (value) { diff --git a/lib/oath/views/unlock_form.dart b/lib/oath/views/unlock_form.dart index 7ef200a0..b357c640 100755 --- a/lib/oath/views/unlock_form.dart +++ b/lib/oath/views/unlock_form.dart @@ -85,19 +85,33 @@ class _UnlockFormState extends ConsumerState { errorText: _passwordIsWrong ? l10n.s_wrong_password : null, helperText: '', // Prevents resizing when errorText shown prefixIcon: const Icon(Icons.password_outlined), - suffixIcon: IconButton( - icon: Icon( - _isObscure ? Icons.visibility : Icons.visibility_off, - color: IconTheme.of(context).color, - ), - onPressed: () { - setState(() { - _isObscure = !_isObscure; - }); - }, - tooltip: _isObscure - ? l10n.s_show_password - : l10n.s_hide_password, + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon( + _isObscure + ? Icons.visibility + : Icons.visibility_off, + color: !_passwordIsWrong + ? IconTheme.of(context).color + : null), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + tooltip: _isObscure + ? l10n.s_show_password + : l10n.s_hide_password, + ), + if (_passwordIsWrong) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], ), ), onChanged: (_) => setState(() { diff --git a/lib/otp/views/configure_chalresp_dialog.dart b/lib/otp/views/configure_chalresp_dialog.dart index 8cf38f21..f0babd1f 100644 --- a/lib/otp/views/configure_chalresp_dialog.dart +++ b/lib/otp/views/configure_chalresp_dialog.dart @@ -48,8 +48,7 @@ class ConfigureChalrespDialog extends ConsumerStatefulWidget { class _ConfigureChalrespDialogState extends ConsumerState { final _secretController = TextEditingController(); - bool _validateSecretLength = false; - bool _validateSecretFormat = false; + bool _validateSecret = false; bool _requireTouch = false; final int secretMaxLength = 40; @@ -74,17 +73,11 @@ class _ConfigureChalrespDialogState actions: [ TextButton( key: keys.saveButton, - onPressed: !_validateSecretLength + onPressed: !_validateSecret ? () async { - if (!secretLengthValid) { + if (!secretLengthValid || !secretFormatValid) { setState(() { - _validateSecretLength = true; - }); - return; - } - if (!secretFormatValid) { - setState(() { - _validateSecretFormat = true; + _validateSecret = true; }); return; } @@ -136,48 +129,49 @@ class _ConfigureChalrespDialogState autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: secretMaxLength, decoration: InputDecoration( - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: () { + border: const OutlineInputBorder(), + labelText: l10n.s_secret_key, + errorText: _validateSecret && !secretLengthValid + ? l10n.s_invalid_length + : _validateSecret && !secretFormatValid + ? l10n.l_invalid_format_allowed_chars( + Format.hex.allowedCharacters) + : null, + prefixIcon: const Icon(Icons.key_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + setState(() { + final random = Random.secure(); + final key = List.generate( + 20, + (_) => random + .nextInt(256) + .toRadixString(16) + .padLeft(2, '0')).join(); setState(() { - final random = Random.secure(); - final key = List.generate( - 20, - (_) => random - .nextInt(256) - .toRadixString(16) - .padLeft(2, '0')).join(); - setState(() { - _secretController.text = key; - }); + _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), + }); + }, + tooltip: l10n.s_generate_random, + ), + if (_validateSecret) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], + ), + ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { - _validateSecretLength = false; - _validateSecretFormat = false; + _validateSecret = false; }); }, ), diff --git a/lib/otp/views/configure_hotp_dialog.dart b/lib/otp/views/configure_hotp_dialog.dart index ad856af2..ce1b5dbf 100644 --- a/lib/otp/views/configure_hotp_dialog.dart +++ b/lib/otp/views/configure_hotp_dialog.dart @@ -47,8 +47,7 @@ class ConfigureHotpDialog extends ConsumerStatefulWidget { class _ConfigureHotpDialogState extends ConsumerState { final _secretController = TextEditingController(); - bool _validateSecretLength = false; - bool _validateSecretFormat = false; + bool _validateSecret = false; int _digits = defaultDigits; final List _digitsValues = [6, 8]; bool _appendEnter = true; @@ -73,17 +72,11 @@ class _ConfigureHotpDialogState extends ConsumerState { actions: [ TextButton( key: keys.saveButton, - onPressed: !_validateSecretLength + onPressed: !_validateSecret ? () async { - if (!secretLengthValid) { + if (!secretLengthValid || !secretFormatValid) { setState(() { - _validateSecretLength = true; - }); - return; - } - if (!secretFormatValid) { - setState(() { - _validateSecretFormat = true; + _validateSecret = true; }); return; } @@ -133,47 +126,47 @@ class _ConfigureHotpDialogState extends ConsumerState { obscureText: _isObscure, autofillHints: isAndroid ? [] : const [AutofillHints.password], decoration: InputDecoration( - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon( - _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), + border: const OutlineInputBorder(), + labelText: l10n.s_secret_key, + helperText: '', // Prevents resizing when errorText shown + errorText: _validateSecret && !secretLengthValid + ? l10n.s_invalid_length + : _validateSecret && !secretFormatValid + ? l10n.l_invalid_format_allowed_chars( + Format.base32.allowedCharacters) + : null, + prefixIcon: const Icon(Icons.key_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon( + _isObscure ? Icons.visibility : Icons.visibility_off, + color: !_validateSecret + ? IconTheme.of(context).color + : null), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + tooltip: _isObscure + ? l10n.s_show_secret_key + : l10n.s_hide_secret_key, + ), + if (_validateSecret) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], + ), + ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { - _validateSecretLength = false; - _validateSecretFormat = false; + _validateSecret = false; }); }, ), diff --git a/lib/otp/views/configure_static_dialog.dart b/lib/otp/views/configure_static_dialog.dart index f3831560..0c08089f 100644 --- a/lib/otp/views/configure_static_dialog.dart +++ b/lib/otp/views/configure_static_dialog.dart @@ -149,44 +149,40 @@ class _ConfigureStaticDialogState extends ConsumerState { autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: passwordMaxLength, decoration: InputDecoration( - 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) - .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, - errorText: _validatePassword && - !passwordLengthValid && - passwordFormatValid - ? l10n.s_invalid_length - : _validatePassword && - passwordLengthValid && - !passwordFormatValid - ? l10n.l_invalid_keyboard_character - : null), + border: const OutlineInputBorder(), + labelText: l10n.s_password, + errorText: _validatePassword && !passwordLengthValid + ? l10n.s_invalid_length + : _validatePassword && !passwordFormatValid + ? l10n.l_invalid_keyboard_character + : null, + prefixIcon: const Icon(Icons.key_outlined), + 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) + .generateStaticPassword( + passwordMaxLength, _keyboardLayout); + setState(() { + _validatePassword = false; + _passwordController.text = password; + }); + }, + ), + if (_validatePassword) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], + ), + ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { diff --git a/lib/otp/views/configure_yubiotp_dialog.dart b/lib/otp/views/configure_yubiotp_dialog.dart index ec3b5681..d7b1438c 100644 --- a/lib/otp/views/configure_yubiotp_dialog.dart +++ b/lib/otp/views/configure_yubiotp_dialog.dart @@ -205,39 +205,40 @@ class _ConfigureYubiOtpDialogState autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: publicIdLength, decoration: InputDecoration( - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - tooltip: l10n.s_use_serial, - icon: const Icon(Icons.auto_awesome_outlined), - onPressed: (info?.serial != null) - ? () async { - final publicId = await ref - .read(otpStateProvider(widget.devicePath) - .notifier) - .modhexEncodeSerial(info!.serial!); - setState(() { - _publicIdController.text = publicId; - }); - } - : 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), + border: const OutlineInputBorder(), + labelText: l10n.s_public_id, + errorText: _validatePublicIdFormat && !publicIdFormatValid + ? l10n.l_invalid_format_allowed_chars( + Format.modhex.allowedCharacters) + : null, + prefixIcon: const Icon(Icons.public_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + tooltip: l10n.s_use_serial, + icon: const Icon(Icons.auto_awesome_outlined), + onPressed: (info?.serial != null) + ? () async { + final publicId = await ref + .read(otpStateProvider(widget.devicePath) + .notifier) + .modhexEncodeSerial(info!.serial!); + setState(() { + _publicIdController.text = publicId; + }); + } + : null, + ), + if (_validatePublicIdFormat) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], + ), + ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { @@ -251,40 +252,41 @@ class _ConfigureYubiOtpDialogState autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: privateIdLength, decoration: InputDecoration( - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - tooltip: l10n.s_generate_private_id, - icon: const Icon(Icons.refresh), - onPressed: () { - final random = Random.secure(); - final key = List.generate( - 6, - (_) => random - .nextInt(256) - .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(), - prefixIcon: const Icon(Icons.key_outlined), - errorText: _validatePrivateIdFormat && !privatedIdFormatValid - ? l10n.l_invalid_format_allowed_chars( - Format.hex.allowedCharacters) - : null, - labelText: l10n.s_private_id), + border: const OutlineInputBorder(), + labelText: l10n.s_private_id, + errorText: _validatePrivateIdFormat && !privatedIdFormatValid + ? l10n.l_invalid_format_allowed_chars( + Format.hex.allowedCharacters) + : null, + prefixIcon: const Icon(Icons.key_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + tooltip: l10n.s_generate_random, + icon: const Icon(Icons.refresh), + onPressed: () { + final random = Random.secure(); + final key = List.generate( + 6, + (_) => random + .nextInt(256) + .toRadixString(16) + .padLeft(2, '0')).join(); + setState(() { + _privateIdController.text = key; + }); + }, + ), + if (_validatePrivateIdFormat) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], + ), + ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { @@ -298,40 +300,41 @@ class _ConfigureYubiOtpDialogState autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: secretLength, decoration: InputDecoration( - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - tooltip: l10n.s_generate_secret_key, - icon: const Icon(Icons.refresh), - onPressed: () { - final random = Random.secure(); - final key = List.generate( - 16, - (_) => random - .nextInt(256) - .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(), - prefixIcon: const Icon(Icons.key_outlined), - errorText: _validateSecretFormat && !secretFormatValid - ? l10n.l_invalid_format_allowed_chars( - Format.hex.allowedCharacters) - : null, - labelText: l10n.s_secret_key), + border: const OutlineInputBorder(), + labelText: l10n.s_secret_key, + errorText: _validateSecretFormat && !secretFormatValid + ? l10n.l_invalid_format_allowed_chars( + Format.hex.allowedCharacters) + : null, + prefixIcon: const Icon(Icons.key_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + tooltip: l10n.s_generate_random, + icon: const Icon(Icons.refresh), + onPressed: () { + final random = Random.secure(); + final key = List.generate( + 16, + (_) => random + .nextInt(256) + .toRadixString(16) + .padLeft(2, '0')).join(); + setState(() { + _secretController.text = key; + }); + }, + ), + if (_validateSecretFormat) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], + ), + ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { diff --git a/lib/piv/views/authentication_dialog.dart b/lib/piv/views/authentication_dialog.dart index 503aedda..74ece398 100644 --- a/lib/piv/views/authentication_dialog.dart +++ b/lib/piv/views/authentication_dialog.dart @@ -15,9 +15,9 @@ */ 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/core/models.dart'; import '../../app/models.dart'; import '../../exception/cancellation_exception.dart'; @@ -40,6 +40,7 @@ class AuthenticationDialog extends ConsumerStatefulWidget { class _AuthenticationDialogState extends ConsumerState { bool _defaultKeyUsed = false; bool _keyIsWrong = false; + bool _keyFormatInvalid = false; final _keyController = TextEditingController(); @override @@ -56,6 +57,7 @@ class _AuthenticationDialogState extends ConsumerState { ManagementKeyType.tdes) .keyLength * 2; + final keyFormatInvalid = !Format.hex.isValid(_keyController.text); return ResponsiveDialog( title: Text(l10n.l_unlock_piv_management), actions: [ @@ -63,6 +65,12 @@ class _AuthenticationDialogState extends ConsumerState { key: keys.unlockButton, onPressed: _keyController.text.length == keyLen ? () async { + if (keyFormatInvalid) { + setState(() { + _keyFormatInvalid = true; + }); + return; + } final navigator = Navigator.of(context); try { final status = await ref @@ -99,42 +107,59 @@ class _AuthenticationDialogState extends ConsumerState { autofocus: true, autofillHints: const [AutofillHints.password], controller: _keyController, - inputFormatters: [ - FilteringTextInputFormatter.allow( - RegExp('[a-f0-9]', caseSensitive: false)) - ], readOnly: _defaultKeyUsed, maxLength: !_defaultKeyUsed ? keyLen : null, decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_management_key, - prefixIcon: const Icon(Icons.key_outlined), - errorText: _keyIsWrong ? l10n.l_wrong_key : null, - errorMaxLines: 3, helperText: _defaultKeyUsed ? l10n.l_default_key_used : null, - suffixIcon: hasMetadata + errorText: _keyIsWrong + ? l10n.l_wrong_key + : _keyFormatInvalid + ? l10n.l_invalid_format_allowed_chars( + Format.hex.allowedCharacters) + : null, + errorMaxLines: 3, + prefixIcon: const Icon(Icons.key_outlined), + suffixIcon: hasMetadata && (!_keyIsWrong && !_keyFormatInvalid) ? null - : IconButton( - icon: Icon(_defaultKeyUsed - ? Icons.auto_awesome - : Icons.auto_awesome_outlined), - tooltip: l10n.s_use_default, - onPressed: () { - setState(() { - _defaultKeyUsed = !_defaultKeyUsed; - if (_defaultKeyUsed) { - _keyController.text = defaultManagementKey; - } else { - _keyController.clear(); - } - }); - }, - ), + : hasMetadata && (_keyIsWrong || _keyFormatInvalid) + ? const Icon(Icons.error) + : Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon(_defaultKeyUsed + ? Icons.auto_awesome + : Icons.auto_awesome_outlined), + tooltip: l10n.s_use_default, + onPressed: () { + setState(() { + _keyFormatInvalid = false; + _defaultKeyUsed = !_defaultKeyUsed; + if (_defaultKeyUsed) { + _keyController.text = + defaultManagementKey; + } else { + _keyController.clear(); + } + }); + }, + ), + if (_keyIsWrong || _keyFormatInvalid) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], + ), ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { _keyIsWrong = false; + _keyFormatInvalid = false; }); }, ), diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart index 8afc3fd9..b96e2d58 100644 --- a/lib/piv/views/generate_key_dialog.dart +++ b/lib/piv/views/generate_key_dialog.dart @@ -162,11 +162,15 @@ class _GenerateKeyDialogState extends ConsumerState { autofocus: true, key: keys.subjectField, decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_subject, - errorText: _subject.isNotEmpty && _invalidSubject - ? l10n.l_rfc4514_invalid - : null), + border: const OutlineInputBorder(), + labelText: l10n.s_subject, + errorText: _subject.isNotEmpty && _invalidSubject + ? l10n.l_rfc4514_invalid + : null, + suffixIcon: _subject.isNotEmpty && _invalidSubject + ? const Icon(Icons.error) + : null, + ), textInputAction: TextInputAction.next, enabled: !_generating, onChanged: (value) { diff --git a/lib/piv/views/import_file_dialog.dart b/lib/piv/views/import_file_dialog.dart index a6020186..b3f13d8a 100644 --- a/lib/piv/views/import_file_dialog.dart +++ b/lib/piv/views/import_file_dialog.dart @@ -129,11 +129,13 @@ class _ImportFileDialogState extends ConsumerState { autofillHints: const [AutofillHints.password], key: keys.managementKeyField, decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_password, - prefixIcon: const Icon(Icons.password_outlined), - errorText: _passwordIsWrong ? l10n.s_wrong_password : null, - errorMaxLines: 3), + border: const OutlineInputBorder(), + labelText: l10n.s_password, + errorText: _passwordIsWrong ? l10n.s_wrong_password : null, + errorMaxLines: 3, + prefixIcon: const Icon(Icons.password_outlined), + suffixIcon: _passwordIsWrong ? const Icon(Icons.error) : null, + ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index 2402aa26..440e3ed6 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -17,9 +17,9 @@ 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/core/models.dart'; import '../../app/message.dart'; import '../../app/models.dart'; @@ -49,6 +49,8 @@ class _ManageKeyDialogState extends ConsumerState { late bool _usesStoredKey; late bool _storeKey; bool _currentIsWrong = false; + bool _currentInvalidFormat = false; + bool _newInvalidFormat = false; int _attemptsRemaining = -1; ManagementKeyType _keyType = ManagementKeyType.tdes; final _currentController = TextEditingController(); @@ -76,6 +78,16 @@ class _ManageKeyDialogState extends ConsumerState { } _submit() async { + final currentInvalidFormat = Format.hex.isValid(_currentController.text); + final newInvalidFormat = Format.hex.isValid(_keyController.text); + if (!currentInvalidFormat || !newInvalidFormat) { + setState(() { + _currentInvalidFormat = !currentInvalidFormat; + _newInvalidFormat = !newInvalidFormat; + }); + return; + } + final notifier = ref.read(pivStateProvider(widget.path).notifier); if (_usesStoredKey) { final status = (await notifier.verifyPin(_currentController.text)).when( @@ -161,18 +173,25 @@ class _ManageKeyDialogState extends ConsumerState { maxLength: 8, controller: _currentController, decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_pin, - prefixIcon: const Icon(Icons.pin_outlined), - errorText: _currentIsWrong - ? l10n - .l_wrong_pin_attempts_remaining(_attemptsRemaining) - : null, - errorMaxLines: 3), + border: const OutlineInputBorder(), + labelText: l10n.s_pin, + errorText: _currentIsWrong + ? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining) + : _currentInvalidFormat + ? l10n.l_invalid_format_allowed_chars( + Format.hex.allowedCharacters) + : null, + errorMaxLines: 3, + prefixIcon: const Icon(Icons.pin_outlined), + suffixIcon: _currentIsWrong || _currentInvalidFormat + ? const Icon(Icons.error) + : null, + ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { _currentIsWrong = false; + _currentInvalidFormat = false; }); }, ), @@ -187,33 +206,52 @@ class _ManageKeyDialogState extends ConsumerState { decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_current_management_key, - prefixIcon: const Icon(Icons.key_outlined), - errorText: _currentIsWrong ? l10n.l_wrong_key : null, - errorMaxLines: 3, helperText: _defaultKeyUsed ? l10n.l_default_key_used : null, - suffixIcon: _hasMetadata + errorText: _currentIsWrong + ? l10n.l_wrong_key + : _currentInvalidFormat + ? l10n.l_invalid_format_allowed_chars( + Format.hex.allowedCharacters) + : null, + errorMaxLines: 3, + prefixIcon: const Icon(Icons.key_outlined), + suffixIcon: (_hasMetadata && + !_currentIsWrong && + !_currentInvalidFormat) ? null - : IconButton( - icon: Icon(_defaultKeyUsed - ? Icons.auto_awesome - : Icons.auto_awesome_outlined), - tooltip: l10n.s_use_default, - onPressed: () { - setState(() { - _defaultKeyUsed = !_defaultKeyUsed; - if (_defaultKeyUsed) { - _currentController.text = defaultManagementKey; - } else { - _currentController.clear(); - } - }); - }, - ), + : (_hasMetadata && _currentIsWrong || + _currentInvalidFormat) + ? const Icon(Icons.error) + : Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon(_defaultKeyUsed + ? Icons.auto_awesome + : Icons.auto_awesome_outlined), + tooltip: l10n.s_use_default, + onPressed: () { + setState(() { + _defaultKeyUsed = !_defaultKeyUsed; + if (_defaultKeyUsed) { + _currentController.text = + defaultManagementKey; + } else { + _currentController.clear(); + } + }); + }, + ), + if (_currentIsWrong || + _currentInvalidFormat) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], + ), ), - inputFormatters: [ - FilteringTextInputFormatter.allow( - RegExp('[a-f0-9]', caseSensitive: false)) - ], textInputAction: TextInputAction.next, onChanged: (value) { setState(() { @@ -227,33 +265,44 @@ class _ManageKeyDialogState extends ConsumerState { autofillHints: const [AutofillHints.newPassword], maxLength: hexLength, controller: _keyController, - inputFormatters: [ - FilteringTextInputFormatter.allow( - RegExp('[a-f0-9]', caseSensitive: false)) - ], decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_new_management_key, - prefixIcon: const Icon(Icons.key_outlined), + errorText: _newInvalidFormat + ? l10n.l_invalid_format_allowed_chars( + Format.hex.allowedCharacters) + : null, enabled: currentLenOk, - suffixIcon: IconButton( - key: keys.managementKeyRefresh, - icon: const Icon(Icons.refresh), - tooltip: l10n.s_generate_random, - onPressed: currentLenOk - ? () { - final random = Random.secure(); - final key = List.generate( - _keyType.keyLength, - (_) => random - .nextInt(256) - .toRadixString(16) - .padLeft(2, '0')).join(); - setState(() { - _keyController.text = key; - }); - } - : null, + prefixIcon: const Icon(Icons.key_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.refresh), + tooltip: l10n.s_generate_random, + onPressed: currentLenOk + ? () { + final random = Random.secure(); + final key = List.generate( + _keyType.keyLength, + (_) => random + .nextInt(256) + .toRadixString(16) + .padLeft(2, '0')).join(); + setState(() { + _keyController.text = key; + _newInvalidFormat = false; + }); + } + : null, + ), + if (_newInvalidFormat) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], ), ), textInputAction: TextInputAction.next, diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart index 5131cde0..b171e9a2 100644 --- a/lib/piv/views/manage_pin_puk_dialog.dart +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -109,19 +109,21 @@ class _ManagePinPukDialogState extends ConsumerState { autofillHints: const [AutofillHints.password], key: keys.pinPukField, decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: widget.target == ManageTarget.pin - ? l10n.s_current_pin - : l10n.s_current_puk, - prefixIcon: const Icon(Icons.password_outlined), - errorText: _currentIsWrong - ? (widget.target == ManageTarget.pin - ? l10n.l_wrong_pin_attempts_remaining( - _attemptsRemaining) - : l10n.l_wrong_puk_attempts_remaining( - _attemptsRemaining)) - : null, - errorMaxLines: 3), + border: const OutlineInputBorder(), + labelText: widget.target == ManageTarget.pin + ? l10n.s_current_pin + : l10n.s_current_puk, + errorText: _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(Icons.password_outlined), + suffixIcon: _currentIsWrong ? const Icon(Icons.error) : null, + ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { diff --git a/lib/piv/views/pin_dialog.dart b/lib/piv/views/pin_dialog.dart index 25edcd25..dee6f074 100644 --- a/lib/piv/views/pin_dialog.dart +++ b/lib/piv/views/pin_dialog.dart @@ -96,22 +96,34 @@ class _PinDialogState extends ConsumerState { decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_pin, - prefixIcon: const Icon(Icons.pin_outlined), errorText: _pinIsWrong ? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining) : null, errorMaxLines: 3, - suffixIcon: IconButton( - icon: Icon( - _isObscure ? Icons.visibility : Icons.visibility_off, - color: IconTheme.of(context).color, - ), - onPressed: () { - setState(() { - _isObscure = !_isObscure; - }); - }, - tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin, + prefixIcon: const Icon(Icons.pin_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon( + _isObscure ? Icons.visibility : Icons.visibility_off, + color: !_pinIsWrong + ? IconTheme.of(context).color + : null), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin, + ), + if (_pinIsWrong) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], ), ), textInputAction: TextInputAction.next, From 613d05fbb24c64d1cb7bc9694cb7b3b75ef388a9 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Mon, 27 Nov 2023 15:12:57 +0100 Subject: [PATCH 07/27] Add visibility icon to fields with obscureText --- lib/fido/views/pin_dialog.dart | 85 +++++++++++++--- lib/l10n/app_en.arb | 4 + lib/oath/views/manage_password_dialog.dart | 77 +++++++++++++-- lib/piv/views/import_file_dialog.dart | 26 ++++- lib/piv/views/manage_key_dialog.dart | 27 +++++- lib/piv/views/manage_pin_puk_dialog.dart | 107 +++++++++++++++++---- 6 files changed, 281 insertions(+), 45 deletions(-) diff --git a/lib/fido/views/pin_dialog.dart b/lib/fido/views/pin_dialog.dart index 8064227f..33fd37f5 100755 --- a/lib/fido/views/pin_dialog.dart +++ b/lib/fido/views/pin_dialog.dart @@ -48,6 +48,9 @@ class _FidoPinDialogState extends ConsumerState { String? _newPinError; bool _currentIsWrong = false; bool _newIsWrong = false; + bool _isObscureCurrent = true; + bool _isObscureNew = true; + bool _isObscureConfirm = true; @override Widget build(BuildContext context) { @@ -76,16 +79,38 @@ class _FidoPinDialogState extends ConsumerState { AppTextFormField( initialValue: _currentPin, autofocus: true, - obscureText: true, + obscureText: _isObscureCurrent, autofillHints: const [AutofillHints.password], decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_current_pin, - errorText: _currentIsWrong ? _currentPinError : null, - errorMaxLines: 3, - prefixIcon: const Icon(Icons.pin_outlined), - suffixIcon: _currentIsWrong ? const Icon(Icons.error) : null, - ), + border: const OutlineInputBorder(), + labelText: l10n.s_current_pin, + errorText: _currentIsWrong ? _currentPinError : null, + errorMaxLines: 3, + prefixIcon: const Icon(Icons.pin_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon(_isObscureCurrent + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureCurrent = !_isObscureCurrent; + }); + }, + tooltip: _isObscureCurrent + ? l10n.s_show_pin + : l10n.s_hide_pin, + ), + if (_currentIsWrong) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], + )), onChanged: (value) { setState(() { _currentIsWrong = false; @@ -99,7 +124,7 @@ class _FidoPinDialogState extends ConsumerState { AppTextFormField( initialValue: _newPin, autofocus: !hasPin, - obscureText: true, + obscureText: _isObscureNew, autofillHints: const [AutofillHints.password], decoration: InputDecoration( border: const OutlineInputBorder(), @@ -108,7 +133,28 @@ class _FidoPinDialogState extends ConsumerState { errorText: _newIsWrong ? _newPinError : null, errorMaxLines: 3, prefixIcon: const Icon(Icons.pin_outlined), - suffixIcon: _newIsWrong ? const Icon(Icons.error) : null, + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon(_isObscureNew + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureNew = !_isObscureNew; + }); + }, + tooltip: + _isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin, + ), + if (_newIsWrong) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ]), ), onChanged: (value) { setState(() { @@ -119,12 +165,29 @@ class _FidoPinDialogState extends ConsumerState { ), AppTextFormField( initialValue: _confirmPin, - obscureText: true, + obscureText: _isObscureConfirm, autofillHints: const [AutofillHints.password], decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_confirm_pin, prefixIcon: const Icon(Icons.pin_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon(_isObscureConfirm + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureConfirm = !_isObscureConfirm; + }); + }, + tooltip: + _isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin, + ) + ], + ), enabled: (!hasPin || _currentPin.isNotEmpty) && _newPin.isNotEmpty, ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 786ae7cb..ffb91674 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -198,6 +198,8 @@ "s_change_puk": "Change PUK", "s_show_pin": "Show PIN", "s_hide_pin": "Hide PIN", + "s_show_puk": "Show PUK", + "s_hide_puk": "Hide PUK", "s_current_pin": "Current PIN", "s_current_puk": "Current PUK", "s_new_pin": "New PIN", @@ -295,6 +297,8 @@ "s_management_key": "Management key", "s_current_management_key": "Current management key", "s_new_management_key": "New management key", + "s_show_management_key": "Show management key", + "s_hide_management_key": "Hide management key", "l_change_management_key": "Change management key", "p_change_management_key_desc": "Change your management key. You can optionally choose to allow the PIN to be used instead of the management key.", "l_management_key_changed": "Management key changed", diff --git a/lib/oath/views/manage_password_dialog.dart b/lib/oath/views/manage_password_dialog.dart index d6d7adf7..1ac00dae 100755 --- a/lib/oath/views/manage_password_dialog.dart +++ b/lib/oath/views/manage_password_dialog.dart @@ -42,6 +42,9 @@ class _ManagePasswordDialogState extends ConsumerState { String _newPassword = ''; String _confirmPassword = ''; bool _currentIsWrong = false; + bool _isObscureCurrent = true; + bool _isObscureNew = true; + bool _isObscureConfirm = true; _submit() async { FocusUtils.unfocus(context); @@ -85,17 +88,38 @@ class _ManagePasswordDialogState extends ConsumerState { Text(l10n.p_enter_current_password_or_reset), AppTextField( autofocus: true, - obscureText: true, + obscureText: _isObscureCurrent, autofillHints: const [AutofillHints.password], key: keys.currentPasswordField, decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_current_password, - errorText: _currentIsWrong ? l10n.s_wrong_password : null, - errorMaxLines: 3, - prefixIcon: const Icon(Icons.password_outlined), - suffixIcon: _currentIsWrong ? const Icon(Icons.error) : null, - ), + border: const OutlineInputBorder(), + labelText: l10n.s_current_password, + errorText: _currentIsWrong ? l10n.s_wrong_password : null, + errorMaxLines: 3, + prefixIcon: const Icon(Icons.password_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon(_isObscureCurrent + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureCurrent = !_isObscureCurrent; + }); + }, + tooltip: _isObscureCurrent + ? l10n.s_show_password + : l10n.s_hide_password), + if (_currentIsWrong) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], + )), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { @@ -147,12 +171,28 @@ class _ManagePasswordDialogState extends ConsumerState { AppTextField( key: keys.newPasswordField, autofocus: !widget.state.hasKey, - obscureText: true, + obscureText: _isObscureNew, autofillHints: const [AutofillHints.newPassword], decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_new_password, prefixIcon: const Icon(Icons.password_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon(_isObscureNew + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureNew = !_isObscureNew; + }); + }, + tooltip: _isObscureNew + ? l10n.s_show_password + : l10n.s_hide_password), + ]), enabled: !widget.state.hasKey || _currentPassword.isNotEmpty, ), textInputAction: TextInputAction.next, @@ -169,12 +209,29 @@ class _ManagePasswordDialogState extends ConsumerState { ), AppTextField( key: keys.confirmPasswordField, - obscureText: true, + obscureText: _isObscureConfirm, autofillHints: const [AutofillHints.newPassword], decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_confirm_password, prefixIcon: const Icon(Icons.password_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon(_isObscureConfirm + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureConfirm = !_isObscureConfirm; + }); + }, + tooltip: _isObscureConfirm + ? l10n.s_show_password + : l10n.s_hide_password) + ], + ), enabled: (!widget.state.hasKey || _currentPassword.isNotEmpty) && _newPassword.isNotEmpty, diff --git a/lib/piv/views/import_file_dialog.dart b/lib/piv/views/import_file_dialog.dart index b3f13d8a..ebc02385 100644 --- a/lib/piv/views/import_file_dialog.dart +++ b/lib/piv/views/import_file_dialog.dart @@ -51,6 +51,7 @@ class _ImportFileDialogState extends ConsumerState { String _password = ''; bool _passwordIsWrong = false; bool _importing = false; + bool _isObscure = true; @override void initState() { @@ -125,7 +126,7 @@ class _ImportFileDialogState extends ConsumerState { Text(l10n.p_password_protected_file), AppTextField( autofocus: true, - obscureText: true, + obscureText: _isObscure, autofillHints: const [AutofillHints.password], key: keys.managementKeyField, decoration: InputDecoration( @@ -134,7 +135,28 @@ class _ImportFileDialogState extends ConsumerState { errorText: _passwordIsWrong ? l10n.s_wrong_password : null, errorMaxLines: 3, prefixIcon: const Icon(Icons.password_outlined), - suffixIcon: _passwordIsWrong ? const Icon(Icons.error) : null, + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon(_isObscure + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + tooltip: _isObscure + ? l10n.s_show_password + : l10n.s_hide_password), + if (_passwordIsWrong) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ]), ), textInputAction: TextInputAction.next, onChanged: (value) { diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index 440e3ed6..c61a9c66 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -55,6 +55,7 @@ class _ManageKeyDialogState extends ConsumerState { ManagementKeyType _keyType = ManagementKeyType.tdes; final _currentController = TextEditingController(); final _keyController = TextEditingController(); + bool _isObscure = true; @override void initState() { @@ -167,7 +168,7 @@ class _ManageKeyDialogState extends ConsumerState { if (protected) AppTextField( autofocus: true, - obscureText: true, + obscureText: _isObscure, autofillHints: const [AutofillHints.password], key: keys.pinPukField, maxLength: 8, @@ -183,9 +184,27 @@ class _ManageKeyDialogState extends ConsumerState { : null, errorMaxLines: 3, prefixIcon: const Icon(Icons.pin_outlined), - suffixIcon: _currentIsWrong || _currentInvalidFormat - ? const Icon(Icons.error) - : null, + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon(_isObscure + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + tooltip: + _isObscure ? l10n.s_show_pin : l10n.s_hide_pin), + if (_currentIsWrong || _currentInvalidFormat) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ]), ), textInputAction: TextInputAction.next, onChanged: (value) { diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart index b171e9a2..09aa8d16 100644 --- a/lib/piv/views/manage_pin_puk_dialog.dart +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -44,6 +44,9 @@ class _ManagePinPukDialogState extends ConsumerState { String _confirmPin = ''; bool _currentIsWrong = false; int _attemptsRemaining = -1; + bool _isObscureCurrent = true; + bool _isObscureNew = true; + bool _isObscureConfirm = true; _submit() async { final notifier = ref.read(pivStateProvider(widget.path).notifier); @@ -104,26 +107,52 @@ class _ManagePinPukDialogState extends ConsumerState { : l10n.p_enter_current_puk_or_reset), AppTextField( autofocus: true, - obscureText: true, + obscureText: _isObscureCurrent, maxLength: 8, autofillHints: const [AutofillHints.password], key: keys.pinPukField, decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: widget.target == ManageTarget.pin - ? l10n.s_current_pin - : l10n.s_current_puk, - errorText: _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(Icons.password_outlined), - suffixIcon: _currentIsWrong ? const Icon(Icons.error) : null, - ), + border: const OutlineInputBorder(), + labelText: widget.target == ManageTarget.pin + ? l10n.s_current_pin + : l10n.s_current_puk, + errorText: _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(Icons.password_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon(_isObscureCurrent + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureCurrent = !_isObscureCurrent; + }); + }, + tooltip: widget.target == ManageTarget.pin + ? (_isObscureCurrent + ? l10n.s_show_pin + : l10n.s_hide_pin) + : (_isObscureCurrent + ? l10n.s_show_puk + : l10n.s_hide_puk), + ), + if (_currentIsWrong) ...[ + const Icon(Icons.error_outlined), + const SizedBox( + width: 8.0, + ) + ] + ], + )), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { @@ -136,7 +165,7 @@ class _ManagePinPukDialogState extends ConsumerState { widget.target == ManageTarget.puk ? l10n.s_puk : l10n.s_pin)), AppTextField( key: keys.newPinPukField, - obscureText: true, + obscureText: _isObscureNew, maxLength: 8, autofillHints: const [AutofillHints.newPassword], decoration: InputDecoration( @@ -145,6 +174,27 @@ class _ManagePinPukDialogState extends ConsumerState { ? l10n.s_new_puk : l10n.s_new_pin, prefixIcon: const Icon(Icons.password_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon(_isObscureNew + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureNew = !_isObscureNew; + }); + }, + tooltip: widget.target == ManageTarget.pin + ? (_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, ), @@ -162,7 +212,7 @@ class _ManagePinPukDialogState extends ConsumerState { ), AppTextField( key: keys.confirmPinPukField, - obscureText: true, + obscureText: _isObscureConfirm, maxLength: 8, autofillHints: const [AutofillHints.newPassword], decoration: InputDecoration( @@ -171,6 +221,27 @@ class _ManagePinPukDialogState extends ConsumerState { ? l10n.s_confirm_puk : l10n.s_confirm_pin, prefixIcon: const Icon(Icons.password_outlined), + suffixIcon: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: Icon(_isObscureConfirm + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureConfirm = !_isObscureConfirm; + }); + }, + tooltip: widget.target == ManageTarget.pin + ? (_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, ), textInputAction: TextInputAction.done, From ab8cda0efcbbaaf7eaec2134e34b2275afa8bba9 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Wed, 29 Nov 2023 13:31:30 +0100 Subject: [PATCH 08/27] Chip instead of checkbox for remembering pw --- lib/oath/views/unlock_form.dart | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/oath/views/unlock_form.dart b/lib/oath/views/unlock_form.dart index b357c640..c5b23c51 100755 --- a/lib/oath/views/unlock_form.dart +++ b/lib/oath/views/unlock_form.dart @@ -129,17 +129,24 @@ class _UnlockFormState extends ConsumerState { dense: true, minLeadingWidth: 0, ) - : CheckboxListTile( - title: Text(l10n.s_remember_password), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - value: _remember, - onChanged: (value) { - setState(() { - _remember = value ?? false; - }); - }, - ), + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: Alignment.centerLeft, + child: FilterChip( + label: Text(l10n.s_remember_password), + selected: _remember, + onSelected: (value) { + setState(() { + _remember = value; + }); + }, + )), + const SizedBox(height: 16.0), + ])), Padding( padding: const EdgeInsets.symmetric(horizontal: 18), child: Align( From be386c4a9ed7e681214010d9838ba3dd54e5ead5 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Thu, 30 Nov 2023 11:24:39 +0100 Subject: [PATCH 09/27] Progressbar for reseting FIDO --- lib/fido/views/reset_dialog.dart | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/fido/views/reset_dialog.dart b/lib/fido/views/reset_dialog.dart index 2fdd853e..a912b7c8 100755 --- a/lib/fido/views/reset_dialog.dart +++ b/lib/fido/views/reset_dialog.dart @@ -43,6 +43,8 @@ class ResetDialog extends ConsumerStatefulWidget { class _ResetDialogState extends ConsumerState { StreamSubscription? _subscription; InteractionEvent? _interaction; + int _currentStep = 0; + final _totalSteps = 4; String _getMessage() { final l10n = AppLocalizations.of(context)!; @@ -60,6 +62,7 @@ class _ResetDialogState extends ConsumerState { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; + double progress = _currentStep == 0 ? 0.0 : _currentStep / (_totalSteps); return ResponsiveDialog( title: Text(l10n.s_factory_reset), onCancel: () { @@ -74,9 +77,13 @@ class _ResetDialogState extends ConsumerState { .reset() .listen((event) { setState(() { + _currentStep++; _interaction = event; }); }, onDone: () { + setState(() { + _currentStep++; + }); _subscription = null; Navigator.of(context).pop(); showMessage(context, l10n.l_fido_app_reset); @@ -113,10 +120,8 @@ class _ResetDialogState extends ConsumerState { Text( l10n.p_warning_disable_accounts, ), - Center( - child: Text(_getMessage(), - style: Theme.of(context).textTheme.titleLarge), - ), + Text('Status: ${_getMessage()}'), + LinearProgressIndicator(value: progress), ] .map((e) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), From 70ed3bbf0584280829a21476674d622a86893e52 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Tue, 5 Dec 2023 10:00:44 +0100 Subject: [PATCH 10/27] Remove duplicate strings. --- lib/app/models.dart | 4 ++-- lib/l10n/app_en.arb | 6 ++---- lib/otp/views/otp_screen.dart | 8 ++++---- lib/piv/views/piv_screen.dart | 6 +++--- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/app/models.dart b/lib/app/models.dart index 9d87b6ab..9398dd57 100755 --- a/lib/app/models.dart +++ b/lib/app/models.dart @@ -54,8 +54,8 @@ enum Application { String getDisplayName(AppLocalizations l10n) => switch (this) { Application.oath => l10n.s_authenticator, Application.fido => l10n.s_webauthn, - Application.piv => l10n.s_piv, - Application.otp => l10n.s_otp, + Application.piv => l10n.s_certificates, + Application.otp => l10n.s_slots, _ => name.substring(0, 1).toUpperCase() + name.substring(1), }; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ffb91674..2d4af866 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -61,9 +61,9 @@ "s_manage": "Manage", "s_setup": "Setup", "s_settings": "Settings", - "s_piv": "Certificates", + "s_certificates": "Certificates", "s_webauthn": "WebAuthn", - "s_otp": "Slots", + "s_slots": "Slots", "s_help_and_about": "Help and about", "s_help_and_feedback": "Help and feedback", "s_send_feedback": "Send us feedback", @@ -442,7 +442,6 @@ "@_certificates": {}, "s_certificate": "Certificate", - "s_certificates": "Certificates", "s_csr": "CSR", "s_subject": "Subject", "l_export_csr_file": "Save CSR to file", @@ -518,7 +517,6 @@ "s_slot_9e": "Card Authentication", "@_otp_slots": {}, - "s_otp_slots": "Slots", "s_otp_slot_one": "Short touch", "s_otp_slot_two": "Long touch", "l_otp_slot_empty": "Slot is empty", diff --git a/lib/otp/views/otp_screen.dart b/lib/otp/views/otp_screen.dart index 0c145022..e875e1e1 100644 --- a/lib/otp/views/otp_screen.dart +++ b/lib/otp/views/otp_screen.dart @@ -45,21 +45,21 @@ class OtpScreen extends ConsumerWidget { final hasFeature = ref.watch(featureProvider); return ref.watch(otpStateProvider(devicePath)).when( loading: () => MessagePage( - title: Text(l10n.s_otp), + title: Text(l10n.s_slots), graphic: const CircularProgressIndicator(), delayedContent: true, ), error: (error, _) => - AppFailurePage(title: Text(l10n.s_otp), cause: error), + AppFailurePage(title: Text(l10n.s_slots), cause: error), data: (otpState) { return AppPage( - title: Text(l10n.s_otp), + title: Text(l10n.s_slots), keyActionsBuilder: hasFeature(features.actions) ? (context) => otpBuildActions(context, devicePath, otpState, ref) : null, child: Column(children: [ - ListTitle(l10n.s_otp_slots), + ListTitle(l10n.s_slots), ...otpState.slots.map((e) => registerOtpActions(devicePath, e, ref: ref, actions: { diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart index 4a1f31b7..3227d54b 100644 --- a/lib/piv/views/piv_screen.dart +++ b/lib/piv/views/piv_screen.dart @@ -46,18 +46,18 @@ class PivScreen extends ConsumerWidget { final hasFeature = ref.watch(featureProvider); return ref.watch(pivStateProvider(devicePath)).when( loading: () => MessagePage( - title: Text(l10n.s_piv), + title: Text(l10n.s_certificates), graphic: const CircularProgressIndicator(), delayedContent: true, ), error: (error, _) => AppFailurePage( - title: Text(l10n.s_piv), + title: Text(l10n.s_certificates), cause: error, ), data: (pivState) { final pivSlots = ref.watch(pivSlotsProvider(devicePath)).asData; return AppPage( - title: Text(l10n.s_piv), + title: Text(l10n.s_certificates), keyActionsBuilder: hasFeature(features.actions) ? (context) => pivBuildActions(context, devicePath, pivState, ref) From 564f074244a3b89761d3a8bbb6470b8cf9e6f619 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Wed, 13 Dec 2023 12:07:02 +0100 Subject: [PATCH 11/27] Change view in OATH `unlock_form`. --- lib/oath/views/unlock_form.dart | 78 +++++++++++++++++---------------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/lib/oath/views/unlock_form.dart b/lib/oath/views/unlock_form.dart index c5b23c51..f3cdde21 100755 --- a/lib/oath/views/unlock_form.dart +++ b/lib/oath/views/unlock_form.dart @@ -119,44 +119,48 @@ class _UnlockFormState extends ConsumerState { }), // Update state on change onSubmitted: (_) => _submit(), ), - ], - ), - ), - keystoreFailed - ? ListTile( - leading: const Icon(Icons.warning_amber_rounded), - title: Text(l10n.l_keystore_unavailable), - dense: true, - minLeadingWidth: 0, - ) - : Padding( - padding: const EdgeInsets.symmetric(horizontal: 18), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + const SizedBox(height: 8.0), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Wrap( + alignment: WrapAlignment.spaceBetween, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4.0, + runSpacing: 8.0, children: [ - Align( - alignment: Alignment.centerLeft, - child: FilterChip( - label: Text(l10n.s_remember_password), - selected: _remember, - onSelected: (value) { - setState(() { - _remember = value; - }); - }, - )), - const SizedBox(height: 16.0), - ])), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 18), - child: Align( - alignment: Alignment.centerRight, - child: ElevatedButton.icon( - key: keys.unlockButton, - label: Text(l10n.s_unlock), - icon: const Icon(Icons.lock_open), - onPressed: _passwordController.text.isNotEmpty ? _submit : null, - ), + keystoreFailed + ? Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4.0, + runSpacing: 8.0, + children: [ + const Icon(Icons.warning_amber_rounded), + Text(l10n.l_keystore_unavailable) + ], + ) + : FilterChip( + label: Text(l10n.s_remember_password), + selected: _remember, + onSelected: (value) { + setState(() { + _remember = value; + }); + }, + ), + ElevatedButton.icon( + key: keys.unlockButton, + label: Text(l10n.s_unlock), + icon: const Icon(Icons.lock_open), + onPressed: _passwordController.text.isNotEmpty + ? _submit + : null, + ), + ], + ), + ], + ), + ], ), ), ], From 635289499c31c633f26210a9311366ba89e79250 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Wed, 13 Dec 2023 19:35:17 +0100 Subject: [PATCH 12/27] Linting fixes. --- helper/helper/yubiotp.py | 27 +- lib/app/views/main_page.dart | 2 +- lib/desktop/init.dart | 4 +- lib/l10n/app_de.arb | 80 ++++- lib/l10n/app_en.arb | 1 - lib/l10n/app_fr.arb | 80 ++++- lib/l10n/app_ja.arb | 80 ++++- lib/l10n/app_pl.arb | 80 ++++- lib/oath/views/add_account_page.dart | 2 +- lib/otp/models.dart | 2 +- lib/otp/models.freezed.dart | 331 ++++++++++--------- lib/otp/models.g.dart | 63 ++-- lib/otp/views/actions.dart | 20 +- lib/otp/views/configure_chalresp_dialog.dart | 8 +- lib/otp/views/configure_hotp_dialog.dart | 10 +- lib/otp/views/configure_static_dialog.dart | 8 +- lib/otp/views/configure_yubiotp_dialog.dart | 12 +- lib/otp/views/delete_slot_dialog.dart | 2 +- lib/otp/views/key_actions.dart | 8 +- lib/otp/views/otp_screen.dart | 2 +- lib/otp/views/slot_dialog.dart | 4 +- lib/otp/views/swap_slots_dialog.dart | 4 +- lib/piv/views/authentication_dialog.dart | 2 +- lib/piv/views/manage_key_dialog.dart | 2 +- 24 files changed, 568 insertions(+), 266 deletions(-) diff --git a/helper/helper/yubiotp.py b/helper/helper/yubiotp.py index e8a37981..390cb86c 100644 --- a/helper/helper/yubiotp.py +++ b/helper/helper/yubiotp.py @@ -38,6 +38,7 @@ _FAIL_MSG = ( "have restricted access" ) + class YubiOtpNode(RpcNode): def __init__(self, connection): super().__init__() @@ -78,20 +79,16 @@ class YubiOtpNode(RpcNode): @action(closes_child=False) def serial_modhex(self, params, event, signal): serial = params["serial"] - return dict( - encoded=modhex_encode(b"\xff\x00" + struct.pack(b">I", serial)) - ) + return dict(encoded=modhex_encode(b"\xff\x00" + struct.pack(b">I", serial))) @action(closes_child=False) def generate_static(self, params, event, signal): layout, length = params["layout"], int(params["length"]) - return dict( - password=generate_static_pw(length, KEYBOARD_LAYOUT[layout]) - ) + return dict(password=generate_static_pw(length, KEYBOARD_LAYOUT[layout])) @action(closes_child=False) def keyboard_layouts(self, params, event, signal): - return {layout.name:[sc for sc in layout.value] for layout in KEYBOARD_LAYOUT} + return {layout.name: [sc for sc in layout.value] for layout in KEYBOARD_LAYOUT} @action(closes_child=False) def format_yubiotp_csv(self, params, even, signal): @@ -111,6 +108,7 @@ _CONFIG_TYPES = dict( static_ticket=StaticTicketSlotConfiguration, ) + class SlotNode(RpcNode): def __init__(self, session, slot): super().__init__() @@ -197,22 +195,20 @@ class SlotNode(RpcNode): if type in _CONFIG_TYPES: if type == "hmac_sha1": - config = _CONFIG_TYPES[type]( - bytes.fromhex(kwargs["key"]) - ) + config = _CONFIG_TYPES[type](bytes.fromhex(kwargs["key"])) elif type == "hotp": - config = _CONFIG_TYPES[type]( - parse_b32_key(kwargs["key"]) - ) + config = _CONFIG_TYPES[type](parse_b32_key(kwargs["key"])) elif type == "static_password": config = _CONFIG_TYPES[type]( - encode(kwargs["password"], KEYBOARD_LAYOUT[kwargs["keyboard_layout"]]) + encode( + kwargs["password"], KEYBOARD_LAYOUT[kwargs["keyboard_layout"]] + ) ) elif type == "yubiotp": config = _CONFIG_TYPES[type]( fixed=modhex_decode(kwargs["public_id"]), uid=bytes.fromhex(kwargs["private_id"]), - key=bytes.fromhex(kwargs["key"]) + key=bytes.fromhex(kwargs["key"]), ) else: raise ValueError("No supported configuration type provided.") @@ -251,4 +247,3 @@ class SlotNode(RpcNode): params.pop("cur_acc_code", None), ) return dict() - diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index b3d44c98..2d59bc49 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -17,7 +17,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:yubico_authenticator/otp/views/otp_screen.dart'; import '../../android/app_methods.dart'; import '../../android/qr_scanner/qr_scanner_provider.dart'; @@ -26,6 +25,7 @@ import '../../core/state.dart'; import '../../exception/cancellation_exception.dart'; import '../../fido/views/fido_screen.dart'; import '../../oath/views/oath_screen.dart'; +import '../../otp/views/otp_screen.dart'; import '../../piv/views/piv_screen.dart'; import '../../widgets/custom_icons.dart'; import '../models.dart'; diff --git a/lib/desktop/init.dart b/lib/desktop/init.dart index ff673696..b66277f6 100755 --- a/lib/desktop/init.dart +++ b/lib/desktop/init.dart @@ -42,15 +42,15 @@ import '../core/state.dart'; import '../fido/state.dart'; import '../management/state.dart'; import '../oath/state.dart'; -import '../piv/state.dart'; import '../otp/state.dart'; +import '../piv/state.dart'; import '../version.dart'; import 'devices.dart'; import 'fido/state.dart'; import 'management/state.dart'; import 'oath/state.dart'; -import 'piv/state.dart'; import 'otp/state.dart'; +import 'piv/state.dart'; import 'qr_scanner.dart'; import 'rpc.dart'; import 'state.dart'; diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index b1ad2c1d..1c8552bf 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -61,8 +61,9 @@ "s_manage": "Verwalten", "s_setup": "Einrichten", "s_settings": "Einstellungen", - "s_piv": null, + "s_certificates": null, "s_webauthn": "WebAuthn", + "s_slots": null, "s_help_and_about": "Hilfe und Über", "s_help_and_feedback": "Hilfe und Feedback", "s_send_feedback": "Senden Sie uns Feedback", @@ -78,6 +79,14 @@ "s_hide_secret_key": null, "s_private_key": null, "s_invalid_length": "Ungültige Länge", + "s_invalid_format": null, + "l_invalid_format_allowed_chars": null, + "@l_invalid_format_allowed_chars": { + "placeholders": { + "characters": {} + } + }, + "l_invalid_keyboard_character": null, "s_require_touch": "Berührung ist erforderlich", "q_have_account_info": "Haben Sie Konto-Informationen?", "s_run_diagnostics": "Diagnose ausführen", @@ -189,6 +198,8 @@ "s_change_puk": null, "s_show_pin": null, "s_hide_pin": null, + "s_show_puk": null, + "s_hide_puk": null, "s_current_pin": "Derzeitige PIN", "s_current_puk": null, "s_new_pin": "Neue PIN", @@ -286,6 +297,8 @@ "s_management_key": null, "s_current_management_key": null, "s_new_management_key": null, + "s_show_management_key": null, + "s_hide_management_key": null, "l_change_management_key": null, "p_change_management_key_desc": null, "l_management_key_changed": null, @@ -429,7 +442,6 @@ "@_certificates": {}, "s_certificate": null, - "s_certificates": null, "s_csr": null, "s_subject": null, "l_export_csr_file": null, @@ -504,6 +516,69 @@ "s_slot_9d": null, "s_slot_9e": null, + "@_otp_slots": {}, + "s_otp_slot_one": null, + "s_otp_slot_two": null, + "l_otp_slot_empty": null, + "l_otp_slot_configured": null, + + "@_otp_slot_configurations": {}, + "s_yubiotp": null, + "l_yubiotp_desc": null, + "s_challenge_response": null, + "l_challenge_response_desc": null, + "s_static_password": null, + "l_static_password_desc": null, + "s_hotp": null, + "l_hotp_desc": null, + "s_public_id": null, + "s_private_id": null, + "s_allow_any_character": null, + "s_use_serial": null, + "s_generate_private_id": null, + "s_generate_secret_key": null, + "s_generate_passowrd": null, + + "@_otp_slot_actions": {}, + "s_delete_slot": null, + "l_delete_slot_desc": null, + "p_warning_delete_slot_configuration": null, + "@p_warning_delete_slot_configuration": { + "placeholders": { + "slot_id": {} + } + }, + "l_slot_deleted": null, + "s_swap": null, + "s_swap_slots": null, + "l_swap_slots_desc": null, + "p_swap_slots_desc": null, + "l_slots_swapped": null, + "l_slot_credential_configured": null, + "@l_slot_credential_configured": { + "placeholders": { + "type": {} + } + }, + "l_slot_credential_configured_and_exported": null, + "@l_slot_credential_configured_and_exported": { + "placeholders": { + "type": {}, + "file": {} + } + }, + "s_append_enter": null, + "l_append_enter_desc": null, + + "@_otp_errors": {}, + "p_otp_slot_configuration_error": null, + "@p_otp_slot_configuration_error": { + "placeholders": { + "slot": {} + } + }, + + "@_permissions": {}, "s_enable_nfc": "NFC aktivieren", "s_permission_denied": "Zugriff verweigert", @@ -539,7 +614,6 @@ "l_oath_application_reset": "OATH Anwendung zurücksetzen", "s_reset_fido": "FIDO zurücksetzen", "l_fido_app_reset": "FIDO Anwendung zurückgesetzt", - "l_press_reset_to_begin": "Drücken Sie Zurücksetzen um zu beginnen\u2026", "l_reset_failed": "Fehler beim Zurücksetzen: {message}", "@l_reset_failed": { "placeholders": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2d4af866..7e705d8b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -614,7 +614,6 @@ "l_oath_application_reset": "OATH application reset", "s_reset_fido": "Reset FIDO", "l_fido_app_reset": "FIDO application reset", - "l_press_reset_to_begin": "Press reset to begin\u2026", "l_reset_failed": "Error performing reset: {message}", "@l_reset_failed": { "placeholders": { diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 73203e3a..93ce0e73 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -61,8 +61,9 @@ "s_manage": "Gérer", "s_setup": "Configuration", "s_settings": "Paramètres", - "s_piv": "PIV", + "s_certificates": "Certificats", "s_webauthn": "WebAuthn", + "s_slots": null, "s_help_and_about": "Aide et à propos", "s_help_and_feedback": "Aide et retours", "s_send_feedback": "Envoyer nous un retour", @@ -78,6 +79,14 @@ "s_hide_secret_key": null, "s_private_key": "Clé privée", "s_invalid_length": "Longueur invalide", + "s_invalid_format": null, + "l_invalid_format_allowed_chars": null, + "@l_invalid_format_allowed_chars": { + "placeholders": { + "characters": {} + } + }, + "l_invalid_keyboard_character": null, "s_require_touch": "Touché requis", "q_have_account_info": "Avez-vous des informations de compte?", "s_run_diagnostics": "Exécuter un diagnostique", @@ -189,6 +198,8 @@ "s_change_puk": "Changez PUK", "s_show_pin": null, "s_hide_pin": null, + "s_show_puk": null, + "s_hide_puk": null, "s_current_pin": "PIN actuel", "s_current_puk": "PUK actuel", "s_new_pin": "Nouveau PIN", @@ -286,6 +297,8 @@ "s_management_key": "Gestion des clés", "s_current_management_key": "Clé actuelle de gestion", "s_new_management_key": "Nouvelle clé de gestion", + "s_show_management_key": null, + "s_hide_management_key": null, "l_change_management_key": "Changer la clé de gestion", "p_change_management_key_desc": "Changer votre clé de gestion. Vous pouvez optionnellement autoriser le PIN à être utilisé à la place de la clé de gestion.", "l_management_key_changed": "Ché de gestion changée", @@ -429,7 +442,6 @@ "@_certificates": {}, "s_certificate": "Certificat", - "s_certificates": "Certificats", "s_csr": "CSR", "s_subject": "Sujet", "l_export_csr_file": "Sauvegarder le CSR vers un fichier", @@ -504,6 +516,69 @@ "s_slot_9d": "Gestion des clés", "s_slot_9e": "Authentification par carte", + "@_otp_slots": {}, + "s_otp_slot_one": null, + "s_otp_slot_two": null, + "l_otp_slot_empty": null, + "l_otp_slot_configured": null, + + "@_otp_slot_configurations": {}, + "s_yubiotp": null, + "l_yubiotp_desc": null, + "s_challenge_response": null, + "l_challenge_response_desc": null, + "s_static_password": null, + "l_static_password_desc": null, + "s_hotp": null, + "l_hotp_desc": null, + "s_public_id": null, + "s_private_id": null, + "s_allow_any_character": null, + "s_use_serial": null, + "s_generate_private_id": null, + "s_generate_secret_key": null, + "s_generate_passowrd": null, + + "@_otp_slot_actions": {}, + "s_delete_slot": null, + "l_delete_slot_desc": null, + "p_warning_delete_slot_configuration": null, + "@p_warning_delete_slot_configuration": { + "placeholders": { + "slot_id": {} + } + }, + "l_slot_deleted": null, + "s_swap": null, + "s_swap_slots": null, + "l_swap_slots_desc": null, + "p_swap_slots_desc": null, + "l_slots_swapped": null, + "l_slot_credential_configured": null, + "@l_slot_credential_configured": { + "placeholders": { + "type": {} + } + }, + "l_slot_credential_configured_and_exported": null, + "@l_slot_credential_configured_and_exported": { + "placeholders": { + "type": {}, + "file": {} + } + }, + "s_append_enter": null, + "l_append_enter_desc": null, + + "@_otp_errors": {}, + "p_otp_slot_configuration_error": null, + "@p_otp_slot_configuration_error": { + "placeholders": { + "slot": {} + } + }, + + "@_permissions": {}, "s_enable_nfc": "Activer le NFC", "s_permission_denied": "Permission refusée", @@ -539,7 +614,6 @@ "l_oath_application_reset": "L'application OATH à été réinitialisée", "s_reset_fido": "Réinitialiser le FIDO", "l_fido_app_reset": "L'application FIDO à été réinitialisée", - "l_press_reset_to_begin": "Appuyez sur réinitialiser pour commencer\u2026", "l_reset_failed": "Erreur pendant la réinitialisation: {message}", "@l_reset_failed": { "placeholders": { diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 8875fd1c..255bdf92 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -61,8 +61,9 @@ "s_manage": "管理", "s_setup": "セットアップ", "s_settings": "設定", - "s_piv": "PIV", + "s_certificates": "証明書", "s_webauthn": "WebAuthn", + "s_slots": null, "s_help_and_about": "ヘルプと概要", "s_help_and_feedback": "ヘルプとフィードバック", "s_send_feedback": "フィードバックの送信", @@ -78,6 +79,14 @@ "s_hide_secret_key": null, "s_private_key": "秘密鍵", "s_invalid_length": "無効な長さです", + "s_invalid_format": null, + "l_invalid_format_allowed_chars": null, + "@l_invalid_format_allowed_chars": { + "placeholders": { + "characters": {} + } + }, + "l_invalid_keyboard_character": null, "s_require_touch": "タッチが必要", "q_have_account_info": "アカウント情報をお持ちですか?", "s_run_diagnostics": "診断を実行する", @@ -189,6 +198,8 @@ "s_change_puk": "PUKを変更する", "s_show_pin": null, "s_hide_pin": null, + "s_show_puk": null, + "s_hide_puk": null, "s_current_pin": "現在のPIN", "s_current_puk": "現在のPUK", "s_new_pin": "新しいPIN", @@ -286,6 +297,8 @@ "s_management_key": "Management key", "s_current_management_key": "現在のManagement key", "s_new_management_key": "新しいManagement key", + "s_show_management_key": null, + "s_hide_management_key": null, "l_change_management_key": "Management keyの変更", "p_change_management_key_desc": "Management keyを変更してください。Management keyの代わりにPINを使用することも可能です", "l_management_key_changed": "Management keyは変更されました", @@ -429,7 +442,6 @@ "@_certificates": {}, "s_certificate": "証明書", - "s_certificates": "証明書", "s_csr": "CSR", "s_subject": "サブジェクト", "l_export_csr_file": "CSRをファイルに保存", @@ -504,6 +516,69 @@ "s_slot_9d": "鍵の管理", "s_slot_9e": "カード認証", + "@_otp_slots": {}, + "s_otp_slot_one": null, + "s_otp_slot_two": null, + "l_otp_slot_empty": null, + "l_otp_slot_configured": null, + + "@_otp_slot_configurations": {}, + "s_yubiotp": null, + "l_yubiotp_desc": null, + "s_challenge_response": null, + "l_challenge_response_desc": null, + "s_static_password": null, + "l_static_password_desc": null, + "s_hotp": null, + "l_hotp_desc": null, + "s_public_id": null, + "s_private_id": null, + "s_allow_any_character": null, + "s_use_serial": null, + "s_generate_private_id": null, + "s_generate_secret_key": null, + "s_generate_passowrd": null, + + "@_otp_slot_actions": {}, + "s_delete_slot": null, + "l_delete_slot_desc": null, + "p_warning_delete_slot_configuration": null, + "@p_warning_delete_slot_configuration": { + "placeholders": { + "slot_id": {} + } + }, + "l_slot_deleted": null, + "s_swap": null, + "s_swap_slots": null, + "l_swap_slots_desc": null, + "p_swap_slots_desc": null, + "l_slots_swapped": null, + "l_slot_credential_configured": null, + "@l_slot_credential_configured": { + "placeholders": { + "type": {} + } + }, + "l_slot_credential_configured_and_exported": null, + "@l_slot_credential_configured_and_exported": { + "placeholders": { + "type": {}, + "file": {} + } + }, + "s_append_enter": null, + "l_append_enter_desc": null, + + "@_otp_errors": {}, + "p_otp_slot_configuration_error": null, + "@p_otp_slot_configuration_error": { + "placeholders": { + "slot": {} + } + }, + + "@_permissions": {}, "s_enable_nfc": "NFCを有効にする", "s_permission_denied": "権限がありません", @@ -539,7 +614,6 @@ "l_oath_application_reset": "OATHアプリケーションのリセット", "s_reset_fido": "FIDOのリセット", "l_fido_app_reset": "FIDOアプリケーションのリセット", - "l_press_reset_to_begin": "リセットを押して開始してください\u2026", "l_reset_failed": "リセット実行中のエラー:{message}", "@l_reset_failed": { "placeholders": { diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 6503d990..53d810c1 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -61,8 +61,9 @@ "s_manage": "Zarządzaj", "s_setup": "Konfiguruj", "s_settings": "Ustawienia", - "s_piv": "PIV", + "s_certificates": "Certyfikaty", "s_webauthn": "WebAuthn", + "s_slots": null, "s_help_and_about": "Pomoc i informacje", "s_help_and_feedback": "Pomoc i opinie", "s_send_feedback": "Prześlij opinię", @@ -78,6 +79,14 @@ "s_hide_secret_key": "Ukryj tajny klucz", "s_private_key": "Klucz prywatny", "s_invalid_length": "Nieprawidłowa długość", + "s_invalid_format": null, + "l_invalid_format_allowed_chars": null, + "@l_invalid_format_allowed_chars": { + "placeholders": { + "characters": {} + } + }, + "l_invalid_keyboard_character": null, "s_require_touch": "Wymagaj dotknięcia", "q_have_account_info": "Masz dane konta?", "s_run_diagnostics": "Uruchom diagnostykę", @@ -189,6 +198,8 @@ "s_change_puk": "Zmień PUK", "s_show_pin": "Pokaż PIN", "s_hide_pin": "Ukryj PIN", + "s_show_puk": null, + "s_hide_puk": null, "s_current_pin": "Aktualny PIN", "s_current_puk": "Aktualny PUK", "s_new_pin": "Nowy PIN", @@ -286,6 +297,8 @@ "s_management_key": "Klucz zarządzania", "s_current_management_key": "Aktualny klucz zarządzania", "s_new_management_key": "Nowy klucz zarządzania", + "s_show_management_key": null, + "s_hide_management_key": null, "l_change_management_key": "Zmień klucz zarządzania", "p_change_management_key_desc": "Zmień swój klucz zarządzania. Opcjonalnie możesz zezwolić na używanie kodu PIN zamiast klucza zarządzania.", "l_management_key_changed": "Zmieniono klucz zarządzania", @@ -429,7 +442,6 @@ "@_certificates": {}, "s_certificate": "Certyfikat", - "s_certificates": "Certyfikaty", "s_csr": "CSR", "s_subject": "Temat", "l_export_csr_file": "Zapisz CSR do pliku", @@ -504,6 +516,69 @@ "s_slot_9d": "Menedżer kluczy", "s_slot_9e": "Autoryzacja karty", + "@_otp_slots": {}, + "s_otp_slot_one": null, + "s_otp_slot_two": null, + "l_otp_slot_empty": null, + "l_otp_slot_configured": null, + + "@_otp_slot_configurations": {}, + "s_yubiotp": null, + "l_yubiotp_desc": null, + "s_challenge_response": null, + "l_challenge_response_desc": null, + "s_static_password": null, + "l_static_password_desc": null, + "s_hotp": null, + "l_hotp_desc": null, + "s_public_id": null, + "s_private_id": null, + "s_allow_any_character": null, + "s_use_serial": null, + "s_generate_private_id": null, + "s_generate_secret_key": null, + "s_generate_passowrd": null, + + "@_otp_slot_actions": {}, + "s_delete_slot": null, + "l_delete_slot_desc": null, + "p_warning_delete_slot_configuration": null, + "@p_warning_delete_slot_configuration": { + "placeholders": { + "slot_id": {} + } + }, + "l_slot_deleted": null, + "s_swap": null, + "s_swap_slots": null, + "l_swap_slots_desc": null, + "p_swap_slots_desc": null, + "l_slots_swapped": null, + "l_slot_credential_configured": null, + "@l_slot_credential_configured": { + "placeholders": { + "type": {} + } + }, + "l_slot_credential_configured_and_exported": null, + "@l_slot_credential_configured_and_exported": { + "placeholders": { + "type": {}, + "file": {} + } + }, + "s_append_enter": null, + "l_append_enter_desc": null, + + "@_otp_errors": {}, + "p_otp_slot_configuration_error": null, + "@p_otp_slot_configuration_error": { + "placeholders": { + "slot": {} + } + }, + + "@_permissions": {}, "s_enable_nfc": "Włącz NFC", "s_permission_denied": "Odmowa dostępu", @@ -539,7 +614,6 @@ "l_oath_application_reset": "Reset funkcji OATH", "s_reset_fido": "Zresetuj FIDO", "l_fido_app_reset": "Reset funkcji FIDO", - "l_press_reset_to_begin": "Naciśnij reset, aby rozpocząć\u2026", "l_reset_failed": "Błąd podczas resetowania: {message}", "@l_reset_failed": { "placeholders": { diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index aa9fbb33..34be1744 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -22,7 +22,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; -import 'package:yubico_authenticator/core/models.dart'; import '../../android/oath/state.dart'; import '../../app/logging.dart'; @@ -30,6 +29,7 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; import '../../app/views/user_interaction.dart'; +import '../../core/models.dart'; import '../../core/state.dart'; import '../../desktop/models.dart'; import '../../exception/apdu_exception.dart'; diff --git a/lib/otp/models.dart b/lib/otp/models.dart index 526419f8..d28d8766 100644 --- a/lib/otp/models.dart +++ b/lib/otp/models.dart @@ -14,8 +14,8 @@ * limitations under the License. */ -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; part 'models.freezed.dart'; part 'models.g.dart'; diff --git a/lib/otp/models.freezed.dart b/lib/otp/models.freezed.dart index 5db587e2..b5f18802 100644 --- a/lib/otp/models.freezed.dart +++ b/lib/otp/models.freezed.dart @@ -67,21 +67,22 @@ class _$OtpStateCopyWithImpl<$Res, $Val extends OtpState> } /// @nodoc -abstract class _$$_OtpStateCopyWith<$Res> implements $OtpStateCopyWith<$Res> { - factory _$$_OtpStateCopyWith( - _$_OtpState value, $Res Function(_$_OtpState) then) = - __$$_OtpStateCopyWithImpl<$Res>; +abstract class _$$OtpStateImplCopyWith<$Res> + implements $OtpStateCopyWith<$Res> { + factory _$$OtpStateImplCopyWith( + _$OtpStateImpl value, $Res Function(_$OtpStateImpl) then) = + __$$OtpStateImplCopyWithImpl<$Res>; @override @useResult $Res call({bool slot1Configured, bool slot2Configured}); } /// @nodoc -class __$$_OtpStateCopyWithImpl<$Res> - extends _$OtpStateCopyWithImpl<$Res, _$_OtpState> - implements _$$_OtpStateCopyWith<$Res> { - __$$_OtpStateCopyWithImpl( - _$_OtpState _value, $Res Function(_$_OtpState) _then) +class __$$OtpStateImplCopyWithImpl<$Res> + extends _$OtpStateCopyWithImpl<$Res, _$OtpStateImpl> + implements _$$OtpStateImplCopyWith<$Res> { + __$$OtpStateImplCopyWithImpl( + _$OtpStateImpl _value, $Res Function(_$OtpStateImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @@ -90,7 +91,7 @@ class __$$_OtpStateCopyWithImpl<$Res> Object? slot1Configured = null, Object? slot2Configured = null, }) { - return _then(_$_OtpState( + return _then(_$OtpStateImpl( slot1Configured: null == slot1Configured ? _value.slot1Configured : slot1Configured // ignore: cast_nullable_to_non_nullable @@ -105,12 +106,12 @@ class __$$_OtpStateCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$_OtpState extends _OtpState { - _$_OtpState({required this.slot1Configured, required this.slot2Configured}) +class _$OtpStateImpl extends _OtpState { + _$OtpStateImpl({required this.slot1Configured, required this.slot2Configured}) : super._(); - factory _$_OtpState.fromJson(Map json) => - _$$_OtpStateFromJson(json); + factory _$OtpStateImpl.fromJson(Map json) => + _$$OtpStateImplFromJson(json); @override final bool slot1Configured; @@ -126,7 +127,7 @@ class _$_OtpState extends _OtpState { bool operator ==(dynamic other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$_OtpState && + other is _$OtpStateImpl && (identical(other.slot1Configured, slot1Configured) || other.slot1Configured == slot1Configured) && (identical(other.slot2Configured, slot2Configured) || @@ -141,12 +142,12 @@ class _$_OtpState extends _OtpState { @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$_OtpStateCopyWith<_$_OtpState> get copyWith => - __$$_OtpStateCopyWithImpl<_$_OtpState>(this, _$identity); + _$$OtpStateImplCopyWith<_$OtpStateImpl> get copyWith => + __$$OtpStateImplCopyWithImpl<_$OtpStateImpl>(this, _$identity); @override Map toJson() { - return _$$_OtpStateToJson( + return _$$OtpStateImplToJson( this, ); } @@ -155,10 +156,11 @@ class _$_OtpState extends _OtpState { abstract class _OtpState extends OtpState { factory _OtpState( {required final bool slot1Configured, - required final bool slot2Configured}) = _$_OtpState; + required final bool slot2Configured}) = _$OtpStateImpl; _OtpState._() : super._(); - factory _OtpState.fromJson(Map json) = _$_OtpState.fromJson; + factory _OtpState.fromJson(Map json) = + _$OtpStateImpl.fromJson; @override bool get slot1Configured; @@ -166,7 +168,7 @@ abstract class _OtpState extends OtpState { bool get slot2Configured; @override @JsonKey(ignore: true) - _$$_OtpStateCopyWith<_$_OtpState> get copyWith => + _$$OtpStateImplCopyWith<_$OtpStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -217,20 +219,21 @@ class _$OtpSlotCopyWithImpl<$Res, $Val extends OtpSlot> } /// @nodoc -abstract class _$$_OtpSlotCopyWith<$Res> implements $OtpSlotCopyWith<$Res> { - factory _$$_OtpSlotCopyWith( - _$_OtpSlot value, $Res Function(_$_OtpSlot) then) = - __$$_OtpSlotCopyWithImpl<$Res>; +abstract class _$$OtpSlotImplCopyWith<$Res> implements $OtpSlotCopyWith<$Res> { + factory _$$OtpSlotImplCopyWith( + _$OtpSlotImpl value, $Res Function(_$OtpSlotImpl) then) = + __$$OtpSlotImplCopyWithImpl<$Res>; @override @useResult $Res call({SlotId slot, bool isConfigured}); } /// @nodoc -class __$$_OtpSlotCopyWithImpl<$Res> - extends _$OtpSlotCopyWithImpl<$Res, _$_OtpSlot> - implements _$$_OtpSlotCopyWith<$Res> { - __$$_OtpSlotCopyWithImpl(_$_OtpSlot _value, $Res Function(_$_OtpSlot) _then) +class __$$OtpSlotImplCopyWithImpl<$Res> + extends _$OtpSlotCopyWithImpl<$Res, _$OtpSlotImpl> + implements _$$OtpSlotImplCopyWith<$Res> { + __$$OtpSlotImplCopyWithImpl( + _$OtpSlotImpl _value, $Res Function(_$OtpSlotImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @@ -239,7 +242,7 @@ class __$$_OtpSlotCopyWithImpl<$Res> Object? slot = null, Object? isConfigured = null, }) { - return _then(_$_OtpSlot( + return _then(_$OtpSlotImpl( slot: null == slot ? _value.slot : slot // ignore: cast_nullable_to_non_nullable @@ -254,8 +257,8 @@ class __$$_OtpSlotCopyWithImpl<$Res> /// @nodoc -class _$_OtpSlot implements _OtpSlot { - _$_OtpSlot({required this.slot, required this.isConfigured}); +class _$OtpSlotImpl implements _OtpSlot { + _$OtpSlotImpl({required this.slot, required this.isConfigured}); @override final SlotId slot; @@ -271,7 +274,7 @@ class _$_OtpSlot implements _OtpSlot { bool operator ==(dynamic other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$_OtpSlot && + other is _$OtpSlotImpl && (identical(other.slot, slot) || other.slot == slot) && (identical(other.isConfigured, isConfigured) || other.isConfigured == isConfigured)); @@ -283,14 +286,14 @@ class _$_OtpSlot implements _OtpSlot { @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$_OtpSlotCopyWith<_$_OtpSlot> get copyWith => - __$$_OtpSlotCopyWithImpl<_$_OtpSlot>(this, _$identity); + _$$OtpSlotImplCopyWith<_$OtpSlotImpl> get copyWith => + __$$OtpSlotImplCopyWithImpl<_$OtpSlotImpl>(this, _$identity); } abstract class _OtpSlot implements OtpSlot { factory _OtpSlot( {required final SlotId slot, - required final bool isConfigured}) = _$_OtpSlot; + required final bool isConfigured}) = _$OtpSlotImpl; @override SlotId get slot; @@ -298,7 +301,7 @@ abstract class _OtpSlot implements OtpSlot { bool get isConfigured; @override @JsonKey(ignore: true) - _$$_OtpSlotCopyWith<_$_OtpSlot> get copyWith => + _$$OtpSlotImplCopyWith<_$OtpSlotImpl> get copyWith => throw _privateConstructorUsedError; } @@ -364,24 +367,25 @@ class _$SlotConfigurationOptionsCopyWithImpl<$Res, } /// @nodoc -abstract class _$$_SlotConfigurationOptionsCopyWith<$Res> +abstract class _$$SlotConfigurationOptionsImplCopyWith<$Res> implements $SlotConfigurationOptionsCopyWith<$Res> { - factory _$$_SlotConfigurationOptionsCopyWith( - _$_SlotConfigurationOptions value, - $Res Function(_$_SlotConfigurationOptions) then) = - __$$_SlotConfigurationOptionsCopyWithImpl<$Res>; + factory _$$SlotConfigurationOptionsImplCopyWith( + _$SlotConfigurationOptionsImpl value, + $Res Function(_$SlotConfigurationOptionsImpl) then) = + __$$SlotConfigurationOptionsImplCopyWithImpl<$Res>; @override @useResult $Res call({bool? digits8, bool? requireTouch, bool? appendCr}); } /// @nodoc -class __$$_SlotConfigurationOptionsCopyWithImpl<$Res> +class __$$SlotConfigurationOptionsImplCopyWithImpl<$Res> extends _$SlotConfigurationOptionsCopyWithImpl<$Res, - _$_SlotConfigurationOptions> - implements _$$_SlotConfigurationOptionsCopyWith<$Res> { - __$$_SlotConfigurationOptionsCopyWithImpl(_$_SlotConfigurationOptions _value, - $Res Function(_$_SlotConfigurationOptions) _then) + _$SlotConfigurationOptionsImpl> + implements _$$SlotConfigurationOptionsImplCopyWith<$Res> { + __$$SlotConfigurationOptionsImplCopyWithImpl( + _$SlotConfigurationOptionsImpl _value, + $Res Function(_$SlotConfigurationOptionsImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @@ -391,7 +395,7 @@ class __$$_SlotConfigurationOptionsCopyWithImpl<$Res> Object? requireTouch = freezed, Object? appendCr = freezed, }) { - return _then(_$_SlotConfigurationOptions( + return _then(_$SlotConfigurationOptionsImpl( digits8: freezed == digits8 ? _value.digits8 : digits8 // ignore: cast_nullable_to_non_nullable @@ -411,11 +415,12 @@ class __$$_SlotConfigurationOptionsCopyWithImpl<$Res> /// @nodoc @JsonSerializable(includeIfNull: false) -class _$_SlotConfigurationOptions implements _SlotConfigurationOptions { - _$_SlotConfigurationOptions({this.digits8, this.requireTouch, this.appendCr}); +class _$SlotConfigurationOptionsImpl implements _SlotConfigurationOptions { + _$SlotConfigurationOptionsImpl( + {this.digits8, this.requireTouch, this.appendCr}); - factory _$_SlotConfigurationOptions.fromJson(Map json) => - _$$_SlotConfigurationOptionsFromJson(json); + factory _$SlotConfigurationOptionsImpl.fromJson(Map json) => + _$$SlotConfigurationOptionsImplFromJson(json); @override final bool? digits8; @@ -433,7 +438,7 @@ class _$_SlotConfigurationOptions implements _SlotConfigurationOptions { bool operator ==(dynamic other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$_SlotConfigurationOptions && + other is _$SlotConfigurationOptionsImpl && (identical(other.digits8, digits8) || other.digits8 == digits8) && (identical(other.requireTouch, requireTouch) || other.requireTouch == requireTouch) && @@ -448,13 +453,13 @@ class _$_SlotConfigurationOptions implements _SlotConfigurationOptions { @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$_SlotConfigurationOptionsCopyWith<_$_SlotConfigurationOptions> - get copyWith => __$$_SlotConfigurationOptionsCopyWithImpl< - _$_SlotConfigurationOptions>(this, _$identity); + _$$SlotConfigurationOptionsImplCopyWith<_$SlotConfigurationOptionsImpl> + get copyWith => __$$SlotConfigurationOptionsImplCopyWithImpl< + _$SlotConfigurationOptionsImpl>(this, _$identity); @override Map toJson() { - return _$$_SlotConfigurationOptionsToJson( + return _$$SlotConfigurationOptionsImplToJson( this, ); } @@ -464,10 +469,10 @@ abstract class _SlotConfigurationOptions implements SlotConfigurationOptions { factory _SlotConfigurationOptions( {final bool? digits8, final bool? requireTouch, - final bool? appendCr}) = _$_SlotConfigurationOptions; + final bool? appendCr}) = _$SlotConfigurationOptionsImpl; factory _SlotConfigurationOptions.fromJson(Map json) = - _$_SlotConfigurationOptions.fromJson; + _$SlotConfigurationOptionsImpl.fromJson; @override bool? get digits8; @@ -477,7 +482,7 @@ abstract class _SlotConfigurationOptions implements SlotConfigurationOptions { bool? get appendCr; @override @JsonKey(ignore: true) - _$$_SlotConfigurationOptionsCopyWith<_$_SlotConfigurationOptions> + _$$SlotConfigurationOptionsImplCopyWith<_$SlotConfigurationOptionsImpl> get copyWith => throw _privateConstructorUsedError; } @@ -619,11 +624,12 @@ class _$SlotConfigurationCopyWithImpl<$Res, $Val extends SlotConfiguration> } /// @nodoc -abstract class _$$_SlotConfigurationHotpCopyWith<$Res> +abstract class _$$SlotConfigurationHotpImplCopyWith<$Res> implements $SlotConfigurationCopyWith<$Res> { - factory _$$_SlotConfigurationHotpCopyWith(_$_SlotConfigurationHotp value, - $Res Function(_$_SlotConfigurationHotp) then) = - __$$_SlotConfigurationHotpCopyWithImpl<$Res>; + factory _$$SlotConfigurationHotpImplCopyWith( + _$SlotConfigurationHotpImpl value, + $Res Function(_$SlotConfigurationHotpImpl) then) = + __$$SlotConfigurationHotpImplCopyWithImpl<$Res>; @override @useResult $Res call({String key, SlotConfigurationOptions? options}); @@ -633,11 +639,11 @@ abstract class _$$_SlotConfigurationHotpCopyWith<$Res> } /// @nodoc -class __$$_SlotConfigurationHotpCopyWithImpl<$Res> - extends _$SlotConfigurationCopyWithImpl<$Res, _$_SlotConfigurationHotp> - implements _$$_SlotConfigurationHotpCopyWith<$Res> { - __$$_SlotConfigurationHotpCopyWithImpl(_$_SlotConfigurationHotp _value, - $Res Function(_$_SlotConfigurationHotp) _then) +class __$$SlotConfigurationHotpImplCopyWithImpl<$Res> + extends _$SlotConfigurationCopyWithImpl<$Res, _$SlotConfigurationHotpImpl> + implements _$$SlotConfigurationHotpImplCopyWith<$Res> { + __$$SlotConfigurationHotpImplCopyWithImpl(_$SlotConfigurationHotpImpl _value, + $Res Function(_$SlotConfigurationHotpImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @@ -646,7 +652,7 @@ class __$$_SlotConfigurationHotpCopyWithImpl<$Res> Object? key = null, Object? options = freezed, }) { - return _then(_$_SlotConfigurationHotp( + return _then(_$SlotConfigurationHotpImpl( key: null == key ? _value.key : key // ignore: cast_nullable_to_non_nullable @@ -662,14 +668,14 @@ class __$$_SlotConfigurationHotpCopyWithImpl<$Res> /// @nodoc @JsonSerializable(explicitToJson: true, includeIfNull: false) -class _$_SlotConfigurationHotp extends _SlotConfigurationHotp { - const _$_SlotConfigurationHotp( +class _$SlotConfigurationHotpImpl extends _SlotConfigurationHotp { + const _$SlotConfigurationHotpImpl( {required this.key, this.options, final String? $type}) : $type = $type ?? 'hotp', super._(); - factory _$_SlotConfigurationHotp.fromJson(Map json) => - _$$_SlotConfigurationHotpFromJson(json); + factory _$SlotConfigurationHotpImpl.fromJson(Map json) => + _$$SlotConfigurationHotpImplFromJson(json); @override final String key; @@ -688,7 +694,7 @@ class _$_SlotConfigurationHotp extends _SlotConfigurationHotp { bool operator ==(dynamic other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$_SlotConfigurationHotp && + other is _$SlotConfigurationHotpImpl && (identical(other.key, key) || other.key == key) && (identical(other.options, options) || other.options == options)); } @@ -700,9 +706,9 @@ class _$_SlotConfigurationHotp extends _SlotConfigurationHotp { @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$_SlotConfigurationHotpCopyWith<_$_SlotConfigurationHotp> get copyWith => - __$$_SlotConfigurationHotpCopyWithImpl<_$_SlotConfigurationHotp>( - this, _$identity); + _$$SlotConfigurationHotpImplCopyWith<_$SlotConfigurationHotpImpl> + get copyWith => __$$SlotConfigurationHotpImplCopyWithImpl< + _$SlotConfigurationHotpImpl>(this, _$identity); @override @optionalTypeArgs @@ -794,7 +800,7 @@ class _$_SlotConfigurationHotp extends _SlotConfigurationHotp { @override Map toJson() { - return _$$_SlotConfigurationHotpToJson( + return _$$SlotConfigurationHotpImplToJson( this, ); } @@ -803,28 +809,28 @@ class _$_SlotConfigurationHotp extends _SlotConfigurationHotp { abstract class _SlotConfigurationHotp extends SlotConfiguration { const factory _SlotConfigurationHotp( {required final String key, - final SlotConfigurationOptions? options}) = _$_SlotConfigurationHotp; + final SlotConfigurationOptions? options}) = _$SlotConfigurationHotpImpl; const _SlotConfigurationHotp._() : super._(); factory _SlotConfigurationHotp.fromJson(Map json) = - _$_SlotConfigurationHotp.fromJson; + _$SlotConfigurationHotpImpl.fromJson; String get key; @override SlotConfigurationOptions? get options; @override @JsonKey(ignore: true) - _$$_SlotConfigurationHotpCopyWith<_$_SlotConfigurationHotp> get copyWith => - throw _privateConstructorUsedError; + _$$SlotConfigurationHotpImplCopyWith<_$SlotConfigurationHotpImpl> + get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class _$$_SlotConfigurationHmacSha1CopyWith<$Res> +abstract class _$$SlotConfigurationHmacSha1ImplCopyWith<$Res> implements $SlotConfigurationCopyWith<$Res> { - factory _$$_SlotConfigurationHmacSha1CopyWith( - _$_SlotConfigurationHmacSha1 value, - $Res Function(_$_SlotConfigurationHmacSha1) then) = - __$$_SlotConfigurationHmacSha1CopyWithImpl<$Res>; + factory _$$SlotConfigurationHmacSha1ImplCopyWith( + _$SlotConfigurationHmacSha1Impl value, + $Res Function(_$SlotConfigurationHmacSha1Impl) then) = + __$$SlotConfigurationHmacSha1ImplCopyWithImpl<$Res>; @override @useResult $Res call({String key, SlotConfigurationOptions? options}); @@ -834,12 +840,13 @@ abstract class _$$_SlotConfigurationHmacSha1CopyWith<$Res> } /// @nodoc -class __$$_SlotConfigurationHmacSha1CopyWithImpl<$Res> - extends _$SlotConfigurationCopyWithImpl<$Res, _$_SlotConfigurationHmacSha1> - implements _$$_SlotConfigurationHmacSha1CopyWith<$Res> { - __$$_SlotConfigurationHmacSha1CopyWithImpl( - _$_SlotConfigurationHmacSha1 _value, - $Res Function(_$_SlotConfigurationHmacSha1) _then) +class __$$SlotConfigurationHmacSha1ImplCopyWithImpl<$Res> + extends _$SlotConfigurationCopyWithImpl<$Res, + _$SlotConfigurationHmacSha1Impl> + implements _$$SlotConfigurationHmacSha1ImplCopyWith<$Res> { + __$$SlotConfigurationHmacSha1ImplCopyWithImpl( + _$SlotConfigurationHmacSha1Impl _value, + $Res Function(_$SlotConfigurationHmacSha1Impl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @@ -848,7 +855,7 @@ class __$$_SlotConfigurationHmacSha1CopyWithImpl<$Res> Object? key = null, Object? options = freezed, }) { - return _then(_$_SlotConfigurationHmacSha1( + return _then(_$SlotConfigurationHmacSha1Impl( key: null == key ? _value.key : key // ignore: cast_nullable_to_non_nullable @@ -864,14 +871,14 @@ class __$$_SlotConfigurationHmacSha1CopyWithImpl<$Res> /// @nodoc @JsonSerializable(explicitToJson: true, includeIfNull: false) -class _$_SlotConfigurationHmacSha1 extends _SlotConfigurationHmacSha1 { - const _$_SlotConfigurationHmacSha1( +class _$SlotConfigurationHmacSha1Impl extends _SlotConfigurationHmacSha1 { + const _$SlotConfigurationHmacSha1Impl( {required this.key, this.options, final String? $type}) : $type = $type ?? 'hmac_sha1', super._(); - factory _$_SlotConfigurationHmacSha1.fromJson(Map json) => - _$$_SlotConfigurationHmacSha1FromJson(json); + factory _$SlotConfigurationHmacSha1Impl.fromJson(Map json) => + _$$SlotConfigurationHmacSha1ImplFromJson(json); @override final String key; @@ -890,7 +897,7 @@ class _$_SlotConfigurationHmacSha1 extends _SlotConfigurationHmacSha1 { bool operator ==(dynamic other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$_SlotConfigurationHmacSha1 && + other is _$SlotConfigurationHmacSha1Impl && (identical(other.key, key) || other.key == key) && (identical(other.options, options) || other.options == options)); } @@ -902,9 +909,9 @@ class _$_SlotConfigurationHmacSha1 extends _SlotConfigurationHmacSha1 { @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$_SlotConfigurationHmacSha1CopyWith<_$_SlotConfigurationHmacSha1> - get copyWith => __$$_SlotConfigurationHmacSha1CopyWithImpl< - _$_SlotConfigurationHmacSha1>(this, _$identity); + _$$SlotConfigurationHmacSha1ImplCopyWith<_$SlotConfigurationHmacSha1Impl> + get copyWith => __$$SlotConfigurationHmacSha1ImplCopyWithImpl< + _$SlotConfigurationHmacSha1Impl>(this, _$identity); @override @optionalTypeArgs @@ -996,7 +1003,7 @@ class _$_SlotConfigurationHmacSha1 extends _SlotConfigurationHmacSha1 { @override Map toJson() { - return _$$_SlotConfigurationHmacSha1ToJson( + return _$$SlotConfigurationHmacSha1ImplToJson( this, ); } @@ -1004,29 +1011,30 @@ class _$_SlotConfigurationHmacSha1 extends _SlotConfigurationHmacSha1 { abstract class _SlotConfigurationHmacSha1 extends SlotConfiguration { const factory _SlotConfigurationHmacSha1( - {required final String key, - final SlotConfigurationOptions? options}) = _$_SlotConfigurationHmacSha1; + {required final String key, + final SlotConfigurationOptions? options}) = + _$SlotConfigurationHmacSha1Impl; const _SlotConfigurationHmacSha1._() : super._(); factory _SlotConfigurationHmacSha1.fromJson(Map json) = - _$_SlotConfigurationHmacSha1.fromJson; + _$SlotConfigurationHmacSha1Impl.fromJson; String get key; @override SlotConfigurationOptions? get options; @override @JsonKey(ignore: true) - _$$_SlotConfigurationHmacSha1CopyWith<_$_SlotConfigurationHmacSha1> + _$$SlotConfigurationHmacSha1ImplCopyWith<_$SlotConfigurationHmacSha1Impl> get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class _$$_SlotConfigurationStaticPasswordCopyWith<$Res> +abstract class _$$SlotConfigurationStaticPasswordImplCopyWith<$Res> implements $SlotConfigurationCopyWith<$Res> { - factory _$$_SlotConfigurationStaticPasswordCopyWith( - _$_SlotConfigurationStaticPassword value, - $Res Function(_$_SlotConfigurationStaticPassword) then) = - __$$_SlotConfigurationStaticPasswordCopyWithImpl<$Res>; + factory _$$SlotConfigurationStaticPasswordImplCopyWith( + _$SlotConfigurationStaticPasswordImpl value, + $Res Function(_$SlotConfigurationStaticPasswordImpl) then) = + __$$SlotConfigurationStaticPasswordImplCopyWithImpl<$Res>; @override @useResult $Res call( @@ -1039,13 +1047,13 @@ abstract class _$$_SlotConfigurationStaticPasswordCopyWith<$Res> } /// @nodoc -class __$$_SlotConfigurationStaticPasswordCopyWithImpl<$Res> +class __$$SlotConfigurationStaticPasswordImplCopyWithImpl<$Res> extends _$SlotConfigurationCopyWithImpl<$Res, - _$_SlotConfigurationStaticPassword> - implements _$$_SlotConfigurationStaticPasswordCopyWith<$Res> { - __$$_SlotConfigurationStaticPasswordCopyWithImpl( - _$_SlotConfigurationStaticPassword _value, - $Res Function(_$_SlotConfigurationStaticPassword) _then) + _$SlotConfigurationStaticPasswordImpl> + implements _$$SlotConfigurationStaticPasswordImplCopyWith<$Res> { + __$$SlotConfigurationStaticPasswordImplCopyWithImpl( + _$SlotConfigurationStaticPasswordImpl _value, + $Res Function(_$SlotConfigurationStaticPasswordImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @@ -1055,7 +1063,7 @@ class __$$_SlotConfigurationStaticPasswordCopyWithImpl<$Res> Object? keyboardLayout = null, Object? options = freezed, }) { - return _then(_$_SlotConfigurationStaticPassword( + return _then(_$SlotConfigurationStaticPasswordImpl( password: null == password ? _value.password : password // ignore: cast_nullable_to_non_nullable @@ -1075,9 +1083,9 @@ class __$$_SlotConfigurationStaticPasswordCopyWithImpl<$Res> /// @nodoc @JsonSerializable(explicitToJson: true, includeIfNull: false) -class _$_SlotConfigurationStaticPassword +class _$SlotConfigurationStaticPasswordImpl extends _SlotConfigurationStaticPassword { - const _$_SlotConfigurationStaticPassword( + const _$SlotConfigurationStaticPasswordImpl( {required this.password, required this.keyboardLayout, this.options, @@ -1085,9 +1093,9 @@ class _$_SlotConfigurationStaticPassword : $type = $type ?? 'static_password', super._(); - factory _$_SlotConfigurationStaticPassword.fromJson( + factory _$SlotConfigurationStaticPasswordImpl.fromJson( Map json) => - _$$_SlotConfigurationStaticPasswordFromJson(json); + _$$SlotConfigurationStaticPasswordImplFromJson(json); @override final String password; @@ -1108,7 +1116,7 @@ class _$_SlotConfigurationStaticPassword bool operator ==(dynamic other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$_SlotConfigurationStaticPassword && + other is _$SlotConfigurationStaticPasswordImpl && (identical(other.password, password) || other.password == password) && (identical(other.keyboardLayout, keyboardLayout) || @@ -1124,10 +1132,10 @@ class _$_SlotConfigurationStaticPassword @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$_SlotConfigurationStaticPasswordCopyWith< - _$_SlotConfigurationStaticPassword> - get copyWith => __$$_SlotConfigurationStaticPasswordCopyWithImpl< - _$_SlotConfigurationStaticPassword>(this, _$identity); + _$$SlotConfigurationStaticPasswordImplCopyWith< + _$SlotConfigurationStaticPasswordImpl> + get copyWith => __$$SlotConfigurationStaticPasswordImplCopyWithImpl< + _$SlotConfigurationStaticPasswordImpl>(this, _$identity); @override @optionalTypeArgs @@ -1219,7 +1227,7 @@ class _$_SlotConfigurationStaticPassword @override Map toJson() { - return _$$_SlotConfigurationStaticPasswordToJson( + return _$$SlotConfigurationStaticPasswordImplToJson( this, ); } @@ -1230,11 +1238,11 @@ abstract class _SlotConfigurationStaticPassword extends SlotConfiguration { {required final String password, required final String keyboardLayout, final SlotConfigurationOptions? options}) = - _$_SlotConfigurationStaticPassword; + _$SlotConfigurationStaticPasswordImpl; const _SlotConfigurationStaticPassword._() : super._(); factory _SlotConfigurationStaticPassword.fromJson(Map json) = - _$_SlotConfigurationStaticPassword.fromJson; + _$SlotConfigurationStaticPasswordImpl.fromJson; String get password; String get keyboardLayout; @@ -1242,18 +1250,18 @@ abstract class _SlotConfigurationStaticPassword extends SlotConfiguration { SlotConfigurationOptions? get options; @override @JsonKey(ignore: true) - _$$_SlotConfigurationStaticPasswordCopyWith< - _$_SlotConfigurationStaticPassword> + _$$SlotConfigurationStaticPasswordImplCopyWith< + _$SlotConfigurationStaticPasswordImpl> get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class _$$_SlotConfigurationYubiOtpCopyWith<$Res> +abstract class _$$SlotConfigurationYubiOtpImplCopyWith<$Res> implements $SlotConfigurationCopyWith<$Res> { - factory _$$_SlotConfigurationYubiOtpCopyWith( - _$_SlotConfigurationYubiOtp value, - $Res Function(_$_SlotConfigurationYubiOtp) then) = - __$$_SlotConfigurationYubiOtpCopyWithImpl<$Res>; + factory _$$SlotConfigurationYubiOtpImplCopyWith( + _$SlotConfigurationYubiOtpImpl value, + $Res Function(_$SlotConfigurationYubiOtpImpl) then) = + __$$SlotConfigurationYubiOtpImplCopyWithImpl<$Res>; @override @useResult $Res call( @@ -1267,11 +1275,13 @@ abstract class _$$_SlotConfigurationYubiOtpCopyWith<$Res> } /// @nodoc -class __$$_SlotConfigurationYubiOtpCopyWithImpl<$Res> - extends _$SlotConfigurationCopyWithImpl<$Res, _$_SlotConfigurationYubiOtp> - implements _$$_SlotConfigurationYubiOtpCopyWith<$Res> { - __$$_SlotConfigurationYubiOtpCopyWithImpl(_$_SlotConfigurationYubiOtp _value, - $Res Function(_$_SlotConfigurationYubiOtp) _then) +class __$$SlotConfigurationYubiOtpImplCopyWithImpl<$Res> + extends _$SlotConfigurationCopyWithImpl<$Res, + _$SlotConfigurationYubiOtpImpl> + implements _$$SlotConfigurationYubiOtpImplCopyWith<$Res> { + __$$SlotConfigurationYubiOtpImplCopyWithImpl( + _$SlotConfigurationYubiOtpImpl _value, + $Res Function(_$SlotConfigurationYubiOtpImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @@ -1282,7 +1292,7 @@ class __$$_SlotConfigurationYubiOtpCopyWithImpl<$Res> Object? key = null, Object? options = freezed, }) { - return _then(_$_SlotConfigurationYubiOtp( + return _then(_$SlotConfigurationYubiOtpImpl( publicId: null == publicId ? _value.publicId : publicId // ignore: cast_nullable_to_non_nullable @@ -1306,8 +1316,8 @@ class __$$_SlotConfigurationYubiOtpCopyWithImpl<$Res> /// @nodoc @JsonSerializable(explicitToJson: true, includeIfNull: false) -class _$_SlotConfigurationYubiOtp extends _SlotConfigurationYubiOtp { - const _$_SlotConfigurationYubiOtp( +class _$SlotConfigurationYubiOtpImpl extends _SlotConfigurationYubiOtp { + const _$SlotConfigurationYubiOtpImpl( {required this.publicId, required this.privateId, required this.key, @@ -1316,8 +1326,8 @@ class _$_SlotConfigurationYubiOtp extends _SlotConfigurationYubiOtp { : $type = $type ?? 'yubiotp', super._(); - factory _$_SlotConfigurationYubiOtp.fromJson(Map json) => - _$$_SlotConfigurationYubiOtpFromJson(json); + factory _$SlotConfigurationYubiOtpImpl.fromJson(Map json) => + _$$SlotConfigurationYubiOtpImplFromJson(json); @override final String publicId; @@ -1340,7 +1350,7 @@ class _$_SlotConfigurationYubiOtp extends _SlotConfigurationYubiOtp { bool operator ==(dynamic other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$_SlotConfigurationYubiOtp && + other is _$SlotConfigurationYubiOtpImpl && (identical(other.publicId, publicId) || other.publicId == publicId) && (identical(other.privateId, privateId) || @@ -1357,9 +1367,9 @@ class _$_SlotConfigurationYubiOtp extends _SlotConfigurationYubiOtp { @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$_SlotConfigurationYubiOtpCopyWith<_$_SlotConfigurationYubiOtp> - get copyWith => __$$_SlotConfigurationYubiOtpCopyWithImpl< - _$_SlotConfigurationYubiOtp>(this, _$identity); + _$$SlotConfigurationYubiOtpImplCopyWith<_$SlotConfigurationYubiOtpImpl> + get copyWith => __$$SlotConfigurationYubiOtpImplCopyWithImpl< + _$SlotConfigurationYubiOtpImpl>(this, _$identity); @override @optionalTypeArgs @@ -1451,7 +1461,7 @@ class _$_SlotConfigurationYubiOtp extends _SlotConfigurationYubiOtp { @override Map toJson() { - return _$$_SlotConfigurationYubiOtpToJson( + return _$$SlotConfigurationYubiOtpImplToJson( this, ); } @@ -1459,14 +1469,15 @@ class _$_SlotConfigurationYubiOtp extends _SlotConfigurationYubiOtp { abstract class _SlotConfigurationYubiOtp extends SlotConfiguration { const factory _SlotConfigurationYubiOtp( - {required final String publicId, - required final String privateId, - required final String key, - final SlotConfigurationOptions? options}) = _$_SlotConfigurationYubiOtp; + {required final String publicId, + required final String privateId, + required final String key, + final SlotConfigurationOptions? options}) = + _$SlotConfigurationYubiOtpImpl; const _SlotConfigurationYubiOtp._() : super._(); factory _SlotConfigurationYubiOtp.fromJson(Map json) = - _$_SlotConfigurationYubiOtp.fromJson; + _$SlotConfigurationYubiOtpImpl.fromJson; String get publicId; String get privateId; @@ -1475,6 +1486,6 @@ abstract class _SlotConfigurationYubiOtp extends SlotConfiguration { SlotConfigurationOptions? get options; @override @JsonKey(ignore: true) - _$$_SlotConfigurationYubiOtpCopyWith<_$_SlotConfigurationYubiOtp> + _$$SlotConfigurationYubiOtpImplCopyWith<_$SlotConfigurationYubiOtpImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/otp/models.g.dart b/lib/otp/models.g.dart index 876aa236..5d8e80e8 100644 --- a/lib/otp/models.g.dart +++ b/lib/otp/models.g.dart @@ -6,27 +6,28 @@ part of 'models.dart'; // JsonSerializableGenerator // ************************************************************************** -_$_OtpState _$$_OtpStateFromJson(Map json) => _$_OtpState( +_$OtpStateImpl _$$OtpStateImplFromJson(Map json) => + _$OtpStateImpl( slot1Configured: json['slot1_configured'] as bool, slot2Configured: json['slot2_configured'] as bool, ); -Map _$$_OtpStateToJson(_$_OtpState instance) => +Map _$$OtpStateImplToJson(_$OtpStateImpl instance) => { 'slot1_configured': instance.slot1Configured, 'slot2_configured': instance.slot2Configured, }; -_$_SlotConfigurationOptions _$$_SlotConfigurationOptionsFromJson( +_$SlotConfigurationOptionsImpl _$$SlotConfigurationOptionsImplFromJson( Map json) => - _$_SlotConfigurationOptions( + _$SlotConfigurationOptionsImpl( digits8: json['digits8'] as bool?, requireTouch: json['require_touch'] as bool?, appendCr: json['append_cr'] as bool?, ); -Map _$$_SlotConfigurationOptionsToJson( - _$_SlotConfigurationOptions instance) { +Map _$$SlotConfigurationOptionsImplToJson( + _$SlotConfigurationOptionsImpl instance) { final val = {}; void writeNotNull(String key, dynamic value) { @@ -41,9 +42,9 @@ Map _$$_SlotConfigurationOptionsToJson( return val; } -_$_SlotConfigurationHotp _$$_SlotConfigurationHotpFromJson( +_$SlotConfigurationHotpImpl _$$SlotConfigurationHotpImplFromJson( Map json) => - _$_SlotConfigurationHotp( + _$SlotConfigurationHotpImpl( key: json['key'] as String, options: json['options'] == null ? null @@ -52,8 +53,8 @@ _$_SlotConfigurationHotp _$$_SlotConfigurationHotpFromJson( $type: json['type'] as String?, ); -Map _$$_SlotConfigurationHotpToJson( - _$_SlotConfigurationHotp instance) { +Map _$$SlotConfigurationHotpImplToJson( + _$SlotConfigurationHotpImpl instance) { final val = { 'key': instance.key, }; @@ -69,9 +70,9 @@ Map _$$_SlotConfigurationHotpToJson( return val; } -_$_SlotConfigurationHmacSha1 _$$_SlotConfigurationHmacSha1FromJson( +_$SlotConfigurationHmacSha1Impl _$$SlotConfigurationHmacSha1ImplFromJson( Map json) => - _$_SlotConfigurationHmacSha1( + _$SlotConfigurationHmacSha1Impl( key: json['key'] as String, options: json['options'] == null ? null @@ -80,8 +81,8 @@ _$_SlotConfigurationHmacSha1 _$$_SlotConfigurationHmacSha1FromJson( $type: json['type'] as String?, ); -Map _$$_SlotConfigurationHmacSha1ToJson( - _$_SlotConfigurationHmacSha1 instance) { +Map _$$SlotConfigurationHmacSha1ImplToJson( + _$SlotConfigurationHmacSha1Impl instance) { final val = { 'key': instance.key, }; @@ -97,20 +98,20 @@ Map _$$_SlotConfigurationHmacSha1ToJson( return val; } -_$_SlotConfigurationStaticPassword _$$_SlotConfigurationStaticPasswordFromJson( - Map json) => - _$_SlotConfigurationStaticPassword( - password: json['password'] as String, - keyboardLayout: json['keyboard_layout'] as String, - options: json['options'] == null - ? null - : SlotConfigurationOptions.fromJson( - json['options'] as Map), - $type: json['type'] as String?, - ); +_$SlotConfigurationStaticPasswordImpl + _$$SlotConfigurationStaticPasswordImplFromJson(Map json) => + _$SlotConfigurationStaticPasswordImpl( + password: json['password'] as String, + keyboardLayout: json['keyboard_layout'] as String, + options: json['options'] == null + ? null + : SlotConfigurationOptions.fromJson( + json['options'] as Map), + $type: json['type'] as String?, + ); -Map _$$_SlotConfigurationStaticPasswordToJson( - _$_SlotConfigurationStaticPassword instance) { +Map _$$SlotConfigurationStaticPasswordImplToJson( + _$SlotConfigurationStaticPasswordImpl instance) { final val = { 'password': instance.password, 'keyboard_layout': instance.keyboardLayout, @@ -127,9 +128,9 @@ Map _$$_SlotConfigurationStaticPasswordToJson( return val; } -_$_SlotConfigurationYubiOtp _$$_SlotConfigurationYubiOtpFromJson( +_$SlotConfigurationYubiOtpImpl _$$SlotConfigurationYubiOtpImplFromJson( Map json) => - _$_SlotConfigurationYubiOtp( + _$SlotConfigurationYubiOtpImpl( publicId: json['public_id'] as String, privateId: json['private_id'] as String, key: json['key'] as String, @@ -140,8 +141,8 @@ _$_SlotConfigurationYubiOtp _$$_SlotConfigurationYubiOtpFromJson( $type: json['type'] as String?, ); -Map _$$_SlotConfigurationYubiOtpToJson( - _$_SlotConfigurationYubiOtp instance) { +Map _$$SlotConfigurationYubiOtpImplToJson( + _$SlotConfigurationYubiOtpImpl instance) { final val = { 'public_id': instance.publicId, 'private_id': instance.privateId, diff --git a/lib/otp/views/actions.dart b/lib/otp/views/actions.dart index d4fa8a2a..9c60d4b6 100644 --- a/lib/otp/views/actions.dart +++ b/lib/otp/views/actions.dart @@ -15,23 +15,23 @@ */ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:yubico_authenticator/otp/state.dart'; -import 'package:yubico_authenticator/otp/views/configure_chalresp_dialog.dart'; -import 'package:yubico_authenticator/otp/views/configure_hotp_dialog.dart'; -import 'package:yubico_authenticator/otp/views/configure_static_dialog.dart'; -import 'package:yubico_authenticator/otp/views/configure_yubiotp_dialog.dart'; -import 'package:yubico_authenticator/otp/views/delete_slot_dialog.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; +import '../../app/models.dart'; import '../../app/shortcuts.dart'; import '../../app/state.dart'; -import '../../app/models.dart'; import '../../core/state.dart'; -import '../models.dart'; -import '../keys.dart' as keys; import '../features.dart' as features; +import '../keys.dart' as keys; +import '../models.dart'; +import '../state.dart'; +import 'configure_chalresp_dialog.dart'; +import 'configure_hotp_dialog.dart'; +import 'configure_static_dialog.dart'; +import 'configure_yubiotp_dialog.dart'; +import 'delete_slot_dialog.dart'; class ConfigureIntent extends Intent { const ConfigureIntent({required this.configurationType}); diff --git a/lib/otp/views/configure_chalresp_dialog.dart b/lib/otp/views/configure_chalresp_dialog.dart index f0babd1f..ac593b15 100644 --- a/lib/otp/views/configure_chalresp_dialog.dart +++ b/lib/otp/views/configure_chalresp_dialog.dart @@ -19,18 +19,18 @@ import 'dart:math'; import 'package:flutter/material.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 '../../app/logging.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; +import '../../core/models.dart'; +import '../../core/state.dart'; import '../../widgets/responsive_dialog.dart'; +import '../keys.dart' as keys; import '../models.dart'; import '../state.dart'; -import '../keys.dart' as keys; import 'overwrite_confirm_dialog.dart'; final _log = Logger('otp.view.configure_Chalresp_dialog'); diff --git a/lib/otp/views/configure_hotp_dialog.dart b/lib/otp/views/configure_hotp_dialog.dart index ce1b5dbf..dcf34c33 100644 --- a/lib/otp/views/configure_hotp_dialog.dart +++ b/lib/otp/views/configure_hotp_dialog.dart @@ -17,20 +17,20 @@ import 'package:flutter/material.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'; +import '../../app/logging.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; +import '../../core/models.dart'; +import '../../core/state.dart'; +import '../../oath/models.dart'; import '../../widgets/choice_filter_chip.dart'; import '../../widgets/responsive_dialog.dart'; +import '../keys.dart' as keys; import '../models.dart'; import '../state.dart'; -import '../keys.dart' as keys; import 'overwrite_confirm_dialog.dart'; final _log = Logger('otp.view.configure_hotp_dialog'); diff --git a/lib/otp/views/configure_static_dialog.dart b/lib/otp/views/configure_static_dialog.dart index 0c08089f..6315e49b 100644 --- a/lib/otp/views/configure_static_dialog.dart +++ b/lib/otp/views/configure_static_dialog.dart @@ -17,18 +17,18 @@ import 'package:flutter/material.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/state.dart'; import 'package:logging/logging.dart'; -import 'package:yubico_authenticator/widgets/choice_filter_chip.dart'; +import '../../app/logging.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; +import '../../core/state.dart'; +import '../../widgets/choice_filter_chip.dart'; import '../../widgets/responsive_dialog.dart'; +import '../keys.dart' as keys; import '../models.dart'; import '../state.dart'; -import '../keys.dart' as keys; import 'overwrite_confirm_dialog.dart'; final _log = Logger('otp.view.configure_Chalresp_dialog'); diff --git a/lib/otp/views/configure_yubiotp_dialog.dart b/lib/otp/views/configure_yubiotp_dialog.dart index d7b1438c..6eb66989 100644 --- a/lib/otp/views/configure_yubiotp_dialog.dart +++ b/lib/otp/views/configure_yubiotp_dialog.dart @@ -14,26 +14,26 @@ * limitations under the License. */ -import 'dart:math'; import 'dart:io'; +import 'dart:math'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.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'; +import '../../app/logging.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; +import '../../core/models.dart'; +import '../../core/state.dart'; +import '../../widgets/choice_filter_chip.dart'; import '../../widgets/responsive_dialog.dart'; +import '../keys.dart' as keys; import '../models.dart'; import '../state.dart'; -import '../keys.dart' as keys; import 'overwrite_confirm_dialog.dart'; final _log = Logger('otp.view.configure_yubiotp_dialog'); diff --git a/lib/otp/views/delete_slot_dialog.dart b/lib/otp/views/delete_slot_dialog.dart index 1a88e1b2..0844d301 100644 --- a/lib/otp/views/delete_slot_dialog.dart +++ b/lib/otp/views/delete_slot_dialog.dart @@ -22,9 +22,9 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; import '../../widgets/responsive_dialog.dart'; +import '../keys.dart' as keys; import '../models.dart'; import '../state.dart'; -import '../keys.dart' as keys; class DeleteSlotDialog extends ConsumerWidget { final DevicePath devicePath; diff --git a/lib/otp/views/key_actions.dart b/lib/otp/views/key_actions.dart index b70882a3..76b370e9 100644 --- a/lib/otp/views/key_actions.dart +++ b/lib/otp/views/key_actions.dart @@ -15,16 +15,16 @@ */ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; -import '../../app/views/fs_dialog.dart'; import '../../app/views/action_list.dart'; -import '../models.dart'; -import '../keys.dart' as keys; +import '../../app/views/fs_dialog.dart'; import '../features.dart' as features; +import '../keys.dart' as keys; +import '../models.dart'; import 'swap_slots_dialog.dart'; Widget otpBuildActions(BuildContext context, DevicePath devicePath, diff --git a/lib/otp/views/otp_screen.dart b/lib/otp/views/otp_screen.dart index e875e1e1..44fbd390 100644 --- a/lib/otp/views/otp_screen.dart +++ b/lib/otp/views/otp_screen.dart @@ -27,9 +27,9 @@ import '../../app/views/app_page.dart'; import '../../app/views/message_page.dart'; import '../../core/state.dart'; import '../../widgets/list_title.dart'; +import '../features.dart' as features; import '../models.dart'; import '../state.dart'; -import '../features.dart' as features; import 'actions.dart'; import 'key_actions.dart'; import 'slot_dialog.dart'; diff --git a/lib/otp/views/slot_dialog.dart b/lib/otp/views/slot_dialog.dart index 73cee688..bc4ee4cd 100644 --- a/lib/otp/views/slot_dialog.dart +++ b/lib/otp/views/slot_dialog.dart @@ -18,12 +18,12 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:yubico_authenticator/otp/state.dart'; import '../../app/state.dart'; -import '../../app/views/fs_dialog.dart'; import '../../app/views/action_list.dart'; +import '../../app/views/fs_dialog.dart'; import '../models.dart'; +import '../state.dart'; import 'actions.dart'; class SlotDialog extends ConsumerWidget { diff --git a/lib/otp/views/swap_slots_dialog.dart b/lib/otp/views/swap_slots_dialog.dart index 7a7eddbc..50334e72 100644 --- a/lib/otp/views/swap_slots_dialog.dart +++ b/lib/otp/views/swap_slots_dialog.dart @@ -19,10 +19,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; -import '../../widgets/responsive_dialog.dart'; -import '../state.dart'; import '../../app/models.dart'; import '../../app/state.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../state.dart'; class SwapSlotsDialog extends ConsumerWidget { final DevicePath devicePath; diff --git a/lib/piv/views/authentication_dialog.dart b/lib/piv/views/authentication_dialog.dart index 74ece398..bce757e1 100644 --- a/lib/piv/views/authentication_dialog.dart +++ b/lib/piv/views/authentication_dialog.dart @@ -17,9 +17,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:yubico_authenticator/core/models.dart'; import '../../app/models.dart'; +import '../../core/models.dart'; import '../../exception/cancellation_exception.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/responsive_dialog.dart'; diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index c61a9c66..6ba6d3cc 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -19,11 +19,11 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:yubico_authenticator/core/models.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; +import '../../core/models.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/app_text_form_field.dart'; import '../../widgets/choice_filter_chip.dart'; From 8fd753431d1bb741a0466de7d20e91d4a84476ec Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Wed, 13 Dec 2023 19:36:50 +0100 Subject: [PATCH 13/27] Don't close FIDO reset dialog on done. --- lib/fido/views/reset_dialog.dart | 105 ++++++++++++++++--------------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/lib/fido/views/reset_dialog.dart b/lib/fido/views/reset_dialog.dart index a912b7c8..034db399 100755 --- a/lib/fido/views/reset_dialog.dart +++ b/lib/fido/views/reset_dialog.dart @@ -43,71 +43,76 @@ class ResetDialog extends ConsumerStatefulWidget { class _ResetDialogState extends ConsumerState { StreamSubscription? _subscription; InteractionEvent? _interaction; - int _currentStep = 0; - final _totalSteps = 4; + int _currentStep = -1; + final _totalSteps = 3; String _getMessage() { final l10n = AppLocalizations.of(context)!; final nfc = widget.node.transport == Transport.nfc; + if (_currentStep == 3) { + return l10n.l_fido_app_reset; + } return switch (_interaction) { InteractionEvent.remove => nfc ? l10n.l_remove_yk_from_reader : l10n.l_unplug_yk, InteractionEvent.insert => nfc ? l10n.l_replace_yk_on_reader : l10n.l_reinsert_yk, InteractionEvent.touch => l10n.l_touch_button_now, - null => l10n.l_press_reset_to_begin + null => '' }; } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - double progress = _currentStep == 0 ? 0.0 : _currentStep / (_totalSteps); + double progress = _currentStep == -1 ? 0.0 : _currentStep / (_totalSteps); return ResponsiveDialog( title: Text(l10n.s_factory_reset), - onCancel: () { - _subscription?.cancel(); - }, - actions: [ - TextButton( - onPressed: _subscription == null - ? () async { - _subscription = ref - .read(fidoStateProvider(widget.node.path).notifier) - .reset() - .listen((event) { - setState(() { - _currentStep++; - _interaction = event; - }); - }, onDone: () { - setState(() { - _currentStep++; - }); - _subscription = null; - Navigator.of(context).pop(); - showMessage(context, l10n.l_fido_app_reset); - }, onError: (e) { - _log.error('Error performing FIDO reset', e); - Navigator.of(context).pop(); - final String errorMessage; - // TODO: Make this cleaner than importing desktop specific RpcError. - if (e is RpcError) { - errorMessage = e.message; - } else { - errorMessage = e.toString(); - } - showMessage( - context, - l10n.l_reset_failed(errorMessage), - duration: const Duration(seconds: 4), - ); - }); - } - : null, - child: Text(l10n.s_reset), - ), - ], + onCancel: _currentStep < 3 + ? () { + _subscription?.cancel(); + } + : null, + actions: _currentStep < 3 + ? [ + TextButton( + onPressed: _subscription == null + ? () async { + _subscription = ref + .read(fidoStateProvider(widget.node.path).notifier) + .reset() + .listen((event) { + setState(() { + _currentStep++; + _interaction = event; + }); + }, onDone: () { + setState(() { + _currentStep++; + }); + _subscription = null; + }, onError: (e) { + _log.error('Error performing FIDO reset', e); + Navigator.of(context).pop(); + final String errorMessage; + // TODO: Make this cleaner than importing desktop specific RpcError. + if (e is RpcError) { + errorMessage = e.message; + } else { + errorMessage = e.toString(); + } + showMessage( + context, + l10n.l_reset_failed(errorMessage), + duration: const Duration(seconds: 4), + ); + }); + } + : null, + child: Text(l10n.s_reset), + ), + ] + : [], child: Padding( padding: const EdgeInsets.symmetric(horizontal: 18.0), child: Column( @@ -120,8 +125,10 @@ class _ResetDialogState extends ConsumerState { Text( l10n.p_warning_disable_accounts, ), - Text('Status: ${_getMessage()}'), - LinearProgressIndicator(value: progress), + if (_currentStep > -1) ...[ + Text('Status: ${_getMessage()}'), + LinearProgressIndicator(value: progress) + ], ] .map((e) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), From 261a0c1f145641ddd0e4bb87ea9477c25305bd02 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Thu, 14 Dec 2023 13:16:34 +0100 Subject: [PATCH 14/27] Use `AppTextField` instead of `TextField`. --- lib/otp/views/configure_chalresp_dialog.dart | 3 ++- lib/otp/views/configure_hotp_dialog.dart | 3 ++- lib/otp/views/configure_static_dialog.dart | 3 ++- lib/otp/views/configure_yubiotp_dialog.dart | 7 ++++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/otp/views/configure_chalresp_dialog.dart b/lib/otp/views/configure_chalresp_dialog.dart index ac593b15..aac9325b 100644 --- a/lib/otp/views/configure_chalresp_dialog.dart +++ b/lib/otp/views/configure_chalresp_dialog.dart @@ -27,6 +27,7 @@ import '../../app/models.dart'; import '../../app/state.dart'; import '../../core/models.dart'; import '../../core/state.dart'; +import '../../widgets/app_text_field.dart'; import '../../widgets/responsive_dialog.dart'; import '../keys.dart' as keys; import '../models.dart'; @@ -122,7 +123,7 @@ class _ConfigureChalrespDialogState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextField( + AppTextField( key: keys.secretField, autofocus: true, controller: _secretController, diff --git a/lib/otp/views/configure_hotp_dialog.dart b/lib/otp/views/configure_hotp_dialog.dart index dcf34c33..feca8a7a 100644 --- a/lib/otp/views/configure_hotp_dialog.dart +++ b/lib/otp/views/configure_hotp_dialog.dart @@ -26,6 +26,7 @@ import '../../app/state.dart'; import '../../core/models.dart'; import '../../core/state.dart'; import '../../oath/models.dart'; +import '../../widgets/app_text_field.dart'; import '../../widgets/choice_filter_chip.dart'; import '../../widgets/responsive_dialog.dart'; import '../keys.dart' as keys; @@ -120,7 +121,7 @@ class _ConfigureHotpDialogState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextField( + AppTextField( key: keys.secretField, controller: _secretController, obscureText: _isObscure, diff --git a/lib/otp/views/configure_static_dialog.dart b/lib/otp/views/configure_static_dialog.dart index 6315e49b..15f69a56 100644 --- a/lib/otp/views/configure_static_dialog.dart +++ b/lib/otp/views/configure_static_dialog.dart @@ -24,6 +24,7 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; import '../../core/state.dart'; +import '../../widgets/app_text_field.dart'; import '../../widgets/choice_filter_chip.dart'; import '../../widgets/responsive_dialog.dart'; import '../keys.dart' as keys; @@ -142,7 +143,7 @@ class _ConfigureStaticDialogState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextField( + AppTextField( key: keys.secretField, autofocus: true, controller: _passwordController, diff --git a/lib/otp/views/configure_yubiotp_dialog.dart b/lib/otp/views/configure_yubiotp_dialog.dart index 6eb66989..c5969fd6 100644 --- a/lib/otp/views/configure_yubiotp_dialog.dart +++ b/lib/otp/views/configure_yubiotp_dialog.dart @@ -29,6 +29,7 @@ import '../../app/models.dart'; import '../../app/state.dart'; import '../../core/models.dart'; import '../../core/state.dart'; +import '../../widgets/app_text_field.dart'; import '../../widgets/choice_filter_chip.dart'; import '../../widgets/responsive_dialog.dart'; import '../keys.dart' as keys; @@ -198,7 +199,7 @@ class _ConfigureYubiOtpDialogState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextField( + AppTextField( key: keys.publicIdField, autofocus: true, controller: _publicIdController, @@ -246,7 +247,7 @@ class _ConfigureYubiOtpDialogState }); }, ), - TextField( + AppTextField( key: keys.privateIdField, controller: _privateIdController, autofillHints: isAndroid ? [] : const [AutofillHints.password], @@ -294,7 +295,7 @@ class _ConfigureYubiOtpDialogState }); }, ), - TextField( + AppTextField( key: keys.secretField, controller: _secretController, autofillHints: isAndroid ? [] : const [AutofillHints.password], From d69328e767a39b8ff8741a7eb989005169b137be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Tro=C3=ABng?= Date: Thu, 14 Dec 2023 14:43:49 +0100 Subject: [PATCH 15/27] First OTP test case. --- integration_test/otp_test.dart | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 integration_test/otp_test.dart diff --git a/integration_test/otp_test.dart b/integration_test/otp_test.dart new file mode 100644 index 00000000..80364ee2 --- /dev/null +++ b/integration_test/otp_test.dart @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@Tags(['android', 'desktop', 'oath']) +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:yubico_authenticator/app/views/keys.dart'; + +import 'utils/test_util.dart'; + +void main() { + var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; + + group('OTP UI tests', () { + appTest('OTP menu items exist', (WidgetTester tester) async { + await tester.tap(find.byKey(otpAppDrawer)); + await tester.shortWait(); + }); + }); +} From d7db9fd56594e04cade053a97a8fc95064e7c1d3 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Thu, 14 Dec 2023 15:53:17 +0100 Subject: [PATCH 16/27] support OTP app on YubiKey Neo (fw 3.x) --- helper/helper/device.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helper/helper/device.py b/helper/helper/device.py index a3a336d8..09c9a6d4 100644 --- a/helper/helper/device.py +++ b/helper/helper/device.py @@ -413,7 +413,9 @@ class ConnectionNode(RpcNode): or ( # SmartCardConnection can be used over NFC, or on 5.3 and later. isinstance(self._connection, SmartCardConnection) and ( - self._transport == TRANSPORT.NFC or self._info.version >= (5, 3, 0) + self._transport == TRANSPORT.NFC or + self._info.version >= (5, 3, 0) or + self._info.version[0] == 3 ) ) ) From ce5b8abe815dc1d93d36dbdbee9a20ad1994ffc9 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Thu, 14 Dec 2023 16:37:24 +0100 Subject: [PATCH 17/27] Fix typo in logger. --- lib/otp/views/configure_chalresp_dialog.dart | 2 +- lib/otp/views/configure_static_dialog.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/otp/views/configure_chalresp_dialog.dart b/lib/otp/views/configure_chalresp_dialog.dart index aac9325b..47fdebc5 100644 --- a/lib/otp/views/configure_chalresp_dialog.dart +++ b/lib/otp/views/configure_chalresp_dialog.dart @@ -34,7 +34,7 @@ import '../models.dart'; import '../state.dart'; import 'overwrite_confirm_dialog.dart'; -final _log = Logger('otp.view.configure_Chalresp_dialog'); +final _log = Logger('otp.view.configure_chalresp_dialog'); class ConfigureChalrespDialog extends ConsumerStatefulWidget { final DevicePath devicePath; diff --git a/lib/otp/views/configure_static_dialog.dart b/lib/otp/views/configure_static_dialog.dart index 15f69a56..1b4020d2 100644 --- a/lib/otp/views/configure_static_dialog.dart +++ b/lib/otp/views/configure_static_dialog.dart @@ -32,7 +32,7 @@ import '../models.dart'; import '../state.dart'; import 'overwrite_confirm_dialog.dart'; -final _log = Logger('otp.view.configure_Chalresp_dialog'); +final _log = Logger('otp.view.configure_static_dialog'); class ConfigureStaticDialog extends ConsumerStatefulWidget { final DevicePath devicePath; From dc5d3ce9dc0df59d072095bc355802b41743e804 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Thu, 14 Dec 2023 16:38:10 +0100 Subject: [PATCH 18/27] Add `AppInputDecoration`. --- lib/fido/views/locked_page.dart | 35 ++-- lib/oath/views/add_account_page.dart | 70 +++----- lib/oath/views/manage_password_dialog.dart | 109 +++++------- lib/oath/views/unlock_form.dart | 43 ++--- lib/otp/views/configure_chalresp_dialog.dart | 64 +++---- lib/otp/views/configure_hotp_dialog.dart | 64 +++---- lib/otp/views/configure_static_dialog.dart | 58 +++--- lib/otp/views/configure_yubiotp_dialog.dart | 178 ++++++++----------- lib/piv/views/authentication_dialog.dart | 46 ++--- lib/piv/views/generate_key_dialog.dart | 2 +- lib/piv/views/import_file_dialog.dart | 35 ++-- lib/piv/views/manage_key_dialog.dart | 81 +++------ lib/piv/views/manage_pin_puk_dialog.dart | 141 ++++++--------- lib/piv/views/pin_dialog.dart | 35 ++-- lib/widgets/app_text_field.dart | 89 +++++++++- 15 files changed, 458 insertions(+), 592 deletions(-) diff --git a/lib/fido/views/locked_page.dart b/lib/fido/views/locked_page.dart index 85e9c06a..204b5b9d 100755 --- a/lib/fido/views/locked_page.dart +++ b/lib/fido/views/locked_page.dart @@ -166,36 +166,23 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> { obscureText: _isObscure, autofillHints: const [AutofillHints.password], controller: _pinController, - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_pin, helperText: '', // Prevents dialog resizing errorText: _pinIsWrong ? _getErrorText() : null, errorMaxLines: 3, prefixIcon: const Icon(Icons.pin_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon( - _isObscure ? Icons.visibility : Icons.visibility_off, - color: !_pinIsWrong - ? IconTheme.of(context).color - : null), - onPressed: () { - setState(() { - _isObscure = !_isObscure; - }); - }, - tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin, - ), - if (_pinIsWrong) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], + suffixIcon: IconButton( + icon: Icon( + _isObscure ? Icons.visibility : Icons.visibility_off, + color: !_pinIsWrong ? IconTheme.of(context).color : null), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin, ), ), onChanged: (value) { diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index 34be1744..f73ae139 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -363,7 +363,7 @@ class _OathAddAccountPageState extends ConsumerState { limitBytesLength(issuerRemaining), ], buildCounter: buildByteCounterFor(issuerText), - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_issuer_optional, helperText: @@ -395,7 +395,7 @@ class _OathAddAccountPageState extends ConsumerState { maxLength: nameMaxLength, buildCounter: buildByteCounterFor(nameText), inputFormatters: [limitBytesLength(nameRemaining)], - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_account_name, helperText: '', @@ -429,45 +429,33 @@ class _OathAddAccountPageState extends ConsumerState { // would hint to use saved passwords for this field autofillHints: isAndroid ? [] : const [AutofillHints.password], - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_secret_key, - errorText: _validateSecret && !secretLengthValid - ? l10n.s_invalid_length - : _validateSecret && !secretFormatValid - ? l10n.l_invalid_format_allowed_chars( - Format.base32.allowedCharacters) - : null, - prefixIcon: const Icon(Icons.key_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon( - _isObscure - ? Icons.visibility - : Icons.visibility_off, - color: !_validateSecret - ? IconTheme.of(context).color - : null), - onPressed: () { - setState(() { - _isObscure = !_isObscure; - }); - }, - tooltip: _isObscure - ? l10n.s_show_secret_key - : l10n.s_hide_secret_key, - ), - if (_validateSecret) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], - ), - ), + decoration: AppInputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_secret_key, + errorText: _validateSecret && !secretLengthValid + ? l10n.s_invalid_length + : _validateSecret && !secretFormatValid + ? l10n.l_invalid_format_allowed_chars( + Format.base32.allowedCharacters) + : null, + prefixIcon: const Icon(Icons.key_outlined), + suffixIcon: IconButton( + icon: Icon( + _isObscure + ? Icons.visibility + : Icons.visibility_off, + color: !_validateSecret + ? IconTheme.of(context).color + : null), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + tooltip: _isObscure + ? l10n.s_show_secret_key + : l10n.s_hide_secret_key, + )), readOnly: _dataLoaded, textInputAction: TextInputAction.done, onChanged: (value) { diff --git a/lib/oath/views/manage_password_dialog.dart b/lib/oath/views/manage_password_dialog.dart index 1ac00dae..d9d568b8 100755 --- a/lib/oath/views/manage_password_dialog.dart +++ b/lib/oath/views/manage_password_dialog.dart @@ -91,35 +91,25 @@ class _ManagePasswordDialogState extends ConsumerState { obscureText: _isObscureCurrent, autofillHints: const [AutofillHints.password], key: keys.currentPasswordField, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_current_password, - errorText: _currentIsWrong ? l10n.s_wrong_password : null, - errorMaxLines: 3, - prefixIcon: const Icon(Icons.password_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon(_isObscureCurrent - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - setState(() { - _isObscureCurrent = !_isObscureCurrent; - }); - }, - tooltip: _isObscureCurrent - ? l10n.s_show_password - : l10n.s_hide_password), - if (_currentIsWrong) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], - )), + decoration: AppInputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_current_password, + errorText: _currentIsWrong ? l10n.s_wrong_password : null, + errorMaxLines: 3, + prefixIcon: const Icon(Icons.password_outlined), + suffixIcon: IconButton( + icon: Icon(_isObscureCurrent + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureCurrent = !_isObscureCurrent; + }); + }, + tooltip: _isObscureCurrent + ? l10n.s_show_password + : l10n.s_hide_password), + ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { @@ -173,26 +163,22 @@ class _ManagePasswordDialogState extends ConsumerState { autofocus: !widget.state.hasKey, obscureText: _isObscureNew, autofillHints: const [AutofillHints.newPassword], - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_new_password, prefixIcon: const Icon(Icons.password_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon(_isObscureNew - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - setState(() { - _isObscureNew = !_isObscureNew; - }); - }, - tooltip: _isObscureNew - ? l10n.s_show_password - : l10n.s_hide_password), - ]), + suffixIcon: IconButton( + icon: Icon(_isObscureNew + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureNew = !_isObscureNew; + }); + }, + tooltip: _isObscureNew + ? l10n.s_show_password + : l10n.s_hide_password), enabled: !widget.state.hasKey || _currentPassword.isNotEmpty, ), textInputAction: TextInputAction.next, @@ -211,27 +197,22 @@ class _ManagePasswordDialogState extends ConsumerState { key: keys.confirmPasswordField, obscureText: _isObscureConfirm, autofillHints: const [AutofillHints.newPassword], - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_confirm_password, prefixIcon: const Icon(Icons.password_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon(_isObscureConfirm - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - setState(() { - _isObscureConfirm = !_isObscureConfirm; - }); - }, - tooltip: _isObscureConfirm - ? l10n.s_show_password - : l10n.s_hide_password) - ], - ), + suffixIcon: IconButton( + icon: Icon(_isObscureConfirm + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureConfirm = !_isObscureConfirm; + }); + }, + tooltip: _isObscureConfirm + ? l10n.s_show_password + : l10n.s_hide_password), enabled: (!widget.state.hasKey || _currentPassword.isNotEmpty) && _newPassword.isNotEmpty, diff --git a/lib/oath/views/unlock_form.dart b/lib/oath/views/unlock_form.dart index f3cdde21..4bfdb69e 100755 --- a/lib/oath/views/unlock_form.dart +++ b/lib/oath/views/unlock_form.dart @@ -79,39 +79,26 @@ class _UnlockFormState extends ConsumerState { autofocus: true, obscureText: _isObscure, autofillHints: const [AutofillHints.password], - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_password, errorText: _passwordIsWrong ? l10n.s_wrong_password : null, helperText: '', // Prevents resizing when errorText shown prefixIcon: const Icon(Icons.password_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon( - _isObscure - ? Icons.visibility - : Icons.visibility_off, - color: !_passwordIsWrong - ? IconTheme.of(context).color - : null), - onPressed: () { - setState(() { - _isObscure = !_isObscure; - }); - }, - tooltip: _isObscure - ? l10n.s_show_password - : l10n.s_hide_password, - ), - if (_passwordIsWrong) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], + suffixIcon: IconButton( + icon: Icon( + _isObscure ? Icons.visibility : Icons.visibility_off, + color: !_passwordIsWrong + ? IconTheme.of(context).color + : null), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + tooltip: _isObscure + ? l10n.s_show_password + : l10n.s_hide_password, ), ), onChanged: (_) => setState(() { diff --git a/lib/otp/views/configure_chalresp_dialog.dart b/lib/otp/views/configure_chalresp_dialog.dart index 47fdebc5..2e5421c4 100644 --- a/lib/otp/views/configure_chalresp_dialog.dart +++ b/lib/otp/views/configure_chalresp_dialog.dart @@ -129,46 +129,34 @@ class _ConfigureChalrespDialogState controller: _secretController, autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: secretMaxLength, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_secret_key, - errorText: _validateSecret && !secretLengthValid - ? l10n.s_invalid_length - : _validateSecret && !secretFormatValid - ? l10n.l_invalid_format_allowed_chars( - Format.hex.allowedCharacters) - : null, - prefixIcon: const Icon(Icons.key_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: () { + decoration: AppInputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_secret_key, + errorText: _validateSecret && !secretLengthValid + ? l10n.s_invalid_length + : _validateSecret && !secretFormatValid + ? l10n.l_invalid_format_allowed_chars( + Format.hex.allowedCharacters) + : null, + prefixIcon: const Icon(Icons.key_outlined), + suffixIcon: IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + setState(() { + final random = Random.secure(); + final key = List.generate( + 20, + (_) => random + .nextInt(256) + .toRadixString(16) + .padLeft(2, '0')).join(); setState(() { - final random = Random.secure(); - final key = List.generate( - 20, - (_) => random - .nextInt(256) - .toRadixString(16) - .padLeft(2, '0')).join(); - setState(() { - _secretController.text = key; - }); + _secretController.text = key; }); - }, - tooltip: l10n.s_generate_random, - ), - if (_validateSecret) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], - ), - ), + }); + }, + tooltip: l10n.s_generate_random, + )), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { diff --git a/lib/otp/views/configure_hotp_dialog.dart b/lib/otp/views/configure_hotp_dialog.dart index feca8a7a..8aea8eb2 100644 --- a/lib/otp/views/configure_hotp_dialog.dart +++ b/lib/otp/views/configure_hotp_dialog.dart @@ -126,44 +126,32 @@ class _ConfigureHotpDialogState extends ConsumerState { controller: _secretController, obscureText: _isObscure, autofillHints: isAndroid ? [] : const [AutofillHints.password], - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_secret_key, - helperText: '', // Prevents resizing when errorText shown - errorText: _validateSecret && !secretLengthValid - ? l10n.s_invalid_length - : _validateSecret && !secretFormatValid - ? l10n.l_invalid_format_allowed_chars( - Format.base32.allowedCharacters) - : null, - prefixIcon: const Icon(Icons.key_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon( - _isObscure ? Icons.visibility : Icons.visibility_off, - color: !_validateSecret - ? IconTheme.of(context).color - : null), - onPressed: () { - setState(() { - _isObscure = !_isObscure; - }); - }, - tooltip: _isObscure - ? l10n.s_show_secret_key - : l10n.s_hide_secret_key, - ), - if (_validateSecret) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], - ), - ), + decoration: AppInputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_secret_key, + helperText: '', // Prevents resizing when errorText shown + errorText: _validateSecret && !secretLengthValid + ? l10n.s_invalid_length + : _validateSecret && !secretFormatValid + ? l10n.l_invalid_format_allowed_chars( + Format.base32.allowedCharacters) + : null, + prefixIcon: const Icon(Icons.key_outlined), + suffixIcon: IconButton( + icon: Icon( + _isObscure ? Icons.visibility : Icons.visibility_off, + color: !_validateSecret + ? IconTheme.of(context).color + : null), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + tooltip: _isObscure + ? l10n.s_show_secret_key + : l10n.s_hide_secret_key, + )), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { diff --git a/lib/otp/views/configure_static_dialog.dart b/lib/otp/views/configure_static_dialog.dart index 1b4020d2..6395c76d 100644 --- a/lib/otp/views/configure_static_dialog.dart +++ b/lib/otp/views/configure_static_dialog.dart @@ -149,41 +149,29 @@ class _ConfigureStaticDialogState extends ConsumerState { controller: _passwordController, autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: passwordMaxLength, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_password, - errorText: _validatePassword && !passwordLengthValid - ? l10n.s_invalid_length - : _validatePassword && !passwordFormatValid - ? l10n.l_invalid_keyboard_character - : null, - prefixIcon: const Icon(Icons.key_outlined), - 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) - .generateStaticPassword( - passwordMaxLength, _keyboardLayout); - setState(() { - _validatePassword = false; - _passwordController.text = password; - }); - }, - ), - if (_validatePassword) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], - ), - ), + decoration: AppInputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_password, + errorText: _validatePassword && !passwordLengthValid + ? l10n.s_invalid_length + : _validatePassword && !passwordFormatValid + ? l10n.l_invalid_keyboard_character + : null, + prefixIcon: const Icon(Icons.key_outlined), + suffixIcon: IconButton( + tooltip: l10n.s_generate_passowrd, + icon: const Icon(Icons.refresh), + onPressed: () async { + final password = await ref + .read(otpStateProvider(widget.devicePath).notifier) + .generateStaticPassword( + passwordMaxLength, _keyboardLayout); + setState(() { + _validatePassword = false; + _passwordController.text = password; + }); + }, + )), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { diff --git a/lib/otp/views/configure_yubiotp_dialog.dart b/lib/otp/views/configure_yubiotp_dialog.dart index c5969fd6..4c089323 100644 --- a/lib/otp/views/configure_yubiotp_dialog.dart +++ b/lib/otp/views/configure_yubiotp_dialog.dart @@ -205,41 +205,29 @@ class _ConfigureYubiOtpDialogState controller: _publicIdController, autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: publicIdLength, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_public_id, - errorText: _validatePublicIdFormat && !publicIdFormatValid - ? l10n.l_invalid_format_allowed_chars( - Format.modhex.allowedCharacters) - : null, - prefixIcon: const Icon(Icons.public_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - tooltip: l10n.s_use_serial, - icon: const Icon(Icons.auto_awesome_outlined), - onPressed: (info?.serial != null) - ? () async { - final publicId = await ref - .read(otpStateProvider(widget.devicePath) - .notifier) - .modhexEncodeSerial(info!.serial!); - setState(() { - _publicIdController.text = publicId; - }); - } - : null, - ), - if (_validatePublicIdFormat) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], - ), - ), + decoration: AppInputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_public_id, + errorText: _validatePublicIdFormat && !publicIdFormatValid + ? l10n.l_invalid_format_allowed_chars( + Format.modhex.allowedCharacters) + : null, + prefixIcon: const Icon(Icons.public_outlined), + suffixIcon: IconButton( + tooltip: l10n.s_use_serial, + icon: const Icon(Icons.auto_awesome_outlined), + onPressed: (info?.serial != null) + ? () async { + final publicId = await ref + .read(otpStateProvider(widget.devicePath) + .notifier) + .modhexEncodeSerial(info!.serial!); + setState(() { + _publicIdController.text = publicId; + }); + } + : null, + )), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { @@ -252,42 +240,30 @@ class _ConfigureYubiOtpDialogState controller: _privateIdController, autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: privateIdLength, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_private_id, - errorText: _validatePrivateIdFormat && !privatedIdFormatValid - ? l10n.l_invalid_format_allowed_chars( - Format.hex.allowedCharacters) - : null, - prefixIcon: const Icon(Icons.key_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - tooltip: l10n.s_generate_random, - icon: const Icon(Icons.refresh), - onPressed: () { - final random = Random.secure(); - final key = List.generate( - 6, - (_) => random - .nextInt(256) - .toRadixString(16) - .padLeft(2, '0')).join(); - setState(() { - _privateIdController.text = key; - }); - }, - ), - if (_validatePrivateIdFormat) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], - ), - ), + decoration: AppInputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_private_id, + errorText: _validatePrivateIdFormat && !privatedIdFormatValid + ? l10n.l_invalid_format_allowed_chars( + Format.hex.allowedCharacters) + : null, + prefixIcon: const Icon(Icons.key_outlined), + suffixIcon: IconButton( + tooltip: l10n.s_generate_random, + icon: const Icon(Icons.refresh), + onPressed: () { + final random = Random.secure(); + final key = List.generate( + 6, + (_) => random + .nextInt(256) + .toRadixString(16) + .padLeft(2, '0')).join(); + setState(() { + _privateIdController.text = key; + }); + }, + )), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { @@ -300,42 +276,30 @@ class _ConfigureYubiOtpDialogState controller: _secretController, autofillHints: isAndroid ? [] : const [AutofillHints.password], maxLength: secretLength, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_secret_key, - errorText: _validateSecretFormat && !secretFormatValid - ? l10n.l_invalid_format_allowed_chars( - Format.hex.allowedCharacters) - : null, - prefixIcon: const Icon(Icons.key_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - tooltip: l10n.s_generate_random, - icon: const Icon(Icons.refresh), - onPressed: () { - final random = Random.secure(); - final key = List.generate( - 16, - (_) => random - .nextInt(256) - .toRadixString(16) - .padLeft(2, '0')).join(); - setState(() { - _secretController.text = key; - }); - }, - ), - if (_validateSecretFormat) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], - ), - ), + decoration: AppInputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_secret_key, + errorText: _validateSecretFormat && !secretFormatValid + ? l10n.l_invalid_format_allowed_chars( + Format.hex.allowedCharacters) + : null, + prefixIcon: const Icon(Icons.key_outlined), + suffixIcon: IconButton( + tooltip: l10n.s_generate_random, + icon: const Icon(Icons.refresh), + onPressed: () { + final random = Random.secure(); + final key = List.generate( + 16, + (_) => random + .nextInt(256) + .toRadixString(16) + .padLeft(2, '0')).join(); + setState(() { + _secretController.text = key; + }); + }, + )), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { diff --git a/lib/piv/views/authentication_dialog.dart b/lib/piv/views/authentication_dialog.dart index bce757e1..8ad9304a 100644 --- a/lib/piv/views/authentication_dialog.dart +++ b/lib/piv/views/authentication_dialog.dart @@ -109,7 +109,7 @@ class _AuthenticationDialogState extends ConsumerState { controller: _keyController, readOnly: _defaultKeyUsed, maxLength: !_defaultKeyUsed ? keyLen : null, - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_management_key, helperText: _defaultKeyUsed ? l10n.l_default_key_used : null, @@ -125,34 +125,22 @@ class _AuthenticationDialogState extends ConsumerState { ? null : hasMetadata && (_keyIsWrong || _keyFormatInvalid) ? const Icon(Icons.error) - : Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon(_defaultKeyUsed - ? Icons.auto_awesome - : Icons.auto_awesome_outlined), - tooltip: l10n.s_use_default, - onPressed: () { - setState(() { - _keyFormatInvalid = false; - _defaultKeyUsed = !_defaultKeyUsed; - if (_defaultKeyUsed) { - _keyController.text = - defaultManagementKey; - } else { - _keyController.clear(); - } - }); - }, - ), - if (_keyIsWrong || _keyFormatInvalid) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], + : IconButton( + icon: Icon(_defaultKeyUsed + ? Icons.auto_awesome + : Icons.auto_awesome_outlined), + tooltip: l10n.s_use_default, + onPressed: () { + setState(() { + _keyFormatInvalid = false; + _defaultKeyUsed = !_defaultKeyUsed; + if (_defaultKeyUsed) { + _keyController.text = defaultManagementKey; + } else { + _keyController.clear(); + } + }); + }, ), ), textInputAction: TextInputAction.next, diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart index b96e2d58..82c313ca 100644 --- a/lib/piv/views/generate_key_dialog.dart +++ b/lib/piv/views/generate_key_dialog.dart @@ -161,7 +161,7 @@ class _GenerateKeyDialogState extends ConsumerState { AppTextField( autofocus: true, key: keys.subjectField, - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_subject, errorText: _subject.isNotEmpty && _invalidSubject diff --git a/lib/piv/views/import_file_dialog.dart b/lib/piv/views/import_file_dialog.dart index ebc02385..4dd52468 100644 --- a/lib/piv/views/import_file_dialog.dart +++ b/lib/piv/views/import_file_dialog.dart @@ -129,34 +129,23 @@ class _ImportFileDialogState extends ConsumerState { obscureText: _isObscure, autofillHints: const [AutofillHints.password], key: keys.managementKeyField, - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_password, errorText: _passwordIsWrong ? l10n.s_wrong_password : null, errorMaxLines: 3, prefixIcon: const Icon(Icons.password_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon(_isObscure - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - setState(() { - _isObscure = !_isObscure; - }); - }, - tooltip: _isObscure - ? l10n.s_show_password - : l10n.s_hide_password), - if (_passwordIsWrong) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ]), + suffixIcon: IconButton( + icon: Icon( + _isObscure ? Icons.visibility : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + tooltip: _isObscure + ? l10n.s_show_password + : l10n.s_hide_password), ), textInputAction: TextInputAction.next, onChanged: (value) { diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index 6ba6d3cc..53cf57df 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -173,7 +173,7 @@ class _ManageKeyDialogState extends ConsumerState { key: keys.pinPukField, maxLength: 8, controller: _currentController, - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_pin, errorText: _currentIsWrong @@ -184,27 +184,15 @@ class _ManageKeyDialogState extends ConsumerState { : null, errorMaxLines: 3, prefixIcon: const Icon(Icons.pin_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon(_isObscure - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - setState(() { - _isObscure = !_isObscure; - }); - }, - tooltip: - _isObscure ? l10n.s_show_pin : l10n.s_hide_pin), - if (_currentIsWrong || _currentInvalidFormat) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ]), + suffixIcon: IconButton( + icon: Icon( + _isObscure ? Icons.visibility : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin), ), textInputAction: TextInputAction.next, onChanged: (value) { @@ -284,7 +272,7 @@ class _ManageKeyDialogState extends ConsumerState { autofillHints: const [AutofillHints.newPassword], maxLength: hexLength, controller: _keyController, - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_new_management_key, errorText: _newInvalidFormat @@ -293,35 +281,24 @@ class _ManageKeyDialogState extends ConsumerState { : null, enabled: currentLenOk, prefixIcon: const Icon(Icons.key_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.refresh), - tooltip: l10n.s_generate_random, - onPressed: currentLenOk - ? () { - final random = Random.secure(); - final key = List.generate( - _keyType.keyLength, - (_) => random - .nextInt(256) - .toRadixString(16) - .padLeft(2, '0')).join(); - setState(() { - _keyController.text = key; - _newInvalidFormat = false; - }); - } - : null, - ), - if (_newInvalidFormat) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], + suffixIcon: IconButton( + icon: const Icon(Icons.refresh), + tooltip: l10n.s_generate_random, + onPressed: currentLenOk + ? () { + final random = Random.secure(); + final key = List.generate( + _keyType.keyLength, + (_) => random + .nextInt(256) + .toRadixString(16) + .padLeft(2, '0')).join(); + setState(() { + _keyController.text = key; + _newInvalidFormat = false; + }); + } + : null, ), ), textInputAction: TextInputAction.next, diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart index 09aa8d16..f0cae705 100644 --- a/lib/piv/views/manage_pin_puk_dialog.dart +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -111,48 +111,34 @@ class _ManagePinPukDialogState extends ConsumerState { maxLength: 8, autofillHints: const [AutofillHints.password], key: keys.pinPukField, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: widget.target == ManageTarget.pin - ? l10n.s_current_pin - : l10n.s_current_puk, - errorText: _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(Icons.password_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon(_isObscureCurrent - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - setState(() { - _isObscureCurrent = !_isObscureCurrent; - }); - }, - tooltip: widget.target == ManageTarget.pin - ? (_isObscureCurrent - ? l10n.s_show_pin - : l10n.s_hide_pin) - : (_isObscureCurrent - ? l10n.s_show_puk - : l10n.s_hide_puk), - ), - if (_currentIsWrong) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], - )), + decoration: AppInputDecoration( + border: const OutlineInputBorder(), + labelText: widget.target == ManageTarget.pin + ? l10n.s_current_pin + : l10n.s_current_puk, + errorText: _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(Icons.password_outlined), + suffixIcon: IconButton( + icon: Icon(_isObscureCurrent + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureCurrent = !_isObscureCurrent; + }); + }, + tooltip: widget.target == ManageTarget.pin + ? (_isObscureCurrent ? l10n.s_show_pin : l10n.s_hide_pin) + : (_isObscureCurrent ? l10n.s_show_puk : l10n.s_hide_puk), + ), + ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { @@ -168,33 +154,24 @@ class _ManagePinPukDialogState extends ConsumerState { obscureText: _isObscureNew, maxLength: 8, autofillHints: const [AutofillHints.newPassword], - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: widget.target == ManageTarget.puk ? l10n.s_new_puk : l10n.s_new_pin, prefixIcon: const Icon(Icons.password_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon(_isObscureNew - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - setState(() { - _isObscureNew = !_isObscureNew; - }); - }, - tooltip: widget.target == ManageTarget.pin - ? (_isObscureNew - ? l10n.s_show_pin - : l10n.s_hide_pin) - : (_isObscureNew - ? l10n.s_show_puk - : l10n.s_hide_puk), - ), - ]), + suffixIcon: IconButton( + icon: Icon( + _isObscureNew ? Icons.visibility : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureNew = !_isObscureNew; + }); + }, + tooltip: widget.target == ManageTarget.pin + ? (_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, ), @@ -215,33 +192,25 @@ class _ManagePinPukDialogState extends ConsumerState { obscureText: _isObscureConfirm, maxLength: 8, autofillHints: const [AutofillHints.newPassword], - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: widget.target == ManageTarget.puk ? l10n.s_confirm_puk : l10n.s_confirm_pin, prefixIcon: const Icon(Icons.password_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon(_isObscureConfirm - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - setState(() { - _isObscureConfirm = !_isObscureConfirm; - }); - }, - tooltip: widget.target == ManageTarget.pin - ? (_isObscureConfirm - ? l10n.s_show_pin - : l10n.s_hide_pin) - : (_isObscureConfirm - ? l10n.s_show_puk - : l10n.s_hide_puk), - ) - ]), + suffixIcon: IconButton( + icon: Icon(_isObscureConfirm + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureConfirm = !_isObscureConfirm; + }); + }, + tooltip: widget.target == ManageTarget.pin + ? (_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, ), textInputAction: TextInputAction.done, diff --git a/lib/piv/views/pin_dialog.dart b/lib/piv/views/pin_dialog.dart index dee6f074..1b5f6a25 100644 --- a/lib/piv/views/pin_dialog.dart +++ b/lib/piv/views/pin_dialog.dart @@ -93,7 +93,7 @@ class _PinDialogState extends ConsumerState { autofillHints: const [AutofillHints.password], key: keys.managementKeyField, controller: _pinController, - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_pin, errorText: _pinIsWrong @@ -101,29 +101,16 @@ class _PinDialogState extends ConsumerState { : null, errorMaxLines: 3, prefixIcon: const Icon(Icons.pin_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon( - _isObscure ? Icons.visibility : Icons.visibility_off, - color: !_pinIsWrong - ? IconTheme.of(context).color - : null), - onPressed: () { - setState(() { - _isObscure = !_isObscure; - }); - }, - tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin, - ), - if (_pinIsWrong) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], + suffixIcon: IconButton( + icon: Icon( + _isObscure ? Icons.visibility : Icons.visibility_off, + color: !_pinIsWrong ? IconTheme.of(context).color : null), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin, ), ), textInputAction: TextInputAction.next, diff --git a/lib/widgets/app_text_field.dart b/lib/widgets/app_text_field.dart index a4f70d1f..bf395747 100644 --- a/lib/widgets/app_text_field.dart +++ b/lib/widgets/app_text_field.dart @@ -16,6 +16,91 @@ import 'package:flutter/material.dart'; +class AppInputDecoration extends InputDecoration { + final List? suffixIcons; + + const AppInputDecoration({ + // allow multiple suffixIcons + this.suffixIcons, + // forward other TextField parameters + super.icon, + super.iconColor, + super.label, + super.labelText, + super.labelStyle, + super.floatingLabelStyle, + super.helperText, + super.helperStyle, + super.helperMaxLines, + super.hintText, + super.hintStyle, + super.hintTextDirection, + super.hintMaxLines, + super.hintFadeDuration, + super.error, + super.errorText, + super.errorStyle, + super.errorMaxLines, + super.floatingLabelBehavior, + super.floatingLabelAlignment, + super.isCollapsed, + super.isDense, + super.contentPadding, + super.prefixIcon, + super.prefixIconConstraints, + super.prefix, + super.prefixText, + super.prefixStyle, + super.prefixIconColor, + super.suffixIcon, + super.suffix, + super.suffixText, + super.suffixStyle, + super.suffixIconColor, + super.suffixIconConstraints, + super.counter, + super.counterText, + super.counterStyle, + super.filled, + super.fillColor, + super.focusColor, + super.hoverColor, + super.errorBorder, + super.focusedBorder, + super.focusedErrorBorder, + super.disabledBorder, + super.enabledBorder, + super.border, + super.enabled = true, + super.semanticCounterText, + super.alignLabelWithHint, + super.constraints, + }) : assert(!(suffixIcon != null && suffixIcons != null), + 'Declaring both suffixIcon and suffixIcons is not supported.'); + + @override + Widget? get suffixIcon { + final hasError = errorText != null; + + if (hasError || suffixIcons != null) { + final errorIcon = hasError ? const Icon(Icons.error_outlined) : null; + + final existingSuffixIcon = super.suffixIcon; + + return Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (suffixIcons != null) ...suffixIcons!, + if (existingSuffixIcon != null) existingSuffixIcon, + if (errorIcon != null) ...[errorIcon, const SizedBox(width: 8.0)], + ], + ); + } + + return super.suffixIcon; + } +} + /// TextField without autocorrect and suggestions class AppTextField extends TextField { const AppTextField({ @@ -28,7 +113,7 @@ class AppTextField extends TextField { super.controller, super.focusNode, super.undoController, - super.decoration, + AppInputDecoration? decoration, super.textInputAction, super.textCapitalization, super.style, @@ -83,5 +168,5 @@ class AppTextField extends TextField { super.canRequestFocus, super.spellCheckConfiguration, super.magnifierConfiguration, - }); + }) : super(decoration: decoration); } From 25bdf2012a81521062c59daed9270bdbaf0c2593 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Thu, 14 Dec 2023 16:49:36 +0100 Subject: [PATCH 19/27] fix python formatting --- helper/helper/device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helper/helper/device.py b/helper/helper/device.py index 09c9a6d4..4bcf07b8 100644 --- a/helper/helper/device.py +++ b/helper/helper/device.py @@ -413,9 +413,9 @@ class ConnectionNode(RpcNode): or ( # SmartCardConnection can be used over NFC, or on 5.3 and later. isinstance(self._connection, SmartCardConnection) and ( - self._transport == TRANSPORT.NFC or - self._info.version >= (5, 3, 0) or - self._info.version[0] == 3 + self._transport == TRANSPORT.NFC + or self._info.version >= (5, 3, 0) + or self._info.version[0] == 3 ) ) ) From a7f6284dd7a5825281d118ca9038f71d11b898f7 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Thu, 14 Dec 2023 16:55:57 +0100 Subject: [PATCH 20/27] Add missing localizations. --- lib/fido/views/reset_dialog.dart | 2 +- lib/l10n/app_de.arb | 6 ++++++ lib/l10n/app_en.arb | 6 ++++++ lib/l10n/app_fr.arb | 6 ++++++ lib/l10n/app_ja.arb | 6 ++++++ lib/l10n/app_pl.arb | 6 ++++++ lib/otp/views/configure_yubiotp_dialog.dart | 10 +++++----- 7 files changed, 36 insertions(+), 6 deletions(-) diff --git a/lib/fido/views/reset_dialog.dart b/lib/fido/views/reset_dialog.dart index 034db399..9b27f687 100755 --- a/lib/fido/views/reset_dialog.dart +++ b/lib/fido/views/reset_dialog.dart @@ -126,7 +126,7 @@ class _ResetDialogState extends ConsumerState { l10n.p_warning_disable_accounts, ), if (_currentStep > -1) ...[ - Text('Status: ${_getMessage()}'), + Text('${l10n.s_status}: ${_getMessage()}'), LinearProgressIndicator(value: progress) ], ] diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 1c8552bf..ee786cdc 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -29,6 +29,7 @@ "s_close": "Schließen", "s_delete": "Löschen", "s_quit": "Beenden", + "s_status": null, "s_unlock": "Entsperren", "s_calculate": "Berechnen", "s_import": null, @@ -538,6 +539,11 @@ "s_generate_private_id": null, "s_generate_secret_key": null, "s_generate_passowrd": null, + "l_select_file": null, + "l_no_export_file": null, + "s_no_export": null, + "s_export": null, + "l_export_configuration_file": null, "@_otp_slot_actions": {}, "s_delete_slot": null, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7e705d8b..9d63a036 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -29,6 +29,7 @@ "s_close": "Close", "s_delete": "Delete", "s_quit": "Quit", + "s_status": "Status", "s_unlock": "Unlock", "s_calculate": "Calculate", "s_import": "Import", @@ -538,6 +539,11 @@ "s_generate_private_id": "Generate private ID", "s_generate_secret_key": "Generate secret key", "s_generate_passowrd": "Generate password", + "l_select_file": "Select file", + "l_no_export_file": "No export file", + "s_no_export": "No export", + "s_export": "Export", + "l_export_configuration_file": "Export configuration to file", "@_otp_slot_actions": {}, "s_delete_slot": "Delete credential", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 93ce0e73..fb0439d5 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -29,6 +29,7 @@ "s_close": "Fermer", "s_delete": "Supprimer", "s_quit": "Quitter", + "s_status": null, "s_unlock": "Déverrouiller", "s_calculate": "Calculer", "s_import": "Importer", @@ -538,6 +539,11 @@ "s_generate_private_id": null, "s_generate_secret_key": null, "s_generate_passowrd": null, + "l_select_file": null, + "l_no_export_file": null, + "s_no_export": null, + "s_export": null, + "l_export_configuration_file": null, "@_otp_slot_actions": {}, "s_delete_slot": null, diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 255bdf92..53595019 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -29,6 +29,7 @@ "s_close": "閉じる", "s_delete": "消去", "s_quit": "終了", + "s_status": null, "s_unlock": "ロック解除", "s_calculate": "計算", "s_import": "インポート", @@ -538,6 +539,11 @@ "s_generate_private_id": null, "s_generate_secret_key": null, "s_generate_passowrd": null, + "l_select_file": null, + "l_no_export_file": null, + "s_no_export": null, + "s_export": null, + "l_export_configuration_file": null, "@_otp_slot_actions": {}, "s_delete_slot": null, diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 53d810c1..44cd4c22 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -29,6 +29,7 @@ "s_close": "Zamknij", "s_delete": "Usuń", "s_quit": "Wyjdź", + "s_status": null, "s_unlock": "Odblokuj", "s_calculate": "Oblicz", "s_import": "Importuj", @@ -538,6 +539,11 @@ "s_generate_private_id": null, "s_generate_secret_key": null, "s_generate_passowrd": null, + "l_select_file": null, + "l_no_export_file": null, + "s_no_export": null, + "s_export": null, + "l_export_configuration_file": null, "@_otp_slot_actions": {}, "s_delete_slot": null, diff --git a/lib/otp/views/configure_yubiotp_dialog.dart b/lib/otp/views/configure_yubiotp_dialog.dart index 4c089323..f294a38d 100644 --- a/lib/otp/views/configure_yubiotp_dialog.dart +++ b/lib/otp/views/configure_yubiotp_dialog.dart @@ -46,8 +46,8 @@ enum OutputActions { const OutputActions(); String getDisplayName(AppLocalizations l10n) => switch (this) { - OutputActions.selectFile => 'Select file', - OutputActions.noOutput => 'No export file' + OutputActions.selectFile => l10n.l_select_file, + OutputActions.noOutput => l10n.l_no_export_file }; } @@ -108,7 +108,7 @@ class _ConfigureYubiOtpDialogState Future selectFile() async { final filePath = await FilePicker.platform.saveFile( - dialogTitle: 'Export configuration to file', + dialogTitle: l10n.l_export_configuration_file, allowedExtensions: ['csv'], type: FileType.custom, lockParentWindow: true); @@ -323,7 +323,7 @@ class _ConfigureYubiOtpDialogState }, ), ChoiceFilterChip( - tooltip: outputFile?.path ?? 'No export', + tooltip: outputFile?.path ?? l10n.s_no_export, selected: outputFile != null, avatar: outputFile != null ? Icon(Icons.check, @@ -338,7 +338,7 @@ class _ConfigureYubiOtpDialogState constraints: const BoxConstraints(maxWidth: 140), child: Text( fileName != null - ? 'Export $fileName' + ? '${l10n.s_export} $fileName' : _action.getDisplayName(l10n), overflow: TextOverflow.ellipsis, ), From 7ea025f9f0380428609fd7042f0191487b7134cd Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Thu, 14 Dec 2023 17:21:46 +0100 Subject: [PATCH 21/27] Add `AppInputDecoration` to `AppTextFormField`. --- lib/fido/views/add_fingerprint_dialog.dart | 3 +- lib/fido/views/pin_dialog.dart | 113 +++++++----------- lib/fido/views/rename_fingerprint_dialog.dart | 3 +- lib/oath/views/oath_screen.dart | 3 +- lib/oath/views/rename_account_dialog.dart | 5 +- lib/piv/views/manage_key_dialog.dart | 46 +++---- lib/widgets/app_text_form_field.dart | 6 +- 7 files changed, 73 insertions(+), 106 deletions(-) diff --git a/lib/fido/views/add_fingerprint_dialog.dart b/lib/fido/views/add_fingerprint_dialog.dart index 2b1a556e..bcf82110 100755 --- a/lib/fido/views/add_fingerprint_dialog.dart +++ b/lib/fido/views/add_fingerprint_dialog.dart @@ -28,6 +28,7 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../desktop/models.dart'; import '../../fido/models.dart'; +import '../../widgets/app_text_field.dart'; import '../../widgets/app_text_form_field.dart'; import '../../widgets/responsive_dialog.dart'; import '../../widgets/utf8_utils.dart'; @@ -206,7 +207,7 @@ class _AddFingerprintDialogState extends ConsumerState inputFormatters: [limitBytesLength(15)], buildCounter: buildByteCounterFor(_label), autofocus: true, - decoration: InputDecoration( + decoration: AppInputDecoration( enabled: _fingerprint != null, border: const OutlineInputBorder(), labelText: l10n.s_name, diff --git a/lib/fido/views/pin_dialog.dart b/lib/fido/views/pin_dialog.dart index 33fd37f5..88617421 100755 --- a/lib/fido/views/pin_dialog.dart +++ b/lib/fido/views/pin_dialog.dart @@ -24,6 +24,7 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; import '../../desktop/models.dart'; +import '../../widgets/app_text_field.dart'; import '../../widgets/app_text_form_field.dart'; import '../../widgets/responsive_dialog.dart'; import '../models.dart'; @@ -81,36 +82,25 @@ class _FidoPinDialogState extends ConsumerState { autofocus: true, obscureText: _isObscureCurrent, autofillHints: const [AutofillHints.password], - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_current_pin, - errorText: _currentIsWrong ? _currentPinError : null, - errorMaxLines: 3, - prefixIcon: const Icon(Icons.pin_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon(_isObscureCurrent - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - setState(() { - _isObscureCurrent = !_isObscureCurrent; - }); - }, - tooltip: _isObscureCurrent - ? l10n.s_show_pin - : l10n.s_hide_pin, - ), - if (_currentIsWrong) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], - )), + decoration: AppInputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_current_pin, + errorText: _currentIsWrong ? _currentPinError : null, + errorMaxLines: 3, + prefixIcon: const Icon(Icons.pin_outlined), + suffixIcon: IconButton( + icon: Icon(_isObscureCurrent + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureCurrent = !_isObscureCurrent; + }); + }, + tooltip: + _isObscureCurrent ? l10n.s_show_pin : l10n.s_hide_pin, + ), + ), onChanged: (value) { setState(() { _currentIsWrong = false; @@ -126,35 +116,23 @@ class _FidoPinDialogState extends ConsumerState { autofocus: !hasPin, obscureText: _isObscureNew, autofillHints: const [AutofillHints.password], - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_new_pin, enabled: !hasPin || _currentPin.isNotEmpty, errorText: _newIsWrong ? _newPinError : null, errorMaxLines: 3, prefixIcon: const Icon(Icons.pin_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon(_isObscureNew - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - setState(() { - _isObscureNew = !_isObscureNew; - }); - }, - tooltip: - _isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin, - ), - if (_newIsWrong) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ]), + suffixIcon: IconButton( + icon: Icon( + _isObscureNew ? Icons.visibility : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureNew = !_isObscureNew; + }); + }, + tooltip: _isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin, + ), ), onChanged: (value) { setState(() { @@ -167,26 +145,21 @@ class _FidoPinDialogState extends ConsumerState { initialValue: _confirmPin, obscureText: _isObscureConfirm, autofillHints: const [AutofillHints.password], - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_confirm_pin, prefixIcon: const Icon(Icons.pin_outlined), - suffixIcon: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon(_isObscureConfirm - ? Icons.visibility - : Icons.visibility_off), - onPressed: () { - setState(() { - _isObscureConfirm = !_isObscureConfirm; - }); - }, - tooltip: - _isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin, - ) - ], + suffixIcon: IconButton( + icon: Icon(_isObscureConfirm + ? Icons.visibility + : Icons.visibility_off), + onPressed: () { + setState(() { + _isObscureConfirm = !_isObscureConfirm; + }); + }, + tooltip: + _isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin, ), enabled: (!hasPin || _currentPin.isNotEmpty) && _newPin.isNotEmpty, diff --git a/lib/fido/views/rename_fingerprint_dialog.dart b/lib/fido/views/rename_fingerprint_dialog.dart index aead748c..240b1db5 100755 --- a/lib/fido/views/rename_fingerprint_dialog.dart +++ b/lib/fido/views/rename_fingerprint_dialog.dart @@ -21,6 +21,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../desktop/models.dart'; +import '../../widgets/app_text_field.dart'; import '../../widgets/app_text_form_field.dart'; import '../../widgets/responsive_dialog.dart'; import '../../widgets/utf8_utils.dart'; @@ -95,7 +96,7 @@ class _RenameAccountDialogState extends ConsumerState { maxLength: 15, inputFormatters: [limitBytesLength(15)], buildCounter: buildByteCounterFor(_label), - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_label, prefixIcon: const Icon(Icons.fingerprint_outlined), diff --git a/lib/oath/views/oath_screen.dart b/lib/oath/views/oath_screen.dart index 26efcccb..197521f9 100755 --- a/lib/oath/views/oath_screen.dart +++ b/lib/oath/views/oath_screen.dart @@ -26,6 +26,7 @@ import '../../app/views/app_page.dart'; import '../../app/views/graphics.dart'; import '../../app/views/message_page.dart'; import '../../core/state.dart'; +import '../../widgets/app_text_field.dart'; import '../../widgets/app_text_form_field.dart'; import '../features.dart' as features; import '../keys.dart' as keys; @@ -161,7 +162,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> { // Use the default style, but with a smaller font size: style: textTheme.titleMedium ?.copyWith(fontSize: textTheme.titleSmall?.fontSize), - decoration: InputDecoration( + decoration: AppInputDecoration( hintText: l10n.s_search_accounts, border: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(32)), diff --git a/lib/oath/views/rename_account_dialog.dart b/lib/oath/views/rename_account_dialog.dart index 3dfc236c..0d773f87 100755 --- a/lib/oath/views/rename_account_dialog.dart +++ b/lib/oath/views/rename_account_dialog.dart @@ -25,6 +25,7 @@ import '../../app/models.dart'; import '../../app/state.dart'; import '../../desktop/models.dart'; import '../../exception/cancellation_exception.dart'; +import '../../widgets/app_text_field.dart'; import '../../widgets/app_text_form_field.dart'; import '../../widgets/focus_utils.dart'; import '../../widgets/responsive_dialog.dart'; @@ -179,7 +180,7 @@ class _RenameAccountDialogState extends ConsumerState { buildCounter: buildByteCounterFor(_issuer), inputFormatters: [limitBytesLength(issuerRemaining)], key: keys.issuerField, - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_issuer_optional, helperText: '', // Prevents dialog resizing when disabled @@ -198,7 +199,7 @@ class _RenameAccountDialogState extends ConsumerState { inputFormatters: [limitBytesLength(nameRemaining)], buildCounter: buildByteCounterFor(_name), key: keys.nameField, - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_account_name, helperText: '', // Prevents dialog resizing when disabled diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index 53cf57df..dff55966 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -210,7 +210,7 @@ class _ManageKeyDialogState extends ConsumerState { controller: _currentController, readOnly: _defaultKeyUsed, maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null, - decoration: InputDecoration( + decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_current_management_key, helperText: _defaultKeyUsed ? l10n.l_default_key_used : null, @@ -229,34 +229,22 @@ class _ManageKeyDialogState extends ConsumerState { : (_hasMetadata && _currentIsWrong || _currentInvalidFormat) ? const Icon(Icons.error) - : Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - icon: Icon(_defaultKeyUsed - ? Icons.auto_awesome - : Icons.auto_awesome_outlined), - tooltip: l10n.s_use_default, - onPressed: () { - setState(() { - _defaultKeyUsed = !_defaultKeyUsed; - if (_defaultKeyUsed) { - _currentController.text = - defaultManagementKey; - } else { - _currentController.clear(); - } - }); - }, - ), - if (_currentIsWrong || - _currentInvalidFormat) ...[ - const Icon(Icons.error_outlined), - const SizedBox( - width: 8.0, - ) - ] - ], + : IconButton( + icon: Icon(_defaultKeyUsed + ? Icons.auto_awesome + : Icons.auto_awesome_outlined), + tooltip: l10n.s_use_default, + onPressed: () { + setState(() { + _defaultKeyUsed = !_defaultKeyUsed; + if (_defaultKeyUsed) { + _currentController.text = + defaultManagementKey; + } else { + _currentController.clear(); + } + }); + }, ), ), textInputAction: TextInputAction.next, diff --git a/lib/widgets/app_text_form_field.dart b/lib/widgets/app_text_form_field.dart index ea9020df..7e4b395b 100644 --- a/lib/widgets/app_text_form_field.dart +++ b/lib/widgets/app_text_form_field.dart @@ -16,6 +16,8 @@ import 'package:flutter/material.dart'; +import 'app_text_field.dart'; + /// TextFormField without autocorrect and suggestions class AppTextFormField extends TextFormField { AppTextFormField({ @@ -28,7 +30,7 @@ class AppTextFormField extends TextFormField { super.controller, super.initialValue, super.focusNode, - super.decoration, + AppInputDecoration? decoration, super.textCapitalization, super.textInputAction, super.style, @@ -87,5 +89,5 @@ class AppTextFormField extends TextFormField { super.clipBehavior, super.scribbleEnabled, super.canRequestFocus, - }); + }) : super(decoration: decoration); } From e607d0a1c017d9bd2052fcdac75e7ff16f45f0a6 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Thu, 14 Dec 2023 17:40:29 +0100 Subject: [PATCH 22/27] Put `AppInputDecoration` in separate file. --- lib/fido/views/add_fingerprint_dialog.dart | 2 +- lib/fido/views/locked_page.dart | 1 + lib/fido/views/pin_dialog.dart | 2 +- lib/fido/views/rename_fingerprint_dialog.dart | 2 +- lib/oath/views/add_account_page.dart | 1 + lib/oath/views/manage_password_dialog.dart | 1 + lib/oath/views/oath_screen.dart | 2 +- lib/oath/views/rename_account_dialog.dart | 2 +- lib/oath/views/unlock_form.dart | 1 + lib/otp/views/configure_chalresp_dialog.dart | 1 + lib/otp/views/configure_hotp_dialog.dart | 1 + lib/otp/views/configure_static_dialog.dart | 1 + lib/otp/views/configure_yubiotp_dialog.dart | 1 + lib/piv/views/authentication_dialog.dart | 1 + lib/piv/views/generate_key_dialog.dart | 1 + lib/piv/views/import_file_dialog.dart | 1 + lib/piv/views/manage_key_dialog.dart | 1 + lib/piv/views/manage_pin_puk_dialog.dart | 1 + lib/piv/views/pin_dialog.dart | 1 + lib/widgets/app_input_decoration.dart | 86 +++++++++++++++++++ lib/widgets/app_text_field.dart | 85 +----------------- lib/widgets/app_text_form_field.dart | 2 +- 22 files changed, 107 insertions(+), 90 deletions(-) create mode 100644 lib/widgets/app_input_decoration.dart diff --git a/lib/fido/views/add_fingerprint_dialog.dart b/lib/fido/views/add_fingerprint_dialog.dart index bcf82110..26625fd9 100755 --- a/lib/fido/views/add_fingerprint_dialog.dart +++ b/lib/fido/views/add_fingerprint_dialog.dart @@ -28,7 +28,7 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../desktop/models.dart'; import '../../fido/models.dart'; -import '../../widgets/app_text_field.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_form_field.dart'; import '../../widgets/responsive_dialog.dart'; import '../../widgets/utf8_utils.dart'; diff --git a/lib/fido/views/locked_page.dart b/lib/fido/views/locked_page.dart index 204b5b9d..5ea1488e 100755 --- a/lib/fido/views/locked_page.dart +++ b/lib/fido/views/locked_page.dart @@ -23,6 +23,7 @@ import '../../app/views/app_page.dart'; import '../../app/views/graphics.dart'; import '../../app/views/message_page.dart'; import '../../core/state.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../features.dart' as features; import '../models.dart'; diff --git a/lib/fido/views/pin_dialog.dart b/lib/fido/views/pin_dialog.dart index 88617421..172d02a7 100755 --- a/lib/fido/views/pin_dialog.dart +++ b/lib/fido/views/pin_dialog.dart @@ -24,7 +24,7 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; import '../../desktop/models.dart'; -import '../../widgets/app_text_field.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_form_field.dart'; import '../../widgets/responsive_dialog.dart'; import '../models.dart'; diff --git a/lib/fido/views/rename_fingerprint_dialog.dart b/lib/fido/views/rename_fingerprint_dialog.dart index 240b1db5..60587b20 100755 --- a/lib/fido/views/rename_fingerprint_dialog.dart +++ b/lib/fido/views/rename_fingerprint_dialog.dart @@ -21,7 +21,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../desktop/models.dart'; -import '../../widgets/app_text_field.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_form_field.dart'; import '../../widgets/responsive_dialog.dart'; import '../../widgets/utf8_utils.dart'; diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index f73ae139..2b677b2d 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -35,6 +35,7 @@ import '../../desktop/models.dart'; import '../../exception/apdu_exception.dart'; import '../../exception/cancellation_exception.dart'; import '../../management/models.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/choice_filter_chip.dart'; import '../../widgets/file_drop_target.dart'; diff --git a/lib/oath/views/manage_password_dialog.dart b/lib/oath/views/manage_password_dialog.dart index d9d568b8..0a4a579e 100755 --- a/lib/oath/views/manage_password_dialog.dart +++ b/lib/oath/views/manage_password_dialog.dart @@ -20,6 +20,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/focus_utils.dart'; import '../../widgets/responsive_dialog.dart'; diff --git a/lib/oath/views/oath_screen.dart b/lib/oath/views/oath_screen.dart index 197521f9..acedf30d 100755 --- a/lib/oath/views/oath_screen.dart +++ b/lib/oath/views/oath_screen.dart @@ -26,7 +26,7 @@ import '../../app/views/app_page.dart'; import '../../app/views/graphics.dart'; import '../../app/views/message_page.dart'; import '../../core/state.dart'; -import '../../widgets/app_text_field.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_form_field.dart'; import '../features.dart' as features; import '../keys.dart' as keys; diff --git a/lib/oath/views/rename_account_dialog.dart b/lib/oath/views/rename_account_dialog.dart index 0d773f87..88983228 100755 --- a/lib/oath/views/rename_account_dialog.dart +++ b/lib/oath/views/rename_account_dialog.dart @@ -25,7 +25,7 @@ import '../../app/models.dart'; import '../../app/state.dart'; import '../../desktop/models.dart'; import '../../exception/cancellation_exception.dart'; -import '../../widgets/app_text_field.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_form_field.dart'; import '../../widgets/focus_utils.dart'; import '../../widgets/responsive_dialog.dart'; diff --git a/lib/oath/views/unlock_form.dart b/lib/oath/views/unlock_form.dart index 4bfdb69e..b395c533 100755 --- a/lib/oath/views/unlock_form.dart +++ b/lib/oath/views/unlock_form.dart @@ -20,6 +20,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../keys.dart' as keys; import '../models.dart'; diff --git a/lib/otp/views/configure_chalresp_dialog.dart b/lib/otp/views/configure_chalresp_dialog.dart index 2e5421c4..d75d5325 100644 --- a/lib/otp/views/configure_chalresp_dialog.dart +++ b/lib/otp/views/configure_chalresp_dialog.dart @@ -27,6 +27,7 @@ import '../../app/models.dart'; import '../../app/state.dart'; import '../../core/models.dart'; import '../../core/state.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/responsive_dialog.dart'; import '../keys.dart' as keys; diff --git a/lib/otp/views/configure_hotp_dialog.dart b/lib/otp/views/configure_hotp_dialog.dart index 8aea8eb2..4ef1c3b5 100644 --- a/lib/otp/views/configure_hotp_dialog.dart +++ b/lib/otp/views/configure_hotp_dialog.dart @@ -26,6 +26,7 @@ import '../../app/state.dart'; import '../../core/models.dart'; import '../../core/state.dart'; import '../../oath/models.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/choice_filter_chip.dart'; import '../../widgets/responsive_dialog.dart'; diff --git a/lib/otp/views/configure_static_dialog.dart b/lib/otp/views/configure_static_dialog.dart index 6395c76d..6c383caa 100644 --- a/lib/otp/views/configure_static_dialog.dart +++ b/lib/otp/views/configure_static_dialog.dart @@ -24,6 +24,7 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; import '../../core/state.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/choice_filter_chip.dart'; import '../../widgets/responsive_dialog.dart'; diff --git a/lib/otp/views/configure_yubiotp_dialog.dart b/lib/otp/views/configure_yubiotp_dialog.dart index f294a38d..a3b4222b 100644 --- a/lib/otp/views/configure_yubiotp_dialog.dart +++ b/lib/otp/views/configure_yubiotp_dialog.dart @@ -29,6 +29,7 @@ import '../../app/models.dart'; import '../../app/state.dart'; import '../../core/models.dart'; import '../../core/state.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/choice_filter_chip.dart'; import '../../widgets/responsive_dialog.dart'; diff --git a/lib/piv/views/authentication_dialog.dart b/lib/piv/views/authentication_dialog.dart index 8ad9304a..204497a7 100644 --- a/lib/piv/views/authentication_dialog.dart +++ b/lib/piv/views/authentication_dialog.dart @@ -21,6 +21,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/models.dart'; import '../../core/models.dart'; import '../../exception/cancellation_exception.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/responsive_dialog.dart'; import '../keys.dart' as keys; diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart index 82c313ca..ea470803 100644 --- a/lib/piv/views/generate_key_dialog.dart +++ b/lib/piv/views/generate_key_dialog.dart @@ -22,6 +22,7 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; import '../../core/models.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/choice_filter_chip.dart'; import '../../widgets/responsive_dialog.dart'; diff --git a/lib/piv/views/import_file_dialog.dart b/lib/piv/views/import_file_dialog.dart index 4dd52468..2954f499 100644 --- a/lib/piv/views/import_file_dialog.dart +++ b/lib/piv/views/import_file_dialog.dart @@ -23,6 +23,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/responsive_dialog.dart'; import '../keys.dart' as keys; diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index dff55966..2a1a5947 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -24,6 +24,7 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; import '../../core/models.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/app_text_form_field.dart'; import '../../widgets/choice_filter_chip.dart'; diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart index f0cae705..d25571ad 100644 --- a/lib/piv/views/manage_pin_puk_dialog.dart +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -20,6 +20,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/responsive_dialog.dart'; import '../keys.dart' as keys; diff --git a/lib/piv/views/pin_dialog.dart b/lib/piv/views/pin_dialog.dart index 1b5f6a25..0181121c 100644 --- a/lib/piv/views/pin_dialog.dart +++ b/lib/piv/views/pin_dialog.dart @@ -20,6 +20,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/models.dart'; import '../../exception/cancellation_exception.dart'; +import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/responsive_dialog.dart'; import '../keys.dart' as keys; diff --git a/lib/widgets/app_input_decoration.dart b/lib/widgets/app_input_decoration.dart new file mode 100644 index 00000000..df7c8c51 --- /dev/null +++ b/lib/widgets/app_input_decoration.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +class AppInputDecoration extends InputDecoration { + final List? suffixIcons; + + const AppInputDecoration({ + // allow multiple suffixIcons + this.suffixIcons, + // forward other TextField parameters + super.icon, + super.iconColor, + super.label, + super.labelText, + super.labelStyle, + super.floatingLabelStyle, + super.helperText, + super.helperStyle, + super.helperMaxLines, + super.hintText, + super.hintStyle, + super.hintTextDirection, + super.hintMaxLines, + super.hintFadeDuration, + super.error, + super.errorText, + super.errorStyle, + super.errorMaxLines, + super.floatingLabelBehavior, + super.floatingLabelAlignment, + super.isCollapsed, + super.isDense, + super.contentPadding, + super.prefixIcon, + super.prefixIconConstraints, + super.prefix, + super.prefixText, + super.prefixStyle, + super.prefixIconColor, + super.suffixIcon, + super.suffix, + super.suffixText, + super.suffixStyle, + super.suffixIconColor, + super.suffixIconConstraints, + super.counter, + super.counterText, + super.counterStyle, + super.filled, + super.fillColor, + super.focusColor, + super.hoverColor, + super.errorBorder, + super.focusedBorder, + super.focusedErrorBorder, + super.disabledBorder, + super.enabledBorder, + super.border, + super.enabled = true, + super.semanticCounterText, + super.alignLabelWithHint, + super.constraints, + }) : assert(!(suffixIcon != null && suffixIcons != null), + 'Declaring both suffixIcon and suffixIcons is not supported.'); + + @override + Widget? get suffixIcon { + final hasError = errorText != null; + + if (hasError || suffixIcons != null) { + final errorIcon = hasError ? const Icon(Icons.error_outlined) : null; + + final existingSuffixIcon = super.suffixIcon; + + return Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (suffixIcons != null) ...suffixIcons!, + if (existingSuffixIcon != null) existingSuffixIcon, + if (errorIcon != null) ...[errorIcon, const SizedBox(width: 8.0)], + ], + ); + } + + return super.suffixIcon; + } +} diff --git a/lib/widgets/app_text_field.dart b/lib/widgets/app_text_field.dart index bf395747..ff0c49e3 100644 --- a/lib/widgets/app_text_field.dart +++ b/lib/widgets/app_text_field.dart @@ -16,90 +16,7 @@ import 'package:flutter/material.dart'; -class AppInputDecoration extends InputDecoration { - final List? suffixIcons; - - const AppInputDecoration({ - // allow multiple suffixIcons - this.suffixIcons, - // forward other TextField parameters - super.icon, - super.iconColor, - super.label, - super.labelText, - super.labelStyle, - super.floatingLabelStyle, - super.helperText, - super.helperStyle, - super.helperMaxLines, - super.hintText, - super.hintStyle, - super.hintTextDirection, - super.hintMaxLines, - super.hintFadeDuration, - super.error, - super.errorText, - super.errorStyle, - super.errorMaxLines, - super.floatingLabelBehavior, - super.floatingLabelAlignment, - super.isCollapsed, - super.isDense, - super.contentPadding, - super.prefixIcon, - super.prefixIconConstraints, - super.prefix, - super.prefixText, - super.prefixStyle, - super.prefixIconColor, - super.suffixIcon, - super.suffix, - super.suffixText, - super.suffixStyle, - super.suffixIconColor, - super.suffixIconConstraints, - super.counter, - super.counterText, - super.counterStyle, - super.filled, - super.fillColor, - super.focusColor, - super.hoverColor, - super.errorBorder, - super.focusedBorder, - super.focusedErrorBorder, - super.disabledBorder, - super.enabledBorder, - super.border, - super.enabled = true, - super.semanticCounterText, - super.alignLabelWithHint, - super.constraints, - }) : assert(!(suffixIcon != null && suffixIcons != null), - 'Declaring both suffixIcon and suffixIcons is not supported.'); - - @override - Widget? get suffixIcon { - final hasError = errorText != null; - - if (hasError || suffixIcons != null) { - final errorIcon = hasError ? const Icon(Icons.error_outlined) : null; - - final existingSuffixIcon = super.suffixIcon; - - return Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - if (suffixIcons != null) ...suffixIcons!, - if (existingSuffixIcon != null) existingSuffixIcon, - if (errorIcon != null) ...[errorIcon, const SizedBox(width: 8.0)], - ], - ); - } - - return super.suffixIcon; - } -} +import 'app_input_decoration.dart'; /// TextField without autocorrect and suggestions class AppTextField extends TextField { diff --git a/lib/widgets/app_text_form_field.dart b/lib/widgets/app_text_form_field.dart index 7e4b395b..61e0540b 100644 --- a/lib/widgets/app_text_form_field.dart +++ b/lib/widgets/app_text_form_field.dart @@ -16,7 +16,7 @@ import 'package:flutter/material.dart'; -import 'app_text_field.dart'; +import 'app_input_decoration.dart'; /// TextFormField without autocorrect and suggestions class AppTextFormField extends TextFormField { From 2b8c5259ba1bff14993e9cc3992523848f0a25fc Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Thu, 14 Dec 2023 18:31:58 +0100 Subject: [PATCH 23/27] Add yubiotp interface fallback mechanism. --- lib/desktop/otp/state.dart | 54 ++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/lib/desktop/otp/state.dart b/lib/desktop/otp/state.dart index 586e070c..82ca8d46 100644 --- a/lib/desktop/otp/state.dart +++ b/lib/desktop/otp/state.dart @@ -22,6 +22,7 @@ import 'package:logging/logging.dart'; import '../../app/logging.dart'; import '../../app/models.dart'; +import '../../core/models.dart'; import '../../otp/models.dart'; import '../../otp/state.dart'; import '../rpc.dart'; @@ -31,8 +32,8 @@ final _log = Logger('desktop.otp.state'); final _sessionProvider = Provider.autoDispose.family( - (ref, devicePath) => RpcNodeSession( - ref.watch(rpcProvider).requireValue, devicePath, ['ccid', 'yubiotp']), + (ref, devicePath) => + RpcNodeSession(ref.watch(rpcProvider).requireValue, devicePath, []), ); final desktopOtpState = AsyncNotifierProvider.autoDispose @@ -41,6 +42,7 @@ final desktopOtpState = AsyncNotifierProvider.autoDispose class _DesktopOtpStateNotifier extends OtpStateNotifier { late RpcNodeSession _session; + List _subpath = []; @override FutureOr build(DevicePath devicePath) async { @@ -53,33 +55,49 @@ class _DesktopOtpStateNotifier extends OtpStateNotifier { }); final result = await _session.command('get'); - _log.debug('application status', jsonEncode(result)); - return OtpState.fromJson(result['data']); + final interfaces = (result['children'] as Map).keys.toSet(); + + // Will try to connect over ccid first + for (final iface in [UsbInterface.otp, UsbInterface.ccid]) { + if (interfaces.contains(iface.name)) { + final path = [iface.name, 'yubiotp']; + try { + final otpStateResult = await _session.command('get', target: path); + _subpath = path; + _log.debug('Using transport $iface for yubiotp'); + _log.debug('application status', jsonEncode(result)); + return OtpState.fromJson(otpStateResult['data']); + } catch (e) { + _log.warning('Failed connecting to yubiotp via $iface'); + } + } + } + throw 'Failed connecting over ${UsbInterface.ccid.name} and ${UsbInterface.otp.name}'; } @override Future swapSlots() async { - await _session.command('swap'); + await _session.command('swap', target: _subpath); ref.invalidate(_sessionProvider(_session.devicePath)); } @override Future generateStaticPassword(int length, String layout) async { final result = await _session.command('generate_static', - params: {'length': length, 'layout': layout}); + target: _subpath, params: {'length': length, 'layout': layout}); return result['password']; } @override Future modhexEncodeSerial(int serial) async { - final result = - await _session.command('serial_modhex', params: {'serial': serial}); + final result = await _session + .command('serial_modhex', target: _subpath, params: {'serial': serial}); return result['encoded']; } @override Future>> getKeyboardLayouts() async { - final result = await _session.command('keyboard_layouts'); + final result = await _session.command('keyboard_layouts', target: _subpath); return Map>.from(result.map((key, value) => MapEntry(key, (value as List).cast().toList()))); } @@ -87,18 +105,20 @@ class _DesktopOtpStateNotifier extends OtpStateNotifier { @override Future formatYubiOtpCsv( int serial, String publicId, String privateId, String key) async { - final result = await _session.command('format_yubiotp_csv', params: { - 'serial': serial, - 'public_id': publicId, - 'private_id': privateId, - 'key': key - }); + final result = await _session.command('format_yubiotp_csv', + target: _subpath, + params: { + 'serial': serial, + 'public_id': publicId, + 'private_id': privateId, + 'key': key + }); return result['csv']; } @override Future deleteSlot(SlotId slot) async { - await _session.command('delete', target: [slot.id]); + await _session.command('delete', target: [..._subpath, slot.id]); ref.invalidateSelf(); } @@ -106,7 +126,7 @@ class _DesktopOtpStateNotifier extends OtpStateNotifier { Future configureSlot(SlotId slot, {required SlotConfiguration configuration}) async { await _session.command('put', - target: [slot.id], params: configuration.toJson()); + target: [..._subpath, slot.id], params: configuration.toJson()); ref.invalidateSelf(); } } From 868c96b72bc00fcd0499e395c2f099bb950c410c Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Fri, 15 Dec 2023 08:39:11 +0100 Subject: [PATCH 24/27] Add feature per OTP configuration type. --- lib/otp/features.dart | 6 +- lib/otp/models.dart | 2 - lib/otp/views/actions.dart | 133 +++++++++++++++++++++---------------- 3 files changed, 80 insertions(+), 61 deletions(-) diff --git a/lib/otp/features.dart b/lib/otp/features.dart index b63fa3bd..353533d1 100644 --- a/lib/otp/features.dart +++ b/lib/otp/features.dart @@ -22,5 +22,9 @@ final actionsSwap = actions.feature('swap'); final slots = otp.feature('slots'); -final slotsConfigure = slots.feature('configure'); +final slotsConfigureChalResp = slots.feature('configureChalResp'); +final slotsConfigureHotp = slots.feature('configureHotp'); +final slotsConfigureStatic = slots.feature('configureSlots'); +final slotsConfigureYubiOtp = slots.feature('configureYubiOtp'); + final slotsDelete = slots.feature('delete'); diff --git a/lib/otp/models.dart b/lib/otp/models.dart index d28d8766..63491d1f 100644 --- a/lib/otp/models.dart +++ b/lib/otp/models.dart @@ -39,8 +39,6 @@ enum SlotId { SlotId.values.firstWhere((e) => e.id == value); } -enum SlotConfigurationType { yubiotp, static, hotp, chalresp } - @freezed class OtpState with _$OtpState { const OtpState._(); diff --git a/lib/otp/views/actions.dart b/lib/otp/views/actions.dart index 9c60d4b6..a96d65cc 100644 --- a/lib/otp/views/actions.dart +++ b/lib/otp/views/actions.dart @@ -33,10 +33,20 @@ import 'configure_static_dialog.dart'; import 'configure_yubiotp_dialog.dart'; import 'delete_slot_dialog.dart'; -class ConfigureIntent extends Intent { - const ConfigureIntent({required this.configurationType}); +class ConfigureChalRespIntent extends Intent { + const ConfigureChalRespIntent(); +} - final SlotConfigurationType configurationType; +class ConfigureHotpIntent extends Intent { + const ConfigureHotpIntent(); +} + +class ConfigureStaticIntent extends Intent { + const ConfigureStaticIntent(); +} + +class ConfigureYubiOtpIntent extends Intent { + const ConfigureYubiOtpIntent(); } Widget registerOtpActions( @@ -49,48 +59,58 @@ Widget registerOtpActions( final hasFeature = ref.watch(featureProvider); return Actions( actions: { - if (hasFeature(features.slotsConfigure)) - ConfigureIntent: - CallbackAction(onInvoke: (intent) async { + if (hasFeature(features.slotsConfigureChalResp)) + ConfigureChalRespIntent: + CallbackAction(onInvoke: (intent) async { final withContext = ref.read(withContextProvider); - final configurationType = intent.configurationType; - switch (configurationType) { - case SlotConfigurationType.chalresp: - await withContext((context) async { - await showBlurDialog( - context: context, - builder: (context) => - ConfigureChalrespDialog(devicePath, otpSlot)); - }); - case SlotConfigurationType.hotp: - await withContext((context) async { - await showBlurDialog( - context: context, - builder: (context) => - ConfigureHotpDialog(devicePath, otpSlot)); - }); - case SlotConfigurationType.static: - final keyboardLayouts = await ref - .read(otpStateProvider(devicePath).notifier) - .getKeyboardLayouts(); - await withContext((context) async { - await showBlurDialog( - context: context, - builder: (context) => ConfigureStaticDialog( - devicePath, otpSlot, keyboardLayouts)); - }); - case SlotConfigurationType.yubiotp: - await withContext((context) async { - await showBlurDialog( - context: context, - builder: (context) => - ConfigureYubiOtpDialog(devicePath, otpSlot)); - }); - default: - break; - } + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => + ConfigureChalrespDialog(devicePath, otpSlot)); + }); + return null; + }), + if (hasFeature(features.slotsConfigureHotp)) + ConfigureHotpIntent: + CallbackAction(onInvoke: (intent) async { + final withContext = ref.read(withContextProvider); + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => ConfigureHotpDialog(devicePath, otpSlot)); + }); + return null; + }), + if (hasFeature(features.slotsConfigureStatic)) + ConfigureStaticIntent: + CallbackAction(onInvoke: (intent) async { + final withContext = ref.read(withContextProvider); + + final keyboardLayouts = await ref + .read(otpStateProvider(devicePath).notifier) + .getKeyboardLayouts(); + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => ConfigureStaticDialog( + devicePath, otpSlot, keyboardLayouts)); + }); + return null; + }), + if (hasFeature(features.slotsConfigureYubiOtp)) + ConfigureYubiOtpIntent: + CallbackAction(onInvoke: (intent) async { + final withContext = ref.read(withContextProvider); + + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => + ConfigureYubiOtpDialog(devicePath, otpSlot)); + }); return null; }), if (hasFeature(features.slotsDelete)) @@ -114,37 +134,34 @@ Widget registerOtpActions( List buildSlotActions(bool isConfigured, AppLocalizations l10n) { return [ ActionItem( - key: keys.configureYubiOtp, - feature: features.slotsConfigure, - icon: const Icon(Icons.shuffle_outlined), - title: l10n.s_yubiotp, - subtitle: l10n.l_yubiotp_desc, - intent: const ConfigureIntent( - configurationType: SlotConfigurationType.yubiotp)), + key: keys.configureYubiOtp, + feature: features.slotsConfigureYubiOtp, + icon: const Icon(Icons.shuffle_outlined), + title: l10n.s_yubiotp, + subtitle: l10n.l_yubiotp_desc, + intent: const ConfigureYubiOtpIntent(), + ), ActionItem( key: keys.configureChalResp, - feature: features.slotsConfigure, + feature: features.slotsConfigureChalResp, icon: const Icon(Icons.key_outlined), title: l10n.s_challenge_response, subtitle: l10n.l_challenge_response_desc, - intent: const ConfigureIntent( - configurationType: SlotConfigurationType.chalresp)), + intent: const ConfigureChalRespIntent()), ActionItem( key: keys.configureStatic, - feature: features.slotsConfigure, + feature: features.slotsConfigureStatic, icon: const Icon(Icons.password_outlined), title: l10n.s_static_password, subtitle: l10n.l_static_password_desc, - intent: const ConfigureIntent( - configurationType: SlotConfigurationType.static)), + intent: const ConfigureStaticIntent()), ActionItem( key: keys.configureHotp, - feature: features.slotsConfigure, + feature: features.slotsConfigureHotp, icon: const Icon(Icons.tag_outlined), title: l10n.s_hotp, subtitle: l10n.l_hotp_desc, - intent: const ConfigureIntent( - configurationType: SlotConfigurationType.hotp)), + intent: const ConfigureHotpIntent()), ActionItem( key: keys.deleteAction, feature: features.slotsDelete, From b37d19164d3285d28f32d853adadb20161f757eb Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Fri, 15 Dec 2023 09:37:34 +0100 Subject: [PATCH 25/27] Fix duplicate error icons. --- lib/oath/views/add_account_page.dart | 4 --- lib/oath/views/rename_account_dialog.dart | 2 -- lib/piv/views/authentication_dialog.dart | 38 ++++++++++----------- lib/piv/views/generate_key_dialog.dart | 3 -- lib/piv/views/manage_key_dialog.dart | 40 ++++++++++------------- lib/widgets/app_input_decoration.dart | 5 +++ 6 files changed, 40 insertions(+), 52 deletions(-) diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index 2b677b2d..8fd3ec7b 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -375,10 +375,6 @@ class _OathAddAccountPageState extends ConsumerState { ? null : l10n.l_invalid_character_issuer, prefixIcon: const Icon(Icons.business_outlined), - suffixIcon: (!issuerNoColon || - byteLength(issuerText) > issuerMaxLength) - ? const Icon(Icons.error) - : null, ), textInputAction: TextInputAction.next, onChanged: (value) { diff --git a/lib/oath/views/rename_account_dialog.dart b/lib/oath/views/rename_account_dialog.dart index 88983228..589b92f3 100755 --- a/lib/oath/views/rename_account_dialog.dart +++ b/lib/oath/views/rename_account_dialog.dart @@ -209,8 +209,6 @@ class _RenameAccountDialogState extends ConsumerState { ? l10n.l_name_already_exists : null, prefixIcon: const Icon(Icons.people_alt_outlined), - suffixIcon: - !nameNotEmpty || !isUnique ? const Icon(Icons.error) : null, ), textInputAction: TextInputAction.done, onChanged: (value) { diff --git a/lib/piv/views/authentication_dialog.dart b/lib/piv/views/authentication_dialog.dart index 204497a7..b527831b 100644 --- a/lib/piv/views/authentication_dialog.dart +++ b/lib/piv/views/authentication_dialog.dart @@ -122,27 +122,25 @@ class _AuthenticationDialogState extends ConsumerState { : null, errorMaxLines: 3, prefixIcon: const Icon(Icons.key_outlined), - suffixIcon: hasMetadata && (!_keyIsWrong && !_keyFormatInvalid) + suffixIcon: hasMetadata ? null - : hasMetadata && (_keyIsWrong || _keyFormatInvalid) - ? const Icon(Icons.error) - : IconButton( - icon: Icon(_defaultKeyUsed - ? Icons.auto_awesome - : Icons.auto_awesome_outlined), - tooltip: l10n.s_use_default, - onPressed: () { - setState(() { - _keyFormatInvalid = false; - _defaultKeyUsed = !_defaultKeyUsed; - if (_defaultKeyUsed) { - _keyController.text = defaultManagementKey; - } else { - _keyController.clear(); - } - }); - }, - ), + : IconButton( + icon: Icon(_defaultKeyUsed + ? Icons.auto_awesome + : Icons.auto_awesome_outlined), + tooltip: l10n.s_use_default, + onPressed: () { + setState(() { + _keyFormatInvalid = false; + _defaultKeyUsed = !_defaultKeyUsed; + if (_defaultKeyUsed) { + _keyController.text = defaultManagementKey; + } else { + _keyController.clear(); + } + }); + }, + ), ), textInputAction: TextInputAction.next, onChanged: (value) { diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart index ea470803..a2afc332 100644 --- a/lib/piv/views/generate_key_dialog.dart +++ b/lib/piv/views/generate_key_dialog.dart @@ -168,9 +168,6 @@ class _GenerateKeyDialogState extends ConsumerState { errorText: _subject.isNotEmpty && _invalidSubject ? l10n.l_rfc4514_invalid : null, - suffixIcon: _subject.isNotEmpty && _invalidSubject - ? const Icon(Icons.error) - : null, ), textInputAction: TextInputAction.next, enabled: !_generating, diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index 2a1a5947..51338881 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -223,30 +223,24 @@ class _ManageKeyDialogState extends ConsumerState { : null, errorMaxLines: 3, prefixIcon: const Icon(Icons.key_outlined), - suffixIcon: (_hasMetadata && - !_currentIsWrong && - !_currentInvalidFormat) + suffixIcon: _hasMetadata ? null - : (_hasMetadata && _currentIsWrong || - _currentInvalidFormat) - ? const Icon(Icons.error) - : IconButton( - icon: Icon(_defaultKeyUsed - ? Icons.auto_awesome - : Icons.auto_awesome_outlined), - tooltip: l10n.s_use_default, - onPressed: () { - setState(() { - _defaultKeyUsed = !_defaultKeyUsed; - if (_defaultKeyUsed) { - _currentController.text = - defaultManagementKey; - } else { - _currentController.clear(); - } - }); - }, - ), + : IconButton( + icon: Icon(_defaultKeyUsed + ? Icons.auto_awesome + : Icons.auto_awesome_outlined), + tooltip: l10n.s_use_default, + onPressed: () { + setState(() { + _defaultKeyUsed = !_defaultKeyUsed; + if (_defaultKeyUsed) { + _currentController.text = defaultManagementKey; + } else { + _currentController.clear(); + } + }); + }, + ), ), textInputAction: TextInputAction.next, onChanged: (value) { diff --git a/lib/widgets/app_input_decoration.dart b/lib/widgets/app_input_decoration.dart index df7c8c51..56c7be94 100644 --- a/lib/widgets/app_input_decoration.dart +++ b/lib/widgets/app_input_decoration.dart @@ -71,8 +71,13 @@ class AppInputDecoration extends InputDecoration { final existingSuffixIcon = super.suffixIcon; + if (errorIcon != null && + (existingSuffixIcon == null && suffixIcons == null)) { + return errorIcon; + } return Wrap( crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, children: [ if (suffixIcons != null) ...suffixIcons!, if (existingSuffixIcon != null) existingSuffixIcon, From d8debd239ac765a5c3473f0863a2b8be57805528 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 15 Dec 2023 13:03:54 +0100 Subject: [PATCH 26/27] Ensure padding around suffix icons. --- lib/widgets/app_input_decoration.dart | 54 ++++++++++++++++----------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/lib/widgets/app_input_decoration.dart b/lib/widgets/app_input_decoration.dart index 56c7be94..a2cb922f 100644 --- a/lib/widgets/app_input_decoration.dart +++ b/lib/widgets/app_input_decoration.dart @@ -64,28 +64,38 @@ class AppInputDecoration extends InputDecoration { @override Widget? get suffixIcon { - final hasError = errorText != null; + final icons = [ + if (super.suffixIcon != null) super.suffixIcon!, + if (suffixIcons != null) ...suffixIcons!, + if (errorText != null) const Icon(Icons.error_outlined), + ]; - if (hasError || suffixIcons != null) { - final errorIcon = hasError ? const Icon(Icons.error_outlined) : null; - - final existingSuffixIcon = super.suffixIcon; - - if (errorIcon != null && - (existingSuffixIcon == null && suffixIcons == null)) { - return errorIcon; - } - return Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - runAlignment: WrapAlignment.center, - children: [ - if (suffixIcons != null) ...suffixIcons!, - if (existingSuffixIcon != null) existingSuffixIcon, - if (errorIcon != null) ...[errorIcon, const SizedBox(width: 8.0)], - ], - ); - } - - return super.suffixIcon; + return switch (icons.length) { + 0 => null, + 1 => icons.single, + _ => Builder( + builder: (context) { + // Apply the constraints to *each* icon. + final constraints = suffixIconConstraints ?? + Theme.of(context).visualDensity.effectiveConstraints( + const BoxConstraints( + minWidth: kMinInteractiveDimension, + minHeight: kMinInteractiveDimension, + ), + ); + return Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, + children: [ + for (Widget icon in icons) + ConstrainedBox( + constraints: constraints, + child: icon, + ) + ], + ); + }, + ), + }; } } From 0d3c7ef4a057d79e2390e85299dd7d8c231a7f2d Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Fri, 15 Dec 2023 14:34:10 +0100 Subject: [PATCH 27/27] Fix broken tests. --- integration_test/piv_test.dart | 12 +++++++++++- lib/piv/views/manage_key_dialog.dart | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/integration_test/piv_test.dart b/integration_test/piv_test.dart index 3afbcdd7..0d249ff2 100644 --- a/integration_test/piv_test.dart +++ b/integration_test/piv_test.dart @@ -158,7 +158,7 @@ void main() { const shortmanagementkey = 'aaaabbbbccccaaaabbbbccccaaaabbbbccccaaaabbbbccc'; - appTest('Bad managementkey key', (WidgetTester tester) async { + appTest('Out of bounds managementkey key', (WidgetTester tester) async { await tester.configurePiv(); await tester.shortWait(); await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); @@ -169,12 +169,22 @@ void main() { await tester.longWait(); await tester.tap(find.byKey(saveButton).hitTestable()); await tester.longWait(); + expect(tester.isTextButtonEnabled(saveButton), true); + // TODO assert that errorText and errorIcon are shown + }); + + appTest('Short managementkey key', (WidgetTester tester) async { + await tester.configurePiv(); + await tester.shortWait(); + await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); + await tester.longWait(); // testing too short management key does not work await tester.enterText( find.byKey(newPinPukField).hitTestable(), shortmanagementkey); await tester.longWait(); expect(tester.isTextButtonEnabled(saveButton), false); }); + appTest('Change managementkey key', (WidgetTester tester) async { await tester.configurePiv(); await tester.shortWait(); diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index 51338881..33bd3aa8 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -265,6 +265,7 @@ class _ManageKeyDialogState extends ConsumerState { enabled: currentLenOk, prefixIcon: const Icon(Icons.key_outlined), suffixIcon: IconButton( + key: keys.managementKeyRefresh, icon: const Icon(Icons.refresh), tooltip: l10n.s_generate_random, onPressed: currentLenOk