diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index d8dde312..cab4abce 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -13,19 +13,23 @@ jobs: distribution: 'zulu' java-version: '11' + - uses: actions/checkout@v3 + with: + path: 'app' + + - name: Read variables from repo + run: cat .github/workflows/env >> $GITHUB_ENV + working-directory: ./app + - name: Install Flutter uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.10.1' + flutter-version: ${{ env.FLUTTER }} - run: | flutter config flutter --version - - uses: actions/checkout@v3 - with: - path: 'app' - - name: Check app versions run: | python set-version.py diff --git a/.github/workflows/check-strings.yml b/.github/workflows/check-strings.yml index 3e0841c2..15a10289 100644 --- a/.github/workflows/check-strings.yml +++ b/.github/workflows/check-strings.yml @@ -6,12 +6,13 @@ jobs: strings: runs-on: ubuntu-latest - env: - FLUTTER: '3.10.1' steps: - uses: actions/checkout@v3 + - name: Read variables from repo + run: cat .github/workflows/env >> $GITHUB_ENV + - name: Ensure main locale is correct run: python check_strings.py lib/l10n/app_en.arb diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 51b548a3..48d9cc71 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,19 +39,23 @@ jobs: distribution: 'zulu' java-version: '11' + - uses: actions/checkout@v3 + with: + path: 'app' + + - name: Read variables from repo + run: cat .github/workflows/env >> $GITHUB_ENV + working-directory: ./app + - name: Install Flutter uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.10.1' + flutter-version: ${{ env.FLUTTER }} - run: | flutter config flutter --version - - uses: actions/checkout@v3 - with: - path: 'app' - - name: Run flutter tests run: | flutter test diff --git a/.github/workflows/env b/.github/workflows/env new file mode 100644 index 00000000..3e6f43bf --- /dev/null +++ b/.github/workflows/env @@ -0,0 +1,2 @@ +FLUTTER=3.13.4 +PYVER=3.11.5 diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 51636de1..acd1d7db 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -6,21 +6,25 @@ jobs: build: runs-on: ubuntu-latest - env: - PYVER: '3.11.3' - FLUTTER: '3.10.1' container: - image: ubuntu:18.04 + image: ubuntu:20.04 env: DEBIAN_FRONTEND: noninteractive steps: + - uses: actions/checkout@v3 + with: + sparse-checkout: .github/workflows/env + + - name: Read variables from repo + run: cat .github/workflows/env >> $GITHUB_ENV + - name: Install dependencies run: | export PYVER_MINOR=${PYVER%.*} 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 @@ -61,7 +65,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 9042fec1..37059331 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -6,13 +6,13 @@ jobs: build: runs-on: macos-latest - env: - PYVER: '3.11.3' - MACOSX_DEPLOYMENT_TARGET: "10.15" steps: - uses: actions/checkout@v3 + - name: Read variables from repo + run: cat .github/workflows/env >> $GITHUB_ENV + - name: Check app versions run: | python3 set-version.py @@ -49,7 +49,7 @@ jobs: with: channel: 'stable' architecture: 'x64' - flutter-version: '3.10.1' + flutter-version: ${{ env.FLUTTER }} - run: flutter config --enable-macos-desktop - run: flutter --version diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index e69583c0..06d7f737 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -6,12 +6,14 @@ jobs: build: runs-on: windows-latest - env: - PYVER: '3.11.3' steps: - uses: actions/checkout@v3 + - name: Read variables from repo + shell: bash + run: cat .github/workflows/env >> $GITHUB_ENV + - name: Check app versions run: | python set-version.py @@ -45,7 +47,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.10.1' + flutter-version: ${{ env.FLUTTER }} - run: flutter config --enable-windows-desktop - run: flutter --version diff --git a/NEWS b/NEWS index 7112d6ed..4e331663 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,11 @@ +* Version 6.3.0 (released 2023-09-04) + ** Add support for importing accounts through QR codes from Google Authenticator. + ** Add community translations for French, Japanese, German and Polish languages. + ** Improve user interface with new Material UI widgets. + ** Bug fixes and improvements based on user feedback. + ** Desktop: Add support for PIV. + ** Android: Update Android 14 compatibility. + * Version 6.2.0 (released 2023-04-19) ** Add support for custom account icons. ** Desktop: Add systray icon for quick access to pinned accounts. diff --git a/android/app/build.gradle b/android/app/build.gradle index d1d735d0..1f17f43b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -93,15 +93,15 @@ dependencies { api "com.yubico.yubikit:oath:$project.yubiKitVersion" api "com.yubico.yubikit:support:$project.yubiKitVersion" - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1' - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0' // Lifecycle implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation "androidx.core:core-ktx:1.10.1" - implementation 'androidx.fragment:fragment-ktx:1.5.7' - implementation 'androidx.preference:preference-ktx:1.2.0' + implementation 'androidx.fragment:fragment-ktx:1.6.1' + implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'com.google.android.material:material:1.9.0' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index bc27f12a..1939fb7e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -50,6 +50,7 @@ + diff --git a/android/app/src/main/assets/logback.xml b/android/app/src/main/assets/logback.xml index 2d8d6982..b53ea3b4 100644 --- a/android/app/src/main/assets/logback.xml +++ b/android/app/src/main/assets/logback.xml @@ -24,8 +24,14 @@ - + + + %d{HH:mm:ss:SSS} [%thread] %-5level %logger{36} - %X{app} %msg + + + + \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt index 49e8214f..c3df2e04 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt @@ -24,10 +24,16 @@ import kotlinx.serialization.json.Json typealias OnDialogCancelled = suspend () -> Unit -enum class Icon(val value: String) { - NFC("nfc"), - SUCCESS("success"), - ERROR("error"); +enum class DialogIcon(val value: Int) { + Nfc(0), + Success(1), + Failure(2); +} + +enum class DialogTitle(val value: Int) { + TapKey(0), + OperationSuccessful(1), + OperationFailed(2) } class DialogManager(messenger: BinaryMessenger, private val coroutineScope: CoroutineScope) { @@ -45,16 +51,21 @@ class DialogManager(messenger: BinaryMessenger, private val coroutineScope: Coro } } - fun showDialog(icon: Icon, title: String, description: String, cancelled: OnDialogCancelled?) { + fun showDialog( + dialogIcon: DialogIcon, + dialogTitle: DialogTitle, + dialogDescriptionId: Int, + cancelled: OnDialogCancelled? + ) { onCancelled = cancelled coroutineScope.launch { channel.invoke( "show", Json.encodeToString( mapOf( - "title" to title, - "description" to description, - "icon" to icon.value + "title" to dialogTitle.value, + "description" to dialogDescriptionId, + "icon" to dialogIcon.value ) ) ) @@ -62,17 +73,17 @@ class DialogManager(messenger: BinaryMessenger, private val coroutineScope: Coro } suspend fun updateDialogState( - icon: Icon? = null, - title: String? = null, - description: String? = null + dialogIcon: DialogIcon? = null, + dialogTitle: DialogTitle, + dialogDescriptionId: Int? = null, ) { channel.invoke( "state", Json.encodeToString( mapOf( - "title" to title, - "description" to description, - "icon" to icon?.value + "title" to dialogTitle.value, + "description" to dialogDescriptionId, + "icon" to dialogIcon?.value ) ) ) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt index 36e97a58..f816cb71 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt @@ -182,18 +182,17 @@ class MainActivity : FlutterFragmentActivity() { @SuppressLint("WrongConstant") override fun onStart() { super.onStart() - val receiverFlags = ContextCompat.RECEIVER_NOT_EXPORTED ContextCompat.registerReceiver( this, qrScannerCameraClosedBR, QRScannerCameraClosedBR.intentFilter, - receiverFlags + ContextCompat.RECEIVER_NOT_EXPORTED ) ContextCompat.registerReceiver( this, nfcAdapterStateChangeBR, NfcAdapterStateChangedBR.intentFilter, - receiverFlags + ContextCompat.RECEIVER_EXPORTED ) } @@ -222,7 +221,10 @@ class MainActivity : FlutterFragmentActivity() { // Handle opening through otpauth:// link val intentData = intent.data - if (intentData != null && intentData.scheme == "otpauth") { + if (intentData != null && + (intentData.scheme == "otpauth" || + intentData.scheme == "otpauth-migration") + ) { intent.data = null appLinkMethodChannel.handleUri(intentData) } @@ -366,11 +368,15 @@ class MainActivity : FlutterFragmentActivity() { * this receiver restarts the YubiKit NFC discovery when the QR Scanner camera is closed. */ class QRScannerCameraClosedBR : BroadcastReceiver() { + + private val logger = LoggerFactory.getLogger(QRScannerCameraClosedBR::class.java) + companion object { val intentFilter = IntentFilter("com.yubico.authenticator.QRScannerView.CameraClosed") } override fun onReceive(context: Context?, intent: Intent?) { + logger.debug("Restarting nfc discovery after camera was closed.") (context as? MainActivity)?.startNfcDiscovery() } } diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/logging/BufferAppender.kt b/android/app/src/main/kotlin/com/yubico/authenticator/logging/BufferAppender.kt new file mode 100644 index 00000000..fc0dfca9 --- /dev/null +++ b/android/app/src/main/kotlin/com/yubico/authenticator/logging/BufferAppender.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yubico.authenticator.logging + +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.UnsynchronizedAppenderBase + +class BufferAppender : UnsynchronizedAppenderBase() { + + private var encoder: PatternLayoutEncoder? = null + + private val buffer = arrayListOf() + + public override fun append(event: ILoggingEvent) { + if (!isStarted) { + return + } + + if (buffer.size > MAX_BUFFER_SIZE) { + buffer.removeAt(0) + } + + buffer.add(encoder!!.layout.doLayout(event)) + } + + fun getEncoder(): PatternLayoutEncoder? = encoder + + fun setEncoder(encoder: PatternLayoutEncoder?) { + this.encoder = encoder + } + + fun getLogBuffer(): ArrayList { + return buffer + } + + companion object { + private const val MAX_BUFFER_SIZE = 1000 + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/logging/FlutterLog.kt b/android/app/src/main/kotlin/com/yubico/authenticator/logging/FlutterLog.kt index 6d095c6a..71f86fac 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/logging/FlutterLog.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/logging/FlutterLog.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * Copyright (C) 2022-2023 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,18 @@ package com.yubico.authenticator.logging import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodChannel +import org.slf4j.Logger +import org.slf4j.LoggerFactory class FlutterLog(messenger: BinaryMessenger) { private var channel = MethodChannel(messenger, "android.log.redirect") + private val bufferAppender = + (LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger) + .getAppender("buffer") as BufferAppender + init { + channel.setMethodCallHandler { call, result -> when (call.method) { @@ -53,7 +60,7 @@ class FlutterLog(messenger: BinaryMessenger) { result.success(null) } "getLogs" -> { - result.success(Log.getBuffer()) + result.success(bufferAppender.getLogBuffer()) } else -> { result.notImplemented() diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/logging/Log.kt b/android/app/src/main/kotlin/com/yubico/authenticator/logging/Log.kt index ce11c8a4..1250bd9e 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/logging/Log.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/logging/Log.kt @@ -24,7 +24,7 @@ import org.slf4j.LoggerFactory object Log { - private val logger = LoggerFactory.getLogger("com.yubico.authenticator") + private val logger = LoggerFactory.getLogger("com.yubico.authenticator.Log") enum class LogLevel { TRAFFIC, @@ -34,13 +34,6 @@ object Log { ERROR } - private const val MAX_BUFFER_SIZE = 1000 - private val buffer = arrayListOf() - - fun getBuffer() : List { - return buffer - } - private var level = if (BuildConfig.DEBUG) { LogLevel.DEBUG } else { @@ -56,17 +49,10 @@ object Log { return } - if (buffer.size > MAX_BUFFER_SIZE) { - buffer.removeAt(0) - } - val logMessage = (if (error == null) - "[$loggerName] ${level.name}: $message" + "$message [$loggerName]" else - "[$loggerName] ${level.name}: $message (err: $error)" - ).also { - buffer.add(it) - } + "$message [$loggerName] (err: $error)") when (level) { LogLevel.TRAFFIC -> logger.trace(logMessage) diff --git a/lib/android/qr_scanner/qr_scanner_util.dart b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt similarity index 56% rename from lib/android/qr_scanner/qr_scanner_util.dart rename to android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt index c8f10b3b..ac78d2c5 100644 --- a/lib/android/qr_scanner/qr_scanner_util.dart +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * 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. @@ -14,8 +14,22 @@ * limitations under the License. */ -import 'dart:ui'; +package com.yubico.authenticator.oath -const double scannerAreaRadius = 40.0; +const val dialogDescriptionOathIndex = 100 -double getScannerAreaWidth(Size size) => size.width - scannerAreaRadius; +enum class OathActionDescription(private val value: Int) { + Reset(0), + Unlock(1), + SetPassword(2), + UnsetPassword(3), + AddAccount(4), + RenameAccount(5), + DeleteAccount(6), + CalculateCode(7), + ActionFailure(8), + AddMultipleAccounts(9); + + val id: Int + get() = value + dialogDescriptionOathIndex +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt index e8b9d37e..2157eba6 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 @@ -63,6 +63,7 @@ import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.* import kotlinx.serialization.encodeToString import org.slf4j.LoggerFactory +import java.io.IOException import java.net.URI import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean @@ -197,6 +198,7 @@ class OathManager( // OATH methods callable from Flutter: oathChannel.setHandler(coroutineScope) { method, args -> + @Suppress("UNCHECKED_CAST") when (method) { "reset" -> reset() "unlock" -> unlock( @@ -229,6 +231,11 @@ class OathManager( args["requireTouch"] as Boolean ) + "addAccountsToAny" -> addAccountsToAny( + args["uris"] as List, + args["requireTouch"] as List + ) + else -> throw NotImplementedError() } } @@ -367,7 +374,7 @@ class OathManager( val credentialData: CredentialData = CredentialData.parseUri(URI.create(uri)) addToAny = true - return useOathSessionNfc("Add account") { session -> + return useOathSessionNfc(OathActionDescription.AddAccount) { session -> // We need to check for duplicates here since we haven't yet read the credentials if (session.credentials.any { it.id.contentEquals(credentialData.id) }) { throw Exception("A credential with this ID already exists!") @@ -390,8 +397,46 @@ class OathManager( } } + private suspend fun addAccountsToAny( + uris: List, + requireTouch: List, + ): String { + logger.trace("Adding following accounts: {}", uris) + + addToAny = true + return useOathSessionNfc(OathActionDescription.AddMultipleAccounts) { session -> + var successCount = 0 + for (index in uris.indices) { + + val credentialData: CredentialData = + CredentialData.parseUri(URI.create(uris[index])) + + if (session.credentials.any { it.id.contentEquals(credentialData.id) }) { + logger.info("A credential with this ID already exists, skipping") + continue + } + + val credential = session.putCredential(credentialData, requireTouch[index]) + val code = + if (credentialData.oathType == YubiKitOathType.TOTP && !requireTouch[index]) { + // recalculate the code + calculateCode(session, credential) + } else null + + oathViewModel.addCredential( + Credential(credential, session.deviceId), + Code.from(code) + ) + + logger.trace("Added cred {}", credential) + successCount++ + } + jsonSerializer.encodeToString(mapOf("succeeded" to successCount)) + } + } + private suspend fun reset(): String = - useOathSession("Reset YubiKey") { + useOathSession(OathActionDescription.Reset) { // note, it is ok to reset locked session it.reset() keyManager.removeKey(it.deviceId) @@ -403,7 +448,7 @@ class OathManager( } private suspend fun unlock(password: String, remember: Boolean): String = - useOathSession("Unlocking") { + useOathSession(OathActionDescription.Unlock) { val accessKey = it.deriveAccessKey(password.toCharArray()) keyManager.addKey(it.deviceId, accessKey, remember) @@ -425,7 +470,7 @@ class OathManager( currentPassword: String?, newPassword: String, ): String = - useOathSession("Set password", unlock = false) { session -> + useOathSession(OathActionDescription.SetPassword, unlock = false) { session -> if (session.isAccessKeySet) { if (currentPassword == null) { throw Exception("Must provide current password to be able to change it") @@ -444,7 +489,7 @@ class OathManager( } private suspend fun unsetPassword(currentPassword: String): String = - useOathSession("Unset password", unlock = false) { session -> + useOathSession(OathActionDescription.UnsetPassword, unlock = false) { session -> if (session.isAccessKeySet) { // test current password sent by the user if (session.unlock(currentPassword.toCharArray())) { @@ -476,7 +521,7 @@ class OathManager( uri: String, requireTouch: Boolean, ): String = - useOathSession("Add account") { session -> + useOathSession(OathActionDescription.AddAccount) { session -> val credentialData: CredentialData = CredentialData.parseUri(URI.create(uri)) @@ -497,7 +542,7 @@ class OathManager( } private suspend fun renameAccount(uri: String, name: String, issuer: String?): String = - useOathSession("Rename") { session -> + useOathSession(OathActionDescription.RenameAccount) { session -> val credential = getOathCredential(session, uri) val renamedCredential = Credential(session.renameCredential(credential, name, issuer), session.deviceId) @@ -510,39 +555,57 @@ class OathManager( } private suspend fun deleteAccount(credentialId: String): String = - useOathSession("Delete account") { session -> + useOathSession(OathActionDescription.DeleteAccount) { session -> val credential = getOathCredential(session, credentialId) session.deleteCredential(credential) oathViewModel.removeCredential(Credential(credential, session.deviceId)) NULL } - private suspend fun requestRefresh() = - appViewModel.connectedYubiKey.value?.let { usbYubiKeyDevice -> - useOathSessionUsb(usbYubiKeyDevice) { session -> - try { - oathViewModel.updateCredentials(calculateOathCodes(session)) - } catch (apduException: ApduException) { - if (apduException.sw == SW.SECURITY_CONDITION_NOT_SATISFIED) { - logger.debug("Handled oath credential refresh on locked session.") - oathViewModel.setSessionState( - Session( - session, - keyManager.isRemembered(session.deviceId) - ) - ) - } else { - logger.error( - "Unexpected sw when refreshing oath credentials", - apduException - ) - } - } - } + private suspend fun requestRefresh() { + + val clearCodes = { + val currentCredentials = oathViewModel.credentials.value + oathViewModel.updateCredentials(currentCredentials?.associate { + it.credential to null + } ?: emptyMap()) } + appViewModel.connectedYubiKey.value?.let { usbYubiKeyDevice -> + try { + useOathSessionUsb(usbYubiKeyDevice) { session -> + try { + oathViewModel.updateCredentials(calculateOathCodes(session)) + } catch (apduException: ApduException) { + if (apduException.sw == SW.SECURITY_CONDITION_NOT_SATISFIED) { + logger.debug("Handled oath credential refresh on locked session.") + oathViewModel.setSessionState( + Session( + session, + keyManager.isRemembered(session.deviceId) + ) + ) + } else { + logger.error( + "Unexpected sw when refreshing oath credentials", + apduException + ) + } + } + } + } catch (ioException: IOException) { + logger.error("IOException when accessing USB device: ", ioException) + clearCodes() + } catch (illegalStateException: IllegalStateException) { + logger.error("IllegalStateException when accessing USB device: ", illegalStateException) + clearCodes() + } + } + } + + private suspend fun calculate(credentialId: String): String = - useOathSession("Calculate") { session -> + useOathSession(OathActionDescription.CalculateCode) { session -> val credential = getOathCredential(session, credentialId) val code = Code.from(calculateCode(session, credential)) @@ -655,7 +718,7 @@ class OathManager( } private suspend fun useOathSession( - title: String, + oathActionDescription: OathActionDescription, unlock: Boolean = true, action: (YubiKitOathSession) -> T ): T { @@ -664,7 +727,7 @@ class OathManager( unlockOnConnect.set(unlock) return appViewModel.connectedYubiKey.value?.let { useOathSessionUsb(it, action) - } ?: useOathSessionNfc(title, action) + } ?: useOathSessionNfc(oathActionDescription, action) } private suspend fun useOathSessionUsb( @@ -675,7 +738,7 @@ class OathManager( } private suspend fun useOathSessionNfc( - title: String, + oathActionDescription: OathActionDescription, block: (YubiKitOathSession) -> T ): T { try { @@ -685,15 +748,15 @@ class OathManager( block.invoke(it.value) }) } - dialogManager.showDialog(Icon.NFC, "Tap your key", title) { - logger.debug("Cancelled Dialog {}", title) + dialogManager.showDialog(DialogIcon.Nfc, DialogTitle.TapKey, oathActionDescription.id) { + logger.debug("Cancelled Dialog {}", oathActionDescription.name) pendingAction?.invoke(Result.failure(CancellationException())) pendingAction = null } } dialogManager.updateDialogState( - icon = Icon.SUCCESS, - title = "Success" + dialogIcon = DialogIcon.Success, + dialogTitle = DialogTitle.OperationSuccessful ) // TODO: This delays the closing of the dialog, but also the return value delay(500) @@ -702,9 +765,9 @@ class OathManager( throw cancelled } catch (error: Throwable) { dialogManager.updateDialogState( - icon = Icon.ERROR, - title = "Failure", - description = "Action failed - try again" + dialogIcon = DialogIcon.Failure, + dialogTitle = DialogTitle.OperationFailed, + dialogDescriptionId = OathActionDescription.ActionFailure.id ) // TODO: This delays the closing of the dialog, but also the return value delay(1500) @@ -721,4 +784,4 @@ class OathManager( } ?: throw Exception("Failed to find account") -} \ No newline at end of file +} diff --git a/android/build.gradle b/android/build.gradle index c91fe98b..f2914fc5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.8.10' + ext.kotlin_version = '1.9.0' repositories { google() mavenCentral() @@ -26,7 +26,7 @@ allprojects { yubiKitVersion = "2.3.0" junitVersion = "4.13.2" - mockitoVersion = "5.3.1" + mockitoVersion = "5.5.0" } } diff --git a/android/flutter_plugins/qrscanner_zxing/android/build.gradle b/android/flutter_plugins/qrscanner_zxing/android/build.gradle index c472236c..17bb15b9 100644 --- a/android/flutter_plugins/qrscanner_zxing/android/build.gradle +++ b/android/flutter_plugins/qrscanner_zxing/android/build.gradle @@ -2,7 +2,7 @@ group 'com.yubico.authenticator.flutter_plugins.qrscanner_zxing' version '1.0' buildscript { - ext.kotlin_version = '1.8.10' + ext.kotlin_version = '1.9.0' repositories { google() mavenCentral() @@ -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}" diff --git a/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt b/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt index 5da59aa2..184a6b5a 100644 --- a/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt +++ b/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerView.kt @@ -26,6 +26,7 @@ import android.os.Handler import android.os.Looper import android.provider.Settings import android.util.Log +import android.util.Size import android.view.View import androidx.camera.core.* import androidx.camera.lifecycle.ProcessCameraProvider @@ -81,8 +82,6 @@ internal class QRScannerView( private val stateChangeObserver = StateChangeObserver(context) private val uiThreadHandler = Handler(Looper.getMainLooper()) - private var marginPct: Double? = null - companion object { const val TAG = "QRScannerView" @@ -93,9 +92,6 @@ internal class QRScannerView( Manifest.permission.CAMERA, ).toTypedArray() - // view related - private const val QR_SCANNER_ASPECT_RATIO = AspectRatio.RATIO_4_3 - // communication channel private const val CHANNEL_NAME = "com.yubico.authenticator.flutter_plugins.qr_scanner_channel" @@ -128,10 +124,20 @@ internal class QRScannerView( private var imageAnalysis: ImageAnalysis? = null private var preview: Preview? = null - private var barcodeAnalyzer : BarcodeAnalyzer = BarcodeAnalyzer(marginPct) { analyzeResult -> - if (analyzeResult.isSuccess) { - analyzeResult.getOrNull()?.let { result -> - reportCodeFound(result) + private val barcodeAnalyzer = with(creationParams) { + var marginPct : Double? = null + if (this?.get("margin") is Number) { + val marginValue = this["margin"] as Number + if (marginValue.toDouble() > 0.0 && marginValue.toDouble() < 45) { + marginPct = marginValue.toDouble() + } + } + + BarcodeAnalyzer(marginPct) { analyzeResult -> + if (analyzeResult.isSuccess) { + analyzeResult.getOrNull()?.let { result -> + reportCodeFound(result) + } } } } @@ -155,19 +161,11 @@ internal class QRScannerView( private val methodChannel: MethodChannel = MethodChannel(binaryMessenger, CHANNEL_NAME) private var permissionsGranted = false + private val screenSize = with(context.resources.displayMetrics) { + Size(widthPixels, heightPixels) + } + init { - - // read margin parameter - // only use it if it has reasonable value - if (creationParams?.get("margin") is Number) { - val marginValue = creationParams["margin"] as Number - if (marginValue.toDouble() > 0.0 && marginValue.toDouble() < 45) { - marginPct = marginValue.toDouble() - } - } - - Log.v(TAG, "marginPct: $marginPct") - if (context is Activity) { permissionsGranted = allPermissionsGranted(context) @@ -259,14 +257,14 @@ internal class QRScannerView( imageAnalysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .setTargetAspectRatio(QR_SCANNER_ASPECT_RATIO) + .setTargetResolution(Size(768,1024)) .build() .also { it.setAnalyzer(cameraExecutor, barcodeAnalyzer) } preview = Preview.Builder() - .setTargetAspectRatio(QR_SCANNER_ASPECT_RATIO) + .setTargetResolution(screenSize) .build() .also { it.setSurfaceProvider(previewView.surfaceProvider) @@ -369,7 +367,7 @@ internal class QRScannerView( val fullSize = BinaryBitmap(HybridBinarizer(luminanceSource)) - val bitmapToProcess = if (marginPct != null) { + val bitmapToProcess = if (marginPct != null && fullSize.isCropSupported) { val shorterDim = min(imageProxy.width, imageProxy.height) val cropMargin = marginPct * 0.01 * shorterDim val cropWH = shorterDim - 2.0 * cropMargin @@ -395,6 +393,10 @@ internal class QRScannerView( } val result: com.google.zxing.Result = multiFormatReader.decode(bitmapToProcess) + if (analysisPaused) { + return + } + analysisPaused = true // pause Log.v(TAG, "Analysis result: ${result.text}") listener.invoke(Result.success(result.text)) diff --git a/build-helper.sh b/build-helper.sh index e8d81c76..5ddad4d0 100755 --- a/build-helper.sh +++ b/build-helper.sh @@ -7,7 +7,7 @@ set -e case "$(uname)" in - Darwin*) + Darwin*) OS="macos";; Linux*) OS="linux";; @@ -20,6 +20,50 @@ OUTPUT="build/$OS" cd helper poetry install + +# Create a universal binary on MacOS +if [ "$OS" = "macos" ]; then + PYTHON=`poetry run python -c "import sys; print(sys.executable)"` + echo "Using Python: $PYTHON" + if [ $(lipo -archs $PYTHON | grep -c 'x86_64 arm64') -ne 0 ]; then + echo "Fixing single-arch dependencies..." + export MACOSX_DEPLOYMENT_TARGET="10.15" + export CFLAGS="-arch x86_64 -arch arm64" + export ARCHFLAGS="-arch x86_64 -arch arm64" + HELPER="../$OUTPUT/helper" + rm -rf $HELPER + mkdir -p $HELPER + + # Needed to build zxing-cpp properly + export CMAKE_OSX_ARCHITECTURES="arm64;x86_64" + + # Export exact versions + poetry export --without-hashes > $HELPER/requirements.txt + grep cryptography $HELPER/requirements.txt > $HELPER/cryptography.txt + grep cffi $HELPER/requirements.txt > $HELPER/cffi.txt + grep pillow $HELPER/requirements.txt > $HELPER/pillow.txt + grep zxing-cpp $HELPER/requirements.txt > $HELPER/zxing-cpp.txt + # Remove non-universal packages + poetry run pip uninstall -y cryptography cffi pillow zxing-cpp + # Build cffi from source to get universal build + poetry run pip install --upgrade -r $HELPER/cffi.txt --no-binary cffi + # Build zxing-cpp from source to get universal build + poetry run pip install --upgrade -r $HELPER/zxing-cpp.txt --no-binary zxing-cpp + # Explicitly install pre-build universal build of cryptography + poetry run pip download -r $HELPER/cryptography.txt --platform macosx_10_12_universal2 --only-binary :all: --no-deps --dest $HELPER + poetry run pip install -r $HELPER/cryptography.txt --no-cache-dir --no-index --find-links $HELPER + # Combine wheels of pillow to get universal build + poetry run pip download -r $HELPER/pillow.txt --platform macosx_10_10_x86_64 --only-binary :all: --no-deps --dest $HELPER + poetry run pip download -r $HELPER/pillow.txt --platform macosx_11_0_arm64 --only-binary :all: --no-deps --dest $HELPER + poetry run pip install delocate + poetry run delocate-fuse $HELPER/Pillow*.whl + WHL=$(ls $HELPER/Pillow*x86_64.whl) + UNIVERSAL_WHL=${WHL//x86_64/universal2} + mv $WHL $UNIVERSAL_WHL + poetry run pip install --upgrade $UNIVERSAL_WHL + fi +fi + rm -rf ../$OUTPUT/helper poetry run pyinstaller authenticator-helper.spec --distpath ../$OUTPUT cd .. diff --git a/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/doc/Development.adoc b/doc/Development.adoc index 0ed555f0..7d56a93f 100644 --- a/doc/Development.adoc +++ b/doc/Development.adoc @@ -28,12 +28,30 @@ on `yubikey-manager >=5, <6`. It will likely not work with versions outside this range! === Building the Yubico Authenticator Helper -Requirements: Python >= 3.8 and Poetry. - The GUI requires a compiled version of Helper to run, which is built from the -sources in helper/ in this repository. This needs to be build prior to running -`flutter build` or `flutter run`, by running `build-helper.sh` (or -`build-helper.bat` on Windows). +sources in helper/ in this repository. Requirements for all platforms are +Python >= 3.8 and Poetry. This needs to be built prior to running +`flutter build` or `flutter run`. + +==== Windows + +Make sure the http://www.swig.org/[swig] executable is in your PATH. + +==== macOS + + $ brew install swig + +==== Linux (Debian-based distributions) + + $ sudo apt install swig libu2f-udev pcscd libpcsclite-dev + +==== Linux (RPM-based distributons) + + # Tested on Fedora 34 + $ sudo dnf install pcsc-lite-devel python3-devel swig + +When prerequisites are installed you build the helper by running `build-helper.sh` +(or `build-helper.bat` on Windows). NOTE: You will need to re-run the build script if changes have been made to Helper's code, or if `flutter clean` has been run. diff --git a/helper/authenticator-helper.spec b/helper/authenticator-helper.spec index 4c260d56..fb2aa9fa 100755 --- a/helper/authenticator-helper.spec +++ b/helper/authenticator-helper.spec @@ -1,4 +1,6 @@ # -*- mode: python ; coding: utf-8 -*- +import sys +import subprocess block_cipher = None @@ -21,6 +23,13 @@ a = Analysis( ) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) +target_arch = None +# MacOS: If the running Python process is "universal", build a univeral2 binary. +if sys.platform == "darwin": + r = subprocess.run(['lipo', '-archs', sys.executable], capture_output=True).stdout + if b"x86_64" in r and b"arm64" in r: + target_arch = "universal2" + exe = EXE( pyz, a.scripts, @@ -36,7 +45,7 @@ exe = EXE( manifest="authenticator-helper.exe.manifest", version="version_info.txt", disable_windowed_traceback=False, - target_arch=None, + target_arch=target_arch, codesign_identity=None, entitlements_file=None, ) diff --git a/helper/helper/base.py b/helper/helper/base.py index ca2a940f..78b9b653 100644 --- a/helper/helper/base.py +++ b/helper/helper/base.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from yubikit.core import InvalidPinError from functools import partial import logging @@ -123,6 +124,8 @@ class RpcNode: except ChildResetException as e: self._close_child() raise StateResetException(e.message, traversed) + except InvalidPinError: + raise # Prevent catching this as a ValueError below except ValueError as e: raise InvalidParametersException(e) raise NoSuchActionException(action) diff --git a/helper/helper/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..64ae044a --- /dev/null +++ b/helper/helper/piv.py @@ -0,0 +1,481 @@ +# 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, InvalidPinError +from yubikit.core.smartcard import ApduError, SW +from yubikit.piv import ( + PivSession, + OBJECT_ID, + MANAGEMENT_KEY_TYPE, + 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, + parse_rfc4514_string, +) +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) + + @action(closes_child=False) + def examine_file(self, params, event, signal): + data = bytes.fromhex(params.pop("data")) + password = params.pop("password", None) + try: + private_key, certs = _parse_file(data, password) + certificate = _choose_cert(certs) + + return dict( + status=True, + password=password is not None, + key_type=KEY_TYPE.from_public_key(private_key.public_key()) + if private_key + else None, + cert_info=_get_cert_info(certificate), + ) + except InvalidPasswordError: + logger.debug("Invalid or missing password", exc_info=True) + return dict(status=False) + + @action(closes_child=False) + def validate_rfc4514(self, params, event, signal): + try: + parse_rfc4514_string(params.pop("data")) + return dict(status=True) + except ValueError: + return dict(status=False) + + +def _slot_for(name): + return SLOT(int(name, base=16)) + + +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 + + +def _choose_cert(certs): + if certs: + if len(certs) > 1: + leafs = get_leaf_certificates(certs) + return leafs[0] + else: + return certs[0] + return None + + +def _get_cert_info(cert): + if cert is None: + return None + return dict( + subject=cert.subject.rfc4514_string(), + issuer=cert.issuer.rfc4514_string(), + serial=hex(cert.serial_number)[2:], + not_valid_before=cert.not_valid_before.isoformat(), + not_valid_after=cert.not_valid_after.isoformat(), + fingerprint=cert.fingerprint(hashes.SHA256()), + ) + + +class SlotsNode(RpcNode): + def __init__(self, session): + super().__init__() + 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=_get_cert_info(cert), + ) + 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) + + +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("Invalid or missing password", 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, NotSupportedError): + pass + + certificate = _choose_cert(certs) + if certificate: + self.session.put_certificate(self.slot, certificate) + self.session.put_object(OBJECT_ID.CHUID, generate_chuid()) + self.certificate = certificate + + 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/helper/helper/qr.py b/helper/helper/qr.py index 1109f71f..fd2670c0 100644 --- a/helper/helper/qr.py +++ b/helper/helper/qr.py @@ -22,7 +22,6 @@ import subprocess # nosec import tempfile from mss.exception import ScreenShotError from PIL import Image -import numpy.core.multiarray # noqa def _capture_screen(): diff --git a/helper/poetry.lock b/helper/poetry.lock index 3a5e30d5..1aacc057 100755 --- a/helper/poetry.lock +++ b/helper/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "altgraph" version = "0.17.3" description = "Python graph (network) package" -category = "dev" optional = false python-versions = "*" files = [ @@ -16,7 +15,6 @@ files = [ name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = "*" files = [ @@ -91,14 +89,13 @@ pycparser = "*" [[package]] name = "click" -version = "8.1.3" +version = "8.1.7" 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.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -108,7 +105,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -118,31 +114,34 @@ files = [ [[package]] name = "cryptography" -version = "40.0.2" +version = "41.0.4" 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.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839"}, + {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13"}, + {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143"}, + {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397"}, + {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860"}, + {file = "cryptography-41.0.4-cp37-abi3-win32.whl", hash = "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd"}, + {file = "cryptography-41.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829"}, + {file = "cryptography-41.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9"}, + {file = "cryptography-41.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6"}, + {file = "cryptography-41.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311"}, + {file = "cryptography-41.0.4.tar.gz", hash = "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a"}, ] [package.dependencies] @@ -151,23 +150,22 @@ 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" -version = "1.1.1" +version = "1.1.3" 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.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, ] [package.extras] @@ -175,32 +173,30 @@ 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.4.1" +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.4.1-py3-none-any.whl", hash = "sha256:63ace321e24167d12fbb176b6015f4dbe06868c54a2af4f15849586afb9027fd"}, - {file = "importlib_metadata-6.4.1.tar.gz", hash = "sha256:eb1a7933041f0f85c94cd130258df3fb0dec060ad8c1c9318892ef4192c47ce1"}, + {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,32 +205,30 @@ 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 (>=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.1" 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.1-py3-none-any.whl", hash = "sha256:134832a506243891221b88b4ae1213327eea96ceb4e407a00d790bb0626f45cf"}, + {file = "importlib_resources-6.0.1.tar.gz", hash = "sha256:4359457e42708462b9626a04657c6208ad799ceb41e5c58c57ffa0e6a098a5d4"}, ] [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" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -244,28 +238,26 @@ 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" version = "0.8.0" description = "Low-level, pure Python DBus protocol wrapper." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -281,7 +273,6 @@ trio = ["async_generator", "trio"] name = "keyring" version = "23.13.1" description = "Store and access your passwords safely." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -306,7 +297,6 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec name = "macholib" version = "1.16.2" description = "Mach-O header analysis and editing" -category = "dev" optional = false python-versions = "*" files = [ @@ -319,71 +309,30 @@ altgraph = ">=0.17" [[package]] name = "more-itertools" -version = "9.1.0" +version = "10.1.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.1.0.tar.gz", hash = "sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a"}, + {file = "more_itertools-10.1.0-py3-none-any.whl", hash = "sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6"}, ] [[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"}, -] - -[[package]] -name = "numpy" -version = "1.24.2" -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 = "mss-9.0.1-py3-none-any.whl", hash = "sha256:7ee44db7ab14cbea6a3eb63813c57d677a109ca5979d3b76046e4bddd3ca1a0b"}, + {file = "mss-9.0.1.tar.gz", hash = "sha256:6eb7b9008cf27428811fa33aeb35f3334db81e3f7cc2dd49ec7c6e5a94b39f12"}, ] [[package]] name = "packaging" version = "23.1" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -395,7 +344,6 @@ files = [ name = "pefile" version = "2023.2.7" description = "Python PE parsing module" -category = "dev" optional = false python-versions = ">=3.6.0" files = [ @@ -405,78 +353,65 @@ files = [ [[package]] name = "pillow" -version = "9.5.0" +version = "10.0.1" description = "Python Imaging Library (Fork)" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, - {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, - {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, - {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, - {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, - {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, - {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, - {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, - {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, - {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, - {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, - {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, - {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, - {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, - {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, - {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, - {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, + {file = "Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a"}, + {file = "Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd"}, + {file = "Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1"}, + {file = "Pillow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08"}, + {file = "Pillow-10.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7"}, + {file = "Pillow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf"}, + {file = "Pillow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d"}, + {file = "Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d"}, ] [package.extras] @@ -485,14 +420,13 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa [[package]] name = "pluggy" -version = "1.0.0" +version = "1.3.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" 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.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [package.extras] @@ -503,7 +437,6 @@ testing = ["pytest", "pytest-benchmark"] name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -513,24 +446,23 @@ files = [ [[package]] name = "pyinstaller" -version = "5.10.1" +version = "5.13.2" 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.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.13.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:16cbd66b59a37f4ee59373a003608d15df180a0d9eb1a29ff3bfbfae64b23d0f"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8f6dd0e797ae7efdd79226f78f35eb6a4981db16c13325e962a83395c0ec7420"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_i686.whl", hash = "sha256:65133ed89467edb2862036b35d7c5ebd381670412e1e4361215e289c786dd4e6"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:7d51734423685ab2a4324ab2981d9781b203dcae42839161a9ee98bfeaabdade"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:2c2fe9c52cb4577a3ac39626b84cf16cf30c2792f785502661286184f162ae0d"}, + {file = "pyinstaller-5.13.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c63ef6133eefe36c4b2f4daf4cfea3d6412ece2ca218f77aaf967e52a95ac9b8"}, + {file = "pyinstaller-5.13.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:aadafb6f213549a5906829bb252e586e2cf72a7fbdb5731810695e6516f0ab30"}, + {file = "pyinstaller-5.13.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b2e1c7f5cceb5e9800927ddd51acf9cc78fbaa9e79e822c48b0ee52d9ce3c892"}, + {file = "pyinstaller-5.13.2-py3-none-win32.whl", hash = "sha256:421cd24f26144f19b66d3868b49ed673176765f92fa9f7914cd2158d25b6d17e"}, + {file = "pyinstaller-5.13.2-py3-none-win_amd64.whl", hash = "sha256:ddcc2b36052a70052479a9e5da1af067b4496f43686ca3cdda99f8367d0627e4"}, + {file = "pyinstaller-5.13.2-py3-none-win_arm64.whl", hash = "sha256:27cd64e7cc6b74c5b1066cbf47d75f940b71356166031deb9778a2579bb874c6"}, + {file = "pyinstaller-5.13.2.tar.gz", hash = "sha256:c8e5d3489c3a7cc5f8401c2d1f48a70e588f9967e391c3b06ddac1f685f8d5d2"}, ] [package.dependencies] @@ -538,7 +470,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,29 +479,32 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2023.2" +version = "2023.8" 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.8.tar.gz", hash = "sha256:318ccc316fb2b8c0bbdff2456b444bf1ce0e94cb3948a0f4dd48f6fc33d41c01"}, + {file = "pyinstaller_hooks_contrib-2023.8-py2.py3-none-any.whl", hash = "sha256:d091a52fbeed71cde0359aa9ad66288521a8441cfba163d9446606c5136c72a8"}, ] [[package]] name = "pyscard" version = "2.0.7" description = "Smartcard module for Python." -category = "main" optional = false python-versions = "*" files = [ + {file = "pyscard-2.0.7-cp310-cp310-win32.whl", hash = "sha256:06666a597e1293421fa90e0d4fc2418add447b10b7dc85f49b3cafc23480f046"}, {file = "pyscard-2.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a2266345bd387854298153264bff8b74f494581880a76e3e8679460c1b090fab"}, + {file = "pyscard-2.0.7-cp311-cp311-win32.whl", hash = "sha256:beacdcdc3d1516e195f7a38ec3966c5d4df7390c8f036cb41f6fef72bc5cc646"}, {file = "pyscard-2.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e37b697327e8dc4848c481428d1cbd10b7ae2ce037bc799e5b8bbd2fc3ab5ed"}, + {file = "pyscard-2.0.7-cp37-cp37m-win32.whl", hash = "sha256:a0c5edbedafba62c68160884f878d9f53996d7219a3fc11b1cea6bab59c7f34a"}, {file = "pyscard-2.0.7-cp37-cp37m-win_amd64.whl", hash = "sha256:f704ad40dc40306e1c0981941789518ab16aa1f84443b1d52ec0264884092b3b"}, + {file = "pyscard-2.0.7-cp38-cp38-win32.whl", hash = "sha256:59a466ab7ae20188dd197664b9ca1ea9524d115a5aa5b16b575a6b772cdcb73c"}, {file = "pyscard-2.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:da70aa5b7be5868b88cdb6d4a419d2791b6165beeb90cd01d2748033302a0f43"}, {file = "pyscard-2.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2d4bdc1f4e0e6c46e417ac1bc9d5990f7cfb24a080e890d453781405f7bd29dc"}, + {file = "pyscard-2.0.7-cp39-cp39-win32.whl", hash = "sha256:39e030c47878b37ae08038a917959357be6468da52e8b144e84ffc659f50e6e2"}, {file = "pyscard-2.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:5a5865675be294c8d91f22dc91e7d897c4138881e5295fb6b2cd821f7c0389d9"}, {file = "pyscard-2.0.7.tar.gz", hash = "sha256:278054525fa75fbe8b10460d87edcd03a70ad94d688b11345e4739987f85c1bf"}, ] @@ -580,14 +515,13 @@ pyro = ["Pyro"] [[package]] name = "pytest" -version = "7.3.1" +version = "7.4.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.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, ] [package.dependencies] @@ -599,13 +533,12 @@ 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" version = "306" description = "Python for Window Extensions" -category = "main" optional = false python-versions = "*" files = [ @@ -627,21 +560,19 @@ files = [ [[package]] name = "pywin32-ctypes" -version = "0.2.0" -description = "" -category = "main" +version = "0.2.2" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" 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.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, + {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, ] [[package]] name = "secretstorage" version = "3.3.3" description = "Python bindings to FreeDesktop.org Secret Service API" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -655,26 +586,24 @@ jeepney = ">=0.6" [[package]] name = "setuptools" -version = "67.6.1" +version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, - {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "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-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"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -684,19 +613,18 @@ files = [ [[package]] name = "yubikey-manager" -version = "5.1.0" +version = "5.2.0" description = "Tool for managing your YubiKey configuration." -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "yubikey_manager-5.1.0-py3-none-any.whl", hash = "sha256:72ac412319ee9c9db13173a68326de11478f1e8b3ed13b25bb3d33157b3f958e"}, - {file = "yubikey_manager-5.1.0.tar.gz", hash = "sha256:d33efc9f82e511fd4d7c9397f6c40b37c7260221ca06fac93daeb4a46b1eb173"}, + {file = "yubikey_manager-5.2.0-py3-none-any.whl", hash = "sha256:6e0c82605f92012363ae3d69673eec6c7876e2e366aa049cff66cc6734049165"}, + {file = "yubikey_manager-5.2.0.tar.gz", hash = "sha256:45e0f09e3cee2375b6f930dd5d89c1d3a7ca5d5cccb599b16a12f8f7d989fd36"}, ] [package.dependencies] click = ">=8.0,<9.0" -cryptography = ">=3.0,<43" +cryptography = ">=3.0,<44" fido2 = ">=1.0,<2.0" keyring = ">=23.4,<24.0" pyscard = ">=2.0,<3.0" @@ -704,50 +632,50 @@ pywin32 = {version = ">=223", markers = "sys_platform == \"win32\""} [[package]] name = "zipp" -version = "3.15.0" +version = "3.17.0" 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.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "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 (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [[package]] name = "zxing-cpp" -version = "2.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-cp311-cp311-win_amd64.whl", hash = "sha256:ced97aac933484556fcaa40fb402459b7af801dace4d87f4ac515661d267b211"}, ] -[package.dependencies] -numpy = "*" +[package.source] +type = "file" +url = "zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl" + +[[package]] +name = "zxing-cpp" +version = "2.1.0" +description = "Python bindings for the zxing-cpp barcode library" +optional = false +python-versions = ">=3.6" +files = [] +develop = false + +[package.source] +type = "git" +url = "https://github.com/zxing-cpp/zxing-cpp.git" +reference = "18a722a" +resolved_reference = "18a722a443855063a00b27d03d33794fa573a61f" +subdirectory = "wrappers/python" [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "f0fc2e7d5ef423dc8b247ab6b968a63c331e78bd74bd72020b634f6823a74e3d" +content-hash = "cc024d1775a438623aad52e1c9bf693808fbbf6b0a2c5001bde7964dbd87bba7" diff --git a/helper/pyproject.toml b/helper/pyproject.toml index 296b3ff6..d5a224fa 100644 --- a/helper/pyproject.toml +++ b/helper/pyproject.toml @@ -10,14 +10,17 @@ packages = [ [tool.poetry.dependencies] python = "^3.8" -yubikey-manager = "5.1.0" -mss = "^8.0.3" -zxing-cpp = "^2.0.0" -Pillow = "^9.5.0" +yubikey-manager = "5.2.0" +mss = "^9.0.1" +zxing-cpp = [ + {git = "https://github.com/zxing-cpp/zxing-cpp.git", rev="18a722a", subdirectory = "wrappers/python", markers = "sys_platform != 'win32'"}, + {path = "zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl", markers = "sys_platform == 'win32'"} +] +Pillow = "^10.0.0" [tool.poetry.dev-dependencies] -pyinstaller = {version = "^5.10.1", python = "<3.12"} -pytest = "^7.3.1" +pyinstaller = {version = "^5.13.0", python = "<3.12"} +pytest = "^7.4.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/helper/version_info.txt b/helper/version_info.txt index 725640d4..c92cb8ea 100755 --- a/helper/version_info.txt +++ b/helper/version_info.txt @@ -6,8 +6,8 @@ VSVersionInfo( ffi=FixedFileInfo( # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) # Set not needed items to zero 0. - filevers=(6, 3, 0, 0), - prodvers=(6, 3, 0, 0), + filevers=(6, 3, 1, 0), + prodvers=(6, 3, 1, 0), # Contains a bitmask that specifies the valid bits 'flags'r mask=0x3f, # Contains a bitmask that specifies the Boolean attributes of the file. @@ -31,11 +31,11 @@ VSVersionInfo( '040904b0', [StringStruct('CompanyName', 'Yubico'), StringStruct('FileDescription', 'Yubico Authenticator Helper'), - StringStruct('FileVersion', '6.3.0-dev.0'), + StringStruct('FileVersion', '6.3.1-dev.0'), StringStruct('LegalCopyright', 'Copyright (c) Yubico'), StringStruct('OriginalFilename', 'authenticator-helper.exe'), StringStruct('ProductName', 'Yubico Authenticator'), - StringStruct('ProductVersion', '6.3.0-dev.0')]) + StringStruct('ProductVersion', '6.3.1-dev.0')]) ]), VarFileInfo([VarStruct('Translation', [1033, 1200])]) ] diff --git a/helper/zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl b/helper/zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl new file mode 100644 index 00000000..5bcad06d Binary files /dev/null and b/helper/zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl differ diff --git a/integration_test/approved_yubikeys.dart b/integration_test/_approved_yubikeys.dart similarity index 94% rename from integration_test/approved_yubikeys.dart rename to integration_test/_approved_yubikeys.dart index 7712e142..fdc17899 100644 --- a/integration_test/approved_yubikeys.dart +++ b/integration_test/_approved_yubikeys.dart @@ -15,4 +15,6 @@ */ /// list of YubiKey serial numbers which are approved to be used with integration tests -var approvedYubiKeys = []; +var approvedYubiKeys = [ + '', +]; diff --git a/integration_test/management_test.dart b/integration_test/management_test.dart index b661c980..f152c640 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'); @@ -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/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 96% rename from integration_test/android/util.dart rename to integration_test/utils/android/util.dart index 8a0f6963..a7078731 100644 --- a/integration_test/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/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 96% rename from integration_test/oath_test_util.dart rename to integration_test/utils/oath_test_util.dart index ed8844b6..aee4f5c8 100644 --- a/integration_test/oath_test_util.dart +++ b/integration_test/utils/oath_test_util.dart @@ -17,12 +17,13 @@ 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'; import 'android/util.dart'; -import 'test_util.dart'; +import '../utils/test_util.dart'; class Account { final String? issuer; @@ -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/test_util.dart b/integration_test/utils/test_util.dart similarity index 89% rename from integration_test/test_util.dart rename to integration_test/utils/test_util.dart index ea9333e8..275475c7 100644 --- a/integration_test/test_util.dart +++ b/integration_test/utils/test_util.dart @@ -17,18 +17,17 @@ 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'; 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; @@ -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/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/otp_auth_link_handler.dart b/lib/android/oath/otp_auth_link_handler.dart index d2f9c9f5..260747b7 100644 --- a/lib/android/oath/otp_auth_link_handler.dart +++ b/lib/android/oath/otp_auth_link_handler.dart @@ -18,10 +18,9 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../../app/message.dart'; -import '../../oath/models.dart'; -import '../../oath/views/add_account_page.dart'; +import '../../oath/views/utils.dart'; const _appLinkMethodsChannel = MethodChannel('app.link.methods'); @@ -30,24 +29,11 @@ void setupOtpAuthLinkHandler(BuildContext context) { final args = jsonDecode(call.arguments); switch (call.method) { case 'handleOtpAuthLink': - { - var url = args['link']; - var otpauth = CredentialData.fromUri(Uri.parse(url)); - Navigator.popUntil(context, ModalRoute.withName('/')); - await showBlurDialog( - context: context, - routeSettings: const RouteSettings(name: 'oath_add_account'), - builder: (_) { - return OathAddAccountPage( - null, - null, - credentials: null, - credentialData: otpauth, - ); - }, - ); - break; - } + Navigator.popUntil(context, ModalRoute.withName('/')); + final l10n = AppLocalizations.of(context)!; + final uri = args['link']; + await handleUri(context, null, uri, null, null, l10n); + break; default: throw PlatformException( code: 'NotImplemented', diff --git a/lib/android/oath/state.dart b/lib/android/oath/state.dart index e2c448ef..471a7a5c 100755 --- a/lib/android/oath/state.dart +++ b/lib/android/oath/state.dart @@ -22,6 +22,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:logging/logging.dart'; +import 'package:yubico_authenticator/exception/cancellation_exception.dart'; import '../../app/logging.dart'; import '../../app/models.dart'; @@ -36,33 +37,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 @@ -141,6 +140,35 @@ final addCredentialToAnyProvider = } }); +final addCredentialsToAnyProvider = Provider( + (ref) => (List credentialUris, List touchRequired) async { + try { + _log.debug( + 'Calling android with ${credentialUris.length} credentials to be added'); + + String resultString = await _methods.invokeMethod( + 'addAccountsToAny', + { + 'uris': credentialUris, + 'requireTouch': touchRequired, + }, + ); + + _log.debug('Call result: $resultString'); + var result = jsonDecode(resultString); + return result['succeeded'] == credentialUris.length; + } on PlatformException catch (pe) { + var decodedException = pe.decode(); + if (decodedException is CancellationException) { + _log.debug('User cancelled adding multiple accounts'); + } else { + _log.error('Failed to add multiple accounts.', pe); + } + + throw decodedException; + } + }); + final androidCredentialListProvider = StateNotifierProvider.autoDispose .family?, DevicePath>( (ref, devicePath) { diff --git a/lib/android/qr_scanner/qr_scanner_overlay_view.dart b/lib/android/qr_scanner/qr_scanner_overlay_view.dart index 80590e09..f7032a90 100644 --- a/lib/android/qr_scanner/qr_scanner_overlay_view.dart +++ b/lib/android/qr_scanner/qr_scanner_overlay_view.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * Copyright (C) 2022-2023 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,114 +14,142 @@ * limitations under the License. */ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'qr_scanner_scan_status.dart'; -import 'qr_scanner_util.dart'; -/// Return the rounded rect which represents the scanner area for the background -/// overlay and the stroke -RRect _getScannerAreaRRect(Size size) { - double scannerAreaWidth = getScannerAreaWidth(size); - var scannerAreaRect = Rect.fromCenter( - center: Offset(size.width / 2, size.height / 2), - width: scannerAreaWidth, - height: scannerAreaWidth); +class QRScannerOverlay extends StatelessWidget { + final ScanStatus status; + final Size screenSize; + final GlobalKey overlayWidgetKey; - return RRect.fromRectAndRadius( - scannerAreaRect, const Radius.circular(scannerAreaRadius)); + const QRScannerOverlay( + {super.key, + required this.status, + required this.screenSize, + required this.overlayWidgetKey}); + + RRect getOverlayRRect(Size size) { + final renderBox = + overlayWidgetKey.currentContext?.findRenderObject() as RenderBox; + final renderObjectSize = renderBox.size; + final renderObjectOffset = renderBox.globalToLocal(Offset.zero); + + final double shorterEdge = + min(renderObjectSize.width, renderObjectSize.height); + + var top = (size.height - shorterEdge) / 2 - 32; + + if (top + renderObjectOffset.dy < 0) { + top = -renderObjectOffset.dy; + } + + return RRect.fromRectAndRadius( + Rect.fromLTWH( + (size.width - shorterEdge) / 2, top, shorterEdge, shorterEdge), + const Radius.circular(10)); + } + + @override + Widget build(BuildContext context) { + overlayRectProvider(Size size) { + return getOverlayRRect(size); + } + + return Stack(fit: StackFit.expand, children: [ + /// clip scanner area "hole" into a darkened background + ClipPath( + clipper: _OverlayClipper(overlayRectProvider), + child: const Opacity( + opacity: 0.6, + child: ColoredBox( + color: Colors.black, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [Spacer()], + ), + ), + ), + ), + + /// draw a stroke around the scanner area + CustomPaint( + painter: _OverlayPainter(status, overlayRectProvider), + ), + ]); + } } -// CustomPainter which strokes the scannerArea -class _ScannerAreaStrokePainter extends CustomPainter { - final Color _strokeColor; +/// Paints a colored stroke and status icon. +/// The stroke area is acquired through passed in rectangle provider. +/// The color is computed from the scan status. +class _OverlayPainter extends CustomPainter { + final ScanStatus _status; + final Function(Size) _rectProvider; - _ScannerAreaStrokePainter(this._strokeColor) : super(); + _OverlayPainter(this._status, this._rectProvider) : super(); @override void paint(Canvas canvas, Size size) { + final color = _status == ScanStatus.error + ? Colors.red.shade400 + : Colors.green.shade400; Paint paint = Paint() - ..color = _strokeColor + ..color = color ..style = PaintingStyle.stroke ..strokeWidth = 3.0; - Path path = Path()..addRRect(_getScannerAreaRRect(size)); + final RRect overlayRRect = _rectProvider(size); + + Path path = Path()..addRRect(overlayRRect); canvas.drawPath(path, paint); + + if (_status == ScanStatus.success) { + const icon = Icons.check_circle; + final iconSize = + overlayRRect.width < 150 ? overlayRRect.width - 5.0 : 150.0; + TextPainter iconPainter = TextPainter( + textDirection: TextDirection.rtl, + textAlign: TextAlign.center, + ); + iconPainter.text = TextSpan( + text: String.fromCharCode(icon.codePoint), + style: TextStyle( + fontSize: iconSize, + fontFamily: icon.fontFamily, + color: color.withAlpha(240), + )); + iconPainter.layout(); + iconPainter.paint( + canvas, + overlayRRect.center.translate(-iconSize / 2, -iconSize / 2), + ); + } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } -/// clips the scanner area rounded rect of specific Size -class _ScannerAreaClipper extends CustomClipper { +/// Clips a hole into the background. +/// The clipped area is acquired through passed in rectangle provider. +class _OverlayClipper extends CustomClipper { + final Function(Size) _rectProvider; + + _OverlayClipper(this._rectProvider); + @override Path getClip(Size size) { return Path() ..addRect(Rect.fromLTWH(0, 0, size.width, size.height)) - ..addRRect(_getScannerAreaRRect(size)) + ..addRRect(_rectProvider(size)) ..fillType = PathFillType.evenOdd; } @override - bool shouldReclip(covariant CustomClipper oldClipper) => true; -} - -class QRScannerOverlay extends StatelessWidget { - final ScanStatus status; - final Size screenSize; - - const QRScannerOverlay({ - super.key, - required this.status, - required this.screenSize, - }); - - @override - Widget build(BuildContext context) { - var size = screenSize; - - return Stack(children: [ - /// clip scanner area "hole" into a darkened background - ClipPath( - clipper: _ScannerAreaClipper(), - child: const Opacity( - opacity: 0.6, - child: ColoredBox( - color: Colors.black, - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [Spacer()], - )))), - - /// draw a stroke around the scanner area - Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - CustomPaint( - painter: _ScannerAreaStrokePainter(status == ScanStatus.error - ? Colors.red.shade400 - : Colors.green.shade400), - ), - ], - ), - - /// extra icon when successful scan occurred - if (status == ScanStatus.success) - Positioned.fromRect( - rect: Rect.fromCenter( - center: Offset(size.width / 2, size.height / 2), - width: size.width, - height: size.height), - child: Icon( - Icons.check_circle, - size: 200, - color: Colors.green.shade400, - )), - ]); - } + bool shouldReclip(covariant CustomClipper oldClipper) => false; } diff --git a/lib/android/qr_scanner/qr_scanner_permissions_view.dart b/lib/android/qr_scanner/qr_scanner_permissions_view.dart index 30bdb050..935c0520 100644 --- a/lib/android/qr_scanner/qr_scanner_permissions_view.dart +++ b/lib/android/qr_scanner/qr_scanner_permissions_view.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * Copyright (C) 2022-2023 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'qr_scanner_scan_status.dart'; -import 'qr_scanner_util.dart'; class QRScannerPermissionsUI extends StatelessWidget { final ScanStatus status; @@ -34,71 +33,62 @@ class QRScannerPermissionsUI extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final scannerAreaWidth = getScannerAreaWidth(screenSize); - return Stack(children: [ - /// instruction text under the scanner area - Positioned.fromRect( - rect: Rect.fromCenter( - center: Offset(screenSize.width / 2, - screenSize.height - scannerAreaWidth / 2.0 + 8.0), - width: screenSize.width, - height: screenSize.height), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 36), - child: Text( + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( l10n.p_need_camera_permission, style: const TextStyle(color: Colors.white), textAlign: TextAlign.center, ), - )), - - /// button for manual entry - Positioned.fromRect( - rect: Rect.fromCenter( - center: Offset(screenSize.width / 2, screenSize.height), - width: screenSize.width, - height: screenSize.height), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Text( - l10n.q_have_account_info, - textScaleFactor: 0.7, - style: const TextStyle(color: Colors.white), - ), - OutlinedButton( - onPressed: () { - Navigator.of(context).pop(''); - }, - child: Text( - l10n.s_enter_manually, + Column( + children: [ + Text( + l10n.q_have_account_info, + textScaleFactor: 0.7, style: const TextStyle(color: Colors.white), - )), - ], - ), - Column( - children: [ - Text( - l10n.q_want_to_scan, - textScaleFactor: 0.7, - style: const TextStyle(color: Colors.white), + ), + OutlinedButton( + onPressed: () { + Navigator.of(context).pop(''); + }, + child: Text( + l10n.s_enter_manually, + style: const TextStyle(color: Colors.white), + )), + ], ), - OutlinedButton( - onPressed: () { - onPermissionRequest(); - }, - child: Text( - l10n.s_review_permissions, + Column( + children: [ + Text( + l10n.q_want_to_scan, + textScaleFactor: 0.7, style: const TextStyle(color: Colors.white), - )), - ], - ) - ]), + ), + OutlinedButton( + onPressed: () { + onPermissionRequest(); + }, + child: Text( + l10n.s_review_permissions, + style: const TextStyle(color: Colors.white), + )), + ], + ) + ]) + ], + ), ), - ]); + ); } } diff --git a/lib/android/qr_scanner/qr_scanner_ui_view.dart b/lib/android/qr_scanner/qr_scanner_ui_view.dart index e730b964..45c396e8 100644 --- a/lib/android/qr_scanner/qr_scanner_ui_view.dart +++ b/lib/android/qr_scanner/qr_scanner_ui_view.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * Copyright (C) 2022-2023 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,66 +19,76 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../keys.dart' as keys; import 'qr_scanner_scan_status.dart'; -import 'qr_scanner_util.dart'; class QRScannerUI extends StatelessWidget { final ScanStatus status; final Size screenSize; + final GlobalKey overlayWidgetKey; - const QRScannerUI({ - super.key, - required this.status, - required this.screenSize, - }); + const QRScannerUI( + {super.key, + required this.status, + required this.screenSize, + required this.overlayWidgetKey}); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final scannerAreaWidth = getScannerAreaWidth(screenSize); - return Stack(children: [ - /// instruction text under the scanner area - Positioned.fromRect( - rect: Rect.fromCenter( - center: Offset(screenSize.width / 2, - screenSize.height + scannerAreaWidth / 2.0 + 8.0), - width: screenSize.width, - height: screenSize.height), - child: Text( - status != ScanStatus.error - ? l10n.l_point_camera_scan - : l10n.l_invalid_qr, - style: const TextStyle(color: Colors.white), - textAlign: TextAlign.center, - ), - ), - - /// button for manual entry - Positioned.fromRect( - rect: Rect.fromCenter( - center: Offset(screenSize.width / 2, - screenSize.height + scannerAreaWidth / 2.0 + 80.0), - width: screenSize.width, - height: screenSize.height), - child: Column( - children: [ - Text( - l10n.q_no_qr, - textScaleFactor: 0.7, - style: const TextStyle(color: Colors.white), - ), - OutlinedButton( - onPressed: () { - Navigator.of(context).pop(''); - }, - key: keys.manualEntryButton, + return Stack( + fit: StackFit.expand, + children: [ + SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 16, right: 16, top: 0, bottom: 0), + child: SizedBox( + // other widgets can find the RenderObject of this + // widget by its key value and query its size and offset. + key: overlayWidgetKey, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 0.0), child: Text( - l10n.s_enter_manually, + status != ScanStatus.error + ? l10n.l_point_camera_scan + : l10n.l_invalid_qr, style: const TextStyle(color: Colors.white), - )), - ], - ), - ), - ]); + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 16), + Column( + children: [ + Text( + l10n.q_no_qr, + textScaleFactor: 0.7, + style: const TextStyle(color: Colors.white), + ), + OutlinedButton( + onPressed: () { + Navigator.of(context).pop(''); + }, + key: keys.manualEntryButton, + child: Text( + l10n.s_enter_manually, + style: const TextStyle(color: Colors.white), + )), + ], + ), + const SizedBox(height: 8) + ], + ), + ) + ], + ); } } diff --git a/lib/android/qr_scanner/qr_scanner_view.dart b/lib/android/qr_scanner/qr_scanner_view.dart index 06ace3dd..9ca1e0c8 100755 --- a/lib/android/qr_scanner/qr_scanner_view.dart +++ b/lib/android/qr_scanner/qr_scanner_view.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * Copyright (C) 2022-2023 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,15 +38,11 @@ GlobalKey _zxingViewKey = GlobalKey(); class _QrScannerViewState extends State { String? _scannedString; - // will be used later - // ignore: unused_field - CredentialData? _credentialData; ScanStatus _status = ScanStatus.scanning; bool _previewInitialized = false; bool _permissionsGranted = false; void setError() { - _credentialData = null; _scannedString = null; _status = ScanStatus.error; @@ -59,7 +55,6 @@ class _QrScannerViewState extends State { void resetError() { setState(() { - _credentialData = null; _scannedString = null; _status = ScanStatus.scanning; @@ -67,17 +62,17 @@ class _QrScannerViewState extends State { }); } - void handleResult(String barCode) { + void handleResult(String qrCodeData) { if (_status != ScanStatus.scanning) { // on success and error ignore reported codes return; } setState(() { - if (barCode.isNotEmpty) { + if (qrCodeData.isNotEmpty) { try { - var parsedCredential = CredentialData.fromUri(Uri.parse(barCode)); - _credentialData = parsedCredential; - _scannedString = barCode; + CredentialData.fromUri(Uri.parse( + qrCodeData)); // throws ArgumentError if validation fails + _scannedString = qrCodeData; _status = ScanStatus.success; final navigator = Navigator.of(context); @@ -108,6 +103,7 @@ class _QrScannerViewState extends State { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final screenSize = MediaQuery.of(context).size; + final overlayWidgetKey = GlobalKey(); return Scaffold( resizeToAvoidBottomInset: false, extendBodyBehindAppBar: true, @@ -143,7 +139,7 @@ class _QrScannerViewState extends State { visible: _permissionsGranted, child: QRScannerZxingView( key: _zxingViewKey, - marginPct: 50, + marginPct: 10, onDetect: (scannedData) => handleResult(scannedData), onViewInitialized: (bool permissionsGranted) { Future.delayed(const Duration(milliseconds: 50), () { @@ -158,12 +154,14 @@ class _QrScannerViewState extends State { child: QRScannerOverlay( status: _status, screenSize: screenSize, + overlayWidgetKey: overlayWidgetKey, )), Visibility( visible: _permissionsGranted, child: QRScannerUI( status: _status, screenSize: screenSize, + overlayWidgetKey: overlayWidgetKey, )), Visibility( visible: _previewInitialized && !_permissionsGranted, diff --git a/lib/android/tap_request_dialog.dart b/lib/android/tap_request_dialog.dart index 34ae7a91..7e80473c 100755 --- a/lib/android/tap_request_dialog.dart +++ b/lib/android/tap_request_dialog.dart @@ -19,6 +19,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'views/nfc/nfc_activity_widget.dart'; @@ -28,6 +29,55 @@ import '../app/views/user_interaction.dart'; const _channel = MethodChannel('com.yubico.authenticator.channel.dialog'); +// _DDesc contains id of title resource for the dialog +enum _DTitle { + tapKey, + operationSuccessful, + operationFailed, + invalid; + + static _DTitle fromId(int? id) => + const { + 0: _DTitle.tapKey, + 1: _DTitle.operationSuccessful, + 2: _DTitle.operationFailed + }[id] ?? + _DTitle.invalid; +} + +// _DDesc contains action description in the dialog +enum _DDesc { + // oath descriptions + oathResetApplet, + oathUnlockSession, + oathSetPassword, + oathUnsetPassword, + oathAddAccount, + oathRenameAccount, + oathDeleteAccount, + oathCalculateCode, + oathActionFailure, + oathAddMultipleAccounts, + invalid; + + static const int dialogDescriptionOathIndex = 100; + + static _DDesc fromId(int? id) => + const { + dialogDescriptionOathIndex + 0: _DDesc.oathResetApplet, + dialogDescriptionOathIndex + 1: _DDesc.oathUnlockSession, + dialogDescriptionOathIndex + 2: _DDesc.oathSetPassword, + dialogDescriptionOathIndex + 3: _DDesc.oathUnsetPassword, + dialogDescriptionOathIndex + 4: _DDesc.oathAddAccount, + dialogDescriptionOathIndex + 5: _DDesc.oathRenameAccount, + dialogDescriptionOathIndex + 6: _DDesc.oathDeleteAccount, + dialogDescriptionOathIndex + 7: _DDesc.oathCalculateCode, + dialogDescriptionOathIndex + 8: _DDesc.oathActionFailure, + dialogDescriptionOathIndex + 9: _DDesc.oathAddMultipleAccounts + }[id] ?? + _DDesc.invalid; +} + final androidDialogProvider = Provider<_DialogProvider>( (ref) { return _DialogProvider(ref.watch(withContextProvider)); @@ -67,24 +117,51 @@ class _DialogProvider { _controller = null; } + String _getTitle(BuildContext context, int? titleId) { + final l10n = AppLocalizations.of(context)!; + return switch (_DTitle.fromId(titleId)) { + _DTitle.tapKey => l10n.s_nfc_dialog_tap_key, + _DTitle.operationSuccessful => l10n.s_nfc_dialog_operation_success, + _DTitle.operationFailed => l10n.s_nfc_dialog_operation_failed, + _ => '' + }; + } + + String _getDialogDescription(BuildContext context, int? descriptionId) { + final l10n = AppLocalizations.of(context)!; + return switch (_DDesc.fromId(descriptionId)) { + _DDesc.oathResetApplet => l10n.s_nfc_dialog_oath_reset, + _DDesc.oathUnlockSession => l10n.s_nfc_dialog_oath_unlock, + _DDesc.oathSetPassword => l10n.s_nfc_dialog_oath_set_password, + _DDesc.oathUnsetPassword => l10n.s_nfc_dialog_oath_unset_password, + _DDesc.oathAddAccount => l10n.s_nfc_dialog_oath_add_account, + _DDesc.oathRenameAccount => l10n.s_nfc_dialog_oath_rename_account, + _DDesc.oathDeleteAccount => l10n.s_nfc_dialog_oath_delete_account, + _DDesc.oathCalculateCode => l10n.s_nfc_dialog_oath_calculate_code, + _DDesc.oathActionFailure => l10n.s_nfc_dialog_oath_failure, + _DDesc.oathAddMultipleAccounts => l10n.s_nfc_dialog_oath_add_multiple_accounts, + _ => '' + }; + } + Future _updateDialogState( - String? title, String? description, String? iconName) async { + int? title, int? description, int? iconName) async { await _withContext((context) async { _controller?.updateContent( - title: title, - description: description, + title: _getTitle(context, title), + description: _getDialogDescription(context, description), icon: _icon, ); }); } Future _showDialog( - String title, String description, String? iconName) async { + int title, int description, int? iconName) async { _controller = await _withContext((context) async { return promptUserInteraction( context, - title: title, - description: description, + title: _getTitle(context, title), + description: _getDialogDescription(context, description), icon: _icon, onCancel: () { _channel.invokeMethod('cancel'); diff --git a/lib/app/message.dart b/lib/app/message.dart index 11a23478..2b2f2b41 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,44 +28,16 @@ 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, RouteSettings? routeSettings, -}) => - showGeneralDialog( + Color barrierColor = const Color(0x80000000), +}) async => + await showGeneralDialog( context: context, barrierDismissible: true, + barrierColor: barrierColor, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, pageBuilder: (ctx, anim1, anim2) => builder(ctx), transitionDuration: const Duration(milliseconds: 150), diff --git a/lib/app/models.dart b/lib/app/models.dart index 22c4875f..5599848f 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 => l10n.s_piv, _ => name.substring(0, 1).toUpperCase() + name.substring(1), }; @@ -115,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/shortcuts.dart b/lib/app/shortcuts.dart index 5c180ff7..f29a370f 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() @@ -145,18 +149,24 @@ Widget registerGlobalShortcuts( child: Shortcuts( shortcuts: { ctrlOrCmd(LogicalKeyboardKey.keyC): const CopyIntent(), - ctrlOrCmd(LogicalKeyboardKey.keyW): const HideIntent(), + const SingleActivator(LogicalKeyboardKey.copy): const CopyIntent(), ctrlOrCmd(LogicalKeyboardKey.keyF): const SearchIntent(), if (isDesktop) ...{ const SingleActivator(LogicalKeyboardKey.tab, control: true): const NextDeviceIntent(), }, if (Platform.isMacOS) ...{ + const SingleActivator(LogicalKeyboardKey.keyW, meta: true): + const HideIntent(), const SingleActivator(LogicalKeyboardKey.keyQ, meta: true): const CloseIntent(), const SingleActivator(LogicalKeyboardKey.comma, meta: true): const SettingsIntent(), }, + if (Platform.isWindows) ...{ + const SingleActivator(LogicalKeyboardKey.keyW, control: true): + const HideIntent(), + }, if (Platform.isLinux) ...{ const SingleActivator(LogicalKeyboardKey.keyQ, control: true): const CloseIntent(), diff --git a/lib/app/state.dart b/lib/app/state.dart index 717c9900..06626078 100755 --- a/lib/app/state.dart +++ b/lib/app/state.dart @@ -15,6 +15,7 @@ */ import 'dart:async'; +import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; @@ -64,14 +65,32 @@ class CommunityTranslationsNotifier extends StateNotifier { } } -final supportedLocalesProvider = Provider>((ref) => - ref.watch(communityTranslationsProvider) - ? AppLocalizations.supportedLocales - : officialLocales); +final supportedLocalesProvider = Provider>((ref) { + final locales = [...officialLocales]; + final localeStr = Platform.environment['_YA_LOCALE']; + if (localeStr != null) { + // Force locale + final locale = Locale(localeStr, ''); + locales.add(locale); + } + return ref.watch(communityTranslationsProvider) + ? AppLocalizations.supportedLocales + : locales; +}); final currentLocaleProvider = Provider( - (ref) => basicLocaleListResolution( - PlatformDispatcher.instance.locales, ref.watch(supportedLocalesProvider)), + (ref) { + final localeStr = Platform.environment['_YA_LOCALE']; + if (localeStr != null) { + // Force locale + final locale = Locale(localeStr, ''); + return basicLocaleListResolution( + [locale], AppLocalizations.supportedLocales); + } + // Choose from supported + return basicLocaleListResolution(PlatformDispatcher.instance.locales, + ref.watch(supportedLocalesProvider)); + }, ); final l10nProvider = Provider( diff --git a/lib/app/views/action_list.dart b/lib/app/views/action_list.dart new file mode 100644 index 00000000..be6733df --- /dev/null +++ b/lib/app/views/action_list.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 '../../widgets/list_title.dart'; +import '../models.dart'; + +class ActionListItem extends StatelessWidget { + final Widget icon; + final String title; + final String? subtitle; + final Widget? trailing; + final void Function(BuildContext context)? onTap; + final ActionStyle actionStyle; + + const ActionListItem({ + super.key, + required this.icon, + required this.title, + this.subtitle, + this.trailing, + this.onTap, + this.actionStyle = ActionStyle.normal, + }); + + @override + Widget build(BuildContext context) { + 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: Opacity( + opacity: onTap != null ? 1.0 : 0.4, + child: CircleAvatar( + foregroundColor: foreground, + backgroundColor: background, + child: icon, + ), + ), + trailing: trailing, + onTap: onTap != null ? () => onTap?.call(context) : null, + enabled: onTap != null, + ); + } +} + +class ActionListSection extends StatelessWidget { + final String title; + final List children; + + 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, + child: Column(children: [ + ListTitle( + title, + textStyle: Theme.of(context).textTheme.bodyLarge, + ), + ...children, + ]), + ); +} 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/app/views/app_page.dart b/lib/app/views/app_page.dart index 4028ee28..773fd634 100755 --- a/lib/app/views/app_page.dart +++ b/lib/app/views/app_page.dart @@ -16,18 +16,23 @@ 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'; -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; 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,31 +45,49 @@ class AppPage extends StatelessWidget { this.keyActionsBuilder, this.actionButtonBuilder, this.delayedContent = false, + this.keyActionsBadge = false, }); @override Widget build(BuildContext context) => LayoutBuilder( builder: (context, constraints) { - if (constraints.maxWidth < 540) { - // Single column layout - return _buildScaffold(context, true); + final bool singleColumn; + final bool hasRail; + if (isAndroid) { + final isPortrait = constraints.maxWidth < constraints.maxHeight; + singleColumn = isPortrait || constraints.maxWidth < 600; + hasRail = constraints.maxWidth > 600; } else { - // Two-column layout + singleColumn = constraints.maxWidth < 600; + hasRail = constraints.maxWidth > 400; + } + + if (singleColumn) { + // Single column layout, maybe with rail + return _buildScaffold(context, true, hasRail); + } else { + // 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), ), ], ), @@ -73,7 +96,51 @@ 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: 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), + ], + ), + Material( + type: MaterialType.transparency, + child: NavigationContent(key: _navExpandedKey, extended: true)), + ], + ), + ), + )); + } + + Widget _buildMainContent() { final content = Column( children: [ child, @@ -81,8 +148,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, @@ -93,6 +159,7 @@ class AppPage extends StatelessWidget { ], ); return SingleChildScrollView( + primary: false, child: SafeArea( child: Center( child: SizedBox( @@ -110,7 +177,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( @@ -118,6 +205,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( @@ -125,22 +226,31 @@ class AppPage extends StatelessWidget { child: IconButton( key: actionsIconButtonKey, onPressed: () { - showBlurDialog(context: context, builder: keyActionsBuilder!); + showBlurDialog( + context: context, + barrierColor: Colors.transparent, + 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), ), ), - 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_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 new file mode 100644 index 00000000..68b19a4c --- /dev/null +++ b/lib/app/views/device_picker.dart @@ -0,0 +1,428 @@ +/* + * 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 '../../android/state.dart'; +import '../../core/state.dart'; +import '../../management/models.dart'; +import '../models.dart'; +import '../state.dart'; +import 'device_avatar.dart'; +import 'keys.dart' as keys; + +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; + + Widget? androidNoKeyWidget; + if (isAndroid && devices.isEmpty) { + var hasNfcSupport = ref.watch(androidNfcSupportProvider); + var isNfcEnabled = ref.watch(androidNfcStateProvider); + final subtitle = hasNfcSupport && isNfcEnabled + ? l10n.l_insert_or_tap_yk + : l10n.l_insert_yk; + + androidNoKeyWidget = _DeviceRow( + leading: const DeviceAvatar(child: Icon(Icons.usb)), + title: l10n.l_no_yk_present, + subtitle: subtitle, + onTap: () { + ref.read(currentDeviceProvider.notifier).setCurrentDevice(null); + }, + selected: currentNode == null, + extended: extended, + ); + } + + List children = [ + if (showUsb) + _DeviceRow( + 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, + ), + if (androidNoKeyWidget != null) + androidNoKeyWidget, + ...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, overflow: TextOverflow.fade, softWrap: false), + 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( + key: keys.deviceInfoListTile, + 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/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 new file mode 100644 index 00000000..029538f5 --- /dev/null +++ b/lib/app/views/fs_dialog.dart @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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; + 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.withOpacity(0.7), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: SingleChildScrollView(child: child), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: TextButton.icon( + key: keys.closeButton, + icon: const Icon(Icons.close), + label: Text(l10n.s_close), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ) + ], + ), + ), + ); + } +} 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'); 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/main_page.dart b/lib/app/views/main_page.dart index 5091dcf6..c61b61a6 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -17,16 +17,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - import '../../android/app_methods.dart'; import '../../android/state.dart'; import '../../android/views/nfc/main_page_nfc_activity_widget.dart'; import '../../exception/cancellation_exception.dart'; import '../../core/state.dart'; import '../../fido/views/fido_screen.dart'; -import '../../oath/models.dart'; import '../../oath/views/add_account_page.dart'; import '../../oath/views/oath_screen.dart'; +import '../../oath/views/utils.dart'; +import '../../piv/views/piv_screen.dart'; import '../../widgets/custom_icons.dart'; import '../message.dart'; import '../models.dart'; @@ -100,30 +100,30 @@ class MainPage extends ConsumerWidget { icon: const Icon(Icons.person_add_alt_1), tooltip: l10n.s_add_account, onPressed: () async { - CredentialData? otpauth; + final withContext = ref.read(withContextProvider); final scanner = ref.read(qrScannerProvider); if (scanner != null) { try { - final url = await scanner.scanQr(); - if (url != null) { - otpauth = CredentialData.fromUri(Uri.parse(url)); + final qrData = await scanner.scanQr(); + if (qrData != null) { + await withContext((context) => + handleUri(context, null, qrData, null, null, l10n)); + return; } } on CancellationException catch (_) { // ignored - user cancelled return; } } - - await ref.read(withContextProvider)((context) => showBlurDialog( + await withContext((context) => showBlurDialog( context: context, routeSettings: const RouteSettings(name: 'oath_add_account'), builder: (context) { - return OathAddAccountPage( + return const OathAddAccountPage( null, null, credentials: null, - credentialData: otpauth, ); }, )); @@ -162,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/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/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()); + }, + ), + ], + ), + ); + } +} 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/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..bd28cdcb 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'; @@ -45,47 +46,70 @@ 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; + + 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)); 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), + + 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); + } + } + }, ); - 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']); - }); + return _build(devicePath); + } @override Stream reset() { @@ -105,8 +129,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); } @@ -151,22 +175,38 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier { } } -final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family< - FidoFingerprintsNotifier, AsyncValue>, DevicePath>( - (ref, devicePath) => _DesktopFidoFingerprintsNotifier( - ref.watch(_sessionProvider(devicePath)), - )); +final desktopFingerprintProvider = AsyncNotifierProvider.autoDispose + .family, DevicePath>( + _DesktopFidoFingerprintsNotifier.new); class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier { - final RpcNodeSession _session; + late RpcNodeSession _session; - _DesktopFidoFingerprintsNotifier(this._session) { - _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 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()); @@ -176,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 @@ -184,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 = () { @@ -210,7 +246,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); @@ -227,25 +263,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)), - )); +final desktopCredentialProvider = AsyncNotifierProvider.autoDispose + .family, DevicePath>( + _DesktopFidoCredentialsNotifier.new); class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier { - final RpcNodeSession _session; + late RpcNodeSession _session; - _DesktopFidoCredentialsNotifier(this._session) { - _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,7 +310,7 @@ class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier { userName: e.value['user_name'])); } } - setItems(creds); + return List.unmodifiable(creds); } @override @@ -268,6 +320,6 @@ class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier { credential.rpId, credential.credentialId, ]); - await _refresh(); + ref.invalidate(fidoStateProvider(_session.devicePath)); } } 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/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..92aeb880 100755 --- a/lib/desktop/oath/state.dart +++ b/lib/desktop/oath/state.dart @@ -57,56 +57,51 @@ 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.onDispose(() { - 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) { + ..setErrorHandler('auth-required', (e) async { + final key = ref.read(_oathLockKeyProvider(_session.devicePath)); + if (key != null) { final result = await _session.command('validate', params: {'key': key}); if (result['valid']) { - oathState = oathState.copyWith(locked: false); + ref.invalidateSelf(); + return; } else { - _ref + ref .read(_oathLockKeyProvider(_session.devicePath).notifier) .unsetKey(); } } - return oathState; + throw e; }); + ref.onDispose(() { + _session + ..unsetErrorHandler('state-reset') + ..unsetErrorHandler('auth-required'); + }); + final result = await _session.command('get'); + _log.debug('application status', jsonEncode(result)); + return OathState.fromJson(result['data']); + } @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 +115,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 +153,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 +172,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 +180,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/desktop/piv/state.dart b/lib/desktop/piv/state.dart new file mode 100644 index 00000000..955a4743 --- /dev/null +++ b/lib/desktop/piv/state.dart @@ -0,0 +1,435 @@ +/* + * 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 '../../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 '../models.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', (e) 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.invalidateSelf(); + throw e; + } + } else { + ref.invalidateSelf(); + throw e; + } + }); + 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.read(_managementKeyProvider(_devicePath).notifier).state = null; + 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.read(_managementKeyProvider(_devicePath).notifier).state = + managementKey; + 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(); + +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.hexId]); + 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.hexId, + ], + 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', params: { + 'data': data, + 'password': password, + }); + + if (result['status']) { + return PivExamineResult.fromJson({'runtimeType': 'result', ...result}); + } else { + return PivExamineResult.invalidPassword(); + } + } + + @override + Future validateRfc4514(String value) async { + final result = await _session.command('validate_rfc4514', params: { + 'data': value, + }); + return result['status']; + } + + @override + Future import(SlotId slot, String data, + {String? password, + PinPolicy pinPolicy = PinPolicy.dfault, + TouchPolicy touchPolicy = TouchPolicy.dfault}) async { + final result = await _session.command('import_file', target: [ + 'slots', + slot.hexId, + ], 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.hexId, + ]); + final data = result['data']; + final metadata = data['metadata']; + return ( + metadata != null ? SlotMetadata.fromJson(metadata) : null, + data['certificate'] as String?, + ); + } +} diff --git a/lib/desktop/systray.dart b/lib/desktop/systray.dart index 246bc987..6ba2219f 100755 --- a/lib/desktop/systray.dart +++ b/lib/desktop/systray.dart @@ -94,7 +94,7 @@ Future _calculateCode( String _getIcon() { if (Platform.isMacOS) { - return 'resources/icons/systray-template.eps'; + return 'resources/icons/systray-template.png'; } if (Platform.isWindows) { return 'resources/icons/com.yubico.yubioath.ico'; diff --git a/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/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/state.dart b/lib/fido/state.dart index 1b998c2b..4fb4ee25 100755 --- a/lib/fido/state.dart +++ b/lib/fido/state.dart @@ -14,16 +14,15 @@ * limitations under the License. */ -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; 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 { @@ -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); } 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 new file mode 100644 index 00000000..7b9d1119 --- /dev/null +++ b/lib/fido/views/credential_dialog.dart @@ -0,0 +1,93 @@ +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 '../../app/views/action_list.dart'; +import '../models.dart'; +import 'actions.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(); + } + + final l10n = AppLocalizations.of(context)!; + 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), + ], + ), + ), + ActionListSection.fromMenuActions( + context, + l10n.s_actions, + actions: buildCredentialActions(l10n), + ), + ], + ), + ), + ), + ); + } +} 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/fingerprint_dialog.dart b/lib/fido/views/fingerprint_dialog.dart new file mode 100644 index 00000000..398493cb --- /dev/null +++ b/lib/fido/views/fingerprint_dialog.dart @@ -0,0 +1,109 @@ +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 '../../app/views/action_list.dart'; +import '../models.dart'; +import 'actions.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(); + } + + final l10n = AppLocalizations.of(context)!; + 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), + ], + ), + ), + ActionListSection.fromMenuActions( + context, + l10n.s_actions, + actions: buildFingerprintActions(l10n), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/fido/views/key_actions.dart b/lib/fido/views/key_actions.dart index 51ab8ac5..e2d153ab 100755 --- a/lib/fido/views/key_actions.dart +++ b/lib/fido/views/key_actions.dart @@ -19,72 +19,90 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../app/message.dart'; import '../../app/models.dart'; -import '../../widgets/list_title.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'; +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, - 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 - ? () { + + return FsDialog( + child: Column( + children: [ + if (state.bioEnroll != null) + ActionListSection( + l10n.s_setup, + children: [ + ActionListItem( + key: keys.addFingerprintAction, + actionStyle: ActionStyle.primary, + 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 + ? (context) { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => AddFingerprintDialog(node.path), + ); + } + : null, + ), + ], + ), + ActionListSection( + 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 + ? l10n.s_fido_pin_protection + : l10n.l_fido_pin_protection_optional, + trailing: state.alwaysUv && !state.hasPin + ? const Icon(Icons.warning_amber) + : null, + onTap: (context) { Navigator.of(context).pop(); showBlurDialog( context: context, - builder: (context) => AddFingerprintDialog(node.path), + builder: (context) => FidoPinDialog(node.path, state), ); - } - : null, - ), + }), + 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: (context) { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ResetDialog(node), + ); + }, + ), + ], + ) ], - 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), - 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/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/pin_dialog.dart b/lib/fido/views/pin_dialog.dart index fdf4b411..c70487c4 100755 --- a/lib/fido/views/pin_dialog.dart +++ b/lib/fido/views/pin_dialog.dart @@ -22,6 +22,7 @@ import 'package:logging/logging.dart'; import '../../app/logging.dart'; import '../../app/message.dart'; import '../../app/models.dart'; +import '../../app/state.dart'; import '../../desktop/models.dart'; import '../../widgets/responsive_dialog.dart'; import '../models.dart'; @@ -184,10 +185,14 @@ class _FidoPinDialogState extends ConsumerState { } else { errorMessage = e.toString(); } - showMessage( - context, - l10n.l_set_pin_failed(errorMessage), - duration: const Duration(seconds: 4), + await ref.read(withContextProvider)( + (context) async { + showMessage( + context, + l10n.l_set_pin_failed(errorMessage), + duration: const Duration(seconds: 4), + ); + }, ); } } diff --git a/lib/fido/views/unlocked_page.dart b/lib/fido/views/unlocked_page.dart index b682f745..05395b53 100755 --- a/lib/fido/views/unlocked_page.dart +++ b/lib/fido/views/unlocked_page.dart @@ -20,14 +20,19 @@ 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'; @@ -48,42 +53,27 @@ 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( + children.add(ListTitle(l10n.s_passkeys)); + children.addAll(creds.map((cred) => Actions( + actions: { + OpenIntent: CallbackAction( + onInvoke: (_) => showBlurDialog( context: context, - builder: (context) => - DeleteCredentialDialog(node.path, cred), - ); - }, - icon: const Icon(Icons.delete_outline)), - ], - ), - ), - ), - ); + barrierColor: Colors.transparent, + builder: (context) => CredentialDialog(cred), + )), + DeleteIntent: CallbackAction( + onInvoke: (_) => showBlurDialog( + context: context, + builder: (context) => DeleteCredentialDialog( + node.path, + cred, + ), + ), + ), + }, + child: _CredentialListItem(cred), + ))); } } @@ -97,40 +87,32 @@ 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( + children.addAll(fingerprints.map((fp) => Actions( + actions: { + OpenIntent: CallbackAction( + onInvoke: (_) => showBlurDialog( context: context, - builder: (context) => - RenameFingerprintDialog(node.path, fp), - ); - }, - icon: const Icon(Icons.edit_outlined)), - IconButton( - onPressed: () { - showBlurDialog( + barrierColor: Colors.transparent, + builder: (context) => FingerprintDialog(fp), + )), + EditIntent: CallbackAction( + onInvoke: (_) => showBlurDialog( context: context, - builder: (context) => - DeleteFingerprintDialog(node.path, fp), - ); - }, - icon: const Icon(Icons.delete_outline)), - ], - ), + builder: (context) => RenameFingerprintDialog( + node.path, + fp, + ), + )), + DeleteIntent: CallbackAction( + onInvoke: (_) => showBlurDialog( + context: context, + builder: (context) => DeleteFingerprintDialog( + node.path, + fp, + ), + )), + }, + child: _FingerprintListItem(fp), ))); } } @@ -140,6 +122,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 +136,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 +146,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 +157,50 @@ class FidoUnlockedPage extends ConsumerWidget { child: const CircularProgressIndicator(), ); } + +class _CredentialListItem extends StatelessWidget { + final FidoCredential credential; + const _CredentialListItem(this.credential); + + @override + Widget build(BuildContext context) { + return AppListItem( + leading: CircleAvatar( + foregroundColor: Theme.of(context).colorScheme.onPrimary, + backgroundColor: Theme.of(context).colorScheme.primary, + child: const Icon(Icons.person), + ), + title: credential.userName, + subtitle: credential.rpId, + trailing: OutlinedButton( + onPressed: Actions.handler(context, const OpenIntent()), + child: const Icon(Icons.more_horiz), + ), + buildPopupActions: (context) => + buildCredentialActions(AppLocalizations.of(context)!), + ); + } +} + +class _FingerprintListItem extends StatelessWidget { + final Fingerprint fingerprint; + const _FingerprintListItem(this.fingerprint); + + @override + Widget build(BuildContext context) { + return AppListItem( + leading: CircleAvatar( + foregroundColor: Theme.of(context).colorScheme.onSecondary, + backgroundColor: Theme.of(context).colorScheme.secondary, + child: const Icon(Icons.fingerprint), + ), + title: fingerprint.label, + trailing: OutlinedButton( + onPressed: Actions.handler(context, const OpenIntent()), + child: const Icon(Icons.more_horiz), + ), + buildPopupActions: (context) => + buildFingerprintActions(AppLocalizations.of(context)!), + ); + } +} 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 diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2677935a..d041dcff 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -29,10 +29,13 @@ "s_quit": "Quit", "s_unlock": "Unlock", "s_calculate": "Calculate", + "s_import": "Import", + "s_overwrite": "Overwrite", "s_label": "Label", "s_name": "Name", "s_usb": "USB", "s_nfc": "NFC", + "s_options": "Options", "s_show_window": "Show window", "s_hide_window": "Hide window", "q_rename_target": "Rename {label}?", @@ -41,13 +44,22 @@ "label": {} } }, + "l_bullet": "• {item}", + "@l_bullet" : { + "placeholders": { + "item": {} + } + }, "s_about": "About", + "s_algorithm": "Algorithm", "s_appearance": "Appearance", "s_authenticator": "Authenticator", + "s_actions": "Actions", "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", @@ -60,6 +72,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?", @@ -165,11 +178,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": { @@ -177,18 +196,31 @@ } }, "s_pin_set": "PIN set", + "s_puk_set": "PUK set", "l_set_pin_failed": "Failed to set PIN: {message}", "@l_set_pin_failed" : { "placeholders": { "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": { "retries": {} } }, + "l_wrong_puk_attempts_remaining": "Wrong PUK, {retries} attempt(s) remaining", + "@l_wrong_puk_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, "s_fido_pin_protection": "FIDO PIN protection", "l_fido_pin_protection_optional": "Optional FIDO PIN protection", "l_enter_fido2_pin": "Enter the FIDO2 PIN for your YubiKey", @@ -197,13 +229,24 @@ "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": { "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", + "l_piv_pin_puk_blocked": "Blocked, factory reset needed", + "p_enter_new_piv_pin_puk": "Enter a new {name} to set. Must be 6-8 characters.", + "@p_enter_new_piv_pin_puk" : { + "placeholders": { + "name": {} + } + }, "@_passwords": {}, "s_password": "Password", @@ -227,6 +270,23 @@ "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", + "s_generate_random": "Generate random", + "s_use_default": "Use default", + "l_warning_default_key": "Warning: Default key used", + "s_protect_key": "Protect with PIN", + "l_pin_protected_key": "PIN can be used instead", + "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" : { @@ -237,6 +297,9 @@ "s_accounts": "Accounts", "s_no_accounts": "No accounts", "s_add_account": "Add account", + "s_add_accounts" : "Add account(s)", + "p_add_description" : "To scan a QR code, make sure the full code is visible on screen and press the button below. You can also drag a saved image from a folder onto this dialog. If you have the account credential details in writing, use the manual entry instead.", + "s_add_manually" : "Add manually", "s_account_added": "Account added", "l_account_add_failed": "Failed adding account: {message}", "@l_account_add_failed" : { @@ -246,15 +309,20 @@ }, "l_account_name_required": "Your account must have a name", "l_name_already_exists": "This name already exists for the issuer", + "l_account_already_exists": "This account already exists on the YubiKey", "l_invalid_character_issuer": "Invalid character: ':' is not allowed in issuer", + "l_select_accounts" : "Select account(s) to add to the YubiKey", "s_pinned": "Pinned", "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,21 +350,25 @@ "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}", - "@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}", @@ -318,12 +390,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" : { @@ -341,6 +415,83 @@ "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_certificates": "Certificates", + "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_importing_file": "Importing file\u2026", + "s_file_imported": "File imported", + "l_delete_certificate": "Delete certificate", + "l_delete_certificate_desc": "Remove the certificate from your YubiKey", + "s_issuer": "Issuer", + "s_serial": "Serial", + "s_certificate_fingerprint": "Fingerprint", + "s_valid_from": "Valid from", + "s_valid_to": "Valid to", + "l_no_certificate": "No certificate loaded", + "l_key_no_certificate": "Key without certificate loaded", + "s_generate_key": "Generate key", + "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": {} + } + }, + "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" : { + "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 item(s) will be imported into PIV slot {slot}.", + "@p_import_items_desc" : { + "placeholders": { + "slot": {} + } + }, + "p_subject_desc": "A distinguished name (DN) formatted in accordance to the RFC 4514 specification.", + "l_rfc4514_invalid": "Invalid RFC 4514 format", + "rfc4514_examples": "Examples:\nCN=Example Name\nCN=jsmith,DC=example,DC=net", + "p_cert_options_desc": "Key algorithm to use, output format, and expiration date (certificate only).", + "s_overwrite_slot": "Overwrite slot", + "p_overwrite_slot_desc": "This will permanently overwrite existing content in slot {slot}.", + "@p_overwrite_slot_desc" : { + "placeholders": { + "slot": {} + } + }, + "l_overwrite_cert": "The certificate will be overwritten", + "l_overwrite_key": "The private key will be overwritten", + "l_overwrite_key_maybe": "Any existing private key in the slot will be overwritten", + + "@_piv_slots": {}, + "s_slot_display_name": "{name} ({hexid})", + "@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", @@ -381,10 +532,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 default values.", "@_copy_to_clipboard": {}, "l_copy_to_clipboard": "Copy to clipboard", @@ -441,5 +596,20 @@ "l_launch_app_on_usb_off": "Other apps can use the YubiKey over USB", "s_allow_screenshots": "Allow screenshots", + "s_nfc_dialog_tap_key": "Tap your key", + "s_nfc_dialog_operation_success": "Success", + "s_nfc_dialog_operation_failed": "Failed", + + "s_nfc_dialog_oath_reset": "Action: reset OATH applet", + "s_nfc_dialog_oath_unlock": "Action: unlock OATH applet", + "s_nfc_dialog_oath_set_password": "Action: set OATH password", + "s_nfc_dialog_oath_unset_password": "Action: remove OATH password", + "s_nfc_dialog_oath_add_account": "Action: add new account", + "s_nfc_dialog_oath_rename_account": "Action: rename account", + "s_nfc_dialog_oath_delete_account": "Action: delete account", + "s_nfc_dialog_oath_calculate_code": "Action: calculate OATH code", + "s_nfc_dialog_oath_failure": "OATH operation failed", + "s_nfc_dialog_oath_add_multiple_accounts": "Action: add multiple accounts", + "@_eof": {} } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb new file mode 100644 index 00000000..c35cfeb1 --- /dev/null +++ b/lib/l10n/app_fr.arb @@ -0,0 +1,615 @@ +{ + "@@locale": "fr", + + "@_readme": { + "notes": [ + "Toutes les chaînes de caractères commencent par une lettre majuscule.", + "Regroupez les chaînes de caractères par catégories, mais ne les liez pas inutilement à une section de l'application si elles peuvent être réutilisées entre plusieurs.", + "Exécutez check_strings.py sur le fichier .arb pour détecter les problèmes et ajustez @_lint_rules si nécessaire par langue." + ], + "prefixes": { + "s_": "Un seul ou peu de mots. Doit être assez court pour être affiché sur un bouton ou un entête.", + "l_": "Une seule ligne, peut être enroulée. Ne doit pas être plus d'une phrase et ne finis pas par un point.", + "p_": "Une ou plusieurs phrases avec ponctuation.", + "q_": "Une question se terminant par un point d'interrogation." + } + }, + + "@_lint_rules": { + "s_max_words": 5, + "s_max_length": 32 + }, + + "app_name": "Yubico Authenticator", + + "s_save": "Sauvegarder", + "s_cancel": "Annuler", + "s_close": "Fermer", + "s_delete": "Supprimer", + "s_quit": "Quitter", + "s_unlock": "Déverrouiller", + "s_calculate": "Calculer", + "s_import": "Importer", + "s_overwrite": "Écraser", + "s_label": "Étiquette", + "s_name": "Nom", + "s_usb": "USB", + "s_nfc": "NFC", + "s_options": "Options", + "s_show_window": "Afficher la fenêtre", + "s_hide_window": "Masquer la fenêtre", + "q_rename_target": "Renommer {label}?", + "@q_rename_target" : { + "placeholders": { + "label": {} + } + }, + "l_bullet": "• {item}", + "@l_bullet" : { + "placeholders": { + "item": {} + } + }, + + "s_about": "À propos", + "s_algorithm": "Algorithme", + "s_appearance": "Apparence", + "s_authenticator": "Authenticator", + "s_actions": "Actions", + "s_manage": "Gérer", + "s_setup": "Configuration", + "s_settings": "Paramètres", + "s_piv": "PIV", + "s_webauthn": "WebAuthn", + "s_help_and_about": "Aide et à propos", + "s_help_and_feedback": "Aide et retours", + "s_send_feedback": "Envoyer nous un retour", + "s_i_need_help": "J'ai besoin d'aide", + "s_troubleshooting": "Dépannage", + "s_terms_of_use": "Termes d'utilisation", + "s_privacy_policy": "Politique de confidentialité", + "s_open_src_licenses": "Licenses Open source", + "s_configure_yk": "Configurer la YubiKey", + "s_please_wait": "Veuillez patienter\u2026", + "s_secret_key": "Clé secrète", + "s_private_key": "Clé privée", + "s_invalid_length": "Longueur invalide", + "s_require_touch": "Touché requis", + "q_have_account_info": "Avez-vous des informations de compte?", + "s_run_diagnostics": "Exécuter un diagnostique", + "s_log_level": "Niveau de log: {level}", + "@s_log_level": { + "placeholders": { + "level": {} + } + }, + "s_character_count": "Nombre de caractères", + "s_learn_more": "En savoir\u00a0plus", + + "@_language": {}, + "s_language": "Langue", + "l_enable_community_translations": "Activer les traductions de la communauté", + "p_community_translations_desc": "Ces traductions sont fournies et maintenues par la communauté. Elles peuvent contenir des erreurs ou être incomplètes.", + + "@_theme": {}, + "s_app_theme": "Thèmes de l'application", + "s_choose_app_theme": "Choix du thème", + "s_system_default": "Thème du système", + "s_light_mode": "Thème clair", + "s_dark_mode": "Thème sombre", + + "@_yubikey_selection": {}, + "s_yk_information": "Informations de la YubiKey", + "s_select_yk": "Sélectionner une YubiKey", + "s_select_to_scan": "Sélectionnez pour scanner", + "s_hide_device": "Cacher l'appareil", + "s_show_hidden_devices": "Afficher les appareils cachés", + "s_sn_serial": "S/N: {serial}", + "@s_sn_serial" : { + "placeholders": { + "serial": {} + } + }, + "s_fw_version": "F/W: {version}", + "@s_fw_version" : { + "placeholders": { + "version": {} + } + }, + + "@_yubikey_interactions": {}, + "l_insert_yk": "Insérez votre YubiKey", + "l_insert_or_tap_yk": "Insérez ou touchez votre YubiKey", + "l_unplug_yk": "Déconnectez votre YubiKey", + "l_reinsert_yk": "Réinsérez votre YubiKey", + "l_place_on_nfc_reader": "Placez votre YubiKey sur le lecteur NFC", + "l_replace_yk_on_reader": "Placez votre YubiKey à nouveau sur le lecteur", + "l_remove_yk_from_reader": "Retirez votre YubiKey du lecteur NFC", + "p_try_reinsert_yk": "Essayez de réinsérer votre YubiKey.", + "s_touch_required": "Touché requis", + "l_touch_button_now": "Touchez votre YubiKey maintenant", + "l_keep_touching_yk": "Continuez de toucher votre YubiKey\u2026", + + "@_app_configuration": {}, + "s_toggle_applications": "Changer les applications", + "l_min_one_interface": "Au moins une interface doit être activée", + "s_reconfiguring_yk": "Reconfiguration de la YubiKey\u2026", + "s_config_updated": "Configuration mise à jour", + "l_config_updated_reinsert": "Configuration mise à jour; retirez et réinsérez votre YubiKey", + "s_app_not_supported": "Application non supportée", + "l_app_not_supported_on_yk": "La YubiKey utilisée ne supporte pas l'application '{app}'", + "@l_app_not_supported_on_yk" : { + "placeholders": { + "app": {} + } + }, + "l_app_not_supported_desc": "Cette application n'est pas supportée", + "s_app_disabled": "Application désactivée", + "l_app_disabled_desc": "Activez l'application '{app}' sur votre YubiKey", + "@l_app_disabled_desc" : { + "placeholders": { + "app": {} + } + }, + "s_fido_disabled": "FIDO2 désactivé", + "l_webauthn_req_fido2": "WebAuthn demande que le FIDO2 soit activé sur votre YubiKey", + + "@_connectivity_issues": {}, + "l_helper_not_responding": "Le processus Helper ne réponds pas", + "l_yk_no_access": "Cette YubiKey Ne peut pas être accédée", + "s_yk_inaccessible": "Appareil inaccessible", + "l_open_connection_failed": "Échec d'ouverture de connection", + "l_ccid_connection_failed": "Échec d'ouverture de connection smart card", + "p_ccid_service_unavailable": "Assurez vous que votre service smart card fonctionne.", + "p_pcscd_unavailable": "Assurez vous que pcscd est installé et opérationnel.", + "l_no_yk_present": "Aucune YubiKey détectée", + "s_unknown_type": "Type inconnu", + "s_unknown_device": "Appareil non reconnu", + "s_unsupported_yk": "YubiKey non supportée", + "s_yk_not_recognized": "Appareil non reconnu", + + "@_general_errors": {}, + "l_error_occured": "Une erreur est survenue", + "s_application_error": "Erreur d'application", + "l_import_error": "Erreur d'importation", + "l_file_not_found": "Fichier non trouvé", + "l_file_too_big": "Fichier trop volumineux", + "l_filesystem_error": "Erreur d'accès au système de fichier", + + "@_pins": {}, + "s_pin": "PIN", + "s_puk": "PUK", + "s_set_pin": "Entrez un PIN", + "s_change_pin": "Changez PIN", + "s_change_puk": "Changez PUK", + "s_current_pin": "PIN actuel", + "s_current_puk": "PUK actuel", + "s_new_pin": "Nouveau PIN", + "s_new_puk": "Nouveau PUK", + "s_confirm_pin": "Confirmez le PIN", + "s_confirm_puk": "Confirmez le PUK", + "s_unblock_pin": "Débloquer le PIN", + "l_new_pin_len": "Le nouveau PIN doit avoir au moins {length} caractères", + "@l_new_pin_len" : { + "placeholders": { + "length": {} + } + }, + "s_pin_set": "PIN défini", + "s_puk_set": "PUK défini", + "l_set_pin_failed": "Échec du changement de PIN: {message}", + "@l_set_pin_failed" : { + "placeholders": { + "message": {} + } + }, + "l_attempts_remaining": "Nombre de tentative(s) restante(s) : {retries}", + "@l_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, + "l_wrong_pin_attempts_remaining": "Mauvais PIN, {retries} tentative(s) restante(s)", + "@l_wrong_pin_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, + "l_wrong_puk_attempts_remaining": "Mauvais PUK, {retries} tentative(s) restante(s)", + "@l_wrong_puk_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, + "s_fido_pin_protection": "PIN de protection FIDO", + "l_fido_pin_protection_optional": "PIN de protection optionnel FIDO", + "l_enter_fido2_pin": "Entrez le PIN FIDO2 de votre YubiKey", + "l_optionally_set_a_pin": "(Optionnel) Entrez un PIN pour protéger l'accès de votre YubiKey\nEnregistrer en tant que clé de sécurité sur les sites web", + "l_pin_blocked_reset": "PIN bloqué; Réinitialisez à l'état d'usine le FIDO", + "l_set_pin_first": "Un PIN est d'abord requis", + "l_unlock_pin_first": "Débloquez avec un PIN d'abord", + "l_pin_soft_locked": "Le PIN est bloqué tant que votre YubiKey ne sera pas réinsérée", + "p_enter_current_pin_or_reset": "Entrez votre PIN. Si vous ne savez pas votre PIN, vous devez déverrouiller votre YubiKey avec un code PUK ou la réinitialiser.", + "p_enter_current_puk_or_reset": "Entrez votre PUK. Si vous ne savez pas votre PUK, vous devez déverrouiller réinitialiser votre YubiKey.", + "p_enter_new_fido2_pin": "Entrez votre nouveau PIN. Un code PIN doit avoir au moins {length} caractères et peut contenir des lettres, nombre et caractères spéciaux.", + "@p_enter_new_fido2_pin" : { + "placeholders": { + "length": {} + } + }, + "s_pin_required": "PIN requis", + "p_pin_required_desc": "L'action que vous allez faire demande d'entrer le code PIN du PIV.", + "l_piv_pin_blocked": "Vous êtes bloqué, utilisez le code PUK pour réinitialiser", + "l_piv_pin_puk_blocked": "Bloqué, réinitialisation nécesssaire", + "p_enter_new_piv_pin_puk": "Entrez un nouveau {name} entre 6 et 8 caractères.", + "@p_enter_new_piv_pin_puk" : { + "placeholders": { + "name": {} + } + }, + + "@_passwords": {}, + "s_password": "Mot de passe", + "s_manage_password": "Gérer les mots de passes", + "s_set_password": "Définir le mot de passe", + "s_password_set": "Mot de passe défini", + "l_optional_password_protection": "Mot de passe optionnel de protection", + "s_new_password": "Nouveau mot de passe", + "s_current_password": "Mot de passe actuel", + "s_confirm_password": "Confirmez le mot de passe", + "s_wrong_password": "Mauvais mot de passe", + "s_remove_password": "Supprimer le mot de passe", + "s_password_removed": "Mot de passe supprimé", + "s_remember_password": "Retenir le mot de passe", + "s_clear_saved_password": "Effacer le mot de passe", + "s_password_forgotten": "Mot de passe oublié", + "l_keystore_unavailable": "OS Keystore indisponible", + "l_remember_pw_failed": "Échec de la mémorisation du mot de passe", + "l_unlock_first": "Déverrouillez initialement avec un mot de passe", + "l_enter_oath_pw": "Entrez le mot de passe OATH de votre YubiKey", + "p_enter_current_password_or_reset": "Entrez votre mot de passe actuel. Si vous ne vous souvenez plus de votre mot de passe, vous devrez réinitialiser votre YubiKey.", + "p_enter_new_password": "Entrez votre nouveau mot de passe. Un mot de passe peut contenir des lettres, nombres et des caractères spéciaux.", + + "@_management_key": {}, + "s_management_key": "Gestion des clés", + "s_current_management_key": "Clé actuelle de gestion", + "s_new_management_key": "Nouvelle clé de gestion", + "l_change_management_key": "Changer la clé de gestion", + "p_change_management_key_desc": "Changer votre clé de gestion. Vous pouvez optionnellement autoriser le PIN à être utilisé à la place de la clé de gestion.", + "l_management_key_changed": "Ché de gestion changée", + "l_default_key_used": "Clé de gestion par défaut utilisée", + "s_generate_random": "Génération aléatoire", + "s_use_default": "Utiliser la valeur par défaut", + "l_warning_default_key": "Attention: Clé par défaut utilisée", + "s_protect_key": "Protection par PIN", + "l_pin_protected_key": "Un PIN peut être utilisé à la place", + "l_wrong_key": "Mauvaise clé", + "l_unlock_piv_management": "Déverrouiller la gestion PIV", + "p_unlock_piv_management_desc": "L'action que vous allez réaliser demande la clé de gestion PIV. Donner cette clé déverrouillera cette fonctionnalité pour cette session.", + + "@_oath_accounts": {}, + "l_account": "Compte: {label}", + "@l_account" : { + "placeholders": { + "label": {} + } + }, + "s_accounts": "Comptes", + "s_no_accounts": "Aucun compte", + "s_add_account": "Ajouter un compte", + "s_add_accounts" : "Ajouter un/des compte(s)", + "p_add_description" : "Pour scanner un code QR, assurez-vous que le code complet est visible à l'écran et appuyez sur le bouton ci-dessous. Vous pouvez également faire glisser une image enregistrée dans un dossier vers cette boîte de dialogue. Si vous disposez des informations d'identification du compte par écrit, utilisez plutôt la saisie manuelle.", + "s_add_manually" : "Ajouter manuellement", + "s_account_added": "Compte ajouté", + "l_account_add_failed": "Échec d'ajout d'un compte: {message}", + "@l_account_add_failed" : { + "placeholders": { + "message": {} + } + }, + "l_account_name_required": "Votre compte doit avoir un nom", + "l_name_already_exists": "Ce nom existe déjà pour cet émetteur", + "l_account_already_exists": "Ce compte existe déjà sur la YubiKey", + "l_invalid_character_issuer": "Caractère invalide: ':' n'est pas autorisé pour un émetteur", + "l_select_accounts" : "Sélectionner le/les compte(s) à ajouter à la YubiKey", + "s_pinned": "Épinglé", + "s_pin_account": "Épingler un compte", + "s_unpin_account": "Désépingler un compte", + "s_no_pinned_accounts": "Aucun compte épinglé", + "l_pin_account_desc": "Gardez vos comptes importants ensembles", + "s_rename_account": "Renommer le compte", + "l_rename_account_desc": "Éditer l'émetteur / le nom du compte", + "s_account_renamed": "Compte renommé", + "p_rename_will_change_account_displayed": "Cette action changera la façon dont le compte est affiché dans la liste.", + "s_delete_account": "Supprimer le compte", + "l_delete_account_desc": "Supprimer le compte de votre YubiKey", + "s_account_deleted": "Compte supprimé", + "p_warning_delete_account": "Attention! Cette action supprimera le compte de votre YubiKey.", + "p_warning_disable_credential": "Vous ne pourrez plus générer de code OTPs pour ce compte. Assurez vous de désactiver les identifiants des sites pour ne pas être verrouillé hors de vos comptes.", + "s_account_name": "Nom du compte", + "s_search_accounts": "Rechercher des comptes", + "l_accounts_used": "{used} sur {capacity} comptes utilisés", + "@l_accounts_used" : { + "placeholders": { + "used": {}, + "capacity": {} + } + }, + "s_num_digits": "{num} chiffres", + "@s_num_digits" : { + "placeholders": { + "num": {} + } + }, + "s_num_sec": "{num} secondes", + "@s_num_sec" : { + "placeholders": { + "num": {} + } + }, + "s_issuer_optional": "Émetteur (optionnel)", + "s_counter_based": "Basé sur un décompte", + "s_time_based": "Basé sur le temps", + "l_copy_code_desc": "Coller facilement le code depuis une autre application", + "s_calculate_code": "Calculer un code", + "l_calculate_code_desc": "Récupérer un nouveau code depuis votre YubiKey", + + "@_fido_credentials": {}, + "l_passkey": "Passkey: {label}", + "@l_passkey" : { + "placeholders": { + "label": {} + } + }, + "s_passkeys": "Passkeys", + "l_ready_to_use": "Prêt à l'emploi", + "l_register_sk_on_websites": "Enregistrer comme clé de sécurité sur les sites internet", + "l_no_discoverable_accounts": "Aucune Passkey détectée", + "s_delete_passkey": "Supprimer une Passkey", + "l_delete_passkey_desc": "Supprimer la Passkey de votre YubiKey", + "s_passkey_deleted": "Passkey supprimée", + "p_warning_delete_passkey": "Cette action supprimera cette Passkey de votre YubiKey.", + + "@_fingerprints": {}, + "l_fingerprint": "Empreinte: {label}", + "@l_fingerprint" : { + "placeholders": { + "label": {} + } + }, + "s_fingerprints": "Empreintes", + "l_fingerprint_captured": "Empreinte capturée avec succès!", + "s_fingerprint_added": "Empreinte ajoutée", + "l_setting_name_failed": "Erreur lors de l'ajout du nom: {message}", + "@l_setting_name_failed" : { + "placeholders": { + "message": {} + } + }, + "s_add_fingerprint": "Ajouter une empreinte", + "l_fp_step_1_capture": "Étape 1/2: Entrez votre empreinte", + "l_fp_step_2_name": "Étape 2/2: Nommez votre empreinte", + "s_delete_fingerprint": "Supprimer l'empreinte", + "l_delete_fingerprint_desc": "Supprimer l'empreinte de votre YubiKey", + "s_fingerprint_deleted": "Empreinte supprimée", + "p_warning_delete_fingerprint": "Cette action supprimera cette empreinte de votre YubiKey.", + "s_no_fingerprints": "Aucune empreinte", + "l_set_pin_fingerprints": "Ajoutez un PIN pour enregistrer des empreintes", + "l_no_fps_added": "Aucune empreinte n'a été ajoutée", + "s_rename_fp": "Renommer une empreinte", + "l_rename_fp_desc": "Changer la description", + "s_fingerprint_renamed": "Empreinte renommée", + "l_rename_fp_failed": "Erreur lors du renommage: {message}", + "@l_rename_fp_failed" : { + "placeholders": { + "message": {} + } + }, + "l_add_one_or_more_fps": "Ajouter une ou plusieurs (jusqu'a 5) empreintes", + "l_fingerprints_used": "{used}/5 empreintes enregistrées", + "@l_fingerprints_used": { + "placeholders": { + "used": {} + } + }, + "p_press_fingerprint_begin": "Posez votre doigt sur votre YubiKey pour commencer.", + "p_will_change_label_fp": "Cette action changera la description de votre empreinte.", + + "@_certificates": {}, + "s_certificate": "Certificat", + "s_certificates": "Certificats", + "s_csr": "CSR", + "s_subject": "Sujet", + "l_export_csr_file": "Sauvegarder le CSR vers un fichier", + "l_select_import_file": "Sélectionnez un fichier à importer", + "l_export_certificate": "Exporter le certificat", + "l_export_certificate_file": "Exporter le certificat vers un fichier", + "l_export_certificate_desc": "Exporter le certificat vers un fichier", + "l_certificate_exported": "Certificat exporté", + "l_import_file": "Importer un fichier", + "l_import_desc": "Importer une clé et/ou un certificat", + "l_importing_file": "Importation d'un fichier\u2026", + "s_file_imported": "Fichier importé", + "l_delete_certificate": "Supprimer un certificat", + "l_delete_certificate_desc": "Supprimer un certificat de votre YubiKey", + "s_issuer": "Émetteur", + "s_serial": "Série", + "s_certificate_fingerprint": "Empreinte digitale", + "s_valid_from": "Valide à partir de", + "s_valid_to": "Valide jusqu'à", + "l_no_certificate": "Aucun certificat chargé", + "l_key_no_certificate": "Clé sans certificat chargé", + "s_generate_key": "Générer une clé", + "l_generate_desc": "Générer un nouveau certificat ou CSR", + "p_generate_desc": "Cette action génèrera une nouvelle clé sur l'emplacement PIV {slot} de votre YubiKey. La clé publique sera incorporée dans un certificat auto-signé stocké sur votre YubiKey, ou dans un fichier CSR (Certificate Signing Request).", + "@p_generate_desc" : { + "placeholders": { + "slot": {} + } + }, + "l_generating_private_key": "Génération d'une clé privée\u2026", + "s_private_key_generated": "Clé privée générée", + "p_warning_delete_certificate": "Attention! Cette action supprimera le certificat de votre YubiKey.", + "q_delete_certificate_confirm": "Supprimer le certficat du slot PIV {slot}?", + "@q_delete_certificate_confirm" : { + "placeholders": { + "slot": {} + } + }, + "l_certificate_deleted": "Certificat supprimé", + "p_password_protected_file": "Le fichier sélectionné est protégé par un mot de passe. Enterez le mot de passe pour continuer.", + "p_import_items_desc": "Les éléments suivants seront importés dans le slot PIV {slot}.", + "@p_import_items_desc" : { + "placeholders": { + "slot": {} + } + }, + "p_subject_desc": "Un nom distinctif (DN) formaté conformément à la spécification RFC 4514.", + "l_rfc4514_invalid": "Format RFC 4514 invalide", + "rfc4514_examples": "Exemples:\nCN=Example Name\nCN=jsmith,DC=example,DC=net", + "p_cert_options_desc": "Algorithme de clé à utiliser, format de sortie et date d'expiration (certificat uniquement).", + "s_overwrite_slot": "Écraser l'emplacement", + "p_overwrite_slot_desc": "Cette opération écrase de manière permanente le contenu existant dans le slot {slot}.", + "@p_overwrite_slot_desc" : { + "placeholders": { + "slot": {} + } + }, + "l_overwrite_cert": "Le certificat sera écrasé", + "l_overwrite_key": "La clé privée sera écrasée", + "l_overwrite_key_maybe": "Toute clé privée existante dans le slot sera écrasée", + + "@_piv_slots": {}, + "s_slot_display_name": "{name} ({hexid})", + "@s_slot_display_name" : { + "placeholders": { + "name": {}, + "hexid": {} + } + }, + "s_slot_9a": "Authentification", + "s_slot_9c": "Signature digitale", + "s_slot_9d": "Gestion des clés", + "s_slot_9e": "Authentification par carte", + + "@_permissions": {}, + "s_enable_nfc": "Activer le NFC", + "s_permission_denied": "Permission refusée", + "l_elevating_permissions": "Élevation des permissions\u2026", + "s_review_permissions": "Révision des permissions", + "p_elevated_permissions_required": "Gérer cet appareil demande des privilèges plus élevés.", + "p_webauthn_elevated_permissions_required": "La gestion WebAuthn demande des privilèges plus élevés.", + "p_need_camera_permission": "Yubico Authenticator a besoin des permission d'utiliser la caméra pour scanner les QR code.", + + "@_qr_codes": {}, + "s_qr_scan": "Scanner un QR code", + "l_qr_scanned": "QR code scanné", + "l_invalid_qr": "QR code invalide", + "l_qr_not_found": "Aucun QR code trouvé", + "l_qr_not_read": "Erreur de lecture du QR code: {message}", + "@l_qr_not_read" : { + "placeholders": { + "message": {} + } + }, + "l_point_camera_scan": "Dirigez votre caméra vers le QR code pour le scanner", + "q_want_to_scan": "Voulez vous scanner un code?", + "q_no_qr": "Pas de QR code?", + "s_enter_manually": "Entrer un code manuellement", + + "@_factory_reset": {}, + "s_reset": "Réinitialiser", + "s_factory_reset": "Réinitialisation", + "l_factory_reset_this_app": "Réinitialiser cette application", + "s_reset_oath": "Réinitialiser l'OATH", + "l_oath_application_reset": "L'application OATH à été réinitialisée", + "s_reset_fido": "Réinitialiser le FIDO", + "l_fido_app_reset": "L'application FIDO à été réinitialisée", + "l_press_reset_to_begin": "Appuyez sur réinitialiser pour commencer\u2026", + "l_reset_failed": "Erreur pendant la réinitialisation: {message}", + "@l_reset_failed" : { + "placeholders": { + "message": {} + } + }, + "s_reset_piv": "Réinitialiser le PIV", + "l_piv_app_reset": "L'application PIV à été réinitialisée", + "p_warning_factory_reset": "Attention! Cette action supprimera de manière irrévocable tous les comptes OATH TOTP/HOTP de votre YubiKey.", + "p_warning_disable_credentials": "Vos identifiants OATH, ainsi que vos mots de passes, seront supprimés de votre YubiKey. Assurez vous de désactiver les identifiants des sites pour ne pas être verrouillé hors de vos comptes.", + "p_warning_deletes_accounts": "Attention! Cette action supprimera de manière irrévocable tous les comptes U2F et FIDO2 de votre YubiKey.", + "p_warning_disable_accounts": "Vos identifiants, ainsi que les codes PIN associés, seront supprimés de votre YubiKey. Assurez vous de désactiver les identifiants des sites pour ne pas être verrouillé hors de vos comptes.", + "p_warning_piv_reset": "Attention! Cette action supprimera de manière irrévocable toutes les données PIV stockées sur votre YubiKey.", + "p_warning_piv_reset_desc": "Cela inclus les clé privées et les certificats. Votre PIN, PUK, clé de management seront réinitialisés à leur valeurs d'usine.", + + "@_copy_to_clipboard": {}, + "l_copy_to_clipboard": "Copier vers le presse papier", + "s_code_copied": "Code copié", + "l_code_copied_clipboard": "Code copé vers le presse papier", + "s_copy_log": "Copier les logs", + "l_log_copied": "Logs copiés vers le presse papier", + "l_diagnostics_copied": "Données de diagnostique copiés vers le presse papier", + "p_target_copied_clipboard": "{label} copié vers le presse papier.", + "@p_target_copied_clipboard" : { + "placeholders": { + "label": {} + } + }, + + "@_custom_icons": {}, + "s_custom_icons": "Icônes personnalisées", + "l_set_icons_for_accounts": "Sélectionner les icônes pour les comptes", + "p_custom_icons_description": "Les packs d'icônes peuvent rendre vos comptes plus facilement repérables grâce à des logos et couleurs familières.", + "s_replace_icon_pack": "Remplacer le pack d'icônes", + "l_loading_icon_pack": "Chargement du pack d'icônes\u2026", + "s_load_icon_pack": "Charger le pack d'icônes", + "s_remove_icon_pack": "Supprimer le pack d'icônes", + "l_icon_pack_removed": "Pack d'icônes supprimé", + "l_remove_icon_pack_failed": "Erreur lors de la suppression du pack d'icônes", + "s_choose_icon_pack": "Choisissez votre pack d'icônes", + "l_icon_pack_imported": "Pack d'icônes importé", + "l_import_icon_pack_failed": "Erreur lors de l'importation du pack d'icônes: {message}", + "@l_import_icon_pack_failed": { + "placeholders": { + "message": {} + } + }, + "l_invalid_icon_pack": "Pack d'icônes invalide", + "l_icon_pack_copy_failed": "Échec de la copie des fichiers du pack d'icônes", + + "@_android_settings": {}, + "s_nfc_options": "Options NFC", + "l_on_yk_nfc_tap": "Lorsque le NFC de la YubiKey est en contact", + "l_launch_ya": "Démarrer Yubico Authenticator", + "l_copy_otp_clipboard": "Copier le code OTP vers le presse papier", + "l_launch_and_copy_otp": "Démarrer l'application et copier le code OTP", + "l_kbd_layout_for_static": "Arrangement clavier (pour les mot de passes statiques)", + "s_choose_kbd_layout": "Choisissez l'arrangement clavier", + "l_bypass_touch_requirement": "Contourner la nécessité de toucher la YubiKey", + "l_bypass_touch_requirement_on": "Les comptes nécessitant un contact sont automatiquement affichés via NFC", + "l_bypass_touch_requirement_off": "Les comptes nécessitant un contact sur la YubiKey doivent faire l'objet d'un contact NFC supplémentaire", + "s_silence_nfc_sounds": "Couper le son NFC", + "l_silence_nfc_sounds_on": "Aucun son ne sera joué lors du contact NFC", + "l_silence_nfc_sounds_off": "Du son sera joué lors du contact NFC", + "s_usb_options": "Options USB", + "l_launch_app_on_usb": "Lancer lorsque la YubiKey est connectée", + "l_launch_app_on_usb_on": "Cela empêchera que d'autres applications utilisent la YubiKey via USB", + "l_launch_app_on_usb_off": "D'autres applications peuvent utiliser la YubiKey via USB", + "s_allow_screenshots": "Autoriser les captures d'écrans", + + "s_nfc_dialog_tap_key": "Effleurez votre YubiKey", + "s_nfc_dialog_operation_success": "Succès", + "s_nfc_dialog_operation_failed": "Échec", + + "s_nfc_dialog_oath_reset": "Action: réinitialiser l'applet OATH", + "s_nfc_dialog_oath_unlock": "Action: déverouiller l'applet OATH", + "s_nfc_dialog_oath_set_password": "Action: définir le mot de passe OATH", + "s_nfc_dialog_oath_unset_password": "Action: supprimer le mot de passe OATH", + "s_nfc_dialog_oath_add_account": "Action: ajouter un nouveau compte", + "s_nfc_dialog_oath_rename_account": "Action: renommer le compte", + "s_nfc_dialog_oath_delete_account": "Action: supprimer le compte", + "s_nfc_dialog_oath_calculate_code": "Action: calculer le code OATH", + "s_nfc_dialog_oath_failure": "Échec de l'opération OATH", + "s_nfc_dialog_oath_add_multiple_accounts": "Action: ajouter plusieurs comptes", + + "@_eof": {} +} \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb new file mode 100644 index 00000000..967772d8 --- /dev/null +++ b/lib/l10n/app_ja.arb @@ -0,0 +1,615 @@ +{ + "@@locale": "ja", + + "@_readme": { + "notes": [ + "All strings start with a Captial letter.", + "Group strings by category, but don't needlessly tie them to a section of the app if they can be re-used between several.", + "Run check_strings.py on the .arb file to detect problems, tweak @_lint_rules as needed per language." + ], + "prefixes": { + "s_": "A single, or few words. Should be short enough to display on a button, or a header.", + "l_": "A single line, can be wrapped. Should not be more than one sentence, and not end with a period.", + "p_": "One or more full sentences, with proper punctuation.", + "q_": "A question, ending in question mark." + } + }, + + "@_lint_rules": { + "s_max_words": 4, + "s_max_length": 32 + }, + + "app_name": "Yubico Authenticator", + + "s_save": "保存", + "s_cancel": "キャンセル", + "s_close": "閉じる", + "s_delete": "消去", + "s_quit": "終了", + "s_unlock": "ロック解除", + "s_calculate": "計算", + "s_import": "インポート", + "s_overwrite": "上書き", + "s_label": "ラベル", + "s_name": "名前", + "s_usb": "USB", + "s_nfc": "NFC", + "s_options": "オプション", + "s_show_window": "ウィンドウを表示", + "s_hide_window": "ウィンドウを表示しない", + "q_rename_target": "{label}の名前を変更しますか?", + "@q_rename_target" : { + "placeholders": { + "label": {} + } + }, + "l_bullet": "• {item}", + "@l_bullet" : { + "placeholders": { + "item": {} + } + }, + + "s_about": "情報", + "s_algorithm": "アルゴリズム", + "s_appearance": "外観", + "s_authenticator": "Authenticator", + "s_actions": "アクション", + "s_manage": "管理", + "s_setup": "セットアップ", + "s_settings": "設定", + "s_piv": "PIV", + "s_webauthn": "WebAuthn", + "s_help_and_about": "ヘルプと概要", + "s_help_and_feedback": "ヘルプとフィードバック", + "s_send_feedback": "フィードバックの送信", + "s_i_need_help": "ヘルプが必要", + "s_troubleshooting": "トラブルシューティング", + "s_terms_of_use": "利用規約", + "s_privacy_policy": "プライバシーポリシー", + "s_open_src_licenses": "オープンソースライセンス", + "s_configure_yk": "YubiKeyを構成する", + "s_please_wait": "お待ちください\u2026", + "s_secret_key": "秘密鍵", + "s_private_key": "秘密鍵", + "s_invalid_length": "無効な長さです", + "s_require_touch": "タッチが必要", + "q_have_account_info": "アカウント情報をお持ちですか?", + "s_run_diagnostics": "診断を実行する", + "s_log_level": "ログレベル: {level}", + "@s_log_level": { + "placeholders": { + "level": {} + } + }, + "s_character_count": "文字数", + "s_learn_more": "もっと詳しく知る", + + "@_language": {}, + "s_language": "言語", + "l_enable_community_translations": "コミュニティ翻訳を有効にする", + "p_community_translations_desc": "これらの翻訳はコミュニティによって提供および維持されます。 エラーが含まれているか不完全である可能性があります。", + + "@_theme": {}, + "s_app_theme": "アプリのテーマ", + "s_choose_app_theme": "アプリのテーマを選択", + "s_system_default": "システムのデフォルト", + "s_light_mode": "ライトモード", + "s_dark_mode": "ダークモード", + + "@_yubikey_selection": {}, + "s_yk_information": "YubiKey情報", + "s_select_yk": "YubiKeyを選択", + "s_select_to_scan": "選択してスキャン", + "s_hide_device": "デバイスを非表示", + "s_show_hidden_devices": "非表示のデバイスを表示", + "s_sn_serial": "S/N: {serial}", + "@s_sn_serial" : { + "placeholders": { + "serial": {} + } + }, + "s_fw_version": "F/W: {version}", + "@s_fw_version" : { + "placeholders": { + "version": {} + } + }, + + "@_yubikey_interactions": {}, + "l_insert_yk": "YubiKeyを挿入する", + "l_insert_or_tap_yk": "YubiKeyを挿入またはタップする", + "l_unplug_yk": "YubiKeyを取り外す", + "l_reinsert_yk": "YubiKeyを再挿入する", + "l_place_on_nfc_reader": "YubiKeyをNFCリーダーに置く", + "l_replace_yk_on_reader": "YubiKeyをリーダーに戻す", + "l_remove_yk_from_reader": "YubiKeyをNFCリーダーから取り外す", + "p_try_reinsert_yk": "YubiKeyを取り外して再挿入してみてください", + "s_touch_required": "タッチが必要です", + "l_touch_button_now": "今すぐYubiKeyのボタンをタッチしてください", + "l_keep_touching_yk": "YubiKeyを繰り返しタッチし続けてください\u2026", + + "@_app_configuration": {}, + "s_toggle_applications": "アプリケーションの切替え", + "l_min_one_interface": "少なくとも 1 つのインターフェイスを有効にする必要があります", + "s_reconfiguring_yk": "YubiKeyを再構成しています\u2026", + "s_config_updated": "構成が更新されました", + "l_config_updated_reinsert": "設定が更新されました。YubiKeyを取り外して再挿入してください", + "s_app_not_supported": "アプリケーションがサポートされていません", + "l_app_not_supported_on_yk": "使用されているYubiKeyは '{app}' アプリケーションをサポートしていません", + "@l_app_not_supported_on_yk" : { + "placeholders": { + "app": {} + } + }, + "l_app_not_supported_desc": "このアプリケーションはサポートされていません", + "s_app_disabled": "アプリケーションが無効になっています", + "l_app_disabled_desc": "YubiKeyの「{app}」アプリケーションへのアクセスを許可", + "@l_app_disabled_desc" : { + "placeholders": { + "app": {} + } + }, + "s_fido_disabled": "FIDO2が無効になっています", + "l_webauthn_req_fido2": "WebAuthnでは、YubiKeyでFIDO2アプリケーションを有効にする必要があります", + + "@_connectivity_issues": {}, + "l_helper_not_responding": "ヘルパープロセスが応答していません", + "l_yk_no_access": "このYubiKeyにはアクセスできません", + "s_yk_inaccessible": "デバイスにアクセスできません", + "l_open_connection_failed": "接続を開けませんでした", + "l_ccid_connection_failed": "スマートカード接続を開けませんでした", + "p_ccid_service_unavailable": "スマートカード サービスが機能していることを確認してください", + "p_pcscd_unavailable": "pcscdがインストールされ、実行されていることを確認してください", + "l_no_yk_present": "YubiKeyが存在しません", + "s_unknown_type": "不明なタイプ", + "s_unknown_device": "認識されないデバイス", + "s_unsupported_yk": "サポートされていないYubiKey", + "s_yk_not_recognized": "デバイスが認識されない", + + "@_general_errors": {}, + "l_error_occured": "エラーが発生しました", + "s_application_error": "アプリケーションエラー", + "l_import_error": "インポートエラー", + "l_file_not_found": "ファイルが見つかりません", + "l_file_too_big": "ファイルサイズが大きすぎます", + "l_filesystem_error": "ファイルシステム操作エラー", + + "@_pins": {}, + "s_pin": "PIN", + "s_puk": "PUK", + "s_set_pin": "PINを設定する", + "s_change_pin": "PINを変更する", + "s_change_puk": "PUKを変更する", + "s_current_pin": "現在のPIN", + "s_current_puk": "現在のPUK", + "s_new_pin": "新しいPIN", + "s_new_puk": "新しいPUK", + "s_confirm_pin": "PINの確認", + "s_confirm_puk": "PUKの確認", + "s_unblock_pin": "ブロックを解除", + "l_new_pin_len": "新しいPINは少なくとも{length}文字である必要があります", + "@l_new_pin_len" : { + "placeholders": { + "length": {} + } + }, + "s_pin_set": "PINの設定", + "s_puk_set": "PUKの設定", + "l_set_pin_failed": "PIN設定に失敗しました:{message}", + "@l_set_pin_failed" : { + "placeholders": { + "message": {} + } + }, + "l_attempts_remaining": "あと{retries}回試行できます", + "@l_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, + "l_wrong_pin_attempts_remaining": "PINが間違っています。あと{retries}回試行できます", + "@l_wrong_pin_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, + "l_wrong_puk_attempts_remaining": "PUKが間違っています。あと{retries}回試行できます", + "@l_wrong_puk_attempts_remaining" : { + "placeholders": { + "retries": {} + } + }, + "s_fido_pin_protection": "FIDO PINによる保護", + "l_fido_pin_protection_optional": "任意FIDO PINによる保護", + "l_enter_fido2_pin": "YubiKeyのFIDO2 PINを入力してください", + "l_optionally_set_a_pin": "YubiKeyアクセスを保護するために、任意でPINの設定ができます\nWebサイトにはセキュリティ キーとして登録されます", + "l_pin_blocked_reset": "PINはブロックされています。FIDOアプリケーションを出荷時設定にリセットしてください", + "l_set_pin_first": "最初にPINが必要です", + "l_unlock_pin_first": "最初にPINでロックを解除してください", + "l_pin_soft_locked": "YubiKeyを取り外して再挿入するまで、PINはブロックされています", + "p_enter_current_pin_or_reset": "現在のPINを入力してください。PIN がわからない場合は、PUK でブロック解除するか、YubiKey をリセットする必要があります", + "p_enter_current_puk_or_reset": "現在のPUKを入力してください。 PUK がわからない場合は、YubiKeyをリセットする必要があります", + "p_enter_new_fido2_pin": "新しいPINを入力してください。 PINは少なくとも{length}文字の長さである必要があり、文字、数字、特殊文字を含めることができます", + "@p_enter_new_fido2_pin" : { + "placeholders": { + "length": {} + } + }, + "s_pin_required": "PINが必要", + "p_pin_required_desc": "実行しようとしている操作には、PIV PINの入力が必要です", + "l_piv_pin_blocked": "ブロックされています。PUK を使用してリセットしてください", + "l_piv_pin_puk_blocked": "ブロックされています。工場出荷リセットしてください", + "p_enter_new_piv_pin_puk": "新{name}を入力してください。6 ~ 8 文字でなければな りま せん。", + "@p_enter_new_piv_pin_puk" : { + "placeholders": { + "name": {} + } + }, + + "@_passwords": {}, + "s_password": "パスワード", + "s_manage_password": "パスワードの管理", + "s_set_password": "パスワードを設定", + "s_password_set": "パスワード設定", + "l_optional_password_protection": "任意パスワードによる保護", + "s_new_password": "新しいパスワード", + "s_current_password": "現在のパスワード", + "s_confirm_password": "パスワードを確認", + "s_wrong_password": "間違ったパスワード", + "s_remove_password": "パスワードの削除", + "s_password_removed": "パスワードが削除されました", + "s_remember_password": "パスワードを覚える", + "s_clear_saved_password": "保存されたパスワードを削除する", + "s_password_forgotten": "パスワードを忘れた場合", + "l_keystore_unavailable": "OSキーストアは使用できません", + "l_remember_pw_failed": "パスワードを忘れました", + "l_unlock_first": "最初にパスワードでロックを解除します", + "l_enter_oath_pw": "YubiKeyのOATHパスワードを入力してください", + "p_enter_current_password_or_reset": "現在のパスワードを入力してください。パスワードがわからない場合は、YubiKeyをリセットする必要があります", + "p_enter_new_password": "新しいパスワードを入力してください。 パスワードには文字、数字、特殊文字を含めることができます", + + "@_management_key": {}, + "s_management_key": "Management key", + "s_current_management_key": "現在のManagement key", + "s_new_management_key": "新しいManagement key", + "l_change_management_key": "Management keyの変更", + "p_change_management_key_desc": "Management keyを変更してください。Management keyの代わりにPINを使用することも可能です", + "l_management_key_changed": "Management keyは変更されました", + "l_default_key_used": "デフォルトManagement keyが使用されています", + "s_generate_random": "ランダムに生成する", + "s_use_default": "デフォルトの使用", + "l_warning_default_key": "警告: デフォルトのキーが使用されています", + "s_protect_key": "PINで保護する", + "l_pin_protected_key": "代わりにPINを使用できます", + "l_wrong_key": "間違ったキー", + "l_unlock_piv_management": "PIV管理のロックの解除", + "p_unlock_piv_management_desc": "実行しようとしている操作にはPIVのManagement keyが必要です。このセッションの管理機能のロックを解除するために、キーを入力してください", + + "@_oath_accounts": {}, + "l_account": "アカウント:{label}", + "@l_account" : { + "placeholders": { + "label": {} + } + }, + "s_accounts": "アカウント", + "s_no_accounts": "アカウントがありません", + "s_add_account": "アカウントの追加", + "s_add_accounts" : "アカウントの追加", + "p_add_description" : "QR コードをスキャンするには、コード全体が画面に表示されていることを確認し、下のボタンを押してください。保存した画像をこのダイアログにドラッグすることもできます。アカウントクレデンシャル情報を書面で持っている場合は、代わりに手動で入力をしてください。", + "s_add_manually" : "手動で追加", + "s_account_added": "アカウントが追加されました", + "l_account_add_failed": "アカウントの追加に失敗しました:{message}", + "@l_account_add_failed" : { + "placeholders": { + "message": {} + } + }, + "l_account_name_required": "アカウントには名前が必要です", + "l_name_already_exists": "発行者名は既に使われています", + "l_account_already_exists": "このアカウントはすでに YubiKey に存在します", + "l_invalid_character_issuer": "無効な文字: ':' は発行者名で使用できません", + "l_select_accounts" : "YubiKey に追加するアカウントを選択してください", + "s_pinned": "固定", + "s_pin_account": "アカウントを固定する", + "s_unpin_account": "アカウントの固定を解除する", + "s_no_pinned_accounts": "固定されたアカウントはありません", + "l_pin_account_desc": "重要なアカウントは一緒にまとめてください", + "s_rename_account": "アカウント名の変更", + "l_rename_account_desc": "アカウントの発行者/名前の編集", + "s_account_renamed": "アカウント名が変更されました", + "p_rename_will_change_account_displayed": "これにより、リスト内でのアカウントの表示方法が変わります。", + "s_delete_account": "アカウントを削除する", + "l_delete_account_desc": "YubiKeyからアカウントの削除", + "s_account_deleted": "アカウントが削除されました", + "p_warning_delete_account": "警告!この操作によってYubiKeyからアカウントが削除されます", + "p_warning_disable_credential": "このアカウントのOTPを生成できなくなります。 アカウントからロックアウトされないように、必ず最初にWebサイトからこのクレデンシャルを無効化してください", + "s_account_name": "アカウント名", + "s_search_accounts": "アカウントを検索", + "l_accounts_used": "{capacity}個のアカウントのうち{used}個が使用されています", + "@l_accounts_used" : { + "placeholders": { + "used": {}, + "capacity": {} + } + }, + "s_num_digits": "{num}桁", + "@s_num_digits" : { + "placeholders": { + "num": {} + } + }, + "s_num_sec": "{num}秒", + "@s_num_sec" : { + "placeholders": { + "num": {} + } + }, + "s_issuer_optional": "発行者(任意)", + "s_counter_based": "カウンターベース", + "s_time_based": "時間ベース", + "l_copy_code_desc": "コードを別のアプリに貼り付ける", + "s_calculate_code": " コードの計算", + "l_calculate_code_desc": "YubiKey から新しいコードの取得", + + "@_fido_credentials": {}, + "l_passkey": "パスキー: {label}", + "@l_passkey" : { + "placeholders": { + "label": {} + } + }, + "s_passkeys": "パスキー", + "l_ready_to_use": "すぐに使用可能", + "l_register_sk_on_websites": "Webサイトにセキュリティキーとして登録する", + "l_no_discoverable_accounts": "パスキーは保存されていません", + "s_delete_passkey": "パスキーを削除", + "l_delete_passkey_desc": "YubiKeyからパスキーの削除", + "s_passkey_deleted": "パスキーが削除されました", + "p_warning_delete_passkey": "これにより、YubiKeyからパスキーが削除されます", + + "@_fingerprints": {}, + "l_fingerprint": "指紋:{label}", + "@l_fingerprint" : { + "placeholders": { + "label": {} + } + }, + "s_fingerprints": "指紋", + "l_fingerprint_captured": "指紋の取得に成功しました!", + "s_fingerprint_added": "指紋が追加されました", + "l_setting_name_failed": "名前設定時名エラー:{message}", + "@l_setting_name_failed" : { + "placeholders": { + "message": {} + } + }, + "s_add_fingerprint": "指紋を追加", + "l_fp_step_1_capture": "ステップ 1/2:指紋を取得する", + "l_fp_step_2_name": "ステップ 2/2:指紋の名前を付ける", + "s_delete_fingerprint": "指紋を削除", + "l_delete_fingerprint_desc": "YubiKeyから指紋の削除", + "s_fingerprint_deleted": "指紋が削除されました", + "p_warning_delete_fingerprint": "これによりYubiKeyから指紋が削除されます", + "s_no_fingerprints": "指紋は登録されていません", + "l_set_pin_fingerprints": "指紋登録のためにPINを設定してください", + "l_no_fps_added": "指紋は追加されていません", + "s_rename_fp": "指紋の名前を変更", + "l_rename_fp_desc": "ラベルの変更", + "s_fingerprint_renamed": "指紋の名前が変更されました", + "l_rename_fp_failed": "大友:名前変更エラー:{message}", + "@l_rename_fp_failed" : { + "placeholders": { + "message": {} + } + }, + "l_add_one_or_more_fps": "1 つ以上 (最大 5 つ) の指紋を追加します", + "l_fingerprints_used": "{used}/5つの指紋が登録されました", + "@l_fingerprints_used": { + "placeholders": { + "used": {} + } + }, + "p_press_fingerprint_begin": "YubiKeyに指を押し当てて開始します", + "p_will_change_label_fp": "これにより指紋のラベルが変更されます", + + "@_certificates": {}, + "s_certificate": "証明書", + "s_certificates": "証明書", + "s_csr": "CSR", + "s_subject": "サブジェクト", + "l_export_csr_file": "CSRをファイルに保存", + "l_select_import_file": "インポートするファイルの選択", + "l_export_certificate": "証明書をエクスポートする", + "l_export_certificate_file": "証明書をファイルにエクスポートする", + "l_export_certificate_desc": "証明書をファイルにエクスポートする", + "l_certificate_exported": "証明書がエクスポートされました", + "l_import_file": "ファイルのインポート", + "l_import_desc": "キーや証明書のインポート", + "l_importing_file": "ファイルのインポート中\u2026", + "s_file_imported": "ファイル をインポートしました", + "l_delete_certificate": "証明書を削除", + "l_delete_certificate_desc": "YubiKeyか証明書の削除", + "s_issuer": "発行者", + "s_serial": "シリアル番号", + "s_certificate_fingerprint": "指紋", + "s_valid_from": "有効期限の開始", + "s_valid_to": "有効期限の終了", + "l_no_certificate": "証明書はロードされていません", + "l_key_no_certificate": "証明書がロードされていない鍵", + "s_generate_key": "鍵の生成", + "l_generate_desc": "新しい証明書またはCSRの生成", + "p_generate_desc": "これにより、YubiKeyのPIVスロット{slot}に新しい鍵が生成されます。公開鍵は、YubiKeyに保存されている自己署名証明書、またはファイルに保存されている証明書署名要求(CSR)に埋め込まれます", + "@p_generate_desc" : { + "placeholders": { + "slot": {} + } + }, + "l_generating_private_key": "秘密鍵を生成しています\u2026", + "s_private_key_generated": "秘密鍵を生成しました", + "p_warning_delete_certificate": "警告!この操作によってYubiKeyから証明書が削除されます", + "q_delete_certificate_confirm": "PIVスロット{slot}の証明書を削除しますか?", + "@q_delete_certificate_confirm" : { + "placeholders": { + "slot": {} + } + }, + "l_certificate_deleted": "証明書が削除されました", + "p_password_protected_file": "選択したファイルはパスワードで保護されています。パスワードを入力して続行します", + "p_import_items_desc": "次のアイテムはPIVスロット{slot}にインポートされます", + "@p_import_items_desc" : { + "placeholders": { + "slot": {} + } + }, + "p_subject_desc": "RFC 4514フォーマットの識別名識別名 (DN)", + "l_rfc4514_invalid": "無効な RFC 4514 形式です", + "rfc4514_examples": "例:\nCN=Example Name\nCN=jsmith,DC=example,DC=net", + "p_cert_options_desc": "使用する鍵アルゴリズム、出力形式、および有効期限 (該当する場合)", + "s_overwrite_slot": "スロットの上書き", + "p_overwrite_slot_desc": "これにより、スロット{slot}内の既存のデータが永久に上書きされます", + "@p_overwrite_slot_desc" : { + "placeholders": { + "slot": {} + } + }, + "l_overwrite_cert": "証明書は上書きされます", + "l_overwrite_key": "秘密鍵は上書きされます", + "l_overwrite_key_maybe": "スロット内の既存の秘密鍵は上書きされます", + + "@_piv_slots": {}, + "s_slot_display_name": "{name} ({hexid})", + "@s_slot_display_name" : { + "placeholders": { + "name": {}, + "hexid": {} + } + }, + "s_slot_9a": "認証", + "s_slot_9c": "デジタル署名", + "s_slot_9d": "鍵の管理", + "s_slot_9e": "カード認証", + + "@_permissions": {}, + "s_enable_nfc": "NFCを有効にする", + "s_permission_denied": "権限がありません", + "l_elevating_permissions": "権限の昇格\u2026", + "s_review_permissions": "権限の確認", + "p_elevated_permissions_required": "このデバイスを管理するには権限の昇格が必要です", + "p_webauthn_elevated_permissions_required": "WebAuthn管理には権限の昇格が必要です", + "p_need_camera_permission": "Yubico AuthenticatorにはQRコードをスキャンするためのカメラ権限が必要です", + + "@_qr_codes": {}, + "s_qr_scan": "QRコードをスキャン", + "l_qr_scanned": "スキャンしたQRコード", + "l_invalid_qr": "無効なQRコード", + "l_qr_not_found": "QRコードが見つかりませんでした", + "l_qr_not_read": "QRコードの読み取りに失敗しました:{message}", + "@l_qr_not_read" : { + "placeholders": { + "message": {} + } + }, + "l_point_camera_scan": "カメラをQRコードに向けてスキャンする", + "q_want_to_scan": "スキャンしますか?", + "q_no_qr": "QRコードはありませんか?", + "s_enter_manually": "手動で入力", + + "@_factory_reset": {}, + "s_reset": "リセット", + "s_factory_reset": "工場出荷リセット", + "l_factory_reset_this_app": "このアプリケーションを出荷時設定にリセット", + "s_reset_oath": "OATHのリセット", + "l_oath_application_reset": "OATHアプリケーションのリセット", + "s_reset_fido": "FIDOのリセット", + "l_fido_app_reset": "FIDOアプリケーションのリセット", + "l_press_reset_to_begin": "リセットを押して開始してください\u2026", + "l_reset_failed": "リセット実行中のエラー:{message}", + "@l_reset_failed" : { + "placeholders": { + "message": {} + } + }, + "s_reset_piv": "PIVのリセット", + "l_piv_app_reset": "PIVアプリケーションのリセット", + "p_warning_factory_reset": "警告!これによりすべてのOATH TOTP/HOTPアカウントがYubiKeyから削除されて、復旧不可能となります", + "p_warning_disable_credentials": "あなたのOATHクレデンシャル情報とパスワードは、このYubiKeyから削除されます。アカウントからロックアウトされないように、まずそれぞれのWebサイトからこれらを無効化してください", + "p_warning_deletes_accounts": ":警告!これによりYubiKeyからすべてのU2FおよびFIDO2アカウントが削除されて、復旧不可能となります", + "p_warning_disable_accounts": "あなたのクレデンシャル情報とすべてのPINは、このYubiKeyから削除されます。 アカウントからロックアウトされないように、まずそれぞれのWebサイトでこれらを無効化してください", + "p_warning_piv_reset": "警告!PIVデータは、YubiKeyから削除されて、復旧不可能となります", + "p_warning_piv_reset_desc": "これには秘密鍵と証明書が含まれます。 PIN、PUK、およびManagement keyは工場出荷時のデフォルト値にリセットされます", + + "@_copy_to_clipboard": {}, + "l_copy_to_clipboard": "クリップボードにコピー", + "s_code_copied": "コードをコピーしました", + "l_code_copied_clipboard": "コードをクリップボードにコピーしました", + "s_copy_log": "ログのコピー", + "l_log_copied": "ログをクリップボードにコピーしました", + "l_diagnostics_copied": "診断データをクリップボードにコピーしました", + "p_target_copied_clipboard": "{label}をクリップボードにコピーしました", + "@p_target_copied_clipboard" : { + "placeholders": { + "label": {} + } + }, + + "@_custom_icons": {}, + "s_custom_icons": "カスタムアイコン", + "l_set_icons_for_accounts": "アカウントのアイコンの設定", + "p_custom_icons_description": "アイコンパックを使用すると、見慣れたロゴと色でアカウントをより簡単に区別できるようになります", + "s_replace_icon_pack": "アイコンパックを置き換える", + "l_loading_icon_pack": "アイコンパックをロード中\u2026", + "s_load_icon_pack": "アイコンパックのロード", + "s_remove_icon_pack": "アイコンパックの削除", + "l_icon_pack_removed": "アイコンパックが削除されました", + "l_remove_icon_pack_failed": "アイコンパックの削除中にエラーが発生しました", + "s_choose_icon_pack": "アイコンパックを選択", + "l_icon_pack_imported": "アイコンパックがインポートされました", + "l_import_icon_pack_failed": "アイコンパックのインポート中にエラーが発生しました:{message}", + "@l_import_icon_pack_failed": { + "placeholders": { + "message": {} + } + }, + "l_invalid_icon_pack": "無効なアイコンパック", + "l_icon_pack_copy_failed": "アイコンパックのコピーに失敗しました", + + "@_android_settings": {}, + "s_nfc_options": "NFCオプション", + "l_on_yk_nfc_tap": "YubiKey NFCタップ時", + "l_launch_ya": "Yubico Authenticatorを起動", + "l_copy_otp_clipboard": "OTPをクリップボードにコピー", + "l_launch_and_copy_otp": "アプリを起動してOTPをコピー", + "l_kbd_layout_for_static": "キーボードレイアウト (静的パスワード用)", + "s_choose_kbd_layout": "キーボードレイアウトの選択", + "l_bypass_touch_requirement": "タッチ要件をバイパス", + "l_bypass_touch_requirement_on": "タッチが必要なアカウントはNFC経由で自動的に表示されます", + "l_bypass_touch_requirement_off": "タッチが必要なアカウントはNFC経由でさらにタップする必要があります", + "s_silence_nfc_sounds": "NFC音をミュートする", + "l_silence_nfc_sounds_on": "NFCタップでは音は鳴りません", + "l_silence_nfc_sounds_off": "NFCタップで音が再生されます", + "s_usb_options": "USBオプション", + "l_launch_app_on_usb": "YubiKeyが接続されているときに起動", + "l_launch_app_on_usb_on": "これにより他のアプリがUSB経由でYubiKeyを使用できなくなります", + "l_launch_app_on_usb_off": "他のアプリはUSB経由でYubiKeyを使用できます", + "s_allow_screenshots": "スクリーンショットを許可する", + + "s_nfc_dialog_tap_key": "キーをタップする", + "s_nfc_dialog_operation_success": "成功", + "s_nfc_dialog_operation_failed": "失敗", + + "s_nfc_dialog_oath_reset": "操作:OATHアプレットのリセット", + "s_nfc_dialog_oath_unlock": "操作:OATHアプレットのロック解除", + "s_nfc_dialog_oath_set_password": "操作:OATHパスワードの設定", + "s_nfc_dialog_oath_unset_password": "操作:OATHパスワードの削除", + "s_nfc_dialog_oath_add_account": "操作:新アカウントの追加", + "s_nfc_dialog_oath_rename_account": "操作:アカウント名の変更", + "s_nfc_dialog_oath_delete_account": "操作:アカウントの削除", + "s_nfc_dialog_oath_calculate_code": "操作:OATHコードの計算", + "s_nfc_dialog_oath_failure": "OATH操作は失敗しました", + "s_nfc_dialog_oath_add_multiple_accounts": "操作:複数アカウントの追加", + + "@_eof": {} +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 2d31918b..bfc39741 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1,445 +1,547 @@ { "@@locale": "pl", - - "@_readme": { - "notes": [ - "Wszystkie ciągi zaczynają się od wielkiej litery.", - "Pogrupuj ciągi znaków według kategorii, ale nie łącz ich nadmiernie z daną sekcją aplikacji, jeżeli mogą być ponownie wykorzystane w kilku różnych sekcjach.", - "Uruchom check_strings.py na pliku .arb, aby wykryć problemy, dostosuj @_lint_rules zgodnie z potrzebami dla danego języka." - ], - "prefixes": { - "s_": "Jedno lub kilka słów. Powinny być na tyle krótkie, aby można je było wyświetlić na przycisku lub w nagłówku.", - "l_": "Pojedyncza linia może być zawinięta. Nie powinna być dłuższa niż jedno zdanie i nie powinna kończyć się kropką.", - "p_": "Jedno lub więcej pełnych zdań z odpowiednią interpunkcją.", - "q_": "Pytania kończą się znakiem zapytania." - } - }, - - "@_lint_rules": { - "s_max_words": 4, - "s_max_length": 32 - }, - "app_name": "Yubico Authenticator", - - "s_save": "Zapisz", - "s_cancel": "Anuluj", - "s_close": "Zamknij", - "s_delete": "Usuń", - "s_quit": "Wyjdź", - "s_unlock": "Odblokuj", - "s_calculate": "Oblicz", - "s_label": "Etykieta", - "s_name": "Nazwa", - "s_usb": "USB", - "s_nfc": "NFC", - "s_show_window": "Pokaż okno", - "s_hide_window": "Ukryj okno", - "q_rename_target": "Zmienić nazwę {label}?", - "@q_rename_target" : { + "@l_account": { "placeholders": { "label": {} } }, - - "s_about": "O aplikacji", - "s_appearance": "Wygląd", - "s_authenticator": "Authenticator", - "s_manage": "Zarządzaj", - "s_setup": "Konfiguruj", - "s_settings": "Ustawienia", - "s_webauthn": "WebAuthn", - "s_help_and_about": "Pomoc i informacje", - "s_help_and_feedback": "Pomoc i opinie", - "s_send_feedback": "Prześlij opinię", - "s_i_need_help": "Pomoc", - "s_troubleshooting": "Rozwiązywanie problemów", - "s_terms_of_use": "Warunki użytkowania", - "s_privacy_policy": "Polityka prywatności", - "s_open_src_licenses": "Licencje open source", - "s_configure_yk": "Skonfiguruj YubiKey", - "s_please_wait": "Proszę czekać\u2026", - "s_secret_key": "Tajny klucz", - "s_invalid_length": "Nieprawidłowa długość", - "s_require_touch": "Wymagaj dotknięcia", - "q_have_account_info": "Masz dane konta?", - "s_run_diagnostics": "Uruchom diagnostykę", - "s_log_level": "Poziom logowania: {level}", - "@s_log_level": { - "placeholders": { - "level": {} - } - }, - "s_character_count": "Liczba znaków", - "s_learn_more": "Dowiedz się więcej", - - "@_language": {}, - "s_language": "Język", - "l_enable_community_translations": "Włącz tłumaczenia społecznościowe", - "p_community_translations_desc": "Tłumaczenia te są dostarczane i utrzymywane przez społeczność. Mogą zawierać błędy lub być niekompletne.", - - "@_theme": {}, - "s_app_theme": "Motyw aplikacji", - "s_choose_app_theme": "Wybierz motyw aplikacji", - "s_system_default": "Zgodny z systemem", - "s_light_mode": "Jasny", - "s_dark_mode": "Ciemny", - - "@_yubikey_selection": {}, - "s_yk_information": "Informacja o YubiKey", - "s_select_yk": "Wybierz YubiKey", - "s_select_to_scan": "Wybierz, aby skanować", - "s_hide_device": "Ukryj urządzenie", - "s_show_hidden_devices": "Pokaż ukryte urządzenia", - "s_sn_serial": "S/N: {serial}", - "@s_sn_serial" : { - "placeholders": { - "serial": {} - } - }, - "s_fw_version": "F/W: {version}", - "@s_fw_version" : { - "placeholders": { - "version": {} - } - }, - - "@_yubikey_interactions": {}, - "l_insert_yk": "Podłącz klucz YubiKey", - "l_insert_or_tap_yk": "Podłącz lub przystaw YubiKey", - "l_unplug_yk": "Odłącz klucz YubiKey", - "l_reinsert_yk": "Ponownie podłącz YubiKey", - "l_place_on_nfc_reader": "Przyłóż klucz YubiKey do czytnika NFC", - "l_replace_yk_on_reader": "Umieść klucz YubiKey z powrotem na czytniku", - "l_remove_yk_from_reader": "Odsuń klucz YubiKey od czytnika NFC", - "p_try_reinsert_yk": "Spróbuj ponownie podłączyć klucz YubiKey.", - "s_touch_required": "Wymagane dotknięcie", - "l_touch_button_now": "Dotknij teraz przycisku na kluczu YubiKey", - "l_keep_touching_yk": "Wielokrotnie dotykaj klucza YubiKey\u2026", - - "@_app_configuration": {}, - "s_toggle_applications": "Przełączanie funkcji", - "l_min_one_interface": "Przynajmniej jeden interfejs musi być włączony", - "s_reconfiguring_yk": "Rekonfigurowanie YubiKey\u2026", - "s_config_updated": "Zaktualizowano konfigurację", - "l_config_updated_reinsert": "Zaktualizowano konfigurację, podłącz ponownie klucz YubiKey", - "s_app_not_supported": "Funkcja nie jest obsługiwana", - "l_app_not_supported_on_yk": "Używany klucz YubiKey nie obsługuje funkcji „{app}”", - "@l_app_not_supported_on_yk" : { - "placeholders": { - "app": {} - } - }, - "l_app_not_supported_desc": "Ta funkcja nie jest obsługiwana", - "s_app_disabled": "Wyłączona funkcja", - "l_app_disabled_desc": "Włącz funkcję „{app}” w kluczu YubiKey, aby uzyskać dostęp", - "@l_app_disabled_desc" : { - "placeholders": { - "app": {} - } - }, - "s_fido_disabled": "FIDO2 wyłączone", - "l_webauthn_req_fido2": "WebAuthn wymaga włączenia funkcji FIDO2 w kluczu YubiKey", - - "@_connectivity_issues": {}, - "l_helper_not_responding": "Proces pomocnika nie odpowiada", - "l_yk_no_access": "Dostęp do tego klucza YubiKey jest niemożliwy", - "s_yk_inaccessible": "Urządzenie niedostępne", - "l_open_connection_failed": "Nie udało się nawiązać połączenia", - "l_ccid_connection_failed": "Nie udało się nawiązać połączenia z kartą inteligentną", - "p_ccid_service_unavailable": "Upewnij się, że usługa kart inteligentnych działa.", - "p_pcscd_unavailable": "Upewnij się, że pcscd jest zainstalowany i uruchomiony.", - "l_no_yk_present": "Klucz YubiKey nie jest obecny", - "s_unknown_type": "Nieznany typ", - "s_unknown_device": "Nierozpoznane urządzenie", - "s_unsupported_yk": "Nieobsługiwany klucz YubiKey", - "s_yk_not_recognized": "Urządzenie nie rozpoznane", - - "@_general_errors": {}, - "l_error_occured": "Wystąpił błąd", - "s_application_error": "Błąd funkcji", - "l_import_error": "Błąd importowania", - "l_file_not_found": "Nie odnaleziono pliku", - "l_file_too_big": "Zbyt duży rozmiar pliku", - "l_filesystem_error": "Błąd operacji systemu plików", - - "@_pins": {}, - "s_pin": "PIN", - "s_set_pin": "Ustaw PIN", - "s_change_pin": "Zmień PIN", - "s_current_pin": "Aktualny PIN", - "s_new_pin": "Nowy PIN", - "s_confirm_pin": "Potwierdź PIN", - "l_new_pin_len": "Nowy PIN musi mieć co najmniej {length} znaków", - "@l_new_pin_len" : { - "placeholders": { - "length": {} - } - }, - "s_pin_set": "PIN ustawiony", - "l_set_pin_failed": "Nie udało się ustawić kodu PIN: {message}", - "@l_set_pin_failed" : { + "l_account": "Konto: {label}", + "@l_account_add_failed": { "placeholders": { "message": {} } }, - "l_wrong_pin_attempts_remaining": "Błędny PIN, pozostało {retries} prób", - "@l_wrong_pin_attempts_remaining" : { + "l_account_add_failed": "Nie udało się dodać konta: {message}", + "l_account_already_exists": "To konto już istnieje w YubiKey", + "l_account_name_required": "Twoje konto musi mieć nazwę", + "@l_accounts_used": { + "placeholders": { + "capacity": {}, + "used": {} + } + }, + "l_accounts_used": "Użyto {used} z {capacity} kont", + "l_add_one_or_more_fps": "Dodaj jeden lub więcej (do pięciu) odcisków palców", + "@l_app_disabled_desc": { + "placeholders": { + "app": {} + } + }, + "l_app_disabled_desc": "Włącz funkcję '{app}' w kluczu YubiKey, aby uzyskać dostęp", + "l_app_not_supported_desc": "Ta funkcja nie jest obsługiwana", + "@l_app_not_supported_on_yk": { + "placeholders": { + "app": {} + } + }, + "l_app_not_supported_on_yk": "Używany klucz YubiKey nie obsługuje funkcji '{app}'", + "@l_attempts_remaining": { "placeholders": { "retries": {} } }, - "s_fido_pin_protection": "Ochrona FIDO kodem PIN", - "l_fido_pin_protection_optional": "Opcjonalna ochrona FIDO kodem PIN", - "l_enter_fido2_pin": "Wprowadź kod PIN FIDO2 dla klucza YubiKey", - "l_optionally_set_a_pin": "Opcjonalnie ustaw PIN, aby chronić dostęp do YubiKey\nZarejestruj jako klucz bezpieczeństwa na stronach internetowych", - "l_pin_blocked_reset": "PIN jest zablokowany; przywróć ustawienia fabryczne funkcji FIDO", - "l_set_pin_first": "Najpierw wymagany jest kod PIN", - "l_unlock_pin_first": "Najpierw odblokuj kodem PIN", - "l_pin_soft_locked": "PIN został zablokowany do momentu ponownego podłączenia klucza YubiKey", - "p_enter_current_pin_or_reset": "Wprowadź aktualny kod PIN. Jeśli nie znasz kodu PIN, musisz zresetować klucz YubiKey.", - "p_enter_new_fido2_pin": "Wprowadź nowy PIN. Kod PIN musi mieć co najmniej {length} znaków i może zawierać litery, cyfry i znaki specjalne.", - "@p_enter_new_fido2_pin" : { + "l_attempts_remaining": "Pozostało prób: {retries}", + "@l_bullet": { "placeholders": { - "length": {} + "item": {} } }, - - "@_passwords": {}, - "s_password": "Hasło", - "s_manage_password": "Zarządzaj hasłem", - "s_set_password": "Ustaw hasło", - "s_password_set": "Hasło zostało ustawione", - "l_optional_password_protection": "Opcjonalna ochrona hasłem", - "s_new_password": "Nowe hasło", - "s_current_password": "Aktualne hasło", - "s_confirm_password": "Potwierdź hasło", - "s_wrong_password": "Błędne hasło", - "s_remove_password": "Usuń hasło", - "s_password_removed": "Hasło zostało usunięte", - "s_remember_password": "Zapamiętaj hasło", - "s_clear_saved_password": "Usuń zapisane hasło", - "s_password_forgotten": "Hasło zostało zapomniane", - "l_keystore_unavailable": "Magazyn kluczy systemu operacyjnego jest niedostępny", - "l_remember_pw_failed": "Nie udało się zapamiętać hasła", - "l_unlock_first": "Najpierw odblokuj hasłem", + "l_bullet": "• {item}", + "l_bypass_touch_requirement": "Obejdź wymóg dotknięcia", + "l_bypass_touch_requirement_off": "Konta, które wymagają dotknięcia, potrzebują dodatkowego przyłożenia do NFC", + "l_bypass_touch_requirement_on": "Konta, które wymagają dotknięcia, są automatycznie wyświetlane przez NFC", + "l_calculate_code_desc": "Uzyskaj nowy kod z klucza YubiKey", + "l_ccid_connection_failed": "Nie udało się nawiązać połączenia z kartą inteligentną", + "l_certificate_deleted": "Certyfikat został usunięty", + "l_certificate_exported": "Wyeksportowano certyfikat", + "l_change_management_key": "Zmień klucz zarządzania", + "l_code_copied_clipboard": "Kod skopiowany do schowka", + "l_config_updated_reinsert": "Zaktualizowano konfigurację, podłącz ponownie klucz YubiKey", + "l_copy_code_desc": "Łatwe wklejanie kodu do innych aplikacji", + "l_copy_otp_clipboard": "Skopiuj OTP do schowka", + "l_copy_to_clipboard": "Skopiuj do schowka", + "l_default_key_used": "Używany jest domyślny klucz zarządzania", + "l_delete_account_desc": "Usuń konto z klucza YubiKey", + "l_delete_certificate": "Usuń certyfikat", + "l_delete_certificate_desc": "Usuń certyfikat z klucza YubiKey", + "l_delete_fingerprint_desc": "Usuń odcisk palca z klucza YubiKey", + "l_delete_passkey_desc": "Usuń klucz dostępu z klucza YubiKey", + "l_diagnostics_copied": "Dane diagnostyczne skopiowane do schowka", + "l_elevating_permissions": "Podnoszenie uprawnień…", + "l_enable_community_translations": "Tłumaczenia społecznościowe", + "l_enter_fido2_pin": "Wprowadź kod PIN FIDO2 klucza YubiKey", "l_enter_oath_pw": "Wprowadź hasło OATH dla klucza YubiKey", - "p_enter_current_password_or_reset": "Wprowadź aktualne hasło. Jeśli nie znasz hasła, musisz zresetować klucz YubiKey.", - "p_enter_new_password": "Wprowadź nowe hasło. Hasło może zawierać litery, cyfry i znaki specjalne.", - - "@_oath_accounts": {}, - "l_account": "Konto: {label}", - "@l_account" : { + "l_error_occured": "Wystąpił błąd", + "l_export_certificate": "Eksportuj certyfikat", + "l_export_certificate_desc": "Pozwala wyeksportować certyfikat do pliku", + "l_export_certificate_file": "Eksportuj certyfikat do pliku", + "l_export_csr_file": "Zapisz CSR do pliku", + "l_factory_reset_this_app": "Przywróć ustawienia fabryczne tej funkcji", + "l_fido_app_reset": "Reset funkcji FIDO", + "l_fido_pin_protection_optional": "Opcjonalna ochrona FIDO kodem PIN", + "l_file_not_found": "Nie odnaleziono pliku", + "l_file_too_big": "Zbyt duży rozmiar pliku", + "l_filesystem_error": "Błąd operacji systemu plików", + "@l_fingerprint": { "placeholders": { "label": {} } }, - "s_accounts": "Konta", - "s_no_accounts": "Brak kont", - "s_add_account": "Dodaj konto", - "s_account_added": "Konto zostało dodane", - "l_account_add_failed": "Nie udało się dodać konta: {message}", - "@l_account_add_failed" : { - "placeholders": { - "message": {} - } - }, - "l_account_name_required": "Twoje konto musi mieć nazwę", - "l_name_already_exists": "Ta nazwa już istnieje dla tego wydawcy", - "l_invalid_character_issuer": "Nieprawidłowy znak: „:” nie jest dozwolony w polu wydawcy", - "s_pinned": "Przypięte", - "s_pin_account": "Przypnij konto", - "s_unpin_account": "Odepnij konto", - "s_no_pinned_accounts": "Brak przypiętych kont", - "s_rename_account": "Zmień nazwę konta", - "s_account_renamed": "Zmieniono nazwę konta", - "p_rename_will_change_account_displayed": "Spowoduje to zmianę sposobu wyświetlania konta na liście.", - "s_delete_account": "Usuń konto", - "s_account_deleted": "Konto zostało usunięte", - "p_warning_delete_account": "Uwaga! Ta czynność spowoduje usunięcie konta z klucza YubiKey.", - "p_warning_disable_credential": "Nie będzie już możliwe generowanie OTP dla tego konta. Upewnij się, że najpierw wyłączono te dane uwierzytelniające w witrynie, aby uniknąć zablokowania konta.", - "s_account_name": "Nazwa konta", - "s_search_accounts": "Wyszukaj konta", - "l_accounts_used": "Użyto {used} z {capacity} kont", - "@l_accounts_used" : { - "placeholders": { - "used": {}, - "capacity": {} - } - }, - "s_num_digits": "{num} cyfr", - "@s_num_digits" : { - "placeholders": { - "num": {} - } - }, - "s_num_sec": "{num} sek", - "@s_num_sec" : { - "placeholders": { - "num": {} - } - }, - "s_issuer_optional": "Wydawca (opcjonalnie)", - "s_counter_based": "Na podstawie licznika", - "s_time_based": "Na podstawie czasu", - - "@_fido_credentials": {}, - "l_credential": "Poświadczenie: {label}", - "@l_credential" : { - "placeholders": { - "label": {} - } - }, - "s_credentials": "Poświadczenia", - "l_ready_to_use": "Gotowe do użycia", - "l_register_sk_on_websites": "Zarejestruj jako klucz bezpieczeństwa na stronach internetowych", - "l_no_discoverable_accounts": "Nie wykryto kont", - "s_delete_credential": "Usuń poświadczenie", - "s_credential_deleted": "Poświadczenie zostało usunięte", - "p_warning_delete_credential": "Spowoduje to usunięcie poświadczenia z klucza YubiKey.", - - "@_fingerprints": {}, "l_fingerprint": "Odcisk palca: {label}", - "@l_fingerprint" : { - "placeholders": { - "label": {} - } - }, - "s_fingerprints": "Odciski palców", "l_fingerprint_captured": "Odcisk palca zarejestrowany pomyślnie!", - "s_fingerprint_added": "Dodano odcisk palca", - "l_setting_name_failed": "Błąd ustawienia nazwy: {message}", - "@l_setting_name_failed" : { - "placeholders": { - "message": {} - } - }, - "s_add_fingerprint": "Dodaj odcisk palca", - "l_fp_step_1_capture": "Krok 1/2: Pobranie odcisku palca", - "l_fp_step_2_name": "Krok 2/2: Nazwij odcisk palca", - "s_delete_fingerprint": "Usuń odcisk palca", - "s_fingerprint_deleted": "Odcisk palca został usunięty", - "p_warning_delete_fingerprint": "Spowoduje to usunięcie odcisku palca z twojego YubiKey.", - "s_no_fingerprints": "Brak odcisków palców", - "l_set_pin_fingerprints": "Ustaw kod PIN, aby zarejestrować odciski palców", - "l_no_fps_added": "Nie dodano odcisków palców", - "s_rename_fp": "Zmień nazwę odcisku palca", - "s_fingerprint_renamed": "Zmieniono nazwę odcisku palca", - "l_rename_fp_failed": "Błąd zmiany nazwy: {message}", - "@l_rename_fp_failed" : { - "placeholders": { - "message": {} - } - }, - "l_add_one_or_more_fps": "Dodaj jeden lub więcej (do pięciu) odcisków palców", - "l_fingerprints_used": "Zarejestrowano {used}/5 odcisków palców", "@l_fingerprints_used": { "placeholders": { "used": {} } }, - "p_press_fingerprint_begin": "Przytrzymaj palec na kluczu YubiKey, aby rozpocząć.", - "p_will_change_label_fp": "Spowoduje to zmianę etykiety odcisku palca.", - - "@_permissions": {}, - "s_enable_nfc": "Włącz NFC", - "s_permission_denied": "Odmowa dostępu", - "l_elevating_permissions": "Podnoszenie uprawnień\u2026", - "s_review_permissions": "Przegląd uprawnień", - "p_elevated_permissions_required": "Zarządzanie tym urządzeniem wymaga podwyższonych uprawnień.", - "p_webauthn_elevated_permissions_required": "Zarządzanie WebAuthn wymaga podwyższonych uprawnień.", - "p_need_camera_permission": "Yubico Authenticator wymaga dostępu do aparatu w celu skanowania kodów QR.", - - "@_qr_codes": {}, - "s_qr_scan": "Skanuj kod QR", - "l_qr_scanned": "Zeskanowany kod QR", - "l_invalid_qr": "Nieprawidłowy kod QR", - "l_qr_not_found": "Nie znaleziono kodu QR", - "l_qr_not_read": "Odczytanie kodu QR nie powiodło się: {message}", - "@l_qr_not_read" : { - "placeholders": { - "message": {} - } - }, - "l_point_camera_scan": "Skieruj aparat na kod QR, by go zeskanować", - "q_want_to_scan": "Czy chcesz zeskanować?", - "q_no_qr": "Nie masz kodu QR?", - "s_enter_manually": "Wprowadź ręcznie", - - "@_factory_reset": {}, - "s_reset": "Zresetuj", - "s_factory_reset": "Ustawienia fabryczne", - "l_factory_reset_this_app": "Przywróć ustawienia fabryczne tej funkcji", - "s_reset_oath": "Zresetuj OATH", - "l_oath_application_reset": "Reset funkcji OATH", - "s_reset_fido": "Zresetuj FIDO", - "l_fido_app_reset": "Reset funkcji FIDO", - "l_press_reset_to_begin": "Naciśnij reset, aby rozpocząć\u2026", - "l_reset_failed": "Błąd podczas resetowania: {message}", - "@l_reset_failed" : { - "placeholders": { - "message": {} - } - }, - "p_warning_factory_reset": "Uwaga! Spowoduje to nieodwracalne usunięcie wszystkich kont OATH TOTP/HOTP z klucza YubiKey.", - "p_warning_disable_credentials": "Twoje poświadczenia OATH, jak również wszelkie ustawione hasła, zostaną usunięte z tego klucza YubiKey. Upewnij się, że najpierw wyłączono je w odpowiednich witrynach internetowych, aby uniknąć zablokowania kont.", - "p_warning_deletes_accounts": "Uwaga! Spowoduje to nieodwracalne usunięcie wszystkich kont U2F i FIDO2 z klucza YubiKey.", - "p_warning_disable_accounts": "Twoje poświadczenia, a także wszelkie ustawione kody PIN, zostaną usunięte z tego klucza YubiKey. Upewnij się, że najpierw wyłączono je w odpowiednich witrynach internetowych, aby uniknąć zablokowania kont.", - - "@_copy_to_clipboard": {}, - "l_copy_to_clipboard": "Skopiuj do schowka", - "s_code_copied": "Kod skopiowany", - "l_code_copied_clipboard": "Kod skopiowany do schowka", - "s_copy_log": "Kopiuj logi", - "l_log_copied": "Logi skopiowane do schowka", - "l_diagnostics_copied": "Dane diagnostyczne skopiowane do schowka", - "p_target_copied_clipboard": "{label} skopiowano do schowka.", - "@p_target_copied_clipboard" : { - "placeholders": { - "label": {} - } - }, - - "@_custom_icons": {}, - "s_custom_icons": "Niestandardowe ikony", - "l_set_icons_for_accounts": "Ustaw ikony dla kont", - "p_custom_icons_description": "Pakiety ikon mogą sprawić, że Twoje konta będą łatwiejsze do odróżnienia dzięki znanym logo i kolorom.", - "s_replace_icon_pack": "Zastąp pakiet ikon", - "l_loading_icon_pack": "Wczytywanie pakietu ikon\u2026", - "s_load_icon_pack": "Wczytaj pakiet ikon", - "s_remove_icon_pack": "Usuń pakiet ikon", - "l_icon_pack_removed": "Usunięto pakiet ikon", - "l_remove_icon_pack_failed": "Błąd podczas usuwania pakietu ikon", - "s_choose_icon_pack": "Wybierz pakiet ikon", + "l_fingerprints_used": "Zarejestrowano {used}/5 odcisków palców", + "l_fp_step_1_capture": "Krok 1/2: Pobranie odcisku palca", + "l_fp_step_2_name": "Krok 2/2: Nazwij odcisk palca", + "l_generate_desc": "Generuj nowy certyfikat lub CSR", + "l_generating_private_key": "Generowanie prywatnego klucza…", + "l_helper_not_responding": "Proces pomocnika nie odpowiada", + "l_icon_pack_copy_failed": "Nie udało się skopiować plików z pakietu ikon", "l_icon_pack_imported": "Zaimportowano pakiet ikon", - "l_import_icon_pack_failed": "Błąd importu pakietu ikon: {message}", + "l_icon_pack_removed": "Usunięto pakiet ikon", + "l_import_desc": "Zaimportuj klucz i/lub certyfikat", + "l_import_error": "Błąd importowania", + "l_import_file": "Importuj plik", "@l_import_icon_pack_failed": { "placeholders": { "message": {} } }, + "l_import_icon_pack_failed": "Błąd importu pakietu ikon: {message}", + "l_importing_file": "Importowanie pliku…", + "l_insert_or_tap_yk": "Podłącz lub przystaw YubiKey", + "l_insert_yk": "Podłącz klucz YubiKey", + "l_invalid_character_issuer": "Nieprawidłowy znak: „:” nie jest dozwolony w polu wydawcy", "l_invalid_icon_pack": "Nieprawidłowy pakiet ikon", - "l_icon_pack_copy_failed": "Nie udało się skopiować plików z pakietu ikon", - - "@_android_settings": {}, - "s_nfc_options": "Opcje NFC", - "l_on_yk_nfc_tap": "Podczas kontaktu YubiKey z NFC", - "l_launch_ya": "Uruchom Yubico Authenticator", - "l_copy_otp_clipboard": "Skopiuj OTP do schowka", - "l_launch_and_copy_otp": "Uruchom aplikację i skopiuj OTP", + "l_invalid_qr": "Nieprawidłowy kod QR", "l_kbd_layout_for_static": "Układ klawiatury (dla hasła statycznego)", - "s_choose_kbd_layout": "Wybierz układ klawiatury", - "l_bypass_touch_requirement": "Obejdź wymóg dotyku", - "l_bypass_touch_requirement_on": "Konta, które wymagają dotknięcia, są automatycznie wyświetlane przez NFC", - "l_bypass_touch_requirement_off": "Konta, które wymagają dotknięcia, potrzebują dodatkowego przyłożenia do NFC", - "s_silence_nfc_sounds": "Wycisz dźwięki NFC", - "l_silence_nfc_sounds_on": "Dźwięki nie będą odtwarzane po przyłożeniu do NFC", - "l_silence_nfc_sounds_off": "Dźwięki będą odtwarzane po przyłożeniu do NFC", - "s_usb_options": "Opcje USB", + "l_keep_touching_yk": "Wielokrotnie dotykaj klucza YubiKey…", + "l_key_no_certificate": "Załadowano klucz bez certyfikatu", + "l_keystore_unavailable": "Magazyn kluczy systemu operacyjnego jest niedostępny", + "l_launch_and_copy_otp": "Uruchom aplikację i skopiuj OTP", "l_launch_app_on_usb": "Uruchom po podłączeniu YubiKey", - "l_launch_app_on_usb_on": "Uniemożliwia to innym aplikacjom korzystanie z YubiKey przez USB", "l_launch_app_on_usb_off": "Inne aplikacje mogą korzystać z YubiKey przez USB", + "l_launch_app_on_usb_on": "Uniemożliwia to innym aplikacjom korzystanie z YubiKey przez USB", + "l_launch_ya": "Uruchom Yubico Authenticator", + "l_loading_icon_pack": "Wczytywanie pakietu ikon…", + "l_log_copied": "Logi skopiowane do schowka", + "l_management_key_changed": "Zmieniono klucz zarządzania", + "l_min_one_interface": "Przynajmniej jeden interfejs musi być włączony", + "l_name_already_exists": "Ta nazwa już istnieje dla tego wydawcy", + "@l_new_pin_len": { + "placeholders": { + "length": {} + } + }, + "l_new_pin_len": "Nowy PIN musi mieć co najmniej {length} znaków", + "l_no_certificate": "Nie załadowano certyfikatu", + "l_no_discoverable_accounts": "Nie wykryto kont", + "l_no_fps_added": "Nie dodano odcisków palców", + "l_no_yk_present": "Nie wykryto YubiKey", + "l_oath_application_reset": "Reset funkcji OATH", + "l_on_yk_nfc_tap": "Podczas kontaktu YubiKey z NFC", + "l_open_connection_failed": "Nie udało się nawiązać połączenia", + "l_optional_password_protection": "Opcjonalna ochrona hasłem", + "l_optionally_set_a_pin": "Opcjonalnie ustaw PIN, aby chronić dostęp do YubiKey\nZarejestruj jako klucz bezpieczeństwa na stronach internetowych", + "l_overwrite_cert": "Certyfikat zostanie nadpisany", + "l_overwrite_key": "Klucz prywatny zostanie nadpisany", + "l_overwrite_key_maybe": "Każdy istniejący klucz prywatny w slocie zostanie nadpisany", + "@l_passkey": { + "placeholders": { + "label": {} + } + }, + "l_passkey": "Klucz dostępu: {label}", + "l_pin_account_desc": "Przechowuj ważne konta razem", + "l_pin_blocked_reset": "PIN jest zablokowany; przywróć ustawienia fabryczne funkcji FIDO", + "l_pin_protected_key": "Zamiast tego można użyć kodu PIN", + "l_pin_soft_locked": "PIN został zablokowany do momentu ponownego podłączenia klucza YubiKey", + "l_piv_app_reset": "Funkcja PIV została zresetowana", + "l_piv_pin_blocked": "Zablokowano, użyj PUK, aby zresetować", + "l_piv_pin_puk_blocked": "Zablokowano, konieczny reset do ustawień fabrycznych", + "l_place_on_nfc_reader": "Przyłóż klucz YubiKey do czytnika NFC", + "l_point_camera_scan": "Skieruj aparat na kod QR, by go zeskanować", + "l_press_reset_to_begin": "Naciśnij reset, aby rozpocząć…", + "l_qr_not_found": "Nie znaleziono kodu QR", + "@l_qr_not_read": { + "placeholders": { + "message": {} + } + }, + "l_qr_not_read": "Odczytanie kodu QR nie powiodło się: {message}", + "l_qr_scanned": "Zeskanowany kod QR", + "l_ready_to_use": "Gotowe do użycia", + "l_register_sk_on_websites": "Zarejestruj jako klucz bezpieczeństwa na stronach internetowych", + "l_reinsert_yk": "Ponownie podłącz YubiKey", + "l_remember_pw_failed": "Nie udało się zapamiętać hasła", + "l_remove_icon_pack_failed": "Błąd podczas usuwania pakietu ikon", + "l_remove_yk_from_reader": "Odsuń klucz YubiKey od czytnika NFC", + "l_rename_account_desc": "Edytuj wydawcę/nazwę konta", + "l_rename_fp_desc": "Zmień etykietę", + "@l_rename_fp_failed": { + "placeholders": { + "message": {} + } + }, + "l_rename_fp_failed": "Błąd zmiany nazwy: {message}", + "l_replace_yk_on_reader": "Umieść klucz YubiKey z powrotem na czytniku", + "@l_reset_failed": { + "placeholders": { + "message": {} + } + }, + "l_reset_failed": "Błąd podczas resetowania: {message}", + "l_rfc4514_invalid": "Nieprawidłowy format RFC 4514", + "l_select_accounts": "Wybierz konta, które chcesz dodać do YubiKey", + "l_select_import_file": "Wybierz plik do zaimportowania", + "l_set_icons_for_accounts": "Ustaw ikony dla kont", + "@l_set_pin_failed": { + "placeholders": { + "message": {} + } + }, + "l_set_pin_failed": "Nie udało się ustawić kodu PIN: {message}", + "l_set_pin_fingerprints": "Ustaw kod PIN, aby zarejestrować odciski palców", + "l_set_pin_first": "Najpierw wymagany jest kod PIN", + "@l_setting_name_failed": { + "placeholders": { + "message": {} + } + }, + "l_setting_name_failed": "Błąd ustawienia nazwy: {message}", + "l_silence_nfc_sounds_off": "Dźwięki będą odtwarzane po przyłożeniu do NFC", + "l_silence_nfc_sounds_on": "Dźwięki nie będą odtwarzane po przyłożeniu do NFC", + "l_touch_button_now": "Dotknij teraz przycisku na kluczu YubiKey", + "l_unlock_first": "Najpierw odblokuj hasłem", + "l_unlock_pin_first": "Najpierw odblokuj kodem PIN", + "l_unlock_piv_management": "Odblokuj zarządzanie PIV", + "l_unplug_yk": "Odłącz klucz YubiKey", + "l_warning_default_key": "Uwaga: Używany jest klucz domyślny", + "l_webauthn_req_fido2": "WebAuthn wymaga włączenia funkcji FIDO2 w kluczu YubiKey", + "l_wrong_key": "Błędny klucz", + "@l_wrong_pin_attempts_remaining": { + "placeholders": { + "retries": {} + } + }, + "l_wrong_pin_attempts_remaining": "Błędny PIN, pozostało prób: {retries}", + "@l_wrong_puk_attempts_remaining": { + "placeholders": { + "retries": {} + } + }, + "l_wrong_puk_attempts_remaining": "Nieprawidłowy PUK, pozostało prób: {retries}", + "l_yk_no_access": "Dostęp do tego klucza YubiKey jest niemożliwy", + "p_add_description": "W celu zeskanowania kodu QR, upewnij się, że pełny kod jest widoczny na ekranie a następnie naciśnij poniższy przycisk. Jeśli posiadasz dane uwierzytelniające do konta w tekstowej formie, skorzystaj z opcji ręcznego wprowadzania danych.", + "p_ccid_service_unavailable": "Upewnij się, że usługa kart inteligentnych działa.", + "p_cert_options_desc": "Algorytm klucza do użycia, format wyjściowy i data wygaśnięcia (tylko certyfikat).", + "p_change_management_key_desc": "Zmień swój klucz zarządzania. Opcjonalnie możesz zezwolić na używanie kodu PIN zamiast klucza zarządzania.", + "p_community_translations_desc": "Tłumaczenia te są dostarczane i utrzymywane przez społeczność. Mogą zawierać błędy lub być niekompletne.", + "p_custom_icons_description": "Pakiety ikon mogą sprawić, że Twoje konta będą łatwiejsze do odróżnienia dzięki znanym logo i kolorom.", + "p_elevated_permissions_required": "Zarządzanie tym urządzeniem wymaga podwyższonych uprawnień.", + "p_enter_current_password_or_reset": "Wprowadź aktualne hasło. Jeśli go nie znasz, musisz zresetować klucz YubiKey.", + "p_enter_current_pin_or_reset": "Wprowadź aktualny kod PIN. Jeśli go nie znasz, musisz zresetować klucz YubiKey.", + "p_enter_current_puk_or_reset": "Wprowadź aktualny kod PUK. Jeśli go nie znasz, musisz zresetować klucz YubiKey.", + "@p_enter_new_fido2_pin": { + "placeholders": { + "length": {} + } + }, + "p_enter_new_fido2_pin": "Wprowadź nowy kod PIN. Musi zawierać co najmniej {length} znaków. Może zawierać litery, cyfry i znaki specjalne.", + "p_enter_new_password": "Wprowadź nowe hasło. Może ono zawierać litery, cyfry i znaki specjalne.", + "@p_enter_new_piv_pin_puk": { + "placeholders": { + "name": {} + } + }, + "p_enter_new_piv_pin_puk": "Wprowadź nową {name} do ustawienia. Musi składać się z 6-8 znaków.", + "@p_generate_desc": { + "placeholders": { + "slot": {} + } + }, + "p_generate_desc": "Spowoduje to wygenerowanie nowego klucza w kluczu YubiKey w slocie PIV {slot}. Klucz publiczny zostanie osadzony w samopodpisanym certyfikacie przechowywanym w kluczu YubiKey lub w żądaniu podpisania certyfikatu (CSR) zapisanym w pliku.", + "@p_import_items_desc": { + "placeholders": { + "slot": {} + } + }, + "p_import_items_desc": "Następujące elementy zostaną zaimportowane do slotu PIV {slot}.", + "p_need_camera_permission": "Yubico Authenticator wymaga dostępu do aparatu w celu skanowania kodów QR.", + "@p_overwrite_slot_desc": { + "placeholders": { + "slot": {} + } + }, + "p_overwrite_slot_desc": "Spowoduje to trwałe nadpisanie istniejącej zawartości w slocie {slot}.", + "p_password_protected_file": "Wybrany plik jest chroniony hasłem. Wprowadź je, aby kontynuować.", + "p_pcscd_unavailable": "Upewnij się, że pcscd jest zainstalowany i uruchomiony.", + "p_pin_required_desc": "Czynność, którą zamierzasz wykonać, wymaga wprowadzenia kodu PIN PIV.", + "p_press_fingerprint_begin": "Przytrzymaj palec na kluczu YubiKey, aby rozpocząć.", + "p_rename_will_change_account_displayed": "Spowoduje to zmianę sposobu wyświetlania konta na liście.", + "p_subject_desc": "Nazwa wyróżniająca (DN) sformatowana zgodnie ze specyfikacją RFC 4514.", + "@p_target_copied_clipboard": { + "placeholders": { + "label": {} + } + }, + "p_target_copied_clipboard": "{label} skopiowano do schowka.", + "p_try_reinsert_yk": "Spróbuj ponownie podłączyć klucz YubiKey.", + "p_unlock_piv_management_desc": "Czynność, którą zamierzasz wykonać, wymaga klucza zarządzania PIV. Podaj ten klucz, aby odblokować funkcje zarządzania dla tej sesji.", + "p_warning_delete_account": "Uwaga! Ta czynność spowoduje usunięcie konta z klucza YubiKey.", + "p_warning_delete_certificate": "Uwaga! Ta czynność spowoduje usunięcie certyfikatu z klucza YubiKey.", + "p_warning_delete_fingerprint": "Spowoduje to usunięcie odcisku palca z twojego YubiKey.", + "p_warning_delete_passkey": "Spowoduje to usunięcie klucza dostępu z klucza YubiKey.", + "p_warning_deletes_accounts": "Uwaga! Spowoduje to nieodwracalne usunięcie wszystkich kont U2F i FIDO2 z klucza YubiKey.", + "p_warning_disable_accounts": "Twoje poświadczenia, a także wszelkie ustawione kody PIN, zostaną usunięte z tego klucza YubiKey. Upewnij się, że najpierw wyłączono je w odpowiednich witrynach internetowych, aby uniknąć zablokowania kont.", + "p_warning_disable_credential": "Nie będzie już możliwe generowanie OTP dla tego konta. Upewnij się, że najpierw wyłączono te dane uwierzytelniające w witrynie, aby uniknąć zablokowania konta.", + "p_warning_disable_credentials": "Twoje poświadczenia OATH, jak również wszelkie ustawione hasła, zostaną usunięte z tego klucza YubiKey. Upewnij się, że najpierw wyłączono je w odpowiednich witrynach internetowych, aby uniknąć zablokowania kont.", + "p_warning_factory_reset": "Uwaga! Spowoduje to nieodwracalne usunięcie wszystkich kont OATH TOTP/HOTP z klucza YubiKey.", + "p_warning_piv_reset": "Ostrzeżenie! Wszystkie dane przechowywane dla PIV zostaną nieodwracalnie usunięte z klucza YubiKey.", + "p_warning_piv_reset_desc": "Obejmuje to klucze prywatne i certyfikaty. Kod PIN, PUK i klucz zarządzania zostaną zresetowane do domyślnych wartości fabrycznych.", + "p_webauthn_elevated_permissions_required": "Zarządzanie WebAuthn wymaga podwyższonych uprawnień.", + "p_will_change_label_fp": "Spowoduje to zmianę etykiety odcisku palca.", + "@q_delete_certificate_confirm": { + "placeholders": { + "slot": {} + } + }, + "q_delete_certificate_confirm": "Usunąć certyfikat ze slotu PIV {slot}?", + "q_have_account_info": "Masz dane konta?", + "q_no_qr": "Nie masz kodu QR?", + "@q_rename_target": { + "placeholders": { + "label": {} + } + }, + "q_rename_target": "Zmienić nazwę {label}?", + "q_want_to_scan": "Czy chcesz zeskanować?", + "rfc4514_examples": "Przykłady:\nCN=Przykładowa Nazwa\nCN=jkowalski,DC=przyklad,DC=pl", + "s_about": "O aplikacji", + "s_account_added": "Konto zostało dodane", + "s_account_deleted": "Konto zostało usunięte", + "s_account_name": "Nazwa konta", + "s_account_renamed": "Zmieniono nazwę konta", + "s_accounts": "Konta", + "s_actions": "Działania", + "s_add_account": "Dodaj konto", + "s_add_accounts": "Dodaj konto(-a)", + "s_add_fingerprint": "Dodaj odcisk palca", + "s_add_manually": "Dodaj ręcznie", + "s_algorithm": "Algorytm", "s_allow_screenshots": "Zezwalaj na zrzuty ekranu", - - "@_eof": {} -} + "s_app_disabled": "Wyłączona funkcja", + "s_app_not_supported": "Funkcja nie jest obsługiwana", + "s_app_theme": "Motyw aplikacji", + "s_appearance": "Wygląd", + "s_application_error": "Błąd funkcji", + "s_authenticator": "Authenticator", + "s_calculate": "Oblicz", + "s_calculate_code": "Oblicz kod", + "s_cancel": "Anuluj", + "s_certificate": "Certyfikat", + "s_certificate_fingerprint": "Odcisk palca", + "s_certificates": "Certyfikaty", + "s_change_pin": "Zmień PIN", + "s_change_puk": "Zmień PUK", + "s_character_count": "Liczba znaków", + "s_choose_app_theme": "Wybierz motyw aplikacji", + "s_choose_icon_pack": "Wybierz pakiet ikon", + "s_choose_kbd_layout": "Wybierz układ klawiatury", + "s_clear_saved_password": "Usuń zapisane hasło", + "s_close": "Zamknij", + "s_code_copied": "Kod skopiowany", + "s_config_updated": "Zaktualizowano konfigurację", + "s_configure_yk": "Skonfiguruj YubiKey", + "s_confirm_password": "Potwierdź hasło", + "s_confirm_pin": "Potwierdź PIN", + "s_confirm_puk": "Potwierdź PUK", + "s_copy_log": "Kopiuj logi", + "s_counter_based": "Na podstawie licznika", + "s_csr": "CSR", + "s_current_management_key": "Aktualny klucz zarządzania", + "s_current_password": "Aktualne hasło", + "s_current_pin": "Aktualny PIN", + "s_current_puk": "Aktualny PUK", + "s_custom_icons": "Niestandardowe ikony", + "s_dark_mode": "Ciemny", + "s_delete": "Usuń", + "s_delete_account": "Usuń konto", + "s_delete_fingerprint": "Usuń odcisk palca", + "s_delete_passkey": "Usuń klucz dostępu", + "s_enable_nfc": "Włącz NFC", + "s_enter_manually": "Wprowadź ręcznie", + "s_factory_reset": "Ustawienia fabryczne", + "s_fido_disabled": "FIDO2 wyłączone", + "s_fido_pin_protection": "Ochrona FIDO kodem PIN", + "s_file_imported": "Plik został zaimportowany", + "s_fingerprint_added": "Dodano odcisk palca", + "s_fingerprint_deleted": "Odcisk palca został usunięty", + "s_fingerprint_renamed": "Zmieniono nazwę odcisku palca", + "s_fingerprints": "Odciski palców", + "@s_fw_version": { + "placeholders": { + "version": {} + } + }, + "s_fw_version": "F/W: {version}", + "s_generate_key": "Generuj klucz", + "s_generate_random": "Generuj losowo", + "s_help_and_about": "Pomoc i informacje", + "s_help_and_feedback": "Pomoc i opinie", + "s_hide_device": "Ukryj urządzenie", + "s_hide_window": "Ukryj okno", + "s_i_need_help": "Pomoc", + "s_import": "Importuj", + "s_invalid_length": "Nieprawidłowa długość", + "s_issuer": "Wydawca", + "s_issuer_optional": "Wydawca (opcjonalnie)", + "s_label": "Etykieta", + "s_language": "Język", + "s_learn_more": "Dowiedz się więcej", + "s_light_mode": "Jasny", + "s_load_icon_pack": "Wczytaj pakiet ikon", + "@s_log_level": { + "placeholders": { + "level": {} + } + }, + "s_log_level": "Poziom logowania: {level}", + "s_manage": "Zarządzaj", + "s_manage_password": "Zarządzaj hasłem", + "s_management_key": "Klucz zarządzania", + "s_name": "Nazwa", + "s_new_management_key": "Nowy klucz zarządzania", + "s_new_password": "Nowe hasło", + "s_new_pin": "Nowy PIN", + "s_new_puk": "Nowy PUK", + "s_nfc": "NFC", + "s_nfc_dialog_oath_add_account": "Działanie: dodaj nowe konto", + "s_nfc_dialog_oath_add_multiple_accounts": "Działanie: dodawanie wielu kont", + "s_nfc_dialog_oath_calculate_code": "Działanie: oblicz kod OATH", + "s_nfc_dialog_oath_delete_account": "Działanie: usuń konto", + "s_nfc_dialog_oath_failure": "Operacja OATH nie powiodła się", + "s_nfc_dialog_oath_rename_account": "Działanie: zmień nazwę konta", + "s_nfc_dialog_oath_reset": "Działanie: resetuj aplet OATH", + "s_nfc_dialog_oath_set_password": "Działanie: ustaw hasło OATH", + "s_nfc_dialog_oath_unlock": "Działanie: odblokuj aplet OATH", + "s_nfc_dialog_oath_unset_password": "Działanie: usuń hasło OATH", + "s_nfc_dialog_operation_failed": "Niepowodzenie", + "s_nfc_dialog_operation_success": "Powodzenie", + "s_nfc_dialog_tap_key": "Przystaw swój klucz", + "s_nfc_options": "Opcje NFC", + "s_no_accounts": "Brak kont", + "s_no_fingerprints": "Brak odcisków palców", + "s_no_pinned_accounts": "Brak przypiętych kont", + "@s_num_digits": { + "placeholders": { + "num": {} + } + }, + "s_num_digits": "{num} cyfr", + "@s_num_sec": { + "placeholders": { + "num": {} + } + }, + "s_num_sec": "{num} sek", + "s_open_src_licenses": "Licencje open source", + "s_options": "Opcje", + "s_overwrite": "Nadpisz", + "s_overwrite_slot": "Nadpisz slot", + "s_passkey_deleted": "Usunięto klucz dostępu", + "s_passkeys": "Klucze dostępu", + "s_password": "Hasło", + "s_password_forgotten": "Hasło zostało zapomniane", + "s_password_removed": "Hasło zostało usunięte", + "s_password_set": "Hasło zostało ustawione", + "s_permission_denied": "Odmowa dostępu", + "s_pin": "PIN", + "s_pin_account": "Przypnij konto", + "s_pin_required": "Wymagany PIN", + "s_pin_set": "PIN ustawiony", + "s_pinned": "Przypięte", + "s_piv": "PIV", + "s_please_wait": "Proszę czekać…", + "s_privacy_policy": "Polityka prywatności", + "s_private_key": "Klucz prywatny", + "s_private_key_generated": "Wygenerowano klucz prywatny", + "s_protect_key": "Zabezpiecz kodem PIN", + "s_puk": "PUK", + "s_puk_set": "PUK ustawiony", + "s_qr_scan": "Skanuj kod QR", + "s_quit": "Wyjdź", + "s_reconfiguring_yk": "Rekonfigurowanie YubiKey…", + "s_remember_password": "Zapamiętaj hasło", + "s_remove_icon_pack": "Usuń pakiet ikon", + "s_remove_password": "Usuń hasło", + "s_rename_account": "Zmień nazwę konta", + "s_rename_fp": "Zmień nazwę odcisku palca", + "s_replace_icon_pack": "Zastąp pakiet ikon", + "s_require_touch": "Wymagaj dotknięcia", + "s_reset": "Zresetuj", + "s_reset_fido": "Zresetuj FIDO", + "s_reset_oath": "Zresetuj OATH", + "s_reset_piv": "Resetuj PIV", + "s_review_permissions": "Przegląd uprawnień", + "s_run_diagnostics": "Uruchom diagnostykę", + "s_save": "Zapisz", + "s_search_accounts": "Wyszukaj konta", + "s_secret_key": "Tajny klucz", + "s_select_to_scan": "Wybierz, aby skanować", + "s_select_yk": "Wybierz YubiKey", + "s_send_feedback": "Prześlij opinię", + "s_serial": "Nr. seryjny", + "s_set_password": "Ustaw hasło", + "s_set_pin": "Ustaw PIN", + "s_settings": "Ustawienia", + "s_setup": "Konfiguruj", + "s_show_hidden_devices": "Pokaż ukryte urządzenia", + "s_show_window": "Pokaż okno", + "s_silence_nfc_sounds": "Wycisz dźwięki NFC", + "s_slot_9a": "Uwierzytelnienie", + "s_slot_9c": "Cyfrowy podpis", + "s_slot_9d": "Menedżer kluczy", + "s_slot_9e": "Autoryzacja karty", + "@s_slot_display_name": { + "placeholders": { + "hexid": {}, + "name": {} + } + }, + "s_slot_display_name": "{name} ({hexid})", + "@s_sn_serial": { + "placeholders": { + "serial": {} + } + }, + "s_sn_serial": "S/N: {serial}", + "s_subject": "Temat", + "s_system_default": "Zgodny z systemem", + "s_terms_of_use": "Warunki użytkowania", + "s_time_based": "Na podstawie czasu", + "s_toggle_applications": "Przełączanie funkcji", + "s_touch_required": "Wymagane dotknięcie", + "s_troubleshooting": "Rozwiązywanie problemów", + "s_unblock_pin": "Odblokuj PIN", + "s_unknown_device": "Nierozpoznane urządzenie", + "s_unknown_type": "Nieznany typ", + "s_unlock": "Odblokuj", + "s_unpin_account": "Odepnij konto", + "s_unsupported_yk": "Nieobsługiwany klucz YubiKey", + "s_usb": "USB", + "s_usb_options": "Opcje USB", + "s_use_default": "Użyj domyślnego", + "s_valid_from": "Ważny od", + "s_valid_to": "Ważny do", + "s_webauthn": "WebAuthn", + "s_wrong_password": "Błędne hasło", + "s_yk_inaccessible": "Urządzenie niedostępne", + "s_yk_information": "Informacja o YubiKey", + "s_yk_not_recognized": "Urządzenie nie rozpoznane" +} \ No newline at end of file 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/icon_provider/icon_pack_manager.dart b/lib/oath/icon_provider/icon_pack_manager.dart index 3f725819..c9c4bb25 100644 --- a/lib/oath/icon_provider/icon_pack_manager.dart +++ b/lib/oath/icon_provider/icon_pack_manager.dart @@ -20,11 +20,11 @@ import 'dart:io'; import 'package:archive/archive.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:io/io.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:yubico_authenticator/app/logging.dart'; -import 'package:io/io.dart'; import 'icon_cache.dart'; import 'icon_pack.dart'; @@ -116,7 +116,16 @@ class IconPackManager extends StateNotifier> { final unpackDirectory = Directory(join(tempDirectory.path, 'unpack')); - final archive = ZipDecoder().decodeBytes(bytes, verify: true); + Archive archive; + try { + archive = ZipDecoder().decodeBytes(bytes, verify: true); + } on Exception catch (_) { + _log.error('File is not an icon pack: zip decoding failed'); + _lastError = l10n.l_invalid_icon_pack; + state = AsyncValue.error('File is not an icon pack', StackTrace.current); + return false; + } + for (final file in archive) { final filename = file.name; if (file.size > 0) { @@ -172,7 +181,8 @@ class IconPackManager extends StateNotifier> { } catch (e) { _log.error('Failed to copy icon pack files to destination: $e'); _lastError = l10n.l_icon_pack_copy_failed; - state = AsyncValue.error('Failed to copy icon pack files.', StackTrace.current); + state = AsyncValue.error( + 'Failed to copy icon pack files.', StackTrace.current); return false; } diff --git a/lib/oath/keys.dart b/lib/oath/keys.dart index 8eb64593..6539938a 100755 --- a/lib/oath/keys.dart +++ b/lib/oath/keys.dart @@ -17,18 +17,29 @@ 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 migrateAccountAction = Key('$_keyAction.migrate_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/models.dart b/lib/oath/models.dart index 2a9ba7cc..0e0998f5 100755 --- a/lib/oath/models.dart +++ b/lib/oath/models.dart @@ -14,6 +14,9 @@ * limitations under the License. */ +import 'dart:typed_data'; +import 'dart:convert'; +import 'package:base32/base32.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -120,10 +123,83 @@ class CredentialData with _$CredentialData { factory CredentialData.fromJson(Map json) => _$CredentialDataFromJson(json); - factory CredentialData.fromUri(Uri uri) { - if (uri.scheme.toLowerCase() != 'otpauth') { - throw ArgumentError('Invalid scheme, must be "otpauth://"'); + static List fromUri(Uri uri) { + if (uri.scheme.toLowerCase() == 'otpauth-migration') { + return CredentialData.fromMigration(uri); + } else if (uri.scheme.toLowerCase() == 'otpauth') { + return [CredentialData.fromOtpauth(uri)]; + } else { + throw ArgumentError('Invalid scheme'); } + } + + static List fromMigration(Uri uri) { + // Parse single protobuf encoded integer + (int value, Uint8List rem) protoInt(Uint8List data) { + final extras = data.takeWhile((b) => b & 0x80 != 0).length; + int value = 0; + for (int i = extras; i >= 0; i--) { + value = (value << 7) | (data[i] & 0x7F); + } + return (value, data.sublist(1 + extras)); + } + + // Parse a single protobuf value from a buffer + (int tag, dynamic value, Uint8List rem) protoValue(Uint8List data) { + final first = data[0]; + final int len; + (len, data) = protoInt(data.sublist(1)); + final index = first >> 3; + switch (first & 0x07) { + case 0: + return (index, len, data); + case 2: + return (index, data.sublist(0, len), data.sublist(len)); + } + throw ArgumentError('Unsupported value type!'); + } + + // Parse a protobuf message into map of tags and values + Map protoMap(Uint8List data) { + Map values = {}; + while (data.isNotEmpty) { + final (tag, value, rem) = protoValue(data); + values[tag] = value; + data = rem; + } + return values; + } + + // Parse encoded credentials from data (tag 1) ignoring trailing extra data + Iterable> splitCreds(Uint8List rem) sync* { + Uint8List credrem; + while (rem[0] == 0x0a) { + (_, credrem, rem) = protoValue(rem); + yield protoMap(credrem); + } + } + + // Convert parsed credential values into CredentialData objects + return splitCreds(base64.decode(uri.queryParameters['data']!)) + .map((values) => CredentialData( + secret: base32.encode(values[1]), + name: utf8.decode(values[2], allowMalformed: true), + issuer: values[3] != null + ? utf8.decode(values[3], allowMalformed: true) + : null, + hashAlgorithm: switch (values[4]) { + 2 => HashAlgorithm.sha256, + 3 => HashAlgorithm.sha512, + _ => HashAlgorithm.sha1, + }, + digits: values[5] == 2 ? 8 : defaultDigits, + oathType: values[6] == 1 ? OathType.hotp : OathType.totp, + counter: values[7] ?? defaultCounter, + )) + .toList(); + } + + factory CredentialData.fromOtpauth(Uri uri) { final oathType = OathType.values.byName(uri.host.toLowerCase()); final params = uri.queryParameters; String? issuer; 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_dialog.dart b/lib/oath/views/account_dialog.dart index ac26cba8..fa771f37 100755 --- a/lib/oath/views/account_dialog.dart +++ b/lib/oath/views/account_dialog.dart @@ -23,6 +23,8 @@ 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 '../../app/views/action_list.dart'; import '../../core/models.dart'; import '../../core/state.dart'; import '../models.dart'; @@ -37,60 +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 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), - }; - - // If we can't copy, but can calculate, highlight that button instead - if (copy.intent == null) { - final calculates = actions.where(((e) => e.text == l10n.s_calculate)); - if (calculates.isNotEmpty) { - colors[calculates.first] = (theme.primary, theme.onPrimary); - } - } - - return actions.map((e) { - 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, - 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, - ), - ), - ); - }).toList(); - } - @override Widget build(BuildContext context, WidgetRef ref) { // TODO: Solve this in a cleaner way @@ -111,15 +59,16 @@ class AccountDialog extends ConsumerWidget { EditIntent: CallbackAction(onInvoke: (_) async { final credentials = ref.read(credentialsProvider); final withContext = ref.read(withContextProvider); - final OathCredential? renamed = + final renamed = await withContext((context) async => await showBlurDialog( - context: context, - builder: (context) => RenameAccountDialog( - node, - credential, - credentials, - ), - )); + context: context, + builder: (context) => RenameAccountDialog.forOathCredential( + ref, + node, + credential, + credentials?.map((e) => (e.issuer, e.name)).toList() ?? + [], + ))); if (renamed != null) { // Replace the dialog with the renamed credential await withContext((context) async { @@ -168,42 +117,16 @@ 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: [ - if (subtitle != null) - Text( - subtitle, - softWrap: true, - textAlign: TextAlign.center, - // This is what ListTile uses for subtitle - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).textTheme.bodySmall!.color, - ), - ), - const SizedBox(height: 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), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 32), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, @@ -220,20 +143,36 @@ class AccountDialog extends ConsumerWidget { ], ), ), - ), + Text( + helper.title, + style: Theme.of(context).textTheme.headlineSmall, + softWrap: true, + textAlign: TextAlign.center, + ), + if (subtitle != null) + Text( + subtitle, + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: + Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context) + .textTheme + .bodySmall! + .color, + ), + ), + ], ), ), + ActionListSection.fromMenuActions( + context, + AppLocalizations.of(context)!.s_actions, + actions: helper.buildActions(), + ), ], ), - 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..32174f47 100755 --- a/lib/oath/views/account_helper.dart +++ b/lib/oath/views/account_helper.dart @@ -29,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. @@ -53,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,38 +62,51 @@ 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'; + final canCopy = code != null && !expired; + return [ - MenuAction( - text: l10n.l_copy_to_clipboard, + ActionItem( + key: keys.copyAction, icon: const Icon(Icons.copy), - intent: code == null || expired ? null : const CopyIntent(), - trailing: shortcut, + 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( - text: l10n.s_calculate, + ActionItem( + key: keys.calculateAction, + actionStyle: !canCopy ? ActionStyle.primary : null, icon: const Icon(Icons.refresh), + title: l10n.s_calculate, + subtitle: l10n.l_calculate_code_desc, intent: ready ? const CalculateIntent() : null, ), - MenuAction( - text: pinned ? l10n.s_unpin_account : l10n.s_pin_account, + ActionItem( + key: keys.togglePinAction, icon: pinned ? pushPinStrokeIcon : const Icon(Icons.push_pin_outlined), + 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, + title: l10n.s_rename_account, + subtitle: l10n.l_rename_account_desc, intent: const EditIntent(), ), - MenuAction( - text: l10n.s_delete_account, + ActionItem( + key: keys.deleteAction, + actionStyle: ActionStyle.error, icon: const Icon(Icons.delete_outline), + title: l10n.s_delete_account, + subtitle: l10n.l_delete_account_desc, intent: const DeleteIntent(), ), ]; @@ -157,6 +171,7 @@ class _CodeLabel extends StatelessWidget { // This helps with vertical centering on desktop applyHeightToFirstAscent: !isDesktop, ), + semanticsLabel: code?.value.characters.map((c) => '$c ' ).toString(), ), ); } diff --git a/lib/oath/views/account_view.dart b/lib/oath/views/account_view.dart index 92670559..09222c47 100755 --- a/lib/oath/views/account_view.dart +++ b/lib/oath/views/account_view.dart @@ -22,8 +22,7 @@ import 'package:flutter_riverpod/flutter_riverpod.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'; @@ -48,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], @@ -87,23 +77,6 @@ class _AccountViewState extends ConsumerState { return colors[label.hashCode % colors.length]!; } - List _buildPopupMenu( - BuildContext context, AccountHelper helper) { - 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.trailing, - ); - }).toList(); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -116,6 +89,7 @@ class _AccountViewState extends ConsumerState { OpenIntent: CallbackAction(onInvoke: (_) async { await showBlurDialog( context: context, + barrierColor: Colors.transparent, builder: (context) => AccountDialog(credential), ); return null; @@ -123,12 +97,16 @@ class _AccountViewState extends ConsumerState { EditIntent: CallbackAction(onInvoke: (_) async { final node = ref.read(currentDeviceProvider)!; final credentials = ref.read(credentialsProvider); - return await ref.read(withContextProvider)( - (context) async => await showBlurDialog( - context: context, - builder: (context) => - RenameAccountDialog(node, credential, credentials), - )); + final withContext = ref.read(withContextProvider); + return await withContext((context) async => await showBlurDialog( + context: context, + builder: (context) => RenameAccountDialog.forOathCredential( + ref, + node, + credential, + credentials?.map((e) => (e.issuer, e.name)).toList() ?? [], + ), + )); }), DeleteIntent: CallbackAction(onInvoke: (_) async { final node = ref.read(currentDeviceProvider)!; @@ -165,83 +143,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/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/oath/views/add_account_dialog.dart b/lib/oath/views/add_account_dialog.dart new file mode 100644 index 00000000..17e37b54 --- /dev/null +++ b/lib/oath/views/add_account_dialog.dart @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:yubico_authenticator/app/message.dart'; +import 'package:yubico_authenticator/app/state.dart'; +import 'package:yubico_authenticator/widgets/responsive_dialog.dart'; + +import '../../app/models.dart'; +import '../../widgets/file_drop_target.dart'; +import '../models.dart'; +import '../state.dart'; +import 'add_account_page.dart'; +import 'utils.dart'; + +class AddAccountDialog extends ConsumerStatefulWidget { + final DevicePath? devicePath; + final OathState? state; + + const AddAccountDialog(this.devicePath, this.state, {super.key}); + + @override + ConsumerState createState() => + _AddAccountDialogState(); +} + +class _AddAccountDialogState extends ConsumerState { + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final credentials = ref.read(credentialsProvider); + final withContext = ref.read(withContextProvider); + + final qrScanner = ref.watch(qrScannerProvider); + return ResponsiveDialog( + title: Text(l10n.s_add_account), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: FileDropTarget( + onFileDropped: (fileData) async { + Navigator.of(context).pop(); + if (qrScanner != null) { + final b64Image = base64Encode(fileData); + final qrData = await qrScanner.scanQr(b64Image); + await withContext( + (context) async { + if (qrData != null) { + await handleUri(context, credentials, qrData, + widget.devicePath, widget.state, l10n); + } else { + showMessage(context, l10n.l_qr_not_found); + } + }, + ); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.p_add_description), + const SizedBox(height: 4), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4.0, + runSpacing: 8.0, + children: [ + ActionChip( + avatar: const Icon(Icons.qr_code_scanner_outlined), + label: Text(l10n.s_qr_scan), + onPressed: () async { + if (qrScanner != null) { + final qrData = await qrScanner.scanQr(); + await withContext( + (context) async { + if (qrData != null) { + Navigator.of(context).pop(); + await handleUri(context, credentials, qrData, + widget.devicePath, widget.state, l10n); + } else { + showMessage(context, l10n.l_qr_not_found); + } + }, + ); + } + }, + ), + ActionChip( + avatar: const Icon(Icons.edit_outlined), + label: Text(l10n.s_add_manually), + onPressed: () async { + Navigator.of(context).pop(); + await withContext((context) async { + await showBlurDialog( + context: context, + builder: (context) => OathAddAccountPage( + widget.devicePath, + widget.state, + credentials: credentials, + ), + ); + }); + }), + ]) + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + )); + } +} diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index b99a88ca..0bbf8385 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * Copyright (C) 2022-2023 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ import '../../desktop/models.dart'; import '../../management/models.dart'; import '../../widgets/choice_filter_chip.dart'; import '../../widgets/file_drop_target.dart'; +import '../../widgets/focus_utils.dart'; import '../../widgets/responsive_dialog.dart'; import '../../widgets/utf8_utils.dart'; import '../keys.dart' as keys; @@ -50,8 +51,6 @@ final _log = Logger('oath.view.add_account_page'); final _secretFormatterPattern = RegExp('[abcdefghijklmnopqrstuvwxyz234567 ]', caseSensitive: false); -enum _QrScanState { none, scanning, success, failed } - class OathAddAccountPage extends ConsumerStatefulWidget { final DevicePath? devicePath; final OathState? state; @@ -81,8 +80,9 @@ class _OathAddAccountPageState extends ConsumerState { OathType _oathType = defaultOathType; HashAlgorithm _hashAlgorithm = defaultHashAlgorithm; int _digits = defaultDigits; + int _counter = defaultCounter; bool _validateSecretLength = false; - _QrScanState _qrState = _QrScanState.none; + bool _dataLoaded = false; bool _isObscure = true; List _periodValues = [20, 30, 45, 60]; List _digitsValues = [6, 8]; @@ -106,55 +106,6 @@ class _OathAddAccountPageState extends ConsumerState { } } - _scanQrCode(QrScanner qrScanner) async { - final l10n = AppLocalizations.of(context)!; - try { - setState(() { - // If we have a previous scan result stored, clear it - if (_qrState == _QrScanState.success) { - _issuerController.text = ''; - _accountController.text = ''; - _secretController.text = ''; - _oathType = defaultOathType; - _hashAlgorithm = defaultHashAlgorithm; - _periodController.text = '$defaultPeriod'; - _digits = defaultDigits; - } - _qrState = _QrScanState.scanning; - }); - final otpauth = await qrScanner.scanQr(); - if (otpauth == null) { - if (!mounted) return; - showMessage(context, l10n.l_qr_not_found); - setState(() { - _qrState = _QrScanState.failed; - }); - } else { - final data = CredentialData.fromUri(Uri.parse(otpauth)); - _loadCredentialData(data); - } - } catch (e) { - final String errorMessage; - // TODO: Make this cleaner than importing desktop specific RpcError. - if (e is RpcError) { - errorMessage = e.message; - } else { - errorMessage = e.toString(); - } - - if (e is! CancellationException) { - showMessage( - context, - l10n.l_qr_not_read(errorMessage), - duration: const Duration(seconds: 4), - ); - } - setState(() { - _qrState = _QrScanState.failed; - }); - } - } - _loadCredentialData(CredentialData data) { setState(() { _issuerController.text = data.issuer?.trim() ?? ''; @@ -166,8 +117,9 @@ class _OathAddAccountPageState extends ConsumerState { _periodController.text = '${data.period}'; _digitsValues = [data.digits]; _digits = data.digits; + _counter = data.counter; _isObscure = true; - _qrState = _QrScanState.success; + _dataLoaded = true; }); } @@ -175,6 +127,8 @@ class _OathAddAccountPageState extends ConsumerState { {DevicePath? devicePath, required Uri credUri}) async { final l10n = AppLocalizations.of(context)!; try { + FocusUtils.unfocus(context); + if (devicePath == null) { assert(isAndroid, 'devicePath is only optional for Android'); await ref @@ -300,8 +254,6 @@ class _OathAddAccountPageState extends ConsumerState { nameRemaining >= 0 && period > 0; - final qrScanner = ref.watch(qrScannerProvider); - final hashAlgorithms = HashAlgorithm.values .where((alg) => alg != HashAlgorithm.sha512 || @@ -326,6 +278,7 @@ class _OathAddAccountPageState extends ConsumerState { hashAlgorithm: _hashAlgorithm, digits: _digits, period: period, + counter: _counter, ); final devicePath = deviceNode?.path; @@ -364,6 +317,7 @@ class _OathAddAccountPageState extends ConsumerState { ], child: FileDropTarget( onFileDropped: (fileData) async { + final qrScanner = ref.read(qrScannerProvider); if (qrScanner != null) { final b64Image = base64Encode(fileData); final otpauth = await qrScanner.scanQr(b64Image); @@ -371,8 +325,20 @@ class _OathAddAccountPageState extends ConsumerState { if (!mounted) return; showMessage(context, l10n.l_qr_not_found); } else { - final data = CredentialData.fromUri(Uri.parse(otpauth)); - _loadCredentialData(data); + try { + final data = CredentialData.fromOtpauth(Uri.parse(otpauth)); + _loadCredentialData(data); + } catch (e) { + final String errorMessage; + // TODO: Make this cleaner than importing desktop specific RpcError. + if (e is RpcError) { + errorMessage = e.message; + } else { + errorMessage = e.toString(); + } + if (!mounted) return; + showMessage(context, errorMessage); + } } } }, @@ -479,7 +445,7 @@ class _OathAddAccountPageState extends ConsumerState { errorText: _validateSecretLength && !secretLengthValid ? l10n.s_invalid_length : null), - readOnly: _qrState == _QrScanState.success, + readOnly: _dataLoaded, textInputAction: TextInputAction.done, onChanged: (value) { setState(() { @@ -490,25 +456,7 @@ class _OathAddAccountPageState extends ConsumerState { if (isValid) submit(); }, ), - if (isDesktop && qrScanner != null) - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: ActionChip( - avatar: _qrState != _QrScanState.scanning - ? (_qrState == _QrScanState.success - ? const Icon(Icons.qr_code) - : const Icon( - Icons.qr_code_scanner_outlined)) - : const CircularProgressIndicator( - strokeWidth: 2.0), - label: _qrState == _QrScanState.success - ? Text(l10n.l_qr_scanned) - : Text(l10n.s_qr_scan), - onPressed: () { - _scanQrCode(qrScanner); - }), - ), - const Divider(), + const SizedBox(height: 8), Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 4.0, @@ -530,7 +478,7 @@ class _OathAddAccountPageState extends ConsumerState { selected: _oathType != defaultOathType, itemBuilder: (value) => Text(value.getDisplayName(l10n)), - onChanged: _qrState != _QrScanState.success + onChanged: !_dataLoaded ? (value) { setState(() { _oathType = value; @@ -543,7 +491,7 @@ class _OathAddAccountPageState extends ConsumerState { value: _hashAlgorithm, selected: _hashAlgorithm != defaultHashAlgorithm, itemBuilder: (value) => Text(value.displayName), - onChanged: _qrState != _QrScanState.success + onChanged: !_dataLoaded ? (value) { setState(() { _hashAlgorithm = value; @@ -560,7 +508,7 @@ class _OathAddAccountPageState extends ConsumerState { defaultPeriod, itemBuilder: ((value) => Text(l10n.s_num_sec(value))), - onChanged: _qrState != _QrScanState.success + onChanged: !_dataLoaded ? (period) { setState(() { _periodController.text = '$period'; @@ -574,7 +522,7 @@ class _OathAddAccountPageState extends ConsumerState { selected: _digits != defaultDigits, itemBuilder: (value) => Text(l10n.s_num_digits(value)), - onChanged: _qrState != _QrScanState.success + onChanged: !_dataLoaded ? (digits) { setState(() { _digits = digits; diff --git a/lib/oath/views/add_multi_account_page.dart b/lib/oath/views/add_multi_account_page.dart new file mode 100644 index 00000000..b0b05536 --- /dev/null +++ b/lib/oath/views/add_multi_account_page.dart @@ -0,0 +1,327 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:yubico_authenticator/app/logging.dart'; +import 'package:yubico_authenticator/exception/apdu_exception.dart'; + +import '../../android/oath/state.dart'; +import '../../app/models.dart'; +import '../../core/models.dart'; +import '../../desktop/models.dart'; +import '../../widgets/responsive_dialog.dart'; + +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../models.dart'; +import '../../app/state.dart'; +import '../../core/state.dart'; +import '../state.dart'; +import '../../app/message.dart'; + +import '../../exception/cancellation_exception.dart'; +import 'rename_account_dialog.dart'; + +final _log = Logger('oath.views.list_screen'); + +class OathAddMultiAccountPage extends ConsumerStatefulWidget { + final DevicePath? devicePath; + final OathState? state; + final List? credentialsFromUri; + + const OathAddMultiAccountPage( + this.devicePath, this.state, this.credentialsFromUri, + {super.key}); + + @override + ConsumerState createState() => + _OathAddMultiAccountPageState(); +} + +class _OathAddMultiAccountPageState + extends ConsumerState { + int? _numCreds; + + late Map _credStates; + List? _credentials; + + @override + void initState() { + super.initState(); + _credStates = Map.fromIterable(widget.credentialsFromUri!, + value: (v) => (true, false, false)); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context)!; + + if (widget.devicePath != null) { + _credentials = ref + .watch(credentialListProvider(widget.devicePath!)) + ?.map((e) => e.credential) + .toList(); + + _numCreds = ref.watch(credentialListProvider(widget.devicePath!) + .select((value) => value?.length)); + } + + // If the credential is not unique, make sure the checkbox is not checked + checkForDuplicates(); + + return ResponsiveDialog( + title: Text(l10n.s_add_accounts), + actions: [ + TextButton( + onPressed: isValid() ? submit : null, + child: Text(l10n.s_save), + ) + ], + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Text(l10n.l_select_accounts)), + ...widget.credentialsFromUri!.map( + (cred) { + final (checked, touch, unique) = _credStates[cred]!; + return CheckboxListTile( + controlAffinity: ListTileControlAffinity.leading, + secondary: Row(mainAxisSize: MainAxisSize.min, children: [ + if (isTouchSupported()) + Semantics( + label: l10n.s_require_touch, + child: IconButton( + tooltip: l10n.s_require_touch, + color: touch ? colorScheme.primary : null, + onPressed: unique + ? () { + setState(() { + _credStates[cred] = + (checked, !touch, unique); + }); + } + : null, + icon: Icon(touch + ? Icons.touch_app + : Icons.touch_app_outlined)), + ), + Semantics( + label: l10n.s_rename_account, + child: IconButton( + tooltip: l10n.s_rename_account, + onPressed: () async { + final node = ref + .read(currentDeviceDataProvider) + .valueOrNull + ?.node; + final withContext = ref.read(withContextProvider); + CredentialData? renamed = await withContext( + (context) async => await showBlurDialog( + context: context, + builder: (context) => RenameAccountDialog( + device: node!, + issuer: cred.issuer, + name: cred.name, + oathType: cred.oathType, + period: cred.period, + existing: (widget.credentialsFromUri ?? + []) + .map((e) => (e.issuer, e.name)) + .followedBy((_credentials ?? []) + .map((e) => (e.issuer, e.name))) + .toList(), + rename: (issuer, name) async => cred + .copyWith(issuer: issuer, name: name), + ), + )); + if (renamed != null) { + setState(() { + int index = widget.credentialsFromUri!.indexWhere( + (element) => + element.name == cred.name && + (element.issuer == cred.issuer)); + widget.credentialsFromUri![index] = renamed; + _credStates.remove(cred); + _credStates[renamed] = (true, touch, true); + }); + } + }, + icon: IconTheme( + data: IconTheme.of(context), + child: const Icon(Icons.edit_outlined)), + ), + ), + ]), + title: Text(cred.issuer ?? cred.name, + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: false), + value: unique && checked, + enabled: unique, + subtitle: cred.issuer != null || !unique + ? Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (cred.issuer != null) + Text(cred.name, + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: false), + if (!unique) + Text( + l10n.l_account_already_exists, + style: TextStyle( + color: colorScheme.error, + fontSize: 12, // TODO: use Theme + ), + ) + ]) + : null, + onChanged: (bool? value) { + setState(() { + _credStates[cred] = (value == true, touch, unique); + }); + }, + ); + }, + ) + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + )); + } + + bool isTouchSupported() => widget.state?.version.isAtLeast(4, 2) ?? true; + + void checkForDuplicates() { + for (final item in _credStates.entries) { + CredentialData cred = item.key; + final (checked, touch, _) = item.value; + final unique = isUnique(cred); + _credStates[cred] = (checked && unique, touch, unique); + } + } + + bool isUnique(CredentialData cred) { + String nameText = cred.name; + String? issuerText = cred.issuer ?? ''; + bool ans = _credentials + ?.where((element) => + element.name == nameText && + (element.issuer ?? '') == issuerText) + .isEmpty ?? + true; + + return ans; + } + + bool isValid() { + if (widget.state != null) { + final credsToAdd = + _credStates.values.where((element) => element.$1).length; + final capacity = widget.state!.version.isAtLeast(4) ? 32 : null; + return (credsToAdd > 0) && + (capacity == null || (_numCreds! + credsToAdd <= capacity)); + } else { + return true; + } + } + + void submit() async { + final deviceNode = ref.watch(currentDeviceProvider); + if (isAndroid && + (widget.devicePath == null || deviceNode?.transport == Transport.nfc)) { + var uris = []; + var touchRequired = []; + + // build list of uris and touch required flags for unique credentials + for (final item in _credStates.entries) { + CredentialData cred = item.key; + final (checked, touch, _) = item.value; + if (checked) { + uris.add(cred.toUri().toString()); + touchRequired.add(touch); + } + } + + await _addCredentials(uris: uris, touchRequired: touchRequired); + } else { + _credStates.forEach((cred, value) { + if (value.$1) { + accept(cred, value.$2); + } + }); + + Navigator.of(context).pop(); + } + } + + Future _addCredentials( + {required List uris, required List touchRequired}) async { + final l10n = AppLocalizations.of(context)!; + try { + await ref.read(addCredentialsToAnyProvider).call(uris, touchRequired); + if (!mounted) return; + Navigator.of(context).pop(); + showMessage(context, l10n.s_account_added); + } on CancellationException catch (_) { + // ignored + } catch (e) { + _log.error('Failed to add multiple accounts', e.toString()); + final String errorMessage; + if (e is ApduException) { + errorMessage = e.message; + } else { + errorMessage = e.toString(); + } + showMessage( + context, + l10n.l_account_add_failed(errorMessage), + duration: const Duration(seconds: 4), + ); + } + } + + void accept(CredentialData cred, bool touch) async { + final l10n = AppLocalizations.of(context)!; + final devicePath = widget.devicePath; + try { + if (devicePath == null) { + assert(isAndroid, 'devicePath is only optional for Android'); + await ref + .read(addCredentialToAnyProvider) + .call(cred.toUri(), requireTouch: touch); + } else { + await ref + .read(credentialListProvider(devicePath).notifier) + .addAccount(cred.toUri(), requireTouch: touch); + } + if (!mounted) return; + //Navigator.of(context).pop(); + showMessage(context, l10n.s_account_added); + } on CancellationException catch (_) { + // ignored + } catch (e) { + _log.error('Failed to add account', e); + final String errorMessage; + // TODO: Make this cleaner than importing desktop specific RpcError. + if (e is RpcError) { + errorMessage = e.message; + } else if (e is ApduException) { + errorMessage = e.message; + } else { + errorMessage = e.toString(); + } + showMessage( + context, + l10n.l_account_add_failed(errorMessage), + duration: const Duration(seconds: 4), + ); + } + } +} diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index 9578e125..09cf0dac 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -18,19 +18,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:yubico_authenticator/oath/icon_provider/icon_pack_dialog.dart'; +import 'package:yubico_authenticator/oath/views/add_account_dialog.dart'; import '../../app/message.dart'; import '../../app/models.dart'; 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; +import '../state.dart'; import 'add_account_page.dart'; import 'manage_password_dialog.dart'; import 'reset_dialog.dart'; +import 'utils.dart'; Widget oathBuildActions( BuildContext context, @@ -41,100 +43,109 @@ 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)); + + return FsDialog( + child: Column( + children: [ + ActionListSection(l10n.s_setup, children: [ + ActionListItem( + title: l10n.s_add_account, + subtitle: used == null + ? l10n.l_unlock_first + : (capacity != null + ? l10n.l_accounts_used(used, capacity) + : ''), + actionStyle: ActionStyle.primary, + icon: const Icon(Icons.person_add_alt_1_outlined), + onTap: used != null && (capacity == null || capacity > used) + ? (context) async { + final credentials = ref.read(credentialsProvider); + final withContext = ref.read(withContextProvider); + Navigator.of(context).pop(); + if (isAndroid) { + final qrScanner = ref.read(qrScannerProvider); + if (qrScanner != null) { + final qrData = await qrScanner.scanQr(); + if (qrData != null) { + await withContext((context) => handleUri( + context, + credentials, + qrData, + devicePath, + oathState, + l10n, + )); + return; + } + } + await withContext((context) => showBlurDialog( + context: context, + routeSettings: + const RouteSettings(name: 'oath_add_account'), + builder: (context) { + return OathAddAccountPage( + devicePath, + oathState, + credentials: credentials, + credentialData: null, + ); + }, + )); + } else { + await showBlurDialog( + context: context, + builder: (context) => + AddAccountDialog(devicePath, oathState), + ); } - } on CancellationException catch (_) { - // ignored - user cancelled - return; } - } - } - await withContext((context) async { - await 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( + : 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: (context) 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(), + )); + }), + 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: (context) { + Navigator.of(context).pop(); + 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), - ); - }), - ], + builder: (context) => + ManagePasswordDialog(devicePath, oathState), + ); + }), + 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, + onTap: (context) { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ResetDialog(devicePath), + ); + }), + ]), + ], + ), ); } diff --git a/lib/oath/views/manage_password_dialog.dart b/lib/oath/views/manage_password_dialog.dart index 3b95d5c2..536c80e9 100755 --- a/lib/oath/views/manage_password_dialog.dart +++ b/lib/oath/views/manage_password_dialog.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * Copyright (C) 2022-2023 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; +import '../../widgets/focus_utils.dart'; import '../../widgets/responsive_dialog.dart'; import '../models.dart'; import '../state.dart'; @@ -42,6 +43,9 @@ class _ManagePasswordDialogState extends ConsumerState { bool _currentIsWrong = false; _submit() async { + + FocusUtils.unfocus(context); + final result = await ref .read(oathStateProvider(widget.path).notifier) .setPassword(_currentPassword, _newPassword); diff --git a/lib/oath/views/rename_account_dialog.dart b/lib/oath/views/rename_account_dialog.dart index 1e83bf8c..9d0fede4 100755 --- a/lib/oath/views/rename_account_dialog.dart +++ b/lib/oath/views/rename_account_dialog.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * Copyright (C) 2022-2023 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,10 @@ import 'package:logging/logging.dart'; import '../../app/logging.dart'; import '../../app/message.dart'; import '../../app/models.dart'; +import '../../app/state.dart'; import '../../exception/cancellation_exception.dart'; import '../../desktop/models.dart'; +import '../../widgets/focus_utils.dart'; import '../../widgets/responsive_dialog.dart'; import '../../widgets/utf8_utils.dart'; import '../models.dart'; @@ -35,94 +37,121 @@ final _log = Logger('oath.view.rename_account_dialog'); class RenameAccountDialog extends ConsumerStatefulWidget { final DeviceNode device; - final OathCredential credential; - final List? credentials; + final String? issuer; + final String name; + final OathType oathType; + final int period; + final List<(String? issuer, String name)> existing; + final Future Function(String? issuer, String name) rename; - const RenameAccountDialog(this.device, this.credential, this.credentials, - {super.key}); + const RenameAccountDialog({ + required this.device, + required this.issuer, + required this.name, + required this.oathType, + this.period = defaultPeriod, + this.existing = const [], + required this.rename, + super.key, + }); @override ConsumerState createState() => _RenameAccountDialogState(); + + factory RenameAccountDialog.forOathCredential( + WidgetRef ref, + DeviceNode device, + OathCredential credential, + List<(String? issuer, String name)> existing) { + return RenameAccountDialog( + device: device, + issuer: credential.issuer, + name: credential.name, + oathType: credential.oathType, + period: credential.period, + existing: existing, + rename: (issuer, name) async { + final withContext = ref.read(withContextProvider); + try { + // Rename credentials + final renamed = await ref + .read(credentialListProvider(device.path).notifier) + .renameAccount(credential, issuer, name); + + // Update favorite + ref + .read(favoritesProvider.notifier) + .renameCredential(credential.id, renamed.id); + + await withContext((context) async => showMessage( + context, AppLocalizations.of(context)!.s_account_renamed)); + return renamed; + } on CancellationException catch (_) { + // ignored + } catch (e) { + _log.error('Failed to add account', e); + final String errorMessage; + // TODO: Make this cleaner than importing desktop specific RpcError. + if (e is RpcError) { + errorMessage = e.message; + } else { + errorMessage = e.toString(); + } + await withContext((context) async => showMessage( + context, + AppLocalizations.of(context)! + .l_account_add_failed(errorMessage), + duration: const Duration(seconds: 4), + )); + return null; + } + }, + ); + } } class _RenameAccountDialogState extends ConsumerState { late String _issuer; - late String _account; + late String _name; @override void initState() { super.initState(); - _issuer = widget.credential.issuer?.trim() ?? ''; - _account = widget.credential.name.trim(); + _issuer = widget.issuer?.trim() ?? ''; + _name = widget.name.trim(); } void _submit() async { - final l10n = AppLocalizations.of(context)!; - try { - // Rename credentials - final renamed = await ref - .read(credentialListProvider(widget.device.path).notifier) - .renameAccount( - widget.credential, _issuer.isNotEmpty ? _issuer : null, _account); - - // Update favorite - ref - .read(favoritesProvider.notifier) - .renameCredential(widget.credential.id, renamed.id); - - if (!mounted) return; - Navigator.of(context).pop(renamed); - showMessage(context, l10n.s_account_renamed); - } on CancellationException catch (_) { - // ignored - } catch (e) { - _log.error('Failed to add account', e); - final String errorMessage; - // TODO: Make this cleaner than importing desktop specific RpcError. - if (e is RpcError) { - errorMessage = e.message; - } else { - errorMessage = e.toString(); - } - showMessage( - context, - l10n.l_account_add_failed(errorMessage), - duration: const Duration(seconds: 4), - ); - } + FocusUtils.unfocus(context); + final nav = Navigator.of(context); + final renamed = + await widget.rename(_issuer.isNotEmpty ? _issuer : null, _name); + nav.pop(renamed); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final credential = widget.credential; final (issuerRemaining, nameRemaining) = getRemainingKeySpace( - oathType: credential.oathType, - period: credential.period, + oathType: widget.oathType, + period: widget.period, issuer: _issuer, - name: _account, + name: _name, ); - // is this credentials name/issuer pair different from all other? - final isUnique = widget.credentials - ?.where((element) => - element != credential && - element.name == _account && - (element.issuer ?? '') == _issuer) - .isEmpty ?? - false; + // are the name/issuer values different from original + final didChange = (widget.issuer ?? '') != _issuer || widget.name != _name; + + // is this credentials name/issuer pair different from all other, or initial value? + final isUnique = !widget.existing.contains((_issuer, _name)) || !didChange; // is this credential name/issuer of valid format - final isValidFormat = _account.isNotEmpty; - - // are the name/issuer values different from original - final didChange = (widget.credential.issuer ?? '') != _issuer || - widget.credential.name != _account; + final nameNotEmpty = _name.isNotEmpty; // can we rename with the new values - final isValid = isUnique && isValidFormat; + final isValid = isUnique && nameNotEmpty; return ResponsiveDialog( title: Text(l10n.s_rename_account), @@ -138,7 +167,9 @@ class _RenameAccountDialogState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.q_rename_target(getTextName(credential))), + Text(l10n.q_rename_target(widget.issuer != null + ? '${widget.issuer} (${widget.name})' + : widget.name)), Text(l10n.p_rename_will_change_account_displayed), TextFormField( initialValue: _issuer, @@ -161,16 +192,16 @@ class _RenameAccountDialogState extends ConsumerState { }, ), TextFormField( - initialValue: _account, + initialValue: _name, maxLength: nameRemaining, inputFormatters: [limitBytesLength(nameRemaining)], - buildCounter: buildByteCounterFor(_account), + buildCounter: buildByteCounterFor(_name), key: keys.nameField, decoration: InputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_account_name, helperText: '', // Prevents dialog resizing when disabled - errorText: !isValidFormat + errorText: !nameNotEmpty ? l10n.l_account_name_required : !isUnique ? l10n.l_name_already_exists @@ -180,7 +211,7 @@ class _RenameAccountDialogState extends ConsumerState { textInputAction: TextInputAction.done, onChanged: (value) { setState(() { - _account = value.trim(); + _name = value.trim(); }); }, onFieldSubmitted: (_) { diff --git a/lib/oath/views/utils.dart b/lib/oath/views/utils.dart index 131088a0..ee8ebb2f 100755 --- a/lib/oath/views/utils.dart +++ b/lib/oath/views/utils.dart @@ -16,8 +16,16 @@ import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; import '../../widgets/utf8_utils.dart'; +import '../keys.dart'; import '../models.dart'; +import 'add_account_page.dart'; +import 'add_multi_account_page.dart'; /// Calculates the available space for issuer and account name. /// @@ -53,3 +61,39 @@ String getTextName(OathCredential credential) { ? '${credential.issuer} (${credential.name})' : credential.name; } + +Future handleUri( + BuildContext context, + List? credentials, + String qrData, + DevicePath? devicePath, + OathState? state, + AppLocalizations l10n, +) async { + List creds; + try { + creds = CredentialData.fromUri(Uri.parse(qrData)); + } catch (_) { + showMessage(context, l10n.l_invalid_qr); + return; + } + if (creds.isEmpty) { + showMessage(context, l10n.l_qr_not_found); + } else if (creds.length == 1) { + await showBlurDialog( + context: context, + builder: (context) => OathAddAccountPage( + devicePath, + state, + credentials: credentials, + credentialData: creds[0], + ), + ); + } else { + await showBlurDialog( + context: context, + builder: (context) => OathAddMultiAccountPage(devicePath, state, creds, + key: migrateAccountAction), + ); + } +} diff --git a/lib/piv/keys.dart b/lib/piv/keys.dart new file mode 100644 index 00000000..07271b2c --- /dev/null +++ b/lib/piv/keys.dart @@ -0,0 +1,44 @@ +/* + * 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 _keyAction = '$_prefix.actions'; +const _slotAction = '$_prefix.slot.actions'; + +// 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 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..d54eaeee --- /dev/null +++ b/lib/piv/models.dart @@ -0,0 +1,312 @@ +/* + * 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.eccp256; +const defaultGenerateType = GenerateType.certificate; + +enum GenerateType { + certificate, + csr; + + String getDisplayName(AppLocalizations l10n) { + return switch (this) { + GenerateType.certificate => l10n.s_certificate, + GenerateType.csr => l10n.s_csr, + }; + } +} + +enum SlotId { + authentication(0x9a), + signature(0x9c), + keyManagement(0x9d), + cardAuth(0x9e); + + 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) { + 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), + }; + } + + 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: Should these be translatable? + _ => name.toUpperCase() + }; + } +} + +enum ManagementKeyType { + @JsonValue(0x03) + tdes(24), + @JsonValue(0x08) + aes128(16), + @JsonValue(0x0A) + aes192(24), + @JsonValue(0x0C) + aes256(32); + + const ManagementKeyType(this.keyLength); + final int keyLength; + + int get value => _$ManagementKeyTypeEnumMap[this]!; + + String getDisplayName(AppLocalizations l10n) { + return switch (this) { + // TODO: Should these be translatable? + _ => name.toUpperCase() + }; + } +} + +@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 KeyType? keyType, + required CertInfo? certInfo, + }) = _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..9352b2d4 --- /dev/null +++ b/lib/piv/models.freezed.dart @@ -0,0 +1,3005 @@ +// 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, KeyType? keyType, CertInfo? certInfo) + result, + required TResult Function() invalidPassword, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo)? + result, + TResult? Function()? invalidPassword, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(bool password, KeyType? keyType, CertInfo? certInfo)? + 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, KeyType? keyType, CertInfo? certInfo}); + + $CertInfoCopyWith<$Res>? get certInfo; +} + +/// @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? keyType = freezed, + Object? certInfo = freezed, + }) { + return _then(_$_ExamineResult( + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as bool, + keyType: freezed == keyType + ? _value.keyType + : keyType // ignore: cast_nullable_to_non_nullable + as KeyType?, + certInfo: freezed == certInfo + ? _value.certInfo + : certInfo // ignore: cast_nullable_to_non_nullable + as CertInfo?, + )); + } + + @override + @pragma('vm:prefer-inline') + $CertInfoCopyWith<$Res>? get certInfo { + if (_value.certInfo == null) { + return null; + } + + return $CertInfoCopyWith<$Res>(_value.certInfo!, (value) { + return _then(_value.copyWith(certInfo: value)); + }); + } +} + +/// @nodoc +@JsonSerializable() +class _$_ExamineResult implements _ExamineResult { + _$_ExamineResult( + {required this.password, + required this.keyType, + required this.certInfo, + final String? $type}) + : $type = $type ?? 'result'; + + factory _$_ExamineResult.fromJson(Map json) => + _$$_ExamineResultFromJson(json); + + @override + final bool password; + @override + final KeyType? keyType; + @override + final CertInfo? certInfo; + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + String toString() { + return 'PivExamineResult.result(password: $password, keyType: $keyType, certInfo: $certInfo)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_ExamineResult && + (identical(other.password, password) || + other.password == password) && + (identical(other.keyType, keyType) || other.keyType == keyType) && + (identical(other.certInfo, certInfo) || + other.certInfo == certInfo)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, password, keyType, certInfo); + + @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, KeyType? keyType, CertInfo? certInfo) + result, + required TResult Function() invalidPassword, + }) { + return result(password, keyType, certInfo); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo)? + result, + TResult? Function()? invalidPassword, + }) { + return result?.call(password, keyType, certInfo); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(bool password, KeyType? keyType, CertInfo? certInfo)? + result, + TResult Function()? invalidPassword, + required TResult orElse(), + }) { + if (result != null) { + return result(password, keyType, certInfo); + } + 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 KeyType? keyType, + required final CertInfo? certInfo}) = _$_ExamineResult; + + factory _ExamineResult.fromJson(Map json) = + _$_ExamineResult.fromJson; + + bool get password; + KeyType? get keyType; + CertInfo? get certInfo; + @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, KeyType? keyType, CertInfo? certInfo) + result, + required TResult Function() invalidPassword, + }) { + return invalidPassword(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo)? + result, + TResult? Function()? invalidPassword, + }) { + return invalidPassword?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(bool password, KeyType? keyType, CertInfo? certInfo)? + 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..763d8393 --- /dev/null +++ b/lib/piv/models.g.dart @@ -0,0 +1,230 @@ +// 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, + keyType: $enumDecodeNullable(_$KeyTypeEnumMap, json['key_type']), + certInfo: json['cert_info'] == null + ? null + : CertInfo.fromJson(json['cert_info'] as Map), + $type: json['runtimeType'] as String?, + ); + +Map _$$_ExamineResultToJson(_$_ExamineResult instance) => + { + 'password': instance.password, + 'key_type': _$KeyTypeEnumMap[instance.keyType], + 'cert_info': instance.certInfo, + '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..ec3361b5 --- /dev/null +++ b/lib/piv/state.dart @@ -0,0 +1,71 @@ +/* + * 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 validateRfc4514(String value); + 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..47ac2a32 --- /dev/null +++ b/lib/piv/views/actions.dart @@ -0,0 +1,259 @@ +/* + * 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: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 '../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 GenerateIntent extends Intent { + const GenerateIntent(); +} + +class ImportIntent extends Intent { + const ImportIntent(); +} + +class ExportIntent extends Intent { + const ExportIntent(); +} + +Future _authenticate( + BuildContext context, DevicePath devicePath, PivState pivState) async { + return await showBlurDialog( + context: context, + builder: (context) => pivState.protectedKey + ? PinDialog(devicePath) + : AuthenticationDialog( + devicePath, + pivState, + ), + ) ?? + false; +} + +Future _authIfNeeded( + BuildContext context, DevicePath devicePath, PivState pivState) async { + if (pivState.needsAuth) { + return await _authenticate(context, 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: { + GenerateIntent: + CallbackAction(onInvoke: (intent) async { + final withContext = ref.read(withContextProvider); + if (!pivState.protectedKey && + !await withContext( + (context) => _authIfNeeded(context, devicePath, pivState))) { + return false; + } + + // 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 l10n = AppLocalizations.of(context)!; + 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: l10n.l_export_csr_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 { + final withContext = ref.read(withContextProvider); + + if (!await withContext( + (context) => _authIfNeeded(context, devicePath, pivState))) { + return false; + } + + 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; + } + + 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 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; + } + + final file = File(filePath); + await file.writeAsString(cert, flush: true); + + await withContext((context) async { + final l10n = AppLocalizations.of(context)!; + showMessage(context, l10n.l_certificate_exported); + }); + return true; + }), + DeleteIntent: CallbackAction(onInvoke: (_) async { + final withContext = ref.read(withContextProvider); + if (!await withContext( + (context) => _authIfNeeded(context, devicePath, pivState))) { + return false; + } + + final bool? deleted = await withContext((context) async => + await showBlurDialog( + context: context, + builder: (context) => DeleteCertificateDialog( + devicePath, + pivSlot, + ), + ) ?? + false); + 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/authentication_dialog.dart b/lib/piv/views/authentication_dialog.dart new file mode 100644 index 00000000..7132910e --- /dev/null +++ b/lib/piv/views/authentication_dialog.dart @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../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 { + bool _defaultKeyUsed = false; + bool _keyIsWrong = false; + final _keyController = TextEditingController(); + + @override + void dispose() { + _keyController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final hasMetadata = widget.pivState.metadata != null; + final keyLen = (widget.pivState.metadata?.managementKeyMetadata.keyType ?? + ManagementKeyType.tdes) + .keyLength * + 2; + return ResponsiveDialog( + title: Text(l10n.l_unlock_piv_management), + actions: [ + TextButton( + key: keys.unlockButton, + onPressed: _keyController.text.length == keyLen + ? () async { + final navigator = Navigator.of(context); + try { + final status = await ref + .read(pivStateProvider(widget.devicePath).notifier) + .authenticate(_keyController.text); + 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), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.p_unlock_piv_management_desc), + TextField( + key: keys.managementKeyField, + autofocus: true, + autofillHints: const [AutofillHints.password], + controller: _keyController, + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp('[a-f0-9]', caseSensitive: false)) + ], + readOnly: _defaultKeyUsed, + maxLength: !_defaultKeyUsed ? keyLen : null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_management_key, + prefixIcon: const Icon(Icons.key_outlined), + errorText: _keyIsWrong ? l10n.l_wrong_key : null, + errorMaxLines: 3, + helperText: _defaultKeyUsed ? l10n.l_default_key_used : null, + suffixIcon: hasMetadata + ? null + : IconButton( + icon: Icon(_defaultKeyUsed + ? Icons.auto_awesome + : Icons.auto_awesome_outlined), + tooltip: l10n.s_use_default, + onPressed: () { + setState(() { + _defaultKeyUsed = !_defaultKeyUsed; + if (_defaultKeyUsed) { + _keyController.text = defaultManagementKey; + } else { + _keyController.clear(); + } + }); + }, + ), + ), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _keyIsWrong = false; + }); + }, + ), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/cert_info_view.dart b/lib/piv/views/cert_info_view.dart new file mode 100644 index 00000000..311f23cb --- /dev/null +++ b/lib/piv/views/cert_info_view.dart @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; + +import '../../app/message.dart'; +import '../../app/state.dart'; +import '../../widgets/tooltip_if_truncated.dart'; +import '../models.dart'; + +class CertInfoTable extends ConsumerWidget { + final CertInfo certInfo; + + const CertInfoTable(this.certInfo, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + final textTheme = Theme.of(context).textTheme; + // This is what ListTile uses for subtitle + final subtitleStyle = textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ); + final dateFormat = + DateFormat.yMMMEd(ref.watch(currentLocaleProvider).toString()); + final clipboard = ref.watch(clipboardProvider); + final withContext = ref.watch(withContextProvider); + + Widget header(String title) => Text( + title, + textAlign: TextAlign.right, + ); + + Widget body(String title, String value) => GestureDetector( + onDoubleTap: () async { + await clipboard.setText(value); + if (!clipboard.platformGivesFeedback()) { + await withContext((context) async { + showMessage(context, l10n.p_target_copied_clipboard(title)); + }); + } + }, + child: TooltipIfTruncated( + text: value, + style: subtitleStyle, + tooltip: value.replaceAllMapped( + RegExp(r',([A-Z]+)='), (match) => '\n${match[1]}='), + ), + ); + + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + header(l10n.s_subject), + header(l10n.s_issuer), + header(l10n.s_serial), + header(l10n.s_certificate_fingerprint), + header(l10n.s_valid_from), + header(l10n.s_valid_to), + ], + ), + const SizedBox(width: 8), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + body(l10n.s_subject, certInfo.subject), + body(l10n.s_issuer, certInfo.issuer), + body(l10n.s_serial, certInfo.serial), + body(l10n.s_certificate_fingerprint, certInfo.fingerprint), + body(l10n.s_valid_from, + dateFormat.format(DateTime.parse(certInfo.notValidBefore))), + body(l10n.s_valid_to, + dateFormat.format(DateTime.parse(certInfo.notValidAfter))), + ], + ), + ), + ], + ); + } +} diff --git a/lib/piv/views/delete_certificate_dialog.dart b/lib/piv/views/delete_certificate_dialog.dart new file mode 100644 index 00000000..cc239ac3 --- /dev/null +++ b/lib/piv/views/delete_certificate_dialog.dart @@ -0,0 +1,79 @@ +/* + * 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.l_delete_certificate), + 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.l_certificate_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_certificate), + Text(l10n.q_delete_certificate_confirm( + pivSlot.slot.getDisplayName(l10n))), + ] + .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..77d174f0 --- /dev/null +++ b/lib/piv/views/generate_key_dialog.dart @@ -0,0 +1,248 @@ +/* + * 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 '../../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; +import 'overwrite_confirm_dialog.dart'; + +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 = ''; + bool _invalidSubject = true; + GenerateType _generateType = defaultGenerateType; + KeyType _keyType = defaultKeyType; + late DateTime _validFrom; + late DateTime _validTo; + late DateTime _validToDefault; + late DateTime _validToMax; + bool _generating = false; + + @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 textTheme = Theme.of(context).textTheme; + // This is what ListTile uses for subtitle + final subtitleStyle = textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ); + + return ResponsiveDialog( + allowCancel: !_generating, + title: Text(l10n.s_generate_key), + actions: [ + TextButton( + key: keys.saveButton, + onPressed: _generating || _invalidSubject + ? null + : () async { + if (!await confirmOverwrite( + context, + widget.pivSlot, + writeKey: true, + writeCert: _generateType == GenerateType.certificate, + )) { + return; + } + + setState(() { + _generating = true; + }); + + final pivNotifier = + ref.read(pivSlotsProvider(widget.devicePath).notifier); + final withContext = ref.read(withContextProvider); + + if (!await pivNotifier.validateRfc4514(_subject)) { + setState(() { + _generating = false; + }); + _invalidSubject = true; + return; + } + + void Function()? close; + final PivGenerateResult result; + try { + close = await withContext( + (context) async => showMessage( + context, + l10n.l_generating_private_key, + duration: const Duration(seconds: 30), + )); + result = await pivNotifier.generate( + widget.pivSlot.slot, + _keyType, + parameters: switch (_generateType) { + GenerateType.certificate => + PivGenerateParameters.certificate( + subject: _subject, + validFrom: _validFrom, + validTo: _validTo), + GenerateType.csr => + PivGenerateParameters.csr(subject: _subject), + }, + ); + } 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), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.p_generate_desc(widget.pivSlot.slot.getDisplayName(l10n))), + Text( + l10n.s_subject, + style: textTheme.bodyLarge, + ), + Text(l10n.p_subject_desc), + TextField( + autofocus: true, + key: keys.subjectField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_subject, + errorText: _subject.isNotEmpty && _invalidSubject + ? l10n.l_rfc4514_invalid + : null), + textInputAction: TextInputAction.next, + enabled: !_generating, + onChanged: (value) { + setState(() { + _invalidSubject = value.isEmpty; + _subject = value; + }); + }, + ), + Text( + l10n.rfc4514_examples, + style: subtitleStyle, + ), + Text( + l10n.s_options, + style: textTheme.bodyLarge, + ), + Text(l10n.p_cert_options_desc), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4.0, + runSpacing: 8.0, + children: [ + ChoiceFilterChip( + items: KeyType.values, + value: _keyType, + selected: _keyType != defaultKeyType, + itemBuilder: (value) => Text(value.getDisplayName(l10n)), + onChanged: _generating + ? null + : (value) { + setState(() { + _keyType = value; + }); + }, + ), + ChoiceFilterChip( + items: GenerateType.values, + value: _generateType, + selected: _generateType != defaultGenerateType, + itemBuilder: (value) => Text(value.getDisplayName(l10n)), + onChanged: _generating + ? null + : (value) { + setState(() { + _generateType = value; + }); + }, + ), + if (_generateType == GenerateType.certificate) + FilterChip( + label: Text(dateFormatter.format(_validTo)), + onSelected: _generating + ? null + : (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..e21c2304 --- /dev/null +++ b/lib/piv/views/import_file_dialog.dart @@ -0,0 +1,258 @@ +/* + * 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/message.dart'; +import '../../app/models.dart'; +import '../../app/state.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; +import '../state.dart'; +import '../keys.dart' as keys; +import 'cert_info_view.dart'; +import 'overwrite_confirm_dialog.dart'; + +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; + bool _importing = 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 textTheme = Theme.of(context).textTheme; + // This is what ListTile uses for subtitle + final subtitleStyle = textTheme.bodyMedium!.copyWith( + color: textTheme.bodySmall!.color, + ); + final state = _state; + if (state == null) { + return ResponsiveDialog( + title: Text(l10n.l_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(l10n.l_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: [ + Text(l10n.p_password_protected_file), + TextField( + autofocus: true, + obscureText: true, + autofillHints: const [AutofillHints.password], + key: keys.managementKeyField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_password, + prefixIcon: const Icon(Icons.password_outlined), + errorText: _passwordIsWrong ? l10n.s_wrong_password : null, + errorMaxLines: 3), + 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: (_, keyType, certInfo) => ResponsiveDialog( + title: Text(l10n.l_import_file), + actions: [ + TextButton( + key: keys.unlockButton, + onPressed: (keyType == null && certInfo == null) || _importing + ? null + : () async { + final withContext = ref.read(withContextProvider); + + if (!await confirmOverwrite( + context, + widget.pivSlot, + writeKey: keyType != null, + writeCert: certInfo != null, + )) { + return; + } + + setState(() { + _importing = true; + }); + + void Function()? close; + try { + close = await withContext( + (context) async => showMessage( + context, + l10n.l_importing_file, + duration: const Duration(seconds: 30), + )); + await ref + .read(pivSlotsProvider(widget.devicePath).notifier) + .import(widget.pivSlot.slot, _data, + password: + _password.isNotEmpty ? _password : null); + await withContext( + (context) async { + Navigator.of(context).pop(true); + showMessage(context, l10n.s_file_imported); + }, + ); + } catch (err) { + // TODO: More error cases + setState(() { + _passwordIsWrong = true; + _importing = false; + }); + } finally { + close?.call(); + } + }, + child: Text(l10n.s_import), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.p_import_items_desc( + widget.pivSlot.slot.getDisplayName(l10n))), + if (keyType != null) ...[ + Text( + l10n.s_private_key, + style: textTheme.bodyLarge, + softWrap: true, + textAlign: TextAlign.center, + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(l10n.s_algorithm), + const SizedBox(width: 8), + Text( + keyType.name.toUpperCase(), + style: subtitleStyle, + ), + ], + ) + ], + if (certInfo != null) ...[ + Text( + l10n.s_certificate, + style: textTheme.bodyLarge, + softWrap: true, + textAlign: TextAlign.center, + ), + SizedBox( + height: 120, // Needed for layout, adapt if text sizes changes + child: CertInfoTable(certInfo), + ), + ] + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + 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..360586b0 --- /dev/null +++ b/lib/piv/views/key_actions.dart @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../app/views/fs_dialog.dart'; +import '../../app/views/action_list.dart'; +import '../models.dart'; +import '../keys.dart' as keys; +import '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 colors = Theme.of(context).buttonTheme.colorScheme ?? + Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context)!; + + final usingDefaultMgmtKey = + pivState.metadata?.managementKeyMetadata.defaultValue == true; + + final pinBlocked = pivState.pinAttempts == 0; + final pukAttempts = pivState.metadata?.pukMetadata.attemptsRemaining; + final alertIcon = Icon(Icons.warning_amber, color: colors.tertiary); + + return FsDialog( + child: Column( + children: [ + ActionListSection( + l10n.s_manage, + children: [ + ActionListItem( + key: keys.managePinAction, + title: l10n.s_pin, + subtitle: pinBlocked + ? (pukAttempts != 0 + ? l10n.l_piv_pin_blocked + : l10n.l_piv_pin_puk_blocked) + : l10n.l_attempts_remaining(pivState.pinAttempts), + icon: const Icon(Icons.pin_outlined), + trailing: pinBlocked ? alertIcon : null, + onTap: !(pinBlocked && pukAttempts == 0) + ? (context) { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ManagePinPukDialog( + devicePath, + target: pinBlocked + ? ManageTarget.unblock + : ManageTarget.pin, + ), + ); + } + : null), + ActionListItem( + key: keys.managePukAction, + title: l10n.s_puk, + subtitle: pukAttempts != null + ? (pukAttempts == 0 + ? l10n.l_piv_pin_puk_blocked + : l10n.l_attempts_remaining(pukAttempts)) + : null, + icon: const Icon(Icons.pin_outlined), + trailing: pukAttempts == 0 ? alertIcon : null, + onTap: pukAttempts != 0 + ? (context) { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ManagePinPukDialog(devicePath, + target: ManageTarget.puk), + ); + } + : null), + ActionListItem( + key: keys.manageManagementKeyAction, + title: l10n.s_management_key, + 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 ? alertIcon : null, + onTap: (context) { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ManageKeyDialog(devicePath, pivState), + ); + }), + 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, + onTap: (context) { + Navigator.of(context).pop(); + showBlurDialog( + context: context, + builder: (context) => ResetDialog(devicePath), + ); + }) + ], + ), + // TODO + /* + 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/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart new file mode 100644 index 00000000..fdb2e6ae --- /dev/null +++ b/lib/piv/views/manage_key_dialog.dart @@ -0,0 +1,300 @@ +/* + * 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:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../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 _hasMetadata; + late bool _defaultKeyUsed; + late bool _usesStoredKey; + late bool _storeKey; + bool _currentIsWrong = false; + int _attemptsRemaining = -1; + ManagementKeyType _keyType = ManagementKeyType.tdes; + final _currentController = TextEditingController(); + final _keyController = TextEditingController(); + + @override + void initState() { + super.initState(); + + _hasMetadata = widget.pivState.metadata != null; + _defaultKeyUsed = + widget.pivState.metadata?.managementKeyMetadata.defaultValue ?? false; + _usesStoredKey = widget.pivState.protectedKey; + if (!_usesStoredKey && _defaultKeyUsed) { + _currentController.text = defaultManagementKey; + } + _storeKey = _usesStoredKey; + } + + @override + void dispose() { + _keyController.dispose(); + _currentController.dispose(); + super.dispose(); + } + + _submit() async { + final notifier = ref.read(pivStateProvider(widget.path).notifier); + if (_usesStoredKey) { + final status = (await notifier.verifyPin(_currentController.text)).when( + success: () => true, + failure: (attemptsRemaining) { + setState(() { + _attemptsRemaining = attemptsRemaining; + _currentIsWrong = true; + }); + return false; + }, + ); + if (!status) { + return; + } + } else { + if (!await notifier.authenticate(_currentController.text)) { + 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; + } + } + + await notifier.setManagementKey(_keyController.text, + managementKeyType: _keyType, storeKey: _storeKey); + if (!mounted) return; + + final l10n = AppLocalizations.of(context)!; + showMessage(context, l10n.l_management_key_changed); + + Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final currentType = + widget.pivState.metadata?.managementKeyMetadata.keyType ?? + ManagementKeyType.tdes; + final hexLength = _keyType.keyLength * 2; + final protected = widget.pivState.protectedKey; + final currentKeyOrPin = _currentController.text; + final currentLenOk = protected + ? currentKeyOrPin.length >= 4 + : currentKeyOrPin.length == currentType.keyLength * 2; + final newLenOk = _keyController.text.length == hexLength; + + return ResponsiveDialog( + title: Text(l10n.l_change_management_key), + actions: [ + TextButton( + onPressed: currentLenOk && newLenOk ? _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_change_management_key_desc), + if (protected) + TextField( + autofocus: true, + obscureText: true, + autofillHints: const [AutofillHints.password], + key: keys.pinPukField, + maxLength: 8, + controller: _currentController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_pin, + prefixIcon: const Icon(Icons.pin_outlined), + errorText: _currentIsWrong + ? l10n + .l_wrong_pin_attempts_remaining(_attemptsRemaining) + : null, + errorMaxLines: 3), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _currentIsWrong = false; + }); + }, + ), + if (!protected) + TextFormField( + key: keys.managementKeyField, + autofocus: !_defaultKeyUsed, + autofillHints: const [AutofillHints.password], + controller: _currentController, + readOnly: _defaultKeyUsed, + maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_current_management_key, + prefixIcon: const Icon(Icons.key_outlined), + errorText: _currentIsWrong ? l10n.l_wrong_key : null, + errorMaxLines: 3, + helperText: _defaultKeyUsed ? l10n.l_default_key_used : null, + suffixIcon: _hasMetadata + ? null + : IconButton( + icon: Icon(_defaultKeyUsed + ? Icons.auto_awesome + : Icons.auto_awesome_outlined), + tooltip: l10n.s_use_default, + onPressed: () { + setState(() { + _defaultKeyUsed = !_defaultKeyUsed; + if (_defaultKeyUsed) { + _currentController.text = defaultManagementKey; + } else { + _currentController.clear(); + } + }); + }, + ), + ), + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp('[a-f0-9]', caseSensitive: false)) + ], + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _currentIsWrong = false; + }); + }, + ), + TextField( + key: keys.newPinPukField, + autofocus: _defaultKeyUsed, + autofillHints: const [AutofillHints.newPassword], + maxLength: hexLength, + controller: _keyController, + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp('[a-f0-9]', caseSensitive: false)) + ], + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_new_management_key, + prefixIcon: const Icon(Icons.key_outlined), + enabled: currentLenOk, + suffixIcon: IconButton( + icon: const Icon(Icons.refresh), + tooltip: l10n.s_generate_random, + onPressed: currentLenOk + ? () { + final random = Random.secure(); + final key = List.generate( + _keyType.keyLength, + (_) => random + .nextInt(256) + .toRadixString(16) + .padLeft(2, '0')).join(); + setState(() { + _keyController.text = key; + }); + } + : null, + ), + ), + textInputAction: TextInputAction.next, + onSubmitted: (_) { + if (currentLenOk && newLenOk) { + _submit(); + } + }, + ), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4.0, + runSpacing: 8.0, + children: [ + if (widget.pivState.metadata != null) + ChoiceFilterChip( + items: ManagementKeyType.values, + value: _keyType, + selected: _keyType != defaultManagementKeyType, + itemBuilder: (value) => Text(value.getDisplayName(l10n)), + onChanged: (value) { + setState(() { + _keyType = value; + }); + }, + ), + FilterChip( + label: Text(l10n.s_protect_key), + 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..7cb67950 --- /dev/null +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -0,0 +1,195 @@ +/* + * 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 '../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(); +} + +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 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, + switch (widget.target) { + ManageTarget.puk => l10n.s_puk_set, + _ => l10n.s_pin_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 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), + 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: [ + //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( + border: const OutlineInputBorder(), + labelText: widget.target == ManageTarget.pin + ? l10n.s_current_pin + : l10n.s_current_puk, + prefixIcon: const Icon(Icons.password_outlined), + errorText: _currentIsWrong + ? (widget.target == ManageTarget.pin + ? l10n.l_wrong_pin_attempts_remaining( + _attemptsRemaining) + : l10n.l_wrong_puk_attempts_remaining( + _attemptsRemaining)) + : null, + errorMaxLines: 3), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _currentIsWrong = false; + _currentPin = value; + }); + }, + ), + Text(l10n.p_enter_new_piv_pin_puk( + widget.target == ManageTarget.puk ? l10n.s_puk : l10n.s_pin)), + TextField( + key: keys.newPinPukField, + obscureText: true, + maxLength: 8, + autofillHints: const [AutofillHints.newPassword], + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: widget.target == ManageTarget.puk + ? l10n.s_new_puk + : l10n.s_new_pin, + prefixIcon: const Icon(Icons.password_outlined), + // Old YubiKeys allowed a 4 digit PIN + enabled: _currentPin.length >= 4, + ), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _newPin = value; + }); + }, + onSubmitted: (_) { + if (isValid) { + _submit(); + } + }, + ), + TextField( + key: keys.confirmPinPukField, + obscureText: true, + maxLength: 8, + autofillHints: const [AutofillHints.newPassword], + decoration: InputDecoration( + border: const OutlineInputBorder(), + 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, + ), + 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/overwrite_confirm_dialog.dart b/lib/piv/views/overwrite_confirm_dialog.dart new file mode 100644 index 00000000..50ade8ab --- /dev/null +++ b/lib/piv/views/overwrite_confirm_dialog.dart @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../../app/message.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../models.dart'; + +class _OverwriteConfirmDialog extends StatelessWidget { + final SlotId slot; + final bool certificate; + final bool? privateKey; + + const _OverwriteConfirmDialog({ + required this.certificate, + required this.privateKey, + required this.slot, + }); + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return ResponsiveDialog( + title: Text(l10n.s_overwrite_slot), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text(l10n.s_overwrite)), + ], + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.p_overwrite_slot_desc(slot.getDisplayName(l10n))), + const SizedBox(height: 12), + if (certificate) Text(l10n.l_bullet(l10n.l_overwrite_cert)), + if (privateKey == true) Text(l10n.l_bullet(l10n.l_overwrite_key)), + if (privateKey == null) + Text(l10n.l_bullet(l10n.l_overwrite_key_maybe)), + ], + ), + ), + ); + } +} + +Future confirmOverwrite( + BuildContext context, + PivSlot pivSlot, { + required bool writeKey, + required bool writeCert, +}) async { + final overwritesCert = writeCert && pivSlot.certInfo != null; + final overwritesKey = writeKey ? pivSlot.hasKey : false; + if (overwritesCert || overwritesKey != false) { + return await showBlurDialog( + context: context, + builder: (context) => _OverwriteConfirmDialog( + slot: pivSlot.slot, + certificate: overwritesCert, + privateKey: overwritesKey, + )) ?? + false; + } + return true; +} diff --git a/lib/piv/views/pin_dialog.dart b/lib/piv/views/pin_dialog.dart new file mode 100644 index 00000000..7f849e5f --- /dev/null +++ b/lib/piv/views/pin_dialog.dart @@ -0,0 +1,133 @@ +/* + * 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 { + final _pinController = TextEditingController(); + bool _pinIsWrong = false; + int _attemptsRemaining = -1; + bool _isObscure = true; + + @override + void dispose() { + _pinController.dispose(); + super.dispose(); + } + + Future _submit() async { + final navigator = Navigator.of(context); + try { + final status = await ref + .read(pivStateProvider(widget.devicePath).notifier) + .verifyPin(_pinController.text); + status.when( + success: () { + navigator.pop(true); + }, + failure: (attemptsRemaining) { + setState(() { + _pinController.clear(); + _attemptsRemaining = attemptsRemaining; + _pinIsWrong = true; + }); + }, + ); + } on CancellationException catch (_) { + navigator.pop(false); + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return ResponsiveDialog( + title: Text(l10n.s_pin_required), + actions: [ + TextButton( + key: keys.unlockButton, + onPressed: _pinController.text.length >= 4 ? _submit : null, + child: Text(l10n.s_unlock), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.p_pin_required_desc), + TextField( + autofocus: true, + obscureText: _isObscure, + maxLength: 8, + autofillHints: const [AutofillHints.password], + key: keys.managementKeyField, + controller: _pinController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.s_pin, + prefixIcon: const Icon(Icons.pin_outlined), + errorText: _pinIsWrong + ? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining) + : null, + errorMaxLines: 3, + suffixIcon: IconButton( + icon: Icon( + _isObscure ? Icons.visibility : Icons.visibility_off, + color: IconTheme.of(context).color, + ), + onPressed: () { + setState(() { + _isObscure = !_isObscure; + }); + }, + ), + ), + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() { + _pinIsWrong = false; + }); + }, + 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..1ad9aa0a --- /dev/null +++ b/lib/piv/views/piv_screen.dart @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2022 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../app/message.dart'; +import '../../app/models.dart'; +import '../../app/shortcuts.dart'; +import '../../app/views/app_failure_page.dart'; +import '../../app/views/app_list_item.dart'; +import '../../app/views/app_page.dart'; +import '../../app/views/message_page.dart'; +import '../../widgets/list_title.dart'; +import '../models.dart'; +import '../state.dart'; +import 'actions.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_piv), + graphic: const CircularProgressIndicator(), + delayedContent: true, + ), + error: (error, _) => AppFailurePage( + title: Text(l10n.s_piv), + cause: error, + ), + data: (pivState) { + final pivSlots = ref.watch(pivSlotsProvider(devicePath)).asData; + return AppPage( + title: Text(l10n.s_piv), + keyActionsBuilder: (context) => + pivBuildActions(context, devicePath, pivState, ref), + child: Column( + children: [ + ListTitle(l10n.s_certificates), + if (pivSlots?.hasValue == true) + ...pivSlots!.value.map((e) => registerPivActions( + devicePath, + pivState, + e, + ref: ref, + actions: { + OpenIntent: + CallbackAction(onInvoke: (_) async { + await showBlurDialog( + context: context, + barrierColor: Colors.transparent, + builder: (context) => SlotDialog(e.slot), + ); + return null; + }), + }, + builder: (context) => _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 Semantics( + label: slot.getDisplayName(l10n), + child: AppListItem( + leading: CircleAvatar( + foregroundColor: colorScheme.onSecondary, + backgroundColor: colorScheme.secondary, + child: const Icon(Icons.approval), + ), + title: slot.getDisplayName(l10n), + subtitle: certInfo != null + // Simplify subtitle by stripping "CN=", etc. + ? certInfo.subject.replaceAll(RegExp(r'[A-Z]+='), ' ').trimLeft() + : pivSlot.hasKey == true + ? l10n.l_key_no_certificate + : l10n.l_no_certificate, + trailing: OutlinedButton( + onPressed: Actions.handler(context, const OpenIntent()), + child: const Icon(Icons.more_horiz), + ), + buildPopupActions: (context) => + buildSlotActions(certInfo != null, l10n), + )); + } +} diff --git a/lib/piv/views/reset_dialog.dart b/lib/piv/views/reset_dialog.dart new file mode 100644 index 00000000..50b0c12f --- /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_piv_app_reset); + }); + }, + child: Text(l10n.s_reset), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + children: [ + Text( + l10n.p_warning_piv_reset, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text(l10n.p_warning_piv_reset_desc), + ] + .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..31f0f460 --- /dev/null +++ b/lib/piv/views/slot_dialog.dart @@ -0,0 +1,112 @@ +/* + * 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/state.dart'; +import '../../app/views/fs_dialog.dart'; +import '../../app/views/action_list.dart'; +import '../models.dart'; +import '../state.dart'; +import 'actions.dart'; +import 'cert_info_view.dart'; + +class SlotDialog extends ConsumerWidget { + final SlotId pivSlot; + const SlotDialog(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; + // 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) => + value.whenOrNull( + data: (data) => + data.firstWhere((element) => element.slot == pivSlot)))); + + if (pivState == null || 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: 16), + child: Column( + children: [ + Text( + pivSlot.getDisplayName(l10n), + style: textTheme.headlineSmall, + softWrap: true, + textAlign: TextAlign.center, + ), + if (certInfo != null) ...[ + Padding( + padding: const EdgeInsets.all(16), + child: CertInfoTable(certInfo), + ), + ] else ...[ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + l10n.l_no_certificate, + softWrap: true, + textAlign: TextAlign.center, + style: subtitleStyle, + ), + ), + const SizedBox(height: 16), + ], + ], + ), + ), + ActionListSection.fromMenuActions( + context, + l10n.s_actions, + actions: buildSlotActions(certInfo != null, l10n), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/theme.dart b/lib/theme.dart index 8f99e5ad..aef04bd2 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,13 +33,21 @@ class AppTheme { ).copyWith( primary: primaryBlue, //secondary: accentGreen, + secondary: Colors.grey.shade400, + onSecondary: Colors.grey.shade800, + tertiary: amber.withOpacity(0.7), + error: darkRed, + onError: Colors.white.withOpacity(0.9), ), textTheme: TextTheme( - bodySmall: TextStyle(color: Colors.grey.shade900), + bodySmall: TextStyle(color: Colors.grey.shade600), ), dialogTheme: const DialogTheme( surfaceTintColor: Colors.white70, ), + tooltipTheme: const TooltipThemeData( + waitDuration: Duration(seconds: 1), + ), ); static ThemeData get darkTheme => ThemeData( @@ -50,13 +59,13 @@ class AppTheme { ).copyWith( primary: primaryGreen, //onPrimary: Colors.grey.shade900, - //secondary: accentGreen, - //secondary: const Color(0xff5d7d90), + secondary: Colors.grey.shade400, //onSecondary: Colors.grey.shade900, //primaryContainer: Colors.grey.shade800, //onPrimaryContainer: Colors.grey.shade100, error: darkRed, onError: Colors.white.withOpacity(0.9), + tertiary: amber.withOpacity(0.7), ), textTheme: TextTheme( bodySmall: TextStyle(color: Colors.grey.shade500), @@ -64,6 +73,9 @@ class AppTheme { dialogTheme: DialogTheme( surfaceTintColor: Colors.grey.shade700, ), + tooltipTheme: const TooltipThemeData( + waitDuration: Duration(seconds: 1), + ), ); /* TODO: Remove this. It is left here as a reference as we adjust styles to work with Flutter 3.7. diff --git a/lib/version.dart b/lib/version.dart index abb74145..707958ee 100755 --- a/lib/version.dart +++ b/lib/version.dart @@ -1,5 +1,5 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // This file is generated by running ./set-version.py -const String version = '6.3.0-dev.0'; -const int build = 60300; +const String version = '6.3.1-dev.0'; +const int build = 60301; diff --git a/lib/widgets/focus_utils.dart b/lib/widgets/focus_utils.dart new file mode 100644 index 00000000..94fca1b0 --- /dev/null +++ b/lib/widgets/focus_utils.dart @@ -0,0 +1,18 @@ + +import 'package:flutter/cupertino.dart'; +import 'package:logging/logging.dart'; + +import '../app/logging.dart'; + +final _log = Logger('FocusUtils'); + +class FocusUtils { + static void unfocus(BuildContext context) { + FocusScopeNode currentFocus = FocusScope.of(context); + + if (!currentFocus.hasPrimaryFocus) { + _log.debug('Removing focus...'); + currentFocus.unfocus(); + } + } +} \ No newline at end of file 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, - ), - ); diff --git a/lib/widgets/responsive_dialog.dart b/lib/widgets/responsive_dialog.dart index 047ef6af..dd500f15 100755 --- a/lib/widgets/responsive_dialog.dart +++ b/lib/widgets/responsive_dialog.dart @@ -16,19 +16,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:yubico_authenticator/core/state.dart'; class ResponsiveDialog extends StatefulWidget { final Widget? title; 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(); @@ -36,54 +40,78 @@ class ResponsiveDialog extends StatefulWidget { class _ResponsiveDialogState extends State { final Key _childKey = GlobalKey(); + final _focus = FocusScopeNode(); + bool _hasLostFocus = false; + + @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: 550, + 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: CloseButton( - onPressed: () { - widget.onCancel?.call(); - Navigator.of(context).pop(); - }, - ), - ), - 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 - ], - ); - } + var maxWidth = isDesktop ? 400 : 600; + // This keeps the focus in the dialog, even if the underlying page changes. + return FocusScope( + node: _focus, + autofocus: true, + onFocusChange: (focused) { + if (!focused && !_hasLostFocus) { + _focus.requestFocus(); + _hasLostFocus = true; + } + }, + child: constraints.maxWidth < maxWidth + ? _buildFullscreen(context) + : _buildDialog(context), + ); })); } diff --git a/lib/widgets/tooltip_if_truncated.dart b/lib/widgets/tooltip_if_truncated.dart new file mode 100644 index 00000000..2eaaf932 --- /dev/null +++ b/lib/widgets/tooltip_if_truncated.dart @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; + +class TooltipIfTruncated extends StatelessWidget { + final String text; + final TextStyle style; + final String? tooltip; + const TooltipIfTruncated( + {super.key, required this.text, required this.style, this.tooltip}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final textWidget = Text( + text, + textAlign: TextAlign.left, + overflow: TextOverflow.fade, + softWrap: false, + style: style, + ); + final TextPainter textPainter = TextPainter( + text: TextSpan(text: text, style: style), + textDirection: TextDirection.ltr, + maxLines: 1, + )..layout(minWidth: 0, maxWidth: constraints.maxWidth); + return textPainter.didExceedMaxLines + ? Tooltip( + margin: const EdgeInsets.all(16), + message: tooltip ?? text, + child: textWidget, + ) + : textWidget; + }, + ); + } +} 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) 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/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 58aa19b1..ca9c796a 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -205,7 +205,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5eb9a050..7a6b6f53 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.0.0 <4.0.0" - flutter: ">=3.5.0-0" + dart: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index 70b3099d..7229fc8a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # This field is updated by running ./set-version.py # DO NOT MANUALLY EDIT THIS! -version: 6.3.0-dev.0+60300 +version: 6.3.1-dev.0+60301 environment: sdk: '>=3.0.0 <4.0.0' @@ -34,7 +34,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - intl: ^0.18.0 + intl: ^0.18.1 # The following adds the Cupertino Icons font to your application. @@ -42,10 +42,10 @@ dependencies: # cupertino_icons: ^1.0.2 async: ^2.8.2 - logging: ^1.1.0 + logging: ^1.2.0 collection: ^1.16.0 - shared_preferences: ^2.1.0 - flutter_riverpod: ^2.3.6 + shared_preferences: ^2.1.2 + flutter_riverpod: ^2.3.10 json_annotation: ^4.8.1 freezed_annotation: ^2.2.0 window_manager: ^0.3.2 @@ -54,16 +54,18 @@ dependencies: screen_retriever: ^0.1.6 desktop_drop: ^0.4.0 url_launcher: ^6.1.7 - path_provider: ^2.0.14 - vector_graphics: ^1.1.5 - vector_graphics_compiler: ^1.1.5 + path_provider: ^2.1.0 + vector_graphics: ^1.1.7 + vector_graphics_compiler: ^1.1.7 path: ^1.8.2 - file_picker: ^5.2.9 - archive: ^3.3.2 + file_picker: ^5.3.2 + archive: ^3.3.8 crypto: ^3.0.2 tray_manager: ^0.2.0 local_notifier: ^0.1.5 io: ^1.0.4 + base32: ^2.1.3 + convert: ^3.1.1 dev_dependencies: integration_test: @@ -78,9 +80,9 @@ dev_dependencies: # rules and activating additional ones. flutter_lints: ^2.0.1 - build_runner: ^2.3.3 - freezed: ^2.3.2 - json_serializable: ^6.5.4 + build_runner: ^2.4.5 + freezed: ^2.4.2 + 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 diff --git a/resources/icons/systray-template.png b/resources/icons/systray-template.png new file mode 100644 index 00000000..e2faa8b3 Binary files /dev/null and b/resources/icons/systray-template.png differ diff --git a/resources/win/release-win.ps1 b/resources/win/release-win.ps1 index db18c7ff..4544d5d3 100644 --- a/resources/win/release-win.ps1 +++ b/resources/win/release-win.ps1 @@ -1,4 +1,4 @@ -$version="6.3.0-dev.0" +$version="6.3.1-dev.0" echo "Clean-up of old files" rm *.msi diff --git a/resources/win/yubioath-desktop.wxs b/resources/win/yubioath-desktop.wxs index 55ad622c..0f86aa15 100644 --- a/resources/win/yubioath-desktop.wxs +++ b/resources/win/yubioath-desktop.wxs @@ -1,7 +1,7 @@ - + diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index c0fd6ece..7afed59e 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -39,8 +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. diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index b25e363e..955ee303 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -31,6 +31,11 @@ bool FlutterWindow::OnCreate() { this->Show(); }); + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + return true; }