From e88fed1ef8411ada019bae7e814bf25b44665ad3 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 31 May 2023 09:49:13 +0200 Subject: [PATCH 001/158] fix nfc adapter state change receiver --- .../main/kotlin/com/yubico/authenticator/MainActivity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt index 78a7d6c4..cb468a7b 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt @@ -170,18 +170,17 @@ class MainActivity : FlutterFragmentActivity() { @SuppressLint("WrongConstant") override fun onStart() { super.onStart() - val receiverFlags = ContextCompat.RECEIVER_NOT_EXPORTED ContextCompat.registerReceiver( this, qrScannerCameraClosedBR, QRScannerCameraClosedBR.intentFilter, - receiverFlags + ContextCompat.RECEIVER_NOT_EXPORTED ) ContextCompat.registerReceiver( this, nfcAdapterStateChangeBR, NfcAdapterStateChangedBR.intentFilter, - receiverFlags + ContextCompat.RECEIVER_EXPORTED ) } @@ -351,6 +350,7 @@ class MainActivity : FlutterFragmentActivity() { } override fun onReceive(context: Context?, intent: Intent?) { + Log.d(TAG, "Restarting nfc discovery after camera was closed.") (context as? MainActivity)?.startNfcDiscovery() } } From 6eab3e310b972ec5aece62080c737c20932821e4 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 31 May 2023 10:24:41 +0200 Subject: [PATCH 002/158] unfocus oath text fields before submit --- lib/oath/views/add_account_page.dart | 6 +++++- lib/oath/views/manage_password_dialog.dart | 6 +++++- lib/oath/views/rename_account_dialog.dart | 6 +++++- lib/widgets/focus_utils.dart | 18 ++++++++++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 lib/widgets/focus_utils.dart diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index b99a88ca..b1fac0d6 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * 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. @@ -37,6 +37,7 @@ import '../../desktop/models.dart'; import '../../management/models.dart'; import '../../widgets/choice_filter_chip.dart'; import '../../widgets/file_drop_target.dart'; +import '../../widgets/focus_utils.dart'; import '../../widgets/responsive_dialog.dart'; import '../../widgets/utf8_utils.dart'; import '../keys.dart' as keys; @@ -175,6 +176,9 @@ class _OathAddAccountPageState extends ConsumerState { {DevicePath? devicePath, required Uri credUri}) async { final l10n = AppLocalizations.of(context)!; try { + + FocusUtils.unfocus(context); + if (devicePath == null) { assert(isAndroid, 'devicePath is only optional for Android'); await ref diff --git a/lib/oath/views/manage_password_dialog.dart b/lib/oath/views/manage_password_dialog.dart index 3b95d5c2..536c80e9 100755 --- a/lib/oath/views/manage_password_dialog.dart +++ b/lib/oath/views/manage_password_dialog.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * 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. @@ -20,6 +20,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; +import '../../widgets/focus_utils.dart'; import '../../widgets/responsive_dialog.dart'; import '../models.dart'; import '../state.dart'; @@ -42,6 +43,9 @@ class _ManagePasswordDialogState extends ConsumerState { bool _currentIsWrong = false; _submit() async { + + FocusUtils.unfocus(context); + final result = await ref .read(oathStateProvider(widget.path).notifier) .setPassword(_currentPassword, _newPassword); diff --git a/lib/oath/views/rename_account_dialog.dart b/lib/oath/views/rename_account_dialog.dart index 1e83bf8c..17cb6432 100755 --- a/lib/oath/views/rename_account_dialog.dart +++ b/lib/oath/views/rename_account_dialog.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * 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. @@ -24,6 +24,7 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../exception/cancellation_exception.dart'; import '../../desktop/models.dart'; +import '../../widgets/focus_utils.dart'; import '../../widgets/responsive_dialog.dart'; import '../../widgets/utf8_utils.dart'; import '../models.dart'; @@ -60,6 +61,9 @@ class _RenameAccountDialogState extends ConsumerState { void _submit() async { final l10n = AppLocalizations.of(context)!; try { + + FocusUtils.unfocus(context); + // Rename credentials final renamed = await ref .read(credentialListProvider(widget.device.path).notifier) diff --git a/lib/widgets/focus_utils.dart b/lib/widgets/focus_utils.dart new file mode 100644 index 00000000..94fca1b0 --- /dev/null +++ b/lib/widgets/focus_utils.dart @@ -0,0 +1,18 @@ + +import 'package:flutter/cupertino.dart'; +import 'package:logging/logging.dart'; + +import '../app/logging.dart'; + +final _log = Logger('FocusUtils'); + +class FocusUtils { + static void unfocus(BuildContext context) { + FocusScopeNode currentFocus = FocusScope.of(context); + + if (!currentFocus.hasPrimaryFocus) { + _log.debug('Removing focus...'); + currentFocus.unfocus(); + } + } +} \ No newline at end of file From a2d503448a6b02674e1c514ccd3344bd2514e816 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 31 May 2023 14:57:44 +0200 Subject: [PATCH 003/158] use traffic level for icon pack logging --- lib/oath/icon_provider/icon_cache.dart | 8 ++++---- lib/oath/icon_provider/icon_file_loader.dart | 4 ++-- lib/oath/icon_provider/icon_pack_manager.dart | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/oath/icon_provider/icon_cache.dart b/lib/oath/icon_provider/icon_cache.dart index 1c27a7f2..d88e7ec8 100644 --- a/lib/oath/icon_provider/icon_cache.dart +++ b/lib/oath/icon_provider/icon_cache.dart @@ -30,15 +30,15 @@ class IconCacheFs { final file = await _getFile(fileName); final exists = await file.exists(); if (exists) { - _log.debug('File $fileName exists in cache'); + _log.traffic('File $fileName exists in cache'); } else { - _log.debug('File $fileName does not exist in cache'); + _log.traffic('File $fileName does not exist in cache'); } return exists ? (await file.readAsBytes()).buffer.asByteData() : null; } Future write(String fileName, Uint8List data) async { - _log.debug('Writing $fileName to cache'); + _log.traffic('Writing $fileName to cache'); final file = await _getFile(fileName); if (!await file.exists()) { await file.create(recursive: true, exclusive: false); @@ -52,7 +52,7 @@ class IconCacheFs { try { await cacheDirectory.delete(recursive: true); } catch (e) { - _log.error( + _log.traffic( 'Failed to delete cache directory ${cacheDirectory.path}', e); } } diff --git a/lib/oath/icon_provider/icon_file_loader.dart b/lib/oath/icon_provider/icon_file_loader.dart index 3196dc49..ca927953 100644 --- a/lib/oath/icon_provider/icon_file_loader.dart +++ b/lib/oath/icon_provider/icon_file_loader.dart @@ -45,7 +45,7 @@ class IconFileLoader extends BytesLoader { // check if the requested file exists in memory cache var cachedData = memCache.read(cacheFileName); if (cachedData != null) { - _log.debug('Returning $cacheFileName image data from memory cache'); + _log.traffic('Returning $cacheFileName image data from memory cache'); return cachedData; } @@ -55,7 +55,7 @@ class IconFileLoader extends BytesLoader { cachedData = await fsCache.read(cacheFileName); if (cachedData != null) { memCache.write(cacheFileName, cachedData.buffer.asUint8List()); - _log.debug('Returning $cacheFileName image data from fs cache'); + _log.traffic('Returning $cacheFileName image data from fs cache'); return cachedData; } diff --git a/lib/oath/icon_provider/icon_pack_manager.dart b/lib/oath/icon_provider/icon_pack_manager.dart index 3f725819..d2e3d2a2 100644 --- a/lib/oath/icon_provider/icon_pack_manager.dart +++ b/lib/oath/icon_provider/icon_pack_manager.dart @@ -48,10 +48,10 @@ class IconPackManager extends StateNotifier> { final packFile = File(join(packDirectory.path, getLocalIconFileName('pack.json'))); - _log.debug('Looking for file: ${packFile.path}'); + _log.traffic('Looking for file: ${packFile.path}'); if (!await packFile.exists()) { - _log.debug('Failed to find icons pack ${packFile.path}'); + _log.traffic('Failed to find icons pack ${packFile.path}'); state = AsyncValue.error( 'Failed to find icon pack ${packFile.path}', StackTrace.current); return; @@ -76,10 +76,10 @@ class IconPackManager extends StateNotifier> { directory: packDirectory, icons: icons)); - _log.debug( + _log.traffic( 'Parsed ${state.value?.name} with ${state.value?.icons.length} icons'); } catch (e) { - _log.debug('Failed to parse icons pack ${packFile.path}'); + _log.traffic('Failed to parse icons pack ${packFile.path}'); state = AsyncValue.error( 'Failed to parse icon pack ${packFile.path}', StackTrace.current); return; @@ -123,7 +123,7 @@ class IconPackManager extends StateNotifier> { final data = file.content as List; final extractedFile = File(join(unpackDirectory.path, getLocalIconFileName(filename))); - _log.debug('Writing file: ${extractedFile.path} (size: ${file.size})'); + _log.traffic('Writing file: ${extractedFile.path} (size: ${file.size})'); final createdFile = await extractedFile.create(recursive: true); await createdFile.writeAsBytes(data); } From 6d69ed37f1c4500177a2cf167e20c10c641c9522 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 31 May 2023 15:07:40 +0200 Subject: [PATCH 004/158] add localization support to tap_request_dialog --- .../com/yubico/authenticator/DialogManager.kt | 41 +++--- .../oath/OathActionDescription.kt | 34 +++++ .../yubico/authenticator/oath/OathManager.kt | 38 +++--- lib/android/tap_request_dialog.dart | 117 +++++++++++++++--- lib/l10n/app_en.arb | 14 +++ 5 files changed, 196 insertions(+), 48 deletions(-) create mode 100644 android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt index 0e001c5b..025f6530 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * 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. @@ -24,10 +24,16 @@ import kotlinx.serialization.json.Json typealias OnDialogCancelled = suspend () -> Unit -enum class Icon(val value: String) { - NFC("nfc"), - SUCCESS("success"), - ERROR("error"); +enum class DialogIcon(val value: Int) { + Nfc(0), + Success(1), + Failure(2); +} + +enum class DialogTitle(val value: Int) { + TapKey(0), + OperationSuccessful(1), + OperationFailed(2) } class DialogManager(messenger: BinaryMessenger, private val coroutineScope: CoroutineScope) { @@ -45,16 +51,21 @@ class DialogManager(messenger: BinaryMessenger, private val coroutineScope: Coro } } - fun showDialog(icon: Icon, title: String, description: String, cancelled: OnDialogCancelled?) { + fun showDialog( + dialogIcon: DialogIcon, + dialogTitle: DialogTitle, + dialogDescriptionId: Int, + cancelled: OnDialogCancelled? + ) { onCancelled = cancelled coroutineScope.launch { channel.invoke( "show", Json.encodeToString( mapOf( - "title" to title, - "description" to description, - "icon" to icon.value + "title" to dialogTitle.value, + "description" to dialogDescriptionId, + "icon" to dialogIcon.value ) ) ) @@ -62,17 +73,17 @@ class DialogManager(messenger: BinaryMessenger, private val coroutineScope: Coro } suspend fun updateDialogState( - icon: Icon? = null, - title: String? = null, - description: String? = null + dialogIcon: DialogIcon? = null, + dialogTitle: DialogTitle, + dialogDescriptionId: Int? = null, ) { channel.invoke( "state", Json.encodeToString( mapOf( - "title" to title, - "description" to description, - "icon" to icon?.value + "title" to dialogTitle.value, + "description" to dialogDescriptionId, + "icon" to dialogIcon?.value ) ) ) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt new file mode 100644 index 00000000..07be82ad --- /dev/null +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt @@ -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. + */ + +package com.yubico.authenticator.oath + +const val dialogDescriptionOathIndex = 100 + +enum class OathActionDescription(private val value: Int) { + Reset(0), + Unlock(1), + SetPassword(2), + UnsetPassword(3), + AddAccount(4), + RenameAccount(5), + DeleteAccount(6), + CalculateCode(7), + ActionFailure(8); + + val id: Int + get() = value + dialogDescriptionOathIndex +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt index c550ea1e..ac2f1cbe 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt @@ -359,7 +359,7 @@ class OathManager( val credentialData: CredentialData = CredentialData.parseUri(URI.create(uri)) addToAny = true - return useOathSessionNfc("Add account") { session -> + return useOathSessionNfc(OathActionDescription.AddAccount) { session -> // We need to check for duplicates here since we haven't yet read the credentials if (session.credentials.any { it.id.contentEquals(credentialData.id) }) { throw Exception("A credential with this ID already exists!") @@ -383,7 +383,7 @@ class OathManager( } private suspend fun reset(): String = - useOathSession("Reset YubiKey") { + useOathSession(OathActionDescription.Reset) { // note, it is ok to reset locked session it.reset() keyManager.removeKey(it.deviceId) @@ -395,7 +395,7 @@ class OathManager( } private suspend fun unlock(password: String, remember: Boolean): String = - useOathSession("Unlocking") { + useOathSession(OathActionDescription.Unlock) { val accessKey = it.deriveAccessKey(password.toCharArray()) keyManager.addKey(it.deviceId, accessKey, remember) @@ -417,7 +417,7 @@ class OathManager( currentPassword: String?, newPassword: String, ): String = - useOathSession("Set password", unlock = false) { session -> + useOathSession(OathActionDescription.SetPassword, unlock = false) { session -> if (session.isAccessKeySet) { if (currentPassword == null) { throw Exception("Must provide current password to be able to change it") @@ -436,7 +436,7 @@ class OathManager( } private suspend fun unsetPassword(currentPassword: String): String = - useOathSession("Unset password", unlock = false) { session -> + useOathSession(OathActionDescription.UnsetPassword, unlock = false) { session -> if (session.isAccessKeySet) { // test current password sent by the user if (session.unlock(currentPassword.toCharArray())) { @@ -468,7 +468,7 @@ class OathManager( uri: String, requireTouch: Boolean, ): String = - useOathSession("Add account") { session -> + useOathSession(OathActionDescription.AddAccount) { session -> val credentialData: CredentialData = CredentialData.parseUri(URI.create(uri)) @@ -489,7 +489,7 @@ class OathManager( } private suspend fun renameAccount(uri: String, name: String, issuer: String?): String = - useOathSession("Rename") { session -> + useOathSession(OathActionDescription.RenameAccount) { session -> val credential = getOathCredential(session, uri) val renamedCredential = Credential(session.renameCredential(credential, name, issuer), session.deviceId) @@ -502,7 +502,7 @@ class OathManager( } private suspend fun deleteAccount(credentialId: String): String = - useOathSession("Delete account") { session -> + useOathSession(OathActionDescription.DeleteAccount) { session -> val credential = getOathCredential(session, credentialId) session.deleteCredential(credential) oathViewModel.removeCredential(Credential(credential, session.deviceId)) @@ -535,7 +535,7 @@ class OathManager( } private suspend fun calculate(credentialId: String): String = - useOathSession("Calculate") { session -> + useOathSession(OathActionDescription.CalculateCode) { session -> val credential = getOathCredential(session, credentialId) val code = Code.from(calculateCode(session, credential)) @@ -648,7 +648,7 @@ class OathManager( } private suspend fun useOathSession( - title: String, + oathActionDescription: OathActionDescription, unlock: Boolean = true, action: (YubiKitOathSession) -> T ): T { @@ -657,7 +657,7 @@ class OathManager( unlockOnConnect.set(unlock) return appViewModel.connectedYubiKey.value?.let { useOathSessionUsb(it, action) - } ?: useOathSessionNfc(title, action) + } ?: useOathSessionNfc(oathActionDescription, action) } private suspend fun useOathSessionUsb( @@ -668,7 +668,7 @@ class OathManager( } private suspend fun useOathSessionNfc( - title: String, + oathActionDescription: OathActionDescription, block: (YubiKitOathSession) -> T ): T { try { @@ -678,15 +678,15 @@ class OathManager( block.invoke(it.value) }) } - dialogManager.showDialog(Icon.NFC, "Tap your key", title) { - Log.d(TAG, "Cancelled Dialog $title") + dialogManager.showDialog(DialogIcon.Nfc, DialogTitle.TapKey, oathActionDescription.id) { + Log.d(TAG, "Cancelled Dialog ${oathActionDescription.name}") pendingAction?.invoke(Result.failure(CancellationException())) pendingAction = null } } dialogManager.updateDialogState( - icon = Icon.SUCCESS, - title = "Success" + dialogIcon = DialogIcon.Success, + dialogTitle = DialogTitle.OperationSuccessful ) // TODO: This delays the closing of the dialog, but also the return value delay(500) @@ -695,9 +695,9 @@ class OathManager( throw cancelled } catch (error: Throwable) { dialogManager.updateDialogState( - icon = Icon.ERROR, - title = "Failure", - description = "Action failed - try again" + dialogIcon = DialogIcon.Failure, + dialogTitle = DialogTitle.OperationFailed, + dialogDescriptionId = OathActionDescription.ActionFailure.id ) // TODO: This delays the closing of the dialog, but also the return value delay(1500) diff --git a/lib/android/tap_request_dialog.dart b/lib/android/tap_request_dialog.dart index 1a6c1be1..08a64a24 100755 --- a/lib/android/tap_request_dialog.dart +++ b/lib/android/tap_request_dialog.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * 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. @@ -19,6 +19,7 @@ import 'dart:convert'; 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 '../app/state.dart'; @@ -27,6 +28,69 @@ import '../widgets/custom_icons.dart'; const _channel = MethodChannel('com.yubico.authenticator.channel.dialog'); +// _DIcon identifies the icon which should be displayed on the dialog +enum _DIcon { + nfcIcon, + successIcon, + failureIcon, + invalid; + + static _DIcon fromId(int? id) => + const { + 0: _DIcon.nfcIcon, + 1: _DIcon.successIcon, + 2: _DIcon.failureIcon + }[id] ?? + _DIcon.invalid; +} + +// _DDesc contains id of title resource for the dialog +enum _DTitle { + tapKey, + operationSuccessful, + operationFailed, + invalid; + + static _DTitle fromId(int? id) => + const { + 0: _DTitle.tapKey, + 1: _DTitle.operationSuccessful, + 2: _DTitle.operationFailed + }[id] ?? + _DTitle.invalid; +} + +// _DDesc contains action description in the dialog +enum _DDesc { + // oath descriptions + oathResetApplet, + oathUnlockSession, + oathSetPassword, + oathUnsetPassword, + oathAddAccount, + oathRenameAccount, + oathDeleteAccount, + oathCalculateCode, + oathActionFailure, + invalid; + + static const int dialogDescriptionOathIndex = 100; + + static _DDesc fromId(int? id) => + const { + dialogDescriptionOathIndex + 0: _DDesc.oathResetApplet, + dialogDescriptionOathIndex + 1: _DDesc.oathUnlockSession, + dialogDescriptionOathIndex + 2: _DDesc.oathSetPassword, + dialogDescriptionOathIndex + 3: _DDesc.oathUnsetPassword, + dialogDescriptionOathIndex + 4: _DDesc.oathAddAccount, + dialogDescriptionOathIndex + 5: _DDesc.oathRenameAccount, + dialogDescriptionOathIndex + 6: _DDesc.oathDeleteAccount, + dialogDescriptionOathIndex + 7: _DDesc.oathCalculateCode, + dialogDescriptionOathIndex + 8: _DDesc.oathActionFailure + }[id] ?? + _DDesc.invalid; +} + final androidDialogProvider = Provider<_DialogProvider>( (ref) { return _DialogProvider(ref.watch(withContextProvider)); @@ -65,20 +129,46 @@ class _DialogProvider { _controller = null; } - Widget? _getIcon(String? icon) => switch (icon) { - 'nfc' => nfcIcon, - 'success' => const Icon(Icons.check_circle), - 'error' => const Icon(Icons.error), + Widget? _getIcon(int? icon) => switch (_DIcon.fromId(icon)) { + _DIcon.nfcIcon => nfcIcon, + _DIcon.successIcon => const Icon(Icons.check_circle), + _DIcon.failureIcon => const Icon(Icons.error), _ => null, }; + String _getTitle(BuildContext context, int? titleId) { + final l10n = AppLocalizations.of(context)!; + return switch (_DTitle.fromId(titleId)) { + _DTitle.tapKey => l10n.s_nfc_dialog_tap_key, + _DTitle.operationSuccessful => l10n.s_nfc_dialog_operation_success, + _DTitle.operationFailed => l10n.s_nfc_dialog_operation_failed, + _ => '' + }; + } + + String _getDialogDescription(BuildContext context, int? descriptionId) { + final l10n = AppLocalizations.of(context)!; + return switch (_DDesc.fromId(descriptionId)) { + _DDesc.oathResetApplet => l10n.s_nfc_dialog_oath_reset, + _DDesc.oathUnlockSession => l10n.s_nfc_dialog_oath_unlock, + _DDesc.oathSetPassword => l10n.s_nfc_dialog_oath_set_password, + _DDesc.oathUnsetPassword => l10n.s_nfc_dialog_oath_unset_password, + _DDesc.oathAddAccount => l10n.s_nfc_dialog_oath_add_account, + _DDesc.oathRenameAccount => l10n.s_nfc_dialog_oath_rename_account, + _DDesc.oathDeleteAccount => l10n.s_nfc_dialog_oath_delete_account, + _DDesc.oathCalculateCode => l10n.s_nfc_dialog_oath_calculate_code, + _DDesc.oathActionFailure => l10n.s_nfc_dialog_oath_failure, + _ => '' + }; + } + Future _updateDialogState( - String? title, String? description, String? iconName) async { - final icon = _getIcon(iconName); + int? title, int? description, int? dialogIcon) async { + final icon = _getIcon(dialogIcon); await _withContext((context) async { _controller?.updateContent( - title: title, - description: description, + title: _getTitle(context, title), + description: _getDialogDescription(context, description), icon: icon != null ? IconTheme( data: IconTheme.of(context).copyWith(size: 64), @@ -89,13 +179,12 @@ class _DialogProvider { }); } - Future _showDialog( - String title, String description, String? iconName) async { - final icon = _getIcon(iconName); + Future _showDialog(int title, int description, int? dialogIcon) async { + final icon = _getIcon(dialogIcon); _controller = await _withContext((context) async => promptUserInteraction( context, - title: title, - description: description, + title: _getTitle(context, title), + description: _getDialogDescription(context, description), icon: icon != null ? IconTheme( data: IconTheme.of(context).copyWith(size: 64), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2677935a..182f44fa 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -441,5 +441,19 @@ "l_launch_app_on_usb_off": "Other apps can use the YubiKey over USB", "s_allow_screenshots": "Allow screenshots", + "s_nfc_dialog_tap_key": "Tap your key", + "s_nfc_dialog_operation_success": "Success", + "s_nfc_dialog_operation_failed": "Failed", + + "s_nfc_dialog_oath_reset": "Reset OATH applet", + "s_nfc_dialog_oath_unlock": "Unlock OATH applet", + "s_nfc_dialog_oath_set_password": "Set OATH password", + "s_nfc_dialog_oath_unset_password": "Remove OATH password", + "s_nfc_dialog_oath_add_account": "Add new account", + "s_nfc_dialog_oath_rename_account": "Rename account", + "s_nfc_dialog_oath_delete_account": "Delete account", + "s_nfc_dialog_oath_calculate_code": "Calculate OATH code", + "s_nfc_dialog_oath_failure": "OATH operation failed", + "@_eof": {} } \ No newline at end of file From a93e5078f2ccbd4e941fde5f947779e13769dcc6 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 31 May 2023 15:22:38 +0200 Subject: [PATCH 005/158] make dialog desc strings unique --- lib/l10n/app_en.arb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 182f44fa..2e0d2577 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -445,14 +445,14 @@ "s_nfc_dialog_operation_success": "Success", "s_nfc_dialog_operation_failed": "Failed", - "s_nfc_dialog_oath_reset": "Reset OATH applet", - "s_nfc_dialog_oath_unlock": "Unlock OATH applet", - "s_nfc_dialog_oath_set_password": "Set OATH password", - "s_nfc_dialog_oath_unset_password": "Remove OATH password", - "s_nfc_dialog_oath_add_account": "Add new account", - "s_nfc_dialog_oath_rename_account": "Rename account", - "s_nfc_dialog_oath_delete_account": "Delete account", - "s_nfc_dialog_oath_calculate_code": "Calculate OATH code", + "s_nfc_dialog_oath_reset": "Action: reset OATH applet", + "s_nfc_dialog_oath_unlock": "Action: unlock OATH applet", + "s_nfc_dialog_oath_set_password": "Action: set OATH password", + "s_nfc_dialog_oath_unset_password": "Action: remove OATH password", + "s_nfc_dialog_oath_add_account": "Action: add new account", + "s_nfc_dialog_oath_rename_account": "Action: rename account", + "s_nfc_dialog_oath_delete_account": "Action: delete account", + "s_nfc_dialog_oath_calculate_code": "Action: calculate OATH code", "s_nfc_dialog_oath_failure": "OATH operation failed", "@_eof": {} From bd4cabc9a8fbbbae33bcd033126aa89a6abb5486 Mon Sep 17 00:00:00 2001 From: nebulon42 Date: Sun, 18 Jun 2023 18:52:44 +0200 Subject: [PATCH 006/158] add German translation --- lib/l10n/app_de.arb | 445 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 445 insertions(+) create mode 100644 lib/l10n/app_de.arb diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb new file mode 100644 index 00000000..fbe166a1 --- /dev/null +++ b/lib/l10n/app_de.arb @@ -0,0 +1,445 @@ +{ + "@@locale": "de", + + "@_readme": { + "notes": [ + "Alle Zeichenketten beginnen mit einem Großbuchstaben.", + "Gruppieren Sie Zeichenketten nach Kategorie, aber fügen Sie nicht zu einem Bereich hinzu, wenn sie in mehreren Bereichen verwendet werden können.", + "Führen Sie check_strings.py für die .arb Datei aus, um Probleme zu finden. Passen Sie @_lint_rules nach Sprache an wie nötig." + ], + "prefixes": { + "s_": "Ein einzelnes Wort oder wenige Wörter. Sollte kurz genug sein, um auf einer Schaltfläche oder einer Überschrift angezeigt zu werden.", + "l_": "Eine einzelne Zeile, kann umbgebrochen werden. Sollte nicht mehr als einen Satz umfassen und nicht mit einem Punkt enden.", + "p_": "Ein oder mehrere ganze Sätze mit allen Satzzeichen.", + "q_": "Eine Frage, die mit einem Fragezeichen endet." + } + }, + + "@_lint_rules": { + "s_max_words": 4, + "s_max_length": 32 + }, + + "app_name": "Yubico Authenticator", + + "s_save": "Speichern", + "s_cancel": "Abbrechen", + "s_close": "Schließen", + "s_delete": "Löschen", + "s_quit": "Beenden", + "s_unlock": "Entsperren", + "s_calculate": "Berechnen", + "s_label": "Beschriftung", + "s_name": "Name", + "s_usb": "USB", + "s_nfc": "NFC", + "s_show_window": "Fenster anzeigen", + "s_hide_window": "Fenster verstecken", + "q_rename_target": "{label} umbenennen?", + "@q_rename_target" : { + "placeholders": { + "label": {} + } + }, + + "s_about": "Über", + "s_appearance": "Aussehen", + "s_authenticator": "Authenticator", + "s_manage": "Verwalten", + "s_setup": "Einrichten", + "s_settings": "Einstellungen", + "s_webauthn": "WebAuthn", + "s_help_and_about": "Hilfe und Über", + "s_help_and_feedback": "Hilfe und Feedback", + "s_send_feedback": "Senden Sie uns Feedback", + "s_i_need_help": "Ich brauche Hilfe", + "s_troubleshooting": "Problembehebung", + "s_terms_of_use": "Nutzungsbedingungen", + "s_privacy_policy": "Datenschutzerklärung", + "s_open_src_licenses": "Open Source-Lizenzen", + "s_configure_yk": "YubiKey konfigurieren", + "s_please_wait": "Bitte warten\u2026", + "s_secret_key": "Geheimer Schlüssel", + "s_invalid_length": "Ungültige Länge", + "s_require_touch": "Berührung ist erforderlich", + "q_have_account_info": "Haben Sie Konto-Informationen?", + "s_run_diagnostics": "Diagnose ausführen", + "s_log_level": "Log-Level: {level}", + "@s_log_level": { + "placeholders": { + "level": {} + } + }, + "s_character_count": "Anzahl Zeichen", + "s_learn_more": "Mehr\u00a0erfahren", + + "@_language": {}, + "s_language": "Sprache", + "l_enable_community_translations": "Übersetzungen der Gemeinschaft aktivieren", + "p_community_translations_desc": "Diese Übersetzungen werden von der Gemeinschaft erstellt und gewartet. Sie könnten Fehler enthalten oder unvollständig sein.", + + "@_theme": {}, + "s_app_theme": "App Theme", + "s_choose_app_theme": "App Theme auswählen", + "s_system_default": "Standard des Systems", + "s_light_mode": "Heller Modus", + "s_dark_mode": "Dunkler Modus", + + "@_yubikey_selection": {}, + "s_yk_information": "YubiKey Information", + "s_select_yk": "YubiKey auswählen", + "s_select_to_scan": "Zum Scannen auswählen", + "s_hide_device": "Gerät verstecken", + "s_show_hidden_devices": "Versteckte Geräte anzeigen", + "s_sn_serial": "S/N: {serial}", + "@s_sn_serial" : { + "placeholders": { + "serial": {} + } + }, + "s_fw_version": "F/W: {version}", + "@s_fw_version" : { + "placeholders": { + "version": {} + } + }, + + "@_yubikey_interactions": {}, + "l_insert_yk": "YubiKey anschließen", + "l_insert_or_tap_yk": "YubiKey anschließen oder dagegenhalten", + "l_unplug_yk": "Entfernen Sie Ihren YubiKey", + "l_reinsert_yk": "Schließen Sie Ihren YubiKey wieder an", + "l_place_on_nfc_reader": "Halten Sie Ihren YubiKey zum NFC-Leser", + "l_replace_yk_on_reader": "Halten Sie Ihren YubiKey wieder zum Leser", + "l_remove_yk_from_reader": "Entfernen Sie Ihren YubiKey vom NFC-Leser", + "p_try_reinsert_yk": "Versuchen Sie Ihren YubiKey zu entfernen und wieder anzuschließen.", + "s_touch_required": "Berührung erforderlich", + "l_touch_button_now": "Berühren Sie jetzt die Schaltfläche auf Ihrem YubiKey", + "l_keep_touching_yk": "Berühren Sie Ihren YubiKey wiederholt\u2026", + + "@_app_configuration": {}, + "s_toggle_applications": "Anwendungen umschalten", + "l_min_one_interface": "Mindestens ein Interface muss aktiviert sein", + "s_reconfiguring_yk": "YubiKey wird neu konfiguriert\u2026", + "s_config_updated": "Konfiguration aktualisiert", + "l_config_updated_reinsert": "Konfiguration aktualisiert, entfernen Sie Ihren YubiKey und schließen ihn wieder an", + "s_app_not_supported": "Anwendung nicht unterstützt", + "l_app_not_supported_on_yk": "Der verwendete YubiKey unterstützt die Anwendung '{app}' nicht", + "@l_app_not_supported_on_yk" : { + "placeholders": { + "app": {} + } + }, + "l_app_not_supported_desc": "Diese Anwendung wird nicht unterstützt", + "s_app_disabled": "Anwendung deaktiviert", + "l_app_disabled_desc": "Aktivieren Sie die Anwendung '{app}' auf Ihrem YubiKey für Zugriff", + "@l_app_disabled_desc" : { + "placeholders": { + "app": {} + } + }, + "s_fido_disabled": "FIDO2 deaktiviert", + "l_webauthn_req_fido2": "WebAuthn erfordert, dass die FIDO2 Anwendung auf Ihrem YubiKey aktiviert ist", + + "@_connectivity_issues": {}, + "l_helper_not_responding": "Der Helper-Prozess antwortet nicht", + "l_yk_no_access": "Auf diesen YubiKey kann nicht zugegriffen werden", + "s_yk_inaccessible": "Gerät nicht zugänglich", + "l_open_connection_failed": "Konnte keine Verbindung öffnen", + "l_ccid_connection_failed": "Konnte keine Smartcard-Verbindung öffnen", + "p_ccid_service_unavailable": "Stellen Sie sicher, dass Ihr Smartcard-Service funktioniert.", + "p_pcscd_unavailable": "Stellen Sie sicher, dass pcscd installiert ist und ausgeführt wird.", + "l_no_yk_present": "Kein YubiKey vorhanden", + "s_unknown_type": "Unbekannter Typ", + "s_unknown_device": "Unbekanntes Gerät", + "s_unsupported_yk": "Nicht unterstützter YubiKey", + "s_yk_not_recognized": "Geräte nicht erkannt", + + "@_general_errors": {}, + "l_error_occured": "Es ist ein Fehler aufgetreten", + "s_application_error": "Anwendungs-Fehler", + "l_import_error": "Import-Fehler", + "l_file_not_found": "Datei nicht gefunden", + "l_file_too_big": "Datei ist zu groß", + "l_filesystem_error": "Fehler beim Dateisystem-Zugriff", + + "@_pins": {}, + "s_pin": "PIN", + "s_set_pin": "PIN setzen", + "s_change_pin": "PIN ändern", + "s_current_pin": "Derzeitige PIN", + "s_new_pin": "Neue PIN", + "s_confirm_pin": "PIN bestätigen", + "l_new_pin_len": "Neue PIN muss mindestens {length} Zeichen lang sein", + "@l_new_pin_len" : { + "placeholders": { + "length": {} + } + }, + "s_pin_set": "PIN gesetzt", + "l_set_pin_failed": "PIN konnte nicht gesetzt werden: {message}", + "@l_set_pin_failed" : { + "placeholders": { + "message": {} + } + }, + "l_wrong_pin_attempts_remaining": "Falsche PIN, {retries} Versuch(e) verbleibend", + "@l_wrong_pin_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, + "s_fido_pin_protection": "FIDO PIN Schutz", + "l_fido_pin_protection_optional": "Optionaler FIDO PIN Schutz", + "l_enter_fido2_pin": "Geben Sie die FIDO2 PIN für Ihren YubiKey ein", + "l_optionally_set_a_pin": "Setzen Sie optional eine PIN, um den Zugriff auf Ihren YubiKey zu schützen\nAls Sicherheitsschlüssel auf Webseiten registrieren", + "l_pin_blocked_reset": "PIN ist blockiert; setzen Sie die FIDO Anwendung auf Werkseinstellung zurück", + "l_set_pin_first": "Zuerst ist eine PIN erforderlich", + "l_unlock_pin_first": "Zuerst mit PIN entsperren", + "l_pin_soft_locked": "PIN wurde blockiert bis der YubiKey entfernt und wieder angeschlossen wird", + "p_enter_current_pin_or_reset": "Geben Sie Ihre aktuelle PIN ein. Wenn Sie die PIN nicht wissen, müssen Sie den YubiKey zurücksetzen.", + "p_enter_new_fido2_pin": "Geben Sie Ihre neue PIN ein. Eine PIN muss mindestens {length} Zeichen lang sein und kann Buchstaben, Ziffern und spezielle Zeichen enthalten.", + "@p_enter_new_fido2_pin" : { + "placeholders": { + "length": {} + } + }, + + "@_passwords": {}, + "s_password": "Passwort", + "s_manage_password": "Passwort verwalten", + "s_set_password": "Passwort setzen", + "s_password_set": "Passwort gesetzt", + "l_optional_password_protection": "Optionaler Passwortschutz", + "s_new_password": "Neues Passwort", + "s_current_password": "Aktuelles Passwort", + "s_confirm_password": "Passwort bestätigen", + "s_wrong_password": "Falsches Passwort", + "s_remove_password": "Passwort entfernen", + "s_password_removed": "Passwort entfernt", + "s_remember_password": "Passwort speichern", + "s_clear_saved_password": "Gespeichertes Passwort entfernen", + "s_password_forgotten": "Passwort vergessen", + "l_keystore_unavailable": "Passwortspeicher des Betriebssystems nicht verfügbar", + "l_remember_pw_failed": "Konnte Passwort nicht speichern", + "l_unlock_first": "Zuerst mit Passwort entsperren", + "l_enter_oath_pw": "Das OATH-Passwort für Ihren YubiKey eingeben", + "p_enter_current_password_or_reset": "Geben Sie Ihr aktuelles Passwort ein. Wenn Sie Ihr Passwort nicht wissen, müssen Sie den YubiKey zurücksetzen.", + "p_enter_new_password": "Geben Sie Ihr neues Passwort ein. Ein Passwort kann Buchstaben, Ziffern und spezielle Zeichen enthalten.", + + "@_oath_accounts": {}, + "l_account": "Konto: {label}", + "@l_account" : { + "placeholders": { + "label": {} + } + }, + "s_accounts": "Konten", + "s_no_accounts": "Keine Konten", + "s_add_account": "Konto hinzufügen", + "s_account_added": "Konto hinzugefügt", + "l_account_add_failed": "Fehler beim Hinzufügen des Kontos: {message}", + "@l_account_add_failed" : { + "placeholders": { + "message": {} + } + }, + "l_account_name_required": "Ihr Konto muss einen Namen haben", + "l_name_already_exists": "Für diesen Aussteller existiert dieser Name bereits", + "l_invalid_character_issuer": "Ungültiges Zeichen: ':' ist im Aussteller nicht erlaubt", + "s_pinned": "Angepinnt", + "s_pin_account": "Konto anpinnen", + "s_unpin_account": "Konto nicht mehr anpinnen", + "s_no_pinned_accounts": "Keine angepinnten Konten", + "s_rename_account": "Konto umbenennen", + "s_account_renamed": "Konto umbenannt", + "p_rename_will_change_account_displayed": "Das ändert die Anzeige dieses Kontos in der Liste.", + "s_delete_account": "Konto löschen", + "s_account_deleted": "Konto gelöscht", + "p_warning_delete_account": "Vorsicht! Das löscht das Konto von Ihrem YubiKey.", + "p_warning_disable_credential": "Sie werden keine OTPs für dieses Konto mehr erstellen können. Deaktivieren Sie diese Anmeldeinformation zuerst auf der Webseite, um nicht aus dem Konto ausgesperrt zu werden.", + "s_account_name": "Kontoname", + "s_search_accounts": "Konten durchsuchen", + "l_accounts_used": "{used} von {capacity} Konten verwendet", + "@l_accounts_used" : { + "placeholders": { + "used": {}, + "capacity": {} + } + }, + "s_num_digits": "{num} Ziffern", + "@s_num_digits" : { + "placeholders": { + "num": {} + } + }, + "s_num_sec": "{num} sek", + "@s_num_sec" : { + "placeholders": { + "num": {} + } + }, + "s_issuer_optional": "Aussteller (optional)", + "s_counter_based": "Zähler-basiert", + "s_time_based": "Zeit-basiert", + + "@_fido_credentials": {}, + "l_credential": "Anmeldeinformation: {label}", + "@l_credential" : { + "placeholders": { + "label": {} + } + }, + "s_credentials": "Anmeldeinformationen", + "l_ready_to_use": "Bereit zur Verwendung", + "l_register_sk_on_websites": "Als Sicherheitsschlüssel auf Webseiten registrieren", + "l_no_discoverable_accounts": "Keine erkennbaren Konten", + "s_delete_credential": "Anmeldeinformation löschen", + "s_credential_deleted": "Anmeldeinformation gelöscht", + "p_warning_delete_credential": "Das löscht die Anmeldeinformation von Ihrem YubiKey.", + + "@_fingerprints": {}, + "l_fingerprint": "Fingerabdruck: {label}", + "@l_fingerprint" : { + "placeholders": { + "label": {} + } + }, + "s_fingerprints": "Fingerabdrücke", + "l_fingerprint_captured": "Fingerabdruck erfolgreich aufgenommen!", + "s_fingerprint_added": "Fingerabdruck hinzugefügt", + "l_setting_name_failed": "Fehler beim Setzen des Namens: {message}", + "@l_setting_name_failed" : { + "placeholders": { + "message": {} + } + }, + "s_add_fingerprint": "Fingerabdruck hinzufügen", + "l_fp_step_1_capture": "Schritt 1/2: Fingerabdruck aufnehmen", + "l_fp_step_2_name": "Schritt 2/2: Fingerabdruck benennen", + "s_delete_fingerprint": "Fingerabdruck löschen", + "s_fingerprint_deleted": "Fingerabdruck gelöscht", + "p_warning_delete_fingerprint": "Das löscht den Fingerabdruck von Ihrem YubiKey.", + "s_no_fingerprints": "Keine Fingerabdrücke", + "l_set_pin_fingerprints": "Setzen Sie eine PIN um Fingerabdrücke zu registrieren", + "l_no_fps_added": "Es wurden keine Fingerabdrücke hinzugefügt", + "s_rename_fp": "Fingerabdruck umbenennen", + "s_fingerprint_renamed": "Fingerabdruck umbenannt", + "l_rename_fp_failed": "Fehler beim Umbenennen: {message}", + "@l_rename_fp_failed" : { + "placeholders": { + "message": {} + } + }, + "l_add_one_or_more_fps": "Fügen Sie einen oder bis zu fünf Fingerabdrücke hinzu", + "l_fingerprints_used": "{used}/5 Fingerabdrücke registriert", + "@l_fingerprints_used": { + "placeholders": { + "used": {} + } + }, + "p_press_fingerprint_begin": "Drücken Sie Ihren Finger gegen den YubiKey um zu beginnen.", + "p_will_change_label_fp": "Das ändert die Beschriftung des Fingerabdrucks.", + + "@_permissions": {}, + "s_enable_nfc": "NFC aktivieren", + "s_permission_denied": "Zugriff verweigert", + "l_elevating_permissions": "Erhöhe Berechtigungen\u2026", + "s_review_permissions": "Berechtigungen überprüfen", + "p_elevated_permissions_required": "Die Verwaltung dieses Geräts benötigt erhöhte Berechtigungen.", + "p_webauthn_elevated_permissions_required": "WebAuthn-Verwaltung benötigt erhöhte Berechtigungen.", + "p_need_camera_permission": "Yubico Authenticator benötigt Zugriff auf die Kamera um QR-Codes aufnehmen zu können.", + + "@_qr_codes": {}, + "s_qr_scan": "QR-Code aufnehmen", + "l_qr_scanned": "QR-Code aufgenommen", + "l_invalid_qr": "Ungültiger QR-Code", + "l_qr_not_found": "Kein QR-Code gefunden", + "l_qr_not_read": "Fehler beim Lesen des QR-Codes: {message}", + "@l_qr_not_read" : { + "placeholders": { + "message": {} + } + }, + "l_point_camera_scan": "Halten Sie Ihre Kamera auf einen QR-Code um ihn aufzunehmen", + "q_want_to_scan": "Möchten Sie aufnehmen?", + "q_no_qr": "Kein QR-Code?", + "s_enter_manually": "Manuell eingeben", + + "@_factory_reset": {}, + "s_reset": "Zurücksetzen", + "s_factory_reset": "Werkseinstellungen", + "l_factory_reset_this_app": "Anwendung auf Werkseinstellung zurücksetzen", + "s_reset_oath": "OATH zurücksetzen", + "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": { + "message": {} + } + }, + "p_warning_factory_reset": "Achtung! Das löscht alle OATH TOTP/HOTP Konten unwiederbringlich von Ihrem YubiKey.", + "p_warning_disable_credentials": "Ihre OATH Anmeldeinformationen und jedes gesetzte Passwort wird von diesem YubiKey entfernt. Deaktivieren Sie diese zuerst auf den zugehörigen Webseiten, um nicht aus ihren Konten ausgesperrt zu werden.", + "p_warning_deletes_accounts": "Achtung! Das löscht alle U2F und FIDO2 Konten unwiederbringlich von Ihrem YubiKey.", + "p_warning_disable_accounts": "Ihre Anmeldeinformationen und jede gesetzte PIN wird von diesem YubiKey entfernt. Deaktivieren Sie diese zuerst auf den zugehörigen Webseiten, um nicht aus ihren Konten ausgesperrt zu werden.", + + "@_copy_to_clipboard": {}, + "l_copy_to_clipboard": "In die Zwischenablage kopieren", + "s_code_copied": "Code kopiert", + "l_code_copied_clipboard": "Code in die Zwischenablage kopiert", + "s_copy_log": "Log kopiert", + "l_log_copied": "Log in die Zwischenablage kopiert", + "l_diagnostics_copied": "Diagnosedaten in die Zwischenablage kopiert", + "p_target_copied_clipboard": "{label} in die Zwischenablage kopiert.", + "@p_target_copied_clipboard" : { + "placeholders": { + "label": {} + } + }, + + "@_custom_icons": {}, + "s_custom_icons": "Eigene Icons", + "l_set_icons_for_accounts": "Icons für Konten setzen", + "p_custom_icons_description": "Icon-Pakete machen Ihre Konten mit bekannten Logos und Farben leichter unterscheidbar.", + "s_replace_icon_pack": "Icon-Paket ersetzen", + "l_loading_icon_pack": "Lade Icon-Paket\u2026", + "s_load_icon_pack": "Icon-Paket laden", + "s_remove_icon_pack": "Icon-Paket entfernen", + "l_icon_pack_removed": "Icon-Paket entfernt", + "l_remove_icon_pack_failed": "Fehler beim Entfernen des Icon-Pakets", + "s_choose_icon_pack": "Icon-Paket auswählen", + "l_icon_pack_imported": "Icon-Paket importiert", + "l_import_icon_pack_failed": "Fehler beim Importieren des Icon-Pakets: {message}", + "@l_import_icon_pack_failed": { + "placeholders": { + "message": {} + } + }, + "l_invalid_icon_pack": "Ungültiges Icon-Paket", + "l_icon_pack_copy_failed": "Kopieren der Dateien des Icon-Pakets fehlgeschlagen", + + "@_android_settings": {}, + "s_nfc_options": "NFC Optionen", + "l_on_yk_nfc_tap": "Bei YubiKey NFC-Berührung", + "l_launch_ya": "Yubico Authenticator starten", + "l_copy_otp_clipboard": "OTP in Zwischenablage kopieren", + "l_launch_and_copy_otp": "App starten und OTP kopieren", + "l_kbd_layout_for_static": "Tastaturlayout (für statisches Passwort)", + "s_choose_kbd_layout": "Tastaturlayout auswählen", + "l_bypass_touch_requirement": "Notwendigkeit zur Berührung umgehen", + "l_bypass_touch_requirement_on": "Konten, die Berührung erfordern, werden automatisch über NFC angezeigt", + "l_bypass_touch_requirement_off": "Konten, die Berührung erfordern, benötigen eine zusätzliche NFC-Berührung", + "s_silence_nfc_sounds": "NFC-Töne stummschalten", + "l_silence_nfc_sounds_on": "Keine Töne werden bei NFC-Berührung abgespielt", + "l_silence_nfc_sounds_off": "Töne werden bei NFC-Berührung abgespielt", + "s_usb_options": "USB Optionen", + "l_launch_app_on_usb": "Starten, wenn YubiKey angesteckt wird", + "l_launch_app_on_usb_on": "Das verhindert, dass andere Anwendungen den YubiKey über USB nutzen", + "l_launch_app_on_usb_off": "Andere Anwendungen können den YubiKey über USB nutzen", + "s_allow_screenshots": "Bildschirmfotos erlauben", + + "@_eof": {} +} \ No newline at end of file From 454144e73fe9265128f7856209f17b41b388fd65 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 21 Jun 2023 09:13:34 +0200 Subject: [PATCH 007/158] bump linux build system version --- .github/workflows/linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 51636de1..b22b6b34 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -10,7 +10,7 @@ jobs: PYVER: '3.11.3' FLUTTER: '3.10.1' container: - image: ubuntu:18.04 + image: ubuntu:20.04 env: DEBIAN_FRONTEND: noninteractive From f416b88ee943cf665524bb17db48721448820b21 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 21 Jun 2023 09:20:04 +0200 Subject: [PATCH 008/158] bump python version --- .github/workflows/linux.yml | 2 +- .github/workflows/macos.yml | 2 +- .github/workflows/windows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index b22b6b34..7f342d39 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest env: - PYVER: '3.11.3' + PYVER: '3.11.4' FLUTTER: '3.10.1' container: image: ubuntu:20.04 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 9042fec1..7c861b58 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -7,7 +7,7 @@ jobs: runs-on: macos-latest env: - PYVER: '3.11.3' + PYVER: '3.11.4' MACOSX_DEPLOYMENT_TARGET: "10.15" steps: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index e69583c0..7820f100 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -7,7 +7,7 @@ jobs: runs-on: windows-latest env: - PYVER: '3.11.3' + PYVER: '3.11.4' steps: - uses: actions/checkout@v3 From 2f03b7c1a7f2a34fead1dfc1cf4c0041ca27fe05 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 21 Jun 2023 09:34:54 +0200 Subject: [PATCH 009/158] install curl on linux --- .github/workflows/linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 7f342d39..24be1c42 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -20,7 +20,7 @@ jobs: export PYVER_MINOR=${PYVER%.*} echo "PYVER_MINOR: $PYVER_MINOR" apt-get update - apt-get install -qq software-properties-common libnotify-dev libayatana-appindicator3-dev patchelf + apt-get install -qq curl software-properties-common libnotify-dev libayatana-appindicator3-dev patchelf add-apt-repository -y ppa:git-core/ppa add-apt-repository -y ppa:deadsnakes/ppa apt-get install -qq git python$PYVER_MINOR-dev python$PYVER_MINOR-venv From ed47f64e5227b0713046e6d19eead90d5f3e8fdd Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 21 Jun 2023 09:51:01 +0200 Subject: [PATCH 010/158] bump flutter --- .github/workflows/android.yml | 2 +- .github/workflows/check-strings.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/linux.yml | 2 +- .github/workflows/macos.yml | 2 +- .github/workflows/windows.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index d8dde312..f861a4b1 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -17,7 +17,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.10.1' + flutter-version: '3.10.5' - run: | flutter config flutter --version diff --git a/.github/workflows/check-strings.yml b/.github/workflows/check-strings.yml index 3e0841c2..1b9045e7 100644 --- a/.github/workflows/check-strings.yml +++ b/.github/workflows/check-strings.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest env: - FLUTTER: '3.10.1' + FLUTTER: '3.10.5' steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 51b548a3..5bc24f4b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -43,7 +43,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.10.1' + flutter-version: '3.10.5' - run: | flutter config flutter --version diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 24be1c42..579da0c9 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest env: PYVER: '3.11.4' - FLUTTER: '3.10.1' + FLUTTER: '3.10.5' container: image: ubuntu:20.04 env: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 7c861b58..f3928abd 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -49,7 +49,7 @@ jobs: with: channel: 'stable' architecture: 'x64' - flutter-version: '3.10.1' + flutter-version: '3.10.5' - run: flutter config --enable-macos-desktop - run: flutter --version diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 7820f100..285f90ee 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -45,7 +45,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.10.1' + flutter-version: '3.10.5' - run: flutter config --enable-windows-desktop - run: flutter --version From 2db56315265e8f78aa26776220a3fe7cd4b611ef Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 21 Jun 2023 09:52:23 +0200 Subject: [PATCH 011/158] bump flutter packages --- pubspec.lock | 24 ++++++++++++------------ pubspec.yaml | 10 +++++----- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index f80a4033..e62a2742 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,10 +85,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "220ae4553e50d7c21a17c051afc7b183d28a24a420502e842f303f8e4e6edced" + sha256: "5e1929ad37d48bd382b124266cb8e521de5548d406a45a5ae6656c13dab73e37" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.5" build_runner_core: dependency: transitive description: @@ -221,10 +221,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: c7a8e25ca60e7f331b153b0cb3d405828f18d3e72a6fa1d9440c86556fffc877 + sha256: b1729fc96627dd44012d0a901558177418818d6bd428df59dcfeb594e5f66432 url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "5.3.2" fixnum: dependency: transitive description: @@ -286,10 +286,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "2edb9ef971d0f803860ecd9084afd48c717d002141ad77b69be3e976bee7190e" + sha256: a9520490532087cf38bf3f7de478ab6ebeb5f68bb1eb2641546d92719b224445 url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.5" freezed_annotation: dependency: "direct main" description: @@ -520,10 +520,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.7" petitparser: dependency: transitive description: @@ -615,10 +615,10 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022" + sha256: "396f85b8afc6865182610c0a2fc470853d56499f75f7499e2a73a9f0539d23d0" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" shared_preferences_android: dependency: transitive description: @@ -948,10 +948,10 @@ packages: dependency: transitive description: name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + sha256: "1414f27dd781737e51afa9711f2ac2ace6ab4498ee98e20863fa5505aa00c58c" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "5.0.4" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 70b3099d..17ae1a01 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,9 +42,9 @@ dependencies: # cupertino_icons: ^1.0.2 async: ^2.8.2 - logging: ^1.1.0 + logging: 1.1.1 collection: ^1.16.0 - shared_preferences: ^2.1.0 + shared_preferences: ^2.1.2 flutter_riverpod: ^2.3.6 json_annotation: ^4.8.1 freezed_annotation: ^2.2.0 @@ -58,7 +58,7 @@ dependencies: vector_graphics: ^1.1.5 vector_graphics_compiler: ^1.1.5 path: ^1.8.2 - file_picker: ^5.2.9 + file_picker: ^5.3.2 archive: ^3.3.2 crypto: ^3.0.2 tray_manager: ^0.2.0 @@ -78,8 +78,8 @@ dev_dependencies: # rules and activating additional ones. flutter_lints: ^2.0.1 - build_runner: ^2.3.3 - freezed: ^2.3.2 + build_runner: ^2.4.5 + freezed: ^2.3.5 json_serializable: ^6.5.4 # For information on the generic Dart part of this file, see the From ef905c05341bec2ce3706950d4e02f43dad4a432 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 21 Jun 2023 09:57:10 +0200 Subject: [PATCH 012/158] bump python packages --- helper/poetry.lock | 188 +++++++++++++++++++++--------------------- helper/pyproject.toml | 8 +- 2 files changed, 98 insertions(+), 98 deletions(-) diff --git a/helper/poetry.lock b/helper/poetry.lock index 3a5e30d5..ba9687af 100755 --- a/helper/poetry.lock +++ b/helper/poetry.lock @@ -118,31 +118,31 @@ files = [ [[package]] name = "cryptography" -version = "40.0.2" +version = "41.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b"}, - {file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440"}, - {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d"}, - {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288"}, - {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2"}, - {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b"}, - {file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9"}, - {file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c"}, - {file = "cryptography-40.0.2-cp36-abi3-win32.whl", hash = "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9"}, - {file = "cryptography-40.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b"}, - {file = "cryptography-40.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b"}, - {file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e"}, - {file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a"}, - {file = "cryptography-40.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958"}, - {file = "cryptography-40.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b"}, - {file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636"}, - {file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e"}, - {file = "cryptography-40.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404"}, - {file = "cryptography-40.0.2.tar.gz", hash = "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99"}, + {file = "cryptography-41.0.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699"}, + {file = "cryptography-41.0.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3"}, + {file = "cryptography-41.0.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db"}, + {file = "cryptography-41.0.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31"}, + {file = "cryptography-41.0.1-cp37-abi3-win32.whl", hash = "sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5"}, + {file = "cryptography-41.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5"}, + {file = "cryptography-41.0.1.tar.gz", hash = "sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006"}, ] [package.dependencies] @@ -151,12 +151,12 @@ cffi = ">=1.12" [package.extras] docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "check-manifest", "mypy", "ruff"] -sdist = ["setuptools-rust (>=0.11.4)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] -tox = ["tox"] [[package]] name = "exceptiongroup" @@ -193,14 +193,14 @@ pcsc = ["pyscard (>=1.9,<3)"] [[package]] name = "importlib-metadata" -version = "6.4.1" +version = "6.7.0" description = "Read metadata from Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "importlib_metadata-6.4.1-py3-none-any.whl", hash = "sha256:63ace321e24167d12fbb176b6015f4dbe06868c54a2af4f15849586afb9027fd"}, - {file = "importlib_metadata-6.4.1.tar.gz", hash = "sha256:eb1a7933041f0f85c94cd130258df3fb0dec060ad8c1c9318892ef4192c47ce1"}, + {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, + {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, ] [package.dependencies] @@ -209,7 +209,7 @@ zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] [[package]] name = "importlib-resources" @@ -331,52 +331,52 @@ files = [ [[package]] name = "mss" -version = "8.0.3" +version = "9.0.1" description = "An ultra fast cross-platform multiple screenshots module in pure python using ctypes." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "mss-8.0.3-py3-none-any.whl", hash = "sha256:87c1eda213dab83431013ca98ee7217e536439f28446b979bb38d8f7af5c7d34"}, - {file = "mss-8.0.3.tar.gz", hash = "sha256:07dc0602e325434e867621f257a8ec6ea14bdffd00bfa554a69bef554af7f524"}, + {file = "mss-9.0.1-py3-none-any.whl", hash = "sha256:7ee44db7ab14cbea6a3eb63813c57d677a109ca5979d3b76046e4bddd3ca1a0b"}, + {file = "mss-9.0.1.tar.gz", hash = "sha256:6eb7b9008cf27428811fa33aeb35f3334db81e3f7cc2dd49ec7c6e5a94b39f12"}, ] [[package]] name = "numpy" -version = "1.24.2" +version = "1.24.3" description = "Fundamental package for array computing in Python" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "numpy-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eef70b4fc1e872ebddc38cddacc87c19a3709c0e3e5d20bf3954c147b1dd941d"}, - {file = "numpy-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d2859428712785e8a8b7d2b3ef0a1d1565892367b32f915c4a4df44d0e64f5"}, - {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6524630f71631be2dabe0c541e7675db82651eb998496bbe16bc4f77f0772253"}, - {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a51725a815a6188c662fb66fb32077709a9ca38053f0274640293a14fdd22978"}, - {file = "numpy-1.24.2-cp310-cp310-win32.whl", hash = "sha256:2620e8592136e073bd12ee4536149380695fbe9ebeae845b81237f986479ffc9"}, - {file = "numpy-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:97cf27e51fa078078c649a51d7ade3c92d9e709ba2bfb97493007103c741f1d0"}, - {file = "numpy-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7de8fdde0003f4294655aa5d5f0a89c26b9f22c0a58790c38fae1ed392d44a5a"}, - {file = "numpy-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4173bde9fa2a005c2c6e2ea8ac1618e2ed2c1c6ec8a7657237854d42094123a0"}, - {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cecaed30dc14123020f77b03601559fff3e6cd0c048f8b5289f4eeabb0eb281"}, - {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a23f8440561a633204a67fb44617ce2a299beecf3295f0d13c495518908e910"}, - {file = "numpy-1.24.2-cp311-cp311-win32.whl", hash = "sha256:e428c4fbfa085f947b536706a2fc349245d7baa8334f0c5723c56a10595f9b95"}, - {file = "numpy-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:557d42778a6869c2162deb40ad82612645e21d79e11c1dc62c6e82a2220ffb04"}, - {file = "numpy-1.24.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d0a2db9d20117bf523dde15858398e7c0858aadca7c0f088ac0d6edd360e9ad2"}, - {file = "numpy-1.24.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c72a6b2f4af1adfe193f7beb91ddf708ff867a3f977ef2ec53c0ffb8283ab9f5"}, - {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29e6bd0ec49a44d7690ecb623a8eac5ab8a923bce0bea6293953992edf3a76a"}, - {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eabd64ddb96a1239791da78fa5f4e1693ae2dadc82a76bc76a14cbb2b966e96"}, - {file = "numpy-1.24.2-cp38-cp38-win32.whl", hash = "sha256:e3ab5d32784e843fc0dd3ab6dcafc67ef806e6b6828dc6af2f689be0eb4d781d"}, - {file = "numpy-1.24.2-cp38-cp38-win_amd64.whl", hash = "sha256:76807b4063f0002c8532cfeac47a3068a69561e9c8715efdad3c642eb27c0756"}, - {file = "numpy-1.24.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4199e7cfc307a778f72d293372736223e39ec9ac096ff0a2e64853b866a8e18a"}, - {file = "numpy-1.24.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:adbdce121896fd3a17a77ab0b0b5eedf05a9834a18699db6829a64e1dfccca7f"}, - {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889b2cc88b837d86eda1b17008ebeb679d82875022200c6e8e4ce6cf549b7acb"}, - {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f64bb98ac59b3ea3bf74b02f13836eb2e24e48e0ab0145bbda646295769bd780"}, - {file = "numpy-1.24.2-cp39-cp39-win32.whl", hash = "sha256:63e45511ee4d9d976637d11e6c9864eae50e12dc9598f531c035265991910468"}, - {file = "numpy-1.24.2-cp39-cp39-win_amd64.whl", hash = "sha256:a77d3e1163a7770164404607b7ba3967fb49b24782a6ef85d9b5f54126cc39e5"}, - {file = "numpy-1.24.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:92011118955724465fb6853def593cf397b4a1367495e0b59a7e69d40c4eb71d"}, - {file = "numpy-1.24.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9006288bcf4895917d02583cf3411f98631275bc67cce355a7f39f8c14338fa"}, - {file = "numpy-1.24.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f"}, - {file = "numpy-1.24.2.tar.gz", hash = "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22"}, + {file = "numpy-1.24.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c1104d3c036fb81ab923f507536daedc718d0ad5a8707c6061cdfd6d184e570"}, + {file = "numpy-1.24.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:202de8f38fc4a45a3eea4b63e2f376e5f2dc64ef0fa692838e31a808520efaf7"}, + {file = "numpy-1.24.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8535303847b89aa6b0f00aa1dc62867b5a32923e4d1681a35b5eef2d9591a463"}, + {file = "numpy-1.24.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d926b52ba1367f9acb76b0df6ed21f0b16a1ad87c6720a1121674e5cf63e2b6"}, + {file = "numpy-1.24.3-cp310-cp310-win32.whl", hash = "sha256:f21c442fdd2805e91799fbe044a7b999b8571bb0ab0f7850d0cb9641a687092b"}, + {file = "numpy-1.24.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f23af8c16022663a652d3b25dcdc272ac3f83c3af4c02eb8b824e6b3ab9d7"}, + {file = "numpy-1.24.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9a7721ec204d3a237225db3e194c25268faf92e19338a35f3a224469cb6039a3"}, + {file = "numpy-1.24.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d6cc757de514c00b24ae8cf5c876af2a7c3df189028d68c0cb4eaa9cd5afc2bf"}, + {file = "numpy-1.24.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76e3f4e85fc5d4fd311f6e9b794d0c00e7002ec122be271f2019d63376f1d385"}, + {file = "numpy-1.24.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1d3c026f57ceaad42f8231305d4653d5f05dc6332a730ae5c0bea3513de0950"}, + {file = "numpy-1.24.3-cp311-cp311-win32.whl", hash = "sha256:c91c4afd8abc3908e00a44b2672718905b8611503f7ff87390cc0ac3423fb096"}, + {file = "numpy-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:5342cf6aad47943286afa6f1609cad9b4266a05e7f2ec408e2cf7aea7ff69d80"}, + {file = "numpy-1.24.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7776ea65423ca6a15255ba1872d82d207bd1e09f6d0894ee4a64678dd2204078"}, + {file = "numpy-1.24.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ae8d0be48d1b6ed82588934aaaa179875e7dc4f3d84da18d7eae6eb3f06c242c"}, + {file = "numpy-1.24.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecde0f8adef7dfdec993fd54b0f78183051b6580f606111a6d789cd14c61ea0c"}, + {file = "numpy-1.24.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4749e053a29364d3452c034827102ee100986903263e89884922ef01a0a6fd2f"}, + {file = "numpy-1.24.3-cp38-cp38-win32.whl", hash = "sha256:d933fabd8f6a319e8530d0de4fcc2e6a61917e0b0c271fded460032db42a0fe4"}, + {file = "numpy-1.24.3-cp38-cp38-win_amd64.whl", hash = "sha256:56e48aec79ae238f6e4395886b5eaed058abb7231fb3361ddd7bfdf4eed54289"}, + {file = "numpy-1.24.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4719d5aefb5189f50887773699eaf94e7d1e02bf36c1a9d353d9f46703758ca4"}, + {file = "numpy-1.24.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ec87a7084caa559c36e0a2309e4ecb1baa03b687201d0a847c8b0ed476a7187"}, + {file = "numpy-1.24.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea8282b9bcfe2b5e7d491d0bf7f3e2da29700cec05b49e64d6246923329f2b02"}, + {file = "numpy-1.24.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210461d87fb02a84ef243cac5e814aad2b7f4be953b32cb53327bb49fd77fbb4"}, + {file = "numpy-1.24.3-cp39-cp39-win32.whl", hash = "sha256:784c6da1a07818491b0ffd63c6bbe5a33deaa0e25a20e1b3ea20cf0e43f8046c"}, + {file = "numpy-1.24.3-cp39-cp39-win_amd64.whl", hash = "sha256:d5036197ecae68d7f491fcdb4df90082b0d4960ca6599ba2659957aafced7c17"}, + {file = "numpy-1.24.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:352ee00c7f8387b44d19f4cada524586f07379c0d49270f87233983bc5087ca0"}, + {file = "numpy-1.24.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7d6acc2e7524c9955e5c903160aa4ea083736fde7e91276b0e5d98e6332812"}, + {file = "numpy-1.24.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:35400e6a8d102fd07c71ed7dcadd9eb62ee9a6e84ec159bd48c28235bbb0f8e4"}, + {file = "numpy-1.24.3.tar.gz", hash = "sha256:ab344f1bf21f140adab8e47fdbc7c35a477dc01408791f8ba00d018dd0bc5155"}, ] [[package]] @@ -513,24 +513,24 @@ files = [ [[package]] name = "pyinstaller" -version = "5.10.1" +version = "5.12.0" description = "PyInstaller bundles a Python application and all its dependencies into a single package." category = "dev" optional = false python-versions = "<3.12,>=3.7" files = [ - {file = "pyinstaller-5.10.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:247b99c52dc3cf69eba905da30dbca0a8ea309e1058cab44658ac838d9b8f2f0"}, - {file = "pyinstaller-5.10.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:2d16641a495593d174504263b038a6d3d46b3b15a381ccb216cf6cce67723512"}, - {file = "pyinstaller-5.10.1-py3-none-manylinux2014_i686.whl", hash = "sha256:df97aaf1103a1c485aa3c9947792a86675e370f5ce9b436b4a84e34a4180c8d2"}, - {file = "pyinstaller-5.10.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:333b4ffda38d9c0a561c38429dd9848d37aa78f3b8ea8a6f2b2e69a60d523c02"}, - {file = "pyinstaller-5.10.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:6afc7aa4885ffd3e6121a8cf2138830099f874c18cb5869bed8c1a42db82d060"}, - {file = "pyinstaller-5.10.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:85e39e36d03355423636907a26a9bfa06fdc93cb1086441b19d2d0ca448479fa"}, - {file = "pyinstaller-5.10.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:7a1db833bb0302b66ae3ae337fbd5487699658ce869ca4d538b5359b8179e83a"}, - {file = "pyinstaller-5.10.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:bb7de35cd209a0a0358aec761a273ae951d2161c03728f15d9a640d06a88e472"}, - {file = "pyinstaller-5.10.1-py3-none-win32.whl", hash = "sha256:9e9a38f41f8280c8e29b294716992852281b41fbe64ba330ebab671efe27b26d"}, - {file = "pyinstaller-5.10.1-py3-none-win_amd64.whl", hash = "sha256:915a502802c751bafd92d568ac57468ec6cdf252b8308aa9a167bbc2c565ad2d"}, - {file = "pyinstaller-5.10.1-py3-none-win_arm64.whl", hash = "sha256:f677fbc151db1eb00ada94e86ed128e7b359cbd6bf3f6ea815afdde687692d46"}, - {file = "pyinstaller-5.10.1.tar.gz", hash = "sha256:6ecc464bf56919bf2d6bff275f38d85ff08ae747b8ead3a0c26cf85573b3c723"}, + {file = "pyinstaller-5.12.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:edcb6eb6618f3b763c11487db1d3516111d54bd5598b9470e295c1f628a95496"}, + {file = "pyinstaller-5.12.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:303952c2a8ece894b655c2a0783a0bdc844282f47790707446bde3eaf355f0da"}, + {file = "pyinstaller-5.12.0-py3-none-manylinux2014_i686.whl", hash = "sha256:7eed9996c12aeee7530cbc7c57350939f46391ecf714ac176579190dbd3ec7bf"}, + {file = "pyinstaller-5.12.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:96ad645347671c9fce190506c09523c02f01a503fe3ea65f79bb0cfe22a8c83e"}, + {file = "pyinstaller-5.12.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:c3ceb6c3a34b9407ba16fb68a32f83d5fd94f21d43d9fe38d8f752feb75ca5bb"}, + {file = "pyinstaller-5.12.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:92eeacd052092a0a4368f50ddecbeb6e020b5a70cdf113243fbd6bd8ee25524e"}, + {file = "pyinstaller-5.12.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:3605ac72311318455907a88efb4a4b334b844659673a2a371bbaac2d8b52843a"}, + {file = "pyinstaller-5.12.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d14c1c2b753af5efed96584f075a6740ea634ca55789113d325dc8c32aef50fe"}, + {file = "pyinstaller-5.12.0-py3-none-win32.whl", hash = "sha256:b64d8a3056e6c7e4ed4d1f95e793ef401bf5b166ef00ad544b5812be0ac63b4b"}, + {file = "pyinstaller-5.12.0-py3-none-win_amd64.whl", hash = "sha256:62d75bb70cdbeea1a0d55067d7201efa2f7d7c19e56c241291c03d551b531684"}, + {file = "pyinstaller-5.12.0-py3-none-win_arm64.whl", hash = "sha256:2f70e2d9b032e5f24a336f41affcb4624e66a84cd863ba58f6a92bd6040653bb"}, + {file = "pyinstaller-5.12.0.tar.gz", hash = "sha256:a1c2667120730604c3ad1e0739a45bb72ca4a502a91e2f5c5b220fbfbb05f0d4"}, ] [package.dependencies] @@ -547,14 +547,14 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2023.2" +version = "2023.3" description = "Community maintained hooks for PyInstaller" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pyinstaller-hooks-contrib-2023.2.tar.gz", hash = "sha256:7fb856a81fd06a717188a3175caa77e902035cc067b00b583c6409c62497b23f"}, - {file = "pyinstaller_hooks_contrib-2023.2-py2.py3-none-any.whl", hash = "sha256:e02c5f0ee3d4f5814588c2128caf5036c058ba764aaf24d957bb5311ad8690ad"}, + {file = "pyinstaller-hooks-contrib-2023.3.tar.gz", hash = "sha256:bb39e1038e3e0972420455e0b39cd9dce73f3d80acaf4bf2b3615fea766ff370"}, + {file = "pyinstaller_hooks_contrib-2023.3-py2.py3-none-any.whl", hash = "sha256:062ad7a1746e1cfc24d3a8c4be4e606fced3b123bda7d419f14fcf7507804b07"}, ] [[package]] @@ -580,14 +580,14 @@ pyro = ["Pyro"] [[package]] name = "pytest" -version = "7.3.1" +version = "7.3.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"}, - {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"}, + {file = "pytest-7.3.2-py3-none-any.whl", hash = "sha256:cdcbd012c9312258922f8cd3f1b62a6580fdced17db6014896053d47cddf9295"}, + {file = "pytest-7.3.2.tar.gz", hash = "sha256:ee990a3cc55ba808b80795a79944756f315c67c12b56abd3ac993a7b8c17030b"}, ] [package.dependencies] @@ -599,7 +599,7 @@ pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pywin32" @@ -627,14 +627,14 @@ files = [ [[package]] name = "pywin32-ctypes" -version = "0.2.0" -description = "" +version = "0.2.1" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, - {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, + {file = "pywin32-ctypes-0.2.1.tar.gz", hash = "sha256:934a2def1e5cbc472b2b6bf80680c0f03cd87df65dfd58bfd1846969de095b03"}, + {file = "pywin32_ctypes-0.2.1-py3-none-any.whl", hash = "sha256:b9a53ef754c894a525469933ab2a447c74ec1ea6b9d2ef446f40ec50d3dcec9f"}, ] [[package]] @@ -655,19 +655,19 @@ jeepney = ">=0.6" [[package]] name = "setuptools" -version = "67.6.1" +version = "68.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, - {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -684,14 +684,14 @@ files = [ [[package]] name = "yubikey-manager" -version = "5.1.0" +version = "5.1.1" description = "Tool for managing your YubiKey configuration." category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "yubikey_manager-5.1.0-py3-none-any.whl", hash = "sha256:72ac412319ee9c9db13173a68326de11478f1e8b3ed13b25bb3d33157b3f958e"}, - {file = "yubikey_manager-5.1.0.tar.gz", hash = "sha256:d33efc9f82e511fd4d7c9397f6c40b37c7260221ca06fac93daeb4a46b1eb173"}, + {file = "yubikey_manager-5.1.1-py3-none-any.whl", hash = "sha256:67291f1d9396d99845b710eabfb4b5ba41b5fa6cc0011104267f91914c1867e3"}, + {file = "yubikey_manager-5.1.1.tar.gz", hash = "sha256:684102affd4a0d29611756da263c22f8e67226e80f65c5460c8c5608f9c0d58d"}, ] [package.dependencies] @@ -750,4 +750,4 @@ numpy = "*" [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "f0fc2e7d5ef423dc8b247ab6b968a63c331e78bd74bd72020b634f6823a74e3d" +content-hash = "0ada1b4785281f6f18fb483a1d84504be84adc84aec8c8c7812cc92ea44656d8" diff --git a/helper/pyproject.toml b/helper/pyproject.toml index 296b3ff6..3b65a5b3 100644 --- a/helper/pyproject.toml +++ b/helper/pyproject.toml @@ -10,14 +10,14 @@ packages = [ [tool.poetry.dependencies] python = "^3.8" -yubikey-manager = "5.1.0" -mss = "^8.0.3" +yubikey-manager = "5.1.1" +mss = "^9.0.1" zxing-cpp = "^2.0.0" Pillow = "^9.5.0" [tool.poetry.dev-dependencies] -pyinstaller = {version = "^5.10.1", python = "<3.12"} -pytest = "^7.3.1" +pyinstaller = {version = "^5.12.0", python = "<3.12"} +pytest = "^7.3.2" [build-system] requires = ["poetry-core>=1.0.0"] From 36471f2776a151f6c6eed33d20c52b360929ca2c Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 21 Jun 2023 09:59:58 +0200 Subject: [PATCH 013/158] bump android dependencies --- android/app/build.gradle | 2 +- android/build.gradle | 2 +- android/flutter_plugins/qrscanner_zxing/android/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d1d735d0..aaa45e2c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -100,7 +100,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation "androidx.core:core-ktx:1.10.1" - implementation 'androidx.fragment:fragment-ktx:1.5.7' + implementation 'androidx.fragment:fragment-ktx:1.6.0' implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'com.google.android.material:material:1.9.0' diff --git a/android/build.gradle b/android/build.gradle index c91fe98b..16b2224e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,7 +26,7 @@ allprojects { yubiKitVersion = "2.3.0" junitVersion = "4.13.2" - mockitoVersion = "5.3.1" + mockitoVersion = "5.4.0" } } diff --git a/android/flutter_plugins/qrscanner_zxing/android/build.gradle b/android/flutter_plugins/qrscanner_zxing/android/build.gradle index c472236c..14a4bcb2 100644 --- a/android/flutter_plugins/qrscanner_zxing/android/build.gradle +++ b/android/flutter_plugins/qrscanner_zxing/android/build.gradle @@ -49,7 +49,7 @@ android { } dependencies { - def camerax_version = "1.2.2" + def camerax_version = "1.2.3" implementation "androidx.camera:camera-lifecycle:${camerax_version}" implementation "androidx.camera:camera-view:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}" From 94dc10a72039c2209d394382ccef92e13c579370 Mon Sep 17 00:00:00 2001 From: Joakim Troeng Date: Fri, 30 Jun 2023 15:32:10 +0200 Subject: [PATCH 014/158] folders --- .../{approved_yubikeys.dart => _approved_yubikeys.dart} | 8 +++++++- integration_test/management_test.dart | 4 ++-- integration_test/oath_test.dart | 4 ++-- integration_test/{ => utils}/android/test_driver.dart | 0 integration_test/{ => utils}/android/util.dart | 0 integration_test/{ => utils}/desktop/util.dart | 0 integration_test/{ => utils}/oath_test_util.dart | 2 +- integration_test/{ => utils}/test_util.dart | 6 +++--- 8 files changed, 15 insertions(+), 9 deletions(-) rename integration_test/{approved_yubikeys.dart => _approved_yubikeys.dart} (81%) rename integration_test/{ => utils}/android/test_driver.dart (100%) rename integration_test/{ => utils}/android/util.dart (100%) rename integration_test/{ => utils}/desktop/util.dart (100%) rename integration_test/{ => utils}/oath_test_util.dart (99%) rename integration_test/{ => utils}/test_util.dart (98%) diff --git a/integration_test/approved_yubikeys.dart b/integration_test/_approved_yubikeys.dart similarity index 81% rename from integration_test/approved_yubikeys.dart rename to integration_test/_approved_yubikeys.dart index 7712e142..c2da89d1 100644 --- a/integration_test/approved_yubikeys.dart +++ b/integration_test/_approved_yubikeys.dart @@ -15,4 +15,10 @@ */ /// list of YubiKey serial numbers which are approved to be used with integration tests -var approvedYubiKeys = []; +var approvedYubiKeys = [ + '11790010', + '13820900', + '13820901', +]; + +///var approvedYubiKeys = []; approvedYubikeys.add = '13820900'; \ No newline at end of file diff --git a/integration_test/management_test.dart b/integration_test/management_test.dart index b661c980..6b16a3be 100644 --- a/integration_test/management_test.dart +++ b/integration_test/management_test.dart @@ -14,14 +14,14 @@ * limitations under the License. */ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; import 'package:integration_test/integration_test.dart'; import 'package:yubico_authenticator/app/views/keys.dart' as app_keys; import 'package:yubico_authenticator/management/views/keys.dart' as management_keys; -import 'test_util.dart'; +import 'utils/test_util.dart'; Key _getCapabilityWidgetKey(bool isUsb, String name) => Key('management.keys.capability.${isUsb ? 'usb' : 'nfc'}.$name'); diff --git a/integration_test/oath_test.dart b/integration_test/oath_test.dart index c90b5d14..28c423d4 100644 --- a/integration_test/oath_test.dart +++ b/integration_test/oath_test.dart @@ -19,8 +19,8 @@ import 'package:integration_test/integration_test.dart'; import 'package:yubico_authenticator/core/state.dart'; import 'package:yubico_authenticator/oath/keys.dart' as keys; -import 'oath_test_util.dart'; -import 'test_util.dart'; +import 'utils/oath_test_util.dart'; +import 'utils/test_util.dart'; void main() { var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/integration_test/android/test_driver.dart b/integration_test/utils/android/test_driver.dart similarity index 100% rename from integration_test/android/test_driver.dart rename to integration_test/utils/android/test_driver.dart diff --git a/integration_test/android/util.dart b/integration_test/utils/android/util.dart similarity index 100% rename from integration_test/android/util.dart rename to integration_test/utils/android/util.dart diff --git a/integration_test/desktop/util.dart b/integration_test/utils/desktop/util.dart similarity index 100% rename from integration_test/desktop/util.dart rename to integration_test/utils/desktop/util.dart diff --git a/integration_test/oath_test_util.dart b/integration_test/utils/oath_test_util.dart similarity index 99% rename from integration_test/oath_test_util.dart rename to integration_test/utils/oath_test_util.dart index ed8844b6..4847524b 100644 --- a/integration_test/oath_test_util.dart +++ b/integration_test/utils/oath_test_util.dart @@ -22,7 +22,7 @@ import 'package:yubico_authenticator/oath/views/account_list.dart'; import 'package:yubico_authenticator/oath/views/account_view.dart'; import 'android/util.dart'; -import 'test_util.dart'; +import '../utils/test_util.dart'; class Account { final String? issuer; diff --git a/integration_test/test_util.dart b/integration_test/utils/test_util.dart similarity index 98% rename from integration_test/test_util.dart rename to integration_test/utils/test_util.dart index ea9333e8..714f79ff 100644 --- a/integration_test/test_util.dart +++ b/integration_test/utils/test_util.dart @@ -24,11 +24,11 @@ import 'package:yubico_authenticator/core/state.dart'; import 'package:yubico_authenticator/management/views/keys.dart'; import 'android/util.dart' as android_test_util; -import 'approved_yubikeys.dart'; +import '../_approved_yubikeys.dart'; import 'desktop/util.dart' as desktop_test_util; -const shortWaitMs = 10; -const longWaitMs = 50; +const shortWaitMs = 500; +const longWaitMs = 500; /// information about YubiKey as seen by the app String? yubiKeyName; From a8d693a50a0831dbbc8cf9575e16fbfa55f921d7 Mon Sep 17 00:00:00 2001 From: Joakim Troeng Date: Fri, 30 Jun 2023 15:35:50 +0200 Subject: [PATCH 015/158] removed approved sers. --- integration_test/_approved_yubikeys.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/integration_test/_approved_yubikeys.dart b/integration_test/_approved_yubikeys.dart index c2da89d1..d635907d 100644 --- a/integration_test/_approved_yubikeys.dart +++ b/integration_test/_approved_yubikeys.dart @@ -16,9 +16,7 @@ /// list of YubiKey serial numbers which are approved to be used with integration tests var approvedYubiKeys = [ - '11790010', - '13820900', - '13820901', + '', ]; ///var approvedYubiKeys = []; approvedYubikeys.add = '13820900'; \ No newline at end of file From 7b45236fac3b7f016e59b157c30181b01c0206de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Tro=C3=ABng?= Date: Fri, 30 Jun 2023 15:50:47 +0200 Subject: [PATCH 016/158] cleaning up _approved_yubikeys.dart --- integration_test/_approved_yubikeys.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/integration_test/_approved_yubikeys.dart b/integration_test/_approved_yubikeys.dart index d635907d..fdc17899 100644 --- a/integration_test/_approved_yubikeys.dart +++ b/integration_test/_approved_yubikeys.dart @@ -18,5 +18,3 @@ var approvedYubiKeys = [ '', ]; - -///var approvedYubiKeys = []; approvedYubikeys.add = '13820900'; \ No newline at end of file From fbe3cab253fff65bcf6062af97c9a3a9584ae5cd Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 2 May 2023 14:55:18 +0200 Subject: [PATCH 017/158] Revamp OATH key actions and account dialogs. --- lib/app/views/fs_dialog.dart | 49 ++++++++ lib/l10n/app_en.arb | 6 + lib/oath/views/account_dialog.dart | 119 +++++++----------- lib/oath/views/account_helper.dart | 17 +-- lib/oath/views/account_view.dart | 8 +- lib/oath/views/key_actions.dart | 196 ++++++++++++++++------------- 6 files changed, 223 insertions(+), 172 deletions(-) create mode 100644 lib/app/views/fs_dialog.dart diff --git a/lib/app/views/fs_dialog.dart b/lib/app/views/fs_dialog.dart new file mode 100644 index 00000000..0aaa45de --- /dev/null +++ b/lib/app/views/fs_dialog.dart @@ -0,0 +1,49 @@ +/* + * 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'; + +class FsDialog extends StatelessWidget { + final Widget child; + const FsDialog({required this.child, super.key}); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Dialog.fullscreen( + backgroundColor: Theme.of(context).colorScheme.background.withAlpha(100), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: SingleChildScrollView(child: child), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: TextButton.icon( + icon: const Icon(Icons.close), + label: Text(l10n.s_close), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ) + ], + ), + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2677935a..a2fbb2f6 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -251,10 +251,13 @@ "s_pin_account": "Pin account", "s_unpin_account": "Unpin account", "s_no_pinned_accounts": "No pinned accounts", + "l_pin_account_desc": "Keep your important accounts together", "s_rename_account": "Rename account", + "l_rename_account_desc": "Edit the issuer/name of the account", "s_account_renamed": "Account renamed", "p_rename_will_change_account_displayed": "This will change how the account is displayed in the list.", "s_delete_account": "Delete account", + "l_delete_account_desc": "Remove the account from your YubiKey", "s_account_deleted": "Account deleted", "p_warning_delete_account": "Warning! This action will delete the account from your YubiKey.", "p_warning_disable_credential": "You will no longer be able to generate OTPs for this account. Make sure to first disable this credential from the website to avoid being locked out of your account.", @@ -282,6 +285,9 @@ "s_issuer_optional": "Issuer (optional)", "s_counter_based": "Counter based", "s_time_based": "Time based", + "l_copy_code_desc": "Easily paste the code into another app", + "s_calculate_code": "Calculate code", + "l_calculate_code_desc": "Get a new code from your YubiKey", "@_fido_credentials": {}, "l_credential": "Credential: {label}", diff --git a/lib/oath/views/account_dialog.dart b/lib/oath/views/account_dialog.dart index ac26cba8..f432872e 100755 --- a/lib/oath/views/account_dialog.dart +++ b/lib/oath/views/account_dialog.dart @@ -23,8 +23,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../app/message.dart'; import '../../app/shortcuts.dart'; import '../../app/state.dart'; +import '../../app/views/fs_dialog.dart'; import '../../core/models.dart'; import '../../core/state.dart'; +import '../../widgets/list_title.dart'; import '../models.dart'; import '../state.dart'; import 'account_helper.dart'; @@ -64,29 +66,21 @@ class AccountDialog extends ConsumerWidget { final intent = e.intent; final (firstColor, secondColor) = colors[e] ?? (theme.secondary, theme.onSecondary); - final tooltip = e.trailing != null ? '${e.text}\n${e.trailing}' : e.text; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: CircleAvatar( - backgroundColor: intent != null ? firstColor : theme.secondary, + return ListTile( + leading: CircleAvatar( + backgroundColor: + intent != null ? firstColor : theme.secondary.withOpacity(0.2), foregroundColor: secondColor, - child: IconButton( - style: IconButton.styleFrom( - backgroundColor: intent != null ? firstColor : theme.secondary, - foregroundColor: secondColor, - disabledBackgroundColor: theme.onSecondary.withOpacity(0.2), - fixedSize: const Size.square(38), - ), - icon: e.icon, - iconSize: 22, - tooltip: tooltip, - onPressed: intent != null - ? () { - Actions.invoke(context, intent); - } - : null, - ), + //disabledBackgroundColor: theme.onSecondary.withOpacity(0.2), + child: e.icon, ), + title: Text(e.text), + subtitle: e.trailing != null ? Text(e.trailing!) : null, + onTap: intent != null + ? () { + Actions.invoke(context, intent); + } + : null, ); }).toList(); } @@ -168,20 +162,33 @@ class AccountDialog extends ConsumerWidget { } return FocusScope( autofocus: true, - child: AlertDialog( - title: Center( - child: Text( - helper.title, - style: Theme.of(context).textTheme.headlineSmall, - softWrap: true, - textAlign: TextAlign.center, - ), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 12.0), - content: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, + child: FsDialog( + child: Column( children: [ + Padding( + padding: const EdgeInsets.only(top: 48, bottom: 16), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconTheme( + data: IconTheme.of(context).copyWith(size: 24), + child: helper.buildCodeIcon(), + ), + const SizedBox(width: 8.0), + DefaultTextStyle.merge( + style: const TextStyle(fontSize: 28), + child: helper.buildCodeLabel(), + ), + ], + ), + ), + Text( + helper.title, + style: Theme.of(context).textTheme.headlineSmall, + softWrap: true, + textAlign: TextAlign.center, + ), if (subtitle != null) Text( subtitle, @@ -192,48 +199,12 @@ class AccountDialog extends ConsumerWidget { color: Theme.of(context).textTheme.bodySmall!.color, ), ), - const SizedBox(height: 12.0), - DecoratedBox( - decoration: BoxDecoration( - shape: BoxShape.rectangle, - color: Theme.of(context).colorScheme.surfaceVariant, - borderRadius: const BorderRadius.all(Radius.circular(30.0)), - ), - child: Center( - child: FittedBox( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, vertical: 8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconTheme( - data: IconTheme.of(context).copyWith(size: 24), - child: helper.buildCodeIcon(), - ), - const SizedBox(width: 8.0), - DefaultTextStyle.merge( - style: const TextStyle(fontSize: 28), - child: helper.buildCodeLabel(), - ), - ], - ), - ), - ), - ), - ), + const SizedBox(height: 32), + ListTitle('Actions', + textStyle: Theme.of(context).textTheme.bodyLarge), + ..._buildActions(context, helper), ], ), - actionsPadding: const EdgeInsets.symmetric(vertical: 10.0), - actions: [ - Center( - child: FittedBox( - alignment: Alignment.center, - child: Row(children: _buildActions(context, helper)), - ), - ) - ], ), ); }, diff --git a/lib/oath/views/account_helper.dart b/lib/oath/views/account_helper.dart index 283ae3b1..79ca0299 100755 --- a/lib/oath/views/account_helper.dart +++ b/lib/oath/views/account_helper.dart @@ -61,38 +61,41 @@ class AccountHelper { credential.touchRequired || credential.oathType == OathType.hotp; final ready = expired || credential.oathType == OathType.hotp; final pinned = _ref.watch(favoritesProvider).contains(credential.id); - final l10n = AppLocalizations.of(_context)!; - final shortcut = Platform.isMacOS ? '\u2318 C' : 'Ctrl+C'; + return [ MenuAction( - text: l10n.l_copy_to_clipboard, icon: const Icon(Icons.copy), + text: l10n.l_copy_to_clipboard, + trailing: l10n.l_copy_code_desc, intent: code == null || expired ? null : const CopyIntent(), - trailing: shortcut, ), if (manual) MenuAction( - text: l10n.s_calculate, icon: const Icon(Icons.refresh), + text: l10n.s_calculate, + trailing: l10n.l_calculate_code_desc, intent: ready ? const CalculateIntent() : null, ), MenuAction( - text: pinned ? l10n.s_unpin_account : l10n.s_pin_account, icon: pinned ? pushPinStrokeIcon : const Icon(Icons.push_pin_outlined), + text: pinned ? l10n.s_unpin_account : l10n.s_pin_account, + trailing: l10n.l_pin_account_desc, intent: const TogglePinIntent(), ), if (data.info.version.isAtLeast(5, 3)) MenuAction( icon: const Icon(Icons.edit_outlined), text: l10n.s_rename_account, + trailing: l10n.l_rename_account_desc, intent: const EditIntent(), ), MenuAction( - text: l10n.s_delete_account, icon: const Icon(Icons.delete_outline), + text: l10n.s_delete_account, + trailing: l10n.l_delete_account_desc, intent: const DeleteIntent(), ), ]; diff --git a/lib/oath/views/account_view.dart b/lib/oath/views/account_view.dart index 92670559..a0bb6b8f 100755 --- a/lib/oath/views/account_view.dart +++ b/lib/oath/views/account_view.dart @@ -14,10 +14,13 @@ * limitations under the License. */ +import 'dart:io'; + import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../app/message.dart'; import '../../app/shortcuts.dart'; @@ -89,6 +92,9 @@ class _AccountViewState extends ConsumerState { List _buildPopupMenu( BuildContext context, AccountHelper helper) { + final shortcut = Platform.isMacOS ? '\u2318 C' : 'Ctrl+C'; + final copyText = AppLocalizations.of(context)!.l_copy_to_clipboard; + return helper.buildActions().map((e) { final intent = e.intent; return buildMenuItem( @@ -99,7 +105,7 @@ class _AccountViewState extends ConsumerState { Actions.invoke(context, intent); } : null, - trailing: e.trailing, + trailing: e.text == copyText ? shortcut : null, ); }).toList(); } diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index 9578e125..4e70f0d6 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -22,6 +22,7 @@ import 'package:yubico_authenticator/oath/icon_provider/icon_pack_dialog.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; +import '../../app/views/fs_dialog.dart'; import '../../core/state.dart'; import '../../exception/cancellation_exception.dart'; import '../../widgets/list_title.dart'; @@ -41,100 +42,115 @@ Widget oathBuildActions( }) { final l10n = AppLocalizations.of(context)!; final capacity = oathState.version.isAtLeast(4) ? 32 : null; - final theme = Theme.of(context).colorScheme; - return SimpleDialog( - children: [ - ListTitle(l10n.s_setup, textStyle: Theme.of(context).textTheme.bodyLarge), - ListTile( - title: Text(l10n.s_add_account), - key: keys.addAccountAction, - leading: - const CircleAvatar(child: Icon(Icons.person_add_alt_1_outlined)), - subtitle: Text(used == null - ? l10n.l_unlock_first - : (capacity != null ? l10n.l_accounts_used(used, capacity) : '')), - enabled: used != null && (capacity == null || capacity > used), - onTap: used != null && (capacity == null || capacity > used) - ? () async { - final credentials = ref.read(credentialsProvider); - final withContext = ref.read(withContextProvider); - Navigator.of(context).pop(); - CredentialData? otpauth; - if (isAndroid) { - final scanner = ref.read(qrScannerProvider); - if (scanner != null) { - try { - final url = await scanner.scanQr(); - if (url != null) { - otpauth = CredentialData.fromUri(Uri.parse(url)); + //final theme = Theme.of(context).colorScheme; + final theme = + ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; + return FsDialog( + child: Column( + children: [ + ListTitle(l10n.s_setup, + textStyle: Theme.of(context).textTheme.bodyLarge), + ListTile( + title: Text(l10n.s_add_account), + key: keys.addAccountAction, + leading: CircleAvatar( + backgroundColor: theme.primary, + foregroundColor: theme.onPrimary, + child: const Icon(Icons.person_add_alt_1_outlined), + ), + subtitle: Text(used == null + ? l10n.l_unlock_first + : (capacity != null ? l10n.l_accounts_used(used, capacity) : '')), + enabled: used != null && (capacity == null || capacity > used), + onTap: used != null && (capacity == null || capacity > used) + ? () async { + final credentials = ref.read(credentialsProvider); + final withContext = ref.read(withContextProvider); + Navigator.of(context).pop(); + CredentialData? otpauth; + if (isAndroid) { + final scanner = ref.read(qrScannerProvider); + if (scanner != null) { + try { + final url = await scanner.scanQr(); + if (url != null) { + otpauth = CredentialData.fromUri(Uri.parse(url)); + } + } on CancellationException catch (_) { + // ignored - user cancelled + return; } - } on CancellationException catch (_) { - // ignored - user cancelled - return; } } + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => OathAddAccountPage( + devicePath, + oathState, + credentials: credentials, + credentialData: otpauth, + ), + ); + }); } - await withContext((context) async { - await showBlurDialog( + : null, + ), + ListTitle(l10n.s_manage, + textStyle: Theme.of(context).textTheme.bodyLarge), + ListTile( + key: keys.customIconsAction, + title: Text(l10n.s_custom_icons), + subtitle: Text(l10n.l_set_icons_for_accounts), + leading: CircleAvatar( + backgroundColor: theme.secondary, + foregroundColor: theme.onSecondary, + child: const Icon(Icons.image_outlined), + ), + onTap: () async { + Navigator.of(context).pop(); + await ref.read(withContextProvider)((context) => showBlurDialog( context: context, - builder: (context) => OathAddAccountPage( - devicePath, - oathState, - credentials: credentials, - credentialData: otpauth, - ), - ); - }); - } - : null, - ), - ListTitle(l10n.s_manage, - textStyle: Theme.of(context).textTheme.bodyLarge), - ListTile( - key: keys.customIconsAction, - title: Text(l10n.s_custom_icons), - subtitle: Text(l10n.l_set_icons_for_accounts), - leading: const CircleAvatar( - child: Icon(Icons.image_outlined), - ), - onTap: () async { - Navigator.of(context).pop(); - await ref.read(withContextProvider)((context) => showBlurDialog( - context: context, - routeSettings: - const RouteSettings(name: 'oath_icon_pack_dialog'), - builder: (context) => const IconPackDialog(), - )); - }), - ListTile( - key: keys.setOrManagePasswordAction, - title: Text( - oathState.hasKey ? l10n.s_manage_password : l10n.s_set_password), - subtitle: Text(l10n.l_optional_password_protection), - leading: const CircleAvatar(child: Icon(Icons.password_outlined)), - onTap: () { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => ManagePasswordDialog(devicePath, oathState), - ); - }), - ListTile( - key: keys.resetAction, - title: Text(l10n.s_reset_oath), - subtitle: Text(l10n.l_factory_reset_this_app), - leading: CircleAvatar( - foregroundColor: theme.onError, - backgroundColor: theme.error, - child: const Icon(Icons.delete_outline), - ), - onTap: () { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => ResetDialog(devicePath), - ); - }), - ], + routeSettings: + const RouteSettings(name: 'oath_icon_pack_dialog'), + builder: (context) => const IconPackDialog(), + )); + }), + ListTile( + key: keys.setOrManagePasswordAction, + title: Text(oathState.hasKey + ? l10n.s_manage_password + : l10n.s_set_password), + subtitle: Text(l10n.l_optional_password_protection), + leading: CircleAvatar( + backgroundColor: theme.secondary, + foregroundColor: theme.onSecondary, + child: const Icon(Icons.password_outlined)), + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => + ManagePasswordDialog(devicePath, oathState), + ); + }), + ListTile( + key: keys.resetAction, + title: Text(l10n.s_reset_oath), + subtitle: Text(l10n.l_factory_reset_this_app), + leading: CircleAvatar( + foregroundColor: theme.onError, + backgroundColor: theme.error, + child: const Icon(Icons.delete_outline), + ), + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ResetDialog(devicePath), + ); + }), + ], + ), ); } From 16f6732f09502cbe1e71ee3823c2bc46d506415b Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 3 May 2023 21:20:08 +0200 Subject: [PATCH 018/158] Convert StateNotifiers to AsyncNotifiers for App states. --- lib/android/management/state.dart | 23 ++++----- lib/android/oath/state.dart | 30 ++++++----- lib/core/state.dart | 15 +++--- lib/desktop/fido/state.dart | 59 +++++++++++----------- lib/desktop/management/state.dart | 80 +++++++++++++++--------------- lib/desktop/oath/state.dart | 74 ++++++++++++--------------- lib/fido/state.dart | 6 +-- lib/management/state.dart | 6 +-- lib/oath/state.dart | 6 +-- lib/oath/views/account_helper.dart | 1 - lib/oath/views/key_actions.dart | 1 - 11 files changed, 141 insertions(+), 160 deletions(-) diff --git a/lib/android/management/state.dart b/lib/android/management/state.dart index 4438e516..4836013e 100755 --- a/lib/android/management/state.dart +++ b/lib/android/management/state.dart @@ -23,22 +23,19 @@ import '../../app/models.dart'; import '../../app/state.dart'; import '../../management/state.dart'; -final androidManagementState = StateNotifierProvider.autoDispose - .family, DevicePath>( - (ref, devicePath) { - // Make sure to rebuild if currentDevice changes (as on reboot) - ref.watch(currentDeviceProvider); - final notifier = _AndroidManagementStateNotifier(ref); - return notifier..refresh(); - }, +final androidManagementState = AsyncNotifierProvider.autoDispose + .family( + _AndroidManagementStateNotifier.new, ); class _AndroidManagementStateNotifier extends ManagementStateNotifier { - final Ref _ref; + @override + FutureOr build(DevicePath devicePath) { + // Make sure to rebuild if currentDevice changes (as on reboot) + ref.watch(currentDeviceProvider); - _AndroidManagementStateNotifier(this._ref) : super(); - - void refresh() async {} + return Completer().future; + } @override Future setMode( @@ -55,6 +52,6 @@ class _AndroidManagementStateNotifier extends ManagementStateNotifier { state = const AsyncValue.loading(); } - _ref.read(attachedDevicesProvider.notifier).refresh(); + ref.read(attachedDevicesProvider.notifier).refresh(); } } diff --git a/lib/android/oath/state.dart b/lib/android/oath/state.dart index e2c448ef..a8ee45e3 100755 --- a/lib/android/oath/state.dart +++ b/lib/android/oath/state.dart @@ -36,33 +36,31 @@ final _log = Logger('android.oath.state'); const _methods = MethodChannel('android.oath.methods'); -final androidOathStateProvider = StateNotifierProvider.autoDispose - .family, DevicePath>( - (ref, devicePath) => _AndroidOathStateNotifier()); +final androidOathStateProvider = AsyncNotifierProvider.autoDispose + .family( + _AndroidOathStateNotifier.new); class _AndroidOathStateNotifier extends OathStateNotifier { final _events = const EventChannel('android.oath.sessionState'); late StreamSubscription _sub; - _AndroidOathStateNotifier() : super() { + + @override + FutureOr build(DevicePath arg) { _sub = _events.receiveBroadcastStream().listen((event) { final json = jsonDecode(event); - if (mounted) { - if (json == null) { - state = const AsyncValue.loading(); - } else { - final oathState = OathState.fromJson(json); - state = AsyncValue.data(oathState); - } + if (json == null) { + state = const AsyncValue.loading(); + } else { + final oathState = OathState.fromJson(json); + state = AsyncValue.data(oathState); } }, onError: (err, stackTrace) { state = AsyncValue.error(err, stackTrace); }); - } - @override - void dispose() { - _sub.cancel(); - super.dispose(); + ref.onDispose(_sub.cancel); + + return Completer().future; } @override diff --git a/lib/core/state.dart b/lib/core/state.dart index 6dd6f6ec..30daaa1e 100644 --- a/lib/core/state.dart +++ b/lib/core/state.dart @@ -18,6 +18,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../app/models.dart'; + bool get isDesktop { return const [ TargetPlatform.windows, @@ -36,21 +38,16 @@ final prefProvider = Provider((ref) { }); abstract class ApplicationStateNotifier - extends StateNotifier> { - ApplicationStateNotifier() : super(const AsyncValue.loading()); + extends AutoDisposeFamilyAsyncNotifier { + ApplicationStateNotifier() : super(); @protected Future updateState(Future Function() guarded) async { - final result = await AsyncValue.guard(guarded); - if (mounted) { - state = result; - } + state = await AsyncValue.guard(guarded); } @protected void setData(T value) { - if (mounted) { - state = AsyncValue.data(value); - } + state = AsyncValue.data(value); } } diff --git a/lib/desktop/fido/state.dart b/lib/desktop/fido/state.dart index c2cdacdb..cfa88bc5 100755 --- a/lib/desktop/fido/state.dart +++ b/lib/desktop/fido/state.dart @@ -45,47 +45,42 @@ final _sessionProvider = }, ); -final desktopFidoState = StateNotifierProvider.autoDispose - .family, DevicePath>( - (ref, devicePath) { - final session = ref.watch(_sessionProvider(devicePath)); +final desktopFidoState = AsyncNotifierProvider.autoDispose + .family( + _DesktopFidoStateNotifier.new); + +class _DesktopFidoStateNotifier extends FidoStateNotifier { + late RpcNodeSession _session; + late StateController _pinController; + + @override + FutureOr build(DevicePath devicePath) async { + _session = ref.watch(_sessionProvider(devicePath)); if (Platform.isWindows) { // Make sure to rebuild if isAdmin changes ref.watch(rpcStateProvider.select((state) => state.isAdmin)); } - final notifier = _DesktopFidoStateNotifier( - session, - ref.watch(_pinProvider(devicePath).notifier), - ); - session.setErrorHandler('state-reset', (_) async { + _pinController = ref.watch(_pinProvider(devicePath).notifier); + _session.setErrorHandler('state-reset', (_) async { ref.invalidate(_sessionProvider(devicePath)); }); - session.setErrorHandler('auth-required', (_) async { + _session.setErrorHandler('auth-required', (_) async { final pin = ref.read(_pinProvider(devicePath)); if (pin != null) { - await notifier.unlock(pin); + await unlock(pin); } }); ref.onDispose(() { - session.unsetErrorHandler('auth-required'); + _session.unsetErrorHandler('auth-required'); }); ref.onDispose(() { - session.unsetErrorHandler('state-reset'); + _session.unsetErrorHandler('state-reset'); }); - return notifier..refresh(); - }, -); -class _DesktopFidoStateNotifier extends FidoStateNotifier { - final RpcNodeSession _session; - final StateController _pinController; - _DesktopFidoStateNotifier(this._session, this._pinController) : super(); - - Future refresh() => updateState(() async { - final result = await _session.command('get'); - _log.debug('application status', jsonEncode(result)); - return FidoState.fromJson(result['data']); - }); + final result = await _session.command('get'); + _log.debug('application status', jsonEncode(result)); + return FidoState.fromJson(result['data']); + } @override Stream reset() { @@ -105,8 +100,8 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier { controller.onListen = () async { try { await _session.command('reset', signal: signaler); - await refresh(); await controller.sink.close(); + ref.invalidateSelf(); } catch (e) { controller.sink.addError(e); } @@ -155,16 +150,19 @@ final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family< FidoFingerprintsNotifier, AsyncValue>, DevicePath>( (ref, devicePath) => _DesktopFidoFingerprintsNotifier( ref.watch(_sessionProvider(devicePath)), + ref, )); class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier { final RpcNodeSession _session; + final Ref _ref; - _DesktopFidoFingerprintsNotifier(this._session) { + _DesktopFidoFingerprintsNotifier(this._session, this._ref) { _refresh(); } Future _refresh() async { + _ref.invalidate(fidoStateProvider(_session.devicePath)); final result = await _session.command('fingerprints'); setItems((result['children'] as Map) .entries @@ -236,12 +234,14 @@ final desktopCredentialProvider = StateNotifierProvider.autoDispose.family< FidoCredentialsNotifier, AsyncValue>, DevicePath>( (ref, devicePath) => _DesktopFidoCredentialsNotifier( ref.watch(_sessionProvider(devicePath)), + ref, )); class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier { final RpcNodeSession _session; + final Ref _ref; - _DesktopFidoCredentialsNotifier(this._session) { + _DesktopFidoCredentialsNotifier(this._session, this._ref) { _refresh(); } @@ -259,6 +259,7 @@ class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier { } } setItems(creds); + _ref.invalidate(fidoStateProvider(_session.devicePath)); } @override diff --git a/lib/desktop/management/state.dart b/lib/desktop/management/state.dart index 9683b425..298c75dd 100755 --- a/lib/desktop/management/state.dart +++ b/lib/desktop/management/state.dart @@ -36,53 +36,51 @@ final _sessionProvider = RpcNodeSession(ref.watch(rpcProvider).requireValue, devicePath, []), ); -final desktopManagementState = StateNotifierProvider.autoDispose - .family, DevicePath>( - (ref, devicePath) { +final desktopManagementState = AsyncNotifierProvider.autoDispose + .family( + _DesktopManagementStateNotifier.new); + +class _DesktopManagementStateNotifier extends ManagementStateNotifier { + late RpcNodeSession _session; + List _subpath = []; + _DesktopManagementStateNotifier() : super(); + + @override + FutureOr build(DevicePath devicePath) async { // Make sure to rebuild if currentDevice changes (as on reboot) ref.watch(currentDeviceProvider); - final session = ref.watch(_sessionProvider(devicePath)); - final notifier = _DesktopManagementStateNotifier(ref, session); - session.setErrorHandler('state-reset', (_) async { + + _session = ref.watch(_sessionProvider(devicePath)); + _session.setErrorHandler('state-reset', (_) async { ref.invalidate(_sessionProvider(devicePath)); }); ref.onDispose(() { - session.unsetErrorHandler('state-reset'); + _session.unsetErrorHandler('state-reset'); }); - return notifier..refresh(); - }, -); -class _DesktopManagementStateNotifier extends ManagementStateNotifier { - final Ref _ref; - final RpcNodeSession _session; - List _subpath = []; - _DesktopManagementStateNotifier(this._ref, this._session) : super(); - - Future refresh() => updateState(() async { - final result = await _session.command('get'); - final info = DeviceInfo.fromJson(result['data']['info']); - final interfaces = (result['children'] as Map).keys.toSet(); - for (final iface in [ - // This is the preferred order - UsbInterface.ccid, - UsbInterface.otp, - UsbInterface.fido, - ]) { - if (interfaces.contains(iface.name)) { - final path = [iface.name, 'management']; - try { - await _session.command('get', target: path); - _subpath = path; - _log.debug('Using transport $iface for management'); - return info; - } catch (e) { - _log.warning('Failed connecting to management via $iface'); - } - } + final result = await _session.command('get'); + final info = DeviceInfo.fromJson(result['data']['info']); + final interfaces = (result['children'] as Map).keys.toSet(); + for (final iface in [ + // This is the preferred order + UsbInterface.ccid, + UsbInterface.otp, + UsbInterface.fido, + ]) { + if (interfaces.contains(iface.name)) { + final path = [iface.name, 'management']; + try { + await _session.command('get', target: path); + _subpath = path; + _log.debug('Using transport $iface for management'); + return info; + } catch (e) { + _log.warning('Failed connecting to management via $iface'); } - throw 'Failed connection over all interfaces'; - }); + } + } + throw 'Failed connection over all interfaces'; + } @override Future setMode( @@ -94,7 +92,7 @@ class _DesktopManagementStateNotifier extends ManagementStateNotifier { 'challenge_response_timeout': challengeResponseTimeout, 'auto_eject_timeout': autoEjectTimeout, }); - _ref.read(attachedDevicesProvider.notifier).refresh(); + ref.read(attachedDevicesProvider.notifier).refresh(); } @override @@ -111,6 +109,6 @@ class _DesktopManagementStateNotifier extends ManagementStateNotifier { 'new_lock_code': newLockCode, 'reboot': reboot, }); - _ref.read(attachedDevicesProvider.notifier).refresh(); + ref.read(attachedDevicesProvider.notifier).refresh(); } } diff --git a/lib/desktop/oath/state.dart b/lib/desktop/oath/state.dart index 81486270..66029c1f 100755 --- a/lib/desktop/oath/state.dart +++ b/lib/desktop/oath/state.dart @@ -57,56 +57,48 @@ class _LockKeyNotifier extends StateNotifier { } } -final desktopOathState = StateNotifierProvider.autoDispose - .family, DevicePath>( - (ref, devicePath) { - final session = ref.watch(_sessionProvider(devicePath)); - final notifier = _DesktopOathStateNotifier(session, ref); - session +final desktopOathState = AsyncNotifierProvider.autoDispose + .family( + _DesktopOathStateNotifier.new); + +class _DesktopOathStateNotifier extends OathStateNotifier { + late RpcNodeSession _session; + + @override + FutureOr build(DevicePath devicePath) async { + _session = ref.watch(_sessionProvider(devicePath)); + _session ..setErrorHandler('state-reset', (_) async { ref.invalidate(_sessionProvider(devicePath)); }) ..setErrorHandler('auth-required', (_) async { - await notifier.refresh(); + ref.invalidateSelf(); }); ref.onDispose(() { - session + _session ..unsetErrorHandler('state-reset') ..unsetErrorHandler('auth-required'); }); - return notifier..refresh(); - }, -); - -class _DesktopOathStateNotifier extends OathStateNotifier { - final RpcNodeSession _session; - final Ref _ref; - _DesktopOathStateNotifier(this._session, this._ref) : super(); - - refresh() => updateState(() async { - final result = await _session.command('get'); - _log.debug('application status', jsonEncode(result)); - var oathState = OathState.fromJson(result['data']); - final key = _ref.read(_oathLockKeyProvider(_session.devicePath)); - if (oathState.locked && key != null) { - final result = - await _session.command('validate', params: {'key': key}); - if (result['valid']) { - oathState = oathState.copyWith(locked: false); - } else { - _ref - .read(_oathLockKeyProvider(_session.devicePath).notifier) - .unsetKey(); - } - } - return oathState; - }); + final result = await _session.command('get'); + _log.debug('application status', jsonEncode(result)); + var oathState = OathState.fromJson(result['data']); + final key = ref.read(_oathLockKeyProvider(_session.devicePath)); + if (oathState.locked && key != null) { + final result = await _session.command('validate', params: {'key': key}); + if (result['valid']) { + oathState = oathState.copyWith(locked: false); + } else { + ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey(); + } + } + return oathState; + } @override Future reset() async { await _session.command('reset'); - _ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey(); - _ref.invalidate(_sessionProvider(_session.devicePath)); + ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey(); + ref.invalidate(_sessionProvider(_session.devicePath)); } @override @@ -120,7 +112,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier { final bool remembered = validate['remembered']; if (valid) { _log.debug('applet unlocked'); - _ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key); + ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key); setData(state.value!.copyWith( locked: false, remembered: remembered, @@ -158,7 +150,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier { await _session.command('derive', params: {'password': password}); var key = derive['key']; await _session.command('set_key', params: {'key': key}); - _ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key); + ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key); } _log.debug('OATH key set'); @@ -177,7 +169,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier { } } await _session.command('unset_key'); - _ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey(); + ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey(); setData(oathState.copyWith(hasKey: false, locked: false)); return true; } @@ -185,7 +177,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier { @override Future forgetPassword() async { await _session.command('forget'); - _ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey(); + ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey(); setData(state.value!.copyWith(remembered: false)); } } diff --git a/lib/fido/state.dart b/lib/fido/state.dart index 1b998c2b..6c3366f2 100755 --- a/lib/fido/state.dart +++ b/lib/fido/state.dart @@ -21,9 +21,9 @@ import '../app/models.dart'; import '../core/state.dart'; import 'models.dart'; -final fidoStateProvider = StateNotifierProvider.autoDispose - .family, DevicePath>( - (ref, devicePath) => throw UnimplementedError(), +final fidoStateProvider = AsyncNotifierProvider.autoDispose + .family( + () => throw UnimplementedError(), ); abstract class FidoStateNotifier extends ApplicationStateNotifier { diff --git a/lib/management/state.dart b/lib/management/state.dart index 146b49a8..e080aab4 100755 --- a/lib/management/state.dart +++ b/lib/management/state.dart @@ -20,9 +20,9 @@ import 'package:yubico_authenticator/management/models.dart'; import '../app/models.dart'; import '../core/state.dart'; -final managementStateProvider = StateNotifierProvider.autoDispose - .family, DevicePath>( - (ref, devicePath) => throw UnimplementedError(), +final managementStateProvider = AsyncNotifierProvider.autoDispose + .family( + () => throw UnimplementedError(), ); abstract class ManagementStateNotifier diff --git a/lib/oath/state.dart b/lib/oath/state.dart index 608cf303..9a5c0684 100755 --- a/lib/oath/state.dart +++ b/lib/oath/state.dart @@ -37,9 +37,9 @@ class SearchNotifier extends StateNotifier { } } -final oathStateProvider = StateNotifierProvider.autoDispose - .family, DevicePath>( - (ref, devicePath) => throw UnimplementedError(), +final oathStateProvider = AsyncNotifierProvider.autoDispose + .family( + () => throw UnimplementedError(), ); abstract class OathStateNotifier extends ApplicationStateNotifier { diff --git a/lib/oath/views/account_helper.dart b/lib/oath/views/account_helper.dart index 79ca0299..1788c659 100755 --- a/lib/oath/views/account_helper.dart +++ b/lib/oath/views/account_helper.dart @@ -14,7 +14,6 @@ * limitations under the License. */ -import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index 4e70f0d6..a06703cb 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -42,7 +42,6 @@ Widget oathBuildActions( }) { final l10n = AppLocalizations.of(context)!; final capacity = oathState.version.isAtLeast(4) ? 32 : null; - //final theme = Theme.of(context).colorScheme; final theme = ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; return FsDialog( From d1be87ffd7a88cb9e18d137a09c45b177a4456a2 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 3 May 2023 21:20:48 +0200 Subject: [PATCH 019/158] Revamp FIDO key actions and fingerprint dialog. --- lib/app/views/app_page.dart | 8 +- lib/app/views/message_page.dart | 3 + lib/fido/models.dart | 2 + lib/fido/views/fingerprint_dialog.dart | 142 +++++++++++++++++++++++++ lib/fido/views/key_actions.dart | 122 ++++++++++++--------- lib/fido/views/locked_page.dart | 2 + lib/fido/views/unlocked_page.dart | 79 +++++++------- lib/l10n/app_en.arb | 3 + lib/oath/views/account_dialog.dart | 2 +- 9 files changed, 275 insertions(+), 88 deletions(-) create mode 100644 lib/fido/views/fingerprint_dialog.dart diff --git a/lib/app/views/app_page.dart b/lib/app/views/app_page.dart index 4028ee28..2c4f443b 100755 --- a/lib/app/views/app_page.dart +++ b/lib/app/views/app_page.dart @@ -28,6 +28,7 @@ class AppPage extends StatelessWidget { final Widget child; final List actions; final Widget Function(BuildContext context)? keyActionsBuilder; + final bool keyActionsBadge; final bool centered; final bool delayedContent; final Widget Function(BuildContext context)? actionButtonBuilder; @@ -40,6 +41,7 @@ class AppPage extends StatelessWidget { this.keyActionsBuilder, this.actionButtonBuilder, this.delayedContent = false, + this.keyActionsBadge = false, }); @override @@ -127,7 +129,11 @@ class AppPage extends StatelessWidget { onPressed: () { showBlurDialog(context: context, builder: keyActionsBuilder!); }, - icon: const Icon(Icons.tune), + icon: keyActionsBadge + ? const Badge( + child: Icon(Icons.tune), + ) + : const Icon(Icons.tune), iconSize: 24, tooltip: AppLocalizations.of(context)!.s_configure_yk, padding: const EdgeInsets.all(12), diff --git a/lib/app/views/message_page.dart b/lib/app/views/message_page.dart index fb517861..b59fd607 100755 --- a/lib/app/views/message_page.dart +++ b/lib/app/views/message_page.dart @@ -27,6 +27,7 @@ class MessagePage extends StatelessWidget { final bool delayedContent; final Widget Function(BuildContext context)? keyActionsBuilder; final Widget Function(BuildContext context)? actionButtonBuilder; + final bool keyActionsBadge; const MessagePage({ super.key, @@ -38,6 +39,7 @@ class MessagePage extends StatelessWidget { this.keyActionsBuilder, this.actionButtonBuilder, this.delayedContent = false, + this.keyActionsBadge = false, }); @override @@ -46,6 +48,7 @@ class MessagePage extends StatelessWidget { centered: true, actions: actions, keyActionsBuilder: keyActionsBuilder, + keyActionsBadge: keyActionsBadge, actionButtonBuilder: actionButtonBuilder, delayedContent: delayedContent, child: Padding( diff --git a/lib/fido/models.dart b/lib/fido/models.dart index cdc1a25d..1aa139be 100755 --- a/lib/fido/models.dart +++ b/lib/fido/models.dart @@ -41,6 +41,8 @@ class FidoState with _$FidoState { info['options']['credentialMgmtPreview'] == true; bool? get bioEnroll => info['options']['bioEnroll']; + + bool get alwaysUv => info['options']['alwaysUv'] == true; } @freezed diff --git a/lib/fido/views/fingerprint_dialog.dart b/lib/fido/views/fingerprint_dialog.dart new file mode 100644 index 00000000..86af6f3c --- /dev/null +++ b/lib/fido/views/fingerprint_dialog.dart @@ -0,0 +1,142 @@ +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/shortcuts.dart'; +import '../../app/state.dart'; +import '../../app/views/fs_dialog.dart'; +import '../../widgets/list_title.dart'; +import '../models.dart'; +import 'delete_fingerprint_dialog.dart'; +import 'rename_fingerprint_dialog.dart'; + +class FingerprintDialog extends ConsumerWidget { + final Fingerprint fingerprint; + const FingerprintDialog(this.fingerprint, {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(); + } + + return Actions( + actions: { + EditIntent: CallbackAction(onInvoke: (_) async { + final withContext = ref.read(withContextProvider); + final Fingerprint? renamed = + await withContext((context) async => await showBlurDialog( + context: context, + builder: (context) => RenameFingerprintDialog( + node.path, + fingerprint, + ), + )); + if (renamed != null) { + // Replace the dialog with the renamed credential + await withContext((context) async { + Navigator.of(context).pop(); + await showBlurDialog( + context: context, + builder: (context) { + return FingerprintDialog(renamed); + }, + ); + }); + } + return renamed; + }), + DeleteIntent: CallbackAction(onInvoke: (_) async { + final withContext = ref.read(withContextProvider); + final bool? deleted = + await ref.read(withContextProvider)((context) async => + await showBlurDialog( + context: context, + builder: (context) => DeleteFingerprintDialog( + node.path, + fingerprint, + ), + ) ?? + false); + + // Pop the account dialog if deleted + if (deleted == true) { + await withContext((context) async { + Navigator.of(context).pop(); + }); + } + return deleted; + }), + }, + child: FocusScope( + autofocus: true, + child: FsDialog( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 48, bottom: 32), + child: Column( + children: [ + Text( + fingerprint.label, + style: Theme.of(context).textTheme.headlineSmall, + softWrap: true, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + const Icon(Icons.fingerprint, size: 72), + ], + ), + ), + ListTitle(AppLocalizations.of(context)!.s_actions, + textStyle: Theme.of(context).textTheme.bodyLarge), + _FingerprintDialogActions(), + ], + ), + ), + ), + ); + } +} + +class _FingerprintDialogActions extends StatelessWidget { + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final theme = + ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; + return Column( + children: [ + ListTile( + leading: CircleAvatar( + backgroundColor: theme.secondary, + foregroundColor: theme.onSecondary, + child: const Icon(Icons.edit), + ), + title: Text(l10n.s_rename_fp), + subtitle: Text(l10n.l_rename_fp_desc), + onTap: () { + Actions.invoke(context, const EditIntent()); + }, + ), + ListTile( + leading: CircleAvatar( + backgroundColor: theme.error, + foregroundColor: theme.onError, + child: const Icon(Icons.delete), + ), + title: Text(l10n.s_delete_fingerprint), + subtitle: Text(l10n.l_delete_fingerprint_desc), + onTap: () { + Actions.invoke(context, const DeleteIntent()); + }, + ), + ], + ); + } +} diff --git a/lib/fido/views/key_actions.dart b/lib/fido/views/key_actions.dart index 51ab8ac5..38c6d3d2 100755 --- a/lib/fido/views/key_actions.dart +++ b/lib/fido/views/key_actions.dart @@ -19,72 +19,94 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../app/message.dart'; import '../../app/models.dart'; +import '../../app/views/fs_dialog.dart'; import '../../widgets/list_title.dart'; import '../models.dart'; import 'add_fingerprint_dialog.dart'; import 'pin_dialog.dart'; import 'reset_dialog.dart'; +bool fidoShowActionsNotifier(FidoState state) { + return (state.alwaysUv && !state.hasPin) || state.bioEnroll == false; +} + Widget fidoBuildActions( BuildContext context, DeviceNode node, FidoState state, int fingerprints) { final l10n = AppLocalizations.of(context)!; - final theme = Theme.of(context).colorScheme; - return SimpleDialog( - children: [ - if (state.bioEnroll != null) ...[ - ListTitle(l10n.s_setup, + final theme = + ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; + + return FsDialog( + child: Column( + children: [ + if (state.bioEnroll != null) ...[ + ListTitle(l10n.s_setup, + textStyle: Theme.of(context).textTheme.bodyLarge), + ListTile( + leading: CircleAvatar( + backgroundColor: theme.primary, + foregroundColor: theme.onPrimary, + child: const Icon(Icons.fingerprint_outlined), + ), + title: Text(l10n.s_add_fingerprint), + subtitle: state.unlocked + ? Text(l10n.l_fingerprints_used(fingerprints)) + : Text(state.hasPin + ? l10n.l_unlock_pin_first + : l10n.l_set_pin_first), + trailing: + fingerprints == 0 ? const Icon(Icons.warning_amber) : null, + enabled: state.unlocked && fingerprints < 5, + onTap: state.unlocked && fingerprints < 5 + ? () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => AddFingerprintDialog(node.path), + ); + } + : null, + ), + ], + ListTitle(l10n.s_manage, textStyle: Theme.of(context).textTheme.bodyLarge), ListTile( - leading: const CircleAvatar(child: Icon(Icons.fingerprint_outlined)), - title: Text(l10n.s_add_fingerprint), - subtitle: state.unlocked - ? Text(l10n.l_fingerprints_used(fingerprints)) - : Text(state.hasPin - ? l10n.l_unlock_pin_first - : l10n.l_set_pin_first), - enabled: state.unlocked && fingerprints < 5, - onTap: state.unlocked && fingerprints < 5 - ? () { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => AddFingerprintDialog(node.path), - ); - } - : null, - ), - ], - ListTitle(l10n.s_manage, - textStyle: Theme.of(context).textTheme.bodyLarge), - ListTile( - leading: const CircleAvatar(child: Icon(Icons.pin_outlined)), - title: Text(state.hasPin ? l10n.s_change_pin : l10n.s_set_pin), - subtitle: Text(state.hasPin - ? l10n.s_fido_pin_protection - : l10n.l_fido_pin_protection_optional), + leading: CircleAvatar( + backgroundColor: theme.secondary, + foregroundColor: theme.onSecondary, + child: const Icon(Icons.pin_outlined), + ), + title: Text(state.hasPin ? l10n.s_change_pin : l10n.s_set_pin), + subtitle: Text(state.hasPin + ? l10n.s_fido_pin_protection + : l10n.l_fido_pin_protection_optional), + trailing: state.alwaysUv && !state.hasPin + ? const Icon(Icons.warning_amber) + : null, + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => FidoPinDialog(node.path, state), + ); + }), + ListTile( + leading: CircleAvatar( + foregroundColor: theme.onError, + backgroundColor: theme.error, + child: const Icon(Icons.delete_outline), + ), + title: Text(l10n.s_reset_fido), + subtitle: Text(l10n.l_factory_reset_this_app), onTap: () { Navigator.of(context).pop(); showBlurDialog( context: context, - builder: (context) => FidoPinDialog(node.path, state), + builder: (context) => ResetDialog(node), ); - }), - ListTile( - leading: CircleAvatar( - foregroundColor: theme.onError, - backgroundColor: theme.error, - child: const Icon(Icons.delete_outline), + }, ), - title: Text(l10n.s_reset_fido), - subtitle: Text(l10n.l_factory_reset_this_app), - onTap: () { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => ResetDialog(node), - ); - }, - ), - ], + ], + ), ); } diff --git a/lib/fido/views/locked_page.dart b/lib/fido/views/locked_page.dart index 80839119..d6cc93e9 100755 --- a/lib/fido/views/locked_page.dart +++ b/lib/fido/views/locked_page.dart @@ -43,6 +43,7 @@ class FidoLockedPage extends ConsumerWidget { header: l10n.s_no_fingerprints, message: l10n.l_set_pin_fingerprints, keyActionsBuilder: _buildActions, + keyActionsBadge: fidoShowActionsNotifier(state), ); } else { return MessagePage( @@ -53,6 +54,7 @@ class FidoLockedPage extends ConsumerWidget { : l10n.l_ready_to_use, message: l10n.l_optionally_set_a_pin, keyActionsBuilder: _buildActions, + keyActionsBadge: fidoShowActionsNotifier(state), ); } } diff --git a/lib/fido/views/unlocked_page.dart b/lib/fido/views/unlocked_page.dart index b682f745..65fe853a 100755 --- a/lib/fido/views/unlocked_page.dart +++ b/lib/fido/views/unlocked_page.dart @@ -20,6 +20,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; +import '../../app/shortcuts.dart'; import '../../app/views/app_page.dart'; import '../../app/views/graphics.dart'; import '../../app/views/message_page.dart'; @@ -27,9 +28,8 @@ import '../../widgets/list_title.dart'; import '../models.dart'; import '../state.dart'; import 'delete_credential_dialog.dart'; -import 'delete_fingerprint_dialog.dart'; +import 'fingerprint_dialog.dart'; import 'key_actions.dart'; -import 'rename_fingerprint_dialog.dart'; class FidoUnlockedPage extends ConsumerWidget { final DeviceNode node; @@ -97,40 +97,17 @@ class FidoUnlockedPage extends ConsumerWidget { if (fingerprints.isNotEmpty) { nFingerprints = fingerprints.length; children.add(ListTitle(l10n.s_fingerprints)); - children.addAll(fingerprints.map((fp) => ListTile( - leading: CircleAvatar( - foregroundColor: Theme.of(context).colorScheme.onSecondary, - backgroundColor: Theme.of(context).colorScheme.secondary, - child: const Icon(Icons.fingerprint), - ), - title: Text( - fp.label, - softWrap: false, - overflow: TextOverflow.fade, - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () { - showBlurDialog( - context: context, - builder: (context) => - RenameFingerprintDialog(node.path, fp), - ); - }, - icon: const Icon(Icons.edit_outlined)), - IconButton( - onPressed: () { - showBlurDialog( - context: context, - builder: (context) => - DeleteFingerprintDialog(node.path, fp), - ); - }, - icon: const Icon(Icons.delete_outline)), - ], - ), + children.addAll(fingerprints.map((fp) => Actions( + actions: { + OpenIntent: CallbackAction(onInvoke: (_) async { + await showBlurDialog( + context: context, + builder: (context) => FingerprintDialog(fp), + ); + return null; + }), + }, + child: _FingerprintListItem(fp), ))); } } @@ -140,6 +117,7 @@ class FidoUnlockedPage extends ConsumerWidget { title: Text(l10n.s_webauthn), keyActionsBuilder: (context) => fidoBuildActions(context, node, state, nFingerprints), + keyActionsBadge: fidoShowActionsNotifier(state), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: children), ); @@ -153,6 +131,7 @@ class FidoUnlockedPage extends ConsumerWidget { message: l10n.l_add_one_or_more_fps, keyActionsBuilder: (context) => fidoBuildActions(context, node, state, 0), + keyActionsBadge: fidoShowActionsNotifier(state), ); } @@ -162,6 +141,7 @@ class FidoUnlockedPage extends ConsumerWidget { header: l10n.l_no_discoverable_accounts, message: l10n.l_register_sk_on_websites, keyActionsBuilder: (context) => fidoBuildActions(context, node, state, 0), + keyActionsBadge: fidoShowActionsNotifier(state), ); } @@ -172,3 +152,30 @@ class FidoUnlockedPage extends ConsumerWidget { child: const CircularProgressIndicator(), ); } + +class _FingerprintListItem extends StatelessWidget { + final Fingerprint fingerprint; + const _FingerprintListItem(this.fingerprint); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: CircleAvatar( + foregroundColor: Theme.of(context).colorScheme.onSecondary, + backgroundColor: Theme.of(context).colorScheme.secondary, + child: const Icon(Icons.fingerprint), + ), + title: Text( + fingerprint.label, + softWrap: false, + overflow: TextOverflow.fade, + ), + trailing: OutlinedButton( + onPressed: () { + Actions.maybeInvoke(context, const OpenIntent()); + }, + child: const Icon(Icons.more_horiz), + ), + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a2fbb2f6..2103cbcb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -45,6 +45,7 @@ "s_about": "About", "s_appearance": "Appearance", "s_authenticator": "Authenticator", + "s_actions": "Actions", "s_manage": "Manage", "s_setup": "Setup", "s_settings": "Settings", @@ -324,12 +325,14 @@ "l_fp_step_1_capture": "Step 1/2: Capture fingerprint", "l_fp_step_2_name": "Step 2/2: Name fingerprint", "s_delete_fingerprint": "Delete fingerprint", + "l_delete_fingerprint_desc": "Remove the fingerprint from the YubiKey", "s_fingerprint_deleted": "Fingerprint deleted", "p_warning_delete_fingerprint": "This will delete the fingerprint from your YubiKey.", "s_no_fingerprints": "No fingerprints", "l_set_pin_fingerprints": "Set a PIN to register fingerprints", "l_no_fps_added": "No fingerprints have been added", "s_rename_fp": "Rename fingerprint", + "l_rename_fp_desc": "Change the label", "s_fingerprint_renamed": "Fingerprint renamed", "l_rename_fp_failed": "Error renaming: {message}", "@l_rename_fp_failed" : { diff --git a/lib/oath/views/account_dialog.dart b/lib/oath/views/account_dialog.dart index f432872e..d656a0b3 100755 --- a/lib/oath/views/account_dialog.dart +++ b/lib/oath/views/account_dialog.dart @@ -200,7 +200,7 @@ class AccountDialog extends ConsumerWidget { ), ), const SizedBox(height: 32), - ListTitle('Actions', + ListTitle(AppLocalizations.of(context)!.s_actions, textStyle: Theme.of(context).textTheme.bodyLarge), ..._buildActions(context, helper), ], From 7011f816d4f11c0b4f783d4aafb968b2d9b5f5c8 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 4 May 2023 13:57:40 +0200 Subject: [PATCH 020/158] More FIDO state refactoring. --- lib/desktop/fido/state.dart | 121 ++++++++++++++++++++++++++---------- lib/fido/state.dart | 29 +++------ 2 files changed, 96 insertions(+), 54 deletions(-) diff --git a/lib/desktop/fido/state.dart b/lib/desktop/fido/state.dart index cfa88bc5..4f5f3a9c 100755 --- a/lib/desktop/fido/state.dart +++ b/lib/desktop/fido/state.dart @@ -23,6 +23,7 @@ import 'package:logging/logging.dart'; import 'package:yubico_authenticator/app/logging.dart'; import '../../app/models.dart'; +import '../../app/state.dart'; import '../../fido/models.dart'; import '../../fido/state.dart'; import '../models.dart'; @@ -53,6 +54,22 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier { late RpcNodeSession _session; late StateController _pinController; + FutureOr _build(DevicePath devicePath) async { + var result = await _session.command('get'); + FidoState fidoState = FidoState.fromJson(result['data']); + if (fidoState.hasPin && !fidoState.unlocked) { + final pin = ref.read(_pinProvider(devicePath)); + if (pin != null) { + await unlock(pin); + result = await _session.command('get'); + fidoState = FidoState.fromJson(result['data']); + } + } + + _log.debug('application status', jsonEncode(fidoState)); + return fidoState; + } + @override FutureOr build(DevicePath devicePath) async { _session = ref.watch(_sessionProvider(devicePath)); @@ -60,6 +77,20 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier { // Make sure to rebuild if isAdmin changes ref.watch(rpcStateProvider.select((state) => state.isAdmin)); } + + ref.listen( + windowStateProvider, + (prev, next) async { + if (prev?.active == false && next.active) { + // Refresh state on active + final newState = await _build(devicePath); + if (state.valueOrNull != newState) { + state = AsyncValue.data(newState); + } + } + }, + ); + _pinController = ref.watch(_pinProvider(devicePath).notifier); _session.setErrorHandler('state-reset', (_) async { ref.invalidate(_sessionProvider(devicePath)); @@ -77,9 +108,7 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier { _session.unsetErrorHandler('state-reset'); }); - final result = await _session.command('get'); - _log.debug('application status', jsonEncode(result)); - return FidoState.fromJson(result['data']); + return _build(devicePath); } @override @@ -146,25 +175,38 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier { } } -final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family< - FidoFingerprintsNotifier, AsyncValue>, DevicePath>( - (ref, devicePath) => _DesktopFidoFingerprintsNotifier( - ref.watch(_sessionProvider(devicePath)), - ref, - )); +final desktopFingerprintProvider = AsyncNotifierProvider.autoDispose + .family, DevicePath>( + _DesktopFidoFingerprintsNotifier.new); class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier { - final RpcNodeSession _session; - final Ref _ref; + late RpcNodeSession _session; - _DesktopFidoFingerprintsNotifier(this._session, this._ref) { - _refresh(); + @override + FutureOr> build(DevicePath devicePath) async { + _session = ref.watch(_sessionProvider(devicePath)); + ref.watch(fidoStateProvider(devicePath)); + + // Refresh on active + ref.listen( + windowStateProvider, + (prev, next) async { + if (prev?.active == false && next.active) { + // Refresh state on active + final newState = await _build(devicePath); + if (state.valueOrNull != newState) { + state = AsyncValue.data(newState); + } + } + }, + ); + + return _build(devicePath); } - Future _refresh() async { - _ref.invalidate(fidoStateProvider(_session.devicePath)); + FutureOr> _build(DevicePath devicePath) async { final result = await _session.command('fingerprints'); - setItems((result['children'] as Map) + return List.unmodifiable((result['children'] as Map) .entries .map((e) => Fingerprint(e.key, e.value['name'])) .toList()); @@ -174,7 +216,7 @@ class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier { Future deleteFingerprint(Fingerprint fingerprint) async { await _session .command('delete', target: ['fingerprints', fingerprint.templateId]); - await _refresh(); + ref.invalidate(fidoStateProvider(_session.devicePath)); } @override @@ -208,7 +250,7 @@ class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier { ); controller.sink .add(FingerprintEvent.complete(Fingerprint.fromJson(result))); - await _refresh(); + ref.invalidate(fidoStateProvider(_session.devicePath)); await controller.sink.close(); } catch (e) { controller.sink.addError(e); @@ -225,27 +267,41 @@ class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier { target: ['fingerprints', fingerprint.templateId], params: {'name': name}); final renamed = fingerprint.copyWith(name: name); - await _refresh(); + ref.invalidate(fidoStateProvider(_session.devicePath)); return renamed; } } -final desktopCredentialProvider = StateNotifierProvider.autoDispose.family< - FidoCredentialsNotifier, AsyncValue>, DevicePath>( - (ref, devicePath) => _DesktopFidoCredentialsNotifier( - ref.watch(_sessionProvider(devicePath)), - ref, - )); +final desktopCredentialProvider = AsyncNotifierProvider.autoDispose + .family, DevicePath>( + _DesktopFidoCredentialsNotifier.new); class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier { - final RpcNodeSession _session; - final Ref _ref; + late RpcNodeSession _session; - _DesktopFidoCredentialsNotifier(this._session, this._ref) { - _refresh(); + @override + FutureOr> build(DevicePath devicePath) async { + _session = ref.watch(_sessionProvider(devicePath)); + ref.watch(fidoStateProvider(devicePath)); + + // Refresh on active + ref.listen( + windowStateProvider, + (prev, next) async { + if (prev?.active == false && next.active) { + // Refresh state on active + final newState = await _build(devicePath); + if (state.valueOrNull != newState) { + state = AsyncValue.data(newState); + } + } + }, + ); + + return _build(devicePath); } - Future _refresh() async { + FutureOr> _build(DevicePath devicePath) async { final List creds = []; final rps = await _session.command('credentials'); for (final rpId in (rps['children'] as Map).keys) { @@ -258,8 +314,7 @@ class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier { userName: e.value['user_name'])); } } - setItems(creds); - _ref.invalidate(fidoStateProvider(_session.devicePath)); + return List.unmodifiable(creds); } @override @@ -269,6 +324,6 @@ class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier { credential.rpId, credential.credentialId, ]); - await _refresh(); + ref.invalidate(fidoStateProvider(_session.devicePath)); } } diff --git a/lib/fido/state.dart b/lib/fido/state.dart index 6c3366f2..4fb4ee25 100755 --- a/lib/fido/state.dart +++ b/lib/fido/state.dart @@ -14,7 +14,6 @@ * limitations under the License. */ -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/models.dart'; @@ -32,36 +31,24 @@ abstract class FidoStateNotifier extends ApplicationStateNotifier { Future unlock(String pin); } -abstract class LockedCollectionNotifier - extends StateNotifier>> { - LockedCollectionNotifier() : super(const AsyncValue.loading()); - - @protected - void setItems(List items) { - if (mounted) { - state = AsyncValue.data(List.unmodifiable(items)); - } - } -} - -final fingerprintProvider = StateNotifierProvider.autoDispose.family< - FidoFingerprintsNotifier, AsyncValue>, DevicePath>( - (ref, arg) => throw UnimplementedError(), +final fingerprintProvider = AsyncNotifierProvider.autoDispose + .family, DevicePath>( + () => throw UnimplementedError(), ); abstract class FidoFingerprintsNotifier - extends LockedCollectionNotifier { + extends AutoDisposeFamilyAsyncNotifier, DevicePath> { Stream registerFingerprint({String? name}); Future renameFingerprint(Fingerprint fingerprint, String name); Future deleteFingerprint(Fingerprint fingerprint); } -final credentialProvider = StateNotifierProvider.autoDispose.family< - FidoCredentialsNotifier, AsyncValue>, DevicePath>( - (ref, arg) => throw UnimplementedError(), +final credentialProvider = AsyncNotifierProvider.autoDispose + .family, DevicePath>( + () => throw UnimplementedError(), ); abstract class FidoCredentialsNotifier - extends LockedCollectionNotifier { + extends AutoDisposeFamilyAsyncNotifier, DevicePath> { Future deleteCredential(FidoCredential credential); } From 9eeb44f3acc503fbc5d134509dbdf180f13786c4 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 4 May 2023 16:20:50 +0200 Subject: [PATCH 021/158] Update FIDO passkeys views. --- lib/fido/views/credential_dialog.dart | 114 +++++++++++++++++++ lib/fido/views/delete_credential_dialog.dart | 8 +- lib/fido/views/unlocked_page.dart | 83 ++++++++------ lib/l10n/app_en.arb | 15 +-- 4 files changed, 172 insertions(+), 48 deletions(-) create mode 100644 lib/fido/views/credential_dialog.dart diff --git a/lib/fido/views/credential_dialog.dart b/lib/fido/views/credential_dialog.dart new file mode 100644 index 00000000..db9b78ea --- /dev/null +++ b/lib/fido/views/credential_dialog.dart @@ -0,0 +1,114 @@ +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/shortcuts.dart'; +import '../../app/state.dart'; +import '../../app/views/fs_dialog.dart'; +import '../../widgets/list_title.dart'; +import '../models.dart'; +import 'delete_credential_dialog.dart'; + +class CredentialDialog extends ConsumerWidget { + final FidoCredential credential; + const CredentialDialog(this.credential, {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(); + } + + return Actions( + actions: { + DeleteIntent: CallbackAction(onInvoke: (_) async { + final withContext = ref.read(withContextProvider); + final bool? deleted = + await ref.read(withContextProvider)((context) async => + await showBlurDialog( + context: context, + builder: (context) => DeleteCredentialDialog( + node.path, + credential, + ), + ) ?? + false); + + // Pop the account dialog if deleted + if (deleted == true) { + await withContext((context) async { + Navigator.of(context).pop(); + }); + } + return deleted; + }), + }, + child: FocusScope( + autofocus: true, + child: FsDialog( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 48, bottom: 32), + child: Column( + children: [ + Text( + credential.userName, + style: Theme.of(context).textTheme.headlineSmall, + softWrap: true, + textAlign: TextAlign.center, + ), + Text( + credential.rpId, + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).textTheme.bodySmall!.color, + ), + ), + const SizedBox(height: 16), + const Icon(Icons.person, size: 72), + ], + ), + ), + ListTitle(AppLocalizations.of(context)!.s_actions, + textStyle: Theme.of(context).textTheme.bodyLarge), + _CredentialDialogActions(), + ], + ), + ), + ), + ); + } +} + +class _CredentialDialogActions extends StatelessWidget { + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final theme = + ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; + return Column( + children: [ + ListTile( + leading: CircleAvatar( + backgroundColor: theme.error, + foregroundColor: theme.onError, + child: const Icon(Icons.delete), + ), + title: Text(l10n.s_delete_passkey), + subtitle: Text(l10n.l_delete_account_desc), + onTap: () { + Actions.invoke(context, const DeleteIntent()); + }, + ), + ], + ); + } +} diff --git a/lib/fido/views/delete_credential_dialog.dart b/lib/fido/views/delete_credential_dialog.dart index b1bb30aa..b74279e4 100755 --- a/lib/fido/views/delete_credential_dialog.dart +++ b/lib/fido/views/delete_credential_dialog.dart @@ -38,14 +38,14 @@ class DeleteCredentialDialog extends ConsumerWidget { final label = credential.userName; return ResponsiveDialog( - title: Text(l10n.s_delete_credential), + title: Text(l10n.s_delete_passkey), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 18.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.p_warning_delete_credential), - Text(l10n.l_credential(label)), + Text(l10n.p_warning_delete_passkey), + Text(l10n.l_passkey(label)), ] .map((e) => Padding( child: e, @@ -63,7 +63,7 @@ class DeleteCredentialDialog extends ConsumerWidget { await ref.read(withContextProvider)( (context) async { Navigator.of(context).pop(true); - showMessage(context, l10n.s_credential_deleted); + showMessage(context, l10n.s_passkey_deleted); }, ); }, diff --git a/lib/fido/views/unlocked_page.dart b/lib/fido/views/unlocked_page.dart index 65fe853a..f2f2305f 100755 --- a/lib/fido/views/unlocked_page.dart +++ b/lib/fido/views/unlocked_page.dart @@ -27,7 +27,7 @@ import '../../app/views/message_page.dart'; import '../../widgets/list_title.dart'; import '../models.dart'; import '../state.dart'; -import 'delete_credential_dialog.dart'; +import 'credential_dialog.dart'; import 'fingerprint_dialog.dart'; import 'key_actions.dart'; @@ -48,42 +48,19 @@ class FidoUnlockedPage extends ConsumerWidget { } final creds = data.value; if (creds.isNotEmpty) { - children.add(ListTitle(l10n.s_credentials)); - children.addAll( - creds.map( - (cred) => ListTile( - leading: CircleAvatar( - foregroundColor: Theme.of(context).colorScheme.onPrimary, - backgroundColor: Theme.of(context).colorScheme.primary, - child: const Icon(Icons.person), - ), - title: Text( - cred.userName, - softWrap: false, - overflow: TextOverflow.fade, - ), - subtitle: Text( - cred.rpId, - softWrap: false, - overflow: TextOverflow.fade, - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () { - showBlurDialog( - context: context, - builder: (context) => - DeleteCredentialDialog(node.path, cred), - ); - }, - icon: const Icon(Icons.delete_outline)), - ], - ), - ), - ), - ); + children.add(ListTitle(l10n.s_passkeys)); + children.addAll(creds.map((cred) => Actions( + actions: { + OpenIntent: CallbackAction(onInvoke: (_) async { + await showBlurDialog( + context: context, + builder: (context) => CredentialDialog(cred), + ); + return null; + }), + }, + child: _CredentialListItem(cred), + ))); } } @@ -153,6 +130,38 @@ class FidoUnlockedPage extends ConsumerWidget { ); } +class _CredentialListItem extends StatelessWidget { + final FidoCredential credential; + const _CredentialListItem(this.credential); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: CircleAvatar( + foregroundColor: Theme.of(context).colorScheme.onPrimary, + backgroundColor: Theme.of(context).colorScheme.primary, + child: const Icon(Icons.person), + ), + title: Text( + credential.userName, + softWrap: false, + overflow: TextOverflow.fade, + ), + subtitle: Text( + credential.rpId, + softWrap: false, + overflow: TextOverflow.fade, + ), + trailing: OutlinedButton( + onPressed: () { + Actions.maybeInvoke(context, const OpenIntent()); + }, + child: const Icon(Icons.more_horiz), + ), + ); + } +} + class _FingerprintListItem extends StatelessWidget { final Fingerprint fingerprint; const _FingerprintListItem(this.fingerprint); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2103cbcb..3ea7697c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -291,19 +291,20 @@ "l_calculate_code_desc": "Get a new code from your YubiKey", "@_fido_credentials": {}, - "l_credential": "Credential: {label}", - "@l_credential" : { + "l_passkey": "Passkey: {label}", + "@l_passkey" : { "placeholders": { "label": {} } }, - "s_credentials": "Credentials", + "s_passkeys": "Passkeys", "l_ready_to_use": "Ready to use", "l_register_sk_on_websites": "Register as a Security Key on websites", - "l_no_discoverable_accounts": "No discoverable accounts", - "s_delete_credential": "Delete credential", - "s_credential_deleted": "Credential deleted", - "p_warning_delete_credential": "This will delete the credential from your YubiKey.", + "l_no_discoverable_accounts": "No Passkeys stored", + "s_delete_passkey": "Delete Passkey", + "l_delete_passkey_desc": "Remove the Passkey from the YubiKey", + "s_passkey_deleted": "Passkey deleted", + "p_warning_delete_passkey": "This will delete the Passkey from your YubiKey.", "@_fingerprints": {}, "l_fingerprint": "Fingerprint: {label}", From efa8f35e05558d882129a9de67e787e80b945551 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 27 Apr 2023 09:13:38 +0200 Subject: [PATCH 022/158] Add PIV to helper. --- helper/helper/device.py | 8 + helper/helper/piv.py | 456 +++ lib/app/models.dart | 1 + lib/app/views/main_page.dart | 2 + lib/core/models.dart | 3 + lib/desktop/init.dart | 18 +- lib/desktop/piv/state.dart | 425 +++ lib/oath/views/actions.dart | 16 + lib/piv/keys.dart | 35 + lib/piv/models.dart | 313 ++ lib/piv/models.freezed.dart | 2984 ++++++++++++++++++ lib/piv/models.g.dart | 228 ++ lib/piv/state.dart | 70 + lib/piv/views/actions.dart | 221 ++ lib/piv/views/authentication_dialog.dart | 109 + lib/piv/views/delete_certificate_dialog.dart | 83 + lib/piv/views/generate_key_dialog.dart | 166 + lib/piv/views/import_file_dialog.dart | 186 ++ lib/piv/views/key_actions.dart | 141 + lib/piv/views/manage_key_dialog.dart | 247 ++ lib/piv/views/manage_pin_puk_dialog.dart | 192 ++ lib/piv/views/pin_dialog.dart | 111 + lib/piv/views/piv_screen.dart | 119 + lib/piv/views/reset_dialog.dart | 67 + lib/piv/views/slot_dialog.dart | 191 ++ pubspec.lock | 4 +- pubspec.yaml | 2 +- 27 files changed, 6389 insertions(+), 9 deletions(-) create mode 100644 helper/helper/piv.py create mode 100644 lib/desktop/piv/state.dart create mode 100644 lib/piv/keys.dart create mode 100644 lib/piv/models.dart create mode 100644 lib/piv/models.freezed.dart create mode 100644 lib/piv/models.g.dart create mode 100644 lib/piv/state.dart create mode 100644 lib/piv/views/actions.dart create mode 100644 lib/piv/views/authentication_dialog.dart create mode 100644 lib/piv/views/delete_certificate_dialog.dart create mode 100644 lib/piv/views/generate_key_dialog.dart create mode 100644 lib/piv/views/import_file_dialog.dart create mode 100644 lib/piv/views/key_actions.dart create mode 100644 lib/piv/views/manage_key_dialog.dart create mode 100644 lib/piv/views/manage_pin_puk_dialog.dart create mode 100644 lib/piv/views/pin_dialog.dart create mode 100644 lib/piv/views/piv_screen.dart create mode 100644 lib/piv/views/reset_dialog.dart create mode 100644 lib/piv/views/slot_dialog.dart diff --git a/helper/helper/device.py b/helper/helper/device.py index d47df9df..a3a336d8 100644 --- a/helper/helper/device.py +++ b/helper/helper/device.py @@ -24,6 +24,7 @@ from .oath import OathNode from .fido import Ctap2Node from .yubiotp import YubiOtpNode from .management import ManagementNode +from .piv import PivNode from .qr import scan_qr from ykman import __version__ as ykman_version from ykman.base import PID @@ -391,6 +392,13 @@ class ConnectionNode(RpcNode): def oath(self): return OathNode(self._connection) + @child( + condition=lambda self: isinstance(self._connection, SmartCardConnection) + and CAPABILITY.PIV in self.capabilities + ) + def piv(self): + return PivNode(self._connection) + @child( condition=lambda self: isinstance(self._connection, FidoConnection) and CAPABILITY.FIDO2 in self.capabilities diff --git a/helper/helper/piv.py b/helper/helper/piv.py new file mode 100644 index 00000000..df01dab7 --- /dev/null +++ b/helper/helper/piv.py @@ -0,0 +1,456 @@ +# 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. + +from .base import ( + RpcNode, + action, + child, + RpcException, + ChildResetException, + TimeoutException, + AuthRequiredException, +) +from yubikit.core import NotSupportedError, BadResponseError +from yubikit.core.smartcard import ApduError, SW +from yubikit.piv import ( + PivSession, + OBJECT_ID, + MANAGEMENT_KEY_TYPE, + InvalidPinError, + SLOT, + require_version, + KEY_TYPE, + PIN_POLICY, + TOUCH_POLICY, +) +from ykman.piv import ( + get_pivman_data, + get_pivman_protected_data, + derive_management_key, + pivman_set_mgm_key, + pivman_change_pin, + generate_self_signed_certificate, + generate_csr, + generate_chuid, +) +from ykman.util import ( + parse_certificates, + parse_private_key, + get_leaf_certificates, + InvalidPasswordError, +) +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat +from cryptography.hazmat.primitives import hashes +from dataclasses import asdict +from enum import Enum, unique +from threading import Timer +from time import time +import datetime +import logging + +logger = logging.getLogger(__name__) + +_date_format = "%Y-%m-%d" + + +class InvalidPinException(RpcException): + def __init__(self, cause): + super().__init__( + "invalid-pin", + "Wrong PIN", + dict(attempts_remaining=cause.attempts_remaining), + ) + + +@unique +class GENERATE_TYPE(str, Enum): + CSR = "csr" + CERTIFICATE = "certificate" + + +class PivNode(RpcNode): + def __init__(self, connection): + super().__init__() + self.session = PivSession(connection) + self._pivman_data = get_pivman_data(self.session) + self._authenticated = False + + def __call__(self, *args, **kwargs): + try: + return super().__call__(*args, **kwargs) + except ApduError as e: + if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: + raise AuthRequiredException() + # TODO: This should probably be in a baseclass of all "AppNodes". + raise ChildResetException(f"SW: {e.sw:x}") + except InvalidPinError as e: + raise InvalidPinException(cause=e) + + def _get_object(self, object_id): + try: + return self.session.get_object(object_id) + except ApduError as e: + if e.sw == SW.FILE_NOT_FOUND: + return None + raise + except BadResponseError: + logger.warning(f"Couldn't read data object {object_id}", exc_info=True) + return None + + def get_data(self): + try: + pin_md = self.session.get_pin_metadata() + puk_md = self.session.get_puk_metadata() + mgm_md = self.session.get_management_key_metadata() + pin_attempts = pin_md.attempts_remaining + metadata = dict( + pin_metadata=asdict(pin_md), + puk_metadata=asdict(puk_md), + management_key_metadata=asdict(mgm_md), + ) + except NotSupportedError: + pin_attempts = self.session.get_pin_attempts() + metadata = None + + return dict( + version=self.session.version, + authenticated=self._authenticated, + derived_key=self._pivman_data.has_derived_key, + stored_key=self._pivman_data.has_stored_key, + chuid=self._get_object(OBJECT_ID.CHUID), + ccc=self._get_object(OBJECT_ID.CAPABILITY), + pin_attempts=pin_attempts, + metadata=metadata, + ) + + def _authenticate(self, key, signal): + try: + metadata = self.session.get_management_key_metadata() + key_type = metadata.key_type + if metadata.touch_policy != TOUCH_POLICY.NEVER: + signal("touch") + timer = None + except NotSupportedError: + key_type = MANAGEMENT_KEY_TYPE.TDES + timer = Timer(0.5, lambda: signal("touch")) + timer.start() + try: + # TODO: Check if this is needed, maybe SW is enough + start = time() + self.session.authenticate(key_type, key) + except ApduError as e: + if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED and time() - start > 5: + raise TimeoutException() + raise + finally: + if timer: + timer.cancel() + self._authenticated = True + + @action + def verify_pin(self, params, event, signal): + pin = params.pop("pin") + + self.session.verify_pin(pin) + key = None + + if self._pivman_data.has_derived_key: + key = derive_management_key(pin, self._pivman_data.salt) + elif self._pivman_data.has_stored_key: + pivman_prot = get_pivman_protected_data(self.session) + key = pivman_prot.key + if key: + try: + self._authenticate(key, signal) + except ApduError as e: + if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: + pass # Authenticate failed, bad derived key? + + # Ensure verify was the last thing we did + self.session.verify_pin(pin) + + return dict(status=True, authenticated=self._authenticated) + + @action + def authenticate(self, params, event, signal): + key = bytes.fromhex(params.pop("key")) + try: + self._authenticate(key, signal) + return dict(status=True) + except ApduError as e: + if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: + return dict(status=False) + raise + + @action(condition=lambda self: self._authenticated) + def set_key(self, params, event, signal): + key_type = MANAGEMENT_KEY_TYPE(params.pop("key_type", MANAGEMENT_KEY_TYPE.TDES)) + key = bytes.fromhex(params.pop("key")) + store_key = params.pop("store_key", False) + pivman_set_mgm_key(self.session, key, key_type, False, store_key) + self._pivman_data = get_pivman_data(self.session) + return dict() + + @action + def change_pin(self, params, event, signal): + old_pin = params.pop("pin") + new_pin = params.pop("new_pin") + pivman_change_pin(self.session, old_pin, new_pin) + return dict() + + @action + def change_puk(self, params, event, signal): + old_puk = params.pop("puk") + new_puk = params.pop("new_puk") + self.session.change_puk(old_puk, new_puk) + return dict() + + @action + def unblock_pin(self, params, event, signal): + puk = params.pop("puk") + new_pin = params.pop("new_pin") + self.session.unblock_pin(puk, new_pin) + return dict() + + @action + def reset(self, params, event, signal): + self.session.reset() + self._authenticated = False + self._pivman_data = get_pivman_data(self.session) + return dict() + + @child + def slots(self): + return SlotsNode(self.session) + + +def _slot_for(name): + return SLOT(int(name, base=16)) + + +def _parse_file(data, password=None): + if password: + password = password.encode() + try: + certs = parse_certificates(data, password) + except (ValueError, TypeError): + certs = [] + + try: + private_key = parse_private_key(data, password) + except (ValueError, TypeError): + private_key = None + + return private_key, certs + + +class SlotsNode(RpcNode): + def __init__(self, session): + super().__init__() + self.session = session + try: + require_version(session.version, (5, 3, 0)) + self._has_metadata = True + except NotSupportedError: + self._has_metadata = False + self.refresh() + + def refresh(self): + self._slots = {} + for slot in set(SLOT) - {SLOT.ATTESTATION}: + metadata = None + if self._has_metadata: + try: + metadata = self.session.get_slot_metadata(slot) + except (ApduError, BadResponseError): + pass + try: + certificate = self.session.get_certificate(slot) + except (ApduError, BadResponseError): + # TODO: Differentiate between none and malformed + certificate = None + self._slots[slot] = (metadata, certificate) + if self._child and _slot_for(self._child_name) not in self._slots: + self._close_child() + + def list_children(self): + return { + f"{int(slot):02x}": dict( + slot=int(slot), + name=slot.name, + has_key=metadata is not None if self._has_metadata else None, + cert_info=dict( + subject=cert.subject.rfc4514_string(), + issuer=cert.issuer.rfc4514_string(), + serial=hex(cert.serial_number)[2:], + not_valid_before=cert.not_valid_before.isoformat(), + not_valid_after=cert.not_valid_after.isoformat(), + fingerprint=cert.fingerprint(hashes.SHA256()), + ) + if cert + else None, + ) + for slot, (metadata, cert) in self._slots.items() + } + + def create_child(self, name): + slot = _slot_for(name) + if slot in self._slots: + metadata, certificate = self._slots[slot] + return SlotNode(self.session, slot, metadata, certificate, self.refresh) + return super().create_child(name) + + @action + def examine_file(self, params, event, signal): + data = bytes.fromhex(params.pop("data")) + password = params.pop("password", None) + try: + private_key, certs = _parse_file(data, password) + return dict( + status=True, + password=password is not None, + private_key=bool(private_key), + certificates=len(certs), + ) + except InvalidPasswordError: + return dict(status=False) + + +class SlotNode(RpcNode): + def __init__(self, session, slot, metadata, certificate, refresh): + super().__init__() + self.session = session + self.slot = slot + self.metadata = metadata + self.certificate = certificate + self._refresh = refresh + + def get_data(self): + return dict( + id=f"{int(self.slot):02x}", + name=self.slot.name, + metadata=asdict(self.metadata) if self.metadata else None, + certificate=self.certificate.public_bytes(encoding=Encoding.PEM).decode() + if self.certificate + else None, + ) + + @action(condition=lambda self: self.certificate) + def delete(self, params, event, signal): + self.session.delete_certificate(self.slot) + self.session.put_object(OBJECT_ID.CHUID, generate_chuid()) + self._refresh() + self.certificate = None + return dict() + + @action + def import_file(self, params, event, signal): + data = bytes.fromhex(params.pop("data")) + password = params.pop("password", None) + + try: + private_key, certs = _parse_file(data, password) + except InvalidPasswordError: + logger.debug("InvalidPassword", exc_info=True) + raise ValueError("Wrong/Missing password") + + # Exception? + if not certs and not private_key: + raise ValueError("Failed to parse") + + metadata = None + if private_key: + pin_policy = PIN_POLICY(params.pop("pin_policy", PIN_POLICY.DEFAULT)) + touch_policy = TOUCH_POLICY( + params.pop("touch_policy", TOUCH_POLICY.DEFAULT) + ) + self.session.put_key(self.slot, private_key, pin_policy, touch_policy) + try: + metadata = self.session.get_slot_metadata(self.slot) + except (ApduError, BadResponseError): + pass + + if certs: + if len(certs) > 1: + leafs = get_leaf_certificates(certs) + certificate = leafs[0] + else: + certificate = certs[0] + self.session.put_certificate(self.slot, certificate) + self.session.put_object(OBJECT_ID.CHUID, generate_chuid()) + self.certificate = certificate + + self._refresh() + + return dict( + metadata=asdict(metadata) if metadata else None, + public_key=private_key.public_key() + .public_bytes( + encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo + ) + .decode() + if private_key + else None, + certificate=self.certificate.public_bytes(encoding=Encoding.PEM).decode() + if certs + else None, + ) + + @action + def generate(self, params, event, signal): + key_type = KEY_TYPE(params.pop("key_type")) + pin_policy = PIN_POLICY(params.pop("pin_policy", PIN_POLICY.DEFAULT)) + touch_policy = TOUCH_POLICY(params.pop("touch_policy", TOUCH_POLICY.DEFAULT)) + subject = params.pop("subject") + generate_type = GENERATE_TYPE(params.pop("generate_type", GENERATE_TYPE.CERTIFICATE)) + public_key = self.session.generate_key( + self.slot, key_type, pin_policy, touch_policy + ) + + if pin_policy != PIN_POLICY.NEVER: + # TODO: Check if verified? + pin = params.pop("pin") + self.session.verify_pin(pin) + + if touch_policy in (TOUCH_POLICY.ALWAYS, TOUCH_POLICY.CACHED): + signal("touch") + + if generate_type == GENERATE_TYPE.CSR: + result = generate_csr(self.session, self.slot, public_key, subject) + elif generate_type == GENERATE_TYPE.CERTIFICATE: + now = datetime.datetime.utcnow() + then = now + datetime.timedelta(days=365) + valid_from = params.pop("valid_from", now.strftime(_date_format)) + valid_to = params.pop("valid_to", then.strftime(_date_format)) + result = generate_self_signed_certificate( + self.session, + self.slot, + public_key, + subject, + datetime.datetime.strptime(valid_from, _date_format), + datetime.datetime.strptime(valid_to, _date_format), + ) + self.session.put_certificate(self.slot, result) + self.session.put_object(OBJECT_ID.CHUID, generate_chuid()) + else: + raise ValueError("Unsupported GENERATE_TYPE") + + self._refresh() + + return dict( + public_key=public_key.public_bytes( + encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo + ).decode(), + result=result.public_bytes(encoding=Encoding.PEM).decode(), + ) diff --git a/lib/app/models.dart b/lib/app/models.dart index 22c4875f..78cec6be 100755 --- a/lib/app/models.dart +++ b/lib/app/models.dart @@ -53,6 +53,7 @@ enum Application { String getDisplayName(AppLocalizations l10n) => switch (this) { Application.oath => l10n.s_authenticator, Application.fido => l10n.s_webauthn, + Application.piv => "PIV", //TODO _ => 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 229272c3..46d87bbb 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -26,6 +26,7 @@ import '../../fido/views/fido_screen.dart'; import '../../oath/models.dart'; import '../../oath/views/add_account_page.dart'; import '../../oath/views/oath_screen.dart'; +import '../../piv/views/piv_screen.dart'; import '../../widgets/custom_icons.dart'; import '../message.dart'; import '../models.dart'; @@ -161,6 +162,7 @@ class MainPage extends ConsumerWidget { return switch (app) { Application.oath => OathScreen(data.node.path), Application.fido => FidoScreen(data), + Application.piv => PivScreen(data.node.path), _ => MessagePage( header: l10n.s_app_not_supported, message: l10n.l_app_not_supported_desc, diff --git a/lib/core/models.dart b/lib/core/models.dart index eee2b3c6..4187431f 100644 --- a/lib/core/models.dart +++ b/lib/core/models.dart @@ -16,6 +16,7 @@ import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:intl/intl.dart'; import '../management/models.dart'; @@ -152,3 +153,5 @@ class Version with _$Version implements Comparable { return a - b; } } + +final DateFormat dateFormatter = DateFormat('yyyy-MM-dd'); diff --git a/lib/desktop/init.dart b/lib/desktop/init.dart index 04885c0b..76f74492 100755 --- a/lib/desktop/init.dart +++ b/lib/desktop/init.dart @@ -41,11 +41,13 @@ import '../core/state.dart'; import '../fido/state.dart'; import '../management/state.dart'; import '../oath/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 'qr_scanner.dart'; import 'rpc.dart'; import 'state.dart'; @@ -177,6 +179,7 @@ Future initialize(List argv) async { supportedAppsProvider.overrideWithValue([ Application.oath, Application.fido, + Application.piv, Application.management, ]), prefProvider.overrideWithValue(prefs), @@ -184,6 +187,12 @@ Future initialize(List argv) async { windowStateProvider.overrideWith( (ref) => ref.watch(desktopWindowStateProvider), ), + clipboardProvider.overrideWith( + (ref) => ref.watch(desktopClipboardProvider), + ), + supportedThemesProvider.overrideWith( + (ref) => ref.watch(desktopSupportedThemesProvider), + ), attachedDevicesProvider.overrideWith( () => DesktopDevicesNotifier(), ), @@ -206,12 +215,9 @@ Future initialize(List argv) async { fidoStateProvider.overrideWithProvider(desktopFidoState), fingerprintProvider.overrideWithProvider(desktopFingerprintProvider), credentialProvider.overrideWithProvider(desktopCredentialProvider), - clipboardProvider.overrideWith( - (ref) => ref.watch(desktopClipboardProvider), - ), - supportedThemesProvider.overrideWith( - (ref) => ref.watch(desktopSupportedThemesProvider), - ) + // PIV + pivStateProvider.overrideWithProvider(desktopPivState), + pivSlotsProvider.overrideWithProvider(desktopPivSlots), ], child: YubicoAuthenticatorApp( page: Consumer( diff --git a/lib/desktop/piv/state.dart b/lib/desktop/piv/state.dart new file mode 100644 index 00000000..27c9a961 --- /dev/null +++ b/lib/desktop/piv/state.dart @@ -0,0 +1,425 @@ +/* + * 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/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:logging/logging.dart'; +import 'package:yubico_authenticator/desktop/models.dart'; + +import '../../app/logging.dart'; +import '../../app/models.dart'; +import '../../app/state.dart'; +import '../../app/views/user_interaction.dart'; +import '../../core/models.dart'; +import '../../piv/models.dart'; +import '../../piv/state.dart'; +import '../rpc.dart'; +import '../state.dart'; + +final _log = Logger('desktop.piv.state'); + +final _managementKeyProvider = + StateProvider.autoDispose.family( + (ref, _) => null, +); + +final _pinProvider = StateProvider.autoDispose.family( + (ref, _) => null, +); + +final _sessionProvider = + Provider.autoDispose.family( + (ref, devicePath) { + // Make sure the managementKey and PIN are held for the duration of the session. + ref.watch(_managementKeyProvider(devicePath)); + ref.watch(_pinProvider(devicePath)); + return RpcNodeSession( + ref.watch(rpcProvider).requireValue, devicePath, ['ccid', 'piv']); + }, +); + +final desktopPivState = AsyncNotifierProvider.autoDispose + .family( + _DesktopPivStateNotifier.new); + +class _DesktopPivStateNotifier extends PivStateNotifier { + late RpcNodeSession _session; + late DevicePath _devicePath; + + @override + FutureOr build(DevicePath devicePath) async { + _session = ref.watch(_sessionProvider(devicePath)); + _session + ..setErrorHandler('state-reset', (_) async { + ref.invalidate(_sessionProvider(devicePath)); + }) + ..setErrorHandler('auth-required', (_) async { + final String? mgmtKey; + if (state.valueOrNull?.metadata?.managementKeyMetadata.defaultValue == + true) { + mgmtKey = defaultManagementKey; + } else { + mgmtKey = ref.read(_managementKeyProvider(devicePath)); + } + if (mgmtKey != null) { + if (await authenticate(mgmtKey)) { + ref.invalidateSelf(); + } else { + ref.read(_managementKeyProvider(devicePath).notifier).state = null; + } + } + }); + ref.onDispose(() { + _session + ..unsetErrorHandler('state-reset') + ..unsetErrorHandler('auth-required'); + }); + _devicePath = devicePath; + + final result = await _session.command('get'); + _log.debug('application status', jsonEncode(result)); + final pivState = PivState.fromJson(result['data']); + + return pivState; + } + + @override + Future reset() async { + await _session.command('reset'); + ref.invalidate(_sessionProvider(_session.devicePath)); + } + + @override + Future authenticate(String managementKey) async { + final withContext = ref.watch(withContextProvider); + + final signaler = Signaler(); + UserInteractionController? controller; + try { + signaler.signals.listen((signal) async { + if (signal.status == 'touch') { + controller = await withContext( + (context) async { + final l10n = AppLocalizations.of(context)!; + return promptUserInteraction( + context, + icon: const Icon(Icons.touch_app), + title: l10n.s_touch_required, + description: l10n.l_touch_button_now, + ); + }, + ); + } + }); + + final result = await _session.command( + 'authenticate', + params: {'key': managementKey}, + signal: signaler, + ); + + if (result['status']) { + ref.read(_managementKeyProvider(_devicePath).notifier).state = + managementKey; + final oldState = state.valueOrNull; + if (oldState != null) { + state = AsyncData(oldState.copyWith(authenticated: true)); + } + return true; + } else { + return false; + } + } finally { + controller?.close(); + } + } + + @override + Future verifyPin(String pin) async { + final pivState = state.valueOrNull; + + final signaler = Signaler(); + UserInteractionController? controller; + try { + if (pivState?.protectedKey == true) { + // Might require touch as this will also authenticate + final withContext = ref.watch(withContextProvider); + signaler.signals.listen((signal) async { + if (signal.status == 'touch') { + controller = await withContext( + (context) async { + final l10n = AppLocalizations.of(context)!; + return promptUserInteraction( + context, + icon: const Icon(Icons.touch_app), + title: l10n.s_touch_required, + description: l10n.l_touch_button_now, + ); + }, + ); + } + }); + } + await _session.command( + 'verify_pin', + params: {'pin': pin}, + signal: signaler, + ); + + ref.read(_pinProvider(_devicePath).notifier).state = pin; + + return const PinVerificationStatus.success(); + } on RpcError catch (e) { + if (e.status == 'invalid-pin') { + return PinVerificationStatus.failure(e.body['attempts_remaining']); + } + rethrow; + } finally { + controller?.close(); + ref.invalidateSelf(); + } + } + + @override + Future changePin(String pin, String newPin) async { + try { + await _session.command( + 'change_pin', + params: {'pin': pin, 'new_pin': newPin}, + ); + ref.read(_pinProvider(_devicePath).notifier).state = null; + return const PinVerificationStatus.success(); + } on RpcError catch (e) { + if (e.status == 'invalid-pin') { + return PinVerificationStatus.failure(e.body['attempts_remaining']); + } + rethrow; + } finally { + ref.invalidateSelf(); + } + } + + @override + Future changePuk(String puk, String newPuk) async { + try { + await _session.command( + 'change_puk', + params: {'puk': puk, 'new_puk': newPuk}, + ); + return const PinVerificationStatus.success(); + } on RpcError catch (e) { + if (e.status == 'invalid-pin') { + return PinVerificationStatus.failure(e.body['attempts_remaining']); + } + rethrow; + } finally { + ref.invalidateSelf(); + } + } + + @override + Future setManagementKey(String managementKey, + {ManagementKeyType managementKeyType = defaultManagementKeyType, + bool storeKey = false}) async { + await _session.command( + 'set_key', + params: { + 'key': managementKey, + 'key_type': managementKeyType.value, + 'store_key': storeKey, + }, + ); + ref.invalidateSelf(); + } + + @override + Future unblockPin(String puk, String newPin) async { + try { + await _session.command( + 'unblock_pin', + params: {'puk': puk, 'new_pin': newPin}, + ); + return const PinVerificationStatus.success(); + } on RpcError catch (e) { + if (e.status == 'invalid-pin') { + return PinVerificationStatus.failure(e.body['attempts_remaining']); + } + rethrow; + } finally { + ref.invalidateSelf(); + } + } +} + +final _shownSlots = SlotId.values.map((slot) => slot.id).toList(); + +extension on SlotId { + String get node => id.toRadixString(16).padLeft(2, '0'); +} + +final desktopPivSlots = AsyncNotifierProvider.autoDispose + .family, DevicePath>( + _DesktopPivSlotsNotifier.new); + +class _DesktopPivSlotsNotifier extends PivSlotsNotifier { + late RpcNodeSession _session; + + @override + FutureOr> build(DevicePath devicePath) async { + _session = ref.watch(_sessionProvider(devicePath)); + + final result = await _session.command('get', target: ['slots']); + return (result['children'] as Map) + .values + .where((e) => _shownSlots.contains(e['slot'])) + .map((e) => PivSlot.fromJson(e)) + .toList(); + } + + @override + Future delete(SlotId slot) async { + await _session.command('delete', target: ['slots', slot.node]); + ref.invalidateSelf(); + } + + @override + Future generate( + SlotId slot, + KeyType keyType, { + required PivGenerateParameters parameters, + PinPolicy pinPolicy = PinPolicy.dfault, + TouchPolicy touchPolicy = TouchPolicy.dfault, + String? pin, + }) async { + final withContext = ref.watch(withContextProvider); + + final signaler = Signaler(); + UserInteractionController? controller; + try { + signaler.signals.listen((signal) async { + if (signal.status == 'touch') { + controller = await withContext( + (context) async { + final l10n = AppLocalizations.of(context)!; + return promptUserInteraction( + context, + icon: const Icon(Icons.touch_app), + title: l10n.s_touch_required, + description: l10n.l_touch_button_now, + ); + }, + ); + } + }); + + final (type, subject, validFrom, validTo) = parameters.when( + certificate: (subject, validFrom, validTo) => ( + GenerateType.certificate, + subject, + dateFormatter.format(validFrom), + dateFormatter.format(validTo), + ), + csr: (subject) => ( + GenerateType.csr, + subject, + null, + null, + ), + ); + + final pin = ref.read(_pinProvider(_session.devicePath)); + + final result = await _session.command( + 'generate', + target: [ + 'slots', + slot.node, + ], + params: { + 'key_type': keyType.value, + 'pin_policy': pinPolicy.value, + 'touch_policy': touchPolicy.value, + 'subject': subject, + 'generate_type': type.name, + 'valid_from': validFrom, + 'valid_to': validTo, + 'pin': pin, + }, + signal: signaler, + ); + + ref.invalidateSelf(); + + return PivGenerateResult.fromJson( + {'generate_type': type.name, ...result}); + } finally { + controller?.close(); + } + } + + @override + Future examine(String data, {String? password}) async { + final result = await _session.command('examine_file', target: [ + 'slots', + ], params: { + 'data': data, + 'password': password, + }); + + if (result['status']) { + return PivExamineResult.fromJson({'runtimeType': 'result', ...result}); + } else { + return PivExamineResult.invalidPassword(); + } + } + + @override + Future import(SlotId slot, String data, + {String? password, + PinPolicy pinPolicy = PinPolicy.dfault, + TouchPolicy touchPolicy = TouchPolicy.dfault}) async { + final result = await _session.command('import_file', target: [ + 'slots', + slot.node, + ], params: { + 'data': data, + 'password': password, + 'pin_policy': pinPolicy.value, + 'touch_policy': touchPolicy.value, + }); + + ref.invalidateSelf(); + return PivImportResult.fromJson(result); + } + + @override + Future<(SlotMetadata?, String?)> read(SlotId slot) async { + final result = await _session.command('get', target: [ + 'slots', + slot.node, + ]); + final data = result['data']; + final metadata = data['metadata']; + return ( + metadata != null ? SlotMetadata.fromJson(metadata) : null, + data['certificate'] as String?, + ); + } +} diff --git a/lib/oath/views/actions.dart b/lib/oath/views/actions.dart index d10868b3..cea57615 100755 --- a/lib/oath/views/actions.dart +++ b/lib/oath/views/actions.dart @@ -1,3 +1,19 @@ +/* + * 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'; diff --git a/lib/piv/keys.dart b/lib/piv/keys.dart new file mode 100644 index 00000000..3ee1b6ee --- /dev/null +++ b/lib/piv/keys.dart @@ -0,0 +1,35 @@ +/* + * 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 = 'piv.keys'; + +const managePinAction = Key('$_prefix.manage_pin'); +const managePukAction = Key('$_prefix.manage_puk'); +const manageManagementKeyAction = Key('$_prefix.manage_management_key'); +const resetAction = Key('$_prefix.reset'); + +const setupMacOsAction = Key('$_prefix.setup_macos'); +const saveButton = Key('$_prefix.save'); +const deleteButton = Key('$_prefix.delete'); +const unlockButton = Key('$_prefix.unlock'); + +const managementKeyField = Key('$_prefix.management_key'); +const pinPukField = Key('$_prefix.pin_puk'); +const newPinPukField = Key('$_prefix.new_pin_puk'); +const confirmPinPukField = Key('$_prefix.confirm_pin_puk'); +const subjectField = Key('$_prefix.subject'); diff --git a/lib/piv/models.dart b/lib/piv/models.dart new file mode 100644 index 00000000..4a77206a --- /dev/null +++ b/lib/piv/models.dart @@ -0,0 +1,313 @@ +/* + * 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'; + +import '../core/models.dart'; + +part 'models.freezed.dart'; +part 'models.g.dart'; + +const defaultManagementKey = '010203040506070801020304050607080102030405060708'; +const defaultManagementKeyType = ManagementKeyType.tdes; +const defaultKeyType = KeyType.rsa2048; +const defaultGenerateType = GenerateType.certificate; + +enum GenerateType { + certificate, + csr; + + String getDisplayName(AppLocalizations l10n) { + return switch (this) { + // TODO: + _ => name + }; + } +} + +enum SlotId { + authentication(0x9a), + signature(0x9c), + keyManagement(0x9d), + cardAuth(0x9e); + + final int id; + const SlotId(this.id); + + String getDisplayName(AppLocalizations l10n) { + return switch (this) { + // TODO: + _ => name + }; + } + + factory SlotId.fromJson(int value) => + SlotId.values.firstWhere((e) => e.id == value); +} + +@JsonEnum(alwaysCreate: true) +enum PinPolicy { + @JsonValue(0x00) + dfault, + @JsonValue(0x01) + never, + @JsonValue(0x02) + once, + @JsonValue(0x03) + always; + + const PinPolicy(); + + int get value => _$PinPolicyEnumMap[this]!; + + String getDisplayName(AppLocalizations l10n) { + return switch (this) { + // TODO: + _ => name + }; + } +} + +@JsonEnum(alwaysCreate: true) +enum TouchPolicy { + @JsonValue(0x00) + dfault, + @JsonValue(0x01) + never, + @JsonValue(0x02) + always, + @JsonValue(0x03) + cached; + + const TouchPolicy(); + + int get value => _$TouchPolicyEnumMap[this]!; + + String getDisplayName(AppLocalizations l10n) { + return switch (this) { + // TODO: + _ => name + }; + } +} + +@JsonEnum(alwaysCreate: true) +enum KeyType { + @JsonValue(0x06) + rsa1024, + @JsonValue(0x07) + rsa2048, + @JsonValue(0x11) + eccp256, + @JsonValue(0x14) + eccp384; + + const KeyType(); + + int get value => _$KeyTypeEnumMap[this]!; + + String getDisplayName(AppLocalizations l10n) { + return switch (this) { + // TODO: + _ => name + }; + } +} + +enum ManagementKeyType { + @JsonValue(0x03) + tdes, + @JsonValue(0x08) + aes128, + @JsonValue(0x0A) + aes192, + @JsonValue(0x0C) + aes256; + + const ManagementKeyType(); + + int get value => _$ManagementKeyTypeEnumMap[this]!; + + int get keyLength => switch (this) { + ManagementKeyType.tdes => 24, + ManagementKeyType.aes128 => 16, + ManagementKeyType.aes192 => 24, + ManagementKeyType.aes256 => 32, + }; + + String getDisplayName(AppLocalizations l10n) { + return switch (this) { + // TODO: + _ => name + }; + } +} + +@freezed +class PinMetadata with _$PinMetadata { + factory PinMetadata( + bool defaultValue, + int totalAttempts, + int attemptsRemaining, + ) = _PinMetadata; + + factory PinMetadata.fromJson(Map json) => + _$PinMetadataFromJson(json); +} + +@freezed +class PinVerificationStatus with _$PinVerificationStatus { + const factory PinVerificationStatus.success() = _PinSuccess; + factory PinVerificationStatus.failure(int attemptsRemaining) = _PinFailure; +} + +@freezed +class ManagementKeyMetadata with _$ManagementKeyMetadata { + factory ManagementKeyMetadata( + ManagementKeyType keyType, + bool defaultValue, + TouchPolicy touchPolicy, + ) = _ManagementKeyMetadata; + + factory ManagementKeyMetadata.fromJson(Map json) => + _$ManagementKeyMetadataFromJson(json); +} + +@freezed +class SlotMetadata with _$SlotMetadata { + factory SlotMetadata( + KeyType keyType, + PinPolicy pinPolicy, + TouchPolicy touchPolicy, + bool generated, + String publicKeyEncoded, + ) = _SlotMetadata; + + factory SlotMetadata.fromJson(Map json) => + _$SlotMetadataFromJson(json); +} + +@freezed +class PivStateMetadata with _$PivStateMetadata { + factory PivStateMetadata({ + required ManagementKeyMetadata managementKeyMetadata, + required PinMetadata pinMetadata, + required PinMetadata pukMetadata, + }) = _PivStateMetadata; + + factory PivStateMetadata.fromJson(Map json) => + _$PivStateMetadataFromJson(json); +} + +@freezed +class PivState with _$PivState { + const PivState._(); + + factory PivState({ + required Version version, + required bool authenticated, + required bool derivedKey, + required bool storedKey, + required int pinAttempts, + String? chuid, + String? ccc, + PivStateMetadata? metadata, + }) = _PivState; + + bool get protectedKey => derivedKey || storedKey; + bool get needsAuth => + !authenticated && metadata?.managementKeyMetadata.defaultValue != true; + + factory PivState.fromJson(Map json) => + _$PivStateFromJson(json); +} + +@freezed +class CertInfo with _$CertInfo { + factory CertInfo({ + required String subject, + required String issuer, + required String serial, + required String notValidBefore, + required String notValidAfter, + required String fingerprint, + }) = _CertInfo; + + factory CertInfo.fromJson(Map json) => + _$CertInfoFromJson(json); +} + +@freezed +class PivSlot with _$PivSlot { + factory PivSlot({ + required SlotId slot, + bool? hasKey, + CertInfo? certInfo, + }) = _PivSlot; + + factory PivSlot.fromJson(Map json) => + _$PivSlotFromJson(json); +} + +@freezed +class PivExamineResult with _$PivExamineResult { + factory PivExamineResult.result({ + required bool password, + required bool privateKey, + required int certificates, + }) = _ExamineResult; + factory PivExamineResult.invalidPassword() = _InvalidPassword; + + factory PivExamineResult.fromJson(Map json) => + _$PivExamineResultFromJson(json); +} + +@freezed +class PivGenerateParameters with _$PivGenerateParameters { + factory PivGenerateParameters.certificate({ + required String subject, + required DateTime validFrom, + required DateTime validTo, + }) = _GenerateCertificate; + factory PivGenerateParameters.csr({ + required String subject, + }) = _GenerateCsr; +} + +@freezed +class PivGenerateResult with _$PivGenerateResult { + factory PivGenerateResult({ + required GenerateType generateType, + required String publicKey, + required String result, + }) = _PivGenerateResult; + + factory PivGenerateResult.fromJson(Map json) => + _$PivGenerateResultFromJson(json); +} + +@freezed +class PivImportResult with _$PivImportResult { + factory PivImportResult({ + required SlotMetadata? metadata, + required String? publicKey, + required String? certificate, + }) = _PivImportResult; + + factory PivImportResult.fromJson(Map json) => + _$PivImportResultFromJson(json); +} diff --git a/lib/piv/models.freezed.dart b/lib/piv/models.freezed.dart new file mode 100644 index 00000000..8a1637f5 --- /dev/null +++ b/lib/piv/models.freezed.dart @@ -0,0 +1,2984 @@ +// 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'); + +PinMetadata _$PinMetadataFromJson(Map json) { + return _PinMetadata.fromJson(json); +} + +/// @nodoc +mixin _$PinMetadata { + bool get defaultValue => throw _privateConstructorUsedError; + int get totalAttempts => throw _privateConstructorUsedError; + int get attemptsRemaining => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PinMetadataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PinMetadataCopyWith<$Res> { + factory $PinMetadataCopyWith( + PinMetadata value, $Res Function(PinMetadata) then) = + _$PinMetadataCopyWithImpl<$Res, PinMetadata>; + @useResult + $Res call({bool defaultValue, int totalAttempts, int attemptsRemaining}); +} + +/// @nodoc +class _$PinMetadataCopyWithImpl<$Res, $Val extends PinMetadata> + implements $PinMetadataCopyWith<$Res> { + _$PinMetadataCopyWithImpl(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? defaultValue = null, + Object? totalAttempts = null, + Object? attemptsRemaining = null, + }) { + return _then(_value.copyWith( + defaultValue: null == defaultValue + ? _value.defaultValue + : defaultValue // ignore: cast_nullable_to_non_nullable + as bool, + totalAttempts: null == totalAttempts + ? _value.totalAttempts + : totalAttempts // ignore: cast_nullable_to_non_nullable + as int, + attemptsRemaining: null == attemptsRemaining + ? _value.attemptsRemaining + : attemptsRemaining // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_PinMetadataCopyWith<$Res> + implements $PinMetadataCopyWith<$Res> { + factory _$$_PinMetadataCopyWith( + _$_PinMetadata value, $Res Function(_$_PinMetadata) then) = + __$$_PinMetadataCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool defaultValue, int totalAttempts, int attemptsRemaining}); +} + +/// @nodoc +class __$$_PinMetadataCopyWithImpl<$Res> + extends _$PinMetadataCopyWithImpl<$Res, _$_PinMetadata> + implements _$$_PinMetadataCopyWith<$Res> { + __$$_PinMetadataCopyWithImpl( + _$_PinMetadata _value, $Res Function(_$_PinMetadata) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? defaultValue = null, + Object? totalAttempts = null, + Object? attemptsRemaining = null, + }) { + return _then(_$_PinMetadata( + null == defaultValue + ? _value.defaultValue + : defaultValue // ignore: cast_nullable_to_non_nullable + as bool, + null == totalAttempts + ? _value.totalAttempts + : totalAttempts // ignore: cast_nullable_to_non_nullable + as int, + null == attemptsRemaining + ? _value.attemptsRemaining + : attemptsRemaining // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_PinMetadata implements _PinMetadata { + _$_PinMetadata(this.defaultValue, this.totalAttempts, this.attemptsRemaining); + + factory _$_PinMetadata.fromJson(Map json) => + _$$_PinMetadataFromJson(json); + + @override + final bool defaultValue; + @override + final int totalAttempts; + @override + final int attemptsRemaining; + + @override + String toString() { + return 'PinMetadata(defaultValue: $defaultValue, totalAttempts: $totalAttempts, attemptsRemaining: $attemptsRemaining)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PinMetadata && + (identical(other.defaultValue, defaultValue) || + other.defaultValue == defaultValue) && + (identical(other.totalAttempts, totalAttempts) || + other.totalAttempts == totalAttempts) && + (identical(other.attemptsRemaining, attemptsRemaining) || + other.attemptsRemaining == attemptsRemaining)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, defaultValue, totalAttempts, attemptsRemaining); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PinMetadataCopyWith<_$_PinMetadata> get copyWith => + __$$_PinMetadataCopyWithImpl<_$_PinMetadata>(this, _$identity); + + @override + Map toJson() { + return _$$_PinMetadataToJson( + this, + ); + } +} + +abstract class _PinMetadata implements PinMetadata { + factory _PinMetadata(final bool defaultValue, final int totalAttempts, + final int attemptsRemaining) = _$_PinMetadata; + + factory _PinMetadata.fromJson(Map json) = + _$_PinMetadata.fromJson; + + @override + bool get defaultValue; + @override + int get totalAttempts; + @override + int get attemptsRemaining; + @override + @JsonKey(ignore: true) + _$$_PinMetadataCopyWith<_$_PinMetadata> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$PinVerificationStatus { + @optionalTypeArgs + TResult when({ + required TResult Function() success, + required TResult Function(int attemptsRemaining) failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? success, + TResult? Function(int attemptsRemaining)? failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? success, + TResult Function(int attemptsRemaining)? failure, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_PinSuccess value) success, + required TResult Function(_PinFailure value) failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_PinSuccess value)? success, + TResult? Function(_PinFailure value)? failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_PinSuccess value)? success, + TResult Function(_PinFailure value)? failure, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PinVerificationStatusCopyWith<$Res> { + factory $PinVerificationStatusCopyWith(PinVerificationStatus value, + $Res Function(PinVerificationStatus) then) = + _$PinVerificationStatusCopyWithImpl<$Res, PinVerificationStatus>; +} + +/// @nodoc +class _$PinVerificationStatusCopyWithImpl<$Res, + $Val extends PinVerificationStatus> + implements $PinVerificationStatusCopyWith<$Res> { + _$PinVerificationStatusCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$_PinSuccessCopyWith<$Res> { + factory _$$_PinSuccessCopyWith( + _$_PinSuccess value, $Res Function(_$_PinSuccess) then) = + __$$_PinSuccessCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_PinSuccessCopyWithImpl<$Res> + extends _$PinVerificationStatusCopyWithImpl<$Res, _$_PinSuccess> + implements _$$_PinSuccessCopyWith<$Res> { + __$$_PinSuccessCopyWithImpl( + _$_PinSuccess _value, $Res Function(_$_PinSuccess) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_PinSuccess implements _PinSuccess { + const _$_PinSuccess(); + + @override + String toString() { + return 'PinVerificationStatus.success()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_PinSuccess); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() success, + required TResult Function(int attemptsRemaining) failure, + }) { + return success(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? success, + TResult? Function(int attemptsRemaining)? failure, + }) { + return success?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? success, + TResult Function(int attemptsRemaining)? failure, + required TResult orElse(), + }) { + if (success != null) { + return success(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_PinSuccess value) success, + required TResult Function(_PinFailure value) failure, + }) { + return success(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_PinSuccess value)? success, + TResult? Function(_PinFailure value)? failure, + }) { + return success?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_PinSuccess value)? success, + TResult Function(_PinFailure value)? failure, + required TResult orElse(), + }) { + if (success != null) { + return success(this); + } + return orElse(); + } +} + +abstract class _PinSuccess implements PinVerificationStatus { + const factory _PinSuccess() = _$_PinSuccess; +} + +/// @nodoc +abstract class _$$_PinFailureCopyWith<$Res> { + factory _$$_PinFailureCopyWith( + _$_PinFailure value, $Res Function(_$_PinFailure) then) = + __$$_PinFailureCopyWithImpl<$Res>; + @useResult + $Res call({int attemptsRemaining}); +} + +/// @nodoc +class __$$_PinFailureCopyWithImpl<$Res> + extends _$PinVerificationStatusCopyWithImpl<$Res, _$_PinFailure> + implements _$$_PinFailureCopyWith<$Res> { + __$$_PinFailureCopyWithImpl( + _$_PinFailure _value, $Res Function(_$_PinFailure) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? attemptsRemaining = null, + }) { + return _then(_$_PinFailure( + null == attemptsRemaining + ? _value.attemptsRemaining + : attemptsRemaining // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc + +class _$_PinFailure implements _PinFailure { + _$_PinFailure(this.attemptsRemaining); + + @override + final int attemptsRemaining; + + @override + String toString() { + return 'PinVerificationStatus.failure(attemptsRemaining: $attemptsRemaining)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PinFailure && + (identical(other.attemptsRemaining, attemptsRemaining) || + other.attemptsRemaining == attemptsRemaining)); + } + + @override + int get hashCode => Object.hash(runtimeType, attemptsRemaining); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PinFailureCopyWith<_$_PinFailure> get copyWith => + __$$_PinFailureCopyWithImpl<_$_PinFailure>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() success, + required TResult Function(int attemptsRemaining) failure, + }) { + return failure(attemptsRemaining); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? success, + TResult? Function(int attemptsRemaining)? failure, + }) { + return failure?.call(attemptsRemaining); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? success, + TResult Function(int attemptsRemaining)? failure, + required TResult orElse(), + }) { + if (failure != null) { + return failure(attemptsRemaining); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_PinSuccess value) success, + required TResult Function(_PinFailure value) failure, + }) { + return failure(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_PinSuccess value)? success, + TResult? Function(_PinFailure value)? failure, + }) { + return failure?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_PinSuccess value)? success, + TResult Function(_PinFailure value)? failure, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this); + } + return orElse(); + } +} + +abstract class _PinFailure implements PinVerificationStatus { + factory _PinFailure(final int attemptsRemaining) = _$_PinFailure; + + int get attemptsRemaining; + @JsonKey(ignore: true) + _$$_PinFailureCopyWith<_$_PinFailure> get copyWith => + throw _privateConstructorUsedError; +} + +ManagementKeyMetadata _$ManagementKeyMetadataFromJson( + Map json) { + return _ManagementKeyMetadata.fromJson(json); +} + +/// @nodoc +mixin _$ManagementKeyMetadata { + ManagementKeyType get keyType => throw _privateConstructorUsedError; + bool get defaultValue => throw _privateConstructorUsedError; + TouchPolicy get touchPolicy => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ManagementKeyMetadataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ManagementKeyMetadataCopyWith<$Res> { + factory $ManagementKeyMetadataCopyWith(ManagementKeyMetadata value, + $Res Function(ManagementKeyMetadata) then) = + _$ManagementKeyMetadataCopyWithImpl<$Res, ManagementKeyMetadata>; + @useResult + $Res call( + {ManagementKeyType keyType, bool defaultValue, TouchPolicy touchPolicy}); +} + +/// @nodoc +class _$ManagementKeyMetadataCopyWithImpl<$Res, + $Val extends ManagementKeyMetadata> + implements $ManagementKeyMetadataCopyWith<$Res> { + _$ManagementKeyMetadataCopyWithImpl(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? keyType = null, + Object? defaultValue = null, + Object? touchPolicy = null, + }) { + return _then(_value.copyWith( + keyType: null == keyType + ? _value.keyType + : keyType // ignore: cast_nullable_to_non_nullable + as ManagementKeyType, + defaultValue: null == defaultValue + ? _value.defaultValue + : defaultValue // ignore: cast_nullable_to_non_nullable + as bool, + touchPolicy: null == touchPolicy + ? _value.touchPolicy + : touchPolicy // ignore: cast_nullable_to_non_nullable + as TouchPolicy, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_ManagementKeyMetadataCopyWith<$Res> + implements $ManagementKeyMetadataCopyWith<$Res> { + factory _$$_ManagementKeyMetadataCopyWith(_$_ManagementKeyMetadata value, + $Res Function(_$_ManagementKeyMetadata) then) = + __$$_ManagementKeyMetadataCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {ManagementKeyType keyType, bool defaultValue, TouchPolicy touchPolicy}); +} + +/// @nodoc +class __$$_ManagementKeyMetadataCopyWithImpl<$Res> + extends _$ManagementKeyMetadataCopyWithImpl<$Res, _$_ManagementKeyMetadata> + implements _$$_ManagementKeyMetadataCopyWith<$Res> { + __$$_ManagementKeyMetadataCopyWithImpl(_$_ManagementKeyMetadata _value, + $Res Function(_$_ManagementKeyMetadata) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? keyType = null, + Object? defaultValue = null, + Object? touchPolicy = null, + }) { + return _then(_$_ManagementKeyMetadata( + null == keyType + ? _value.keyType + : keyType // ignore: cast_nullable_to_non_nullable + as ManagementKeyType, + null == defaultValue + ? _value.defaultValue + : defaultValue // ignore: cast_nullable_to_non_nullable + as bool, + null == touchPolicy + ? _value.touchPolicy + : touchPolicy // ignore: cast_nullable_to_non_nullable + as TouchPolicy, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_ManagementKeyMetadata implements _ManagementKeyMetadata { + _$_ManagementKeyMetadata(this.keyType, this.defaultValue, this.touchPolicy); + + factory _$_ManagementKeyMetadata.fromJson(Map json) => + _$$_ManagementKeyMetadataFromJson(json); + + @override + final ManagementKeyType keyType; + @override + final bool defaultValue; + @override + final TouchPolicy touchPolicy; + + @override + String toString() { + return 'ManagementKeyMetadata(keyType: $keyType, defaultValue: $defaultValue, touchPolicy: $touchPolicy)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_ManagementKeyMetadata && + (identical(other.keyType, keyType) || other.keyType == keyType) && + (identical(other.defaultValue, defaultValue) || + other.defaultValue == defaultValue) && + (identical(other.touchPolicy, touchPolicy) || + other.touchPolicy == touchPolicy)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, keyType, defaultValue, touchPolicy); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_ManagementKeyMetadataCopyWith<_$_ManagementKeyMetadata> get copyWith => + __$$_ManagementKeyMetadataCopyWithImpl<_$_ManagementKeyMetadata>( + this, _$identity); + + @override + Map toJson() { + return _$$_ManagementKeyMetadataToJson( + this, + ); + } +} + +abstract class _ManagementKeyMetadata implements ManagementKeyMetadata { + factory _ManagementKeyMetadata( + final ManagementKeyType keyType, + final bool defaultValue, + final TouchPolicy touchPolicy) = _$_ManagementKeyMetadata; + + factory _ManagementKeyMetadata.fromJson(Map json) = + _$_ManagementKeyMetadata.fromJson; + + @override + ManagementKeyType get keyType; + @override + bool get defaultValue; + @override + TouchPolicy get touchPolicy; + @override + @JsonKey(ignore: true) + _$$_ManagementKeyMetadataCopyWith<_$_ManagementKeyMetadata> get copyWith => + throw _privateConstructorUsedError; +} + +SlotMetadata _$SlotMetadataFromJson(Map json) { + return _SlotMetadata.fromJson(json); +} + +/// @nodoc +mixin _$SlotMetadata { + KeyType get keyType => throw _privateConstructorUsedError; + PinPolicy get pinPolicy => throw _privateConstructorUsedError; + TouchPolicy get touchPolicy => throw _privateConstructorUsedError; + bool get generated => throw _privateConstructorUsedError; + String get publicKeyEncoded => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $SlotMetadataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SlotMetadataCopyWith<$Res> { + factory $SlotMetadataCopyWith( + SlotMetadata value, $Res Function(SlotMetadata) then) = + _$SlotMetadataCopyWithImpl<$Res, SlotMetadata>; + @useResult + $Res call( + {KeyType keyType, + PinPolicy pinPolicy, + TouchPolicy touchPolicy, + bool generated, + String publicKeyEncoded}); +} + +/// @nodoc +class _$SlotMetadataCopyWithImpl<$Res, $Val extends SlotMetadata> + implements $SlotMetadataCopyWith<$Res> { + _$SlotMetadataCopyWithImpl(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? keyType = null, + Object? pinPolicy = null, + Object? touchPolicy = null, + Object? generated = null, + Object? publicKeyEncoded = null, + }) { + return _then(_value.copyWith( + keyType: null == keyType + ? _value.keyType + : keyType // ignore: cast_nullable_to_non_nullable + as KeyType, + pinPolicy: null == pinPolicy + ? _value.pinPolicy + : pinPolicy // ignore: cast_nullable_to_non_nullable + as PinPolicy, + touchPolicy: null == touchPolicy + ? _value.touchPolicy + : touchPolicy // ignore: cast_nullable_to_non_nullable + as TouchPolicy, + generated: null == generated + ? _value.generated + : generated // ignore: cast_nullable_to_non_nullable + as bool, + publicKeyEncoded: null == publicKeyEncoded + ? _value.publicKeyEncoded + : publicKeyEncoded // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_SlotMetadataCopyWith<$Res> + implements $SlotMetadataCopyWith<$Res> { + factory _$$_SlotMetadataCopyWith( + _$_SlotMetadata value, $Res Function(_$_SlotMetadata) then) = + __$$_SlotMetadataCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {KeyType keyType, + PinPolicy pinPolicy, + TouchPolicy touchPolicy, + bool generated, + String publicKeyEncoded}); +} + +/// @nodoc +class __$$_SlotMetadataCopyWithImpl<$Res> + extends _$SlotMetadataCopyWithImpl<$Res, _$_SlotMetadata> + implements _$$_SlotMetadataCopyWith<$Res> { + __$$_SlotMetadataCopyWithImpl( + _$_SlotMetadata _value, $Res Function(_$_SlotMetadata) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? keyType = null, + Object? pinPolicy = null, + Object? touchPolicy = null, + Object? generated = null, + Object? publicKeyEncoded = null, + }) { + return _then(_$_SlotMetadata( + null == keyType + ? _value.keyType + : keyType // ignore: cast_nullable_to_non_nullable + as KeyType, + null == pinPolicy + ? _value.pinPolicy + : pinPolicy // ignore: cast_nullable_to_non_nullable + as PinPolicy, + null == touchPolicy + ? _value.touchPolicy + : touchPolicy // ignore: cast_nullable_to_non_nullable + as TouchPolicy, + null == generated + ? _value.generated + : generated // ignore: cast_nullable_to_non_nullable + as bool, + null == publicKeyEncoded + ? _value.publicKeyEncoded + : publicKeyEncoded // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_SlotMetadata implements _SlotMetadata { + _$_SlotMetadata(this.keyType, this.pinPolicy, this.touchPolicy, + this.generated, this.publicKeyEncoded); + + factory _$_SlotMetadata.fromJson(Map json) => + _$$_SlotMetadataFromJson(json); + + @override + final KeyType keyType; + @override + final PinPolicy pinPolicy; + @override + final TouchPolicy touchPolicy; + @override + final bool generated; + @override + final String publicKeyEncoded; + + @override + String toString() { + return 'SlotMetadata(keyType: $keyType, pinPolicy: $pinPolicy, touchPolicy: $touchPolicy, generated: $generated, publicKeyEncoded: $publicKeyEncoded)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_SlotMetadata && + (identical(other.keyType, keyType) || other.keyType == keyType) && + (identical(other.pinPolicy, pinPolicy) || + other.pinPolicy == pinPolicy) && + (identical(other.touchPolicy, touchPolicy) || + other.touchPolicy == touchPolicy) && + (identical(other.generated, generated) || + other.generated == generated) && + (identical(other.publicKeyEncoded, publicKeyEncoded) || + other.publicKeyEncoded == publicKeyEncoded)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, keyType, pinPolicy, touchPolicy, + generated, publicKeyEncoded); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_SlotMetadataCopyWith<_$_SlotMetadata> get copyWith => + __$$_SlotMetadataCopyWithImpl<_$_SlotMetadata>(this, _$identity); + + @override + Map toJson() { + return _$$_SlotMetadataToJson( + this, + ); + } +} + +abstract class _SlotMetadata implements SlotMetadata { + factory _SlotMetadata( + final KeyType keyType, + final PinPolicy pinPolicy, + final TouchPolicy touchPolicy, + final bool generated, + final String publicKeyEncoded) = _$_SlotMetadata; + + factory _SlotMetadata.fromJson(Map json) = + _$_SlotMetadata.fromJson; + + @override + KeyType get keyType; + @override + PinPolicy get pinPolicy; + @override + TouchPolicy get touchPolicy; + @override + bool get generated; + @override + String get publicKeyEncoded; + @override + @JsonKey(ignore: true) + _$$_SlotMetadataCopyWith<_$_SlotMetadata> get copyWith => + throw _privateConstructorUsedError; +} + +PivStateMetadata _$PivStateMetadataFromJson(Map json) { + return _PivStateMetadata.fromJson(json); +} + +/// @nodoc +mixin _$PivStateMetadata { + ManagementKeyMetadata get managementKeyMetadata => + throw _privateConstructorUsedError; + PinMetadata get pinMetadata => throw _privateConstructorUsedError; + PinMetadata get pukMetadata => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PivStateMetadataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivStateMetadataCopyWith<$Res> { + factory $PivStateMetadataCopyWith( + PivStateMetadata value, $Res Function(PivStateMetadata) then) = + _$PivStateMetadataCopyWithImpl<$Res, PivStateMetadata>; + @useResult + $Res call( + {ManagementKeyMetadata managementKeyMetadata, + PinMetadata pinMetadata, + PinMetadata pukMetadata}); + + $ManagementKeyMetadataCopyWith<$Res> get managementKeyMetadata; + $PinMetadataCopyWith<$Res> get pinMetadata; + $PinMetadataCopyWith<$Res> get pukMetadata; +} + +/// @nodoc +class _$PivStateMetadataCopyWithImpl<$Res, $Val extends PivStateMetadata> + implements $PivStateMetadataCopyWith<$Res> { + _$PivStateMetadataCopyWithImpl(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? managementKeyMetadata = null, + Object? pinMetadata = null, + Object? pukMetadata = null, + }) { + return _then(_value.copyWith( + managementKeyMetadata: null == managementKeyMetadata + ? _value.managementKeyMetadata + : managementKeyMetadata // ignore: cast_nullable_to_non_nullable + as ManagementKeyMetadata, + pinMetadata: null == pinMetadata + ? _value.pinMetadata + : pinMetadata // ignore: cast_nullable_to_non_nullable + as PinMetadata, + pukMetadata: null == pukMetadata + ? _value.pukMetadata + : pukMetadata // ignore: cast_nullable_to_non_nullable + as PinMetadata, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $ManagementKeyMetadataCopyWith<$Res> get managementKeyMetadata { + return $ManagementKeyMetadataCopyWith<$Res>(_value.managementKeyMetadata, + (value) { + return _then(_value.copyWith(managementKeyMetadata: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $PinMetadataCopyWith<$Res> get pinMetadata { + return $PinMetadataCopyWith<$Res>(_value.pinMetadata, (value) { + return _then(_value.copyWith(pinMetadata: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $PinMetadataCopyWith<$Res> get pukMetadata { + return $PinMetadataCopyWith<$Res>(_value.pukMetadata, (value) { + return _then(_value.copyWith(pukMetadata: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_PivStateMetadataCopyWith<$Res> + implements $PivStateMetadataCopyWith<$Res> { + factory _$$_PivStateMetadataCopyWith( + _$_PivStateMetadata value, $Res Function(_$_PivStateMetadata) then) = + __$$_PivStateMetadataCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {ManagementKeyMetadata managementKeyMetadata, + PinMetadata pinMetadata, + PinMetadata pukMetadata}); + + @override + $ManagementKeyMetadataCopyWith<$Res> get managementKeyMetadata; + @override + $PinMetadataCopyWith<$Res> get pinMetadata; + @override + $PinMetadataCopyWith<$Res> get pukMetadata; +} + +/// @nodoc +class __$$_PivStateMetadataCopyWithImpl<$Res> + extends _$PivStateMetadataCopyWithImpl<$Res, _$_PivStateMetadata> + implements _$$_PivStateMetadataCopyWith<$Res> { + __$$_PivStateMetadataCopyWithImpl( + _$_PivStateMetadata _value, $Res Function(_$_PivStateMetadata) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? managementKeyMetadata = null, + Object? pinMetadata = null, + Object? pukMetadata = null, + }) { + return _then(_$_PivStateMetadata( + managementKeyMetadata: null == managementKeyMetadata + ? _value.managementKeyMetadata + : managementKeyMetadata // ignore: cast_nullable_to_non_nullable + as ManagementKeyMetadata, + pinMetadata: null == pinMetadata + ? _value.pinMetadata + : pinMetadata // ignore: cast_nullable_to_non_nullable + as PinMetadata, + pukMetadata: null == pukMetadata + ? _value.pukMetadata + : pukMetadata // ignore: cast_nullable_to_non_nullable + as PinMetadata, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_PivStateMetadata implements _PivStateMetadata { + _$_PivStateMetadata( + {required this.managementKeyMetadata, + required this.pinMetadata, + required this.pukMetadata}); + + factory _$_PivStateMetadata.fromJson(Map json) => + _$$_PivStateMetadataFromJson(json); + + @override + final ManagementKeyMetadata managementKeyMetadata; + @override + final PinMetadata pinMetadata; + @override + final PinMetadata pukMetadata; + + @override + String toString() { + return 'PivStateMetadata(managementKeyMetadata: $managementKeyMetadata, pinMetadata: $pinMetadata, pukMetadata: $pukMetadata)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PivStateMetadata && + (identical(other.managementKeyMetadata, managementKeyMetadata) || + other.managementKeyMetadata == managementKeyMetadata) && + (identical(other.pinMetadata, pinMetadata) || + other.pinMetadata == pinMetadata) && + (identical(other.pukMetadata, pukMetadata) || + other.pukMetadata == pukMetadata)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, managementKeyMetadata, pinMetadata, pukMetadata); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PivStateMetadataCopyWith<_$_PivStateMetadata> get copyWith => + __$$_PivStateMetadataCopyWithImpl<_$_PivStateMetadata>(this, _$identity); + + @override + Map toJson() { + return _$$_PivStateMetadataToJson( + this, + ); + } +} + +abstract class _PivStateMetadata implements PivStateMetadata { + factory _PivStateMetadata( + {required final ManagementKeyMetadata managementKeyMetadata, + required final PinMetadata pinMetadata, + required final PinMetadata pukMetadata}) = _$_PivStateMetadata; + + factory _PivStateMetadata.fromJson(Map json) = + _$_PivStateMetadata.fromJson; + + @override + ManagementKeyMetadata get managementKeyMetadata; + @override + PinMetadata get pinMetadata; + @override + PinMetadata get pukMetadata; + @override + @JsonKey(ignore: true) + _$$_PivStateMetadataCopyWith<_$_PivStateMetadata> get copyWith => + throw _privateConstructorUsedError; +} + +PivState _$PivStateFromJson(Map json) { + return _PivState.fromJson(json); +} + +/// @nodoc +mixin _$PivState { + Version get version => throw _privateConstructorUsedError; + bool get authenticated => throw _privateConstructorUsedError; + bool get derivedKey => throw _privateConstructorUsedError; + bool get storedKey => throw _privateConstructorUsedError; + int get pinAttempts => throw _privateConstructorUsedError; + String? get chuid => throw _privateConstructorUsedError; + String? get ccc => throw _privateConstructorUsedError; + PivStateMetadata? get metadata => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PivStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivStateCopyWith<$Res> { + factory $PivStateCopyWith(PivState value, $Res Function(PivState) then) = + _$PivStateCopyWithImpl<$Res, PivState>; + @useResult + $Res call( + {Version version, + bool authenticated, + bool derivedKey, + bool storedKey, + int pinAttempts, + String? chuid, + String? ccc, + PivStateMetadata? metadata}); + + $VersionCopyWith<$Res> get version; + $PivStateMetadataCopyWith<$Res>? get metadata; +} + +/// @nodoc +class _$PivStateCopyWithImpl<$Res, $Val extends PivState> + implements $PivStateCopyWith<$Res> { + _$PivStateCopyWithImpl(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? version = null, + Object? authenticated = null, + Object? derivedKey = null, + Object? storedKey = null, + Object? pinAttempts = null, + Object? chuid = freezed, + Object? ccc = freezed, + Object? metadata = freezed, + }) { + return _then(_value.copyWith( + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as Version, + authenticated: null == authenticated + ? _value.authenticated + : authenticated // ignore: cast_nullable_to_non_nullable + as bool, + derivedKey: null == derivedKey + ? _value.derivedKey + : derivedKey // ignore: cast_nullable_to_non_nullable + as bool, + storedKey: null == storedKey + ? _value.storedKey + : storedKey // ignore: cast_nullable_to_non_nullable + as bool, + pinAttempts: null == pinAttempts + ? _value.pinAttempts + : pinAttempts // ignore: cast_nullable_to_non_nullable + as int, + chuid: freezed == chuid + ? _value.chuid + : chuid // ignore: cast_nullable_to_non_nullable + as String?, + ccc: freezed == ccc + ? _value.ccc + : ccc // ignore: cast_nullable_to_non_nullable + as String?, + metadata: freezed == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as PivStateMetadata?, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $VersionCopyWith<$Res> get version { + return $VersionCopyWith<$Res>(_value.version, (value) { + return _then(_value.copyWith(version: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $PivStateMetadataCopyWith<$Res>? get metadata { + if (_value.metadata == null) { + return null; + } + + return $PivStateMetadataCopyWith<$Res>(_value.metadata!, (value) { + return _then(_value.copyWith(metadata: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_PivStateCopyWith<$Res> implements $PivStateCopyWith<$Res> { + factory _$$_PivStateCopyWith( + _$_PivState value, $Res Function(_$_PivState) then) = + __$$_PivStateCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {Version version, + bool authenticated, + bool derivedKey, + bool storedKey, + int pinAttempts, + String? chuid, + String? ccc, + PivStateMetadata? metadata}); + + @override + $VersionCopyWith<$Res> get version; + @override + $PivStateMetadataCopyWith<$Res>? get metadata; +} + +/// @nodoc +class __$$_PivStateCopyWithImpl<$Res> + extends _$PivStateCopyWithImpl<$Res, _$_PivState> + implements _$$_PivStateCopyWith<$Res> { + __$$_PivStateCopyWithImpl( + _$_PivState _value, $Res Function(_$_PivState) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? version = null, + Object? authenticated = null, + Object? derivedKey = null, + Object? storedKey = null, + Object? pinAttempts = null, + Object? chuid = freezed, + Object? ccc = freezed, + Object? metadata = freezed, + }) { + return _then(_$_PivState( + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as Version, + authenticated: null == authenticated + ? _value.authenticated + : authenticated // ignore: cast_nullable_to_non_nullable + as bool, + derivedKey: null == derivedKey + ? _value.derivedKey + : derivedKey // ignore: cast_nullable_to_non_nullable + as bool, + storedKey: null == storedKey + ? _value.storedKey + : storedKey // ignore: cast_nullable_to_non_nullable + as bool, + pinAttempts: null == pinAttempts + ? _value.pinAttempts + : pinAttempts // ignore: cast_nullable_to_non_nullable + as int, + chuid: freezed == chuid + ? _value.chuid + : chuid // ignore: cast_nullable_to_non_nullable + as String?, + ccc: freezed == ccc + ? _value.ccc + : ccc // ignore: cast_nullable_to_non_nullable + as String?, + metadata: freezed == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as PivStateMetadata?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_PivState extends _PivState { + _$_PivState( + {required this.version, + required this.authenticated, + required this.derivedKey, + required this.storedKey, + required this.pinAttempts, + this.chuid, + this.ccc, + this.metadata}) + : super._(); + + factory _$_PivState.fromJson(Map json) => + _$$_PivStateFromJson(json); + + @override + final Version version; + @override + final bool authenticated; + @override + final bool derivedKey; + @override + final bool storedKey; + @override + final int pinAttempts; + @override + final String? chuid; + @override + final String? ccc; + @override + final PivStateMetadata? metadata; + + @override + String toString() { + return 'PivState(version: $version, authenticated: $authenticated, derivedKey: $derivedKey, storedKey: $storedKey, pinAttempts: $pinAttempts, chuid: $chuid, ccc: $ccc, metadata: $metadata)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PivState && + (identical(other.version, version) || other.version == version) && + (identical(other.authenticated, authenticated) || + other.authenticated == authenticated) && + (identical(other.derivedKey, derivedKey) || + other.derivedKey == derivedKey) && + (identical(other.storedKey, storedKey) || + other.storedKey == storedKey) && + (identical(other.pinAttempts, pinAttempts) || + other.pinAttempts == pinAttempts) && + (identical(other.chuid, chuid) || other.chuid == chuid) && + (identical(other.ccc, ccc) || other.ccc == ccc) && + (identical(other.metadata, metadata) || + other.metadata == metadata)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, version, authenticated, + derivedKey, storedKey, pinAttempts, chuid, ccc, metadata); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PivStateCopyWith<_$_PivState> get copyWith => + __$$_PivStateCopyWithImpl<_$_PivState>(this, _$identity); + + @override + Map toJson() { + return _$$_PivStateToJson( + this, + ); + } +} + +abstract class _PivState extends PivState { + factory _PivState( + {required final Version version, + required final bool authenticated, + required final bool derivedKey, + required final bool storedKey, + required final int pinAttempts, + final String? chuid, + final String? ccc, + final PivStateMetadata? metadata}) = _$_PivState; + _PivState._() : super._(); + + factory _PivState.fromJson(Map json) = _$_PivState.fromJson; + + @override + Version get version; + @override + bool get authenticated; + @override + bool get derivedKey; + @override + bool get storedKey; + @override + int get pinAttempts; + @override + String? get chuid; + @override + String? get ccc; + @override + PivStateMetadata? get metadata; + @override + @JsonKey(ignore: true) + _$$_PivStateCopyWith<_$_PivState> get copyWith => + throw _privateConstructorUsedError; +} + +CertInfo _$CertInfoFromJson(Map json) { + return _CertInfo.fromJson(json); +} + +/// @nodoc +mixin _$CertInfo { + String get subject => throw _privateConstructorUsedError; + String get issuer => throw _privateConstructorUsedError; + String get serial => throw _privateConstructorUsedError; + String get notValidBefore => throw _privateConstructorUsedError; + String get notValidAfter => throw _privateConstructorUsedError; + String get fingerprint => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $CertInfoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CertInfoCopyWith<$Res> { + factory $CertInfoCopyWith(CertInfo value, $Res Function(CertInfo) then) = + _$CertInfoCopyWithImpl<$Res, CertInfo>; + @useResult + $Res call( + {String subject, + String issuer, + String serial, + String notValidBefore, + String notValidAfter, + String fingerprint}); +} + +/// @nodoc +class _$CertInfoCopyWithImpl<$Res, $Val extends CertInfo> + implements $CertInfoCopyWith<$Res> { + _$CertInfoCopyWithImpl(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? subject = null, + Object? issuer = null, + Object? serial = null, + Object? notValidBefore = null, + Object? notValidAfter = null, + Object? fingerprint = null, + }) { + return _then(_value.copyWith( + subject: null == subject + ? _value.subject + : subject // ignore: cast_nullable_to_non_nullable + as String, + issuer: null == issuer + ? _value.issuer + : issuer // ignore: cast_nullable_to_non_nullable + as String, + serial: null == serial + ? _value.serial + : serial // ignore: cast_nullable_to_non_nullable + as String, + notValidBefore: null == notValidBefore + ? _value.notValidBefore + : notValidBefore // ignore: cast_nullable_to_non_nullable + as String, + notValidAfter: null == notValidAfter + ? _value.notValidAfter + : notValidAfter // ignore: cast_nullable_to_non_nullable + as String, + fingerprint: null == fingerprint + ? _value.fingerprint + : fingerprint // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_CertInfoCopyWith<$Res> implements $CertInfoCopyWith<$Res> { + factory _$$_CertInfoCopyWith( + _$_CertInfo value, $Res Function(_$_CertInfo) then) = + __$$_CertInfoCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String subject, + String issuer, + String serial, + String notValidBefore, + String notValidAfter, + String fingerprint}); +} + +/// @nodoc +class __$$_CertInfoCopyWithImpl<$Res> + extends _$CertInfoCopyWithImpl<$Res, _$_CertInfo> + implements _$$_CertInfoCopyWith<$Res> { + __$$_CertInfoCopyWithImpl( + _$_CertInfo _value, $Res Function(_$_CertInfo) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? subject = null, + Object? issuer = null, + Object? serial = null, + Object? notValidBefore = null, + Object? notValidAfter = null, + Object? fingerprint = null, + }) { + return _then(_$_CertInfo( + subject: null == subject + ? _value.subject + : subject // ignore: cast_nullable_to_non_nullable + as String, + issuer: null == issuer + ? _value.issuer + : issuer // ignore: cast_nullable_to_non_nullable + as String, + serial: null == serial + ? _value.serial + : serial // ignore: cast_nullable_to_non_nullable + as String, + notValidBefore: null == notValidBefore + ? _value.notValidBefore + : notValidBefore // ignore: cast_nullable_to_non_nullable + as String, + notValidAfter: null == notValidAfter + ? _value.notValidAfter + : notValidAfter // ignore: cast_nullable_to_non_nullable + as String, + fingerprint: null == fingerprint + ? _value.fingerprint + : fingerprint // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_CertInfo implements _CertInfo { + _$_CertInfo( + {required this.subject, + required this.issuer, + required this.serial, + required this.notValidBefore, + required this.notValidAfter, + required this.fingerprint}); + + factory _$_CertInfo.fromJson(Map json) => + _$$_CertInfoFromJson(json); + + @override + final String subject; + @override + final String issuer; + @override + final String serial; + @override + final String notValidBefore; + @override + final String notValidAfter; + @override + final String fingerprint; + + @override + String toString() { + return 'CertInfo(subject: $subject, issuer: $issuer, serial: $serial, notValidBefore: $notValidBefore, notValidAfter: $notValidAfter, fingerprint: $fingerprint)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_CertInfo && + (identical(other.subject, subject) || other.subject == subject) && + (identical(other.issuer, issuer) || other.issuer == issuer) && + (identical(other.serial, serial) || other.serial == serial) && + (identical(other.notValidBefore, notValidBefore) || + other.notValidBefore == notValidBefore) && + (identical(other.notValidAfter, notValidAfter) || + other.notValidAfter == notValidAfter) && + (identical(other.fingerprint, fingerprint) || + other.fingerprint == fingerprint)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, subject, issuer, serial, + notValidBefore, notValidAfter, fingerprint); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_CertInfoCopyWith<_$_CertInfo> get copyWith => + __$$_CertInfoCopyWithImpl<_$_CertInfo>(this, _$identity); + + @override + Map toJson() { + return _$$_CertInfoToJson( + this, + ); + } +} + +abstract class _CertInfo implements CertInfo { + factory _CertInfo( + {required final String subject, + required final String issuer, + required final String serial, + required final String notValidBefore, + required final String notValidAfter, + required final String fingerprint}) = _$_CertInfo; + + factory _CertInfo.fromJson(Map json) = _$_CertInfo.fromJson; + + @override + String get subject; + @override + String get issuer; + @override + String get serial; + @override + String get notValidBefore; + @override + String get notValidAfter; + @override + String get fingerprint; + @override + @JsonKey(ignore: true) + _$$_CertInfoCopyWith<_$_CertInfo> get copyWith => + throw _privateConstructorUsedError; +} + +PivSlot _$PivSlotFromJson(Map json) { + return _PivSlot.fromJson(json); +} + +/// @nodoc +mixin _$PivSlot { + SlotId get slot => throw _privateConstructorUsedError; + bool? get hasKey => throw _privateConstructorUsedError; + CertInfo? get certInfo => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PivSlotCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivSlotCopyWith<$Res> { + factory $PivSlotCopyWith(PivSlot value, $Res Function(PivSlot) then) = + _$PivSlotCopyWithImpl<$Res, PivSlot>; + @useResult + $Res call({SlotId slot, bool? hasKey, CertInfo? certInfo}); + + $CertInfoCopyWith<$Res>? get certInfo; +} + +/// @nodoc +class _$PivSlotCopyWithImpl<$Res, $Val extends PivSlot> + implements $PivSlotCopyWith<$Res> { + _$PivSlotCopyWithImpl(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? hasKey = freezed, + Object? certInfo = freezed, + }) { + return _then(_value.copyWith( + slot: null == slot + ? _value.slot + : slot // ignore: cast_nullable_to_non_nullable + as SlotId, + hasKey: freezed == hasKey + ? _value.hasKey + : hasKey // ignore: cast_nullable_to_non_nullable + as bool?, + certInfo: freezed == certInfo + ? _value.certInfo + : certInfo // ignore: cast_nullable_to_non_nullable + as CertInfo?, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $CertInfoCopyWith<$Res>? get certInfo { + if (_value.certInfo == null) { + return null; + } + + return $CertInfoCopyWith<$Res>(_value.certInfo!, (value) { + return _then(_value.copyWith(certInfo: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_PivSlotCopyWith<$Res> implements $PivSlotCopyWith<$Res> { + factory _$$_PivSlotCopyWith( + _$_PivSlot value, $Res Function(_$_PivSlot) then) = + __$$_PivSlotCopyWithImpl<$Res>; + @override + @useResult + $Res call({SlotId slot, bool? hasKey, CertInfo? certInfo}); + + @override + $CertInfoCopyWith<$Res>? get certInfo; +} + +/// @nodoc +class __$$_PivSlotCopyWithImpl<$Res> + extends _$PivSlotCopyWithImpl<$Res, _$_PivSlot> + implements _$$_PivSlotCopyWith<$Res> { + __$$_PivSlotCopyWithImpl(_$_PivSlot _value, $Res Function(_$_PivSlot) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? slot = null, + Object? hasKey = freezed, + Object? certInfo = freezed, + }) { + return _then(_$_PivSlot( + slot: null == slot + ? _value.slot + : slot // ignore: cast_nullable_to_non_nullable + as SlotId, + hasKey: freezed == hasKey + ? _value.hasKey + : hasKey // ignore: cast_nullable_to_non_nullable + as bool?, + certInfo: freezed == certInfo + ? _value.certInfo + : certInfo // ignore: cast_nullable_to_non_nullable + as CertInfo?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_PivSlot implements _PivSlot { + _$_PivSlot({required this.slot, this.hasKey, this.certInfo}); + + factory _$_PivSlot.fromJson(Map json) => + _$$_PivSlotFromJson(json); + + @override + final SlotId slot; + @override + final bool? hasKey; + @override + final CertInfo? certInfo; + + @override + String toString() { + return 'PivSlot(slot: $slot, hasKey: $hasKey, certInfo: $certInfo)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PivSlot && + (identical(other.slot, slot) || other.slot == slot) && + (identical(other.hasKey, hasKey) || other.hasKey == hasKey) && + (identical(other.certInfo, certInfo) || + other.certInfo == certInfo)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, slot, hasKey, certInfo); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PivSlotCopyWith<_$_PivSlot> get copyWith => + __$$_PivSlotCopyWithImpl<_$_PivSlot>(this, _$identity); + + @override + Map toJson() { + return _$$_PivSlotToJson( + this, + ); + } +} + +abstract class _PivSlot implements PivSlot { + factory _PivSlot( + {required final SlotId slot, + final bool? hasKey, + final CertInfo? certInfo}) = _$_PivSlot; + + factory _PivSlot.fromJson(Map json) = _$_PivSlot.fromJson; + + @override + SlotId get slot; + @override + bool? get hasKey; + @override + CertInfo? get certInfo; + @override + @JsonKey(ignore: true) + _$$_PivSlotCopyWith<_$_PivSlot> get copyWith => + throw _privateConstructorUsedError; +} + +PivExamineResult _$PivExamineResultFromJson(Map json) { + switch (json['runtimeType']) { + case 'result': + return _ExamineResult.fromJson(json); + case 'invalidPassword': + return _InvalidPassword.fromJson(json); + + default: + throw CheckedFromJsonException(json, 'runtimeType', 'PivExamineResult', + 'Invalid union type "${json['runtimeType']}"!'); + } +} + +/// @nodoc +mixin _$PivExamineResult { + @optionalTypeArgs + TResult when({ + required TResult Function(bool password, bool privateKey, int certificates) + result, + required TResult Function() invalidPassword, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(bool password, bool privateKey, int certificates)? result, + TResult? Function()? invalidPassword, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(bool password, bool privateKey, int certificates)? result, + TResult Function()? invalidPassword, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_ExamineResult value) result, + required TResult Function(_InvalidPassword value) invalidPassword, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ExamineResult value)? result, + TResult? Function(_InvalidPassword value)? invalidPassword, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ExamineResult value)? result, + TResult Function(_InvalidPassword value)? invalidPassword, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + Map toJson() => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivExamineResultCopyWith<$Res> { + factory $PivExamineResultCopyWith( + PivExamineResult value, $Res Function(PivExamineResult) then) = + _$PivExamineResultCopyWithImpl<$Res, PivExamineResult>; +} + +/// @nodoc +class _$PivExamineResultCopyWithImpl<$Res, $Val extends PivExamineResult> + implements $PivExamineResultCopyWith<$Res> { + _$PivExamineResultCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$_ExamineResultCopyWith<$Res> { + factory _$$_ExamineResultCopyWith( + _$_ExamineResult value, $Res Function(_$_ExamineResult) then) = + __$$_ExamineResultCopyWithImpl<$Res>; + @useResult + $Res call({bool password, bool privateKey, int certificates}); +} + +/// @nodoc +class __$$_ExamineResultCopyWithImpl<$Res> + extends _$PivExamineResultCopyWithImpl<$Res, _$_ExamineResult> + implements _$$_ExamineResultCopyWith<$Res> { + __$$_ExamineResultCopyWithImpl( + _$_ExamineResult _value, $Res Function(_$_ExamineResult) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? password = null, + Object? privateKey = null, + Object? certificates = null, + }) { + return _then(_$_ExamineResult( + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as bool, + privateKey: null == privateKey + ? _value.privateKey + : privateKey // ignore: cast_nullable_to_non_nullable + as bool, + certificates: null == certificates + ? _value.certificates + : certificates // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_ExamineResult implements _ExamineResult { + _$_ExamineResult( + {required this.password, + required this.privateKey, + required this.certificates, + final String? $type}) + : $type = $type ?? 'result'; + + factory _$_ExamineResult.fromJson(Map json) => + _$$_ExamineResultFromJson(json); + + @override + final bool password; + @override + final bool privateKey; + @override + final int certificates; + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + String toString() { + return 'PivExamineResult.result(password: $password, privateKey: $privateKey, certificates: $certificates)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_ExamineResult && + (identical(other.password, password) || + other.password == password) && + (identical(other.privateKey, privateKey) || + other.privateKey == privateKey) && + (identical(other.certificates, certificates) || + other.certificates == certificates)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, password, privateKey, certificates); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_ExamineResultCopyWith<_$_ExamineResult> get copyWith => + __$$_ExamineResultCopyWithImpl<_$_ExamineResult>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(bool password, bool privateKey, int certificates) + result, + required TResult Function() invalidPassword, + }) { + return result(password, privateKey, certificates); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(bool password, bool privateKey, int certificates)? result, + TResult? Function()? invalidPassword, + }) { + return result?.call(password, privateKey, certificates); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(bool password, bool privateKey, int certificates)? result, + TResult Function()? invalidPassword, + required TResult orElse(), + }) { + if (result != null) { + return result(password, privateKey, certificates); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_ExamineResult value) result, + required TResult Function(_InvalidPassword value) invalidPassword, + }) { + return result(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ExamineResult value)? result, + TResult? Function(_InvalidPassword value)? invalidPassword, + }) { + return result?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ExamineResult value)? result, + TResult Function(_InvalidPassword value)? invalidPassword, + required TResult orElse(), + }) { + if (result != null) { + return result(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$_ExamineResultToJson( + this, + ); + } +} + +abstract class _ExamineResult implements PivExamineResult { + factory _ExamineResult( + {required final bool password, + required final bool privateKey, + required final int certificates}) = _$_ExamineResult; + + factory _ExamineResult.fromJson(Map json) = + _$_ExamineResult.fromJson; + + bool get password; + bool get privateKey; + int get certificates; + @JsonKey(ignore: true) + _$$_ExamineResultCopyWith<_$_ExamineResult> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$_InvalidPasswordCopyWith<$Res> { + factory _$$_InvalidPasswordCopyWith( + _$_InvalidPassword value, $Res Function(_$_InvalidPassword) then) = + __$$_InvalidPasswordCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_InvalidPasswordCopyWithImpl<$Res> + extends _$PivExamineResultCopyWithImpl<$Res, _$_InvalidPassword> + implements _$$_InvalidPasswordCopyWith<$Res> { + __$$_InvalidPasswordCopyWithImpl( + _$_InvalidPassword _value, $Res Function(_$_InvalidPassword) _then) + : super(_value, _then); +} + +/// @nodoc +@JsonSerializable() +class _$_InvalidPassword implements _InvalidPassword { + _$_InvalidPassword({final String? $type}) + : $type = $type ?? 'invalidPassword'; + + factory _$_InvalidPassword.fromJson(Map json) => + _$$_InvalidPasswordFromJson(json); + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + String toString() { + return 'PivExamineResult.invalidPassword()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_InvalidPassword); + } + + @JsonKey(ignore: true) + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(bool password, bool privateKey, int certificates) + result, + required TResult Function() invalidPassword, + }) { + return invalidPassword(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(bool password, bool privateKey, int certificates)? result, + TResult? Function()? invalidPassword, + }) { + return invalidPassword?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(bool password, bool privateKey, int certificates)? result, + TResult Function()? invalidPassword, + required TResult orElse(), + }) { + if (invalidPassword != null) { + return invalidPassword(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_ExamineResult value) result, + required TResult Function(_InvalidPassword value) invalidPassword, + }) { + return invalidPassword(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ExamineResult value)? result, + TResult? Function(_InvalidPassword value)? invalidPassword, + }) { + return invalidPassword?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ExamineResult value)? result, + TResult Function(_InvalidPassword value)? invalidPassword, + required TResult orElse(), + }) { + if (invalidPassword != null) { + return invalidPassword(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$_InvalidPasswordToJson( + this, + ); + } +} + +abstract class _InvalidPassword implements PivExamineResult { + factory _InvalidPassword() = _$_InvalidPassword; + + factory _InvalidPassword.fromJson(Map json) = + _$_InvalidPassword.fromJson; +} + +/// @nodoc +mixin _$PivGenerateParameters { + String get subject => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function( + String subject, DateTime validFrom, DateTime validTo) + certificate, + required TResult Function(String subject) csr, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String subject, DateTime validFrom, DateTime validTo)? + certificate, + TResult? Function(String subject)? csr, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String subject, DateTime validFrom, DateTime validTo)? + certificate, + TResult Function(String subject)? csr, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_GenerateCertificate value) certificate, + required TResult Function(_GenerateCsr value) csr, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GenerateCertificate value)? certificate, + TResult? Function(_GenerateCsr value)? csr, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GenerateCertificate value)? certificate, + TResult Function(_GenerateCsr value)? csr, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $PivGenerateParametersCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivGenerateParametersCopyWith<$Res> { + factory $PivGenerateParametersCopyWith(PivGenerateParameters value, + $Res Function(PivGenerateParameters) then) = + _$PivGenerateParametersCopyWithImpl<$Res, PivGenerateParameters>; + @useResult + $Res call({String subject}); +} + +/// @nodoc +class _$PivGenerateParametersCopyWithImpl<$Res, + $Val extends PivGenerateParameters> + implements $PivGenerateParametersCopyWith<$Res> { + _$PivGenerateParametersCopyWithImpl(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? subject = null, + }) { + return _then(_value.copyWith( + subject: null == subject + ? _value.subject + : subject // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_GenerateCertificateCopyWith<$Res> + implements $PivGenerateParametersCopyWith<$Res> { + factory _$$_GenerateCertificateCopyWith(_$_GenerateCertificate value, + $Res Function(_$_GenerateCertificate) then) = + __$$_GenerateCertificateCopyWithImpl<$Res>; + @override + @useResult + $Res call({String subject, DateTime validFrom, DateTime validTo}); +} + +/// @nodoc +class __$$_GenerateCertificateCopyWithImpl<$Res> + extends _$PivGenerateParametersCopyWithImpl<$Res, _$_GenerateCertificate> + implements _$$_GenerateCertificateCopyWith<$Res> { + __$$_GenerateCertificateCopyWithImpl(_$_GenerateCertificate _value, + $Res Function(_$_GenerateCertificate) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? subject = null, + Object? validFrom = null, + Object? validTo = null, + }) { + return _then(_$_GenerateCertificate( + subject: null == subject + ? _value.subject + : subject // ignore: cast_nullable_to_non_nullable + as String, + validFrom: null == validFrom + ? _value.validFrom + : validFrom // ignore: cast_nullable_to_non_nullable + as DateTime, + validTo: null == validTo + ? _value.validTo + : validTo // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc + +class _$_GenerateCertificate implements _GenerateCertificate { + _$_GenerateCertificate( + {required this.subject, required this.validFrom, required this.validTo}); + + @override + final String subject; + @override + final DateTime validFrom; + @override + final DateTime validTo; + + @override + String toString() { + return 'PivGenerateParameters.certificate(subject: $subject, validFrom: $validFrom, validTo: $validTo)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_GenerateCertificate && + (identical(other.subject, subject) || other.subject == subject) && + (identical(other.validFrom, validFrom) || + other.validFrom == validFrom) && + (identical(other.validTo, validTo) || other.validTo == validTo)); + } + + @override + int get hashCode => Object.hash(runtimeType, subject, validFrom, validTo); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_GenerateCertificateCopyWith<_$_GenerateCertificate> get copyWith => + __$$_GenerateCertificateCopyWithImpl<_$_GenerateCertificate>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function( + String subject, DateTime validFrom, DateTime validTo) + certificate, + required TResult Function(String subject) csr, + }) { + return certificate(subject, validFrom, validTo); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String subject, DateTime validFrom, DateTime validTo)? + certificate, + TResult? Function(String subject)? csr, + }) { + return certificate?.call(subject, validFrom, validTo); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String subject, DateTime validFrom, DateTime validTo)? + certificate, + TResult Function(String subject)? csr, + required TResult orElse(), + }) { + if (certificate != null) { + return certificate(subject, validFrom, validTo); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_GenerateCertificate value) certificate, + required TResult Function(_GenerateCsr value) csr, + }) { + return certificate(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GenerateCertificate value)? certificate, + TResult? Function(_GenerateCsr value)? csr, + }) { + return certificate?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GenerateCertificate value)? certificate, + TResult Function(_GenerateCsr value)? csr, + required TResult orElse(), + }) { + if (certificate != null) { + return certificate(this); + } + return orElse(); + } +} + +abstract class _GenerateCertificate implements PivGenerateParameters { + factory _GenerateCertificate( + {required final String subject, + required final DateTime validFrom, + required final DateTime validTo}) = _$_GenerateCertificate; + + @override + String get subject; + DateTime get validFrom; + DateTime get validTo; + @override + @JsonKey(ignore: true) + _$$_GenerateCertificateCopyWith<_$_GenerateCertificate> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$_GenerateCsrCopyWith<$Res> + implements $PivGenerateParametersCopyWith<$Res> { + factory _$$_GenerateCsrCopyWith( + _$_GenerateCsr value, $Res Function(_$_GenerateCsr) then) = + __$$_GenerateCsrCopyWithImpl<$Res>; + @override + @useResult + $Res call({String subject}); +} + +/// @nodoc +class __$$_GenerateCsrCopyWithImpl<$Res> + extends _$PivGenerateParametersCopyWithImpl<$Res, _$_GenerateCsr> + implements _$$_GenerateCsrCopyWith<$Res> { + __$$_GenerateCsrCopyWithImpl( + _$_GenerateCsr _value, $Res Function(_$_GenerateCsr) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? subject = null, + }) { + return _then(_$_GenerateCsr( + subject: null == subject + ? _value.subject + : subject // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$_GenerateCsr implements _GenerateCsr { + _$_GenerateCsr({required this.subject}); + + @override + final String subject; + + @override + String toString() { + return 'PivGenerateParameters.csr(subject: $subject)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_GenerateCsr && + (identical(other.subject, subject) || other.subject == subject)); + } + + @override + int get hashCode => Object.hash(runtimeType, subject); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_GenerateCsrCopyWith<_$_GenerateCsr> get copyWith => + __$$_GenerateCsrCopyWithImpl<_$_GenerateCsr>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function( + String subject, DateTime validFrom, DateTime validTo) + certificate, + required TResult Function(String subject) csr, + }) { + return csr(subject); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String subject, DateTime validFrom, DateTime validTo)? + certificate, + TResult? Function(String subject)? csr, + }) { + return csr?.call(subject); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String subject, DateTime validFrom, DateTime validTo)? + certificate, + TResult Function(String subject)? csr, + required TResult orElse(), + }) { + if (csr != null) { + return csr(subject); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_GenerateCertificate value) certificate, + required TResult Function(_GenerateCsr value) csr, + }) { + return csr(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GenerateCertificate value)? certificate, + TResult? Function(_GenerateCsr value)? csr, + }) { + return csr?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GenerateCertificate value)? certificate, + TResult Function(_GenerateCsr value)? csr, + required TResult orElse(), + }) { + if (csr != null) { + return csr(this); + } + return orElse(); + } +} + +abstract class _GenerateCsr implements PivGenerateParameters { + factory _GenerateCsr({required final String subject}) = _$_GenerateCsr; + + @override + String get subject; + @override + @JsonKey(ignore: true) + _$$_GenerateCsrCopyWith<_$_GenerateCsr> get copyWith => + throw _privateConstructorUsedError; +} + +PivGenerateResult _$PivGenerateResultFromJson(Map json) { + return _PivGenerateResult.fromJson(json); +} + +/// @nodoc +mixin _$PivGenerateResult { + GenerateType get generateType => throw _privateConstructorUsedError; + String get publicKey => throw _privateConstructorUsedError; + String get result => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PivGenerateResultCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivGenerateResultCopyWith<$Res> { + factory $PivGenerateResultCopyWith( + PivGenerateResult value, $Res Function(PivGenerateResult) then) = + _$PivGenerateResultCopyWithImpl<$Res, PivGenerateResult>; + @useResult + $Res call({GenerateType generateType, String publicKey, String result}); +} + +/// @nodoc +class _$PivGenerateResultCopyWithImpl<$Res, $Val extends PivGenerateResult> + implements $PivGenerateResultCopyWith<$Res> { + _$PivGenerateResultCopyWithImpl(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? generateType = null, + Object? publicKey = null, + Object? result = null, + }) { + return _then(_value.copyWith( + generateType: null == generateType + ? _value.generateType + : generateType // ignore: cast_nullable_to_non_nullable + as GenerateType, + publicKey: null == publicKey + ? _value.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as String, + result: null == result + ? _value.result + : result // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_PivGenerateResultCopyWith<$Res> + implements $PivGenerateResultCopyWith<$Res> { + factory _$$_PivGenerateResultCopyWith(_$_PivGenerateResult value, + $Res Function(_$_PivGenerateResult) then) = + __$$_PivGenerateResultCopyWithImpl<$Res>; + @override + @useResult + $Res call({GenerateType generateType, String publicKey, String result}); +} + +/// @nodoc +class __$$_PivGenerateResultCopyWithImpl<$Res> + extends _$PivGenerateResultCopyWithImpl<$Res, _$_PivGenerateResult> + implements _$$_PivGenerateResultCopyWith<$Res> { + __$$_PivGenerateResultCopyWithImpl( + _$_PivGenerateResult _value, $Res Function(_$_PivGenerateResult) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? generateType = null, + Object? publicKey = null, + Object? result = null, + }) { + return _then(_$_PivGenerateResult( + generateType: null == generateType + ? _value.generateType + : generateType // ignore: cast_nullable_to_non_nullable + as GenerateType, + publicKey: null == publicKey + ? _value.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as String, + result: null == result + ? _value.result + : result // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_PivGenerateResult implements _PivGenerateResult { + _$_PivGenerateResult( + {required this.generateType, + required this.publicKey, + required this.result}); + + factory _$_PivGenerateResult.fromJson(Map json) => + _$$_PivGenerateResultFromJson(json); + + @override + final GenerateType generateType; + @override + final String publicKey; + @override + final String result; + + @override + String toString() { + return 'PivGenerateResult(generateType: $generateType, publicKey: $publicKey, result: $result)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PivGenerateResult && + (identical(other.generateType, generateType) || + other.generateType == generateType) && + (identical(other.publicKey, publicKey) || + other.publicKey == publicKey) && + (identical(other.result, result) || other.result == result)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, generateType, publicKey, result); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PivGenerateResultCopyWith<_$_PivGenerateResult> get copyWith => + __$$_PivGenerateResultCopyWithImpl<_$_PivGenerateResult>( + this, _$identity); + + @override + Map toJson() { + return _$$_PivGenerateResultToJson( + this, + ); + } +} + +abstract class _PivGenerateResult implements PivGenerateResult { + factory _PivGenerateResult( + {required final GenerateType generateType, + required final String publicKey, + required final String result}) = _$_PivGenerateResult; + + factory _PivGenerateResult.fromJson(Map json) = + _$_PivGenerateResult.fromJson; + + @override + GenerateType get generateType; + @override + String get publicKey; + @override + String get result; + @override + @JsonKey(ignore: true) + _$$_PivGenerateResultCopyWith<_$_PivGenerateResult> get copyWith => + throw _privateConstructorUsedError; +} + +PivImportResult _$PivImportResultFromJson(Map json) { + return _PivImportResult.fromJson(json); +} + +/// @nodoc +mixin _$PivImportResult { + SlotMetadata? get metadata => throw _privateConstructorUsedError; + String? get publicKey => throw _privateConstructorUsedError; + String? get certificate => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PivImportResultCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivImportResultCopyWith<$Res> { + factory $PivImportResultCopyWith( + PivImportResult value, $Res Function(PivImportResult) then) = + _$PivImportResultCopyWithImpl<$Res, PivImportResult>; + @useResult + $Res call({SlotMetadata? metadata, String? publicKey, String? certificate}); + + $SlotMetadataCopyWith<$Res>? get metadata; +} + +/// @nodoc +class _$PivImportResultCopyWithImpl<$Res, $Val extends PivImportResult> + implements $PivImportResultCopyWith<$Res> { + _$PivImportResultCopyWithImpl(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? metadata = freezed, + Object? publicKey = freezed, + Object? certificate = freezed, + }) { + return _then(_value.copyWith( + metadata: freezed == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as SlotMetadata?, + publicKey: freezed == publicKey + ? _value.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as String?, + certificate: freezed == certificate + ? _value.certificate + : certificate // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $SlotMetadataCopyWith<$Res>? get metadata { + if (_value.metadata == null) { + return null; + } + + return $SlotMetadataCopyWith<$Res>(_value.metadata!, (value) { + return _then(_value.copyWith(metadata: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_PivImportResultCopyWith<$Res> + implements $PivImportResultCopyWith<$Res> { + factory _$$_PivImportResultCopyWith( + _$_PivImportResult value, $Res Function(_$_PivImportResult) then) = + __$$_PivImportResultCopyWithImpl<$Res>; + @override + @useResult + $Res call({SlotMetadata? metadata, String? publicKey, String? certificate}); + + @override + $SlotMetadataCopyWith<$Res>? get metadata; +} + +/// @nodoc +class __$$_PivImportResultCopyWithImpl<$Res> + extends _$PivImportResultCopyWithImpl<$Res, _$_PivImportResult> + implements _$$_PivImportResultCopyWith<$Res> { + __$$_PivImportResultCopyWithImpl( + _$_PivImportResult _value, $Res Function(_$_PivImportResult) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? metadata = freezed, + Object? publicKey = freezed, + Object? certificate = freezed, + }) { + return _then(_$_PivImportResult( + metadata: freezed == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as SlotMetadata?, + publicKey: freezed == publicKey + ? _value.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as String?, + certificate: freezed == certificate + ? _value.certificate + : certificate // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_PivImportResult implements _PivImportResult { + _$_PivImportResult( + {required this.metadata, + required this.publicKey, + required this.certificate}); + + factory _$_PivImportResult.fromJson(Map json) => + _$$_PivImportResultFromJson(json); + + @override + final SlotMetadata? metadata; + @override + final String? publicKey; + @override + final String? certificate; + + @override + String toString() { + return 'PivImportResult(metadata: $metadata, publicKey: $publicKey, certificate: $certificate)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PivImportResult && + (identical(other.metadata, metadata) || + other.metadata == metadata) && + (identical(other.publicKey, publicKey) || + other.publicKey == publicKey) && + (identical(other.certificate, certificate) || + other.certificate == certificate)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, metadata, publicKey, certificate); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PivImportResultCopyWith<_$_PivImportResult> get copyWith => + __$$_PivImportResultCopyWithImpl<_$_PivImportResult>(this, _$identity); + + @override + Map toJson() { + return _$$_PivImportResultToJson( + this, + ); + } +} + +abstract class _PivImportResult implements PivImportResult { + factory _PivImportResult( + {required final SlotMetadata? metadata, + required final String? publicKey, + required final String? certificate}) = _$_PivImportResult; + + factory _PivImportResult.fromJson(Map json) = + _$_PivImportResult.fromJson; + + @override + SlotMetadata? get metadata; + @override + String? get publicKey; + @override + String? get certificate; + @override + @JsonKey(ignore: true) + _$$_PivImportResultCopyWith<_$_PivImportResult> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/piv/models.g.dart b/lib/piv/models.g.dart new file mode 100644 index 00000000..109f280d --- /dev/null +++ b/lib/piv/models.g.dart @@ -0,0 +1,228 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$_PinMetadata _$$_PinMetadataFromJson(Map json) => + _$_PinMetadata( + json['default_value'] as bool, + json['total_attempts'] as int, + json['attempts_remaining'] as int, + ); + +Map _$$_PinMetadataToJson(_$_PinMetadata instance) => + { + 'default_value': instance.defaultValue, + 'total_attempts': instance.totalAttempts, + 'attempts_remaining': instance.attemptsRemaining, + }; + +_$_ManagementKeyMetadata _$$_ManagementKeyMetadataFromJson( + Map json) => + _$_ManagementKeyMetadata( + $enumDecode(_$ManagementKeyTypeEnumMap, json['key_type']), + json['default_value'] as bool, + $enumDecode(_$TouchPolicyEnumMap, json['touch_policy']), + ); + +Map _$$_ManagementKeyMetadataToJson( + _$_ManagementKeyMetadata instance) => + { + 'key_type': _$ManagementKeyTypeEnumMap[instance.keyType]!, + 'default_value': instance.defaultValue, + 'touch_policy': _$TouchPolicyEnumMap[instance.touchPolicy]!, + }; + +const _$ManagementKeyTypeEnumMap = { + ManagementKeyType.tdes: 3, + ManagementKeyType.aes128: 8, + ManagementKeyType.aes192: 10, + ManagementKeyType.aes256: 12, +}; + +const _$TouchPolicyEnumMap = { + TouchPolicy.dfault: 0, + TouchPolicy.never: 1, + TouchPolicy.always: 2, + TouchPolicy.cached: 3, +}; + +_$_SlotMetadata _$$_SlotMetadataFromJson(Map json) => + _$_SlotMetadata( + $enumDecode(_$KeyTypeEnumMap, json['key_type']), + $enumDecode(_$PinPolicyEnumMap, json['pin_policy']), + $enumDecode(_$TouchPolicyEnumMap, json['touch_policy']), + json['generated'] as bool, + json['public_key_encoded'] as String, + ); + +Map _$$_SlotMetadataToJson(_$_SlotMetadata instance) => + { + 'key_type': _$KeyTypeEnumMap[instance.keyType]!, + 'pin_policy': _$PinPolicyEnumMap[instance.pinPolicy]!, + 'touch_policy': _$TouchPolicyEnumMap[instance.touchPolicy]!, + 'generated': instance.generated, + 'public_key_encoded': instance.publicKeyEncoded, + }; + +const _$KeyTypeEnumMap = { + KeyType.rsa1024: 6, + KeyType.rsa2048: 7, + KeyType.eccp256: 17, + KeyType.eccp384: 20, +}; + +const _$PinPolicyEnumMap = { + PinPolicy.dfault: 0, + PinPolicy.never: 1, + PinPolicy.once: 2, + PinPolicy.always: 3, +}; + +_$_PivStateMetadata _$$_PivStateMetadataFromJson(Map json) => + _$_PivStateMetadata( + managementKeyMetadata: ManagementKeyMetadata.fromJson( + json['management_key_metadata'] as Map), + pinMetadata: + PinMetadata.fromJson(json['pin_metadata'] as Map), + pukMetadata: + PinMetadata.fromJson(json['puk_metadata'] as Map), + ); + +Map _$$_PivStateMetadataToJson(_$_PivStateMetadata instance) => + { + 'management_key_metadata': instance.managementKeyMetadata, + 'pin_metadata': instance.pinMetadata, + 'puk_metadata': instance.pukMetadata, + }; + +_$_PivState _$$_PivStateFromJson(Map json) => _$_PivState( + version: Version.fromJson(json['version'] as List), + authenticated: json['authenticated'] as bool, + derivedKey: json['derived_key'] as bool, + storedKey: json['stored_key'] as bool, + pinAttempts: json['pin_attempts'] as int, + chuid: json['chuid'] as String?, + ccc: json['ccc'] as String?, + metadata: json['metadata'] == null + ? null + : PivStateMetadata.fromJson(json['metadata'] as Map), + ); + +Map _$$_PivStateToJson(_$_PivState instance) => + { + 'version': instance.version, + 'authenticated': instance.authenticated, + 'derived_key': instance.derivedKey, + 'stored_key': instance.storedKey, + 'pin_attempts': instance.pinAttempts, + 'chuid': instance.chuid, + 'ccc': instance.ccc, + 'metadata': instance.metadata, + }; + +_$_CertInfo _$$_CertInfoFromJson(Map json) => _$_CertInfo( + subject: json['subject'] as String, + issuer: json['issuer'] as String, + serial: json['serial'] as String, + notValidBefore: json['not_valid_before'] as String, + notValidAfter: json['not_valid_after'] as String, + fingerprint: json['fingerprint'] as String, + ); + +Map _$$_CertInfoToJson(_$_CertInfo instance) => + { + 'subject': instance.subject, + 'issuer': instance.issuer, + 'serial': instance.serial, + 'not_valid_before': instance.notValidBefore, + 'not_valid_after': instance.notValidAfter, + 'fingerprint': instance.fingerprint, + }; + +_$_PivSlot _$$_PivSlotFromJson(Map json) => _$_PivSlot( + slot: SlotId.fromJson(json['slot'] as int), + hasKey: json['has_key'] as bool?, + certInfo: json['cert_info'] == null + ? null + : CertInfo.fromJson(json['cert_info'] as Map), + ); + +Map _$$_PivSlotToJson(_$_PivSlot instance) => + { + 'slot': _$SlotIdEnumMap[instance.slot]!, + 'has_key': instance.hasKey, + 'cert_info': instance.certInfo, + }; + +const _$SlotIdEnumMap = { + SlotId.authentication: 'authentication', + SlotId.signature: 'signature', + SlotId.keyManagement: 'keyManagement', + SlotId.cardAuth: 'cardAuth', +}; + +_$_ExamineResult _$$_ExamineResultFromJson(Map json) => + _$_ExamineResult( + password: json['password'] as bool, + privateKey: json['private_key'] as bool, + certificates: json['certificates'] as int, + $type: json['runtimeType'] as String?, + ); + +Map _$$_ExamineResultToJson(_$_ExamineResult instance) => + { + 'password': instance.password, + 'private_key': instance.privateKey, + 'certificates': instance.certificates, + 'runtimeType': instance.$type, + }; + +_$_InvalidPassword _$$_InvalidPasswordFromJson(Map json) => + _$_InvalidPassword( + $type: json['runtimeType'] as String?, + ); + +Map _$$_InvalidPasswordToJson(_$_InvalidPassword instance) => + { + 'runtimeType': instance.$type, + }; + +_$_PivGenerateResult _$$_PivGenerateResultFromJson(Map json) => + _$_PivGenerateResult( + generateType: $enumDecode(_$GenerateTypeEnumMap, json['generate_type']), + publicKey: json['public_key'] as String, + result: json['result'] as String, + ); + +Map _$$_PivGenerateResultToJson( + _$_PivGenerateResult instance) => + { + 'generate_type': _$GenerateTypeEnumMap[instance.generateType]!, + 'public_key': instance.publicKey, + 'result': instance.result, + }; + +const _$GenerateTypeEnumMap = { + GenerateType.certificate: 'certificate', + GenerateType.csr: 'csr', +}; + +_$_PivImportResult _$$_PivImportResultFromJson(Map json) => + _$_PivImportResult( + metadata: json['metadata'] == null + ? null + : SlotMetadata.fromJson(json['metadata'] as Map), + publicKey: json['public_key'] as String?, + certificate: json['certificate'] as String?, + ); + +Map _$$_PivImportResultToJson(_$_PivImportResult instance) => + { + 'metadata': instance.metadata, + 'public_key': instance.publicKey, + 'certificate': instance.certificate, + }; diff --git a/lib/piv/state.dart b/lib/piv/state.dart new file mode 100644 index 00000000..8ebb628c --- /dev/null +++ b/lib/piv/state.dart @@ -0,0 +1,70 @@ +/* + * 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 pivStateProvider = AsyncNotifierProvider.autoDispose + .family( + () => throw UnimplementedError(), +); + +abstract class PivStateNotifier extends ApplicationStateNotifier { + Future reset(); + + Future authenticate(String managementKey); + Future setManagementKey( + String managementKey, { + ManagementKeyType managementKeyType = defaultManagementKeyType, + bool storeKey = false, + }); + + Future verifyPin( + String pin); //TODO: Maybe return authenticated? + Future changePin(String pin, String newPin); + Future changePuk(String puk, String newPuk); + Future unblockPin(String puk, String newPin); +} + +final pivSlotsProvider = AsyncNotifierProvider.autoDispose + .family, DevicePath>( + () => throw UnimplementedError(), +); + +abstract class PivSlotsNotifier + extends AutoDisposeFamilyAsyncNotifier, DevicePath> { + Future examine(String data, {String? password}); + Future<(SlotMetadata?, String?)> read(SlotId slot); + Future generate( + SlotId slot, + KeyType keyType, { + required PivGenerateParameters parameters, + PinPolicy pinPolicy = PinPolicy.dfault, + TouchPolicy touchPolicy = TouchPolicy.dfault, + String? pin, + }); + Future import( + SlotId slot, + String data, { + String? password, + PinPolicy pinPolicy = PinPolicy.dfault, + TouchPolicy touchPolicy = TouchPolicy.dfault, + }); + Future delete(SlotId slot); +} diff --git a/lib/piv/views/actions.dart b/lib/piv/views/actions.dart new file mode 100644 index 00000000..f6aa53c0 --- /dev/null +++ b/lib/piv/views/actions.dart @@ -0,0 +1,221 @@ +/* + * 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:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:yubico_authenticator/app/models.dart'; + +import '../../app/message.dart'; +import '../../app/shortcuts.dart'; +import '../../app/state.dart'; +import '../models.dart'; +import '../state.dart'; +import 'authentication_dialog.dart'; +import 'delete_certificate_dialog.dart'; +import 'generate_key_dialog.dart'; +import 'import_file_dialog.dart'; +import 'pin_dialog.dart'; + +class AuthenticateIntent extends Intent { + const AuthenticateIntent(); +} + +class VerifyPinIntent extends Intent { + const VerifyPinIntent(); +} + +class GenerateIntent extends Intent { + const GenerateIntent(); +} + +class ImportIntent extends Intent { + const ImportIntent(); +} + +class ExportIntent extends Intent { + const ExportIntent(); +} + +Future _authenticate( + WidgetRef ref, DevicePath devicePath, PivState pivState) async { + final withContext = ref.read(withContextProvider); + return await withContext((context) async => + await showBlurDialog( + context: context, + builder: (context) => AuthenticationDialog( + devicePath, + pivState, + ), + ) ?? + false); +} + +Future _authIfNeeded( + WidgetRef ref, DevicePath devicePath, PivState pivState) async { + if (pivState.needsAuth) { + return await _authenticate(ref, devicePath, pivState); + } + return true; +} + +Widget registerPivActions( + DevicePath devicePath, + PivState pivState, + PivSlot pivSlot, { + required WidgetRef ref, + required Widget Function(BuildContext context) builder, + Map> actions = const {}, +}) => + Actions( + actions: { + AuthenticateIntent: CallbackAction( + onInvoke: (intent) => _authenticate(ref, devicePath, pivState), + ), + GenerateIntent: + CallbackAction(onInvoke: (intent) async { + if (!await _authIfNeeded(ref, devicePath, pivState)) { + return false; + } + + final withContext = ref.read(withContextProvider); + + // TODO: Avoid asking for PIN if not needed? + final verified = await withContext((context) async => + await showBlurDialog( + context: context, + builder: (context) => PinDialog(devicePath))) ?? + false; + + if (!verified) { + return false; + } + + return await withContext((context) async { + final PivGenerateResult? result = await showBlurDialog( + context: context, + builder: (context) => GenerateKeyDialog( + devicePath, + pivState, + pivSlot, + ), + ); + + switch (result?.generateType) { + case GenerateType.csr: + final filePath = await FilePicker.platform.saveFile( + dialogTitle: 'Save CSR to file', + allowedExtensions: ['csr'], + type: FileType.custom, + lockParentWindow: true, + ); + if (filePath != null) { + final file = File(filePath); + await file.writeAsString(result!.result, flush: true); + } + break; + default: + break; + } + + return result != null; + }); + }), + ImportIntent: CallbackAction(onInvoke: (intent) async { + if (!await _authIfNeeded(ref, devicePath, pivState)) { + return false; + } + + final picked = await FilePicker.platform.pickFiles( + allowedExtensions: ['pem', 'der', 'pfx', 'p12', 'key', 'crt'], + type: FileType.custom, + allowMultiple: false, + lockParentWindow: true, + dialogTitle: 'Select file to import'); + if (picked == null || picked.files.isEmpty) { + return false; + } + + final withContext = ref.read(withContextProvider); + return await withContext((context) async => + await showBlurDialog( + context: context, + builder: (context) => ImportFileDialog( + devicePath, + pivState, + pivSlot, + File(picked.paths.first!), + ), + ) ?? + false); + }), + ExportIntent: CallbackAction(onInvoke: (intent) async { + final (_, cert) = await ref + .read(pivSlotsProvider(devicePath).notifier) + .read(pivSlot.slot); + + if (cert == null) { + return false; + } + + final filePath = await FilePicker.platform.saveFile( + dialogTitle: 'Export certificate to file', + allowedExtensions: ['pem'], + type: FileType.custom, + lockParentWindow: true, + ); + if (filePath == null) { + return false; + } + + final file = File(filePath); + await file.writeAsString(cert, flush: true); + + await ref.read(withContextProvider)((context) async { + showMessage(context, 'Certificate exported'); + }); + return true; + }), + DeleteIntent: CallbackAction(onInvoke: (_) async { + if (!await _authIfNeeded(ref, devicePath, pivState)) { + return false; + } + final withContext = ref.read(withContextProvider); + final bool? deleted = await withContext((context) async => + await showBlurDialog( + context: context, + builder: (context) => DeleteCertificateDialog( + devicePath, + pivSlot, + ), + ) ?? + false); + + // Needs to move to slot dialog(?) or react to state change + // Pop the slot dialog if deleted + if (deleted == true) { + await withContext((context) async { + Navigator.of(context).pop(); + }); + } + return deleted; + }), //TODO + ...actions, + }, + child: Builder(builder: builder), + ); diff --git a/lib/piv/views/authentication_dialog.dart b/lib/piv/views/authentication_dialog.dart new file mode 100644 index 00000000..aabd5f36 --- /dev/null +++ b/lib/piv/views/authentication_dialog.dart @@ -0,0 +1,109 @@ +/* + * 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/models.dart'; +import '../../exception/cancellation_exception.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; + +class AuthenticationDialog extends ConsumerStatefulWidget { + final DevicePath devicePath; + final PivState pivState; + const AuthenticationDialog(this.devicePath, this.pivState, {super.key}); + + @override + ConsumerState createState() => + _AuthenticationDialogState(); +} + +class _AuthenticationDialogState extends ConsumerState { + String _managementKey = ''; + bool _keyIsWrong = false; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return ResponsiveDialog( + title: Text("Unlock management functions"), + actions: [ + TextButton( + key: keys.unlockButton, + onPressed: () async { + final navigator = Navigator.of(context); + try { + final status = await ref + .read(pivStateProvider(widget.devicePath).notifier) + .authenticate(_managementKey); + if (status) { + navigator.pop(true); + } else { + setState(() { + _keyIsWrong = true; + }); + } + } on CancellationException catch (_) { + navigator.pop(false); + } catch (_) { + // TODO: More error cases + setState(() { + _keyIsWrong = true; + }); + } + }, + child: Text(l10n.s_unlock), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + autofocus: true, + obscureText: true, + autofillHints: const [AutofillHints.password], + key: keys.managementKeyField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "Management key", + prefixIcon: const Icon(Icons.key_outlined), + errorText: _keyIsWrong ? l10n.s_wrong_password : null, + errorMaxLines: 3), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _keyIsWrong = false; + _managementKey = value; + }); + }, + ), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/delete_certificate_dialog.dart b/lib/piv/views/delete_certificate_dialog.dart new file mode 100644 index 00000000..57f908b1 --- /dev/null +++ b/lib/piv/views/delete_certificate_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 '../../exception/cancellation_exception.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; + +class DeleteCertificateDialog extends ConsumerWidget { + final DevicePath devicePath; + final PivSlot pivSlot; + const DeleteCertificateDialog(this.devicePath, this.pivSlot, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + return ResponsiveDialog( + title: Text(l10n.s_delete_account), + actions: [ + TextButton( + key: keys.deleteButton, + onPressed: () async { + try { + await ref + .read(pivSlotsProvider(devicePath).notifier) + .delete(pivSlot.slot); + await ref.read(withContextProvider)( + (context) async { + Navigator.of(context).pop(true); + showMessage(context, l10n.s_account_deleted); + }, + ); + } on CancellationException catch (_) { + // ignored + } + }, + 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_account), + Text( + l10n.p_warning_disable_credential, + style: Theme.of(context).textTheme.bodyLarge, + ), + Text(// TODO + 'Delete certificate in ${pivSlot.slot.getDisplayName(l10n)} (Slot ${pivSlot.slot.id.toRadixString(16).padLeft(2, '0')})?'), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart new file mode 100644 index 00000000..ca83dbc8 --- /dev/null +++ b/lib/piv/views/generate_key_dialog.dart @@ -0,0 +1,166 @@ +/* + * 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/models.dart'; +import '../../core/models.dart'; +import '../../widgets/choice_filter_chip.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; + +class GenerateKeyDialog extends ConsumerStatefulWidget { + final DevicePath devicePath; + final PivState pivState; + final PivSlot pivSlot; + const GenerateKeyDialog(this.devicePath, this.pivState, this.pivSlot, + {super.key}); + + @override + ConsumerState createState() => + _GenerateKeyDialogState(); +} + +class _GenerateKeyDialogState extends ConsumerState { + String _subject = ''; + GenerateType _generateType = defaultGenerateType; + KeyType _keyType = defaultKeyType; + late DateTime _validFrom; + late DateTime _validTo; + late DateTime _validToDefault; + late DateTime _validToMax; + + @override + void initState() { + super.initState(); + + final now = DateTime.now(); + _validFrom = DateTime.utc(now.year, now.month, now.day); + _validToDefault = DateTime.utc(now.year + 1, now.month, now.day); + _validTo = _validToDefault; + _validToMax = DateTime.utc(now.year + 10, now.month, now.day); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final navigator = Navigator.of(context); + return ResponsiveDialog( + title: Text("Generate key"), + actions: [ + TextButton( + key: keys.saveButton, + onPressed: () async { + final result = await ref + .read(pivSlotsProvider(widget.devicePath).notifier) + .generate( + widget.pivSlot.slot, + _keyType, + parameters: switch (_generateType) { + GenerateType.certificate => + PivGenerateParameters.certificate( + subject: _subject, + validFrom: _validFrom, + validTo: _validTo), + GenerateType.csr => + PivGenerateParameters.csr(subject: _subject), + }, + ); + + navigator.pop(result); + }, + child: Text(l10n.s_save), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + autofocus: true, + key: keys.subjectField, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: "Subject", + ), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _subject = value; + }); + }, + ), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4.0, + runSpacing: 8.0, + children: [ + ChoiceFilterChip( + items: GenerateType.values, + value: _generateType, + selected: _generateType != defaultGenerateType, + itemBuilder: (value) => Text(value.getDisplayName(l10n)), + onChanged: (value) { + setState(() { + _generateType = value; + }); + }, + ), + ChoiceFilterChip( + items: KeyType.values, + value: _keyType, + selected: _keyType != defaultKeyType, + itemBuilder: (value) => Text(value.getDisplayName(l10n)), + onChanged: (value) { + setState(() { + _keyType = value; + }); + }, + ), + if (_generateType == GenerateType.certificate) + FilterChip( + label: Text(dateFormatter.format(_validTo)), + onSelected: (value) async { + final selected = await showDatePicker( + context: context, + initialDate: _validTo, + firstDate: _validFrom, + lastDate: _validToMax, + ); + if (selected != null) { + setState(() { + _validTo = selected; + }); + } + }, + ), + ]), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/import_file_dialog.dart b/lib/piv/views/import_file_dialog.dart new file mode 100644 index 00000000..d85bb991 --- /dev/null +++ b/lib/piv/views/import_file_dialog.dart @@ -0,0 +1,186 @@ +/* + * 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:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/models.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; + +class ImportFileDialog extends ConsumerStatefulWidget { + final DevicePath devicePath; + final PivState pivState; + final PivSlot pivSlot; + final File file; + const ImportFileDialog( + this.devicePath, this.pivState, this.pivSlot, this.file, + {super.key}); + + @override + ConsumerState createState() => + _ImportFileDialogState(); +} + +class _ImportFileDialogState extends ConsumerState { + late String _data; + PivExamineResult? _state; + String _password = ''; + bool _passwordIsWrong = false; + + @override + void initState() { + super.initState(); + _init(); + } + + void _init() async { + final bytes = await widget.file.readAsBytes(); + _data = bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(); + _examine(); + } + + void _examine() async { + setState(() { + _state = null; + }); + final result = await ref + .read(pivSlotsProvider(widget.devicePath).notifier) + .examine(_data, password: _password.isNotEmpty ? _password : null); + setState(() { + _state = result; + _passwordIsWrong = result.maybeWhen( + invalidPassword: () => _password.isNotEmpty, + orElse: () => true, + ); + }); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final state = _state; + if (state == null) { + return ResponsiveDialog( + title: Text("Import file"), + actions: [ + TextButton( + key: keys.unlockButton, + onPressed: null, + child: Text(l10n.s_unlock), + ), + ], + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 18.0), + child: Center( + child: CircularProgressIndicator(), + )), + ); + } + + return state.when( + invalidPassword: () => ResponsiveDialog( + title: Text("Import file"), + actions: [ + TextButton( + key: keys.unlockButton, + onPressed: () => _examine(), + child: Text(l10n.s_unlock), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + autofocus: true, + obscureText: true, + autofillHints: const [AutofillHints.password], + key: keys.managementKeyField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "Password", + prefixIcon: const Icon(Icons.password_outlined), + errorText: _passwordIsWrong ? l10n.s_wrong_password : null, + errorMaxLines: 3), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _passwordIsWrong = false; + _password = value; + }); + }, + onSubmitted: (_) => _examine(), + ), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ), + result: (_, privateKey, certificates) => ResponsiveDialog( + title: Text("Import file"), + actions: [ + TextButton( + key: keys.unlockButton, + onPressed: () async { + final navigator = Navigator.of(context); + try { + await ref + .read(pivSlotsProvider(widget.devicePath).notifier) + .import(widget.pivSlot.slot, _data, + password: _password.isNotEmpty ? _password : null); + navigator.pop(true); + } catch (_) { + // TODO: More error cases + setState(() { + _passwordIsWrong = true; + }); + } + }, + child: Text(l10n.s_save), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Import the following into slot ${widget.pivSlot.slot.getDisplayName(l10n)}?"), + if (privateKey) Text("- Private key"), + if (certificates > 0) Text("- Certificate"), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ), + ); + } +} diff --git a/lib/piv/views/key_actions.dart b/lib/piv/views/key_actions.dart new file mode 100644 index 00000000..68b833a8 --- /dev/null +++ b/lib/piv/views/key_actions.dart @@ -0,0 +1,141 @@ +/* + * 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 '../../widgets/list_title.dart'; +import '../models.dart'; +import '../keys.dart' as keys; +import 'manage_key_dialog.dart'; +import 'manage_pin_puk_dialog.dart'; +import 'reset_dialog.dart'; + +Widget pivBuildActions(BuildContext context, DevicePath devicePath, + PivState pivState, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + final theme = + ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; + + final usingDefaultMgmtKey = + pivState.metadata?.managementKeyMetadata.defaultValue == true; + + final pinBlocked = pivState.pinAttempts == 0; + + return FsDialog( + child: Column( + children: [ + ListTitle(l10n.s_manage, + textStyle: Theme.of(context).textTheme.bodyLarge), + ListTile( + key: keys.managePinAction, + title: Text(l10n.s_pin), + subtitle: Text(pinBlocked + ? 'Blocked, use PUK to reset' + : '${pivState.pinAttempts} attempts remaining'), + leading: CircleAvatar( + foregroundColor: theme.onSecondary, + backgroundColor: theme.secondary, + child: const Icon(Icons.pin_outlined), + ), + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ManagePinPukDialog( + devicePath, + target: pinBlocked ? ManageTarget.unblock : ManageTarget.pin, + ), + ); + }), + ListTile( + key: keys.managePukAction, + title: Text('PUK'), // TODO + subtitle: Text( + '${pivState.metadata?.pukMetadata.attemptsRemaining ?? '?'} attempts remaining'), + leading: CircleAvatar( + foregroundColor: theme.onSecondary, + backgroundColor: theme.secondary, + child: const Icon(Icons.pin_outlined), + ), + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => + ManagePinPukDialog(devicePath, target: ManageTarget.puk), + ); + }), + ListTile( + key: keys.manageManagementKeyAction, + title: Text('Management Key'), // TODO + subtitle: Text(usingDefaultMgmtKey + ? 'Warning: Default key used' + : (pivState.protectedKey + ? 'PIN can be used instead' + : 'Change your management key')), + leading: CircleAvatar( + foregroundColor: theme.onSecondary, + backgroundColor: theme.secondary, + child: const Icon(Icons.key_outlined), + ), + trailing: + usingDefaultMgmtKey ? const Icon(Icons.warning_amber) : null, + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ManageKeyDialog(devicePath, pivState), + ); + }), + ListTile( + key: keys.resetAction, + title: Text('Reset PIV'), //TODO + subtitle: Text(l10n.l_factory_reset_this_app), + leading: CircleAvatar( + foregroundColor: theme.onError, + backgroundColor: theme.error, + child: const Icon(Icons.delete_outline), + ), + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ResetDialog(devicePath), + ); + }), + ListTitle(l10n.s_setup, + textStyle: Theme.of(context).textTheme.bodyLarge), + ListTile( + key: keys.setupMacOsAction, + title: Text('Setup for macOS'), //TODO + subtitle: Text('Create certificates for macOS login'), //TODO + leading: CircleAvatar( + backgroundColor: theme.secondary, + foregroundColor: theme.onSecondary, + child: const Icon(Icons.laptop), + ), + onTap: () async { + Navigator.of(context).pop(); + }), + ], + ), + ); +} diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart new file mode 100644 index 00000000..faf12dac --- /dev/null +++ b/lib/piv/views/manage_key_dialog.dart @@ -0,0 +1,247 @@ +/* + * 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/services.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/choice_filter_chip.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; +import 'pin_dialog.dart'; + +class ManageKeyDialog extends ConsumerStatefulWidget { + final DevicePath path; + final PivState pivState; + const ManageKeyDialog(this.path, this.pivState, {super.key}); + + @override + ConsumerState createState() => + _ManageKeyDialogState(); +} + +class _ManageKeyDialogState extends ConsumerState { + late bool _defaultKeyUsed; + late bool _usesStoredKey; + late bool _storeKey; + String _currentKeyOrPin = ''; + bool _currentIsWrong = false; + int _attemptsRemaining = -1; + String _newKey = ''; + ManagementKeyType _keyType = ManagementKeyType.tdes; + + @override + void initState() { + super.initState(); + + _defaultKeyUsed = + widget.pivState.metadata?.managementKeyMetadata.defaultValue ?? false; + _usesStoredKey = widget.pivState.protectedKey; + if (!_usesStoredKey && _defaultKeyUsed) { + _currentKeyOrPin = defaultManagementKey; + } + _storeKey = _usesStoredKey; + } + + _submit() async { + final notifier = ref.read(pivStateProvider(widget.path).notifier); + if (_usesStoredKey) { + final status = (await notifier.verifyPin(_currentKeyOrPin)).when( + success: () => true, + failure: (attemptsRemaining) { + setState(() { + _attemptsRemaining = attemptsRemaining; + _currentIsWrong = true; + }); + return false; + }, + ); + if (!status) { + return; + } + } else { + if (!await notifier.authenticate(_currentKeyOrPin)) { + setState(() { + _currentIsWrong = true; + }); + return; + } + } + + if (_storeKey && !_usesStoredKey) { + final withContext = ref.read(withContextProvider); + final verified = await withContext((context) async => + await showBlurDialog( + context: context, + builder: (context) => PinDialog(widget.path))) ?? + false; + + if (!verified) { + return; + } + } + + print("Set new key: $_newKey"); + await notifier.setManagementKey(_newKey, + managementKeyType: _keyType, storeKey: _storeKey); + if (!mounted) return; + showMessage(context, "Management key changed"); + + Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final currentType = widget.pivState.metadata?.managementKeyMetadata.keyType; + final hexLength = _keyType.keyLength * 2; + + return ResponsiveDialog( + title: Text('Change Management Key'), + actions: [ + TextButton( + onPressed: _submit, + key: keys.saveButton, + child: Text(l10n.s_save), + ) + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.p_enter_current_password_or_reset), + if (widget.pivState.protectedKey) + TextField( + autofocus: true, + obscureText: true, + autofillHints: const [AutofillHints.password], + key: keys.managementKeyField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "PIN", + prefixIcon: const Icon(Icons.pin_outlined), + errorText: _currentIsWrong + ? "Wrong PIN ($_attemptsRemaining attempts left)" + : null, + errorMaxLines: 3), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _currentIsWrong = false; + _currentKeyOrPin = value; + }); + }, + ), + if (!widget.pivState.protectedKey) + TextFormField( + key: keys.pinPukField, + autofocus: !_defaultKeyUsed, + autofillHints: const [AutofillHints.password], + initialValue: _defaultKeyUsed ? defaultManagementKey : null, + readOnly: _defaultKeyUsed, + maxLength: !_defaultKeyUsed && currentType != null + ? currentType.keyLength * 2 + : null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'Current management key', + prefixIcon: const Icon(Icons.password_outlined), + errorText: _currentIsWrong ? 'Wrong key' : null, + errorMaxLines: 3, + helperText: + _defaultKeyUsed ? "Default management key used" : null, + ), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _currentIsWrong = false; + _currentKeyOrPin = value; + }); + }, + ), + Text("Enter your new management key."), + TextField( + key: keys.newPinPukField, + autofocus: _defaultKeyUsed, + autofillHints: const [AutofillHints.newPassword], + maxLength: hexLength, + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp('[a-f0-9]', caseSensitive: false)) + ], + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "New management key", + prefixIcon: const Icon(Icons.password_outlined), + enabled: _currentKeyOrPin.isNotEmpty, + ), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _newKey = value; + }); + }, + onSubmitted: (_) { + if (_newKey.length == hexLength) { + _submit(); + } + }, + ), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4.0, + runSpacing: 8.0, + children: [ + if (currentType != null) + ChoiceFilterChip( + items: ManagementKeyType.values, + value: _keyType, + selected: _keyType != defaultManagementKeyType, + itemBuilder: (value) => Text(value.getDisplayName(l10n)), + onChanged: (value) { + setState(() { + _keyType = value; + }); + }, + ), + FilterChip( + label: Text("Protect with PIN"), + selected: _storeKey, + onSelected: (value) { + setState(() { + _storeKey = value; + }); + }, + ), + ]), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart new file mode 100644 index 00000000..eb7f455c --- /dev/null +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -0,0 +1,192 @@ +/* + * 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 '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; + +enum ManageTarget { pin, puk, unblock } + +class ManagePinPukDialog extends ConsumerStatefulWidget { + final DevicePath path; + final ManageTarget target; + const ManagePinPukDialog(this.path, + {super.key, this.target = ManageTarget.pin}); + + @override + ConsumerState createState() => + _ManagePinPukDialogState(); +} + +//TODO: Use switch expressions in Dart 3 +class _ManagePinPukDialogState extends ConsumerState { + String _currentPin = ''; + String _newPin = ''; + String _confirmPin = ''; + bool _currentIsWrong = false; + int _attemptsRemaining = -1; + + _submit() async { + final notifier = ref.read(pivStateProvider(widget.path).notifier); + final PinVerificationStatus result; + switch (widget.target) { + case ManageTarget.pin: + result = await notifier.changePin(_currentPin, _newPin); + break; + case ManageTarget.puk: + result = await notifier.changePuk(_currentPin, _newPin); + break; + case ManageTarget.unblock: + result = await notifier.unblockPin(_currentPin, _newPin); + break; + } + + result.when(success: () { + if (!mounted) return; + Navigator.of(context).pop(); + showMessage(context, AppLocalizations.of(context)!.s_password_set); + }, failure: (attemptsRemaining) { + setState(() { + _attemptsRemaining = attemptsRemaining; + _currentIsWrong = true; + _currentPin = ''; + }); + }); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final isValid = + _newPin.isNotEmpty && _newPin == _confirmPin && _currentPin.isNotEmpty; + + final String titleText; + switch (widget.target) { + case ManageTarget.pin: + titleText = "Change PIN"; + break; + case ManageTarget.puk: + titleText = l10n.s_manage_password; + break; + case ManageTarget.unblock: + titleText = "Unblock PIN"; + break; + } + + return ResponsiveDialog( + title: Text(titleText), + actions: [ + TextButton( + onPressed: isValid ? _submit : null, + key: keys.saveButton, + child: Text(l10n.s_save), + ) + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.p_enter_current_password_or_reset), + TextField( + autofocus: true, + obscureText: true, + autofillHints: const [AutofillHints.password], + key: keys.pinPukField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: widget.target == ManageTarget.pin + ? 'Current PIN' + : 'Current PUK', + prefixIcon: const Icon(Icons.password_outlined), + errorText: _currentIsWrong + ? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining) + : null, + errorMaxLines: 3), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _currentIsWrong = false; + _currentPin = value; + }); + }, + ), + Text( + "Enter your new ${widget.target == ManageTarget.puk ? 'PUK' : 'PIN'}. Must be 6-8 characters."), + TextField( + key: keys.newPinPukField, + obscureText: true, + autofillHints: const [AutofillHints.newPassword], + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: widget.target == ManageTarget.puk + ? "New PUK" + : l10n.s_new_pin, + prefixIcon: const Icon(Icons.password_outlined), + enabled: _currentPin.isNotEmpty, + ), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _newPin = value; + }); + }, + onSubmitted: (_) { + if (isValid) { + _submit(); + } + }, + ), + TextField( + key: keys.confirmPinPukField, + obscureText: true, + autofillHints: const [AutofillHints.newPassword], + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_confirm_pin, + prefixIcon: const Icon(Icons.password_outlined), + enabled: _currentPin.isNotEmpty && _newPin.isNotEmpty, + ), + textInputAction: TextInputAction.done, + onChanged: (value) { + setState(() { + _confirmPin = value; + }); + }, + onSubmitted: (_) { + if (isValid) { + _submit(); + } + }, + ), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/pin_dialog.dart b/lib/piv/views/pin_dialog.dart new file mode 100644 index 00000000..84485f8f --- /dev/null +++ b/lib/piv/views/pin_dialog.dart @@ -0,0 +1,111 @@ +/* + * 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/models.dart'; +import '../../exception/cancellation_exception.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../state.dart'; +import '../keys.dart' as keys; + +class PinDialog extends ConsumerStatefulWidget { + final DevicePath devicePath; + const PinDialog(this.devicePath, {super.key}); + + @override + ConsumerState createState() => _PinDialogState(); +} + +class _PinDialogState extends ConsumerState { + String _pin = ''; + bool _pinIsWrong = false; + int _attemptsRemaining = -1; + + Future _submit() async { + final navigator = Navigator.of(context); + try { + final status = await ref + .read(pivStateProvider(widget.devicePath).notifier) + .verifyPin(_pin); + status.when( + success: () { + navigator.pop(true); + }, + failure: (attemptsRemaining) { + setState(() { + _attemptsRemaining = attemptsRemaining; + _pinIsWrong = true; + }); + }, + ); + } on CancellationException catch (_) { + navigator.pop(false); + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return ResponsiveDialog( + title: Text("PIN required"), + actions: [ + TextButton( + key: keys.unlockButton, + onPressed: _submit, + child: Text(l10n.s_unlock), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + autofocus: true, + obscureText: true, + autofillHints: const [AutofillHints.password], + key: keys.managementKeyField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "PIN", + prefixIcon: const Icon(Icons.pin_outlined), + errorText: _pinIsWrong + ? "Wrong PIN ($_attemptsRemaining attempts left)" + : null, + errorMaxLines: 3), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _pinIsWrong = false; + _pin = value; + }); + }, + onSubmitted: (_) => _submit(), + ), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart new file mode 100644 index 00000000..39550804 --- /dev/null +++ b/lib/piv/views/piv_screen.dart @@ -0,0 +1,119 @@ +/* + * 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_page.dart'; +import '../../app/views/message_page.dart'; +import '../models.dart'; +import '../state.dart'; +import 'key_actions.dart'; +import 'slot_dialog.dart'; + +class PivScreen extends ConsumerWidget { + final DevicePath devicePath; + + const PivScreen(this.devicePath, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + return ref.watch(pivStateProvider(devicePath)).when( + loading: () => MessagePage( + title: Text(l10n.s_authenticator), + graphic: const CircularProgressIndicator(), + delayedContent: true, + ), + error: (error, _) => AppFailurePage( + title: Text(l10n.s_authenticator), + cause: error, + ), + data: (pivState) { + final pivSlots = ref.watch(pivSlotsProvider(devicePath)).asData; + return AppPage( + title: const Text('PIV'), + keyActionsBuilder: (context) => + pivBuildActions(context, devicePath, pivState, ref), + child: Column( + children: [ + if (pivSlots?.hasValue == true) + ...pivSlots!.value.map((e) => Actions( + actions: { + OpenIntent: + CallbackAction(onInvoke: (_) async { + await showBlurDialog( + context: context, + builder: (context) => + SlotDialog(pivState, e.slot), + ); + return null; + }), + }, + child: _CertificateListItem(e), + )) + ], + ), + ); + }, + ); + } +} + +class _CertificateListItem extends StatelessWidget { + final PivSlot pivSlot; + const _CertificateListItem(this.pivSlot); + + @override + Widget build(BuildContext context) { + final slot = pivSlot.slot; + final certInfo = pivSlot.certInfo; + final l10n = AppLocalizations.of(context)!; + final colorScheme = Theme.of(context).colorScheme; + return ListTile( + leading: CircleAvatar( + foregroundColor: colorScheme.onSecondary, + backgroundColor: colorScheme.secondary, + child: const Icon(Icons.approval), + ), + title: Text( + '${slot.getDisplayName(l10n)} (Slot ${slot.id.toRadixString(16).padLeft(2, '0')})', + softWrap: false, + overflow: TextOverflow.fade, + ), + subtitle: certInfo != null + ? Text( + 'Subject: ${certInfo.subject}, Issuer: ${certInfo.issuer}', + softWrap: false, + overflow: TextOverflow.fade, + ) + : Text(pivSlot.hasKey == true + ? 'Key without certificate loaded' + : 'No certificate loaded'), + trailing: OutlinedButton( + onPressed: () { + Actions.maybeInvoke(context, const OpenIntent()); + }, + child: const Icon(Icons.more_horiz), + ), + ); + } +} diff --git a/lib/piv/views/reset_dialog.dart b/lib/piv/views/reset_dialog.dart new file mode 100644 index 00000000..8bac4186 --- /dev/null +++ b/lib/piv/views/reset_dialog.dart @@ -0,0 +1,67 @@ +/* + * 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 ResetDialog extends ConsumerWidget { + final DevicePath devicePath; + const ResetDialog(this.devicePath, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + return ResponsiveDialog( + title: Text(l10n.s_factory_reset), + actions: [ + TextButton( + onPressed: () async { + await ref.read(pivStateProvider(devicePath).notifier).reset(); + await ref.read(withContextProvider)((context) async { + Navigator.of(context).pop(); + showMessage(context, l10n.l_oath_application_reset); //TODO + }); + }, + child: Text(l10n.s_reset), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + children: [ + Text( + l10n.p_warning_factory_reset, // TODO + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text(l10n.p_warning_disable_credentials), //TODO + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/slot_dialog.dart b/lib/piv/views/slot_dialog.dart new file mode 100644 index 00000000..9e04683d --- /dev/null +++ b/lib/piv/views/slot_dialog.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/shortcuts.dart'; +import '../../app/state.dart'; +import '../../app/views/fs_dialog.dart'; +import '../../widgets/list_title.dart'; +import '../models.dart'; +import '../state.dart'; +import 'actions.dart'; + +class SlotDialog extends ConsumerWidget { + final PivState pivState; + final SlotId pivSlot; + const SlotDialog(this.pivState, this.pivSlot, {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 slotData = ref.watch(pivSlotsProvider(node.path).select((value) => + value.whenOrNull( + data: (data) => + data.firstWhere((element) => element.slot == pivSlot)))); + + if (slotData == null) { + return const FsDialog(child: CircularProgressIndicator()); + } + + final certInfo = slotData.certInfo; + return registerPivActions( + node.path, + pivState, + slotData, + ref: ref, + builder: (context) => FocusScope( + autofocus: true, + child: FsDialog( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 48, bottom: 32), + child: Column( + children: [ + Text( + '${pivSlot.getDisplayName(l10n)} (Slot ${pivSlot.id.toRadixString(16).padLeft(2, '0')})', + style: textTheme.headlineSmall, + softWrap: true, + textAlign: TextAlign.center, + ), + if (certInfo != null) ...[ + Text( + 'Subject: ${certInfo.subject}, Issuer: ${certInfo.issuer}', + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ), + ), + Text( + 'Serial: ${certInfo.serial}', + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ), + ), + Text( + 'Fingerprint: ${certInfo.fingerprint}', + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ), + ), + Text( + 'Not before: ${certInfo.notValidBefore}, Not after: ${certInfo.notValidAfter}', + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ), + ), + ] else ...[ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + 'No certificate loaded', + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ), + ), + ), + ], + const SizedBox(height: 16), + ], + ), + ), + ListTitle(AppLocalizations.of(context)!.s_actions, + textStyle: textTheme.bodyLarge), + _SlotDialogActions(certInfo), + ], + ), + ), + ), + ); + } +} + +class _SlotDialogActions extends StatelessWidget { + final CertInfo? certInfo; + const _SlotDialogActions(this.certInfo); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final theme = + ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; + return Column( + children: [ + ListTile( + leading: CircleAvatar( + backgroundColor: theme.primary, + foregroundColor: theme.onPrimary, + child: const Icon(Icons.add_outlined), + ), + title: Text('Generate key'), + subtitle: Text('Generate a new certificate or CSR'), + onTap: () { + Actions.invoke(context, const GenerateIntent()); + }, + ), + ListTile( + leading: CircleAvatar( + backgroundColor: theme.secondary, + foregroundColor: theme.onSecondary, + child: const Icon(Icons.file_download_outlined), + ), + title: Text('Import file'), + subtitle: Text('Import a key and/or certificate from file'), + onTap: () { + Actions.invoke(context, const ImportIntent()); + }, + ), + if (certInfo != null) ...[ + ListTile( + leading: CircleAvatar( + backgroundColor: theme.secondary, + foregroundColor: theme.onSecondary, + child: const Icon(Icons.file_upload_outlined), + ), + title: Text('Export certificate'), + subtitle: Text('Export the certificate to file'), + onTap: () { + Actions.invoke(context, const ExportIntent()); + }, + ), + ListTile( + leading: CircleAvatar( + backgroundColor: theme.error, + foregroundColor: theme.onError, + child: const Icon(Icons.delete_outline), + ), + title: Text('Delete certificate'), + subtitle: Text('Remove the certificate from the YubiKey'), + onTap: () { + Actions.invoke(context, const DeleteIntent()); + }, + ), + ], + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index e62a2742..0291513d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -384,10 +384,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: "61a60716544392a82726dd0fa1dd6f5f1fd32aec66422b6e229e7b90d52325c4" + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.7.1" lints: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 17ae1a01..f1801cea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -80,7 +80,7 @@ dev_dependencies: build_runner: ^2.4.5 freezed: ^2.3.5 - json_serializable: ^6.5.4 + json_serializable: ^6.7.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From fcd226f12367408d8f4ffa3764127a01faba5ed6 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 5 Jun 2023 15:56:13 +0200 Subject: [PATCH 023/158] Add localized strings. --- check_strings.py | 2 +- lib/app/models.dart | 2 +- lib/desktop/piv/state.dart | 12 +- lib/l10n/app_en.arb | 128 +++++++++++++++++++ lib/piv/models.dart | 39 +++--- lib/piv/views/actions.dart | 51 +++++--- lib/piv/views/authentication_dialog.dart | 7 +- lib/piv/views/delete_certificate_dialog.dart | 14 +- lib/piv/views/generate_key_dialog.dart | 8 +- lib/piv/views/import_file_dialog.dart | 19 +-- lib/piv/views/key_actions.dart | 55 ++++---- lib/piv/views/manage_key_dialog.dart | 26 ++-- lib/piv/views/manage_pin_puk_dialog.dart | 16 +-- lib/piv/views/pin_dialog.dart | 7 +- lib/piv/views/piv_screen.dart | 14 +- lib/piv/views/reset_dialog.dart | 6 +- lib/piv/views/slot_dialog.dart | 33 ++--- 17 files changed, 292 insertions(+), 147 deletions(-) diff --git a/check_strings.py b/check_strings.py index 57a2bfa7..b34d8eeb 100755 --- a/check_strings.py +++ b/check_strings.py @@ -93,7 +93,7 @@ if len(sys.argv) != 2: target = sys.argv[1] -with open(target) as f: +with open(target, encoding='utf-8') as f: values = json.load(f, object_pairs_hook=check_duplicate_keys) strings = {k: v for k, v in values.items() if not k.startswith("@")} diff --git a/lib/app/models.dart b/lib/app/models.dart index 78cec6be..d23b1ac3 100755 --- a/lib/app/models.dart +++ b/lib/app/models.dart @@ -53,7 +53,7 @@ enum Application { String getDisplayName(AppLocalizations l10n) => switch (this) { Application.oath => l10n.s_authenticator, Application.fido => l10n.s_webauthn, - Application.piv => "PIV", //TODO + Application.piv => l10n.s_piv, _ => name.substring(0, 1).toUpperCase() + name.substring(1), }; diff --git a/lib/desktop/piv/state.dart b/lib/desktop/piv/state.dart index 27c9a961..b588d929 100644 --- a/lib/desktop/piv/state.dart +++ b/lib/desktop/piv/state.dart @@ -270,10 +270,6 @@ class _DesktopPivStateNotifier extends PivStateNotifier { final _shownSlots = SlotId.values.map((slot) => slot.id).toList(); -extension on SlotId { - String get node => id.toRadixString(16).padLeft(2, '0'); -} - final desktopPivSlots = AsyncNotifierProvider.autoDispose .family, DevicePath>( _DesktopPivSlotsNotifier.new); @@ -295,7 +291,7 @@ class _DesktopPivSlotsNotifier extends PivSlotsNotifier { @override Future delete(SlotId slot) async { - await _session.command('delete', target: ['slots', slot.node]); + await _session.command('delete', target: ['slots', slot.hexId]); ref.invalidateSelf(); } @@ -350,7 +346,7 @@ class _DesktopPivSlotsNotifier extends PivSlotsNotifier { 'generate', target: [ 'slots', - slot.node, + slot.hexId, ], params: { 'key_type': keyType.value, @@ -397,7 +393,7 @@ class _DesktopPivSlotsNotifier extends PivSlotsNotifier { TouchPolicy touchPolicy = TouchPolicy.dfault}) async { final result = await _session.command('import_file', target: [ 'slots', - slot.node, + slot.hexId, ], params: { 'data': data, 'password': password, @@ -413,7 +409,7 @@ class _DesktopPivSlotsNotifier extends PivSlotsNotifier { Future<(SlotMetadata?, String?)> read(SlotId slot) async { final result = await _session.command('get', target: [ 'slots', - slot.node, + slot.hexId, ]); final data = result['data']; final metadata = data['metadata']; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3ea7697c..380f4412 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -29,6 +29,7 @@ "s_quit": "Quit", "s_unlock": "Unlock", "s_calculate": "Calculate", + "s_import": "Import", "s_label": "Label", "s_name": "Name", "s_usb": "USB", @@ -41,6 +42,12 @@ "label": {} } }, + "l_bullet": "• {item}", + "@l_bullet" : { + "placeholders": { + "item": {} + } + }, "s_about": "About", "s_appearance": "Appearance", @@ -49,6 +56,7 @@ "s_manage": "Manage", "s_setup": "Setup", "s_settings": "Settings", + "s_piv": "PIV", "s_webauthn": "WebAuthn", "s_help_and_about": "Help and about", "s_help_and_feedback": "Help and feedback", @@ -61,6 +69,7 @@ "s_configure_yk": "Configure YubiKey", "s_please_wait": "Please wait\u2026", "s_secret_key": "Secret key", + "s_private_key": "Private key", "s_invalid_length": "Invalid length", "s_require_touch": "Require touch", "q_have_account_info": "Have account info?", @@ -166,11 +175,17 @@ "@_pins": {}, "s_pin": "PIN", + "s_puk": "PUK", "s_set_pin": "Set PIN", "s_change_pin": "Change PIN", + "s_change_puk": "Change PUK", "s_current_pin": "Current PIN", + "s_current_puk": "Current PUK", "s_new_pin": "New PIN", + "s_new_puk": "New PUK", "s_confirm_pin": "Confirm PIN", + "s_confirm_puk": "Confirm PUK", + "s_unblock_pin": "Unblock PIN", "l_new_pin_len": "New PIN must be at least {length} characters", "@l_new_pin_len" : { "placeholders": { @@ -184,6 +199,12 @@ "message": {} } }, + "l_attempts_remaining": "{retries} attempt(s) remaining", + "@l_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, "l_wrong_pin_attempts_remaining": "Wrong PIN, {retries} attempt(s) remaining", "@l_wrong_pin_attempts_remaining" : { "placeholders": { @@ -205,6 +226,15 @@ "length": {} } }, + "s_pin_required": "PIN required", + "p_pin_required_desc": "The action you are about to perform requires the PIV PIN to be entered.", + "l_piv_pin_blocked": "Blocked, use PUK to reset", + "p_enter_new_piv_pin_puk": "Enter a new {name} to set. Must be 6-8 characters.", + "@p_enter_new_piv_pin_puk" : { + "placeholders": { + "name": {} + } + }, "@_passwords": {}, "s_password": "Password", @@ -228,6 +258,21 @@ "p_enter_current_password_or_reset": "Enter your current password. If you don't know your password, you'll need to reset the YubiKey.", "p_enter_new_password": "Enter your new password. A password may contain letters, numbers and special characters.", + "@_management_key": {}, + "s_management_key": "Management key", + "s_current_management_key": "Current management key", + "s_new_management_key": "New 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", + "l_default_key_used": "Default management key used", + "l_warning_default_key": "Warning: Default key used", + "s_protect_key": "Protect with PIN", + "l_pin_protected_key": "PIN can be used instead", + "l_wrong_key": "Wrong key", + "l_unlock_piv_management": "Unlock PIV management", + "p_unlock_piv_management_desc": "The action you are about to perform requires the PIV management key. Provide this key to unlock management functionality for this session.", + "@_oath_accounts": {}, "l_account": "Account: {label}", "@l_account" : { @@ -351,6 +396,85 @@ "p_press_fingerprint_begin": "Press your finger against the YubiKey to begin.", "p_will_change_label_fp": "This will change the label of the fingerprint.", + "@_certificates": {}, + "s_certificate": "Certificate", + "s_csr": "CSR", + "s_subject": "Subject", + "l_export_csr_file": "Save CSR to file", + "l_select_import_file": "Select file to import", + "l_export_certificate": "Export certificate", + "l_export_certificate_file": "Export certificate to file", + "l_export_certificate_desc": "Export the certificate to a file", + "l_certificate_exported": "Certificate exported", + "l_import_file": "Import file", + "l_import_desc": "Import a key and/or certificate", + "l_delete_certificate": "Delete certificate", + "l_delete_certificate_desc": "Remove the certificate from your YubiKey", + "l_subject_issuer": "Subject: {subject}, Issuer: {issuer}", + "@l_subject_issuer" : { + "placeholders": { + "subject": {}, + "issuer": {} + } + }, + "l_serial": "Serial: {serial}", + "@l_serial" : { + "placeholders": { + "serial": {} + } + }, + "l_certificate_fingerprint": "Fingerprint: {fingerprint}", + "@l_certificate_fingerprint" : { + "placeholders": { + "fingerprint": {} + } + }, + "l_valid": "Valid: {not_before} - {not_after}", + "@l_valid" : { + "placeholders": { + "not_before": {}, + "not_after": {} + } + }, + "l_no_certificate": "No certificate loaded", + "l_key_no_certificate": "Key without certificate loaded", + "s_generate_key": "Generate key", + "l_generate_desc": "Generate a new certificate or CSR", + "p_generate_desc": "This will generate a new key on the YubiKey in PIV slot {slot}. The public key will be embedded into a self-signed certificate stored on the YubiKey, or in a certificate signing request (CSR) saved to file.", + "@p_generate_desc" : { + "placeholders": { + "slot": {} + } + }, + "p_warning_delete_certificate": "Warning! This action will delete the certificate from your YubiKey.", + "q_delete_certificate_confirm": "Delete the certficate in PIV slot {slot}?", + "@q_delete_certificate_confirm" : { + "placeholders": { + "slot": {} + } + }, + "l_certificate_deleted": "Certificate deleted", + "p_password_protected_file": "The selected file is password protected. Enter the password to proceed.", + "p_import_items_desc": "The following items will be imported into PIV slot {slot}.", + "@p_import_items_desc" : { + "placeholders": { + "slot": {} + } + }, + + "@_piv_slots": {}, + "s_slot_display_name": "{name} ({hexid})", + "@s_slot_display_name" : { + "placeholders": { + "name": {}, + "hexid": {} + } + }, + "s_slot_9a": "Authentication", + "s_slot_9c": "Digital Signature", + "s_slot_9d": "Key Management", + "s_slot_9e": "Card Authentication", + "@_permissions": {}, "s_enable_nfc": "Enable NFC", "s_permission_denied": "Permission denied", @@ -391,10 +515,14 @@ "message": {} } }, + "s_reset_piv": "Reset PIV", + "l_piv_app_reset": "PIV application reset", "p_warning_factory_reset": "Warning! This will irrevocably delete all OATH TOTP/HOTP accounts from your YubiKey.", "p_warning_disable_credentials": "Your OATH credentials, as well as any password set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.", "p_warning_deletes_accounts": "Warning! This will irrevocably delete all U2F and FIDO2 accounts from your YubiKey.", "p_warning_disable_accounts": "Your credentials, as well as any PIN set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.", + "p_warning_piv_reset": "Warning! All data stored for PIV will be irrevocably deleted from your YubiKey.", + "p_warning_piv_reset_desc": "This includes private keys and certificates. Your PIN, PUK, and management key will be reset to their factory detault values.", "@_copy_to_clipboard": {}, "l_copy_to_clipboard": "Copy to clipboard", diff --git a/lib/piv/models.dart b/lib/piv/models.dart index 4a77206a..458dedb0 100644 --- a/lib/piv/models.dart +++ b/lib/piv/models.dart @@ -33,8 +33,8 @@ enum GenerateType { String getDisplayName(AppLocalizations l10n) { return switch (this) { - // TODO: - _ => name + GenerateType.certificate => l10n.s_certificate, + GenerateType.csr => l10n.s_csr, }; } } @@ -48,10 +48,15 @@ enum SlotId { final int id; const SlotId(this.id); + String get hexId => id.toRadixString(16).padLeft(2, '0'); + String getDisplayName(AppLocalizations l10n) { + String nameFor(String name) => l10n.s_slot_display_name(name, hexId); return switch (this) { - // TODO: - _ => name + SlotId.authentication => nameFor(l10n.s_slot_9a), + SlotId.signature => nameFor(l10n.s_slot_9c), + SlotId.keyManagement => nameFor(l10n.s_slot_9d), + SlotId.cardAuth => nameFor(l10n.s_slot_9e), }; } @@ -122,37 +127,31 @@ enum KeyType { String getDisplayName(AppLocalizations l10n) { return switch (this) { - // TODO: - _ => name + // TODO: Should these be translatable? + _ => name.toUpperCase() }; } } enum ManagementKeyType { @JsonValue(0x03) - tdes, + tdes(24), @JsonValue(0x08) - aes128, + aes128(16), @JsonValue(0x0A) - aes192, + aes192(24), @JsonValue(0x0C) - aes256; + aes256(32); - const ManagementKeyType(); + const ManagementKeyType(this.keyLength); + final int keyLength; int get value => _$ManagementKeyTypeEnumMap[this]!; - int get keyLength => switch (this) { - ManagementKeyType.tdes => 24, - ManagementKeyType.aes128 => 16, - ManagementKeyType.aes192 => 24, - ManagementKeyType.aes256 => 32, - }; - String getDisplayName(AppLocalizations l10n) { return switch (this) { - // TODO: - _ => name + // TODO: Should these be translatable? + _ => name.toUpperCase() }; } } diff --git a/lib/piv/views/actions.dart b/lib/piv/views/actions.dart index f6aa53c0..b56372e1 100644 --- a/lib/piv/views/actions.dart +++ b/lib/piv/views/actions.dart @@ -19,11 +19,12 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:yubico_authenticator/app/models.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../app/message.dart'; import '../../app/shortcuts.dart'; import '../../app/state.dart'; +import '../../app/models.dart'; import '../models.dart'; import '../state.dart'; import 'authentication_dialog.dart'; @@ -107,6 +108,7 @@ Widget registerPivActions( } return await withContext((context) async { + final l10n = AppLocalizations.of(context)!; final PivGenerateResult? result = await showBlurDialog( context: context, builder: (context) => GenerateKeyDialog( @@ -119,7 +121,7 @@ Widget registerPivActions( switch (result?.generateType) { case GenerateType.csr: final filePath = await FilePicker.platform.saveFile( - dialogTitle: 'Save CSR to file', + dialogTitle: l10n.l_export_csr_file, allowedExtensions: ['csr'], type: FileType.custom, lockParentWindow: true, @@ -141,17 +143,23 @@ Widget registerPivActions( return false; } - final picked = await FilePicker.platform.pickFiles( - allowedExtensions: ['pem', 'der', 'pfx', 'p12', 'key', 'crt'], - type: FileType.custom, - allowMultiple: false, - lockParentWindow: true, - dialogTitle: 'Select file to import'); + final withContext = ref.read(withContextProvider); + + final picked = await withContext( + (context) async { + final l10n = AppLocalizations.of(context)!; + return await FilePicker.platform.pickFiles( + allowedExtensions: ['pem', 'der', 'pfx', 'p12', 'key', 'crt'], + type: FileType.custom, + allowMultiple: false, + lockParentWindow: true, + dialogTitle: l10n.l_select_import_file); + }, + ); if (picked == null || picked.files.isEmpty) { return false; } - final withContext = ref.read(withContextProvider); return await withContext((context) async => await showBlurDialog( context: context, @@ -173,12 +181,18 @@ Widget registerPivActions( return false; } - final filePath = await FilePicker.platform.saveFile( - dialogTitle: 'Export certificate to file', - allowedExtensions: ['pem'], - type: FileType.custom, - lockParentWindow: true, - ); + final withContext = ref.read(withContextProvider); + + final filePath = await withContext((context) async { + final l10n = AppLocalizations.of(context)!; + return await FilePicker.platform.saveFile( + dialogTitle: l10n.l_export_certificate_file, + allowedExtensions: ['pem'], + type: FileType.custom, + lockParentWindow: true, + ); + }); + if (filePath == null) { return false; } @@ -186,8 +200,9 @@ Widget registerPivActions( final file = File(filePath); await file.writeAsString(cert, flush: true); - await ref.read(withContextProvider)((context) async { - showMessage(context, 'Certificate exported'); + await withContext((context) async { + final l10n = AppLocalizations.of(context)!; + showMessage(context, l10n.l_certificate_exported); }); return true; }), @@ -214,7 +229,7 @@ Widget registerPivActions( }); } return deleted; - }), //TODO + }), ...actions, }, child: Builder(builder: builder), diff --git a/lib/piv/views/authentication_dialog.dart b/lib/piv/views/authentication_dialog.dart index aabd5f36..1874443c 100644 --- a/lib/piv/views/authentication_dialog.dart +++ b/lib/piv/views/authentication_dialog.dart @@ -43,7 +43,7 @@ class _AuthenticationDialogState extends ConsumerState { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return ResponsiveDialog( - title: Text("Unlock management functions"), + title: Text(l10n.l_unlock_piv_management), actions: [ TextButton( key: keys.unlockButton, @@ -77,6 +77,7 @@ class _AuthenticationDialogState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(l10n.p_unlock_piv_management_desc), TextField( autofocus: true, obscureText: true, @@ -84,9 +85,9 @@ class _AuthenticationDialogState extends ConsumerState { key: keys.managementKeyField, decoration: InputDecoration( border: const OutlineInputBorder(), - labelText: "Management key", + labelText: l10n.s_management_key, prefixIcon: const Icon(Icons.key_outlined), - errorText: _keyIsWrong ? l10n.s_wrong_password : null, + errorText: _keyIsWrong ? l10n.l_wrong_key : null, errorMaxLines: 3), textInputAction: TextInputAction.next, onChanged: (value) { diff --git a/lib/piv/views/delete_certificate_dialog.dart b/lib/piv/views/delete_certificate_dialog.dart index 57f908b1..cc239ac3 100644 --- a/lib/piv/views/delete_certificate_dialog.dart +++ b/lib/piv/views/delete_certificate_dialog.dart @@ -36,7 +36,7 @@ class DeleteCertificateDialog extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context)!; return ResponsiveDialog( - title: Text(l10n.s_delete_account), + title: Text(l10n.l_delete_certificate), actions: [ TextButton( key: keys.deleteButton, @@ -48,7 +48,7 @@ class DeleteCertificateDialog extends ConsumerWidget { await ref.read(withContextProvider)( (context) async { Navigator.of(context).pop(true); - showMessage(context, l10n.s_account_deleted); + showMessage(context, l10n.l_certificate_deleted); }, ); } on CancellationException catch (_) { @@ -63,13 +63,9 @@ class DeleteCertificateDialog extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.p_warning_delete_account), - Text( - l10n.p_warning_disable_credential, - style: Theme.of(context).textTheme.bodyLarge, - ), - Text(// TODO - 'Delete certificate in ${pivSlot.slot.getDisplayName(l10n)} (Slot ${pivSlot.slot.id.toRadixString(16).padLeft(2, '0')})?'), + Text(l10n.p_warning_delete_certificate), + Text(l10n.q_delete_certificate_confirm( + pivSlot.slot.getDisplayName(l10n))), ] .map((e) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart index ca83dbc8..d86abcb6 100644 --- a/lib/piv/views/generate_key_dialog.dart +++ b/lib/piv/views/generate_key_dialog.dart @@ -63,7 +63,7 @@ class _GenerateKeyDialogState extends ConsumerState { final l10n = AppLocalizations.of(context)!; final navigator = Navigator.of(context); return ResponsiveDialog( - title: Text("Generate key"), + title: Text(l10n.s_generate_key), actions: [ TextButton( key: keys.saveButton, @@ -97,9 +97,9 @@ class _GenerateKeyDialogState extends ConsumerState { TextField( autofocus: true, key: keys.subjectField, - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: "Subject", + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_subject, ), textInputAction: TextInputAction.next, onChanged: (value) { diff --git a/lib/piv/views/import_file_dialog.dart b/lib/piv/views/import_file_dialog.dart index d85bb991..1af99ef9 100644 --- a/lib/piv/views/import_file_dialog.dart +++ b/lib/piv/views/import_file_dialog.dart @@ -80,7 +80,7 @@ class _ImportFileDialogState extends ConsumerState { final state = _state; if (state == null) { return ResponsiveDialog( - title: Text("Import file"), + title: Text(l10n.l_import_file), actions: [ TextButton( key: keys.unlockButton, @@ -98,7 +98,7 @@ class _ImportFileDialogState extends ConsumerState { return state.when( invalidPassword: () => ResponsiveDialog( - title: Text("Import file"), + title: Text(l10n.l_import_file), actions: [ TextButton( key: keys.unlockButton, @@ -111,6 +111,7 @@ class _ImportFileDialogState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(l10n.p_password_protected_file), TextField( autofocus: true, obscureText: true, @@ -118,7 +119,7 @@ class _ImportFileDialogState extends ConsumerState { key: keys.managementKeyField, decoration: InputDecoration( border: const OutlineInputBorder(), - labelText: "Password", + labelText: l10n.s_password, prefixIcon: const Icon(Icons.password_outlined), errorText: _passwordIsWrong ? l10n.s_wrong_password : null, errorMaxLines: 3), @@ -141,7 +142,7 @@ class _ImportFileDialogState extends ConsumerState { ), ), result: (_, privateKey, certificates) => ResponsiveDialog( - title: Text("Import file"), + title: Text(l10n.l_import_file), actions: [ TextButton( key: keys.unlockButton, @@ -160,7 +161,7 @@ class _ImportFileDialogState extends ConsumerState { }); } }, - child: Text(l10n.s_save), + child: Text(l10n.s_import), ), ], child: Padding( @@ -168,10 +169,10 @@ class _ImportFileDialogState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Import the following into slot ${widget.pivSlot.slot.getDisplayName(l10n)}?"), - if (privateKey) Text("- Private key"), - if (certificates > 0) Text("- Certificate"), + Text(l10n.p_import_items_desc( + widget.pivSlot.slot.getDisplayName(l10n))), + if (privateKey) Text(l10n.l_bullet(l10n.s_private_key)), + if (certificates > 0) Text(l10n.l_bullet(l10n.s_certificate)), ] .map((e) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), diff --git a/lib/piv/views/key_actions.dart b/lib/piv/views/key_actions.dart index 68b833a8..5327b744 100644 --- a/lib/piv/views/key_actions.dart +++ b/lib/piv/views/key_actions.dart @@ -38,6 +38,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, pivState.metadata?.managementKeyMetadata.defaultValue == true; final pinBlocked = pivState.pinAttempts == 0; + final pukAttempts = pivState.metadata?.pukMetadata.attemptsRemaining; return FsDialog( child: Column( @@ -48,8 +49,8 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, key: keys.managePinAction, title: Text(l10n.s_pin), subtitle: Text(pinBlocked - ? 'Blocked, use PUK to reset' - : '${pivState.pinAttempts} attempts remaining'), + ? l10n.l_piv_pin_blocked + : l10n.l_attempts_remaining(pivState.pinAttempts)), leading: CircleAvatar( foregroundColor: theme.onSecondary, backgroundColor: theme.secondary, @@ -67,9 +68,10 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, }), ListTile( key: keys.managePukAction, - title: Text('PUK'), // TODO - subtitle: Text( - '${pivState.metadata?.pukMetadata.attemptsRemaining ?? '?'} attempts remaining'), + title: Text(l10n.s_puk), + subtitle: pukAttempts != null + ? Text(l10n.l_attempts_remaining(pukAttempts)) + : null, leading: CircleAvatar( foregroundColor: theme.onSecondary, backgroundColor: theme.secondary, @@ -85,12 +87,12 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, }), ListTile( key: keys.manageManagementKeyAction, - title: Text('Management Key'), // TODO + title: Text(l10n.s_management_key), subtitle: Text(usingDefaultMgmtKey - ? 'Warning: Default key used' + ? l10n.l_warning_default_key : (pivState.protectedKey - ? 'PIN can be used instead' - : 'Change your management key')), + ? l10n.l_pin_protected_key + : l10n.l_change_management_key)), leading: CircleAvatar( foregroundColor: theme.onSecondary, backgroundColor: theme.secondary, @@ -107,7 +109,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, }), ListTile( key: keys.resetAction, - title: Text('Reset PIV'), //TODO + title: Text(l10n.s_reset_piv), subtitle: Text(l10n.l_factory_reset_this_app), leading: CircleAvatar( foregroundColor: theme.onError, @@ -121,20 +123,25 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, builder: (context) => ResetDialog(devicePath), ); }), - ListTitle(l10n.s_setup, - textStyle: Theme.of(context).textTheme.bodyLarge), - ListTile( - key: keys.setupMacOsAction, - title: Text('Setup for macOS'), //TODO - subtitle: Text('Create certificates for macOS login'), //TODO - leading: CircleAvatar( - backgroundColor: theme.secondary, - foregroundColor: theme.onSecondary, - child: const Icon(Icons.laptop), - ), - onTap: () async { - Navigator.of(context).pop(); - }), + // TODO + /* + if (false == true) ...[ + ListTitle(l10n.s_setup, + textStyle: Theme.of(context).textTheme.bodyLarge), + ListTile( + key: keys.setupMacOsAction, + title: Text('Setup for macOS'), + subtitle: Text('Create certificates for macOS login'), + leading: CircleAvatar( + backgroundColor: theme.secondary, + foregroundColor: theme.onSecondary, + child: const Icon(Icons.laptop), + ), + onTap: () async { + Navigator.of(context).pop(); + }), + ], + */ ], ), ); diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index faf12dac..348d3875 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -100,11 +100,12 @@ class _ManageKeyDialogState extends ConsumerState { } } - print("Set new key: $_newKey"); await notifier.setManagementKey(_newKey, managementKeyType: _keyType, storeKey: _storeKey); if (!mounted) return; - showMessage(context, "Management key changed"); + + final l10n = AppLocalizations.of(context)!; + showMessage(context, l10n.l_management_key_changed); Navigator.of(context).pop(); } @@ -116,7 +117,7 @@ class _ManageKeyDialogState extends ConsumerState { final hexLength = _keyType.keyLength * 2; return ResponsiveDialog( - title: Text('Change Management Key'), + title: Text(l10n.l_change_management_key), actions: [ TextButton( onPressed: _submit, @@ -129,7 +130,7 @@ class _ManageKeyDialogState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.p_enter_current_password_or_reset), + Text(l10n.p_change_management_key_desc), if (widget.pivState.protectedKey) TextField( autofocus: true, @@ -138,10 +139,11 @@ class _ManageKeyDialogState extends ConsumerState { key: keys.managementKeyField, decoration: InputDecoration( border: const OutlineInputBorder(), - labelText: "PIN", + labelText: l10n.s_pin, prefixIcon: const Icon(Icons.pin_outlined), errorText: _currentIsWrong - ? "Wrong PIN ($_attemptsRemaining attempts left)" + ? l10n + .l_wrong_pin_attempts_remaining(_attemptsRemaining) : null, errorMaxLines: 3), textInputAction: TextInputAction.next, @@ -164,12 +166,11 @@ class _ManageKeyDialogState extends ConsumerState { : null, decoration: InputDecoration( border: const OutlineInputBorder(), - labelText: 'Current management key', + labelText: l10n.s_current_management_key, prefixIcon: const Icon(Icons.password_outlined), - errorText: _currentIsWrong ? 'Wrong key' : null, + errorText: _currentIsWrong ? l10n.l_wrong_key : null, errorMaxLines: 3, - helperText: - _defaultKeyUsed ? "Default management key used" : null, + helperText: _defaultKeyUsed ? l10n.l_default_key_used : null, ), textInputAction: TextInputAction.next, onChanged: (value) { @@ -179,7 +180,6 @@ class _ManageKeyDialogState extends ConsumerState { }); }, ), - Text("Enter your new management key."), TextField( key: keys.newPinPukField, autofocus: _defaultKeyUsed, @@ -191,7 +191,7 @@ class _ManageKeyDialogState extends ConsumerState { ], decoration: InputDecoration( border: const OutlineInputBorder(), - labelText: "New management key", + labelText: l10n.s_new_management_key, prefixIcon: const Icon(Icons.password_outlined), enabled: _currentKeyOrPin.isNotEmpty, ), @@ -225,7 +225,7 @@ class _ManageKeyDialogState extends ConsumerState { }, ), FilterChip( - label: Text("Protect with PIN"), + label: Text(l10n.s_protect_key), selected: _storeKey, onSelected: (value) { setState(() { diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart index eb7f455c..6a5c6299 100644 --- a/lib/piv/views/manage_pin_puk_dialog.dart +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -83,13 +83,13 @@ class _ManagePinPukDialogState extends ConsumerState { final String titleText; switch (widget.target) { case ManageTarget.pin: - titleText = "Change PIN"; + titleText = l10n.s_change_pin; break; case ManageTarget.puk: - titleText = l10n.s_manage_password; + titleText = l10n.s_change_puk; break; case ManageTarget.unblock: - titleText = "Unblock PIN"; + titleText = l10n.s_unblock_pin; break; } @@ -116,8 +116,8 @@ class _ManagePinPukDialogState extends ConsumerState { decoration: InputDecoration( border: const OutlineInputBorder(), labelText: widget.target == ManageTarget.pin - ? 'Current PIN' - : 'Current PUK', + ? l10n.s_current_pin + : l10n.s_current_puk, prefixIcon: const Icon(Icons.password_outlined), errorText: _currentIsWrong ? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining) @@ -131,8 +131,8 @@ class _ManagePinPukDialogState extends ConsumerState { }); }, ), - Text( - "Enter your new ${widget.target == ManageTarget.puk ? 'PUK' : 'PIN'}. Must be 6-8 characters."), + Text(l10n.p_enter_new_piv_pin_puk( + widget.target == ManageTarget.puk ? l10n.s_puk : l10n.s_pin)), TextField( key: keys.newPinPukField, obscureText: true, @@ -140,7 +140,7 @@ class _ManagePinPukDialogState extends ConsumerState { decoration: InputDecoration( border: const OutlineInputBorder(), labelText: widget.target == ManageTarget.puk - ? "New PUK" + ? l10n.s_new_puk : l10n.s_new_pin, prefixIcon: const Icon(Icons.password_outlined), enabled: _currentPin.isNotEmpty, diff --git a/lib/piv/views/pin_dialog.dart b/lib/piv/views/pin_dialog.dart index 84485f8f..62238233 100644 --- a/lib/piv/views/pin_dialog.dart +++ b/lib/piv/views/pin_dialog.dart @@ -63,7 +63,7 @@ class _PinDialogState extends ConsumerState { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return ResponsiveDialog( - title: Text("PIN required"), + title: Text(l10n.s_pin_required), actions: [ TextButton( key: keys.unlockButton, @@ -76,6 +76,7 @@ class _PinDialogState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(l10n.p_pin_required_desc), TextField( autofocus: true, obscureText: true, @@ -83,10 +84,10 @@ class _PinDialogState extends ConsumerState { key: keys.managementKeyField, decoration: InputDecoration( border: const OutlineInputBorder(), - labelText: "PIN", + labelText: l10n.s_pin, prefixIcon: const Icon(Icons.pin_outlined), errorText: _pinIsWrong - ? "Wrong PIN ($_attemptsRemaining attempts left)" + ? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining) : null, errorMaxLines: 3), textInputAction: TextInputAction.next, diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart index 39550804..51dff16a 100644 --- a/lib/piv/views/piv_screen.dart +++ b/lib/piv/views/piv_screen.dart @@ -39,18 +39,18 @@ class PivScreen extends ConsumerWidget { final l10n = AppLocalizations.of(context)!; return ref.watch(pivStateProvider(devicePath)).when( loading: () => MessagePage( - title: Text(l10n.s_authenticator), + title: Text(l10n.s_piv), graphic: const CircularProgressIndicator(), delayedContent: true, ), error: (error, _) => AppFailurePage( - title: Text(l10n.s_authenticator), + title: Text(l10n.s_piv), cause: error, ), data: (pivState) { final pivSlots = ref.watch(pivSlotsProvider(devicePath)).asData; return AppPage( - title: const Text('PIV'), + title: Text(l10n.s_piv), keyActionsBuilder: (context) => pivBuildActions(context, devicePath, pivState, ref), child: Column( @@ -95,19 +95,19 @@ class _CertificateListItem extends StatelessWidget { child: const Icon(Icons.approval), ), title: Text( - '${slot.getDisplayName(l10n)} (Slot ${slot.id.toRadixString(16).padLeft(2, '0')})', + slot.getDisplayName(l10n), softWrap: false, overflow: TextOverflow.fade, ), subtitle: certInfo != null ? Text( - 'Subject: ${certInfo.subject}, Issuer: ${certInfo.issuer}', + l10n.l_subject_issuer(certInfo.subject, certInfo.issuer), softWrap: false, overflow: TextOverflow.fade, ) : Text(pivSlot.hasKey == true - ? 'Key without certificate loaded' - : 'No certificate loaded'), + ? l10n.l_key_no_certificate + : l10n.l_no_certificate), trailing: OutlinedButton( onPressed: () { Actions.maybeInvoke(context, const OpenIntent()); diff --git a/lib/piv/views/reset_dialog.dart b/lib/piv/views/reset_dialog.dart index 8bac4186..50b0c12f 100644 --- a/lib/piv/views/reset_dialog.dart +++ b/lib/piv/views/reset_dialog.dart @@ -39,7 +39,7 @@ class ResetDialog extends ConsumerWidget { await ref.read(pivStateProvider(devicePath).notifier).reset(); await ref.read(withContextProvider)((context) async { Navigator.of(context).pop(); - showMessage(context, l10n.l_oath_application_reset); //TODO + showMessage(context, l10n.l_piv_app_reset); }); }, child: Text(l10n.s_reset), @@ -50,10 +50,10 @@ class ResetDialog extends ConsumerWidget { child: Column( children: [ Text( - l10n.p_warning_factory_reset, // TODO + l10n.p_warning_piv_reset, style: const TextStyle(fontWeight: FontWeight.bold), ), - Text(l10n.p_warning_disable_credentials), //TODO + Text(l10n.p_warning_piv_reset_desc), ] .map((e) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), diff --git a/lib/piv/views/slot_dialog.dart b/lib/piv/views/slot_dialog.dart index 9e04683d..9909ba32 100644 --- a/lib/piv/views/slot_dialog.dart +++ b/lib/piv/views/slot_dialog.dart @@ -53,14 +53,15 @@ class SlotDialog extends ConsumerWidget { child: Column( children: [ Text( - '${pivSlot.getDisplayName(l10n)} (Slot ${pivSlot.id.toRadixString(16).padLeft(2, '0')})', + pivSlot.getDisplayName(l10n), style: textTheme.headlineSmall, softWrap: true, textAlign: TextAlign.center, ), if (certInfo != null) ...[ Text( - 'Subject: ${certInfo.subject}, Issuer: ${certInfo.issuer}', + l10n.l_subject_issuer( + certInfo.subject, certInfo.issuer), softWrap: true, textAlign: TextAlign.center, // This is what ListTile uses for subtitle @@ -69,7 +70,7 @@ class SlotDialog extends ConsumerWidget { ), ), Text( - 'Serial: ${certInfo.serial}', + l10n.l_serial(certInfo.serial), softWrap: true, textAlign: TextAlign.center, // This is what ListTile uses for subtitle @@ -78,7 +79,7 @@ class SlotDialog extends ConsumerWidget { ), ), Text( - 'Fingerprint: ${certInfo.fingerprint}', + l10n.l_certificate_fingerprint(certInfo.fingerprint), softWrap: true, textAlign: TextAlign.center, // This is what ListTile uses for subtitle @@ -87,7 +88,8 @@ class SlotDialog extends ConsumerWidget { ), ), Text( - 'Not before: ${certInfo.notValidBefore}, Not after: ${certInfo.notValidAfter}', + l10n.l_valid( + certInfo.notValidBefore, certInfo.notValidAfter), softWrap: true, textAlign: TextAlign.center, // This is what ListTile uses for subtitle @@ -99,7 +101,7 @@ class SlotDialog extends ConsumerWidget { Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Text( - 'No certificate loaded', + l10n.l_no_certificate, softWrap: true, textAlign: TextAlign.center, // This is what ListTile uses for subtitle @@ -113,8 +115,7 @@ class SlotDialog extends ConsumerWidget { ], ), ), - ListTitle(AppLocalizations.of(context)!.s_actions, - textStyle: textTheme.bodyLarge), + ListTitle(l10n.s_actions, textStyle: textTheme.bodyLarge), _SlotDialogActions(certInfo), ], ), @@ -141,8 +142,8 @@ class _SlotDialogActions extends StatelessWidget { foregroundColor: theme.onPrimary, child: const Icon(Icons.add_outlined), ), - title: Text('Generate key'), - subtitle: Text('Generate a new certificate or CSR'), + title: Text(l10n.s_generate_key), + subtitle: Text(l10n.l_generate_desc), onTap: () { Actions.invoke(context, const GenerateIntent()); }, @@ -153,8 +154,8 @@ class _SlotDialogActions extends StatelessWidget { foregroundColor: theme.onSecondary, child: const Icon(Icons.file_download_outlined), ), - title: Text('Import file'), - subtitle: Text('Import a key and/or certificate from file'), + title: Text(l10n.l_import_file), + subtitle: Text(l10n.l_import_desc), onTap: () { Actions.invoke(context, const ImportIntent()); }, @@ -166,8 +167,8 @@ class _SlotDialogActions extends StatelessWidget { foregroundColor: theme.onSecondary, child: const Icon(Icons.file_upload_outlined), ), - title: Text('Export certificate'), - subtitle: Text('Export the certificate to file'), + title: Text(l10n.l_export_certificate), + subtitle: Text(l10n.l_export_certificate_desc), onTap: () { Actions.invoke(context, const ExportIntent()); }, @@ -178,8 +179,8 @@ class _SlotDialogActions extends StatelessWidget { foregroundColor: theme.onError, child: const Icon(Icons.delete_outline), ), - title: Text('Delete certificate'), - subtitle: Text('Remove the certificate from the YubiKey'), + title: Text(l10n.l_delete_certificate), + subtitle: Text(l10n.l_delete_certificate_desc), onTap: () { Actions.invoke(context, const DeleteIntent()); }, From 3f821497cd2685f143c7cbb4d8c9e6129366be0f Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 8 Jun 2023 15:39:09 +0200 Subject: [PATCH 024/158] More debug logging. --- helper/helper/piv.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helper/helper/piv.py b/helper/helper/piv.py index df01dab7..a6ae5511 100644 --- a/helper/helper/piv.py +++ b/helper/helper/piv.py @@ -324,6 +324,7 @@ class SlotsNode(RpcNode): certificates=len(certs), ) except InvalidPasswordError: + logger.debug("Invalid or missing password", exc_info=True) return dict(status=False) @@ -362,7 +363,7 @@ class SlotNode(RpcNode): try: private_key, certs = _parse_file(data, password) except InvalidPasswordError: - logger.debug("InvalidPassword", exc_info=True) + logger.debug("Invalid or missing password", exc_info=True) raise ValueError("Wrong/Missing password") # Exception? From 4bd322a26851de3d6be9cdca64360f51efd1b47e Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 9 Jun 2023 14:46:16 +0200 Subject: [PATCH 025/158] Extract ActionList classes. --- lib/app/views/action_list.dart | 89 ++++++++++++ lib/fido/views/credential_dialog.dart | 48 +++---- lib/fido/views/fingerprint_dialog.dart | 68 ++++----- lib/fido/views/key_actions.dart | 128 +++++++++-------- lib/oath/views/account_dialog.dart | 18 +-- lib/oath/views/key_actions.dart | 178 +++++++++++------------ lib/piv/views/key_actions.dart | 186 ++++++++++++------------- lib/piv/views/slot_dialog.dart | 115 ++++++--------- 8 files changed, 425 insertions(+), 405 deletions(-) create mode 100644 lib/app/views/action_list.dart diff --git a/lib/app/views/action_list.dart b/lib/app/views/action_list.dart new file mode 100644 index 00000000..8cc1d607 --- /dev/null +++ b/lib/app/views/action_list.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:flutter/material.dart'; + +import '../../widgets/list_title.dart'; + +class ActionListItem extends StatelessWidget { + final String title; + final String? subtitle; + final Widget? leading; + final Widget? icon; + final Color? foregroundColor; + final Color? backgroundColor; + final Widget? trailing; + final void Function()? onTap; + + const ActionListItem({ + super.key, + required this.title, + this.subtitle, + this.leading, + this.icon, + this.foregroundColor, + this.backgroundColor, + this.trailing, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + // Either leading is defined only, or we need at least an icon. + assert((leading != null && + (icon == null && + foregroundColor == null && + backgroundColor == null)) || + (leading == null && icon != null)); + + final theme = + ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; + + return ListTile( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), + title: Text(title), + subtitle: subtitle != null ? Text(subtitle!) : null, + leading: leading ?? + CircleAvatar( + foregroundColor: foregroundColor ?? theme.onSecondary, + backgroundColor: backgroundColor ?? theme.secondary, + child: icon, + ), + trailing: trailing, + onTap: onTap, + enabled: onTap != null, + ); + } +} + +class ActionListSection extends StatelessWidget { + final String title; + final List children; + + const ActionListSection(this.title, {super.key, required this.children}); + + @override + Widget build(BuildContext context) => SizedBox( + width: 360, + child: Column(children: [ + ListTitle( + title, + textStyle: Theme.of(context).textTheme.bodyLarge, + ), + ...children, + ]), + ); +} diff --git a/lib/fido/views/credential_dialog.dart b/lib/fido/views/credential_dialog.dart index db9b78ea..a2e7538f 100644 --- a/lib/fido/views/credential_dialog.dart +++ b/lib/fido/views/credential_dialog.dart @@ -6,7 +6,7 @@ import '../../app/message.dart'; import '../../app/shortcuts.dart'; import '../../app/state.dart'; import '../../app/views/fs_dialog.dart'; -import '../../widgets/list_title.dart'; +import '../../app/views/action_list.dart'; import '../models.dart'; import 'delete_credential_dialog.dart'; @@ -24,6 +24,9 @@ class CredentialDialog extends ConsumerWidget { return const SizedBox(); } + final l10n = AppLocalizations.of(context)!; + final theme = + ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; return Actions( actions: { DeleteIntent: CallbackAction(onInvoke: (_) async { @@ -77,9 +80,21 @@ class CredentialDialog extends ConsumerWidget { ], ), ), - ListTitle(AppLocalizations.of(context)!.s_actions, - textStyle: Theme.of(context).textTheme.bodyLarge), - _CredentialDialogActions(), + ActionListSection( + l10n.s_actions, + children: [ + ActionListItem( + backgroundColor: theme.error, + foregroundColor: theme.onError, + icon: const Icon(Icons.delete), + title: l10n.s_delete_passkey, + subtitle: l10n.l_delete_account_desc, + onTap: () { + Actions.invoke(context, const DeleteIntent()); + }, + ), + ], + ), ], ), ), @@ -87,28 +102,3 @@ class CredentialDialog extends ConsumerWidget { ); } } - -class _CredentialDialogActions extends StatelessWidget { - @override - Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; - final theme = - ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; - return Column( - children: [ - ListTile( - leading: CircleAvatar( - backgroundColor: theme.error, - foregroundColor: theme.onError, - child: const Icon(Icons.delete), - ), - title: Text(l10n.s_delete_passkey), - subtitle: Text(l10n.l_delete_account_desc), - onTap: () { - Actions.invoke(context, const DeleteIntent()); - }, - ), - ], - ); - } -} diff --git a/lib/fido/views/fingerprint_dialog.dart b/lib/fido/views/fingerprint_dialog.dart index 86af6f3c..c5efeffa 100644 --- a/lib/fido/views/fingerprint_dialog.dart +++ b/lib/fido/views/fingerprint_dialog.dart @@ -6,7 +6,7 @@ import '../../app/message.dart'; import '../../app/shortcuts.dart'; import '../../app/state.dart'; import '../../app/views/fs_dialog.dart'; -import '../../widgets/list_title.dart'; +import '../../app/views/action_list.dart'; import '../models.dart'; import 'delete_fingerprint_dialog.dart'; import 'rename_fingerprint_dialog.dart'; @@ -25,6 +25,9 @@ class FingerprintDialog extends ConsumerWidget { return const SizedBox(); } + final l10n = AppLocalizations.of(context)!; + final theme = + ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; return Actions( actions: { EditIntent: CallbackAction(onInvoke: (_) async { @@ -93,9 +96,29 @@ class FingerprintDialog extends ConsumerWidget { ], ), ), - ListTitle(AppLocalizations.of(context)!.s_actions, - textStyle: Theme.of(context).textTheme.bodyLarge), - _FingerprintDialogActions(), + ActionListSection( + l10n.s_actions, + children: [ + ActionListItem( + icon: const Icon(Icons.edit), + title: l10n.s_rename_fp, + subtitle: l10n.l_rename_fp_desc, + onTap: () { + Actions.invoke(context, const EditIntent()); + }, + ), + ActionListItem( + backgroundColor: theme.error, + foregroundColor: theme.onError, + icon: const Icon(Icons.delete), + title: l10n.s_delete_fingerprint, + subtitle: l10n.l_delete_fingerprint_desc, + onTap: () { + Actions.invoke(context, const DeleteIntent()); + }, + ), + ], + ), ], ), ), @@ -103,40 +126,3 @@ class FingerprintDialog extends ConsumerWidget { ); } } - -class _FingerprintDialogActions extends StatelessWidget { - @override - Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; - final theme = - ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; - return Column( - children: [ - ListTile( - leading: CircleAvatar( - backgroundColor: theme.secondary, - foregroundColor: theme.onSecondary, - child: const Icon(Icons.edit), - ), - title: Text(l10n.s_rename_fp), - subtitle: Text(l10n.l_rename_fp_desc), - onTap: () { - Actions.invoke(context, const EditIntent()); - }, - ), - ListTile( - leading: CircleAvatar( - backgroundColor: theme.error, - foregroundColor: theme.onError, - child: const Icon(Icons.delete), - ), - title: Text(l10n.s_delete_fingerprint), - subtitle: Text(l10n.l_delete_fingerprint_desc), - onTap: () { - Actions.invoke(context, const DeleteIntent()); - }, - ), - ], - ); - } -} diff --git a/lib/fido/views/key_actions.dart b/lib/fido/views/key_actions.dart index 38c6d3d2..8a276553 100755 --- a/lib/fido/views/key_actions.dart +++ b/lib/fido/views/key_actions.dart @@ -20,7 +20,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/views/fs_dialog.dart'; -import '../../widgets/list_title.dart'; +import '../../app/views/action_list.dart'; import '../models.dart'; import 'add_fingerprint_dialog.dart'; import 'pin_dialog.dart'; @@ -39,73 +39,69 @@ Widget fidoBuildActions( return FsDialog( child: Column( children: [ - if (state.bioEnroll != null) ...[ - ListTitle(l10n.s_setup, - textStyle: Theme.of(context).textTheme.bodyLarge), - ListTile( - leading: CircleAvatar( - backgroundColor: theme.primary, - foregroundColor: theme.onPrimary, - child: const Icon(Icons.fingerprint_outlined), - ), - title: Text(l10n.s_add_fingerprint), - subtitle: state.unlocked - ? Text(l10n.l_fingerprints_used(fingerprints)) - : Text(state.hasPin - ? l10n.l_unlock_pin_first - : l10n.l_set_pin_first), - trailing: - fingerprints == 0 ? const Icon(Icons.warning_amber) : null, - enabled: state.unlocked && fingerprints < 5, - onTap: state.unlocked && fingerprints < 5 - ? () { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => AddFingerprintDialog(node.path), - ); - } - : null, + if (state.bioEnroll != null) + ActionListSection( + l10n.s_setup, + children: [ + ActionListItem( + backgroundColor: theme.primary, + foregroundColor: theme.onPrimary, + icon: const Icon(Icons.fingerprint_outlined), + title: l10n.s_add_fingerprint, + subtitle: state.unlocked + ? l10n.l_fingerprints_used(fingerprints) + : state.hasPin + ? l10n.l_unlock_pin_first + : l10n.l_set_pin_first, + trailing: + fingerprints == 0 ? const Icon(Icons.warning_amber) : null, + onTap: state.unlocked && fingerprints < 5 + ? () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => AddFingerprintDialog(node.path), + ); + } + : null, + ), + ], ), - ], - ListTitle(l10n.s_manage, - textStyle: Theme.of(context).textTheme.bodyLarge), - ListTile( - leading: CircleAvatar( - backgroundColor: theme.secondary, - foregroundColor: theme.onSecondary, - child: const Icon(Icons.pin_outlined), + ActionListSection( + l10n.s_manage, + children: [ + ActionListItem( + icon: const Icon(Icons.pin_outlined), + title: state.hasPin ? l10n.s_change_pin : l10n.s_set_pin, + subtitle: state.hasPin + ? l10n.s_fido_pin_protection + : l10n.l_fido_pin_protection_optional, + trailing: state.alwaysUv && !state.hasPin + ? const Icon(Icons.warning_amber) + : null, + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => FidoPinDialog(node.path, state), + ); + }), + ActionListItem( + foregroundColor: theme.onError, + backgroundColor: theme.error, + icon: const Icon(Icons.delete_outline), + title: l10n.s_reset_fido, + subtitle: l10n.l_factory_reset_this_app, + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ResetDialog(node), + ); + }, ), - title: Text(state.hasPin ? l10n.s_change_pin : l10n.s_set_pin), - subtitle: Text(state.hasPin - ? l10n.s_fido_pin_protection - : l10n.l_fido_pin_protection_optional), - trailing: state.alwaysUv && !state.hasPin - ? const Icon(Icons.warning_amber) - : null, - onTap: () { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => FidoPinDialog(node.path, state), - ); - }), - ListTile( - leading: CircleAvatar( - foregroundColor: theme.onError, - backgroundColor: theme.error, - child: const Icon(Icons.delete_outline), - ), - title: Text(l10n.s_reset_fido), - subtitle: Text(l10n.l_factory_reset_this_app), - onTap: () { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => ResetDialog(node), - ); - }, - ), + ], + ) ], ), ); diff --git a/lib/oath/views/account_dialog.dart b/lib/oath/views/account_dialog.dart index d656a0b3..03b80aad 100755 --- a/lib/oath/views/account_dialog.dart +++ b/lib/oath/views/account_dialog.dart @@ -24,9 +24,9 @@ import '../../app/message.dart'; import '../../app/shortcuts.dart'; import '../../app/state.dart'; import '../../app/views/fs_dialog.dart'; +import '../../app/views/action_list.dart'; import '../../core/models.dart'; import '../../core/state.dart'; -import '../../widgets/list_title.dart'; import '../models.dart'; import '../state.dart'; import 'account_helper.dart'; @@ -39,7 +39,8 @@ class AccountDialog extends ConsumerWidget { const AccountDialog(this.credential, {super.key}); - List _buildActions(BuildContext context, AccountHelper helper) { + List _buildActions( + BuildContext context, AccountHelper helper) { final l10n = AppLocalizations.of(context)!; final actions = helper.buildActions(); @@ -66,7 +67,7 @@ class AccountDialog extends ConsumerWidget { final intent = e.intent; final (firstColor, secondColor) = colors[e] ?? (theme.secondary, theme.onSecondary); - return ListTile( + return ActionListItem( leading: CircleAvatar( backgroundColor: intent != null ? firstColor : theme.secondary.withOpacity(0.2), @@ -74,8 +75,8 @@ class AccountDialog extends ConsumerWidget { //disabledBackgroundColor: theme.onSecondary.withOpacity(0.2), child: e.icon, ), - title: Text(e.text), - subtitle: e.trailing != null ? Text(e.trailing!) : null, + title: e.text, + subtitle: e.trailing, onTap: intent != null ? () { Actions.invoke(context, intent); @@ -200,9 +201,10 @@ class AccountDialog extends ConsumerWidget { ), ), const SizedBox(height: 32), - ListTitle(AppLocalizations.of(context)!.s_actions, - textStyle: Theme.of(context).textTheme.bodyLarge), - ..._buildActions(context, helper), + ActionListSection( + AppLocalizations.of(context)!.s_actions, + children: _buildActions(context, helper), + ), ], ), ), diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index a06703cb..a3a00e18 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -23,9 +23,9 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; import '../../app/views/fs_dialog.dart'; +import '../../app/views/action_list.dart'; import '../../core/state.dart'; import '../../exception/cancellation_exception.dart'; -import '../../widgets/list_title.dart'; import '../models.dart'; import '../state.dart'; import '../keys.dart' as keys; @@ -47,108 +47,98 @@ Widget oathBuildActions( return FsDialog( child: Column( children: [ - ListTitle(l10n.s_setup, - textStyle: Theme.of(context).textTheme.bodyLarge), - ListTile( - title: Text(l10n.s_add_account), - key: keys.addAccountAction, - leading: CircleAvatar( + ActionListSection(l10n.s_setup, children: [ + ActionListItem( + key: keys.addAccountAction, + title: l10n.s_add_account, backgroundColor: theme.primary, foregroundColor: theme.onPrimary, - child: const Icon(Icons.person_add_alt_1_outlined), - ), - subtitle: Text(used == null - ? l10n.l_unlock_first - : (capacity != null ? l10n.l_accounts_used(used, capacity) : '')), - enabled: used != null && (capacity == null || capacity > used), - onTap: used != null && (capacity == null || capacity > used) - ? () async { - final credentials = ref.read(credentialsProvider); - final withContext = ref.read(withContextProvider); - Navigator.of(context).pop(); - CredentialData? otpauth; - if (isAndroid) { - final scanner = ref.read(qrScannerProvider); - if (scanner != null) { - try { - final url = await scanner.scanQr(); - if (url != null) { - otpauth = CredentialData.fromUri(Uri.parse(url)); + icon: const Icon(Icons.person_add_alt_1_outlined), + subtitle: used == null + ? l10n.l_unlock_first + : (capacity != null + ? l10n.l_accounts_used(used, capacity) + : ''), + onTap: used != null && (capacity == null || capacity > used) + ? () async { + final credentials = ref.read(credentialsProvider); + final withContext = ref.read(withContextProvider); + Navigator.of(context).pop(); + CredentialData? otpauth; + if (isAndroid) { + final scanner = ref.read(qrScannerProvider); + if (scanner != null) { + try { + final url = await scanner.scanQr(); + if (url != null) { + otpauth = CredentialData.fromUri(Uri.parse(url)); + } + } on CancellationException catch (_) { + // ignored - user cancelled + return; } - } on CancellationException catch (_) { - // ignored - user cancelled - return; } } + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => OathAddAccountPage( + devicePath, + oathState, + credentials: credentials, + credentialData: otpauth, + ), + ); + }); } - await withContext((context) async { - await showBlurDialog( + : null, + ), + ]), + ActionListSection(l10n.s_manage, children: [ + ActionListItem( + key: keys.customIconsAction, + title: l10n.s_custom_icons, + subtitle: l10n.l_set_icons_for_accounts, + icon: const Icon(Icons.image_outlined), + onTap: () async { + Navigator.of(context).pop(); + await ref.read(withContextProvider)((context) => showBlurDialog( context: context, - builder: (context) => OathAddAccountPage( - devicePath, - oathState, - credentials: credentials, - credentialData: otpauth, - ), - ); - }); - } - : null, - ), - ListTitle(l10n.s_manage, - textStyle: Theme.of(context).textTheme.bodyLarge), - ListTile( - key: keys.customIconsAction, - title: Text(l10n.s_custom_icons), - subtitle: Text(l10n.l_set_icons_for_accounts), - leading: CircleAvatar( - backgroundColor: theme.secondary, - foregroundColor: theme.onSecondary, - child: const Icon(Icons.image_outlined), - ), - onTap: () async { - Navigator.of(context).pop(); - await ref.read(withContextProvider)((context) => showBlurDialog( - context: context, - routeSettings: - const RouteSettings(name: 'oath_icon_pack_dialog'), - builder: (context) => const IconPackDialog(), - )); - }), - ListTile( - key: keys.setOrManagePasswordAction, - title: Text(oathState.hasKey - ? l10n.s_manage_password - : l10n.s_set_password), - subtitle: Text(l10n.l_optional_password_protection), - leading: CircleAvatar( - backgroundColor: theme.secondary, - foregroundColor: theme.onSecondary, - child: const Icon(Icons.password_outlined)), - onTap: () { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => - ManagePasswordDialog(devicePath, oathState), - ); - }), - ListTile( - key: keys.resetAction, - title: Text(l10n.s_reset_oath), - subtitle: Text(l10n.l_factory_reset_this_app), - leading: CircleAvatar( + routeSettings: + const RouteSettings(name: 'oath_icon_pack_dialog'), + builder: (context) => const IconPackDialog(), + )); + }), + ActionListItem( + key: keys.setOrManagePasswordAction, + title: oathState.hasKey + ? l10n.s_manage_password + : l10n.s_set_password, + subtitle: l10n.l_optional_password_protection, + icon: const Icon(Icons.password_outlined), + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => + ManagePasswordDialog(devicePath, oathState), + ); + }), + ActionListItem( + key: keys.resetAction, + title: l10n.s_reset_oath, + subtitle: l10n.l_factory_reset_this_app, foregroundColor: theme.onError, backgroundColor: theme.error, - child: const Icon(Icons.delete_outline), - ), - onTap: () { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => ResetDialog(devicePath), - ); - }), + icon: const Icon(Icons.delete_outline), + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ResetDialog(devicePath), + ); + }), + ]), ], ), ); diff --git a/lib/piv/views/key_actions.dart b/lib/piv/views/key_actions.dart index 5327b744..427f3df1 100644 --- a/lib/piv/views/key_actions.dart +++ b/lib/piv/views/key_actions.dart @@ -21,7 +21,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/views/fs_dialog.dart'; -import '../../widgets/list_title.dart'; +import '../../app/views/action_list.dart'; import '../models.dart'; import '../keys.dart' as keys; import 'manage_key_dialog.dart'; @@ -43,105 +43,95 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, return FsDialog( child: Column( children: [ - ListTitle(l10n.s_manage, - textStyle: Theme.of(context).textTheme.bodyLarge), - ListTile( - key: keys.managePinAction, - title: Text(l10n.s_pin), - subtitle: Text(pinBlocked - ? l10n.l_piv_pin_blocked - : l10n.l_attempts_remaining(pivState.pinAttempts)), - leading: CircleAvatar( - foregroundColor: theme.onSecondary, - backgroundColor: theme.secondary, - child: const Icon(Icons.pin_outlined), - ), - onTap: () { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => ManagePinPukDialog( - devicePath, - target: pinBlocked ? ManageTarget.unblock : ManageTarget.pin, - ), - ); - }), - ListTile( - key: keys.managePukAction, - title: Text(l10n.s_puk), - subtitle: pukAttempts != null - ? Text(l10n.l_attempts_remaining(pukAttempts)) - : null, - leading: CircleAvatar( - foregroundColor: theme.onSecondary, - backgroundColor: theme.secondary, - child: const Icon(Icons.pin_outlined), - ), - onTap: () { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => - ManagePinPukDialog(devicePath, target: ManageTarget.puk), - ); - }), - ListTile( - key: keys.manageManagementKeyAction, - title: Text(l10n.s_management_key), - subtitle: Text(usingDefaultMgmtKey - ? l10n.l_warning_default_key - : (pivState.protectedKey - ? l10n.l_pin_protected_key - : l10n.l_change_management_key)), - leading: CircleAvatar( - foregroundColor: theme.onSecondary, - backgroundColor: theme.secondary, - child: const Icon(Icons.key_outlined), - ), - trailing: - usingDefaultMgmtKey ? const Icon(Icons.warning_amber) : null, - onTap: () { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => ManageKeyDialog(devicePath, pivState), - ); - }), - ListTile( - key: keys.resetAction, - title: Text(l10n.s_reset_piv), - subtitle: Text(l10n.l_factory_reset_this_app), - leading: CircleAvatar( - foregroundColor: theme.onError, - backgroundColor: theme.error, - child: const Icon(Icons.delete_outline), - ), - onTap: () { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => ResetDialog(devicePath), - ); - }), + ActionListSection( + l10n.s_manage, + children: [ + ActionListItem( + key: keys.managePinAction, + title: l10n.s_pin, + subtitle: pinBlocked + ? l10n.l_piv_pin_blocked + : l10n.l_attempts_remaining(pivState.pinAttempts), + icon: const Icon(Icons.pin_outlined), + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ManagePinPukDialog( + devicePath, + target: + pinBlocked ? ManageTarget.unblock : ManageTarget.pin, + ), + ); + }), + ActionListItem( + key: keys.managePukAction, + title: l10n.s_puk, + subtitle: pukAttempts != null + ? l10n.l_attempts_remaining(pukAttempts) + : null, + icon: const Icon(Icons.pin_outlined), + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ManagePinPukDialog(devicePath, + target: ManageTarget.puk), + ); + }), + ActionListItem( + key: keys.manageManagementKeyAction, + title: l10n.s_management_key, + subtitle: usingDefaultMgmtKey + ? l10n.l_warning_default_key + : (pivState.protectedKey + ? l10n.l_pin_protected_key + : l10n.l_change_management_key), + icon: const Icon(Icons.key_outlined), + trailing: usingDefaultMgmtKey + ? const Icon(Icons.warning_amber) + : null, + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ManageKeyDialog(devicePath, pivState), + ); + }), + ActionListItem( + key: keys.resetAction, + title: l10n.s_reset_piv, + subtitle: l10n.l_factory_reset_this_app, + foregroundColor: theme.onError, + backgroundColor: theme.error, + icon: const Icon(Icons.delete_outline), + onTap: () { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ResetDialog(devicePath), + ); + }) + ], + ), // TODO /* - if (false == true) ...[ - ListTitle(l10n.s_setup, - textStyle: Theme.of(context).textTheme.bodyLarge), - ListTile( - key: keys.setupMacOsAction, - title: Text('Setup for macOS'), - subtitle: Text('Create certificates for macOS login'), - leading: CircleAvatar( - backgroundColor: theme.secondary, - foregroundColor: theme.onSecondary, - child: const Icon(Icons.laptop), - ), - onTap: () async { - Navigator.of(context).pop(); - }), - ], - */ + if (false == true) ...[ + KeyActionTitle(l10n.s_setup), + KeyActionItem( + key: keys.setupMacOsAction, + title: Text('Setup for macOS'), + subtitle: Text('Create certificates for macOS login'), + leading: CircleAvatar( + backgroundColor: theme.secondary, + foregroundColor: theme.onSecondary, + child: const Icon(Icons.laptop), + ), + onTap: () async { + Navigator.of(context).pop(); + }), + ], + */ ], ), ); diff --git a/lib/piv/views/slot_dialog.dart b/lib/piv/views/slot_dialog.dart index 9909ba32..fa0cd0b5 100644 --- a/lib/piv/views/slot_dialog.dart +++ b/lib/piv/views/slot_dialog.dart @@ -5,7 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/shortcuts.dart'; import '../../app/state.dart'; import '../../app/views/fs_dialog.dart'; -import '../../widgets/list_title.dart'; +import '../../app/views/action_list.dart'; import '../models.dart'; import '../state.dart'; import 'actions.dart'; @@ -27,6 +27,8 @@ class SlotDialog extends ConsumerWidget { final l10n = AppLocalizations.of(context)!; final textTheme = Theme.of(context).textTheme; + final theme = + ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; final slotData = ref.watch(pivSlotsProvider(node.path).select((value) => value.whenOrNull( @@ -115,8 +117,49 @@ class SlotDialog extends ConsumerWidget { ], ), ), - ListTitle(l10n.s_actions, textStyle: textTheme.bodyLarge), - _SlotDialogActions(certInfo), + ActionListSection( + l10n.s_actions, + children: [ + ActionListItem( + backgroundColor: theme.primary, + foregroundColor: theme.onPrimary, + icon: const Icon(Icons.add_outlined), + title: l10n.s_generate_key, + subtitle: l10n.l_generate_desc, + onTap: () { + Actions.invoke(context, const GenerateIntent()); + }, + ), + ActionListItem( + icon: const Icon(Icons.file_download_outlined), + title: l10n.l_import_file, + subtitle: l10n.l_import_desc, + onTap: () { + Actions.invoke(context, const ImportIntent()); + }, + ), + if (certInfo != null) ...[ + ActionListItem( + icon: const Icon(Icons.file_upload_outlined), + title: l10n.l_export_certificate, + subtitle: l10n.l_export_certificate_desc, + onTap: () { + Actions.invoke(context, const ExportIntent()); + }, + ), + ActionListItem( + backgroundColor: theme.error, + foregroundColor: theme.onError, + icon: const Icon(Icons.delete_outline), + title: l10n.l_delete_certificate, + subtitle: l10n.l_delete_certificate_desc, + onTap: () { + Actions.invoke(context, const DeleteIntent()); + }, + ), + ], + ], + ), ], ), ), @@ -124,69 +167,3 @@ class SlotDialog extends ConsumerWidget { ); } } - -class _SlotDialogActions extends StatelessWidget { - final CertInfo? certInfo; - const _SlotDialogActions(this.certInfo); - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; - final theme = - ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; - return Column( - children: [ - ListTile( - leading: CircleAvatar( - backgroundColor: theme.primary, - foregroundColor: theme.onPrimary, - child: const Icon(Icons.add_outlined), - ), - title: Text(l10n.s_generate_key), - subtitle: Text(l10n.l_generate_desc), - onTap: () { - Actions.invoke(context, const GenerateIntent()); - }, - ), - ListTile( - leading: CircleAvatar( - backgroundColor: theme.secondary, - foregroundColor: theme.onSecondary, - child: const Icon(Icons.file_download_outlined), - ), - title: Text(l10n.l_import_file), - subtitle: Text(l10n.l_import_desc), - onTap: () { - Actions.invoke(context, const ImportIntent()); - }, - ), - if (certInfo != null) ...[ - ListTile( - leading: CircleAvatar( - backgroundColor: theme.secondary, - foregroundColor: theme.onSecondary, - child: const Icon(Icons.file_upload_outlined), - ), - title: Text(l10n.l_export_certificate), - subtitle: Text(l10n.l_export_certificate_desc), - onTap: () { - Actions.invoke(context, const ExportIntent()); - }, - ), - ListTile( - leading: CircleAvatar( - backgroundColor: theme.error, - foregroundColor: theme.onError, - child: const Icon(Icons.delete_outline), - ), - title: Text(l10n.l_delete_certificate), - subtitle: Text(l10n.l_delete_certificate_desc), - onTap: () { - Actions.invoke(context, const DeleteIntent()); - }, - ), - ], - ], - ); - } -} From 4525198f9bc43cb6e7eeba6d0acb04cbd24ede2d Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 12 Jun 2023 11:28:22 +0200 Subject: [PATCH 026/158] Refactor ActionItem avatar colors. --- lib/app/views/action_list.dart | 41 +++++++++++++------------- lib/fido/views/credential_dialog.dart | 5 +--- lib/fido/views/fingerprint_dialog.dart | 5 +--- lib/fido/views/key_actions.dart | 8 ++--- lib/oath/views/account_dialog.dart | 25 +++++----------- lib/oath/views/key_actions.dart | 13 ++++---- lib/piv/views/key_actions.dart | 7 ++--- lib/piv/views/slot_dialog.dart | 8 ++--- 8 files changed, 41 insertions(+), 71 deletions(-) diff --git a/lib/app/views/action_list.dart b/lib/app/views/action_list.dart index 8cc1d607..5affe9a2 100644 --- a/lib/app/views/action_list.dart +++ b/lib/app/views/action_list.dart @@ -18,50 +18,49 @@ import 'package:flutter/material.dart'; import '../../widgets/list_title.dart'; +enum ActionStyle { normal, primary, error } + class ActionListItem extends StatelessWidget { final String title; final String? subtitle; - final Widget? leading; - final Widget? icon; - final Color? foregroundColor; - final Color? backgroundColor; + final Widget icon; final Widget? trailing; + final ActionStyle actionStyle; final void Function()? onTap; const ActionListItem({ super.key, + required this.icon, required this.title, this.subtitle, - this.leading, - this.icon, - this.foregroundColor, - this.backgroundColor, this.trailing, this.onTap, + this.actionStyle = ActionStyle.normal, }); @override Widget build(BuildContext context) { - // Either leading is defined only, or we need at least an icon. - assert((leading != null && - (icon == null && - foregroundColor == null && - backgroundColor == null)) || - (leading == null && icon != null)); - final theme = ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; + final (foreground, background) = switch (actionStyle) { + ActionStyle.normal => (theme.onSecondary, theme.secondary), + ActionStyle.primary => (theme.onPrimary, theme.primary), + ActionStyle.error => (theme.onError, theme.error), + }; + return ListTile( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), title: Text(title), subtitle: subtitle != null ? Text(subtitle!) : null, - leading: leading ?? - CircleAvatar( - foregroundColor: foregroundColor ?? theme.onSecondary, - backgroundColor: backgroundColor ?? theme.secondary, - child: icon, - ), + leading: Opacity( + opacity: onTap != null ? 1.0 : 0.4, + child: CircleAvatar( + foregroundColor: foreground, + backgroundColor: background, + child: icon, + ), + ), trailing: trailing, onTap: onTap, enabled: onTap != null, diff --git a/lib/fido/views/credential_dialog.dart b/lib/fido/views/credential_dialog.dart index a2e7538f..e4f2addc 100644 --- a/lib/fido/views/credential_dialog.dart +++ b/lib/fido/views/credential_dialog.dart @@ -25,8 +25,6 @@ class CredentialDialog extends ConsumerWidget { } final l10n = AppLocalizations.of(context)!; - final theme = - ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; return Actions( actions: { DeleteIntent: CallbackAction(onInvoke: (_) async { @@ -84,8 +82,7 @@ class CredentialDialog extends ConsumerWidget { l10n.s_actions, children: [ ActionListItem( - backgroundColor: theme.error, - foregroundColor: theme.onError, + actionStyle: ActionStyle.error, icon: const Icon(Icons.delete), title: l10n.s_delete_passkey, subtitle: l10n.l_delete_account_desc, diff --git a/lib/fido/views/fingerprint_dialog.dart b/lib/fido/views/fingerprint_dialog.dart index c5efeffa..c32d1e90 100644 --- a/lib/fido/views/fingerprint_dialog.dart +++ b/lib/fido/views/fingerprint_dialog.dart @@ -26,8 +26,6 @@ class FingerprintDialog extends ConsumerWidget { } final l10n = AppLocalizations.of(context)!; - final theme = - ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; return Actions( actions: { EditIntent: CallbackAction(onInvoke: (_) async { @@ -108,8 +106,7 @@ class FingerprintDialog extends ConsumerWidget { }, ), ActionListItem( - backgroundColor: theme.error, - foregroundColor: theme.onError, + actionStyle: ActionStyle.error, icon: const Icon(Icons.delete), title: l10n.s_delete_fingerprint, subtitle: l10n.l_delete_fingerprint_desc, diff --git a/lib/fido/views/key_actions.dart b/lib/fido/views/key_actions.dart index 8a276553..4aaff140 100755 --- a/lib/fido/views/key_actions.dart +++ b/lib/fido/views/key_actions.dart @@ -33,8 +33,6 @@ bool fidoShowActionsNotifier(FidoState state) { Widget fidoBuildActions( BuildContext context, DeviceNode node, FidoState state, int fingerprints) { final l10n = AppLocalizations.of(context)!; - final theme = - ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; return FsDialog( child: Column( @@ -44,8 +42,7 @@ Widget fidoBuildActions( l10n.s_setup, children: [ ActionListItem( - backgroundColor: theme.primary, - foregroundColor: theme.onPrimary, + actionStyle: ActionStyle.primary, icon: const Icon(Icons.fingerprint_outlined), title: l10n.s_add_fingerprint, subtitle: state.unlocked @@ -87,8 +84,7 @@ Widget fidoBuildActions( ); }), ActionListItem( - foregroundColor: theme.onError, - backgroundColor: theme.error, + actionStyle: ActionStyle.error, icon: const Icon(Icons.delete_outline), title: l10n.s_reset_fido, subtitle: l10n.l_factory_reset_this_app, diff --git a/lib/oath/views/account_dialog.dart b/lib/oath/views/account_dialog.dart index 03b80aad..ef9d41d0 100755 --- a/lib/oath/views/account_dialog.dart +++ b/lib/oath/views/account_dialog.dart @@ -44,37 +44,28 @@ class AccountDialog extends ConsumerWidget { final l10n = AppLocalizations.of(context)!; final actions = helper.buildActions(); - final theme = - ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; - final copy = actions.firstWhere(((e) => e.text == l10n.l_copy_to_clipboard)); final delete = actions.firstWhere(((e) => e.text == l10n.s_delete_account)); - final colors = { - copy: (theme.primary, theme.onPrimary), - delete: (theme.error, theme.onError), + final canCopy = copy.intent != null; + final actionStyles = { + copy: canCopy ? ActionStyle.primary : ActionStyle.normal, + delete: ActionStyle.error, }; // If we can't copy, but can calculate, highlight that button instead - if (copy.intent == null) { + if (!canCopy) { final calculates = actions.where(((e) => e.text == l10n.s_calculate)); if (calculates.isNotEmpty) { - colors[calculates.first] = (theme.primary, theme.onPrimary); + actionStyles[calculates.first] = ActionStyle.primary; } } return actions.map((e) { final intent = e.intent; - final (firstColor, secondColor) = - colors[e] ?? (theme.secondary, theme.onSecondary); return ActionListItem( - leading: CircleAvatar( - backgroundColor: - intent != null ? firstColor : theme.secondary.withOpacity(0.2), - foregroundColor: secondColor, - //disabledBackgroundColor: theme.onSecondary.withOpacity(0.2), - child: e.icon, - ), + actionStyle: actionStyles[e] ?? ActionStyle.normal, + icon: e.icon, title: e.text, subtitle: e.trailing, onTap: intent != null diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index a3a00e18..25da1e04 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -42,18 +42,16 @@ Widget oathBuildActions( }) { final l10n = AppLocalizations.of(context)!; final capacity = oathState.version.isAtLeast(4) ? 32 : null; - final theme = - ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; + return FsDialog( child: Column( children: [ ActionListSection(l10n.s_setup, children: [ ActionListItem( key: keys.addAccountAction, - title: l10n.s_add_account, - backgroundColor: theme.primary, - foregroundColor: theme.onPrimary, + actionStyle: ActionStyle.primary, icon: const Icon(Icons.person_add_alt_1_outlined), + title: l10n.s_add_account, subtitle: used == null ? l10n.l_unlock_first : (capacity != null @@ -126,11 +124,10 @@ Widget oathBuildActions( }), ActionListItem( key: keys.resetAction, + icon: const Icon(Icons.delete_outline), + actionStyle: ActionStyle.error, title: l10n.s_reset_oath, subtitle: l10n.l_factory_reset_this_app, - foregroundColor: theme.onError, - backgroundColor: theme.error, - icon: const Icon(Icons.delete_outline), onTap: () { Navigator.of(context).pop(); showBlurDialog( diff --git a/lib/piv/views/key_actions.dart b/lib/piv/views/key_actions.dart index 427f3df1..69de0d18 100644 --- a/lib/piv/views/key_actions.dart +++ b/lib/piv/views/key_actions.dart @@ -31,8 +31,6 @@ import 'reset_dialog.dart'; Widget pivBuildActions(BuildContext context, DevicePath devicePath, PivState pivState, WidgetRef ref) { final l10n = AppLocalizations.of(context)!; - final theme = - ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; final usingDefaultMgmtKey = pivState.metadata?.managementKeyMetadata.defaultValue == true; @@ -100,11 +98,10 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, }), ActionListItem( key: keys.resetAction, + icon: const Icon(Icons.delete_outline), + actionStyle: ActionStyle.error, title: l10n.s_reset_piv, subtitle: l10n.l_factory_reset_this_app, - foregroundColor: theme.onError, - backgroundColor: theme.error, - icon: const Icon(Icons.delete_outline), onTap: () { Navigator.of(context).pop(); showBlurDialog( diff --git a/lib/piv/views/slot_dialog.dart b/lib/piv/views/slot_dialog.dart index fa0cd0b5..432ceffb 100644 --- a/lib/piv/views/slot_dialog.dart +++ b/lib/piv/views/slot_dialog.dart @@ -27,8 +27,6 @@ class SlotDialog extends ConsumerWidget { final l10n = AppLocalizations.of(context)!; final textTheme = Theme.of(context).textTheme; - final theme = - ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; final slotData = ref.watch(pivSlotsProvider(node.path).select((value) => value.whenOrNull( @@ -121,9 +119,8 @@ class SlotDialog extends ConsumerWidget { l10n.s_actions, children: [ ActionListItem( - backgroundColor: theme.primary, - foregroundColor: theme.onPrimary, icon: const Icon(Icons.add_outlined), + actionStyle: ActionStyle.primary, title: l10n.s_generate_key, subtitle: l10n.l_generate_desc, onTap: () { @@ -148,8 +145,7 @@ class SlotDialog extends ConsumerWidget { }, ), ActionListItem( - backgroundColor: theme.error, - foregroundColor: theme.onError, + actionStyle: ActionStyle.error, icon: const Icon(Icons.delete_outline), title: l10n.l_delete_certificate, subtitle: l10n.l_delete_certificate_desc, From 25c728b145af2aa38c395c89babb8284b53d1694 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 13 Jun 2023 14:33:23 +0200 Subject: [PATCH 027/158] Various fixes. --- lib/app/message.dart | 39 +++++++------ lib/desktop/fido/state.dart | 14 ++--- lib/desktop/piv/state.dart | 12 +++- lib/l10n/app_en.arb | 4 +- lib/piv/views/actions.dart | 3 +- lib/piv/views/authentication_dialog.dart | 72 ++++++++++++++---------- lib/piv/views/generate_key_dialog.dart | 2 +- lib/piv/views/manage_key_dialog.dart | 66 ++++++++++++++++------ lib/piv/views/manage_pin_puk_dialog.dart | 57 +++++++++---------- lib/piv/views/pin_dialog.dart | 3 +- lib/piv/views/piv_screen.dart | 3 +- lib/piv/views/slot_dialog.dart | 6 +- 12 files changed, 167 insertions(+), 114 deletions(-) diff --git a/lib/app/message.dart b/lib/app/message.dart index 11a23478..45e1c771 100755 --- a/lib/app/message.dart +++ b/lib/app/message.dart @@ -63,20 +63,27 @@ Future showBlurDialog({ required BuildContext context, required Widget Function(BuildContext) builder, RouteSettings? routeSettings, -}) => - showGeneralDialog( - context: context, - barrierDismissible: true, - barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, - pageBuilder: (ctx, anim1, anim2) => builder(ctx), - transitionDuration: const Duration(milliseconds: 150), - transitionBuilder: (ctx, anim1, anim2, child) => BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 20 * anim1.value, sigmaY: 20 * anim1.value), - child: FadeTransition( - opacity: anim1, - child: child, - ), +}) async { + const transitionDelay = Duration(milliseconds: 150); + final result = await showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, + pageBuilder: (ctx, anim1, anim2) => builder(ctx), + transitionDuration: transitionDelay, + transitionBuilder: (ctx, anim1, anim2, child) => BackdropFilter( + filter: + ImageFilter.blur(sigmaX: 20 * anim1.value, sigmaY: 20 * anim1.value), + child: FadeTransition( + opacity: anim1, + child: child, ), - routeSettings: routeSettings, - ); + ), + routeSettings: routeSettings, + ); + // Make sure we wait for the dialog to fade out before returning the result. + // This is needed for subsequent dialogs with autofocus. + await Future.delayed(transitionDelay); + + return result; +} diff --git a/lib/desktop/fido/state.dart b/lib/desktop/fido/state.dart index 4f5f3a9c..bd28cdcb 100755 --- a/lib/desktop/fido/state.dart +++ b/lib/desktop/fido/state.dart @@ -224,15 +224,11 @@ class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier { final controller = StreamController(); final signaler = Signaler(); signaler.signals.listen((signal) { - switch (signal.status) { - case 'capture': - controller.sink - .add(FingerprintEvent.capture(signal.body['remaining'])); - break; - case 'capture-error': - controller.sink.add(FingerprintEvent.error(signal.body['code'])); - break; - } + controller.sink.add(switch (signal.status) { + 'capture' => FingerprintEvent.capture(signal.body['remaining']), + 'capture-error' => FingerprintEvent.error(signal.body['code']), + final other => throw UnimplementedError(other), + }); }); controller.onCancel = () { diff --git a/lib/desktop/piv/state.dart b/lib/desktop/piv/state.dart index b588d929..c5f86b57 100644 --- a/lib/desktop/piv/state.dart +++ b/lib/desktop/piv/state.dart @@ -21,7 +21,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:logging/logging.dart'; -import 'package:yubico_authenticator/desktop/models.dart'; import '../../app/logging.dart'; import '../../app/models.dart'; @@ -30,6 +29,7 @@ import '../../app/views/user_interaction.dart'; import '../../core/models.dart'; import '../../piv/models.dart'; import '../../piv/state.dart'; +import '../models.dart'; import '../rpc.dart'; import '../state.dart'; @@ -70,7 +70,7 @@ class _DesktopPivStateNotifier extends PivStateNotifier { ..setErrorHandler('state-reset', (_) async { ref.invalidate(_sessionProvider(devicePath)); }) - ..setErrorHandler('auth-required', (_) async { + ..setErrorHandler('auth-required', (e) async { final String? mgmtKey; if (state.valueOrNull?.metadata?.managementKeyMetadata.defaultValue == true) { @@ -83,7 +83,12 @@ class _DesktopPivStateNotifier extends PivStateNotifier { ref.invalidateSelf(); } else { ref.read(_managementKeyProvider(devicePath).notifier).state = null; + ref.invalidateSelf(); + throw e; } + } else { + ref.invalidateSelf(); + throw e; } }); ref.onDispose(() { @@ -103,6 +108,7 @@ class _DesktopPivStateNotifier extends PivStateNotifier { @override Future reset() async { await _session.command('reset'); + ref.read(_managementKeyProvider(_devicePath).notifier).state = null; ref.invalidate(_sessionProvider(_session.devicePath)); } @@ -246,6 +252,8 @@ class _DesktopPivStateNotifier extends PivStateNotifier { 'store_key': storeKey, }, ); + ref.read(_managementKeyProvider(_devicePath).notifier).state = + managementKey; ref.invalidateSelf(); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 380f4412..6fde5d07 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -193,6 +193,7 @@ } }, "s_pin_set": "PIN set", + "s_puk_set": "PUK set", "l_set_pin_failed": "Failed to set PIN: {message}", "@l_set_pin_failed" : { "placeholders": { @@ -219,7 +220,8 @@ "l_set_pin_first": "A PIN is required first", "l_unlock_pin_first": "Unlock with PIN first", "l_pin_soft_locked": "PIN has been blocked until the YubiKey is removed and reinserted", - "p_enter_current_pin_or_reset": "Enter your current PIN. If you don't know your PIN, you'll need to reset the YubiKey.", + "p_enter_current_pin_or_reset": "Enter your current PIN. If you don't know your PIN, you'll need to unblock it with the PUK or reset the YubiKey.", + "p_enter_current_puk_or_reset": "Enter your current PUK. If you don't know your PUK, you'll need to reset the YubiKey.", "p_enter_new_fido2_pin": "Enter your new PIN. A PIN must be at least {length} characters long and may contain letters, numbers and special characters.", "@p_enter_new_fido2_pin" : { "placeholders": { diff --git a/lib/piv/views/actions.dart b/lib/piv/views/actions.dart index b56372e1..b1ae3188 100644 --- a/lib/piv/views/actions.dart +++ b/lib/piv/views/actions.dart @@ -90,7 +90,8 @@ Widget registerPivActions( ), GenerateIntent: CallbackAction(onInvoke: (intent) async { - if (!await _authIfNeeded(ref, devicePath, pivState)) { + if (!pivState.protectedKey && + !await _authIfNeeded(ref, devicePath, pivState)) { return false; } diff --git a/lib/piv/views/authentication_dialog.dart b/lib/piv/views/authentication_dialog.dart index 1874443c..2ae4a45a 100644 --- a/lib/piv/views/authentication_dialog.dart +++ b/lib/piv/views/authentication_dialog.dart @@ -15,6 +15,7 @@ */ 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'; @@ -42,33 +43,39 @@ class _AuthenticationDialogState extends ConsumerState { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; + final keyLen = (widget.pivState.metadata?.managementKeyMetadata.keyType ?? + ManagementKeyType.tdes) + .keyLength * + 2; return ResponsiveDialog( title: Text(l10n.l_unlock_piv_management), actions: [ TextButton( key: keys.unlockButton, - onPressed: () async { - final navigator = Navigator.of(context); - try { - final status = await ref - .read(pivStateProvider(widget.devicePath).notifier) - .authenticate(_managementKey); - if (status) { - navigator.pop(true); - } else { - setState(() { - _keyIsWrong = true; - }); - } - } on CancellationException catch (_) { - navigator.pop(false); - } catch (_) { - // TODO: More error cases - setState(() { - _keyIsWrong = true; - }); - } - }, + onPressed: _managementKey.length == keyLen + ? () async { + final navigator = Navigator.of(context); + try { + final status = await ref + .read(pivStateProvider(widget.devicePath).notifier) + .authenticate(_managementKey); + if (status) { + navigator.pop(true); + } else { + setState(() { + _keyIsWrong = true; + }); + } + } on CancellationException catch (_) { + navigator.pop(false); + } catch (_) { + // TODO: More error cases + setState(() { + _keyIsWrong = true; + }); + } + } + : null, child: Text(l10n.s_unlock), ), ], @@ -79,16 +86,21 @@ class _AuthenticationDialogState extends ConsumerState { children: [ Text(l10n.p_unlock_piv_management_desc), TextField( - autofocus: true, - obscureText: true, - autofillHints: const [AutofillHints.password], key: keys.managementKeyField, + autofocus: true, + maxLength: keyLen, + autofillHints: const [AutofillHints.password], + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp('[a-f0-9]', caseSensitive: false)) + ], 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), + border: const OutlineInputBorder(), + labelText: l10n.s_management_key, + prefixIcon: const Icon(Icons.key_outlined), + errorText: _keyIsWrong ? l10n.l_wrong_key : null, + errorMaxLines: 3, + ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart index d86abcb6..3a79925f 100644 --- a/lib/piv/views/generate_key_dialog.dart +++ b/lib/piv/views/generate_key_dialog.dart @@ -104,7 +104,7 @@ class _GenerateKeyDialogState extends ConsumerState { textInputAction: TextInputAction.next, onChanged: (value) { setState(() { - _subject = value; + _subject = value.contains('=') ? value : 'CN=$value'; }); }, ), diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index 348d3875..64354f20 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -14,6 +14,8 @@ * 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'; @@ -46,8 +48,8 @@ class _ManageKeyDialogState extends ConsumerState { String _currentKeyOrPin = ''; bool _currentIsWrong = false; int _attemptsRemaining = -1; - String _newKey = ''; ManagementKeyType _keyType = ManagementKeyType.tdes; + final _keyController = TextEditingController(); @override void initState() { @@ -62,6 +64,12 @@ class _ManageKeyDialogState extends ConsumerState { _storeKey = _usesStoredKey; } + @override + void dispose() { + _keyController.dispose(); + super.dispose(); + } + _submit() async { final notifier = ref.read(pivStateProvider(widget.path).notifier); if (_usesStoredKey) { @@ -100,7 +108,7 @@ class _ManageKeyDialogState extends ConsumerState { } } - await notifier.setManagementKey(_newKey, + await notifier.setManagementKey(_keyController.text, managementKeyType: _keyType, storeKey: _storeKey); if (!mounted) return; @@ -113,8 +121,14 @@ class _ManageKeyDialogState extends ConsumerState { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final currentType = widget.pivState.metadata?.managementKeyMetadata.keyType; + final currentType = + widget.pivState.metadata?.managementKeyMetadata.keyType ?? + ManagementKeyType.tdes; final hexLength = _keyType.keyLength * 2; + final protected = widget.pivState.protectedKey; + final currentLenOk = protected + ? _currentKeyOrPin.length >= 4 + : _currentKeyOrPin.length == currentType.keyLength * 2; return ResponsiveDialog( title: Text(l10n.l_change_management_key), @@ -131,12 +145,13 @@ class _ManageKeyDialogState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(l10n.p_change_management_key_desc), - if (widget.pivState.protectedKey) + if (protected) TextField( autofocus: true, obscureText: true, autofillHints: const [AutofillHints.password], - key: keys.managementKeyField, + key: keys.pinPukField, + maxLength: 8, decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_pin, @@ -154,16 +169,14 @@ class _ManageKeyDialogState extends ConsumerState { }); }, ), - if (!widget.pivState.protectedKey) + if (!protected) TextFormField( - key: keys.pinPukField, + key: keys.managementKeyField, autofocus: !_defaultKeyUsed, autofillHints: const [AutofillHints.password], initialValue: _defaultKeyUsed ? defaultManagementKey : null, readOnly: _defaultKeyUsed, - maxLength: !_defaultKeyUsed && currentType != null - ? currentType.keyLength * 2 - : null, + maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null, decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_current_management_key, @@ -172,6 +185,10 @@ class _ManageKeyDialogState extends ConsumerState { errorMaxLines: 3, helperText: _defaultKeyUsed ? l10n.l_default_key_used : null, ), + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp('[a-f0-9]', caseSensitive: false)) + ], textInputAction: TextInputAction.next, onChanged: (value) { setState(() { @@ -185,6 +202,7 @@ class _ManageKeyDialogState extends ConsumerState { autofocus: _defaultKeyUsed, autofillHints: const [AutofillHints.newPassword], maxLength: hexLength, + controller: _keyController, inputFormatters: [ FilteringTextInputFormatter.allow( RegExp('[a-f0-9]', caseSensitive: false)) @@ -193,16 +211,28 @@ class _ManageKeyDialogState extends ConsumerState { border: const OutlineInputBorder(), labelText: l10n.s_new_management_key, prefixIcon: const Icon(Icons.password_outlined), - enabled: _currentKeyOrPin.isNotEmpty, + enabled: currentLenOk, + suffixIcon: IconButton( + icon: const Icon(Icons.refresh), + 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, + ), ), textInputAction: TextInputAction.next, - onChanged: (value) { - setState(() { - _newKey = value; - }); - }, onSubmitted: (_) { - if (_newKey.length == hexLength) { + if (_keyController.text.length == hexLength) { _submit(); } }, @@ -212,7 +242,7 @@ class _ManageKeyDialogState extends ConsumerState { spacing: 4.0, runSpacing: 8.0, children: [ - if (currentType != null) + if (widget.pivState.metadata != null) ChoiceFilterChip( items: ManagementKeyType.values, value: _keyType, diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart index 6a5c6299..f61b4024 100644 --- a/lib/piv/views/manage_pin_puk_dialog.dart +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -21,7 +21,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../widgets/responsive_dialog.dart'; -import '../models.dart'; import '../state.dart'; import '../keys.dart' as keys; @@ -38,7 +37,6 @@ class ManagePinPukDialog extends ConsumerStatefulWidget { _ManagePinPukDialogState(); } -//TODO: Use switch expressions in Dart 3 class _ManagePinPukDialogState extends ConsumerState { String _currentPin = ''; String _newPin = ''; @@ -48,23 +46,22 @@ class _ManagePinPukDialogState extends ConsumerState { _submit() async { final notifier = ref.read(pivStateProvider(widget.path).notifier); - final PinVerificationStatus result; - switch (widget.target) { - case ManageTarget.pin: - result = await notifier.changePin(_currentPin, _newPin); - break; - case ManageTarget.puk: - result = await notifier.changePuk(_currentPin, _newPin); - break; - case ManageTarget.unblock: - result = await notifier.unblockPin(_currentPin, _newPin); - break; - } + final result = await switch (widget.target) { + ManageTarget.pin => notifier.changePin(_currentPin, _newPin), + ManageTarget.puk => notifier.changePuk(_currentPin, _newPin), + ManageTarget.unblock => notifier.unblockPin(_currentPin, _newPin), + }; result.when(success: () { if (!mounted) return; + final l10n = AppLocalizations.of(context)!; Navigator.of(context).pop(); - showMessage(context, AppLocalizations.of(context)!.s_password_set); + showMessage( + context, + switch (widget.target) { + ManageTarget.puk => l10n.s_puk_set, + _ => l10n.s_pin_set, + }); }, failure: (attemptsRemaining) { setState(() { _attemptsRemaining = attemptsRemaining; @@ -80,18 +77,11 @@ class _ManagePinPukDialogState extends ConsumerState { final isValid = _newPin.isNotEmpty && _newPin == _confirmPin && _currentPin.isNotEmpty; - final String titleText; - switch (widget.target) { - case ManageTarget.pin: - titleText = l10n.s_change_pin; - break; - case ManageTarget.puk: - titleText = l10n.s_change_puk; - break; - case ManageTarget.unblock: - titleText = l10n.s_unblock_pin; - break; - } + final titleText = switch (widget.target) { + ManageTarget.pin => l10n.s_change_pin, + ManageTarget.puk => l10n.s_change_puk, + ManageTarget.unblock => l10n.s_unblock_pin, + }; return ResponsiveDialog( title: Text(titleText), @@ -107,10 +97,14 @@ class _ManagePinPukDialogState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.p_enter_current_password_or_reset), + //TODO fix string + Text(widget.target == ManageTarget.pin + ? l10n.p_enter_current_pin_or_reset + : l10n.p_enter_current_puk_or_reset), TextField( autofocus: true, obscureText: true, + maxLength: 8, autofillHints: const [AutofillHints.password], key: keys.pinPukField, decoration: InputDecoration( @@ -136,6 +130,7 @@ class _ManagePinPukDialogState extends ConsumerState { TextField( key: keys.newPinPukField, obscureText: true, + maxLength: 8, autofillHints: const [AutofillHints.newPassword], decoration: InputDecoration( border: const OutlineInputBorder(), @@ -143,7 +138,8 @@ class _ManagePinPukDialogState extends ConsumerState { ? l10n.s_new_puk : l10n.s_new_pin, prefixIcon: const Icon(Icons.password_outlined), - enabled: _currentPin.isNotEmpty, + // Old YubiKeys allowed a 4 digit PIN + enabled: _currentPin.length >= 4, ), textInputAction: TextInputAction.next, onChanged: (value) { @@ -160,12 +156,13 @@ class _ManagePinPukDialogState extends ConsumerState { TextField( key: keys.confirmPinPukField, obscureText: true, + maxLength: 8, autofillHints: const [AutofillHints.newPassword], decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_confirm_pin, prefixIcon: const Icon(Icons.password_outlined), - enabled: _currentPin.isNotEmpty && _newPin.isNotEmpty, + enabled: _currentPin.length >= 4 && _newPin.length >= 6, ), textInputAction: TextInputAction.done, onChanged: (value) { diff --git a/lib/piv/views/pin_dialog.dart b/lib/piv/views/pin_dialog.dart index 62238233..4a4f6084 100644 --- a/lib/piv/views/pin_dialog.dart +++ b/lib/piv/views/pin_dialog.dart @@ -67,7 +67,7 @@ class _PinDialogState extends ConsumerState { actions: [ TextButton( key: keys.unlockButton, - onPressed: _submit, + onPressed: _pin.length >= 4 ? _submit : null, child: Text(l10n.s_unlock), ), ], @@ -80,6 +80,7 @@ class _PinDialogState extends ConsumerState { TextField( autofocus: true, obscureText: true, + maxLength: 8, autofillHints: const [AutofillHints.password], key: keys.managementKeyField, decoration: InputDecoration( diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart index 51dff16a..f2f3fef1 100644 --- a/lib/piv/views/piv_screen.dart +++ b/lib/piv/views/piv_screen.dart @@ -62,8 +62,7 @@ class PivScreen extends ConsumerWidget { CallbackAction(onInvoke: (_) async { await showBlurDialog( context: context, - builder: (context) => - SlotDialog(pivState, e.slot), + builder: (context) => SlotDialog(e.slot), ); return null; }), diff --git a/lib/piv/views/slot_dialog.dart b/lib/piv/views/slot_dialog.dart index 432ceffb..80ce6640 100644 --- a/lib/piv/views/slot_dialog.dart +++ b/lib/piv/views/slot_dialog.dart @@ -11,9 +11,8 @@ import '../state.dart'; import 'actions.dart'; class SlotDialog extends ConsumerWidget { - final PivState pivState; final SlotId pivSlot; - const SlotDialog(this.pivState, this.pivSlot, {super.key}); + const SlotDialog(this.pivSlot, {super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -28,12 +27,13 @@ class SlotDialog extends ConsumerWidget { final l10n = AppLocalizations.of(context)!; final textTheme = Theme.of(context).textTheme; + final pivState = ref.watch(pivStateProvider(node.path)).valueOrNull; final slotData = ref.watch(pivSlotsProvider(node.path).select((value) => value.whenOrNull( data: (data) => data.firstWhere((element) => element.slot == pivSlot)))); - if (slotData == null) { + if (pivState == null || slotData == null) { return const FsDialog(child: CircularProgressIndicator()); } From 52bff18471ec39ce28c011904255ff6607181e6c Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 15 Jun 2023 17:39:17 +0200 Subject: [PATCH 028/158] Start refactoring actions. --- lib/app/message.dart | 31 ---- lib/app/models.dart | 16 +- lib/app/models.freezed.dart | 213 ++++++++++++++++++------- lib/app/views/action_list.dart | 31 +++- lib/app/views/action_popup_menu.dart | 66 ++++++++ lib/app/views/app_list_item.dart | 137 ++++++++++++++++ lib/fido/keys.dart | 45 ++++++ lib/fido/views/actions.dart | 55 +++++++ lib/fido/views/credential_dialog.dart | 16 +- lib/fido/views/fingerprint_dialog.dart | 24 +-- lib/fido/views/key_actions.dart | 10 +- lib/fido/views/unlocked_page.dart | 86 +++++----- lib/l10n/app_en.arb | 1 + lib/oath/keys.dart | 26 ++- lib/oath/views/account_dialog.dart | 43 +---- lib/oath/views/account_helper.dart | 46 ++++-- lib/oath/views/account_view.dart | 133 +++------------ lib/oath/views/key_actions.dart | 8 +- lib/piv/keys.dart | 19 ++- lib/piv/views/actions.dart | 57 ++++--- lib/piv/views/key_actions.dart | 8 +- lib/piv/views/piv_screen.dart | 38 ++--- lib/piv/views/slot_dialog.dart | 72 ++------- lib/widgets/menu_list_tile.dart | 47 ------ 24 files changed, 723 insertions(+), 505 deletions(-) create mode 100644 lib/app/views/action_popup_menu.dart create mode 100644 lib/app/views/app_list_item.dart create mode 100644 lib/fido/keys.dart create mode 100644 lib/fido/views/actions.dart delete mode 100755 lib/widgets/menu_list_tile.dart diff --git a/lib/app/message.dart b/lib/app/message.dart index 45e1c771..93b47748 100755 --- a/lib/app/message.dart +++ b/lib/app/message.dart @@ -20,7 +20,6 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import '../widgets/toast.dart'; -import 'models.dart'; void Function() showMessage( BuildContext context, @@ -29,36 +28,6 @@ void Function() showMessage( }) => showToast(context, message, duration: duration); -Future showBottomMenu( - BuildContext context, List actions) async { - await showBlurDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Options'), - contentPadding: const EdgeInsets.only(bottom: 24, top: 4), - content: Column( - mainAxisSize: MainAxisSize.min, - children: actions - .map((a) => ListTile( - leading: a.icon, - title: Text(a.text), - contentPadding: - const EdgeInsets.symmetric(horizontal: 24), - enabled: a.intent != null, - onTap: a.intent == null - ? null - : () { - Navigator.pop(context); - Actions.invoke(context, a.intent!); - }, - )) - .toList(), - ), - ); - }); -} - Future showBlurDialog({ required BuildContext context, required Widget Function(BuildContext) builder, diff --git a/lib/app/models.dart b/lib/app/models.dart index d23b1ac3..5599848f 100755 --- a/lib/app/models.dart +++ b/lib/app/models.dart @@ -116,14 +116,20 @@ class DeviceNode with _$DeviceNode { map(usbYubiKey: (_) => Transport.usb, nfcReader: (_) => Transport.nfc); } +enum ActionStyle { normal, primary, error } + @freezed -class MenuAction with _$MenuAction { - factory MenuAction({ - required String text, +class ActionItem with _$ActionItem { + factory ActionItem({ required Widget icon, - String? trailing, + required String title, + String? subtitle, + String? shortcut, + Widget? trailing, Intent? intent, - }) = _MenuAction; + ActionStyle? actionStyle, + Key? key, + }) = _ActionItem; } @freezed diff --git a/lib/app/models.freezed.dart b/lib/app/models.freezed.dart index 945299b5..d4479d69 100644 --- a/lib/app/models.freezed.dart +++ b/lib/app/models.freezed.dart @@ -624,30 +624,42 @@ abstract class NfcReaderNode extends DeviceNode { } /// @nodoc -mixin _$MenuAction { - String get text => throw _privateConstructorUsedError; +mixin _$ActionItem { Widget get icon => throw _privateConstructorUsedError; - String? get trailing => throw _privateConstructorUsedError; + String get title => throw _privateConstructorUsedError; + String? get subtitle => throw _privateConstructorUsedError; + String? get shortcut => throw _privateConstructorUsedError; + Widget? get trailing => throw _privateConstructorUsedError; Intent? get intent => throw _privateConstructorUsedError; + ActionStyle? get actionStyle => throw _privateConstructorUsedError; + Key? get key => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $MenuActionCopyWith get copyWith => + $ActionItemCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class $MenuActionCopyWith<$Res> { - factory $MenuActionCopyWith( - MenuAction value, $Res Function(MenuAction) then) = - _$MenuActionCopyWithImpl<$Res, MenuAction>; +abstract class $ActionItemCopyWith<$Res> { + factory $ActionItemCopyWith( + ActionItem value, $Res Function(ActionItem) then) = + _$ActionItemCopyWithImpl<$Res, ActionItem>; @useResult - $Res call({String text, Widget icon, String? trailing, Intent? intent}); + $Res call( + {Widget icon, + String title, + String? subtitle, + String? shortcut, + Widget? trailing, + Intent? intent, + ActionStyle? actionStyle, + Key? key}); } /// @nodoc -class _$MenuActionCopyWithImpl<$Res, $Val extends MenuAction> - implements $MenuActionCopyWith<$Res> { - _$MenuActionCopyWithImpl(this._value, this._then); +class _$ActionItemCopyWithImpl<$Res, $Val extends ActionItem> + implements $ActionItemCopyWith<$Res> { + _$ActionItemCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; @@ -657,140 +669,223 @@ class _$MenuActionCopyWithImpl<$Res, $Val extends MenuAction> @pragma('vm:prefer-inline') @override $Res call({ - Object? text = null, Object? icon = null, + Object? title = null, + Object? subtitle = freezed, + Object? shortcut = freezed, Object? trailing = freezed, Object? intent = freezed, + Object? actionStyle = freezed, + Object? key = freezed, }) { return _then(_value.copyWith( - text: null == text - ? _value.text - : text // ignore: cast_nullable_to_non_nullable - as String, icon: null == icon ? _value.icon : icon // ignore: cast_nullable_to_non_nullable as Widget, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + subtitle: freezed == subtitle + ? _value.subtitle + : subtitle // ignore: cast_nullable_to_non_nullable + as String?, + shortcut: freezed == shortcut + ? _value.shortcut + : shortcut // ignore: cast_nullable_to_non_nullable + as String?, trailing: freezed == trailing ? _value.trailing : trailing // ignore: cast_nullable_to_non_nullable - as String?, + as Widget?, intent: freezed == intent ? _value.intent : intent // ignore: cast_nullable_to_non_nullable as Intent?, + actionStyle: freezed == actionStyle + ? _value.actionStyle + : actionStyle // ignore: cast_nullable_to_non_nullable + as ActionStyle?, + key: freezed == key + ? _value.key + : key // ignore: cast_nullable_to_non_nullable + as Key?, ) as $Val); } } /// @nodoc -abstract class _$$_MenuActionCopyWith<$Res> - implements $MenuActionCopyWith<$Res> { - factory _$$_MenuActionCopyWith( - _$_MenuAction value, $Res Function(_$_MenuAction) then) = - __$$_MenuActionCopyWithImpl<$Res>; +abstract class _$$_ActionItemCopyWith<$Res> + implements $ActionItemCopyWith<$Res> { + factory _$$_ActionItemCopyWith( + _$_ActionItem value, $Res Function(_$_ActionItem) then) = + __$$_ActionItemCopyWithImpl<$Res>; @override @useResult - $Res call({String text, Widget icon, String? trailing, Intent? intent}); + $Res call( + {Widget icon, + String title, + String? subtitle, + String? shortcut, + Widget? trailing, + Intent? intent, + ActionStyle? actionStyle, + Key? key}); } /// @nodoc -class __$$_MenuActionCopyWithImpl<$Res> - extends _$MenuActionCopyWithImpl<$Res, _$_MenuAction> - implements _$$_MenuActionCopyWith<$Res> { - __$$_MenuActionCopyWithImpl( - _$_MenuAction _value, $Res Function(_$_MenuAction) _then) +class __$$_ActionItemCopyWithImpl<$Res> + extends _$ActionItemCopyWithImpl<$Res, _$_ActionItem> + implements _$$_ActionItemCopyWith<$Res> { + __$$_ActionItemCopyWithImpl( + _$_ActionItem _value, $Res Function(_$_ActionItem) _then) : super(_value, _then); @pragma('vm:prefer-inline') @override $Res call({ - Object? text = null, Object? icon = null, + Object? title = null, + Object? subtitle = freezed, + Object? shortcut = freezed, Object? trailing = freezed, Object? intent = freezed, + Object? actionStyle = freezed, + Object? key = freezed, }) { - return _then(_$_MenuAction( - text: null == text - ? _value.text - : text // ignore: cast_nullable_to_non_nullable - as String, + return _then(_$_ActionItem( icon: null == icon ? _value.icon : icon // ignore: cast_nullable_to_non_nullable as Widget, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + subtitle: freezed == subtitle + ? _value.subtitle + : subtitle // ignore: cast_nullable_to_non_nullable + as String?, + shortcut: freezed == shortcut + ? _value.shortcut + : shortcut // ignore: cast_nullable_to_non_nullable + as String?, trailing: freezed == trailing ? _value.trailing : trailing // ignore: cast_nullable_to_non_nullable - as String?, + as Widget?, intent: freezed == intent ? _value.intent : intent // ignore: cast_nullable_to_non_nullable as Intent?, + actionStyle: freezed == actionStyle + ? _value.actionStyle + : actionStyle // ignore: cast_nullable_to_non_nullable + as ActionStyle?, + key: freezed == key + ? _value.key + : key // ignore: cast_nullable_to_non_nullable + as Key?, )); } } /// @nodoc -class _$_MenuAction implements _MenuAction { - _$_MenuAction( - {required this.text, required this.icon, this.trailing, this.intent}); +class _$_ActionItem implements _ActionItem { + _$_ActionItem( + {required this.icon, + required this.title, + this.subtitle, + this.shortcut, + this.trailing, + this.intent, + this.actionStyle, + this.key}); - @override - final String text; @override final Widget icon; @override - final String? trailing; + final String title; + @override + final String? subtitle; + @override + final String? shortcut; + @override + final Widget? trailing; @override final Intent? intent; + @override + final ActionStyle? actionStyle; + @override + final Key? key; @override String toString() { - return 'MenuAction(text: $text, icon: $icon, trailing: $trailing, intent: $intent)'; + return 'ActionItem(icon: $icon, title: $title, subtitle: $subtitle, shortcut: $shortcut, trailing: $trailing, intent: $intent, actionStyle: $actionStyle, key: $key)'; } @override bool operator ==(dynamic other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$_MenuAction && - (identical(other.text, text) || other.text == text) && + other is _$_ActionItem && (identical(other.icon, icon) || other.icon == icon) && + (identical(other.title, title) || other.title == title) && + (identical(other.subtitle, subtitle) || + other.subtitle == subtitle) && + (identical(other.shortcut, shortcut) || + other.shortcut == shortcut) && (identical(other.trailing, trailing) || other.trailing == trailing) && - (identical(other.intent, intent) || other.intent == intent)); + (identical(other.intent, intent) || other.intent == intent) && + (identical(other.actionStyle, actionStyle) || + other.actionStyle == actionStyle) && + (identical(other.key, key) || other.key == key)); } @override - int get hashCode => Object.hash(runtimeType, text, icon, trailing, intent); + int get hashCode => Object.hash(runtimeType, icon, title, subtitle, shortcut, + trailing, intent, actionStyle, key); @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$_MenuActionCopyWith<_$_MenuAction> get copyWith => - __$$_MenuActionCopyWithImpl<_$_MenuAction>(this, _$identity); + _$$_ActionItemCopyWith<_$_ActionItem> get copyWith => + __$$_ActionItemCopyWithImpl<_$_ActionItem>(this, _$identity); } -abstract class _MenuAction implements MenuAction { - factory _MenuAction( - {required final String text, - required final Widget icon, - final String? trailing, - final Intent? intent}) = _$_MenuAction; +abstract class _ActionItem implements ActionItem { + factory _ActionItem( + {required final Widget icon, + required final String title, + final String? subtitle, + final String? shortcut, + final Widget? trailing, + final Intent? intent, + final ActionStyle? actionStyle, + final Key? key}) = _$_ActionItem; - @override - String get text; @override Widget get icon; @override - String? get trailing; + String get title; + @override + String? get subtitle; + @override + String? get shortcut; + @override + Widget? get trailing; @override Intent? get intent; @override + ActionStyle? get actionStyle; + @override + Key? get key; + @override @JsonKey(ignore: true) - _$$_MenuActionCopyWith<_$_MenuAction> get copyWith => + _$$_ActionItemCopyWith<_$_ActionItem> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/app/views/action_list.dart b/lib/app/views/action_list.dart index 5affe9a2..be6733df 100644 --- a/lib/app/views/action_list.dart +++ b/lib/app/views/action_list.dart @@ -17,16 +17,15 @@ import 'package:flutter/material.dart'; import '../../widgets/list_title.dart'; - -enum ActionStyle { normal, primary, error } +import '../models.dart'; class ActionListItem extends StatelessWidget { + final Widget icon; final String title; final String? subtitle; - final Widget icon; final Widget? trailing; + final void Function(BuildContext context)? onTap; final ActionStyle actionStyle; - final void Function()? onTap; const ActionListItem({ super.key, @@ -62,7 +61,7 @@ class ActionListItem extends StatelessWidget { ), ), trailing: trailing, - onTap: onTap, + onTap: onTap != null ? () => onTap?.call(context) : null, enabled: onTap != null, ); } @@ -74,6 +73,28 @@ class ActionListSection extends StatelessWidget { const ActionListSection(this.title, {super.key, required this.children}); + factory ActionListSection.fromMenuActions(BuildContext context, String title, + {Key? key, required List actions}) { + return ActionListSection( + key: key, + title, + children: actions.map((action) { + final intent = action.intent; + return ActionListItem( + key: action.key, + actionStyle: action.actionStyle ?? ActionStyle.normal, + icon: action.icon, + title: action.title, + subtitle: action.subtitle, + onTap: intent != null + ? (context) => Actions.invoke(context, intent) + : null, + trailing: action.trailing, + ); + }).toList(), + ); + } + @override Widget build(BuildContext context) => SizedBox( width: 360, diff --git a/lib/app/views/action_popup_menu.dart b/lib/app/views/action_popup_menu.dart new file mode 100644 index 00000000..784a4766 --- /dev/null +++ b/lib/app/views/action_popup_menu.dart @@ -0,0 +1,66 @@ +/* + * 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 'dart:async'; + +import 'package:flutter/material.dart'; + +import '../models.dart'; + +Future showPopupMenu(BuildContext context, Offset globalPosition, + List actions) => + showMenu( + context: context, + position: RelativeRect.fromLTRB( + globalPosition.dx, + globalPosition.dy, + globalPosition.dx, + 0, + ), + items: actions.map((e) => _buildMenuItem(context, e)).toList(), + ); + +PopupMenuItem _buildMenuItem(BuildContext context, ActionItem actionItem) { + final intent = actionItem.intent; + final enabled = intent != null; + final shortcut = actionItem.shortcut; + return PopupMenuItem( + enabled: enabled, + onTap: enabled + ? () { + // Wait for popup menu to close before running action. + Timer.run(() { + Actions.invoke(context, intent); + }); + } + : null, + child: ListTile( + key: actionItem.key, + enabled: enabled, + dense: true, + contentPadding: EdgeInsets.zero, + minLeadingWidth: 0, + title: Text(actionItem.title), + leading: actionItem.icon, + trailing: shortcut != null + ? Opacity( + opacity: 0.5, + child: Text(shortcut, textScaleFactor: 0.7), + ) + : null, + ), + ); +} diff --git a/lib/app/views/app_list_item.dart b/lib/app/views/app_list_item.dart new file mode 100644 index 00000000..fc783d6a --- /dev/null +++ b/lib/app/views/app_list_item.dart @@ -0,0 +1,137 @@ +/* + * 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 '../../core/state.dart'; +import '../models.dart'; +import '../shortcuts.dart'; +import 'action_popup_menu.dart'; + +class AppListItem extends StatefulWidget { + final Widget? leading; + final String title; + final String? subtitle; + final Widget? trailing; + final List Function(BuildContext context)? buildPopupActions; + final Intent? activationIntent; + + const AppListItem({ + super.key, + this.leading, + required this.title, + this.subtitle, + this.trailing, + this.buildPopupActions, + this.activationIntent, + }); + + @override + State createState() => _AppListItemState(); +} + +class _AppListItemState extends State { + final FocusNode _focusNode = FocusNode(); + int _lastTap = 0; + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final subtitle = widget.subtitle; + final buildPopupActions = widget.buildPopupActions; + final activationIntent = widget.activationIntent; + final trailing = widget.trailing; + + return Shortcuts( + shortcuts: { + LogicalKeySet(LogicalKeyboardKey.enter): const OpenIntent(), + LogicalKeySet(LogicalKeyboardKey.space): const OpenIntent(), + }, + child: InkWell( + focusNode: _focusNode, + borderRadius: BorderRadius.circular(30), + onSecondaryTapDown: buildPopupActions == null + ? null + : (details) { + showPopupMenu( + context, + details.globalPosition, + buildPopupActions(context), + ); + }, + onTap: () { + if (isDesktop) { + final now = DateTime.now().millisecondsSinceEpoch; + if (now - _lastTap < 500) { + setState(() { + _lastTap = 0; + }); + Actions.invoke(context, activationIntent ?? const OpenIntent()); + } else { + _focusNode.requestFocus(); + setState(() { + _lastTap = now; + }); + } + } else { + Actions.invoke(context, const OpenIntent()); + } + }, + onLongPress: activationIntent == null + ? null + : () { + Actions.invoke(context, activationIntent); + }, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + const SizedBox(height: 64), + ListTile( + leading: widget.leading, + title: Text( + widget.title, + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: false, + ), + subtitle: subtitle != null + ? Text( + subtitle, + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: false, + ) + : null, + trailing: trailing == null + ? null + : Focus( + skipTraversal: true, + descendantsAreTraversable: false, + child: trailing, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/fido/keys.dart b/lib/fido/keys.dart new file mode 100644 index 00000000..028bf562 --- /dev/null +++ b/lib/fido/keys.dart @@ -0,0 +1,45 @@ +/* + * 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 = 'fido.keys'; +const _keyAction = '$_prefix.actions'; +const _credentialAction = '$_prefix.credential.actions'; +const _fingerprintAction = '$_prefix.fingerprint.actions'; + +// Key actions +const managePinAction = Key('$_keyAction.manage_pin'); +const addFingerprintAction = Key('$_keyAction.add_fingerprint'); +const resetAction = Key('$_keyAction.reset'); + +// Credential actions +const editCredentialAction = Key('$_credentialAction.edit'); +const deleteCredentialAction = Key('$_credentialAction.delete'); + +// Fingerprint actions +const editFingerintAction = Key('$_fingerprintAction.edit'); +const deleteFingerprintAction = Key('$_fingerprintAction.delete'); + +const saveButton = Key('$_prefix.save'); +const deleteButton = Key('$_prefix.delete'); +const unlockButton = Key('$_prefix.unlock'); + +const managementKeyField = Key('$_prefix.management_key'); +const pinPukField = Key('$_prefix.pin_puk'); +const newPinPukField = Key('$_prefix.new_pin_puk'); +const confirmPinPukField = Key('$_prefix.confirm_pin_puk'); +const subjectField = Key('$_prefix.subject'); diff --git a/lib/fido/views/actions.dart b/lib/fido/views/actions.dart new file mode 100644 index 00000000..6c07c47e --- /dev/null +++ b/lib/fido/views/actions.dart @@ -0,0 +1,55 @@ +/* + * 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/models.dart'; +import '../../app/shortcuts.dart'; +import '../keys.dart' as keys; + +List buildFingerprintActions(AppLocalizations l10n) { + return [ + ActionItem( + key: keys.editFingerintAction, + icon: const Icon(Icons.edit), + title: l10n.s_rename_fp, + subtitle: l10n.l_rename_fp_desc, + intent: const EditIntent(), + ), + ActionItem( + key: keys.deleteFingerprintAction, + actionStyle: ActionStyle.error, + icon: const Icon(Icons.delete), + title: l10n.s_delete_fingerprint, + subtitle: l10n.l_delete_fingerprint_desc, + intent: const DeleteIntent(), + ), + ]; +} + +List buildCredentialActions(AppLocalizations l10n) { + return [ + ActionItem( + key: keys.deleteCredentialAction, + actionStyle: ActionStyle.error, + icon: const Icon(Icons.delete), + title: l10n.s_delete_passkey, + subtitle: l10n.l_delete_account_desc, + intent: const DeleteIntent(), + ), + ]; +} diff --git a/lib/fido/views/credential_dialog.dart b/lib/fido/views/credential_dialog.dart index e4f2addc..7b9d1119 100644 --- a/lib/fido/views/credential_dialog.dart +++ b/lib/fido/views/credential_dialog.dart @@ -8,6 +8,7 @@ import '../../app/state.dart'; import '../../app/views/fs_dialog.dart'; import '../../app/views/action_list.dart'; import '../models.dart'; +import 'actions.dart'; import 'delete_credential_dialog.dart'; class CredentialDialog extends ConsumerWidget { @@ -78,19 +79,10 @@ class CredentialDialog extends ConsumerWidget { ], ), ), - ActionListSection( + ActionListSection.fromMenuActions( + context, l10n.s_actions, - children: [ - ActionListItem( - actionStyle: ActionStyle.error, - icon: const Icon(Icons.delete), - title: l10n.s_delete_passkey, - subtitle: l10n.l_delete_account_desc, - onTap: () { - Actions.invoke(context, const DeleteIntent()); - }, - ), - ], + actions: buildCredentialActions(l10n), ), ], ), diff --git a/lib/fido/views/fingerprint_dialog.dart b/lib/fido/views/fingerprint_dialog.dart index c32d1e90..398493cb 100644 --- a/lib/fido/views/fingerprint_dialog.dart +++ b/lib/fido/views/fingerprint_dialog.dart @@ -8,6 +8,7 @@ import '../../app/state.dart'; import '../../app/views/fs_dialog.dart'; import '../../app/views/action_list.dart'; import '../models.dart'; +import 'actions.dart'; import 'delete_fingerprint_dialog.dart'; import 'rename_fingerprint_dialog.dart'; @@ -94,27 +95,10 @@ class FingerprintDialog extends ConsumerWidget { ], ), ), - ActionListSection( + ActionListSection.fromMenuActions( + context, l10n.s_actions, - children: [ - ActionListItem( - icon: const Icon(Icons.edit), - title: l10n.s_rename_fp, - subtitle: l10n.l_rename_fp_desc, - onTap: () { - Actions.invoke(context, const EditIntent()); - }, - ), - ActionListItem( - actionStyle: ActionStyle.error, - icon: const Icon(Icons.delete), - title: l10n.s_delete_fingerprint, - subtitle: l10n.l_delete_fingerprint_desc, - onTap: () { - Actions.invoke(context, const DeleteIntent()); - }, - ), - ], + actions: buildFingerprintActions(l10n), ), ], ), diff --git a/lib/fido/views/key_actions.dart b/lib/fido/views/key_actions.dart index 4aaff140..e2d153ab 100755 --- a/lib/fido/views/key_actions.dart +++ b/lib/fido/views/key_actions.dart @@ -22,6 +22,7 @@ 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 'add_fingerprint_dialog.dart'; import 'pin_dialog.dart'; import 'reset_dialog.dart'; @@ -42,6 +43,7 @@ Widget fidoBuildActions( l10n.s_setup, children: [ ActionListItem( + key: keys.addFingerprintAction, actionStyle: ActionStyle.primary, icon: const Icon(Icons.fingerprint_outlined), title: l10n.s_add_fingerprint, @@ -53,7 +55,7 @@ Widget fidoBuildActions( trailing: fingerprints == 0 ? const Icon(Icons.warning_amber) : null, onTap: state.unlocked && fingerprints < 5 - ? () { + ? (context) { Navigator.of(context).pop(); showBlurDialog( context: context, @@ -68,6 +70,7 @@ Widget fidoBuildActions( l10n.s_manage, children: [ ActionListItem( + key: keys.managePinAction, icon: const Icon(Icons.pin_outlined), title: state.hasPin ? l10n.s_change_pin : l10n.s_set_pin, subtitle: state.hasPin @@ -76,7 +79,7 @@ Widget fidoBuildActions( trailing: state.alwaysUv && !state.hasPin ? const Icon(Icons.warning_amber) : null, - onTap: () { + onTap: (context) { Navigator.of(context).pop(); showBlurDialog( context: context, @@ -84,11 +87,12 @@ Widget fidoBuildActions( ); }), ActionListItem( + key: keys.resetAction, actionStyle: ActionStyle.error, icon: const Icon(Icons.delete_outline), title: l10n.s_reset_fido, subtitle: l10n.l_factory_reset_this_app, - onTap: () { + onTap: (context) { Navigator.of(context).pop(); showBlurDialog( context: context, diff --git a/lib/fido/views/unlocked_page.dart b/lib/fido/views/unlocked_page.dart index f2f2305f..bf796e31 100755 --- a/lib/fido/views/unlocked_page.dart +++ b/lib/fido/views/unlocked_page.dart @@ -21,15 +21,20 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/shortcuts.dart'; +import '../../app/views/app_list_item.dart'; import '../../app/views/app_page.dart'; import '../../app/views/graphics.dart'; import '../../app/views/message_page.dart'; import '../../widgets/list_title.dart'; import '../models.dart'; import '../state.dart'; +import 'actions.dart'; import 'credential_dialog.dart'; +import 'delete_credential_dialog.dart'; +import 'delete_fingerprint_dialog.dart'; import 'fingerprint_dialog.dart'; import 'key_actions.dart'; +import 'rename_fingerprint_dialog.dart'; class FidoUnlockedPage extends ConsumerWidget { final DeviceNode node; @@ -51,13 +56,20 @@ class FidoUnlockedPage extends ConsumerWidget { children.add(ListTitle(l10n.s_passkeys)); children.addAll(creds.map((cred) => Actions( actions: { - OpenIntent: CallbackAction(onInvoke: (_) async { - await showBlurDialog( + OpenIntent: CallbackAction( + onInvoke: (_) => showBlurDialog( + context: context, + builder: (context) => CredentialDialog(cred), + )), + DeleteIntent: CallbackAction( + onInvoke: (_) => showBlurDialog( context: context, - builder: (context) => CredentialDialog(cred), - ); - return null; - }), + builder: (context) => DeleteCredentialDialog( + node.path, + cred, + ), + ), + ), }, child: _CredentialListItem(cred), ))); @@ -76,13 +88,27 @@ class FidoUnlockedPage extends ConsumerWidget { children.add(ListTitle(l10n.s_fingerprints)); children.addAll(fingerprints.map((fp) => Actions( actions: { - OpenIntent: CallbackAction(onInvoke: (_) async { - await showBlurDialog( - context: context, - builder: (context) => FingerprintDialog(fp), - ); - return null; - }), + OpenIntent: CallbackAction( + onInvoke: (_) => showBlurDialog( + context: context, + builder: (context) => FingerprintDialog(fp), + )), + EditIntent: CallbackAction( + onInvoke: (_) => showBlurDialog( + context: context, + builder: (context) => RenameFingerprintDialog( + node.path, + fp, + ), + )), + DeleteIntent: CallbackAction( + onInvoke: (_) => showBlurDialog( + context: context, + builder: (context) => DeleteFingerprintDialog( + node.path, + fp, + ), + )), }, child: _FingerprintListItem(fp), ))); @@ -136,28 +162,20 @@ class _CredentialListItem extends StatelessWidget { @override Widget build(BuildContext context) { - return ListTile( + return AppListItem( leading: CircleAvatar( foregroundColor: Theme.of(context).colorScheme.onPrimary, backgroundColor: Theme.of(context).colorScheme.primary, child: const Icon(Icons.person), ), - title: Text( - credential.userName, - softWrap: false, - overflow: TextOverflow.fade, - ), - subtitle: Text( - credential.rpId, - softWrap: false, - overflow: TextOverflow.fade, - ), + title: credential.userName, + subtitle: credential.rpId, trailing: OutlinedButton( - onPressed: () { - Actions.maybeInvoke(context, const OpenIntent()); - }, + onPressed: Actions.handler(context, const OpenIntent()), child: const Icon(Icons.more_horiz), ), + buildPopupActions: (context) => + buildCredentialActions(AppLocalizations.of(context)!), ); } } @@ -168,23 +186,19 @@ class _FingerprintListItem extends StatelessWidget { @override Widget build(BuildContext context) { - return ListTile( + return AppListItem( leading: CircleAvatar( foregroundColor: Theme.of(context).colorScheme.onSecondary, backgroundColor: Theme.of(context).colorScheme.secondary, child: const Icon(Icons.fingerprint), ), - title: Text( - fingerprint.label, - softWrap: false, - overflow: TextOverflow.fade, - ), + title: fingerprint.label, trailing: OutlinedButton( - onPressed: () { - Actions.maybeInvoke(context, const OpenIntent()); - }, + onPressed: Actions.handler(context, const OpenIntent()), child: const Icon(Icons.more_horiz), ), + buildPopupActions: (context) => + buildFingerprintActions(AppLocalizations.of(context)!), ); } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6fde5d07..499180b4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -400,6 +400,7 @@ "@_certificates": {}, "s_certificate": "Certificate", + "s_certificates": "Certificates", "s_csr": "CSR", "s_subject": "Subject", "l_export_csr_file": "Save CSR to file", diff --git a/lib/oath/keys.dart b/lib/oath/keys.dart index 8eb64593..910890bf 100755 --- a/lib/oath/keys.dart +++ b/lib/oath/keys.dart @@ -17,18 +17,28 @@ import 'package:flutter/material.dart'; const _prefix = 'oath.keys'; - -const setOrManagePasswordAction = Key('$_prefix.set_or_manage_password'); -const addAccountAction = Key('$_prefix.add_account'); -const resetAction = Key('$_prefix.reset'); - -const customIconsAction = Key('$_prefix.custom_icons'); - -const noAccountsView = Key('$_prefix.no_accounts'); +const _keyAction = '$_prefix.actions'; +const _accountAction = '$_prefix.account.actions'; // This is global so we can access it from the global Ctrl+F shortcut. final searchAccountsField = GlobalKey(); +// Key actions +const setOrManagePasswordAction = + Key('$_keyAction.action.set_or_manage_password'); +const addAccountAction = Key('$_keyAction.add_account'); +const resetAction = Key('$_keyAction.reset'); +const customIconsAction = Key('$_keyAction.custom_icons'); + +// Credential actions +const copyAction = Key('$_accountAction.copy'); +const calculateAction = Key('$_accountAction.calculate'); +const togglePinAction = Key('$_accountAction.toggle_pin'); +const editAction = Key('$_accountAction.edit'); +const deleteAction = Key('$_accountAction.delete'); + +const noAccountsView = Key('$_prefix.no_accounts'); + const passwordField = Key('$_prefix.password'); const currentPasswordField = Key('$_prefix.current_password'); const newPasswordField = Key('$_prefix.new_password'); diff --git a/lib/oath/views/account_dialog.dart b/lib/oath/views/account_dialog.dart index ef9d41d0..963994d0 100755 --- a/lib/oath/views/account_dialog.dart +++ b/lib/oath/views/account_dialog.dart @@ -39,44 +39,6 @@ class AccountDialog extends ConsumerWidget { const AccountDialog(this.credential, {super.key}); - List _buildActions( - BuildContext context, AccountHelper helper) { - final l10n = AppLocalizations.of(context)!; - final actions = helper.buildActions(); - - final copy = - actions.firstWhere(((e) => e.text == l10n.l_copy_to_clipboard)); - final delete = actions.firstWhere(((e) => e.text == l10n.s_delete_account)); - final canCopy = copy.intent != null; - final actionStyles = { - copy: canCopy ? ActionStyle.primary : ActionStyle.normal, - delete: ActionStyle.error, - }; - - // If we can't copy, but can calculate, highlight that button instead - if (!canCopy) { - final calculates = actions.where(((e) => e.text == l10n.s_calculate)); - if (calculates.isNotEmpty) { - actionStyles[calculates.first] = ActionStyle.primary; - } - } - - return actions.map((e) { - final intent = e.intent; - return ActionListItem( - actionStyle: actionStyles[e] ?? ActionStyle.normal, - icon: e.icon, - title: e.text, - subtitle: e.trailing, - onTap: intent != null - ? () { - Actions.invoke(context, intent); - } - : null, - ); - }).toList(); - } - @override Widget build(BuildContext context, WidgetRef ref) { // TODO: Solve this in a cleaner way @@ -192,9 +154,10 @@ class AccountDialog extends ConsumerWidget { ), ), const SizedBox(height: 32), - ActionListSection( + ActionListSection.fromMenuActions( + context, AppLocalizations.of(context)!.s_actions, - children: _buildActions(context, helper), + actions: helper.buildActions(), ), ], ), diff --git a/lib/oath/views/account_helper.dart b/lib/oath/views/account_helper.dart index 1788c659..bca35a01 100755 --- a/lib/oath/views/account_helper.dart +++ b/lib/oath/views/account_helper.dart @@ -14,6 +14,7 @@ * limitations under the License. */ +import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; @@ -28,6 +29,7 @@ import '../../widgets/circle_timer.dart'; import '../../widgets/custom_icons.dart'; import '../models.dart'; import '../state.dart'; +import '../keys.dart' as keys; import 'actions.dart'; /// Support class for presenting an OATH account. @@ -52,7 +54,7 @@ class AccountHelper { String get title => credential.issuer ?? credential.name; String? get subtitle => credential.issuer != null ? credential.name : null; - List buildActions() => _ref + List buildActions() => _ref .watch(currentDeviceDataProvider) .maybeWhen( data: (data) { @@ -61,40 +63,50 @@ class AccountHelper { final ready = expired || credential.oathType == OathType.hotp; final pinned = _ref.watch(favoritesProvider).contains(credential.id); final l10n = AppLocalizations.of(_context)!; + final canCopy = code != null && !expired; return [ - MenuAction( + ActionItem( + key: keys.copyAction, icon: const Icon(Icons.copy), - text: l10n.l_copy_to_clipboard, - trailing: l10n.l_copy_code_desc, - intent: code == null || expired ? null : const CopyIntent(), + title: l10n.l_copy_to_clipboard, + subtitle: l10n.l_copy_code_desc, + shortcut: Platform.isMacOS ? '\u2318 C' : 'Ctrl+C', + actionStyle: canCopy ? ActionStyle.primary : null, + intent: canCopy ? const CopyIntent() : null, ), if (manual) - MenuAction( + ActionItem( + key: keys.calculateAction, + actionStyle: !canCopy ? ActionStyle.primary : null, icon: const Icon(Icons.refresh), - text: l10n.s_calculate, - trailing: l10n.l_calculate_code_desc, + title: l10n.s_calculate, + subtitle: l10n.l_calculate_code_desc, intent: ready ? const CalculateIntent() : null, ), - MenuAction( + ActionItem( + key: keys.togglePinAction, icon: pinned ? pushPinStrokeIcon : const Icon(Icons.push_pin_outlined), - text: pinned ? l10n.s_unpin_account : l10n.s_pin_account, - trailing: l10n.l_pin_account_desc, + title: pinned ? l10n.s_unpin_account : l10n.s_pin_account, + subtitle: l10n.l_pin_account_desc, intent: const TogglePinIntent(), ), if (data.info.version.isAtLeast(5, 3)) - MenuAction( + ActionItem( + key: keys.editAction, icon: const Icon(Icons.edit_outlined), - text: l10n.s_rename_account, - trailing: l10n.l_rename_account_desc, + title: l10n.s_rename_account, + subtitle: l10n.l_rename_account_desc, intent: const EditIntent(), ), - MenuAction( + ActionItem( + key: keys.deleteAction, + actionStyle: ActionStyle.error, icon: const Icon(Icons.delete_outline), - text: l10n.s_delete_account, - trailing: l10n.l_delete_account_desc, + title: l10n.s_delete_account, + subtitle: l10n.l_delete_account_desc, intent: const DeleteIntent(), ), ]; diff --git a/lib/oath/views/account_view.dart b/lib/oath/views/account_view.dart index a0bb6b8f..6801e168 100755 --- a/lib/oath/views/account_view.dart +++ b/lib/oath/views/account_view.dart @@ -14,19 +14,15 @@ * limitations under the License. */ -import 'dart:io'; - import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../app/message.dart'; import '../../app/shortcuts.dart'; import '../../app/state.dart'; -import '../../core/state.dart'; -import '../../widgets/menu_list_tile.dart'; +import '../../app/views/app_list_item.dart'; import '../models.dart'; import '../state.dart'; import 'account_dialog.dart'; @@ -51,15 +47,6 @@ String _a11yCredentialLabel(String? issuer, String name, String? code) { class _AccountViewState extends ConsumerState { OathCredential get credential => widget.credential; - final _focusNode = FocusNode(); - int _lastTap = 0; - - @override - void dispose() { - _focusNode.dispose(); - super.dispose(); - } - Color _iconColor(int shade) { final colors = [ Colors.red[shade], @@ -90,26 +77,6 @@ class _AccountViewState extends ConsumerState { return colors[label.hashCode % colors.length]!; } - List _buildPopupMenu( - BuildContext context, AccountHelper helper) { - final shortcut = Platform.isMacOS ? '\u2318 C' : 'Ctrl+C'; - final copyText = AppLocalizations.of(context)!.l_copy_to_clipboard; - - return helper.buildActions().map((e) { - final intent = e.intent; - return buildMenuItem( - leading: e.icon, - title: Text(e.text), - action: intent != null - ? () { - Actions.invoke(context, intent); - } - : null, - trailing: e.text == copyText ? shortcut : null, - ); - }).toList(); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -171,83 +138,27 @@ class _AccountViewState extends ConsumerState { child: Semantics( label: _a11yCredentialLabel( credential.issuer, credential.name, helper.code?.value), - child: InkWell( - focusNode: _focusNode, - borderRadius: BorderRadius.circular(30), - onSecondaryTapDown: (details) { - showMenu( - context: context, - position: RelativeRect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx, - 0, - ), - items: _buildPopupMenu(context, helper), - ); - }, - onTap: () { - if (isDesktop) { - final now = DateTime.now().millisecondsSinceEpoch; - if (now - _lastTap < 500) { - setState(() { - _lastTap = 0; - }); - Actions.maybeInvoke(context, const CopyIntent()); - } else { - _focusNode.requestFocus(); - setState(() { - _lastTap = now; - }); - } - } else { - Actions.maybeInvoke( - context, const OpenIntent()); - } - }, - onLongPress: () { - Actions.maybeInvoke(context, const CopyIntent()); - }, - child: ListTile( - leading: showAvatar - ? AccountIcon( - issuer: credential.issuer, - defaultWidget: circleAvatar) - : null, - title: Text( - helper.title, - overflow: TextOverflow.fade, - maxLines: 1, - softWrap: false, - ), - subtitle: subtitle != null - ? Text( - subtitle, - overflow: TextOverflow.fade, - maxLines: 1, - softWrap: false, - ) - : null, - trailing: Focus( - skipTraversal: true, - descendantsAreTraversable: false, - child: helper.code != null - ? FilledButton.tonalIcon( - icon: helper.buildCodeIcon(), - label: helper.buildCodeLabel(), - onPressed: () { - Actions.maybeInvoke( - context, const OpenIntent()); - }, - ) - : FilledButton.tonal( - onPressed: () { - Actions.maybeInvoke( - context, const OpenIntent()); - }, - child: helper.buildCodeIcon()), - ), - ), + child: AppListItem( + leading: showAvatar + ? AccountIcon( + issuer: credential.issuer, + defaultWidget: circleAvatar) + : null, + title: helper.title, + subtitle: subtitle, + trailing: helper.code != null + ? FilledButton.tonalIcon( + icon: helper.buildCodeIcon(), + label: helper.buildCodeLabel(), + onPressed: + Actions.handler(context, const OpenIntent()), + ) + : FilledButton.tonal( + onPressed: + Actions.handler(context, const OpenIntent()), + child: helper.buildCodeIcon()), + activationIntent: const CopyIntent(), + buildPopupActions: (_) => helper.buildActions(), ), )); }); diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index 25da1e04..a096dc13 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -58,7 +58,7 @@ Widget oathBuildActions( ? l10n.l_accounts_used(used, capacity) : ''), onTap: used != null && (capacity == null || capacity > used) - ? () async { + ? (context) async { final credentials = ref.read(credentialsProvider); final withContext = ref.read(withContextProvider); Navigator.of(context).pop(); @@ -98,7 +98,7 @@ Widget oathBuildActions( title: l10n.s_custom_icons, subtitle: l10n.l_set_icons_for_accounts, icon: const Icon(Icons.image_outlined), - onTap: () async { + onTap: (context) async { Navigator.of(context).pop(); await ref.read(withContextProvider)((context) => showBlurDialog( context: context, @@ -114,7 +114,7 @@ Widget oathBuildActions( : l10n.s_set_password, subtitle: l10n.l_optional_password_protection, icon: const Icon(Icons.password_outlined), - onTap: () { + onTap: (context) { Navigator.of(context).pop(); showBlurDialog( context: context, @@ -128,7 +128,7 @@ Widget oathBuildActions( actionStyle: ActionStyle.error, title: l10n.s_reset_oath, subtitle: l10n.l_factory_reset_this_app, - onTap: () { + onTap: (context) { Navigator.of(context).pop(); showBlurDialog( context: context, diff --git a/lib/piv/keys.dart b/lib/piv/keys.dart index 3ee1b6ee..07271b2c 100644 --- a/lib/piv/keys.dart +++ b/lib/piv/keys.dart @@ -17,13 +17,22 @@ import 'package:flutter/material.dart'; const _prefix = 'piv.keys'; +const _keyAction = '$_prefix.actions'; +const _slotAction = '$_prefix.slot.actions'; -const managePinAction = Key('$_prefix.manage_pin'); -const managePukAction = Key('$_prefix.manage_puk'); -const manageManagementKeyAction = Key('$_prefix.manage_management_key'); -const resetAction = Key('$_prefix.reset'); +// Key actions +const managePinAction = Key('$_keyAction.manage_pin'); +const managePukAction = Key('$_keyAction.manage_puk'); +const manageManagementKeyAction = Key('$_keyAction.manage_management_key'); +const resetAction = Key('$_keyAction.reset'); +const setupMacOsAction = Key('$_keyAction.setup_macos'); + +// Slot actions +const generateAction = Key('$_slotAction.generate'); +const importAction = Key('$_slotAction.import'); +const exportAction = Key('$_slotAction.export'); +const deleteAction = Key('$_slotAction.delete'); -const setupMacOsAction = Key('$_prefix.setup_macos'); const saveButton = Key('$_prefix.save'); const deleteButton = Key('$_prefix.delete'); const unlockButton = Key('$_prefix.unlock'); diff --git a/lib/piv/views/actions.dart b/lib/piv/views/actions.dart index b1ae3188..19bebbc2 100644 --- a/lib/piv/views/actions.dart +++ b/lib/piv/views/actions.dart @@ -27,20 +27,13 @@ import '../../app/state.dart'; import '../../app/models.dart'; import '../models.dart'; import '../state.dart'; +import '../keys.dart' as keys; import 'authentication_dialog.dart'; import 'delete_certificate_dialog.dart'; import 'generate_key_dialog.dart'; import 'import_file_dialog.dart'; import 'pin_dialog.dart'; -class AuthenticateIntent extends Intent { - const AuthenticateIntent(); -} - -class VerifyPinIntent extends Intent { - const VerifyPinIntent(); -} - class GenerateIntent extends Intent { const GenerateIntent(); } @@ -85,9 +78,6 @@ Widget registerPivActions( }) => Actions( actions: { - AuthenticateIntent: CallbackAction( - onInvoke: (intent) => _authenticate(ref, devicePath, pivState), - ), GenerateIntent: CallbackAction(onInvoke: (intent) async { if (!pivState.protectedKey && @@ -221,17 +211,46 @@ Widget registerPivActions( ), ) ?? false); - - // Needs to move to slot dialog(?) or react to state change - // Pop the slot dialog if deleted - if (deleted == true) { - await withContext((context) async { - Navigator.of(context).pop(); - }); - } return deleted; }), ...actions, }, child: Builder(builder: builder), ); + +List buildSlotActions(bool hasCert, AppLocalizations l10n) { + return [ + ActionItem( + key: keys.generateAction, + icon: const Icon(Icons.add_outlined), + actionStyle: ActionStyle.primary, + title: l10n.s_generate_key, + subtitle: l10n.l_generate_desc, + intent: const GenerateIntent(), + ), + ActionItem( + key: keys.importAction, + icon: const Icon(Icons.file_download_outlined), + title: l10n.l_import_file, + subtitle: l10n.l_import_desc, + intent: const ImportIntent(), + ), + if (hasCert) ...[ + ActionItem( + key: keys.exportAction, + icon: const Icon(Icons.file_upload_outlined), + title: l10n.l_export_certificate, + subtitle: l10n.l_export_certificate_desc, + intent: const ExportIntent(), + ), + ActionItem( + key: keys.deleteAction, + actionStyle: ActionStyle.error, + icon: const Icon(Icons.delete_outline), + title: l10n.l_delete_certificate, + subtitle: l10n.l_delete_certificate_desc, + intent: const DeleteIntent(), + ), + ], + ]; +} diff --git a/lib/piv/views/key_actions.dart b/lib/piv/views/key_actions.dart index 69de0d18..c81232c0 100644 --- a/lib/piv/views/key_actions.dart +++ b/lib/piv/views/key_actions.dart @@ -51,7 +51,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, ? l10n.l_piv_pin_blocked : l10n.l_attempts_remaining(pivState.pinAttempts), icon: const Icon(Icons.pin_outlined), - onTap: () { + onTap: (context) { Navigator.of(context).pop(); showBlurDialog( context: context, @@ -69,7 +69,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, ? l10n.l_attempts_remaining(pukAttempts) : null, icon: const Icon(Icons.pin_outlined), - onTap: () { + onTap: (context) { Navigator.of(context).pop(); showBlurDialog( context: context, @@ -89,7 +89,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, trailing: usingDefaultMgmtKey ? const Icon(Icons.warning_amber) : null, - onTap: () { + onTap: (context) { Navigator.of(context).pop(); showBlurDialog( context: context, @@ -102,7 +102,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, actionStyle: ActionStyle.error, title: l10n.s_reset_piv, subtitle: l10n.l_factory_reset_this_app, - onTap: () { + onTap: (context) { Navigator.of(context).pop(); showBlurDialog( context: context, diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart index f2f3fef1..6d3474a7 100644 --- a/lib/piv/views/piv_screen.dart +++ b/lib/piv/views/piv_screen.dart @@ -22,10 +22,13 @@ 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 '../../widgets/list_title.dart'; import '../models.dart'; import '../state.dart'; +import 'actions.dart'; import 'key_actions.dart'; import 'slot_dialog.dart'; @@ -55,8 +58,13 @@ class PivScreen extends ConsumerWidget { pivBuildActions(context, devicePath, pivState, ref), child: Column( children: [ + ListTitle(l10n.s_certificates), if (pivSlots?.hasValue == true) - ...pivSlots!.value.map((e) => Actions( + ...pivSlots!.value.map((e) => registerPivActions( + devicePath, + pivState, + e, + ref: ref, actions: { OpenIntent: CallbackAction(onInvoke: (_) async { @@ -67,8 +75,8 @@ class PivScreen extends ConsumerWidget { return null; }), }, - child: _CertificateListItem(e), - )) + builder: (context) => _CertificateListItem(e), + )), ], ), ); @@ -87,32 +95,24 @@ class _CertificateListItem extends StatelessWidget { final certInfo = pivSlot.certInfo; final l10n = AppLocalizations.of(context)!; final colorScheme = Theme.of(context).colorScheme; - return ListTile( + + return AppListItem( leading: CircleAvatar( foregroundColor: colorScheme.onSecondary, backgroundColor: colorScheme.secondary, child: const Icon(Icons.approval), ), - title: Text( - slot.getDisplayName(l10n), - softWrap: false, - overflow: TextOverflow.fade, - ), + title: slot.getDisplayName(l10n), subtitle: certInfo != null - ? Text( - l10n.l_subject_issuer(certInfo.subject, certInfo.issuer), - softWrap: false, - overflow: TextOverflow.fade, - ) - : Text(pivSlot.hasKey == true + ? l10n.l_subject_issuer(certInfo.subject, certInfo.issuer) + : pivSlot.hasKey == true ? l10n.l_key_no_certificate - : l10n.l_no_certificate), + : l10n.l_no_certificate, trailing: OutlinedButton( - onPressed: () { - Actions.maybeInvoke(context, const OpenIntent()); - }, + onPressed: Actions.handler(context, const OpenIntent()), child: const Icon(Icons.more_horiz), ), + buildPopupActions: (context) => buildSlotActions(certInfo != null, l10n), ); } } diff --git a/lib/piv/views/slot_dialog.dart b/lib/piv/views/slot_dialog.dart index 80ce6640..c1aa361e 100644 --- a/lib/piv/views/slot_dialog.dart +++ b/lib/piv/views/slot_dialog.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../app/shortcuts.dart'; import '../../app/state.dart'; import '../../app/views/fs_dialog.dart'; import '../../app/views/action_list.dart'; @@ -26,6 +25,10 @@ class SlotDialog extends ConsumerWidget { final l10n = AppLocalizations.of(context)!; final textTheme = Theme.of(context).textTheme; + // This is what ListTile uses for subtitle + final subtitleStyle = textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ); final pivState = ref.watch(pivStateProvider(node.path)).valueOrNull; final slotData = ref.watch(pivSlotsProvider(node.path).select((value) => @@ -64,38 +67,26 @@ class SlotDialog extends ConsumerWidget { certInfo.subject, certInfo.issuer), softWrap: true, textAlign: TextAlign.center, - // This is what ListTile uses for subtitle - style: textTheme.bodyMedium!.copyWith( - color: textTheme.bodySmall!.color, - ), + style: subtitleStyle, ), Text( l10n.l_serial(certInfo.serial), softWrap: true, textAlign: TextAlign.center, - // This is what ListTile uses for subtitle - style: textTheme.bodyMedium!.copyWith( - color: textTheme.bodySmall!.color, - ), + style: subtitleStyle, ), Text( l10n.l_certificate_fingerprint(certInfo.fingerprint), softWrap: true, textAlign: TextAlign.center, - // This is what ListTile uses for subtitle - style: textTheme.bodyMedium!.copyWith( - color: textTheme.bodySmall!.color, - ), + style: subtitleStyle, ), Text( l10n.l_valid( certInfo.notValidBefore, certInfo.notValidAfter), softWrap: true, textAlign: TextAlign.center, - // This is what ListTile uses for subtitle - style: textTheme.bodyMedium!.copyWith( - color: textTheme.bodySmall!.color, - ), + style: subtitleStyle, ), ] else ...[ Padding( @@ -104,10 +95,7 @@ class SlotDialog extends ConsumerWidget { l10n.l_no_certificate, softWrap: true, textAlign: TextAlign.center, - // This is what ListTile uses for subtitle - style: textTheme.bodyMedium!.copyWith( - color: textTheme.bodySmall!.color, - ), + style: subtitleStyle, ), ), ], @@ -115,46 +103,10 @@ class SlotDialog extends ConsumerWidget { ], ), ), - ActionListSection( + ActionListSection.fromMenuActions( + context, l10n.s_actions, - children: [ - ActionListItem( - icon: const Icon(Icons.add_outlined), - actionStyle: ActionStyle.primary, - title: l10n.s_generate_key, - subtitle: l10n.l_generate_desc, - onTap: () { - Actions.invoke(context, const GenerateIntent()); - }, - ), - ActionListItem( - icon: const Icon(Icons.file_download_outlined), - title: l10n.l_import_file, - subtitle: l10n.l_import_desc, - onTap: () { - Actions.invoke(context, const ImportIntent()); - }, - ), - if (certInfo != null) ...[ - ActionListItem( - icon: const Icon(Icons.file_upload_outlined), - title: l10n.l_export_certificate, - subtitle: l10n.l_export_certificate_desc, - onTap: () { - Actions.invoke(context, const ExportIntent()); - }, - ), - ActionListItem( - actionStyle: ActionStyle.error, - icon: const Icon(Icons.delete_outline), - title: l10n.l_delete_certificate, - subtitle: l10n.l_delete_certificate_desc, - onTap: () { - Actions.invoke(context, const DeleteIntent()); - }, - ), - ], - ], + actions: buildSlotActions(certInfo != null, l10n), ), ], ), diff --git a/lib/widgets/menu_list_tile.dart b/lib/widgets/menu_list_tile.dart deleted file mode 100755 index e670f599..00000000 --- a/lib/widgets/menu_list_tile.dart +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 'dart:async'; - -import 'package:flutter/material.dart'; - -PopupMenuItem buildMenuItem({ - required Widget title, - Widget? leading, - String? trailing, - void Function()? action, -}) => - PopupMenuItem( - enabled: action != null, - onTap: () { - // Wait for popup menu to close before running action. - Timer.run(action!); - }, - child: ListTile( - enabled: action != null, - dense: true, - contentPadding: EdgeInsets.zero, - minLeadingWidth: 0, - title: title, - leading: leading, - trailing: trailing != null - ? Opacity( - opacity: 0.5, - child: Text(trailing, textScaleFactor: 0.7), - ) - : null, - ), - ); From 7f1963d310438237124c01538cc36230c6ca4d3b Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 16 Jun 2023 17:27:10 +0200 Subject: [PATCH 029/158] PIV fixes. --- lib/l10n/app_en.arb | 4 +- lib/piv/views/generate_key_dialog.dart | 126 +++++++++++++++-------- lib/piv/views/key_actions.dart | 4 +- lib/piv/views/manage_key_dialog.dart | 5 +- lib/piv/views/manage_pin_puk_dialog.dart | 4 +- lib/theme.dart | 3 + lib/widgets/responsive_dialog.dart | 29 +++--- 7 files changed, 115 insertions(+), 60 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 499180b4..b429bb71 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -449,6 +449,8 @@ "slot": {} } }, + "l_generating_private_key": "Generating private key\u2026", + "s_private_key_generated": "Private key generated", "p_warning_delete_certificate": "Warning! This action will delete the certificate from your YubiKey.", "q_delete_certificate_confirm": "Delete the certficate in PIV slot {slot}?", "@q_delete_certificate_confirm" : { @@ -525,7 +527,7 @@ "p_warning_deletes_accounts": "Warning! This will irrevocably delete all U2F and FIDO2 accounts from your YubiKey.", "p_warning_disable_accounts": "Your credentials, as well as any PIN set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.", "p_warning_piv_reset": "Warning! All data stored for PIV will be irrevocably deleted from your YubiKey.", - "p_warning_piv_reset_desc": "This includes private keys and certificates. Your PIN, PUK, and management key will be reset to their factory detault values.", + "p_warning_piv_reset_desc": "This includes private keys and certificates. Your PIN, PUK, and management key will be reset to their factory default values.", "@_copy_to_clipboard": {}, "l_copy_to_clipboard": "Copy to clipboard", diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart index 3a79925f..4399427b 100644 --- a/lib/piv/views/generate_key_dialog.dart +++ b/lib/piv/views/generate_key_dialog.dart @@ -18,7 +18,9 @@ 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 '../../core/models.dart'; import '../../widgets/choice_filter_chip.dart'; import '../../widgets/responsive_dialog.dart'; @@ -46,6 +48,7 @@ class _GenerateKeyDialogState extends ConsumerState { late DateTime _validTo; late DateTime _validToDefault; late DateTime _validToMax; + bool _generating = false; @override void initState() { @@ -61,31 +64,57 @@ class _GenerateKeyDialogState extends ConsumerState { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final navigator = Navigator.of(context); + return ResponsiveDialog( + allowCancel: !_generating, title: Text(l10n.s_generate_key), actions: [ TextButton( key: keys.saveButton, - onPressed: () async { - final result = await ref - .read(pivSlotsProvider(widget.devicePath).notifier) - .generate( - widget.pivSlot.slot, - _keyType, - parameters: switch (_generateType) { - GenerateType.certificate => - PivGenerateParameters.certificate( - subject: _subject, - validFrom: _validFrom, - validTo: _validTo), - GenerateType.csr => - PivGenerateParameters.csr(subject: _subject), - }, - ); + onPressed: _generating || _subject.isEmpty + ? null + : () async { + setState(() { + _generating = true; + }); - navigator.pop(result); - }, + Function()? close; + final PivGenerateResult result; + try { + close = showMessage( + context, + l10n.l_generating_private_key, + duration: const Duration(seconds: 30), + ); + result = await ref + .read(pivSlotsProvider(widget.devicePath).notifier) + .generate( + widget.pivSlot.slot, + _keyType, + parameters: switch (_generateType) { + GenerateType.certificate => + PivGenerateParameters.certificate( + subject: _subject, + validFrom: _validFrom, + validTo: _validTo), + GenerateType.csr => + PivGenerateParameters.csr(subject: _subject), + }, + ); + } finally { + close?.call(); + } + + await ref.read(withContextProvider)( + (context) async { + Navigator.of(context).pop(result); + showMessage( + context, + l10n.s_private_key_generated, + ); + }, + ); + }, child: Text(l10n.s_save), ), ], @@ -102,9 +131,14 @@ class _GenerateKeyDialogState extends ConsumerState { labelText: l10n.s_subject, ), textInputAction: TextInputAction.next, + enabled: !_generating, onChanged: (value) { setState(() { - _subject = value.contains('=') ? value : 'CN=$value'; + if (value.isEmpty) { + _subject = ''; + } else { + _subject = value.contains('=') ? value : 'CN=$value'; + } }); }, ), @@ -118,39 +152,45 @@ class _GenerateKeyDialogState extends ConsumerState { value: _generateType, selected: _generateType != defaultGenerateType, itemBuilder: (value) => Text(value.getDisplayName(l10n)), - onChanged: (value) { - setState(() { - _generateType = value; - }); - }, + onChanged: _generating + ? null + : (value) { + setState(() { + _generateType = value; + }); + }, ), ChoiceFilterChip( items: KeyType.values, value: _keyType, selected: _keyType != defaultKeyType, itemBuilder: (value) => Text(value.getDisplayName(l10n)), - onChanged: (value) { - setState(() { - _keyType = value; - }); - }, + onChanged: _generating + ? null + : (value) { + setState(() { + _keyType = value; + }); + }, ), if (_generateType == GenerateType.certificate) FilterChip( label: Text(dateFormatter.format(_validTo)), - onSelected: (value) async { - final selected = await showDatePicker( - context: context, - initialDate: _validTo, - firstDate: _validFrom, - lastDate: _validToMax, - ); - if (selected != null) { - setState(() { - _validTo = selected; - }); - } - }, + onSelected: _generating + ? null + : (value) async { + final selected = await showDatePicker( + context: context, + initialDate: _validTo, + firstDate: _validFrom, + lastDate: _validToMax, + ); + if (selected != null) { + setState(() { + _validTo = selected; + }); + } + }, ), ]), ] diff --git a/lib/piv/views/key_actions.dart b/lib/piv/views/key_actions.dart index c81232c0..3a2769fd 100644 --- a/lib/piv/views/key_actions.dart +++ b/lib/piv/views/key_actions.dart @@ -30,6 +30,8 @@ import 'reset_dialog.dart'; Widget pivBuildActions(BuildContext context, DevicePath devicePath, PivState pivState, WidgetRef ref) { + final colors = Theme.of(context).buttonTheme.colorScheme ?? + Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final usingDefaultMgmtKey = @@ -87,7 +89,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, : l10n.l_change_management_key), icon: const Icon(Icons.key_outlined), trailing: usingDefaultMgmtKey - ? const Icon(Icons.warning_amber) + ? Icon(Icons.warning_amber, color: colors.tertiary) : null, onTap: (context) { Navigator.of(context).pop(); diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index 64354f20..4c307ef4 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -129,12 +129,13 @@ class _ManageKeyDialogState extends ConsumerState { final currentLenOk = protected ? _currentKeyOrPin.length >= 4 : _currentKeyOrPin.length == currentType.keyLength * 2; + final newLenOk = _keyController.text.length == hexLength; return ResponsiveDialog( title: Text(l10n.l_change_management_key), actions: [ TextButton( - onPressed: _submit, + onPressed: currentLenOk && newLenOk ? _submit : null, key: keys.saveButton, child: Text(l10n.s_save), ) @@ -232,7 +233,7 @@ class _ManageKeyDialogState extends ConsumerState { ), textInputAction: TextInputAction.next, onSubmitted: (_) { - if (_keyController.text.length == hexLength) { + if (currentLenOk && newLenOk) { _submit(); } }, diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart index f61b4024..b6e9cdd5 100644 --- a/lib/piv/views/manage_pin_puk_dialog.dart +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -160,7 +160,9 @@ class _ManagePinPukDialogState extends ConsumerState { autofillHints: const [AutofillHints.newPassword], decoration: InputDecoration( border: const OutlineInputBorder(), - labelText: l10n.s_confirm_pin, + labelText: widget.target == ManageTarget.puk + ? l10n.s_confirm_puk + : l10n.s_confirm_pin, prefixIcon: const Icon(Icons.password_outlined), enabled: _currentPin.length >= 4 && _newPin.length >= 6, ), diff --git a/lib/theme.dart b/lib/theme.dart index 8f99e5ad..77df071b 100755 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -21,6 +21,7 @@ const accentGreen = Color(0xff9aca3c); const primaryBlue = Color(0xff325f74); const primaryRed = Color(0xffea4335); const darkRed = Color(0xffda4d41); +const amber = Color(0xffffca28); class AppTheme { static ThemeData get lightTheme => ThemeData( @@ -32,6 +33,7 @@ class AppTheme { ).copyWith( primary: primaryBlue, //secondary: accentGreen, + tertiary: amber.withOpacity(0.7), ), textTheme: TextTheme( bodySmall: TextStyle(color: Colors.grey.shade900), @@ -57,6 +59,7 @@ class AppTheme { //onPrimaryContainer: Colors.grey.shade100, error: darkRed, onError: Colors.white.withOpacity(0.9), + tertiary: amber.withOpacity(0.7), ), textTheme: TextTheme( bodySmall: TextStyle(color: Colors.grey.shade500), diff --git a/lib/widgets/responsive_dialog.dart b/lib/widgets/responsive_dialog.dart index 047ef6af..0b3d6d17 100755 --- a/lib/widgets/responsive_dialog.dart +++ b/lib/widgets/responsive_dialog.dart @@ -22,13 +22,16 @@ class ResponsiveDialog extends StatefulWidget { final Widget child; final List actions; final Function()? onCancel; + final bool allowCancel; - const ResponsiveDialog( - {super.key, - required this.child, - this.title, - this.actions = const [], - this.onCancel}); + const ResponsiveDialog({ + super.key, + required this.child, + this.title, + this.actions = const [], + this.onCancel, + this.allowCancel = true, + }); @override State createState() => _ResponsiveDialogState(); @@ -47,12 +50,14 @@ class _ResponsiveDialogState extends State { appBar: AppBar( title: widget.title, actions: widget.actions, - leading: CloseButton( - onPressed: () { - widget.onCancel?.call(); - Navigator.of(context).pop(); - }, - ), + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: widget.allowCancel + ? () { + widget.onCancel?.call(); + Navigator.of(context).pop(); + } + : null), ), body: SingleChildScrollView( child: SafeArea( From e42f7e4e67550f63e225b8e856a254293d17435f Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 19 Jun 2023 14:04:58 +0200 Subject: [PATCH 030/158] Prevent underlying page from stealing focus from ResponsiveDialog. --- lib/app/message.dart | 39 ++++------ lib/widgets/responsive_dialog.dart | 113 +++++++++++++++++------------ 2 files changed, 82 insertions(+), 70 deletions(-) diff --git a/lib/app/message.dart b/lib/app/message.dart index 93b47748..db7fcda8 100755 --- a/lib/app/message.dart +++ b/lib/app/message.dart @@ -32,27 +32,20 @@ Future showBlurDialog({ required BuildContext context, required Widget Function(BuildContext) builder, RouteSettings? routeSettings, -}) async { - const transitionDelay = Duration(milliseconds: 150); - final result = await showGeneralDialog( - context: context, - barrierDismissible: true, - barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, - pageBuilder: (ctx, anim1, anim2) => builder(ctx), - transitionDuration: transitionDelay, - transitionBuilder: (ctx, anim1, anim2, child) => BackdropFilter( - filter: - ImageFilter.blur(sigmaX: 20 * anim1.value, sigmaY: 20 * anim1.value), - child: FadeTransition( - opacity: anim1, - child: child, +}) async => + await showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, + pageBuilder: (ctx, anim1, anim2) => builder(ctx), + transitionDuration: const Duration(milliseconds: 150), + transitionBuilder: (ctx, anim1, anim2, child) => BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 20 * anim1.value, sigmaY: 20 * anim1.value), + child: FadeTransition( + opacity: anim1, + child: child, + ), ), - ), - routeSettings: routeSettings, - ); - // Make sure we wait for the dialog to fade out before returning the result. - // This is needed for subsequent dialogs with autofocus. - await Future.delayed(transitionDelay); - - return result; -} + routeSettings: routeSettings, + ); diff --git a/lib/widgets/responsive_dialog.dart b/lib/widgets/responsive_dialog.dart index 0b3d6d17..d5da0006 100755 --- a/lib/widgets/responsive_dialog.dart +++ b/lib/widgets/responsive_dialog.dart @@ -39,56 +39,75 @@ class ResponsiveDialog extends StatefulWidget { class _ResponsiveDialogState extends State { final Key _childKey = GlobalKey(); + final _focus = FocusScopeNode(); + + @override + void dispose() { + super.dispose(); + _focus.dispose(); + } + + Widget _buildFullscreen(BuildContext context) => Scaffold( + appBar: AppBar( + title: widget.title, + actions: widget.actions, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: widget.allowCancel + ? () { + widget.onCancel?.call(); + Navigator.of(context).pop(); + } + : null), + ), + body: SingleChildScrollView( + child: + SafeArea(child: Container(key: _childKey, child: widget.child)), + ), + ); + + Widget _buildDialog(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final cancelText = widget.onCancel == null && widget.actions.isEmpty + ? l10n.s_close + : l10n.s_cancel; + return AlertDialog( + title: widget.title, + titlePadding: const EdgeInsets.only(top: 24, left: 18, right: 18), + scrollable: true, + contentPadding: const EdgeInsets.symmetric(vertical: 8), + content: SizedBox( + width: 380, + child: Container(key: _childKey, child: widget.child), + ), + actions: [ + TextButton( + child: Text(cancelText), + onPressed: () { + widget.onCancel?.call(); + Navigator.of(context).pop(); + }, + ), + ...widget.actions + ], + ); + } @override Widget build(BuildContext context) => LayoutBuilder(builder: ((context, constraints) { - final l10n = AppLocalizations.of(context)!; - if (constraints.maxWidth < 540) { - // Fullscreen - return Scaffold( - appBar: AppBar( - title: widget.title, - actions: widget.actions, - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: widget.allowCancel - ? () { - widget.onCancel?.call(); - Navigator.of(context).pop(); - } - : null), - ), - body: SingleChildScrollView( - child: SafeArea( - child: Container(key: _childKey, child: widget.child)), - ), - ); - } else { - // Dialog - final cancelText = widget.onCancel == null && widget.actions.isEmpty - ? l10n.s_close - : l10n.s_cancel; - return AlertDialog( - title: widget.title, - titlePadding: const EdgeInsets.only(top: 24, left: 18, right: 18), - scrollable: true, - contentPadding: const EdgeInsets.symmetric(vertical: 8), - content: SizedBox( - width: 380, - child: Container(key: _childKey, child: widget.child), - ), - actions: [ - TextButton( - child: Text(cancelText), - onPressed: () { - widget.onCancel?.call(); - Navigator.of(context).pop(); - }, - ), - ...widget.actions - ], - ); - } + // This keeps the focus in the dialog, even if the underlying page changes. + return FocusScope( + node: _focus, + autofocus: true, + onFocusChange: (focused) { + if (!focused) { + _focus.requestFocus(); + } + }, + child: constraints.maxWidth < 540 + ? _buildFullscreen(context) + : _buildDialog(context), + ); })); } From 05220e8089a33b6970e80ab7a601695bde624f10 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 28 Jun 2023 17:14:15 +0200 Subject: [PATCH 031/158] Implement navigation rail with collapsed/expanded views. --- lib/app/shortcuts.dart | 6 +- lib/app/views/app_page.dart | 129 ++++++++-- lib/app/views/device_picker.dart | 402 +++++++++++++++++++++++++++++++ lib/app/views/main_drawer.dart | 147 ----------- lib/app/views/navigation.dart | 213 ++++++++++++++++ 5 files changed, 727 insertions(+), 170 deletions(-) create mode 100644 lib/app/views/device_picker.dart delete mode 100755 lib/app/views/main_drawer.dart create mode 100644 lib/app/views/navigation.dart diff --git a/lib/app/shortcuts.dart b/lib/app/shortcuts.dart index 5c180ff7..4bcd8125 100755 --- a/lib/app/shortcuts.dart +++ b/lib/app/shortcuts.dart @@ -28,6 +28,7 @@ import '../oath/keys.dart'; import 'message.dart'; import 'models.dart'; import 'state.dart'; +import 'views/keys.dart'; import 'views/settings_page.dart'; class OpenIntent extends Intent { @@ -100,7 +101,10 @@ Widget registerGlobalShortcuts( }), NextDeviceIntent: CallbackAction(onInvoke: (_) { ref.read(withContextProvider)((context) async { - if (!Navigator.of(context).canPop()) { + // Only allow switching keys if no other views are open, + // with the exception of the drawer. + if (!Navigator.of(context).canPop() || + scaffoldGlobalKey.currentState?.isDrawerOpen == true) { final attached = ref .read(attachedDevicesProvider) .whereType() diff --git a/lib/app/views/app_page.dart b/lib/app/views/app_page.dart index 2c4f443b..a41423d3 100755 --- a/lib/app/views/app_page.dart +++ b/lib/app/views/app_page.dart @@ -19,9 +19,12 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../widgets/delayed_visibility.dart'; import '../message.dart'; -import 'device_button.dart'; import 'keys.dart'; -import 'main_drawer.dart'; +import 'navigation.dart'; + +// We use global keys here to maintain the NavigatorContent between AppPages. +final _navKey = GlobalKey(); +final _navExpandedKey = GlobalKey(); class AppPage extends StatelessWidget { final Widget? title; @@ -47,26 +50,33 @@ class AppPage extends StatelessWidget { @override Widget build(BuildContext context) => LayoutBuilder( builder: (context, constraints) { - if (constraints.maxWidth < 540) { - // Single column layout - return _buildScaffold(context, true); + if (constraints.maxWidth < 600) { + // Single column layout, maybe with rail + final hasRail = constraints.maxWidth > 400; + return _buildScaffold(context, true, hasRail); } else { - // Two-column layout + // Fully expanded layout return Scaffold( body: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 280, - child: DrawerTheme( - data: DrawerTheme.of(context).copyWith( - // Don't color the drawer differently - surfaceTintColor: Colors.transparent, + child: SingleChildScrollView( + child: Column( + children: [ + _buildLogo(context), + NavigationContent( + key: _navExpandedKey, + shouldPop: false, + extended: true, + ), + ], ), - child: const MainPageDrawer(shouldPop: false), ), ), Expanded( - child: _buildScaffold(context, false), + child: _buildScaffold(context, false, false), ), ], ), @@ -75,7 +85,47 @@ class AppPage extends StatelessWidget { }, ); - Widget _buildScrollView() { + Widget _buildLogo(BuildContext context) { + final color = + Theme.of(context).brightness == Brightness.dark ? 'white' : 'green'; + return Padding( + padding: const EdgeInsets.only(top: 16, bottom: 12), + child: Image.asset( + 'assets/graphics/yubico-$color.png', + alignment: Alignment.centerLeft, + height: 28, + filterQuality: FilterQuality.medium, + ), + ); + } + + Widget _buildDrawer(BuildContext context) { + return Drawer( + child: SingleChildScrollView( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16), + child: DrawerButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + _buildLogo(context), + const SizedBox(width: 48), + ], + ), + NavigationContent(key: _navExpandedKey, extended: true), + ], + ), + )); + } + + Widget _buildMainContent() { final content = Column( children: [ child, @@ -83,8 +133,7 @@ class AppPage extends StatelessWidget { Align( alignment: centered ? Alignment.center : Alignment.centerLeft, child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 16.0, horizontal: 18.0), + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 18), child: Wrap( spacing: 4, runSpacing: 4, @@ -95,6 +144,7 @@ class AppPage extends StatelessWidget { ], ); return SingleChildScrollView( + primary: false, child: SafeArea( child: Center( child: SizedBox( @@ -112,7 +162,27 @@ class AppPage extends StatelessWidget { ); } - Scaffold _buildScaffold(BuildContext context, bool hasDrawer) { + Scaffold _buildScaffold(BuildContext context, bool hasDrawer, bool hasRail) { + var body = + centered ? Center(child: _buildMainContent()) : _buildMainContent(); + if (hasRail) { + body = Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 72, + child: SingleChildScrollView( + child: NavigationContent( + key: _navKey, + shouldPop: false, + extended: false, + ), + ), + ), + Expanded(child: body), + ], + ); + } return Scaffold( key: scaffoldGlobalKey, appBar: AppBar( @@ -120,6 +190,20 @@ class AppPage extends StatelessWidget { titleSpacing: hasDrawer ? 2 : 8, centerTitle: true, titleTextStyle: Theme.of(context).textTheme.titleLarge, + leadingWidth: hasRail ? 84 : null, + leading: hasRail + ? const Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: DrawerButton(), + )), + SizedBox(width: 12), + ], + ) + : null, actions: [ if (actionButtonBuilder == null && keyActionsBuilder != null) Padding( @@ -139,14 +223,15 @@ class AppPage extends StatelessWidget { padding: const EdgeInsets.all(12), ), ), - Padding( - padding: const EdgeInsets.only(right: 12), - child: actionButtonBuilder?.call(context) ?? const DeviceButton(), - ), + if (actionButtonBuilder != null) + Padding( + padding: const EdgeInsets.only(right: 12), + child: actionButtonBuilder!.call(context), + ), ], ), - drawer: hasDrawer ? const MainPageDrawer() : null, - body: centered ? Center(child: _buildScrollView()) : _buildScrollView(), + drawer: hasDrawer ? _buildDrawer(context) : null, + body: body, ); } } diff --git a/lib/app/views/device_picker.dart b/lib/app/views/device_picker.dart new file mode 100644 index 00000000..2a01b491 --- /dev/null +++ b/lib/app/views/device_picker.dart @@ -0,0 +1,402 @@ +/* + * 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'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../../core/state.dart'; +import '../../management/models.dart'; +import '../models.dart'; +import '../state.dart'; +import 'device_avatar.dart'; + +final _hiddenDevicesProvider = + StateNotifierProvider<_HiddenDevicesNotifier, List>( + (ref) => _HiddenDevicesNotifier(ref.watch(prefProvider))); + +class _HiddenDevicesNotifier extends StateNotifier> { + static const String _key = 'DEVICE_PICKER_HIDDEN'; + final SharedPreferences _prefs; + _HiddenDevicesNotifier(this._prefs) : super(_prefs.getStringList(_key) ?? []); + + void showAll() { + state = []; + _prefs.setStringList(_key, state); + } + + void hideDevice(DevicePath devicePath) { + state = [...state, devicePath.key]; + _prefs.setStringList(_key, state); + } +} + +List<(Widget, bool)> buildDeviceList( + BuildContext context, WidgetRef ref, bool extended) { + final l10n = AppLocalizations.of(context)!; + final hidden = ref.watch(_hiddenDevicesProvider); + final devices = ref + .watch(attachedDevicesProvider) + .where((e) => !hidden.contains(e.path.key)) + .toList(); + final currentNode = ref.watch(currentDeviceProvider); + + final showUsb = isDesktop && devices.whereType().isEmpty; + + return [ + if (showUsb) + ( + _DeviceRow( + leading: const DeviceAvatar(child: Icon(Icons.usb)), + title: l10n.s_usb, + subtitle: l10n.l_no_yk_present, + onTap: () { + ref.read(currentDeviceProvider.notifier).setCurrentDevice(null); + }, + selected: currentNode == null, + extended: extended, + ), + currentNode == null + ), + ...devices.map( + (e) => e.path == currentNode?.path + ? ( + _buildCurrentDeviceRow( + context, + ref, + e, + ref.watch(currentDeviceDataProvider), + extended, + ), + true + ) + : ( + e.map( + usbYubiKey: (node) => _buildDeviceRow( + context, + ref, + node, + node.info, + extended, + ), + nfcReader: (node) => _NfcDeviceRow(node, extended: extended), + ), + false + ), + ), + ]; +} + +class DevicePickerContent extends ConsumerWidget { + final bool extended; + const DevicePickerContent({super.key, this.extended = true}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + final hidden = ref.watch(_hiddenDevicesProvider); + final devices = ref + .watch(attachedDevicesProvider) + .where((e) => !hidden.contains(e.path.key)) + .toList(); + final currentNode = ref.watch(currentDeviceProvider); + + final showUsb = isDesktop && devices.whereType().isEmpty; + + List children = [ + if (showUsb) + _DeviceRow( + leading: const DeviceAvatar(child: Icon(Icons.usb)), + title: l10n.s_usb, + subtitle: l10n.l_no_yk_present, + onTap: () { + ref.read(currentDeviceProvider.notifier).setCurrentDevice(null); + }, + selected: currentNode == null, + extended: extended, + ), + ...devices.map( + (e) => e.path == currentNode?.path + ? _buildCurrentDeviceRow( + context, + ref, + e, + ref.watch(currentDeviceDataProvider), + extended, + ) + : e.map( + usbYubiKey: (node) => _buildDeviceRow( + context, + ref, + node, + node.info, + extended, + ), + nfcReader: (node) => _NfcDeviceRow(node, extended: extended), + ), + ), + ]; + + return GestureDetector( + onSecondaryTapDown: hidden.isEmpty + ? null + : (details) { + showMenu( + context: context, + position: RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx, + 0, + ), + items: [ + PopupMenuItem( + onTap: () { + ref.read(_hiddenDevicesProvider.notifier).showAll(); + }, + child: ListTile( + title: Text(l10n.s_show_hidden_devices), + dense: true, + contentPadding: EdgeInsets.zero, + ), + ), + ], + ); + }, + child: Column( + children: children, + ), + ); + } +} + +String _getDeviceInfoString(BuildContext context, DeviceInfo info) { + final l10n = AppLocalizations.of(context)!; + final serial = info.serial; + return [ + if (serial != null) l10n.s_sn_serial(serial), + if (info.version.isAtLeast(1)) + l10n.s_fw_version(info.version) + else + l10n.s_unknown_type, + ].join(' '); +} + +List _getDeviceStrings( + BuildContext context, DeviceNode node, AsyncValue data) { + final l10n = AppLocalizations.of(context)!; + final messages = data.whenOrNull( + data: (data) => [data.name, _getDeviceInfoString(context, data.info)], + error: (error, _) => switch (error) { + 'device-inaccessible' => [node.name, l10n.s_yk_inaccessible], + 'unknown-device' => [l10n.s_unknown_device], + _ => null, + }, + ) ?? + [l10n.l_no_yk_present]; + + // Add the NFC reader name, unless it's already included (as device name, like on Android) + if (node is NfcReaderNode && !messages.contains(node.name)) { + messages.add(node.name); + } + + return messages; +} + +class _DeviceRow extends StatelessWidget { + final Widget leading; + final String title; + final String subtitle; + final bool extended; + final bool selected; + final void Function() onTap; + + const _DeviceRow({ + super.key, + required this.leading, + required this.title, + required this.subtitle, + required this.extended, + required this.selected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final tooltip = '$title\n$subtitle'; + if (extended) { + final colorScheme = Theme.of(context).colorScheme; + return Tooltip( + message: tooltip, + child: ListTile( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(48)), + contentPadding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 0), + horizontalTitleGap: 8, + leading: IconTheme( + // Force the standard icon theme + data: IconTheme.of(context), + child: leading, + ), + title: Text(title, overflow: TextOverflow.fade, softWrap: false), + subtitle: Text(subtitle), + dense: true, + tileColor: selected ? colorScheme.primary : null, + textColor: selected ? colorScheme.onPrimary : null, + onTap: onTap, + ), + ); + } else { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.5), + child: selected + ? IconButton.filled( + tooltip: tooltip, + icon: IconTheme( + // Force the standard icon theme + data: IconTheme.of(context), + child: leading, + ), + padding: const EdgeInsets.symmetric(horizontal: 12), + onPressed: onTap, + ) + : IconButton( + tooltip: tooltip, + icon: IconTheme( + // Force the standard icon theme + data: IconTheme.of(context), + child: leading, + ), + padding: const EdgeInsets.symmetric(horizontal: 8), + onPressed: onTap, + ), + ); + } + } +} + +_DeviceRow _buildDeviceRow( + BuildContext context, + WidgetRef ref, + DeviceNode node, + DeviceInfo? info, + bool extended, +) { + final l10n = AppLocalizations.of(context)!; + final subtitle = node.when( + usbYubiKey: (_, __, ___, info) => info == null + ? l10n.s_yk_inaccessible + : _getDeviceInfoString(context, info), + nfcReader: (_, __) => l10n.s_select_to_scan, + ); + return _DeviceRow( + key: ValueKey(node.path.key), + leading: IconTheme( + // Force the standard icon theme + data: IconTheme.of(context), + child: DeviceAvatar.deviceNode(node), + ), + title: node.name, + subtitle: subtitle, + extended: extended, + selected: false, + onTap: () { + ref.read(currentDeviceProvider.notifier).setCurrentDevice(node); + }, + ); +} + +_DeviceRow _buildCurrentDeviceRow( + BuildContext context, + WidgetRef ref, + DeviceNode node, + AsyncValue data, + bool extended, +) { + final messages = _getDeviceStrings(context, node, data); + if (messages.length > 2) { + // Don't show readername + messages.removeLast(); + } + final title = messages.removeAt(0); + final subtitle = messages.join('\n'); + + return _DeviceRow( + leading: data.maybeWhen( + data: (data) => + DeviceAvatar.yubiKeyData(data, radius: extended ? null : 16), + orElse: () => DeviceAvatar.deviceNode(node, radius: extended ? null : 16), + ), + title: title, + subtitle: subtitle, + extended: extended, + selected: true, + onTap: () {}, + ); +} + +class _NfcDeviceRow extends ConsumerWidget { + final DeviceNode node; + final bool extended; + + const _NfcDeviceRow(this.node, {required this.extended}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + final hidden = ref.watch(_hiddenDevicesProvider); + return GestureDetector( + onSecondaryTapDown: (details) { + showMenu( + context: context, + position: RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx, + 0, + ), + items: [ + PopupMenuItem( + enabled: hidden.isNotEmpty, + onTap: () { + ref.read(_hiddenDevicesProvider.notifier).showAll(); + }, + child: ListTile( + title: Text(l10n.s_show_hidden_devices), + dense: true, + contentPadding: EdgeInsets.zero, + enabled: hidden.isNotEmpty, + ), + ), + PopupMenuItem( + onTap: () { + ref.read(_hiddenDevicesProvider.notifier).hideDevice(node.path); + }, + child: ListTile( + title: Text(l10n.s_hide_device), + dense: true, + contentPadding: EdgeInsets.zero, + ), + ), + ], + ); + }, + child: _buildDeviceRow(context, ref, node, null, extended), + ); + } +} diff --git a/lib/app/views/main_drawer.dart b/lib/app/views/main_drawer.dart deleted file mode 100755 index 4064be04..00000000 --- a/lib/app/views/main_drawer.dart +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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 '../../management/views/management_screen.dart'; -import '../message.dart'; -import '../models.dart'; -import '../shortcuts.dart'; -import '../state.dart'; -import 'keys.dart'; - -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.piv => Icons.approval_outlined, - Application.management => Icons.construction_outlined, - Application.openpgp => Icons.key_outlined, - Application.hsmauth => Icons.key_outlined, - }; - - IconData get _filledIcon => switch (this) { - Application.oath => Icons.supervisor_account, - Application.fido => Icons.security, - Application.otp => Icons.password, - Application.piv => Icons.approval, - Application.management => Icons.construction, - Application.openpgp => Icons.key, - Application.hsmauth => Icons.key, - }; -} - -class MainPageDrawer extends ConsumerWidget { - final bool shouldPop; - const MainPageDrawer({this.shouldPop = true, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final l10n = AppLocalizations.of(context)!; - final supportedApps = ref.watch(supportedAppsProvider); - final data = ref.watch(currentDeviceDataProvider).valueOrNull; - final color = - Theme.of(context).brightness == Brightness.dark ? 'white' : 'green'; - - final availableApps = data != null - ? supportedApps - .where( - (app) => app.getAvailability(data) != Availability.unsupported) - .toList() - : []; - final hasManagement = availableApps.remove(Application.management); - - return NavigationDrawer( - selectedIndex: availableApps.indexOf(ref.watch(currentAppProvider)), - onDestinationSelected: (index) { - if (shouldPop) Navigator.of(context).pop(); - - if (index < availableApps.length) { - // Switch to selected app - final app = availableApps[index]; - ref.read(currentAppProvider.notifier).setCurrentApp(app); - } else { - // Handle action - index -= availableApps.length; - - if (!hasManagement) { - index++; - } - - switch (index) { - case 0: - showBlurDialog( - context: context, - // data must be non-null when index == 0 - builder: (context) => ManagementScreen(data!), - ); - break; - case 1: - Actions.maybeInvoke(context, const SettingsIntent()); - break; - case 2: - Actions.maybeInvoke(context, const AboutIntent()); - break; - } - } - }, - children: [ - Padding( - padding: const EdgeInsets.only(top: 19.0, left: 30.0, bottom: 12.0), - child: Image.asset( - 'assets/graphics/yubico-$color.png', - alignment: Alignment.centerLeft, - height: 28, - filterQuality: FilterQuality.medium, - ), - ), - const Divider(indent: 16.0, endIndent: 28.0), - if (data != null) ...[ - // Normal YubiKey Applications - ...availableApps.map((app) => NavigationDrawerDestination( - label: Text(app.getDisplayName(l10n)), - icon: Icon(app._icon), - selectedIcon: Icon(app._filledIcon), - )), - // Management app - if (hasManagement) ...[ - NavigationDrawerDestination( - key: managementAppDrawer, - label: Text( - l10n.s_toggle_applications, - ), - icon: Icon(Application.management._icon), - selectedIcon: Icon(Application.management._filledIcon), - ), - ], - const Divider(indent: 16.0, endIndent: 28.0), - ], - // Non-YubiKey pages - NavigationDrawerDestination( - label: Text(l10n.s_settings), - icon: const Icon(Icons.settings_outlined), - ), - NavigationDrawerDestination( - label: Text(l10n.s_help_and_about), - icon: const Icon(Icons.help_outline), - ), - ], - ); - } -} diff --git a/lib/app/views/navigation.dart b/lib/app/views/navigation.dart new file mode 100644 index 00000000..3543d451 --- /dev/null +++ b/lib/app/views/navigation.dart @@ -0,0 +1,213 @@ +/* + * 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 '../../management/views/management_screen.dart'; +import '../message.dart'; +import '../models.dart'; +import '../shortcuts.dart'; +import '../state.dart'; +import 'device_picker.dart'; +import 'keys.dart'; + +class NavigationItem extends StatelessWidget { + final Widget leading; + final String title; + final bool collapsed; + final bool selected; + final void Function() onTap; + + const NavigationItem({ + super.key, + required this.leading, + required this.title, + this.collapsed = false, + this.selected = false, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + if (collapsed) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: selected + ? Theme( + data: theme.copyWith( + colorScheme: colorScheme.copyWith( + primary: colorScheme.secondaryContainer, + onPrimary: colorScheme.onSecondaryContainer)), + child: IconButton.filled( + icon: leading, + tooltip: title, + padding: const EdgeInsets.symmetric(horizontal: 16), + onPressed: onTap, + ), + ) + : IconButton( + icon: leading, + tooltip: title, + padding: const EdgeInsets.symmetric(horizontal: 16), + onPressed: onTap, + ), + ); + } else { + return ListTile( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(48)), + leading: leading, + title: Text(title), + minVerticalPadding: 16, + onTap: onTap, + tileColor: selected ? colorScheme.secondaryContainer : null, + textColor: selected ? colorScheme.onSecondaryContainer : null, + iconColor: selected ? colorScheme.onSecondaryContainer : null, + ); + } + } +} + +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.piv => Icons.approval_outlined, + Application.management => Icons.construction_outlined, + Application.openpgp => Icons.key_outlined, + Application.hsmauth => Icons.key_outlined, + }; + + IconData get _filledIcon => switch (this) { + Application.oath => Icons.supervisor_account, + Application.fido => Icons.security, + Application.otp => Icons.password, + Application.piv => Icons.approval, + Application.management => Icons.construction, + Application.openpgp => Icons.key, + Application.hsmauth => Icons.key, + }; +} + +class NavigationContent extends ConsumerWidget { + final bool shouldPop; + final bool extended; + const NavigationContent( + {super.key, this.shouldPop = true, this.extended = false}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + final supportedApps = ref.watch(supportedAppsProvider); + final data = ref.watch(currentDeviceDataProvider).valueOrNull; + + final availableApps = data != null + ? supportedApps + .where( + (app) => app.getAvailability(data) != Availability.unsupported) + .toList() + : []; + final hasManagement = availableApps.remove(Application.management); + final currentApp = ref.watch(currentAppProvider); + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + AnimatedSize( + duration: const Duration(milliseconds: 150), + child: DevicePickerContent(extended: extended), + ), + + const SizedBox(height: 32), + + AnimatedSize( + duration: const Duration(milliseconds: 150), + child: Column( + children: [ + if (data != null) ...[ + // Normal YubiKey Applications + ...availableApps.map((app) => NavigationItem( + title: app.getDisplayName(l10n), + leading: app == currentApp + ? Icon(app._filledIcon) + : Icon(app._icon), + collapsed: !extended, + selected: app == currentApp, + onTap: () { + ref + .read(currentAppProvider.notifier) + .setCurrentApp(app); + if (shouldPop) { + Navigator.of(context).pop(); + } + }, + )), + // Management app + if (hasManagement) ...[ + NavigationItem( + key: managementAppDrawer, + leading: Icon(Application.management._icon), + title: l10n.s_toggle_applications, + collapsed: !extended, + onTap: () { + showBlurDialog( + context: context, + // data must be non-null when index == 0 + builder: (context) => ManagementScreen(data), + ); + }, + ), + ], + const SizedBox(height: 32), + ], + ], + ), + ), + + // Non-YubiKey pages + NavigationItem( + leading: const Icon(Icons.settings_outlined), + title: l10n.s_settings, + collapsed: !extended, + onTap: () { + if (shouldPop) { + Navigator.of(context).pop(); + } + Actions.maybeInvoke(context, const SettingsIntent()); + }, + ), + NavigationItem( + leading: const Icon(Icons.help_outline), + title: l10n.s_help_and_about, + collapsed: !extended, + onTap: () { + if (shouldPop) { + Navigator.of(context).pop(); + } + Actions.maybeInvoke(context, const AboutIntent()); + }, + ), + ], + ), + ); + } +} From d49d57abe7cb9821e96a58b1c99e350384a92bd8 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 3 Jul 2023 11:27:25 +0200 Subject: [PATCH 032/158] Fix integration tests. --- integration_test/management_test.dart | 3 +- integration_test/utils/android/util.dart | 3 +- integration_test/utils/oath_test_util.dart | 9 +- integration_test/utils/test_util.dart | 28 +- lib/app/views/device_button.dart | 62 ---- lib/app/views/device_picker.dart | 2 + lib/app/views/device_picker_dialog.dart | 362 --------------------- lib/app/views/fs_dialog.dart | 2 + lib/app/views/keys.dart | 3 + 9 files changed, 29 insertions(+), 445 deletions(-) delete mode 100755 lib/app/views/device_button.dart delete mode 100755 lib/app/views/device_picker_dialog.dart diff --git a/integration_test/management_test.dart b/integration_test/management_test.dart index 6b16a3be..f152c640 100644 --- a/integration_test/management_test.dart +++ b/integration_test/management_test.dart @@ -37,7 +37,8 @@ void main() { group('Management UI tests', () { appTest('Drawer items exist', (WidgetTester tester) async { await tester.openDrawer(); - expect(find.byKey(app_keys.managementAppDrawer), findsOneWidget); + expect(find.byKey(app_keys.managementAppDrawer).hitTestable(), + findsOneWidget); }); }); diff --git a/integration_test/utils/android/util.dart b/integration_test/utils/android/util.dart index 8a0f6963..a7078731 100644 --- a/integration_test/utils/android/util.dart +++ b/integration_test/utils/android/util.dart @@ -30,9 +30,10 @@ Future startUp(WidgetTester tester, // only wait for yubikey connection when needed // needs_yubikey defaults to true if (startUpParams['needs_yubikey'] != false) { + await tester.openDrawer(); // wait for a YubiKey connection await tester.waitForFinder(find.descendant( - of: tester.findDeviceButton(), + of: find.byKey(app_keys.deviceInfoListTile), matching: find.byWidgetPredicate((widget) => widget is DeviceAvatar && widget.key != app_keys.noDeviceAvatar))); } diff --git a/integration_test/utils/oath_test_util.dart b/integration_test/utils/oath_test_util.dart index 4847524b..aee4f5c8 100644 --- a/integration_test/utils/oath_test_util.dart +++ b/integration_test/utils/oath_test_util.dart @@ -17,6 +17,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:yubico_authenticator/core/state.dart'; +import 'package:yubico_authenticator/app/views/keys.dart' as app_keys; import 'package:yubico_authenticator/oath/keys.dart' as keys; import 'package:yubico_authenticator/oath/views/account_list.dart'; import 'package:yubico_authenticator/oath/views/account_view.dart'; @@ -235,8 +236,12 @@ extension OathFunctions on WidgetTester { /// now the account dialog is shown /// TODO verify it shows correct issuer and name - /// close the account dialog by tapping out of it - await tapAt(const Offset(10, 10)); + /// close the account dialog by tapping the close button + var closeButton = find.byKey(app_keys.closeButton).hitTestable(); + // Wait for toast to clear + await waitForFinder(closeButton); + + await tap(closeButton); await longWait(); /// verify accounts in the list diff --git a/integration_test/utils/test_util.dart b/integration_test/utils/test_util.dart index 714f79ff..275475c7 100644 --- a/integration_test/utils/test_util.dart +++ b/integration_test/utils/test_util.dart @@ -17,7 +17,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:yubico_authenticator/app/views/device_button.dart'; import 'package:yubico_authenticator/app/views/keys.dart' as app_keys; import 'package:yubico_authenticator/app/views/keys.dart'; import 'package:yubico_authenticator/core/state.dart'; @@ -65,16 +64,6 @@ extension AppWidgetTester on WidgetTester { return f; } - Finder findDeviceButton() { - return find.byType(DeviceButton).hitTestable(); - } - - /// Taps the device button - Future tapDeviceButton() async { - await tap(findDeviceButton()); - await pump(const Duration(milliseconds: 500)); - } - Finder findActionIconButton() { return find.byKey(actionsIconButtonKey).hitTestable(); } @@ -119,7 +108,7 @@ extension AppWidgetTester on WidgetTester { await openDrawer(); } - await tap(find.byKey(managementAppDrawer)); + await tap(find.byKey(managementAppDrawer).hitTestable()); await pump(const Duration(milliseconds: 500)); expect(find.byKey(screenKey), findsOneWidget); @@ -153,17 +142,22 @@ extension AppWidgetTester on WidgetTester { return; } - await tapDeviceButton(); + await openDrawer(); var deviceInfo = find.byKey(app_keys.deviceInfoListTile); if (deviceInfo.evaluate().isNotEmpty) { - ListTile lt = deviceInfo.evaluate().single.widget as ListTile; + ListTile lt = find + .descendant(of: deviceInfo, matching: find.byType(ListTile)) + .evaluate() + .single + .widget as ListTile; + //ListTile lt = deviceInfo.evaluate().single.widget as ListTile; yubiKeyName = (lt.title as Text).data; var subtitle = (lt.subtitle as Text?)?.data; if (subtitle != null) { - RegExpMatch? match = RegExp(r'S/N: (\d.*) F/W: (\d\.\d\.\d)') - .firstMatch(subtitle); + RegExpMatch? match = + RegExp(r'S/N: (\d.*) F/W: (\d\.\d\.\d)').firstMatch(subtitle); if (match != null) { yubiKeySerialNumber = match.group(1); yubiKeyFirmware = match.group(2); @@ -177,7 +171,7 @@ extension AppWidgetTester on WidgetTester { } // close the opened menu - await tapTopLeftCorner(); + await closeDrawer(); testLog(false, 'Connected YubiKey: $yubiKeySerialNumber/$yubiKeyFirmware - $yubiKeyName'); diff --git a/lib/app/views/device_button.dart b/lib/app/views/device_button.dart deleted file mode 100755 index 50772ce4..00000000 --- a/lib/app/views/device_button.dart +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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_riverpod/flutter_riverpod.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import '../../core/state.dart'; -import '../message.dart'; -import 'device_avatar.dart'; -import 'device_picker_dialog.dart'; - -class _CircledDeviceAvatar extends ConsumerWidget { - final double radius; - const _CircledDeviceAvatar(this.radius); - - @override - Widget build(BuildContext context, WidgetRef ref) => CircleAvatar( - radius: radius, - backgroundColor: Theme.of(context).colorScheme.primary, - child: IconTheme( - // Force the standard icon theme - data: IconTheme.of(context), - child: DeviceAvatar.currentDevice(ref, radius: radius - 1), - ), - ); -} - -class DeviceButton extends ConsumerWidget { - final double radius; - const DeviceButton({super.key, this.radius = 16}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return IconButton( - tooltip: isAndroid - ? AppLocalizations.of(context)!.s_yk_information - : AppLocalizations.of(context)!.s_select_yk, - icon: _CircledDeviceAvatar(radius), - onPressed: () async { - await showBlurDialog( - context: context, - builder: (context) => const DevicePickerDialog(), - routeSettings: const RouteSettings(name: 'device_picker'), - ); - }, - ); - } -} diff --git a/lib/app/views/device_picker.dart b/lib/app/views/device_picker.dart index 2a01b491..6a7a2974 100644 --- a/lib/app/views/device_picker.dart +++ b/lib/app/views/device_picker.dart @@ -24,6 +24,7 @@ import '../../management/models.dart'; import '../models.dart'; import '../state.dart'; import 'device_avatar.dart'; +import 'keys.dart' as keys; final _hiddenDevicesProvider = StateNotifierProvider<_HiddenDevicesNotifier, List>( @@ -337,6 +338,7 @@ _DeviceRow _buildCurrentDeviceRow( final subtitle = messages.join('\n'); return _DeviceRow( + key: keys.deviceInfoListTile, leading: data.maybeWhen( data: (data) => DeviceAvatar.yubiKeyData(data, radius: extended ? null : 16), diff --git a/lib/app/views/device_picker_dialog.dart b/lib/app/views/device_picker_dialog.dart deleted file mode 100755 index 454d97c7..00000000 --- a/lib/app/views/device_picker_dialog.dart +++ /dev/null @@ -1,362 +0,0 @@ -/* - * 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_riverpod/flutter_riverpod.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import '../../core/state.dart'; -import '../../management/models.dart'; -import '../models.dart'; -import '../state.dart'; -import 'device_avatar.dart'; -import 'keys.dart'; - -final _hiddenDevicesProvider = - StateNotifierProvider<_HiddenDevicesNotifier, List>( - (ref) => _HiddenDevicesNotifier(ref.watch(prefProvider))); - -class _HiddenDevicesNotifier extends StateNotifier> { - static const String _key = 'DEVICE_PICKER_HIDDEN'; - final SharedPreferences _prefs; - _HiddenDevicesNotifier(this._prefs) : super(_prefs.getStringList(_key) ?? []); - - void showAll() { - state = []; - _prefs.setStringList(_key, state); - } - - void hideDevice(DevicePath devicePath) { - state = [...state, devicePath.key]; - _prefs.setStringList(_key, state); - } -} - -class DevicePickerDialog extends StatefulWidget { - const DevicePickerDialog({super.key}); - - @override - State createState() => _DevicePickerDialogState(); -} - -class _DevicePickerDialogState extends State { - late FocusScopeNode _focus; - - @override - void initState() { - super.initState(); - _focus = FocusScopeNode(); - } - - @override - void dispose() { - _focus.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - // This keeps the focus in the dialog, even if the underlying page - // changes as it does when a new device is selected. - return FocusScope( - node: _focus, - autofocus: true, - onFocusChange: (focused) { - if (!focused) { - _focus.requestFocus(); - } - }, - child: const _DevicePickerContent(), - ); - } -} - -class _DevicePickerContent extends ConsumerWidget { - const _DevicePickerContent(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final l10n = AppLocalizations.of(context)!; - final hidden = ref.watch(_hiddenDevicesProvider); - final devices = ref - .watch(attachedDevicesProvider) - .where((e) => !hidden.contains(e.path.key)) - .toList(); - final currentNode = ref.watch(currentDeviceProvider); - - final Widget hero; - final bool showUsb; - if (currentNode != null) { - showUsb = isDesktop && devices.whereType().isEmpty; - devices.removeWhere((e) => e.path == currentNode.path); - hero = _CurrentDeviceRow( - currentNode, - ref.watch(currentDeviceDataProvider), - ); - } else { - hero = Column( - children: [ - _HeroAvatar( - child: DeviceAvatar( - radius: 64, - child: Icon(isAndroid ? Icons.no_cell : Icons.usb), - ), - ), - ListTile( - title: Center(child: Text(l10n.l_no_yk_present)), - subtitle: Center( - child: Text(isAndroid ? l10n.l_insert_or_tap_yk : l10n.s_usb)), - ), - ], - ); - showUsb = false; - } - - List others = [ - if (showUsb) - ListTile( - leading: const Padding( - padding: EdgeInsets.symmetric(horizontal: 4), - child: DeviceAvatar(child: Icon(Icons.usb)), - ), - title: Text(l10n.s_usb), - subtitle: Text(l10n.l_no_yk_present), - onTap: () { - ref.read(currentDeviceProvider.notifier).setCurrentDevice(null); - }, - ), - ...devices.map( - (e) => e.map( - usbYubiKey: (node) => _DeviceRow(node, info: node.info), - nfcReader: (node) => _NfcDeviceRow(node), - ), - ), - ]; - - return GestureDetector( - onSecondaryTapDown: hidden.isEmpty - ? null - : (details) { - showMenu( - context: context, - position: RelativeRect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx, - 0, - ), - items: [ - PopupMenuItem( - onTap: () { - ref.read(_hiddenDevicesProvider.notifier).showAll(); - }, - child: ListTile( - title: Text(l10n.s_show_hidden_devices), - dense: true, - contentPadding: EdgeInsets.zero, - ), - ), - ], - ); - }, - child: SimpleDialog( - children: [ - hero, - if (others.isNotEmpty) - const Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Divider(), - ), - ...others, - ], - ), - ); - } -} - -String _getDeviceInfoString(BuildContext context, DeviceInfo info) { - final l10n = AppLocalizations.of(context)!; - final serial = info.serial; - return [ - if (serial != null) l10n.s_sn_serial(serial), - if (info.version.isAtLeast(1)) - l10n.s_fw_version(info.version) - else - l10n.s_unknown_type, - ].join(' '); -} - -List _getDeviceStrings( - BuildContext context, DeviceNode node, AsyncValue data) { - final l10n = AppLocalizations.of(context)!; - final messages = data.whenOrNull( - data: (data) => [data.name, _getDeviceInfoString(context, data.info)], - error: (error, _) => switch (error) { - 'device-inaccessible' => [node.name, l10n.s_yk_inaccessible], - 'unknown-device' => [l10n.s_unknown_device], - _ => null, - }, - ) ?? - [l10n.l_no_yk_present]; - - // Add the NFC reader name, unless it's already included (as device name, like on Android) - if (node is NfcReaderNode && !messages.contains(node.name)) { - messages.add(node.name); - } - - return messages; -} - -class _HeroAvatar extends StatelessWidget { - final Widget child; - const _HeroAvatar({required this.child}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - theme.colorScheme.inverseSurface.withOpacity(0.6), - theme.colorScheme.inverseSurface.withOpacity(0.25), - (DialogTheme.of(context).backgroundColor ?? - theme.dialogBackgroundColor) - .withOpacity(0), - ], - ), - ), - padding: const EdgeInsets.all(12), - child: Theme( - // Give the avatar a transparent background - data: theme.copyWith( - colorScheme: - theme.colorScheme.copyWith(surfaceVariant: Colors.transparent)), - child: child, - ), - ); - } -} - -class _CurrentDeviceRow extends StatelessWidget { - final DeviceNode node; - final AsyncValue data; - - const _CurrentDeviceRow(this.node, this.data); - - @override - Widget build(BuildContext context) { - final hero = data.maybeWhen( - data: (data) => DeviceAvatar.yubiKeyData(data, radius: 64), - orElse: () => DeviceAvatar.deviceNode(node, radius: 64), - ); - final messages = _getDeviceStrings(context, node, data); - - return Column( - children: [ - _HeroAvatar(child: hero), - ListTile( - key: deviceInfoListTile, - title: Text(messages.removeAt(0), textAlign: TextAlign.center), - isThreeLine: messages.length > 1, - subtitle: Text(messages.join('\n'), textAlign: TextAlign.center), - ) - ], - ); - } -} - -class _DeviceRow extends ConsumerWidget { - final DeviceNode node; - final DeviceInfo? info; - - const _DeviceRow(this.node, {this.info}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final l10n = AppLocalizations.of(context)!; - return ListTile( - leading: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: DeviceAvatar.deviceNode(node), - ), - title: Text(node.name), - subtitle: Text( - node.when( - usbYubiKey: (_, __, ___, info) => info == null - ? l10n.s_yk_inaccessible - : _getDeviceInfoString(context, info), - nfcReader: (_, __) => l10n.s_select_to_scan, - ), - ), - onTap: () { - ref.read(currentDeviceProvider.notifier).setCurrentDevice(node); - }, - ); - } -} - -class _NfcDeviceRow extends ConsumerWidget { - final DeviceNode node; - - const _NfcDeviceRow(this.node); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final l10n = AppLocalizations.of(context)!; - final hidden = ref.watch(_hiddenDevicesProvider); - return GestureDetector( - onSecondaryTapDown: (details) { - showMenu( - context: context, - position: RelativeRect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx, - 0, - ), - items: [ - PopupMenuItem( - enabled: hidden.isNotEmpty, - onTap: () { - ref.read(_hiddenDevicesProvider.notifier).showAll(); - }, - child: ListTile( - title: Text(l10n.s_show_hidden_devices), - dense: true, - contentPadding: EdgeInsets.zero, - enabled: hidden.isNotEmpty, - ), - ), - PopupMenuItem( - onTap: () { - ref.read(_hiddenDevicesProvider.notifier).hideDevice(node.path); - }, - child: ListTile( - title: Text(l10n.s_hide_device), - dense: true, - contentPadding: EdgeInsets.zero, - ), - ), - ], - ); - }, - child: _DeviceRow(node), - ); - } -} diff --git a/lib/app/views/fs_dialog.dart b/lib/app/views/fs_dialog.dart index 0aaa45de..30d24928 100644 --- a/lib/app/views/fs_dialog.dart +++ b/lib/app/views/fs_dialog.dart @@ -16,6 +16,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'keys.dart' as keys; class FsDialog extends StatelessWidget { final Widget child; @@ -35,6 +36,7 @@ class FsDialog extends StatelessWidget { Padding( padding: const EdgeInsets.only(bottom: 16.0), child: TextButton.icon( + key: keys.closeButton, icon: const Icon(Icons.close), label: Text(l10n.s_close), onPressed: () { diff --git a/lib/app/views/keys.dart b/lib/app/views/keys.dart index b69b9d2b..06c29fb1 100644 --- a/lib/app/views/keys.dart +++ b/lib/app/views/keys.dart @@ -30,3 +30,6 @@ const managementAppDrawer = Key('$_prefix.drawer.management'); // settings page const themeModeSetting = Key('$_prefix.settings.theme_mode'); Key themeModeOption(ThemeMode mode) => Key('$_prefix.theme_mode.${mode.name}'); + +// misc buttons +const closeButton = Key('$_prefix.close_button'); From 3d330e6d35926a403e722e8a092a07dd1ff654fd Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 4 Jul 2023 14:18:35 +0200 Subject: [PATCH 033/158] Fix NFC reader name overflow. --- lib/app/views/device_picker.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/app/views/device_picker.dart b/lib/app/views/device_picker.dart index 6a7a2974..dc4331eb 100644 --- a/lib/app/views/device_picker.dart +++ b/lib/app/views/device_picker.dart @@ -255,7 +255,8 @@ class _DeviceRow extends StatelessWidget { child: leading, ), title: Text(title, overflow: TextOverflow.fade, softWrap: false), - subtitle: Text(subtitle), + subtitle: + Text(subtitle, overflow: TextOverflow.fade, softWrap: false), dense: true, tileColor: selected ? colorScheme.primary : null, textColor: selected ? colorScheme.onPrimary : null, From fd606fa788e8f809780ec5521fead453a97a8b0c Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 5 Jul 2023 16:07:11 +0200 Subject: [PATCH 034/158] Don't show nav rail on phones. --- lib/app/views/app_page.dart | 55 +++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/lib/app/views/app_page.dart b/lib/app/views/app_page.dart index a41423d3..b8ad31f8 100755 --- a/lib/app/views/app_page.dart +++ b/lib/app/views/app_page.dart @@ -16,6 +16,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:yubico_authenticator/core/state.dart'; import '../../widgets/delayed_visibility.dart'; import '../message.dart'; @@ -50,9 +51,19 @@ class AppPage extends StatelessWidget { @override Widget build(BuildContext context) => LayoutBuilder( builder: (context, constraints) { - if (constraints.maxWidth < 600) { + final bool singleColumn; + final bool hasRail; + if (isAndroid) { + final isPortrait = constraints.maxWidth < constraints.maxHeight; + singleColumn = isPortrait || constraints.maxWidth < 600; + hasRail = constraints.maxWidth > 600; + } else { + singleColumn = constraints.maxWidth < 600; + hasRail = constraints.maxWidth > 400; + } + + if (singleColumn) { // Single column layout, maybe with rail - final hasRail = constraints.maxWidth > 400; return _buildScaffold(context, true, hasRail); } else { // Fully expanded layout @@ -101,26 +112,28 @@ class AppPage extends StatelessWidget { Widget _buildDrawer(BuildContext context) { return Drawer( - child: SingleChildScrollView( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16), - child: DrawerButton( - onPressed: () { - Navigator.of(context).pop(); - }, + child: SafeArea( + child: SingleChildScrollView( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16), + child: DrawerButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), ), - ), - _buildLogo(context), - const SizedBox(width: 48), - ], - ), - NavigationContent(key: _navExpandedKey, extended: true), - ], + _buildLogo(context), + const SizedBox(width: 48), + ], + ), + NavigationContent(key: _navExpandedKey, extended: true), + ], + ), ), )); } From 9ec23c3443d2e64663ead137beb58d8107391220 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 5 Jul 2023 17:03:29 +0200 Subject: [PATCH 035/158] Enable /GUARD:CF during Windows compilation. --- windows/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index c0fd6ece..2fc55831 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -39,6 +39,8 @@ function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_options(${TARGET} PRIVATE /GUARD:CF) + target_link_options(${TARGET} PRIVATE /GUARD:CF) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() From ba762a926d6ba5d16ba5a325a1afbf4ce77c7eeb Mon Sep 17 00:00:00 2001 From: JulienDeveaux Date: Fri, 7 Jul 2023 13:31:21 +0200 Subject: [PATCH 036/158] Add french translation --- lib/l10n/app_fr.arb | 588 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 588 insertions(+) create mode 100644 lib/l10n/app_fr.arb diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb new file mode 100644 index 00000000..25e1bb81 --- /dev/null +++ b/lib/l10n/app_fr.arb @@ -0,0 +1,588 @@ +{ + "@@locale": "fr", + + "@_readme": { + "notes": [ + "Toutes les chaînes de caractères commencent par une lettre majuscule.", + "Regroupez les chaînes de caractères par catégories, mais ne les liez pas inutilement à une section de l'application si elles peuvent être réutilisées entre plusieurs.", + "Exécutez check_strings.py sur le fichier .arb pour détecter les problèmes et ajustez @_lint_rules si nécessaire par langue." + ], + "prefixes": { + "s_": "Un seul ou peu de mots. Doit être assez court pour être affiché sur un bouton ou un entête.", + "l_": "Une seule ligne, peut être enroulée. Ne doit pas être plus d'une phrase et ne finis pas par un point.", + "p_": "Une ou plusieurs phrases avec ponctuation.", + "q_": "Une question se terminant par un point d'interrogation." + } + }, + + "@_lint_rules": { + "s_max_words": 4, + "s_max_length": 32 + }, + + "app_name": "Yubico Authenticator", + + "s_save": "Sauvegarder", + "s_cancel": "Annuler", + "s_close": "Fermer", + "s_delete": "Supprimer", + "s_quit": "Quitter", + "s_unlock": "Déverrouiller", + "s_calculate": "Calculer", + "s_import": "Importer", + "s_label": "étiquette", + "s_name": "Nom", + "s_usb": "USB", + "s_nfc": "NFC", + "s_show_window": "Afficher la fenêtre", + "s_hide_window": "masquer la fenêtre", + "q_rename_target": "Renommer l'{label}?", + "@q_rename_target" : { + "placeholders": { + "label": {} + } + }, + "l_bullet": "• {item}", + "@l_bullet" : { + "placeholders": { + "item": {} + } + }, + + "s_about": "À propos", + "s_appearance": "Apparence", + "s_authenticator": "Authenticator", + "s_actions": "Actions", + "s_manage": "Gérer", + "s_setup": "Configuration", + "s_settings": "Paramètres", + "s_piv": "PIV", + "s_webauthn": "WebAuthn", + "s_help_and_about": "Aide et à propos", + "s_help_and_feedback": "Aide et retours", + "s_send_feedback": "Envoyer nous un retour", + "s_i_need_help": "J'ai besoin d'aide", + "s_troubleshooting": "Dépannage", + "s_terms_of_use": "Termes d'utilisation", + "s_privacy_policy": "Politique de confidentialité", + "s_open_src_licenses": "Licenses Open source", + "s_configure_yk": "Configurer la YubiKey", + "s_please_wait": "Veuillez patienter\u2026", + "s_secret_key": "Clé secrète", + "s_private_key": "Clé privée", + "s_invalid_length": "Longueur invalide", + "s_require_touch": "Touché requis", + "q_have_account_info": "Avez-vous des informations de compte?", + "s_run_diagnostics": "Exécuter un diagnostique", + "s_log_level": "Niveau de log: {level}", + "@s_log_level": { + "placeholders": { + "level": {} + } + }, + "s_character_count": "Nombre de caractères", + "s_learn_more": "En savoir\u00a0plus", + + "@_language": {}, + "s_language": "Langue", + "l_enable_community_translations": "Activer les traductions de la communauté", + "p_community_translations_desc": "Ces traductions sont fournies et maintenues par la communauté. Elles peuvent contenir des erreurs ou être incomplètes.", + + "@_theme": {}, + "s_app_theme": "Thèmes de l'application", + "s_choose_app_theme": "Choix du thème", + "s_system_default": "Thème du système", + "s_light_mode": "Thème clair", + "s_dark_mode": "Thème sombre", + + "@_yubikey_selection": {}, + "s_yk_information": "Informations de la YubiKey", + "s_select_yk": "Sélectionner une YubiKey", + "s_select_to_scan": "Sélectionnez pour scanner", + "s_hide_device": "Cacher l'appareil", + "s_show_hidden_devices": "Afficher les appareils cachés", + "s_sn_serial": "S/N: {serial}", + "@s_sn_serial" : { + "placeholders": { + "serial": {} + } + }, + "s_fw_version": "F/W: {version}", + "@s_fw_version" : { + "placeholders": { + "version": {} + } + }, + + "@_yubikey_interactions": {}, + "l_insert_yk": "Insérez votre YubiKey", + "l_insert_or_tap_yk": "Insérez ou touchez votre YubiKey", + "l_unplug_yk": "Déconnectez votre YubiKey", + "l_reinsert_yk": "Réinsérez votre YubiKey", + "l_place_on_nfc_reader": "Placez votre YubiKey sur le lecteur NFC", + "l_replace_yk_on_reader": "Placez votre YubiKey à nouveau sur le lecteur", + "l_remove_yk_from_reader": "Retirez votre YubiKey du lecteur NFC", + "p_try_reinsert_yk": "Essayez de réinsérer votre YubiKey.", + "s_touch_required": "Touché requis", + "l_touch_button_now": "Touchez votre YubiKey maintenant", + "l_keep_touching_yk": "Continuez de toucher votre YubiKey\u2026", + + "@_app_configuration": {}, + "s_toggle_applications": "Changer les applications", + "l_min_one_interface": "Au moins une interface doit être activée", + "s_reconfiguring_yk": "Reconfiguration de la YubiKey\u2026", + "s_config_updated": "Configuration mise à jour", + "l_config_updated_reinsert": "Configuration mise à jour; retirez et réinsérez votre YubiKey", + "s_app_not_supported": "Application non supportée", + "l_app_not_supported_on_yk": "La YubiKey utilisée ne supporte pas l'application '{app}'", + "@l_app_not_supported_on_yk" : { + "placeholders": { + "app": {} + } + }, + "l_app_not_supported_desc": "Cette application n'est pas supportée", + "s_app_disabled": "Application désactivée", + "l_app_disabled_desc": "Activez l'application '{app}' sur votre YubiKey", + "@l_app_disabled_desc" : { + "placeholders": { + "app": {} + } + }, + "s_fido_disabled": "FIDO2 désactivé", + "l_webauthn_req_fido2": "WebAuthn demande que le FIDO2 soit activé sur votre YubiKey", + + "@_connectivity_issues": {}, + "l_helper_not_responding": "Le processus Helper ne réponds pas", + "l_yk_no_access": "Cette YubiKey Ne peut pas être accédée", + "s_yk_inaccessible": "Appareil inaccessible", + "l_open_connection_failed": "Échec d'ouverture de connection", + "l_ccid_connection_failed": "Échec d'ouverture de connection smart card", + "p_ccid_service_unavailable": "Assurez vous que votre service smart card fonctionne.", + "p_pcscd_unavailable": "Assurez vous que pcscd est installé et opérationnel.", + "l_no_yk_present": "Aucune YubiKey détectée", + "s_unknown_type": "Type inconnu", + "s_unknown_device": "Appareil non reconnu", + "s_unsupported_yk": "YubiKey non supportée", + "s_yk_not_recognized": "Appareil non reconnu", + + "@_general_errors": {}, + "l_error_occured": "Une erreur est survenue", + "s_application_error": "Erreur d'application", + "l_import_error": "Erreur d'importation", + "l_file_not_found": "Fichier non trouvé", + "l_file_too_big": "Fichier trop volumineux", + "l_filesystem_error": "Erreur d'accès au système de fichier", + + "@_pins": {}, + "s_pin": "PIN", + "s_puk": "PUK", + "s_set_pin": "Entrez un PIN", + "s_change_pin": "Changez PIN", + "s_change_puk": "Changez PUK", + "s_current_pin": "PIN actuel", + "s_current_puk": "PUK actuel", + "s_new_pin": "Nouveau PIN", + "s_new_puk": "Nouveau PUK", + "s_confirm_pin": "Confirmez le PIN", + "s_confirm_puk": "Confirmez le PUK", + "s_unblock_pin": "Débloquer le PIN", + "l_new_pin_len": "Le nouveau PIN doit avoir au moins {length} caractères", + "@l_new_pin_len" : { + "placeholders": { + "length": {} + } + }, + "s_pin_set": "PIN défini", + "s_puk_set": "PUK défini", + "l_set_pin_failed": "Échec du changement de PIN: {message}", + "@l_set_pin_failed" : { + "placeholders": { + "message": {} + } + }, + "l_attempts_remaining": "Nombre de tentative(s) restante(s) : {retries}", + "@l_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, + "l_wrong_pin_attempts_remaining": "Mauvais PIN, {retries} tentative(s) restante(s)", + "@l_wrong_pin_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, + "s_fido_pin_protection": "PIN de protection FIDO", + "l_fido_pin_protection_optional": "PIN de protection optionnel FIDO", + "l_enter_fido2_pin": "Entrez le PIN FIDO2 de votre YubiKey", + "l_optionally_set_a_pin": "(Optionnel) Entrez un PIN pour protéger l'accès de votre YubiKey\nEnregistrer en tant que clé de sécurité sur les sites web", + "l_pin_blocked_reset": "PIN bloqué; Réinitialisez à l'état d'usine le FIDO", + "l_set_pin_first": "Un PIN est d'abord requis", + "l_unlock_pin_first": "Débloquez avec un PIN d'abord", + "l_pin_soft_locked": "Le PIN est bloqué tant que votre YubiKey ne sera pas réinsérée", + "p_enter_current_pin_or_reset": "Entrez votre PIN. Si vous ne savez pas votre PIN, vous devez déverrouiller votre YubiKey avec un code PUK ou la réinitialiser.", + "p_enter_current_puk_or_reset": "Entrez votre PUK. Si vous ne savez pas votre PUK, vous devez déverrouiller réinitialiser votre YubiKey.", + "p_enter_new_fido2_pin": "Entrez votre nouveau PIN. Un code PIN doit avoir au moins {length} caractères et peut contenir des lettres, nombre et caractères spéciaux.", + "@p_enter_new_fido2_pin" : { + "placeholders": { + "length": {} + } + }, + "s_pin_required": "PIN requis", + "p_pin_required_desc": "L'action que vous allez faire demande d'entrer le code PIN du PIV.", + "l_piv_pin_blocked": "Vous êtes bloqué. Utilisez le code PUK pour réinitialiser", + "p_enter_new_piv_pin_puk": "Entrez un nouveau {name} entre 6 et 8 caractères.", + "@p_enter_new_piv_pin_puk" : { + "placeholders": { + "name": {} + } + }, + + "@_passwords": {}, + "s_password": "Mot de passe", + "s_manage_password": "Gérer les mots de passes", + "s_set_password": "Définir le mot de passe", + "s_password_set": "Mot de passe défini", + "l_optional_password_protection": "Mot de passe optionnel de protection", + "s_new_password": "Nouveau mot de passe", + "s_current_password": "Mot de passe actuel", + "s_confirm_password": "Confirmez le mot de passe", + "s_wrong_password": "Mauvais mot de passe", + "s_remove_password": "Supprimer le mot de passe", + "s_password_removed": "Mot de passe supprimé", + "s_remember_password": "Se souvenir du mot de passe", + "s_clear_saved_password": "Effacer le mot de passe enregistré", + "s_password_forgotten": "Mot de passe oublié", + "l_keystore_unavailable": "Le magasin de clés de l'OS est indisponible", + "l_remember_pw_failed": "Échec de la mémorisation du mot de passe", + "l_unlock_first": "Déverrouillez initialement avec un mot de passe", + "l_enter_oath_pw": "Entrez le mot de passe OATH de votre YubiKey", + "p_enter_current_password_or_reset": "Entrez votre mot de passe actuel. Si vous ne vous souvenez plus de votre mot de passe, vous devrez réinitialiser votre YubiKey.", + "p_enter_new_password": "Entrez votre nouveau mot de passe. Un mot de passe peut contenir des lettres, nombres et des caractères spéciaux.", + + "@_management_key": {}, + "s_management_key": "Gestion des clés", + "s_current_management_key": "Clé actuelle de gestion", + "s_new_management_key": "Nouvelle clé de gestion", + "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", + "l_default_key_used": "Clé de gestion par défaut utilisée", + "l_warning_default_key": "Attention: Clé par défaut utilisée", + "s_protect_key": "Protection par PIN", + "l_pin_protected_key": "Un PIN peut être utilisé à la place", + "l_wrong_key": "Mauvaise clé", + "l_unlock_piv_management": "Déverrouiller la gestion PIV", + "p_unlock_piv_management_desc": "L'action que vous allez réaliser demande la clé de gestion PIV. Donner cette clé déverrouillera cette fonctionnalité pour cette session.", + + "@_oath_accounts": {}, + "l_account": "Compte: {label}", + "@l_account" : { + "placeholders": { + "label": {} + } + }, + "s_accounts": "Comptes", + "s_no_accounts": "Aucun compte", + "s_add_account": "Ajouter un compte", + "s_account_added": "Compte ajouté", + "l_account_add_failed": "Échec d'ajout d'un compte: {message}", + "@l_account_add_failed" : { + "placeholders": { + "message": {} + } + }, + "l_account_name_required": "Votre compte doit avoir un nom", + "l_name_already_exists": "Ce nom existe déjà pour cet émetteur", + "l_invalid_character_issuer": "Caractère invalide: ':' n'est pas autorisé pour un émetteur", + "s_pinned": "Épinglé", + "s_pin_account": "Épingler un compte", + "s_unpin_account": "Désépingler un compte", + "s_no_pinned_accounts": "Aucun compte épinglé", + "l_pin_account_desc": "Gardez vos comptes importants ensembles", + "s_rename_account": "Renommer le compte", + "l_rename_account_desc": "Éditer l'émetteur / le nom du comte", + "s_account_renamed": "Compte renommé", + "p_rename_will_change_account_displayed": "Cette action changera comment le compte est affiché dans la liste.", + "s_delete_account": "Supprimer le compte", + "l_delete_account_desc": "Supprimer le compte de votre YubiKey", + "s_account_deleted": "Compte supprimé", + "p_warning_delete_account": "Attention! Cette action supprimera le compte de votre YubiKey.", + "p_warning_disable_credential": "Vous ne pourrez plus générer de code OTPs pour ce compte. Assurez vous de désactiver les identifiants des sites pour ne pas être verrouillé hors de vos comptes.", + "s_account_name": "Nom du compte", + "s_search_accounts": "Rechercher des comptes", + "l_accounts_used": "{used} sur {capacity} comptes utilisés", + "@l_accounts_used" : { + "placeholders": { + "used": {}, + "capacity": {} + } + }, + "s_num_digits": "{num} chiffres", + "@s_num_digits" : { + "placeholders": { + "num": {} + } + }, + "s_num_sec": "{num} secondes", + "@s_num_sec" : { + "placeholders": { + "num": {} + } + }, + "s_issuer_optional": "Émetteur (optional)", + "s_counter_based": "Basé sur un décompte", + "s_time_based": "Basé sur le temps", + "l_copy_code_desc": "Coller facilement le code depuis une autre application", + "s_calculate_code": "Calculer un code", + "l_calculate_code_desc": "Récupérer un nouveau code depuis votre YubiKey", + + "@_fido_credentials": {}, + "l_passkey": "Passkey: {label}", + "@l_passkey" : { + "placeholders": { + "label": {} + } + }, + "s_passkeys": "Passkeys", + "l_ready_to_use": "Prêt à l'emploi", + "l_register_sk_on_websites": "Enregistrer comme clé de sécurité sur les sites internet", + "l_no_discoverable_accounts": "Aucune Passkeys détectée", + "s_delete_passkey": "Supprimer une Passkey", + "l_delete_passkey_desc": "Supprimer la Passkey de votre YubiKey", + "s_passkey_deleted": "Passkey supprimée", + "p_warning_delete_passkey": "Cette action supprimera cette Passkey de votre YubiKey.", + + "@_fingerprints": {}, + "l_fingerprint": "Empreinte: {label}", + "@l_fingerprint" : { + "placeholders": { + "label": {} + } + }, + "s_fingerprints": "Empreintes", + "l_fingerprint_captured": "Empreinte capturée avec succès!", + "s_fingerprint_added": "Empreinte ajoutée", + "l_setting_name_failed": "Erreur lors de l'ajout du nom: {message}", + "@l_setting_name_failed" : { + "placeholders": { + "message": {} + } + }, + "s_add_fingerprint": "Ajouter une empreinte", + "l_fp_step_1_capture": "Étape 1/2: Entrez votre empreinte", + "l_fp_step_2_name": "Étape 2/2: Nommez votre empreinte", + "s_delete_fingerprint": "Supprimer l'empreinte", + "l_delete_fingerprint_desc": "Supprimer l'empreinte de votre YubiKey", + "s_fingerprint_deleted": "Empreinte supprimée", + "p_warning_delete_fingerprint": "Cette action supprimera cette empreinte de votre YubiKey.", + "s_no_fingerprints": "Aucune empreinte", + "l_set_pin_fingerprints": "Ajoutez un PIN pour enregistrer des empreintes", + "l_no_fps_added": "Aucune empreinte n'a été ajoutée", + "s_rename_fp": "Renommer une empreinte", + "l_rename_fp_desc": "Changer la description", + "s_fingerprint_renamed": "Empreinte renommée", + "l_rename_fp_failed": "Erreur lors du renommage: {message}", + "@l_rename_fp_failed" : { + "placeholders": { + "message": {} + } + }, + "l_add_one_or_more_fps": "Ajouter une ou plusieurs (jusqu'a 5) empreintes", + "l_fingerprints_used": "{used}/5 empreintes enregistrées", + "@l_fingerprints_used": { + "placeholders": { + "used": {} + } + }, + "p_press_fingerprint_begin": "Posez votre doigt sur votre YubiKey pour commencer.", + "p_will_change_label_fp": "Cette action changera la description de votre empreinte.", + + "@_certificates": {}, + "s_certificate": "Certificat", + "s_certificates": "Certificats", + "s_csr": "CSR", + "s_subject": "Sujet", + "l_export_csr_file": "Sauvegarder le CSR vers un fichier", + "l_select_import_file": "Sélectionnez un fichier à importer", + "l_export_certificate": "Exporter le certificat", + "l_export_certificate_file": "Exporter le certificat vers un fichier", + "l_export_certificate_desc": "Exporter le certificat vers un fichier", + "l_certificate_exported": "Certificat exporté", + "l_import_file": "Importer un fichier", + "l_import_desc": "Importer une clé et/ou un certificat", + "l_delete_certificate": "Supprimer un certificat", + "l_delete_certificate_desc": "Supprimer un certificat de votre YubiKey", + "l_subject_issuer": "Sujet: {subject}, Émetteur: {issuer}", + "@l_subject_issuer" : { + "placeholders": { + "subject": {}, + "issuer": {} + } + }, + "l_serial": "Serial: {serial}", + "@l_serial" : { + "placeholders": { + "serial": {} + } + }, + "l_certificate_fingerprint": "Empreinte: {fingerprint}", + "@l_certificate_fingerprint" : { + "placeholders": { + "fingerprint": {} + } + }, + "l_valid": "Valide: {not_before} - {not_after}", + "@l_valid" : { + "placeholders": { + "not_before": {}, + "not_after": {} + } + }, + "l_no_certificate": "Aucun certificat chargé", + "l_key_no_certificate": "Clé sans certificat chargé", + "s_generate_key": "Générer une clé", + "l_generate_desc": "Générer un nouveau certificat ou CSR", + "p_generate_desc": "Cette action génèrera une nouvelle clé sur l'emplacement PIV {slot} de votre YubiKey. La clé publique sera incorporée dans un certificat auto-signé stocké sur votre YubiKey, ou dans un fichier CSR (Certificate Signing Request).", + "@p_generate_desc" : { + "placeholders": { + "slot": {} + } + }, + "l_generating_private_key": "Génération d'une clé privée\u2026", + "s_private_key_generated": "Clé privée générée", + "p_warning_delete_certificate": "Attention! Cette action supprimera le certificat de votre YubiKey.", + "q_delete_certificate_confirm": "Supprimer le certficat du slot PIV {slot}?", + "@q_delete_certificate_confirm" : { + "placeholders": { + "slot": {} + } + }, + "l_certificate_deleted": "Certificat supprimé", + "p_password_protected_file": "Le fichier sélectionné est protégé par un mot de passe. Enterez le mot de passe pour continuer.", + "p_import_items_desc": "Les éléments suivants seront importés dans le slot PIV {slot}.", + "@p_import_items_desc" : { + "placeholders": { + "slot": {} + } + }, + + "@_piv_slots": {}, + "s_slot_display_name": "{name} ({hexid})", + "@s_slot_display_name" : { + "placeholders": { + "name": {}, + "hexid": {} + } + }, + "s_slot_9a": "Authentification", + "s_slot_9c": "Signature digitale", + "s_slot_9d": "Gestion des clés", + "s_slot_9e": "Authentification par carte", + + "@_permissions": {}, + "s_enable_nfc": "Activer le NFC", + "s_permission_denied": "Permission refusée", + "l_elevating_permissions": "Élevation des permissions\u2026", + "s_review_permissions": "Révision des permissions", + "p_elevated_permissions_required": "Gérer cet appareil demande des privilèges plus élevés.", + "p_webauthn_elevated_permissions_required": "La gestion WebAuthn demande des privilèges plus élevés.", + "p_need_camera_permission": "Yubico Authenticator a besoin des permission d'utiliser la caméra pour scanner les QR code.", + + "@_qr_codes": {}, + "s_qr_scan": "Scanner un QR code", + "l_qr_scanned": "QR code scanné", + "l_invalid_qr": "QR code invalide", + "l_qr_not_found": "Aucun QR code trouvé", + "l_qr_not_read": "Erreur de lecture du QR code: {message}", + "@l_qr_not_read" : { + "placeholders": { + "message": {} + } + }, + "l_point_camera_scan": "Dirigez votre caméra vers le QR code pour le scanner", + "q_want_to_scan": "Voulez vous scanner un code?", + "q_no_qr": "Pas de QR code?", + "s_enter_manually": "Entrer un code manuellement", + + "@_factory_reset": {}, + "s_reset": "Réinitialiser", + "s_factory_reset": "Réinitialisation à l'état d'usine", + "l_factory_reset_this_app": "Réinitialiser cette application", + "s_reset_oath": "Réinitialiser l'OATH", + "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": { + "message": {} + } + }, + "s_reset_piv": "Réinitialiser le PIV", + "l_piv_app_reset": "L'application PIV à été réinitialisée", + "p_warning_factory_reset": "Attention! Cette action supprimera de manière irrévocable tous les compte OATH TOTP/HOTP de votre YubiKey.", + "p_warning_disable_credentials": "Vos identifiants OATH, ainsi que vos mots de passes, seront supprimés de votre YubiKey. Assurez vous de désactiver les identifiants des sites pour ne pas être verrouillé hors de vos comptes.", + "p_warning_deletes_accounts": "Attention! Cette action supprimera de manière irrévocable tous les comptes U2F et FIDO2 de votre YubiKey.", + "p_warning_disable_accounts": "Vos identifiants, ainsi que les codes PIN associés, seront supprimés de votre YubiKey. Assurez vous de désactiver les identifiants des sites pour ne pas être verrouillé hors de vos comptes.", + "p_warning_piv_reset": "Attention! Cette action supprimera de manière irrévocable toutes les données PIV stockées sur votre YubiKey.", + "p_warning_piv_reset_desc": "Cela inclus les clé privées et les certificats. Votre PIN, PUK, clé de management seront réinitialisés à leur valeurs d'usines.", + + "@_copy_to_clipboard": {}, + "l_copy_to_clipboard": "Copier vers le presse papier", + "s_code_copied": "Code copié", + "l_code_copied_clipboard": "Code copé vers le presse papier", + "s_copy_log": "Copier les logs", + "l_log_copied": "Logs copiés vers le presse papier", + "l_diagnostics_copied": "données de diagnostique copiés vers le presse papier", + "p_target_copied_clipboard": "{label} copié vers le presse papier.", + "@p_target_copied_clipboard" : { + "placeholders": { + "label": {} + } + }, + + "@_custom_icons": {}, + "s_custom_icons": "Icônes personnalisées", + "l_set_icons_for_accounts": "Sélectionner les icônes pour les comptesSet icons for accounts", + "p_custom_icons_description": "Les packs d'cônes peuvent rendre vos comptes plus facilement repérables grâce à des logos et couleurs familières.", + "s_replace_icon_pack": "Remplacer le pack d'icônes", + "l_loading_icon_pack": "Chargement du pack d'icônes\u2026", + "s_load_icon_pack": "Charger le pack d'icônes", + "s_remove_icon_pack": "Supprimer le pack d'icônes", + "l_icon_pack_removed": "Pack d'icônes supprimé", + "l_remove_icon_pack_failed": "Erreur lors de la suppression du pack d'icônes", + "s_choose_icon_pack": "Choisissez votre pack d'icônes", + "l_icon_pack_imported": "Pack d'icônes importé", + "l_import_icon_pack_failed": "Erreur lors de l'importation du pack d'icônes: {message}", + "@l_import_icon_pack_failed": { + "placeholders": { + "message": {} + } + }, + "l_invalid_icon_pack": "Pack d'icônes invalide", + "l_icon_pack_copy_failed": "Échec de la copie des fichiers du pack d'icônes", + + "@_android_settings": {}, + "s_nfc_options": "Options NFC", + "l_on_yk_nfc_tap": "Lorsque le NFC de la YubiKey est en contact", + "l_launch_ya": "Démarrer Yubico Authenticator", + "l_copy_otp_clipboard": "Copier le code OTP vers le presse papier", + "l_launch_and_copy_otp": "Démarrer l'application et copier le code OTP", + "l_kbd_layout_for_static": "Arrangement clavier (pour les mot de passes statiques)", + "s_choose_kbd_layout": "Choisissez l'arrangement clavier", + "l_bypass_touch_requirement": "Contourner la nécessité de toucher la YubiKey", + "l_bypass_touch_requirement_on": "Les comptes qui demande le touché sont automatiquement montrés via NFC", + "l_bypass_touch_requirement_off": "Les compte qui demande un couché ont besoin d'un contact supplémentaire NFC", + "s_silence_nfc_sounds": "Couper le son NFC", + "l_silence_nfc_sounds_on": "Aucun sons ne sera joué lors du contact NFC", + "l_silence_nfc_sounds_off": "Du son sera joué lors du contact NFC", + "s_usb_options": "Options USB", + "l_launch_app_on_usb": "Lancer lorsque la YubiKey est connectée", + "l_launch_app_on_usb_on": "Cela empêchera que d'autre applications utilisent la YubiKey via l'USB", + "l_launch_app_on_usb_off": "D'autres applications peuvent utiliser la YubiKey via l'USB", + "s_allow_screenshots": "Autoriser les captures d'écrans", + + "@_eof": {} +} \ No newline at end of file From 0a07e21ef766d065d93f30ff6df83adc072cb33f Mon Sep 17 00:00:00 2001 From: JulienDeveaux Date: Fri, 7 Jul 2023 14:13:11 +0200 Subject: [PATCH 037/158] Fix some errors --- lib/l10n/app_fr.arb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 25e1bb81..d67e4ed6 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -16,7 +16,7 @@ }, "@_lint_rules": { - "s_max_words": 4, + "s_max_words": 5, "s_max_length": 32 }, @@ -30,12 +30,12 @@ "s_unlock": "Déverrouiller", "s_calculate": "Calculer", "s_import": "Importer", - "s_label": "étiquette", + "s_label": "Étiquette", "s_name": "Nom", "s_usb": "USB", "s_nfc": "NFC", "s_show_window": "Afficher la fenêtre", - "s_hide_window": "masquer la fenêtre", + "s_hide_window": "Masquer la fenêtre", "q_rename_target": "Renommer l'{label}?", "@q_rename_target" : { "placeholders": { @@ -230,7 +230,7 @@ }, "s_pin_required": "PIN requis", "p_pin_required_desc": "L'action que vous allez faire demande d'entrer le code PIN du PIV.", - "l_piv_pin_blocked": "Vous êtes bloqué. Utilisez le code PUK pour réinitialiser", + "l_piv_pin_blocked": "Vous êtes bloqué, utilisez le code PUK pour réinitialiser", "p_enter_new_piv_pin_puk": "Entrez un nouveau {name} entre 6 et 8 caractères.", "@p_enter_new_piv_pin_puk" : { "placeholders": { @@ -250,8 +250,8 @@ "s_wrong_password": "Mauvais mot de passe", "s_remove_password": "Supprimer le mot de passe", "s_password_removed": "Mot de passe supprimé", - "s_remember_password": "Se souvenir du mot de passe", - "s_clear_saved_password": "Effacer le mot de passe enregistré", + "s_remember_password": "Retenir le mot de passe", + "s_clear_saved_password": "Effacer le mot de passe", "s_password_forgotten": "Mot de passe oublié", "l_keystore_unavailable": "Le magasin de clés de l'OS est indisponible", "l_remember_pw_failed": "Échec de la mémorisation du mot de passe", @@ -507,7 +507,7 @@ "@_factory_reset": {}, "s_reset": "Réinitialiser", - "s_factory_reset": "Réinitialisation à l'état d'usine", + "s_factory_reset": "Réinitialisation", "l_factory_reset_this_app": "Réinitialiser cette application", "s_reset_oath": "Réinitialiser l'OATH", "l_oath_application_reset": "L'application OATH à été réinitialisée", @@ -535,7 +535,7 @@ "l_code_copied_clipboard": "Code copé vers le presse papier", "s_copy_log": "Copier les logs", "l_log_copied": "Logs copiés vers le presse papier", - "l_diagnostics_copied": "données de diagnostique copiés vers le presse papier", + "l_diagnostics_copied": "Données de diagnostique copiés vers le presse papier", "p_target_copied_clipboard": "{label} copié vers le presse papier.", "@p_target_copied_clipboard" : { "placeholders": { From 649da32b8312d06b13f2a90b9ec471f2fabe79ec Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 7 Jul 2023 18:10:16 +0200 Subject: [PATCH 038/158] Only enable CFG for Release builds. --- windows/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 2fc55831..c87e8c47 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -39,10 +39,10 @@ function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) - target_compile_options(${TARGET} PRIVATE /GUARD:CF) - target_link_options(${TARGET} PRIVATE /GUARD:CF) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") + target_compile_options(${TARGET} PRIVATE "$<$:/GUARD:CF>") + target_link_options(${TARGET} PRIVATE "$<$:/GUARD:CF>") endfunction() # Flutter library and tool build rules. From 188a58dd7d83a82005b0f691145636703d2878d4 Mon Sep 17 00:00:00 2001 From: Alexandru Geana Date: Mon, 17 Jul 2023 13:43:20 +0200 Subject: [PATCH 039/158] Additional security flags for Windows builds --- windows/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index c87e8c47..7afed59e 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -39,10 +39,14 @@ function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_options(${TARGET} PRIVATE /GS) + target_compile_options(${TARGET} PRIVATE /Gs) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") target_compile_options(${TARGET} PRIVATE "$<$:/GUARD:CF>") + target_compile_options(${TARGET} PRIVATE "$<$:/NXCOMPAT>") target_link_options(${TARGET} PRIVATE "$<$:/GUARD:CF>") + target_link_options(${TARGET} PRIVATE "$<$:/NXCOMPAT>") endfunction() # Flutter library and tool build rules. From e087f6f21ce5a336ed796fb29c032768630371ad Mon Sep 17 00:00:00 2001 From: Alexandru Geana Date: Mon, 17 Jul 2023 13:59:15 +0200 Subject: [PATCH 040/158] Additional security flags for Linux builds --- linux/CMakeLists.txt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index bb223ffe..a3ebbbb6 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -30,8 +30,15 @@ endif() function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_14) target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE -fstack-protector-all) + target_compile_options(${TARGET} PRIVATE -fpie) + target_compile_options(${TARGET} PRIVATE -fpic) target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") + target_link_options(${TARGET} PRIVATE -fstack-protector-all) + target_link_options(${TARGET} PRIVATE -pie) + target_link_options(${TARGET} PRIVATE -Wl,-z,noexecstack) + target_link_options(${TARGET} PRIVATE -Wl,-z,relro,-z,now) endfunction() set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") @@ -128,4 +135,4 @@ install(FILES "../assets/graphics/app-icon.png" install(FILES "../resources/linux/desktop_integration.sh" DESTINATION "${BUILD_BUNDLE_DIR}" - PERMISSIONS OWNER_EXECUTE OWNER_READ OWNER_WRITE) \ No newline at end of file + PERMISSIONS OWNER_EXECUTE OWNER_READ OWNER_WRITE) From 3804e6d5497fb020b5a010f6f8a3e0c169e0075b Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Fri, 12 May 2023 11:38:56 +0200 Subject: [PATCH 041/158] First steps of importing creds --- lib/oath/models.dart | 93 +++++++++++++++++++++++++++++++++++++++++++- pubspec.lock | 10 ++++- pubspec.yaml | 2 + 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/lib/oath/models.dart b/lib/oath/models.dart index 2a9ba7cc..2f389fb9 100755 --- a/lib/oath/models.dart +++ b/lib/oath/models.dart @@ -14,6 +14,10 @@ * limitations under the License. */ +import 'dart:typed_data'; +import 'dart:convert'; +import 'package:base32/base32.dart'; +import 'package:convert/convert.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -121,9 +125,96 @@ class CredentialData with _$CredentialData { _$CredentialDataFromJson(json); factory CredentialData.fromUri(Uri uri) { - if (uri.scheme.toLowerCase() != 'otpauth') { + List read(Uint8List bytes) { + final index = bytes[0]; + final sublist1 = bytes.sublist(1, index + 1); + final sublist2 = bytes.sublist(index + 1); + return [sublist1, sublist2]; + } + + String b32Encode(Uint8List data) { + final encodedData = base32.encode(data); + return utf8.decode(encodedData.runes.toList()); + } + + if (uri.scheme.toLowerCase() == 'otpauth-migration') { + final uriString = uri.toString(); + var data = Uint8List.fromList( + base64.decode(Uri.decodeComponent(uriString.split('=')[1]))); + + var credentials = []; + + var tag = data[0]; + + /* + Assuming the credential(s) follow the format: + cred = 0aLENGTH0aSECRET12NAME1aISSUER200128013002xxx + where xxx can be another cred. + */ + while (tag == 10) { + // 0a tag means new credential. + + var length = data[1]; // The length of this credential + var secretTag = data[2]; + if (secretTag != 10) { + // tag before secret is 0a hex + throw ArgumentError('Invalid scheme, no secret tag'); + } + data = data.sublist(3); + final result1 = read(data); + final secret = result1[0]; + data = result1[1]; + final decodedSecret = b32Encode(secret); + + var nameTag = data[0]; + if (nameTag != 18) { + // tag before name is 12 hex + throw ArgumentError('Invalid scheme, no name tag'); + } + data = data.sublist(1); + final result2 = read(data); + final name = result2[0]; + data = result2[1]; + + var issuerTag = data[0]; + var issuer; + if (issuerTag == 26) { + // tag before issuer is 1a hex, but issuer is optional. + data = data.sublist(1); + final result3 = read(data); + issuer = result3[0]; + data = result3[1]; + } + + final credential = CredentialData( + issuer: issuerTag != 26 + ? null + : utf8.decode(issuer, allowMalformed: true), + name: utf8.decode(name, allowMalformed: true), + secret: decodedSecret, + ); + + credentials.add(credential); + + var endTag = data.sublist(0, 6); + if (hex.encode(endTag) != '200128013002') { + // At the end of every credential there is 200128013002 + throw ArgumentError('Invalid scheme, no end tag'); + } + data = data.sublist(6); + tag = data[0]; + } + + // Print all the extracted credentials + for (var credential in credentials) { + print('${credential.issuer} (${credential.name}) ${credential.secret}'); + } + + return credentials[0]; // For now, return only the first credential. + } else if (uri.scheme.toLowerCase() != 'otpauth') { throw ArgumentError('Invalid scheme, must be "otpauth://"'); } + final oathType = OathType.values.byName(uri.host.toLowerCase()); final params = uri.queryParameters; String? issuer; diff --git a/pubspec.lock b/pubspec.lock index 0291513d..4dac2184 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + base32: + dependency: "direct main" + description: + name: base32 + sha256: ddad4ebfedf93d4500818ed8e61443b734ffe7cf8a45c668c9b34ef6adde02e2 + url: "https://pub.dev" + source: hosted + version: "2.1.3" boolean_selector: dependency: transitive description: @@ -154,7 +162,7 @@ packages: source: hosted version: "1.17.1" convert: - dependency: transitive + dependency: "direct main" description: name: convert sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" diff --git a/pubspec.yaml b/pubspec.yaml index f1801cea..16cb8e57 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,8 @@ dependencies: tray_manager: ^0.2.0 local_notifier: ^0.1.5 io: ^1.0.4 + base32: ^2.1.3 + convert: ^3.1.1 dev_dependencies: integration_test: From 5550ebd7d33cfbab09d72a8b252700ef7e45b419 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Fri, 12 May 2023 12:33:32 +0200 Subject: [PATCH 042/158] Logging and minor fixes --- lib/oath/models.dart | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/oath/models.dart b/lib/oath/models.dart index 2f389fb9..7f9da362 100755 --- a/lib/oath/models.dart +++ b/lib/oath/models.dart @@ -17,9 +17,11 @@ import 'dart:typed_data'; import 'dart:convert'; import 'package:base32/base32.dart'; +import 'package:logging/logging.dart'; import 'package:convert/convert.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:yubico_authenticator/app/logging.dart'; import '../core/models.dart'; @@ -32,6 +34,8 @@ const defaultCounter = 0; const defaultOathType = OathType.totp; const defaultHashAlgorithm = HashAlgorithm.sha1; +final _log = Logger('oath.models'); + enum HashAlgorithm { @JsonValue(0x01) sha1('SHA-1'), @@ -154,7 +158,7 @@ class CredentialData with _$CredentialData { while (tag == 10) { // 0a tag means new credential. - var length = data[1]; // The length of this credential + //var length = data[1]; // The length of this credential var secretTag = data[2]; if (secretTag != 10) { // tag before secret is 0a hex @@ -177,7 +181,8 @@ class CredentialData with _$CredentialData { data = result2[1]; var issuerTag = data[0]; - var issuer; + List? issuer; + if (issuerTag == 26) { // tag before issuer is 1a hex, but issuer is optional. data = data.sublist(1); @@ -185,11 +190,10 @@ class CredentialData with _$CredentialData { issuer = result3[0]; data = result3[1]; } - final credential = CredentialData( issuer: issuerTag != 26 ? null - : utf8.decode(issuer, allowMalformed: true), + : utf8.decode(issuer!, allowMalformed: true), name: utf8.decode(name, allowMalformed: true), secret: decodedSecret, ); @@ -207,7 +211,8 @@ class CredentialData with _$CredentialData { // Print all the extracted credentials for (var credential in credentials) { - print('${credential.issuer} (${credential.name}) ${credential.secret}'); + _log.debug( + '${credential.issuer} (${credential.name}) ${credential.secret}'); } return credentials[0]; // For now, return only the first credential. From 445034151a9fd817cc3047dd95d8e95b6a2a8600 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Tue, 13 Jun 2023 13:48:41 +0200 Subject: [PATCH 043/158] Extract more data from the URI --- lib/oath/models.dart | 167 +++++++++++++++++---------- lib/oath/views/add_account_page.dart | 4 +- 2 files changed, 105 insertions(+), 66 deletions(-) diff --git a/lib/oath/models.dart b/lib/oath/models.dart index 7f9da362..7d44fc35 100755 --- a/lib/oath/models.dart +++ b/lib/oath/models.dart @@ -18,7 +18,6 @@ import 'dart:typed_data'; import 'dart:convert'; import 'package:base32/base32.dart'; import 'package:logging/logging.dart'; -import 'package:convert/convert.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:yubico_authenticator/app/logging.dart'; @@ -128,7 +127,17 @@ class CredentialData with _$CredentialData { factory CredentialData.fromJson(Map json) => _$CredentialDataFromJson(json); - factory CredentialData.fromUri(Uri uri) { + static List multiFromUri(uri) { + if (uri.scheme.toLowerCase() == 'otpauth-migration') { + return CredentialData.fromMigration(uri); + } else if (uri.scheme.toLowerCase() == 'otpauth') { + return [CredentialData.fromUri(uri)]; + } else { + throw ArgumentError('Invalid scheme'); + } + } + + static List fromMigration(uri) { List read(Uint8List bytes) { final index = bytes[0]; final sublist1 = bytes.sublist(1, index + 1); @@ -141,85 +150,115 @@ class CredentialData with _$CredentialData { return utf8.decode(encodedData.runes.toList()); } - if (uri.scheme.toLowerCase() == 'otpauth-migration') { - final uriString = uri.toString(); - var data = Uint8List.fromList( - base64.decode(Uri.decodeComponent(uriString.split('=')[1]))); + final uriString = uri.toString(); + var data = Uint8List.fromList( + base64.decode(Uri.decodeComponent(uriString.split('=')[1]))); - var credentials = []; + var credentials = []; - var tag = data[0]; + var tag = data[0]; - /* + /* Assuming the credential(s) follow the format: - cred = 0aLENGTH0aSECRET12NAME1aISSUER200128013002xxx + cred = 0aLENGTH0aSECRET12NAME1aISSUER20ALGO28DIGITS30OATHxxx where xxx can be another cred. */ - while (tag == 10) { - // 0a tag means new credential. + while (tag == 10) { + // 0a tag means new credential. - //var length = data[1]; // The length of this credential - var secretTag = data[2]; - if (secretTag != 10) { - // tag before secret is 0a hex - throw ArgumentError('Invalid scheme, no secret tag'); - } - data = data.sublist(3); - final result1 = read(data); - final secret = result1[0]; - data = result1[1]; - final decodedSecret = b32Encode(secret); + // Extract secret, name, and issuer + var secretTag = data[2]; + if (secretTag != 10) { + // tag before secret is 0a hex + throw ArgumentError('Invalid scheme, no secret tag'); + } + data = data.sublist(3); + final result1 = read(data); + final secret = result1[0]; + data = result1[1]; + final decodedSecret = b32Encode(secret); - var nameTag = data[0]; - if (nameTag != 18) { - // tag before name is 12 hex - throw ArgumentError('Invalid scheme, no name tag'); - } + var nameTag = data[0]; + if (nameTag != 18) { + // tag before name is 12 hex + throw ArgumentError('Invalid scheme, no name tag'); + } + data = data.sublist(1); + final result2 = read(data); + final name = result2[0]; + data = result2[1]; + + var issuerTag = data[0]; + List? issuer; + + if (issuerTag == 26) { + // tag before issuer is 1a hex, but issuer is optional. data = data.sublist(1); - final result2 = read(data); - final name = result2[0]; - data = result2[1]; + final result3 = read(data); + issuer = result3[0]; + data = result3[1]; + } - var issuerTag = data[0]; - List? issuer; + // Extract algorithm, number of digits, and oath type: + var algoTag = data[0]; + if (algoTag != 32) { + // tag before algo is 20 hex + throw ArgumentError('Invalid scheme, no algo tag'); + } + int algo = data[1]; - if (issuerTag == 26) { - // tag before issuer is 1a hex, but issuer is optional. - data = data.sublist(1); - final result3 = read(data); - issuer = result3[0]; - data = result3[1]; - } - final credential = CredentialData( - issuer: issuerTag != 26 - ? null - : utf8.decode(issuer!, allowMalformed: true), - name: utf8.decode(name, allowMalformed: true), - secret: decodedSecret, - ); + var digitsTag = data[2]; + if (digitsTag != 40) { + // tag before digits is 28 hex + throw ArgumentError('Invalid scheme, no digits tag'); + } + var digits = data[3]; - credentials.add(credential); + var oathTag = data[4]; + if (oathTag != 48) { + // tag before oath is 30 hex + throw ArgumentError('Invalid scheme, no oath tag'); + } + var oathType = data[5]; - var endTag = data.sublist(0, 6); - if (hex.encode(endTag) != '200128013002') { - // At the end of every credential there is 200128013002 - throw ArgumentError('Invalid scheme, no end tag'); - } + int counter = defaultCounter; + if (oathType == 1) { + // if hotp, extract counter + counter = data[7]; + } + + final credential = CredentialData( + issuer: + issuerTag != 26 ? null : utf8.decode(issuer!, allowMalformed: true), + name: utf8.decode(name, allowMalformed: true), + oathType: oathType == 1 ? OathType.hotp : OathType.totp, + secret: decodedSecret, + hashAlgorithm: algo == 1 + ? HashAlgorithm.sha1 + : (algo == 2 ? HashAlgorithm.sha256 : HashAlgorithm.sha512), + digits: digits == 1 ? defaultDigits : 8, + counter: counter, + ); + + credentials.add(credential); + if (oathType == 1) { + data = data.sublist(8); + } else { data = data.sublist(6); - tag = data[0]; } - - // Print all the extracted credentials - for (var credential in credentials) { - _log.debug( - '${credential.issuer} (${credential.name}) ${credential.secret}'); - } - - return credentials[0]; // For now, return only the first credential. - } else if (uri.scheme.toLowerCase() != 'otpauth') { - throw ArgumentError('Invalid scheme, must be "otpauth://"'); + tag = data[0]; } + // Print all the extracted credentials + for (var credential in credentials) { + _log.debug( + '${credential.issuer} (${credential.name}) ${credential.secret}'); + } + + return credentials; + } + + factory CredentialData.fromUri(Uri uri) { final oathType = OathType.values.byName(uri.host.toLowerCase()); final params = uri.queryParameters; String? issuer; diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index b99a88ca..d78a6360 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -130,8 +130,8 @@ class _OathAddAccountPageState extends ConsumerState { _qrState = _QrScanState.failed; }); } else { - final data = CredentialData.fromUri(Uri.parse(otpauth)); - _loadCredentialData(data); + final data = CredentialData.multiFromUri(Uri.parse(otpauth)); + _loadCredentialData(data[0]); // TODO } } catch (e) { final String errorMessage; From 13fbfe0f94140aa3681932c5f167bdf261adabc8 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Tue, 11 Jul 2023 10:58:06 +0200 Subject: [PATCH 044/158] Added UI and logic --- lib/l10n/app_en.arb | 2 + lib/oath/views/key_actions.dart | 42 +++++ lib/oath/views/list_screen.dart | 202 ++++++++++++++++++++++ lib/oath/views/rename_list_account.dart | 212 ++++++++++++++++++++++++ 4 files changed, 458 insertions(+) create mode 100644 lib/oath/views/list_screen.dart create mode 100644 lib/oath/views/rename_list_account.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b429bb71..f157acbf 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -285,6 +285,7 @@ "s_accounts": "Accounts", "s_no_accounts": "No accounts", "s_add_account": "Add account", + "s_add_accounts" : "Add account(s)", "s_account_added": "Account added", "l_account_add_failed": "Failed adding account: {message}", "@l_account_add_failed" : { @@ -295,6 +296,7 @@ "l_account_name_required": "Your account must have a name", "l_name_already_exists": "This name already exists for the issuer", "l_invalid_character_issuer": "Invalid character: ':' is not allowed in issuer", + "l_select_accounts" : "Select account(s) to add to the YubiKey", "s_pinned": "Pinned", "s_pin_account": "Pin account", "s_unpin_account": "Unpin account", diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index a096dc13..8fcb100d 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -32,6 +32,7 @@ import '../keys.dart' as keys; import 'add_account_page.dart'; import 'manage_password_dialog.dart'; import 'reset_dialog.dart'; +import 'list_screen.dart'; Widget oathBuildActions( BuildContext context, @@ -91,6 +92,47 @@ Widget oathBuildActions( } : null, ), + ActionListItem( + title: l10n.s_qr_scan, + icon: const Icon(Icons.qr_code_scanner_outlined), + onTap: (context) async { + final withContext = ref.read(withContextProvider); + final credentials = ref.read(credentialsProvider); + Navigator.of(context).pop(); + final qrScanner = ref.watch(qrScannerProvider); + if (qrScanner != null) { + final otpauth = await qrScanner.scanQr(); + if (otpauth == null) { + showMessage(context, l10n.l_qr_not_found); + } else { + String s = 'otpauth-migration'; + if (otpauth.contains(s)) { + final data = + CredentialData.multiFromUri(Uri.parse(otpauth)); + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => ListScreen(devicePath, data), + ); + }); + } else if (otpauth.contains('otpauth')) { + final data = + CredentialData.multiFromUri(Uri.parse(otpauth)); + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => OathAddAccountPage( + devicePath, + oathState, + credentials: credentials, + credentialData: data[0], + ), + ); + }); + } + } + } + }), ]), ActionListSection(l10n.s_manage, children: [ ActionListItem( diff --git a/lib/oath/views/list_screen.dart b/lib/oath/views/list_screen.dart new file mode 100644 index 00000000..0058584a --- /dev/null +++ b/lib/oath/views/list_screen.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:yubico_authenticator/app/logging.dart'; + +import '../../android/oath/state.dart'; +import '../../app/models.dart'; +import '../../widgets/responsive_dialog.dart'; + +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../keys.dart'; +import '../models.dart'; +import '../../app/state.dart'; +import '../../core/state.dart'; +import '../state.dart'; +import '../../app/message.dart'; + +import '../../exception/cancellation_exception.dart'; +import 'rename_list_account.dart'; + +final _log = Logger('oath.views.list_screen'); + +class ListScreen extends ConsumerStatefulWidget { + final DevicePath devicePath; + final List? credentialsFromUri; + + const ListScreen(this.devicePath, this.credentialsFromUri) + : super(key: setOrManagePasswordAction); + + @override + ConsumerState createState() => _ListScreenState(); +} + +class _ListScreenState extends ConsumerState { + bool isChecked = true; + int? numCreds; + late Map checkedCreds; + List? _credentials; + + bool unique = true; + + @override + void initState() { + super.initState(); + checkedCreds = + Map.fromIterable(widget.credentialsFromUri!, value: (v) => true); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + numCreds = ref.watch(credentialListProvider(widget.devicePath) + .select((value) => value?.length)); + final deviceNode = ref.watch(currentDeviceProvider); + + _credentials = ref + .watch(credentialListProvider(deviceNode!.path)) + ?.map((e) => e.credential) + .toList(); + + return ResponsiveDialog( + title: Text(l10n.s_add_accounts), + actions: [ + TextButton( + onPressed: isValid() ? submit : null, + child: Text(l10n.s_save), + ) + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + children: [ + Text(l10n.l_select_accounts), + //Padding(padding: EdgeInsets.only(top: 20.0, bottom: 2.0)), + ...widget.credentialsFromUri!.map( + (cred) => CheckboxListTile( + controlAffinity: ListTileControlAffinity.leading, + secondary: Row(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + onPressed: () async { + _log.debug('pressed'); + }, + icon: const Icon(Icons.touch_app_outlined)), + IconButton( + onPressed: () async { + final node = ref + .watch(currentDeviceDataProvider) + .valueOrNull + ?.node; + final withContext = ref.read(withContextProvider); + CredentialData renamed = await withContext( + (context) async => await showBlurDialog( + context: context, + builder: (context) => RenameList( + node!, + cred, + widget.credentialsFromUri, + _credentials), + )); + + setState(() { + int index = widget.credentialsFromUri!.indexWhere( + (element) => + element.name == cred.name && + (element.issuer == cred.issuer)); + widget.credentialsFromUri![index] = renamed; + checkedCreds[cred] = false; + checkedCreds[renamed] = true; + }); + }, + icon: const Icon(Icons.edit_outlined)), + ]), + title: cred.issuer != null + ? Text(cred.issuer!) + : Text(cred.name), + value: isUnique(cred.name, cred.issuer ?? '') + ? (checkedCreds[cred] ?? true) + : false, + enabled: isUnique(cred.name, cred.issuer ?? ''), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + cred.issuer != null ? Text(cred.name) : Text(''), + isUnique(cred.name, cred.issuer ?? '') + ? Text('') + : Text( + l10n.l_name_already_exists, + style: TextStyle(color: Colors.red), + ) + ]), + onChanged: (bool? value) { + setState(() { + checkedCreds[cred] = value!; + }); + }, + ), + ) + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ))); + } + + bool isUnique(String nameText, String? issuerText) { + bool ans = _credentials + ?.where((element) => + element.name == nameText && + (element.issuer ?? '') == issuerText) + .isEmpty ?? + true; + return ans; + } + + bool isValid() { + int credsToAdd = 0; + checkedCreds.forEach((k, v) => v ? credsToAdd++ : null); + if (numCreds! + credsToAdd <= 32) return true; + return false; + } + + void submit() async { + checkedCreds.forEach((k, v) => v ? accept(k) : null); + Navigator.of(context).pop(); + } + + void accept(CredentialData cred) async { + final deviceNode = ref.watch(currentDeviceProvider); + final devicePath = deviceNode?.path; + if (devicePath != null) { + await _doAddCredential(devicePath: devicePath, credUri: cred.toUri()); + } else if (isAndroid) { + // Send the credential to Android to be added to the next YubiKey + await _doAddCredential(devicePath: null, credUri: cred.toUri()); + } + } + + Future _doAddCredential( + {DevicePath? devicePath, required Uri credUri}) async { + try { + if (devicePath == null) { + assert(isAndroid, 'devicePath is only optional for Android'); + await ref.read(addCredentialToAnyProvider).call(credUri); + } else { + await ref + .read(credentialListProvider(devicePath).notifier) + .addAccount(credUri); + } + if (!mounted) return; + //Navigator.of(context).pop(); + showMessage(context, 'added'); + } on CancellationException catch (_) { + // ignored + } catch (e) { + _log.debug('Failed to add account'); + final String errorMessage; + // TODO: Make this cleaner than importing desktop specific RpcError. + } + } +} diff --git a/lib/oath/views/rename_list_account.dart b/lib/oath/views/rename_list_account.dart new file mode 100644 index 00000000..438a953a --- /dev/null +++ b/lib/oath/views/rename_list_account.dart @@ -0,0 +1,212 @@ +/* + * 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 'package:logging/logging.dart'; + +import '../../app/logging.dart'; +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../exception/cancellation_exception.dart'; +import '../../desktop/models.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../../widgets/utf8_utils.dart'; +import '../models.dart'; +import '../keys.dart' as keys; +import 'utils.dart'; + +final _log = Logger('oath.view.rename_account_dialog'); + +class RenameList extends ConsumerStatefulWidget { + final DeviceNode device; + final CredentialData credential; + final List? credentialsFromUri; + final List? credentials; + + const RenameList( + this.device, this.credential, this.credentialsFromUri, this.credentials, + {super.key}); + + @override + ConsumerState createState() => _RenameListState(); +} + +class _RenameListState extends ConsumerState { + late String _issuer; + late String _account; + + @override + void initState() { + super.initState(); + _issuer = widget.credential.issuer?.trim() ?? ''; + _account = widget.credential.name.trim(); + } + + void _submit() async { + final l10n = AppLocalizations.of(context)!; + try { + // Rename credentials + final credential = CredentialData( + issuer: _issuer, + name: _account, + oathType: widget.credential.oathType, + secret: widget.credential.secret, + hashAlgorithm: widget.credential.hashAlgorithm, + digits: widget.credential.digits, + counter: widget.credential.counter, + ); + + if (!mounted) return; + Navigator.of(context).pop(credential); + showMessage(context, l10n.s_account_renamed); + } on CancellationException catch (_) { + // ignored + } catch (e) { + _log.error('Failed to add account', e); + 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_account_add_failed(errorMessage), + duration: const Duration(seconds: 4), + ); + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final credential = widget.credential; + + final remaining = getRemainingKeySpace( + oathType: credential.oathType, + period: credential.period, + issuer: _issuer, + name: _account, + ); + final issuerRemaining = remaining.first; + final nameRemaining = remaining.second; + + // is this credentials name/issuer pair different from all other? + final isUniqueFromUri = widget.credentialsFromUri + ?.where((element) => + element != credential && + element.name == _account && + (element.issuer ?? '') == _issuer) + .isEmpty ?? + false; + + final isUniqueFromDevice = widget.credentials + ?.where((element) => + element != credential && + element.name == _account && + (element.issuer ?? '') == _issuer) + .isEmpty ?? + false; + + // is this credential name/issuer of valid format + final isValidFormat = _account.isNotEmpty; + + // are the name/issuer values different from original + final didChange = (widget.credential.issuer ?? '') != _issuer || + widget.credential.name != _account; + + // can we rename with the new values + final isValid = isUniqueFromUri && isUniqueFromDevice && isValidFormat; + + return ResponsiveDialog( + title: Text(l10n.s_rename_account), + actions: [ + TextButton( + onPressed: didChange && isValid ? _submit : null, + key: keys.saveButton, + child: Text(l10n.s_save), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.q_rename_target(credential.name)), + Text(l10n.p_rename_will_change_account_displayed), + TextFormField( + initialValue: _issuer, + enabled: issuerRemaining > 0, + maxLength: issuerRemaining > 0 ? issuerRemaining : null, + buildCounter: buildByteCounterFor(_issuer), + inputFormatters: [limitBytesLength(issuerRemaining)], + key: keys.issuerField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_issuer_optional, + helperText: '', // Prevents dialog resizing when disabled + prefixIcon: const Icon(Icons.business_outlined), + ), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _issuer = value.trim(); + }); + }, + ), + TextFormField( + initialValue: _account, + maxLength: nameRemaining, + inputFormatters: [limitBytesLength(nameRemaining)], + buildCounter: buildByteCounterFor(_account), + key: keys.nameField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_account_name, + helperText: '', // Prevents dialog resizing when disabled + errorText: !isValidFormat + ? l10n.l_account_name_required + : (!isUniqueFromUri || !isUniqueFromDevice) + ? l10n.l_name_already_exists + : null, + prefixIcon: const Icon(Icons.people_alt_outlined), + ), + textInputAction: TextInputAction.done, + onChanged: (value) { + setState(() { + _account = value.trim(); + }); + }, + onFieldSubmitted: (_) { + if (didChange && isValid) { + _submit(); + } + }, + ), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} From e94e5919f12a92bb9642aa28e24e87e911256a71 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Wed, 12 Jul 2023 08:08:40 +0200 Subject: [PATCH 045/158] Added touchRequired option --- lib/oath/views/list_screen.dart | 53 +++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/lib/oath/views/list_screen.dart b/lib/oath/views/list_screen.dart index 0058584a..302fc4dc 100644 --- a/lib/oath/views/list_screen.dart +++ b/lib/oath/views/list_screen.dart @@ -32,18 +32,18 @@ class ListScreen extends ConsumerStatefulWidget { } class _ListScreenState extends ConsumerState { - bool isChecked = true; int? numCreds; late Map checkedCreds; + late Map touchEnabled; List? _credentials; - bool unique = true; - @override void initState() { super.initState(); checkedCreds = Map.fromIterable(widget.credentialsFromUri!, value: (v) => true); + touchEnabled = + Map.fromIterable(widget.credentialsFromUri!, value: (v) => false); } @override @@ -62,7 +62,7 @@ class _ListScreenState extends ConsumerState { title: Text(l10n.s_add_accounts), actions: [ TextButton( - onPressed: isValid() ? submit : null, + onPressed: isValid() && areUnique() ? submit : null, child: Text(l10n.s_save), ) ], @@ -77,8 +77,12 @@ class _ListScreenState extends ConsumerState { controlAffinity: ListTileControlAffinity.leading, secondary: Row(mainAxisSize: MainAxisSize.min, children: [ IconButton( - onPressed: () async { - _log.debug('pressed'); + isSelected: touchEnabled[cred], + color: touchEnabled[cred]! ? Colors.green : null, + onPressed: () { + setState(() { + touchEnabled[cred] = !touchEnabled[cred]!; + }); }, icon: const Icon(Icons.touch_app_outlined)), IconButton( @@ -106,6 +110,7 @@ class _ListScreenState extends ConsumerState { widget.credentialsFromUri![index] = renamed; checkedCreds[cred] = false; checkedCreds[renamed] = true; + touchEnabled[renamed] = false; }); }, icon: const Icon(Icons.edit_outlined)), @@ -113,15 +118,14 @@ class _ListScreenState extends ConsumerState { title: cred.issuer != null ? Text(cred.issuer!) : Text(cred.name), - value: isUnique(cred.name, cred.issuer ?? '') - ? (checkedCreds[cred] ?? true) - : false, - enabled: isUnique(cred.name, cred.issuer ?? ''), + value: + isUnique(cred) ? (checkedCreds[cred] ?? true) : false, + enabled: isUnique(cred), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ cred.issuer != null ? Text(cred.name) : Text(''), - isUnique(cred.name, cred.issuer ?? '') + isUnique(cred) ? Text('') : Text( l10n.l_name_already_exists, @@ -144,20 +148,32 @@ class _ListScreenState extends ConsumerState { ))); } - bool isUnique(String nameText, String? issuerText) { + bool areUnique() { + bool unique = false; + checkedCreds.forEach((k, v) => unique = unique || isUnique(k)); + return unique; + } + + bool isUnique(CredentialData cred) { + String nameText = cred.name; + String? issuerText = cred.issuer ?? ''; bool ans = _credentials ?.where((element) => element.name == nameText && (element.issuer ?? '') == issuerText) .isEmpty ?? true; + // If the credential is not unique, make sure the checkbox is not checked. + if (!ans) { + checkedCreds[cred] = false; + } return ans; } bool isValid() { int credsToAdd = 0; checkedCreds.forEach((k, v) => v ? credsToAdd++ : null); - if (numCreds! + credsToAdd <= 32) return true; + if ((credsToAdd > 0) && (numCreds! + credsToAdd <= 32)) return true; return false; } @@ -170,7 +186,10 @@ class _ListScreenState extends ConsumerState { final deviceNode = ref.watch(currentDeviceProvider); final devicePath = deviceNode?.path; if (devicePath != null) { - await _doAddCredential(devicePath: devicePath, credUri: cred.toUri()); + await _doAddCredential( + devicePath: devicePath, + credUri: cred.toUri(), + requireTouch: touchEnabled[cred]); } else if (isAndroid) { // Send the credential to Android to be added to the next YubiKey await _doAddCredential(devicePath: null, credUri: cred.toUri()); @@ -178,7 +197,9 @@ class _ListScreenState extends ConsumerState { } Future _doAddCredential( - {DevicePath? devicePath, required Uri credUri}) async { + {DevicePath? devicePath, + required Uri credUri, + bool? requireTouch}) async { try { if (devicePath == null) { assert(isAndroid, 'devicePath is only optional for Android'); @@ -186,7 +207,7 @@ class _ListScreenState extends ConsumerState { } else { await ref .read(credentialListProvider(devicePath).notifier) - .addAccount(credUri); + .addAccount(credUri, requireTouch: requireTouch!); } if (!mounted) return; //Navigator.of(context).pop(); From 3094e64f782dc2439407976460b3c51ff2fa7c82 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Thu, 13 Jul 2023 12:20:18 +0200 Subject: [PATCH 046/158] Smaller changes --- lib/oath/views/key_actions.dart | 2 +- lib/oath/views/list_screen.dart | 156 ++++++++++++++++---------------- 2 files changed, 81 insertions(+), 77 deletions(-) diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index 8fcb100d..b28815f6 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -98,7 +98,6 @@ Widget oathBuildActions( onTap: (context) async { final withContext = ref.read(withContextProvider); final credentials = ref.read(credentialsProvider); - Navigator.of(context).pop(); final qrScanner = ref.watch(qrScannerProvider); if (qrScanner != null) { final otpauth = await qrScanner.scanQr(); @@ -132,6 +131,7 @@ Widget oathBuildActions( } } } + Navigator.of(context).pop(); }), ]), ActionListSection(l10n.s_manage, children: [ diff --git a/lib/oath/views/list_screen.dart b/lib/oath/views/list_screen.dart index 302fc4dc..721eb980 100644 --- a/lib/oath/views/list_screen.dart +++ b/lib/oath/views/list_screen.dart @@ -66,86 +66,90 @@ class _ListScreenState extends ConsumerState { child: Text(l10n.s_save), ) ], - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: Column( - children: [ - Text(l10n.l_select_accounts), - //Padding(padding: EdgeInsets.only(top: 20.0, bottom: 2.0)), - ...widget.credentialsFromUri!.map( - (cred) => CheckboxListTile( - controlAffinity: ListTileControlAffinity.leading, - secondary: Row(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - isSelected: touchEnabled[cred], - color: touchEnabled[cred]! ? Colors.green : null, - onPressed: () { - setState(() { - touchEnabled[cred] = !touchEnabled[cred]!; - }); - }, - icon: const Icon(Icons.touch_app_outlined)), - IconButton( - onPressed: () async { - final node = ref - .watch(currentDeviceDataProvider) - .valueOrNull - ?.node; - final withContext = ref.read(withContextProvider); - CredentialData renamed = await withContext( - (context) async => await showBlurDialog( - context: context, - builder: (context) => RenameList( - node!, - cred, - widget.credentialsFromUri, - _credentials), - )); + child: //Padding( + //padding: const EdgeInsets.symmetric(horizontal: 18.0), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Text(l10n.l_select_accounts)), + //Padding(padding: EdgeInsets.only(top: 20.0, bottom: 2.0)), + ...widget.credentialsFromUri!.map( + (cred) => CheckboxListTile( + //contentPadding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0), + controlAffinity: ListTileControlAffinity.leading, + secondary: Row(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + isSelected: touchEnabled[cred], + color: touchEnabled[cred]! ? Colors.green : null, + onPressed: () { + setState(() { + touchEnabled[cred] = !touchEnabled[cred]!; + }); + }, + icon: const Icon(Icons.touch_app_outlined)), + IconButton( + onPressed: () async { + final node = ref + .watch(currentDeviceDataProvider) + .valueOrNull + ?.node; + final withContext = ref.read(withContextProvider); + CredentialData renamed = await withContext( + (context) async => await showBlurDialog( + context: context, + builder: (context) => RenameList( + node!, + cred, + widget.credentialsFromUri, + _credentials), + )); - setState(() { - int index = widget.credentialsFromUri!.indexWhere( - (element) => - element.name == cred.name && - (element.issuer == cred.issuer)); - widget.credentialsFromUri![index] = renamed; - checkedCreds[cred] = false; - checkedCreds[renamed] = true; - touchEnabled[renamed] = false; - }); - }, - icon: const Icon(Icons.edit_outlined)), - ]), - title: cred.issuer != null - ? Text(cred.issuer!) - : Text(cred.name), - value: - isUnique(cred) ? (checkedCreds[cred] ?? true) : false, - enabled: isUnique(cred), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - cred.issuer != null ? Text(cred.name) : Text(''), - isUnique(cred) - ? Text('') - : Text( - l10n.l_name_already_exists, - style: TextStyle(color: Colors.red), - ) - ]), - onChanged: (bool? value) { - setState(() { - checkedCreds[cred] = value!; - }); - }, - ), - ) - ] - .map((e) => Padding( + setState(() { + int index = widget.credentialsFromUri!.indexWhere( + (element) => + element.name == cred.name && + (element.issuer == cred.issuer)); + widget.credentialsFromUri![index] = renamed; + checkedCreds[cred] = false; + checkedCreds[renamed] = true; + touchEnabled[renamed] = false; + }); + }, + icon: const Icon(Icons.edit_outlined)), + ]), + title: cred.issuer != null + ? Text(cred.issuer!) + : Text(cred.name), + value: isUnique(cred) ? (checkedCreds[cred] ?? true) : false, + enabled: isUnique(cred), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + cred.issuer != null ? Text(cred.name) : Text(''), + isUnique(cred) + ? Text('') + : Text( + l10n.l_name_already_exists, + style: TextStyle(color: Colors.red), + ) + ]), + onChanged: (bool? value) { + setState(() { + checkedCreds[cred] = value!; + }); + }, + ), + ) + ] + /* .map((e) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: e, )) - .toList(), - ))); + .toList(),*/ + )); } bool areUnique() { From b8182e4b9e22675352282eed3850287c0635b1ff Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Thu, 13 Jul 2023 14:54:14 +0200 Subject: [PATCH 047/158] Change Column to Wrap and a fix for issuer --- lib/oath/views/list_screen.dart | 20 +++++++++----------- lib/oath/views/rename_list_account.dart | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/oath/views/list_screen.dart b/lib/oath/views/list_screen.dart index 721eb980..83480410 100644 --- a/lib/oath/views/list_screen.dart +++ b/lib/oath/views/list_screen.dart @@ -125,17 +125,15 @@ class _ListScreenState extends ConsumerState { : Text(cred.name), value: isUnique(cred) ? (checkedCreds[cred] ?? true) : false, enabled: isUnique(cred), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - cred.issuer != null ? Text(cred.name) : Text(''), - isUnique(cred) - ? Text('') - : Text( - l10n.l_name_already_exists, - style: TextStyle(color: Colors.red), - ) - ]), + subtitle: Wrap(children: [ + cred.issuer != null ? Text(cred.name) : Text(''), + isUnique(cred) + ? Text('') + : Text( + l10n.l_name_already_exists, + style: TextStyle(color: Colors.red), + ) + ]), onChanged: (bool? value) { setState(() { checkedCreds[cred] = value!; diff --git a/lib/oath/views/rename_list_account.dart b/lib/oath/views/rename_list_account.dart index 438a953a..015fabc1 100644 --- a/lib/oath/views/rename_list_account.dart +++ b/lib/oath/views/rename_list_account.dart @@ -62,7 +62,7 @@ class _RenameListState extends ConsumerState { try { // Rename credentials final credential = CredentialData( - issuer: _issuer, + issuer: _issuer == '' ? null : _issuer, name: _account, oathType: widget.credential.oathType, secret: widget.credential.secret, From c764eb1ab52d4b0be96ad813d86bae389403da15 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Thu, 13 Jul 2023 15:30:24 +0200 Subject: [PATCH 048/158] Fix padding --- lib/oath/views/list_screen.dart | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/oath/views/list_screen.dart b/lib/oath/views/list_screen.dart index 83480410..12cbf5ac 100644 --- a/lib/oath/views/list_screen.dart +++ b/lib/oath/views/list_screen.dart @@ -75,7 +75,7 @@ class _ListScreenState extends ConsumerState { Padding( padding: const EdgeInsets.symmetric(horizontal: 18.0), child: Text(l10n.l_select_accounts)), - //Padding(padding: EdgeInsets.only(top: 20.0, bottom: 2.0)), + const Padding(padding: EdgeInsets.symmetric(vertical: 8.0)), ...widget.credentialsFromUri!.map( (cred) => CheckboxListTile( //contentPadding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0), @@ -125,15 +125,22 @@ class _ListScreenState extends ConsumerState { : Text(cred.name), value: isUnique(cred) ? (checkedCreds[cred] ?? true) : false, enabled: isUnique(cred), - subtitle: Wrap(children: [ - cred.issuer != null ? Text(cred.name) : Text(''), - isUnique(cred) - ? Text('') - : Text( - l10n.l_name_already_exists, - style: TextStyle(color: Colors.red), - ) - ]), + subtitle: cred.issuer != null + ? Wrap(children: [ + Text(cred.name), + isUnique(cred) + ? Text('') + : Text( + l10n.l_name_already_exists, + style: TextStyle(color: Colors.red), + ) + ]) + : isUnique(cred) + ? null + : Text( + l10n.l_name_already_exists, + style: TextStyle(color: Colors.red), + ), onChanged: (bool? value) { setState(() { checkedCreds[cred] = value!; From 9a27b8f45ee6eea0fe393c0e647c274d68346d5f Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Thu, 13 Jul 2023 16:06:21 +0200 Subject: [PATCH 049/158] Change padding and marginals --- lib/oath/views/list_screen.dart | 166 ++++++++++++++++---------------- 1 file changed, 81 insertions(+), 85 deletions(-) diff --git a/lib/oath/views/list_screen.dart b/lib/oath/views/list_screen.dart index 12cbf5ac..219ffabd 100644 --- a/lib/oath/views/list_screen.dart +++ b/lib/oath/views/list_screen.dart @@ -69,92 +69,88 @@ class _ListScreenState extends ConsumerState { child: //Padding( //padding: const EdgeInsets.symmetric(horizontal: 18.0), Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: Text(l10n.l_select_accounts)), - const Padding(padding: EdgeInsets.symmetric(vertical: 8.0)), - ...widget.credentialsFromUri!.map( - (cred) => CheckboxListTile( - //contentPadding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0), - controlAffinity: ListTileControlAffinity.leading, - secondary: Row(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - isSelected: touchEnabled[cred], - color: touchEnabled[cred]! ? Colors.green : null, - onPressed: () { - setState(() { - touchEnabled[cred] = !touchEnabled[cred]!; - }); - }, - icon: const Icon(Icons.touch_app_outlined)), - IconButton( - onPressed: () async { - final node = ref - .watch(currentDeviceDataProvider) - .valueOrNull - ?.node; - final withContext = ref.read(withContextProvider); - CredentialData renamed = await withContext( - (context) async => await showBlurDialog( - context: context, - builder: (context) => RenameList( - node!, - cred, - widget.credentialsFromUri, - _credentials), - )); + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Text(l10n.l_select_accounts)), + //const Padding(padding: EdgeInsets.symmetric(vertical: 8.0)), + ...widget.credentialsFromUri!.map( + (cred) => CheckboxListTile( + //contentPadding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0), + controlAffinity: ListTileControlAffinity.leading, + secondary: Row(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + isSelected: touchEnabled[cred], + color: touchEnabled[cred]! ? Colors.green : null, + onPressed: () { + setState(() { + touchEnabled[cred] = !touchEnabled[cred]!; + }); + }, + icon: const Icon(Icons.touch_app_outlined)), + IconButton( + onPressed: () async { + final node = ref + .watch(currentDeviceDataProvider) + .valueOrNull + ?.node; + final withContext = ref.read(withContextProvider); + CredentialData renamed = await withContext( + (context) async => await showBlurDialog( + context: context, + builder: (context) => RenameList(node!, cred, + widget.credentialsFromUri, _credentials), + )); - setState(() { - int index = widget.credentialsFromUri!.indexWhere( - (element) => - element.name == cred.name && - (element.issuer == cred.issuer)); - widget.credentialsFromUri![index] = renamed; - checkedCreds[cred] = false; - checkedCreds[renamed] = true; - touchEnabled[renamed] = false; - }); - }, - icon: const Icon(Icons.edit_outlined)), - ]), - title: cred.issuer != null - ? Text(cred.issuer!) - : Text(cred.name), - value: isUnique(cred) ? (checkedCreds[cred] ?? true) : false, - enabled: isUnique(cred), - subtitle: cred.issuer != null - ? Wrap(children: [ - Text(cred.name), - isUnique(cred) - ? Text('') - : Text( - l10n.l_name_already_exists, - style: TextStyle(color: Colors.red), - ) - ]) - : isUnique(cred) - ? null - : Text( - l10n.l_name_already_exists, - style: TextStyle(color: Colors.red), - ), - onChanged: (bool? value) { - setState(() { - checkedCreds[cred] = value!; - }); - }, - ), - ) - ] - /* .map((e) => Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: e, - )) - .toList(),*/ - )); + setState(() { + int index = widget.credentialsFromUri!.indexWhere( + (element) => + element.name == cred.name && + (element.issuer == cred.issuer)); + widget.credentialsFromUri![index] = renamed; + checkedCreds[cred] = false; + checkedCreds[renamed] = true; + touchEnabled[renamed] = false; + }); + }, + icon: const Icon(Icons.edit_outlined)), + ]), + title: + cred.issuer != null ? Text(cred.issuer!) : Text(cred.name), + value: isUnique(cred) ? (checkedCreds[cred] ?? true) : false, + enabled: isUnique(cred), + subtitle: cred.issuer != null + ? Wrap(children: [ + Text(cred.name), + isUnique(cred) + ? Text('') + : Text( + l10n.l_name_already_exists, + style: TextStyle(color: Colors.red), + ) + ]) + : isUnique(cred) + ? null + : Text( + l10n.l_name_already_exists, + style: TextStyle(color: Colors.red), + ), + onChanged: (bool? value) { + setState(() { + checkedCreds[cred] = value!; + }); + }, + ), + ) + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + )); } bool areUnique() { From a6be6a60064d6aa868a8f0a3917f740e93c46e5b Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Fri, 14 Jul 2023 09:04:13 +0200 Subject: [PATCH 050/158] Change colors --- lib/oath/views/list_screen.dart | 12 +++++++++--- lib/oath/views/rename_list_account.dart | 5 ++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/oath/views/list_screen.dart b/lib/oath/views/list_screen.dart index 219ffabd..6a203938 100644 --- a/lib/oath/views/list_screen.dart +++ b/lib/oath/views/list_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:yubico_authenticator/app/logging.dart'; +import 'package:yubico_authenticator/theme.dart'; import '../../android/oath/state.dart'; import '../../app/models.dart'; @@ -48,6 +49,9 @@ class _ListScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final darkMode = theme.brightness == Brightness.dark; + final l10n = AppLocalizations.of(context)!; numCreds = ref.watch(credentialListProvider(widget.devicePath) .select((value) => value?.length)); @@ -83,7 +87,9 @@ class _ListScreenState extends ConsumerState { secondary: Row(mainAxisSize: MainAxisSize.min, children: [ IconButton( isSelected: touchEnabled[cred], - color: touchEnabled[cred]! ? Colors.green : null, + color: touchEnabled[cred]! + ? (darkMode ? primaryGreen : primaryBlue) + : null, onPressed: () { setState(() { touchEnabled[cred] = !touchEnabled[cred]!; @@ -128,14 +134,14 @@ class _ListScreenState extends ConsumerState { ? Text('') : Text( l10n.l_name_already_exists, - style: TextStyle(color: Colors.red), + style: TextStyle(color: primaryRed), ) ]) : isUnique(cred) ? null : Text( l10n.l_name_already_exists, - style: TextStyle(color: Colors.red), + style: TextStyle(color: primaryRed), ), onChanged: (bool? value) { setState(() { diff --git a/lib/oath/views/rename_list_account.dart b/lib/oath/views/rename_list_account.dart index 015fabc1..6b6eed15 100644 --- a/lib/oath/views/rename_list_account.dart +++ b/lib/oath/views/rename_list_account.dart @@ -148,7 +148,10 @@ class _RenameListState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.q_rename_target(credential.name)), + credential.issuer != null + ? Text(l10n.q_rename_target( + '${credential.issuer} (${credential.name})')) + : Text(l10n.q_rename_target(credential.name)), Text(l10n.p_rename_will_change_account_displayed), TextFormField( initialValue: _issuer, From 7d26c4d40955a4b9fea34cd4d91075f12bbb1b8d Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Fri, 14 Jul 2023 09:51:32 +0200 Subject: [PATCH 051/158] Fixup --- lib/oath/views/list_screen.dart | 102 +++++++++++++++++++------------- 1 file changed, 61 insertions(+), 41 deletions(-) diff --git a/lib/oath/views/list_screen.dart b/lib/oath/views/list_screen.dart index 6a203938..b8bc7775 100644 --- a/lib/oath/views/list_screen.dart +++ b/lib/oath/views/list_screen.dart @@ -90,58 +90,71 @@ class _ListScreenState extends ConsumerState { color: touchEnabled[cred]! ? (darkMode ? primaryGreen : primaryBlue) : null, - onPressed: () { - setState(() { - touchEnabled[cred] = !touchEnabled[cred]!; - }); - }, + onPressed: isUnique(cred) + ? () { + setState(() { + touchEnabled[cred] = !touchEnabled[cred]!; + }); + } + : null, icon: const Icon(Icons.touch_app_outlined)), IconButton( - onPressed: () async { - final node = ref - .watch(currentDeviceDataProvider) - .valueOrNull - ?.node; - final withContext = ref.read(withContextProvider); - CredentialData renamed = await withContext( - (context) async => await showBlurDialog( - context: context, - builder: (context) => RenameList(node!, cred, - widget.credentialsFromUri, _credentials), - )); + onPressed: () async { + final node = ref + .watch(currentDeviceDataProvider) + .valueOrNull + ?.node; + final withContext = ref.read(withContextProvider); + CredentialData renamed = await withContext( + (context) async => await showBlurDialog( + context: context, + builder: (context) => RenameList(node!, cred, + widget.credentialsFromUri, _credentials), + )); - setState(() { - int index = widget.credentialsFromUri!.indexWhere( - (element) => - element.name == cred.name && - (element.issuer == cred.issuer)); - widget.credentialsFromUri![index] = renamed; - checkedCreds[cred] = false; - checkedCreds[renamed] = true; - touchEnabled[renamed] = false; - }); - }, - icon: const Icon(Icons.edit_outlined)), + setState(() { + int index = widget.credentialsFromUri!.indexWhere( + (element) => + element.name == cred.name && + (element.issuer == cred.issuer)); + widget.credentialsFromUri![index] = renamed; + checkedCreds[cred] = false; + checkedCreds[renamed] = true; + touchEnabled[renamed] = false; + }); + }, + icon: const Icon(Icons.edit_outlined), + color: Colors.white, + ), ]), - title: - cred.issuer != null ? Text(cred.issuer!) : Text(cred.name), + title: Text(getTitle(cred), + overflow: TextOverflow.fade, maxLines: 1, softWrap: false), + value: isUnique(cred) ? (checkedCreds[cred] ?? true) : false, enabled: isUnique(cred), subtitle: cred.issuer != null - ? Wrap(children: [ - Text(cred.name), - isUnique(cred) - ? Text('') - : Text( - l10n.l_name_already_exists, - style: TextStyle(color: primaryRed), - ) - ]) + ? Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(cred.name, + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: false), + isUnique(cred) + ? Text('') + : Text( + l10n.l_name_already_exists, + style: TextStyle( + color: primaryRed, + fontSize: 12, + ), + ) + ]) : isUnique(cred) ? null : Text( l10n.l_name_already_exists, - style: TextStyle(color: primaryRed), + style: TextStyle(color: primaryRed, fontSize: 12), ), onChanged: (bool? value) { setState(() { @@ -159,6 +172,13 @@ class _ListScreenState extends ConsumerState { )); } + String getTitle(CredentialData cred) { + if (cred.issuer != null) { + return cred.issuer!; + } + return cred.name; + } + bool areUnique() { bool unique = false; checkedCreds.forEach((k, v) => unique = unique || isUnique(k)); From 5d0afa3bfb1646f10baf1580d88f25e56e98fb78 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Fri, 14 Jul 2023 10:55:22 +0200 Subject: [PATCH 052/158] Small fixup --- lib/oath/views/list_screen.dart | 37 ++++++++++++++------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/lib/oath/views/list_screen.dart b/lib/oath/views/list_screen.dart index b8bc7775..7a883c0f 100644 --- a/lib/oath/views/list_screen.dart +++ b/lib/oath/views/list_screen.dart @@ -124,7 +124,7 @@ class _ListScreenState extends ConsumerState { }); }, icon: const Icon(Icons.edit_outlined), - color: Colors.white, + color: darkMode ? Colors.white : Colors.black, ), ]), title: Text(getTitle(cred), @@ -132,30 +132,25 @@ class _ListScreenState extends ConsumerState { value: isUnique(cred) ? (checkedCreds[cred] ?? true) : false, enabled: isUnique(cred), - subtitle: cred.issuer != null + subtitle: cred.issuer != null || !isUnique(cred) ? Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text(cred.name, - overflow: TextOverflow.fade, - maxLines: 1, - softWrap: false), - isUnique(cred) - ? Text('') - : Text( - l10n.l_name_already_exists, - style: TextStyle( - color: primaryRed, - fontSize: 12, - ), - ) + if (cred.issuer != null) + Text(cred.name, + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: false), + if (!isUnique(cred)) + Text( + l10n.l_name_already_exists, + style: const TextStyle( + color: primaryRed, + fontSize: 12, + ), + ) ]) - : isUnique(cred) - ? null - : Text( - l10n.l_name_already_exists, - style: TextStyle(color: primaryRed, fontSize: 12), - ), + : null, onChanged: (bool? value) { setState(() { checkedCreds[cred] = value!; From c5cd264f1329567bd3094f68c81962d04278c9fd Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Wed, 19 Jul 2023 10:20:25 +0200 Subject: [PATCH 053/158] Check capacity and if touch is supported --- lib/oath/views/key_actions.dart | 3 +- lib/oath/views/list_screen.dart | 71 +++++++++++++++++++-------------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index b28815f6..71bede2c 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -111,7 +111,8 @@ Widget oathBuildActions( await withContext((context) async { await showBlurDialog( context: context, - builder: (context) => ListScreen(devicePath, data), + builder: (context) => + ListScreen(devicePath, oathState, data), ); }); } else if (otpauth.contains('otpauth')) { diff --git a/lib/oath/views/list_screen.dart b/lib/oath/views/list_screen.dart index 7a883c0f..128163ec 100644 --- a/lib/oath/views/list_screen.dart +++ b/lib/oath/views/list_screen.dart @@ -23,9 +23,10 @@ final _log = Logger('oath.views.list_screen'); class ListScreen extends ConsumerStatefulWidget { final DevicePath devicePath; + final OathState? state; final List? credentialsFromUri; - const ListScreen(this.devicePath, this.credentialsFromUri) + const ListScreen(this.devicePath, this.state, this.credentialsFromUri) : super(key: setOrManagePasswordAction); @override @@ -62,42 +63,41 @@ class _ListScreenState extends ConsumerState { ?.map((e) => e.credential) .toList(); + // If the credential is not unique, make sure the checkbox is not checked + uncheckDuplicates(); + return ResponsiveDialog( title: Text(l10n.s_add_accounts), actions: [ TextButton( - onPressed: isValid() && areUnique() ? submit : null, + onPressed: isValid() ? submit : null, child: Text(l10n.s_save), ) ], - child: //Padding( - //padding: const EdgeInsets.symmetric(horizontal: 18.0), - Column( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 18.0), child: Text(l10n.l_select_accounts)), - //const Padding(padding: EdgeInsets.symmetric(vertical: 8.0)), ...widget.credentialsFromUri!.map( (cred) => CheckboxListTile( - //contentPadding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0), controlAffinity: ListTileControlAffinity.leading, secondary: Row(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - isSelected: touchEnabled[cred], - color: touchEnabled[cred]! - ? (darkMode ? primaryGreen : primaryBlue) - : null, - onPressed: isUnique(cred) - ? () { - setState(() { - touchEnabled[cred] = !touchEnabled[cred]!; - }); - } - : null, - icon: const Icon(Icons.touch_app_outlined)), + if (isTouchSupported()) + IconButton( + color: touchEnabled[cred]! + ? (darkMode ? primaryGreen : primaryBlue) + : null, + onPressed: isUnique(cred) + ? () { + setState(() { + touchEnabled[cred] = !touchEnabled[cred]!; + }); + } + : null, + icon: const Icon(Icons.touch_app_outlined)), IconButton( onPressed: () async { final node = ref @@ -129,7 +129,6 @@ class _ListScreenState extends ConsumerState { ]), title: Text(getTitle(cred), overflow: TextOverflow.fade, maxLines: 1, softWrap: false), - value: isUnique(cred) ? (checkedCreds[cred] ?? true) : false, enabled: isUnique(cred), subtitle: cred.issuer != null || !isUnique(cred) @@ -167,6 +166,15 @@ class _ListScreenState extends ConsumerState { )); } + bool isTouchSupported() { + bool touch = true; + if (!(widget.state?.version.isAtLeast(4, 2) ?? true)) { + // Touch not supported + touch = false; + } + return touch; + } + String getTitle(CredentialData cred) { if (cred.issuer != null) { return cred.issuer!; @@ -174,10 +182,14 @@ class _ListScreenState extends ConsumerState { return cred.name; } - bool areUnique() { - bool unique = false; - checkedCreds.forEach((k, v) => unique = unique || isUnique(k)); - return unique; + void uncheckDuplicates() { + for (var item in checkedCreds.entries) { + CredentialData cred = item.key; + + if (!isUnique(cred)) { + checkedCreds[cred] = false; + } + } } bool isUnique(CredentialData cred) { @@ -189,17 +201,16 @@ class _ListScreenState extends ConsumerState { (element.issuer ?? '') == issuerText) .isEmpty ?? true; - // If the credential is not unique, make sure the checkbox is not checked. - if (!ans) { - checkedCreds[cred] = false; - } + return ans; } bool isValid() { int credsToAdd = 0; + int? capacity = widget.state!.version.isAtLeast(4) ? 32 : null; checkedCreds.forEach((k, v) => v ? credsToAdd++ : null); - if ((credsToAdd > 0) && (numCreds! + credsToAdd <= 32)) return true; + if ((credsToAdd > 0) && + (capacity == null || (numCreds! + credsToAdd <= capacity))) return true; return false; } From 90bae74e531bdb392eca8a2ff845924655ad1b10 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Thu, 20 Jul 2023 10:24:03 +0200 Subject: [PATCH 054/158] Map for keeping track of duplicates --- lib/oath/views/list_screen.dart | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/oath/views/list_screen.dart b/lib/oath/views/list_screen.dart index 128163ec..3cc570aa 100644 --- a/lib/oath/views/list_screen.dart +++ b/lib/oath/views/list_screen.dart @@ -21,22 +21,24 @@ import 'rename_list_account.dart'; final _log = Logger('oath.views.list_screen'); -class ListScreen extends ConsumerStatefulWidget { +class MigrateAccountPage extends ConsumerStatefulWidget { final DevicePath devicePath; final OathState? state; final List? credentialsFromUri; - const ListScreen(this.devicePath, this.state, this.credentialsFromUri) + const MigrateAccountPage(this.devicePath, this.state, this.credentialsFromUri) : super(key: setOrManagePasswordAction); @override - ConsumerState createState() => _ListScreenState(); + ConsumerState createState() => + _MigrateAccountPageState(); } -class _ListScreenState extends ConsumerState { +class _MigrateAccountPageState extends ConsumerState { int? numCreds; late Map checkedCreds; late Map touchEnabled; + late Map uniqueCreds; List? _credentials; @override @@ -46,6 +48,8 @@ class _ListScreenState extends ConsumerState { Map.fromIterable(widget.credentialsFromUri!, value: (v) => true); touchEnabled = Map.fromIterable(widget.credentialsFromUri!, value: (v) => false); + uniqueCreds = + Map.fromIterable(widget.credentialsFromUri!, value: (v) => false); } @override @@ -63,6 +67,7 @@ class _ListScreenState extends ConsumerState { ?.map((e) => e.credential) .toList(); + checkForDuplicates(); // If the credential is not unique, make sure the checkbox is not checked uncheckDuplicates(); @@ -90,7 +95,7 @@ class _ListScreenState extends ConsumerState { color: touchEnabled[cred]! ? (darkMode ? primaryGreen : primaryBlue) : null, - onPressed: isUnique(cred) + onPressed: uniqueCreds[cred]! ? () { setState(() { touchEnabled[cred] = !touchEnabled[cred]!; @@ -129,9 +134,10 @@ class _ListScreenState extends ConsumerState { ]), title: Text(getTitle(cred), overflow: TextOverflow.fade, maxLines: 1, softWrap: false), - value: isUnique(cred) ? (checkedCreds[cred] ?? true) : false, - enabled: isUnique(cred), - subtitle: cred.issuer != null || !isUnique(cred) + value: + uniqueCreds[cred]! ? (checkedCreds[cred] ?? true) : false, + enabled: uniqueCreds[cred]!, + subtitle: cred.issuer != null || !uniqueCreds[cred]! ? Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -140,7 +146,7 @@ class _ListScreenState extends ConsumerState { overflow: TextOverflow.fade, maxLines: 1, softWrap: false), - if (!isUnique(cred)) + if (!uniqueCreds[cred]!) Text( l10n.l_name_already_exists, style: const TextStyle( @@ -182,11 +188,18 @@ class _ListScreenState extends ConsumerState { return cred.name; } + void checkForDuplicates() { + for (var item in checkedCreds.entries) { + CredentialData cred = item.key; + uniqueCreds[cred] = isUnique(cred); + } + } + void uncheckDuplicates() { for (var item in checkedCreds.entries) { CredentialData cred = item.key; - if (!isUnique(cred)) { + if (!uniqueCreds[cred]!) { checkedCreds[cred] = false; } } From 5958ba4e700b959f72b6ee3a6d798f0b85cde366 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Thu, 20 Jul 2023 10:25:30 +0200 Subject: [PATCH 055/158] Rename file --- lib/oath/views/key_actions.dart | 4 ++-- .../views/{list_screen.dart => migrate_account_page.dart} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename lib/oath/views/{list_screen.dart => migrate_account_page.dart} (100%) diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index 71bede2c..ff839997 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -32,7 +32,7 @@ import '../keys.dart' as keys; import 'add_account_page.dart'; import 'manage_password_dialog.dart'; import 'reset_dialog.dart'; -import 'list_screen.dart'; +import 'migrate_account_page.dart'; Widget oathBuildActions( BuildContext context, @@ -112,7 +112,7 @@ Widget oathBuildActions( await showBlurDialog( context: context, builder: (context) => - ListScreen(devicePath, oathState, data), + MigrateAccountPage(devicePath, oathState, data), ); }); } else if (otpauth.contains('otpauth')) { diff --git a/lib/oath/views/list_screen.dart b/lib/oath/views/migrate_account_page.dart similarity index 100% rename from lib/oath/views/list_screen.dart rename to lib/oath/views/migrate_account_page.dart From 6884b5e3c4ce028be52dfd7954960d367fccf449 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Mon, 24 Jul 2023 14:15:19 +0200 Subject: [PATCH 056/158] l10n: account already exists --- lib/l10n/app_en.arb | 1 + lib/oath/views/migrate_account_page.dart | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f157acbf..1af81c8b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -295,6 +295,7 @@ }, "l_account_name_required": "Your account must have a name", "l_name_already_exists": "This name already exists for the issuer", + "l_account_already_exists": "This account already exists on the YubiKey", "l_invalid_character_issuer": "Invalid character: ':' is not allowed in issuer", "l_select_accounts" : "Select account(s) to add to the YubiKey", "s_pinned": "Pinned", diff --git a/lib/oath/views/migrate_account_page.dart b/lib/oath/views/migrate_account_page.dart index 3cc570aa..2025518f 100644 --- a/lib/oath/views/migrate_account_page.dart +++ b/lib/oath/views/migrate_account_page.dart @@ -2,10 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:yubico_authenticator/app/logging.dart'; +import 'package:yubico_authenticator/exception/apdu_exception.dart'; import 'package:yubico_authenticator/theme.dart'; import '../../android/oath/state.dart'; import '../../app/models.dart'; +import '../../desktop/models.dart'; import '../../widgets/responsive_dialog.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -148,7 +150,7 @@ class _MigrateAccountPageState extends ConsumerState { softWrap: false), if (!uniqueCreds[cred]!) Text( - l10n.l_name_already_exists, + l10n.l_account_already_exists, style: const TextStyle( color: primaryRed, fontSize: 12, @@ -250,6 +252,7 @@ class _MigrateAccountPageState extends ConsumerState { {DevicePath? devicePath, required Uri credUri, bool? requireTouch}) async { + final l10n = AppLocalizations.of(context)!; try { if (devicePath == null) { assert(isAndroid, 'devicePath is only optional for Android'); @@ -261,13 +264,25 @@ class _MigrateAccountPageState extends ConsumerState { } if (!mounted) return; //Navigator.of(context).pop(); - showMessage(context, 'added'); + showMessage(context, l10n.s_account_added); } on CancellationException catch (_) { // ignored } catch (e) { - _log.debug('Failed to add account'); + _log.error('Failed to add account', e); final String errorMessage; // TODO: Make this cleaner than importing desktop specific RpcError. + if (e is RpcError) { + errorMessage = e.message; + } else if (e is ApduException) { + errorMessage = e.message; + } else { + errorMessage = e.toString(); + } + showMessage( + context, + l10n.l_account_add_failed(errorMessage), + duration: const Duration(seconds: 4), + ); } } } From f4ead734a1de4da333dd34849a24c9e4cce3ed64 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Thu, 27 Jul 2023 11:41:43 +0200 Subject: [PATCH 057/158] Replace Pair with records --- lib/oath/views/rename_list_account.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/oath/views/rename_list_account.dart b/lib/oath/views/rename_list_account.dart index 6b6eed15..db3b61d6 100644 --- a/lib/oath/views/rename_list_account.dart +++ b/lib/oath/views/rename_list_account.dart @@ -98,14 +98,12 @@ class _RenameListState extends ConsumerState { final l10n = AppLocalizations.of(context)!; final credential = widget.credential; - final remaining = getRemainingKeySpace( + final (issuerRemaining, nameRemaining) = getRemainingKeySpace( oathType: credential.oathType, period: credential.period, issuer: _issuer, name: _account, ); - final issuerRemaining = remaining.first; - final nameRemaining = remaining.second; // is this credentials name/issuer pair different from all other? final isUniqueFromUri = widget.credentialsFromUri From 67925601a0c71708cc39c2e272fc1f925659b3e0 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Thu, 27 Jul 2023 14:25:10 +0200 Subject: [PATCH 058/158] Fix for responsive dialog freeze --- lib/widgets/responsive_dialog.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/widgets/responsive_dialog.dart b/lib/widgets/responsive_dialog.dart index d5da0006..871423ce 100755 --- a/lib/widgets/responsive_dialog.dart +++ b/lib/widgets/responsive_dialog.dart @@ -40,6 +40,7 @@ class ResponsiveDialog extends StatefulWidget { class _ResponsiveDialogState extends State { final Key _childKey = GlobalKey(); final _focus = FocusScopeNode(); + bool _hasLostFocus = false; @override void dispose() { @@ -101,8 +102,9 @@ class _ResponsiveDialogState extends State { node: _focus, autofocus: true, onFocusChange: (focused) { - if (!focused) { + if (!focused && !_hasLostFocus) { _focus.requestFocus(); + _hasLostFocus = true; } }, child: constraints.maxWidth < 540 From 75197726ced0fa81c4180cb28a7033cb75511ac4 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Thu, 27 Jul 2023 14:25:32 +0200 Subject: [PATCH 059/158] Small cleanup --- lib/oath/keys.dart | 1 + lib/oath/models.dart | 16 ------ lib/oath/views/add_account_page.dart | 4 +- lib/oath/views/key_actions.dart | 7 ++- lib/oath/views/migrate_account_page.dart | 65 ++++++++++++------------ lib/oath/views/rename_list_account.dart | 8 ++- 6 files changed, 42 insertions(+), 59 deletions(-) diff --git a/lib/oath/keys.dart b/lib/oath/keys.dart index 910890bf..6539938a 100755 --- a/lib/oath/keys.dart +++ b/lib/oath/keys.dart @@ -27,6 +27,7 @@ final searchAccountsField = GlobalKey(); const setOrManagePasswordAction = Key('$_keyAction.action.set_or_manage_password'); const addAccountAction = Key('$_keyAction.add_account'); +const migrateAccountAction = Key('$_keyAction.migrate_account'); const resetAction = Key('$_keyAction.reset'); const customIconsAction = Key('$_keyAction.custom_icons'); diff --git a/lib/oath/models.dart b/lib/oath/models.dart index 7d44fc35..3c49a78e 100755 --- a/lib/oath/models.dart +++ b/lib/oath/models.dart @@ -127,16 +127,6 @@ class CredentialData with _$CredentialData { factory CredentialData.fromJson(Map json) => _$CredentialDataFromJson(json); - static List multiFromUri(uri) { - if (uri.scheme.toLowerCase() == 'otpauth-migration') { - return CredentialData.fromMigration(uri); - } else if (uri.scheme.toLowerCase() == 'otpauth') { - return [CredentialData.fromUri(uri)]; - } else { - throw ArgumentError('Invalid scheme'); - } - } - static List fromMigration(uri) { List read(Uint8List bytes) { final index = bytes[0]; @@ -249,12 +239,6 @@ class CredentialData with _$CredentialData { tag = data[0]; } - // Print all the extracted credentials - for (var credential in credentials) { - _log.debug( - '${credential.issuer} (${credential.name}) ${credential.secret}'); - } - return credentials; } diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index d78a6360..b99a88ca 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -130,8 +130,8 @@ class _OathAddAccountPageState extends ConsumerState { _qrState = _QrScanState.failed; }); } else { - final data = CredentialData.multiFromUri(Uri.parse(otpauth)); - _loadCredentialData(data[0]); // TODO + final data = CredentialData.fromUri(Uri.parse(otpauth)); + _loadCredentialData(data); } } catch (e) { final String errorMessage; diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index ff839997..27ee2798 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -107,7 +107,7 @@ Widget oathBuildActions( String s = 'otpauth-migration'; if (otpauth.contains(s)) { final data = - CredentialData.multiFromUri(Uri.parse(otpauth)); + CredentialData.fromMigration(Uri.parse(otpauth)); await withContext((context) async { await showBlurDialog( context: context, @@ -116,8 +116,7 @@ Widget oathBuildActions( ); }); } else if (otpauth.contains('otpauth')) { - final data = - CredentialData.multiFromUri(Uri.parse(otpauth)); + final data = CredentialData.fromUri(Uri.parse(otpauth)); await withContext((context) async { await showBlurDialog( context: context, @@ -125,7 +124,7 @@ Widget oathBuildActions( devicePath, oathState, credentials: credentials, - credentialData: data[0], + credentialData: data, ), ); }); diff --git a/lib/oath/views/migrate_account_page.dart b/lib/oath/views/migrate_account_page.dart index 2025518f..4673f5ed 100644 --- a/lib/oath/views/migrate_account_page.dart +++ b/lib/oath/views/migrate_account_page.dart @@ -29,7 +29,7 @@ class MigrateAccountPage extends ConsumerStatefulWidget { final List? credentialsFromUri; const MigrateAccountPage(this.devicePath, this.state, this.credentialsFromUri) - : super(key: setOrManagePasswordAction); + : super(key: migrateAccountAction); @override ConsumerState createState() => @@ -37,20 +37,20 @@ class MigrateAccountPage extends ConsumerStatefulWidget { } class _MigrateAccountPageState extends ConsumerState { - int? numCreds; - late Map checkedCreds; - late Map touchEnabled; - late Map uniqueCreds; + int? _numCreds; + late Map _checkedCreds; + late Map _touchEnabled; + late Map _uniqueCreds; List? _credentials; @override void initState() { super.initState(); - checkedCreds = + _checkedCreds = Map.fromIterable(widget.credentialsFromUri!, value: (v) => true); - touchEnabled = + _touchEnabled = Map.fromIterable(widget.credentialsFromUri!, value: (v) => false); - uniqueCreds = + _uniqueCreds = Map.fromIterable(widget.credentialsFromUri!, value: (v) => false); } @@ -58,10 +58,7 @@ class _MigrateAccountPageState extends ConsumerState { Widget build(BuildContext context) { final theme = Theme.of(context); final darkMode = theme.brightness == Brightness.dark; - final l10n = AppLocalizations.of(context)!; - numCreds = ref.watch(credentialListProvider(widget.devicePath) - .select((value) => value?.length)); final deviceNode = ref.watch(currentDeviceProvider); _credentials = ref @@ -69,6 +66,9 @@ class _MigrateAccountPageState extends ConsumerState { ?.map((e) => e.credential) .toList(); + _numCreds = ref.watch(credentialListProvider(widget.devicePath) + .select((value) => value?.length)); + checkForDuplicates(); // If the credential is not unique, make sure the checkbox is not checked uncheckDuplicates(); @@ -94,13 +94,13 @@ class _MigrateAccountPageState extends ConsumerState { secondary: Row(mainAxisSize: MainAxisSize.min, children: [ if (isTouchSupported()) IconButton( - color: touchEnabled[cred]! + color: _touchEnabled[cred]! ? (darkMode ? primaryGreen : primaryBlue) : null, - onPressed: uniqueCreds[cred]! + onPressed: _uniqueCreds[cred]! ? () { setState(() { - touchEnabled[cred] = !touchEnabled[cred]!; + _touchEnabled[cred] = !_touchEnabled[cred]!; }); } : null, @@ -125,9 +125,9 @@ class _MigrateAccountPageState extends ConsumerState { element.name == cred.name && (element.issuer == cred.issuer)); widget.credentialsFromUri![index] = renamed; - checkedCreds[cred] = false; - checkedCreds[renamed] = true; - touchEnabled[renamed] = false; + _checkedCreds[cred] = false; + _checkedCreds[renamed] = true; + _touchEnabled[renamed] = false; }); }, icon: const Icon(Icons.edit_outlined), @@ -136,10 +136,9 @@ class _MigrateAccountPageState extends ConsumerState { ]), title: Text(getTitle(cred), overflow: TextOverflow.fade, maxLines: 1, softWrap: false), - value: - uniqueCreds[cred]! ? (checkedCreds[cred] ?? true) : false, - enabled: uniqueCreds[cred]!, - subtitle: cred.issuer != null || !uniqueCreds[cred]! + value: _uniqueCreds[cred]! ? _checkedCreds[cred] : false, + enabled: _uniqueCreds[cred]!, + subtitle: cred.issuer != null || !_uniqueCreds[cred]! ? Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -148,7 +147,7 @@ class _MigrateAccountPageState extends ConsumerState { overflow: TextOverflow.fade, maxLines: 1, softWrap: false), - if (!uniqueCreds[cred]!) + if (!_uniqueCreds[cred]!) Text( l10n.l_account_already_exists, style: const TextStyle( @@ -160,7 +159,7 @@ class _MigrateAccountPageState extends ConsumerState { : null, onChanged: (bool? value) { setState(() { - checkedCreds[cred] = value!; + _checkedCreds[cred] = value!; }); }, ), @@ -191,18 +190,18 @@ class _MigrateAccountPageState extends ConsumerState { } void checkForDuplicates() { - for (var item in checkedCreds.entries) { + for (var item in _checkedCreds.entries) { CredentialData cred = item.key; - uniqueCreds[cred] = isUnique(cred); + _uniqueCreds[cred] = isUnique(cred); } } void uncheckDuplicates() { - for (var item in checkedCreds.entries) { + for (var item in _checkedCreds.entries) { CredentialData cred = item.key; - if (!uniqueCreds[cred]!) { - checkedCreds[cred] = false; + if (!_uniqueCreds[cred]!) { + _checkedCreds[cred] = false; } } } @@ -223,14 +222,16 @@ class _MigrateAccountPageState extends ConsumerState { bool isValid() { int credsToAdd = 0; int? capacity = widget.state!.version.isAtLeast(4) ? 32 : null; - checkedCreds.forEach((k, v) => v ? credsToAdd++ : null); + _checkedCreds.forEach((k, v) => v ? credsToAdd++ : null); if ((credsToAdd > 0) && - (capacity == null || (numCreds! + credsToAdd <= capacity))) return true; + (capacity == null || (_numCreds! + credsToAdd <= capacity))) { + return true; + } return false; } void submit() async { - checkedCreds.forEach((k, v) => v ? accept(k) : null); + _checkedCreds.forEach((k, v) => v ? accept(k) : null); Navigator.of(context).pop(); } @@ -241,7 +242,7 @@ class _MigrateAccountPageState extends ConsumerState { await _doAddCredential( devicePath: devicePath, credUri: cred.toUri(), - requireTouch: touchEnabled[cred]); + requireTouch: _touchEnabled[cred]); } else if (isAndroid) { // Send the credential to Android to be added to the next YubiKey await _doAddCredential(devicePath: null, credUri: cred.toUri()); diff --git a/lib/oath/views/rename_list_account.dart b/lib/oath/views/rename_list_account.dart index db3b61d6..e75db433 100644 --- a/lib/oath/views/rename_list_account.dart +++ b/lib/oath/views/rename_list_account.dart @@ -105,7 +105,7 @@ class _RenameListState extends ConsumerState { name: _account, ); - // is this credentials name/issuer pair different from all other? + // is this credential's name/issuer pair different from all other? final isUniqueFromUri = widget.credentialsFromUri ?.where((element) => element != credential && @@ -116,13 +116,11 @@ class _RenameListState extends ConsumerState { final isUniqueFromDevice = widget.credentials ?.where((element) => - element != credential && - element.name == _account && - (element.issuer ?? '') == _issuer) + element.name == _account && (element.issuer ?? '') == _issuer) .isEmpty ?? false; - // is this credential name/issuer of valid format + // is this credential's name/issuer of valid format final isValidFormat = _account.isNotEmpty; // are the name/issuer values different from original From ed7f99b8773d8d9731c85e30d4e7ab34d002fd0b Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Thu, 27 Jul 2023 15:47:50 +0200 Subject: [PATCH 060/158] Solve errors --- lib/oath/models.dart | 4 ---- lib/oath/views/key_actions.dart | 6 ++++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/oath/models.dart b/lib/oath/models.dart index 3c49a78e..bb48e0b9 100755 --- a/lib/oath/models.dart +++ b/lib/oath/models.dart @@ -17,10 +17,8 @@ import 'dart:typed_data'; import 'dart:convert'; import 'package:base32/base32.dart'; -import 'package:logging/logging.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:yubico_authenticator/app/logging.dart'; import '../core/models.dart'; @@ -33,8 +31,6 @@ const defaultCounter = 0; const defaultOathType = OathType.totp; const defaultHashAlgorithm = HashAlgorithm.sha1; -final _log = Logger('oath.models'); - enum HashAlgorithm { @JsonValue(0x01) sha1('SHA-1'), diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index 27ee2798..55f1d3d9 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -102,7 +102,8 @@ Widget oathBuildActions( if (qrScanner != null) { final otpauth = await qrScanner.scanQr(); if (otpauth == null) { - showMessage(context, l10n.l_qr_not_found); + await ref.read(withContextProvider)((context) async => + showMessage(context, l10n.l_qr_not_found)); } else { String s = 'otpauth-migration'; if (otpauth.contains(s)) { @@ -131,7 +132,8 @@ Widget oathBuildActions( } } } - Navigator.of(context).pop(); + await ref.read(withContextProvider)( + (context) async => Navigator.of(context).pop()); }), ]), ActionListSection(l10n.s_manage, children: [ From 643ae270ddac8615525c85ee53df1b3bb365261e Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 31 Jul 2023 11:22:26 +0200 Subject: [PATCH 061/158] Bump dependencies. --- .github/workflows/android.yml | 2 +- .github/workflows/check-strings.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/linux.yml | 6 +- .github/workflows/macos.yml | 2 +- .github/workflows/windows.yml | 2 +- helper/poetry.lock | 268 +++++++++++++------------- macos/Podfile.lock | 6 +- pubspec.lock | 150 +++++++------- 9 files changed, 223 insertions(+), 217 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index f861a4b1..4b70246a 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -17,7 +17,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.10.5' + flutter-version: '3.10.6' - run: | flutter config flutter --version diff --git a/.github/workflows/check-strings.yml b/.github/workflows/check-strings.yml index 1b9045e7..fe9b1c4a 100644 --- a/.github/workflows/check-strings.yml +++ b/.github/workflows/check-strings.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest env: - FLUTTER: '3.10.5' + FLUTTER: '3.10.6' steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5bc24f4b..8185a102 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -43,7 +43,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.10.5' + flutter-version: '3.10.6' - run: | flutter config flutter --version diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 579da0c9..1329e448 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest env: PYVER: '3.11.4' - FLUTTER: '3.10.5' + FLUTTER: '3.10.6' container: image: ubuntu:20.04 env: @@ -61,7 +61,9 @@ jobs: apt-get install -qq swig libpcsclite-dev build-essential cmake python -m ensurepip --user python -m pip install -U pip pipx - pipx ensurepath + # pipx ensurepath + echo "export PATH=$PATH:$HOME/.local/bin" >> ~/.bashrc + . ~/.bashrc # Needed to ensure poetry on PATH pipx install poetry - name: Build the Helper diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index f3928abd..cf559c60 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -49,7 +49,7 @@ jobs: with: channel: 'stable' architecture: 'x64' - flutter-version: '3.10.5' + flutter-version: '3.10.6' - run: flutter config --enable-macos-desktop - run: flutter --version diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 285f90ee..820cac50 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -45,7 +45,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.10.5' + flutter-version: '3.10.6' - run: flutter config --enable-windows-desktop - run: flutter --version diff --git a/helper/poetry.lock b/helper/poetry.lock index ba9687af..0c51e742 100755 --- a/helper/poetry.lock +++ b/helper/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. [[package]] name = "altgraph" @@ -91,14 +91,14 @@ pycparser = "*" [[package]] name = "click" -version = "8.1.3" +version = "8.1.6" description = "Composable command line interface toolkit" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, + {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, + {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, ] [package.dependencies] @@ -118,31 +118,35 @@ files = [ [[package]] name = "cryptography" -version = "41.0.1" +version = "41.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699"}, - {file = "cryptography-41.0.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a"}, - {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca"}, - {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43"}, - {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b"}, - {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3"}, - {file = "cryptography-41.0.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db"}, - {file = "cryptography-41.0.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31"}, - {file = "cryptography-41.0.1-cp37-abi3-win32.whl", hash = "sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5"}, - {file = "cryptography-41.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c"}, - {file = "cryptography-41.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb"}, - {file = "cryptography-41.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3"}, - {file = "cryptography-41.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039"}, - {file = "cryptography-41.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc"}, - {file = "cryptography-41.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485"}, - {file = "cryptography-41.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c"}, - {file = "cryptography-41.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a"}, - {file = "cryptography-41.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5"}, - {file = "cryptography-41.0.1.tar.gz", hash = "sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006"}, + {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"}, + {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"}, + {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"}, + {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"}, + {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"}, + {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"}, + {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"}, ] [package.dependencies] @@ -160,14 +164,14 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "exceptiongroup" -version = "1.1.1" +version = "1.1.2" description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, - {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, + {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, + {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, ] [package.extras] @@ -175,32 +179,32 @@ test = ["pytest (>=6)"] [[package]] name = "fido2" -version = "1.1.1" +version = "1.1.2" description = "FIDO2/WebAuthn library for implementing clients and servers." category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "fido2-1.1.1-py3-none-any.whl", hash = "sha256:54017b69522b1581e4222443a0b3fff5eb2626f8e773a4a7b955f3e55fb3b4fc"}, - {file = "fido2-1.1.1.tar.gz", hash = "sha256:5dc495ca8c59c1c337383b4b8c314d46b92d5c6fc650e71984c6d7f954079fc3"}, + {file = "fido2-1.1.2-py3-none-any.whl", hash = "sha256:a3b7d7d233dec3a4fa0d6178fc34d1cce17b820005a824f6ab96917a1e3be8d8"}, + {file = "fido2-1.1.2.tar.gz", hash = "sha256:6110d913106f76199201b32d262b2857562cc46ba1d0b9c51fbce30dc936c573"}, ] [package.dependencies] -cryptography = ">=2.6,<35 || >35,<43" +cryptography = ">=2.6,<35 || >35,<44" [package.extras] pcsc = ["pyscard (>=1.9,<3)"] [[package]] name = "importlib-metadata" -version = "6.7.0" +version = "6.8.0" description = "Read metadata from Python packages" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, - {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, + {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, + {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, ] [package.dependencies] @@ -209,26 +213,26 @@ zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] [[package]] name = "importlib-resources" -version = "5.12.0" +version = "6.0.0" description = "Read resources from Python packages" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, - {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, + {file = "importlib_resources-6.0.0-py3-none-any.whl", hash = "sha256:d952faee11004c045f785bb5636e8f885bed30dc3c940d5d42798a2a4541c185"}, + {file = "importlib_resources-6.0.0.tar.gz", hash = "sha256:4cf94875a8368bd89531a756df9a9ebe1f150e0f885030b461237bc7f2d905f2"}, ] [package.dependencies] zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [[package]] name = "iniconfig" @@ -244,22 +248,22 @@ files = [ [[package]] name = "jaraco-classes" -version = "3.2.3" +version = "3.3.0" description = "Utility functions for Python class constructs" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"}, - {file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"}, + {file = "jaraco.classes-3.3.0-py3-none-any.whl", hash = "sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb"}, + {file = "jaraco.classes-3.3.0.tar.gz", hash = "sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621"}, ] [package.dependencies] more-itertools = "*" [package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [[package]] name = "jeepney" @@ -319,14 +323,14 @@ altgraph = ">=0.17" [[package]] name = "more-itertools" -version = "9.1.0" +version = "10.0.0" description = "More routines for operating on iterables, beyond itertools" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "more-itertools-9.1.0.tar.gz", hash = "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d"}, - {file = "more_itertools-9.1.0-py3-none-any.whl", hash = "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3"}, + {file = "more-itertools-10.0.0.tar.gz", hash = "sha256:cd65437d7c4b615ab81c0640c0480bc29a550ea032891977681efd28344d51e1"}, + {file = "more_itertools-10.0.0-py3-none-any.whl", hash = "sha256:928d514ffd22b5b0a8fce326d57f423a55d2ff783b093bab217eda71e732330f"}, ] [[package]] @@ -343,40 +347,40 @@ files = [ [[package]] name = "numpy" -version = "1.24.3" +version = "1.24.4" description = "Fundamental package for array computing in Python" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "numpy-1.24.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c1104d3c036fb81ab923f507536daedc718d0ad5a8707c6061cdfd6d184e570"}, - {file = "numpy-1.24.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:202de8f38fc4a45a3eea4b63e2f376e5f2dc64ef0fa692838e31a808520efaf7"}, - {file = "numpy-1.24.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8535303847b89aa6b0f00aa1dc62867b5a32923e4d1681a35b5eef2d9591a463"}, - {file = "numpy-1.24.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d926b52ba1367f9acb76b0df6ed21f0b16a1ad87c6720a1121674e5cf63e2b6"}, - {file = "numpy-1.24.3-cp310-cp310-win32.whl", hash = "sha256:f21c442fdd2805e91799fbe044a7b999b8571bb0ab0f7850d0cb9641a687092b"}, - {file = "numpy-1.24.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f23af8c16022663a652d3b25dcdc272ac3f83c3af4c02eb8b824e6b3ab9d7"}, - {file = "numpy-1.24.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9a7721ec204d3a237225db3e194c25268faf92e19338a35f3a224469cb6039a3"}, - {file = "numpy-1.24.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d6cc757de514c00b24ae8cf5c876af2a7c3df189028d68c0cb4eaa9cd5afc2bf"}, - {file = "numpy-1.24.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76e3f4e85fc5d4fd311f6e9b794d0c00e7002ec122be271f2019d63376f1d385"}, - {file = "numpy-1.24.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1d3c026f57ceaad42f8231305d4653d5f05dc6332a730ae5c0bea3513de0950"}, - {file = "numpy-1.24.3-cp311-cp311-win32.whl", hash = "sha256:c91c4afd8abc3908e00a44b2672718905b8611503f7ff87390cc0ac3423fb096"}, - {file = "numpy-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:5342cf6aad47943286afa6f1609cad9b4266a05e7f2ec408e2cf7aea7ff69d80"}, - {file = "numpy-1.24.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7776ea65423ca6a15255ba1872d82d207bd1e09f6d0894ee4a64678dd2204078"}, - {file = "numpy-1.24.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ae8d0be48d1b6ed82588934aaaa179875e7dc4f3d84da18d7eae6eb3f06c242c"}, - {file = "numpy-1.24.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecde0f8adef7dfdec993fd54b0f78183051b6580f606111a6d789cd14c61ea0c"}, - {file = "numpy-1.24.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4749e053a29364d3452c034827102ee100986903263e89884922ef01a0a6fd2f"}, - {file = "numpy-1.24.3-cp38-cp38-win32.whl", hash = "sha256:d933fabd8f6a319e8530d0de4fcc2e6a61917e0b0c271fded460032db42a0fe4"}, - {file = "numpy-1.24.3-cp38-cp38-win_amd64.whl", hash = "sha256:56e48aec79ae238f6e4395886b5eaed058abb7231fb3361ddd7bfdf4eed54289"}, - {file = "numpy-1.24.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4719d5aefb5189f50887773699eaf94e7d1e02bf36c1a9d353d9f46703758ca4"}, - {file = "numpy-1.24.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ec87a7084caa559c36e0a2309e4ecb1baa03b687201d0a847c8b0ed476a7187"}, - {file = "numpy-1.24.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea8282b9bcfe2b5e7d491d0bf7f3e2da29700cec05b49e64d6246923329f2b02"}, - {file = "numpy-1.24.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210461d87fb02a84ef243cac5e814aad2b7f4be953b32cb53327bb49fd77fbb4"}, - {file = "numpy-1.24.3-cp39-cp39-win32.whl", hash = "sha256:784c6da1a07818491b0ffd63c6bbe5a33deaa0e25a20e1b3ea20cf0e43f8046c"}, - {file = "numpy-1.24.3-cp39-cp39-win_amd64.whl", hash = "sha256:d5036197ecae68d7f491fcdb4df90082b0d4960ca6599ba2659957aafced7c17"}, - {file = "numpy-1.24.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:352ee00c7f8387b44d19f4cada524586f07379c0d49270f87233983bc5087ca0"}, - {file = "numpy-1.24.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7d6acc2e7524c9955e5c903160aa4ea083736fde7e91276b0e5d98e6332812"}, - {file = "numpy-1.24.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:35400e6a8d102fd07c71ed7dcadd9eb62ee9a6e84ec159bd48c28235bbb0f8e4"}, - {file = "numpy-1.24.3.tar.gz", hash = "sha256:ab344f1bf21f140adab8e47fdbc7c35a477dc01408791f8ba00d018dd0bc5155"}, + {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, + {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, + {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, + {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, + {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, + {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, + {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, + {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, + {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, + {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, + {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, ] [[package]] @@ -485,14 +489,14 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa [[package]] name = "pluggy" -version = "1.0.0" +version = "1.2.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, ] [package.extras] @@ -513,24 +517,24 @@ files = [ [[package]] name = "pyinstaller" -version = "5.12.0" +version = "5.13.0" description = "PyInstaller bundles a Python application and all its dependencies into a single package." category = "dev" optional = false -python-versions = "<3.12,>=3.7" +python-versions = "<3.13,>=3.7" files = [ - {file = "pyinstaller-5.12.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:edcb6eb6618f3b763c11487db1d3516111d54bd5598b9470e295c1f628a95496"}, - {file = "pyinstaller-5.12.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:303952c2a8ece894b655c2a0783a0bdc844282f47790707446bde3eaf355f0da"}, - {file = "pyinstaller-5.12.0-py3-none-manylinux2014_i686.whl", hash = "sha256:7eed9996c12aeee7530cbc7c57350939f46391ecf714ac176579190dbd3ec7bf"}, - {file = "pyinstaller-5.12.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:96ad645347671c9fce190506c09523c02f01a503fe3ea65f79bb0cfe22a8c83e"}, - {file = "pyinstaller-5.12.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:c3ceb6c3a34b9407ba16fb68a32f83d5fd94f21d43d9fe38d8f752feb75ca5bb"}, - {file = "pyinstaller-5.12.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:92eeacd052092a0a4368f50ddecbeb6e020b5a70cdf113243fbd6bd8ee25524e"}, - {file = "pyinstaller-5.12.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:3605ac72311318455907a88efb4a4b334b844659673a2a371bbaac2d8b52843a"}, - {file = "pyinstaller-5.12.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d14c1c2b753af5efed96584f075a6740ea634ca55789113d325dc8c32aef50fe"}, - {file = "pyinstaller-5.12.0-py3-none-win32.whl", hash = "sha256:b64d8a3056e6c7e4ed4d1f95e793ef401bf5b166ef00ad544b5812be0ac63b4b"}, - {file = "pyinstaller-5.12.0-py3-none-win_amd64.whl", hash = "sha256:62d75bb70cdbeea1a0d55067d7201efa2f7d7c19e56c241291c03d551b531684"}, - {file = "pyinstaller-5.12.0-py3-none-win_arm64.whl", hash = "sha256:2f70e2d9b032e5f24a336f41affcb4624e66a84cd863ba58f6a92bd6040653bb"}, - {file = "pyinstaller-5.12.0.tar.gz", hash = "sha256:a1c2667120730604c3ad1e0739a45bb72ca4a502a91e2f5c5b220fbfbb05f0d4"}, + {file = "pyinstaller-5.13.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:7fdd319828de679f9c5e381eff998ee9b4164bf4457e7fca56946701cf002c3f"}, + {file = "pyinstaller-5.13.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0df43697c4914285ecd333be968d2cd042ab9b2670124879ee87931d2344eaf5"}, + {file = "pyinstaller-5.13.0-py3-none-manylinux2014_i686.whl", hash = "sha256:28d9742c37e9fb518444b12f8c8ab3cb4ba212d752693c34475c08009aa21ccf"}, + {file = "pyinstaller-5.13.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e5fb17de6c325d3b2b4ceaeb55130ad7100a79096490e4c5b890224406fa42f4"}, + {file = "pyinstaller-5.13.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:78975043edeb628e23a73fb3ef0a273cda50e765f1716f75212ea3e91b09dede"}, + {file = "pyinstaller-5.13.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:cd7d5c06f2847195a23d72ede17c60857d6f495d6f0727dc6c9bc1235f2eb79c"}, + {file = "pyinstaller-5.13.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:24009eba63cfdbcde6d2634e9c87f545eb67249ddf3b514e0cd3b2cdaa595828"}, + {file = "pyinstaller-5.13.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:1fde4381155f21d6354dc450dcaa338cd8a40aaacf6bd22b987b0f3e1f96f3ee"}, + {file = "pyinstaller-5.13.0-py3-none-win32.whl", hash = "sha256:2d03419904d1c25c8968b0ad21da0e0f33d8d65716e29481b5bd83f7f342b0c5"}, + {file = "pyinstaller-5.13.0-py3-none-win_amd64.whl", hash = "sha256:9fc27c5a853b14a90d39c252707673c7a0efec921cd817169aff3af0fca8c127"}, + {file = "pyinstaller-5.13.0-py3-none-win_arm64.whl", hash = "sha256:3a331951f9744bc2379ea5d65d36f3c828eaefe2785f15039592cdc08560b262"}, + {file = "pyinstaller-5.13.0.tar.gz", hash = "sha256:5e446df41255e815017d96318e39f65a3eb807e74a796c7e7ff7f13b6366a2e9"}, ] [package.dependencies] @@ -538,7 +542,7 @@ altgraph = "*" macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} pyinstaller-hooks-contrib = ">=2021.4" -pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} setuptools = ">=42.0.0" [package.extras] @@ -547,14 +551,14 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2023.3" +version = "2023.6" description = "Community maintained hooks for PyInstaller" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pyinstaller-hooks-contrib-2023.3.tar.gz", hash = "sha256:bb39e1038e3e0972420455e0b39cd9dce73f3d80acaf4bf2b3615fea766ff370"}, - {file = "pyinstaller_hooks_contrib-2023.3-py2.py3-none-any.whl", hash = "sha256:062ad7a1746e1cfc24d3a8c4be4e606fced3b123bda7d419f14fcf7507804b07"}, + {file = "pyinstaller-hooks-contrib-2023.6.tar.gz", hash = "sha256:596a72009d8692b043e0acbf5e1b476d93149900142ba01845dded91a0770cb5"}, + {file = "pyinstaller_hooks_contrib-2023.6-py2.py3-none-any.whl", hash = "sha256:aa6d7d038814df6aa7bec7bdbebc7cb4c693d3398df858f6062957f0797d397b"}, ] [[package]] @@ -580,14 +584,14 @@ pyro = ["Pyro"] [[package]] name = "pytest" -version = "7.3.2" +version = "7.4.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.3.2-py3-none-any.whl", hash = "sha256:cdcbd012c9312258922f8cd3f1b62a6580fdced17db6014896053d47cddf9295"}, - {file = "pytest-7.3.2.tar.gz", hash = "sha256:ee990a3cc55ba808b80795a79944756f315c67c12b56abd3ac993a7b8c17030b"}, + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, ] [package.dependencies] @@ -627,14 +631,14 @@ files = [ [[package]] name = "pywin32-ctypes" -version = "0.2.1" +version = "0.2.2" description = "A (partial) reimplementation of pywin32 using ctypes/cffi" category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "pywin32-ctypes-0.2.1.tar.gz", hash = "sha256:934a2def1e5cbc472b2b6bf80680c0f03cd87df65dfd58bfd1846969de095b03"}, - {file = "pywin32_ctypes-0.2.1-py3-none-any.whl", hash = "sha256:b9a53ef754c894a525469933ab2a447c74ec1ea6b9d2ef446f40ec50d3dcec9f"}, + {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, + {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, ] [[package]] @@ -704,44 +708,44 @@ pywin32 = {version = ">=223", markers = "sys_platform == \"win32\""} [[package]] name = "zipp" -version = "3.15.0" +version = "3.16.2" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, + {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, + {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [[package]] name = "zxing-cpp" -version = "2.0.0" +version = "2.1.0" description = "Python bindings for the zxing-cpp barcode library" category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "zxing-cpp-2.0.0.tar.gz", hash = "sha256:1b67b221aae15aad9b5609d99c38d57875bc0a4fef864142d7ca37e9ee7880b0"}, - {file = "zxing_cpp-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:54282d0e5c573754049113a0cdbf14cc1c6b986432a367d8a788112afa92a1d5"}, - {file = "zxing_cpp-2.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76caafb8fc1e12c2e5ec33ce4f340a0e15e9a2aabfbfeaec170e8a2b405b8a77"}, - {file = "zxing_cpp-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95dd06dc559f53c1ca0eb59dbaebd802ebc839937baaf2f8d2b3def3e814c07f"}, - {file = "zxing_cpp-2.0.0-cp310-cp310-win32.whl", hash = "sha256:ea54fd242f93eea7bf039a68287e5e57fdf77d78e3bd5b4cbb2d289bb3380d63"}, - {file = "zxing_cpp-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:8da9c912cca5829eedb2800ce3eaa1b1e52742f536aa9e798be69bf09639f399"}, - {file = "zxing_cpp-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f70eefa5dc1fd9238087c024ef22f3d99ba79cb932a2c5bc5b0f1e152037722e"}, - {file = "zxing_cpp-2.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97919f07c62edf1c8e0722fd64893057ce636b7067cf47bd593e98cc7e404d74"}, - {file = "zxing_cpp-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd89065f620d6b78281308c6abfb760d95760a1c9b88eb7ac612b52b331bd41"}, - {file = "zxing_cpp-2.0.0-cp311-cp311-win32.whl", hash = "sha256:631a0c783ad233c85295e0cf4cd7740f1fe2853124c61b1ef6bcf7eb5d2fa5e6"}, - {file = "zxing_cpp-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f0c2c03f5df470ef71a7590be5042161e7590da767d4260a6d0d61a3fa80b88"}, - {file = "zxing_cpp-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5ce391f21763f00d5be3431e16d075e263e4b9205c2cf55d708625cb234b1f15"}, - {file = "zxing_cpp-2.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0eefdfad91e15e3f5b7ed16d83806a36f96ca482f4b042baa6297784a58b0b3"}, - {file = "zxing_cpp-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d665c45029346c70ae3df5dbc36f6335ffe4f275e98dc43772fa32a65844196"}, - {file = "zxing_cpp-2.0.0-cp39-cp39-win32.whl", hash = "sha256:214a6a0e49b92fda8d2761c74f5bfd24a677b9bf1d0ef0e083412486af97faa9"}, - {file = "zxing_cpp-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:a788551ddf3a6ba1152ff9a0b81d57018a3cc586544087c39d881428745faf1f"}, + {file = "zxing-cpp-2.1.0.tar.gz", hash = "sha256:7a8a468b420bf391707431d5a0dd881cb41033ae15f87820d93d5707c7bc55bc"}, + {file = "zxing_cpp-2.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:26d27f61d627c06cc3e91b1ce816bd780c9227fd10b7ca961264f67bfb3bdf66"}, + {file = "zxing_cpp-2.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d9655c7d682ce252fe5c25f22c6fafe4c5ac493830fa8a2c062c85d061ce3b4"}, + {file = "zxing_cpp-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313bac052bd38bd2cedaa2610d880b3d62254dd6d8be01795559b73872c54ed0"}, + {file = "zxing_cpp-2.1.0-cp310-cp310-win32.whl", hash = "sha256:0a178683b66422ac01ae35f749d58c50b271f9ab18def1c286f5fc61bcf81fa7"}, + {file = "zxing_cpp-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:650d8f6731f11c04f4662a48f1efa9dc26c97bbdfa4f9b14b4683f43b7ccde4d"}, + {file = "zxing_cpp-2.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4163d72975191d40c879bc130d5e8aa1eef5d5e6bfe820d94b5c9a2cb10d664e"}, + {file = "zxing_cpp-2.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:843f72a1f2a8c397b4d92f757488b03d8597031e907442382d5662fd96b0fd21"}, + {file = "zxing_cpp-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66d01d40bacc7e5b40e9fa474dab64f2e75a091c6e7c9d4a6b539b5a724127e3"}, + {file = "zxing_cpp-2.1.0-cp311-cp311-win32.whl", hash = "sha256:8397ce7e1a7a92cd8f0045a4c64e4fcd97f4aaa51441d27bcb76eeda0a1917bc"}, + {file = "zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a54cd56c0898cb63a08517b7d630484690a9bad4da1e443aebe64b7077444d90"}, + {file = "zxing_cpp-2.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab8fff5791e1d858390e45325500f6a17d5d3b6ac0237ae84ceda6f5b7a3685a"}, + {file = "zxing_cpp-2.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba91ba2af0cc75c9e53bf95963f409c6fa26aa7df38469e2cdcb5b38a6c7c1c7"}, + {file = "zxing_cpp-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7ba898e4f5ee9cd426d4271ff8b26911e3346b1cb4262f06fdc917e42b7c123"}, + {file = "zxing_cpp-2.1.0-cp39-cp39-win32.whl", hash = "sha256:da081b763032b05326ddc53d3ad28a8b7603d662ccce2ff29fd204d587d3cac9"}, + {file = "zxing_cpp-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7245e551fc30e9708c0fd0f4d0d15f29c0b85075d20c18ddc53b87956a469544"}, ] [package.dependencies] diff --git a/macos/Podfile.lock b/macos/Podfile.lock index e03419e4..b377e817 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -54,11 +54,11 @@ SPEC CHECKSUMS: desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 - url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451 + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 diff --git a/pubspec.lock b/pubspec.lock index 0291513d..4af21464 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: args - sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" async: dependency: "direct main" description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: build - sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" build_config: dependency: transitive description: @@ -77,18 +77,18 @@ packages: dependency: transitive description: name: build_resolvers - sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "5e1929ad37d48bd382b124266cb8e521de5548d406a45a5ae6656c13dab73e37" + sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.6" build_runner_core: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: built_value - sha256: "2f17434bd5d52a26762043d6b43bb53b3acd029b4d9071a329f46d67ef297e6d" + sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" url: "https://pub.dev" source: hosted - version: "8.5.0" + version: "8.6.1" characters: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.5.0" collection: dependency: "direct main" description: @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: dart_style - sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" desktop_drop: dependency: "direct main" description: @@ -221,10 +221,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: b1729fc96627dd44012d0a901558177418818d6bd428df59dcfeb594e5f66432 + sha256: "21145c9c268d54b1f771d8380c195d2d6f655e0567dc1ca2f9c134c02c819e0a" url: "https://pub.dev" source: hosted - version: "5.3.2" + version: "5.3.3" fixnum: dependency: transitive description: @@ -247,10 +247,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" flutter_localizations: dependency: "direct main" description: flutter @@ -286,18 +286,18 @@ packages: dependency: "direct dev" description: name: freezed - sha256: a9520490532087cf38bf3f7de478ab6ebeb5f68bb1eb2641546d92719b224445 + sha256: "2df89855fe181baae3b6d714dc3c4317acf4fccd495a6f36e5e00f24144c6c3b" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.4.1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.1" frontend_server_client: dependency: transitive description: @@ -323,10 +323,10 @@ packages: dependency: transitive description: name: graphs - sha256: "772db3d53d23361d4ffcf5a9bb091cf3ee9b22f2be52cd107cd7a2683a89ba0e" + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" http_multi_server: dependency: transitive description: @@ -392,10 +392,10 @@ packages: dependency: transitive description: name: lints - sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" local_notifier: dependency: "direct main" description: @@ -496,18 +496,18 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" + sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.4" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" + sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 url: "https://pub.dev" source: hosted - version: "2.1.10" + version: "2.1.11" path_provider_platform_interface: dependency: transitive description: @@ -544,10 +544,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pointycastle: dependency: transitive description: @@ -615,58 +615,58 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "396f85b8afc6865182610c0a2fc470853d56499f75f7499e2a73a9f0539d23d0" + sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.2.0" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749" + sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb + sha256: f39696b83e844923b642ce9dd4bd31736c17e697f6731a5adf445b1274cf3cd4 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" + sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d + sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" + sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" + sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shelf: dependency: transitive description: @@ -700,18 +700,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" + sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.4" source_span: dependency: transitive description: @@ -812,18 +812,18 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" url: "https://pub.dev" source: hosted - version: "6.1.11" + version: "6.1.12" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "1a5848f598acc5b7d8f7c18b8cb834ab667e59a13edc3c93e9d09cf38cc6bc87" + sha256: "78cb6dea3e93148615109e58e42c35d1ffbf5ef66c44add673d0ab75f12ff3af" url: "https://pub.dev" source: hosted - version: "6.0.34" + version: "6.0.37" url_launcher_ios: dependency: transitive description: @@ -844,34 +844,34 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" + sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" + sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.0.18" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" + sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" uuid: dependency: transitive description: @@ -884,26 +884,26 @@ packages: dependency: "direct main" description: name: vector_graphics - sha256: b96f10cbdfcbd03a65758633a43e7d04574438f059b1043104b5d61b23d38a4f + sha256: "670f6e07aca990b4a2bcdc08a784193c4ccdd1932620244c3a86bb72a0eac67f" url: "https://pub.dev" source: hosted - version: "1.1.6" + version: "1.1.7" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "57a8e6e24662a3bdfe3b3d61257db91768700c0b8f844e235877b56480f31c69" + sha256: "7451721781d967db9933b63f5733b1c4533022c0ba373a01bdd79d1a5457f69f" url: "https://pub.dev" source: hosted - version: "1.1.6" + version: "1.1.7" vector_graphics_compiler: dependency: "direct main" description: name: vector_graphics_compiler - sha256: "7430f5d834d0db4560d7b19863362cd892f1e52b43838553a3c5cdfc9ab28e5b" + sha256: "80a13c613c8bde758b1464a1755a7b3a8f2b6cec61fbf0f5a53c94c30f03ba2e" url: "https://pub.dev" source: hosted - version: "1.1.6" + version: "1.1.7" vector_math: dependency: transitive description: @@ -948,26 +948,26 @@ packages: dependency: transitive description: name: win32 - sha256: "1414f27dd781737e51afa9711f2ac2ace6ab4498ee98e20863fa5505aa00c58c" + sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 url: "https://pub.dev" source: hosted - version: "5.0.4" + version: "5.0.6" window_manager: dependency: "direct main" description: name: window_manager - sha256: "95096fede562cbb65f30d38b62d819a458f59ba9fe4a317f6cee669710f6676b" + sha256: "9eef00e393e7f9308309ce9a8b2398c9ee3ca78b50c96e8b4f9873945693ac88" url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.3.5" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" xml: dependency: transitive description: @@ -986,4 +986,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.0.0 <4.0.0" - flutter: ">=3.5.0-0" + flutter: ">=3.10.0" From 58342093f2937a5f08e3d914c30e90ce3a66da4f Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 1 Aug 2023 11:56:57 +0200 Subject: [PATCH 062/158] support otpauth-migration on Android --- lib/android/qr_scanner/qr_scanner_view.dart | 26 +++++++++++++-------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/android/qr_scanner/qr_scanner_view.dart b/lib/android/qr_scanner/qr_scanner_view.dart index 06ace3dd..79d9717b 100755 --- a/lib/android/qr_scanner/qr_scanner_view.dart +++ b/lib/android/qr_scanner/qr_scanner_view.dart @@ -38,15 +38,11 @@ GlobalKey _zxingViewKey = GlobalKey(); class _QrScannerViewState extends State { String? _scannedString; - // will be used later - // ignore: unused_field - CredentialData? _credentialData; ScanStatus _status = ScanStatus.scanning; bool _previewInitialized = false; bool _permissionsGranted = false; void setError() { - _credentialData = null; _scannedString = null; _status = ScanStatus.error; @@ -59,7 +55,6 @@ class _QrScannerViewState extends State { void resetError() { setState(() { - _credentialData = null; _scannedString = null; _status = ScanStatus.scanning; @@ -67,17 +62,16 @@ class _QrScannerViewState extends State { }); } - void handleResult(String barCode) { + void handleResult(String qrCodeData) { if (_status != ScanStatus.scanning) { // on success and error ignore reported codes return; } setState(() { - if (barCode.isNotEmpty) { + if (qrCodeData.isNotEmpty) { try { - var parsedCredential = CredentialData.fromUri(Uri.parse(barCode)); - _credentialData = parsedCredential; - _scannedString = barCode; + _validateQrCodeUri(Uri.parse(qrCodeData)); // throws ArgumentError if validation fails + _scannedString = qrCodeData; _status = ScanStatus.success; final navigator = Navigator.of(context); @@ -98,6 +92,18 @@ class _QrScannerViewState extends State { }); } + void _validateQrCodeUri(Uri qrCodeUri) { + try { + CredentialData.fromUri(qrCodeUri); + } on ArgumentError catch (_) { + try { + CredentialData.fromMigration(qrCodeUri); + } on ArgumentError catch (_) { + throw ArgumentError(); + } + } + } + @override void initState() { super.initState(); From 9d1b37ee32aecc4a978b196bd53cbf7c4185759b Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 2 Aug 2023 13:06:37 +0200 Subject: [PATCH 063/158] improve scanning migration QR codes --- .../qrscanner_zxing/QRScannerView.kt | 41 ++++++++++--------- lib/android/qr_scanner/qr_scanner_view.dart | 2 +- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt b/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt index 5da59aa2..627519f9 100644 --- a/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt +++ b/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt @@ -26,6 +26,7 @@ import android.os.Handler import android.os.Looper import android.provider.Settings import android.util.Log +import android.util.Size import android.view.View import androidx.camera.core.* import androidx.camera.lifecycle.ProcessCameraProvider @@ -81,8 +82,6 @@ internal class QRScannerView( private val stateChangeObserver = StateChangeObserver(context) private val uiThreadHandler = Handler(Looper.getMainLooper()) - private var marginPct: Double? = null - companion object { const val TAG = "QRScannerView" @@ -128,10 +127,20 @@ internal class QRScannerView( private var imageAnalysis: ImageAnalysis? = null private var preview: Preview? = null - private var barcodeAnalyzer : BarcodeAnalyzer = BarcodeAnalyzer(marginPct) { analyzeResult -> - if (analyzeResult.isSuccess) { - analyzeResult.getOrNull()?.let { result -> - reportCodeFound(result) + private val barcodeAnalyzer = with(creationParams) { + var marginPct : Double? = null + if (this?.get("margin") is Number) { + val marginValue = this["margin"] as Number + if (marginValue.toDouble() > 0.0 && marginValue.toDouble() < 45) { + marginPct = marginValue.toDouble() + } + } + + BarcodeAnalyzer(marginPct) { analyzeResult -> + if (analyzeResult.isSuccess) { + analyzeResult.getOrNull()?.let { result -> + reportCodeFound(result) + } } } } @@ -155,19 +164,11 @@ internal class QRScannerView( private val methodChannel: MethodChannel = MethodChannel(binaryMessenger, CHANNEL_NAME) private var permissionsGranted = false + private val screenSize = with(context.resources.displayMetrics) { + Size(widthPixels, heightPixels) + } + init { - - // read margin parameter - // only use it if it has reasonable value - if (creationParams?.get("margin") is Number) { - val marginValue = creationParams["margin"] as Number - if (marginValue.toDouble() > 0.0 && marginValue.toDouble() < 45) { - marginPct = marginValue.toDouble() - } - } - - Log.v(TAG, "marginPct: $marginPct") - if (context is Activity) { permissionsGranted = allPermissionsGranted(context) @@ -266,7 +267,7 @@ internal class QRScannerView( } preview = Preview.Builder() - .setTargetAspectRatio(QR_SCANNER_ASPECT_RATIO) + .setTargetResolution(screenSize) .build() .also { it.setSurfaceProvider(previewView.surfaceProvider) @@ -369,7 +370,7 @@ internal class QRScannerView( val fullSize = BinaryBitmap(HybridBinarizer(luminanceSource)) - val bitmapToProcess = if (marginPct != null) { + val bitmapToProcess = if (marginPct != null && fullSize.isCropSupported) { val shorterDim = min(imageProxy.width, imageProxy.height) val cropMargin = marginPct * 0.01 * shorterDim val cropWH = shorterDim - 2.0 * cropMargin diff --git a/lib/android/qr_scanner/qr_scanner_view.dart b/lib/android/qr_scanner/qr_scanner_view.dart index 79d9717b..68b2018f 100755 --- a/lib/android/qr_scanner/qr_scanner_view.dart +++ b/lib/android/qr_scanner/qr_scanner_view.dart @@ -149,7 +149,7 @@ class _QrScannerViewState extends State { visible: _permissionsGranted, child: QRScannerZxingView( key: _zxingViewKey, - marginPct: 50, + marginPct: 10, onDetect: (scannedData) => handleResult(scannedData), onViewInitialized: (bool permissionsGranted) { Future.delayed(const Duration(milliseconds: 50), () { From a7558edf41cd16ab63c2aa93d3d9e2f4a55003c5 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 2 Aug 2023 15:48:41 +0200 Subject: [PATCH 064/158] force higher resolution for analysis --- .../flutter_plugins/qrscanner_zxing/QRScannerView.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt b/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt index 627519f9..184a6b5a 100644 --- a/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt +++ b/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt @@ -92,9 +92,6 @@ internal class QRScannerView( Manifest.permission.CAMERA, ).toTypedArray() - // view related - private const val QR_SCANNER_ASPECT_RATIO = AspectRatio.RATIO_4_3 - // communication channel private const val CHANNEL_NAME = "com.yubico.authenticator.flutter_plugins.qr_scanner_channel" @@ -260,7 +257,7 @@ internal class QRScannerView( imageAnalysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .setTargetAspectRatio(QR_SCANNER_ASPECT_RATIO) + .setTargetResolution(Size(768,1024)) .build() .also { it.setAnalyzer(cameraExecutor, barcodeAnalyzer) @@ -396,6 +393,10 @@ internal class QRScannerView( } val result: com.google.zxing.Result = multiFormatReader.decode(bitmapToProcess) + if (analysisPaused) { + return + } + analysisPaused = true // pause Log.v(TAG, "Analysis result: ${result.text}") listener.invoke(Result.success(result.text)) From 8936a7f04edb674c05c967ef1012188718f3a11e Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 2 Aug 2023 16:07:40 +0200 Subject: [PATCH 065/158] Revert "use traffic level for icon pack logging" This reverts commit a2d503448a6b02674e1c514ccd3344bd2514e816. --- lib/oath/icon_provider/icon_cache.dart | 8 ++++---- lib/oath/icon_provider/icon_file_loader.dart | 4 ++-- lib/oath/icon_provider/icon_pack_manager.dart | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/oath/icon_provider/icon_cache.dart b/lib/oath/icon_provider/icon_cache.dart index d88e7ec8..1c27a7f2 100644 --- a/lib/oath/icon_provider/icon_cache.dart +++ b/lib/oath/icon_provider/icon_cache.dart @@ -30,15 +30,15 @@ class IconCacheFs { final file = await _getFile(fileName); final exists = await file.exists(); if (exists) { - _log.traffic('File $fileName exists in cache'); + _log.debug('File $fileName exists in cache'); } else { - _log.traffic('File $fileName does not exist in cache'); + _log.debug('File $fileName does not exist in cache'); } return exists ? (await file.readAsBytes()).buffer.asByteData() : null; } Future write(String fileName, Uint8List data) async { - _log.traffic('Writing $fileName to cache'); + _log.debug('Writing $fileName to cache'); final file = await _getFile(fileName); if (!await file.exists()) { await file.create(recursive: true, exclusive: false); @@ -52,7 +52,7 @@ class IconCacheFs { try { await cacheDirectory.delete(recursive: true); } catch (e) { - _log.traffic( + _log.error( 'Failed to delete cache directory ${cacheDirectory.path}', e); } } diff --git a/lib/oath/icon_provider/icon_file_loader.dart b/lib/oath/icon_provider/icon_file_loader.dart index ca927953..3196dc49 100644 --- a/lib/oath/icon_provider/icon_file_loader.dart +++ b/lib/oath/icon_provider/icon_file_loader.dart @@ -45,7 +45,7 @@ class IconFileLoader extends BytesLoader { // check if the requested file exists in memory cache var cachedData = memCache.read(cacheFileName); if (cachedData != null) { - _log.traffic('Returning $cacheFileName image data from memory cache'); + _log.debug('Returning $cacheFileName image data from memory cache'); return cachedData; } @@ -55,7 +55,7 @@ class IconFileLoader extends BytesLoader { cachedData = await fsCache.read(cacheFileName); if (cachedData != null) { memCache.write(cacheFileName, cachedData.buffer.asUint8List()); - _log.traffic('Returning $cacheFileName image data from fs cache'); + _log.debug('Returning $cacheFileName image data from fs cache'); return cachedData; } diff --git a/lib/oath/icon_provider/icon_pack_manager.dart b/lib/oath/icon_provider/icon_pack_manager.dart index d2e3d2a2..3f725819 100644 --- a/lib/oath/icon_provider/icon_pack_manager.dart +++ b/lib/oath/icon_provider/icon_pack_manager.dart @@ -48,10 +48,10 @@ class IconPackManager extends StateNotifier> { final packFile = File(join(packDirectory.path, getLocalIconFileName('pack.json'))); - _log.traffic('Looking for file: ${packFile.path}'); + _log.debug('Looking for file: ${packFile.path}'); if (!await packFile.exists()) { - _log.traffic('Failed to find icons pack ${packFile.path}'); + _log.debug('Failed to find icons pack ${packFile.path}'); state = AsyncValue.error( 'Failed to find icon pack ${packFile.path}', StackTrace.current); return; @@ -76,10 +76,10 @@ class IconPackManager extends StateNotifier> { directory: packDirectory, icons: icons)); - _log.traffic( + _log.debug( 'Parsed ${state.value?.name} with ${state.value?.icons.length} icons'); } catch (e) { - _log.traffic('Failed to parse icons pack ${packFile.path}'); + _log.debug('Failed to parse icons pack ${packFile.path}'); state = AsyncValue.error( 'Failed to parse icon pack ${packFile.path}', StackTrace.current); return; @@ -123,7 +123,7 @@ class IconPackManager extends StateNotifier> { final data = file.content as List; final extractedFile = File(join(unpackDirectory.path, getLocalIconFileName(filename))); - _log.traffic('Writing file: ${extractedFile.path} (size: ${file.size})'); + _log.debug('Writing file: ${extractedFile.path} (size: ${file.size})'); final createdFile = await extractedFile.create(recursive: true); await createdFile.writeAsBytes(data); } From a8db39ef7bf4829717166c791c51558144545aa9 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 2 Aug 2023 16:34:43 +0200 Subject: [PATCH 066/158] fix after merge compile error --- .../src/main/kotlin/com/yubico/authenticator/MainActivity.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt index d59f97dd..19f68fa7 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt @@ -328,12 +328,15 @@ class MainActivity : FlutterFragmentActivity() { * this receiver restarts the YubiKit NFC discovery when the QR Scanner camera is closed. */ class QRScannerCameraClosedBR : BroadcastReceiver() { + + private val logger = LoggerFactory.getLogger(QRScannerCameraClosedBR::class.java) + companion object { val intentFilter = IntentFilter("com.yubico.authenticator.QRScannerView.CameraClosed") } override fun onReceive(context: Context?, intent: Intent?) { - Log.d(TAG, "Restarting nfc discovery after camera was closed.") + logger.debug("Restarting nfc discovery after camera was closed.") (context as? MainActivity)?.startNfcDiscovery() } } From f23a38987c95f44c973f98c93d32589af6625006 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Thu, 3 Aug 2023 15:11:47 +0200 Subject: [PATCH 067/158] [android] add multiple accounts over nfc --- .../oath/OathActionDescription.kt | 3 +- .../yubico/authenticator/oath/OathManager.kt | 53 +++++++++++++++++ lib/android/oath/state.dart | 30 ++++++++++ lib/android/tap_request_dialog.dart | 5 +- lib/l10n/app_en.arb | 1 + lib/oath/views/migrate_account_page.dart | 58 +++++++++++++++++-- 6 files changed, 143 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt index 07be82ad..ac78d2c5 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt @@ -27,7 +27,8 @@ enum class OathActionDescription(private val value: Int) { RenameAccount(5), DeleteAccount(6), CalculateCode(7), - ActionFailure(8); + ActionFailure(8), + AddMultipleAccounts(9); val id: Int get() = value + dialogDescriptionOathIndex diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt index 2923582e..b85568f8 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt @@ -195,6 +195,7 @@ class OathManager( // OATH methods callable from Flutter: oathChannel.setHandler(coroutineScope) { method, args -> + @Suppress("UNCHECKED_CAST") when (method) { "reset" -> reset() "unlock" -> unlock( @@ -227,6 +228,11 @@ class OathManager( args["requireTouch"] as Boolean ) + "addAccountsToAny" -> addAccountsToAny( + args["uris"] as List, + args["requireTouch"] as List + ) + else -> throw NotImplementedError() } } @@ -383,6 +389,53 @@ class OathManager( } } + private suspend fun addAccountsToAny( + uris: List, + requireTouch: List, + ): String { + logger.trace("Adding following accounts: {}", uris) + + addToAny = true + return useOathSessionNfc(OathActionDescription.AddMultipleAccounts) { session -> + + var successCount = 0 + + // try { + for (index in uris.indices) { + + val credentialData: CredentialData = + CredentialData.parseUri(URI.create(uris[index])) + + if (session.credentials.any { it.id.contentEquals(credentialData.id) }) { + logger.info("A credential with this ID already exists, skipping") + continue + } + + + val credential = session.putCredential(credentialData, requireTouch[index]) + val code = + if (credentialData.oathType == YubiKitOathType.TOTP && !requireTouch[index]) { + // recalculate the code + calculateCode(session, credential) + } else null + + oathViewModel.addCredential( + Credential(credential, session.deviceId), + Code.from(code) + ) + + logger.trace("Added cred {}", credential) + successCount++ + } +// } catch (cancelled: CancellationException) { +// +// } catch (e: Throwable) { +// logger.error("Caught exception when adding multiple credentials: ", e) +// } + jsonSerializer.encodeToString(mapOf("succeeded" to successCount)) + } + } + private suspend fun reset(): String = useOathSession(OathActionDescription.Reset) { // note, it is ok to reset locked session diff --git a/lib/android/oath/state.dart b/lib/android/oath/state.dart index a8ee45e3..471a7a5c 100755 --- a/lib/android/oath/state.dart +++ b/lib/android/oath/state.dart @@ -22,6 +22,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:logging/logging.dart'; +import 'package:yubico_authenticator/exception/cancellation_exception.dart'; import '../../app/logging.dart'; import '../../app/models.dart'; @@ -139,6 +140,35 @@ final addCredentialToAnyProvider = } }); +final addCredentialsToAnyProvider = Provider( + (ref) => (List credentialUris, List touchRequired) async { + try { + _log.debug( + 'Calling android with ${credentialUris.length} credentials to be added'); + + String resultString = await _methods.invokeMethod( + 'addAccountsToAny', + { + 'uris': credentialUris, + 'requireTouch': touchRequired, + }, + ); + + _log.debug('Call result: $resultString'); + var result = jsonDecode(resultString); + return result['succeeded'] == credentialUris.length; + } on PlatformException catch (pe) { + var decodedException = pe.decode(); + if (decodedException is CancellationException) { + _log.debug('User cancelled adding multiple accounts'); + } else { + _log.error('Failed to add multiple accounts.', pe); + } + + throw decodedException; + } + }); + final androidCredentialListProvider = StateNotifierProvider.autoDispose .family?, DevicePath>( (ref, devicePath) { diff --git a/lib/android/tap_request_dialog.dart b/lib/android/tap_request_dialog.dart index 08a64a24..87dad68e 100755 --- a/lib/android/tap_request_dialog.dart +++ b/lib/android/tap_request_dialog.dart @@ -72,6 +72,7 @@ enum _DDesc { oathDeleteAccount, oathCalculateCode, oathActionFailure, + oathAddMultipleAccounts, invalid; static const int dialogDescriptionOathIndex = 100; @@ -86,7 +87,8 @@ enum _DDesc { dialogDescriptionOathIndex + 5: _DDesc.oathRenameAccount, dialogDescriptionOathIndex + 6: _DDesc.oathDeleteAccount, dialogDescriptionOathIndex + 7: _DDesc.oathCalculateCode, - dialogDescriptionOathIndex + 8: _DDesc.oathActionFailure + dialogDescriptionOathIndex + 8: _DDesc.oathActionFailure, + dialogDescriptionOathIndex + 9: _DDesc.oathAddMultipleAccounts }[id] ?? _DDesc.invalid; } @@ -158,6 +160,7 @@ class _DialogProvider { _DDesc.oathDeleteAccount => l10n.s_nfc_dialog_oath_delete_account, _DDesc.oathCalculateCode => l10n.s_nfc_dialog_oath_calculate_code, _DDesc.oathActionFailure => l10n.s_nfc_dialog_oath_failure, + _DDesc.oathAddMultipleAccounts => l10n.s_nfc_dialog_oath_add_multiple_accounts, _ => '' }; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d63bec54..b2a8f084 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -600,6 +600,7 @@ "s_nfc_dialog_oath_delete_account": "Action: delete account", "s_nfc_dialog_oath_calculate_code": "Action: calculate OATH code", "s_nfc_dialog_oath_failure": "OATH operation failed", + "s_nfc_dialog_oath_add_multiple_accounts": "Action: add multiple accounts", "@_eof": {} } \ No newline at end of file diff --git a/lib/oath/views/migrate_account_page.dart b/lib/oath/views/migrate_account_page.dart index 4673f5ed..ee371798 100644 --- a/lib/oath/views/migrate_account_page.dart +++ b/lib/oath/views/migrate_account_page.dart @@ -231,8 +231,59 @@ class _MigrateAccountPageState extends ConsumerState { } void submit() async { - _checkedCreds.forEach((k, v) => v ? accept(k) : null); - Navigator.of(context).pop(); + + _log.debug('Submitting following credentials:'); + for (var element in _checkedCreds.entries) { + if (element.value) { + _log.debug('Adding: ${element.key.toUri().toString()} ' + '/ requireTouch: ${_touchEnabled[element.key]} ' + '/ isUnique: ${_uniqueCreds[element.key] == true}'); + } + } + + if (isAndroid) { + var uris = []; + var touchRequired = []; + + // build list of uris and touch required flags for unique credentials + for (var element in _checkedCreds.entries) { + if (element.value == true && _uniqueCreds[element.key] == true) { + uris.add(element.key.toUri().toString()); + touchRequired.add(_touchEnabled[element.key] == true); + } + } + + await _addCredentials(uris: uris, touchRequired: touchRequired); + } else { + _checkedCreds.forEach((k, v) => v ? accept(k) : null); + Navigator.of(context).pop(); + } + } + + Future _addCredentials( + {required List uris, required List touchRequired}) async { + final l10n = AppLocalizations.of(context)!; + try { + await ref.read(addCredentialsToAnyProvider).call(uris, touchRequired); + if (!mounted) return; + Navigator.of(context).pop(); + showMessage(context, l10n.s_account_added); + } on CancellationException catch (_) { + // ignored + } catch (e) { + _log.error('Failed to add multiple accounts', e.toString()); + final String errorMessage; + if (e is ApduException) { + errorMessage = e.message; + } else { + errorMessage = e.toString(); + } + showMessage( + context, + l10n.l_account_add_failed(errorMessage), + duration: const Duration(seconds: 4), + ); + } } void accept(CredentialData cred) async { @@ -243,9 +294,6 @@ class _MigrateAccountPageState extends ConsumerState { devicePath: devicePath, credUri: cred.toUri(), requireTouch: _touchEnabled[cred]); - } else if (isAndroid) { - // Send the credential to Android to be added to the next YubiKey - await _doAddCredential(devicePath: null, credUri: cred.toUri()); } } From c0f927a98e009d3b7f3ef0acdaee9088e0799dc6 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Thu, 3 Aug 2023 15:34:36 +0200 Subject: [PATCH 068/158] [android] batch credentials only for NFC --- lib/oath/views/migrate_account_page.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/oath/views/migrate_account_page.dart b/lib/oath/views/migrate_account_page.dart index ee371798..96ff6722 100644 --- a/lib/oath/views/migrate_account_page.dart +++ b/lib/oath/views/migrate_account_page.dart @@ -7,6 +7,7 @@ import 'package:yubico_authenticator/theme.dart'; import '../../android/oath/state.dart'; import '../../app/models.dart'; +import '../../core/models.dart'; import '../../desktop/models.dart'; import '../../widgets/responsive_dialog.dart'; @@ -232,6 +233,9 @@ class _MigrateAccountPageState extends ConsumerState { void submit() async { + final deviceNode = ref.watch(currentDeviceProvider); + final devicePath = deviceNode?.path; + _log.debug('Submitting following credentials:'); for (var element in _checkedCreds.entries) { if (element.value) { @@ -241,7 +245,7 @@ class _MigrateAccountPageState extends ConsumerState { } } - if (isAndroid) { + if (isAndroid && (devicePath == null || deviceNode?.transport == Transport.nfc)) { var uris = []; var touchRequired = []; From fdcf841bce0479134e82f09d5d43c16aa00ff848 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Thu, 3 Aug 2023 15:35:20 +0200 Subject: [PATCH 069/158] refactor - remove commented code --- .../yubico/authenticator/oath/OathManager.kt | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt index b85568f8..977b10f0 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt @@ -397,41 +397,32 @@ class OathManager( addToAny = true return useOathSessionNfc(OathActionDescription.AddMultipleAccounts) { session -> - var successCount = 0 + for (index in uris.indices) { - // try { - for (index in uris.indices) { + val credentialData: CredentialData = + CredentialData.parseUri(URI.create(uris[index])) - val credentialData: CredentialData = - CredentialData.parseUri(URI.create(uris[index])) - - if (session.credentials.any { it.id.contentEquals(credentialData.id) }) { - logger.info("A credential with this ID already exists, skipping") - continue - } - - - val credential = session.putCredential(credentialData, requireTouch[index]) - val code = - if (credentialData.oathType == YubiKitOathType.TOTP && !requireTouch[index]) { - // recalculate the code - calculateCode(session, credential) - } else null - - oathViewModel.addCredential( - Credential(credential, session.deviceId), - Code.from(code) - ) - - logger.trace("Added cred {}", credential) - successCount++ + if (session.credentials.any { it.id.contentEquals(credentialData.id) }) { + logger.info("A credential with this ID already exists, skipping") + continue } -// } catch (cancelled: CancellationException) { -// -// } catch (e: Throwable) { -// logger.error("Caught exception when adding multiple credentials: ", e) -// } + + val credential = session.putCredential(credentialData, requireTouch[index]) + val code = + if (credentialData.oathType == YubiKitOathType.TOTP && !requireTouch[index]) { + // recalculate the code + calculateCode(session, credential) + } else null + + oathViewModel.addCredential( + Credential(credential, session.deviceId), + Code.from(code) + ) + + logger.trace("Added cred {}", credential) + successCount++ + } jsonSerializer.encodeToString(mapOf("succeeded" to successCount)) } } From 3f3298427ce865bd1c550a1b8cba8e94e551f1eb Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 8 Aug 2023 15:38:31 +0200 Subject: [PATCH 070/158] Add SafeArea to FsDialog. --- lib/app/views/fs_dialog.dart | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/lib/app/views/fs_dialog.dart b/lib/app/views/fs_dialog.dart index 30d24928..d76fa7de 100644 --- a/lib/app/views/fs_dialog.dart +++ b/lib/app/views/fs_dialog.dart @@ -27,24 +27,26 @@ class FsDialog extends StatelessWidget { final l10n = AppLocalizations.of(context)!; return Dialog.fullscreen( backgroundColor: Theme.of(context).colorScheme.background.withAlpha(100), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: SingleChildScrollView(child: child), - ), - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: TextButton.icon( - key: keys.closeButton, - icon: const Icon(Icons.close), - label: Text(l10n.s_close), - onPressed: () { - Navigator.of(context).pop(); - }, + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: SingleChildScrollView(child: child), ), - ) - ], + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: TextButton.icon( + key: keys.closeButton, + icon: const Icon(Icons.close), + label: Text(l10n.s_close), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ) + ], + ), ), ); } From 84787c912f2207ccc4c1659e572846d04c54e833 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Tue, 8 Aug 2023 16:21:56 +0200 Subject: [PATCH 071/158] Cleanup --- lib/android/oath/otp_auth_link_handler.dart | 2 +- lib/app/views/main_page.dart | 2 +- lib/oath/models.dart | 87 ++--- lib/oath/views/add_account_page.dart | 5 +- lib/oath/views/add_multi_account_page.dart | 305 +++++++++++++++++ lib/oath/views/key_actions.dart | 62 ++-- lib/oath/views/migrate_account_page.dart | 341 -------------------- 7 files changed, 384 insertions(+), 420 deletions(-) create mode 100644 lib/oath/views/add_multi_account_page.dart delete mode 100644 lib/oath/views/migrate_account_page.dart diff --git a/lib/android/oath/otp_auth_link_handler.dart b/lib/android/oath/otp_auth_link_handler.dart index d2f9c9f5..5c629bc4 100644 --- a/lib/android/oath/otp_auth_link_handler.dart +++ b/lib/android/oath/otp_auth_link_handler.dart @@ -32,7 +32,7 @@ void setupOtpAuthLinkHandler(BuildContext context) { case 'handleOtpAuthLink': { var url = args['link']; - var otpauth = CredentialData.fromUri(Uri.parse(url)); + var otpauth = CredentialData.fromOtpauth(Uri.parse(url)); Navigator.popUntil(context, ModalRoute.withName('/')); await showBlurDialog( context: context, diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index 46d87bbb..618df60c 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -106,7 +106,7 @@ class MainPage extends ConsumerWidget { try { final url = await scanner.scanQr(); if (url != null) { - otpauth = CredentialData.fromUri(Uri.parse(url)); + otpauth = CredentialData.fromOtpauth(Uri.parse(url)); } } on CancellationException catch (_) { // ignored - user cancelled diff --git a/lib/oath/models.dart b/lib/oath/models.dart index bb48e0b9..17bb2af2 100755 --- a/lib/oath/models.dart +++ b/lib/oath/models.dart @@ -123,12 +123,22 @@ class CredentialData with _$CredentialData { factory CredentialData.fromJson(Map json) => _$CredentialDataFromJson(json); + static List fromUri(Uri uri) { + if (uri.scheme.toLowerCase() == 'otpauth-migration') { + return CredentialData.fromMigration(uri); + } else if (uri.scheme.toLowerCase() == 'otpauth') { + return [CredentialData.fromOtpauth(uri)]; + } else { + throw ArgumentError('Invalid scheme'); + } + } + static List fromMigration(uri) { - List read(Uint8List bytes) { + (Uint8List, Uint8List) read(Uint8List bytes) { final index = bytes[0]; final sublist1 = bytes.sublist(1, index + 1); final sublist2 = bytes.sublist(index + 1); - return [sublist1, sublist2]; + return (sublist1, sublist2); } String b32Encode(Uint8List data) { @@ -153,92 +163,87 @@ class CredentialData with _$CredentialData { // 0a tag means new credential. // Extract secret, name, and issuer - var secretTag = data[2]; + final secretTag = data[2]; if (secretTag != 10) { // tag before secret is 0a hex throw ArgumentError('Invalid scheme, no secret tag'); } data = data.sublist(3); - final result1 = read(data); - final secret = result1[0]; - data = result1[1]; - final decodedSecret = b32Encode(secret); + final Uint8List secret; + (secret, data) = read(data); - var nameTag = data[0]; + final nameTag = data[0]; if (nameTag != 18) { // tag before name is 12 hex throw ArgumentError('Invalid scheme, no name tag'); } data = data.sublist(1); - final result2 = read(data); - final name = result2[0]; - data = result2[1]; + final Uint8List name; + (name, data) = read(data); - var issuerTag = data[0]; - List? issuer; + final issuerTag = data[0]; + Uint8List? issuer; if (issuerTag == 26) { // tag before issuer is 1a hex, but issuer is optional. data = data.sublist(1); - final result3 = read(data); - issuer = result3[0]; - data = result3[1]; + (issuer, data) = read(data); } // Extract algorithm, number of digits, and oath type: - var algoTag = data[0]; + final algoTag = data[0]; if (algoTag != 32) { // tag before algo is 20 hex throw ArgumentError('Invalid scheme, no algo tag'); } - int algo = data[1]; + final algo = data[1]; - var digitsTag = data[2]; + final digitsTag = data[2]; if (digitsTag != 40) { // tag before digits is 28 hex throw ArgumentError('Invalid scheme, no digits tag'); } - var digits = data[3]; + final digits = data[3]; - var oathTag = data[4]; + final oathTag = data[4]; if (oathTag != 48) { // tag before oath is 30 hex throw ArgumentError('Invalid scheme, no oath tag'); } - var oathType = data[5]; + final oathType = data[5]; - int counter = defaultCounter; + var counter = defaultCounter; if (oathType == 1) { // if hotp, extract counter counter = data[7]; - } - - final credential = CredentialData( - issuer: - issuerTag != 26 ? null : utf8.decode(issuer!, allowMalformed: true), - name: utf8.decode(name, allowMalformed: true), - oathType: oathType == 1 ? OathType.hotp : OathType.totp, - secret: decodedSecret, - hashAlgorithm: algo == 1 - ? HashAlgorithm.sha1 - : (algo == 2 ? HashAlgorithm.sha256 : HashAlgorithm.sha512), - digits: digits == 1 ? defaultDigits : 8, - counter: counter, - ); - - credentials.add(credential); - if (oathType == 1) { data = data.sublist(8); } else { data = data.sublist(6); } + + final credential = CredentialData( + issuer: + issuer != null ? utf8.decode(issuer, allowMalformed: true) : null, + name: utf8.decode(name, allowMalformed: true), + oathType: oathType == 1 ? OathType.hotp : OathType.totp, + secret: b32Encode(secret), + hashAlgorithm: switch (algo) { + 2 => HashAlgorithm.sha256, + 3 => HashAlgorithm.sha512, + _ => HashAlgorithm.sha1, + }, + digits: digits == 2 ? 8 : defaultDigits, + counter: counter, + ); + + credentials.add(credential); tag = data[0]; } return credentials; } - factory CredentialData.fromUri(Uri uri) { + factory CredentialData.fromOtpauth(Uri uri) { final oathType = OathType.values.byName(uri.host.toLowerCase()); final params = uri.queryParameters; String? issuer; diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index b1fac0d6..1219b472 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -131,7 +131,7 @@ class _OathAddAccountPageState extends ConsumerState { _qrState = _QrScanState.failed; }); } else { - final data = CredentialData.fromUri(Uri.parse(otpauth)); + final data = CredentialData.fromOtpauth(Uri.parse(otpauth)); _loadCredentialData(data); } } catch (e) { @@ -176,7 +176,6 @@ class _OathAddAccountPageState extends ConsumerState { {DevicePath? devicePath, required Uri credUri}) async { final l10n = AppLocalizations.of(context)!; try { - FocusUtils.unfocus(context); if (devicePath == null) { @@ -375,7 +374,7 @@ class _OathAddAccountPageState extends ConsumerState { if (!mounted) return; showMessage(context, l10n.l_qr_not_found); } else { - final data = CredentialData.fromUri(Uri.parse(otpauth)); + final data = CredentialData.fromOtpauth(Uri.parse(otpauth)); _loadCredentialData(data); } } diff --git a/lib/oath/views/add_multi_account_page.dart b/lib/oath/views/add_multi_account_page.dart new file mode 100644 index 00000000..2dcea2d2 --- /dev/null +++ b/lib/oath/views/add_multi_account_page.dart @@ -0,0 +1,305 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:yubico_authenticator/app/logging.dart'; +import 'package:yubico_authenticator/exception/apdu_exception.dart'; + +import '../../android/oath/state.dart'; +import '../../app/models.dart'; +import '../../core/models.dart'; +import '../../desktop/models.dart'; +import '../../widgets/responsive_dialog.dart'; + +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../models.dart'; +import '../../app/state.dart'; +import '../../core/state.dart'; +import '../state.dart'; +import '../../app/message.dart'; + +import '../../exception/cancellation_exception.dart'; +import 'rename_list_account.dart'; + +final _log = Logger('oath.views.list_screen'); + +class OathAddMultiAccountPage extends ConsumerStatefulWidget { + final DevicePath? devicePath; + final OathState? state; + final List? credentialsFromUri; + + const OathAddMultiAccountPage( + this.devicePath, this.state, this.credentialsFromUri, + {super.key}); + + @override + ConsumerState createState() => + _OathAddMultiAccountPageState(); +} + +class _OathAddMultiAccountPageState + extends ConsumerState { + int? _numCreds; + + late Map _credStates; + List? _credentials; + + @override + void initState() { + super.initState(); + _credStates = Map.fromIterable(widget.credentialsFromUri!, + value: (v) => (true, false, false)); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context)!; + + if (widget.devicePath != null) { + _credentials = ref + .watch(credentialListProvider(widget.devicePath!)) + ?.map((e) => e.credential) + .toList(); + + _numCreds = ref.watch(credentialListProvider(widget.devicePath!) + .select((value) => value?.length)); + } + + // If the credential is not unique, make sure the checkbox is not checked + checkForDuplicates(); + + return ResponsiveDialog( + title: Text(l10n.s_add_accounts), + actions: [ + TextButton( + onPressed: isValid() ? submit : null, + child: Text(l10n.s_save), + ) + ], + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Text(l10n.l_select_accounts)), + ...widget.credentialsFromUri!.map( + (cred) { + final (checked, touch, unique) = _credStates[cred]!; + return CheckboxListTile( + controlAffinity: ListTileControlAffinity.leading, + secondary: Row(mainAxisSize: MainAxisSize.min, children: [ + if (isTouchSupported()) + IconButton( + color: touch ? colorScheme.primary : null, + onPressed: unique + ? () { + setState(() { + _credStates[cred] = + (checked, !touch, unique); + }); + } + : null, + icon: const Icon(Icons.touch_app_outlined)), + IconButton( + onPressed: () async { + final node = ref + .read(currentDeviceDataProvider) + .valueOrNull + ?.node; + final withContext = ref.read(withContextProvider); + CredentialData renamed = await withContext( + (context) async => await showBlurDialog( + context: context, + builder: (context) => RenameList(node!, cred, + widget.credentialsFromUri, _credentials), + )); + + setState(() { + int index = widget.credentialsFromUri!.indexWhere( + (element) => + element.name == cred.name && + (element.issuer == cred.issuer)); + widget.credentialsFromUri![index] = renamed; + _credStates.remove(cred); + _credStates[renamed] = (true, touch, true); + }); + }, + icon: IconTheme( + data: IconTheme.of(context), + child: const Icon(Icons.edit_outlined)), + ), + ]), + title: Text(cred.issuer ?? cred.name, + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: false), + value: unique && checked, + enabled: unique, + subtitle: cred.issuer != null || !unique + ? Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (cred.issuer != null) + Text(cred.name, + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: false), + if (!unique) + Text( + l10n.l_account_already_exists, + style: TextStyle( + color: colorScheme.error, + fontSize: 12, // TODO: use Theme + ), + ) + ]) + : null, + onChanged: (bool? value) { + setState(() { + _credStates[cred] = (value == true, touch, unique); + }); + }, + ); + }, + ) + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + )); + } + + bool isTouchSupported() { + bool touch = true; + if (!(widget.state?.version.isAtLeast(4, 2) ?? true)) { + // Touch not supported + touch = false; + } + return touch; + } + + void checkForDuplicates() { + for (final item in _credStates.entries) { + CredentialData cred = item.key; + final (checked, touch, _) = item.value; + final unique = isUnique(cred); + _credStates[cred] = (checked && unique, touch, unique); + } + } + + bool isUnique(CredentialData cred) { + String nameText = cred.name; + String? issuerText = cred.issuer ?? ''; + bool ans = _credentials + ?.where((element) => + element.name == nameText && + (element.issuer ?? '') == issuerText) + .isEmpty ?? + true; + + return ans; + } + + bool isValid() { + final credsToAdd = _credStates.values.where((element) => element.$1).length; + int? capacity = widget.state!.version.isAtLeast(4) ? 32 : null; + return (credsToAdd > 0) && + (capacity == null || (_numCreds! + credsToAdd <= capacity)); + } + + void submit() async { + final deviceNode = ref.watch(currentDeviceProvider); + if (isAndroid && + (widget.devicePath == null || deviceNode?.transport == Transport.nfc)) { + var uris = []; + var touchRequired = []; + + // build list of uris and touch required flags for unique credentials + for (final item in _credStates.entries) { + CredentialData cred = item.key; + final (checked, touch, _) = item.value; + if (checked) { + uris.add(cred.toUri().toString()); + touchRequired.add(touch); + } + } + + await _addCredentials(uris: uris, touchRequired: touchRequired); + } else { + _credStates.forEach((cred, value) { + if (value.$1) { + accept(cred, value.$2); + } + }); + + Navigator.of(context).pop(); + } + } + + Future _addCredentials( + {required List uris, required List touchRequired}) async { + final l10n = AppLocalizations.of(context)!; + try { + await ref.read(addCredentialsToAnyProvider).call(uris, touchRequired); + if (!mounted) return; + Navigator.of(context).pop(); + showMessage(context, l10n.s_account_added); + } on CancellationException catch (_) { + // ignored + } catch (e) { + _log.error('Failed to add multiple accounts', e.toString()); + final String errorMessage; + if (e is ApduException) { + errorMessage = e.message; + } else { + errorMessage = e.toString(); + } + showMessage( + context, + l10n.l_account_add_failed(errorMessage), + duration: const Duration(seconds: 4), + ); + } + } + + void accept(CredentialData cred, bool touch) async { + final l10n = AppLocalizations.of(context)!; + final devicePath = widget.devicePath; + try { + if (devicePath == null) { + assert(isAndroid, 'devicePath is only optional for Android'); + await ref + .read(addCredentialToAnyProvider) + .call(cred.toUri(), requireTouch: touch); + } else { + await ref + .read(credentialListProvider(devicePath).notifier) + .addAccount(cred.toUri(), requireTouch: touch); + } + if (!mounted) return; + //Navigator.of(context).pop(); + showMessage(context, l10n.s_account_added); + } on CancellationException catch (_) { + // ignored + } catch (e) { + _log.error('Failed to add account', e); + final String errorMessage; + // TODO: Make this cleaner than importing desktop specific RpcError. + if (e is RpcError) { + errorMessage = e.message; + } else if (e is ApduException) { + errorMessage = e.message; + } else { + errorMessage = e.toString(); + } + showMessage( + context, + l10n.l_account_add_failed(errorMessage), + duration: const Duration(seconds: 4), + ); + } + } +} diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index 55f1d3d9..fafa35e0 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -26,13 +26,14 @@ import '../../app/views/fs_dialog.dart'; import '../../app/views/action_list.dart'; import '../../core/state.dart'; import '../../exception/cancellation_exception.dart'; +import '../keys.dart'; import '../models.dart'; import '../state.dart'; import '../keys.dart' as keys; import 'add_account_page.dart'; import 'manage_password_dialog.dart'; import 'reset_dialog.dart'; -import 'migrate_account_page.dart'; +import 'add_multi_account_page.dart'; Widget oathBuildActions( BuildContext context, @@ -70,7 +71,8 @@ Widget oathBuildActions( try { final url = await scanner.scanQr(); if (url != null) { - otpauth = CredentialData.fromUri(Uri.parse(url)); + otpauth = + CredentialData.fromOtpauth(Uri.parse(url)); } } on CancellationException catch (_) { // ignored - user cancelled @@ -100,39 +102,33 @@ Widget oathBuildActions( final credentials = ref.read(credentialsProvider); final qrScanner = ref.watch(qrScannerProvider); if (qrScanner != null) { - final otpauth = await qrScanner.scanQr(); - if (otpauth == null) { - await ref.read(withContextProvider)((context) async => - showMessage(context, l10n.l_qr_not_found)); - } else { - String s = 'otpauth-migration'; - if (otpauth.contains(s)) { - final data = - CredentialData.fromMigration(Uri.parse(otpauth)); - await withContext((context) async { - await showBlurDialog( - context: context, - builder: (context) => - MigrateAccountPage(devicePath, oathState, data), - ); - }); - } else if (otpauth.contains('otpauth')) { - final data = CredentialData.fromUri(Uri.parse(otpauth)); - await withContext((context) async { - await showBlurDialog( - context: context, - builder: (context) => OathAddAccountPage( - devicePath, - oathState, - credentials: credentials, - credentialData: data, - ), - ); - }); + final uri = await qrScanner.scanQr(); + List creds = + uri != null ? CredentialData.fromUri(Uri.parse(uri)) : []; + await withContext((context) async { + if (creds.isEmpty) { + showMessage(context, l10n.l_qr_not_found); + } else if (creds.length == 1) { + await showBlurDialog( + context: context, + builder: (context) => OathAddAccountPage( + devicePath, + oathState, + credentials: credentials, + credentialData: creds[0], + ), + ); + } else { + await showBlurDialog( + context: context, + builder: (context) => OathAddMultiAccountPage( + devicePath, oathState, creds, + key: migrateAccountAction), + ); } - } + }); } - await ref.read(withContextProvider)( + await withContext( (context) async => Navigator.of(context).pop()); }), ]), diff --git a/lib/oath/views/migrate_account_page.dart b/lib/oath/views/migrate_account_page.dart deleted file mode 100644 index 96ff6722..00000000 --- a/lib/oath/views/migrate_account_page.dart +++ /dev/null @@ -1,341 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logging/logging.dart'; -import 'package:yubico_authenticator/app/logging.dart'; -import 'package:yubico_authenticator/exception/apdu_exception.dart'; -import 'package:yubico_authenticator/theme.dart'; - -import '../../android/oath/state.dart'; -import '../../app/models.dart'; -import '../../core/models.dart'; -import '../../desktop/models.dart'; -import '../../widgets/responsive_dialog.dart'; - -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../keys.dart'; -import '../models.dart'; -import '../../app/state.dart'; -import '../../core/state.dart'; -import '../state.dart'; -import '../../app/message.dart'; - -import '../../exception/cancellation_exception.dart'; -import 'rename_list_account.dart'; - -final _log = Logger('oath.views.list_screen'); - -class MigrateAccountPage extends ConsumerStatefulWidget { - final DevicePath devicePath; - final OathState? state; - final List? credentialsFromUri; - - const MigrateAccountPage(this.devicePath, this.state, this.credentialsFromUri) - : super(key: migrateAccountAction); - - @override - ConsumerState createState() => - _MigrateAccountPageState(); -} - -class _MigrateAccountPageState extends ConsumerState { - int? _numCreds; - late Map _checkedCreds; - late Map _touchEnabled; - late Map _uniqueCreds; - List? _credentials; - - @override - void initState() { - super.initState(); - _checkedCreds = - Map.fromIterable(widget.credentialsFromUri!, value: (v) => true); - _touchEnabled = - Map.fromIterable(widget.credentialsFromUri!, value: (v) => false); - _uniqueCreds = - Map.fromIterable(widget.credentialsFromUri!, value: (v) => false); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final darkMode = theme.brightness == Brightness.dark; - final l10n = AppLocalizations.of(context)!; - final deviceNode = ref.watch(currentDeviceProvider); - - _credentials = ref - .watch(credentialListProvider(deviceNode!.path)) - ?.map((e) => e.credential) - .toList(); - - _numCreds = ref.watch(credentialListProvider(widget.devicePath) - .select((value) => value?.length)); - - checkForDuplicates(); - // If the credential is not unique, make sure the checkbox is not checked - uncheckDuplicates(); - - return ResponsiveDialog( - title: Text(l10n.s_add_accounts), - actions: [ - TextButton( - onPressed: isValid() ? submit : null, - child: Text(l10n.s_save), - ) - ], - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: Text(l10n.l_select_accounts)), - ...widget.credentialsFromUri!.map( - (cred) => CheckboxListTile( - controlAffinity: ListTileControlAffinity.leading, - secondary: Row(mainAxisSize: MainAxisSize.min, children: [ - if (isTouchSupported()) - IconButton( - color: _touchEnabled[cred]! - ? (darkMode ? primaryGreen : primaryBlue) - : null, - onPressed: _uniqueCreds[cred]! - ? () { - setState(() { - _touchEnabled[cred] = !_touchEnabled[cred]!; - }); - } - : null, - icon: const Icon(Icons.touch_app_outlined)), - IconButton( - onPressed: () async { - final node = ref - .watch(currentDeviceDataProvider) - .valueOrNull - ?.node; - final withContext = ref.read(withContextProvider); - CredentialData renamed = await withContext( - (context) async => await showBlurDialog( - context: context, - builder: (context) => RenameList(node!, cred, - widget.credentialsFromUri, _credentials), - )); - - setState(() { - int index = widget.credentialsFromUri!.indexWhere( - (element) => - element.name == cred.name && - (element.issuer == cred.issuer)); - widget.credentialsFromUri![index] = renamed; - _checkedCreds[cred] = false; - _checkedCreds[renamed] = true; - _touchEnabled[renamed] = false; - }); - }, - icon: const Icon(Icons.edit_outlined), - color: darkMode ? Colors.white : Colors.black, - ), - ]), - title: Text(getTitle(cred), - overflow: TextOverflow.fade, maxLines: 1, softWrap: false), - value: _uniqueCreds[cred]! ? _checkedCreds[cred] : false, - enabled: _uniqueCreds[cred]!, - subtitle: cred.issuer != null || !_uniqueCreds[cred]! - ? Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (cred.issuer != null) - Text(cred.name, - overflow: TextOverflow.fade, - maxLines: 1, - softWrap: false), - if (!_uniqueCreds[cred]!) - Text( - l10n.l_account_already_exists, - style: const TextStyle( - color: primaryRed, - fontSize: 12, - ), - ) - ]) - : null, - onChanged: (bool? value) { - setState(() { - _checkedCreds[cred] = value!; - }); - }, - ), - ) - ] - .map((e) => Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: e, - )) - .toList(), - )); - } - - bool isTouchSupported() { - bool touch = true; - if (!(widget.state?.version.isAtLeast(4, 2) ?? true)) { - // Touch not supported - touch = false; - } - return touch; - } - - String getTitle(CredentialData cred) { - if (cred.issuer != null) { - return cred.issuer!; - } - return cred.name; - } - - void checkForDuplicates() { - for (var item in _checkedCreds.entries) { - CredentialData cred = item.key; - _uniqueCreds[cred] = isUnique(cred); - } - } - - void uncheckDuplicates() { - for (var item in _checkedCreds.entries) { - CredentialData cred = item.key; - - if (!_uniqueCreds[cred]!) { - _checkedCreds[cred] = false; - } - } - } - - bool isUnique(CredentialData cred) { - String nameText = cred.name; - String? issuerText = cred.issuer ?? ''; - bool ans = _credentials - ?.where((element) => - element.name == nameText && - (element.issuer ?? '') == issuerText) - .isEmpty ?? - true; - - return ans; - } - - bool isValid() { - int credsToAdd = 0; - int? capacity = widget.state!.version.isAtLeast(4) ? 32 : null; - _checkedCreds.forEach((k, v) => v ? credsToAdd++ : null); - if ((credsToAdd > 0) && - (capacity == null || (_numCreds! + credsToAdd <= capacity))) { - return true; - } - return false; - } - - void submit() async { - - final deviceNode = ref.watch(currentDeviceProvider); - final devicePath = deviceNode?.path; - - _log.debug('Submitting following credentials:'); - for (var element in _checkedCreds.entries) { - if (element.value) { - _log.debug('Adding: ${element.key.toUri().toString()} ' - '/ requireTouch: ${_touchEnabled[element.key]} ' - '/ isUnique: ${_uniqueCreds[element.key] == true}'); - } - } - - if (isAndroid && (devicePath == null || deviceNode?.transport == Transport.nfc)) { - var uris = []; - var touchRequired = []; - - // build list of uris and touch required flags for unique credentials - for (var element in _checkedCreds.entries) { - if (element.value == true && _uniqueCreds[element.key] == true) { - uris.add(element.key.toUri().toString()); - touchRequired.add(_touchEnabled[element.key] == true); - } - } - - await _addCredentials(uris: uris, touchRequired: touchRequired); - } else { - _checkedCreds.forEach((k, v) => v ? accept(k) : null); - Navigator.of(context).pop(); - } - } - - Future _addCredentials( - {required List uris, required List touchRequired}) async { - final l10n = AppLocalizations.of(context)!; - try { - await ref.read(addCredentialsToAnyProvider).call(uris, touchRequired); - if (!mounted) return; - Navigator.of(context).pop(); - showMessage(context, l10n.s_account_added); - } on CancellationException catch (_) { - // ignored - } catch (e) { - _log.error('Failed to add multiple accounts', e.toString()); - final String errorMessage; - if (e is ApduException) { - errorMessage = e.message; - } else { - errorMessage = e.toString(); - } - showMessage( - context, - l10n.l_account_add_failed(errorMessage), - duration: const Duration(seconds: 4), - ); - } - } - - void accept(CredentialData cred) async { - final deviceNode = ref.watch(currentDeviceProvider); - final devicePath = deviceNode?.path; - if (devicePath != null) { - await _doAddCredential( - devicePath: devicePath, - credUri: cred.toUri(), - requireTouch: _touchEnabled[cred]); - } - } - - Future _doAddCredential( - {DevicePath? devicePath, - required Uri credUri, - bool? requireTouch}) async { - final l10n = AppLocalizations.of(context)!; - try { - if (devicePath == null) { - assert(isAndroid, 'devicePath is only optional for Android'); - await ref.read(addCredentialToAnyProvider).call(credUri); - } else { - await ref - .read(credentialListProvider(devicePath).notifier) - .addAccount(credUri, requireTouch: requireTouch!); - } - if (!mounted) return; - //Navigator.of(context).pop(); - showMessage(context, l10n.s_account_added); - } on CancellationException catch (_) { - // ignored - } catch (e) { - _log.error('Failed to add account', e); - final String errorMessage; - // TODO: Make this cleaner than importing desktop specific RpcError. - if (e is RpcError) { - errorMessage = e.message; - } else if (e is ApduException) { - errorMessage = e.message; - } else { - errorMessage = e.toString(); - } - showMessage( - context, - l10n.l_account_add_failed(errorMessage), - duration: const Duration(seconds: 4), - ); - } - } -} From b61791f8e8e0cc4c2831e664a5e6b4c982af2272 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Fri, 11 Aug 2023 08:55:38 +0200 Subject: [PATCH 072/158] Use protobuf --- lib/oath/models.dart | 150 ++++++++++++++----------------------------- 1 file changed, 49 insertions(+), 101 deletions(-) diff --git a/lib/oath/models.dart b/lib/oath/models.dart index 17bb2af2..a054b6cf 100755 --- a/lib/oath/models.dart +++ b/lib/oath/models.dart @@ -133,114 +133,62 @@ class CredentialData with _$CredentialData { } } - static List fromMigration(uri) { - (Uint8List, Uint8List) read(Uint8List bytes) { - final index = bytes[0]; - final sublist1 = bytes.sublist(1, index + 1); - final sublist2 = bytes.sublist(index + 1); - return (sublist1, sublist2); + static List fromMigration(Uri uri) { + // Parse a single protobuf value from a buffer + (int tag, dynamic value, Uint8List rem) protoValue(Uint8List data) { + final first = data[0]; + final index = first >> 3; + final second = data[1]; + data = data.sublist(2); + switch (first & 0x07) { + case 0: + assert(second & 0x80 == 0); + return (index, second, data); + case 2: + assert(second & 0x80 == 0); + return (index, data.sublist(0, second), data.sublist(second)); + } + throw ArgumentError('Unsupported value type!'); } - String b32Encode(Uint8List data) { - final encodedData = base32.encode(data); - return utf8.decode(encodedData.runes.toList()); + // Parse a protobuf message into map of tags and values + Map protoMap(Uint8List data) { + Map values = {}; + while (data.isNotEmpty) { + final (tag, value, rem) = protoValue(data); + values[tag] = value; + data = rem; + } + return values; } - final uriString = uri.toString(); - var data = Uint8List.fromList( - base64.decode(Uri.decodeComponent(uriString.split('=')[1]))); - - var credentials = []; - - var tag = data[0]; - - /* - Assuming the credential(s) follow the format: - cred = 0aLENGTH0aSECRET12NAME1aISSUER20ALGO28DIGITS30OATHxxx - where xxx can be another cred. - */ - while (tag == 10) { - // 0a tag means new credential. - - // Extract secret, name, and issuer - final secretTag = data[2]; - if (secretTag != 10) { - // tag before secret is 0a hex - throw ArgumentError('Invalid scheme, no secret tag'); + // Parse encoded credentials from data (tag 1) ignoring trailing extra data + Iterable> splitCreds(Uint8List rem) sync* { + Uint8List credrem; + while (rem[0] == 0x0a) { + (_, credrem, rem) = protoValue(rem); + yield protoMap(credrem); } - data = data.sublist(3); - final Uint8List secret; - (secret, data) = read(data); - - final nameTag = data[0]; - if (nameTag != 18) { - // tag before name is 12 hex - throw ArgumentError('Invalid scheme, no name tag'); - } - data = data.sublist(1); - final Uint8List name; - (name, data) = read(data); - - final issuerTag = data[0]; - Uint8List? issuer; - - if (issuerTag == 26) { - // tag before issuer is 1a hex, but issuer is optional. - data = data.sublist(1); - (issuer, data) = read(data); - } - - // Extract algorithm, number of digits, and oath type: - final algoTag = data[0]; - if (algoTag != 32) { - // tag before algo is 20 hex - throw ArgumentError('Invalid scheme, no algo tag'); - } - final algo = data[1]; - - final digitsTag = data[2]; - if (digitsTag != 40) { - // tag before digits is 28 hex - throw ArgumentError('Invalid scheme, no digits tag'); - } - final digits = data[3]; - - final oathTag = data[4]; - if (oathTag != 48) { - // tag before oath is 30 hex - throw ArgumentError('Invalid scheme, no oath tag'); - } - final oathType = data[5]; - - var counter = defaultCounter; - if (oathType == 1) { - // if hotp, extract counter - counter = data[7]; - data = data.sublist(8); - } else { - data = data.sublist(6); - } - - final credential = CredentialData( - issuer: - issuer != null ? utf8.decode(issuer, allowMalformed: true) : null, - name: utf8.decode(name, allowMalformed: true), - oathType: oathType == 1 ? OathType.hotp : OathType.totp, - secret: b32Encode(secret), - hashAlgorithm: switch (algo) { - 2 => HashAlgorithm.sha256, - 3 => HashAlgorithm.sha512, - _ => HashAlgorithm.sha1, - }, - digits: digits == 2 ? 8 : defaultDigits, - counter: counter, - ); - - credentials.add(credential); - tag = data[0]; } - return credentials; + // Convert parsed credential values into CredentialData objects + return splitCreds(base64.decode(uri.queryParameters['data']!)) + .map((values) => CredentialData( + secret: base32.encode(values[1]), + name: utf8.decode(values[2], allowMalformed: true), + issuer: values[3] != null + ? utf8.decode(values[3], allowMalformed: true) + : null, + hashAlgorithm: switch (values[4]) { + 2 => HashAlgorithm.sha256, + 3 => HashAlgorithm.sha512, + _ => HashAlgorithm.sha1, + }, + digits: values[5] == 2 ? 8 : defaultDigits, + oathType: values[6] == 1 ? OathType.hotp : OathType.totp, + counter: values[7] ?? defaultCounter, + )) + .toList(); } factory CredentialData.fromOtpauth(Uri uri) { From bdcc33ea614ff243240977e1d0881af777b0c381 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 11 Aug 2023 11:59:32 +0200 Subject: [PATCH 073/158] Handle values >127. --- lib/oath/models.dart | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/oath/models.dart b/lib/oath/models.dart index a054b6cf..0e0998f5 100755 --- a/lib/oath/models.dart +++ b/lib/oath/models.dart @@ -134,19 +134,27 @@ class CredentialData with _$CredentialData { } static List fromMigration(Uri uri) { + // Parse single protobuf encoded integer + (int value, Uint8List rem) protoInt(Uint8List data) { + final extras = data.takeWhile((b) => b & 0x80 != 0).length; + int value = 0; + for (int i = extras; i >= 0; i--) { + value = (value << 7) | (data[i] & 0x7F); + } + return (value, data.sublist(1 + extras)); + } + // Parse a single protobuf value from a buffer (int tag, dynamic value, Uint8List rem) protoValue(Uint8List data) { final first = data[0]; + final int len; + (len, data) = protoInt(data.sublist(1)); final index = first >> 3; - final second = data[1]; - data = data.sublist(2); switch (first & 0x07) { case 0: - assert(second & 0x80 == 0); - return (index, second, data); + return (index, len, data); case 2: - assert(second & 0x80 == 0); - return (index, data.sublist(0, second), data.sublist(second)); + return (index, data.sublist(0, len), data.sublist(len)); } throw ArgumentError('Unsupported value type!'); } From 97d0dd46ce8aa405cc421f9410f1ae6f68cb5d07 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 11 Aug 2023 12:00:14 +0200 Subject: [PATCH 074/158] Fix: Retain counter value scanned from QR. --- lib/oath/views/add_account_page.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index 1219b472..4d272ab3 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -82,6 +82,7 @@ class _OathAddAccountPageState extends ConsumerState { OathType _oathType = defaultOathType; HashAlgorithm _hashAlgorithm = defaultHashAlgorithm; int _digits = defaultDigits; + int _counter = defaultCounter; bool _validateSecretLength = false; _QrScanState _qrState = _QrScanState.none; bool _isObscure = true; @@ -167,6 +168,7 @@ class _OathAddAccountPageState extends ConsumerState { _periodController.text = '${data.period}'; _digitsValues = [data.digits]; _digits = data.digits; + _counter = data.counter; _isObscure = true; _qrState = _QrScanState.success; }); @@ -329,6 +331,7 @@ class _OathAddAccountPageState extends ConsumerState { hashAlgorithm: _hashAlgorithm, digits: _digits, period: period, + counter: _counter, ); final devicePath = deviceNode?.path; From 5d45de051024093313544545d316e39501da771d Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 11 Aug 2023 16:30:22 +0200 Subject: [PATCH 075/158] Use copyWith to keep existing values. --- lib/oath/views/rename_list_account.dart | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/oath/views/rename_list_account.dart b/lib/oath/views/rename_list_account.dart index e75db433..123c3484 100644 --- a/lib/oath/views/rename_list_account.dart +++ b/lib/oath/views/rename_list_account.dart @@ -57,21 +57,17 @@ class _RenameListState extends ConsumerState { _account = widget.credential.name.trim(); } - void _submit() async { + void _submit() { + if (!mounted) return; + final l10n = AppLocalizations.of(context)!; try { // Rename credentials - final credential = CredentialData( + final credential = widget.credential.copyWith( issuer: _issuer == '' ? null : _issuer, name: _account, - oathType: widget.credential.oathType, - secret: widget.credential.secret, - hashAlgorithm: widget.credential.hashAlgorithm, - digits: widget.credential.digits, - counter: widget.credential.counter, ); - if (!mounted) return; Navigator.of(context).pop(credential); showMessage(context, l10n.s_account_renamed); } on CancellationException catch (_) { From 0ff06a52e5bd802d221ab63b8a3864b0f6786eae Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Wed, 16 Aug 2023 09:47:25 +0200 Subject: [PATCH 076/158] Add a dialog for adding account --- lib/l10n/app_en.arb | 2 + lib/oath/views/add_account_dialog.dart | 153 +++++++++++++++++++++++++ lib/oath/views/key_actions.dart | 133 +++++++++------------ 3 files changed, 207 insertions(+), 81 deletions(-) create mode 100644 lib/oath/views/add_account_dialog.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b2a8f084..38bc8f36 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -286,6 +286,8 @@ "s_no_accounts": "No accounts", "s_add_account": "Add account", "s_add_accounts" : "Add account(s)", + "p_add_description" : "To scan a QR code, make sure the full code is visible on screen and press the button below. You can also drag a saved image from a folder onto this dialog. If you have the account credential details in writing, use the manual entry instead.", + "s_add_manually" : "Add manually", "s_account_added": "Account added", "l_account_add_failed": "Failed adding account: {message}", "@l_account_add_failed" : { diff --git a/lib/oath/views/add_account_dialog.dart b/lib/oath/views/add_account_dialog.dart new file mode 100644 index 00000000..8ebfb775 --- /dev/null +++ b/lib/oath/views/add_account_dialog.dart @@ -0,0 +1,153 @@ +/* + * 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:convert'; +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/message.dart'; +import 'package:yubico_authenticator/app/state.dart'; +import 'package:yubico_authenticator/widgets/responsive_dialog.dart'; + +import '../../app/models.dart'; +import '../../widgets/file_drop_target.dart'; +import '../keys.dart'; +import '../models.dart'; +import '../state.dart'; +import 'add_account_page.dart'; +import 'add_multi_account_page.dart'; + +class AddAccountDialog extends ConsumerStatefulWidget { + final DevicePath? devicePath; + final OathState? state; + + const AddAccountDialog(this.devicePath, this.state, {super.key}); + + @override + ConsumerState createState() => + _AddAccountDialogState(); +} + +class _AddAccountDialogState extends ConsumerState { + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final credentials = ref.read(credentialsProvider); + final withContext = ref.read(withContextProvider); + + final qrScanner = ref.watch(qrScannerProvider); + return ResponsiveDialog( + title: Text(l10n.s_add_account), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: FileDropTarget( + onFileDropped: (fileData) async { + Navigator.of(context).pop(); + if (qrScanner != null) { + final b64Image = base64Encode(fileData); + final uri = await qrScanner.scanQr(b64Image); + if (uri == null) { + if (!mounted) return; + showMessage(context, l10n.l_qr_not_found); + } else { + final otpauth = CredentialData.fromOtpauth(Uri.parse(uri)); + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => OathAddAccountPage( + widget.devicePath, + widget.state, + credentials: credentials, + credentialData: otpauth, + ), + ); + }); + } + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.p_add_description), + const SizedBox(height: 4), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4.0, + runSpacing: 8.0, + children: [ + ActionChip( + avatar: const Icon(Icons.qr_code_scanner_outlined), + label: Text(l10n.s_qr_scan), + onPressed: () async { + Navigator.of(context).pop(); + if (qrScanner != null) { + final uri = await qrScanner.scanQr(); + List creds = uri != null + ? CredentialData.fromUri(Uri.parse(uri)) + : []; + await withContext((context) async { + if (creds.isEmpty) { + showMessage(context, l10n.l_qr_not_found); + } else if (creds.length == 1) { + await showBlurDialog( + context: context, + builder: (context) => OathAddAccountPage( + widget.devicePath, + widget.state, + credentials: credentials, + credentialData: creds[0], + ), + ); + } else { + await showBlurDialog( + context: context, + builder: (context) => OathAddMultiAccountPage( + widget.devicePath, widget.state, creds, + key: migrateAccountAction), + ); + } + }); + } + }, + ), + ActionChip( + avatar: const Icon(Icons.edit_outlined), + label: Text(l10n.s_add_manually), + onPressed: () async { + Navigator.of(context).pop(); + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => OathAddAccountPage( + widget.devicePath, + widget.state, + credentials: credentials, + ), + ); + }); + }), + ]) + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + )); + } +} diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index fafa35e0..2d8b12f6 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -18,6 +18,7 @@ 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/oath/icon_provider/icon_pack_dialog.dart'; +import 'package:yubico_authenticator/oath/views/add_account_dialog.dart'; import '../../app/message.dart'; import '../../app/models.dart'; @@ -25,15 +26,14 @@ import '../../app/state.dart'; import '../../app/views/fs_dialog.dart'; import '../../app/views/action_list.dart'; import '../../core/state.dart'; -import '../../exception/cancellation_exception.dart'; import '../keys.dart'; import '../models.dart'; -import '../state.dart'; import '../keys.dart' as keys; +import '../state.dart'; import 'add_account_page.dart'; +import 'add_multi_account_page.dart'; import 'manage_password_dialog.dart'; import 'reset_dialog.dart'; -import 'add_multi_account_page.dart'; Widget oathBuildActions( BuildContext context, @@ -50,87 +50,58 @@ Widget oathBuildActions( children: [ ActionListSection(l10n.s_setup, children: [ ActionListItem( - key: keys.addAccountAction, - actionStyle: ActionStyle.primary, - icon: const Icon(Icons.person_add_alt_1_outlined), - title: l10n.s_add_account, - subtitle: used == null - ? l10n.l_unlock_first - : (capacity != null - ? l10n.l_accounts_used(used, capacity) - : ''), - onTap: used != null && (capacity == null || capacity > used) - ? (context) async { - final credentials = ref.read(credentialsProvider); - final withContext = ref.read(withContextProvider); - Navigator.of(context).pop(); - CredentialData? otpauth; - if (isAndroid) { - final scanner = ref.read(qrScannerProvider); - if (scanner != null) { - try { - final url = await scanner.scanQr(); - if (url != null) { - otpauth = - CredentialData.fromOtpauth(Uri.parse(url)); - } - } on CancellationException catch (_) { - // ignored - user cancelled - return; + title: l10n.s_add_account, + subtitle: used == null + ? l10n.l_unlock_first + : (capacity != null + ? l10n.l_accounts_used(used, capacity) + : ''), + actionStyle: ActionStyle.primary, + icon: const Icon(Icons.person_add_alt_1_outlined), + onTap: used != null && (capacity == null || capacity > used) + ? (context) async { + final credentials = ref.read(credentialsProvider); + final withContext = ref.read(withContextProvider); + Navigator.of(context).pop(); + if (isAndroid) { + final qrScanner = ref.read(qrScannerProvider); + if (qrScanner != null) { + final uri = await qrScanner.scanQr(); + List creds = uri != null + ? CredentialData.fromUri(Uri.parse(uri)) + : []; + await withContext((context) async { + if (creds.isEmpty) { + showMessage(context, l10n.l_qr_not_found); + } else if (creds.length == 1) { + await showBlurDialog( + context: context, + builder: (context) => OathAddAccountPage( + devicePath, + oathState, + credentials: credentials, + credentialData: creds[0], + ), + ); + } else { + await showBlurDialog( + context: context, + builder: (context) => OathAddMultiAccountPage( + devicePath, oathState, creds, + key: migrateAccountAction), + ); + } + }); } + } else { + await showBlurDialog( + context: context, + builder: (context) => + AddAccountDialog(devicePath, oathState), + ); } } - await withContext((context) async { - await showBlurDialog( - context: context, - builder: (context) => OathAddAccountPage( - devicePath, - oathState, - credentials: credentials, - credentialData: otpauth, - ), - ); - }); - } - : null, - ), - ActionListItem( - title: l10n.s_qr_scan, - icon: const Icon(Icons.qr_code_scanner_outlined), - onTap: (context) async { - final withContext = ref.read(withContextProvider); - final credentials = ref.read(credentialsProvider); - final qrScanner = ref.watch(qrScannerProvider); - if (qrScanner != null) { - final uri = await qrScanner.scanQr(); - List creds = - uri != null ? CredentialData.fromUri(Uri.parse(uri)) : []; - await withContext((context) async { - if (creds.isEmpty) { - showMessage(context, l10n.l_qr_not_found); - } else if (creds.length == 1) { - await showBlurDialog( - context: context, - builder: (context) => OathAddAccountPage( - devicePath, - oathState, - credentials: credentials, - credentialData: creds[0], - ), - ); - } else { - await showBlurDialog( - context: context, - builder: (context) => OathAddMultiAccountPage( - devicePath, oathState, creds, - key: migrateAccountAction), - ); - } - }); - } - await withContext( - (context) async => Navigator.of(context).pop()); - }), + : null), ]), ActionListSection(l10n.s_manage, children: [ ActionListItem( From c82119d7260a2762954abaedf25af32701d16e70 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Wed, 16 Aug 2023 12:38:29 +0200 Subject: [PATCH 077/158] Refactor code for handling uri --- lib/app/views/main_page.dart | 12 ++++--- lib/oath/views/add_account_dialog.dart | 50 ++++---------------------- lib/oath/views/key_actions.dart | 31 ++-------------- lib/oath/views/utils.dart | 42 ++++++++++++++++++++++ 4 files changed, 58 insertions(+), 77 deletions(-) diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index 618df60c..4f3e101e 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -17,15 +17,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - import '../../android/app_methods.dart'; import '../../android/state.dart'; import '../../exception/cancellation_exception.dart'; import '../../core/state.dart'; import '../../fido/views/fido_screen.dart'; import '../../oath/models.dart'; +import '../../oath/state.dart'; import '../../oath/views/add_account_page.dart'; import '../../oath/views/oath_screen.dart'; +import '../../oath/views/utils.dart'; import '../../piv/views/piv_screen.dart'; import '../../widgets/custom_icons.dart'; import '../message.dart'; @@ -104,10 +105,11 @@ class MainPage extends ConsumerWidget { final scanner = ref.read(qrScannerProvider); if (scanner != null) { try { - final url = await scanner.scanQr(); - if (url != null) { - otpauth = CredentialData.fromOtpauth(Uri.parse(url)); - } + final uri = await scanner.scanQr(); + final withContext = ref.read(withContextProvider); + final credentials = ref.read(credentialsProvider); + handleUri( + ref, withContext, credentials, uri, null, null, l10n); } on CancellationException catch (_) { // ignored - user cancelled return; diff --git a/lib/oath/views/add_account_dialog.dart b/lib/oath/views/add_account_dialog.dart index 8ebfb775..cd5533f8 100644 --- a/lib/oath/views/add_account_dialog.dart +++ b/lib/oath/views/add_account_dialog.dart @@ -24,11 +24,10 @@ import 'package:yubico_authenticator/widgets/responsive_dialog.dart'; import '../../app/models.dart'; import '../../widgets/file_drop_target.dart'; -import '../keys.dart'; import '../models.dart'; import '../state.dart'; import 'add_account_page.dart'; -import 'add_multi_account_page.dart'; +import 'utils.dart'; class AddAccountDialog extends ConsumerStatefulWidget { final DevicePath? devicePath; @@ -59,23 +58,9 @@ class _AddAccountDialogState extends ConsumerState { if (qrScanner != null) { final b64Image = base64Encode(fileData); final uri = await qrScanner.scanQr(b64Image); - if (uri == null) { - if (!mounted) return; - showMessage(context, l10n.l_qr_not_found); - } else { - final otpauth = CredentialData.fromOtpauth(Uri.parse(uri)); - await withContext((context) async { - await showBlurDialog( - context: context, - builder: (context) => OathAddAccountPage( - widget.devicePath, - widget.state, - credentials: credentials, - credentialData: otpauth, - ), - ); - }); - } + + handleUri(ref, withContext, credentials, uri, widget.devicePath, + widget.state, l10n); } }, child: Column( @@ -95,31 +80,8 @@ class _AddAccountDialogState extends ConsumerState { Navigator.of(context).pop(); if (qrScanner != null) { final uri = await qrScanner.scanQr(); - List creds = uri != null - ? CredentialData.fromUri(Uri.parse(uri)) - : []; - await withContext((context) async { - if (creds.isEmpty) { - showMessage(context, l10n.l_qr_not_found); - } else if (creds.length == 1) { - await showBlurDialog( - context: context, - builder: (context) => OathAddAccountPage( - widget.devicePath, - widget.state, - credentials: credentials, - credentialData: creds[0], - ), - ); - } else { - await showBlurDialog( - context: context, - builder: (context) => OathAddMultiAccountPage( - widget.devicePath, widget.state, creds, - key: migrateAccountAction), - ); - } - }); + handleUri(ref, withContext, credentials, uri, + widget.devicePath, widget.state, l10n); } }, ), diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index 2d8b12f6..0354ba60 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -26,14 +26,12 @@ import '../../app/state.dart'; import '../../app/views/fs_dialog.dart'; import '../../app/views/action_list.dart'; import '../../core/state.dart'; -import '../keys.dart'; import '../models.dart'; import '../keys.dart' as keys; import '../state.dart'; -import 'add_account_page.dart'; -import 'add_multi_account_page.dart'; import 'manage_password_dialog.dart'; import 'reset_dialog.dart'; +import 'utils.dart'; Widget oathBuildActions( BuildContext context, @@ -67,31 +65,8 @@ Widget oathBuildActions( final qrScanner = ref.read(qrScannerProvider); if (qrScanner != null) { final uri = await qrScanner.scanQr(); - List creds = uri != null - ? CredentialData.fromUri(Uri.parse(uri)) - : []; - await withContext((context) async { - if (creds.isEmpty) { - showMessage(context, l10n.l_qr_not_found); - } else if (creds.length == 1) { - await showBlurDialog( - context: context, - builder: (context) => OathAddAccountPage( - devicePath, - oathState, - credentials: credentials, - credentialData: creds[0], - ), - ); - } else { - await showBlurDialog( - context: context, - builder: (context) => OathAddMultiAccountPage( - devicePath, oathState, creds, - key: migrateAccountAction), - ); - } - }); + handleUri(ref, withContext, credentials, uri, + devicePath, oathState, l10n); } } else { await showBlurDialog( diff --git a/lib/oath/views/utils.dart b/lib/oath/views/utils.dart index 131088a0..c5d8a0c2 100755 --- a/lib/oath/views/utils.dart +++ b/lib/oath/views/utils.dart @@ -16,8 +16,16 @@ import 'dart:math'; +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 '../../widgets/utf8_utils.dart'; +import '../keys.dart'; import '../models.dart'; +import 'add_account_page.dart'; +import 'add_multi_account_page.dart'; /// Calculates the available space for issuer and account name. /// @@ -53,3 +61,37 @@ String getTextName(OathCredential credential) { ? '${credential.issuer} (${credential.name})' : credential.name; } + +void handleUri( + WidgetRef ref, + final withContext, + final credentials, + String? uri, + DevicePath? devicePath, + OathState? state, + AppLocalizations l10n, +) async { + List creds = + uri != null ? CredentialData.fromUri(Uri.parse(uri)) : []; + await withContext((context) async { + if (creds.isEmpty) { + showMessage(context, l10n.l_qr_not_found); + } else if (creds.length == 1) { + await showBlurDialog( + context: context, + builder: (context) => OathAddAccountPage( + devicePath, + state, + credentials: credentials, + credentialData: creds[0], + ), + ); + } else { + await showBlurDialog( + context: context, + builder: (context) => OathAddMultiAccountPage(devicePath, state, creds, + key: migrateAccountAction), + ); + } + }); +} From 8b7032d9f65e72c5404bbdd3dd994098e4dc2f1c Mon Sep 17 00:00:00 2001 From: Joakim Troeng Date: Wed, 16 Aug 2023 12:50:55 +0200 Subject: [PATCH 078/158] Changing macos systray icon file format from .eps to .png resolves a crash on startup when loading systray in macos14. --- lib/desktop/systray.dart | 2 +- resources/icons/systray-template.png | Bin 0 -> 7734 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 resources/icons/systray-template.png diff --git a/lib/desktop/systray.dart b/lib/desktop/systray.dart index 246bc987..6ba2219f 100755 --- a/lib/desktop/systray.dart +++ b/lib/desktop/systray.dart @@ -94,7 +94,7 @@ Future _calculateCode( String _getIcon() { if (Platform.isMacOS) { - return 'resources/icons/systray-template.eps'; + return 'resources/icons/systray-template.png'; } if (Platform.isWindows) { return 'resources/icons/com.yubico.yubioath.ico'; diff --git a/resources/icons/systray-template.png b/resources/icons/systray-template.png new file mode 100644 index 0000000000000000000000000000000000000000..b8c4cafa56a75eef156cf90767ccd3a2e64afa7b GIT binary patch literal 7734 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 literal 0 HcmV?d00001 From 96639b970c1346516f6d44925a90719970fb8783 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Wed, 16 Aug 2023 13:42:15 +0200 Subject: [PATCH 079/158] Check capacity if state is not null --- lib/app/views/main_page.dart | 18 ------------------ lib/oath/views/add_multi_account_page.dart | 10 +++++++--- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index 4f3e101e..023aac59 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -22,14 +22,11 @@ import '../../android/state.dart'; import '../../exception/cancellation_exception.dart'; import '../../core/state.dart'; import '../../fido/views/fido_screen.dart'; -import '../../oath/models.dart'; import '../../oath/state.dart'; -import '../../oath/views/add_account_page.dart'; import '../../oath/views/oath_screen.dart'; import '../../oath/views/utils.dart'; import '../../piv/views/piv_screen.dart'; import '../../widgets/custom_icons.dart'; -import '../message.dart'; import '../models.dart'; import '../state.dart'; import 'device_error_screen.dart'; @@ -101,7 +98,6 @@ class MainPage extends ConsumerWidget { icon: const Icon(Icons.person_add_alt_1), tooltip: l10n.s_add_account, onPressed: () async { - CredentialData? otpauth; final scanner = ref.read(qrScannerProvider); if (scanner != null) { try { @@ -115,20 +111,6 @@ class MainPage extends ConsumerWidget { return; } } - - await ref.read(withContextProvider)((context) => showBlurDialog( - context: context, - routeSettings: - const RouteSettings(name: 'oath_add_account'), - builder: (context) { - return OathAddAccountPage( - null, - null, - credentials: null, - credentialData: otpauth, - ); - }, - )); }, ), ); diff --git a/lib/oath/views/add_multi_account_page.dart b/lib/oath/views/add_multi_account_page.dart index 2dcea2d2..ca3fdb42 100644 --- a/lib/oath/views/add_multi_account_page.dart +++ b/lib/oath/views/add_multi_account_page.dart @@ -205,9 +205,13 @@ class _OathAddMultiAccountPageState bool isValid() { final credsToAdd = _credStates.values.where((element) => element.$1).length; - int? capacity = widget.state!.version.isAtLeast(4) ? 32 : null; - return (credsToAdd > 0) && - (capacity == null || (_numCreds! + credsToAdd <= capacity)); + if (widget.state != null) { + int? capacity = widget.state!.version.isAtLeast(4) ? 32 : null; + return (credsToAdd > 0) && + (capacity == null || (_numCreds! + credsToAdd <= capacity)); + } else { + return true; + } } void submit() async { From 10d81f511f53a5166a02dc2585c3b7a26aca2b56 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 16 Aug 2023 14:57:24 +0200 Subject: [PATCH 080/158] Improve keyboard shortcuts: - Remove CTRL+W on Linux - Add support for Copy key for copying Oath codes, etc. --- lib/app/shortcuts.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/app/shortcuts.dart b/lib/app/shortcuts.dart index 4bcd8125..f29a370f 100755 --- a/lib/app/shortcuts.dart +++ b/lib/app/shortcuts.dart @@ -149,18 +149,24 @@ Widget registerGlobalShortcuts( child: Shortcuts( shortcuts: { ctrlOrCmd(LogicalKeyboardKey.keyC): const CopyIntent(), - ctrlOrCmd(LogicalKeyboardKey.keyW): const HideIntent(), + const SingleActivator(LogicalKeyboardKey.copy): const CopyIntent(), ctrlOrCmd(LogicalKeyboardKey.keyF): const SearchIntent(), if (isDesktop) ...{ const SingleActivator(LogicalKeyboardKey.tab, control: true): const NextDeviceIntent(), }, if (Platform.isMacOS) ...{ + const SingleActivator(LogicalKeyboardKey.keyW, meta: true): + const HideIntent(), const SingleActivator(LogicalKeyboardKey.keyQ, meta: true): const CloseIntent(), const SingleActivator(LogicalKeyboardKey.comma, meta: true): const SettingsIntent(), }, + if (Platform.isWindows) ...{ + const SingleActivator(LogicalKeyboardKey.keyW, control: true): + const HideIntent(), + }, if (Platform.isLinux) ...{ const SingleActivator(LogicalKeyboardKey.keyQ, control: true): const CloseIntent(), From 7cbbd054bc679045a0e84f69ca71cb4b09d5a44a Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 16 Aug 2023 15:27:31 +0200 Subject: [PATCH 081/158] otpauth-migration Android support --- android/app/src/main/AndroidManifest.xml | 1 + .../com/yubico/authenticator/MainActivity.kt | 5 ++- lib/android/oath/otp_auth_link_handler.dart | 36 +++++++++++++------ lib/app/views/main_page.dart | 3 +- lib/oath/views/add_account_dialog.dart | 4 +-- lib/oath/views/key_actions.dart | 4 +-- lib/oath/views/utils.dart | 1 - 7 files changed, 35 insertions(+), 19 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index bc27f12a..1939fb7e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -50,6 +50,7 @@ + diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt index 19f68fa7..6849160f 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt @@ -193,7 +193,10 @@ class MainActivity : FlutterFragmentActivity() { // Handle opening through otpauth:// link val intentData = intent.data - if (intentData != null && intentData.scheme == "otpauth") { + if (intentData != null && + (intentData.scheme == "otpauth" || + intentData.scheme == "otpauth-migration") + ) { intent.data = null appLinkMethodChannel.handleUri(intentData) } diff --git a/lib/android/oath/otp_auth_link_handler.dart b/lib/android/oath/otp_auth_link_handler.dart index 5c629bc4..17b5400b 100644 --- a/lib/android/oath/otp_auth_link_handler.dart +++ b/lib/android/oath/otp_auth_link_handler.dart @@ -18,10 +18,13 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../app/message.dart'; +import '../../oath/keys.dart'; import '../../oath/models.dart'; import '../../oath/views/add_account_page.dart'; +import '../../oath/views/add_multi_account_page.dart'; const _appLinkMethodsChannel = MethodChannel('app.link.methods'); @@ -31,21 +34,32 @@ void setupOtpAuthLinkHandler(BuildContext context) { switch (call.method) { case 'handleOtpAuthLink': { - var url = args['link']; - var otpauth = CredentialData.fromOtpauth(Uri.parse(url)); + final l10n = AppLocalizations.of(context)!; Navigator.popUntil(context, ModalRoute.withName('/')); - await showBlurDialog( - context: context, - routeSettings: const RouteSettings(name: 'oath_add_account'), - builder: (_) { - return OathAddAccountPage( + var uri = args['link']; + + List creds = + uri != null ? CredentialData.fromUri(Uri.parse(uri)) : []; + + if (creds.isEmpty) { + showMessage(context, l10n.l_qr_not_found); + } else if (creds.length == 1) { + await showBlurDialog( + context: context, + builder: (context) => OathAddAccountPage( null, null, credentials: null, - credentialData: otpauth, - ); - }, - ); + credentialData: creds[0], + ), + ); + } else { + await showBlurDialog( + context: context, + builder: (context) => OathAddMultiAccountPage(null, null, creds, + key: migrateAccountAction), + ); + } break; } default: diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index 023aac59..9a98d60c 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -104,8 +104,7 @@ class MainPage extends ConsumerWidget { final uri = await scanner.scanQr(); final withContext = ref.read(withContextProvider); final credentials = ref.read(credentialsProvider); - handleUri( - ref, withContext, credentials, uri, null, null, l10n); + handleUri(withContext, credentials, uri, null, null, l10n); } on CancellationException catch (_) { // ignored - user cancelled return; diff --git a/lib/oath/views/add_account_dialog.dart b/lib/oath/views/add_account_dialog.dart index cd5533f8..bba0ec45 100644 --- a/lib/oath/views/add_account_dialog.dart +++ b/lib/oath/views/add_account_dialog.dart @@ -59,7 +59,7 @@ class _AddAccountDialogState extends ConsumerState { final b64Image = base64Encode(fileData); final uri = await qrScanner.scanQr(b64Image); - handleUri(ref, withContext, credentials, uri, widget.devicePath, + handleUri(withContext, credentials, uri, widget.devicePath, widget.state, l10n); } }, @@ -80,7 +80,7 @@ class _AddAccountDialogState extends ConsumerState { Navigator.of(context).pop(); if (qrScanner != null) { final uri = await qrScanner.scanQr(); - handleUri(ref, withContext, credentials, uri, + handleUri(withContext, credentials, uri, widget.devicePath, widget.state, l10n); } }, diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index 0354ba60..add11e62 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -65,8 +65,8 @@ Widget oathBuildActions( final qrScanner = ref.read(qrScannerProvider); if (qrScanner != null) { final uri = await qrScanner.scanQr(); - handleUri(ref, withContext, credentials, uri, - devicePath, oathState, l10n); + handleUri(withContext, credentials, uri, devicePath, + oathState, l10n); } } else { await showBlurDialog( diff --git a/lib/oath/views/utils.dart b/lib/oath/views/utils.dart index c5d8a0c2..376aaab6 100755 --- a/lib/oath/views/utils.dart +++ b/lib/oath/views/utils.dart @@ -63,7 +63,6 @@ String getTextName(OathCredential credential) { } void handleUri( - WidgetRef ref, final withContext, final credentials, String? uri, From fe77610d596d1266526043961a38319fd0738545 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Wed, 16 Aug 2023 15:33:49 +0200 Subject: [PATCH 082/158] Remove unused import --- lib/oath/views/utils.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/oath/views/utils.dart b/lib/oath/views/utils.dart index 376aaab6..d655b0ab 100755 --- a/lib/oath/views/utils.dart +++ b/lib/oath/views/utils.dart @@ -16,7 +16,6 @@ import 'dart:math'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../app/message.dart'; From 48679705136b64ce2b25b88ff43410fc8eb7be9a Mon Sep 17 00:00:00 2001 From: Joakim Troeng Date: Thu, 17 Aug 2023 10:56:01 +0200 Subject: [PATCH 083/158] fixing issue of color inversion --- lib/desktop/systray.dart | 2 +- resources/icons/systray-template-inv.png | Bin 0 -> 7665 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 resources/icons/systray-template-inv.png diff --git a/lib/desktop/systray.dart b/lib/desktop/systray.dart index 6ba2219f..e2de966a 100755 --- a/lib/desktop/systray.dart +++ b/lib/desktop/systray.dart @@ -94,7 +94,7 @@ Future _calculateCode( String _getIcon() { if (Platform.isMacOS) { - return 'resources/icons/systray-template.png'; + return 'resources/icons/systray-template-inv.png'; } if (Platform.isWindows) { return 'resources/icons/com.yubico.yubioath.ico'; diff --git a/resources/icons/systray-template-inv.png b/resources/icons/systray-template-inv.png new file mode 100644 index 0000000000000000000000000000000000000000..e2faa8b343e6d0878550d6ac3be1f5d5e42abee6 GIT binary patch literal 7665 zcma)Bd0dQL`#&?y)XcO`gJ>Jg(1I2zsboemlPnbyPclL(A=Oi%bT?5GEqAsjYePMj z#2Z3cG9?K~S)(vi$Rkv?tiS6_c^{AW{rx__KlpJ(O`%D#Svj(vc zLfT$pPd|jzmcZXO1{FqhGi`4pL|GT+;o;-u;lYiJS{@P>u?Qi{oWz{5;tK-fi}R*m zwC>l>Rpz|#z|{Vp6RNxp`K)E_@ejRS(R#x+^afc_>dZPq*BICTkd95=X_r!#RH-35 zv|-cU4VyonFQ_-alhU&0?3oXxt2G$6&2JW+v=`i5>Y%@2{lT||jh*Y!s)ls;+M3#t zPx)3^TU-7zbuWK7A>RM5=%lP=jtlG+7VPUIFO;N@mhE?1n$qpQLe{ou%g3GZw+b}b z&wwRU0fZfDQoPqznz1#cbg_{8f;abf#r+X6TLG0RunS-;5tyhYM{ zyZnC*kKXlEFF2rz&6oN3Or%U`l+_n*3LY%{+|d7<@UCr+_uyHS3}a18Vam%-72lO{ zr~G4so4r{Rliv@x)MKdb5}Q2d;)?QYll`&Ok9o<%&ONrBncP0@#xU&>3ADEnY6I?S zPr0XN6GtRlcsMPZ)pe6|Y_{@iM!A*t+eg&7OM;faOWf&>yk1zZQ8PBC^2S^VTHg6e z5WW2S?t>pbs3rMF72TO>@CEjT87ZC}4cjvx|4|N%+aCsl8Zll|CTKik=re|n4!6r3 ziICBEFHhl&`1>6X?!`WE-YO7w+I;

tN<)<;XruTahZe^rA3gM^!Q9N?iwiRpW=R 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 f%pH`I@J-G3 Date: Thu, 17 Aug 2023 16:51:57 +0200 Subject: [PATCH 084/158] OATH: Remove Scan QR button from manual entry. --- lib/oath/views/add_account_page.dart | 102 ++++++--------------------- 1 file changed, 22 insertions(+), 80 deletions(-) diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index 4d272ab3..ac19d0e3 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -51,8 +51,6 @@ final _log = Logger('oath.view.add_account_page'); final _secretFormatterPattern = RegExp('[abcdefghijklmnopqrstuvwxyz234567 ]', caseSensitive: false); -enum _QrScanState { none, scanning, success, failed } - class OathAddAccountPage extends ConsumerStatefulWidget { final DevicePath? devicePath; final OathState? state; @@ -84,7 +82,7 @@ class _OathAddAccountPageState extends ConsumerState { int _digits = defaultDigits; int _counter = defaultCounter; bool _validateSecretLength = false; - _QrScanState _qrState = _QrScanState.none; + bool _dataLoaded = false; bool _isObscure = true; List _periodValues = [20, 30, 45, 60]; List _digitsValues = [6, 8]; @@ -108,55 +106,6 @@ class _OathAddAccountPageState extends ConsumerState { } } - _scanQrCode(QrScanner qrScanner) async { - final l10n = AppLocalizations.of(context)!; - try { - setState(() { - // If we have a previous scan result stored, clear it - if (_qrState == _QrScanState.success) { - _issuerController.text = ''; - _accountController.text = ''; - _secretController.text = ''; - _oathType = defaultOathType; - _hashAlgorithm = defaultHashAlgorithm; - _periodController.text = '$defaultPeriod'; - _digits = defaultDigits; - } - _qrState = _QrScanState.scanning; - }); - final otpauth = await qrScanner.scanQr(); - if (otpauth == null) { - if (!mounted) return; - showMessage(context, l10n.l_qr_not_found); - setState(() { - _qrState = _QrScanState.failed; - }); - } else { - final data = CredentialData.fromOtpauth(Uri.parse(otpauth)); - _loadCredentialData(data); - } - } catch (e) { - final String errorMessage; - // TODO: Make this cleaner than importing desktop specific RpcError. - if (e is RpcError) { - errorMessage = e.message; - } else { - errorMessage = e.toString(); - } - - if (e is! CancellationException) { - showMessage( - context, - l10n.l_qr_not_read(errorMessage), - duration: const Duration(seconds: 4), - ); - } - setState(() { - _qrState = _QrScanState.failed; - }); - } - } - _loadCredentialData(CredentialData data) { setState(() { _issuerController.text = data.issuer?.trim() ?? ''; @@ -170,7 +119,7 @@ class _OathAddAccountPageState extends ConsumerState { _digits = data.digits; _counter = data.counter; _isObscure = true; - _qrState = _QrScanState.success; + _dataLoaded = true; }); } @@ -305,8 +254,6 @@ class _OathAddAccountPageState extends ConsumerState { nameRemaining >= 0 && period > 0; - final qrScanner = ref.watch(qrScannerProvider); - final hashAlgorithms = HashAlgorithm.values .where((alg) => alg != HashAlgorithm.sha512 || @@ -370,6 +317,7 @@ class _OathAddAccountPageState extends ConsumerState { ], child: FileDropTarget( onFileDropped: (fileData) async { + final qrScanner = ref.read(qrScannerProvider); if (qrScanner != null) { final b64Image = base64Encode(fileData); final otpauth = await qrScanner.scanQr(b64Image); @@ -377,8 +325,20 @@ class _OathAddAccountPageState extends ConsumerState { if (!mounted) return; showMessage(context, l10n.l_qr_not_found); } else { - final data = CredentialData.fromOtpauth(Uri.parse(otpauth)); - _loadCredentialData(data); + try { + final data = CredentialData.fromOtpauth(Uri.parse(otpauth)); + _loadCredentialData(data); + } catch (e) { + final String errorMessage; + // TODO: Make this cleaner than importing desktop specific RpcError. + if (e is RpcError) { + errorMessage = e.message; + } else { + errorMessage = e.toString(); + } + if (!mounted) return; + showMessage(context, errorMessage); + } } } }, @@ -485,7 +445,7 @@ class _OathAddAccountPageState extends ConsumerState { errorText: _validateSecretLength && !secretLengthValid ? l10n.s_invalid_length : null), - readOnly: _qrState == _QrScanState.success, + readOnly: _dataLoaded, textInputAction: TextInputAction.done, onChanged: (value) { setState(() { @@ -496,24 +456,6 @@ class _OathAddAccountPageState extends ConsumerState { if (isValid) submit(); }, ), - if (isDesktop && qrScanner != null) - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: ActionChip( - avatar: _qrState != _QrScanState.scanning - ? (_qrState == _QrScanState.success - ? const Icon(Icons.qr_code) - : const Icon( - Icons.qr_code_scanner_outlined)) - : const CircularProgressIndicator( - strokeWidth: 2.0), - label: _qrState == _QrScanState.success - ? Text(l10n.l_qr_scanned) - : Text(l10n.s_qr_scan), - onPressed: () { - _scanQrCode(qrScanner); - }), - ), const Divider(), Wrap( crossAxisAlignment: WrapCrossAlignment.center, @@ -536,7 +478,7 @@ class _OathAddAccountPageState extends ConsumerState { selected: _oathType != defaultOathType, itemBuilder: (value) => Text(value.getDisplayName(l10n)), - onChanged: _qrState != _QrScanState.success + onChanged: !_dataLoaded ? (value) { setState(() { _oathType = value; @@ -549,7 +491,7 @@ class _OathAddAccountPageState extends ConsumerState { value: _hashAlgorithm, selected: _hashAlgorithm != defaultHashAlgorithm, itemBuilder: (value) => Text(value.displayName), - onChanged: _qrState != _QrScanState.success + onChanged: !_dataLoaded ? (value) { setState(() { _hashAlgorithm = value; @@ -566,7 +508,7 @@ class _OathAddAccountPageState extends ConsumerState { defaultPeriod, itemBuilder: ((value) => Text(l10n.s_num_sec(value))), - onChanged: _qrState != _QrScanState.success + onChanged: !_dataLoaded ? (period) { setState(() { _periodController.text = '$period'; @@ -580,7 +522,7 @@ class _OathAddAccountPageState extends ConsumerState { selected: _digits != defaultDigits, itemBuilder: (value) => Text(l10n.s_num_digits(value)), - onChanged: _qrState != _QrScanState.success + onChanged: !_dataLoaded ? (digits) { setState(() { _digits = digits; From 916d9e24db177947b3ffc7bf725f8cf2dd5cf20a Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 18 Aug 2023 09:43:13 +0200 Subject: [PATCH 085/158] Minor cleanups. --- lib/android/oath/otp_auth_link_handler.dart | 40 +++--------------- lib/app/views/main_page.dart | 5 ++- lib/oath/views/add_account_dialog.dart | 14 +++--- lib/oath/views/add_multi_account_page.dart | 14 ++---- lib/oath/views/key_actions.dart | 4 +- lib/oath/views/utils.dart | 47 ++++++++++----------- 6 files changed, 47 insertions(+), 77 deletions(-) diff --git a/lib/android/oath/otp_auth_link_handler.dart b/lib/android/oath/otp_auth_link_handler.dart index 17b5400b..260747b7 100644 --- a/lib/android/oath/otp_auth_link_handler.dart +++ b/lib/android/oath/otp_auth_link_handler.dart @@ -20,11 +20,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../../app/message.dart'; -import '../../oath/keys.dart'; -import '../../oath/models.dart'; -import '../../oath/views/add_account_page.dart'; -import '../../oath/views/add_multi_account_page.dart'; +import '../../oath/views/utils.dart'; const _appLinkMethodsChannel = MethodChannel('app.link.methods'); @@ -33,35 +29,11 @@ void setupOtpAuthLinkHandler(BuildContext context) { final args = jsonDecode(call.arguments); switch (call.method) { case 'handleOtpAuthLink': - { - final l10n = AppLocalizations.of(context)!; - Navigator.popUntil(context, ModalRoute.withName('/')); - var uri = args['link']; - - List creds = - uri != null ? CredentialData.fromUri(Uri.parse(uri)) : []; - - if (creds.isEmpty) { - showMessage(context, l10n.l_qr_not_found); - } else if (creds.length == 1) { - await showBlurDialog( - context: context, - builder: (context) => OathAddAccountPage( - null, - null, - credentials: null, - credentialData: creds[0], - ), - ); - } else { - await showBlurDialog( - context: context, - builder: (context) => OathAddMultiAccountPage(null, null, creds, - key: migrateAccountAction), - ); - } - break; - } + Navigator.popUntil(context, ModalRoute.withName('/')); + final l10n = AppLocalizations.of(context)!; + final uri = args['link']; + await handleUri(context, null, uri, null, null, l10n); + break; default: throw PlatformException( code: 'NotImplemented', diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index 9a98d60c..dd83bfec 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -102,9 +102,10 @@ class MainPage extends ConsumerWidget { if (scanner != null) { try { final uri = await scanner.scanQr(); - final withContext = ref.read(withContextProvider); final credentials = ref.read(credentialsProvider); - handleUri(withContext, credentials, uri, null, null, l10n); + final withContext = ref.read(withContextProvider); + await withContext((context) => + handleUri(context, credentials, uri, null, null, l10n)); } on CancellationException catch (_) { // ignored - user cancelled return; diff --git a/lib/oath/views/add_account_dialog.dart b/lib/oath/views/add_account_dialog.dart index bba0ec45..df714843 100644 --- a/lib/oath/views/add_account_dialog.dart +++ b/lib/oath/views/add_account_dialog.dart @@ -58,9 +58,8 @@ class _AddAccountDialogState extends ConsumerState { if (qrScanner != null) { final b64Image = base64Encode(fileData); final uri = await qrScanner.scanQr(b64Image); - - handleUri(withContext, credentials, uri, widget.devicePath, - widget.state, l10n); + await withContext((context) => handleUri(context, credentials, + uri, widget.devicePath, widget.state, l10n)); } }, child: Column( @@ -80,8 +79,13 @@ class _AddAccountDialogState extends ConsumerState { Navigator.of(context).pop(); if (qrScanner != null) { final uri = await qrScanner.scanQr(); - handleUri(withContext, credentials, uri, - widget.devicePath, widget.state, l10n); + await withContext((context) => handleUri( + context, + credentials, + uri, + widget.devicePath, + widget.state, + l10n)); } }, ), diff --git a/lib/oath/views/add_multi_account_page.dart b/lib/oath/views/add_multi_account_page.dart index ca3fdb42..12577b49 100644 --- a/lib/oath/views/add_multi_account_page.dart +++ b/lib/oath/views/add_multi_account_page.dart @@ -172,14 +172,7 @@ class _OathAddMultiAccountPageState )); } - bool isTouchSupported() { - bool touch = true; - if (!(widget.state?.version.isAtLeast(4, 2) ?? true)) { - // Touch not supported - touch = false; - } - return touch; - } + bool isTouchSupported() => widget.state?.version.isAtLeast(4, 2) ?? true; void checkForDuplicates() { for (final item in _credStates.entries) { @@ -204,9 +197,10 @@ class _OathAddMultiAccountPageState } bool isValid() { - final credsToAdd = _credStates.values.where((element) => element.$1).length; if (widget.state != null) { - int? capacity = widget.state!.version.isAtLeast(4) ? 32 : null; + final credsToAdd = + _credStates.values.where((element) => element.$1).length; + final capacity = widget.state!.version.isAtLeast(4) ? 32 : null; return (credsToAdd > 0) && (capacity == null || (_numCreds! + credsToAdd <= capacity)); } else { diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index add11e62..003a05ba 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -65,8 +65,8 @@ Widget oathBuildActions( final qrScanner = ref.read(qrScannerProvider); if (qrScanner != null) { final uri = await qrScanner.scanQr(); - handleUri(withContext, credentials, uri, devicePath, - oathState, l10n); + await withContext((context) => handleUri(context, + credentials, uri, devicePath, oathState, l10n)); } } else { await showBlurDialog( diff --git a/lib/oath/views/utils.dart b/lib/oath/views/utils.dart index d655b0ab..ae88fa22 100755 --- a/lib/oath/views/utils.dart +++ b/lib/oath/views/utils.dart @@ -16,6 +16,7 @@ import 'dart:math'; +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../app/message.dart'; @@ -61,9 +62,9 @@ String getTextName(OathCredential credential) { : credential.name; } -void handleUri( - final withContext, - final credentials, +Future handleUri( + BuildContext context, + List? credentials, String? uri, DevicePath? devicePath, OathState? state, @@ -71,25 +72,23 @@ void handleUri( ) async { List creds = uri != null ? CredentialData.fromUri(Uri.parse(uri)) : []; - await withContext((context) async { - if (creds.isEmpty) { - showMessage(context, l10n.l_qr_not_found); - } else if (creds.length == 1) { - await showBlurDialog( - context: context, - builder: (context) => OathAddAccountPage( - devicePath, - state, - credentials: credentials, - credentialData: creds[0], - ), - ); - } else { - await showBlurDialog( - context: context, - builder: (context) => OathAddMultiAccountPage(devicePath, state, creds, - key: migrateAccountAction), - ); - } - }); + if (creds.isEmpty) { + showMessage(context, l10n.l_qr_not_found); + } else if (creds.length == 1) { + await showBlurDialog( + context: context, + builder: (context) => OathAddAccountPage( + devicePath, + state, + credentials: credentials, + credentialData: creds[0], + ), + ); + } else { + await showBlurDialog( + context: context, + builder: (context) => OathAddMultiAccountPage(devicePath, state, creds, + key: migrateAccountAction), + ); + } } From f129d415a4745d21bfc285cad56d34a5b4a0f2b4 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 18 Aug 2023 10:44:08 +0200 Subject: [PATCH 086/158] Replace divider with space. --- lib/oath/views/add_account_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index ac19d0e3..0bbf8385 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -456,7 +456,7 @@ class _OathAddAccountPageState extends ConsumerState { if (isValid) submit(); }, ), - const Divider(), + const SizedBox(height: 8), Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 4.0, From 859ad926c4784d8ab50fb81b56eda237299e0d88 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 18 Aug 2023 10:44:24 +0200 Subject: [PATCH 087/158] Simplify QR URI validation. --- lib/android/qr_scanner/qr_scanner_view.dart | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/lib/android/qr_scanner/qr_scanner_view.dart b/lib/android/qr_scanner/qr_scanner_view.dart index 68b2018f..56ca3db2 100755 --- a/lib/android/qr_scanner/qr_scanner_view.dart +++ b/lib/android/qr_scanner/qr_scanner_view.dart @@ -70,7 +70,8 @@ class _QrScannerViewState extends State { setState(() { if (qrCodeData.isNotEmpty) { try { - _validateQrCodeUri(Uri.parse(qrCodeData)); // throws ArgumentError if validation fails + CredentialData.fromUri(Uri.parse( + qrCodeData)); // throws ArgumentError if validation fails _scannedString = qrCodeData; _status = ScanStatus.success; @@ -92,18 +93,6 @@ class _QrScannerViewState extends State { }); } - void _validateQrCodeUri(Uri qrCodeUri) { - try { - CredentialData.fromUri(qrCodeUri); - } on ArgumentError catch (_) { - try { - CredentialData.fromMigration(qrCodeUri); - } on ArgumentError catch (_) { - throw ArgumentError(); - } - } - } - @override void initState() { super.initState(); From 61f05b1ea528ffeea017dc859e04be5be38e83ca Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 18 Aug 2023 11:34:15 +0200 Subject: [PATCH 088/158] Fix OATH manual entry on Android and improve failure to scan handling. --- lib/app/views/main_page.dart | 27 +++++++++++++++----- lib/oath/views/add_account_dialog.dart | 35 +++++++++++++++++--------- lib/oath/views/key_actions.dart | 29 ++++++++++++++++++--- lib/oath/views/utils.dart | 5 ++-- 4 files changed, 72 insertions(+), 24 deletions(-) diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index dd83bfec..64a3635e 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -22,11 +22,12 @@ import '../../android/state.dart'; import '../../exception/cancellation_exception.dart'; import '../../core/state.dart'; import '../../fido/views/fido_screen.dart'; -import '../../oath/state.dart'; +import '../../oath/views/add_account_page.dart'; import '../../oath/views/oath_screen.dart'; import '../../oath/views/utils.dart'; import '../../piv/views/piv_screen.dart'; import '../../widgets/custom_icons.dart'; +import '../message.dart'; import '../models.dart'; import '../state.dart'; import 'device_error_screen.dart'; @@ -98,19 +99,33 @@ class MainPage extends ConsumerWidget { icon: const Icon(Icons.person_add_alt_1), tooltip: l10n.s_add_account, onPressed: () async { + final withContext = ref.read(withContextProvider); final scanner = ref.read(qrScannerProvider); if (scanner != null) { try { - final uri = await scanner.scanQr(); - final credentials = ref.read(credentialsProvider); - final withContext = ref.read(withContextProvider); - await withContext((context) => - handleUri(context, credentials, uri, null, null, l10n)); + final qrData = await scanner.scanQr(); + if (qrData != null) { + await withContext((context) => + handleUri(context, null, qrData, null, null, l10n)); + return; + } } on CancellationException catch (_) { // ignored - user cancelled return; } } + await withContext((context) => showBlurDialog( + context: context, + routeSettings: + const RouteSettings(name: 'oath_add_account'), + builder: (context) { + return const OathAddAccountPage( + null, + null, + credentials: null, + ); + }, + )); }, ), ); diff --git a/lib/oath/views/add_account_dialog.dart b/lib/oath/views/add_account_dialog.dart index df714843..17e37b54 100644 --- a/lib/oath/views/add_account_dialog.dart +++ b/lib/oath/views/add_account_dialog.dart @@ -57,9 +57,17 @@ class _AddAccountDialogState extends ConsumerState { Navigator.of(context).pop(); if (qrScanner != null) { final b64Image = base64Encode(fileData); - final uri = await qrScanner.scanQr(b64Image); - await withContext((context) => handleUri(context, credentials, - uri, widget.devicePath, widget.state, l10n)); + final qrData = await qrScanner.scanQr(b64Image); + await withContext( + (context) async { + if (qrData != null) { + await handleUri(context, credentials, qrData, + widget.devicePath, widget.state, l10n); + } else { + showMessage(context, l10n.l_qr_not_found); + } + }, + ); } }, child: Column( @@ -76,16 +84,19 @@ class _AddAccountDialogState extends ConsumerState { avatar: const Icon(Icons.qr_code_scanner_outlined), label: Text(l10n.s_qr_scan), onPressed: () async { - Navigator.of(context).pop(); if (qrScanner != null) { - final uri = await qrScanner.scanQr(); - await withContext((context) => handleUri( - context, - credentials, - uri, - widget.devicePath, - widget.state, - l10n)); + final qrData = await qrScanner.scanQr(); + await withContext( + (context) async { + if (qrData != null) { + Navigator.of(context).pop(); + await handleUri(context, credentials, qrData, + widget.devicePath, widget.state, l10n); + } else { + showMessage(context, l10n.l_qr_not_found); + } + }, + ); } }, ), diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index 003a05ba..09cf0dac 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -29,6 +29,7 @@ import '../../core/state.dart'; import '../models.dart'; import '../keys.dart' as keys; import '../state.dart'; +import 'add_account_page.dart'; import 'manage_password_dialog.dart'; import 'reset_dialog.dart'; import 'utils.dart'; @@ -64,10 +65,32 @@ Widget oathBuildActions( if (isAndroid) { final qrScanner = ref.read(qrScannerProvider); if (qrScanner != null) { - final uri = await qrScanner.scanQr(); - await withContext((context) => handleUri(context, - credentials, uri, devicePath, oathState, l10n)); + final qrData = await qrScanner.scanQr(); + if (qrData != null) { + await withContext((context) => handleUri( + context, + credentials, + qrData, + devicePath, + oathState, + l10n, + )); + return; + } } + await withContext((context) => showBlurDialog( + context: context, + routeSettings: + const RouteSettings(name: 'oath_add_account'), + builder: (context) { + return OathAddAccountPage( + devicePath, + oathState, + credentials: credentials, + credentialData: null, + ); + }, + )); } else { await showBlurDialog( context: context, diff --git a/lib/oath/views/utils.dart b/lib/oath/views/utils.dart index ae88fa22..cc2a6952 100755 --- a/lib/oath/views/utils.dart +++ b/lib/oath/views/utils.dart @@ -65,13 +65,12 @@ String getTextName(OathCredential credential) { Future handleUri( BuildContext context, List? credentials, - String? uri, + String qrData, DevicePath? devicePath, OathState? state, AppLocalizations l10n, ) async { - List creds = - uri != null ? CredentialData.fromUri(Uri.parse(uri)) : []; + List creds = CredentialData.fromUri(Uri.parse(qrData)); if (creds.isEmpty) { showMessage(context, l10n.l_qr_not_found); } else if (creds.length == 1) { From 5ad56952791ccc4a6d6102b4428a615d0c6d9122 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 18 Aug 2023 14:54:55 +0200 Subject: [PATCH 089/158] Combine OATH rename dialogs. --- lib/oath/views/account_dialog.dart | 17 +- lib/oath/views/account_view.dart | 16 +- lib/oath/views/add_multi_account_page.dart | 47 +++-- lib/oath/views/rename_account_dialog.dart | 159 +++++++++------- lib/oath/views/rename_list_account.dart | 207 --------------------- 5 files changed, 144 insertions(+), 302 deletions(-) delete mode 100644 lib/oath/views/rename_list_account.dart diff --git a/lib/oath/views/account_dialog.dart b/lib/oath/views/account_dialog.dart index 963994d0..3446a55b 100755 --- a/lib/oath/views/account_dialog.dart +++ b/lib/oath/views/account_dialog.dart @@ -59,15 +59,16 @@ class AccountDialog extends ConsumerWidget { EditIntent: CallbackAction(onInvoke: (_) async { final credentials = ref.read(credentialsProvider); final withContext = ref.read(withContextProvider); - final OathCredential? renamed = + final renamed = await withContext((context) async => await showBlurDialog( - context: context, - builder: (context) => RenameAccountDialog( - node, - credential, - credentials, - ), - )); + context: context, + builder: (context) => RenameAccountDialog.forOathCredential( + ref, + node, + credential, + credentials?.map((e) => (e.issuer, e.name)).toList() ?? + [], + ))); if (renamed != null) { // Replace the dialog with the renamed credential await withContext((context) async { diff --git a/lib/oath/views/account_view.dart b/lib/oath/views/account_view.dart index 6801e168..8beed7dc 100755 --- a/lib/oath/views/account_view.dart +++ b/lib/oath/views/account_view.dart @@ -96,12 +96,16 @@ class _AccountViewState extends ConsumerState { EditIntent: CallbackAction(onInvoke: (_) async { final node = ref.read(currentDeviceProvider)!; final credentials = ref.read(credentialsProvider); - return await ref.read(withContextProvider)( - (context) async => await showBlurDialog( - context: context, - builder: (context) => - RenameAccountDialog(node, credential, credentials), - )); + final withContext = ref.read(withContextProvider); + return await withContext((context) async => await showBlurDialog( + context: context, + builder: (context) => RenameAccountDialog.forOathCredential( + ref, + node, + credential, + credentials?.map((e) => (e.issuer, e.name)).toList() ?? [], + ), + )); }), DeleteIntent: CallbackAction(onInvoke: (_) async { final node = ref.read(currentDeviceProvider)!; diff --git a/lib/oath/views/add_multi_account_page.dart b/lib/oath/views/add_multi_account_page.dart index 12577b49..08280c34 100644 --- a/lib/oath/views/add_multi_account_page.dart +++ b/lib/oath/views/add_multi_account_page.dart @@ -18,7 +18,7 @@ import '../state.dart'; import '../../app/message.dart'; import '../../exception/cancellation_exception.dart'; -import 'rename_list_account.dart'; +import 'rename_account_dialog.dart'; final _log = Logger('oath.views.list_screen'); @@ -91,6 +91,7 @@ class _OathAddMultiAccountPageState secondary: Row(mainAxisSize: MainAxisSize.min, children: [ if (isTouchSupported()) IconButton( + tooltip: l10n.s_require_touch, color: touch ? colorScheme.primary : null, onPressed: unique ? () { @@ -100,30 +101,46 @@ class _OathAddMultiAccountPageState }); } : null, - icon: const Icon(Icons.touch_app_outlined)), + icon: Icon(touch + ? Icons.touch_app + : Icons.touch_app_outlined)), IconButton( + tooltip: l10n.s_rename_account, onPressed: () async { final node = ref .read(currentDeviceDataProvider) .valueOrNull ?.node; final withContext = ref.read(withContextProvider); - CredentialData renamed = await withContext( + CredentialData? renamed = await withContext( (context) async => await showBlurDialog( context: context, - builder: (context) => RenameList(node!, cred, - widget.credentialsFromUri, _credentials), + builder: (context) => RenameAccountDialog( + device: node!, + issuer: cred.issuer, + name: cred.name, + oathType: cred.oathType, + period: cred.period, + existing: (widget.credentialsFromUri ?? []) + .map((e) => (e.issuer, e.name)) + .followedBy((_credentials ?? []) + .map((e) => (e.issuer, e.name))) + .toList(), + rename: (issuer, name) async => cred + .copyWith(issuer: issuer, name: name), + ), )); - - setState(() { - int index = widget.credentialsFromUri!.indexWhere( - (element) => - element.name == cred.name && - (element.issuer == cred.issuer)); - widget.credentialsFromUri![index] = renamed; - _credStates.remove(cred); - _credStates[renamed] = (true, touch, true); - }); + if (renamed != null) { + setState(() { + int index = widget.credentialsFromUri!.indexWhere( + (element) => + element.name == cred.name && + (element.issuer == cred.issuer)); + widget.credentialsFromUri![index] = renamed; + _credStates.remove(cred); + _credStates[renamed] = (true, touch, true); + }); + } }, icon: IconTheme( data: IconTheme.of(context), diff --git a/lib/oath/views/rename_account_dialog.dart b/lib/oath/views/rename_account_dialog.dart index 17cb6432..9d0fede4 100755 --- a/lib/oath/views/rename_account_dialog.dart +++ b/lib/oath/views/rename_account_dialog.dart @@ -22,6 +22,7 @@ import 'package:logging/logging.dart'; import '../../app/logging.dart'; import '../../app/message.dart'; import '../../app/models.dart'; +import '../../app/state.dart'; import '../../exception/cancellation_exception.dart'; import '../../desktop/models.dart'; import '../../widgets/focus_utils.dart'; @@ -36,97 +37,121 @@ final _log = Logger('oath.view.rename_account_dialog'); class RenameAccountDialog extends ConsumerStatefulWidget { final DeviceNode device; - final OathCredential credential; - final List? credentials; + final String? issuer; + final String name; + final OathType oathType; + final int period; + final List<(String? issuer, String name)> existing; + final Future Function(String? issuer, String name) rename; - const RenameAccountDialog(this.device, this.credential, this.credentials, - {super.key}); + const RenameAccountDialog({ + required this.device, + required this.issuer, + required this.name, + required this.oathType, + this.period = defaultPeriod, + this.existing = const [], + required this.rename, + super.key, + }); @override ConsumerState createState() => _RenameAccountDialogState(); + + factory RenameAccountDialog.forOathCredential( + WidgetRef ref, + DeviceNode device, + OathCredential credential, + List<(String? issuer, String name)> existing) { + return RenameAccountDialog( + device: device, + issuer: credential.issuer, + name: credential.name, + oathType: credential.oathType, + period: credential.period, + existing: existing, + rename: (issuer, name) async { + final withContext = ref.read(withContextProvider); + try { + // Rename credentials + final renamed = await ref + .read(credentialListProvider(device.path).notifier) + .renameAccount(credential, issuer, name); + + // Update favorite + ref + .read(favoritesProvider.notifier) + .renameCredential(credential.id, renamed.id); + + await withContext((context) async => showMessage( + context, AppLocalizations.of(context)!.s_account_renamed)); + return renamed; + } on CancellationException catch (_) { + // ignored + } catch (e) { + _log.error('Failed to add account', e); + final String errorMessage; + // TODO: Make this cleaner than importing desktop specific RpcError. + if (e is RpcError) { + errorMessage = e.message; + } else { + errorMessage = e.toString(); + } + await withContext((context) async => showMessage( + context, + AppLocalizations.of(context)! + .l_account_add_failed(errorMessage), + duration: const Duration(seconds: 4), + )); + return null; + } + }, + ); + } } class _RenameAccountDialogState extends ConsumerState { late String _issuer; - late String _account; + late String _name; @override void initState() { super.initState(); - _issuer = widget.credential.issuer?.trim() ?? ''; - _account = widget.credential.name.trim(); + _issuer = widget.issuer?.trim() ?? ''; + _name = widget.name.trim(); } void _submit() async { - final l10n = AppLocalizations.of(context)!; - try { - - FocusUtils.unfocus(context); - - // Rename credentials - final renamed = await ref - .read(credentialListProvider(widget.device.path).notifier) - .renameAccount( - widget.credential, _issuer.isNotEmpty ? _issuer : null, _account); - - // Update favorite - ref - .read(favoritesProvider.notifier) - .renameCredential(widget.credential.id, renamed.id); - - if (!mounted) return; - Navigator.of(context).pop(renamed); - showMessage(context, l10n.s_account_renamed); - } on CancellationException catch (_) { - // ignored - } catch (e) { - _log.error('Failed to add account', e); - 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_account_add_failed(errorMessage), - duration: const Duration(seconds: 4), - ); - } + FocusUtils.unfocus(context); + final nav = Navigator.of(context); + final renamed = + await widget.rename(_issuer.isNotEmpty ? _issuer : null, _name); + nav.pop(renamed); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final credential = widget.credential; final (issuerRemaining, nameRemaining) = getRemainingKeySpace( - oathType: credential.oathType, - period: credential.period, + oathType: widget.oathType, + period: widget.period, issuer: _issuer, - name: _account, + name: _name, ); - // is this credentials name/issuer pair different from all other? - final isUnique = widget.credentials - ?.where((element) => - element != credential && - element.name == _account && - (element.issuer ?? '') == _issuer) - .isEmpty ?? - false; + // are the name/issuer values different from original + final didChange = (widget.issuer ?? '') != _issuer || widget.name != _name; + + // is this credentials name/issuer pair different from all other, or initial value? + final isUnique = !widget.existing.contains((_issuer, _name)) || !didChange; // is this credential name/issuer of valid format - final isValidFormat = _account.isNotEmpty; - - // are the name/issuer values different from original - final didChange = (widget.credential.issuer ?? '') != _issuer || - widget.credential.name != _account; + final nameNotEmpty = _name.isNotEmpty; // can we rename with the new values - final isValid = isUnique && isValidFormat; + final isValid = isUnique && nameNotEmpty; return ResponsiveDialog( title: Text(l10n.s_rename_account), @@ -142,7 +167,9 @@ class _RenameAccountDialogState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.q_rename_target(getTextName(credential))), + Text(l10n.q_rename_target(widget.issuer != null + ? '${widget.issuer} (${widget.name})' + : widget.name)), Text(l10n.p_rename_will_change_account_displayed), TextFormField( initialValue: _issuer, @@ -165,16 +192,16 @@ class _RenameAccountDialogState extends ConsumerState { }, ), TextFormField( - initialValue: _account, + initialValue: _name, maxLength: nameRemaining, inputFormatters: [limitBytesLength(nameRemaining)], - buildCounter: buildByteCounterFor(_account), + buildCounter: buildByteCounterFor(_name), key: keys.nameField, decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_account_name, helperText: '', // Prevents dialog resizing when disabled - errorText: !isValidFormat + errorText: !nameNotEmpty ? l10n.l_account_name_required : !isUnique ? l10n.l_name_already_exists @@ -184,7 +211,7 @@ class _RenameAccountDialogState extends ConsumerState { textInputAction: TextInputAction.done, onChanged: (value) { setState(() { - _account = value.trim(); + _name = value.trim(); }); }, onFieldSubmitted: (_) { diff --git a/lib/oath/views/rename_list_account.dart b/lib/oath/views/rename_list_account.dart deleted file mode 100644 index 123c3484..00000000 --- a/lib/oath/views/rename_list_account.dart +++ /dev/null @@ -1,207 +0,0 @@ -/* - * 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 'package:logging/logging.dart'; - -import '../../app/logging.dart'; -import '../../app/message.dart'; -import '../../app/models.dart'; -import '../../exception/cancellation_exception.dart'; -import '../../desktop/models.dart'; -import '../../widgets/responsive_dialog.dart'; -import '../../widgets/utf8_utils.dart'; -import '../models.dart'; -import '../keys.dart' as keys; -import 'utils.dart'; - -final _log = Logger('oath.view.rename_account_dialog'); - -class RenameList extends ConsumerStatefulWidget { - final DeviceNode device; - final CredentialData credential; - final List? credentialsFromUri; - final List? credentials; - - const RenameList( - this.device, this.credential, this.credentialsFromUri, this.credentials, - {super.key}); - - @override - ConsumerState createState() => _RenameListState(); -} - -class _RenameListState extends ConsumerState { - late String _issuer; - late String _account; - - @override - void initState() { - super.initState(); - _issuer = widget.credential.issuer?.trim() ?? ''; - _account = widget.credential.name.trim(); - } - - void _submit() { - if (!mounted) return; - - final l10n = AppLocalizations.of(context)!; - try { - // Rename credentials - final credential = widget.credential.copyWith( - issuer: _issuer == '' ? null : _issuer, - name: _account, - ); - - Navigator.of(context).pop(credential); - showMessage(context, l10n.s_account_renamed); - } on CancellationException catch (_) { - // ignored - } catch (e) { - _log.error('Failed to add account', e); - 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_account_add_failed(errorMessage), - duration: const Duration(seconds: 4), - ); - } - } - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; - final credential = widget.credential; - - final (issuerRemaining, nameRemaining) = getRemainingKeySpace( - oathType: credential.oathType, - period: credential.period, - issuer: _issuer, - name: _account, - ); - - // is this credential's name/issuer pair different from all other? - final isUniqueFromUri = widget.credentialsFromUri - ?.where((element) => - element != credential && - element.name == _account && - (element.issuer ?? '') == _issuer) - .isEmpty ?? - false; - - final isUniqueFromDevice = widget.credentials - ?.where((element) => - element.name == _account && (element.issuer ?? '') == _issuer) - .isEmpty ?? - false; - - // is this credential's name/issuer of valid format - final isValidFormat = _account.isNotEmpty; - - // are the name/issuer values different from original - final didChange = (widget.credential.issuer ?? '') != _issuer || - widget.credential.name != _account; - - // can we rename with the new values - final isValid = isUniqueFromUri && isUniqueFromDevice && isValidFormat; - - return ResponsiveDialog( - title: Text(l10n.s_rename_account), - actions: [ - TextButton( - onPressed: didChange && isValid ? _submit : null, - key: keys.saveButton, - child: Text(l10n.s_save), - ), - ], - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - credential.issuer != null - ? Text(l10n.q_rename_target( - '${credential.issuer} (${credential.name})')) - : Text(l10n.q_rename_target(credential.name)), - Text(l10n.p_rename_will_change_account_displayed), - TextFormField( - initialValue: _issuer, - enabled: issuerRemaining > 0, - maxLength: issuerRemaining > 0 ? issuerRemaining : null, - buildCounter: buildByteCounterFor(_issuer), - inputFormatters: [limitBytesLength(issuerRemaining)], - key: keys.issuerField, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_issuer_optional, - helperText: '', // Prevents dialog resizing when disabled - prefixIcon: const Icon(Icons.business_outlined), - ), - textInputAction: TextInputAction.next, - onChanged: (value) { - setState(() { - _issuer = value.trim(); - }); - }, - ), - TextFormField( - initialValue: _account, - maxLength: nameRemaining, - inputFormatters: [limitBytesLength(nameRemaining)], - buildCounter: buildByteCounterFor(_account), - key: keys.nameField, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_account_name, - helperText: '', // Prevents dialog resizing when disabled - errorText: !isValidFormat - ? l10n.l_account_name_required - : (!isUniqueFromUri || !isUniqueFromDevice) - ? l10n.l_name_already_exists - : null, - prefixIcon: const Icon(Icons.people_alt_outlined), - ), - textInputAction: TextInputAction.done, - onChanged: (value) { - setState(() { - _account = value.trim(); - }); - }, - onFieldSubmitted: (_) { - if (didChange && isValid) { - _submit(); - } - }, - ), - ] - .map((e) => Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: e, - )) - .toList(), - ), - ), - ); - } -} From ffabdffa49560521c000aedadee9e28e06e50175 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 15 Aug 2023 11:23:17 +0200 Subject: [PATCH 090/158] Various PIV UI updates. - Certificate details in table, with copyable values. - Better error handling for PIN management. --- lib/l10n/app_en.arb | 44 +++++------ lib/piv/views/key_actions.dart | 58 ++++++++------ lib/piv/views/manage_pin_puk_dialog.dart | 6 +- lib/piv/views/piv_screen.dart | 2 +- lib/piv/views/slot_dialog.dart | 99 +++++++++++++++++------- lib/widgets/tooltip_if_truncated.dart | 51 ++++++++++++ 6 files changed, 181 insertions(+), 79 deletions(-) create mode 100644 lib/widgets/tooltip_if_truncated.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 38bc8f36..7434e1cc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -48,6 +48,12 @@ "item": {} } }, + "s_definition": "{item}:", + "@s_definition" : { + "placeholders": { + "item": {} + } + }, "s_about": "About", "s_appearance": "Appearance", @@ -212,6 +218,12 @@ "retries": {} } }, + "l_wrong_puk_attempts_remaining": "Wrong PUK, {retries} attempt(s) remaining", + "@l_wrong_puk_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, "s_fido_pin_protection": "FIDO PIN protection", "l_fido_pin_protection_optional": "Optional FIDO PIN protection", "l_enter_fido2_pin": "Enter the FIDO2 PIN for your YubiKey", @@ -231,6 +243,7 @@ "s_pin_required": "PIN required", "p_pin_required_desc": "The action you are about to perform requires the PIV PIN to be entered.", "l_piv_pin_blocked": "Blocked, use PUK to reset", + "l_piv_pin_puk_blocked": "Blocked, factory reset needed", "p_enter_new_piv_pin_puk": "Enter a new {name} to set. Must be 6-8 characters.", "@p_enter_new_piv_pin_puk" : { "placeholders": { @@ -418,32 +431,11 @@ "l_import_desc": "Import a key and/or certificate", "l_delete_certificate": "Delete certificate", "l_delete_certificate_desc": "Remove the certificate from your YubiKey", - "l_subject_issuer": "Subject: {subject}, Issuer: {issuer}", - "@l_subject_issuer" : { - "placeholders": { - "subject": {}, - "issuer": {} - } - }, - "l_serial": "Serial: {serial}", - "@l_serial" : { - "placeholders": { - "serial": {} - } - }, - "l_certificate_fingerprint": "Fingerprint: {fingerprint}", - "@l_certificate_fingerprint" : { - "placeholders": { - "fingerprint": {} - } - }, - "l_valid": "Valid: {not_before} - {not_after}", - "@l_valid" : { - "placeholders": { - "not_before": {}, - "not_after": {} - } - }, + "s_issuer": "Issuer", + "s_serial": "Serial", + "s_certificate_fingerprint": "Fingerprint", + "s_valid_from": "Valid from", + "s_valid_to": "Valid to", "l_no_certificate": "No certificate loaded", "l_key_no_certificate": "Key without certificate loaded", "s_generate_key": "Generate key", diff --git a/lib/piv/views/key_actions.dart b/lib/piv/views/key_actions.dart index 3a2769fd..360586b0 100644 --- a/lib/piv/views/key_actions.dart +++ b/lib/piv/views/key_actions.dart @@ -39,6 +39,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, final pinBlocked = pivState.pinAttempts == 0; final pukAttempts = pivState.metadata?.pukMetadata.attemptsRemaining; + final alertIcon = Icon(Icons.warning_amber, color: colors.tertiary); return FsDialog( child: Column( @@ -50,35 +51,46 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, key: keys.managePinAction, title: l10n.s_pin, subtitle: pinBlocked - ? l10n.l_piv_pin_blocked + ? (pukAttempts != 0 + ? l10n.l_piv_pin_blocked + : l10n.l_piv_pin_puk_blocked) : l10n.l_attempts_remaining(pivState.pinAttempts), icon: const Icon(Icons.pin_outlined), - onTap: (context) { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => ManagePinPukDialog( - devicePath, - target: - pinBlocked ? ManageTarget.unblock : ManageTarget.pin, - ), - ); - }), + trailing: pinBlocked ? alertIcon : null, + onTap: !(pinBlocked && pukAttempts == 0) + ? (context) { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ManagePinPukDialog( + devicePath, + target: pinBlocked + ? ManageTarget.unblock + : ManageTarget.pin, + ), + ); + } + : null), ActionListItem( key: keys.managePukAction, title: l10n.s_puk, subtitle: pukAttempts != null - ? l10n.l_attempts_remaining(pukAttempts) + ? (pukAttempts == 0 + ? l10n.l_piv_pin_puk_blocked + : l10n.l_attempts_remaining(pukAttempts)) : null, icon: const Icon(Icons.pin_outlined), - onTap: (context) { - Navigator.of(context).pop(); - showBlurDialog( - context: context, - builder: (context) => ManagePinPukDialog(devicePath, - target: ManageTarget.puk), - ); - }), + trailing: pukAttempts == 0 ? alertIcon : null, + onTap: pukAttempts != 0 + ? (context) { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ManagePinPukDialog(devicePath, + target: ManageTarget.puk), + ); + } + : null), ActionListItem( key: keys.manageManagementKeyAction, title: l10n.s_management_key, @@ -88,9 +100,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, ? l10n.l_pin_protected_key : l10n.l_change_management_key), icon: const Icon(Icons.key_outlined), - trailing: usingDefaultMgmtKey - ? Icon(Icons.warning_amber, color: colors.tertiary) - : null, + trailing: usingDefaultMgmtKey ? alertIcon : null, onTap: (context) { Navigator.of(context).pop(); showBlurDialog( diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart index b6e9cdd5..b45736ba 100644 --- a/lib/piv/views/manage_pin_puk_dialog.dart +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -114,7 +114,11 @@ class _ManagePinPukDialogState extends ConsumerState { : l10n.s_current_puk, prefixIcon: const Icon(Icons.password_outlined), errorText: _currentIsWrong - ? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining) + ? (widget.target == ManageTarget.puk + ? l10n.l_wrong_pin_attempts_remaining( + _attemptsRemaining) + : l10n.l_wrong_puk_attempts_remaining( + _attemptsRemaining)) : null, errorMaxLines: 3), textInputAction: TextInputAction.next, diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart index 6d3474a7..57b0f8ff 100644 --- a/lib/piv/views/piv_screen.dart +++ b/lib/piv/views/piv_screen.dart @@ -104,7 +104,7 @@ class _CertificateListItem extends StatelessWidget { ), title: slot.getDisplayName(l10n), subtitle: certInfo != null - ? l10n.l_subject_issuer(certInfo.subject, certInfo.issuer) + ? certInfo.subject : pivSlot.hasKey == true ? l10n.l_key_no_certificate : l10n.l_no_certificate, diff --git a/lib/piv/views/slot_dialog.dart b/lib/piv/views/slot_dialog.dart index c1aa361e..92f289b5 100644 --- a/lib/piv/views/slot_dialog.dart +++ b/lib/piv/views/slot_dialog.dart @@ -1,10 +1,29 @@ +/* + * 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 'package:intl/intl.dart'; +import '../../app/message.dart'; import '../../app/state.dart'; import '../../app/views/fs_dialog.dart'; import '../../app/views/action_list.dart'; +import '../../widgets/tooltip_if_truncated.dart'; import '../models.dart'; import '../state.dart'; import 'actions.dart'; @@ -29,6 +48,8 @@ class SlotDialog extends ConsumerWidget { final subtitleStyle = textTheme.bodyMedium!.copyWith( color: textTheme.bodySmall!.color, ); + final clipboard = ref.watch(clipboardProvider); + final withContext = ref.read(withContextProvider); final pivState = ref.watch(pivStateProvider(node.path)).valueOrNull; final slotData = ref.watch(pivSlotsProvider(node.path).select((value) => @@ -40,6 +61,32 @@ class SlotDialog extends ConsumerWidget { return const FsDialog(child: CircularProgressIndicator()); } + TableRow detailRow(String title, String value) { + return TableRow( + children: [ + Text( + l10n.s_definition(title), + textAlign: TextAlign.right, + ), + const SizedBox(width: 8.0), + GestureDetector( + onDoubleTap: () async { + await clipboard.setText(value); + if (!clipboard.platformGivesFeedback()) { + await withContext((context) async { + showMessage(context, l10n.p_target_copied_clipboard(title)); + }); + } + }, + child: TooltipIfTruncated( + text: value, + style: subtitleStyle, + ), + ), + ], + ); + } + final certInfo = slotData.certInfo; return registerPivActions( node.path, @@ -52,7 +99,7 @@ class SlotDialog extends ConsumerWidget { child: Column( children: [ Padding( - padding: const EdgeInsets.only(top: 48, bottom: 32), + padding: const EdgeInsets.only(top: 48, bottom: 16), child: Column( children: [ Text( @@ -62,31 +109,29 @@ class SlotDialog extends ConsumerWidget { textAlign: TextAlign.center, ), if (certInfo != null) ...[ - Text( - l10n.l_subject_issuer( - certInfo.subject, certInfo.issuer), - softWrap: true, - textAlign: TextAlign.center, - style: subtitleStyle, - ), - Text( - l10n.l_serial(certInfo.serial), - softWrap: true, - textAlign: TextAlign.center, - style: subtitleStyle, - ), - Text( - l10n.l_certificate_fingerprint(certInfo.fingerprint), - softWrap: true, - textAlign: TextAlign.center, - style: subtitleStyle, - ), - Text( - l10n.l_valid( - certInfo.notValidBefore, certInfo.notValidAfter), - softWrap: true, - textAlign: TextAlign.center, - style: subtitleStyle, + Padding( + padding: const EdgeInsets.all(16), + child: Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + columnWidths: const {2: FlexColumnWidth()}, + children: [ + detailRow(l10n.s_subject, certInfo.subject), + detailRow(l10n.s_issuer, certInfo.issuer), + detailRow(l10n.s_serial, certInfo.serial), + detailRow(l10n.s_certificate_fingerprint, + certInfo.fingerprint), + detailRow( + l10n.s_valid_from, + DateFormat.yMMMEd().format( + DateTime.parse(certInfo.notValidBefore)), + ), + detailRow( + l10n.s_valid_to, + DateFormat.yMMMEd().format( + DateTime.parse(certInfo.notValidAfter)), + ), + ], + ), ), ] else ...[ Padding( @@ -98,8 +143,8 @@ class SlotDialog extends ConsumerWidget { style: subtitleStyle, ), ), + const SizedBox(height: 16), ], - const SizedBox(height: 16), ], ), ), diff --git a/lib/widgets/tooltip_if_truncated.dart b/lib/widgets/tooltip_if_truncated.dart new file mode 100644 index 00000000..3694143e --- /dev/null +++ b/lib/widgets/tooltip_if_truncated.dart @@ -0,0 +1,51 @@ +/* + * 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'; + +class TooltipIfTruncated extends StatelessWidget { + final String text; + final TextStyle style; + const TooltipIfTruncated( + {super.key, required this.text, required this.style}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final textWidget = Text( + text, + textAlign: TextAlign.left, + overflow: TextOverflow.fade, + softWrap: false, + style: style, + ); + final TextPainter textPainter = TextPainter( + text: TextSpan(text: text, style: style), + textDirection: TextDirection.ltr, + maxLines: 1, + )..layout(minWidth: 0, maxWidth: constraints.maxWidth); + return textPainter.didExceedMaxLines + ? Tooltip( + margin: const EdgeInsets.all(16), + message: text, + child: textWidget, + ) + : textWidget; + }, + ); + } +} From 9f3cf07253276f212c7e3e571dcd7fd45e24d3d3 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 15 Aug 2023 14:29:38 +0200 Subject: [PATCH 091/158] Increase tooltip wait duration, and change default key type. --- lib/piv/models.dart | 2 +- lib/theme.dart | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/piv/models.dart b/lib/piv/models.dart index 458dedb0..d42ab88f 100644 --- a/lib/piv/models.dart +++ b/lib/piv/models.dart @@ -24,7 +24,7 @@ part 'models.g.dart'; const defaultManagementKey = '010203040506070801020304050607080102030405060708'; const defaultManagementKeyType = ManagementKeyType.tdes; -const defaultKeyType = KeyType.rsa2048; +const defaultKeyType = KeyType.eccp256; const defaultGenerateType = GenerateType.certificate; enum GenerateType { diff --git a/lib/theme.dart b/lib/theme.dart index 77df071b..9ed39f58 100755 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -41,6 +41,9 @@ class AppTheme { dialogTheme: const DialogTheme( surfaceTintColor: Colors.white70, ), + tooltipTheme: const TooltipThemeData( + waitDuration: Duration(seconds: 1), + ), ); static ThemeData get darkTheme => ThemeData( @@ -67,6 +70,9 @@ class AppTheme { dialogTheme: DialogTheme( surfaceTintColor: Colors.grey.shade700, ), + tooltipTheme: const TooltipThemeData( + waitDuration: Duration(seconds: 1), + ), ); /* TODO: Remove this. It is left here as a reference as we adjust styles to work with Flutter 3.7. From 9648a1396cc4e972591356970ed37510db914fca Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 16 Aug 2023 16:52:49 +0200 Subject: [PATCH 092/158] Use transparent barrier for FsDialogs. --- lib/app/message.dart | 2 ++ lib/app/views/app_page.dart | 6 +++++- lib/app/views/fs_dialog.dart | 3 ++- lib/fido/views/unlocked_page.dart | 2 ++ lib/oath/views/account_view.dart | 1 + lib/piv/views/piv_screen.dart | 1 + 6 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/app/message.dart b/lib/app/message.dart index db7fcda8..2b2f2b41 100755 --- a/lib/app/message.dart +++ b/lib/app/message.dart @@ -32,10 +32,12 @@ Future showBlurDialog({ required BuildContext context, required Widget Function(BuildContext) builder, RouteSettings? routeSettings, + Color barrierColor = const Color(0x80000000), }) async => await showGeneralDialog( context: context, barrierDismissible: true, + barrierColor: barrierColor, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, pageBuilder: (ctx, anim1, anim2) => builder(ctx), transitionDuration: const Duration(milliseconds: 150), diff --git a/lib/app/views/app_page.dart b/lib/app/views/app_page.dart index b8ad31f8..f4e14ecd 100755 --- a/lib/app/views/app_page.dart +++ b/lib/app/views/app_page.dart @@ -224,7 +224,11 @@ class AppPage extends StatelessWidget { child: IconButton( key: actionsIconButtonKey, onPressed: () { - showBlurDialog(context: context, builder: keyActionsBuilder!); + showBlurDialog( + context: context, + barrierColor: Colors.transparent, + builder: keyActionsBuilder!, + ); }, icon: keyActionsBadge ? const Badge( diff --git a/lib/app/views/fs_dialog.dart b/lib/app/views/fs_dialog.dart index d76fa7de..029538f5 100644 --- a/lib/app/views/fs_dialog.dart +++ b/lib/app/views/fs_dialog.dart @@ -26,7 +26,8 @@ class FsDialog extends StatelessWidget { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Dialog.fullscreen( - backgroundColor: Theme.of(context).colorScheme.background.withAlpha(100), + backgroundColor: + Theme.of(context).colorScheme.background.withOpacity(0.7), child: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/fido/views/unlocked_page.dart b/lib/fido/views/unlocked_page.dart index bf796e31..05395b53 100755 --- a/lib/fido/views/unlocked_page.dart +++ b/lib/fido/views/unlocked_page.dart @@ -59,6 +59,7 @@ class FidoUnlockedPage extends ConsumerWidget { OpenIntent: CallbackAction( onInvoke: (_) => showBlurDialog( context: context, + barrierColor: Colors.transparent, builder: (context) => CredentialDialog(cred), )), DeleteIntent: CallbackAction( @@ -91,6 +92,7 @@ class FidoUnlockedPage extends ConsumerWidget { OpenIntent: CallbackAction( onInvoke: (_) => showBlurDialog( context: context, + barrierColor: Colors.transparent, builder: (context) => FingerprintDialog(fp), )), EditIntent: CallbackAction( diff --git a/lib/oath/views/account_view.dart b/lib/oath/views/account_view.dart index 8beed7dc..09222c47 100755 --- a/lib/oath/views/account_view.dart +++ b/lib/oath/views/account_view.dart @@ -89,6 +89,7 @@ class _AccountViewState extends ConsumerState { OpenIntent: CallbackAction(onInvoke: (_) async { await showBlurDialog( context: context, + barrierColor: Colors.transparent, builder: (context) => AccountDialog(credential), ); return null; diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart index 57b0f8ff..e54b879b 100644 --- a/lib/piv/views/piv_screen.dart +++ b/lib/piv/views/piv_screen.dart @@ -70,6 +70,7 @@ class PivScreen extends ConsumerWidget { CallbackAction(onInvoke: (_) async { await showBlurDialog( context: context, + barrierColor: Colors.transparent, builder: (context) => SlotDialog(e.slot), ); return null; From de64a6b647b01ee5650603ea2a3c89e44c2210b6 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 18 Aug 2023 15:59:15 +0200 Subject: [PATCH 093/158] Tweak colors. --- lib/theme.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/theme.dart b/lib/theme.dart index 9ed39f58..a171869a 100755 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -36,7 +36,7 @@ class AppTheme { tertiary: amber.withOpacity(0.7), ), textTheme: TextTheme( - bodySmall: TextStyle(color: Colors.grey.shade900), + bodySmall: TextStyle(color: Colors.grey.shade600), ), dialogTheme: const DialogTheme( surfaceTintColor: Colors.white70, @@ -55,8 +55,7 @@ class AppTheme { ).copyWith( primary: primaryGreen, //onPrimary: Colors.grey.shade900, - //secondary: accentGreen, - //secondary: const Color(0xff5d7d90), + secondary: Colors.grey.shade400, //onSecondary: Colors.grey.shade900, //primaryContainer: Colors.grey.shade800, //onPrimaryContainer: Colors.grey.shade100, From ac131982bffc830434423ecdfc2b79c4abd1cca3 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 18 Aug 2023 16:51:42 +0200 Subject: [PATCH 094/158] Tweak colors and margins. --- lib/oath/views/account_dialog.dart | 67 +++++++++++++++++------------- lib/theme.dart | 4 ++ 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/lib/oath/views/account_dialog.dart b/lib/oath/views/account_dialog.dart index 3446a55b..fa771f37 100755 --- a/lib/oath/views/account_dialog.dart +++ b/lib/oath/views/account_dialog.dart @@ -121,40 +121,51 @@ class AccountDialog extends ConsumerWidget { child: Column( children: [ Padding( - padding: const EdgeInsets.only(top: 48, bottom: 16), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 32), + child: Column( children: [ - IconTheme( - data: IconTheme.of(context).copyWith(size: 24), - child: helper.buildCodeIcon(), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconTheme( + data: IconTheme.of(context).copyWith(size: 24), + child: helper.buildCodeIcon(), + ), + const SizedBox(width: 8.0), + DefaultTextStyle.merge( + style: const TextStyle(fontSize: 28), + child: helper.buildCodeLabel(), + ), + ], + ), ), - const SizedBox(width: 8.0), - DefaultTextStyle.merge( - style: const TextStyle(fontSize: 28), - child: helper.buildCodeLabel(), + Text( + helper.title, + style: Theme.of(context).textTheme.headlineSmall, + softWrap: true, + textAlign: TextAlign.center, ), + if (subtitle != null) + Text( + subtitle, + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: + Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context) + .textTheme + .bodySmall! + .color, + ), + ), ], ), ), - Text( - helper.title, - style: Theme.of(context).textTheme.headlineSmall, - softWrap: true, - textAlign: TextAlign.center, - ), - if (subtitle != null) - Text( - subtitle, - softWrap: true, - textAlign: TextAlign.center, - // This is what ListTile uses for subtitle - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).textTheme.bodySmall!.color, - ), - ), - const SizedBox(height: 32), ActionListSection.fromMenuActions( context, AppLocalizations.of(context)!.s_actions, diff --git a/lib/theme.dart b/lib/theme.dart index a171869a..aef04bd2 100755 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -33,7 +33,11 @@ class AppTheme { ).copyWith( primary: primaryBlue, //secondary: accentGreen, + secondary: Colors.grey.shade400, + onSecondary: Colors.grey.shade800, tertiary: amber.withOpacity(0.7), + error: darkRed, + onError: Colors.white.withOpacity(0.9), ), textTheme: TextTheme( bodySmall: TextStyle(color: Colors.grey.shade600), From df9e04d112d51ef3404009082dcbd6c507c97380 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 21 Aug 2023 15:06:16 +0200 Subject: [PATCH 095/158] bump android deps --- android/app/build.gradle | 4 ++-- android/build.gradle | 2 +- android/flutter_plugins/qrscanner_zxing/android/build.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index aaa45e2c..70428f24 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -100,8 +100,8 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation "androidx.core:core-ktx:1.10.1" - implementation 'androidx.fragment:fragment-ktx:1.6.0' - implementation 'androidx.preference:preference-ktx:1.2.0' + implementation 'androidx.fragment:fragment-ktx:1.6.1' + implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'com.google.android.material:material:1.9.0' diff --git a/android/build.gradle b/android/build.gradle index 16b2224e..ec9d3803 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.8.10' + ext.kotlin_version = '1.9.0' repositories { google() mavenCentral() diff --git a/android/flutter_plugins/qrscanner_zxing/android/build.gradle b/android/flutter_plugins/qrscanner_zxing/android/build.gradle index 14a4bcb2..17bb15b9 100644 --- a/android/flutter_plugins/qrscanner_zxing/android/build.gradle +++ b/android/flutter_plugins/qrscanner_zxing/android/build.gradle @@ -2,7 +2,7 @@ group 'com.yubico.authenticator.flutter_plugins.qrscanner_zxing' version '1.0' buildscript { - ext.kotlin_version = '1.8.10' + ext.kotlin_version = '1.9.0' repositories { google() mavenCentral() From ce4f50968357479f262724676ffd179049dcb83c Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 21 Aug 2023 15:28:58 +0200 Subject: [PATCH 096/158] bump Flutter to 3.13.0 --- .github/workflows/android.yml | 2 +- .github/workflows/check-strings.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/linux.yml | 2 +- .github/workflows/macos.yml | 2 +- .github/workflows/windows.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 4b70246a..f34f2d5c 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -17,7 +17,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.10.6' + flutter-version: '3.13.0' - run: | flutter config flutter --version diff --git a/.github/workflows/check-strings.yml b/.github/workflows/check-strings.yml index fe9b1c4a..0ef4c4f4 100644 --- a/.github/workflows/check-strings.yml +++ b/.github/workflows/check-strings.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest env: - FLUTTER: '3.10.6' + FLUTTER: '3.13.0' steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8185a102..ae37d9aa 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -43,7 +43,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.10.6' + flutter-version: '3.13.0' - run: | flutter config flutter --version diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 1329e448..c7a7947c 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest env: PYVER: '3.11.4' - FLUTTER: '3.10.6' + FLUTTER: '3.13.0' container: image: ubuntu:20.04 env: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index cf559c60..4e0cd7c1 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -49,7 +49,7 @@ jobs: with: channel: 'stable' architecture: 'x64' - flutter-version: '3.10.6' + flutter-version: '3.13.0' - run: flutter config --enable-macos-desktop - run: flutter --version diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 820cac50..4e8f6d95 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -45,7 +45,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.10.6' + flutter-version: '3.13.0' - run: flutter config --enable-windows-desktop - run: flutter --version From 4296ad4cf8f897c7d4fa4d4aa7a356efce82de73 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 21 Aug 2023 15:32:21 +0200 Subject: [PATCH 097/158] bump flutter packages --- pubspec.lock | 82 ++++++++++++++++++++++++++++------------------------ pubspec.yaml | 12 ++++---- 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 45cc1eea..a5fe9a11 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "64.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.2.0" archive: dependency: "direct main" description: @@ -157,10 +157,10 @@ packages: dependency: "direct main" description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" convert: dependency: "direct main" description: @@ -276,10 +276,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: b83ac5827baadefd331ea1d85110f34645827ea234ccabf53a655f41901a9bf4 + sha256: b6cb0041c6c11cefb2dcb97ef436eba43c6d41287ac6d8ca93e02a497f53a4f3 url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" flutter_test: dependency: "direct dev" description: flutter @@ -294,10 +294,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "2df89855fe181baae3b6d714dc3c4317acf4fccd495a6f36e5e00f24144c6c3b" + sha256: "83462cfc33dc9680533a7f3a4a6ab60aa94f287db5f4ee6511248c22833c497f" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" freezed_annotation: dependency: "direct main" description: @@ -360,10 +360,10 @@ packages: dependency: "direct main" description: name: intl - sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.18.0" + version: "0.18.1" io: dependency: "direct main" description: @@ -424,18 +424,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" menu_base: dependency: transitive description: @@ -488,50 +488,50 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" + sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0" url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.1.0" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.1.0" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297" + sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.0" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 + sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 url: "https://pub.dev" source: hosted - version: "2.1.11" + version: "2.2.0" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.1.0" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.0" petitparser: dependency: transitive description: @@ -607,10 +607,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "80e48bebc83010d5e67a11c9514af6b44bbac1ec77b4333c8ea65dbc79e2d8ef" + sha256: b0657b5b30c81a3184bdaab353045f0a403ebd60bb381591a8b7ad77dcade793 url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" screen_retriever: dependency: "direct main" description: @@ -724,10 +724,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: @@ -788,10 +788,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" timing: dependency: transitive description: @@ -924,10 +924,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6deed8ed625c52864792459709183da231ebf66ff0cf09e69b573227c377efe + sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f url: "https://pub.dev" source: hosted - version: "11.3.0" + version: "11.7.1" watcher: dependency: transitive description: @@ -936,6 +936,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -993,5 +1001,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 16cb8e57..cfaf64f5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - intl: ^0.18.0 + intl: ^0.18.1 # The following adds the Cupertino Icons font to your application. @@ -45,7 +45,7 @@ dependencies: logging: 1.1.1 collection: ^1.16.0 shared_preferences: ^2.1.2 - flutter_riverpod: ^2.3.6 + flutter_riverpod: ^2.3.7 json_annotation: ^4.8.1 freezed_annotation: ^2.2.0 window_manager: ^0.3.2 @@ -54,9 +54,9 @@ dependencies: screen_retriever: ^0.1.6 desktop_drop: ^0.4.0 url_launcher: ^6.1.7 - path_provider: ^2.0.14 - vector_graphics: ^1.1.5 - vector_graphics_compiler: ^1.1.5 + path_provider: ^2.1.0 + vector_graphics: ^1.1.7 + vector_graphics_compiler: ^1.1.7 path: ^1.8.2 file_picker: ^5.3.2 archive: ^3.3.2 @@ -81,7 +81,7 @@ dev_dependencies: flutter_lints: ^2.0.1 build_runner: ^2.4.5 - freezed: ^2.3.5 + freezed: ^2.4.2 json_serializable: ^6.7.0 # For information on the generic Dart part of this file, see the From 23c56fcf2633ca2a686c10b4a9a21077f47810ba Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 21 Aug 2023 15:37:24 +0200 Subject: [PATCH 098/158] bump python dependencies --- helper/poetry.lock | 228 ++++++++++++++++++++---------------------- helper/pyproject.toml | 8 +- 2 files changed, 113 insertions(+), 123 deletions(-) diff --git a/helper/poetry.lock b/helper/poetry.lock index 0c51e742..703c0116 100755 --- a/helper/poetry.lock +++ b/helper/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. [[package]] name = "altgraph" @@ -91,14 +91,14 @@ pycparser = "*" [[package]] name = "click" -version = "8.1.6" +version = "8.1.7" description = "Composable command line interface toolkit" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, - {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -118,35 +118,35 @@ files = [ [[package]] name = "cryptography" -version = "41.0.2" +version = "41.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"}, - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"}, - {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"}, - {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"}, - {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, + {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, + {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, + {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, ] [package.dependencies] @@ -164,14 +164,14 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "exceptiongroup" -version = "1.1.2" +version = "1.1.3" description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, - {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, ] [package.extras] @@ -217,14 +217,14 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs [[package]] name = "importlib-resources" -version = "6.0.0" +version = "6.0.1" description = "Read resources from Python packages" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_resources-6.0.0-py3-none-any.whl", hash = "sha256:d952faee11004c045f785bb5636e8f885bed30dc3c940d5d42798a2a4541c185"}, - {file = "importlib_resources-6.0.0.tar.gz", hash = "sha256:4cf94875a8368bd89531a756df9a9ebe1f150e0f885030b461237bc7f2d905f2"}, + {file = "importlib_resources-6.0.1-py3-none-any.whl", hash = "sha256:134832a506243891221b88b4ae1213327eea96ceb4e407a00d790bb0626f45cf"}, + {file = "importlib_resources-6.0.1.tar.gz", hash = "sha256:4359457e42708462b9626a04657c6208ad799ceb41e5c58c57ffa0e6a098a5d4"}, ] [package.dependencies] @@ -323,14 +323,14 @@ altgraph = ">=0.17" [[package]] name = "more-itertools" -version = "10.0.0" +version = "10.1.0" description = "More routines for operating on iterables, beyond itertools" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "more-itertools-10.0.0.tar.gz", hash = "sha256:cd65437d7c4b615ab81c0640c0480bc29a550ea032891977681efd28344d51e1"}, - {file = "more_itertools-10.0.0-py3-none-any.whl", hash = "sha256:928d514ffd22b5b0a8fce326d57f423a55d2ff783b093bab217eda71e732330f"}, + {file = "more-itertools-10.1.0.tar.gz", hash = "sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a"}, + {file = "more_itertools-10.1.0-py3-none-any.whl", hash = "sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6"}, ] [[package]] @@ -409,78 +409,68 @@ files = [ [[package]] name = "pillow" -version = "9.5.0" +version = "10.0.0" description = "Python Imaging Library (Fork)" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, - {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, - {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, - {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, - {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, - {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, - {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, - {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, - {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, - {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, - {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, - {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, - {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, - {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, - {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, - {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, - {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, + {file = "Pillow-10.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f62406a884ae75fb2f818694469519fb685cc7eaff05d3451a9ebe55c646891"}, + {file = "Pillow-10.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d5db32e2a6ccbb3d34d87c87b432959e0db29755727afb37290e10f6e8e62614"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf4392b77bdc81f36e92d3a07a5cd072f90253197f4a52a55a8cec48a12483b"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:520f2a520dc040512699f20fa1c363eed506e94248d71f85412b625026f6142c"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:8c11160913e3dd06c8ffdb5f233a4f254cb449f4dfc0f8f4549eda9e542c93d1"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a74ba0c356aaa3bb8e3eb79606a87669e7ec6444be352870623025d75a14a2bf"}, + {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d0dae4cfd56969d23d94dc8e89fb6a217be461c69090768227beb8ed28c0a3"}, + {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22c10cc517668d44b211717fd9775799ccec4124b9a7f7b3635fc5386e584992"}, + {file = "Pillow-10.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:dffe31a7f47b603318c609f378ebcd57f1554a3a6a8effbc59c3c69f804296de"}, + {file = "Pillow-10.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:9fb218c8a12e51d7ead2a7c9e101a04982237d4855716af2e9499306728fb485"}, + {file = "Pillow-10.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d35e3c8d9b1268cbf5d3670285feb3528f6680420eafe35cccc686b73c1e330f"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ed64f9ca2f0a95411e88a4efbd7a29e5ce2cea36072c53dd9d26d9c76f753b3"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b6eb5502f45a60a3f411c63187db83a3d3107887ad0d036c13ce836f8a36f1d"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c1fbe7621c167ecaa38ad29643d77a9ce7311583761abf7836e1510c580bf3dd"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cd25d2a9d2b36fcb318882481367956d2cf91329f6892fe5d385c346c0649629"}, + {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538"}, + {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d"}, + {file = "Pillow-10.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f"}, + {file = "Pillow-10.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc2ec7c7b5d66b8ec9ce9f720dbb5fa4bace0f545acd34870eff4a369b44bf37"}, + {file = "Pillow-10.0.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883"}, + {file = "Pillow-10.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce543ed15570eedbb85df19b0a1a7314a9c8141a36ce089c0a894adbfccb4568"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:685ac03cc4ed5ebc15ad5c23bc555d68a87777586d970c2c3e216619a5476223"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d72e2ecc68a942e8cf9739619b7f408cc7b272b279b56b2c83c6123fcfa5cdff"}, + {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551"}, + {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5"}, + {file = "Pillow-10.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199"}, + {file = "Pillow-10.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:1ce91b6ec08d866b14413d3f0bbdea7e24dfdc8e59f562bb77bc3fe60b6144ca"}, + {file = "Pillow-10.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3"}, + {file = "Pillow-10.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f07ea8d2f827d7d2a49ecf1639ec02d75ffd1b88dcc5b3a61bbb37a8759ad8d"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:040586f7d37b34547153fa383f7f9aed68b738992380ac911447bb78f2abe530"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f88a0b92277de8e3ca715a0d79d68dc82807457dae3ab8699c758f07c20b3c51"}, + {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c7cf14a27b0d6adfaebb3ae4153f1e516df54e47e42dcc073d7b3d76111a8d86"}, + {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3400aae60685b06bb96f99a21e1ada7bc7a413d5f49bce739828ecd9391bb8f7"}, + {file = "Pillow-10.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbc02381779d412145331789b40cc7b11fdf449e5d94f6bc0b080db0a56ea3f0"}, + {file = "Pillow-10.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9211e7ad69d7c9401cfc0e23d49b69ca65ddd898976d660a2fa5904e3d7a9baa"}, + {file = "Pillow-10.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:faaf07ea35355b01a35cb442dd950d8f1bb5b040a7787791a535de13db15ed90"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f72a021fbb792ce98306ffb0c348b3c9cb967dce0f12a49aa4c3d3fdefa967"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f7c16705f44e0504a3a2a14197c1f0b32a95731d251777dcb060aa83022cb2d"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:76edb0a1fa2b4745fb0c99fb9fb98f8b180a1bbceb8be49b087e0b21867e77d3"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:368ab3dfb5f49e312231b6f27b8820c823652b7cd29cfbd34090565a015e99ba"}, + {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:608bfdee0d57cf297d32bcbb3c728dc1da0907519d1784962c5f0c68bb93e5a3"}, + {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5c6e3df6bdd396749bafd45314871b3d0af81ff935b2d188385e970052091017"}, + {file = "Pillow-10.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:7be600823e4c8631b74e4a0d38384c73f680e6105a7d3c6824fcf226c178c7e6"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:92be919bbc9f7d09f7ae343c38f5bb21c973d2576c1d45600fce4b74bafa7ac0"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8182b523b2289f7c415f589118228d30ac8c355baa2f3194ced084dac2dbba"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:38250a349b6b390ee6047a62c086d3817ac69022c127f8a5dc058c31ccef17f3"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88af2003543cc40c80f6fca01411892ec52b11021b3dc22ec3bc9d5afd1c5334"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c189af0545965fa8d3b9613cfdb0cd37f9d71349e0f7750e1fd704648d475ed2"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7b031a6fc11365970e6a5686d7ba8c63e4c1cf1ea143811acbb524295eabed"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db24668940f82321e746773a4bc617bfac06ec831e5c88b643f91f122a785684"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:efe8c0681042536e0d06c11f48cebe759707c9e9abf880ee213541c5b46c5bf3"}, + {file = "Pillow-10.0.0.tar.gz", hash = "sha256:9c82b5b3e043c7af0d95792d0d20ccf68f61a1fec6b3530e718b688422727396"}, ] [package.extras] @@ -551,14 +541,14 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2023.6" +version = "2023.7" description = "Community maintained hooks for PyInstaller" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pyinstaller-hooks-contrib-2023.6.tar.gz", hash = "sha256:596a72009d8692b043e0acbf5e1b476d93149900142ba01845dded91a0770cb5"}, - {file = "pyinstaller_hooks_contrib-2023.6-py2.py3-none-any.whl", hash = "sha256:aa6d7d038814df6aa7bec7bdbebc7cb4c693d3398df858f6062957f0797d397b"}, + {file = "pyinstaller-hooks-contrib-2023.7.tar.gz", hash = "sha256:0c436a4c3506020e34116a8a7ddfd854c1ad6ddca9a8cd84500bd6e69c9e68f9"}, + {file = "pyinstaller_hooks_contrib-2023.7-py2.py3-none-any.whl", hash = "sha256:3c10df14c0f71ab388dfbf1625375b087e7330d9444cbfd2b310ba027fa0cff0"}, ] [[package]] @@ -659,19 +649,19 @@ jeepney = ">=0.6" [[package]] name = "setuptools" -version = "68.0.0" +version = "68.1.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, - {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, + {file = "setuptools-68.1.2-py3-none-any.whl", hash = "sha256:3d8083eed2d13afc9426f227b24fd1659489ec107c0e86cec2ffdde5c92e790b"}, + {file = "setuptools-68.1.2.tar.gz", hash = "sha256:3d4dfa6d95f1b101d695a6160a7626e15583af71a5f52176efa5d39a054d475d"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5,<=7.1.2)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -688,19 +678,19 @@ files = [ [[package]] name = "yubikey-manager" -version = "5.1.1" +version = "5.2.0" description = "Tool for managing your YubiKey configuration." category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "yubikey_manager-5.1.1-py3-none-any.whl", hash = "sha256:67291f1d9396d99845b710eabfb4b5ba41b5fa6cc0011104267f91914c1867e3"}, - {file = "yubikey_manager-5.1.1.tar.gz", hash = "sha256:684102affd4a0d29611756da263c22f8e67226e80f65c5460c8c5608f9c0d58d"}, + {file = "yubikey_manager-5.2.0-py3-none-any.whl", hash = "sha256:6e0c82605f92012363ae3d69673eec6c7876e2e366aa049cff66cc6734049165"}, + {file = "yubikey_manager-5.2.0.tar.gz", hash = "sha256:45e0f09e3cee2375b6f930dd5d89c1d3a7ca5d5cccb599b16a12f8f7d989fd36"}, ] [package.dependencies] click = ">=8.0,<9.0" -cryptography = ">=3.0,<43" +cryptography = ">=3.0,<44" fido2 = ">=1.0,<2.0" keyring = ">=23.4,<24.0" pyscard = ">=2.0,<3.0" @@ -754,4 +744,4 @@ numpy = "*" [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "0ada1b4785281f6f18fb483a1d84504be84adc84aec8c8c7812cc92ea44656d8" +content-hash = "631e6fb7384478d8b248b77737a90cf1f11ba4b9a3b8215e08fca1157695d2d7" diff --git a/helper/pyproject.toml b/helper/pyproject.toml index 3b65a5b3..bf04eb54 100644 --- a/helper/pyproject.toml +++ b/helper/pyproject.toml @@ -10,14 +10,14 @@ packages = [ [tool.poetry.dependencies] python = "^3.8" -yubikey-manager = "5.1.1" +yubikey-manager = "5.2.0" mss = "^9.0.1" zxing-cpp = "^2.0.0" -Pillow = "^9.5.0" +Pillow = "^10.0.0" [tool.poetry.dev-dependencies] -pyinstaller = {version = "^5.12.0", python = "<3.12"} -pytest = "^7.3.2" +pyinstaller = {version = "^5.13.0", python = "<3.12"} +pytest = "^7.4.0" [build-system] requires = ["poetry-core>=1.0.0"] From 752665e122d56a14ecbbeb5586bcb1668b90299b Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 21 Aug 2023 15:42:39 +0200 Subject: [PATCH 099/158] update runner change --- macos/Runner.xcodeproj/project.pbxproj | 2 +- macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 58aa19b1..ca9c796a 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -205,7 +205,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5eb9a050..7a6b6f53 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Date: Mon, 21 Aug 2023 15:43:17 +0200 Subject: [PATCH 100/158] fix warning: Don't use 'BuildContext's across async gaps --- lib/fido/views/pin_dialog.dart | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/fido/views/pin_dialog.dart b/lib/fido/views/pin_dialog.dart index fdf4b411..c70487c4 100755 --- a/lib/fido/views/pin_dialog.dart +++ b/lib/fido/views/pin_dialog.dart @@ -22,6 +22,7 @@ import 'package:logging/logging.dart'; import '../../app/logging.dart'; import '../../app/message.dart'; import '../../app/models.dart'; +import '../../app/state.dart'; import '../../desktop/models.dart'; import '../../widgets/responsive_dialog.dart'; import '../models.dart'; @@ -184,10 +185,14 @@ class _FidoPinDialogState extends ConsumerState { } else { errorMessage = e.toString(); } - showMessage( - context, - l10n.l_set_pin_failed(errorMessage), - duration: const Duration(seconds: 4), + await ref.read(withContextProvider)( + (context) async { + showMessage( + context, + l10n.l_set_pin_failed(errorMessage), + duration: const Duration(seconds: 4), + ); + }, ); } } From 43d52c5f86ac0cad1f1f21fa8778e87e95d08474 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 21 Aug 2023 16:23:58 +0200 Subject: [PATCH 101/158] Add automatic change to CPP code. --- windows/runner/flutter_window.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index b25e363e..955ee303 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -31,6 +31,11 @@ bool FlutterWindow::OnCreate() { this->Show(); }); + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + return true; } From d796f68a006b34c424557ac6cf9328e054023129 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 21 Aug 2023 17:24:29 +0200 Subject: [PATCH 102/158] upgrade flutter deps --- pubspec.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index a5fe9a11..a7383f6f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -213,10 +213,10 @@ packages: dependency: transitive description: name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" file: dependency: transitive description: @@ -639,10 +639,10 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: f39696b83e844923b642ce9dd4bd31736c17e697f6731a5adf445b1274cf3cd4 + sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" shared_preferences_linux: dependency: transitive description: @@ -740,10 +740,10 @@ packages: dependency: transitive description: name: state_notifier - sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb url: "https://pub.dev" source: hosted - version: "0.7.2+1" + version: "1.0.0" stream_channel: dependency: transitive description: @@ -828,10 +828,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "78cb6dea3e93148615109e58e42c35d1ffbf5ef66c44add673d0ab75f12ff3af" + sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025" url: "https://pub.dev" source: hosted - version: "6.0.37" + version: "6.0.38" url_launcher_ios: dependency: transitive description: @@ -980,10 +980,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff + sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" xml: dependency: transitive description: From 33eccbb53ca69b9eb233e22d14a09669d6813ceb Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 22 Aug 2023 11:06:03 +0200 Subject: [PATCH 103/158] Avoid catching InvalidPinError as ValueError. --- helper/helper/base.py | 3 +++ helper/helper/piv.py | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/helper/helper/base.py b/helper/helper/base.py index ca2a940f..78b9b653 100644 --- a/helper/helper/base.py +++ b/helper/helper/base.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from yubikit.core import InvalidPinError from functools import partial import logging @@ -123,6 +124,8 @@ class RpcNode: except ChildResetException as e: self._close_child() raise StateResetException(e.message, traversed) + except InvalidPinError: + raise # Prevent catching this as a ValueError below except ValueError as e: raise InvalidParametersException(e) raise NoSuchActionException(action) diff --git a/helper/helper/piv.py b/helper/helper/piv.py index a6ae5511..9a9b72f6 100644 --- a/helper/helper/piv.py +++ b/helper/helper/piv.py @@ -21,13 +21,12 @@ from .base import ( TimeoutException, AuthRequiredException, ) -from yubikit.core import NotSupportedError, BadResponseError +from yubikit.core import NotSupportedError, BadResponseError, InvalidPinError from yubikit.core.smartcard import ApduError, SW from yubikit.piv import ( PivSession, OBJECT_ID, MANAGEMENT_KEY_TYPE, - InvalidPinError, SLOT, require_version, KEY_TYPE, From cd006085a6ffd4255bdb46da0ca0fd2bd30ecaa7 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 22 Aug 2023 11:06:37 +0200 Subject: [PATCH 104/158] PIV: Validate subject. --- helper/helper/piv.py | 45 +++++++++++------- lib/desktop/piv/state.dart | 12 +++-- lib/l10n/app_en.arb | 1 + lib/piv/state.dart | 1 + lib/piv/views/generate_key_dialog.dart | 64 ++++++++++++++++---------- 5 files changed, 79 insertions(+), 44 deletions(-) diff --git a/helper/helper/piv.py b/helper/helper/piv.py index 9a9b72f6..5d7390c1 100644 --- a/helper/helper/piv.py +++ b/helper/helper/piv.py @@ -42,6 +42,7 @@ from ykman.piv import ( generate_self_signed_certificate, generate_csr, generate_chuid, + parse_rfc4514_string, ) from ykman.util import ( parse_certificates, @@ -233,6 +234,30 @@ class PivNode(RpcNode): def slots(self): return SlotsNode(self.session) + @action(closes_child=False) + def examine_file(self, params, event, signal): + data = bytes.fromhex(params.pop("data")) + password = params.pop("password", None) + try: + private_key, certs = _parse_file(data, password) + return dict( + status=True, + password=password is not None, + private_key=bool(private_key), + certificates=len(certs), + ) + except InvalidPasswordError: + logger.debug("Invalid or missing password", exc_info=True) + return dict(status=False) + + @action(closes_child=False) + def validate_rfc4514(self, params, event, signal): + try: + parse_rfc4514_string(params.pop("data")) + return dict(status=True) + except ValueError: + return dict(status=False) + def _slot_for(name): return SLOT(int(name, base=16)) @@ -310,22 +335,6 @@ class SlotsNode(RpcNode): return SlotNode(self.session, slot, metadata, certificate, self.refresh) return super().create_child(name) - @action - def examine_file(self, params, event, signal): - data = bytes.fromhex(params.pop("data")) - password = params.pop("password", None) - try: - private_key, certs = _parse_file(data, password) - return dict( - status=True, - password=password is not None, - private_key=bool(private_key), - certificates=len(certs), - ) - except InvalidPasswordError: - logger.debug("Invalid or missing password", exc_info=True) - return dict(status=False) - class SlotNode(RpcNode): def __init__(self, session, slot, metadata, certificate, refresh): @@ -413,7 +422,9 @@ class SlotNode(RpcNode): pin_policy = PIN_POLICY(params.pop("pin_policy", PIN_POLICY.DEFAULT)) touch_policy = TOUCH_POLICY(params.pop("touch_policy", TOUCH_POLICY.DEFAULT)) subject = params.pop("subject") - generate_type = GENERATE_TYPE(params.pop("generate_type", GENERATE_TYPE.CERTIFICATE)) + generate_type = GENERATE_TYPE( + params.pop("generate_type", GENERATE_TYPE.CERTIFICATE) + ) public_key = self.session.generate_key( self.slot, key_type, pin_policy, touch_policy ) diff --git a/lib/desktop/piv/state.dart b/lib/desktop/piv/state.dart index c5f86b57..955a4743 100644 --- a/lib/desktop/piv/state.dart +++ b/lib/desktop/piv/state.dart @@ -380,9 +380,7 @@ class _DesktopPivSlotsNotifier extends PivSlotsNotifier { @override Future examine(String data, {String? password}) async { - final result = await _session.command('examine_file', target: [ - 'slots', - ], params: { + final result = await _session.command('examine_file', params: { 'data': data, 'password': password, }); @@ -394,6 +392,14 @@ class _DesktopPivSlotsNotifier extends PivSlotsNotifier { } } + @override + Future validateRfc4514(String value) async { + final result = await _session.command('validate_rfc4514', params: { + 'data': value, + }); + return result['status']; + } + @override Future import(SlotId slot, String data, {String? password, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7434e1cc..d7da84a7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -463,6 +463,7 @@ "slot": {} } }, + "l_invalid_rfc4514": "Invalid RFC4514 string", "@_piv_slots": {}, "s_slot_display_name": "{name} ({hexid})", diff --git a/lib/piv/state.dart b/lib/piv/state.dart index 8ebb628c..ec3361b5 100644 --- a/lib/piv/state.dart +++ b/lib/piv/state.dart @@ -50,6 +50,7 @@ final pivSlotsProvider = AsyncNotifierProvider.autoDispose abstract class PivSlotsNotifier extends AutoDisposeFamilyAsyncNotifier, DevicePath> { Future examine(String data, {String? password}); + Future validateRfc4514(String value); Future<(SlotMetadata?, String?)> read(SlotId slot); Future generate( SlotId slot, diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart index 4399427b..2329b220 100644 --- a/lib/piv/views/generate_key_dialog.dart +++ b/lib/piv/views/generate_key_dialog.dart @@ -42,6 +42,7 @@ class GenerateKeyDialog extends ConsumerStatefulWidget { class _GenerateKeyDialogState extends ConsumerState { String _subject = ''; + bool _invalidSubject = true; GenerateType _generateType = defaultGenerateType; KeyType _keyType = defaultKeyType; late DateTime _validFrom; @@ -71,36 +72,47 @@ class _GenerateKeyDialogState extends ConsumerState { actions: [ TextButton( key: keys.saveButton, - onPressed: _generating || _subject.isEmpty + onPressed: _generating || _invalidSubject ? null : () async { setState(() { _generating = true; }); - Function()? close; + final pivNotifier = + ref.read(pivSlotsProvider(widget.devicePath).notifier); + final withContext = ref.read(withContextProvider); + + if (!await pivNotifier.validateRfc4514(_subject)) { + setState(() { + _generating = false; + }); + _invalidSubject = true; + return; + } + + void Function()? close; final PivGenerateResult result; try { - close = showMessage( - context, - l10n.l_generating_private_key, - duration: const Duration(seconds: 30), + close = await withContext( + (context) async => showMessage( + context, + l10n.l_generating_private_key, + duration: const Duration(seconds: 30), + )); + result = await pivNotifier.generate( + widget.pivSlot.slot, + _keyType, + parameters: switch (_generateType) { + GenerateType.certificate => + PivGenerateParameters.certificate( + subject: _subject, + validFrom: _validFrom, + validTo: _validTo), + GenerateType.csr => + PivGenerateParameters.csr(subject: _subject), + }, ); - result = await ref - .read(pivSlotsProvider(widget.devicePath).notifier) - .generate( - widget.pivSlot.slot, - _keyType, - parameters: switch (_generateType) { - GenerateType.certificate => - PivGenerateParameters.certificate( - subject: _subject, - validFrom: _validFrom, - validTo: _validTo), - GenerateType.csr => - PivGenerateParameters.csr(subject: _subject), - }, - ); } finally { close?.call(); } @@ -127,17 +139,21 @@ class _GenerateKeyDialogState extends ConsumerState { autofocus: true, key: keys.subjectField, decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.s_subject, - ), + border: const OutlineInputBorder(), + labelText: l10n.s_subject, + errorText: _subject.isNotEmpty && _invalidSubject + ? l10n.l_invalid_rfc4514 + : null), textInputAction: TextInputAction.next, enabled: !_generating, onChanged: (value) { setState(() { if (value.isEmpty) { _subject = ''; + _invalidSubject = true; } else { _subject = value.contains('=') ? value : 'CN=$value'; + _invalidSubject = false; } }); }, From 567cc5284da8364929899f2e0a6c95fa2488ebd4 Mon Sep 17 00:00:00 2001 From: Daviteusz Date: Tue, 22 Aug 2023 11:54:44 +0200 Subject: [PATCH 105/158] Update Polish translations --- lib/l10n/app_pl.arb | 1232 +++++++++++++++++++++++++++++-------------- 1 file changed, 824 insertions(+), 408 deletions(-) diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 2d31918b..644efc5b 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1,445 +1,861 @@ { "@@locale": "pl", - - "@_readme": { - "notes": [ - "Wszystkie ciągi zaczynają się od wielkiej litery.", - "Pogrupuj ciągi znaków według kategorii, ale nie łącz ich nadmiernie z daną sekcją aplikacji, jeżeli mogą być ponownie wykorzystane w kilku różnych sekcjach.", - "Uruchom check_strings.py na pliku .arb, aby wykryć problemy, dostosuj @_lint_rules zgodnie z potrzebami dla danego języka." - ], - "prefixes": { - "s_": "Jedno lub kilka słów. Powinny być na tyle krótkie, aby można je było wyświetlić na przycisku lub w nagłówku.", - "l_": "Pojedyncza linia może być zawinięta. Nie powinna być dłuższa niż jedno zdanie i nie powinna kończyć się kropką.", - "p_": "Jedno lub więcej pełnych zdań z odpowiednią interpunkcją.", - "q_": "Pytania kończą się znakiem zapytania." - } - }, - - "@_lint_rules": { - "s_max_words": 4, - "s_max_length": 32 - }, - - "app_name": "Yubico Authenticator", - - "s_save": "Zapisz", - "s_cancel": "Anuluj", - "s_close": "Zamknij", - "s_delete": "Usuń", - "s_quit": "Wyjdź", - "s_unlock": "Odblokuj", - "s_calculate": "Oblicz", - "s_label": "Etykieta", - "s_name": "Nazwa", - "s_usb": "USB", - "s_nfc": "NFC", - "s_show_window": "Pokaż okno", - "s_hide_window": "Ukryj okno", - "q_rename_target": "Zmienić nazwę {label}?", - "@q_rename_target" : { + "@app_name": {}, + "@l_account": { "placeholders": { "label": {} } }, - - "s_about": "O aplikacji", - "s_appearance": "Wygląd", - "s_authenticator": "Authenticator", - "s_manage": "Zarządzaj", - "s_setup": "Konfiguruj", - "s_settings": "Ustawienia", - "s_webauthn": "WebAuthn", - "s_help_and_about": "Pomoc i informacje", - "s_help_and_feedback": "Pomoc i opinie", - "s_send_feedback": "Prześlij opinię", - "s_i_need_help": "Pomoc", - "s_troubleshooting": "Rozwiązywanie problemów", - "s_terms_of_use": "Warunki użytkowania", - "s_privacy_policy": "Polityka prywatności", - "s_open_src_licenses": "Licencje open source", - "s_configure_yk": "Skonfiguruj YubiKey", - "s_please_wait": "Proszę czekać\u2026", - "s_secret_key": "Tajny klucz", - "s_invalid_length": "Nieprawidłowa długość", - "s_require_touch": "Wymagaj dotknięcia", - "q_have_account_info": "Masz dane konta?", - "s_run_diagnostics": "Uruchom diagnostykę", - "s_log_level": "Poziom logowania: {level}", - "@s_log_level": { - "placeholders": { - "level": {} - } - }, - "s_character_count": "Liczba znaków", - "s_learn_more": "Dowiedz się więcej", - - "@_language": {}, - "s_language": "Język", - "l_enable_community_translations": "Włącz tłumaczenia społecznościowe", - "p_community_translations_desc": "Tłumaczenia te są dostarczane i utrzymywane przez społeczność. Mogą zawierać błędy lub być niekompletne.", - - "@_theme": {}, - "s_app_theme": "Motyw aplikacji", - "s_choose_app_theme": "Wybierz motyw aplikacji", - "s_system_default": "Zgodny z systemem", - "s_light_mode": "Jasny", - "s_dark_mode": "Ciemny", - - "@_yubikey_selection": {}, - "s_yk_information": "Informacja o YubiKey", - "s_select_yk": "Wybierz YubiKey", - "s_select_to_scan": "Wybierz, aby skanować", - "s_hide_device": "Ukryj urządzenie", - "s_show_hidden_devices": "Pokaż ukryte urządzenia", - "s_sn_serial": "S/N: {serial}", - "@s_sn_serial" : { - "placeholders": { - "serial": {} - } - }, - "s_fw_version": "F/W: {version}", - "@s_fw_version" : { - "placeholders": { - "version": {} - } - }, - - "@_yubikey_interactions": {}, - "l_insert_yk": "Podłącz klucz YubiKey", - "l_insert_or_tap_yk": "Podłącz lub przystaw YubiKey", - "l_unplug_yk": "Odłącz klucz YubiKey", - "l_reinsert_yk": "Ponownie podłącz YubiKey", - "l_place_on_nfc_reader": "Przyłóż klucz YubiKey do czytnika NFC", - "l_replace_yk_on_reader": "Umieść klucz YubiKey z powrotem na czytniku", - "l_remove_yk_from_reader": "Odsuń klucz YubiKey od czytnika NFC", - "p_try_reinsert_yk": "Spróbuj ponownie podłączyć klucz YubiKey.", - "s_touch_required": "Wymagane dotknięcie", - "l_touch_button_now": "Dotknij teraz przycisku na kluczu YubiKey", - "l_keep_touching_yk": "Wielokrotnie dotykaj klucza YubiKey\u2026", - - "@_app_configuration": {}, - "s_toggle_applications": "Przełączanie funkcji", - "l_min_one_interface": "Przynajmniej jeden interfejs musi być włączony", - "s_reconfiguring_yk": "Rekonfigurowanie YubiKey\u2026", - "s_config_updated": "Zaktualizowano konfigurację", - "l_config_updated_reinsert": "Zaktualizowano konfigurację, podłącz ponownie klucz YubiKey", - "s_app_not_supported": "Funkcja nie jest obsługiwana", - "l_app_not_supported_on_yk": "Używany klucz YubiKey nie obsługuje funkcji „{app}”", - "@l_app_not_supported_on_yk" : { - "placeholders": { - "app": {} - } - }, - "l_app_not_supported_desc": "Ta funkcja nie jest obsługiwana", - "s_app_disabled": "Wyłączona funkcja", - "l_app_disabled_desc": "Włącz funkcję „{app}” w kluczu YubiKey, aby uzyskać dostęp", - "@l_app_disabled_desc" : { - "placeholders": { - "app": {} - } - }, - "s_fido_disabled": "FIDO2 wyłączone", - "l_webauthn_req_fido2": "WebAuthn wymaga włączenia funkcji FIDO2 w kluczu YubiKey", - - "@_connectivity_issues": {}, - "l_helper_not_responding": "Proces pomocnika nie odpowiada", - "l_yk_no_access": "Dostęp do tego klucza YubiKey jest niemożliwy", - "s_yk_inaccessible": "Urządzenie niedostępne", - "l_open_connection_failed": "Nie udało się nawiązać połączenia", - "l_ccid_connection_failed": "Nie udało się nawiązać połączenia z kartą inteligentną", - "p_ccid_service_unavailable": "Upewnij się, że usługa kart inteligentnych działa.", - "p_pcscd_unavailable": "Upewnij się, że pcscd jest zainstalowany i uruchomiony.", - "l_no_yk_present": "Klucz YubiKey nie jest obecny", - "s_unknown_type": "Nieznany typ", - "s_unknown_device": "Nierozpoznane urządzenie", - "s_unsupported_yk": "Nieobsługiwany klucz YubiKey", - "s_yk_not_recognized": "Urządzenie nie rozpoznane", - - "@_general_errors": {}, - "l_error_occured": "Wystąpił błąd", - "s_application_error": "Błąd funkcji", - "l_import_error": "Błąd importowania", - "l_file_not_found": "Nie odnaleziono pliku", - "l_file_too_big": "Zbyt duży rozmiar pliku", - "l_filesystem_error": "Błąd operacji systemu plików", - - "@_pins": {}, - "s_pin": "PIN", - "s_set_pin": "Ustaw PIN", - "s_change_pin": "Zmień PIN", - "s_current_pin": "Aktualny PIN", - "s_new_pin": "Nowy PIN", - "s_confirm_pin": "Potwierdź PIN", - "l_new_pin_len": "Nowy PIN musi mieć co najmniej {length} znaków", - "@l_new_pin_len" : { - "placeholders": { - "length": {} - } - }, - "s_pin_set": "PIN ustawiony", - "l_set_pin_failed": "Nie udało się ustawić kodu PIN: {message}", - "@l_set_pin_failed" : { + "@l_account_add_failed": { "placeholders": { "message": {} } }, - "l_wrong_pin_attempts_remaining": "Błędny PIN, pozostało {retries} prób", - "@l_wrong_pin_attempts_remaining" : { + "@l_account_already_exists": {}, + "@l_account_name_required": {}, + "@l_accounts_used": { + "placeholders": { + "capacity": {}, + "used": {} + } + }, + "@l_add_one_or_more_fps": {}, + "@l_app_disabled_desc": { + "placeholders": { + "app": {} + } + }, + "@l_app_not_supported_desc": {}, + "@l_app_not_supported_on_yk": { + "placeholders": { + "app": {} + } + }, + "@l_attempts_remaining": { "placeholders": { "retries": {} } }, - "s_fido_pin_protection": "Ochrona FIDO kodem PIN", - "l_fido_pin_protection_optional": "Opcjonalna ochrona FIDO kodem PIN", - "l_enter_fido2_pin": "Wprowadź kod PIN FIDO2 dla klucza YubiKey", - "l_optionally_set_a_pin": "Opcjonalnie ustaw PIN, aby chronić dostęp do YubiKey\nZarejestruj jako klucz bezpieczeństwa na stronach internetowych", - "l_pin_blocked_reset": "PIN jest zablokowany; przywróć ustawienia fabryczne funkcji FIDO", - "l_set_pin_first": "Najpierw wymagany jest kod PIN", - "l_unlock_pin_first": "Najpierw odblokuj kodem PIN", - "l_pin_soft_locked": "PIN został zablokowany do momentu ponownego podłączenia klucza YubiKey", - "p_enter_current_pin_or_reset": "Wprowadź aktualny kod PIN. Jeśli nie znasz kodu PIN, musisz zresetować klucz YubiKey.", - "p_enter_new_fido2_pin": "Wprowadź nowy PIN. Kod PIN musi mieć co najmniej {length} znaków i może zawierać litery, cyfry i znaki specjalne.", - "@p_enter_new_fido2_pin" : { + "@l_bullet": { "placeholders": { - "length": {} + "item": {} } }, - - "@_passwords": {}, - "s_password": "Hasło", - "s_manage_password": "Zarządzaj hasłem", - "s_set_password": "Ustaw hasło", - "s_password_set": "Hasło zostało ustawione", - "l_optional_password_protection": "Opcjonalna ochrona hasłem", - "s_new_password": "Nowe hasło", - "s_current_password": "Aktualne hasło", - "s_confirm_password": "Potwierdź hasło", - "s_wrong_password": "Błędne hasło", - "s_remove_password": "Usuń hasło", - "s_password_removed": "Hasło zostało usunięte", - "s_remember_password": "Zapamiętaj hasło", - "s_clear_saved_password": "Usuń zapisane hasło", - "s_password_forgotten": "Hasło zostało zapomniane", - "l_keystore_unavailable": "Magazyn kluczy systemu operacyjnego jest niedostępny", - "l_remember_pw_failed": "Nie udało się zapamiętać hasła", - "l_unlock_first": "Najpierw odblokuj hasłem", - "l_enter_oath_pw": "Wprowadź hasło OATH dla klucza YubiKey", - "p_enter_current_password_or_reset": "Wprowadź aktualne hasło. Jeśli nie znasz hasła, musisz zresetować klucz YubiKey.", - "p_enter_new_password": "Wprowadź nowe hasło. Hasło może zawierać litery, cyfry i znaki specjalne.", - - "@_oath_accounts": {}, - "l_account": "Konto: {label}", - "@l_account" : { + "@l_bypass_touch_requirement": {}, + "@l_bypass_touch_requirement_off": {}, + "@l_bypass_touch_requirement_on": {}, + "@l_calculate_code_desc": {}, + "@l_ccid_connection_failed": {}, + "@l_certificate_deleted": {}, + "@l_certificate_exported": {}, + "@l_change_management_key": {}, + "@l_code_copied_clipboard": {}, + "@l_config_updated_reinsert": {}, + "@l_copy_code_desc": {}, + "@l_copy_otp_clipboard": {}, + "@l_copy_to_clipboard": {}, + "@l_default_key_used": {}, + "@l_delete_account_desc": {}, + "@l_delete_certificate": {}, + "@l_delete_certificate_desc": {}, + "@l_delete_fingerprint_desc": {}, + "@l_delete_passkey_desc": {}, + "@l_diagnostics_copied": {}, + "@l_elevating_permissions": {}, + "@l_enable_community_translations": {}, + "@l_enter_fido2_pin": {}, + "@l_enter_oath_pw": {}, + "@l_error_occured": {}, + "@l_export_certificate": {}, + "@l_export_certificate_desc": {}, + "@l_export_certificate_file": {}, + "@l_export_csr_file": {}, + "@l_factory_reset_this_app": {}, + "@l_fido_app_reset": {}, + "@l_fido_pin_protection_optional": {}, + "@l_file_not_found": {}, + "@l_file_too_big": {}, + "@l_filesystem_error": {}, + "@l_fingerprint": { "placeholders": { "label": {} } }, - "s_accounts": "Konta", - "s_no_accounts": "Brak kont", - "s_add_account": "Dodaj konto", - "s_account_added": "Konto zostało dodane", - "l_account_add_failed": "Nie udało się dodać konta: {message}", - "@l_account_add_failed" : { - "placeholders": { - "message": {} - } - }, - "l_account_name_required": "Twoje konto musi mieć nazwę", - "l_name_already_exists": "Ta nazwa już istnieje dla tego wydawcy", - "l_invalid_character_issuer": "Nieprawidłowy znak: „:” nie jest dozwolony w polu wydawcy", - "s_pinned": "Przypięte", - "s_pin_account": "Przypnij konto", - "s_unpin_account": "Odepnij konto", - "s_no_pinned_accounts": "Brak przypiętych kont", - "s_rename_account": "Zmień nazwę konta", - "s_account_renamed": "Zmieniono nazwę konta", - "p_rename_will_change_account_displayed": "Spowoduje to zmianę sposobu wyświetlania konta na liście.", - "s_delete_account": "Usuń konto", - "s_account_deleted": "Konto zostało usunięte", - "p_warning_delete_account": "Uwaga! Ta czynność spowoduje usunięcie konta z klucza YubiKey.", - "p_warning_disable_credential": "Nie będzie już możliwe generowanie OTP dla tego konta. Upewnij się, że najpierw wyłączono te dane uwierzytelniające w witrynie, aby uniknąć zablokowania konta.", - "s_account_name": "Nazwa konta", - "s_search_accounts": "Wyszukaj konta", - "l_accounts_used": "Użyto {used} z {capacity} kont", - "@l_accounts_used" : { - "placeholders": { - "used": {}, - "capacity": {} - } - }, - "s_num_digits": "{num} cyfr", - "@s_num_digits" : { - "placeholders": { - "num": {} - } - }, - "s_num_sec": "{num} sek", - "@s_num_sec" : { - "placeholders": { - "num": {} - } - }, - "s_issuer_optional": "Wydawca (opcjonalnie)", - "s_counter_based": "Na podstawie licznika", - "s_time_based": "Na podstawie czasu", - - "@_fido_credentials": {}, - "l_credential": "Poświadczenie: {label}", - "@l_credential" : { - "placeholders": { - "label": {} - } - }, - "s_credentials": "Poświadczenia", - "l_ready_to_use": "Gotowe do użycia", - "l_register_sk_on_websites": "Zarejestruj jako klucz bezpieczeństwa na stronach internetowych", - "l_no_discoverable_accounts": "Nie wykryto kont", - "s_delete_credential": "Usuń poświadczenie", - "s_credential_deleted": "Poświadczenie zostało usunięte", - "p_warning_delete_credential": "Spowoduje to usunięcie poświadczenia z klucza YubiKey.", - - "@_fingerprints": {}, - "l_fingerprint": "Odcisk palca: {label}", - "@l_fingerprint" : { - "placeholders": { - "label": {} - } - }, - "s_fingerprints": "Odciski palców", - "l_fingerprint_captured": "Odcisk palca zarejestrowany pomyślnie!", - "s_fingerprint_added": "Dodano odcisk palca", - "l_setting_name_failed": "Błąd ustawienia nazwy: {message}", - "@l_setting_name_failed" : { - "placeholders": { - "message": {} - } - }, - "s_add_fingerprint": "Dodaj odcisk palca", - "l_fp_step_1_capture": "Krok 1/2: Pobranie odcisku palca", - "l_fp_step_2_name": "Krok 2/2: Nazwij odcisk palca", - "s_delete_fingerprint": "Usuń odcisk palca", - "s_fingerprint_deleted": "Odcisk palca został usunięty", - "p_warning_delete_fingerprint": "Spowoduje to usunięcie odcisku palca z twojego YubiKey.", - "s_no_fingerprints": "Brak odcisków palców", - "l_set_pin_fingerprints": "Ustaw kod PIN, aby zarejestrować odciski palców", - "l_no_fps_added": "Nie dodano odcisków palców", - "s_rename_fp": "Zmień nazwę odcisku palca", - "s_fingerprint_renamed": "Zmieniono nazwę odcisku palca", - "l_rename_fp_failed": "Błąd zmiany nazwy: {message}", - "@l_rename_fp_failed" : { - "placeholders": { - "message": {} - } - }, - "l_add_one_or_more_fps": "Dodaj jeden lub więcej (do pięciu) odcisków palców", - "l_fingerprints_used": "Zarejestrowano {used}/5 odcisków palców", + "@l_fingerprint_captured": {}, "@l_fingerprints_used": { "placeholders": { "used": {} } }, - "p_press_fingerprint_begin": "Przytrzymaj palec na kluczu YubiKey, aby rozpocząć.", - "p_will_change_label_fp": "Spowoduje to zmianę etykiety odcisku palca.", - - "@_permissions": {}, - "s_enable_nfc": "Włącz NFC", - "s_permission_denied": "Odmowa dostępu", - "l_elevating_permissions": "Podnoszenie uprawnień\u2026", - "s_review_permissions": "Przegląd uprawnień", - "p_elevated_permissions_required": "Zarządzanie tym urządzeniem wymaga podwyższonych uprawnień.", - "p_webauthn_elevated_permissions_required": "Zarządzanie WebAuthn wymaga podwyższonych uprawnień.", - "p_need_camera_permission": "Yubico Authenticator wymaga dostępu do aparatu w celu skanowania kodów QR.", - - "@_qr_codes": {}, - "s_qr_scan": "Skanuj kod QR", - "l_qr_scanned": "Zeskanowany kod QR", - "l_invalid_qr": "Nieprawidłowy kod QR", - "l_qr_not_found": "Nie znaleziono kodu QR", - "l_qr_not_read": "Odczytanie kodu QR nie powiodło się: {message}", - "@l_qr_not_read" : { - "placeholders": { - "message": {} - } - }, - "l_point_camera_scan": "Skieruj aparat na kod QR, by go zeskanować", - "q_want_to_scan": "Czy chcesz zeskanować?", - "q_no_qr": "Nie masz kodu QR?", - "s_enter_manually": "Wprowadź ręcznie", - - "@_factory_reset": {}, - "s_reset": "Zresetuj", - "s_factory_reset": "Ustawienia fabryczne", - "l_factory_reset_this_app": "Przywróć ustawienia fabryczne tej funkcji", - "s_reset_oath": "Zresetuj OATH", - "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": { - "message": {} - } - }, - "p_warning_factory_reset": "Uwaga! Spowoduje to nieodwracalne usunięcie wszystkich kont OATH TOTP/HOTP z klucza YubiKey.", - "p_warning_disable_credentials": "Twoje poświadczenia OATH, jak również wszelkie ustawione hasła, zostaną usunięte z tego klucza YubiKey. Upewnij się, że najpierw wyłączono je w odpowiednich witrynach internetowych, aby uniknąć zablokowania kont.", - "p_warning_deletes_accounts": "Uwaga! Spowoduje to nieodwracalne usunięcie wszystkich kont U2F i FIDO2 z klucza YubiKey.", - "p_warning_disable_accounts": "Twoje poświadczenia, a także wszelkie ustawione kody PIN, zostaną usunięte z tego klucza YubiKey. Upewnij się, że najpierw wyłączono je w odpowiednich witrynach internetowych, aby uniknąć zablokowania kont.", - - "@_copy_to_clipboard": {}, - "l_copy_to_clipboard": "Skopiuj do schowka", - "s_code_copied": "Kod skopiowany", - "l_code_copied_clipboard": "Kod skopiowany do schowka", - "s_copy_log": "Kopiuj logi", - "l_log_copied": "Logi skopiowane do schowka", - "l_diagnostics_copied": "Dane diagnostyczne skopiowane do schowka", - "p_target_copied_clipboard": "{label} skopiowano do schowka.", - "@p_target_copied_clipboard" : { - "placeholders": { - "label": {} - } - }, - - "@_custom_icons": {}, - "s_custom_icons": "Niestandardowe ikony", - "l_set_icons_for_accounts": "Ustaw ikony dla kont", - "p_custom_icons_description": "Pakiety ikon mogą sprawić, że Twoje konta będą łatwiejsze do odróżnienia dzięki znanym logo i kolorom.", - "s_replace_icon_pack": "Zastąp pakiet ikon", - "l_loading_icon_pack": "Wczytywanie pakietu ikon\u2026", - "s_load_icon_pack": "Wczytaj pakiet ikon", - "s_remove_icon_pack": "Usuń pakiet ikon", - "l_icon_pack_removed": "Usunięto pakiet ikon", - "l_remove_icon_pack_failed": "Błąd podczas usuwania pakietu ikon", - "s_choose_icon_pack": "Wybierz pakiet ikon", - "l_icon_pack_imported": "Zaimportowano pakiet ikon", - "l_import_icon_pack_failed": "Błąd importu pakietu ikon: {message}", + "@l_fp_step_1_capture": {}, + "@l_fp_step_2_name": {}, + "@l_generate_desc": {}, + "@l_generating_private_key": {}, + "@l_helper_not_responding": {}, + "@l_icon_pack_copy_failed": {}, + "@l_icon_pack_imported": {}, + "@l_icon_pack_removed": {}, + "@l_import_desc": {}, + "@l_import_error": {}, + "@l_import_file": {}, "@l_import_icon_pack_failed": { "placeholders": { "message": {} } }, - "l_invalid_icon_pack": "Nieprawidłowy pakiet ikon", - "l_icon_pack_copy_failed": "Nie udało się skopiować plików z pakietu ikon", - - "@_android_settings": {}, - "s_nfc_options": "Opcje NFC", - "l_on_yk_nfc_tap": "Podczas kontaktu YubiKey z NFC", - "l_launch_ya": "Uruchom Yubico Authenticator", - "l_copy_otp_clipboard": "Skopiuj OTP do schowka", - "l_launch_and_copy_otp": "Uruchom aplikację i skopiuj OTP", - "l_kbd_layout_for_static": "Układ klawiatury (dla hasła statycznego)", - "s_choose_kbd_layout": "Wybierz układ klawiatury", + "@l_insert_or_tap_yk": {}, + "@l_insert_yk": {}, + "@l_invalid_character_issuer": {}, + "@l_invalid_icon_pack": {}, + "@l_invalid_qr": {}, + "@l_kbd_layout_for_static": {}, + "@l_keep_touching_yk": {}, + "@l_key_no_certificate": {}, + "@l_keystore_unavailable": {}, + "@l_launch_and_copy_otp": {}, + "@l_launch_app_on_usb": {}, + "@l_launch_app_on_usb_off": {}, + "@l_launch_app_on_usb_on": {}, + "@l_launch_ya": {}, + "@l_loading_icon_pack": {}, + "@l_log_copied": {}, + "@l_management_key_changed": {}, + "@l_min_one_interface": {}, + "@l_name_already_exists": {}, + "@l_new_pin_len": { + "placeholders": { + "length": {} + } + }, + "@l_no_certificate": {}, + "@l_no_discoverable_accounts": {}, + "@l_no_fps_added": {}, + "@l_no_yk_present": {}, + "@l_oath_application_reset": {}, + "@l_on_yk_nfc_tap": {}, + "@l_open_connection_failed": {}, + "@l_optional_password_protection": {}, + "@l_optionally_set_a_pin": {}, + "@l_passkey": { + "placeholders": { + "label": {} + } + }, + "@l_pin_account_desc": {}, + "@l_pin_blocked_reset": {}, + "@l_pin_protected_key": {}, + "@l_pin_soft_locked": {}, + "@l_piv_app_reset": {}, + "@l_piv_pin_blocked": {}, + "@l_piv_pin_puk_blocked": {}, + "@l_place_on_nfc_reader": {}, + "@l_point_camera_scan": {}, + "@l_press_reset_to_begin": {}, + "@l_qr_not_found": {}, + "@l_qr_not_read": { + "placeholders": { + "message": {} + } + }, + "@l_qr_scanned": {}, + "@l_ready_to_use": {}, + "@l_register_sk_on_websites": {}, + "@l_reinsert_yk": {}, + "@l_remember_pw_failed": {}, + "@l_remove_icon_pack_failed": {}, + "@l_remove_yk_from_reader": {}, + "@l_rename_account_desc": {}, + "@l_rename_fp_desc": {}, + "@l_rename_fp_failed": { + "placeholders": { + "message": {} + } + }, + "@l_replace_yk_on_reader": {}, + "@l_reset_failed": { + "placeholders": { + "message": {} + } + }, + "@l_select_accounts": {}, + "@l_select_import_file": {}, + "@l_set_icons_for_accounts": {}, + "@l_set_pin_failed": { + "placeholders": { + "message": {} + } + }, + "@l_set_pin_fingerprints": {}, + "@l_set_pin_first": {}, + "@l_setting_name_failed": { + "placeholders": { + "message": {} + } + }, + "@l_silence_nfc_sounds_off": {}, + "@l_silence_nfc_sounds_on": {}, + "@l_touch_button_now": {}, + "@l_unlock_first": {}, + "@l_unlock_pin_first": {}, + "@l_unlock_piv_management": {}, + "@l_unplug_yk": {}, + "@l_warning_default_key": {}, + "@l_webauthn_req_fido2": {}, + "@l_wrong_key": {}, + "@l_wrong_pin_attempts_remaining": { + "placeholders": { + "retries": {} + } + }, + "@l_wrong_puk_attempts_remaining": { + "placeholders": { + "retries": {} + } + }, + "@l_yk_no_access": {}, + "@p_add_description": {}, + "@p_ccid_service_unavailable": {}, + "@p_change_management_key_desc": {}, + "@p_community_translations_desc": {}, + "@p_custom_icons_description": {}, + "@p_elevated_permissions_required": {}, + "@p_enter_current_password_or_reset": {}, + "@p_enter_current_pin_or_reset": {}, + "@p_enter_current_puk_or_reset": {}, + "@p_enter_new_fido2_pin": { + "placeholders": { + "length": {} + } + }, + "@p_enter_new_password": {}, + "@p_enter_new_piv_pin_puk": { + "placeholders": { + "name": {} + } + }, + "@p_generate_desc": { + "placeholders": { + "slot": {} + } + }, + "@p_import_items_desc": { + "placeholders": { + "slot": {} + } + }, + "@p_need_camera_permission": {}, + "@p_password_protected_file": {}, + "@p_pcscd_unavailable": {}, + "@p_pin_required_desc": {}, + "@p_press_fingerprint_begin": {}, + "@p_rename_will_change_account_displayed": {}, + "@p_target_copied_clipboard": { + "placeholders": { + "label": {} + } + }, + "@p_try_reinsert_yk": {}, + "@p_unlock_piv_management_desc": {}, + "@p_warning_delete_account": {}, + "@p_warning_delete_certificate": {}, + "@p_warning_delete_fingerprint": {}, + "@p_warning_delete_passkey": {}, + "@p_warning_deletes_accounts": {}, + "@p_warning_disable_accounts": {}, + "@p_warning_disable_credential": {}, + "@p_warning_disable_credentials": {}, + "@p_warning_factory_reset": {}, + "@p_warning_piv_reset": {}, + "@p_warning_piv_reset_desc": {}, + "@p_webauthn_elevated_permissions_required": {}, + "@p_will_change_label_fp": {}, + "@q_delete_certificate_confirm": { + "placeholders": { + "slot": {} + } + }, + "@q_have_account_info": {}, + "@q_no_qr": {}, + "@q_rename_target": { + "placeholders": { + "label": {} + } + }, + "@q_want_to_scan": {}, + "@s_about": {}, + "@s_account_added": {}, + "@s_account_deleted": {}, + "@s_account_name": {}, + "@s_account_renamed": {}, + "@s_accounts": {}, + "@s_actions": {}, + "@s_add_account": {}, + "@s_add_accounts": {}, + "@s_add_fingerprint": {}, + "@s_add_manually": {}, + "@s_allow_screenshots": {}, + "@s_app_disabled": {}, + "@s_app_not_supported": {}, + "@s_app_theme": {}, + "@s_appearance": {}, + "@s_application_error": {}, + "@s_authenticator": {}, + "@s_calculate": {}, + "@s_calculate_code": {}, + "@s_cancel": {}, + "@s_certificate": {}, + "@s_certificate_fingerprint": {}, + "@s_certificates": {}, + "@s_change_pin": {}, + "@s_change_puk": {}, + "@s_character_count": {}, + "@s_choose_app_theme": {}, + "@s_choose_icon_pack": {}, + "@s_choose_kbd_layout": {}, + "@s_clear_saved_password": {}, + "@s_close": {}, + "@s_code_copied": {}, + "@s_config_updated": {}, + "@s_configure_yk": {}, + "@s_confirm_password": {}, + "@s_confirm_pin": {}, + "@s_confirm_puk": {}, + "@s_copy_log": {}, + "@s_counter_based": {}, + "@s_csr": {}, + "@s_current_management_key": {}, + "@s_current_password": {}, + "@s_current_pin": {}, + "@s_current_puk": {}, + "@s_custom_icons": {}, + "@s_dark_mode": {}, + "@s_definition": { + "placeholders": { + "item": {} + } + }, + "@s_delete": {}, + "@s_delete_account": {}, + "@s_delete_fingerprint": {}, + "@s_delete_passkey": {}, + "@s_enable_nfc": {}, + "@s_enter_manually": {}, + "@s_factory_reset": {}, + "@s_fido_disabled": {}, + "@s_fido_pin_protection": {}, + "@s_fingerprint_added": {}, + "@s_fingerprint_deleted": {}, + "@s_fingerprint_renamed": {}, + "@s_fingerprints": {}, + "@s_fw_version": { + "placeholders": { + "version": {} + } + }, + "@s_generate_key": {}, + "@s_help_and_about": {}, + "@s_help_and_feedback": {}, + "@s_hide_device": {}, + "@s_hide_window": {}, + "@s_i_need_help": {}, + "@s_import": {}, + "@s_invalid_length": {}, + "@s_issuer": {}, + "@s_issuer_optional": {}, + "@s_label": {}, + "@s_language": {}, + "@s_learn_more": {}, + "@s_light_mode": {}, + "@s_load_icon_pack": {}, + "@s_log_level": { + "placeholders": { + "level": {} + } + }, + "@s_manage": {}, + "@s_manage_password": {}, + "@s_management_key": {}, + "@s_name": {}, + "@s_new_management_key": {}, + "@s_new_password": {}, + "@s_new_pin": {}, + "@s_new_puk": {}, + "@s_nfc": {}, + "@s_nfc_dialog_oath_add_account": {}, + "@s_nfc_dialog_oath_add_multiple_accounts": {}, + "@s_nfc_dialog_oath_calculate_code": {}, + "@s_nfc_dialog_oath_delete_account": {}, + "@s_nfc_dialog_oath_failure": {}, + "@s_nfc_dialog_oath_rename_account": {}, + "@s_nfc_dialog_oath_reset": {}, + "@s_nfc_dialog_oath_set_password": {}, + "@s_nfc_dialog_oath_unlock": {}, + "@s_nfc_dialog_oath_unset_password": {}, + "@s_nfc_dialog_operation_failed": {}, + "@s_nfc_dialog_operation_success": {}, + "@s_nfc_dialog_tap_key": {}, + "@s_nfc_options": {}, + "@s_no_accounts": {}, + "@s_no_fingerprints": {}, + "@s_no_pinned_accounts": {}, + "@s_num_digits": { + "placeholders": { + "num": {} + } + }, + "@s_num_sec": { + "placeholders": { + "num": {} + } + }, + "@s_open_src_licenses": {}, + "@s_passkey_deleted": {}, + "@s_passkeys": {}, + "@s_password": {}, + "@s_password_forgotten": {}, + "@s_password_removed": {}, + "@s_password_set": {}, + "@s_permission_denied": {}, + "@s_pin": {}, + "@s_pin_account": {}, + "@s_pin_required": {}, + "@s_pin_set": {}, + "@s_pinned": {}, + "@s_piv": {}, + "@s_please_wait": {}, + "@s_privacy_policy": {}, + "@s_private_key": {}, + "@s_private_key_generated": {}, + "@s_protect_key": {}, + "@s_puk": {}, + "@s_puk_set": {}, + "@s_qr_scan": {}, + "@s_quit": {}, + "@s_reconfiguring_yk": {}, + "@s_remember_password": {}, + "@s_remove_icon_pack": {}, + "@s_remove_password": {}, + "@s_rename_account": {}, + "@s_rename_fp": {}, + "@s_replace_icon_pack": {}, + "@s_require_touch": {}, + "@s_reset": {}, + "@s_reset_fido": {}, + "@s_reset_oath": {}, + "@s_reset_piv": {}, + "@s_review_permissions": {}, + "@s_run_diagnostics": {}, + "@s_save": {}, + "@s_search_accounts": {}, + "@s_secret_key": {}, + "@s_select_to_scan": {}, + "@s_select_yk": {}, + "@s_send_feedback": {}, + "@s_serial": {}, + "@s_set_password": {}, + "@s_set_pin": {}, + "@s_settings": {}, + "@s_setup": {}, + "@s_show_hidden_devices": {}, + "@s_show_window": {}, + "@s_silence_nfc_sounds": {}, + "@s_slot_9a": {}, + "@s_slot_9c": {}, + "@s_slot_9d": {}, + "@s_slot_9e": {}, + "@s_slot_display_name": { + "placeholders": { + "hexid": {}, + "name": {} + } + }, + "@s_sn_serial": { + "placeholders": { + "serial": {} + } + }, + "@s_subject": {}, + "@s_system_default": {}, + "@s_terms_of_use": {}, + "@s_time_based": {}, + "@s_toggle_applications": {}, + "@s_touch_required": {}, + "@s_troubleshooting": {}, + "@s_unblock_pin": {}, + "@s_unknown_device": {}, + "@s_unknown_type": {}, + "@s_unlock": {}, + "@s_unpin_account": {}, + "@s_unsupported_yk": {}, + "@s_usb": {}, + "@s_usb_options": {}, + "@s_valid_from": {}, + "@s_valid_to": {}, + "@s_webauthn": {}, + "@s_wrong_password": {}, + "@s_yk_inaccessible": {}, + "@s_yk_information": {}, + "@s_yk_not_recognized": {}, + "app_name": "Yubico Authenticator", + "l_account": "Konto: {label}", + "l_account_add_failed": "Nie udało się dodać konta: {message}", + "l_account_already_exists": "To konto już istnieje w YubiKey", + "l_account_name_required": "Twoje konto musi mieć nazwę", + "l_accounts_used": "Użyto {used} z {capacity} kont", + "l_add_one_or_more_fps": "Dodaj jeden lub więcej (do pięciu) odcisków palców", + "l_app_disabled_desc": "Włącz funkcję '{app}' w kluczu YubiKey, aby uzyskać dostęp", + "l_app_not_supported_desc": "Ta funkcja nie jest obsługiwana", + "l_app_not_supported_on_yk": "Używany klucz YubiKey nie obsługuje funkcji '{app}'", + "l_attempts_remaining": "Pozostało prób: {retries}", + "l_bullet": "• {item}", "l_bypass_touch_requirement": "Obejdź wymóg dotyku", - "l_bypass_touch_requirement_on": "Konta, które wymagają dotknięcia, są automatycznie wyświetlane przez NFC", "l_bypass_touch_requirement_off": "Konta, które wymagają dotknięcia, potrzebują dodatkowego przyłożenia do NFC", - "s_silence_nfc_sounds": "Wycisz dźwięki NFC", - "l_silence_nfc_sounds_on": "Dźwięki nie będą odtwarzane po przyłożeniu do NFC", - "l_silence_nfc_sounds_off": "Dźwięki będą odtwarzane po przyłożeniu do NFC", - "s_usb_options": "Opcje USB", + "l_bypass_touch_requirement_on": "Konta, które wymagają dotknięcia, są automatycznie wyświetlane przez NFC", + "l_calculate_code_desc": "Uzyskaj nowy kod z klucza YubiKey", + "l_ccid_connection_failed": "Nie udało się nawiązać połączenia z kartą inteligentną", + "l_certificate_deleted": "Certyfikat został usunięty", + "l_certificate_exported": "Wyeksportowano certyfikat", + "l_change_management_key": "Zmień klucz zarządzania", + "l_code_copied_clipboard": "Kod skopiowany do schowka", + "l_config_updated_reinsert": "Zaktualizowano konfigurację, podłącz ponownie klucz YubiKey", + "l_copy_code_desc": "Łatwe wklejanie kodu do innych aplikacji", + "l_copy_otp_clipboard": "Skopiuj OTP do schowka", + "l_copy_to_clipboard": "Skopiuj do schowka", + "l_default_key_used": "Używany jest domyślny klucz zarządzania", + "l_delete_account_desc": "Usuń konto z klucza YubiKey", + "l_delete_certificate": "Usuń certyfikat", + "l_delete_certificate_desc": "Usuń certyfikat z klucza YubiKey", + "l_delete_fingerprint_desc": "Usuń odcisk palca z klucza YubiKey", + "l_delete_passkey_desc": "Usuń klucz dostępu z klucza YubiKey", + "l_diagnostics_copied": "Dane diagnostyczne skopiowane do schowka", + "l_elevating_permissions": "Podnoszenie uprawnień…", + "l_enable_community_translations": "Tłumaczenia społecznościowe", + "l_enter_fido2_pin": "Wprowadź kod PIN FIDO2 klucza YubiKey", + "l_enter_oath_pw": "Wprowadź hasło OATH dla klucza YubiKey", + "l_error_occured": "Wystąpił błąd", + "l_export_certificate": "Eksportuj certyfikat", + "l_export_certificate_desc": "Pozwala wyeksportować certyfikat do pliku", + "l_export_certificate_file": "Eksportuj certyfikat do pliku", + "l_export_csr_file": "Zapisz CSR do pliku", + "l_factory_reset_this_app": "Przywróć ustawienia fabryczne tej funkcji", + "l_fido_app_reset": "Reset funkcji FIDO", + "l_fido_pin_protection_optional": "Opcjonalna ochrona FIDO kodem PIN", + "l_file_not_found": "Nie odnaleziono pliku", + "l_file_too_big": "Zbyt duży rozmiar pliku", + "l_filesystem_error": "Błąd operacji systemu plików", + "l_fingerprint": "Odcisk palca: {label}", + "l_fingerprint_captured": "Odcisk palca zarejestrowany pomyślnie!", + "l_fingerprints_used": "Zarejestrowano {used}/5 odcisków palców", + "l_fp_step_1_capture": "Krok 1/2: Pobranie odcisku palca", + "l_fp_step_2_name": "Krok 2/2: Nazwij odcisk palca", + "l_generate_desc": "Generuj nowy certyfikat lub CSR", + "l_generating_private_key": "Generowanie prywatnego klucza…", + "l_helper_not_responding": "Proces pomocnika nie odpowiada", + "l_icon_pack_copy_failed": "Nie udało się skopiować plików z pakietu ikon", + "l_icon_pack_imported": "Zaimportowano pakiet ikon", + "l_icon_pack_removed": "Usunięto pakiet ikon", + "l_import_desc": "Zaimportuj klucz i/lub certyfikat", + "l_import_error": "Błąd importowania", + "l_import_file": "Importuj plik", + "l_import_icon_pack_failed": "Błąd importu pakietu ikon: {message}", + "l_insert_or_tap_yk": "Podłącz lub przystaw YubiKey", + "l_insert_yk": "Podłącz klucz YubiKey", + "l_invalid_character_issuer": "Nieprawidłowy znak: „:” nie jest dozwolony w polu wydawcy", + "l_invalid_icon_pack": "Nieprawidłowy pakiet ikon", + "l_invalid_qr": "Nieprawidłowy kod QR", + "l_kbd_layout_for_static": "Układ klawiatury (dla hasła statycznego)", + "l_keep_touching_yk": "Wielokrotnie dotykaj klucza YubiKey…", + "l_key_no_certificate": "Załadowano klucz bez certyfikatu", + "l_keystore_unavailable": "Magazyn kluczy systemu operacyjnego jest niedostępny", + "l_launch_and_copy_otp": "Uruchom aplikację i skopiuj OTP", "l_launch_app_on_usb": "Uruchom po podłączeniu YubiKey", - "l_launch_app_on_usb_on": "Uniemożliwia to innym aplikacjom korzystanie z YubiKey przez USB", "l_launch_app_on_usb_off": "Inne aplikacje mogą korzystać z YubiKey przez USB", + "l_launch_app_on_usb_on": "Uniemożliwia to innym aplikacjom korzystanie z YubiKey przez USB", + "l_launch_ya": "Uruchom Yubico Authenticator", + "l_loading_icon_pack": "Wczytywanie pakietu ikon…", + "l_log_copied": "Logi skopiowane do schowka", + "l_management_key_changed": "Zmieniono klucz zarządzania", + "l_min_one_interface": "Przynajmniej jeden interfejs musi być włączony", + "l_name_already_exists": "Ta nazwa już istnieje dla tego wydawcy", + "l_new_pin_len": "Nowy PIN musi mieć co najmniej {length} znaków", + "l_no_certificate": "Nie załadowano certyfikatu", + "l_no_discoverable_accounts": "Nie wykryto kont", + "l_no_fps_added": "Nie dodano odcisków palców", + "l_no_yk_present": "Nie wykryto YubiKey", + "l_oath_application_reset": "Reset funkcji OATH", + "l_on_yk_nfc_tap": "Podczas kontaktu YubiKey z NFC", + "l_open_connection_failed": "Nie udało się nawiązać połączenia", + "l_optional_password_protection": "Opcjonalna ochrona hasłem", + "l_optionally_set_a_pin": "Opcjonalnie ustaw PIN, aby chronić dostęp do YubiKey\nZarejestruj jako klucz bezpieczeństwa na stronach internetowych", + "l_passkey": "Klucz dostępu: {label}", + "l_pin_account_desc": "Przechowuj ważne konta razem", + "l_pin_blocked_reset": "PIN jest zablokowany; przywróć ustawienia fabryczne funkcji FIDO", + "l_pin_protected_key": "Zamiast tego można użyć kodu PIN", + "l_pin_soft_locked": "PIN został zablokowany do momentu ponownego podłączenia klucza YubiKey", + "l_piv_app_reset": "Funkcja PIV została zresetowana", + "l_piv_pin_blocked": "Zablokowano, użyj PUK, aby zresetować", + "l_piv_pin_puk_blocked": "Zablokowano, konieczny reset do ustawień fabrycznych", + "l_place_on_nfc_reader": "Przyłóż klucz YubiKey do czytnika NFC", + "l_point_camera_scan": "Skieruj aparat na kod QR, by go zeskanować", + "l_press_reset_to_begin": "Naciśnij reset, aby rozpocząć…", + "l_qr_not_found": "Nie znaleziono kodu QR", + "l_qr_not_read": "Odczytanie kodu QR nie powiodło się: {message}", + "l_qr_scanned": "Zeskanowany kod QR", + "l_ready_to_use": "Gotowe do użycia", + "l_register_sk_on_websites": "Zarejestruj jako klucz bezpieczeństwa na stronach internetowych", + "l_reinsert_yk": "Ponownie podłącz YubiKey", + "l_remember_pw_failed": "Nie udało się zapamiętać hasła", + "l_remove_icon_pack_failed": "Błąd podczas usuwania pakietu ikon", + "l_remove_yk_from_reader": "Odsuń klucz YubiKey od czytnika NFC", + "l_rename_account_desc": "Edytuj wydawcę/nazwę konta", + "l_rename_fp_desc": "Zmień etykietę", + "l_rename_fp_failed": "Błąd zmiany nazwy: {message}", + "l_replace_yk_on_reader": "Umieść klucz YubiKey z powrotem na czytniku", + "l_reset_failed": "Błąd podczas resetowania: {message}", + "l_select_accounts": "Wybierz konta, które chcesz dodać do YubiKey", + "l_select_import_file": "Wybierz plik do zaimportowania", + "l_set_icons_for_accounts": "Ustaw ikony dla kont", + "l_set_pin_failed": "Nie udało się ustawić kodu PIN: {message}", + "l_set_pin_fingerprints": "Ustaw kod PIN, aby zarejestrować odciski palców", + "l_set_pin_first": "Najpierw wymagany jest kod PIN", + "l_setting_name_failed": "Błąd ustawienia nazwy: {message}", + "l_silence_nfc_sounds_off": "Dźwięki będą odtwarzane po przyłożeniu do NFC", + "l_silence_nfc_sounds_on": "Dźwięki nie będą odtwarzane po przyłożeniu do NFC", + "l_touch_button_now": "Dotknij teraz przycisku na kluczu YubiKey", + "l_unlock_first": "Najpierw odblokuj hasłem", + "l_unlock_pin_first": "Najpierw odblokuj kodem PIN", + "l_unlock_piv_management": "Odblokuj zarządzanie PIV", + "l_unplug_yk": "Odłącz klucz YubiKey", + "l_warning_default_key": "Uwaga: Używany jest klucz domyślny", + "l_webauthn_req_fido2": "WebAuthn wymaga włączenia funkcji FIDO2 w kluczu YubiKey", + "l_wrong_key": "Błędny klucz", + "l_wrong_pin_attempts_remaining": "Błędny PIN, pozostało prób: {retries}", + "l_wrong_puk_attempts_remaining": "Nieprawidłowy PUK, pozostało prób: {retries}", + "l_yk_no_access": "Dostęp do tego klucza YubiKey jest niemożliwy", + "p_add_description": "W celu zeskanowania kodu QR, upewnij się, że pełny kod jest widoczny na ekranie a następnie naciśnij poniższy przycisk. Jeśli posiadasz dane uwierzytelniające do konta w tekstowej formie, skorzystaj z opcji ręcznego wprowadzania danych.", + "p_ccid_service_unavailable": "Upewnij się, że usługa kart inteligentnych działa.", + "p_change_management_key_desc": "Zmień swój klucz zarządzania. Opcjonalnie możesz zezwolić na używanie kodu PIN zamiast klucza zarządzania.", + "p_community_translations_desc": "Tłumaczenia te są dostarczane i utrzymywane przez społeczność. Mogą zawierać błędy lub być niekompletne.", + "p_custom_icons_description": "Pakiety ikon mogą sprawić, że Twoje konta będą łatwiejsze do odróżnienia dzięki znanym logo i kolorom.", + "p_elevated_permissions_required": "Zarządzanie tym urządzeniem wymaga podwyższonych uprawnień.", + "p_enter_current_password_or_reset": "Wprowadź aktualne hasło. Jeśli go nie znasz, musisz zresetować klucz YubiKey.", + "p_enter_current_pin_or_reset": "Wprowadź aktualny kod PIN. Jeśli go nie znasz, musisz zresetować klucz YubiKey.", + "p_enter_current_puk_or_reset": "Wprowadź aktualny kod PUK. Jeśli go nie znasz, musisz zresetować klucz YubiKey.", + "p_enter_new_fido2_pin": "Wprowadź nowy kod PIN. Musi zawierać co najmniej {length} znaków. Może zawierać litery, cyfry i znaki specjalne.", + "p_enter_new_password": "Wprowadź nowe hasło. Może ono zawierać litery, cyfry i znaki specjalne.", + "p_enter_new_piv_pin_puk": "Wprowadź nową {name} do ustawienia. Musi składać się z 6-8 znaków.", + "p_generate_desc": "Spowoduje to wygenerowanie nowego klucza w kluczu YubiKey w slocie PIV {slot}. Klucz publiczny zostanie osadzony w samopodpisanym certyfikacie przechowywanym w kluczu YubiKey lub w żądaniu podpisania certyfikatu (CSR) zapisanym w pliku.", + "p_import_items_desc": "Następujące elementy zostaną zaimportowane do slotu PIV {slot}.", + "p_need_camera_permission": "Yubico Authenticator wymaga dostępu do aparatu w celu skanowania kodów QR.", + "p_password_protected_file": "Wybrany plik jest chroniony hasłem. Wprowadź je, aby kontynuować.", + "p_pcscd_unavailable": "Upewnij się, że pcscd jest zainstalowany i uruchomiony.", + "p_pin_required_desc": "Czynność, którą zamierzasz wykonać, wymaga wprowadzenia kodu PIN PIV.", + "p_press_fingerprint_begin": "Przytrzymaj palec na kluczu YubiKey, aby rozpocząć.", + "p_rename_will_change_account_displayed": "Spowoduje to zmianę sposobu wyświetlania konta na liście.", + "p_target_copied_clipboard": "{label} skopiowano do schowka.", + "p_try_reinsert_yk": "Spróbuj ponownie podłączyć klucz YubiKey.", + "p_unlock_piv_management_desc": "Czynność, którą zamierzasz wykonać, wymaga klucza zarządzania PIV. Podaj ten klucz, aby odblokować funkcje zarządzania dla tej sesji.", + "p_warning_delete_account": "Uwaga! Ta czynność spowoduje usunięcie konta z klucza YubiKey.", + "p_warning_delete_certificate": "Uwaga! Ta czynność spowoduje usunięcie certyfikatu z klucza YubiKey.", + "p_warning_delete_fingerprint": "Spowoduje to usunięcie odcisku palca z twojego YubiKey.", + "p_warning_delete_passkey": "Spowoduje to usunięcie klucza dostępu z klucza YubiKey.", + "p_warning_deletes_accounts": "Uwaga! Spowoduje to nieodwracalne usunięcie wszystkich kont U2F i FIDO2 z klucza YubiKey.", + "p_warning_disable_accounts": "Twoje poświadczenia, a także wszelkie ustawione kody PIN, zostaną usunięte z tego klucza YubiKey. Upewnij się, że najpierw wyłączono je w odpowiednich witrynach internetowych, aby uniknąć zablokowania kont.", + "p_warning_disable_credential": "Nie będzie już możliwe generowanie OTP dla tego konta. Upewnij się, że najpierw wyłączono te dane uwierzytelniające w witrynie, aby uniknąć zablokowania konta.", + "p_warning_disable_credentials": "Twoje poświadczenia OATH, jak również wszelkie ustawione hasła, zostaną usunięte z tego klucza YubiKey. Upewnij się, że najpierw wyłączono je w odpowiednich witrynach internetowych, aby uniknąć zablokowania kont.", + "p_warning_factory_reset": "Uwaga! Spowoduje to nieodwracalne usunięcie wszystkich kont OATH TOTP/HOTP z klucza YubiKey.", + "p_warning_piv_reset": "Ostrzeżenie! Wszystkie dane przechowywane dla PIV zostaną nieodwracalnie usunięte z klucza YubiKey.", + "p_warning_piv_reset_desc": "Obejmuje to klucze prywatne i certyfikaty. Kod PIN, PUK i klucz zarządzania zostaną zresetowane do domyślnych wartości fabrycznych.", + "p_webauthn_elevated_permissions_required": "Zarządzanie WebAuthn wymaga podwyższonych uprawnień.", + "p_will_change_label_fp": "Spowoduje to zmianę etykiety odcisku palca.", + "q_delete_certificate_confirm": "Usunąć certyfikat ze slotu PIV {slot}?", + "q_have_account_info": "Masz dane konta?", + "q_no_qr": "Nie masz kodu QR?", + "q_rename_target": "Zmienić nazwę {label}?", + "q_want_to_scan": "Czy chcesz zeskanować?", + "s_about": "O aplikacji", + "s_account_added": "Konto zostało dodane", + "s_account_deleted": "Konto zostało usunięte", + "s_account_name": "Nazwa konta", + "s_account_renamed": "Zmieniono nazwę konta", + "s_accounts": "Konta", + "s_actions": "Działania", + "s_add_account": "Dodaj konto", + "s_add_accounts": "Dodaj konto(-a)", + "s_add_fingerprint": "Dodaj odcisk palca", + "s_add_manually": "Dodaj ręcznie", "s_allow_screenshots": "Zezwalaj na zrzuty ekranu", - - "@_eof": {} + "s_app_disabled": "Wyłączona funkcja", + "s_app_not_supported": "Funkcja nie jest obsługiwana", + "s_app_theme": "Motyw aplikacji", + "s_appearance": "Wygląd", + "s_application_error": "Błąd funkcji", + "s_authenticator": "Authenticator", + "s_calculate": "Oblicz", + "s_calculate_code": "Oblicz kod", + "s_cancel": "Anuluj", + "s_certificate": "Certyfikat", + "s_certificate_fingerprint": "Odcisk palca", + "s_certificates": "Certyfikaty", + "s_change_pin": "Zmień PIN", + "s_change_puk": "Zmień PUK", + "s_character_count": "Liczba znaków", + "s_choose_app_theme": "Wybierz motyw aplikacji", + "s_choose_icon_pack": "Wybierz pakiet ikon", + "s_choose_kbd_layout": "Wybierz układ klawiatury", + "s_clear_saved_password": "Usuń zapisane hasło", + "s_close": "Zamknij", + "s_code_copied": "Kod skopiowany", + "s_config_updated": "Zaktualizowano konfigurację", + "s_configure_yk": "Skonfiguruj YubiKey", + "s_confirm_password": "Potwierdź hasło", + "s_confirm_pin": "Potwierdź PIN", + "s_confirm_puk": "Potwierdź PUK", + "s_copy_log": "Kopiuj logi", + "s_counter_based": "Na podstawie licznika", + "s_csr": "CSR", + "s_current_management_key": "Aktualny klucz zarządzania", + "s_current_password": "Aktualne hasło", + "s_current_pin": "Aktualny PIN", + "s_current_puk": "Aktualny PUK", + "s_custom_icons": "Niestandardowe ikony", + "s_dark_mode": "Ciemny", + "s_definition": "{item}:", + "s_delete": "Usuń", + "s_delete_account": "Usuń konto", + "s_delete_fingerprint": "Usuń odcisk palca", + "s_delete_passkey": "Usuń klucz dostępu", + "s_enable_nfc": "Włącz NFC", + "s_enter_manually": "Wprowadź ręcznie", + "s_factory_reset": "Ustawienia fabryczne", + "s_fido_disabled": "FIDO2 wyłączone", + "s_fido_pin_protection": "Ochrona FIDO kodem PIN", + "s_fingerprint_added": "Dodano odcisk palca", + "s_fingerprint_deleted": "Odcisk palca został usunięty", + "s_fingerprint_renamed": "Zmieniono nazwę odcisku palca", + "s_fingerprints": "Odciski palców", + "s_fw_version": "F/W: {version}", + "s_generate_key": "Generuj klucz", + "s_help_and_about": "Pomoc i informacje", + "s_help_and_feedback": "Pomoc i opinie", + "s_hide_device": "Ukryj urządzenie", + "s_hide_window": "Ukryj okno", + "s_i_need_help": "Pomoc", + "s_import": "Importuj", + "s_invalid_length": "Nieprawidłowa długość", + "s_issuer": "Wydawca", + "s_issuer_optional": "Wydawca (opcjonalnie)", + "s_label": "Etykieta", + "s_language": "Język", + "s_learn_more": "Dowiedz się więcej", + "s_light_mode": "Jasny", + "s_load_icon_pack": "Wczytaj pakiet ikon", + "s_log_level": "Poziom logowania: {level}", + "s_manage": "Zarządzaj", + "s_manage_password": "Zarządzaj hasłem", + "s_management_key": "Klucz zarządzania", + "s_name": "Nazwa", + "s_new_management_key": "Nowy klucz zarządzania", + "s_new_password": "Nowe hasło", + "s_new_pin": "Nowy PIN", + "s_new_puk": "Nowy PUK", + "s_nfc": "NFC", + "s_nfc_dialog_oath_add_account": "Działanie: dodaj nowe konto", + "s_nfc_dialog_oath_add_multiple_accounts": "Działanie: dodawanie wielu kont", + "s_nfc_dialog_oath_calculate_code": "Działanie: oblicz kod OATH", + "s_nfc_dialog_oath_delete_account": "Działanie: usuń konto", + "s_nfc_dialog_oath_failure": "Operacja OATH nie powiodła się", + "s_nfc_dialog_oath_rename_account": "Działanie: zmień nazwę konta", + "s_nfc_dialog_oath_reset": "Działanie: resetuj aplet OATH", + "s_nfc_dialog_oath_set_password": "Działanie: ustaw hasło OATH", + "s_nfc_dialog_oath_unlock": "Działanie: odblokuj aplet OATH", + "s_nfc_dialog_oath_unset_password": "Działanie: usuń hasło OATH", + "s_nfc_dialog_operation_failed": "Niepowodzenie", + "s_nfc_dialog_operation_success": "Powodzenie", + "s_nfc_dialog_tap_key": "Dotknij swój klucz", + "s_nfc_options": "Opcje NFC", + "s_no_accounts": "Brak kont", + "s_no_fingerprints": "Brak odcisków palców", + "s_no_pinned_accounts": "Brak przypiętych kont", + "s_num_digits": "{num} cyfr", + "s_num_sec": "{num} sek", + "s_open_src_licenses": "Licencje open source", + "s_passkey_deleted": "Usunięto klucz dostępu", + "s_passkeys": "Klucze dostępu", + "s_password": "Hasło", + "s_password_forgotten": "Hasło zostało zapomniane", + "s_password_removed": "Hasło zostało usunięte", + "s_password_set": "Hasło zostało ustawione", + "s_permission_denied": "Odmowa dostępu", + "s_pin": "PIN", + "s_pin_account": "Przypnij konto", + "s_pin_required": "Wymagany PIN", + "s_pin_set": "PIN ustawiony", + "s_pinned": "Przypięte", + "s_piv": "PIV", + "s_please_wait": "Proszę czekać…", + "s_privacy_policy": "Polityka prywatności", + "s_private_key": "Klucz prywatny", + "s_private_key_generated": "Wygenerowano klucz prywatny", + "s_protect_key": "Zabezpiecz kodem PIN", + "s_puk": "PUK", + "s_puk_set": "PUK ustawiony", + "s_qr_scan": "Skanuj kod QR", + "s_quit": "Wyjdź", + "s_reconfiguring_yk": "Rekonfigurowanie YubiKey…", + "s_remember_password": "Zapamiętaj hasło", + "s_remove_icon_pack": "Usuń pakiet ikon", + "s_remove_password": "Usuń hasło", + "s_rename_account": "Zmień nazwę konta", + "s_rename_fp": "Zmień nazwę odcisku palca", + "s_replace_icon_pack": "Zastąp pakiet ikon", + "s_require_touch": "Wymagaj dotknięcia", + "s_reset": "Zresetuj", + "s_reset_fido": "Zresetuj FIDO", + "s_reset_oath": "Zresetuj OATH", + "s_reset_piv": "Resetuj PIV", + "s_review_permissions": "Przegląd uprawnień", + "s_run_diagnostics": "Uruchom diagnostykę", + "s_save": "Zapisz", + "s_search_accounts": "Wyszukaj konta", + "s_secret_key": "Tajny klucz", + "s_select_to_scan": "Wybierz, aby skanować", + "s_select_yk": "Wybierz YubiKey", + "s_send_feedback": "Prześlij opinię", + "s_serial": "Nr. seryjny", + "s_set_password": "Ustaw hasło", + "s_set_pin": "Ustaw PIN", + "s_settings": "Ustawienia", + "s_setup": "Konfiguruj", + "s_show_hidden_devices": "Pokaż ukryte urządzenia", + "s_show_window": "Pokaż okno", + "s_silence_nfc_sounds": "Wycisz dźwięki NFC", + "s_slot_9a": "Uwierzytelnienie", + "s_slot_9c": "Cyfrowy podpis", + "s_slot_9d": "Menedżer kluczy", + "s_slot_9e": "Autoryzacja karty", + "s_slot_display_name": "{name} ({hexid})", + "s_sn_serial": "S/N: {serial}", + "s_subject": "Temat", + "s_system_default": "Zgodny z systemem", + "s_terms_of_use": "Warunki użytkowania", + "s_time_based": "Na podstawie czasu", + "s_toggle_applications": "Przełączanie funkcji", + "s_touch_required": "Wymagane dotknięcie", + "s_troubleshooting": "Rozwiązywanie problemów", + "s_unblock_pin": "Odblokuj PIN", + "s_unknown_device": "Nierozpoznane urządzenie", + "s_unknown_type": "Nieznany typ", + "s_unlock": "Odblokuj", + "s_unpin_account": "Odepnij konto", + "s_unsupported_yk": "Nieobsługiwany klucz YubiKey", + "s_usb": "USB", + "s_usb_options": "Opcje USB", + "s_valid_from": "Ważny od", + "s_valid_to": "Ważny do", + "s_webauthn": "WebAuthn", + "s_wrong_password": "Błędne hasło", + "s_yk_inaccessible": "Urządzenie niedostępne", + "s_yk_information": "Informacja o YubiKey", + "s_yk_not_recognized": "Urządzenie nie rozpoznane" } From 27ffceabfd8849f23459eed8a012aa0d4211d6e9 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 22 Aug 2023 14:22:32 +0200 Subject: [PATCH 106/158] PIV: Make PIN-entry consistent with other views. --- lib/piv/views/pin_dialog.dart | 44 +++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/lib/piv/views/pin_dialog.dart b/lib/piv/views/pin_dialog.dart index 4a4f6084..7f849e5f 100644 --- a/lib/piv/views/pin_dialog.dart +++ b/lib/piv/views/pin_dialog.dart @@ -33,22 +33,30 @@ class PinDialog extends ConsumerStatefulWidget { } class _PinDialogState extends ConsumerState { - String _pin = ''; + final _pinController = TextEditingController(); bool _pinIsWrong = false; int _attemptsRemaining = -1; + bool _isObscure = true; + + @override + void dispose() { + _pinController.dispose(); + super.dispose(); + } Future _submit() async { final navigator = Navigator.of(context); try { final status = await ref .read(pivStateProvider(widget.devicePath).notifier) - .verifyPin(_pin); + .verifyPin(_pinController.text); status.when( success: () { navigator.pop(true); }, failure: (attemptsRemaining) { setState(() { + _pinController.clear(); _attemptsRemaining = attemptsRemaining; _pinIsWrong = true; }); @@ -67,7 +75,7 @@ class _PinDialogState extends ConsumerState { actions: [ TextButton( key: keys.unlockButton, - onPressed: _pin.length >= 4 ? _submit : null, + onPressed: _pinController.text.length >= 4 ? _submit : null, child: Text(l10n.s_unlock), ), ], @@ -79,23 +87,35 @@ class _PinDialogState extends ConsumerState { Text(l10n.p_pin_required_desc), TextField( autofocus: true, - obscureText: true, + obscureText: _isObscure, maxLength: 8, autofillHints: const [AutofillHints.password], key: keys.managementKeyField, + controller: _pinController, 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), + 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; + }); + }, + ), + ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { _pinIsWrong = false; - _pin = value; }); }, onSubmitted: (_) => _submit(), From a0232eb0cada27ac416d5935b0fe3654bfa62c4b Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 22 Aug 2023 14:22:59 +0200 Subject: [PATCH 107/158] Add more text to PIV generate key view. --- lib/l10n/app_en.arb | 6 ++- lib/piv/views/generate_key_dialog.dart | 56 ++++++++++++++++---------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d7da84a7..a8753b45 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -34,6 +34,7 @@ "s_name": "Name", "s_usb": "USB", "s_nfc": "NFC", + "s_options": "Options", "s_show_window": "Show window", "s_hide_window": "Hide window", "q_rename_target": "Rename {label}?", @@ -463,7 +464,10 @@ "slot": {} } }, - "l_invalid_rfc4514": "Invalid RFC4514 string", + "p_subject_desc": "A distinguished name (DN) formatted in accordance to the RFC 4514 specification.", + "l_rfc4514_invalid": "Invalid RFC 4514 format", + "rfc4514_examples": "Examples:\nCN=Example Name\nCN=jsmith,DC=example,DC=net", + "p_cert_options_desc": "Key algorithm to use, output format, and an expiration date (if applicable).", "@_piv_slots": {}, "s_slot_display_name": "{name} ({hexid})", diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart index 2329b220..901ea632 100644 --- a/lib/piv/views/generate_key_dialog.dart +++ b/lib/piv/views/generate_key_dialog.dart @@ -65,6 +65,11 @@ class _GenerateKeyDialogState extends ConsumerState { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; + final textTheme = Theme.of(context).textTheme; + // This is what ListTile uses for subtitle + final subtitleStyle = textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ); return ResponsiveDialog( allowCancel: !_generating, @@ -135,6 +140,11 @@ class _GenerateKeyDialogState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + l10n.s_subject, + style: textTheme.bodyLarge, + ), + Text(l10n.p_subject_desc), TextField( autofocus: true, key: keys.subjectField, @@ -142,40 +152,31 @@ class _GenerateKeyDialogState extends ConsumerState { border: const OutlineInputBorder(), labelText: l10n.s_subject, errorText: _subject.isNotEmpty && _invalidSubject - ? l10n.l_invalid_rfc4514 + ? l10n.l_rfc4514_invalid : null), textInputAction: TextInputAction.next, enabled: !_generating, onChanged: (value) { setState(() { - if (value.isEmpty) { - _subject = ''; - _invalidSubject = true; - } else { - _subject = value.contains('=') ? value : 'CN=$value'; - _invalidSubject = false; - } + _invalidSubject = value.isEmpty; + _subject = value; }); }, ), + Text( + l10n.rfc4514_examples, + style: subtitleStyle, + ), + Text( + l10n.s_options, + style: textTheme.bodyLarge, + ), + Text(l10n.p_cert_options_desc), Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 4.0, runSpacing: 8.0, children: [ - ChoiceFilterChip( - items: GenerateType.values, - value: _generateType, - selected: _generateType != defaultGenerateType, - itemBuilder: (value) => Text(value.getDisplayName(l10n)), - onChanged: _generating - ? null - : (value) { - setState(() { - _generateType = value; - }); - }, - ), ChoiceFilterChip( items: KeyType.values, value: _keyType, @@ -189,6 +190,19 @@ class _GenerateKeyDialogState extends ConsumerState { }); }, ), + ChoiceFilterChip( + items: GenerateType.values, + value: _generateType, + selected: _generateType != defaultGenerateType, + itemBuilder: (value) => Text(value.getDisplayName(l10n)), + onChanged: _generating + ? null + : (value) { + setState(() { + _generateType = value; + }); + }, + ), if (_generateType == GenerateType.certificate) FilterChip( label: Text(dateFormatter.format(_validTo)), From 0c37afc0e2c53d96745c9de4b88acd71bdac6782 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 22 Aug 2023 16:20:04 +0200 Subject: [PATCH 108/158] Various PIV UI improvements. --- lib/l10n/app_en.arb | 2 ++ lib/piv/views/manage_key_dialog.dart | 44 ++++++++++++++++++------ lib/piv/views/manage_pin_puk_dialog.dart | 2 +- lib/piv/views/piv_screen.dart | 3 +- lib/piv/views/slot_dialog.dart | 2 ++ lib/widgets/tooltip_if_truncated.dart | 5 +-- 6 files changed, 43 insertions(+), 15 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a8753b45..03b20f80 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -282,6 +282,8 @@ "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", "l_default_key_used": "Default management key used", + "s_generate_random": "Generate random", + "s_use_default": "Use default", "l_warning_default_key": "Warning: Default key used", "s_protect_key": "Protect with PIN", "l_pin_protected_key": "PIN can be used instead", diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index 4c307ef4..fdb2e6ae 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -42,24 +42,26 @@ class ManageKeyDialog extends ConsumerStatefulWidget { } class _ManageKeyDialogState extends ConsumerState { + late bool _hasMetadata; late bool _defaultKeyUsed; late bool _usesStoredKey; late bool _storeKey; - String _currentKeyOrPin = ''; bool _currentIsWrong = false; int _attemptsRemaining = -1; ManagementKeyType _keyType = ManagementKeyType.tdes; + final _currentController = TextEditingController(); final _keyController = TextEditingController(); @override void initState() { super.initState(); + _hasMetadata = widget.pivState.metadata != null; _defaultKeyUsed = widget.pivState.metadata?.managementKeyMetadata.defaultValue ?? false; _usesStoredKey = widget.pivState.protectedKey; if (!_usesStoredKey && _defaultKeyUsed) { - _currentKeyOrPin = defaultManagementKey; + _currentController.text = defaultManagementKey; } _storeKey = _usesStoredKey; } @@ -67,13 +69,14 @@ class _ManageKeyDialogState extends ConsumerState { @override void dispose() { _keyController.dispose(); + _currentController.dispose(); super.dispose(); } _submit() async { final notifier = ref.read(pivStateProvider(widget.path).notifier); if (_usesStoredKey) { - final status = (await notifier.verifyPin(_currentKeyOrPin)).when( + final status = (await notifier.verifyPin(_currentController.text)).when( success: () => true, failure: (attemptsRemaining) { setState(() { @@ -87,7 +90,7 @@ class _ManageKeyDialogState extends ConsumerState { return; } } else { - if (!await notifier.authenticate(_currentKeyOrPin)) { + if (!await notifier.authenticate(_currentController.text)) { setState(() { _currentIsWrong = true; }); @@ -126,9 +129,10 @@ class _ManageKeyDialogState extends ConsumerState { ManagementKeyType.tdes; final hexLength = _keyType.keyLength * 2; final protected = widget.pivState.protectedKey; + final currentKeyOrPin = _currentController.text; final currentLenOk = protected - ? _currentKeyOrPin.length >= 4 - : _currentKeyOrPin.length == currentType.keyLength * 2; + ? currentKeyOrPin.length >= 4 + : currentKeyOrPin.length == currentType.keyLength * 2; final newLenOk = _keyController.text.length == hexLength; return ResponsiveDialog( @@ -153,6 +157,7 @@ class _ManageKeyDialogState extends ConsumerState { autofillHints: const [AutofillHints.password], key: keys.pinPukField, maxLength: 8, + controller: _currentController, decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_pin, @@ -166,7 +171,6 @@ class _ManageKeyDialogState extends ConsumerState { onChanged: (value) { setState(() { _currentIsWrong = false; - _currentKeyOrPin = value; }); }, ), @@ -175,16 +179,34 @@ class _ManageKeyDialogState extends ConsumerState { key: keys.managementKeyField, autofocus: !_defaultKeyUsed, autofillHints: const [AutofillHints.password], - initialValue: _defaultKeyUsed ? defaultManagementKey : null, + controller: _currentController, readOnly: _defaultKeyUsed, maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null, decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_current_management_key, - prefixIcon: const Icon(Icons.password_outlined), + 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 + ? 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(); + } + }); + }, + ), ), inputFormatters: [ FilteringTextInputFormatter.allow( @@ -194,7 +216,6 @@ class _ManageKeyDialogState extends ConsumerState { onChanged: (value) { setState(() { _currentIsWrong = false; - _currentKeyOrPin = value; }); }, ), @@ -211,10 +232,11 @@ class _ManageKeyDialogState extends ConsumerState { decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_new_management_key, - prefixIcon: const Icon(Icons.password_outlined), + prefixIcon: const Icon(Icons.key_outlined), enabled: currentLenOk, suffixIcon: IconButton( icon: const Icon(Icons.refresh), + tooltip: l10n.s_generate_random, onPressed: currentLenOk ? () { final random = Random.secure(); diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart index b45736ba..7cb67950 100644 --- a/lib/piv/views/manage_pin_puk_dialog.dart +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -114,7 +114,7 @@ class _ManagePinPukDialogState extends ConsumerState { : l10n.s_current_puk, prefixIcon: const Icon(Icons.password_outlined), errorText: _currentIsWrong - ? (widget.target == ManageTarget.puk + ? (widget.target == ManageTarget.pin ? l10n.l_wrong_pin_attempts_remaining( _attemptsRemaining) : l10n.l_wrong_puk_attempts_remaining( diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart index e54b879b..3727f812 100644 --- a/lib/piv/views/piv_screen.dart +++ b/lib/piv/views/piv_screen.dart @@ -105,7 +105,8 @@ class _CertificateListItem extends StatelessWidget { ), title: slot.getDisplayName(l10n), subtitle: certInfo != null - ? certInfo.subject + // Simplify subtitle by stripping "CN=", etc. + ? certInfo.subject.replaceAll(RegExp(r'[A-Z]+='), ' ').trimLeft() : pivSlot.hasKey == true ? l10n.l_key_no_certificate : l10n.l_no_certificate, diff --git a/lib/piv/views/slot_dialog.dart b/lib/piv/views/slot_dialog.dart index 92f289b5..7477f847 100644 --- a/lib/piv/views/slot_dialog.dart +++ b/lib/piv/views/slot_dialog.dart @@ -81,6 +81,8 @@ class SlotDialog extends ConsumerWidget { child: TooltipIfTruncated( text: value, style: subtitleStyle, + tooltip: value.replaceAllMapped( + RegExp(r',([A-Z]+)='), (match) => '\n${match[1]}='), ), ), ], diff --git a/lib/widgets/tooltip_if_truncated.dart b/lib/widgets/tooltip_if_truncated.dart index 3694143e..2eaaf932 100644 --- a/lib/widgets/tooltip_if_truncated.dart +++ b/lib/widgets/tooltip_if_truncated.dart @@ -19,8 +19,9 @@ import 'package:flutter/material.dart'; class TooltipIfTruncated extends StatelessWidget { final String text; final TextStyle style; + final String? tooltip; const TooltipIfTruncated( - {super.key, required this.text, required this.style}); + {super.key, required this.text, required this.style, this.tooltip}); @override Widget build(BuildContext context) { @@ -41,7 +42,7 @@ class TooltipIfTruncated extends StatelessWidget { return textPainter.didExceedMaxLines ? Tooltip( margin: const EdgeInsets.all(16), - message: text, + message: tooltip ?? text, child: textWidget, ) : textWidget; From c33a2e72bff026bb086fb8a450289df5f6dfa92d Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 22 Aug 2023 16:36:13 +0200 Subject: [PATCH 109/158] PIV: Add Default Key button to auth dialog. --- lib/piv/views/authentication_dialog.dart | 38 ++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/piv/views/authentication_dialog.dart b/lib/piv/views/authentication_dialog.dart index 2ae4a45a..7132910e 100644 --- a/lib/piv/views/authentication_dialog.dart +++ b/lib/piv/views/authentication_dialog.dart @@ -37,12 +37,20 @@ class AuthenticationDialog extends ConsumerStatefulWidget { } class _AuthenticationDialogState extends ConsumerState { - String _managementKey = ''; + bool _defaultKeyUsed = false; bool _keyIsWrong = false; + final _keyController = TextEditingController(); + + @override + void dispose() { + _keyController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; + final hasMetadata = widget.pivState.metadata != null; final keyLen = (widget.pivState.metadata?.managementKeyMetadata.keyType ?? ManagementKeyType.tdes) .keyLength * @@ -52,13 +60,13 @@ class _AuthenticationDialogState extends ConsumerState { actions: [ TextButton( key: keys.unlockButton, - onPressed: _managementKey.length == keyLen + onPressed: _keyController.text.length == keyLen ? () async { final navigator = Navigator.of(context); try { final status = await ref .read(pivStateProvider(widget.devicePath).notifier) - .authenticate(_managementKey); + .authenticate(_keyController.text); if (status) { navigator.pop(true); } else { @@ -88,24 +96,44 @@ class _AuthenticationDialogState extends ConsumerState { TextField( key: keys.managementKeyField, autofocus: true, - maxLength: keyLen, 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 + ? 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(); + } + }); + }, + ), ), textInputAction: TextInputAction.next, onChanged: (value) { setState(() { _keyIsWrong = false; - _managementKey = value; }); }, ), From 5f4835c1bbfd27bacfe20c29581d53034929037f Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 22 Aug 2023 16:39:26 +0200 Subject: [PATCH 110/158] Tweak widths for ResponsiveDialog. --- lib/widgets/responsive_dialog.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/responsive_dialog.dart b/lib/widgets/responsive_dialog.dart index 871423ce..511457da 100755 --- a/lib/widgets/responsive_dialog.dart +++ b/lib/widgets/responsive_dialog.dart @@ -78,7 +78,7 @@ class _ResponsiveDialogState extends State { scrollable: true, contentPadding: const EdgeInsets.symmetric(vertical: 8), content: SizedBox( - width: 380, + width: 550, child: Container(key: _childKey, child: widget.child), ), actions: [ @@ -107,7 +107,7 @@ class _ResponsiveDialogState extends State { _hasLostFocus = true; } }, - child: constraints.maxWidth < 540 + child: constraints.maxWidth < 400 ? _buildFullscreen(context) : _buildDialog(context), ); From e985130ba3358e37f228835054d9aa4ce7f6f2c8 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 22 Aug 2023 17:25:06 +0200 Subject: [PATCH 111/158] Rename inverted icon. --- lib/desktop/systray.dart | 2 +- resources/icons/systray-template-inv.png | Bin 7665 -> 0 bytes resources/icons/systray-template.png | Bin 7734 -> 7665 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 resources/icons/systray-template-inv.png diff --git a/lib/desktop/systray.dart b/lib/desktop/systray.dart index e2de966a..6ba2219f 100755 --- a/lib/desktop/systray.dart +++ b/lib/desktop/systray.dart @@ -94,7 +94,7 @@ Future _calculateCode( String _getIcon() { if (Platform.isMacOS) { - return 'resources/icons/systray-template-inv.png'; + return 'resources/icons/systray-template.png'; } if (Platform.isWindows) { return 'resources/icons/com.yubico.yubioath.ico'; diff --git a/resources/icons/systray-template-inv.png b/resources/icons/systray-template-inv.png deleted file mode 100644 index e2faa8b343e6d0878550d6ac3be1f5d5e42abee6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7665 zcma)Bd0dQL`#&?y)XcO`gJ>Jg(1I2zsboemlPnbyPclL(A=Oi%bT?5GEqAsjYePMj z#2Z3cG9?K~S)(vi$Rkv?tiS6_c^{AW{rx__KlpJ(O`%D#Svj(vc zLfT$pPd|jzmcZXO1{FqhGi`4pL|GT+;o;-u;lYiJS{@P>u?Qi{oWz{5;tK-fi}R*m zwC>l>Rpz|#z|{Vp6RNxp`K)E_@ejRS(R#x+^afc_>dZPq*BICTkd95=X_r!#RH-35 zv|-cU4VyonFQ_-alhU&0?3oXxt2G$6&2JW+v=`i5>Y%@2{lT||jh*Y!s)ls;+M3#t zPx)3^TU-7zbuWK7A>RM5=%lP=jtlG+7VPUIFO;N@mhE?1n$qpQLe{ou%g3GZw+b}b z&wwRU0fZfDQoPqznz1#cbg_{8f;abf#r+X6TLG0RunS-;5tyhYM{ zyZnC*kKXlEFF2rz&6oN3Or%U`l+_n*3LY%{+|d7<@UCr+_uyHS3}a18Vam%-72lO{ zr~G4so4r{Rliv@x)MKdb5}Q2d;)?QYll`&Ok9o<%&ONrBncP0@#xU&>3ADEnY6I?S zPr0XN6GtRlcsMPZ)pe6|Y_{@iM!A*t+eg&7OM;faOWf&>yk1zZQ8PBC^2S^VTHg6e z5WW2S?t>pbs3rMF72TO>@CEjT87ZC}4cjvx|4|N%+aCsl8Zll|CTKik=re|n4!6r3 ziICBEFHhl&`1>6X?!`WE-YO7w+I;

tN<)<;XruTahZe^rA3gM^!Q9N?iwiRpW=R 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

tN<)<;XruTahZe^rA3gM^!Q9N?iwiRpW=R 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 f%pH`I@J-G3^P)KJU+RKIilK+*?Y(M3E+YAR8g1 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 From d47f8d7fe20b0f8aa460487702665439bb136907 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 23 Aug 2023 15:53:30 +0200 Subject: [PATCH 112/158] PIV: improve Import File dialog. --- helper/helper/piv.py | 50 +++++++++----- lib/l10n/app_en.arb | 15 +--- lib/piv/models.dart | 4 +- lib/piv/models.freezed.dart | 97 ++++++++++++++++---------- lib/piv/models.g.dart | 10 +-- lib/piv/views/actions.dart | 7 +- lib/piv/views/cert_info_view.dart | 99 +++++++++++++++++++++++++++ lib/piv/views/import_file_dialog.dart | 41 ++++++++++- lib/piv/views/slot_dialog.dart | 56 +-------------- 9 files changed, 245 insertions(+), 134 deletions(-) create mode 100644 lib/piv/views/cert_info_view.dart diff --git a/helper/helper/piv.py b/helper/helper/piv.py index 5d7390c1..2478aa78 100644 --- a/helper/helper/piv.py +++ b/helper/helper/piv.py @@ -240,11 +240,15 @@ class PivNode(RpcNode): password = params.pop("password", None) try: private_key, certs = _parse_file(data, password) + certificate = _choose_cert(certs) + return dict( status=True, password=password is not None, - private_key=bool(private_key), - certificates=len(certs), + key_type=KEY_TYPE.from_public_key(private_key.public_key()) + if private_key + else None, + cert_info=_get_cert_info(certificate), ) except InvalidPasswordError: logger.debug("Invalid or missing password", exc_info=True) @@ -279,6 +283,29 @@ def _parse_file(data, password=None): return private_key, certs +def _choose_cert(certs): + if certs: + if len(certs) > 1: + leafs = get_leaf_certificates(certs) + return leafs[0] + else: + return certs[0] + return None + + +def _get_cert_info(cert): + if cert is None: + return None + return dict( + subject=cert.subject.rfc4514_string(), + issuer=cert.issuer.rfc4514_string(), + serial=hex(cert.serial_number)[2:], + not_valid_before=cert.not_valid_before.isoformat(), + not_valid_after=cert.not_valid_after.isoformat(), + fingerprint=cert.fingerprint(hashes.SHA256()), + ) + + class SlotsNode(RpcNode): def __init__(self, session): super().__init__() @@ -314,16 +341,7 @@ class SlotsNode(RpcNode): slot=int(slot), name=slot.name, has_key=metadata is not None if self._has_metadata else None, - cert_info=dict( - subject=cert.subject.rfc4514_string(), - issuer=cert.issuer.rfc4514_string(), - serial=hex(cert.serial_number)[2:], - not_valid_before=cert.not_valid_before.isoformat(), - not_valid_after=cert.not_valid_after.isoformat(), - fingerprint=cert.fingerprint(hashes.SHA256()), - ) - if cert - else None, + cert_info=_get_cert_info(cert), ) for slot, (metadata, cert) in self._slots.items() } @@ -390,12 +408,8 @@ class SlotNode(RpcNode): except (ApduError, BadResponseError): pass - if certs: - if len(certs) > 1: - leafs = get_leaf_certificates(certs) - certificate = leafs[0] - else: - certificate = certs[0] + certificate = _choose_cert(certs) + if certificate: self.session.put_certificate(self.slot, certificate) self.session.put_object(OBJECT_ID.CHUID, generate_chuid()) self.certificate = certificate diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 03b20f80..e8cae693 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -43,20 +43,9 @@ "label": {} } }, - "l_bullet": "• {item}", - "@l_bullet" : { - "placeholders": { - "item": {} - } - }, - "s_definition": "{item}:", - "@s_definition" : { - "placeholders": { - "item": {} - } - }, "s_about": "About", + "s_algorithm": "Algorithm", "s_appearance": "Appearance", "s_authenticator": "Authenticator", "s_actions": "Actions", @@ -460,7 +449,7 @@ }, "l_certificate_deleted": "Certificate deleted", "p_password_protected_file": "The selected file is password protected. Enter the password to proceed.", - "p_import_items_desc": "The following items will be imported into PIV slot {slot}.", + "p_import_items_desc": "The following item(s) will be imported into PIV slot {slot}.", "@p_import_items_desc" : { "placeholders": { "slot": {} diff --git a/lib/piv/models.dart b/lib/piv/models.dart index d42ab88f..d54eaeee 100644 --- a/lib/piv/models.dart +++ b/lib/piv/models.dart @@ -266,8 +266,8 @@ class PivSlot with _$PivSlot { class PivExamineResult with _$PivExamineResult { factory PivExamineResult.result({ required bool password, - required bool privateKey, - required int certificates, + required KeyType? keyType, + required CertInfo? certInfo, }) = _ExamineResult; factory PivExamineResult.invalidPassword() = _InvalidPassword; diff --git a/lib/piv/models.freezed.dart b/lib/piv/models.freezed.dart index 8a1637f5..9352b2d4 100644 --- a/lib/piv/models.freezed.dart +++ b/lib/piv/models.freezed.dart @@ -1860,20 +1860,23 @@ PivExamineResult _$PivExamineResultFromJson(Map json) { mixin _$PivExamineResult { @optionalTypeArgs TResult when({ - required TResult Function(bool password, bool privateKey, int certificates) + required TResult Function( + bool password, KeyType? keyType, CertInfo? certInfo) result, required TResult Function() invalidPassword, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ - TResult? Function(bool password, bool privateKey, int certificates)? result, + TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo)? + result, TResult? Function()? invalidPassword, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ - TResult Function(bool password, bool privateKey, int certificates)? result, + TResult Function(bool password, KeyType? keyType, CertInfo? certInfo)? + result, TResult Function()? invalidPassword, required TResult orElse(), }) => @@ -1924,7 +1927,9 @@ abstract class _$$_ExamineResultCopyWith<$Res> { _$_ExamineResult value, $Res Function(_$_ExamineResult) then) = __$$_ExamineResultCopyWithImpl<$Res>; @useResult - $Res call({bool password, bool privateKey, int certificates}); + $Res call({bool password, KeyType? keyType, CertInfo? certInfo}); + + $CertInfoCopyWith<$Res>? get certInfo; } /// @nodoc @@ -1939,24 +1944,36 @@ class __$$_ExamineResultCopyWithImpl<$Res> @override $Res call({ Object? password = null, - Object? privateKey = null, - Object? certificates = null, + Object? keyType = freezed, + Object? certInfo = freezed, }) { return _then(_$_ExamineResult( password: null == password ? _value.password : password // ignore: cast_nullable_to_non_nullable as bool, - privateKey: null == privateKey - ? _value.privateKey - : privateKey // ignore: cast_nullable_to_non_nullable - as bool, - certificates: null == certificates - ? _value.certificates - : certificates // ignore: cast_nullable_to_non_nullable - as int, + keyType: freezed == keyType + ? _value.keyType + : keyType // ignore: cast_nullable_to_non_nullable + as KeyType?, + certInfo: freezed == certInfo + ? _value.certInfo + : certInfo // ignore: cast_nullable_to_non_nullable + as CertInfo?, )); } + + @override + @pragma('vm:prefer-inline') + $CertInfoCopyWith<$Res>? get certInfo { + if (_value.certInfo == null) { + return null; + } + + return $CertInfoCopyWith<$Res>(_value.certInfo!, (value) { + return _then(_value.copyWith(certInfo: value)); + }); + } } /// @nodoc @@ -1964,8 +1981,8 @@ class __$$_ExamineResultCopyWithImpl<$Res> class _$_ExamineResult implements _ExamineResult { _$_ExamineResult( {required this.password, - required this.privateKey, - required this.certificates, + required this.keyType, + required this.certInfo, final String? $type}) : $type = $type ?? 'result'; @@ -1975,16 +1992,16 @@ class _$_ExamineResult implements _ExamineResult { @override final bool password; @override - final bool privateKey; + final KeyType? keyType; @override - final int certificates; + final CertInfo? certInfo; @JsonKey(name: 'runtimeType') final String $type; @override String toString() { - return 'PivExamineResult.result(password: $password, privateKey: $privateKey, certificates: $certificates)'; + return 'PivExamineResult.result(password: $password, keyType: $keyType, certInfo: $certInfo)'; } @override @@ -1994,16 +2011,14 @@ class _$_ExamineResult implements _ExamineResult { other is _$_ExamineResult && (identical(other.password, password) || other.password == password) && - (identical(other.privateKey, privateKey) || - other.privateKey == privateKey) && - (identical(other.certificates, certificates) || - other.certificates == certificates)); + (identical(other.keyType, keyType) || other.keyType == keyType) && + (identical(other.certInfo, certInfo) || + other.certInfo == certInfo)); } @JsonKey(ignore: true) @override - int get hashCode => - Object.hash(runtimeType, password, privateKey, certificates); + int get hashCode => Object.hash(runtimeType, password, keyType, certInfo); @JsonKey(ignore: true) @override @@ -2014,31 +2029,34 @@ class _$_ExamineResult implements _ExamineResult { @override @optionalTypeArgs TResult when({ - required TResult Function(bool password, bool privateKey, int certificates) + required TResult Function( + bool password, KeyType? keyType, CertInfo? certInfo) result, required TResult Function() invalidPassword, }) { - return result(password, privateKey, certificates); + return result(password, keyType, certInfo); } @override @optionalTypeArgs TResult? whenOrNull({ - TResult? Function(bool password, bool privateKey, int certificates)? result, + TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo)? + result, TResult? Function()? invalidPassword, }) { - return result?.call(password, privateKey, certificates); + return result?.call(password, keyType, certInfo); } @override @optionalTypeArgs TResult maybeWhen({ - TResult Function(bool password, bool privateKey, int certificates)? result, + TResult Function(bool password, KeyType? keyType, CertInfo? certInfo)? + result, TResult Function()? invalidPassword, required TResult orElse(), }) { if (result != null) { - return result(password, privateKey, certificates); + return result(password, keyType, certInfo); } return orElse(); } @@ -2085,15 +2103,15 @@ class _$_ExamineResult implements _ExamineResult { abstract class _ExamineResult implements PivExamineResult { factory _ExamineResult( {required final bool password, - required final bool privateKey, - required final int certificates}) = _$_ExamineResult; + required final KeyType? keyType, + required final CertInfo? certInfo}) = _$_ExamineResult; factory _ExamineResult.fromJson(Map json) = _$_ExamineResult.fromJson; bool get password; - bool get privateKey; - int get certificates; + KeyType? get keyType; + CertInfo? get certInfo; @JsonKey(ignore: true) _$$_ExamineResultCopyWith<_$_ExamineResult> get copyWith => throw _privateConstructorUsedError; @@ -2145,7 +2163,8 @@ class _$_InvalidPassword implements _InvalidPassword { @override @optionalTypeArgs TResult when({ - required TResult Function(bool password, bool privateKey, int certificates) + required TResult Function( + bool password, KeyType? keyType, CertInfo? certInfo) result, required TResult Function() invalidPassword, }) { @@ -2155,7 +2174,8 @@ class _$_InvalidPassword implements _InvalidPassword { @override @optionalTypeArgs TResult? whenOrNull({ - TResult? Function(bool password, bool privateKey, int certificates)? result, + TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo)? + result, TResult? Function()? invalidPassword, }) { return invalidPassword?.call(); @@ -2164,7 +2184,8 @@ class _$_InvalidPassword implements _InvalidPassword { @override @optionalTypeArgs TResult maybeWhen({ - TResult Function(bool password, bool privateKey, int certificates)? result, + TResult Function(bool password, KeyType? keyType, CertInfo? certInfo)? + result, TResult Function()? invalidPassword, required TResult orElse(), }) { diff --git a/lib/piv/models.g.dart b/lib/piv/models.g.dart index 109f280d..763d8393 100644 --- a/lib/piv/models.g.dart +++ b/lib/piv/models.g.dart @@ -168,16 +168,18 @@ const _$SlotIdEnumMap = { _$_ExamineResult _$$_ExamineResultFromJson(Map json) => _$_ExamineResult( password: json['password'] as bool, - privateKey: json['private_key'] as bool, - certificates: json['certificates'] as int, + keyType: $enumDecodeNullable(_$KeyTypeEnumMap, json['key_type']), + certInfo: json['cert_info'] == null + ? null + : CertInfo.fromJson(json['cert_info'] as Map), $type: json['runtimeType'] as String?, ); Map _$$_ExamineResultToJson(_$_ExamineResult instance) => { 'password': instance.password, - 'private_key': instance.privateKey, - 'certificates': instance.certificates, + 'key_type': _$KeyTypeEnumMap[instance.keyType], + 'cert_info': instance.certInfo, 'runtimeType': instance.$type, }; diff --git a/lib/piv/views/actions.dart b/lib/piv/views/actions.dart index 19bebbc2..183e711c 100644 --- a/lib/piv/views/actions.dart +++ b/lib/piv/views/actions.dart @@ -130,7 +130,8 @@ Widget registerPivActions( }); }), ImportIntent: CallbackAction(onInvoke: (intent) async { - if (!await _authIfNeeded(ref, devicePath, pivState)) { + if (!pivState.protectedKey && + !await _authIfNeeded(ref, devicePath, pivState)) { return false; } @@ -198,9 +199,11 @@ Widget registerPivActions( return true; }), DeleteIntent: CallbackAction(onInvoke: (_) async { - if (!await _authIfNeeded(ref, devicePath, pivState)) { + if (!pivState.protectedKey && + !await _authIfNeeded(ref, devicePath, pivState)) { return false; } + final withContext = ref.read(withContextProvider); final bool? deleted = await withContext((context) async => await showBlurDialog( diff --git a/lib/piv/views/cert_info_view.dart b/lib/piv/views/cert_info_view.dart new file mode 100644 index 00000000..013dc742 --- /dev/null +++ b/lib/piv/views/cert_info_view.dart @@ -0,0 +1,99 @@ +/* + * 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 'package:intl/intl.dart'; +import 'package:yubico_authenticator/app/message.dart'; +import 'package:yubico_authenticator/app/state.dart'; +import 'package:yubico_authenticator/piv/models.dart'; +import 'package:yubico_authenticator/widgets/tooltip_if_truncated.dart'; + +class CertInfoTable extends ConsumerWidget { + final CertInfo certInfo; + + const CertInfoTable(this.certInfo, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + final textTheme = Theme.of(context).textTheme; + // This is what ListTile uses for subtitle + final subtitleStyle = textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ); + final dateFormat = DateFormat.yMMMEd(); + final clipboard = ref.watch(clipboardProvider); + final withContext = ref.watch(withContextProvider); + + Widget header(String title) => Text( + title, + textAlign: TextAlign.right, + ); + + Widget body(String title, String value) => GestureDetector( + onDoubleTap: () async { + await clipboard.setText(value); + if (!clipboard.platformGivesFeedback()) { + await withContext((context) async { + showMessage(context, l10n.p_target_copied_clipboard(title)); + }); + } + }, + child: TooltipIfTruncated( + text: value, + style: subtitleStyle, + tooltip: value.replaceAllMapped( + RegExp(r',([A-Z]+)='), (match) => '\n${match[1]}='), + ), + ); + + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + header(l10n.s_subject), + header(l10n.s_issuer), + header(l10n.s_serial), + header(l10n.s_certificate_fingerprint), + header(l10n.s_valid_from), + header(l10n.s_valid_to), + ], + ), + const SizedBox(width: 8), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + body(l10n.s_subject, certInfo.subject), + body(l10n.s_issuer, certInfo.issuer), + body(l10n.s_serial, certInfo.serial), + body(l10n.s_certificate_fingerprint, certInfo.fingerprint), + body(l10n.s_valid_from, + dateFormat.format(DateTime.parse(certInfo.notValidBefore))), + body(l10n.s_valid_to, + dateFormat.format(DateTime.parse(certInfo.notValidAfter))), + ], + ), + ), + ], + ); + } +} diff --git a/lib/piv/views/import_file_dialog.dart b/lib/piv/views/import_file_dialog.dart index 1af99ef9..92e616e1 100644 --- a/lib/piv/views/import_file_dialog.dart +++ b/lib/piv/views/import_file_dialog.dart @@ -25,6 +25,7 @@ import '../../widgets/responsive_dialog.dart'; import '../models.dart'; import '../state.dart'; import '../keys.dart' as keys; +import 'cert_info_view.dart'; class ImportFileDialog extends ConsumerStatefulWidget { final DevicePath devicePath; @@ -77,6 +78,11 @@ class _ImportFileDialogState extends ConsumerState { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; + final textTheme = Theme.of(context).textTheme; + // This is what ListTile uses for subtitle + final subtitleStyle = textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ); final state = _state; if (state == null) { return ResponsiveDialog( @@ -141,7 +147,7 @@ class _ImportFileDialogState extends ConsumerState { ), ), ), - result: (_, privateKey, certificates) => ResponsiveDialog( + result: (_, keyType, certInfo) => ResponsiveDialog( title: Text(l10n.l_import_file), actions: [ TextButton( @@ -171,8 +177,37 @@ class _ImportFileDialogState extends ConsumerState { children: [ Text(l10n.p_import_items_desc( widget.pivSlot.slot.getDisplayName(l10n))), - if (privateKey) Text(l10n.l_bullet(l10n.s_private_key)), - if (certificates > 0) Text(l10n.l_bullet(l10n.s_certificate)), + if (keyType != null) ...[ + Text( + l10n.s_private_key, + style: textTheme.bodyLarge, + softWrap: true, + textAlign: TextAlign.center, + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(l10n.s_algorithm), + const SizedBox(width: 8), + Text( + keyType.name.toUpperCase(), + style: subtitleStyle, + ), + ], + ) + ], + if (certInfo != null) ...[ + Text( + l10n.s_certificate, + style: textTheme.bodyLarge, + softWrap: true, + textAlign: TextAlign.center, + ), + SizedBox( + height: 120, // Needed for layout, adapt if text sizes changes + child: CertInfoTable(certInfo), + ), + ] ] .map((e) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), diff --git a/lib/piv/views/slot_dialog.dart b/lib/piv/views/slot_dialog.dart index 7477f847..31f0f460 100644 --- a/lib/piv/views/slot_dialog.dart +++ b/lib/piv/views/slot_dialog.dart @@ -17,16 +17,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; -import '../../app/message.dart'; import '../../app/state.dart'; import '../../app/views/fs_dialog.dart'; import '../../app/views/action_list.dart'; -import '../../widgets/tooltip_if_truncated.dart'; import '../models.dart'; import '../state.dart'; import 'actions.dart'; +import 'cert_info_view.dart'; class SlotDialog extends ConsumerWidget { final SlotId pivSlot; @@ -48,8 +46,6 @@ class SlotDialog extends ConsumerWidget { final subtitleStyle = textTheme.bodyMedium!.copyWith( color: textTheme.bodySmall!.color, ); - final clipboard = ref.watch(clipboardProvider); - final withContext = ref.read(withContextProvider); final pivState = ref.watch(pivStateProvider(node.path)).valueOrNull; final slotData = ref.watch(pivSlotsProvider(node.path).select((value) => @@ -61,34 +57,6 @@ class SlotDialog extends ConsumerWidget { return const FsDialog(child: CircularProgressIndicator()); } - TableRow detailRow(String title, String value) { - return TableRow( - children: [ - Text( - l10n.s_definition(title), - textAlign: TextAlign.right, - ), - const SizedBox(width: 8.0), - GestureDetector( - onDoubleTap: () async { - await clipboard.setText(value); - if (!clipboard.platformGivesFeedback()) { - await withContext((context) async { - showMessage(context, l10n.p_target_copied_clipboard(title)); - }); - } - }, - child: TooltipIfTruncated( - text: value, - style: subtitleStyle, - tooltip: value.replaceAllMapped( - RegExp(r',([A-Z]+)='), (match) => '\n${match[1]}='), - ), - ), - ], - ); - } - final certInfo = slotData.certInfo; return registerPivActions( node.path, @@ -113,27 +81,7 @@ class SlotDialog extends ConsumerWidget { if (certInfo != null) ...[ Padding( padding: const EdgeInsets.all(16), - child: Table( - defaultColumnWidth: const IntrinsicColumnWidth(), - columnWidths: const {2: FlexColumnWidth()}, - children: [ - detailRow(l10n.s_subject, certInfo.subject), - detailRow(l10n.s_issuer, certInfo.issuer), - detailRow(l10n.s_serial, certInfo.serial), - detailRow(l10n.s_certificate_fingerprint, - certInfo.fingerprint), - detailRow( - l10n.s_valid_from, - DateFormat.yMMMEd().format( - DateTime.parse(certInfo.notValidBefore)), - ), - detailRow( - l10n.s_valid_to, - DateFormat.yMMMEd().format( - DateTime.parse(certInfo.notValidAfter)), - ), - ], - ), + child: CertInfoTable(certInfo), ), ] else ...[ Padding( From 7bc79a08facd62078725b2bd312519e6ed58d34d Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 24 Aug 2023 09:51:43 +0200 Subject: [PATCH 113/158] Add overwrite confirmation. --- lib/l10n/app_en.arb | 17 +++++ lib/piv/views/actions.dart | 42 +++++------ lib/piv/views/generate_key_dialog.dart | 10 +++ lib/piv/views/import_file_dialog.dart | 44 +++++++---- lib/piv/views/overwrite_confirm_dialog.dart | 83 +++++++++++++++++++++ 5 files changed, 160 insertions(+), 36 deletions(-) create mode 100644 lib/piv/views/overwrite_confirm_dialog.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e8cae693..360dc8df 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -30,6 +30,7 @@ "s_unlock": "Unlock", "s_calculate": "Calculate", "s_import": "Import", + "s_overwrite": "Overwrite", "s_label": "Label", "s_name": "Name", "s_usb": "USB", @@ -43,6 +44,12 @@ "label": {} } }, + "l_bullet": "• {item}", + "@l_bullet" : { + "placeholders": { + "item": {} + } + }, "s_about": "About", "s_algorithm": "Algorithm", @@ -459,6 +466,16 @@ "l_rfc4514_invalid": "Invalid RFC 4514 format", "rfc4514_examples": "Examples:\nCN=Example Name\nCN=jsmith,DC=example,DC=net", "p_cert_options_desc": "Key algorithm to use, output format, and an expiration date (if applicable).", + "s_overwrite_slot": "Overwrite slot", + "p_overwrite_slot_desc": "This will permanently overwrite existing content in slot {slot}.", + "@p_overwrite_slot_desc" : { + "placeholders": { + "slot": {} + } + }, + "l_overwrite_cert": "The certificate will be overwritten", + "l_overwrite_key": "The private key will be overwritten", + "l_overwrite_key_maybe": "Any existing private key in the slot will be overwritten", "@_piv_slots": {}, "s_slot_display_name": "{name} ({hexid})", diff --git a/lib/piv/views/actions.dart b/lib/piv/views/actions.dart index 183e711c..47ac2a32 100644 --- a/lib/piv/views/actions.dart +++ b/lib/piv/views/actions.dart @@ -47,23 +47,23 @@ class ExportIntent extends Intent { } Future _authenticate( - WidgetRef ref, DevicePath devicePath, PivState pivState) async { - final withContext = ref.read(withContextProvider); - return await withContext((context) async => - await showBlurDialog( + BuildContext context, DevicePath devicePath, PivState pivState) async { + return await showBlurDialog( context: context, - builder: (context) => AuthenticationDialog( - devicePath, - pivState, - ), + builder: (context) => pivState.protectedKey + ? PinDialog(devicePath) + : AuthenticationDialog( + devicePath, + pivState, + ), ) ?? - false); + false; } Future _authIfNeeded( - WidgetRef ref, DevicePath devicePath, PivState pivState) async { + BuildContext context, DevicePath devicePath, PivState pivState) async { if (pivState.needsAuth) { - return await _authenticate(ref, devicePath, pivState); + return await _authenticate(context, devicePath, pivState); } return true; } @@ -80,13 +80,13 @@ Widget registerPivActions( actions: { GenerateIntent: CallbackAction(onInvoke: (intent) async { + final withContext = ref.read(withContextProvider); if (!pivState.protectedKey && - !await _authIfNeeded(ref, devicePath, pivState)) { + !await withContext( + (context) => _authIfNeeded(context, devicePath, pivState))) { return false; } - final withContext = ref.read(withContextProvider); - // TODO: Avoid asking for PIN if not needed? final verified = await withContext((context) async => await showBlurDialog( @@ -130,13 +130,13 @@ Widget registerPivActions( }); }), ImportIntent: CallbackAction(onInvoke: (intent) async { - if (!pivState.protectedKey && - !await _authIfNeeded(ref, devicePath, pivState)) { + final withContext = ref.read(withContextProvider); + + if (!await withContext( + (context) => _authIfNeeded(context, devicePath, pivState))) { return false; } - final withContext = ref.read(withContextProvider); - final picked = await withContext( (context) async { final l10n = AppLocalizations.of(context)!; @@ -199,12 +199,12 @@ Widget registerPivActions( return true; }), DeleteIntent: CallbackAction(onInvoke: (_) async { - if (!pivState.protectedKey && - !await _authIfNeeded(ref, devicePath, pivState)) { + final withContext = ref.read(withContextProvider); + if (!await withContext( + (context) => _authIfNeeded(context, devicePath, pivState))) { return false; } - final withContext = ref.read(withContextProvider); final bool? deleted = await withContext((context) async => await showBlurDialog( context: context, diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart index 901ea632..56762d44 100644 --- a/lib/piv/views/generate_key_dialog.dart +++ b/lib/piv/views/generate_key_dialog.dart @@ -27,6 +27,7 @@ import '../../widgets/responsive_dialog.dart'; import '../models.dart'; import '../state.dart'; import '../keys.dart' as keys; +import 'overwrite_confirm_dialog.dart'; class GenerateKeyDialog extends ConsumerStatefulWidget { final DevicePath devicePath; @@ -80,6 +81,15 @@ class _GenerateKeyDialogState extends ConsumerState { onPressed: _generating || _invalidSubject ? null : () async { + if (!await confirmOverwrite( + context, + widget.pivSlot, + writeKey: true, + writeCert: _generateType == GenerateType.certificate, + )) { + return; + } + setState(() { _generating = true; }); diff --git a/lib/piv/views/import_file_dialog.dart b/lib/piv/views/import_file_dialog.dart index 92e616e1..c11fdba7 100644 --- a/lib/piv/views/import_file_dialog.dart +++ b/lib/piv/views/import_file_dialog.dart @@ -26,6 +26,7 @@ import '../models.dart'; import '../state.dart'; import '../keys.dart' as keys; import 'cert_info_view.dart'; +import 'overwrite_confirm_dialog.dart'; class ImportFileDialog extends ConsumerStatefulWidget { final DevicePath devicePath; @@ -152,21 +153,34 @@ class _ImportFileDialogState extends ConsumerState { actions: [ TextButton( key: keys.unlockButton, - onPressed: () async { - final navigator = Navigator.of(context); - try { - await ref - .read(pivSlotsProvider(widget.devicePath).notifier) - .import(widget.pivSlot.slot, _data, - password: _password.isNotEmpty ? _password : null); - navigator.pop(true); - } catch (_) { - // TODO: More error cases - setState(() { - _passwordIsWrong = true; - }); - } - }, + onPressed: (keyType == null && certInfo == null) + ? null + : () async { + final navigator = Navigator.of(context); + + if (!await confirmOverwrite( + context, + widget.pivSlot, + writeKey: keyType != null, + writeCert: certInfo != null, + )) { + return; + } + + try { + await ref + .read(pivSlotsProvider(widget.devicePath).notifier) + .import(widget.pivSlot.slot, _data, + password: + _password.isNotEmpty ? _password : null); + navigator.pop(true); + } catch (err) { + // TODO: More error cases + setState(() { + _passwordIsWrong = true; + }); + } + }, child: Text(l10n.s_import), ), ], diff --git a/lib/piv/views/overwrite_confirm_dialog.dart b/lib/piv/views/overwrite_confirm_dialog.dart new file mode 100644 index 00000000..50ade8ab --- /dev/null +++ b/lib/piv/views/overwrite_confirm_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 '../../app/message.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; + +class _OverwriteConfirmDialog extends StatelessWidget { + final SlotId slot; + final bool certificate; + final bool? privateKey; + + const _OverwriteConfirmDialog({ + required this.certificate, + required this.privateKey, + required this.slot, + }); + @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(slot.getDisplayName(l10n))), + const SizedBox(height: 12), + if (certificate) Text(l10n.l_bullet(l10n.l_overwrite_cert)), + if (privateKey == true) Text(l10n.l_bullet(l10n.l_overwrite_key)), + if (privateKey == null) + Text(l10n.l_bullet(l10n.l_overwrite_key_maybe)), + ], + ), + ), + ); + } +} + +Future confirmOverwrite( + BuildContext context, + PivSlot pivSlot, { + required bool writeKey, + required bool writeCert, +}) async { + final overwritesCert = writeCert && pivSlot.certInfo != null; + final overwritesKey = writeKey ? pivSlot.hasKey : false; + if (overwritesCert || overwritesKey != false) { + return await showBlurDialog( + context: context, + builder: (context) => _OverwriteConfirmDialog( + slot: pivSlot.slot, + certificate: overwritesCert, + privateKey: overwritesKey, + )) ?? + false; + } + return true; +} From bfc47ee1929b3e3244ce2717e56bb6c1aa4f55c6 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 24 Aug 2023 10:14:35 +0200 Subject: [PATCH 114/158] Add Generate description. --- lib/piv/views/generate_key_dialog.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart index 56762d44..77d174f0 100644 --- a/lib/piv/views/generate_key_dialog.dart +++ b/lib/piv/views/generate_key_dialog.dart @@ -150,6 +150,8 @@ class _GenerateKeyDialogState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + l10n.p_generate_desc(widget.pivSlot.slot.getDisplayName(l10n))), Text( l10n.s_subject, style: textTheme.bodyLarge, From 2e98de5c1efb3a1aa17df0762dcca3f4ff53639d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Tro=C3=ABng?= Date: Thu, 24 Aug 2023 12:31:28 +0200 Subject: [PATCH 115/158] Adding swig to build requirements. --- doc/Development.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/Development.adoc b/doc/Development.adoc index 0ed555f0..db5ac6a5 100644 --- a/doc/Development.adoc +++ b/doc/Development.adoc @@ -28,7 +28,7 @@ on `yubikey-manager >=5, <6`. It will likely not work with versions outside this range! === Building the Yubico Authenticator Helper -Requirements: Python >= 3.8 and Poetry. +*Requirements: Python >= 3.8, SWIG, and Poetry.* The GUI requires a compiled version of Helper to run, which is built from the sources in helper/ in this repository. This needs to be build prior to running From 1be66e1851a47de46c4d992ca54896d7545ac421 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 24 Aug 2023 15:43:23 +0200 Subject: [PATCH 116/158] Tweak string. --- lib/l10n/app_en.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 360dc8df..32b00d5b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -465,7 +465,7 @@ "p_subject_desc": "A distinguished name (DN) formatted in accordance to the RFC 4514 specification.", "l_rfc4514_invalid": "Invalid RFC 4514 format", "rfc4514_examples": "Examples:\nCN=Example Name\nCN=jsmith,DC=example,DC=net", - "p_cert_options_desc": "Key algorithm to use, output format, and an expiration date (if applicable).", + "p_cert_options_desc": "Key algorithm to use, output format, and expiration date (certificate only).", "s_overwrite_slot": "Overwrite slot", "p_overwrite_slot_desc": "This will permanently overwrite existing content in slot {slot}.", "@p_overwrite_slot_desc" : { From 34fc3ab788533d8566098d3e47535c3eab351a4a Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 25 Aug 2023 10:23:59 +0200 Subject: [PATCH 117/158] Use Flutter 3.13.1 with external config. --- .github/workflows/android.yml | 14 +++++++++----- .github/workflows/check-strings.yml | 5 +++-- .github/workflows/codeql-analysis.yml | 14 +++++++++----- .github/workflows/env | 2 ++ .github/workflows/linux.yml | 10 +++++++--- .github/workflows/macos.yml | 6 ++++-- .github/workflows/windows.yml | 8 +++++--- 7 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/env diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index f34f2d5c..cab4abce 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -13,19 +13,23 @@ jobs: distribution: 'zulu' java-version: '11' + - uses: actions/checkout@v3 + with: + path: 'app' + + - name: Read variables from repo + run: cat .github/workflows/env >> $GITHUB_ENV + working-directory: ./app + - name: Install Flutter uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.13.0' + flutter-version: ${{ env.FLUTTER }} - run: | flutter config flutter --version - - uses: actions/checkout@v3 - with: - path: 'app' - - name: Check app versions run: | python set-version.py diff --git a/.github/workflows/check-strings.yml b/.github/workflows/check-strings.yml index 0ef4c4f4..15a10289 100644 --- a/.github/workflows/check-strings.yml +++ b/.github/workflows/check-strings.yml @@ -6,12 +6,13 @@ jobs: strings: runs-on: ubuntu-latest - env: - FLUTTER: '3.13.0' steps: - uses: actions/checkout@v3 + - name: Read variables from repo + run: cat .github/workflows/env >> $GITHUB_ENV + - name: Ensure main locale is correct run: python check_strings.py lib/l10n/app_en.arb diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ae37d9aa..48d9cc71 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,19 +39,23 @@ jobs: distribution: 'zulu' java-version: '11' + - uses: actions/checkout@v3 + with: + path: 'app' + + - name: Read variables from repo + run: cat .github/workflows/env >> $GITHUB_ENV + working-directory: ./app + - name: Install Flutter uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.13.0' + flutter-version: ${{ env.FLUTTER }} - run: | flutter config flutter --version - - uses: actions/checkout@v3 - with: - path: 'app' - - name: Run flutter tests run: | flutter test diff --git a/.github/workflows/env b/.github/workflows/env new file mode 100644 index 00000000..b33ad367 --- /dev/null +++ b/.github/workflows/env @@ -0,0 +1,2 @@ +FLUTTER=3.13.1 +PYVER=3.11.4 diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index c7a7947c..acd1d7db 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -6,15 +6,19 @@ jobs: build: runs-on: ubuntu-latest - env: - PYVER: '3.11.4' - FLUTTER: '3.13.0' container: image: ubuntu:20.04 env: DEBIAN_FRONTEND: noninteractive steps: + - uses: actions/checkout@v3 + with: + sparse-checkout: .github/workflows/env + + - name: Read variables from repo + run: cat .github/workflows/env >> $GITHUB_ENV + - name: Install dependencies run: | export PYVER_MINOR=${PYVER%.*} diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 4e0cd7c1..ed5d30b5 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -7,12 +7,14 @@ jobs: runs-on: macos-latest env: - PYVER: '3.11.4' MACOSX_DEPLOYMENT_TARGET: "10.15" steps: - uses: actions/checkout@v3 + - name: Read variables from repo + run: cat .github/workflows/env >> $GITHUB_ENV + - name: Check app versions run: | python3 set-version.py @@ -49,7 +51,7 @@ jobs: with: channel: 'stable' architecture: 'x64' - flutter-version: '3.13.0' + flutter-version: ${{ env.FLUTTER }} - run: flutter config --enable-macos-desktop - run: flutter --version diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 4e8f6d95..06d7f737 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -6,12 +6,14 @@ jobs: build: runs-on: windows-latest - env: - PYVER: '3.11.4' steps: - uses: actions/checkout@v3 + - name: Read variables from repo + shell: bash + run: cat .github/workflows/env >> $GITHUB_ENV + - name: Check app versions run: | python set-version.py @@ -45,7 +47,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.13.0' + flutter-version: ${{ env.FLUTTER }} - run: flutter config --enable-windows-desktop - run: flutter --version From f19ccd3cf1f62ef55ecc21958222c649b39a57f3 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 25 Aug 2023 10:48:47 +0200 Subject: [PATCH 118/158] Bump desktop_drop. --- pubspec.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index a7383f6f..940c9346 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: built_value - sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + sha256: ff627b645b28fb8bdb69e645f910c2458fd6b65f6585c3a53e0626024897dedf url: "https://pub.dev" source: hosted - version: "8.6.1" + version: "8.6.2" characters: dependency: transitive description: @@ -197,10 +197,10 @@ packages: dependency: "direct main" description: name: desktop_drop - sha256: "4ca4d960f4b11c032e9adfd2a0a8ac615bc3fddb4cbe73dcf840dd8077582186" + sha256: ebba9c9cb0b54385998a977d741cc06fd8324878c08d5a36e9da61cd56b04cc6 url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.3" fake_async: dependency: transitive description: @@ -964,10 +964,10 @@ packages: dependency: transitive description: name: win32 - sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 + sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" url: "https://pub.dev" source: hosted - version: "5.0.6" + version: "5.0.7" window_manager: dependency: "direct main" description: From 06c396d7904cbc50c2c33fc764f4cc6eec39b1cf Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 25 Aug 2023 10:51:25 +0200 Subject: [PATCH 119/158] Bump to Python 3.11.5. --- .github/workflows/env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/env b/.github/workflows/env index b33ad367..be866198 100644 --- a/.github/workflows/env +++ b/.github/workflows/env @@ -1,2 +1,2 @@ FLUTTER=3.13.1 -PYVER=3.11.4 +PYVER=3.11.5 From 3b9aa5b706416087b1cb22cfe546811b504ce096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Tro=C3=ABng?= Date: Fri, 25 Aug 2023 11:31:03 +0200 Subject: [PATCH 120/158] Inserting all the prerequisites relating to building helper, i.e. yubikey-manager. --- doc/Development.adoc | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/doc/Development.adoc b/doc/Development.adoc index db5ac6a5..4d116bf1 100644 --- a/doc/Development.adoc +++ b/doc/Development.adoc @@ -28,12 +28,30 @@ on `yubikey-manager >=5, <6`. It will likely not work with versions outside this range! === Building the Yubico Authenticator Helper -*Requirements: Python >= 3.8, SWIG, and Poetry.* - The GUI requires a compiled version of Helper to run, which is built from the -sources in helper/ in this repository. This needs to be build prior to running -`flutter build` or `flutter run`, by running `build-helper.sh` (or -`build-helper.bat` on Windows). +sources in helper/ in this repository. Requirements for all platforms are +Python >= 3.8 and Poetry. This needs to be built prior to running +`flutter build` or `flutter run`. + +==== Windows + +Make sure the http://www.swig.org/[swig] executable is in your PATH. + +==== macOS + + $ brew install swig + +==== Linux (Debian-based distributions) + + $ sudo apt install swig libu2f-udev pcscd libpcsclite-dev + +==== Linux (RPM-based distributons) + + # Tested on Fedora 34 + $ sudo dnf install pcsc-lite-devel python3-devel swig + + When prerequisites are installed you build the helper by running +`build-helper.sh` (or`build-helper.bat` on Windows). NOTE: You will need to re-run the build script if changes have been made to Helper's code, or if `flutter clean` has been run. From 5212374101338170a872374985377b03ea43b16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Tro=C3=ABng?= Date: Fri, 25 Aug 2023 12:02:05 +0200 Subject: [PATCH 121/158] fixing some spacing and some formatting --- doc/Development.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/Development.adoc b/doc/Development.adoc index 4d116bf1..7d56a93f 100644 --- a/doc/Development.adoc +++ b/doc/Development.adoc @@ -50,8 +50,8 @@ Make sure the http://www.swig.org/[swig] executable is in your PATH. # Tested on Fedora 34 $ sudo dnf install pcsc-lite-devel python3-devel swig - When prerequisites are installed you build the helper by running -`build-helper.sh` (or`build-helper.bat` on Windows). +When prerequisites are installed you build the helper by running `build-helper.sh` +(or `build-helper.bat` on Windows). NOTE: You will need to re-run the build script if changes have been made to Helper's code, or if `flutter clean` has been run. From da116796b29738870f58537aa1b6746268ac28b9 Mon Sep 17 00:00:00 2001 From: Daviteusz Date: Fri, 25 Aug 2023 12:24:47 +0200 Subject: [PATCH 122/158] Update Polish translations --- lib/l10n/app_pl.arb | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index bae772e3..6949d83a 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -48,7 +48,7 @@ } }, "l_bullet": "• {item}", - "l_bypass_touch_requirement": "Obejdź wymóg dotyku", + "l_bypass_touch_requirement": "Obejdź wymóg dotknięcia", "l_bypass_touch_requirement_off": "Konta, które wymagają dotknięcia, potrzebują dodatkowego przyłożenia do NFC", "l_bypass_touch_requirement_on": "Konta, które wymagają dotknięcia, są automatycznie wyświetlane przez NFC", "l_calculate_code_desc": "Uzyskaj nowy kod z klucza YubiKey", @@ -147,6 +147,9 @@ "l_open_connection_failed": "Nie udało się nawiązać połączenia", "l_optional_password_protection": "Opcjonalna ochrona hasłem", "l_optionally_set_a_pin": "Opcjonalnie ustaw PIN, aby chronić dostęp do YubiKey\nZarejestruj jako klucz bezpieczeństwa na stronach internetowych", + "l_overwrite_cert": "Certyfikat zostanie nadpisany", + "l_overwrite_key": "Klucz prywatny zostanie nadpisany", + "l_overwrite_key_maybe": "Każdy istniejący klucz prywatny w slocie zostanie nadpisany", "@l_passkey": { "placeholders": { "label": {} @@ -192,6 +195,7 @@ } }, "l_reset_failed": "Błąd podczas resetowania: {message}", + "l_rfc4514_invalid": "Nieprawidłowy format RFC 4514", "l_select_accounts": "Wybierz konta, które chcesz dodać do YubiKey", "l_select_import_file": "Wybierz plik do zaimportowania", "l_set_icons_for_accounts": "Ustaw ikony dla kont", @@ -234,6 +238,7 @@ "l_yk_no_access": "Dostęp do tego klucza YubiKey jest niemożliwy", "p_add_description": "W celu zeskanowania kodu QR, upewnij się, że pełny kod jest widoczny na ekranie a następnie naciśnij poniższy przycisk. Jeśli posiadasz dane uwierzytelniające do konta w tekstowej formie, skorzystaj z opcji ręcznego wprowadzania danych.", "p_ccid_service_unavailable": "Upewnij się, że usługa kart inteligentnych działa.", + "p_cert_options_desc": "Algorytm klucza do użycia, format wyjściowy i data wygaśnięcia (tylko certyfikat).", "p_change_management_key_desc": "Zmień swój klucz zarządzania. Opcjonalnie możesz zezwolić na używanie kodu PIN zamiast klucza zarządzania.", "p_community_translations_desc": "Tłumaczenia te są dostarczane i utrzymywane przez społeczność. Mogą zawierać błędy lub być niekompletne.", "p_custom_icons_description": "Pakiety ikon mogą sprawić, że Twoje konta będą łatwiejsze do odróżnienia dzięki znanym logo i kolorom.", @@ -267,11 +272,18 @@ }, "p_import_items_desc": "Następujące elementy zostaną zaimportowane do slotu PIV {slot}.", "p_need_camera_permission": "Yubico Authenticator wymaga dostępu do aparatu w celu skanowania kodów QR.", + "@p_overwrite_slot_desc": { + "placeholders": { + "slot": {} + } + }, + "p_overwrite_slot_desc": "Spowoduje to trwałe nadpisanie istniejącej zawartości w slocie {slot}.", "p_password_protected_file": "Wybrany plik jest chroniony hasłem. Wprowadź je, aby kontynuować.", "p_pcscd_unavailable": "Upewnij się, że pcscd jest zainstalowany i uruchomiony.", "p_pin_required_desc": "Czynność, którą zamierzasz wykonać, wymaga wprowadzenia kodu PIN PIV.", "p_press_fingerprint_begin": "Przytrzymaj palec na kluczu YubiKey, aby rozpocząć.", "p_rename_will_change_account_displayed": "Spowoduje to zmianę sposobu wyświetlania konta na liście.", + "p_subject_desc": "Nazwa wyróżniająca (DN) sformatowana zgodnie ze specyfikacją RFC 4514.", "@p_target_copied_clipboard": { "placeholders": { "label": {} @@ -308,6 +320,7 @@ }, "q_rename_target": "Zmienić nazwę {label}?", "q_want_to_scan": "Czy chcesz zeskanować?", + "rfc4514_examples": "Przykłady:\nCN=Przykładowa Nazwa\nCN=jkowalski,DC=przyklad,DC=pl", "s_about": "O aplikacji", "s_account_added": "Konto zostało dodane", "s_account_deleted": "Konto zostało usunięte", @@ -319,6 +332,7 @@ "s_add_accounts": "Dodaj konto(-a)", "s_add_fingerprint": "Dodaj odcisk palca", "s_add_manually": "Dodaj ręcznie", + "s_algorithm": "Algorytm", "s_allow_screenshots": "Zezwalaj na zrzuty ekranu", "s_app_disabled": "Wyłączona funkcja", "s_app_not_supported": "Funkcja nie jest obsługiwana", @@ -355,12 +369,6 @@ "s_current_puk": "Aktualny PUK", "s_custom_icons": "Niestandardowe ikony", "s_dark_mode": "Ciemny", - "@s_definition": { - "placeholders": { - "item": {} - } - }, - "s_definition": "{item}:", "s_delete": "Usuń", "s_delete_account": "Usuń konto", "s_delete_fingerprint": "Usuń odcisk palca", @@ -381,6 +389,7 @@ }, "s_fw_version": "F/W: {version}", "s_generate_key": "Generuj klucz", + "s_generate_random": "Generuj losowo", "s_help_and_about": "Pomoc i informacje", "s_help_and_feedback": "Pomoc i opinie", "s_hide_device": "Ukryj urządzenie", @@ -422,7 +431,7 @@ "s_nfc_dialog_oath_unset_password": "Działanie: usuń hasło OATH", "s_nfc_dialog_operation_failed": "Niepowodzenie", "s_nfc_dialog_operation_success": "Powodzenie", - "s_nfc_dialog_tap_key": "Dotknij swój klucz", + "s_nfc_dialog_tap_key": "Przystaw swój klucz", "s_nfc_options": "Opcje NFC", "s_no_accounts": "Brak kont", "s_no_fingerprints": "Brak odcisków palców", @@ -440,6 +449,9 @@ }, "s_num_sec": "{num} sek", "s_open_src_licenses": "Licencje open source", + "s_options": "Opcje", + "s_overwrite": "Nadpisz", + "s_overwrite_slot": "Nadpisz slot", "s_passkey_deleted": "Usunięto klucz dostępu", "s_passkeys": "Klucze dostępu", "s_password": "Hasło", @@ -522,6 +534,7 @@ "s_unsupported_yk": "Nieobsługiwany klucz YubiKey", "s_usb": "USB", "s_usb_options": "Opcje USB", + "s_use_default": "Użyj domyślnego", "s_valid_from": "Ważny od", "s_valid_to": "Ważny do", "s_webauthn": "WebAuthn", From 729628f56267aa05952005f9efc4e88175279f8e Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Fri, 25 Aug 2023 14:39:47 +0200 Subject: [PATCH 123/158] Use notarytool for notarization --- macos/release-macos.sh | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/macos/release-macos.sh b/macos/release-macos.sh index 1da27815..3ef31b4a 100644 --- a/macos/release-macos.sh +++ b/macos/release-macos.sh @@ -45,32 +45,10 @@ if [ -n "$1" ] && [ -n "$2" ] # Standalone then echo "# Compress the .app to .zip and notarize" ditto -c -k --sequesterRsrc --keepParent "Yubico Authenticator.app" "Yubico Authenticator.zip" - RES=$(xcrun altool -t osx -f "Yubico Authenticator.zip" --primary-bundle-id com.yubico.authenticator --notarize-app -u $1 -p $2) - echo ${RES} - ERRORS=${RES:0:9} - if [ "$ERRORS" != "No errors" ]; then - echo "Error uploading for notarization" - exit - fi - UUID=${RES#*=} - STATUS=$(xcrun altool --notarization-info $UUID -u $1 -p $2) + STATUS=$(xcrun notarytool submit "Yubico Authenticator.zip" --apple-id $1 --team-id LQA3CS5MM7 --password $2 --wait) + echo ${STATUS} - while true - do - if [[ "$STATUS" == *"in progress"* ]]; then - echo "Notarization still in progress. Sleep 30s." - sleep 30 - echo "Retrieving status again." - STATUS=$(xcrun altool --notarization-info $UUID -u $1 -p $2) - else - echo "Status changed." - break - fi - done - - echo "${STATUS}" - - if [[ "$STATUS" == *"success"* ]]; then + if [[ "$STATUS" == *"Accepted"* ]]; then echo "Notarization successfull. Staple the .app" xcrun stapler staple -v "Yubico Authenticator.app" @@ -80,6 +58,9 @@ then mv "Yubico Authenticator.app" source_folder sh create-dmg.sh echo "# .dmg created. Everything should be ready for release!" + else + echo "Error uploading for notarization" + exit fi else # App store echo "# Build the package for AppStore submission" From bea86de340a11ea03d2cfc8f954c6047a539f06f Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 28 Aug 2023 14:51:59 +0200 Subject: [PATCH 124/158] Bump Flutter dependencies. --- pubspec.lock | 28 ++++++++++++++-------------- pubspec.yaml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 940c9346..7a301c29 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: "30859c90e9ddaccc484f56303931f477b1f1ba2bab74aa32ed5d6ce15870f8cf" + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" url: "https://pub.dev" source: hosted - version: "7.2.8" + version: "7.2.10" built_collection: dependency: transitive description: @@ -229,10 +229,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "21145c9c268d54b1f771d8380c195d2d6f655e0567dc1ca2f9c134c02c819e0a" + sha256: bdfa035a974a0c080576c4c8ed01cdf9d1b406a04c7daa05443ef0383a97bedc url: "https://pub.dev" source: hosted - version: "5.3.3" + version: "5.3.4" fixnum: dependency: transitive description: @@ -276,10 +276,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: b6cb0041c6c11cefb2dcb97ef436eba43c6d41287ac6d8ca93e02a497f53a4f3 + sha256: "4615271bb6c1302d41cf4daff689d39a87045137986eb71de154e6f99ff18139" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.9" flutter_test: dependency: "direct dev" description: flutter @@ -416,10 +416,10 @@ packages: dependency: "direct main" description: name: logging - sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" matcher: dependency: transitive description: @@ -607,18 +607,18 @@ packages: dependency: transitive description: name: riverpod - sha256: b0657b5b30c81a3184bdaab353045f0a403ebd60bb381591a8b7ad77dcade793 + sha256: "52b2937f5b9552987f35419f1deef22474d7fc609aea2f4c5b6c11b4449a287c" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.9" screen_retriever: dependency: "direct main" description: name: screen_retriever - sha256: "4931f226ca158123ccd765325e9fbf360bfed0af9b460a10f960f9bb13d58323" + sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "0.1.9" shared_preferences: dependency: "direct main" description: @@ -972,10 +972,10 @@ packages: dependency: "direct main" description: name: window_manager - sha256: "9eef00e393e7f9308309ce9a8b2398c9ee3ca78b50c96e8b4f9873945693ac88" + sha256: "6ee795be9124f90660ea9d05e581a466de19e1c89ee74fc4bf528f60c8600edd" url: "https://pub.dev" source: hosted - version: "0.3.5" + version: "0.3.6" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cfaf64f5..097edcd3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,7 +42,7 @@ dependencies: # cupertino_icons: ^1.0.2 async: ^2.8.2 - logging: 1.1.1 + logging: ^1.2.0 collection: ^1.16.0 shared_preferences: ^2.1.2 flutter_riverpod: ^2.3.7 From 45f75c0215791287d61f528090e154a9ed560df5 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 28 Aug 2023 14:57:41 +0200 Subject: [PATCH 125/158] Update Python dependencies. --- helper/poetry.lock | 72 ++++++++++++---------------------------------- 1 file changed, 18 insertions(+), 54 deletions(-) diff --git a/helper/poetry.lock b/helper/poetry.lock index 703c0116..0eec8b21 100755 --- a/helper/poetry.lock +++ b/helper/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "altgraph" version = "0.17.3" description = "Python graph (network) package" -category = "dev" optional = false python-versions = "*" files = [ @@ -16,7 +15,6 @@ files = [ name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = "*" files = [ @@ -93,7 +91,6 @@ pycparser = "*" name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -108,7 +105,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -120,7 +116,6 @@ files = [ name = "cryptography" version = "41.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -166,7 +161,6 @@ test-randomorder = ["pytest-randomly"] name = "exceptiongroup" version = "1.1.3" description = "Backport of PEP 654 (exception groups)" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -181,7 +175,6 @@ test = ["pytest (>=6)"] name = "fido2" version = "1.1.2" description = "FIDO2/WebAuthn library for implementing clients and servers." -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -199,7 +192,6 @@ pcsc = ["pyscard (>=1.9,<3)"] name = "importlib-metadata" version = "6.8.0" description = "Read metadata from Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -219,7 +211,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs name = "importlib-resources" version = "6.0.1" description = "Read resources from Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -238,7 +229,6 @@ testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -250,7 +240,6 @@ files = [ name = "jaraco-classes" version = "3.3.0" description = "Utility functions for Python class constructs" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -269,7 +258,6 @@ testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", name = "jeepney" version = "0.8.0" description = "Low-level, pure Python DBus protocol wrapper." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -285,7 +273,6 @@ trio = ["async_generator", "trio"] name = "keyring" version = "23.13.1" description = "Store and access your passwords safely." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -310,7 +297,6 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec name = "macholib" version = "1.16.2" description = "Mach-O header analysis and editing" -category = "dev" optional = false python-versions = "*" files = [ @@ -325,7 +311,6 @@ altgraph = ">=0.17" name = "more-itertools" version = "10.1.0" description = "More routines for operating on iterables, beyond itertools" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -337,7 +322,6 @@ files = [ name = "mss" version = "9.0.1" description = "An ultra fast cross-platform multiple screenshots module in pure python using ctypes." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -349,7 +333,6 @@ files = [ name = "numpy" version = "1.24.4" description = "Fundamental package for array computing in Python" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -387,7 +370,6 @@ files = [ name = "packaging" version = "23.1" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -399,7 +381,6 @@ files = [ name = "pefile" version = "2023.2.7" description = "Python PE parsing module" -category = "dev" optional = false python-versions = ">=3.6.0" files = [ @@ -411,7 +392,6 @@ files = [ name = "pillow" version = "10.0.0" description = "Python Imaging Library (Fork)" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -433,7 +413,6 @@ files = [ {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538"}, {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d"}, {file = "Pillow-10.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f"}, - {file = "Pillow-10.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc2ec7c7b5d66b8ec9ce9f720dbb5fa4bace0f545acd34870eff4a369b44bf37"}, {file = "Pillow-10.0.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883"}, {file = "Pillow-10.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e"}, {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640"}, @@ -443,7 +422,6 @@ files = [ {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551"}, {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5"}, {file = "Pillow-10.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199"}, - {file = "Pillow-10.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:1ce91b6ec08d866b14413d3f0bbdea7e24dfdc8e59f562bb77bc3fe60b6144ca"}, {file = "Pillow-10.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3"}, {file = "Pillow-10.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3"}, {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43"}, @@ -479,14 +457,13 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa [[package]] name = "pluggy" -version = "1.2.0" +version = "1.3.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [package.extras] @@ -497,7 +474,6 @@ testing = ["pytest", "pytest-benchmark"] name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -507,24 +483,23 @@ files = [ [[package]] name = "pyinstaller" -version = "5.13.0" +version = "5.13.1" description = "PyInstaller bundles a Python application and all its dependencies into a single package." -category = "dev" optional = false python-versions = "<3.13,>=3.7" files = [ - {file = "pyinstaller-5.13.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:7fdd319828de679f9c5e381eff998ee9b4164bf4457e7fca56946701cf002c3f"}, - {file = "pyinstaller-5.13.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0df43697c4914285ecd333be968d2cd042ab9b2670124879ee87931d2344eaf5"}, - {file = "pyinstaller-5.13.0-py3-none-manylinux2014_i686.whl", hash = "sha256:28d9742c37e9fb518444b12f8c8ab3cb4ba212d752693c34475c08009aa21ccf"}, - {file = "pyinstaller-5.13.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e5fb17de6c325d3b2b4ceaeb55130ad7100a79096490e4c5b890224406fa42f4"}, - {file = "pyinstaller-5.13.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:78975043edeb628e23a73fb3ef0a273cda50e765f1716f75212ea3e91b09dede"}, - {file = "pyinstaller-5.13.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:cd7d5c06f2847195a23d72ede17c60857d6f495d6f0727dc6c9bc1235f2eb79c"}, - {file = "pyinstaller-5.13.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:24009eba63cfdbcde6d2634e9c87f545eb67249ddf3b514e0cd3b2cdaa595828"}, - {file = "pyinstaller-5.13.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:1fde4381155f21d6354dc450dcaa338cd8a40aaacf6bd22b987b0f3e1f96f3ee"}, - {file = "pyinstaller-5.13.0-py3-none-win32.whl", hash = "sha256:2d03419904d1c25c8968b0ad21da0e0f33d8d65716e29481b5bd83f7f342b0c5"}, - {file = "pyinstaller-5.13.0-py3-none-win_amd64.whl", hash = "sha256:9fc27c5a853b14a90d39c252707673c7a0efec921cd817169aff3af0fca8c127"}, - {file = "pyinstaller-5.13.0-py3-none-win_arm64.whl", hash = "sha256:3a331951f9744bc2379ea5d65d36f3c828eaefe2785f15039592cdc08560b262"}, - {file = "pyinstaller-5.13.0.tar.gz", hash = "sha256:5e446df41255e815017d96318e39f65a3eb807e74a796c7e7ff7f13b6366a2e9"}, + {file = "pyinstaller-5.13.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:3c9cfe6d5d2f392d5d47389f6d377a8f225db460cdd01048b5a3de1d99c24ebe"}, + {file = "pyinstaller-5.13.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:29341d2e86d5ce7df993e797ee96ef679041fc85376d31c35c7b714085a21299"}, + {file = "pyinstaller-5.13.1-py3-none-manylinux2014_i686.whl", hash = "sha256:ad6e31a8f35a463c6140e4cf979859197edc9831a1039253408b0fe5eec274dc"}, + {file = "pyinstaller-5.13.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:5d801db3ceee58d01337473ea897e96e4bb21421a169dd7cf8716754617ff7fc"}, + {file = "pyinstaller-5.13.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:2519db3edec87d8c33924c2c4b7e176d8c1bbd9ba892d77efb67281925e621d6"}, + {file = "pyinstaller-5.13.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e033218c8922f0342b6095fb444ecb3bc6747dfa58cac5eac2b985350f4b681e"}, + {file = "pyinstaller-5.13.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:086e68aa1e72f6aa13b9d170a395755e2b194b8ab410caeed02d16b432410c8c"}, + {file = "pyinstaller-5.13.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:aa609aca62edd8cdcf7740677a21525e6c23b5e9a8f821ec8a80c68947771b5d"}, + {file = "pyinstaller-5.13.1-py3-none-win32.whl", hash = "sha256:b8d4000af72bf72f8185d420cd0a0aee0961f03a5c3511dc3ff08cdaef0583de"}, + {file = "pyinstaller-5.13.1-py3-none-win_amd64.whl", hash = "sha256:b70ebc10811b30bbea4cf5b81fd1477db992c2614cf215edc987cda9c5468911"}, + {file = "pyinstaller-5.13.1-py3-none-win_arm64.whl", hash = "sha256:78d1601a11475b95dceff6eaf0c9cd74d93e3f47b5ce4ad63cd76e7a369d3d04"}, + {file = "pyinstaller-5.13.1.tar.gz", hash = "sha256:a2e7a1d76a7ac26f1db849d691a374f2048b0e204233028d25d79a90ecd1fec8"}, ] [package.dependencies] @@ -543,7 +518,6 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] name = "pyinstaller-hooks-contrib" version = "2023.7" description = "Community maintained hooks for PyInstaller" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -555,7 +529,6 @@ files = [ name = "pyscard" version = "2.0.7" description = "Smartcard module for Python." -category = "main" optional = false python-versions = "*" files = [ @@ -576,7 +549,6 @@ pyro = ["Pyro"] name = "pytest" version = "7.4.0" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -599,7 +571,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pywin32" version = "306" description = "Python for Window Extensions" -category = "main" optional = false python-versions = "*" files = [ @@ -623,7 +594,6 @@ files = [ name = "pywin32-ctypes" version = "0.2.2" description = "A (partial) reimplementation of pywin32 using ctypes/cffi" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -635,7 +605,6 @@ files = [ name = "secretstorage" version = "3.3.3" description = "Python bindings to FreeDesktop.org Secret Service API" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -651,7 +620,6 @@ jeepney = ">=0.6" name = "setuptools" version = "68.1.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -668,7 +636,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -680,7 +647,6 @@ files = [ name = "yubikey-manager" version = "5.2.0" description = "Tool for managing your YubiKey configuration." -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -700,7 +666,6 @@ pywin32 = {version = ">=223", markers = "sys_platform == \"win32\""} name = "zipp" version = "3.16.2" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -716,7 +681,6 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p name = "zxing-cpp" version = "2.1.0" description = "Python bindings for the zxing-cpp barcode library" -category = "main" optional = false python-versions = ">=3.6" files = [ From 2730e2a96e658cd960c4a00f1bc455c23d061505 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 28 Aug 2023 15:41:40 +0200 Subject: [PATCH 126/158] bump --- android/app/build.gradle | 4 ++-- android/build.gradle | 4 ++-- android/flutter_plugins/qrscanner_zxing/android/build.gradle | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 70428f24..1f17f43b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -93,8 +93,8 @@ dependencies { api "com.yubico.yubikit:oath:$project.yubiKitVersion" api "com.yubico.yubikit:support:$project.yubiKitVersion" - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1' - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0' // Lifecycle implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' diff --git a/android/build.gradle b/android/build.gradle index ec9d3803..7533c553 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.9.0' + ext.kotlin_version = '1.9.10' repositories { google() mavenCentral() @@ -26,7 +26,7 @@ allprojects { yubiKitVersion = "2.3.0" junitVersion = "4.13.2" - mockitoVersion = "5.4.0" + mockitoVersion = "5.5.0" } } diff --git a/android/flutter_plugins/qrscanner_zxing/android/build.gradle b/android/flutter_plugins/qrscanner_zxing/android/build.gradle index 17bb15b9..1eaf8b29 100644 --- a/android/flutter_plugins/qrscanner_zxing/android/build.gradle +++ b/android/flutter_plugins/qrscanner_zxing/android/build.gradle @@ -2,7 +2,7 @@ group 'com.yubico.authenticator.flutter_plugins.qrscanner_zxing' version '1.0' buildscript { - ext.kotlin_version = '1.9.0' + ext.kotlin_version = '1.9.10' repositories { google() mavenCentral() From 74a3cd2568c45e7014e4350b4e56290759d6b87d Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 28 Aug 2023 15:53:54 +0200 Subject: [PATCH 127/158] use kotlin 1.9.0 because of codeql --- android/build.gradle | 2 +- android/flutter_plugins/qrscanner_zxing/android/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 7533c553..f2914fc5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.9.10' + ext.kotlin_version = '1.9.0' repositories { google() mavenCentral() diff --git a/android/flutter_plugins/qrscanner_zxing/android/build.gradle b/android/flutter_plugins/qrscanner_zxing/android/build.gradle index 1eaf8b29..17bb15b9 100644 --- a/android/flutter_plugins/qrscanner_zxing/android/build.gradle +++ b/android/flutter_plugins/qrscanner_zxing/android/build.gradle @@ -2,7 +2,7 @@ group 'com.yubico.authenticator.flutter_plugins.qrscanner_zxing' version '1.0' buildscript { - ext.kotlin_version = '1.9.10' + ext.kotlin_version = '1.9.0' repositories { google() mavenCentral() From 00593596424bbb63b9f20cdae62eefbd9d5f8c92 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 30 Aug 2023 08:53:17 +0200 Subject: [PATCH 128/158] Add Japanese support. Also add override of locale by setting env "_YA_LOCALE" variable. --- lib/app/state.dart | 31 +- lib/l10n/app_ja.arb | 613 ++++++++++++++++++++++++++++++ lib/piv/views/cert_info_view.dart | 12 +- 3 files changed, 645 insertions(+), 11 deletions(-) create mode 100644 lib/l10n/app_ja.arb diff --git a/lib/app/state.dart b/lib/app/state.dart index 717c9900..06626078 100755 --- a/lib/app/state.dart +++ b/lib/app/state.dart @@ -15,6 +15,7 @@ */ import 'dart:async'; +import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; @@ -64,14 +65,32 @@ class CommunityTranslationsNotifier extends StateNotifier { } } -final supportedLocalesProvider = Provider>((ref) => - ref.watch(communityTranslationsProvider) - ? AppLocalizations.supportedLocales - : officialLocales); +final supportedLocalesProvider = Provider>((ref) { + final locales = [...officialLocales]; + final localeStr = Platform.environment['_YA_LOCALE']; + if (localeStr != null) { + // Force locale + final locale = Locale(localeStr, ''); + locales.add(locale); + } + return ref.watch(communityTranslationsProvider) + ? AppLocalizations.supportedLocales + : locales; +}); final currentLocaleProvider = Provider( - (ref) => basicLocaleListResolution( - PlatformDispatcher.instance.locales, ref.watch(supportedLocalesProvider)), + (ref) { + final localeStr = Platform.environment['_YA_LOCALE']; + if (localeStr != null) { + // Force locale + final locale = Locale(localeStr, ''); + return basicLocaleListResolution( + [locale], AppLocalizations.supportedLocales); + } + // Choose from supported + return basicLocaleListResolution(PlatformDispatcher.instance.locales, + ref.watch(supportedLocalesProvider)); + }, ); final l10nProvider = Provider( diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb new file mode 100644 index 00000000..f69777dd --- /dev/null +++ b/lib/l10n/app_ja.arb @@ -0,0 +1,613 @@ +{ + "@@locale": "ja", + + "@_readme": { + "notes": [ + "All strings start with a Captial letter.", + "Group strings by category, but don't needlessly tie them to a section of the app if they can be re-used between several.", + "Run check_strings.py on the .arb file to detect problems, tweak @_lint_rules as needed per language." + ], + "prefixes": { + "s_": "A single, or few words. Should be short enough to display on a button, or a header.", + "l_": "A single line, can be wrapped. Should not be more than one sentence, and not end with a period.", + "p_": "One or more full sentences, with proper punctuation.", + "q_": "A question, ending in question mark." + } + }, + + "@_lint_rules": { + "s_max_words": 4, + "s_max_length": 32 + }, + + "app_name": "Yubico Authenticator", + + "s_save": "保存", + "s_cancel": "キャンセル", + "s_close": "閉じる", + "s_delete": "消去", + "s_quit": "終了", + "s_unlock": "ロック解除", + "s_calculate": "計算", + "s_import": "インポート", + "s_overwrite": "上書き", + "s_label": "ラベル", + "s_name": "名前", + "s_usb": "USB", + "s_nfc": "NFC", + "s_options": "オプション", + "s_show_window": "ウィンドウを表示", + "s_hide_window": "ウィンドウを表示しない", + "q_rename_target": "{label}の名前を変更しますか?", + "@q_rename_target" : { + "placeholders": { + "label": {} + } + }, + "l_bullet": "• {item}", + "@l_bullet" : { + "placeholders": { + "item": {} + } + }, + + "s_about": "情報", + "s_algorithm": "アルゴリズム", + "s_appearance": "外観", + "s_authenticator": "Authenticator", + "s_actions": "アクション", + "s_manage": "管理", + "s_setup": "セットアップ", + "s_settings": "設定", + "s_piv": "PIV", + "s_webauthn": "WebAuthn", + "s_help_and_about": "ヘルプと概要", + "s_help_and_feedback": "ヘルプとフィードバック", + "s_send_feedback": "フィードバックの送信", + "s_i_need_help": "ヘルプが必要", + "s_troubleshooting": "トラブルシューティング", + "s_terms_of_use": "利用規約", + "s_privacy_policy": "プライバシーポリシー", + "s_open_src_licenses": "オープンソースライセンス", + "s_configure_yk": "YubiKeyを構成する", + "s_please_wait": "お待ちください\u2026", + "s_secret_key": "秘密鍵", + "s_private_key": "秘密鍵", + "s_invalid_length": "無効な長さです", + "s_require_touch": "タッチが必要", + "q_have_account_info": "アカウント情報をお持ちですか?", + "s_run_diagnostics": "診断を実行する", + "s_log_level": "ログレベル: {level}", + "@s_log_level": { + "placeholders": { + "level": {} + } + }, + "s_character_count": "文字数", + "s_learn_more": "もっと詳しく知る", + + "@_language": {}, + "s_language": "言語", + "l_enable_community_translations": "コミュニティ翻訳を有効にする", + "p_community_translations_desc": "これらの翻訳はコミュニティによって提供および維持されます。 エラーが含まれているか不完全である可能性があります。", + + "@_theme": {}, + "s_app_theme": "アプリのテーマ", + "s_choose_app_theme": "アプリのテーマを選択", + "s_system_default": "システムのデフォルト", + "s_light_mode": "ライトモード", + "s_dark_mode": "ダークモード", + + "@_yubikey_selection": {}, + "s_yk_information": "YubiKey情報", + "s_select_yk": "YubiKeyを選択", + "s_select_to_scan": "選択してスキャン", + "s_hide_device": "デバイスを非表示", + "s_show_hidden_devices": "非表示のデバイスを表示", + "s_sn_serial": "S/N: {serial}", + "@s_sn_serial" : { + "placeholders": { + "serial": {} + } + }, + "s_fw_version": "F/W: {version}", + "@s_fw_version" : { + "placeholders": { + "version": {} + } + }, + + "@_yubikey_interactions": {}, + "l_insert_yk": "YubiKeyを挿入する", + "l_insert_or_tap_yk": "YubiKeyを挿入またはタップする", + "l_unplug_yk": "YubiKeyを取り外す", + "l_reinsert_yk": "YubiKeyを再挿入する", + "l_place_on_nfc_reader": "YubiKeyをNFCリーダーに置く", + "l_replace_yk_on_reader": "YubiKeyをリーダーに戻す", + "l_remove_yk_from_reader": "YubiKeyをNFCリーダーから取り外す", + "p_try_reinsert_yk": "YubiKeyを取り外して再挿入してみてください", + "s_touch_required": "タッチが必要です", + "l_touch_button_now": "今すぐYubiKeyのボタンをタッチしてください", + "l_keep_touching_yk": "YubiKeyを繰り返しタッチし続けてください\u2026", + + "@_app_configuration": {}, + "s_toggle_applications": "アプリケーションの切替え", + "l_min_one_interface": "少なくとも 1 つのインターフェイスを有効にする必要があります", + "s_reconfiguring_yk": "YubiKeyを再構成しています\u2026", + "s_config_updated": "構成が更新されました", + "l_config_updated_reinsert": "設定が更新されました。YubiKeyを取り外して再挿入してください", + "s_app_not_supported": "アプリケーションがサポートされていません", + "l_app_not_supported_on_yk": "使用されているYubiKeyは '{app}' アプリケーションをサポートしていません", + "@l_app_not_supported_on_yk" : { + "placeholders": { + "app": {} + } + }, + "l_app_not_supported_desc": "このアプリケーションはサポートされていません", + "s_app_disabled": "アプリケーションが無効になっています", + "l_app_disabled_desc": "YubiKeyの「{app}」アプリケーションへのアクセスを許可", + "@l_app_disabled_desc" : { + "placeholders": { + "app": {} + } + }, + "s_fido_disabled": "FIDO2が無効になっています", + "l_webauthn_req_fido2": "WebAuthnでは、YubiKeyでFIDO2アプリケーションを有効にする必要があります", + + "@_connectivity_issues": {}, + "l_helper_not_responding": "ヘルパープロセスが応答していません", + "l_yk_no_access": "このYubiKeyにはアクセスできません", + "s_yk_inaccessible": "デバイスにアクセスできません", + "l_open_connection_failed": "接続を開けませんでした", + "l_ccid_connection_failed": "スマートカード接続を開けませんでした", + "p_ccid_service_unavailable": "スマートカード サービスが機能していることを確認してください", + "p_pcscd_unavailable": "pcscdがインストールされ、実行されていることを確認してください", + "l_no_yk_present": "YubiKeyが存在しません", + "s_unknown_type": "不明なタイプ", + "s_unknown_device": "認識されないデバイス", + "s_unsupported_yk": "サポートされていないYubiKey", + "s_yk_not_recognized": "デバイスが認識されない", + + "@_general_errors": {}, + "l_error_occured": "エラーが発生しました", + "s_application_error": "アプリケーションエラー", + "l_import_error": "インポートエラー", + "l_file_not_found": "ファイルが見つかりません", + "l_file_too_big": "ファイルサイズが大きすぎます", + "l_filesystem_error": "ファイルシステム操作エラー", + + "@_pins": {}, + "s_pin": "PIN", + "s_puk": "PUK", + "s_set_pin": "PINを設定する", + "s_change_pin": "PINを変更する", + "s_change_puk": "PUKを変更する", + "s_current_pin": "現在のPIN", + "s_current_puk": "現在のPUK", + "s_new_pin": "新しいPIN", + "s_new_puk": "新しいPUK", + "s_confirm_pin": "PINの確認", + "s_confirm_puk": "PUKの確認", + "s_unblock_pin": "ブロックを解除", + "l_new_pin_len": "新しいPINは少なくとも{length}文字である必要があります", + "@l_new_pin_len" : { + "placeholders": { + "length": {} + } + }, + "s_pin_set": "PINの設定", + "s_puk_set": "PUKの設定", + "l_set_pin_failed": "PIN設定に失敗しました:{message}", + "@l_set_pin_failed" : { + "placeholders": { + "message": {} + } + }, + "l_attempts_remaining": "あと{retries}回試行できます", + "@l_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, + "l_wrong_pin_attempts_remaining": "PINが間違っています。あと{retries}回試行できます", + "@l_wrong_pin_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, + "l_wrong_puk_attempts_remaining": "PUKが間違っています。あと{retries}回試行できます", + "@l_wrong_puk_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, + "s_fido_pin_protection": "FIDO PINによる保護", + "l_fido_pin_protection_optional": "任意FIDO PINによる保護", + "l_enter_fido2_pin": "YubiKeyのFIDO2 PINを入力してください", + "l_optionally_set_a_pin": "YubiKeyアクセスを保護するために、任意でPINの設定ができます\nWebサイトにはセキュリティ キーとして登録されます", + "l_pin_blocked_reset": "PINはブロックされています。FIDOアプリケーションを出荷時設定にリセットしてください", + "l_set_pin_first": "最初にPINが必要です", + "l_unlock_pin_first": "最初にPINでロックを解除してください", + "l_pin_soft_locked": "YubiKeyを取り外して再挿入するまで、PINはブロックされています", + "p_enter_current_pin_or_reset": "現在のPINを入力してください。PIN がわからない場合は、PUK でブロック解除するか、YubiKey をリセットする必要があります", + "p_enter_current_puk_or_reset": "現在のPUKを入力してください。 PUK がわからない場合は、YubiKeyをリセットする必要があります", + "p_enter_new_fido2_pin": "新しいPINを入力してください。 PINは少なくとも{length}文字の長さである必要があり、文字、数字、特殊文字を含めることができます", + "@p_enter_new_fido2_pin" : { + "placeholders": { + "length": {} + } + }, + "s_pin_required": "PINが必要", + "p_pin_required_desc": "実行しようとしている操作には、PIV PINの入力が必要です", + "l_piv_pin_blocked": "ブロックされています。PUK を使用してリセットしてください", + "l_piv_pin_puk_blocked": "ブロックされています。工場出荷リセットしてください", + "p_enter_new_piv_pin_puk": "新{name}を入力してください。6 ~ 8 文字でなければな りま せん。", + "@p_enter_new_piv_pin_puk" : { + "placeholders": { + "name": {} + } + }, + + "@_passwords": {}, + "s_password": "パスワード", + "s_manage_password": "パスワードの管理", + "s_set_password": "パスワードを設定", + "s_password_set": "パスワード設定", + "l_optional_password_protection": "任意パスワードによる保護", + "s_new_password": "新しいパスワード", + "s_current_password": "現在のパスワード", + "s_confirm_password": "パスワードを確認", + "s_wrong_password": "間違ったパスワード", + "s_remove_password": "パスワードの削除", + "s_password_removed": "パスワードが削除されました", + "s_remember_password": "パスワードを覚える", + "s_clear_saved_password": "保存されたパスワードを削除する", + "s_password_forgotten": "パスワードを忘れた場合", + "l_keystore_unavailable": "OSキーストアは使用できません", + "l_remember_pw_failed": "パスワードを忘れました", + "l_unlock_first": "最初にパスワードでロックを解除します", + "l_enter_oath_pw": "YubiKeyのOATHパスワードを入力してください", + "p_enter_current_password_or_reset": "現在のパスワードを入力してください。パスワードがわからない場合は、YubiKeyをリセットする必要があります", + "p_enter_new_password": "新しいパスワードを入力してください。 パスワードには文字、数字、特殊文字を含めることができます", + + "@_management_key": {}, + "s_management_key": "Management key", + "s_current_management_key": "現在のManagement key", + "s_new_management_key": "新しいManagement key", + "l_change_management_key": "Management keyの変更", + "p_change_management_key_desc": "Management keyを変更してください。Management keyの代わりにPINを使用することも可能です", + "l_management_key_changed": "Management keyは変更されました", + "l_default_key_used": "デフォルトManagement keyが使用されています", + "s_generate_random": "ランダムに生成する", + "s_use_default": "デフォルトの使用", + "l_warning_default_key": "警告: デフォルトのキーが使用されています", + "s_protect_key": "PINで保護する", + "l_pin_protected_key": "代わりにPINを使用できます", + "l_wrong_key": "間違ったキー", + "l_unlock_piv_management": "PIV管理のロックの解除", + "p_unlock_piv_management_desc": "実行しようとしている操作にはPIVのManagement keyが必要です。このセッションの管理機能のロックを解除するために、キーを入力してください", + + "@_oath_accounts": {}, + "l_account": "アカウント:{label}", + "@l_account" : { + "placeholders": { + "label": {} + } + }, + "s_accounts": "アカウント", + "s_no_accounts": "アカウントがありません", + "s_add_account": "アカウントの追加", + "s_add_accounts" : "アカウントの追加", + "p_add_description" : "QR コードをスキャンするには、コード全体が画面に表示されていることを確認し、下のボタンを押してください。保存した画像をこのダイアログにドラッグすることもできます。アカウントクレデンシャル情報を書面で持っている場合は、代わりに手動で入力をしてください。", + "s_add_manually" : "手動で追加", + "s_account_added": "アカウントが追加されました", + "l_account_add_failed": "アカウントの追加に失敗しました:{message}", + "@l_account_add_failed" : { + "placeholders": { + "message": {} + } + }, + "l_account_name_required": "アカウントには名前が必要です", + "l_name_already_exists": "発行者名は既に使われています", + "l_account_already_exists": "このアカウントはすでに YubiKey に存在します", + "l_invalid_character_issuer": "無効な文字: ':' は発行者名で使用できません", + "l_select_accounts" : "YubiKey に追加するアカウントを選択してください", + "s_pinned": "固定", + "s_pin_account": "アカウントを固定する", + "s_unpin_account": "アカウントの固定を解除する", + "s_no_pinned_accounts": "固定されたアカウントはありません", + "l_pin_account_desc": "重要なアカウントは一緒にまとめてください", + "s_rename_account": "アカウント名の変更", + "l_rename_account_desc": "アカウントの発行者/名前の編集", + "s_account_renamed": "アカウント名が変更されました", + "p_rename_will_change_account_displayed": "これにより、リスト内でのアカウントの表示方法が変わります。", + "s_delete_account": "アカウントを削除する", + "l_delete_account_desc": "YubiKeyからアカウントの削除", + "s_account_deleted": "アカウントが削除されました", + "p_warning_delete_account": "警告!この操作によってYubiKeyからアカウントが削除されます", + "p_warning_disable_credential": "このアカウントのOTPを生成できなくなります。 アカウントからロックアウトされないように、必ず最初にWebサイトからこのクレデンシャルを無効化してください", + "s_account_name": "アカウント名", + "s_search_accounts": "アカウントを検索", + "l_accounts_used": "{capacity}個のアカウントのうち{used}個が使用されています", + "@l_accounts_used" : { + "placeholders": { + "used": {}, + "capacity": {} + } + }, + "s_num_digits": "{num}桁", + "@s_num_digits" : { + "placeholders": { + "num": {} + } + }, + "s_num_sec": "{num}秒", + "@s_num_sec" : { + "placeholders": { + "num": {} + } + }, + "s_issuer_optional": "発行者(任意)", + "s_counter_based": "カウンターベース", + "s_time_based": "時間ベース", + "l_copy_code_desc": "コードを別のアプリに貼り付ける", + "s_calculate_code": " コードの計算", + "l_calculate_code_desc": "YubiKey から新しいコードの取得", + + "@_fido_credentials": {}, + "l_passkey": "パスキー: {label}", + "@l_passkey" : { + "placeholders": { + "label": {} + } + }, + "s_passkeys": "パスキー", + "l_ready_to_use": "すぐに使用可能", + "l_register_sk_on_websites": "Webサイトにセキュリティキーとして登録する", + "l_no_discoverable_accounts": "パスキーは保存されていません", + "s_delete_passkey": "パスキーを削除", + "l_delete_passkey_desc": "YubiKeyからパスキーの削除", + "s_passkey_deleted": "パスキーが削除されました", + "p_warning_delete_passkey": "これにより、YubiKeyからパスキーが削除されます", + + "@_fingerprints": {}, + "l_fingerprint": "指紋:{label}", + "@l_fingerprint" : { + "placeholders": { + "label": {} + } + }, + "s_fingerprints": "指紋", + "l_fingerprint_captured": "指紋の取得に成功しました!", + "s_fingerprint_added": "指紋が追加されました", + "l_setting_name_failed": "名前設定時名エラー:{message}", + "@l_setting_name_failed" : { + "placeholders": { + "message": {} + } + }, + "s_add_fingerprint": "指紋を追加", + "l_fp_step_1_capture": "ステップ 1/2:指紋を取得する", + "l_fp_step_2_name": "ステップ 2/2:指紋の名前を付ける", + "s_delete_fingerprint": "指紋を削除", + "l_delete_fingerprint_desc": "YubiKeyから指紋の削除", + "s_fingerprint_deleted": "指紋が削除されました", + "p_warning_delete_fingerprint": "これによりYubiKeyから指紋が削除されます", + "s_no_fingerprints": "指紋は登録されていません", + "l_set_pin_fingerprints": "指紋登録のためにPINを設定してください", + "l_no_fps_added": "指紋は追加されていません", + "s_rename_fp": "指紋の名前を変更", + "l_rename_fp_desc": "ラベルの変更", + "s_fingerprint_renamed": "指紋の名前が変更されました", + "l_rename_fp_failed": "大友:名前変更エラー:{message}", + "@l_rename_fp_failed" : { + "placeholders": { + "message": {} + } + }, + "l_add_one_or_more_fps": "1 つ以上 (最大 5 つ) の指紋を追加します", + "l_fingerprints_used": "{used}/5つの指紋が登録されました", + "@l_fingerprints_used": { + "placeholders": { + "used": {} + } + }, + "p_press_fingerprint_begin": "YubiKeyに指を押し当てて開始します", + "p_will_change_label_fp": "これにより指紋のラベルが変更されます", + + "@_certificates": {}, + "s_certificate": "証明書", + "s_certificates": "証明書", + "s_csr": "CSR", + "s_subject": "サブジェクト", + "l_export_csr_file": "CSRをファイルに保存", + "l_select_import_file": "インポートするファイルの選択", + "l_export_certificate": "証明書をエクスポートする", + "l_export_certificate_file": "証明書をファイルにエクスポートする", + "l_export_certificate_desc": "証明書をファイルにエクスポートする", + "l_certificate_exported": "証明書がエクスポートされました", + "l_import_file": "ファイルのインポート", + "l_import_desc": "キーや証明書のインポート", + "l_delete_certificate": "証明書を削除", + "l_delete_certificate_desc": "YubiKeyか証明書の削除", + "s_issuer": "発行者", + "s_serial": "シリアル番号", + "s_certificate_fingerprint": "指紋", + "s_valid_from": "有効期限の開始", + "s_valid_to": "有効期限の終了", + "l_no_certificate": "証明書はロードされていません", + "l_key_no_certificate": "証明書がロードされていない鍵", + "s_generate_key": "鍵の生成", + "l_generate_desc": "新しい証明書またはCSRの生成", + "p_generate_desc": "これにより、YubiKeyのPIVスロット{slot}に新しい鍵が生成されます。公開鍵は、YubiKeyに保存されている自己署名証明書、またはファイルに保存されている証明書署名要求(CSR)に埋め込まれます", + "@p_generate_desc" : { + "placeholders": { + "slot": {} + } + }, + "l_generating_private_key": "秘密鍵を生成しています\u2026", + "s_private_key_generated": "秘密鍵を生成しました", + "p_warning_delete_certificate": "警告!この操作によってYubiKeyから証明書が削除されます", + "q_delete_certificate_confirm": "PIVスロット{slot}の証明書を削除しますか?", + "@q_delete_certificate_confirm" : { + "placeholders": { + "slot": {} + } + }, + "l_certificate_deleted": "証明書が削除されました", + "p_password_protected_file": "選択したファイルはパスワードで保護されています。パスワードを入力して続行します", + "p_import_items_desc": "次のアイテムはPIVスロット{slot}にインポートされます", + "@p_import_items_desc" : { + "placeholders": { + "slot": {} + } + }, + "p_subject_desc": "RFC 4514フォーマットの識別名識別名 (DN)", + "l_rfc4514_invalid": "無効な RFC 4514 形式です", + "rfc4514_examples": "例:\nCN=Example Name\nCN=jsmith,DC=example,DC=net", + "p_cert_options_desc": "使用する鍵アルゴリズム、出力形式、および有効期限 (該当する場合)", + "s_overwrite_slot": "スロットの上書き", + "p_overwrite_slot_desc": "これにより、スロット{slot}内の既存のデータが永久に上書きされます", + "@p_overwrite_slot_desc" : { + "placeholders": { + "slot": {} + } + }, + "l_overwrite_cert": "証明書は上書きされます", + "l_overwrite_key": "秘密鍵は上書きされます", + "l_overwrite_key_maybe": "スロット内の既存の秘密鍵は上書きされます", + + "@_piv_slots": {}, + "s_slot_display_name": "{name} ({hexid})", + "@s_slot_display_name" : { + "placeholders": { + "name": {}, + "hexid": {} + } + }, + "s_slot_9a": "認証", + "s_slot_9c": "デジタル署名", + "s_slot_9d": "鍵の管理", + "s_slot_9e": "カード認証", + + "@_permissions": {}, + "s_enable_nfc": "NFCを有効にする", + "s_permission_denied": "権限がありません", + "l_elevating_permissions": "権限の昇格\u2026", + "s_review_permissions": "権限の確認", + "p_elevated_permissions_required": "このデバイスを管理するには権限の昇格が必要です", + "p_webauthn_elevated_permissions_required": "WebAuthn管理には権限の昇格が必要です", + "p_need_camera_permission": "Yubico AuthenticatorにはQRコードをスキャンするためのカメラ権限が必要です", + + "@_qr_codes": {}, + "s_qr_scan": "QRコードをスキャン", + "l_qr_scanned": "スキャンしたQRコード", + "l_invalid_qr": "無効なQRコード", + "l_qr_not_found": "QRコードが見つかりませんでした", + "l_qr_not_read": "QRコードの読み取りに失敗しました:{message}", + "@l_qr_not_read" : { + "placeholders": { + "message": {} + } + }, + "l_point_camera_scan": "カメラをQRコードに向けてスキャンする", + "q_want_to_scan": "スキャンしますか?", + "q_no_qr": "QRコードはありませんか?", + "s_enter_manually": "手動で入力", + + "@_factory_reset": {}, + "s_reset": "リセット", + "s_factory_reset": "工場出荷リセット", + "l_factory_reset_this_app": "このアプリケーションを出荷時設定にリセット", + "s_reset_oath": "OATHのリセット", + "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": { + "message": {} + } + }, + "s_reset_piv": "PIVのリセット", + "l_piv_app_reset": "PIVアプリケーションのリセット", + "p_warning_factory_reset": "警告!これによりすべてのOATH TOTP/HOTPアカウントがYubiKeyから削除されて、復旧不可能となります", + "p_warning_disable_credentials": "あなたのOATHクレデンシャル情報とパスワードは、このYubiKeyから削除されます。アカウントからロックアウトされないように、まずそれぞれのWebサイトからこれらを無効化してください", + "p_warning_deletes_accounts": ":警告!これによりYubiKeyからすべてのU2FおよびFIDO2アカウントが削除されて、復旧不可能となります", + "p_warning_disable_accounts": "あなたのクレデンシャル情報とすべてのPINは、このYubiKeyから削除されます。 アカウントからロックアウトされないように、まずそれぞれのWebサイトでこれらを無効化してください", + "p_warning_piv_reset": "警告!PIVデータは、YubiKeyから削除されて、復旧不可能となります", + "p_warning_piv_reset_desc": "これには秘密鍵と証明書が含まれます。 PIN、PUK、およびManagement keyは工場出荷時のデフォルト値にリセットされます", + + "@_copy_to_clipboard": {}, + "l_copy_to_clipboard": "クリップボードにコピー", + "s_code_copied": "コードをコピーしました", + "l_code_copied_clipboard": "コードをクリップボードにコピーしました", + "s_copy_log": "ログのコピー", + "l_log_copied": "ログをクリップボードにコピーしました", + "l_diagnostics_copied": "診断データをクリップボードにコピーしました", + "p_target_copied_clipboard": "{label}をクリップボードにコピーしました", + "@p_target_copied_clipboard" : { + "placeholders": { + "label": {} + } + }, + + "@_custom_icons": {}, + "s_custom_icons": "カスタムアイコン", + "l_set_icons_for_accounts": "アカウントのアイコンの設定", + "p_custom_icons_description": "アイコンパックを使用すると、見慣れたロゴと色でアカウントをより簡単に区別できるようになります", + "s_replace_icon_pack": "アイコンパックを置き換える", + "l_loading_icon_pack": "アイコンパックをロード中\u2026", + "s_load_icon_pack": "アイコンパックのロード", + "s_remove_icon_pack": "アイコンパックの削除", + "l_icon_pack_removed": "アイコンパックが削除されました", + "l_remove_icon_pack_failed": "アイコンパックの削除中にエラーが発生しました", + "s_choose_icon_pack": "アイコンパックを選択", + "l_icon_pack_imported": "アイコンパックがインポートされました", + "l_import_icon_pack_failed": "アイコンパックのインポート中にエラーが発生しました:{message}", + "@l_import_icon_pack_failed": { + "placeholders": { + "message": {} + } + }, + "l_invalid_icon_pack": "無効なアイコンパック", + "l_icon_pack_copy_failed": "アイコンパックのコピーに失敗しました", + + "@_android_settings": {}, + "s_nfc_options": "NFCオプション", + "l_on_yk_nfc_tap": "YubiKey NFCタップ時", + "l_launch_ya": "Yubico Authenticatorを起動", + "l_copy_otp_clipboard": "OTPをクリップボードにコピー", + "l_launch_and_copy_otp": "アプリを起動してOTPをコピー", + "l_kbd_layout_for_static": "キーボードレイアウト (静的パスワード用)", + "s_choose_kbd_layout": "キーボードレイアウトの選択", + "l_bypass_touch_requirement": "タッチ要件をバイパス", + "l_bypass_touch_requirement_on": "タッチが必要なアカウントはNFC経由で自動的に表示されます", + "l_bypass_touch_requirement_off": "タッチが必要なアカウントはNFC経由でさらにタップする必要があります", + "s_silence_nfc_sounds": "NFC音をミュートする", + "l_silence_nfc_sounds_on": "NFCタップでは音は鳴りません", + "l_silence_nfc_sounds_off": "NFCタップで音が再生されます", + "s_usb_options": "USBオプション", + "l_launch_app_on_usb": "YubiKeyが接続されているときに起動", + "l_launch_app_on_usb_on": "これにより他のアプリがUSB経由でYubiKeyを使用できなくなります", + "l_launch_app_on_usb_off": "他のアプリはUSB経由でYubiKeyを使用できます", + "s_allow_screenshots": "スクリーンショットを許可する", + + "s_nfc_dialog_tap_key": "キーをタップする", + "s_nfc_dialog_operation_success": "成功", + "s_nfc_dialog_operation_failed": "失敗", + + "s_nfc_dialog_oath_reset": "操作:OATHアプレットのリセット", + "s_nfc_dialog_oath_unlock": "操作:OATHアプレットのロック解除", + "s_nfc_dialog_oath_set_password": "操作:OATHパスワードの設定", + "s_nfc_dialog_oath_unset_password": "操作:OATHパスワードの削除", + "s_nfc_dialog_oath_add_account": "操作:新アカウントの追加", + "s_nfc_dialog_oath_rename_account": "操作:アカウント名の変更", + "s_nfc_dialog_oath_delete_account": "操作:アカウントの削除", + "s_nfc_dialog_oath_calculate_code": "操作:OATHコードの計算", + "s_nfc_dialog_oath_failure": "OATH操作は失敗しました", + "s_nfc_dialog_oath_add_multiple_accounts": "操作:複数アカウントの追加", + + "@_eof": {} +} \ No newline at end of file diff --git a/lib/piv/views/cert_info_view.dart b/lib/piv/views/cert_info_view.dart index 013dc742..311f23cb 100644 --- a/lib/piv/views/cert_info_view.dart +++ b/lib/piv/views/cert_info_view.dart @@ -18,10 +18,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; -import 'package:yubico_authenticator/app/message.dart'; -import 'package:yubico_authenticator/app/state.dart'; -import 'package:yubico_authenticator/piv/models.dart'; -import 'package:yubico_authenticator/widgets/tooltip_if_truncated.dart'; + +import '../../app/message.dart'; +import '../../app/state.dart'; +import '../../widgets/tooltip_if_truncated.dart'; +import '../models.dart'; class CertInfoTable extends ConsumerWidget { final CertInfo certInfo; @@ -36,7 +37,8 @@ class CertInfoTable extends ConsumerWidget { final subtitleStyle = textTheme.bodyMedium!.copyWith( color: textTheme.bodySmall!.color, ); - final dateFormat = DateFormat.yMMMEd(); + final dateFormat = + DateFormat.yMMMEd(ref.watch(currentLocaleProvider).toString()); final clipboard = ref.watch(clipboardProvider); final withContext = ref.watch(withContextProvider); From 01cc8aa6f74fc3eeadd1ee0823994e9458299099 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 30 Aug 2023 10:20:05 +0200 Subject: [PATCH 129/158] Show error message on invalid QR code. --- lib/oath/views/utils.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/oath/views/utils.dart b/lib/oath/views/utils.dart index cc2a6952..ee8ebb2f 100755 --- a/lib/oath/views/utils.dart +++ b/lib/oath/views/utils.dart @@ -70,7 +70,13 @@ Future handleUri( OathState? state, AppLocalizations l10n, ) async { - List creds = CredentialData.fromUri(Uri.parse(qrData)); + List creds; + try { + creds = CredentialData.fromUri(Uri.parse(qrData)); + } catch (_) { + showMessage(context, l10n.l_invalid_qr); + return; + } if (creds.isEmpty) { showMessage(context, l10n.l_qr_not_found); } else if (creds.length == 1) { From 0ce2e3163014dd9cad82e5d62d7bed5c14120fe6 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 30 Aug 2023 10:58:37 +0200 Subject: [PATCH 130/158] OATH: Correcly validate on auth-required. --- lib/desktop/oath/state.dart | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/desktop/oath/state.dart b/lib/desktop/oath/state.dart index 66029c1f..92aeb880 100755 --- a/lib/desktop/oath/state.dart +++ b/lib/desktop/oath/state.dart @@ -71,8 +71,21 @@ class _DesktopOathStateNotifier extends OathStateNotifier { ..setErrorHandler('state-reset', (_) async { ref.invalidate(_sessionProvider(devicePath)); }) - ..setErrorHandler('auth-required', (_) async { - ref.invalidateSelf(); + ..setErrorHandler('auth-required', (e) async { + final key = ref.read(_oathLockKeyProvider(_session.devicePath)); + if (key != null) { + final result = + await _session.command('validate', params: {'key': key}); + if (result['valid']) { + ref.invalidateSelf(); + return; + } else { + ref + .read(_oathLockKeyProvider(_session.devicePath).notifier) + .unsetKey(); + } + } + throw e; }); ref.onDispose(() { _session @@ -81,17 +94,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier { }); final result = await _session.command('get'); _log.debug('application status', jsonEncode(result)); - var oathState = OathState.fromJson(result['data']); - final key = ref.read(_oathLockKeyProvider(_session.devicePath)); - if (oathState.locked && key != null) { - final result = await _session.command('validate', params: {'key': key}); - if (result['valid']) { - oathState = oathState.copyWith(locked: false); - } else { - ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey(); - } - } - return oathState; + return OathState.fromJson(result['data']); } @override From af18b4ba52dda7e82ee497e85e56fe9a122f65b6 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 30 Aug 2023 14:13:08 +0200 Subject: [PATCH 131/158] PIV: Fix file import for FW < 5.3. --- helper/helper/piv.py | 2 +- lib/l10n/app_en.arb | 2 ++ lib/piv/views/import_file_dialog.dart | 28 ++++++++++++++++++++++++--- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/helper/helper/piv.py b/helper/helper/piv.py index 2478aa78..64ae044a 100644 --- a/helper/helper/piv.py +++ b/helper/helper/piv.py @@ -405,7 +405,7 @@ class SlotNode(RpcNode): self.session.put_key(self.slot, private_key, pin_policy, touch_policy) try: metadata = self.session.get_slot_metadata(self.slot) - except (ApduError, BadResponseError): + except (ApduError, BadResponseError, NotSupportedError): pass certificate = _choose_cert(certs) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 32b00d5b..d041dcff 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -428,6 +428,8 @@ "l_certificate_exported": "Certificate exported", "l_import_file": "Import file", "l_import_desc": "Import a key and/or certificate", + "l_importing_file": "Importing file\u2026", + "s_file_imported": "File imported", "l_delete_certificate": "Delete certificate", "l_delete_certificate_desc": "Remove the certificate from your YubiKey", "s_issuer": "Issuer", diff --git a/lib/piv/views/import_file_dialog.dart b/lib/piv/views/import_file_dialog.dart index c11fdba7..e21c2304 100644 --- a/lib/piv/views/import_file_dialog.dart +++ b/lib/piv/views/import_file_dialog.dart @@ -20,7 +20,9 @@ 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'; @@ -47,6 +49,7 @@ class _ImportFileDialogState extends ConsumerState { PivExamineResult? _state; String _password = ''; bool _passwordIsWrong = false; + bool _importing = false; @override void initState() { @@ -153,10 +156,10 @@ class _ImportFileDialogState extends ConsumerState { actions: [ TextButton( key: keys.unlockButton, - onPressed: (keyType == null && certInfo == null) + onPressed: (keyType == null && certInfo == null) || _importing ? null : () async { - final navigator = Navigator.of(context); + final withContext = ref.read(withContextProvider); if (!await confirmOverwrite( context, @@ -167,18 +170,37 @@ class _ImportFileDialogState extends ConsumerState { return; } + setState(() { + _importing = true; + }); + + void Function()? close; try { + close = await withContext( + (context) async => showMessage( + context, + l10n.l_importing_file, + duration: const Duration(seconds: 30), + )); await ref .read(pivSlotsProvider(widget.devicePath).notifier) .import(widget.pivSlot.slot, _data, password: _password.isNotEmpty ? _password : null); - navigator.pop(true); + await withContext( + (context) async { + Navigator.of(context).pop(true); + showMessage(context, l10n.s_file_imported); + }, + ); } catch (err) { // TODO: More error cases setState(() { _passwordIsWrong = true; + _importing = false; }); + } finally { + close?.call(); } }, child: Text(l10n.s_import), From eb9056fe9c947508b5122a29bcca8afefce535d3 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 30 Aug 2023 14:20:15 +0200 Subject: [PATCH 132/158] add padding to camera scanner info text widget --- lib/android/qr_scanner/qr_scanner_ui_view.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/android/qr_scanner/qr_scanner_ui_view.dart b/lib/android/qr_scanner/qr_scanner_ui_view.dart index e730b964..440f1caf 100644 --- a/lib/android/qr_scanner/qr_scanner_ui_view.dart +++ b/lib/android/qr_scanner/qr_scanner_ui_view.dart @@ -44,12 +44,15 @@ class QRScannerUI extends StatelessWidget { screenSize.height + scannerAreaWidth / 2.0 + 8.0), width: screenSize.width, height: screenSize.height), - child: Text( - status != ScanStatus.error - ? l10n.l_point_camera_scan - : l10n.l_invalid_qr, - style: const TextStyle(color: Colors.white), - textAlign: TextAlign.center, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + status != ScanStatus.error + ? l10n.l_point_camera_scan + : l10n.l_invalid_qr, + style: const TextStyle(color: Colors.white), + textAlign: TextAlign.center, + ), ), ), From 3aaf7c712a993517c867dd334e9fa819671ffbeb Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 30 Aug 2023 14:48:49 +0200 Subject: [PATCH 133/158] handle failure when decoding icon pack files --- lib/oath/icon_provider/icon_pack_manager.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/oath/icon_provider/icon_pack_manager.dart b/lib/oath/icon_provider/icon_pack_manager.dart index 3f725819..c9c4bb25 100644 --- a/lib/oath/icon_provider/icon_pack_manager.dart +++ b/lib/oath/icon_provider/icon_pack_manager.dart @@ -20,11 +20,11 @@ import 'dart:io'; import 'package:archive/archive.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:io/io.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:yubico_authenticator/app/logging.dart'; -import 'package:io/io.dart'; import 'icon_cache.dart'; import 'icon_pack.dart'; @@ -116,7 +116,16 @@ class IconPackManager extends StateNotifier> { final unpackDirectory = Directory(join(tempDirectory.path, 'unpack')); - final archive = ZipDecoder().decodeBytes(bytes, verify: true); + Archive archive; + try { + archive = ZipDecoder().decodeBytes(bytes, verify: true); + } on Exception catch (_) { + _log.error('File is not an icon pack: zip decoding failed'); + _lastError = l10n.l_invalid_icon_pack; + state = AsyncValue.error('File is not an icon pack', StackTrace.current); + return false; + } + for (final file in archive) { final filename = file.name; if (file.size > 0) { @@ -172,7 +181,8 @@ class IconPackManager extends StateNotifier> { } catch (e) { _log.error('Failed to copy icon pack files to destination: $e'); _lastError = l10n.l_icon_pack_copy_failed; - state = AsyncValue.error('Failed to copy icon pack files.', StackTrace.current); + state = AsyncValue.error( + 'Failed to copy icon pack files.', StackTrace.current); return false; } From c67f89ed3f129013dee1cc6f17fc4e89b975607b Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 30 Aug 2023 15:28:44 +0200 Subject: [PATCH 134/158] Android No Key Present item in Drawer --- lib/app/views/device_picker.dart | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/app/views/device_picker.dart b/lib/app/views/device_picker.dart index dc4331eb..68b19a4c 100644 --- a/lib/app/views/device_picker.dart +++ b/lib/app/views/device_picker.dart @@ -19,6 +19,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../../android/state.dart'; import '../../core/state.dart'; import '../../management/models.dart'; import '../models.dart'; @@ -118,6 +119,26 @@ class DevicePickerContent extends ConsumerWidget { final showUsb = isDesktop && devices.whereType().isEmpty; + Widget? androidNoKeyWidget; + if (isAndroid && devices.isEmpty) { + var hasNfcSupport = ref.watch(androidNfcSupportProvider); + var isNfcEnabled = ref.watch(androidNfcStateProvider); + final subtitle = hasNfcSupport && isNfcEnabled + ? l10n.l_insert_or_tap_yk + : l10n.l_insert_yk; + + androidNoKeyWidget = _DeviceRow( + leading: const DeviceAvatar(child: Icon(Icons.usb)), + title: l10n.l_no_yk_present, + subtitle: subtitle, + onTap: () { + ref.read(currentDeviceProvider.notifier).setCurrentDevice(null); + }, + selected: currentNode == null, + extended: extended, + ); + } + List children = [ if (showUsb) _DeviceRow( @@ -130,6 +151,8 @@ class DevicePickerContent extends ConsumerWidget { selected: currentNode == null, extended: extended, ), + if (androidNoKeyWidget != null) + androidNoKeyWidget, ...devices.map( (e) => e.path == currentNode?.path ? _buildCurrentDeviceRow( From 406ac7500bf0dba63a971157bc7a615343ce1636 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 31 Aug 2023 09:20:15 +0200 Subject: [PATCH 135/158] Add new strings. --- lib/l10n/app_ja.arb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index f69777dd..967772d8 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -428,6 +428,8 @@ "l_certificate_exported": "証明書がエクスポートされました", "l_import_file": "ファイルのインポート", "l_import_desc": "キーや証明書のインポート", + "l_importing_file": "ファイルのインポート中\u2026", + "s_file_imported": "ファイル をインポートしました", "l_delete_certificate": "証明書を削除", "l_delete_certificate_desc": "YubiKeyか証明書の削除", "s_issuer": "発行者", From 8839a0ddccfd9136cf7d26268a8c1b98fb086405 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Thu, 31 Aug 2023 09:38:32 +0200 Subject: [PATCH 136/158] update polish translation --- lib/l10n/app_pl.arb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 6949d83a..bfc39741 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -113,6 +113,7 @@ } }, "l_import_icon_pack_failed": "Błąd importu pakietu ikon: {message}", + "l_importing_file": "Importowanie pliku…", "l_insert_or_tap_yk": "Podłącz lub przystaw YubiKey", "l_insert_yk": "Podłącz klucz YubiKey", "l_invalid_character_issuer": "Nieprawidłowy znak: „:” nie jest dozwolony w polu wydawcy", @@ -378,6 +379,7 @@ "s_factory_reset": "Ustawienia fabryczne", "s_fido_disabled": "FIDO2 wyłączone", "s_fido_pin_protection": "Ochrona FIDO kodem PIN", + "s_file_imported": "Plik został zaimportowany", "s_fingerprint_added": "Dodano odcisk palca", "s_fingerprint_deleted": "Odcisk palca został usunięty", "s_fingerprint_renamed": "Zmieniono nazwę odcisku palca", From 1f8c9dc8fb4042ef5d44515200c8519af323beba Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Thu, 31 Aug 2023 13:11:48 +0200 Subject: [PATCH 137/158] Add semantics --- lib/oath/views/add_multi_account_page.dart | 117 +++++++++++---------- lib/piv/views/piv_screen.dart | 41 ++++---- 2 files changed, 84 insertions(+), 74 deletions(-) diff --git a/lib/oath/views/add_multi_account_page.dart b/lib/oath/views/add_multi_account_page.dart index 08280c34..b0b05536 100644 --- a/lib/oath/views/add_multi_account_page.dart +++ b/lib/oath/views/add_multi_account_page.dart @@ -90,61 +90,68 @@ class _OathAddMultiAccountPageState controlAffinity: ListTileControlAffinity.leading, secondary: Row(mainAxisSize: MainAxisSize.min, children: [ if (isTouchSupported()) - IconButton( - tooltip: l10n.s_require_touch, - color: touch ? colorScheme.primary : null, - onPressed: unique - ? () { - setState(() { - _credStates[cred] = - (checked, !touch, unique); - }); - } - : null, - icon: Icon(touch - ? Icons.touch_app - : Icons.touch_app_outlined)), - IconButton( - tooltip: l10n.s_rename_account, - onPressed: () async { - final node = ref - .read(currentDeviceDataProvider) - .valueOrNull - ?.node; - final withContext = ref.read(withContextProvider); - CredentialData? renamed = await withContext( - (context) async => await showBlurDialog( - context: context, - builder: (context) => RenameAccountDialog( - device: node!, - issuer: cred.issuer, - name: cred.name, - oathType: cred.oathType, - period: cred.period, - existing: (widget.credentialsFromUri ?? []) - .map((e) => (e.issuer, e.name)) - .followedBy((_credentials ?? []) - .map((e) => (e.issuer, e.name))) - .toList(), - rename: (issuer, name) async => cred - .copyWith(issuer: issuer, name: name), - ), - )); - if (renamed != null) { - setState(() { - int index = widget.credentialsFromUri!.indexWhere( - (element) => - element.name == cred.name && - (element.issuer == cred.issuer)); - widget.credentialsFromUri![index] = renamed; - _credStates.remove(cred); - _credStates[renamed] = (true, touch, true); - }); - } - }, - icon: IconTheme( - data: IconTheme.of(context), - child: const Icon(Icons.edit_outlined)), + Semantics( + label: l10n.s_require_touch, + child: IconButton( + tooltip: l10n.s_require_touch, + color: touch ? colorScheme.primary : null, + onPressed: unique + ? () { + setState(() { + _credStates[cred] = + (checked, !touch, unique); + }); + } + : null, + icon: Icon(touch + ? Icons.touch_app + : Icons.touch_app_outlined)), + ), + Semantics( + label: l10n.s_rename_account, + child: IconButton( + tooltip: l10n.s_rename_account, + onPressed: () async { + final node = ref + .read(currentDeviceDataProvider) + .valueOrNull + ?.node; + final withContext = ref.read(withContextProvider); + CredentialData? renamed = await withContext( + (context) async => await showBlurDialog( + context: context, + builder: (context) => RenameAccountDialog( + device: node!, + issuer: cred.issuer, + name: cred.name, + oathType: cred.oathType, + period: cred.period, + existing: (widget.credentialsFromUri ?? + []) + .map((e) => (e.issuer, e.name)) + .followedBy((_credentials ?? []) + .map((e) => (e.issuer, e.name))) + .toList(), + rename: (issuer, name) async => cred + .copyWith(issuer: issuer, name: name), + ), + )); + if (renamed != null) { + setState(() { + int index = widget.credentialsFromUri!.indexWhere( + (element) => + element.name == cred.name && + (element.issuer == cred.issuer)); + widget.credentialsFromUri![index] = renamed; + _credStates.remove(cred); + _credStates[renamed] = (true, touch, true); + }); + } + }, + icon: IconTheme( + data: IconTheme.of(context), + child: const Icon(Icons.edit_outlined)), + ), ), ]), title: Text(cred.issuer ?? cred.name, diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart index 3727f812..1ad9aa0a 100644 --- a/lib/piv/views/piv_screen.dart +++ b/lib/piv/views/piv_screen.dart @@ -97,24 +97,27 @@ class _CertificateListItem extends StatelessWidget { final l10n = AppLocalizations.of(context)!; final colorScheme = Theme.of(context).colorScheme; - return AppListItem( - leading: CircleAvatar( - foregroundColor: colorScheme.onSecondary, - backgroundColor: colorScheme.secondary, - child: const Icon(Icons.approval), - ), - title: slot.getDisplayName(l10n), - subtitle: certInfo != null - // Simplify subtitle by stripping "CN=", etc. - ? certInfo.subject.replaceAll(RegExp(r'[A-Z]+='), ' ').trimLeft() - : pivSlot.hasKey == true - ? l10n.l_key_no_certificate - : l10n.l_no_certificate, - trailing: OutlinedButton( - onPressed: Actions.handler(context, const OpenIntent()), - child: const Icon(Icons.more_horiz), - ), - buildPopupActions: (context) => buildSlotActions(certInfo != null, l10n), - ); + return Semantics( + label: slot.getDisplayName(l10n), + child: AppListItem( + leading: CircleAvatar( + foregroundColor: colorScheme.onSecondary, + backgroundColor: colorScheme.secondary, + child: const Icon(Icons.approval), + ), + title: slot.getDisplayName(l10n), + subtitle: certInfo != null + // Simplify subtitle by stripping "CN=", etc. + ? certInfo.subject.replaceAll(RegExp(r'[A-Z]+='), ' ').trimLeft() + : pivSlot.hasKey == true + ? l10n.l_key_no_certificate + : l10n.l_no_certificate, + trailing: OutlinedButton( + onPressed: Actions.handler(context, const OpenIntent()), + child: const Icon(Icons.more_horiz), + ), + buildPopupActions: (context) => + buildSlotActions(certInfo != null, l10n), + )); } } From da42147811e062637763a71eac2e439c92ac4387 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Thu, 31 Aug 2023 16:52:08 +0200 Subject: [PATCH 138/158] fixes dialog display on Android --- lib/widgets/responsive_dialog.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/widgets/responsive_dialog.dart b/lib/widgets/responsive_dialog.dart index 511457da..dd500f15 100755 --- a/lib/widgets/responsive_dialog.dart +++ b/lib/widgets/responsive_dialog.dart @@ -16,6 +16,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:yubico_authenticator/core/state.dart'; class ResponsiveDialog extends StatefulWidget { final Widget? title; @@ -97,6 +98,7 @@ class _ResponsiveDialogState extends State { @override Widget build(BuildContext context) => LayoutBuilder(builder: ((context, constraints) { + var maxWidth = isDesktop ? 400 : 600; // This keeps the focus in the dialog, even if the underlying page changes. return FocusScope( node: _focus, @@ -107,7 +109,7 @@ class _ResponsiveDialogState extends State { _hasLostFocus = true; } }, - child: constraints.maxWidth < 400 + child: constraints.maxWidth < maxWidth ? _buildFullscreen(context) : _buildDialog(context), ); From c2a72dcdfd051731d310d209a534a0388cec5edf Mon Sep 17 00:00:00 2001 From: Rikard Braathen Date: Thu, 31 Aug 2023 17:33:23 +0200 Subject: [PATCH 139/158] added 40+ strings, fixed grammatics and minor errors --- lib/l10n/app_fr.arb | 109 +++++++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index d67e4ed6..c35cfeb1 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -30,13 +30,15 @@ "s_unlock": "Déverrouiller", "s_calculate": "Calculer", "s_import": "Importer", + "s_overwrite": "Écraser", "s_label": "Étiquette", "s_name": "Nom", "s_usb": "USB", "s_nfc": "NFC", + "s_options": "Options", "s_show_window": "Afficher la fenêtre", "s_hide_window": "Masquer la fenêtre", - "q_rename_target": "Renommer l'{label}?", + "q_rename_target": "Renommer {label}?", "@q_rename_target" : { "placeholders": { "label": {} @@ -50,6 +52,7 @@ }, "s_about": "À propos", + "s_algorithm": "Algorithme", "s_appearance": "Apparence", "s_authenticator": "Authenticator", "s_actions": "Actions", @@ -212,6 +215,12 @@ "retries": {} } }, + "l_wrong_puk_attempts_remaining": "Mauvais PUK, {retries} tentative(s) restante(s)", + "@l_wrong_puk_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, "s_fido_pin_protection": "PIN de protection FIDO", "l_fido_pin_protection_optional": "PIN de protection optionnel FIDO", "l_enter_fido2_pin": "Entrez le PIN FIDO2 de votre YubiKey", @@ -231,6 +240,7 @@ "s_pin_required": "PIN requis", "p_pin_required_desc": "L'action que vous allez faire demande d'entrer le code PIN du PIV.", "l_piv_pin_blocked": "Vous êtes bloqué, utilisez le code PUK pour réinitialiser", + "l_piv_pin_puk_blocked": "Bloqué, réinitialisation nécesssaire", "p_enter_new_piv_pin_puk": "Entrez un nouveau {name} entre 6 et 8 caractères.", "@p_enter_new_piv_pin_puk" : { "placeholders": { @@ -253,7 +263,7 @@ "s_remember_password": "Retenir le mot de passe", "s_clear_saved_password": "Effacer le mot de passe", "s_password_forgotten": "Mot de passe oublié", - "l_keystore_unavailable": "Le magasin de clés de l'OS est indisponible", + "l_keystore_unavailable": "OS Keystore indisponible", "l_remember_pw_failed": "Échec de la mémorisation du mot de passe", "l_unlock_first": "Déverrouillez initialement avec un mot de passe", "l_enter_oath_pw": "Entrez le mot de passe OATH de votre YubiKey", @@ -268,6 +278,8 @@ "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", "l_default_key_used": "Clé de gestion par défaut utilisée", + "s_generate_random": "Génération aléatoire", + "s_use_default": "Utiliser la valeur par défaut", "l_warning_default_key": "Attention: Clé par défaut utilisée", "s_protect_key": "Protection par PIN", "l_pin_protected_key": "Un PIN peut être utilisé à la place", @@ -285,6 +297,9 @@ "s_accounts": "Comptes", "s_no_accounts": "Aucun compte", "s_add_account": "Ajouter un compte", + "s_add_accounts" : "Ajouter un/des compte(s)", + "p_add_description" : "Pour scanner un code QR, assurez-vous que le code complet est visible à l'écran et appuyez sur le bouton ci-dessous. Vous pouvez également faire glisser une image enregistrée dans un dossier vers cette boîte de dialogue. Si vous disposez des informations d'identification du compte par écrit, utilisez plutôt la saisie manuelle.", + "s_add_manually" : "Ajouter manuellement", "s_account_added": "Compte ajouté", "l_account_add_failed": "Échec d'ajout d'un compte: {message}", "@l_account_add_failed" : { @@ -294,16 +309,18 @@ }, "l_account_name_required": "Votre compte doit avoir un nom", "l_name_already_exists": "Ce nom existe déjà pour cet émetteur", + "l_account_already_exists": "Ce compte existe déjà sur la YubiKey", "l_invalid_character_issuer": "Caractère invalide: ':' n'est pas autorisé pour un émetteur", + "l_select_accounts" : "Sélectionner le/les compte(s) à ajouter à la YubiKey", "s_pinned": "Épinglé", "s_pin_account": "Épingler un compte", "s_unpin_account": "Désépingler un compte", "s_no_pinned_accounts": "Aucun compte épinglé", "l_pin_account_desc": "Gardez vos comptes importants ensembles", "s_rename_account": "Renommer le compte", - "l_rename_account_desc": "Éditer l'émetteur / le nom du comte", + "l_rename_account_desc": "Éditer l'émetteur / le nom du compte", "s_account_renamed": "Compte renommé", - "p_rename_will_change_account_displayed": "Cette action changera comment le compte est affiché dans la liste.", + "p_rename_will_change_account_displayed": "Cette action changera la façon dont le compte est affiché dans la liste.", "s_delete_account": "Supprimer le compte", "l_delete_account_desc": "Supprimer le compte de votre YubiKey", "s_account_deleted": "Compte supprimé", @@ -330,7 +347,7 @@ "num": {} } }, - "s_issuer_optional": "Émetteur (optional)", + "s_issuer_optional": "Émetteur (optionnel)", "s_counter_based": "Basé sur un décompte", "s_time_based": "Basé sur le temps", "l_copy_code_desc": "Coller facilement le code depuis une autre application", @@ -347,7 +364,7 @@ "s_passkeys": "Passkeys", "l_ready_to_use": "Prêt à l'emploi", "l_register_sk_on_websites": "Enregistrer comme clé de sécurité sur les sites internet", - "l_no_discoverable_accounts": "Aucune Passkeys détectée", + "l_no_discoverable_accounts": "Aucune Passkey détectée", "s_delete_passkey": "Supprimer une Passkey", "l_delete_passkey_desc": "Supprimer la Passkey de votre YubiKey", "s_passkey_deleted": "Passkey supprimée", @@ -411,34 +428,15 @@ "l_certificate_exported": "Certificat exporté", "l_import_file": "Importer un fichier", "l_import_desc": "Importer une clé et/ou un certificat", + "l_importing_file": "Importation d'un fichier\u2026", + "s_file_imported": "Fichier importé", "l_delete_certificate": "Supprimer un certificat", "l_delete_certificate_desc": "Supprimer un certificat de votre YubiKey", - "l_subject_issuer": "Sujet: {subject}, Émetteur: {issuer}", - "@l_subject_issuer" : { - "placeholders": { - "subject": {}, - "issuer": {} - } - }, - "l_serial": "Serial: {serial}", - "@l_serial" : { - "placeholders": { - "serial": {} - } - }, - "l_certificate_fingerprint": "Empreinte: {fingerprint}", - "@l_certificate_fingerprint" : { - "placeholders": { - "fingerprint": {} - } - }, - "l_valid": "Valide: {not_before} - {not_after}", - "@l_valid" : { - "placeholders": { - "not_before": {}, - "not_after": {} - } - }, + "s_issuer": "Émetteur", + "s_serial": "Série", + "s_certificate_fingerprint": "Empreinte digitale", + "s_valid_from": "Valide à partir de", + "s_valid_to": "Valide jusqu'à", "l_no_certificate": "Aucun certificat chargé", "l_key_no_certificate": "Clé sans certificat chargé", "s_generate_key": "Générer une clé", @@ -466,6 +464,20 @@ "slot": {} } }, + "p_subject_desc": "Un nom distinctif (DN) formaté conformément à la spécification RFC 4514.", + "l_rfc4514_invalid": "Format RFC 4514 invalide", + "rfc4514_examples": "Exemples:\nCN=Example Name\nCN=jsmith,DC=example,DC=net", + "p_cert_options_desc": "Algorithme de clé à utiliser, format de sortie et date d'expiration (certificat uniquement).", + "s_overwrite_slot": "Écraser l'emplacement", + "p_overwrite_slot_desc": "Cette opération écrase de manière permanente le contenu existant dans le slot {slot}.", + "@p_overwrite_slot_desc" : { + "placeholders": { + "slot": {} + } + }, + "l_overwrite_cert": "Le certificat sera écrasé", + "l_overwrite_key": "La clé privée sera écrasée", + "l_overwrite_key_maybe": "Toute clé privée existante dans le slot sera écrasée", "@_piv_slots": {}, "s_slot_display_name": "{name} ({hexid})", @@ -522,12 +534,12 @@ }, "s_reset_piv": "Réinitialiser le PIV", "l_piv_app_reset": "L'application PIV à été réinitialisée", - "p_warning_factory_reset": "Attention! Cette action supprimera de manière irrévocable tous les compte OATH TOTP/HOTP de votre YubiKey.", + "p_warning_factory_reset": "Attention! Cette action supprimera de manière irrévocable tous les comptes OATH TOTP/HOTP de votre YubiKey.", "p_warning_disable_credentials": "Vos identifiants OATH, ainsi que vos mots de passes, seront supprimés de votre YubiKey. Assurez vous de désactiver les identifiants des sites pour ne pas être verrouillé hors de vos comptes.", "p_warning_deletes_accounts": "Attention! Cette action supprimera de manière irrévocable tous les comptes U2F et FIDO2 de votre YubiKey.", "p_warning_disable_accounts": "Vos identifiants, ainsi que les codes PIN associés, seront supprimés de votre YubiKey. Assurez vous de désactiver les identifiants des sites pour ne pas être verrouillé hors de vos comptes.", "p_warning_piv_reset": "Attention! Cette action supprimera de manière irrévocable toutes les données PIV stockées sur votre YubiKey.", - "p_warning_piv_reset_desc": "Cela inclus les clé privées et les certificats. Votre PIN, PUK, clé de management seront réinitialisés à leur valeurs d'usines.", + "p_warning_piv_reset_desc": "Cela inclus les clé privées et les certificats. Votre PIN, PUK, clé de management seront réinitialisés à leur valeurs d'usine.", "@_copy_to_clipboard": {}, "l_copy_to_clipboard": "Copier vers le presse papier", @@ -545,8 +557,8 @@ "@_custom_icons": {}, "s_custom_icons": "Icônes personnalisées", - "l_set_icons_for_accounts": "Sélectionner les icônes pour les comptesSet icons for accounts", - "p_custom_icons_description": "Les packs d'cônes peuvent rendre vos comptes plus facilement repérables grâce à des logos et couleurs familières.", + "l_set_icons_for_accounts": "Sélectionner les icônes pour les comptes", + "p_custom_icons_description": "Les packs d'icônes peuvent rendre vos comptes plus facilement repérables grâce à des logos et couleurs familières.", "s_replace_icon_pack": "Remplacer le pack d'icônes", "l_loading_icon_pack": "Chargement du pack d'icônes\u2026", "s_load_icon_pack": "Charger le pack d'icônes", @@ -573,16 +585,31 @@ "l_kbd_layout_for_static": "Arrangement clavier (pour les mot de passes statiques)", "s_choose_kbd_layout": "Choisissez l'arrangement clavier", "l_bypass_touch_requirement": "Contourner la nécessité de toucher la YubiKey", - "l_bypass_touch_requirement_on": "Les comptes qui demande le touché sont automatiquement montrés via NFC", - "l_bypass_touch_requirement_off": "Les compte qui demande un couché ont besoin d'un contact supplémentaire NFC", + "l_bypass_touch_requirement_on": "Les comptes nécessitant un contact sont automatiquement affichés via NFC", + "l_bypass_touch_requirement_off": "Les comptes nécessitant un contact sur la YubiKey doivent faire l'objet d'un contact NFC supplémentaire", "s_silence_nfc_sounds": "Couper le son NFC", - "l_silence_nfc_sounds_on": "Aucun sons ne sera joué lors du contact NFC", + "l_silence_nfc_sounds_on": "Aucun son ne sera joué lors du contact NFC", "l_silence_nfc_sounds_off": "Du son sera joué lors du contact NFC", "s_usb_options": "Options USB", "l_launch_app_on_usb": "Lancer lorsque la YubiKey est connectée", - "l_launch_app_on_usb_on": "Cela empêchera que d'autre applications utilisent la YubiKey via l'USB", - "l_launch_app_on_usb_off": "D'autres applications peuvent utiliser la YubiKey via l'USB", + "l_launch_app_on_usb_on": "Cela empêchera que d'autres applications utilisent la YubiKey via USB", + "l_launch_app_on_usb_off": "D'autres applications peuvent utiliser la YubiKey via USB", "s_allow_screenshots": "Autoriser les captures d'écrans", + "s_nfc_dialog_tap_key": "Effleurez votre YubiKey", + "s_nfc_dialog_operation_success": "Succès", + "s_nfc_dialog_operation_failed": "Échec", + + "s_nfc_dialog_oath_reset": "Action: réinitialiser l'applet OATH", + "s_nfc_dialog_oath_unlock": "Action: déverouiller l'applet OATH", + "s_nfc_dialog_oath_set_password": "Action: définir le mot de passe OATH", + "s_nfc_dialog_oath_unset_password": "Action: supprimer le mot de passe OATH", + "s_nfc_dialog_oath_add_account": "Action: ajouter un nouveau compte", + "s_nfc_dialog_oath_rename_account": "Action: renommer le compte", + "s_nfc_dialog_oath_delete_account": "Action: supprimer le compte", + "s_nfc_dialog_oath_calculate_code": "Action: calculer le code OATH", + "s_nfc_dialog_oath_failure": "Échec de l'opération OATH", + "s_nfc_dialog_oath_add_multiple_accounts": "Action: ajouter plusieurs comptes", + "@_eof": {} } \ No newline at end of file From 0a79dda34ef82800c58be26fab603e79d9b8f2ed Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Fri, 1 Sep 2023 13:19:16 +0200 Subject: [PATCH 140/158] add semanticsLabel to code widget --- lib/oath/views/account_helper.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/oath/views/account_helper.dart b/lib/oath/views/account_helper.dart index bca35a01..32174f47 100755 --- a/lib/oath/views/account_helper.dart +++ b/lib/oath/views/account_helper.dart @@ -171,6 +171,7 @@ class _CodeLabel extends StatelessWidget { // This helps with vertical centering on desktop applyHeightToFirstAscent: !isDesktop, ), + semanticsLabel: code?.value.characters.map((c) => '$c ' ).toString(), ), ); } From f5937f8416d2a20d3229b712305b2c42e05a3e20 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Fri, 1 Sep 2023 14:51:17 +0200 Subject: [PATCH 141/158] bump version to 6.3.0 --- helper/version_info.txt | 4 ++-- lib/version.dart | 2 +- pubspec.yaml | 2 +- resources/win/release-win.ps1 | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/helper/version_info.txt b/helper/version_info.txt index 725640d4..81c1797a 100755 --- a/helper/version_info.txt +++ b/helper/version_info.txt @@ -31,11 +31,11 @@ VSVersionInfo( '040904b0', [StringStruct('CompanyName', 'Yubico'), StringStruct('FileDescription', 'Yubico Authenticator Helper'), - StringStruct('FileVersion', '6.3.0-dev.0'), + StringStruct('FileVersion', '6.3.0'), StringStruct('LegalCopyright', 'Copyright (c) Yubico'), StringStruct('OriginalFilename', 'authenticator-helper.exe'), StringStruct('ProductName', 'Yubico Authenticator'), - StringStruct('ProductVersion', '6.3.0-dev.0')]) + StringStruct('ProductVersion', '6.3.0')]) ]), VarFileInfo([VarStruct('Translation', [1033, 1200])]) ] diff --git a/lib/version.dart b/lib/version.dart index abb74145..8ea1b383 100755 --- a/lib/version.dart +++ b/lib/version.dart @@ -1,5 +1,5 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // This file is generated by running ./set-version.py -const String version = '6.3.0-dev.0'; +const String version = '6.3.0'; const int build = 60300; diff --git a/pubspec.yaml b/pubspec.yaml index 097edcd3..0c3934dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # This field is updated by running ./set-version.py # DO NOT MANUALLY EDIT THIS! -version: 6.3.0-dev.0+60300 +version: 6.3.0+60300 environment: sdk: '>=3.0.0 <4.0.0' diff --git a/resources/win/release-win.ps1 b/resources/win/release-win.ps1 index db18c7ff..fc36733e 100644 --- a/resources/win/release-win.ps1 +++ b/resources/win/release-win.ps1 @@ -1,4 +1,4 @@ -$version="6.3.0-dev.0" +$version="6.3.0" echo "Clean-up of old files" rm *.msi From 1c584b846cacb4a3705962846ca7d8fb5999553c Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 4 Sep 2023 08:49:47 +0200 Subject: [PATCH 142/158] bump archive to 3.3.8 --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 7a301c29..630a1405 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: "direct main" description: name: archive - sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e" url: "https://pub.dev" source: hosted - version: "3.3.7" + version: "3.3.8" args: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0c3934dd..cc3352c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,7 +59,7 @@ dependencies: vector_graphics_compiler: ^1.1.7 path: ^1.8.2 file_picker: ^5.3.2 - archive: ^3.3.2 + archive: ^3.3.8 crypto: ^3.0.2 tray_manager: ^0.2.0 local_notifier: ^0.1.5 From 97ab9667c5038e1db44d67863dc8e7244af5740b Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 4 Sep 2023 09:09:45 +0200 Subject: [PATCH 143/158] bump riverpod to 2.3.10 --- pubspec.lock | 8 ++++---- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 630a1405..63df709d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -276,10 +276,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "4615271bb6c1302d41cf4daff689d39a87045137986eb71de154e6f99ff18139" + sha256: b04d4e9435a563673746ccb328d22018c6c9496bb547e11dd56c1b0cc9829fe5 url: "https://pub.dev" source: hosted - version: "2.3.9" + version: "2.3.10" flutter_test: dependency: "direct dev" description: flutter @@ -607,10 +607,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "52b2937f5b9552987f35419f1deef22474d7fc609aea2f4c5b6c11b4449a287c" + sha256: "6c0a2c30c04206ac05494bcccd8148b76866e1a9248a5a8c84ca7b16fbcb3f6a" url: "https://pub.dev" source: hosted - version: "2.3.9" + version: "2.3.10" screen_retriever: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index cc3352c5..5053cb8b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,7 +45,7 @@ dependencies: logging: ^1.2.0 collection: ^1.16.0 shared_preferences: ^2.1.2 - flutter_riverpod: ^2.3.7 + flutter_riverpod: ^2.3.10 json_annotation: ^4.8.1 freezed_annotation: ^2.2.0 window_manager: ^0.3.2 From 641e24dc6db839f3b9b314222936628135d61768 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 4 Sep 2023 10:22:33 +0200 Subject: [PATCH 144/158] update NEWS --- NEWS | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/NEWS b/NEWS index 7112d6ed..858581e6 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,11 @@ +* Version 6.3.0 (released 2023-09-04) + ** Add support for importing accounts through QR codes from Google Authenticator. + ** Add community translations for French, Japanese, German and Polish languages + ** Improve user interface with new Material UI widgets + ** Security and general bug fixes based on user feedback + ** Desktop: Add support for PIV. + ** Android: Update Android 14 compatibility + * Version 6.2.0 (released 2023-04-19) ** Add support for custom account icons. ** Desktop: Add systray icon for quick access to pinned accounts. From 0fec64bba0b4bf5dd8cb4d75043f234c67393067 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 4 Sep 2023 10:55:22 +0200 Subject: [PATCH 145/158] fix NEWS style --- NEWS | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index 858581e6..799a9f68 100644 --- a/NEWS +++ b/NEWS @@ -1,10 +1,10 @@ * Version 6.3.0 (released 2023-09-04) ** Add support for importing accounts through QR codes from Google Authenticator. - ** Add community translations for French, Japanese, German and Polish languages - ** Improve user interface with new Material UI widgets - ** Security and general bug fixes based on user feedback + ** Add community translations for French, Japanese, German and Polish languages. + ** Improve user interface with new Material UI widgets. + ** Security and general bug fixes based on user feedback. ** Desktop: Add support for PIV. - ** Android: Update Android 14 compatibility + ** Android: Update Android 14 compatibility. * Version 6.2.0 (released 2023-04-19) ** Add support for custom account icons. From 9467d9a4a03df483c938f7de96fb661a81568d39 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 4 Sep 2023 11:01:26 +0200 Subject: [PATCH 146/158] improve wording in NEWS --- NEWS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 799a9f68..4e331663 100644 --- a/NEWS +++ b/NEWS @@ -2,7 +2,7 @@ ** Add support for importing accounts through QR codes from Google Authenticator. ** Add community translations for French, Japanese, German and Polish languages. ** Improve user interface with new Material UI widgets. - ** Security and general bug fixes based on user feedback. + ** Bug fixes and improvements based on user feedback. ** Desktop: Add support for PIV. ** Android: Update Android 14 compatibility. From a1ce32536a556ebc8c330a41168211b35cd95ade Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 4 Sep 2023 16:52:22 +0200 Subject: [PATCH 147/158] Bump version to 6.3.1-dev.0. --- helper/version_info.txt | 8 ++++---- lib/version.dart | 4 ++-- pubspec.yaml | 2 +- resources/win/release-win.ps1 | 2 +- resources/win/yubioath-desktop.wxs | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/helper/version_info.txt b/helper/version_info.txt index 81c1797a..c92cb8ea 100755 --- a/helper/version_info.txt +++ b/helper/version_info.txt @@ -6,8 +6,8 @@ VSVersionInfo( ffi=FixedFileInfo( # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) # Set not needed items to zero 0. - filevers=(6, 3, 0, 0), - prodvers=(6, 3, 0, 0), + filevers=(6, 3, 1, 0), + prodvers=(6, 3, 1, 0), # Contains a bitmask that specifies the valid bits 'flags'r mask=0x3f, # Contains a bitmask that specifies the Boolean attributes of the file. @@ -31,11 +31,11 @@ VSVersionInfo( '040904b0', [StringStruct('CompanyName', 'Yubico'), StringStruct('FileDescription', 'Yubico Authenticator Helper'), - StringStruct('FileVersion', '6.3.0'), + StringStruct('FileVersion', '6.3.1-dev.0'), StringStruct('LegalCopyright', 'Copyright (c) Yubico'), StringStruct('OriginalFilename', 'authenticator-helper.exe'), StringStruct('ProductName', 'Yubico Authenticator'), - StringStruct('ProductVersion', '6.3.0')]) + StringStruct('ProductVersion', '6.3.1-dev.0')]) ]), VarFileInfo([VarStruct('Translation', [1033, 1200])]) ] diff --git a/lib/version.dart b/lib/version.dart index 8ea1b383..707958ee 100755 --- a/lib/version.dart +++ b/lib/version.dart @@ -1,5 +1,5 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // This file is generated by running ./set-version.py -const String version = '6.3.0'; -const int build = 60300; +const String version = '6.3.1-dev.0'; +const int build = 60301; diff --git a/pubspec.yaml b/pubspec.yaml index 5053cb8b..7229fc8a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # This field is updated by running ./set-version.py # DO NOT MANUALLY EDIT THIS! -version: 6.3.0+60300 +version: 6.3.1-dev.0+60301 environment: sdk: '>=3.0.0 <4.0.0' diff --git a/resources/win/release-win.ps1 b/resources/win/release-win.ps1 index fc36733e..4544d5d3 100644 --- a/resources/win/release-win.ps1 +++ b/resources/win/release-win.ps1 @@ -1,4 +1,4 @@ -$version="6.3.0" +$version="6.3.1-dev.0" echo "Clean-up of old files" rm *.msi diff --git a/resources/win/yubioath-desktop.wxs b/resources/win/yubioath-desktop.wxs index 55ad622c..0f86aa15 100644 --- a/resources/win/yubioath-desktop.wxs +++ b/resources/win/yubioath-desktop.wxs @@ -1,7 +1,7 @@ - + From 68b25b89ba2642049697012facad2faf045b41a3 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 18 Sep 2023 13:24:40 +0200 Subject: [PATCH 148/158] Build universal2 helper on MacOS. --- .github/workflows/macos.yml | 3 + build-helper.sh | 43 +++++- helper/authenticator-helper.spec | 2 +- helper/helper/qr.py | 1 - helper/poetry.lock | 232 +++++++++++++------------------ helper/pyproject.toml | 2 +- 6 files changed, 141 insertions(+), 142 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index ed5d30b5..7766f79c 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -8,6 +8,9 @@ jobs: runs-on: macos-latest env: MACOSX_DEPLOYMENT_TARGET: "10.15" + CFLAGS: -arch x86_64 -arch arm64 + ARCHFLAGS: -arch x86_64 -arch arm64 + CMAKE_OSX_ARCHITECTURES: arm64;x86_64 steps: - uses: actions/checkout@v3 diff --git a/build-helper.sh b/build-helper.sh index e8d81c76..ab894d4d 100755 --- a/build-helper.sh +++ b/build-helper.sh @@ -7,7 +7,7 @@ set -e case "$(uname)" in - Darwin*) + Darwin*) OS="macos";; Linux*) OS="linux";; @@ -20,6 +20,47 @@ OUTPUT="build/$OS" cd helper poetry install + +# Create a universal binary on MacOS +if [ "$OS" = "macos" ]; then + PYTHON=`poetry run python -c "import sys; print(sys.executable)"` + echo "Using Python: $PYTHON" + if [ $(lipo -archs $PYTHON | grep -c 'x86_64 arm64') -ne 0 ]; then + echo "Fixing single-arch dependencies..." + HELPER="../$OUTPUT/helper" + rm -rf $HELPER + mkdir -p $HELPER + + # Needed to build zxing-cpp properly + export CMAKE_OSX_ARCHITECTURES="arm64;x86_64" + + # Export exact versions + poetry export --without-hashes > $HELPER/requirements.txt + grep cryptography $HELPER/requirements.txt > $HELPER/cryptography.txt + grep cffi $HELPER/requirements.txt > $HELPER/cffi.txt + grep pillow $HELPER/requirements.txt > $HELPER/pillow.txt + grep zxing-cpp $HELPER/requirements.txt > $HELPER/zxing-cpp.txt + # Remove non-universal packages + poetry run pip uninstall -y cryptography cffi pillow zxing-cpp + # Build cffi from source to get universal build + poetry run pip install --upgrade -r $HELPER/cffi.txt --no-binary cffi + # Build zxing-cpp from source to get universal build + poetry run pip install --upgrade -r $HELPER/zxing-cpp.txt --no-binary zxing-cpp + # Explicitly install pre-build universal build of cryptography + poetry run pip download -r $HELPER/cryptography.txt --platform macosx_10_12_universal2 --only-binary :all: --no-deps --dest $HELPER + poetry run pip install -r $HELPER/cryptography.txt --no-cache-dir --no-index --find-links $HELPER + # Combine wheels of pillow to get universal build + poetry run pip download -r $HELPER/pillow.txt --platform macosx_10_10_x86_64 --only-binary :all: --no-deps --dest $HELPER + poetry run pip download -r $HELPER/pillow.txt --platform macosx_11_0_arm64 --only-binary :all: --no-deps --dest $HELPER + poetry run pip install delocate + poetry run delocate-fuse $HELPER/Pillow*.whl + WHL=$(ls $HELPER/Pillow*x86_64.whl) + UNIVERSAL_WHL=${WHL//x86_64/universal2} + mv $WHL $UNIVERSAL_WHL + poetry run pip install --upgrade $UNIVERSAL_WHL + fi +fi + rm -rf ../$OUTPUT/helper poetry run pyinstaller authenticator-helper.spec --distpath ../$OUTPUT cd .. diff --git a/helper/authenticator-helper.spec b/helper/authenticator-helper.spec index 4c260d56..5d714ae0 100755 --- a/helper/authenticator-helper.spec +++ b/helper/authenticator-helper.spec @@ -36,7 +36,7 @@ exe = EXE( manifest="authenticator-helper.exe.manifest", version="version_info.txt", disable_windowed_traceback=False, - target_arch=None, + target_arch="universal2", codesign_identity=None, entitlements_file=None, ) diff --git a/helper/helper/qr.py b/helper/helper/qr.py index 1109f71f..fd2670c0 100644 --- a/helper/helper/qr.py +++ b/helper/helper/qr.py @@ -22,7 +22,6 @@ import subprocess # nosec import tempfile from mss.exception import ScreenShotError from PIL import Image -import numpy.core.multiarray # noqa def _capture_screen(): diff --git a/helper/poetry.lock b/helper/poetry.lock index 0eec8b21..e5a531f6 100755 --- a/helper/poetry.lock +++ b/helper/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "altgraph" @@ -329,43 +329,6 @@ files = [ {file = "mss-9.0.1.tar.gz", hash = "sha256:6eb7b9008cf27428811fa33aeb35f3334db81e3f7cc2dd49ec7c6e5a94b39f12"}, ] -[[package]] -name = "numpy" -version = "1.24.4" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, - {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, - {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, - {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, - {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, - {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, - {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, - {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, - {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, - {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, - {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, -] - [[package]] name = "packaging" version = "23.1" @@ -390,65 +353,65 @@ files = [ [[package]] name = "pillow" -version = "10.0.0" +version = "10.0.1" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "Pillow-10.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f62406a884ae75fb2f818694469519fb685cc7eaff05d3451a9ebe55c646891"}, - {file = "Pillow-10.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d5db32e2a6ccbb3d34d87c87b432959e0db29755727afb37290e10f6e8e62614"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf4392b77bdc81f36e92d3a07a5cd072f90253197f4a52a55a8cec48a12483b"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:520f2a520dc040512699f20fa1c363eed506e94248d71f85412b625026f6142c"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:8c11160913e3dd06c8ffdb5f233a4f254cb449f4dfc0f8f4549eda9e542c93d1"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a74ba0c356aaa3bb8e3eb79606a87669e7ec6444be352870623025d75a14a2bf"}, - {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d0dae4cfd56969d23d94dc8e89fb6a217be461c69090768227beb8ed28c0a3"}, - {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22c10cc517668d44b211717fd9775799ccec4124b9a7f7b3635fc5386e584992"}, - {file = "Pillow-10.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:dffe31a7f47b603318c609f378ebcd57f1554a3a6a8effbc59c3c69f804296de"}, - {file = "Pillow-10.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:9fb218c8a12e51d7ead2a7c9e101a04982237d4855716af2e9499306728fb485"}, - {file = "Pillow-10.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d35e3c8d9b1268cbf5d3670285feb3528f6680420eafe35cccc686b73c1e330f"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ed64f9ca2f0a95411e88a4efbd7a29e5ce2cea36072c53dd9d26d9c76f753b3"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b6eb5502f45a60a3f411c63187db83a3d3107887ad0d036c13ce836f8a36f1d"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c1fbe7621c167ecaa38ad29643d77a9ce7311583761abf7836e1510c580bf3dd"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cd25d2a9d2b36fcb318882481367956d2cf91329f6892fe5d385c346c0649629"}, - {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538"}, - {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d"}, - {file = "Pillow-10.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f"}, - {file = "Pillow-10.0.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883"}, - {file = "Pillow-10.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce543ed15570eedbb85df19b0a1a7314a9c8141a36ce089c0a894adbfccb4568"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:685ac03cc4ed5ebc15ad5c23bc555d68a87777586d970c2c3e216619a5476223"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d72e2ecc68a942e8cf9739619b7f408cc7b272b279b56b2c83c6123fcfa5cdff"}, - {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551"}, - {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5"}, - {file = "Pillow-10.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199"}, - {file = "Pillow-10.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3"}, - {file = "Pillow-10.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f07ea8d2f827d7d2a49ecf1639ec02d75ffd1b88dcc5b3a61bbb37a8759ad8d"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:040586f7d37b34547153fa383f7f9aed68b738992380ac911447bb78f2abe530"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f88a0b92277de8e3ca715a0d79d68dc82807457dae3ab8699c758f07c20b3c51"}, - {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c7cf14a27b0d6adfaebb3ae4153f1e516df54e47e42dcc073d7b3d76111a8d86"}, - {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3400aae60685b06bb96f99a21e1ada7bc7a413d5f49bce739828ecd9391bb8f7"}, - {file = "Pillow-10.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbc02381779d412145331789b40cc7b11fdf449e5d94f6bc0b080db0a56ea3f0"}, - {file = "Pillow-10.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9211e7ad69d7c9401cfc0e23d49b69ca65ddd898976d660a2fa5904e3d7a9baa"}, - {file = "Pillow-10.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:faaf07ea35355b01a35cb442dd950d8f1bb5b040a7787791a535de13db15ed90"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f72a021fbb792ce98306ffb0c348b3c9cb967dce0f12a49aa4c3d3fdefa967"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f7c16705f44e0504a3a2a14197c1f0b32a95731d251777dcb060aa83022cb2d"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:76edb0a1fa2b4745fb0c99fb9fb98f8b180a1bbceb8be49b087e0b21867e77d3"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:368ab3dfb5f49e312231b6f27b8820c823652b7cd29cfbd34090565a015e99ba"}, - {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:608bfdee0d57cf297d32bcbb3c728dc1da0907519d1784962c5f0c68bb93e5a3"}, - {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5c6e3df6bdd396749bafd45314871b3d0af81ff935b2d188385e970052091017"}, - {file = "Pillow-10.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:7be600823e4c8631b74e4a0d38384c73f680e6105a7d3c6824fcf226c178c7e6"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:92be919bbc9f7d09f7ae343c38f5bb21c973d2576c1d45600fce4b74bafa7ac0"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8182b523b2289f7c415f589118228d30ac8c355baa2f3194ced084dac2dbba"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:38250a349b6b390ee6047a62c086d3817ac69022c127f8a5dc058c31ccef17f3"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88af2003543cc40c80f6fca01411892ec52b11021b3dc22ec3bc9d5afd1c5334"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c189af0545965fa8d3b9613cfdb0cd37f9d71349e0f7750e1fd704648d475ed2"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7b031a6fc11365970e6a5686d7ba8c63e4c1cf1ea143811acbb524295eabed"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db24668940f82321e746773a4bc617bfac06ec831e5c88b643f91f122a785684"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:efe8c0681042536e0d06c11f48cebe759707c9e9abf880ee213541c5b46c5bf3"}, - {file = "Pillow-10.0.0.tar.gz", hash = "sha256:9c82b5b3e043c7af0d95792d0d20ccf68f61a1fec6b3530e718b688422727396"}, + {file = "Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a"}, + {file = "Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd"}, + {file = "Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1"}, + {file = "Pillow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08"}, + {file = "Pillow-10.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7"}, + {file = "Pillow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf"}, + {file = "Pillow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d"}, + {file = "Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d"}, ] [package.extras] @@ -483,23 +446,23 @@ files = [ [[package]] name = "pyinstaller" -version = "5.13.1" +version = "5.13.2" description = "PyInstaller bundles a Python application and all its dependencies into a single package." optional = false python-versions = "<3.13,>=3.7" files = [ - {file = "pyinstaller-5.13.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:3c9cfe6d5d2f392d5d47389f6d377a8f225db460cdd01048b5a3de1d99c24ebe"}, - {file = "pyinstaller-5.13.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:29341d2e86d5ce7df993e797ee96ef679041fc85376d31c35c7b714085a21299"}, - {file = "pyinstaller-5.13.1-py3-none-manylinux2014_i686.whl", hash = "sha256:ad6e31a8f35a463c6140e4cf979859197edc9831a1039253408b0fe5eec274dc"}, - {file = "pyinstaller-5.13.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:5d801db3ceee58d01337473ea897e96e4bb21421a169dd7cf8716754617ff7fc"}, - {file = "pyinstaller-5.13.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:2519db3edec87d8c33924c2c4b7e176d8c1bbd9ba892d77efb67281925e621d6"}, - {file = "pyinstaller-5.13.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e033218c8922f0342b6095fb444ecb3bc6747dfa58cac5eac2b985350f4b681e"}, - {file = "pyinstaller-5.13.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:086e68aa1e72f6aa13b9d170a395755e2b194b8ab410caeed02d16b432410c8c"}, - {file = "pyinstaller-5.13.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:aa609aca62edd8cdcf7740677a21525e6c23b5e9a8f821ec8a80c68947771b5d"}, - {file = "pyinstaller-5.13.1-py3-none-win32.whl", hash = "sha256:b8d4000af72bf72f8185d420cd0a0aee0961f03a5c3511dc3ff08cdaef0583de"}, - {file = "pyinstaller-5.13.1-py3-none-win_amd64.whl", hash = "sha256:b70ebc10811b30bbea4cf5b81fd1477db992c2614cf215edc987cda9c5468911"}, - {file = "pyinstaller-5.13.1-py3-none-win_arm64.whl", hash = "sha256:78d1601a11475b95dceff6eaf0c9cd74d93e3f47b5ce4ad63cd76e7a369d3d04"}, - {file = "pyinstaller-5.13.1.tar.gz", hash = "sha256:a2e7a1d76a7ac26f1db849d691a374f2048b0e204233028d25d79a90ecd1fec8"}, + {file = "pyinstaller-5.13.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:16cbd66b59a37f4ee59373a003608d15df180a0d9eb1a29ff3bfbfae64b23d0f"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8f6dd0e797ae7efdd79226f78f35eb6a4981db16c13325e962a83395c0ec7420"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_i686.whl", hash = "sha256:65133ed89467edb2862036b35d7c5ebd381670412e1e4361215e289c786dd4e6"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:7d51734423685ab2a4324ab2981d9781b203dcae42839161a9ee98bfeaabdade"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:2c2fe9c52cb4577a3ac39626b84cf16cf30c2792f785502661286184f162ae0d"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c63ef6133eefe36c4b2f4daf4cfea3d6412ece2ca218f77aaf967e52a95ac9b8"}, + {file = "pyinstaller-5.13.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:aadafb6f213549a5906829bb252e586e2cf72a7fbdb5731810695e6516f0ab30"}, + {file = "pyinstaller-5.13.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b2e1c7f5cceb5e9800927ddd51acf9cc78fbaa9e79e822c48b0ee52d9ce3c892"}, + {file = "pyinstaller-5.13.2-py3-none-win32.whl", hash = "sha256:421cd24f26144f19b66d3868b49ed673176765f92fa9f7914cd2158d25b6d17e"}, + {file = "pyinstaller-5.13.2-py3-none-win_amd64.whl", hash = "sha256:ddcc2b36052a70052479a9e5da1af067b4496f43686ca3cdda99f8367d0627e4"}, + {file = "pyinstaller-5.13.2-py3-none-win_arm64.whl", hash = "sha256:27cd64e7cc6b74c5b1066cbf47d75f940b71356166031deb9778a2579bb874c6"}, + {file = "pyinstaller-5.13.2.tar.gz", hash = "sha256:c8e5d3489c3a7cc5f8401c2d1f48a70e588f9967e391c3b06ddac1f685f8d5d2"}, ] [package.dependencies] @@ -516,13 +479,13 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2023.7" +version = "2023.8" description = "Community maintained hooks for PyInstaller" optional = false python-versions = ">=3.7" files = [ - {file = "pyinstaller-hooks-contrib-2023.7.tar.gz", hash = "sha256:0c436a4c3506020e34116a8a7ddfd854c1ad6ddca9a8cd84500bd6e69c9e68f9"}, - {file = "pyinstaller_hooks_contrib-2023.7-py2.py3-none-any.whl", hash = "sha256:3c10df14c0f71ab388dfbf1625375b087e7330d9444cbfd2b310ba027fa0cff0"}, + {file = "pyinstaller-hooks-contrib-2023.8.tar.gz", hash = "sha256:318ccc316fb2b8c0bbdff2456b444bf1ce0e94cb3948a0f4dd48f6fc33d41c01"}, + {file = "pyinstaller_hooks_contrib-2023.8-py2.py3-none-any.whl", hash = "sha256:d091a52fbeed71cde0359aa9ad66288521a8441cfba163d9446606c5136c72a8"}, ] [[package]] @@ -532,11 +495,16 @@ description = "Smartcard module for Python." optional = false python-versions = "*" files = [ + {file = "pyscard-2.0.7-cp310-cp310-win32.whl", hash = "sha256:06666a597e1293421fa90e0d4fc2418add447b10b7dc85f49b3cafc23480f046"}, {file = "pyscard-2.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a2266345bd387854298153264bff8b74f494581880a76e3e8679460c1b090fab"}, + {file = "pyscard-2.0.7-cp311-cp311-win32.whl", hash = "sha256:beacdcdc3d1516e195f7a38ec3966c5d4df7390c8f036cb41f6fef72bc5cc646"}, {file = "pyscard-2.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e37b697327e8dc4848c481428d1cbd10b7ae2ce037bc799e5b8bbd2fc3ab5ed"}, + {file = "pyscard-2.0.7-cp37-cp37m-win32.whl", hash = "sha256:a0c5edbedafba62c68160884f878d9f53996d7219a3fc11b1cea6bab59c7f34a"}, {file = "pyscard-2.0.7-cp37-cp37m-win_amd64.whl", hash = "sha256:f704ad40dc40306e1c0981941789518ab16aa1f84443b1d52ec0264884092b3b"}, + {file = "pyscard-2.0.7-cp38-cp38-win32.whl", hash = "sha256:59a466ab7ae20188dd197664b9ca1ea9524d115a5aa5b16b575a6b772cdcb73c"}, {file = "pyscard-2.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:da70aa5b7be5868b88cdb6d4a419d2791b6165beeb90cd01d2748033302a0f43"}, {file = "pyscard-2.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2d4bdc1f4e0e6c46e417ac1bc9d5990f7cfb24a080e890d453781405f7bd29dc"}, + {file = "pyscard-2.0.7-cp39-cp39-win32.whl", hash = "sha256:39e030c47878b37ae08038a917959357be6468da52e8b144e84ffc659f50e6e2"}, {file = "pyscard-2.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:5a5865675be294c8d91f22dc91e7d897c4138881e5295fb6b2cd821f7c0389d9"}, {file = "pyscard-2.0.7.tar.gz", hash = "sha256:278054525fa75fbe8b10460d87edcd03a70ad94d688b11345e4739987f85c1bf"}, ] @@ -547,13 +515,13 @@ pyro = ["Pyro"] [[package]] name = "pytest" -version = "7.4.0" +version = "7.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, ] [package.dependencies] @@ -618,19 +586,19 @@ jeepney = ">=0.6" [[package]] name = "setuptools" -version = "68.1.2" +version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.1.2-py3-none-any.whl", hash = "sha256:3d8083eed2d13afc9426f227b24fd1659489ec107c0e86cec2ffdde5c92e790b"}, - {file = "setuptools-68.1.2.tar.gz", hash = "sha256:3d4dfa6d95f1b101d695a6160a7626e15583af71a5f52176efa5d39a054d475d"}, + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5,<=7.1.2)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "tomli" @@ -683,29 +651,17 @@ version = "2.1.0" description = "Python bindings for the zxing-cpp barcode library" optional = false python-versions = ">=3.6" -files = [ - {file = "zxing-cpp-2.1.0.tar.gz", hash = "sha256:7a8a468b420bf391707431d5a0dd881cb41033ae15f87820d93d5707c7bc55bc"}, - {file = "zxing_cpp-2.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:26d27f61d627c06cc3e91b1ce816bd780c9227fd10b7ca961264f67bfb3bdf66"}, - {file = "zxing_cpp-2.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d9655c7d682ce252fe5c25f22c6fafe4c5ac493830fa8a2c062c85d061ce3b4"}, - {file = "zxing_cpp-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313bac052bd38bd2cedaa2610d880b3d62254dd6d8be01795559b73872c54ed0"}, - {file = "zxing_cpp-2.1.0-cp310-cp310-win32.whl", hash = "sha256:0a178683b66422ac01ae35f749d58c50b271f9ab18def1c286f5fc61bcf81fa7"}, - {file = "zxing_cpp-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:650d8f6731f11c04f4662a48f1efa9dc26c97bbdfa4f9b14b4683f43b7ccde4d"}, - {file = "zxing_cpp-2.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4163d72975191d40c879bc130d5e8aa1eef5d5e6bfe820d94b5c9a2cb10d664e"}, - {file = "zxing_cpp-2.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:843f72a1f2a8c397b4d92f757488b03d8597031e907442382d5662fd96b0fd21"}, - {file = "zxing_cpp-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66d01d40bacc7e5b40e9fa474dab64f2e75a091c6e7c9d4a6b539b5a724127e3"}, - {file = "zxing_cpp-2.1.0-cp311-cp311-win32.whl", hash = "sha256:8397ce7e1a7a92cd8f0045a4c64e4fcd97f4aaa51441d27bcb76eeda0a1917bc"}, - {file = "zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a54cd56c0898cb63a08517b7d630484690a9bad4da1e443aebe64b7077444d90"}, - {file = "zxing_cpp-2.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab8fff5791e1d858390e45325500f6a17d5d3b6ac0237ae84ceda6f5b7a3685a"}, - {file = "zxing_cpp-2.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba91ba2af0cc75c9e53bf95963f409c6fa26aa7df38469e2cdcb5b38a6c7c1c7"}, - {file = "zxing_cpp-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7ba898e4f5ee9cd426d4271ff8b26911e3346b1cb4262f06fdc917e42b7c123"}, - {file = "zxing_cpp-2.1.0-cp39-cp39-win32.whl", hash = "sha256:da081b763032b05326ddc53d3ad28a8b7603d662ccce2ff29fd204d587d3cac9"}, - {file = "zxing_cpp-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7245e551fc30e9708c0fd0f4d0d15f29c0b85075d20c18ddc53b87956a469544"}, -] +files = [] +develop = false -[package.dependencies] -numpy = "*" +[package.source] +type = "git" +url = "https://github.com/zxing-cpp/zxing-cpp.git" +reference = "18a722a" +resolved_reference = "18a722a443855063a00b27d03d33794fa573a61f" +subdirectory = "wrappers/python" [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "631e6fb7384478d8b248b77737a90cf1f11ba4b9a3b8215e08fca1157695d2d7" +content-hash = "957b95781926056890de7c0e97f0d1bc142533aa4021713801977c8135b27c14" diff --git a/helper/pyproject.toml b/helper/pyproject.toml index bf04eb54..ea03a9df 100644 --- a/helper/pyproject.toml +++ b/helper/pyproject.toml @@ -12,7 +12,7 @@ packages = [ python = "^3.8" yubikey-manager = "5.2.0" mss = "^9.0.1" -zxing-cpp = "^2.0.0" +zxing-cpp = {git = "https://github.com/zxing-cpp/zxing-cpp.git", rev="18a722a", subdirectory = "wrappers/python"} Pillow = "^10.0.0" [tool.poetry.dev-dependencies] From 48589b1b47f745333788e68768561f7ef7544704 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 18 Sep 2023 16:21:06 +0200 Subject: [PATCH 149/158] Build universal2 if Python running is universal. --- .github/workflows/macos.yml | 5 ----- build-helper.sh | 3 +++ helper/authenticator-helper.spec | 11 ++++++++++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 7766f79c..37059331 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -6,11 +6,6 @@ jobs: build: runs-on: macos-latest - env: - MACOSX_DEPLOYMENT_TARGET: "10.15" - CFLAGS: -arch x86_64 -arch arm64 - ARCHFLAGS: -arch x86_64 -arch arm64 - CMAKE_OSX_ARCHITECTURES: arm64;x86_64 steps: - uses: actions/checkout@v3 diff --git a/build-helper.sh b/build-helper.sh index ab894d4d..5ddad4d0 100755 --- a/build-helper.sh +++ b/build-helper.sh @@ -27,6 +27,9 @@ if [ "$OS" = "macos" ]; then echo "Using Python: $PYTHON" if [ $(lipo -archs $PYTHON | grep -c 'x86_64 arm64') -ne 0 ]; then echo "Fixing single-arch dependencies..." + export MACOSX_DEPLOYMENT_TARGET="10.15" + export CFLAGS="-arch x86_64 -arch arm64" + export ARCHFLAGS="-arch x86_64 -arch arm64" HELPER="../$OUTPUT/helper" rm -rf $HELPER mkdir -p $HELPER diff --git a/helper/authenticator-helper.spec b/helper/authenticator-helper.spec index 5d714ae0..fb2aa9fa 100755 --- a/helper/authenticator-helper.spec +++ b/helper/authenticator-helper.spec @@ -1,4 +1,6 @@ # -*- mode: python ; coding: utf-8 -*- +import sys +import subprocess block_cipher = None @@ -21,6 +23,13 @@ a = Analysis( ) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) +target_arch = None +# MacOS: If the running Python process is "universal", build a univeral2 binary. +if sys.platform == "darwin": + r = subprocess.run(['lipo', '-archs', sys.executable], capture_output=True).stdout + if b"x86_64" in r and b"arm64" in r: + target_arch = "universal2" + exe = EXE( pyz, a.scripts, @@ -36,7 +45,7 @@ exe = EXE( manifest="authenticator-helper.exe.manifest", version="version_info.txt", disable_windowed_traceback=False, - target_arch="universal2", + target_arch=target_arch, codesign_identity=None, entitlements_file=None, ) From 9228b3f28edd8debeb3fea8dbed8c3e61c5aaa92 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 19 Sep 2023 10:24:31 +0200 Subject: [PATCH 150/158] Shorten paths for Windows limitation. --- .github/workflows/windows.yml | 6 +++++- helper/poetry.lock | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 06d7f737..bd016fb9 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -42,7 +42,11 @@ jobs: - name: Build the Helper if: steps.cache-helper.outputs.cache-hit != 'true' - run: .\build-helper.bat + run: | + # Needed for zxing-cpp build + $env:TMPDIR = "C:\e" + poetry config virtualenvs.path C:\e + .\build-helper.bat - uses: subosito/flutter-action@v2 with: diff --git a/helper/poetry.lock b/helper/poetry.lock index e5a531f6..5bd09223 100755 --- a/helper/poetry.lock +++ b/helper/poetry.lock @@ -632,17 +632,17 @@ pywin32 = {version = ">=223", markers = "sys_platform == \"win32\""} [[package]] name = "zipp" -version = "3.16.2" +version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, - {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [[package]] From d5a7543fd14e973b3e0f42c85f6e171e2530f364 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Tue, 19 Sep 2023 14:17:50 +0200 Subject: [PATCH 151/158] Use precompiled wheel for zxing on Windows. --- .github/workflows/windows.yml | 6 +----- helper/poetry.lock | 16 +++++++++++++++- helper/pyproject.toml | 5 ++++- .../zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl | Bin 0 -> 677185 bytes 4 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 helper/zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index bd016fb9..06d7f737 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -42,11 +42,7 @@ jobs: - name: Build the Helper if: steps.cache-helper.outputs.cache-hit != 'true' - run: | - # Needed for zxing-cpp build - $env:TMPDIR = "C:\e" - poetry config virtualenvs.path C:\e - .\build-helper.bat + run: .\build-helper.bat - uses: subosito/flutter-action@v2 with: diff --git a/helper/poetry.lock b/helper/poetry.lock index 5bd09223..65a884c8 100755 --- a/helper/poetry.lock +++ b/helper/poetry.lock @@ -645,6 +645,20 @@ files = [ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +[[package]] +name = "zxing-cpp" +version = "2.1.0" +description = "Python bindings for the zxing-cpp barcode library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ced97aac933484556fcaa40fb402459b7af801dace4d87f4ac515661d267b211"}, +] + +[package.source] +type = "file" +url = "zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl" + [[package]] name = "zxing-cpp" version = "2.1.0" @@ -664,4 +678,4 @@ subdirectory = "wrappers/python" [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "957b95781926056890de7c0e97f0d1bc142533aa4021713801977c8135b27c14" +content-hash = "cc024d1775a438623aad52e1c9bf693808fbbf6b0a2c5001bde7964dbd87bba7" diff --git a/helper/pyproject.toml b/helper/pyproject.toml index ea03a9df..d5a224fa 100644 --- a/helper/pyproject.toml +++ b/helper/pyproject.toml @@ -12,7 +12,10 @@ packages = [ python = "^3.8" yubikey-manager = "5.2.0" mss = "^9.0.1" -zxing-cpp = {git = "https://github.com/zxing-cpp/zxing-cpp.git", rev="18a722a", subdirectory = "wrappers/python"} +zxing-cpp = [ + {git = "https://github.com/zxing-cpp/zxing-cpp.git", rev="18a722a", subdirectory = "wrappers/python", markers = "sys_platform != 'win32'"}, + {path = "zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl", markers = "sys_platform == 'win32'"} +] Pillow = "^10.0.0" [tool.poetry.dev-dependencies] diff --git a/helper/zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl b/helper/zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl new file mode 100644 index 0000000000000000000000000000000000000000..5bcad06d3518126fbb2aa1fc343299e481379182 GIT binary patch literal 677185 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

aH(J^R$NgqYA>Stv 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%0Ar4$0SYESrhEsA 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

YFY@~E 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

-|`66OLN!)3i>arM}4aYxMe1P+H6G& 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�^j4sg!t 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ℜOITlr^+s z1)R_64Fo;3;!QZ5J5R`|a8uetX@!uZ70{tK1Xm0B)h4)F%&+c-t7ZIZD_pJOS24}d za(>l;6uW|^e!4e{=11^ZnG9Jk7Qj!7){a?T+J8er*M9urtCbl>Vudh}Ly`lZAIKre zfzJ=*kmSJU2XaVq;PV4HBsuW;fgF+?%+E!TU{qBWP8tNo!(K$(7y+Z>WcDnsmBPvj z9E(aT%sArs=_Bj_JF)xpF`00+jbF`ytHkKj$H2ViTt0mS%xli#)5pNPCjO3Fu-JW> zPuMXf#zv&95$jb}$7iXl0aY7f!K#zrWMSoyPTB`(Fyx*FviZ|^OaK0(Xp|8H4hG0^ zrs(_8+5mI_G%424<(fT7veoF4l#BfV{zIJs=0iOJ&O_Y*wnKdYp2Iu9_lBthOHCae zc*S%=2Z#|EU?UpxWK1u&0-Om6&K)}ECov>&n zfX~*;!SQ8NszkQNSu2%C1y56Oe3e3VTHsU`JPoQgD5%tY4T@z2c6fN%upFTCi59dZ z`jb7Ni$#$0i>{BznhOg?C~?`jd@Gt38|o`UL~#zl)B6pGI7hKVNPnxD4yR@b>96T@ zSePrMzt~2H6Wv1kvvw-#QBWYHKb=X3av`qtRw6hz?iE$!Q^L41#4MWlo}gTajDiZ` ztc=c-5;=H7i5k@07|;}~kga@uwj!-QPl;oKFHWR}Xk3Ta+nna%Li!x19sc^9neg{P zXAbvMkVbqNty8#yd^cZf=_^D4{^fkhC4k2OXQ`hW^<2b zPYuMr0-|auaqQiQiCS99hX*~i{Ur8nLW!-pa4@&Q*&w<$c?5NzYajfH)PWmFM)J9m4!m-yyv3Tr>)P`UFo(LshB*YuvZjb+*SDTTNw{AB?2uq?R93 zjo_zZFWW3)7XAH(vt7P~t`WoS@=%MvuUL($mH01n%U8tg(eJny)-GY;KRIhRw*^90 zSA($R1GqUqN=Y~uzG3FQ%lEQczP;Da!n3<9rEi}_Ng#Ly6gkjt6BhVU_Of8M#s}cY z8g|wA(p>EVh~_4fvoV;;t>Vk|W$jo@x|P3=Zlsn6!gv4aP&-zFRkNq+gYvN~b%V?b z94d2X8s4RWHZ!0PI4t+ytBK{EHctuL92c1)7s|2SPgUzPa1IvPTM0Tev3Y%6CYqia zC_dYhE%N|v74r%|>X5@?CYd(OCPf>YkK&B1SO2j+YD7^@o`F*ZWgIkGT}ehO%ARo2 zQGK#Mj;gter=%-O$Y_T_rYC>@CVIbSi3Q8o<-tR_pvztF;p7A=|j%dG}j3k#7{$P1oh;tMTf*WwftNBh^k48zR^{nqWr$ek1O z+~$3#_h#G{(qJZA2+|(KCKwx{v|p{peBHX{OKJjYMWc#Z^qtDS(C78W6{yXNnIoDg zjbR_q)L@l87Mol^xChA(7c?%P)L|A@UAnL^llBTf<^=$@{u4KA;El1Gjf*4N0P+u) zxjA_4^AY=!F8<-`pm_GUmL-S?9rkT%ZD~f1;Z)lL2U>2!`)0Rg+3&J`YRu@=uWp z;5w*QXmu+ITx*^?iL%e%V^LkLrRZ0TVCa{ePr+19wC&9yxnX3MD6CGiv1Gt$Q3ED6 z5mu}6@!`R=RiL{n(fVke5?Et#onM+lpNPD!vU`d>t_QOVeAtIR2KOmZDIQiE-$a>t z(t-H{tpI;loCC6P0}fLnvdLuxpX%JDz$I_R4&S{A`8P%v*`7g%aw|dB07IPxV+(Zd zTwk$HSRABfPg**h^{bML)}dWr)zT_OrW}=6ipWCFTCJa*)rwA#>zV{_lNh|=s1mL9 zy0*@E0oKq;l9w|7ZHDQx6q!Fr_|hvqZwzI1H`{whGw#U%W#sZ5k!rn>#kZZ7q8*qT z;H5OBO5Drt)Nr^98|k#&jcg<>gRGp5`YgsbothEK8=x`$^tZ28_d}la`wj@3X}bFL zh*iRfB#@S{_3J6I?ddmPNX>8hL&wH>A@YJ^N(uz+f`}p6Prun-)a{4>IHE)N6oTgo6TR<%T zOk|B7Elr|Sa;&b3)k)<$e0Gq2g=a&NWDz9UWg!qBuVYf7D7c~a5e|e0;24Pob!U1EXuKdmT5D8H<1yT!IO`Nl ze5bK|qs7YD2D#oC&;8r(AhI&$oaQbI9X7&ZMb<&~aD2u;f)T zvJ?kG7FVmV)vaPHZ1g0K&CFaEim=X=V+d^e60+GRWi=5qsQ`Ct zZvcqRS;Jxdg7wuVf;LsOp0BR`(uJGLDDwaMZ*}GX^eg+9! z<8T4(m(6#Zr2-7zdt_rY3dljI@<8+*2@fO_IJk6%$ZjxACVM!pkaAI4HKyLW!^~HE zT2Par1F^+^l6)Mq%5Bv9o@FtyzF29~*+#S0<7)Q^-Zt`VYSw?uK+!=cA3+-uBeOuE z2E71<2R8{1OBE0t#N!eye-}&v4tkEF^^=vNFg+oxwg978ipL`K9@g&UU|O{t4JX16 zL$*MnbTuGx86F|6r_b8!`yekKQjV#srQN2u@fj9TtjK6E_v%w;?tW@;PavKkgK~g? zcx~#MODSV;AcfZ}x$_F**Xv(O<>wpz2*atnoo)N^~A?va`8B2PqCU?pwBySER`$<@1BRA*L~I;wV3of7Soc`=X!iMwXD0Ws~z>0JH;ac zi{2-(-EtAe)Ue1XZP1mHRvHzZKbG9U%+ZibpQp%0AOQC(10jvVC9F3%oWo0SZzT~-7f6LAjErT08oR6w`HNih z4Ya#SAGHSkxb2BeWumx{#n2p3(*n&M6r`G#pWxk!y(ME=3PCuMAcS@$P4vy+c1xcS4(q;2q@Pp%s0smPQJWlo#?^H!9GGGQVZHp~#5NT_w}#d(HFi;qk8f zWLAC|b%Q;189C9qYJC|-zE!-C zo_aNODBFs6HYfmz$XZ3`KBXw6U`6K!71!a)Gy>)iumoR#0q^I4P^^cc#g%?flTfSxa5pPbOI02r)3!UM212LMUD((Umh?fPmZzdV|uOA zgkuZtK3q4aL!ZmxK|lGJf0uAezBwc8h8+j7i=PR)V&PZ(L5^VM*C6FHYQd)anf`*H91oiv|7xmpaIv>#%B>Ni+yl3G~z)xKlR;b(sB=D11C?O z5g(bt*vL#pTA$4=pa8#$qf&CZ9T<&U5wAYhF)CAKo;iZXV`^+XieuwZk}w|s(O7K8 zy_4L>x*rSW*Ng>b>CksRN-Y}MaIq?omuU7{Xz{d3)`cxDdX$@b8e)K*Hb*&vT9Wi= z0!Im!HdFp@qs6w(Z$*3arn;?zeNg-UFB zESfD(x8Xly!_#KbC{!AwP({t6ib;mAd;^IcV{sDaoo5Yyud@>1%^@}!Pui^V(oUxd zEH6SeT|#<7od zCQ|AJG%jP~GN;zlZT#_A&ti_Ok31+=5@&)q;=3 z4yyDT6Sv`9^Kc5^;4Qw8Y&hGVO6BL<%kgV}HYFxZ=rNzeO>g|y^EnK8xQBcWFCUKQ znF45XWUWuqoL(k<)}#~CPkrcNn^o<+mBlG0asHRls1>yQx_9=Znqu|65w6X;G_+rt zS4%4^JyQq*5@ApD;G zb#g#A`+q`jfWTqL*x_}*?^}=9@i=QF zSDhqyHb|}wK8*Np!K$#bc2kzfixs%GGYXVqb*eC`ah6JHPq$2|Iv`9p8HL#z;Mk36 zp|jwmc8V~)$?K}|@5DPXe<*w6iH)DkU5ECnGYopL+Mxj(8moRbVtCy6ry$U778d`J zYn-&Zeu)+Sjj;g`@VohdaC*0@&N$d2CY&_$jXH?^vex05v zFrLYzXT}-N4CwUCM7lRgW*NU-pGt8>m^(OQ{#`JqF^g=NrzeV!Cp03n#)R?JY=3|k zJT`c3l4lY8y#>BGu|c?W{mfqI0TyVt&)F_qy7`e5i4DjVayDbORG6+=%KWxPPME*1 ziRiq+GaleB%IN!eUfe2LnQt?44(z$3!@r2vACoXu)^=>^T`E83+OD#7B5ROXlPHX8 z1|pj^nvXIcnuObSh^!4*&$|>SBBYyz zoUMTBj4M-n;i>4Tl2|_mPlr9$Ey8UfOfw7Pa7!JP47+^|V6IlUv}VScVl)KE6WJCq z&}J9P&m)&yTTmMjH5v$=UIU+*c>~7Vb*K0CU(K=4n}6NsC&wGG{JI7P#2c{wx&|;N zk2hfZbq)ME-ayi+H6V0qz&9CR_G2QFf=fymWs$>K20hj*JXX^T3;rM*O%GN-STd1Fcymn z?o$cVXrCPPuAof(K*DRUJ35Su-n8>L&X_vhh`a*-u!;*Hu zF&cJ-RY@Pg;S{V>BdqMR9*!&3?2+iwHA<`&%{-Vf--ymLgZvOo>BWN?ORa|Pg zU=j4^#Gt&aH~ z_4(LNQmV9E9x5#rDll_g*RSuNllb+_E?@sBCI0noDaP09-I*q_dQ3Mcnv!gw=<_e* zD7x|A$Ed2@QYtevIt9ma{41R*%LxkxIW2EQS8)ue6*NK5KrfP6_f zKzo0*U^bRUnbl9B{cT#0vJ}@E|Icxk0IJemS_a*p(@{{KyaDdT*5^kF@=Imy`riS< zgvTeKJ^tmpaf&zawAz8t@8rPl-EvTFiwMz-SwC91Q`UmJc{SBySsMUjG`S4ugu4zT zKg!(~ONc~>Sdb?bQ(B*KWunEs|fP{ z*?3M=qOiq*7=C4$1AHqr@$d~A77!$Jm$g5Uh`cvn%qVt%3ZrE3<*gJPHIYbL9EtO_q;F2aqB}@Rx^FwPl+V~w!F^eb`?~OZR zU3(HotnST5`%qmD0%Gyg>0Y*$+?6-z|2NA*AuEA;P?sdMMq~mvhLIL3-lRMk{uO+t zB2R{{6jOlp0NjsT>HR}xB~M30(`D4iqfxqht#Xvln_{BD!DK}kU)F^NlPo>Z;P(^+ ze&1e0BhNRK`(NdKcHv%}AWsxoOop7CkNI)Y{VcYkxGR0x`C|1QU3dZpa8EJE_}}Nm z6A&5L(Xsz`tfPMjT9>}{2+h;qBQ(~cj_FzqBwE(NVqQyRF_VzROfm9>pC+Su?k=1t zGh5+b8>W%{PZ`aQ2tuyhmB45ajU9}}ZSI!QWSe^!q&*nejNLXf9)AmpEk z2)PXr^6T)iv4`LpRXDgUPWb$ec`%+~sJiEamEZ$oW zgqb3TC4tWo{~=ksX6G@xDR3Ax%v21mTUokFSUGtWjmJB^NsZd5AMlznz^HmvVeggV zp&un}$k}4e@g$LL0zKMe-6sZ)4-ytWjr$Zu^{SS^pvjTK$~4^HNdcS*vTOZJ7#h(G zz@^BVbO=KPWMPDK$WxhSulE2Sjr0fcEKE@hFgp+FOTK9bK`FxxO^FZG{~n#H0#W!% zU;0Y;w3|Nzny>3^zfDj-D>GEB_}Nx$%b#egJ^Ufl|RC3QyL+ zyX(t_U3QWGK}B;%WmoG=6`PgH*U5rZTQ(=xe3AcZSsQD{UHb+5>Fe>^AcLpW+w7*u z@36kW3snT#{Udp6FN$8O=X#qbzILhm$N zzI%*20*^jo$NufodpVDx8S#y|sr>xOVw66#jDh@tu&!T14?Mvi2~MqL7$HRJ_Vl`2yX**D&pj5)PB$%oBVs zyUnF>rEzLGZaB_mtr=#ngz8EiTZvJBVwTy_rVRkb~9ds*FpbF z(k7ZCqeb8{zSvVw!c)S^t-#TPYq8Nln^h=32EtpQElDW!iU)m! z@*@P@$3S%Mx{i-}pgFmyEf6sa<#q4`9lSsX7NHzV5eJUggo>h6lj)7KIMO#?cf_c> z>V#9xcAZ)jQmr-IH^_-TMy}8!SsfBFoC*scM5`tLs9Rrs@v$f-o!(b(`K(U z9Ghg>H7>irmtJY%XE3FJaAeF2M-m(Y@Ii=&Z^@OIRF&b3PvG+HVYMB*kdH|`V{o5)OHrwVj1w$c2>+m1>t!Pwjcs z8$K{b9m|6erj3y|ki4Pg=FTJk47~9F(e^IzQB~Le_`H$~k8mc+BPx}&jT#J^dMPU0X@ zKe{n)@Xje{NO{LSlEE4*cJ)RNSjh$le-{!#`N~w!(!l}U9n@NufEEb7 znpXw3v>^#O5`-8#X)+UlTTuS5Jg_ZB5}?8SR^*iaRA(9j@BQYjYbm?7^J-8 zvgMNT6R$Xx*8PZDR9^F|J~|T}BR(E?wp-u)_@C#Z;=vVXyP@$3AJAv*p_K{cN7qCq zr&muX3yp|-zK@=lAHB8as5_Fb*IflFJ)v5v$RQA3_H@4WXMSr`%~8*w7I}+ZSXbtn zf4}8ywDpf?qNw^u*{I*%`pYxjuzM+eNs9=mcii21bT?W}_g%Q(1&t5T607{H^m~a= zR#ZQFQ@hBY6y3G$by~;T->><^DZN-*eZ=82TpqEV~|Q zZhICIQ=VCP`0`>xhP4y8+{nGEPHf2@V#?Z(YpgAjtd?IGzHK;)kn-A8;|_c0hrhg^ zcuc)5meRJfDq8;s^yLfh3klO#JeuL~N}3%P)AAj`yQn!eS0C_sv** z_j|zNrr9KEcw)hr#i|^O$1J66;H9~1Yr!wAu8%Fz#j3P=PiqK~{B@BRCUPzEy$Y|A zUoqPOOXXC%NP3Co+~N~B&jsg9zQ?kA#hD6Xkc+cibO%QVBX))3@M%w_3qcUpv-HMd z_I|_JBHl~JSs}hst*`Q}uMl?wCPegz;~u@}OD|c!HvJy!JVfXB+O51OYdO0*PzLHj zj5Y`M5h%0RZehTByG?8z(K(|12#2S&>6@%?pSQl<()BGK*^22NJECwuSBrj4e&wD4 zi0-u^9ZY?_xX;BvEMtDk6c0Y(GNd(Y(>X$q@l%GQh0bioRm4T%n0-24&FvjPJu{n2n<*@PTh=u{Ax*hP4=51bZ+v`jc! zd6wo_M0D}Rq2C9GfPVS6cSpa)JwWrfp3_NH=PqLO^da3yw%c+ zoM;_yqou{SvD)dWbko+OnC6RHb^e=+8O6mg(XQl3oxJ%7t{=k6>j4M5wK%&)9)(-W z{^M?2=it3qNRsC^4q4cxsNVazcmwfwOy91v5JAo_8Q67iXOY8ZdKaCJnl~*%hYed8 zU*^AXnIpTGS!$J8$?+_9a+!ay%H(^?QRcCQT-@f*N;2ExRSw>1X9v;VDF7j4aK(U<@q< z`EYWa_~$1;l@ba`#ElTXDulQ8Yioieb9!!w$bG2iX+>GDvu=tpivpKnw2vx-#JA?tNMFl9d7 zkNDs<%+tP%E>)c8ppwrI6g3B(1@3_JEv4ea5{W*@tdBO(xiqsa;?Pe(qkhTiK}3OE za%N4mA(ABPHT0ksX>Yvss(v_7)FcJk)S5_|UMIJ=P&IUTN735|fRiFqy<2tGD9$gb zqq!1a=3zOyi%x08&Qe)tP|1+h0}(SibANOv-P-Rx;M3oe0%yFR`=r3T*@yJ~KD{nl zt2o25cmEoRL|TWxs39-Ml~U3g8Q}B2Cska6&38SVc|xkHRW}8k%}6M9p3uR2+@~M* z=|`yq6ei;DMYk)?dc}K`%d(5I29%g6>#$T&&oXzpRD~ax}ROmDW2`t z^jH}%9!!6%*s|e~iNlQ+J8Q9zy2{9l%FgZ5)&}UsSM!EQW~f_xKRM9xbo|p zHrC$Nw9HSqacr_yta;GL)L0`s3*_?ZW94-=uhnWEM{L&Xi)F3Ah9b|1LK{%nE=Ds# zSAY3u4z-+Wad3nnAaMCN z-sYvUv~{wBo+RRKVlmwrD`gxO{VZ=|I%y0mX?5E~5PW3dMKAv=(q2gwe?uKbGu&`E z%Y0om#!zMNF1x?eUTCXRHI}(V#lySRo71b_t9^9q0@apCwROTEsx1=LJ~lO^SP2kG zO#W)=v*{HwnT7CIxl&4!lrqy3 zsqeH;Bw_P1DI+3n&2zv=%$b)M(BGD}4kYlVJLqRtVzG0Yi~il1j(O-3N$KN5 z#Tb!d>ER>P64wc-@NK&JA&f=n+D+FVAP+r;FO4pa(1QkPYYnp2;~lW?bo+h0Q)7Bg zNDGdj;96XtkW$jq>0t|A6gL*o`PifMU|za=xm(KEEoIb25~Yk?QpWZZ(%gi$aY@9C zo}!l;q;;=w|9wuA;z}4SQLzmtr8{?^YI?VhUaFDS!9eYVJB zJVf`0O}YSsi^3gjiyxSLi#--?yW2gj>#7>jcnqUAs>ph4$>7 zRApedXT?>~T7P++qx?+5Dk`q<9fFSDdiR8MXB~zz4QWm8qSN}gtM2QWn_V*tdgnL+ z6*wVFW9<6nuz9?9O;9%j`e{`wU7>0P$MyDr5o$W2NMrnNO=&0oTvMW8mJehn*?d~? zEUbB(;9qGJyoMOR3e?HR@fNWz)HH?G#O=kJ5{3cFYvyJW9LLE!>A4VmI73YV=mcM!*wJF(+y=%LncWnt&+sc(z zO~+Bw7Q^*RZ^xQ@sQ;CASOPak>K>rJhdf-%{NWX05eIDJJ_nBk*M`SAv?{!>8SKjBy)t39~9y2V9w44@u z%f&?`Bz-P{%bk^&_W>d?g{t!;ja8Bu)R`V@NT;S*13D9l(6Vp`nb0txo|*Yby_Fw; zhcD7u?&>wFM+zVg%_zhx)~LQ-z@wUExiVR)pJM~jyTcyXSNOGQpT}7_7ANL$+Gqa* zyQw9+MAqlQSJKfVlBxE{`uy*^ASnFKB>Bp`@Kq1=>6!moMpjPjOmjf^(AAY9-!tRn zUgqC+y}X2Zlq<}?%U$`NyXAR~w@)MrG7yVVGtr)Xv?A$h_Vpau~FsFp*afb3*f z=ePz+8>=G;EODyF9iGaHLnSWn+0v!BKtP-OqhXxFAiv2cRSlG?rn!918E)r{!3tIH zRdk2qZ79vA^RcpbnyYj)y+y-3&7(MLf<+BMJ&)(4w{iKS*^QHP2D_z=H3()HNe~(K zYFH#X6na9R=F-Dbl|!m(vQKemSKCp;p<>AwCeAjon2>Nd0hd7BS!N3@A&wx-{60e} z73AFDJY$?@mJ`nQ$TeF$D<)=k#m8XE!-Pk4er`}dt>{OPa_kE&#iz+Go#Qm9MJ+Jp zCdQZkJ@lW7nOqMfic8qrBCWep5Qw`}V}g~iR~WKQ=rm+=uz%`Ak+Anl)li8LyibHc z?(l0fkRxflUz?H|B*GfGF*@uw4cIoWh?ZOi)3)a(BB?b|WoNkaB|wAR);ZwC0|?`w zw}CRquw!J}0BTM`zcJHPYg(OtV`b_|_4l0dq|oeFAGJQbgnTocTsf_p}i?VQ@R zzi2-%d$)(a!)7M);Fa`e!ReQ8&lO#~Z~=5sGd%-rLg$ds)($<%!*>s~ZafUNmO1GJ z&0n+S!}hNHIOR3&)^`0|SN;b-`~ME)Y1*1H$zki}P!gZ7^s@Lo@0o%>>74IP=KE*6 z&-Wb@ z@8#3k-dwea(DB6%>eWr}xP{Nm0PHIfa6@a0EiPQG`2>u6Lh?z6uyjtt6V8+;*xSN{ z6DO=k8Rkz%+QClKLy}jCRPCqUb1%JD);+QE=w|Km*(bmyao7 zmRx;88dF36JEf{-zn<%H?&Sy$`EEgNmZz0X7-%a=hSQc6jdh_2%z8t$xG;Cjv?i#? z|3MwHm}cn`nxrd5I#SMtqtA6Cd#djD`1R91X>@Kwpy&iQj^tGIMLykBw{Z?*>Kr|_ zc(U}c>xEd$JHD`5o{vVj{uH}^BzC{hEWH9zWJvPTchr;oZkMW@ymtoL0E>TP4R|cci>fCC zHpL9j+BvOz!PD#pK*Fq&jZ~c(gBUpFwa#>Gg(OatldQ518vnol@*D@_&*EGuG}5wO ztp{K040~snemaaE=>_YI$D*>c17^OuQf~Yx>TE+^teE19jy@cQ* zi!%W#s-`J96CAWsTB~G5V`wZ1S!`qFBSK>YpEVz!g?-nM1R|mwBf0B%>)kv_l;;9^ z!Lzd7p4~{m&=V5UjNmcuET8_+3;O&jk~Nr~J#y$%{#pYw=MyZ1SJ@K4#*N=203t>b*N z#hL>gMLNI(N6|(!+5s^=mvHC{;`d7ipd(vhX+sDGrb0)IUw!`5NJe55e1R@Chy3k) z6lOykbO*Mv9&VQI+ILd&{NT#;BobB^yeVb84W-wL&m43)mqh8p!+yPqNcfJo+&1`U zmC}=L@pGlEzT{)9cDbuj+UiPS)y!SQOU@;F47J2=qQcU;E7%0O2En2SuG)poz3ZAE zOO)0+p$nF_QWeTE9^Ya%z;`gj-r7)`w6%tcY?JOfMeuz^r_frW`~~&VVNx}mrmGG0 ztMb!{(mL#$8fvuD1#s?LQpO%SG||kE=6pssO=?lAltG{s?vU#V59h7Ehn=ytLwdr+cgDUWJ${DwJ|CVOTUhF(Cyrs71=90r`pwo# zf2xIk4tEZ?>1e^HHRYEyK2HA)q_oJZhVQbVm^Fvmb>+@LZ%saqr=vEkauLOg@~Rq#bb%(DO0li;Y46WP+RS$ ze*$22Dv6b8+g#jNG5^Le?>MUlu}!e{Ki<0je^g+<;u(#NVx4GkiFG0u>jbSO@e9SDT_P2Zsu+(%6{m^R z0zTgVaMmCIv`;Iw5z%pl6*n&#%3vnj+Cv_?J2b4Mf_8YM?nx>?Ol??If7$Jy- zKpbvd)JkN7xQXPZQ6MLtiDT)8g2g`0zm+D%5>+1p>b~-%w0a7Op>HW=+wLeRC_n^J z{e)ce`6!?B1kv&d`UnygZ|V{iGrdz?rB@SOcZ$H$b3_7pi>y!RP1b3>xvJYbL_Cgp zI=~CjU|xuh&_ZNJhAYMtJJFmY;*F#)h*#LEm{7+Zfda3M-s%v9^bCjh-tk}OiO!y4 zT8hLRFH-zMEr(6qY3q35Rd{brX6EKh4biZJ_BS;nOvY zcKrGmaLIfg&s9&r$lXN5-=uf20JPvygE@+gi-MdvfFzk+G)oFN5^0(=8XIXD9g=4V zy-FxZkM8i`ZW>=_nMYZUX=71pY*wGhi{&8l3y0$%G+FFI^Sev-cPC-2jM zmX#(&8_OHdMQhlCj9^;f&a5#8duZ9wq|91vswYC6et0UfF4%Z8(T)A4B|IiJA#%lS zA!1)~QCC!gKKh@g0&zP5ZBbgh)K$$Q>5>*&M&JI|*fM$zw$_e*00anKf&R}>aU#vT z78gt0+319&ZprgK{Z5aRv4hsz{9ti958OPo5`Vb(PG)2sGrU3s5m6~&O(=m0OI1kuul*3dmok1d-U4j z%$AC%82e9mx*@YGBTyQxd84E` zr$DE-Lc^jduW>37i#1JCc*zW7hwVUEX~GtldBq2`R$f>Cn4{z&lp`VgSyF$({@%zS z2>Wv-Fl?noZMU;Avw_ZE%OuQwxW?L`O*qfs&L!-GfBYjO zlHe9|4JLfi)ndX|3*;g>epv>|@>xN>D2FG%HQVRc^?_fhs+RBRmA`fIHo`DZPqr*4X>0=GlV02< zZ)3{_gT+Z&w4ZyA#?%w6L@1M7tnESxrAn(wc!)}C$FZMlaI@+?Ej@k>a=#Wmv}u|# z^byrt@VvBc815KrTp|so`HSD5CvIw{!&xJ3m7FZj(QuxkK~C!2#+Dgj3fnLnwty+P zK)10ITG&CiO1A}dbd*j#<~>!iI6i(#Mmk<8}X@=_|wj|nX`n?^hG_@W&@qbZ~h-jW# zexyx=MnU`=YSnqfMVacM6OM_ZmWPq}!$Wd9VxydokBxFZJ~m3%g+pAdZlu~uhIh5M z*eOwI4!Rrq5<&Hg5g-dO0KOl_%%A~VByCN&k}hCMv6#eXV{op>8567orYs@`p^zDm zlq1n-ZceDB^aBm+nrDc)-;m|F(fk~U#LDByR6oy&PSs@XK1m7&n4+O0RlwY;6)lU zUAP7>5$UrszfkvHP@Sikhq(~rVe0$hVSccMJ?Y;vH(d>{((RxCp1zm-o|(ep=D#)f zXLU>7>=scywu$-wJk5VdY`N>{XZ=cKvyettE*8g4h2pqb702gu#Boc8IBrYjt-QBY z5ebCW4>-CJ-!?Fy=ee=x_-f}~hF;$*F&<4(Lzwo?a+`mOoR7k|JPkh?GRrI8bSs<) zu|v7ER_1z&!?~$;J4=lql)v8LwwY&hcv(`)aOZcp(`wyWZ^h7t_}gl@lNT%85iz0b?A=IOFX4bdOJ8 z?J-;feBKV}r*$qtB7f{Yp-7&G-8la~PKOg7d&p?3| z)DgR69=aJaYeqZ0Gw&<03}OE1Ua z_|i*>!}aU$1+Z~ArBH2Lcy80DKbhzzQ zWH26@`)_gx4zdvsUl$MG<%c`W7x`g}ltD~s;VvSOUr6h&MB2%WkLkw8tOZ+_%s2Ma zjkk!T%FZbs=VXtwx>L4Yknm6)iMw}#ok#ZRaq~X-^XxCokz)x|Rl`Nrqn0ghq;pDw^D#)$??n)?A=IB}5(yDnN~ zjv$Q;vICFZtx8kgqW_crpPYxDXz9acYc8{uBtk0@xn0$hu)AL}T4ghaCR~IrQN8X^ zGH1BUb@SuT%O1VVrb8jTY1`%nBomK|=QTdQGB zkx0|{2rzw$hj9K9ApSIu%G+(4vMr$9Tm2Gt5KT`SB0qzTBPzcngF2365ldp2SoR^$fzcnA>nO zOFor?JfabTx57$;9ml_Z59WWPne&&sdFYQn&ekmX1FM-0(7pkLBd@_g3@H z6*LfwtgOS>r+x5?vr(o;)(|XT!SHa;CPIjgOjC7F*<*BhHC?)KhCy&(5NcKZcD_S{ zu#yL%IdX-n=XrRjQrJHXAvkmSM#)K0<)hg)o3$O=V5V_@f*$It& z@)|+LRPRPmQBcRBo+Nf2;!vvjs{Rc7Ic@mP*=RJ9WL*Y$e?}LSx4br$>7}YGm7t&- z>ds(IA#$o0Y^Jq>xw-)OuMO+@NoY$zyE>p1lh|GCk;`}6dCdt{d{**(bz97&i}UKO zv5UNV`X2N3n`}0#C8MzSEJ82E@mm37at6Brc<=C(jDxI8%z`Z99!>)y=90vo2eTeE zi|(+MRxbAC@^aPkDQ*=8zkAFn2La(^(XHv&o8%rJR$Qri3q2T`md^R{8C^uK;X7_d ztSh?bM>weJB#JliyI(NOYv=HC%dJ=_cH3|2gdh9ukg%=z@$c}SyQ*KoBWm7urM2gN zfWNnfxyc-VB~}1IK)%1&daxH@2*6sHU@cm}S~NhLu#J9ZJx@P(ZK9u|_4M;6unBa0 zVX-*A0yd!=8* zHC+PSCAN|~Xu6lAucY77G9j&ijIq2DNMHO7+u~!#I^Y1%TUuElHo1doog3UgBWUWU z(0t7J2YVJ0x?-Pu37^t?BqQHCqMK4o1d5A9`D z(ZqZCm_o}=%4#>&W{*^wFI^Ys6C>p5v~F_r^BEVIl@@8*``7^)^=K?)9(ze5ax2Mt^9k*!M|d>skC@-)Cx;UmNAu7Lt&? z&_n;JeuM76YHdj&zKE8p8o6$N`RDe~$<~?3znqZ}9!Q+oXpY}Un0@eNAh{t72NE)0 z!Bs_@c$Ump238*VZ_)Mh(#Nr0Vqg;U)%iX>qCy$YA--T3DXPZmoP2pRyd0ydQ6U7D zJ)Q7C-(?`GW~)7PTs9QfRhGuBp}gT-l<>PltvTjWje;eJNMLaD)y%g82p`Rhg;_fW z){$c2aPAVp=a!g%9)scM>QrN+@YWw|Z4QkO|&J)bOW;GmRe}oH@ImH%BM4Y_pBBAy{DB-NXFuSoB2!ecqfSl&E zfJ+u|C3b-|)^5GASiCW+H{{qblegGA-P)I!U%egWeL2LS$v&hS6MwbUZ4>;2baCM3 zGPtUR>Km(LqoucVFAgjNaV1PI*8pIi!SM(iywGVj}E?~41#A-ro^-SREFm)*pmWJwuHHIwy>U^9_+ zT5XggbWQhzZFHRr{@X%$>BW1}ua~1ch?NhfkDFqoKQsWH8_hn1^=UiSCkS?hP%xc@f(d;I9cu~o zrH^C$TAeAz-ecyT;;ERzds_4w=bK%E=lT3R?UYTJn*lctEM3?grI*j_!iuGnyJ5ug zao>c;lkEbJO9s9y>{%KkiSSC$_i}~#T;T$rp2!$EsvYorVCi zDRtJ#NYO+U<&{ma+gN2Ea?*n7taWxo>tydc(uR7X(Rs$;zg$Ak*s`+y{k@-f&#adF zA*hv3r zSe-UIdM0`Af6HcK27-d!%)i&;x6k#bxqX@S$1d$Wvu>0;C zgv42UNEs8Q zzY$SPDP=>%8wWa*pm_P5+LFpEzG!c!DWhfmpk#5&j3sb@o^hGYR@v7F&fXsY&CKug z_+UoX*HpnvX(YA3FZ!qhRN8zAU!Y09-z3x-+s%@z971(}RYOdLaj&J#ePiq&i$QVlstdwU~Xmy-4d@$^V*j&7uYKn(1f38&G%cMhVcx&D({0yP=tR9S>lfTKE>unrT59 zwt51{bph}tC74R6?(sn%_6BE1s67#lmX&U?m5e}*_io1T{O76Cb5LlUjU*FPhl_pE zu)}n6tXNvJ3bvTlyz2qIcI_Tsvf1aBsHRqd>8nr4&?;PflB-p~(u3A{T8;-JzJb9r z-1NI0Ha(R;fu}xy@}{0+j8Zrv$nSyh>|CtsdC#^N?czZL%s*a^>CU=AJG57$YAb=RPGZGycd1a*pHW9%NE7y zyn4ve*zL7H8p3G0H8xT9M{_CZ_1d7hrNZ7Ib~zD zK$nz_4Y89W))Q0U-n=)9rJ*F6Zfxd#CkBD~dyD9AOl)(?C&=`7{s+>Af4vkoV@ezE zrC`&|W`~w+9On*`%XhS6w_=f8UjLPl4~W~y$v$K(OR(}0$ohH3`?<8?sAUJC*XvEn z+Gt6lTJwHd#HH38NmBc6^8xiUwed#?9r=* z{n;WpGi+IfJyXdLx4NGgkw22$juI8wEi&XvZm2AoIE%C~-bc$iqU4Qw% z#3lrDF*}Z-!iq}X8LCKQ3nybUUm00^Rk1}p%N5V6#j|`to-3M}#HLgrhzXQ^e;0oF zNKyAsP?UP5(#AUD%p?Q>Cg!?~7axdEl`IzI^Sa~Hs}VDUgo3wraq`cF^vNi-=By*S zi`^^@29DE&YNe(KWMqk>s*1#Y8gmRYQi%@6Lf&OIA?)WKBCN4Hm*I{vBa2>&h386a zf|>EDK+*o7{z*XF2%=VTNLruh1TPzvjXPY{d;x!+eM$?pS=YLPM6&hHaLjyPX((IO zKr(*Cf1r$0p*2X_3v2!c!9?Wdgeprel~N25WvBv&R!)qqBm1(uI8SFZc-nX%H?|42$Pv@~NmsFZpk^~3j>`HDsY0CTOzF(Z{Ht`{C1WDLXNY>bBivjZ2R${!_s>x#&dQz3D+C+!# z=nl)!_#R?V1`q~=UPNx|9}d@$ZeTzl4#MjoKSqB5MrLa#1QrSEW5^`WJ-vG)QA)Yj z6PeZTxcG*2?}jYYp9AJeH3~dj`G1)_LujGu27#2ZlQ>A2G6hrF5^)&V4eEo!H|V&Z zEut)WnKQ2Fp|*hjmo}b!&}p_2tI!6U$t8kWC^I*HV#ShU$gizeZ1z5zNi$D4m#}5JC4b+xBC&V9Io%vMrX;KBW(nUC5qGW#<(0-4vUjx%R#TZlI;L)337|=uc zKJRhq$$#hNiQ`At`7`T@jJUud8VJLohs%%Hjll}LGlut??8VTqa-r8A^tiAs6uaGv6bs>MB3$kSeg>sAj$e7dr2Lsp2t4vSLEm;bN?KrBTBb zSZLTlC8YvOjg*o6lT(RwWwclekAVdT+$>F-H1wbW(qx_;et|^I7I|A5FAA6#G}Tfe z8c$B+o;FZ}ncZ|gs#Y8-86px!(89Sf0l5qSjNAncFJIvb7FGl_8N6wmeDW)XCdX+( zqdL2wdD$Vj5eDdoeF1cLH-J!y`5$|F0^|ZfGQ4+YU|mU%eP>5T)`b8 z%>zAohM~wfOSk_i-kdAeWoh0S;*ZZmW)atOpRqn8IcC9Eun__|3FTnB%*wqvUe!0I zaU?~=I&b^N!~s5}ZOC_VR5b2m6hZ|u&v3DYB%+W(F4mH-Vxm+6y%Vq2oH$gd2|$BV zK8ERhunbn+AVaF;Yp7Qife--`c^V=NCn5GwiyYaB8a0C&u?JtXN3A$j;tgbe>@WY2 zK&$uv(5h^nLi6 zAbszT|NcvnIReiA*IM6q&2Bj=$eKdD<&)Q(1yQVF0_fc-J?TbBQT_L5d9dgcYF`vY zrZ%3?x*9B{rzgP68u2(~tJ3pr(%KIhn<@k^nA^n*sxe-WJ^woSKzA1(I6Hk&JSTYD zZ@(oc_|`Xaf}g+pf5!=Cp9)rdCar&k)q$sC`cLpr@%x>6eNWDWSBuaC@yqUi_s&eO7Lb#qRmZQ6kDM^a4AW<5{?W4s_sW_!bzOzd9XNvbrpX4*U_IFthS9QWS*CwAH z*yQr*L$~O&x45z!*+FiXtXF5&Xgmnn)w1{8OVCV8K3Wv$?o%9*tCmC$tpZyHd4}s+ zt>Ra9A%M2P`vF~pC_wuxWGQ6thsLfx>L@U)$e?Sjn#q!I>6g#$wIv>)k_p5jydB)Yy&pmrnCrh5$HK`});M$A& z7?tTNu-A=hxAFcOBepd9_sx9x`w@Y>0&6!49A*?XuRp{~-}9=sU`xru7#gQIVkn$4 z%@f1ll)SVU@}^A7vJe-0`Vd2p2qd1Cua+y%7e=m%4tsjN14)!C#X(!odBVAP*3@i+ zHzaU~1{X}4>TQut0itY143J{N>@D9M1LPvI)rQwu*=ldNV2`n6n-S_{{+5UX+dIJq^XPPr zTF6=$E9Jb{V(YbV^c(+n9ml^tZzqZE87@Y9ra>G;L{71vM`Z`Gve_?wQQC>&UM>pe zGPGw>#eqw)RPa{8i@rs{ud_~76f6SVK2s{~3O$ggK^KSu?NEdnc? z=;{tTZxOV)`rjhJruG(WYH!A-b|w8hPzEE6B|VKV=GVcvT_8^ny*I{ol&X4&()4meoq{D1t*s`@8H`dZn@b1v- zioS#AbCbF8BbzOLHqHLlv*}W9$4}1KvH#lF2=L7P-;Bb4mvj8vGyi{#LIIauclshm z;q7x@Hws(N^)m{;K8I2G>~VxSX&-z#8gIYR(-!){^yk%|d|LRwwS*2JGUGwSnZ(Kt z^Q6VFxAgpotuA3~>tTDb@wmLeepK3Qc6jooX9(XVu^YXIb;kB8hfQXenv`Iflo6JN z4^PGOVx1tqI;J6QP^Bp~BqxxFupoyS^Bl1+Y&oZ?;mL06=&_Ee)-jEb+l2Jgu}0M< zXV8BZ{pX17ev)%q+!vyiyr~MkCGYcu{tK|pA=)&jgKd50HnHu`-1`1K6DAL0Un}gq zK&$fORQg$#rZei6pLNR8Q#G9`xHR)*?m2{uo5wu!?DQOZBjl#)TIj!7%GimXkuvJg zDbR`JbJW5))yq5m$q+vzGPu(;VqSXT326+rR>rJk4gb9K48;1Y(<(q9ap#_N49}SD zng~*<|6S~JmZWEZuN#9b-P%U(@;HAc{jl7A$fvQ9ivD&xO}V!a^rfP4ChLlkdo>7{$_;M<-#-}r&R4~V}5V0Y=4YH(Emv=;^ir>>H|Ia|rclQZ8@uT0LVhkRS;8YV8 zeK^TWr8gqPgzTLnLQK@kD6bdbw(fxU198daKK-8<5}ij;Z`OEvwBCCGK{{5-Ry)R1 zJ8tB5jP22m>YzTus#jPMJeh>h7L#*^+NF)PR$vRC&ZR2O?LM71zkSX|M9fI^QQ5JG z8CG+uBn421SIvLhGi#t_6U4$Fx=HV-QCxLuc%3 zT~AMm$}C~Obzgc3TN<-z2lU;niF@Xc4xw+-J=#XhZk=g`RJBvpFEC0ISAlKGd9Pa} zCP0D<0(8Qp+@dr-p>A9ZQA)-9@=H^yF@bClg;iNUBvrkQognN9wQz`45|zhj6`0Z3 z5h36ogeEX9o<=}F=CAn_M&1vfi@89Z##MW~NlOTlSm`EO1~&6dyv?n8Z}XHuaK3^5 zA0z%mc6-=or_$8^dOq6urtDTT?NG1LDqs)6vfCr0t<`38B1~NQHS8vr42RJ+cKL+G zm-0M#@5o@fnF?zJy4rHW+U|$j)gJl}gSgSV{v2dci8i1Y;3o2li*p`~Yj45Ek)prr zld7KC#+}eir#t`(&DT6xAL8?#F1-a&U2aso#f*<(5zK`qFvKDR5)+71&E}o11mL5b zGt}LMk2?3T?5vw?48DcX{1yw%Zy-#(jwe<|zxe$(L=lJuezU^aFcx}u3M34Tpn+9u zj;4`V85`Et2OSsPg63{5pqZ??;&$S!8sW)I-}nUe=avi|OOsTzq`6m1YWrwOo70-9 zaXlkivR6`SK+e{}_(;$V-*^L~z^fR^-wldX)vC_}&Q~3j^&J)tqlrGNi4)Moo4JYC z_Z%d@HM?UrxDW8YP_?fI@q^_xVhb6rL9#J)6ygsN36#D2h#*yiAW{3qf&`*{y(r-D zSojx8)7XAcKN#1nTbW#Sl-`VH-N4Nnqt+ZxLv(g$ zR4qEtbJ(77^gC=cURHa)&3FYIUpHQz&8UDMA`=d)`xNJSpT@+5yTIo>=o9>|k3Ja# zJN67~e5jq$dc^cpypts3Dy(JI()#ZJ2?#XFK)^=g!D&iXY-4K3IMII4`kWw7jG=HR z_2#~~&(4dHk~8=cEJl+JS8ARybo{p*(C^x<4!E?x0o^NGm|OA2nP^nAthiUndJ85b zk=j8mKIduf4eV8~PQ>D3QQ^+yi>4xi+Y77o@EDM}-1XJA5;XJBM_jCq0lo!-R zhvhGik1QC2zoZN5JX+wUaYJ%M*im)@`95O81&lo5Q*yoP&G&>dg&&XQQ4$$x`2>X| zRUgQHKzrHw=!>9%Nj}7{k7o>Ef0I;p>Pk@c(~)FVU+M9C>!k{$$i{uYo*Htora(^& zj9=moCBi!@G9)lQJCs0ej?lv&djfi5aC}y1T&$%1k*nDd_yAXMd3<~vIJ4~S^k?yF z2*wu}ztkgD{MxFrWH2VacBiLx1Aj2p6LT(;iYtX@SP@Bb z)#7gtMH}O;T_^`*AXS`T=4*gVFxW#wh*6?Gq{gg$OR!CR6-t!_aC_-s!$!&v90GFb z#mT8Otn1}eg4&Mv>1lm#s@;Sic`IUrapN8m-wzUPLs9~#DEuNCH`l{p4~mT8#ua+_ zfhX?4rQF)i%g)w?Ff~0gH{pBSmvnNOXhWhOF{dsMc=O#-#R3uwA`@e8jF8cSxYo|( zz8KWAvG!P7VH%H+<|?mj^_bQ@Ud(gWyj%Oc>pUbpEs3H)8HbCrf zCz*rmHFZZ#)FIXdB6A&n{Vnr9x#4ZXUWNl8UK*gK4_VO8+HI9VcoBsm*}|1X*^}C4F z?jtH|#rQqVwEB7?(AOZ;=|nW*>_`@y5zSS(BPZBIwMoIs2jDdQt4<2_=IO5|?xoh35myV)3a{tjvYNo@;v zOBp6_htIEZk9gg;#o~3#og60HzLI4XQN-6gT>3+Z^Y_ZpI$7T@>${|? zSstls`cUUAw{!Yn;v2n+ZU^IR1?u*92I}^Ee9l@wm`B+yVvsIhsPB<;277u3>fT9J zh75hyP{o)W&0YH$B5vm>k`MfUv#d8zjUM}~!FFOtK&9yIT35 zC@3-jMalwQX>m4lH}=e4VA0d^N8L7Q?XLxAlnUr`Fp_Y%MyNXGYp9zaO+_^L zCCo{8@wbOn{Z=03{)Sk!I#;F6Ew7=|)1y}0f@SJ&NBSA!U*drn-_FDc?EWp*FZU0- z#yB>zJ(AW;sF&uvsv4|#*=C4|h>zLMe`w9>uN^bhD?SUba2*)$BLuf+{p z_P)3=qamEi@%KMs-VvsARc6A44l%ElDw_G5@V#GONKDiS%dwN1|JjMyzzzMisugS{ z{!R~71&ba@RrLGPeA>NhWJ9^|2WnF^G>@j}U(nxv=j*DG@M4+UrjJ}-tj%9jbJinm ztt)PCDb^}}f_8fklt{X=Mx%1`TnIxO85S^_g=plccBW%In;bgje7_W_3fD?G*1NvK8E_X6{ink?$Vl5W#QDPrH`RP+#go z)C=^9)mEQS)O2n?d4ss?1wg zav#+ah6zC~6?p)Y93J z>F?S(X^#FYj;h{Ao;LG*C4w8YSDuN+&8N6jBD|ay#80V4E2^LH0#~w6$@&GK7J4qA zg`TGWXJunXjydg+oyRK{@JM;uLb}8B%5nN{r%$5lHW5hVXD)TT@~pJBm5pCG3Ud>^ zbAXGAzD*Q+pNIhki8h~L%Fod9v(zYJH6!X~s*t}hYIYw4hbbOtwJ(LIlm3Cv$V>1U z)0{rzc9+k%(-EQRt{4y7eO?plCF;G^shqSjQI=lyx$Gh-=-| z(_>1~%T8ZcGPLaUEmB1?+^El#ccjUZuOl)lc6vH8GG61IoWkd#yN^3dyrdCh| z^6(Oovoc0{8V5vHqQJSYWE)1#-&p`3;peQgs5h&J?0N5p$v z7Z6cV!W$J z_}na23hqQIQ{*t+OYek^ss_pKydoSnj}ip>LJPRy?}v^i`S~x`voicv!6qpsdM?g?}b{i zQ2S^=Xt@b@Cp6(6gl0_#UWua!fBYAw2u7* z<5yi{@w8dh0-_U+3-y@e*O%q`^&hKos?zy&s*!x;A&1SpP}glm@6>;?=#wi8g{OY7 z{IQX?(6#16r>NJbrV{T)Yn(QDq&_uGgO#&L{{hZok(Ca_2&8A2lj!wIdNZ@Fn2Ib! z$h|ue)hA2!&Q8xMNkQfY4VLFFa1U7#DZs(J`P;vqg!%7mXsUOaUwsv;H#~VH`YKj& z0Fl<%mKoV?El%m1`*GOa=*RK{n326;@5b*IsQRZOMPtII8)NZg+UgT(ZyyNPxMA<7i&G^DrN%TM3AnqK_HEb-!gI=*zb&DK3XfFb{;>;1>@{{6GX`%CHg zQdaNx_jSELesvo%`z>CHT$u&UAn?ug3FGL)!2u)LPLS0nWYAUe4UJ*t$jI+pIV!At zm^*L5!mwg4T2;CR@qwjRxv;$T8{9A?y--tjdgRhp6#HE&mN*?zEPbCj<_Fi^u#ZX@ zdy97hl`wJAt#=iNl~3pYxL*0Z)biKxORu`uHm-K(t$%8O%|+$a`lHolXGSgk7^&Cv zU>h_A2lle2AZtF!+eOH9MDnzMWWaEhsYQ9I0VgpY30JLf+w!E>TxCu1oX5#`Cv~7rDVA5^g`BrC84+ z0(8V$XHDjDJK}g;1f=c8WD36>5_u4ys39ezSh;&O+kcd@yNreJe} z#^7K3^)CWiunnvoTzW#LzleaOm9>N>(cm)b#&<%%jiKxF^pSrg_EwE!Z>)M#V?T8g`pMNC#kdMen`FV#dZQL%GZ+Gbm&1)({ z+Pd8;R`s@nWrnO(ULRK~6o4qT<#_i}<3rG7o(kxn`)keR=}Nw=O(mvo;YeLMB0IO!RT`}O zZ}AJ(&fkNz>tWpwn(_WT!P>#V-@o53qBB@2a$||JVtn0I>%iBITGvZJ?2?yyqmrV( zA3(aY)7huWA4_%QN*n8`|FqDzKq{4y>K}1PwX3ZUq?%7SRbcDFslR^Ig;R`BE`dx+ zsxWa+c=vWB{u-*PMM}Q(>R?Z?7F4CJ^%sjrAAi<;#EVH%Qn!)t6ulUHdA`Tj-IjF5 zn2-H6=LE(~iN}~D(z-t}#=HTHsX5*QV>Wlkn7@3;81q{^m31YR5hgrUEH}X;q=27Krol+B1FRw=1#afq3OwE~j&DwL+$*1+?!5w7cC1 zdzxeVnjm1cb+Na$Sokhgze&|6soo3H+OxRq6a*^$`jy1+lswR>k1*zVzyq7J&ItGa zPnk*bs>Up&q4SttzF+cPu!#XJ2fQOQjIvve2Hl*=^mViTGw^;PYWd-y_d@881a3Fs z!!51-1+(T4rf0wnH%l<*<$KToPJo&v08u^WCsdX`GlRwO$aK~4Yi8*LRYm?g@-K?b|77IuHcg%pyRT5K51bcfg0IP%+d?@5VN%R zWqVKkJN8d{&1XyX0iT?p-ipjtG@)6Vxx`;ojlE%7_SBjqZq=FauQLb2i+&)@yJIBk zxa{kd`>YASRm2mAJflqT?s%6_nH3e+9PE!CD1?Vkbe+wA=%n7zOL zIte{Dd`SH7BW0!c*;Y%kcX?{bfbt6tsp4U_&}j0p6ttDwehW)lEq=W;4$u9Jb9Zy( z%!fvh@G}ba3#?1t4XkTGBt#{9UqB;7*JlwH%_3|b6d<99Hx=_!16pnxNg%Q&XOIX& zhcP6B&>_oDh=^YCYvcW#X0l*ZK$}TN_y4N#NJ3#Y??#T~`)q60_=~2*sPnBsi#qGd zBV`jjwpIVqqYs`q&HnGrewvH_n#88#0+8`v56G}S09m1!(|5BZ`v?#)TF=ouS%-31 z{YeeGW^2L2M2y#b4|*h$_l~s|KGM69HfE zo6S^xmpO44dgE8=rqJ>9%t1}GW5pt(9UET+?dV}&5&%&z0KNEwr79(sPS#skjdI$@ zmKUsnyXOS8-KX!8g_ng<0N2O^=KQZjmq6Y0-wC2i4xmfmmv-#icggu#=#mF^+HK`) zQe7nv_O8*}-c{qR{|7bdT;u6RyUmz<9{HHoy@3vV3C82TOsd@xU;9H}Qtk3f91{$I zv5f*GiMhrs__7Ns_=Q(;+5-zUZvOUAeRVsaDr&U6etA+ z;QufQaL%s8dffx&pUke79CpU_Z~p(R{!Fg_7Y%lVbva<`(mx;mUv|;H+fZ1((Eq8>{{{4W zo7D2s1NPZ85`_R0Io32I>M=!-1Dj;}jyWJM7_$)NQ8j;A5 zfR?3l&b}!$JV{h~m^lq&{%dl1lf$Q#GKZ9i9Xbr?tnG2i+(cGm4q*$T8NzOei<$1G zZ(Z`Ku%jeRESf>?ZUV>t}&GzLJKD}qLV*} z*NBeZfiUmwpS%kGj#f$eG8pD~*Z)F`U6I2CYSk9auYoA!ts zrgPfZZfwd_!KO?VY|2z-Q`9In#ivm3UN6{`8G=QbhoIV>skh6wXF;2n{K8!Dzg4~Y z-@f+yI}q_tKkbjnVr%@~W@+6*J4A>3-GR)}{=MhL?!Kh%JuNo(v2Vni`_j5B_Jkcb z(C__nS%P2xGBUvLZIjkrhVPAWH=(B%`>o6uS$<>2HGc0N=?UbVH1lh*)ma^zONG~{ z^-A8 z_kFD~K_>RcJ_3B&C=yAFz)t%O*Og$Yds`By0npLg?{AOpHvhc5w>?2xr+HUFkAW&O zecEWB=69*akKFJBq#70w4lN*>_oFnwcBP*#>7V5NdFe~?4A(=fBQK)yEv8lu$Mzzp z&Ix!=FB?dZK886KnY4U@6>U}B+u_#_1iT%~B@sN7`n{2OLFz*42xGz}0X>|(58>r* ze3Nk5c?<#Bxw0VD;O|yLE7$Ntzg2@ju7)=QUel@pBYR(;HSB+bYY0*e+XMrlM3suq zR@2h7Utg8J*ov<*q*!y^=B$>zJ60uRhvn~A6MyUXzPaine^H0u8!f%2n?9C00cpB2 zcSu_Ud5&Uj@MCH;Os}o_80y4Kq(Z>!aqlOp(rbeqem${R8wnq8<93JaZCUlPNRP+q z*t?DJ{C@MLAIzqO#-CaN#mXlhpwnpRy_W~Lz$mguhLP8<8sAxm4`|jU!EV7wUNW9Y z*4dqyfkyIg@%?}PLn!>Y&~EU|>mQ!cF?h^d{RbbH67Jo~H?(t+xtz zR-9va+DLk4-Wu@ir)TEye&N}DN5Ak~7K5iH9v&y)xtif=zg569nIW0>DnWCep!tIZ zP3u{PW+|X~w^2Z|1JJC0r6)99@J#3*9xVos8b4mk0MEa@0(kDT`sWAsxc=Es5RI_L zYmR`Z?Ph{#x&=`-zCZPHfBp04f_~u{f2##g$D}xTzK<3rGCU)!{<&22&*xVEG)@xG zG@cRt18Dx)AfWk(o|%h#LDL1#$o}CO9fQY=hv!njv;1YopIiIspRE=|Hw%c`ZX$>> zEr|95qPJe^uYWeo?jL_*@U+Ck;{-e(zr^@6sc-zTp!vgT(LaFZnXrXFfJW^FO&2`p zX7vkC<3tOd^%LXZd4Vp>I~bnlCNexoR104m0_P}!bLT_>&gBA}lQ$ALLo9HX2EdRkkf5W;KJJb%=eAAW0*oZ5+8rD>eO4t!e%o zzjj-$8lA!{y^_q!QOrcLYzNJ=ch8cRPGkA$<)!!VGkB@wN`S`DS;L6gd z&`Q6M1j>XYKvM0pEJy(h7P59|?hL}*6~7alHO!1r>A;s&db{;~>b_psLUHmCe?UUHpcZ)%W8t?Npo4-s;;rO+F7>CZ9vBhcVO7*88?GK>hF=6Jz|w zH(7n&3ohY`ZGCea6Jy-QH^qYZV#{t60GyIf~|Bs1~VW! z9;eTrGi%l`aVvO^p>viB#Y-3+=_eFD@NMd0n{`TWqc zLK?YDj@B#QFQq4!Uf_su^gbJ=(s04RQK|Uw(JR#UgBJfz6b^|&PDO@Bhh2B8!&bR^ z5WNTInu^9XU%{W?8*O9EJA8o0NPcx1P0e>U#|ga_e)W*0tor@g2;0$_-WJJNk9`OItiR@%!|MAcHLR>bANw=U`7^0)?P|>hM|PuH9(66v zjbBGd4xY^AzQt;XPxoPa*Cc8A?>t1#wg9pQzkvPsc2>pm}P3Skg&A17#B9cvoxz!7q3~pi9!0^U&Y2mhuE0d(qm)7OsnmP z?pBRqg|lHJLdZ1go_3mF!`8n!KwN7(da8SN2cUwQCA`FPnS{P_!P4=tCDm!{eG>_M ze4btG=Pp+D`-KTSZ^#D37B7+JpfO=MOzp~dCaK>2D@LjM>nLGwE9U?)R}4`N*J$W( zz0InW*PrD68K^TBYKU!x@z;nA{~zy6V!;c#R3nA-k> zW9kqbQy<%=pU1Gp&t#Cf93A-0q@>vRp9PvOwq{f)v*bHm%4(Z0OF~}xZ1{qYrQ-XTcjfA~Q>;3q4YxF^={H;I-5q6@``qEu46)AqJo+y~ zg^_EZwwjx4rpTqSM^&*$m%yXvZ$dx^5Z0TIAuK@G;g6;W8~;o~`*#O7roZ+E{z!7> zVJBnu6lg49QHOo`V1=a!+vhlwgzX-uCG?5S(g&u@JH# ze0QkQg4<+4dfe7bAx1r_HB$_=r->{`DYk>+-9aqo3dbseK0Ho>ibSYMq^dlYu$Wkx z$ZN|3fSDHp495X_9aAg^LTA(TM!CD1g9>0~Y|7`8Yr)5D8nL5228O3x(N;#9M0siD(^EP+p7uUPkZ4Nq^yL zlcLW1cNcPmBWnKktkx>^3F#xH24R^=;v_wwQW7pFk0v ze`TV*DY_Fq+LAIi*y_?dmn*JAAK%LkUi?msE0|XFjPJm zz*{?MZ*y*&>_2N49xp}zn`kvT4qSXpyL~*4X^np~o0{G5BF6J_lVSm-Qkqehv<_1a z`6Zhby@-?F@Ns4GLB+YKkL`=o|2%Z2=1veQ^ke)cu<-S&Gr|)Q_#KNk{*8~#aoD{j z#}(G#a~~D_Pf@SmjTs`c6o0#IIdoX7W2&%YK#?%qq)u)0x2LVVLUHe1F%*3vLU`2- zFLalMqH^SYe^*BN%o20}E?70q8*Xwss8OYW@SIS2_mx zt+>?(a+^i)I&^p*VTZLSx1R4$sQHMXu-*-FrXW3I;UXHM2(?F7Tyo)y{*H|Dde;%x zaersBpne9JxXbnnmB%RS)Hk^V^lk#m{q+s+!yq{Us=%x>Fsu=Le~|cdHJ&B{sQzj^ zS9hJ_u3j-X;Z=N1M$qrTxRFs|lcTkYyLI_yS6kH?>LP-tg$rR+4lIO;)55_%_aWhN z4Cd-BIpcF3^10vl2~%6C)^bOdUhgb*N|C%XW}{xHJO%FRhOkK@ptqq z8vx(g!sFZF4Y*n@tp7XCU5M#|e=}S+D_t18 zWLCDGCHJj>6h%~#cmuki1HOEnV{3IY==`jr4!1b}PsMudhK0xBnT?bX^jKj# zc?dxe{4^ z-GvIIcO-v-2G*}$CL~W8^%tu2GnUESnP`2OW~ zFvs(IMwV=X!Np0d6UkZ0vB4$F8}NlC){Y}|abS+4hz~D~xyL}KEpUwEQwR?RRsR%6 ztiqkQ%$++IMt&Nlqt%vbxumi5iXP+4NJ=S>`Ah!13ul#gI0|c}4W?bl3Vl+t3AnfU zRSX8Hef~$}CEQW*@Rx5hCBF7OOR;Lm6Y+A)&#-*~*!(a$$>OUh)=Io9D()_?Ahn?b zlq|A3S1m0H!_2|k2n1n(v?3hb!(57+$P-e-qZbO3?06dTag5jQEjdf+`|7;H)OHxx z6n_Qp`4S9Bqh+7FU08pMmHK-N@9$CDiYvT<3T>u3 zz9}+WVPKr>PgUIQ4<{?EJp!g#w2vZ8?U4D}X_NNp*Jt$;sZCfNE>s$-#K;%%8b?p0 zg-JHIg|at}|0sOMx`(R>;0`GP0S13$@ z6vj0~AFkquwKc2Gq47G-0#i7scN_-XjxE;(`9gyQ_^{_kEw158ns(r&`1$}R7{=jc zSmiXRa_#q5+3Rw{gWBWq>U3^G``D>{#v0nE{bChe098P$zoSipig*qycUuJ*>-RS{ z_oV>3e|M3#`xF27JO1w(`1dxhtTNxG*uu-^???XoEW$#pMtke`ah7ZX5;o&ey~$qJ zNyY!!Ib3$rb3P2=IUg=Kp4Zn!vCAo{@`~T1HSEhm%Q*K1Gg`#h2NL6pT{gZmx=H(N zTRVKOmV1PEG5CY`>U2Q|JqjiEv9F}j!;Z$b8!#sn!wfc{!>XmKV5zIe$!{q+cJf=( z(q3z);}9M;x6s2D$~gW6DjIzyHK0mPKv%~N8)$-jnJsKVoyg{9QJZqLXbFMsIxb(N zg;Sw236#6P*(_9c;%VAi|A{sJ4s+>JcWv23xujNB$Ny|ZTXgLkIopP2nxdsw3U5`? zWk7mYb{`ii*Td@p%ynahk)fseSqx;zPS#294yC;?DDgE&QJsZ;>ZP#D1Gde@Kpxm` z01C80a;u{aCOB`T??ee!@Dy-{6*Du2L${Z0- z!NBa~-OFQ`+(DRJ-U}x0!>EL@^hG=mR$Z|eXGn350lU2UNh}1cafiwZKrYn`$$?C< zai#z$3kP*+3{;01L0#GlsCxlaH+xF$iL8lagH`r!?Cj2(pPbbXzAS9qh2{;W1=|q`+*^UN!nmcJ78uK|VbRV$V`dhO}#4O6sJGLY& z;OL9quD^9}rpMNEVx9n?F^{s2s`^!P9_v7(x1va9H``?QPiaQ*BCR17=re)`ueZ@& z>ZA9ci{kkZEG1O^lZ-lyf(eg`>CuANvEs*~WJqtsbM}u#pIV%564c+)NOv=x2q=mH z`dtE`BpuKtP#a%$JEe{rj37f@X{;7k-CERXQ3}0@6{O)pTY*hq<|0HU4m}&ayS9?@ z!~h&uJH8z#*I&3LFqvLRKo!v|pYvEWt0di2;f=GQe_hfGG3*|zyKnP1(a666$Dd8% zI6iRSxu|~o`8f1fhnzLC0c_BTND@V9|Hn+)aTxSwnto%@KN7bbpjBiK#;arpF&G`I z_qdW=X3-rio9Jcc+=E_pj2+qSU#GAnVSBn=%t^Py%+SZo3dO++!USjer+|Qr@J~&N z!M}#X|MeR&_`m*p5BM?vTpr(shH}0Vo+GgirKE)-<#oi)---T?D7rRN7-?6nKbZQQ z|C3ZpH5fJ#3rut563rL53SAH}&IrnB)mIph^F0*ZO=CNv3LEBSdteR>jJx9RXsB9U z=0T8#9zamC!lxVfcrr7|9$K6!ES>gUD z#kd2p44r|gGeK9X(LVkg%~!cilMVCJRcF#-=i}G@yc86Bl2D1q9mu@+1HUM}-`|{6 zTHtTCmR{=bNG=@!6FdD>c!&Zn?9N_TQzxC^1L|kE^gWe%s@{zidfr&b$_pj;EVS}_ zYO0g8SlOmn*_rXOv|dH$>D@M(Qk=koAH)h?5icnE+tb$AK=xGWMC3B~b=jJ}l%^hT zd=^Qg4S%@QylgLsEm8klo%(x``pHOr8c;7(b&)l2!p3pz!*NXKI6i$M2Kwb+8R*f= z+Id+!FRRAOK58tR#>@8QWixPn(@<@pu_%ZASvoJ;kG6D7v65HCORjyS)Kr?x%+YWS zoeN4lodNyju`hX^_vCxN)uo}57VIkzmW}dvWURbp=dLS33;rRz!ixa`HuBYcfKmNR zGPv>W8f=BuNz@7+N3p-dS}|NM2{BJ)vGI*fhBu%Si<5Y-F4oq+&>j_cl7&D!QUAnv z3RCsRIa-#z6FdEZSJLzwzuyw?^h94DW>&2dw&(>!_Vm~9_IY1s%MJQip2AKCGJUQ# zg?&wjVDTtBCcVd)Ut$(?ccC(Xh#BnrW?}tNnrRQAkW}(xoiy5AE2}d{%I*eX-FBS% zJ^VlMAe8-#K`7DBVcIZ?KzXdxMm}a(GBBNHp?E@?9hNOMaFs zM3?Udq%Yy$e>?$co8a^X1tJi;*B3JRk# z{z@f%CvcSt-oRTs_#1nL%w%gznh6WSdk3I`BQ{~wz#@9Q;;2=~9Exy6b%0IS+6YC$ zP*J0hneh?awS?gY+ZOpeEaB_i-aM-U!nG$SIJ z8NZ3~h_L&10`lj<8e$JNDYZz(({WS)1tfPhpqqv0su>6RVv_mQM>$(FhmL~V9!oVL z;t{q6i6NN-uRu5mya{lC=bk_^(yP7nJa(dt7q0@LoArd*)Vv60QCvU74|MWOR>_M` z#iV>X%V>3euP5wmsB4V_q4z^BpS(1IKN*Zq22SBmM&c8v{?0_<)_T18vRC!B!2tI1YkUE4 zXxk;+)i#Q5T6NNn6dEKbm>s_VtP!T`dIQW^MwsU74KS@Sm~c`o9rX<|5zQq!$|)#?(d`jdL#XtCR~jEJ{xlj{3G`=k`X}q~#~JzGHpu}07h{d`XM}Gbe=+*gIs9Ej|91)WU%~0W zL5Kgx90UCIM)>y_>Hlhi0e(yF#puuA@OKdX2p?a2PM`j~j6Xgw!oAT5_huv9r;Tu* zHNbrtBc|%~@6zG!c&(oS?q9#v;a;c1J&Zp*Mz|;Q3~*n&&M0q2xH&!OMYtyu;0gw~ zV`FfSOg7SctpV<}MtYYU>D^<58@?#Lb_3kkzR}_Sa-k;ReTE zjNVKG+^I3Rijm$SMtb*8G{C*x*srU`8{ig=y%@ch7~t-0)#09=WT5vcBi!{yxOU8qGO30h8EL39~P$kL?A z8pTo{FE|!EMqi86Q z&9TTwENUV;7GI3*=EZW(izjp}))=w4!ia@rR8UQ03|Mr!F2;+Wa4g0m7T3qHs2tnP zi-$Qcex+ma7h{V&pJTw{lu=7xV#MOTUIvLoUR=Ska3B^tzRN}WQLD2AoIz! z24r3_a>iyvhFyIjXTC{5#=(&}n+eFg&>U-@oNmtC!#PvQIrBRmnU}6IATz><%uXXR zkB&AV^UX%BO>G1bYy;hZQ{@hocRUk%%62+mX0tW!;Hvu85P^_jmUiKyclOja%5gdWX_z_ zkr{ez;y??Wd4hB1uR1dK8j<T$m(RAL-S4}~gK=Qy$fkV9nExk>(LQpI={KG6Kw z$8+J&6=LBLx$qZiPI~NPys(7vh>6t_8X>^n-C;BQg_^S>)^CKJVN6b&DycJ5Jq&%w ztzNa<;S^a=aUU+*EJka1oc0fds{8R|K)Yp4im1+uMr^sk$mOCsKgHiUa;2aZLr=#; zh4-FbC0knbllFzU@lnL1e`6Gw?mf<$*#5K3l3zHI7CZ8k<6-r69)|q7s4+hw&Zc0R?!#_h~Kh9~#?@WcM@k4neL z?yjHPA)i0T5zDRf<= zC0UO;qtl~Kwcs1555o>PLTat!}o4lYhp}$&jq{e2o zn@wi(61W&Z9?65uI+%$ZEO+>$1x3aOs_ECsSSKvJ5<8fmJmw|2Q&BWc5^)tHH;r{haPS>HW}T zn^5&C(rq~)M?wD|T>hcI%SsrrcVgtB0^ShN%Q9m1 zA&ozy>O^+f^*+-0MXLWS(AdbTd97hy>)xt^P-_Tk-K)66D@H8Q^To`;x~qX+Y>Vp2 zcC5Re>JB5l(Hd664vFp~!p2Lwc_Se;W|C+Arj_M9(GdphtTzFl_&5Y2Wk{Kz|T@kco2>* z*he0A7}IqjJ#4?ICnx-BwGZR5SwK$r8S>RmHnTctSM99p5p&rK9;RcWpXx?Os#OtN3^8^JU<*&Ir>g!)CB5vp$Ufxw z)y44pY_&r91>c4SPsDFOv@uu^fL}0jiN8I$JlVge%cAM0i$V$_0@%IV{V0Ztf~E!F z_#6y2z%m_CNnIUfZNMbz_D>K({B)|QE{rk^XKTFt0sp_NrMRA>!H&>KKh>pb@8<;f+0(khdgQuy#t0{y->LTb^UqeDJAdAAl>f0rCCchfmF)sbh9i-b?5}j+TniL z7b45Ii7pdAO;FOLeSS6?^{7@SNcxQ!_V_tDw;FxOFhhqW2tV68)%3NZnhtj!wuD5> zZcHU-P{E1upK>VtIpvW)fZIoMER zI0hyO8&~2HOD5S-^)K_WJ8d!FC(J}pIR|~6@pQbxzNG*NUe>;3lgOqx6zKewnHV8< zILPBL(chU;Hdu6b3YEuAI>c0WyHI%me_7y%$K6zxh6mOw--27}gI(fVILPC97q;hB zG>%+#AhLv@CwH&Mwb$dugeUd>4l~;)3Qt!HTlb0z&_uC) zItVn1mKvQ*&jyb! zJ711f0WqD5fF*T?8OAB-fgy423A1N|H3`Ps{uD;4WUs^yAigQAFVq3NDBt77Lw)QW z?bTG&O^47qc~Khnt{OaaxsSB(LX`DpJem`eN0xEZzBCi1h()K$G0?eCg4CBRu#H@{veMx7*S@K`sj{EOv)R9h5fQRPXuU(k$!B1jk#z*%)n*zgZ3)+x3f2YDCW3mjHK zeU9#h)SJ3+66M}2Jh6)I9Dx=(rcSvuULV8Uv{r{`33^Lfe3nv&O@A*t* z)B=07iUA5nV#3%GQ`b((gelY0p@>BqTOZw{M63N>QK9lYqSlovtiO}u&1NiXJriXc z|M|RtmSXbl@JqIYqaWuB+xIu`rddcs^BeF#;V`A8QMKOeaR*_HVSsw`=2!t^oC@lX4WDN(Jd(@G%H*y!9x}!ySo);RS*Bj}^O=JzO za8iSq6vh%~4y^syTv=ag71y3NOB?!$(QtIXWI65uWiFLJsESuyljmbk7Di94zD~H#lucshpf5#w_!MKJf?|<_eR6dO_&l+9XN_=;sdqz z*~a$hCUM)fbVzC=x&CYUzi7v@HTpeFxvWiL?L53XLHww-15tS9i5LpctTvlAX{l@4 zqi-+S^oNWz)8FCqANafnpKsyQw_lpc1)oCr{P22;_!j0oU}rt9R(6_wA?`wFlTD&( z9Ya$j(9cZ@47;A(qaN2$XlG1YpDE4Sg1!auQ>^I__27Io;5*57BX5A-?CnCZ;m;c-;3_)etCg8Me&~k zy*?NS_pmeStx=Dw&7;mv1zSm76jdKc726Ji^<GSlL>UiG}0kV30FaeZ>oEKU^@?G))~c5s|o7M`9$PekGwkv^UgY zEKI=NrN4G1;69vizqD7}@jWOG%GL&@hgL;bT`Ia)Su6U3-Q5fBRId34%cUaMZ~U)N zPx?Qio|MRU^N;2G_ZLhx`RgoGRqJ4XyG_{mCIy0909GVUnQ#>^2Uqd(;wyMw_JIiP@o^2;4ivD$aQ*jET5|O`@@apP|MEHt@mYK9SusAl`ZE35GTI-;U!t(4 zU<>VEUCMvW+=N@PUoy9MzcB1CN_r&oew=7DE>DVniFo6~wXB z&(}mYDXuUJfdx*H2EkSr)?u(m5wpBN=bgd4Z|BD|&8J#I^0TsKx^06U!&5-Zr>fSV zt0o$b-Hr@mv#tKnX-k8!RRHyZXG5Sb+wj;&s3#ikrt-(oJkFzxjf$($klA-gFPVMu zB-Wd!NQH+r=Mpnb=z{vd@Y!t%vBeK97Fvu8>sRyk#WOuyP zHq#wUC|w85MMl9W|M^s5;|d&f@Z37ylX8!FF|js=zgXbN!=k7m-@*kbbygHq@}5KO z4)X?})$2Z1njvnmz-*6R;m5zT6+BCanJAxa=p-S_^J2uwsxxJ~UK=qS7YO=k8JG`K zdkg)z^!$%h^?QfTuKmQkVNSg6{@+*kl%93JC6iI>`Us{0mk3pLM9M*NXC@i=a9!_n zHwo*`b1BF{DNv$eHr?tEekWR>6Q)8JjJ3Cjy*U-0Sf*RyAzI*;*|rUt{J@W;{68>% z6#ZvY%D3S9=mgpI5v4fpjJ0u1Zz+zCd}YSdc84&QyLbn0=FPG3e=$$yL6H#I{2Ybd zo5w?)F49wic0L#z+l#fuedB}qltFwjN2^~OT-7~D%i61_G3w*Mqm_J+mbKQ0age@! zC7vnZ@iK0Sh4<8@|K-u3!e>)g_Z&IAE>*T;gFm^v=uao|U zvcdLh9}dEE>@8yGbBm{M+b%R-VGv*)BC927!T5#ztY4{ZLlS0Ch@|6&3@#!+RNon` zik9tvZDKMC$2(ffH#iKo>>?NU*nEW7_C6Poxi3@vR(AnbXQ%1}UR@*BIGoqm5KEjc z2g-KsA|Ez3cZV)-Sl`Y|=9o+|{M_%C3V<4v;E9=bL^7#LOi27r7^Q9Qj;d&Bip@O& zE9?|b)53G0VLA=W#DRhICdz@8aBHPqkKb355UuvtU&K%({JcH$$N9f3rd^SbeQc@S z=UQsRjF@6{Uo2LG6IZ37UZvzcsp_lJt9t};dB)5Gxj3~CAHi4w7<>_C)Yu5Sc=|uY z1xHMA(v89s|Dv&`O^=^iBfIus1WeJrM^NzyG@h#sYb7JV3<;Jk$28glXtFgMwAgef z3c-PP#a%0`y9#b&%GQTOCb(!x7}z50@)mXlHad19T=z$%8G*k1$TBKJhgYrSPh$FT z<0l95aavXu@-|EpVdgcy^nbdZwGZ2>8AunI}~gu1?5QLhl@$$`F9uhnBHt& zl%j7l9JG^Ix*w12JGhr)|M=SjptiK{zUqp0&%Y_CCJ8|=Svrap7V@DIgUR>DxLrcT zu}5J3HAxOdle{Hua-Eq7trKnfeIZTSn=qk_L@p57J`|3SEbO{(YL`f7Or+eP57(dZWSm!>LRm(*epB4- zrE`@MGuR$`1pmV=dO}EU7wHI#gz}vgK!?euOwL4v_#R;U<>V5g4%3zuIal&BY&H z)w)wD!S{y)WtZdY`pb7;@Uj@NQ(lp-ii}u$G(Sf3aN|k$p2FCFK};B>ek6?lyCx=# z|N9^cR8We5ylOW_y1qZ3EuD|<5gD-&hGg6}_2D^b z%m@@yVA)dcR}wpKNM$QR&+I-E3H-c4b{`SecUj`fAzO=*HfsS1dA66t$X`9cli13t z^>>kER3h0p9bc-Co*TS@`=3Rvxs0NAQKlb#f39LJUgYo9{uS~i=Jc?^Q{&WdXz&bZ1OpSZF5 zRpgQxdPnq9=zp{cfkXZ`(IPUfUJ5Ge93A7V+Q2)9z8*Fk47>fHYh^Z@4?m=jDGK>J z(AhzNZsK&kG#ug5+hV{ZqE_9b_%*AS*XVP9S^h7;1GB$ofo|3JoBesFvN-_T3L3tw zHGhX0nntJ`hFhJkZ!ku+tPbFjr{ftDg~!j}_|r@J&~+bT?7@N0ehM7cTct@4AFbgJ ziXw#oqJ`ROiaXBAR(Y}Cy9Oztvq=)$hqkzrYVf*_DVS%u>e%XA zp#_(00%ezl%1)w?Y+;$*3i=L5!D`}qNLaTB7xH&mXNYXCD_m6%EeG?d!l`V4iS$*< zb#peH1N&P6o=Wb9^8ZDi!py4>O_4b~3DEb3p#Dj3rdN7&;IvPGqd;cl6>V!#cSxr~ zl-pwLOcGX{gKd#e@kNtde)X}LC%G&tBCE@{S^MHeayRldX(wKc(pW8%ur_}}bMXf4 z8eSA-?ky;hFKnlD$IJMVV2=5MCBge{MeXCWYcHme^07Ud#($^Xi-!IqcVVcd74-e) z=Uwo<`_pji4e>Ks!9FMn7%obrfm8w&}hlDC_~7?od%^#t?%)x1ox_899a*06|ss9KNjV zNlZD%eCri1K+&)}yjn!z%Esm|(zn|HfWi*qEI$t!=k$X93edud?-_iW=VJ}tz{2$~ zcX-kpke&fJvJ062Kv>OluIdBr0qH@ny3*zj3s01yMWKQ+#^cn(L8b;PEZrdo1SK%Q z;bW`qzQ8oQk3C?6|DdpM@C8!5?r$((v_-)jeFZ$bA1koKf3Sos-T)?&1{+$HFfeB6 z63pWVeZz-kdUg$?`1VMm#sht#>H7R!jKmkDiji^1B01pp2JQtwkAgwhj`kH;OOn?W zRlua0p$>SHcaXXsG1(HrhY9OPP$d5S+T^)rQ+L(C@FHY5jWIV|J&9Zg8Ism} zfw{P@k_TLVhMIwAxkmnrpmc@GMy}U(pwcVi()HpXij2PoI>!zz>2k~@{w`gw8;IAy zb|&pWn+~SCJ7o5#ol|Z1vPW&4dKWNQ##5NUSSO`;z{v}yn8wKqIt~}uV{$7pJZHW? zVxz*YNf^5CvN?JS+`kKi9iUb0*G2fC?3#n3bvB8!r(1aUg7`KH>KTmQ z$ST&j=RPQ?zatNcio0-}0#4dl;W@C}svaRDfk(>6*m_(4%ai~=(;oS0Jc!(jfaO+1 z#8*`hdY)PsIzeKMO1wtl@qS5swDUo&ih=ZX-_dCI*AL7X^X?BH@Vo_t#UJ7N8N+I9_VE4wQP;k^Hu zSlU_ThttmX-soOz^by@P9;Rd_x(F#GPyc0&l-%*qP4OBrEy{lEY596?-Z`MEe_WP@ zSsJ^kdyLezJgt3VgI`4hJ9f+;ooyj}6iws_Q0drUvx}5Yy*Bcu-udbMzoqfLgJ2vi z?46}of(-2Ux&PRLL-Y0EQd>J zpzyhBuylOc@)dXih`oWv*8SSU7(1{DdN?NMep{QEMcqASwmY*>ZIM1qMQ!H9U`Hii zB?>IRIAoq1afeh2xHh_hAypRL3{VCSB439 zpa@W13Z~yOJ^l|5XCsVS)+CHt74`_D@^`|Qyam1rVWz6lTHa|zU6wD{li=wFg*AEI zwL&FM{uFhUU3PZ~l^DmU)g##DP5527f!04-1^bQgS!shW3w&nUT18<&J$_ZU3iImW z6KoZV!W3F%OlW255|40GD3UHXL)s-kl}B&}vG8g9It!)Gz$XfCwDC7;SEeixZmNyg z1ZS=G@nW+H-oG{oO?F)N*AZS-kKkgP>qD@XF;xZmhUXrP=NAiLl3|;y=G!Ul(kYx& zJm>$!B3r8cCoI@p7C_MM-Ghi1%|MJl` zSPc}PodxBwGX3@G_Qdz6*{Omos358L3Y>-t1UtS8cumiaSCCB=IH3Yd?-k_rT0u^{ zf*h(KFEQy`w!-eE#BYML)Uix(mOF|i!CBAxWy*^h*( zGIS9ToO2x6@a=Wvz&9vO_+HR$eD4R>k~Wu zIB252Q{ogx0l(pDMIPYxnJ@|%55KPeED9s(Z-T=1M);KmpLxic!SH#|hKDvl%qm+g{Oo`WFiWU0QDqET`y);)hF*x#=+Gk-$nFqsg`N|o;{Z>a zx#~|Iao$Xkmd5`C7r6K&5WQA&VcOH59q_0t(KAApSP~wIif#Q@1y_w1-BYa<*XlP! zVQbLGrrL1J7c8==l$#Kj1lQxjeNfD^+J&cskyL6qwzul5Qo*avw0l^{eO^#E(OMb( z^n*oHxt8{aVEPF6qIGOPI}G(JR`|-a%uE+8jjL{u*kz*oP{jm5Zmj5@VXYVqPaAw7 zDVDw7l90k?016E#(d(>;!oh;P0T!%Nc)B{0Ob&(aYEhjP6$Q^bf~V2LPP1x}h0!a$ zY9R)|Wr~&HW{S~0!q)u&)jTtu%}fvc)F!$zaUH=mbHEbef$DFB&p};15QhKt$P3iY zsJM&5sLS9<2ur~AX)Hxgv1q&md^#lem%_%gx2ckCf1-+1|d#6)-iRfuCdH+`X>7BCA24 zy0|}`NAMnDwa{V1xyl_nk;du688Xy@EGlH-wj)5Mhdsr)T`g<{ zR^SpJJ1uN2g-h(wRiCYrMApya{#NbJW)< z?iyj;ISa^r{nWhl;lk4)oDD>hX}?3X?2&X&vV4)<;Bgw0~UX7lk687J+ENrYiYOGps-yk(q zGsgBuLPVXANkGVWBIMeO5i-e0h)x7f7YilJhf~!lldGburid)X-Ci*siOP}Oi>wt_ zL)Wdb)b&Eh7fA_(^iG6Oi5Xf6)FiKCik=ZDENndhO3H(+@>i1?y5= zFwo&v_~lqx6mHq_^QNTa3s2WYl6S?)a-S8XYH9}}4GlLsvJ^z6*>(gExptFsBkIa_bB7(l709aZboyBOz61kdQghzj^o3fqE=w zJzx+xJr>Oa1LbH^D0*u!4b%i4quKBpZiBgR5L67I4Qgq_UvQ%e@DfxEm@TsXt_G3q z(NzkaZ_xWX3q6-x?Wz{tHKO3HVcgNz9a?j*=ssSSEV|E^-lkgjnK*7R^IUVY1PxH- z??m~!&#^$LS_PMD-*7ZxnbM5M-XOhj3WzQn85>bR@i)PzQPy1$z@!7?Ewjj0n{PKq z(qp&b7jDEZO*jgp?YJ5DF8)d1f1~d|-K@_k!M^tdWY9jcEiv=Y(tm=HyOW#rTO>^M zd_>BP!mu+MbC%$!Hag#idGb7gip6>oxf0xib?Qqk(t4QDE{6Y43fn8zaOGdi=C0(c z4+G|*@-MS{aUJ|oS_g0b1_lDQ@JTS!vYGFH+K$M5k1*q?AiW5%?vh!>ddyE7s5lIt z#j$LqK6V_JcBkr_m|%C%KD@y;geiZNG7R~4XnSUz2c5C7P7W-5l1_0jsU#r%8wQE- zS9}YDgzw3KZ?T8n$vgnf%kJMr=~M&0mp!4%m;wi+P0r1auXB?CGeuUV^ThCt=qetFwXYY~X!@P5`@aqgB`0 zeQ@xReyew4Huo9P@~)(&r=m)@fiz7Rthj?{#oYo{9BIptwoTT5%eVeecdCN90r{O2MYN}4lWTa(0(Q@s@XsNXrXweCg zxhWu8b{D7v1_S3{1&i;JbS2xzIIK3HuAi@PM=!>YEA8G)Y=qQ6LeW*iLzf zg+*HIEot-y<}Qv6MRWq@1sY!uqB6jt*ONCO1y2yjtn4t1wcUhzh*E4Lq85|z2wRWR zd7A@mN7+#Wd3;D}ub7DBjV1C%%b`<1Tf1n9$|Z+=Y?@9RpLUp&^d?K{t;s}nz;GaC zvcytEccXDc!Z8t>ENGAc#T^vZZHrAJ3=@d%=86$fg)YBb;vyF2TdafhSUNzFt@l_? zcmj7auFt4#Xdp^A>jRO+%m<>*_&{`;2BO^_JRc*%h=YmWKr@K0`tA-NAB|uz_XgSA z2i?_c3MKdcvSi8qZs{}yy$ht5ai$sSu&UPgP>H$2Yp(O)Qrj3BlkWVtQ03@E4lTU( zv5LEBAR0(Jqqt!?-yZRKDUtTVTNCD{xc!ssb2TY2lLn2X=x)3N6@N|7ZJ(9*!R%IH zhqTA?$Xioz{)aoXJ+>Q%oP>l&7r+5k{4!3Z>%6Wb2n*(%F<5_B$YnTj_u`r8z(0p@ zBY)dW9Qyiv*#X~wr0d_&_p>v*7g+OY26T(=Hzc#~vj_4Zm|DRw%(8Wj7LG=q$TytU2x?v523dtLraY85Uu z=Rb=JejDIx(<)kB8};l`>SxKvpNhFd=aB&+v(xdjwmIFIB|i4DR^Y_4xNT92yB5o| zqSIg=l{@l*ccO(aR4j1h;Xm5Ky~Wll38NeE3%W!IXSC(eaZ}JMY?){Y(%B8Pv#ZHG zodH!)9*W9uK`XE-Qb}EeM)#@C$%Ze(XjqG zbR-`A594$cO6+ZZsgCU2-hz| z+S@WElzFbL+_8$BA7YCOON47f!g`EjCPwRU`JjdW>j0Be*jNIr2mR`gnoF0UIz+3a z4owumxY$hV2(+FsdOv(>fnWW=N-1{~!8bvPxHXCgT=v1G5;_TUlXkTFve1*oiS5vBj}4dAxof@T zP=an-&?b2j+lP>HD3*=Hb`$dX!YFAinrEA|9bbYeQbX^wHOVmHqGUEuP1`YgpI(M{ z=Rl1+$(;&n#0h(MWmFN?sNDmVt*zJw@`agu*H(n#>KA-9SlTSB&;5e$<^Bmdj2$+B;dNGFAsZt{TO~`Vl%0<`Gp{uD_Y>|(Gxv(2;gh}X5&&4 z<};c|J(BT|D8^*PoCVQ>ys*U>hiI6 zS|i3R+pGwN={(xylGcJ*y_U|2?AOMhgcsfIrORb^P3iqIWv%7@=KfHN>^^{;>Lb|A zLS~YXnI`+I?QpZ~6PY#e0wFA5qXH%>5X2myP{7?;I!yN0wBQ945d^zM$V?S7`||Cp z&azJwHc>&}NY^`j#0|tV@_-Shgg&-G3SGxa>C z^%d!uHi-LXbPpZrL1~zttq($Z+C68_MbSB_AKy;}X1<+6t|SOMAT{DeKBghX3mGm> zkb;02j)oV-n2wV=`ZAmb*#J%>uQ*e~1&&l?PTU+(dNRj{ak+Z(MzMrZ#pEGV?j@$2 z=&ts)lRc}wHJ(XQ$^7gf|b4!d5%GP$m5t-f#yHN3Kc2Jhq-sgkNq z^ef}^aI8l~yU54(Kt0Q}kJ_=Gxq3BKblRw{Drl%F%xjv(&FYTe{j%W~r0 z)Ix4F_ZY3ZxhBY=E-N1O8ZWy~3+wNqtdhF-@=G_8G|KEFeV>LK@q@fDZi8GtDVlNK zY@g=TYT;7Y)q+AfQFa}tog3Ur>WumC)LiH(ccq#8afA3~fWGs0B?*;PR*dja3pQ01 znmp)GT{Vp~3@GY=WLGc{5N118R0;oQb0^k-jV>Fa}V4v9k@htt`gJj)VjQoH>ctO|$h8y0xe<_%P^Ug!T zr?*hSYF@-6EI`gI>VSf+a51l}2rGeSSkry*`G9ewwsUZ_d{U3dXHyS?n&<6DY@g1eS%nk5DQfll z5g(+<{(_SV*U&Sl1)F?PNN|1wH`?@hfJR(c34JpgalL0b-}g-*_=J|2vNgQzv&{d(x``80u$?I=l0c+o<- zm7fi;vD98DRm@A})aK`#y=ul}PzdTx`5slYk}ljniaR1U;v8YIJF*`%MihPjKnOrN?~ui{2bHXVqVFaNL+179VZ;OpMC@b&LyAd&F@^ z63&!>kcZmNMPEa`?@_1jCQ>5KZc>EGdr5WlMozD8fJ?MW;6(HIsD6T6htl@~C{+ni zlD`+q*4|wAeiI$|mw(~pxBrKUbYATKI)X&5at^Y^+B!Pn9ae5Q+YU!0)kdxY&} zOFbKK*|egVCOv#=ibop$)=Zvsc%Z-y)19(aG|z;lo`BA1#Xq*K{|0(QD{<250JW<* z=EkzZ`9p1c?`D47cVP_~Pv{6oURvnohX^jye)e6=J~t(iRbJ{?B{;F`Nn7AoIeN!{ znZ3Zb`Fs7F>bVcfSU35ggt;Eh$A3PW=FMHpy`A7v+JKiW7jb;jCY!mH#=SP6*$ZRJ zJcPA4vF`f|^@jipwQeZ()D}ZPuveh()`G)uRrV}i)nZo<;@3g=Dyl}# zXR$EaG%lYC$}D5~pcE7QI`Hr~bm0Lr*CIH5Yx$aow29U`oW9Mt=7BS?Z2-6+pX9T7 z2~11)DhNORB2+#}M@V`TQ{nLk@L4aSx^6DUSs7Tcl{_*q^=4IY)nJ*Wxy==J-8;hO z7OfSQZRVP>!5#s`WdD~qA-v|@fhi_wY(2VcWP8~WblE`fdFXvs2h4rRIb+)~^u9eq z27LTH_tfYJT+paUjfVZ)woEC&YO$bHGir-9=%XtX!K(O&JoN38GU& zHu^5up@&%=vVV784c!|X>~Z(P^MJZ53_5Ay=%9Njw6EcL5c3tV>e*hIwQ6g+qEYm6 z*-rlZ4cdeH??2N-^*{W`5Nb7H)EkasZb3qeRFcOmTx_J64sh`l1!si|uBhPRkF?qb z7k{TEI1g5b_OS=6qJ_JI7Tx&W=-?SEo3vf}>eh-Z{C4BlPf2Gt5xX7Q0znv zg`S!@WP8|)vOx1;A34l}51}5*kIC}{iBn^1C z02%ctzGDXi9c`YyA~(;-Y(9dOe+F8qsa0PX0Z>BG5&iWd6!&>y3E2l46Ro_Wk6V#; z8OlFT)S`_g%$u2mtbxLHf-}-e44(Hj7H<-DlM)#;?-brTLa!8QgbjW*;vyY1P*F22 z%w$u2@DmNnISwbc>Y!iQ|HG*j3Cm0lpAd=N{s>9Iv5NqLNr=R7zZE-LZ%%YCnukWC zU2iMGK`1&vkfGPkd%G1`g)XBY2DD#^9!Pc%=N@wB1Yf0tvIRNupTLmJk?@&L%Kt7q zelN~~8xK;uOh@&}28+ST#omKU{k$VMV_m};>nzS#_i@HLlrz@FoV?Rv zHDq5pdG90-SL&GPshI`VL3YejlmDRtgXsjrIe2^WQO1Kfb61Abg!)2(D7CQq85D$ACT|N+o4=$p9BpAN{1%%==Dj?p5$~_D6{0F z74hi4`GS3hU9cwsE0^2h>oWNK9FSN54Qjs`9YJpgFffcm!%YQN+V9B|MtL3Z%8VQw z5Kw>`;ZZi;#bIp*B7cP3`^fhd&HM!)z_{D=^L}ON@W1irDP`7e=19`mp!^!whb=eC zft&IxgQbPkqV`;LlOF9Lc>oj^>>(LJtUyLG@5aWwEf4)K;lhkVZT1pj0ZouaY;DCR zWQtkMdui2R0dyPg@{MA@pq;bz6F%*U@NPE`Cs?ge?n?mVaFFmLG%kMVncCTk3;lr0 zBIqd#+7JQ6;%>xXULH<)P{MKg#&LYeNtPP6@d8v_z=X^+xK&Mcqacbl-tqZ+wfsHf zEAJyES^S+$F&oE!I^zQ~jF&Ej(KqfqP5#1#@?*QPwT=0C`T8gN=F5Rq+^@pmRIx?h zSOQ}^48}Iv+4);QINp3To`)9q7xG0GBqurw!Xf2GC5*}Ra4K$E*9%YFpNb1PudJn| z^$9-qFwO9jmmVVfKyjZDDo5fxTwR3HRgkYemBVNKLghd!9^QUnW_#S{5PqeQ>jL>H zf}MvZl5eM1mA1($%rjtI+;I-aXggLW+hBlxUwC|3!%@x0Q&WD=(!g{&!$D0+9KIsQ-sn{_4#>>3-lef1tx zOf{gxrL4GWaW5+y69c$wgsLIQD9lw{m~-fqPyj^F-6B*@q79)XHjjHrx}eg=I6E&1 zThDd-->_lsMOH!m04{Nd>l_e`PD(}Xdi((3M9qaPF7IYBH1aIF_Ff0^c^CeJmY{4iyxV)&nd? zUqq;(4?GoXt^K0W8YFAu4Y==tOcSHKCCho@!^SkeM7fPx6xS?|fY~achxVPt$kaG9 zZNyA>Sg4!{nE5A})(r4JVl|cbn>5e1Ce1xBU3lVM6K#PMMVesT5QR*uLqs0g={uXg zbMP#=4+O!6Uv~V;QqX~yqLh0BlVnw~$$rgT_DyqN3$0ViY+*6Z!!ir(e80IMC-NKEtb`t>9c`ngmuv9Qg{*%RWy8M%5S2B2RIfEb_R4+xS!M0c%Y zzNc0X5FoK=cfDRfJW#Po4lI0D4peNG17%yJz-R~bHLnQD&gz31qK$Vwj5&l?%kU~o zuuGe`@IzPMEFx7ok%^RX-3%6i9jyZR@kl-1q)iFj zYgYm*vXsE1+3_%jB1V2|k=Yksb~-o6eHlECIT$1WmHz^CBBix&zRXvQb40W-mw}*w zE*Mo1#wjOU%*6Rqem=S}7HF?x9&xRxznQ%J^JKNC2q)pPxU}c8X39>H zkE=U*6bizF{!6tohBA6btHJcQ{H$+Pu=G~c0W$92?fe59^P2V&8zZ+h%b~MYjN%^B zZj>#J^1TO(end#fzrLV{gd8{kgA~*n!UNhBcCS8Xc+^SjhBf&=y#)_Gs;Qredh^rB%mP-{JnSJlDexDWNYc@KW0mbx4Cg-g!#|ne_{uutCk%G*MN|V&7mu zQ2bw*%RYryN6BoEE8;ClW&6ZKQSs18!hkhOSMC=Nb+Cp*pNK5fw^}q0!wBcAa8O@H z0ncUs?)=4z=HsbgTWzX-Ja^WKPL#*C+}L^D!y|g0*JYV(s?80r6N~CiKd5 z@(bVD%4eGnJIR~1=_JA8IR~6_<{o1Rm14A zvahTT>7%%hE5g*{a^QK2XsvISmXD;6SF{bT_&-14wY2!uCD|zY* zPn${o3(6>L9~OfrKDJvM2e&~zH25$D&-;kkLEOCs^%2XL#r*|qw5xX;PvG(McXWp7 zD7Nh!uYNqx>pI|7MSE^kWcff+es;iuZ37f&KtU5wb3zHM(%^&tn`Q<)N$@qteqv_8 zo1`hK+@iP}%BtzOpbw4QLujk>c)&G!@+tW9Z0J5;m5?Q(yD3-s)Z#3WVNm&e^gN1( z1Ip9G0m&GB7d`7iF?NSqOeK%@S1f#qGEckRigq-V_UdP~^zCaLKmo~oN&E|*XJO3 zedB!;m~_2%z4oq*4^hHjM#K){1+oDKT#|_6e zUrG}RY}}WBVkRjlmRYUOeZF*<6qty)F_gd>kffC$Nz=0we_b{z-2BK@w8x{wJXXTz z&?SU8QSPA797}mj9O$>B5@U(>x-)l!_5~mp-6Oh3J7UZ{D1P^JwA%+dH~;7k_`#WMuSE` ziJI2vYK_E4=%Vg~U6_GcMHECt#l|95v?#j}%PV0M+4W`=UzPf(^kJo~t@I%VpSua< z0SEz90<;nz)d?$tpm~5WzjMx=+3bd(w%^b1^ZVn+ht2NH+&TAk?s?pM9{$oUb-b=r zwAaR#-0m;k?#C=`u*fizN+FEfk}-f%3TyG(zry#(t^?)=1NVtQvMsU>S)vFKH^NqZ8INbhmT|h)N*~PJ%EHf3tC{uv|$zKV+G{PSL}qI zTSPtX_iSGDS6YP7WoW^ohb6-U2PvZDw)#8=0auMgUFmkj7?;zq_rZi3n&k-KjeEZw z{!fR*Hptb-xLhHJHw+Y~bMliUSGJOWUd0{X%91WemXwRF zjk=}yrx}v{YY_W0D4MMO7)M!hXO1p(rA3Ur;|Xgr-#}p>DVu6U;xNMkJOD#JS{r{h z?gCSLE*dkOqM~jqLQZ4%rU) zx6p~*k?mbPDDDhc?L2_St> zIYJquCpm*TFxSPhCl=aYntEnu`2Uk^(t+ZDD=klN6FMJa>s2Cw&dWXEDJ%LWNj z^u$2Si;14Lr5B?wa)3>62`_IAxHs818uFS{5m!7d^B0O$W9*jDoqB^FhvG}LQL&Wa z_pxlz)4MpqM2Fsz0(7^;Zo-)O3cJaLiJshh&|#%T@$8y^LDiTXOXzeR-Dge&NI%zD z`GvM}M5_HeO21H%#CHEGz;>at++4Ie_OUkfz%64|Kil{#iY4wQ*Sq3vJ+EPeY0R^Q z$f0GEKDyp}Gmn?hpW#)1-cF_SSOb$Czj8M~i80%HMAqZ(!R(V|BHIPz_lg+}8+$Z%?P7hH;<7b4XBs0Y65?{G&eckJ=yc6a_Dh6y{+u1g5)ZEeB64a;*sz6jf zE3FEE90Q0xb%{>4xFS2YLd4d-YvtMH$j^ezQ4p3-b&T}0tq6#{ukCQEJ5oQQr(8S`Q1ZGZ&oZv=;5i<>ssn?Lzp?*l1-ZM)W8iVf`oqnS^o}a|Q zX^nIWzJfMdr?(#VK@omjcvzURX%<~g{|L(gfbDcpUPWhr)3?E=h0dy`H$b`lP#(%m z@8Wk43m5n>3g%52b+1`1!o}<7p8^Gr0-ywZABOLb6E64*cV@tyX>e!8$HMgHHP%By zo()eR%bnOq2|GL;&FS(69WrmyIpTD*r^`EqZVJ=aL#x)2 zoegFM7K&Si8wUS;9of+4O{A2ag|a#k&l9#8hCH|8DekRK!!PBgR?)DWY&cmAo>8aq z7(7DpPCVs%7i}gDhnDSzL(4Y)UIUr`G@$uUgLZT%wc^&8KD3Krx`P&weerzj85-Oz z01i5x+`qqg)BV@8Q&aZh4u?DB(7UGwD_;?Sd9pS?W4H5O2AKtCQa6zMDxOK#5@;i+S zW%lvSjVKNF(1seGIpzVWO}wF9Ff=vih+(fX2ld0C5kxjk&*w*gX596YL|R{mh^oD- zgY1pbH`NGU%Ta|4&dFuL>mn=7JTJ7v>xIuG_{@aQTp_OtM_S6@@A!(GrrS>GeyQRL zrQ+v~04BhE6wwZIa$i~hdREf@$X~h#ojYs0mdpZ9F>3`o12~9+N%yiSZbX|x=HcTF zSIqhMawnn-D4AF?O_)-#F<{Rv0tRSR9h2=bcVjq~Aqszv`6E^llfyJf;kZNbyuavq zOrO>Ss$XTm^Yx-?+F_l+bMnna$+a?VKcG>2mR9Jj;IsDzo#1TGrU4bGf z4eoOIc~kzcN*ge2a)apJNR2mtGDBKJ&yK3Y^A!wu-({6!alzFnX?uKJZPI--b# zB8j}v9PmLg3%6>=;HFsOT&O!ArWw8p@Zb*c#sMD`$-KbIG1kEtALg7P7Y#XTBM%KZ zT233@68r!G*--@F1v0~Y*)H5SG1~6~DXQRPI~4_rD!crppWuOur!^>mBJHym^61?0 z&{w6g()~0(&^UkTegIJEQ|H=5%K6|E{=DBe`p3e<31n01e5;XfknQ!eE!}&JRIR(p z)iLM#Krt3sOmsH^BL=zCFI@A!Pq?PV?`c@_1HbTcyXa|MnvL;Rx_f;LuT;@0TpJhl z7KVkE?!f{>4ft3}kMnX%4uZ5X7o-jJnggV+MRnD$`oHnn8~x}A?6rED7R`amp;1sj zg+%6!U>j4L;WqJK1Zsn|Ww8dRhqay9n1AEr(~{?v4b&Vexa5$}y*qr|Dm?s>g@QxO z7S+IU`zG9R*^D^|8Qz{zRO#L&y7z+QW&f9ZBj&|T$!{pC<3MEE;c54&xdkACzsAS< znl|_}LYqAsmz;|p)S{;)rHw6S8(XoB8?lWoOHZNvoqg?VS#rpJ1QXDHVGXUs8DmYt z%gsJ_dm`JjsA%>QyQpshJn{+u*iSZ-u--ukg1Ps3ek|0T%X9fv>T%HF9Des_hz=m0 zpzb(R)VG9tZ9?6LWK+4m;N<-Cg!ood6?O^cR>Bae><}g06xkjTy)L)N>JpdY=@y0Y zI~H7rJmkjhhlM5C#Di{c0XzkEcYGJz!UGyS%DTM;Zfpa(6xs@Ty~+Vvh?qL$hveL*oZ{0zZMXO^Y8EE{us$D-^OonE@Z#zU`st z__n9z-}+Q^e(SsRbl?|ru~SYzYaIB^Tv-)iN?L-a8vNXEx5`AyTS8ySYPm~fI}}e_ zFh7aU_ETiQ7q}PtN@SbV>EDEp3!oeNSqp0ro{0P0@3?#2P40KX&ENRc{5;Vf3;m=P z$ORDCMnT$YWQeu5gcN`6c77XfYy?e2YJ|I&6Szj<`Is2)u=+f)U{UyZo+v!q82-pA zdUgbdVoX%b0md*%1BM2{n22->Rp`{496ycJK3+EA=JH3=Y1Hs z4jqQqO4hDj@Ym)aj`cg~!ajU^_#=ler=e@t@a?60!XMe#Cb%k_<`ABY`*PyKS|>gM z^vwu=9+XL*BSz@ze(3zIRX`pTTfB^mN%-6Wb?nW)Xv`0t4r?Ux87I%+#V(O|o z3~cBg6z+weJ4v4&@9f{L!oF^~s9SL5z$>SDmC@H_=pA0zsK=SYla0QdM(l3*BQNi! zbKXyQ=fW|&6dmtO>7-Z{b7QwIIH6l&WX2$x$4UN(V^GQ~AY!x_G!pxZ{oJ*rNAEv; zn3Lq=4C7F1_69oT$~(h6<@$0E?(eEYo0CQSyvP>?W|1v}`u5AXXg9@36V}!jTcCN0 zY4yc7(0Y;SdO^s-4fsn*bn~y0n*D1qVdv|Vcq@8Kz3^(r)P^j}EOba*n-PGY5&v&} zk`E{0g?SN~Mz3(fV zi=n1+Ho`Nf`OHRTU1sg*>j8J-A7((ILH#Po=~qE+Y6Vr5Loj)7lbEvw9t}=~FR5Mu zLq(>{RHiXWQhkZ>52gvG>i13lKhUIL7FMAS(*=vKVR|Lsk=#Leq>E~>`%=ux#-P6Mj`=-z3C8^X;42>Ghy@DKkl~qwmQ+KA5}C~_ zGhJ>lmh5e`evq{|d#wSuvipN>2}JLw|k**UOgtY<~TG7Eh`c>c}vX z>>ZBNih6danRRcGQRE@20n_&tQWdk(tGJt25!3#DFI90juOg<6$ui@MVl)d(Yy~UO zKRIcXJFRaC7K89QhtAt*c+KswuBAp@`RWDpR$VZX(aj7%F|Khp?-{_(#vj04u&-zJP4bZWM7W1pkH^-Ujs zrDlOS2o&GNDgJxcniL;s%G&k_nd^O|aI}+3fvVb)cmncqFFHtv%bhj@6=x#`R&rGI zR~eWnzritaHelk_ZxAN_%o|x$bhm+tvjP1h+T*8Crq{qqpaiRu+`xAK4}crkpgk)k z)gQs31lr+`yjD8e0zmVf0LW=e6cIzPafa~q>jp#cnxO1 zIwp(+{Mu6g4$Ont-4Wks=cH53b=g{d7sNqaDDg)!eXOB&d+2>mv{y^f#r5!sK|_zp zZ<>d^isxWxC_Z|NbKOuZGG%oqqoyuCCcBTzo@2uDRhSzjth+z}K`71s7?#FRY3SDz zSgpNlvs#*bSFVPs4}tKwxIQrvL)V432(stv`57|%I+0s{8VP3yC87MFq)xTci?7&? zR9@Dddn!rqDw%XB+fn5FwRM9cQQccyH|yUBPr)l_n)pn6uw%f+4>U zhEwPp@*{l6CHpQu;%N8KY{AJJ32R`0@qR1BS#^wPS{Y~6tG@?H+b|zVTIqh=l`ie# zX?a>S;dH*{+tInPwu!d$(aJZAOemVi7a+Qd5rO=PvBpbyjV<3f!@aoQxc+lm8ts7i z9SEt(-(sQhNC6f3PEZ(MLVMtI#0C^H$ea6ejniWCxvO4}#53J1d&)}1y-)FU3d`Dv zC_z2p#j9u~U?c<3^%r*#&1pfxTEfq6qBl_bwTQ6xh9WPXYwcamhr1(HGHUrA!#y>G z!@WLn^8O3UE<$_O{$n64%hONdM%qKBBi~?lS?v#xb&=s`(<|K4Zj$y%Jux3OH`};z z&v6ThLt}Sl7C+S zp=Y?_vQj&cLlP4j6jo&Vt2cwZ~az=>WgF<3>8W?WnGlGB5i;2`@e1?O*8~7FSG_$@kKyVL#N{n zk+M!J(Zy?;{3|V;PZ1vuT_m}?<+n|iEql6!5FB`0*D< z0H&i9w`#qJz5>_8R>~k6peDAGOdB_Ala6*1xmlA+AXk1Tfy6^{ZQ5rR%UzAD9<*9~ zZjH;xB1Mc{sf}JUkc^BK?md!P;iV^5cryAne=gFuPd??**Cm-*lZ(wI)IDkEds_c5 zYcH5{khS>Ayx&KI$?Vf$3R5*0SE>fXYZ#oY0bmO#QHnY3eXES?GqlfEebn+xjMzBL z#^a%s%~~PrP34&OKGYS!`KMv7?CsWBnmE$Jo&O?bWGFG|5I;YG zTWG`vV8T7f{`@Ox|3Ex(1Ho8ztzuQYWPBvohWiIT+(7WJd0-uGAlzu>Xp4{xwDqsX z{;llk){}VsKVVbwy@*Y}`~*<89($@@&lgqIUrvU&KkWG*5%8vTx}Iu^e~e23y*x@#ZdtOI|R6MJM75j*LQRwmk&O~x%{POv48MlGt6RF8gM2W?U<&0 zxNm^%lM8t}>a}^i9T)O;)N9ASCK=&7^xLn!N&S}e+D_7I{iUrEI|Xx}N^Tpvq1OG) zl>U?Yo5Uu-(?Q{0hfs&{7)(yix8Yc;uS~L=`{A6>a0M`_4LZ3t=-|*mOXKnV{!9K7 zQDDu%XBFv#N#=r_=rnw^j<3?dDqkcJ>3oPYqGQO2kp19Z53J`^R0!G7FyIy|lk6v= zHGW0xCt^uVR9Ke`ieSLgJwGGBx(!7T6i;A02W74ZMzoi{HW^QZKt)5V`WR2>E6#Xm zd;_@Qx02MqnD`dg%&+|NQL9DvG~Hh*t6tj)x}c z63zXKNz;Kwa%G1r#}Vz4w*@p6@xO;K;RPh`U zmL2BW2G=i8+h~Dqf)c(sM%u<|6oyT0gC~1#F|>_$wEGsODtDKDf8|b_jcuK}*VtJ$ zwi%lIxUKNt>n=|dH0NGifE!GOYs2nuRk*aF4|0X%vOjW#WUhAoOGID3(VfPPe|>48 zLy^C4%rNeh@FKgCMb=>vt+h7A8C!X zR8S8X3K9V{^$?y7VGWT#*iw%Vdj7ZL1HW;6&|))>4|>v%51u~Qt%pB7mOMbX?}pCw8?MviTho`^1(KP&YnXmL$kl$5ckO!B(O@m{s7V2RhERD6!V)<%s-eS=6`8W z+f~|C2Tf{=E~C)IP`v>RsBIR)a-3x;-A-j~zdfb{L2JPiHieBZkUcHtwkIB~?UZ34 zvE8TpV6>ZjgRGWBzHyFbHO|p)`^o=!GV}f&xQ2au%ncpCB@q3o3qA#cD~qlL*R|-x z9dIepJAruetseB-!?QrF?Lw+e_*P4M5iRXM*o~T38W2u6=s2kEeNH$coN)BduC5Pt z-@%V?;^f!0`wjW0iIWwtw(CVAE6a{LjT=ANk7VUB zkmNRNbE-L^y5j*;%sKMm0OFQ^o^eW4$E)-6>SEey`z@CK`}xi(68jvF_|c7!+#Cjp zEQXGVQ#3zvuE4=?k5#BUXrp6g*@{@;bI1KPt--S;sW zmPU~TqWAc_ldPh!wzX@Q$TpFH&4;n|?dUYli&-Tth9nLd|%ymNR%QSwh`&TyM-sYiuUG2EEP7 z*@hi0S94V)?9kMPmhh1*Vfm{#l@$#zYggaWrL($a>wgbS!oE@eP*DfxM$ykYqce*0 zOQWTozrX*GL%B~Pl;{qM?ly{Q)`7~chjNL(!jIP@1o$N=op=GRU&aSd!jC`j2an@} zI=KEdTz6v5QuzHCf3^Ugb>5>B`NM1nd3H>rAT^Jr1?7tjPpK2lJg_<98cY~_5IeB? zKm8llS7?-3C_GT1G>ox+{9Prk-Frd_b?8S88C(;&{rCL*tH;T&6P=vl$ISv>UZAtv zD6r8eu*xhjk{4((3;fY2u+%JY{C8B~MYBM}C~&J;;NQH!gJyv{vA{dO&9;0EpEG}# zZTTU5E{D(6@VOQ~H^b+<&gXU+2HGHRTz07`Z;VvXPaaS(@H~7zD~(0{J6T)Q{|t`> zK#3@uMJ|(CoBcbHrU#hcBC>!p8hBA;UZ*=A?y(DXi!$*^i;uwqy5%Wh?PBaU4$y5vH$969QDMyEW}d4C2j zY&3g9k97=RSmw3k220m1Pa(YlKyg^llo=U*`t7mO)-uU3a{`Lsng`PTI|14aeg@|E zDebx4y55;jV0+FK!sxLjlI_vw87AeR79yNXYEilBpq%D8ZF1P}%p$we@R8~DWOp&P z#kNsqlU)H;4*a@p-r15`PKHA=YvBA^X0}MKtH}ova3>z`LBCu5!rJDfRn(uS_voF= z9Mle{Q1@oC9e9ET#I>$24#vgWV&%ogi269C&YivXPnC$bZ`hMxF;U{|n^7lhdCH;8_GP57#TMB205-s2V0#+6>ma%V51i?HOeB+P7-P*!Vml zxo|8l6q)E`Euwpq=(b|O33PFjD&jN)L$D9f193}uLaJ2R23-S!k8Kp9j783k|CGC~FwK{l=LWR{g&%85KE-)M~7R_&^( zc>eC+N{kvR0-E?4=AHNzF%Fy;HszTVuEW_(AMa6L=*5uMQE;A2xC+-^HbTWR_{?vFgw_ULy|uvgUb~IP@n6 zocwyjhn$5!V-^_33pAPqes2`my)uj6=zWF?{MIZGHVWKiLTKj&s?7pF;RWE=SvyhI zzcXFdU&jF-&{jz#>)-T+lV4w80JfNbmvX?xCg5zNz}|I6fm?ZjEVF>t!3(@)7PyiZ z`1A{-K(kTc*i%O5M(_d~%mRPtFx&o<_p$BQq_^F8?|}BncxwA|`;E5WYXJ6{H8(y@ zfajQi14e;!%>uvW1tyvWE;I_*%mUTCzy)T39HW3}wtXfqkZl&&x0e@))f*75<^{C< zMuFD7mi`7(phig&!-EGOV5!e-K|kgaP;s5;1h#$2nWZt1Ff3)zdD)6bGkPngHp^-7 z*S0SG80a3zr4l0C#Cp^Aw)Iz3>;3zk#*N7-)ehiKKZ)(eY~pORWMph8A#=Yn1x<`* zpAn*T4ke?fjaWP=O8)R(S7J!Bk9=<}x$7Qub*^Vqb5GytEbo(lT*IwGFHmNM@DTDy8QsDHhsFVbwbTZ+Fi?& z*f0|JH%Xq|!Fy@H91lsvs>(Ag!6NwSq@SUD+2eJEhR7@yFz*FFhpWWdbPMi0fTehR zigXm#8d3P!m5R5ScKZgE$E=#=bt@s1X)2F znZ!L!p+brlHDP02RNFb2hMQV^s^ppG3Ju1+9B45; z+z%Itk#xsN?L8ISOX|E;Vl7nWOKMWh0*bKLoMseW!tMP_V@)F98A|_Au^=KAV)#?| zlte@fj_QSv6cH!E*EIOdgwI^~%%%+X=q%=)f*Qx)42|QE;F1eK%fN_}=ivo9a0UeY zg%NfG-S}%JBZTrP@(jXt@Dg$oVW+qiq9AjgI2rZ0*xy#+TdFu>}Rk<_Rr|LN8QW}BLFfEv`~ z|IF&IjV+mjbxp#$3VB^3o8U;VPZf2ewlB!e)o36p?V+1s zEZ2arm}kMU6fq46MnD$Ht<^Q51n-o<9j{0ZuC57_@XjQ-GgBm2SJ#BOcxNukF>R?> z0&|VfttNEYgf5!UWfQt+LYGbGq6u9-33QlP|6YrtPR1Nr<8cEh>c3jJu@$~{g7!I0 zI3G|~owBpZ-)*7OBSn>q*>$-XoC>%ip*O4pj6Hz~rcQ72p9dHexL84JHtkFvnPHkx z?BBV@DijAk6Y|D7o9yt@-!0@#EPz{CaH|-uPlxL=xIPcACAhu;vtKVnM>`o}g(+d46OH(Fe_3CrokN*$9Ydydb`klFD> zw(RK@mdkY80c2wgY28Q%X3Nz&ZFZN)>b2*|o~^=z=inK|vkev=scJC2H0?Yo{BegA z9ovJ*SXtVdI4@AE2lG)NzYmM@tYG*cyR;Sf2YNOV{E4dL4w=0#yWb670hMncVq|Ze z6_vhj@^)LAykA+QW^WI*rJL{7!pyD zcsj-bM@7w_EX6;vr|ss;OzKhGaar~9-TYAsi{pm=;6@ydv!Jo_B{jEEieBTCF&1rf zoQq=Sqy$sbht5&(q+$MDUHWFNZGNx5d2|2ypJDh4lM@^UFhH7Tr3mMFGObaHnTesx zfpFmpg=<=%a82tIt}$2L1dRPG&qe3X=uMSM^jD}*!541&_@dQlm(R+BY{Fc0$dAL7 zJC5ltS|rA>r1Z9lRmi##s~Y5ZkB#TX2CRw&Ky}T7UdvLv!!AX`_)_BD`E#iFpDE^D z+Mp_)jq^`ML1QMG?y3_jH33#zVobo@5(L&G=2G6O8)K#0X+Q8fmlB=UqeKhLISt1P za9$}i7hnu;B;Q@_^D=v>&Wxpa&OD0k$v!c~SoSW8*BBaLs=`hQpx|;pX!f0o=Y65( z--OdJ_5pQ*1Cg)5@TUf9Ce-}iLi+@{_0dQqBC>_KBD>isGN5eT8ZhDb5l2#&JqvVM z)#359EWOQJvp-}H$8Ggfr%ajB-Dwn29h0HMrO=DukZ`XxG!ea|MUOpk5tRdoMnTbY zy(2UXilcVyX-*6-R~>$Gpti?6jlom#sH({Zd@|1}Hj6GCBRSjv;}b8yF4c-8buB{I zOT;O3wY(KZYyI^UGzfEKgelGe$f!pU&uZ|@P+?-FqFz@SK=Todwq*ofSE92!l<3@c zCHe#f?Fe|{!E%i6f=s@Ik4yJ#muR2l>KhH}keWLgp5(Fy`!>AUAV$6UayV815JK_r z>(~O2Z<}SdU3em{%{Z>>#?8>#7;GJRHl}mH=(uoMNP%%@l0idaBo@G25))@83rwN{ z5_dGhU}SG|hlm@BCXqLSB={P9>_BN3i44F#(Y2Px$S-kEoJ;)FCtP^Y=bks$=lN1l zG4iNf(WFFwPp&LV#fOUO@XCPmio0V4hNM_SW9S@PHO2|JvKFq!vKh|&@G(7MN^LyS8RP5%N%b#nIJ&j9;$*Qc$_LiFMp%KE` zJUl83cNV~tCHvu4HcRjdP6x~x3j^eIjBWNg^Kn$h;s%9?fgoalK%ZNM+H)ygxzT6# zk`cr54I>mBkJuu?{d#)N!?glWTS;3PeNN}N-U_FRQY{3WD^Q|Ku+Pl)n;0R*xlgpUfVvvT!qb_d6%B0MC)d zB-)1V)VJu(lHSPIR-!=GTcq&Oj76`M!t%3Pn0IBx9V=;UAj?v3RvsBTX=t#-8LtUGadalxHNO0W4$ymIhtWmMfe>f}_JUG3j z89rOpiN#vM>qyVwKZbwzl45K{2O=!+Dtz9?LWf@)xDXb25k4@KNic$s#Kd*=Tj_F?(~k=f(x)BD49UBZ?^Cwuo}Q2q=|nObSW?j5BEYPSb(0|oG68AKxB=dc3ld`H_p3$jk<)E4Ch%HXwO7_9 zYsrE-PB-g7;q7{Wtz!tBG}zx6yZVcABX;#oeXtJzY@Ox<+6+P=s1NCF{vP%tzeKp2 zqYvm#0QwmKeF`HA;wDJwl3B)n&{!8>#Z!b8Bg02@+%00mU!zT}Mj)nGIJgU6<@a8$ z=Vo&+CLy^E7%vi@?OB!%V`(ak7x}%r4H&N`VU(KtLk6r@34~yG}pah0;#(0!7 zu1(4r<4=$?#-p5ZZBot{pDJf?>49Vnlpjd!5MMW?4H7gkeg_I0?(uV_iZ;B0UrI%r z?Ab1;I29mNOy)wxc7IM2N13BS+0E}zuUs%W21^QUvxJH z&&OC?XN#)iEKkd#GmzsWi|@zwQI6Qfc|Ish%O!TN6W;)ZNn-CLxjL}fc&|;WXqP;@ z=4Ap`eL69AzF)!|=>`q)w9g-^s9X}F$jw`{Qp-1bqEK@0!T`kYNa~2^IJz}z!&^y} zbfbr}!XDvb{I!)ROsux>bCFfVE?R>Ztf(4o2@Y4#2odzSitF;tHScMg=MZ(r3e*aq zWC*nNJ{(BZr#u^jSM~)Nl}OZ0jbEvf9--yikLpr&BUUVV_J*{S;GW;5pO*kXM*aM) zU-5rd8Km#!(G>lB-JQl}Hj+OOG}vw_el!zx7m_vFllpm_iy@gO>gP>dKVOiesF&si zJYNNb@~;q~BeZVd_NOp4Aqc~`lXo>6Ip|B>c%fe4ZDiyZG zDYIh|J4^QL4HnK?VT36U(ZN(R4F?|;c#iqo&%$gqFjq^QFigQSR+!`Wt~Gu9Xv#>x zmg>#kpV&Dy|1nUrGrm>L9%HE4fA|M3;U9s*`FjjnEZa9A0>S0lzDrmC=;iv*qhpXX z|MX5n)$SJ`&QP+=Km{~T#R~0(0-u~zy1#V0SWlgTLHS7&kgdqYl6o5tKZ838V>)4x zNAIqbqJJhtj*{XV{WB3EV*pV!rKnGTH#Whi@eBY!hh}rdwGY39wKh<+BUXjAf-3Ly zqcSWyL76YW{Yy+=2G_-KT?W@>a19hll;CF7@wf{=g{$b8}G&=Seg`;B; zY+fL`Vg>&)u1GwKR`ahY;Z*cdEF!WhlIWw=D>?V>AD=CCX+7YS*cpfx=bX5{{;41N zUh50i)l|wkz`oSh(CPaYtvCQ&K%>8tzJ07s`*#-7?+(BHBeYYV7Ro8b1FSNb@EMH( zK)L$9m39rKtH;a|fOi!8G#&Z$jh^>}hsRjC-akoxBZ;OMIh{w~#`C7W-8IxP3dOgv z56S)koz4Pj_7CuLqB@LI~N#$;N8o^qyS z=7)Rr;F$w#7J%SeKH8U=8_ZYKM>q#-;KUre#VIqLG`92j7ctxwLv zK=^_2K|p_51)Q8`{meLXh-nu#DZtK<*_Q!~Y-p7#Ho}6QESZ~(>UFxJ<~}9^B?|y0 z)8oK5qPW~)*x<3?+lV*T4WY^H$WZLWdvi;-C+8ctZ#p(p1h(I(0jb@fp)Dd$B(t!J& zeXt9FBs-=0e#xI2czTZcJmW+dLC&JPO`9efH)yX3d}_Vk-h zm?fd+OCkdwtQd=)+(OM?2#~)+ z-hh!cC3e2TKCD`7v4jQ@k0?NW!pMy!G1VOMG8%FK21A}g=*JWRJ@uFB1B`sR!7)mZ zMR;+#fJ=9!@Pka4fL-{LEy~RIgD#AIc{0l_lB;7l{e8nA)cg%6Dty!?s4rURXilhk zm>Ni%^|Ji~z@gw$ADhT0VC0DtAhK;7hsWjyFAwW_u-M$=NETr`%@U!U=t69gk8Mq4 zi+UpgJ?o^iiZE?@Q%Zwtucd9l+9b!Jft2-Tn!6O1WAYL5^7`yJzC;EKu9(_-tC+JG9&2S^X+<#MsK(wS!;l zdTnfp!_&Cv3nR5tbAOQ$YATlMP0b56BoH-nA0yqz$aQ#$q>ATBp7{JL(d$Tm0l~MR z0biaJj-4lkJ36GO1iHrhVo(=0mtZKu+SXt;-ss%+0wp=YXIeafF2PtIYnZ6!SEXw% zOrqo#$FZq60*qA1HVo6P1r!!cU` zmHjPBMT_j-D1%CBp7dp=pY&Y{;%yXjo5*%hq4!FjLOCa&JE-f4p#hLz*%r8kzONGw={maeN$j2inO!A$ zjtMolBKwTKQKXTO_&ljL$<C^-M>9zQ`5Y<9k4aa$)Qmb>2bHn8|YB3EFWuEG(_z zIpbCSEwU}qIv$&z@_y%qD^UQ$IQ&B1#0UlX7xKog#5*vy6IbIM7}&A(m>8TmVBSPL zQ{oZt;Q<(~yUm{O!G6`{X zN>`j{r-Ub1x0G)1C#<#kOSea?7`#AX6F{0PcPY`y1@OUdrxX=orN@=12$ymJn)_gK zB@Fn>O$4R%J&0z}(CRWK+>-?bt<)m&B`QW=BFs=F7Dnh@;Q8FBFe1WZg+eIik!|5j zxSma}M`-+qCWa)(O3g$z`2$$r)qHEYdKG`R+I+_E1F>(e#x+O{YOby~el}Ofj9-nM zUn%O~p$CuauN9&Uua+Ow4nL23>d0i$_ER3^+o8J*Z&ntiYV^KyngKFW3CpLGJ3xyz zA6f|_P;=O9rWAc08}5y~&YSx<$-vwNnN6Jgoq(+d;4@bdEIvSCbAY^&g>xl#{p=*b zN#6;OXAN!9Hb0L57s4croo#@c1OQ49lYwVn@8wynzWLTnT(c0;G7bqQdLc~oLd@z_ zObxmS;JH#7G$4p`5yaITL?JY$*l0|8=B_^#_osjT^NRtFOX@G2l6sR95&cWt61x*f zed&Je(U`uxTs_~~90i-R=GPX=Ef&2SYPs8mVY;GrZfHH0C&I?eYtf6P=+EKS&yA*X z`OM4nj~VSgbePR!DtdzL)xBa$jPd29iwE3g8JuX?)%U7n)%tTxK8 z!SeU;@~e&V^<>W-Sc5ySF)|l;Pd0Udi`L!Ren_0SE%!>l6p&fAHN5> zGtFYAR-B2t!CXqur)53Mx4dWaYAFAD8Gg@(SIUyFc=44?-Jdd26A0OI*gY!+S)U(5W9^u0BPGb$UJc|Q! zcMs*3kfK?XT7tMWqx?qpWCKxh$3$?640vKls08bH*4dc;sOj}vJq2x zptD8VFSwrTDjA#Z%pzJFwRSaFgM(hfG(_5{XSj)3GW}=@eMYZ`UQIco>s^i3;NaJ| zVa!(T_|y13u(pyGp9A-~p=q7Ctvnm^?-q)fR`nxo8m+etKGwtI^G!P&#Jhw3T3vYf zWfJY?6wUOr1H_L5pqIXm!h=%LG_k@_J+VTmc1*0eq1t(E#iDALbXN>VACH0VisLVQ zo{RG3T@C3E6nhW1-=6}yZZk0W9X%18e$NzF&>68wy5Gs&1mAMIKC z4m4Grnt=^{xeyyFhxc2B+6tW~4V}rz4+FXgs@z?youb+Gtydn#bc$1|*y0n`IwHPg zrP;m{YTc`&3kTF@#o9!wO$sNhp>;UmJXOuDW=Ko8VPvy4ZW;4U2g;lSX8f|-pm8;H zkOq>8%)azT^H6R5v5$@Pdm0ztDObcqJ?3M%%f;$m9I8cf_0ebHJ9Lh}c6;zlBS6@` zq(d9%pFYnvq4uw+N~qNp-=J5jZNWvQF#zyujK%as3;^0=3A?B_Kpl&og+Ib07h}@3 z+U=p!N@Ix|MAoWZTZrk^gc@YqiGT5W*v7P~9FMtMs*l!UYsN%})V78OHD_kfro_IC z{tauuoMpuHVU3*o1Z`mAK;S_cyjhvp!s9R({{}s$_)baEbLwzKmJtBrkEf99)KBW2 z#*I;bCchZ^>7y|CDrS>ht$W&8S#Kdk`55zD}aYT=LaDWW?pH#LXp&mGQ6p zd-WCQ%%qIcEf~@c8Zg%vSyWWAjwr~AClL~*?kp44rB>8lM)4$4(Mo5uG8ahCBy4Fk z6B!;65%n6E*7Zl@$MCQ5m|kYxih3`I#4nM0DH$ifKs(v+EsGkc zdGt8dhU-*NcV!|YfM$w(d*RkI3CK=LIIQqo#u7Rr*Iq&Hw2Pg(PJv1vH7F@Ob<89@Rld>HMPmO#GSq`TnB!rZWK4GGC*YwE>D?qx4e#cYU@-@%wCYd?JgJF}>0 zT2eeR<&Khv$_b_*q8H0r?}m)Ug9tKsv@XBuz#+V5Fw#pT_SH{C=fxMCuK! zYB8{?tq-fnnirq78@e{8-(|r!j5WrXZ{qg} zObHf%?=pV>$9R7hZhv9E5g$;M4gd<{{s{@*JQt}HeuEM}Vi+ra2h(%Y@WNbRE3~e^ zz+7M`Cg^B3t_Z;J{DV4f+HpdK7R0pi1q6(zHhH#4vT;I2-?e*!m(Xe$ORM2ba~1G= zxp*(zxHl;Y6Lz=6KDBGGtt#mQIe+m?Y=>q($hBec5!S|@Lr(L!idrgp2Zizj`U zhUPAyo%jIp-MIT}=bUo?2;5QCG6=-W=^)ysa}XU^Jj_9iF+kA0YyvU# zpizz>{rQ&&>7RX&>==*7DB*(P&_;7ehyLp9yZ82Q2-5pTITAH)CrInkA&oIgc=4?R zPJ*=BfJFB~0BQ5A74c89E$_l70iVbIlWjR0Mw0$xmje09`V1(;*?!dspv86dS8~B{kZxp zd{g1?jJ7UbHnuL_4%F(Q!KK@!s4J0e*!$9E#;bBg6B?*U>fnjzprF;#_;!mW{4uEifo@!m zekh&wU+U{~v}@{;R?FSk41o4BLK_Xx-u^G4{XnzSVbbI+v;olUkUd{5E}*IC{2kZ- zhjRUYC?))wf&n;|YX$dnlj6NZ+kG3E-B8%*de>8iYN_xnnUmFu`?_7CH3jix^;8 z9c%|Y(lUXRhp+5DJKF+sYc|Hg!C0_LQHhrSIc|y%KIX7mw3a6+nD5uR^4i}yO z{stchUev}sjznW;y7Zbg)rgYumV*e!fhkF9>kFEqc^3Z!v!6>{lib{lo zpP0pCoL{e~{AC{JlzxBDNawC(q;sn0fi)QJ^sg8cO?}b?2~$JA6-I62;cDAH21V`T zqY`^^rFrtem!DddPFn^RWPX7Zchz!C>n?|z9DL~N@X(S?(STZXz{D=j?cy;}Yb}P_ zv1cWz9V&*}X`;IpPH0Pv;bRR&=Zvuhe~6)56^0ICc(RC*pIre@JRp?EK_W_wf%Oj{ zR&+HuLgz>n%GHTW{xtXvIN^e?7Nx=hMHPEIF$DU}r7b=pAY2Uwl*Zb%jlYEjEUN*~ zWT-ikrzKbfLucWbU0`4~T>}_wxAx25QdIRP3#{qmX0d*caZ4bZpABS%dnB;aJUiAG3`1mnwr|ltM~uw}|R>HbBHxG*nx(tr$tIM^;si zGyJ|WJoG#KCIX=0S;esn;c5$h!-uEdKr3O{HX0a}UP_+&z(}2Bh%i{M=7+V{xzbeA zYGb_NZM@;_$%e;L8Xhwmewxwnz$D4IX|y`VTitH9I>uX#K(+WP-s<*btB3PeW1&vJ zZbbL3?cfbL$Q#m=Y)D5+LpqFxs2N5>fTZ!8MngJyLwd}Hbnu2CP_4{pNKdjMj}bT( z3w6GM8!A)_TE#;hy>_evV|@TMRT6iZ52xY7f6+tImgOi-cS14?%)|n(n{Uq)qcl{B zV)JfR((G%D(tWx)Jt`#|u; zrHX?ohZ=W)(yw0n3Jw=~$c2wPgk_khTB#(8P!Xq2-j46GQ4KTR-7VUTV7^4|r8alP8l&TIG0gpFnBYsH@z`GJks89-K zs>C=%Y6-d&cO$ykqbXF;FjQbcEA}exk1?9V1U3H!E`l}olPhipTy9L2E1bC~T|tHO zmRZsn)>&742gA}fFX~3Yph_S5P&S411-|HV65o5W%%nb7aOzX|{Z#1(a!}0q48{go zyV_P$ieJg)d%J;k5h9vfLR3K7wG^lNdfSmBLv{rDFEgmfE77kXItLy%C;Pa&wBVB zfbUDYvn~1E*_Kp!ukQ?H@(0OHa-1re9V8zsM5kiy&6|7m6-~aP8F{$Ci}^Z;fbhdK zL%5pB3l`npi-O)gh;*?0RSeJ-UQ|?x#0q}%=pJJIAX<0i0tjN>QYRPl3WXY!o5EM0 z6P!oJPAIb>qF!!fv7Grm&~&>K(bT(?Py?Ivw}ag}%H?&`FGSe`zu+iKyY%MS35M$% z{KAARU_(FSI97!hzxf4;%IW7#Km7N%jMbyl2Nk5$iYIC(d4dU~`5L#+QV z9sUp#xN6rfNp?7P0Q2socKG-MmefNc=nu$x4^JVyGUR?EMosB{B6g(8&cO&D2Nm@P z2W(wBrWp9rr`{u$?&0ywPE#s2No?aa6^@6*>YnHDJnbgD-pKN+ljju+yjTdgnv5fhWQ9-)2Ye7zD6=66p){6yT+!~?yXfzooFtBOMsQ2b zGtL#viMS+voU;?7NEl4A4WZ$ByBtFfwHB0mp6AC`Jyh zB#|AUZUcC)m>3?~xTY|12ETdc;@6>Vf7ykB!PYt~HL>7dBsSF6aX-U0Vy zZqeodDJt#ajaW2JU2bCf1Ra_(#B;{tQPn!3`fyqebUIg!RgSzBQT?<|2Z#uO?RpBf zvK^9iGY!Q4>NO?|Q%tPp3hm!P# z5a8xUJ}`e+hWzWB^j(Sj{N{n~d0!Ft>iCs;pu46Hc~>D+;xh%jK|7#}&Ve4yS^*>EV~%A&S7%7(%GS()Zn4u7uKuLVGi=dviXuW*T|DHGVb^sNRC9{$ z`}eszPR;EdvEXc}B93uCkMW6Wr)Do}vl$b$#@aY4*0`b-t~dV|@K4wes{WjtC1+gl zesW>0B1zm>SkPrFbGdiew@VfKOsSo_X9zVl{ls)lSpGgnDN@Iv$CJejKz4pI02wA~ z(uU7B#PrKaq@N37`lleKkLyVc#mHlWG4fb9q8>Y#uww#YH1!IoMOPpX^9EQm`mAr! z?tcgsuRDs$P*{f&{!x2qy$I@4v`2Z7@Xgmc+u^U-Bh=P%vH-YI4HM3>V{r*rr5`lH zBc+FmistcPQNF0?2shh0QMwjcOLZqPteRy=Kss}SV=z$E72#fMaFkdb!^sor1pf)# z!>))S#!bp3jFU1AjAOgSe^@)oHlEJ@qmBoScE8Ix9s`QhNYA3V=A(5M;+BMUwJ)LK z_ml~s+410`bS^^$c@QLc9psZZ5TurIEVnAf%;h${F`!PeV$^xofRS?=d{j%Or(p5u za+bRSpO;(BhskGW8pbi9d?-`}q)CsZl^FundCpk$FwiR(?*lfj5uWMIylHi4z(@uI zd=GR|H(zpKmlVO>jsA2fxVnPLCvkFaD6+&S6MLl7N48lz`XD#(BbE7G0M7&UnnCnnd^^WH+O2C>guC?< z=r=h|iiYX54P%;NjMy*906fKdd^+@Sx>JfDwV`%>b1HFZU+`1x6?%T7w~{s!>cSWU z6j(rYtc33Znce7=*=;V0fZzrK)`+7}j+^!fQguAf9i;IUqSA&bF52@;yeEy?f@Nv# zH{%VX>Ag8)0i3a3DVis#4`3)~C7Pu~$2%1YbbcDTZ<}E`@bu%seB1^fZ4+ui*S1h_ zEOuB@%N;AiWtLDboyNo*lIP3&55S+${XUp336Mf&EohC3U_6`V50+K01M}E+zK@$s z;Wmqqs(nb2RrD9T+kQ61q#Q@vJgYYncj z+dy<0=R_w6HGd6-dK9#m;Yk9cSO!6~iX)h9&LjE?xnCG!E_$Z>@21#ulaX|C9eHKk zncAn|0Y(f>u}52l+QTIB%TPVWL*bEwkoUYtQ(_HWM4A2G(I(UJ%!dB4rvY2gaM8;# zypduvvuj=E?BaCWNH3nbp)w)WjKp1+T1YL^h!7fEKXv-dpDsw+Ml~&1!JGx z?TT7NK!Q1#QxK5A9YzGgSH`D=ikWxwHEQk;rS1WAzDvm>-rkvJlyEBIx^Lq&mwKzeZd2jY0p5X&9H1)z=L$GT)mA zyw!s|;KH8+Hx%J*?X4w9pqjH97Jzr*UKCGUK3(`~@o-YGF99 zb7JUiG1**I(qSTn2mghu)$tJde<(nK757FNrXEdA#^7~n3mqf^^{;bu*5Wl0psb#9Gs`i9+yzdH}iq`u>~+V z2j%#2TcEZ}sQCxnAMF%so}%M?FA#y5@Vo*IEYtgWZt`nZ(4jq~*}smel^us$f%ALzLv`#N=gsNEI$nG;mURoAE^u9CY! zhWu9_zAH?S)m*z|4?r<124^e6i5Mxf>k7xgke8RkG)%^6sA!Xc${5q&XB&KW6cbC%X8}92Mh2%AX&%z=%s`P zyfj<2UFkS+P*_f}lDNiRtOcTo6G@u+$^bNT%#tL{M5bSi$z%a+xf%cMbVP{>75NdE z>yZIgUN}+BtrTHeCkb!o+1sRwjWR2D3UB9230vv*vC;g8Wj(HVw%otooJQ=>`tJYB z41RhTXYhufVb4zBNCq94(o10;RH4Rc3gI{@V3p~B2}~|C&VtY z_sXOB4}xx@c$)8TFlOXb?AWNT|I3Wra0q9lVYU$xV+KkFbI?Q7h{0w`2(+rS%PZ-$ zVCbyN%oTL$Fj_&+F>?hyw=kJsdZ6{g#TeYx>YIfW`iL|f#wu+JdW7X4(puQbYY-ny zqmOS6uog-drmO{wf(0;3(#9Y7J?4{Td2FLQcMXfX8$28TAL8BxzKJS(98a3G4bVD4 zTSY~!TC{G}?b7aA3`hcr%wU2=Ku|=hK*e`~1W-Z1w9+tSt-G+kc9mV(@A_C*c10d4 z(zK;7UM(OL6fDZ>g!({%((;=BIp@wJlcwN)_xt<&`}t%#cjnH0o_k*RoO3x}gDaG_ z2Dw(p@frJKN&1$f|IHx(a2N-9(u3y!`_@^D=Z)9qZNkr=x0`;zdHd^B znl~gk#XxXCsC@*QJrfAe(}gP0xu-_DB*50jB?Hy9(eEN!8w)GcwXtvxmghPOWC$gp ze#nH7_%$vzvLBsMIt?Zlg|O*1bcMd058VsUa4(31t}Hx7m=E0r|L=myyDLvIq62wz zQgiq=tF&w~83$JJ;UeI@#xa=uu9S3Ii1}^CVl(u?vHyJi=n)g!_}mhF^G+rZ<-l-*zo(2JVm=icM)cku3*y{mt5_l zlUuXRCZ+I8taNJF1~h_f$FJLm{1Mev6rTXTx5RvZO8fqx$)q6vLpJukm8fHpT?m!h z1(CPGe?Su^0Wl&B{cX_dG4|j#v$Lb#>Msg@b5a}L;o9(?duZoMI24EOWpkIK?QcM- zN&d;2C&gX=fhWavROE~IGFu?VD*8!@RD55JdYbO9_z(y=Aq*naEzeo_dE@%a!&%~S z2fqsss4Z)t#i2|W9;(~pbX`ZbzxUyplwR>ZyGv{lhkA2AbBC>`$!kgHPbN2b9ecRv z9CSW*S|67f;g(fdw)bhqvg9f#uGieXt`G6JS@$FLsgNIav-C|W`{v@_1J`Y zyV#*0bNkY%U?$+mFwlOg3)P!{C;I{oaOTp^OZ5PPoK(3X*l* zTqVF`A3jD4@LLPhK;~$4t;G#>Zdt_HvEOrPhb#DXlCxu<_`{`D!8WVA&*8B3Q)~?0 zWo&Tw`SJKZ-M*no8?W7_*amtm9gyGhDaOfvvegxyA$!D#D?GB(@#EZIaqpc`;=N4X z4PXf0@8zC*k$r!G`hAtVPnEpYa@z6ZyjGW^Y2E-=cyJm&`MB4yf7auWG2f6eBG*Hv zxnHau}c$HMfuA#)BXE_PMc9 zZEB?KCm7G(lfTJfwh8&yb*jI~MLsbqpsn7dFUaSFA^`h#v$OKT!324w)q8?uC6T$V7yMh>$HW{-O`N+SNE#`~gXeGjAn;!wB(@U4zH*;aLl4$%x(s;5 zaqgp@{0^~8+|RwY$H-rZb=-TN_bl~1o(tsJfGT>_S?1tb$aLOGIr5P%ZneiU*vbWm zTgd7<`PemNb?uS)$=-4qPncemN`1=wZ2vJ?T(WptwzAJ0PP-|Jj`B`!_0}j_ICcwv z2V{0>B>FtO3a8!DEj+HH0p7Ozfwju5Zs`;ojGrepwU5Pkr@uJft~ux5VQGZb03Qua z@J6g*C%yhL($1n?>aVSLh?{a7oJm`qN!vPF7923U#J)aJu(=k@OEUR;iS^uj)t0*U zR2;Hu9I<95Jc&*_9&zBP=AJ|32gBAEmU8vL78teqq$)SJ`h>f8rwh#hU_xENlQw^U zxLN}b1m|Y+=Du%@{b{a>Btfo5*lNEPxq>SN zaXW#}KR!9p1NacQc-AnGk?Z7bx%*wgGfCW%K4|VM*NUD+2tte8j;SK^E{N9D%X8fJQ|BlXZ5)TMsCtxO+mT}uC_=!)3 z-{0_n6T*L=1O8JaxHU)v;T`+{w8s*# ztL=cVjxl_-hkI{-9KQ1G=H9DQ@zpmfzQV)G6?_$B_{yDa!85KGHIa!(rh6}N88 zSV<6vYY3^XXM`kXfTsUjJ5J6&R|bAD)?4XQSwO9g)uKZpUNqiM}_u)#F~|mh^^q3w}Zf3cJpT zzE=T?KoSOt`~7GO;hae_J6U@T^2y@3RL3yDJAcvfj)nO7;Wmnt1-$l6-rPoDvjr99 z2!L`D*?)X)!n0ypq#MQ%s`)}iH9!0eRC6ybpQCpuEczcQ4n!hjkx(CRXbecOw-IL9 z-o5lSi7?Aw+E?KnNR$Sq##5;wWH_Zlq#F2ilkvJQVTKe?qn%_Y`w@72335hAb%%2fo)7=k~) z%2jB!6VJKbjp9I~D|eIIv1Q)J!5_>|z5(@!O;Y1d>VUt1Th05SH95#)!vE`&*#Eq~ zh)2dQ$>H9^W1b6;t+R|>lIL=K>c2pc>(F20DzD=(S3Z>NBAICiFr-DAwjNdYUKTMI zEi{47tn!i)e}Ze$#2RV#6y5#<6&3wq0LP7Ip==d|no98(ql8wiW~SA3qKF1H{iK*$ zALJunC*u%PV2Ui&XXQ7V&kFkNO6T29#&G_An!k#;3AG;>TP$qNZ~rC8|9eP*(}CV> zTcv%Ym=}wsj^%7m7sSs;hOO5K;%0JR@c=xHgXPrQD{cWU*3O5A_fzMSj_ARlv=*-K zaJx5D+>C+1;D0Op+IX0Tz~^36zVASt$NnKKyzm6F2}aJIr;`SNE4Pt<3~nIbH>pPQ zbQ}W^f8EmUz)Z3?QyqFK&1?|7f9{mGG2tzx_ zv<1HK^%X++v22&Kli|m3=|`ODC~!c^Z8>5}XG`{q}!|{vH=#27Q*JvGxyJU1%{kHakloKgTT5V?~Rp zCvDKMEQ`m=zvLBfw|T`>L2N|h9Y+KWY0>~fZddfCd^#Yz5gOawT1`74&?si#zV*Ux% zY$z@X3NZ_!3Cc730G^fd(8>rpIa%@oP8sf=rm*jbJfV*m$`bi{c$S=I-&q%5v z@Q=Wo{>jCuCegZ(#TNyMBYFMX8t%@5EM?pM`6!m%yB|0Xx41Fc(d)I zwG6AzLR@9zsm{V0{6vI&!8QwJjm`Ac2qjb8dZhNGT#Ui=tdW|S^QRYJTrJY4BwwMn z&NByyN@!eZ*m^#m^VcG|^Q++AwXhACoN;b6O=*ZklZJ(@wDXYPpx!M!kPm+->ty9O zh%I6ya=;onWR0|0^P3lb$-!TKi-@OUMQxEbTYf|2OWVQ&vPs6s0;Ty)blQ0&0+`D) zClc*Tj^iPppA?Kl@1%prPf>)gQgXKD!^yvgE>XPe5Mp?Y#BJx{dv+25kZy+bVMycsVX}dIm%?kY7tMr0So{hT#=~No!jRC`Aq>DOQI0&C1QfFRAQi08xObHYiwvf~z85 zS^-d5q1DhOgw-}1KpfNwHw@lfVq^gPmt71JUGT0?1RyU`Whdwb=!-~wVT9(Af}4dH zBk;t;0wqxxT9Q4-%v|5{m>PWeu{Z$zDKphnQu+T1K0iEvEOTH|fOM(p{|rd)YC#~a zA3;ExzdaHK;Il~iCuNyn(vnS(2r$XLr~#o8xC;Pr;&A3d0!egGJBxja-6vk>4QFTj z#3ya&*7NH*f3=x>xu?C|)fh=~pSd*WWUk^ggykCnAEl~z%xe>8p0D=^NG6P&Z zx(}er5*dK-^iTsDc}JrC+O0>pwFJSAhIto2kya4SA|PtHDn^<;6(f^TH!C=H1YUB< z5e$mpactw3phvF|thFMeYV$hUxbjPzPF!nFix82LypAR>HzI-(^Kc>9X`L~IwylJ)>y$A0 zW{^}C_#92MlLbfh%*zER2shNlu2Bl>0FpVnk-ol@Tp_a%j9B4u z;71me{paqN0o>nZN4bm+uL1dOkT-Nc2toIQgnuQTM0q8!sN|L~ObK*9480N{$5rY^ zHm+G{QK$b7UhJ)4(>v@xCjj~=R5>ghhUfV`XoDZfFGB_K6pAk(zX=#0yj=F*%;$bV zfr-C5rEXUPUY-v{)Bs+tkHyQ8O(+#ITmS2Zv0a$6Sgyy&V@4?qz9IRA7w-d_cR#?w zFTLU+jFP1aC#Vx14hH7* z;dwAm-}qD;7C)SFcQF?_1fM6HGn8;8oP=1Q>jyLw0#;K=y z2qcQ*?vmrMe*&7$qk2ma@BDeqIK)d!;MVc^_@vkgoOl@(G@|BSkdQo}}&#<8` z3|(t-$<@e=YZ`Z1L%^Nvl3Gt9wXKOKe7u+fpKxW%2Taq+cz@jSuOC|X}|{mNPi z5-2NCsg=&ro$)y$?}Aj!OQ!oZv1fBS$BbvH6vSD$UrN>h)Q@J`<2MT-6XCJkVT2cf zKYYl>9LanpwY74i+@LB3QDNoug-dO~(%kr=ndv+vcPcxJ8VxDW1I(WrLCh`vP=v2j zLV3|!sJ0zA^)J!*0@wgJPPUjN*s@ zG*IS!;+Jmmd$+i2ITfgXK@=cMi1jQSnszqqeUn`xrteIi@Sz} zGICwyriHc9TGwiHIW|4>kT2}BGJZ0gmNRN{MlXyse(lJ~)?9QB(93tBo$_cb-O>_M zJ`9;lt{L{L3>#6hZgsdFd*=QX^*wWNAY7=bxEv{M?k3bc&A}{!*bD$B1UFjAo+|jA zHMdEQU|jPhX-a5!UI;&7LyZtcc+IWC*fqlBDjw$gk=9d!Wsl(4<3E2;exMgDMk^!& zNAQwMla7n<_GFy45$L=q`b_GapCtXG#}vwIAK=a11Q`WtPm6Q~<;SS9JJ<&`QUhhi z8QY4UNPzobpE9IJm;s<-NKopfBQi+0a^J)nXDb8sCy<7|Kce##)M!IL9~)Vi zFakK*P7Y;Ff*28Uf#Hde7!DkWz>R1?lskzJcG+gYLOjsPjc??}Z&%m%5$*{S>aj|= z9~$7^X$ORt1&Qhs?ullvBNEt;Zy*sV;byklViQ(MxVUE;ct^GWd?vrR^O+ZkqdIVu zIq+a(N853)8TM@>5uvm08@aZ+p?g~2%Z4_7bEl! z*P!3x0v0d7FMW*SCCCT;vjKx)MVnc?6gmMg2aivolbJ8@iU)!>1H$nu!8Hi`JZM%< zLLl9@H~eH4J4G9Ds*PL#T0o`0tQ15U9xbDhsij$JqvIgPqV=>J?Mb+N88<1UImZy5 zEpG7#{8q(`xW(}S&CZ=RvPDIiL-E}be?TlY4f^X9zd+^sgQLO)%m)GBP_+0aXQbeY zy`B$Se*x&h2KQM$(VC6R7FVl+8q!9`F5BG#tZRMQY8A6Oy^huyE1;Qzyb(x(4OgV% zyXkdAxo2m}OqD*37;Fl!fk%2@C(wy80-{SiRpd50I7T3*;&w(`Lc{Uijt;nJkWLDC zfllvfa#I!U^~i=fkfA00~zx8ZwBsL=3PLVpqRMxKl@Aib0K!m3~tfZT0e zRQauTRM(HP>7}=iWYZq%*ZwFG7Q_IKh<|YYXBdyc4r6&}1$ zCuKf&5BUbi_zFuL_APA9C%X{iH#Jg`rdOOkG3~wjSA= zYk4#mKi-~!=Dr-+pKKDuGrn-aYe(MkhGyAt>Efv_xQw?_7s3aFYr1ccBmf?m@j<&FvOp8e2y*t%eggu4A&;#t)x@o_lmt6(2qf2~ zn%h(0HWz4LOp$EG1GOxc&KsqcEMstPut%`JY}EZ$*0w3ZfRBT1|ej3bF23Vs9Qv%)NQktmo@q? z1)lVX);`%x;yoZe0G%IWnmaO_b{OLtw^Z@vX0GgQc%JMmO2aq_xC+z^+(@yT$*=J-KTB=T_fIOY} zccr6-iAA%3XA8;ETwQ7~$*2M=_A-AyG!qDq0GYH9&#&*~Db9gI^53PLcIUDGN9TvV zs7TlqdAJG!@S8{0YUrH=!c=}0J0>g(clQhV+&W`4@47fYu34R!MHRI@h}V9ZhnU)r z?D9q>!15NU4&gjzNmI;==+hWf8c5%#e8^{O-zI0yUuVK--*eWH+zo>?$YbJ`)u0~A z!*)JFus1ZQKF;_ZUvfd$p62DvQgUlwJ-Om5d6ARP_zb@2&gd-z1f}K+yep( zFYC;OUbJ@GgS{l8PlE4q#R%|#T}fw|fr zmowYf-~gfwj_3dKxj-<_Sa`?xvkKwEV=7&q?!rzyV}o`&_}uT2qU*&j=$P6~ojV_KJ1B+&aKO8yT6Fo^z5ct_7pnCOz(C!Dzfn z{MK!Hpt_0Cjkg|`CA*;~2Ks!?_&cm)HKy43JLl|Jg7`a|Z#Bf<>16SDcr^ger%Lz! z1~|9ZgncX+~ulB z!TCOQ9%T8&GZ%5IJ#2;VrWaeKukb}*S6vE9V!UHG@ngdkE2`KnKV!t3g!>tHGF9hD z8*2M~vewPmhgojKclFi?vQq8W81$j==>?2*ZAziOBb2gD~f!03=|RWBZuBRDcAU}Rcm z_9j7tc8_NDUdu#WxEI!$*xv%krQC_U1P0_Do}wI=x}O(-(Gj3WNFvI6;hf z9gTsnm1F1y(Z-nKL42oc9oFLFhP99a1<$Wf`&XR)$rx|NU>)ZZyC}@mAv%N^1ZLc& z@qu7e=1x+!WpY!A?`2>sII6g3(X$JdLw-l0IdGM$;x|J0E}IZW0R}N8?nfh-Vu;x2 z4m9c5xs-0wWB7AAGVCBWxjy0x3sx;5#w^ z6vFq#42yvFlGNV8q~xeWPKv^vV?aMz$2)|u&n8{ghY@b}dXtpihaz~R%@y0%LgA;! zGg*4_7(D(ST3AfIMk%XMv(n&y{$VBWQyEuY!<>Cgu8LHVsoDlva1RCDom>?h>1F_9 z-i(N-y?_knFbu90ITw7!tv0(V%mUII$2R{36qH6sqtA3;uo`mq>M5WM8ldtZojCzw ziU4WxwlPLYnC&?MSVtKYIbWin#B<)n`vq`Q>4jxy^E!aSbq138@LXm(kcD>2yw}m> z<2*mo4lPI)xEMB&J(d87jW#qaOaov+8S>dT@ZEZqru!VnfVzc8n{mGo4FxG%^Q^wS z<5O;t+sd{&uKV>VSrEsWe83Sa9mmJdq81WO7)XbWA#HnmLpKY-TW!iFdk*IM`@xg& z@x9QP%lXJj?Ns1AcAy=`iw8E7ThvNz-oYg2H*e9HpFH3R_AzGdda~JeGUwt&H!&0L zLKdha55*TIx1utYA$&X;Hg}u}Q5_IB!Szgr{oad5sg1GS9=`q?lmIWm#hy7T+@JWD+r)b;ohYvE zXm9VG|C#!jMUn%NYkMFWgu`~g#9MC{)A|WJ4%`+_`%18!=j{WF&+N+u1Zw79tVY3* z;RB#CfDdjDS#!D7CqOntI-vDF+e^)!}62L@)n1LZJ zk*L9=+25fIHaS5Yb{uh!mS7o;;TnAgMg{FX&?O2bpVN4GoGglenQ6Y1M zXbUp-BFO=6A;I4bM6c=-)de zawfk+heB_YO0PtI3H`bqP8*^jrwtWe{kko{M#=nBY>bj&Opo&)<605VmX*4)6kJ=Z z;o8!lk84Lz86RYkzOBNjZ<|;ny|lAau8fseQCHP3k^Q?*?8JlH8UKdiY07S;jW9rN zgOa$iLDm?nc17{!^g6oy>0ZYPzYXov#f#c)h#2!QbR&jPaP0P{q74MHhKg|psl0Q~ zLkJguhXQc#pN+_gx9s6rcn^Yorc5Q1f_7on2tiY(T2Prvw(6`jl5gLK*=WClg_hi> z2%dw80DYxG>I!a zLWzBVmh!)Ii=Psv5^|5TO`EaVHiaJ$#1wD1e=8!X%Mecc@d`o}EE`9OUX!Y>IuCs# z^jERCiWfg^?=9dty%{D|b#;N@xFCV*>H?;^;xSM+w82M*WmmHuDk!X(d+t#{Ol6Iz zjlvP|51=?cX;iJE-?H|lXV$(<$SXAY+iA0LRd%J)|8SbbbVQ1(?lg@y3`GaUCQjr+(7Cp_N_6jdLOk?MG$UI#U6YNtnvV{Hs?A8J1jWayYzKBhwfW)q`Y)+6qQ z@vxft9c_G{YTg1MVF9rKy?$UksuZyxWZ_o-gsMEbhMIzHqPU?E<9KYRU{ih!YBW+C z3{_v5U_A7}V8bW`>ywBSGtMg#Ng}Wjg0j=an4M>9(e+H;$cNM~zO%)GpF`rFEu&cX zA2UlGe5WC3TmnAF8Got33~ENOD3%FPZodR5cXj~L?Xv!OVtJJ>l9_cccVEzC9)wax6x=IuS+!wEDqc39bGE|4IF!Vsr|M!Kp|+*`Z!> zt_mnu1z?LvuE`qspx(MFU;2bWYTq9nq=qq;Zjet=?=r|` z3s~~2Y8DJKiOx=c((zU;59!n5{FkeM%V(!MCx1Fy^&}}ILf3`jqZ4!f*9nH}Y zxQy$Y#MckA69uRr)(;QRDydJ(GevnMfqgw7k5203>pgU|R>dS~!BCU`JV=BoEqu_?D97dQ5Wuv6wO5ujVg ze`IIpdc~92rBd828Njd8-KQhmq5(vko7vXb%!BARI~VONkKh-)S8T`sMX1}NA!k}Z z48}|0e=W%U-=*zvUO@p(W8?r{T0T&VnaIQ3v@#hyMz%OT;ah?V6Tj@nboaappap9?eUnTrr!tL#_uR_YpIYn0J z(*(ycc4ALSr59eXSkK{c%ULro{t5ABpu+12R0gaAtJ{?MQyt-^wWICOqm zdw*@mnTD)to;GCnFe6o-clfi50iv(urUWL&uk#`03*S9ba)y<%ec-utKHOomsj*)jBMPca5T)uU}~M1RbXczLQV^C zU=hZ+T=fewY^th54_)*QZNH=rongIVqnl38rh_kil62Ey3=cm}s}Sxiy9LKS|CN<| zcsPtk`ot{w%|YKmG8sSq%E1s zA=@4$*kDhaRQ(fdQeiRuUZ*{wtx_H2HBUQ5@v zUcu|DbPdyWK3%8NbuwMY(RBn}2hlZ`t~b1@$FGK!l>FEX&7Y76?*uQ}TeOgqH>}RX zWWwL&`jBuL9U*uRbXiQk7`}^0NpAvId7I+JJO@~g!$`r4#X9afBnlKf4V@@ZO>*7b z>^wJjog0;aa*3OJBKe`OQcOHI;#1yI#f?A6jsFfPOOWzXi#4hsZ z2uAEBF7Awk@z=^BLmke^-PvQ%RMmy}eBeI3nSZWw(I{Pgf<5#I!)*l(qDG3s4P?0P%YQ2wSNJ5YH-M112zlK;Mw?4IlsEn2b=7_X&ao z@7RR^kwpMu2kD<1u?z0gvJo)Siw^)`SgJf2^Naw)8@Bd_0bRARbYx!>gNm+Q3PhZa z3XTSjPTqv^9y0a-&GcSna=%vr18->lP4z7COlyhT(J&82eGSZdU@wBnH~%^dnB1&^ z$<2zXC6z^~009lb3()x%iCa_Vsf4zcaF17EzBZ_@4Qgr@B41j)mhE244kHNF!JtBC zo@j%SsPkH?q8Dl~)U}n>HWgs$dj*6{@Rq>Ej)0VffRv|u;lP`;Y>KS~awica21k#^ z$j96oGxi@K3VHY>yodUI5gZN#j4uS*df)+$Kpre{Tkxs>7ct-Wg>RYwq}1=5;QQe% z@O_ivdwqWe@y+SSejdI-;ElyMZ$0+kNXC3Zd+>-$S9?_>Pg`f+Tp>cm$qhL;wU~f-V7oI)R9$9puwsHNn?O zM1|$KW`eI8gs-oVQ~<&#t^KER_e`6kW?nJE)vfZ2d#E`iJWEyD1g{G+=!sSallU2-_gK%j}Uht6%|y*X+f4L_49}Y69~Kf}wP`;Nk2hk|57A?6VIxjWV|39O zLYpveaD{oK6;O<8qm;{0yjVmA52!jqFo7)hp#A77QiFKPWHmyDzYtZX^R)OMy&apx z2fyDNC$ijAGKh2Vk1{+qBAUpND>(Bm-i#f>H0{qQSo zWco^&GII&%2i&x~3d*<<@hg_`4H}YgC6(?%v){(c(7~p_5#Y{ij&L7W0jydgP(cmc z!(FU`p<90zJs8uU4O)Mq_ix|WRl42Oy*~l9KM(fQpA9knQTkF6MZL@7#Fii2)mS1LbZx#IJ2r)hbsv^Oly_{jb`uPMdm- z5`t1z?Uw=5=e0GbO}0^ZyL022`F(MOY9r6g?}xqHi9>{5P*`2ojL~B9ZBrEz;ddjJ zJ-iF5TYFfo3o54Ski=d^U+Dr4o~LWuzZl{8~ptRxdH!YaI5UECT|3As)t zmAFFG=eD@`05Sca0R5k=_Ww5M|7GuedhY&zIrTSvYpbXJhHt|`fKK;PJ8j2CeL zLLRV7L}A^z1g`_>noZXW>1w0v3Ci;=UH8+qnXc7z{kv$2KN1!GV)n-z>C+#unF4k` z0*vnQh%5)pHGHJe9vx}NFG(Ly`sELobHpwE?p2#f&ipt;iN|)PKw>^7M6|RCWWvMc z^zdQrp?^Mb*YE5^%e75%=1BT7^COn##ZEc%CVW&bqhWg$#_t+(=c_m($>ZT)`{fvp zBfl!Y(G?zTmNPHKyrE<(=78MU`HfOPkXOs9{1-#jm*D`fJx_6-A(N`C`)byu(zu_G zY9#VJOW*PecH)N$%$UCR6)daKpCV`8P@!0IX>*4Cq0D>XRq$%;bS0&$S=OwHQM40} z&0k9I->yKe^V?!n?4(bEM`GsR@I_T9c{e-^S*G%$b-)Dp*0t_>;yA3$V)DoEYIw*B z9eqG6e_CAmb}D})w#Q)#^bPir+Y8oB!7gF|`}RXYhwT4BD_u;_gZ!?w*Aq9B(I6&I zfI3_us~w8k__6|QygcB?Me6v7$@^Yr!+SpDd4c80fh?a?(ts^`85{7{B1$}^zkYs^ z0p63JhgZ;IDCaWdS;Coj(uuz5Lt|=vq>=z>FctYYEAm=MU}Ufza%RCIdVN{U7o!)k zW7u8zOdjyla|Ea1^j|?bx-uhXtlBxWvHBu|++Hrg`#l*Z>en}Hh4I0YvOh(7zAviy zB7Zw3$G|(YZ*4yxKl>vq;!>U++WxXq%E1^k+t`P?%o6BpC=X&vB-!Oz30-rl|&70YIf&qemi zH8JO88qS$4qPh-~sVm0Gqm2c}e*eWnu8akjvV!HzgTKYS)0*~u?IzVgOF10iE6tuX zI`smY-zboDr0GCRqJFzJ-r;1f1=sW*K4k8wHv4;j*biZHs~k(`5x&Z|`v(ONnf*C< zxiEMFvg#(3(l8sqaI$oP4ebzBo5*Gg5`y||+_YO3p=3Wr1Hk0?S!zN z`Yl1?WcVk&eLliN;5z9_mK{2uIgq`t31cbeW1%1a!DhnXP0oSx;!V;YK*~kiprO)W zJHw2)H=T{vSzl2z{r$XR9YuQ*$jQ_zrjLTcTyl$?JQa$fF`|<q!qIszcNd|PQ_z%+yBBEaU=~IQHk&mG>zb2 zG38TMfD=Fx;|%;*zCFMV%B+B*=cndZMZbrS!^7lYrx|@yk{hC40bvK+)}e}jtF#Kd?m*ocU)h)v0nvr6L z9ivHzJO-7pb-2wI&fkYy4gWzn$0O|Xp_;~t(GQNxvNGS$`e`uPrGSX$DC(p;o(*Z?Kwn`HM^vvmD=gKw90k7)orPAmk+o4`+CJ~KE!*w&?{+y zO4obXCO4k-ZbzPh`wI{rPyT0aal32b;m7?`A9XGK>7mD`3T|tX6`Sp zg+Gp%`gkB@&Gz(ZfpV;37vfQnuG0b&`Cz)^E}%~X4iA+%zCQVCNT51&NL7m2ex2E5CbYtHZ$xHhxW(i@{|-oRu)*vOCI10#+f(6L z7;e_m`4?gSq~q}1Yp~hS5VwdxVpW^LefU+cnY}%IBJl0q@JW60(g_S{xkKqMz%BRT zHyK>;1@>SrJopHfwJCI2fW5nP=96&kKXWD&Z*JxzUuN-rn#mfQjDxPKxw$4VUQ8YV zxq}V1`sClh4IX?DN-lsqS4T~jzcAR4RiAtrB$MgIrEs^_WU@57uWofaS_A#4r0llA zZGrS)`l<0YlQVSLPw)?CEeCOrpU9>CzMANGCMWg?GryO1)E7@XV&M@ zx-MuC#KAWr(>OxBBG_)8a}_`N2p_CV;=={U$>^&J`MGprqQ195jwhpd(g$v$_r=ZH zm($RZssD|h{iw5YpX3pnHMm(2``-eU1$uwv#3z<7 z+H-@B%L6y8!zq(Sr;#b8*edl+L+tZeAUW6|w_hrV)?3gd1Q<bE?eTU zzEf#h@I+FeSZ7+q^x>5;gXLPw>?krChAkMJWcFV_tj?Nb=EIjQBerVDZ}Hsj^ULER;3R2VFZwoBx)W$h`^C~4NCWA6v=2Q-Gk{cSDkN5#=~Mg$2>1{)5%6VID*mFf z9_)ip$#&p;>28)tl}5u&`?oRQOGDv_bhBRQ=I;JM3_W!rK>|D*m2#+=feV8t&4Ipz z{>hKF9{7Dc@fy#y6TN7pVFVUJ!LRhj(Sv%Co|7aKPT%=k#vMmrg_qJl-tLs4^knJf z-U>=q`ngO=N0;@v@OSaPuz<^bSjLo08@g1=*3$5#Ia)BD(r-3WD1aM_j@ zfwlpZf?%9{I!ppu3gp8i{)Z$7|LT(Wz&kUII5dHx{Yzh3iAN893oK4#t?zD=sbo;{ zKc6}#2j=jT(+@sHL00~Z|13=l2Niitroeq``y^pDSHHkmZ*DcWW^@K0tnP>6{~8zC zG%oOl)6fbl8(nSsi6hgzj>wGO;xL;f;&oKbutDye_Ag-m0Ey5z1)s_`&BMFi0GBE7 zKZ(G!pCaom-XrCvB5wnI?o;+zAeCQ~nG#lIadjL|EdCf*;($;E%UnT6MftZZL+Z z3l9179l)S*;mOd}=xEqa9jrrscguA!D$nAmG!nWZv!&#RaPY%;PXwe`s_ii|MUC;wTwTZ65IUdkRUg8KMYSm=Nj^hlZFz{mfDN*_ZY6;U(11&lY3fB8%0&eY9BZ+s4J+`?emQiRi=~$^bN<{#p&R z*mp+pRHN8^N*u#Ia^^p0V;erAqYJ_m(RCXL-E!u|vzc@|kPlADE~Ll@6DZ`XS8Tv( z%mWsb46-nhgAM^uteknrY{s1-pg8YSWBq8g4X4-dV{Q2|1x6W*fQ4}!X0iGP>Ge&+ z`ik}X>L?PfvA!?iGr2w}C(ZpQ)yN{!kj#zpHXq}4+*niJoq)ypXJH-Q{4JWZ{ep_H8UcEN{SJ~2}>X4tQhTwVw7VhRKYEd_<@hI6@uBdWa z;e%`+4gAX&H@u7%j*gmupASy$hr`-W&b-%87JB_Kc!Hcc0a9WW|0}#7%kIOzESC0S z+5@ZJXXhhrS$w_+e|A1Uq8fEVNmoN6U#|*Kc?4GMl|IJs^89h&uakTG7!8?3v>pXH zJCKI<4S+KOz;e~hG)QQCIC(TIe8#`b|ELpIt}R>onw(8-Nm`!u@Oe#)t}1-l8`of1 zvmt+kf3X~0+B}V;n-R;=8NQv(zAcUWws~&iZ>hPk*z&6eC40RfTr8{7?Rgh|`x0%J zUblo+6z!Oo;{JIt8fO$SQw-WZ&nw}6Xhi!(CzsQ}<=pV+q+TX2XUN<5uXpR-g(h5X<{?{4!~!1T-{E03D@2N zziO`funC?W<0jNGV+Y#y&ExFQAkGd=bK{A0rEq191~7byb#ZeEceAZ3NvFT^fV~0K zs}H8@Ep&C#br@Z}bR9+4BD&r|*RgcHldco!dN*C~q3gYLEvD-Obp1PB>*(4{*J`?M zrt2=c9;WMdy87u_Lf1KTMSLR8@-PvZRqcHirpLZzNTWYT)XDm* zr0vl>L%Lt|@GFR!7gtdEuW02@qw&?#JdLlO&cXDmSI#wmrBdZ93Hqm$ze4ZN9D)}; zP4hWcudfi(Zaq!#H{~Pa_$>WxTzftwv0d~{kkX^F-VFGj#_M3?akV^_epxFYCq8O@ z)7by)*R}dq68t}}q%qXjm~re_k3)2J67nJ8^oh6G;9v6f!0n?9Iw=n~9;Rai9vCAK zB$lnD_BBX{dZW4iCMoITV{#==?n@>{P6}h}KD6=cslN7+RNoR_sgK;#%?avzit4i< z&tGIeXN5i1_cW_-tXdyh=1VV}>|S5*nEGtMk>3$uUKC<4Y5vA@cDcv;PYSGlL8+g| z`uirRe{nBk{Wtry`sLdHef>|f`ZI1<>ZcIvFPuow|G4@;oT>N!zo`Et&)`cP{|VIp z1obbr#r0pWU#|Ht`p@bg>r?74rv7&&=zm=Oi%a$X{}=V296|N(yj7`x8udRx{fkrM z`aeVOfAxRSe^&oouTp<0^}jPg|KsX^c)H&I|Dyhrw^IFnb^Pa0{}a@|IK^213B7(f z@*mc(70amIFRAF{H>pHm&$msdowmd4m}vm13qM+WOm0t=Gk27t|3%B-s227t zD{ND2VUH_?J(sw!%{o2shhp%vN)vz_z&~?0yca{E;`8T753i0Nq*#Im^q~~%D`+y( zv!gG?(aS$~qnBA4z03lB!ifXs4JkP|Rgt8tEO;%Yr{^ z-*-~`bp8#uVWS82G@4CDcmB>uZ&y8bT%~_b8sATj#iohB%GyBu1*I285JI9pC7a$K z?R^!#zoF;%s2hoH>G?fs#-s1{{2mXWi4OUyquYDlr0_?@@KR6ri!ONmm>m1Q)bQRG zt@;uVY)m)Lx0#}U+$YD4e-SMVUzYTzw%GAk_%rwVc>WCdQ7BLU-p0PqktVen=a1m$ z;A`4^RMPyvuBB<9$Li_jRG-eTu=zKpsXvvk#qz^h(c=9b27cM}dhB~XdiXWs7a2TM zYV{HP?|jY3uUXd^<%3nPb%Xyr4gT{G{);4PPaOOk^DF#z9`f4=w$criJ@eanjNium zz;A~UaY?72KWl!QlK(j5M>bp9a`yaoCI1hQAD4~vr}+Hpc&z$UqW)(o-+uw$ld5LR z2i?CXe$)NDaeSA4qVcbU@5cVgh#&lAiSQp_89B3M3Tdg}Za&?O%R{w^oEg!c&z=&G zUbP0P^IdD}Fvxd1b@{Hy9K(-88ohd>WqKM-0iu3A{Sv09=xLH~YFFv-h40<^du&&9 zh5o(@-)HOZOOp}5eWtaK@Y&KaTAJjYY`wkB_<>7b|J}TNw z_@+orAO5@}CfzUE_A9A>7UxK6YwL*P4Du!=l%Le^rPnrMi+HCLuy)90RsLRlIQb%9hul6y z`u#e5Re^&qjo05^j&HAkif{V_`JnXdsYoSy$(g|iagBzPWp9TZQ?s$XVt6GCmh`uG z<5?K3q22irJL5;fNJgX~|Ll&Bh`wb1(@1|Z^fc2K===|!LmXZ8(z)L>bH6JI~M8me9XD<-fNS<=Xrv8Aft7p&CEBmK<57j^h0{P&`< z|5iOeF*2+4_r!m#h|BM;(b6`;m+E_w;X}WkpYZ*P_l)%Zpu5|8SUpYO zXIJa^kJhvPp7^7s|LBRoX7DVAaoD9B{?NUB^nGo_$j_{bX#HpW^0GL6+%hdc@#j1B zG~vsoi;VI>nVzQkT(QhJ-c~(L^uA25kLK5}zsLQeXn|f{6{eTItHT4PGxYaw68y&1 zx3kKaf9$))@^(I_(JIgKkV}NEs)ZMc_g#=eXP55}mQN+F=;J&pN(SG`~#khY~#fyDaV06BNXiv#* zd5$Hh$c}PEw&6Al+&BG=_NU(IOf{*G%#y4I@amy`f%VirDmiqNwQrEtKB{B;5o#Z% zc|_{x|DEc|?V@_<(aYax^~9`4JS-_%_Io4zXKe3?AKy6|e9u9AU5q8BzZ^flBI9=o zey`0J$qz@%wDKAMxF`{R*6}UsbMqD@#2=-aywnPnYw~;!ljnbbFUj*H8C}7KdJYZL zCTaPb@nk8dn*y?%JWldE>2dB=^%!#Itm2*+R_Gt$FaO(Ge<__EvwuPOMt@KIiCa&j z4REwTPZR%_t*43q%+S-ziGR}5#7`Z4OG{^Ac?b10@q;_{bSI{3^|Xojy-CLn?+b!@ zd$GI>{rzD~TlMsE3)X*+E-&DEK6sB&e#ofn247ih{xf0yBE2mFnHoV!&RjlTA7bTu z++X130UZ#Ngu!JP2>C(JgdPo?I+YeuccXjJ*~;R z%Xh@exBa4(&l7)0>tV%^ZuB!tqn}wwKY8hJyl}_LC;Gipm(Pe_*g5fd5BQMvANifq zg$eu5^hblcwI@ewPtHkF^b{v*k1}2d@9tKAj#hupar!K&=_qv%IO`s~CBa#%IO<=B{=#rL6fq!_&bLotk_~uA-o+{*JtGtlIN&>wJVJLnDypF>r>aCV);c& zdhY+y&02e@KS$?jMM&3^src=FF+6fF*=?eD)Ixod%KAF zy)k?}rN{cz{edFM`E){fVEDpez!x+hcFb?!Uq#O}7{fV)=Bl6Rl=HXeH%b@mi{V0h z&VPrR|EtgA^K1N`S1eyZU^A`|m~hf)I*L1GFIr-W<8st}qZiepUiv(q0q&{19JRbx z=}4;bm)PSxwMh5=4pyYcSfmcmg(k%w;B zme2HG+i|~wttbD}8b3t%>F6Az{Jbg-KaL&L4L=#|J5c|!a?V-xH3b^}FF4KcS%Ul; zJzi=Y?^O?W8R?DQUZM{LYPv^w-Tv5d1`+vtdxr1w3LRVtj+T6;z|rYF^i>@G3A6kF z&LJCR+qK6C!3zA2*S`1@`(ix-Y&hd-KAh(8vdSD&-(!?VW7daKkkSwO99l`XOVhFF*kXRMr<=H`kGD>*2)@SF8t^xx&rJkR z1RvH_oe9T(0t_b10br|PdzF!Vzx6EmMbH1!lL}G@8uF{{o1?+6CU35+iQPX_=#^8t zY@>1g(fV<3j%jbq_<4Y!jDzoebEL>l#8fIoWYHULFc#NsKoCiD1UENvmZZWVuLs8EW7|S5SfD z;|o;EBg1Vbfv=~gJ+#eOCjZdxm8d7T?BK-@DhYvwHSitFwn?r1 zk*{%->gb3aX4KRxrakq$61OLQyi`ENwn+^>G~?jHZ+k}_OF)a8~3-59*^gj6!_w$_0>lBT0rY1KM_9{OTV2` zQB}v;@TDSeUWM`|P?x0qcy(WmJ~egCTIM*T#F-^kxv{YH9g-C&e2c7{8RPn#{K4{OF9kXU$!KUzglACpQbG70sj{( z1Sm`T^Icttx#uXzzgSv6Q=v#dw&?U%mFFjQb<;1kP3n?0`R`tB{EhjQ`Pr$bslBy& znyt@=wKUtGm|^_BOidg4lVuFOb{CPXwm)b>qDTwY>%YmMh@@)phy$Tv-HMZRJ7 zxzmmP+qt;gdd<`HTX`p${rBTN!^2Ufyzcljlk|0AU!JC~w0jKlnKECfuiWK6yS_3O z{}f4Ci#yMOzm54TA3H059G8EZR24dF`G);c+RsZ+J|9jG4Ya}dR%qkR_G|u_h@V&1 zYk_9JzVFeV*K3?U)G4iu&)hA)O_`752K{ldD`q}=koWRbit&!~xGFztO#1j{g`b+R zwHrUB>a$-r_J8MEqrN6%x%Qsuw{^KO|Is&%@On@$kMK|SN`IFzR)%$X?a=OBp0Z8p^3*McE_df2u_T4pt1zkjBddjRaCZ&W`L&C-oST27 zrfK}6Ql^1_Twv1p$Ia1m@Q=Bl$MBCi$UipTqPJ(!=5z6ntS1IRr{WgkAE{*ulQ(p8 z&-^26?Y4NK=>s+eTcuu61ONC7D&3Q#7JYD&fqy)QJb7x1GH>s0(&fps$cKQr(mslj z55ILV`S2rneo~bWU;MpMJ{-O2eC6tpPTeScYcrTGYY;l((8jC82OVGYjpaF`&mb?)%05!)grno$B-s{SJpJz&msP3 zss3KE2P%x3rghhc@i19Mgw19=qELHF(^=%znEaCz(Qrp&O#Yt7gT~yqs{42le^Rij z8$Xq;eV=_sv!_b%J@Jn#R%zo$_I3rYYU9WD|J$`RjsMs~TAKLBRqyNgk@hQJ(eWLQ zzyBIdK3Y!nT>qZcr>BX3wd(0YDlclppYHb)=ywhJo1d&F*!a$DFzN#se{DHo;D?ST zX=!F3tG}o5@lP}I(;0gIR?>dyD~b3Cqx_LKEdjmlvHimuRsBc3vHfHp;$GKb82x}y|8G4%Z|EvCGUb$_puKz3hmyv%iyYxhSf8+S^X_E87g!~t+ z&+G?{`wJObI{ms|0RJ&Xqqph5!t_`@JqXioJ?+ADfu0sHovo)wV>&}m--T(bp1vQ` zN7rcShcJCmPfy46PCfl3rfc=|LQJpH(;-Z+(9K2(2>Mf zDouDsS$SQx-R)C)#D{m(DezuXn;0MFY3)P)mC1+urgWFbm#grbwYuXh`xk#v>l;&h zZvAeSIv|g#UtL!FzoN(Y6eOU>sY^NyEJCE_e?yPW1&khR#XMD$fFAdxzs!-Q-=@m1 z%apzBQt5`N3Q@aJDNCJyx2VAr;e#pv=th5XwE4=>;3<52S7La|{zn|X`iBwUSi6@W zhhIyj<_FFyPh@NPvzb0EuQ(xoqV;l6r)O+G`+Xz+tbISeJ>>{@tcnYQf5im$x($1M z{BEt+YX7ny(C{D8w-xv4_>lMyJ-tAM=c4xsA2a`m3pIQ}__OTQZtFRYf1vSCf4}>B zCi{5Ut@A{WcGW8VvHmLo{^IO3 zN?c!G9%rWpaeY0`v$lu5^5co+L8idLy#rsK^i1~2*v(VgOc?1t$>wq52AId5{MW^B5!nM) zcJ(OFyS4o}^k4VQkydtfC0b9+p0!KYCnm+ho2qYoz8n7(hwrf%$roRR%CD_@$H~G-TqyEcu`ySGdR>s#ylgGLQ>lYJ9Whq))>OC5wh_mNkR+>brb+iGHig2=E2IzE& zax@zNka!8BScS^*peTTfS0GhU{yQ_-kse`B~6CCULa~PA1NeVv&-+h~=jh*QclQWAgJXKiL3(HnE5Nm3n&$S^h%B(M3vq zw<9q;qdGf&eC}+$cR*9m(l@Sxpom7Ah(J&}f`lTysq`WdP^$DM(m^^T6cHj_xyzzeT^WOV=-rpZdHhcD*Gdug)nb|peW|n+JIv@s5pM(K4 z;RiF!ow4jA6}X__b=e^ZZ(vRJNSU(qJUB>XJ^2_$l3ug zwWA897|PrrazXMA8aieo1wuh50r4B9PyC$21&1xqHklh`Beuf)&mdZ; ziE)5sqyArE^zg4RGT7V-Pc!>bpqM5Yu=e`gKB4-s#B*=>-cjn3`;U!tdfAN1aD%(p zsC44aEw4bL0%nOq@W)#MehJhS`GMo%y@xB2HijRXVQ=0?zA?0UX{>J(9PIXN`Tjkl zbQ+pMreua>rgXnzfx_ZnT75si|N6eh$1ir6rjt&S@VU?vIt>^56(y>Vz3`LAZV+XM zC)QCuraQ(cM^uZT-S84-$G%=q0s^S2%Eq67u)w&TwRKZs53#ndaK5ZSdoeBU@Boh8 zIfWw8!)t&lSjrHq6U7mvO6R|(E3tSZBGaH4Mo*Fb*FRuJ1cJ6JQ~Cijklf%>UDOZ5i>c_ zgeKVT86p|X@9ju!7Oa`gStAGfK{AGKc@{rbyH=N(g36fdBH5dWX6_?Fa-hVst<}Eo ziUq)I)9GwQ({-i;rBkqJBh7_Y(AX*V&Dxn;>Jh4-2Pu)YCk%0&KebMEX)}`?;GbML zQQ;u;>dj@3)+~u?OM0hbiSHIR{D(JYi{ZW6Z?XOdQ4HIuSdMhBhGM zB)!7%NRH=2@7B&I8ya`;m#pRXNtteg0!OIKH%#hqn*}`DusHwL$MHvAs~#D-foqV3 zhxS1|#I^;aYy!?>yXP!zd!joWjh~p@$AWtrNa2`J6DBd3GVeBpuIf1gF?z^8^Mq+< zjR}jtDZSQ&!Sb5d@xDB_y8YZ=vW#4}Kh8vPSyHj(0V`{-mOQ+Fz-sj`A{k-pP>P5sLSmbk? zcf%W^sTFd1GCY9n=HOCPw~x`DaDL0AkMfMQMnD%zdODIXrZ)SY%GnY$yv6@n4|GTS zC@6W}oUB)>(y*zrehHi|yY_yVkQeQ;Pasa<77dYEaZToNDW=d>FcEnFL($$N&^l8a zAU5$-`E)g~q3z3#Z~yI$@hok*5y>`buQjaij_Et=wxlhAVacV;t(!UL4+~r}+4Qnb znA@O($y@vQo3q&hYpB^CV%{ukQvUVCx-r>oZ)^C=md{I0*e`lhk@bqubM@O8Y$-XBmUJLr6ccHvls7?5M`yi-D z4{EZxq8)@YTE*)ZK-xy0YZH9F`Us-NsuY#a_a~5yXW9ioX9P-{pu++2+5YkB7+J_j z6_l=bp|tr*JLuQ#QD^}%X~Z+jA1m~?2>x0ieFAz9+?ePEFc5sUhpFU#7GiHPl8O>H zT8<6`3Cv$nKHG`IcStp$>n(kSfV5aO5_OO81GLkBFGB8X-I_7$+ocVQ(wzPhdA>()DD8M$y9*FMgkBH<$JmK zpg?HlktTMnu8%Y*s1xl)BL4XXO6&#;fxiVQY0e-yWMJgINDNp5$(5#RFGjL`v-8?x zjQ0Kcu1~#=fInky9NU8;-OuJx;CI^?WE^<+EC5F1D*pW41;myxLASK#uz@njVT8-B zWBCKPrCKk7dgSD@{r9u?RQ07RiH-eu>?v@gsknA50xZ)89H!KE;eUQwg^cUNiSNgi zZqWGINiCv6$iU{Uy$A85Q_{rYbJJ|BU7-C%sQsPZBg&K*ET)-d8t)uK{Dt|2S~Z{Z zl1=gsx(HZtIF~!ip3-lc7&x)7So6EdfRxyGUn*;TI8H#l8W52qE+uWXwtM(r)IDR<)I2#(%OsGQ-_xbi zH>miCP!Oe{6@X_}I2-@`4OJcl_uh!xUSJ#Be=wZ&oJQi8vT}t z#Ob(t-5RobdYU+YAp7He?UbAuwm`+N0rf5>o2D%(&JzkB73prs7GJ%PT55F#G)r^= z^*_qK&osiskK?R1+#WkcZgV(xZlhAkBKw#mKtQ%*KP}J9?kWHs&SaJPwH50KxNt;l#)A2~6h}VV!_l z_4|~XB8`Hg)-VC&OwcGKNAnam2^l3rln?4W!n0v6qN07je%y z(#DEB-T-x#*cmKJ4Xu~dV>Ym4c3$gv(=<%)vtVn3x$GeEQTu8@b9E1zHQwcdHS`@w z?8mQn?mmPWBv(!S#-|5@WhDXQ^)XKX4%#MxAAE(6W~#dlC_YZVA80GQ8r4<+Yb$CX zK6f7Z-96|$`;x;&d8AwOIR1&wx)*BZY;V_EL4*ZWx_w$*!dy`3c+R*a@+3XLa{AV;OWnqmFMFT)yDiZxDr*7Xy2IwQCuW@34Nlwmxaj}^AHQ$nUEeM&dABY4 zs?v+1M_MABdnEh|vENwomti@}i#`Jk=-4Lc+$sbRwD|noXtYfrL0Ti+!*%2_J{{-W z%lkg>5!@*{^uXgt#hU()SJerA$@l8PWVKRr&0dNpJy)$xbDlGG_iyAHqaFR;_m@)= zqc!q7$Rd6kC=E*bn0qPmi=k9QfT14xY)Rm$^On7L`{wR8LJOtVw)E>%Xwu5fbEGOU0^Y^(;CHzlIE+_ zFhc0}wc!dqE@Q&VEGXi*!dl6!hR|5UA+urr448X4WvqHS>+(qX+=3nCmC2(Al{M) zbRq4Gw8nbB5^LJ|2G~0z0v7gG#()CCYTr~fQCR*&sSc_K)Btj5R~YOq*R0e(#Ey0?WX~&QiJbdM_0O%s5Yn(4oJ7UdcckHG;df6%5J_ip?j+#S5r3f( z_{1t7VFjDkaPnJ;H9ssh`pL>A?laa6n{0WXoWN0T~X z6I4OCI?Dzdq%}`6oN(MTOV9j0nVm(ea!)6xw0b%F!SwFN)v`A;G15IJT0XL{=N)_Mzuz9s^Ptd z1PaY2(&#}k4%yRPMdr@=i*WY{>^t*p*jhnm_I+V#eVuQ%T9VLat6rFezoomG_v|U1 zhR4yLYZsbT0|Qk2nf{3WCDJRh9$8#6o05MH%0 zl!D2i@*KFKa_2Y0&_iCvk|;K@hu%q}e{w5XYZ5U2jAjM1(*;i8fV?VbDO-fIxA)dh zDZlk8QY?N)9rXhNOBor)bZam`UcqadO|g-Oxxe%Lw?nt*pOlo8q~wkD#+cq&9pUk- z4KBXA-$(f0?(I>f^saS2TYO%Yx^=T|(_?9O#y5kt1Uo-2x?i#LD4CXV)_st$gf}(c zT0(uKw2>;CIr;6B7-Cel>pLOZGUDP2s^BC}yFPua z{$`hIZ_>TmT$m@|6B~4g!IqTvUh9Q!(^TI}+m9ZlA{Cl!h0(8u^w^t@dAW>XI#$V& zwn?;Q(fYJW!u_Wjl4cwS(x{|EMAZ18nvXDLq~v_TeZ_dUT-l)qYQcv5k>5`}a#=r) z0hl7GRfnDkd_2OX>%mR+?v$6>)KrNk<=JIDCswSbFRGDr)bnGYmfj zl@IaprkfzX!h0{)CDzv&Oma(RlTvQmW*Nns_8fdR;HG-sH{^9MjhYC`GTFZ?r_A>- zbIn^wS-BU{6Z4TROUN#;|NR6kQBWfz@PKP8Rl7l8?U27TQ+n@5QGvPV-f6;=XO}A6 zr}ifsPfuug!lNF_=sUK-tD5X9w{I~fB3Uy9dZ<|L_fXM8{ZV{9x8S!vL>_zq-`=CZ ze>|WlyVL~p3kF}obNI9)DY^#}u~BPO^b+C=;V(Nf+HE6oDK1=<*QBOPptDkb$;ifn_94T>lpsptNw@N@WBSP(*;W86T`h{Af_SMH@j)e%Qd#{l)%5{FgV1h3XuAsPBblr56hxWn`NZXA*2($HaJtW(G^Jx<#{KV=D z;y$c{WF{DMeb9`KmwaZGMACcLY3aDQHMNq{xP?uvJ|GHd3p9Tvk?aEllCotdSu3RA z{%V;bk4zg5D<7}-7mg>xo(X3B(8}~^5P@`ZY4juBajk32b#n7(-RyX^%;|Q8Ih-G) zIP)g&8~CLzj}nKlngDHq784Zau`Z^^8z;Bqi`ES+$mgXi%lx91(;OAgAMc>Hjd*8G zU`@uar10VoDph)V`MIXdc(KE4yY;W<7`4lH05a#Cf*zY=3U1A;MR`FX2rAehV0H(P za1s?)Kl^}WImJ6{_{q7j^fs-XE!T@g$9{aJ(u?put;w`!bM@m-S;aZ_Xaky*R|VNH zi$F}6c*b*!qKHKS_clO41_1w*4dcy*(PqQs0=+4L*ine>d!MGhF38I{LeLDE>@Ge} zZltT#crGk(CM;-1F>t0tes@KFmqvbcLuC>Q6oQxT<&UJ ziUvqPV`iu>XfohJ4Ct*Y+cGtM#C?ljuFP1fk2Z@rK)7W-FE`X^7meDgH4V;m3&%gv8+b zHQZsj493+lu)3fl;(`LuWR0BMv{cwNBoUh!dxEkC`ka=N7*lt8H`^P~L>70(tID+H z;)56T%K=SZd06*Ybh*sx87}~DrutfZ4ByNdFykXR)fR`b!SUKR#u4rO zW>^nihQwh~Ny8m|+^T)&jJK^vRjP*6JHK3)Jgi?CcBQQ+LhhkPAZ0j$SL3nlJ>Vl> z0TC*gstUgbwS#b8cl@0jaoyq+EMd;nwZgbKnBq^-53<%u{#Qgk$X4VgrPcFt-h(~0 zN>0<+%pFm^4vdJ_U45g)5(CQ>cGs!UV7d7*_o@41UP0qc_iK>H4`WrjN54znV^Gaq zwNi_FAot<;?wGX-e{P?`=kx!3Rm@md^jgJr1Eb`o%6s`Ml9)8p|LaSb-TCpS;AkBy zruQ?3=T-+%-Jm-PXH3jdY+-boQ0pyM0YXfETZMaGZvs%&KU1wBQ?lQZ#fm9nMh7Q+ zry^Qez?w#es!a#iod^W`%WfUy_5QoSp5?L17tt4KD`J%%ZOF}$^0&BZ)lJQRC6}v5 zEjsxwk9!hn(UZ%?JtJ(KLicC@ma?ui;E`i1HCmbZ#z?zbfM-r;=&nr9&qFI4KjF&e zyGCZw0w9$XM(ZRA>HZ~vkcw3?NhaoUN}sE0YQ;sMibQVd9zq<$*kbv>kn6H37OWv{_BFVrw2xH_S8NO1r5QFtJ$5 zQZb()x7$a=;1|Y#o^^GUvwMR;@0~YW7xk*AdiiY;a{Q`iAfGzQCD%H9m9+=aez&%l z&oo=VZ@%O6L8$w^)s?XMdf*tRyadPe=|sfdzZ4G zy2JFedpB2iw_jD|z>N*BpzAyQ(9eV=UY$_)s+R^#v+Gki%}&0@4o6Wao&oAXxM*AX zg0Z1&fsn*7WI8%QwZa_wOZPlTE$-s^bzBs?d@0rv#_U_{Nwu+I#9-^8L&KqOa{+m_ z@YVKFf2&kk@eUEVR+@cCTWpKL4;FiFM53N*O3@$Q;QQr-R_HNbq#c52x6ctks1AX-u{_<4d`~ zxPZK@_^6=-i0=01sC}b|?a-y|G0tsTaAV|PzC7ck+~BubN94JP23swvH1=p!L9E47 zdlq`3!+N;$iN-z_jzzJZx4!NFI3r^}MdXuM@~*ou;+K(FY9gMecZ^<_bM=3>5ujjZ zuZ<*hH`&{`-USj*V28?AU7-P!w!@5aRm;TB<@<~g#@X<*`5Ue-=KiNe85{*0@>0u9 zX>q->Z_-~>peu4OP7RLg!@ARNF2Wm+r{qq}m&*6%f*UixOPEPv6RV1h*R+YUSbR6( z2BkA$BZSqU?5D5eUPRS&iFPC5CmA31@h!QfwG&P6o2?!{f4{03hs($kyKhJYp-1Vk zaskS7j@hoK*1rd)Wj{&e5>rC*oe5TenguEE)k^u6D|!LQ^AXk{wzg%gdN!+x=O}(4 zkq-Jp;Cds3s0k?g+@~<|xfy-#I1PUN_?y#39g=|URcQ4Od=WTW$}<%GYr&1s<7fsy ze}a`?aj21_>|v;0kZJVADlQ5#ql) zdv0cc%#Cnca%*>3E`}Esc^)yH#AW$piQ4+Lc@~s5?9DKOF{~+OK2tM?wRG7?zol1> z8$>jI8UP=KB91?3vN7LIoIjtM1Q9)v*jgy4)ncgwxfF2LGrWIS%6hzd!)4zWI*?pc zRVN>S?*s2g4I+u#%9^>JpW*2>Yxx8E$G0>~8r&coO&6aV_6k3>V6jxsi{D&qo?M?) z)crBk>YHvJ_a!p=Y}Q;}T5@YDHvl*~c)3HzC{39OF!OZN{>H-^^l zB}Grf1rQrH5MF%gMTR2YLL7c`)eaUnNe*uExZzIsefC3d*nEY+JNMlvygbW>&t&&E zHF_jKW@zn4)L?Q{_SesHP9ImB?Ied#Kcb!|Z`LDqeow!4`lu-%ML(Ha{KNs^%{OjI zmo5K^QbWhoX?5Iu`xfc9RQ5$mS;GuMb>TM4Z+6n9X1adXO3mews(4->5_MlPX5gY< z>|(>NYLZ%qs%kDF^8+KE2Y#~gi#-q}wc2>t4JpgD4%8A9KcJEDAZ5Ub>+9y0w<`L) zJO)jSdLC$|q-%O+?@<98_U?vU6y-wM<&gOhca(jhCSI92wLS?1^flTUgFHwVBsg-n zjOG(UfP0Ltf^WPWpkGw+67hmS_T4vsi``_0-7PV(n5g4>spn){FS53a&8r==G>I0s z?i|!Qs1n!-4&CyqtK->WIclC$pt0@>{;?f68Ujse2Gs}Ov`&~=g_#hWw5uAdj;xV4 z`{$RNkPOgmw$n4~ulIAwuHv0y6(D>{A7cEKaB@kEa zeSgPndy|)OXU#Dsbr?dHKk;o-(7Ij=j$nk>mViL>FP3&&r@j=R%qL#~Mj5ceXfg3d zkL@%fs{**mUJlxg8lKxRg74=lG^QRlC+G7QV1{d64|`wCN*{^?;HR=X2m0_j?*L}7 ztLLio647UXAVWacy0FM|gZ9Rn^U?!<)zvk_Z9mKQpU9SK0lgC=;`%gMf2hj4_J-F% ze2a!fNhG(Ptw~A13CFLp%fvHY2eyXb$YKsD##DT3tf{`3t-2CKBdN?ic_mMa7z)SX2T2QVm6}}e*>qKH;;fJXmV+9@>k5ol0#BSUeGL zEwvV~0v;$w;XWxMzOZYvexQD}dyt7s{*24Y#?7~zpDyaHL0{$*C|@<4NW5}bEPTj- z=JZ{=_li$>_)6mS)|@B&Gq@|n?3D`+qT@jXPX}|sS{(eUxLMqA6fQ6p%q>r9&L>N= zBp+7oHPgX~o??Evp7#Ah&k0%7|3|yuFkHc(-T#j4Wq78F%<=;_e%ZUgYqh1^$hN)+ zS8M6h5xUW;OVU@7(3PkXdiXW)CB=@91La{b3*NDM+HCMkTRmpm4!ULj z<-WqC2w@PlWnm2lnUn~#2o zC{83klRsYH)b|w%`=*5 z^}2W$WdOI_Z6_8LCV(ylrpM~=E6-8z8K#(t_2)THTW6U$|5I~ z9cby609gD8j08UMCp9q_U3mYMn) zx~%KQSI?*pft^%GdGXBMDhmVCI7fg&@L`i*u201Oet?@^1M1pi?x1Hw zp!XNDtKW{DuT0K7w;-!soVoWN6iC^8t5tD==UfTsR#*PuxGr;=->BySbS~%z9@{%uw8WH5>u}J9S+qF#pMo*V6+q+ z+cCM@)w}HPIueTG((1^HQ_Tx^gb#B-s((+@TnRy6s#kbJQMLa7Oy{R0!^Q(R^Em$P zdw3EO6}YK?Y%xZr1|X%K{{yf);Zmq=!P&%cE1a1fDv=o1mQ-!wdkJ&NEliEk>*G6| zhg)I!^$;GEWhtP-EcOLY^DX(Sz-#7Lu#`nl>C(=5Tjvk*&HG$e<62E{QS!OdD^+B2 zxA(!uVy#3{Agp`-9TTH$~)~A}mzGWWtn7FyMdE&XeIe(Dx-Ylxs z^fwPFGoG?BS2OLx)jMldnz$~X>*hFdoHyz70QbO`!kOu5(^$Ku#ly5vn-{~^=4L8G z9^-_3MP;+AxrMII>kEZ>sJt-Hq=%p_Loevej{^fguTZ{MaCnX`nW+n<>q zOq$P@?(C!cptNir3^w7DmZgMyF?ueAdEGGXuyEVb|HJg}Y0p$_>IQPqL9e4N-1zQZ z`m8Kn$r#j7ej}C>Ibb}~V~hu_ckL{PEYps#6;nL7@bEeRfzCbu{S=pj;&!HLWN5jS z&B_k?i`GB_39VuY1~_ZS+pTC;v^4IPPAZyi>)HF~d0{6qhK$I<{-{iew<}xddv3Ib zSM}LXG|^v|&Unxv1zmr^%Jx5C>yji<;Hdbly>f=a6 zN5;x2np-IRxWt0O`QB4zlN3FQz_ex8l_+=|SoAy?%~Nu{l>txUuXyyAtVbQc!vJ)F z)WpqGTy5 zMD9kY<%~!Bwe1~n(~a-^EK9YpcfNN66=W$}0nuMdiPcrXjjQ42S0HGiU1y24D0o;a zTPvUykQo8FyQ`SxKEV)%&@q!*Pk!K@m%27@^z|XDS|yv~ox&oUmQZ5kb%)#HHrI7y zc2&IZpG+20auQglu1H=lzQUj2Pv;2p*bbZ-lLayuGNmtYjbY2?#fk{~u&=|JbBpZH!{V0#?KEfgjBjw}$g)rZK@`Cl@g zBuSJr8!xw}9^0%=@&0eEc*lK@;=iWH{jvC#92QH(PcU=l$RJVWixn?4U(N-Z%MDt7 z?{UbzPg>OTm-c(?!lyfY+R0Wlmx&f)Z?1@Orq9$~UWgf_DhopeQ+B4d6FT0GkiS(E z5q2Z1XU-_ zgSrw`G1*y1s&W3Oh97kWU|AII8ZjL_-%b)ig*>rKSAm`SH;|-!kaZ!iEb`SM>8tnPe}ChnR#v z-&7;FJuWzR6Qbt3?<$*ifHM-mwc?NI(L?3mP7uF zmw`+F4=-sUKV5XV>lKrD#@>?%lRH2iE0F9pl|S_GTMEWMn>%?r;uwYM&>DI0dLL;* zWFG1Sfv(x?#!YnA-GGk0U4At~nh_)s&EtaP3RExSaVb+q#+uzgYM+Vw-fX4{U@*L{ zA4)o>X*oizj3@U0|0wRCqf`ISQNHs8#T@w+j`I%ZWpVWFw5Jy;krjQ`tX&bXiay&% zU5vqe78H9dPuWZ&y~dZjmPwOt>~lN4lS#*&)dhCn0mi(pqm@B4S)N#kNrm%OlSn+E z)5PonE9BA=wZao)x_X9r{c>Ag2s*_muM5zgu|lclc%{y2o(f10=@blXzcx2%e#{YJ zt#V98O~&yC44pH360kbz*&*a4WpUX#GNO*gaibF(CbcYI9#bCxY~JIF-uLJ%GI&_MId4>?~N=f@M8L&yL!VB1KQ1rvkz>bJ?2~ z<@2eIZ1+;Y3l+fOEl$*v!)UHy!@!NGHi_FN;EQm=iOv-Hf2?i-Hj+BBDaV=*xO=~&cf8(u4Lf2VvovgO>`K4wiNN_c` z8-Hzyg{JxW@}pe!2)1B}EDuamNs4IUQ6X81&5 zRasitUa;92u!Uy9L2&Xo67PMz;c8HNnP$>fa+2t0YX!TNmq_fq%Imd?s9PlO#CBl2 zu-9(^??BMDIc2uh8Fz`7M-6;wX*tiH1p-ww@Yl4Xfn3#hJgL zMA(8y_#IO-nLxp#EX1homBL)ICDJ%)`shL;p7ihEW_isy_e22x{`kvM$i!?|BZcm; z;Wm@nsWo4Kylq_muhS%-=z56!Itz!mf=cTkVw(wBt}<^F@Q*$x>Cpf z2(e7rVTZmnL++w%9N7y99bCR#etqGg(=*j6q26rwVj$bk#t_gYgHW0zg3!sG+g%Y| zBpeD+3Vb>D^<}Ow5DZjEBikpNkF5IBF(3K2!+(Ki!7Plk^ksG@F$&_wRu7OB7;Dd3 zt!L4ciA+=oM^b^GrNC-w@TK-zY_~J-;%t3y??vW54{GTUlYmqA^0F>qZn zNeWsoL1)h$`Zbi!(S<^S#&<_wMP#$YF6hYp%Z(RXI~=y5=Gg zudgpt36WP{Ght_UTBdUADv7?aHPDeCYK^>M$F3KzW)s9gQ0zy20jU+J;1fW~CKayq zdkFapZF0P@sB`%XFt68r=1;s>_3cZkRI$#Wnet%&+Hywnf?&<|x0W2vb>$&4?OSS` zTU*PV9TkjW9PySvqs@LZMJtE|u}tf1Yf$n$NSOK9LGx26@5C=Q`Qp3AT5|EYz0?OE zt0tEDdw&>~;2oLm{OxzF(x!IQ0zy15HxuriPkYD*vt`991oEHj``&Q>^VG5pXpXy# zJJ1STxzS!?aYK0hfAmzsoz21mvXD70RSmT;EWfy zYU^4ip>j+*;n3hywqdbc^5F`D1#Q~35A5zaHxtnhfKfEg!8{MvbIevNMMx+tAlu5z zyp>0Xyp<<+6>3%dZ^(t@uvnFF+6bI(Bx3JY=w3Y-x3kJu5_;)oqbIUM;~*P%2+SE~ z8`)o}c3?W`n&;rXVRo521ZHu0qZo5^5R@iNzcEXL>-0o4P{ZF>S4d=$VnZfGQwCnH z2RL!qNmK7rVjnmolp;F*(a{_KbfZ<{)cjNS-Moaff*?7sFLks>FWVClK3~vEzE|#2 zeF!eGPefflq%p4^;x?B&9hr}6pZ-ZnT8LYL$rU?Z2x%YN-#v{K?U&-|z1ET@JJXMl zx(6rCm;vRsNEzwb-jwUwEMu2ZHsE>n0Gak%<%#0&Xm5B0vs&=j`rMB}@a=*0xGSFV zAE;lIy>3PYe$Ny9)#BQlr*mCsE|)jMy^L^j(!FRDIYYnBJnIOt0!Fg+e8iBRVs|9A zS5&*p?`43SVkdTMVv+i1m;*Y3SgOz;Q zNj(b9P`+o_-wL6_WB)E$uNc3Nk-OlwBr^zRu9Q?wXk~f*P!iK(!7XytVXwCyWTiEm zSVuVu@V%NwPMXd?{Yx2C@oQbJ=eF*Pe+PB7afsMVy=yZ(5%Nw~n{o{1N;&ou-a-&Q zEO&LWEi=0e4o&@+LxY!et9Xn&t}??T1efgA@z0=K z?AT`dAx-phG%fV-pFl}^lEb9~c*Xe14Z{w&k7z&-6{ZD-rowcC+ z4-%D(g0^jqFoGS)2i1B7coh&r8FokL(&HQEn2L;<#FWa^=YM@K?FP$qSB%Sz+Mn+l zNWk8Ou$#IGMGwGyn?Vq@?JCQ*e4$z?8&9+VOsy0IaJJeB&Q7rKKXQ4FSbHKM#Y~$ zV3`V2lrdZMk9+7ZHHOjh>gd_uM z1@hr1D)FFytoKe8|Ag|8Qw#F3>|-|}Ew|kRi>vtk!CfecLmdganBj~W08cP~`|S~S zr`;Hd>&*36ep$sehsYgKV@Cda!5^^2lvMGbQS|sbiixuZ66MoF=V?;G|6eP9Wxz}} z(f_YIii+fpBFpCLWHb>=Q$KA|ZhVq(3we2}3Y1M0!y8T6{g+<~PB?qlAC<>?vb*6p z`oBC={2f~pH0lA~*P%GU6+kvQ@)nW;Nue_K&B`fa1n|}V#h+{2V6upiLJO)-PtUGx zzkOh)76u5D=sHq&yrNg`d{y;@eu5e$faEZ`EOe!fn8bMN!vmTbyq?NshZXc|A?jb3 z@lmMn%SX#kahRu}BWol0VBux%)p&-V6jljL2(EwW!TB5Fx3g`Q1_Oij+PN%h$`fi$ zX4+XT4fiS!3O39M6Xu_%4XQVG+Y&D4W;v$|);VaE#ogR%(jBkMY^c zGbM#7%qSqiDE2^LR{^C1Sw&oTYYL`RW$FdS{@9vyF~7U>j?3C}hkTXPt?)oVFepr6 zQ7MSsAfr}#SY*B9zAz3HL{Z)?2EYV})`_i}sQIQN&$O&SM_E0yUE=?o;tY#X1j zoV?C|%7pZSx_y<_4$wjsit#c!l0x3RE%$yBKw(r6{@s-yV(fYnKleQ7MZ@c=ZN@i! z+b;-$>oWPqK~I|uSqo&p-5S#`vRn7J{ZP7t`f>5Xqq<`|rXRHpbgqSDeAb-zE|Qt3 z>5qC_>s4x10L4MFww>y!LM)#wq?Swa<)-Tgs{n zBPQ=T$6*q@^DDIm^+R8og2y-Ba<7Y@(6%TY8BjVd*7%BPvT4i~8j}-%`I%plUtM!P z@Em7X^Gx~@{MT>fD;G}7Bu7=V@0C|-&E_oSg%@cavrVWB<~eNRx2*_N#*MU&Uq0s- zPu`ngtYI?#Ca)4;a;njwIQyODB}Gw;hD1x-St#w-g&c85Hqj~K*-?Mow%ZB7xuMde z>1oAxlL^OUg$Z8UHW7`u5&n}W1RCd9adXozpC?Tw9cykqQfyJ_n=aMAH^E>Wf_X0j z-g_ZG{>@QtGGju6pc|C&Iaj|j7Bt#dr$&Tuk6>lnBxXW=3_#nOy(QwuYFZNvUno^p@?m1_V@;_9jzaFU0ysCs(wEeiqs_o|cRe(>D2pb|`iv~tM{&~%ey#rBISt*R-5J_D5X zFMB?5G5v!A9we{g#PGy?by+oicaO$?A*7gl%ls{;=!`>C?7QOosk@PYs;a+i%X*F)zKGzjnL}-p?=er!iJSoPLy}LT{NehAH|=DzU#9FN7{`d;GXZdEw!BO)0^DN`>nJbrT!t^VCh(47)o}kT;}U&`(|(r|12L_Vkq>X|`KPxBj;}e_ z)Bd=rv%w#-z4znQh5n#ro=;($qr}4y|8K-#3@M|ri@gm{r);W#Pc)g+mW_iBA=#X3 zHuTWshf=vQh36D(YAiXRM&<-mJ!+hxU-IKuj->cJLzVao9^8cJvEh*5jY+Y7JKJLT zH|@!@O^(TmIp^)fZB`bj0$&_2h!k8aF8NjQwquf*M$%dEy|0fQ)~b^=QizZ2TXs(R z8goi?G2a7{wDfIU7k~V5f0?(3EX)B)^o&B%k@4Mo5@PlBn%R zaZ*B)?0kUb)v`(XS9k~aGrGA`jhdxAqGp4}-b=F=A0Dz5Mb#LtKi?B6fw9>0*FD_$ zvGw-7H~g^KKk=}9%Jkyny*NkZyrlwwF47vIU)aW{KNK7#DPfjp_Hj7Fm`rMWBI;y* z5-d5O?C@%ia!&1$dV@LN@65wiOrCC8(AVU*j6#XvLjELhlHnumCgY%286`Fj6?0A} zue$RTx=s(ie<~4oh^xpsJbC6^Y1WbMe9c<^Pb)jKXr9yn>Lao1?}4}nDoNs6ODTkA1o(O=l47<1m4yClk9Ujx6+@s<#D~EM@u@XM@#39XdxTdUQ zw*XX+3tlv4O5E=^U}hQCzwUNQcm?VvE>Inqg~%Ub4WA0ObxZ42D9l?0pPNqIKQ$?- zqQ_ThQ*h{a6X8ulrvV^Wuc$g15!Dl=o?Xcv4* z6xeCqtlmd-Z!SaZKQK#qDJweMEw~iT4M&WhG=mzRV?dn0M`7fMmnY4% zKQY7pU^L@_LziD+o#1$mSBh{kTF~`mA}SLP6v{5mo>|!Sz@U_(l9@MJC#O1}EhdkL z2}Tp=SZ5c`F&=3o{il|cQ^=`FQ@S43VKh!0CHAHop6*Xa68@>a5-B2oPF~l0nyk5n z;(G9qV{6X+nYVj0M0Li)G_K<^+NVhrJJ8(r@y{*oAN;WW;Je>G^Xk*DeDceu9sl{Qv31wc)vrCb@!2=_o_zkz zFMj{(kF}fDZFzC?>nm+9za{(e=#aODJYw6s0tszt-*aN`NxR*qjS8~$v2q}XMMTK4HabK+5-reVGur}e7Msn#NB>}(ZJ^B-daiAR%{I(-md&Qx zdi&YB*?QV~*=*gs7Nj#4w#>y0rfQfC|e7d_v0;N5R`9ew`I|NOT4wWnTs_Kmfh_MUuhSUf^B=}uJ%VWFT&{VM>PR(S9jzv) z7pRHqM0JvSv3d#nm`k}LUTw^NzB)@SPz%+6s5h!*>Kr-iV$MZ1jjd0;)ZEt6{@ljx z2S51X`8S_=_2Zx4`|jVru3Y`vC%?S4<FK_=#tQR`zKG`PQEIZvFVhuh>x^z&mN8O6TcLab*R z7G~QPac-1qRop)RQdE;>(oH6lpUG_MX7V?6H}x=`VG1x=OubBjrrxGLroN^iQ$JIG z)0w7V(*V;z(;!ob=`2&IX|Ty^8e$4F4K;w!H3&F+VOGAC!hX% z=YKx?eAgFWezkkg-mhD~+4tZ5-yZm`?fV~oJowY0pMUB2wez>bM~?n}?2qFoZSBm5 zs_7;_b2tC)JO-*jxY~5pKCtP zJi>gwInq4R9A%C+$CzWyapqCxc=Kp;f_aR2tU1{{-kf5dU`{npG*2>LWKJ_rHeYPM z#GG!v)I7y}nK{FJx%mq7+BdiO9um*dX#b_d^|10d3dK^>Y|hc~92~>R%Lp0$eE)*` zB3%3Z+(s{zJLclV$*#ykR^;h-R^w;$b`WyO2{Y`z7{+7O3-=a6_Z|ht2cl0LxU45JWp5Cm#uYaI_sBhOl z(s$_ntijd+*0t7ka^9a~wX#?@d0VQdU8(J^OZCLbbx*Ba^^7=wOupE5NqX08r5O{Y z={9DGbG)~Cvi+S^tep76c8lx0eXbl)TKqoo`_%8>emnjCFE zetZ1(`hD%!>i3P`KEMC^?f3iE?||QTerDJ#_ zj??AICxwXiiffp-Zi(xaalZYVAGZve>6CQ0R_lJs9o9RP1wY(;0=ez~nd^_QYfnF` z^Sblw8!KNH*PW+ddFIts>)-os^=q~@8$S7E9oL=bHa`F6=SN=L{KfBUH@zjUH~yAB zf&Fa*%tMBrXFKOCo8G-w-`;2126hVzA8~$!=k+4*|yR6Sgy5PM!!C>ui3nQ z!z!Dio@Pm(_DB`u?e&`Dxa5fO-!uQ``B7S1Eh54zWt*}~IiMWvT9=M}vFEyUU6W!( zI`+q6Wjc06Vs9+^7O5?g9gP z*di@`f`bM|kR^g#X3x#jaCYc@Q$@vY5k zHz~I6J(#jHY-idA1hAL(?`hHWUV*mW!F>ky4ILB`>hgid`!>m!(G1jq^9X3RqkMw|ZhV^GWhuFlWiJtWw%+j!S z3$yj=hW)y}h76A#X^SJ?)Wl$lqtX_|!E9g4`cPY+guegdc=MC_ z|MhpfUi@Dgoz$A{H~ra6_a72AWX-}mo_p)%bx$u^vh?1k?)$spipM6S6(@a-P{htOvuID;2ze|=cdhE%iEAFhR^S&VM`^|;Q zT_4wqKl2&aO>s8)e>+aP*}TA9ZLTrjZ>}>xYhGvGXx?mo+x)3Hz}nL)cCLTzC>H&@ zv*xLdK4LD%w0RE?45mjJ{yfMUyX{k~(qh)PyV4uR&|SPybcX{2McGPWoWp&1vSZN46gstJ~I${(0O` zQ^PeUKKRvUdvfpZHrw+@Zc`c$F5PnIE1TYOY~nY^PJaFKmgfV1v^{rx+|MV%U#OUO zdqBJGr^6o~8}a**8-F}8c*{dK(Pbe17vRzwc}xp0K@>oP4BY z>08IXe#F$+X}j*0pT28ZU}`=3q%G<7leb0IoUq-#`Q)8Vhp+zi#D;~R-S+dWt7|U0 z&Gz*>w)SU7x88QVv}D|42VY6Qqk6@-%soAi3`l$WWH;rib+&|_bsK)ZFgfe>9lJ)= zZYi^kJ92yP`EMSvJ+Ig{mn1%0cJk+s2mNy6S3NopytClv<&Os*PHXS%|LV!g%U^o$ z&3<iNj(&FN#01;x>t>vjvi9-n!=InC z>G0i0ULW<*bMwCWZt|1+GN*Q|8+YEW>c=Kc1ZMQtMr8VTlZMF%AZ#ZfD`QJO5 zUb}bG3%h6j*!!BVTZXRR{ME1ji?25iYFkb7e{+(PlYP(0zVG|q%k(lKVND2ugaAp{ zvXYhTfg}Xz{dD(C_vvMNdZzpIIqg-JWeW=nJ2u8RELLR8vIQ1cU>Rf(U@(~7fC1l3 z-}&7;w|>96`c~;5RY~vj=y_j#KlPUC>D-Jp-wd};>@N-8{C=-q`N?#+C>S=(3?KWa zz+`y#k3#t)hDDzQ!=do^D${0!TM7b&6~AnDei#bB?d{79r!FeWR{VRonC?3n@qYYq zXZWxA`M<4vl-X{7yg9qLwPm`war%=P;ewDe+_L$2rQIR5?O zX`<);7ZcAX|1|l{H%o&PTGm!xG-mVX44wSJRQRMS=qvgR{`b$O^XSHG>g14Uj$Qkh z`X;LQN&#E{{gMjMUiiImYp7@R6Eqy2cvGpJVVYV}9NrwBoDTnK#te7a&A>mOEDo)o zF{5iV6!^O3-8bQrGaLosk>-~0$^NOz8C!l`MHgEjW9=jj$g!_u0H-&f9STloz z)+v84I$p^pu>bj+P;q%dNr=6ICr*}4w3TG%hQoy?XS`|-hc)4%lF;Fqe+^>+$v>Rv z3@jWR2u~GPelo*tm>Q}4N7I>q-aBK4F+5%z4m~QG3R}OPHKUCUg^8BYk5`w?82cDc z=MNeK(}9)+rpn%yY1`CD!Bj*zoULsM9~o~6{nW<#xCCT4<8&&kh z@`>{3*50r4SO18qOey-6=kfd4Pd#rkgWBy=+3KykSEXK8+|=c|sxgFQ>i=$?|!-oyXeA1+Sr@A{iAT$U65vh&Bx zQsdjN-nUU-9p7zOm>bqkY6C4FXUc-gg6Xj6A)=Fl_j@QC+rj=*yE22RHvc5U%`WPyXyD-wMxa z8n2tcDo<|A_+I$9zeM}h^tUg5^Jb!QTW9c_@MQVnjhfGTr#3bgwgk5K5cr+{{CeDT066}g(f5ab{Lj-{J{HQ)>Bf8i;Em?T z;c!hcLk5-@3kI_K?W5XoFzaUlYh%mekFYj*^1qsot_U|SDVZw9Dke71_}`9*&hJ(n zAMYvdo$e~mdmB^-!X?vM*W++yC^QyqzPNha`{C!YFMs#DK-=)wxhD_xeS5YcI8OaYGO`g0Qw6uI-tNeS|mR>nMr2ztg{I6QHC-cX4|7}uRW(l-h4QU4c z$-g=9@vg;-W=uDQDvHmCkDIQJR8D_W8P5JVjmS2^Zb|zlINdV$M=c){8J-S?!n5v%?o2!g7kjhA;pgF&FG8JD zJwULi5(^A%`bkSTuc!roUD#v{{B=|7+fcsoi_k|;+Z@g~*_s$GUYs4yuVfSZ|0|PY zON^qHsST4;;lj;dhr$(Z&*PIJeQ`@5w7$@P=(i=o!6gNK_J7}K&Yx2>68;I(QrwXh z#`>lMuT%eG4`)wjc^>_$YdmZCzy1P#Yb^eL3;Q=Z;8UDP_B98ha<7jc1 zFWCI)j@ibq$J?3*TLO>6Q?DB)ty4YWz<(FUf5dUCfAr%1^o-)%fvMtmL(`jE^9KIC z?_|MmdZxl*rP>|E7u!SO-k+arG|UOZOQuVvD)Ryp<==4^ z>qTKrFm>!&OXu})%ejy$I^0`0aWcGl@*{MVwAEujZAl9i2Eu_?D@@bD?D7+BS&M?B zf$7G|8GjoI20a&A!}xUZtWUB7l>^@Z*ER;i155t**^hIaMNKfL>RLjmz_kyU**fe(PWbg0t ziwi&edSYUHXy2CjKzPLdx*n1&coY0GRPo#Y&d|{LskC)7eyM;jTd+ z8LQ2?*&m(fJ$raK6mka>XKeY-vd{m=Ge$p|Idj3xB{Nsg+%j|T%zZNt&iwYwlQYlE zOqh9P=B=6cXTF^Isj(y>RF~))>)0SytDkXCT4y1krp?6(K(^ry3D zFP^=2_SV^ZW*?vZ-Pw_|<7S_meQ|c$?Ax<*XTO>Kv)R9!{ncz>wrn;!yLvV`o1Wb^ zJ1{#mdusOl&sKh9l-(a`p4H0^TnLs&-rSOaE@}0 zdQSPAnmLv^^qjUi19Qga%$~b^?z*|V<{p~+Pjh4DCeFP&_xjxXbMxo^Xzmwte>3+_ za|Lsyb4%vFoBMvQeJ(TCJGXc4_}tItEuXh(-u`*V=KXM9(NT<{+Y1Ph=Ar3)$+U<=3v%!0NB0}G}W%w4#6;rfNU79Lx8YGJ~{l!aLf z?=F10@b$u7fvpmzi92E&5QOe`qrYzMKOz# z7iBHFvFPcdA1(TqMZa70%_7C3cZ*DmutoHu)p1*k2;%$o$Ek3sRpBJB7 zoVNJ-;@rh~i+{5CH;ezgSh5&dtXuqkF}b*DamV7G#i7M>mMmJba>?c;hnIYJNyL)) zC0CZ@EP1))^^#vM`O^~N68VzSC6!CaC9Wl&CEZI#m&{taaOv8mJC+_?`cF$wFHKl_ zb?NP;PnH%f{pHf%E&c0K$x>wLyQQ^Dsip3vol6InhL+A=wsP6VWqX$$UG}|Yk;@X6 zU0HT>S?;pDWj|T=ugm_l?3-osWo64MmsyuJF5{N>y~d=zI*w%mq#v7 zTAuMyIm@3ef4ltG%YVE456iz<4lUO#uUT$cUca1M-nV>m`J5GtS1enxamC&h-&qm0 z;{1x_71=8utteRW#fpDf@uwAn70MO56{Z!|6%8xAEBaOptO%``yK?2qjVpJpJhJk8 zD^IVCUzxHpbLE4TuU7tK<-e}{x0QciDPO5sX0dd%a`xxTKVSFx&d-m0 z{{7EmK2QDp#^*Vozxe#epMUxJSDy<%FZsOubL{iR&znAP`MmG*(C713EnBr})wWfK zSDjpSepULatW~#GJzMqTRliyFmsP+jc$IpUX%(@GS>;YqzgG zxc0kiPp>_<_R88@YoDwwT>I0tU#|VjTG85)wc52+Yw@)WYg^a$tQ}iBd)@MN>(=dD zcXZtk*2S&6xGsI&-E|Mw6|DQ&y5Fw*%R13Iuyn04H`-gRT^W~`sTe)an8 z>kqB}*7_f;k6xd!K4pF8`djOtt}k5w>-B$JFIul$uU~Io?^xfso?G9uesujO8y0L> zzG3}_of{5q_}+%t4M`iWZn(MO>4t(0Ki}|g8~$?xumRa%*nn@S+rVz<+Ay?Xa>J~R z%QvpuxNYO^jmI~BZ)4QP#ElsnZ*6?C@%6@^Z~WcHKW`LnEZL~tShKNiBfGIMSDSvi>B~+3xkLGQ{BrZ#&A;0G$IV}DmTX2g>o!+y zwr-|2vzyyC2Q~*cf3juSmaSX%ZTaq&$SsLm(zo2+@?^{FEkED#$1UG%k!?|Lsn}xP z;@r}@rGHCk%e<{Cwr<^eVC#4O5Ble=v0E=}&D#24Yu?r$ZT;7+f88qETDsM=mE792 zwR3CV*0HVATjy+Bux<6Wt=kT4`}VdUY>V5LvMqDl{cTUTz25e-ZNJ*~$8BG21GbfJ zGj98^t!`WEHvhJe*+upL>zdf}5lO3PzShQp9j$J#B@A&qP=p7e!Wbb&i~kqoojaP*m-Q{shu%9FYe6T`EY06&R^{Otl3HJ zWOsJ%?B6-HbN;RsyEg6Gzw5-VAM84_D|y$AU5|FX-SvxIzuon>UD93XuJ^kvyP9@& z?;6@QwQIrdmAkj^KD7ILyW@5z@6O)+VE60YU+n(6ryce{3Ty92vN zcYnHP@t*a2_Ut*a=LdVv?n&EoW6%9PFZLAe`Nf_u_WWVb*L$EnWqaQ5`LL&M54)#p z&(NM3dl&3oxp&jvy?ej4H*)X!y%~G&?R~ZP=X-y@_iuZly_&tIy_UW7|3U7(-Ft`j zhW9Smw`$+^eMk14+;?_g%Dx->9_@R*@5ld-{$<~v_Iv&)@%M|4;V+X8(We7w<>*oAz7w)BC;q2lkKtL*x6y z`#(Lf@W6@#n-A#I=O&G;(Oh;RT0R z9$tTV@8J`NzkfL9aPr}_|6gSszH|8L;ex|o9R9=MuMdm=A?ab{u>Nq(Ve)X};kLtp z!xM*R9a(r}{gJ&#jvo2pkuygw9l3Gj!I9@j-W>Vqk>4Hp>WKIVazuNi`iT8V{SnU* z|B>L4Pmaz#y7K6jqx+5?{)fJGG~#I7(X&UBkES2Jb@b`c!lPdt{oT>8kACwHNsl5& z%a7I`tv|{h?LHbj`pL0H$JQO&bL_;ifBtWsK9+DStoQd zvSSs;J{%*DH5~IE>pC`YEOhL%Qcq-^$T{)oM8S!l zpZLRxznqYqC_Q03L7Z@%Xg|?^V&KHsiT~F>#t37DQ9?v;Sy(Sf5>SFVfl*`-RfsA@ z<)SE2v?xXtD~c1H5uFvqi_VD>MCV0`q9oA;(M3_R=#nT!lqyOST^3ytT@|H^GDMl8 zEK#=Tn&`UdhUli~mgu%9M|4MYS9DKwU-UrqP?RfrBzi1*B6=!%CVDPXV+dA?p;#HF z!L-;rOo!>Qa?F5LU`DJGGhy$sDy$l-!D=xxhG8Et9J63njKFM|9dlq5R)^JNPK?GH zutv;17tPN|&c&r2K#JVsNOBGxeNCZ*=Adm^71?L3u0)+q) zs03Alv%>d+c%e?H7nTbR!U~~LSSd6K-wUgRqk>^UP%t7G6O0Q&f(e0B&@AW@bPA>f zlY({uFYpU`1pz^ypiSTwGznONOTY*^1U`We>&AL8Kh}%&VF9ck8^8v!A#4~M!A7wl zHinI36IckF#PWr&gl~ie!neXg;Y;BQ;d9|L;ZvblC=miesjx^W5Q>CCp+cw>!onNE zo5EYd+rk{-9pPQ!J>e7KW8ouVuJEDof$+XCPxx9`AutFuf-*s+z$hpe=mlzlR)7j* zLQn_^I4yb;_J+!sU(Zwqn+ zpg=CD7S;%Bg=Qfp{2;`I7NJ!rF-y&WS!M>!ax-LBn3ZPOTw+$45p$_oZAQ&yW{p{E zerML1_2zQ3!CYZBnk&sF^LulZx!PP~t~Hy@nE8VlH(ShBGhw!wNweMTFjM9_bG_MV zrp*oJMzhPzn48SaX1AF&d(187Rx@Yzn%m6nX5QRk?lgCqedca+kJ)eTHTRhV=6>^l zdC)v$9yX7dN6kUA&@3{Kna9l&=8$>PJY}9XM_{M0(^w=Hg+*gASS%KYox#px@z^;m z0XvT+VoBHq>>`$oUBXhZR4fg#Fz>@Ic> zyN^A<9%8xJBkVEu1bd1-!=7U=u$NdKmXE!{USn^t0_-hTh!tT1Oo)jvF($#J7=Xzz z5R+pNrofaKjFn(2OkjQ?dMU~i<%?d4UW?v{3Pf*3g`y&nKqM52L}HOdBozT7nFtig zMUY4#Qi@%mc&Y8Bwk6Iq(){?o)VwJqs3X`OmVxURtBr$oz)t| zUFwbzgJQERRvd?Ci{)a4_yVpJLtV>t~BOYc|)q*1`QbWECVnUn^l*DQA}w=5IVDQTZH7Ra zt~f6`QB|r_r!uPUI4?N$svKvT>ar72B|8(Hna(8VdFNH&d=OyQDr$JS&dZ(&Tm8evz59%yenyXfw?MiW7cGak_xG;65E7^6?g{v)Uv)Zb@ z;L31abzO2*t5aPTO^rtDwrMKdCbwCGY4q+|O|{#qsc~1j2@UGTHDzw2+u(lZ{-Dvg zNlm%C%B^$1cWZQN9jY5|9cV4pmFNar5#2~@uywk1s&%Xt*7dh4bVIFL-DvAXt4gQT zO}2(w%XGu7W99yKU;B7@u)M#$x4ol%w0xqxvwfs|s=T|sr#)0YSw7I-*B)q>8^^i; z<46~1gp8wIGGnl7xGT?8;m5SNP$;t1(Epk3A`<7KQ;VLXptlB7sZN$*>(LD^8Nqs39J zGKy1hoAo->s`MyZluBc&ah07x@A6DkE}};kllp(ZS~f> z(17h0bO*|TZbLVqE=Q-b$H6N*9bL)}rBCT|bUQj6eg~zlRo!zwaNc)5bedJ9+OBq} z9U9DSc7Jf=Zc1a%dfxoEBBClSgpX2bEJ@udY+q zX=qJ@#;K{-L~>EwNPDzlxE(Q8`d^#+22yHnimSz`c#XJLoGHnYoRL1YG*}z0xlo^j zP+3(sT{m1wVzW3~VpB!p5Akb~Dm-3#R{9>l40J0WI~z68oIy9yH8q5ZUDjcnLq)3W zste*F4RHzhYDxp*}`A{&$q%Z6l+AX$)AjLOpRJCYp9Icb9Q zx#bFQ6$r}4WaF~O(1@)^8HE$#8vKRjrG>FJS%bDw+qiAa_5^z3>{SLFe&sD!gZj2B zhO5xU7y<*A@w*aAl`Ot3CdD?fT^y24$R=e^p+04(E3qm^d|&cf@k zX_2MCatC;8Ne424m%tN1Vkxv_1J?nuMGJsql&+;6&1v~;C1NoMRz>L;ko&vqrDDVt;5p1)%t&t#WZL!9HabUBx)p`L;0K2To;91aPjRjAG z5uo3C9*iSHws>;N_6m|gFCYLagv8J@Xwnu*il9VN0=0-Rn|i|VbjStWFKsPdcz&Nt3MC8v@&WzOf$E;Z=vR57XsRgtq^C3QBbqE``qZynF7;EF zN8PTjDD$cxxdQ5Db(=cR)#xs8J$B`|p1X2gocgtkRS&4U)h%kbx>Nne_0aXq^}==6 zmG5HIcU-ONezjkH&()#sRr}Pu`iZMYO}i;~tJ~tPck`MKjYs2lcWLZy(oMKMZnwte zwz*q04elmQottqtyIb6C8mpVtINUBxyQazAs^K)9nr2P5kkL5ZUJdKE=|1RExC>l7 zSEV!RE^(E*^IQ^_%+=_ux?0^C&aA_9=Qu)lmP_QSb#WZ7d#^L;61a<;MHkB@7-IP- zKGu+IxMVnMIL)6kjJIDj#PDYfaeSO1l8@%&4X5~t_F(%1*Hrsx`(%5leXKpvaNdw) zm~JmMM)1+TsV<}Ov`=Hy8&COk#xmnOquN+ujPy-+8H|%%<;Eyqgzti((ir2@8d0O! z|G{5i!u^=P)?ecFp>8;7+PpUEm^nuDiMW8%j3|y-o92go%tr;HZ z9~c=J7`RY1J#=dLN=?M@<(jmbt2MPF=8>ivrlz^3X2dnxIGS7gxb{&kGy0)cIyO)% z8S5K6gBMEPO0uO{QVh?On(@~FXpvd+fj0nPc?I-a2dtMsITS~op`zg!SmE@kdQ_0J zTlLm8sP<{PHJsb7BXwu^IA5&q!T25V75owY7{4LSk>dC*=}qYe{I>ME^qSOyO92s3 z2*@pvMF1!*1psVOSc(84AOXa{TcE^p1)R2~gIB=_;xd>9j#!7SgVvyR)H-CHv`$$k ztYg+xFl5aDPZ8r*2ucAJ&?WLJSqfbyv&aM|7zpM?{t6zU9omQq1_XDXFI#lhz&#OYV{tKK_HoE6RiRi(4m zi91bB)M;>9oFAM1Vr`l=d}fh7P>;}akbbrs+PG#E{UtqHLNamAuiZ8qz>73vhM1hc!XXpr*s!?dIKn&8Wuf?$`L-eVSg4-yP8OxqI9_nh{OE zyWQREZgUT4hBRI70e2?n(B0%{T{f4)rE!fqN|(W<7}B|0T%E2#ca^)w-R4p`r!I@T z!QJI9bJw|g-5u@MUf7Ou4pW`nZ5`2liv%dGn zb3T*tysz4rut`n!@PO(7p6c&4OZO&;m>KapxX;__Gcc05N)EG^Hrcr0`!AHasODO!I^s&^A+wezH2VRF0_*3Z< zX|D8vl*HA53eZ?|mUk98Pzsa)Wfrvs20%aopq6rr)}jOu00l}ddJ6>H0Uv{jL>%!1 zOeUg;cp?XkCT@Y}h$P}Bc!4-eBoNQQd*CJFBJl`(2%aGxfU!gjaUZ-3J_U2Z+u(UZ z2brM9nYkq^j6Le3MFs)8NbSRrm^=No7&DD5rB= zRpqL8b~q!DcIUKeLe=YJovqGDq}%CrhE!2Vr;~R!ID4Ee&L*eB>36!FcIRn?bM`qs z&N^qKlXRX!nw>7^q>6I-oKq^sNjtlo5oo#VoeOtWxGb*IsM%HRs&Qd1tEh2$ zx~JUJnkmhsCgeWFj%!Y7huzceA@`(vLUWoOafdX~>?6*r<8*DhH{260k9)wq=3a4c zxtE+r_kwHJ<#H{$0pYpUr3S zHw{^aYla(!YkZn7)tBPC?7QH*=)3Am_SG6QeCfVRKFTjM+5Jv`z5j|2FxB}OF2+sNh2;3dEhJX4VVuWfUm(1P%VT(IAn%$sC@D+b%(kMXT#Uv8}KdoI-CRF zhR-5{&H?8cBo2v1;*l7HcGbDgpp>h@6^}Yyap+mJ-WA85Vb8K1x;|YI*QN981YEDq zr|Z^r>UwlJ28Zj8;kMzHA=CH4=<+lETh-Y$kHxQ~7U&*z2R@G^Am@;CXad^k3g~d- z6LALKAbx>6@z>I~U_AR&+$g4TqovYf09>x~=s8wydM1{MUE-HGATa@zKm}j~t`J3_ z0K7^Rf+X~cv_V#gfbPS0;Rn<`_&${d4>^;N3rHf;uM={24fhP!dB{{1s1C3-vaxua z5kD8_<9YZSX(R5!8*m24ELD~oi`i0bsR2xu_m=lS6;NyWV5tUh%U$9Ikx7Wa+r&*G zgSbaLB<>K`2nm=&NWtsGEh3%BCLRy~CWWsp`iJ~LuPFJ6+ z&2<57aRpqA%kS!TwYz#<9WIZn-{p4sTurV4*G06)mB_NLM3i$Sp{*|7<#lzrlG#-D zJe$O(u$S0KZL{kl8>LNPFR&n|;s6fjB%G2%I58*VMBIQ*$_?ri9K^}F67DvChkszW z%irX0@DKU>{Cz_%e~Z7zKj3rt>%JV{HQ#MtmhZ0bmhYbLrZ3xf$9La%!`I|5F)2+7 z6KtvpJR6uAh#HO_zE*>ecxo&op3!HuPixsxytX0uLR=ti!Y!6ESvK-gESG>1nWPqY z0jIHf;(YNdag&&WO2}mNwKx-pBnrtZ{H@e#F$07J1|Ja$5CU@v5PVF?K_yrRIUzNv zA`!BbM9F&S1@)ZDgI~fokQn+paud0Rq@dAsDmvs!LkC@#(5q|)8?8-eudtU{lnd%c zbfdabPR$MJp7M|RT*DLok>R0XSeNU2;CtwM^zjp|GBx{a1LnXZ-}Jy6u~JebZN>|w zC18xUSqw{P=rZ~me}lX64}il$T5!N-v0Eq$2B<+4L_ihzjw}TmAT6mQHKd+wgkHlh zseCFQenq{4^Qba1mOe|z({c12BnP>L+(ypOcaf`TI+}rIvf1o)HclI>y@Fn2v)Ff> zhBI(ux(ZIujq9{r8CT9dF+AsW+%v-q{;A=G;U)jr@Z9j+_tf{q_ssX$ms1@x96M|s zX&Fs1mw^_*1r>D1^o+Jp%!)l?M52;-aG|u`LI6*RXT)YRm z{42wIF5fVv^Y~x-N=>XkVlZy_%y5giR8oM~0Zzb+yDW{C1`CI`;$mq%Kmj6Y1^9|E zf~=**;<3CYnk+7$0bnf67S2KgZVPWQfnG}^&}wP3RD$opc1sB%B#H=_0Eteh3X~H{ zq770I)!+xv42p=igoLnxDxw{d51}r4Rgn+08 zD@hY+AqA9`?1Mkxdey|Q6QQ0 znCm)SfK0e<(UYz#^bItPzDxs%6iKHcB%2l^88nFGpf}Nw>ndG{JU}0#5zGZ`E*ix= zMQ^h)OtSVK`Vfs};+XTybM%t-ER)DYGFjRb?KAWglf-0dpP=Wp3Cs&LnwfUpM^7{H zOoH~J_6~ZENz&d$Gqly*6ZS4!#XV#nu@BgE?R_>|TgP41M(CfiHQZ(GJvNt})+K6l z*gNbgeVX=)HdXtW{lMWI$rT!CE><6-cW`eFwOq8`$wlg;^b{xO-xwaD^_<9H27PCoqT&1XpBu%@^^c@f9!ffxOHhH4sKAFXR=5S3bExz~}iS zyo@j8A%nz_@3R>J{$r^d#JtiV;@|QGywwQtZ+xwOhf(M&@^OC3DDb`Yp{5r9YhQt{ z%v5hwo9c{OQ=!jpvEXQkk>~U;u=$gCtl&DG?ZX zi9TbmX|HQvuzBoDwvlV#&gfm7f`<)G<2zGYmCnS8J1nIH1vq!-*f=iG* z^aA4*7m3@%g}7Ftk)V9jNheSqIGXlVt!K$pd5>9+I&oq!+cvGf2e;0AmE z2O0@2;RXYsn$QylqLSza%L$a|0^boPf&nXtHqZ-dh(^!_Hh_I#2iOWaK^@To(qJdp z4?4+qu#9K|S;#qp%6Gy(MYzC(~yU3Cfi7! z3_>C3v_cKHlH*Xcf*~iNQg{?x(t;CECRrJ;0iEk#h}uz@P4s^Kaah4pX+{DG>Xw6L18 zQ17W~3Zw4OTEvQ2kV@n={gQr0OKCk)isaKWRD%d;0>P02`ab;u5z+VP3gkT^q@UAO zhyl4vzd|MSQyN2F&|=z*Tx8V9WBMIZjTn(v^dtHqT|}#p8U#i1(YJINf*>ZO9MK_9 z=vpL~enaQcmzZSc0ey>kgWhMZFn5@Iwh(>5JYwFW8B8wokSRh_nAfNX6`=Q+Y(|1U zVeT?d86^s#Im~V5CX>mCQ6Z|MQW-6q#uT7(^ffC*0rWESn0dusW6~KAz0PDYF#1IM zmPJ?%D`y{S%h`L{H*5iWL;FB0W*=!EYb9(c`;NV@y`>egg{+>Hu^?N-qAbLgu}b!~ zmf`MdbF?~E#VS|Q)oPmt0%&Nvfec);p6?jlh z2O0u(f%K}=gVBRAgHeOAgAc1C2hR`Rs!13=Hyl5Fv*vbf~p;~Y(FxD=9#+3LJ(x7Dk2mt-Sdtv~LQFM}BWUPXuYT#PfM%5#B1fUxb z8X-SUCnw@S@)#RZf+A=s`jUCYsLYC2!ct;Q4V5Sw8a(ulla3Ycfw5iU(%>gyE0lMI4YgpZ6< zFvwe`41KPB&sXvN{_Lv$u`Y2nF$A7bbdwldM>!~p`T*OhX2gxiXclQg3Yj9t$X?N3 z)(`lZz}dkwgNegg_*sQdtdqb}rL;t?3=~AURArU>qi3J4{m+xSk?mE8IZYUxk3xQ}S_gf^4K* zR2^IoJK++#1F1saqaWBeEg`PLcfIOPjbCh#R7lDtMu}dck&fbJQmu3V@5hJmL3{*9 zr2%{xpR!B=)0RlqmeDOrRy#ySo}Swp}uFb$;1okXNO zL4IByBfl(vzc_VQ~-a&K{Erg3mmA4ZYG&A@-8A;eooF1UgDBGRvs^JCDQE~ihGJw`#Jj!#aVm0BHDgianU|ap0Y>TL*xbf zb^A3%jQz5Gip;iOv){B|wa43&?Kz4oitCD8MXKVmBGP_eaZ{0LkFY;f+)K9hryaD4rs!&#rg7Sj zV00%ki3}p6$OwYaB<-XLx}G-CrF0!#MRy}X#7g%g7Ftb@Arpv=4k127PuuALGL9H% zGp(h|>0v}ecOe5vA5uwsU=!1W3?bDF!;nk^N}&}@9cp9DOg%%Q5JNLHsG7l0GuntM z89U=(aI_XJXB?=8p%?B|Yg|@Mk49Rw}5-raPwJoet+rSR7fY$7RwP(F?-Yjp7_p-N8o2@_X z&Gg24Q@pqInfi2Zg!iiVvHpVhx;IyU#+#*&_g?Yd(MNe7=&$P&ypi5XF3o#af6Y6^ z-PR|2bM(>PB=0%zdG9rSw)c@f(R;%?&0X@|)IZeU*PrsHdhh9P=r4LRyfjboHlE`> zyv@M#PTpl`<{f+!PaFF99-ie%-pjZ1l)=T<^A?^oG#EPhMuU~_OI>#<;P7Cy~BQoDYkdq zPnbgfF@K$D!f!Q^rpVq=e@yS`-kVjOfxA`rs%}(u1lj`aft)H&z#F(-)e^W&jQBz1GJx(>yE}Eg6m}az@VHr2tgu0mtc8m?OqwEMZ5ZH(dCuqcwfWBVI!zC&t`2t9WaCd2ckBUp8fihjOqmwd-t*q~vIdWV_qF?c zfwo}3_!@4KM9Si<&lDQ%fcUQcx#FIkHVumX#C`kex>I%2)D#t27f}~cdaAS)ZDZOQ zo;l???TPSAvD2(p`@kFINBI$cjPLYy_(H}mUxO*3w>ywN*f}~Ru9Cc$WXiAOMR>V1 zh@Y{ZwPwkCi5H3o_J{UeV8-fSmLAZrJUD}3ns2A;Ep6Q3h0=!xx#?2BDUMs1Q zM9GBs7(R(l;}MoxsaYDr-%H2wDZENrAvH>Cq!YM78ZE1onxxgzbn6xCIqL;kvh|!S z!J1;dXibz|u%^ncS})74%HpjFvJC4bS(+@`7Awn=rO3`(&&saIQmt{aGqOlqqBTjD zF3YqgSxtzxqOm1YkR49Ex##0V@t4&5s9`4+e7&<@jzZEe3EWp>E^Mj=z=+U1H@ z3RnT!AKM{CR$ZL!ts-BMXMbc@D8vep9aI2{*LH#8sa>cLDW2J-_ScFc`?LN?;)g{%%*G%rSx<|Hyxq_bW}rDX-sKG=`cM=57Eh`eRMA!SK3L(G@LCRpzoI6DD9xL zOEXJzO2_Eyr5EeQ>1(B@8lp=_=(JKFomzUS^jv9tX(toe&_ho$38kmXu9o(pqs%1Q z&Gew1=$WQobO80EBWOZXRMP-6fexcnOgnnEDYB^x4KhJAr75+EN8_5t(Gb(i^f6QD zAT!PkF(EXnEWmUzKBkB1M#q>j^mN$-liU>F)PY`T8btfiw59-hs%e;+W@5{tn@%_N zGm&M9P3M{}HTlqprkJwWrXh3`jcJN58)4e%q^9#t7n}S{MA>!EHBXGELYwV5<2mcW zw0AvK+AL3`=Y!U$z37Sa#CvXf-f8c9Zg}2n(>#RMs;$-D^XRlGo^zfnp4%Rqwp@G3 zli;b*R%??yS3S2pv7QSagErlRYfakoo=R=BC)snyGmWbBsh)@4%bpyMS)1t5Yc1M- zLzE}eli`tjmHI-j*eldO@|NiH^|@Y^7tufUioDOgYHx}6rCy*d-7E%Uz7>%2vJK>x%$!N2yt(kt|!zCbVW zzVOQRfcN8)sei5qy#ntuuSAb{QE!1)=6$ViF-X1RywaQJjp{hx;WZ?5ToXBaT}4d*(9-nfqJjxIxw z;d)1_A+;mKXLPif&USEy=nmd6$wT^69alR}cSLk_8!mTD^A|djIx;(AJNgVmzB856 z#)$47->J%=FRn7Oa>SR|8}gm*p78npFRI@By=^r+6V$zZtGZIDq;D;qu2j!-ZmKFt zWxAhEO*+Y>lV>K{Y)i5v%UW#7i>!U$_q}c^l|1PzlXNB1lj)hB6se7(NP!{+5+nqI z;HHJ9ND&es6Er~+G(iC*2ohW^*_qov&L41|bH4XE=X>AxmvgQQ*OlB|?!EaASBci3 z_s|d5chTZ?;yQi({`wADj^3I}(bV<(xAE)bbs3kr=C11#6`XK=d;KuMSV#+HX-c#u z9w%N|{FXPCCyB;H!15^3l6aa3TV7j2mgYpn612RvTv=KZ4T*em(wa%;lH=BNQc4QR z+;uWJVdayf)5laC%#gh!Gi)EoyvRJuv}d|9y_ud&f97ds(B7Tt%M4~ZGkEqQdzF2a zC9-7pGJ9=baPYZU?mEZi79DT%kvveyIMfAAVXu%aJa+av*NY=XdC^-uFJ?E6w#rW1 zT`v?}iWja9MW>?G{lwkkZge-hHpwg-i zRC<+8rS*Jas8t6Z(6i?ORT`DXGw)mQE&3XOT42mK=UetY1V(&QzG2_2Z_-x>JOGw_ zL%vbpv~R|@;+yc*1Ebn{qJgL-T8SFsF)^sEBA)sii4koR@rZarJRq8hA?>iXji@84 ziKoOvqJ_9mRDsoCum1tq=I`>i`fI@sf4l#MzXt5~Kl4BF-v^)jJNL~=@)$jR z{swR(7&JJ;g~JuwyN!PT&Bn=A`RVaiucAlMt>|zmmAlHPZj%S`m{hRGs5Ff8K{SFB+K1q^Dc9=pflIaJv^m`CB;^lkfE zfyY1#u;JVBH3N@;Enh#e?o;}@h(4m181pas2Z(NB)IX!0)^-v-#H@BoJE`p;28jv( zyuTTo^H2L9fwTT`|D=D$Kjm)$7yL~iZBPeQL1i#(cw^WL?gmf8uqkBF1c9JC>TCQ_EPr8}`` z{g7O+uGwU^QTteCH1j%}%q=-$c{YEYKi+hD+JHIj5OK1(ee!;e`3R&dq7>|ggU`Cou5{tds<-v+jW&%o#4WANHw2*wO>a6fn$)CP4y zBxnjkL1PdM9t6$7w+4NXGeiu$A#Pv|QNvl-Y(h+U`1TMrVWx{PW3ZUKVPDu2J`e9k zmC@~JDbmH2AzRT8hl=QCR36=lu17bb?+*tPeOxcs&-Epqpxs<6`V4)F_HYAS8#2=$5dd!}% zr*d(-loNC5oY$c(oNkV}4}FMlRf`6nqZ_tmhpyn-9CvrfNzawX>_dHPTA6m&Zw+EW zOYkyGgvqegq>iehgIqiM0-Z{XCnge;iE6P*T(QolgV~pxPL*713%WNgzIAOk*aMzz zdN%K#4k-o|N2*sImEXI0zUkY zn6C@y1gyR-?J}`MY-l&NePA!R=U4l;wF!eGC>ZR)tMFd592w%K6I-@T+lu32bK0%Y zz6#q+C2hW42!Ly({m@#;RFM}6BS1@Zx8d8RnAQe6`(P5`45O$c3 zgPve8d~6DZUx)qS{U{hUM0HVZR1>{}R3KGIHF6NWivZDkh~1=*Rw5(ZFgMMOa#P$m z*MUxQooE+2!HsbR%S<9~nNQ3n<`P9qohY-`i>uZL;+nNqY!GY2ZQDXxVH>yK7bon3 zozAW~GPzX;m(MzKj+e!ARnRjAj)Rw*r(5?+K;$&tblg$yEv+fCC3M7JW!uOFGu2G!IkejfQRMDO*6`{IR zz4T22Q@}K!(QXrf*682YPJ?8yU?>_+OsA$=qz9ely3xhNJ4=%|ow?c^Q!KdW-G|CK zH=mB)ws&V63rh40cw_|AN8-z+c( zkUpPJLtOdhff+za>=3(zir6F6gq8rcTCHBIAPm}l;y?=!I_;s~>^J#gKkCQ)RzKpm z_-8;W7z$nm1Hm`J*THv&3h2GzRnQ+iGu?$g7%HJ#10B2r1%qYKjiDS02czLwxD;Z- zFHP5Bw}}mh!*9b}*ky`@>k%|+jy^;lAcxU91c^2vtDKBmVlo^h{1{1+pkPGs`NU#p7g{mMn_yB5v zqQUE6Dp&*Ehmyg!!F1Sb%7o*=T=>G24@=>5Q!@)Uh`Nws0PDN zwPpe(FaptD`Y-&r-{;Q-8zCu}4K_i|P$pQ$6vOYrr0LQ`m@1fZ=0o^>_%?hKE@cW~ zSJWFli@KvPqn_v&+>} zq+&v`;kGMnN%03A1(;B>Y!?zkdZ>0!jZe_{zTsmeKj(yWof56X-U06D$Oa!B(i0ehfW>oZP5v+8tOvFUay9m*|tsW++m;1N}Npy12- zZhZzI@4N98eeZn7#3_MlPY8=v4$8nakn&g3&!Kjxf-a|P7|P@~-Dhf8|g)YQ6d^O z1)^8c4rl=BLHdw>WDb2DC8I-#KWgD%&cV%~2)BURI1^{!oZLff5w&ssIm5nJ)+h+B<@>>#Q|~8Iw*FDfK_eXP4|f-q9U!eX>7{0D!r55x9!;g zo6fdiS7vrH+nKG5A~SDaw2St)*+}+X?l$)!_da)%TX)DExWnhTa3u2a{7nHW+!k7$ zZO+l+n)BtB!hOek*IVh8RQJ49-fC|({s6DTVcvKLB!m z*8hgkfY<&~N zR`pI*P~G?DRVZKvp5RA76W)f~fJVF(e}r2BJJ5n7Kr?;}IDiwt2|UCb@W;3XI0P_2 zB(4b%j1e9!p~bbYv{%{-ZJ2mVL zwP4Yo^9%mG{{T$-dH)EM@$ZB0{19jY6MiEo`gLHzpYmt@l3x$T{V;f@HGqfU0Q8)G zL3h%<^i#Tx9)ddP9{L&GNW#52hQ_95cZrOj%RLB$~AoC1}hFF79<;4V2ocg7LiI(orvp_{0eJLlXS$!(xP z?ltF65Q!InGBtW)BI2&D}+$aY`@Z3k&> zTAw!A3~5aoNbA!3X}t|ht1@f$Rl7O^WcD&LJD-hZud{5H%ckw|Y|37qzjTleAx}7r zh01e$P65C6*8kp5b3@ph1Z*{>jcK_(k*&xV9Mz{M%2`F7w*!BQ zzrdg4E}$KMhNp-R{!(g?9-_yfacF=Zru*qps1%-nmY5YJ6CfDbYo7L4<8c8nGSPG#Mc)*}Yp0-uJ3r{0!*9yKy&gL%b()M4ou9Ef7KN zEm0)i5wz9}qF^O8PEXKlOa)v9m&41<3bV=-qwk_JBp=O13(;&;jYha#RE0(p!`Rz| zBDo|ki-%UkI)a(gXnH@hZa?4hZGBL^RC<7iUJHnCm8l!OP2N&t^;^z_dfPM@pgMVz18Y|Z>_q=`^?+st){xXmFiyaBkv3EUG;r+hxdWH zPTlIQQMY^R)pyi=UO&J9R{#MF;X!}`M)23bFg}Qn;b9;IR8iyjC{6;efB--PZ-9C- zqP;`jBdf>;vX-nN>&S=XTkW;>F8P3DwNdSTvYKSHG3_zTB!_C@D!2~bVm6s=rUu?%?!!uEhfy%qa2Z?4Dv%0x8(Bv-kWHkN{Se(k%GvkP zYfj?!Q4N~lk{rhgT#QR`04j2EF3s`W3>HfWi3My4o5$v`S!^1+PQ(*aSR%2A@d++r zv0_%4xGJuR)-;wj+YT}tc3noy-pOw{DvFiGD~ErJR5nue@G_Q6P+M<-2+%}jxWHC} zx|wRB9#IcqE&6)vN;%+-0uwl=eN1gY4X_Sf!K_v=6Wn^GWPwKV415UhGIv;d%LA^{ zjc{md-#zGksGh<(AO>6mJTQqrCY#AeB(F_q7VO&75qZ@T&?gMvN`C4gmN0hAcjnc0)yCJv3eXV?}3@H(J9Dvb+20Po%gn=W!^URV|Al?+`HzT_Re`Xy?yFc zZ@YTIyWySnE_zqIquzDzvUkKg;eDZgqHa>Rs+-mQ>M`#o{#3o>Ejv@-?|?ac9bd(@ zxDsE(cklqbo>fEwSx6Mzn%!BzMzkO!vmERX?;z%HHw3c!^28MTEk;WxlCF2g?n zvp9h7;d)#GMBo6|;M=$y&jD#*9v6UpJP9n~?PLepOD>YP+O+mL`Cj`$J3x+;vt$=J zNS5m6$tm)kwxAs&hsbASQrk(kk$G)V+edbj8SONg()N%`?cRboOXenBxlI1c7*DnNN|ZH8jCN)GUs2%DGd=0(=-+*hBADpB5C?D8P z$*EncmkNOzYLXhI&cV0f3dMp8Q~)HvAc%tzaFOb#mZ>gki`t`JP)GD@x&?YkpVA<` z13BndG)C`3S2Rj9pog~51g(J-kdoHYF4{&1XeX_QNLme@(C4(C_R??YecDIU^e#<- zMjE04=#VzkZaPS7AswWlccBB?Pai-kXb%FR5RKDkG)$XlC8VOaAv>+6t+aur=q*S` z1N0@mM|Z$DV`Ws#DbotWjDvX!o0&EkVBE|egE49bWsVsqV`4x?#~d(6jE^~Fp1=mi z#XN`gjE1o=op1}>4nvHVc?@6Bk6;_q40prNU^`=E_L-Lq!gRr2<^}9wPM9<1oS9~4 z*nYN;?P3oQ6*9-pvODw(wwry@XEjJK3m`hAgB@UZkOg*y-9;X=!|XWQ%r3H8WFM(zL1c=3#163yY&+Y=s*ycp zgk5Eq*h#jIRU*}VDPO_E=pDYumGSp@2t7pg=nYrP-{o()7XATW;O+_^I0IV4H}fd^ zkblor@>P5}|A=qm8+apXLe1zq?lF&`b^Lw4o^KLLg*!r{P$}Fb$^tD?JBz^qqgtbtr>+qV^3xC*_{0_lgqx(X0kWg zcUdTt&FaMuSt(n{-e%L;ntaw?%x3cU^VRu#`Ko+v{*@!07xUS?l+We;j#U1zaHn{; zc&~U=M2ZhiIpu&F!VP$-rd+o|E|VMxK?cYOT`+5GEB}OV<9D&gf=S#gMz&7er|x6- zk(*cUc(=V<-a+-SdPuGC+Hnj=@iL7SzpFXKUAP-Z@CwZteuBfe1wX}|xDBt;yu{7; z5pKlqX(}}i{1`XkcQoaiTHPjjU#B2<$tvAl-5R+=Zj&3Nid2&TsUcU%J32WjBlpOA zx^=QzSE<_~>vT0bC7A+I>VOhJmPV+3Dh}$YBxs@_Dh=|WmI5ge)KO;YkTOt43Z@cZ zj27q!&Cw<(N?RZlGD8U(gRBrki?j_wphGB5+o3T1mQK<XWlZG%pe?RMqrWQm_9hhgqa@r74wEkFf_w3 z7;;T(S(x2t)vS`WA_{gGma!IOn?(^7yTcmU19p$qu`sgBnpru!&YIXm1ZB;Lh1IZE zjGhHphy~d#)`%di3E5x~q=z@KhioSgAr^F)@8^5@4t|`sqtEyOevp5`xAP->7yq2^ z=EwM_{3t)fTTu+Pp?$ndY!mje=fV@AQ|K3-2@}E|HYB_dhJ`+1RA?7;m=^00#)MX3 zL{MQpLbos=JQXz90oEl93Lw@is4)OL5*^}8@kDfsX3;60i{pY-^jJ4pL_8JkA|@V+ zu;>*nA}YGA$Kp%tnbjtG#9*3EpV$Ivr!A5`wMElZ`b>TDvEEnlwJ?+#VypmoTU)m8d2|h_sM#lmBJ_qbU+wu zL+pqHy>+l#DsMu$=T&=0)#vyXUacYU3!K7zIF8q7>NNqpPUFW({1U&fsnxu~8+2N- zMQ0_Ob#@XW8+A5PM>gqD5+)JSK$^)1IumImLDE7Vk_V)oe5k|79O$M_C>Q0V9F&bZ zrCw6U)De{dv!I7^LutA|OLT@V(jMptI)hTQojQe%p*)?VPaqeRrJc}AD96me<&m<; z4KoLqM)FL78G|R__e_?Vf+yi3^MSc#rr~$Y88r@D*%Q`@xY<+0!P?lDtOIeeN60Z^ zXHSq**2x~R$LuUT!LRVA=sds3AET4}EWf}z(Gzr;pXO)yC4P#ZC=qH z?#fi>ZtVASxAv-BO-|ux%x^p1=Wp{j`Kn@d@qV$U*l-%#8dE#SJot|GTGQ!u#bezg zotG+rKI)vhp}kN=*sv?9_w{%&kG*lishd$8n>B>-b0XlU zx(?kF-BaBu*{XAq&vhr{GhL3tc*HnJ?R{zXjicRUsPkLzmE7h=6?18z=~cAQB2dICKFugz7?fLN%c< z^f1Igub_LO2ci3+`cQ4?HFP(01=U6t;AOZzvI*BkR$&kFAhH49k8Htfrm;zKw2U0%Vj@hV=%Z}V;xN7cLseT8120Kda~Q918N*ZB>eLa)$s zIhQ+wZh{d$WRwg~j z5w?(+Py1|z^!xOM?b4P@mrCW*yYy}PL;5CNA)VVu8*cO3ycvW2EaS`I8PI-_InQ`9 zko~}ZnbF&I_7C<_M}4j?*N}UdD|0-`Kg}y0t@+3KC;7H~MIqoQFO(L_3LhNr9azCy zD0S8q>x=!)QRhzaxVWWolV@aOs3G!}bz%-o5G$q1!nPuaKhbpRnnO*Y01{f=?L-JonVrf6SHDQys}nF)zX!%MtWs4*^TzbT)Cqu_bk8bP&w`t z%AHEZkZw@-Ec86I2OD_~G4V!WSE2RJsb|#;{z4PMFUboMCk3iipP(M=lhh;qi%?gn zBh(q{4Yh~*LfxUBkOqDf0pK(%vOJPxQ*0c8`9t1~)k^i!eW^|&GR?X1qDnEZev7ks z6u%-zbObr9dqw(5lDwfh^b%F1p6c86X{t@1qeeo{^(iV(WvF+QNEN7O`WN~v^+cbB z20|%lJTw+6Kt*UGBtfI0JTw&=3=M}eP=9DBG#Sc5)1e%+4?m4`M%p4>k=DrbNOzh6c^r8bc@jw>0&>Ha#VTT@u>$*!y%Q^s-LiLMMYb}QM6!s4NNffXkqnz-v+R5J z1Di$?NS<}_VHDvRG>Tezlz)rbcspO%i2$D1~gVZ4DA_ zYnJ@BL%Z4jDudX`Ou%-Pf$bHJJB~-WN=HksGp}}Zk3pg2)97k7(ePPL*#^A5@($2@IqkyOP!g`iwiFQ~gUF}zcAjdy8ik|IZS zLGm>jAjfne@(o!LC=1*P+zr$Qssc5E^1v-s9k?H;4BQL6r@Hi|fe%!tzFYqunhni| z=0eM%8)zXk6Iu){g>In_&`PL3(iiE8^hR_r2y5X3xGq*7tB%#g3dnmTk994k81if3>hUSbW6H9 zU4&fI&FfZlQ@U~8q;6IBmYmfs=w@__I+k44P3yvBU7%P0AkeQL)Yk|4^h5dq{XOG) z=z(!9)L^`0l!a=H@=%4b+*oIm zxy;-Yt2CFJAI2JDBKjaEpb6B=-!UiAb3Tpw_!N4D*#r^`V1A6k80#y{D&TzB8nH%f zZ*8GW(DB-#SG1gtYZ_0RP9L5&pDJD3E|p8+GAK-n9hXtD>w*-QZql7m;%>ry;Z7^F zN=XTM54}e3wpyWvy@(g}ZmP}REwwvcL*q_J5o$DIumv8AOhyJHqY*1S5*dn2M1~^+k?}}dtjb)IdK7yc zYl%G!+%rFkHOKCntIe&kOI|_=p5*PqYwUiiHdU8;EXnI<=M5}6@i1UduH108{JeS2U+KmEGJ*lcVvszU0}bYwQ- zgl(_`w!<@#sYqw6J@z72XYPnSjXjIinD3jP$7;O;4P?1wUGFt42VpqMXo7A(`yb|+TyyX`QnmNsqrt)G@Gq0(*Q0Q*SZQYKp{AySC zfh@hslS*C1Rgrv07RW7K+0_mCo*WH~1cm}#fyqE`U_3Av7z-@wXZ78I!N9zJAh4ic z(hmow^wawJz-(Y9&=;5r%;{(J(}A8qe_$eDiWowgkS+8wqziQ#EuqK8He;`GKlH@- z%-CmqYV0u@LwlhvBNS>ko`$-O$DzZJGt^-OLMI`6$Q5ce_8XrY!4ML{LXOZ2<3UIp zGKb)hDTIcsp`*}38BKC&0lM2rz_WG%82Ifvcw zN@O?Uf;S_lur4BxEJn^?D6$*@BC5!CWIi$%kwp$79@r3BjjTr+%}>lN=8c#v)*ovy zKQKQs_r{vdld=IF@YbC&BtbA1F^2yYV5IjEw&IFjSa{8Vq>x8 z*idXKCXY3l=VB|dsn|&Dp;-}`h_#sqV;Me=O1#Kd#&6KFc!95oU-LQsJ^F^H`SN&a z`~#oh<9wJewZ!-||DLa~+=<7qJC+YsNPAP61kp`rcZA$8rdZlaIj5I1uNJ(49mbMK_JyOCZ*g7Q67PE=A zaY?cbOS4ke)-R1oQl>nc&D>`Cq<5L?Olj6;Ph>u1G8wDAGMmfXWZq}kOj$Oa$!GXX zGEEe`g!a3=jcUp>A5iOn+3(Cr?g}`QPPAV!@>SfKH ztGib!-Ku^muozg^ujrQpYkIj}reD?%7`>shkSF90`QS^Kgb5gjFW~LiR_v)+5!;D9 zGuOrM$0eaUUK77-xf`#KSH)}NNg*Yq1ql;`G$sm3OvF;yy?E9-mKsTorX*|5nz7DH z3sTN@Cwnh@H*2>yI7V|Lx#8S9rShcp^m^;jY1`@J(>)iauqrezQ~|iuE{npfxN=kO z_e#I}mHSqCqb%JGxIZYJ-aR#-)~JuZFb!(8-RtmLy&D>t=FWv&v#Ggzv8GwktZLRZ zYF+gesH?iVf3>fxx!TnOI<0O`r_&wiG`f3N+xpeOdO#jf=@t4N{YqdhAPa2hxAaQ= zrXCNShkT(S{DR{(q84AFvSVO$VlEWraGpV9=GWE_{u+FA# ztkbDQDQ_*<+=cR8NN3P#^@_lDK&O8dnl?@tuR>EsA~b0vLrCN`48)$B)vLyFrp;gyi?F4oM%E0T8KQwEcGg6^I$P#fxf-oAvBF7P1)T~4i~R#Jh*e6z@T3;t{CZ1C=?9M8yAer z#zmtm@&*pWhp~RMA*PSPv0n323lu|QrkF9-XKu4RiMLpu#@phpmPeK+mdBPm$(rQd zWOeceyGT?e?wBJvLX2(`5xO#sZ%w!DqFQpmD-mo>>3G>D($V=$Jv%_bN1MNWN*vP<&BQ{++)XF z?ooC=A8~X#+lu=|le*!mB?+s2k@HAL{BiP8veu@Rbkd1kTU2OJy;&a#MME3Lx1n|8 zrjZR@M2N^`>5KQoyW@lLfp~wsE8b=4jQ7TSEKic{$*0NZ$!E#7hZGVw{Y46G|ihfsxP*T zR3r`uA{Q|{Hei`E&zs2@5xb0y#V6wv@$q=SWgyv?>`nG3AJ`64NUFgGr)-i{a!B{= zcBwx*m>tNj=eBYixy{^W9&t1mS_+Q}_nqBNeQ{T_U>>xXQ(Jj^vF2jgsnSfv(UepA z?<*J7n%9wFBmoQXtJsox(R>vfOj=Ub6qahTHQFB9+8v7AbH`3zk*{@L77!g5dJ~}| zp-4FPI_8hPi7~Mib0`*wQ879ej4hj2;|uY*xGX*&pN=oaXW~=I*|F!dh;vw3bLle(ou!XaF)k)ECu2fv)qEXG#MaEJ__$>|K5W^DE92_;gk>kb72k_*#&_c*mi73kWz3?8 z?Zz^HzK~2wG3kLlA~o0(5-Yuy>g}BLR*Fhj5-kZ5FELV53QDh} zxI{_UQb-b|fK+SuOK+sGw3=Pc%Cmdf&Fo5cA-kPj%dTgavdZjyR+in$DzdxT#VnB3 zWOuUa>_!&L*>jx^TkbgL%yl`CoGz!$^*GR+C3l!R$Qg60EST$dz&TS+pR?u+IXLgg z9p%h9D0i4Q=b^mS;mF(aHitEjS;(ZcJ(+d{a&6aodhz!u&VLWN)@BVpcZ}(n6f?w?#^*LB<;nFs zyt%X7V&S^5YdlEmlG6-%+HXzFdM-IkM#S=6Ka;bOHjdUxSv!?8T1IZp{FYIuZuwU9Q za(HggG30PM+Whb&{vO3uV*+W^QtVz}^Ta~TJWHPyIUA7^6DRaxtWI-7%3(4Nd!m?U< zoxEONEmO-XhRD43K$bWiqd z5wa>-wcI23%FpFKIWE7DU&;wNDZi4xlKbV9JRpB956WqINd871mNW8*{H;7HXXP<@ zx!f+hmUD7m9+xNNf;=e~4UbbGoUa{UO zYms%y>SZM*fBB>3A07O!ElI`AbE)FvpD!)F`QQFJzx?n1>Yv8mF@NwsCKMk>|HPC1 ztCrub3hn-r@uzK9-`@S{53b42KL36DpJZ#;uih!qeCz-0nlEYo^YhQX_1UME<6r&N zk`l!?Klxh8=cS)mCVuc0%U3=r`M9Ly^W}d#^{wwO-u&Tj{D;VQe)k`ezkmK;KmNft z6@#d!*ZEstA3g#8#qTsV$p7qpMUkvAAef%k+W0w=GX66mYgY% zgbVr<{?nO1_-u2eqV&(d{71gOT>PixA3n(^W3HRuds_Vq@Bel4N5X$>xtjg=SLkLT&%}H{Z~GUZNcO;cvHVmj0Wn=*pOln*f!!znuI$U-X#V>BuYQ5t$Ci?klD{wcs02n=e(B$SAO61; zKl;(v7MaYazYzNEvXB0;@z17y_u$iS{L^oEzE?hM`S{Pi{6FKr^0hxu%D(ZHPd=Xd zwU55?vb3Sf+x2P3=l?^=ZHMMPVhMmwfhF$w1Wg|8|W1=jlJPbo}@~{?4aA ztom}vGvxfQZiqLZ#VSg^T>q7lFL~|1^4H4$^k(bhU;3yd@vD!1?=Qb&_?69Xe%$-B zA1-K*zV~+{-@X1(WabZ;zxnJRe(?vt_Ur!-T8)1Dx0_Yf#$Wp7j<03Rr=NZP%`ckc7k;UvwB)nTOW(79{d>*-5ci`~N8U{M&!|U$x4QetYT4Uh-D5eNuPv`*N9Vs^P2uaQl-+?7Z^tKec}LdD*As zUxfZa>Bs-g_t9_dD@u{y{(c>P-}}97wfCoAmCdTu*B^c7^Iw_&wV(d$pULRc&p!V0 zUrqn$@gJ7_)6aWLhCcuE61(g-OTP3!eg8{8SNgqQ`tHD&KL7e>UzdMU^0z(JRX_gb z<@bL0%b)+|PrveU^Vk3Uqt8D1S;;4#|MACv`rVGdE&1r@zgzP0H@;r_CnX5561v+4A2IN5A{4M_=AAeDj~T zbdCMpFaO26v0uFT;V%`de#_0T{MsfggYLpErI&{JTHoe8w-R(bEwJ@Y)DbI-jOXb6p14QYYU zKp(>^1PU#*rOX9NTW)|r+Q4f_X_y`*_VIW|c^+h0vgO&bEl;v#NtWbEp5^(>=e~0; z;jKd3wVr;<%U;XdOVa*6OW)r1{{J5w9vdFNIdYkrL&LL^Lw5}iJ#`UI^GBmEeES#g z8#?^wH!h3~J&A)C5gGr~#LZi89lGniLl5I`nf=YhZ!LcF%F@e*CvV;}B)K^>d~;~% zuA#O3_N{v#*z!1E>{;0vy)^_xM_%yJ$L}BM{`~pV50t<1k`F(P4&C+m&=p|l=2H*d z|HNqEd$VK8k^3cgj}CqE+gD~k{=Tm+4i7z>9lFbL^T`LVJbY{9GR;RH{!;|MwfOAx z5AOo^-}ypv@db~Lu72~uCkVwG({KKQ?WJFQ2R*m)^ea~-Pk!lDua8dM^YFuyPYiu3 z@s5Xv7jLfL`gk$*Pj?~fk39X#p>Mx!lUsXoXk_+pt_|hx{p|QH`TEOM7pc3k^LG#O z6Za8@@_pa`h~(koT|<9=Yw_0S|M_QIL%(tV-ShVj4=w#*^zNa_n~Ot>|Ld(IzEip6 z*4XcS>muSqlYjGvuXxF0>8&d-f9EHkdSGhk3G22OMbn_N6I`r7c zU6--gc-_#jqW$Uz=07ie4fphw$A+dZd*bmYo=c>gw|?<-^FJAtPk!UM7Z=T+xDAWK z?c4BeQM`S2`{W$Hef#TUuO8ApKK=*m_Yc2!vGDY*G2Pwoz2~t%yMJcmYp?6C6ep_c zKYG*Z*wFa!E%N$<&}*B|^gi-g@arEO zo1A_3>cx&;z487p-?NsUhfe7PA)W+%V)#M9H~aqaVN zS^n;-{~K57liRmm|LFa*AGor>zIO8m4{KgDKJlUt9uD8zx-xw4MgP5E`VD_M_N@=y zS{}Z&s$71_A3po3#Vbz@E&lVjhBt1#d2-_B{fk#t-v!(n+E{cyHaz@|moHu<%@cPG z-9L2eB2q)#$^)bCA3mRW)64Gp(GR~c8V#=m7eDqJZ~fj&ci#7^`=9pRdgO`W*WY~4 zSAOf|Puv=^yzIqqnVp|};R8eWPmX?aa(rlX`QCe;8vgwcJl%v(QZe9Ie8zWAAcdePVSQuROJ9{s>G&;HBQZ+-NhXXmE2Uh%HP*WRsl zJ^4D1dhw$dVZML%*6{Fy=Z}B<_lbl2vu~J~oh{rmdTTJe_@DQ%_=}UheEjNJ-27#7 z4&C}?qTc+^?_YEscN@BMx?lggaB|Tx2Ny5Jf4qIb9t_xvOn&#R|M;OH@uJ!71LODu z|7h;N@!lt%x!N0gRQ>&xTSM>oe7E!Bp)23<-5<|C`0&t+$KEnq$;T#dP2L*aux~E^Hy`?y?WxkIUQ6HptJ}YKczV>m?5+og z9~pjh_*27Q9RA1Qe;WSY@b`y*JS-jFA2tj-heN~3;p}i>xG_8!J|7+#Ssd9IxohO5 zBfmBB+L70fym{ook@t-J$;byr9v%7A$X}0qe&kCdUmp4Uky{shd*q)+{(0oPBi|qS z!N?Csel+sqk)MpnMz%&YBhZL@1RIHuFeAm0@Jy`%3N{ov@Mqkl2_>Cw-Rer@#e(Wgd#JSrJgjP8yCqvla$G&D+% z7DmO9Mu3Yhy1Od*#@x$KE{l_OU-6`^eZ|jD33S^J9NI_VuxEk9~LSsj(lA zJv+8NrXACbfn&&+Z!9*J9%IMyW92b%tTT2nc7Ac&8Xun+pBkSTpB-NrzczmN_{+v$ zF@E3p>&9=6zhnH7@%N8^Wc*{}pC140_?O22Vf?Z2Z;yX>{HgIDj6XB}x5&%I}w>kOk^j@6OD=9#Nou*#Kh#>J0>5Q{NUurCOSG?Nql9y-sZUOQdg_Z)e?Rq&sc%nxZ|a9rKbw+IsiyX(_NR1H#wp8`ZOT4{Ou42!Q-P__ zRCo%XN>AmcDpTUr(bVm!sp-Y()#(>ZzhwG(Ib>{0c-bD+3Y7~|7!McX1_4|<=MZRy*2yz>=Uz3&OSB!{n=+`e>(f@ ztZH_5Rx@juwah}Z@T_wdo%PHHX2Y}iY+{y}C1>ebb~gWORA!sA-Pzvk$?W;;T~(YvHN>;pPk>FSIlqSVRwFS z9+=n8>*h`K_IY$ZFdvyu%`@}-e09DtFU}9<59d$jZ_i&@7+;uKm|s|4SXZ47v8Y&mW4l9c-O+i3y&;(VByh)Pb_?T;mZqOTlnU}lM7ET z{OiIq3qM)->B6%MvIX_R{(^DAx&SXA3!Vl40=_^jq!!o(ZlSPHU1%({7TOo|7ETs! zFAOhEEiNuzUwq->OBP?g_^QSG7T>t|z~Z|X-@o{g#lKkm^x_v5|8DV{i%%~8%i_N- zKC}3fMaiOkQMtIis9Urx!i%28&?3H=T4Waa#o}UPvAuY(c)WOJX<}(|X=Z74>H0ED)qy7YgRezvr^w6(OqWLg53oJ*c1-%?}=Uy5CjS>l(fORc5OQg`WK>1^rB^5pXT z^3~<%FTZH{x0Zi<`FEFJzx?LqcP#(W^25vTTmInkhnGLL{PE?#SpL-VU*F+#%b#EV z+VZXC?=1iC(T>0$Emsb99^mtNqo{wW+nawUxDv zwHL3wa_!!=-(7pt+S}INvG%UDKVExe?E`BcS^LD=U$1>;?Q?5iS^I~zZ>~MD_T<`k z*S^2@gSDTo{d`TjCR^KD+gk(H3~S~!a1CB_tf6bJHP4!FEwC0^i>(oB*|p+YWv#t- zur{wmodC+i*{sQx^CUF4y_~W&UM$id)>Ppc#hP1 zc0IpdSTEf{STEn9vR+%S{|b!@n$OXCj@#>VSD$zFl~>f=|x zbM?uq|8n&QSD*PcetcDR)qK@=HF1@>n!j4UTDw}m+P`{wb#!B9V`XDw<9Qn|+IZQ< zy&JFIc>TtkH{QDO2OAG<{L#ic{|o+PKX(d}Hd`;0 z4qbO$k6e#lC$3Z1i`OgHYu8&B46dJCzkPl9#`uky8*_J9y>a!|xckOEH-7KN0~fsQ z#=CAje8GEe{PB%P?(n`FkKXvJ8-IJ_){SrcH+=iXzul1B*t=o80o_1vcyC}g!Z)Hf z;y02v(l>H93jZIJZiqLIZj3)~<#{i7-YcFrG*p()T&*TMzyKhR_&{FDuc?XGO5feg+wlqD5Q!_g-jt=T>4I^ z6l%qmVq3AJ*j4P^(NMos{njb;3WLI^Fe%Imhr+2q6)wf45~^3>Q}`7DMNok$LW-~= zqQDiG>v-degd(XR6e&eop;StgQst&nrj#ocN|jQr+){2Uca*!zJ>|XJ~rB10= z8k9!mr5l$;X;p$s+a32QScxbd%BV7?j4Kn$q>@mkm83GGq?ELhQD&8_l2h`^oHDO0 zD2vLHQc#wa6=hXfQ`VIYW%G{KeOsAQZb^0|dy;(#Akj#6CAB+l_D7PEq#^n5W=u~c zr;=mInIs_bN<0!wl9ps85lL9$lDH*)iBA%f#3h`Bm7tQ~omt$Zgp?#Cw1kmlB$Om7 z2}y9te>H>qtN!wsG$alGm$SdWK3{B+TBV@WCWWMSDJ(^#4yjXm=~pjP%hd|?r6-nJ zy`|n(@2Gdxd+L2Ppw_6h>Pv?sgZk1T$b84W2UOeCklL<>)ri`mcB)aeOYK&B)Lyku z?NP$$)d`qH0+RAVmqcE~y1| zSzS?A)wMe=9Zj{U9!UC{KWC3=ZLVw9L9W{E{& zm4Fhv1eTmjv=W`fCV?apnN+qZlgZ>Vg-j_^$<(qf*|uy)wkz9{?aKg}=8jjoUUun} zZjza07Mb;qTQelH%i!lclTq2FN3cicmHA|TSwI$)VX}}cEQ`o+SyUF2#bpUuQbx#9 zvb2npWn`3$zT>jQ$~YM>%gOSxf~+Vj$pl$hR*_X@HCbKOkTqqZtR-v9IbMQ)XY@=Nu5yBwAya);b0 zN98WLTkes2uE)E)b!)>uNGloRrlJS`{X8961V<%~QlXXTgv^_Q;n z1$j|kk_+;(ydtm4Yx26hA#dJsdvD7-@~*rm@5={rnZ&K~s4m^;Fa76(Dohnpg;fz1 zu8OK+s<JG_yDIICs&ObS zl7o;H1|hkXiydn@tI^ensO_?l!;$i~$sQT>RnnQ5HW2V#`nq}211RKa>1ydVRDlw{ zxIXP9w0J5QI0<4&quC$HkgfEVx0ycHHyw#sBkhfeX)ij6l0+xnPoLW;TiC~#oTjel zFztv{!m#bsBsVd5hH%G4kO$joi601W>$~Zoe~?Z&57L_OQF>2{ST@NDP;%7$?w}_q zBg=+u0~ih&B&l919xq~HtCXZkE`FA7nGRCt>EpD7%)5;7UYa3K(nU9pw4xLcwJAtB zxkK)e+oUC+CXGZ^ry|b~EU-l?Nr2QC)fOa%M4V0+R8MkRpSK3@lKW&e+_VzjyoS^_ zjCxW6XUVYl95Iur;F-xln#fHJAGeTh+(>FjEva_s$gVAmtIc^kL|RE5;3Qq7Q3I0} zL)Y%ZI#xV}l4WRH=O$}*4+)a2%T78-gxpISh`iTFlC}V8BZK6&$4?GYn;{8=kv6v> z6e7K(Tz3u|%%{j9Fwlp|2r1R$B&&A-QPSXVM`EPgL6Cd?B$*&nBo$OcN>7|jlO$R6 z(a4c;GaX25B?!ErOC^1goFxe7(3lYoHOUsKGZ>6Ad(F4)3j*7gBS((3AzDk?62uQR z1rKX47<{S2XvvlhK(-Poq%*FB8S?R3G6A4GD$TGyJ$`CeyEB%}j5;C9oRB-ln(0V$ zN)~kk`vBI&3W&~lPM(pVo%7gIl8nXCF_ayBTT;(B&Rk}j)+Czhq}{U%2f={BPo;26 z$Oa5_2Vj8Ow-ypcL)Sp0aUWsAy{Ys*3{vSdhVOy)8t&*jI*FuNukY*Ze%wvEPmq>| zOouIyA(QqH(%`;N>&NsM+_O+=IxQF-wkmcIVbXvXw`@XLEa2hN*)*Fz2L$_Opl8#V zo90|P2o=)#^r^d;E~OO?z*h$pmbk4QhN!q>-*`;yxJk%P!PFrlwTBRd@&vZD&UhZ( zjykC%f>IjT=E`_oRGCcq^_EuHmpBDtdJ?aY!EB8L()*xBlhTFL(S!~XVK9QHgQjR& z?m6^Au~@p`+6ySXiu9RZ2-9E<*aCNPKvPJ;DY>ENjJVWVleVbQvbLlO-f#vx zBZoS#J($KQxi1|#_l31Yyrcze(fD>yZ<3md0f~ULA-h79bs6zmO$ku|U& zfkR}{2W%SsunN<$ra&wnNgHEst37DPOl-@bXZ!Jl?!atiBi1cj->?%pLA%x-;tX{n zX4V}+oEEm_JhQV7EOZbHnH;Q(ZJYc6VN95ItY=sP)&zSV72#%cVV&+YY|wkdEh`^} zq6pj52XrMf$huuN7Gmuz=!RJ*t4W}&)te5dT>&=2(uAMQu*R_08fJZL)?M@Wtfy$o z?qNf0+O0^2te72&Bjy6(W$hLveCi0Y7>lzMTVzu#!TM8qw!qek%2IJg%2ZivdNmYthLTMs6w@&e)TSa!bYeOcIaLuSNQ--R(*ru>;p8C*v+|nz)`TU>Z&V z?{k*$Hs|s0A$wfI3RuPVHe@ z*Z>};;&27#Z8|JV#erHlZL*}C9vNc61d6Auo;+2i4%}6$LLqR8DpD5x4kC5tr~(zj z8x(_dsGPA&#VE67J5i^iaEppkB}0uW!cD489Z?d?CeU_zbs}|0#XLQ#PaRML>YVDt zt=hATQtgB~rcNo&eFSdOavH^=Mud^jQrh6H7?rewR?{*KW&mI|>NKk8<6yT&2hW2J z?8LW4H?eIxPVLh>w9bAK*rfx0iwpAZ(St+@-*zR92`ZyWQUqnUB0f0a_vuncelL7z zwZ*JCh2E^un_UrsR77&22FV#MG08yC+upPXwxaorTQM5#+TJVDJ+onXX^&W?*#VT+iuKNv7H4`>rqkg9T(3+nv~sK zc1C?C#egwrJBvz6O;lDozbN@qA&2R-cvjpjF)md}SwhjQXRBm#NlUgs9zrdOQWD<} zt4lyh5!-@xOZ%lLg2j|JO=%Ddrp`mzkixlL+9|=%-W|nOW64|+T|QUUF8WQSma%|V z;`)-IB=tchOUdnNhrp7pQ~~WJYsrMj;&7?wM@z2K0EbW)>MS`*-jXKLs|8B#Qr4#S z8*K5&rB-FQgqNbFU@2+1)MKPvo>Hh3DZkq)K`*m?@#@ za7v0tP_|^ZIW1I48I@Wi&h2z9&@9rWzNzHvyR^k~*J)5!+(|a$>3|jp2dkl~)lk$I zGbz$iB2>m!C>T>{41uz)>kq*vcr0KnIzc+JV@V|xRv~&CiP=m=#@B!cnrzT*bJ>n# zG)lvu*@^C1rHQO}-&NN}Env~-*iYJu_M)Iq7@fh3Qp!LEa;AcR8?uL6DG5&cPmJkO z&Sux)ezGLJ_)_@dp{z6(&W7xf>`ve`#_Fm?-X*bX!yOl4S9$WGtKAX$giPMk*Me{L2$dz;6 zcs*B#8@XZ*(KTJo9EuODm0Z@O19z}su#hX|s<~S3AlJ+F6W!dd`#9-zBU&-n$?ZFA zL5blgr?Itj3EYzC=LpLn7x%VubV|}V%*h+)xoE)Pz(E4eTO~n7W3wS`5TLTr!)1-E z^}r~}YmIG8X^}=G-Wc3A9_LQ(XtpL@(!3+C%)7j@h&pews`43dGcU_;`G-?`yPqr)?(>8!BEPzhxIgU9gPp=QVj{WG}A{Ghoe7w8yj|okg>q zk7{=etfg$MMND}N^!ft0Ip5T&8^`fNe5+x}=VFF@HjbNhd1Ib6>GM*=nl~qWdNA(> z5Bxe}`wK*aI^dCC|v8Z9uDRGL%nSeAs7PQ)t%Rhp{%#I3}X8p5D*#k1CE zMb|+OOg&=L(9-H%71u73$d&<@n{y;Bv^^aP1CbDs#qIV~GK(h}Am`e2wrqyD1(CQF z+IB*Ppn+8OGznOCed-{YgSv8YYgzBMEn9gcK3Is|~Sr4GM#{FGa*>2QgNGsU3fuPCIzqr}t zvy7e1n)D3BWg9L}gfnD~nLy7_%zBc1HiVsf+-`lWV~;x_%&dz;Y1k9WL~Hs&mdOg) z^D5xXWf4MY=xNK@PPUP4XE`6A60>EL$BJ5eteLH4lVPQP8@AhHP$j#U$Y!fq7^!Ew z+KO4}bU^*=VRl<9(^vHh{~#L;=FOLSzeiao#sf{gwYZPhw31pcoA-CK89)*{(ClNa ztSq>dJjpV8h3hPPj!Ik-7IU0tMZ4DH)(w2t;CWWYYEvd1>Iiz}+KPTVRd-3*O;*n8 ztO_=Zpng(Y@+w){t74_KcyNnVvkq;*z0J}Ihi`{%*>_nbVX7Ky$-rsaWtLmqrt@UL zqQpZz+|WaPc2f-kFi7hlG%8)pEcK_2=9=Dq7C^lx$*##2-cEKkx;hIO>Lz2vaopG7 z#bi7zLOI>G*_ebH5u+vO#37Tht_~#Jmb$r)I!*O6+*$|g#gwfM)$5*Y*k?=n)CRxZ zMhJ<$Xd5?IEmdpPVATZr@No#KS^;}ij)2uO?EzW_Q?XtcuG*?-)r;%mR=or5q@qc0 zz+H7!y;V#o{O* zgaYM3xRE3ruCft5u_y>%nG2K>NZDQXmMz}A$zNvRSUFWrmT6nWMwPSWO!-)o26QgW zob%Xyyf0Etl!5Qdtr1z-FLbZk3T#p{x#V z`pqB(Rm-_@zFaID3_{uMt(6;P0NP6(miuKOG$>mg<+2HF0lo4;dD9Z}hfywELb^c) zGvIunTdv!V%2~ANIk5DAbZW;Yx?Px}vg?;tatI2ams6ILvaAx0ZC12|A*HOCpwseM zSyCZFJ27oVUD?uu`t6DemsiepJC!`LRk2jemA%SwSykDAH5F?`?%c1q&ASyke(8#8 zs(33N#9aX@V5JS%D)OZLj>llzVe~;2X9cb3D#nTiL@Eb1SH)8aRKgWX>#OXUf)%Xd zNN&2)72fKvY{nCnR0Xd@Dh_X~LRNgbc%>ByRStASB~#I3yOCstu27Y1C0b!BY$br2 zykWf&MDWeJ$$b)+)#Y`{(}WduWqk`bd&_23-9nrtgL+6?2n!xQ)Yl_H)^_Q%SB(}c zNBTlV7tIB5ZMjma2$hT7RH~I)rC!;uTijt|H`X+epr)RW<-=wdjM|Nsv`-V!L^bDb zT(ga8lCqk^YQ^L=WldF6)YP@@+E#6+76};AZB&jPdUk68*IsSE2Go+KW0R(q^Xh6c zYd@+1v^9Ng%iwdCy@uLekj*1;ij;(QiLRF;^Ptt5%Wp@xyw`8B>Z5$V>f5*MLTMcp zMzC_elJA9fF`ccNujMoNi6>)Xy@41JF68U^j^CjZ@_A1&uWOX@I)BM+uz+Yj54mLy z(wWjW@(OU*)yik=C;5IpZpBTZc$kPM9Tt5u9kt*C@0L%m&7jTvX$5FU>;h)B%(f+VqNJ%?-sICSJXiIn_sJQ~pz%mquKf`zdJ zVkiI(V*w94z__97Q$pV4j$djH6tIFTYK`q$6IQg)FoaS_;Vc3dXh#4O0x4Gl^A)@W zN5NItHINsVq@!3~uLqlcyS@&Fq7BT@D0)?iU?G`u7UYn-P>TBthkkpZ7(e&=ji(8n zHdSzjLj_xyG82UW9FEsfpF3S}g(8JSAz3)LmLhO6R=_XUCWf{Gn}`C67kp-+pifW* z(NQS~vCB1JTM;IhDMSm!0`1Ee1Xr%W7fyBM0&l=lrNRyvOGU85TA?togb1=g7ml6T zLeS|#aA#km#li-$uY2M|Iz6)Tg3) zoz@3l6aqm4t~=`PddUuB8Mn>ush?|HbyYHNaMq0%B_Ucj2T|l4i#dGtk~NNW2rO!c zb*7}>ThDPRJ2b5~6MsM+eT2kHnIyeQVK8q#EeuzD#Ps)N30JzfuiMO&n9 z@>|h-J(n>ZxB0vdy3R$Zx|m?< z9x7Yk(Xe%{u1FznTWt^M+0Ewt7y%p^bm^TSli%@_0FT>Jv(~^G65Vuns#=%96-K&M zhf55%s=ex|f76@B&cl~FwmT64IjF+M!|GA>xO!4;SND81-fXJ#4PKvW@*cKQ`VQa%bK#GtS#%xm#df~ zmYvpa3-D9{*vE!KVNF$Ag*|?~t|}yt7=6`HHC6*=fbIKR*s=e_kH!^2g?X>#)R>DR z=8varRli`sT@}M#a~I4C`%Se@+tf4#2xywZ5qHlKrgY6No^X~uJ0^XzraJ)*O{?s7CA11HiB?LO<0ku#ui83@VgzNav_PXir0_@_wN|s$Xti4PmK_vZ2CUOc>Jpl6 ztJfN|czwHd&^m4%w)(BJ)=}%Ub>5P+9bs8}vpsN}w3t9!-_~d_MO)e4X@`^Qw$3MQ zZ?{!#d3&p^26o%~ZA}|!@3o~-ZClqCEnY(kAc020Wk^5)P}$0wRV@{uZZ$(&t?ib~ zQzQ!QV!Ml84EKq$dp}-^Ihr6~NQR@F4YwY`UbDl1xut<1>TJR;sj=RWp&EjVh>ftD zHcA4GMyt_o*ib#(Z1ftPMk&dn-NvA?ncR%_8#sE<=)raSVdDU!qVCindD=K>95s#` zXAOmYKPYKJMrjjqoj026ysc2Tvj!xN>H7yZU^T4TYZZ)^FI*-TOC;SF& zaJ#u3+GzsG-KJ~cr8f0DXsK-u#A91TM@-vMs4HT|ZD2oOY?vBH;eAsVHaBWcz;9^` zJUZCcICSGtb;{ZR8@VXlKpL)wuib-+>IkdqAy$U zMzj%VL>h--V-RbE8vRJD(Q#YL`{`bqt?-iYs|YY5}Tta%&?T0>FusYdh?ga@vneKC#ZQU_vOm-aTIKH^Xu zh-aWjmxMeq4wOVr#S`&J#B>Qi5Ih&NfS))M8@lAc7nZgpExRFOI|Ykor)#rS3o2Tw zC~R+fjcrpq39I$yHW*5HcaxU3wH@)wVi5vtM+iOM!L9-iXnfsBH)=?AW8FkI z-aRqFns7JJ%^2`*w43aPx_R4~gXm_uSl4H0LUdPVYQg7(Hq_Fy+H{xfn*B^S+vU4# zH{Z>5scsb(bzE2N?bvEv*iz{3YR|!Zu+gn_X-o6Yn%`=dchr(nb_i_cO6{#R?_euA-o7SIqkGiMbv#zAK+4FEJ=I2Srj8(#Na4TCd*AA81UCUMji^w?%xS>s7Y{W+B&mf5vUZBvCa_6c6$qHqunwE-b0U_wX; z3lRYql<|J>Qspx)Bm}=dDG)+RNDHLkhcbe_n=)&Ba2M%1x>?I%sGQbVvrbcU(`0Fy zn@Ip{5=k+I5>T@hur=*XqzN}WQJWV*d9&c~^jO^6qas$HujlUtdcoc?d2VhOTXC+3 zg?XL6-Hh?QawONoya_`=f2nA7scj|nv{o1>_jXN<*mf*}*x=0?oAgz??L*^cFys>2 z<@SlW(so(hi5O6A*V^@Vqup$a?RLA>PP#koV`Hzq7xQKVL0Ts@NG&~AzddNX948Kd zIA|ZXw;W~7bPy3jX8qpWX{JT5}TtY!*hh!m^LG|uoiwoHwxafzxY z;bLn_cc0W~3pF>CtL1Au=4hx`<7+yH)YHXEH2^WkgxbD&*IcfF-fpN`tJNAcAKs}o zYwg;R?=)JkRcaP987aENTC0}9e4!37sP$`FEoVNc)irKR=8{K_YSMbgeOz<<+1z<8 z9Fo*2jVVsY&T1z$n+~WZQro?r&OtbdN-|(q_Ea%dPu<(<1?_B{^YeaZw-o4L#+*J_ z4pYQ#Z?CuC)Ak^%u9pXZo~9>-lG;m$gOU-@Mk4y&j^5BS_bfeS$l3#Ywq7g<_3S;k zm+jdcW!ttt8|@c$bTX!=cK`#Ofh@E$ZKAtz!f&OGG)PNP8||hcnhnc|2pythbP3va zn&}`NrtNgh=*AyY#?Trd?nc zN!TiMm2T4m`iRyWNS^?n(`WQ{T*7QJa;6=VF$zY>sAGPsnlUmOM$Z6D*waDw{dIbe zF)=NApD{B#jD^uLyUdnL#psZ2#}>2AXqh%>U{rA{12Rf9#@LuB16h*xV|&0FVB8GM z1hps=WMtYTV`p#=&Lo(e!($3F5yr(pOo)kMe#Xi8P@88jj4?jO!FU+jsg7^O<4iAd z=qoW^hGPm&o=Gu>1j(eCmYHB8o?T;(IkNHzfx-0|<^;|&EWdsz~ZXpC7+ zR-08s4cYxHkcDu4wh^#o%~?|x%od@vwu)M_R<|unVq2zS0?NW!smqbIXOXNm>CEnR zq@dObxV_H(P6cP;P_3i`I-1UIK(OjMhK{~d2wEfBjy@2H)tzYA+!+9-j=dAnI-=H& zty4*W9b*UXKpjiR+i`U~oj}LgK|1cv3E}7*d0D@&XLVLOFRwX+j1cKX^*XB+Q#eBe>1)fsdSJI9@Fr`Ylr|{)9qM0(@wSPzCeg+ zLq)osZL{s-#Q>@caTmkdgYZ`Dq^&id*eFZMLZb0Nxr!itd!<^f)~fYtquQ*BRWEeX zKkJ|OPy0DYGLY+~gUx}qo`_aS*}&S=CePzE<_aK1`C#9!_Q*3Ho7!kIM|^UdBBRU@ z80u4H&OC-_C1#9zZCa=Rsxw;|UvxXuz(phFj9ir9Wj|^k}&(va?j5edoBm(-(cEFG^W+FO~Fl7L6&tT42GEK{Vv>dZ$P;)gRYQfCK z?FXX6VjRkJ!}d%vUIgKcA&3W&OwHRQ92tY#VRU9NQy4}wuFP)GojEXgGEN7W@@C9f zHKuX-GG~S;=+EfzKqi=h!dNDh31<$p(M%*$j^ddl8q35piA*vB;_}!LKxDSPsmuW+ zLDLyBbB3C*OoqzXk(7tdFd4l!n_)9PO9QkR4|_+we8|%0YTXD7B35~VB(HqU8))o zb4sZ0q)vVDbSRxo93@=-y?ETr>2m%-FKIpKv8i?sFej1`bIXJ3I=vkolsNGAdfi?S zlll6+dhtz=pT(%YPfMoAex^_NbA7JQ_L)B4 z&-M%bVn2_;MYAE_FZVaGQeWuX`z{UKcN+@YdcV?785{j-zt+#_tBGd+NGJB&{Z_xz z@Ae!1Ez5~FZ}ek|szG<2s9TQkMrAA2tPswIuV57`txCJX6P-%8(y#O?gUUgL_a0W# ziK9vwI|4$&lB%@2Sv@rE+6sX%x1WF_rlhQzP0Fhd+-C9W zNsR_6X#`DK1L?{>N0HU|9im?4O~;Qs=IEKF@7#?C6Q_`~XfUBg3z$nDLdJwVyyGBs z1I$x&6@A4((OZlde4bN-yEq8=iwV@N*|eEb!B8j|2*fe07%Y}tlBnsT(87!6P>YBZ z8G}6(En1DLP{HC3M0I4ao`@Bzrf@M?Jl7M&crj6AVyx4n$;BCCJ(wyE;uTHLm?@@< zWDzmAV{|cFWQtnvW{fK4E)t(Bwo`nO)|^Ch#X>P(EEdzTpn(|#}CJ^7UN~yN@G;@-n{AU>nl}(*zG7dS_iz_J^rJ`(*AK9c7)Fz>(cBwsThjLnF z@HVwYc@wa9pE|Mt6sS`eY(@>$H)<)rMo;M|17)I&l$o+nRw@!d?)ANBs^C8Do%K$7 z9=nLgO`*gVChbf5D(iWV2+H~~q^jEr$@`mqJ*4WZ`yt4T@-QFV>MQ!ne#^Dfx9JI<2fl%S z5Ex*C;6<{82H}CL>27+OwWQ4GYkHghW}qqe2AfzDFbxc$W~3Qzddzq;+Ke?Rl)K4hrn%>(n)?9XKp?+fx~bGM&1{oxa!tNj zP311tQEi20Fn)~fXp4?wQ{ml3OHH9!ZdRJrX02IoHk!?**lgWV(d;$5&3rDETFjBbiD(Fwt^BduIih)VG=rV`HxwTOAP#BEXU z*b$E-hrvB@S3K4oTJ}Xi%;6eQE0#UcKH-d@|(J8t_rSa63vty`R^oU+DBKkzX7!aM_wiq%7MNABd9-|!% zi?|pSV`3*67Y|$s@z9kN39(>408(OFB*lzKiL}UwStk^l{+!$FZ3s4bJau6gah=`&H2q-8)a&RpHVLkUb=l?f-SdyG(Q&VQA6Gn&&3T>pJm*Wui=4Qem}hS9-bWP%M!$;Bu|0d4@FplB z=T%Nra$?TwocN@qoHsddbCPr3C%wz5eH8rkeU8`Dl$;MasX1vm={f!Z*=cY6zNF=( z<))Q9|C;vHzbfhbo62`7q4_EKX?baG58oyfq=g2=g@i`lj*RsEmgX7xFzQKYW@2Gl z+P+C26H;Woh3c-o4C7%zJw)vOMi;d_`JreEplMfXX!Q zhkl8P?~3EA(xP(iMEvPO7GX<@3V5EAl>Rz8$vY$=u&OlXanRdHTdnUiTlte{+qbvw zHjg9^o7I*Rk@D2dr}$O=YcHEE(bHD;?w&31Syk`@+kIPR)Q1Sq_>e?z+e4d=%`Yd-q$uOB|N``GXO8Xnnw?T_tov58^+ zc0YTj&l7uqJ5f$STcB|rDh+BoF;&#QeH&0?>6Yf;_yp0aee);FWh2JHW z_*pAFDzct?Ik`Q|MD%o zXTNVxk9c7BwwFhFm)#9{SXTWkAUZh7r|eOgZ`tFrCuJc?MM?f;er4{-ZxRB^g31ER zg3Ch63IjsR@`J+4!pjQ6o|YB)MU+LBJu8bUd-6QGET$~BtSb6>*^9EcvQT?`*~_wo zvR7q^Whp_g%aY38l)WuWE_+w@zU)KUowq4vsbxXm10%oKzuI%{dG>t!ZEg6(;Lg)UA5U zZOgw6LQY!@=$T;OD;Fdc0Y_2Ff&uW@@j#E#uvmr}G&oj%_D%j*((>hiMqd)`~l)o!h> zw{nG??IPK9rb^??$kH+aj;{;n%_H)>OW=nc`7<MK<5H1*$EcIz?o?kdZASR9s>=Lk=(tNe@Or=v8( zk>U9DlC3+qmD|6p+q!*S42@0dLuDo9SBy=&c-E0t*P)}LP$K>rdgd_CRKWZVjH!RU zbY1w&g!D(FACLREe&x8s-<`-+E4$Wzb@_y+9saV_ssEI$y6O}36>F5ELDe{i;&R6E zH>dZGns@0nw(ySy8Cs3YuPx-3T8Fc%pZ}$zkslLtUEUwI*Ljtksn4q2>Zo zhO?u-D#KA%ToQiKQUBHPKVqBXLg2JwM_uDu$JYT#zLj;g^ITHbHU3_AA|<2WR`}?M z-{UfWuU^``=!fSu;ZW|Wrb2m>#DZr2uP1f}54&IR+3K5wbouSGhu>eEOVaN}cN;uD z{c^j!(5v^{PkqgzMyE%uDXhu6;eOiT@AyNzRDWdCg%NMhotg7D{AX!h@TFKs$%-f8 zf224b1m7t9R!TccFFE|{YU--PfBq>=M_r}ekyYn#{zd1!>tizmsRT#!Q&D9#j+5hN z|6-(C{SodG;>hxHc#jLKOe?)mda=GFrOqCn=x_>m)P*}7S&nbLSG&9lUh|iKr2nO8 zRjrF-+1Q_-ub&V7*=Aez_uy_=>aD|ncVwM+)Gu>{J3k3MQ#<8&WzG7Zp-vgK4((ih zrK=}eTIbk0z>)RO@RR=a-Y!4?lzFYU+aKdxO1FOBmoNstbH8sV z9X~6DTt}Z*T(!%_g7@owNjGm5;@|g2!ia_45{}$-^6r`CfLuylm{U8}I6j4Dl)tKT zlpL(haM;639i^6#pVJy|baJVAdEpo14!iL9UFzjwYwF)W(wu{Bjx%$@9hZJ#Iri-B z%pOaxzq0=-3IB(>)v>|-c)}Cb?YrmJ-PSiX;TxW0W-U$p%hTfef0TYty%v1Q+huRK z%OACl%RjUJ;vI|6R|gb?)_IloI_y%jIKwOP^3~9)JUF&hQO_S; z*7`mxtZ`J=xeWd4Kl6FV{jG0ue*jCS-;4|HaWlx_92gd!9aZggt*Bwm`MMbY8hho# z!e5Qz^W5>oZ^O!GHhJuP%5mQD&+HzBznxpmG}v|ZaHAPR;WLN7Hyphr$7L>(KlM`n zpe7AmVs3<1-mNd||9ta7@1-BBe|)^tyzW)|`np%Y@caBdTB*5k+oib9rEq<5>f^0y zX}z%S6N)C6Ui%bs{KG2Gq|n3DT<)EziwS>#y?uYi#W6P}J*=|ao*C>2wbyF) zYDaDHt3>Czh}-A;y?7s7T9|UF)Ci5g_^kfRz1rG}@{Gy{j$()7k6*B#dGa5eTlNp| z($3f&m;b|zG0BYLF!uzz&Q+p+F-*8b9dl_N4z_mx%Gg)i}R z-r`c??buse`+u7LxA@~%zlI%}^tV@qTw4$p)r8_tf4s6{c&C7VH}3A=dervzdg9g! z+nRSiqu_^2{&q6zu|e;h!HJ%Z;k#@S=lSG)}>`#>HpoK^xup9 z^Z1{u2>&l}I$ZussNw(j^1*#V|=6O^?(2NA@BR;&~=XQ_RDq`yGw1}jLpUWACvw^IQLMW4{9Jch-}`tS)1Db zKQn$I%1`guyqqU88H#*tqd?Wb(_|H_C2Q*r>cwO`|4_+coah zczEN9jel=EtMRhN8yoL$e6sQ7##m#a@zutb#vY9yG=9=JwDI%CNsTib=Qb{BT;BM5 zH4O7o1SV4H5Hq>HoepI zUQ@rOQB7Yqecv>*>DQ)(O>IpbP1`i<*sO1}5zQtv`$x0+%~m#ZYIdO6*=9&Hq1p9j z_nQSYi){9~Syr>+X0^?lH}BNEZ}VZz$2OnV{GZL|HDBG_srl~a&dpCZ2b*)vmFBma zKWP4>`P1evo2N9-XtHswAr7fykG-=tkWw)07T8?ZvzU9=GGg>Zexwhr@mU~+sYk9sU)>3JC zz2&`@{w<%iOlX+&0VGY;AL}&Dl0c z8?nvJHV@hawRzqqxy|P`Wo_!)G;7^Xpgk#+MDg&+TUyM*FLiStM=*b^V|Pu-=xED9eQ*a)M0doDIKPFnAc%> zhxHw{bvXDh2Qs(=-$Chcqk~5W?+!s7Vml;uc-tYfLr#aX4z(RxcI?!#f5%ZBr*)j& zacRd59rtuR(eZLerla1`((yq@zm5?d6FR1L{Mxa+qoZS+-+KKv{I|)!P5*7_ZySHx z_uHx8z~8vvTz~WWE%>+i-_n08{H^x4)}6X^>fdQpr@wZZ)oFRBwVk$ia_)4#6WodJ zq<6aA$-C3zPC=cbIwf>U?v&XnuTx20I2owsXraoxAk!GOEj@F8}N@ zx69Hlo4f4ma;nSaE@T(Ai=~Tqm!K|DUEX&2+@+*TZI>2ZJ9i!2b#m7kU6*%t>UyZ_ zg|1jvzN>53+g-i8`ge`!n$R`5>*ubeU4L|K)~!>we%;1&`@P$&Zp*uE?zX?%>27d0 zvD@`-_q&C3i|_WKTV}U!-Kx6Pb!*z>s;zk5}8NB4F;y7w5;V{DJ#d(7*xtcO#N13gam0D7=JuJyRz zBdEu-9*I3Z_W0VPq(@bcx*jchcInx_=jfi3dj7TNA3f*xT-I}4&+R=A_B`41LQkkC z*VDD9wddoWp*>&pOzD}^v#@7HPe;!dy*l>l+iO^_3BCT&YeBCSy|(n)+v{YnOTDmO zYA;JK&t8vvh4qT+mC!4>S5~irURAvs^={j{OYZ@_NA{l5dq(eNy*KsV-}_8&w71av zYHv&Ld%XjDKkNOb_vhY)y{mdVdN=RWsZZ}d!~0C_^GBbheKz+w)aPWMOMT!zY@e%r zy!!a}iRhEi=Y5|~ee(L0_o?mEsBine-TMydJFf4PzBBtS?Yp7xuD(b6UhGTu)%xD) z`>1bZ-#2|T`WE%A>|5WrdA|<*diEREZ%V(}{Z{we+RwS4OFypP^?vvJh4xG6m)@_i zUv0nE{d@Hv)&Fn(7xmxN|3Lq<{qg>Ke`|mL{?Ywo`oH*UIK4A8Mr2{q$*fwDA0OtXx2e=HN1{ed}2Y3(g9}qI&*?_nK zZw90c$QY12pm;#-fMx?b4(vH_@W7D+rwp7vaPhzm19uEOKJd~&d>}v27N~@q^wC${JKKsA7;~P}{*>2M-uLYVg#-vj(pmyk+o#!6yd0 z45kO0gKrOhI5=$Zi^1;)=L{|#TsOG&ke)+^51Be-_K+1rHV@f15=hK?Tk`_S1#mk-@M^uW-wL*b#~&>KTN zhx!i<9U43I_0Wu=1w(B^8w_hTtjn-I!$u98GHk}MrNcH4J233rFl?AS?Dnw7!=i@8 z4tqT;eb~2QKZdm!-hKG6;Zujt9lmz>?%^kfgTuw)Zo?l9j~xDHc=qt};SEQ07}0;k z_z}}bEFIxA;>d{0BV0xRBft^R2>4ebBhX*Tjxa{JkMJE4J|b?!hY|TBevD`_vir#4 zBYz(`f8?5xYyXuSM{XZ^aOCNc&`5ElG16_M=Sbg?!6Rcvz8RT5GIwP0$eNK&N3|c- zYt*n&lSj=QwPMuvQO8CBql8hnM){127?m{Y^Qin^`E69us2`&mjcz@<{pc>E`;8tn zdidxGqyIK~&giA1*N@&c+IjTZ(N{(zquJ5g=o_QGMhA|L9i2Qndvw9*%F)%M>qa*p z^V^u-V@8abIA;2od&{Y2)UNTQ_d+xHIFhaq76c`%Bd-5r(BqFWePk+nqryaJLTDww^MSaR8DC;wbRr=QzuQG zHFee0ol{Rtg{DeVEmM7`Mox{M`eJJQ)VEVpr+%4QHudM!*3-I98!&C;v`N$cn6`M@ z`f0nSIZwMhjh=ROn%A_4(?X{Gzx&7Aw?eI9)?n*X>vZc3>rCrrk6BjFTeGcmtaGjN ztn;lKJZ`zUxmnz9yWMfS>*nreb@OoZbX#CuXkBDoY+YhqYF%bsZe3wrX%(%jtgEeS ztZS|7tm~~CtQ)PHtedS{tWMUg)@|19)*aTJ)?L=!);-p})_vCf)&tgq)rv}5>v8J|>q+Y=>uKv5>sjkL>v`)1>qYA&>t*W|D_{k!kQKHfR@91FaVudZt(29v zGFH~gS$V5q-R+@zNLJaZSf_i;@R;c_%VW039FMsk^E~EzZ1>pVvBzVt$2O0h9=kj? zdMxl*=&{ISvBwgRr5?*XmV2!5Sn09KW3|V0Uoz;a*(uo74WtOnKqYk_sZdSC;v5!eK52DSiBz*b-zupQU|>;!fJyMaBxUSJ=v zA228&C)o0mVQGPzsa*0o({~ z0yl$OKqqi3xDDJ6?f`d!yTIMx9&j(X58MwP01twPz{B7X&>1`m9s`eqC%}{7DeyFS z20RO%1J8pOz>DA|@G^J>1V9jkKo~?o6vRLrBtR0RKpJE~7UV!46hIM_Kp9j(71Tf- zG(Zz{1+RkF!0X@*@FsW*bOSBmZSW3w7jy@$paaLit%O!VtD!Z}T4)`#9@+qHgf>B&p)HUTv=!P0ZHIP1JE2|BZfFm*7upBy zhYmmop+nGN=m_Kt9fgiT$DtF@N$3=G8ae}=h0a0ep$pJO=n`}px&i?Z2tg1GArJ~- z5DpO#2~iLYF%S!J5Dy8E2uY9(DUb?jkPaD;3AsX7p=;1}=mvBXx&^sG7U(u~2f7Qn zLsrNG@`Svgd(eI80ptxmgnXb!kT3KYdII@D{!joE2n9jGPzV$Xg+bxaQz!z8gq}fB zP&5<+#X`@a7f>7&550sEpjS{L^cqTn-av1mWau6A9!h~eK&em~ln#A_GN4Q-3;G0o zhO(g?=nM1}%7yZve5e5W1{Fd@P%%^jl|p4uIaC2vLRHXr$OhS=A5b+^1Jy!5p*pA@ znhwu^XTr1K+3*~AE<6vO4=;ch!i(U=@Dg|_ybN9ruYgy=tKik}8h9z6@W10T_fK7={rTg)tb137CW_n1&gcg*ljq1z3b7ScVl?g*8}* z4cLTT;j8d9_&R(8z6sxg-CzrR8@>bIh23E*>;ZeiUhqBmKKuaoh9AN{@FUn4ehfc> z{a}AM01kwM;9xie4u!+uaQG=40Y}2m;3zm6j)7z0=kNS;%Z;4l);+hs;M7APbR2$YNv( zvJ_c{EJs!#E0I;mYGe(v7Fma^M>ZfEkxj^EWDDYiY(=&q+mRi}PGlFd8`*>GMfM^4 zkpsv<6WfLD#`a))v3=Nn z>;QHUJA@s^j$qE%QS2CY96N!X#7<$Su`}3N>>PF;yMSH9E@79kD;R)*7=*zXf}t3O z;TVCD7=_UogRvNg@tA;#n1sogf~lB>>6n3;m@9S_yM|rIZeTaDTbLVW!ER%Bu)CN$ zX2m=(Ps|Iuhuy~>VBXk6%m;gf`C^Z;Czv1Rj|E_XSP&MBg=_n? zMPo5oEcP6GfyH6**h?${dxa%pudyWT4fYmG#@=D?u@vkBmWrie>DWgs1Ixs+uus@$ zEE~(gzF=RmTr3aE#|p4-SRqz~6=Nk>DOQG+V-;8>R)u}XY?vMUfmLHQSS|JwtHbKC z>G%wMCO!+FjnBd7;`8wN_yT+(z6f88FTt1M%kbs+3VbEL3SW({!PnyJ@b&lxd?UUI z-;8g;o$#&rHheq21K)}7!gu3)@V)pxd_R5wKZqZ~593F0XZ$FB3_p&az)#|*@YDDi z{49PBKaXF)FXET*%lH)>z(E|sVI09x9K&&(z)76KX`I1XoWprsz(ribWn95kT*GzT zz)jp0zlvYOuj4oHoA@o<4Y%O8@jLik+#R>#9=Ip&h2O*P;}39e{2}gxKf-}*hFk5wh&IlR$?2mo!CL_Bz6(Ii9N(#Vjr=e zI6xdE4iSfmBZMWF$` zIyr-!NzNi?lXJ+qRB zH*NjcCV7i=BQ4}@@(y{IbSJH(2kA+Ak@v{^W2l zQfe8soLWJxq*hU@sWsGEY8|zn+CXijHc^|YEtC_rmD)yar*=>~sa@1=Y7e!S+DGlD z4p0ZFL)2mF2<1#2rH)a@sT0&m>J)XFIzyeM&Qa&73)DsG5_OrnLID&=K@?0O6iQ(f zP7xGIQ4~!v6iaawPYIMrNt8?}luBuoP8pO*xl&iDYt(h>26dCVMY&NH>Na(Ux=XoJ zR?36&q`atm)P3p!5U$ zG!;X|QqQRuR2&sgy`&PTS5zYPno6SHP;aSZ>K*l-N})bbsZ<)3PJN^@s7xx0`b2%E zvZ);E3-y)CrShnJs(|`N6;ef1F;zm9Qe{**RY6r!Rn&LNM%k$!R5evY)lxsHI;x(U zPS2oc(zEE<^c;FFJ&&GGFQ6CFi|EDl5_&1Uj9yN!pjXnX=+*QZdM&+t_N#CN~XbXLtzC+)o-DxZBL3`3(^ga4M{ebqSAJRVbBifgKOh2Lh zXn#6@4y1$VU^;{jrNiiO`Y9bjN7B#eC_0*sp=0Ui^b0zUj;CMJ3G^#Ek$z1l(QoLt zbTa*peov>+ALvv%jZUXO(iwCnokf45KhxQC4*iAxO6SsfbUs}`f1?ZOBD$C^p-bs9 zx}2_{E9olwJ8h%w^bfk4uAyt`pL88vPfurNFf*B1%xq>3Gnbji%x4xb3z0&|hM#9U^sFaQHG5Cby^gEAO{ zGXz626hku%!!jJhGXf(r5+gGTqcR$!GX`TauFO^D8grew!Q5nSF>Z{7xy{^R?lSI- zmGNLa887A@bDw#@cryfGas1@CX>lxJ~5w} zY$k{K!hB_NnLH++DPX=ag-j7s%#<*tOc_(oR4|oH74x03F?Qw$Q_a*cwaibZj;Uv+ zvoqM4>@0RRJBOXi&SU4Z3)qG1B6cymgk8!mW0$il*p=)mb~U?(UCXXx*RvbgjqE0N zGrNU#Vz;u}*zN2Nb|<@w-OcV{_p>Kti zo6Npr-?J&~2R4;WW7FA>YzCXjX0e~x&uliE!+v4Evbk&?o6i=o-`GO7h%IJI*iyEP zEoUp(O16sq&e~Wz`-81!YuH-$CtJtXv(vd5+)Qp3H=CQo&E@8C^SK4wLT(Yam|Mav z<(6^FxfR?>ZWXthTf?p8)^Y2(4cta<6StY$!Z~qUxozBbZU?uM+r{nX_HcW-ecXQT z0C$i(#2x02aL(LO?ihERJHeggPI0HXGu&D39Cx0(z+L1nahJI(9KeAb#K9cGp&Z8H z9Kn$s#nBwYu^h+ooWO~k#L1k(shq~?oWYr#D|eN<#$D%ba5uSIoEvB1ZgY3IyPP{` zo|b0u6USH_id6A@9RK;(htY{1e`f z_vZunKt6~M=0o^UK8z3NpYjoWB>#+$;-mQ(K9+yZzu@Eec>X1yz`x=X`PY0B|Av3d zC-d+4_k0TffluYr_;mgwpTTGHS^OvdGoQ`p@L%|^d@i5I=ko>pH@=WB;*0qbzLYQH z%lQhvlCR>w^ETej|KO|n8orkQ$=C7q{B&W4FjJT%%ogSdbA@@rd|`pGP*@}^7M2K0 zg=NBWVTG_#SS73$)(C5bb;5dKgRoK9By1M82u{LQVVkgB*dgo`b_u(MJ;GjLpRivz zARH7935SIvg0pZ`I3^qyP6#K3Q^INCjBr*sC!7~92p5G*!e!x#00^J}39x_&sDKH$ zKnSEj3ADfntiTDpAPAx$39_IFs-OwFUAM})k2L>EBqAdgnD7R zI76H%&Jt&fbHusgJaN9bKwKy;5*Le0#HHdgak;ocTq&*+SBq=JwcE#htQj(Ase7pKZly_h0?5L3l8FTKS|P2JR!OU+HPTvXowQ!sAZ?U3Nt>lDl9RMm+9qw6c1Sy=UD9r8kF;0XC+(LG zNC%}u(qZX{`<8(sk*EbW^$|xk(o3wsc3jE4fQn z$wTs#yrg^5ed&SZEj^Tcq(_pk^jLZ#`APm#fD|YNNx@Qx6e@*D;nGtnLW-20Nl{X? z6eGn-&!rbqoD?s;loF&@Qlj))N|N44Z>41Eo%CKxkv>SNQks-5eUvh!OestHBz=~$ zr5x#t^i|50@}zvJK>8*XN<~t!R3ep1Wm36RAyrCM(s#)w*`*&+wNxY3N`H%A4fP z@)p@i-YRdCx63=^o$@Yux4cK*EANx{%Ln9x@*(-Kd_;DZkIKj7p%7l!N48IYbVX!{l)JsT?6k%FpB|Ia-d9W98@a3pq}XmtV>W@+&z}ek~`-Z{)Xf zviwedFQ>>Kdk!$6ja-Cc+PgiCrGnHA&Y-NrzSDB~GR~9G>l|{;8Wr?y> zS*9#kRwyf#Rmy5*jj~o*r>s{tC>xbc%4TJY;-qXa$UKh+*EEUZi+>@t=v)WD(;F^ z@lZS!FXf(cUwNQ-D-RVP<&om6JXW44eu}>mpad#GO0W{5geqZ5xbjqqP$HFQN|X|< z#3-@KbLE8+r^G8Sl?3ILlBm2^l9V^fTP0a}r@U8Eln+X(lBT39AC(LxQ^`_3DW8>W zB}e(9d{uIlJSAT#P`)XJN|92mlqjW2nNqG)D3wZ;@?EhhcIAgst<)&B%1@Lzuw zxH+njdPqI29#Nguqv|pBxOzf8sh(0#t7p`+ z>N)kidO^LYUQ#csS5!a+RY-+ZL`79h#Z^KjRZ68*MrBn_N7P;jaFmSSoOL3LXA`7)t735`bteyU#m}+T4 ztiDs=un`Y5&Yj?D}n!9Gz zJTyb2?m z41K0POP{UJ(dX*(^!fS%eWAWcU#u_Dm+H&(<@ySJrM^mEt*_D7>g)9N`UZWYzDeJ# zZ_%Cft@<{7yS_u;sqfNv>wEOQ`aXTXen3B{AJPx&M|5ZXsD4a8uAk6P>ZkP6`WgMK zeojBHU(he=m-Nf}6&=t)9nxVP(NP`Kah=dfoziKY(OI3-d0o&&UD9P;(N$g3b=}ZS z-BrJ;U(>JaH}sqOE!|DG=(qJd`d!^!x9T3cr|zZS)9>pKbZ`Bk?xR1_ef7us6Wveu z*8}uGJxCAML-bHROb^$e>JfUR{!EY3qxBd)R)4O)(Bt%Y{iU9uztR)+*LsrvMt`d( z>+kgUdW!x*Pu0`(bp4~Ap=at@`X~Lfo~`HTU-YkfuAZmo>jnBZy-+XGi}ez{R4>!Z z^$NXGuhPHkHr=lO(5v+ty;lFJ*Xi~8bYq4w)0kzan?9zoHs5Q7mZ8CW#ftg7@z?euz?t;ff=|# z7^FcNw80px!5O?E7@{E=vY{BNp&7bi7^dNBTs5v4*Nq#-P2-l~W>}2d#vS9X;ci$B z55v>&GVU4ojR%Ie@zC%w9vQyIW8;b8XZRZdMxYU71REhns1as_8&8c0Bhq+gL>bXW zj1g-*H(nTVM!fOTNHAU*iN<~bH2I2Txc#b7n@7WrRFkoxw*nzX|6I?n`_Lq<~nn|xxw6MZZbEU zTTCZ&tGUhGZtgI5n!C*1<{opexzF5h9xxA@hs?v~5!2Z`Y92F>nygiP2(Ow`0o+$2oWq)ggmOxEN~-V{vHluX%FOx4s(-84+o zbTzM<*UanY4fCdX%XBj>=56zidDnC|t)_?RX?mIW%=_j8)7yM#`k0SQU-Pl~#Pl=$ z%>Xmd3^Iew5Hr*aGsDfNW`r4OJ~N}tXfwu)HJ_U=%s4aNd}$__ugpaAwV7nTG2fcW z<~#GfnPPr0Q_VCp-TY`~n3-ml`N{ljW}7+Y7xSx`Yv!5xW`X(5EHsPEVzb07HOtI$ zv%;)2tIY4F&9s|8%xbg7tTlg{b!NRe-F1fROxIbivt8%7&UKyVI^T7H>q6H>u8Uol zxGr^F=DOTU^*F&y{{~wC(LA#+qK@{k()wXThZdYx)J-nQmv}t3jv7N>?o1}Me zF|*!!`$sk~1xy1oz$`Ea%mWL+BCrH311rEPum-FH8^9*81#AO5z%H-{>;nhDA#em7 z11G>Ka0Z+M7r-TO1zZC+z%6hG+yf85Bk%+~124cU001BW0Wg37DDVg1kHDXRKLdXO z{tEmX@bAEX02ts6cn7e+2k;5t06ahdhyV#70~CM?&;UBX0GI#^U;`Y03-Ew1fDZ@& zAs_<8fCP{NGC&R}041OT)PM%i0y;ns7yu(+0=@w=U;(Uv4X^_azzMhjH{b!hfDiBk z0U!v3fG`jNqCgCY0|_7rq<|kF4P<~U@C*D6_)p-!fd2;m2lzYi58$7`zkvS*{tw6j zbHO|?A1nY1!6L92ECEZwGO!%104u>Nuo|oZYr#6O9&7*`!6vX7Yyn%rHn1J+06W1h zup8_Fd%-@i9~=M&!69%M905ncF>oB504KpIa2lKeXTdpe9$Wwy!6k4RTme_XHEX|058ES@EW`UZ^1k89(({F!6)z;d;woU z00cn@gh7;@XIPT`!}l#Uw=`5NGq1}`+m2K$GY2ZBDK5k!M;f3KE(Ap2#65CWu7*u> zbY-cTS~)UzrQ$?$D}tIkcWTdh{qN^^p8M5(9QRA!c?g^*=lA>hettrxHHM)qp+7=9 z2e%~F(F?<$St5S~>q7lT^kan$KO^^wj2Z8?5W4)=!R%1Slsz5Bdzyr<{-wYTy`CZh zgfVOd#EthI-*fD#Olnx5?XHu?vUz)Sp2~HFg|Gz<86Q}edSEQ!B}{y(7!+#57XD}q znG|L|Rgw%f))0!W5tSGAbU%D0^sxLu4HLTD-Lf{Ma zb(Xtys>FoxhV3pe9yhKBnIG{q;|W_~M=_Lfx2=?;M7QyZtzeg^j&X~vM3?w`<5^pg zE-|w4pe>|JV#RoyEubwbWn9Y^1&dzeakh}Qn6+^yTT)x1#rPL{H`v-5e`AZ6h!+_D zWbY{vBO3R!rAs7cjeoOu1&JyeH?qZo#Iua~Y~dg=7h?`vDoA3`c#SQ%ENWof#+FzH zo2&l|sci4sJi4ql*~WHzT5a-Y&$j-u(%wBG5kr5UUf#tt(G!L2`%Bq0ylJoSa}(`Y zY0bY>>%x1%|FFr$Nr^G|ZVS=4zcf|CvQs4Q!o@Ab^8eDw4l7BK?tt%U621S_uSxRi zU#D(H%%lhgnw)KtqWz`Ljo1R;0F%FuOPW1BE)y|kyW7$P?5Qo`2aii^o3J8w4s5DV zRzLd_wj;eQxT&_RKFNCaCn9j)bM{LR^=A!8%uH}%q7?BaM$c0CAKb!Xov1)O>yD>KuZpA1T9-!3t_6pK(D9ywOf2>&XJE=8x1L$QE^7X~A$5d?E2i=}`n%nPzbbd{$OAv(U5; zPgTZRoVO2iR7P3o*hh4M2c#Hugla2WTb%0%FHt61=y!w#DZ5y}IwF>pnHGjxp_0mQ z@YWfgpzH}2W?@&99h$VYBEBn=nl2WHo>sPM(gOQ0WkS=1;;>uFm?qugh#6&i)1|=B z!^(&z&A{+S%J`=9fnnE`(M>vm5d+H9CWEEWi^@pw<{Mt2{QtNVmeEe^me|I*B`Ei8 zQ}y3}lD0&`jfYaGLh+2ho`_1<92JU;P1&nuVw@)`_3Wr*WL)81-7m)FqL2-f^;m&@ z%F3n*7JE-9!@04$?UYZNCVK5X^Ble$yGv79m8+n6RIcuJ?_T{eqern40Y{J2#jo!* z+;0?aAu4uspUUmJ6urAfcP+#>j5{p$G%Hs(4X6Au-I3Vv*iQAIcaO3_- zIjjwBf&XgXU9$I#NqqbM5*hG!Z@YNO{sNPK+7Fe;6~KSC?EYz;S4f*rUA!~9D8b-8hO`F z+|tbZxFYuHmDH$WJL#`xv`Oe!gmB(o*=HV(;g)s>Pa)21$Y3M)@tyJ9xm1rC|Qz2p<0+G$5Ov_drcrCgaMkoV^Z_CmK@SEj6*3fmeic_B%x7 zH*~rCXp%ESt_bGr>x}enC~?20Nz9~N5y?5w88y|g>>i@Y%mls*;P#%2JlK%nZm#K> z8Sq^QxBpz!{f6)EI89O}?fY)rzITyV8(zCRX%aHY;Mu|l-bM8_%(zoD>6y&$yTGC< z@_fT1cNB<{#}r_?^h(Pp~Br&Q(#Y>k-U6_yX}=$%p7J2 zvxBL@jAGg`ilE{{`LO4er|tP|8oEL{+a%P{;mEm{bT(d{Y(8{ z`=|SV^l$bz^z-_A`d9l~`{(+H`gi(k`bYcQ`xpD0`=|Q*`ZxOP`zQLl`&ar~`e*wG z`?vdR`^Wn``+xO+>;I{+u&2t{Z$V)xx@w|=U&XKFS3@hHRnSUkbw))-RYqk-HM#;_ zg|0+bb1FDhoJvkLtO8aAtAtgTRg_hgRhCs#E2ve}N^135#ah)`Uj(m!x3;~I1xb z2DF*ltP(~EP{J%>1u=qvAZ8G2nXwEkGnZMC3`szeDalG;BmfD_1lASC72pc<3hO)L zJMf+PopqXV8aT~7&3esv4ZLQ)X5C`k0&X#Hv1S-EzzlPSb(nD&ILtiEdc=4HJYqg# zU1wYet~0N*1{edt0CRwKk#P~Y$h^p^U{nACN+Tl6yI6lIO(I(HbNC_r4!#CIif_j+ z;+yeP_&)pwz8*h;@5Zm-Tkx~^LHstp7C(;f#Q(y7!~ewhfI;Memf_}%;!ehYt=Kgi$a*Yd~to%~GXL-PY5LhroJ#1(h!3B`Y zX#=~#4l)wcP%z>YKqw~<>;X&1sQ!l85lR5PoH?)yEFdEF8?r`h0`TRe0pYp*)TmDl zgCo8H)N=ZOAV>g3(ix}i}V@7PROJ9OMiD_$ujz}z;t$TnVtc+WNG=J%4YY7%h@v zvg=ujt?6;q)V-Es*vLQ=;b(Vk&GJ;c_WFpCBSTCCpWR`bda0J|y(UJCq?m|2y9a{P zs>^#r#F&x57l9io8m989346`OJR<|X2;I1=VV0@-eJ@Uo6iNHC`^KF@Q)kuJd!57x zk>oFXZrm$0>s6iEOA(_-GQaE^P4PF?Q+>48MhqWG`XW4f*WavMbzrZr7&VgqMR4@a zf@!d71z1rD>{2lj$_(9WYa*?gk%{=OCbw52&aBSEL#!y(+5EJc)812Ys5&1HsiJge z#A`L(y$W&GbvO@+qO@M~TWTP`80S*w>j5ds=taz^N$(YjgV)^{6aAl2v4FI4^XqEh zM<@ZAHT<=SOpHpN~f{~m|~sjj4iTxMni9Rl7d+NOt86I0ADdu9i82zlStHhZAjl7hQT z%BFSf_P$eMdQG(;#pyC3o7}O-`(BCJ57pTe%4K>svt!p(N|5OV)vOeo%lK?khcMXc zn0-_oO!2);1zQ=xsXNQ2A*z)ru9pSGRE$+JBU5bcrO#(PLbR$uQzV+rxE_IGA5yQI zpH)MroN7jKeFCLEq+dt8QG=x@G+T3Vff66mz{Xw;Y~`C>xW0jq4;cf9IW@bRSj%bnlb-Hgn?y>no9X}XK~4>^6nuH7!_+&c#P-?=Vk*T_yr(6}>0n!sd)5}3Z`FmJ7AL2H^+2w# zEi50?MFbmxjE{)H+#|M-d_ys^m3_%$2YS;=7~l*e#mV$kP8!?N8(R{1O}OB$li5Ex-E1Fka!JTF!Gb$I zri7gL>}%e{63R7^f_pt?KXX>tA>Paq0NiY(=$b0zB(lxDJxc<92odkEVdK0> zCA1&A!64oglT*NU@+Op!!A-}#QnUV?SvJL+Uc&sb3;al%>gQy!ZM^X%q#wewcPVC{ zat7JH-qaHM55d_xtETjvO17)FfK;}zc4jo&_L{Uo#$!ZVE|e`XWwz`QB37B|YObD( zW}liuE&G66ce*R$Z7z(hFlD`r3z4Wy8#4FHrLy&>T$X)7v^!%6F`o-zi%h|nZ)}P( zQ!kk-C3GpOXoeiC`TE>DIcUqOXTdp4)HicOxZb~yVsu07uN^Hm! zV%c+3Y&#V&H_Qd2>nZdyVN+^59Y9p)8nTt9kjwZ@iS0CLw($kim?=@kmwb)DgR}$& zxP45qt2>TM)v!E>O$am>esR~XJ`dNW;d79j5MnO);!b;=7p_F(+CgFh#a!gYz4rP^ z+_FZU!+G~c> zu76TcbtiFYg_c*buLGTg|GA5*e}(HV^tnoY9pWVT&zlX_Sm=9|`kLM&`18(6 zT?h``hF=wsxM_SkGpf+mNm@T6tFaXiEtKdpoACf)lT??6vv_pjsXo+<4@K%zx=Z65 zJgiWm&w2(&k@%E0*x-w&7V7u8%=l6upE3p;=kSn1kv{m$ja5;4szJjMJhD)&&tb-U zRg#`=(D)dyUI^{80x>{wdRk?JE8edV)`yuPu1eE0DjSFJN`;U<#Ej>v*zZ(k!zH|1 zp?V*BhOjF2JDu5Bg*Plz>O;=pS0#R@NfjFF)~b*Rthb@}_8CqD|Y5zM{QS?A3w@xOMS_=sX7l6$YSeu}s3A99}g2xt|+ zr<|)h$V>1yKkxY{pj8Ne_gwvb-gkf8dD0^qxV^jcuI?)DwZGGO!Xt9)9{jy`^?ke< zf696KBWCL^ehR7XJnxae&3XJIQmZikE~)+lZ@}O8JoOR1Rgiz@R~?O4;qQ80K!j^_ zI5WcE);?^^u|j5C4Z$4ATTy3 zv6d#`Z+x~c>VqhBg0~Pzqg60mcY&}%-6>w$f+Y=G5$G!X_Aa{qA71x@4~<+A;wt#| z4yTU5d%tjvMy#N?ioCtYssG7aSqPyqD}W&Z@cvt;z)M^(r+HQc3<>$&h1EagwJhLh zqzc;5ZofNabr@d3f)kBUK_1!zk`ML$yx9c`jb6bV+BKg-t<&daE!fcT6{I2I`McEm zPrSheUmCT7J|sAQXRVITt6XrU2|&2U7c*lPY+YN?bLb)T4!Q&!dOYe>k5xBOEqo zfz!m9gSbTj zM5^x%*U$6T7U&zyEnr80m10;2u38ol8=hMMJ3=6BQU8S3wt(LtZP9jiv+h*ax$)jE zpf?CxWN>+LueyGiH@`sLpl>mEcKu0V)fw`hEFd@VTcjP~KX+O6)x6;azYXdZeMj)m zoj-LfUiE_8hJX?m&d5w&umh6*LZ_%TL1je6h0Ve}jN_g<+NvIhI*OhYMuqzrCq8wu zRm+2FiyjiT4#yeCKSi-sy`Vv&7ld8HeT|c!qSoF8mjWpE265x z4&mN43GPlBYMIc}qH@Ak;n5&jhI4* z-eWUz;N>lf`2`pa^|#eiIV_hXiiq<35-=L+Z?AXkkzA`NK8o~(HX814SFUnh?u{rq zitvRz8tv~;er!Mv{MJU%zc5F`7Hor6F3LR?MMmMjkVc~x?1PV0$PJ15{Y%J=Ml9HY zyGJ>202CFvSH{G|Iabu}M$&O~s@hkmY^=DM*>exOxI9Nk)ss+E>`61!b053JJSRuB zS5TeULuS^`adz=}s4i6>C^`0mnaguuyW~7{m)bN`Dt3<<{P~UcST9Fy)k9G0SXDEJ z=icoJUQXI-51?ma<;<*}W7}_gp-NP*L5Z=tW|-&1_9QQKiP{gSVyv_o;<;yg+@xcW z>IJAv>{&DPb3%LKq!T!ggBrvhF+)Dbx5rPSgkp{Msf3%wipoyJlIm$=jDWCm+v_T4 zak;i0a4}l;P^$36HBNkkA(z8l-muc_{x>VV(N)wlz^CWhk;{ra%-{ldgd6g z7#eJQQRRqSax9|Wb1YynGT8p&vBz?4vG{t@7;P~;*semwRqkypx}GpbUW^WQs5mwx zHy=x_r;jlg!`5vXDwpJ*#3JkQW2D8Xb$iCKD!JiUzk2EzeKBI)Zc7D_1F_cnP?Nv`?I9i-MhPvx>IWgPGUM)XE+v3nM>$|wi z@!6;jRqu=-iwnnG?)qL%&PI2rO=U=0>^TO%d!sq_rlXeX!Hg>ws>dAedN(KBbkb70 zpK;nk?wHkGZ1e4#sAAQt8MiESk74c-o0D#$i`Duv4qHecL)`Ujj^jE8s-DjP_ch1R zcL~jjT&F;_4;dFNjvPbY#W%-uQM)aS#8kqL#R{cyEl4kE9SrX<@S#KHOnSP7)k|zg zpm$V>{k3BS={**Nm*kER?}!q+A1cK3QVYyWVh6=Lro`dLvDx%h3;IiD2QU>HWP3qH zF+Irw@zS#+U@9`m{=%`W^i~V}OHv1IDm=*Uql!!V8;k$O-wmeESx{g8hq(ivZd44? zA6p<_;yXxFQOour$12nRCF!Ug^r?ttyMNg^5Y~7ZDwAqr?|j?B&O7NeI$iBshHR5K z)~wj$YTRo_C)HCKsHT%xRI$(1#Me$vYX4;DG#$cP7vrwRzee?_5;Dk57qBkHzE_i9 zqkGhTW=J*d!NQAg^u^wC)Kyi;ux?VtIuv{NCERk-ReP9mrb!NKRgCSs4MJb4m<(c* zE*4Wv>`S_ZE>-K#P;8RMB8olx;$|Evs`?o&O=q#_VnScyj1x%KXBadc!6J+CeepA> zJxxXjRU)vll4%o7qzT#*gBAupVX3I6XEa$&V3z`EQ5E*C$KIxMng|o*r4U*~h24;f zUwT;+W`ejx0T-+eL&xUR*P7@P%q3tWlwo^GMJYYG2{GZh6tEG=u)lQdNqSooeuA__ z12?U9RVr@jZ=28)geCGuG{d3l*l_xM6Lo^V#M}tmvISHO)1NdUC-6(8ji@bq5Wr3! zZt|O;F3~q4w(NFPSn1VGZWE!0Q{jQmNlkXNBxUrS+$OZladBC*+a6|d&mHY@j-wrq zpOi)2_AyI*?qrvnht@uRNY?r`&Mf{psy)XG9d!JHtjld*v*hRK_S{Ld+ zQWsJM|nVq>;adU?Ym{`m%31l^(~W5x-Kf zF~tsjFK3)qkJHnbUxA6xK-=@#hnut_J;X}Mp~ zvU%btvsWIdxL1y-oRer&-bobdl@BWMl@luW6?siGWSd3PuHIO*g*K%dEzLs`ATcHj0vz0bLZ zCg$m)Ft3Q+Nj~WJxj)c~dD1AvE6?t@X-9I-1++`vSrqyep*wNf354>{26;zN$XEF8 z_-T}Io{?;JI4V{gHIYZ^rcE=*VIOT>vd=nYbAYXI~D3FXx=r^61^nX<#KZ#MU7Dh*NSNqT6#iU?nod-r(h9r?xzN zH))!-5*}h#neFQIHV@rRm?p18hd5Ne{13I)O`m42gl*a~voAS4$wPMIr%5YOoAw~P z5+p@$sdKF*1GcH|^g^u63>j*OSM4(M~Sj>~a$$T`7r z^g5}7df;Q7nB(M-o6phqI;3O$0B0SagX+xj<^*|N&~bU-Yn_~f?#!LyNP6wjfj_v> z5{q*@mvfMF#YMob z2$+qe*z3Q{>S^`DzbE~m&4yF#K4rV~yzxT6C;TAKMpGOZRcnRwKyJ^f{6&ybhjd+HDRY{aVF?`&qzzYt!iT$jl;=XftWV$xZ3M(#I` z?4&r^?4yTE+#5%9&M6LR@+2Ac(Z?n6jT1WeACAuCA+q&H9JrH5adHS8^5g}w%ST_A zsUq@Ju0>^q%mF)1*doaP*36}eib7oSGZ1oX4c-t3MmV@CC zCw0k~kHoNIA_=`d0iGON6A2CE@#uiga_oRExVuWEH!#P;7TLi!FX}#)M<(JMNaIn99l_WN-646u zf6=}1h{g8xn+)A5dAG#SecC4Gy|MCko=GP-srauj*-UY3vm6hHxcn|h+({TJ^Q1K@ z$HyTtzsC{(3Z|2J$l5vw=MbOY4ThgEa^?kVmmFV*U*u1E!cMZH>tB z?2Mc03c_80xn!QTM&}SZ6Q_ED@E>6YnMbUVIrz@_scxZ6qXRd?tz*S>CK!Bx&*Zb9 z3@89)LRlG%3?PGW}o!dPXDGN6oE#-cK)0F_B)tufYs zHRc)%!hiq}CWMvDNCuLb$t)xT2_TtBRvV)YXk)gq)EVl4I#ZqXmhl#N%Y4i7WB37n zOh48l>1bD)H!g6D{0d7n;)-Yoj7-kN$3_-JiA=8jm&8P+h zheu$`QmmuHlhma1oJaUp7&KF&#f;}ciu=%Y9d{On&OFtE;`xvgKlEJ3zk$Ir6hTzI~uKJ=fE?xbcR*fOSb*Z6Q!xnmS?c(fmTs3JM6Lf zrCrWGXUORxRuM1TJ8pWHmN;KKLrkYw#k}n7z)qEd%aSw9bl_VkmaTR3U}=K0`58}e zWEhF<(8As?{qBrALrSN83&*w>-@ID-+S%z0A)Wj!8ruo}94(!3rktUtGrxsRumf+N zFMZ@}a|WMI`W7|O5s3XzI^gVkhMG?Q7BSJjbdy$E;p}=QRJg=Qt~cD-)+$Md1KwrI zAw;0W>WyMN$_WfW)q0+$|N zLh^gg5BzMIw6jP79Q1L-lDjV86w7R!)e0Oy06l@+a{-@KcGekMVAX^Dd7In~Ug67p zonZx-9^%g=G6%fymK|}16d-y$f5xqJh2RXzT%FYm&^?5oi7P!J_{y?N&PoNy9{kVv zm2Poo<5M@I3Swp7^Lhh`v{D8p3=G?Eo+?f2wIpIo12Itr9ZuMP{&OG)cpL|)2x;%R zNhp2adyPmerGOQEC-^s%uJncwnWaE~D3PsuQ=v4m*PQ5C8qgm}?9jzNEN$t<5lN-A z{%~S@=}k;&L9Y{$P)Y{-{LWJF2uD!FN~D)E`@_Ig|E7LvR<8{aUrGXfBOMg%r_#Y* zUm~@X-XAgBzIu~hTG{JL43#P|*6oe%wZ$YEa315^%Amaxvu3Lv^tj3{SDbnoy7$y9 z3OqU^v9jmiV>4jA3bWR$IC^|#_YlsnjM}R|>$2(#4r4h(`1vwOugENX^~Ud5X4fU0 zQW>&WZPsDc`*#Af=Mw%&nR+jD)@l{|`!=(?3g=el*9)7)tP+1GF*#NE;WDLO$Sh*j z^LN~K7l1P?bL&-~MXwTmCvNuu`06slUZq*&D*ku;cDHn|vHs1N*;vK*e7!(LS`q^h z_JVE4Jx)p0vsA<;1tOwebl6SgQM&Ye6v;^;h=>>M?OZQPiQY9uViE-r^P;nTVv+)? zYZRGDKx3#UTaznKNzgM_^h^q9jP&f#oXDhn*TX52l4yO!^YVGTs_JoJsU-Q5~(q2ydz+uoHC&2t4K|vH%5%NFLHw^6?(3U zp@Kn1(!F7Nwuq#2ocsLmR5?9~EVEV*eB8UPtGv@xC%scys8%0*;=7)!{MS@nJ%udm zRvbS5U3VYv7L}r>pXJi(i%)*X>Eq8(rS(Lz;H@|Kv81l^yu(x*J+&-{R&Ra+spmZZ z5%sJdG|Q?L%fC(P{=mCV_0@xAVOoj&BogNXe}H;K50ZrdXV-DRx@f$MR98LqEOaY@ zpZKeX#;>4W(o@Ppw&MBmzq-ZrjMcc2S+NJmyz)R7+8YKs>>oCYdyq31)=S2K$U*U#OD$8A6LM4KX&-O-@+oF^7Ia&Nx zDzsc;&}`0w8uzKog?E;UEgCWut2yl2ZF+Yl&z0&| z4jaVG5!aIFoJ#%>RjC{@h?w(Si~HThgwxYCj-J`a$7XM^W8+>>P#@jfc}`-7RG0jxaU_N1=|?lACuam_#LE) z532*gz6R9C^tK3o`!6nSwIbNnAXH@8=tyrwuq`qP#(BtZS(6Kvs5Be$@QW+!!tl(`jXDhdddbaEzs zuCA(r!eVN%;B(kHkGTtnze4j#8;7qqrg?p#HZE4bzdg6ias1M-@eABuU4+R zGD9KD#+Q0y)@|LA3^`BuZEMhViEXnT4^~`tmm5!g4ZVJ98@1!ZO04d2ltt)I> z@8DSR)!oB9zcuQ*{f7iY z!k@%HJuH6pn&GKhXX)2MPJ)E6mCxqx=L%(er9pqDAU$mR zv+;-qsLYgpBxECq5Bv4m40J`xmP>1gd7*-MUcM8TuNcE2I!%UBh#?D%9zfy9J>L z=|{NK%(){AVZG29h_sMM1iWT+#KK=&Pwp_pMo2Bfp=N62iodQN^bzE&5U8!JnHaJ3 z*D05~4)GNNJ(V@HBR2l9a_9i$h!7+KQ8PYr8MIN#U4*y_sYjq|evVi#=mtY8AeV%c zB9Jxw5vv6qQK6c@WFjLh_Db<01L3r1Teik|8jdn2rPCrU;n-(OAXTL0DEmsfJJJVE zezs`~&(rLZ@sWNXc@0i{wrY#WJJ%&UEe$3XaOSfewvm^Hw#*^v#7J{EctQY(AZcmK zK9B|lk#N$p4YrAwW{J!-Y0wA>Cp=qWn|qxDC6LnKgE*Z2Y@2O7sSzY|K{_ka29AIB z3*?owf@D8R4@UaJsn341;h-2!CPcb2(iLtbDrx+;USuTO) z2pxhbMM7#3qvH#g*R>gPmmqGD>b2<6p9|KYKMYz0F^p8IMS^-mt92d8$Qliq=voVT zDZXjoQQDm?3*&4JdzlkSsiu}kv3Hg%%(AuYW%H9joaiX|&ZY$%G%m|{CxL{}QR1Cd z3qx}E1`Rig7lj+hKGA1_?m=e&?5upiLbWNA~87x4DcNsA!u3v!Cd zHl}JX9NMQQuYdwUXjbxBQ|JpT@aK~yXbhBdN%l2`y}-23PTB;)K0*hRkC;MUAlk<# zFE49{$QdNNnt~$0_Mem1%eo;@P~>L{`up1XlUB<*;-)pnWujhK$RzUW0zGL(Th7KH zFClX(DXq@Z6I-+dy2-ShWdBJ5`2bIH(WW!}wPueDAqlMfJ&8rDAcJ5ZoLkFZ9ll|&oOi&K;y9=|&<451WUwV2M=$erz~1WsHSTvEd`uI>{cON0>wwYjD@I~^VJd7>8!g!VjPFP}6^VOh z&0{w&7i(WlKaF%!Idu=!Jblx;SodniYoxAu-MeUwL^Az_=pf1SYARn@w?`=WllzPiugjnoU-=jRQXa7l=1%fnb^q7-Tz8 z^M7~2!8v+rhz*!cI~a*yF+QOemCbf+Mb06IkUPj4KO>bs2DsrVwIe$)^Tqte>o^b)!^Y{D-dhzh@X1790)dg z4_vK4j;-V2RKYSOM6L26C`ikNbi_=(SPlRSz6anqJ6F9U1N57bH&sEcnH7kpbtF&a zE|WHoZ9mx4;eN=n)>|bphotr4e#I3a=P!rM6JKTUmRc|G7hj>}C_CKG|LTbM(RzRX z;}vGk-<_G>U$ybqvdG#ykCDZ!n{`>DCxTEjx_k9IvFs)DbE58H0zd-lcR}lZw z%Dw*%)Qu^V?tl0S@?KhH_dk7S^8WsnN&5-{URrMt{w0WeD*4D*%S%=5;lq}1JiJ74 zRNwPk0i`OK2WP&e<4+Z*^}V`92J6L#1>bt`3dMEst@Egcf6~}!+SQ3B)D;|hvSpin8Z@@R!Dc}_AAK)Jr0U)q`0zX*_fCB3w z@Q{T8Fsy!{pQR7zvpxZzSag8S`VIVMi2x$37$Am)01&K3ppm5psIhW^ToxX{v%tiX z1qHyY5&&~b0L&->FrNg#Y!U!-NdU|w0WgmQz$_8~b4UQpAOSFc1iWz>O952hmH9{(!=64CTJcK8dowEb9epSx%L*798x9KNK?g`0-qEQ6f+k5fqOGhLu2Ar!mfUKjXwJmikH(!t(U=Pf6_(Jq_>(DwWBlWYivPIz0{ z!z!q3djZK^XD-H!CD%W>4UH#)49dBnQzgPb#EB8U5v6ebr&;V-_Ss44Nt?;DVC)VH zf&{4tDV2zPR0DRayVr74+#UY*@(yCt8gT|&8W?!&A1J&ZlrFa zZmbS}YxLGc{;2*Ch#E#&IW>0HQNz%TF@4z8bONbl^v z1YwD>gjrH}XHs!Xz z+m5RLrLL_0xBAhyf4x-}N#Ue$?!Z!Dcgj-A?od;xch*wY?nE?+G{rO_Ec63K&nFx< zPEu}68E+Efsw`;+if;&=Ph{L?L^F~e8Mi9)Q`A#_HDxrRn>bCvTm>$MtIwr#MYssA z8dtd^+Sdg3qUjJ+7#bB86v=xVk$BP znFp8>Oa-P06Uu}zl?-BmP#_kFEPWBfuASVsd^|+bAnCCY^10HBTsD&3Ihi|&pX5&- zTs|Ely}jEY8HfX-fJ9fLYU66NYIrq*WyCUNnW#S}_Nv}GM>$n0+<%)`>swpusb84Y zJKKA9Mta6(=IkvP1xA5T)G11(BA=A#0`$FfQMx=`h%Q5yq#vg5?vFQ!o2`BPWz`eu z+2-En(RQQFQ{7$NL;Z%j=Uex;9wLRDLQWB^5LQ%HSXM+Wq!z6et`$Y}iuA_xBAoRp zqWTGnHAxu<^1n%)h4Nt^eyf9!{G7GXHtL?qBCIb$o4PZES6nzVLfdWI<#x zW+7$~v4B`?TxeWW1AWN%K*{?FhT)F(j^xhiox?j9cXsb6@9f{v+7aJ52?(&HSc)t` z)&Z6TOMxZAg0dhiCBsigCAsULW|_w9a>SWYY_4i*cGD~m0QqsCI>)?(M9j8usOkoT&rawj>6B$E{ zsf{VOoBu2CExU_pD&Xoa=?3b5h#0s$WHYBc|JS_AyfXD3^&a@$y1RCV-t@a!q)DVX zrYWWw(S&GjY-(&)YpUel<6c@i5ooxfy&<`AdgJiM#f{w?${YJPv^K;yPSQddiHs~p z1mhkfp7D?o!^mJHGoDZ8?atiM<)EM@h(L=b&DM?UpF3KZ?U@O@G0pr4o3 zHQRM|T6)@M`YhP?!N?G@I$7y`%*Q7op&= z(xJ_^&A!d9%|YE(-Co^J-Qlh6TYHf_P97&8mIuo(%PY&L=27$4^49Vrx&S%d*r*=|1*Vt%b;eBAF7Mks&P=#u^W-VAL#Z=&5;@tq zoV$!)<}V-IJk3nn3BQD7U1piFtU)=zv+9@CFRF7{Pg&Wl=jt6q!owcjw}T%`vBaN0 zO5gYQl)jyH?*Bmf{o~Dh{hqn&p9n;VGq_rE<{sEEJ8$PIvsc*cpq-Ptu08ejVh%lfG2t(en&8l_g zn{he!JE+I`0->veu(#`Pw{-l28^6?MJ$x&STP<}fn#CB`eYA=%4tdQjRB+;R zLAA?ZNI7}Dv@iM|YmjNZvG~u~#}&m(T=hPqhe5SFT-nb$(@u}8D^`#DuYVfe2_DXY zZ$r-u{Or9~AP`TkOj;?+k1i&AMcs6e`nhkcP(&#|TV^<{2rZmi)(3 zEopYuGa~i(s;-=X|8m;r>QX~#0qCyQ4YELCS;5v7Z|2%AL&L8X4}Sfs9l}sadY@4f zpT33`eN z7ffZntExzR_u;Vnt&Q`_2^VapeifSw9Dl#7Afs%1>+q(}Qgq=O*Xme(&>no=1-sEZa_(+e7E?Kb>0MLUXHMIN3PkH03zGnK+vGH_K18ah|qj z&{|x6G}dR(l;!Y3rOWv5<&;4)75j$k`B=h6?a240@%@(M0?7Bq&B4N_Gn1FCI=o?= z?1%Kf@jp9>Pjww0*nwtKATC5NR@b`9t9+}g2$iV9YVlfj+Un~1q=+;J*Xu5X8R5>~ zczJv6)_YOEUss&lKr5eHn~W&l2n-7=cAd6=J{ZI%iG4NsuhzwU@Y0!AY`xQ?TLP8d zRfXbetn%tOeWiM_oYlGAh2VEkX4s=_SwSK#Q`|$5> zZ<^G+TH@95Yqkaq?zy68iXbz~_7=5uEP#5*W!qPzY(OfyX z@|dH^MUTH3y4CZV9&50?l|1jdIqRtH!!855>usakk;)}U=dlX4d2-5&xmVFoyzQgQ zlIE08e|tun)PF?(dX$KOlQW@{3ITMq&(hU_jvbmy&P`{NpCv6(R`hqQQkC_-=~Z$J zJpG=CN`5e5^S-D%z0DPNy@)whp=-hT@hN2k_5Y@ zr;aa5&BddDp0dZp3c&55VT@lEd4=>n*jWmuE7qn2Q~v)-zPjGNsLa|P4*2^6skz{4wNNXSa=xMogOZ8s-UnKSn#uk-LiboTpszSIX@&+|CIXUU(0gZi+!{giwtbe zYkf<-<)((Mn_Zd9tdJ3M6NjDTME{3@)99v3sfW_8)m_I0L%&C!!M3#@RTKe(*R+h9 zlOm|dMA<*Q#Zj1$mM2zAdD}_G)I8`{{Epx;b>|n}K9GM%b(Svv=vlgB=J$Qlg3b_~ z?H61P_%B9opDR14UP$7O;IW-I94WJYCM_uwF#<@$Lt;lTzbw= zU{*3bxl*VW|H%|zNTzfzk44#V>a2RxqF0{|oVaIyk!9y>*_h2_0eu4&bgFQW?EH~t z2BllTX;?F59S7j>)qnR$eH_`!+G=>^cx@#$`mObs`B#IbGaiL<&O-WBRsOaUEDIa^ za(k+p^hUew!{bH7PivNHBU=K_c~Q}VPi&*z`~yWa+^W9y+5 zw=YTlabL8ZcCh?{H7Vqs-u@Kmz_?K)U#{wU_^bNw#>Y{L>+ftW_HKKukv{0s-{9_k ze#Ra7jWfX*RV-F(Ju%mR@L*V2a(az}r|2Z4@5(skQvjvODm)%tLB3|U@yCSrurc$S z39c6!%3G9q4i^E48Iph6GZ2Ggr|WQ+cnh?Ggydrr8c=#M3ziyg@lX5gqcdb{S~Hl?W% zsI$BRfC2t@iTF2Bj9po6WX$5&y^B&=THZsy_C9!NLz??~TsYw$=|WLfwQ{|&tm51Pc~k&D(qE>+XGd(vzwc;>Lg`^G+ViACdn7*pb* zK^xJtfbzoAJk$iPNXEWd8xMEH-ihvAk4f@4n?Hx#3GU(SWski{i{rWF$u*Oh^bNSf zzw+2y8S_eelm4DN4k|P(Ht-nj9qOIpopZ;3Ww7TpcAoBAlD4E@khwHTX^Ddj9BhxF z@3}{i?NPerv}&3oj~W3yjcQc0Lk-+(YMir^(LCE@SiFkoy2rKJ118<|<_bkq39j%v>$&SeeEX}%SWgYO#=aDd$+%n&`%iWO$mJOJ1 z^<~h=zcMlHJLTQ<5l)uwipQqQu#sGryJV{5-rNL0AVZtVpA^yxN*E;vH&_^{gDcC96Q{^(q<0~Q?cndmZ zzK}u$0pp-wDvmB-AK^JwJ|#yXum$)!{J!SsGoluz1I*RUt`sr3wjk^N7Y!p3NPd8jSGgZG%T zqPw=KwyEB;_RR9sOnhW{)F<(p?U-mjvm&U%b6#tnd7fSGka3NwILS9G=AXB=(l znwa>>Mo_?=tGbHsClTM^U5!2l*Hj2ET}jO+rE3esC%}2Nf9<{0btd8*_)qbA`sfpE z68O#N$?rN2QE)VI6mWELv~;u!QE#kW22z#9f=c062wQ&rr(A>;zXyK|e|LqxiJQl` z2fyIlW9{AsyRl{=ol!p0Y|<|H^K> zH3O4>W8o1ntM#D_Z(a%VBO@ncI%620m;Z`Cia(OvDyB_A*P^TzAq45*56G}p?~p5D z)YT~qfW(t`ywus28lMHaF?(ihUBjxjvy-`ewN?))n$} z&ak}LT-#iaJTt31w>-Dr=<7=LlYDr2Q+V=o=~LZpeXdRS5C8NhmIwZuvi*v-7^9+1gD^c%MSJgWuOdpPi}OnBCGp3sO+Xw>tb zyrM8GL|S#%e>jvu%Is`ye+lpj8m|9_hnl4R-4DIS?zU?w+4N#~p8D+9hmHLW^4G0z zgZz2DrqxWfuPJm5`yHRy`fos=8#X5&rR|sY&9%^53JHkN(r@Ly^w4UGF1x=~o900^ zFC7jX4jf4#q>gE}X;0lXQ#Fe#s1a1~h4pQ1u68bdZ~p23d1~98u~3a&GO$xm~qwY-}!m+q`Pu`meV$=c`V{x83KE z2d^{6Gl4UlGpg10)~_w^T;Fd=jQIt($2|~wN#2s3)vuDMbo7h=@XLNcyQMmNkaUZG zz-!L_8vHf-Yu4Am2ku)y#HUu9*8LWf&)qx5zx*e6-3APWn^Q%HsJ3w2ezx$g3ZLRIr)Y|O#ujF$1l7+AEkrRyF%^TMgDHb10?#_z&g9<~ zzQ}Hm$9!{KUHxLPZ4`4WaAxOQb}>EpkvtdT+jbH150jI>YyPjo_DamT0JXzo&4t|d z(jfAU4uk@MF>5iM|H1+W69#JsF9wGP(+5KaixuPx>$KL&9OwAS$sJ>26>JLIr%mVp zbn9sjYW&s|m@$?grT|NT6~IRSGns~t>TFwy3ICJM?0pG5Q$XOU`0!laAk&lo)Xt~D z?%V9B`kRb(C!g-appti)%b7TWRCXfk?sbRTC6k#GeA&+TZHMh8?=!xr2Qqr=n9Q2a zYR-P1m5VqmX)PHjLEys}{m|W#8i(h1Y>^MAGhe9><1dx?>g1z4$z=RyY_qF8l*}B- zV9g*KAKM$T2Dz!bIr6r@-v9T;>B?%n8{*{HRd**iz98`L%*KDi*Y7H3yqWJ`rMKHv z>|T4uEuZAvqwQ7wI46Ji`lHiT&z|gfSZ18yx7A0xtEIiGapZ5Js{h@%Wb%P+ir_8( zt>8PsIKfYXlmCJS_KNqCt*z>-mgXGnJ^8Cz}$RQv*Zpn-2m^WP4p6<0R zhm<>RHt_umN=X@2d92&jm2{IItu_D~R5`Vl>@MRkG(L9izGJ9zURrduh>&c0EZ;RT zcwW79X8-o|{;|`_ZM2fz0Nm8L3Nlw5tx6H~->XVlWA|!a)!BN+fA=g%&3<7y+LdAh z&T;l`+?5)&XLoMuKwNr`in53Q+yFDNH%f%c`h)-%3yWLO|6 z(+PP4k{m>2)41p|V^aDO8D` zM$hN^`&y!tCSyW?Q6h)t*!iaFU!E@L#_*fF`d;D9j?dC9OosRzn7YC6|H^gzw@$|# z`n62Z6cZo%xJ=X(O9T2-?{_XHEcCwKa4wc;^zGhYE@n0K+1_X_)-m*}-oP5B;NTiu zyaE|Drj%5yRIeda*L*{IPBPZi5i8f$|F2_8pH^unXy%S}tGrXv$}Uc;&RJsnE_JK! zS<=JqOK(l;#IRjFZvb`D&s}tH?VQBYT{3S_PSXA^s1^D@lm!Z15u7{7e%_zK+ zpY$twX^f&Cey(1VUF=I#^Y@Zt?|z?}qrOWv#n+ZmwjItWQe0#`A-KVO6CBh_yIF%& zPOF(!z8?Pn6w{XUV5OnDnNqWTm&bi&ril%a zVlP9v&U?uh$T)=smSVLe1>M!(><&X*FQ+k0<#RbD?-=W*jcq`+o1MH}yK@#<1I!VF&6n-9}ISRf8|K z$+jloIS%~*W94E%zB!1qxz;OXomK%2I<9Op-PHX{aBPV!+hA_t{}pi zoiw3JQWQ1Aa9D@bWU&C1lC_fo>pCiESVw5c*kmn>3Gv-C#XT`lE_QACyQtTw^K6!$Ou6t;$HBl*l~XV5z{vnw~VKN=zS>$S_=IDPPB; zr3A%KrXRfhtvi5Gt6hN)|Buk1R`rDVE0X^*y9i&T7l>jvhc7ZBi#=O7wVG@MqXXGn z94^gBEgb6^!EMx={{(!nB8#__FQ7tfvEhYVX^WgV;Xzw=7tvopVjpuzIKxQ>RJW`y zBK1DZ|EK7)NFxwQWnL;Y^eIB#JXeVI=IUd=-9_FPKo;)D0b`f&_xLvbE-qrlW90D? znY?TJ*_W}~@vN_-A|;xK;_Q43XH3ODm9;bHXr(MD?xGpW?w5*N>@Yb^4Fz`5=SwH| z)|lk;+!09YmI{`i2=o!NiESS$m2Ypo#()2DTMH;qka;~M%VI!%3wbSP5=_q+L6j_F zFK`ue4;8Xhx~3~qsLljo%q0HF`uM$NrpA7*lp9+{V>?eiJ~9~p4(h6)uf*VvYM6F; zgg;%RaULlOY(N}(Vd;1DAX)yfQY9;q=sq&`rbN==p{w2wMu~v!O}UY+997KP+M4lU z-LmULHJ-+U3shaAi@-s#;4!-tc9fn8Um9*3A`*flwYo`;qH(1kV&Oe7=@wpSbjHCIDGf20Psr%NxI zosfrU*p=w!$yL1l`nzH}EyL}djk;DyV8d+iT%a@)iUswhHyvwxeTQLdR8Zr ztw>sFvc+jJYf_13i~$6p#LML}8v4aSa>hc;MLD8FAhNn-UkvS~tVCXgMorrKvBMJA zB(wlXxF-B<7%^EypFoU?BO-I=qKMA)yihcVW%RqziJglw3; zt+@4%=&J7&A8poxq`iEk`*hIOoM03mvBfIs`CeBRw*?*^uu*;fvm$fmz!A6eRbNlyd)N!wm_H-`7h9LA5clvqVb(3fWS=ZEW*a~^G*PM7lb2jR$m zkCS?i@1x8Py<2wj2y0U8OgW$Oco5}EFsD=}1c?p)h5eMBWWsMza2_F5qw+*|Y=%rR z_ZE!nKMl)Ae;#k|{31YJB8oeLGm~z+STTA7yR$Xv65J?F5$=za`W#5O_>)H=ng$g- zLnK#ZXWUbbhbOU`I1>EjON!bS6`8k80S!$Qc|M4ifU%b_r#P?=#>=rxhwe3vhe3;K zIib3Ohe)dx-T5gmTa7R7=k-%b**F>Mv$J9 zA=L5K{?>FQa6ZzI7e9eXi2yp*Y7 zaH?1@IJ#xQ5GQkWI*Alrg{Y-lFoDJLts7de4qD>abW&HIHTv--n@?$P_TlRUt*9BJ zL-*MIGYt#COeh=;L9=<2iL%VMQTf9n!d=LYZvefUh47N|XBK(AJubWUfMK&Wm-A3n z|D|%0>qr>tc7z1RG&C*l&z)7I=4UY{bi&9KHIrA;r=Y)I|C%_i!CP{pmBA=)RJ7|VtTwO9Z5@dxW%wJp=S_vCCa#OF!67*&C_I?9K<)Ui{LV+LYye;#g#=!=clFFXeK1eCgza z@fH`W*o6PNN|xAmw++p5Et4%wNoU)AFQ3xcnP0}bSBD!uoxq9fE@eOHZrq4J5n?%l zdJO)tdFErt4b=4&Jn`crv9cNjMrdFW|KoLBd*zI>ZRh^u(6Zy5NOx!G9YT1Iy-pcz!Wi@ ztqLf0#JUiP8MIO>OdCMMLaEY{fghCOM_2|Y+a)IP*M#*wb0pT z+-pcadQ8l{W@EB-08?N*B$&xY8oKnI9&xbdGl~jKpdkPQuG*5{tF{n z=fjr?QL3EzzvVd%In;^`Egyv8q})4*LDOIj#qd17(moU@Ddxik7QXsNv@+pEs}MJC zuVtm4SsZP7Qq*{UQD=-qySmv_fp0@UMhg!FYLv#~4bu~2 zuT=U-C-9*%Q+^vAyW@=^!?`Z|OO7!H+AYU?nk7qwvE!KLDxNZ@u$ zbsLh5R)5$V4$gd!Yi0g6nrZcA_E5I_cK#VnabX@h@D+B*m9b>xR-rxx+LosSPrW5( zC|}JQRbw3a-nd!VgxSIhA9XP$9hFNFX_CF0Fge&|!d6YXl6rz~asFXWn z)10I&ksa3fnOiJ%mh}={%g$AwBO?p_oi-+|W~1CY#-dH`>V;~r=)d`-j)boXL&6K+ z17~IB?X6!DFZPGvJ(T(J!)uwQIXEdP_|jrn!d?4Ml--GPn8+!E2J_SAdC)2gv=P1& z1w2QLF-5bbSY%q3V-+0t$(nTpiQ3GB5%N?l?gCjC(|h3x3Ne|;OJxc$QetDJ^=O{W zq0-p~Oj97nghd*^00L1XQA2*To7Tbhdbc5p^C@`k@&RNfaq9 zs(zljC_1dy{?g;a)5>l8xVUdf6C|H6uEMg$xTaY&x}W#PD6LvBBgm+!_)pn4Mxmhn zEsd{bLOYRR#BTM{M8zdgRG>74>)vU<5_O5odorpLnK?Q#?AT>4%V{eekB5SIR#m$- zeLJ(r6xP8*t2R#9C#iMlIzh&}s1t&t&(m4d^@_7WmKag@CoqO2E{x35XnNq=9%HtY z!4&?tj!{cN>J!TMYEj6Jv5Xor|=BRbHBg4-!7cYF zl;J6A@uUIR*v3q&cX~cb`z=|iEgVzl5Y3zTIc>kU;#hoXaUY5xv3sgzwk+0U{p#4>m zDcjhHD~~Z`?WA@IVQT%#1qN~>Y$P`bDl7YWbUEZ^&(iHPmg{jN9DI*QVoq>?aukd? z&g`lqlDcAEN9tjh<1-cI?Oh@fLx=WpT$*YuXuwlnk8Bv}9>G~XRHgH_>YWI?2C2m{ z{FdwP%N*ZJ`0dU}m^MF*F24FcJJKElMFTt8<(=G{3X}t=$l1a4^0d-)RRRPAHP7a0 zt}wv5%fhK&6iJfiLZSThX)(2zs%DrLN3)3a9do#zVbDNdfR<4Y{fvDFE(8Avu*;#v zowOti52Bu~bn6o+v=MCUkrFJNq>JD2xmvhVz2sqxVDH76zWa>0ORa>`t-VY`=z&)F zv@yFRkDLeb<5JaDBCFl)0KI>|n*ADer#RjWamloj6n@R?Ydf)Z`@^e#U!FqNrPFL( z&2(>aosAob)(OpmQsiO*|QTuh^d$}<=&w&iUmz)@oKgelTPhr zcS__he4m|;RmwTh@+2yN=#G(RO9zSMSZJdj0nZxq0Gu({3~vpIYH3Fqi`Ptb$+ajB zewfCUw!biEZCV;*Gj=D4P_|r^tQ3$w3QyTe;@A+f^!&4litpkhj#`R2npaD2mNq0B z9&9np9fc#G+UGi2hxtbpg{~e^#0phus8-V0D}2e?SwN;Srl}j59o&7Tjc6r zE@kJXEBkYlmDhYdbRA}@ImZT>`|^0fhV_T#UG9Q&(i7Ts(Nlo$CuFE(fVh<`GeTb4 zJP%-bT$!tpz=tmDoSJJw%K$wWj!cy}xdloc7FfLd;;WQ~^_g>CP1a%kLXj{Cwq2$j zBDp#CYlu8QU-tPxcuOK5t6Iw|quTzm;Vq<~L&S!X#}oFpXwsZ_v7q;tZ3Ay9owss= zvbsh_r#C*?Z~AdovzNk<%da9bee+lX2i$sNfA!|FZ!NG z(I$bAA!Q2n!E)MDS8$FCy#Fm8-Vcs8$xc{=Rq2xkUD<0bJmI5^;p(FO`fOtC@hIt{ z#psgL&BET#{#lWVeRnSFMdtju*~_L z#|gx5DW7#4%tRMu^EBclHy3Er8*L7MHj*COtQ>`YE&C!{Fh5>OpvU0*&&za4T8>UI zrgvC!&-QD$MAY2-G?-ahT^@7pdoe4Q0|1Z1oQvwu zsKuF{m?(OTZMI9R|59xRU?5bSAf;wnU5~Oek|=CwC5{3E6QdXnbUTHA%b|`ni47@xYu9*S z>P20QH z|7_lwk%vdriFB~&ZmDTv}7h`=2j>ywp{v5d-|IHCnn0GI+Yq|lL^-GCrDffXubsNbzE%c+#L5Rg$`WDRnpw`xI0J3VVzE_az! z1eOz4X=_VF45v*jSD>mTc%-pGz77f)aQV2!%6-2vYjScQG6ODRw2Z4-HoAGrYP=`s zxDN+3adO$sETZ7rX6T3(+yu@^SRK1Fpg>V|h15XdrR1}*Kdx@loVddozG7}KYZOYR z%wYWuq@G*Tm5S7`96uVsU@CKyllvXaU*@W?dgkvZa$GBwU-90}-ogxZlK&t{CmXsb zkVx&i$F?S)FLEBgC2*DlVw{f==#ElRWo}E%N{-~rms=q+b++UBjmD>H4g$n;m)qKF z4q7Fc>5O$^nF~(r8g0EWUy*pP3^Wux`Bd%#p1r~v*#ugWCztAvykVD)u3lbFGjvz) zHf!vFA16v)p_i%<1dW$1w}2?;D({r4Md`xJlM_#*2KPu2LF+kNfe<|uYgGe7A7x8u6B$T!8_xh*iVdl>>ktvw_c&bI?6|_8Ie)?|P;(_(Zc`=ibC zRHjm%hp5*DCH}+ zixRTE7_|MS5IA+_qV~Rii!4TfAv%*i>ZtZlk-v&)&Jlk<5Yv-IqBvK{BlPQ3Fr-Jw zBA;FCU(p-6*Ej)I=z9;TTTvlG&^pqjBxYTjLzmr>B=X}1V^ABO3vS0QG z$|ljZiZw$UjC#=GE|AI-hb83%E|Bg==U69fND#oct6E8xvOS~3!KLBeqn)UxUg`i? zxo~;v$^|7a@ zG_Ut0g2AGH345Q<{(`m+m&sOfy1?krOH_f%(iIR<>eoeP8+dkY9Yoco)h1X9K>B^h zT4@q+24=0j2)VJt3MUS~^PuRj38xPdH0lAKpY<5WybgVXtaFx>SuP5+-&W#+RE5sy zJoscVsFEh6Fo0iO;Y#zf_G6k?waLE5NrU;lnzn=!t(Vef4jnYV%P3t!KG>F0MxYwv zx+g`R$f#L0F^Zn@%sB02ioe~OdZ2fqJYy`g>|G!+wUxjz?1ZHJw@D zTP)^K*@@RLM~A)Hv*~NJoYy(I;%j90=hPW|PWvLuxTCMhY+kL)=&}Dq995~MGMHV< zw}=4#CF4 z@HO>aI`H%``vBuG>M42a2^UfKkt;UP9!vkpHyTOZyGIq2WX2?VkOIS$hR)?%G~^n~ zTK|cvP0$>ZgsV`LWWC&A$-jwX@(n@wu5XXCo5Iw3>dD#B9<{7AiTWEZPsrJee9+q$ zez{3S`i5B-;Hshf@id4YqGLYt@LQ4$@Ex^Cs-pVUzYI{=(N}Ft>dGPre8oCQ&zDx) zW9+s#lY}L6MDzL@@AEwTKwYC$*G^06`|ZB=W|{wz6Lu>>ncY~vXW8elZ_BM^GsT0+ z@^O4!=I^W-D>1>18gdEVDB^H0G~F1mE|}paTJcM{R&p4YoQ*pEg5eJkU+C#Tae)rI z?xc>^Y?g6&rbR|x@6KB-$`6mgY& zpRVCaDzo3ug>WkfKG2V>Rpv9CSJy4$c?OoJiKX8^e&3HgEtdS*E+DXN}G` z28u|&|J~A2HBrH~HCReuq)!Ey8~Q+v?Gzd)avh$oAWgEBkmBz^bH*dLrKE|6IMDnV zOqX&YH1&!Mw|tQ5A{xarAN^lN$->-2_urP$gQhB9=f^s>bZKj*XdN}t>~%uxlG(2| zN2Yya#YLOYnG(lD#zVdMe)Bp9DouO?YeL@orNngQL}lxeKgw=4;$F|k635+L=M&2| zsy4ZPH|yPkxgM3x4j#7URo_fqT-S{`AsH29POw~Cqp@?|Cim-i8m*EvxY7cAg{<~PU4sb zADOk->|Se$#FC!p@n?@E*QM}w^$v5YsVaTn-loK=)x8An>sj?W-^LBQgSm0en+nhp zG1WEp6E=a{2RDz#@$BoPvzfL{JmsXdHDPB%<|BOwIEzql>*-vpZbx1%?LAxd31GX=;*{!M`JumUfuaLt)pj7TfmSzT~}ONyu%z$cDbzRCdzue zfCggxI!JjXu0HM;sQe%$|1ec&QwCdKIw#f3HZ)E@WI3QPCa;_ayleO~b~`c3NQ?#} zUtna{*epUU%}#Ap9k|W2*w}|+7+@R~spB*kX$cFMtabj#ZCU`ts9oQV>sE-sA3)-~ zIuaXmi*naP0Cjo$o0CWInY(JP?Tm10=RuY+99#{9yN$uDbEEgY{@z1U4G|s|j-#ie zen24A9U>Navu9FTV^-%qpUFFScIIU;&zQP@)ESi_a!lE+y5g8QY78M0@boE#o?V1o zS{XIxd9RReaT8c$Zli{(&S78bJWc&vzYK~9^EIr41?LdC~V zr`l?je4K6U>|L!Nla_P_#4}%~nhj?Tdn_*c`rW%m98M2_AUhk_Kp_JJvWB#g0L4Q*M#?{) zsdy!=h_R&4HR2$P_9`D0L6@nGoi_v5#f`;@Ihzw_Jpuli^=3VyO{-?N?AOt3*H{w^ z>AGNX9FyIOWuvoX^`rI%kG-b*(BymA(!4}_y|0|8gMRt@nkuH0hql*ndB@J2z}<+D zjF930I}?*I`?f)#QFY~v*C!6PTP4s)Mi`QZ59J7}pD8y0ml->er#YXzxBFTL(8+D@ z(p;$FF(|xB;Z)<-751>79skfclN=f_&{_h1)-*14qbc?H%>9k8J-^V&fxo*RDZHB( z1h3BTA3D^k$8zwulN>MKGaCHd?`U-654r-LJ#+l5^RF~roGF2GoP!;xa(LVq<|jST zmlZq6o2};dZ(3482!61a?y31#XRoy{vZ5P0?S{q%x+b+#EfNy*h{WRrey`?ewtXDz zCV=P~JMTEF_@{S*j-R5Ani89iuB#e*yO#EDJp9L9A8fZDJN)VjwN|u*45tTjAyw>0 ztfyM)=pW?>8ZoQT4WddviLacOdtMo+XMchn9oM4@zv}vbh=u$YA%aQ&Uw{bVt70hz zO-#O5l~Nz4FtJ`0{-Ec=WP1fi_SP{e168Dx^T3)P%7~B?TU6{X=MhoLT0KSkGo;V9ESMO#4#LWhU&_kzu` zYm66Z+%b%L2TL)()+0NGEDi)sn4-biy&PuEHUFI<%Ow_;X`cG<2O~G7t`~2!?6cYd zK`iFS;G|yW&4$kkLabQ?6PP5yQN0YCb;y_@iy=W3X71a1d11OPrvG&yz0^HSBn%f41iA(rP^PN zG$OUO$gtsJ4$#%=vEiu>@YEVs;Ia;|)PgJU><2_@jU8|)1{iDg9q3qXwVCgSG+!GCC)FJQ3`IYg5Y`vaI;@)wC;^gHN`iK-(O20ryKB~qfBB(e@C z?WkhVha}P|*A~fXvoI$?Raxj?MQG9`hBM$t0O*p27%(EV3lhs1$Rj`nNe2vQ5nASn zX$-^>K=Y(U1{~$$A{A|RWP(uzISEfhi_qChOn8R`KKdS;-QyR2Aq86S*Tk@o=QatErUG&nA)$S6FKBaO%gisie@4CK3(9 zkUA(1Z@S@>J*B%DL%SS46EF=slvp{cm|;fSF1^7J{D0DF#oFlrj{kS+jI|gz1F?%~ z0D(VNrV9KALi^vij0w;K~IE^Dmhl!?|&N+A+T#=Qyfg)9l5l;t9zxNU!eqI zV@*gN8L`~0!$_B6ghp7Jr^_)W4_msX%alONEg|U&CCP`DKFB{v&~!`dbb0IKB}>nA zSr9WasWu9+Y^)PmnswFOgSJ}6Aj2y474q{e_DPMSB9_^89Qw@_iu0_;N!O!jmO=jo zul-LDZZ9cjl+iM&j(MrUK|z2uGiiL3*fOe)VX4kRzKO*ksd6-DsBTx-wM$Gl=i_85 z$q?qQ{H3{fLThD&#OFZJ)Te2u$kOrV^@sk^L~z`G1zqi4CC$|0@a z$TgLid{=>#RvD8vA*%{uaU_p?Yk>@{=364V5ItHgB_f^>!+cF9B9;(vzLqVKNQkkS zCMgkPh`yPY9+5zZ(WWMV1xAR7B$ux&(9o*3)f6J)kS{Dyp02RfYG7F=JdMPWFD+1= zt~}8C!*Wmf6!~%>ueb6yQ%5B2K#jQ^H;h#0FX471`aqGn3U_6i))>nk;Z-E+K!Le( zZH1mz1M`IR^HnnHhzv4-si$AS225cdO(s$a&@Lln9#DzUhC!Kk zRYJ7O0n7-M7;X3*^R-IQOc@{ZxJuLvtbzGdCG7uF2$?B&XQoJ2AF;NIsskdmOuC*- zqRB=hPF8Vsx=34+E)LV1WT#@qLm&=wSn`Jvt=+VlQc7J7rjN-wBX+ydt{~ykEL{#J zl4R8ptKCRfU~7q?u7s8~+!f#ptYZ0;T+O0M1d9X=!0>_PFg0KmOcNa-ldQx5QU+)z zt1;*@15}b#ia~Y&aI!|RE*U@}S=kDt4*(^rTj>e|l3^^sMwkK+4%0!v$pIS4s)#a( zHa{E;sDuds+hMYZ@*`~zxDTKcCIRe*NFk3X;bJV+5GDQ!a;+HHAV30X9G2lP&(Kzf zTLbE0!iWxtbYmG7o&Zc8Qomf#1apM6FXtmG5andf zAgGQskg-pLwZb#tZTSNN{hL?8Xm0%Krx`2yn8+)@T5wn-9vcy`AggD)h5PQA87P6x(33$a$gcQ|^#9zO@W$_3DyOv(;o%SddlnxIsAC43@nDUttD!%qQNe z^eHt+3EwPoSWscD!CS3A#Rf^?8_ggZhgqxFS9uo<&+pmZEgL%K2$&e;vCk3PMKv-k zYdfYl85-mh*@l16ibmT?aOQlKhb?O( za2etl(*!aoty~bW%?79b-(nyS4Gz;cwqG0lmif>C5yLqHWD@_ZXyHuD(is0frP+ zAB(tWw{aLXS1B&Qk7uus(cFVXnkNdcjzZo0+VIxOoYeMbV~!c!liHZq8k`ga;hD4J z$HeYYNFiOFlYBGGV7Br&=b-Ld*r!WEH@9?hf#d-5TK?YLFQN-zQo^~Qdw}YbZ(Kxj z%-~aQT#|ZB1tb5GwQ)yhfaE$F!xLzc9%rGFQo$yL)rf_|<7;f6 z()D8JIl;rsYxa8(QcGvFk$*ntb=ZC_esAoT+hrn|_kOPZnz~b0@_$r7d~*E_CG&IV zJ{^``^WN(}W%(O-=b6lL9;RNi-UFX9{Ehy}=|Hf_&6_f4C|DkEi#6w7-f>j*K zBiowaOT+aRn=WXPhFb}nCulW~iwT=0XgQDD7F#4}-IR+In=xp~lv@v5AZYCu7l0gw zg$N~=t<0a$bhhOZ!sd`I%paa|K#FI{m$6Pmab!#LN2eT-E?M$>tf$bIeR(~Ozezhn zY5Qu-`ne#aynnH_L(%(+%tp8z)40dT_pq)){|~zEu{+K;T>O4)+nU(6ZMHEdwryLD zZT;HDYGWsjn>3l&Xc|mB@qhNCeXqUN{RZaIwXT^t&*S)h5JxJlrkY(0dA^aYVEl_? zigU3}%XFU+I3~SKMI4uf%B&zmAiDr`*|?i^ddn)v{#*!)1Uc2MYT{G?i!?qJFs^fuzi3Y(q)Eb*syFU6Z)14ss$a@jkD&eg-F#Ev!-kWjnwtCW^IH-)N^WC^*S2HKRH9&l0{_d znpxA!4oszxFEYz@TTLX3$y4v-u zSvB$-6nvc(I5ze!`LVre=~rlWtmvKfV-2c<3Un20%u*g}LKWJLP3ud6$AXtx_+y#2 zjdPyh|K&0s3MOXpp+isEf$QSdr2_K>C$s3s3b$VZ*Ls2G0!;<0vsyE2xfJL-U!14f z4o%S5V{_@$T53jhomkqkO<39Ck{9S&6FBgam+9IjIna`SSGLx3kR`8Fww-byB`*S7 z^Err;{{Y)S99U{KHNSLS8(Nl)&)DyCk&>78TltL(*=KW!)V|it{&s^dJQ)9E-^!&@ zYpR+5?M~W^VZ2F{KITl?ieaM2UX{y}JkRS^(5!A;%f1AC0>9bL@wyu|vm58KPv;U& z|7Sbf>J|u`>O3}lS}}Dl71%Mh;MF#N7x^vBKj|H>F8`gt9T>o%^PorWggU03{^<2#nt zZ7(_>41>Riu7v%I@Lf3T4Ekmcg>yloK+29ZqtF%4e=)wlMQuwu=M3$?d#?oki}GFk zX=B#eXlU@=dByi1zH=wX#CX@y`i;v|C!9&e_ep&hww9drUoJ5* zNrk2*+rE!MagVl+4N;epPEM1q?-NjSqwRhJ$t7B_Yu5U9J_G&b%W6* ztCMA;-E~~hxu|V+gUBTrnxkxW9qV+0e$YlKsP#g`umAr?xIpx)FtZ`vVDM`&v&-8c z^s6zmHP{gF`)+2xx`F9ewPho;!QuCH%dTsK@QtJ|`5Zx>oC zy5M-whej#4pf&q;|FxbAi3juGvc4TjOX~KX3#y(~NgL|Ubdb`8wO?82rleI_yVhFW z1>b|wOL6FSzokVx*IM2M+r#IV!qBbv(b9`aPcY?=G;yW=szIGoW-kIURf?K4UZo#Z z!?YeKlyvbRT2Ez4-uTT54`xc%`1J};2TIZSZ7UBFO2+s#D^Ej8!T7B`k58iiQT$0G zQ~F*tr*q}tAxz1sR8=*#c<$iYF1k)}k%pyIS2YdIH+$ZRK2yArt*2Vf4L!e!u2B3-V@iuV%RQ?&dtTpL zZ~gh_xfp6}%E|f5WwotAsbC*Jk)9HXIdl6m??I7MA2A8dCU20)K80_Z$OLp1hy{vP z4E@+Q^==d^6ZSU!{}>a$OzHCGY`qH1i1KD_{Y6Z38d(iGzOH;e`OjaVyO=ZYf3=B= zMl6H$ud9Eq$UJ_SQRS`vuQS22+U`6q@UPH&wui_}jnjR}SkK&B#rtU~sfk|9Ri0d-1=xj}MPnkD8A;_vw%G*mgb1n@3a?N{2c@%T%co`wh4J0j5xU zGs*r>+i~Fj)WdNQdM|WEu4$0%E~(F}Y5424aW6CgW^&{kXwrMJ&FS9Mwe#;GsMq|e z`G0tW`#`3?rESuyUjLz?3SKOhW7&tQ<_#xI4LV+Kr6R`k;Eh`mlPdde3_Q zNr^*BV+t}dDymYlQYv!_b1HMnFkVw`T~S@WR)~-DZOU9KXIf*bBS{ znrOEk=UK{kosrb%)0bM|hPD?{J5rBc&_c%g(aHvy?h-?E%ZBT2NJA9M{@CAse*Q0H zw%^Sny=mv<0rM#?G#Sb+CUFIe+ZRho@@$J#lo%D@&JoRM%*=AIi*x*=NKQ>IQPWdf zV-sQ9&Z+Eh3BxLzpFcXezI{S`l?}rk$hR2gI?B7Aej*HkwU{Kn9t%OW_?35k6oLp{ zO}qXBT|1jzy}l2D- zZM{;3aSf#J%v0}T-)x0o?@UwgW8W%4s$O~e)1~IhcC~JxTWqhS@9g1eQu7wOTsL`; z=~u#cRw$D&H$UGn*EkjSwJZy={#ba?2=rbR#ulQVR7r>YyED5H8!_s1q4$QA>o226+X^n7$^%c6vy-^Wh~ z7A=n&%AfVMo%kWfzO_Z(rSF_AP7OQ}yS+2qKRl#oI-=ZxkmJ`E5*{O)ui9R5!-(fA z%pH}*Vx&oAJHzHZT+n1`G~sX)#@e|A0SNQrOYK~GnV3T;o1Rk3G6OU-=p^l#5i-4n zK{)t73fj4Z1;nKqY`B)1VaY@0_tS+bB9!G~k!a1O5dnS|ui3aMmZKIZYJn_;;BDv# zLIHJ^toc?eZswfyIhtq&zR6~H!^M!4_T&6J?c?f85vd)I=#~6_9I?gP)A2a-^5t^I z$^LvyU2Qx~W-(?rU;g)j;M$e}fOUOyUDJ`VbSrKPc_NA&eI1>4%ZA>LbV7%a+VqEvUl^f7!bfkFuBXCJK1KsMYq+$!|#MAxBqbGtfibNJDtNED)8vkvo^? zpMBX9-rE2MMkahX9W?IFnwWASh>OH3hy0c6dR+{E)=DM2oS^Y^l#d~&{WV^F(Gr6w zPjnq^|zizL0{$&*vkzyb+n->lXgNM7; zq!a&K@x*`%#<3i;$5+RZ&XRlp zyUm$}AfTgdh(T{8uBWk=qgJ9Z-HVNkiphT9gQs!!qxt9uI%y*|R&SDk`R%Fx^AlGi!FHvb@Y_7`Oe@6o-`;!@fJ(`YeV{K2MCBuylYb1dljajdx1cpN50rWu z;X_f3^F+QJ*^$sYP(#7+Xy#(QlxM}tOhJ>Ol6IjRZ@<`%qwnK)8!XquC8@U1{<$NT zK&fTF@kyH|Dq-?zi%)@u=h_6Pfn;=#Z}P{E#8PmPM*{O1vq%zBewH)5x>J?&U?%SaNrmoQF}H2Dd$V;l0I1|2z$!8Q4ne zSXLNl57UCKp?GF;Am1$f47i6I!hDs^NZ_xVne6DDP*31F=M}0%q30UWvyg~y^bKSrH?uTWk zqdU|NYc0c0Zgyp4{Ln~l4H7@tDYZ4Q)ERj4KD*N& zGl(dGVet4D9w{Ea z!!z?Ff7_WikP=GWB0w+vev2KLt8y(Xh84e+IA2drF`LL|>Wd_vIER@Mapf|Xu# z%(CzKQ2`(xvTO=Z`(#?y1W4OQ+>3)0LM8Y0by^9L4@8ZHi*jq~4q)qgxQ!uOeqa&A z5_(`c+Y$&XuonkF^$!FB;T#bsy19Ko6kw{E5@${33JC zH_tC|%3+W5{5Q&at>pJ3LsT3H&&vh>$VuMA++vVCm!m~&%<)5cgI@7jB3uF>_^PgI z=;U!z`)QsRrz2ePf(T4j#w4E(; zXXtuRLA%7i^4p8=Dl#S+ji1a}h1eJ%r~MHhZ(+YtOwwGX5yjJ$1CH!x){qB$*{ID> zp6=QVa6TBA#qPsR1YBZpxZa?z15Z6Qxq5Qa1mv(#lx4Amlo-rz`ca*f)9*I)cn?|< zxBj1k3~}~X#vvcNHQU(#yb zM&A0Z^q84Y@@Reojd&xBxd#+S$%>m{>n@;-HYvSrCvn{adIPcX)z)GJo7_I@)6Q|@ z*l%kn$n*Ucj0HYYDbSQ%dK*|sS69=98(8x+#g5g~J|SK@hcjTbU0YV}3_)KJNT1dP z2I}T5x514&SIfS-G`y!6qGY%&#HqhZW#6xco*q4AwH@o;I>@!_mUae+?$^UDMrpp+ z-wDU={4N1q8_cJmmjWtH%8K5OrVpDvoD-JXUbx|7CH$uQ_?;v7Py>N$rBSw zhGa-a3!0V&Y=18<7LkOjnXR|8alHNbxV+vb8dX7vN+wI~>*RTlZR3f@u{T%Ygsg^h zL+V1x5}t;x?&+2bVD{DGxasLgik|DTC$b*_?|*EfEk1oRZuM+RC%p6N=rM4@R(@5o z;3vzBlR>Cisd);GmT~J37e7@PSj9#Dcz!JRe_43J3_~Cw$~=3#(s~hoA{cJD&$%n% zqM?X?dks&pD%W-tMnIEG!jyXVX|0n~?0aut+y3|gh)q{#4G%dMkms^vy?j7&8Hsl+ z5~6$M(ds%Y1hqdKPA@UC6urMG$%m0ieYL15TWXqZns3^fzYyB%Iy_0ofVwI$QpXkRB;DLkmd#%U1Cws`qyU6?4yKH#uz{<|P#mUau&e7F1^$qoH*|i1uW0pEK zNs*z-T%j*92>BlN46tkm%~_c*<1X_o*VS|B7-$>lNR@}lZUr$m?CZ!AU@9oJXv3WG z{0H+nhE)L-dPj>vpb8$aHYCRXu`khu`0bvt3pbtOr}i9LN+}59X6CaHT1Z_ zu9CXKdvVNx);M;hf&jYf<{s+?UK zhXx}9ff+sp_83M@+E-dm+FIIQ`f3RSApkB8UJ;%Vz83x%E*DN8{uM47z6u^6&H`l< z2NJZq5qX95&fwv^+h`AWmN+;M&xH_18Ni1_u8d`aWQRB$`8xPTTBlrafy@y96vGAU z9OE4I4gm{M6NwYKQSvP*Itmz#7iEqSBygH8bA|ZEa5@krhM!IZg>^a67vDayVK@jcXzYJ=*%NQKl1lk!K?igxa5?v}fRGnB7Sqj>FS`OL^ z+AUgm+7VhkS~XfW+JV?|SvlD%*(}))oM+=xlZp@A=dOkD7&q)K{GUR14ySb$#Ij8> zVK3_BZ}z8s+CXNGY)Z7);~1j2+&KN1B-xsZU)mb=EX!nPDBsatQG8J+sYd~qVfXTyaf&m)`j{EH3EKbmXolN>Aq z0s_cYhu*J@eQwO-S#k}#oi86$DYKT#OB53#U~Hj?j?Enc1v!Ond4t#xMR3u zxnomfP@|S%lwn$+S)hmGT~g!X;F3iqCZq>&?ms2u(FrQPvh0^6_ygW4uFG(#6{;0o z(m+HlVnD6Yp#%UfQ|a-i(Sn3F+&n;kDui}_!anHqd%^~;KL7={8buL7Xx~a+MqVL? zeHh0oy*}L`#Ub%D6)X>-V+0TZ^Z_e$uXH+@Lz#P-Ety>M{bWyehtzr@+I4NiG1FQW)Wr#-V@aD!^T78;WP)0j4sSl@)Ci>4oqT5TNu~I^Y(d zMu(XBMcKAm@HYt*IaAPAT#@va4yHo}^kw2_W@WZzvSn(~z46fIbE^|D;%21;qTVtd zbIge4V{?DRt|dK@v>|&F+_D_Q9kb0~=ac7)<@2kH#>H7#wQ_SaQtQRAj4AW*rXW>K z(Ub4Tan&Jfs<-jX8`65iycV?ca%Q`G|Xks%NdgKOd z1xAnvA6u!1se7tBLZL0(s?@~^L*Rebg+ODVC@=_!1dIcQ@aXZh@!awx^JoEaTHfb< zW_HYGVn8R>7D=HT4;P?E;0MJ^7ieHYQi5dsGIfhOnC8Lb{2HVMluCZ1dyqZP0X11S zjlGk+sIdz^gh4Mu?q_;t(l`=0(g{adfgLTihR)vror0aToeZ6Pe%yY-e!PC1ev*Dq z1@?()=;_HxYAL~-mo?o|v~Y>{^lSIA52aU&eW{d>;MZeF$#w7n_84{Q2mSjkr0tscU?Ay(=iTnL>H6^? z@p{?bYTSNID(Ph6WFl`u?ZEf?^EKwR>oxrK!8Q8zz!)+E0+T$wJi{pCC<8rHZw44B zt2!JPlZU^p4AC17Mx~~Im9X+2PK((>oxqb+6E5<}TpA|0wz!a}=|K2Zuj`SA}dhe|04N;|CK!cry;V&)zEx|3xAd419kP>79;u3;Ynm#{vvUhup2$2oJ?I*E_;u_1_ zo4zItfHhAb-Wv-*HBZXhyAOchP7vCg4M5*c>e{;qK<>wbdavWFyUy3Xkf0Fw?L_}Q z@@oxgI zcn@KZeve>}e-D4SE@mF348NbInAQ{!q6LZcC;O@ji@;U2^v zDE!FOhIFBhp#?7gIYB3iNeahkuI1Mj0g$naP$$+#^k>T)kQhkj8}bXxMx?JLTT9S4 zEU_5M@mlLXL!nOWavBFzpe(M8$^tHZjLW$-J9Sa??8q^Y z+KL)T%Z`n1z1mDeH29@i{+z;(NGw`o%x&Clh-#vAv~;+1JS_bUHkk4S@W6CF4}z$(0*ji6U&sL5zQd=>(HP5Gmraj zq+%GQXvn)YwdcmJoyRyUO?S0nanI{6^x{-!YYTr&AE=7FL|RX0yD zwoqEppvHc!$5QteNCH#^vIc#6wRZ{Po#UQ6h)#a_y{j(t2IHF1NN0W$QhJ}?Q>F~C_4Xz~ z+GT2uI;e1N#VDq@3?(Y-k)J#A`-0Uh-Y)JY*%cAYe_(%eof6iW=_P#IT9@&!hS+m# zROSZ3mYI(jX!E`Gsk-VA=!R-gaPO!u+LltJQe=nTU54AkVn*Qq{P~iwG2@xpY?9D( zY!gs`cS$goHTNrKO8iF7i&`y*;pkVl*X3%8-km!MS!2@I<82yyJ*EWJ#we4)AJSr4 zOWYls)FM>lsNd3@cd>Yb+bI49HO4v6Nm(wkC|<6l;C@LoDcF`HWr(DZ8P~X>UmTGb zskJfe{x7BfTVG0YJ9}Z8;r#lHm&e!NFGh8L>CRTZk=5opoP_1tLerv>|E2a2{&yhy zF?3wAY^>(9H+gsJCfOyx%lFy3wX_=(j@Pl6^jBCM|JPXW zs9}37obd84ubytoO*=PI=yl_i>xrM2arXv%e2$1I1&Y-Cd@=+)*y=|qnWyh##0Pdi`S(2xOlAM8$z?Kxg~u2 z(qP@7nPB`KY4f<`@GG1!Jb^vCwx;(y+z%TFoZCe=LRYGkpg%l zp@@)ija{NQBAB*zBS#lLF!~_1K$bhaKi|?{wwazmm=^;YOpwwrT;L(Apz>?lJ2O18 z<=PG}SemCU^*aBPG#$)iq+WLLpbt@I<6Ql|I83+EUl`ngNk&-26agosSPawJ#RfMb z!b$5iJx3Ty;lXMc^<6{8$X_tR*_^Bb0C-DS9Q%AMSOh#|OZXk$wg~lI3YJtjcm^ZW z2;>MDm}NT--IUP+cC0s~7pi-n3``8Wi{qO}234%)vTXF%>;|CgywlMkZ0g9-c}RH% z{Y;2F7mRo@UU`NN%5r9M1l?~*RTgceYv)>^p59t zO7cO5h~uD09i4kdShWRC&=T8IFf=85zE{!C?lYWjB|I~|0c=*mA8EN2==gR6>{*2I zuEDsi6llJW@e?8^%y28&qLAB}Xf4qf}T#2jsUkUie z&is0DAYeuS$njW;@#kr?( z#04SE)wm-*{WTCS8X~l$GDLJ)zSs?U4)Z{U@J!a>^F2ld55Ap;?z(|dbYvw4L8$wn zp23N~N`o9mfp>aPf*Z!ch6-V#U}Dhp*~)|F7XOzg7$JZ_9Fq!eK7x3#ZMSY$0Zent z+g@lY^TKBpjbKRz9j-q$yauS@xw+ zP{UMNY65PM!pZg73<<@UMV%Ex))G4&SrU#qDt*X>JNb*^46L_|WIpybkT_!h&h4x& z+ubM@nv#Ueje=hey1AYKHe)R7b!o@alm+E~#_DX5r%9uQ8QVP}KxIbk(jB0g`kxA< zttDbT+#ecR@0dnJ-*P1W2qxa%zfztlyR2rKJy|?5WEI;p1kFFPjccj+BN=N++am-$ z5*(Gj#dH@J9eT&M9nEn7%S17&8Raq>muKG16SBRQ?{`>BmXXAHKU&+1P>}U zW1l1T47oe3lB^cIl_&A!YEC-XV@AQ3lix*{d!T$f!H6c>M}erUz<#0ZmS5#n8p>4H zsl~qIPH*#_DHA!^ML@wNY*%UK3`HwO@*Gvs#SV_+!+xJEh94+@{UUa(2gwGi>LS z3w`Ry9U1RDca#?)Oqu8&LBr}9aQV3kB9ohp?}Y&P+x$pkKEEnv>EoHAF>^cH!NiH<%YKxIXORy?Wr z?Dlz@h#Nv5Dp7P?L;G^B3U4TX3QCjkORU7%A_%-Vt<{ihK#2uv%=5y%- ze0xa{C7dJgaXM63&syj-o&+YR-ZY4!Kt?TmB9oFAZ%^wz=Yov~`MC5Mwsty^R+wdLSQV zgmis$XFShH|D53@sMv~04yOb|&iT;TPvjbz3>$hv8KpmkU4eh@b^N(A)h`;=?npYM zJ~1>u7x_pIaRH+NhczlGv918V8jo43Eiy3c6Vhktt!mwR3OrdjWrRdpfu3w}9NYMIoFW}W zm4-jsn4hr!$aH9nn!pOkY1YGf$)swdAI>?&7M2Uv$1h{~%2H|*FJnHDn4BVrmj1|M zt-7q_os`HZJ}bYI{ZU|HcPG=+Fjh_Z4e7>ror@w=QHo#9OD^3odI9DzQqeGAJe?nT zWNK4AZ01H*ZbA6X=9rlu4pUBij^BsP8-px;N41Y0;vhr`Q2m2Z6wgsyH z68Mp)m`_^$>}E5G%3W5>oe=xPGHytJuTE)6`OE(tb~g5n z!GZK=ZVTM3{l!tE%-014UDjU0Z6x=|fnAipV!Ib$JBW1vQ!n~9`c>TH;EN^P;>0?3 zbhgNTOA0)gx2MD#XL~{d;h(V5p3)|on}};}C`P~BoE1k6b-cF-a{W%rxY0p@mnz{A zLI~bF1y%L6k?o-#*liAR#SwzvUHr=OO8SaeJS7QV7CR7=J{Z5}4JQ4CrAc@%%#`Pw z<4r}h2-PD8iT4_2N>kXl(+y|3F)X6ybxmj7cbj&`AJ(Bd@t}$A`Pt9{0qG`5{#2Q_lq}lfCFW40R(6DZ%m-z0x5t1{tW_sI`Ly z9!N7Wq+nShbfOGwT^UiBEGyAhwqALg5ef(yTNx`r*NU?k5q%1A=B1j8g`XrQ5Db8J zWulo3ac1I)6W2VjUU9-Of|+|-N2`lQO(CBYRf62dUAsv*AKT@ulN5I9{JHTNuZeeUgT)5RQOCI*fEq;-2Q8i$}pKwG&Yz5N=WQOQMwdi{+ zUT!~Qs$)`{VLOKfL2w&Z5-xF+9t_{2tqM`yfK-)eyHzPf0SS8WtHZt?e47ZjyfyU1 zA*Eh1pRlJ}i#_y$0H%mSgWtg-ssbTQ&i?wyoA6RLoObuaW*%ajNd6;76u2+Yn!;tw zi&cWI3`L}EHEjS1XL*p7kT^>EFnlw%Iiu#_frq3sS!s*}w|YU;$*xfV6*T<4s}O)n zb^L8`8hmo68&}jVYZ}|}_j8H>=v3p#ilr#> z^)`Z>#Bv@FbdvS*(5N7=Sml^%n{(TI+ihFY98sw-EN`i-rOZEpskV%fdYAG<Gfv=5Qx(%ST43YJt946-tMbje7w~O9;3Uuj%z4v) zSaP(;C%N|>PwP7W(D9gOTV)&9NlZ>vYUD*STA^P)NLtm_^f+{+XZS#;|H!o#w_)RN z*-b!uQ-F|z#jX0^yZ}UYK_mJ8U(;$ry6HT%hK(uCy*)Fs$8PJ7z5jZz0{SQf{t0>) z=*g)ay!|@hrP6h~IrQOru^gZ86`Mg^cP&kM$G;-p#wL}z(Pi;h_76}n5?HSKX3~o_ zuEQGXSq4G9MYa%^sj;1wbb6YQtmp=6psdUg15Qo8AG*flUsl(V%lES|2=ZD0_ku@@X_{T=lZkJhn zTZe2CJ|LcV^gP-n0r~c{_?FbfAuHvImn#d>8{f-j6xi!xj>VPwl>aEgmG_unFv@m( zVVQT+`5P^uEMq$0Nt{+|@at|Ov_(#DHj{N*El!#~q<2Fg&Z#xVcCuC75J*;OLS(WoZI^QLGMT3V@2rP*n4qR@OOdR=&M5C z+jI3xW&LqKCWGe(#z*?q$X~k+^<&i04-9$Ia#aWWFImW6T&D!C$60#He?uBzNH?h3 z6uTI)7_gDCnG6EgLtQffVf01>{0e8$KNc1bvjT=UaKuZ+bHy`FiyX8hqcj=wVzn5k zqwK%bW!Yz)d`Tai(6Jr8a>fuH!K`2?y`6I0o}StqtT}ug{JAi8_`EYl76j-oII6RbAb*GkS5sxUPA2L!)TC6<6`6*!3`_J&5R+ey<#4145!qY z0c1-1Bbg?VvzfRF^))!9)TD{xND2ugBJlGOR^6~v1le>U%@nzS9|?GYFrp#6@{xzM z-xsn+&F5dX1;(McP|EiN~5eqBLiM4iLNvp5VH@O z6wu)GhJxskL^Bx=iTKG(ahPGT5y>(V-xuGd+p*S;9OcJpUHkXmF?Sux&;xN$Mt;Z$ z(WS*_9Brd2Qxqh)s6jG4TS$0OjrOkT&LXg3GwtY8T(HavW*~Kp;!qGA#=~kbQaS-2 zgASLTBovKP?qL}95H*2DltiHx(3YuB&xDsWr&LSlo#Ch~*$!ibH7h3=Wo>L}L*(^q zDe7BvmU6g*KYh}SJfmE4CU?Sf9$i)@!=$MVS}TrGjEf?cqCL&O%+$#SWkWE8Cd*58@MXd zD)NXW(rEytOf!_Ub9qhL_{_ISDjO~@go0er1P8caI{abejIBw@(L$I#xk<-EUN58y zT)ZDKrt(-yjB)b-dt3~w46&B4D9P10Xo(6p`zJVXYj~mvJ4yjIh2cN_DFQIo$bP16 z%26Qzb|)tZ4;j6PjgHvvw-leB9+cc^{WF{=F-KvI>C!XfQT~NT+{rm-C{5xYvhY$+ zCLv|eo?80A{GyFsh;+ab?Y2dDp3FsvrI;&-Apo>$!t* z)h$m|nEao1>Q#(8e!a{e2il;8BJ#z`A{mMQ>g<^GOtUdUh6p*EB>M=RwCd!Xa4cqa)OMhJ`*#440NlC$u_;s=J zG;#_)M8JWgFe!L1TqrafBJ<*N&7Hkt$o-dAEHeb zNl*EG(AKF1CpPx=_ zdM`ZHBNxXT1C#N(}nQANfzsj#PTmkgWfOcFCw47(5a-~IShmsH8AfnW#UzJP?$)~{d<(`#b zG&3B@7%G{2SPY4DDy8{~+daNh#JlEjduFNMX-RlYiz5#7(BJ>5xm_-3blhp2NoI1Z zFO;-+X2r~CoAOOTja3N0Dp=atWvT#G>_PBHi(A~Wn&^@WG8r;WwJEO4inM3$*P(q_rN8Xx|PdR+VPVbYO zDb6JikxFPu1u0B$rp7guR9l8XG%oal1msbv(48t8#&LR(pmrilv#EM4X{8rCw)YEl zOpT!oJ2l(ray}8;Fd8>EH-9HlDg%E$RS*7()d)A25L%lh<4go851)R@+Pts zFBQlu$WlyPoTM>?BwJ^aMUsTbgHRNwk)|H01|~O;ffgZ`AiQs)o!qSiRei8KoB@Qr!))%P=<~@jUoGN^_nCk}y(m)uevtF- zDZZ*MM_@nX>{fw+n^8sAGS`*OlHcxA$JIR9%;PpZdb-H$fky#|=CQ=Ge!X}*cofm; zW6Ar$avcGf=JE4roLKag_mFpGKpG>NhdU@%YmjL_2UqBFun6qgjsLhi3!&dke({i^ z8jKcBkPAS;J`ls_5sy;x#0WvVN*)QI(AwpIeCj1EOVJw25{{~Z@P~-@3kx!e!l#cO zUKc>v#hFAA_>t@myum*L{5&Kju`MFFM%O@eAv~8F97B2g)8NdZqizX7tj@r%eQe;* z5LJq##@L(1KM?%@%zkunQj_GSk=28f-2-qf3dwdtkL-)A33wL5_^RR}B)mF+zLVx$ zVG9-vLGH)jNxB+;+)V}u3-h!R@5FcQWtCGdMh77FQcFcp4`Cm~gRLPty_8as)Ds)~ z4-1W>Dj~ji1kg#f^s=$}J$o=ZMBx=LjMM_t8sbYb;fl*W{(?q_&_Cx-LU)}Ci&l4S z^nL(K)TN|_;L;xnJ~i}hryV?ak(N!b}G+kL#&Yw3Z&_ew3MdkkxNZu4c3NK-)fIWhKV zM=gQOL%S5EayIzhQ7-!q9weFnR-fV^n=*m2vI(fx0SL5oit#=R%0S^vd}s8A115M5 z9Tr*u`zT9UtpaW?zEa;Kor9^2`U6X%I9=IfT0;VytVZzzBbmg^_#a&lzTL3ADS9r>s)Rm} zfAO;8NiA_-nqGR$Rh~gSP8cpHa5a!P;8%hdklmB(lT3ExXM|JthqXkpey4q&WuiWW zE!1V4#sn@?C4QU^d5OiH9c7FMt(#Ph6adAja#K-+4DlX4Bc6slACC}EA{eKODLN(( zj2i~DrIR*2z>1BMPpHTGo+vxIpq#I*L^rBl*4e?+D&CEpg13Z{4~FcIV$dm4V0GYp zA8kIhsYLx2%{W?zr!Fj`PWxrFYLFdh6Qci3AZT1DD_`?l3az}JjjE5=otq^lPkg=- zJ3SHZZ6e~RDNryHFMfZbk1f%5)Cow4n>rhZ9;KGhF^UEhX~{QKJx3JCWLjtkF#;9O zdD}~c2{zOzl2?^haLE%=M~Q$+9tJ*4EzuxVW|Ie8yn`pA333F zR1%EinJ+Bcjz5vm({eC+Fq#7)-KY1A5=JXwoWrY2NFEj07jH?1W=iNd@q6}5x?`}P zTi=9R=q@932N7Dtagzz-k9eFNlH(a1qUg23$_W6HD7r>Gg9L&h{MCeiqoSusp2^)R z6!#~wS2%iAU5P!Tzq+Md&^7@N^i(+5iAtl^Kp+@Z=NLIuq(AJC14I#^ib2m}NSQUZ zRRMe0QqHhIardl0_L5cWfg8fb4gC_}jp@x7rX*1%k`XyvT=*g_Sb)ENC=`#JIWjqP zNb;eqtNQ#3P-ZZ~L!lQ8u&JJBVq~!4Wu=SHl9PceJeUU%+HkTkO}|Z#JlSxYsuN{8 z=_*NLls*zR-Ofg&8%EFRhN<3o>4W0 zj7x4L+d1(V{j!Hn#r8z6qZ-%S;6UogG?DQKYAi8yav3V5F*r|p{U#wp$ctlDL0*IY zQYKenGSWJmvR-&M#5YoVKu^ENM#qfG-b1zF6G`;R!k-fTtq@h6SB!yu8>}q#7a(Vo z{|Oo1CMM+zj~A~IPVAI$0gcI|z89KEjyMdXN{sxJG7q-bG8wDW46MtutsahkgLDoOzhJkRy zs}1=^CZUX?DUL%$-VL#hp43gvOB%Dpy$Z8K&EM!3jE@ zc;u-EOY*>kVsddjN%dSL&0(dErbn{59FYA-2H~fJ~;&P4gb6-v-JFahS zm_s6al_fPS4{dMoe$Yo{Mbk#nPxTTdRjcIMOct~tFAnGa;3*X+`juFxoFoj-bLD>s_(YN4M}&2XAQxgnm7d9po$3Vl9-T@O5fL&e)+sG3ZZeL5n$9_tSoPB zpg0TiVE%(gB8yp-t57?J95WxQkeyGanOd%>dj!kCP*Zpi7T{niJHYZJt|0TC|7r>HHNU%eB zr?Xo(udm` zjFwH&&6M-JQKI%~2(&qgLO* z;|}ETO<32^h)fDS3Eik~+kRNkJX0?h|C#=iY)7@^x)f|b@u*kSAhE3DB-RmaOuVIb zQ5iRkT(4Hju`Kgn>dn}CLoxvzaM|iim{8rc{6D-~JPurHl00J?mwo zGduxt!FlhbpXwc>^Eylo2edj4GGGFiq@`uwGmIRVt9a$4s!FS*pR%dRrZwlN#lPT< zSQR)TnshM6^wvBEa!r=y&KNr+0%Al-%8;bWFZH?_#Ik<}r~j0pOy8_24XTapnxNKs zJ6&q2TTWcq-h~Oua;MoM3zsb^r_!_KSgKrva2w02{gvUTz+aR*bum`Nu8Cj5fITgje zq*OCl5H!YTuSy15pL+c)ZLD-YRbAIKSgMQaXqN;&5=UaT6Z!m3ZI3o5cCMUQJcbnH zCx_wvKLE@?Gr#uJ9-8+y(>d@YdICL>2I&CpqrJ47u8G9xd2ob|(m@)c!*qzIp!}8= zx|K%h9B2xS&@dgRr_txUQ|ak+lAcU2k0fYMY&Knxm_^T|7twR*C3JIQIX#b_PcNpI z(M##M^a6S#y^3BzucX(}>**Y@$hVfBL9d}V(5vZ9^cH$Ey^Y>V*U`J^?eq?MC%v2Y z`}fj&XmO}AFdN-RmxuDf{q$Jx$_Gsb!!3 zgwN5Z=#}A<^cngHeU?5?UyGflFVL6hWBzt}tLGwpHgGp_na1dNcv=Fd6$yeSX(CS1 zTLRM)Je{IXg*bXrf~Hw|BZNQ3KcMFM#F6JKZ+DG)qXpq zXJ$lojKr^H?uAGAiWmcPIu!65nc?t72=?89OTwcV69h%hgo+spQ^ITtYzv#2v!TMc ziSY+3yf&uB9}EV9))p&M$_xoxeTEhlcrG-W8N-w_Wy~qRE)fb2iB&RNprVK}QO%_M zo1qG(hPmReV(iRV27~rSMUnH~>oJ3GMWmjoWlAGkp`&0OQxEM+=vo?>NzBSfGvj7_ z%ml{4JdTWG8kv3xDClIGn2Ag*wAKGGJPNM!VxE~%FXLgTXn=7sekRB)@Pmv!a6A}d zY7!A9&O{kS9AYl{VhqeoW)e&*)51()mIjl|RK^oWnOZ->Ok<`qM&DNQL1+eZ)jyM& z#S{h)CTBBq7-?uOGmn|iEMOKg!y=2ACCp-GR&*&d0)957VFk02ITJk}TE+MRi&|DQ zYZz^OEwhzb$82EMGaH#=coVak*}`mNMu44lk7I%AKw12v_h4)%a|}EV?O^sYtq~mF z&Fo=D`t~vI*v0UTm?pZ5F~KSC0cL2+w#cZKaBx4fG<=R38onPL6F89Me4l-b0Y{t3&S)+F(h+^;r}yRf@M-n!grlH3Ef~$`>!(BnCzC-k?inI z=FWfSP1Gi4NAELtnS0CwCYycCJYgO&51A*Ce(WH&Kl_v!zz$@Gv4h!D&`>sq9m0-a zWuf8hNLIv3SqUp<<*bao3oBU#o6F|0DmI@rLj~*=pPDUXwXBZSuzL1Hu!t4=Wd22d z1A9GM7&Wpcc15t59mUr9&1?l&!k!CQ*s*LWYh%gyXx7S>vt_KrH-@cX>td%8@=zt) zKVfI9*jiSfs9^(OHQT_}vvsUA+Q>Gs<5+)Skl(>NSr==8-0Z^O1a=~ewj!;~EJRLX z2S$cLx|oMO9&e8zf$T_*w*{_-n*5i2C*oe#$Aau7@7<`MO~ho;06R7mVz*?PznY|oV zgi4^P>=gEzC&`N9v)QvzA5EP7Ez%hlO*(glB(ZDSI=voL$B)VJ8snV31wOu3%TO zd5O!>k??BvbZ`y3mR-m8i*laYfU0E^EBCKwH?XmIC77Ss?5m7!X1B0g+5XY(>^AmT zbO);pT?*F6!jYRnKeUtG#Wq5_|1)VMl*kS3XZNvnfoMP%JjgbO+Smgu8@}e>4lRl0 zhYqn9Lr2-e>=Cw|J;pu^9cNFlC)vZXV}Z-TQ|xJWMq*UxDtsVuhQ)km*#XfDYzsIV zI>(-8FGZ^0$G%JKW%eSAvlpWVkY)*%VllRiBUwIi#(#~KM>+Ni%d^+n8!XG-Wn*53 zT^LKTmqPUcGfeo3LmQ$a!nfJ0>@9XXye4voy~z#_KVa{(_gE1;CG?QJ7_ame$0Lyz z-($8hFg*B(4M0JtA2)z|6zb0nWRd~QTk%E>q-m&a}K6>thp z&S^Ovr{@Ye4Tpv1Mb+FW&cGSDBJO^;m@{z}?l@@X9>SGe31{U-bETY(8^e`zW4S7> zg0piqTs5~gLc_J($Z#FEBVNx91J&M>;0dsSYvh`^ah!uY1y+GgfmJaV=j7a6gSVNR z$W7oTaUi$bGhCZILk%*2i8FfR_R3a*apyjK#w#CmR2^igmF zw~;G_HgQ)IYq>4lHf}4oncL3o;3kJL?@n$Fyo=k-<+MnUJ=|VyA9s-3&mG{}xI^3y z=pMWiI?UaQ9pNfc2jHXJF|M7f22XIc&`EBf{}gwQJHwsjqTp%nJa>WH))} zKMjrGFTx}FhD4E99~ALdy<%R%%Xm4j;{6?^WM5Uo>Llulufft^8=dlrQ0H_%ePB zU(S!^EBOk(HdMt|^D>{EpAuf~ujSjqb^O9;J@4R~_(pyl-@vel5SrvyPu1U&F8GH}D(zP5fp)2#*Lp42Z&8`0e~weg}WUzm4C?@8Wm!`}v$8 z5kA20<@fOW_;LQj{2~4zzua?%Kf#~mkMXDZHUmxN4)v;?l&!>0^yaPTQz0Tj@H$|`V zSNM6}2mF0L?!C+3;cxL%z?=NC;9Rde@R)zZKjiQ6Pxz<&pwz(B;M53jzf}KJEU*?H zkdnf~QbSV1Q`xDZsRsXu)X0=5m6N*7i&K{)@{}SaOG#6CDM?D1%1x2I#`cz%2A=Q{_N{vf7QqGhs zr#jR3(9=a-1c}Vy?Igb|qXbu2$D%*BGb4S?-)Jm?4-cm?fAkm?M}gm?xMoSRhy^SR`01SRz;| zSSDC5SRq&`SS463SR+^~SSMI7*dW*_*d*92*do{}*e2L6*df>{*d^F4*dy30*eBR8 zI3Q>f926W992OiA92FcBvsD-MQO;-)ZarSbu-#dZ#-7eOZ7BZ~-Bp1hjw=umVoN3sQnBf~$gSg6o1C zf?I;yf;)n{g8PC8f=7bKf+vEfg0vv=Tr{I4V`j#jjQJVMGgf7+&e)c*Gh6?ta-l+~6y^%^g!w|1ut2C5YJ`PCtxzY_3yXvXp;0(WSS&ON%|eT?L^xVlDzpk6 z!U;l;&@Y69VPQlV6;2bT7f+d*oU>%X!(0C_kLNC3v~XT96mdYaTPNcar@1FIhbMt9 zk2f&g@0%5$5slqX%!HjSD8W-)3cGThq}gjvkw;Ii-gt2H&h7bk@9kQ7ZtJ$QJ6G-4 zzWdOIlc)BtTf6bZ@qJr1Z`!l>;OaH&kF~cQIeOsC>BE;VoWFQ!+42<~@AdiY(|>pE z)}xoO=a*mfe*2yGyTAMGcVGVi{P^KV&u3%~{OOa=zsdUQgBMUB1Ck=ax%ljr9t#VSk5C~al==z{986=h?L22G*8N+GSQt#7FL{o|ki>ihRA z{eJ!Bw?BsT|8r3^*gv>0N8aG_usq!)jgxf3q88@(Dx|nVd!D(F{(##4^t0w z4@-}d9#D^Pk4TSb54cC7M@x^^9?w$Rt!MY1pY`lCtmjufzwY^6&mVjK)bo#?vYz_R z?3#L-d(Q5;wWq#UQ7=O;W3N%YihG%QnR{7!mGr9WW$#tptEN|Nuex6Ky&8Ho_G;?& z|CDfTXNE@$ZNf6)7-6|^tgu2@DRc^5LU(6^CkmT|lR9(k6@o%vXPN`TpfDti3FDo4 zo-SM=Tq)cn+$`K9+$(Go9u^)E5<*f)32EUAy&;W&i7SI8Dpa?Jk zMqm_B4442jU;#>i(LgC+1#CbWFa{_G#sU>UB~S&}foh-zs0HePdY}Pl1e$1X$5CDQe2!Mbv5CNh<5;d)y`+)tx0iX>y2pj?q14n?Pz%ig5I1ZcuP6DTZ)4&#eM}v);*iH|xEu_p?68`Y`LGtdFz0W_^XFqm z>$9w0S;DN|S)XTpk@aQP%pZb%LVc$8N%ledOz*R+&*47D`n2~s+2_^AuRR7jvO4;7 z{MhkR$Il(VboA}`wd1#r-#h;3__O1$j=wwlb@cBT&@r%MP)AP3u#Vv!BRWQQh&sd_ zk`7siqC?q{+mY9y>L}5tQ0)1Rb2P5(RH zE!{ocBi%FoS-MwRnC_kaJpD!b%k)?2uhZY8zfFIa{yq()v(i7L`=ozN|CIhY{Y$!U z`q%Vt=|9qcr~9V|qz9%4r3a^nq=%-n(>dv3>EY=S>5*wsTAY@orD<7Oo>rul>D+W) zIzO#S7o^o`O}a2WD*b;NaRj`Ye%6fF(>=R)?R>Uw>F3fwNAFHrNIFKPWgTk;|6K=7 z`kn4s9q)GU(;*Q2*s0O8cYUU+x^vBE%06q*Gj*T!>6yaMI`&NEXMKC7^s~M_Q~O!x zo+RAJS>ZDXgrZg z!wWtB>Mjuc-T6k({CnozGY|jErB^yVc*ghTf{%NC(y8{R>1V9cck)cu$*uq0!$1DR zwb`AF>(ZIZ`Cbo7`QP@etXVSgV=|pCFw%~2x z*{SkEr<)m_RQ<0^ocwQjJlvVcXX)J4XPH1HRZF$fLTQoIC}ov=)#1kfE8#+&WD9f> z&eusgZzmUj?BrvwPEN*z&$t=?ALM+-$?{H8KI7)-PI5jY9^J{j|KjHVEOq}g*8ksG zClCyh3k3fwnfzbLZ!iqC!!#$S>L; z`deBoEtO(Yf$TZi6xn<7{&J0cpS+9c712wgmqlNT`igMrU{SV6C(??7qUoZ3(hS*S z*+BVzIVta!cP0Nl)#s{8l|(I7%heCnZ)kdI`fA2%E*8>-w+e3;-YM)Ttk8C7U(vmz z8)UTNOVBdCfX$$DD5vb zOKs9Jsa2Yg-j}{8`Iq{;ND&K2$zfu9Z)ax5*F456Q3Qf2sOf^_A*d zl|ucNrkCb7%^1zy!UkyUi-A>(0hMx_?4I;z$#gmE?#j}dn7t_TzO)r_hFn2FC zl-8BDm7XX)U;2jiM{8;MR~0{3{9ZA%LRF!zSXAMw{7Ce^=snT9q7OyCi$;pXB9Vv| zoe`Z7ofMrCofeTIN_19qNi(nZoo(#O(=(wAk^WT@-~`HS)oc{wM4Dt{=yC%+K(|}>vO#W87-R;iVRkW7{GIt5 z^H=7t&EJ}hrH4x2vc75UYwd6CXZ_2%v|>V~xAH<2Q}v;}oBa#>mv+FOW&gq6$L_N? zRWGUDT7A9xd<{`^r{=f1Ep=7(ZT0^abrZ=%a#4w>RWwU9M>JcM7IhJSBz{@^p7=HK zo8q^`Z-{S;?u(v?Zi?=R{t-PVenp%k9V#6n&6ZlEW2FJlkCQ@o_eP`s{qQSr6{%e|C)F}F`1koQI2 zw|U>>eV_Mb-bbpBRllhEswSvvRc=+2s!runIaPV;eDx%?M?F#PRXmHr+9GG5=})(VS)e$=suK zRB1!$`_@8ho>gVlS@l+lRcS4+%EpI{KaU5;Zyx`gBg65k z<84PD$9RX=;dA&Mq%-Bb<9zCT)z#J2t@Acq;Tq>EZSLn8;W2u~dwak?h~|k#ipAog z;ykfd{G(VcR*C_!L@X8mDgI5|S3FRx5dR?_A~uROV!il#u}J*0SS2nLXNwEOBc#Kn zBc*of8tK~3D_t+ym$I*9U&t29=F1ky7Rf%7SIX`3PZi%P{;l{+(OV%>e6ARx7^vu@ z$W{zcbX9z*7^e7D@w=k0;ycApik}tT6@MuDDY6t_E4nGZQKWKj=2E#=a);$f^ZMnD z%o~~~%KIx%k|)mlJ?}>TU#g@kqMD~dR5MkrDp(az%~C~GplY_NP_0oHsMTt}+NTbw zztQy5^w<2Y`AY+7!kU=IuennAsy3vZqy@DPw3)hYx=}iVu0~g`4^7xyi` zReaZ^GmFe(v&1YlYs|T3tvSa$#H=^V&3WeH(j%n{^@%GzwLwg#**Ym3!otuAjYZz``XH&hf==qq+r?5Kb%zp;O9 z|HaMBTk9~;!5#&u~j@p>=QSO-QsewUF;E;h#SPPc$_#S4vXu> z3aL!0l$J1OFa(%)r$WxvRNlL4~tWIxOL%eKfi%4BkdTrQW&P4XIft-MZt zN`69qT3)HJD_}*fBCc>K8Wc5(35tlKS<$F~6k`?D3cq5q!l1Aye2OMTQ1Lj|omZ1L zI^(eJbols9!x2UJ8Th&w4A8WETgEd1mYcooH<3pL9%D>WZ#Ki0mleNWp} z`u=c*Tx9)S@mpX^esT;3r)LqnJx_@-{bZOli`gir;>wnU}um4K_ zp1z0vbN!e4uk~H^?;GAT{B8Kl(9ba5u-|adu*@&@^%RtTvo8EH|7noHCp?>@h4d95(z_JhymL@#ErmP4AdqHN9wh-t@Xj zV0y#!p6O-Nzf7;0UoqRvK6BhW);!f*X|6F(GAGRm^B8lvdA!+c9%qKkcJpL&)ZAzu zZLTnXR%$9WmyR!e$J*6OS~=?->vHQsD{WnA-E7@r-DD-K3#})u$F1wEyR7T2+pSBi zr>(oKx2#*O8?CtYy!C)}k@bf4g7uUYv&PGV<&ko*ytVwBif=3WR%BP0D@rQ1RUEE3 zTXDMLXvO@>d6j78!phl|f7+AwR{Lc8gz6pDx2x|~(>4Fp{8iVlu7BPBx_x!^^>y`+ z>pL11@m4V=J|;dQz9`-)UL!suzAQc#qeBStX@!;Zx#hZ(XVxj3FK4xBJK4+%Q>&!>Ym&|v}%gkrZg!!Vm-8{O~UD{l_y0pFYbZIy1 ze(Ni?A8cRQ`rCxIS8Z9gf7?E={ciiy_LuEvTUXmX>#w%YZGCND+P<^>WP9EAwQXVf zhVqr=i^`XlFD_qKzNCCsIZ#niF{Yxbg0H|T@QSsS2P!vKuBbdzxvg?#<;V7K?PB`~ zyU0G=zR*6)KHEOSKGR-X-CRAX+Ecx+dQbJv>fP1P)jX-bE^n~v8UZ#jN;oN~~PbB>*kU5 zVqBSS+V#AycuFbL?{)aL)SuALR5$g9e@r)+o7G%@bIwkp|C41frrB* z;E}K)QIcp&Tua<$TNK7Pz&wHD9z+^s3}F$!C%q;@%RW*8-EUr9Q|UrV}38l|(Ov!yenholFk zd!%jBT$x6ukf~*bvajUh<)}OKb%0@Ygu9~OL4(5;|r!8-+4Rox3d zEBLZNSnz5=mx5AtzGj(vrFxBem3oPKwR*jJojOw^)hIO*%{9$GnkSl+hSS{C+}Au; zcvo}1@LTQI+J)N1+PT^+T|^hwK{}P*s4vz_^h&)=U!otQpP)zell1|8Oz+oE)WiCr zhC7CrjA?_&_?Pi}qtZCQIK(J6<`{o7N{oLRzcPMp{K7cWIKr4+oKxIdyuEmL@c>hf zNo*Qul9@)B2Aleu9-7`Yzi0lS^Mc#gl5H7ckz0PX46rCHUs_gLKCq0iNG*R_?wTc* zewIHh11(=!23y{-d}!%!0ZYB5YfC?~es2B3+S}U8deb`E7Pon9lWZnit*yqUvPErH zo7YxmE3j4BN^E``XsfeLv=!T8wgcs7%Bk|R<@?Hyl>b^$SMjvsQN=$M4=Wy2G*@1$ z>{4~T601yAUg|ua3+)^YK67m)^@M$R{KM(vQAQ`uhZ4#)@kbs>+Zm%h zj;*^~$JFt4XX+-@d+N{Dzu53rgSW9q(+5qjG`-x^y{Tu@yG?I4z1Q?{(%PaCzuC=tby3NB}(!-3?_x|AbzE9){u&4qb!@ zNERL$9uYnhz8F3oz7ReecE=5{5iWw&a3MSj&WE+|Mwn<>)H-kSnaRQ_KTP>!$~aNI zq)DQY43!L&jF1#aawNr)YKcgql-MOzk{XFv@`hxL#3s>5D@Q5&D!)@|l;0?g z%09~e$~@&ud9UPk$-A8QLjKG7FXm%;RNmvf=ktHce~|Ymue-{i`njNgK~90GV01yh zf%K*Jw0_ntv6(SonHj*TVM;dltS` z_+{a{g&!66D*U|ggF>RPul8qcAMG#Nwc2&sHQLqME!v-Szv%LGR$Ym%RF}}T>JVLv zZn|!oZmMp)ex}~5pQfLuZ_}^RGy2W?-TICC)%pwiP5LwXllndS%ldWtbNapdj}2W7 z*@kJxxyA}3Y@A`7ZLBexjV;Cn#<9lvMz7IroMJ3B&NEIhdW@~c>BdFIh%sbz7#oZw zMvJlDSXi7}EGbWwD}oYVnlfMa4&o+lmhsA1*F56_`euEGCmlYkFil zE$eD_T9#U7SfZBImhqNq%W}(HOTx0u;aq-CB3wM@6TEI*WPD7{j8 zwe)Li(6-5T$9C4X!iL)p+9=x-+hf}cWqWOHwxzaXwk5Vpwnw&gwg_6o<%Kxlrs&H0(QrWfg{mOSL zKdPKm`9jspRT)(;R&`XqR)tmFuX<4RiM^-2*lw~L?B({5{e=CbeYO37{g6FaeWJR( z`h4{-HGkFoQS)<6-C-L0-E|X4Ql$nX+YBtO<7HUjz`8%9)EEB;qmR`KXD9m z3~_w#eBb$*^K0jqPQcmA`Mm2V*DtQ0UB9}5Zp7WiFcT)jZ1`H34}0UIVLMz2*TUs+ z6v-D3v}~z$Mm>7mZDqw*Ng7!yA%nE-Y)89$T7?` z`i)17+l;(%zj2T8kg?rJ8jl)r<0<1c<8@=T@vbptJa3eZI%j-j)E1kH4aLUda4}T8 zw7ASvZmKd>o5q;Jrh3yjQ;n&^ls0uUe`)Mfc)y~=XS%F4bi>s>ac?8mZ^WxthuQ#P>d)3U$H+RD6TAC-y9 zjAet$D#}XAbY&ln{djEevB20KV}Bl7HujUTBPu3TI4XXw6juIN`DNvARbN&0sp?rJ ztoo_y=c*s8zOU+9)w}A8Dxtl%z0^M1PT0@eN&8v*8T%zWW^br&tcI(j)vKy6REuk_ z)fCoRYsb{SR{v7{EA=nb3+h|y6ZLRCT~F5MG~_l6X^=F?8Was98-_QC8>9_rB%{py5+?tk33-3Q!#C%ir( zbHWApzb2e;Uv{gTFE^iUzSex=f1~c6`W$(n2jS2B#sax-!vayoJ=ay0TQ`aSwPS{myg9~K`J9~mDUA0GcN@g-g>@i#6^h!W=#O|fQJ z7pxZ69;=5n#A;xTu;NrKRg_Akk|`_|O`+*%x+v{P`_qARB&|)u=>zE_>BH%x>0{}4 z>5u8tnQGbU*_zpB*_YXO*%#T5**Dp>xiz^%KJV~ z$cET+ITp?d&T&p-E}grYyO!IU-+(`p-+}*$*M|RtSD9avU%;QspUR)dAI~4k@4>If zug)LA?Vhh*Dpeg;T~@7BEmLh)6=>)hPy=XYX(Sq! zMxhaF=4wP5j%K<>s-b8`Yl+$sx@o!zy74+l&(M?gTs=oWNk2hP*E97jz0qJcR5HFd zR5pGzd@%ep{4;zud@|fM-ZL^ya+BD^F$ql;lgy+ru}mV9)Fd#yHAT(q%^7pn95#o{ zc5}j|5+fhsBX_ zcpNbY)1h*KE}2W~TH#vmTI`~@Y3_gNDu{rVC@xu7zI4M3hPLAW_Gvc^-L401E5Fa0(6mOhpkx(RL2{3UYF#@AvBe8MV zL~K09#O7cCM!<$*v#@#CG;9<$0b^joUNPvo&B8sll`0Bl-rQooO@96qSRYnzoJP+$BJk8xA-UctN1tgoA@jE zbNCDR5BRhA7kB~T6TwIH5(gEoDf~;WL}^25LHR>&N~un%MX5)rPx(e}PU%3YNoh#= zN^VJQPBqY+G#@QVLunS8pXQ+bp>?Borgx>w=pg+8<0<0>;~L{C<2vIJ!@#`De8zmh ze8POne9nBxe9XMZtj7|tU^c>La)cZ*hsq&wE^)4MYICP^)!ZH28T<+SIeZgez!&nl zdnNVPUFy^8d!sA#%nQcT02#T z*8zH=UZz*;mHMiNgrSGAp0SRxma&QPf$@On$ z?AYko;MnO{=-B7j;@Iw3giGO5@OF4VdZ$J`c7Iy$dcy!)Q6W2#uf%&?LGTU5PG1m!nxUftH}lP+2$< z4u>y=FN80K<&j(@75N=$8fzSD7Hbk)6ywEdaaLRyr^h*QL7W|D##<#?CfX+q33I}f z&?n588}nmXERET)Bvy(o$Lv@FTaa3wT9I0mT9R6v%B4%w`Sj`Z#dM{NJ>$=O&9un2 z%&yCB$X3Z!%hk=*${on9D_LLiv}96#a{hDa*V0d=yUWK^%&w?Ps6_aM|APO4|Bq0U zP>WEFaGQV-gTxRqLPUx0i5m<1Q^rt6Q3g{cP=-=^Q3@#q6e>ke8BQ5M8Tdbqs)ANd z+e9m+t);D?Eu|^xwV1ycb(mimb(uApZ<()|FPN{G087LwV;8dp94?2?dC0lRxyQM~ zdBnNTnaR~~w{f>{jeH4T#b@)$d@tY5SM%k3Cx00~&R@lk@>laq_>1|){I&f40uX=z z3s4Sh1cE>uSPv`#l0XVr0bsydU=6SeC;?UjIbb2M2{4FEBC}|%c$Ijwc!Ri$tb^=1 zSXtIy)=KsV>?CUDB8TXKu6Vq`Xaqg@6@~XE<;CSFJlK|S7Uc$ zCu3)0iE*d#u`y~wOkq>PRAf43K4RW!K55=-zGB{OK5w329c!Iv9cLY39b;W*U29!q z+h99v+iW{(J7zmzn{PX9J88e~`0RM+_~p3nc8 z5#b?X#EeK0IzmO}B83Pu$PP9P)d)2XH3>Bf)eh}OFQUiL!)OXUg?>jjq9@RU=st8k zdKEo`Zbf&Xx6rxag0LcdKYTa*H2gUHI{YU5F#ITdJNz>IAgqj3L>5K*L>oriMLS0u zM0-U$L_0;>M|Vcs#oEL=#CpZL$9l)w#@ffa#lW~KE{!YWrue`_zeJD3pahZ#C5jTh z#1U*4wheoUt;QZ>2e3`pZtNm<4114l#MY!Xq^f4BXKG|>Wq6rLrYIB3bjtS0w#{zJ z*3LD_HO{rn-N~KL-OJs|UC3R@UCiCcZ7X?O@~)&uep-HN-js*()yry?RViChzPJ2r z`EnYuqA{T*p(&vup&7wVSV_1;ND!06PeeLJPO(rx3P3SaWE3eyMA1+nih?4b7%2y6 zduhjLM`$W~JLXWPnfabspEaAik)PnN;G_IA{H^@G{FD6S{DFcazyaVCunX7?+yo8- z$AE*t8DJl95x5NO1@xk$;uGQ>;=|%&;_c#bvXQdUGP+DIqsd0erpg2|zHE+chHRLO zE1M~!%T4lXii6MwXaRHxS_~b4mO-1K{m>3*E3_1v4{e6_YEEd*Yc6VzYp!T^XwGVG zXl`o`X%1_ox>EfjeO6zgFV{cR*EQ5L3^k54jy66sJ~7TWm76L|OH9wrH_flj&&*3L z^Q;Bd>DIZ{Io6%l8@5LF6Sl{;Teb(b7q+LiEB34Q+RjGKn$9-PF3$GOcFtPPhE9uX zqwBrvgX^uUr+brUg=e|vm#3ze=B0aQ`|19feyJbyPw~(4EB!J*$L~WbkaA=p5<&RE z-l4&vZlOM*PN9{drJ<+jPxKY~0j*SY8-0epMgO9ei~cM6hQ2{Rp@tdW$ESVmFX+#`k6+V=9zk#CYc5qW`>m!XR?`r*^${X z*%8@(*-_c9*@n5Ex%Rm>xlXy(xi`6oxevMRCA&+0mAonGnNQ@Km31lWUe>d$aao(P z>*bfrBNe3;y$Kx%od|;oUx-nPn-Zt^DF`J-xk9@{yFfcj8_4XwN9+V9)$+i%BkI*!w%j zJEuEGI0rkYIQuxqIR`nXIVU-XI!8HYI)^zYI0rbVx<0vnyEJaCTkVG2I`>h}AO>X!ueBNvcO$VKERvI^OS z>_mU8RO>S$_1dVBh6x^1R?W`5>UW_osOc37@*$-ms%{HpxQ z{F?ljvI%8H<>$)pmftSFQ+}&_e8rrKhlMH1TFMRDb=pWKoBN1AMDQ8-2>b=Ai+%#X zfN#Jy(QR>e$tKxq*&*33*>>4M*;?5;8CPzWuannO*HQn4s;hrOjnoa)zcjx!e>LAV zCY?dIOutXx%s?^Hj6~xe(>~KK({59J%UTQHDzNgbhpk7fee6B#z3uJoUF@Cg&+H^8 z*~xJ-oO~zINq5e4{c!zqncPPA8P7RSZSOh%3jZ1ZdH)IjdjC%U4*ydBJLEO;5xI{9 zLK{P$Lp_T|74TCA?2hcF?2@dFx`Vo>y0yBUx|zDAy1KTW zmZn{(HS4UpJ^GXSM|#8%F#ONDVmxMQW!YenTS4o1`vm()`#Adu`$W6ksdQSLpi}1b zyL8U2t{txJuIru`{yYAc{ww~+{wMx#{$EJ7V7=fE(`tJy1PZ-B4@Oozn}9B4c+;Z%bcGAImZ8SLcY2+3 zU6tH7J!0=K{||qoz)yemK%GF1KL;0l%{kA>kRuh|1ZCtsI4d^dLjNLt1AB~+bEx_ZlhhMy{f;g2aT6Zczetl zcJ6ky2=oiI4s;224t5H52+Bf=(4LUK$W-JgY7#-Ck!Vqr8(SCKndo0UqrUqFFKATj3FGSoTQwfxR{sO7uf~efBdeZ+H$^}pr)zay2pCdpfW0qn=Cv#*Y0#X z-1j`%5ES|yau+p^w1_04$tXWIGcmQ8P)sZyml>ZqlsS-*XUREo$-I(_C9U(bN(YsW zFC9~Qsq9+W&CvY%!4az}ZM z3R8u);uEL2T&PyqRrWE#{h%Ft1qTaG z6dW(e5`GgEg*ys6QmfGW(i^h=u^)&w$xq9hDkf+L6^t!-UU0kMMZtrDI|Vlj>Jr-) z&MoXr?LuuqA3*moVP<327?z%;V^!t2IbO~QZY=>`&`dF0F-0*!F-EagyIzaxwp&nl z`*eL=OI%;vmx89a4+WKQ-wJBt+Ts4gRl{}0HNjQIeJ*H>+dybStVL`{TuSU**r9M< zAzE02^nh}Y(x2L!I+Qw=?xx%6e!7kBq?_n2I)&Mc)s1CmSy?uglXZ`6;J}>!xUIQ* z?iKE3?l~??z!3lfzCbJx2@VZ`Z2nIDPks>Ddtqx4AxrKV%B`t7S=k}a@J~A1#1y&4QnB5C2JY0jJ1Td zm9?I=l+}XMl+%jSliQiwh1-=&;(EDGu7i7*`-Iz%*PYjjH<(w4HBp`t$ zdPzhQmRKbfl7u8D@ktyKkHjpoNQx!h{J|5996tgd{kUi+)->+po%huTX9#h|9{5O8O1Thdc_aLImHXb1x2wstM;pt z>VO(nC)8=RS6!m^sb$)$+Ed!=+7sH_+KbxD+MC+b+6&snx&^v5x}~~>y7{`*x-#7z z{d4^U!$iY)L)efttTTQxelWf_eldPG{WkqD{WASDeKSqAOt2jJ-|p1<%WAOyu@7^{ z+-u#7+}qt-+-uw&z4N{My{o(>-mEw8-Qr#5UF+TLE%)y9ZuTzsF7U4K9`r8u9`bJW zF7&DcP(T?_1t{T*u~V^2u`{u&u`97_v5oP~@pbV{@%8ah>2c|?>Cx#a=`*+^xJ$T= zxD&WjxC6M$xZAiJxLvqSxLdemxU0CGxc#^txYxL;#974s#Dm0fh4#WsVY)C^m@V8% zT0mMuT0%NUIz+1H+eTVTx=PwgIzl>1nom-bc9Ry8_L8)uyQKA`E!1*q1$8ksOwCZs zsCjBJHA{_AbJQi&5^9S2nO2WpNPj@TOMgqR#P~qJL;p$tL;p?xN`FlMNdHE^MSns6 zME^p+Pyb8*On*aP&Ya6S&$`Y!&AP@q#k$41%eu)r!8*n|$-2tA#JbEn$2!hB$~w!s z$NJ9t!S2NA%<01E$mz;i&soe_$jNgSah7muaQky9+yHk3ZxoNj)ABgHX}m(-4Bjjr zlQ)+)kvEyg<#pvv<p_DvPE)6vPtqy@>TLy@Ft5@>6m`a#C_g@=|h2(o^o0!}6{2tMcpe`|^&8PKut&p~`;B4$860 z3ChOG+RB@XA^8un##_~TFQpX zZR!g3UiAj`di4VJX7v{J3iTxIEbVNqLaWy5v?}dM?PKk8?JMm)?Mv-b?IW#UcUpH) zcU*Tww?ns8w?(&E_e!@H!#;X*D_ZzH!;t)%(cw1OtVb2T(O+7oU>fD zoVB>EPU{70C0k{i)o!+1>@K_8?y*;K)O1vK&|C>u#I@H|!(HFqz}?h6!hPL+%zfW| z!F}9)(0$&0#Qn&9!+qO*(tX8!!hOzt&HdDU#{JLJ$y@Bb;r;9V;jQ6&?ycmz;vMaK z<-O|t?0xNh;Qi?R;;rlZ<^Ae??|tcg?7i>(>HXw=<242J0ee6fm=r7sN`kT=7`zm! ziq=52kw>uyvA41Nu@CV*@qO_t@gwoQ@ni8*@#FE!iAxDm8kZ)d3)8dH1?l$qYWN!X zCiu?yf%rD~FSyURA@~vazWA@WZ@3otw)l?t9{6GS0r)}q`gjJRn2;o#BpfCjAsi>1 zCGd$1B9X`;a*1T3fOv*jzp%WpJNY501-UW#8R--0F{w5AAE_$2CixYq5xE-q1F1Io z7wH@6JLx{DKDi3H4!JV*Kk8}fVA>VxMe0uKQR*J*Zt8aGR_Z?LG3t5h4(b7Fefn&A zD@JohKSqB>Ge!eO3r1~5d&Urkg1L;jhPj$qg+*bJSg%?CSZ`S$Sl?Kc*gsgkI6XPb zIcqs9IjcECxC6NoE}O^TNqCcZ3wc4Fn}_i{yfU7Tm*VAl86LtbND|V8{e&}wLxc<=K{!%K6;2e+6wVe+ z7abKH5*-#D6wMZM#WTbNabrmnNe5|b=`87B=@97{=^QCe+FUwF+EzMD+DFy-O^pw-O=6D zUDI9HJ=fjU-_w8Af6;d`Fb!P8dcy(3F2iBNe#1V)R>MvMYCK_VXKG_=Xli0=Z0cfa zWNKz=Wom8eZ0cd^V(x41Y#wGFVD4$|Z|-C6WbS1iV(x7oWF}dd7Mz7;DYOtR1j{SS zYs-DhTgw~EOUrG`UCSLyz#6jptwq+b6}2MP>(++0s4=8h(g4vxl-29CN8mW%J=xOgtMi|Ih`U-vReSLj%d_8`Uyw_`le%Sk?IN*w@(A`2F~e z_|5pE_`Ud(`1Sa$__g@$_~SS}QINnTt|T5NZYEf1ZhCk6QTATRfV`_fjhEs1cn+S2 zFT~Hmv+-K|O#D3jBs>#88!x~&!%xF=@gQD~7vcf@bo@lT3a`Uc@dW&Nf}E%%Du^oL zRpKRLzrxZ&1KCI(OP)u@lZTRflL=%2c{q6(c_5ico<^=q9!wUI3&^9$Gs*qPW5^T9 zbI7%+HK@O+O=%CQU#YjLpQ)8-Z>WE$HEG|eb!eZc=cqMkZ>f){AE{5Muc;5H^=ZGU z4QbD)m1%WpAE+;=@2T}@RcS0bk3nOQ84?DEF_JNip=970T!x6DU=%U{29+^|!DbK{ zQyEOgOh%M3o-vOxiZPd=W`K+t3<86~n9N`?v`iKA0CPW+&T7xD!*0iJ&F;vq%kIgp z$L`MV#%{nK$ZpK;!tTKC&mPYi%Gto#%-O-&$l1gh&K=3kbJN@kF3OAZ0=&Jv^}LIBM0s0zM|n&6S$R^qNBKtiUb$0wMR`zpPI*FkUindZN%=>4 zO?gK7NV!Y-Q(aHw+u7$3Ku92>#uC}hG?w_`SuDvd(`>Ok{`=YC=udM&0tD&!?|Ec?}`=_g`ucQC0 ztEmSJLc>|ZMZ-D6DZ^>Q1;cqm%os9WGLAA0F!eJHG7UBjH4QP1Gj%hKF%2`#G!xCU z%=65%&2!D&&C|?yGhmTeK#R;Gw8$-d%Pz}L%XiCH%Qs8RT5OG5huLP>hT6K>2HHB? zy4r@=TH1Qs+Svx#`q=u}2G|DMM%r52y4Z^CS$o>P$Ue+5&N0$4+%eiQ&C%D<%Q49@ z!!h15!7zzA|6lx6)VSbNZI}*7<_IjBmZq zwgnCb4g`(_b_Ui5jt6!GHU+i^P6svzb_I3^4h8lE_6CT-P%sr-9$Xk)87v7Vf(wEb z!EA6@Fc-`RGr`5d)xmHu8H@(Kp{t=Ap(~+*Xb-e6+8!Mh9usDT*%`~8hs3AE_r&AGkHpKwtHig&^TgYPH5Et&Q}(nnEl+FGPV#g zHDnWc8f76lNe+@@^&EM$~3%#5WBJ7WoBH6z7X z%NW6c87L#e@G*)QD;PG0gArzwGGdG*qm1#Ad4_q4d6{{Ed6IdSS(i1CHG(~bEoV<+ z&tXqs4`UP91#B{V2zxY}$R5WY#HO<+vq!OKu}8AUvInz=vnR5rv8Qq-acJD7+$G#q z+!MSVywkjgytBN+yr%q*yxRQc{A&C!ye9nbyx+XPyaT){{FeN7{D%C0ysG><{MmwH z;RfM);aTAm;d9|T;UQre@JV=5cwhKcct!Y8_(jME?g{q_?+9-T4-2mgF9{C{j|oo; zPYI6;-wUq^ZwLpABqEsz5V1s3kw_F4m5ItlYenZp7e#ilLTnSe#Tv0iEE8+Rl_hn- zOVX;~ZE0`twe+R*gR}v7Rr*c(O8Qv(RoVn>2!4`2kk$ZCOKXB^aI>@#_)dCM+7$dQ zJum$u{VjbitpnBuTYzV!$EA0qcco9IpQRV1)xpcs7t&^6H}IHrlzfbQf_$vJSe}vR zLRV)=>HAvM{#Zzfia@8nRXVpMep-QCcp)#ugl}SZbSyV)oST#&FT*Xjz z)U?+O(EL*O*9_8h(R9;v)(qBk(Dc-d*0j;g(K56_ZAcr?`n5&c#=1JXfx6DRLAoKj z9=gH0e!A|uF1lg5y1M@QzWRasKKkbR*7{ERe)>WBuKF(ecKTlWj{3p+U-|(C$e=Jh zGu$*hFg!KfG2AmeHe55LjF|DJak^=SX{u?SX_{%aslYVHG}m<3^xDKT17?w#WmcLs zW|>)KR+~Yy)XXsp%zBH?qP3_k|5tXE z&9=~9ZeMO+V5d1ChtN^zpg1HBnFHrgI#>?5gXkbRKnKCWbrd+pyC%3qF0pH&YrboN z>za$;#<}tCzwXQKDW2ILq6g=h;Th+d=NavJ>Urz==K1dV;i>8!>mBEv;5B&1_|Ex$ z_`1OReb0T3;b*?bzTdvDzJ~Bo-*Mk7-y`2XUuF1??}_ia@1O61@09PX@2c;M?}V=w z+yK7oYXnz^5Bfg&uK0fXj`=S5UizBAhkUPnZ+(Y-zkJtx4}ERmTfV=(>w$-Xr-4g> zM}eDxmw`KhSApAs*MS#-Yk}v13xSzIN{}4f8N3$U5!@c!AKV+<8r&V+7Tgou7_1yR z5ln@$p-kvb=w|3vr~sXZjza0^M3jos(5dJ&l!(qm3FvHeE{a3BVNci{-WlE-ULW2O z-V%l*?no$dJ#r~>CUPutHF7(0E^;MuBXT@)EpjCCF8VF{Gx{U?H2OCBB>E-#Ec!h9 zHTo+0A^JM{Ir<{nFFrIrAYMJuH&He5FaA4TGw~z-D_%KKDN!R)Em0-GPXGySLXhAk zDr2>=ZdetpHP#(#fYrq+VU4kWi9dx#9)T42?%=9n!NNrhAK6q0tP zgXvJ(mv*NQrjMr&r9Y(Krxh7>MwO|NU6Fm2eOmgX+*be>Y%SP>--kbnUyDDCKZ)Or zKaM|$Uynb4--+LVKZQSm--_RkzecbU%|si~PCP`uMLtbFPd-IHNxnngPrgh(OnyP$ zN4`jokk65?kROt-l8=)=lFyKDkPniZQVVHP8bHI*=Fw)+AR3ugKx5PBG!czS8%Gn; zG_>EeV~oR$Q;eOA?TihKEsXVyeT?IbU5ulQZH)bllZ?%bt&B5_9gH~RD)Tz?2D1Ze z2us5jvS+hJY%N>C=CGw~30uH^&&F{III}tPICD5hIHx&BIj1>rZ`q}z9dZM17r|Ku_aeBPIKtEYOT|ZAx(m&Au)Bn}~(YG^L3?_pQd}nB2tYZ9T z_-puW_+j{NXlVRp_-c4#_+t2D$Qid7?-^7&(HnZL0wKyymOCxJbYb$GGYYS^LYiDZ{Ya44r>muty zYq|B6Rcr%nN}JvW+O#&2O=r{C)Hc4&Y%|(~wvG19_I384Bj9j5A`X|s@31mVOJVuYi z19_%;_?~v&S>CDMiQdWHY2FX;1egLFVGA6CBd`c2z!TvBEP)5Y^I!+efa$Ov*1>GJ zhMxnEfJehaVJkcZHp7GAX>c$2hHnl$0GRY_ky=P)JR7_l zycN6|JRLkA^oR1Hd!Z+x$Ds$I`=N)S+o7i+BN{+mC?B1GnoutaqXyK6dQd%TMaPEc zglC6EVIV9H2f}dJA3hzHM!-lS5{^V8u}CD6j1))W5iIgPazFAq@*(mzaz64R@+k5! z@+|T(@+$HXr5@AsFSFl zP$i(mOpJ&P#71LcYzQX8IG7LXkIlxWV@gbc;V>R1!60ln#>dpyAdG|wF+8Tl3a~KuFGxA zt_KTpsY_{0`Au#>X-lb0X+rsr@{`<-QkC+L{DT~#*=ccFmKLCS zXfB$bRz$PX5L$v3r5R~nT99U=71Lt0zce}hHsdYh3F8XmCF3UJ4dWH#6XPM{KI0bS zHRCSh4&w&nIpZF~#JtaZ#1yi;Y&YA%4zR6k58K2>*&%k2?PS~85w?ZxV$(QuP9f(y z=K|*%=Pc(o=L+Wr=OX7aXDW9J7vgT^_T~5FPvUd<8orKCaNxt_F952`~pn!DV0sTmkL{ z*Ma-M!{8opF?bo=0iFa)z;oaxFbS>%7l7s94KM@7!Gquya0z%6oGqUxpCd1qSIFnf z56KV9|H{v*zNkK^szBYK@2U~dFI9i27E}PWf?7j8pq9`Ws4?_LRT~-(>7XB~x2k^7 zL}(y178(Yz)T)hMVf^jkF=>HyV)+Cw#=P7tDrYeE{g#;@^eVj7z! zpz&xjnyK1CZAP2YCbd&^^K=B=T%AC#(`)nwy;d*LtMru&{~4+n9EP}|xv{3PuCbZ1 zk+HV1sd2l}W3rq4Ca+0la+@HN)8sR4G_N#oHm@-+H#^O%%;n}K=K1E;=7r|P=Bx#^ z6j{=il%?2`v}7zn3ucL1`dDXM2U-VMds@3&ms?j^S6km&gSJu|YD?JSwzMr`3)}Lx zsLgN7+F)YNw%F#i?XvH)@3!x>FLP{j9CR#ltaKc3>~|b-lsV2i4m*xIPB~6H);V@K z(vIDZ3dd5%TE_~<0>=r5&h^ms$o0f^*VWa{cC*|ePr&2zAfAXP>?!gz^Jd`1@B+93 z-U*+D55lYAweTu<2fP_R0H1+Z!X@wq_&i(&FN1f%3*ln;7^hD&yM5I5`9_fU%LfRlxkiN(yWIQq*>4*$QdL#Xi zu}EWNG%^SohD=7fBR!CjNLQpaG7xEmd<#Aceh7XH{tW&Nz775kehvN#z7M_*z6zFw z7KAE7<)Ig$SE1*jRcH<^LNPRkrqOX>X&4M&37-pJ3#TKsqW?wzMe0TWL~2K?L~BH= zN2^8uMk+mSBr9BbLXk z*nF%QTZu)mrC15J3bSAt%!Mt&-eOVAi>0uIm;(!93osN5U|}qWEyJ7`f~~;Hup+D< zR-Rsv{+Fg@92slInek=Z8E?jxfis?rD|0q;Ceu8-Cc8HKU#?QFMy@PZnmd%+k=vEq znLC)jK7kAIJcuKny4XP#_E}0xAG0umQ*dMv+xy5!pl= zMVmxh#OuW?#2dxi#4E+?#Fb>#Wi4b+!0%u)*$413*irTmd<{MX+sN9<{(?PZAHla^ zQ`tB088}f^Q}z`61=f@O248`-WL0G?W!+>AWM9GVvKq3_U~?HkzC^xAzEHkYURhB| z@lXDrViMFHazY4n6vChg6oSH#0K!3jhykS`Bcy=55E;@!L0D>bE>WtvTz zthPi;(-CzX9aqQGv2{!xU&qkVbpjot59v{TK=08z^nQI%Z`WJ&H4QZkPD3|idt)nO zJ7Zg87h`K3HV2 z=6K+!;{54&>iFvT;CSzN>$vAIx>mc^yVkhgxL&zlx?a1Ux}LiLx5T~3Q{gH1q&y2f zIZwvZ+>7%TdWl}Vx4=vAcJ+_(H}F^T_wv{G*Yel%cktKt_wcv%|APDYf5J8Wb^V{= z-u{npGygaE75oYQ4LA0`gkQkT{eR(}{&xQU{{Q@c;P(En@F0J8|1|A1Ti zJNgIu>-byw2lxd63L-=R#DqLRbO;S0Axwmb@DT<=KsbmJks)}b0O2BR1VR*u6;UB9 zM1A*QK){XR%l^pacEKKP3V2-b?6*= z6y1(qLNB0~(JSaK^cuPs-H#qYucL?1o#-C)20A`GFB}U$3EvCf2tN~ zMixZ!k&e+W(QeVM(f-k9(KgZM(MHi$(eBY6(WcSH(U#FB(YDbR(bmzvv97U}vDUGU zu`aRZu@*61TpfqvlDIrRAkin$E73bKIMF@PKjBXV5@^Dc@Fw7dJ8>R6gdN8AV>hs^ z*g5P2b`QIby}+(vcd*mgJM19#96N~}$IfC;u`}2e>;!fnyNf-;E?}3iE!YF>D7G21 zr&gy{rPij_rxvFdrZ1(dWol;r%P=x%CYlLm&S%bL+GpEkyJvf4duBUk+hluWH)d<* zer6lxn&z74TI8DLZs*SA&gCxUF6YkXPUNoUuIEnWuH{bWHkG_Cc~$bM(i&xz%6^poDy>;ot*mO9ul!PZgNlX~ zZ3#^XEeNd%jR4DME^pqNQ*sCQ4gs8|rb|9@+`oKH5&&LE3KG zQCbUTD`p#JCuUP-6J|qZb7oIwYi37gd*(alYW4>93idMg7WR7fTJ}oza`r~{8umK& zX10VQ<%l^D=LP2(=PBno=Lu&HcQ?0;znq`uAK`E2ALSq5pXTr6@8fUhhxylltH62S z9B=~I1B8G>z%Aeta03`3a*6Dsg`(r)z2g1ilj2k2bK*VXUE-bM)8a0Yp)#>-mP{fW zER)K{%h)od%qU~Z2(q3shHQ*%f^3pZCIe)68BRu$K{8OrlZ};eWE2@$HcwU{n=T{E zNpgyOnS7;umHfE;n7pc@ilVw=4Ri`x1RaBRLuaAm5C=L8?SaanlhAUgs(J~u7TO7| zhE_u7pncGGXd!e0S_Pef)oI*wzd&Ed;5G~}_A?GLjx!E1jx`Q5 z4mJ)n?lL|#mYB*+3rwY^ylJ6nktt=$nKGt3<`?EC=9lKX=11mR<_G47<~Qbh=KJQw zmZ{d6)*05V*6r3U)(zHe*1NVxwr93Gwkx(Lwwt!+wjZ|3wwJbtw%fK>w(GWgwu|=5 z_G|X@_6zpL&Q8u2&brPH&W_F|&ic-_&SuWm&d$y{&IZor&U((K&Th`G&M7XN%kHwe zHn`rodb@kM748k5rJl8(9iAp)t=3sjh=0uWuA|o|L-dU#Xrq2_s{be`uTpE zf1-c7AMj7|3;a|41pf@b&_CYK^lSVDewE+gC;KISoLRl!(YO0!ym%$ z!=J*6h$^xqvMe$rIzBowIx#veIy^cyIxadXIxspRx+gk3);~5ZHaIpaHX=4OW{g|n zws;`^Gd?6SEHONhOvDq3L?jVQ^hmZ()=YLtR!=raT9YG_W0P%?U6PHHm6Owx{gU;Q z^^&KscFCVuw`7y#59}K@H2Gh$Yw{n~D>*7TEZIBRG+8%UC;1nvm8_Kfg*8v^PMt~Z zP3=hSOl?i=OKnN*Nv%j|JWc5t?9u0yUzu2-%+_cr%B z_ayf?_dfS1_cHe)_bj)gWM9eVl3gWVOMaI8DEU@0B=64q^GM#Fcjj$*Z$6pF^6|Vi zpUOM(!F;!}7G)jF+Ltvg>s8jStYul}vW8`?$~u$<%LC;|`P%X|eD^))p7;G;`^LEM9XD(I#++-;wN^4_CiD3}VT}1Fj3guy5(u?~w}f|u zodp~ci?olthkUqjd*N?|p~Cxxj|(3aHW%J4d`*8!e@K5$e?)Jl%Na7p4#rK!#Udli z$&#~Hv-GSW%g0*50$Ey?jb&mvSV~qStBz%5Nm=zQFH6lbu$Hk1?Dg!9oYQ4ZWgp5e zmpv@IUG}mpRQ9CoZrQuCJ7s^AHJ3doe_P&M{;vE{`J3`T$}1~NE6XZfRa>goR#~dn zRIRVtT(!DtV^t4+Pkt|ccfN{0N3cMUC73U$6Fe1c7p@g<5$+W35N;E07B&js2;W!# zsE!hStNvd7uDY%0OLe%qjcBc?QADgE)a2IW)nIG#YxdO~s5w~ER@`0ut`;J0sr^>l zP5hy@v-o{&l(?ffS_~C`uKiKlPW-0!ZEX*6CvmfQf@G>>wj@=uz?~r(E6I|~l+2fm zkz`AfBvT}lB)>=|OXf)Bbi8bj{D}Ot{G|M-{J8u#`BOPTk*^>slxmsU ztZq;{)XUX?dX>6XU8nY{LA6jVR_oO()HUjAHBN0(JJlMsQSDI&)N=JowL+~^tJNa4 zK^^ach22m{YhVJJ7085{3Y*fl(MGrB+UDEDwmcirCa`7MkT#T!V&mG(wrm^ACbHpdIX1Kn zVJo)@ZMC*S8_7nqF>Nv%+=jOucN}n>a2$3Vb?kI(aBgz0bH=!ay9T(1x(2$ut`#o7 z3v>lsS6r7}Z(Xz8zql8=v)pstbKEe`IL~;`SWl`a#gpin;7Rr*dEA~=o)w-b?^n+U z&nM3p&u7m^&o|ExPuTO_6YbsS4SRohp}z0l1wOB@(Fgi`zIxvZ-)di-PvcwZTkhNF zOAX8lFawhU34z2wav&p+6qp;B9heiC9!L+&3``6BDV-2l3hF^4xC#6nya|2-Tfq3> zh+te08(bbtMomB^pxn9sTvx6)H<0Ve<>99iN{B4tfdVe6lytrDUg68aSA}m2pB26- zd|r69@M&QSy$$0B{WBfPh-Q4J+ZneQH;Zl+T`xMy+RfU_dcZoy`jxeVwUc$6wT-o& zMPi>|cjmO`b>Vg8L3wR?eR$n?EoBhix3ZDE?`5CLI`NRPDHL5A|4|iD2@>i7f%om6^|5;7RQNU;v_Lqf|TS+Xp%e$RYI1aBoqlwf{|>O zZk2wLewOlO7vvY^=jCJtNwH0ROnpFoSba#nRlQxkSAAT4M7>{qN_|%SyZWelvwDa6 zqB>r)OTAOQPhFx3X`g8S(B9HM*FMue*51>;*FMr-*51}$(!SJQ*FMzNX|HK-Xra0f z+Ggz?Ek?gckI-l9=joApls-#878nJL08)V@U?MOdNCc(=(}83F2BZTcfk{9D5D!cN zrU8?I*#^GBV{jW>hOnWHG0F%r5=~SS#e_5Enn)&wiD@F3@=b-N0#k_zYno@CZ(d+t zWFBpuY@K4AZf&%#wl-KdTGv>&+YGj4ww*SgEnw5z8f+%p8XIVfM&o5vQk8Eu0|dGWj< zys^A46`d;DS435`tyo=AUr|@Fs$xaOsj8z@JF5;?_2W|nCxj=3M}%*MLq&r{v7#ZO z{-R-`O(JGZaZRdtsyI_TUOYwoLi|iDknknd5}rgTDU;MlDkN2sV#y-uLa9=!k?xfK zkcOqzvTO1ya=M~WK~p?ezg53f-%vkLH>+=|uc=?C->E;VudDB=AF6MwFR0(BpQvA} z@2dY$Lv&xY(YlY?Puh;UF1pUTcDnD{4!Ra?CtZ||s?XOG^f-N?o~X~$7wGYNoqnf& zhyJ4;2`m5>0y6;&kPBdeML-UK0%ikf00+!52n_;**U-+`-q_K&&bY?7-niDtHC3A` zO)OKHsn&GJgf^qh2s6@*F>f=^w9c?@vaYw@wH>#evmLVCw4JwovAwXJw%xEb*`C-= z+AiB(*&f=i*pArF+78$*+CsJ^j+>5Kjt9;$u5qsMuF8z{c{}?$ z`MUYK_|kpPe9wLNeRqA2d{2CT_}=&~`>y$3_-^}d_#XSN_#XIf`h)>_U}-=V&;`l^ zvOsk}6958+z>)wvP!-?@c!83DI#3y?4JZO-0Z~92&<2VFcF+nQ0KbD1f-{4&f{TN> zL0m8|n2yA~%EP7P*p7n##LlOlqNwFtBKK!(G1hTG-Gr_b%S*=x_I3X z-8kJs9amqXFV(a4U-crO8kh^z0JT65V;|#2qse42k<0{hfq9ZtD#D6#FFm z82dE)RC|&=(>~UoZXa)-X-~F4c06)CbWC(jcI|gPaM9f-+{fGp+E%E;IiOu__5rhxubU8%ZJb*+jkSTC3;+9u-Fl-J5`vY4DEmx*Gg zn&(@y?MVAV`yxBSKHomqzQCSkpJ$)pdgx-gOWdd2SP#Ji_hP(QZ?+fVUEp2l&GMqX zNbh;?K;Jmu6ko0{$2ZTvz(2$Pi$Bw!>YwGG>QDEl`BVJU{FD5*0v7{!12+Sw0_Ov_ z0~Z4K0#5@s0@njq1Ahjt1nvZOfN#Kw!PH<%aCVR#bOcuhr=dLP=Dbi|1%5H%dO@NN zL55PJscooZ7%+yD@w})fyBE7V`y`vd%jd;aC@STZ%1WN#vXCh*5<4Uu#bnJ)O{Qk5 zCRsB@GhLIUOVK6hQgs4-mA+CR)*Ap50025*u3@0jZK9iL=HIMTdyc)pj7aS7I2&M-o1?L12!SWz4SQ;z~Udla_3ny@i z6BsXxUKRCblX-=_0v?4oqT*K7hXv;atAhNXAb2@9fw7A*fIX1ipFN08<&CUZQ*m8*LzpQl z5wpb~#a@Y9;*)qJ_vH`d^EC4{3p65qjb5y;)wk%~fCpG@sxz%JmD;)XGJCnb(w^n2 zaFad7{!%~J-x=%*_5izsy}+K}3h+;`V{m@3I#?U535tTla7DZ_@l@RwW4-CHwaQ*? z7uflBinq#N=`Z*5{e8gR;771iur654WAHwSS(@p(X}TFY5bztCOeai>T$S!B_i1;b zXRU|sE%GwG@xH!be{eytJ#`p+R7JL?(WJ1;>~edFx7eHD8wif6SfmL8O1sFv6XXln z-WvZ=R?r=UK)yg{3|ki?Pf#W35`Y9lf-9jRVNJq@gsllX5_Tr+NjRDC zCh=|JyTtd29}@pe{FwMD@pIyr#IK3p5~nB4PpeD=Cw7}OXwrbp(#&(2=QHnRR!)Xb zDV=(MYVJ((tn@kS=4_sGXU^4MvgZ}f8!>;(eDQqa{P|fmSxd4MS;{PRmL|)W70lX^ zwKGeY9n4;yy&`*M_NwgOi~B6@ySU%t{)-1J9=LeJ;%&k0!5zV!!Ck@K!C!-Wf<+-_ zs5n#-V*T_rFAbH2c%kx8MW`}V72<~kAz`RGBns7pYD0hVFqek@;$W@}wYrx_l7^5j zqz^3(0U<-k7&3*-A zrqG(u+R(bt`p|~Z#?YqF=FpbV*3h=l_RwE^!dtz;_k?~6{lyjhK3Dv$AV3RhSps?f zs{35`rS5Cpx4Q3jKkCADpH_cf{blvn)!$ZsU;Sfsc=hi3U+eeO|5m@Zeqa6m`UCYh z>TlNHs=r--r~YpJz4{mRFYDjef2{vh|GEB4{nz?$_227%)Q9U2H5_g@(r~olSi|v# z-y2ReoNPGNaJu14!`X&&4d)x)HN0>5(C}x&$A(W0pBuh3d~NvF@V((jL%6}&=xTH~ zdK$frzD9pzpb=~gHZE^G*m$V%aO082XN}JrUo^gKeAW26@lE5~#&?bH8$UGu+4!;X zQ{(5xFO6Rtzcqeu{LvV0%xfYv6*Q5W$W5*$cax{d+vIEVHwBu&reM?ZrWH*qn^rZw zX?olAuIYW#ho(Q9J~n-7`rP!T>1)%srteKZn!-(AuYJ4r{o0Rf;cKWcI*j>WW6Ft) zs^I6iS|cNSBQh2@|6w%#e!i@N)I&Bxc0jH|?mPe zum`X=;OS%NY_o3J#&nNXk5l(0TwOTzYqUlUFwyiROMY?Cx3DJE%J(##}p z((=I~x$?`Rd6ao^^GDCGoo|>wFH4jq&XQ-TvbIc z`x^Ik=K=(rTLH9!sKo3F>K@UTZLXSa@Lw|>!fS!b&j?AjF(3{Y^ z(6`X{(C^S6kvWBpS`)QC>SEOGs5?=2qwYsNjCvIHIO<7MbJWwQXHn0iUPQf&dKL9L z>P^(UsP|DHqW+Bf81*UYbJUlpuTkHkzDNCt3P-g>L8772QPI)SZKB&ow~KBc-7&gz zbeHI^(cPnaME8vz5%AW5DYAuU2igo(yTn<;XiIZ2+pEYcn!BSN|((icpUPl}KpAtOR&gvg|o zCr3z$khCh&w>DB=A9?1^$h{HLcSYJyMXrgE5g{`|+J#8{Vx%4+dG#cDYJ{{1lUnQb zljJE4kupMBgvyOC=jMDBeWsT49{45*X==khDC~zA9aw8X>(gU7pd(j|_QII76Nq zVPY%tOnH(rQ=SrGVuZ{H$*N3wT7(RBq`oTB9$`|1$oZp8dHUl>Uxd`Bk!zntu8lA$Lgt^5^RJQj^(HBjS0^b_>XQ`d?;_>5 zBt?cMS&^bkR%AMp73u4e6=^3T<>g4*#uUY*6Df-H&nb#jO{yY2LK2Xw$XJ!CNInta zRH`EFZmJ?PLUJh5-yG?4rYSOfX^JF&nj$U2#8$vaTZE()X^PZPnj+JidgQ75Z1BBW@NRjCouB1~*89g*{ukVepc|^PZ%Uoey z%zrgo=n+xJJ^w);|DVnlW(@lR{jX#U)06(S4DP?4OUM)cl1-=*D%0ktwPujpg5bnK zle$f6%_K`R2V`y!p8J^uHY6lB?+UI-Xw4BfB&5&zORhPkd2=LR+>_9n zFBZ^u;I07syx0cL)^P8#)*|4!Ri1jwpun(2LN^(5ukv5$$jr zdOYex)cmJuLZcyCNxM6WIaj>{i zaif2FUbp&Or^QW-n-rHGH!W^KTy`8Kt}u=s$A~M9D~t2Qt&Zz8;@`XLm?T~rFN>GQ zE8>;$s(5w0CSDt_i`U05jR)cl@y2*lygA+yZ;f9TZ;Q9bJK~-3u6TF6C*B+Hi%3-{ zED9D4YXfTwYX@r&>j3Kr>jdiz>jLWv>jvu%>jCQt>jmo#>jUcx>j&!(8vq*!8w48+ z8v+{&8wMK=i-E<$;$S0S@h}){By1FHG;9oPENmQXJZu6i0hS0$f+fRJV5zV)*hJVQ zSUM~NmI<2-n*y5(n+BT>n*o~%n+2N3xV|Np!0 z{kQMB_buta&(%97B{n54WkgDR3M^%0%BYmlDPvNGgE3$%7zd63Vc3+Fd0k%Q^ASgBrqM!05ieK;52YLI0Kvo&IadzzkqYWdEk6-0hk3Y1Q&tXARI)1 zC@=@afq7s)NC1f-38aB^un1&=#b61@0@)x3W^hVyT5x*smmo5T3QB{rpfacms)L%KHmD2g zgG+-z&=B+ly+L0v7+fBNwnVi=x3p<#+tRM3eM^UyjxC*9I=6Ib>Dtn*rF+XibDAI2 zGPq?(%g~l#EyG)4TK+R$@>5!-woGf8-ZG2Ss8fED~FL zjqv=8s)dnQx*!r$hejf5Yb9qvjsLKX4%g;Zj0@m1o_{*{=M<`-y6UGS?q6ppMSP)wzmIk zI^^fCSU=a1$NvM@k%uk+GotuEpA-L+V*5XS_W!7=|Hu3PqbmP@_xJT5dHu_L;U9Mi z{`X}J|Mv6$>-!1+>(7QlA3)AR??ZY+pF##hn<3+%FCpilFCdGd?;#hV?;vdGSIA}P z7s%f-u==^fEjO7``1_*FDu8>E2-lZ za<`PZprVv|nwo}qShWxXqEAZdEeRw|vA;wnzaY6-e6-r2K{<@bc*9-AU0GX9FFgaT zQh#yYLB!^jdjf>b)Ya5&MIwXI->#yJX|3KuL1Fr1bmr5tp~e;VuE^gj+T)*729~eY zVezjO+9Wrm3&6Z?Lbjp(HKo3;(5~ zfYFJ3lpHi4%sV8PS_sbduHEK-((%Sw)cuI-7!sz@++<#BUTkS!%p$wx#k#A0pl}>| ztbju{FfNugm+m7hpz~3iP)C>p;U`35&sN@1zJRo+^1gCCy_ve*{s$)?a~R#7e#maG zO+*YPO;YreFO|oap5#<|b7(}&S1dz!AJ-KgO=7TTh?_0_C{O4^Di7+A%*E6=BS$#K zblh4(zO9T^_wXFTI5YtJC8e1n<(*L^D=xtsIOo)7=r_baO1>0jDIJ*H+6IIHwTL^L z$TeN0oxqG&PEtN*ZmXU~zelhWUloLaW7H4K8Uuv>D=kY4c={V+r0?tv@E*=OcbB4m zm6LHpi5VoRLyBB3Q(#Dpx%}juSftvhqJs9J%#_^HvOfwYVwdrX%w^O%`YKv2Sy%SK z@=(6XJb+kS^p>pWEO5$=9G#W^Msya3XZ4^iz~;d_*F2@D%5DNu>M#r7eI$y;e&9oC z12GiNJ@a5P&pSmX4eAh+iFD&p>1xJa-dxlc6kF52U^!tgm1C{qcQM7(Knq2+$KjtP zYGF_KOZiarS5!Hx5u-J=ah!7=ETud1OJ-I#BCiR1(4Xkr;2+ZN6i92PmBb)274DoA z_y)eKhDusWdhHmD&crU|325;S1rAm;qeN?MpYxu(Q4*(1rM{7D=Z{t%tUm1o%?k=Y zRhr6s$wGlS2i&#V@$` zr6^_|;WOb>^#*!x`Wo6e+Cawy`aUzZc8mZ+9fhBV6|q+#)2Jg!8umkYx)IQ+$qAMi z$~4(gY9>4f^VzbCc1-q!k-(XUctv@@y$fWLPiZjZSXw@+P)RF>plP%Prd~)NQ7HOO zNJ3Ak`Hh0HToddhyT~d5$5h6v6=c<}5xz&+Eb)18#D~&n=4{5`+9MTp+y5N1=^S9u7>*IJ|_FKpcDsd>x9WI#k$* z?p*9buqk^`!?;A0lj>p=I_DYsTdwneSZKnF?4=biD!u^|^F+dIBau1N;g!{sVoR6d zU)Q`8PH}8!uV-|Vw#!4IhC9Cyn({_tD@;WUsrylGz#bONvS?|?u!HcE@jl!Z(?sT1 z<3z-$y@_G$#?;i zFGbY$Kp@}{=M~QR%BSjfBo_ahv=b{VTxOpod4PJtJA=7wj#iE_Zmi&XZgCdkwkmcr z*2rd1Q=E?S~eE*1Bk)?DhrZ!aWakT6#3RM*uk83=$#TW zZcw>|qZcp2|A4oU;|te_li*O|T4t2?4Y9N1ta~H>2QSXD5j9l07s$dtLJ<%nvE%SU zMxBgL5Ks?ME#|qT;hd4R-4WLjU74laHC(f3v#vjR9N$Y-n_nYtTN@aOa+KUpLdvU* zWoV6lk+CaYjl2#Y;+#|=k`FhYCaeN}acc2{q!z3ow_EOGAd{ge@z}2l^raP~0sMK) z3(S_9fJ|TX3d#t;U0tzNTdJ5{+3XDQU2US9L@`0XMfAO%4XXI*QFxeGhZnQjgjl*u|UL^cLEzQZ1 zzLEY$Urc+;*jH$zbSlSjvN(x&E_1BnN}ef~Wq8iMV_AalFZ+Zk;qtM89HH`@GNX8( z$bij3biiH|=nYZMt(;w4wy}qv4o)`C$h`nw;p@ovD~c)Kv5oMttU?`{!ZRPiUgwE4 zGr+N2GhqOEr9_{z5s_9o73;+eM0Uf9v2gega(>mvJhtkAtV(;n;-omYbQBq`8J|-} zT?SvnNvCdBT*JaK(Z_Gueiy~YW}pE*CI!)LB{ZWF&`*f4a9;k@J}Tjr5&+y=JjH$d9nDI^CSjF z*~hQoRANr#H%Q`X_fehXZwv0JWKg+7NuDsPokE&J1$Y&@t>aICNW2xlOZ6ex6= zr$?!}w1NAT^e6H*z8!HMm(0=d0abBvE`AFTgDJNzc4n!2l|V%!%RkEM;3qBfxhik_ z{NHlh(%;yh3s)ls$sbf))Yo{@q<1A!G@ik?O3h01bBwd*C2ciLBls-B=_WbHQrDst z3T!ctDWtK5FKeLOOOl8B^Y~uW6YAB9Z2oZMDfnvsb6_DcT(GFNj&Z?y5)R{%^`n-I zbR30`Hrz5-x)v45MHkF3c-7o##4YG~N-CyU7~kb>6((#l z(aT@%x=y`Ov6nxWDDs|E&LqmPPleOr8#wv0i_}tDbAb?b$2iQC=y;2Gj0sWJ@t9~N zZ4Nj*eh}(nY8#=#_*puu(QAryf^HLE$D~Yif`bETMt6oca($ zqaG)0WWK}(Yn~{CRvYP&E(5;-*NxU}d@aM_gtbkG=j6SF>Fihxvi4BLyy6z&B;hB< zc9a=@S+JkDx`wZU<7fF~NS$X?p|X^YYgaUn_9piQwS;DWu0x_eS)I5f7PSm_hz`hV#m_izi%BIrQEp6QvAp6e z__p?Q#U4@bvUXK)=4or4f2v5Y#45XE`xj4x>oJ?r)2lU@F=!nUK%7VX!9P{AOuU-2 z!|6jStDa(?BCxV8%DZNs;{<64CCD8>A}})KVf!M=Z@j~j4dN3hdGTg(nIaypF`u(a ziZ;O88k*<}WjB#7{@L=!guUjXG7Vfor#Y^YJdFP6L%QF2tI*vlQ>$FePu2ZgY4AOk zyZ%$$Tc}yaq1dz3EtPMTADyb;Vf!S6S9%)7=kcBEi5mA4%p1Gkq~~p=(J=^( z+;0O7)hVvm#@R+GNGel_a>(%V`Y$3Tml&B;o?Rp0h*pD=<{`s}?7nVj1CRs$Jpzted79%++{rOLX`> zpsx~Iteenq2B3dGI~eTwYJke(HV!g0YeE z!_loMs2u^mw0FfW!Hzdn(?#^2npl=!-dtTm$NF|t$1&F`8}qJ%YdJAxcL~|f&IWSD zdFF9yVMP=C82${~mbalOhjb5}Q#hLWN>C(ZR?Mqup+3VRvCHkNEJO0QO8Rl;zyAH~d~samQn7N68uc{)hin7Bw`(l&F=s0A9+iTafvJ_Zv;0~* zPW?NzinOnKJH3|nCwV}=)H)oTK}67I%a#yIa~3)lJJj4p%6;bDiaRJdAw<2)Ayjaw z8|-@}z2z9LAJv9jgnI!&~O&+i=wNllJpYwvFZqbAQs}jbF}Vx`DxX^_@0{`;#@R}`qh36 zu2B}Mo+`JfF5}Xe(YVs0B5tzbPEk+ZaOPxozv4mEJ@{z&5mdXfi%c^|fI3w?AJ1eL z@w*lDkRyZF7z;$RL{_iRs34NyP(&gYh3`^boZE+U#2igt!gJsx&)uX%tjGehnfWX6}UXgsEyyU&eI|%<3euf-! zeS_Bl(dec4wGu4#4E!;p8#P0DLPb*=wf6EIrMn3iB`(wn+GVC*mZ|Kk>xHTzDDa!j zmGA-%0|nEUnG>7~)ugH(jyH~a?qfwVc^SPIceJWhe%MdSBN#iv4`PNPXbe>GrJPal zK6I~_O+A!PuBuQpabFdOkS1kR^-N|a+QZo_*@I-{kje?Q=%SOA&lOqBa$*d6NjY2h z%#q_fN-s2Sm8)`cIP<9q(q(z^h!Qwiewes*pmoXTX>siI~WIWA0f@W_G1stx@OgAn_QcMuvkvDR(I4py)GZD7==f zk$ojR%(<0288H=6;dx&mVP?ppC<-^!0uX)!{se~5^AvaKKXT3~7}jfum?gt4LGgPe zDsN-ye!>YrDXX^nRj%448o+6GK9*eanyTeQT+M7tgOe=66UemAwHH+Di`(Ulk@l#H zTLN{=q-N(`g)0dnJT{M8IgGZ*)ZKJec3H8G`UdlcL$Z#=Ln%DYV%$RdSmQXs(^3LX z#(Kl&mQ65pFcjflvzO_%RINEZZD{wXfIJ~Ihky;}EjlXp1#$qfw&eYR!p5}J{ViMI0pZX%g#&U58-nteH8QLJ!yr^Kg^Sb?~Q53gZ6$THD)~aFmER36N!ji4_{B3 zOq;J5WI+~}TDJH+Oc|wWf-=MctC!Ynnz8RPXuQ=OTA+6gp$?xOVsH({bJh%8mZeq?o%4GQBvJaBuif*dQ_~D4Li0Q<2 zlCjJ|>Pjqve^{o-LHHIUCY1IzbR_Z$*_h#iF{Mj2TE;iWZpjpEYE7pluN)6@4oY8D z4W;$huHXi0dQw*~n3xANCFgko5!-0bh8J>P0E>wC8Gc1W@pNn&_7uB_ZHLdMEx-&@ zbVY1HkZ_%-VJE$)q@o4A0v)8h2Kp3v$}LQ(9$o!I$SazXn}LmW)Koxf$|zk(dP+dv z7k`_gDu2yeQ;OusDpaiXx(Or-d~3cx=b^nn_nX+`>`pxfb|t34~E zYY0Lo(z##qS+Ei#aCkhMYk$$_l@`J;OBtdkz{__U{i&dkbq?7RTWA=iDyVMb zT8MsyU6l)Q=(yVn$GDiH?!*E@Y_U^Tozt^+0Rvm&gU@HfwSWtO8BUu_Ut{qz4$68M zJg7~@)tE2Z-wTg$zH#CVDB>FRMnxCL2)56>iSdnmnEU`SS2w(fAswpn)jyRW=_e8uxfYNEqqd6TM2SiTYLrE~N3l-wZ z*9^$5bz8{0fFmlL^#6FqX()?$Vmy!~5L(VF~I?5@AsIO#`5N*I#vi~pLYenLfu2p^tOqs%oh(Pq0OmL9NN@r;6FUyRSAnna zYx))7bsM=yofoiP_zs|B`Ce>C=RMvR?s)+W-lw{kwu$(W1X=2LcXOI}`#Hbhn`l#o zi*aWTL?;AIXK|8v0zCYo@G-nbVWsvivRXvM{csWam^{OxlC9;}!!PT)7anCFBWJ-+3Radr1U3Mj z3>SbX@^|TJ=~>A{C0ltvcPIP3E6w>0KOFnf{+xT1Q0mDf_9a`KJj`1~GOH?|2ii%a zrMs2!Dwv9?oUc7=rW$`2l#`uqPIXMtJ?vvD4RKihJAW7HjI)#Ji^hz7&oOW_I8w@S z^l1JP(g4JAN{Ad!Oy+N}9I)@M*v-i(&9Zi6p2EDxpb#glZ-L)2V$vb=Bd!U4NT4Yp zX$BRxQ{WMI5z{SVf5*b1rF}}(P8~cszuDOhE+y_3X$dcZy;6PUJshAoXn|tqTlVBU zBd#EKC`@6$t=J;dFRkKUB3#j_+-w8an+kR+twYU0t-{H5LCgo5!~8Ng58Dnmqta43 zKziGJK{2CnG==JXQ&cZmYCLRQZQn|)6L(?1%fGd&O)6xSE&oEIYocE zatK2)8P&6y(~3`ECA>|9C1xXPG;TAt7ei4*GR`q}mvdBH%RO-m>YI8M^C@FRS)E31 zV&%qI6aoS7arp@2Q|dtU56)C$Hwu_%DhwAVMd+39^qi_=AQ)GyAo;h9TT0qR3m7!gmUtm z3O&3JrHb*IN@LGpj3OQ9U$7hm0*U}Lk&%oYpnWV{DT|WN;w*w6mt@O62)dRy*-YwG zqzW+;*PcdY?=wEbG&6Qdu8{}PcGBLlh8FqPep@jMjJi6%lx~gdpOH# zCcuvh?vsOdq3(Cd8PR^(8S6WEf5dP_G4^gj3imXyT3lsz%Lb8_VK)mrgsDY!-usnf zi*IR?nPV$I1Gj)4mM=B8c%|h>s7=D;+$nIJ>MgSgeL$07{A@?nA`s1-0#U8kMjc!6 zgW4P3ubN}tM9()Kt1T&3k&fy#c#!@oZ%0M95}4zMInPMJL8R>r$52_c8&nJ?yEv{2 zs?4_jk~32%<}KukP>tAMj4k+6rAN3#!nh@u;Y-AqN|A(T@~)*gLbUHn@mQ0K@Le*6 zvR$&+l+KI@wj4paofJEG7<|By+ zB(8z9_23q%GZA5nlHBXEcm#I&EzrB;L2m5T6laklD!hi1Eno$OPmBWEe37*#!whjz&t5 zbC5a61xPrOgRDiaMm8XukiR0YBHN)}B3~e%AYUQdqdKGRz^8HY5Ddgw#PZ-gPL;S6 z^`qbf==R7_eehq=ws+>=w}#c7;G3|7-|@9h%v+( zV1`kKv4(Mm1Vf@B#gJ;4XqaRu!PH=eI^&$N&SdA-xn+57 zd2I<>+F0YP!>l8$$<{<`rge^Wp0z7_Hhv{~9eO=_AG!s59(@)427M3ns{D-b1L;rH zcT}Qp2L?*c=f5t<#x>zq;3Rp!?mF%!?gs86ZWrzY?ltZ+?h)<|u5(`3 zydSuiI7r^Gyn%VY%J$0J ziVup5it9f;7cVJ#DhDasDf=kH$c@{bw~L_c~v`| ztGc8*soJmlRdrvr;^)rRF4bezLsb`bSAK^2jVe<;Ts>KxtDd56QGHk8)eF?cYPy=F zu2HWP&QVwV+&!*QtyP=VBJ~EfU#(W}R_|1AQeRRZRNq#&s7GnqYQ|{#X{Ktzqz>e% z_*wXLSq?rQpNH>`i6g>@Pivtaq1~mOrJ?D^X!~e$>9gpa>4WJpKRq#G=_I;} z&ZRr)HT0eIU33rqSNZ|^dHOZ_Bl;iojtm$hfzg?Kkvx#!lT60E#r(iL#WZ8SVR~Wv zVcTIBVbNF&7K_cr&cViFaacTd1U3b$z}m4wY&n*W^?Q1FYz1}|_6l|nc0cws_6GJQ_BOT&y9Qf_y@Nf6y^9^36T-&i{J{3fd5?|D z8IjXHr)SQnoU#8GUw7HwM8mdg7!1cyIV4mnT$&^J{hYUHJ}32AgNJ`ySux) zyWY6FOIhpYdEaFp)}OdOT-$b>J1G41p~itKI1B{AAs_$_1p9yx_^*r?0rmicKpdO_ zUIeFr8^9f4Dwqh)1LMG}U^W;7-UIi5dqE$V5554uf+xV{&;{@%_zJuXJ^}B5hrzet zWAFp`5j+i^0iS`N!AGDkvl&zYssfdST0^a%Vz30%7U~SaP(R28b%G{AZU}*JXd>i< zk|7o{Aq9$oVxei!Y-k;H8M+4Df&PI$LocE4&>yIbtCFjttBI>R*^sP5)+D=;1IU47 zA95t=Br%dC8FD5bji=*Tcn+S5=i&KyHeP@i;#=^A_#S*Uz6#%fFTxMt+wjBqMf?$d z3V)23B1#i&i8e$RqBYTsC`U9VU}7-QlNdyJ35?JQf>4MF#CSp?LWv|IpO{O8%-lvC zAwsAKDv3&=(x{n~pUS5SspZrfY74cSa;GxXn&9=pn}fFnZx22cd?5I2@R{I?!Ow!< z20st}5d10lYjDYskHMuwegyvvt{+l6Bp{?xNVAZpA$3DqhV%;w4CxfoH>6t#5Hcym z6+(x2L-dfikj6=Y$kV*mNGZgVFXW5)Qofw8dno}Zqdm7kqI zJKvY@&rcw;$r)r0xrCfg?jX02d&y0tBmE+IfxJaNBVUj^6Ax1-sH@aDsx2mZ7>NWL^YDo8@+t7{a>U34QJ>8M6L=T|@=tw$>j-jLJ;j~OorgP{5dd*+i zbQV36oD1DROOt)a#Fm;($%qO}8Q;q4!jAz1_)r^l>#%yCQGoP6| z)TW&0{+Isu{*V68{xAMt{$hXWoH9A(aw_Ch%Bh@FC8ugm^_*Hcb#vr8ZKns$F%eJ=APfSBup7Dyhy^ zeJZZfYOLx}8)?VXBkEnXnzm29uh!I_sCU#GYCUb0T18u{cGNV@qAA)&jna}ehvw9} zYk}HG?Vh$l%hT>_o%ILW5^b5bN86=s)2?ZowR2jgc2JwAx%JWd7`?S#SO2bc)qCp` z^iaKnUQutar|IeXOg&N$)?@W({epf%U#9QSx9SJ=fAlx{UH!iPR&QjqG8!BGj9!M- zAdLiUD(1&#V@t5@Sl6(kf}ZRn>d7vIxmVP9m$2 zB4jag5!r~WL#`ovk+Ntx^bS%1eTZB~z9Uu8ifDJVCt4G2h1N!gq5?V|1<`@18x2EW zC9Fi}pb=;^x)*(kZbUDlm(lI$ee@yv9=(b_L9e5?(TZ4otRB`K>w^u&>f%js6yvZE z%!WBIHx^b9Q4m=WT@YU|xgen+v0z$3YQgk^tb$nuvkP(x@(Sh@6c)@am{%~rU}3?c zf+YpZ3g*Qvh+7o5ByM@!inuj#>*Myu?TtA5vqp9g%*ZpgeHV0hNgth2rUX-9=aklD|A`t#n7Fh zFLGbzzRH~_I0RA{D@ei;Aw`gd5Ft-kCWHygg(6{%FhhtCVug1?Me({&NqjD>7pjOi zge}5NVUuu2C?OsawhIr0YT^OmxA3n=OpA;%re74~m-fOKdK+kiLtprFUXwsifqR9*Q+2Q|c$t(r}58rb>#GAmvH_ zdQqpP6e(3YE2T-iQ9ZW~kkENYVJC$}i?QGh)wDV~f(zM`t!9*}0tOv&h#|KXho)MfL zoEbbTxPqs`Fn5SM&7I{ga#y(oqtM7P z<{G<<{l+$9m9fEiW}Gx?n6=G%W>>SD8Diq5XpS<2&GBZW8Exj7o6Ifda&x|U-@I%- zHNTrpgWCmrjRq!f?l1?t2f0VM$GXS42Sg2x8XYw*3i~TbqfzcCR}>wkM+s5UQPZNP zM-@aZi`o=*fjUnOr2pFvM`#bdiY~`gWe8>kGnYBVTwuz%6B6PRW_!PQf2m(0zD1OX ztQc88vO#3c$W>hD$R?3ZBl|`6j~o!$JaS-UU}UGrR*~%^Es<&@6zPg=6A46)iJTZI zN6v|y9a#{W5?L6zAaYUU#>gF!OCm2v-iSOA`RebI^ry($QLUqzMfHrDGw(lL-23n{ z5mh6;hnJ72@K?-k7|}VRO+>GVz7ar#En-Z>hzK-7jBrPA5%Ce}5tAblB4$QpN6d?u zAF)1SOT?at{$WGIEMXJE#)e^G!C_HhQ^MeQCZ38PJ6((qjW^??;uGWJ;?IOX4)2~i z&6n&;@y+l}_s#UB`!ao5KA$hw=l2!(=J*PIi+qcHt9&bcYkaGH>wN2d8-1I7TYcMn z+kHEHJAJ!-dwlzRhkeI=CwwP;r+lY1O0>jL;S=2 zz5FBnqx_@&{G^}q6Mn|;_OpJ@&-(?x=$HJmU-5hW zy5IB%`$PQw{ZAuaMV!dF67@RjVbsg0a?xXm($S@&n?=`&?ibxB+7^ATXi2^iof@7I zK07==d~x{Q(EFjC!deuy{6A{U?nT{-dKdLA>hrh49Q?Py9QnWM%m1{O|0ys3cX#=J zSC?jyS`<<=E6f*`6E;6=ZrJLuMPZx5HivBwI~=wlY<<|Fu!CV&!_I^~40{&#BXsnP!ErP1@FS4S_8UK713noX5b zgHtD`#-}EwE=)a|x-E5g>WS0`sb^Ckran%6pV}zxSL(CW;?#Gk0cj1>YNs_$tCZF= ztz%m6v|(v{T1?uEu*V4-!#9WDj;NJTC!tL+UXi>t`Ec@<}^lXoU>NM4tGBKchM z=H%ze?~^|yzfS&`d_K8$%H8Bo$@h}4CtpY|ol-vKTXKbzN-0%SDyFnb8IaN}B`~E+ zN~4s)DPvQ5rgTebpVB8~NJ>&l%aq9}aVaq=bc&HOF-1$6k>W`4q{ODMDM$*JGCO5m z%JP(pDMwRMQ>LY?Ny$x_lCmo0amwnHEh(E)PNiH**^_c1Wq-=alv^n;Qtqd`NvW4w zJGD5aQflMWfYg4e15a=L z=@Qc|rbSHPUw?XVOtTm`28|gN6BaWuhKQkKw3wI}cT8}M8Iu*WG-g@M_L$W%b7Pjr zOp93=Qy8-_W>w7QmjxgtsL7twnJ>2*uJsD zVu!{SCzebql~g9_RpQ9hfTWg5t&%z?bxmrQ)G?`V(txBMNj;MWCyh)Rmoy>CnuI1% zNn%o5>a^6<)a$ADQpvQr@$=%B#jlHBAHOj^Hg+7B9XmI+Ft#XmVeH1(-LZRO55yje zJso={_IT`(*lV%RVzs2@{J!|J@mJy>$Nv-mB>s8)oA{6M-(n;FT3=)0=EQ9;+FTTp zJ~iE!o}IoST}W5c$IKizbA0xHbbB`2oy}w$*;2NiJvlorJ2*QuJ1To-_O$G*?DXvH z?1kBjvR7uW$zGeiE_+M%=Iou>;LNz9?ddPm%Vr=MQpT3_ed)W?ccvdsznXq6{bu^z z^t0*b(r=|7O@EdCHT_%q!}RazKhqzjSIqdGUMHhzM)QnL8C^13WpvHxpD{F}Uq-Kt zK^fgL#$^o47@lFvuxE_VFfzh3Vl(uN(2QvrnHj!}tc=2pqKwFyNi(OT_z(X+EdboPSI70dfDkQ@=X<-%kBME&BbUdqoe5o)2e|i`p&fw5Z!6utl#HWdkY(1OylXWZB56k7UQ?rRAI+5+u?jzDK17zhQr z0^vX;5Dmlv@jxPw45R|-Kqk-~=m}&4xj;Tp2owXQKsite^aiSdTA&_i1e$@tfgyp3 zfk}Z=0;dK}3rr472}})43!EM}BXDM5dSFIiX5j2VU!Xs5ci^7D2Z0X*9|b-Rtk|Yp zQ2C%*K~0021qB4P3~CkBIw&xxO;FpQc0uif<^&Z6%?&CF`W5s$s5q!w_eI?^dSvz} z>@l}TQI7>Z4)-|Q<3f*%J+AcV^gZgk|NGPL@4r|1QT0c)AJu=<{89TyogWQ;H2=}! zN5GF(KU)82`=i~D_CJD(yA}5+?pfTcxOZ`%;=aZGiu)H2C>~fmsCaPkkm8}m!-|I& zk0>5lJg#_r@r2@u#gmFH#n$5gtpEJ?_Rs%s`RBiPe;)jK^#9cIr7UGEWi90_)h)Fw z^)1aUEi3_+R+cuF_LeS|u9j|=9+qB~K9+%&p_XBm;g*q>(U$QRn+3GEET{#uxGf$F zYvC+{MYPBk#p1Q77R{ntf-RwzFiW^4!V+nTvP4^AEU}i!mUv5oCDD>(`ByZaW=XM3 zw`5szEV-6^OQB`1rO2|#veL57vfi@6ve~lLvdyyHvct05vd6O5a@=y-a?Wzza?x_r za?5hta^Ld6^2GAg^4#*;^2YMk^4{{v^3C$y^274WQfw(vZc(Ylbz;I?FoS>bK@t^Q`&S0&AhQ$U4uuz`D@7*t*KP#=73R!Mf49 z$-3FP)w<2P-MYiN)4I#L+q%cP*Sg<&zU4bB=8_*r-0rUiV z0lk4fKwqFAFaQ_?3;~7$!+_zyNMIB&8W;zR2POcM006K7cEABR0SIsbFn|CUKma5_ z0W`n>ZomVu01pU&2uOeoD1ZuRfDRad2?PTnKqwFfL;_JjG!P3+1`>fJUn>wxvZ24EAg8Q21B1-1d(fgQkZU=Oet*az$f4gd#%L%?C+2yhfQ1{?=Y04IS{ zz-izNa27ZZTmUWtmw?N_72q0h9k>D90&W9$fV;pw;6CsV@BnxSJOUmA&w%H^3*aU2 z7I+7|2R;CwfG@yT;2ZEA_yPO^egnn8AE1Qoe;!3!XlHZCf2%U0Xd{eOm)tLt7(TV_OqjQ(H4zb6X2rOIs^jYg?eLjjgS%ovppCgRP^j z(_guyt1ZaZ&DP!4!}hQC(A(C>*4Nh0*55Y3HqbW6HrO`YHpVvIHqmCWS#5yLZgbdN zHrR&PFdJbbZIq3+F*c8lwedE=CfQ`0ZVR?W+v05Twkfu$wq#q1ZMtoSEyI>+%d%zL zX4`zWJX?WnuC2&6&$hs}$hOqB%(mRN!nV@3#&-TFf(DvB&#P-zo%=X;&!uHDc z#`fO!!S>1a)mCgPV=rs3Xs>LqVy|wmVXtkkW3Ov(VsB<|ZV#}xw70Sc+S}OM+B?`g z*}K}i*}L0&+I!gt+6UW5+DF+(+sD|)+Q-=^*e!Oe-EMc-LA%Qi+Yviz$LxfiveS0A z-D79%yj`>_cCTHvYj(qK+C%K2_Aq<6J;EMokFrPqHPz$n3HB-WsrG61WP6G|%|6}! zucDc5&#-6Ov+c9&KD*zZW6!nc+4JoM_Cot&`x5(7`!f4l`#Sr2`)2zV`&Rol`*!;d z`%e2V`)>Ol`+oZY`$79*`w{z5`*Hh8`)T_b`&s)r`vv?WIj%tpYj#`e|jyjG8j>e8Aj%JSLj+TzrjzC9SM>|J* zM@L5|M`uS@N06hNqr0Prqo<>nqqn1vqpxG2V~}I0W4L3aW3*#}W1?e{18_JUumf>m z4#L4XM2GC~I#frnBis??h;hU^COhIB364a^6i139)iK?X;mCAkIkFwI9J3uhM~)-c zk?)w}D0CD#<~tTR7CV+XRytNWRy)=>);ZQYHaIpqHaRvswm7yqwmWt>b~<)D_Bi%B z_B#$a4m*xGjyjGx&N|LH&O0tTE;+6^ZaQu|?l|r`{&75TJav3>{BRUI{y0iFOF7Fr z%R4JO>o^-Y8#)^~n>brKTRQ`t9h^bV9?ss*KFstOQmDtAJI(YG5s}Hdq&|2i6B0fDOS$U=y$@*bHnA27rNJ8?YVN9_$Eq2D^hj z!Cqi*urJsT><tq;x9Cv+;M%|Gas8%Nsv4v#)*M>-w&>@?g{vm_vwW(N-lz4q);j=;2Zb zvQiifl6a+(WS(JEnh1=Vupeh()ObiRU}gGPc*ycFhQk(mtzDV2uxx^jfPyli17we0 zlX{puc%qQ2KEzFFSKM|IV26#E8OGk5zRBh)*p6g^UJJhzM&38IsqZS%j=zR^3V#=d z+#9w@d(Ps6A&OOipb=u*=K(~qq4{8TV*&bCXj^aBX23b@Jpl*{inI}Y)_b?fe2#vP z`H1xwCMXn$t}|z?6wpOtLr)JImq~|=YwA@Z>Ec%-E=C+lek}6S45+02isAq#2sW04 z)l^}@ae!|Dx_u}i3BZerodDq`*iR||0@Vxx>VQzlFfhRpQUgV}vx;^E0%TqrV56e%1u3mn*F9K<;scwZWaj{2dmzlkkLj zgF+dKC8Gc-ndLnY_Jn(bm>)4MXw5s_iQ9?rE~Hj^sfc{m`GEGC#1BpkTp%@5 zL_EuWfO$;>#L$OBCC!RlW;G6ob^G<^^~UwS>8P_kW&>PTO(3{Zvt~a8$ zq&K6tvDmvfyEwYIy!dr-a zbi?$Bca8UqcaQgq{}}I~=DP9@D{}#{lZN<2+T=s#$WRAD=2BS)M&==!7r3+EB)OKw zpxLpJeVXu0*_E+V+oY-%=%_U4WFJUB9Cv@;1>)*qA4xwR_t@(~GXd9j)Y2`9F5xY? zFDWlM0FRa564DaW648?WlF$;?lKK+Y(uXCgB_QH>DR2pP$zln2No$FB$!!UBNomP` ziFC<#32}*WNpDGTiF3($>D`h^sNtVG+fk z!UCGz`bxHV&kcPjM5zj3-kP&x^m;ABhVdYDnIp(sU<_Npm79|RMW!bqMp==FhT)8c z5qgGUAD4sGgh{YMWVU#ODH4L|w^)S95<;xD##}}75H!H4TLi*{1z0n|w*7~$NO`EX zeJl0Yw*#+Tx$=-_LeRH5&Ott~-8iNQ+!2|*DqHW*m3;`i$xPwvz)O9{TbSqaKKR|F z>u@gMw!ZIMl;`R`#NEj2=x1Sf1ISx4=N9)YAPjM=!eEX5ip_;{(t8dNmN>RqICo#> zRt-@9^acbEMcoK>8SvihIv2Yaf5d)-2}F^IDD2hPvOV{>M|>m;MDK}|=q=owIXAc` ze}w;w`WzZGFmz7cj-ZXl8lNUNPD_{+qsUmwr;S(@@m11+t|e(zF`yK7p2R*zK*pE` zR#Byta30A%(o;G#=~NNBlz5(;G#En~iC!kDXcWbofRBtcQB{_S7RFM60wvTOB>@K| zS`#JA2Zv}27Zw*6H5nK2f{v}2_KhVC6JA_6Ev}*fUf3X=o1$n+u&T^_66vTa1N@k5 zW0KQgS zQ@Xn3SEFxtDKDuWfV32!u+(TcbTV~*IpDn(_QU%(--`EVuMQBe z@%#|KM664?6t~Ta9K5@xdKq`(@`c4Sv&0AN*Mh&10%D;u5)kS+jRV_j^xuR}5rGOp z5Q#an1D9r!r?8A(Wy`Yqc|+1q9Qs)7iF&OaKyfOWK4neF;-G^?OT!z(cffT4 z$1z-RILBPA!N$Bv=LX47nBRyTBU?t^m{T^W8`^$C|3--3-)SLMZ))iB ziTfK+REXJ&Wci%D8(}fffV-^dL?A>)9u_||Y_Zc2^i-sjh1g{2h0(7VH%y}1E34Y4 zt2*GxL4rQi-+;n_7u#=HZ^(faJCagw?er!i;k~($p^y;zmBs62Q76(?ink%3hrgLS zHhcu2GWu4`@6o^dy{!H;z%SFD2z$YaMCbQUo7*;cEF+%qc_DViyc?(m>Vk@pT-RxK{(YJDle*-Gu8%E|EZx&?e=@G%zXHslvTS{5QXxulrDg5XAAQ z;}iRsw^Ln3PJ8o(!@FEb6x=XI+~6u)Urleracw^vr=pz>>86c*0V61XB1ITTG$|Dy|3`DB+5u;mUU5 zQdzP*xYFUcTe1kaO0u|fvKY83s!ZEaK6D|94COdF(e|=@$fw)LYE%Q6_YWEHpoHz_Pf!iCG#PG(>o@QA2}xmaGkNhU%Ya^zmw<76-{J zIU5oTHJoUK@N%Q328k@$8}gT>P8hs!yQ0MgO)aS#3WX&p7_e|7ql5IicLq_n8Qfxo z)$$mpardLq2j#Z$+_KxHO&Du&m!ioB)wYG)65BOI8K`i%qYMTew<#~O@1;TXP<)A~ z!a?qBY^_iZnG9z3l=M-6m1rb0iv6VajX5`YYK+JlW+ep}zD)3AUe^AN@GHqDvR;f~ zqo=?eW?qRsQM;x8lNvTgyQ}F=@;a14ErE6E`m(HEHBhN&tmcbyoW~^oRjk{P!$eQ8@ChwH4(I2~1?$jNHqLNe$ z*vXM&^t)P@B))l~as^B?Nw#AiyS|rvK+>?hDYHw`Dj5rN-O5*6q zF8(F!Lmo&8N@te(%oct&dbf*wN&k@jC?Ciil+rW0VQo}Rp;u6?s8>|2y;w=6%UO-3 zm*}7Ck0^F$CPYWvFh@rtH0OG))f-Wz0`WG3o96zqe7^4Rr&B$+5xvE%UBvHtJ7ky z)cfk-K-p5TD(PG*q?KFs`Ky-$a!cw9@pG&7yc&yKW%bqVh~Z<*8!kisNF|G19yw2csC8}U zhar|hrI1*`JWsT${M`=uS1IkP+<<`mj)0PmfEvGm!d0_;t|t$ra^xJYCq=#_^Ssiv zz?*FKBAvs-Ym7Hx)1^%ZPJTkaDm)HF-xP9{#sY6BX+PdC;imF+5QBNg!<%a+KlCr@ z>+)w1?|J`&FW2IJ5&^;Diqyr}3nK?R*Qmb*0wTm!3QK@CghQ3<_rKW!;-OL-#b@)+ zv)Bib*Yv-6pF#sQfH&BJ*`dp|#&6=MT(0ux*|My8Rnl?}or0=CU7Kni!K@rr&T=80 z$f}XAwq!h**~6;%<*W;FsOH9eA*v$fVhbUMgPt~>Jnyn;%E=e<5BsjIf&XT z^+F+OFNQS|Z&-HGB#(_Se<-lU|rr0qX8{Q^TiPV8zK3`RFo!&_cv}`&XD*i&FUJh#_-q>u$ zNo^a(rWm7vPw&6+9%o}t%G=;KWv}!*y>I0g$;zMPvU$}MxiWHMi^UU}H86?$lf@(M zbzh#%G>>hz$D}XN4-M`ZH?gYaf0gwD90++tc1)~Uo$^2PU}r^6(*NYWO7k5SwKe78 z&Nc{Y`Pnup@>A?8lZ|(pa;hB^-_v+{c^Y%N3NXnL z0F_*RYI90-s(&hU+Is4BdV891I(=GuO5^>?+uNJl+r`_=Tf#f&4hBdjb-hyrO0(?l zeA*T<+ofLZdUfEwIvV~Fp6jXeVRfqg$A@oI9dd1lSYDBx18caair&p4!(uD>2kPz` zZW%3r=+LZr=H#sA`AI@w;fkMz^5IjbU+K2Hqga|cBgztc4rLqbNSuL-!a|U z-01^tU0!$UccyoNcd&P~cdU1>?-1_@?6` zp5hhE7w{D{7PuEE7mOFM6;u}-6_6De7pxY%0SY327a$cx6fhMC6|@%I7N{3Y7jPBO zXuQ(E*3i)4)^O3F)-aoa0jPiT8Jro-8J-!%8Il>p8K1-{t2<#)b?jX?UvunjDX}c% zI~=Oj0Y72vGhDGog$p0C&XIEje`(R80v?T0=5hSv?33uLL4Q0k9Z?;z1<|nr(gIHn zplfbq|H9v0?4bZ|hC-vrwzguFf2OIo2?#4_8gH6z>T8;78fsc>8fcnm8flto+VJT0 znDrR-81$I*So4_o824E9=<^u!Sn(M0m;$4O}f;P3WAkPGwk3H0-UQ0q-EZx9PxH%o;e0t@t?w z0BUlQ^PtfC+$PQyeJ4kuBupSrPDhBNCQBR^CMF#FRTUZ4=rP(Xiu?(C8UuLJrlAw+RtAF@_Nnx(areFUOB2LeJcI0- ze!abFx5W-J6SZ0)gUFhR#hoJ$%no_rkwslfa~u}jH+F?JQK=Z;NI5${n>bJO$>PCXz!x@ z`@ZaoyHa;(tl6HD-{t=qd)f~47+RyQWiX52p2*y1KPP+`2kAkP%u<0P@6PcR=Dj?~ z3b=B|VQ;>@{CxinL=F|(2ss<|-tIb=0(nD`HzLm_?sooM!T&o9vqW zJPHSjN<_Dv!?^5NP;K#2?V}d#+OJRf`y>$iL}&Vf?=IO~fTlha>S2XTEf5CvgvQUE z2L$ot!hVgN)CXPh_`>O#&4mXH@zj}Z+lwY&>a+c;dk{3ZaDezy?ol*QyeHUflzW@_ zO69)n5iO9hC&p~5Zfoh{_yH3*yx{kMH^yCd+O9+%-aUQ@#O_Jln4#XpzKpydc*On7 z5)>yfUAU=nS#iJcNcxu}C_!SzY?J#k^M2xy@Gl!MdZ#yb&o1xo|2#@OhV^{Aw))HZ z92eBrvuk$o%vR;{aQA5Y7yUW@x$k*5=!y}nEp1;Er>W1voJL%YvKW3OtyYww`L>c- z0EY}MKAcIKzX*&&(@bED`WY-)^m*3wfDQrm2bfCQr6_UM;D8we2MH|&M-ybX67R%EM7ds|a7{C*Ar4WZJWt;D1293l9p9?5E#~KG*eO z0+b!JI`FH0LG~3~~z=mZhLq$6=1v zpW~7)RtcArm6cN+l#^zVQ%;kstBlF1jE%61>9dR?VUmy0vTP=yAC5^K5`JkyNgb@R zjLBjvk6sue-B#s&H;L0U=;W4hfd6Efkj4Bn7IB-RK{Jn?2pIMc+i-5l?S>|FwKxVb zHG_-W#%^KlT5B912-~8+4^eKbyJfb&U86rGyo*L2l-ahpi1N`DWuhY1h^ZKA+J1e} z?IYFAU`kXMvouJ)&3V!4qtMO1j{l5H9hEpZvWX;O6JjVE9O%k)*6tR>r6Ox~f}Os+vVFrCM>ZEJxe9DnZwvnt3skOcAq8UfZfF z`m3%36QUw#nZ7pUtGoj{aEw)AE&`Roz!JG8)V@Bo&!5 zi+L3LQUPT$&iLeE<(<-tc?A1n0i{MoebgkUly~lm5Coa$x!+8w*@-*7_%0}9)0xCJ2dx{;} zP+gj-^q5lLd60cbv!*cvXGVsiN*Uq2vVBXlzA^LXOd;jmQqFmO`>tl0Rn}h_w~GE{ z0rP=}5KqN79F)lk$|I#a^Qebup7L*)-z9M=XO^mBBJzU!p@k@i8wyR>UwVjljm z=vvW_LoB&Kd8TxK9{sTFTG@{tIN>Utl_kzIA2waf0cY4$sER~s;XHShAscOGn#x$& z9-q4ra9=X5r7=ijpU~T_b{FjEG?A%gT}pAB__*5%T%U@7>l4ST@@%u zux?SQ0v3%F&PKgfVEITZ@?JB-Ix}v(U3Iinb*%X$LyV98s*xtPkzt|{sQJ+QX8lfR z*Zj3CR{FrM>uc$Rw30m%Um)CSWFp!<<>ku8Ji+Q-oyVk+X*@xpwVnP z`Dz!`Az46A3@F!hz6F4)$HJXLJOLDH0k^x3Zf#Tgy7Z;7<2}qvdEeA-qjfr$^tQ3@ zdz6>zzM0)`*XhsF@5cV@!Cy)}B!LXY>8R5a$42(>FO?qBK*r+qh3PY6`+Mk@at|pW zBj6&KUN^S1M}DdHkO6uNrQb+D8@t;J0)fpik-g<~^&#Fr`$>5s;N^gZbt zV`qEVmkJN5k4As#g3^1&Huk75H6Ah_-*ROjA&`cU47?HNH#wM=T%H{geat%z`?T=M8}$tX>Z5RJvqY{Jgq-ou}p-w*0hdW#Y0L zyi!jcKNhicQ)QR3IzCC#N(}7?7MXO5ao6{ye9ELXaRy8*sRXf>dN4!f%Y>2Jx{`?6 zCTO1^Xyuz|MM-F7vuGtzF{Laq23mw_bA&Ry>E?{GsCxZru8h*ry2a^Z%6WKo3@i(2 zy6?+Vimcg;lv}rT9+hiTDt6hy<0fT_*%gxpL~OBXL*sbws~S~}YCmb}1LiuxapU*- zjixIQC*4-I@9C7|>hCif->#IL=-skMrVWg1?Pa-}ye`VqRcE(NzZyr}>vY%Y_zVEe zY=Ih%GZZGpS%i7$#x+m?3ry1Na|*-YyUZX8U1 zIQq$STdbqFl)hRe--*8&_Q&tm`NFz>xK6t6=@RkkyU4`x-A^BnFTQ}9U*uieHY&2w zHQTiY?8&FQ)`4C5P}gGDRM$e+hJUaBtpBM0SN}<%#%$bw*#DdVw11!f9ME$&;J@Ji z9dPx`{2Bc-_~+}NwLjy3R{!+<8T<3&&&b=C8@>KS_1;%BKw?NS*85!hP1CWuW$}%} zyMGt_Vj;cU8cv`UtZY@;`E#q*_o`ou>JGLoHDsl?y8bl*U!e{`AEVyrQr0Huj#T06 zNh0bgeypmGsR~!FiaXLbO4mWu(a{~{PQ}j}M2DhI&3DDM`b(9^iPqazkt3a(?B6B< zMNs{X>a(xdN6a@(zvZ6F|7r%+3>{IogzIawm!*Fjx7%yJG)DYftZ!6PyEyOQ_^|~E zu+BBSxMT&Fj2)btTa86Nhka8zE=IKf(M)B`{kiR%!f^%WERsW5bKfeCbES|fd2Rf> zyrWff!>X`zw~$nBrIDaylf4(_R}-@0BC=XsvZ^_FoQoNb?9FI+{nOk=_YrX^a z4<)xoe~JSZq4pBZg{$1Z+Vnp?%_$!+-pu=ve)(i-`l5`mAM0IL`w4!DFx9CmTUt0i z#=MRIULy_bvM$xH7IKeafv+Q-sX;9r{Zho`3d@;~S$}&5L=;rn9{Y_y6dIiB2QobN^M<|?gqg{n#P;vM8amPBYx zR62jRCtFngmWjiOKR+6{z`NO7Gg>K8jmK1^sq|9+T!}dFIZr_A$^Imb05XY+wzu$>* zTWq&k6Zk;x9oID+utU8Qaxv@U)XlR_au#nv(@m7Uz_{jU)f8lVdCY z_gu=!1RLh2rj?M`$rbpK6H42La0fk(v$%dc8 z9-*(tnsb8f`G1uUc;-M*v$^nvaPb9sQ+ZfZg-92i{ZyyZR9AFW>#A}(?X${B9U`)~ zY{E(34`(&ltH?@y)~*sASF}pPKrq?~w~Qee8kjYp(y~m!IMu zJ-)r7c2q)%Sqf8DKi#gvedk5r7YNN{?N46)M7#>Z@iv(Fzz&mEJ}?RVdu?H3WAoEV ziO8st^R1InE5mooGah~q_kN##;IC_fKRkH#76Jr`uV9WX#d-~%!OgMu2J^M$?^L=9 zyy$<~tbT6(Ztp#RTK+@BKk1g|m)2@(v#tHif@vv$U8mo^`Q^OY)qH19JuhA!p!_HA zmhqS4YFjh4y~ILb*^~O8)Z15_@JYC%2-ZlAFh(IKl2|m%N$8`PyResV55bR;un-ze z6c!8u*ciB>;B84%2!kdX3uYSZIP6{snxq^A4`^V8(?zU?T@1;QbcS$gVpn1dzyTCw zaE7EKgmaea0LByH4Qzf07tjTcJc|#=zlgtKpF&`JY0PMfrUXgw|$&I zgaxq=j3@#XY+?v=ug)goIo>_SONd2Jh(xa$4M1rEcqU>~$VwlDxjN6QEaXY_pUCLJ zh<(!Lxb+BzcpQis$n3%CeQf4}KrDM`moG_Wc+2) z6F6Y08Su1DJWnQ5EMd6`gk`DT`7cN^H9{`n8S2> ztNy3S19cj1KlG|k00%}22TqIzgN_Exk_O993XVZaf&_+@2PHxh$y$nvKngW3Sc?a# z8iF+3hfo=YWEVo-8z{g>KE^!X$gWHW@ic}YD(B}w*d@%W-cVn(2o`qreMsCudqyGE2 z5X?ecdfPTd0Dc1Z5g`x>kWu=8`j2zud;CYlzevx)L46pS@+|m(XA)~K*j>gK(yb|8 zNu!IxhCvt-BRvG!)yh{2?e$= zMP8czmGoaxk1#%lbV~n#oB}^<=C9~Sm^UH6rJo>wXJP&yH6>#OxtK+;qZ2^Q!B7h+ zmtKGz&SKax383d-PJw?)+2g>3;2`+Y5cJbfTuLLzN(Zu_00>nB4mN5MijpQmj5J!Q zG#No8o+chZQ}NNFH_{R-M-oY*7s<_9+MxpS3)1IMeHlkc%j_FFv1Zs+6z5PO8Gv=2 z6|xg)h6ligh+9ejqJY`J1DI=WPn5ccTA@q>=%|;#rUXhYm=c6=BaR9PnRb{>FL=4wUYJY|K)#gfW*er!qG& z*D&WYcQHpYS1@NXw=pL&7ghvUBvnLJR90kFv{y7&e5%N+2(2itNU3P7h^fe_=&Z=7 zD5!|8$gTiav{b}bLYb%F{Y0sj!+cy5mt=et6KC+xH47k- zw>(tS1O5n!2g#zii6_y%0vi2RK2C`{O1?zC8NM0*`ab%8`o7=%T76sny?nj=Z~d`+ z{@zgBzQ25Te2*VC9}XT) z9=0Ej9)3P-Jsdv#de|95unlSITiK#;RTqAhM?8(ZABo-%-t7N}0itoiXM!jC2!W!; zj{pXUT#LLEN!}m7Y2)hAj<|;R0kJJaq?dAQ+LgGS9iRfSfIy1=FPl(TiFOH57%CL( z(8%6_%`Kn~hd~sL3O+GxWB`9N`rO(FO_Zmw#*JoWh4olv+IumL!~Z8Ge8V z&WZ;Mzn&Vamw+Oa)G%1iFqGbvAfbYd3}| z7V!A>*K97HllgFVQvv*8L|gy&&0ptOKFrzF|IPXC`x`177~+It>=c7iPo)fgb& znmGS>j|4)8VgmAN-`y7Sxy--E6HOwlaNva}V80gxa7C1$h@M`vE$VZP`&W;|)4|r# zeDp@iPKsO%*eTd!i3E~pbl{|ZMch&rZ4B0gG)ZOp_eqV4uS@B)NvlG2<*MmlXeY{2 zs(BduC;@3QdbK1XMfOt6c_M&!l6^z}E~!@0yA*jI{}6mF|C=TtIdD|c8i$XBG!#QF zf_5Mocht@rgO7+bGERYse&GdWvc}>g&W@Z^n8$&E#YMTG;R&T-38gm;mtBkj&zimw6(F8u zTIofS@<+K|h^N=VkFgZ8ShSJJ^rKq;!#v5V(^e!uCpC?}-W7F+?T9jw=At)9avc4* zi{wt;5%wPhr7()WOL__C8`~|jPEVbrF>1TZeo6Td@+ccfn?1*2r!PRBLpc=!97swY zW*_Zfn|X{0IHNPk6rnVB1+5Z?OVt`?i#4e7U9XQYe7)=VX_yVbCnVI7KIrIa$YcxN? z|D~9|A5dO%`C)%a1nxA&iL)aI_}8qzDFQ;EvSuYNvu!|%huCl2r?^0=o}!Jp!UJv~ zhywX3)~1)wBD;aZP>4e$1AZcUuixFux4zp@oI^q%hCPa~KgL|H!P3wTz=-kK!Rf=} z=I+9x7?@|v*4p(Bf%1|&n7Pq^Pm9Ag9ra;%>JQO6>H zV2pYk_puMan;Ks5ChTv-$B{Q9NapYjeakpb!Y>C8wSFcmod!}sP9_o}&yAfLndgCl z=S8_P=ZUZ|jq6pMjC^RIA?i1b8}L!>?avCA;~|Z;Kr6Q-BO1Xo#9QEn=jSE&;2~DA zI0Rf7w5=#_<2d^=EGOy-mzA9eTgl#reIEK|aSTN7$UEV;B7To?9%!`^sTW%gJfZO- z5ecOnNU&mVSXd5PHaejIc$DyWegEyW)J}-J$h*RR44hhF*N-go1Lq<%|FAD3;^x#1 z8q2o8;eqf^!c%XcMNoZ@p}Z)10pU!-ey_)N$GNCaU^k2@4tF^5aP0QbIi8PFw}2_q z{}im9v-#)&>?CDf$kL$WR?Ed3pLg9qCaMNpsrwMK~n29UFTaV|oh!sw_75myHoV6mcWQWuR?GiMqBr_ocJ3~a@97^||6&9s z0Ap{v?)=rg$fMX_*yku9L}AD5ybhRx%)lZh%=X;o+UC;c#^&zl_0Nl+H$Sh4&WSFG zZi%jl?lR6ZE;BANZZhuluk|nWZ}soKU4Ogzb|-W$bSZQpbR%>nbk};`df9r>dewU8 zb?$ZTb?J5Mb>nq+dwqLxdxLd}b%Ax2aGr3RaFcMQey)D0eye_=exrV+em8wSeLa0S zeLH7GR@N=$GOFTl#J#`wCDhp?d9qw| z+{uY7k^~HBxZr3yS*>DWtrr>%h)8-L)hPSA7)~p>QdXC#8b>dhOjfN}NGq~Z1qjT+ z$Bc@Qr7z~43%64ipwGeQjLMK@2lA96>{L7%02VZwOV*&6d@lb$>YBk1_e(TXR-#yf zH<&~bgANH_CaP$V$5NO#ltd|xP9EPXs$o#rQj|9&OHq}M3126wYEZ!P4R2VM(j=V? zzDHEYps1yIvOG+9WH4&DBx+pm3tgjFdR)p07Ko}-!u zUvG=Lg|sUI8zbCTQMrT8+gNUeK9Vm`SzuH*o3PsZ=bzs9hv6&mqZvQMPF7`u{xj8R!@G!n1mp2*xX{z-lsgWZ*M z$9bJaA&tctnLIFtyQ}C<@H&S=CV_Dzd1nlDSJoZxb#|V#I%7rh!WikUsyor^+&r0S z`u$XU2FXwcSwF=rS*5r^C2pl8hC})%>)_H+8d5s@6k4McdS7iCIX+co>+rNO7E*db zZAL*pb*52|Z4yp>BgmOSXL7ku{Pzq=s z)C9T)6@_*~LC{Ai95fUv4ef{0L5rc5&}}FlGzF>+odB@_)B1678OL)oC! zPzUG{lnj~!HHNN2-#|N|e$d}gBxnRw9y$zVf|f&Vpg*BOAWjfxz~=yGkaNH<(60c> zfP7GXz&p^pfFA*3AhCcipf3Rdpn!lU&{F_B2tFVT6c!)@k_i|94Fu4G=mSbXB>`3- ztAHKQP5?d#KOhy98lVHx30MFv1n`6S1DZfh0UjWafGf~dfW3Gx@^6~L5#&EO)JFrL z_L*aPQ0dJ6)gL(VapKE4@!^r*!qjK@pknTmR1j|AN$hV+@u7;}0%mKO=ZpcJGw0dj z6Z`ijX5($sZ9{F}+eW;H?ndv%?|x)XnDv@XZ>#}j2ICv68^aslHl{b$H~KclHWoLg zHhydjY%FX{Y<%As*;v|`k(iYjmG~;rFEK8$DlshaO=4YQOkzc1NMcc9N@7CdyTpjZ zjKl`^Z?VAT#Ct7?)4SiO5{Gwx;QvA-b}OFZ3U53D*SYTp{zge0B|Z@L94$Sf2F=&~ z!R$F~drAlj2}%xX2&xLo4gv?21*HeI1jPq^4$2QI32F+84XOchdAfo!gW7@$pM#$v z&q>eq&y~+v&+X6Q&uPzbfG;`kx%)Zvx%fHex%#>DIpVqeIpet%Aa+BZi=LAKMKt>v z{9N{&{@n8X`8n)4^||Re_PORc_qppi^11Cf@wrYyhsK{qfyS1`fX0j_2x1O#h3G;& zAx;nz2nb>cafc{Fyddh34-ivGAVdnH1yO>?LEb|?LYyH15E+OEL+lMEHBZkj}%YEr5q5O*El%3@u&Y6iU=HktIYaUH_S z7+uC{K0R=?vR+BGj$maDK$;5aWmYQ)LdbN~D$8{}06*4$tf}U&%3EFJYWl_KBh`;Z zomyg*;<{$l)PhN5%9te)Iwh5JUr8J|1QT*3)k>MPQmd-IdO9!(#^$I^mB65tiV~>O z5-4lwMisM0Sh6PIRq`-u;VPF~rYYO&<|%6`DOaeD7i;UjX$dw~lpO?H zB8*i&mweM9uZ;i71SI{XugW`Dcxj4MiG4M7pl&IAqe@wnpara%zOo)++)#hac%zVC z!KF!DrSkRt5!%P}PWg8gA2eT8z5VKZg!RIk{-~hROspIM+MGyl^8A#(6s>EYRo;C? zKH|Rt|5gqtf$B(9ZhSRk^WgF4QQ)!VG2rp$G2;oUH?McCx2rd-_oguZKYUgU`D(kB2>glTL>f{P?m3Fms)pm6U`d%G?##eb)8&`c-FIROQ&I8nbDTXMd zFH&?QDB69RNvMrdTr3D>=77b12Z*L3Z4UL~AvD1;AuypZp#rj2$<~P02-hgr@YkqC zaYRW(2}JQk$wYBQiM#Q-sk(8yDM2_OG7u4n3WN(H1W|(ULBx+Zk0g&2k9dznkGPM7 zkCc!2kK~g$TH$c;c@p<7HlnmH3ke$d0Ec*6#*&31MLJnifB>Df$cF`2IpR8rv{Cv4 zi&!&HGfT5@GefgLGe@&%GgGrrGgq^CvtYAGGk3FuCyytKr?97hr>G~LCz~hJwb_Bm z0q8&)$k=s1a5(TgkUy|FP(P45usF~2=@~e{E19#500=4AGYIGn`QV_^5hv@ zet@I4fH5^sM_E+j8K-ZT{_Q|GAClTE^|o29%QHs3f_OIeUQ7iqv)iu4yTzb^Oa@usT!42=!E>oI_qUSsD3O* z(n+lv0v==Dn`|UinR48PEc6LnL$XIzEz5ayvh61^Y?%0Cv!^C4e)3&qJ*bkw z^HKf7o>3yKA60{kvS{!v4I^w3?tXNhTZqCh*F*la&`wSJsb7DjV*m#FZIf4aE0<^zw9` zwK{J_HveQ0z%RpjM0fO?yszb5$|j#w`#*m9gcbl8?)nzSBlKe@zRBsENdxfmY z8{$`(oOD`Azvnsk0l5JU&daS%BCTTILw=2VS&B5hTds9_)hhBm?AO?>rGFFriqr{? z7mG+7<#d9%LPN<4FW^}c1@PBFb(@NMofZ2NPcI~q)ck>ItNkVz^hTUx3kxC(Euq1A zj)6vwk;_lL<4$Su4QMruZbD;6pZan>4XrxGsM!+Nqp!&Q<5_dwO#ptUhTN53Cs;5M0uT8GV=1YnJ3{70|uzj3APPzUyE=u7FxV>jEs zU7yxuSa(aN4 zV%&ma`k@0x!A)Sqor2kwj@gZmc+WDgzse*}#Z_r~Q+0aVX?n|qYlw4`$UU}w2#{e} z+v46%f8N!1J8s8ZlmCF<2L3+myzk{I0&MoEK2Wz6{>c;>rQF8yP-wSWb2~-w&Jh`S z8p+?A2C%ycz}Cv<9sdtqb!+PoS$lUX4#>EpV%r6-8XdN49;b|VG5%9u_SW6bIwjaE<%))m`R9uk9}Xh0*;dkdU}YVHiL*{k%H&eE@+1p)h7i+=Ikh zu~&@uDIiuThFJ#p$P4qD{=fz>uViNVb$v^_ju$NtZ$R&$RAz-Py{~r3uhj0#LHy9y z&)6F&E<8uF>K^9CQtxnsPq%g`3n1#?0!F1AE zMOm6AEVOC3<7j)~m(qwuDVj!=bbw`xPzH zc#Qu;V)4OB{ni#SJZf27lQ=eLP2ns36c)8S8d=<>sKGF3Nt?Llu(+DA_^u&1ihaxo z;Sfo5Ko{tyK`F)|VxXlPmDY_yje(%$;1J_UYtJIe@F1$xBPzIp(~`J2aEQ=i0Y(a{bO5Pv{x19$a{Z9&f^fmH)x0qP8xxSxHi1@JyIcN2@F6o$|A69aZ; zwR7zR^#;RhOhS$zVg&6lFBzWQoOFk);B7NN}%W9tIyRVH+TZiX0q7xUn%q zgWHy<4W)+49Gn@r6ES;(mw=K{Y^Ve{OmS;s76*?kF&oMO`j+z>?s3e`;BQO#h9bZh z${~cC8#6Wdb6dg^y`ck2Hn8`_o?PS|hp|HQ%% zh5$N9J%Hr0Vc|!{(hnwYv$!=2%Tur?;E%-O4@LvVyGCKTJT`Uwidg=^>}?LWR$+xa z_G$e6SbI8}P&x)bSwmS_-9foJ#i-I35LdEv5JG}ypNwmijPJ|Kp=>FeHdIN1PpCz> zV=1i~(`gq~ZWrBp5LKInw=jgbE&U&*%SD(OJuyVMt?ZWGZfrsi@cGflLzvt0ZmI1? zYjpoV(C(ih_-&xh?jp&@P?U~}Ffn>$2!C7YBF)EGl>RR>puI-#520_%U8MLJ0h$eA zUG&lr`L^0chR@q>`gOvy=({20Z5cqqF$B?x6Bb6#3=wauT%_L{1Fk_r!23Cby{&MO zdT;bd7f1-Glta|p8W)-OZy)LZ67q-hNAQFBBb&mTBAUP+;T{nlV2{YF@T-U`@Kq#w zIC=y+7(FsJJT@X092=<;t`eaFR*9SlpNNJ&VBMkKVcwzNVPm6UV_>6UV_~CWV`8IYV<(~{VkDv^VkM#`VkV*| zVr!siU}&IeU}>Ogple{ypv+**pv|DpV9ub=U{`=cct|4p3wS8x`|U{3cPw18P*N?- zmZ`DAt6X^~&=xG1CL=m{sEPV5g())nlj|uGBCISJKSi_vu>mN|1AfaWxfbkBk@@xH z>fnU|$A0pGu7Q#M{eJHL*JbYW658lhWxy?qvn6>|`B&+!wnA0vSED030C86SQYx-( zR#o^vh_g~ySrj|8ek;CyX82`}00u=j(KC0m3WJxb!}C+p@Ql z5a(nuvurkFj`fIk8E>1Ez*EHzigcuXA(#SOm2H!FR$Wd!pQ zbu}wWrzUAP6*l3%x-vrlMLQ2qKH97TPV0h;i5{BGrSp@Fo7KL0twM5%OqvO${XhZ- zJ=6{oY)`b-tSOzHq~AmUWSUS&qP%87DR3O#gdOUf30@|KYIc+^Pcm=Tq_QRpCqGS= zPUcJ&PZmg)OJ+|NP3B9MP3BIPOcqL3Fk~@&X2@eGWyoPDX2@^&+>p&s#E{oe#*oX9 zRfI+4nFzm#pa`dkoCv#!sEDkHq=-=RVl$RR3*+SNri(J&92rHYf35mOJzZww~nRmSshOuN1a$5f8FyswmOkI-a45& zt~!Z2!8-Xm*1D&40(EkAqII%$lHM%)o~y4`)mKedA*(8@cB|T}ma9suZ&nRgy;sdw zgI0}Ktye==6<6P`YOFf12Cn+8>a1F=hO8>DzFgH>HCfeL_2h`iH5lV?Os^l9Tix*{ zmu}8A8RMo&Bpv#++U0G4%Cxi;{9j!7ZB((PYBjG(3T#+>73XdBpC0YMAo$=YfH)g_ zy8>LU?k-0_Qrl4bn!L9Ss$jteDAuM&4k)j^-uDVDeXhwQkdV|rw6aP7ZXS!DYvl9m zB~=Vgt!3@=2Uh2DMbv@R?2P*CERTmucT#S8>yr;Qj0}C{A1G!>=agmxIB-}U7x!u@ zeE755P`cZ2b*`XLBF#|DYVST(AoQ!YwIFbBIRsm6+?NTA_-e4k@0rv*G{1`9CkL3| z|AXZ2`2WFj|G$vjSM1kmqAH?lqN<{xdX;*$dewT6kBX0)kE##oK;=N~K=lAbszRzp zs!9rqQHfEDQH=pu=zacx3tjHoHB{B(#MhYX5$hSxW@-A`^ z2}L?1X^~k-GvpjH5P6Dhc=F(;t=IjiB4Pf|{a=OI)?*BWW%_!sPiUy$ywQ$UedEnf z7t=cp4D`g(3iGY^7$Og`AF#>TN$fs08assD!s4*y*kf!yb_sih4aat3i?C=c0UHHO z*=%C7u%EDH*fne#wh?=TjlWzB#mia^m4PcgLl-{3)K*QovhYcDXSkR_Ht%Ta`jCRAR$1Dl zxpN7C89&7XpTwZD`bpKza$gf-o`}@Wp$GPrB9^p8v4(1VE2)GbBl~O-tE{pSgBQ0= zzTPXk^-%LJFY6~`(@!QUs>a39fgNmu6@jy z!{%ZDG~_J-@1>Czve z&^Qd2%ge?roaH6Ry{O5^0*b&$xL7pi;jHn6{zkIUJdB`=Nn;Mq3SR^;_t18Xyo*g^ z0nUc8klcs_8i8Tm()Y`hu>|f$jX=;6jPRDJU#^UGUZJiLH#Fs6zTPie#wxF9%*YX1 zkCEQ8^vjpA(RoSq$ViSC&c<8yhPOHhti%Z}*fP1vTSkmY)P%(jRpk!BKJZq(1{<=w z7{x)OtE#f;OsHK9F<@nJuvQ}&J%W1V*EZQ{+fVN@ewygaMT5aSCuV73@6wpWv`x9eq1{5mE^-;xN7G3g9Xi z1%TVoZ-zE7&*|nZ*`s#ABv2V@r~tE>uGx}2YCBT~Xlp|teVFcWAr>e}*BFM|qw8Ge z8g|Ud1uD|Dh7oJ%eV5aQ3v(jizopIxe}s;9>23H0Gzm+MHTe)}=pmQ?*$mKt!NKS% z7fizzApI&g)`B50=q;C{hBJUQDR-V~q{Z>OH%WkUV53v(uFiCY;dG^eVy4PsM%`kT2QDAgT!3c4 zZ81$!7uL9$Cvh05xY=7e(_Pu6s$ym_&e7#A*^g(b%nhf+l|{w1>cv4mjj8F?0uzs2 zVj6mZWff?Xwna5!!q^4YfWgrLjSpR*L@*)YlG8AXd+ZKt(wVQuPx!mEH7w$o{<~8! zafKmwiEQY`QQ|B8wPmaGCUl)E8bxtoI5T{%zja%o)wG~<7Dye$BO5bA(Qg1HVrj6Nqie1L4saR zhPs9|beMmJ^g+rY>yUKF45SHi42g$~L8>9UkbKAz5|{}21Z%<)L7wo95K6cL zmP*12iiA!AE1`(+n(&iANk}3X5YPl+0+R5SKp@;DL=iLygM_C9D8ZSqNuVWU5#|We z1Qa2Va7t*9dO)(Gzd#+iqRtR!`}X;69Q9j~-I(*0aH&TWeadfTs(Vu2vWN1AN`wlB zJ`H8R;v2P=1Rl&O+NsY3(`d`P|djQ{J=QlioAa)6{d^6W=q| zQ{A)Mli#z{^R4HqC%mU4Dk4fbsym83swB!bY9)#~DkbV=)Krvc)HRhO^#;`jH4POF zbp}-iwF#98HDHXRmZXxTZl-Fc4xkF4KA}3Hrl6vrPM}Jl)}zv+uAvH}o=f?Z!kJu_ zvX=ZPIV}a83{Jr$V^YME#Zwwn{F41rj#9{yM^mmjEI2H<=DB1!WI2Pm&biJxNjONj zNH`<8A~_?uK^!115O+UEKUY6DH)jP$1(ze|2FC{12ImI%RPrk-oi`fKH+85qL^id~ zQo=dX?sF+?$P99RD$Pty_BPb0(hvZoGAYKBx~+poB627e)H4(hN(#k+5<~H$*ia%U zUX%=q3nhUPM9HIsQBP5vC~=e=iXFv=l0_*5J`3atd>$wg$QvjaC?6;s_%x6+P&`l| zP%e-?P&AM)P&SY|P%=>Al;!lISz7pbssg6kw&f1< zgM`|fmq2^4ld1FVn}?le4?;5KEt#PSGyfV;G|ucos^oPFs>i1tH|vk2f#OxZyp7%z z(P9a^OqH!nb+Dh|$!4fr;maU>ip_?xt=zHg4}x3Cg4+gi<*q?WdRwg?Tj?I#@w;13 z<*aNfg(fq8YaQ92d53%gKI6-3(6gKW)y9GSr;Rfau+?_Bc*Yb`^G$ELhI!%|L$MWk z*bN|dm7&@S)ddrJTNQ^>XSA0Jm+(+sg&La)p{gWF_5P?2Z~adqvXlfIBc) zNOu~0p+;0vXfVLz_E0#Qt~d6V+TD^;gCQRGRN*1Iwb*^N+a<*YgOzSZ!c}yb*ex}( zlJft%z%$_FCMKLsHyXR%r=Ug(lz4`m+yOR{4j+50Mp9CO9{la*E8I%A7`v-RQBr{( z{_TD!d`5Q}d)4=kZK3eM6Sw=qpXs_|fA-z6EfpSm;vOs9N4FA7=(}ZGEIgR$rY&4f zHx;|tci*;LcsSF21ekaM3OH)nL{ETf&ue7~Blg#seQ#_(Atz1k;SvBr80Xg)WE+7* zW9|N{j^p0vZyO09diFaK$4`ReLi$2i-V3CVw$NP`(ugo%#hJghcKV_(i04J zQ8-J_K2>OQH`I0oBz@&2rp(Yxy^K6TO<;A^)>yJk}~!eQW{_F$^caDz_2?`Ue4 zhu|}s?3j^Wz-E($TCjAk7BR_j9cf&b@o7hO0*Qf$WRArM0tkoNP~HDmPv>G5XzGYi z0M|`RVApf!d=b4I$C2xi`gQ4puYsLkovcNKa!f{G>*5Ekf!$x7mqa{s0!G>Z z56-vK@9&)CMVNE+Mk>}t4^XFF@0`Dh*yMPQG_T7Zw4ZifIE9KR1H5jJkkRJ7@LvFs zS_&TGbx%VMnXke3jc>z>!Gl$9fHK`21K%piH&6)c^i*xt}Jdx9sQmg5g zahC|f9yp(+HR8-|5skpn452gQ;2P720gh`;$L8noOMn5Z^_!M)U~JYkR!!BWCW!4Bvh8xe;a2xW@VEbV$}s4 zn(DOX?RV<@rQ1GOp{wmokT4u(XUm_gtsF?k+-H&7xT76npt*@$?D24stk0Rh2GoG* z049rA9P{uDXbi@+?=1P-v~|c$FxgQcBXQk3KY`q=6fnWKCm{QAD?5baTfxP0gZXZH z$O_!l&gSv`;BvX)eD`tW2JT?z)IXaHTSzv?MBhY7-zZGq+%EAtrD^sU`cprW0&1KIAy`{QYH3 z$b`Hdb5jBqgtt9@3q*U9^7aKyw47KlQTE=?f1CE0Mp$+=VF082oYlHVZ4mgjz zoVv0Hpd)M;{=TjncS_3K;4eGBI_bGQ23=k5jFkDo6##gXKFzxUI5QJpVxRtc=oc;B zn@6RqQIYx*_jD!IZ%BGA@1Cw|Me@rz=r1F`Drrm}ovuzr`pbFf%CsNwbdyJ{s|-MY zSm$42e%aEad11Mab+szerf|+H4SqOjd>*+js3K)*Zu6I~A0R)=W75^D$e5bnTmc3k zr7!b{0C#YrD3W-)2PS-*f#U3w~yc1+tb zU35NcWyBBogvb*k@p?hx{hWM9eX<)k2jmig=hc=UOZwei-}mq0$z4J;2S` z7K@0VGrq2<#zh$qtLA)*W%nNvzVxWLMJ|t*%~|scm;LTPn|v)$h(!qx3&7&Dg#Y31 zD~rlo6!b7@&Y54t|2*~$Mzt@>d)PD=%r7(kAonFfMJ%!c?ea|71;(H8z93Y|qVTRM zp!`|N`=RU0jY?U33OvDP%P!{q1gznx`bFto%Yb~@WgQiaKV^(_}UXraj3!}S_17K~Vi@UpC0j+Hd zi@Qt#HErXIdrV%}eiW^d3*EbvCzZh?a=Sp5+ghy9=E^$F|d2v>8VR24zNpYdurG=^7w<{7KW_KAvhH}^p?RXQ54T~P;y)kst zRF8W4^pVCO)QLCU(2!unmZrV=l-<4@o|@-v2%Y{g-8EedY-S}-PfqVoM^6t;S50qC zXHVm%Tc^*aKTr2fmrt)x&rCN>A5V`>S5N0pUrmQkcR(Yc%20M_3Dg$40!@Lwgib+y zprp_k=rEKI3WIJ#>7laFb7&+K1m%W0LNlNy&{?P?Gyr-6rGO?t^`PTWA!rTM6Uq!- zhIT;jZ_-YQmO1Kg<0hi3qNpvc%xYMl=paAr|xOa4p)v{%Fepx3Vp!1PyR;k@`3Du+<0!* za_*_X>>mozXCs!gAz{0&)-@+b(k#+E(j3zK(rnVao$9qzl2L7d+UKb>7a{ophe}lQ zM-Eo4cN`C+YCiH7Y5b;Ai0b&r`dZVJstK^y^W07L;^<6%pi#-8O(htWsG`|ImCvC` zB@mU=qxp-2kbLW|O)`&0da1fMXJGQXD0M@Yr>PG$bk(Cd-)KaY8W{>o3rMp|b4v>W zk6=6~j?<1kZ3tlj z&uBVu%q#hy3!w1Bfgp8JeM>Myzki| zjkEV|SXnK&7 z40N8T%HKbIjj5$G71%kO%`)not|iVS22OjQ8^0euBXzswfAd&whiT{T&ZC_GY%rDv zD@N~_QzpSfPeb1@s+PkjQ7s|s?mx%SAZ_P^?Hlb|1n%7GgkzDhD1x80XB7Ht-UZg^-i)us%x$@oHK|#oAjlS&pu;?4yR!_EFK!XGnoAI5cQeqYnoA3uKl^%IbleL*fr?i49NZW+lDX&3o0 z(kN04_)5gjK9cW#+f-6gl2TH?q6rEBQGk-!=g|F0ztsJJC0WKxhD%1;cx_NR`@Hav zg5&B((!e-~%wy?%FaE0&SlRO13CeY`zQGyiuPJ#`92HRAsoZs00dMo2)gK>y&9@px zoKaKvD$;aex!-)uw;RTsVN*>tvyU-R>#KV`UiWQ1FvN}?|>R33bYxTE%wrR5G2+day-FN!^Eza9|2+`7e_^hI6i&V#`( z4^^apiT%2-^g${{DpD$;<$cSSmX9s5Egyhk@jTE}5ice-h^bUI zeGu{`6i_-p5dQc?EtbWD#e>a*)q{PPWtZ(6XhC@a^!&xed&n2aM@TH>!(U3mLjth4 zfn(`saZgG~dYQP)Qo)8#no68X5=|6Ms!yy>@=5ebI!HW7B26Ssib;$~(n=&(2bEIq zTX5Wn0(zktY#FQ>>?SNGY$mKG>>bM0ic0DaCvQioRjXDjSA#rWc&KCR zdw_Odw6X=T1+WINpRfSVP}UQ63Kj}B3RVhs(j>>EjYOJ6nxu@xj3kpp!_xa^ad1Uz z(9HE)A!k*Wq@YqFXBB_e;KX)yQlR}g6PSA-IrcJ-Mn=fr-*0K{-*Y1{Xd^3{Yc}S# z@z2H!$9SJw(ff7beJ`348O4aQ@92EsQho^_^3X&%F|ur;(T@&_vPD)C9{;VYk0gIV zQlvcb`u82*hg~mB?~wdtl^kH)fB(G5Hjzf<{g*^Zl@9}n6e{8GSb0?WAed5%+_KHe8^h4p^MbCMdsT6PPvl`vXix^X;DU6xm1oZT`la)k%^ZJq{D?ifb zG`jtih0}-(^P*9o(U-9*(kPM6y{C`0as(1zR=P#vByLC;Qj``&J)vif9PnD*PnmkfZnBfrI0 zgJ2Ubz)}GvLQ%3P2QBd1xZ&~QXOC-hA3rBK+)o!`2?Lbwk==eNYO(6eN(aIg5a zH@&o_ytM~+0ZRc-bB11uUdDLhcY41B);2^joG>J}dP~}jiHjn`2MC-1fw(1$UV6pQ)avH`%053zKVM)xDQ1;>b!~sj zJL&>{kqanG2Wy7b_IXiC)yc3mV4>x!Ms-@iK%95|J9T-s4xuoXFpg`3E2FEF3)Dr~ zMcPHqG6phtUrFM2EPZO``K$kA*Yx|^25TVXT?|Hx~Q z=E^5&+%yL!CKsxtr<2pxwvAQsu;ntFn=(^-C+SY8=wHhl_yyz-NlY730nu4)GF!Cu{?zilaO z_PQn$YFcSpYFZBD@i3`EsZy!p?>q~SnC|I)%J@+E$AE+9@4av~ourZh@G9TEhh@pfJlk^!{Rkmr$jw7F`YQrQHL%tM(^im#0_A3jzsc%%PT z`K=AX^mRpUVQy(Iyun1*WK4eyJoZvhU(iUiW}0=G+EPK zV_GBP)QvLvSL1h!(@X>0$77pzS2J8hH46$L?0i0HpN81h?gDx}%ctP%w8v|O`|)%j*}upr%|uP%{WjTj5qUCgSBi+Gz%VYbC6 zOt(@Ordu;s^%zWJR8T315QNE_g*2U69(~%R%eO3oaP z@LO#d1tW#3P|M5GGv}jm%s9-(=-TkI?o8)sek%aeVbW1UjA5=)Ft+HBe}kL;?z-c$ z<64dxsmwAHg*m*A1I0OxBI%UKpr0+3p#%h>=Jj-~%MF(gE=n$4E?urIz$4>_?A7e) z?1t>a>|8p7(WvZt@eP+!@%`+0I;%KjIjqle>ZL7|7EW753q_gvw5@xLUS@;D^<3pK z*x50JmFc#A8{9t-KZI*+Y%za_00FtaxfbjQWO0IEKK9sqSH^w!u+9;^+xgKRSoK6oM~*9l6e~&8E$g)O5679iN@oB;X~EY_~HoLNzwvj&i1|Wz!C5sMclv+07%+ zmfV)yHuUJ~Xy>f$DAvn!!#&pI^1}0;+00^~* zw`wrM+k9KD+uK{sA_hma79oqEMJ1hBuach*-4(>9>|ft%u1xni$NA9HNXUo@dRor4 zG`+Q2IqQpyv!H91@NivNYN>eZxpLJP5eEWV9W81JYD(+~bWPG!=3M3!kIPpqkVi?* zNX$q!N%)J=(F?@gjN7AET&YlVW!Dx}v#rP~%qq=-XBB5vWtC?^v&ym%StVI8vqG~< zvr;p-88A$gm|d2=mW`oPkC=pG+@@Px{9XKAkDZQP$X(){;$6Jxn02zbtWuzZGpkPf zV%%cA*;G1N>!s`P_1yL1^=P#!1E$GqwQ6mRCq82%vG(9w3hn$Ao zhFqv5qO$L0M`uUp3}x#|q)A%28Hz!(iE}<@dx$MM2hAK-ZMsjL*U%f!uOmlOaw^2; zCCA;Yv0p0#H4gIE+eb;V6U1z@<+9~++OwlZmqtq+TtpFb9a)PEScVyfC5E{X_jU1p zNp7)&oZF+$qtc_O(bLi5^$+Wl>vay!^<}7GXy4xpaV?;&Ye9__yNU%$G?x=X^yJBbpG^h;KTJ z{uOwS`OBtA_XJ$1NAMExyly|g-L#BbY7TEoa4*2U!u#W?aoo*%&IM@s`Viyuh2u~7 zFaCDtb;oP?;Q2W`5kB`ne|#n|Zu-$bG}t`YAy_xq^*ru42OogjYh3ocF1mC+uRo^2 zd(JO6c6c^#*Y3UWg>Su=nelhSV`ttLc6UUeJ9mHhddWf*Gxr zYh;)FzwZ5=c#VI*>E@f_cifuQ`l;2vb*&XFquy-LtkJC6tkVo?HVJqUpdX+fpb}sd ziAHTd_sy3PVpIf8pZ?~NsfsT@m__=%22RkW@vxOmD&Kc`FEYx9C)R#@`<(*2T*zt_ zlPbR1Jj|ePW2;P_X2p#7mRKv7ReiN`#nh%Bah`$Wq~F#^D^s58Yz6)F;>6;t+=Se; z+>G38`^05&a9J?yT<4^1H)XeeH)gCV0w@vM9<3a%9QmIO1_fVQUOvCPaj672H>ZWB zY9?!D!YU#Uoi5EUWiE*?xh@~K!$K=U6RzGfRrGH!9x(;!$(ikyRBTC~>4&t)s{sVp z1nFVczeY}ZlQ1P4_`m8+p)H}!n&0+8pRA!Z7}%jpNW?dA4W@b9FR1>Tg(sR~Yw!2o z*4{Sr?}Dv@&4O*4&E@Uzt9mAnC$6=k+7csTBO)WhBcf7I7|8B)i`=w(K|Ar;?=#tF z#AkP6lWwMN)^5sfgPSw&C0l40$e4)rx`iV$!e6n_R}wd|4ty8?1ElA-ZZ0P}sf(A= zPLk1n)>ckebQ%!-a{u;cYGPzjkG_kPvy`ipW6P@+rxv>ww?um0`}dR)indN74NuI; z%p=gRL^8?bh+}?!{#pGq=_m9jipZfsxGT=TFG?+VMQr8%3jd1RiqZ;ONd)CU;^wa$AMx&aZG*BY+I6Cf#ja{cAuSXmGB~DA@lW!#QGDTIPTbrII0!t zlIcoe30m1$wURk0CkB!DC!TbB^vq9~X_sjgXcg#RbG10bopYV*oTHtqoU@&)ozt8V zcOAQOo@?Kt#;Ay%W=WMlqn=HDH6&)A<*m*35(*vh9rDFZyG&-wXcMQPr=9CE&$Mrr zvrg4+mT#tS#&1?{e%mCOVxLMV-zcXk&nP!3pDjP(dUa;^!4K!B`#X198s($8F*1hA z8HqQ2qf)ZFwOZVv6dyR*B4-`QsP{S_Tpe-OHH zFxH&sZ*^=X+P3a5?JMn%-;tT{tKC+osg9XY-PzyV-;3Ui#`EqO`jPp|ZJi;v&1UTm z9@e)9mIoFECjFb#Tf100SbJF8Uya?*lFXLOYR+uVZq91XImtZ9rpQdllpD!+5EAXw z7JE5*+WUZ~=^Fg4-;3BRgj1GBouOzYr))f!f4}1>z z>UEl}PCo7qE*>xXv~Bna{kEQt`)xhbUKRXO5|x9)FOP1L?~>!m_u@ClcI44k3syL* zr4Zz~;Yk{hUKO{M%rCfahT<;2o`X-$Jti)Z=RT5O`>P$j$4Bx5<;dmY<-X+G%jr{8 zku26%M@}g3u%7%q)(>`)+sS{QUzq=c$%CSXyPUg-8^PTUnC)Po>6a5e#T5-3ZsVwY zQaPGn!J~m--k=7#gdnDq=lE09M&S5-et=xZ$=5w1V9PGY8d76b6SCK^MB82;L~|^2 z$QERBRK068MS4^mBzYWk7<5b(G}wLxbcc;SUwE2&LWG(n%S|S7mU5<4Z3KEJ0xnuGRoBhhCjwkCy_1f#wfD_G^9S$;2%HEc z@Q(`!@e2vm@Ye|J3A{Wjls8(&eTxgpIk!CbKgRFQPc=91sV}4DEek-*sPT&NW`I1} zT{+}DEIYD4&cn;j1|aot6~A8|V-7J#;)l*>=I0)V?fBqj$!}pw9U<3MSC~uvYupv$ za{esfaBoLpLT~qEkLHm3q~gf&IOdG>EarUpeEisEC*y?g9DKPIsuQLa<{Dp9Q`e`Yw6%_@*d{0|h^WsaUPu_t)Rw z{v%!M_UiO|6_L}cmG9-@0yiy4n#gu&v9gKJzB50#UrM%mf0cZd;tTOTVoj1E;vteT za&;2Bo3q5c#4aR8H?zazi5B0#{Vw*3wng~Qs~y5rHy6OexgnH@P7Gy2XY582a0v_{3oeo*(3rH5fK5zd%*Sc zJNdsZe=dLYcaBH=VQniKR2GeNARDE{$uzhR_&JSFBo&n?V_7dAy#6Ak!cuys&;dK)}zq+JWi$+xz-*1`p9d= ztH0I*E9J(tOV7l>MEgzpUD#jrpQ)cF-Fu%E$ikRD$9)Ht$Bi&%N!EGwytbPg*iTAh zhk<6|;q-G-b$)i^16N5A>@AcRU-}1-lr@xHciw)KQKBc%zmB~X>!sGE*1~?5{UT{K zX(S1rluLono8Dk}&+c_&`NpjmHympp^4}cpyjM#FzX9tEctr*j zEnEH(GbNn~KYDc&dq*OC^c5-gUQ;;!6^Ykv*-pV)`W>R+PWf79hJQ#pS-qYx5P^t@ zUlEc22&eZVVfs$ZaMSwzKVQDnC;mhG_n&)zZX3P0>HCA^g6QTmBGOvLf9~8;e1G$U z;+;FUU$Zc%yjTBz`}u?0#fta8+J3Od{Mm1bKCw0ht%z%PG>J-_3vh0m_R z>_s-9c)Riz_ytjFA>H(QbA6KPh@iq7E))OIg%gW~J51a#4}aVc?mt1~`-0;B_dD8! z)V@Tbzlr4vX-r=*|0cQl>cRc+*UxTvywCaZxP@%@ecO*%kohq;sc5A8c5k&S1AH)It06_{TMZ~~k5L$3@V-PIm$bzpcx!!TM~@OsM40Sj+NIf!29^{oqc#F}}uElV6>0^x%bkW94I+BZ;-+{$Mvw{Cc3aTV#4%FAY2`@; zL061hvVyNxfP5vlUnYPxI7v?ZreVVK$mcO|MPv4yL^`{l%&R`B!DH{$qSoG3xa-E` z&Ri;rgR(Yw+8N3+`Et{*6`zOIUTLVh=efLo2zxyeCmhhfKeL}XpI z!ddrao?sW3loGkQ9bX+&AmipWNeum(SLm3%uG;D+1&Gy8W?#MNuOGg~rn1ffF-ga1 z#G+MX-2T#=eyV;E5YiTE`=b!Q~mCZ;4E511VNE+XizM0NiL@SZ00{Ml2PoO_$fbfv);iEa&Jk8i%td$DvG z!PZow^IO(Q?yTc#ucUkgoLN6QYkqpv-tAB^N|u?Ub8{#7$<+G9tlky4^{mE}^12a) zzC|ytD`o@RUxue}+rSR^u$5@ht%DZaPF)~{Pcw?-{D$ouLzA@AL5;{-SnrL~ns%xq z=JH8}GqG#goyf3n4p)-Wl-H*n*Cn3z2ST(a*GET(3G$DxY((m=VwkUoiU0mT{Lr|+ zcgL#lgfe{3^9$Y_JAmv6|IC~@mw6fymagIm(>$kB>Nw^!HKm8<7L!2@<4dwbznfILs$s}WE?{!fY za5gw(v*(Tdw#DDId?1ox(jEMy?90+f4OLwJhB5!?jFqnc&&9_rkJBHgEbQC~__8tJ z^KR>DP6)-R+SEJgw_jJjp5IM+$Em< z2E3SrJM~J%d*`AINPo%a5|Lj*Wxi#d-_weB-h^)iQY{S=PjghJY~4%$Nw;{CTyS2?-Os#i-pT9tp}*efN|B4-8((>Zc&^d; z@JE+v_0HYDmd#4d2jSUH=UttmvkNo&VWSG~Q_$9b{mwk?D{eP^cHk-)mSJVm`T-hX z|MAr{)T>xKF|T*@nEI!lBYH)OsYvQ-z0~v5-M`8WQ-p)M_Ip0jtC>VQZ!ASU7_7Hv z@ad~e>uH)&8c+Yk6%RVgT`{Qf)F+*$n{|!d(WMAx1pek<{@$MZQo`f5ySn=@>B z3OKQ>riuKz_73Ai1)M?pb{4J3>GRd8m+qzk5sGxQ3;!Tye~eqcSsi79($+H%;Aw1 z{Y1}c549?%+TIu0Bs)GS?g72f9j+1 zI({{OdMhd-xZ6cIkUr%U%p~EE4U*m_Zx-@r5mz=9c$XbR$tZ|3k$DkF_ev*>m5y}m zmzKu3MrZ@Y4qg$|HRI*!HQu|D>UFLXRy5Q(> zU!Hd6u`okaOqc*m-mBJveNs)C&y+vNDZg>U3kA!}ey6|BtJv4@In@mJyi(O>)&=JA zaa^*6vmXX@0m7RO4gpIyDPz6fTW)bj!8k_$#Kpm4hqaP6q*o+b+XG_!n-7AN z6aM5pnP&RRdY0T~!XYci>sbjAg~>hR7c+GlleLr}R}r=S;NsJiPe&2zZ-f8Dx^*c} zeDM$^S6;@i{DK7&bNyQd+Hb5ElmR(Gm_6oY?C|@ur)H^{4D2CmVzzB&NynwT$) z9F(W@1+(%|59@2*%XdKrOh?SSifdc3jBd-{M-`X7|K>i7XKfLi%xe3P)7Abx?k6W9 z+19#mCw7rJm_95wv$>Yep_$T<@4Vjsqs2dWCGzlPQH2H)iJKc5;* z=+gc3_tS3YE?>(!idMnpmB3-BQqGN_P2)3#Y*QMQumI$@JJ$}jQCTtHk$()>w~Meu zuP%RGd|hBvl#ii%K>O>K#Gfm%hVnlkM#PtqF-wxLF;|?Jm%o^;aFb~&%GFlb_WRCO-Vg@cjC9s;E0$HF_Sz>IWY&f ztIgEd=AU?7zkD=8|1{Qd?%*1IaK`^cyuB%z*~h;{^5Y11wcg@KRQy~d;xPBzZ{BC5 zIiK*6^$wg=hHjtj%zok5$Gualeo|4Zd+MrjSEEI5lX&wZe`jtO)DYteJHk&{G9OYg zRFQ}D&I>qcE@kDP`Pr`KwU<_YIcOZ|)f+vaoC}U;eQ?Kj2y#YyeZIr9?B;d$0L9>3Rux0h-{UM_!*)+HKN<3MV$|rdIp`1DJLU4^ zFm}gt%kqb3E|pUkbr724N6Y^eKWg4h| zH2M$~BOeq0ST$-d^^hcvlwBn^;T7miwYf(=S|N>RAf58Q5!Z@nNmf?38H2~Yld&uV zx>$|)wwR=qHiEK8id^+XmzmVD-0`m;AN9#(#20crdE>X;=M7IWsykma^>A;$ zvaW#22#7R!rkhSsU=aj4Q@JGA14&11390 z;XLI9_n(`2mv4+bm6Q{eaWcRTnFd`|Q&EzsU8fV=e|P@j-?a#>{E;EHap9>N8J+#d zXF;$?ZvM+FKepZc~19i@!Pl z1Fk?(zxY7O+)|n(8+#?RGPbs$Ui`fq6=eE%_zwh9Lg$9rNBkINBN%s_eEenZ*nF3g z&{H2y^UmbNB*mWPn^NILNoR`A3=8iO;J!;)OT&7()PzKVBYs&JJ)$$FE$3Fw+nnFU z$1%5XzmSG0WsIKiT~QU$hXkL}5=2W+fStkRAydO9aH(7(4-pjugK5#-9wwh zrh(()ez$DF9lRM&30N4{%zvHA6W_uvcPn+D>hX;ygM5XGVM*DX;N*z+5#zY2TsruH z$4Q7yQYB@j_GUiFG#6M3eh`~ZE%HbVwS<>)KcrvHyCC)|mz*hKbp@ljPm^bttcvN1 zbBHfY2=EW&{>szDJdKf;d7NrI^F7Iv;t*P%d@VIoG#+!*eYtnP55|{H+vXR*{KT|| ztPJ%FI}^PsZASW*%o+K6%eIs+^iTDKGApr#_vWEyh3AzmjQTIR6C!#+oRD}7h6uCx6DwubBp znGhYBke8H^{!RAp`LvR^XX?q=f)e%swlU*!*D6foKY ze&G;8UqpKFoT45@eTuTge2iHYyN|yr<$lIs{`At9rR{Em^h^GT5PWDtR9CLDfL{2^ z$=#>E#pU7-c}a=O$onbLzB}mK8ES@>F@c%QDi3H2(Zx9OuL=aoD^qGx&ZXrXUm={6 zeOcrp7D=Yy-eCOz@GAAaLof6TW$g}`7v3Ia7TglN&3u?;N2z0P=U_OuIaLuI z@wTZ$oXy;ixJ)-K;f8-n;1%w%s4FpZj{i`+se}P2?l!){Y-xT~zEFg8yA*w}+)gqN zGX}@QlgQDO#mvpj4wjBx6K2EH@l3II{^zv1#D9n58 zL81-$CHWPZ<=4;t9Cki@C3j`w^RzJ;PNM$8H|0|>H!v@;>9}KVp>EC|B9HrCD~U{R zZxWAWCY4i|Ac`K%xWWhtz7(=P!a06N!ulj!GCuiavTI6NiY(PucsA!r&c_^XZbR-< z(Vc>Y;yvPE0QXYO0WSkRD4HF)~{~kT||MjGDlzAad~Q&{<(V5fzbFat3m?lorr7a0mjMlSeVxm{XV`%x3IXY!db! z){6Vp{k{7*&xfA3kNr%b6Q_CqO!`2Ali&EPq?GwCqO$$U8C8tmSRYvx?D(KE&Isq* zkO^UXBge#Sk9Ccu#%g1q#n0hK@Q(|oq~xXcr5TRj%ftyO!bRD4a+tXl`Bz1sO7E4O zK6A(|0L)|sf>PWHw{Wr_!;{k)`7Q4>uOz-aEv5h}iN+NAHw00_W$6R>l0&B#R+Ub~)Z=<_OYr+VuX@fo=1Q7JzE9pr5mL@l+9+?Rcs4DN5V9@8 zHKH}*eH0^B89$ixCi87pq>z^Lea^xX3ZaSnImh710kk+Lend({#1bBo7YydbrxQLB zG(P#1>lBf%C$*5Cd0lSmgx`{{{{E{018 zKir19hnvA|;c}v`#@^z4CQ6gyl53JTrOZG6JcB2Em>VN1C`6t(T2^_A?T#mV1axqg z2d@m7oN_9COV(=PRpC!!mW1Y}W>yD@6Tf6k$xSWx5ieq71^*Hr0(!g#h;q_W>IbSl zeF@W-xjYg?-j1?~=ENTpLWb)x#h-%2)|9KmwPzN~^^NkUHI?CjaOOGH7W&puQ9Thqv*2c;R2*`$-yMArAg zON5t&HloR;8{EPGJ}fUrTojf$Qt%6Q#Qn49tz#)(p9yW=nWSsvRx*-eLz_ZBOwaUN z=)ay3!`Kuk3|h;X7mN!1G5lFXOyqcOHP<WpzF_ZA zNw_0d%}p14PJQ5$;d|A07xg(~Iy*GLJuoY1UGSUWhml{R;OC=3|l{CYPpOP47v!&6p^>mUk*Y9O&{<#EYaSq%7()(j;=b zZ!#wWybhieHlJ6O@|&;~v(r5i+!RmcoWXQp-S8Aor1w+uG&(V&DdlSFm*d3DMZz6o zop?ssgwsB!ClYGNO+I@l52&+f5B$aiB16U$cobnu?aQMjStJ#Cf*+Q7fW0+ZE6y$P zD3zQ#$FRxn%F`6D^OTW?ed;JB%p*~6Qw_xf1P#3>V%h0enD2;Fsau$-K|jVipDuE5 z^=S8+?X!S3%&ZE|jY&)?74IoYb5Ed){i=f}CLT^cTKX~YNK)15`Dc7FTX5I$3yFaG zn;#UgIbywl3wjDrzN5JqIR9}?RSZxBTWZf*GaEIB+Q7 z+rUY|HesCbe%_R*n%J{fj3u{}hY9|*qc6C7|HpNS!2|1Fh6 z9L?qU2KjK(Kpl5>Y&sa9vNvT@>W+M${NrG5*`HaR%NQZJ=N0QagE^YnxyFl449(63 z<3yyBmyRtZyeBU4t|jGx-zcL=9-QA6zXGr=AcNDw$qOzDeiuA5v=H12TOUpgF9Pl1 zL*XX?HYPNt81UjB^2K0HVhK2!c`xtJ%nzyX6!@+D&)iQsH9c4Yl;NZ4A7ycc`=1G* z0oEmuH!59nqf+`)E`!G6$0zw`I+$00HvC}PXfo(9VKf`0@(K5q0|#avGd7?yU?b-Z zXMM=d&|9Gk!q$Z0!taHLMOFY3w-O9;jZv7mqQub@(d!hduqR&$4ir^`c_pKHqFtC3 zxH@3x_Q3rOGnmsEIx#XSnjTk~ayNa<@#|nP(@)6H4$7$qHH9iLqr|@S1ZE1>gssLM zaNq8M_PFIqCo~XP#0B1uND`_VbkdG7tPFqV6}C60CbTz99_|*&kGIRXD0C?wFF8z3 z3%$KHI;@|BHV!xWjA# z2LfAxHQb-81(|VGar+Y~i7-J}avOM=k(}j_O%$~Qfyk{ak>(P_4PKIn%u?p}VJmPY zqd3(oi=ZZ{D7$FI0Xe~}kn15Q!s*;)yj8qN!Nycm+QhsQMc)^7fIJKy8--heH@bJZ zzj4nX4UiDz`Lq<;TLvkjB+`}J7KMu~h*igq<2NN`rwpZ2j<@CS7rhhxRFE!yDt;y& zc4PUzV4#?5*yQl_aaz&D(gKVQxCNdG5`|`k&lRXr+|v(>ip9T*KbFi-N1YO3gy9|H z-FS)n1CJSA)4eWuo$~JRZYM3H?x4N(OXq9~jtCKjKoL74y|{fT$(eiesUkg~iwUI* zPTR4Bxs<}A*n_z5apCS$yatIj-V?}SG!b)kh)cv_?n{2V;F~mN+KzPJ3{vLYtTEZ1 zqOKC`sYpx#b~-)~ZvbmNWu9@QOQZw}pVCY@OT9<6qfMZ7_-$lNU>U(6>lVudXaV#9 zHD_sXC)gO$1s;U1kK7Px24pUZ*A3jFM$>B*{4dEH(}prjg|~$-gizkDyhEbxqBzmP z0#iXq(P*}90-+xa1Z4@T1)~|ao%kT)Ch};~%|G-?V8!g$@h$KYe^6EInT7NiLtq~KFWQ+G`{H8~lg(d^yZ(#d6``MV8a zol%`h=A=(a^RpW;T8yn*nEMORV&K&>T z2f4m^Kjnk`08wAjZfqR32FJ$7;E%cmy3q&;1S-*v>`A^&IYou&GuSObb{u6WCftDw z@iy}CyojhH@!9be@%s4u#1P^3{Db*B3X2MzidLTZDE2N5DqUPQ-facFluiuxkLrwT zjgR6}-J8iJl#QY4u+oHSC3&a5KXb`1&Oa){22+fwz>sk~+yRev;z^P#rGoN1CD~U; z-R!r6!DW~kQOp?T7Zy7pE6^{^^)KUgqXYE^jPVIIx$Q*+(nPYG`%U_y$fdXv(ivX`HHbFEz8U--_q(`V z{Bk}jF(`RO-nzWRqPZt$wcP1K<{92$n((2;i0^TaZZ_zBA%#AY%Q}%U5)t_voEu| zfbV`gFeqRrCob&_A&>Tj`I3G&Y#PtNb4ygE9n7}NJ&gIu{h9~l?+1ztn5>L&05VbfuJNilNgv3RO zACuRlmt<}h_T~JXr^t5^RTTM&BgNxO(y*`HewJHurG?JKlwz0QOz!tQLw$bnaSZYg>WVoT{{&y!g9rWAo?;4#Jx27oRM4&|BUlsZ!T*|%0?TDM6kdj!PP?~r$dtxsCgnikOvQ2LH zNKgD%v%cfMz_q)_5(qRR?E_8DxEc3#Ymz+y=ZNFEp***G-=~E6uBA~JlbP$8=b7CB z9U(5Evm+g&isCjU6H|s$HEDIYS6;28r=$SibAHY-b^JT2j_HPicLhTHJJzvyc}@qu zk=R7_JiZWT1s-mj$$Z*;`T+e`|4R&Wh(pxGWNMzTxKVr$v)jGYb2;&@kIvu3`4D^} zGAV9B%H{M&Ire$cq8|$$6g!IbCGYXSdK~vu`(E_V46Wra$v>LE?t}?9o7~L#7`%Yj z7`-jQKOt7wCR|KtCeEfr`?*C3c|p;4gqob``EMv(=KEm2K%3en#Fq@8f_>QSW@Q!! zzYMw0e^HIfu~Xv2f|!({ zl!K`U(^GQNbLj=y1(gNu*uG;g!bhTE8Sk^+lZJiqEUD-RcW=Tw(nV@Ajq1NGF-(9= zPc5wUm*Zyoz*yykCc4z`n}C>r#eqQ~4N+aOjVa61dWF*5&-v2}4i}Uc*cZ($I$3mE zy#C~r@@W_{&nNan96sI@|6O878ZnQX_dd^Fgk=%mBwxtQD)2q|>uD~o19R5>BXw1XKRFYN zz&-N3N%o~Ms3+KsoQ~k;@Il_&|HIx}Ksj}Vd*5epch`Xe#flYocjB2OlT6%w;yQ7i z%p@6!j!>+)L-FDiEmn%Vw#6w9^X@YVE%e^|%9Zu4yS{r^KlVv-WS=e1-aC2z&!`!i z4u)BVL*{p8spW-jnY&Z)aAZRqQ#k{30P`@m+*cEEnpa1*33-!T%JYhKl8&mcbyvcs z;{Bf6`oV_ZY3C4&aO3f-iHk{3=vd}C=5QWfFw8zNvMzol-XDdTNf=2E|q)X%nnRZlz4x;%g*e#71_ zC?z+i%A_^l3S4v1Pq2MSne;gfw@5C2BJN^5VLfX`R9A@(9!m|cs5KsUp6 zB{cEV7#e=TXIuwDll2poU8tR+OVdxd-N*RRd*)MYZ z%vJ2SY%TAmc)X;)Vz_FkPHS0Zzf{mR)&nz{VByVF+)^zIED1I*>KhTEA7baS1jt(W z2%W%5iY=14iVupm29K{iAfXk?-pH5QnlOGuB2kl=GZcr6_stJ1^Fphl?Gu{=LV}XI zme<&c_h;hTkXh85)CM#MXPb4L%U4Q{%tY44HpVu^x^ZuDH3>DTZRknnIwqb~=j{zK zSF%n$M0eFX(00|%aISY2cw6`yhJGvh5DO=#Q8#k0aGT5PDC)lb6KTc>X;=o9*Irmd zc2mXI^S%1Oq1gV|l*ArfR|8h?%0D}>G`2c!qOD-ASFhHr@LEEXBcrfmX$NS>#K)9O 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 Date: Wed, 20 Sep 2023 10:25:47 +0200 Subject: [PATCH 152/158] Bump dependencies. --- .github/workflows/env | 2 +- helper/poetry.lock | 48 +++++++-------- pubspec.lock | 140 +++++++++++++++++++++--------------------- 3 files changed, 95 insertions(+), 95 deletions(-) diff --git a/.github/workflows/env b/.github/workflows/env index be866198..3e6f43bf 100644 --- a/.github/workflows/env +++ b/.github/workflows/env @@ -1,2 +1,2 @@ -FLUTTER=3.13.1 +FLUTTER=3.13.4 PYVER=3.11.5 diff --git a/helper/poetry.lock b/helper/poetry.lock index 65a884c8..1aacc057 100755 --- a/helper/poetry.lock +++ b/helper/poetry.lock @@ -114,34 +114,34 @@ files = [ [[package]] name = "cryptography" -version = "41.0.3" +version = "41.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, - {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, - {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, - {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, - {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, - {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, - {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, + {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839"}, + {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143"}, + {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397"}, + {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860"}, + {file = "cryptography-41.0.4-cp37-abi3-win32.whl", hash = "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd"}, + {file = "cryptography-41.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311"}, + {file = "cryptography-41.0.4.tar.gz", hash = "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a"}, ] [package.dependencies] diff --git a/pubspec.lock b/pubspec.lock index 63df709d..399a2619 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: "direct main" description: name: archive - sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e" + sha256: e0902a06f0e00414e4e3438a084580161279f137aeb862274710f29ec10cf01e url: "https://pub.dev" source: hosted - version: "3.3.8" + version: "3.3.9" args: dependency: transitive description: @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: d912852cce27c9e80a93603db721c267716894462e7033165178b91138587972 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.2" build_runner: dependency: "direct dev" description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + sha256: "315a598c7fbe77f22de1c9da7cfd6fd21816312f16ffa124453b4fc679e540f1" url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.6.0" collection: dependency: "direct main" description: @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb url: "https://pub.dev" source: hosted - version: "0.3.3+4" + version: "0.3.3+5" crypto: dependency: "direct main" description: @@ -189,10 +189,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" desktop_drop: dependency: "direct main" description: @@ -229,10 +229,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: bdfa035a974a0c080576c4c8ed01cdf9d1b406a04c7daa05443ef0383a97bedc + sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 url: "https://pub.dev" source: hosted - version: "5.3.4" + version: "5.5.0" fixnum: dependency: transitive description: @@ -255,10 +255,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" flutter_localizations: dependency: "direct main" description: flutter @@ -268,18 +268,18 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.16" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: b04d4e9435a563673746ccb328d22018c6c9496bb547e11dd56c1b0cc9829fe5 + sha256: "1bd39b04f1bcd217a969589777ca6bd642d116e3e5de65c3e6a8e8bdd8b178ec" url: "https://pub.dev" source: hosted - version: "2.3.10" + version: "2.4.0" flutter_test: dependency: "direct dev" description: flutter @@ -488,50 +488,50 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0" + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" + sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" petitparser: dependency: transitive description: @@ -552,10 +552,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" pointycastle: dependency: transitive description: @@ -607,10 +607,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "6c0a2c30c04206ac05494bcccd8148b76866e1a9248a5a8c84ca7b16fbcb3f6a" + sha256: a600120d6f213a9922860eea1abc32597436edd5b2c4e73b91410f8c2af67d22 url: "https://pub.dev" source: hosted - version: "2.3.10" + version: "2.4.0" screen_retriever: dependency: "direct main" description: @@ -623,58 +623,58 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" + sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef + sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.4" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" + sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d + sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" shelf: dependency: transitive description: @@ -820,66 +820,66 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" + sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" url: "https://pub.dev" source: hosted - version: "6.1.12" + version: "6.1.14" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025" + sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 url: "https://pub.dev" source: hosted - version: "6.0.38" + version: "6.1.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.1.5" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" + sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea + sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.5" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 + sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" url: "https://pub.dev" source: hosted - version: "2.0.18" + version: "2.0.20" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" + sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.0.8" uuid: dependency: transitive description: @@ -964,10 +964,10 @@ packages: dependency: transitive description: name: win32 - sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" + sha256: c97defd418eef4ec88c0d1652cdce84b9f7b63dd7198e266d06ac1710d527067 url: "https://pub.dev" source: hosted - version: "5.0.7" + version: "5.0.8" window_manager: dependency: "direct main" description: @@ -980,10 +980,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" xml: dependency: transitive description: @@ -1001,5 +1001,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=3.10.0" + dart: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" From d25a1fddb1a4b0cc80efd6264f3e81386301e15c Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Thu, 21 Sep 2023 11:40:41 +0200 Subject: [PATCH 153/158] improve QRScanner views for responsiveness --- .../qr_scanner/qr_scanner_overlay_view.dart | 190 ++++++++++-------- .../qr_scanner_permissions_view.dart | 106 +++++----- .../qr_scanner/qr_scanner_ui_view.dart | 113 ++++++----- lib/android/qr_scanner/qr_scanner_util.dart | 21 -- lib/android/qr_scanner/qr_scanner_view.dart | 5 +- 5 files changed, 221 insertions(+), 214 deletions(-) delete mode 100644 lib/android/qr_scanner/qr_scanner_util.dart diff --git a/lib/android/qr_scanner/qr_scanner_overlay_view.dart b/lib/android/qr_scanner/qr_scanner_overlay_view.dart index 80590e09..f7032a90 100644 --- a/lib/android/qr_scanner/qr_scanner_overlay_view.dart +++ b/lib/android/qr_scanner/qr_scanner_overlay_view.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * 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. @@ -14,114 +14,142 @@ * limitations under the License. */ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'qr_scanner_scan_status.dart'; -import 'qr_scanner_util.dart'; -/// Return the rounded rect which represents the scanner area for the background -/// overlay and the stroke -RRect _getScannerAreaRRect(Size size) { - double scannerAreaWidth = getScannerAreaWidth(size); - var scannerAreaRect = Rect.fromCenter( - center: Offset(size.width / 2, size.height / 2), - width: scannerAreaWidth, - height: scannerAreaWidth); +class QRScannerOverlay extends StatelessWidget { + final ScanStatus status; + final Size screenSize; + final GlobalKey overlayWidgetKey; - return RRect.fromRectAndRadius( - scannerAreaRect, const Radius.circular(scannerAreaRadius)); + const QRScannerOverlay( + {super.key, + required this.status, + required this.screenSize, + required this.overlayWidgetKey}); + + RRect getOverlayRRect(Size size) { + final renderBox = + overlayWidgetKey.currentContext?.findRenderObject() as RenderBox; + final renderObjectSize = renderBox.size; + final renderObjectOffset = renderBox.globalToLocal(Offset.zero); + + final double shorterEdge = + min(renderObjectSize.width, renderObjectSize.height); + + var top = (size.height - shorterEdge) / 2 - 32; + + if (top + renderObjectOffset.dy < 0) { + top = -renderObjectOffset.dy; + } + + return RRect.fromRectAndRadius( + Rect.fromLTWH( + (size.width - shorterEdge) / 2, top, shorterEdge, shorterEdge), + const Radius.circular(10)); + } + + @override + Widget build(BuildContext context) { + overlayRectProvider(Size size) { + return getOverlayRRect(size); + } + + return Stack(fit: StackFit.expand, children: [ + /// clip scanner area "hole" into a darkened background + ClipPath( + clipper: _OverlayClipper(overlayRectProvider), + child: const Opacity( + opacity: 0.6, + child: ColoredBox( + color: Colors.black, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [Spacer()], + ), + ), + ), + ), + + /// draw a stroke around the scanner area + CustomPaint( + painter: _OverlayPainter(status, overlayRectProvider), + ), + ]); + } } -// CustomPainter which strokes the scannerArea -class _ScannerAreaStrokePainter extends CustomPainter { - final Color _strokeColor; +/// Paints a colored stroke and status icon. +/// The stroke area is acquired through passed in rectangle provider. +/// The color is computed from the scan status. +class _OverlayPainter extends CustomPainter { + final ScanStatus _status; + final Function(Size) _rectProvider; - _ScannerAreaStrokePainter(this._strokeColor) : super(); + _OverlayPainter(this._status, this._rectProvider) : super(); @override void paint(Canvas canvas, Size size) { + final color = _status == ScanStatus.error + ? Colors.red.shade400 + : Colors.green.shade400; Paint paint = Paint() - ..color = _strokeColor + ..color = color ..style = PaintingStyle.stroke ..strokeWidth = 3.0; - Path path = Path()..addRRect(_getScannerAreaRRect(size)); + final RRect overlayRRect = _rectProvider(size); + + Path path = Path()..addRRect(overlayRRect); canvas.drawPath(path, paint); + + if (_status == ScanStatus.success) { + const icon = Icons.check_circle; + final iconSize = + overlayRRect.width < 150 ? overlayRRect.width - 5.0 : 150.0; + TextPainter iconPainter = TextPainter( + textDirection: TextDirection.rtl, + textAlign: TextAlign.center, + ); + iconPainter.text = TextSpan( + text: String.fromCharCode(icon.codePoint), + style: TextStyle( + fontSize: iconSize, + fontFamily: icon.fontFamily, + color: color.withAlpha(240), + )); + iconPainter.layout(); + iconPainter.paint( + canvas, + overlayRRect.center.translate(-iconSize / 2, -iconSize / 2), + ); + } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } -/// clips the scanner area rounded rect of specific Size -class _ScannerAreaClipper extends CustomClipper { +/// Clips a hole into the background. +/// The clipped area is acquired through passed in rectangle provider. +class _OverlayClipper extends CustomClipper { + final Function(Size) _rectProvider; + + _OverlayClipper(this._rectProvider); + @override Path getClip(Size size) { return Path() ..addRect(Rect.fromLTWH(0, 0, size.width, size.height)) - ..addRRect(_getScannerAreaRRect(size)) + ..addRRect(_rectProvider(size)) ..fillType = PathFillType.evenOdd; } @override - bool shouldReclip(covariant CustomClipper oldClipper) => true; -} - -class QRScannerOverlay extends StatelessWidget { - final ScanStatus status; - final Size screenSize; - - const QRScannerOverlay({ - super.key, - required this.status, - required this.screenSize, - }); - - @override - Widget build(BuildContext context) { - var size = screenSize; - - return Stack(children: [ - /// clip scanner area "hole" into a darkened background - ClipPath( - clipper: _ScannerAreaClipper(), - child: const Opacity( - opacity: 0.6, - child: ColoredBox( - color: Colors.black, - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [Spacer()], - )))), - - /// draw a stroke around the scanner area - Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - CustomPaint( - painter: _ScannerAreaStrokePainter(status == ScanStatus.error - ? Colors.red.shade400 - : Colors.green.shade400), - ), - ], - ), - - /// extra icon when successful scan occurred - if (status == ScanStatus.success) - Positioned.fromRect( - rect: Rect.fromCenter( - center: Offset(size.width / 2, size.height / 2), - width: size.width, - height: size.height), - child: Icon( - Icons.check_circle, - size: 200, - color: Colors.green.shade400, - )), - ]); - } + bool shouldReclip(covariant CustomClipper oldClipper) => false; } diff --git a/lib/android/qr_scanner/qr_scanner_permissions_view.dart b/lib/android/qr_scanner/qr_scanner_permissions_view.dart index 30bdb050..935c0520 100644 --- a/lib/android/qr_scanner/qr_scanner_permissions_view.dart +++ b/lib/android/qr_scanner/qr_scanner_permissions_view.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * 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. @@ -18,7 +18,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'qr_scanner_scan_status.dart'; -import 'qr_scanner_util.dart'; class QRScannerPermissionsUI extends StatelessWidget { final ScanStatus status; @@ -34,71 +33,62 @@ class QRScannerPermissionsUI extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final scannerAreaWidth = getScannerAreaWidth(screenSize); - return Stack(children: [ - /// instruction text under the scanner area - Positioned.fromRect( - rect: Rect.fromCenter( - center: Offset(screenSize.width / 2, - screenSize.height - scannerAreaWidth / 2.0 + 8.0), - width: screenSize.width, - height: screenSize.height), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 36), - child: Text( + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( l10n.p_need_camera_permission, style: const TextStyle(color: Colors.white), textAlign: TextAlign.center, ), - )), - - /// button for manual entry - Positioned.fromRect( - rect: Rect.fromCenter( - center: Offset(screenSize.width / 2, screenSize.height), - width: screenSize.width, - height: screenSize.height), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Text( - l10n.q_have_account_info, - textScaleFactor: 0.7, - style: const TextStyle(color: Colors.white), - ), - OutlinedButton( - onPressed: () { - Navigator.of(context).pop(''); - }, - child: Text( - l10n.s_enter_manually, + Column( + children: [ + Text( + l10n.q_have_account_info, + textScaleFactor: 0.7, style: const TextStyle(color: Colors.white), - )), - ], - ), - Column( - children: [ - Text( - l10n.q_want_to_scan, - textScaleFactor: 0.7, - style: const TextStyle(color: Colors.white), + ), + OutlinedButton( + onPressed: () { + Navigator.of(context).pop(''); + }, + child: Text( + l10n.s_enter_manually, + style: const TextStyle(color: Colors.white), + )), + ], ), - OutlinedButton( - onPressed: () { - onPermissionRequest(); - }, - child: Text( - l10n.s_review_permissions, + Column( + children: [ + Text( + l10n.q_want_to_scan, + textScaleFactor: 0.7, style: const TextStyle(color: Colors.white), - )), - ], - ) - ]), + ), + OutlinedButton( + onPressed: () { + onPermissionRequest(); + }, + child: Text( + l10n.s_review_permissions, + style: const TextStyle(color: Colors.white), + )), + ], + ) + ]) + ], + ), ), - ]); + ); } } diff --git a/lib/android/qr_scanner/qr_scanner_ui_view.dart b/lib/android/qr_scanner/qr_scanner_ui_view.dart index 440f1caf..45c396e8 100644 --- a/lib/android/qr_scanner/qr_scanner_ui_view.dart +++ b/lib/android/qr_scanner/qr_scanner_ui_view.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * 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. @@ -19,69 +19,76 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../keys.dart' as keys; import 'qr_scanner_scan_status.dart'; -import 'qr_scanner_util.dart'; class QRScannerUI extends StatelessWidget { final ScanStatus status; final Size screenSize; + final GlobalKey overlayWidgetKey; - const QRScannerUI({ - super.key, - required this.status, - required this.screenSize, - }); + const QRScannerUI( + {super.key, + required this.status, + required this.screenSize, + required this.overlayWidgetKey}); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final scannerAreaWidth = getScannerAreaWidth(screenSize); - return Stack(children: [ - /// instruction text under the scanner area - Positioned.fromRect( - rect: Rect.fromCenter( - center: Offset(screenSize.width / 2, - screenSize.height + scannerAreaWidth / 2.0 + 8.0), - width: screenSize.width, - height: screenSize.height), - child: Padding( - padding: const EdgeInsets.all(4.0), - child: Text( - status != ScanStatus.error - ? l10n.l_point_camera_scan - : l10n.l_invalid_qr, - style: const TextStyle(color: Colors.white), - textAlign: TextAlign.center, - ), - ), - ), - - /// button for manual entry - Positioned.fromRect( - rect: Rect.fromCenter( - center: Offset(screenSize.width / 2, - screenSize.height + scannerAreaWidth / 2.0 + 80.0), - width: screenSize.width, - height: screenSize.height), - child: Column( - children: [ - Text( - l10n.q_no_qr, - textScaleFactor: 0.7, - style: const TextStyle(color: Colors.white), - ), - OutlinedButton( - onPressed: () { - Navigator.of(context).pop(''); - }, - key: keys.manualEntryButton, + return Stack( + fit: StackFit.expand, + children: [ + SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 16, right: 16, top: 0, bottom: 0), + child: SizedBox( + // other widgets can find the RenderObject of this + // widget by its key value and query its size and offset. + key: overlayWidgetKey, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 0.0), child: Text( - l10n.s_enter_manually, + status != ScanStatus.error + ? l10n.l_point_camera_scan + : l10n.l_invalid_qr, style: const TextStyle(color: Colors.white), - )), - ], - ), - ), - ]); + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 16), + Column( + children: [ + Text( + l10n.q_no_qr, + textScaleFactor: 0.7, + style: const TextStyle(color: Colors.white), + ), + OutlinedButton( + onPressed: () { + Navigator.of(context).pop(''); + }, + key: keys.manualEntryButton, + child: Text( + l10n.s_enter_manually, + style: const TextStyle(color: Colors.white), + )), + ], + ), + const SizedBox(height: 8) + ], + ), + ) + ], + ); } } diff --git a/lib/android/qr_scanner/qr_scanner_util.dart b/lib/android/qr_scanner/qr_scanner_util.dart deleted file mode 100644 index c8f10b3b..00000000 --- a/lib/android/qr_scanner/qr_scanner_util.dart +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 'dart:ui'; - -const double scannerAreaRadius = 40.0; - -double getScannerAreaWidth(Size size) => size.width - scannerAreaRadius; diff --git a/lib/android/qr_scanner/qr_scanner_view.dart b/lib/android/qr_scanner/qr_scanner_view.dart index 56ca3db2..9ca1e0c8 100755 --- a/lib/android/qr_scanner/qr_scanner_view.dart +++ b/lib/android/qr_scanner/qr_scanner_view.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * 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. @@ -103,6 +103,7 @@ class _QrScannerViewState extends State { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final screenSize = MediaQuery.of(context).size; + final overlayWidgetKey = GlobalKey(); return Scaffold( resizeToAvoidBottomInset: false, extendBodyBehindAppBar: true, @@ -153,12 +154,14 @@ class _QrScannerViewState extends State { child: QRScannerOverlay( status: _status, screenSize: screenSize, + overlayWidgetKey: overlayWidgetKey, )), Visibility( visible: _permissionsGranted, child: QRScannerUI( status: _status, screenSize: screenSize, + overlayWidgetKey: overlayWidgetKey, )), Visibility( visible: _previewInitialized && !_permissionsGranted, From 6be42ba0b39700f9e3e51b05c424aa96154cb0cb Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Fri, 22 Sep 2023 11:01:15 +0200 Subject: [PATCH 154/158] wrap ListTile into Material --- lib/app/views/navigation.dart | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/app/views/navigation.dart b/lib/app/views/navigation.dart index 3543d451..f2734b49 100644 --- a/lib/app/views/navigation.dart +++ b/lib/app/views/navigation.dart @@ -71,15 +71,18 @@ class NavigationItem extends StatelessWidget { ), ); } else { - return ListTile( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(48)), - leading: leading, - title: Text(title), - minVerticalPadding: 16, - onTap: onTap, - tileColor: selected ? colorScheme.secondaryContainer : null, - textColor: selected ? colorScheme.onSecondaryContainer : null, - iconColor: selected ? colorScheme.onSecondaryContainer : null, + return Material( + type: MaterialType.transparency, + child: ListTile( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(48)), + leading: leading, + title: Text(title), + minVerticalPadding: 16, + onTap: onTap, + tileColor: selected ? colorScheme.secondaryContainer : null, + textColor: selected ? colorScheme.onSecondaryContainer : null, + iconColor: selected ? colorScheme.onSecondaryContainer : null, + ), ); } } From 3d01d7867eaccd2bc09003fd7f83e97c58056346 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Fri, 22 Sep 2023 11:28:19 +0200 Subject: [PATCH 155/158] wrap only one widget into Material --- lib/app/views/app_page.dart | 4 +++- lib/app/views/navigation.dart | 21 +++++++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/app/views/app_page.dart b/lib/app/views/app_page.dart index f4e14ecd..773fd634 100755 --- a/lib/app/views/app_page.dart +++ b/lib/app/views/app_page.dart @@ -131,7 +131,9 @@ class AppPage extends StatelessWidget { const SizedBox(width: 48), ], ), - NavigationContent(key: _navExpandedKey, extended: true), + Material( + type: MaterialType.transparency, + child: NavigationContent(key: _navExpandedKey, extended: true)), ], ), ), diff --git a/lib/app/views/navigation.dart b/lib/app/views/navigation.dart index f2734b49..3543d451 100644 --- a/lib/app/views/navigation.dart +++ b/lib/app/views/navigation.dart @@ -71,18 +71,15 @@ class NavigationItem extends StatelessWidget { ), ); } else { - return Material( - type: MaterialType.transparency, - child: ListTile( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(48)), - leading: leading, - title: Text(title), - minVerticalPadding: 16, - onTap: onTap, - tileColor: selected ? colorScheme.secondaryContainer : null, - textColor: selected ? colorScheme.onSecondaryContainer : null, - iconColor: selected ? colorScheme.onSecondaryContainer : null, - ), + return ListTile( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(48)), + leading: leading, + title: Text(title), + minVerticalPadding: 16, + onTap: onTap, + tileColor: selected ? colorScheme.secondaryContainer : null, + textColor: selected ? colorScheme.onSecondaryContainer : null, + iconColor: selected ? colorScheme.onSecondaryContainer : null, ); } } From 6c23e27f35da9e9616d300d73b1f46b35c4f9eb2 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Fri, 22 Sep 2023 14:24:38 +0200 Subject: [PATCH 156/158] catch exceptions during refresh --- .../yubico/authenticator/oath/OathManager.kt | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt index 977b10f0..85b3cad2 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt @@ -62,6 +62,7 @@ import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.* import kotlinx.serialization.encodeToString import org.slf4j.LoggerFactory +import java.io.IOException import java.net.URI import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean @@ -554,29 +555,50 @@ class OathManager( NULL } - private suspend fun requestRefresh() = + private suspend fun requestRefresh() { + + val clearCodes = { + val currentCredentials = oathViewModel.credentials.value + oathViewModel.updateCredentials(currentCredentials?.associate { + it.credential to null + } ?: emptyMap()) + } + appViewModel.connectedYubiKey.value?.let { usbYubiKeyDevice -> - useOathSessionUsb(usbYubiKeyDevice) { session -> - try { - oathViewModel.updateCredentials(calculateOathCodes(session)) - } catch (apduException: ApduException) { - if (apduException.sw == SW.SECURITY_CONDITION_NOT_SATISFIED) { - logger.debug("Handled oath credential refresh on locked session.") - oathViewModel.setSessionState( - Session( - session, - keyManager.isRemembered(session.deviceId) + try { + useOathSessionUsb(usbYubiKeyDevice) { session -> + try { + oathViewModel.updateCredentials(calculateOathCodes(session)) + } catch (ioException: IOException) { + logger.error("Exception while calculating codes: ", ioException) + clearCodes() + } catch (apduException: ApduException) { + if (apduException.sw == SW.SECURITY_CONDITION_NOT_SATISFIED) { + logger.debug("Handled oath credential refresh on locked session.") + oathViewModel.setSessionState( + Session( + session, + keyManager.isRemembered(session.deviceId) + ) ) - ) - } else { - logger.error( - "Unexpected sw when refreshing oath credentials", - apduException - ) + } else { + logger.error( + "Unexpected sw when refreshing oath credentials", + apduException + ) + } } } + } catch (ioException: IOException) { + logger.error("IOException when accessing USB device: ", ioException) + clearCodes() + } catch (illegalStateException: IllegalStateException) { + logger.error("IllegalStateException when accessing USB device: ", illegalStateException) + clearCodes() } } + } + private suspend fun calculate(credentialId: String): String = useOathSession(OathActionDescription.CalculateCode) { session -> From dd3b91a05a2fc3789bd88b1f9d070b6926b42bc4 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Fri, 22 Sep 2023 18:56:12 +0200 Subject: [PATCH 157/158] add BufferAppender --- android/app/src/main/assets/logback.xml | 8 ++- .../authenticator/logging/BufferAppender.kt | 54 +++++++++++++++++++ .../authenticator/logging/FlutterLog.kt | 11 +++- .../com/yubico/authenticator/logging/Log.kt | 20 ++----- 4 files changed, 73 insertions(+), 20 deletions(-) create mode 100644 android/app/src/main/kotlin/com/yubico/authenticator/logging/BufferAppender.kt diff --git a/android/app/src/main/assets/logback.xml b/android/app/src/main/assets/logback.xml index 2d8d6982..b53ea3b4 100644 --- a/android/app/src/main/assets/logback.xml +++ b/android/app/src/main/assets/logback.xml @@ -24,8 +24,14 @@ - + + + %d{HH:mm:ss:SSS} [%thread] %-5level %logger{36} - %X{app} %msg + + + + \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/logging/BufferAppender.kt b/android/app/src/main/kotlin/com/yubico/authenticator/logging/BufferAppender.kt new file mode 100644 index 00000000..fc0dfca9 --- /dev/null +++ b/android/app/src/main/kotlin/com/yubico/authenticator/logging/BufferAppender.kt @@ -0,0 +1,54 @@ +/* + * 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. + */ + +package com.yubico.authenticator.logging + +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.UnsynchronizedAppenderBase + +class BufferAppender : UnsynchronizedAppenderBase() { + + private var encoder: PatternLayoutEncoder? = null + + private val buffer = arrayListOf() + + public override fun append(event: ILoggingEvent) { + if (!isStarted) { + return + } + + if (buffer.size > MAX_BUFFER_SIZE) { + buffer.removeAt(0) + } + + buffer.add(encoder!!.layout.doLayout(event)) + } + + fun getEncoder(): PatternLayoutEncoder? = encoder + + fun setEncoder(encoder: PatternLayoutEncoder?) { + this.encoder = encoder + } + + fun getLogBuffer(): ArrayList { + return buffer + } + + companion object { + private const val MAX_BUFFER_SIZE = 1000 + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/logging/FlutterLog.kt b/android/app/src/main/kotlin/com/yubico/authenticator/logging/FlutterLog.kt index 6d095c6a..71f86fac 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/logging/FlutterLog.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/logging/FlutterLog.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * 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. @@ -18,11 +18,18 @@ package com.yubico.authenticator.logging import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodChannel +import org.slf4j.Logger +import org.slf4j.LoggerFactory class FlutterLog(messenger: BinaryMessenger) { private var channel = MethodChannel(messenger, "android.log.redirect") + private val bufferAppender = + (LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger) + .getAppender("buffer") as BufferAppender + init { + channel.setMethodCallHandler { call, result -> when (call.method) { @@ -53,7 +60,7 @@ class FlutterLog(messenger: BinaryMessenger) { result.success(null) } "getLogs" -> { - result.success(Log.getBuffer()) + result.success(bufferAppender.getLogBuffer()) } else -> { result.notImplemented() diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/logging/Log.kt b/android/app/src/main/kotlin/com/yubico/authenticator/logging/Log.kt index ce11c8a4..1250bd9e 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/logging/Log.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/logging/Log.kt @@ -24,7 +24,7 @@ import org.slf4j.LoggerFactory object Log { - private val logger = LoggerFactory.getLogger("com.yubico.authenticator") + private val logger = LoggerFactory.getLogger("com.yubico.authenticator.Log") enum class LogLevel { TRAFFIC, @@ -34,13 +34,6 @@ object Log { ERROR } - private const val MAX_BUFFER_SIZE = 1000 - private val buffer = arrayListOf() - - fun getBuffer() : List { - return buffer - } - private var level = if (BuildConfig.DEBUG) { LogLevel.DEBUG } else { @@ -56,17 +49,10 @@ object Log { return } - if (buffer.size > MAX_BUFFER_SIZE) { - buffer.removeAt(0) - } - val logMessage = (if (error == null) - "[$loggerName] ${level.name}: $message" + "$message [$loggerName]" else - "[$loggerName] ${level.name}: $message (err: $error)" - ).also { - buffer.add(it) - } + "$message [$loggerName] (err: $error)") when (level) { LogLevel.TRAFFIC -> logger.trace(logMessage) From 452491a4bb507dece584c9741ef50e943298cd58 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Mon, 25 Sep 2023 14:21:22 +0200 Subject: [PATCH 158/158] catch IOException in outer scope only --- .../main/kotlin/com/yubico/authenticator/oath/OathManager.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt index 85b3cad2..b1562d89 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt @@ -569,9 +569,6 @@ class OathManager( useOathSessionUsb(usbYubiKeyDevice) { session -> try { oathViewModel.updateCredentials(calculateOathCodes(session)) - } catch (ioException: IOException) { - logger.error("Exception while calculating codes: ", ioException) - clearCodes() } catch (apduException: ApduException) { if (apduException.sw == SW.SECURITY_CONDITION_NOT_SATISFIED) { logger.debug("Handled oath credential refresh on locked session.")