diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 1fbdb1212..2f7e4887c 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: set up JDK 17 + - name: Set up JDK 17 uses: actions/setup-java@v4 with: distribution: 'temurin' diff --git a/.github/workflows/env b/.github/workflows/env index bee7c20e1..65d425ce6 100644 --- a/.github/workflows/env +++ b/.github/workflows/env @@ -1,2 +1,2 @@ -FLUTTER=3.19.6 -PYVER=3.12.4 +FLUTTER=3.24.2 +PYVER=3.12.6 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 67b7a30d4..42932db64 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -52,7 +52,6 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: 'stable' - architecture: 'x64' flutter-version: ${{ env.FLUTTER }} - run: flutter config --enable-macos-desktop - run: flutter --version diff --git a/android/app/build.gradle b/android/app/build.gradle index a9311d43f..23a030bcb 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -91,26 +91,19 @@ dependencies { api "com.yubico.yubikit:fido:$project.yubiKitVersion" api "com.yubico.yubikit:support:$project.yubiKitVersion" - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.2' // Lifecycle - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.5' - implementation "androidx.core:core-ktx:1.13.0" - implementation 'androidx.fragment:fragment-ktx:1.6.2' + implementation "androidx.core:core-ktx:1.13.1" + implementation 'androidx.fragment:fragment-ktx:1.8.3' implementation 'androidx.preference:preference-ktx:1.2.1' - implementation 'com.google.android.material:material:1.11.0' + implementation 'com.google.android.material:material:1.12.0' implementation 'com.github.tony19:logback-android:3.0.0' - implementation('commons-codec:commons-codec') { - version { - // use version 1.15 for compatibility reasons - strictly '1.15' - } - } - // testing dependencies testImplementation "junit:junit:$project.junitVersion" testImplementation "org.mockito:mockito-core:$project.mockitoVersion" diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/CompatUtil.kt b/android/app/src/main/kotlin/com/yubico/authenticator/CompatUtil.kt index c9dc3d2e7..d48e0266f 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/CompatUtil.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/CompatUtil.kt @@ -37,7 +37,7 @@ import android.os.Build * @param sdkVersion the version this instance uses for compatibility checking. The release app * uses `Build.VERSION.SDK_INT`, tests use appropriate other values. */ -@Suppress("MemberVisibilityCanBePrivate", "unused") +@Suppress("MemberVisibilityCanBePrivate") class CompatUtil(private val sdkVersion: Int) { /** * Wrapper class holding values computed by [CompatUtil] 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 1d26c67c9..00abfc222 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt @@ -40,6 +40,7 @@ import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope import com.google.android.material.color.DynamicColors import com.yubico.authenticator.device.DeviceManager +import com.yubico.authenticator.device.noScp11bNfcSupport import com.yubico.authenticator.fido.FidoManager import com.yubico.authenticator.fido.FidoViewModel import com.yubico.authenticator.logging.FlutterLog @@ -47,13 +48,20 @@ import com.yubico.authenticator.management.ManagementHandler import com.yubico.authenticator.oath.AppLinkMethodChannel import com.yubico.authenticator.oath.OathManager import com.yubico.authenticator.oath.OathViewModel -import com.yubico.authenticator.yubikit.getDeviceInfo +import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo +import com.yubico.authenticator.yubikit.withConnection import com.yubico.yubikit.android.YubiKitManager import com.yubico.yubikit.android.transport.nfc.NfcConfiguration import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice import com.yubico.yubikit.android.transport.usb.UsbConfiguration +import com.yubico.yubikit.core.Transport import com.yubico.yubikit.core.YubiKeyDevice +import com.yubico.yubikit.core.smartcard.SmartCardConnection +import com.yubico.yubikit.core.smartcard.scp.Scp11KeyParams +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams +import com.yubico.yubikit.core.smartcard.scp.ScpKid +import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.BinaryMessenger @@ -62,7 +70,9 @@ import kotlinx.coroutines.launch import org.json.JSONObject import org.slf4j.LoggerFactory import java.io.Closeable +import java.security.NoSuchAlgorithmException import java.util.concurrent.Executors +import javax.crypto.Mac class MainActivity : FlutterFragmentActivity() { private val viewModel: MainViewModel by viewModels() @@ -116,7 +126,7 @@ class MainActivity : FlutterFragmentActivity() { } hasNfc = true - } catch (e: NfcNotAvailable) { + } catch (_: NfcNotAvailable) { hasNfc = false } @@ -231,7 +241,7 @@ class MainActivity : FlutterFragmentActivity() { startNfcDiscovery() } - val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager + val usbManager = getSystemService(USB_SERVICE) as UsbManager if (UsbManager.ACTION_USB_DEVICE_ATTACHED == intent.action) { val device = intent.parcelableExtra(UsbManager.EXTRA_DEVICE) if (device != null) { @@ -275,12 +285,38 @@ class MainActivity : FlutterFragmentActivity() { private suspend fun processYubiKey(device: YubiKeyDevice) { val deviceInfo = getDeviceInfo(device) - deviceManager.setDeviceInfo(deviceInfo) if (deviceInfo == null) { + deviceManager.setDeviceInfo(null) + return + } + + // If NFC and FIPS check for SCP11b key + if (device.transport == Transport.NFC && deviceInfo.fipsCapable != 0) { + logger.debug("Checking for usable SCP11b key...") + deviceManager.scpKeyParams = + device.withConnection { connection -> + val scp = SecurityDomainSession(connection) + val keyRef = scp.keyInformation.keys.firstOrNull { it.kid == ScpKid.SCP11b } + keyRef?.let { + val certs = scp.getCertificateBundle(it) + if (certs.isNotEmpty()) Scp11KeyParams( + keyRef, + certs[certs.size - 1].publicKey + ) else null + }?.also { + logger.debug("Found SCP11b key: {}", keyRef) + } + } + } + + // this YubiKey provides SCP11b key but the phone cannot perform AESCMAC + if (deviceManager.scpKeyParams != null && !supportsScp11b) { + deviceManager.setDeviceInfo(noScp11bNfcSupport) return } + deviceManager.setDeviceInfo(deviceInfo) val supportedContexts = DeviceManager.getSupportedContexts(deviceInfo) logger.debug("Connected key supports: {}", supportedContexts) if (!supportedContexts.contains(viewModel.appContext.value)) { @@ -293,7 +329,7 @@ class MainActivity : FlutterFragmentActivity() { switchContext(preferredContext) } - if (contextManager == null) { + if (contextManager == null && supportedContexts.isNotEmpty()) { switchContext(DeviceManager.getPreferredContext(supportedContexts)) } @@ -406,6 +442,12 @@ class MainActivity : FlutterFragmentActivity() { companion object { const val YUBICO_VENDOR_ID = 4176 const val FLAG_SECURE = WindowManager.LayoutParams.FLAG_SECURE + val supportsScp11b = try { + Mac.getInstance("AESCMAC"); + true + } catch (_: NoSuchAlgorithmException) { + false + } } /** We observed that some devices (Pixel 2, OnePlus 6) automatically end NFC discovery @@ -427,7 +469,7 @@ class MainActivity : FlutterFragmentActivity() { } private val sharedPreferencesListener = OnSharedPreferenceChangeListener { _, key -> - if ( AppPreferences.PREF_NFC_SILENCE_SOUNDS == key) { + if (AppPreferences.PREF_NFC_SILENCE_SOUNDS == key) { stopNfcDiscovery() startNfcDiscovery() } @@ -493,9 +535,10 @@ class MainActivity : FlutterFragmentActivity() { } result.success(true) } + "hasCamera" -> { val cameraService = - getSystemService(Context.CAMERA_SERVICE) as CameraManager + getSystemService(CAMERA_SERVICE) as CameraManager result.success( cameraService.cameraIdList.any { cameraService.getCameraCharacteristics(it) @@ -503,9 +546,11 @@ class MainActivity : FlutterFragmentActivity() { } ) } + "hasNfc" -> result.success( packageManager.hasSystemFeature(PackageManager.FEATURE_NFC) ) + "isNfcEnabled" -> { val nfcAdapter = NfcAdapter.getDefaultAdapter(this@MainActivity) @@ -513,6 +558,7 @@ class MainActivity : FlutterFragmentActivity() { nfcAdapter != null && nfcAdapter.isEnabled ) } + "openNfcSettings" -> { startActivity(Intent(ACTION_NFC_SETTINGS)) result.success(true) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/device/DeviceManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/device/DeviceManager.kt index 5c1d4a6ca..01e7f04f8 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/device/DeviceManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/device/DeviceManager.kt @@ -24,6 +24,7 @@ import com.yubico.authenticator.MainViewModel import com.yubico.authenticator.OperationContext import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice import com.yubico.yubikit.core.YubiKeyDevice +import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams import com.yubico.yubikit.management.Capability import org.slf4j.LoggerFactory @@ -46,6 +47,15 @@ class DeviceManager( private val deviceListeners = HashSet() + val deviceInfo: Info? + get() = appViewModel.deviceInfo.value + + var scpKeyParams: ScpKeyParams? = null + set(value) { + field = value + logger.debug("SCP params set to {}", value) + } + fun addDeviceListener(listener: DeviceListener) { deviceListeners.add(listener) } @@ -157,6 +167,7 @@ class DeviceManager( fun setDeviceInfo(deviceInfo: Info?) { appViewModel.setDeviceInfo(deviceInfo) + scpKeyParams = null } fun isUsbKeyConnected(): Boolean { diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/device/Info.kt b/android/app/src/main/kotlin/com/yubico/authenticator/device/Info.kt index c8f93683d..64173b692 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/device/Info.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/device/Info.kt @@ -21,7 +21,7 @@ import com.yubico.yubikit.management.DeviceInfo import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -private fun DeviceInfo.capabilitiesFor(transport: Transport) : Int? = +private fun DeviceInfo.capabilitiesFor(transport: Transport): Int? = when { hasTransport(transport) -> getSupportedCapabilities(transport) else -> null @@ -30,7 +30,7 @@ private fun DeviceInfo.capabilitiesFor(transport: Transport) : Int? = @Serializable data class Info( @SerialName("config") - val config : Config, + val config: Config, @SerialName("serial") val serialNumber: Int?, @SerialName("version") @@ -53,11 +53,21 @@ data class Info( val pinComplexity: Boolean, @SerialName("supported_capabilities") val supportedCapabilities: Capabilities, + @SerialName("fips_capable") + val fipsCapable: Int, + @SerialName("fips_approved") + val fipsApproved: Int, + @SerialName("reset_blocked") + val resetBlocked: Int, ) { constructor(name: String, isNfc: Boolean, usbPid: Int?, deviceInfo: DeviceInfo) : this( config = Config(deviceInfo.config), serialNumber = deviceInfo.serialNumber, - version = Version(deviceInfo.version.major, deviceInfo.version.minor, deviceInfo.version.micro), + version = Version( + deviceInfo.version.major, + deviceInfo.version.minor, + deviceInfo.version.micro + ), formFactor = deviceInfo.formFactor.value, isLocked = deviceInfo.isLocked, isSky = deviceInfo.isSky, @@ -69,6 +79,9 @@ data class Info( supportedCapabilities = Capabilities( nfc = deviceInfo.capabilitiesFor(Transport.NFC), usb = deviceInfo.capabilitiesFor(Transport.USB), - ) + ), + fipsCapable = deviceInfo.fipsCapable, + fipsApproved = deviceInfo.fipsApproved, + resetBlocked = deviceInfo.resetBlocked, ) } diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/device/UnknownDevice.kt b/android/app/src/main/kotlin/com/yubico/authenticator/device/UnknownDevice.kt index 52b61e2b9..38a556a6f 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/device/UnknownDevice.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/device/UnknownDevice.kt @@ -1,3 +1,19 @@ +/* + * Copyright (C) 2023-2024 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.device import com.yubico.yubikit.core.Transport @@ -17,11 +33,14 @@ val UnknownDevice = Info( isLocked = false, isSky = false, isFips = false, - name = "Unrecognized device", + name = "unknown-device", isNfc = false, usbPid = null, pinComplexity = false, - supportedCapabilities = Capabilities() + supportedCapabilities = Capabilities(), + fipsCapable = 0, + fipsApproved = 0, + resetBlocked = 0 ) fun unknownDeviceWithCapability(transport: Transport, bit: Int = 0) : Info { @@ -47,4 +66,21 @@ fun unknownFido2DeviceInfo(transport: Transport) : Info { return unknownDeviceWithCapability(transport, Capability.FIDO2.bit).copy( name = "FIDO2 device" ) -} \ No newline at end of file +} + +fun restrictedNfcDeviceInfo(transport: Transport) : Info { + if (transport != Transport.NFC) { + return UnknownDevice + } + + return UnknownDevice.copy( + isNfc = true, + name = "restricted-nfc" + ) +} + +// the YubiKey requires SCP11b communication but the phone cannot handle it +val noScp11bNfcSupport = UnknownDevice.copy( + isNfc = true, + name = "no-scp11b-nfc-support" +) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoActionDescription.kt b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoActionDescription.kt index 4482f8cce..ae0d8945d 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoActionDescription.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoActionDescription.kt @@ -26,7 +26,8 @@ enum class FidoActionDescription(private val value: Int) { DeleteFingerprint(4), RenameFingerprint(5), RegisterFingerprint(6), - ActionFailure(7); + EnableEnterpriseAttestation(7), + ActionFailure(8); val id: Int get() = value + dialogDescriptionFidoIndex diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoConnectionHelper.kt b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoConnectionHelper.kt index ba83fa2ee..88ab68530 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoConnectionHelper.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoConnectionHelper.kt @@ -19,9 +19,9 @@ package com.yubico.authenticator.fido import com.yubico.authenticator.DialogIcon import com.yubico.authenticator.DialogManager import com.yubico.authenticator.DialogTitle -import com.yubico.authenticator.MainViewModel import com.yubico.authenticator.device.DeviceManager import com.yubico.authenticator.fido.data.YubiKitFidoSession +import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo import com.yubico.authenticator.yubikit.withConnection import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice import com.yubico.yubikit.core.fido.FidoConnection @@ -52,18 +52,25 @@ class FidoConnectionHelper( suspend fun useSession( actionDescription: FidoActionDescription, + updateDeviceInfo: Boolean = false, action: (YubiKitFidoSession) -> T ): T { + FidoManager.updateDeviceInfo.set(updateDeviceInfo) return deviceManager.withKey( onNfc = { useSessionNfc(actionDescription,action) }, - onUsb = { useSessionUsb(it, action) }) + onUsb = { useSessionUsb(it, updateDeviceInfo, action) }) } suspend fun useSessionUsb( device: UsbYubiKeyDevice, + updateDeviceInfo: Boolean = false, block: (YubiKitFidoSession) -> T ): T = device.withConnection { block(YubiKitFidoSession(it)) + }.also { + if (updateDeviceInfo) { + deviceManager.setDeviceInfo(getDeviceInfo(device)) + } } suspend fun useSessionNfc( diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoManager.kt index d876740ea..138f0a38d 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoManager.kt @@ -30,6 +30,7 @@ import com.yubico.authenticator.fido.data.Session import com.yubico.authenticator.fido.data.SessionInfo import com.yubico.authenticator.fido.data.YubiKitFidoSession import com.yubico.authenticator.setHandler +import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo import com.yubico.authenticator.yubikit.withConnection import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice import com.yubico.yubikit.core.YubiKeyConnection @@ -42,6 +43,7 @@ import com.yubico.yubikit.core.smartcard.SmartCardConnection import com.yubico.yubikit.core.util.Result import com.yubico.yubikit.fido.ctap.BioEnrollment import com.yubico.yubikit.fido.ctap.ClientPin +import com.yubico.yubikit.fido.ctap.Config import com.yubico.yubikit.fido.ctap.CredentialManagement import com.yubico.yubikit.fido.ctap.Ctap2Session.InfoData import com.yubico.yubikit.fido.ctap.FingerprintBioEnrollment @@ -60,6 +62,7 @@ import org.slf4j.LoggerFactory import java.io.IOException import java.util.Arrays import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean typealias FidoAction = (Result) -> Unit @@ -79,6 +82,7 @@ class FidoManager( } companion object { + val updateDeviceInfo = AtomicBoolean(false) fun getPreferredPinUvAuthProtocol(infoData: InfoData): PinUvAuthProtocol { val pinUvAuthProtocols = infoData.pinUvAuthProtocols val pinSupported = infoData.options["clientPin"] != null @@ -119,6 +123,8 @@ class FidoManager( pinStore ) + + init { pinRetries = null @@ -159,6 +165,8 @@ class FidoManager( "cancelRegisterFingerprint" -> cancelRegisterFingerprint() + "enableEnterpriseAttestation" -> enableEnterpriseAttestation() + else -> throw NotImplementedError() } } @@ -170,6 +178,7 @@ class FidoManager( fidoChannel.setMethodCallHandler(null) fidoViewModel.clearSessionState() fidoViewModel.updateCredentials(null) + connectionHelper.cancelPending() coroutineScope.cancel() } @@ -184,6 +193,10 @@ class FidoManager( processYubiKey(connection, device) } } + + if (updateDeviceInfo.getAndSet(false)) { + deviceManager.setDeviceInfo(getDeviceInfo(device)) + } } catch (e: Exception) { // something went wrong, try to get DeviceInfo from any available connection type logger.error("Failure when processing YubiKey: ", e) @@ -210,7 +223,7 @@ class FidoManager( currentSession ) - val sameDevice = currentSession.equals(previousSession) + val sameDevice = currentSession == previousSession if (device is NfcYubiKeyDevice && (sameDevice || resetHelper.inProgress)) { connectionHelper.invokePending(fidoSession) @@ -377,7 +390,7 @@ class FidoManager( } private suspend fun setPin(pin: CharArray?, newPin: CharArray): String = - connectionHelper.useSession(FidoActionDescription.SetPin) { fidoSession -> + connectionHelper.useSession(FidoActionDescription.SetPin, updateDeviceInfo = true) { fidoSession -> try { val clientPin = ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)) @@ -603,6 +616,42 @@ class FidoManager( ).toString() } + private suspend fun enableEnterpriseAttestation(): String = + connectionHelper.useSession(FidoActionDescription.EnableEnterpriseAttestation) { fidoSession -> + try { + val uvAuthProtocol = getPreferredPinUvAuthProtocol(fidoSession.cachedInfo) + val clientPin = ClientPin(fidoSession, uvAuthProtocol) + val token = if (pinStore.hasPin()) { + clientPin.getPinToken( + pinStore.getPin(), + ClientPin.PIN_PERMISSION_ACFG, + null + ) + } else null + + val config = Config(fidoSession, uvAuthProtocol, token) + config.enableEnterpriseAttestation() + fidoViewModel.setSessionState( + Session( + fidoSession.info, + pinStore.hasPin(), + pinRetries + ) + ) + return@useSession JSONObject( + mapOf( + "success" to true, + ) + ).toString() + } catch (e: Exception) { + logger.error("Failed to enable enterprise attestation. ", e) + return@useSession JSONObject( + mapOf( + "success" to false, + ) + ).toString() + } + } override fun onDisconnected() { if (!resetHelper.inProgress) { diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoResetHelper.kt b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoResetHelper.kt index a89a8526d..33d54f92e 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoResetHelper.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoResetHelper.kt @@ -162,8 +162,9 @@ class FidoResetHelper( coroutineScope.launch(Dispatchers.Main) { fidoViewModel.updateResetState(FidoResetState.Touch) logger.debug("Waiting for touch") - deviceManager.withKey { usbYubiKeyDevice -> - connectionHelper.useSessionUsb(usbYubiKeyDevice) { fidoSession -> + deviceManager.withKey { + usbYubiKeyDevice -> + connectionHelper.useSessionUsb(usbYubiKeyDevice, updateDeviceInfo = true) { fidoSession -> resetCommandState = CommandState() try { if (cancelReset) { @@ -211,6 +212,7 @@ class FidoResetHelper( coroutineScope.launch { fidoViewModel.updateResetState(FidoResetState.Touch) try { + FidoManager.updateDeviceInfo.set(true) connectionHelper.useSessionNfc(FidoActionDescription.Reset) { fidoSession -> doReset(fidoSession) continuation.resume(Unit) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/fido/data/Session.kt b/android/app/src/main/kotlin/com/yubico/authenticator/fido/data/Session.kt index 3cf75159a..93cd770b1 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/fido/data/Session.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/fido/data/Session.kt @@ -29,14 +29,16 @@ data class Options( val credMgmt: Boolean, val credentialMgmtPreview: Boolean, val bioEnroll: Boolean?, - val alwaysUv: Boolean + val alwaysUv: Boolean, + val ep: Boolean?, ) { constructor(infoData: InfoData) : this( - infoData.getOptionsBoolean("clientPin") ?: false, - infoData.getOptionsBoolean("credMgmt") ?: false, - infoData.getOptionsBoolean("credentialMgmtPreview") ?: false, + infoData.getOptionsBoolean("clientPin") == true, + infoData.getOptionsBoolean("credMgmt") == true, + infoData.getOptionsBoolean("credentialMgmtPreview") == true, infoData.getOptionsBoolean("bioEnroll"), - infoData.getOptionsBoolean("alwaysUv") ?: false, + infoData.getOptionsBoolean("alwaysUv") == true, + infoData.getOptionsBoolean("ep"), ) companion object { 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 71f86facf..07f5af1bd 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-2023 Yubico. + * Copyright (C) 2022-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,7 +70,7 @@ class FlutterLog(messenger: BinaryMessenger) { } private fun logLevelFromArgument(argValue: String?): Log.LogLevel? = - Log.LogLevel.values().firstOrNull { it.name == argValue?.uppercase() } + Log.LogLevel.entries.firstOrNull { it.name == argValue?.uppercase() } private fun loggerError(message: String) { log(Log.LogLevel.ERROR,"FlutterLog", message, null) 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 adc592ee5..03c9dd7a0 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 @@ -26,7 +26,6 @@ import com.yubico.authenticator.* import com.yubico.authenticator.device.Capabilities import com.yubico.authenticator.device.DeviceListener import com.yubico.authenticator.device.DeviceManager -import com.yubico.authenticator.device.Info import com.yubico.authenticator.device.UnknownDevice import com.yubico.authenticator.oath.data.Code import com.yubico.authenticator.oath.data.CodeType @@ -43,21 +42,20 @@ import com.yubico.authenticator.oath.keystore.ClearingMemProvider import com.yubico.authenticator.oath.keystore.KeyProvider import com.yubico.authenticator.oath.keystore.KeyStoreProvider import com.yubico.authenticator.oath.keystore.SharedPrefProvider -import com.yubico.authenticator.yubikit.getDeviceInfo +import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo import com.yubico.authenticator.yubikit.withConnection import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice import com.yubico.yubikit.core.Transport import com.yubico.yubikit.core.YubiKeyDevice -import com.yubico.yubikit.core.application.ApplicationNotAvailableException import com.yubico.yubikit.core.smartcard.ApduException import com.yubico.yubikit.core.smartcard.AppId import com.yubico.yubikit.core.smartcard.SW import com.yubico.yubikit.core.smartcard.SmartCardConnection import com.yubico.yubikit.core.smartcard.SmartCardProtocol import com.yubico.yubikit.core.util.Result +import com.yubico.yubikit.management.Capability import com.yubico.yubikit.oath.CredentialData -import com.yubico.yubikit.support.DeviceUtil import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.* @@ -108,6 +106,7 @@ class OathManager( private var pendingAction: OathAction? = null private var refreshJob: Job? = null private var addToAny = false + private val updateDeviceInfo = AtomicBoolean(false) override fun onPause() { // cancel any pending actions, except for addToAny @@ -209,6 +208,7 @@ class OathManager( oathChannel.setMethodCallHandler(null) oathViewModel.clearSession() oathViewModel.updateCredentials(mapOf()) + pendingAction?.invoke(Result.failure(Exception())) coroutineScope.cancel() } @@ -268,7 +268,7 @@ class OathManager( try { SmartCardProtocol(connection).select(AppId.OTP) } catch (e: Exception) { - logger.error("Failed to recognize this OATH device.") + logger.error("Failed to recognize this OATH device.", e) // we know this is NFC device and it supports OATH val oathCapabilities = Capabilities(nfc = 0x20) deviceManager.setDeviceInfo( @@ -287,6 +287,10 @@ class OathManager( logger.debug( "Successfully read Oath session info (and credentials if unlocked) from connected key" ) + + if (updateDeviceInfo.getAndSet(false)) { + deviceManager.setDeviceInfo(getDeviceInfo(device)) + } } catch (e: Exception) { // OATH not enabled/supported, try to get DeviceInfo over other USB interfaces logger.error("Failed to connect to CCID: ", e) @@ -365,7 +369,7 @@ class OathManager( } private suspend fun reset(): String = - useOathSession(OathActionDescription.Reset) { + useOathSession(OathActionDescription.Reset, updateDeviceInfo = true) { // note, it is ok to reset locked session it.reset() keyManager.removeKey(it.deviceId) @@ -399,7 +403,11 @@ class OathManager( currentPassword: String?, newPassword: String, ): String = - useOathSession(OathActionDescription.SetPassword, unlock = false) { session -> + useOathSession( + OathActionDescription.SetPassword, + unlock = false, + updateDeviceInfo = true + ) { session -> if (session.isAccessKeySet) { if (currentPassword == null) { throw Exception("Must provide current password to be able to change it") @@ -613,8 +621,10 @@ class OathManager( * @param connection the device SmartCard connection * @return a [YubiKitOathSession] which is unlocked or locked based on an internal parameter */ - private fun getOathSession(connection: SmartCardConnection) : YubiKitOathSession { - val session = YubiKitOathSession(connection) + private fun getOathSession(connection: SmartCardConnection): YubiKitOathSession { + // If OATH is FIPS capable, and we have scpKeyParams, we should use them + val fips = (deviceManager.deviceInfo?.fipsCapable ?: 0) and Capability.OATH.bit != 0 + val session = YubiKitOathSession(connection, if (fips) deviceManager.scpKeyParams else null) if (!unlockOnConnect.compareAndSet(false, true)) { tryToUnlockOathSession(session) @@ -649,22 +659,30 @@ class OathManager( private suspend fun useOathSession( oathActionDescription: OathActionDescription, unlock: Boolean = true, + updateDeviceInfo: Boolean = false, action: (YubiKitOathSession) -> T ): T { // callers can decide whether the session should be unlocked first unlockOnConnect.set(unlock) + // callers can request whether device info should be updated after session operation + this@OathManager.updateDeviceInfo.set(updateDeviceInfo) return deviceManager.withKey( - onUsb = { useOathSessionUsb(it, action) }, + onUsb = { useOathSessionUsb(it, updateDeviceInfo, action) }, onNfc = { useOathSessionNfc(oathActionDescription, action) } ) } private suspend fun useOathSessionUsb( device: UsbYubiKeyDevice, + updateDeviceInfo: Boolean = false, block: (YubiKitOathSession) -> T ): T = device.withConnection { block(getOathSession(it)) + }.also { + if (updateDeviceInfo) { + deviceManager.setDeviceInfo(getDeviceInfo(device)) + } } private suspend fun useOathSessionNfc( diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/data/Code.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/data/Code.kt index a958c555b..e4a1cb07b 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/data/Code.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/data/Code.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Yubico. + * Copyright (C) 2023-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,10 +25,8 @@ typealias YubiKitCode = com.yubico.yubikit.oath.Code data class Code( val value: String? = null, @SerialName("valid_from") - @Suppress("unused") val validFrom: Long, @SerialName("valid_to") - @Suppress("unused") val validTo: Long ) { diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/keystore/ClearingMemProvider.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/keystore/ClearingMemProvider.kt index 513dc5add..1c45cdf23 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/keystore/ClearingMemProvider.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/keystore/ClearingMemProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * Copyright (C) 2022,2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package com.yubico.authenticator.oath.keystore -import android.security.keystore.KeyProperties import com.yubico.yubikit.oath.AccessKey import java.util.* import javax.crypto.Mac diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/DeviceInfoHelper.kt b/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/DeviceInfoHelper.kt index df85db35b..a0f04d779 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/DeviceInfoHelper.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/DeviceInfoHelper.kt @@ -16,69 +16,121 @@ package com.yubico.authenticator.yubikit -import com.yubico.authenticator.device.Info import com.yubico.authenticator.compatUtil +import com.yubico.authenticator.device.Info +import com.yubico.authenticator.device.restrictedNfcDeviceInfo import com.yubico.authenticator.device.unknownDeviceWithCapability import com.yubico.authenticator.device.unknownFido2DeviceInfo import com.yubico.authenticator.device.unknownOathDeviceInfo import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice +import com.yubico.yubikit.core.Version import com.yubico.yubikit.core.YubiKeyDevice import com.yubico.yubikit.core.application.ApplicationNotAvailableException +import com.yubico.yubikit.core.application.SessionVersionOverride import com.yubico.yubikit.core.fido.FidoConnection import com.yubico.yubikit.core.otp.OtpConnection +import com.yubico.yubikit.core.smartcard.Apdu import com.yubico.yubikit.core.smartcard.SmartCardConnection +import com.yubico.yubikit.core.smartcard.SmartCardProtocol import com.yubico.yubikit.fido.ctap.Ctap2Session import com.yubico.yubikit.management.DeviceInfo import com.yubico.yubikit.oath.OathSession import com.yubico.yubikit.support.DeviceUtil - import org.slf4j.LoggerFactory -suspend fun getDeviceInfo(device: YubiKeyDevice): Info? { - val pid = (device as? UsbYubiKeyDevice)?.pid - val logger = LoggerFactory.getLogger("getDeviceInfo") +class DeviceInfoHelper { + companion object { + private val logger = LoggerFactory.getLogger("DeviceInfoHelper") + private val nfcTagReaderAid = byteArrayOf(0xD2.toByte(), 0x76, 0, 0, 0x85.toByte(), 1, 1) + private val uri = "yubico.com/getting-started".toByteArray() + private val restrictedNfcBytes = + byteArrayOf(0x00, 0x1F, 0xD1.toByte(), 0x01, 0x1b, 0x55, 0x04) + uri + + suspend fun getDeviceInfo(device: YubiKeyDevice): Info? { + SessionVersionOverride.set(null) + var deviceInfo = readDeviceInfo(device) + if (deviceInfo?.version?.major == 0.toByte()) { + SessionVersionOverride.set(Version(5, 7, 2)) + deviceInfo = readDeviceInfo(device) + } + return deviceInfo + } + + private suspend fun readDeviceInfo(device: YubiKeyDevice): Info? { + val pid = (device as? UsbYubiKeyDevice)?.pid - val deviceInfo = runCatching { - device.withConnection { DeviceUtil.readInfo(it, pid) } - }.recoverCatching { t -> - logger.debug("Smart card connection not available: {}", t.message) - device.withConnection { DeviceUtil.readInfo(it, pid) } - }.recoverCatching { t -> - logger.debug("OTP connection not available: {}", t.message) - device.withConnection { DeviceUtil.readInfo(it, pid) } - }.recoverCatching { t -> - logger.debug("FIDO connection not available: {}", t.message) - return SkyHelper(compatUtil).getDeviceInfo(device) - }.getOrElse { - // this is not a YubiKey - logger.debug("Probing unknown device") - try { - device.openConnection(SmartCardConnection::class.java).use { smartCardConnection -> + val deviceInfo = runCatching { + device.withConnection { + DeviceUtil.readInfo( + it, + pid + ) + } + }.recoverCatching { t -> + logger.debug("Smart card connection not available: {}", t.message) + device.withConnection { DeviceUtil.readInfo(it, pid) } + }.recoverCatching { t -> + logger.debug("OTP connection not available: {}", t.message) + device.withConnection { DeviceUtil.readInfo(it, pid) } + }.recoverCatching { t -> + logger.debug("FIDO connection not available: {}", t.message) + return SkyHelper(compatUtil).getDeviceInfo(device) + }.getOrElse { + // this is not a YubiKey + logger.debug("Probing unknown device") try { - // if OATH session is available use it - OathSession(smartCardConnection) - logger.debug("Device supports OATH") - return unknownOathDeviceInfo(device.transport) - } catch (applicationNotAvailable: ApplicationNotAvailableException) { - try { - // probe for CTAP2 availability - Ctap2Session(smartCardConnection) - logger.debug("Device supports FIDO2") - return unknownFido2DeviceInfo(device.transport) - } catch (applicationNotAvailable: ApplicationNotAvailableException) { - logger.debug("Device not recognized") - return unknownDeviceWithCapability(device.transport) - } + device.openConnection(SmartCardConnection::class.java) + .use { smartCardConnection -> + try { + // if OATH session is available use it + OathSession(smartCardConnection) + logger.debug("Device supports OATH") + return unknownOathDeviceInfo(device.transport) + } catch (_: ApplicationNotAvailableException) { + try { + // probe for CTAP2 availability + Ctap2Session(smartCardConnection) + logger.debug("Device supports FIDO2") + return unknownFido2DeviceInfo(device.transport) + } catch (_: ApplicationNotAvailableException) { + // probe for NFC restricted device + if (isNfcRestricted(smartCardConnection)) { + logger.debug("Device has restricted NFC") + return restrictedNfcDeviceInfo(device.transport) + } + logger.debug("Device not recognized") + return unknownDeviceWithCapability(device.transport) + } + } + } + } catch (e: Exception) { + // no smart card connectivity + logger.error("Failure getting device info", e) + return null } } + + val name = DeviceUtil.getName(deviceInfo, pid?.type) + return Info(name, device is NfcYubiKeyDevice, pid?.value, deviceInfo) + } + + private fun isNfcRestricted(connection: SmartCardConnection): Boolean = + restrictedNfcBytes.contentEquals(readNdef(connection).also { + logger.debug("ndef: {}", it) + }) + + private fun readNdef(connection: SmartCardConnection): ByteArray? = try { + with(SmartCardProtocol(connection)) { + select(nfcTagReaderAid) + sendAndReceive(Apdu(0x00, 0xA4, 0x00, 0x0C, byteArrayOf(0xE1.toByte(), 0x04))) + sendAndReceive(Apdu(0x00, 0xB0, 0x00, 0x00, null)) + } } catch (e: Exception) { - // no smart card connectivity - logger.error("Failure getting device info", e) - return null + logger.debug("Failed to read ndef tag: ", e) + null } } - val name = DeviceUtil.getName(deviceInfo, pid?.type) - return Info(name, device is NfcYubiKeyDevice, pid?.value, deviceInfo) } + diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/SkyHelper.kt b/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/SkyHelper.kt index 05458eb51..6754991d1 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/SkyHelper.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/SkyHelper.kt @@ -75,7 +75,10 @@ class SkyHelper(private val compatUtil: CompatUtil) { isNfc = false, usbPid = pid.value, pinComplexity = false, - supportedCapabilities = Capabilities(usb = 0) + supportedCapabilities = Capabilities(usb = 0), + fipsCapable = 0, + fipsApproved = 0, + resetBlocked = 0 ) } diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml index 69937a225..2538d9a52 100644 --- a/android/app/src/main/res/values-de/strings.xml +++ b/android/app/src/main/res/values-de/strings.xml @@ -1,2 +1,7 @@ - \ No newline at end of file + + OTP-Code wurde erfolgreich von Ihrem YubiKey in die Zwischenablage kopiert. + Passwort wurde erfolgreich von Ihrem YubiKey in die Zwischenablage kopiert. + Beim Parsen des OTP-Codes von Ihrem YubiKey ist ein Fehler aufgetreten. + Konnte während dem Versuch den OTP-Code von Ihrem YubiKey zu kopieren nicht auf die Zwischenablage zugreifen. + \ No newline at end of file diff --git a/android/app/src/main/res/values-vi/strings.xml b/android/app/src/main/res/values-vi/strings.xml new file mode 100644 index 000000000..b484efa90 --- /dev/null +++ b/android/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,7 @@ + + + Đã sao chép mã OTP từ YubiKey vào clipboard. + Đã sao chép mật khẩu từ YubiKey vào clipboard. + Không thể phân tích mã OTP từ YubiKey. + Không thể truy cập clipboard khi cố gắng sao chép mã OTP từ YubiKey. + \ No newline at end of file diff --git a/android/app/src/test/java/com/yubico/authenticator/device/ConfigTest.kt b/android/app/src/test/java/com/yubico/authenticator/device/ConfigTest.kt index 00dc112e8..af5f68ef4 100644 --- a/android/app/src/test/java/com/yubico/authenticator/device/ConfigTest.kt +++ b/android/app/src/test/java/com/yubico/authenticator/device/ConfigTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Yubico. + * Copyright (C) 2023-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package com.yubico.authenticator.device import com.yubico.authenticator.jsonSerializer -import com.yubico.yubikit.management.DeviceConfig import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject diff --git a/android/app/src/test/java/com/yubico/authenticator/yubikit/SkyHelperTest.kt b/android/app/src/test/java/com/yubico/authenticator/yubikit/SkyHelperTest.kt index dfce521c1..ab5e11de1 100644 --- a/android/app/src/test/java/com/yubico/authenticator/yubikit/SkyHelperTest.kt +++ b/android/app/src/test/java/com/yubico/authenticator/yubikit/SkyHelperTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2023 Yubico. + * Copyright (C) 2022-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ class SkyHelperTest { fun `supports three specific UsbPids`() { val skyHelper = SkyHelper(CompatUtil(33)) - for (pid in UsbPid.values()) { + for (pid in UsbPid.entries) { val ykDevice = getUsbYubiKeyDeviceMock().also { `when`(it.pid).thenReturn(pid) } diff --git a/android/build.gradle b/android/build.gradle index 6c80715e2..92bf1bceb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -9,9 +9,9 @@ allprojects { targetSdkVersion = 34 compileSdkVersion = 34 - yubiKitVersion = "2.5.0" + yubiKitVersion = "2.7.0" junitVersion = "4.13.2" - mockitoVersion = "5.11.0" + mockitoVersion = "5.13.0" } } diff --git a/android/flutter_plugins/qrscanner_zxing/android/build.gradle b/android/flutter_plugins/qrscanner_zxing/android/build.gradle index d747315b4..9e34f8827 100644 --- a/android/flutter_plugins/qrscanner_zxing/android/build.gradle +++ b/android/flutter_plugins/qrscanner_zxing/android/build.gradle @@ -2,14 +2,14 @@ group 'com.yubico.authenticator.flutter_plugins.qrscanner_zxing' version '1.0' buildscript { - ext.kotlin_version = '1.9.23' + ext.kotlin_version = '2.0.20' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.3.2' + classpath 'com.android.tools.build:gradle:8.6.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -50,7 +50,7 @@ android { } dependencies { - def camerax_version = "1.3.3" + def camerax_version = "1.3.4" 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/gradle/wrapper/gradle-wrapper.properties b/android/flutter_plugins/qrscanner_zxing/android/gradle/wrapper/gradle-wrapper.properties index 35f231b5d..338e7b09e 100644 --- a/android/flutter_plugins/qrscanner_zxing/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/flutter_plugins/qrscanner_zxing/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Oct 16 08:48:17 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerZxingPlugin.kt b/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerZxingPlugin.kt index 2ad04d494..43c664b07 100644 --- a/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerZxingPlugin.kt +++ b/android/flutter_plugins/qrscanner_zxing/android/src/main/kotlin/com/yubico/authenticator/flutter_plugins/qrscanner_zxing/QRScannerZxingPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * Copyright (C) 2022,2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ class PermissionsResultRegistrar { requestCode, permissions, grantResults - ) ?: false + ) == true } } diff --git a/android/flutter_plugins/qrscanner_zxing/example/android/build.gradle b/android/flutter_plugins/qrscanner_zxing/example/android/build.gradle index ad5d690e3..396d55d44 100644 --- a/android/flutter_plugins/qrscanner_zxing/example/android/build.gradle +++ b/android/flutter_plugins/qrscanner_zxing/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.9.10' + ext.kotlin_version = '2.0.0' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.2' + classpath 'com.android.tools.build:gradle:8.5.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/android/flutter_plugins/qrscanner_zxing/example/android/gradle/wrapper/gradle-wrapper.properties b/android/flutter_plugins/qrscanner_zxing/example/android/gradle/wrapper/gradle-wrapper.properties index d07c7fcf8..7aeeb11c6 100644 --- a/android/flutter_plugins/qrscanner_zxing/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/flutter_plugins/qrscanner_zxing/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip diff --git a/android/flutter_plugins/qrscanner_zxing/example/pubspec.lock b/android/flutter_plugins/qrscanner_zxing/example/pubspec.lock index f929cb24d..e86901281 100644 --- a/android/flutter_plugins/qrscanner_zxing/example/pubspec.lock +++ b/android/flutter_plugins/qrscanner_zxing/example/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" fake_async: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6" + sha256: "1bbf65dd997458a08b531042ec3794112a6c39c07c37ff22113d2e7e4f81d4e4" url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.2.1" flutter: dependency: "direct main" description: flutter @@ -82,18 +82,18 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.20" flutter_test: dependency: "direct dev" description: flutter @@ -108,26 +108,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -156,10 +156,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" path: dependency: transitive description: @@ -232,10 +232,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" vector_math: dependency: transitive description: @@ -248,18 +248,18 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" win32: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.5.1" sdks: - dart: ">=3.3.0-279.1.beta <4.0.0" - flutter: ">=3.7.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 09098360f..ae12d1ff6 100755 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Aug 15 14:34:17 CEST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/android/settings.gradle b/android/settings.gradle index 8a80d6929..6e165341c 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -26,9 +26,9 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.3.2" apply false - id "org.jetbrains.kotlin.android" version "1.9.23" apply false - id "org.jetbrains.kotlin.plugin.serialization" version "1.9.23" apply false + id "com.android.application" version "8.6.0" apply false + id "org.jetbrains.kotlin.android" version "2.0.20" apply false + id "org.jetbrains.kotlin.plugin.serialization" version "2.0.20" apply false id "com.google.android.gms.oss-licenses-plugin" version "0.10.6" apply false } diff --git a/build-helper.sh b/build-helper.sh index 2612335f4..49accf99b 100755 --- a/build-helper.sh +++ b/build-helper.sh @@ -50,10 +50,8 @@ if [ "$OS" = "macos" ]; then 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 delocate-merge $HELPER/pillow*.whl + UNIVERSAL_WHL=$(ls $HELPER/pillow*universal2.whl) poetry run pip install --upgrade $UNIVERSAL_WHL fi fi diff --git a/helper/helper/__init__.py b/helper/helper/__init__.py index 42bf1bfab..aa8527d9d 100644 --- a/helper/helper/__init__.py +++ b/helper/helper/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .base import RpcException, encode_bytes +from .base import RpcResponse, RpcException, encode_bytes from .device import RootNode from queue import Queue @@ -80,7 +80,7 @@ def _handle_incoming(event, recv, error, cmd_queue): def process( send: Callable[[Dict], None], recv: Callable[[], Dict], - handler: Callable[[str, List, Dict, Event, Callable[[str], None]], Dict], + handler: Callable[[str, List, Dict, Event, Callable[[str], None]], RpcResponse], ) -> None: def error(status: str, message: str, body: Dict = {}): send(dict(kind="error", status=status, message=message, body=body)) @@ -88,8 +88,8 @@ def error(status: str, message: str, body: Dict = {}): def signal(status: str, body: Dict = {}): send(dict(kind="signal", status=status, body=body)) - def success(body: Dict): - send(dict(kind="success", body=body)) + def success(response: RpcResponse): + send(dict(kind="success", body=response.body, flags=response.flags)) event = Event() cmd_queue: Queue = Queue(1) diff --git a/helper/helper/base.py b/helper/helper/base.py index 3d0212763..622273506 100644 --- a/helper/helper/base.py +++ b/helper/helper/base.py @@ -27,6 +27,12 @@ def encode_bytes(value: bytes) -> str: decode_bytes = bytes.fromhex +class RpcResponse: + def __init__(self, body, flags=None): + self.body = body + self.flags = flags or [] + + class RpcException(Exception): """An exception that is returned as the result of an RPC command.i @@ -116,16 +122,20 @@ def __call__(self, action, target, params, event, signal, traversed=None): try: if target: traversed += [target[0]] - return self.get_child(target[0])( + response = self.get_child(target[0])( action, target[1:], params, event, signal, traversed ) - if action in self.list_actions(): - return self.get_action(action)(params, event, signal) - if action in self.list_children(): + elif action in self.list_actions(): + response = self.get_action(action)(params, event, signal) + elif action in self.list_children(): traversed += [action] - return self.get_child(action)( + response = self.get_child(action)( "get", [], params, event, signal, traversed ) + + if isinstance(response, RpcResponse): + return response + return RpcResponse(response) except ChildResetException as e: self._close_child() raise StateResetException(e.message, traversed) diff --git a/helper/helper/device.py b/helper/helper/device.py index d5d79f930..a2db4192e 100644 --- a/helper/helper/device.py +++ b/helper/helper/device.py @@ -31,18 +31,28 @@ from ykman.device import scan_devices, list_all_devices from ykman.diagnostics import get_diagnostics from ykman.logging import set_log_level -from yubikit.core import TRANSPORT -from yubikit.core.smartcard import SmartCardConnection, ApduError, SW +from yubikit.core import TRANSPORT, NotSupportedError +from yubikit.core.smartcard import ( + SmartCardConnection, + ApduError, + SW, + SmartCardProtocol, + ApplicationNotAvailableError, +) +from yubikit.core.smartcard.scp import Scp11KeyParams from yubikit.core.otp import OtpConnection from yubikit.core.fido import FidoConnection from yubikit.support import get_name, read_info from yubikit.management import CAPABILITY +from yubikit.securitydomain import SecurityDomainSession from yubikit.logging import LOG_LEVEL from ykman.pcsc import list_devices, YK_READER_NAME from smartcard.Exceptions import SmartcardException, NoCardException from smartcard.pcsc.PCSCExceptions import EstablishContextException from smartcard.CardMonitoring import CardObserver, CardMonitor +from fido2.ctap import CtapError +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey from hashlib import sha256 from dataclasses import asdict from typing import Mapping, Tuple @@ -255,12 +265,21 @@ def __init__(self, device, info): super().__init__() self._device = device self._info = info + self._data = None def __call__(self, *args, **kwargs): try: - return super().__call__(*args, **kwargs) + response = super().__call__(*args, **kwargs) + if "device_info" in response.flags: + # Clear DeviceInfo cache + self._info = None + self._data = None + + return response + except (SmartcardException, OSError): logger.exception("Device error") + self._child = None name = self._child_name self._child_name = None @@ -273,6 +292,14 @@ def create_child(self, name): logger.exception(f"Unable to create child {name}") raise NoSuchNodeException(name) + def get_data(self): + if not self._data: + self._data = self._refresh_data() + return self._data + + def _refresh_data(self): + ... + def _read_data(self, conn): pid = self._device.pid self._info = read_info(conn, pid) @@ -293,7 +320,7 @@ def _create_connection(self, conn_type): connection = self._device.open_connection(conn_type) return ConnectionNode(self._device, connection, self._info) - def get_data(self): + def _refresh_data(self): for conn_type in (SmartCardConnection, OtpConnection, FidoConnection): if self._supports_connection(conn_type): try: @@ -326,13 +353,18 @@ def fido(self): except (ValueError, OSError) as e: logger.warning("Error opening connection", exc_info=True) raise ConnectionException(self._device.fingerprint, "fido", e) + except Exception as e: # TODO: Replace with ConnectionError once added + if "Wrong" in str(e): + logger.warning("Error opening connection", exc_info=True) + raise ConnectionException(self._device.fingerprint, "fido", e) + raise class _ReaderObserver(CardObserver): def __init__(self, device): self.device = device self.card = None - self.data = None + self.needs_refresh = True def update(self, observable, actions): added, removed = actions @@ -343,10 +375,13 @@ def update(self, observable, actions): break else: self.card = None - self.data = None + self.needs_refresh = True logger.debug(f"NFC card: {self.card}") +RESTRICTED_NDEF = bytes.fromhex("001fd1011b5504") + b"yubico.com/getting-started" + + class ReaderDeviceNode(AbstractDeviceNode): def __init__(self, device, info): super().__init__(device, info) @@ -359,18 +394,37 @@ def close(self): super().close() def get_data(self): - if self._observer.data is None: - card = self._observer.card - if card is None: - return dict(present=False, status="no-card") - try: - with self._device.open_connection(SmartCardConnection) as conn: - self._observer.data = dict(self._read_data(conn), present=True) - except NoCardException: - return dict(present=False, status="no-card") - except ValueError: - self._observer.data = dict(present=False, status="unknown-device") - return self._observer.data + if self._observer.needs_refresh: + self._data = None + return super().get_data() + + def _refresh_data(self): + card = self._observer.card + if card is None: + return dict(present=False, status="no-card") + try: + with self._device.open_connection(SmartCardConnection) as conn: + try: + data = dict(self._read_data(conn), present=True) + except ValueError: + # Unknown device, maybe NFC restricted + try: + p = SmartCardProtocol(conn) + p.select(bytes.fromhex("D2760000850101")) + p.send_apdu(0, 0xA4, 0x00, 0x0C, bytes.fromhex("E104")) + ndef = p.send_apdu(0, 0xB0, 0, 0) + except (ApduError, ApplicationNotAvailableError): + ndef = None + + if ndef == RESTRICTED_NDEF: + data = dict(present=False, status="restricted-nfc") + else: + data = dict(present=False, status="unknown-device") + + self._observer.needs_refresh = False + return data + except NoCardException: + return dict(present=False, status="no-card") @action(closes_child=False) def get(self, params, event, signal): @@ -381,7 +435,7 @@ def ccid(self): try: connection = self._device.open_connection(SmartCardConnection) info = read_info(connection) - return ConnectionNode(self._device, connection, info) + return ScpConnectionNode(self._device, connection, info) except (ValueError, SmartcardException, EstablishContextException) as e: logger.warning("Error opening connection", exc_info=True) raise ConnectionException(self._device.fingerprint, "ccid", e) @@ -408,7 +462,20 @@ def __init__(self, device, connection, info): def __call__(self, *args, **kwargs): try: - return super().__call__(*args, **kwargs) + response = super().__call__(*args, **kwargs) + if "device_info" in response.flags: + # Refresh DeviceInfo + info = read_info(self._connection, self._device.pid) + if self._info != info: + self._info = info + # Make sure any child node is re-opened after this, + # as enabled applications may have changed + self.close() + else: + # No change to DeviceInfo, further propagation not needed. + response.flags.remove("device_info") + + return response except (SmartcardException, OSError) as e: logger.exception("Connection error") raise ChildResetException(f"{e}") @@ -416,6 +483,14 @@ def __call__(self, *args, **kwargs): if e.sw == SW.INVALID_INSTRUCTION: raise ChildResetException(f"SW: {e.sw}") raise e + except CtapError as e: + if e.code == CtapError.ERR.CHANNEL_BUSY: + raise ChildResetException(str(e)) + raise + except Exception as e: # TODO: Replace with ConnectionError once added + if "Wrong" in str(e): + raise ChildResetException(str(e)) + raise @property def capabilities(self): @@ -436,33 +511,36 @@ def get_data(self): self._info = read_info(self._connection, self._device.pid) return dict(version=self._info.version, serial=self._info.serial) + def _init_child_node(self, child_cls, capability=CAPABILITY(0)): + return child_cls(self._connection) + @child( condition=lambda self: self._transport == TRANSPORT.USB or isinstance(self._connection, SmartCardConnection) ) def management(self): - return ManagementNode(self._connection) + return self._init_child_node(ManagementNode) @child( condition=lambda self: isinstance(self._connection, SmartCardConnection) and CAPABILITY.OATH in self.capabilities ) def oath(self): - return OathNode(self._connection) + return self._init_child_node(OathNode, CAPABILITY.OATH) @child( condition=lambda self: isinstance(self._connection, SmartCardConnection) and CAPABILITY.PIV in self.capabilities ) def piv(self): - return PivNode(self._connection) + return self._init_child_node(PivNode, CAPABILITY.PIV) @child( condition=lambda self: isinstance(self._connection, FidoConnection) and CAPABILITY.FIDO2 in self.capabilities ) def ctap2(self): - return Ctap2Node(self._connection) + return self._init_child_node(Ctap2Node) @child( condition=lambda self: CAPABILITY.OTP in self.capabilities @@ -479,4 +557,31 @@ def ctap2(self): ) ) def yubiotp(self): - return YubiOtpNode(self._connection) + return self._init_child_node(YubiOtpNode) + + +class ScpConnectionNode(ConnectionNode): + def __init__(self, device, connection, info): + super().__init__(device, connection, info) + + self.fips_capable = info.fips_capable + self.scp_params = None + try: + if self.fips_capable != 0: + scp = SecurityDomainSession(connection) + + for ref in scp.get_key_information().keys(): + if ref.kid == 0x13: + chain = scp.get_certificate_bundle(ref) + if chain: + pub_key = chain[-1].public_key() + assert isinstance(pub_key, EllipticCurvePublicKey) # nosec + self.scp_params = Scp11KeyParams(ref, pub_key) + break + except NotSupportedError: + pass + + def _init_child_node(self, child_cls, capability=CAPABILITY(0)): + if capability in self.fips_capable: + return child_cls(self._connection, self.scp_params) + return child_cls(self._connection) diff --git a/helper/helper/fido.py b/helper/helper/fido.py index b334eec9e..181cac426 100644 --- a/helper/helper/fido.py +++ b/helper/helper/fido.py @@ -13,6 +13,7 @@ # limitations under the License. from .base import ( + RpcResponse, RpcNode, action, child, @@ -22,7 +23,7 @@ PinComplexityException, ) from fido2.ctap import CtapError -from fido2.ctap2 import Ctap2, ClientPin +from fido2.ctap2 import Ctap2, ClientPin, Config from fido2.ctap2.credman import CredentialManagement from fido2.ctap2.bio import BioEnrollment, FPBioEnrollment, CaptureError from fido2.pcsc import CtapPcscDevice @@ -185,11 +186,16 @@ def reset(self, params, event, signal): try: self.ctap.reset(event=event) except CtapError as e: - if e.code == CtapError.ERR.USER_ACTION_TIMEOUT: + if e.code in ( + # Different keys respond with different errors here + CtapError.ERR.USER_ACTION_TIMEOUT, + CtapError.ERR.ACTION_TIMEOUT, + ): raise InactivityException() + raise self._info = self.ctap.get_info() self._token = None - return dict() + return RpcResponse(dict(), ["device_info"]) @action(condition=lambda self: self._info.options["clientPin"]) def unlock(self, params, event, signal): @@ -199,6 +205,8 @@ def unlock(self, params, event, signal): permissions |= ClientPin.PERMISSION.CREDENTIAL_MGMT if BioEnrollment.is_supported(self._info): permissions |= ClientPin.PERMISSION.BIO_ENROLL + if Config.is_supported(self._info): + permissions |= ClientPin.PERMISSION.AUTHENTICATOR_CFG try: if permissions: self._token = self.client_pin.get_pin_token(pin, permissions) @@ -224,10 +232,18 @@ def set_pin(self, params, event, signal): params.pop("new_pin"), ) self._info = self.ctap.get_info() - return dict() + return RpcResponse(dict(), ["device_info"]) except CtapError as e: return _handle_pin_error(e, self.client_pin) + @action(condition=lambda self: Config.is_supported(self._info)) + def enable_ep_attestation(self, params, event, signal): + if self._info.options["clientPin"] and not self._token: + raise AuthRequiredException() + config = Config(self.ctap, self.client_pin.protocol, self._token) + config._call(Config.CMD.ENABLE_ENTERPRISE_ATT) + return dict() + @child(condition=lambda self: BioEnrollment.is_supported(self._info)) def fingerprints(self): if not self._token: @@ -322,6 +338,7 @@ def get_data(self): def delete(self, params, event, signal): self.credman.delete_cred(self.data["credential_id"]) self.refresh_rps() + return dict() class FingerprintsNode(RpcNode): @@ -390,8 +407,10 @@ def rename(self, params, event, signal): self.bio.set_name(self.template_id, name) self.name = name self.refresh() + return dict() @action def delete(self, params, event, signal): self.bio.remove_enrollment(self.template_id) self.refresh() + return dict() diff --git a/helper/helper/management.py b/helper/helper/management.py index 8fc024389..3086a1bde 100644 --- a/helper/helper/management.py +++ b/helper/helper/management.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .base import RpcNode, action +from .base import RpcResponse, RpcNode, action from yubikit.core import require_version, NotSupportedError, TRANSPORT, Connection from yubikit.core.smartcard import SmartCardConnection from yubikit.core.otp import OtpConnection @@ -28,10 +28,10 @@ class ManagementNode(RpcNode): - def __init__(self, connection): + def __init__(self, connection, scp_params=None): super().__init__() self._connection_type: Type[Connection] = type(connection) - self.session = ManagementSession(connection) + self.session = ManagementSession(connection, scp_params) def get_data(self): try: @@ -90,7 +90,7 @@ def configure(self, params, event, signal): if reboot: enabled = config.enabled_capabilities.get(TRANSPORT.USB) self._await_reboot(serial, enabled) - return dict() + return RpcResponse(dict(), ["device_info"]) @action def set_mode(self, params, event, signal): @@ -106,4 +106,4 @@ def set_mode(self, params, event, signal): ) def device_reset(self, params, event, signal): self.session.device_reset() - return dict() + return RpcResponse(dict(), ["device_info"]) diff --git a/helper/helper/oath.py b/helper/helper/oath.py index 1f434d198..c56efbbfb 100644 --- a/helper/helper/oath.py +++ b/helper/helper/oath.py @@ -13,6 +13,7 @@ # limitations under the License. from .base import ( + RpcResponse, RpcNode, action, child, @@ -77,9 +78,9 @@ def _get_access_key(self, device_id): logger.warning("Failed to unwrap access key", exc_info=True) return None - def __init__(self, connection): + def __init__(self, connection, scp_params=None): super().__init__() - self.session = OathSession(connection) + self.session = OathSession(connection, scp_params) self._key_verifier = None if self.session.locked: @@ -193,7 +194,7 @@ def set_key(self, params, event, signal): self.session.set_key(key) self._set_key_verifier(key) remember &= self._remember_key(key if remember else None) - return dict(remembered=remember) + return RpcResponse(dict(remembered=remember), ["device_info"]) @action(condition=lambda self: self.session.has_key) def unset_key(self, params, event, signal): @@ -207,7 +208,7 @@ def reset(self, params, event, signal): self.session.reset() self._key_verifier = None self._remember_key(None) - return dict() + return RpcResponse(dict(), ["device_info"]) @child def accounts(self): diff --git a/helper/helper/piv.py b/helper/helper/piv.py index 468271200..7045343b9 100644 --- a/helper/helper/piv.py +++ b/helper/helper/piv.py @@ -13,6 +13,7 @@ # limitations under the License. from .base import ( + RpcResponse, RpcNode, action, child, @@ -91,9 +92,9 @@ def _handle_pin_puk_error(e): class PivNode(RpcNode): - def __init__(self, connection): + def __init__(self, connection, scp_params=None): super().__init__() - self.session = PivSession(connection) + self.session = PivSession(connection, scp_params) self._pivman_data = get_pivman_data(self.session) self._authenticated = False @@ -134,11 +135,18 @@ def get_data(self): pin_attempts = self.session.get_pin_attempts() metadata = None + try: + self.session.get_bio_metadata() + supports_bio = True + except NotSupportedError: + supports_bio = False + 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, + supports_bio=supports_bio, chuid=self._get_object(OBJECT_ID.CHUID), ccc=self._get_object(OBJECT_ID.CAPABILITY), pin_attempts=pin_attempts, @@ -212,7 +220,7 @@ def set_key(self, params, event, signal): 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() + return RpcResponse(dict(), ["device_info"]) @action def change_pin(self, params, event, signal): @@ -220,9 +228,9 @@ def change_pin(self, params, event, signal): new_pin = params.pop("new_pin") try: pivman_change_pin(self.session, old_pin, new_pin) + return RpcResponse(dict(), ["device_info"]) except Exception as e: _handle_pin_puk_error(e) - return dict() @action def change_puk(self, params, event, signal): @@ -230,9 +238,9 @@ def change_puk(self, params, event, signal): new_puk = params.pop("new_puk") try: self.session.change_puk(old_puk, new_puk) + return RpcResponse(dict(), ["device_info"]) except Exception as e: _handle_pin_puk_error(e) - return dict() @action def unblock_pin(self, params, event, signal): @@ -240,16 +248,16 @@ def unblock_pin(self, params, event, signal): new_pin = params.pop("new_pin") try: self.session.unblock_pin(puk, new_pin) + return RpcResponse(dict(), ["device_info"]) except Exception as e: _handle_pin_puk_error(e) - 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() + return RpcResponse(dict(), ["device_info"]) @child def slots(self): @@ -266,9 +274,11 @@ def examine_file(self, params, event, signal): 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, + key_type=( + KEY_TYPE.from_public_key(private_key.public_key()) + if private_key + else None + ), cert_info=_get_cert_info(certificate), ) except InvalidPasswordError: @@ -413,9 +423,11 @@ def get_data(self): id=f"{int(self.slot):02x}", name=self.slot.name, metadata=_metadata_dict(self.metadata), - certificate=self.certificate.public_bytes(encoding=Encoding.PEM).decode() - if self.certificate - else None, + certificate=( + self.certificate.public_bytes(encoding=Encoding.PEM).decode() + if self.certificate + else None + ), ) @action(condition=lambda self: self.certificate or self.metadata) @@ -490,19 +502,24 @@ def import_file(self, params, event, signal): self._refresh() - return dict( + response = dict( metadata=_metadata_dict(metadata), - 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, + 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 + ), ) + return RpcResponse(response, ["device_info"]) @action def generate(self, params, event, signal): @@ -554,4 +571,5 @@ def generate(self, params, event, signal): self._refresh() - return dict(public_key=public_key_pem, result=result) + response = dict(public_key=public_key_pem, result=result) + return RpcResponse(response, ["device_info"]) diff --git a/helper/helper/yubiotp.py b/helper/helper/yubiotp.py index 0f9eaf59d..5da9d057c 100644 --- a/helper/helper/yubiotp.py +++ b/helper/helper/yubiotp.py @@ -40,9 +40,9 @@ class YubiOtpNode(RpcNode): - def __init__(self, connection): + def __init__(self, connection, scp_params=None): super().__init__() - self.session = YubiOtpSession(connection) + self.session = YubiOtpSession(connection, scp_params) def get_data(self): state = self.session.get_config_state() @@ -154,6 +154,7 @@ def delete(self, params, event, signal): access_code = params.pop("curr_acc_code", None) access_code = bytes.fromhex(access_code) if access_code else None self.session.delete_slot(self.slot, access_code) + return dict() except CommandError: raise ValueError(_FAIL_MSG) diff --git a/helper/poetry.lock b/helper/poetry.lock index 1f15a2f51..f95858e1a 100644 --- a/helper/poetry.lock +++ b/helper/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "altgraph" @@ -11,65 +11,95 @@ files = [ {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"}, ] +[[package]] +name = "backports-tarfile" +version = "1.2.0" +description = "Backport of CPython tarfile module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, + {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] + [[package]] name = "cffi" -version = "1.16.0" +version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] @@ -102,43 +132,38 @@ files = [ [[package]] name = "cryptography" -version = "42.0.5" +version = "43.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16"}, - {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec"}, - {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb"}, - {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4"}, - {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278"}, - {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"}, - {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee"}, - {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1"}, - {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d"}, - {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da"}, - {file = "cryptography-42.0.5-cp37-abi3-win32.whl", hash = "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74"}, - {file = "cryptography-42.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940"}, - {file = "cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8"}, - {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1"}, - {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e"}, - {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc"}, - {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a"}, - {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7"}, - {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922"}, - {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc"}, - {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30"}, - {file = "cryptography-42.0.5-cp39-abi3-win32.whl", hash = "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413"}, - {file = "cryptography-42.0.5-cp39-abi3-win_amd64.whl", hash = "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400"}, - {file = "cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8"}, - {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2"}, - {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c"}, - {file = "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576"}, - {file = "cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6"}, - {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e"}, - {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac"}, - {file = "cryptography-42.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd"}, - {file = "cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1"}, + {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, + {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, + {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, + {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, + {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, + {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, + {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, ] [package.dependencies] @@ -151,18 +176,18 @@ nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] name = "exceptiongroup" -version = "1.2.1" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -187,40 +212,44 @@ pcsc = ["pyscard (>=1.9,<3)"] [[package]] name = "importlib-metadata" -version = "7.1.0" +version = "8.4.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, - {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, + {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, + {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "importlib-resources" -version = "6.4.0" +version = "6.4.5" description = "Read resources from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, - {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, + {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, + {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, ] [package.dependencies] zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] +type = ["pytest-mypy"] [[package]] name = "iniconfig" @@ -251,6 +280,42 @@ more-itertools = "*" docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +[[package]] +name = "jaraco-context" +version = "6.0.1" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"}, + {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"}, +] + +[package.dependencies] +"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-functools" +version = "4.0.2" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.functools-4.0.2-py3-none-any.whl", hash = "sha256:c9d16a3ed4ccb5a889ad8e0b7a343401ee5b2a71cee6ed192d3f68bc351e94e3"}, + {file = "jaraco_functools-4.0.2.tar.gz", hash = "sha256:3460c74cd0d32bf82b9576bbb3527c4364d5b27a21f5158a62aed6c4b42e23f5"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + [[package]] name = "jeepney" version = "0.8.0" @@ -268,27 +333,29 @@ trio = ["async_generator", "trio"] [[package]] name = "keyring" -version = "24.3.1" +version = "25.3.0" description = "Store and access your passwords safely." optional = false python-versions = ">=3.8" files = [ - {file = "keyring-24.3.1-py3-none-any.whl", hash = "sha256:df38a4d7419a6a60fea5cef1e45a948a3e8430dd12ad88b0f423c5c143906218"}, - {file = "keyring-24.3.1.tar.gz", hash = "sha256:c3327b6ffafc0e8befbdb597cacdb4928ffe5c1212f7645f186e6d9957a898db"}, + {file = "keyring-25.3.0-py3-none-any.whl", hash = "sha256:8d963da00ccdf06e356acd9bf3b743208878751032d8599c6cc89eb51310ffae"}, + {file = "keyring-25.3.0.tar.gz", hash = "sha256:8d85a1ea5d6db8515b59e1c5d1d1678b03cf7fc8b8dcfb1651e8c4a524eb42ef"}, ] [package.dependencies] importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} importlib-resources = {version = "*", markers = "python_version < \"3.9\""} "jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] completion = ["shtab (>=1.1.0)"] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [[package]] name = "macholib" @@ -306,66 +373,70 @@ altgraph = ">=0.17" [[package]] name = "more-itertools" -version = "10.2.0" +version = "10.5.0" description = "More routines for operating on iterables, beyond itertools" optional = false python-versions = ">=3.8" files = [ - {file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"}, - {file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"}, + {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, + {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, ] [[package]] name = "mss" -version = "9.0.1" +version = "9.0.2" description = "An ultra fast cross-platform multiple screenshots module in pure python using ctypes." optional = false python-versions = ">=3.8" files = [ - {file = "mss-9.0.1-py3-none-any.whl", hash = "sha256:7ee44db7ab14cbea6a3eb63813c57d677a109ca5979d3b76046e4bddd3ca1a0b"}, - {file = "mss-9.0.1.tar.gz", hash = "sha256:6eb7b9008cf27428811fa33aeb35f3334db81e3f7cc2dd49ec7c6e5a94b39f12"}, + {file = "mss-9.0.2-py3-none-any.whl", hash = "sha256:685fa442cc96d8d88b4eb7aadbcccca7b858e789c9259b603e1ef0e435b60425"}, + {file = "mss-9.0.2.tar.gz", hash = "sha256:c96a4ec73224da7db22bc07ef3cfaa18f8b86900d1872e29113bbcef0093a21e"}, ] +[package.extras] +dev = ["build (==1.2.1)", "mypy (==1.11.2)", "ruff (==0.6.3)", "twine (==5.1.1)", "wheel (==0.44.0)"] +test = ["numpy (==2.1.0)", "pillow (==10.4.0)", "pytest (==8.3.2)", "pytest-cov (==5.0.0)", "pytest-rerunfailures (==14.0.0)", "pyvirtualdisplay (==3.0)", "sphinx (==8.0.2)"] + [[package]] name = "mypy" -version = "1.10.0" +version = "1.11.2" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, - {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, - {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, - {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, - {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, - {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, - {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, - {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, - {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, - {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, - {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, - {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, - {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, - {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, - {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, - {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, - {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, - {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, - {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, + {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, + {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, + {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, + {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, + {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, + {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, + {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, + {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, + {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, + {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, + {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, + {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, + {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, + {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, + {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, + {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, + {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" +typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] @@ -386,106 +457,117 @@ files = [ [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] name = "pefile" -version = "2023.2.7" +version = "2024.8.26" description = "Python PE parsing module" optional = false python-versions = ">=3.6.0" files = [ - {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"}, - {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, + {file = "pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f"}, + {file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"}, ] [[package]] name = "pillow" -version = "10.3.0" +version = "10.4.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, - {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, - {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, - {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, - {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, - {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, - {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, - {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, - {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, - {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, - {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, - {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, - {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, - {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, - {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, - {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, - {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] @@ -520,23 +602,23 @@ files = [ [[package]] name = "pyinstaller" -version = "6.6.0" +version = "6.10.0" description = "PyInstaller bundles a Python application and all its dependencies into a single package." optional = false -python-versions = "<3.13,>=3.8" +python-versions = "<3.14,>=3.8" files = [ - {file = "pyinstaller-6.6.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d2705efe79f8749526f65c4bce70ae88eea8b6adfb051f123122e86542fe3802"}, - {file = "pyinstaller-6.6.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:2aa771693ee3e0a899be3e9d946a24eab9896a98d0d4035f05a22f1193004cfb"}, - {file = "pyinstaller-6.6.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1fc15e8cebf76361568359a40926aa5746fc0a84ca365fb2ac6caeea014a2cd3"}, - {file = "pyinstaller-6.6.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:7c4a55a5d872c118bc7a5e641c2df46ad18585c002d96adad129b4ee8c104463"}, - {file = "pyinstaller-6.6.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:97197593344f11f3dd2bdadbab14c61fbc4cdf9cc692a89b047cb671764c1824"}, - {file = "pyinstaller-6.6.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:00d81ddeee97710245a7ed03b0f9d5a4daf6c3a07adf978487b10991e1e20470"}, - {file = "pyinstaller-6.6.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:b7cab21db6fcfbdab47ee960239d1b44cd95383a4463177bd592613941d67959"}, - {file = "pyinstaller-6.6.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:00996d2090734d9ae4a1e53ed40351b07d593c37118d3e0d435bbcfa8db9edee"}, - {file = "pyinstaller-6.6.0-py3-none-win32.whl", hash = "sha256:cfe3ed214601de0723cb660994b44934efacb77a1cf0e4cc5133da996bcf36ce"}, - {file = "pyinstaller-6.6.0-py3-none-win_amd64.whl", hash = "sha256:e2f55fbbdf8a99ea84b39bc5669a68624473c303486d7eb2cd9063b339f0aa28"}, - {file = "pyinstaller-6.6.0-py3-none-win_arm64.whl", hash = "sha256:abbd591967593dab264bcc3bcb2466c0a1582f19a112e37e916c4212069c7933"}, - {file = "pyinstaller-6.6.0.tar.gz", hash = "sha256:be6bc2c3073d3e84fb7148d3af33ce9b6a7f01cfb154e06314cd1d4c05798a32"}, + {file = "pyinstaller-6.10.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d60fb22859e11483af735aec115fdde09467cdbb29edd9844839f2c920b748c0"}, + {file = "pyinstaller-6.10.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:46d75359668993ddd98630a3669dc5249f3c446e35239b43bc7f4155bc574748"}, + {file = "pyinstaller-6.10.0-py3-none-manylinux2014_i686.whl", hash = "sha256:3398a98fa17d47ccb31f8779ecbdacec025f7adb2f22757a54b706ac8b4fe906"}, + {file = "pyinstaller-6.10.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e9989f354ae4ed8a3bec7bdb37ae0d170751d6520e500f049c7cd0632d31d5c3"}, + {file = "pyinstaller-6.10.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b7c90c91921b3749083115b28f30f40abf2bb481ceff196d2b2ce0eaa2b3d429"}, + {file = "pyinstaller-6.10.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf876d7d93b8b4f28d1ad57fa24645cf43119c79e985dd5e5f7a801245e6f53"}, + {file = "pyinstaller-6.10.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:db05e3f2f10f9f78c56f1fb163d9cb453433429fe4281218ebaf1ebfd39ba942"}, + {file = "pyinstaller-6.10.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:28eca3817f176fdc19747e1afcf434f13bb9f17a644f611be2c5a61b1f498ed7"}, + {file = "pyinstaller-6.10.0-py3-none-win32.whl", hash = "sha256:703e041718987e46ba0568a2c71ecf2459fddef57cf9edf3efeed4a53e3dae3f"}, + {file = "pyinstaller-6.10.0-py3-none-win_amd64.whl", hash = "sha256:95b55966e563e8b8f31a43882aea10169e9a11fdf38e626d86a2907b640c0701"}, + {file = "pyinstaller-6.10.0-py3-none-win_arm64.whl", hash = "sha256:308e0a8670c9c9ac0cebbf1bbb492e71b6675606f2ec78bc4adfc830d209e087"}, + {file = "pyinstaller-6.10.0.tar.gz", hash = "sha256:143840f8056ff7b910bf8f16f6cd92cc10a6c2680bb76d0a25d558d543d21270"}, ] [package.dependencies] @@ -545,7 +627,7 @@ importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} packaging = ">=22.0" pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} -pyinstaller-hooks-contrib = ">=2024.3" +pyinstaller-hooks-contrib = ">=2024.8" pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} setuptools = ">=42.0.0" @@ -555,13 +637,13 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2024.5" +version = "2024.8" description = "Community maintained hooks for PyInstaller" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pyinstaller_hooks_contrib-2024.5-py2.py3-none-any.whl", hash = "sha256:0852249b7fb1e9394f8f22af2c22fa5294c2c0366157969f98c96df62410c4c6"}, - {file = "pyinstaller_hooks_contrib-2024.5.tar.gz", hash = "sha256:aa5dee25ea7ca317ad46fa16b5afc8dba3b0e43f2847e498930138885efd3cab"}, + {file = "pyinstaller_hooks_contrib-2024.8-py3-none-any.whl", hash = "sha256:0057fe9a5c398d3f580e73e58793a1d4a8315ca91c3df01efea1c14ed557825a"}, + {file = "pyinstaller_hooks_contrib-2024.8.tar.gz", hash = "sha256:29b68d878ab739e967055b56a93eb9b58e529d5b054fbab7a2f2bacf80cef3e2"}, ] [package.dependencies] @@ -571,25 +653,25 @@ setuptools = ">=42.0.0" [[package]] name = "pyscard" -version = "2.0.9" +version = "2.1.1" description = "Smartcard module for Python." optional = false python-versions = "*" files = [ - {file = "pyscard-2.0.9-cp310-cp310-win32.whl", hash = "sha256:c192e48ac21b5c009ab9d6af7af876fa9f8d72ca546c508fb66392c17ba0a018"}, - {file = "pyscard-2.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:368a12a175a6e0746ff6a86fc98c3949d3eca5cddf4fe1496e747a38d8298e4b"}, - {file = "pyscard-2.0.9-cp311-cp311-win32.whl", hash = "sha256:bca5e59b964dc10646796937d862880211222fc9834820e4d224b109ca98976f"}, - {file = "pyscard-2.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:2507af4c90866fd6beb6910503f6f70ae3077805dbc2eb158ce9b44fd775fead"}, - {file = "pyscard-2.0.9-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:a7bb2c2596f62143a39942487ce7d769334700e66e54cf6a3195091e0eb202db"}, - {file = "pyscard-2.0.9-cp312-cp312-win32.whl", hash = "sha256:2b78b1d1f9901b4f272628dbf40722116e263d27c3cdd4a75c4c846f42ff7d89"}, - {file = "pyscard-2.0.9-cp312-cp312-win_amd64.whl", hash = "sha256:8ae085d9b240533a204c5175b5698f2efa5a102327cc5dddef931f17197dcd10"}, - {file = "pyscard-2.0.9-cp37-cp37m-win32.whl", hash = "sha256:e96f83f36e5e6efdcb375923eb30af64eb5acddf24663c69dcca2ece2f77b47c"}, - {file = "pyscard-2.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:8809e98e81ee4f861b1f056d0ffefb93464d4916705cd51342fb28c0da769e55"}, - {file = "pyscard-2.0.9-cp38-cp38-win32.whl", hash = "sha256:c2c925d2e6ab3d07b8bf77e8d0889069596e9e8cf632ec5096d98346d3a72195"}, - {file = "pyscard-2.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:d537b8b0f6fa01aaa8f0a5834caf87f6c205e2ba07dc1e5a46b4ca8a8f1a2157"}, - {file = "pyscard-2.0.9-cp39-cp39-win32.whl", hash = "sha256:438619c916f46b2ad948529859c99182e66736ab49ba86d3a1f68ec48f1a4662"}, - {file = "pyscard-2.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:b5ee6b4f5fdc618b3c241f0984df9147922e41df13d6a7c01929e87274c6cbda"}, - {file = "pyscard-2.0.9.tar.gz", hash = "sha256:1b74764289ac2fb6efe0a089e38692934e691f97c87aa561af19d1411bb98822"}, + {file = "pyscard-2.1.1-cp310-cp310-win32.whl", hash = "sha256:73738b401bd8ed56dc508e8c035fda876d3612f2fd6ebd1fd1ecb1f70c81280e"}, + {file = "pyscard-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d8e57488f311299b60c1067bb1ad123f0367a356b4b011383ca4d592d365a81e"}, + {file = "pyscard-2.1.1-cp311-cp311-win32.whl", hash = "sha256:07a35f1c2d6521f82c0e5b793f0968cc0b3c2473667435414eb467cb3e5b3765"}, + {file = "pyscard-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:0fb446d519e9ebc437fabc0c797e869ccd9f0340f9d2d72d7edc628f22267ac3"}, + {file = "pyscard-2.1.1-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:df5fa9423b61a2528b91de7a171fe110b7bee7e1d36e1a7e479512014a9e90a2"}, + {file = "pyscard-2.1.1-cp312-cp312-win32.whl", hash = "sha256:bc1f2185e696261f6e29eb0d2beba0c86796e1b4f0b17cd69900c597b591be2a"}, + {file = "pyscard-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5294303aee527999accfe18aec5087223f3e0b164c323f6c62bdbde2d06fca79"}, + {file = "pyscard-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:213cab68b10e3d66702a56d12b5f30a3096a27e25a39a168d292775509d34014"}, + {file = "pyscard-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ee9925d8c69fd853f06fa637f6823463d6cf4353396f455c8e53028fdaa74342"}, + {file = "pyscard-2.1.1-cp38-cp38-win32.whl", hash = "sha256:9e2ee52e4420125af3bf67b7f4dcb80b7f7242e1482d247c13698ae1727a7d00"}, + {file = "pyscard-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:b9bdd5255a69b28e32a79152a66e8ac135d67eb4f81d0e2834a12e263a50f62b"}, + {file = "pyscard-2.1.1-cp39-cp39-win32.whl", hash = "sha256:486e2b52f4de1ecdfe2342fcabb8991d9e8ba713d33cb1a73ad9972fead846ff"}, + {file = "pyscard-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:68c0cbaec6468e2eac599ca90efa2b0427721f9f3ca8dbcf7c5ca3b996045984"}, + {file = "pyscard-2.1.1.tar.gz", hash = "sha256:f9b0dc3fad83ac72a9335af4d04b608edc9d01e2b90e0c38ed0ef1fd014c4414"}, ] [package.extras] @@ -598,13 +680,13 @@ pyro = ["Pyro"] [[package]] name = "pytest" -version = "8.2.0" +version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, - {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] @@ -612,7 +694,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.5,<2.0" +pluggy = ">=1.5,<2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] @@ -643,13 +725,13 @@ files = [ [[package]] name = "pywin32-ctypes" -version = "0.2.2" +version = "0.2.3" description = "A (partial) reimplementation of pywin32 using ctypes/cffi" optional = false python-versions = ">=3.6" files = [ - {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, - {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, ] [[package]] @@ -669,19 +751,23 @@ jeepney = ">=0.6" [[package]] name = "setuptools" -version = "69.5.1" +version = "74.1.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, - {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, + {file = "setuptools-74.1.2-py3-none-any.whl", hash = "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308"}, + {file = "setuptools-74.1.2.tar.gz", hash = "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "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.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] [[package]] name = "tomli" @@ -696,59 +782,63 @@ files = [ [[package]] name = "types-pillow" -version = "10.2.0.20240423" +version = "10.2.0.20240822" description = "Typing stubs for Pillow" optional = false python-versions = ">=3.8" files = [ - {file = "types-Pillow-10.2.0.20240423.tar.gz", hash = "sha256:696e68b9b6a58548fc307a8669830469237c5b11809ddf978ac77fafa79251cd"}, - {file = "types_Pillow-10.2.0.20240423-py3-none-any.whl", hash = "sha256:bd12923093b96c91d523efcdb66967a307f1a843bcfaf2d5a529146c10a9ced3"}, + {file = "types-Pillow-10.2.0.20240822.tar.gz", hash = "sha256:559fb52a2ef991c326e4a0d20accb3bb63a7ba8d40eb493e0ecb0310ba52f0d3"}, + {file = "types_Pillow-10.2.0.20240822-py3-none-any.whl", hash = "sha256:d9dab025aba07aeb12fd50a6799d4eac52a9603488eca09d7662543983f16c5d"}, ] [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "yubikey-manager" -version = "5.4.0" +version = "5.5.1" description = "Tool for managing your YubiKey configuration." optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "yubikey_manager-5.4.0-py3-none-any.whl", hash = "sha256:d53acb06c4028a833be7a05ca4145833afef1affa67aaab4347bc50ecce37985"}, - {file = "yubikey_manager-5.4.0.tar.gz", hash = "sha256:53726a186722cd2683b2f5fd781fc0a2861f47ce62ba9d3527960832c8fabec8"}, + {file = "yubikey_manager-5.5.1-py3-none-any.whl", hash = "sha256:611a6cd088bb18f1ee8d11dfb2c4218ac086d5e40dc718f4a4183b9c4d0e932f"}, + {file = "yubikey_manager-5.5.1.tar.gz", hash = "sha256:2b1f4e70813973c646eb301c8f2513faf5e4736dd3c564422efdce0349c02afd"}, ] [package.dependencies] click = ">=8.0,<9.0" cryptography = ">=3.0,<45" fido2 = ">=1.0,<2.0" -keyring = ">=23.4,<25" +keyring = ">=23.4,<26" pyscard = ">=2.0,<3.0" pywin32 = {version = ">=223", markers = "sys_platform == \"win32\""} [[package]] name = "zipp" -version = "3.18.1" +version = "3.20.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, - {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, + {file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"}, + {file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] [[package]] name = "zxing-cpp" @@ -788,4 +878,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "7543cc0ac90ea4eb701a7f52321d831bfe05c65e0ae896a6107eb7a9540d8543" +content-hash = "123356b2b1ed4b00f453721795be4c7c10a3583c7d7b42368127f4c46461e50c" diff --git a/helper/pyproject.toml b/helper/pyproject.toml index 18fe63fcf..188751162 100644 --- a/helper/pyproject.toml +++ b/helper/pyproject.toml @@ -10,7 +10,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.8" -yubikey-manager = "^5.4" +yubikey-manager = "^5.5" mss = "^9.0.1" Pillow = "^10.2.0" zxing-cpp = "^2.2.0" diff --git a/helper/shell.py b/helper/shell.py index ea0d39145..d5ab9a95f 100755 --- a/helper/shell.py +++ b/helper/shell.py @@ -94,9 +94,10 @@ def completepath(self, text, nodes_only=False): cmd = target.pop() if target else "" node = self.get_node(target) if node: - names = [n + "/" for n in node.get("children", [])] + body = node.get("body", {}) + names = [n + "/" for n in body.get("children", [])] if not nodes_only: - actions = node.get("actions", []) + actions = body.get("actions", []) if "get" in actions: actions.remove("get") names += actions @@ -104,10 +105,10 @@ def completepath(self, text, nodes_only=False): return res return [] - def completedefault(self, cmd, text, *args): + def completedefault(self, cmd, text, *args): # type: ignore return self.completepath(text) - def completenames(self, cmd, text, *ignored): + def completenames(self, cmd, text, *ignored): # type: ignore return self.completepath(text) def emptyline(self): diff --git a/integration_test/keyless_test.dart b/integration_test/keyless_test.dart index 6c8a20eef..b01342c60 100644 --- a/integration_test/keyless_test.dart +++ b/integration_test/keyless_test.dart @@ -15,6 +15,7 @@ */ @Tags(['desktop', 'android']) +library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -35,21 +36,22 @@ void main() { }); group('Settings', () { appTestKeyless('Click through all Themes', (WidgetTester tester) async { - var settingDrawerButton = find.byKey(settingDrawerIcon).hitTestable(); - await tester.tap(settingDrawerButton); - await tester.longWait(); + await tester.tap(find.byKey(actionsIconButtonKey).hitTestable()); + await tester.shortWait(); + await tester.tap(find.byKey(settingDrawerIcon).hitTestable()); + await tester.shortWait(); await tester.tap(find.byKey(themeModeSetting)); - await tester.longWait(); + await tester.shortWait(); await tester .tap(find.byKey(themeModeOption(ThemeMode.light)).hitTestable()); await tester.longWait(); await tester.tap(find.byKey(themeModeSetting)); - await tester.longWait(); + await tester.shortWait(); await tester .tap(find.byKey(themeModeOption(ThemeMode.dark)).hitTestable()); await tester.longWait(); await tester.tap(find.byKey(themeModeSetting)); - await tester.longWait(); + await tester.shortWait(); await tester .tap(find.byKey(themeModeOption(ThemeMode.system)).hitTestable()); await tester.longWait(); @@ -59,6 +61,8 @@ void main() { var helpDrawerButton = find.byKey(helpDrawerIcon).hitTestable(); appTestKeyless('Check Licenses view', (WidgetTester tester) async { + await tester.tap(find.byKey(actionsIconButtonKey).hitTestable()); + await tester.shortWait(); await tester.tap(helpDrawerButton); await tester.shortWait(); var licensesButtonText = find.byKey(licensesButton).hitTestable(); @@ -69,8 +73,10 @@ void main() { }); group('Opening of URLs', () { appTestKeyless('TOS link', (WidgetTester tester) async { + await tester.tap(find.byKey(actionsIconButtonKey).hitTestable()); + await tester.shortWait(); await tester.tap(helpDrawerButton); - await tester.longWait(); + await tester.shortWait(); if (isAndroid) { expect(find.byKey(tosButton).hitTestable(), findsOneWidget); } else { @@ -79,8 +85,10 @@ void main() { } }); appTestKeyless('Privacy link', (WidgetTester tester) async { + await tester.tap(find.byKey(actionsIconButtonKey).hitTestable()); + await tester.shortWait(); await tester.tap(helpDrawerButton); - await tester.longWait(); + await tester.shortWait(); if (isAndroid) { expect(find.byKey(privacyButton).hitTestable(), findsOneWidget); } else { @@ -89,8 +97,10 @@ void main() { } }); appTestKeyless('Feedback link', (WidgetTester tester) async { + await tester.tap(find.byKey(actionsIconButtonKey).hitTestable()); + await tester.shortWait(); await tester.tap(helpDrawerButton); - await tester.longWait(); + await tester.shortWait(); if (isAndroid) { expect(find.byKey(userGuideButton).hitTestable(), findsOneWidget); } else { @@ -99,6 +109,8 @@ void main() { } }); appTestKeyless('Help link', (WidgetTester tester) async { + await tester.tap(find.byKey(actionsIconButtonKey).hitTestable()); + await tester.shortWait(); await tester.tap(helpDrawerButton); await tester.longWait(); if (isAndroid) { @@ -112,14 +124,18 @@ void main() { group('Troubleshooting', () { appTestKeyless('Diagnostics Button', skip: isAndroid, (WidgetTester tester) async { + await tester.tap(find.byKey(actionsIconButtonKey).hitTestable()); + await tester.shortWait(); await tester.tap(helpDrawerButton); - await tester.longWait(); + await tester.shortWait(); await tester.tap(find.byKey(diagnosticsChip).hitTestable()); await tester.longWait(); }); appTestKeyless('Log button', (WidgetTester tester) async { + await tester.tap(find.byKey(actionsIconButtonKey).hitTestable()); + await tester.shortWait(); await tester.tap(helpDrawerButton); - await tester.longWait(); + await tester.shortWait(); await tester.tap(find.byKey(logChip).hitTestable()); await tester.longWait(); }); diff --git a/integration_test/management_test.dart b/integration_test/management_test.dart index ee5667d38..7dba02ae6 100644 --- a/integration_test/management_test.dart +++ b/integration_test/management_test.dart @@ -15,245 +15,290 @@ */ @Tags(['desktop', 'management']) -import 'package:flutter/material.dart'; +library; + import 'package:flutter_test/flutter_test.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 'package:yubico_authenticator/management/views/keys.dart'; import 'utils/test_util.dart'; -Key _getCapabilityWidgetKey(bool isUsb, String name) => - Key('management.keys.capability.${isUsb ? 'usb' : 'nfc'}.$name'); - -Future _getCapabilityWidget(Key key) async { - return find.byKey(key).hitTestable().evaluate().single.widget as FilterChip; -} +// Key _getCapabilityWidgetKey(bool isUsb, String name) => +// Key('management.keys.capability.${isUsb ? 'usb' : 'nfc'}.$name'); +// +// Future _getCapabilityWidget(Key key) async { +// return find.byKey(key).hitTestable().evaluate().single.widget as FilterChip; +// } void main() { var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; - group('Management UI tests', () { - appTest('Drawer items exist', (WidgetTester tester) async { - await tester.openDrawer(); - expect(find.byKey(app_keys.managementAppDrawer).hitTestable(), - findsOneWidget); - }); - }); - group('Toggle Applications on key', () { - appTest('Toggle OTP', (WidgetTester tester) async { - await tester.openManagementScreen(); - - // find USB OTP capability - var usbOtpKey = _getCapabilityWidgetKey(true, 'OTP'); - var otpChip = await _getCapabilityWidget(usbOtpKey); - if (otpChip != null) { - // we expect OTP to be enabled on the Key for this test - expect(otpChip.selected, equals(true)); - await tester.tap(find.byKey(usbOtpKey)); - await tester.shortWait(); - await tester.tap(find.byKey(management_keys.saveButtonKey)); - // long wait - await tester.ultraLongWait(); - expect(find.byKey(management_keys.screenKey), findsNothing); - await tester.shortWait(); - } - await tester.openManagementScreen(); - if (otpChip != null) { - await tester.tap(find.byKey(usbOtpKey)); - await tester.shortWait(); - await tester.tap(find.byKey(management_keys.saveButtonKey)); - // long wait - await tester.ultraLongWait(); - - // no management screen visible now - expect(find.byKey(management_keys.screenKey), findsNothing); - await tester.longWait(); - } - }); - appTest('Toggle PIV', (WidgetTester tester) async { - await tester.openManagementScreen(); - var usbPivKey = _getCapabilityWidgetKey(true, 'PIV'); - var pivChip = await _getCapabilityWidget(usbPivKey); - - // find USB PIV capability - if (pivChip != null) { - expect(pivChip.selected, equals(true)); - await tester.tap(find.byKey(usbPivKey)); - await tester.shortWait(); - await tester.tap(find.byKey(management_keys.saveButtonKey)); - // long wait - await tester.ultraLongWait(); - expect(find.byKey(management_keys.screenKey), findsNothing); - await tester.shortWait(); - } - await tester.openManagementScreen(); - if (pivChip != null) { - // we expect PIV to be enabled on the Key for this test - await tester.tap(find.byKey(usbPivKey)); - await tester.shortWait(); - await tester.tap(find.byKey(management_keys.saveButtonKey)); - // long wait - await tester.ultraLongWait(); - - // no management screen visible now - expect(find.byKey(management_keys.screenKey), findsNothing); - await tester.longWait(); - } - }); - - appTest('Toggle OATH', (WidgetTester tester) async { - await tester.openManagementScreen(); - - // find USB OATH capability - var usbOathKey = _getCapabilityWidgetKey(true, 'OATH'); - var oathChip = await _getCapabilityWidget(usbOathKey); - if (oathChip != null) { - // we expect OATH to be enabled on the Key for this test - expect(oathChip.selected, equals(true)); - await tester.tap(find.byKey(usbOathKey)); - await tester.shortWait(); - await tester.tap(find.byKey(management_keys.saveButtonKey)); - // long wait - await tester.ultraLongWait(); - expect(find.byKey(management_keys.screenKey), findsNothing); - await tester.shortWait(); - } - await tester.openManagementScreen(); - if (oathChip != null) { - await tester.tap(find.byKey(usbOathKey)); - await tester.shortWait(); - await tester.tap(find.byKey(management_keys.saveButtonKey)); - // long wait - await tester.ultraLongWait(); - - // no management screen visible now - expect(find.byKey(management_keys.screenKey), findsNothing); - await tester.longWait(); - } - }); - appTest('Toggle OpenPGP', (WidgetTester tester) async { - await tester.openManagementScreen(); - - // find USB OPENPGP capability - var usbPgpKey = _getCapabilityWidgetKey(true, 'OpenPGP'); - var pgpChip = await _getCapabilityWidget(usbPgpKey); - if (pgpChip != null) { - // we expect OPENPGP to be enabled on the Key for this test - expect(pgpChip.selected, equals(true)); - await tester.tap(find.byKey(usbPgpKey)); - await tester.shortWait(); - await tester.tap(find.byKey(management_keys.saveButtonKey)); - // long wait - await tester.ultraLongWait(); - expect(find.byKey(management_keys.screenKey), findsNothing); - await tester.shortWait(); - } - await tester.openManagementScreen(); - if (pgpChip != null) { - await tester.tap(find.byKey(usbPgpKey)); - await tester.shortWait(); - await tester.tap(find.byKey(management_keys.saveButtonKey)); - // long wait - await tester.ultraLongWait(); - - // no management screen visible now - expect(find.byKey(management_keys.screenKey), findsNothing); - await tester.longWait(); - } - }); - appTest('Toggle YubiHSM Auth', (WidgetTester tester) async { - await tester.openManagementScreen(); - - // find USB YubiHSM Auth capability - var usbHsmKey = _getCapabilityWidgetKey(true, 'YubiHSM Auth'); - var hsmChip = await _getCapabilityWidget(usbHsmKey); - if (hsmChip != null) { - // we expect YubiHSM Auth to be enabled on the Key for this test - expect(hsmChip.selected, equals(true)); - await tester.tap(find.byKey(usbHsmKey)); - await tester.shortWait(); - await tester.tap(find.byKey(management_keys.saveButtonKey)); - // long wait - await tester.ultraLongWait(); - expect(find.byKey(management_keys.screenKey), findsNothing); - await tester.shortWait(); - } - await tester.openManagementScreen(); - if (hsmChip != null) { - await tester.tap(find.byKey(usbHsmKey)); - await tester.shortWait(); - await tester.tap(find.byKey(management_keys.saveButtonKey)); - // long wait - await tester.ultraLongWait(); - - // no management screen visible now - expect(find.byKey(management_keys.screenKey), findsNothing); - await tester.longWait(); - } - }); - }); - appTest('Toggle FIDO U2F', (WidgetTester tester) async { - await tester.openManagementScreen(); - - // find USB FIDO U2F capability - var usbU2fKey = _getCapabilityWidgetKey(true, 'FIDO U2F'); - var u2fChip = await _getCapabilityWidget(usbU2fKey); - if (u2fChip != null) { - // we expect FIDO U2F to be enabled on the Key for this test - expect(u2fChip.selected, equals(true)); - await tester.tap(find.byKey(usbU2fKey)); + appTest('Toggle all but PIV 1', (WidgetTester tester) async { + await tester.openHomeAndToggleScreen(); await tester.shortWait(); - await tester.tap(find.byKey(management_keys.saveButtonKey)); - // long wait - await tester.ultraLongWait(); - expect(find.byKey(management_keys.screenKey), findsNothing); + await tester.tap(find.text('Yubico OTP').hitTestable()); await tester.shortWait(); - } - await tester.openManagementScreen(); - if (u2fChip != null) { - await tester.tap(find.byKey(usbU2fKey)); + await tester.tap(find.text('OATH').hitTestable()); await tester.shortWait(); - await tester.tap(find.byKey(management_keys.saveButtonKey)); - // long wait - await tester.ultraLongWait(); - - // no management screen visible now - expect(find.byKey(management_keys.screenKey), findsNothing); + await tester.tap(find.text('OpenPGP').hitTestable()); + await tester.shortWait(); + await tester.tap(find.text('YubiHSM Auth').hitTestable()); + await tester.shortWait(); + await tester.tap(find.text('FIDO U2F').hitTestable()); + await tester.shortWait(); + await tester.tap(find.text('FIDO2').hitTestable()); + await tester.shortWait(); + await tester.tap(find.byKey(saveButtonKey).hitTestable()); await tester.longWait(); - } - }); - appTest('Toggle FIDO2', (WidgetTester tester) async { - await tester.openManagementScreen(); - - // find USB FIDO2 capability - var usbFido2Key = _getCapabilityWidgetKey(true, 'FIDO2'); - var fido2Chip = await _getCapabilityWidget(usbFido2Key); - if (fido2Chip != null) { - // we expect FIDO2 to be enabled on the Key for this test - expect(fido2Chip.selected, equals(true)); - await tester.tap(find.byKey(usbFido2Key)); + }); + appTest('Toggle all but PIV 2', (WidgetTester tester) async { + await tester.openHomeAndToggleScreen(); await tester.shortWait(); - await tester.tap(find.byKey(management_keys.saveButtonKey)); - // long wait - await tester.ultraLongWait(); - expect(find.byKey(management_keys.screenKey), findsNothing); + await tester.tap(find.text('Yubico OTP').hitTestable()); await tester.shortWait(); - } - await tester.openManagementScreen(); - if (fido2Chip != null) { - await tester.tap(find.byKey(usbFido2Key)); + await tester.tap(find.text('OATH').hitTestable()); await tester.shortWait(); - await tester.tap(find.byKey(management_keys.saveButtonKey)); - // long wait - await tester.ultraLongWait(); - - // no management screen visible now - expect(find.byKey(management_keys.screenKey), findsNothing); + await tester.tap(find.text('OpenPGP').hitTestable()); + await tester.shortWait(); + await tester.tap(find.text('YubiHSM Auth').hitTestable()); + await tester.shortWait(); + await tester.tap(find.text('FIDO U2F').hitTestable()); + await tester.shortWait(); + await tester.tap(find.text('FIDO2').hitTestable()); + await tester.shortWait(); + await tester.tap(find.byKey(saveButtonKey).hitTestable()); await tester.longWait(); - } + }); }); + + // group('OLD: Toggle Applications on key', () { + // appTest('Toggle OTP', (WidgetTester tester) async { + // await tester.openHomeAndToggleScreen(); + // await tester.shortWait(); + // await tester.tap(find.text('Yubico OTP').hitTestable()); + // await tester.shortWait(); + // await tester.tap(find.byKey(saveButtonKey).hitTestable()); + // await tester.ultraLongWait(); + // + // // TODO: expecter that the Yubico OTP is not present + // + // await tester.openHomeAndToggleScreen(); + // await tester.shortWait(); + // await tester.tap(find.text('Yubico OTP').hitTestable()); + // await tester.shortWait(); + // await tester.tap(find.byKey(saveButtonKey).hitTestable()); + // await tester.ultraLongWait(); + // + // // TODO: this is old method of doing this test, review if usable. + // // find USB OTP capability + // // var usbOtpKey = _getCapabilityWidgetKey(true, 'Yubico OTP'); + // // var otpChip = await _getCapabilityWidget(usbOtpKey); + // // if (otpChip != null) { + // // // we expect OTP to be enabled on the Key for this test + // // expect(otpChip.selected, equals(true)); + // // await tester.tap(find.byKey(usbOtpKey)); + // // await tester.shortWait(); + // // await tester.tap(find.byKey(management_keys.saveButtonKey)); + // // // long wait + // // await tester.ultraLongWait(); + // // expect(find.byKey(management_keys.screenKey), findsNothing); + // // await tester.shortWait(); + // // } + // // await tester.openToggleScreen(); + // // if (otpChip != null) { + // // await tester.tap(find.byKey(usbOtpKey)); + // // await tester.shortWait(); + // // await tester.tap(find.byKey(management_keys.saveButtonKey)); + // // // long wait + // // await tester.ultraLongWait(); + // // + // // // no management screen visible now + // // expect(find.byKey(management_keys.screenKey), findsNothing); + // // await tester.longWait(); + // // } + // }); + // appTest('Toggle PIV', (WidgetTester tester) async { + // await tester.openToggleScreen(); + // var usbPivKey = _getCapabilityWidgetKey(true, 'PIV'); + // var pivChip = await _getCapabilityWidget(usbPivKey); + // + // // find USB PIV capability + // if (pivChip != null) { + // expect(pivChip.selected, equals(true)); + // await tester.tap(find.byKey(usbPivKey)); + // await tester.shortWait(); + // await tester.tap(find.byKey(management_keys.saveButtonKey)); + // // long wait + // await tester.ultraLongWait(); + // expect(find.byKey(management_keys.screenKey), findsNothing); + // await tester.shortWait(); + // } + // await tester.openToggleScreen(); + // if (pivChip != null) { + // // we expect PIV to be enabled on the Key for this test + // await tester.tap(find.byKey(usbPivKey)); + // await tester.shortWait(); + // await tester.tap(find.byKey(management_keys.saveButtonKey)); + // // long wait + // await tester.ultraLongWait(); + // + // // no management screen visible now + // expect(find.byKey(management_keys.screenKey), findsNothing); + // await tester.longWait(); + // } + // }); + // + // appTest('Toggle OATH', (WidgetTester tester) async { + // await tester.openToggleScreen(); + // + // // find USB OATH capability + // var usbOathKey = _getCapabilityWidgetKey(true, 'OATH'); + // var oathChip = await _getCapabilityWidget(usbOathKey); + // if (oathChip != null) { + // // we expect OATH to be enabled on the Key for this test + // expect(oathChip.selected, equals(true)); + // await tester.tap(find.byKey(usbOathKey)); + // await tester.shortWait(); + // await tester.tap(find.byKey(management_keys.saveButtonKey)); + // // long wait + // await tester.ultraLongWait(); + // expect(find.byKey(management_keys.screenKey), findsNothing); + // await tester.shortWait(); + // } + // await tester.openToggleScreen(); + // if (oathChip != null) { + // await tester.tap(find.byKey(usbOathKey)); + // await tester.shortWait(); + // await tester.tap(find.byKey(management_keys.saveButtonKey)); + // // long wait + // await tester.ultraLongWait(); + // + // // no management screen visible now + // expect(find.byKey(management_keys.screenKey), findsNothing); + // await tester.longWait(); + // } + // }); + // appTest('Toggle OpenPGP', (WidgetTester tester) async { + // await tester.openToggleScreen(); + // + // // find USB OPENPGP capability + // var usbPgpKey = _getCapabilityWidgetKey(true, 'OpenPGP'); + // var pgpChip = await _getCapabilityWidget(usbPgpKey); + // if (pgpChip != null) { + // // we expect OPENPGP to be enabled on the Key for this test + // expect(pgpChip.selected, equals(true)); + // await tester.tap(find.byKey(usbPgpKey)); + // await tester.shortWait(); + // await tester.tap(find.byKey(management_keys.saveButtonKey)); + // // long wait + // await tester.ultraLongWait(); + // expect(find.byKey(management_keys.screenKey), findsNothing); + // await tester.shortWait(); + // } + // await tester.openToggleScreen(); + // if (pgpChip != null) { + // await tester.tap(find.byKey(usbPgpKey)); + // await tester.shortWait(); + // await tester.tap(find.byKey(management_keys.saveButtonKey)); + // // long wait + // await tester.ultraLongWait(); + // + // // no management screen visible now + // expect(find.byKey(management_keys.screenKey), findsNothing); + // await tester.longWait(); + // } + // }); + // appTest('Toggle YubiHSM Auth', (WidgetTester tester) async { + // await tester.openToggleScreen(); + // + // // find USB YubiHSM Auth capability + // var usbHsmKey = _getCapabilityWidgetKey(true, 'YubiHSM Auth'); + // var hsmChip = await _getCapabilityWidget(usbHsmKey); + // if (hsmChip != null) { + // // we expect YubiHSM Auth to be enabled on the Key for this test + // expect(hsmChip.selected, equals(true)); + // await tester.tap(find.byKey(usbHsmKey)); + // await tester.shortWait(); + // await tester.tap(find.byKey(management_keys.saveButtonKey)); + // // long wait + // await tester.ultraLongWait(); + // expect(find.byKey(management_keys.screenKey), findsNothing); + // await tester.shortWait(); + // } + // await tester.openToggleScreen(); + // if (hsmChip != null) { + // await tester.tap(find.byKey(usbHsmKey)); + // await tester.shortWait(); + // await tester.tap(find.byKey(management_keys.saveButtonKey)); + // // long wait + // await tester.ultraLongWait(); + // + // // no management screen visible now + // expect(find.byKey(management_keys.screenKey), findsNothing); + // await tester.longWait(); + // } + // }); + // }); + // appTest('Toggle FIDO U2F', (WidgetTester tester) async { + // await tester.openToggleScreen(); + // + // // find USB FIDO U2F capability + // var usbU2fKey = _getCapabilityWidgetKey(true, 'FIDO U2F'); + // var u2fChip = await _getCapabilityWidget(usbU2fKey); + // if (u2fChip != null) { + // // we expect FIDO U2F to be enabled on the Key for this test + // expect(u2fChip.selected, equals(true)); + // await tester.tap(find.byKey(usbU2fKey)); + // await tester.shortWait(); + // await tester.tap(find.byKey(management_keys.saveButtonKey)); + // // long wait + // await tester.ultraLongWait(); + // expect(find.byKey(management_keys.screenKey), findsNothing); + // await tester.shortWait(); + // } + // await tester.openToggleScreen(); + // if (u2fChip != null) { + // await tester.tap(find.byKey(usbU2fKey)); + // await tester.shortWait(); + // await tester.tap(find.byKey(management_keys.saveButtonKey)); + // // long wait + // await tester.ultraLongWait(); + // + // // no management screen visible now + // expect(find.byKey(management_keys.screenKey), findsNothing); + // await tester.longWait(); + // } + // }); + // appTest('Toggle FIDO2', (WidgetTester tester) async { + // await tester.openToggleScreen(); + // + // // find USB FIDO2 capability + // var usbFido2Key = _getCapabilityWidgetKey(true, 'FIDO2'); + // var fido2Chip = await _getCapabilityWidget(usbFido2Key); + // if (fido2Chip != null) { + // // we expect FIDO2 to be enabled on the Key for this test + // expect(fido2Chip.selected, equals(true)); + // await tester.tap(find.byKey(usbFido2Key)); + // await tester.shortWait(); + // await tester.tap(find.byKey(management_keys.saveButtonKey)); + // // long wait + // await tester.ultraLongWait(); + // expect(find.byKey(management_keys.screenKey), findsNothing); + // await tester.shortWait(); + // } + // await tester.openToggleScreen(); + // if (fido2Chip != null) { + // await tester.tap(find.byKey(usbFido2Key)); + // await tester.shortWait(); + // await tester.tap(find.byKey(management_keys.saveButtonKey)); + // // long wait + // await tester.ultraLongWait(); + // + // // no management screen visible now + // expect(find.byKey(management_keys.screenKey), findsNothing); + // await tester.longWait(); + // } + // }); } diff --git a/integration_test/oath_test.dart b/integration_test/oath_test.dart index 82c6101d5..3aa4e6906 100644 --- a/integration_test/oath_test.dart +++ b/integration_test/oath_test.dart @@ -15,11 +15,12 @@ */ @Tags(['android', 'desktop', 'oath']) +library; + import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:yubico_authenticator/app/views/keys.dart'; import 'package:yubico_authenticator/core/state.dart'; -import 'package:yubico_authenticator/oath/keys.dart' as keys; import 'package:yubico_authenticator/oath/models.dart'; import 'package:yubico_authenticator/oath/views/account_list.dart'; @@ -30,17 +31,17 @@ void main() { var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; - group('OATH UI tests', () { - appTest('Menu items exist', (WidgetTester tester) async { - await tester.tapActionIconButton(); - await tester.shortWait(); - expect(find.byKey(keys.addAccountAction), findsOneWidget); - expect(find.byKey(keys.setOrManagePasswordAction), findsOneWidget); - // close dialog - await tester.tapTopLeftCorner(); - await tester.longWait(); - }); - }); + // group('OATH UI tests', () { + // appTest('Menu items exist', (WidgetTester tester) async { + // await tester.tapActionIconButton(); + // await tester.shortWait(); + // expect(find.byKey(keys.addAccountAction), findsOneWidget); + // expect(find.byKey(keys.setOrManagePasswordAction), findsOneWidget); + // // close dialog + // await tester.tapTopLeftCorner(); + // await tester.longWait(); + // }); + // }); group('Account creation', () { appTest('Initial reset OATH', (WidgetTester tester) async { @@ -62,11 +63,11 @@ void main() { await tester.addAccount(testAccount); await tester.shortWait(); - expect( - find.descendant( - of: find.byType(AccountList), - matching: find.textContaining(testAccount.name)), - findsOneWidget); + // expect( + // find.descendant( + // of: find.byType(AccountList), + // matching: find.textContaining(testAccount.name)), + // findsOneWidget); await tester.shortWait(); } @@ -239,20 +240,20 @@ void main() { await tester.longWait(); }); - /// adds an account, renames, verifies - appTest('Rename OATH account', (WidgetTester tester) async { - var testAccount = - const Account(issuer: 'IssuerToRename', name: 'NameToRename'); - - /// delete account if it exists - await tester.deleteAccount(testAccount); - await tester.deleteAccount( - const Account(issuer: 'RenamedIssuer', name: 'RenamedName')); - await tester.longWait(); - await tester.addAccount(testAccount); - await tester.longWait(); - await tester.renameAccount(testAccount, 'RenamedIssuer', 'RenamedName'); - }); + // /// adds an account, renames, verifies + // appTest('Rename OATH account', (WidgetTester tester) async { + // var testAccount = + // const Account(issuer: 'IssuerToRename', name: 'NameToRename'); + // + // /// delete account if it exists + // await tester.deleteAccount(testAccount); + // await tester.deleteAccount( + // const Account(issuer: 'RenamedIssuer', name: 'RenamedName')); + // await tester.longWait(); + // await tester.addAccount(testAccount); + // await tester.longWait(); + // await tester.renameAccount(testAccount, 'RenamedIssuer', 'RenamedName'); + // }); }); group('Password tests', () { @@ -263,20 +264,28 @@ void main() { var secondPassword = 'secondPassword'; var thirdPassword = 'thirdPassword'; appTest('Reset OATH', (WidgetTester tester) async { + await tester.tapAppDrawerButton(oathAppDrawer); await tester.resetOATH(); + await tester.longWait(); }); appTest('Set first OATH password', (WidgetTester tester) async { + await tester.tapAppDrawerButton(oathAppDrawer); + // Sets a password for OATH await tester.setOathPassword(firstPassword); }); appTest('Set second OATH password', (WidgetTester tester) async { + await tester.tapAppDrawerButton(oathAppDrawer); + // Without removing the first, change to a second password await tester.unlockOathSession(firstPassword); await tester.replaceOathPassword(firstPassword, secondPassword); }); appTest('Set third OATH password', (WidgetTester tester) async { + await tester.tapAppDrawerButton(oathAppDrawer); + // Without removing the second, set a third password await tester.unlockOathSession(secondPassword); await tester.replaceOathPassword(secondPassword, thirdPassword); diff --git a/integration_test/otp_test.dart b/integration_test/otp_test.dart index ed97ee795..8d1284e06 100644 --- a/integration_test/otp_test.dart +++ b/integration_test/otp_test.dart @@ -15,6 +15,8 @@ */ @Tags(['desktop', 'otp']) +library; + import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:yubico_authenticator/app/views/keys.dart'; diff --git a/integration_test/passkey_test.dart b/integration_test/passkey_test.dart index d09612602..f9d77886a 100644 --- a/integration_test/passkey_test.dart +++ b/integration_test/passkey_test.dart @@ -15,6 +15,8 @@ */ @Tags(['desktop', 'passkey']) +library; + import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:yubico_authenticator/fido/keys.dart'; @@ -45,9 +47,9 @@ void main() { await tester.shortWait(); await tester.enterText(find.byKey(newPin), simplePin); - await tester.shortWait(); + await tester.longWait(); await tester.enterText(find.byKey(confirmPin), simplePin); - await tester.shortWait(); + await tester.longWait(); await tester.tap(find.byKey(saveButton).hitTestable()); await tester.shortWait(); diff --git a/integration_test/piv_test.dart b/integration_test/piv_test.dart index fa986a854..8cabcf92a 100644 --- a/integration_test/piv_test.dart +++ b/integration_test/piv_test.dart @@ -15,6 +15,8 @@ */ @Tags(['desktop', 'piv']) +library; + import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -32,87 +34,121 @@ void main() { group('PIV Settings', skip: isAndroid, () { const factoryPin = '123456'; const factoryPuk = '12345678'; - appTest('Reset PIV (settings-init)', (WidgetTester tester) async { + const uno = 'abcdabcd'; + const due = 'bcdabcda'; + const tre = 'cdabcdab'; + + appTest('reset PIV (settings-init)', (WidgetTester tester) async { await tester.resetPiv(); await tester.shortWait(); }); - appTest('Lock PIN, unlock with PUK', (WidgetTester tester) async { - await tester.configurePiv(); - await tester.shortWait(); - await tester.tap(find.byKey(managePinAction).hitTestable()); + + appTest('pin lock-unlock', (WidgetTester tester) async { + await tester.resetPiv(); await tester.shortWait(); - await tester.lockPinPuk(); + + await tester.pinView(); + await tester.pivFirst(); + + await tester.longWait(); + await tester.pinView(); + await tester.longWait(); + + await tester.pivLockTest(); + + await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.shortWait(); + await tester.tap(find.byKey(actionsIconButtonKey).hitTestable()); + await tester.longWait(); + + expect(find.text('Blocked, use PUK to reset'), findsOne); - /// TODO: This expect needs to verify that Pin subtitle is 'Blocked' - /// expect(find.byKey(managePinAction), find.byTooltip('Blocked')); - await tester.shortWait(); await tester.tap(find.byKey(managePinAction).hitTestable()); await tester.shortWait(); - await tester.enterText(find.byKey(pinPukField).hitTestable(), factoryPuk); + + // PUK field is pre-filled + await tester.pivFirst(); + await tester.tap(find.byKey(actionsIconButtonKey).hitTestable()); + await tester.longWait(); + + expect(find.text('Blocked, use PUK to reset'), findsNothing); + }); + + appTest('lock PUK, lock PIN, factory reset', (WidgetTester tester) async { + await tester.resetPiv(); await tester.shortWait(); - await tester.enterText( - find.byKey(newPinPukField).hitTestable(), factoryPin); + + // set first pin/puk + await tester.pinView(); + await tester.pivFirst(); + await tester.pukView(); + await tester.pivFirst(); + + // lock pin and puk + await tester.pinView(); + await tester.pivLockTest(); + await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.shortWait(); - await tester.enterText( - find.byKey(confirmPinPukField).hitTestable(), factoryPin); + await tester.pukView(); + await tester.pivLockTest(); + await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.shortWait(); - await tester.tap(find.byKey(saveButton).hitTestable()); + + // verify blockedness + + await tester.tap(find.byKey(actionsIconButtonKey).hitTestable()); await tester.shortWait(); + + expect(find.text('Blocked, factory reset needed'), findsAny); + await tester.sendKeyEvent(LogicalKeyboardKey.escape); - await tester.longWait(); - await tester.resetPiv(); - }); - appTest('Lock PUK, lock PIN, factory reset', (WidgetTester tester) async { - await tester.configurePiv(); - await tester.shortWait(); - await tester.tap(find.byKey(managePukAction).hitTestable()); await tester.shortWait(); - await tester.lockPinPuk(); + }); + appTest('Change PIN', (WidgetTester tester) async { + await tester.resetPiv(); await tester.shortWait(); - await tester.tap(find.byKey(actionsIconButtonKey).hitTestable()); - /// TODO: This expect needs to verify that PUK underline is 'Blocked' - /// expect(find.byKey(managePukAction), find.byTooltip('Blocked')); + //reset factorypin + await tester.pinView(); + await tester.pivFirst(); + // onepin + await tester.pinView(); + await tester.enterText(find.byKey(pinPukField).hitTestable(), 'firstpin'); await tester.shortWait(); - await tester.tap(find.byKey(managePinAction).hitTestable()); + await tester.enterText(find.byKey(newPinPukField).hitTestable(), uno); await tester.shortWait(); - await tester.lockPinPuk(); + await tester.enterText(find.byKey(confirmPinPukField).hitTestable(), uno); + await tester.shortWait(); + await tester.tap(find.byKey(saveButton).hitTestable()); await tester.shortWait(); - await tester.tap(find.byKey(actionsIconButtonKey).hitTestable()); - - /// TODO: This expect needs to verify that Pin underline is 'Blocked' - /// expect(find.byKey(managePinAction), find.byTooltip('Blocked')); + // onepin + await tester.pinView(); + await tester.enterText(find.byKey(pinPukField).hitTestable(), uno); await tester.shortWait(); - await tester.tap(find.byKey(managePinAction).hitTestable()); + await tester.enterText(find.byKey(newPinPukField).hitTestable(), due); await tester.shortWait(); - await tester.tap(find.byKey(managePukAction).hitTestable()); + await tester.enterText(find.byKey(confirmPinPukField).hitTestable(), due); await tester.shortWait(); - await tester.sendKeyEvent(LogicalKeyboardKey.escape); - await tester.longWait(); - await tester.resetPiv(); - }); - appTest('Change PIN', (WidgetTester tester) async { - const newpin = '123123'; - await tester.configurePiv(); - await tester.tap(find.byKey(managePinAction).hitTestable()); + await tester.tap(find.byKey(saveButton).hitTestable()); await tester.shortWait(); - await tester.enterText(find.byKey(pinPukField).hitTestable(), factoryPin); + + // onepin + await tester.pinView(); + await tester.enterText(find.byKey(pinPukField).hitTestable(), due); await tester.shortWait(); - await tester.enterText(find.byKey(newPinPukField).hitTestable(), newpin); + await tester.enterText(find.byKey(newPinPukField).hitTestable(), tre); await tester.shortWait(); - await tester.enterText( - find.byKey(confirmPinPukField).hitTestable(), newpin); + await tester.enterText(find.byKey(confirmPinPukField).hitTestable(), tre); await tester.shortWait(); await tester.tap(find.byKey(saveButton).hitTestable()); - await tester.longWait(); - await tester.configurePiv(); - await tester.tap(find.byKey(managePinAction).hitTestable()); await tester.shortWait(); - await tester.enterText(find.byKey(pinPukField).hitTestable(), newpin); + + // factorpin + await tester.pinView(); + await tester.enterText(find.byKey(pinPukField).hitTestable(), tre); await tester.shortWait(); await tester.enterText( find.byKey(newPinPukField).hitTestable(), factoryPin); @@ -121,26 +157,52 @@ void main() { find.byKey(confirmPinPukField).hitTestable(), factoryPin); await tester.shortWait(); await tester.tap(find.byKey(saveButton).hitTestable()); - await tester.longWait(); + await tester.shortWait(); }); appTest('Change PUK', (WidgetTester tester) async { - const newpuk = '12341234'; - await tester.configurePiv(); - await tester.tap(find.byKey(managePukAction).hitTestable()); + await tester.resetPiv(); await tester.shortWait(); - await tester.enterText(find.byKey(pinPukField).hitTestable(), factoryPuk); + + //reset factorypuk + await tester.pinView(); + await tester.pivFirst(); + + // onepin + await tester.pinView(); + await tester.enterText(find.byKey(pinPukField).hitTestable(), 'firstpin'); await tester.shortWait(); - await tester.enterText(find.byKey(newPinPukField).hitTestable(), newpuk); + await tester.enterText(find.byKey(newPinPukField).hitTestable(), uno); await tester.shortWait(); - await tester.enterText( - find.byKey(confirmPinPukField).hitTestable(), newpuk); + await tester.enterText(find.byKey(confirmPinPukField).hitTestable(), uno); await tester.shortWait(); await tester.tap(find.byKey(saveButton).hitTestable()); - await tester.longWait(); - await tester.configurePiv(); - await tester.tap(find.byKey(managePukAction).hitTestable()); await tester.shortWait(); - await tester.enterText(find.byKey(pinPukField).hitTestable(), newpuk); + + // onepin + await tester.pinView(); + await tester.enterText(find.byKey(pinPukField).hitTestable(), uno); + await tester.shortWait(); + await tester.enterText(find.byKey(newPinPukField).hitTestable(), due); + await tester.shortWait(); + await tester.enterText(find.byKey(confirmPinPukField).hitTestable(), due); + await tester.shortWait(); + await tester.tap(find.byKey(saveButton).hitTestable()); + await tester.shortWait(); + + // onepin + await tester.pinView(); + await tester.enterText(find.byKey(pinPukField).hitTestable(), due); + await tester.shortWait(); + await tester.enterText(find.byKey(newPinPukField).hitTestable(), tre); + await tester.shortWait(); + await tester.enterText(find.byKey(confirmPinPukField).hitTestable(), tre); + await tester.shortWait(); + await tester.tap(find.byKey(saveButton).hitTestable()); + await tester.shortWait(); + + // factorpuk + await tester.pinView(); + await tester.enterText(find.byKey(pinPukField).hitTestable(), tre); await tester.shortWait(); await tester.enterText( find.byKey(newPinPukField).hitTestable(), factoryPuk); @@ -149,127 +211,148 @@ void main() { find.byKey(confirmPinPukField).hitTestable(), factoryPuk); await tester.shortWait(); await tester.tap(find.byKey(saveButton).hitTestable()); - await tester.longWait(); - }); - group('PIV Management Key', () { - const newmanagementkey = - 'aaaabbbbccccaaaabbbbccccaaaabbbbccccaaaabbbbcccc'; - const boundsmanagementkey = - 'llllkkkkmmmmllllkkkkmmmmllllkkkkmmmmllllkkkkmmmm'; - const shortmanagementkey = - 'aaaabbbbccccaaaabbbbccccaaaabbbbccccaaaabbbbccc'; - - appTest('Out of bounds managementkey key', (WidgetTester tester) async { - await tester.configurePiv(); - await tester.shortWait(); - await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); - await tester.longWait(); - // testing out of bounds management key does not work - await tester.enterText( - find.byKey(newPinPukField).hitTestable(), boundsmanagementkey); - await tester.longWait(); - await tester.tap(find.byKey(saveButton).hitTestable()); - await tester.longWait(); - expect(tester.isTextButtonEnabled(saveButton), true); - // TODO assert that errorText and errorIcon are shown - }); - - appTest('Short managementkey key', (WidgetTester tester) async { - await tester.configurePiv(); - await tester.shortWait(); - await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); - await tester.longWait(); - // testing too short management key does not work - await tester.enterText( - find.byKey(newPinPukField).hitTestable(), shortmanagementkey); - await tester.longWait(); - expect(tester.isTextButtonEnabled(saveButton), false); - }); - - appTest('Change managementkey key', (WidgetTester tester) async { - await tester.configurePiv(); - await tester.shortWait(); - await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); - await tester.shortWait(); - // setting newmanagementkey - await tester.enterText( - find.byKey(newPinPukField).hitTestable(), newmanagementkey); - await tester.longWait(); - await tester.tap(find.byKey(saveButton).hitTestable()); - await tester.longWait(); - // verifying newmanagementkey - await tester.configurePiv(); - await tester.shortWait(); - await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); - await tester.shortWait(); - await tester.enterText( - find.byKey(managementKeyField).hitTestable(), newmanagementkey); - await tester.shortWait(); - await tester.enterText( - find.byKey(newPinPukField).hitTestable(), newmanagementkey); - await tester.shortWait(); - await tester.tap(find.byKey(saveButton).hitTestable()); - await tester.longWait(); - await tester.resetPiv(); - }); - appTest('Change managementkey type', (WidgetTester tester) async { - await tester.configurePiv(); - await tester.shortWait(); - await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); - await tester.shortWait(); - // TODO: this needs to use manageManagementKeyAction chip - await tester.enterText( - find.byKey(newPinPukField).hitTestable(), newmanagementkey); - await tester.shortWait(); - await tester.tap(find.byKey(saveButton).hitTestable()); - await tester.longWait(); - - await tester.resetPiv(); - await tester.shortWait(); - }); - appTest('Change managementkey PIN-lock', (WidgetTester tester) async { - await tester.configurePiv(); - await tester.shortWait(); - await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); - await tester.shortWait(); - // testing out of bounds management key does not work - await tester.enterText( - find.byKey(newPinPukField).hitTestable(), newmanagementkey); - await tester.shortWait(); - // TODO: Investigate why chip-tap fails - //await tester.tap(find.byKey(pinLockManagementKeyChip).hitTestable()); - //await tester.shortWait(); - await tester.tap(find.byKey(saveButton).hitTestable()); - await tester.longWait(); - await tester.resetPiv(); - await tester.shortWait(); - }); - - appTest('Random managementkeytype', (WidgetTester tester) async { - await tester.configurePiv(); - await tester.shortWait(); - await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); - await tester.shortWait(); - // rndm 3x, for luck - await tester.tap(find.byKey(managementKeyRefresh).hitTestable()); - await tester.shortWait(); - await tester.tap(find.byKey(managementKeyRefresh).hitTestable()); - await tester.shortWait(); - await tester.tap(find.byKey(managementKeyRefresh).hitTestable()); - await tester.shortWait(); - await tester.enterText( - find.byKey(newPinPukField).hitTestable(), newmanagementkey); - await tester.shortWait(); - await tester.tap(find.byKey(saveButton).hitTestable()); - await tester.longWait(); - await tester.resetPiv(); - }); - - appTest('Reset PIV (settings-exit)', (WidgetTester tester) async { - await tester.resetPiv(); - await tester.shortWait(); - }); + await tester.shortWait(); }); + // group('PIV Management Key', () { + // const newmanagementkey = + // 'aaaabbbbccccaaaabbbbccccaaaabbbbccccaaaabbbbcccc'; + // const boundsmanagementkey = + // 'llllkkkkmmmmllllkkkkmmmmllllkkkkmmmmllllkkkkssssllllkkkkmmmmllllkkkkmmmmllllkkkkmmmmllllkkkkmmmm'; + // const shortmanagementkey = + // 'aaaabbbbccccaaaabbbbccccaaaabbbbccccaaaabbbbccc'; + // + // appTest('Out of bounds managementkey key', (WidgetTester tester) async { + // //await tester.resetPiv(); + // await tester.shortWait(); + // + // // short management key + // await tester.configurePiv(); + // await tester.shortWait(); + // await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); + // await tester.longWait(); + // // testing out of bounds management key does not work + // await tester.enterText( + // find.text('New management key').hitTestable(), shortmanagementkey); + // await tester.longWait(); + // + // expect(tester.isTextButtonEnabled(saveButton), false); + // expect(find.text('47/48'), findsOne); + // await tester.sendKeyEvent(LogicalKeyboardKey.escape); + // + // // out of bounds management key + // await tester.configurePiv(); + // await tester.shortWait(); + // await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); + // await tester.shortWait(); + // // testing out of bounds management key does not work + // await tester.enterText( + // find.byKey(newManagementKeyField).hitTestable(), shortmanagementkey); + // await tester.shortWait(); + // + // expect(tester.isTextButtonEnabled(saveButton), false); + // expect(find.text('48/48'), findsOne); + // expect(find.text('llllkkkkmmmmllllkkkkmmmmllllkkkkmmmmllllkkkkssssllllkkkkmmmmllllkkkkmmmmllllkkkkmmmmllllkkkkmmmm'), findsNothing); + // await tester.sendKeyEvent(LogicalKeyboardKey.escape); + // + // + // }); + // + // appTest('Short managementkey key', (WidgetTester tester) async { + // await tester.configurePiv(); + // await tester.shortWait(); + // await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); + // await tester.longWait(); + // // testing too short management key does not work + // await tester.enterText( + // find.byKey(newPinPukField).hitTestable(), shortmanagementkey); + // await tester.longWait(); + // expect(tester.isTextButtonEnabled(saveButton), false); + // }); + // + // appTest('Change managementkey key', (WidgetTester tester) async { + // await tester.configurePiv(); + // await tester.shortWait(); + // await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); + // await tester.shortWait(); + // // setting newmanagementkey + // await tester.enterText( + // find.byKey(newPinPukField).hitTestable(), newmanagementkey); + // await tester.longWait(); + // await tester.tap(find.byKey(saveButton).hitTestable()); + // await tester.longWait(); + // // verifying newmanagementkey + // await tester.configurePiv(); + // await tester.shortWait(); + // await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); + // await tester.shortWait(); + // await tester.enterText( + // find.byKey(managementKeyField).hitTestable(), newmanagementkey); + // await tester.shortWait(); + // await tester.enterText( + // find.byKey(newPinPukField).hitTestable(), newmanagementkey); + // await tester.shortWait(); + // await tester.tap(find.byKey(saveButton).hitTestable()); + // await tester.longWait(); + // await tester.resetPiv(); + // }); + // appTest('Change managementkey type', (WidgetTester tester) async { + // await tester.configurePiv(); + // await tester.shortWait(); + // await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); + // await tester.shortWait(); + // // TODO: this needs to use manageManagementKeyAction chip + // await tester.enterText( + // find.byKey(newPinPukField).hitTestable(), newmanagementkey); + // await tester.shortWait(); + // await tester.tap(find.byKey(saveButton).hitTestable()); + // await tester.longWait(); + // + // await tester.resetPiv(); + // await tester.shortWait(); + // }); + // appTest('Change managementkey PIN-lock', (WidgetTester tester) async { + // await tester.configurePiv(); + // await tester.shortWait(); + // await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); + // await tester.shortWait(); + // // testing out of bounds management key does not work + // await tester.enterText( + // find.byKey(newPinPukField).hitTestable(), newmanagementkey); + // await tester.shortWait(); + // // TODO: Investigate why chip-tap fails + // //await tester.tap(find.byKey(pinLockManagementKeyChip).hitTestable()); + // //await tester.shortWait(); + // await tester.tap(find.byKey(saveButton).hitTestable()); + // await tester.longWait(); + // await tester.resetPiv(); + // await tester.shortWait(); + // }); + // + // appTest('Random managementkeytype', (WidgetTester tester) async { + // await tester.configurePiv(); + // await tester.shortWait(); + // await tester.tap(find.byKey(manageManagementKeyAction).hitTestable()); + // await tester.shortWait(); + // // rndm 3x, for luck + // await tester.tap(find.byKey(managementKeyRefresh).hitTestable()); + // await tester.shortWait(); + // await tester.tap(find.byKey(managementKeyRefresh).hitTestable()); + // await tester.shortWait(); + // await tester.tap(find.byKey(managementKeyRefresh).hitTestable()); + // await tester.shortWait(); + // await tester.enterText( + // find.byKey(newPinPukField).hitTestable(), newmanagementkey); + // await tester.shortWait(); + // await tester.tap(find.byKey(saveButton).hitTestable()); + // await tester.longWait(); + // await tester.resetPiv(); + // }); + // + // appTest('Reset PIV (settings-exit)', (WidgetTester tester) async { + // await tester.resetPiv(); + // await tester.shortWait(); + // }); + // }); }); // Distinguished name schema according to RFC 4514 @@ -301,11 +384,11 @@ void main() { await tester.tap(find.byKey(generateAction).hitTestable()); await tester.longWait(); // 4. enter PIN and click Unlock - await tester.enterText( - find.byKey(managementKeyField).hitTestable(), '123456'); - await tester.longWait(); - await tester.tap(find.byKey(unlockButton).hitTestable()); - await tester.longWait(); + // await tester.enterText( + // find.byKey(managementKeyField).hitTestable(), '123456'); + // await tester.longWait(); + // await tester.tap(find.byKey(unlockButton).hitTestable()); + // await tester.longWait(); // 5. Enter DN await tester.enterText( @@ -329,7 +412,7 @@ void main() { return false; }), findsOneWidget);*/ - await tester.pump(const Duration(milliseconds: 5000)); + await tester.longWait(); // 10. Export Certificate // await tester.tap(find.byKey(exportAction).hitTestable()); // await tester.enterText( @@ -353,11 +436,11 @@ void main() { await tester.tap(find.byKey(generateAction).hitTestable()); await tester.longWait(); // 4. enter PIN and click Unlock - await tester.enterText( - find.byKey(managementKeyField).hitTestable(), '123456'); - await tester.longWait(); - await tester.tap(find.byKey(unlockButton).hitTestable()); - await tester.longWait(); + // await tester.enterText( + // find.byKey(managementKeyField).hitTestable(), '123456'); + // await tester.longWait(); + // await tester.tap(find.byKey(unlockButton).hitTestable()); + // await tester.longWait(); // 5. Enter DN await tester.enterText( @@ -401,11 +484,11 @@ void main() { await tester.tap(find.byKey(generateAction).hitTestable()); await tester.longWait(); // 4. enter PIN and click Unlock - await tester.enterText( - find.byKey(managementKeyField).hitTestable(), '123456'); - await tester.longWait(); - await tester.tap(find.byKey(unlockButton).hitTestable()); - await tester.longWait(); + // await tester.enterText( + // find.byKey(managementKeyField).hitTestable(), '123456'); + // await tester.longWait(); + // await tester.tap(find.byKey(unlockButton).hitTestable()); + // await tester.longWait(); // 5. Enter DN await tester.enterText( @@ -454,11 +537,11 @@ void main() { await tester.tap(find.byKey(generateAction).hitTestable()); await tester.longWait(); // 4. enter PIN and click Unlock - await tester.enterText( - find.byKey(managementKeyField).hitTestable(), '123456'); - await tester.longWait(); - await tester.tap(find.byKey(unlockButton).hitTestable()); - await tester.longWait(); + // await tester.enterText( + // find.byKey(managementKeyField).hitTestable(), '123456'); + // await tester.longWait(); + // await tester.tap(find.byKey(unlockButton).hitTestable()); + // await tester.longWait(); // 5. Enter DN await tester.enterText( @@ -495,59 +578,59 @@ void main() { await tester.tap(find.byKey(deleteButton).hitTestable()); await tester.longWait(); }); - appTest('Import outdated Key+Certificate from file', - (WidgetTester tester) async {}); - - /// TODO fileload needs to be handled - appTest('Import neverexpire Key+Certificate from file', - (WidgetTester tester) async { - /// TODO fileload needs to be handled - }); - appTest('Generate a CSR', (WidgetTester tester) async { - // 1. open PIV view - var pivDrawerButton = find.byKey(pivAppDrawer).hitTestable(); - await tester.tap(pivDrawerButton); - await tester.longWait(); - // 2. click meatball menu for 9e - await tester.tap(find.byKey(meatballButton9e).hitTestable()); - await tester.longWait(); - // 3. click generate - await tester.tap(find.byKey(generateAction).hitTestable()); - await tester.longWait(); - // 4. enter PIN and click Unlock - await tester.enterText( - find.byKey(managementKeyField).hitTestable(), '123456'); - await tester.longWait(); - await tester.tap(find.byKey(unlockButton).hitTestable()); - await tester.longWait(); - - // 5. Enter DN - await tester.enterText( - find.byKey(subjectField).hitTestable(), 'CN=Generate9e-CSR'); - await tester.longWait(); - // 6. Change 'output format': CSR - // enum models.dart, generate_key_dialog.dart - // 7. Choose File Name > Save As > 'File Name generate93-csr' - // TODO: where are files saved? - // 8. click save - await tester.tap(find.byKey(saveButton).hitTestable()); - await tester.longWait(); - // 9 Verify 'No certificate loaded' -/* expect(find.byWidgetPredicate((widget) { - if (widget is TooltipIfTruncated) { - final TooltipIfTruncated textWidget = widget; - if (textWidget.key == certInfoSubjectKey && - textWidget.text == 'CN=Generate9e') { - return true; - } - } - return false; - }), findsOneWidget);*/ - }); - // appTest('Reset PIV (load-exit)', (WidgetTester tester) async { - // /// TODO: investigate why this reset randomly fails! - // await tester.resetPiv(); - // await tester.shortWait(); - // }); +// appTest('Import outdated Key+Certificate from file', +// (WidgetTester tester) async {}); +// +// /// TODO fileload needs to be handled +// appTest('Import neverexpire Key+Certificate from file', +// (WidgetTester tester) async { +// /// TODO fileload needs to be handled +// }); +// appTest('Generate a CSR', (WidgetTester tester) async { +// // 1. open PIV view +// var pivDrawerButton = find.byKey(pivAppDrawer).hitTestable(); +// await tester.tap(pivDrawerButton); +// await tester.longWait(); +// // 2. click meatball menu for 9e +// await tester.tap(find.byKey(meatballButton9e).hitTestable()); +// await tester.longWait(); +// // 3. click generate +// await tester.tap(find.byKey(generateAction).hitTestable()); +// await tester.longWait(); +// // 4. enter PIN and click Unlock +// // await tester.enterText( +// // find.byKey(managementKeyField).hitTestable(), '123456'); +// // await tester.longWait(); +// // await tester.tap(find.byKey(unlockButton).hitTestable()); +// // await tester.longWait(); +// +// // 5. Enter DN +// await tester.enterText( +// find.byKey(subjectField).hitTestable(), 'CN=Generate9e-CSR'); +// await tester.longWait(); +// // 6. Change 'output format': CSR +// // enum models.dart, generate_key_dialog.dart +// // 7. Choose File Name > Save As > 'File Name generate93-csr' +// // TODO: where are files saved? +// // 8. click save +// await tester.tap(find.byKey(saveButton).hitTestable()); +// await tester.longWait(); +// // 9 Verify 'No certificate loaded' +// /* expect(find.byWidgetPredicate((widget) { +// if (widget is TooltipIfTruncated) { +// final TooltipIfTruncated textWidget = widget; +// if (textWidget.key == certInfoSubjectKey && +// textWidget.text == 'CN=Generate9e') { +// return true; +// } +// } +// return false; +// }), findsOneWidget);*/ +// }); +// // appTest('Reset PIV (load-exit)', (WidgetTester tester) async { +// // /// TODO: investigate why this reset randomly fails! +// // await tester.resetPiv(); +// // await tester.shortWait(); +// // }); }); } diff --git a/integration_test/utils/desktop/util.dart b/integration_test/utils/desktop/util.dart index df0adf0fc..63a2047d6 100644 --- a/integration_test/utils/desktop/util.dart +++ b/integration_test/utils/desktop/util.dart @@ -19,4 +19,5 @@ import 'package:yubico_authenticator/desktop/init.dart'; Future startUp(WidgetTester tester, [Map? startUpParams]) async => - tester.pumpWidget(await initialize([]), const Duration(milliseconds: 2000)); + tester.pumpWidget(await initialize([]), + duration: const Duration(milliseconds: 2000)); diff --git a/integration_test/utils/oath_test_util.dart b/integration_test/utils/oath_test_util.dart index 42ef47aa9..337f230dd 100644 --- a/integration_test/utils/oath_test_util.dart +++ b/integration_test/utils/oath_test_util.dart @@ -149,7 +149,7 @@ extension OathFunctions on WidgetTester { Finder findAccountList() { var accountList = find.byType(AccountList).hitTestable(at: Alignment.topCenter); - expect(accountList, findsOneWidget); + // expect(accountList, findsOneWidget); return accountList; } @@ -310,19 +310,23 @@ extension OathFunctions on WidgetTester { await switchToKey(targetKey); await shortWait(); - /// 2. open the key menu - await tapPopupMenu(targetKey); + /// 2. open the home view + await tap(find.byKey(homeDrawer).hitTestable()); + await shortWait(); + + /// 3. open menu + await tap(find.byKey(actionsIconButtonKey).hitTestable()); + await shortWait(); + await tap(find.byKey(yubikeyFactoryResetMenuButton)); await shortWait(); - await tap(find.byKey(yubikeyFactoryResetMenuButton).hitTestable()); - await longWait(); - /// 3. then toggle 'OATH' in the 'Factory reset' reset_dialog.dart + /// 4. then toggle 'Piv' in the 'Factory reset' reset_dialog.dart await tap(find.byKey(factoryResetPickResetOath)); await longWait(); - /// 4. Click reset TextButton: done + /// 5. Click reset TextButton: done await tap(find.byKey(factoryResetReset)); - await shortWait(); + await longWait(); } /// Opens the device menu and taps the "Set/Manage password" menu item diff --git a/integration_test/utils/passkey_test_util.dart b/integration_test/utils/passkey_test_util.dart index 624872b1f..6e4ed83a1 100644 --- a/integration_test/utils/passkey_test_util.dart +++ b/integration_test/utils/passkey_test_util.dart @@ -35,7 +35,7 @@ extension Fido2Functions on WidgetTester { await shortWait(); } - /// Factory reset Fido2 application + /// Factory reset FIDO application Future resetFido2() async { final targetKey = approvedKeys[0]; // only reset approved keys! @@ -43,24 +43,55 @@ extension Fido2Functions on WidgetTester { await switchToKey(targetKey); await shortWait(); - /// 2. open the key menu - await tapPopupMenu(targetKey); + /// 2. open the home view + await tap(find.byKey(homeDrawer).hitTestable()); await shortWait(); - await tap(find.byKey(yubikeyFactoryResetMenuButton).hitTestable()); - await longWait(); - /// 3. then toggle 'Fido2' in the 'Factory reset' reset_dialog.dart + /// 3. open menu + await tap(find.byKey(actionsIconButtonKey).hitTestable()); + await shortWait(); + await tap(find.byKey(yubikeyFactoryResetMenuButton)); + await shortWait(); + + /// 4. then toggle 'Piv' in the 'Factory reset' reset_dialog.dart await tap(find.byKey(factoryResetPickResetFido2)); await longWait(); - /// 4. Click reset TextButton: done + /// 5. Click reset TextButton: done await tap(find.byKey(factoryResetReset)); await fido2DanceWait(); - /// 5. Click the 'Close' button + /// 6. Click the close button await tap(find.text('Close').hitTestable()); await shortWait(); - - /// TODO 6. Verify Resetedness } + + // /// Factory reset Fido2 application + // Future resetFido2() async { + // final targetKey = approvedKeys[0]; // only reset approved keys! + // + // /// 1. make sure we are using approved key + // await switchToKey(targetKey); + // await shortWait(); + // + // /// 2. open the key menu + // await tapPopupMenu(targetKey); + // await shortWait(); + // await tap(find.byKey(yubikeyFactoryResetMenuButton).hitTestable()); + // await longWait(); + // + // /// 3. then toggle 'Fido2' in the 'Factory reset' reset_dialog.dart + // await tap(find.byKey(factoryResetPickResetFido2)); + // await longWait(); + // + // /// 4. Click reset TextButton: done + // await tap(find.byKey(factoryResetReset)); + // await fido2DanceWait(); + // + // /// 5. Click the 'Close' button + // await tap(find.text('Close').hitTestable()); + // await shortWait(); + // + // /// TODO 6. Verify Resetedness + // } } diff --git a/integration_test/utils/piv_test_util.dart b/integration_test/utils/piv_test_util.dart index 7b636da56..254a04a77 100644 --- a/integration_test/utils/piv_test_util.dart +++ b/integration_test/utils/piv_test_util.dart @@ -22,30 +22,112 @@ import 'package:yubico_authenticator/piv/keys.dart'; import 'test_util.dart'; extension PIVFunctions on WidgetTester { + static const ett = 'firstpin'; + static const lock1 = 'lockpinn1'; + static const lock2 = 'lockpinn2'; + static const lock3 = 'lockpinn3'; + /// Open the PIV Configuration Future configurePiv() async { - // 1. open PIV view - var pivDrawerButton = find.byKey(pivAppDrawer).hitTestable(); - await tap(pivDrawerButton); - await longWait(); + await tap(find.byKey(pivAppDrawer).hitTestable()); + await shortWait(); + await tap(find.byKey(actionsIconButtonKey).hitTestable()); + await shortWait(); + } + + Future pinView() async { + await tap(find.byKey(pivAppDrawer).hitTestable()); + await shortWait(); + await tap(find.byKey(actionsIconButtonKey).hitTestable()); + await shortWait(); + await tap(find.byKey(managePinAction)); + await shortWait(); + } + + Future pukView() async { + await tap(find.byKey(pivAppDrawer).hitTestable()); + await shortWait(); + await tap(find.byKey(actionsIconButtonKey).hitTestable()); + await shortWait(); + await tap(find.byKey(managePukAction)); + await shortWait(); + } + + Future managementKeyView() async { + await tap(find.byKey(pivAppDrawer).hitTestable()); + await shortWait(); await tap(find.byKey(actionsIconButtonKey).hitTestable()); + await shortWait(); + await tap(find.byKey(manageManagementKeyAction)); + await shortWait(); + } + + Future pivFirst() async { + // when in pin or puk view, remove factorypin/puk + await enterText(find.byKey(newPinPukField), ett); + await shortWait(); + await enterText(find.byKey(confirmPinPukField), ett); + await shortWait(); + await tap(find.byKey(saveButton).hitTestable()); + await shortWait(); + } + + Future pivLockTest() async { + // when in pin or puk view this will lock it + var pintext1 = 'lockpin1'; + await enterText(find.byKey(pinPukField), pintext1); + await shortWait(); + await enterText(find.byKey(newPinPukField), pintext1); + await shortWait(); + await enterText(find.byKey(confirmPinPukField), pintext1); + await shortWait(); + await tap(find.byKey(saveButton).hitTestable()); + await shortWait(); + + var pintext2 = 'lockpin2'; + await enterText(find.byKey(pinPukField), pintext2); + await shortWait(); + await enterText(find.byKey(newPinPukField), pintext2); + await shortWait(); + await enterText(find.byKey(confirmPinPukField), pintext2); + await shortWait(); + await tap(find.byKey(saveButton).hitTestable()); + await shortWait(); + + var pintext3 = 'lockpin3'; + await enterText(find.byKey(pinPukField), pintext3); + await shortWait(); + await enterText(find.byKey(newPinPukField), pintext3); + await shortWait(); + await enterText(find.byKey(confirmPinPukField), pintext3); + await shortWait(); + await tap(find.byKey(saveButton).hitTestable()); await longWait(); } - /// Locks PIN or PUK - Future lockPinPuk() async { - for (var i = 0; i < 3; i += 1) { - var wrongpin = '123456$i'; - await enterText(find.byKey(pinPukField).hitTestable(), wrongpin); - await shortWait(); - await enterText(find.byKey(newPinPukField).hitTestable(), wrongpin); - await shortWait(); - await enterText(find.byKey(confirmPinPukField).hitTestable(), wrongpin); - await shortWait(); - await tap(find.byKey(saveButton).hitTestable()); - await longWait(); - } + Future pivLock() async { + // when in pin or puk view this will lock it + // for (var i = 0; i < 3; i += 1) { + + await enterText(find.byKey(pinPukField), 'skrivhär'); + await shortWait(); + await enterText(find.byKey(newPinPukField), lock1); + await shortWait(); + await enterText(find.byKey(confirmPinPukField), lock1); + await shortWait(); + await tap(find.byKey(saveButton).hitTestable()); + await shortWait(); + await enterText(find.byKey(pinPukField), lock2); + await shortWait(); + await enterText(find.byKey(newPinPukField), lock2); + await shortWait(); + await enterText(find.byKey(confirmPinPukField), lock2); + await shortWait(); + await tap(find.byKey(saveButton).hitTestable()); + await shortWait(); + // } await sendKeyEvent(LogicalKeyboardKey.escape); + await shortWait(); } /// Factory reset Piv application @@ -56,19 +138,23 @@ extension PIVFunctions on WidgetTester { await switchToKey(targetKey); await shortWait(); - /// 2. open the key menu - await tapPopupMenu(targetKey); + /// 2. open the home view + await tap(find.byKey(homeDrawer).hitTestable()); await shortWait(); - await tap(find.byKey(yubikeyFactoryResetMenuButton).hitTestable()); - await longWait(); - /// 3. then toggle 'Piv' in the 'Factory reset' reset_dialog.dart + /// 3. open menu + await tap(find.byKey(actionsIconButtonKey).hitTestable()); + await shortWait(); + await tap(find.byKey(yubikeyFactoryResetMenuButton)); + await shortWait(); + + /// 4. then toggle 'Piv' in the 'Factory reset' reset_dialog.dart await tap(find.byKey(factoryResetPickResetPiv)); await longWait(); - /// 4. Click reset TextButton: done + /// 5. Click reset TextButton: done await tap(find.byKey(factoryResetReset)); - await shortWait(); + await longWait(); // 5. Verify Resetedness // /// TODO: this expect algorithm is flaky diff --git a/integration_test/utils/test_util.dart b/integration_test/utils/test_util.dart index 5a2783105..8d15e2f6d 100644 --- a/integration_test/utils/test_util.dart +++ b/integration_test/utils/test_util.dart @@ -22,12 +22,11 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:yubico_authenticator/app/views/device_picker.dart'; 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 'desktop/util.dart' as desktop_test_util; -const shortWaitMs = 200; +const shortWaitMs = 240; const longWaitMs = 500; const ultraLongWaitMs = 3000; @@ -235,15 +234,32 @@ extension AppWidgetTester on WidgetTester { } /// Management screen - Future openManagementScreen() async { - if (!isDrawerOpened()) { - await openDrawer(); - } + Future openHomeAndToggleScreen() async { + // if (!isDrawerOpened()) { + // await openDrawer(); + // } + + await tap(find.byKey(homeDrawer).hitTestable()); + await shortWait(); + + await openToggleScreen(); + + //expect(find.byKey(screenKey), findsOneWidget); + } + + /// Toggle Application screen + Future openToggleScreen() async { + // if (!isDrawerOpened()) { + // await openDrawer(); + // } + + await tap(find.byKey(actionsIconButtonKey).hitTestable()); + await shortWait(); - await tap(find.byKey(managementAppDrawer).hitTestable()); - await pump(const Duration(milliseconds: 500)); + await tap(find.byKey(yubikeyApplicationToggleMenuButton).hitTestable()); + await shortWait(); - expect(find.byKey(screenKey), findsOneWidget); + //expect(find.byKey(screenKey), findsOneWidget); } /// Retrieve a list of test approved serial numbers. diff --git a/lib/about_page.dart b/lib/about_page.dart index d39445577..9078371f3 100755 --- a/lib/about_page.dart +++ b/lib/about_page.dart @@ -155,7 +155,6 @@ class AboutPage extends ConsumerWidget { ActionChip( key: diagnosticsChip, avatar: const Icon(Symbols.bug_report), - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, label: Text(l10n.s_run_diagnostics), onPressed: () async { _log.info('Running diagnostics...'); @@ -188,7 +187,6 @@ class AboutPage extends ConsumerWidget { FilterChip( key: screenshotChip, label: Text(l10n.s_allow_screenshots), - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, selected: ref.watch(androidAllowScreenshotsProvider), onSelected: (value) async { ref @@ -236,7 +234,6 @@ class LoggingPanel extends ConsumerWidget { ActionChip( key: logChip, avatar: const Icon(Symbols.content_copy), - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, label: Text(l10n.s_copy_log), onPressed: () async { _log.info('Copying log to clipboard ($version)...'); diff --git a/lib/android/fido/state.dart b/lib/android/fido/state.dart index 3c6182b0e..956c58b78 100644 --- a/lib/android/fido/state.dart +++ b/lib/android/fido/state.dart @@ -161,6 +161,29 @@ class _FidoStateNotifier extends FidoStateNotifier { throw decodedException; } } + + @override + Future enableEnterpriseAttestation() async { + try { + final response = jsonDecode(await _methods.invokeMethod( + 'enableEnterpriseAttestation', + )); + + if (response['success'] == true) { + _log.debug('Enterprise attestation enabled'); + } + } on PlatformException catch (pe) { + var decodedException = pe.decode(); + if (decodedException is CancellationException) { + _log.debug('User cancelled unlock FIDO operation'); + throw decodedException; + } + + _log.debug( + 'Platform exception during enable enterprise attestation: $pe'); + rethrow; + } + } } final androidFingerprintProvider = AsyncNotifierProvider.autoDispose diff --git a/lib/android/oath/state.dart b/lib/android/oath/state.dart index 03b0bdcf2..887d4ff91 100755 --- a/lib/android/oath/state.dart +++ b/lib/android/oath/state.dart @@ -35,6 +35,8 @@ import '../../exception/no_data_exception.dart'; import '../../exception/platform_exception_decoder.dart'; import '../../oath/models.dart'; import '../../oath/state.dart'; +import '../../widgets/toast.dart'; +import '../tap_request_dialog.dart'; final _log = Logger('android.oath.state'); @@ -136,27 +138,45 @@ class _AndroidOathStateNotifier extends OathStateNotifier { } } -// Converts Platform exception during Add Account operation -// Returns CancellationException for situations we don't want to show a Toast -Exception _decodeAddAccountException(PlatformException platformException) { - final decodedException = platformException.decode(); - - // Auth required, the app will show Unlock dialog - if (decodedException is ApduException && decodedException.sw == 0x6982) { - _log.error('Add account failed: Auth required'); - return CancellationException(); - } - - // Thrown in native code when the account already exists on the YubiKey - // The entry dialog will show an error message and that is why we convert - // this to CancellationException to avoid showing a Toast - if (platformException.code == 'IllegalArgumentException') { - _log.error('Add account failed: Account already exists'); - return CancellationException(); +Exception handlePlatformException( + Ref ref, PlatformException platformException) { + final decoded = platformException.decode(); + final l10n = ref.read(l10nProvider); + final withContext = ref.read(withContextProvider); + + toast(String message, {bool popStack = false}) => + withContext((context) async { + ref.read(androidDialogProvider).closeDialog(); + if (popStack) { + Navigator.of(context).popUntil((route) { + return route.isFirst; + }); + } + showToast(context, message, duration: const Duration(seconds: 4)); + }); + + switch (decoded) { + case ApduException apduException: + if (apduException.sw == 0x6985) { + // pop stack to show the OATH view with "Set password" + toast(l10n.l_add_account_password_required, popStack: true); + return CancellationException(); + } + if (apduException.sw == 0x6982) { + toast(l10n.l_add_account_unlock_required); + return CancellationException(); + } + case PlatformException pe: + if (pe.code == 'JobCancellationException') { + // pop stack to show FIDO view + toast(l10n.l_add_account_func_missing, popStack: true); + return CancellationException(); + } else if (pe.code == 'IllegalArgumentException') { + toast(l10n.l_add_account_already_exists); + return CancellationException(); + } } - - // original exception - return decodedException; + return decoded; } final addCredentialToAnyProvider = @@ -171,7 +191,7 @@ final addCredentialToAnyProvider = var result = jsonDecode(resultString); return OathCredential.fromJson(result['credential']); } on PlatformException catch (pe) { - throw _decodeAddAccountException(pe); + throw handlePlatformException(ref, pe); } }); @@ -286,7 +306,7 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier { var result = jsonDecode(resultString); return OathCredential.fromJson(result['credential']); } on PlatformException catch (pe) { - throw _decodeAddAccountException(pe); + throw handlePlatformException(_ref, pe); } } diff --git a/lib/android/state.dart b/lib/android/state.dart index 34437cb96..551cd3add 100644 --- a/lib/android/state.dart +++ b/lib/android/state.dart @@ -157,8 +157,20 @@ class AndroidAttachedDevicesNotifier extends AttachedDevicesNotifier { .maybeWhen(data: (data) => [data.node], orElse: () => []); } -final androidDeviceDataProvider = Provider>( - (ref) => ref.watch(androidYubikeyProvider)); +final androidDeviceDataProvider = Provider>((ref) { + return ref.watch(androidYubikeyProvider).when(data: (d) { + if (d.name == 'restricted-nfc' || + d.name == 'unknown-device' || + d.name == 'no-scp11b-nfc-support') { + return AsyncError(d.name, StackTrace.current); + } + return AsyncData(d); + }, error: (Object error, StackTrace stackTrace) { + return AsyncError(error, stackTrace); + }, loading: () { + return const AsyncLoading(); + }); +}); class AndroidCurrentDeviceNotifier extends CurrentDeviceNotifier { @override diff --git a/lib/android/tap_request_dialog.dart b/lib/android/tap_request_dialog.dart index 589052137..8073525c8 100755 --- a/lib/android/tap_request_dialog.dart +++ b/lib/android/tap_request_dialog.dart @@ -80,6 +80,8 @@ enum _DDesc { fidoDeleteCredential, fidoDeleteFingerprint, fidoRenameFingerprint, + fidoRegisterFingerprint, + fidoEnableEnterpriseAttestation, fidoActionFailure, // Others invalid; @@ -105,7 +107,9 @@ enum _DDesc { dialogDescriptionFidoIndex + 3: fidoDeleteCredential, dialogDescriptionFidoIndex + 4: fidoDeleteFingerprint, dialogDescriptionFidoIndex + 5: fidoRenameFingerprint, - dialogDescriptionFidoIndex + 6: fidoActionFailure, + dialogDescriptionFidoIndex + 6: fidoRegisterFingerprint, + dialogDescriptionFidoIndex + 7: fidoEnableEnterpriseAttestation, + dialogDescriptionFidoIndex + 8: fidoActionFailure, }[id] ?? _DDesc.invalid; } @@ -125,7 +129,7 @@ class _DialogProvider { final args = jsonDecode(call.arguments); switch (call.method) { case 'close': - _closeDialog(); + closeDialog(); break; case 'show': await _showDialog(args['title'], args['description'], args['icon']); @@ -143,7 +147,7 @@ class _DialogProvider { }); } - void _closeDialog() { + void closeDialog() { _controller?.close(); _controller = null; } diff --git a/lib/app/message.dart b/lib/app/message.dart index df7a7fd5c..431e5d6ad 100755 --- a/lib/app/message.dart +++ b/lib/app/message.dart @@ -32,7 +32,7 @@ Future showBlurDialog({ required BuildContext context, required Widget Function(BuildContext) builder, RouteSettings? routeSettings, - Color barrierColor = const Color(0x00cccccc), + Color barrierColor = const Color(0x33000000), }) async => await showGeneralDialog( context: context, diff --git a/lib/app/models.freezed.dart b/lib/app/models.freezed.dart index 7b15fb312..37629fcb5 100644 --- a/lib/app/models.freezed.dart +++ b/lib/app/models.freezed.dart @@ -20,7 +20,9 @@ mixin _$YubiKeyData { String get name => throw _privateConstructorUsedError; DeviceInfo get info => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of YubiKeyData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $YubiKeyDataCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -47,6 +49,8 @@ class _$YubiKeyDataCopyWithImpl<$Res, $Val extends YubiKeyData> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of YubiKeyData + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -70,6 +74,8 @@ class _$YubiKeyDataCopyWithImpl<$Res, $Val extends YubiKeyData> ) as $Val); } + /// Create a copy of YubiKeyData + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $DeviceNodeCopyWith<$Res> get node { @@ -78,6 +84,8 @@ class _$YubiKeyDataCopyWithImpl<$Res, $Val extends YubiKeyData> }); } + /// Create a copy of YubiKeyData + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $DeviceInfoCopyWith<$Res> get info { @@ -111,6 +119,8 @@ class __$$YubiKeyDataImplCopyWithImpl<$Res> _$YubiKeyDataImpl _value, $Res Function(_$YubiKeyDataImpl) _then) : super(_value, _then); + /// Create a copy of YubiKeyData + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -165,7 +175,9 @@ class _$YubiKeyDataImpl implements _YubiKeyData { @override int get hashCode => Object.hash(runtimeType, node, name, info); - @JsonKey(ignore: true) + /// Create a copy of YubiKeyData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$YubiKeyDataImplCopyWith<_$YubiKeyDataImpl> get copyWith => @@ -183,8 +195,11 @@ abstract class _YubiKeyData implements YubiKeyData { String get name; @override DeviceInfo get info; + + /// Create a copy of YubiKeyData + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$YubiKeyDataImplCopyWith<_$YubiKeyDataImpl> get copyWith => throw _privateConstructorUsedError; } @@ -238,7 +253,9 @@ mixin _$DeviceNode { }) => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of DeviceNode + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $DeviceNodeCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -262,6 +279,8 @@ class _$DeviceNodeCopyWithImpl<$Res, $Val extends DeviceNode> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of DeviceNode + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -302,6 +321,8 @@ class __$$UsbYubiKeyNodeImplCopyWithImpl<$Res> _$UsbYubiKeyNodeImpl _value, $Res Function(_$UsbYubiKeyNodeImpl) _then) : super(_value, _then); + /// Create a copy of DeviceNode + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -330,6 +351,8 @@ class __$$UsbYubiKeyNodeImplCopyWithImpl<$Res> )); } + /// Create a copy of DeviceNode + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $DeviceInfoCopyWith<$Res>? get info { @@ -376,7 +399,9 @@ class _$UsbYubiKeyNodeImpl extends UsbYubiKeyNode { @override int get hashCode => Object.hash(runtimeType, path, name, pid, info); - @JsonKey(ignore: true) + /// Create a copy of DeviceNode + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$UsbYubiKeyNodeImplCopyWith<_$UsbYubiKeyNodeImpl> get copyWith => @@ -463,8 +488,11 @@ abstract class UsbYubiKeyNode extends DeviceNode { String get name; UsbPid get pid; DeviceInfo? get info; + + /// Create a copy of DeviceNode + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$UsbYubiKeyNodeImplCopyWith<_$UsbYubiKeyNodeImpl> get copyWith => throw _privateConstructorUsedError; } @@ -488,6 +516,8 @@ class __$$NfcReaderNodeImplCopyWithImpl<$Res> _$NfcReaderNodeImpl _value, $Res Function(_$NfcReaderNodeImpl) _then) : super(_value, _then); + /// Create a copy of DeviceNode + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -534,7 +564,9 @@ class _$NfcReaderNodeImpl extends NfcReaderNode { @override int get hashCode => Object.hash(runtimeType, path, name); - @JsonKey(ignore: true) + /// Create a copy of DeviceNode + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$NfcReaderNodeImplCopyWith<_$NfcReaderNodeImpl> get copyWith => @@ -618,8 +650,11 @@ abstract class NfcReaderNode extends DeviceNode { DevicePath get path; @override String get name; + + /// Create a copy of DeviceNode + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$NfcReaderNodeImplCopyWith<_$NfcReaderNodeImpl> get copyWith => throw _privateConstructorUsedError; } @@ -636,7 +671,9 @@ mixin _$ActionItem { Key? get key => throw _privateConstructorUsedError; Feature? get feature => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of ActionItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $ActionItemCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -669,6 +706,8 @@ class _$ActionItemCopyWithImpl<$Res, $Val extends ActionItem> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of ActionItem + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -751,6 +790,8 @@ class __$$ActionItemImplCopyWithImpl<$Res> _$ActionItemImpl _value, $Res Function(_$ActionItemImpl) _then) : super(_value, _then); + /// Create a copy of ActionItem + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -867,7 +908,9 @@ class _$ActionItemImpl implements _ActionItem { int get hashCode => Object.hash(runtimeType, icon, title, subtitle, shortcut, trailing, intent, actionStyle, key, feature); - @JsonKey(ignore: true) + /// Create a copy of ActionItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ActionItemImplCopyWith<_$ActionItemImpl> get copyWith => @@ -904,8 +947,11 @@ abstract class _ActionItem implements ActionItem { Key? get key; @override Feature? get feature; + + /// Create a copy of ActionItem + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$ActionItemImplCopyWith<_$ActionItemImpl> get copyWith => throw _privateConstructorUsedError; } @@ -917,7 +963,9 @@ mixin _$WindowState { bool get active => throw _privateConstructorUsedError; bool get hidden => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of WindowState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $WindowStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -941,6 +989,8 @@ class _$WindowStateCopyWithImpl<$Res, $Val extends WindowState> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of WindowState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -989,6 +1039,8 @@ class __$$WindowStateImplCopyWithImpl<$Res> _$WindowStateImpl _value, $Res Function(_$WindowStateImpl) _then) : super(_value, _then); + /// Create a copy of WindowState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1057,7 +1109,9 @@ class _$WindowStateImpl implements _WindowState { int get hashCode => Object.hash(runtimeType, focused, visible, active, hidden); - @JsonKey(ignore: true) + /// Create a copy of WindowState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$WindowStateImplCopyWith<_$WindowStateImpl> get copyWith => @@ -1079,8 +1133,11 @@ abstract class _WindowState implements WindowState { bool get active; @override bool get hidden; + + /// Create a copy of WindowState + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$WindowStateImplCopyWith<_$WindowStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -1098,8 +1155,12 @@ mixin _$KeyCustomization { @_ColorConverter() Color? get color => throw _privateConstructorUsedError; + /// Serializes this KeyCustomization to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of KeyCustomization + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $KeyCustomizationCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -1126,6 +1187,8 @@ class _$KeyCustomizationCopyWithImpl<$Res, $Val extends KeyCustomization> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of KeyCustomization + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1172,6 +1235,8 @@ class __$$KeyCustomizationImplCopyWithImpl<$Res> $Res Function(_$KeyCustomizationImpl) _then) : super(_value, _then); + /// Create a copy of KeyCustomization + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1232,11 +1297,13 @@ class _$KeyCustomizationImpl implements _KeyCustomization { (identical(other.color, color) || other.color == color)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, serial, name, color); - @JsonKey(ignore: true) + /// Create a copy of KeyCustomization + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$KeyCustomizationImplCopyWith<_$KeyCustomizationImpl> get copyWith => @@ -1271,8 +1338,11 @@ abstract class _KeyCustomization implements KeyCustomization { @JsonKey(includeIfNull: false) @_ColorConverter() Color? get color; + + /// Create a copy of KeyCustomization + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$KeyCustomizationImplCopyWith<_$KeyCustomizationImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/app/models.g.dart b/lib/app/models.g.dart index 42a87b196..a55bb0b3c 100644 --- a/lib/app/models.g.dart +++ b/lib/app/models.g.dart @@ -9,9 +9,9 @@ part of 'models.dart'; _$KeyCustomizationImpl _$$KeyCustomizationImplFromJson( Map json) => _$KeyCustomizationImpl( - serial: json['serial'] as int, + serial: (json['serial'] as num).toInt(), name: json['name'] as String?, - color: const _ColorConverter().fromJson(json['color'] as int?), + color: const _ColorConverter().fromJson((json['color'] as num?)?.toInt()), ); Map _$$KeyCustomizationImplToJson( diff --git a/lib/app/views/app_list_item.dart b/lib/app/views/app_list_item.dart index 6f04f7528..76635958c 100644 --- a/lib/app/views/app_list_item.dart +++ b/lib/app/views/app_list_item.dart @@ -30,8 +30,10 @@ class AppListItem extends ConsumerStatefulWidget { final String? semanticTitle; final Widget? trailing; final List Function(BuildContext context)? buildPopupActions; + final Widget Function(BuildContext context)? itemBuilder; final Intent? tapIntent; final Intent? doubleTapIntent; + final Color? tileColor; final bool selected; const AppListItem( @@ -43,8 +45,10 @@ class AppListItem extends ConsumerStatefulWidget { this.subtitle, this.trailing, this.buildPopupActions, + this.itemBuilder, this.tapIntent, this.doubleTapIntent, + this.tileColor, this.selected = false, }); @@ -78,7 +82,7 @@ class _AppListItemState extends ConsumerState { item: widget.item, child: InkWell( focusNode: _focusNode, - borderRadius: BorderRadius.circular(48), + borderRadius: BorderRadius.circular(16), onSecondaryTapDown: buildPopupActions == null ? null : (details) { @@ -118,57 +122,62 @@ class _AppListItemState extends ConsumerState { : () { Actions.invoke(context, doubleTapIntent); }, - child: Stack( - alignment: AlignmentDirectional.center, - children: [ - const SizedBox(height: 64), - ListTile( - mouseCursor: - widget.tapIntent != null ? SystemMouseCursors.click : null, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(48)), - selectedTileColor: colorScheme.secondaryContainer, - selectedColor: colorScheme.onSecondaryContainer, - selected: widget.selected, - leading: widget.leading, - title: subtitle == null - // We use SizedBox to fill entire space - ? SizedBox( - height: 48, - child: Align( - alignment: Alignment.centerLeft, - child: Text( - widget.title, - overflow: TextOverflow.fade, - maxLines: 1, - softWrap: false, - ), - ), - ) - : 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, - ), - ), - ], - ), + child: widget.itemBuilder != null + ? widget.itemBuilder!.call(context) + : Stack( + alignment: AlignmentDirectional.center, + children: [ + const SizedBox(height: 64), + ListTile( + mouseCursor: widget.tapIntent != null + ? SystemMouseCursors.click + : null, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + selectedTileColor: colorScheme.secondaryContainer, + selectedColor: colorScheme.onSecondaryContainer, + tileColor: widget.tileColor, + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + selected: widget.selected, + leading: widget.leading, + title: subtitle == null + // We use SizedBox to fill entire space + ? SizedBox( + height: 48, + child: Align( + alignment: Alignment.centerLeft, + child: Text( + widget.title, + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: false, + ), + ), + ) + : 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 fed94f73d..741eb9fa1 100755 --- a/lib/app/views/app_page.dart +++ b/lib/app/views/app_page.dart @@ -22,6 +22,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../../core/state.dart'; import '../../management/models.dart'; @@ -29,18 +30,29 @@ import '../../widgets/delayed_visibility.dart'; import '../../widgets/file_drop_target.dart'; import '../message.dart'; import '../shortcuts.dart'; +import '../state.dart'; import 'fs_dialog.dart'; import 'keys.dart'; import 'navigation.dart'; -final _navigationProvider = StateNotifierProvider<_NavigationProvider, bool>( - (ref) => _NavigationProvider()); +final _navigationVisibilityProvider = + StateNotifierProvider<_VisibilityNotifier, bool>((ref) => + _VisibilityNotifier('NAVIGATION_VISIBILITY', ref.watch(prefProvider))); -class _NavigationProvider extends StateNotifier { - _NavigationProvider() : super(true); +final _detailViewVisibilityProvider = + StateNotifierProvider<_VisibilityNotifier, bool>((ref) => + _VisibilityNotifier('DETAIL_VIEW_VISIBILITY', ref.watch(prefProvider))); + +class _VisibilityNotifier extends StateNotifier { + final String _key; + final SharedPreferences _prefs; + _VisibilityNotifier(this._key, this._prefs) + : super(_prefs.getBool(_key) ?? true); void toggleExpanded() { - state = !state; + final newValue = !state; + state = newValue; + _prefs.setBool(_key, newValue); } } @@ -50,7 +62,6 @@ final _navKey = GlobalKey(); final _navExpandedKey = GlobalKey(); final _sliverTitleGlobalKey = GlobalKey(); final _sliverTitleWrapperGlobalKey = GlobalKey(); -final _headerSliverGlobalKey = GlobalKey(); final _detailsViewGlobalKey = GlobalKey(); final _mainContentGlobalKey = GlobalKey(); @@ -308,14 +319,17 @@ class _AppPageState extends ConsumerState { Widget? _buildAppBarTitle( BuildContext context, bool hasRail, bool hasManage, bool fullyExpanded) { - final showNavigation = ref.watch(_navigationProvider); + final showNavigation = ref.watch(_navigationVisibilityProvider); + final showDetailView = ref.watch(_detailViewVisibilityProvider); + EdgeInsets padding; if (fullyExpanded) { - padding = EdgeInsets.only(left: showNavigation ? 280 : 72, right: 320); + padding = EdgeInsets.only( + left: showNavigation ? 280 : 72, right: showDetailView ? 320 : 0.0); } else if (!hasRail && hasManage) { padding = const EdgeInsets.only(right: 320); } else if (hasRail && hasManage) { - padding = const EdgeInsets.only(left: 72, right: 320); + padding = EdgeInsets.only(left: 72, right: showDetailView ? 320 : 0.0); } else if (hasRail && !hasManage) { padding = const EdgeInsets.only(left: 72); } else { @@ -344,21 +358,23 @@ class _AppPageState extends ConsumerState { } Widget _buildMainContent(BuildContext context, bool expanded) { - final actions = widget.actionsBuilder?.call(context, expanded) ?? []; + final showDetailView = ref.watch(_detailViewVisibilityProvider); + final actions = + widget.actionsBuilder?.call(context, expanded && showDetailView) ?? []; final content = Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: widget.centered ? CrossAxisAlignment.center : CrossAxisAlignment.start, children: [ - widget.builder(context, expanded), + widget.builder(context, expanded && showDetailView), if (actions.isNotEmpty) Align( alignment: widget.centered ? Alignment.center : Alignment.centerLeft, child: Padding( padding: const EdgeInsets.only( - top: 16, bottom: 0, left: 16, right: 16), + top: 16, bottom: 0, left: 18, right: 18), child: Wrap( spacing: 8, runSpacing: 4, @@ -369,7 +385,7 @@ class _AppPageState extends ConsumerState { if (widget.footnote != null) Padding( padding: - const EdgeInsets.only(bottom: 16, top: 33, left: 16, right: 16), + const EdgeInsets.only(bottom: 16, top: 33, left: 18, right: 18), child: Opacity( opacity: 0.6, child: Text( @@ -399,7 +415,7 @@ class _AppPageState extends ConsumerState { alignment: Alignment.topLeft, child: Padding( padding: const EdgeInsets.only( - left: 16.0, right: 16.0, bottom: 24.0, top: 4.0), + left: 18.0, right: 18.0, bottom: 24.0, top: 4.0), child: _buildTitle(context), ), ), @@ -429,16 +445,19 @@ class _AppPageState extends ConsumerState { targetKey: _sliverTitleGlobalKey, controller: _sliverTitleController, subTargetKey: - widget.headerSliver != null ? _headerSliverGlobalKey : null, + widget.headerSliver != null ? headerSliverGlobalKey : null, subController: widget.headerSliver != null ? _headerSliverController : null, subAnchorKey: widget.headerSliver != null ? _sliverTitleWrapperGlobalKey : null, child: CustomScrollView( physics: isAndroid - ? const ClampingScrollPhysics( - parent: AlwaysScrollableScrollPhysics()) - : null, + ? const _NoImplicitScrollPhysics( + parent: ClampingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + ) + : const _NoImplicitScrollPhysics(), controller: _sliverTitleScrollController, key: _mainContentGlobalKey, slivers: [ @@ -448,11 +467,11 @@ class _AppPageState extends ConsumerState { pinned: true, delegate: _SliverTitleDelegate( child: ColoredBox( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, child: Padding( key: _sliverTitleWrapperGlobalKey, padding: const EdgeInsets.only( - left: 16.0, right: 16.0, bottom: 12.0, top: 4.0), + left: 18.0, right: 18.0, bottom: 12.0, top: 4.0), child: _buildTitle(context), ), ), @@ -470,11 +489,11 @@ class _AppPageState extends ConsumerState { _sliverTitleScrollController, _headerSliverController.scrollDirection, _headerSliverController, - _headerSliverGlobalKey, + headerSliverGlobalKey, _sliverTitleWrapperGlobalKey); return Container( - key: _headerSliverGlobalKey, + key: headerSliverGlobalKey, child: widget.headerSliver); }, )) @@ -499,16 +518,17 @@ class _AppPageState extends ConsumerState { BuildContext context, bool hasDrawer, bool hasRail, bool hasManage) { final l10n = AppLocalizations.of(context)!; final fullyExpanded = !hasDrawer && hasRail && hasManage; - final showNavigation = ref.watch(_navigationProvider); + final showNavigation = ref.watch(_navigationVisibilityProvider); + final showDetailView = ref.watch(_detailViewVisibilityProvider); final hasDetailsOrKeyActions = widget.detailViewBuilder != null || widget.keyActionsBuilder != null; var body = _buildMainContent(context, hasManage); - var navigationText = showNavigation - ? (fullyExpanded + var navigationText = fullyExpanded + ? (showNavigation ? l10n.s_collapse_navigation - : MaterialLocalizations.of(context).openAppDrawerTooltip) - : l10n.s_expand_navigation; + : l10n.s_expand_navigation) + : l10n.s_show_navigation; if (widget.onFileDropped != null) { body = FileDropTarget( @@ -518,187 +538,218 @@ class _AppPageState extends ConsumerState { ); } if (hasRail || hasManage) { - body = Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (hasRail && (!fullyExpanded || !showNavigation)) - SizedBox( - width: 72, - child: _VisibilityListener( - targetKey: _navKey, - controller: _navController, - child: SingleChildScrollView( - child: NavigationContent( - key: _navKey, - shouldPop: false, - extended: false, - ), - ), - ), - ), - if (fullyExpanded && showNavigation) - SizedBox( - width: 280, + body = GestureDetector( + behavior: HitTestBehavior.deferToChild, + onTap: () { + Actions.invoke(context, const EscapeIntent()); + FocusManager.instance.primaryFocus?.unfocus(); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (hasRail && (!fullyExpanded || !showNavigation)) + SizedBox( + width: 72, child: _VisibilityListener( + targetKey: _navKey, controller: _navController, - targetKey: _navExpandedKey, child: SingleChildScrollView( - child: Material( - type: MaterialType.transparency, - child: NavigationContent( - key: _navExpandedKey, - shouldPop: false, - extended: true, - ), + child: NavigationContent( + key: _navKey, + shouldPop: false, + extended: false, ), ), - )), - const SizedBox(width: 8), - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.deferToChild, - onTap: () { - Actions.invoke(context, const EscapeIntent()); - }, - child: Stack(children: [ - Container( - color: Colors.transparent, + ), ), - body - ]), - )), - if (hasManage && - !hasDetailsOrKeyActions && - widget.capabilities != null && - widget.capabilities?.first != Capability.u2f) - // Add a placeholder for the Manage/Details column. Exceptions are: - // - the "Security Key" because it does not have any actions/details. - // - pages without Capabilities - const SizedBox(width: 336), // simulate column - if (hasManage && hasDetailsOrKeyActions) - _VisibilityListener( - controller: _detailsController, - targetKey: _detailsViewGlobalKey, - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: SizedBox( - width: 320, - child: Column( - key: _detailsViewGlobalKey, - children: [ - if (widget.detailViewBuilder != null) - widget.detailViewBuilder!(context), - if (widget.keyActionsBuilder != null) - widget.keyActionsBuilder!(context), - ], + if (fullyExpanded && showNavigation) + SizedBox( + width: 280, + child: _VisibilityListener( + controller: _navController, + targetKey: _navExpandedKey, + child: SingleChildScrollView( + child: Material( + type: MaterialType.transparency, + child: NavigationContent( + key: _navExpandedKey, + shouldPop: false, + extended: true, + ), + ), + ), + )), + const SizedBox(width: 8), + Expanded(child: body), + if (hasManage && + !hasDetailsOrKeyActions && + showDetailView && + widget.capabilities != null && + widget.capabilities?.first != Capability.u2f) + // Add a placeholder for the Manage/Details column. Exceptions are: + // - the "Security Key" because it does not have any actions/details. + // - pages without Capabilities + const SizedBox(width: 336), // simulate column + if (hasManage && hasDetailsOrKeyActions && showDetailView) + _VisibilityListener( + controller: _detailsController, + targetKey: _detailsViewGlobalKey, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: SizedBox( + width: 320, + child: Column( + key: _detailsViewGlobalKey, + children: [ + if (widget.detailViewBuilder != null) + widget.detailViewBuilder!(context), + if (widget.keyActionsBuilder != null) + widget.keyActionsBuilder!(context), + ], + ), ), ), ), ), - ), - ], + ], + ), ); } return Scaffold( key: scaffoldGlobalKey, - appBar: AppBar( - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: ListenableBuilder( - listenable: _scrolledUnderController, - builder: (context, child) { - final visible = _scrolledUnderController.someIsScrolledUnder; - return AnimatedOpacity( - opacity: visible ? 1 : 0, - duration: const Duration(milliseconds: 300), - child: Container( - color: Theme.of(context).colorScheme.secondaryContainer, - height: 1.0, - ), - ); - }, + appBar: _GestureDetectorAppBar( + onTap: () { + Actions.invoke(context, const EscapeIntent()); + FocusManager.instance.primaryFocus?.unfocus(); + }, + appBar: AppBar( + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: ListenableBuilder( + listenable: _scrolledUnderController, + builder: (context, child) { + final visible = _scrolledUnderController.someIsScrolledUnder; + return AnimatedOpacity( + opacity: visible ? 1 : 0, + duration: const Duration(milliseconds: 300), + child: Container( + color: Theme.of(context).hoverColor, + height: 1.0, + ), + ); + }, + ), ), - ), - scrolledUnderElevation: 0.0, - leadingWidth: hasRail ? 84 : null, - backgroundColor: Theme.of(context).colorScheme.background, - title: _buildAppBarTitle( - context, - hasRail, - hasManage, - fullyExpanded, - ), - leading: hasRail - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: IconButton( - icon: Icon(Symbols.menu, semanticLabel: navigationText), - tooltip: navigationText, - onPressed: fullyExpanded - ? () { - ref - .read(_navigationProvider.notifier) - .toggleExpanded(); - } - : () { - scaffoldGlobalKey.currentState?.openDrawer(); - }, - ), - )), - const SizedBox(width: 12), - ], - ) - : Builder( - builder: (context) { - // Need to wrap with builder to get Scaffold context - return IconButton( - onPressed: () => Scaffold.of(context).openDrawer(), - icon: const Icon(Symbols.menu), - ); - }, - ), - actions: [ - if (widget.actionButtonBuilder == null && - (widget.keyActionsBuilder != null && !hasManage)) - Padding( - padding: const EdgeInsets.only(left: 4), - child: IconButton( - key: actionsIconButtonKey, - onPressed: () { - showBlurDialog( - context: context, - barrierColor: Colors.transparent, - builder: (context) => FsDialog( - child: Padding( - padding: const EdgeInsets.only(top: 32), - child: widget.keyActionsBuilder!(context), + iconTheme: IconThemeData( + color: Theme.of(context).colorScheme.onSurfaceVariant), + scrolledUnderElevation: 0.0, + leadingWidth: hasRail ? 84 : null, + backgroundColor: Theme.of(context).colorScheme.surface, + title: _buildAppBarTitle( + context, + hasRail, + hasManage, + fullyExpanded, + ), + centerTitle: true, + leading: hasRail + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: IconButton( + icon: Icon(Symbols.menu, semanticLabel: navigationText), + tooltip: navigationText, + onPressed: fullyExpanded + ? () { + ref + .read( + _navigationVisibilityProvider.notifier) + .toggleExpanded(); + } + : () { + scaffoldGlobalKey.currentState?.openDrawer(); + }, ), - ), - ); - }, - icon: widget.keyActionsBadge - ? Badge( - child: Icon(Symbols.more_vert, - semanticLabel: l10n.s_configure_yk), - ) - : Icon(Symbols.more_vert, - semanticLabel: l10n.s_configure_yk), - iconSize: 24, - tooltip: l10n.s_configure_yk, - padding: const EdgeInsets.all(12), + )), + const SizedBox(width: 12), + ], + ) + : Builder( + builder: (context) { + // Need to wrap with builder to get Scaffold context + return IconButton( + tooltip: l10n.s_show_navigation, + onPressed: () => Scaffold.of(context).openDrawer(), + icon: Icon( + Symbols.menu, + semanticLabel: l10n.s_show_navigation, + ), + ); + }, + ), + actions: [ + if (widget.actionButtonBuilder == null && + (widget.keyActionsBuilder != null && !hasManage)) + Padding( + padding: const EdgeInsets.only(left: 4), + child: IconButton( + key: actionsIconButtonKey, + onPressed: () { + showBlurDialog( + context: context, + barrierColor: Colors.transparent, + builder: (context) => FsDialog( + child: Padding( + padding: const EdgeInsets.only(top: 32), + child: widget.keyActionsBuilder!(context), + ), + ), + ); + }, + icon: widget.keyActionsBadge + ? Badge( + child: Icon(Symbols.more_vert, + semanticLabel: l10n.s_show_menu), + ) + : Icon(Symbols.more_vert, + semanticLabel: l10n.s_show_menu), + iconSize: 24, + tooltip: l10n.s_show_menu, + padding: const EdgeInsets.all(12), + ), ), - ), - if (widget.actionButtonBuilder != null) - Padding( - padding: const EdgeInsets.only(right: 12), - child: widget.actionButtonBuilder!.call(context), - ), - ], + if (hasManage && + (widget.keyActionsBuilder != null || + widget.detailViewBuilder != null)) + Padding( + padding: const EdgeInsets.only(left: 4), + child: IconButton( + key: toggleDetailViewIconButtonKey, + onPressed: () { + ref + .read(_detailViewVisibilityProvider.notifier) + .toggleExpanded(); + }, + icon: const Icon( + Symbols.more_vert, + weight: 600.0, + ), + iconSize: 24, + tooltip: showDetailView ? l10n.s_hide_menu : l10n.s_show_menu, + padding: const EdgeInsets.all(12), + ), + ), + if (widget.actionButtonBuilder != null) + Padding( + padding: const EdgeInsets.only(right: 12), + child: widget.actionButtonBuilder!.call(context), + ), + ], + ), ), drawer: hasDrawer ? _buildDrawer(context) : null, body: body, @@ -706,23 +757,66 @@ class _AppPageState extends ConsumerState { } } -class CapabilityBadge extends StatelessWidget { - final Capability capability; +class _GestureDetectorAppBar extends StatelessWidget + implements PreferredSizeWidget { + final AppBar appBar; + final void Function() onTap; - const CapabilityBadge(this.capability, {super.key}); + const _GestureDetectorAppBar({required this.appBar, required this.onTap}); @override Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.deferToChild, onTap: onTap, child: appBar); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +class CapabilityBadge extends ConsumerWidget { + final Capability capability; + final bool noTooltip; + + const CapabilityBadge(this.capability, {super.key, this.noTooltip = false}); + + @override + Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context)!; final colorScheme = Theme.of(context).colorScheme; + final text = Text(capability.getDisplayName(l10n)); + final (fipsCapable, fipsApproved) = ref + .watch(currentDeviceDataProvider) + .valueOrNull + ?.info + .getFipsStatus(capability) ?? + (false, false); + final label = fipsCapable + ? Row( + children: [ + Icon( + Symbols.shield, + color: colorScheme.onSecondaryContainer, + size: 12, + fill: fipsApproved ? 1 : 0, + ), + const SizedBox(width: 4), + text, + ], + ) + : text; return Badge( backgroundColor: colorScheme.secondaryContainer, textColor: colorScheme.onSecondaryContainer, padding: const EdgeInsets.symmetric(horizontal: 6), largeSize: MediaQuery.of(context).textScaler.scale(20), - label: Text( - capability.getDisplayName(l10n), - ), + label: fipsCapable && !noTooltip + ? Tooltip( + message: + fipsApproved ? l10n.l_fips_approved : l10n.l_fips_capable, + child: label, + ) + : label, ); } } @@ -949,3 +1043,15 @@ class _SliverTitleDelegate extends SliverPersistentHeaderDelegate { @override bool shouldRebuild(_SliverTitleDelegate oldDelegate) => true; } + +class _NoImplicitScrollPhysics extends ScrollPhysics { + const _NoImplicitScrollPhysics({super.parent}); + + @override + bool get allowImplicitScrolling => false; + + @override + _NoImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) { + return _NoImplicitScrollPhysics(parent: buildParent(ancestor)); + } +} diff --git a/lib/app/views/device_error_screen.dart b/lib/app/views/device_error_screen.dart index fe97c8bca..7947c62e9 100755 --- a/lib/app/views/device_error_screen.dart +++ b/lib/app/views/device_error_screen.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * Copyright (C) 2022-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,6 +78,27 @@ class DeviceErrorScreen extends ConsumerWidget { ), header: l10n.s_unknown_device, ), + 'restricted-nfc' => HomeMessagePage( + centered: true, + graphic: Icon( + Symbols.contactless, + size: 96, + color: Theme.of(context).colorScheme.tertiary, + ), + header: l10n.l_deactivate_restricted_nfc, + message: l10n.p_deactivate_restricted_nfc_desc, + footnote: l10n.p_deactivate_restricted_nfc_footer, + ), + 'no-scp11b-nfc-support' => HomeMessagePage( + centered: true, + graphic: Icon( + Symbols.contactless, + size: 96, + color: Theme.of(context).colorScheme.tertiary, + ), + header: l10n.l_configuration_unsupported, + message: l10n.p_scp_unsupported, + ), _ => HomeMessagePage( centered: true, graphic: Image.asset( diff --git a/lib/app/views/device_picker.dart b/lib/app/views/device_picker.dart index b8b028ecd..126c90da3 100644 --- a/lib/app/views/device_picker.dart +++ b/lib/app/views/device_picker.dart @@ -155,6 +155,7 @@ List _getDeviceStrings( error: (error, _) => switch (error) { 'device-inaccessible' => [node.name, l10n.s_yk_inaccessible], 'unknown-device' => [l10n.s_unknown_device], + 'restricted-nfc' => [l10n.s_restricted_nfc], _ => null, }, ) ?? diff --git a/lib/app/views/fs_dialog.dart b/lib/app/views/fs_dialog.dart index 7d3f6d898..e601a5b8d 100644 --- a/lib/app/views/fs_dialog.dart +++ b/lib/app/views/fs_dialog.dart @@ -28,8 +28,7 @@ class FsDialog extends StatelessWidget { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Dialog.fullscreen( - backgroundColor: - Theme.of(context).colorScheme.background.withOpacity(0.7), + backgroundColor: Theme.of(context).colorScheme.surface.withOpacity(0.7), child: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/app/views/keys.dart b/lib/app/views/keys.dart index ebce196f6..1ef73f195 100644 --- a/lib/app/views/keys.dart +++ b/lib/app/views/keys.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; // global keys final scaffoldGlobalKey = GlobalKey(); +final headerSliverGlobalKey = GlobalKey(); // This is global so we can access it from the global Ctrl+F shortcut. final searchField = GlobalKey(); @@ -25,6 +26,8 @@ const _prefix = 'app.keys'; const deviceInfoListTile = Key('$_prefix.device_info_list_tile'); const noDeviceAvatar = Key('$_prefix.no_device_avatar'); const actionsIconButtonKey = Key('$_prefix.actions_icon_button'); +const toggleDetailViewIconButtonKey = + Key('$_prefix.toggle_detail_view_icon_button'); // drawer items const homeDrawer = Key('$_prefix.drawer.home'); diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index 48d1a204b..63afad570 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2023 Yubico. + * Copyright (C) 2022-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,6 +69,7 @@ class MainPage extends ConsumerWidget { 'oath_add_account', 'oath_icon_pack_dialog', 'android_qr_scanner_view', + 'android_alert_dialog' ].contains(route.settings.name); }); }); @@ -119,19 +120,7 @@ class MainPage extends ConsumerWidget { data: (data) { final section = ref.watch(currentSectionProvider); final capabilities = section.capabilities; - if (data.info.supportedCapabilities.isEmpty && - data.name == 'Unrecognized device') { - return HomeMessagePage( - centered: true, - graphic: Icon( - Symbols.help, - size: 96, - color: Theme.of(context).colorScheme.error, - ), - header: l10n.s_yk_not_recognized, - ); - } else if (section.getAvailability(data) == - Availability.unsupported) { + if (section.getAvailability(data) == Availability.unsupported) { return MessagePage( title: section.getDisplayName(l10n), capabilities: capabilities, diff --git a/lib/app/views/message_page.dart b/lib/app/views/message_page.dart index 46a21110f..56174b567 100755 --- a/lib/app/views/message_page.dart +++ b/lib/app/views/message_page.dart @@ -71,12 +71,12 @@ class MessagePage extends StatelessWidget { delayedContent: delayedContent, builder: (context, _) => Padding( padding: EdgeInsets.only( - left: 16.0, + left: 18.0, top: 0.0, - right: 16.0, + right: 18.0, bottom: centered && actionsBuilder == null ? 96 : 0), child: SizedBox( - width: centered ? 250 : 350, + width: 350, child: Column( crossAxisAlignment: centered ? CrossAxisAlignment.center @@ -93,7 +93,7 @@ class MessagePage extends StatelessWidget { if (message != null) ...[ const SizedBox(height: 12.0), Container( - constraints: const BoxConstraints(maxWidth: 300), + constraints: const BoxConstraints(maxWidth: 350), child: Text(message!, textAlign: centered ? TextAlign.center : TextAlign.left, style: Theme.of(context).textTheme.titleSmall?.apply( diff --git a/lib/app/views/reset_dialog.dart b/lib/app/views/reset_dialog.dart index bfe268268..52c3d1ad4 100644 --- a/lib/app/views/reset_dialog.dart +++ b/lib/app/views/reset_dialog.dart @@ -72,6 +72,7 @@ class _ResetDialogState extends ConsumerState { StreamSubscription? _subscription; InteractionEvent? _interaction; int _currentStep = -1; + bool _resetting = false; late final int _totalSteps; @override @@ -114,7 +115,7 @@ class _ResetDialogState extends ConsumerState { final isBio = [FormFactor.usbABio, FormFactor.usbCBio] .contains(widget.data.info.formFactor); - final globalReset = isBio && (supported & Capability.piv.value) != 0; + final globalReset = isBio && (enabled & Capability.piv.value) != 0; final l10n = AppLocalizations.of(context)!; double progress = _currentStep == -1 ? 0.0 : _currentStep / (_totalSteps); @@ -125,6 +126,7 @@ class _ResetDialogState extends ConsumerState { return ResponsiveDialog( title: Text(l10n.s_factory_reset), key: factoryResetCancel, + allowCancel: !_resetting || _application == Capability.fido2, onCancel: switch (_application) { Capability.fido2 => _currentStep < _totalSteps ? () { @@ -144,81 +146,97 @@ class _ResetDialogState extends ConsumerState { actions: [ if (_currentStep < _totalSteps) TextButton( - onPressed: switch (_application) { - Capability.fido2 => _subscription == null - ? () async { - _subscription = ref - .read( - fidoStateProvider(widget.data.node.path).notifier) - .reset() - .listen((event) { + onPressed: !_resetting + ? switch (_application) { + Capability.fido2 => () async { + _subscription = ref + .read(fidoStateProvider(widget.data.node.path) + .notifier) + .reset() + .listen((event) { + setState(() { + _resetting = true; + _currentStep++; + _interaction = event; + }); + }, onDone: () { + setState(() { + _currentStep++; + }); + _subscription = null; + }, onError: (e) { + _log.error('Error performing FIDO reset', e); + + if (!context.mounted) return; + Navigator.of(context).pop(); + final String errorMessage; + // TODO: Make this cleaner than importing desktop specific RpcError. + if (e is RpcError) { + if (e.status == 'connection-error') { + errorMessage = l10n.l_failed_connecting_to_fido; + } else if (e.status == 'key-mismatch') { + errorMessage = l10n.l_wrong_inserted_yk_error; + } else if (e.status == 'user-action-timeout') { + errorMessage = l10n.l_user_action_timeout_error; + } else { + errorMessage = e.message; + } + } else { + errorMessage = e.toString(); + } + showMessage( + context, + l10n.l_reset_failed(errorMessage), + duration: const Duration(seconds: 4), + ); + }); + }, + Capability.oath => () async { setState(() { - _currentStep++; - _interaction = event; + _resetting = true; + }); + await ref + .read(oathStateProvider(widget.data.node.path) + .notifier) + .reset(); + await ref.read(withContextProvider)((context) async { + Navigator.of(context).pop(); + showMessage(context, l10n.l_oath_application_reset); }); - }, onDone: () { + }, + Capability.piv => () async { setState(() { - _currentStep++; + _resetting = true; }); - _subscription = null; - }, onError: (e) { - _log.error('Error performing FIDO reset', e); - Navigator.of(context).pop(); - final String errorMessage; - // TODO: Make this cleaner than importing desktop specific RpcError. - if (e is RpcError) { - if (e.status == 'connection-error') { - errorMessage = l10n.l_failed_connecting_to_fido; - } else if (e.status == 'key-mismatch') { - errorMessage = l10n.l_wrong_inserted_yk_error; - } else if (e.status == 'user-action-timeout') { - errorMessage = l10n.l_user_action_timeout_error; - } else { - errorMessage = e.message; + await ref + .read(pivStateProvider(widget.data.node.path) + .notifier) + .reset(); + await ref.read(withContextProvider)((context) async { + Navigator.of(context).pop(); + showMessage(context, l10n.l_piv_app_reset); + }); + }, + null => globalReset + ? () async { + setState(() { + _resetting = true; + }); + await ref + .read(managementStateProvider( + widget.data.node.path) + .notifier) + .deviceReset(); + await ref.read(withContextProvider)( + (context) async { + Navigator.of(context).pop(); + showMessage(context, l10n.s_factory_reset); + }); } - } else { - errorMessage = e.toString(); - } - showMessage( - context, - l10n.l_reset_failed(errorMessage), - duration: const Duration(seconds: 4), - ); - }); - } - : null, - Capability.oath => () async { - await ref - .read(oathStateProvider(widget.data.node.path).notifier) - .reset(); - await ref.read(withContextProvider)((context) async { - Navigator.of(context).pop(); - showMessage(context, l10n.l_oath_application_reset); - }); - }, - Capability.piv => () async { - await ref - .read(pivStateProvider(widget.data.node.path).notifier) - .reset(); - await ref.read(withContextProvider)((context) async { - Navigator.of(context).pop(); - showMessage(context, l10n.l_piv_app_reset); - }); - }, - null => globalReset - ? () async { - await ref - .read(managementStateProvider(widget.data.node.path) - .notifier) - .deviceReset(); - await ref.read(withContextProvider)((context) async { - Navigator.of(context).pop(); - showMessage(context, l10n.s_factory_reset); - }); - } - : null, - _ => throw UnsupportedError('Application cannot be reset'), - }, + : null, + _ => throw UnsupportedError('Application cannot be reset'), + } + : null, key: factoryResetReset, child: Text(l10n.s_reset), ) @@ -304,10 +322,12 @@ class _ResetDialogState extends ConsumerState { }, ), ], - if (_application == Capability.fido2 && _currentStep >= 0) ...[ - Text('${l10n.s_status}: ${_getMessage()}'), - LinearProgressIndicator(value: progress) - ], + if (_resetting) + if (_application == Capability.fido2 && _currentStep >= 0) ...[ + Text('${l10n.s_status}: ${_getMessage()}'), + LinearProgressIndicator(value: progress), + ] else + const LinearProgressIndicator() ] .map((e) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), diff --git a/lib/app/views/user_interaction.dart b/lib/app/views/user_interaction.dart index 8b6e872e6..ad5907003 100755 --- a/lib/app/views/user_interaction.dart +++ b/lib/app/views/user_interaction.dart @@ -175,7 +175,7 @@ UserInteractionController _dialogUserInteraction( builder: (context) { return PopScope( canPop: onCancel != null, - onPopInvoked: (didPop) { + onPopInvokedWithResult: (didPop, _) { if (didPop) { wasPopped = true; if (!completed && onCancel != null) { diff --git a/lib/core/models.freezed.dart b/lib/core/models.freezed.dart index 545c06be6..5fe345793 100644 --- a/lib/core/models.freezed.dart +++ b/lib/core/models.freezed.dart @@ -20,7 +20,9 @@ mixin _$Version { int get minor => throw _privateConstructorUsedError; int get patch => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of Version + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $VersionCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -42,6 +44,8 @@ class _$VersionCopyWithImpl<$Res, $Val extends Version> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of Version + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -84,6 +88,8 @@ class __$$VersionImplCopyWithImpl<$Res> _$VersionImpl _value, $Res Function(_$VersionImpl) _then) : super(_value, _then); + /// Create a copy of Version + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -140,7 +146,9 @@ class _$VersionImpl extends _Version { @override int get hashCode => Object.hash(runtimeType, major, minor, patch); - @JsonKey(ignore: true) + /// Create a copy of Version + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$VersionImplCopyWith<_$VersionImpl> get copyWith => @@ -158,8 +166,11 @@ abstract class _Version extends Version { int get minor; @override int get patch; + + /// Create a copy of Version + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$VersionImplCopyWith<_$VersionImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/core/state.dart b/lib/core/state.dart index 537eae615..f250a852b 100644 --- a/lib/core/state.dart +++ b/lib/core/state.dart @@ -21,6 +21,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../app/models.dart'; +import '../widgets/flex_box.dart'; bool get isDesktop => const [ TargetPlatform.windows, @@ -119,3 +120,20 @@ final featureProvider = Provider((ref) { return isEnabled; }); + +class LayoutNotifier extends StateNotifier { + final String _key; + final SharedPreferences _prefs; + LayoutNotifier(this._key, this._prefs) + : super(_fromName(_prefs.getString(_key))); + + void setLayout(FlexLayout layout) { + state = layout; + _prefs.setString(_key, layout.name); + } + + static FlexLayout _fromName(String? name) => FlexLayout.values.firstWhere( + (element) => element.name == name, + orElse: () => FlexLayout.list, + ); +} diff --git a/lib/desktop/devices.dart b/lib/desktop/devices.dart index df28c0df5..aae49762e 100755 --- a/lib/desktop/devices.dart +++ b/lib/desktop/devices.dart @@ -87,15 +87,16 @@ class UsbDeviceNotifier extends StateNotifier> { return; } - final pids = { - for (var e in (scan['pids'] as Map).entries) - UsbPid.fromValue(int.parse(e.key)): e.value as int - }; - final numDevices = pids.values.fold(0, (a, b) => a + b); + final numDevices = + (scan['pids'] as Map).values.fold(0, (a, b) => a + b as int); if (_usbState != scan['state'] || state.length != numDevices) { var usbResult = await rpc.command('get', ['usb']); _log.info('USB state change', jsonEncode(usbResult)); _usbState = usbResult['data']['state']; + final pids = { + for (var e in (usbResult['data']['pids'] as Map).entries) + UsbPid.fromValue(int.parse(e.key)): e.value as int + }; List usbDevices = []; for (String id in (usbResult['children'] as Map).keys) { @@ -224,11 +225,12 @@ final _desktopDeviceDataProvider = ref.watch(rpcProvider).valueOrNull, ref.watch(currentDeviceProvider), ); - if (notifier._deviceNode is NfcReaderNode) { - // If this is an NFC reader, listen on WindowState. - ref.listen(windowStateProvider, (_, windowState) { - notifier._notifyWindowState(windowState); - }, fireImmediately: true); + ref.listen(windowStateProvider, (_, windowState) { + notifier._notifyWindowState(windowState); + }); + if (notifier._deviceNode is NfcReaderNode && + ref.read(windowStateProvider).active) { + notifier._pollCard(); } return notifier; }); @@ -243,6 +245,7 @@ class CurrentDeviceDataNotifier extends StateNotifier> { final RpcSession? _rpc; final DeviceNode? _deviceNode; Timer? _pollTimer; + StreamSubscription? _flagSubscription; CurrentDeviceDataNotifier(this._rpc, this._deviceNode) : super(const AsyncValue.loading()) { @@ -255,11 +258,27 @@ class CurrentDeviceDataNotifier extends StateNotifier> { state = AsyncValue.error('device-inaccessible', StackTrace.current); } } + _flagSubscription = _rpc?.flags.listen( + (flag) { + if (flag == 'device_info') { + _pollDevice(); + } + }, + ); + } + + void _pollDevice() { + switch (_deviceNode) { + case UsbYubiKeyNode _: + _refreshUsb(); + case NfcReaderNode _: + _pollCard(); + } } void _notifyWindowState(WindowState windowState) { if (windowState.active) { - _pollCard(); + _pollDevice(); } else { _pollTimer?.cancel(); // TODO: Should we clear the key here? @@ -271,25 +290,39 @@ class CurrentDeviceDataNotifier extends StateNotifier> { @override void dispose() { + _flagSubscription?.cancel(); _pollTimer?.cancel(); super.dispose(); } + void _refreshUsb() async { + final node = _deviceNode!; + var result = await _rpc?.command('get', node.path.segments); + if (mounted && result != null) { + final newState = YubiKeyData(node, result['data']['name'], + DeviceInfo.fromJson(result['data']['info'])); + if (state.valueOrNull != newState) { + _log.info('Configuration change in current USB device'); + state = AsyncValue.data(newState); + } + } + } + void _pollCard() async { _pollTimer?.cancel(); final node = _deviceNode!; try { - _log.debug('Polling for NFC device changes...'); var result = await _rpc?.command('get', node.path.segments); if (mounted && result != null) { if (result['data']['present']) { final oldState = state.valueOrNull; final newState = YubiKeyData(node, result['data']['name'], DeviceInfo.fromJson(result['data']['info'])); - if (oldState != null && oldState != newState) { - // Ensure state is cleared - state = const AsyncValue.loading(); - } else { + if (oldState != newState) { + if (oldState != null) { + // Ensure state is cleared + state = const AsyncValue.loading(); + } state = AsyncValue.data(newState); } } else { diff --git a/lib/desktop/fido/state.dart b/lib/desktop/fido/state.dart index d886cfc96..272d8a375 100755 --- a/lib/desktop/fido/state.dart +++ b/lib/desktop/fido/state.dart @@ -184,6 +184,12 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier { rethrow; } } + + @override + Future enableEnterpriseAttestation() async { + await _session.command('enable_ep_attestation'); + ref.invalidateSelf(); + } } final desktopFingerprintProvider = AsyncNotifierProvider.autoDispose diff --git a/lib/desktop/init.dart b/lib/desktop/init.dart index 2473642a1..9a78868e9 100755 --- a/lib/desktop/init.dart +++ b/lib/desktop/init.dart @@ -384,7 +384,6 @@ class _HelperWaiterState extends ConsumerState<_HelperWaiter> { actionsBuilder: (context, expanded) => [ ActionChip( avatar: const Icon(Symbols.content_copy), - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, label: Text(l10n.s_copy_log), onPressed: () async { _log.info('Copying log to clipboard ($version)...'); diff --git a/lib/desktop/models.dart b/lib/desktop/models.dart index b1775a000..8d084983a 100755 --- a/lib/desktop/models.dart +++ b/lib/desktop/models.dart @@ -21,7 +21,8 @@ part 'models.g.dart'; @Freezed(unionKey: 'kind') class RpcResponse with _$RpcResponse { - factory RpcResponse.success(Map body) = Success; + factory RpcResponse.success(Map body, List flags) = + Success; factory RpcResponse.signal(String status, Map body) = Signal; factory RpcResponse.error( String status, String message, Map body) = RpcError; diff --git a/lib/desktop/models.freezed.dart b/lib/desktop/models.freezed.dart index 9e244b566..b346e8a5b 100644 --- a/lib/desktop/models.freezed.dart +++ b/lib/desktop/models.freezed.dart @@ -34,7 +34,8 @@ mixin _$RpcResponse { Map get body => throw _privateConstructorUsedError; @optionalTypeArgs TResult when({ - required TResult Function(Map body) success, + required TResult Function(Map body, List flags) + success, required TResult Function(String status, Map body) signal, required TResult Function( String status, String message, Map body) @@ -43,7 +44,7 @@ mixin _$RpcResponse { throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ - TResult? Function(Map body)? success, + TResult? Function(Map body, List flags)? success, TResult? Function(String status, Map body)? signal, TResult? Function(String status, String message, Map body)? error, @@ -51,7 +52,7 @@ mixin _$RpcResponse { throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ - TResult Function(Map body)? success, + TResult Function(Map body, List flags)? success, TResult Function(String status, Map body)? signal, TResult Function(String status, String message, Map body)? error, @@ -80,8 +81,13 @@ mixin _$RpcResponse { required TResult orElse(), }) => throw _privateConstructorUsedError; + + /// Serializes this RpcResponse to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of RpcResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $RpcResponseCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -105,6 +111,8 @@ class _$RpcResponseCopyWithImpl<$Res, $Val extends RpcResponse> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of RpcResponse + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -127,7 +135,7 @@ abstract class _$$SuccessImplCopyWith<$Res> __$$SuccessImplCopyWithImpl<$Res>; @override @useResult - $Res call({Map body}); + $Res call({Map body, List flags}); } /// @nodoc @@ -138,16 +146,23 @@ class __$$SuccessImplCopyWithImpl<$Res> _$SuccessImpl _value, $Res Function(_$SuccessImpl) _then) : super(_value, _then); + /// Create a copy of RpcResponse + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? body = null, + Object? flags = null, }) { return _then(_$SuccessImpl( null == body ? _value._body : body // ignore: cast_nullable_to_non_nullable as Map, + null == flags + ? _value._flags + : flags // ignore: cast_nullable_to_non_nullable + as List, )); } } @@ -155,8 +170,10 @@ class __$$SuccessImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$SuccessImpl implements Success { - _$SuccessImpl(final Map body, {final String? $type}) + _$SuccessImpl(final Map body, final List flags, + {final String? $type}) : _body = body, + _flags = flags, $type = $type ?? 'success'; factory _$SuccessImpl.fromJson(Map json) => @@ -170,12 +187,20 @@ class _$SuccessImpl implements Success { return EqualUnmodifiableMapView(_body); } + final List _flags; + @override + List get flags { + if (_flags is EqualUnmodifiableListView) return _flags; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_flags); + } + @JsonKey(name: 'kind') final String $type; @override String toString() { - return 'RpcResponse.success(body: $body)'; + return 'RpcResponse.success(body: $body, flags: $flags)'; } @override @@ -183,15 +208,20 @@ class _$SuccessImpl implements Success { return identical(this, other) || (other.runtimeType == runtimeType && other is _$SuccessImpl && - const DeepCollectionEquality().equals(other._body, _body)); + const DeepCollectionEquality().equals(other._body, _body) && + const DeepCollectionEquality().equals(other._flags, _flags)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => - Object.hash(runtimeType, const DeepCollectionEquality().hash(_body)); + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_body), + const DeepCollectionEquality().hash(_flags)); - @JsonKey(ignore: true) + /// Create a copy of RpcResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SuccessImplCopyWith<_$SuccessImpl> get copyWith => @@ -200,37 +230,38 @@ class _$SuccessImpl implements Success { @override @optionalTypeArgs TResult when({ - required TResult Function(Map body) success, + required TResult Function(Map body, List flags) + success, required TResult Function(String status, Map body) signal, required TResult Function( String status, String message, Map body) error, }) { - return success(body); + return success(body, flags); } @override @optionalTypeArgs TResult? whenOrNull({ - TResult? Function(Map body)? success, + TResult? Function(Map body, List flags)? success, TResult? Function(String status, Map body)? signal, TResult? Function(String status, String message, Map body)? error, }) { - return success?.call(body); + return success?.call(body, flags); } @override @optionalTypeArgs TResult maybeWhen({ - TResult Function(Map body)? success, + TResult Function(Map body, List flags)? success, TResult Function(String status, Map body)? signal, TResult Function(String status, String message, Map body)? error, required TResult orElse(), }) { if (success != null) { - return success(body); + return success(body, flags); } return orElse(); } @@ -278,14 +309,19 @@ class _$SuccessImpl implements Success { } abstract class Success implements RpcResponse { - factory Success(final Map body) = _$SuccessImpl; + factory Success(final Map body, final List flags) = + _$SuccessImpl; factory Success.fromJson(Map json) = _$SuccessImpl.fromJson; @override Map get body; + List get flags; + + /// Create a copy of RpcResponse + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SuccessImplCopyWith<_$SuccessImpl> get copyWith => throw _privateConstructorUsedError; } @@ -309,6 +345,8 @@ class __$$SignalImplCopyWithImpl<$Res> _$SignalImpl _value, $Res Function(_$SignalImpl) _then) : super(_value, _then); + /// Create a copy of RpcResponse + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -366,12 +404,14 @@ class _$SignalImpl implements Signal { const DeepCollectionEquality().equals(other._body, _body)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, status, const DeepCollectionEquality().hash(_body)); - @JsonKey(ignore: true) + /// Create a copy of RpcResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SignalImplCopyWith<_$SignalImpl> get copyWith => @@ -380,7 +420,8 @@ class _$SignalImpl implements Signal { @override @optionalTypeArgs TResult when({ - required TResult Function(Map body) success, + required TResult Function(Map body, List flags) + success, required TResult Function(String status, Map body) signal, required TResult Function( String status, String message, Map body) @@ -392,7 +433,7 @@ class _$SignalImpl implements Signal { @override @optionalTypeArgs TResult? whenOrNull({ - TResult? Function(Map body)? success, + TResult? Function(Map body, List flags)? success, TResult? Function(String status, Map body)? signal, TResult? Function(String status, String message, Map body)? error, @@ -403,7 +444,7 @@ class _$SignalImpl implements Signal { @override @optionalTypeArgs TResult maybeWhen({ - TResult Function(Map body)? success, + TResult Function(Map body, List flags)? success, TResult Function(String status, Map body)? signal, TResult Function(String status, String message, Map body)? error, @@ -466,8 +507,11 @@ abstract class Signal implements RpcResponse { String get status; @override Map get body; + + /// Create a copy of RpcResponse + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SignalImplCopyWith<_$SignalImpl> get copyWith => throw _privateConstructorUsedError; } @@ -491,6 +535,8 @@ class __$$RpcErrorImplCopyWithImpl<$Res> _$RpcErrorImpl _value, $Res Function(_$RpcErrorImpl) _then) : super(_value, _then); + /// Create a copy of RpcResponse + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -556,12 +602,14 @@ class _$RpcErrorImpl implements RpcError { const DeepCollectionEquality().equals(other._body, _body)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, status, message, const DeepCollectionEquality().hash(_body)); - @JsonKey(ignore: true) + /// Create a copy of RpcResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$RpcErrorImplCopyWith<_$RpcErrorImpl> get copyWith => @@ -570,7 +618,8 @@ class _$RpcErrorImpl implements RpcError { @override @optionalTypeArgs TResult when({ - required TResult Function(Map body) success, + required TResult Function(Map body, List flags) + success, required TResult Function(String status, Map body) signal, required TResult Function( String status, String message, Map body) @@ -582,7 +631,7 @@ class _$RpcErrorImpl implements RpcError { @override @optionalTypeArgs TResult? whenOrNull({ - TResult? Function(Map body)? success, + TResult? Function(Map body, List flags)? success, TResult? Function(String status, Map body)? signal, TResult? Function(String status, String message, Map body)? error, @@ -593,7 +642,7 @@ class _$RpcErrorImpl implements RpcError { @override @optionalTypeArgs TResult maybeWhen({ - TResult Function(Map body)? success, + TResult Function(Map body, List flags)? success, TResult Function(String status, Map body)? signal, TResult Function(String status, String message, Map body)? error, @@ -658,8 +707,11 @@ abstract class RpcError implements RpcResponse { String get message; @override Map get body; + + /// Create a copy of RpcResponse + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$RpcErrorImplCopyWith<_$RpcErrorImpl> get copyWith => throw _privateConstructorUsedError; } @@ -673,8 +725,12 @@ mixin _$RpcState { String get version => throw _privateConstructorUsedError; bool get isAdmin => throw _privateConstructorUsedError; + /// Serializes this RpcState to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of RpcState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $RpcStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -697,6 +753,8 @@ class _$RpcStateCopyWithImpl<$Res, $Val extends RpcState> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of RpcState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -735,6 +793,8 @@ class __$$RpcStateImplCopyWithImpl<$Res> _$RpcStateImpl _value, $Res Function(_$RpcStateImpl) _then) : super(_value, _then); + /// Create a copy of RpcState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -781,11 +841,13 @@ class _$RpcStateImpl implements _RpcState { (identical(other.isAdmin, isAdmin) || other.isAdmin == isAdmin)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, version, isAdmin); - @JsonKey(ignore: true) + /// Create a copy of RpcState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$RpcStateImplCopyWith<_$RpcStateImpl> get copyWith => @@ -810,8 +872,11 @@ abstract class _RpcState implements RpcState { String get version; @override bool get isAdmin; + + /// Create a copy of RpcState + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$RpcStateImplCopyWith<_$RpcStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/desktop/models.g.dart b/lib/desktop/models.g.dart index 1257b2ffd..099fabf1e 100644 --- a/lib/desktop/models.g.dart +++ b/lib/desktop/models.g.dart @@ -9,12 +9,14 @@ part of 'models.dart'; _$SuccessImpl _$$SuccessImplFromJson(Map json) => _$SuccessImpl( json['body'] as Map, + (json['flags'] as List).map((e) => e as String).toList(), $type: json['kind'] as String?, ); Map _$$SuccessImplToJson(_$SuccessImpl instance) => { 'body': instance.body, + 'flags': instance.flags, 'kind': instance.$type, }; diff --git a/lib/desktop/rpc.dart b/lib/desktop/rpc.dart index 932f6a3b7..03e68f327 100644 --- a/lib/desktop/rpc.dart +++ b/lib/desktop/rpc.dart @@ -102,8 +102,12 @@ class RpcSession { final String executable; late _RpcConnection _connection; final StreamController<_Request> _requests = StreamController(); + final StreamController _flags = StreamController(); + late final Stream flags; - RpcSession(this.executable); + RpcSession(this.executable) { + flags = _flags.stream.asBroadcastStream(); + } static void _logEntry(String entry) { try { @@ -230,7 +234,7 @@ class RpcSession { Future> command(String action, List? target, {Map? params, Signaler? signal}) { - var request = _Request(action, target ?? [], params ?? {}, signal); + final request = _Request(action, target ?? [], params ?? {}, signal); _requests.add(request); return request.completer.future; } @@ -278,6 +282,10 @@ class RpcSession { }, success: (success) { request.completer.complete(success.body); + for (final flag in success.flags) { + _log.traffic('FLAG', flag); + _flags.add(flag); + } completed = true; }, error: (error) { diff --git a/lib/fido/features.dart b/lib/fido/features.dart index 407426119..cf3fcede7 100644 --- a/lib/fido/features.dart +++ b/lib/fido/features.dart @@ -21,6 +21,8 @@ final actions = fido.feature('actions'); final actionsPin = actions.feature('pin'); final actionsAddFingerprint = actions.feature('addFingerprint'); final actionsReset = actions.feature('reset'); +final enableEnterpriseAttestation = + actions.feature('enableEnterpriseAttestation'); final credentials = fido.feature('credentials'); diff --git a/lib/fido/keys.dart b/lib/fido/keys.dart index c967aab0f..7fd4be3e8 100644 --- a/lib/fido/keys.dart +++ b/lib/fido/keys.dart @@ -25,6 +25,8 @@ const _credentialInfo = '$_prefix.credential.info'; // Key actions const managePinAction = Key('$_keyAction.manage_pin'); const addFingerprintAction = Key('$_keyAction.add_fingerprint'); +const enableEnterpriseAttestation = + Key('$_keyAction.enable_enterprise_attestation'); const newPin = Key('$_keyAction.new_pin'); const confirmPin = Key('$_keyAction.confirm_pin'); const currentPin = Key('$_keyAction.current_pin'); diff --git a/lib/fido/models.dart b/lib/fido/models.dart index 8bb39ba2e..a5a6601bb 100755 --- a/lib/fido/models.dart +++ b/lib/fido/models.dart @@ -50,6 +50,8 @@ class FidoState with _$FidoState { bool get forcePinChange => info['force_pin_change'] == true; bool get pinBlocked => pinRetries == 0; + + bool? get enterpriseAttestation => info['options']['ep']; } @freezed diff --git a/lib/fido/models.freezed.dart b/lib/fido/models.freezed.dart index 298fe0ae6..211a52b90 100644 --- a/lib/fido/models.freezed.dart +++ b/lib/fido/models.freezed.dart @@ -24,8 +24,12 @@ mixin _$FidoState { bool get unlocked => throw _privateConstructorUsedError; int? get pinRetries => throw _privateConstructorUsedError; + /// Serializes this FidoState to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of FidoState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $FidoStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -48,6 +52,8 @@ class _$FidoStateCopyWithImpl<$Res, $Val extends FidoState> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of FidoState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -91,6 +97,8 @@ class __$$FidoStateImplCopyWithImpl<$Res> _$FidoStateImpl _value, $Res Function(_$FidoStateImpl) _then) : super(_value, _then); + /// Create a copy of FidoState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -158,12 +166,14 @@ class _$FidoStateImpl extends _FidoState { other.pinRetries == pinRetries)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, const DeepCollectionEquality().hash(_info), unlocked, pinRetries); - @JsonKey(ignore: true) + /// Create a copy of FidoState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$FidoStateImplCopyWith<_$FidoStateImpl> get copyWith => @@ -193,8 +203,11 @@ abstract class _FidoState extends FidoState { bool get unlocked; @override int? get pinRetries; + + /// Create a copy of FidoState + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$FidoStateImplCopyWith<_$FidoStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -256,6 +269,9 @@ class _$PinResultCopyWithImpl<$Res, $Val extends PinResult> final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of PinResult + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -272,6 +288,9 @@ class __$$PinSuccessImplCopyWithImpl<$Res> __$$PinSuccessImplCopyWithImpl( _$PinSuccessImpl _value, $Res Function(_$PinSuccessImpl) _then) : super(_value, _then); + + /// Create a copy of PinResult + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -379,6 +398,8 @@ class __$$PinFailureImplCopyWithImpl<$Res> _$PinFailureImpl _value, $Res Function(_$PinFailureImpl) _then) : super(_value, _then); + /// Create a copy of PinResult + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -392,6 +413,8 @@ class __$$PinFailureImplCopyWithImpl<$Res> )); } + /// Create a copy of PinResult + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $FidoPinFailureReasonCopyWith<$Res> get reason { @@ -425,7 +448,9 @@ class _$PinFailureImpl implements _PinFailure { @override int get hashCode => Object.hash(runtimeType, reason); - @JsonKey(ignore: true) + /// Create a copy of PinResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PinFailureImplCopyWith<_$PinFailureImpl> get copyWith => @@ -498,7 +523,10 @@ abstract class _PinFailure implements PinResult { factory _PinFailure(final FidoPinFailureReason reason) = _$PinFailureImpl; FidoPinFailureReason get reason; - @JsonKey(ignore: true) + + /// Create a copy of PinResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$PinFailureImplCopyWith<_$PinFailureImpl> get copyWith => throw _privateConstructorUsedError; } @@ -562,6 +590,9 @@ class _$FidoPinFailureReasonCopyWithImpl<$Res, final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of FidoPinFailureReason + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -581,6 +612,8 @@ class __$$FidoInvalidPinImplCopyWithImpl<$Res> _$FidoInvalidPinImpl _value, $Res Function(_$FidoInvalidPinImpl) _then) : super(_value, _then); + /// Create a copy of FidoPinFailureReason + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -628,7 +661,9 @@ class _$FidoInvalidPinImpl implements FidoInvalidPin { @override int get hashCode => Object.hash(runtimeType, retries, authBlocked); - @JsonKey(ignore: true) + /// Create a copy of FidoPinFailureReason + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$FidoInvalidPinImplCopyWith<_$FidoInvalidPinImpl> get copyWith => @@ -704,7 +739,10 @@ abstract class FidoInvalidPin implements FidoPinFailureReason { int get retries; bool get authBlocked; - @JsonKey(ignore: true) + + /// Create a copy of FidoPinFailureReason + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$FidoInvalidPinImplCopyWith<_$FidoInvalidPinImpl> get copyWith => throw _privateConstructorUsedError; } @@ -723,6 +761,9 @@ class __$$FidoWeakPinImplCopyWithImpl<$Res> __$$FidoWeakPinImplCopyWithImpl( _$FidoWeakPinImpl _value, $Res Function(_$FidoWeakPinImpl) _then) : super(_value, _then); + + /// Create a copy of FidoPinFailureReason + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -820,8 +861,12 @@ mixin _$Fingerprint { String get templateId => throw _privateConstructorUsedError; String? get name => throw _privateConstructorUsedError; + /// Serializes this Fingerprint to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of Fingerprint + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $FingerprintCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -845,6 +890,8 @@ class _$FingerprintCopyWithImpl<$Res, $Val extends Fingerprint> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of Fingerprint + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -883,6 +930,8 @@ class __$$FingerprintImplCopyWithImpl<$Res> _$FingerprintImpl _value, $Res Function(_$FingerprintImpl) _then) : super(_value, _then); + /// Create a copy of Fingerprint + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -930,11 +979,13 @@ class _$FingerprintImpl extends _Fingerprint { (identical(other.name, name) || other.name == name)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, templateId, name); - @JsonKey(ignore: true) + /// Create a copy of Fingerprint + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$FingerprintImplCopyWith<_$FingerprintImpl> get copyWith => @@ -960,8 +1011,11 @@ abstract class _Fingerprint extends Fingerprint { String get templateId; @override String? get name; + + /// Create a copy of Fingerprint + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$FingerprintImplCopyWith<_$FingerprintImpl> get copyWith => throw _privateConstructorUsedError; } @@ -1030,6 +1084,9 @@ class _$FingerprintEventCopyWithImpl<$Res, $Val extends FingerprintEvent> final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of FingerprintEvent + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -1049,6 +1106,8 @@ class __$$EventCaptureImplCopyWithImpl<$Res> _$EventCaptureImpl _value, $Res Function(_$EventCaptureImpl) _then) : super(_value, _then); + /// Create a copy of FingerprintEvent + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1088,7 +1147,9 @@ class _$EventCaptureImpl implements _EventCapture { @override int get hashCode => Object.hash(runtimeType, remaining); - @JsonKey(ignore: true) + /// Create a copy of FingerprintEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$EventCaptureImplCopyWith<_$EventCaptureImpl> get copyWith => @@ -1167,7 +1228,10 @@ abstract class _EventCapture implements FingerprintEvent { factory _EventCapture(final int remaining) = _$EventCaptureImpl; int get remaining; - @JsonKey(ignore: true) + + /// Create a copy of FingerprintEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$EventCaptureImplCopyWith<_$EventCaptureImpl> get copyWith => throw _privateConstructorUsedError; } @@ -1191,6 +1255,8 @@ class __$$EventCompleteImplCopyWithImpl<$Res> _$EventCompleteImpl _value, $Res Function(_$EventCompleteImpl) _then) : super(_value, _then); + /// Create a copy of FingerprintEvent + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1204,6 +1270,8 @@ class __$$EventCompleteImplCopyWithImpl<$Res> )); } + /// Create a copy of FingerprintEvent + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $FingerprintCopyWith<$Res> get fingerprint { @@ -1238,7 +1306,9 @@ class _$EventCompleteImpl implements _EventComplete { @override int get hashCode => Object.hash(runtimeType, fingerprint); - @JsonKey(ignore: true) + /// Create a copy of FingerprintEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$EventCompleteImplCopyWith<_$EventCompleteImpl> get copyWith => @@ -1317,7 +1387,10 @@ abstract class _EventComplete implements FingerprintEvent { factory _EventComplete(final Fingerprint fingerprint) = _$EventCompleteImpl; Fingerprint get fingerprint; - @JsonKey(ignore: true) + + /// Create a copy of FingerprintEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$EventCompleteImplCopyWith<_$EventCompleteImpl> get copyWith => throw _privateConstructorUsedError; } @@ -1339,6 +1412,8 @@ class __$$EventErrorImplCopyWithImpl<$Res> _$EventErrorImpl _value, $Res Function(_$EventErrorImpl) _then) : super(_value, _then); + /// Create a copy of FingerprintEvent + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1377,7 +1452,9 @@ class _$EventErrorImpl implements _EventError { @override int get hashCode => Object.hash(runtimeType, code); - @JsonKey(ignore: true) + /// Create a copy of FingerprintEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$EventErrorImplCopyWith<_$EventErrorImpl> get copyWith => @@ -1456,7 +1533,10 @@ abstract class _EventError implements FingerprintEvent { factory _EventError(final int code) = _$EventErrorImpl; int get code; - @JsonKey(ignore: true) + + /// Create a copy of FingerprintEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$EventErrorImplCopyWith<_$EventErrorImpl> get copyWith => throw _privateConstructorUsedError; } @@ -1473,8 +1553,12 @@ mixin _$FidoCredential { String get userName => throw _privateConstructorUsedError; String? get displayName => throw _privateConstructorUsedError; + /// Serializes this FidoCredential to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of FidoCredential + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $FidoCredentialCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -1503,6 +1587,8 @@ class _$FidoCredentialCopyWithImpl<$Res, $Val extends FidoCredential> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of FidoCredential + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1561,6 +1647,8 @@ class __$$FidoCredentialImplCopyWithImpl<$Res> _$FidoCredentialImpl _value, $Res Function(_$FidoCredentialImpl) _then) : super(_value, _then); + /// Create a copy of FidoCredential + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1639,12 +1727,14 @@ class _$FidoCredentialImpl implements _FidoCredential { other.displayName == displayName)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, rpId, credentialId, userId, userName, displayName); - @JsonKey(ignore: true) + /// Create a copy of FidoCredential + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$FidoCredentialImplCopyWith<_$FidoCredentialImpl> get copyWith => @@ -1680,8 +1770,11 @@ abstract class _FidoCredential implements FidoCredential { String get userName; @override String? get displayName; + + /// Create a copy of FidoCredential + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$FidoCredentialImplCopyWith<_$FidoCredentialImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/fido/models.g.dart b/lib/fido/models.g.dart index 54fe1a9a6..8715fd9ab 100644 --- a/lib/fido/models.g.dart +++ b/lib/fido/models.g.dart @@ -10,7 +10,7 @@ _$FidoStateImpl _$$FidoStateImplFromJson(Map json) => _$FidoStateImpl( info: json['info'] as Map, unlocked: json['unlocked'] as bool, - pinRetries: json['pin_retries'] as int?, + pinRetries: (json['pin_retries'] as num?)?.toInt(), ); Map _$$FidoStateImplToJson(_$FidoStateImpl instance) => diff --git a/lib/fido/state.dart b/lib/fido/state.dart index 367d5cb94..785bb7433 100755 --- a/lib/fido/state.dart +++ b/lib/fido/state.dart @@ -18,6 +18,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/models.dart'; import '../core/state.dart'; +import '../widgets/flex_box.dart'; import 'models.dart'; final passkeysSearchProvider = @@ -32,6 +33,11 @@ class PasskeysSearchNotifier extends StateNotifier { } } +final passkeysLayoutProvider = + StateNotifierProvider( + (ref) => LayoutNotifier('FIDO_PASSKEYS_LAYOUT', ref.watch(prefProvider)), +); + final fidoStateProvider = AsyncNotifierProvider.autoDispose .family( () => throw UnimplementedError(), @@ -41,6 +47,7 @@ abstract class FidoStateNotifier extends ApplicationStateNotifier { Stream reset(); Future setPin(String newPin, {String? oldPin}); Future unlock(String pin); + Future enableEnterpriseAttestation(); } final fingerprintProvider = AsyncNotifierProvider.autoDispose diff --git a/lib/fido/views/add_fingerprint_dialog.dart b/lib/fido/views/add_fingerprint_dialog.dart index 69b2392a0..fff115b10 100755 --- a/lib/fido/views/add_fingerprint_dialog.dart +++ b/lib/fido/views/add_fingerprint_dialog.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2023 Yubico. + * Copyright (C) 2022-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -123,6 +123,8 @@ class _AddFingerprintDialogState extends ConsumerState }); }, onError: (error, stacktrace) { _log.error('Error adding fingerprint', error, stacktrace); + + if (!mounted) return; Navigator.of(context).pop(); final l10n = AppLocalizations.of(context)!; final String errorMessage; @@ -255,7 +257,11 @@ class _AddFingerprintDialogState extends ConsumerState }); }, onFieldSubmitted: (_) { - _submit(); + if (_label.isNotEmpty) { + _submit(); + } else { + _nameFocus.requestFocus(); + } }, ).init(), ) diff --git a/lib/fido/views/enterprise_attestation_dialog.dart b/lib/fido/views/enterprise_attestation_dialog.dart new file mode 100644 index 000000000..afe695b06 --- /dev/null +++ b/lib/fido/views/enterprise_attestation_dialog.dart @@ -0,0 +1,57 @@ +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 '../state.dart'; + +class EnableEnterpriseAttestationDialog extends ConsumerWidget { + final DevicePath devicePath; + const EnableEnterpriseAttestationDialog(this.devicePath, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + return ResponsiveDialog( + title: Text(l10n.s_enable_ep_attestation), + actions: [ + TextButton( + onPressed: () async { + await ref + .read(fidoStateProvider(devicePath).notifier) + .enableEnterpriseAttestation(); + await ref.read(withContextProvider)((context) async { + Navigator.of(context).pop(); + showMessage(context, l10n.s_ep_attestation_enabled); + }); + }, + child: Text(l10n.s_enable), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.p_enable_ep_attestation_desc, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.w700), + ), + Text(l10n.p_enable_ep_attestation_disable_with_factory_reset), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/fido/views/fingerprints_screen.dart b/lib/fido/views/fingerprints_screen.dart index a92a13419..97b24f3af 100644 --- a/lib/fido/views/fingerprints_screen.dart +++ b/lib/fido/views/fingerprints_screen.dart @@ -30,6 +30,7 @@ import '../../app/views/app_list_item.dart'; import '../../app/views/app_page.dart'; import '../../app/views/message_page.dart'; import '../../app/views/message_page_not_initialized.dart'; +import '../../core/models.dart'; import '../../core/state.dart'; import '../../exception/no_data_exception.dart'; import '../../management/models.dart'; @@ -44,6 +45,14 @@ import 'key_actions.dart'; import 'pin_dialog.dart'; import 'pin_entry_form.dart'; +List _getCapabilities(YubiKeyData deviceData) => [ + Capability.fido2, + if (deviceData.info.config.enabledCapabilities[Transport.usb]! & + Capability.piv.value != + 0) + Capability.piv + ]; + class FingerprintsScreen extends ConsumerWidget { final YubiKeyData deviceData; @@ -52,10 +61,11 @@ class FingerprintsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context)!; + final capabilities = _getCapabilities(deviceData); return ref.watch(fidoStateProvider(deviceData.node.path)).when( loading: () => AppPage( title: l10n.s_fingerprints, - capabilities: const [Capability.fido2], + capabilities: capabilities, centered: true, delayedContent: true, builder: (context, _) => const CircularProgressIndicator(), @@ -64,7 +74,7 @@ class FingerprintsScreen extends ConsumerWidget { if (error is NoDataException) { return MessagePageNotInitialized( title: l10n.s_fingerprints, - capabilities: const [Capability.fido2], + capabilities: capabilities, ); } final enabled = deviceData @@ -73,7 +83,7 @@ class FingerprintsScreen extends ConsumerWidget { if (Capability.fido2.value & enabled == 0) { return MessagePage( title: l10n.s_fingerprints, - capabilities: const [Capability.fido2], + capabilities: capabilities, header: l10n.s_fido_disabled, message: l10n.l_webauthn_req_fido2, ); @@ -85,17 +95,17 @@ class FingerprintsScreen extends ConsumerWidget { }, data: (fidoState) { return fidoState.unlocked - ? _FidoUnlockedPage(deviceData.node, fidoState) - : _FidoLockedPage(deviceData.node, fidoState); + ? _FidoUnlockedPage(deviceData, fidoState) + : _FidoLockedPage(deviceData, fidoState); }); } } class _FidoLockedPage extends ConsumerWidget { - final DeviceNode node; + final YubiKeyData deviceData; final FidoState state; - const _FidoLockedPage(this.node, this.state); + const _FidoLockedPage(this.deviceData, this.state); @override Widget build(BuildContext context, WidgetRef ref) { @@ -103,6 +113,14 @@ class _FidoLockedPage extends ConsumerWidget { final hasFeature = ref.watch(featureProvider); final hasActions = hasFeature(features.actions); + final capabilities = [ + Capability.fido2, + if (deviceData.info.config.enabledCapabilities[Transport.usb]! & + Capability.piv.value != + 0) + Capability.piv + ]; + if (!state.hasPin) { return MessagePage( actionsBuilder: (context, expanded) => [ @@ -112,13 +130,14 @@ class _FidoLockedPage extends ConsumerWidget { onPressed: () async { await showBlurDialog( context: context, - builder: (context) => FidoPinDialog(node.path, state)); + builder: (context) => + FidoPinDialog(deviceData.node.path, state)); }, avatar: const Icon(Symbols.pin), ) ], title: l10n.s_fingerprints, - capabilities: const [Capability.fido2], + capabilities: capabilities, header: l10n.s_fingerprints_get_started, message: l10n.p_set_fingerprints_desc, keyActionsBuilder: hasActions ? _buildActions : null, @@ -129,7 +148,7 @@ class _FidoLockedPage extends ConsumerWidget { if (state.forcePinChange) { return MessagePage( title: l10n.s_fingerprints, - capabilities: const [Capability.fido2], + capabilities: capabilities, header: l10n.s_pin_change_required, message: l10n.l_pin_change_required_desc, keyActionsBuilder: hasActions ? _buildActions : null, @@ -141,7 +160,8 @@ class _FidoLockedPage extends ConsumerWidget { onPressed: () async { await showBlurDialog( context: context, - builder: (context) => FidoPinDialog(node.path, state)); + builder: (context) => + FidoPinDialog(deviceData.node.path, state)); }, avatar: const Icon(Symbols.pin), ) @@ -151,25 +171,26 @@ class _FidoLockedPage extends ConsumerWidget { return AppPage( title: l10n.s_fingerprints, - capabilities: const [Capability.fido2], + capabilities: capabilities, keyActionsBuilder: hasActions ? _buildActions : null, builder: (context, _) => Column( children: [ - PinEntryForm(state, node), + PinEntryForm(state, deviceData.node), ], ), ); } Widget _buildActions(BuildContext context) => - fingerprintsBuildActions(context, node, state, -1); + fingerprintsBuildActions(context, deviceData.node, state, -1); } class _FidoUnlockedPage extends ConsumerStatefulWidget { - final DeviceNode node; + final YubiKeyData deviceData; final FidoState state; - _FidoUnlockedPage(this.node, this.state) : super(key: ObjectKey(node.path)); + _FidoUnlockedPage(this.deviceData, this.state) + : super(key: ObjectKey(deviceData.node.path)); @override ConsumerState createState() => @@ -184,10 +205,12 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { final l10n = AppLocalizations.of(context)!; final hasFeature = ref.watch(featureProvider); final hasActions = hasFeature(features.actions); + final capabilities = _getCapabilities(widget.deviceData); - final data = ref.watch(fingerprintProvider(widget.node.path)).asData; + final data = + ref.watch(fingerprintProvider(widget.deviceData.node.path)).asData; if (data == null) { - return _buildLoadingPage(context); + return _buildLoadingPage(context, capabilities); } final fingerprints = data.value; if (fingerprints.isEmpty) { @@ -200,18 +223,18 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { await showBlurDialog( context: context, builder: (context) => - AddFingerprintDialog(widget.node.path)); + AddFingerprintDialog(widget.deviceData.node.path)); }, avatar: const Icon(Symbols.fingerprint), ) ], title: l10n.s_fingerprints, - capabilities: const [Capability.fido2], + capabilities: capabilities, header: l10n.s_fingerprints_get_started, message: l10n.l_add_one_or_more_fps, keyActionsBuilder: hasActions - ? (context) => - fingerprintsBuildActions(context, widget.node, widget.state, 0) + ? (context) => fingerprintsBuildActions( + context, widget.deviceData.node, widget.state, 0) : null, keyActionsBadge: fingerprintsShowActionsNotifier(widget.state), ); @@ -219,7 +242,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { final fingerprint = _selected; return FidoActions( - devicePath: widget.node.path, + devicePath: widget.deviceData.node.path, actions: (context) => { EscapeIntent: CallbackAction(onInvoke: (intent) { if (_selected != null) { @@ -266,7 +289,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { }, builder: (context) => AppPage( title: l10n.s_fingerprints, - capabilities: const [Capability.fido2], + capabilities: capabilities, detailViewBuilder: fingerprint != null ? (context) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -306,8 +329,8 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { ) : null, keyActionsBuilder: hasActions - ? (context) => fingerprintsBuildActions( - context, widget.node, widget.state, fingerprints.length) + ? (context) => fingerprintsBuildActions(context, + widget.deviceData.node, widget.state, fingerprints.length) : null, keyActionsBadge: fingerprintsShowActionsNotifier(widget.state), builder: (context, expanded) { @@ -331,24 +354,29 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { }), } }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: fingerprints - .map((fp) => _FingerprintListItem( - fp, - expanded: expanded, - selected: fp == _selected, - )) - .toList()), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: fingerprints + .map((fp) => _FingerprintListItem( + fp, + expanded: expanded, + selected: fp == _selected, + )) + .toList()), + ), ); }, ), ); } - Widget _buildLoadingPage(BuildContext context) => AppPage( + Widget _buildLoadingPage( + BuildContext context, List capabilities) => + AppPage( title: AppLocalizations.of(context)!.s_fingerprints, - capabilities: const [Capability.fido2], + capabilities: capabilities, centered: true, delayedContent: true, builder: (context, _) => const CircularProgressIndicator(), diff --git a/lib/fido/views/key_actions.dart b/lib/fido/views/key_actions.dart index 1263344e9..0d33427c9 100755 --- a/lib/fido/views/key_actions.dart +++ b/lib/fido/views/key_actions.dart @@ -25,6 +25,7 @@ import '../features.dart' as features; import '../keys.dart' as keys; import '../models.dart'; import 'add_fingerprint_dialog.dart'; +import 'enterprise_attestation_dialog.dart'; import 'pin_dialog.dart'; bool passkeysShowActionsNotifier(FidoState state) { @@ -50,6 +51,13 @@ Widget _fidoBuildActions(BuildContext context, DeviceNode node, FidoState state, Theme.of(context).colorScheme; final authBlocked = state.pinBlocked; + final enterpriseAttestation = state.enterpriseAttestation; + final showEnterpriseAttestation = + enterpriseAttestation != null && fingerprints == null; + final canEnableEnterpriseAttestation = enterpriseAttestation == false && + !(state.alwaysUv && !state.hasPin) && + !(!state.unlocked && state.hasPin); + return Column( children: [ if (fingerprints != null) @@ -115,8 +123,30 @@ Widget _fidoBuildActions(BuildContext context, DeviceNode node, FidoState state, } : null, ), + if (showEnterpriseAttestation) + ActionListItem( + key: keys.enableEnterpriseAttestation, + feature: features.enableEnterpriseAttestation, + icon: const Icon(Symbols.local_police), + title: l10n.s_ep_attestation, + subtitle: enterpriseAttestation + ? l10n.s_enabled + : (state.alwaysUv && !state.hasPin) + ? l10n.l_set_pin_first + : l10n.s_disabled, + onTap: canEnableEnterpriseAttestation + ? (context) { + Navigator.of(context).popUntil((route) => route.isFirst); + showBlurDialog( + context: context, + builder: (context) => + EnableEnterpriseAttestationDialog(node.path), + ); + } + : null, + ) ], - ) + ), ], ); } diff --git a/lib/fido/views/passkeys_screen.dart b/lib/fido/views/passkeys_screen.dart index 18b1b60b2..b1231088e 100644 --- a/lib/fido/views/passkeys_screen.dart +++ b/lib/fido/views/passkeys_screen.dart @@ -38,6 +38,7 @@ import '../../exception/no_data_exception.dart'; import '../../management/models.dart'; import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_form_field.dart'; +import '../../widgets/flex_box.dart'; import '../../widgets/list_title.dart'; import '../features.dart' as features; import '../models.dart'; @@ -219,6 +220,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { late FocusNode searchFocus; late TextEditingController searchController; FidoCredential? _selected; + bool _canRequestFocus = true; @override void initState() { @@ -236,7 +238,23 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { super.dispose(); } + void _scrollSearchField() { + // Ensures the search field is fully visible when in focus + final headerSliverContext = headerSliverGlobalKey.currentContext; + if (searchFocus.hasFocus && headerSliverContext != null) { + final scrollable = Scrollable.of(headerSliverContext); + if (scrollable.deltaToScrollOrigin.dy > 0) { + scrollable.position.animateTo( + 0, + duration: const Duration(milliseconds: 100), + curve: Curves.ease, + ); + } + } + } + void _onFocusChange() { + _scrollSearchField(); setState(() {}); } @@ -374,60 +392,159 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { } return KeyEventResult.ignored; }, - child: Builder(builder: (context) { + child: LayoutBuilder(builder: (context, constraints) { final textTheme = Theme.of(context).textTheme; - return Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: AppTextFormField( - key: searchField, - controller: searchController, - focusNode: searchFocus, - // Use the default style, but with a smaller font size: - style: textTheme.titleMedium - ?.copyWith(fontSize: textTheme.titleSmall?.fontSize), - decoration: AppInputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(48), - borderSide: BorderSide( - width: 0, - style: searchFocus.hasFocus - ? BorderStyle.solid - : BorderStyle.none, + final width = constraints.maxWidth; + return Consumer( + builder: (context, ref, child) { + final layout = ref.watch(passkeysLayoutProvider); + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10.0, vertical: 8.0), + child: AppTextFormField( + key: searchField, + controller: searchController, + canRequestFocus: _canRequestFocus, + focusNode: searchFocus, + // Use the default style, but with a smaller font size: + style: textTheme.titleMedium + ?.copyWith(fontSize: textTheme.titleSmall?.fontSize), + decoration: AppInputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(48), + borderSide: BorderSide( + width: 0, + style: searchFocus.hasFocus + ? BorderStyle.solid + : BorderStyle.none, + ), + ), + contentPadding: const EdgeInsets.all(16), + fillColor: Theme.of(context).hoverColor, + filled: true, + hintText: l10n.s_search_passkeys, + isDense: true, + prefixIcon: const Padding( + padding: EdgeInsetsDirectional.only(start: 8.0), + child: Icon(Icons.search_outlined), + ), + suffixIcons: [ + if (searchController.text.isNotEmpty) + IconButton( + icon: const Icon(Icons.clear), + iconSize: 16, + onPressed: () { + searchController.clear(); + ref + .read(passkeysSearchProvider.notifier) + .setFilter(''); + setState(() {}); + }, + ), + if (searchController.text.isEmpty) ...[ + if (width >= 450) + ...FlexLayout.values.map( + (e) => MouseRegion( + onEnter: (event) { + if (!searchFocus.hasFocus) { + setState(() { + _canRequestFocus = false; + }); + } + }, + onExit: (event) { + setState(() { + _canRequestFocus = true; + }); + }, + child: IconButton( + tooltip: e.getDisplayName(l10n), + onPressed: () { + ref + .read(passkeysLayoutProvider.notifier) + .setLayout(e); + }, + icon: Icon( + e.icon, + color: e == layout + ? Theme.of(context).colorScheme.primary + : null, + ), + ), + ), + ), + if (width < 450) + MouseRegion( + onEnter: (event) { + if (!searchFocus.hasFocus) { + setState(() { + _canRequestFocus = false; + }); + } + }, + onExit: (event) { + setState(() { + _canRequestFocus = true; + }); + }, + child: PopupMenuButton( + constraints: const BoxConstraints.tightFor(), + tooltip: l10n.s_select_layout, + popUpAnimationStyle: + AnimationStyle(duration: Duration.zero), + icon: Icon( + layout.icon, + color: Theme.of(context).colorScheme.primary, + ), + itemBuilder: (context) => [ + ...FlexLayout.values.map( + (e) => PopupMenuItem( + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Tooltip( + message: e.getDisplayName(l10n), + child: Icon( + e.icon, + color: e == layout + ? Theme.of(context) + .colorScheme + .primary + : null, + ), + ), + ], + ), + onTap: () { + ref + .read( + passkeysLayoutProvider.notifier) + .setLayout(e); + }, + ), + ) + ], + ), + ) + ] + ], ), - ), - contentPadding: const EdgeInsets.all(16), - fillColor: Theme.of(context).hoverColor, - filled: true, - hintText: l10n.s_search_passkeys, - isDense: true, - prefixIcon: const Padding( - padding: EdgeInsetsDirectional.only(start: 8.0), - child: Icon(Icons.search_outlined), - ), - suffixIcon: searchController.text.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear), - iconSize: 16, - onPressed: () { - searchController.clear(); - ref - .read(passkeysSearchProvider.notifier) - .setFilter(''); - setState(() {}); - }, - ) - : null, - ), - onChanged: (value) { - ref.read(passkeysSearchProvider.notifier).setFilter(value); - setState(() {}); - }, - textInputAction: TextInputAction.next, - onFieldSubmitted: (value) { - Focus.of(context).focusInDirection(TraversalDirection.down); - }, - ), + onChanged: (value) { + ref + .read(passkeysSearchProvider.notifier) + .setFilter(value); + _scrollSearchField(); + setState(() {}); + }, + textInputAction: TextInputAction.next, + onFieldSubmitted: (value) { + Focus.of(context) + .focusInDirection(TraversalDirection.down); + }, + ).init(), + ); + }, ); }), ), @@ -483,21 +600,38 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { }), } }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (filteredCredentials.isEmpty) - Center( - child: Text(l10n.s_no_passkeys), - ), - ...filteredCredentials.map( - (cred) => _CredentialListItem( - cred, - expanded: expanded, - selected: _selected == cred, + child: Consumer( + builder: (context, ref, child) { + final layout = ref.watch(passkeysLayoutProvider); + return Padding( + padding: + const EdgeInsets.only(left: 10.0, right: 10.0, top: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (filteredCredentials.isEmpty) + Center( + child: Text(l10n.s_no_passkeys), + ), + FlexBox( + items: filteredCredentials, + itemBuilder: (cred) => _CredentialListItem( + cred, + expanded: expanded, + selected: _selected == cred, + tileColor: layout == FlexLayout.grid + ? Theme.of(context).hoverColor + : null, + ), + layout: layout, + cellMinWidth: 265, + spacing: layout == FlexLayout.grid ? 4.0 : 0.0, + runSpacing: layout == FlexLayout.grid ? 4.0 : 0.0, + ) + ], ), - ), - ], + ); + }, ), ); }, @@ -518,9 +652,10 @@ class _CredentialListItem extends StatelessWidget { final FidoCredential credential; final bool selected; final bool expanded; + final Color? tileColor; const _CredentialListItem(this.credential, - {required this.expanded, required this.selected}); + {required this.expanded, required this.selected, this.tileColor}); @override Widget build(BuildContext context) { @@ -533,6 +668,7 @@ class _CredentialListItem extends StatelessWidget { backgroundColor: colorScheme.secondary, child: const Icon(Symbols.passkey), ), + tileColor: tileColor, title: credential.rpId, subtitle: credential.userName, trailing: expanded diff --git a/lib/fido/views/pin_dialog.dart b/lib/fido/views/pin_dialog.dart index 916087f2e..bb1db2688 100755 --- a/lib/fido/views/pin_dialog.dart +++ b/lib/fido/views/pin_dialog.dart @@ -26,9 +26,11 @@ import '../../app/models.dart'; import '../../app/state.dart'; import '../../desktop/models.dart'; import '../../exception/cancellation_exception.dart'; +import '../../management/models.dart'; import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_form_field.dart'; import '../../widgets/responsive_dialog.dart'; +import '../../widgets/utf8_utils.dart'; import '../keys.dart'; import '../models.dart'; import '../state.dart'; @@ -50,6 +52,7 @@ class _FidoPinDialogState extends ConsumerState { final _currentPinFocus = FocusNode(); final _newPinController = TextEditingController(); final _newPinFocus = FocusNode(); + final _confirmPinFocus = FocusNode(); String _confirmPin = ''; String? _currentPinError; String? _newPinError; @@ -66,6 +69,7 @@ class _FidoPinDialogState extends ConsumerState { _currentPinFocus.dispose(); _newPinController.dispose(); _newPinFocus.dispose(); + _confirmPinFocus.dispose(); super.dispose(); } @@ -84,12 +88,22 @@ class _FidoPinDialogState extends ConsumerState { final isValid = currentPinLenOk && newPinLenOk && _newPinController.text == _confirmPin; - final hasPinComplexity = - ref.read(currentDeviceDataProvider).valueOrNull?.info.pinComplexity ?? - false; + final newPinEnabled = !_isBlocked && currentPinLenOk; + final confirmPinEnabled = !_isBlocked && currentPinLenOk && newPinLenOk; + + final deviceData = ref.read(currentDeviceDataProvider).valueOrNull; + + final hasPinComplexity = deviceData?.info.pinComplexity ?? false; final pinRetries = ref.watch(fidoStateProvider(widget.devicePath) .select((s) => s.whenOrNull(data: (state) => state.pinRetries))); + final isBio = widget.state.bioEnroll != null; + final enabled = deviceData + ?.info.config.enabledCapabilities[deviceData.node.transport] ?? + 0; + final maxPinLength = + isBio && (enabled & Capability.piv.value) != 0 ? 8 : 63; + return ResponsiveDialog( title: Text(hasPin ? l10n.s_change_pin : l10n.s_set_pin), actions: [ @@ -110,6 +124,9 @@ class _FidoPinDialogState extends ConsumerState { key: currentPin, controller: _currentPinController, focusNode: _currentPinFocus, + maxLength: maxPinLength, + inputFormatters: [limitBytesLength(maxPinLength)], + buildCounter: buildByteCounterFor(_currentPinController.text), autofocus: true, obscureText: _isObscureCurrent, autofillHints: const [AutofillHints.password], @@ -136,72 +153,100 @@ class _FidoPinDialogState extends ConsumerState { _isObscureCurrent ? l10n.s_show_pin : l10n.s_hide_pin, ), ), + textInputAction: TextInputAction.next, onChanged: (value) { setState(() { _currentIsWrong = false; }); }, + onFieldSubmitted: (_) { + if (_currentPinController.text.length < minPinLength) { + _currentPinFocus.requestFocus(); + } else { + _newPinFocus.requestFocus(); + } + }, ).init(), ], Text(hasPinComplexity ? l10n.p_enter_new_fido2_pin_complexity_active( - minPinLength, 2, '123456') - : l10n.p_enter_new_fido2_pin(minPinLength)), - // TODO: Set max characters based on UTF-8 bytes + minPinLength, maxPinLength, 2, '123456') + : l10n.p_enter_new_fido2_pin(minPinLength, maxPinLength)), AppTextFormField( key: newPin, controller: _newPinController, focusNode: _newPinFocus, + maxLength: maxPinLength, + inputFormatters: [limitBytesLength(maxPinLength)], + buildCounter: buildByteCounterFor(_newPinController.text), autofocus: !hasPin, obscureText: _isObscureNew, autofillHints: const [AutofillHints.password], decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_new_pin, - enabled: !_isBlocked && currentPinLenOk, + enabled: newPinEnabled, errorText: _newIsWrong ? _newPinError : null, errorMaxLines: 3, prefixIcon: const Icon(Symbols.pin), - suffixIcon: IconButton( - icon: Icon(_isObscureNew - ? Symbols.visibility - : Symbols.visibility_off), - onPressed: () { - setState(() { - _isObscureNew = !_isObscureNew; - }); - }, - tooltip: _isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin, + suffixIcon: ExcludeFocusTraversal( + excluding: !newPinEnabled, + child: IconButton( + icon: Icon(_isObscureNew + ? Symbols.visibility + : Symbols.visibility_off), + onPressed: () { + setState(() { + _isObscureNew = !_isObscureNew; + }); + }, + tooltip: _isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin, + ), ), ), + textInputAction: TextInputAction.next, onChanged: (value) { setState(() { _newIsWrong = false; }); }, + onFieldSubmitted: (_) { + if (_newPinController.text.length < minPinLength) { + _newPinFocus.requestFocus(); + } else { + _confirmPinFocus.requestFocus(); + } + }, ).init(), AppTextFormField( key: confirmPin, initialValue: _confirmPin, + focusNode: _confirmPinFocus, + maxLength: maxPinLength, + inputFormatters: [limitBytesLength(maxPinLength)], + buildCounter: buildByteCounterFor(_confirmPin), obscureText: _isObscureConfirm, autofillHints: const [AutofillHints.password], decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_confirm_pin, prefixIcon: const Icon(Symbols.pin), - suffixIcon: IconButton( - icon: Icon(_isObscureConfirm - ? Symbols.visibility - : Symbols.visibility_off), - onPressed: () { - setState(() { - _isObscureConfirm = !_isObscureConfirm; - }); - }, - tooltip: - _isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin, + suffixIcon: ExcludeFocusTraversal( + excluding: !confirmPinEnabled, + child: IconButton( + icon: Icon(_isObscureConfirm + ? Symbols.visibility + : Symbols.visibility_off), + onPressed: () { + setState(() { + _isObscureConfirm = !_isObscureConfirm; + }); + }, + tooltip: + _isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin, + ), ), - enabled: !_isBlocked && currentPinLenOk && newPinLenOk, + enabled: confirmPinEnabled, errorText: _newPinController.text.length == _confirmPin.length && _newPinController.text != _confirmPin @@ -209,6 +254,7 @@ class _FidoPinDialogState extends ConsumerState { : null, helperText: '', // Prevents resizing when errorText shown ), + textInputAction: TextInputAction.done, onChanged: (value) { setState(() { _confirmPin = value; @@ -217,6 +263,8 @@ class _FidoPinDialogState extends ConsumerState { onFieldSubmitted: (_) { if (isValid) { _submit(); + } else { + _confirmPinFocus.requestFocus(); } }, ).init(), diff --git a/lib/fido/views/rename_fingerprint_dialog.dart b/lib/fido/views/rename_fingerprint_dialog.dart index 2c6c783c1..c1ed6754f 100755 --- a/lib/fido/views/rename_fingerprint_dialog.dart +++ b/lib/fido/views/rename_fingerprint_dialog.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2023 Yubico. + * Copyright (C) 2022-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,12 +41,20 @@ class RenameFingerprintDialog extends ConsumerStatefulWidget { class _RenameAccountDialogState extends ConsumerState { late String _label; + late FocusNode _labelFocus; _RenameAccountDialogState(); @override void initState() { super.initState(); _label = widget.fingerprint.name ?? ''; + _labelFocus = FocusNode(); + } + + @override + void dispose() { + _labelFocus.dispose(); + super.dispose(); } _submit() async { @@ -93,7 +101,9 @@ class _RenameAccountDialogState extends ConsumerState { Text(l10n.q_rename_target(widget.fingerprint.label)), Text(l10n.p_will_change_label_fp), AppTextFormField( + autofocus: true, initialValue: _label, + focusNode: _labelFocus, maxLength: 15, inputFormatters: [limitBytesLength(15)], buildCounter: buildByteCounterFor(_label), @@ -110,6 +120,8 @@ class _RenameAccountDialogState extends ConsumerState { onFieldSubmitted: (_) { if (_label.isNotEmpty) { _submit(); + } else { + _labelFocus.requestFocus(); } }, ).init(), diff --git a/lib/home/views/home_screen.dart b/lib/home/views/home_screen.dart index a2526eac2..08c82dd8d 100644 --- a/lib/home/views/home_screen.dart +++ b/lib/home/views/home_screen.dart @@ -29,7 +29,6 @@ import '../../app/views/app_page.dart'; import '../../core/models.dart'; import '../../core/state.dart'; import '../../management/models.dart'; -import '../../widgets/choice_filter_chip.dart'; import '../../widgets/product_image.dart'; import 'key_actions.dart'; import 'manage_label_dialog.dart'; @@ -72,7 +71,7 @@ class _HomeScreenState extends ConsumerState { homeBuildActions(context, widget.deviceData, ref), builder: (context, expanded) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 18.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -92,16 +91,14 @@ class _HomeScreenState extends ConsumerState { runSpacing: 8, children: Capability.values .where((c) => enabledCapabilities & c.value != 0) - .map((c) => CapabilityBadge(c)) + .map((c) => CapabilityBadge(c, noTooltip: true)) .toList(), ), - if (serial != null) ...[ - const SizedBox(height: 32.0), - _DeviceColor( - deviceData: widget.deviceData, - initialCustomization: keyCustomization ?? - KeyCustomization(serial: serial)) - ] + if (widget.deviceData.info.fipsCapable != 0) + Padding( + padding: const EdgeInsets.only(top: 38), + child: _FipsLegend(), + ), ], ), ), @@ -131,6 +128,62 @@ class _HomeScreenState extends ConsumerState { } } +class _FipsLegend extends StatelessWidget { + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Opacity( + opacity: 0.6, + child: Wrap( + runSpacing: 0, + spacing: 16, + children: [ + RichText( + text: TextSpan( + children: [ + const WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: EdgeInsets.only(right: 4), + child: Icon( + Symbols.shield, + size: 12, + fill: 0.0, + ), + ), + ), + TextSpan( + text: l10n.l_fips_capable, + style: Theme.of(context).textTheme.bodySmall), + ], + ), + ), + RichText( + text: TextSpan( + children: [ + const WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: EdgeInsets.only(right: 4), + child: Icon( + Symbols.shield, + size: 12, + fill: 1.0, + ), + ), + ), + TextSpan( + text: l10n.l_fips_approved, + style: Theme.of(context).textTheme.bodySmall), + ], + ), + ), + ], + ), + ); + } +} + class _DeviceContent extends ConsumerWidget { final YubiKeyData deviceData; final KeyCustomization? initialCustomization; @@ -147,6 +200,9 @@ class _DeviceContent extends ConsumerWidget { final label = initialCustomization?.name; String displayName = label != null ? '$label ($name)' : name; + final defaultColor = ref.watch(defaultColorProvider); + final customColor = initialCustomization?.color; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -158,22 +214,114 @@ class _DeviceContent extends ConsumerWidget { style: Theme.of(context).textTheme.titleLarge, ), ), - if (serial != null) - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: IconButton( - icon: const Icon(Symbols.edit), - onPressed: () async { - await ref.read(withContextProvider)((context) async { - await _showManageLabelDialog( - initialCustomization ?? - KeyCustomization(serial: serial), - context, - ); - }); - }, - ), + if (serial != null) ...[ + const SizedBox(width: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconButton( + icon: const Icon(Symbols.edit), + tooltip: l10n.s_set_label, + color: Theme.of(context).colorScheme.onSurfaceVariant, + onPressed: () async { + await ref.read(withContextProvider)((context) async { + await _showManageLabelDialog( + initialCustomization ?? + KeyCustomization(serial: serial), + context, + ); + }); + }, + ), + Column( + children: [ + PopupMenuButton( + popUpAnimationStyle: + AnimationStyle(duration: Duration.zero), + menuPadding: EdgeInsets.zero, + tooltip: l10n.s_set_color, + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Center( + child: Wrap( + runSpacing: 8, + spacing: 16, + children: [ + ...[ + Colors.teal, + Colors.cyan, + Colors.blueAccent, + Colors.deepPurple, + Colors.red, + Colors.orange, + Colors.yellow, + // add nice color to devices with dynamic color + if (isAndroid && + ref.read(androidSdkVersionProvider) >= + 31) + Colors.lightGreen + ].map((e) => _ColorButton( + color: e, + isSelected: + customColor?.value == e.value, + onPressed: () { + _updateColor(e, ref, serial); + Navigator.of(context).pop(); + }, + )), + + // "use default color" button + RawMaterialButton( + onPressed: () { + _updateColor(null, ref, serial); + Navigator.of(context).pop(); + }, + constraints: const BoxConstraints( + minWidth: 26.0, minHeight: 26.0), + fillColor: defaultColor, + hoverColor: Colors.black12, + shape: const CircleBorder(), + child: Icon( + customColor == null + ? Symbols.circle + : Symbols.clear, + fill: 1, + size: 16, + weight: 700, + opticalSize: 20, + color: defaultColor + .computeLuminance() > + 0.7 + ? Colors.grey // for bright colors + : Colors.white), + ), + ], + ), + ), + ) + ]; + }, + icon: Icon( + Symbols.palette, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + Container( + height: 3.0, + width: 24.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.0), + color: Theme.of(context) + .colorScheme + .primary + .withOpacity(0.9)), + ) + ], + ), + ], ) + ] ], ), const SizedBox( @@ -195,10 +343,42 @@ class _DeviceContent extends ConsumerWidget { .titleSmall ?.apply(color: Theme.of(context).colorScheme.onSurfaceVariant), ), + if (deviceData.info.pinComplexity) + Padding( + padding: const EdgeInsets.only(top: 12), + child: RichText( + text: TextSpan(children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: const EdgeInsets.only(right: 4), + child: Icon( + Symbols.check, + size: 16, + color: Theme.of(context).colorScheme.primary, + ), + )), + TextSpan( + text: l10n.l_pin_complexity, + style: Theme.of(context).textTheme.titleSmall?.apply( + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ]), + ), + ), ], ); } + void _updateColor(Color? color, WidgetRef ref, int serial) async { + final manager = ref.read(keyCustomizationManagerProvider.notifier); + await manager.set( + serial: serial, + name: initialCustomization?.name, + color: color, + ); + } + Future _showManageLabelDialog( KeyCustomization keyCustomization, BuildContext context) async { await showBlurDialog( @@ -210,97 +390,6 @@ class _DeviceContent extends ConsumerWidget { } } -class _DeviceColor extends ConsumerWidget { - final YubiKeyData deviceData; - final KeyCustomization initialCustomization; - const _DeviceColor( - {required this.deviceData, required this.initialCustomization}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final l10n = AppLocalizations.of(context)!; - final defaultColor = ref.watch(defaultColorProvider); - final customColor = initialCustomization.color; - - return ChoiceFilterChip( - disableHover: true, - value: customColor, - items: const [null], - selected: customColor != null && customColor.value != defaultColor.value, - itemBuilder: (e) => Wrap( - alignment: WrapAlignment.center, - runSpacing: 8, - spacing: 16, - children: [ - ...[ - Colors.teal, - Colors.cyan, - Colors.blueAccent, - Colors.deepPurple, - Colors.red, - Colors.orange, - Colors.yellow, - // add nice color to devices with dynamic color - if (isAndroid && ref.read(androidSdkVersionProvider) >= 31) - Colors.lightGreen - ].map((e) => _ColorButton( - color: e, - isSelected: customColor?.value == e.value, - onPressed: () { - _updateColor(e, ref); - Navigator.of(context).pop(); - }, - )), - - // "use default color" button - RawMaterialButton( - onPressed: () { - _updateColor(null, ref); - Navigator.of(context).pop(); - }, - constraints: const BoxConstraints(minWidth: 26.0, minHeight: 26.0), - fillColor: defaultColor, - hoverColor: Colors.black12, - shape: const CircleBorder(), - child: Icon(customColor == null ? Symbols.circle : Symbols.clear, - fill: 1, - size: 16, - weight: 700, - opticalSize: 20, - color: defaultColor.computeLuminance() > 0.7 - ? Colors.grey // for bright colors - : Colors.white), - ), - ], - ), - labelBuilder: (e) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - constraints: const BoxConstraints(minWidth: 22.0, minHeight: 22.0), - decoration: BoxDecoration( - color: customColor ?? defaultColor, shape: BoxShape.circle), - ), - const SizedBox( - width: 12, - ), - Flexible(child: Text(l10n.s_color)) - ], - ), - onChanged: (e) {}, - ); - } - - void _updateColor(Color? color, WidgetRef ref) async { - final manager = ref.read(keyCustomizationManagerProvider.notifier); - await manager.set( - serial: initialCustomization.serial, - name: initialCustomization.name, - color: color, - ); - } -} - class _ColorButton extends StatefulWidget { final Color? color; final bool isSelected; @@ -358,13 +447,7 @@ class _HeroAvatar extends StatelessWidget { ), ), 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, - ), + child: child, ); } } diff --git a/lib/home/views/key_actions.dart b/lib/home/views/key_actions.dart index 2c130ee07..de19cf560 100644 --- a/lib/home/views/key_actions.dart +++ b/lib/home/views/key_actions.dart @@ -25,6 +25,7 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/shortcuts.dart'; import '../../app/views/action_list.dart'; +import '../../app/views/keys.dart'; import '../../app/views/reset_dialog.dart'; import '../../core/models.dart'; import '../../core/state.dart'; @@ -34,6 +35,7 @@ Widget homeBuildActions( BuildContext context, YubiKeyData? deviceData, WidgetRef ref) { final l10n = AppLocalizations.of(context)!; final hasFeature = ref.watch(featureProvider); + final interfacesLocked = deviceData?.info.resetBlocked != 0; final managementAvailability = hasFeature(features.management) && switch (deviceData?.info.version) { Version version => (version.major > 4 || // YK5 and up @@ -56,16 +58,22 @@ Widget homeBuildActions( title: deviceData.info.version.major > 4 ? l10n.s_toggle_applications : l10n.s_toggle_interfaces, - subtitle: deviceData.info.version.major > 4 - ? l10n.l_toggle_applications_desc - : l10n.l_toggle_interfaces_desc, - onTap: (context) { - Navigator.of(context).popUntil((route) => route.isFirst); - showBlurDialog( - context: context, - builder: (context) => ManagementScreen(deviceData), - ); - }, + key: yubikeyApplicationToggleMenuButton, + subtitle: interfacesLocked + ? l10n.l_factory_reset_required + : (deviceData.info.version.major > 4 + ? l10n.l_toggle_applications_desc + : l10n.l_toggle_interfaces_desc), + onTap: interfacesLocked + ? null + : (context) { + Navigator.of(context) + .popUntil((route) => route.isFirst); + showBlurDialog( + context: context, + builder: (context) => ManagementScreen(deviceData), + ); + }, ), if (getResetCapabilities(hasFeature).any((c) => c.value & @@ -76,6 +84,7 @@ Widget homeBuildActions( ActionListItem( icon: const Icon(Symbols.delete_forever), title: l10n.s_factory_reset, + key: yubikeyFactoryResetMenuButton, subtitle: l10n.l_factory_reset_desc, actionStyle: ActionStyle.primary, onTap: (context) { @@ -91,6 +100,7 @@ Widget homeBuildActions( ActionListSection(l10n.s_application, children: [ ActionListItem( icon: const Icon(Symbols.settings), + key: settingDrawerIcon, title: l10n.s_settings, subtitle: l10n.l_settings_desc, actionStyle: ActionStyle.primary, @@ -101,6 +111,7 @@ Widget homeBuildActions( ), ActionListItem( icon: const Icon(Symbols.help), + key: helpDrawerIcon, title: l10n.s_help_and_about, subtitle: l10n.l_help_and_about_desc, actionStyle: ActionStyle.primary, diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index b7f01a6bd..0441f018b 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -16,8 +16,8 @@ }, "@_lint_rules": { - "p_ending_chars": null, - "q_ending_chars": null, + "p_ending_chars": ".!", + "q_ending_chars": "?", "s_max_words": 4, "s_max_length": 32 }, @@ -28,75 +28,81 @@ "s_cancel": "Abbrechen", "s_close": "Schließen", "s_delete": "Löschen", - "s_move": null, + "s_move": "Verschieben", "s_quit": "Beenden", - "s_status": null, + "s_enable": "Aktivieren", + "s_enabled": "Aktiviert", + "s_disabled": "Deaktiviert", + "s_status": "Status", "s_unlock": "Entsperren", "s_calculate": "Berechnen", - "s_import": null, - "s_overwrite": null, + "s_import": "Importieren", + "s_overwrite": "Überschreiben", + "s_done": null, "s_label": "Beschriftung", "s_name": "Name", "s_usb": "USB", "s_nfc": "NFC", - "s_options": null, - "s_details": null, + "s_options": "Optionen", + "s_details": "Details", "s_show_window": "Fenster anzeigen", "s_hide_window": "Fenster verstecken", - "s_expand_navigation": null, - "s_collapse_navigation": null, + "s_show_navigation": null, + "s_expand_navigation": "Navigation erweitern", + "s_collapse_navigation": "Navigation einklappen", + "s_show_menu": null, + "s_hide_menu": null, "q_rename_target": "{label} umbenennen?", "@q_rename_target": { "placeholders": { "label": {} } }, - "l_bullet": null, + "l_bullet": "• {item}", "@l_bullet": { "placeholders": { "item": {} } }, - "s_none": null, + "s_none": "", "s_about": "Über", - "s_algorithm": null, + "s_algorithm": "Algorithmus", "s_appearance": "Aussehen", - "s_actions": null, + "s_actions": "Aktionen", "s_manage": "Verwalten", "s_setup": "Einrichten", - "s_device": null, - "s_application": null, + "s_device": "Gerät", + "s_application": "Anwendung", "s_settings": "Einstellungen", - "l_settings_desc": null, - "s_certificates": null, - "s_security_key": null, - "s_slots": null, + "l_settings_desc": "Anwendungs-Einstellungen ändern", + "s_certificates": "Zertifikate", + "s_security_key": "Sicherheitsschlüssel", + "s_slots": "Slots", "s_help_and_about": "Hilfe und Über", - "l_help_and_about_desc": null, + "l_help_and_about_desc": "Problembehebung und Support", "s_help_and_feedback": "Hilfe und Feedback", - "s_home": null, - "s_user_guide": null, + "s_home": "Start", + "s_user_guide": "Nutzeranleitung", "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_show_secret_key": null, - "s_hide_secret_key": null, - "s_private_key": null, - "s_public_key": null, + "s_show_secret_key": "Geheimen Schlüssel anzeigen", + "s_hide_secret_key": "Geheimen Schlüssel verstecken", + "s_private_key": "Privater Schlüssel", + "s_public_key": "Öffentlicher Schlüssel", "s_invalid_length": "Ungültige Länge", - "l_invalid_format_allowed_chars": null, + "l_invalid_format_allowed_chars": "Ungültiges Format, erlaubte Zeichen: {characters}", "@l_invalid_format_allowed_chars": { "placeholders": { "characters": {} } }, - "l_invalid_keyboard_character": null, + "l_invalid_keyboard_character": "Ungültige Zeichen für ausgewählte Tastatur", "s_require_touch": "Berührung ist erforderlich", "q_have_account_info": "Haben Sie Konto-Informationen?", "s_run_diagnostics": "Diagnose ausführen", @@ -121,6 +127,12 @@ "s_light_mode": "Heller Modus", "s_dark_mode": "Dunkler Modus", + "@_layout": {}, + "s_list_layout": "Listen-Ansicht", + "s_grid_layout": "Kachel-Ansicht", + "s_mixed_layout": "Gemischte Ansicht", + "s_select_layout": "Ansicht wählen", + "@_yubikey_selection": {}, "s_select_to_scan": "Zum Scannen auswählen", "s_hide_device": "Gerät verstecken", @@ -142,13 +154,15 @@ "serial": {} } }, - "l_serial_number": null, + "l_serial_number": "Seriennummer: {serial}", "@l_firmware_version": { "placeholders": { "version": {} } }, - "l_firmware_version": null, + "l_firmware_version": "Firmware-Version: {version}", + "l_fips_capable": "FIPS fähig", + "l_fips_approved": "FIPS freigegeben", "@_yubikey_interactions": {}, "l_insert_yk": "YubiKey anschließen", @@ -164,21 +178,21 @@ "l_keep_touching_yk": "Berühren Sie Ihren YubiKey wiederholt\u2026", "@_capabilities": {}, - "s_capability_otp": null, - "s_capability_u2f": null, - "s_capability_fido2": null, - "s_capability_oath": null, - "s_capability_piv": null, - "s_capability_openpgp": null, - "s_capability_hsmauth": null, + "s_capability_otp": "Yubico OTP", + "s_capability_u2f": "FIDO U2F", + "s_capability_fido2": "FIDO2", + "s_capability_oath": "OATH", + "s_capability_piv": "PIV", + "s_capability_openpgp": "OpenPGP", + "s_capability_hsmauth": "YubiHSM Auth", "@_app_configuration": {}, "s_toggle_applications": "Anwendungen umschalten", - "s_toggle_interfaces": null, - "p_toggle_applications_desc": null, - "p_toggle_interfaces_desc": null, - "l_toggle_applications_desc": null, - "l_toggle_interfaces_desc": null, + "s_toggle_interfaces": "Schnittstellen umschalten", + "p_toggle_applications_desc": "Anwendungen über verfügbare Transports aktivieren/deaktivieren.", + "p_toggle_interfaces_desc": "USB-Schnittstellen aktivieren/deaktivieren.", + "l_toggle_applications_desc": "Anwendungen aktivieren/deaktivieren", + "l_toggle_interfaces_desc": "Schnittstellen aktivieren/deaktivieren", "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", @@ -198,11 +212,11 @@ }, "s_fido_disabled": "FIDO2 deaktiviert", "l_webauthn_req_fido2": "WebAuthn erfordert, dass die FIDO2 Anwendung auf Ihrem YubiKey aktiviert ist", - "s_lock_code": null, - "l_wrong_lock_code": null, - "s_show_lock_code": null, - "s_hide_lock_code": null, - "p_lock_code_required_desc": null, + "s_lock_code": "Sperr-Code", + "l_wrong_lock_code": "Falscher Sperr-Code", + "s_show_lock_code": "Sperr-Code anzeigen", + "s_hide_lock_code": "Sperr-Code verstecken", + "p_lock_code_required_desc": "Die ausgeführte Aktion erfordert die Eingabe des Konfigurations-Sperr-Codes.", "@_connectivity_issues": {}, @@ -216,9 +230,15 @@ "l_no_yk_present": "Kein YubiKey vorhanden", "s_unknown_type": "Unbekannter Typ", "s_unknown_device": "Unbekanntes Gerät", + "s_restricted_nfc": null, + "l_deactivate_restricted_nfc": null, + "p_deactivate_restricted_nfc_desc": null, + "p_deactivate_restricted_nfc_footer": null, "s_unsupported_yk": "Nicht unterstützter YubiKey", "s_yk_not_recognized": "Geräte nicht erkannt", - "p_operation_failed_try_again": null, + "p_operation_failed_try_again": "Die Aktion ist fehlgeschlagen, bitte versuchen Sie es erneut.", + "l_configuration_unsupported": null, + "p_scp_unsupported": null, "@_general_errors": {}, "l_error_occurred": "Es ist ein Fehler aufgetreten", @@ -230,32 +250,32 @@ "@_pins": {}, "s_pin": "PIN", - "s_puk": null, + "s_puk": "PUK", "s_set_pin": "PIN setzen", "s_change_pin": "PIN ändern", - "s_change_puk": null, - "s_show_pin": null, - "s_hide_pin": null, - "s_show_puk": null, - "s_hide_puk": null, + "s_change_puk": "PUK ändern", + "s_show_pin": "PIN anzeigen", + "s_hide_pin": "PIN verstecken", + "s_show_puk": "PUK anzeigen", + "s_hide_puk": "PUK verstecken", "s_current_pin": "Derzeitige PIN", - "s_current_puk": null, + "s_current_puk": "Derzeitiger PUK", "s_new_pin": "Neue PIN", - "s_new_puk": null, + "s_new_puk": "Neuer PUK", "s_confirm_pin": "PIN bestätigen", - "s_confirm_puk": null, - "s_unblock_pin": null, - "l_pin_mismatch": null, - "l_puk_mismatch": null, + "s_confirm_puk": "PUK bestätigen", + "s_unblock_pin": "PIN entsperren", + "l_pin_mismatch": "PINs stimmen nicht überein", + "l_puk_mismatch": "PUKs stimmen nicht überein", "s_pin_set": "PIN gesetzt", - "s_puk_set": null, + "s_puk_set": "PUK gesetzt", "l_set_pin_failed": "PIN konnte nicht gesetzt werden: {message}", "@l_set_pin_failed": { "placeholders": { "message": {} } }, - "l_attempts_remaining": null, + "l_attempts_remaining": "{retries} Versuch(e) verbleibend", "@l_attempts_remaining": { "placeholders": { "retries": {} @@ -267,78 +287,89 @@ "retries": {} } }, - "l_wrong_puk_attempts_remaining": null, + "l_wrong_puk_attempts_remaining": "Falscher PUK, {retries} Versuch(e) verbleibend", "@l_wrong_puk_attempts_remaining": { "placeholders": { "retries": {} } }, "s_fido_pin_protection": "FIDO PIN Schutz", - "s_pin_change_required": null, + "s_pin_change_required": "PIN-Änderung erforderlich", "l_enter_fido2_pin": "Geben Sie die FIDO2 PIN für Ihren YubiKey ein", "l_pin_blocked_reset": "PIN ist blockiert; setzen Sie die FIDO Anwendung auf Werkseinstellung zurück", - "l_pin_blocked": null, + "l_pin_blocked": "PIN ist blockiert", "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", - "l_pin_change_required_desc": null, - "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_current_pin_or_reset_no_puk": null, - "p_enter_current_puk_or_reset": null, - "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.", + "l_pin_change_required_desc": "Bevor Sie die Anwendung verwenden können, muss ein neuer PIN gesetzt werden", + "p_enter_current_pin_or_reset": "Geben Sie Ihre aktuelle PIN ein. Wenn Sie die PIN nicht wissen, müssen Sie diese mit dem PUK entsperren oder den YubiKey zurücksetzen.", + "p_enter_current_pin_or_reset_no_puk": "Geben Sie Ihre aktuelle PIN ein. Wenn Sie die PIN nicht wissen, müssen Sie den YubiKey zurücksetzen.", + "p_enter_current_puk_or_reset": "Geben Sie Ihren aktuellen PUK ein. Wenn Sie den PUK nicht wissen, müssen Sie den YubiKey zurücksetzen.", + "p_enter_new_fido2_pin": "Geben Sie Ihre neue PIN ein. Eine PIN muss {min_length}-{max_length} Zeichen enthalten und kann Buchstaben, Ziffern und Sonderzeichen enthalten.", "@p_enter_new_fido2_pin": { "placeholders": { - "length": {} + "min_length": {}, + "max_length": {} } }, - "p_enter_new_fido2_pin_complexity_active": null, + "p_enter_new_fido2_pin_complexity_active": "Geben Sie Ihre neue PIN ein. Eine PIN muss {min_length}-{max_length} Zeichen enthalten, muss mindestens {unique_characters} eindeutige Zeichen enthalten und keine gewöhnliche PIN wie \"{common_pin}\" sein. Sie kann Buchstaben, Ziffern und Sonderzeichen enthalten.", "@p_enter_new_fido2_pin_complexity_active": { "placeholders": { - "length": {}, + "min_length": {}, + "max_length": {}, "unique_characters": {}, "common_pin": {} } }, - "s_pin_required": null, - "p_pin_required_desc": null, - "l_piv_pin_blocked": null, - "l_piv_pin_puk_blocked": null, - "p_enter_new_piv_pin_puk": null, + "s_ep_attestation": "Unternehmens-Beglaubigung", + "s_ep_attestation_enabled": "Unternehmens-Beglaubigung aktiviert", + "s_enable_ep_attestation": "Unternehmens-Beglaubigung aktivieren", + "p_enable_ep_attestation_desc": "Dies aktiviert die Unternehmens-Beglaubigung, die es berechtigten Domains ermöglicht Ihren YubiKey eindeutig zu identifizieren.", + "p_enable_ep_attestation_disable_with_factory_reset": null, + "s_pin_required": "PIN erforderlich", + "p_pin_required_desc": "Die ausgeführte Aktion erfordert die Eingabe des PIV PINs.", + "l_piv_pin_blocked": "Gesperrt, verwenden Sie den PUK zum Zurücksetzen", + "l_piv_pin_puk_blocked": "Gesperrt, Rücksetzung auf Werkseinstellungen erforderlich", + "p_enter_new_piv_pin_puk": "Geben Sie einen neuen {name} zum Setzen ein. Dieser muss mindestens {length} Zeichen lang sein.", "@p_enter_new_piv_pin_puk": { "placeholders": { - "name": {} + "name": {}, + "length": {} } }, - "p_enter_new_piv_pin_puk_complexity_active": null, + "p_enter_new_piv_pin_puk_complexity_active": "Geben Sie einen neuen {name} zum Setzen ein. Dieser muss mindestens {length} Zeichen lang sein, muss mindestens 2 eindeutige Zeichen enthalten und darf kein gewöhnlicher {name} wie \"{common}\" sein.", "@p_enter_new_piv_pin_puk_complexity_active": { "placeholders": { "name": {}, + "length": {}, "common": {} } }, - "p_pin_puk_complexity_failure": null, + "p_pin_puk_complexity_failure": "Der neue {name} erfüllt die Komplexitätsanforderungen nicht.", "@p_pin_puk_complexity_failure": { "placeholders": { "name": {} } }, - "l_warning_default_pin": null, - "l_warning_default_puk": null, - "l_default_pin_used": null, - "l_default_puk_used": null, + "l_warning_default_pin": "Vorsicht: Standard-PIN verwendet", + "l_warning_default_puk": "Vorsicht: Standard-PUK verwendet", + "l_default_pin_used": "Standard-PIN verwendet", + "l_default_puk_used": "Standard-PUK verwendet", + "l_pin_complexity": "PIN-Komplexität erzwungen", "@_passwords": {}, "s_password": "Passwort", "s_manage_password": "Passwort verwalten", "s_set_password": "Passwort setzen", "s_password_set": "Passwort gesetzt", - "s_show_password": null, - "s_hide_password": null, + "s_show_password": "Passwort anzeigen", + "s_hide_password": "Passwort verstekcen", "l_optional_password_protection": "Optionaler Passwortschutz", + "l_password_protection": "Passwortschutz von Konten", "s_new_password": "Neues Passwort", "s_current_password": "Aktuelles Passwort", "s_confirm_password": "Passwort bestätigen", - "l_password_mismatch": null, + "l_password_mismatch": "Passwörter stimmen nicht überein", "s_wrong_password": "Falsches Passwort", "s_remove_password": "Passwort entfernen", "s_password_removed": "Passwort entfernt", @@ -348,26 +379,27 @@ "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_set_password_first": "Zuerst ein Passwort setzen", "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.", "@_management_key": {}, - "s_management_key": null, - "s_current_management_key": null, - "s_new_management_key": null, - "l_change_management_key": null, - "p_change_management_key_desc": null, - "l_management_key_changed": null, - "l_default_key_used": null, - "s_generate_random": null, - "s_use_default": null, - "l_warning_default_key": null, - "s_protect_key": null, - "l_pin_protected_key": null, - "l_wrong_key": null, - "l_unlock_piv_management": null, - "p_unlock_piv_management_desc": null, + "s_management_key": "Verwaltungs-Schlüssel", + "s_current_management_key": "Aktueller Verwaltung-Schlüssel", + "s_new_management_key": "Neuer Verwaltungs-Schlüssel", + "l_change_management_key": "Verwaltungs-Schlüssel ändern", + "p_change_management_key_desc": "Ändern Sie Ihren Verwaltungs-Schlüssel. Sie können alternativ auch die Verwendung der PIN anstatt des Verwaltungs-Schlüssels erlauben.", + "l_management_key_changed": "Verwaltungs-Schlüssel geändert", + "l_default_key_used": "Standard-Verwaltungs-Schlüssel verwendet", + "s_generate_random": "Zufälligen erzeugen", + "s_use_default": "Standard verwenden", + "l_warning_default_key": "Vorsicht: Standard-Schlüssel verwendet", + "s_protect_key": "Mit PIN schützen", + "l_pin_protected_key": "PIN kann stattdessen verwendet werden", + "l_wrong_key": "Falscher Schlüssel", + "l_unlock_piv_management": "PIV-Verwaltung entsperren", + "p_unlock_piv_management_desc": "Die ausgeführte Aktion erfordert den PIV-Verwaltungs-Schlüssel. Verwenden Sie diesen Schlüssel, um Verwaltungs-Funktionen für diese Sitzung zu entsperren.", "@_oath_accounts": {}, "l_account": "Konto: {label}", @@ -378,19 +410,19 @@ }, "s_accounts": "Konten", "s_no_accounts": "Keine Konten", - "l_results_for": null, + "l_results_for": "Ergebnisse für \"{query}\"", "@l_results_for": { "placeholders": { "query": {} } }, - "l_authenticator_get_started": null, - "l_no_accounts_desc": null, + "l_authenticator_get_started": "Beginnen Sie mit OTP-Konten", + "l_no_accounts_desc": "Fügen Sie Konten von Anbietern zu Ihrem YubiKey hinzu, die OATH TOTP/HOTP unterstützen", "s_add_account": "Konto hinzufügen", - "s_add_accounts": null, - "p_add_description": null, - "l_drop_qr_description": null, - "s_add_manually": null, + "s_add_accounts": "Konten hinzufügen", + "p_add_description": "Um einen QR-Code zu scannen, stellen Sie sicher, dass der komplette Code sichtbar ist und drücken Sie den Knopf unten. Sie können auch ein gespeichertes Bild aus einem Verzeichnis auf diesen Dialog ziehen. Wenn Sie die Anmeldeinformationen niedergeschrieben haben, verwenden Sie stattdessen die manuelle Eingabe.", + "l_drop_qr_description": "QR-Code hierherziehen, um Konten hinzuzufügen", + "s_add_manually": "Manuell hinzufügen", "s_account_added": "Konto hinzugefügt", "l_account_add_failed": "Fehler beim Hinzufügen des Kontos: {message}", "@l_account_add_failed": { @@ -398,21 +430,32 @@ "message": {} } }, + "l_add_account_password_required": null, + "l_add_account_unlock_required": null, + "l_add_account_already_exists": null, + "l_add_account_func_missing": null, "l_account_name_required": "Ihr Konto muss einen Namen haben", "l_name_already_exists": "Für diesen Aussteller existiert dieser Name bereits", - "l_account_already_exists": null, + "l_account_already_exists": "Dieses Konto existiert bereits auf dem YubiKey", "l_invalid_character_issuer": "Ungültiges Zeichen: ':' ist im Aussteller nicht erlaubt", - "l_select_accounts": null, + "l_select_accounts": "Konten zum Hinzufügen zum YubiKey auswählen", "s_pin_account": "Konto anpinnen", "s_unpin_account": "Konto nicht mehr anpinnen", "s_no_pinned_accounts": "Keine angepinnten Konten", - "l_pin_account_desc": null, + "s_pinned": "Angepinnt", + "l_pin_account_desc": "Behalten Sie Ihre wichtigen Konten an einem Ort", "s_rename_account": "Konto umbenennen", - "l_rename_account_desc": null, + "l_rename_account_desc": "Bearbeiten Sie den Aussteller/Namen des Kontos", "s_account_renamed": "Konto umbenannt", + "l_rename_account_failed": null, + "@l_rename_account_failed": { + "placeholders": { + "message": {} + } + }, "p_rename_will_change_account_displayed": "Das ändert die Anzeige dieses Kontos in der Liste.", "s_delete_account": "Konto löschen", - "l_delete_account_desc": null, + "l_delete_account_desc": "Löschen Sie das Konto von Ihrem YubiKey", "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.", @@ -440,33 +483,33 @@ "s_issuer_optional": "Aussteller (optional)", "s_counter_based": "Zähler-basiert", "s_time_based": "Zeit-basiert", - "l_copy_code_desc": null, - "l_calculate_code_desc": null, + "l_copy_code_desc": "Fügen Sie den Code in einer anderen Anwendung ein", + "l_calculate_code_desc": "Erhalten Sie einen neuen Code von Ihrem YubiKey", "@_fido_credentials": {}, - "s_rp_id": null, - "s_user_id": null, - "s_credential_id": null, - "s_display_name": null, - "s_user_name": null, - "l_passkey": null, + "s_rp_id": "RP ID", + "s_user_id": "Benutzer-ID", + "s_credential_id": "Anmeldeinformations-ID", + "s_display_name": "Anzeigename", + "s_user_name": "Benutzername", + "l_passkey": "Passkey: {label}", "@l_passkey": { "placeholders": { "label": {} } }, - "s_passkeys": null, - "s_no_passkeys": null, + "s_passkeys": "Passkeys", + "s_no_passkeys": "Keine Passkeys", "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", - "p_non_passkeys_note": null, - "s_delete_passkey": null, - "l_delete_passkey_desc": null, - "s_passkey_deleted": null, - "p_warning_delete_passkey": null, - "s_search_passkeys": null, - "p_passkeys_used": null, + "l_no_discoverable_accounts": "Keine Passkeys gespeichert", + "p_non_passkeys_note": "Andere Anmeldeinformationen außer Passkeys können existieren, können aber nicht angezeigt werden.", + "s_delete_passkey": "Passkey löschen", + "l_delete_passkey_desc": "Entfernen Sie den Passkey vom YubiKey", + "s_passkey_deleted": "Passkey gelöscht", + "p_warning_delete_passkey": "Das wird den Passkey von Ihrem YubiKey entfernen.", + "s_search_passkeys": "Passkeys durchsuchen", + "p_passkeys_used": "{used} von {max} Passkeys verwendet.", "@p_passkeys_used": { "placeholders": { "used": {}, @@ -474,6 +517,7 @@ } }, "@_fingerprints": {}, + "s_biometrics": null, "l_fingerprint": "Fingerabdruck: {label}", "@l_fingerprint": { "placeholders": { @@ -483,7 +527,7 @@ "s_fingerprints": "Fingerabdrücke", "l_fingerprint_captured": "Fingerabdruck erfolgreich aufgenommen!", "s_fingerprint_added": "Fingerabdruck hinzugefügt", - "l_adding_fingerprint_failed": null, + "l_adding_fingerprint_failed": "Fehler beim Hinzufügen des Fingerabdrucks: {message}", "@l_adding_fingerprint_failed": { "placeholders": {} }, @@ -493,18 +537,18 @@ "message": {} } }, - "s_setup_fingerprints": null, - "p_setup_fingerprints_desc": null, + "s_setup_fingerprints": "Fingerabdrücke einrichten", + "p_setup_fingerprints_desc": "Fingerabdrücken müssen eingerichtet werden, bevor der Schlüssel verwendet werden kann.", "s_add_fingerprint": "Fingerabdruck hinzufügen", "s_delete_fingerprint": "Fingerabdruck löschen", - "l_delete_fingerprint_desc": null, + "l_delete_fingerprint_desc": "Den Fingerabdruck vom YubKey entfernen", "s_fingerprint_deleted": "Fingerabdruck gelöscht", "p_warning_delete_fingerprint": "Das löscht den Fingerabdruck von Ihrem YubiKey.", - "s_fingerprints_get_started": null, - "p_set_fingerprints_desc": null, + "s_fingerprints_get_started": "Starten Sie mit Fingerabdrücken", + "p_set_fingerprints_desc": "Bevor Sie Fingerabdrücke hinzufügen können, muss eine PIN gesetzt werden.", "l_no_fps_added": "Es wurden keine Fingerabdrücke hinzugefügt", "s_rename_fp": "Fingerabdruck umbenennen", - "l_rename_fp_desc": null, + "l_rename_fp_desc": "Den Namen ändern", "s_fingerprint_renamed": "Fingerabdruck umbenannt", "l_rename_fp_failed": "Fehler beim Umbenennen: {message}", "@l_rename_fp_failed": { @@ -521,165 +565,169 @@ }, "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.", - "l_name_fingerprint": null, + "l_name_fingerprint": "Diesen Fingerabdruck benennen", "@_fido_errors": {}, - "l_user_action_timeout_error": null, - "l_wrong_inserted_yk_error": null, - "l_failed_connecting_to_fido": null, + "l_user_action_timeout_error": "Wegen Benutzer-Inaktivität fehlgeschlagen", + "l_wrong_inserted_yk_error": "Verwendeter YubiKey stimmt nicht mit dem initialen Gerät überein", + "l_failed_connecting_to_fido": "Die Verbindung zur FIDO-Schnittstelle ist fehlgeschlagen", "@_certificates": {}, - "s_certificate": null, - "s_csr": null, - "s_subject": null, - "l_export_csr_file": null, - "l_export_public_key": null, - "l_export_public_key_file": null, - "l_export_public_key_desc": null, - "l_public_key_exported": null, - "l_export_certificate": null, - "l_export_certificate_file": null, - "l_export_certificate_desc": null, - "l_certificate_exported": null, - "l_select_import_file": null, - "l_import_file": null, - "l_import_desc": null, - "l_import_nothing": null, - "l_importing_file": null, - "s_file_imported": null, - "l_unsupported_key_type": null, - "l_delete_certificate": null, - "l_delete_certificate_desc": null, - "l_delete_key": null, - "l_delete_key_desc": null, - "l_delete_certificate_or_key": null, - "l_delete_certificate_or_key_desc": null, - "l_move_key": null, - "l_move_key_desc": null, - "s_issuer": null, - "s_serial": null, - "s_certificate_fingerprint": null, - "s_valid_from": null, - "s_valid_to": null, - "l_no_certificate": null, - "l_key_no_certificate": null, - "s_generate_key": null, - "l_generate_desc": null, - "p_generate_desc": null, + "s_certificate": "Zertifikat", + "s_csr": "CSR", + "s_subject": "Subject", + "l_export_csr_file": "CSR in Datei speichern", + "l_export_public_key": "Öffentlichen Schlüssel exportieren", + "l_export_public_key_file": "Öffentlichen Schlüssel in Datei speichern", + "l_export_public_key_desc": "Den öffentlichen Schlüssel in eine Datei exportieren", + "l_public_key_exported": "Öffentlicher Schlüssel wurde exportiert", + "l_export_certificate": "Zertifikat exportieren", + "l_export_certificate_file": "Zertifikat in Datei exportieren", + "l_export_certificate_desc": "Das Zertifikat in eine Datei exportieren", + "l_certificate_exported": "Zertifikat exportiert", + "l_select_import_file": "Datei zum Importieren auswählen", + "l_import_file": "Datei importieren", + "l_import_desc": "Importieren Sie einen Schlüssel und/oder ein Zertifikat", + "l_import_nothing": "Nichts zu importieren", + "l_importing_file": "Importiere Datei\u2026", + "s_file_imported": "Datei importiert", + "l_unsupported_key_type": "Schlüssel-Typ wird nicht unterstützt", + "l_delete_certificate": "Zertifikat löschen", + "l_delete_certificate_desc": "Entfernen Sie das Zertifikat von Ihrem YubiKey", + "l_delete_key": "Schlüssel löschen", + "l_delete_key_desc": "Entfernen Sie den Schlüssel von Ihrem YubiKey", + "l_delete_certificate_or_key": "Zertifikat/Schlüssel löschen", + "l_delete_certificate_or_key_desc": "Entfernen Sie das Zertifikat oder den Schlüssel von Ihrem YubiKey", + "l_move_key": "Schlüssel verschieben", + "l_move_key_desc": "Verschieben Sie einen Schlüssel von einem PIV-Slot in einen anderen", + "l_change_defaults": null, + "s_issuer": "Aussteller", + "s_serial": "Serial", + "s_certificate_fingerprint": "Fingerprint", + "s_valid_from": "Gültig von", + "s_valid_to": "Gültig bis", + "l_no_certificate": "Kein Zertifikat geladen", + "l_key_no_certificate": "Schlüssel ohne Zertifikat geladen", + "s_generate_key": "Schlüssel erzeugen", + "l_generate_desc": "Erzeugen Sie ein neues Zertifikat oder CSR", + "p_generate_desc": "Dies erzeugt einen neuen Schlüssel auf dem YubiKey im PIV-Slot {slot}. Der öffentliche Schlüssel wird in einer Datei gespeichert, die in ein selbstsigniertes Zertifikat gespeichert am YubiKey eingebettet ist, oder in einem in einer Datei gespeicherten Certificate Signing Request (CSR).", "@p_generate_desc": { "placeholders": { "slot": {} } }, - "s_private_key_generated": null, - "p_select_what_to_delete": null, - "p_warning_delete_certificate": null, - "p_warning_delete_key": null, - "p_warning_delete_certificate_and_key": null, - "q_delete_certificate_confirm": null, + "s_private_key_generated": "Privater Schlüssel erzeugt", + "p_select_what_to_delete": "Wählen Sie was vom Slot gelöscht werden soll.", + "p_warning_delete_certificate": "Vorsicht! Diese Aktion löscht das Zertifikat von Ihrem YubiKey.", + "p_warning_delete_key": "Vorsicht! Diese Aktion löscht den privaten Schlüssel von Ihrem YubiKey.", + "p_warning_delete_certificate_and_key": "Vorsicht! Diese Aktion löscht das Zertifikat und den privaten Schlüssel von Ihrem YubiKey.", + "q_delete_certificate_confirm": "Soll das Zertifikat im PIV-Slot {slot} gelöscht werden?", "@q_delete_certificate_confirm": { "placeholders": { "slot": {} } }, - "q_delete_key_confirm": null, + "q_delete_key_confirm": "Soll der private Schlüssel im PIV-Slot {slot} gelöscht werden?", "@q_delete_key_confirm": { "placeholders": { "slot": {} } }, - "q_delete_certificate_and_key_confirm": null, + "q_delete_certificate_and_key_confirm": "Soll das Zertifikat und der private Schlüssel im PIV-Slot {slot} gelöscht werden?", "@q_delete_certificate_and_key_confirm": { "placeholders": { "slot": {} } }, - "l_certificate_deleted": null, - "l_key_deleted": null, - "l_certificate_and_key_deleted": null, - "l_include_certificate": null, - "l_select_destination_slot": null, - "q_move_key_confirm": null, + "l_certificate_deleted": "Zertifikat gelöscht", + "l_key_deleted": "Schlüssel gelöscht", + "l_certificate_and_key_deleted": "Zertifikat und Schlüssel gelöscht", + "l_include_certificate": "Zertifikat einschließen", + "l_select_destination_slot": "Ziel-Slot wählen", + "q_move_key_confirm": "Soll der private Schlüssel im PIV-Slot {from_slot} verschoben werden?", "@q_move_key_confirm": { "placeholders": { "from_slot": {} } }, - "q_move_key_to_slot_confirm": null, + "q_move_key_to_slot_confirm": "Soll der private Schlüssel im PIV-Slot {from_slot} zum Slot {to_slot} verschoben werden?", "@q_move_key_to_slot_confirm": { "placeholders": { "from_slot": {}, "to_slot": {} } }, - "q_move_key_and_certificate_to_slot_confirm": null, + "q_move_key_and_certificate_to_slot_confirm": "Soll der private Schlüssel und das Zertifikat im PIV-Slot {from_slot} zum Slot {to_slot} verschoben werden?", "@q_move_key_and_certificate_to_slot_confirm": { "placeholders": { "from_slot": {}, "to_slot": {} } }, - "p_password_protected_file": null, - "p_import_items_desc": null, + "p_password_protected_file": "Die gewählte Datei ist passwortgeschützt. Geben Sie das Passwort ein, um fortzufahren.", + "p_import_items_desc": "Die folgenden Elemente werden in den PIV-Slot {slot} importiert.", "@p_import_items_desc": { "placeholders": { "slot": {} } }, - "l_key_moved": null, - "l_key_and_certificate_moved": null, - "p_subject_desc": null, - "l_rfc4514_invalid": null, - "rfc4514_examples": null, - "p_cert_options_desc": null, - "s_overwrite_slot": null, - "p_overwrite_slot_desc": null, + "l_key_moved": "Schlüssel verschoben", + "l_key_and_certificate_moved": "Schlüssel und Zertifikat verschoben", + "p_subject_desc": "Ein Distinguished Name (DN) formtiert nach RFC 4514 Spezifikationen.", + "l_rfc4514_invalid": "Ungültiges RFC 4514-Format", + "rfc4514_examples": "Beispiele:\nCN=Beispielname\nCN=jschmitt,DC=beispiel,DC=net", + "s_allow_fingerprint": null, + "p_cert_options_desc": "Verwendeter Schlüssel-Algorithmus, Ausgabeformat und Ablaufdatum (nur Zertifikat).", + "p_cert_options_bio_desc": null, + "p_key_options_bio_desc": null, + "s_overwrite_slot": "Slot überschreiben", + "p_overwrite_slot_desc": "Damit wird vorhandener Inhalt im Slot {slot} dauerhaft überschrieben.", "@p_overwrite_slot_desc": { "placeholders": { "slot": {} } }, - "l_overwrite_cert": null, - "l_overwrite_key": null, - "l_overwrite_key_maybe": null, + "l_overwrite_cert": "Das Zertifikat wird überschrieben", + "l_overwrite_key": "Der private Schlüssel wird überschrieben", + "l_overwrite_key_maybe": "Jeglicher vorhandener privater Schlüssel im Slot wird überschrieben", "@_piv_slots": {}, - "s_slot_display_name": null, + "s_slot_display_name": "{name} ({hexid})", "@s_slot_display_name": { "placeholders": { "name": {}, "hexid": {} } }, - "s_slot_9a": null, - "s_slot_9c": null, - "s_slot_9d": null, - "s_slot_9e": null, - "s_retired_slot": null, + "s_slot_9a": "Authentifizierung", + "s_slot_9c": "Digitale Signatur", + "s_slot_9d": "Schlüssel-Verwaltung", + "s_slot_9e": "Card-Authentifizierung", + "s_retired_slot": "Verwaltung zurückgezogener Schlüssel", "@_otp_slots": {}, - "s_otp_slot_one": null, - "s_otp_slot_two": null, - "l_otp_slot_empty": null, - "l_otp_slot_configured": null, + "s_otp_slot_one": "Kurze Berührung", + "s_otp_slot_two": "Lange Berührung", + "l_otp_slot_empty": "Slot ist leer", + "l_otp_slot_configured": "Slot ist konfiguriert", "@_otp_slot_configurations": {}, - "l_yubiotp_desc": null, - "s_challenge_response": null, - "l_challenge_response_desc": null, - "s_static_password": null, - "l_static_password_desc": null, - "s_hotp": null, - "l_hotp_desc": null, - "s_public_id": null, - "s_private_id": null, - "s_use_serial": null, - "l_select_file": null, - "l_no_export_file": null, - "s_no_export": null, - "s_export": null, - "l_export_configuration_file": null, - "l_exported_can_be_uploaded_at": null, + "l_yubiotp_desc": "Legen Sie eine Yubico OTP-Anmeldeinformation fest", + "s_challenge_response": "Challenge-Response", + "l_challenge_response_desc": "Legen Sie eine Challenge-Response Anmeldeinformation fest", + "s_static_password": "Statisches Passwort", + "l_static_password_desc": "Konfigurieren Sie ein statisches Passwort", + "s_hotp": "OATH-HOTP", + "l_hotp_desc": "Legen Sie eine auf HMAC-SHA1 basierende Anmeldeinformation fest", + "s_public_id": "Öffentliche ID", + "s_private_id": "Private ID", + "s_use_serial": "Serial verwenden", + "l_select_file": "Datei auswählen", + "l_no_export_file": "Keine Datei für den Export", + "s_no_export": "Kein Export", + "s_export": "Export", + "l_export_configuration_file": "Exportiere Konfiguration in Datei", + "l_exported_can_be_uploaded_at": "Exportierte Anmeldeinformationen können bei {url} hochgeladen werden", "@_export_can_be_uploaded_at": { "placeholders": { "url": {} @@ -687,45 +735,45 @@ }, "@_otp_slot_actions": {}, - "s_delete_slot": null, - "l_delete_slot_desc": null, - "p_warning_delete_slot_configuration": null, + "s_delete_slot": "Anmeldeinformation löschen", + "l_delete_slot_desc": "Anmeldeinformation in Slot löschen", + "p_warning_delete_slot_configuration": "Vorsicht! Diese Aktion löscht die Anmeldeinformation dauerhaft vom Slot {slot_id}.", "@p_warning_delete_slot_configuration": { "placeholders": { "slot_id": {} } }, - "l_slot_deleted": null, - "s_swap": null, - "s_swap_slots": null, - "l_swap_slots_desc": null, - "p_swap_slots_desc": null, - "l_slots_swapped": null, - "l_slot_credential_configured": null, + "l_slot_deleted": "Anmeldeinformation gelöscht", + "s_swap": "Vertauschen", + "s_swap_slots": "Slots tauschen", + "l_swap_slots_desc": "Kurze/lange Berührung vertauschen", + "p_swap_slots_desc": "Dies vertauscht die Konfiguration der beiden Slots.", + "l_slots_swapped": "Slot-Konfigurationen vertauscht", + "l_slot_credential_configured": "{type} Anmeldeinformation konfiguriert", "@l_slot_credential_configured": { "placeholders": { "type": {} } }, - "l_slot_credential_configured_and_exported": null, + "l_slot_credential_configured_and_exported": "{type} Anmeldeinformation konfiguriert und in die Datei {file} exportiert", "@l_slot_credential_configured_and_exported": { "placeholders": { "type": {}, "file": {} } }, - "s_append_enter": null, - "l_append_enter_desc": null, + "s_append_enter": "⏎ anhängen", + "l_append_enter_desc": "Nach der Ausgabe des OTP ein Enter-Zeichen anhängen", "@_otp_errors": {}, - "p_otp_swap_error": null, - "l_wrong_access_code": null, + "p_otp_swap_error": "Das Vertauschen der Slots ist fehlgeschlagen! Stellen Sie sicher, dass der YubiKey uneingeschränkten Zugriff zulässt.", + "l_wrong_access_code": "Falscher Zugriffscode", "@_otp_access_code": {}, - "s_access_code": null, - "s_show_access_code": null, - "s_hide_access_code": null, - "p_enter_access_code": null, + "s_access_code": "Auf Zugriffscode zugreifen", + "s_show_access_code": "Zugriffscode anzeigen", + "s_hide_access_code": "Zugriffscode verstecken", + "p_enter_access_code": "Zugriffscode für Slot {slot} eingeben.", "@p_enter_access_code": { "placeholders": { "slot": {} @@ -735,29 +783,29 @@ "@_permissions": {}, "s_enable_nfc": "NFC aktivieren", - "s_request_access": null, + "s_request_access": "Zugriff anfordern", "s_permission_denied": "Zugriff verweigert", "l_elevating_permissions": "Erhöhe Berechtigungen\u2026", "s_review_permissions": "Berechtigungen überprüfen", - "s_open_windows_settings": null, - "l_admin_privileges_required": null, + "s_open_windows_settings": "Windows-Einstellungen öffnen", + "l_admin_privileges_required": "Administrator-Rechte erforderlich", "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.", - "l_ms_store_permission_note": null, + "l_ms_store_permission_note": "Die Version der App aus dem Microsoft Store könnte nicht in der Lage sein erhöhte Berechtigungen zu erhalten", "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_invalid_qr": "Ungültiger QR-Code", "l_qr_not_found": "Kein QR-Code gefunden", - "l_qr_file_too_large": null, + "l_qr_file_too_large": "Datei zu groß (max {max})", "@l_qr_file_too_large": { "placeholders": { "max": {} } }, - "l_qr_invalid_image_file": null, - "l_qr_select_file": null, + "l_qr_invalid_image_file": "Ungültige Bilddatei", + "l_qr_select_file": "Datei mit QR-Code auswählen", "l_qr_not_read": "Fehler beim Lesen des QR-Codes: {message}", "@l_qr_not_read": { "placeholders": { @@ -768,12 +816,13 @@ "q_want_to_scan": "Möchten Sie aufnehmen?", "q_no_qr": "Kein QR-Code?", "s_enter_manually": "Manuell eingeben", - "s_read_from_file": null, + "s_read_from_file": "Von Datei lesen", "@_factory_reset": {}, "s_reset": "Zurücksetzen", "s_factory_reset": "Werkseinstellungen", - "l_factory_reset_desc": null, + "l_factory_reset_desc": "YubiKey-Standardeinstellungen wiederherstellen", + "l_factory_reset_required": null, "l_oath_application_reset": "OATH Anwendung zurücksetzen", "l_fido_app_reset": "FIDO Anwendung zurückgesetzt", "l_reset_failed": "Fehler beim Zurücksetzen: {message}", @@ -782,17 +831,17 @@ "message": {} } }, - "l_piv_app_reset": null, - "p_factory_reset_an_app": null, - "p_factory_reset_desc": null, - "p_warning_factory_reset": "Achtung! Das löscht alle OATH TOTP/HOTP Konten unwiederbringlich von Ihrem YubiKey.", + "l_piv_app_reset": "PIV-Anwendung zurücksetzen", + "p_factory_reset_an_app": "Setzen Sie eine Anwendung auf Ihrem YubiKey auf Werkseinstellungen zurück.", + "p_factory_reset_desc": "Daten werden in mehreren Anwendungen auf dem YubiKey gespeichert. Manche können unabhängig voneinander auf Werkseinstellungen zurückgesetzt werden.\n\nWählen Sie eine Anwendung oben zum Zurücksetzen aus.", + "p_warning_factory_reset": "Vorsicht! Dies löscht alle OATH TOTP/HOTP Konten dauerhaft 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_deletes_accounts": "Vorsicht! Dies löscht alle U2F und FIDO2 Konten dauerhaft 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.", - "p_warning_piv_reset": null, - "p_warning_piv_reset_desc": null, - "p_warning_global_reset": null, - "p_warning_global_reset_desc": null, + "p_warning_piv_reset": "Vorsicht! Alle gespeicherten Daten für PIV werden dauerhaft von Ihrem YubiKey gelöscht.", + "p_warning_piv_reset_desc": "Dies schließt private Schlüssel und Zertifikate mit ein. Ihre PIN, PUK und Verwaltungsschlüssel werden auf ihren Werksstandard zurückgesetzt.", + "p_warning_global_reset": "Vorsicht! Dies löscht alle gespeicherten Daten einschließlich Anmeldeinformationen dauerhaft von Ihrem YubiKey.", + "p_warning_global_reset_desc": "Setzt die Anwendungen auf Ihrem YubiKey auf Werkseinstellungen zurück. Ihre PIN wird auf ihre Werkseinstellung zurückgesetzt, gespeicherte Fingerabdrücke werden entfernt, jegliche Schlüssel, Zertifikate oder andere Anmeldeinformationen werden dauerhaft entfernt.", "@_copy_to_clipboard": {}, "l_copy_to_clipboard": "In die Zwischenablage kopieren", @@ -832,7 +881,7 @@ "@_android_settings": {}, "s_nfc_options": "NFC Optionen", "l_on_yk_nfc_tap": "Bei YubiKey NFC-Berührung", - "l_do_nothing": null, + "l_do_nothing": "Nichts tun", "l_launch_ya": "Yubico Authenticator starten", "l_copy_otp_clipboard": "OTP in Zwischenablage kopieren", "l_launch_and_copy_otp": "App starten und OTP kopieren", @@ -850,41 +899,48 @@ "l_launch_app_on_usb_off": "Andere Anwendungen können den YubiKey über USB nutzen", "s_allow_screenshots": "Bildschirmfotos erlauben", - "l_nfc_dialog_tap_key": null, - "s_nfc_dialog_operation_success": null, - "s_nfc_dialog_operation_failed": null, - - "s_nfc_dialog_oath_reset": null, - "s_nfc_dialog_oath_unlock": null, - "s_nfc_dialog_oath_set_password": null, - "s_nfc_dialog_oath_unset_password": null, - "s_nfc_dialog_oath_add_account": null, - "s_nfc_dialog_oath_rename_account": null, - "s_nfc_dialog_oath_delete_account": null, - "s_nfc_dialog_oath_calculate_code": null, - "s_nfc_dialog_oath_failure": null, - "s_nfc_dialog_oath_add_multiple_accounts": null, - - "s_nfc_dialog_fido_reset": null, - "s_nfc_dialog_fido_unlock": null, - "l_nfc_dialog_fido_set_pin": null, - "s_nfc_dialog_fido_delete_credential": null, - "s_nfc_dialog_fido_delete_fingerprint": null, - "s_nfc_dialog_fido_rename_fingerprint": null, - "s_nfc_dialog_fido_failure": null, + "l_nfc_dialog_tap_key": "Halten Sie Ihren Schlüssel dagegen", + "s_nfc_dialog_operation_success": "Erfolgreich", + "s_nfc_dialog_operation_failed": "Fehlgeschlagen", + + "s_nfc_dialog_oath_reset": "Aktion: OATH-Anwendung zurücksetzen", + "s_nfc_dialog_oath_unlock": "Aktion: OATH-Anwendung entsperren", + "s_nfc_dialog_oath_set_password": "Aktion: OATH-Passwort setzen", + "s_nfc_dialog_oath_unset_password": "Aktion: OATH-Passwort entfernen", + "s_nfc_dialog_oath_add_account": "Aktion: neues Konto hinzufügen", + "s_nfc_dialog_oath_rename_account": "Aktion: Konto umbenennen", + "s_nfc_dialog_oath_delete_account": "Aktion: Konto löschen", + "s_nfc_dialog_oath_calculate_code": "Aktion: OATH-Code berechnen", + "s_nfc_dialog_oath_failure": "OATH-Operation fehlgeschlagen", + "s_nfc_dialog_oath_add_multiple_accounts": "Aktion: mehrere Konten hinzufügen", + + "s_nfc_dialog_fido_reset": "Aktion: FIDO-Anwendung zurücksetzen", + "s_nfc_dialog_fido_unlock": "Aktion: FIDO-Anwendung entsperren", + "l_nfc_dialog_fido_set_pin": "Aktion: FIDO-PIN setzen oder ändern", + "s_nfc_dialog_fido_delete_credential": "Aktion: Passkey löschen", + "s_nfc_dialog_fido_delete_fingerprint": "Aktion: Fingerabdruck löschen", + "s_nfc_dialog_fido_rename_fingerprint": "Aktion: Fingerabdruck umbenennen", + "s_nfc_dialog_fido_failure": "FIDO-Operation fehlgeschlagen", + + "@_nfc": {}, + "s_nfc_ready_to_scan": null, + "s_nfc_hold_still": null, + "s_nfc_tap_your_yubikey": null, + "l_nfc_failed_to_scan": null, "@_ndef": {}, - "p_ndef_set_otp": null, - "p_ndef_set_password": null, - "p_ndef_parse_failure": null, - "p_ndef_set_clip_failure": null, + "p_ndef_set_otp": "OTP-Code wurde erfolgreich von Ihrem YubiKey in die Zwischenablage kopiert.", + "p_ndef_set_password": "Passwort wurde erfolgreich von Ihrem YubiKey in die Zwischenablage kopiert.", + "p_ndef_parse_failure": "Beim Parsen des OTP-Codes von Ihrem YubiKey ist ein Fehler aufgetreten.", + "p_ndef_set_clip_failure": "Konnte während dem Versuch den OTP-Code von Ihrem YubiKey zu kopieren nicht auf die Zwischenablage zugreifen.", "@_key_customization": {}, - "s_set_label": null, - "s_change_label": null, - "s_color": null, - "p_set_will_add_custom_name": null, - "p_rename_will_change_custom_name": null, + "s_set_label": "Label setzen", + "s_set_color": "Farbe setzen", + "s_change_label": "Label ändern", + "s_color": "Farbe", + "p_set_will_add_custom_name": "Das gibt Ihrem YubiKey einen eigenen Namen.", + "p_rename_will_change_custom_name": "Das ändert das Label Ihres YubiKeys.", "@_eof": {} } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index edb419838..c3d4b7e27 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -30,11 +30,15 @@ "s_delete": "Delete", "s_move": "Move", "s_quit": "Quit", + "s_enable": "Enable", + "s_enabled": "Enabled", + "s_disabled": "Disabled", "s_status": "Status", "s_unlock": "Unlock", "s_calculate": "Calculate", "s_import": "Import", "s_overwrite": "Overwrite", + "s_done": "Done", "s_label": "Label", "s_name": "Name", "s_usb": "USB", @@ -43,8 +47,11 @@ "s_details": "Details", "s_show_window": "Show window", "s_hide_window": "Hide window", + "s_show_navigation": "Show navigation", "s_expand_navigation": "Expand navigation", "s_collapse_navigation": "Collapse navigation", + "s_show_menu": "Show menu", + "s_hide_menu": "Hide menu", "q_rename_target": "Rename {label}?", "@q_rename_target": { "placeholders": { @@ -82,7 +89,6 @@ "s_terms_of_use": "Terms of use", "s_privacy_policy": "Privacy policy", "s_open_src_licenses": "Open source licenses", - "s_configure_yk": "Configure YubiKey", "s_please_wait": "Please wait\u2026", "s_secret_key": "Secret key", "s_show_secret_key": "Show secret key", @@ -121,6 +127,12 @@ "s_light_mode": "Light mode", "s_dark_mode": "Dark mode", + "@_layout": {}, + "s_list_layout": "List layout", + "s_grid_layout": "Grid layout", + "s_mixed_layout": "Mixed layout", + "s_select_layout": "Select layout", + "@_yubikey_selection": {}, "s_select_to_scan": "Select to scan", "s_hide_device": "Hide device", @@ -149,6 +161,8 @@ } }, "l_firmware_version": "Firmware version: {version}", + "l_fips_capable": "FIPS capable", + "l_fips_approved": "FIPS approved", "@_yubikey_interactions": {}, "l_insert_yk": "Insert your YubiKey", @@ -216,9 +230,15 @@ "l_no_yk_present": "No YubiKey present", "s_unknown_type": "Unknown type", "s_unknown_device": "Unrecognized device", + "s_restricted_nfc": "NFC activation", + "l_deactivate_restricted_nfc": "How to activate NFC", + "p_deactivate_restricted_nfc_desc": "Connect your YubiKey to any USB power source, such as a computer, for at least 3 seconds.\n\nOnce powered, NFC will be activated and ready for use.", + "p_deactivate_restricted_nfc_footer": "Your YubiKey is equipped with Restricted NFC, a feature designed to safeguard against wireless manipulation during shipping. This means that NFC operations are temporarily disabled until you activate them.", "s_unsupported_yk": "Unsupported YubiKey", "s_yk_not_recognized": "Device not recognized", "p_operation_failed_try_again": "The operation failed, please try again.", + "l_configuration_unsupported": "Configuration not supported", + "p_scp_unsupported": "To communicate over NFC, the YubiKey requires technology which is not supported by this phone. Please plug the YubiKey to the phone's USB port.", "@_general_errors": {}, "l_error_occurred": "An error has occurred", @@ -278,41 +298,50 @@ "l_enter_fido2_pin": "Enter the FIDO2 PIN for your YubiKey", "l_pin_blocked_reset": "PIN is blocked; factory reset the FIDO application", "l_pin_blocked": "PIN is blocked", - "l_set_pin_first": "A PIN is required first", - "l_unlock_pin_first": "Unlock with PIN first", + "l_set_pin_first": "A PIN is required", + "l_unlock_pin_first": "Unlock with PIN", "l_pin_soft_locked": "PIN has been blocked until the YubiKey is removed and reinserted", "l_pin_change_required_desc": "A new PIN must be set before you can use this application", "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_pin_or_reset_no_puk": "Enter your current PIN. If you don't know your PIN, you'll need to 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": "Enter your new PIN. A PIN must be {min_length}-{max_length} characters long and may contain letters, numbers and special characters.", "@p_enter_new_fido2_pin": { "placeholders": { - "length": {} + "min_length": {}, + "max_length": {} } }, - "p_enter_new_fido2_pin_complexity_active": "Enter your new PIN. A PIN must be at least {length} characters long, contain at least {unique_characters} unique characters, and not be a commonly used PIN, like \"{common_pin}\". It may contain letters, numbers, and special characters.", + "p_enter_new_fido2_pin_complexity_active": "Enter your new PIN. A PIN must be {min_length}-{max_length} characters long, contain at least {unique_characters} unique characters, and not be a commonly used PIN, like \"{common_pin}\". It may contain letters, numbers, and special characters.", "@p_enter_new_fido2_pin_complexity_active": { "placeholders": { - "length": {}, + "min_length": {}, + "max_length": {}, "unique_characters": {}, "common_pin": {} } }, + "s_ep_attestation": "Enterprise Attestation", + "s_ep_attestation_enabled": "Enterprise Attestation enabled", + "s_enable_ep_attestation": "Enable Enterprise Attestation", + "p_enable_ep_attestation_desc": "This will enable Enterprise Attestation, allowing authorized domains to uniquely identify your YubiKey.", + "p_enable_ep_attestation_disable_with_factory_reset": "Once enabled, Enterprise Attestation can only be disabled by performing a FIDO factory reset.", "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": "Enter a new {name} to set. Must be at least {length} characters long.", "@p_enter_new_piv_pin_puk": { "placeholders": { - "name": {} + "name": {}, + "length": {} } }, - "p_enter_new_piv_pin_puk_complexity_active": "Enter a new {name} to set. Must be 6-8 characters, contain at least 2 unique characters, and not be a commonly used {name}, like \"{common}\".", + "p_enter_new_piv_pin_puk_complexity_active": "Enter a new {name} to set. Must be at least {length} characters long, contain at least 2 unique characters, and not be a commonly used {name}, like \"{common}\".", "@p_enter_new_piv_pin_puk_complexity_active": { "placeholders": { "name": {}, + "length": {}, "common": {} } }, @@ -326,6 +355,7 @@ "l_warning_default_puk": "Warning: Default PUK used", "l_default_pin_used": "Default PIN used", "l_default_puk_used": "Default PUK used", + "l_pin_complexity": "PIN complexity enforced", "@_passwords": {}, "s_password": "Password", @@ -335,6 +365,7 @@ "s_show_password": "Show password", "s_hide_password": "Hide password", "l_optional_password_protection": "Optional password protection", + "l_password_protection": "Password protection of accounts", "s_new_password": "New password", "s_current_password": "Current password", "s_confirm_password": "Confirm password", @@ -347,7 +378,8 @@ "s_password_forgotten": "Password forgotten", "l_keystore_unavailable": "OS Keystore unavailable", "l_remember_pw_failed": "Failed to remember password", - "l_unlock_first": "Unlock with password first", + "l_unlock_first": "Unlock with password", + "l_set_password_first": "Set a password", "l_enter_oath_pw": "Enter the OATH password for your YubiKey", "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.", @@ -398,6 +430,10 @@ "message": {} } }, + "l_add_account_password_required": "Password required", + "l_add_account_unlock_required": "Unlock required", + "l_add_account_already_exists": "Account already exists", + "l_add_account_func_missing": "Functionality missing or disabled", "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", @@ -406,16 +442,23 @@ "s_pin_account": "Pin account", "s_unpin_account": "Unpin account", "s_no_pinned_accounts": "No pinned accounts", + "s_pinned": "Pinned", "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", + "l_rename_account_failed": "Failed renaming account: {message}", + "@l_rename_account_failed": { + "placeholders": { + "message": {} + } + }, "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.", + "p_warning_disable_credential": "You will no longer be able to generate OTPs for this account. Make sure to disable this credential from the website to avoid being locked out of your account.", "s_account_name": "Account name", "s_search_accounts": "Search accounts", "l_accounts_used": "{used} of {capacity} accounts used", @@ -474,6 +517,7 @@ } }, "@_fingerprints": {}, + "s_biometrics": "Biometrics", "l_fingerprint": "Fingerprint: {label}", "@l_fingerprint": { "placeholders": { @@ -556,6 +600,7 @@ "l_delete_certificate_or_key_desc": "Remove the certificate or key from your YubiKey", "l_move_key": "Move key", "l_move_key_desc": "Move a key from one PIV slot into another", + "l_change_defaults": "Change default access codes", "s_issuer": "Issuer", "s_serial": "Serial", "s_certificate_fingerprint": "Fingerprint", @@ -631,7 +676,10 @@ "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", + "s_allow_fingerprint": "Allow fingerprint", "p_cert_options_desc": "Key algorithm to use, output format, and expiration date (certificate only).", + "p_cert_options_bio_desc": "Key algorithm to use, output format, expiration date (certificate only), and if biometrics can be used instead of PIN.", + "p_key_options_bio_desc": "Allow biometrics to be used instead of PIN.", "s_overwrite_slot": "Overwrite slot", "p_overwrite_slot_desc": "This will permanently overwrite existing content in slot {slot}.", "@p_overwrite_slot_desc": { @@ -774,6 +822,7 @@ "s_reset": "Reset", "s_factory_reset": "Factory reset", "l_factory_reset_desc": "Restore YubiKey defaults", + "l_factory_reset_required": "Factory reset required", "l_oath_application_reset": "OATH application reset", "l_fido_app_reset": "FIDO application reset", "l_reset_failed": "Error performing reset: {message}", @@ -786,13 +835,13 @@ "p_factory_reset_an_app": "Factory reset an application on your YubiKey.", "p_factory_reset_desc": "Data is stored in multiple applications on the YubiKey, some of which can be factory reset independently of each other.\n\nSelect an application above to 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_disable_credentials": "Your OATH credentials, as well as any password set, will be removed from this YubiKey. Make sure to 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, including passkeys, 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_disable_accounts": "Your credentials, as well as any PIN set, will be removed from this YubiKey. Make sure to 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.", "p_warning_global_reset": "Warning! This will irrevocably delete all saved data, including credentials, from your YubiKey.", - "p_warning_global_reset_desc": "Factory reset the applications on your YubiKey. PIN will be reset to its factorey default value, and registered fingerprints will be removed. Any keys, certificates, or other credentials will all be permanently removed.", + "p_warning_global_reset_desc": "Factory reset the applications on your YubiKey. PIN will be reset to its factory default value, and registered fingerprints will be removed. Any keys, certificates, or other credentials will all be permanently removed.", "@_copy_to_clipboard": {}, "l_copy_to_clipboard": "Copy to clipboard", @@ -873,6 +922,12 @@ "s_nfc_dialog_fido_rename_fingerprint": "Action: rename fingerprint", "s_nfc_dialog_fido_failure": "FIDO operation failed", + "@_nfc": {}, + "s_nfc_ready_to_scan": "Ready to scan", + "s_nfc_hold_still": "Hold still\u2026", + "s_nfc_tap_your_yubikey": "Tap your YubiKey", + "l_nfc_failed_to_scan": "Failed to scan, try again", + "@_ndef": {}, "p_ndef_set_otp": "Successfully copied OTP code from YubiKey to clipboard.", "p_ndef_set_password": "Successfully copied password from YubiKey to clipboard.", @@ -881,6 +936,7 @@ "@_key_customization": {}, "s_set_label": "Set label", + "s_set_color": "Set color", "s_change_label": "Change label", "s_color": "Color", "p_set_will_add_custom_name": "This will give your YubiKey a custom name.", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 934b9a086..52b271dec 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -30,11 +30,15 @@ "s_delete": "Supprimer", "s_move": "Déplacer", "s_quit": "Quitter", + "s_enable": null, + "s_enabled": null, + "s_disabled": null, "s_status": "État", "s_unlock": "Déverrouiller", "s_calculate": "Calculer", "s_import": "Importer", "s_overwrite": "Écraser", + "s_done": null, "s_label": "Étiquette", "s_name": "Nom", "s_usb": "USB", @@ -43,8 +47,11 @@ "s_details": "Détails", "s_show_window": "Montrer fenêtre", "s_hide_window": "Masquer fenêtre", + "s_show_navigation": null, "s_expand_navigation": "Développer la navigation", "s_collapse_navigation": "Réduire la navigation", + "s_show_menu": null, + "s_hide_menu": null, "q_rename_target": "Renommer {label}\u00a0?", "@q_rename_target": { "placeholders": { @@ -82,7 +89,6 @@ "s_terms_of_use": "Conditions d'utilisation", "s_privacy_policy": "Confidentialité", "s_open_src_licenses": "Licences Open Source", - "s_configure_yk": "Configurer YubiKey", "s_please_wait": "Patientez\u2026", "s_secret_key": "Clé secrète", "s_show_secret_key": "Afficher clé secrète", @@ -121,6 +127,12 @@ "s_light_mode": "Thème clair", "s_dark_mode": "Thème sombre", + "@_layout": {}, + "s_list_layout": null, + "s_grid_layout": null, + "s_mixed_layout": null, + "s_select_layout": null, + "@_yubikey_selection": {}, "s_select_to_scan": "Sélectionner pour scanner", "s_hide_device": "Masquer appareil", @@ -149,6 +161,8 @@ } }, "l_firmware_version": "Version du firmware : {version}", + "l_fips_capable": null, + "l_fips_approved": null, "@_yubikey_interactions": {}, "l_insert_yk": "Insérez votre YubiKey", @@ -216,9 +230,15 @@ "l_no_yk_present": "Aucune YubiKey présente", "s_unknown_type": "Type inconnu", "s_unknown_device": "Appareil non reconnu", + "s_restricted_nfc": null, + "l_deactivate_restricted_nfc": null, + "p_deactivate_restricted_nfc_desc": null, + "p_deactivate_restricted_nfc_footer": null, "s_unsupported_yk": "YubiKey non prise en charge", "s_yk_not_recognized": "Appareil non reconnu", "p_operation_failed_try_again": "L'opération a échoué, veuillez réessayer.", + "l_configuration_unsupported": null, + "p_scp_unsupported": null, "@_general_errors": {}, "l_error_occurred": "Une erreur s'est produite", @@ -285,34 +305,43 @@ "p_enter_current_pin_or_reset": "Saisissez votre PIN actuel. Vous ne connaissez pas votre PIN\u00a0? Débloquez-le avec le PUK ou réinitialisez la YubiKey.", "p_enter_current_pin_or_reset_no_puk": "Saisissez votre PIN actuel. Vous ne connaissez pas votre PIN\u00a0? Réinitialisez la YubiKey.", "p_enter_current_puk_or_reset": "Saisissez votre PUK actuel. Vous ne connaissez pas votre PUK\u00a0? Réinitialisez la YubiKey.", - "p_enter_new_fido2_pin": "Saisissez votre nouveau PIN. Un PIN doit avoir au moins {length} caractères et peut inclure des lettres, chiffres et caractères spéciaux.", + "p_enter_new_fido2_pin": null, "@p_enter_new_fido2_pin": { "placeholders": { - "length": {} + "min_length": {}, + "max_length": {} } }, - "p_enter_new_fido2_pin_complexity_active": "Saisissez votre nouveau code PIN. Le code PIN doit être composé d'au moins {length} caractères, contenir au moins {unique_characters} caractères uniques et ne pas être un code PIN couramment utilisé, comme \"{common_pin}\". Il peut contenir des lettres, des chiffres et des caractères spéciaux.", + "p_enter_new_fido2_pin_complexity_active": null, "@p_enter_new_fido2_pin_complexity_active": { "placeholders": { - "length": {}, + "min_length": {}, + "max_length": {}, "unique_characters": {}, "common_pin": {} } }, + "s_ep_attestation": null, + "s_ep_attestation_enabled": null, + "s_enable_ep_attestation": null, + "p_enable_ep_attestation_desc": null, + "p_enable_ep_attestation_disable_with_factory_reset": null, "s_pin_required": "PIN requis", "p_pin_required_desc": "L'action que vous allez effectuer nécessite la saisie du PIN PIV.", "l_piv_pin_blocked": "Bloqué, utilisez PUK pour réinitialiser", "l_piv_pin_puk_blocked": "Bloqué, réinitialisation aux paramètres d'usine nécessaire", - "p_enter_new_piv_pin_puk": "Saisissez un nouveau {name} à définir. Doit avoir 6-8 caractères.", + "p_enter_new_piv_pin_puk": null, "@p_enter_new_piv_pin_puk": { "placeholders": { - "name": {} + "name": {}, + "length": {} } }, - "p_enter_new_piv_pin_puk_complexity_active": "Entrez un nouveau {name} à définir. Doit être de 6 à 8 caractères, contenir au moins 2 caractères uniques, et ne pas être un {name}couramment utilisé, comme \"{common}\".", + "p_enter_new_piv_pin_puk_complexity_active": null, "@p_enter_new_piv_pin_puk_complexity_active": { "placeholders": { "name": {}, + "length": {}, "common": {} } }, @@ -326,6 +355,7 @@ "l_warning_default_puk": "Attention : PUK par défaut utilisé", "l_default_pin_used": "Code PIN par défaut utilisé", "l_default_puk_used": "PUK par défaut utilisé", + "l_pin_complexity": null, "@_passwords": {}, "s_password": "Mot de passe", @@ -335,6 +365,7 @@ "s_show_password": "Montrer mot de passe", "s_hide_password": "Masquer mot de passe", "l_optional_password_protection": "Protection par mot de passe facultative", + "l_password_protection": null, "s_new_password": "Nouveau mot de passe", "s_current_password": "Mot de passe actuel", "s_confirm_password": "Confirmer mot de passe", @@ -348,6 +379,7 @@ "l_keystore_unavailable": "OS Keystore indisponible", "l_remember_pw_failed": "Mémorisation mot de passe impossible", "l_unlock_first": "Débloquez d'abord avec mot de passe", + "l_set_password_first": null, "l_enter_oath_pw": "Saisissez le mot de passe OATH de votre YubiKey", "p_enter_current_password_or_reset": "Saisissez votre mot de passe actuel. Vous ne connaissez votre mot de passe\u00a0? Réinitialisez la YubiKey.", "p_enter_new_password": "Saisissez votre nouveau mot de passe. Un mot de passe peut inclure des lettres, chiffres et caractères spéciaux.", @@ -398,6 +430,10 @@ "message": {} } }, + "l_add_account_password_required": null, + "l_add_account_unlock_required": null, + "l_add_account_already_exists": null, + "l_add_account_func_missing": null, "l_account_name_required": "Votre compte doit avoir un nom", "l_name_already_exists": "Ce nom existe déjà pour l'émetteur", "l_account_already_exists": "Ce compte existe déjà sur la YubiKey", @@ -406,10 +442,17 @@ "s_pin_account": "Épingler compte", "s_unpin_account": "Détacher compte", "s_no_pinned_accounts": "Aucun compte épinglé", + "s_pinned": null, "l_pin_account_desc": "Conserver vos comptes importants ensemble", "s_rename_account": "Renommer compte", "l_rename_account_desc": "Modifier émetteur/nom du compte", "s_account_renamed": "Compte renommé", + "l_rename_account_failed": null, + "@l_rename_account_failed": { + "placeholders": { + "message": {} + } + }, "p_rename_will_change_account_displayed": "Cela modifiera l'affichage du compte dans la liste.", "s_delete_account": "Supprimer compte", "l_delete_account_desc": "Supprimer le compte de votre YubiKey", @@ -474,6 +517,7 @@ } }, "@_fingerprints": {}, + "s_biometrics": null, "l_fingerprint": "Empreinte digitale\u00a0: {label}", "@l_fingerprint": { "placeholders": { @@ -556,6 +600,7 @@ "l_delete_certificate_or_key_desc": "Supprimer le certificat ou la clé de votre YubiKey", "l_move_key": "Déplacer la clé", "l_move_key_desc": "Déplacer une clé d'un emplacement PIV vers un autre", + "l_change_defaults": null, "s_issuer": "Émetteur", "s_serial": "Série", "s_certificate_fingerprint": "Empreinte digitale", @@ -631,7 +676,10 @@ "p_subject_desc": "DN (nom distinctif) formaté conformément à la spécification RFC 4514.", "l_rfc4514_invalid": "Format RFC 4514 non valide", "rfc4514_examples": "Exemples\u00a0:\nCN=exemple de nom\nCN=jsmith,DC=exemple,DC=net", + "s_allow_fingerprint": null, "p_cert_options_desc": "Algorithme clé à utiliser, format de sortie et date d'expiration (certificat uniquement).", + "p_cert_options_bio_desc": null, + "p_key_options_bio_desc": null, "s_overwrite_slot": "Écraser slot", "p_overwrite_slot_desc": "Cela écrasera définitivement le contenu du slot {slot}.", "@p_overwrite_slot_desc": { @@ -774,6 +822,7 @@ "s_reset": "Réinitialiser", "s_factory_reset": "Réinitialisation usine", "l_factory_reset_desc": "Restaurer les paramètres par défaut de la YubiKey", + "l_factory_reset_required": null, "l_oath_application_reset": "Réinitialisation OATH", "l_fido_app_reset": "Réinitialisation FIDO", "l_reset_failed": "Erreur de réinitialisation\u00a0: {message}", @@ -873,6 +922,12 @@ "s_nfc_dialog_fido_rename_fingerprint": "Action : renommer l'empreinte digitale", "s_nfc_dialog_fido_failure": "Échec de l'opération FIDO", + "@_nfc": {}, + "s_nfc_ready_to_scan": null, + "s_nfc_hold_still": null, + "s_nfc_tap_your_yubikey": null, + "l_nfc_failed_to_scan": null, + "@_ndef": {}, "p_ndef_set_otp": "Code OTP copié de la YubiKey dans le presse-papiers.", "p_ndef_set_password": "Mot de passe copié de la YubiKey dans le presse-papiers.", @@ -881,6 +936,7 @@ "@_key_customization": {}, "s_set_label": "Définir l'étiquette", + "s_set_color": null, "s_change_label": "Modifier l'étiquette", "s_color": "Couleur", "p_set_will_add_custom_name": "Cela donnera un nom personnalisé à votre YubiKey.", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 6350460ac..fb55e0d06 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -30,11 +30,15 @@ "s_delete": "削除", "s_move": "移動", "s_quit": "終了", + "s_enable": null, + "s_enabled": null, + "s_disabled": null, "s_status": "ステータス", "s_unlock": "ロック解除", "s_calculate": "計算", "s_import": "インポート", "s_overwrite": "上書き", + "s_done": null, "s_label": "ラベル", "s_name": "名前", "s_usb": "USB", @@ -43,8 +47,11 @@ "s_details": "詳細", "s_show_window": "ウィンドウを表示", "s_hide_window": "ウィンドウを非表示", + "s_show_navigation": null, "s_expand_navigation": "ナビゲーションを展開", "s_collapse_navigation": "ナビゲーションを閉じる", + "s_show_menu": null, + "s_hide_menu": null, "q_rename_target": "{label}の名前を変更しますか?", "@q_rename_target": { "placeholders": { @@ -82,7 +89,6 @@ "s_terms_of_use": "利用規約", "s_privacy_policy": "プライバシーポリシー", "s_open_src_licenses": "オープンソースライセンス", - "s_configure_yk": "YubiKeyを設定", "s_please_wait": "お待ちください\u2026", "s_secret_key": "秘密鍵", "s_show_secret_key": "秘密鍵を表示", @@ -121,6 +127,12 @@ "s_light_mode": "ライトモード", "s_dark_mode": "ダークモード", + "@_layout": {}, + "s_list_layout": null, + "s_grid_layout": null, + "s_mixed_layout": null, + "s_select_layout": null, + "@_yubikey_selection": {}, "s_select_to_scan": "選択してスキャン", "s_hide_device": "デバイスを非表示", @@ -149,6 +161,8 @@ } }, "l_firmware_version": "ファームウェアバージョン: {version}", + "l_fips_capable": null, + "l_fips_approved": null, "@_yubikey_interactions": {}, "l_insert_yk": "YubiKeyを挿入してください", @@ -216,9 +230,15 @@ "l_no_yk_present": "YubiKeyがありません", "s_unknown_type": "不明なタイプ", "s_unknown_device": "認識されないデバイス", + "s_restricted_nfc": null, + "l_deactivate_restricted_nfc": null, + "p_deactivate_restricted_nfc_desc": null, + "p_deactivate_restricted_nfc_footer": null, "s_unsupported_yk": "サポートされていないYubiKey", "s_yk_not_recognized": "デバイスが認識されません", "p_operation_failed_try_again": "操作に失敗しました。もう一度やり直してください。", + "l_configuration_unsupported": null, + "p_scp_unsupported": null, "@_general_errors": {}, "l_error_occurred": "エラーが発生しました", @@ -285,34 +305,43 @@ "p_enter_current_pin_or_reset": "現在のPINを入力してください。PINがわからない場合は、PUKでブロックを解除するか、YubiKeyをリセットする必要があります。", "p_enter_current_pin_or_reset_no_puk": "現在のPINを入力してください。PINがわからない場合は、YubiKeyをリセットする必要があります。", "p_enter_current_puk_or_reset": "現在のPUKを入力してください。PUKがわからない場合は、YubiKeyをリセットする必要があります。", - "p_enter_new_fido2_pin": "新しいPINを入力してください。PINは{length}文字以上にする必要があり、文字、数字、特殊文字を含めることができます。", + "p_enter_new_fido2_pin": null, "@p_enter_new_fido2_pin": { "placeholders": { - "length": {} + "min_length": {}, + "max_length": {} } }, - "p_enter_new_fido2_pin_complexity_active": "新しいPINを入力します。PINは少なくとも{length} 文字以上で、少なくとも{unique_characters} ユニークな文字を含み、「{common_pin}」のようなよく使われるPINであってはなりません。文字、数字、特殊文字を含むことができます。", + "p_enter_new_fido2_pin_complexity_active": null, "@p_enter_new_fido2_pin_complexity_active": { "placeholders": { - "length": {}, + "min_length": {}, + "max_length": {}, "unique_characters": {}, "common_pin": {} } }, + "s_ep_attestation": null, + "s_ep_attestation_enabled": null, + "s_enable_ep_attestation": null, + "p_enable_ep_attestation_desc": null, + "p_enable_ep_attestation_disable_with_factory_reset": null, "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": null, "@p_enter_new_piv_pin_puk": { "placeholders": { - "name": {} + "name": {}, + "length": {} } }, - "p_enter_new_piv_pin_puk_complexity_active": "設定する新しい {name} を入力します。6-8文字で、少なくとも2つのユニークな文字を含み、\"{common}\"のようなよく使われる{name} であってはいけません。", + "p_enter_new_piv_pin_puk_complexity_active": null, "@p_enter_new_piv_pin_puk_complexity_active": { "placeholders": { "name": {}, + "length": {}, "common": {} } }, @@ -326,6 +355,7 @@ "l_warning_default_puk": "警告: デフォルトのPUKが使用されています", "l_default_pin_used": "デフォルトのPINが使用されています", "l_default_puk_used": "既定のPUKを使用", + "l_pin_complexity": null, "@_passwords": {}, "s_password": "パスワード", @@ -335,6 +365,7 @@ "s_show_password": "パスワードを表示", "s_hide_password": "パスワードを非表示", "l_optional_password_protection": "オプションのパスワード保護", + "l_password_protection": null, "s_new_password": "新しいパスワード", "s_current_password": "現在のパスワード", "s_confirm_password": "パスワードを確認", @@ -348,6 +379,7 @@ "l_keystore_unavailable": "OSのキーストアを利用できません", "l_remember_pw_failed": "パスワードを記憶できませんでした", "l_unlock_first": "最初にパスワードでロックを解除", + "l_set_password_first": null, "l_enter_oath_pw": "YubiKeyのOATHパスワードを入力", "p_enter_current_password_or_reset": "現在のパスワードを入力してください。パスワードがわからない場合は、YubiKeyをリセットする必要があります。", "p_enter_new_password": "新しいパスワードを入力してください。パスワードには文字、数字、特殊文字を含めることができます。", @@ -398,6 +430,10 @@ "message": {} } }, + "l_add_account_password_required": null, + "l_add_account_unlock_required": null, + "l_add_account_already_exists": null, + "l_add_account_func_missing": null, "l_account_name_required": "アカウントには名前が必要です", "l_name_already_exists": "この名前は発行者にすでに存在します", "l_account_already_exists": "このアカウントはYubiKeyにすでに存在します", @@ -406,10 +442,17 @@ "s_pin_account": "アカウントをピン留めする", "s_unpin_account": "アカウントのピン留めを解除", "s_no_pinned_accounts": "ピン留めされたアカウントはありません", + "s_pinned": null, "l_pin_account_desc": "重要なアカウントをまとめて保持", "s_rename_account": "アカウント名を変更", "l_rename_account_desc": "アカウントの発行者/名前を編集", "s_account_renamed": "アカウントの名前が変更されました", + "l_rename_account_failed": null, + "@l_rename_account_failed": { + "placeholders": { + "message": {} + } + }, "p_rename_will_change_account_displayed": "これにより、リスト内のアカウントの表示が変更されます。", "s_delete_account": "アカウントを削除", "l_delete_account_desc": "YubiKeyからアカウントを削除", @@ -474,6 +517,7 @@ } }, "@_fingerprints": {}, + "s_biometrics": null, "l_fingerprint": "指紋:{label}", "@l_fingerprint": { "placeholders": { @@ -556,6 +600,7 @@ "l_delete_certificate_or_key_desc": "YubiKey から証明書または鍵を削除する", "l_move_key": "キーを移動", "l_move_key_desc": "あるPIVスロットから別のスロットにキーを移動する", + "l_change_defaults": null, "s_issuer": "発行者", "s_serial": "シリアル", "s_certificate_fingerprint": "フィンガープリント", @@ -631,7 +676,10 @@ "p_subject_desc": "RFC 4514仕様に準拠した形式の識別名(DN)。", "l_rfc4514_invalid": "無効なRFC 4514形式", "rfc4514_examples": "例:\nCN=Example Name CN=jsmith,DC=example,\nDC=net", + "s_allow_fingerprint": null, "p_cert_options_desc": "使用する鍵アルゴリズム、出力形式、および有効期限(証明書のみ)。", + "p_cert_options_bio_desc": null, + "p_key_options_bio_desc": null, "s_overwrite_slot": "スロットを上書き", "p_overwrite_slot_desc": "これにより、スロット{slot}内の既存コンテンツが完全に上書きされます。", "@p_overwrite_slot_desc": { @@ -774,6 +822,7 @@ "s_reset": "リセット", "s_factory_reset": "工場出荷時の状態にリセット", "l_factory_reset_desc": "YubiKey の既定値を復元", + "l_factory_reset_required": null, "l_oath_application_reset": "OATHアプリケーションのリセット", "l_fido_app_reset": "FIDOアプリケーションのリセット", "l_reset_failed": "リセットの実行エラー:{message}", @@ -873,6 +922,12 @@ "s_nfc_dialog_fido_rename_fingerprint": "アクション: 指紋の名前を変更する", "s_nfc_dialog_fido_failure": "FIDO操作に失敗しました", + "@_nfc": {}, + "s_nfc_ready_to_scan": null, + "s_nfc_hold_still": null, + "s_nfc_tap_your_yubikey": null, + "l_nfc_failed_to_scan": null, + "@_ndef": {}, "p_ndef_set_otp": "OTPコードがYubiKeyからクリップボードに正常にコピーされました。", "p_ndef_set_password": "パスワードがYubiKeyからクリップボードに正常にコピーされました。", @@ -881,6 +936,7 @@ "@_key_customization": {}, "s_set_label": "ラベルを設定", + "s_set_color": null, "s_change_label": "ラベルを変更", "s_color": "色", "p_set_will_add_custom_name": "これにより、YubiKey にカスタム名を付けることができます。", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index dcc974cbb..2aa42f669 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -30,11 +30,15 @@ "s_delete": "Usuń", "s_move": null, "s_quit": "Wyjdź", + "s_enable": null, + "s_enabled": null, + "s_disabled": null, "s_status": "Status", "s_unlock": "Odblokuj", "s_calculate": "Oblicz", "s_import": "Importuj", "s_overwrite": "Nadpisz", + "s_done": null, "s_label": "Etykieta", "s_name": "Nazwa", "s_usb": "USB", @@ -43,8 +47,11 @@ "s_details": "Szczegóły", "s_show_window": "Pokaż okno", "s_hide_window": "Ukryj okno", + "s_show_navigation": null, "s_expand_navigation": null, "s_collapse_navigation": null, + "s_show_menu": null, + "s_hide_menu": null, "q_rename_target": "Zmienić nazwę {label}?", "@q_rename_target": { "placeholders": { @@ -82,7 +89,6 @@ "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_show_secret_key": "Pokaż tajny klucz", @@ -121,6 +127,12 @@ "s_light_mode": "Jasny", "s_dark_mode": "Ciemny", + "@_layout": {}, + "s_list_layout": null, + "s_grid_layout": null, + "s_mixed_layout": null, + "s_select_layout": null, + "@_yubikey_selection": {}, "s_select_to_scan": "Wybierz, aby skanować", "s_hide_device": "Ukryj urządzenie", @@ -149,6 +161,8 @@ } }, "l_firmware_version": null, + "l_fips_capable": null, + "l_fips_approved": null, "@_yubikey_interactions": {}, "l_insert_yk": "Podłącz klucz YubiKey", @@ -216,9 +230,15 @@ "l_no_yk_present": "Nie wykryto YubiKey", "s_unknown_type": "Nieznany typ", "s_unknown_device": "Nierozpoznane urządzenie", + "s_restricted_nfc": null, + "l_deactivate_restricted_nfc": null, + "p_deactivate_restricted_nfc_desc": null, + "p_deactivate_restricted_nfc_footer": null, "s_unsupported_yk": "Nieobsługiwany klucz YubiKey", "s_yk_not_recognized": "Urządzenie nie rozpoznane", "p_operation_failed_try_again": null, + "l_configuration_unsupported": null, + "p_scp_unsupported": null, "@_general_errors": {}, "l_error_occurred": "Wystąpił błąd", @@ -285,34 +305,43 @@ "p_enter_current_pin_or_reset": "Wprowadź aktualny kod PIN. Jeśli go nie znasz, musisz zresetować klucz YubiKey.", "p_enter_current_pin_or_reset_no_puk": "Wprowadź aktualny PIN. Jeśli go nie znasz, musisz zresetować klucz YubiKey.", "p_enter_current_puk_or_reset": "Wprowadź aktualny kod PUK. Jeśli go nie znasz, musisz zresetować klucz YubiKey.", - "p_enter_new_fido2_pin": "Wprowadź nowy kod PIN. Musi zawierać co najmniej {length} znaków. Może zawierać litery, cyfry i znaki specjalne.", + "p_enter_new_fido2_pin": null, "@p_enter_new_fido2_pin": { "placeholders": { - "length": {} + "min_length": {}, + "max_length": {} } }, "p_enter_new_fido2_pin_complexity_active": null, "@p_enter_new_fido2_pin_complexity_active": { "placeholders": { - "length": {}, + "min_length": {}, + "max_length": {}, "unique_characters": {}, "common_pin": {} } }, + "s_ep_attestation": null, + "s_ep_attestation_enabled": null, + "s_enable_ep_attestation": null, + "p_enable_ep_attestation_desc": null, + "p_enable_ep_attestation_disable_with_factory_reset": null, "s_pin_required": "Wymagany PIN", "p_pin_required_desc": "Czynność, którą zamierzasz wykonać, wymaga wprowadzenia kodu PIN PIV.", "l_piv_pin_blocked": "Zablokowano, użyj PUK, aby zresetować", "l_piv_pin_puk_blocked": "Zablokowano, konieczny reset do ustawień fabrycznych", - "p_enter_new_piv_pin_puk": "Wprowadź nową {name} do ustawienia. Musi składać się z 6-8 znaków.", + "p_enter_new_piv_pin_puk": null, "@p_enter_new_piv_pin_puk": { "placeholders": { - "name": {} + "name": {}, + "length": {} } }, "p_enter_new_piv_pin_puk_complexity_active": null, "@p_enter_new_piv_pin_puk_complexity_active": { "placeholders": { "name": {}, + "length": {}, "common": {} } }, @@ -326,6 +355,7 @@ "l_warning_default_puk": null, "l_default_pin_used": null, "l_default_puk_used": null, + "l_pin_complexity": null, "@_passwords": {}, "s_password": "Hasło", @@ -335,6 +365,7 @@ "s_show_password": "Pokaż hasło", "s_hide_password": "Ukryj hasło", "l_optional_password_protection": "Opcjonalna ochrona hasłem", + "l_password_protection": null, "s_new_password": "Nowe hasło", "s_current_password": "Aktualne hasło", "s_confirm_password": "Potwierdź hasło", @@ -348,6 +379,7 @@ "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_set_password_first": null, "l_enter_oath_pw": "Wprowadź hasło OATH dla klucza YubiKey", "p_enter_current_password_or_reset": "Wprowadź aktualne hasło. Jeśli go nie znasz, musisz zresetować klucz YubiKey.", "p_enter_new_password": "Wprowadź nowe hasło. Może ono zawierać litery, cyfry i znaki specjalne.", @@ -398,6 +430,10 @@ "message": {} } }, + "l_add_account_password_required": null, + "l_add_account_unlock_required": null, + "l_add_account_already_exists": null, + "l_add_account_func_missing": null, "l_account_name_required": "Twoje konto musi mieć nazwę", "l_name_already_exists": "Ta nazwa już istnieje dla tego wydawcy", "l_account_already_exists": "To konto już istnieje w YubiKey", @@ -406,10 +442,17 @@ "s_pin_account": "Przypnij konto", "s_unpin_account": "Odepnij konto", "s_no_pinned_accounts": "Brak przypiętych kont", + "s_pinned": null, "l_pin_account_desc": "Przechowuj ważne konta razem", "s_rename_account": "Zmień nazwę konta", "l_rename_account_desc": "Edytuj wydawcę/nazwę konta", "s_account_renamed": "Zmieniono nazwę konta", + "l_rename_account_failed": null, + "@l_rename_account_failed": { + "placeholders": { + "message": {} + } + }, "p_rename_will_change_account_displayed": "Spowoduje to zmianę sposobu wyświetlania konta na liście.", "s_delete_account": "Usuń konto", "l_delete_account_desc": "Usuń konto z klucza YubiKey", @@ -474,6 +517,7 @@ } }, "@_fingerprints": {}, + "s_biometrics": null, "l_fingerprint": "Odcisk palca: {label}", "@l_fingerprint": { "placeholders": { @@ -556,6 +600,7 @@ "l_delete_certificate_or_key_desc": null, "l_move_key": null, "l_move_key_desc": null, + "l_change_defaults": null, "s_issuer": "Wydawca", "s_serial": "Nr. seryjny", "s_certificate_fingerprint": "Odcisk palca", @@ -631,7 +676,10 @@ "p_subject_desc": "Nazwa wyróżniająca (DN) sformatowana zgodnie ze specyfikacją RFC 4514.", "l_rfc4514_invalid": "Nieprawidłowy format RFC 4514", "rfc4514_examples": "Przykłady:\nCN=Przykładowa Nazwa\nCN=jkowalski,DC=przyklad,DC=pl", + "s_allow_fingerprint": null, "p_cert_options_desc": "Algorytm klucza do użycia, format wyjściowy i data wygaśnięcia (tylko certyfikat).", + "p_cert_options_bio_desc": null, + "p_key_options_bio_desc": null, "s_overwrite_slot": "Nadpisz slot", "p_overwrite_slot_desc": "Spowoduje to trwałe nadpisanie istniejącej zawartości w slocie {slot}.", "@p_overwrite_slot_desc": { @@ -774,6 +822,7 @@ "s_reset": "Zresetuj", "s_factory_reset": "Ustawienia fabryczne", "l_factory_reset_desc": null, + "l_factory_reset_required": null, "l_oath_application_reset": "Reset funkcji OATH", "l_fido_app_reset": "Reset funkcji FIDO", "l_reset_failed": "Błąd podczas resetowania: {message}", @@ -873,6 +922,12 @@ "s_nfc_dialog_fido_rename_fingerprint": null, "s_nfc_dialog_fido_failure": null, + "@_nfc": {}, + "s_nfc_ready_to_scan": null, + "s_nfc_hold_still": null, + "s_nfc_tap_your_yubikey": null, + "l_nfc_failed_to_scan": null, + "@_ndef": {}, "p_ndef_set_otp": "OTP zostało skopiowane do schowka.", "p_ndef_set_password": "Hasło statyczne zostało skopiowane do schowka.", @@ -881,6 +936,7 @@ "@_key_customization": {}, "s_set_label": null, + "s_set_color": null, "s_change_label": null, "s_color": null, "p_set_will_add_custom_name": null, diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb new file mode 100644 index 000000000..d64b6cffc --- /dev/null +++ b/lib/l10n/app_vi.arb @@ -0,0 +1,946 @@ +{ + "@@locale": "vi", + + "@_readme": { + "notes": [ + "Tất cả chuỗi bắt đầu bằng chữ cái viết hoa.", + "Nhóm chuỗi theo danh mục, nhưng không cần thiết ràng buộc chúng với một phần của ứng dụng nếu chúng có thể được sử dụng lại giữa nhiều phần.", + "Chạy check_strings.py trên tệp .arb để phát hiện vấn đề, điều chỉnh @_lint_rules theo yêu cầu cho từng ngôn ngữ." + ], + "prefixes": { + "s_": "Một hoặc vài từ. Nên đủ ngắn để hiển thị trên nút hoặc tiêu đề.", + "l_": "Một dòng đơn, có thể xuống dòng. Không nên quá một câu, và không kết thúc bằng dấu chấm.", + "p_": "Một hoặc nhiều câu đầy đủ, với dấu chấm câu thích hợp.", + "q_": "Một câu hỏi, kết thúc bằng dấu hỏi chấm." + } + }, + + "@_lint_rules": { + "p_ending_chars": ".!", + "q_ending_chars": "?", + "s_max_words": 4, + "s_max_length": 32 + }, + + "app_name": "Yubico Authenticator", + + "s_save": "Lưu", + "s_cancel": "Hủy", + "s_close": "Đóng", + "s_delete": "Xóa", + "s_move": "Di chuyển", + "s_quit": "Thoát", + "s_enable": "Bật", + "s_enabled": "Đã bật", + "s_disabled": "Đã tắt", + "s_status": "Trạng thái", + "s_unlock": "Mở khóa", + "s_calculate": "Tính toán", + "s_import": "Nhập khẩu", + "s_overwrite": "Ghi đè", + "s_done": null, + "s_label": "Nhãn", + "s_name": "Tên", + "s_usb": "USB", + "s_nfc": "NFC", + "s_options": "Tùy chọn", + "s_details": "Chi tiết", + "s_show_window": "Hiển thị cửa sổ", + "s_hide_window": "Ẩn cửa sổ", + "s_show_navigation": null, + "s_expand_navigation": "Mở rộng điều hướng", + "s_collapse_navigation": "Thu gọn điều hướng", + "s_show_menu": null, + "s_hide_menu": null, + "q_rename_target": "Đổi tên {label}?", + "@q_rename_target": { + "placeholders": { + "label": {} + } + }, + "l_bullet": "• {item}", + "@l_bullet": { + "placeholders": { + "item": {} + } + }, + "s_none": "", + + "s_about": "Giới thiệu", + "s_algorithm": "Thuật toán", + "s_appearance": "Giao diện", + "s_actions": "Hành động", + "s_manage": "Quản lý", + "s_setup": "Cài đặt", + "s_device": "Thiết bị", + "s_application": "Ứng dụng", + "s_settings": "Cài đặt", + "l_settings_desc": "Thay đổi tùy chọn ứng dụng", + "s_certificates": "Chứng chỉ", + "s_security_key": "Khóa bảo mật", + "s_slots": "Khe cắm", + "s_help_and_about": "Trợ giúp và giới thiệu", + "l_help_and_about_desc": "Khắc phục sự cố và hỗ trợ", + "s_help_and_feedback": "Trợ giúp và phản hồi", + "s_home": "Trang chủ", + "s_user_guide": "Hướng dẫn sử dụng", + "s_i_need_help": "Tôi cần trợ giúp", + "s_troubleshooting": "Khắc phục sự cố", + "s_terms_of_use": "Điều khoản sử dụng", + "s_privacy_policy": "Chính sách bảo mật", + "s_open_src_licenses": "Giấy phép mã nguồn mở", + "s_please_wait": "Vui lòng chờ\u2026", + "s_secret_key": "Khóa bí mật", + "s_show_secret_key": "Hiển thị khóa bí mật", + "s_hide_secret_key": "Ẩn khóa bí mật", + "s_private_key": "Khóa riêng tư", + "s_public_key": "Khóa công khai", + "s_invalid_length": "Độ dài không hợp lệ", + "l_invalid_format_allowed_chars": "Định dạng không hợp lệ, các ký tự được phép: {characters}", + "@l_invalid_format_allowed_chars": { + "placeholders": { + "characters": {} + } + }, + "l_invalid_keyboard_character": "Ký tự không hợp lệ cho bàn phím đã chọn", + "s_require_touch": "Yêu cầu chạm", + "q_have_account_info": "Có thông tin tài khoản?", + "s_run_diagnostics": "Chạy chẩn đoán", + "s_log_level": "Mức nhật ký: {level}", + "@s_log_level": { + "placeholders": { + "level": {} + } + }, + "s_character_count": "Số lượng ký tự", + "s_learn_more": "Tìm hiểu thêm", + + "@_language": {}, + "s_language": "Ngôn ngữ", + "l_enable_community_translations": "Bật dịch thuật cộng đồng", + "p_community_translations_desc": "Các bản dịch này được cung cấp và duy trì bởi cộng đồng. Chúng có thể chứa lỗi hoặc chưa hoàn chỉnh.", + + "@_theme": {}, + "s_app_theme": "Chủ đề ứng dụng", + "s_choose_app_theme": "Chọn chủ đề", + "s_system_default": "Mặc định hệ thống", + "s_light_mode": "Chế độ sáng", + "s_dark_mode": "Chế độ tối", + + "@_layout": {}, + "s_list_layout": "Bố cục danh sách", + "s_grid_layout": "Bố cục lưới", + "s_mixed_layout": "Bố cục hỗn hợp", + "s_select_layout": "Chọn bố cục", + + "@_yubikey_selection": {}, + "s_select_to_scan": "Chọn để quét", + "s_hide_device": "Ẩn thiết bị", + "s_show_hidden_devices": "Hiển thị các thiết bị ẩn", + "s_sn_serial": "S/N: {serial}", + "@s_sn_serial": { + "placeholders": { + "serial": {} + } + }, + "s_fw_version": "F/W: {version}", + "@s_fw_version": { + "placeholders": { + "version": {} + } + }, + "@l_serial_number": { + "placeholders": { + "serial": {} + } + }, + "l_serial_number": "Số serial: {serial}", + "@l_firmware_version": { + "placeholders": { + "version": {} + } + }, + "l_firmware_version": "Phiên bản firmware: {version}", + "l_fips_capable": "Hỗ trợ FIPS", + "l_fips_approved": "Đã phê duyệt FIPS", + + "@_yubikey_interactions": {}, + "l_insert_yk": "Chèn YubiKey của bạn", + "l_insert_or_tap_yk": "Chèn hoặc chạm YubiKey", + "l_unplug_yk": "Rút YubiKey của bạn", + "l_reinsert_yk": "Cắm lại YubiKey của bạn", + "l_place_on_nfc_reader": "Đặt YubiKey của bạn lên đầu đọc NFC", + "l_replace_yk_on_reader": "Đặt lại YubiKey của bạn lên đầu đọc", + "l_remove_yk_from_reader": "Rút YubiKey của bạn khỏi đầu đọc NFC", + "p_try_reinsert_yk": "Hãy thử rút và cắm lại YubiKey của bạn.", + "s_touch_required": "Yêu cầu chạm", + "l_touch_button_now": "Chạm vào nút trên YubiKey của bạn ngay bây giờ", + "l_keep_touching_yk": "Giữ chạm vào YubiKey của bạn liên tục\u2026", + + "@_capabilities": {}, + "s_capability_otp": "Yubico OTP", + "s_capability_u2f": "FIDO U2F", + "s_capability_fido2": "FIDO2", + "s_capability_oath": "OATH", + "s_capability_piv": "PIV", + "s_capability_openpgp": "OpenPGP", + "s_capability_hsmauth": "YubiHSM Auth", + + "@_app_configuration": {}, + "s_toggle_applications": "Chuyển đổi ứng dụng", + "s_toggle_interfaces": "Chuyển đổi giao diện", + "p_toggle_applications_desc": "Bật hoặc tắt ứng dụng qua các phương tiện có sẵn.", + "p_toggle_interfaces_desc": "Bật hoặc tắt giao diện USB.", + "l_toggle_applications_desc": "Bật/tắt ứng dụng", + "l_toggle_interfaces_desc": "Bật/tắt giao diện", + "s_reconfiguring_yk": "Đang cấu hình lại YubiKey\u2026", + "s_config_updated": "Đã cập nhật cấu hình", + "l_config_updated_reinsert": "Đã cập nhật cấu hình, rút và cắm lại YubiKey của bạn", + "s_app_not_supported": "Ứng dụng không được hỗ trợ", + "l_app_not_supported_on_yk": "YubiKey được sử dụng không hỗ trợ ứng dụng '{app}'", + "@l_app_not_supported_on_yk": { + "placeholders": { + "app": {} + } + }, + "s_app_disabled": "Ứng dụng đã bị tắt", + "l_app_disabled_desc": "Bật ứng dụng '{app}' trên YubiKey của bạn để truy cập", + "@l_app_disabled_desc": { + "placeholders": { + "app": {} + } + }, + "s_fido_disabled": "FIDO2 đã bị tắt", + "l_webauthn_req_fido2": "WebAuthn yêu cầu ứng dụng FIDO2 được bật trên YubiKey của bạn", + "s_lock_code": "Mã khóa", + "l_wrong_lock_code": "Mã khóa sai", + "s_show_lock_code": "Hiển thị mã khóa", + "s_hide_lock_code": "Ẩn mã khóa", + "p_lock_code_required_desc": "Hành động bạn sắp thực hiện yêu cầu nhập mã khóa cấu hình.", + + + "@_connectivity_issues": {}, + "l_helper_not_responding": "Quá trình Helper không phản hồi", + "l_yk_no_access": "YubiKey này không thể truy cập", + "s_yk_inaccessible": "Thiết bị không thể truy cập", + "l_open_connection_failed": "Không mở được kết nối", + "l_ccid_connection_failed": "Không mở được kết nối thẻ thông minh", + "p_ccid_service_unavailable": "Đảm bảo dịch vụ thẻ thông minh của bạn đang hoạt động.", + "p_pcscd_unavailable": "Đảm bảo pcscd đã được cài đặt và đang chạy.", + "l_no_yk_present": "Không có YubiKey nào hiện diện", + "s_unknown_type": "Loại không xác định", + "s_unknown_device": "Thiết bị không nhận dạng", + "s_restricted_nfc": "Kích hoạt NFC", + "l_deactivate_restricted_nfc": "Cách kích hoạt NFC", + "p_deactivate_restricted_nfc_desc": "Kết nối YubiKey của bạn với bất kỳ nguồn điện USB nào, chẳng hạn như máy tính, trong ít nhất 3 giây.\n\nKhi đã có nguồn điện, NFC sẽ được kích hoạt và sẵn sàng sử dụng.", + "p_deactivate_restricted_nfc_footer": "YubiKey của bạn được trang bị NFC Giới hạn, một tính năng được thiết kế để bảo vệ chống lại thao tác không dây trong quá trình vận chuyển. Điều này có nghĩa là các hoạt động NFC tạm thời bị tắt cho đến khi bạn kích hoạt chúng.", + "s_unsupported_yk": "YubiKey không được hỗ trợ", + "s_yk_not_recognized": "Thiết bị không được nhận diện", + "p_operation_failed_try_again": "Thao tác không thành công, vui lòng thử lại.", + "l_configuration_unsupported": null, + "p_scp_unsupported": null, + + "@_general_errors": {}, + "l_error_occurred": "Đã xảy ra lỗi", + "s_application_error": "Lỗi ứng dụng", + "l_import_error": "Lỗi nhập", + "l_file_not_found": "Tệp không tìm thấy", + "l_file_too_big": "Kích thước tệp quá lớn", + "l_filesystem_error": "Lỗi hệ thống tệp", + + "@_pins": {}, + "s_pin": "Mã PIN", + "s_puk": "Mã PUK", + "s_set_pin": "Cài đặt Mã PIN", + "s_change_pin": "Thay đổi Mã PIN", + "s_change_puk": "Thay đổi Mã PUK", + "s_show_pin": "Hiển thị Mã PIN", + "s_hide_pin": "Ẩn Mã PIN", + "s_show_puk": "Hiển thị Mã PUK", + "s_hide_puk": "Ẩn Mã PUK", + "s_current_pin": "Mã PIN hiện tại", + "s_current_puk": "Mã PUK hiện tại", + "s_new_pin": "Mã PIN mới", + "s_new_puk": "Mã PUK mới", + "s_confirm_pin": "Xác nhận Mã PIN", + "s_confirm_puk": "Xác nhận Mã PUK", + "s_unblock_pin": "Bỏ khóa Mã PIN", + "l_pin_mismatch": "Mã PIN không khớp", + "l_puk_mismatch": "Mã PUK không khớp", + "s_pin_set": "Mã PIN đã được cài đặt", + "s_puk_set": "Mã PUK đã được cài đặt", + "l_set_pin_failed": "Cài đặt Mã PIN thất bại: {message}", + "@l_set_pin_failed": { + "placeholders": { + "message": {} + } + }, + "l_attempts_remaining": "Còn {retries} lần thử", + "@l_attempts_remaining": { + "placeholders": { + "retries": {} + } + }, + "l_wrong_pin_attempts_remaining": "Mã PIN sai, còn {retries} lần thử", + "@l_wrong_pin_attempts_remaining": { + "placeholders": { + "retries": {} + } + }, + "l_wrong_puk_attempts_remaining": "Mã PUK sai, còn {retries} lần thử", + "@l_wrong_puk_attempts_remaining": { + "placeholders": { + "retries": {} + } + }, + "s_fido_pin_protection": "Bảo vệ Mã PIN FIDO", + "s_pin_change_required": "Cần thay đổi Mã PIN", + "l_enter_fido2_pin": "Nhập Mã PIN FIDO2 cho YubiKey của bạn", + "l_pin_blocked_reset": "Mã PIN bị khóa; khôi phục cài đặt gốc ứng dụng FIDO", + "l_pin_blocked": "Mã PIN bị khóa", + "l_set_pin_first": "Cần có Mã PIN", + "l_unlock_pin_first": "Mở khóa bằng Mã PIN", + "l_pin_soft_locked": "Mã PIN đã bị khóa cho đến khi YubiKey được tháo ra và cắm lại", + "l_pin_change_required_desc": "Cần thiết lập một Mã PIN mới trước khi bạn có thể sử dụng ứng dụng này", + "p_enter_current_pin_or_reset": "Nhập Mã PIN hiện tại của bạn. Nếu bạn không biết Mã PIN, bạn sẽ cần phải mở khóa bằng Mã PUK hoặc khôi phục cài đặt gốc YubiKey.", + "p_enter_current_pin_or_reset_no_puk": "Nhập Mã PIN hiện tại của bạn. Nếu bạn không biết Mã PIN, bạn sẽ cần phải khôi phục cài đặt gốc YubiKey.", + "p_enter_current_puk_or_reset": "Nhập Mã PUK hiện tại của bạn. Nếu bạn không biết Mã PUK, bạn sẽ cần phải khôi phục cài đặt gốc YubiKey.", + "p_enter_new_fido2_pin": "Nhập Mã PIN mới của bạn. Mã PIN phải có độ dài từ {min_length} đến {max_length} ký tự và có thể chứa chữ cái, số và ký tự đặc biệt.", + "@p_enter_new_fido2_pin": { + "placeholders": { + "min_length": {}, + "max_length": {} + } + }, + "p_enter_new_fido2_pin_complexity_active": "Nhập Mã PIN mới của bạn. Mã PIN phải có độ dài từ {min_length} đến {max_length} ký tự, chứa ít nhất {unique_characters} ký tự độc đáo, và không phải là Mã PIN thường dùng, như \"{common_pin}\". Nó có thể chứa chữ cái, số và ký tự đặc biệt.", + "@p_enter_new_fido2_pin_complexity_active": { + "placeholders": { + "min_length": {}, + "max_length": {}, + "unique_characters": {}, + "common_pin": {} + } + }, + "s_ep_attestation": "Xác thực Doanh Nghiệp", + "s_ep_attestation_enabled": "Xác thực Doanh Nghiệp đã được kích hoạt", + "s_enable_ep_attestation": "Kích hoạt Xác thực Doanh Nghiệp", + "p_enable_ep_attestation_desc": "Điều này sẽ kích hoạt Xác thực Doanh Nghiệp, cho phép các miền được ủy quyền xác định YubiKey của bạn một cách duy nhất.", + "p_enable_ep_attestation_disable_with_factory_reset": "Khi đã kích hoạt, Xác thực Doanh Nghiệp chỉ có thể bị tắt bằng cách thực hiện khôi phục cài đặt gốc FIDO.", + "s_pin_required": "Cần Mã PIN", + "p_pin_required_desc": "Hành động bạn sắp thực hiện yêu cầu phải nhập Mã PIN PIV.", + "l_piv_pin_blocked": "Bị khóa, sử dụng PUK để khôi phục", + "l_piv_pin_puk_blocked": "Bị khóa, cần khôi phục cài đặt gốc", + "p_enter_new_piv_pin_puk": "Nhập một {name} mới để thiết lập. Phải có ít nhất {length} ký tự.", + "@p_enter_new_piv_pin_puk": { + "placeholders": { + "name": {}, + "length": {} + } + }, + "p_enter_new_piv_pin_puk_complexity_active": "Nhập một {name} mới để thiết lập. Phải có ít nhất {length} ký tự, chứa ít nhất 2 ký tự độc đáo, và không phải là {name} thường dùng, như \"{common}\".", + "@p_enter_new_piv_pin_puk_complexity_active": { + "placeholders": { + "name": {}, + "length": {}, + "common": {} + } + }, + "p_pin_puk_complexity_failure": "Mới {name} không đáp ứng yêu cầu độ phức tạp.", + "@p_pin_puk_complexity_failure": { + "placeholders": { + "name": {} + } + }, + "l_warning_default_pin": "Cảnh báo: Đã sử dụng Mã PIN mặc định", + "l_warning_default_puk": "Cảnh báo: Đã sử dụng Mã PUK mặc định", + "l_default_pin_used": "Đã sử dụng Mã PIN mặc định", + "l_default_puk_used": "Đã sử dụng Mã PUK mặc định", + "l_pin_complexity": "Yêu cầu độ phức tạp của Mã PIN", + + "@_passwords": {}, + "s_password": "Mật khẩu", + "s_manage_password": "Quản lý mật khẩu", + "s_set_password": "Đặt mật khẩu", + "s_password_set": "Mật khẩu đã được đặt", + "s_show_password": "Hiển thị mật khẩu", + "s_hide_password": "Ẩn mật khẩu", + "l_optional_password_protection": "Bảo vệ mật khẩu tùy chọn", + "l_password_protection": "Bảo vệ mật khẩu của các tài khoản", + "s_new_password": "Mật khẩu mới", + "s_current_password": "Mật khẩu hiện tại", + "s_confirm_password": "Xác nhận mật khẩu", + "l_password_mismatch": "Mật khẩu không khớp", + "s_wrong_password": "Mật khẩu sai", + "s_remove_password": "Xóa mật khẩu", + "s_password_removed": "Mật khẩu đã bị xóa", + "s_remember_password": "Nhớ mật khẩu", + "s_clear_saved_password": "Xóa mật khẩu đã lưu", + "s_password_forgotten": "Quên mật khẩu", + "l_keystore_unavailable": "Keystore của hệ điều hành không khả dụng", + "l_remember_pw_failed": "Không thể nhớ mật khẩu", + "l_unlock_first": "Mở khóa bằng mật khẩu", + "l_set_password_first": "Đặt mật khẩu", + "l_enter_oath_pw": "Nhập mật khẩu OATH cho YubiKey của bạn", + "p_enter_current_password_or_reset": "Nhập mật khẩu hiện tại của bạn. Nếu bạn không biết mật khẩu của mình, bạn sẽ cần phải khôi phục cài đặt gốc YubiKey.", + "p_enter_new_password": "Nhập mật khẩu mới của bạn. Mật khẩu có thể chứa chữ cái, số và ký tự đặc biệt.", + + "@_management_key": {}, + "s_management_key": "Khóa quản lý", + "s_current_management_key": "Khóa quản lý hiện tại", + "s_new_management_key": "Khóa quản lý mới", + "l_change_management_key": "Thay đổi khóa quản lý", + "p_change_management_key_desc": "Thay đổi khóa quản lý của bạn. Bạn có thể chọn cho phép sử dụng Mã PIN thay vì khóa quản lý.", + "l_management_key_changed": "Khóa quản lý đã được thay đổi", + "l_default_key_used": "Đã sử dụng khóa quản lý mặc định", + "s_generate_random": "Tạo ngẫu nhiên", + "s_use_default": "Sử dụng mặc định", + "l_warning_default_key": "Cảnh báo: Đã sử dụng khóa mặc định", + "s_protect_key": "Bảo vệ bằng Mã PIN", + "l_pin_protected_key": "Mã PIN có thể được sử dụng thay thế", + "l_wrong_key": "Khóa sai", + "l_unlock_piv_management": "Mở khóa quản lý PIV", + "p_unlock_piv_management_desc": "Hành động bạn sắp thực hiện yêu cầu khóa quản lý PIV. Cung cấp khóa này để mở khóa chức năng quản lý cho phiên làm việc này.", + + "@_oath_accounts": {}, + "l_account": "Tài khoản: {label}", + "@l_account": { + "placeholders": { + "label": {} + } + }, + "s_accounts": "Các tài khoản", + "s_no_accounts": "Không có tài khoản", + "l_results_for": "Kết quả cho \"{query}\"", + "@l_results_for": { + "placeholders": { + "query": {} + } + }, + "l_authenticator_get_started": "Bắt đầu với các tài khoản OTP", + "l_no_accounts_desc": "Thêm tài khoản vào YubiKey của bạn từ bất kỳ nhà cung cấp dịch vụ nào hỗ trợ OATH TOTP/HOTP", + "s_add_account": "Thêm tài khoản", + "s_add_accounts": "Thêm tài khoản(s)", + "p_add_description": "Để quét mã QR, hãy chắc chắn rằng mã QR được hiển thị đầy đủ trên màn hình và nhấn nút bên dưới. Bạn cũng có thể kéo một hình ảnh đã lưu từ thư mục vào hộp thoại này. Nếu bạn có thông tin tài khoản dưới dạng văn bản, hãy sử dụng nhập thủ công thay vào đó.", + "l_drop_qr_description": "Kéo mã QR để thêm tài khoản(s)", + "s_add_manually": "Thêm thủ công", + "s_account_added": "Tài khoản đã được thêm", + "l_account_add_failed": "Thêm tài khoản thất bại: {message}", + "@l_account_add_failed": { + "placeholders": { + "message": {} + } + }, + "l_add_account_password_required": null, + "l_add_account_unlock_required": null, + "l_add_account_already_exists": null, + "l_add_account_func_missing": null, + "l_account_name_required": "Tài khoản của bạn phải có tên", + "l_name_already_exists": "Tên này đã tồn tại cho nhà phát hành", + "l_account_already_exists": "Tài khoản này đã tồn tại trên YubiKey", + "l_invalid_character_issuer": "Ký tự không hợp lệ: ':' không được phép trong nhà phát hành", + "l_select_accounts": "Chọn tài khoản(s) để thêm vào YubiKey", + "s_pin_account": "Ghim tài khoản", + "s_unpin_account": "Bỏ ghim tài khoản", + "s_no_pinned_accounts": "Không có tài khoản nào được ghim", + "s_pinned": "Đã ghim", + "l_pin_account_desc": "Giữ các tài khoản quan trọng của bạn cùng nhau", + "s_rename_account": "Đổi tên tài khoản", + "l_rename_account_desc": "Chỉnh sửa nhà phát hành/tên của tài khoản", + "s_account_renamed": "Tài khoản đã được đổi tên", + "l_rename_account_failed": null, + "@l_rename_account_failed": { + "placeholders": { + "message": {} + } + }, + "p_rename_will_change_account_displayed": "Điều này sẽ thay đổi cách tài khoản được hiển thị trong danh sách.", + "s_delete_account": "Xóa tài khoản", + "l_delete_account_desc": "Xóa tài khoản khỏi YubiKey của bạn", + "s_account_deleted": "Tài khoản đã bị xóa", + "p_warning_delete_account": "Cảnh báo! Hành động này sẽ xóa tài khoản khỏi YubiKey của bạn.", + "p_warning_disable_credential": "Bạn sẽ không còn có thể tạo OTP cho tài khoản này. Hãy chắc chắn vô hiệu hóa thông tin đăng nhập này từ trang web để tránh bị khóa tài khoản của bạn.", + "s_account_name": "Tên tài khoản", + "s_search_accounts": "Tìm kiếm tài khoản", + "l_accounts_used": "{used} trong {capacity} tài khoản đã sử dụng", + "@l_accounts_used": { + "placeholders": { + "used": {}, + "capacity": {} + } + }, + "s_num_digits": "{num} chữ số", + "@s_num_digits": { + "placeholders": { + "num": {} + } + }, + "s_num_sec": "{num} giây", + "@s_num_sec": { + "placeholders": { + "num": {} + } + }, + "s_issuer_optional": "Nhà phát hành (tùy chọn)", + "s_counter_based": "Dựa trên bộ đếm", + "s_time_based": "Dựa trên thời gian", + "l_copy_code_desc": "Dán mã vào ứng dụng khác", + "l_calculate_code_desc": "Lấy mã mới từ YubiKey của bạn", + + "@_fido_credentials": {}, + "s_rp_id": "RP ID", + "s_user_id": "User ID", + "s_credential_id": "Credential ID", + "s_display_name": "Tên hiển thị", + "s_user_name": "Tên người dùng", + "l_passkey": "Mã bảo mật: {label}", + "@l_passkey": { + "placeholders": { + "label": {} + } + }, + "s_passkeys": "Mã bảo mật", + "s_no_passkeys": "Không có mã bảo mật", + "l_ready_to_use": "Sẵn sàng sử dụng", + "l_register_sk_on_websites": "Đăng ký làm Security Key trên các trang web", + "l_no_discoverable_accounts": "Không có mã bảo mật đã lưu", + "p_non_passkeys_note": "Có thể có thông tin đăng nhập không phải là mã bảo mật, nhưng không thể được liệt kê.", + "s_delete_passkey": "Xóa mã bảo mật", + "l_delete_passkey_desc": "Xóa mã bảo mật khỏi YubiKey", + "s_passkey_deleted": "Mã bảo mật đã bị xóa", + "p_warning_delete_passkey": "Điều này sẽ xóa mã bảo mật khỏi YubiKey của bạn.", + "s_search_passkeys": "Tìm kiếm mã bảo mật", + "p_passkeys_used": "{used} trong {max} mã bảo mật đã sử dụng.", + "@p_passkeys_used": { + "placeholders": { + "used": {}, + "max": {} + } + }, + "@_fingerprints": {}, + "s_biometrics": "Sinh trắc học", + "l_fingerprint": "Vân tay: {label}", + "@l_fingerprint": { + "placeholders": { + "label": {} + } + }, + "s_fingerprints": "Vân tay", + "l_fingerprint_captured": "Vân tay đã được ghi lại thành công!", + "s_fingerprint_added": "Vân tay đã được thêm", + "l_adding_fingerprint_failed": "Lỗi khi thêm vân tay: {message}", + "@l_adding_fingerprint_failed": { + "placeholders": {} + }, + "l_setting_name_failed": "Lỗi khi thiết lập tên: {message}", + "@l_setting_name_failed": { + "placeholders": { + "message": {} + } + }, + "s_setup_fingerprints": "Thiết lập vân tay", + "p_setup_fingerprints_desc": "Cần thiết lập vân tay trước khi có thể sử dụng khóa.", + "s_add_fingerprint": "Thêm vân tay", + "s_delete_fingerprint": "Xóa vân tay", + "l_delete_fingerprint_desc": "Xóa vân tay khỏi YubiKey", + "s_fingerprint_deleted": "Vân tay đã bị xóa", + "p_warning_delete_fingerprint": "Điều này sẽ xóa vân tay khỏi YubiKey của bạn.", + "s_fingerprints_get_started": "Bắt đầu với vân tay", + "p_set_fingerprints_desc": "Trước khi có thể đăng ký vân tay, cần thiết lập mã PIN.", + "l_no_fps_added": "Chưa có vân tay nào được thêm", + "s_rename_fp": "Đổi tên vân tay", + "l_rename_fp_desc": "Thay đổi tên", + "s_fingerprint_renamed": "Vân tay đã được đổi tên", + "l_rename_fp_failed": "Lỗi khi đổi tên: {message}", + "@l_rename_fp_failed": { + "placeholders": { + "message": {} + } + }, + "l_add_one_or_more_fps": "Thêm một hoặc nhiều (tối đa năm) vân tay", + "l_fingerprints_used": "{used}/5 vân tay đã đăng ký", + "@l_fingerprints_used": { + "placeholders": { + "used": {} + } + }, + "p_press_fingerprint_begin": "Ấn ngón tay của bạn vào YubiKey để bắt đầu.", + "p_will_change_label_fp": "Điều này sẽ thay đổi tên của vân tay.", + "l_name_fingerprint": "Đặt tên cho vân tay này", + + "@_fido_errors": {}, + "l_user_action_timeout_error": "Thất bại do không hoạt động của người dùng", + "l_wrong_inserted_yk_error": "YubiKey được cắm lại không khớp với thiết bị ban đầu", + "l_failed_connecting_to_fido": "Kết nối với giao diện FIDO thất bại", + + "@_certificates": {}, + "s_certificate": "Chứng chỉ", + "s_csr": "CSR", + "s_subject": "Chủ thể", + "l_export_csr_file": "Lưu CSR vào tập tin", + "l_export_public_key": "Xuất khóa công khai", + "l_export_public_key_file": "Lưu khóa công khai vào tập tin", + "l_export_public_key_desc": "Xuất khóa công khai vào một tập tin", + "l_public_key_exported": "Khóa công khai đã được xuất", + "l_export_certificate": "Xuất chứng chỉ", + "l_export_certificate_file": "Xuất chứng chỉ vào tập tin", + "l_export_certificate_desc": "Xuất chứng chỉ vào một tập tin", + "l_certificate_exported": "Chứng chỉ đã được xuất", + "l_select_import_file": "Chọn tập tin để nhập", + "l_import_file": "Nhập tập tin", + "l_import_desc": "Nhập khóa và/hoặc chứng chỉ", + "l_import_nothing": "Không có gì để nhập", + "l_importing_file": "Đang nhập tập tin\u2026", + "s_file_imported": "Tập tin đã được nhập", + "l_unsupported_key_type": "Loại khóa không được hỗ trợ", + "l_delete_certificate": "Xóa chứng chỉ", + "l_delete_certificate_desc": "Xóa chứng chỉ khỏi YubiKey của bạn", + "l_delete_key": "Xóa khóa", + "l_delete_key_desc": "Xóa khóa khỏi YubiKey của bạn", + "l_delete_certificate_or_key": "Xóa chứng chỉ/khóa", + "l_delete_certificate_or_key_desc": "Xóa chứng chỉ hoặc khóa khỏi YubiKey", + "l_move_key": "Di chuyển khóa", + "l_move_key_desc": "Di chuyển một khóa từ khe PIV này sang khe khác", + "l_change_defaults": null, + "s_issuer": "Nhà phát hành", + "s_serial": "Số serial", + "s_certificate_fingerprint": "Dấu vân tay", + "s_valid_from": "Có hiệu lực từ", + "s_valid_to": "Có hiệu lực đến", + "l_no_certificate": "Chưa có chứng chỉ nào được tải", + "l_key_no_certificate": "Khóa không có chứng chỉ được tải", + "s_generate_key": "Tạo khóa", + "l_generate_desc": "Tạo một chứng chỉ hoặc CSR mới", + "p_generate_desc": "Điều này sẽ tạo một khóa mới trên YubiKey trong khe PIV {slot}. Khóa công khai sẽ được lưu vào tập tin, nhúng vào một chứng chỉ tự ký được lưu trữ trên YubiKey, hoặc trong một yêu cầu ký chứng chỉ (CSR) được lưu vào tập tin.", + "@p_generate_desc": { + "placeholders": { + "slot": {} + } + }, + "s_private_key_generated": "Khóa riêng đã được tạo", + "p_select_what_to_delete": "Chọn những gì để xóa từ khe.", + "p_warning_delete_certificate": "Cảnh báo! Hành động này sẽ xóa chứng chỉ khỏi YubiKey của bạn.", + "p_warning_delete_key": "Cảnh báo! Hành động này sẽ xóa khóa riêng khỏi YubiKey của bạn.", + "p_warning_delete_certificate_and_key": "Cảnh báo! Hành động này sẽ xóa chứng chỉ và khóa riêng khỏi YubiKey của bạn.", + "q_delete_certificate_confirm": "Xóa chứng chỉ trong khe PIV {slot}?", + "@q_delete_certificate_confirm": { + "placeholders": { + "slot": {} + } + }, + "q_delete_key_confirm": "Xóa khóa riêng trong khe PIV {slot}?", + "@q_delete_key_confirm": { + "placeholders": { + "slot": {} + } + }, + "q_delete_certificate_and_key_confirm": "Xóa chứng chỉ và khóa riêng trong khe PIV {slot}?", + "@q_delete_certificate_and_key_confirm": { + "placeholders": { + "slot": {} + } + }, + "l_certificate_deleted": "Chứng chỉ đã bị xóa", + "l_key_deleted": "Khóa đã bị xóa", + "l_certificate_and_key_deleted": "Chứng chỉ và khóa đã bị xóa", + "l_include_certificate": "Bao gồm chứng chỉ", + "l_select_destination_slot": "Chọn khe đích", + "q_move_key_confirm": "Di chuyển khóa riêng trong khe PIV {from_slot}?", + "@q_move_key_confirm": { + "placeholders": { + "from_slot": {} + } + }, + "q_move_key_to_slot_confirm": "Di chuyển khóa riêng trong khe PIV {from_slot} đến khe {to_slot}?", + "@q_move_key_to_slot_confirm": { + "placeholders": { + "from_slot": {}, + "to_slot": {} + } + }, + "q_move_key_and_certificate_to_slot_confirm": "Di chuyển khóa riêng và chứng chỉ trong khe PIV {from_slot} đến khe {to_slot}?", + "@q_move_key_and_certificate_to_slot_confirm": { + "placeholders": { + "from_slot": {}, + "to_slot": {} + } + }, + "p_password_protected_file": "Tập tin đã chọn được bảo vệ bằng mật khẩu. Nhập mật khẩu để tiếp tục.", + "p_import_items_desc": "Các mục sau sẽ được nhập vào khe PIV {slot}.", + "@p_import_items_desc": { + "placeholders": { + "slot": {} + } + }, + "l_key_moved": "Khóa đã được di chuyển", + "l_key_and_certificate_moved": "Khóa và chứng chỉ đã được di chuyển", + "p_subject_desc": "Tên phân biệt (DN) được định dạng theo tiêu chuẩn RFC 4514.", + "l_rfc4514_invalid": "Định dạng RFC 4514 không hợp lệ", + "rfc4514_examples": "Ví dụ:\nCN=Tên Ví dụ\nCN=jsmith,DC=example,DC=net", + "s_allow_fingerprint": "Cho phép vân tay", + "p_cert_options_desc": "Thuật toán khóa để sử dụng, định dạng đầu ra và ngày hết hạn (chứng chỉ chỉ).", + "p_cert_options_bio_desc": "Thuật toán khóa để sử dụng, định dạng đầu ra, ngày hết hạn (chứng chỉ chỉ) và nếu sinh trắc học có thể được sử dụng thay cho PIN.", + "p_key_options_bio_desc": "Cho phép sử dụng sinh trắc học thay cho PIN.", + "s_overwrite_slot": "Ghi đè khe", + "p_overwrite_slot_desc": "Điều này sẽ ghi đè vĩnh viễn nội dung hiện có trong khe {slot}.", + "@p_overwrite_slot_desc": { + "placeholders": { + "slot": {} + } + }, + "l_overwrite_cert": "Chứng chỉ sẽ bị ghi đè", + "l_overwrite_key": "Khóa riêng sẽ bị ghi đè", + "l_overwrite_key_maybe": "Bất kỳ khóa riêng nào hiện có trong khe sẽ bị ghi đè", + + "@_piv_slots": {}, + "s_slot_display_name": "{name} ({hexid})", + "@s_slot_display_name": { + "placeholders": { + "name": {}, + "hexid": {} + } + }, + "s_slot_9a": "Xác thực", + "s_slot_9c": "Chữ ký số", + "s_slot_9d": "Quản lý khóa", + "s_slot_9e": "Xác thực thẻ", + "s_retired_slot": "Quản lý khóa đã nghỉ hưu", + + "@_otp_slots": {}, + "s_otp_slot_one": "Chạm ngắn", + "s_otp_slot_two": "Chạm lâu", + "l_otp_slot_empty": "Khe trống", + "l_otp_slot_configured": "Khe đã được cấu hình", + + "@_otp_slot_configurations": {}, + "l_yubiotp_desc": "Cài đặt thông tin xác thực Yubico OTP", + "s_challenge_response": "Thách thức-phản hồi", + "l_challenge_response_desc": "Cài đặt thông tin xác thực thách thức-phản hồi", + "s_static_password": "Mật khẩu tĩnh", + "l_static_password_desc": "Cấu hình mật khẩu tĩnh", + "s_hotp": "OATH-HOTP", + "l_hotp_desc": "Cài đặt thông tin xác thực dựa trên HMAC-SHA1", + "s_public_id": "ID công khai", + "s_private_id": "ID riêng tư", + "s_use_serial": "Sử dụng số sê-ri", + "l_select_file": "Chọn tệp", + "l_no_export_file": "Không có tệp xuất khẩu", + "s_no_export": "Không xuất khẩu", + "s_export": "Xuất khẩu", + "l_export_configuration_file": "Xuất cấu hình ra tệp", + "l_exported_can_be_uploaded_at": "Các thông tin xác thực đã xuất có thể được tải lên tại {url}", + "@_export_can_be_uploaded_at": { + "placeholders": { + "url": {} + } + }, + + "@_otp_slot_actions": {}, + "s_delete_slot": "Xóa thông tin xác thực", + "l_delete_slot_desc": "Xóa thông tin xác thực trong khe", + "p_warning_delete_slot_configuration": "Cảnh báo! Hành động này sẽ xóa vĩnh viễn thông tin xác thực khỏi khe {slot_id}.", + "@p_warning_delete_slot_configuration": { + "placeholders": { + "slot_id": {} + } + }, + "l_slot_deleted": "Thông tin xác thực đã bị xóa", + "s_swap": "Hoán đổi", + "s_swap_slots": "Hoán đổi các khe", + "l_swap_slots_desc": "Hoán đổi chạm ngắn/dài", + "p_swap_slots_desc": "Điều này sẽ hoán đổi cấu hình của hai khe.", + "l_slots_swapped": "Cấu hình khe đã được hoán đổi", + "l_slot_credential_configured": "Thông tin xác thực {type} đã được cấu hình", + "@l_slot_credential_configured": { + "placeholders": { + "type": {} + } + }, + "l_slot_credential_configured_and_exported": "Thông tin xác thực {type} đã được cấu hình và xuất khẩu ra {file}", + "@l_slot_credential_configured_and_exported": { + "placeholders": { + "type": {}, + "file": {} + } + }, + "s_append_enter": "Thêm ⏎", + "l_append_enter_desc": "Thêm một cú nhấn Enter sau khi phát OTP", + + "@_otp_errors": {}, + "p_otp_swap_error": "Không thể hoán đổi khe! Đảm bảo YubiKey không có quyền truy cập hạn chế.", + "l_wrong_access_code": "Mã truy cập sai", + + "@_otp_access_code": {}, + "s_access_code": "Mã truy cập", + "s_show_access_code": "Hiển thị mã truy cập", + "s_hide_access_code": "Ẩn mã truy cập", + "p_enter_access_code": "Nhập mã truy cập cho khe {slot}.", + "@p_enter_access_code": { + "placeholders": { + "slot": {} + } + }, + + + "@_permissions": {}, + "s_enable_nfc": "Bật NFC", + "s_request_access": "Yêu cầu quyền truy cập", + "s_permission_denied": "Quyền truy cập bị từ chối", + "l_elevating_permissions": "Nâng cao quyền\u2026", + "s_review_permissions": "Xem xét quyền", + "s_open_windows_settings": "Mở cài đặt Windows", + "l_admin_privileges_required": "Yêu cầu quyền quản trị", + "p_elevated_permissions_required": "Quản lý thiết bị này yêu cầu quyền quản trị. Bạn cũng có thể sử dụng Cài đặt Windows để quản lý cấu hình FIDO.", + "p_webauthn_elevated_permissions_required": "Quản lý WebAuthn yêu cầu quyền quản trị. Bạn cũng có thể sử dụng Cài đặt Windows để quản lý cấu hình FIDO.", + "l_ms_store_permission_note": "Phiên bản ứng dụng từ Microsoft Store có thể không nâng cao quyền.", + "p_need_camera_permission": "Yubico Authenticator cần quyền truy cập Camera để quét mã QR.", + + "@_qr_codes": {}, + "s_qr_scan": "Quét mã QR", + "l_invalid_qr": "Mã QR không hợp lệ", + "l_qr_not_found": "Không tìm thấy mã QR", + "l_qr_file_too_large": "Tệp quá lớn (tối đa {max})", + "@l_qr_file_too_large": { + "placeholders": { + "max": {} + } + }, + "l_qr_invalid_image_file": "Tệp hình ảnh không hợp lệ", + "l_qr_select_file": "Chọn tệp có mã QR", + "l_qr_not_read": "Không thể đọc mã QR: {message}", + "@l_qr_not_read": { + "placeholders": { + "message": {} + } + }, + "l_point_camera_scan": "Chỉa camera vào mã QR để quét", + "q_want_to_scan": "Bạn có muốn quét không?", + "q_no_qr": "Không có mã QR?", + "s_enter_manually": "Nhập thủ công", + "s_read_from_file": "Đọc từ tệp", + + "@_factory_reset": {}, + "s_reset": "Đặt lại", + "s_factory_reset": "Đặt lại nhà máy", + "l_factory_reset_desc": "Khôi phục các cài đặt mặc định của YubiKey", + "l_factory_reset_required": "Yêu cầu đặt lại nhà máy", + "l_oath_application_reset": "Đặt lại ứng dụng OATH", + "l_fido_app_reset": "Đặt lại ứng dụng FIDO", + "l_reset_failed": "Lỗi khi thực hiện đặt lại: {message}", + "@l_reset_failed": { + "placeholders": { + "message": {} + } + }, + "l_piv_app_reset": "Đặt lại ứng dụng PIV", + "p_factory_reset_an_app": "Đặt lại một ứng dụng trên YubiKey của bạn.", + "p_factory_reset_desc": "Dữ liệu được lưu trữ trong nhiều ứng dụng trên YubiKey, một số trong số đó có thể được đặt lại nhà máy độc lập với nhau.\n\nChọn một ứng dụng ở trên để đặt lại.", + "p_warning_factory_reset": "Cảnh báo! Điều này sẽ xóa vĩnh viễn tất cả các tài khoản OATH TOTP/HOTP khỏi YubiKey của bạn.", + "p_warning_disable_credentials": "Các thông tin xác thực OATH của bạn, cũng như bất kỳ mật khẩu nào đã đặt, sẽ bị xóa khỏi YubiKey này. Hãy đảm bảo vô hiệu hóa chúng từ các trang web tương ứng để tránh bị khóa tài khoản của bạn.", + "p_warning_deletes_accounts": "Cảnh báo! Điều này sẽ xóa vĩnh viễn tất cả các tài khoản U2F và FIDO2, bao gồm cả passkeys, khỏi YubiKey của bạn.", + "p_warning_disable_accounts": "Các thông tin xác thực của bạn, cũng như bất kỳ PIN nào đã đặt, sẽ bị xóa khỏi YubiKey này. Hãy đảm bảo vô hiệu hóa chúng từ các trang web tương ứng để tránh bị khóa tài khoản của bạn.", + "p_warning_piv_reset": "Cảnh báo! Tất cả dữ liệu được lưu trữ cho PIV sẽ bị xóa vĩnh viễn khỏi YubiKey của bạn.", + "p_warning_piv_reset_desc": "Điều này bao gồm các khóa riêng và chứng chỉ. PIN, PUK và khóa quản lý của bạn sẽ được đặt lại về giá trị mặc định của nhà máy.", + "p_warning_global_reset": "Cảnh báo! Điều này sẽ xóa vĩnh viễn tất cả dữ liệu đã lưu, bao gồm các thông tin xác thực, khỏi YubiKey của bạn.", + "p_warning_global_reset_desc": "Đặt lại các ứng dụng trên YubiKey của bạn về cài đặt mặc định của nhà máy. PIN sẽ được đặt lại về giá trị mặc định của nhà máy, và các dấu vân tay đã đăng ký sẽ bị xóa. Tất cả các khóa, chứng chỉ hoặc các thông tin xác thực khác sẽ bị xóa vĩnh viễn.", + + "@_copy_to_clipboard": {}, + "l_copy_to_clipboard": "Sao chép vào clipboard", + "s_code_copied": "Mã đã được sao chép", + "l_code_copied_clipboard": "Mã đã được sao chép vào clipboard", + "s_copy_log": "Sao chép nhật ký", + "l_log_copied": "Nhật ký đã được sao chép vào clipboard", + "l_diagnostics_copied": "Dữ liệu chẩn đoán đã được sao chép vào clipboard", + "p_target_copied_clipboard": "{label} đã được sao chép vào clipboard.", + "@p_target_copied_clipboard": { + "placeholders": { + "label": {} + } + }, + + "@_custom_icons": {}, + "s_custom_icons": "Biểu tượng tùy chỉnh", + "l_set_icons_for_accounts": "Đặt biểu tượng cho các tài khoản", + "p_custom_icons_description": "Các gói biểu tượng có thể làm cho các tài khoản của bạn dễ phân biệt hơn với các logo và màu sắc quen thuộc.", + "s_replace_icon_pack": "Thay thế gói biểu tượng", + "l_loading_icon_pack": "Đang tải gói biểu tượng\u2026", + "s_load_icon_pack": "Tải gói biểu tượng", + "s_remove_icon_pack": "Gỡ gói biểu tượng", + "l_icon_pack_removed": "Gói biểu tượng đã được gỡ bỏ", + "l_remove_icon_pack_failed": "Lỗi khi gỡ bỏ gói biểu tượng", + "s_choose_icon_pack": "Chọn gói biểu tượng", + "l_icon_pack_imported": "Gói biểu tượng đã được nhập khẩu", + "l_import_icon_pack_failed": "Lỗi khi nhập khẩu gói biểu tượng: {message}", + "@l_import_icon_pack_failed": { + "placeholders": { + "message": {} + } + }, + "l_invalid_icon_pack": "Gói biểu tượng không hợp lệ", + "l_icon_pack_copy_failed": "Không thể sao chép các tệp gói biểu tượng", + + "@_android_settings": {}, + "s_nfc_options": "Tùy chọn NFC", + "l_on_yk_nfc_tap": "Khi chạm NFC trên YubiKey", + "l_do_nothing": "Không làm gì", + "l_launch_ya": "Khởi chạy Yubico Authenticator", + "l_copy_otp_clipboard": "Sao chép OTP vào clipboard", + "l_launch_and_copy_otp": "Khởi chạy ứng dụng và sao chép OTP", + "l_kbd_layout_for_static": "Bố cục bàn phím (cho mật khẩu tĩnh)", + "s_choose_kbd_layout": "Chọn bố cục bàn phím", + "l_bypass_touch_requirement": "Bỏ qua yêu cầu chạm", + "l_bypass_touch_requirement_on": "Các tài khoản yêu cầu chạm sẽ tự động được hiển thị qua NFC", + "l_bypass_touch_requirement_off": "Các tài khoản yêu cầu chạm cần thêm một lần chạm qua NFC", + "s_silence_nfc_sounds": "Tắt âm thanh NFC", + "l_silence_nfc_sounds_on": "Không có âm thanh nào sẽ được phát khi chạm NFC", + "l_silence_nfc_sounds_off": "Âm thanh sẽ phát khi chạm NFC", + "s_usb_options": "Tùy chọn USB", + "l_launch_app_on_usb": "Khởi chạy khi kết nối YubiKey", + "l_launch_app_on_usb_on": "Điều này ngăn cản các ứng dụng khác sử dụng YubiKey qua USB", + "l_launch_app_on_usb_off": "Các ứng dụng khác có thể sử dụng YubiKey qua USB", + "s_allow_screenshots": "Cho phép chụp ảnh màn hình", + + "l_nfc_dialog_tap_key": "Chạm và giữ khóa của bạn", + "s_nfc_dialog_operation_success": "Thành công", + "s_nfc_dialog_operation_failed": "Thất bại", + + "s_nfc_dialog_oath_reset": "Hành động: đặt lại ứng dụng OATH", + "s_nfc_dialog_oath_unlock": "Hành động: mở khóa ứng dụng OATH", + "s_nfc_dialog_oath_set_password": "Hành động: đặt mật khẩu OATH", + "s_nfc_dialog_oath_unset_password": "Hành động: xóa mật khẩu OATH", + "s_nfc_dialog_oath_add_account": "Hành động: thêm tài khoản mới", + "s_nfc_dialog_oath_rename_account": "Hành động: đổi tên tài khoản", + "s_nfc_dialog_oath_delete_account": "Hành động: xóa tài khoản", + "s_nfc_dialog_oath_calculate_code": "Hành động: tính toán mã OATH", + "s_nfc_dialog_oath_failure": "Hành động OATH thất bại", + "s_nfc_dialog_oath_add_multiple_accounts": "Hành động: thêm nhiều tài khoản", + + "s_nfc_dialog_fido_reset": "Hành động: đặt lại ứng dụng FIDO", + "s_nfc_dialog_fido_unlock": "Hành động: mở khóa ứng dụng FIDO", + "l_nfc_dialog_fido_set_pin": "Hành động: đặt hoặc thay đổi PIN FIDO", + "s_nfc_dialog_fido_delete_credential": "Hành động: xóa Passkey", + "s_nfc_dialog_fido_delete_fingerprint": "Hành động: xóa dấu vân tay", + "s_nfc_dialog_fido_rename_fingerprint": "Hành động: đổi tên dấu vân tay", + "s_nfc_dialog_fido_failure": "Hành động FIDO thất bại", + + "@_nfc": {}, + "s_nfc_ready_to_scan": null, + "s_nfc_hold_still": null, + "s_nfc_tap_your_yubikey": null, + "l_nfc_failed_to_scan": null, + + "@_ndef": {}, + "p_ndef_set_otp": "Đã sao chép mã OTP từ YubiKey vào clipboard.", + "p_ndef_set_password": "Đã sao chép mật khẩu từ YubiKey vào clipboard.", + "p_ndef_parse_failure": "Không thể phân tích mã OTP từ YubiKey.", + "p_ndef_set_clip_failure": "Không thể truy cập clipboard khi cố gắng sao chép mã OTP từ YubiKey.", + + "@_key_customization": {}, + "s_set_label": "Đặt nhãn", + "s_set_color": "Đặt màu", + "s_change_label": "Thay đổi nhãn", + "s_color": "Màu sắc", + "p_set_will_add_custom_name": "Điều này sẽ đặt tên tùy chỉnh cho YubiKey của bạn.", + "p_rename_will_change_custom_name": "Điều này sẽ thay đổi nhãn của YubiKey của bạn.", + + "@_eof": {} +} diff --git a/lib/main.dart b/lib/main.dart index 6771817c3..c04ead606 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,7 @@ */ import 'dart:developer' as developer; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -27,6 +28,7 @@ import 'app/state.dart'; import 'core/state.dart'; import 'desktop/init.dart' as desktop; import 'error_page.dart'; +import 'version.dart'; final _log = Logger('main'); @@ -43,6 +45,12 @@ void main(List argv) async { _initializeDebugLogging(); throw UnimplementedError('Platform not supported'); } + _log.info('Running Yubico Authenticator...', { + 'app_version': version, + 'dart': Platform.version, + 'os': Platform.operatingSystem, + 'os_version': Platform.operatingSystemVersion, + }); runApp(initializedApp); } catch (e) { _log.warning('Platform initialization failed: $e'); diff --git a/lib/management/models.dart b/lib/management/models.dart index eddb46235..fd9ea661f 100755 --- a/lib/management/models.dart +++ b/lib/management/models.dart @@ -78,6 +78,8 @@ class DeviceConfig with _$DeviceConfig { @freezed class DeviceInfo with _$DeviceInfo { + const DeviceInfo._(); // Added constructor + factory DeviceInfo( DeviceConfig config, int? serial, @@ -87,8 +89,18 @@ class DeviceInfo with _$DeviceInfo { bool isLocked, bool isFips, bool isSky, - bool pinComplexity) = _DeviceInfo; + bool pinComplexity, + int fipsCapable, + int fipsApproved, + int resetBlocked) = _DeviceInfo; factory DeviceInfo.fromJson(Map json) => _$DeviceInfoFromJson(json); + + /// Gets the tuple fipsCapable, fipsApproved for the given capability. + (bool fipsCapable, bool fipsApproved) getFipsStatus(Capability capability) { + final capable = fipsCapable & capability.value != 0; + final approved = capable && fipsApproved & capability.value != 0; + return (capable, approved); + } } diff --git a/lib/management/models.freezed.dart b/lib/management/models.freezed.dart index 776e1b83f..30522cadd 100644 --- a/lib/management/models.freezed.dart +++ b/lib/management/models.freezed.dart @@ -26,8 +26,12 @@ mixin _$DeviceConfig { int? get challengeResponseTimeout => throw _privateConstructorUsedError; int? get deviceFlags => throw _privateConstructorUsedError; + /// Serializes this DeviceConfig to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of DeviceConfig + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $DeviceConfigCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -55,6 +59,8 @@ class _$DeviceConfigCopyWithImpl<$Res, $Val extends DeviceConfig> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of DeviceConfig + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -107,6 +113,8 @@ class __$$DeviceConfigImplCopyWithImpl<$Res> _$DeviceConfigImpl _value, $Res Function(_$DeviceConfigImpl) _then) : super(_value, _then); + /// Create a copy of DeviceConfig + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -183,7 +191,7 @@ class _$DeviceConfigImpl implements _DeviceConfig { other.deviceFlags == deviceFlags)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -192,7 +200,9 @@ class _$DeviceConfigImpl implements _DeviceConfig { challengeResponseTimeout, deviceFlags); - @JsonKey(ignore: true) + /// Create a copy of DeviceConfig + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$DeviceConfigImplCopyWith<_$DeviceConfigImpl> get copyWith => @@ -224,8 +234,11 @@ abstract class _DeviceConfig implements DeviceConfig { int? get challengeResponseTimeout; @override int? get deviceFlags; + + /// Create a copy of DeviceConfig + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$DeviceConfigImplCopyWith<_$DeviceConfigImpl> get copyWith => throw _privateConstructorUsedError; } @@ -246,9 +259,16 @@ mixin _$DeviceInfo { bool get isFips => throw _privateConstructorUsedError; bool get isSky => throw _privateConstructorUsedError; bool get pinComplexity => throw _privateConstructorUsedError; + int get fipsCapable => throw _privateConstructorUsedError; + int get fipsApproved => throw _privateConstructorUsedError; + int get resetBlocked => throw _privateConstructorUsedError; + /// Serializes this DeviceInfo to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of DeviceInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $DeviceInfoCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -268,7 +288,10 @@ abstract class $DeviceInfoCopyWith<$Res> { bool isLocked, bool isFips, bool isSky, - bool pinComplexity}); + bool pinComplexity, + int fipsCapable, + int fipsApproved, + int resetBlocked}); $DeviceConfigCopyWith<$Res> get config; $VersionCopyWith<$Res> get version; @@ -284,6 +307,8 @@ class _$DeviceInfoCopyWithImpl<$Res, $Val extends DeviceInfo> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of DeviceInfo + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -296,6 +321,9 @@ class _$DeviceInfoCopyWithImpl<$Res, $Val extends DeviceInfo> Object? isFips = null, Object? isSky = null, Object? pinComplexity = null, + Object? fipsCapable = null, + Object? fipsApproved = null, + Object? resetBlocked = null, }) { return _then(_value.copyWith( config: null == config @@ -334,9 +362,23 @@ class _$DeviceInfoCopyWithImpl<$Res, $Val extends DeviceInfo> ? _value.pinComplexity : pinComplexity // ignore: cast_nullable_to_non_nullable as bool, + fipsCapable: null == fipsCapable + ? _value.fipsCapable + : fipsCapable // ignore: cast_nullable_to_non_nullable + as int, + fipsApproved: null == fipsApproved + ? _value.fipsApproved + : fipsApproved // ignore: cast_nullable_to_non_nullable + as int, + resetBlocked: null == resetBlocked + ? _value.resetBlocked + : resetBlocked // ignore: cast_nullable_to_non_nullable + as int, ) as $Val); } + /// Create a copy of DeviceInfo + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $DeviceConfigCopyWith<$Res> get config { @@ -345,6 +387,8 @@ class _$DeviceInfoCopyWithImpl<$Res, $Val extends DeviceInfo> }); } + /// Create a copy of DeviceInfo + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $VersionCopyWith<$Res> get version { @@ -371,7 +415,10 @@ abstract class _$$DeviceInfoImplCopyWith<$Res> bool isLocked, bool isFips, bool isSky, - bool pinComplexity}); + bool pinComplexity, + int fipsCapable, + int fipsApproved, + int resetBlocked}); @override $DeviceConfigCopyWith<$Res> get config; @@ -387,6 +434,8 @@ class __$$DeviceInfoImplCopyWithImpl<$Res> _$DeviceInfoImpl _value, $Res Function(_$DeviceInfoImpl) _then) : super(_value, _then); + /// Create a copy of DeviceInfo + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -399,6 +448,9 @@ class __$$DeviceInfoImplCopyWithImpl<$Res> Object? isFips = null, Object? isSky = null, Object? pinComplexity = null, + Object? fipsCapable = null, + Object? fipsApproved = null, + Object? resetBlocked = null, }) { return _then(_$DeviceInfoImpl( null == config @@ -437,13 +489,25 @@ class __$$DeviceInfoImplCopyWithImpl<$Res> ? _value.pinComplexity : pinComplexity // ignore: cast_nullable_to_non_nullable as bool, + null == fipsCapable + ? _value.fipsCapable + : fipsCapable // ignore: cast_nullable_to_non_nullable + as int, + null == fipsApproved + ? _value.fipsApproved + : fipsApproved // ignore: cast_nullable_to_non_nullable + as int, + null == resetBlocked + ? _value.resetBlocked + : resetBlocked // ignore: cast_nullable_to_non_nullable + as int, )); } } /// @nodoc @JsonSerializable() -class _$DeviceInfoImpl implements _DeviceInfo { +class _$DeviceInfoImpl extends _DeviceInfo { _$DeviceInfoImpl( this.config, this.serial, @@ -453,8 +517,12 @@ class _$DeviceInfoImpl implements _DeviceInfo { this.isLocked, this.isFips, this.isSky, - this.pinComplexity) - : _supportedCapabilities = supportedCapabilities; + this.pinComplexity, + this.fipsCapable, + this.fipsApproved, + this.resetBlocked) + : _supportedCapabilities = supportedCapabilities, + super._(); factory _$DeviceInfoImpl.fromJson(Map json) => _$$DeviceInfoImplFromJson(json); @@ -484,10 +552,16 @@ class _$DeviceInfoImpl implements _DeviceInfo { final bool isSky; @override final bool pinComplexity; + @override + final int fipsCapable; + @override + final int fipsApproved; + @override + final int resetBlocked; @override String toString() { - return 'DeviceInfo(config: $config, serial: $serial, version: $version, formFactor: $formFactor, supportedCapabilities: $supportedCapabilities, isLocked: $isLocked, isFips: $isFips, isSky: $isSky, pinComplexity: $pinComplexity)'; + return 'DeviceInfo(config: $config, serial: $serial, version: $version, formFactor: $formFactor, supportedCapabilities: $supportedCapabilities, isLocked: $isLocked, isFips: $isFips, isSky: $isSky, pinComplexity: $pinComplexity, fipsCapable: $fipsCapable, fipsApproved: $fipsApproved, resetBlocked: $resetBlocked)'; } @override @@ -507,10 +581,16 @@ class _$DeviceInfoImpl implements _DeviceInfo { (identical(other.isFips, isFips) || other.isFips == isFips) && (identical(other.isSky, isSky) || other.isSky == isSky) && (identical(other.pinComplexity, pinComplexity) || - other.pinComplexity == pinComplexity)); + other.pinComplexity == pinComplexity) && + (identical(other.fipsCapable, fipsCapable) || + other.fipsCapable == fipsCapable) && + (identical(other.fipsApproved, fipsApproved) || + other.fipsApproved == fipsApproved) && + (identical(other.resetBlocked, resetBlocked) || + other.resetBlocked == resetBlocked)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -522,9 +602,14 @@ class _$DeviceInfoImpl implements _DeviceInfo { isLocked, isFips, isSky, - pinComplexity); + pinComplexity, + fipsCapable, + fipsApproved, + resetBlocked); - @JsonKey(ignore: true) + /// Create a copy of DeviceInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$DeviceInfoImplCopyWith<_$DeviceInfoImpl> get copyWith => @@ -538,7 +623,7 @@ class _$DeviceInfoImpl implements _DeviceInfo { } } -abstract class _DeviceInfo implements DeviceInfo { +abstract class _DeviceInfo extends DeviceInfo { factory _DeviceInfo( final DeviceConfig config, final int? serial, @@ -548,7 +633,11 @@ abstract class _DeviceInfo implements DeviceInfo { final bool isLocked, final bool isFips, final bool isSky, - final bool pinComplexity) = _$DeviceInfoImpl; + final bool pinComplexity, + final int fipsCapable, + final int fipsApproved, + final int resetBlocked) = _$DeviceInfoImpl; + _DeviceInfo._() : super._(); factory _DeviceInfo.fromJson(Map json) = _$DeviceInfoImpl.fromJson; @@ -572,7 +661,16 @@ abstract class _DeviceInfo implements DeviceInfo { @override bool get pinComplexity; @override - @JsonKey(ignore: true) + int get fipsCapable; + @override + int get fipsApproved; + @override + int get resetBlocked; + + /// Create a copy of DeviceInfo + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) _$$DeviceInfoImplCopyWith<_$DeviceInfoImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/management/models.g.dart b/lib/management/models.g.dart index dc33ea9d7..2763c0b4b 100644 --- a/lib/management/models.g.dart +++ b/lib/management/models.g.dart @@ -9,11 +9,12 @@ part of 'models.dart'; _$DeviceConfigImpl _$$DeviceConfigImplFromJson(Map json) => _$DeviceConfigImpl( (json['enabled_capabilities'] as Map).map( - (k, e) => MapEntry($enumDecode(_$TransportEnumMap, k), e as int), + (k, e) => + MapEntry($enumDecode(_$TransportEnumMap, k), (e as num).toInt()), ), - json['auto_eject_timeout'] as int?, - json['challenge_response_timeout'] as int?, - json['device_flags'] as int?, + (json['auto_eject_timeout'] as num?)?.toInt(), + (json['challenge_response_timeout'] as num?)?.toInt(), + (json['device_flags'] as num?)?.toInt(), ); Map _$$DeviceConfigImplToJson(_$DeviceConfigImpl instance) => @@ -33,16 +34,20 @@ const _$TransportEnumMap = { _$DeviceInfoImpl _$$DeviceInfoImplFromJson(Map json) => _$DeviceInfoImpl( DeviceConfig.fromJson(json['config'] as Map), - json['serial'] as int?, + (json['serial'] as num?)?.toInt(), Version.fromJson(json['version'] as List), $enumDecode(_$FormFactorEnumMap, json['form_factor']), (json['supported_capabilities'] as Map).map( - (k, e) => MapEntry($enumDecode(_$TransportEnumMap, k), e as int), + (k, e) => + MapEntry($enumDecode(_$TransportEnumMap, k), (e as num).toInt()), ), json['is_locked'] as bool, json['is_fips'] as bool, json['is_sky'] as bool, json['pin_complexity'] as bool, + (json['fips_capable'] as num).toInt(), + (json['fips_approved'] as num).toInt(), + (json['reset_blocked'] as num).toInt(), ); Map _$$DeviceInfoImplToJson(_$DeviceInfoImpl instance) => @@ -57,6 +62,9 @@ Map _$$DeviceInfoImplToJson(_$DeviceInfoImpl instance) => 'is_fips': instance.isFips, 'is_sky': instance.isSky, 'pin_complexity': instance.pinComplexity, + 'fips_capable': instance.fipsCapable, + 'fips_approved': instance.fipsApproved, + 'reset_blocked': instance.resetBlocked, }; const _$FormFactorEnumMap = { diff --git a/lib/management/views/management_screen.dart b/lib/management/views/management_screen.dart index bd3c5ca2f..8c7451829 100755 --- a/lib/management/views/management_screen.dart +++ b/lib/management/views/management_screen.dart @@ -61,7 +61,6 @@ class _CapabilityForm extends StatelessWidget { .where((c) => capabilities & c.value != 0) .map((c) => FilterChip( label: Text(c.getDisplayName(l10n)), - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, key: Key('$keyPrefix.${c.name}'), selected: enabled & c.value != 0, onSelected: (_) { diff --git a/lib/oath/icon_provider/icon_pack_dialog.dart b/lib/oath/icon_provider/icon_pack_dialog.dart index 576d63c23..cc5762c12 100644 --- a/lib/oath/icon_provider/icon_pack_dialog.dart +++ b/lib/oath/icon_provider/icon_pack_dialog.dart @@ -202,7 +202,6 @@ class _ImportActionChip extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return ActionChip( - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, onPressed: !disabled ? () async { _importAction(context, ref); diff --git a/lib/oath/models.dart b/lib/oath/models.dart index ffcd085d3..bc9a59a13 100755 --- a/lib/oath/models.dart +++ b/lib/oath/models.dart @@ -258,3 +258,5 @@ class CredentialData with _$CredentialData { }, ); } + +enum OathLayout { list, grid, mixed } diff --git a/lib/oath/models.freezed.dart b/lib/oath/models.freezed.dart index f0da383ca..1b35f7e8a 100644 --- a/lib/oath/models.freezed.dart +++ b/lib/oath/models.freezed.dart @@ -29,8 +29,12 @@ mixin _$OathCredential { int get period => throw _privateConstructorUsedError; bool get touchRequired => throw _privateConstructorUsedError; + /// Serializes this OathCredential to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of OathCredential + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $OathCredentialCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -61,6 +65,8 @@ class _$OathCredentialCopyWithImpl<$Res, $Val extends OathCredential> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of OathCredential + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -131,6 +137,8 @@ class __$$OathCredentialImplCopyWithImpl<$Res> _$OathCredentialImpl _value, $Res Function(_$OathCredentialImpl) _then) : super(_value, _then); + /// Create a copy of OathCredential + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -222,12 +230,14 @@ class _$OathCredentialImpl implements _OathCredential { other.touchRequired == touchRequired)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, deviceId, id, issuer, name, oathType, period, touchRequired); - @JsonKey(ignore: true) + /// Create a copy of OathCredential + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$OathCredentialImplCopyWith<_$OathCredentialImpl> get copyWith => @@ -270,8 +280,11 @@ abstract class _OathCredential implements OathCredential { int get period; @override bool get touchRequired; + + /// Create a copy of OathCredential + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$OathCredentialImplCopyWith<_$OathCredentialImpl> get copyWith => throw _privateConstructorUsedError; } @@ -286,8 +299,12 @@ mixin _$OathCode { int get validFrom => throw _privateConstructorUsedError; int get validTo => throw _privateConstructorUsedError; + /// Serializes this OathCode to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of OathCode + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $OathCodeCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -310,6 +327,8 @@ class _$OathCodeCopyWithImpl<$Res, $Val extends OathCode> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of OathCode + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -353,6 +372,8 @@ class __$$OathCodeImplCopyWithImpl<$Res> _$OathCodeImpl _value, $Res Function(_$OathCodeImpl) _then) : super(_value, _then); + /// Create a copy of OathCode + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -408,11 +429,13 @@ class _$OathCodeImpl implements _OathCode { (identical(other.validTo, validTo) || other.validTo == validTo)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, value, validFrom, validTo); - @JsonKey(ignore: true) + /// Create a copy of OathCode + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$OathCodeImplCopyWith<_$OathCodeImpl> get copyWith => @@ -440,8 +463,11 @@ abstract class _OathCode implements OathCode { int get validFrom; @override int get validTo; + + /// Create a copy of OathCode + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$OathCodeImplCopyWith<_$OathCodeImpl> get copyWith => throw _privateConstructorUsedError; } @@ -455,8 +481,12 @@ mixin _$OathPair { OathCredential get credential => throw _privateConstructorUsedError; OathCode? get code => throw _privateConstructorUsedError; + /// Serializes this OathPair to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of OathPair + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $OathPairCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -482,6 +512,8 @@ class _$OathPairCopyWithImpl<$Res, $Val extends OathPair> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of OathPair + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -500,6 +532,8 @@ class _$OathPairCopyWithImpl<$Res, $Val extends OathPair> ) as $Val); } + /// Create a copy of OathPair + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $OathCredentialCopyWith<$Res> get credential { @@ -508,6 +542,8 @@ class _$OathPairCopyWithImpl<$Res, $Val extends OathPair> }); } + /// Create a copy of OathPair + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $OathCodeCopyWith<$Res>? get code { @@ -545,6 +581,8 @@ class __$$OathPairImplCopyWithImpl<$Res> _$OathPairImpl _value, $Res Function(_$OathPairImpl) _then) : super(_value, _then); + /// Create a copy of OathPair + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -592,11 +630,13 @@ class _$OathPairImpl implements _OathPair { (identical(other.code, code) || other.code == code)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, credential, code); - @JsonKey(ignore: true) + /// Create a copy of OathPair + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$OathPairImplCopyWith<_$OathPairImpl> get copyWith => @@ -621,8 +661,11 @@ abstract class _OathPair implements OathPair { OathCredential get credential; @override OathCode? get code; + + /// Create a copy of OathPair + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$OathPairImplCopyWith<_$OathPairImpl> get copyWith => throw _privateConstructorUsedError; } @@ -640,8 +683,12 @@ mixin _$OathState { bool get locked => throw _privateConstructorUsedError; KeystoreState get keystore => throw _privateConstructorUsedError; + /// Serializes this OathState to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of OathState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $OathStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -672,6 +719,8 @@ class _$OathStateCopyWithImpl<$Res, $Val extends OathState> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of OathState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -710,6 +759,8 @@ class _$OathStateCopyWithImpl<$Res, $Val extends OathState> ) as $Val); } + /// Create a copy of OathState + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $VersionCopyWith<$Res> get version { @@ -747,6 +798,8 @@ class __$$OathStateImplCopyWithImpl<$Res> _$OathStateImpl _value, $Res Function(_$OathStateImpl) _then) : super(_value, _then); + /// Create a copy of OathState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -833,12 +886,14 @@ class _$OathStateImpl extends _OathState { other.keystore == keystore)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, deviceId, version, hasKey, remembered, locked, keystore); - @JsonKey(ignore: true) + /// Create a copy of OathState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$OathStateImplCopyWith<_$OathStateImpl> get copyWith => @@ -875,8 +930,11 @@ abstract class _OathState extends OathState { bool get locked; @override KeystoreState get keystore; + + /// Create a copy of OathState + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$OathStateImplCopyWith<_$OathStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -896,8 +954,12 @@ mixin _$CredentialData { int get period => throw _privateConstructorUsedError; int get counter => throw _privateConstructorUsedError; + /// Serializes this CredentialData to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of CredentialData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $CredentialDataCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -929,6 +991,8 @@ class _$CredentialDataCopyWithImpl<$Res, $Val extends CredentialData> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of CredentialData + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1005,6 +1069,8 @@ class __$$CredentialDataImplCopyWithImpl<$Res> _$CredentialDataImpl _value, $Res Function(_$CredentialDataImpl) _then) : super(_value, _then); + /// Create a copy of CredentialData + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1115,12 +1181,14 @@ class _$CredentialDataImpl extends _CredentialData { (identical(other.counter, counter) || other.counter == counter)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, issuer, name, secret, oathType, hashAlgorithm, digits, period, counter); - @JsonKey(ignore: true) + /// Create a copy of CredentialData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$CredentialDataImplCopyWith<_$CredentialDataImpl> get copyWith => @@ -1166,8 +1234,11 @@ abstract class _CredentialData extends CredentialData { int get period; @override int get counter; + + /// Create a copy of CredentialData + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$CredentialDataImplCopyWith<_$CredentialDataImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/oath/models.g.dart b/lib/oath/models.g.dart index e4f577cf3..df7e897e3 100644 --- a/lib/oath/models.g.dart +++ b/lib/oath/models.g.dart @@ -13,7 +13,7 @@ _$OathCredentialImpl _$$OathCredentialImplFromJson(Map json) => const _IssuerConverter().fromJson(json['issuer'] as String?), json['name'] as String, $enumDecode(_$OathTypeEnumMap, json['oath_type']), - json['period'] as int, + (json['period'] as num).toInt(), json['touch_required'] as bool, ); @@ -37,8 +37,8 @@ const _$OathTypeEnumMap = { _$OathCodeImpl _$$OathCodeImplFromJson(Map json) => _$OathCodeImpl( json['value'] as String, - json['valid_from'] as int, - json['valid_to'] as int, + (json['valid_from'] as num).toInt(), + (json['valid_to'] as num).toInt(), ); Map _$$OathCodeImplToJson(_$OathCodeImpl instance) => @@ -98,9 +98,9 @@ _$CredentialDataImpl _$$CredentialDataImplFromJson(Map json) => hashAlgorithm: $enumDecodeNullable(_$HashAlgorithmEnumMap, json['hash_algorithm']) ?? defaultHashAlgorithm, - digits: json['digits'] as int? ?? defaultDigits, - period: json['period'] as int? ?? defaultPeriod, - counter: json['counter'] as int? ?? defaultCounter, + digits: (json['digits'] as num?)?.toInt() ?? defaultDigits, + period: (json['period'] as num?)?.toInt() ?? defaultPeriod, + counter: (json['counter'] as num?)?.toInt() ?? defaultCounter, ); Map _$$CredentialDataImplToJson( diff --git a/lib/oath/state.dart b/lib/oath/state.dart index b77b42000..22188e1ea 100755 --- a/lib/oath/state.dart +++ b/lib/oath/state.dart @@ -38,6 +38,53 @@ class AccountsSearchNotifier extends StateNotifier { } } +final oathLayoutProvider = + StateNotifierProvider.autoDispose((ref) { + final device = ref.watch(currentDeviceProvider); + List credentials = device != null + ? ref.read(filteredCredentialsProvider( + ref.read(credentialListProvider(device.path)) ?? [])) + : []; + final favorites = ref.watch(favoritesProvider); + final pinnedCreds = + credentials.where((entry) => favorites.contains(entry.credential.id)); + return OathLayoutNotfier('OATH_STATE_LAYOUT', ref.watch(prefProvider), + credentials, pinnedCreds.toList()); +}); + +class OathLayoutNotfier extends StateNotifier { + final String _key; + final SharedPreferences _prefs; + OathLayoutNotfier(this._key, this._prefs, List credentials, + List pinnedCredentials) + : super( + _fromName(_prefs.getString(_key), credentials, pinnedCredentials)); + + void setLayout(OathLayout layout) { + state = layout; + _prefs.setString(_key, layout.name); + } + + static OathLayout _fromName(String? name, List credentials, + List pinnedCredentials) { + final layout = OathLayout.values.firstWhere( + (element) => element.name == name, + orElse: () => OathLayout.list, + ); + // Default to list view if current key does not have + // pinned credentials + if (layout == OathLayout.mixed) { + if (pinnedCredentials.isEmpty) { + return OathLayout.list; + } + if (pinnedCredentials.length == credentials.length) { + return OathLayout.grid; + } + } + return layout; + } +} + final oathStateProvider = AsyncNotifierProvider.autoDispose .family( () => throw UnimplementedError(), diff --git a/lib/oath/views/account_helper.dart b/lib/oath/views/account_helper.dart index c82dfe87a..1669836af 100755 --- a/lib/oath/views/account_helper.dart +++ b/lib/oath/views/account_helper.dart @@ -26,7 +26,6 @@ import '../../app/shortcuts.dart'; import '../../app/state.dart'; import '../../core/state.dart'; import '../../widgets/circle_timer.dart'; -import '../../widgets/custom_icons.dart'; import '../features.dart' as features; import '../keys.dart' as keys; import '../models.dart'; @@ -90,7 +89,7 @@ class AccountHelper { ActionItem( key: keys.togglePinAction, feature: features.accountsPin, - icon: pinned ? pushPinStrokeIcon : const Icon(Symbols.push_pin), + icon: Icon(pinned ? Symbols.keep_off : Symbols.keep), title: pinned ? l10n.s_unpin_account : l10n.s_pin_account, subtitle: l10n.l_pin_account_desc, intent: TogglePinIntent(credential), diff --git a/lib/oath/views/account_list.dart b/lib/oath/views/account_list.dart index e0c88bb2a..90e79a9c0 100755 --- a/lib/oath/views/account_list.dart +++ b/lib/oath/views/account_list.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../widgets/flex_box.dart'; import '../models.dart'; import '../state.dart'; import 'account_view.dart'; @@ -32,6 +33,9 @@ class AccountList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context)!; + final theme = Theme.of(context); + final labelStyle = + theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.primary); final credentials = ref.watch(filteredCredentialsProvider(accounts)); final favorites = ref.watch(favoritesProvider); if (credentials.isEmpty) { @@ -45,32 +49,71 @@ class AccountList extends ConsumerWidget { final creds = credentials.where((entry) => !favorites.contains(entry.credential.id)); + final oathLayout = ref.watch(oathLayoutProvider); + final pinnedLayout = + (oathLayout == OathLayout.grid || oathLayout == OathLayout.mixed) + ? FlexLayout.grid + : FlexLayout.list; + final normalLayout = + oathLayout == OathLayout.grid ? FlexLayout.grid : FlexLayout.list; + return FocusTraversalGroup( policy: WidgetOrderTraversalPolicy(), - child: Column( - children: [ - ...pinnedCreds.map( - (entry) => AccountView( - entry.credential, - expanded: expanded, - selected: entry.credential == selected, - ), - ), - if (pinnedCreds.isNotEmpty && creds.isNotEmpty) + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (pinnedCreds.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.only(left: 18, bottom: 8), + child: Text(l10n.s_pinned, style: labelStyle), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: FlexBox( + items: pinnedCreds.toList(), + itemBuilder: (value) => AccountView( + value.credential, + expanded: expanded, + selected: value.credential == selected, + large: pinnedLayout == FlexLayout.grid, + ), + cellMinWidth: 250, + spacing: pinnedLayout == FlexLayout.grid ? 4.0 : 0.0, + runSpacing: pinnedLayout == FlexLayout.grid ? 4.0 : 0.0, + layout: pinnedLayout, + ), + ), + ], + if (pinnedCreds.isNotEmpty && creds.isNotEmpty) ...[ + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.only(left: 18, bottom: 8), + child: Text( + l10n.s_accounts, + style: labelStyle, + ), + ), + ], Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Divider( - color: Theme.of(context).colorScheme.secondaryContainer, + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: FlexBox( + items: creds.toList(), + itemBuilder: (value) => AccountView( + value.credential, + expanded: expanded, + selected: value.credential == selected, + large: normalLayout == FlexLayout.grid, + ), + cellMinWidth: 250, + spacing: normalLayout == FlexLayout.grid ? 4.0 : 0.0, + runSpacing: normalLayout == FlexLayout.grid ? 4.0 : 0.0, + layout: normalLayout, ), ), - ...creds.map( - (entry) => AccountView( - entry.credential, - expanded: expanded, - selected: entry.credential == selected, - ), - ), - ], + ], + ), ), ); } diff --git a/lib/oath/views/account_view.dart b/lib/oath/views/account_view.dart index ce505de65..43da516eb 100755 --- a/lib/oath/views/account_view.dart +++ b/lib/oath/views/account_view.dart @@ -30,8 +30,12 @@ class AccountView extends ConsumerStatefulWidget { final OathCredential credential; final bool expanded; final bool selected; + final bool large; const AccountView(this.credential, - {super.key, required this.expanded, required this.selected}); + {super.key, + required this.expanded, + required this.selected, + this.large = false}); @override ConsumerState createState() => _AccountViewState(); @@ -80,7 +84,7 @@ class _AccountViewState extends ConsumerState { final helper = AccountHelper(context, ref, credential); final subtitle = helper.subtitle; final circleAvatar = CircleAvatar( - foregroundColor: Theme.of(context).colorScheme.background, + foregroundColor: Theme.of(context).colorScheme.surface, backgroundColor: _iconColor(400), child: Text( (credential.issuer ?? credential.name).characters.first.toUpperCase(), @@ -116,6 +120,95 @@ class _AccountViewState extends ConsumerState { ? CopyIntent(credential) : null, buildPopupActions: (_) => helper.buildActions(), + itemBuilder: widget.large + ? (context) { + return ListTile( + mouseCursor: !(isDesktop && !widget.expanded) + ? SystemMouseCursors.click + : null, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + selectedTileColor: + Theme.of(context).colorScheme.secondaryContainer, + selectedColor: + Theme.of(context).colorScheme.onSecondaryContainer, + selected: widget.selected, + tileColor: Theme.of(context).hoverColor, + contentPadding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + title: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AccountIcon( + issuer: credential.issuer, + defaultWidget: circleAvatar), + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + helper.title, + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface), + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: false, + ), + if (subtitle != null) + Text( + subtitle, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant), + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: false, + ) + ], + ), + ) + ], + ), + const SizedBox(height: 8.0), + Focus( + skipTraversal: true, + descendantsAreTraversable: false, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + helper.code != null + ? FilledButton.tonalIcon( + icon: helper.buildCodeIcon(), + label: helper.buildCodeLabel(), + style: buttonStyle, + onPressed: + Actions.handler(context, openIntent), + ) + : FilledButton.tonal( + style: buttonStyle, + onPressed: + Actions.handler(context, openIntent), + child: helper.buildCodeIcon()), + ], + ), + ), + ], + ), + ); + } + : null, ); } } diff --git a/lib/oath/views/add_account_dialog.dart b/lib/oath/views/add_account_dialog.dart index d9545271b..d376491bc 100644 --- a/lib/oath/views/add_account_dialog.dart +++ b/lib/oath/views/add_account_dialog.dart @@ -84,8 +84,6 @@ class _AddAccountDialogState extends ConsumerState { children: [ ActionChip( avatar: const Icon(Symbols.qr_code_scanner), - backgroundColor: - Theme.of(context).colorScheme.surfaceVariant, label: Text(l10n.s_qr_scan), onPressed: () async { if (qrScanner != null) { @@ -107,8 +105,6 @@ class _AddAccountDialogState extends ConsumerState { ActionChip( key: addAccountManuallyButton, avatar: const Icon(Symbols.edit), - backgroundColor: - Theme.of(context).colorScheme.surfaceVariant, label: Text(l10n.s_add_manually), onPressed: () async { Navigator.of(context).pop(); diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index aca626c2d..4a88bcfb3 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -470,8 +470,6 @@ class _OathAddAccountPageState extends ConsumerState { if (oathState?.version.isAtLeast(4, 2) ?? true) FilterChip( key: keys.requireTouchFilterChip, - backgroundColor: - Theme.of(context).colorScheme.surfaceVariant, label: Text(l10n.s_require_touch), selected: _touch, onSelected: (value) { diff --git a/lib/oath/views/key_actions.dart b/lib/oath/views/key_actions.dart index 5f46a1ffb..d52af6c06 100755 --- a/lib/oath/views/key_actions.dart +++ b/lib/oath/views/key_actions.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Yubico. + * Copyright (C) 2023,2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,13 +23,22 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; import '../../app/views/action_list.dart'; +import '../../management/models.dart'; import '../features.dart' as features; import '../icon_provider/icon_pack_dialog.dart'; import '../keys.dart' as keys; import '../models.dart'; -import 'manage_password_dialog.dart'; import 'utils.dart'; +bool oathShowActionNotifier(DeviceInfo? info) { + if (info == null) { + return false; + } + + final (fipsCapable, fipsApproved) = info.getFipsStatus(Capability.oath); + return fipsCapable && !fipsApproved; +} + Widget oathBuildActions( BuildContext context, DevicePath devicePath, @@ -39,6 +48,32 @@ Widget oathBuildActions( }) { final l10n = AppLocalizations.of(context)!; final capacity = oathState.capacity; + final (fipsCapable, fipsApproved) = ref + .watch(currentDeviceDataProvider) + .valueOrNull + ?.info + .getFipsStatus(Capability.oath) ?? + (false, false); + + final String? subtitle; + final bool enabled; + if (used == null) { + subtitle = l10n.l_unlock_first; + enabled = false; + } else if (fipsCapable & !fipsApproved) { + subtitle = l10n.l_set_password_first; + enabled = false; + } else if (capacity != null) { + subtitle = l10n.l_accounts_used(used, capacity); + enabled = capacity > used; + } else { + subtitle = null; + enabled = true; + } + + final colors = Theme.of(context).buttonTheme.colorScheme ?? + Theme.of(context).colorScheme; + final alertIcon = Icon(Symbols.warning_amber, color: colors.tertiary); return Column( children: [ @@ -47,14 +82,10 @@ Widget oathBuildActions( feature: features.actionsAdd, key: keys.addAccountAction, title: l10n.s_add_account, - subtitle: used == null - ? l10n.l_unlock_first - : (capacity != null - ? l10n.l_accounts_used(used, capacity) - : null), + subtitle: subtitle, actionStyle: ActionStyle.primary, icon: const Icon(Symbols.person_add_alt), - onTap: used != null && (capacity == null || capacity > used) + onTap: enabled ? (context) async { Navigator.of(context).popUntil((route) => route.isFirst); await addOathAccount(context, ref, devicePath, oathState); @@ -82,15 +113,12 @@ Widget oathBuildActions( feature: features.actionsPassword, title: oathState.hasKey ? l10n.s_manage_password : l10n.s_set_password, - subtitle: l10n.l_optional_password_protection, + subtitle: l10n.l_password_protection, icon: const Icon(Symbols.password), + trailing: fipsCapable && !fipsApproved ? alertIcon : null, onTap: (context) { Navigator.of(context).popUntil((route) => route.isFirst); - showBlurDialog( - context: context, - builder: (context) => - ManagePasswordDialog(devicePath, oathState), - ); + managePassword(context, ref, devicePath, oathState); }), ]), ], diff --git a/lib/oath/views/manage_password_dialog.dart b/lib/oath/views/manage_password_dialog.dart index 24a7c8238..cc77e3081 100755 --- a/lib/oath/views/manage_password_dialog.dart +++ b/lib/oath/views/manage_password_dialog.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2023 Yubico. + * Copyright (C) 2022-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import 'package:material_symbols_icons/symbols.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; +import '../../management/models.dart'; import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/focus_utils.dart'; @@ -33,6 +34,7 @@ import '../state.dart'; class ManagePasswordDialog extends ConsumerStatefulWidget { final DevicePath path; final OathState state; + const ManagePasswordDialog(this.path, this.state, {super.key}); @override @@ -43,6 +45,8 @@ class ManagePasswordDialog extends ConsumerStatefulWidget { class _ManagePasswordDialogState extends ConsumerState { final _currentPasswordController = TextEditingController(); final _currentPasswordFocus = FocusNode(); + final _newPasswordFocus = FocusNode(); + final _confirmPasswordFocus = FocusNode(); String _newPassword = ''; String _confirmPassword = ''; bool _currentIsWrong = false; @@ -54,6 +58,8 @@ class _ManagePasswordDialogState extends ConsumerState { void dispose() { _currentPasswordController.dispose(); _currentPasswordFocus.dispose(); + _newPasswordFocus.dispose(); + _confirmPasswordFocus.dispose(); super.dispose(); } @@ -82,12 +88,22 @@ class _ManagePasswordDialogState extends ConsumerState { @override Widget build(BuildContext context) { + final fipsCapable = ref.watch(currentDeviceDataProvider).maybeWhen( + data: (data) => data.info.getFipsStatus(Capability.oath).$1, + orElse: () => false); final l10n = AppLocalizations.of(context)!; final isValid = !_currentIsWrong && _newPassword.isNotEmpty && _newPassword == _confirmPassword && (!widget.state.hasKey || _currentPasswordController.text.isNotEmpty); + final newPasswordEnabled = + !widget.state.hasKey || _currentPasswordController.text.isNotEmpty; + + final confirmPasswordEnabled = + (!widget.state.hasKey || _currentPasswordController.text.isNotEmpty) && + _newPassword.isNotEmpty; + return ResponsiveDialog( title: Text( widget.state.hasKey ? l10n.s_manage_password : l10n.s_set_password), @@ -137,42 +153,52 @@ class _ManagePasswordDialogState extends ConsumerState { _currentIsWrong = false; }); }, + onSubmitted: (_) { + if (_currentPasswordController.text.isEmpty) { + _currentPasswordFocus.requestFocus(); + } else { + _newPasswordFocus.requestFocus(); + } + }, ).init(), Wrap( spacing: 4.0, runSpacing: 8.0, children: [ - OutlinedButton( - key: keys.removePasswordButton, - onPressed: _currentPasswordController.text.isNotEmpty && - !_currentIsWrong - ? () async { - final result = await ref - .read(oathStateProvider(widget.path).notifier) - .unsetPassword(_currentPasswordController.text); - if (result) { - if (mounted) { - await ref.read(withContextProvider)( - (context) async { - Navigator.of(context).pop(); - showMessage(context, l10n.s_password_removed); + if (!fipsCapable) + OutlinedButton( + key: keys.removePasswordButton, + onPressed: _currentPasswordController.text.isNotEmpty && + !_currentIsWrong + ? () async { + final result = await ref + .read(oathStateProvider(widget.path).notifier) + .unsetPassword( + _currentPasswordController.text); + if (result) { + if (mounted) { + await ref.read(withContextProvider)( + (context) async { + Navigator.of(context).pop(); + showMessage( + context, l10n.s_password_removed); + }); + } + } else { + _currentPasswordController.selection = + TextSelection( + baseOffset: 0, + extentOffset: _currentPasswordController + .text.length); + _currentPasswordFocus.requestFocus(); + setState(() { + _currentIsWrong = true; }); } - } else { - _currentPasswordController.selection = - TextSelection( - baseOffset: 0, - extentOffset: _currentPasswordController - .text.length); - _currentPasswordFocus.requestFocus(); - setState(() { - _currentIsWrong = true; - }); } - } - : null, - child: Text(l10n.s_remove_password), - ), + : null, + child: Text(l10n.s_remove_password), + ), if (widget.state.remembered) OutlinedButton( child: Text(l10n.s_clear_saved_password), @@ -197,24 +223,27 @@ class _ManagePasswordDialogState extends ConsumerState { autofocus: !widget.state.hasKey, obscureText: _isObscureNew, autofillHints: const [AutofillHints.newPassword], + focusNode: _newPasswordFocus, decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_new_password, prefixIcon: const Icon(Symbols.password), - suffixIcon: IconButton( - icon: Icon(_isObscureNew - ? Symbols.visibility - : Symbols.visibility_off), - onPressed: () { - setState(() { - _isObscureNew = !_isObscureNew; - }); - }, - tooltip: _isObscureNew - ? l10n.s_show_password - : l10n.s_hide_password), - enabled: !widget.state.hasKey || - _currentPasswordController.text.isNotEmpty, + suffixIcon: ExcludeFocusTraversal( + excluding: !newPasswordEnabled, + child: IconButton( + icon: Icon(_isObscureNew + ? Symbols.visibility + : Symbols.visibility_off), + onPressed: () { + setState(() { + _isObscureNew = !_isObscureNew; + }); + }, + tooltip: _isObscureNew + ? l10n.s_show_password + : l10n.s_hide_password), + ), + enabled: newPasswordEnabled, ), textInputAction: TextInputAction.next, onChanged: (value) { @@ -223,34 +252,38 @@ class _ManagePasswordDialogState extends ConsumerState { }); }, onSubmitted: (_) { - if (isValid) { - _submit(); + if (_newPassword.isNotEmpty) { + _confirmPasswordFocus.requestFocus(); + } else if (_newPassword.isEmpty) { + _newPasswordFocus.requestFocus(); } }, ).init(), AppTextField( key: keys.confirmPasswordField, obscureText: _isObscureConfirm, + focusNode: _confirmPasswordFocus, autofillHints: const [AutofillHints.newPassword], decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_confirm_password, prefixIcon: const Icon(Symbols.password), - suffixIcon: IconButton( - icon: Icon(_isObscureConfirm - ? Symbols.visibility - : Symbols.visibility_off), - onPressed: () { - setState(() { - _isObscureConfirm = !_isObscureConfirm; - }); - }, - tooltip: _isObscureConfirm - ? l10n.s_show_password - : l10n.s_hide_password), - enabled: (!widget.state.hasKey || - _currentPasswordController.text.isNotEmpty) && - _newPassword.isNotEmpty, + suffixIcon: ExcludeFocusTraversal( + excluding: !confirmPasswordEnabled, + child: IconButton( + icon: Icon(_isObscureConfirm + ? Symbols.visibility + : Symbols.visibility_off), + onPressed: () { + setState(() { + _isObscureConfirm = !_isObscureConfirm; + }); + }, + tooltip: _isObscureConfirm + ? l10n.s_show_password + : l10n.s_hide_password), + ), + enabled: confirmPasswordEnabled, errorText: _newPassword.length == _confirmPassword.length && _newPassword != _confirmPassword ? l10n.l_password_mismatch @@ -266,6 +299,8 @@ class _ManagePasswordDialogState extends ConsumerState { onSubmitted: (_) { if (isValid) { _submit(); + } else { + _confirmPasswordFocus.requestFocus(); } }, ).init(), diff --git a/lib/oath/views/oath_screen.dart b/lib/oath/views/oath_screen.dart index b0c2eda08..9ac6af303 100755 --- a/lib/oath/views/oath_screen.dart +++ b/lib/oath/views/oath_screen.dart @@ -53,6 +53,19 @@ import 'key_actions.dart'; import 'unlock_form.dart'; import 'utils.dart'; +extension on OathLayout { + IconData get _icon => switch (this) { + OathLayout.list => Symbols.list, + OathLayout.grid => Symbols.grid_view, + OathLayout.mixed => Symbols.vertical_split + }; + String getDisplayName(AppLocalizations l10n) => switch (this) { + OathLayout.list => l10n.s_list_layout, + OathLayout.grid => l10n.s_grid_layout, + OathLayout.mixed => l10n.s_mixed_layout + }; +} + class OathScreen extends ConsumerWidget { final DevicePath devicePath; @@ -123,6 +136,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> { late FocusNode searchFocus; late TextEditingController searchController; OathCredential? _selected; + bool _canRequestFocus = true; @override void initState() { @@ -140,7 +154,23 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> { super.dispose(); } + void _scrollSearchField() { + // Ensures the search field is fully visible when in focus + final headerSliverContext = headerSliverGlobalKey.currentContext; + if (searchFocus.hasFocus && headerSliverContext != null) { + final scrollable = Scrollable.of(headerSliverContext); + if (scrollable.deltaToScrollOrigin.dy > 0) { + scrollable.position.animateTo( + 0, + duration: const Duration(milliseconds: 100), + curve: Curves.ease, + ); + } + } + } + void _onFocusChange() { + _scrollSearchField(); setState(() {}); } @@ -153,7 +183,8 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> { final hasFeature = ref.watch(featureProvider); final hasActions = hasFeature(features.actions); final searchText = searchController.text; - + final deviceInfo = + ref.watch(currentDeviceDataProvider.select((s) => s.valueOrNull?.info)); Future onFileDropped(File file) async { final qrScanner = ref.read(qrScannerProvider); if (qrScanner != null) { @@ -172,21 +203,36 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> { if (numCreds == 0) { return MessagePage( - actionsBuilder: (context, expanded) => [ - if (!expanded) - ActionChip( - label: Text(l10n.s_add_account), - onPressed: () async { - await addOathAccount( - context, - ref, - widget.devicePath, - widget.oathState, - ); - }, - avatar: const Icon(Symbols.person_add_alt), - ) - ], + keyActionsBadge: oathShowActionNotifier(deviceInfo), + actionsBuilder: (context, expanded) { + final (fipsCapable, fipsApproved) = + deviceInfo?.getFipsStatus(Capability.oath) ?? (false, false); + + return [ + if (!expanded && (!fipsCapable || (fipsCapable && fipsApproved))) + ActionChip( + label: Text(l10n.s_add_account), + onPressed: () async { + await addOathAccount( + context, + ref, + widget.devicePath, + widget.oathState, + ); + }, + avatar: const Icon(Symbols.person_add_alt), + ), + if (!expanded && fipsCapable && !fipsApproved) + ActionChip( + label: Text(l10n.s_set_password), + onPressed: () async { + await managePassword( + context, ref, widget.devicePath, widget.oathState); + }, + avatar: const Icon(Symbols.person_add_alt), + ) + ]; + }, title: l10n.s_accounts, capabilities: const [Capability.oath], key: keys.noAccountsView, @@ -211,6 +257,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> { capabilities: const [Capability.oath], centered: true, delayedContent: true, + keyActionsBadge: oathShowActionNotifier(deviceInfo), builder: (context, _) => const CircularProgressIndicator(), ); } @@ -276,6 +323,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> { alternativeTitle: searchText != '' ? l10n.l_results_for(searchText) : null, capabilities: const [Capability.oath], + keyActionsBadge: oathShowActionNotifier(deviceInfo), keyActionsBuilder: hasActions ? (context) => oathBuildActions( context, @@ -376,60 +424,172 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> { } return KeyEventResult.ignored; }, - child: Builder(builder: (context) { + child: LayoutBuilder(builder: (context, constraints) { + final width = constraints.maxWidth; final textTheme = Theme.of(context).textTheme; - return Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: AppTextFormField( - key: searchField, - controller: searchController, - focusNode: searchFocus, - // Use the default style, but with a smaller font size: - style: textTheme.titleMedium - ?.copyWith(fontSize: textTheme.titleSmall?.fontSize), - decoration: AppInputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(48), - borderSide: BorderSide( - width: 0, - style: searchFocus.hasFocus - ? BorderStyle.solid - : BorderStyle.none, + return Consumer( + builder: (context, ref, child) { + final credentials = ref.watch(filteredCredentialsProvider( + ref.watch(credentialListProvider(widget.devicePath)) ?? + [])); + final favorites = ref.watch(favoritesProvider); + final pinnedCreds = credentials + .where((entry) => favorites.contains(entry.credential.id)); + + final availableLayouts = pinnedCreds.isEmpty || + pinnedCreds.length == credentials.length + ? OathLayout.values + .where((element) => element != OathLayout.mixed) + : OathLayout.values; + final oathLayout = ref.watch(oathLayoutProvider); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10.0, vertical: 8.0), + child: AppTextFormField( + key: searchField, + controller: searchController, + canRequestFocus: _canRequestFocus, + focusNode: searchFocus, + // Use the default style, but with a smaller font size: + style: textTheme.titleMedium + ?.copyWith(fontSize: textTheme.titleSmall?.fontSize), + decoration: AppInputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(48), + borderSide: BorderSide( + width: 0, + style: searchFocus.hasFocus + ? BorderStyle.solid + : BorderStyle.none, + ), + ), + contentPadding: const EdgeInsets.all(16), + fillColor: Theme.of(context).hoverColor, + filled: true, + hintText: l10n.s_search_accounts, + isDense: true, + prefixIcon: const Padding( + padding: EdgeInsetsDirectional.only(start: 8.0), + child: Icon(Icons.search_outlined), + ), + suffixIcons: [ + if (searchController.text.isNotEmpty) + IconButton( + icon: const Icon(Icons.clear), + iconSize: 16, + onPressed: () { + searchController.clear(); + ref + .read(accountsSearchProvider.notifier) + .setFilter(''); + setState(() {}); + }, + ), + if (searchController.text.isEmpty) ...[ + if (width >= 450) + ...availableLayouts.map( + (e) => MouseRegion( + onEnter: (event) { + if (!searchFocus.hasFocus) { + setState(() { + _canRequestFocus = false; + }); + } + }, + onExit: (event) { + setState(() { + _canRequestFocus = true; + }); + }, + child: IconButton( + tooltip: e.getDisplayName(l10n), + onPressed: () { + ref + .read(oathLayoutProvider.notifier) + .setLayout(e); + }, + icon: Icon( + e._icon, + color: e == oathLayout + ? Theme.of(context).colorScheme.primary + : null, + ), + ), + ), + ), + if (width < 450) + MouseRegion( + onEnter: (event) { + if (!searchFocus.hasFocus) { + setState(() { + _canRequestFocus = false; + }); + } + }, + onExit: (event) { + setState(() { + _canRequestFocus = true; + }); + }, + child: PopupMenuButton( + constraints: const BoxConstraints.tightFor(), + tooltip: l10n.s_select_layout, + popUpAnimationStyle: + AnimationStyle(duration: Duration.zero), + icon: Icon( + oathLayout._icon, + color: Theme.of(context).colorScheme.primary, + ), + itemBuilder: (context) => [ + ...availableLayouts.map( + (e) => PopupMenuItem( + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Tooltip( + message: e.getDisplayName(l10n), + child: Icon( + e._icon, + color: e == oathLayout + ? Theme.of(context) + .colorScheme + .primary + : null, + ), + ), + ], + ), + onTap: () { + ref + .read(oathLayoutProvider.notifier) + .setLayout(e); + }, + ), + ) + ], + ), + ) + ] + ], ), - ), - contentPadding: const EdgeInsets.all(16), - fillColor: Theme.of(context).hoverColor, - filled: true, - hintText: l10n.s_search_accounts, - isDense: true, - prefixIcon: const Padding( - padding: EdgeInsetsDirectional.only(start: 8.0), - child: Icon(Icons.search_outlined), - ), - suffixIcon: searchController.text.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear), - iconSize: 16, - onPressed: () { - searchController.clear(); - ref - .read(accountsSearchProvider.notifier) - .setFilter(''); - setState(() {}); - }, - ) - : null, - ), - onChanged: (value) { - ref.read(accountsSearchProvider.notifier).setFilter(value); - setState(() {}); - }, - textInputAction: TextInputAction.next, - onFieldSubmitted: (value) { - Focus.of(context).focusInDirection(TraversalDirection.down); - }, - ), + + onChanged: (value) { + ref + .read(accountsSearchProvider.notifier) + .setFilter(value); + _scrollSearchField(); + setState(() {}); + }, + textInputAction: TextInputAction.next, + onFieldSubmitted: (value) { + Focus.of(context) + .focusInDirection(TraversalDirection.down); + }, + ).init(), + ); + }, ); }), ), diff --git a/lib/oath/views/rename_account_dialog.dart b/lib/oath/views/rename_account_dialog.dart index bf6a26ce6..c3eee142f 100755 --- a/lib/oath/views/rename_account_dialog.dart +++ b/lib/oath/views/rename_account_dialog.dart @@ -104,7 +104,7 @@ class RenameAccountDialog extends ConsumerStatefulWidget { await withContext((context) async => showMessage( context, AppLocalizations.of(context)! - .l_account_add_failed(errorMessage), + .l_rename_account_failed(errorMessage), duration: const Duration(seconds: 4), )); return null; diff --git a/lib/oath/views/utils.dart b/lib/oath/views/utils.dart index ef5f90cd4..83072e08e 100755 --- a/lib/oath/views/utils.dart +++ b/lib/oath/views/utils.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * Copyright (C) 2022-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import '../models.dart'; import 'add_account_dialog.dart'; import 'add_account_page.dart'; import 'add_multi_account_page.dart'; +import 'manage_password_dialog.dart'; /// Calculates the available space for issuer and account name. /// @@ -178,3 +179,11 @@ Future addOathAccount(BuildContext context, WidgetRef ref, ); } } + +Future managePassword(BuildContext context, WidgetRef ref, + DevicePath devicePath, OathState oathState) async { + await showBlurDialog( + context: context, + builder: (context) => ManagePasswordDialog(devicePath, oathState), + ); +} diff --git a/lib/otp/models.freezed.dart b/lib/otp/models.freezed.dart index 205528cc1..251c48436 100644 --- a/lib/otp/models.freezed.dart +++ b/lib/otp/models.freezed.dart @@ -23,8 +23,12 @@ mixin _$OtpState { bool get slot1Configured => throw _privateConstructorUsedError; bool get slot2Configured => throw _privateConstructorUsedError; + /// Serializes this OtpState to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of OtpState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $OtpStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -47,6 +51,8 @@ class _$OtpStateCopyWithImpl<$Res, $Val extends OtpState> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of OtpState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -85,6 +91,8 @@ class __$$OtpStateImplCopyWithImpl<$Res> _$OtpStateImpl _value, $Res Function(_$OtpStateImpl) _then) : super(_value, _then); + /// Create a copy of OtpState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -134,12 +142,14 @@ class _$OtpStateImpl extends _OtpState { other.slot2Configured == slot2Configured)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, slot1Configured, slot2Configured); - @JsonKey(ignore: true) + /// Create a copy of OtpState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$OtpStateImplCopyWith<_$OtpStateImpl> get copyWith => @@ -166,8 +176,11 @@ abstract class _OtpState extends OtpState { bool get slot1Configured; @override bool get slot2Configured; + + /// Create a copy of OtpState + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$OtpStateImplCopyWith<_$OtpStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -177,7 +190,9 @@ mixin _$OtpSlot { SlotId get slot => throw _privateConstructorUsedError; bool get isConfigured => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of OtpSlot + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $OtpSlotCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -199,6 +214,8 @@ class _$OtpSlotCopyWithImpl<$Res, $Val extends OtpSlot> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of OtpSlot + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -236,6 +253,8 @@ class __$$OtpSlotImplCopyWithImpl<$Res> _$OtpSlotImpl _value, $Res Function(_$OtpSlotImpl) _then) : super(_value, _then); + /// Create a copy of OtpSlot + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -283,7 +302,9 @@ class _$OtpSlotImpl implements _OtpSlot { @override int get hashCode => Object.hash(runtimeType, slot, isConfigured); - @JsonKey(ignore: true) + /// Create a copy of OtpSlot + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$OtpSlotImplCopyWith<_$OtpSlotImpl> get copyWith => @@ -299,8 +320,11 @@ abstract class _OtpSlot implements OtpSlot { SlotId get slot; @override bool get isConfigured; + + /// Create a copy of OtpSlot + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$OtpSlotImplCopyWith<_$OtpSlotImpl> get copyWith => throw _privateConstructorUsedError; } @@ -316,8 +340,12 @@ mixin _$SlotConfigurationOptions { bool? get requireTouch => throw _privateConstructorUsedError; bool? get appendCr => throw _privateConstructorUsedError; + /// Serializes this SlotConfigurationOptions to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SlotConfigurationOptions + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SlotConfigurationOptionsCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -342,6 +370,8 @@ class _$SlotConfigurationOptionsCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SlotConfigurationOptions + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -388,6 +418,8 @@ class __$$SlotConfigurationOptionsImplCopyWithImpl<$Res> $Res Function(_$SlotConfigurationOptionsImpl) _then) : super(_value, _then); + /// Create a copy of SlotConfigurationOptions + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -446,11 +478,13 @@ class _$SlotConfigurationOptionsImpl implements _SlotConfigurationOptions { other.appendCr == appendCr)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, digits8, requireTouch, appendCr); - @JsonKey(ignore: true) + /// Create a copy of SlotConfigurationOptions + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SlotConfigurationOptionsImplCopyWith<_$SlotConfigurationOptionsImpl> @@ -480,8 +514,11 @@ abstract class _SlotConfigurationOptions implements SlotConfigurationOptions { bool? get requireTouch; @override bool? get appendCr; + + /// Create a copy of SlotConfigurationOptions + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SlotConfigurationOptionsImplCopyWith<_$SlotConfigurationOptionsImpl> get copyWith => throw _privateConstructorUsedError; } @@ -570,8 +607,13 @@ mixin _$SlotConfiguration { required TResult orElse(), }) => throw _privateConstructorUsedError; + + /// Serializes this SlotConfiguration to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SlotConfigurationCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -597,6 +639,8 @@ class _$SlotConfigurationCopyWithImpl<$Res, $Val extends SlotConfiguration> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -610,6 +654,8 @@ class _$SlotConfigurationCopyWithImpl<$Res, $Val extends SlotConfiguration> ) as $Val); } + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $SlotConfigurationOptionsCopyWith<$Res>? get options { @@ -646,6 +692,8 @@ class __$$SlotConfigurationHotpImplCopyWithImpl<$Res> $Res Function(_$SlotConfigurationHotpImpl) _then) : super(_value, _then); + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -699,11 +747,13 @@ class _$SlotConfigurationHotpImpl extends _SlotConfigurationHotp { (identical(other.options, options) || other.options == options)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, key, options); - @JsonKey(ignore: true) + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SlotConfigurationHotpImplCopyWith<_$SlotConfigurationHotpImpl> @@ -818,8 +868,11 @@ abstract class _SlotConfigurationHotp extends SlotConfiguration { String get key; @override SlotConfigurationOptions? get options; + + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SlotConfigurationHotpImplCopyWith<_$SlotConfigurationHotpImpl> get copyWith => throw _privateConstructorUsedError; } @@ -849,6 +902,8 @@ class __$$SlotConfigurationHmacSha1ImplCopyWithImpl<$Res> $Res Function(_$SlotConfigurationHmacSha1Impl) _then) : super(_value, _then); + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -902,11 +957,13 @@ class _$SlotConfigurationHmacSha1Impl extends _SlotConfigurationHmacSha1 { (identical(other.options, options) || other.options == options)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, key, options); - @JsonKey(ignore: true) + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SlotConfigurationHmacSha1ImplCopyWith<_$SlotConfigurationHmacSha1Impl> @@ -1022,8 +1079,11 @@ abstract class _SlotConfigurationHmacSha1 extends SlotConfiguration { String get key; @override SlotConfigurationOptions? get options; + + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SlotConfigurationHmacSha1ImplCopyWith<_$SlotConfigurationHmacSha1Impl> get copyWith => throw _privateConstructorUsedError; } @@ -1056,6 +1116,8 @@ class __$$SlotConfigurationStaticPasswordImplCopyWithImpl<$Res> $Res Function(_$SlotConfigurationStaticPasswordImpl) _then) : super(_value, _then); + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1124,12 +1186,14 @@ class _$SlotConfigurationStaticPasswordImpl (identical(other.options, options) || other.options == options)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, password, keyboardLayout, options); - @JsonKey(ignore: true) + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SlotConfigurationStaticPasswordImplCopyWith< @@ -1248,8 +1312,11 @@ abstract class _SlotConfigurationStaticPassword extends SlotConfiguration { String get keyboardLayout; @override SlotConfigurationOptions? get options; + + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SlotConfigurationStaticPasswordImplCopyWith< _$SlotConfigurationStaticPasswordImpl> get copyWith => throw _privateConstructorUsedError; @@ -1284,6 +1351,8 @@ class __$$SlotConfigurationYubiOtpImplCopyWithImpl<$Res> $Res Function(_$SlotConfigurationYubiOtpImpl) _then) : super(_value, _then); + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1359,12 +1428,14 @@ class _$SlotConfigurationYubiOtpImpl extends _SlotConfigurationYubiOtp { (identical(other.options, options) || other.options == options)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, publicId, privateId, key, options); - @JsonKey(ignore: true) + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SlotConfigurationYubiOtpImplCopyWith<_$SlotConfigurationYubiOtpImpl> @@ -1484,8 +1555,11 @@ abstract class _SlotConfigurationYubiOtp extends SlotConfiguration { String get key; @override SlotConfigurationOptions? get options; + + /// Create a copy of SlotConfiguration + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SlotConfigurationYubiOtpImplCopyWith<_$SlotConfigurationYubiOtpImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/otp/views/otp_screen.dart b/lib/otp/views/otp_screen.dart index eaba53f0a..e01574548 100644 --- a/lib/otp/views/otp_screen.dart +++ b/lib/otp/views/otp_screen.dart @@ -180,13 +180,17 @@ class _OtpScreenState extends ConsumerState { return null; }), }, - child: Column(children: [ - ...otpState.slots.map((e) => _SlotListItem( - e, - expanded: expanded, - selected: e == selected, - )) - ]), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 10.0), + child: Column(children: [ + ...otpState.slots.map((e) => _SlotListItem( + e, + expanded: expanded, + selected: e == selected, + )) + ]), + ), ); }, ), diff --git a/lib/piv/keys.dart b/lib/piv/keys.dart index 33293c123..5070293b2 100644 --- a/lib/piv/keys.dart +++ b/lib/piv/keys.dart @@ -40,6 +40,7 @@ const unlockButton = Key('$_prefix.unlock'); const resetButton = Key('$_prefix.reset'); const managementKeyField = Key('$_prefix.management_key'); +const newManagementKeyField = Key('$_prefix.management_key'); const managementKeyRefresh = Key('$_prefix.management_key_refresh'); const pinPukField = Key('$_prefix.pin_puk'); @@ -99,6 +100,7 @@ const appListItem95 = Key('$_prefix.95.applistitem'); // SlotMetadata body keys const slotMetadataKeyType = Key('$_prefix.slotMetadata.keyType'); +const slotMetadataBiometrics = Key('$_prefix.slotMetadata.biometrics'); // CertInfo body keys const certInfoKeyType = Key('$_prefix.certInfo.keyType'); diff --git a/lib/piv/models.dart b/lib/piv/models.dart index 12607adb9..b07734ccc 100644 --- a/lib/piv/models.dart +++ b/lib/piv/models.dart @@ -270,6 +270,7 @@ class PivState with _$PivState { required bool derivedKey, required bool storedKey, required int pinAttempts, + required bool supportsBio, String? chuid, String? ccc, PivStateMetadata? metadata, diff --git a/lib/piv/models.freezed.dart b/lib/piv/models.freezed.dart index 4344848fb..0d9688692 100644 --- a/lib/piv/models.freezed.dart +++ b/lib/piv/models.freezed.dart @@ -24,8 +24,12 @@ mixin _$PinMetadata { int get totalAttempts => throw _privateConstructorUsedError; int get attemptsRemaining => throw _privateConstructorUsedError; + /// Serializes this PinMetadata to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of PinMetadata + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $PinMetadataCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -49,6 +53,8 @@ class _$PinMetadataCopyWithImpl<$Res, $Val extends PinMetadata> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of PinMetadata + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -92,6 +98,8 @@ class __$$PinMetadataImplCopyWithImpl<$Res> _$PinMetadataImpl _value, $Res Function(_$PinMetadataImpl) _then) : super(_value, _then); + /// Create a copy of PinMetadata + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -150,12 +158,14 @@ class _$PinMetadataImpl implements _PinMetadata { other.attemptsRemaining == attemptsRemaining)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, defaultValue, totalAttempts, attemptsRemaining); - @JsonKey(ignore: true) + /// Create a copy of PinMetadata + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PinMetadataImplCopyWith<_$PinMetadataImpl> get copyWith => @@ -182,8 +192,11 @@ abstract class _PinMetadata implements PinMetadata { int get totalAttempts; @override int get attemptsRemaining; + + /// Create a copy of PinMetadata + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$PinMetadataImplCopyWith<_$PinMetadataImpl> get copyWith => throw _privateConstructorUsedError; } @@ -247,6 +260,9 @@ class _$PinVerificationStatusCopyWithImpl<$Res, final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of PinVerificationStatus + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -263,6 +279,9 @@ class __$$PinSuccessImplCopyWithImpl<$Res> __$$PinSuccessImplCopyWithImpl( _$PinSuccessImpl _value, $Res Function(_$PinSuccessImpl) _then) : super(_value, _then); + + /// Create a copy of PinVerificationStatus + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -370,6 +389,8 @@ class __$$PinFailureImplCopyWithImpl<$Res> _$PinFailureImpl _value, $Res Function(_$PinFailureImpl) _then) : super(_value, _then); + /// Create a copy of PinVerificationStatus + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -383,6 +404,8 @@ class __$$PinFailureImplCopyWithImpl<$Res> )); } + /// Create a copy of PinVerificationStatus + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $PivPinFailureReasonCopyWith<$Res> get reason { @@ -416,7 +439,9 @@ class _$PinFailureImpl implements PinFailure { @override int get hashCode => Object.hash(runtimeType, reason); - @JsonKey(ignore: true) + /// Create a copy of PinVerificationStatus + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PinFailureImplCopyWith<_$PinFailureImpl> get copyWith => @@ -489,7 +514,10 @@ abstract class PinFailure implements PinVerificationStatus { factory PinFailure(final PivPinFailureReason reason) = _$PinFailureImpl; PivPinFailureReason get reason; - @JsonKey(ignore: true) + + /// Create a copy of PinVerificationStatus + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$PinFailureImplCopyWith<_$PinFailureImpl> get copyWith => throw _privateConstructorUsedError; } @@ -552,6 +580,9 @@ class _$PivPinFailureReasonCopyWithImpl<$Res, $Val extends PivPinFailureReason> final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of PivPinFailureReason + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -571,6 +602,8 @@ class __$$PivInvalidPinImplCopyWithImpl<$Res> _$PivInvalidPinImpl _value, $Res Function(_$PivInvalidPinImpl) _then) : super(_value, _then); + /// Create a copy of PivPinFailureReason + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -610,7 +643,9 @@ class _$PivInvalidPinImpl implements PivInvalidPin { @override int get hashCode => Object.hash(runtimeType, attemptsRemaining); - @JsonKey(ignore: true) + /// Create a copy of PivPinFailureReason + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PivInvalidPinImplCopyWith<_$PivInvalidPinImpl> get copyWith => @@ -683,7 +718,10 @@ abstract class PivInvalidPin implements PivPinFailureReason { factory PivInvalidPin(final int attemptsRemaining) = _$PivInvalidPinImpl; int get attemptsRemaining; - @JsonKey(ignore: true) + + /// Create a copy of PivPinFailureReason + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$PivInvalidPinImplCopyWith<_$PivInvalidPinImpl> get copyWith => throw _privateConstructorUsedError; } @@ -702,6 +740,9 @@ class __$$PivWeakPinImplCopyWithImpl<$Res> __$$PivWeakPinImplCopyWithImpl( _$PivWeakPinImpl _value, $Res Function(_$PivWeakPinImpl) _then) : super(_value, _then); + + /// Create a copy of PivPinFailureReason + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -801,8 +842,12 @@ mixin _$ManagementKeyMetadata { bool get defaultValue => throw _privateConstructorUsedError; TouchPolicy get touchPolicy => throw _privateConstructorUsedError; + /// Serializes this ManagementKeyMetadata to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of ManagementKeyMetadata + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $ManagementKeyMetadataCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -828,6 +873,8 @@ class _$ManagementKeyMetadataCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of ManagementKeyMetadata + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -874,6 +921,8 @@ class __$$ManagementKeyMetadataImplCopyWithImpl<$Res> $Res Function(_$ManagementKeyMetadataImpl) _then) : super(_value, _then); + /// Create a copy of ManagementKeyMetadata + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -931,12 +980,14 @@ class _$ManagementKeyMetadataImpl implements _ManagementKeyMetadata { other.touchPolicy == touchPolicy)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, keyType, defaultValue, touchPolicy); - @JsonKey(ignore: true) + /// Create a copy of ManagementKeyMetadata + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ManagementKeyMetadataImplCopyWith<_$ManagementKeyMetadataImpl> @@ -966,8 +1017,11 @@ abstract class _ManagementKeyMetadata implements ManagementKeyMetadata { bool get defaultValue; @override TouchPolicy get touchPolicy; + + /// Create a copy of ManagementKeyMetadata + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$ManagementKeyMetadataImplCopyWith<_$ManagementKeyMetadataImpl> get copyWith => throw _privateConstructorUsedError; } @@ -984,8 +1038,12 @@ mixin _$SlotMetadata { bool get generated => throw _privateConstructorUsedError; String get publicKey => throw _privateConstructorUsedError; + /// Serializes this SlotMetadata to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SlotMetadata + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SlotMetadataCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -1014,6 +1072,8 @@ class _$SlotMetadataCopyWithImpl<$Res, $Val extends SlotMetadata> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SlotMetadata + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1072,6 +1132,8 @@ class __$$SlotMetadataImplCopyWithImpl<$Res> _$SlotMetadataImpl _value, $Res Function(_$SlotMetadataImpl) _then) : super(_value, _then); + /// Create a copy of SlotMetadata + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1147,12 +1209,14 @@ class _$SlotMetadataImpl implements _SlotMetadata { other.publicKey == publicKey)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, keyType, pinPolicy, touchPolicy, generated, publicKey); - @JsonKey(ignore: true) + /// Create a copy of SlotMetadata + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SlotMetadataImplCopyWith<_$SlotMetadataImpl> get copyWith => @@ -1187,8 +1251,11 @@ abstract class _SlotMetadata implements SlotMetadata { bool get generated; @override String get publicKey; + + /// Create a copy of SlotMetadata + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SlotMetadataImplCopyWith<_$SlotMetadataImpl> get copyWith => throw _privateConstructorUsedError; } @@ -1204,8 +1271,12 @@ mixin _$PivStateMetadata { PinMetadata get pinMetadata => throw _privateConstructorUsedError; PinMetadata get pukMetadata => throw _privateConstructorUsedError; + /// Serializes this PivStateMetadata to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of PivStateMetadata + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $PivStateMetadataCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -1236,6 +1307,8 @@ class _$PivStateMetadataCopyWithImpl<$Res, $Val extends PivStateMetadata> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of PivStateMetadata + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1259,6 +1332,8 @@ class _$PivStateMetadataCopyWithImpl<$Res, $Val extends PivStateMetadata> ) as $Val); } + /// Create a copy of PivStateMetadata + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $ManagementKeyMetadataCopyWith<$Res> get managementKeyMetadata { @@ -1268,6 +1343,8 @@ class _$PivStateMetadataCopyWithImpl<$Res, $Val extends PivStateMetadata> }); } + /// Create a copy of PivStateMetadata + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $PinMetadataCopyWith<$Res> get pinMetadata { @@ -1276,6 +1353,8 @@ class _$PivStateMetadataCopyWithImpl<$Res, $Val extends PivStateMetadata> }); } + /// Create a copy of PivStateMetadata + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $PinMetadataCopyWith<$Res> get pukMetadata { @@ -1314,6 +1393,8 @@ class __$$PivStateMetadataImplCopyWithImpl<$Res> $Res Function(_$PivStateMetadataImpl) _then) : super(_value, _then); + /// Create a copy of PivStateMetadata + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1374,12 +1455,14 @@ class _$PivStateMetadataImpl implements _PivStateMetadata { other.pukMetadata == pukMetadata)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, managementKeyMetadata, pinMetadata, pukMetadata); - @JsonKey(ignore: true) + /// Create a copy of PivStateMetadata + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PivStateMetadataImplCopyWith<_$PivStateMetadataImpl> get copyWith => @@ -1409,8 +1492,11 @@ abstract class _PivStateMetadata implements PivStateMetadata { PinMetadata get pinMetadata; @override PinMetadata get pukMetadata; + + /// Create a copy of PivStateMetadata + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$PivStateMetadataImplCopyWith<_$PivStateMetadataImpl> get copyWith => throw _privateConstructorUsedError; } @@ -1426,12 +1512,17 @@ mixin _$PivState { bool get derivedKey => throw _privateConstructorUsedError; bool get storedKey => throw _privateConstructorUsedError; int get pinAttempts => throw _privateConstructorUsedError; + bool get supportsBio => throw _privateConstructorUsedError; String? get chuid => throw _privateConstructorUsedError; String? get ccc => throw _privateConstructorUsedError; PivStateMetadata? get metadata => throw _privateConstructorUsedError; + /// Serializes this PivState to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of PivState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $PivStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -1447,6 +1538,7 @@ abstract class $PivStateCopyWith<$Res> { bool derivedKey, bool storedKey, int pinAttempts, + bool supportsBio, String? chuid, String? ccc, PivStateMetadata? metadata}); @@ -1465,6 +1557,8 @@ class _$PivStateCopyWithImpl<$Res, $Val extends PivState> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of PivState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1473,6 +1567,7 @@ class _$PivStateCopyWithImpl<$Res, $Val extends PivState> Object? derivedKey = null, Object? storedKey = null, Object? pinAttempts = null, + Object? supportsBio = null, Object? chuid = freezed, Object? ccc = freezed, Object? metadata = freezed, @@ -1498,6 +1593,10 @@ class _$PivStateCopyWithImpl<$Res, $Val extends PivState> ? _value.pinAttempts : pinAttempts // ignore: cast_nullable_to_non_nullable as int, + supportsBio: null == supportsBio + ? _value.supportsBio + : supportsBio // ignore: cast_nullable_to_non_nullable + as bool, chuid: freezed == chuid ? _value.chuid : chuid // ignore: cast_nullable_to_non_nullable @@ -1513,6 +1612,8 @@ class _$PivStateCopyWithImpl<$Res, $Val extends PivState> ) as $Val); } + /// Create a copy of PivState + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $VersionCopyWith<$Res> get version { @@ -1521,6 +1622,8 @@ class _$PivStateCopyWithImpl<$Res, $Val extends PivState> }); } + /// Create a copy of PivState + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $PivStateMetadataCopyWith<$Res>? get metadata { @@ -1548,6 +1651,7 @@ abstract class _$$PivStateImplCopyWith<$Res> bool derivedKey, bool storedKey, int pinAttempts, + bool supportsBio, String? chuid, String? ccc, PivStateMetadata? metadata}); @@ -1566,6 +1670,8 @@ class __$$PivStateImplCopyWithImpl<$Res> _$PivStateImpl _value, $Res Function(_$PivStateImpl) _then) : super(_value, _then); + /// Create a copy of PivState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1574,6 +1680,7 @@ class __$$PivStateImplCopyWithImpl<$Res> Object? derivedKey = null, Object? storedKey = null, Object? pinAttempts = null, + Object? supportsBio = null, Object? chuid = freezed, Object? ccc = freezed, Object? metadata = freezed, @@ -1599,6 +1706,10 @@ class __$$PivStateImplCopyWithImpl<$Res> ? _value.pinAttempts : pinAttempts // ignore: cast_nullable_to_non_nullable as int, + supportsBio: null == supportsBio + ? _value.supportsBio + : supportsBio // ignore: cast_nullable_to_non_nullable + as bool, chuid: freezed == chuid ? _value.chuid : chuid // ignore: cast_nullable_to_non_nullable @@ -1624,6 +1735,7 @@ class _$PivStateImpl extends _PivState { required this.derivedKey, required this.storedKey, required this.pinAttempts, + required this.supportsBio, this.chuid, this.ccc, this.metadata}) @@ -1643,6 +1755,8 @@ class _$PivStateImpl extends _PivState { @override final int pinAttempts; @override + final bool supportsBio; + @override final String? chuid; @override final String? ccc; @@ -1651,7 +1765,7 @@ class _$PivStateImpl extends _PivState { @override String toString() { - return 'PivState(version: $version, authenticated: $authenticated, derivedKey: $derivedKey, storedKey: $storedKey, pinAttempts: $pinAttempts, chuid: $chuid, ccc: $ccc, metadata: $metadata)'; + return 'PivState(version: $version, authenticated: $authenticated, derivedKey: $derivedKey, storedKey: $storedKey, pinAttempts: $pinAttempts, supportsBio: $supportsBio, chuid: $chuid, ccc: $ccc, metadata: $metadata)'; } @override @@ -1668,18 +1782,22 @@ class _$PivStateImpl extends _PivState { other.storedKey == storedKey) && (identical(other.pinAttempts, pinAttempts) || other.pinAttempts == pinAttempts) && + (identical(other.supportsBio, supportsBio) || + other.supportsBio == supportsBio) && (identical(other.chuid, chuid) || other.chuid == chuid) && (identical(other.ccc, ccc) || other.ccc == ccc) && (identical(other.metadata, metadata) || other.metadata == metadata)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, version, authenticated, - derivedKey, storedKey, pinAttempts, chuid, ccc, metadata); + derivedKey, storedKey, pinAttempts, supportsBio, chuid, ccc, metadata); - @JsonKey(ignore: true) + /// Create a copy of PivState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PivStateImplCopyWith<_$PivStateImpl> get copyWith => @@ -1700,6 +1818,7 @@ abstract class _PivState extends PivState { required final bool derivedKey, required final bool storedKey, required final int pinAttempts, + required final bool supportsBio, final String? chuid, final String? ccc, final PivStateMetadata? metadata}) = _$PivStateImpl; @@ -1719,13 +1838,18 @@ abstract class _PivState extends PivState { @override int get pinAttempts; @override + bool get supportsBio; + @override String? get chuid; @override String? get ccc; @override PivStateMetadata? get metadata; + + /// Create a copy of PivState + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$PivStateImplCopyWith<_$PivStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -1744,8 +1868,12 @@ mixin _$CertInfo { String get notValidAfter => throw _privateConstructorUsedError; String get fingerprint => throw _privateConstructorUsedError; + /// Serializes this CertInfo to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of CertInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $CertInfoCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -1775,6 +1903,8 @@ class _$CertInfoCopyWithImpl<$Res, $Val extends CertInfo> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of CertInfo + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1845,6 +1975,8 @@ class __$$CertInfoImplCopyWithImpl<$Res> _$CertInfoImpl _value, $Res Function(_$CertInfoImpl) _then) : super(_value, _then); + /// Create a copy of CertInfo + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1941,12 +2073,14 @@ class _$CertInfoImpl implements _CertInfo { other.fingerprint == fingerprint)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, keyType, subject, issuer, serial, notValidBefore, notValidAfter, fingerprint); - @JsonKey(ignore: true) + /// Create a copy of CertInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$CertInfoImplCopyWith<_$CertInfoImpl> get copyWith => @@ -1987,8 +2121,11 @@ abstract class _CertInfo implements CertInfo { String get notValidAfter; @override String get fingerprint; + + /// Create a copy of CertInfo + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$CertInfoImplCopyWith<_$CertInfoImpl> get copyWith => throw _privateConstructorUsedError; } @@ -2003,8 +2140,12 @@ mixin _$PivSlot { SlotMetadata? get metadata => throw _privateConstructorUsedError; CertInfo? get certInfo => throw _privateConstructorUsedError; + /// Serializes this PivSlot to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of PivSlot + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $PivSlotCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -2029,6 +2170,8 @@ class _$PivSlotCopyWithImpl<$Res, $Val extends PivSlot> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of PivSlot + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -2052,6 +2195,8 @@ class _$PivSlotCopyWithImpl<$Res, $Val extends PivSlot> ) as $Val); } + /// Create a copy of PivSlot + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $SlotMetadataCopyWith<$Res>? get metadata { @@ -2064,6 +2209,8 @@ class _$PivSlotCopyWithImpl<$Res, $Val extends PivSlot> }); } + /// Create a copy of PivSlot + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $CertInfoCopyWith<$Res>? get certInfo { @@ -2100,6 +2247,8 @@ class __$$PivSlotImplCopyWithImpl<$Res> _$PivSlotImpl _value, $Res Function(_$PivSlotImpl) _then) : super(_value, _then); + /// Create a copy of PivSlot + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -2156,11 +2305,13 @@ class _$PivSlotImpl implements _PivSlot { other.certInfo == certInfo)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, slot, metadata, certInfo); - @JsonKey(ignore: true) + /// Create a copy of PivSlot + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PivSlotImplCopyWith<_$PivSlotImpl> get copyWith => @@ -2188,8 +2339,11 @@ abstract class _PivSlot implements PivSlot { SlotMetadata? get metadata; @override CertInfo? get certInfo; + + /// Create a copy of PivSlot + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$PivSlotImplCopyWith<_$PivSlotImpl> get copyWith => throw _privateConstructorUsedError; } @@ -2251,6 +2405,8 @@ mixin _$PivExamineResult { required TResult orElse(), }) => throw _privateConstructorUsedError; + + /// Serializes this PivExamineResult to a JSON map. Map toJson() => throw _privateConstructorUsedError; } @@ -2270,6 +2426,9 @@ class _$PivExamineResultCopyWithImpl<$Res, $Val extends PivExamineResult> final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of PivExamineResult + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -2291,6 +2450,8 @@ class __$$ExamineResultImplCopyWithImpl<$Res> _$ExamineResultImpl _value, $Res Function(_$ExamineResultImpl) _then) : super(_value, _then); + /// Create a copy of PivExamineResult + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -2314,6 +2475,8 @@ class __$$ExamineResultImplCopyWithImpl<$Res> )); } + /// Create a copy of PivExamineResult + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $CertInfoCopyWith<$Res>? get certInfo { @@ -2367,11 +2530,13 @@ class _$ExamineResultImpl implements _ExamineResult { other.certInfo == certInfo)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, password, keyType, certInfo); - @JsonKey(ignore: true) + /// Create a copy of PivExamineResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ExamineResultImplCopyWith<_$ExamineResultImpl> get copyWith => @@ -2463,7 +2628,10 @@ abstract class _ExamineResult implements PivExamineResult { bool get password; KeyType? get keyType; CertInfo? get certInfo; - @JsonKey(ignore: true) + + /// Create a copy of PivExamineResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$ExamineResultImplCopyWith<_$ExamineResultImpl> get copyWith => throw _privateConstructorUsedError; } @@ -2482,6 +2650,9 @@ class __$$InvalidPasswordImplCopyWithImpl<$Res> __$$InvalidPasswordImplCopyWithImpl( _$InvalidPasswordImpl _value, $Res Function(_$InvalidPasswordImpl) _then) : super(_value, _then); + + /// Create a copy of PivExamineResult + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -2507,7 +2678,7 @@ class _$InvalidPasswordImpl implements _InvalidPassword { (other.runtimeType == runtimeType && other is _$InvalidPasswordImpl); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => runtimeType.hashCode; @@ -2661,6 +2832,9 @@ class _$PivGenerateParametersCopyWithImpl<$Res, final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of PivGenerateParameters + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -2677,6 +2851,9 @@ class __$$GeneratePublicKeyImplCopyWithImpl<$Res> __$$GeneratePublicKeyImplCopyWithImpl(_$GeneratePublicKeyImpl _value, $Res Function(_$GeneratePublicKeyImpl) _then) : super(_value, _then); + + /// Create a copy of PivGenerateParameters + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -2792,6 +2969,8 @@ class __$$GenerateCertificateImplCopyWithImpl<$Res> $Res Function(_$GenerateCertificateImpl) _then) : super(_value, _then); + /// Create a copy of PivGenerateParameters + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -2848,7 +3027,9 @@ class _$GenerateCertificateImpl implements _GenerateCertificate { @override int get hashCode => Object.hash(runtimeType, subject, validFrom, validTo); - @JsonKey(ignore: true) + /// Create a copy of PivGenerateParameters + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$GenerateCertificateImplCopyWith<_$GenerateCertificateImpl> get copyWith => @@ -2937,7 +3118,10 @@ abstract class _GenerateCertificate implements PivGenerateParameters { String get subject; DateTime get validFrom; DateTime get validTo; - @JsonKey(ignore: true) + + /// Create a copy of PivGenerateParameters + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$GenerateCertificateImplCopyWith<_$GenerateCertificateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -2959,6 +3143,8 @@ class __$$GenerateCsrImplCopyWithImpl<$Res> _$GenerateCsrImpl _value, $Res Function(_$GenerateCsrImpl) _then) : super(_value, _then); + /// Create a copy of PivGenerateParameters + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -2997,7 +3183,9 @@ class _$GenerateCsrImpl implements _GenerateCsr { @override int get hashCode => Object.hash(runtimeType, subject); - @JsonKey(ignore: true) + /// Create a copy of PivGenerateParameters + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$GenerateCsrImplCopyWith<_$GenerateCsrImpl> get copyWith => @@ -3080,7 +3268,10 @@ abstract class _GenerateCsr implements PivGenerateParameters { factory _GenerateCsr({required final String subject}) = _$GenerateCsrImpl; String get subject; - @JsonKey(ignore: true) + + /// Create a copy of PivGenerateParameters + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$GenerateCsrImplCopyWith<_$GenerateCsrImpl> get copyWith => throw _privateConstructorUsedError; } @@ -3095,8 +3286,12 @@ mixin _$PivGenerateResult { String get publicKey => throw _privateConstructorUsedError; String? get result => throw _privateConstructorUsedError; + /// Serializes this PivGenerateResult to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of PivGenerateResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $PivGenerateResultCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -3120,6 +3315,8 @@ class _$PivGenerateResultCopyWithImpl<$Res, $Val extends PivGenerateResult> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of PivGenerateResult + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -3163,6 +3360,8 @@ class __$$PivGenerateResultImplCopyWithImpl<$Res> $Res Function(_$PivGenerateResultImpl) _then) : super(_value, _then); + /// Create a copy of PivGenerateResult + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -3220,11 +3419,13 @@ class _$PivGenerateResultImpl implements _PivGenerateResult { (identical(other.result, result) || other.result == result)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, generateType, publicKey, result); - @JsonKey(ignore: true) + /// Create a copy of PivGenerateResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PivGenerateResultImplCopyWith<_$PivGenerateResultImpl> get copyWith => @@ -3254,8 +3455,11 @@ abstract class _PivGenerateResult implements PivGenerateResult { String get publicKey; @override String? get result; + + /// Create a copy of PivGenerateResult + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$PivGenerateResultImplCopyWith<_$PivGenerateResultImpl> get copyWith => throw _privateConstructorUsedError; } @@ -3270,8 +3474,12 @@ mixin _$PivImportResult { String? get publicKey => throw _privateConstructorUsedError; String? get certificate => throw _privateConstructorUsedError; + /// Serializes this PivImportResult to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of PivImportResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $PivImportResultCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -3297,6 +3505,8 @@ class _$PivImportResultCopyWithImpl<$Res, $Val extends PivImportResult> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of PivImportResult + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -3320,6 +3530,8 @@ class _$PivImportResultCopyWithImpl<$Res, $Val extends PivImportResult> ) as $Val); } + /// Create a copy of PivImportResult + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $SlotMetadataCopyWith<$Res>? get metadata { @@ -3355,6 +3567,8 @@ class __$$PivImportResultImplCopyWithImpl<$Res> _$PivImportResultImpl _value, $Res Function(_$PivImportResultImpl) _then) : super(_value, _then); + /// Create a copy of PivImportResult + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -3415,12 +3629,14 @@ class _$PivImportResultImpl implements _PivImportResult { other.certificate == certificate)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, metadata, publicKey, certificate); - @JsonKey(ignore: true) + /// Create a copy of PivImportResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PivImportResultImplCopyWith<_$PivImportResultImpl> get copyWith => @@ -3450,8 +3666,11 @@ abstract class _PivImportResult implements PivImportResult { String? get publicKey; @override String? get certificate; + + /// Create a copy of PivImportResult + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$PivImportResultImplCopyWith<_$PivImportResultImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/piv/models.g.dart b/lib/piv/models.g.dart index 0d04fda61..f32beaa38 100644 --- a/lib/piv/models.g.dart +++ b/lib/piv/models.g.dart @@ -9,8 +9,8 @@ part of 'models.dart'; _$PinMetadataImpl _$$PinMetadataImplFromJson(Map json) => _$PinMetadataImpl( json['default_value'] as bool, - json['total_attempts'] as int, - json['attempts_remaining'] as int, + (json['total_attempts'] as num).toInt(), + (json['attempts_remaining'] as num).toInt(), ); Map _$$PinMetadataImplToJson(_$PinMetadataImpl instance) => @@ -113,7 +113,8 @@ _$PivStateImpl _$$PivStateImplFromJson(Map json) => authenticated: json['authenticated'] as bool, derivedKey: json['derived_key'] as bool, storedKey: json['stored_key'] as bool, - pinAttempts: json['pin_attempts'] as int, + pinAttempts: (json['pin_attempts'] as num).toInt(), + supportsBio: json['supports_bio'] as bool, chuid: json['chuid'] as String?, ccc: json['ccc'] as String?, metadata: json['metadata'] == null @@ -128,6 +129,7 @@ Map _$$PivStateImplToJson(_$PivStateImpl instance) => 'derived_key': instance.derivedKey, 'stored_key': instance.storedKey, 'pin_attempts': instance.pinAttempts, + 'supports_bio': instance.supportsBio, 'chuid': instance.chuid, 'ccc': instance.ccc, 'metadata': instance.metadata, @@ -157,7 +159,7 @@ Map _$$CertInfoImplToJson(_$CertInfoImpl instance) => _$PivSlotImpl _$$PivSlotImplFromJson(Map json) => _$PivSlotImpl( - slot: SlotId.fromJson(json['slot'] as int), + slot: SlotId.fromJson((json['slot'] as num).toInt()), metadata: json['metadata'] == null ? null : SlotMetadata.fromJson(json['metadata'] as Map), diff --git a/lib/piv/views/actions.dart b/lib/piv/views/actions.dart index d70bb3aca..86d542229 100644 --- a/lib/piv/views/actions.dart +++ b/lib/piv/views/actions.dart @@ -310,7 +310,26 @@ class PivActions extends ConsumerWidget { } List buildSlotActions( - PivState pivState, PivSlot slot, AppLocalizations l10n) { + PivState pivState, PivSlot slot, bool fipsUnready, AppLocalizations l10n) { + if (fipsUnready) { + return [ + ActionItem( + key: keys.generateAction, + feature: features.slotsGenerate, + icon: const Icon(Symbols.add), + actionStyle: ActionStyle.primary, + title: l10n.s_generate_key, + subtitle: l10n.l_change_defaults, + ), + ActionItem( + key: keys.importAction, + feature: features.slotsImport, + icon: const Icon(Symbols.file_download), + title: l10n.l_import_file, + subtitle: l10n.l_change_defaults, + ), + ]; + } final hasCert = slot.certInfo != null; final hasKey = slot.metadata != null; final canDeleteOrMoveKey = hasKey && pivState.version.isAtLeast(5, 7); diff --git a/lib/piv/views/cert_info_view.dart b/lib/piv/views/cert_info_view.dart index 72b1c08bc..945b1a7cf 100644 --- a/lib/piv/views/cert_info_view.dart +++ b/lib/piv/views/cert_info_view.dart @@ -28,9 +28,10 @@ class CertInfoTable extends ConsumerWidget { final CertInfo? certInfo; final SlotMetadata? metadata; final bool alwaysIncludePrivate; + final bool supportsBio; const CertInfoTable(this.certInfo, this.metadata, - {super.key, this.alwaysIncludePrivate = false}); + {super.key, this.alwaysIncludePrivate = false, this.supportsBio = false}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -46,6 +47,16 @@ class CertInfoTable extends ConsumerWidget { metadata.keyType.getDisplayName(l10n), keys.slotMetadataKeyType ), + if (metadata != null && + metadata.pinPolicy != PinPolicy.never && + supportsBio) + l10n.s_biometrics: ( + [PinPolicy.matchAlways, PinPolicy.matchOnce] + .contains(metadata.pinPolicy) + ? l10n.s_enabled + : l10n.s_disabled, + keys.slotMetadataBiometrics + ), if (metadata == null && alwaysIncludePrivate) l10n.s_private_key: (l10n.s_none, keys.slotMetadataKeyType), if (certInfo != null) ...{ diff --git a/lib/piv/views/generate_key_dialog.dart b/lib/piv/views/generate_key_dialog.dart index 11d85f049..92630c37d 100644 --- a/lib/piv/views/generate_key_dialog.dart +++ b/lib/piv/views/generate_key_dialog.dart @@ -36,8 +36,9 @@ class GenerateKeyDialog extends ConsumerStatefulWidget { final DevicePath devicePath; final PivState pivState; final PivSlot pivSlot; - const GenerateKeyDialog(this.devicePath, this.pivState, this.pivSlot, - {super.key}); + final bool showMatch; + GenerateKeyDialog(this.devicePath, this.pivState, this.pivSlot, {super.key}) + : showMatch = pivSlot.slot != SlotId.cardAuth && pivState.supportsBio; @override ConsumerState createState() => @@ -53,6 +54,7 @@ class _GenerateKeyDialogState extends ConsumerState { late DateTime _validTo; late DateTime _validToDefault; late DateTime _validToMax; + late bool _allowMatch; bool _generating = false; @override @@ -64,6 +66,8 @@ class _GenerateKeyDialogState extends ConsumerState { _validToDefault = DateTime.utc(now.year + 1, now.month, now.day); _validTo = _validToDefault; _validToMax = DateTime.utc(now.year + 10, now.month, now.day); + + _allowMatch = widget.showMatch; } @override @@ -117,6 +121,7 @@ class _GenerateKeyDialogState extends ConsumerState { final result = await pivNotifier.generate( widget.pivSlot.slot, _keyType, + pinPolicy: getPinPolicy(widget.pivSlot.slot, _allowMatch), parameters: switch (_generateType) { GenerateType.publicKey => PivGenerateParameters.publicKey(), @@ -183,7 +188,9 @@ class _GenerateKeyDialogState extends ConsumerState { l10n.s_options, style: textTheme.bodyLarge, ), - Text(l10n.p_cert_options_desc), + Text(widget.showMatch + ? l10n.p_cert_options_bio_desc + : l10n.p_cert_options_desc), Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 4.0, @@ -221,8 +228,6 @@ class _GenerateKeyDialogState extends ConsumerState { ), if (_generateType == GenerateType.certificate) FilterChip( - backgroundColor: - Theme.of(context).colorScheme.surfaceVariant, label: Text(dateFormatter.format(_validTo)), onSelected: _generating ? null @@ -240,6 +245,18 @@ class _GenerateKeyDialogState extends ConsumerState { } }, ), + if (widget.showMatch) + FilterChip( + label: Text(l10n.s_allow_fingerprint), + selected: _allowMatch, + onSelected: _generating + ? null + : (value) { + setState(() { + _allowMatch = value; + }); + }, + ), ]), Padding( padding: const EdgeInsets.symmetric(vertical: 4), diff --git a/lib/piv/views/import_file_dialog.dart b/lib/piv/views/import_file_dialog.dart index f65a3e518..6fc4baa82 100644 --- a/lib/piv/views/import_file_dialog.dart +++ b/lib/piv/views/import_file_dialog.dart @@ -39,9 +39,10 @@ class ImportFileDialog extends ConsumerStatefulWidget { final PivState pivState; final PivSlot pivSlot; final File file; - const ImportFileDialog( - this.devicePath, this.pivState, this.pivSlot, this.file, - {super.key}); + final bool showMatch; + ImportFileDialog(this.devicePath, this.pivState, this.pivSlot, this.file, + {super.key}) + : showMatch = pivSlot.slot != SlotId.cardAuth && pivState.supportsBio; @override ConsumerState createState() => @@ -50,6 +51,7 @@ class ImportFileDialog extends ConsumerStatefulWidget { class _ImportFileDialogState extends ConsumerState { late String _data; + late bool _allowMatch; PivExamineResult? _state; String _password = ''; bool _passwordIsWrong = false; @@ -59,6 +61,8 @@ class _ImportFileDialogState extends ConsumerState { @override void initState() { super.initState(); + + _allowMatch = widget.showMatch; _init(); } @@ -214,9 +218,13 @@ class _ImportFileDialogState extends ConsumerState { )); await ref .read(pivSlotsProvider(widget.devicePath).notifier) - .import(widget.pivSlot.slot, _data, - password: - _password.isNotEmpty ? _password : null); + .import( + widget.pivSlot.slot, + _data, + password: _password.isNotEmpty ? _password : null, + pinPolicy: getPinPolicy( + widget.pivSlot.slot, _allowMatch), + ); await withContext( (context) async { Navigator.of(context).pop(true); @@ -297,7 +305,25 @@ class _ImportFileDialogState extends ConsumerState { 140, // Needed for layout, adapt if text sizes changes child: CertInfoTable(certInfo, null), ), - ] + ], + if (keyType != null && !unsupportedKey && widget.showMatch) ...[ + Text( + l10n.s_options, + style: textTheme.bodyLarge, + ), + Text(l10n.p_key_options_bio_desc), + FilterChip( + label: Text(l10n.s_allow_fingerprint), + selected: _allowMatch, + onSelected: _importing + ? null + : (value) { + setState(() { + _allowMatch = value; + }); + }, + ), + ], ] .map((e) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), diff --git a/lib/piv/views/key_actions.dart b/lib/piv/views/key_actions.dart index d6916a210..deab1acc5 100644 --- a/lib/piv/views/key_actions.dart +++ b/lib/piv/views/key_actions.dart @@ -21,7 +21,9 @@ import 'package:material_symbols_icons/symbols.dart'; import '../../app/message.dart'; import '../../app/models.dart'; +import '../../app/state.dart'; import '../../app/views/action_list.dart'; +import '../../management/models.dart'; import '../features.dart' as features; import '../keys.dart' as keys; import '../models.dart'; @@ -52,6 +54,10 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, final pukAttempts = pivState.metadata?.pukMetadata.attemptsRemaining; final alertIcon = Icon(Symbols.warning_amber, color: colors.tertiary); + final deviceData = ref.read(currentDeviceDataProvider).valueOrNull; + final isBio = [FormFactor.usbABio, FormFactor.usbCBio] + .contains(deviceData?.info.formFactor); + return Column( children: [ ActionListSection( @@ -85,32 +91,35 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath, ); } : null), - ActionListItem( - key: keys.managePukAction, - feature: features.actionsPuk, - title: l10n.s_change_puk, - subtitle: pukAttempts != null - ? (pukAttempts == 0 - ? l10n.l_piv_pin_puk_blocked - : usingDefaultPuk - ? '${l10n.l_attempts_remaining(pukAttempts)}\n${l10n.l_warning_default_puk}' - : l10n.l_attempts_remaining(pukAttempts)) - : usingDefaultPuk - ? l10n.l_warning_default_puk - : null, - icon: const Icon(Symbols.pin), - trailing: pukAttempts == 0 || usingDefaultPuk ? alertIcon : null, - onTap: pukAttempts != 0 - ? (context) { - Navigator.of(context).popUntil((route) => route.isFirst); - showBlurDialog( - context: context, - builder: (context) => ManagePinPukDialog( - devicePath, pivState, - target: ManageTarget.puk), - ); - } - : null), + if (!isBio) + ActionListItem( + key: keys.managePukAction, + feature: features.actionsPuk, + title: l10n.s_change_puk, + subtitle: pukAttempts != null + ? (pukAttempts == 0 + ? l10n.l_piv_pin_puk_blocked + : usingDefaultPuk + ? '${l10n.l_attempts_remaining(pukAttempts)}\n${l10n.l_warning_default_puk}' + : l10n.l_attempts_remaining(pukAttempts)) + : usingDefaultPuk + ? l10n.l_warning_default_puk + : null, + icon: const Icon(Symbols.pin), + trailing: + pukAttempts == 0 || usingDefaultPuk ? alertIcon : null, + onTap: pukAttempts != 0 + ? (context) { + Navigator.of(context) + .popUntil((route) => route.isFirst); + showBlurDialog( + context: context, + builder: (context) => ManagePinPukDialog( + devicePath, pivState, + target: ManageTarget.puk), + ); + } + : null), ActionListItem( key: keys.manageManagementKeyAction, feature: features.actionsManagementKey, diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index 7df10a3a1..77aae7e54 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -25,6 +25,7 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; import '../../core/models.dart'; +import '../../management/models.dart'; import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/app_text_form_field.dart'; @@ -176,6 +177,17 @@ class _ManageKeyDialogState extends ConsumerState { ? currentKeyOrPin.length >= 4 : currentKeyOrPin.length == currentType.keyLength * 2; final newLenOk = _keyController.text.length == hexLength; + final (fipsCapable, fipsApproved) = ref + .watch(currentDeviceDataProvider) + .valueOrNull + ?.info + .getFipsStatus(Capability.piv) ?? + (false, false); + final fipsUnready = fipsCapable && !fipsApproved; + final managementKeyTypes = ManagementKeyType.values.toList(); + if (fipsCapable) { + managementKeyTypes.remove(ManagementKeyType.tdes); + } return ResponsiveDialog( title: Text(l10n.l_change_management_key), @@ -280,7 +292,7 @@ class _ManageKeyDialogState extends ConsumerState { }, ).init(), AppTextField( - key: keys.newPinPukField, + key: keys.newManagementKeyField, autofocus: _defaultKeyUsed, autofillHints: const [AutofillHints.newPassword], maxLength: hexLength, @@ -334,7 +346,7 @@ class _ManageKeyDialogState extends ConsumerState { children: [ if (widget.pivState.metadata != null) ChoiceFilterChip( - items: ManagementKeyType.values, + items: managementKeyTypes, value: _keyType, selected: _keyType != currentType, itemBuilder: (value) => Text(value.getDisplayName(l10n)), @@ -344,18 +356,17 @@ class _ManageKeyDialogState extends ConsumerState { }); }, ), - FilterChip( - key: keys.pinLockManagementKeyChip, - backgroundColor: - Theme.of(context).colorScheme.surfaceVariant, - label: Text(l10n.s_protect_key), - selected: _storeKey, - onSelected: (value) { - setState(() { - _storeKey = value; - }); - }, - ), + if (!fipsUnready) + FilterChip( + key: keys.pinLockManagementKeyChip, + label: Text(l10n.s_protect_key), + selected: _storeKey, + onSelected: (value) { + setState(() { + _storeKey = value; + }); + }, + ), ]), ] .map((e) => Padding( diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart index f0a51a96c..0a0572596 100644 --- a/lib/piv/views/manage_pin_puk_dialog.dart +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -22,6 +22,7 @@ import 'package:material_symbols_icons/symbols.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; +import '../../management/models.dart'; import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/responsive_dialog.dart'; @@ -60,14 +61,11 @@ class _ManagePinPukDialogState extends ConsumerState { bool _isObscureConfirm = true; late final bool _defaultPinUsed; late final bool _defaultPukUsed; - late final int _minPinLen; @override void initState() { super.initState(); - // Old YubiKeys allowed a 4 digit PIN - _minPinLen = widget.pivState.version.isAtLeast(4, 3, 1) ? 6 : 4; _defaultPinUsed = widget.pivState.metadata?.pinMetadata.defaultValue ?? false; _defaultPukUsed = @@ -162,9 +160,21 @@ class _ManagePinPukDialogState extends ConsumerState { final showDefaultPukUsed = widget.target != ManageTarget.pin && _defaultPukUsed; - final hasPinComplexity = - ref.read(currentDeviceDataProvider).valueOrNull?.info.pinComplexity ?? - false; + final deviceData = ref.read(currentDeviceDataProvider).valueOrNull; + final hasPinComplexity = deviceData?.info.pinComplexity ?? false; + final isBio = [FormFactor.usbABio, FormFactor.usbCBio] + .contains(deviceData?.info.formFactor); + + final isFipsCapable = + deviceData?.info.getFipsStatus(Capability.piv).$1 ?? false; + + // Old YubiKeys allowed a 4 digit PIN + final currentMinPinLen = isFipsCapable + ? 8 + : widget.pivState.version.isAtLeast(4, 3, 1) + ? 6 + : 4; + final newMinPinLen = currentMinPinLen > 4 ? currentMinPinLen : 6; return ResponsiveDialog( title: Text(titleText), @@ -206,7 +216,7 @@ class _ManagePinPukDialogState extends ConsumerState { ? l10n.s_current_pin : l10n.s_current_puk, errorText: _pinIsBlocked - ? (widget.target == ManageTarget.pin + ? (widget.target == ManageTarget.pin && !isBio ? l10n.l_piv_pin_blocked : l10n.l_piv_pin_puk_blocked) : (_currentIsWrong @@ -242,10 +252,11 @@ class _ManagePinPukDialogState extends ConsumerState { Text(hasPinComplexity ? l10n.p_enter_new_piv_pin_puk_complexity_active( widget.target == ManageTarget.puk ? l10n.s_puk : l10n.s_pin, + newMinPinLen, '123456') - : l10n.p_enter_new_piv_pin_puk(widget.target == ManageTarget.puk - ? l10n.s_puk - : l10n.s_pin)), + : l10n.p_enter_new_piv_pin_puk( + widget.target == ManageTarget.puk ? l10n.s_puk : l10n.s_pin, + newMinPinLen)), AppTextField( key: keys.newPinPukField, autofocus: showDefaultPinUsed || showDefaultPukUsed, @@ -276,7 +287,8 @@ class _ManagePinPukDialogState extends ConsumerState { ? (_isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin) : (_isObscureNew ? l10n.s_show_puk : l10n.s_hide_puk), ), - enabled: currentPinLen >= _minPinLen, + enabled: currentPinLen >= currentMinPinLen || + (isFipsCapable && showDefaultPinUsed), ), textInputAction: TextInputAction.next, onChanged: (value) { @@ -316,7 +328,7 @@ class _ManagePinPukDialogState extends ConsumerState { ? (_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin) : (_isObscureConfirm ? l10n.s_show_puk : l10n.s_hide_puk), ), - enabled: currentPinLen >= _minPinLen && newPinLen >= 6, + enabled: newPinLen >= newMinPinLen, errorText: newPinLen == _confirmPin.length && newPin != _confirmPin ? (widget.target == ManageTarget.pin || diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart index 7fc256374..e4a8139fa 100644 --- a/lib/piv/views/piv_screen.dart +++ b/lib/piv/views/piv_screen.dart @@ -24,6 +24,7 @@ import 'package:material_symbols_icons/symbols.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/shortcuts.dart'; +import '../../app/state.dart'; import '../../app/views/action_list.dart'; import '../../app/views/app_failure_page.dart'; import '../../app/views/app_list_item.dart'; @@ -57,6 +58,14 @@ class _PivScreenState extends ConsumerState { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final hasFeature = ref.watch(featureProvider); + final (fipsCapable, fipsApproved) = ref + .watch(currentDeviceDataProvider) + .valueOrNull + ?.info + .getFipsStatus(Capability.piv) ?? + (false, false); + final fipsUnready = fipsCapable && !fipsApproved; + return ref.watch(pivStateProvider(widget.devicePath)).when( loading: () => MessagePage( title: l10n.s_certificates, @@ -149,6 +158,7 @@ class _PivScreenState extends ConsumerState { selected.metadata, alwaysIncludePrivate: pivState.supportsMetadata, + supportsBio: pivState.supportsBio, ), if (selected.certInfo == null) const SizedBox(height: 16) @@ -168,8 +178,8 @@ class _PivScreenState extends ConsumerState { ActionListSection.fromMenuActions( context, l10n.s_actions, - actions: - buildSlotActions(pivState, selected, l10n), + actions: buildSlotActions( + pivState, selected, fipsUnready, l10n), ), ], ) @@ -200,25 +210,30 @@ class _PivScreenState extends ConsumerState { return null; }), }, - child: Column( - children: [ - ...normalSlots.map( - (e) => _CertificateListItem( - pivState, - e, - expanded: expanded, - selected: e == selected, - ), - ), - ...shownRetiredSlots.map( - (e) => _CertificateListItem( - pivState, - e, - expanded: expanded, - selected: e == selected, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Column( + children: [ + ...normalSlots.map( + (e) => _CertificateListItem( + pivState, + e, + expanded: expanded, + selected: e == selected, + fipsUnready: fipsUnready, + ), ), - ) - ], + ...shownRetiredSlots.map( + (e) => _CertificateListItem( + pivState, + e, + expanded: expanded, + selected: e == selected, + fipsUnready: fipsUnready, + ), + ) + ], + ), ), ); }, @@ -235,9 +250,12 @@ class _CertificateListItem extends ConsumerWidget { final PivSlot pivSlot; final bool expanded; final bool selected; + final bool fipsUnready; const _CertificateListItem(this.pivState, this.pivSlot, - {required this.expanded, required this.selected}); + {required this.expanded, + required this.selected, + required this.fipsUnready}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -272,8 +290,8 @@ class _CertificateListItem extends ConsumerWidget { ), tapIntent: isDesktop && !expanded ? null : OpenIntent(pivSlot), doubleTapIntent: isDesktop && !expanded ? OpenIntent(pivSlot) : null, - buildPopupActions: hasFeature(features.slots) - ? (context) => buildSlotActions(pivState, pivSlot, l10n) + buildPopupActions: hasFeature(features.slots) && !fipsUnready + ? (context) => buildSlotActions(pivState, pivSlot, fipsUnready, l10n) : null, ); } diff --git a/lib/piv/views/slot_dialog.dart b/lib/piv/views/slot_dialog.dart index 79c9dabbe..c1cabe4ef 100644 --- a/lib/piv/views/slot_dialog.dart +++ b/lib/piv/views/slot_dialog.dart @@ -22,6 +22,7 @@ import '../../app/shortcuts.dart'; import '../../app/state.dart'; import '../../app/views/action_list.dart'; import '../../app/views/fs_dialog.dart'; +import '../../management/models.dart'; import '../models.dart'; import '../state.dart'; import 'actions.dart'; @@ -34,12 +35,13 @@ class SlotDialog extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // TODO: Solve this in a cleaner way - final node = ref.watch(currentDeviceDataProvider).valueOrNull?.node; - if (node == null) { + var keyData = ref.watch(currentDeviceDataProvider).valueOrNull; + if (keyData == 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 devicePath = keyData.node.path; final l10n = AppLocalizations.of(context)!; final textTheme = Theme.of(context).textTheme; @@ -48,8 +50,11 @@ class SlotDialog extends ConsumerWidget { color: Theme.of(context).colorScheme.onSurfaceVariant, ); - final pivState = ref.watch(pivStateProvider(node.path)).valueOrNull; - final slotData = ref.watch(pivSlotsProvider(node.path).select((value) => + final (fipsCapable, fipsApproved) = + keyData.info.getFipsStatus(Capability.piv); + + final pivState = ref.watch(pivStateProvider(devicePath)).valueOrNull; + final slotData = ref.watch(pivSlotsProvider(devicePath).select((value) => value.whenOrNull( data: (data) => data.firstWhere((element) => element.slot == pivSlot)))); @@ -61,7 +66,7 @@ class SlotDialog extends ConsumerWidget { final certInfo = slotData.certInfo; final metadata = slotData.metadata; return PivActions( - devicePath: node.path, + devicePath: devicePath, pivState: pivState, builder: (context) => ItemShortcuts( item: slotData, @@ -93,6 +98,7 @@ class SlotDialog extends ConsumerWidget { certInfo, metadata, alwaysIncludePrivate: pivState.supportsMetadata, + supportsBio: pivState.supportsBio, ), if (certInfo == null) const SizedBox(height: 16), ], @@ -113,7 +119,8 @@ class SlotDialog extends ConsumerWidget { ActionListSection.fromMenuActions( context, l10n.s_actions, - actions: buildSlotActions(pivState, slotData, l10n), + actions: buildSlotActions( + pivState, slotData, fipsCapable && !fipsApproved, l10n), ), ], ), diff --git a/lib/piv/views/utils.dart b/lib/piv/views/utils.dart index c0357d2a3..bc0824648 100644 --- a/lib/piv/views/utils.dart +++ b/lib/piv/views/utils.dart @@ -27,5 +27,17 @@ List getSupportedKeyTypes(Version version, bool isFips) => [ if (!isFips) KeyType.x25519, ], KeyType.eccp256, - KeyType.eccp384, + if (version.isAtLeast(4, 0)) ...[ + KeyType.eccp384, + ] ]; + +PinPolicy getPinPolicy(SlotId slot, bool match) { + if (match) { + if (slot == SlotId.signature) { + return PinPolicy.matchAlways; + } + return PinPolicy.matchOnce; + } + return PinPolicy.dfault; +} diff --git a/lib/theme.dart b/lib/theme.dart index 86f50d6e9..d929fa31f 100755 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -25,68 +25,81 @@ class AppTheme { Brightness.dark => getDarkTheme(primaryColor), }; - static ThemeData getLightTheme(Color primaryColor) => ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed( - brightness: Brightness.light, - seedColor: primaryColor, - onSurface: const Color(0xbb000000), - onSurfaceVariant: const Color(0x99000000), - ), - fontFamily: 'Roboto', - appBarTheme: const AppBarTheme( - color: Colors.transparent, - ), - listTileTheme: const ListTileThemeData( - // For alignment under menu button - contentPadding: EdgeInsets.symmetric(horizontal: 18.0), - visualDensity: VisualDensity.compact, - ), - tooltipTheme: const TooltipThemeData( - waitDuration: Duration(milliseconds: 500), - textStyle: TextStyle(color: Color(0xff3c3c3c)), - decoration: BoxDecoration( - color: Color(0xffe2e2e6), - borderRadius: BorderRadius.all(Radius.circular(8.0)), + static ColorScheme _colorScheme(Brightness brightness, Color primaryColor) => + switch (brightness) { + Brightness.dark => ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: brightness, + surface: const Color(0xff282828), + onSurface: const Color(0xeeffffff), + onSurfaceVariant: const Color(0xaaffffff), ), - ), - ); + Brightness.light => ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: brightness, + onSurface: const Color(0xbb000000), + onSurfaceVariant: const Color(0x99000000), + ) + }; - static ThemeData getDarkTheme(Color primaryColor) => ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed( - brightness: Brightness.dark, - seedColor: primaryColor, - background: const Color(0xff282828), - onSurface: const Color(0xeeffffff), - onSurfaceVariant: const Color(0xaaffffff), + static ThemeData getLightTheme(Color primaryColor) { + final colorScheme = _colorScheme(Brightness.light, primaryColor); + return ThemeData( + useMaterial3: true, + colorScheme: colorScheme, + fontFamily: 'Roboto', + appBarTheme: const AppBarTheme( + color: Colors.transparent, + ), + listTileTheme: const ListTileThemeData( + // For alignment under menu button + contentPadding: EdgeInsets.symmetric(horizontal: 18.0), + visualDensity: VisualDensity.compact, + ), + tooltipTheme: const TooltipThemeData( + waitDuration: Duration(milliseconds: 500), + textStyle: TextStyle(color: Color(0xff3c3c3c)), + decoration: BoxDecoration( + color: Color(0xffe2e2e6), + borderRadius: BorderRadius.all(Radius.circular(8.0)), ), - fontFamily: 'Roboto', - appBarTheme: const AppBarTheme( - color: Colors.transparent, - ), - listTileTheme: const ListTileThemeData( - // For alignment under menu button - contentPadding: EdgeInsets.symmetric(horizontal: 18.0), - visualDensity: VisualDensity.compact, - ), - tooltipTheme: const TooltipThemeData( - waitDuration: Duration(milliseconds: 500), - textStyle: TextStyle(color: Color(0xffE2E2E6)), - decoration: BoxDecoration( - color: Color(0xff3c3c3c), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), + ), + chipTheme: ChipThemeData( + backgroundColor: colorScheme.surfaceContainerHighest, + labelStyle: + TextStyle(fontFamily: 'Roboto', color: colorScheme.onSurface), + ), + ); + } + + static ThemeData getDarkTheme(Color primaryColor) { + final colorScheme = _colorScheme(Brightness.dark, primaryColor); + return ThemeData( + useMaterial3: true, + colorScheme: colorScheme, + fontFamily: 'Roboto', + scaffoldBackgroundColor: colorScheme.surface, + appBarTheme: const AppBarTheme( + color: Colors.transparent, + ), + listTileTheme: const ListTileThemeData( + // For alignment under menu button + contentPadding: EdgeInsets.symmetric(horizontal: 18.0), + visualDensity: VisualDensity.compact, + ), + tooltipTheme: const TooltipThemeData( + waitDuration: Duration(milliseconds: 500), + textStyle: TextStyle(color: Color(0xffE2E2E6)), + decoration: BoxDecoration( + color: Color(0xff3c3c3c), + borderRadius: BorderRadius.all(Radius.circular(8.0)), ), - ); + ), + chipTheme: ChipThemeData( + backgroundColor: colorScheme.surfaceContainerHighest, + labelStyle: + TextStyle(fontFamily: 'Roboto', color: colorScheme.onSurface), + ), + ); + } } - -/* TODO: Remove this. It is left here as a reference as we adjust styles to work with Flutter 3.7. -/// This fixes the issue with FilterChip resizing vertically on toggle. -BorderSide? _chipBorder(Color color) => - MaterialStateBorderSide.resolveWith((states) => BorderSide( - width: 1, - color: states.contains(MaterialState.selected) - ? Colors.transparent - : color)); -*/ diff --git a/lib/widgets/choice_filter_chip.dart b/lib/widgets/choice_filter_chip.dart index 83fbf85cb..ee4ead2ac 100755 --- a/lib/widgets/choice_filter_chip.dart +++ b/lib/widgets/choice_filter_chip.dart @@ -71,7 +71,7 @@ class _ChoiceFilterChipState extends State> { shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(4)), ), - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surfaceContainerHighest, popUpAnimationStyle: AnimationStyle(duration: Duration.zero), items: widget.items .map((e) => PopupMenuItem( @@ -91,7 +91,6 @@ class _ChoiceFilterChipState extends State> { return FilterChip( tooltip: widget.tooltip, avatar: widget.avatar, - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, labelPadding: const EdgeInsets.only(left: 4), label: Row( mainAxisSize: MainAxisSize.min, @@ -102,7 +101,7 @@ class _ChoiceFilterChipState extends State> { child: Icon( _showing ? Symbols.arrow_drop_up : Symbols.arrow_drop_down, color: ChipTheme.of(context).checkmarkColor, - size: 18, + size: 16, ), ), ], diff --git a/lib/widgets/custom_icons.dart b/lib/widgets/custom_icons.dart deleted file mode 100755 index 44a053d99..000000000 --- a/lib/widgets/custom_icons.dart +++ /dev/null @@ -1,68 +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:material_symbols_icons/symbols.dart'; - -final Widget pushPinStrokeIcon = Builder(builder: (context) { - return CustomPaint( - painter: _StrikethroughPainter(IconTheme.of(context).color ?? Colors.black), - child: ClipPath( - clipper: _StrikethroughClipper(), child: const Icon(Symbols.push_pin)), - ); -}); - -class _StrikethroughClipper extends CustomClipper { - @override - Path getClip(Size size) { - Path path = Path() - ..moveTo(0, 2) - ..lineTo(0, size.height) - ..lineTo(size.width - 2, size.height) - ..lineTo(0, 2) - ..moveTo(2, 0) - ..lineTo(size.width, size.height - 2) - ..lineTo(size.width, 0) - ..lineTo(2, 0) - ..close(); - return path; - } - - @override - bool shouldReclip(covariant CustomClipper oldClipper) { - return false; - } -} - -class _StrikethroughPainter extends CustomPainter { - final Color color; - _StrikethroughPainter(this.color); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..strokeWidth = 1.3; - - canvas.drawLine(Offset(size.width * 0.15, size.height * 0.15), - Offset(size.width * 0.8, size.height * 0.8), paint); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return false; - } -} diff --git a/lib/widgets/flex_box.dart b/lib/widgets/flex_box.dart new file mode 100644 index 000000000..4b2f9ca1d --- /dev/null +++ b/lib/widgets/flex_box.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +enum FlexLayout { + list, + grid; + + IconData get icon => switch (this) { + FlexLayout.list => Symbols.list, + FlexLayout.grid => Symbols.grid_view + }; + String getDisplayName(AppLocalizations l10n) => switch (this) { + FlexLayout.list => l10n.s_list_layout, + FlexLayout.grid => l10n.s_grid_layout + }; +} + +class FlexBox extends StatelessWidget { + final List items; + final Widget Function(T value) itemBuilder; + final FlexLayout layout; + final double cellMinWidth; + final double spacing; + final double runSpacing; + const FlexBox({ + super.key, + required this.items, + required this.itemBuilder, + required this.cellMinWidth, + this.layout = FlexLayout.list, + this.spacing = 0.0, + this.runSpacing = 0.0, + }); + + int _getItemsPerRow(double width) { + // Calculate the maximum number of cells that can fit in one row + int cellsPerRow = (width / (cellMinWidth + spacing)).floor(); + + // Ensure there's at least one cell per row + if (cellsPerRow < 1) { + cellsPerRow = 1; + } + + // Calculate the total width needed for the calculated number of cells and spacing + double totalWidthNeeded = + cellsPerRow * cellMinWidth + (cellsPerRow - 1) * spacing; + + // Adjust the number of cells per row if the calculated total width exceeds the available width + if (totalWidthNeeded > width) { + cellsPerRow = cellsPerRow - 1 > 0 ? cellsPerRow - 1 : 1; + } + + return cellsPerRow; + } + + List> getChunks(int itemsPerChunk) { + List> chunks = []; + final numChunks = (items.length / itemsPerChunk).ceil(); + for (int i = 0; i < numChunks; i++) { + final index = i * itemsPerChunk; + int endIndex = index + itemsPerChunk; + + if (endIndex > items.length) { + endIndex = items.length; + } + + chunks.add(items.sublist(index, endIndex)); + } + return chunks; + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + final itemsPerRow = + layout == FlexLayout.grid ? _getItemsPerRow(width) : 1; + final chunks = getChunks(itemsPerRow); + + return Column( + children: [ + for (final c in chunks) ...[ + if (chunks.indexOf(c) > 0) SizedBox(height: runSpacing), + Row( + children: [ + for (final entry in c) ...[ + Flexible( + child: itemBuilder(entry), + ), + if (itemsPerRow != 1 && c.indexOf(entry) != c.length - 1) + SizedBox(width: spacing), + ], + if (c.length < itemsPerRow) ...[ + // Prevents resizing when an item is removed + SizedBox(width: 8 * (itemsPerRow - c.length).toDouble()), + Spacer( + flex: itemsPerRow - c.length, + ) + ] + ], + ), + ] + ], + ); + }, + ); + } +} diff --git a/lib/widgets/responsive_dialog.dart b/lib/widgets/responsive_dialog.dart index 8fe0a245e..041ecf46b 100755 --- a/lib/widgets/responsive_dialog.dart +++ b/lib/widgets/responsive_dialog.dart @@ -102,7 +102,7 @@ class _ResponsiveDialogState extends State { ...widget.actions ], ), - onPopInvoked: (didPop) { + onPopInvokedWithResult: (didPop, _) { if (didPop) { widget.onCancel?.call(); } diff --git a/lib/widgets/toast.dart b/lib/widgets/toast.dart index b28244f7f..d8608980f 100755 --- a/lib/widgets/toast.dart +++ b/lib/widgets/toast.dart @@ -17,6 +17,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; class Toast extends StatefulWidget { final String message; @@ -149,5 +150,7 @@ void Function() showToast( Overlay.of(context).insert(entry!); }); + SemanticsService.announce(message, TextDirection.ltr); + return close; } diff --git a/lint/lib/lint.dart b/lint/lib/lint.dart index c3ffc0368..be991ed57 100644 --- a/lint/lib/lint.dart +++ b/lint/lib/lint.dart @@ -57,6 +57,7 @@ class UseRecommendedWidget extends DartLintRule { ) { context.registry.addInstanceCreationExpression((node) { if (node.constructorName.toString() == discouraged) { + // ignore: deprecated_member_use reporter.reportErrorForNode(code, node.constructorName); } }); @@ -89,6 +90,7 @@ class CallInitAfterCreation extends DartLintRule { return; } } + // ignore: deprecated_member_use reporter.reportErrorForNode(code, node.constructorName); } }); diff --git a/lint/pubspec.lock b/lint/pubspec.lock index 28833a5ab..3b4d91b09 100644 --- a/lint/pubspec.lock +++ b/lint/pubspec.lock @@ -5,18 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3" url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "68.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.1.0" analyzer: dependency: "direct main" description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.5.0" analyzer_plugin: dependency: "direct main" description: @@ -29,10 +34,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -77,10 +82,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -101,34 +106,34 @@ packages: dependency: transitive description: name: custom_lint - sha256: "445242371d91d2e24bd7b82e3583a2c05610094ba2d0575262484ad889c8f981" + sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799" url: "https://pub.dev" source: hosted - version: "0.6.2" + version: "0.6.4" custom_lint_builder: dependency: "direct main" description: name: custom_lint_builder - sha256: "4c0aed2a3491096e91cf1281923ba1b6814993f16dde0fd60f697925225bbbd6" + sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9 url: "https://pub.dev" source: hosted - version: "0.6.2" + version: "0.6.4" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: ce5d6215f4e143f7780ce53f73dfa6fc503f39d2d30bef76c48be9ac1a09d9a6 + sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 url: "https://pub.dev" source: hosted - version: "0.6.2" + version: "0.6.3" dart_style: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.6" file: dependency: transitive description: @@ -149,10 +154,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: f54946fdb1fa7b01f780841937b1a80783a20b393485f3f6cdf336fd6f4705f2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" glob: dependency: transitive description: @@ -173,10 +178,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" logging: dependency: transitive description: @@ -185,6 +190,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "12e8a9842b5a7390de7a781ec63d793527582398d16ea26c60fed58833c9ae79" + url: "https://pub.dev" + source: hosted + version: "0.1.0-main.0" matcher: dependency: transitive description: @@ -197,10 +210,10 @@ packages: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" package_config: dependency: transitive description: @@ -229,10 +242,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" rxdart: dependency: transitive description: @@ -301,10 +314,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" typed_data: dependency: transitive description: @@ -317,18 +330,18 @@ packages: dependency: transitive description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.4.0" vm_service: dependency: transitive description: name: vm_service - sha256: a2662fb1f114f4296cf3f5a50786a2d888268d7776cf681aa17d660ffa23b246 + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.0.0" + version: "14.2.4" watcher: dependency: transitive description: @@ -346,4 +359,4 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.4.3 <4.0.0" diff --git a/lint/pubspec.yaml b/lint/pubspec.yaml index 6089cfd0f..cec49ff7c 100644 --- a/lint/pubspec.yaml +++ b/lint/pubspec.yaml @@ -6,7 +6,7 @@ publish_to: none version: 1.0.0 environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.4.3 <4.0.0' dependencies: analyzer: diff --git a/linux/my_application.cc b/linux/my_application.cc index d2f00f237..e3a26a577 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -50,7 +50,6 @@ static void my_application_activate(GApplication* application) { gtk_window_set_default_size(window, 1100, 700); // Sets the minimum window size, should match desktop/window_manager_helper/defaults.dart gtk_widget_set_size_request(GTK_WIDGET(window), 300, 0); - gtk_widget_realize(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); @@ -59,8 +58,11 @@ static void my_application_activate(GApplication* application) { gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + // Registering plugins requires the window to be shown. We hide it immediately after, and it is never visible. + gtk_widget_show(GTK_WIDGET(window)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - + gtk_widget_hide(GTK_WIDGET(window)); + gtk_widget_grab_focus(GTK_WIDGET(view)); } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index cc5e29db7..855ab97c8 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -54,13 +54,13 @@ SPEC CHECKSUMS: desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 - url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index 91cfe9a72..14349e62e 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { // Keep app running if window closes diff --git a/pubspec.lock b/pubspec.lock index 4304255ca..3892377e4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" analyzer: dependency: "direct dev" description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.7.0" analyzer_plugin: dependency: "direct dev" description: @@ -29,10 +34,10 @@ packages: dependency: "direct main" description: name: archive - sha256: "0763b45fa9294197a2885c8567927e2830ade852e5c896fd4ab7e0e348d0f373" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.5.0" + version: "3.6.1" args: dependency: "direct main" description: @@ -85,10 +90,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -101,18 +106,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.12" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.2" built_collection: dependency: transitive description: @@ -197,42 +202,42 @@ packages: dependency: transitive description: name: cross_file - sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.4+1" + version: "0.3.4+2" crypto: dependency: "direct main" description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" custom_lint: dependency: "direct dev" description: name: custom_lint - sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799" + sha256: "6e1ec47427ca968f22bce734d00028ae7084361999b41673291138945c5baca0" url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.7" custom_lint_builder: dependency: "direct dev" description: name: custom_lint_builder - sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9 + sha256: ba2f90fff4eff71d202d097eb14b14f87087eaaef742e956208c0eb9d3a40a21 url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.7" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 + sha256: "4ddbbdaa774265de44c97054dcec058a83d9081d071785ece601e348c18c267d" url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.6.5" dart_style: dependency: transitive description: @@ -261,10 +266,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: @@ -277,10 +282,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a" + sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12" url: "https://pub.dev" source: hosted - version: "8.0.3" + version: "8.1.2" fixnum: dependency: transitive description: @@ -303,10 +308,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "4.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -316,10 +321,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" + sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.0.22" flutter_riverpod: dependency: "direct main" description: @@ -342,18 +347,18 @@ packages: dependency: "direct dev" description: name: freezed - sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 + sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.7" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -379,10 +384,10 @@ packages: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" hotreloader: dependency: transitive description: @@ -395,10 +400,10 @@ packages: dependency: transitive description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -424,10 +429,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: "direct main" description: @@ -464,26 +469,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lint: dependency: "direct dev" description: @@ -495,10 +500,10 @@ packages: dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" local_notifier: dependency: "direct main" description: @@ -515,6 +520,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -527,18 +540,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" material_symbols_icons: dependency: "direct main" description: name: material_symbols_icons - sha256: "36d4e5dd72f2fd282aca127cc4c4c29786d702cb506231ea73a5497fc324bf46" + sha256: "66416c4e30bd363508e12669634fc4f3250b83b69e862de67f4f9c480cf42414" url: "https://pub.dev" source: hosted - version: "4.2741.0" + version: "4.2785.1" menu_base: dependency: transitive description: @@ -551,18 +564,18 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" package_config: dependency: transitive description: @@ -591,26 +604,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.10" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -631,10 +644,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: @@ -647,10 +660,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -687,10 +700,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" qrscanner_zxing: dependency: "direct main" description: @@ -710,10 +723,10 @@ packages: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" screen_retriever: dependency: "direct main" description: @@ -726,58 +739,58 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.2" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.5.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shelf: dependency: transitive description: @@ -790,10 +803,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" shortid: dependency: transitive description: @@ -899,10 +912,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" test_res: dependency: "direct dev" description: @@ -922,10 +935,10 @@ packages: dependency: "direct main" description: name: tray_manager - sha256: e0ac9a88b2700f366b8629b97e8663b6ef450a2f169560a685dc167bfe9c9c29 + sha256: c9a63fd88bd3546287a7eb8ccc978d707eef82c775397af17dda3a4f4c039e64 url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.3" typed_data: dependency: transitive description: @@ -938,42 +951,42 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.2.6" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" + sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.10" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" url_launcher_platform_interface: dependency: transitive description: @@ -986,26 +999,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" uuid: dependency: transitive description: name: uuid - sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.5.0" vector_graphics: dependency: "direct main" description: @@ -1042,10 +1055,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.5" watcher: dependency: transitive description: @@ -1058,18 +1071,26 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.0.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "3.0.1" webdriver: dependency: transitive description: @@ -1082,10 +1103,10 @@ packages: dependency: transitive description: name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.5.4" window_manager: dependency: "direct main" description: @@ -1120,5 +1141,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4955ce8ba..aa71eb5bf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 7.0.2-dev.0+70002 environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.4.3 <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -34,7 +34,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - intl: ^0.18.1 + intl: ^0.19.0 # The following adds the Cupertino Icons font to your application. @@ -70,7 +70,7 @@ dependencies: io: ^1.0.4 base32: ^2.1.3 convert: ^3.1.1 - material_symbols_icons: ^4.2719.3 + material_symbols_icons: ^4.2741.0 dev_dependencies: integration_test: @@ -85,7 +85,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^3.0.1 + flutter_lints: ^4.0.0 build_runner: ^2.4.8 freezed: ^2.4.7