From 96966673ce6225d46554a1787dbc3785b3b87ebb Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 30 May 2023 09:31:50 +0200 Subject: [PATCH 01/39] add padding --- lib/management/views/management_screen.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/management/views/management_screen.dart b/lib/management/views/management_screen.dart index e49d4cb5..c68933a2 100755 --- a/lib/management/views/management_screen.dart +++ b/lib/management/views/management_screen.dart @@ -115,7 +115,10 @@ class _CapabilitiesForm extends StatelessWidget { children: [ if (usbCapabilities != 0) ...[ ListTile( - leading: const Icon(Icons.usb), + leading: const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Icon(Icons.usb), + ), title: Text(l10n.s_usb), contentPadding: const EdgeInsets.only(bottom: 8), horizontalTitleGap: 0, @@ -136,7 +139,10 @@ class _CapabilitiesForm extends StatelessWidget { child: Divider(), ), ListTile( - leading: nfcIcon, + leading: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: nfcIcon, + ), title: Text(l10n.s_nfc), contentPadding: const EdgeInsets.only(bottom: 8), horizontalTitleGap: 0, From 3f24040ff24f5f501b411fc4354ab73e49123b39 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 30 May 2023 16:36:46 +0200 Subject: [PATCH 02/39] use default theme values for title gap --- lib/management/views/management_screen.dart | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/management/views/management_screen.dart b/lib/management/views/management_screen.dart index c68933a2..06731696 100755 --- a/lib/management/views/management_screen.dart +++ b/lib/management/views/management_screen.dart @@ -115,13 +115,9 @@ class _CapabilitiesForm extends StatelessWidget { children: [ if (usbCapabilities != 0) ...[ ListTile( - leading: const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Icon(Icons.usb), - ), + leading: const Icon(Icons.usb), title: Text(l10n.s_usb), contentPadding: const EdgeInsets.only(bottom: 8), - horizontalTitleGap: 0, ), _CapabilityForm( type: _CapabilityType.usb, @@ -139,13 +135,9 @@ class _CapabilitiesForm extends StatelessWidget { child: Divider(), ), ListTile( - leading: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: nfcIcon, - ), + leading: nfcIcon, title: Text(l10n.s_nfc), contentPadding: const EdgeInsets.only(bottom: 8), - horizontalTitleGap: 0, ), _CapabilityForm( type: _CapabilityType.nfc, From 0a055d116e304f6c048cdb195198e0d316fac47e Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 30 May 2023 17:19:36 +0200 Subject: [PATCH 03/39] bump yubikit to 2.3.0 --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index abc6dde0..c91fe98b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -24,7 +24,7 @@ allprojects { targetSdkVersion = 33 compileSdkVersion = 33 - yubiKitVersion = "2.2.0" + yubiKitVersion = "2.3.0" junitVersion = "4.13.2" mockitoVersion = "5.3.1" } From 8ffc088bce3a396a95c0ec1a26b7d6b57d615ec3 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Tue, 30 May 2023 17:47:13 +0200 Subject: [PATCH 04/39] update logging to slf4j --- android/app/build.gradle | 2 + android/app/src/main/assets/logback.xml | 31 ++++++++ .../com/yubico/authenticator/AppContext.kt | 14 ++-- .../yubico/authenticator/AppPreferences.kt | 17 +++-- .../com/yubico/authenticator/ClipboardUtil.kt | 7 +- .../com/yubico/authenticator/DialogManager.kt | 6 +- .../com/yubico/authenticator/MainActivity.kt | 48 +++++------- .../com/yubico/authenticator/NdefActivity.kt | 16 ++-- .../com/yubico/authenticator/logging/Log.kt | 73 ++++++++----------- .../oath/AppLinkMethodChannel.kt | 15 ++-- .../yubico/authenticator/oath/OathManager.kt | 50 ++++++------- .../authenticator/yubikit/DeviceInfoHelper.kt | 13 ++-- 12 files changed, 149 insertions(+), 143 deletions(-) create mode 100644 android/app/src/main/assets/logback.xml diff --git a/android/app/build.gradle b/android/app/build.gradle index 92159109..d1d735d0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -105,6 +105,8 @@ dependencies { implementation 'com.google.android.material:material:1.9.0' + implementation 'com.github.tony19:logback-android:3.0.0' + // testing dependencies testImplementation "junit:junit:$project.junitVersion" testImplementation "org.mockito:mockito-core:$project.mockitoVersion" diff --git a/android/app/src/main/assets/logback.xml b/android/app/src/main/assets/logback.xml new file mode 100644 index 00000000..2d8d6982 --- /dev/null +++ b/android/app/src/main/assets/logback.xml @@ -0,0 +1,31 @@ + + + + + + + %msg + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/AppContext.kt b/android/app/src/main/kotlin/com/yubico/authenticator/AppContext.kt index 71a9a413..2106df02 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/AppContext.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/AppContext.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. @@ -16,14 +16,18 @@ package com.yubico.authenticator -import com.yubico.authenticator.logging.Log import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodChannel + import kotlinx.coroutines.CoroutineScope +import org.slf4j.LoggerFactory + class AppContext(messenger: BinaryMessenger, coroutineScope: CoroutineScope, private val appViewModel: MainViewModel) { private val channel = MethodChannel(messenger, "android.state.appContext") + private val logger = LoggerFactory.getLogger(AppContext::class.java) + init { channel.setHandler(coroutineScope) { method, args -> when (method) { @@ -36,11 +40,7 @@ class AppContext(messenger: BinaryMessenger, coroutineScope: CoroutineScope, pri private suspend fun setContext(subPageIndex: Int): String { val appContext = OperationContext.getByValue(subPageIndex) appViewModel.setAppContext(appContext) - Log.d(TAG, "App context is now $appContext") + logger.debug("App context is now {}", appContext) return NULL } - - companion object { - const val TAG = "appContext" - } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/AppPreferences.kt b/android/app/src/main/kotlin/com/yubico/authenticator/AppPreferences.kt index e5afc9c7..2301e6d7 100755 --- a/android/app/src/main/kotlin/com/yubico/authenticator/AppPreferences.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/AppPreferences.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. @@ -19,7 +19,8 @@ package com.yubico.authenticator import android.content.Context import android.content.SharedPreferences import android.content.SharedPreferences.OnSharedPreferenceChangeListener -import com.yubico.authenticator.logging.Log + +import org.slf4j.LoggerFactory class AppPreferences(context: Context) { companion object { @@ -32,15 +33,15 @@ class AppPreferences(context: Context) { const val PREF_CLIP_KBD_LAYOUT = "flutter.prefClipKbdLayout" const val DEFAULT_CLIP_KBD_LAYOUT = "US" - - const val TAG = "AppPreferences" } + private val logger = LoggerFactory.getLogger(AppContext::class.java) + private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE).also { - Log.d(TAG, "Current app preferences:") + logger.debug("Current app preferences:") it.all.map { preference -> - Log.d(TAG, "${preference.key}: ${preference.value}") + logger.debug("{}: {}", preference.key, preference.value) } } @@ -66,12 +67,12 @@ class AppPreferences(context: Context) { get() = prefs.getBoolean(PREF_USB_OPEN_APP, false) fun registerListener(listener: OnSharedPreferenceChangeListener) { - Log.d(TAG, "registering change listener") + logger.debug("registering change listener") prefs.registerOnSharedPreferenceChangeListener(listener) } fun unregisterListener(listener: OnSharedPreferenceChangeListener) { prefs.unregisterOnSharedPreferenceChangeListener(listener) - Log.d(TAG, "unregistered change listener") + logger.debug("unregistered change listener") } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/ClipboardUtil.kt b/android/app/src/main/kotlin/com/yubico/authenticator/ClipboardUtil.kt index 1121b0f5..3feff47e 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/ClipboardUtil.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/ClipboardUtil.kt @@ -23,11 +23,12 @@ import android.content.ClipboardManager import android.content.Context import android.os.Build import android.os.PersistableBundle -import com.yubico.authenticator.logging.Log + +import org.slf4j.LoggerFactory object ClipboardUtil { - private const val TAG = "ClipboardUtil" + private val logger = LoggerFactory.getLogger(ClipboardUtil::class.java) fun setPrimaryClip(context: Context, toClipboard: String, isSensitive: Boolean) { try { @@ -41,7 +42,7 @@ object ClipboardUtil { clipboardManager.setPrimaryClip(clipData) } catch (e: Exception) { - Log.e(TAG, "Failed to set string to clipboard", e.stackTraceToString()) + logger.error( "Failed to set string to clipboard", e) throw UnsupportedOperationException() } } 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..49e8214f 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. @@ -91,8 +91,4 @@ class DialogManager(messenger: BinaryMessenger, private val coroutineScope: Coro } return NULL } - - companion object { - const val TAG = "dialogManager" - } } \ No newline at end of file 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..173eb214 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt @@ -39,7 +39,6 @@ import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope import com.yubico.authenticator.logging.FlutterLog -import com.yubico.authenticator.logging.Log import com.yubico.authenticator.oath.AppLinkMethodChannel import com.yubico.authenticator.oath.OathManager import com.yubico.authenticator.oath.OathViewModel @@ -48,7 +47,6 @@ import com.yubico.yubikit.android.transport.nfc.NfcConfiguration import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice import com.yubico.yubikit.android.transport.usb.UsbConfiguration -import com.yubico.yubikit.core.Logger import com.yubico.yubikit.core.YubiKeyDevice import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.FlutterEngine @@ -56,6 +54,7 @@ import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.launch import org.json.JSONObject +import org.slf4j.LoggerFactory import java.io.Closeable import java.util.concurrent.Executors @@ -73,6 +72,8 @@ class MainActivity : FlutterFragmentActivity() { private val qrScannerCameraClosedBR = QRScannerCameraClosedBR() private val nfcAdapterStateChangeBR = NfcAdapterStateChangedBR() + private val logger = LoggerFactory.getLogger(MainActivity::class.java) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -85,8 +86,6 @@ class MainActivity : FlutterFragmentActivity() { allowScreenshots(false) yubikit = YubiKitManager(this) - - setupYubiKitLogger() } /** @@ -117,7 +116,7 @@ class MainActivity : FlutterFragmentActivity() { private fun startNfcDiscovery() = try { - Log.d(TAG, "Starting nfc discovery") + logger.debug("Starting nfc discovery") yubikit.startNfcDiscovery( nfcConfiguration.disableNfcDiscoverySound(appPreferences.silenceNfcSounds), this, @@ -131,16 +130,16 @@ class MainActivity : FlutterFragmentActivity() { private fun stopNfcDiscovery() { if (hasNfc) { yubikit.stopNfcDiscovery(this) - Log.d(TAG, "Stopped nfc discovery") + logger.debug("Stopped nfc discovery") } } private fun startUsbDiscovery() { - Log.d(TAG, "Starting usb discovery") + logger.debug("Starting usb discovery") val usbConfiguration = UsbConfiguration().handlePermissions(true) yubikit.startUsbDiscovery(usbConfiguration) { device -> viewModel.setConnectedYubiKey(device) { - Log.d(TAG, "YubiKey was disconnected, stopping usb discovery") + logger.debug("YubiKey was disconnected, stopping usb discovery") stopUsbDiscovery() } processYubiKey(device) @@ -149,22 +148,7 @@ class MainActivity : FlutterFragmentActivity() { private fun stopUsbDiscovery() { yubikit.stopUsbDiscovery() - Log.d(TAG, "Stopped usb discovery") - } - - private fun setupYubiKitLogger() { - Logger.setLogger(object : Logger() { - private val TAG = "yubikit" - - override fun logDebug(message: String) { - // redirect yubikit debug logs to traffic - Log.t(TAG, message) - } - - override fun logError(message: String, throwable: Throwable) { - Log.e(TAG, message, throwable.message ?: throwable.toString()) - } - }) + logger.debug("Stopped usb discovery") } @SuppressLint("WrongConstant") @@ -230,7 +214,7 @@ class MainActivity : FlutterFragmentActivity() { startNfcDiscovery() } } catch (e: Throwable) { - Log.e(TAG, "Error processing YubiKey in AppContextManager", e.toString()) + logger.error("Error processing YubiKey in AppContextManager", e) } } } else { @@ -280,7 +264,7 @@ class MainActivity : FlutterFragmentActivity() { try { it.processYubiKey(device) } catch (e: Throwable) { - Log.e(TAG, "Error processing YubiKey in AppContextManager", e.toString()) + logger.error("Error processing YubiKey in AppContextManager", e) } } } @@ -336,7 +320,6 @@ class MainActivity : FlutterFragmentActivity() { } companion object { - const val TAG = "MainActivity" const val YUBICO_VENDOR_ID = 4176 const val FLAG_SECURE = WindowManager.LayoutParams.FLAG_SECURE } @@ -363,6 +346,9 @@ class MainActivity : FlutterFragmentActivity() { } class NfcAdapterStateChangedBR : BroadcastReceiver() { + + private val logger = LoggerFactory.getLogger(NfcAdapterStateChangedBR::class.java) + companion object { val intentFilter = IntentFilter("android.nfc.action.ADAPTER_STATE_CHANGED") } @@ -370,7 +356,7 @@ class MainActivity : FlutterFragmentActivity() { override fun onReceive(context: Context?, intent: Intent?) { intent?.let { val state = it.getIntExtra("android.nfc.extra.ADAPTER_STATE", 0) - Log.d(TAG, "NfcAdapter state changed to $state") + logger.debug("NfcAdapter state changed to {}", state) if (state == STATE_ON || state == STATE_TURNING_OFF) { (context as? MainActivity)?.appMethodChannel?.nfcAdapterStateChanged(state == STATE_ON) } @@ -430,7 +416,7 @@ class MainActivity : FlutterFragmentActivity() { startActivity(Intent(ACTION_NFC_SETTINGS)) result.success(true) } - else -> Log.w(TAG, "Unknown app method: ${methodCall.method}") + else -> logger.warn("Unknown app method: {}", methodCall.method) } } } @@ -446,10 +432,10 @@ class MainActivity : FlutterFragmentActivity() { private fun allowScreenshots(value: Boolean): Boolean { // Note that FLAG_SECURE is the inverse of allowScreenshots if (value) { - Log.d(TAG, "Clearing FLAG_SECURE (allow screenshots)") + logger.debug("Clearing FLAG_SECURE (allow screenshots)") window.clearFlags(FLAG_SECURE) } else { - Log.d(TAG, "Setting FLAG_SECURE (disallow screenshots)") + logger.debug("Setting FLAG_SECURE (disallow screenshots)") window.setFlags(FLAG_SECURE, FLAG_SECURE) } diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/NdefActivity.kt b/android/app/src/main/kotlin/com/yubico/authenticator/NdefActivity.kt index 47c5ece5..662150f6 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/NdefActivity.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/NdefActivity.kt @@ -24,9 +24,12 @@ import android.nfc.Tag import android.os.Build import android.os.Bundle import android.widget.Toast -import com.yubico.authenticator.logging.Log + import com.yubico.authenticator.ndef.KeyboardLayout import com.yubico.yubikit.core.util.NdefUtils + +import org.slf4j.LoggerFactory + import java.nio.charset.StandardCharsets typealias ResourceId = Int @@ -34,6 +37,8 @@ typealias ResourceId = Int class NdefActivity : Activity() { private lateinit var appPreferences: AppPreferences + private val logger = LoggerFactory.getLogger(NdefActivity::class.java) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) appPreferences = AppPreferences(this) @@ -68,10 +73,9 @@ class NdefActivity : Activity() { } } catch (illegalArgumentException: IllegalArgumentException) { - Log.e( - TAG, + logger.error( illegalArgumentException.message ?: "Failure when handling YubiKey OTP", - illegalArgumentException.stackTraceToString() + illegalArgumentException ) showToast(R.string.otp_parse_failure, Toast.LENGTH_LONG) } catch (_: UnsupportedOperationException) { @@ -111,10 +115,6 @@ class NdefActivity : Activity() { } } - companion object { - const val TAG = "YubicoAuthenticatorOTPActivity" - } - enum class OtpType { Otp, Password } 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 5d33e0e9..ce11c8a4 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 @@ -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. @@ -16,11 +16,16 @@ package com.yubico.authenticator.logging -import android.util.Log +import ch.qos.logback.classic.Level import com.yubico.authenticator.BuildConfig +import org.slf4j.Logger +import org.slf4j.LoggerFactory + object Log { + private val logger = LoggerFactory.getLogger("com.yubico.authenticator") + enum class LogLevel { TRAFFIC, DEBUG, @@ -42,34 +47,10 @@ object Log { LogLevel.INFO } - private const val TAG = "yubico-authenticator" - - @Suppress("unused") - fun t(tag: String, message: String, error: String? = null) { - log(LogLevel.TRAFFIC, tag, message, error) + init { + setLevel(level) } - @Suppress("unused") - fun d(tag: String, message: String, error: String? = null) { - log(LogLevel.DEBUG, tag, message, error) - } - - @Suppress("unused") - fun i(tag: String, message: String, error: String? = null) { - log(LogLevel.INFO, tag, message, error) - } - - @Suppress("unused") - fun w(tag: String, message: String, error: String? = null) { - log(LogLevel.WARNING, tag, message, error) - } - - @Suppress("unused") - fun e(tag: String, message: String, error: String? = null) { - log(LogLevel.ERROR, tag, message, error) - } - - @Suppress("unused") fun log(level: LogLevel, loggerName: String, message: String, error: String?) { if (level < this.level) { return @@ -79,27 +60,33 @@ object Log { buffer.removeAt(0) } - val logMessage = "[$loggerName] ${level.name}: $message".also { - buffer.add(it) - } + val logMessage = (if (error == null) + "[$loggerName] ${level.name}: $message" + else + "[$loggerName] ${level.name}: $message (err: $error)" + ).also { + buffer.add(it) + } when (level) { - LogLevel.TRAFFIC -> Log.v(TAG, logMessage) - LogLevel.DEBUG -> Log.d(TAG, logMessage) - LogLevel.INFO -> Log.i(TAG, logMessage) - LogLevel.WARNING -> Log.w(TAG, logMessage) - LogLevel.ERROR -> Log.e(TAG, logMessage) - } - - error?.let { - Log.e(TAG, "[$loggerName] ${level.name}(details): $error".also { - buffer.add(it) - }) + LogLevel.TRAFFIC -> logger.trace(logMessage) + LogLevel.DEBUG -> logger.debug(logMessage) + LogLevel.INFO -> logger.info(logMessage) + LogLevel.WARNING -> logger.warn(logMessage) + LogLevel.ERROR -> logger.error(logMessage) } } - @Suppress("unused") fun setLevel(newLevel: LogLevel) { level = newLevel + + val root = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger + root.level = when (newLevel) { + LogLevel.TRAFFIC -> Level.TRACE + LogLevel.DEBUG -> Level.DEBUG + LogLevel.INFO -> Level.INFO + LogLevel.WARNING -> Level.WARN + LogLevel.ERROR -> Level.ERROR + } } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/AppLinkMethodChannel.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/AppLinkMethodChannel.kt index 7cb9cd4b..3012601a 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/AppLinkMethodChannel.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/AppLinkMethodChannel.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. @@ -17,25 +17,26 @@ package com.yubico.authenticator.oath import android.net.Uri + import androidx.annotation.UiThread -import com.yubico.authenticator.logging.Log + import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodChannel + import org.json.JSONObject +import org.slf4j.LoggerFactory + class AppLinkMethodChannel(messenger: BinaryMessenger) { private val methodChannel = MethodChannel(messenger, "app.link.methods") + private val logger = LoggerFactory.getLogger(AppLinkMethodChannel::class.java) @UiThread fun handleUri(uri: Uri) { - Log.t(TAG, "Handling URI: $uri") + logger.trace("Handling URI: {}", uri) methodChannel.invokeMethod( "handleOtpAuthLink", JSONObject(mapOf("link" to uri.toString())).toString() ) } - - companion object { - const val TAG = "AppLinkMethodChannel" - } } \ 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..ab88b531 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 @@ -61,6 +61,7 @@ import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.* import kotlinx.serialization.encodeToString +import org.slf4j.LoggerFactory import java.net.URI import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean @@ -77,7 +78,6 @@ class OathManager( private val appPreferences: AppPreferences, ) : AppContextManager { companion object { - const val TAG = "OathManager" const val NFC_DATA_CLEANUP_DELAY = 30L * 1000 // 30s val OTP_AID = byteArrayOf(0xa0.toByte(), 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01) } @@ -98,6 +98,8 @@ class OathManager( ) } + private val logger = LoggerFactory.getLogger(OathManager::class.java) + @TargetApi(Build.VERSION_CODES.M) private fun createKeyStoreProviderM(): KeyProvider = KeyStoreProvider() @@ -117,7 +119,7 @@ class OathManager( // cancel any pending actions, except for addToAny if (!addToAny) { pendingAction?.let { - Log.d(TAG, "Cancelling pending action/closing nfc dialog.") + logger.debug("Cancelling pending action/closing nfc dialog.") it.invoke(Result.failure(CancellationException())) coroutineScope.launch { dialogManager.closeDialog() @@ -134,7 +136,7 @@ class OathManager( if (canInvoke) { if (appViewModel.connectedYubiKey.value == null) { // no USB YubiKey is connected, reset known data on resume - Log.d(TAG, "Removing NFC data after resume.") + logger.debug("Removing NFC data after resume.") appViewModel.setDeviceInfo(null) oathViewModel.setSessionState(null) } @@ -169,7 +171,7 @@ class OathManager( refreshJob = coroutineScope.launch { val delayMs = earliest - now - Log.d(TAG, "Will execute refresh in ${delayMs}ms") + logger.debug("Will execute refresh in {}ms", delayMs) if (delayMs > 0) { delay(delayMs) } @@ -177,9 +179,9 @@ class OathManager( if (currentState.isAtLeast(Lifecycle.State.RESUMED)) { requestRefresh() } else { - Log.d( - TAG, - "Cannot run credential refresh in current lifecycle state: $currentState" + logger.debug( + "Cannot run credential refresh in current lifecycle state: {}", + currentState ) } } @@ -257,7 +259,7 @@ class OathManager( try { oathViewModel.updateCredentials(calculateOathCodes(session)) } catch (error: Exception) { - Log.e(TAG, "Failed to refresh codes", error.toString()) + logger.error("Failed to refresh codes", error) } } } else { @@ -296,7 +298,7 @@ class OathManager( try { SmartCardProtocol(connection).select(OTP_AID) } catch (e: Exception) { - Log.e(TAG, "Failed to recognize this OATH device.") + logger.error("Failed to recognize this OATH device.") // we know this is NFC device and it supports OATH val oathCapabilities = Capabilities(nfc = 0x20) appViewModel.setDeviceInfo( @@ -325,25 +327,24 @@ class OathManager( } } - Log.d( - TAG, + logger.debug( "Successfully read Oath session info (and credentials if unlocked) from connected key" ) } catch (e: Exception) { // OATH not enabled/supported, try to get DeviceInfo over other USB interfaces - Log.e(TAG, "Failed to connect to CCID", e.toString()) + logger.error("Failed to connect to CCID", e) if (device.transport == Transport.USB || e is ApplicationNotAvailableException) { val deviceInfo = try { getDeviceInfo(device) } catch (e: IllegalArgumentException) { - Log.d(TAG, "Device was not recognized") + logger.debug("Device was not recognized") UnknownDevice.copy(isNfc = device.transport == Transport.NFC) } catch (e: Exception) { - Log.d(TAG, "Failure getting device info: ${e.message}") + logger.error("Failure getting device info", e) null } - Log.d(TAG, "Setting device info: $deviceInfo") + logger.debug("Setting device info: {}", deviceInfo) appViewModel.setDeviceInfo(deviceInfo) } @@ -377,7 +378,7 @@ class OathManager( Code.from(code) ) - Log.d(TAG, "Added cred $credential") + logger.debug("Added cred {}", credential) jsonSerializer.encodeToString(addedCred) } } @@ -431,7 +432,7 @@ class OathManager( session.setAccessKey(accessKey) keyManager.addKey(session.deviceId, accessKey, false) oathViewModel.setSessionState(Session(session, false)) - Log.d(TAG, "Successfully set password") + logger.debug("Successfully set password") NULL } @@ -443,7 +444,7 @@ class OathManager( session.deleteAccessKey() keyManager.removeKey(session.deviceId) oathViewModel.setSessionState(Session(session, false)) - Log.d(TAG, "Successfully unset password") + logger.debug("Successfully unset password") return@useOathSession NULL } } @@ -452,7 +453,7 @@ class OathManager( private suspend fun forgetPassword(): String { keyManager.clearAll() - Log.d(TAG, "Cleared all keys.") + logger.debug("Cleared all keys.") oathViewModel.sessionState.value?.let { oathViewModel.setSessionState( it.copy( @@ -516,7 +517,7 @@ class OathManager( oathViewModel.updateCredentials(calculateOathCodes(session)) } catch (apduException: ApduException) { if (apduException.sw == SW.SECURITY_CONDITION_NOT_SATISFIED) { - Log.d(TAG, "Handled oath credential refresh on locked session.") + logger.debug("Handled oath credential refresh on locked session.") oathViewModel.setSessionState( Session( session, @@ -524,10 +525,9 @@ class OathManager( ) ) } else { - Log.e( - TAG, + logger.error( "Unexpected sw when refreshing oath credentials", - apduException.message + apduException ) } } @@ -543,7 +543,7 @@ class OathManager( Credential(credential, session.deviceId), code ) - Log.d(TAG, "Code calculated $code") + logger.debug("Code calculated {}", code) jsonSerializer.encodeToString(code) } @@ -679,7 +679,7 @@ class OathManager( }) } dialogManager.showDialog(Icon.NFC, "Tap your key", title) { - Log.d(TAG, "Cancelled Dialog $title") + logger.debug("Cancelled Dialog {}", title) pendingAction?.invoke(Result.failure(CancellationException())) pendingAction = null } diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/DeviceInfoHelper.kt b/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/DeviceInfoHelper.kt index 433f7b5e..9a985c92 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/DeviceInfoHelper.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/DeviceInfoHelper.kt @@ -17,8 +17,6 @@ package com.yubico.authenticator.yubikit import com.yubico.authenticator.device.Info -import com.yubico.authenticator.logging.Log -import com.yubico.authenticator.oath.OathManager import com.yubico.authenticator.compatUtil import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice @@ -29,22 +27,25 @@ import com.yubico.yubikit.core.smartcard.SmartCardConnection import com.yubico.yubikit.management.DeviceInfo import com.yubico.yubikit.support.DeviceUtil +import org.slf4j.LoggerFactory + suspend fun getDeviceInfo(device: YubiKeyDevice): Info { val pid = (device as? UsbYubiKeyDevice)?.pid + val logger = LoggerFactory.getLogger("getDeviceInfo") val deviceInfo = runCatching { device.withConnection { DeviceUtil.readInfo(it, pid) } }.recoverCatching { t -> - Log.d(OathManager.TAG, "Smart card connection not available: ${t.message}") + logger.debug("Smart card connection not available: {}", t.message) device.withConnection { DeviceUtil.readInfo(it, pid) } }.recoverCatching { t -> - Log.d(OathManager.TAG, "OTP connection not available: ${t.message}") + logger.debug("OTP connection not available: {}", t.message) device.withConnection { DeviceUtil.readInfo(it, pid) } }.recoverCatching { t -> - Log.d(OathManager.TAG, "FIDO connection not available: ${t.message}") + logger.debug("FIDO connection not available: {}", t.message) return SkyHelper(compatUtil).getDeviceInfo(device) }.getOrElse { - Log.e(OathManager.TAG, "Failed to recognize device: ${it.message}") + logger.debug("Failed to recognize device: {}", it.message) throw it } From 63d1cda5de73288ccad166c7ed536b132260e415 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Thu, 1 Jun 2023 08:03:16 +0200 Subject: [PATCH 05/39] fix logger name --- .../src/main/kotlin/com/yubico/authenticator/AppPreferences.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/AppPreferences.kt b/android/app/src/main/kotlin/com/yubico/authenticator/AppPreferences.kt index 2301e6d7..fd063b14 100755 --- a/android/app/src/main/kotlin/com/yubico/authenticator/AppPreferences.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/AppPreferences.kt @@ -35,7 +35,7 @@ class AppPreferences(context: Context) { const val DEFAULT_CLIP_KBD_LAYOUT = "US" } - private val logger = LoggerFactory.getLogger(AppContext::class.java) + private val logger = LoggerFactory.getLogger(AppPreferences::class.java) private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE).also { From bd4cabc9a8fbbbae33bcd033126aa89a6abb5486 Mon Sep 17 00:00:00 2001 From: nebulon42 Date: Sun, 18 Jun 2023 18:52:44 +0200 Subject: [PATCH 06/39] 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 07/39] 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 08/39] 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 09/39] 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 10/39] 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 11/39] 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 12/39] 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 13/39] 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 14/39] 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 15/39] 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 16/39] 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 17/39] 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 18/39] 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 19/39] 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 20/39] 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 21/39] 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 22/39] 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 23/39] 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 24/39] 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 25/39] 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 26/39] 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 27/39] 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 28/39] 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 29/39] 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 30/39] 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 31/39] 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 32/39] 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 33/39] 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 34/39] 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 35/39] 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 649da32b8312d06b13f2a90b9ec471f2fabe79ec Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 7 Jul 2023 18:10:16 +0200 Subject: [PATCH 36/39] 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 37/39] 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 38/39] 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 643ae270ddac8615525c85ee53df1b3bb365261e Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 31 Jul 2023 11:22:26 +0200 Subject: [PATCH 39/39] 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"