diff --git a/NEWS b/NEWS index 34c1c5c45..a651dfcc6 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,17 @@ +* Version 7.1.0 (released 2024-09-25) + ** Improved support for YubiKey Bio - Multi-protocol Edition. + ** Improved support for YubiKey 5.7 FIPS: + *** Use secure messaging (SCP11b) when needed over NFC. + *** Add FIPS status information to Home screen and application pages. + *** Disable locked features in UI while not in FIPS approved mode. + ** Android: Improve UI for NFC communication. + ** Android: Improve error messages shown in UI. + ** FIDO: Add button to enable Enterprise Attestation on supported devices. + ** UI: Add instructions for enabling NFC for NFC-restricted devices. + ** UI: Display PIN complexity info on Home screen for applicable devices. + ** UI: Add grid views for OATH and Passkey credential lists. + ** UI: Add toggling of visibility to the right sidebar menu. + * Version 7.0.1 (released 2024-05-30) Android only release ** Fix: Opening the app by NFC tap needs another tap to reveal accounts. ** Fix: NFC devices attached to mobile phone prevent usage of USB YubiKeys. diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/AppContextManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/AppContextManager.kt index de1a30ae8..7d0af7c0c 100755 --- a/android/app/src/main/kotlin/com/yubico/authenticator/AppContextManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/AppContextManager.kt @@ -22,9 +22,13 @@ import com.yubico.yubikit.core.YubiKeyDevice * Provides behavior to run when a YubiKey is inserted/tapped for a specific view of the app. */ abstract class AppContextManager { - abstract suspend fun processYubiKey(device: YubiKeyDevice) + abstract suspend fun processYubiKey(device: YubiKeyDevice): Boolean open fun dispose() {} open fun onPause() {} -} \ No newline at end of file + + open fun onError(e: Exception) {} +} + +class ContextDisposedException : Exception() \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt deleted file mode 100644 index c3df2e049..000000000 --- a/android/app/src/main/kotlin/com/yubico/authenticator/DialogManager.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2022-2023 Yubico. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.yubico.authenticator - -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodChannel -import kotlinx.coroutines.* -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -typealias OnDialogCancelled = suspend () -> Unit - -enum class DialogIcon(val value: Int) { - Nfc(0), - Success(1), - Failure(2); -} - -enum class DialogTitle(val value: Int) { - TapKey(0), - OperationSuccessful(1), - OperationFailed(2) -} - -class DialogManager(messenger: BinaryMessenger, private val coroutineScope: CoroutineScope) { - private val channel = - MethodChannel(messenger, "com.yubico.authenticator.channel.dialog") - - private var onCancelled: OnDialogCancelled? = null - - init { - channel.setHandler(coroutineScope) { method, _ -> - when (method) { - "cancel" -> dialogClosed() - else -> throw NotImplementedError() - } - } - } - - fun showDialog( - dialogIcon: DialogIcon, - dialogTitle: DialogTitle, - dialogDescriptionId: Int, - cancelled: OnDialogCancelled? - ) { - onCancelled = cancelled - coroutineScope.launch { - channel.invoke( - "show", - Json.encodeToString( - mapOf( - "title" to dialogTitle.value, - "description" to dialogDescriptionId, - "icon" to dialogIcon.value - ) - ) - ) - } - } - - suspend fun updateDialogState( - dialogIcon: DialogIcon? = null, - dialogTitle: DialogTitle, - dialogDescriptionId: Int? = null, - ) { - channel.invoke( - "state", - Json.encodeToString( - mapOf( - "title" to dialogTitle.value, - "description" to dialogDescriptionId, - "icon" to dialogIcon?.value - ) - ) - ) - } - - suspend fun closeDialog() { - channel.invoke("close", NULL) - } - - private suspend fun dialogClosed(): String { - onCancelled?.let { - onCancelled = null - withContext(Dispatchers.Main) { - it.invoke() - } - } - return NULL - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt index 00abfc222..47c45af99 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt @@ -16,8 +16,11 @@ package com.yubico.authenticator +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.annotation.SuppressLint -import android.content.* import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.content.pm.ActivityInfo import android.content.pm.PackageManager @@ -48,13 +51,18 @@ 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.NfcStateDispatcher +import com.yubico.authenticator.yubikit.NfcStateListener +import com.yubico.authenticator.yubikit.NfcState 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.nfc.NfcYubiKeyManager import com.yubico.yubikit.android.transport.usb.UsbConfiguration +import com.yubico.yubikit.android.transport.usb.UsbYubiKeyManager import com.yubico.yubikit.core.Transport import com.yubico.yubikit.core.YubiKeyDevice import com.yubico.yubikit.core.smartcard.SmartCardConnection @@ -66,10 +74,12 @@ import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.json.JSONObject import org.slf4j.LoggerFactory import java.io.Closeable +import java.io.IOException import java.security.NoSuchAlgorithmException import java.util.concurrent.Executors import javax.crypto.Mac @@ -94,6 +104,20 @@ class MainActivity : FlutterFragmentActivity() { private val logger = LoggerFactory.getLogger(MainActivity::class.java) + private val nfcStateListener = object : NfcStateListener { + + var appMethodChannel : AppMethodChannel? = null + + override fun onChange(newState: NfcState) { + appMethodChannel?.let { + logger.debug("set nfc state to ${newState.name}") + it.nfcStateChanged(newState) + } ?: { + logger.warn("failed set nfc state to ${newState.name} - no method channel") + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -105,7 +129,11 @@ class MainActivity : FlutterFragmentActivity() { allowScreenshots(false) - yubikit = YubiKitManager(this) + val nfcManager = if (NfcAdapter.getDefaultAdapter(this) != null) { + NfcYubiKeyManager(this, NfcStateDispatcher(nfcStateListener)) + } else null + + yubikit = YubiKitManager(UsbYubiKeyManager(this), nfcManager) } override fun onNewIntent(intent: Intent) { @@ -284,30 +312,55 @@ class MainActivity : FlutterFragmentActivity() { } private suspend fun processYubiKey(device: YubiKeyDevice) { - val deviceInfo = getDeviceInfo(device) + val deviceInfo = try { - if (deviceInfo == null) { - deviceManager.setDeviceInfo(null) - return - } + if (device is NfcYubiKeyDevice) { + appMethodChannel.nfcStateChanged(NfcState.ONGOING) + } - // 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) + val deviceInfo = getDeviceInfo(device) + + deviceManager.scpKeyParams = null + // 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 = try { + 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) + } } + } catch (e: Exception) { + logger.error("Exception when reading SCP key information: ", e) + // we throw IO exception to unify handling failures as we don't want + // th clear device info + throw IOException("Failure getting SCP keys") } + } + deviceInfo + } catch (e: Exception) { + logger.debug("Exception while getting device info and scp keys: ", e) + contextManager?.onError(e) + if (device is NfcYubiKeyDevice) { + appMethodChannel.nfcStateChanged(NfcState.FAILURE) + } + + // do not clear deviceInfo on IOExceptions, + // this allows for retries of failed actions + if (e !is IOException) { + logger.debug("Resetting device info") + deviceManager.setDeviceInfo(null) + } + + return } // this YubiKey provides SCP11b key but the phone cannot perform AESCMAC @@ -319,6 +372,7 @@ class MainActivity : FlutterFragmentActivity() { deviceManager.setDeviceInfo(deviceInfo) val supportedContexts = DeviceManager.getSupportedContexts(deviceInfo) logger.debug("Connected key supports: {}", supportedContexts) + var switchedContext: Boolean = false if (!supportedContexts.contains(viewModel.appContext.value)) { val preferredContext = DeviceManager.getPreferredContext(supportedContexts) logger.debug( @@ -326,18 +380,28 @@ class MainActivity : FlutterFragmentActivity() { viewModel.appContext.value, preferredContext ) - switchContext(preferredContext) + switchedContext = switchContext(preferredContext) } if (contextManager == null && supportedContexts.isNotEmpty()) { - switchContext(DeviceManager.getPreferredContext(supportedContexts)) + switchedContext = switchContext(DeviceManager.getPreferredContext(supportedContexts)) } contextManager?.let { try { - it.processYubiKey(device) - } catch (e: Throwable) { - logger.error("Error processing YubiKey in AppContextManager", e) + val requestHandled = it.processYubiKey(device) + if (requestHandled) { + appMethodChannel.nfcStateChanged(NfcState.SUCCESS) + } + if (!switchedContext && device is NfcYubiKeyDevice) { + + device.remove { + appMethodChannel.nfcStateChanged(NfcState.IDLE) + } + } + } catch (e: Exception) { + logger.debug("Caught Exception during YubiKey processing: ", e) + appMethodChannel.nfcStateChanged(NfcState.FAILURE) } } } @@ -351,7 +415,7 @@ class MainActivity : FlutterFragmentActivity() { private var contextManager: AppContextManager? = null private lateinit var deviceManager: DeviceManager private lateinit var appContext: AppContext - private lateinit var dialogManager: DialogManager + private lateinit var nfcOverlayManager: NfcOverlayManager private lateinit var appPreferences: AppPreferences private lateinit var flutterLog: FlutterLog private lateinit var flutterStreams: List @@ -365,13 +429,16 @@ class MainActivity : FlutterFragmentActivity() { messenger = flutterEngine.dartExecutor.binaryMessenger flutterLog = FlutterLog(messenger) - deviceManager = DeviceManager(this, viewModel) + appMethodChannel = AppMethodChannel(messenger) + nfcOverlayManager = NfcOverlayManager(messenger, this.lifecycleScope) + deviceManager = DeviceManager(this, viewModel,appMethodChannel, nfcOverlayManager) appContext = AppContext(messenger, this.lifecycleScope, viewModel) - dialogManager = DialogManager(messenger, this.lifecycleScope) + appPreferences = AppPreferences(this) - appMethodChannel = AppMethodChannel(messenger) appLinkMethodChannel = AppLinkMethodChannel(messenger) - managementHandler = ManagementHandler(messenger, deviceManager, dialogManager) + managementHandler = ManagementHandler(messenger, deviceManager) + + nfcStateListener.appMethodChannel = appMethodChannel flutterStreams = listOf( viewModel.deviceInfo.streamTo(this, messenger, "android.devices.deviceInfo"), @@ -390,7 +457,8 @@ class MainActivity : FlutterFragmentActivity() { } } - private fun switchContext(appContext: OperationContext) { + private fun switchContext(appContext: OperationContext) : Boolean { + var switchHappened = false // TODO: refactor this when more OperationContext are handled // only recreate the contextManager object if it cannot be reused if (appContext == OperationContext.Home || @@ -404,6 +472,7 @@ class MainActivity : FlutterFragmentActivity() { } else { contextManager?.dispose() contextManager = null + switchHappened = true } if (contextManager == null) { @@ -413,7 +482,7 @@ class MainActivity : FlutterFragmentActivity() { messenger, deviceManager, oathViewModel, - dialogManager, + nfcOverlayManager, appPreferences ) @@ -422,17 +491,20 @@ class MainActivity : FlutterFragmentActivity() { messenger, this, deviceManager, + appMethodChannel, + nfcOverlayManager, fidoViewModel, - viewModel, - dialogManager + viewModel ) else -> null } } + return switchHappened } override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) { + nfcStateListener.appMethodChannel = null flutterStreams.forEach { it.close() } contextManager?.dispose() deviceManager.dispose() @@ -572,9 +644,18 @@ class MainActivity : FlutterFragmentActivity() { fun nfcAdapterStateChanged(value: Boolean) { methodChannel.invokeMethod( "nfcAdapterStateChanged", - JSONObject(mapOf("nfcEnabled" to value)).toString() + JSONObject(mapOf("enabled" to value)).toString() ) } + + fun nfcStateChanged(activityState: NfcState) { + lifecycleScope.launch(Dispatchers.Main) { + methodChannel.invokeMethod( + "nfcStateChanged", + JSONObject(mapOf("state" to activityState.value)).toString() + ) + } + } } private fun allowScreenshots(value: Boolean): Boolean { diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/NfcOverlayManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/NfcOverlayManager.kt new file mode 100644 index 000000000..c4f29d539 --- /dev/null +++ b/android/app/src/main/kotlin/com/yubico/authenticator/NfcOverlayManager.kt @@ -0,0 +1,63 @@ +/* + * 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. + * 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 + +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +typealias OnCancelled = suspend () -> Unit + +class NfcOverlayManager(messenger: BinaryMessenger, private val coroutineScope: CoroutineScope) { + private val channel = + MethodChannel(messenger, "com.yubico.authenticator.channel.nfc_overlay") + + private var onCancelled: OnCancelled? = null + + init { + channel.setHandler(coroutineScope) { method, _ -> + when (method) { + "cancel" -> onClosed() + else -> throw NotImplementedError() + } + } + } + + fun show(cancelled: OnCancelled?) { + onCancelled = cancelled + coroutineScope.launch { + channel.invoke("show", null) + } + } + + suspend fun close() { + channel.invoke("close", NULL) + } + + private suspend fun onClosed(): String { + onCancelled?.let { + onCancelled = null + withContext(Dispatchers.Main) { + it.invoke() + } + } + return NULL + } +} \ No newline at end of file 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 01e7f04f8..8968fd794 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 @@ -20,13 +20,19 @@ import androidx.collection.ArraySet import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer +import com.yubico.authenticator.ContextDisposedException +import com.yubico.authenticator.MainActivity import com.yubico.authenticator.MainViewModel +import com.yubico.authenticator.NfcOverlayManager import com.yubico.authenticator.OperationContext +import com.yubico.authenticator.yubikit.NfcState 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 kotlinx.coroutines.CancellationException import org.slf4j.LoggerFactory +import java.io.IOException interface DeviceListener { // a USB device is connected @@ -41,7 +47,9 @@ interface DeviceListener { class DeviceManager( private val lifecycleOwner: LifecycleOwner, - private val appViewModel: MainViewModel + private val appViewModel: MainViewModel, + private val appMethodChannel: MainActivity.AppMethodChannel, + private val nfcOverlayManager: NfcOverlayManager ) { var clearDeviceInfoOnDisconnect: Boolean = true @@ -167,7 +175,6 @@ class DeviceManager( fun setDeviceInfo(deviceInfo: Info?) { appViewModel.setDeviceInfo(deviceInfo) - scpKeyParams = null } fun isUsbKeyConnected(): Boolean { @@ -179,8 +186,32 @@ class DeviceManager( onUsb(it) } - suspend fun withKey(onNfc: suspend () -> T, onUsb: suspend (UsbYubiKeyDevice) -> T) = + suspend fun withKey( + onUsb: suspend (UsbYubiKeyDevice) -> T, + onNfc: suspend () -> com.yubico.yubikit.core.util.Result, + onCancelled: () -> Unit + ): T = appViewModel.connectedYubiKey.value?.let { onUsb(it) - } ?: onNfc() + } ?: onNfc(onNfc, onCancelled) + + + private suspend fun onNfc( + onNfc: suspend () -> com.yubico.yubikit.core.util.Result, + onCancelled: () -> Unit + ): T { + nfcOverlayManager.show { + logger.debug("NFC action was cancelled") + onCancelled.invoke() + } + + try { + return onNfc.invoke().value.also { + appMethodChannel.nfcStateChanged(NfcState.SUCCESS) + } + } catch (e: Exception) { + appMethodChannel.nfcStateChanged(NfcState.FAILURE) + throw e + } + } } \ No newline at end of file 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 88ab68530..8ead1d5bf 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 @@ -16,9 +16,6 @@ package com.yubico.authenticator.fido -import com.yubico.authenticator.DialogIcon -import com.yubico.authenticator.DialogManager -import com.yubico.authenticator.DialogTitle import com.yubico.authenticator.device.DeviceManager import com.yubico.authenticator.fido.data.YubiKitFidoSession import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo @@ -27,20 +24,22 @@ import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice import com.yubico.yubikit.core.fido.FidoConnection import com.yubico.yubikit.core.util.Result import org.slf4j.LoggerFactory +import java.util.TimerTask import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.suspendCoroutine -class FidoConnectionHelper( - private val deviceManager: DeviceManager, - private val dialogManager: DialogManager -) { +class FidoConnectionHelper(private val deviceManager: DeviceManager) { private var pendingAction: FidoAction? = null - fun invokePending(fidoSession: YubiKitFidoSession) { + fun invokePending(fidoSession: YubiKitFidoSession): Boolean { + var requestHandled = true pendingAction?.let { action -> - action.invoke(Result.success(fidoSession)) pendingAction = null + // it is the pending action who handles this request + requestHandled = false + action.invoke(Result.success(fidoSession)) } + return requestHandled } fun cancelPending() { @@ -51,14 +50,18 @@ class FidoConnectionHelper( } suspend fun useSession( - actionDescription: FidoActionDescription, updateDeviceInfo: Boolean = false, - action: (YubiKitFidoSession) -> T + block: (YubiKitFidoSession) -> T ): T { FidoManager.updateDeviceInfo.set(updateDeviceInfo) return deviceManager.withKey( - onNfc = { useSessionNfc(actionDescription,action) }, - onUsb = { useSessionUsb(it, updateDeviceInfo, action) }) + onUsb = { useSessionUsb(it, updateDeviceInfo, block) }, + onNfc = { useSessionNfc(block) }, + onCancelled = { + pendingAction?.invoke(Result.failure(CancellationException())) + pendingAction = null + } + ) } suspend fun useSessionUsb( @@ -69,14 +72,13 @@ class FidoConnectionHelper( block(YubiKitFidoSession(it)) }.also { if (updateDeviceInfo) { - deviceManager.setDeviceInfo(getDeviceInfo(device)) + deviceManager.setDeviceInfo(runCatching { getDeviceInfo(device) }.getOrNull()) } } suspend fun useSessionNfc( - actionDescription: FidoActionDescription, block: (YubiKitFidoSession) -> T - ): T { + ): Result { try { val result = suspendCoroutine { outer -> pendingAction = { @@ -84,23 +86,13 @@ class FidoConnectionHelper( block.invoke(it.value) }) } - dialogManager.showDialog( - DialogIcon.Nfc, - DialogTitle.TapKey, - actionDescription.id - ) { - logger.debug("Cancelled Dialog {}", actionDescription.name) - pendingAction?.invoke(Result.failure(CancellationException())) - pendingAction = null - } } - return result + return Result.success(result!!) } catch (cancelled: CancellationException) { - throw cancelled + return Result.failure(cancelled) } catch (error: Throwable) { - throw error - } finally { - dialogManager.closeDialog() + logger.error("Exception during action: ", error) + return Result.failure(error) } } 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 138f0a38d..581a821c7 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 @@ -18,7 +18,8 @@ package com.yubico.authenticator.fido import androidx.lifecycle.LifecycleOwner import com.yubico.authenticator.AppContextManager -import com.yubico.authenticator.DialogManager +import com.yubico.authenticator.NfcOverlayManager +import com.yubico.authenticator.MainActivity import com.yubico.authenticator.MainViewModel import com.yubico.authenticator.NULL import com.yubico.authenticator.asString @@ -70,9 +71,10 @@ class FidoManager( messenger: BinaryMessenger, lifecycleOwner: LifecycleOwner, private val deviceManager: DeviceManager, + private val appMethodChannel: MainActivity.AppMethodChannel, + private val nfcOverlayManager: NfcOverlayManager, private val fidoViewModel: FidoViewModel, - mainViewModel: MainViewModel, - dialogManager: DialogManager, + mainViewModel: MainViewModel ) : AppContextManager(), DeviceListener { @OptIn(ExperimentalStdlibApi::class) @@ -100,7 +102,7 @@ class FidoManager( } } - private val connectionHelper = FidoConnectionHelper(deviceManager, dialogManager) + private val connectionHelper = FidoConnectionHelper(deviceManager) private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher) @@ -117,14 +119,14 @@ class FidoManager( FidoResetHelper( lifecycleOwner, deviceManager, + appMethodChannel, + nfcOverlayManager, fidoViewModel, mainViewModel, connectionHelper, pinStore ) - - init { pinRetries = null @@ -172,6 +174,12 @@ class FidoManager( } } + override fun onError(e: Exception) { + super.onError(e) + logger.error("Cancelling pending action. Cause: ", e) + connectionHelper.cancelPending() + } + override fun dispose() { super.dispose() deviceManager.removeDeviceListener(this) @@ -182,32 +190,40 @@ class FidoManager( coroutineScope.cancel() } - override suspend fun processYubiKey(device: YubiKeyDevice) { + override suspend fun processYubiKey(device: YubiKeyDevice): Boolean { + var requestHandled = true try { if (device.supportsConnection(FidoConnection::class.java)) { device.withConnection { connection -> - processYubiKey(connection, device) + requestHandled = processYubiKey(connection, device) } } else { device.withConnection { connection -> - processYubiKey(connection, device) + requestHandled = processYubiKey(connection, device) } } if (updateDeviceInfo.getAndSet(false)) { - deviceManager.setDeviceInfo(getDeviceInfo(device)) + deviceManager.setDeviceInfo(runCatching { getDeviceInfo(device) }.getOrNull()) } } catch (e: Exception) { - // something went wrong, try to get DeviceInfo from any available connection type - logger.error("Failure when processing YubiKey: ", e) - // Clear any cached FIDO state - fidoViewModel.clearSessionState() + logger.error("Cancelling pending action. Cause: ", e) + connectionHelper.cancelPending() + + if (e !is IOException) { + // we don't clear the session on IOExceptions so that the session is ready for + // a possible re-run of a failed action. + fidoViewModel.clearSessionState() + } + throw e } + return requestHandled } - private fun processYubiKey(connection: YubiKeyConnection, device: YubiKeyDevice) { + private fun processYubiKey(connection: YubiKeyConnection, device: YubiKeyDevice): Boolean { + var requestHandled = true val fidoSession = if (connection is FidoConnection) { YubiKitFidoSession(connection) @@ -223,10 +239,10 @@ class FidoManager( currentSession ) - val sameDevice = currentSession == previousSession + val sameDevice = currentSession.sameDevice(previousSession) if (device is NfcYubiKeyDevice && (sameDevice || resetHelper.inProgress)) { - connectionHelper.invokePending(fidoSession) + requestHandled = connectionHelper.invokePending(fidoSession) } else { if (!sameDevice) { @@ -250,6 +266,8 @@ class FidoManager( Session(infoData, pinStore.hasPin(), pinRetries) ) } + + return requestHandled } private fun getPinPermissionsCM(fidoSession: YubiKitFidoSession): Int { @@ -353,7 +371,7 @@ class FidoManager( } private suspend fun unlock(pin: CharArray): String = - connectionHelper.useSession(FidoActionDescription.Unlock) { fidoSession -> + connectionHelper.useSession { fidoSession -> try { val clientPin = @@ -390,7 +408,7 @@ class FidoManager( } private suspend fun setPin(pin: CharArray?, newPin: CharArray): String = - connectionHelper.useSession(FidoActionDescription.SetPin, updateDeviceInfo = true) { fidoSession -> + connectionHelper.useSession(updateDeviceInfo = true) { fidoSession -> try { val clientPin = ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)) @@ -438,7 +456,7 @@ class FidoManager( } private suspend fun deleteCredential(rpId: String, credentialId: String): String = - connectionHelper.useSession(FidoActionDescription.DeleteCredential) { fidoSession -> + connectionHelper.useSession { fidoSession -> val clientPin = ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)) @@ -486,7 +504,7 @@ class FidoManager( } private suspend fun deleteFingerprint(templateId: String): String = - connectionHelper.useSession(FidoActionDescription.DeleteFingerprint) { fidoSession -> + connectionHelper.useSession { fidoSession -> val clientPin = ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)) @@ -511,7 +529,7 @@ class FidoManager( } private suspend fun renameFingerprint(templateId: String, name: String): String = - connectionHelper.useSession(FidoActionDescription.RenameFingerprint) { fidoSession -> + connectionHelper.useSession { fidoSession -> val clientPin = ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)) @@ -541,7 +559,7 @@ class FidoManager( } private suspend fun registerFingerprint(name: String?): String = - connectionHelper.useSession(FidoActionDescription.RegisterFingerprint) { fidoSession -> + connectionHelper.useSession { fidoSession -> state?.cancel() state = CommandState() val clientPin = @@ -588,7 +606,7 @@ class FidoManager( } else -> throw ctapException } - } catch (io: IOException) { + } catch (_: IOException) { return@useSession JSONObject( mapOf( "success" to false, @@ -617,7 +635,7 @@ class FidoManager( } private suspend fun enableEnterpriseAttestation(): String = - connectionHelper.useSession(FidoActionDescription.EnableEnterpriseAttestation) { fidoSession -> + connectionHelper.useSession { fidoSession -> try { val uvAuthProtocol = getPreferredPinUvAuthProtocol(fidoSession.cachedInfo) val clientPin = ClientPin(fidoSession, uvAuthProtocol) 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 33d54f92e..00c2c4a2b 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 @@ -18,11 +18,14 @@ package com.yubico.authenticator.fido import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner +import com.yubico.authenticator.NfcOverlayManager +import com.yubico.authenticator.MainActivity import com.yubico.authenticator.MainViewModel import com.yubico.authenticator.NULL import com.yubico.authenticator.device.DeviceManager import com.yubico.authenticator.fido.data.Session import com.yubico.authenticator.fido.data.YubiKitFidoSession +import com.yubico.authenticator.yubikit.NfcState import com.yubico.yubikit.core.application.CommandState import com.yubico.yubikit.core.fido.CtapException import kotlinx.coroutines.CoroutineScope @@ -68,6 +71,8 @@ fun createCaptureErrorEvent(code: Int) : FidoRegisterFpCaptureErrorEvent { class FidoResetHelper( private val lifecycleOwner: LifecycleOwner, private val deviceManager: DeviceManager, + private val appMethodChannel: MainActivity.AppMethodChannel, + private val nfcOverlayManager: NfcOverlayManager, private val fidoViewModel: FidoViewModel, private val mainViewModel: MainViewModel, private val connectionHelper: FidoConnectionHelper, @@ -106,7 +111,7 @@ class FidoResetHelper( resetOverNfc() } logger.info("FIDO reset complete") - } catch (e: CancellationException) { + } catch (_: CancellationException) { logger.debug("FIDO reset cancelled") } finally { withContext(Dispatchers.Main) { @@ -210,16 +215,22 @@ class FidoResetHelper( private suspend fun resetOverNfc() = suspendCoroutine { continuation -> coroutineScope.launch { + nfcOverlayManager.show { + + } fidoViewModel.updateResetState(FidoResetState.Touch) try { FidoManager.updateDeviceInfo.set(true) - connectionHelper.useSessionNfc(FidoActionDescription.Reset) { fidoSession -> + connectionHelper.useSessionNfc { fidoSession -> doReset(fidoSession) + appMethodChannel.nfcStateChanged(NfcState.SUCCESS) continuation.resume(Unit) - } + }.value } catch (e: Throwable) { // on NFC, clean device info in this situation mainViewModel.setDeviceInfo(null) + appMethodChannel.nfcStateChanged(NfcState.FAILURE) + logger.error("Failure during FIDO reset:", e) continuation.resumeWithException(e) } } 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 93cd770b1..835a3eca6 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 @@ -41,6 +41,19 @@ data class Options( infoData.getOptionsBoolean("ep"), ) + fun sameDevice(other: Options) : Boolean { + if (this === other) return true + + if (clientPin != other.clientPin) return false + if (credMgmt != other.credMgmt) return false + if (credentialMgmtPreview != other.credentialMgmtPreview) return false + if (bioEnroll != other.bioEnroll) return false + // alwaysUv may differ + // ep may differ + + return true + } + companion object { private fun InfoData.getOptionsBoolean( key: String @@ -67,6 +80,21 @@ data class SessionInfo( infoData.remainingDiscoverableCredentials ) + // this is a more permissive comparison, which does not take in an account properties, + // which might change by using the FIDO authenticator + fun sameDevice(other: SessionInfo?): Boolean { + if (other == null) return false + if (this === other) return true + + if (!options.sameDevice(other.options)) return false + if (!aaguid.contentEquals(other.aaguid)) return false + // minPinLength may differ + // forcePinChange may differ + // remainingDiscoverableCredentials may differ + + return true + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/management/ManagementConnectionHelper.kt b/android/app/src/main/kotlin/com/yubico/authenticator/management/ManagementConnectionHelper.kt index 891043b85..4bacb5246 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/management/ManagementConnectionHelper.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/management/ManagementConnectionHelper.kt @@ -16,15 +16,11 @@ package com.yubico.authenticator.management -import com.yubico.authenticator.DialogIcon -import com.yubico.authenticator.DialogManager -import com.yubico.authenticator.DialogTitle import com.yubico.authenticator.device.DeviceManager import com.yubico.authenticator.yubikit.withConnection import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice import com.yubico.yubikit.core.smartcard.SmartCardConnection import com.yubico.yubikit.core.util.Result -import org.slf4j.LoggerFactory import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.suspendCoroutine @@ -32,19 +28,19 @@ typealias YubiKitManagementSession = com.yubico.yubikit.management.ManagementSes typealias ManagementAction = (Result) -> Unit class ManagementConnectionHelper( - private val deviceManager: DeviceManager, - private val dialogManager: DialogManager + private val deviceManager: DeviceManager ) { private var action: ManagementAction? = null - suspend fun useSession( - actionDescription: ManagementActionDescription, - action: (YubiKitManagementSession) -> T - ): T { - return deviceManager.withKey( - onNfc = { useSessionNfc(actionDescription, action) }, - onUsb = { useSessionUsb(it, action) }) - } + suspend fun useSession(block: (YubiKitManagementSession) -> T): T = + deviceManager.withKey( + onUsb = { useSessionUsb(it, block) }, + onNfc = { useSessionNfc(block) }, + onCancelled = { + action?.invoke(Result.failure(CancellationException())) + action = null + } + ) private suspend fun useSessionUsb( device: UsbYubiKeyDevice, @@ -54,37 +50,20 @@ class ManagementConnectionHelper( } private suspend fun useSessionNfc( - actionDescription: ManagementActionDescription, - block: (YubiKitManagementSession) -> T - ): T { + block: (YubiKitManagementSession) -> T): Result { try { - val result = suspendCoroutine { outer -> + val result = suspendCoroutine { outer -> action = { outer.resumeWith(runCatching { block.invoke(it.value) }) } - dialogManager.showDialog( - DialogIcon.Nfc, - DialogTitle.TapKey, - actionDescription.id - ) { - logger.debug("Cancelled Dialog {}", actionDescription.name) - action?.invoke(Result.failure(CancellationException())) - action = null - } } - return result + return Result.success(result!!) } catch (cancelled: CancellationException) { - throw cancelled + return Result.failure(cancelled) } catch (error: Throwable) { - throw error - } finally { - dialogManager.closeDialog() + return Result.failure(error) } } - - companion object { - private val logger = LoggerFactory.getLogger(ManagementConnectionHelper::class.java) - } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/management/ManagementHandler.kt b/android/app/src/main/kotlin/com/yubico/authenticator/management/ManagementHandler.kt index 4c5096d12..abf5542bd 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/management/ManagementHandler.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/management/ManagementHandler.kt @@ -16,7 +16,6 @@ package com.yubico.authenticator.management -import com.yubico.authenticator.DialogManager import com.yubico.authenticator.NULL import com.yubico.authenticator.device.DeviceManager import com.yubico.authenticator.setHandler @@ -27,25 +26,15 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.asCoroutineDispatcher import java.util.concurrent.Executors -const val dialogDescriptionManagementIndex = 300 - -enum class ManagementActionDescription(private val value: Int) { - DeviceReset(0), ActionFailure(1); - - val id: Int - get() = value + dialogDescriptionManagementIndex -} - class ManagementHandler( messenger: BinaryMessenger, - deviceManager: DeviceManager, - dialogManager: DialogManager + deviceManager: DeviceManager ) { private val channel = MethodChannel(messenger, "android.management.methods") private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher) - private val connectionHelper = ManagementConnectionHelper(deviceManager, dialogManager) + private val connectionHelper = ManagementConnectionHelper(deviceManager) init { channel.setHandler(coroutineScope) { method, _ -> @@ -58,7 +47,7 @@ class ManagementHandler( } private suspend fun deviceReset(): String = - connectionHelper.useSession(ManagementActionDescription.DeviceReset) { managementSession -> + connectionHelper.useSession { managementSession -> managementSession.deviceReset() 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 03c9dd7a0..9f78a6a5e 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt @@ -63,6 +63,7 @@ import kotlinx.serialization.encodeToString import org.slf4j.LoggerFactory import java.io.IOException import java.net.URI +import java.util.TimerTask import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.suspendCoroutine @@ -74,8 +75,8 @@ class OathManager( messenger: BinaryMessenger, private val deviceManager: DeviceManager, private val oathViewModel: OathViewModel, - private val dialogManager: DialogManager, - private val appPreferences: AppPreferences, + private val nfcOverlayManager: NfcOverlayManager, + private val appPreferences: AppPreferences ) : AppContextManager(), DeviceListener { companion object { @@ -107,15 +108,26 @@ class OathManager( private var refreshJob: Job? = null private var addToAny = false private val updateDeviceInfo = AtomicBoolean(false) + private var deviceInfoTimer: TimerTask? = null + + override fun onError(e: Exception) { + super.onError(e) + logger.error("Cancelling pending action in onError. Cause: ", e) + pendingAction?.let { action -> + action.invoke(Result.failure(CancellationException())) + pendingAction = null + } + } override fun onPause() { + deviceInfoTimer?.cancel() // cancel any pending actions, except for addToAny if (!addToAny) { pendingAction?.let { - logger.debug("Cancelling pending action/closing nfc dialog.") + logger.debug("Cancelling pending action/closing nfc overlay.") it.invoke(Result.failure(CancellationException())) coroutineScope.launch { - dialogManager.closeDialog() + nfcOverlayManager.close() } pendingAction = null } @@ -186,6 +198,7 @@ class OathManager( ) "deleteAccount" -> deleteAccount(args["credentialId"] as String) + "addAccountToAny" -> addAccountToAny( args["uri"] as String, args["requireTouch"] as Boolean @@ -208,28 +221,59 @@ class OathManager( oathChannel.setMethodCallHandler(null) oathViewModel.clearSession() oathViewModel.updateCredentials(mapOf()) - pendingAction?.invoke(Result.failure(Exception())) + pendingAction?.invoke(Result.failure(ContextDisposedException())) + pendingAction = null coroutineScope.cancel() } - override suspend fun processYubiKey(device: YubiKeyDevice) { + override suspend fun processYubiKey(device: YubiKeyDevice): Boolean { + var requestHandled = true try { device.withConnection { connection -> val session = getOathSession(connection) val previousId = oathViewModel.currentSession()?.deviceId - if (session.deviceId == previousId && device is NfcYubiKeyDevice) { - // Run any pending action - pendingAction?.let { action -> - action.invoke(Result.success(session)) - pendingAction = null + // only run pending action over NFC + // when the device is still the same + // or when there is no previous device, but we have a pending action + if (device is NfcYubiKeyDevice && + ((session.deviceId == previousId) || + (previousId == null && pendingAction != null)) + ) { + // update session if it is null + if (previousId == null) { + oathViewModel.setSessionState( + Session( + session, + keyManager.isRemembered(session.deviceId) + ) + ) + + if (!session.isLocked) { + try { + // only load the accounts without calculating the codes + oathViewModel.updateCredentials(getAccounts(session)) + } catch (e: IOException) { + oathViewModel.updateCredentials(emptyMap()) + } } } - // Refresh codes - if (!session.isLocked) { - try { - oathViewModel.updateCredentials(calculateOathCodes(session)) - } catch (error: Exception) { - logger.error("Failed to refresh codes", error) + // Either run a pending action, or just refresh codes + if (pendingAction != null) { + pendingAction?.let { action -> + pendingAction = null + // it is the pending action who handles this request + requestHandled = false + action.invoke(Result.success(session)) + } + } else { + // Refresh codes + if (!session.isLocked) { + try { + oathViewModel.updateCredentials(calculateOathCodes(session)) + } catch (error: Exception) { + logger.error("Failed to refresh codes: ", error) + throw error + } } } } else { @@ -246,7 +290,15 @@ class OathManager( ) ) if (!session.isLocked) { - oathViewModel.updateCredentials(calculateOathCodes(session)) + try { + oathViewModel.updateCredentials(calculateOathCodes(session)) + } catch (e: IOException) { + // in this situation we clear the session because otherwise + // the credential list would be in loading state + // clearing the session will prompt the user to try again + oathViewModel.clearSession() + throw e + } } // Awaiting an action for a different or no device? @@ -255,6 +307,7 @@ class OathManager( if (addToAny) { // Special "add to any YubiKey" action, process addToAny = false + requestHandled = false action.invoke(Result.success(session)) } else { // Awaiting an action for a different device? Fail it and stop processing. @@ -284,20 +337,35 @@ class OathManager( } } } + logger.debug( "Successfully read Oath session info (and credentials if unlocked) from connected key" ) if (updateDeviceInfo.getAndSet(false)) { - deviceManager.setDeviceInfo(getDeviceInfo(device)) + deviceManager.setDeviceInfo(runCatching { getDeviceInfo(device) }.getOrNull()) } } catch (e: Exception) { // OATH not enabled/supported, try to get DeviceInfo over other USB interfaces - logger.error("Failed to connect to CCID: ", e) + logger.error("Exception during SmartCard connection/OATH session creation: ", e) - // Clear any cached OATH state - oathViewModel.clearSession() + // Cancel any pending action + pendingAction?.let { action -> + logger.error("Cancelling pending action. Cause: ", e) + action.invoke(Result.failure(CancellationException())) + pendingAction = null + } + + if (e !is IOException) { + // we don't clear the session on IOExceptions so that the session is ready for + // a possible re-run of a failed action. + oathViewModel.clearSession() + } + + throw e } + + return requestHandled } private suspend fun addAccountToAny( @@ -307,7 +375,7 @@ class OathManager( val credentialData: CredentialData = CredentialData.parseUri(URI.create(uri)) addToAny = true - return useOathSessionNfc(OathActionDescription.AddAccount) { session -> + return useOathSession { session -> // We need to check for duplicates here since we haven't yet read the credentials if (session.credentials.any { it.id.contentEquals(credentialData.id) }) { throw IllegalArgumentException() @@ -337,7 +405,7 @@ class OathManager( logger.trace("Adding following accounts: {}", uris) addToAny = true - return useOathSession(OathActionDescription.AddMultipleAccounts) { session -> + return useOathSession { session -> var successCount = 0 for (index in uris.indices) { @@ -369,7 +437,7 @@ class OathManager( } private suspend fun reset(): String = - useOathSession(OathActionDescription.Reset, updateDeviceInfo = true) { + useOathSession(updateDeviceInfo = true) { // note, it is ok to reset locked session it.reset() keyManager.removeKey(it.deviceId) @@ -381,7 +449,7 @@ class OathManager( } private suspend fun unlock(password: String, remember: Boolean): String = - useOathSession(OathActionDescription.Unlock) { + useOathSession { val accessKey = it.deriveAccessKey(password.toCharArray()) keyManager.addKey(it.deviceId, accessKey, remember) @@ -390,9 +458,13 @@ class OathManager( if (unlocked) { oathViewModel.setSessionState(Session(it, remembered)) - // fetch credentials after unlocking only if the YubiKey is connected over USB - if (deviceManager.isUsbKeyConnected()) { + try { oathViewModel.updateCredentials(calculateOathCodes(it)) + } catch (e: Exception) { + // after unlocking there was problem getting the codes + // to avoid inconsistent UI, clear the session + oathViewModel.clearSession() + throw e } } @@ -404,7 +476,6 @@ class OathManager( newPassword: String, ): String = useOathSession( - OathActionDescription.SetPassword, unlock = false, updateDeviceInfo = true ) { session -> @@ -426,7 +497,7 @@ class OathManager( } private suspend fun unsetPassword(currentPassword: String): String = - useOathSession(OathActionDescription.UnsetPassword, unlock = false) { session -> + useOathSession(unlock = false) { session -> if (session.isAccessKeySet) { // test current password sent by the user if (session.unlock(currentPassword.toCharArray())) { @@ -458,7 +529,7 @@ class OathManager( uri: String, requireTouch: Boolean, ): String = - useOathSession(OathActionDescription.AddAccount) { session -> + useOathSession { session -> val credentialData: CredentialData = CredentialData.parseUri(URI.create(uri)) @@ -479,21 +550,24 @@ class OathManager( } private suspend fun renameAccount(uri: String, name: String, issuer: String?): String = - useOathSession(OathActionDescription.RenameAccount) { session -> - val credential = getOathCredential(session, uri) - val renamedCredential = - Credential(session.renameCredential(credential, name, issuer), session.deviceId) + useOathSession { session -> + val credential = getCredential(uri) + val renamed = Credential( + session.renameCredential(credential, name, issuer), + session.deviceId + ) + oathViewModel.renameCredential( Credential(credential, session.deviceId), - renamedCredential + renamed ) - jsonSerializer.encodeToString(renamedCredential) + jsonSerializer.encodeToString(renamed) } private suspend fun deleteAccount(credentialId: String): String = - useOathSession(OathActionDescription.DeleteAccount) { session -> - val credential = getOathCredential(session, credentialId) + useOathSession { session -> + val credential = getCredential(credentialId) session.deleteCredential(credential) oathViewModel.removeCredential(Credential(credential, session.deviceId)) NULL @@ -510,7 +584,7 @@ class OathManager( deviceManager.withKey { usbYubiKeyDevice -> try { - useOathSessionUsb(usbYubiKeyDevice) { session -> + useSessionUsb(usbYubiKeyDevice) { session -> try { oathViewModel.updateCredentials(calculateOathCodes(session)) } catch (apduException: ApduException) { @@ -534,7 +608,10 @@ class OathManager( logger.error("IOException when accessing USB device: ", ioException) clearCodes() } catch (illegalStateException: IllegalStateException) { - logger.error("IllegalStateException when accessing USB device: ", illegalStateException) + logger.error( + "IllegalStateException when accessing USB device: ", + illegalStateException + ) clearCodes() } } @@ -542,8 +619,8 @@ class OathManager( private suspend fun calculate(credentialId: String): String = - useOathSession(OathActionDescription.CalculateCode) { session -> - val credential = getOathCredential(session, credentialId) + useOathSession { session -> + val credential = getCredential(credentialId) val code = Code.from(calculateCode(session, credential)) oathViewModel.updateCode( @@ -633,6 +710,14 @@ class OathManager( return session } + private fun getAccounts(session: YubiKitOathSession): Map { + return session.credentials.map { credential -> + Pair( + Credential(credential, session.deviceId), + null + ) + }.toMap() + } private fun calculateOathCodes(session: YubiKitOathSession): Map { val isUsbKey = deviceManager.isUsbKeyConnected() @@ -645,35 +730,51 @@ class OathManager( return session.calculateCodes(timestamp).map { (credential, code) -> Pair( Credential(credential, session.deviceId), - Code.from(if (credential.isSteamCredential() && (!credential.isTouchRequired || bypassTouch)) { - session.calculateSteamCode(credential, timestamp) - } else if (credential.isTouchRequired && bypassTouch) { - session.calculateCode(credential, timestamp) - } else { - code - }) + Code.from( + if (credential.isSteamCredential() && (!credential.isTouchRequired || bypassTouch)) { + session.calculateSteamCode(credential, timestamp) + } else if (credential.isTouchRequired && bypassTouch) { + session.calculateCode(credential, timestamp) + } else { + code + } + ) ) }.toMap() } + private fun getCredential(id: String): YubiKitCredential { + val credential = + oathViewModel.credentials.value?.find { it.credential.id == id }?.credential + + if (credential == null || credential.data == null) { + logger.debug("Failed to find credential with id: {}", id) + throw Exception("Failed to find account") + } + + return credential.data + } + private suspend fun useOathSession( - oathActionDescription: OathActionDescription, unlock: Boolean = true, updateDeviceInfo: Boolean = false, - action: (YubiKitOathSession) -> T + block: (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, updateDeviceInfo, action) }, - onNfc = { useOathSessionNfc(oathActionDescription, action) } + onUsb = { useSessionUsb(it, updateDeviceInfo, block) }, + onNfc = { useSessionNfc(block) }, + onCancelled = { + pendingAction?.invoke(Result.failure(CancellationException())) + pendingAction = null + } ) } - private suspend fun useOathSessionUsb( + private suspend fun useSessionUsb( device: UsbYubiKeyDevice, updateDeviceInfo: Boolean = false, block: (YubiKitOathSession) -> T @@ -681,14 +782,13 @@ class OathManager( block(getOathSession(it)) }.also { if (updateDeviceInfo) { - deviceManager.setDeviceInfo(getDeviceInfo(device)) + deviceManager.setDeviceInfo(runCatching { getDeviceInfo(device) }.getOrNull()) } } - private suspend fun useOathSessionNfc( - oathActionDescription: OathActionDescription, - block: (YubiKitOathSession) -> T - ): T { + private suspend fun useSessionNfc( + block: (YubiKitOathSession) -> T, + ): Result { try { val result = suspendCoroutine { outer -> pendingAction = { @@ -696,41 +796,18 @@ class OathManager( block.invoke(it.value) }) } - dialogManager.showDialog(DialogIcon.Nfc, DialogTitle.TapKey, oathActionDescription.id) { - logger.debug("Cancelled Dialog {}", oathActionDescription.name) - pendingAction?.invoke(Result.failure(CancellationException())) - pendingAction = null - } + // here the coroutine is suspended and waits till pendingAction is + // invoked - the pending action result will resume this coroutine } - dialogManager.updateDialogState( - dialogIcon = DialogIcon.Success, - dialogTitle = DialogTitle.OperationSuccessful - ) - // TODO: This delays the closing of the dialog, but also the return value - delay(500) - return result + return Result.success(result!!) } catch (cancelled: CancellationException) { - throw cancelled - } catch (error: Throwable) { - dialogManager.updateDialogState( - dialogIcon = DialogIcon.Failure, - dialogTitle = DialogTitle.OperationFailed, - dialogDescriptionId = OathActionDescription.ActionFailure.id - ) - // TODO: This delays the closing of the dialog, but also the return value - delay(1500) - throw error - } finally { - dialogManager.closeDialog() + return Result.failure(cancelled) + } catch (e: Exception) { + logger.error("Exception during action: ", e) + return Result.failure(e) } } - private fun getOathCredential(session: YubiKitOathSession, credentialId: String) = - // we need to use oathSession.calculateCodes() to get proper Credential.touchRequired value - session.calculateCodes().map { e -> e.key }.firstOrNull { credential -> - (credential != null) && credential.id.asString() == credentialId - } ?: throw Exception("Failed to find account") - override fun onConnected(device: YubiKeyDevice) { refreshJob?.cancel() } diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/data/Credential.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/data/Credential.kt index 60c45ab9c..b827605f5 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/data/Credential.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/data/Credential.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. @@ -35,9 +35,10 @@ data class Credential( @SerialName("name") val accountName: String, @SerialName("touch_required") - val touchRequired: Boolean + val touchRequired: Boolean, + @kotlinx.serialization.Transient + val data: YubiKitCredential? = null ) { - constructor(credential: YubiKitCredential, deviceId: String) : this( deviceId = deviceId, id = credential.id.asString(), @@ -48,7 +49,8 @@ data class Credential( period = credential.period, issuer = credential.issuer, accountName = credential.accountName, - touchRequired = credential.isTouchRequired + touchRequired = credential.isTouchRequired, + data = credential ) override fun equals(other: Any?): Boolean = 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 a0f04d779..938014a37 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 @@ -47,17 +47,17 @@ class DeviceInfoHelper { private val restrictedNfcBytes = byteArrayOf(0x00, 0x1F, 0xD1.toByte(), 0x01, 0x1b, 0x55, 0x04) + uri - suspend fun getDeviceInfo(device: YubiKeyDevice): Info? { + suspend fun getDeviceInfo(device: YubiKeyDevice): Info { SessionVersionOverride.set(null) var deviceInfo = readDeviceInfo(device) - if (deviceInfo?.version?.major == 0.toByte()) { + if (deviceInfo.version.major == 0.toByte()) { SessionVersionOverride.set(Version(5, 7, 2)) deviceInfo = readDeviceInfo(device) } return deviceInfo } - private suspend fun readDeviceInfo(device: YubiKeyDevice): Info? { + private suspend fun readDeviceInfo(device: YubiKeyDevice): Info { val pid = (device as? UsbYubiKeyDevice)?.pid val deviceInfo = runCatching { @@ -106,8 +106,8 @@ class DeviceInfoHelper { } } catch (e: Exception) { // no smart card connectivity - logger.error("Failure getting device info", e) - return null + logger.error("Failure getting device info: ", e) + throw e } } diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt b/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/NfcState.kt similarity index 56% rename from android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt rename to android/app/src/main/kotlin/com/yubico/authenticator/yubikit/NfcState.kt index ac78d2c5b..670acf668 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathActionDescription.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/NfcState.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. @@ -14,22 +14,12 @@ * limitations under the License. */ -package com.yubico.authenticator.oath +package com.yubico.authenticator.yubikit -const val dialogDescriptionOathIndex = 100 - -enum class OathActionDescription(private val value: Int) { - Reset(0), - Unlock(1), - SetPassword(2), - UnsetPassword(3), - AddAccount(4), - RenameAccount(5), - DeleteAccount(6), - CalculateCode(7), - ActionFailure(8), - AddMultipleAccounts(9); - - val id: Int - get() = value + dialogDescriptionOathIndex +enum class NfcState(val value: Int) { + DISABLED(0), + IDLE(1), + ONGOING(2), + SUCCESS(3), + FAILURE(4) } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/NfcStateDispatcher.kt b/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/NfcStateDispatcher.kt new file mode 100644 index 000000000..d825bb0ea --- /dev/null +++ b/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/NfcStateDispatcher.kt @@ -0,0 +1,60 @@ +/* + * 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.yubikit + +import android.app.Activity +import android.nfc.NfcAdapter + +import com.yubico.yubikit.android.transport.nfc.NfcConfiguration +import com.yubico.yubikit.android.transport.nfc.NfcDispatcher +import com.yubico.yubikit.android.transport.nfc.NfcReaderDispatcher + +import org.slf4j.LoggerFactory + +interface NfcStateListener { + fun onChange(newState: NfcState) +} + +class NfcStateDispatcher(private val listener: NfcStateListener) : NfcDispatcher { + + private lateinit var adapter: NfcAdapter + private lateinit var yubikitNfcDispatcher: NfcReaderDispatcher + + private val logger = LoggerFactory.getLogger(NfcStateDispatcher::class.java) + + override fun enable( + activity: Activity, + nfcConfiguration: NfcConfiguration, + handler: NfcDispatcher.OnTagHandler + ) { + adapter = NfcAdapter.getDefaultAdapter(activity) + yubikitNfcDispatcher = NfcReaderDispatcher(adapter) + + logger.debug("enabling yubikit NFC state dispatcher") + yubikitNfcDispatcher.enable( + activity, + nfcConfiguration, + handler + ) + } + + override fun disable(activity: Activity) { + listener.onChange(NfcState.DISABLED) + yubikitNfcDispatcher.disable(activity) + logger.debug("disabling yubikit NFC state dispatcher") + } +} \ No newline at end of file diff --git a/android/app/src/main/res/values-pl/strings.xml b/android/app/src/main/res/values-pl/strings.xml index 9bb0debc9..355069db1 100644 --- a/android/app/src/main/res/values-pl/strings.xml +++ b/android/app/src/main/res/values-pl/strings.xml @@ -1,7 +1,7 @@ - OTP zostało skopiowane do schowka. - Hasło statyczne zostało skopiowane do schowka. - Błąd czytania OTP z YubiKey. - Błąd kopiowania OTP do schowka. + Kod OTP został skopiowany do schowka. + Hasło zostało skopiowane do schowka. + Nie udało się odczytać kodu OTP z klucza YubiKey. + Wystąpił błąd podczas kopiowania kodu OTP do schowka. \ No newline at end of file diff --git a/helper/helper/base.py b/helper/helper/base.py index 622273506..c894b10ff 100644 --- a/helper/helper/base.py +++ b/helper/helper/base.py @@ -114,6 +114,7 @@ def child(func=None, *, condition=None): class RpcNode: def __init__(self): + self._closed = False self._child = None self._child_name = None @@ -132,6 +133,8 @@ def __call__(self, action, target, params, event, signal, traversed=None): response = self.get_child(action)( "get", [], params, event, signal, traversed ) + else: + raise NoSuchActionException(action) if isinstance(response, RpcResponse): return response @@ -143,12 +146,17 @@ def __call__(self, action, target, params, event, signal, traversed=None): raise # Prevent catching this as a ValueError below except ValueError as e: raise InvalidParametersException(e) - raise NoSuchActionException(action) def close(self): + logger.debug(f"Closing node {self}") + self._closed = True if self._child: self._close_child() + @property + def closed(self): + return self._closed + def get_data(self): return dict() @@ -208,7 +216,7 @@ def get_child(self, name): if self._child and self._child_name != name: self._close_child() - if not self._child: + if not self._child or self._child.closed: self._child = self.create_child(name) self._child_name = name logger.debug("created child: %s", name) diff --git a/helper/helper/device.py b/helper/helper/device.py index a2db4192e..846fbc077 100644 --- a/helper/helper/device.py +++ b/helper/helper/device.py @@ -168,7 +168,7 @@ def list_children(self): return self._readers def create_child(self, name): - return ReaderDeviceNode(self._reader_mapping[name], None) + return ReaderDeviceNode(self._reader_mapping[name]) class _ScanDevices: @@ -202,7 +202,12 @@ def __init__(self): def __call__(self, *args, **kwargs): with self._get_state: try: - return super().__call__(*args, **kwargs) + response = super().__call__(*args, **kwargs) + if "device_closed" in response.flags: + self._list_state = 0 + self._device_mapping = {} + response.flags.remove("device_closed") + return response except ConnectionException as e: if self._failing_connection == e.body: self._retries += 1 @@ -238,7 +243,7 @@ def list_children(self): dev_id = str(info.serial) else: dev_id = _id_from_fingerprint(dev.fingerprint) - self._device_mapping[dev_id] = (dev, info) + self._device_mapping[dev_id] = dev name = get_name(info, dev.pid.yubikey_type if dev.pid else None) self._devices[dev_id] = dict(pid=dev.pid, name=name, serial=info.serial) @@ -255,30 +260,40 @@ def create_child(self, name): if name not in self._device_mapping and self._list_state == 0: self.list_children() try: - return UsbDeviceNode(*self._device_mapping[name]) + return UsbDeviceNode(self._device_mapping[name]) except KeyError: raise NoSuchNodeException(name) class AbstractDeviceNode(RpcNode): - def __init__(self, device, info): + def __init__(self, device): super().__init__() self._device = device - self._info = info - self._data = None + self._info = None + self._data = self._refresh_data() def __call__(self, *args, **kwargs): try: response = super().__call__(*args, **kwargs) + + # The command resulted in the device closing + if "device_closed" in response.flags: + self.close() + return response + + # The command resulted in device_info modification if "device_info" in response.flags: - # Clear DeviceInfo cache - self._info = None - self._data = None + old_info = self._info + # Refresh data + self._data = self._refresh_data() + if old_info == self._info: + # No change to DeviceInfo, further propagation not needed. + response.flags.remove("device_info") return response except (SmartcardException, OSError): - logger.exception("Device error") + logger.exception("Device error", exc_info=True) self._child = None name = self._child_name @@ -293,9 +308,9 @@ def create_child(self, name): raise NoSuchNodeException(name) def get_data(self): - if not self._data: - self._data = self._refresh_data() - return self._data + if self._data: + return self._data + raise ChildResetException("Unable to read device data") def _refresh_data(self): ... @@ -321,6 +336,18 @@ def _create_connection(self, conn_type): return ConnectionNode(self._device, connection, self._info) def _refresh_data(self): + # Re-use existing connection if possible + if self._child and not self._child.closed: + # Make sure to close any open session + self._child._close_child() + try: + return self._read_data(self._child._connection) + except Exception: + logger.warning( + f"Unable to use {self._child._connection}", exc_info=True + ) + + # No child, open new connection for conn_type in (SmartCardConnection, OtpConnection, FidoConnection): if self._supports_connection(conn_type): try: @@ -328,7 +355,9 @@ def _refresh_data(self): return self._read_data(conn) except Exception: logger.warning(f"Unable to connect via {conn_type}", exc_info=True) - raise ValueError("No supported connections") + # Failed to refresh, close + self.close() + return None @child(condition=lambda self: self._supports_connection(SmartCardConnection)) def ccid(self): @@ -383,11 +412,11 @@ def update(self, observable, actions): class ReaderDeviceNode(AbstractDeviceNode): - def __init__(self, device, info): - super().__init__(device, info) + def __init__(self, device): self._observer = _ReaderObserver(device) self._monitor = CardMonitor() self._monitor.addObserver(self._observer) + super().__init__(device) def close(self): self._monitor.deleteObserver(self._observer) @@ -395,17 +424,21 @@ def close(self): def get_data(self): if self._observer.needs_refresh: - self._data = None + self._data = self._refresh_data() return super().get_data() + def _read_data(self, conn): + return dict(super()._read_data(conn), present=True) + def _refresh_data(self): card = self._observer.card if card is None: return dict(present=False, status="no-card") try: + self._close_child() with self._device.open_connection(SmartCardConnection) as conn: try: - data = dict(self._read_data(conn), present=True) + data = self._read_data(conn) except ValueError: # Unknown device, maybe NFC restricted try: @@ -434,8 +467,7 @@ def get(self, params, event, signal): def ccid(self): try: connection = self._device.open_connection(SmartCardConnection) - info = read_info(connection) - return ScpConnectionNode(self._device, connection, info) + return ScpConnectionNode(self._device, connection, self._info) except (ValueError, SmartcardException, EstablishContextException) as e: logger.warning("Error opening connection", exc_info=True) raise ConnectionException(self._device.fingerprint, "ccid", e) @@ -443,10 +475,8 @@ def ccid(self): @child def fido(self): try: - with self._device.open_connection(SmartCardConnection) as conn: - info = read_info(conn) connection = self._device.open_connection(FidoConnection) - return ConnectionNode(self._device, connection, info) + return ConnectionNode(self._device, connection, self._info) except (ValueError, SmartcardException, EstablishContextException) as e: logger.warning("Error opening connection", exc_info=True) raise ConnectionException(self._device.fingerprint, "fido", e) @@ -458,24 +488,11 @@ def __init__(self, device, connection, info): self._device = device self._transport = device.transport self._connection = connection - self._info = info or read_info(self._connection, device.pid) + self._info = info def __call__(self, *args, **kwargs): try: - 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 + return super().__call__(*args, **kwargs) except (SmartcardException, OSError) as e: logger.exception("Connection error") raise ChildResetException(f"{e}") @@ -504,11 +521,6 @@ def close(self): logger.warning("Error closing connection", exc_info=True) def get_data(self): - if ( - isinstance(self._connection, SmartCardConnection) - or self._transport == TRANSPORT.USB - ): - 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)): diff --git a/helper/helper/fido.py b/helper/helper/fido.py index 181cac426..284c6e4bb 100644 --- a/helper/helper/fido.py +++ b/helper/helper/fido.py @@ -91,6 +91,14 @@ def __init__(self, connection): self.client_pin = ClientPin(self.ctap) self._token = None + def __call__(self, *args, **kwargs): + try: + return super().__call__(*args, **kwargs) + except CtapError as e: + if e.code == CtapError.ERR.PIN_AUTH_INVALID: + raise AuthRequiredException() + raise + def get_data(self): self._info = self.ctap.get_info() logger.debug(f"Info: {self._info}") @@ -195,7 +203,7 @@ def reset(self, params, event, signal): raise self._info = self.ctap.get_info() self._token = None - return RpcResponse(dict(), ["device_info"]) + return RpcResponse(dict(), ["device_info", "device_closed"]) @action(condition=lambda self: self._info.options["clientPin"]) def unlock(self, params, event, signal): diff --git a/helper/helper/management.py b/helper/helper/management.py index 3086a1bde..5474f8e21 100644 --- a/helper/helper/management.py +++ b/helper/helper/management.py @@ -51,20 +51,20 @@ def list_actions(self): def _await_reboot(self, serial, usb_enabled): ifaces = CAPABILITY(usb_enabled or 0).usb_interfaces + types: list[Type[Connection]] = [ + SmartCardConnection, + OtpConnection, + # mypy doesn't support ABC.register() + FidoConnection, # type: ignore + ] + connection_types = [t for t in types if t.usb_interface in ifaces] # Prefer to use the "same" connection type as before - if self._connection_type.usb_interface in ifaces: - connection_types = [self._connection_type] - else: - types: list[Type[Connection]] = [ - SmartCardConnection, - OtpConnection, - # mypy doesn't support ABC.register() - FidoConnection, # type: ignore - ] - connection_types = [t for t in types if t.usb_interface in ifaces] + if self._connection_type in connection_types: + connection_types.remove(self._connection_type) + connection_types.insert(0, self._connection_type) self.session.close() - logger.debug("Waiting for device to re-appear...") + logger.debug(f"Waiting for device to re-appear over {connection_types}...") for _ in range(10): sleep(0.2) # Always sleep initially for dev, info in list_all_devices(connection_types): @@ -87,10 +87,12 @@ def configure(self, params, event, signal): ) serial = self.session.read_device_info().serial self.session.write_device_config(config, reboot, cur_lock_code, new_lock_code) + flags = ["device_info"] if reboot: enabled = config.enabled_capabilities.get(TRANSPORT.USB) + flags.append("device_closed") self._await_reboot(serial, enabled) - return RpcResponse(dict(), ["device_info"]) + return RpcResponse(dict(), flags) @action def set_mode(self, params, event, signal): diff --git a/helper/version_info.txt b/helper/version_info.txt index c4c014f7d..b925ee1a2 100755 --- a/helper/version_info.txt +++ b/helper/version_info.txt @@ -6,8 +6,8 @@ VSVersionInfo( ffi=FixedFileInfo( # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) # Set not needed items to zero 0. - filevers=(7, 0, 2, 0), - prodvers=(7, 0, 2, 0), + filevers=(7, 1, 1, 0), + prodvers=(7, 1, 1, 0), # Contains a bitmask that specifies the valid bits 'flags'r mask=0x3f, # Contains a bitmask that specifies the Boolean attributes of the file. @@ -31,11 +31,11 @@ VSVersionInfo( '040904b0', [StringStruct('CompanyName', 'Yubico'), StringStruct('FileDescription', 'Yubico Authenticator Helper'), - StringStruct('FileVersion', '7.0.2-dev.0'), + StringStruct('FileVersion', '7.1.1-dev.0'), StringStruct('LegalCopyright', 'Copyright (c) Yubico'), StringStruct('OriginalFilename', 'authenticator-helper.exe'), StringStruct('ProductName', 'Yubico Authenticator'), - StringStruct('ProductVersion', '7.0.2-dev.0')]) + StringStruct('ProductVersion', '7.1.1-dev.0')]) ]), VarFileInfo([VarStruct('Translation', [1033, 1200])]) ] diff --git a/lib/android/app_methods.dart b/lib/android/app_methods.dart index f383cfb0d..d152d82a3 100644 --- a/lib/android/app_methods.dart +++ b/lib/android/app_methods.dart @@ -18,6 +18,7 @@ import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../theme.dart'; import 'state.dart'; @@ -73,8 +74,14 @@ void setupAppMethodsChannel(WidgetRef ref) { switch (call.method) { case 'nfcAdapterStateChanged': { - var nfcEnabled = args['nfcEnabled']; - ref.read(androidNfcStateProvider.notifier).setNfcEnabled(nfcEnabled); + var enabled = args['enabled']; + ref.read(androidNfcAdapterState.notifier).enable(enabled); + break; + } + case 'nfcStateChanged': + { + var nfcState = args['state']; + ref.read(androidNfcState.notifier).set(nfcState); break; } default: diff --git a/lib/android/fido/state.dart b/lib/android/fido/state.dart index 956c58b78..5daf430ee 100644 --- a/lib/android/fido/state.dart +++ b/lib/android/fido/state.dart @@ -32,17 +32,18 @@ import '../../exception/no_data_exception.dart'; import '../../exception/platform_exception_decoder.dart'; import '../../fido/models.dart'; import '../../fido/state.dart'; +import '../overlay/nfc/method_channel_notifier.dart'; final _log = Logger('android.fido.state'); -const _methods = MethodChannel('android.fido.methods'); - final androidFidoStateProvider = AsyncNotifierProvider.autoDispose .family(_FidoStateNotifier.new); class _FidoStateNotifier extends FidoStateNotifier { final _events = const EventChannel('android.fido.sessionState'); late StreamSubscription _sub; + late final _FidoMethodChannelNotifier fido = + ref.read(_fidoMethodsProvider.notifier); @override FutureOr build(DevicePath devicePath) async { @@ -79,7 +80,7 @@ class _FidoStateNotifier extends FidoStateNotifier { }); controller.onCancel = () async { - await _methods.invokeMethod('cancelReset'); + await fido.invoke('cancelReset'); if (!controller.isClosed) { await subscription.cancel(); } @@ -87,7 +88,7 @@ class _FidoStateNotifier extends FidoStateNotifier { controller.onListen = () async { try { - await _methods.invokeMethod('reset'); + await fido.invoke('reset'); await controller.sink.close(); ref.invalidateSelf(); } catch (e) { @@ -102,13 +103,8 @@ class _FidoStateNotifier extends FidoStateNotifier { @override Future setPin(String newPin, {String? oldPin}) async { try { - final response = jsonDecode(await _methods.invokeMethod( - 'setPin', - { - 'pin': oldPin, - 'newPin': newPin, - }, - )); + final response = jsonDecode( + await fido.invoke('setPin', {'pin': oldPin, 'newPin': newPin})); if (response['success'] == true) { _log.debug('FIDO PIN set/change successful'); return PinResult.success(); @@ -134,10 +130,7 @@ class _FidoStateNotifier extends FidoStateNotifier { @override Future unlock(String pin) async { try { - final response = jsonDecode(await _methods.invokeMethod( - 'unlock', - {'pin': pin}, - )); + final response = jsonDecode(await fido.invoke('unlock', {'pin': pin})); if (response['success'] == true) { _log.debug('FIDO applet unlocked'); @@ -165,9 +158,8 @@ class _FidoStateNotifier extends FidoStateNotifier { @override Future enableEnterpriseAttestation() async { try { - final response = jsonDecode(await _methods.invokeMethod( - 'enableEnterpriseAttestation', - )); + final response = + jsonDecode(await fido.invoke('enableEnterpriseAttestation')); if (response['success'] == true) { _log.debug('Enterprise attestation enabled'); @@ -193,6 +185,8 @@ final androidFingerprintProvider = AsyncNotifierProvider.autoDispose class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier { final _events = const EventChannel('android.fido.fingerprints'); late StreamSubscription _sub; + late final _FidoMethodChannelNotifier fido = + ref.read(_fidoMethodsProvider.notifier); @override FutureOr> build(DevicePath devicePath) async { @@ -243,7 +237,7 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier { controller.onCancel = () async { if (!controller.isClosed) { _log.debug('Cancelling fingerprint registration'); - await _methods.invokeMethod('cancelRegisterFingerprint'); + await fido.invoke('cancelRegisterFingerprint'); await registerFpSub.cancel(); } }; @@ -251,7 +245,7 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier { controller.onListen = () async { try { final registerFpResult = - await _methods.invokeMethod('registerFingerprint', {'name': name}); + await fido.invoke('registerFingerprint', {'name': name}); _log.debug('Finished registerFingerprint with: $registerFpResult'); @@ -286,13 +280,9 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier { Future renameFingerprint( Fingerprint fingerprint, String name) async { try { - final renameFingerprintResponse = jsonDecode(await _methods.invokeMethod( - 'renameFingerprint', - { - 'templateId': fingerprint.templateId, - 'name': name, - }, - )); + final renameFingerprintResponse = jsonDecode(await fido.invoke( + 'renameFingerprint', + {'templateId': fingerprint.templateId, 'name': name})); if (renameFingerprintResponse['success'] == true) { _log.debug('FIDO rename fingerprint succeeded'); @@ -316,12 +306,8 @@ class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier { @override Future deleteFingerprint(Fingerprint fingerprint) async { try { - final deleteFingerprintResponse = jsonDecode(await _methods.invokeMethod( - 'deleteFingerprint', - { - 'templateId': fingerprint.templateId, - }, - )); + final deleteFingerprintResponse = jsonDecode(await fido + .invoke('deleteFingerprint', {'templateId': fingerprint.templateId})); if (deleteFingerprintResponse['success'] == true) { _log.debug('FIDO delete fingerprint succeeded'); @@ -348,6 +334,8 @@ final androidCredentialProvider = AsyncNotifierProvider.autoDispose class _FidoCredentialsNotifier extends FidoCredentialsNotifier { final _events = const EventChannel('android.fido.credentials'); late StreamSubscription _sub; + late final _FidoMethodChannelNotifier fido = + ref.read(_fidoMethodsProvider.notifier); @override FutureOr> build(DevicePath devicePath) async { @@ -371,20 +359,22 @@ class _FidoCredentialsNotifier extends FidoCredentialsNotifier { @override Future deleteCredential(FidoCredential credential) async { try { - await _methods.invokeMethod( - 'deleteCredential', - { - 'rpId': credential.rpId, - 'credentialId': credential.credentialId, - }, - ); + await fido.invoke('deleteCredential', + {'rpId': credential.rpId, 'credentialId': credential.credentialId}); } on PlatformException catch (pe) { var decodedException = pe.decode(); if (decodedException is CancellationException) { _log.debug('User cancelled delete credential FIDO operation'); - } else { - throw decodedException; } + throw decodedException; } } } + +final _fidoMethodsProvider = NotifierProvider<_FidoMethodChannelNotifier, void>( + () => _FidoMethodChannelNotifier()); + +class _FidoMethodChannelNotifier extends MethodChannelNotifier { + _FidoMethodChannelNotifier() + : super(const MethodChannel('android.fido.methods')); +} diff --git a/lib/android/init.dart b/lib/android/init.dart index 6322acd0b..b638df950 100644 --- a/lib/android/init.dart +++ b/lib/android/init.dart @@ -40,9 +40,10 @@ import 'logger.dart'; import 'management/state.dart'; import 'oath/otp_auth_link_handler.dart'; import 'oath/state.dart'; +import 'overlay/nfc/nfc_event_notifier.dart'; +import 'overlay/nfc/nfc_overlay.dart'; import 'qr_scanner/qr_scanner_provider.dart'; import 'state.dart'; -import 'tap_request_dialog.dart'; import 'window_state_provider.dart'; Future initialize() async { @@ -106,6 +107,8 @@ Future initialize() async { child: DismissKeyboard( child: YubicoAuthenticatorApp(page: Consumer( builder: (context, ref, child) { + ref.read(nfcEventNotifierListener).startListener(context); + Timer.run(() { ref.read(featureFlagProvider.notifier) // TODO: Load feature flags from file/config? @@ -119,8 +122,8 @@ Future initialize() async { // activates window state provider ref.read(androidWindowStateProvider); - // initializes global handler for dialogs - ref.read(androidDialogProvider); + // initializes overlay for nfc events + ref.read(nfcOverlay); // set context which will handle otpauth links setupOtpAuthLinkHandler(context); diff --git a/lib/android/oath/state.dart b/lib/android/oath/state.dart index 887d4ff91..25bf1ce59 100755 --- a/lib/android/oath/state.dart +++ b/lib/android/oath/state.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. @@ -36,12 +36,11 @@ import '../../exception/platform_exception_decoder.dart'; import '../../oath/models.dart'; import '../../oath/state.dart'; import '../../widgets/toast.dart'; -import '../tap_request_dialog.dart'; +import '../overlay/nfc/method_channel_notifier.dart'; +import '../overlay/nfc/nfc_overlay.dart'; final _log = Logger('android.oath.state'); -const _methods = MethodChannel('android.oath.methods'); - final androidOathStateProvider = AsyncNotifierProvider.autoDispose .family( _AndroidOathStateNotifier.new); @@ -49,6 +48,8 @@ final androidOathStateProvider = AsyncNotifierProvider.autoDispose class _AndroidOathStateNotifier extends OathStateNotifier { final _events = const EventChannel('android.oath.sessionState'); late StreamSubscription _sub; + late _OathMethodChannelNotifier oath = + ref.watch(_oathMethodsProvider.notifier); @override FutureOr build(DevicePath arg) { @@ -74,10 +75,7 @@ class _AndroidOathStateNotifier extends OathStateNotifier { @override Future reset() async { try { - // await ref - // .read(androidAppContextHandler) - // .switchAppContext(Application.accounts); - await _methods.invokeMethod('reset'); + await oath.invoke('reset'); } catch (e) { _log.debug('Calling reset failed with exception: $e'); } @@ -86,8 +84,8 @@ class _AndroidOathStateNotifier extends OathStateNotifier { @override Future<(bool, bool)> unlock(String password, {bool remember = false}) async { try { - final unlockResponse = jsonDecode(await _methods.invokeMethod( - 'unlock', {'password': password, 'remember': remember})); + final unlockResponse = jsonDecode(await oath + .invoke('unlock', {'password': password, 'remember': remember})); _log.debug('applet unlocked'); final unlocked = unlockResponse['unlocked'] == true; @@ -108,11 +106,16 @@ class _AndroidOathStateNotifier extends OathStateNotifier { @override Future setPassword(String? current, String password) async { try { - await _methods.invokeMethod( - 'setPassword', {'current': current, 'password': password}); + await oath + .invoke('setPassword', {'current': current, 'password': password}); return true; - } on PlatformException catch (e) { - _log.debug('Calling set password failed with exception: $e'); + } on PlatformException catch (pe) { + final decoded = pe.decode(); + if (decoded is CancellationException) { + _log.debug('Set password cancelled'); + throw decoded; + } + _log.debug('Calling set password failed with exception: $pe'); return false; } } @@ -120,10 +123,15 @@ class _AndroidOathStateNotifier extends OathStateNotifier { @override Future unsetPassword(String current) async { try { - await _methods.invokeMethod('unsetPassword', {'current': current}); + await oath.invoke('unsetPassword', {'current': current}); return true; - } on PlatformException catch (e) { - _log.debug('Calling unset password failed with exception: $e'); + } on PlatformException catch (pe) { + final decoded = pe.decode(); + if (decoded is CancellationException) { + _log.debug('Unset password cancelled'); + throw decoded; + } + _log.debug('Calling unset password failed with exception: $pe'); return false; } } @@ -131,7 +139,7 @@ class _AndroidOathStateNotifier extends OathStateNotifier { @override Future forgetPassword() async { try { - await _methods.invokeMethod('forgetPassword'); + await oath.invoke('forgetPassword'); } on PlatformException catch (e) { _log.debug('Calling forgetPassword failed with exception: $e'); } @@ -146,7 +154,7 @@ Exception handlePlatformException( toast(String message, {bool popStack = false}) => withContext((context) async { - ref.read(androidDialogProvider).closeDialog(); + ref.read(nfcOverlay.notifier).hide(); if (popStack) { Navigator.of(context).popUntil((route) { return route.isFirst; @@ -167,7 +175,7 @@ Exception handlePlatformException( return CancellationException(); } case PlatformException pe: - if (pe.code == 'JobCancellationException') { + if (pe.code == 'ContextDisposedException') { // pop stack to show FIDO view toast(l10n.l_add_account_func_missing, popStack: true); return CancellationException(); @@ -181,46 +189,31 @@ Exception handlePlatformException( final addCredentialToAnyProvider = Provider((ref) => (Uri credentialUri, {bool requireTouch = false}) async { + final oath = ref.watch(_oathMethodsProvider.notifier); try { - String resultString = await _methods.invokeMethod( - 'addAccountToAny', { + var result = jsonDecode(await oath.invoke('addAccountToAny', { 'uri': credentialUri.toString(), 'requireTouch': requireTouch - }); - - var result = jsonDecode(resultString); + })); return OathCredential.fromJson(result['credential']); } on PlatformException catch (pe) { + _log.error('Received exception: $pe'); throw handlePlatformException(ref, pe); } }); final addCredentialsToAnyProvider = Provider( (ref) => (List credentialUris, List touchRequired) async { + final oath = ref.read(_oathMethodsProvider.notifier); try { _log.debug( 'Calling android with ${credentialUris.length} credentials to be added'); - - String resultString = await _methods.invokeMethod( - 'addAccountsToAny', - { - 'uris': credentialUris, - 'requireTouch': touchRequired, - }, - ); - - _log.debug('Call result: $resultString'); - var result = jsonDecode(resultString); + var result = jsonDecode(await oath.invoke('addAccountsToAny', + {'uris': credentialUris, 'requireTouch': touchRequired})); return result['succeeded'] == credentialUris.length; } on PlatformException catch (pe) { - var decodedException = pe.decode(); - if (decodedException is CancellationException) { - _log.debug('User cancelled adding multiple accounts'); - } else { - _log.error('Failed to add multiple accounts.', pe); - } - - throw decodedException; + _log.error('Received exception: $pe'); + throw handlePlatformException(ref, pe); } }); @@ -238,6 +231,8 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier { final WithContext _withContext; final Ref _ref; late StreamSubscription _sub; + late _OathMethodChannelNotifier oath = + _ref.read(_oathMethodsProvider.notifier); _AndroidCredentialListNotifier(this._withContext, this._ref) : super() { _sub = _events.receiveBroadcastStream().listen((event) { @@ -284,8 +279,8 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier { } try { - final resultJson = await _methods - .invokeMethod('calculate', {'credentialId': credential.id}); + final resultJson = + await oath.invoke('calculate', {'credentialId': credential.id}); _log.debug('Calculate', resultJson); return OathCode.fromJson(jsonDecode(resultJson)); } on PlatformException catch (pe) { @@ -300,9 +295,8 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier { Future addAccount(Uri credentialUri, {bool requireTouch = false}) async { try { - String resultString = await _methods.invokeMethod('addAccount', + String resultString = await oath.invoke('addAccount', {'uri': credentialUri.toString(), 'requireTouch': requireTouch}); - var result = jsonDecode(resultString); return OathCredential.fromJson(result['credential']); } on PlatformException catch (pe) { @@ -314,9 +308,8 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier { Future renameAccount( OathCredential credential, String? issuer, String name) async { try { - final response = await _methods.invokeMethod('renameAccount', + final response = await oath.invoke('renameAccount', {'credentialId': credential.id, 'name': name, 'issuer': issuer}); - _log.debug('Rename response: $response'); var responseJson = jsonDecode(response); @@ -331,11 +324,24 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier { @override Future deleteAccount(OathCredential credential) async { try { - await _methods - .invokeMethod('deleteAccount', {'credentialId': credential.id}); + await oath.invoke('deleteAccount', {'credentialId': credential.id}); } on PlatformException catch (e) { - _log.debug('Received exception: $e'); - throw e.decode(); + var decoded = e.decode(); + if (decoded is CancellationException) { + _log.debug('Account delete was cancelled.'); + } else { + _log.debug('Received exception: $e'); + } + + throw decoded; } } } + +final _oathMethodsProvider = NotifierProvider<_OathMethodChannelNotifier, void>( + () => _OathMethodChannelNotifier()); + +class _OathMethodChannelNotifier extends MethodChannelNotifier { + _OathMethodChannelNotifier() + : super(const MethodChannel('android.oath.methods')); +} diff --git a/lib/android/overlay/nfc/method_channel_notifier.dart b/lib/android/overlay/nfc/method_channel_notifier.dart new file mode 100644 index 000000000..d5bfa5b8e --- /dev/null +++ b/lib/android/overlay/nfc/method_channel_notifier.dart @@ -0,0 +1,36 @@ +/* + * Copyright (C) 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. + */ + +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'nfc_overlay.dart'; + +class MethodChannelNotifier extends Notifier { + final MethodChannel _channel; + + MethodChannelNotifier(this._channel); + + @override + void build() {} + + Future invoke(String name, + [Map args = const {}]) async { + final result = await _channel.invokeMethod(name, args); + await ref.read(nfcOverlay.notifier).waitForHide(); + return result; + } +} diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoActionDescription.kt b/lib/android/overlay/nfc/models.dart similarity index 59% rename from android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoActionDescription.kt rename to lib/android/overlay/nfc/models.dart index ae0d8945d..fe24afa85 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoActionDescription.kt +++ b/lib/android/overlay/nfc/models.dart @@ -14,21 +14,16 @@ * limitations under the License. */ -package com.yubico.authenticator.fido +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; -const val dialogDescriptionFidoIndex = 200 +part 'models.freezed.dart'; -enum class FidoActionDescription(private val value: Int) { - Reset(0), - Unlock(1), - SetPin(2), - DeleteCredential(3), - DeleteFingerprint(4), - RenameFingerprint(5), - RegisterFingerprint(6), - EnableEnterpriseAttestation(7), - ActionFailure(8); - - val id: Int - get() = value + dialogDescriptionFidoIndex -} \ No newline at end of file +@freezed +class NfcOverlayWidgetProperties with _$NfcOverlayWidgetProperties { + factory NfcOverlayWidgetProperties({ + required Widget child, + @Default(false) bool visible, + @Default(false) bool hasCloseButton, + }) = _NfcOverlayWidgetProperties; +} diff --git a/lib/android/overlay/nfc/models.freezed.dart b/lib/android/overlay/nfc/models.freezed.dart new file mode 100644 index 000000000..3216a0e47 --- /dev/null +++ b/lib/android/overlay/nfc/models.freezed.dart @@ -0,0 +1,189 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'models.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$NfcOverlayWidgetProperties { + Widget get child => throw _privateConstructorUsedError; + bool get visible => throw _privateConstructorUsedError; + bool get hasCloseButton => throw _privateConstructorUsedError; + + /// Create a copy of NfcOverlayWidgetProperties + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $NfcOverlayWidgetPropertiesCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NfcOverlayWidgetPropertiesCopyWith<$Res> { + factory $NfcOverlayWidgetPropertiesCopyWith(NfcOverlayWidgetProperties value, + $Res Function(NfcOverlayWidgetProperties) then) = + _$NfcOverlayWidgetPropertiesCopyWithImpl<$Res, + NfcOverlayWidgetProperties>; + @useResult + $Res call({Widget child, bool visible, bool hasCloseButton}); +} + +/// @nodoc +class _$NfcOverlayWidgetPropertiesCopyWithImpl<$Res, + $Val extends NfcOverlayWidgetProperties> + implements $NfcOverlayWidgetPropertiesCopyWith<$Res> { + _$NfcOverlayWidgetPropertiesCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of NfcOverlayWidgetProperties + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? child = null, + Object? visible = null, + Object? hasCloseButton = null, + }) { + return _then(_value.copyWith( + child: null == child + ? _value.child + : child // ignore: cast_nullable_to_non_nullable + as Widget, + visible: null == visible + ? _value.visible + : visible // ignore: cast_nullable_to_non_nullable + as bool, + hasCloseButton: null == hasCloseButton + ? _value.hasCloseButton + : hasCloseButton // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$NfcOverlayWidgetPropertiesImplCopyWith<$Res> + implements $NfcOverlayWidgetPropertiesCopyWith<$Res> { + factory _$$NfcOverlayWidgetPropertiesImplCopyWith( + _$NfcOverlayWidgetPropertiesImpl value, + $Res Function(_$NfcOverlayWidgetPropertiesImpl) then) = + __$$NfcOverlayWidgetPropertiesImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({Widget child, bool visible, bool hasCloseButton}); +} + +/// @nodoc +class __$$NfcOverlayWidgetPropertiesImplCopyWithImpl<$Res> + extends _$NfcOverlayWidgetPropertiesCopyWithImpl<$Res, + _$NfcOverlayWidgetPropertiesImpl> + implements _$$NfcOverlayWidgetPropertiesImplCopyWith<$Res> { + __$$NfcOverlayWidgetPropertiesImplCopyWithImpl( + _$NfcOverlayWidgetPropertiesImpl _value, + $Res Function(_$NfcOverlayWidgetPropertiesImpl) _then) + : super(_value, _then); + + /// Create a copy of NfcOverlayWidgetProperties + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? child = null, + Object? visible = null, + Object? hasCloseButton = null, + }) { + return _then(_$NfcOverlayWidgetPropertiesImpl( + child: null == child + ? _value.child + : child // ignore: cast_nullable_to_non_nullable + as Widget, + visible: null == visible + ? _value.visible + : visible // ignore: cast_nullable_to_non_nullable + as bool, + hasCloseButton: null == hasCloseButton + ? _value.hasCloseButton + : hasCloseButton // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$NfcOverlayWidgetPropertiesImpl implements _NfcOverlayWidgetProperties { + _$NfcOverlayWidgetPropertiesImpl( + {required this.child, this.visible = false, this.hasCloseButton = false}); + + @override + final Widget child; + @override + @JsonKey() + final bool visible; + @override + @JsonKey() + final bool hasCloseButton; + + @override + String toString() { + return 'NfcOverlayWidgetProperties(child: $child, visible: $visible, hasCloseButton: $hasCloseButton)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NfcOverlayWidgetPropertiesImpl && + (identical(other.child, child) || other.child == child) && + (identical(other.visible, visible) || other.visible == visible) && + (identical(other.hasCloseButton, hasCloseButton) || + other.hasCloseButton == hasCloseButton)); + } + + @override + int get hashCode => Object.hash(runtimeType, child, visible, hasCloseButton); + + /// Create a copy of NfcOverlayWidgetProperties + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$NfcOverlayWidgetPropertiesImplCopyWith<_$NfcOverlayWidgetPropertiesImpl> + get copyWith => __$$NfcOverlayWidgetPropertiesImplCopyWithImpl< + _$NfcOverlayWidgetPropertiesImpl>(this, _$identity); +} + +abstract class _NfcOverlayWidgetProperties + implements NfcOverlayWidgetProperties { + factory _NfcOverlayWidgetProperties( + {required final Widget child, + final bool visible, + final bool hasCloseButton}) = _$NfcOverlayWidgetPropertiesImpl; + + @override + Widget get child; + @override + bool get visible; + @override + bool get hasCloseButton; + + /// Create a copy of NfcOverlayWidgetProperties + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$NfcOverlayWidgetPropertiesImplCopyWith<_$NfcOverlayWidgetPropertiesImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/android/overlay/nfc/nfc_event_notifier.dart b/lib/android/overlay/nfc/nfc_event_notifier.dart new file mode 100644 index 000000000..ec2401705 --- /dev/null +++ b/lib/android/overlay/nfc/nfc_event_notifier.dart @@ -0,0 +1,123 @@ +/* + * Copyright (C) 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. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; + +import '../../../app/logging.dart'; +import '../../../app/state.dart'; +import 'nfc_overlay.dart'; +import 'views/nfc_overlay_widget.dart'; + +final _log = Logger('android.nfc_event_notifier'); + +class NfcEvent { + const NfcEvent(); +} + +class NfcHideViewEvent extends NfcEvent { + final Duration delay; + + const NfcHideViewEvent({this.delay = Duration.zero}); +} + +class NfcSetViewEvent extends NfcEvent { + final Widget child; + final bool showIfHidden; + + const NfcSetViewEvent({required this.child, this.showIfHidden = true}); +} + +final nfcEventNotifier = + NotifierProvider<_NfcEventNotifier, NfcEvent>(_NfcEventNotifier.new); + +class _NfcEventNotifier extends Notifier { + @override + NfcEvent build() { + return const NfcEvent(); + } + + void send(NfcEvent event) { + state = event; + } +} + +final nfcEventNotifierListener = Provider<_NfcEventNotifierListener>( + (ref) => _NfcEventNotifierListener(ref)); + +class _NfcEventNotifierListener { + final ProviderRef _ref; + ProviderSubscription? listener; + + _NfcEventNotifierListener(this._ref); + + void startListener(BuildContext context) { + listener?.close(); + listener = _ref.listen(nfcEventNotifier, (previous, action) { + _log.debug('Event change: $previous -> $action'); + switch (action) { + case (NfcSetViewEvent a): + if (!visible && a.showIfHidden) { + _show(context, a.child); + } else { + _ref + .read(nfcOverlayWidgetProperties.notifier) + .update(child: a.child); + } + break; + case (NfcHideViewEvent e): + _hide(context, e.delay); + break; + } + }); + } + + void _show(BuildContext context, Widget child) async { + final notifier = _ref.read(nfcOverlayWidgetProperties.notifier); + notifier.update(child: child); + if (!visible) { + visible = true; + final result = await showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return const NfcOverlayWidget(); + }); + if (result == null) { + // the modal sheet was cancelled by Back button, close button or dismiss + _ref.read(nfcOverlay.notifier).onCancel(); + } + visible = false; + } + } + + void _hide(BuildContext context, Duration timeout) { + Future.delayed(timeout, () { + _ref.read(withContextProvider)((context) async { + if (visible) { + Navigator.of(context).pop('HIDDEN'); + visible = false; + } + }); + }); + } + + bool get visible => + _ref.read(nfcOverlayWidgetProperties.select((s) => s.visible)); + + set visible(bool visible) => + _ref.read(nfcOverlayWidgetProperties.notifier).update(visible: visible); +} diff --git a/lib/android/overlay/nfc/nfc_overlay.dart b/lib/android/overlay/nfc/nfc_overlay.dart new file mode 100755 index 000000000..d0014ef6f --- /dev/null +++ b/lib/android/overlay/nfc/nfc_overlay.dart @@ -0,0 +1,165 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; + +import '../../../app/logging.dart'; +import '../../../app/state.dart'; +import '../../state.dart'; +import 'nfc_event_notifier.dart'; +import 'views/nfc_content_widget.dart'; +import 'views/nfc_overlay_icons.dart'; +import 'views/nfc_overlay_widget.dart'; + +final _log = Logger('android.nfc_overlay'); +const _channel = MethodChannel('com.yubico.authenticator.channel.nfc_overlay'); + +final nfcOverlay = + NotifierProvider<_NfcOverlayNotifier, int>(_NfcOverlayNotifier.new); + +class _NfcOverlayNotifier extends Notifier { + Timer? processingViewTimeout; + late final l10n = ref.read(l10nProvider); + late final eventNotifier = ref.read(nfcEventNotifier.notifier); + + @override + int build() { + ref.listen(androidNfcState, (previous, current) { + _log.debug('Received nfc state: $current'); + processingViewTimeout?.cancel(); + final notifier = ref.read(nfcEventNotifier.notifier); + + switch (current) { + case NfcState.ongoing: + // the "Hold still..." view will be shown after this timeout + // if the action is finished before, the timer might be cancelled + // causing the view not to be visible at all + const timeout = 300; + processingViewTimeout = + Timer(const Duration(milliseconds: timeout), () { + notifier.send(showHoldStill()); + }); + break; + case NfcState.success: + notifier.send(showDone()); + notifier + .send(const NfcHideViewEvent(delay: Duration(milliseconds: 400))); + break; + case NfcState.failure: + notifier.send(showFailed()); + notifier + .send(const NfcHideViewEvent(delay: Duration(milliseconds: 800))); + break; + case NfcState.disabled: + _log.debug('Received state: disabled'); + break; + case NfcState.idle: + _log.debug('Received state: idle'); + break; + } + }); + + _channel.setMethodCallHandler((call) async { + switch (call.method) { + case 'show': + eventNotifier.send(showTapYourYubiKey()); + break; + + case 'close': + hide(); + break; + + default: + throw PlatformException( + code: 'NotImplemented', + message: 'Method ${call.method} is not implemented', + ); + } + }); + return 0; + } + + NfcEvent showTapYourYubiKey() { + final nfcAvailable = ref.watch(androidNfcAdapterState); + ref.read(nfcOverlayWidgetProperties.notifier).update(hasCloseButton: true); + return NfcSetViewEvent( + child: NfcContentWidget( + title: l10n.s_nfc_ready_to_scan, + subtitle: nfcAvailable ? l10n.s_nfc_tap_your_yubikey : l10n.l_insert_yk, + icon: nfcAvailable ? const NfcIconProgressBar(false) : const UsbIcon(), + )); + } + + NfcEvent showHoldStill() { + ref.read(nfcOverlayWidgetProperties.notifier).update(hasCloseButton: false); + return NfcSetViewEvent( + child: NfcContentWidget( + title: l10n.s_nfc_ready_to_scan, + subtitle: l10n.s_nfc_hold_still, + icon: const NfcIconProgressBar(true), + )); + } + + NfcEvent showDone() { + ref.read(nfcOverlayWidgetProperties.notifier).update(hasCloseButton: false); + return NfcSetViewEvent( + child: NfcContentWidget( + title: l10n.s_nfc_ready_to_scan, + subtitle: l10n.s_done, + icon: const NfcIconSuccess(), + ), + showIfHidden: false); + } + + NfcEvent showFailed() { + ref.read(nfcOverlayWidgetProperties.notifier).update(hasCloseButton: false); + return NfcSetViewEvent( + child: NfcContentWidget( + title: l10n.s_nfc_ready_to_scan, + subtitle: l10n.l_nfc_failed_to_scan, + icon: const NfcIconFailure(), + ), + showIfHidden: false); + } + + void hide() { + ref.read(nfcEventNotifier.notifier).send(const NfcHideViewEvent()); + } + + void onCancel() async { + await _channel.invokeMethod('cancel'); + } + + Future waitForHide() async { + final completer = Completer(); + + Timer.periodic( + const Duration(milliseconds: 200), + (timer) { + if (ref.read(nfcOverlayWidgetProperties.select((s) => !s.visible))) { + timer.cancel(); + completer.complete(); + } + }, + ); + + await completer.future; + } +} diff --git a/lib/android/overlay/nfc/views/nfc_content_widget.dart b/lib/android/overlay/nfc/views/nfc_content_widget.dart new file mode 100644 index 000000000..fa784005a --- /dev/null +++ b/lib/android/overlay/nfc/views/nfc_content_widget.dart @@ -0,0 +1,55 @@ +/* + * Copyright (C) 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. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class NfcContentWidget extends ConsumerWidget { + final String title; + final String subtitle; + final Widget icon; + + const NfcContentWidget({ + super.key, + required this.title, + required this.subtitle, + required this.icon, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final colorScheme = theme.colorScheme; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + children: [ + Text(title, textAlign: TextAlign.center, style: textTheme.titleLarge), + const SizedBox(height: 8), + Text(subtitle, + textAlign: TextAlign.center, + style: textTheme.titleMedium!.copyWith( + color: colorScheme.onSurfaceVariant, + )), + const SizedBox(height: 32), + icon, + const SizedBox(height: 24) + ], + ), + ); + } +} diff --git a/lib/android/overlay/nfc/views/nfc_overlay_icons.dart b/lib/android/overlay/nfc/views/nfc_overlay_icons.dart new file mode 100644 index 000000000..80881ed1c --- /dev/null +++ b/lib/android/overlay/nfc/views/nfc_overlay_icons.dart @@ -0,0 +1,90 @@ +/* + * Copyright (C) 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. + */ + +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +class NfcIconProgressBar extends StatelessWidget { + final bool inProgress; + + const NfcIconProgressBar(this.inProgress, {super.key}); + + @override + Widget build(BuildContext context) => IconTheme( + data: IconThemeData( + size: 64, + color: Theme.of(context).colorScheme.primary, + ), + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + const Opacity( + opacity: 0.5, + child: Icon(Symbols.contactless), + ), + const ClipOval( + child: SizedBox( + width: 42, + height: 42, + child: OverflowBox( + maxWidth: double.infinity, + maxHeight: double.infinity, + child: Icon(Symbols.contactless), + ), + ), + ), + SizedBox( + width: 50, + height: 50, + child: CircularProgressIndicator(value: inProgress ? null : 1.0), + ), + ], + ), + ); +} + +class UsbIcon extends StatelessWidget { + const UsbIcon({super.key}); + + @override + Widget build(BuildContext context) => Icon( + Symbols.usb, + size: 64, + color: Theme.of(context).colorScheme.primary, + ); +} + +class NfcIconSuccess extends StatelessWidget { + const NfcIconSuccess({super.key}); + + @override + Widget build(BuildContext context) => Icon( + Symbols.check, + size: 64, + color: Theme.of(context).colorScheme.primary, + ); +} + +class NfcIconFailure extends StatelessWidget { + const NfcIconFailure({super.key}); + + @override + Widget build(BuildContext context) => Icon( + Symbols.close, + size: 64, + color: Theme.of(context).colorScheme.error, + ); +} diff --git a/lib/android/overlay/nfc/views/nfc_overlay_widget.dart b/lib/android/overlay/nfc/views/nfc_overlay_widget.dart new file mode 100644 index 000000000..db608ca0d --- /dev/null +++ b/lib/android/overlay/nfc/views/nfc_overlay_widget.dart @@ -0,0 +1,76 @@ +/* + * Copyright (C) 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. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +import '../models.dart'; + +final nfcOverlayWidgetProperties = + NotifierProvider<_NfcOverlayWidgetProperties, NfcOverlayWidgetProperties>( + _NfcOverlayWidgetProperties.new); + +class _NfcOverlayWidgetProperties extends Notifier { + @override + NfcOverlayWidgetProperties build() { + return NfcOverlayWidgetProperties(child: const SizedBox()); + } + + void update({ + Widget? child, + bool? visible, + bool? hasCloseButton, + }) { + state = state.copyWith( + child: child ?? state.child, + visible: visible ?? state.visible, + hasCloseButton: hasCloseButton ?? state.hasCloseButton); + } +} + +class NfcOverlayWidget extends ConsumerWidget { + const NfcOverlayWidget({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final widget = ref.watch(nfcOverlayWidgetProperties.select((s) => s.child)); + final showCloseButton = + ref.watch(nfcOverlayWidgetProperties.select((s) => s.hasCloseButton)); + return Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Stack(fit: StackFit.passthrough, children: [ + if (showCloseButton) + Positioned( + top: 10, + right: 10, + child: IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Symbols.close, fill: 1, size: 24)), + ), + Padding( + padding: const EdgeInsets.fromLTRB(0, 50, 0, 0), + child: widget, + ) + ]), + const SizedBox(height: 32), + ], + ); + } +} diff --git a/lib/android/state.dart b/lib/android/state.dart index 551cd3add..4ccc15bd3 100644 --- a/lib/android/state.dart +++ b/lib/android/state.dart @@ -69,22 +69,50 @@ class _AndroidClipboard extends AppClipboard { } } -class NfcStateNotifier extends StateNotifier { - NfcStateNotifier() : super(false); +class NfcAdapterState extends StateNotifier { + NfcAdapterState() : super(false); - void setNfcEnabled(bool value) { + void enable(bool value) { state = value; } } +enum NfcState { + disabled, + idle, + ongoing, + success, + failure, +} + +class NfcStateNotifier extends StateNotifier { + NfcStateNotifier() : super(NfcState.disabled); + + void set(int stateValue) { + var newState = switch (stateValue) { + 0 => NfcState.disabled, + 1 => NfcState.idle, + 2 => NfcState.ongoing, + 3 => NfcState.success, + 4 => NfcState.failure, + _ => NfcState.disabled + }; + + state = newState; + } +} + final androidSectionPriority = Provider>((ref) => []); final androidSdkVersionProvider = Provider((ref) => -1); final androidNfcSupportProvider = Provider((ref) => false); -final androidNfcStateProvider = - StateNotifierProvider((ref) => NfcStateNotifier()); +final androidNfcAdapterState = + StateNotifierProvider((ref) => NfcAdapterState()); + +final androidNfcState = StateNotifierProvider( + (ref) => NfcStateNotifier()); final androidSupportedThemesProvider = StateProvider>((ref) { if (ref.read(androidSdkVersionProvider) < 29) { @@ -191,6 +219,7 @@ class NfcTapActionNotifier extends StateNotifier { static const _prefNfcOpenApp = 'prefNfcOpenApp'; static const _prefNfcCopyOtp = 'prefNfcCopyOtp'; final SharedPreferences _prefs; + NfcTapActionNotifier._(this._prefs, super._state); factory NfcTapActionNotifier(SharedPreferences prefs) { @@ -232,6 +261,7 @@ class NfcKbdLayoutNotifier extends StateNotifier { static const String _defaultClipKbdLayout = 'US'; static const _prefClipKbdLayout = 'prefClipKbdLayout'; final SharedPreferences _prefs; + NfcKbdLayoutNotifier(this._prefs) : super(_prefs.getString(_prefClipKbdLayout) ?? _defaultClipKbdLayout); @@ -250,6 +280,7 @@ final androidNfcBypassTouchProvider = class NfcBypassTouchNotifier extends StateNotifier { static const _prefNfcBypassTouch = 'prefNfcBypassTouch'; final SharedPreferences _prefs; + NfcBypassTouchNotifier(this._prefs) : super(_prefs.getBool(_prefNfcBypassTouch) ?? false); @@ -268,6 +299,7 @@ final androidNfcSilenceSoundsProvider = class NfcSilenceSoundsNotifier extends StateNotifier { static const _prefNfcSilenceSounds = 'prefNfcSilenceSounds'; final SharedPreferences _prefs; + NfcSilenceSoundsNotifier(this._prefs) : super(_prefs.getBool(_prefNfcSilenceSounds) ?? false); @@ -286,6 +318,7 @@ final androidUsbLaunchAppProvider = class UsbLaunchAppNotifier extends StateNotifier { static const _prefUsbOpenApp = 'prefUsbOpenApp'; final SharedPreferences _prefs; + UsbLaunchAppNotifier(this._prefs) : super(_prefs.getBool(_prefUsbOpenApp) ?? false); diff --git a/lib/android/tap_request_dialog.dart b/lib/android/tap_request_dialog.dart deleted file mode 100755 index 8073525c8..000000000 --- a/lib/android/tap_request_dialog.dart +++ /dev/null @@ -1,231 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.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 '../app/state.dart'; -import '../app/views/user_interaction.dart'; - -const _channel = MethodChannel('com.yubico.authenticator.channel.dialog'); - -// _DIcon identifies the icon which should be displayed on the dialog -enum _DIcon { - nfcIcon, - successIcon, - failureIcon, - invalid; - - static _DIcon fromId(int? id) => - const { - 0: _DIcon.nfcIcon, - 1: _DIcon.successIcon, - 2: _DIcon.failureIcon - }[id] ?? - _DIcon.invalid; -} - -// _DDesc contains id of title resource for the dialog -enum _DTitle { - tapKey, - operationSuccessful, - operationFailed, - invalid; - - static _DTitle fromId(int? id) => - const { - 0: _DTitle.tapKey, - 1: _DTitle.operationSuccessful, - 2: _DTitle.operationFailed - }[id] ?? - _DTitle.invalid; -} - -// _DDesc contains action description in the dialog -enum _DDesc { - // oath descriptions - oathResetApplet, - oathUnlockSession, - oathSetPassword, - oathUnsetPassword, - oathAddAccount, - oathRenameAccount, - oathDeleteAccount, - oathCalculateCode, - oathActionFailure, - oathAddMultipleAccounts, - // FIDO descriptions - fidoResetApplet, - fidoUnlockSession, - fidoSetPin, - fidoDeleteCredential, - fidoDeleteFingerprint, - fidoRenameFingerprint, - fidoRegisterFingerprint, - fidoEnableEnterpriseAttestation, - fidoActionFailure, - // Others - invalid; - - static const int dialogDescriptionOathIndex = 100; - static const int dialogDescriptionFidoIndex = 200; - - static _DDesc fromId(int? id) => - const { - dialogDescriptionOathIndex + 0: oathResetApplet, - dialogDescriptionOathIndex + 1: oathUnlockSession, - dialogDescriptionOathIndex + 2: oathSetPassword, - dialogDescriptionOathIndex + 3: oathUnsetPassword, - dialogDescriptionOathIndex + 4: oathAddAccount, - dialogDescriptionOathIndex + 5: oathRenameAccount, - dialogDescriptionOathIndex + 6: oathDeleteAccount, - dialogDescriptionOathIndex + 7: oathCalculateCode, - dialogDescriptionOathIndex + 8: oathActionFailure, - dialogDescriptionOathIndex + 9: oathAddMultipleAccounts, - dialogDescriptionFidoIndex + 0: fidoResetApplet, - dialogDescriptionFidoIndex + 1: fidoUnlockSession, - dialogDescriptionFidoIndex + 2: fidoSetPin, - dialogDescriptionFidoIndex + 3: fidoDeleteCredential, - dialogDescriptionFidoIndex + 4: fidoDeleteFingerprint, - dialogDescriptionFidoIndex + 5: fidoRenameFingerprint, - dialogDescriptionFidoIndex + 6: fidoRegisterFingerprint, - dialogDescriptionFidoIndex + 7: fidoEnableEnterpriseAttestation, - dialogDescriptionFidoIndex + 8: fidoActionFailure, - }[id] ?? - _DDesc.invalid; -} - -final androidDialogProvider = Provider<_DialogProvider>( - (ref) { - return _DialogProvider(ref.watch(withContextProvider)); - }, -); - -class _DialogProvider { - final WithContext _withContext; - UserInteractionController? _controller; - - _DialogProvider(this._withContext) { - _channel.setMethodCallHandler((call) async { - final args = jsonDecode(call.arguments); - switch (call.method) { - case 'close': - closeDialog(); - break; - case 'show': - await _showDialog(args['title'], args['description'], args['icon']); - break; - case 'state': - await _updateDialogState( - args['title'], args['description'], args['icon']); - break; - default: - throw PlatformException( - code: 'NotImplemented', - message: 'Method ${call.method} is not implemented', - ); - } - }); - } - - void closeDialog() { - _controller?.close(); - _controller = null; - } - - Widget? _getIcon(int? icon) => switch (_DIcon.fromId(icon)) { - _DIcon.nfcIcon => const Icon(Symbols.contactless), - _DIcon.successIcon => const Icon(Symbols.check_circle), - _DIcon.failureIcon => const Icon(Symbols.error), - _ => null, - }; - - String _getTitle(BuildContext context, int? titleId) { - final l10n = AppLocalizations.of(context)!; - return switch (_DTitle.fromId(titleId)) { - _DTitle.tapKey => l10n.l_nfc_dialog_tap_key, - _DTitle.operationSuccessful => l10n.s_nfc_dialog_operation_success, - _DTitle.operationFailed => l10n.s_nfc_dialog_operation_failed, - _ => '' - }; - } - - String _getDialogDescription(BuildContext context, int? descriptionId) { - final l10n = AppLocalizations.of(context)!; - return switch (_DDesc.fromId(descriptionId)) { - _DDesc.oathResetApplet => l10n.s_nfc_dialog_oath_reset, - _DDesc.oathUnlockSession => l10n.s_nfc_dialog_oath_unlock, - _DDesc.oathSetPassword => l10n.s_nfc_dialog_oath_set_password, - _DDesc.oathUnsetPassword => l10n.s_nfc_dialog_oath_unset_password, - _DDesc.oathAddAccount => l10n.s_nfc_dialog_oath_add_account, - _DDesc.oathRenameAccount => l10n.s_nfc_dialog_oath_rename_account, - _DDesc.oathDeleteAccount => l10n.s_nfc_dialog_oath_delete_account, - _DDesc.oathCalculateCode => l10n.s_nfc_dialog_oath_calculate_code, - _DDesc.oathActionFailure => l10n.s_nfc_dialog_oath_failure, - _DDesc.oathAddMultipleAccounts => - l10n.s_nfc_dialog_oath_add_multiple_accounts, - _DDesc.fidoResetApplet => l10n.s_nfc_dialog_fido_reset, - _DDesc.fidoUnlockSession => l10n.s_nfc_dialog_fido_unlock, - _DDesc.fidoSetPin => l10n.l_nfc_dialog_fido_set_pin, - _DDesc.fidoDeleteCredential => l10n.s_nfc_dialog_fido_delete_credential, - _DDesc.fidoDeleteFingerprint => l10n.s_nfc_dialog_fido_delete_fingerprint, - _DDesc.fidoRenameFingerprint => l10n.s_nfc_dialog_fido_rename_fingerprint, - _DDesc.fidoActionFailure => l10n.s_nfc_dialog_fido_failure, - _ => '' - }; - } - - Future _updateDialogState( - int? title, int? description, int? dialogIcon) async { - final icon = _getIcon(dialogIcon); - await _withContext((context) async { - _controller?.updateContent( - title: _getTitle(context, title), - description: _getDialogDescription(context, description), - icon: icon != null - ? IconTheme( - data: IconTheme.of(context).copyWith(size: 64), - child: icon, - ) - : null, - ); - }); - } - - Future _showDialog(int title, int description, int? dialogIcon) async { - final icon = _getIcon(dialogIcon); - _controller = await _withContext((context) async => promptUserInteraction( - context, - title: _getTitle(context, title), - description: _getDialogDescription(context, description), - icon: icon != null - ? IconTheme( - data: IconTheme.of(context).copyWith(size: 64), - child: icon, - ) - : null, - onCancel: () { - _channel.invokeMethod('cancel'); - }, - )); - } -} diff --git a/lib/android/window_state_provider.dart b/lib/android/window_state_provider.dart index 627880288..68353509d 100644 --- a/lib/android/window_state_provider.dart +++ b/lib/android/window_state_provider.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. @@ -58,7 +58,7 @@ class _WindowStateNotifier extends StateNotifier if (lifeCycleState == AppLifecycleState.resumed) { _log.debug('Reading nfc enabled value'); isNfcEnabled().then((value) => - _ref.read(androidNfcStateProvider.notifier).setNfcEnabled(value)); + _ref.read(androidNfcAdapterState.notifier).enable(value)); } } else { _log.debug('Ignoring appLifecycleStateChange'); diff --git a/lib/app/views/device_picker.dart b/lib/app/views/device_picker.dart index 126c90da3..a1046f6a1 100644 --- a/lib/app/views/device_picker.dart +++ b/lib/app/views/device_picker.dart @@ -71,7 +71,7 @@ class DevicePickerContent extends ConsumerWidget { Widget? androidNoKeyWidget; if (isAndroid && devices.isEmpty) { var hasNfcSupport = ref.watch(androidNfcSupportProvider); - var isNfcEnabled = ref.watch(androidNfcStateProvider); + var isNfcEnabled = ref.watch(androidNfcAdapterState); final subtitle = hasNfcSupport && isNfcEnabled ? l10n.l_insert_or_tap_yk : l10n.l_insert_yk; diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index 63afad570..f3473cdc4 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -27,16 +27,13 @@ import '../../fido/views/passkeys_screen.dart'; import '../../fido/views/webauthn_page.dart'; import '../../home/views/home_message_page.dart'; import '../../home/views/home_screen.dart'; -import '../../management/views/management_screen.dart'; import '../../oath/views/oath_screen.dart'; import '../../oath/views/utils.dart'; import '../../otp/views/otp_screen.dart'; import '../../piv/views/piv_screen.dart'; -import '../message.dart'; import '../models.dart'; import '../state.dart'; import 'device_error_screen.dart'; -import 'message_page.dart'; class MainPage extends ConsumerWidget { const MainPage({super.key}); @@ -52,12 +49,22 @@ class MainPage extends ConsumerWidget { ); if (isAndroid) { - isNfcEnabled().then((value) => - ref.read(androidNfcStateProvider.notifier).setNfcEnabled(value)); + isNfcEnabled().then( + (value) => ref.read(androidNfcAdapterState.notifier).enable(value)); } // If the current device changes, we need to pop any open dialogs. - ref.listen>(currentDeviceDataProvider, (_, __) { + ref.listen>(currentDeviceDataProvider, + (prev, next) { + final serial = next.hasValue == true ? next.value?.info.serial : null; + final prevSerial = + prev?.hasValue == true ? prev?.value?.info.serial : null; + if ((serial != null && serial == prevSerial) || + (next.hasValue && (prev != null && prev.isLoading)) || + next.isLoading) { + return; + } + Navigator.of(context).popUntil((route) { return route.isFirst || [ @@ -69,7 +76,6 @@ class MainPage extends ConsumerWidget { 'oath_add_account', 'oath_icon_pack_dialog', 'android_qr_scanner_view', - 'android_alert_dialog' ].contains(route.settings.name); }); }); @@ -84,7 +90,7 @@ class MainPage extends ConsumerWidget { if (deviceNode == null) { if (isAndroid) { var hasNfcSupport = ref.watch(androidNfcSupportProvider); - var isNfcEnabled = ref.watch(androidNfcStateProvider); + var isNfcEnabled = ref.watch(androidNfcAdapterState); return HomeMessagePage( centered: true, graphic: noKeyImage, @@ -103,6 +109,10 @@ class MainPage extends ConsumerWidget { label: Text(l10n.s_add_account), icon: const Icon(Symbols.person_add_alt), onPressed: () async { + // make sure we execute the "Add account" in OATH section + ref + .read(currentSectionProvider.notifier) + .setCurrentSection(Section.accounts); await addOathAccount(context, ref); }) ], @@ -119,41 +129,6 @@ class MainPage extends ConsumerWidget { return ref.watch(currentDeviceDataProvider).when( data: (data) { final section = ref.watch(currentSectionProvider); - final capabilities = section.capabilities; - if (section.getAvailability(data) == Availability.unsupported) { - return MessagePage( - title: section.getDisplayName(l10n), - capabilities: capabilities, - header: l10n.s_app_not_supported, - message: l10n.l_app_not_supported_on_yk(capabilities - .map((c) => c.getDisplayName(l10n)) - .join(',')), - ); - } else if (section.getAvailability(data) != - Availability.enabled) { - return MessagePage( - title: section.getDisplayName(l10n), - capabilities: capabilities, - header: l10n.s_app_disabled, - message: l10n.l_app_disabled_desc(capabilities - .map((c) => c.getDisplayName(l10n)) - .join(',')), - actionsBuilder: (context, expanded) => [ - ActionChip( - label: Text(data.info.version.major > 4 - ? l10n.s_toggle_applications - : l10n.s_toggle_interfaces), - onPressed: () async { - await showBlurDialog( - context: context, - builder: (context) => ManagementScreen(data), - ); - }, - avatar: const Icon(Symbols.construction), - ) - ], - ); - } return switch (section) { Section.home => HomeScreen(data), diff --git a/lib/app/views/message_page_not_initialized.dart b/lib/app/views/message_page_not_initialized.dart index 229ed436d..df72e53ad 100644 --- a/lib/app/views/message_page_not_initialized.dart +++ b/lib/app/views/message_page_not_initialized.dart @@ -46,7 +46,7 @@ class MessagePageNotInitialized extends ConsumerWidget { if (isAndroid) { var hasNfcSupport = ref.watch(androidNfcSupportProvider); - var isNfcEnabled = ref.watch(androidNfcStateProvider); + var isNfcEnabled = ref.watch(androidNfcAdapterState); var isUsbYubiKey = ref.watch(attachedDevicesProvider).firstOrNull?.transport == Transport.usb; diff --git a/lib/app/views/reset_dialog.dart b/lib/app/views/reset_dialog.dart index 52c3d1ad4..a318f838c 100644 --- a/lib/app/views/reset_dialog.dart +++ b/lib/app/views/reset_dialog.dart @@ -123,6 +123,12 @@ class _ResetDialogState extends ConsumerState { _application == Capability.fido2 && !ref.watch(rpcStateProvider.select((state) => state.isAdmin)); + // show the progress widgets on desktop, or on Android when using USB + final showResetProgress = _resetting && + (!Platform.isAndroid || + ref.read(currentDeviceProvider)?.transport == Transport.usb || + _currentStep == _totalSteps); + return ResponsiveDialog( title: Text(l10n.s_factory_reset), key: factoryResetCancel, @@ -322,7 +328,7 @@ class _ResetDialogState extends ConsumerState { }, ), ], - if (_resetting) + if (showResetProgress) if (_application == Capability.fido2 && _currentStep >= 0) ...[ Text('${l10n.s_status}: ${_getMessage()}'), LinearProgressIndicator(value: progress), diff --git a/lib/desktop/oath/state.dart b/lib/desktop/oath/state.dart index cdd133f7f..f15dedf5b 100755 --- a/lib/desktop/oath/state.dart +++ b/lib/desktop/oath/state.dart @@ -28,6 +28,7 @@ import '../../app/logging.dart'; import '../../app/models.dart'; import '../../app/state.dart'; import '../../app/views/user_interaction.dart'; +import '../../management/models.dart'; import '../../oath/models.dart'; import '../../oath/state.dart'; import '../rpc.dart'; @@ -37,8 +38,16 @@ final _log = Logger('desktop.oath.state'); final _sessionProvider = Provider.autoDispose.family( - (ref, devicePath) => RpcNodeSession( - ref.watch(rpcProvider).requireValue, devicePath, ['ccid', 'oath']), + (ref, devicePath) { + // Reset state if the OATH capability is toggled. + ref.watch(currentDeviceDataProvider.select((value) => + (value.valueOrNull?.info.config + .enabledCapabilities[value.valueOrNull?.node.transport] ?? + 0) & + Capability.oath.value)); + return RpcNodeSession( + ref.watch(rpcProvider).requireValue, devicePath, ['ccid', 'oath']); + }, ); // This remembers the key for all devices for the duration of the process. @@ -196,8 +205,11 @@ final desktopOathCredentialListProvider = StateNotifierProvider.autoDispose .select((r) => r.whenOrNull(data: (state) => state.locked) ?? true)), ); ref.listen(windowStateProvider, (_, windowState) { - notifier._notifyWindowState(windowState); + notifier._rescheduleTimer(windowState.active); }, fireImmediately: true); + ref.listen(currentSectionProvider, (_, section) { + notifier._rescheduleTimer(section == Section.accounts); + }); return notifier; }, @@ -231,9 +243,9 @@ class DesktopCredentialListNotifier extends OathCredentialListNotifier { DesktopCredentialListNotifier(this._withContext, this._session, this._locked) : super(); - void _notifyWindowState(WindowState windowState) { + void _rescheduleTimer(bool active) { if (_locked) return; - if (windowState.active) { + if (active) { _scheduleRefresh(); } else { _timer?.cancel(); diff --git a/lib/desktop/state.dart b/lib/desktop/state.dart index a0de9c641..3a383b7aa 100755 --- a/lib/desktop/state.dart +++ b/lib/desktop/state.dart @@ -261,15 +261,10 @@ class DesktopCurrentSectionNotifier extends CurrentSectionNotifier { state.getAvailability(data) != Availability.enabled) { state = Section.passkeys; } - if (state.getAvailability(data) != Availability.unsupported) { - // Keep current app - return; + if (state.getAvailability(data) != Availability.enabled) { + // Default to home if app is not enabled + state = Section.home; } - - state = _supportedSections.firstWhere( - (app) => app.getAvailability(data) == Availability.enabled, - orElse: () => _supportedSections.first, - ); } static Section _fromName(String? name, List
supportedSections) => diff --git a/lib/fido/views/delete_credential_dialog.dart b/lib/fido/views/delete_credential_dialog.dart index 0a07dc7b4..1a7f49dfa 100755 --- a/lib/fido/views/delete_credential_dialog.dart +++ b/lib/fido/views/delete_credential_dialog.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. @@ -23,6 +23,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; +import '../../exception/cancellation_exception.dart'; import '../../widgets/responsive_dialog.dart'; import '../models.dart'; import '../state.dart'; @@ -57,15 +58,19 @@ class DeleteCredentialDialog extends ConsumerWidget { actions: [ TextButton( onPressed: () async { - await ref - .read(credentialProvider(devicePath).notifier) - .deleteCredential(credential); - await ref.read(withContextProvider)( - (context) async { - Navigator.of(context).pop(true); - showMessage(context, l10n.s_passkey_deleted); - }, - ); + try { + await ref + .read(credentialProvider(devicePath).notifier) + .deleteCredential(credential); + await ref.read(withContextProvider)( + (context) async { + Navigator.of(context).pop(true); + showMessage(context, l10n.s_passkey_deleted); + }, + ); + } on CancellationException catch (_) { + // ignored + } }, child: Text(l10n.s_delete), ), diff --git a/lib/fido/views/pin_dialog.dart b/lib/fido/views/pin_dialog.dart index bb1db2688..fdda7bde7 100755 --- a/lib/fido/views/pin_dialog.dart +++ b/lib/fido/views/pin_dialog.dart @@ -280,6 +280,10 @@ class _FidoPinDialogState extends ConsumerState { } void _submit() async { + _currentPinFocus.unfocus(); + _newPinFocus.unfocus(); + _confirmPinFocus.unfocus(); + final l10n = AppLocalizations.of(context)!; final oldPin = _currentPinController.text.isNotEmpty ? _currentPinController.text diff --git a/lib/fido/views/pin_entry_form.dart b/lib/fido/views/pin_entry_form.dart index 371e077c6..2e7acdea8 100644 --- a/lib/fido/views/pin_entry_form.dart +++ b/lib/fido/views/pin_entry_form.dart @@ -30,6 +30,7 @@ import '../state.dart'; class PinEntryForm extends ConsumerStatefulWidget { final FidoState _state; final DeviceNode _deviceNode; + const PinEntryForm(this._state, this._deviceNode, {super.key}); @override @@ -58,6 +59,8 @@ class _PinEntryFormState extends ConsumerState { } void _submit() async { + _pinFocus.unfocus(); + setState(() { _pinIsWrong = false; _isObscure = true; diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 0441f018b..1e6bbce21 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -38,7 +38,7 @@ "s_calculate": "Berechnen", "s_import": "Importieren", "s_overwrite": "Überschreiben", - "s_done": null, + "s_done": "Erledigt", "s_label": "Beschriftung", "s_name": "Name", "s_usb": "USB", @@ -47,11 +47,11 @@ "s_details": "Details", "s_show_window": "Fenster anzeigen", "s_hide_window": "Fenster verstecken", - "s_show_navigation": null, + "s_show_navigation": "Navigation anzeigen", "s_expand_navigation": "Navigation erweitern", "s_collapse_navigation": "Navigation einklappen", - "s_show_menu": null, - "s_hide_menu": null, + "s_show_menu": "Menü anzeigen", + "s_hide_menu": "Menü ausblenden", "q_rename_target": "{label} umbenennen?", "@q_rename_target": { "placeholders": { @@ -203,7 +203,6 @@ "app": {} } }, - "s_app_disabled": "Anwendung deaktiviert", "l_app_disabled_desc": "Aktivieren Sie die Anwendung '{app}' auf Ihrem YubiKey für Zugriff", "@l_app_disabled_desc": { "placeholders": { @@ -230,15 +229,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_restricted_nfc": "NFC-Aktivierung", + "l_deactivate_restricted_nfc": "So aktivieren Sie NFC", + "p_deactivate_restricted_nfc_desc": "Verbinden Sie Ihren YubiKey für mindestens 3 Sekunden mit einer USB-Stromquelle, z. B. einem Computer.\n\nSobald er mit Strom versorgt wird, wird NFC aktiviert und ist einsatzbereit.", + "p_deactivate_restricted_nfc_footer": "Ihr YubiKey ist mit Restricted NFC ausgestattet, einer Funktion zum Schutz vor drahtloser Manipulation während des Transports. Dies bedeutet, dass die NFC-Funktionen vorübergehend deaktiviert sind, bis Sie sie aktivieren.", "s_unsupported_yk": "Nicht unterstützter YubiKey", "s_yk_not_recognized": "Geräte nicht erkannt", "p_operation_failed_try_again": "Die Aktion ist fehlgeschlagen, bitte versuchen Sie es erneut.", - "l_configuration_unsupported": null, - "p_scp_unsupported": null, + "l_configuration_unsupported": "Konfiguration nicht unterstützt", + "p_scp_unsupported": "Um über NFC zu kommunizieren, benötigt der YubiKey eine Technologie, die von diesem Telefon nicht unterstützt wird. Bitte schließen Sie den YubiKey an den USB-Anschluss des Telefons an.", "@_general_errors": {}, "l_error_occurred": "Es ist ein Fehler aufgetreten", @@ -325,7 +324,7 @@ "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, + "p_enable_ep_attestation_disable_with_factory_reset": "Einmal aktiviert, kann Enterprise Attestation nur durch einen FIDO-Werksreset deaktiviert werden.", "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", @@ -430,10 +429,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_add_account_password_required": "Passwort erforderlich", + "l_add_account_unlock_required": "Freischaltung erforderlich", + "l_add_account_already_exists": "Konto existiert bereits", + "l_add_account_func_missing": "Funktionalitäten fehlen oder sind deaktiviert", "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": "Dieses Konto existiert bereits auf dem YubiKey", @@ -447,7 +446,7 @@ "s_rename_account": "Konto umbenennen", "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": "Umbenennung des Kontos fehlgeschlagen: {message}", "@l_rename_account_failed": { "placeholders": { "message": {} @@ -517,7 +516,7 @@ } }, "@_fingerprints": {}, - "s_biometrics": null, + "s_biometrics": "Biometrische Daten", "l_fingerprint": "Fingerabdruck: {label}", "@l_fingerprint": { "placeholders": { @@ -600,7 +599,7 @@ "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, + "l_change_defaults": "Standard-Zugangscodes ändern", "s_issuer": "Aussteller", "s_serial": "Serial", "s_certificate_fingerprint": "Fingerprint", @@ -676,10 +675,10 @@ "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, + "s_allow_fingerprint": "Fingerabdruck zulassen", "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, + "p_cert_options_bio_desc": "Zu verwendender Schlüsselalgorithmus, Ausgabeformat, Ablaufdatum (nur bei Zertifikaten) und ob biometrische Daten anstelle der PIN verwendet werden können.", + "p_key_options_bio_desc": "Erlauben Sie die Verwendung biometrischer Daten anstelle der PIN.", "s_overwrite_slot": "Slot überschreiben", "p_overwrite_slot_desc": "Damit wird vorhandener Inhalt im Slot {slot} dauerhaft überschrieben.", "@p_overwrite_slot_desc": { @@ -822,7 +821,7 @@ "s_reset": "Zurücksetzen", "s_factory_reset": "Werkseinstellungen", "l_factory_reset_desc": "YubiKey-Standardeinstellungen wiederherstellen", - "l_factory_reset_required": null, + "l_factory_reset_required": "Werksreset erforderlich", "l_oath_application_reset": "OATH Anwendung zurücksetzen", "l_fido_app_reset": "FIDO Anwendung zurückgesetzt", "l_reset_failed": "Fehler beim Zurücksetzen: {message}", @@ -899,34 +898,11 @@ "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": "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, + "s_nfc_ready_to_scan": "Bereit zum Scannen", + "s_nfc_hold_still": "Stillhalten\u2026", + "s_nfc_tap_your_yubikey": "Tippen Sie auf Ihren YubiKey", + "l_nfc_failed_to_scan": "Scanvorgang fehlgeschlagen, erneut versuchen", "@_ndef": {}, "p_ndef_set_otp": "OTP-Code wurde erfolgreich von Ihrem YubiKey in die Zwischenablage kopiert.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c3d4b7e27..e403ea671 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -203,7 +203,6 @@ "app": {} } }, - "s_app_disabled": "Application disabled", "l_app_disabled_desc": "Enable the '{app}' application on your YubiKey to access", "@l_app_disabled_desc": { "placeholders": { @@ -899,29 +898,6 @@ "l_launch_app_on_usb_off": "Other apps can use the YubiKey over USB", "s_allow_screenshots": "Allow screenshots", - "l_nfc_dialog_tap_key": "Tap and hold your key", - "s_nfc_dialog_operation_success": "Success", - "s_nfc_dialog_operation_failed": "Failed", - - "s_nfc_dialog_oath_reset": "Action: reset OATH application", - "s_nfc_dialog_oath_unlock": "Action: unlock OATH application", - "s_nfc_dialog_oath_set_password": "Action: set OATH password", - "s_nfc_dialog_oath_unset_password": "Action: remove OATH password", - "s_nfc_dialog_oath_add_account": "Action: add new account", - "s_nfc_dialog_oath_rename_account": "Action: rename account", - "s_nfc_dialog_oath_delete_account": "Action: delete account", - "s_nfc_dialog_oath_calculate_code": "Action: calculate OATH code", - "s_nfc_dialog_oath_failure": "OATH operation failed", - "s_nfc_dialog_oath_add_multiple_accounts": "Action: add multiple accounts", - - "s_nfc_dialog_fido_reset": "Action: reset FIDO application", - "s_nfc_dialog_fido_unlock": "Action: unlock FIDO application", - "l_nfc_dialog_fido_set_pin": "Action: set or change the FIDO PIN", - "s_nfc_dialog_fido_delete_credential": "Action: delete Passkey", - "s_nfc_dialog_fido_delete_fingerprint": "Action: delete fingerprint", - "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", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 52b271dec..d1f0300c3 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -30,15 +30,15 @@ "s_delete": "Supprimer", "s_move": "Déplacer", "s_quit": "Quitter", - "s_enable": null, - "s_enabled": null, - "s_disabled": null, + "s_enable": "Activer", + "s_enabled": "Activé", + "s_disabled": "Handicapés", "s_status": "État", "s_unlock": "Déverrouiller", "s_calculate": "Calculer", "s_import": "Importer", "s_overwrite": "Écraser", - "s_done": null, + "s_done": "Terminé", "s_label": "Étiquette", "s_name": "Nom", "s_usb": "USB", @@ -47,11 +47,11 @@ "s_details": "Détails", "s_show_window": "Montrer fenêtre", "s_hide_window": "Masquer fenêtre", - "s_show_navigation": null, + "s_show_navigation": "Afficher la navigation", "s_expand_navigation": "Développer la navigation", "s_collapse_navigation": "Réduire la navigation", - "s_show_menu": null, - "s_hide_menu": null, + "s_show_menu": "Afficher le menu", + "s_hide_menu": "Cacher le menu", "q_rename_target": "Renommer {label}\u00a0?", "@q_rename_target": { "placeholders": { @@ -128,10 +128,10 @@ "s_dark_mode": "Thème sombre", "@_layout": {}, - "s_list_layout": null, - "s_grid_layout": null, - "s_mixed_layout": null, - "s_select_layout": null, + "s_list_layout": "Présentation de la liste", + "s_grid_layout": "Disposition de la grille", + "s_mixed_layout": "Disposition mixte", + "s_select_layout": "Sélectionner la mise en page", "@_yubikey_selection": {}, "s_select_to_scan": "Sélectionner pour scanner", @@ -161,8 +161,8 @@ } }, "l_firmware_version": "Version du firmware : {version}", - "l_fips_capable": null, - "l_fips_approved": null, + "l_fips_capable": "Compatible FIPS", + "l_fips_approved": "Approuvé par le FIPS", "@_yubikey_interactions": {}, "l_insert_yk": "Insérez votre YubiKey", @@ -203,7 +203,6 @@ "app": {} } }, - "s_app_disabled": "Application désactivée", "l_app_disabled_desc": "Activez l'application {app} sur votre YubiKey pour y accéder", "@l_app_disabled_desc": { "placeholders": { @@ -230,15 +229,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_restricted_nfc": "Activation NFC", + "l_deactivate_restricted_nfc": "Comment activer la NFC", + "p_deactivate_restricted_nfc_desc": "Connectez votre YubiKey à une source d'alimentation USB, telle qu'un ordinateur, pendant au moins 3 secondes.\n\nUne fois alimentée, la NFC sera activée et prête à l'emploi.", + "p_deactivate_restricted_nfc_footer": "Votre YubiKey est équipé d'une fonction NFC restreinte, conçue pour éviter les manipulations sans fil pendant le transport. Cela signifie que les opérations NFC sont temporairement désactivées jusqu'à ce que vous les activiez.", "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, + "l_configuration_unsupported": "La configuration n'est pas prise en charge", + "p_scp_unsupported": "Pour communiquer par NFC, le YubiKey nécessite une technologie qui n'est pas prise en charge par ce téléphone. Veuillez brancher la YubiKey sur le port USB du téléphone.", "@_general_errors": {}, "l_error_occurred": "Une erreur s'est produite", @@ -298,21 +297,21 @@ "l_enter_fido2_pin": "Saisissez le PIN FIDO2 de votre YubiKey", "l_pin_blocked_reset": "PIN bloqué, réinitialisez FIDO aux paramètres d'usine", "l_pin_blocked": "Le code PIN est bloqué", - "l_set_pin_first": "Un PIN est d'abord requis", - "l_unlock_pin_first": "Débloquer d'abord avec PIN", + "l_set_pin_first": "Un code PIN est nécessaire", + "l_unlock_pin_first": "Déverrouiller avec un code PIN", "l_pin_soft_locked": "PIN bloqué jusqu'à ce que la YubiKey soit retirée et réinsérée", "l_pin_change_required_desc": "Vous devez créer un nouveau PIN avant d'utiliser cette appli", "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": null, + "p_enter_new_fido2_pin": "Saisissez votre nouveau code PIN. Le code PIN doit être composé de {min_length}-{max_length} caractères et peut contenir des lettres, des chiffres et des caractères spéciaux.", "@p_enter_new_fido2_pin": { "placeholders": { "min_length": {}, "max_length": {} } }, - "p_enter_new_fido2_pin_complexity_active": null, + "p_enter_new_fido2_pin_complexity_active": "Saisissez votre nouveau code PIN. Le code PIN doit être composé de {min_length}-{max_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": { "placeholders": { "min_length": {}, @@ -321,23 +320,23 @@ "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_ep_attestation": "Attestation d'entreprise", + "s_ep_attestation_enabled": "Attestation d'entreprise activée", + "s_enable_ep_attestation": "Permettre l'attestation de l'entreprise", + "p_enable_ep_attestation_desc": "Cela permettra l'Attestation d'Entreprise, permettant aux domaines autorisés d'identifier votre YubiKey de manière unique.", + "p_enable_ep_attestation_disable_with_factory_reset": "Une fois activée, l'attestation d'entreprise ne peut être désactivée qu'en procédant à une réinitialisation d'usine de FIDO.", "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": null, + "p_enter_new_piv_pin_puk": "Entrez un nouveau {name} à définir. Il doit comporter au moins {length} caractères.", "@p_enter_new_piv_pin_puk": { "placeholders": { "name": {}, "length": {} } }, - "p_enter_new_piv_pin_puk_complexity_active": null, + "p_enter_new_piv_pin_puk_complexity_active": "Saisissez un nouveau {name} à définir. Il doit être composé d'au moins {length} 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": { "placeholders": { "name": {}, @@ -355,7 +354,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, + "l_pin_complexity": "Complexité du code PIN appliquée", "@_passwords": {}, "s_password": "Mot de passe", @@ -365,7 +364,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, + "l_password_protection": "Protection des comptes par mot de passe", "s_new_password": "Nouveau mot de passe", "s_current_password": "Mot de passe actuel", "s_confirm_password": "Confirmer mot de passe", @@ -378,8 +377,8 @@ "s_password_forgotten": "Mot de passe oublié", "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_unlock_first": "Déverrouillage par mot de passe", + "l_set_password_first": "Définir un mot de passe", "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.", @@ -430,10 +429,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_add_account_password_required": "Mot de passe requis", + "l_add_account_unlock_required": "Déverrouillage requis", + "l_add_account_already_exists": "Le compte existe déjà", + "l_add_account_func_missing": "Fonctionnalité manquante ou désactivée", "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", @@ -442,12 +441,12 @@ "s_pin_account": "Épingler compte", "s_unpin_account": "Détacher compte", "s_no_pinned_accounts": "Aucun compte épinglé", - "s_pinned": null, + "s_pinned": "Épinglé", "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": "Échec du renommage du compte : {message}", "@l_rename_account_failed": { "placeholders": { "message": {} @@ -458,7 +457,7 @@ "l_delete_account_desc": "Supprimer le compte de votre YubiKey", "s_account_deleted": "Compte supprimé", "p_warning_delete_account": "Attention\u00a0! Cela supprimera le compte de votre YubiKey.", - "p_warning_disable_credential": "Vous ne pourrez plus générer d'OTP pour ce compte. Vous devez désactiver ces infos d'identification du site Web pour éviter que votre compte soit bloqué.", + "p_warning_disable_credential": "Vous ne pourrez plus générer d'OTP pour ce compte. Veillez à désactiver cet identifiant à partir du site web afin d'éviter que votre compte ne soit verrouillé.", "s_account_name": "Nom du compte", "s_search_accounts": "Rechercher comptes", "l_accounts_used": "{used} comptes sur {capacity} utilisés", @@ -488,7 +487,7 @@ "@_fido_credentials": {}, "s_rp_id": "RP ID", - "s_user_id": "Identifiant de l'utilisateur", + "s_user_id": "ID d'utilisateur", "s_credential_id": "Credential ID", "s_display_name": "Nom affiché", "s_user_name": "Nom d'utilisateur", @@ -498,17 +497,17 @@ "label": {} } }, - "s_passkeys": "Clés d'accès", + "s_passkeys": "Passkeys", "s_no_passkeys": "Aucun mot de passe", "l_ready_to_use": "Prête à l'emploi", "l_register_sk_on_websites": "Enregistrer comme clé de sécurité sur le Web", - "l_no_discoverable_accounts": "Pas de passkey enregistrée", - "p_non_passkeys_note": "Les identifiants de non-mot de passe peuvent exister, mais ne peuvent pas être listés.", + "l_no_discoverable_accounts": "Pas de passkey enregistré", + "p_non_passkeys_note": "Des identifiants non-passkey peuvent exister, mais ne peuvent pas être listés.", "s_delete_passkey": "Supprimer passkey", "l_delete_passkey_desc": "Supprimer passkey de la YubiKey", - "s_passkey_deleted": "Passkey supprimée", + "s_passkey_deleted": "Passkey supprimé", "p_warning_delete_passkey": "Cela supprimera la passkey de votre YubiKey.", - "s_search_passkeys": "Rechercher des mots de passe", + "s_search_passkeys": "Rechercher des passkeys", "p_passkeys_used": "{used} des clés {max} utilisées.", "@p_passkeys_used": { "placeholders": { @@ -517,7 +516,7 @@ } }, "@_fingerprints": {}, - "s_biometrics": null, + "s_biometrics": "Biométrie", "l_fingerprint": "Empreinte digitale\u00a0: {label}", "@l_fingerprint": { "placeholders": { @@ -600,7 +599,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, + "l_change_defaults": "Modifier les codes d'accès par défaut", "s_issuer": "Émetteur", "s_serial": "Série", "s_certificate_fingerprint": "Empreinte digitale", @@ -676,10 +675,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, + "s_allow_fingerprint": "Autoriser les empreintes digitales", "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, + "p_cert_options_bio_desc": "Algorithme de clé à utiliser, format de sortie, date d'expiration (certificat uniquement) et possibilité d'utiliser des données biométriques à la place du code PIN.", + "p_key_options_bio_desc": "Permettre l'utilisation de données biométriques au lieu d'un code PIN.", "s_overwrite_slot": "Écraser slot", "p_overwrite_slot_desc": "Cela écrasera définitivement le contenu du slot {slot}.", "@p_overwrite_slot_desc": { @@ -822,7 +821,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_factory_reset_required": "Réinitialisation d'usine nécessaire", "l_oath_application_reset": "Réinitialisation OATH", "l_fido_app_reset": "Réinitialisation FIDO", "l_reset_failed": "Erreur de réinitialisation\u00a0: {message}", @@ -835,13 +834,13 @@ "p_factory_reset_an_app": "Réinitialisation d'usine d'une appli sur votre YubiKey.", "p_factory_reset_desc": "Les données sont stockées dans plusieurs applis de la YubiKey. Certaines peuvent être réinitialisées indépendamment.\n\nChoisissez une appli ci-dessus à réinitialiser.", "p_warning_factory_reset": "Attention\u00a0! Cela supprimera définitivement tous les comptes OATH TOTP/HOTP de votre YubiKey.", - "p_warning_disable_credentials": "Vos identifiants OATH, ainsi que tout mot de passe défini, seront supprimés de cette YubiKey. Assurez-vous de les désactiver d'abord de leurs sites Web respectifs pour que vos comptes ne soient pas bloqués.", + "p_warning_disable_credentials": "Vos informations d'identification OATH, ainsi que tout mot de passe défini, seront supprimés de cette clé YubiKey. Veillez à les désactiver à partir de leurs sites web respectifs afin d'éviter que vos comptes ne soient verrouillés.", "p_warning_deletes_accounts": "Attention\u00a0! Cela supprimera définitivement tous les comptes U2F et FIDO2, notamment les passkeys, de votre YubiKey.", - "p_warning_disable_accounts": "Vos identifiants, ainsi que tout code PIN défini, seront supprimés de cette YubiKey. Assurez-vous de les désactiver d'abord de leurs sites Web respectifs pour que vos comptes ne soient pas bloqués.", + "p_warning_disable_accounts": "Vos informations d'identification, ainsi que tout code PIN défini, seront supprimés de cette YubiKey. Veillez à les désactiver à partir de leurs sites web respectifs pour éviter que vos comptes ne soient verrouillés.", "p_warning_piv_reset": "Attention\u00a0! Toutes les données PIV seront définitivement supprimées de votre YubiKey.", "p_warning_piv_reset_desc": "Cela inclut les clés privées et les certificats. Vos PIN, PUK et clé de gestion seront réinitialisés à leurs valeurs d'usine.", "p_warning_global_reset": "Attention\u00a0! Cela supprimera définitivement toutes les données enregistrées, notamment les identifiants, de votre YubiKey.", - "p_warning_global_reset_desc": "Réinitialisez les applications de votre YubiKey. Le PIN sera réinitialisé à sa valeur d'usine et les empreintes enregistrées seront supprimées. Les clés, certificats et autres identifiants seront définitivement supprimés.", + "p_warning_global_reset_desc": "Réinitialiser les applications de votre YubiKey. Le code PIN sera réinitialisé à sa valeur d'usine par défaut et les empreintes digitales enregistrées seront supprimées. Les clés, certificats ou autres informations d'identification seront tous définitivement supprimés.", "@_copy_to_clipboard": {}, "l_copy_to_clipboard": "Copier dans presse-papiers", @@ -899,34 +898,11 @@ "l_launch_app_on_usb_off": "D'autres applications peuvent utiliser la YubiKey en USB", "s_allow_screenshots": "Autoriser captures d'écran", - "l_nfc_dialog_tap_key": "Appuyez et maintenez votre clé", - "s_nfc_dialog_operation_success": "Succès", - "s_nfc_dialog_operation_failed": "Échec", - - "s_nfc_dialog_oath_reset": "Action\u00a0: réinitialiser applet OATH", - "s_nfc_dialog_oath_unlock": "Action\u00a0: débloquer applet OATH", - "s_nfc_dialog_oath_set_password": "Action\u00a0: définir mot de passe OATH", - "s_nfc_dialog_oath_unset_password": "Action\u00a0: supprimer mot de passe OATH", - "s_nfc_dialog_oath_add_account": "Action\u00a0: ajouter nouveau compte", - "s_nfc_dialog_oath_rename_account": "Action\u00a0: renommer compte", - "s_nfc_dialog_oath_delete_account": "Action\u00a0: supprimer compte", - "s_nfc_dialog_oath_calculate_code": "Action\u00a0: calculer code OATH", - "s_nfc_dialog_oath_failure": "Opération OATH impossible", - "s_nfc_dialog_oath_add_multiple_accounts": "Action\u00a0: ajouter plusieurs comptes", - - "s_nfc_dialog_fido_reset": "Action : réinitialiser l'application FIDO", - "s_nfc_dialog_fido_unlock": "Action : déverrouiller l'application FIDO", - "l_nfc_dialog_fido_set_pin": "Action : définir ou modifier le code PIN FIDO", - "s_nfc_dialog_fido_delete_credential": "Action : supprimer le Passkey", - "s_nfc_dialog_fido_delete_fingerprint": "Action : supprimer l'empreinte digitale", - "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, + "s_nfc_ready_to_scan": "Prêt à numériser", + "s_nfc_hold_still": "Ne bougez pas\u2026", + "s_nfc_tap_your_yubikey": "Tapez sur votre YubiKey", + "l_nfc_failed_to_scan": "Échec de l'analyse, réessayez", "@_ndef": {}, "p_ndef_set_otp": "Code OTP copié de la YubiKey dans le presse-papiers.", @@ -936,7 +912,7 @@ "@_key_customization": {}, "s_set_label": "Définir l'étiquette", - "s_set_color": null, + "s_set_color": "Définir la couleur", "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 fb55e0d06..a7d59214a 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -30,15 +30,15 @@ "s_delete": "削除", "s_move": "移動", "s_quit": "終了", - "s_enable": null, - "s_enabled": null, - "s_disabled": null, + "s_enable": "有効にする", + "s_enabled": "有効", + "s_disabled": "無効", "s_status": "ステータス", "s_unlock": "ロック解除", "s_calculate": "計算", "s_import": "インポート", "s_overwrite": "上書き", - "s_done": null, + "s_done": "完了", "s_label": "ラベル", "s_name": "名前", "s_usb": "USB", @@ -47,11 +47,11 @@ "s_details": "詳細", "s_show_window": "ウィンドウを表示", "s_hide_window": "ウィンドウを非表示", - "s_show_navigation": null, + "s_show_navigation": "ナビゲーションを表示する", "s_expand_navigation": "ナビゲーションを展開", "s_collapse_navigation": "ナビゲーションを閉じる", - "s_show_menu": null, - "s_hide_menu": null, + "s_show_menu": "メニューを表示する", + "s_hide_menu": "メニューを隠す", "q_rename_target": "{label}の名前を変更しますか?", "@q_rename_target": { "placeholders": { @@ -128,10 +128,10 @@ "s_dark_mode": "ダークモード", "@_layout": {}, - "s_list_layout": null, - "s_grid_layout": null, - "s_mixed_layout": null, - "s_select_layout": null, + "s_list_layout": "リストレイアウト", + "s_grid_layout": "グリッドレイアウト", + "s_mixed_layout": "ミックスレイアウト", + "s_select_layout": "レイアウトを選択", "@_yubikey_selection": {}, "s_select_to_scan": "選択してスキャン", @@ -161,8 +161,8 @@ } }, "l_firmware_version": "ファームウェアバージョン: {version}", - "l_fips_capable": null, - "l_fips_approved": null, + "l_fips_capable": "FIPS対応", + "l_fips_approved": "FIPS承認済み", "@_yubikey_interactions": {}, "l_insert_yk": "YubiKeyを挿入してください", @@ -203,7 +203,6 @@ "app": {} } }, - "s_app_disabled": "アプリケーションが無効です", "l_app_disabled_desc": "アクセスするにはYubiKeyで{app}アプリケーションを有効にしてください", "@l_app_disabled_desc": { "placeholders": { @@ -230,15 +229,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_restricted_nfc": "NFCアクティベーション", + "l_deactivate_restricted_nfc": "NFCの起動方法", + "p_deactivate_restricted_nfc_desc": "YubiKeyをコンピュータなどのUSB電源に3秒以上接続してください。\n\n電源が入ると、NFCが有効になり、使用できるようになります。", + "p_deactivate_restricted_nfc_footer": "YubiKeyには制限付きNFCが搭載されています。これは、配送中の無線操作から保護するために設計された機能です。つまり、NFC 操作は起動するまで一時的に無効になります。", "s_unsupported_yk": "サポートされていないYubiKey", "s_yk_not_recognized": "デバイスが認識されません", "p_operation_failed_try_again": "操作に失敗しました。もう一度やり直してください。", - "l_configuration_unsupported": null, - "p_scp_unsupported": null, + "l_configuration_unsupported": "コンフィギュレーションがサポートされていない", + "p_scp_unsupported": "YubiKeyがNFCで通信するには、この携帯電話ではサポートされていない技術が必要です。YubiKeyを携帯電話のUSBポートに接続してください。", "@_general_errors": {}, "l_error_occurred": "エラーが発生しました", @@ -298,21 +297,21 @@ "l_enter_fido2_pin": "YubiKeyのFIDO2 PINを入力してください", "l_pin_blocked_reset": "PINがブロックされています。FIDOアプリケーションを工場出荷時の状態にリセットしてください", "l_pin_blocked": "PINがブロックされています", - "l_set_pin_first": "最初にPINの入力が必要です", - "l_unlock_pin_first": "最初にPINでロックを解除してください", + "l_set_pin_first": "暗証番号が必要", + "l_unlock_pin_first": "PINロック解除", "l_pin_soft_locked": "YubiKeyを取り外して再挿入するまで、PINがブロックされています", "l_pin_change_required_desc": "このアプリケーションを使用する前に新しいPINを設定する必要があります", "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": null, + "p_enter_new_fido2_pin": "新しいPINを入力してください。PIN は {min_length}-{max_length} 文字で、英字、数字、特殊文字を含むことができます。", "@p_enter_new_fido2_pin": { "placeholders": { "min_length": {}, "max_length": {} } }, - "p_enter_new_fido2_pin_complexity_active": null, + "p_enter_new_fido2_pin_complexity_active": "新しいPINを入力してください。PINは {min_length}-{max_length} 文字で、少なくとも {unique_characters} のユニークな文字を含み、\"{common_pin}\"のような一般的に使用されているPINであってはなりません。文字、数字、特殊文字を含むことができます。", "@p_enter_new_fido2_pin_complexity_active": { "placeholders": { "min_length": {}, @@ -321,23 +320,23 @@ "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_ep_attestation": "企業認証", + "s_ep_attestation_enabled": "エンタープライズ認証に対応", + "s_enable_ep_attestation": "エンタープライズ認証の有効化", + "p_enable_ep_attestation_desc": "これによりEnterprise Attestationが有効になり、認証されたドメインがYubiKeyを一意に識別できるようになります。", + "p_enable_ep_attestation_disable_with_factory_reset": "一旦有効になると、Enterprise Attestation は、FIDO の工場出荷時リセットを実行することによってのみ無効にすることができます。", "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": null, + "p_enter_new_piv_pin_puk": "設定する新しい {name} を入力します。 {length} 文字以上でなければなりません。", "@p_enter_new_piv_pin_puk": { "placeholders": { "name": {}, "length": {} } }, - "p_enter_new_piv_pin_puk_complexity_active": null, + "p_enter_new_piv_pin_puk_complexity_active": "設定する新しい {name} を入力する。少なくとも {length} 文字の長さで、少なくとも 2 つのユニークな文字を含み、\"{common}\"のような一般的に使用される {name}ではない必要があります。", "@p_enter_new_piv_pin_puk_complexity_active": { "placeholders": { "name": {}, @@ -355,7 +354,7 @@ "l_warning_default_puk": "警告: デフォルトのPUKが使用されています", "l_default_pin_used": "デフォルトのPINが使用されています", "l_default_puk_used": "既定のPUKを使用", - "l_pin_complexity": null, + "l_pin_complexity": "暗証番号の複雑さ", "@_passwords": {}, "s_password": "パスワード", @@ -365,7 +364,7 @@ "s_show_password": "パスワードを表示", "s_hide_password": "パスワードを非表示", "l_optional_password_protection": "オプションのパスワード保護", - "l_password_protection": null, + "l_password_protection": "アカウントのパスワード保護", "s_new_password": "新しいパスワード", "s_current_password": "現在のパスワード", "s_confirm_password": "パスワードを確認", @@ -378,8 +377,8 @@ "s_password_forgotten": "パスワードが消去されました", "l_keystore_unavailable": "OSのキーストアを利用できません", "l_remember_pw_failed": "パスワードを記憶できませんでした", - "l_unlock_first": "最初にパスワードでロックを解除", - "l_set_password_first": null, + "l_unlock_first": "パスワードによるロック解除", + "l_set_password_first": "パスワードの設定", "l_enter_oath_pw": "YubiKeyのOATHパスワードを入力", "p_enter_current_password_or_reset": "現在のパスワードを入力してください。パスワードがわからない場合は、YubiKeyをリセットする必要があります。", "p_enter_new_password": "新しいパスワードを入力してください。パスワードには文字、数字、特殊文字を含めることができます。", @@ -430,10 +429,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_add_account_password_required": "パスワードが必要", + "l_add_account_unlock_required": "ロック解除が必要", + "l_add_account_already_exists": "アカウントはすでに存在する", + "l_add_account_func_missing": "機能の欠落または無効化", "l_account_name_required": "アカウントには名前が必要です", "l_name_already_exists": "この名前は発行者にすでに存在します", "l_account_already_exists": "このアカウントはYubiKeyにすでに存在します", @@ -442,12 +441,12 @@ "s_pin_account": "アカウントをピン留めする", "s_unpin_account": "アカウントのピン留めを解除", "s_no_pinned_accounts": "ピン留めされたアカウントはありません", - "s_pinned": null, + "s_pinned": "ピン留め", "l_pin_account_desc": "重要なアカウントをまとめて保持", "s_rename_account": "アカウント名を変更", "l_rename_account_desc": "アカウントの発行者/名前を編集", "s_account_renamed": "アカウントの名前が変更されました", - "l_rename_account_failed": null, + "l_rename_account_failed": "アカウント名の変更に失敗しました: {message}", "@l_rename_account_failed": { "placeholders": { "message": {} @@ -458,7 +457,7 @@ "l_delete_account_desc": "YubiKeyからアカウントを削除", "s_account_deleted": "アカウントが削除されました", "p_warning_delete_account": "警告!このアクションにより、YubiKeyからアカウントが削除されます。", - "p_warning_disable_credential": "このアカウントのOTPを生成できなくなります。アカウントからロックアウトされないよう、初めにWebサイトからこの認証情報を無効にしてください。", + "p_warning_disable_credential": "このアカウントでOTPを生成することはできなくなります。アカウントからロックアウトされないように、必ずウェブサイトからこのクレデンシャルを無効にしてください。", "s_account_name": "アカウント名", "s_search_accounts": "アカウントを検索", "l_accounts_used": "{used}/{capacity}のアカウントが使用済み", @@ -509,7 +508,7 @@ "s_passkey_deleted": "パスキーが削除されました", "p_warning_delete_passkey": "これにより、YubiKeyからパスキーが削除されます。", "s_search_passkeys": "パスキーを検索", - "p_passkeys_used": "{used} の {max} 使用されたパスキー", + "p_passkeys_used": "{used} の {max} 使用されたパスキー。", "@p_passkeys_used": { "placeholders": { "used": {}, @@ -517,7 +516,7 @@ } }, "@_fingerprints": {}, - "s_biometrics": null, + "s_biometrics": "バイオメトリクス", "l_fingerprint": "指紋:{label}", "@l_fingerprint": { "placeholders": { @@ -600,7 +599,7 @@ "l_delete_certificate_or_key_desc": "YubiKey から証明書または鍵を削除する", "l_move_key": "キーを移動", "l_move_key_desc": "あるPIVスロットから別のスロットにキーを移動する", - "l_change_defaults": null, + "l_change_defaults": "デフォルトのアクセスコードを変更する", "s_issuer": "発行者", "s_serial": "シリアル", "s_certificate_fingerprint": "フィンガープリント", @@ -676,10 +675,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, + "s_allow_fingerprint": "指紋認証", "p_cert_options_desc": "使用する鍵アルゴリズム、出力形式、および有効期限(証明書のみ)。", - "p_cert_options_bio_desc": null, - "p_key_options_bio_desc": null, + "p_cert_options_bio_desc": "使用する鍵アルゴリズム、出力形式、有効期限(証明書のみ)、PINの代わりに生体認証を使用できるかどうか。", + "p_key_options_bio_desc": "暗証番号の代わりに生体認証を使用できるようにする。", "s_overwrite_slot": "スロットを上書き", "p_overwrite_slot_desc": "これにより、スロット{slot}内の既存コンテンツが完全に上書きされます。", "@p_overwrite_slot_desc": { @@ -822,7 +821,7 @@ "s_reset": "リセット", "s_factory_reset": "工場出荷時の状態にリセット", "l_factory_reset_desc": "YubiKey の既定値を復元", - "l_factory_reset_required": null, + "l_factory_reset_required": "工場出荷時のリセットが必要", "l_oath_application_reset": "OATHアプリケーションのリセット", "l_fido_app_reset": "FIDOアプリケーションのリセット", "l_reset_failed": "リセットの実行エラー:{message}", @@ -835,13 +834,13 @@ "p_factory_reset_an_app": "YubiKeyのアプリケーションを工場出荷時の状態にリセットします。", "p_factory_reset_desc": "データは、YubiKeyの複数のアプリケーションに保存されています。一部のアプリケーションは、単独で工場出荷時の状態にリセットできます。\n\nリセットするには上のアプリケーションを選択してください。", "p_warning_factory_reset": "警告!これにより、すべてのOATH TOTP/HOTPアカウントがYubiKeyから削除されます。削除は取り消すことができません。", - "p_warning_disable_credentials": "OATHの認証情報とパスワードセットがこのYubiKeyから削除されます。アカウントからロックアウトされないよう、初めに各Webサイトからこれらを無効にしてください。", + "p_warning_disable_credentials": "あなたの OATH 認証情報、および設定されているパスワードは、この YubiKey から削除されます。アカウントからロックアウトされないように、それぞれのウェブサイトからこれらを無効にしてください。", "p_warning_deletes_accounts": "警告!これにより、すべてのU2FおよびFIDO2アカウントがパスキーを含めてYubiKeyから削除されます。削除は取り消すことができません。", - "p_warning_disable_accounts": "認証情報とPINセットがこのYubiKeyから削除されます。アカウントからロックアウトされないよう、初めに各Webサイトからこれらを無効にしてください。", + "p_warning_disable_accounts": "あなたの認証情報および設定された PIN は、この YubiKey から削除されます。アカウントからロックアウトされないように、それぞれのウェブサイトからこれらを無効にしてください。", "p_warning_piv_reset": "警告!PIVに関連して保存されたすべてのデータがYubiKeyから削除されます。削除は取り消すことができません。", "p_warning_piv_reset_desc": "これには、秘密鍵と証明書が含まれます。PIN、PUK、および管理キーが工場出荷時のデフォルト値にリセットされます。", "p_warning_global_reset": "警告!これにより、すべての保存済みデータが認証情報を含めてYubiKeyから削除されます。削除は取り消すことができません。", - "p_warning_global_reset_desc": "YubiKeyのアプリケーションを工場出荷時の状態にリセットします。PINは工場出荷時のデフォルト値にリセットされ、登録された指紋は削除されます。すべての鍵、証明書、またはその他の認証情報が完全に削除されます。", + "p_warning_global_reset_desc": "YubiKeyのアプリケーションをファクトリーリセットします。PINは工場出荷時の値にリセットされ、登録されていた指紋は削除されます。鍵、証明書、その他の認証情報はすべて永久に削除されます。", "@_copy_to_clipboard": {}, "l_copy_to_clipboard": "クリップボードにコピー", @@ -899,34 +898,11 @@ "l_launch_app_on_usb_off": "他のアプリがUSB経由でYubiKeyを使用できます", "s_allow_screenshots": "スクリーンショットを許可", - "l_nfc_dialog_tap_key": "キーをタップして長押しします", - "s_nfc_dialog_operation_success": "成功", - "s_nfc_dialog_operation_failed": "失敗", - - "s_nfc_dialog_oath_reset": "アクション:OATHアプレットをリセット", - "s_nfc_dialog_oath_unlock": "アクション:OATHアプレットをロック解除", - "s_nfc_dialog_oath_set_password": "アクション:OATHパスワードを設定", - "s_nfc_dialog_oath_unset_password": "アクション:OATHパスワードを削除", - "s_nfc_dialog_oath_add_account": "アクション:新しいアカウントを追加", - "s_nfc_dialog_oath_rename_account": "アクション:アカウント名を変更", - "s_nfc_dialog_oath_delete_account": "アクション:アカウントを削除", - "s_nfc_dialog_oath_calculate_code": "アクション:OATHコードを計算", - "s_nfc_dialog_oath_failure": "OATH操作が失敗しました", - "s_nfc_dialog_oath_add_multiple_accounts": "アクション:複数アカウントを追加", - - "s_nfc_dialog_fido_reset": "アクション: FIDOアプリケーションをリセット", - "s_nfc_dialog_fido_unlock": "アクション:FIDOアプリケーションのロックを解除する", - "l_nfc_dialog_fido_set_pin": "アクション:FIDOのPINの設定または変更", - "s_nfc_dialog_fido_delete_credential": "アクション: パスキーを削除", - "s_nfc_dialog_fido_delete_fingerprint": "アクション: 指紋の削除", - "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, + "s_nfc_ready_to_scan": "スキャン準備完了", + "s_nfc_hold_still": "そのまま\u2026", + "s_nfc_tap_your_yubikey": "YubiKeyをタップする", + "l_nfc_failed_to_scan": "スキャンに失敗しました。", "@_ndef": {}, "p_ndef_set_otp": "OTPコードがYubiKeyからクリップボードに正常にコピーされました。", @@ -936,7 +912,7 @@ "@_key_customization": {}, "s_set_label": "ラベルを設定", - "s_set_color": null, + "s_set_color": "設定色", "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 2aa42f669..3855cbf30 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -28,17 +28,17 @@ "s_cancel": "Anuluj", "s_close": "Zamknij", "s_delete": "Usuń", - "s_move": null, + "s_move": "Przenieś", "s_quit": "Wyjdź", - "s_enable": null, - "s_enabled": null, - "s_disabled": null, + "s_enable": "Włącz", + "s_enabled": "Włączone", + "s_disabled": "Wyłączony", "s_status": "Status", "s_unlock": "Odblokuj", "s_calculate": "Oblicz", "s_import": "Importuj", "s_overwrite": "Nadpisz", - "s_done": null, + "s_done": "Gotowe", "s_label": "Etykieta", "s_name": "Nazwa", "s_usb": "USB", @@ -47,11 +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, + "s_show_navigation": "Pokaż nawigację", + "s_expand_navigation": "Rozwiń nawigację", + "s_collapse_navigation": "Zwiń nawigację", + "s_show_menu": "Pokaż menu", + "s_hide_menu": "Ukryj menu", "q_rename_target": "Zmienić nazwę {label}?", "@q_rename_target": { "placeholders": { @@ -64,38 +64,38 @@ "item": {} } }, - "s_none": null, + "s_none": "", "s_about": "O aplikacji", "s_algorithm": "Algorytm", "s_appearance": "Wygląd", "s_actions": "Działania", - "s_manage": "Zarządzaj", - "s_setup": "Konfiguruj", - "s_device": null, - "s_application": null, + "s_manage": "Zarządzanie", + "s_setup": "Konfiguracja", + "s_device": "Urządzenie", + "s_application": "Aplikacja", "s_settings": "Ustawienia", - "l_settings_desc": null, + "l_settings_desc": "Zmień ustawienia aplikacji", "s_certificates": "Certyfikaty", - "s_security_key": null, + "s_security_key": "Klucz bezpieczeństwa", "s_slots": "Sloty", "s_help_and_about": "Pomoc i informacje", - "l_help_and_about_desc": null, + "l_help_and_about_desc": "Rozwiązywanie problemów i wsparcie", "s_help_and_feedback": "Pomoc i opinie", - "s_home": null, - "s_user_guide": null, + "s_home": "Strona główna", + "s_user_guide": "Przewodnik użytkownika", "s_i_need_help": "Pomoc", "s_troubleshooting": "Rozwiązywanie problemów", "s_terms_of_use": "Warunki użytkowania", "s_privacy_policy": "Polityka prywatności", "s_open_src_licenses": "Licencje open source", - "s_please_wait": "Proszę czekać\u2026", + "s_please_wait": "Poczekaj\u2026", "s_secret_key": "Tajny klucz", "s_show_secret_key": "Pokaż tajny klucz", "s_hide_secret_key": "Ukryj tajny klucz", "s_private_key": "Klucz prywatny", - "s_public_key": null, - "s_invalid_length": "Nieprawidłowa długość", + "s_public_key": "Klucz publiczny", + "s_invalid_length": "Długość jest nieprawidłowa", "l_invalid_format_allowed_chars": "Nieprawidłowy format, dozwolone znaki: {characters}", "@l_invalid_format_allowed_chars": { "placeholders": { @@ -106,7 +106,7 @@ "s_require_touch": "Wymagaj dotknięcia", "q_have_account_info": "Masz dane konta?", "s_run_diagnostics": "Uruchom diagnostykę", - "s_log_level": "Poziom logowania: {level}", + "s_log_level": "Poziom logów: {level}", "@s_log_level": { "placeholders": { "level": {} @@ -117,24 +117,24 @@ "@_language": {}, "s_language": "Język", - "l_enable_community_translations": "Tłumaczenia społecznościowe", - "p_community_translations_desc": "Są dostarczane i utrzymywane przez społeczność. Mogą zawierać błędy lub być niekompletne.", + "l_enable_community_translations": "Włącz tłumaczenia społecznościowe", + "p_community_translations_desc": "Tłumaczenia są dostarczane i utrzymywane przez społeczność. Mogą zawierać błędy lub być niekompletne.", "@_theme": {}, "s_app_theme": "Motyw aplikacji", - "s_choose_app_theme": "Wybierz motyw aplikacji", - "s_system_default": "Zgodny z systemem", + "s_choose_app_theme": "Wybierz motyw", + "s_system_default": "Domyślny systemu", "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, + "s_list_layout": "Układ listy", + "s_grid_layout": "Układ siatki", + "s_mixed_layout": "Układ mieszany", + "s_select_layout": "Wybierz układ", "@_yubikey_selection": {}, - "s_select_to_scan": "Wybierz, aby skanować", + "s_select_to_scan": "Wybierz, aby zeskanować", "s_hide_device": "Ukryj urządzenie", "s_show_hidden_devices": "Pokaż ukryte urządzenia", "s_sn_serial": "S/N: {serial}", @@ -154,27 +154,27 @@ "serial": {} } }, - "l_serial_number": null, + "l_serial_number": "Numer seryjny: {serial}", "@l_firmware_version": { "placeholders": { "version": {} } }, - "l_firmware_version": null, - "l_fips_capable": null, - "l_fips_approved": null, + "l_firmware_version": "Wersja oprogramowania: {version}", + "l_fips_capable": "Zgodność ze standardem FIPS", + "l_fips_approved": "Zatwierdzony przez FIPS", "@_yubikey_interactions": {}, "l_insert_yk": "Podłącz klucz YubiKey", - "l_insert_or_tap_yk": "Podłącz lub przystaw YubiKey", + "l_insert_or_tap_yk": "Podłącz lub zbliż klucz YubiKey", "l_unplug_yk": "Odłącz klucz YubiKey", - "l_reinsert_yk": "Ponownie podłącz YubiKey", - "l_place_on_nfc_reader": "Przyłóż klucz YubiKey do czytnika NFC", - "l_replace_yk_on_reader": "Umieść klucz YubiKey z powrotem na czytniku", + "l_reinsert_yk": "Podłącz ponownie klucz YubiKey", + "l_place_on_nfc_reader": "Zbliż klucz YubiKey do czytnika NFC", + "l_replace_yk_on_reader": "Zbliż klucz YubiKey do czytnika", "l_remove_yk_from_reader": "Odsuń klucz YubiKey od czytnika NFC", - "p_try_reinsert_yk": "Spróbuj ponownie podłączyć klucz YubiKey.", - "s_touch_required": "Wymagane dotknięcie", - "l_touch_button_now": "Dotknij teraz przycisku na kluczu YubiKey", + "p_try_reinsert_yk": "Spróbuj podłączyć ponownie klucz YubiKey.", + "s_touch_required": "Dotknięcie wymagane", + "l_touch_button_now": "Dotknij przycisku na kluczu YubiKey", "l_keep_touching_yk": "Wielokrotnie dotykaj klucza YubiKey\u2026", "@_capabilities": {}, @@ -184,91 +184,90 @@ "s_capability_oath": "OATH", "s_capability_piv": "PIV", "s_capability_openpgp": "OpenPGP", - "s_capability_hsmauth": "Aut. YubiHSM", + "s_capability_hsmauth": "YubiHSM Auth", "@_app_configuration": {}, - "s_toggle_applications": "Przełączanie funkcji", - "s_toggle_interfaces": "Przełącz interfejsy", - "p_toggle_applications_desc": null, - "p_toggle_interfaces_desc": null, - "l_toggle_applications_desc": null, - "l_toggle_interfaces_desc": null, - "s_reconfiguring_yk": "Rekonfigurowanie YubiKey\u2026", - "s_config_updated": "Zaktualizowano konfigurację", - "l_config_updated_reinsert": "Zaktualizowano konfigurację, podłącz ponownie klucz YubiKey", - "s_app_not_supported": "Funkcja nie jest obsługiwana", - "l_app_not_supported_on_yk": "Używany klucz YubiKey nie obsługuje funkcji '{app}'", + "s_toggle_applications": "Przełączanie aplikacji", + "s_toggle_interfaces": "Przełączanie interfejsów", + "p_toggle_applications_desc": "Włącz lub wyłącz aplikacje przez dostępne interfejsy.", + "p_toggle_interfaces_desc": "Włącz lub wyłącz interfejs USB.", + "l_toggle_applications_desc": "Włącz / wyłącz aplikacje", + "l_toggle_interfaces_desc": "Włącz / wyłącz interfejsy", + "s_reconfiguring_yk": "Rekonfigurowanie klucza YubiKey\u2026", + "s_config_updated": "Konfiguracja została zaktualizowana", + "l_config_updated_reinsert": "Konfiguracja została zaktualizowana. Podłącz ponownie klucz YubiKey", + "s_app_not_supported": "Aplikacja nie jest obsługiwana", + "l_app_not_supported_on_yk": "Używany klucz YubiKey nie obsługuje aplikacji '{app}'", "@l_app_not_supported_on_yk": { "placeholders": { "app": {} } }, - "s_app_disabled": "Wyłączona funkcja", - "l_app_disabled_desc": "Włącz funkcję '{app}' w kluczu YubiKey, aby uzyskać dostęp", + "l_app_disabled_desc": "Włącz aplikację '{app}' w kluczu YubiKey, aby uzyskać do niej dostęp", "@l_app_disabled_desc": { "placeholders": { "app": {} } }, - "s_fido_disabled": "FIDO2 wyłączone", - "l_webauthn_req_fido2": "WebAuthn wymaga włączenia funkcji FIDO2 w kluczu YubiKey", - "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_fido_disabled": "Aplikacja FIDO2 została wyłączona", + "l_webauthn_req_fido2": "WebAuthn wymaga włączenia apliakcji FIDO2 w kluczu YubiKey", + "s_lock_code": "Kod blokady", + "l_wrong_lock_code": "Kod blokady jest nieprawidłowy", + "s_show_lock_code": "Pokaż kod blokady", + "s_hide_lock_code": "Ukryj kod blokady", + "p_lock_code_required_desc": "Działanie wymaga wpisania kodu blokady.", "@_connectivity_issues": {}, "l_helper_not_responding": "Proces pomocnika nie odpowiada", "l_yk_no_access": "Dostęp do tego klucza YubiKey jest niemożliwy", - "s_yk_inaccessible": "Urządzenie niedostępne", + "s_yk_inaccessible": "Urządzenie jest niedostępne", "l_open_connection_failed": "Nie udało się nawiązać połączenia", "l_ccid_connection_failed": "Nie udało się nawiązać połączenia z kartą inteligentną", - "p_ccid_service_unavailable": "Upewnij się, że usługa kart inteligentnych działa.", - "p_pcscd_unavailable": "Upewnij się, że pcscd jest zainstalowany i uruchomiony.", - "l_no_yk_present": "Nie wykryto YubiKey", + "p_ccid_service_unavailable": "Upewnij się, że aplikacja kart inteligentnych działa.", + "p_pcscd_unavailable": "Upewnij się, że pakiet pcscd jest zainstalowany i uruchomiony.", + "l_no_yk_present": "Nie wykryto klucza 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, + "s_restricted_nfc": "Aktywacja NFC", + "l_deactivate_restricted_nfc": "Jak aktywować NFC", + "p_deactivate_restricted_nfc_desc": "Podłącz YubiKey do dowolnego źródła zasilania USB, takiego jak komputer, na co najmniej 3 sekundy.\n\nPo włączeniu zasilania, NFC zostanie aktywowane i będzie gotowe do użycia.", + "p_deactivate_restricted_nfc_footer": "Urządzenie YubiKey jest wyposażone w funkcję Restricted NFC, zaprojektowaną w celu zabezpieczenia przed manipulacją bezprzewodową podczas transportu. Oznacza to, że operacje NFC są tymczasowo wyłączone do momentu ich aktywacji.", + "s_unsupported_yk": "Klucz YubiKey jest nieobsługiwany", + "s_yk_not_recognized": "Urządzenie nie zostało rozpoznane", + "p_operation_failed_try_again": "Operacja nie powiodła się. Spróbuj ponownie.", + "l_configuration_unsupported": "Konfiguracja nie jest obsługiwana", + "p_scp_unsupported": "Aby komunikować się przez NFC, YubiKey wymaga technologii, która nie jest obsługiwana przez ten telefon. Podłącz klawiaturę YubiKey do portu USB telefonu.", "@_general_errors": {}, "l_error_occurred": "Wystąpił błąd", - "s_application_error": "Błąd funkcji", + "s_application_error": "Błąd aplikacji", "l_import_error": "Błąd importowania", - "l_file_not_found": "Nie odnaleziono pliku", - "l_file_too_big": "Zbyt duży rozmiar pliku", + "l_file_not_found": "Plik nie został znaleziony", + "l_file_too_big": "Plik jest zbyt duży", "l_filesystem_error": "Błąd operacji systemu plików", "@_pins": {}, "s_pin": "PIN", "s_puk": "PUK", - "s_set_pin": "Ustaw PIN", - "s_change_pin": "Zmień PIN", - "s_change_puk": "Zmień PUK", - "s_show_pin": "Pokaż PIN", - "s_hide_pin": "Ukryj PIN", - "s_show_puk": "Pokaż PUK", - "s_hide_puk": "Ukryj PUK", - "s_current_pin": "Aktualny PIN", - "s_current_puk": "Aktualny PUK", - "s_new_pin": "Nowy PIN", - "s_new_puk": "Nowy PUK", - "s_confirm_pin": "Potwierdź PIN", - "s_confirm_puk": "Potwierdź PUK", - "s_unblock_pin": "Odblokuj PIN", - "l_pin_mismatch": null, - "l_puk_mismatch": null, - "s_pin_set": "PIN ustawiony", - "s_puk_set": "PUK ustawiony", + "s_set_pin": "Ustaw kod PIN", + "s_change_pin": "Zmień kod PIN", + "s_change_puk": "Zmień kod PUK", + "s_show_pin": "Pokaż kod PIN", + "s_hide_pin": "Ukryj kod PIN", + "s_show_puk": "Pokaż kod PUK", + "s_hide_puk": "Ukryj kod PUK", + "s_current_pin": "Obecny kod PIN", + "s_current_puk": "Obecny kod PUK", + "s_new_pin": "Nowy kod PIN", + "s_new_puk": "Nowy kod PUK", + "s_confirm_pin": "Potwierdź kod PIN", + "s_confirm_puk": "Potwierdź kod PUK", + "s_unblock_pin": "Odblokuj kod PIN", + "l_pin_mismatch": "Kody PIN nie pasują do siebie", + "l_puk_mismatch": "Kody PUK nie pasują do siebie", + "s_pin_set": "Kod PIN został ustawiony", + "s_puk_set": "Kod PUK został ustawiony", "l_set_pin_failed": "Nie udało się ustawić kodu PIN: {message}", "@l_set_pin_failed": { "placeholders": { @@ -294,25 +293,25 @@ } }, "s_fido_pin_protection": "Ochrona FIDO kodem PIN", - "s_pin_change_required": "Wymagana zmiana PINu", - "l_enter_fido2_pin": "Wprowadź kod PIN FIDO2 klucza YubiKey", - "l_pin_blocked_reset": "PIN jest zablokowany; przywróć ustawienia fabryczne funkcji FIDO", - "l_pin_blocked": null, + "s_pin_change_required": "Wymagana zmiana kodu PIN", + "l_enter_fido2_pin": "Wpisz kod PIN FIDO2 klucza YubiKey", + "l_pin_blocked_reset": "Kod PIN jest zablokowany. Przywróć ustawienia fabryczne aplikacji FIDO", + "l_pin_blocked": "Kod PIN jest zablokowany", "l_set_pin_first": "Najpierw wymagany jest kod PIN", "l_unlock_pin_first": "Najpierw odblokuj kodem PIN", - "l_pin_soft_locked": "PIN został zablokowany do momentu ponownego podłączenia klucza YubiKey", - "l_pin_change_required_desc": "Wymagane ustawienie nowego kodu PIN przed użyciem tej funkcji", - "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": null, + "l_pin_soft_locked": "Kod PIN został zablokowany do momentu ponownego podłączenia klucza YubiKey", + "l_pin_change_required_desc": "Przed użyciem tej aplikacji ustaw nowy kod PIN", + "p_enter_current_pin_or_reset": "Wpisz obecny kod PIN. Jeśli go nie pamiętasz, zresetuj klucz YubiKey lub odblokuj go za pomocą kodu PUK.", + "p_enter_current_pin_or_reset_no_puk": "Wpisz obecny kod PIN. Jeśli go nie pamiętasz, zresetuj klucz YubiKey.", + "p_enter_current_puk_or_reset": "Wpisz obecny kod PUK. Jeśli go nie pamiętasz, zresetuj klucz YubiKey.", + "p_enter_new_fido2_pin": "Wprowadź nowy kod PIN. Kod PIN musi mieć długość {min_length}-{max_length} znaków i może zawierać litery, cyfry i znaki specjalne.", "@p_enter_new_fido2_pin": { "placeholders": { "min_length": {}, "max_length": {} } }, - "p_enter_new_fido2_pin_complexity_active": null, + "p_enter_new_fido2_pin_complexity_active": "Wprowadź nowy kod PIN. Kod PIN musi mieć długość {min_length}-{max_length} znaków, zawierać co najmniej {unique_characters} unikalnych znaków i nie może być powszechnie używanym kodem PIN, takim jak \"{common_pin}\". Może zawierać litery, cyfry i znaki specjalne.", "@p_enter_new_fido2_pin_complexity_active": { "placeholders": { "min_length": {}, @@ -321,23 +320,23 @@ "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": null, + "s_ep_attestation": "Atest przedsiębiorstwa", + "s_ep_attestation_enabled": "Atestacja przedsiębiorstwa włączona", + "s_enable_ep_attestation": "Włącz zaświadczenia dla przedsiębiorstw", + "p_enable_ep_attestation_desc": "Umożliwi to zaświadczenie Enterprise Attestation, pozwalające autoryzowanym domenom na jednoznaczną identyfikację klucza YubiKey.", + "p_enable_ep_attestation_disable_with_factory_reset": "Po włączeniu atest Enterprise Attestation można wyłączyć tylko poprzez przywrócenie ustawień fabrycznych FIDO.", + "s_pin_required": "Kod PIN jest wymagany", + "p_pin_required_desc": "Działanie wymaga wpisania kodu PIN PIV.", + "l_piv_pin_blocked": "Zablokowano, użyj kodu PUK, aby zresetować", + "l_piv_pin_puk_blocked": "Zablokowano, zresetuj do ustawień fabrycznych", + "p_enter_new_piv_pin_puk": "Wprowadź nowy adres {name} do ustawienia. Musi mieć co najmniej {length} znaków.", "@p_enter_new_piv_pin_puk": { "placeholders": { "name": {}, "length": {} } }, - "p_enter_new_piv_pin_puk_complexity_active": null, + "p_enter_new_piv_pin_puk_complexity_active": "Wprowadź nową nazwę {name} do ustawienia. Musi mieć co najmniej {length} znaków, zawierać co najmniej 2 unikalne znaki i nie może być powszechnie używanym {name}, takim jak \"{common}\".", "@p_enter_new_piv_pin_puk_complexity_active": { "placeholders": { "name": {}, @@ -345,17 +344,17 @@ "common": {} } }, - "p_pin_puk_complexity_failure": null, + "p_pin_puk_complexity_failure": "{name} nie spełnia wymogów.", "@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_pin_complexity": null, + "l_warning_default_pin": "Ostrzeżenie: Używany jest domyślny kod PIN", + "l_warning_default_puk": "Ostrzeżenie: Używany jest domyślny kod PUK", + "l_default_pin_used": "Używany jest domyślny kod PIN", + "l_default_puk_used": "Używany jest domyślny kod PUK", + "l_pin_complexity": "Wymuszona złożoność kodu PIN", "@_passwords": {}, "s_password": "Hasło", @@ -365,41 +364,41 @@ "s_show_password": "Pokaż hasło", "s_hide_password": "Ukryj hasło", "l_optional_password_protection": "Opcjonalna ochrona hasłem", - "l_password_protection": null, + "l_password_protection": "Ochrona kont hasłem", "s_new_password": "Nowe hasło", - "s_current_password": "Aktualne hasło", + "s_current_password": "Obecne hasło", "s_confirm_password": "Potwierdź hasło", - "l_password_mismatch": null, - "s_wrong_password": "Błędne hasło", + "l_password_mismatch": "Hasła nie pasują do siebie", + "s_wrong_password": "Hasło jest nieprawidłowe", "s_remove_password": "Usuń hasło", "s_password_removed": "Hasło zostało usunięte", "s_remember_password": "Zapamiętaj hasło", "s_clear_saved_password": "Usuń zapisane hasło", - "s_password_forgotten": "Hasło zostało zapomniane", + "s_password_forgotten": "Zapomniane hasło", "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.", + "l_set_password_first": "Ustaw hasło", + "l_enter_oath_pw": "Wpisz hasło OATH dla klucza YubiKey", + "p_enter_current_password_or_reset": "Wpisz obecne hasło. Jeśli go nie pamiętasz, zresetuj klucz YubiKey.", + "p_enter_new_password": "Wpisz nowe hasło. Hasło może zawierać litery, cyfry i znaki specjalne.", "@_management_key": {}, "s_management_key": "Klucz zarządzania", - "s_current_management_key": "Aktualny klucz zarządzania", + "s_current_management_key": "Obecny klucz zarządzania", "s_new_management_key": "Nowy klucz zarządzania", "l_change_management_key": "Zmień klucz zarządzania", - "p_change_management_key_desc": "Zmień swój klucz zarządzania. Opcjonalnie możesz zezwolić na używanie kodu PIN zamiast klucza zarządzania.", - "l_management_key_changed": "Zmieniono klucz zarządzania", + "p_change_management_key_desc": "Zmień klucz zarządzania. Możesz opcjonalnie używać kodu PIN.", + "l_management_key_changed": "Klucz zarządzania został zmieniony", "l_default_key_used": "Używany jest domyślny klucz zarządzania", "s_generate_random": "Generuj losowo", "s_use_default": "Użyj domyślnego", - "l_warning_default_key": "Uwaga: Używany jest klucz domyślny", + "l_warning_default_key": "Ostrzeżenie: Używany jest domyślny klucz", "s_protect_key": "Zabezpiecz kodem PIN", "l_pin_protected_key": "Zamiast tego można użyć kodu PIN", - "l_wrong_key": "Błędny klucz", + "l_wrong_key": "Klucz jest nieprawidłowy", "l_unlock_piv_management": "Odblokuj zarządzanie PIV", - "p_unlock_piv_management_desc": "Czynność, którą zamierzasz wykonać, wymaga klucza zarządzania PIV. Podaj ten klucz, aby odblokować funkcje zarządzania dla tej sesji.", + "p_unlock_piv_management_desc": "Działanie wymaga klucza zarządzania PIV. Wpisz ten klucz, aby odblokować funkcje zarządzania dla tej sesji.", "@_oath_accounts": {}, "l_account": "Konto: {label}", @@ -410,16 +409,16 @@ }, "s_accounts": "Konta", "s_no_accounts": "Brak kont", - "l_results_for": null, + "l_results_for": "Wyniki dla \"{query}\"", "@l_results_for": { "placeholders": { "query": {} } }, - "l_authenticator_get_started": "Rozpocznij korzystanie z kont OTP", - "l_no_accounts_desc": "Dodaj konta do swojego klucza YubiKey od dowolnego dostawcy usług obsługującego OATH TOTP/HOTP", + "l_authenticator_get_started": "Zacznij korzystać z kont OTP", + "l_no_accounts_desc": "Dodaj do klucza YubiKey konta z dowolnej usługi wspierające kody OATH TOTP/HOTP", "s_add_account": "Dodaj konto", - "s_add_accounts": "Dodaj konto(-a)", + "s_add_accounts": "Dodaj konta", "p_add_description": "W celu zeskanowania kodu QR, upewnij się, że pełny kod jest widoczny na ekranie a następnie naciśnij poniższy przycisk. Jeśli posiadasz dane uwierzytelniające do konta w tekstowej formie, skorzystaj z opcji ręcznego wprowadzania danych.", "l_drop_qr_description": "Upuść kod QR, aby dodać konto(a)", "s_add_manually": "Dodaj ręcznie", @@ -430,24 +429,24 @@ "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", + "l_add_account_password_required": "Wymagane hasło", + "l_add_account_unlock_required": "Wymagane odblokowanie", + "l_add_account_already_exists": "Konto już istnieje", + "l_add_account_func_missing": "Brak lub wyłączona funkcjonalność", + "l_account_name_required": "Konto musi mieć nazwę", + "l_name_already_exists": "Ta nazwa wydawcy już istnieje", + "l_account_already_exists": "To konto już istnieje w kluczu YubiKey", "l_invalid_character_issuer": "Nieprawidłowy znak: „:” nie jest dozwolony w polu wydawcy", - "l_select_accounts": "Wybierz konta, które chcesz dodać do YubiKey", + "l_select_accounts": "Wybierz konta, które chcesz dodać do klucza YubiKey", "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_pinned": "Przypięty", + "l_pin_account_desc": "Trzymaj 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_desc": "Edytuj wydawcę lub nazwę konta", + "s_account_renamed": "Nazwa konta została zmieniona", + "l_rename_account_failed": "Zmiana nazwy konta nie powiodła się: {message}", "@l_rename_account_failed": { "placeholders": { "message": {} @@ -457,10 +456,10 @@ "s_delete_account": "Usuń konto", "l_delete_account_desc": "Usuń konto z klucza YubiKey", "s_account_deleted": "Konto zostało usunięte", - "p_warning_delete_account": "Uwaga! Ta czynność spowoduje usunięcie konta z klucza YubiKey.", + "p_warning_delete_account": "Ostrzeżenie! Spowoduje to usunięcie konta z klucza YubiKey.", "p_warning_disable_credential": "Nie będzie już możliwe generowanie OTP dla tego konta. Upewnij się, że najpierw wyłączono te dane uwierzytelniające w witrynie, aby uniknąć zablokowania konta.", "s_account_name": "Nazwa konta", - "s_search_accounts": "Wyszukaj konta", + "s_search_accounts": "Szukaj kont", "l_accounts_used": "Użyto {used} z {capacity} kont", "@l_accounts_used": { "placeholders": { @@ -474,7 +473,7 @@ "num": {} } }, - "s_num_sec": "{num} sek", + "s_num_sec": "{num} sek.", "@s_num_sec": { "placeholders": { "num": {} @@ -483,15 +482,15 @@ "s_issuer_optional": "Wydawca (opcjonalnie)", "s_counter_based": "Na podstawie licznika", "s_time_based": "Na podstawie czasu", - "l_copy_code_desc": "Łatwe wklejanie kodu do innych aplikacji", + "l_copy_code_desc": "Wklej kod do innej aplikacji", "l_calculate_code_desc": "Uzyskaj nowy kod z klucza YubiKey", "@_fido_credentials": {}, - "s_rp_id": null, - "s_user_id": null, - "s_credential_id": null, - "s_display_name": null, - "s_user_name": null, + "s_rp_id": "RP ID", + "s_user_id": "User ID", + "s_credential_id": "Credential ID", + "s_display_name": "Nazwa wyświetlana", + "s_user_name": "Nazwa użytkownika", "l_passkey": "Klucz dostępu: {label}", "@l_passkey": { "placeholders": { @@ -499,17 +498,17 @@ } }, "s_passkeys": "Klucze dostępu", - "s_no_passkeys": null, - "l_ready_to_use": "Gotowe do użycia", + "s_no_passkeys": "Brak kluczy dostępu", + "l_ready_to_use": "Gotowy do użycia", "l_register_sk_on_websites": "Zarejestruj jako klucz bezpieczeństwa na stronach internetowych", - "l_no_discoverable_accounts": "Nie wykryto kont", - "p_non_passkeys_note": null, + "l_no_discoverable_accounts": "Brak kluczy dostępu", + "p_non_passkeys_note": "Poświadczenia bez klucza mogą istnieć, ale nie mogą być wymienione.", "s_delete_passkey": "Usuń klucz dostępu", "l_delete_passkey_desc": "Usuń klucz dostępu z klucza YubiKey", - "s_passkey_deleted": "Usunięto klucz dostępu", - "p_warning_delete_passkey": "Spowoduje to usunięcie klucza dostępu z klucza YubiKey.", - "s_search_passkeys": null, - "p_passkeys_used": null, + "s_passkey_deleted": "Klucz dostępu został usunięty", + "p_warning_delete_passkey": "Spowoduje to usunięcie klucza dostępu z YubiKey.", + "s_search_passkeys": "Szukaj kluczy dostępu", + "p_passkeys_used": "{used} użytych kluczy dostępu {max} .", "@p_passkeys_used": { "placeholders": { "used": {}, @@ -517,7 +516,7 @@ } }, "@_fingerprints": {}, - "s_biometrics": null, + "s_biometrics": "Biometria", "l_fingerprint": "Odcisk palca: {label}", "@l_fingerprint": { "placeholders": { @@ -525,13 +524,13 @@ } }, "s_fingerprints": "Odciski palców", - "l_fingerprint_captured": "Odcisk palca zarejestrowany pomyślnie!", - "s_fingerprint_added": "Dodano odcisk palca", - "l_adding_fingerprint_failed": "Błąd dodawania odcisku palca: {message}", + "l_fingerprint_captured": "Odcisk palca został dodany!", + "s_fingerprint_added": "Odcisk palca został dodany", + "l_adding_fingerprint_failed": "Wystąpił błąd podczas dodawania odcisku palca: {message}", "@l_adding_fingerprint_failed": { "placeholders": {} }, - "l_setting_name_failed": "Błąd ustawienia nazwy: {message}", + "l_setting_name_failed": "Wystąpił błąd podczas ustawienia nazwy: {message}", "@l_setting_name_failed": { "placeholders": { "message": {} @@ -543,20 +542,20 @@ "s_delete_fingerprint": "Usuń odcisk palca", "l_delete_fingerprint_desc": "Usuń odcisk palca z klucza YubiKey", "s_fingerprint_deleted": "Odcisk palca został usunięty", - "p_warning_delete_fingerprint": "Spowoduje to usunięcie odcisku palca z twojego YubiKey.", + "p_warning_delete_fingerprint": "Spowoduje to usunięcie odcisku palca z klucza YubiKey.", "s_fingerprints_get_started": "Zacznij korzystać z odcisków palców", "p_set_fingerprints_desc": "Przed możliwością zarejestrowania odcisków palców, należy ustawić PIN.", - "l_no_fps_added": "Nie dodano odcisków palców", + "l_no_fps_added": "Brak dodanych odcisków palców", "s_rename_fp": "Zmień nazwę odcisku palca", - "l_rename_fp_desc": "Zmień etykietę", - "s_fingerprint_renamed": "Zmieniono nazwę odcisku palca", - "l_rename_fp_failed": "Błąd zmiany nazwy: {message}", + "l_rename_fp_desc": "Zmień nazwę", + "s_fingerprint_renamed": "Nazwa odcisku palca została zmieniona", + "l_rename_fp_failed": "Wystąpił błąd podczas zmiany nazwy: {message}", "@l_rename_fp_failed": { "placeholders": { "message": {} } }, - "l_add_one_or_more_fps": "Dodaj jeden lub więcej (do pięciu) odcisków palców", + "l_add_one_or_more_fps": "Dodaj do 5 odcisków palców", "l_fingerprints_used": "Zarejestrowano {used}/5 odcisków palców", "@l_fingerprints_used": { "placeholders": { @@ -564,8 +563,8 @@ } }, "p_press_fingerprint_begin": "Przytrzymaj palec na kluczu YubiKey, aby rozpocząć.", - "p_will_change_label_fp": "Spowoduje to zmianę etykiety odcisku palca.", - "l_name_fingerprint": "Nazwij ten odcisk palca", + "p_will_change_label_fp": "Spowoduje to zmianę nazwę odcisku palca.", + "l_name_fingerprint": "Nazwij odcisk palca", "@_fido_errors": {}, "l_user_action_timeout_error": "Nie powiodło się z powodu braku aktywności użytkownika", @@ -577,32 +576,32 @@ "s_csr": "CSR", "s_subject": "Temat", "l_export_csr_file": "Zapisz CSR do pliku", - "l_export_public_key": null, - "l_export_public_key_file": null, - "l_export_public_key_desc": null, - "l_public_key_exported": null, + "l_export_public_key": "Eksportuj klucz publiczny", + "l_export_public_key_file": "Zapisz klucz publiczny do pliku", + "l_export_public_key_desc": "Eksportuj klucz publiczny do pliku", + "l_public_key_exported": "Klucz publiczny został wyeksportowany", "l_export_certificate": "Eksportuj certyfikat", "l_export_certificate_file": "Eksportuj certyfikat do pliku", - "l_export_certificate_desc": "Pozwala wyeksportować certyfikat do pliku", - "l_certificate_exported": "Wyeksportowano certyfikat", + "l_export_certificate_desc": "Eksportuj certyfikat do pliku", + "l_certificate_exported": "Certyfikat został wyeksportowany", "l_select_import_file": "Wybierz plik do zaimportowania", "l_import_file": "Importuj plik", - "l_import_desc": "Zaimportuj klucz i/lub certyfikat", - "l_import_nothing": null, + "l_import_desc": "Importuj klucz i / lub certyfikat", + "l_import_nothing": "Nic do zaimportowania", "l_importing_file": "Importowanie pliku\u2026", "s_file_imported": "Plik został zaimportowany", - "l_unsupported_key_type": null, + "l_unsupported_key_type": "Typ klucza jest nieobsługiwany", "l_delete_certificate": "Usuń certyfikat", "l_delete_certificate_desc": "Usuń certyfikat z klucza YubiKey", - "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, - "l_change_defaults": null, + "l_delete_key": "Usuń klucz", + "l_delete_key_desc": "Usuń klucz z YubiKey", + "l_delete_certificate_or_key": "Usuń certyfikat / klucz", + "l_delete_certificate_or_key_desc": "Usuń certyfikat lub klucz z YubiKey", + "l_move_key": "Przenieś klucz", + "l_move_key_desc": "Przenieś klucz pomiędzy slotami PIV", + "l_change_defaults": "Zmiana domyślnych kodów dostępu", "s_issuer": "Wydawca", - "s_serial": "Nr. seryjny", + "s_serial": "Numer seryjny", "s_certificate_fingerprint": "Odcisk palca", "s_valid_from": "Ważny od", "s_valid_to": "Ważny do", @@ -616,48 +615,48 @@ "slot": {} } }, - "s_private_key_generated": "Wygenerowano klucz prywatny", - "p_select_what_to_delete": null, - "p_warning_delete_certificate": "Uwaga! Ta czynność spowoduje usunięcie certyfikatu z klucza YubiKey.", - "p_warning_delete_key": null, - "p_warning_delete_certificate_and_key": null, + "s_private_key_generated": "Klucz prywatny został wygeneroewany", + "p_select_what_to_delete": "Wybierz, co usunąć ze slotu.", + "p_warning_delete_certificate": "Ostrzeżenie! Spowoduje to usunięcie certyfikatu z klucza YubiKey.", + "p_warning_delete_key": "Uwaga! Ta czynność spowoduje usunięcie klucza prywatnego z YubiKey.", + "p_warning_delete_certificate_and_key": "Uwaga! Ta czynność spowoduje usunięcie certyfikatu i klucza prywatnego z YubiKey.", "q_delete_certificate_confirm": "Usunąć certyfikat ze slotu PIV {slot}?", "@q_delete_certificate_confirm": { "placeholders": { "slot": {} } }, - "q_delete_key_confirm": null, + "q_delete_key_confirm": "Usunąć klucz prywatny ze slotu PIV {slot}?", "@q_delete_key_confirm": { "placeholders": { "slot": {} } }, - "q_delete_certificate_and_key_confirm": null, + "q_delete_certificate_and_key_confirm": "Usunąć certyfikat i klucz prywatny ze slotu PIV {slot}?", "@q_delete_certificate_and_key_confirm": { "placeholders": { "slot": {} } }, "l_certificate_deleted": "Certyfikat został usunięty", - "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_key_deleted": "Klucz został usunięty", + "l_certificate_and_key_deleted": "Certyfikat i klucz został usunięty", + "l_include_certificate": "Dołącz certyfikat", + "l_select_destination_slot": "Wybierz docelowy slot", + "q_move_key_confirm": "Przenieść klucz prywatny ze slotu PIV {from_slot}?", "@q_move_key_confirm": { "placeholders": { "from_slot": {} } }, - "q_move_key_to_slot_confirm": null, + "q_move_key_to_slot_confirm": "Przenieść klucz prywatny ze slotu PIV {from_slot} do {to_slot}?", "@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": "Przenieść klucz prywatny i certyfikat ze slotu PIV {from_slot} do {to_slot}?", "@q_move_key_and_certificate_to_slot_confirm": { "placeholders": { "from_slot": {}, @@ -671,17 +670,17 @@ "slot": {} } }, - "l_key_moved": null, - "l_key_and_certificate_moved": null, + "l_key_moved": "Klucz został przeniesiony", + "l_key_and_certificate_moved": "Klucz i certyfikat został przeniesiony", "p_subject_desc": "Nazwa wyróżniająca (DN) sformatowana zgodnie ze specyfikacją RFC 4514.", - "l_rfc4514_invalid": "Nieprawidłowy format RFC 4514", + "l_rfc4514_invalid": "Format RFC 4514 jest nieprawidłowy", "rfc4514_examples": "Przykłady:\nCN=Przykładowa Nazwa\nCN=jkowalski,DC=przyklad,DC=pl", - "s_allow_fingerprint": null, + "s_allow_fingerprint": "Zezwalaj na odcisk palca", "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, + "p_cert_options_bio_desc": "Używany algorytm klucza, format wyjściowy, data wygaśnięcia (tylko certyfikat) i czy zamiast kodu PIN można użyć danych biometrycznych.", + "p_key_options_bio_desc": "Umożliwienie korzystania z danych biometrycznych zamiast kodu PIN.", "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": "Spowoduje to trwałe nadpisanie obecnej zawartości w slocie {slot}.", "@p_overwrite_slot_desc": { "placeholders": { "slot": {} @@ -689,7 +688,7 @@ }, "l_overwrite_cert": "Certyfikat zostanie nadpisany", "l_overwrite_key": "Klucz prywatny zostanie nadpisany", - "l_overwrite_key_maybe": "Każdy istniejący klucz prywatny w slocie zostanie nadpisany", + "l_overwrite_key_maybe": "Każdy obecny klucz prywatny w slocie zostanie nadpisany", "@_piv_slots": {}, "s_slot_display_name": "{name} ({hexid})", @@ -700,10 +699,10 @@ } }, "s_slot_9a": "Uwierzytelnienie", - "s_slot_9c": "Cyfrowy podpis", - "s_slot_9d": "Menedżer kluczy", - "s_slot_9e": "Autoryzacja karty", - "s_retired_slot": null, + "s_slot_9c": "Podpis cyfrowy", + "s_slot_9d": "Zarządzanie kluczem", + "s_slot_9e": "Uwierzytelnianie kartą", + "s_retired_slot": "Emerytowane kluczowe kierownictwo", "@_otp_slots": {}, "s_otp_slot_one": "Krótkie dotknięcie", @@ -713,21 +712,21 @@ "@_otp_slot_configurations": {}, "l_yubiotp_desc": "Zaprogramuj poświadczenie Yubico OTP", - "s_challenge_response": "Wyzwanie i odpowiedź", - "l_challenge_response_desc": "Zaprogramuj poświadczenie typu challenge-response", + "s_challenge_response": "Challenge-response", + "l_challenge_response_desc": "Zaprogramuj poświadczenie challenge-response", "s_static_password": "Hasło statyczne", "l_static_password_desc": "Skonfiguruj hasło statyczne", "s_hotp": "OATH-HOTP", - "l_hotp_desc": "Zaprogramuj poświadczenie oparte na HMAC-SHA1", - "s_public_id": "Publiczne ID", - "s_private_id": "Prywatne ID", + "l_hotp_desc": "Zaprogramuj poświadczenie HMAC-SHA1", + "s_public_id": "Public ID", + "s_private_id": "Private ID", "s_use_serial": "Użyj numeru seryjnego", "l_select_file": "Wybierz plik", "l_no_export_file": "Brak pliku eksportu", "s_no_export": "Brak eksportu", "s_export": "Eksportuj", "l_export_configuration_file": "Eksportuj konfigurację do pliku", - "l_exported_can_be_uploaded_at": null, + "l_exported_can_be_uploaded_at": "Wyeksportowane poświadczenia może zostać przesłane na stronie {url}", "@_export_can_be_uploaded_at": { "placeholders": { "url": {} @@ -737,25 +736,25 @@ "@_otp_slot_actions": {}, "s_delete_slot": "Usuń poświadczenie", "l_delete_slot_desc": "Usuń poświadczenie ze slotu", - "p_warning_delete_slot_configuration": "Ostrzeżenie! Ta akcja spowoduje trwałe usunięcie poświadczenia ze slotu {slot_id}.", + "p_warning_delete_slot_configuration": "Ostrzeżenie! Spowoduje to trwałe usunięcie poświadczenia ze slotu {slot_id}.", "@p_warning_delete_slot_configuration": { "placeholders": { "slot_id": {} } }, "l_slot_deleted": "Poświadczenie zostało usunięte", - "s_swap": "Zamień", - "s_swap_slots": "Zamiana slotów", - "l_swap_slots_desc": "Zamień krótkie/długie dotknięcie", - "p_swap_slots_desc": "Spowoduje to zamianę konfiguracji dwóch gniazd.", - "l_slots_swapped": "Konfiguracje gniazd zostały zamienione", - "l_slot_credential_configured": "Skonfigurowano poświadczenie {type}", + "s_swap": "Zmień", + "s_swap_slots": "Zmień sloty", + "l_swap_slots_desc": "Zmień krótkie / długie dotknięcie", + "p_swap_slots_desc": "Spowoduje to zmianę miejsc slotów.", + "l_slots_swapped": "Sloty zostały zmienione", + "l_slot_credential_configured": "Poświadczenie {type} zostało skonfigurowane", "@l_slot_credential_configured": { "placeholders": { "type": {} } }, - "l_slot_credential_configured_and_exported": "Skonfigurowano poświadczenie {type} i wyeksportowano do {file}", + "l_slot_credential_configured_and_exported": "Poświadczenie {type} zostało skonfigurowane i wyeksportowane do pliku {file}", "@l_slot_credential_configured_and_exported": { "placeholders": { "type": {}, @@ -763,17 +762,17 @@ } }, "s_append_enter": "Dołącz ⏎", - "l_append_enter_desc": "Dołącz naciśnięcie klawisza Enter po wysłaniu OTP", + "l_append_enter_desc": "Dołącz klawisz Enter po wysłaniu kodu OTP", "@_otp_errors": {}, - "p_otp_swap_error": null, - "l_wrong_access_code": null, + "p_otp_swap_error": "Nie udało się zamienić gniazd! Upewnij się, że klucz YubiKey nie ma ograniczonego dostępu.", + "l_wrong_access_code": "Kod dostępu jest nieprawidłowy", "@_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": "Kod dostępu", + "s_show_access_code": "Pokaż kod dostępu", + "s_hide_access_code": "Ukryj kod dostępu", + "p_enter_access_code": "Wpisz kod dostępu slotu {slot}.", "@p_enter_access_code": { "placeholders": { "slot": {} @@ -783,28 +782,28 @@ "@_permissions": {}, "s_enable_nfc": "Włącz NFC", - "s_request_access": null, + "s_request_access": "Żądanie dostępu", "s_permission_denied": "Odmowa dostępu", "l_elevating_permissions": "Podnoszenie uprawnień\u2026", "s_review_permissions": "Przegląd uprawnień", - "s_open_windows_settings": null, - "l_admin_privileges_required": "Wymagane uprawnienia administratora", + "s_open_windows_settings": "Otwórz ustawienia systemu Windows", + "l_admin_privileges_required": "Uprawnienia administratora są wymagane", "p_elevated_permissions_required": "Zarządzanie tym urządzeniem wymaga podwyższonych uprawnień.", "p_webauthn_elevated_permissions_required": "Zarządzanie WebAuthn wymaga podwyższonych uprawnień.", - "l_ms_store_permission_note": null, + "l_ms_store_permission_note": "Wersja aplikacji ze sklepu Microsoft Store może nie być w stanie podnieść uprawnień", "p_need_camera_permission": "Yubico Authenticator wymaga dostępu do aparatu w celu skanowania kodów QR.", "@_qr_codes": {}, - "s_qr_scan": "Skanuj kod QR", - "l_invalid_qr": "Nieprawidłowy kod QR", + "s_qr_scan": "Zeskanuj kod QR", + "l_invalid_qr": "Kod QR jest nieprawidłowy", "l_qr_not_found": "Nie znaleziono kodu QR", - "l_qr_file_too_large": "Zbyt duży plik (maks. {max})", + "l_qr_file_too_large": "Plik jest zbyt duży (maksymalnie {max})", "@l_qr_file_too_large": { "placeholders": { "max": {} } }, - "l_qr_invalid_image_file": "Nieprawidłowy plik obrazu", + "l_qr_invalid_image_file": "Plik obrazu jest nieprawidłowy", "l_qr_select_file": "Wybierz plik z kodem QR", "l_qr_not_read": "Odczytanie kodu QR nie powiodło się: {message}", "@l_qr_not_read": { @@ -815,28 +814,28 @@ "l_point_camera_scan": "Skieruj aparat na kod QR, by go zeskanować", "q_want_to_scan": "Czy chcesz zeskanować?", "q_no_qr": "Nie masz kodu QR?", - "s_enter_manually": "Wprowadź ręcznie", + "s_enter_manually": "Wpisz ręcznie", "s_read_from_file": "Odczytaj z pliku", "@_factory_reset": {}, "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}", + "l_factory_reset_desc": "Przywróć ustawienia domyślne klucza YubiKey", + "l_factory_reset_required": "Wymagane przywrócenie ustawień fabrycznych", + "l_oath_application_reset": "Resetowanie aplikacji OATH", + "l_fido_app_reset": "Resetowanie aplikacji FIDO", + "l_reset_failed": "Wystąpił błąd podczas resetowania: {message}", "@l_reset_failed": { "placeholders": { "message": {} } }, - "l_piv_app_reset": "Funkcja PIV została zresetowana", - "p_factory_reset_an_app": "Zresetuj funkcję na YubiKey do ustawień fabrycznych.", + "l_piv_app_reset": "Resetowanie aplikacji PIV", + "p_factory_reset_an_app": "Zresetuj aplikację klucza YubiKey do ustawień fabrycznych.", "p_factory_reset_desc": "Dane są przechowywane w wielu funkcjach YubiKey. Niektóre z tych funkcji można zresetować niezależnie od siebie.\n\nWybierz funkcję powyżej, aby ją zresetować.", - "p_warning_factory_reset": "Uwaga! Spowoduje to nieodwracalne usunięcie wszystkich kont OATH TOTP/HOTP z klucza YubiKey.", + "p_warning_factory_reset": "Ostrzeżenie! Spowoduje to nieodwracalne usunięcie wszystkich kont OATH TOTP/HOTP z klucza YubiKey.", "p_warning_disable_credentials": "Twoje poświadczenia OATH, jak również wszelkie ustawione hasła, zostaną usunięte z tego klucza YubiKey. Upewnij się, że najpierw wyłączono je w odpowiednich witrynach internetowych, aby uniknąć zablokowania kont.", - "p_warning_deletes_accounts": "Uwaga! Spowoduje to nieodwracalne usunięcie wszystkich kont U2F i FIDO2 z klucza YubiKey.", + "p_warning_deletes_accounts": "Ostrzeżenie! Spowoduje to nieodwracalne usunięcie wszystkich kont U2F i FIDO2 z klucza YubiKey.", "p_warning_disable_accounts": "Twoje poświadczenia, a także wszelkie ustawione kody PIN, zostaną usunięte z tego klucza YubiKey. Upewnij się, że najpierw wyłączono je w odpowiednich witrynach internetowych, aby uniknąć zablokowania kont.", "p_warning_piv_reset": "Ostrzeżenie! Wszystkie dane przechowywane dla PIV zostaną nieodwracalnie usunięte z klucza YubiKey.", "p_warning_piv_reset_desc": "Obejmuje to klucze prywatne i certyfikaty. Kod PIN, PUK i klucz zarządzania zostaną zresetowane do domyślnych wartości fabrycznych.", @@ -844,12 +843,12 @@ "p_warning_global_reset_desc": "Zresetuj funkcje na swoim YubiKey. PIN zostanie zresetowany do wartości domyślnej i zarejestrowane odciski palców zostaną usunięte. Wszelkie klucze, certyfikaty czy inne dane uwierzytelniające zostaną trwale usunięte.", "@_copy_to_clipboard": {}, - "l_copy_to_clipboard": "Skopiuj do schowka", - "s_code_copied": "Kod skopiowany", - "l_code_copied_clipboard": "Kod skopiowany do schowka", + "l_copy_to_clipboard": "Kopiuj do schowka", + "s_code_copied": "Kod został skopiowany", + "l_code_copied_clipboard": "Kod został skopiowany do schowka", "s_copy_log": "Kopiuj logi", - "l_log_copied": "Logi skopiowane do schowka", - "l_diagnostics_copied": "Dane diagnostyczne skopiowane do schowka", + "l_log_copied": "Logi zostały skopiowane do schowka", + "l_diagnostics_copied": "Dane diagnostyczne zostały skopiowane do schowka", "p_target_copied_clipboard": "{label} skopiowano do schowka.", "@p_target_copied_clipboard": { "placeholders": { @@ -860,87 +859,64 @@ "@_custom_icons": {}, "s_custom_icons": "Niestandardowe ikony", "l_set_icons_for_accounts": "Ustaw ikony dla kont", - "p_custom_icons_description": "Pakiety ikon mogą sprawić, że Twoje konta będą łatwiejsze do odróżnienia dzięki znanym logo i kolorom.", - "s_replace_icon_pack": "Zastąp pakiet ikon", - "l_loading_icon_pack": "Wczytywanie pakietu ikon\u2026", - "s_load_icon_pack": "Wczytaj pakiet ikon", + "p_custom_icons_description": "Pakiety ikon mogą sprawić, że Twoje konta będą łatwiejsze do rozróżnienia dzięki znanym logo i kolorom.", + "s_replace_icon_pack": "Zmień pakiet ikon", + "l_loading_icon_pack": "Ładowanie pakietu ikon\u2026", + "s_load_icon_pack": "Wybierz pakiet ikon", "s_remove_icon_pack": "Usuń pakiet ikon", - "l_icon_pack_removed": "Usunięto pakiet ikon", - "l_remove_icon_pack_failed": "Błąd podczas usuwania pakietu ikon", + "l_icon_pack_removed": "Pakiet ikon został usunięty", + "l_remove_icon_pack_failed": "Wystąpił błąd podczas usuwania pakietu ikon", "s_choose_icon_pack": "Wybierz pakiet ikon", - "l_icon_pack_imported": "Zaimportowano pakiet ikon", - "l_import_icon_pack_failed": "Błąd importu pakietu ikon: {message}", + "l_icon_pack_imported": "Pakiet ikon został zaimportowany", + "l_import_icon_pack_failed": "Wystąpił błąd podczas importowania pakietu ikon: {message}", "@l_import_icon_pack_failed": { "placeholders": { "message": {} } }, - "l_invalid_icon_pack": "Nieprawidłowy pakiet ikon", - "l_icon_pack_copy_failed": "Nie udało się skopiować plików z pakietu ikon", + "l_invalid_icon_pack": "Pakiet ikon jest nieprawidłowy", + "l_icon_pack_copy_failed": "Nie udało się skopiować plików pakietu ikon", "@_android_settings": {}, "s_nfc_options": "Opcje NFC", - "l_on_yk_nfc_tap": "Podczas kontaktu YubiKey z NFC", + "l_on_yk_nfc_tap": "Podczas zbliżenia klucza YubiKey przez NFC", "l_do_nothing": "Nic nie rób", "l_launch_ya": "Uruchom Yubico Authenticator", - "l_copy_otp_clipboard": "Skopiuj OTP do schowka", - "l_launch_and_copy_otp": "Uruchom aplikację i skopiuj OTP", + "l_copy_otp_clipboard": "Kopiuj kod OTP do schowka", + "l_launch_and_copy_otp": "Uruchom aplikację i kopiuj kod OTP", "l_kbd_layout_for_static": "Układ klawiatury (dla hasła statycznego)", "s_choose_kbd_layout": "Wybierz układ klawiatury", - "l_bypass_touch_requirement": "Obejdź wymóg dotknięcia", - "l_bypass_touch_requirement_on": "Konta, które wymagają dotknięcia, są automatycznie wyświetlane przez NFC", - "l_bypass_touch_requirement_off": "Konta, które wymagają dotknięcia, potrzebują dodatkowego przyłożenia do NFC", - "s_silence_nfc_sounds": "Dźwięk NFC", - "l_silence_nfc_sounds_on": "Nie będzie odtwarzany", - "l_silence_nfc_sounds_off": "Będzie odtwarzany", + "l_bypass_touch_requirement": "Pomiń wymagane dotknięcie", + "l_bypass_touch_requirement_on": "Konta, które wymagają dotknięcia będą automatycznie wyświetlane przez NFC", + "l_bypass_touch_requirement_off": "Konta, które wymagają dotknięcia będą wymagały dodatkowego zbliżenia przez NFC", + "s_silence_nfc_sounds": "Wycisz dźwięki NFC", + "l_silence_nfc_sounds_on": "Dźwięk zbliżenia NFC nie będzie odtwarzany", + "l_silence_nfc_sounds_off": "Dźwięk zbliżenia NFC będzie odtwarzany", "s_usb_options": "Opcje USB", - "l_launch_app_on_usb": "Uruchom po podłączeniu YubiKey", - "l_launch_app_on_usb_on": "Inne aplikacje nie mogą korzystać z YubiKey przez USB", - "l_launch_app_on_usb_off": "Inne aplikacje mogą korzystać z YubiKey przez USB", + "l_launch_app_on_usb": "Uruchom po podłączeniu klucza YubiKey", + "l_launch_app_on_usb_on": "Inne aplikacje mogą korzystać z klucza YubiKey przez USB", + "l_launch_app_on_usb_off": "Inne aplikacje mogą korzystać z klucza YubiKey przez USB", "s_allow_screenshots": "Zezwalaj na zrzuty ekranu", - "l_nfc_dialog_tap_key": null, - "s_nfc_dialog_operation_success": "Powodzenie", - "s_nfc_dialog_operation_failed": "Niepowodzenie", - - "s_nfc_dialog_oath_reset": "Działanie: resetuj aplet OATH", - "s_nfc_dialog_oath_unlock": "Działanie: odblokuj aplet OATH", - "s_nfc_dialog_oath_set_password": "Działanie: ustaw hasło OATH", - "s_nfc_dialog_oath_unset_password": "Działanie: usuń hasło OATH", - "s_nfc_dialog_oath_add_account": "Działanie: dodaj nowe konto", - "s_nfc_dialog_oath_rename_account": "Działanie: zmień nazwę konta", - "s_nfc_dialog_oath_delete_account": "Działanie: usuń konto", - "s_nfc_dialog_oath_calculate_code": "Działanie: oblicz kod OATH", - "s_nfc_dialog_oath_failure": "Operacja OATH nie powiodła się", - "s_nfc_dialog_oath_add_multiple_accounts": "Działanie: dodawanie wielu kont", - - "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, - "@_nfc": {}, - "s_nfc_ready_to_scan": null, - "s_nfc_hold_still": null, - "s_nfc_tap_your_yubikey": null, - "l_nfc_failed_to_scan": null, + "s_nfc_ready_to_scan": "Gotowy do skanowania", + "s_nfc_hold_still": "Nie ruszaj się\u2026", + "s_nfc_tap_your_yubikey": "Dotknij przycisku YubiKey", + "l_nfc_failed_to_scan": "Skanowanie nie powiodło się, spróbuj ponownie", "@_ndef": {}, - "p_ndef_set_otp": "OTP zostało skopiowane do schowka.", - "p_ndef_set_password": "Hasło statyczne zostało skopiowane do schowka.", - "p_ndef_parse_failure": "Błąd czytania OTP z YubiKey.", - "p_ndef_set_clip_failure": "Błąd kopiowania OTP do schowka.", + "p_ndef_set_otp": "Kod OTP został skopiowany do schowka.", + "p_ndef_set_password": "Hasło zostało skopiowane do schowka.", + "p_ndef_parse_failure": "Nie udało się odczytać kodu OTP z klucza YubiKey.", + "p_ndef_set_clip_failure": "Wystąpił błąd podczas kopiowania kodu OTP do schowka.", "@_key_customization": {}, - "s_set_label": null, - "s_set_color": 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": "Ustaw etykietę", + "s_set_color": "Ustaw kolor", + "s_change_label": "Zmień etykietę", + "s_color": "Kolor", + "p_set_will_add_custom_name": "Spowoduje to nazwanie klucza YubiKey.", + "p_rename_will_change_custom_name": "Spowoduje to zmianę etykiety klucza YubiKey.", "@_eof": {} } diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index d64b6cffc..7c5c2b616 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -38,7 +38,7 @@ "s_calculate": "Tính toán", "s_import": "Nhập khẩu", "s_overwrite": "Ghi đè", - "s_done": null, + "s_done": "Xong", "s_label": "Nhãn", "s_name": "Tên", "s_usb": "USB", @@ -47,11 +47,11 @@ "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_show_navigation": "Hiển thị điều hướng", "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, + "s_show_menu": "Hiển thị thực đơn", + "s_hide_menu": "Ẩn thực đơn", "q_rename_target": "Đổi tên {label}?", "@q_rename_target": { "placeholders": { @@ -203,7 +203,6 @@ "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": { @@ -237,8 +236,8 @@ "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, + "l_configuration_unsupported": "Cấu hình không được hỗ trợ", + "p_scp_unsupported": "Để giao tiếp qua NFC, YubiKey yêu cầu công nghệ không được điện thoại này hỗ trợ. Vui lòng cắm YubiKey vào cổng USB của điện thoại.", "@_general_errors": {}, "l_error_occurred": "Đã xảy ra lỗi", @@ -430,10 +429,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_add_account_password_required": "Yêu cầu mật khẩu", + "l_add_account_unlock_required": "Yêu cầu mở khóa", + "l_add_account_already_exists": "Tài khoản đã tồn tại", + "l_add_account_func_missing": "Chức năng bị thiếu hoặc bị vô hiệu hóa", "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", @@ -447,7 +446,7 @@ "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": "Đổi tên tài khoản không thành công: {message}", "@l_rename_account_failed": { "placeholders": { "message": {} @@ -600,7 +599,7 @@ "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, + "l_change_defaults": "Thay đổi mã truy cập mặc định", "s_issuer": "Nhà phát hành", "s_serial": "Số serial", "s_certificate_fingerprint": "Dấu vân tay", @@ -899,34 +898,11 @@ "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, + "s_nfc_ready_to_scan": "Sẵn sàng để quét", + "s_nfc_hold_still": "Giữ yên\u2026", + "s_nfc_tap_your_yubikey": "Nhấn vào YubiKey của bạn", + "l_nfc_failed_to_scan": "Không quét được, hãy thử lại", "@_ndef": {}, "p_ndef_set_otp": "Đã sao chép mã OTP từ YubiKey vào clipboard.", diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index 4a88bcfb3..6504d000d 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_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. @@ -40,7 +40,6 @@ import '../../widgets/app_text_field.dart'; import '../../widgets/choice_filter_chip.dart'; import '../../widgets/file_drop_overlay.dart'; import '../../widgets/file_drop_target.dart'; -import '../../widgets/focus_utils.dart'; import '../../widgets/responsive_dialog.dart'; import '../../widgets/utf8_utils.dart'; import '../keys.dart' as keys; @@ -74,6 +73,9 @@ class _OathAddAccountPageState extends ConsumerState { final _issuerController = TextEditingController(); final _accountController = TextEditingController(); final _secretController = TextEditingController(); + final _issuerFocus = FocusNode(); + final _accountFocus = FocusNode(); + final _secretFocus = FocusNode(); final _periodController = TextEditingController(text: '$defaultPeriod'); UserInteractionController? _promptController; Uri? _otpauthUri; @@ -88,6 +90,7 @@ class _OathAddAccountPageState extends ConsumerState { List _periodValues = [20, 30, 45, 60]; List _digitsValues = [6, 8]; List? _credentials; + bool _submitting = false; @override void dispose() { @@ -95,6 +98,9 @@ class _OathAddAccountPageState extends ConsumerState { _accountController.dispose(); _secretController.dispose(); _periodController.dispose(); + _issuerFocus.dispose(); + _accountFocus.dispose(); + _secretFocus.dispose(); super.dispose(); } @@ -121,6 +127,7 @@ class _OathAddAccountPageState extends ConsumerState { _counter = data.counter; _isObscure = true; _dataLoaded = true; + _submitting = false; }); } @@ -128,8 +135,6 @@ class _OathAddAccountPageState extends ConsumerState { {DevicePath? devicePath, required Uri credUri}) async { final l10n = AppLocalizations.of(context)!; try { - FocusUtils.unfocus(context); - if (devicePath == null) { assert(isAndroid, 'devicePath is only optional for Android'); await ref @@ -272,6 +277,14 @@ class _OathAddAccountPageState extends ConsumerState { void submit() async { if (secretLengthValid && secretFormatValid) { + _issuerFocus.unfocus(); + _accountFocus.unfocus(); + _secretFocus.unfocus(); + + setState(() { + _submitting = true; + }); + final cred = CredentialData( issuer: issuerText.isEmpty ? null : issuerText, name: nameText, @@ -302,6 +315,10 @@ class _OathAddAccountPageState extends ConsumerState { }, ); } + + setState(() { + _submitting = false; + }); } else { setState(() { _validateSecret = true; @@ -372,8 +389,7 @@ class _OathAddAccountPageState extends ConsumerState { decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_issuer_optional, - helperText: - '', // Prevents dialog resizing when disabled + helperText: '', // Prevents dialog resizing when errorText: (byteLength(issuerText) > issuerMaxLength) ? '' // needs empty string to render as error : issuerNoColon @@ -382,6 +398,7 @@ class _OathAddAccountPageState extends ConsumerState { prefixIcon: const Icon(Symbols.business), ), textInputAction: TextInputAction.next, + focusNode: _issuerFocus, onChanged: (value) { setState(() { // Update maxlengths @@ -400,19 +417,22 @@ class _OathAddAccountPageState extends ConsumerState { decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_account_name, - helperText: '', - // Prevents dialog resizing when disabled - errorText: (byteLength(nameText) > nameMaxLength) - ? '' // needs empty string to render as error - : isUnique - ? null - : l10n.l_name_already_exists, + helperText: + '', // Prevents dialog resizing when disabled + errorText: _submitting + ? null + : (byteLength(nameText) > nameMaxLength) + ? '' // needs empty string to render as error + : isUnique + ? null + : l10n.l_name_already_exists, prefixIcon: const Icon(Symbols.person), ), textInputAction: TextInputAction.next, + focusNode: _accountFocus, onChanged: (value) { setState(() { - // Update maxlengths + // Update max lengths }); }, onSubmitted: (_) { @@ -452,6 +472,7 @@ class _OathAddAccountPageState extends ConsumerState { )), readOnly: _dataLoaded, textInputAction: TextInputAction.done, + focusNode: _secretFocus, onChanged: (value) { setState(() { _validateSecret = false; diff --git a/lib/oath/views/manage_password_dialog.dart b/lib/oath/views/manage_password_dialog.dart index cc77e3081..782db8fba 100755 --- a/lib/oath/views/manage_password_dialog.dart +++ b/lib/oath/views/manage_password_dialog.dart @@ -22,10 +22,10 @@ import 'package:material_symbols_icons/symbols.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; +import '../../exception/cancellation_exception.dart'; import '../../management/models.dart'; import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; -import '../../widgets/focus_utils.dart'; import '../../widgets/responsive_dialog.dart'; import '../keys.dart' as keys; import '../models.dart'; @@ -63,26 +63,37 @@ class _ManagePasswordDialogState extends ConsumerState { super.dispose(); } + void _removeFocus() { + _currentPasswordFocus.unfocus(); + _newPasswordFocus.unfocus(); + _confirmPasswordFocus.unfocus(); + } + _submit() async { - FocusUtils.unfocus(context); + _removeFocus(); - final result = await ref - .read(oathStateProvider(widget.path).notifier) - .setPassword(_currentPasswordController.text, _newPassword); - if (result) { - if (mounted) { - await ref.read(withContextProvider)((context) async { - Navigator.of(context).pop(); - showMessage(context, AppLocalizations.of(context)!.s_password_set); + try { + final result = await ref + .read(oathStateProvider(widget.path).notifier) + .setPassword(_currentPasswordController.text, _newPassword); + if (result) { + if (mounted) { + await ref.read(withContextProvider)((context) async { + Navigator.of(context).pop(); + showMessage(context, AppLocalizations.of(context)!.s_password_set); + }); + } + } 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; - }); + } on CancellationException catch (_) { + // ignored } } @@ -171,6 +182,8 @@ class _ManagePasswordDialogState extends ConsumerState { onPressed: _currentPasswordController.text.isNotEmpty && !_currentIsWrong ? () async { + _removeFocus(); + final result = await ref .read(oathStateProvider(widget.path).notifier) .unsetPassword( diff --git a/lib/oath/views/rename_account_dialog.dart b/lib/oath/views/rename_account_dialog.dart index c3eee142f..6bbfbdb33 100755 --- a/lib/oath/views/rename_account_dialog.dart +++ b/lib/oath/views/rename_account_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. @@ -28,7 +28,6 @@ import '../../desktop/models.dart'; import '../../exception/cancellation_exception.dart'; import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_form_field.dart'; -import '../../widgets/focus_utils.dart'; import '../../widgets/responsive_dialog.dart'; import '../../widgets/utf8_utils.dart'; import '../keys.dart' as keys; @@ -68,49 +67,15 @@ class RenameAccountDialog extends ConsumerStatefulWidget { OathCredential credential, List<(String? issuer, String name)> existing) { return RenameAccountDialog( - devicePath: devicePath, - issuer: credential.issuer, - name: credential.name, - oathType: credential.oathType, - period: credential.period, - existing: existing, - rename: (issuer, name) async { - final withContext = ref.read(withContextProvider); - try { - // Rename credentials - final renamed = await ref - .read(credentialListProvider(devicePath).notifier) - .renameAccount(credential, issuer, name); - - // Update favorite - ref - .read(favoritesProvider.notifier) - .renameCredential(credential.id, renamed.id); - - await withContext((context) async => showMessage( - context, AppLocalizations.of(context)!.s_account_renamed)); - return renamed; - } on CancellationException catch (_) { - // ignored - } catch (e) { - _log.error('Failed to add account', e); - final String errorMessage; - // TODO: Make this cleaner than importing desktop specific RpcError. - if (e is RpcError) { - errorMessage = e.message; - } else { - errorMessage = e.toString(); - } - await withContext((context) async => showMessage( - context, - AppLocalizations.of(context)! - .l_rename_account_failed(errorMessage), - duration: const Duration(seconds: 4), - )); - return null; - } - }, - ); + devicePath: devicePath, + issuer: credential.issuer, + name: credential.name, + oathType: credential.oathType, + period: credential.period, + existing: existing, + rename: (issuer, name) async => await ref + .read(credentialListProvider(devicePath).notifier) + .renameAccount(credential, issuer, name)); } } @@ -118,6 +83,9 @@ class _RenameAccountDialogState extends ConsumerState { late String _issuer; late String _name; + final _issuerFocus = FocusNode(); + final _nameFocus = FocusNode(); + @override void initState() { super.initState(); @@ -125,12 +93,50 @@ class _RenameAccountDialogState extends ConsumerState { _name = widget.name.trim(); } + @override + void dispose() { + _issuerFocus.dispose(); + _nameFocus.dispose(); + super.dispose(); + } + void _submit() async { - FocusUtils.unfocus(context); + _issuerFocus.unfocus(); + _nameFocus.unfocus(); final nav = Navigator.of(context); - final renamed = - await widget.rename(_issuer.isNotEmpty ? _issuer : null, _name); - nav.pop(renamed); + final withContext = ref.read(withContextProvider); + + try { + // Rename credentials + final renamed = + await widget.rename(_issuer.isNotEmpty ? _issuer : null, _name); + + // Update favorite + ref + .read(favoritesProvider.notifier) + .renameCredential(renamed.id, renamed.id); + + await withContext((context) async => showMessage( + context, AppLocalizations.of(context)!.s_account_renamed)); + + nav.pop(renamed); + } on CancellationException catch (_) { + // ignored + } catch (e) { + _log.error('Failed to rename account', e); + final String errorMessage; + // TODO: Make this cleaner than importing desktop specific RpcError. + if (e is RpcError) { + errorMessage = e.message; + } else { + errorMessage = e.toString(); + } + await withContext((context) async => showMessage( + context, + AppLocalizations.of(context)!.l_rename_account_failed(errorMessage), + duration: const Duration(seconds: 4), + )); + } } @override @@ -188,6 +194,8 @@ class _RenameAccountDialogState extends ConsumerState { prefixIcon: const Icon(Symbols.business), ), textInputAction: TextInputAction.next, + focusNode: _issuerFocus, + autofocus: true, onChanged: (value) { setState(() { _issuer = value.trim(); @@ -212,6 +220,7 @@ class _RenameAccountDialogState extends ConsumerState { prefixIcon: const Icon(Symbols.people_alt), ), textInputAction: TextInputAction.done, + focusNode: _nameFocus, onChanged: (value) { setState(() { _name = value.trim(); diff --git a/lib/theme.dart b/lib/theme.dart index d929fa31f..b2d82e13e 100755 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Yubico. + * Copyright (C) 2021-2024 Yubico. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; const defaultPrimaryColor = Colors.lightGreen; @@ -50,6 +51,9 @@ class AppTheme { fontFamily: 'Roboto', appBarTheme: const AppBarTheme( color: Colors.transparent, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarIconBrightness: Brightness.dark, + statusBarColor: Colors.transparent), ), listTileTheme: const ListTileThemeData( // For alignment under menu button @@ -81,6 +85,9 @@ class AppTheme { scaffoldBackgroundColor: colorScheme.surface, appBarTheme: const AppBarTheme( color: Colors.transparent, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarIconBrightness: Brightness.light, + statusBarColor: Colors.transparent), ), listTileTheme: const ListTileThemeData( // For alignment under menu button diff --git a/lib/version.dart b/lib/version.dart index 936a73055..b28490e82 100755 --- a/lib/version.dart +++ b/lib/version.dart @@ -1,5 +1,5 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // This file is generated by running ./set-version.py -const String version = '7.0.2-dev.0'; -const int build = 70002; +const String version = '7.1.1-dev.0'; +const int build = 70101; diff --git a/pubspec.yaml b/pubspec.yaml index aa71eb5bf..0ff6a4a87 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # This field is updated by running ./set-version.py # DO NOT MANUALLY EDIT THIS! -version: 7.0.2-dev.0+70002 +version: 7.1.1-dev.0+70101 environment: sdk: '>=3.4.3 <4.0.0' diff --git a/resources/win/release-win.ps1 b/resources/win/release-win.ps1 index f9bf9bc9a..0bdca893e 100644 --- a/resources/win/release-win.ps1 +++ b/resources/win/release-win.ps1 @@ -1,4 +1,4 @@ -$version="7.0.2-dev.0" +$version="7.1.1-dev.0" echo "Clean-up of old files" rm *.msi diff --git a/resources/win/yubioath-desktop.wxs b/resources/win/yubioath-desktop.wxs index fc7895cc1..d9f4918ab 100644 --- a/resources/win/yubioath-desktop.wxs +++ b/resources/win/yubioath-desktop.wxs @@ -1,7 +1,7 @@ - +