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 3b97f177..d617c13d 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 @@ -112,6 +112,8 @@ class FidoManager( private val pinStore = FidoPinStore() + private var pinRetries : Int? = null + private val resetHelper = FidoResetHelper(deviceManager, fidoViewModel, mainViewModel, connectionHelper, pinStore) @@ -124,6 +126,8 @@ class FidoManager( } init { + pinRetries = null + deviceManager.addDeviceListener(this) fidoChannel.setHandler(coroutineScope) { method, args -> @@ -239,10 +243,14 @@ class FidoManager( connectionHelper.cancelPending() } + val infoData = fidoSession.cachedInfo + val clientPin = + ClientPin(fidoSession, getPreferredPinUvAuthProtocol(infoData)) + + pinRetries = if (infoData.options["clientPin"] == true) clientPin.pinRetries.count else null + fidoViewModel.setSessionState( - Session( - fidoSession.cachedInfo, pinStore.hasPin() - ) + Session(infoData, pinStore.hasPin(), pinRetries) ) // Update deviceInfo since the deviceId has changed @@ -275,8 +283,6 @@ class FidoManager( pin: CharArray ): String { - //fidoViewModel.setSessionLoadingState() - val pinPermissionsCM = getPinPermissionsCM(fidoSession) val pinPermissionsBE = getPinPermissionsBE(fidoSession) val permissions = pinPermissionsCM or pinPermissionsBE @@ -298,16 +304,23 @@ class FidoManager( pinStore.setPin(pin) + pinRetries = clientPin.pinRetries.count + fidoViewModel.setSessionState( Session( fidoSession.info, - pinStore.hasPin() + pinStore.hasPin(), + pinRetries ) ) return JSONObject(mapOf("success" to true)).toString() } - private fun catchPinErrors(clientPin: ClientPin, block: () -> String): String = + private fun catchPinErrors( + fidoSession: YubiKitFidoSession, + clientPin: ClientPin, + block: () -> String + ): String = try { block() } catch (ctapException: CtapException) { @@ -318,6 +331,15 @@ class FidoManager( ) { pinStore.setPin(null) fidoViewModel.updateCredentials(emptyList()) + pinRetries = clientPin.pinRetries.count + + fidoViewModel.setSessionState( + Session( + fidoSession.info, + pinStore.hasPin(), + pinRetries + ) + ) if (ctapException.ctapError == CtapException.ERR_PIN_POLICY_VIOLATION) { JSONObject( @@ -330,7 +352,7 @@ class FidoManager( JSONObject( mapOf( "success" to false, - "pinRetries" to clientPin.pinRetries.count, + "pinRetries" to pinRetries, "authBlocked" to (ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED), ) ).toString() @@ -347,7 +369,7 @@ class FidoManager( val clientPin = ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)) - catchPinErrors(clientPin) { + catchPinErrors(fidoSession, clientPin) { unlockSession(fidoSession, clientPin, pin) } } catch (e: IOException) { @@ -383,7 +405,7 @@ class FidoManager( val clientPin = ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)) - catchPinErrors(clientPin) { + catchPinErrors(fidoSession, clientPin) { setOrChangePin(fidoSession, clientPin, pin, newPin) unlockSession(fidoSession, clientPin, newPin) } @@ -489,7 +511,7 @@ class FidoManager( val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token) bioEnrollment.removeEnrollment(HexCodec.hexStringToBytes(templateId)) fidoViewModel.removeFingerprint(templateId) - fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin())) + fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin(), pinRetries)) return@useSession JSONObject( mapOf( "success" to true, @@ -513,7 +535,7 @@ class FidoManager( val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token) bioEnrollment.setName(HexCodec.hexStringToBytes(templateId), name) fidoViewModel.renameFingerprint(templateId, name) - fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin())) + fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin(), pinRetries)) return@useSession JSONObject( mapOf( "success" to true, @@ -592,7 +614,7 @@ class FidoManager( val templateIdHexString = HexCodec.bytesToHexString(templateId) fidoViewModel.addFingerprint(FidoFingerprint(templateIdHexString, name)) - fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin())) + fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin(), pinRetries)) return@useSession JSONObject( mapOf( 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 3f7ea04f..b6c883e1 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 @@ -221,7 +221,7 @@ class FidoResetHelper( private fun doReset(fidoSession: YubiKitFidoSession) { logger.debug("Calling FIDO reset") fidoSession.reset(resetCommandState) - fidoViewModel.setSessionState(Session(fidoSession.info, true)) + fidoViewModel.setSessionState(Session(fidoSession.info, true, null)) fidoViewModel.updateCredentials(emptyList()) pinStore.setPin(null) } diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoViewModel.kt b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoViewModel.kt index 52b7cf34..56f12207 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoViewModel.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoViewModel.kt @@ -23,7 +23,6 @@ import com.yubico.authenticator.ViewModelData import com.yubico.authenticator.fido.data.FidoCredential import com.yubico.authenticator.fido.data.FidoFingerprint import com.yubico.authenticator.fido.data.Session -import org.json.JSONObject class FidoViewModel : ViewModel() { private val _sessionState = MutableLiveData() @@ -39,10 +38,6 @@ class FidoViewModel : ViewModel() { _sessionState.postValue(ViewModelData.Empty) } - fun setSessionLoadingState() { - _sessionState.postValue(ViewModelData.Loading) - } - private val _credentials = MutableLiveData>() val credentials: LiveData> = _credentials 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 73e343cf..a0a07d40 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 @@ -86,12 +86,13 @@ data class SessionInfo( @Serializable data class Session( - @SerialName("info") val info: SessionInfo, - val unlocked: Boolean + val unlocked: Boolean, + @SerialName("pin_retries") + val pinRetries: Int? ) : JsonSerializable { - constructor(infoData: InfoData, unlocked: Boolean) : this( - SessionInfo(infoData), unlocked + constructor(infoData: InfoData, unlocked: Boolean, pinRetries: Int?) : this( + SessionInfo(infoData), unlocked, pinRetries ) override fun toJson(): String { diff --git a/helper/helper/fido.py b/helper/helper/fido.py index 8cabf635..b334eec9 100644 --- a/helper/helper/fido.py +++ b/helper/helper/fido.py @@ -88,7 +88,6 @@ class Ctap2Node(RpcNode): self.ctap = Ctap2(connection) self._info = self.ctap.info self.client_pin = ClientPin(self.ctap) - self._auth_blocked = False self._token = None def get_data(self): @@ -96,7 +95,6 @@ class Ctap2Node(RpcNode): logger.debug(f"Info: {self._info}") data = dict( info=asdict(self._info), - auth_blocked=self._auth_blocked, unlocked=self._token is not None, ) if self._info.options.get("clientPin"): @@ -190,7 +188,6 @@ class Ctap2Node(RpcNode): if e.code == CtapError.ERR.USER_ACTION_TIMEOUT: raise InactivityException() self._info = self.ctap.get_info() - self._auth_blocked = False self._token = None return dict() diff --git a/lib/desktop/fido/state.dart b/lib/desktop/fido/state.dart index 431b84a6..d886cfc9 100755 --- a/lib/desktop/fido/state.dart +++ b/lib/desktop/fido/state.dart @@ -153,6 +153,7 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier { return unlock(newPin); } on RpcError catch (e) { if (e.status == 'pin-validation') { + ref.invalidateSelf(); return PinResult.failed(FidoPinFailureReason.invalidPin( e.body['retries'], e.body['auth_blocked'])); } @@ -176,6 +177,7 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier { } on RpcError catch (e) { if (e.status == 'pin-validation') { _pinController.state = null; + ref.invalidateSelf(); return PinResult.failed(FidoPinFailureReason.invalidPin( e.body['retries'], e.body['auth_blocked'])); } diff --git a/lib/fido/models.dart b/lib/fido/models.dart index 4ed0adb4..8bb39ba2 100755 --- a/lib/fido/models.dart +++ b/lib/fido/models.dart @@ -27,7 +27,8 @@ class FidoState with _$FidoState { factory FidoState( {required Map info, - required bool unlocked}) = _FidoState; + required bool unlocked, + int? pinRetries}) = _FidoState; factory FidoState.fromJson(Map json) => _$FidoStateFromJson(json); @@ -47,6 +48,8 @@ class FidoState with _$FidoState { bool get alwaysUv => info['options']['alwaysUv'] == true; bool get forcePinChange => info['force_pin_change'] == true; + + bool get pinBlocked => pinRetries == 0; } @freezed diff --git a/lib/fido/models.freezed.dart b/lib/fido/models.freezed.dart index f36c1694..298fe0ae 100644 --- a/lib/fido/models.freezed.dart +++ b/lib/fido/models.freezed.dart @@ -22,6 +22,7 @@ FidoState _$FidoStateFromJson(Map json) { mixin _$FidoState { Map get info => throw _privateConstructorUsedError; bool get unlocked => throw _privateConstructorUsedError; + int? get pinRetries => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -34,7 +35,7 @@ abstract class $FidoStateCopyWith<$Res> { factory $FidoStateCopyWith(FidoState value, $Res Function(FidoState) then) = _$FidoStateCopyWithImpl<$Res, FidoState>; @useResult - $Res call({Map info, bool unlocked}); + $Res call({Map info, bool unlocked, int? pinRetries}); } /// @nodoc @@ -52,6 +53,7 @@ class _$FidoStateCopyWithImpl<$Res, $Val extends FidoState> $Res call({ Object? info = null, Object? unlocked = null, + Object? pinRetries = freezed, }) { return _then(_value.copyWith( info: null == info @@ -62,6 +64,10 @@ class _$FidoStateCopyWithImpl<$Res, $Val extends FidoState> ? _value.unlocked : unlocked // ignore: cast_nullable_to_non_nullable as bool, + pinRetries: freezed == pinRetries + ? _value.pinRetries + : pinRetries // ignore: cast_nullable_to_non_nullable + as int?, ) as $Val); } } @@ -74,7 +80,7 @@ abstract class _$$FidoStateImplCopyWith<$Res> __$$FidoStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({Map info, bool unlocked}); + $Res call({Map info, bool unlocked, int? pinRetries}); } /// @nodoc @@ -90,6 +96,7 @@ class __$$FidoStateImplCopyWithImpl<$Res> $Res call({ Object? info = null, Object? unlocked = null, + Object? pinRetries = freezed, }) { return _then(_$FidoStateImpl( info: null == info @@ -100,6 +107,10 @@ class __$$FidoStateImplCopyWithImpl<$Res> ? _value.unlocked : unlocked // ignore: cast_nullable_to_non_nullable as bool, + pinRetries: freezed == pinRetries + ? _value.pinRetries + : pinRetries // ignore: cast_nullable_to_non_nullable + as int?, )); } } @@ -108,7 +119,9 @@ class __$$FidoStateImplCopyWithImpl<$Res> @JsonSerializable() class _$FidoStateImpl extends _FidoState { _$FidoStateImpl( - {required final Map info, required this.unlocked}) + {required final Map info, + required this.unlocked, + this.pinRetries}) : _info = info, super._(); @@ -125,10 +138,12 @@ class _$FidoStateImpl extends _FidoState { @override final bool unlocked; + @override + final int? pinRetries; @override String toString() { - return 'FidoState(info: $info, unlocked: $unlocked)'; + return 'FidoState(info: $info, unlocked: $unlocked, pinRetries: $pinRetries)'; } @override @@ -138,13 +153,15 @@ class _$FidoStateImpl extends _FidoState { other is _$FidoStateImpl && const DeepCollectionEquality().equals(other._info, _info) && (identical(other.unlocked, unlocked) || - other.unlocked == unlocked)); + other.unlocked == unlocked) && + (identical(other.pinRetries, pinRetries) || + other.pinRetries == pinRetries)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash( - runtimeType, const DeepCollectionEquality().hash(_info), unlocked); + int get hashCode => Object.hash(runtimeType, + const DeepCollectionEquality().hash(_info), unlocked, pinRetries); @JsonKey(ignore: true) @override @@ -163,7 +180,8 @@ class _$FidoStateImpl extends _FidoState { abstract class _FidoState extends FidoState { factory _FidoState( {required final Map info, - required final bool unlocked}) = _$FidoStateImpl; + required final bool unlocked, + final int? pinRetries}) = _$FidoStateImpl; _FidoState._() : super._(); factory _FidoState.fromJson(Map json) = @@ -174,6 +192,8 @@ abstract class _FidoState extends FidoState { @override bool get unlocked; @override + int? get pinRetries; + @override @JsonKey(ignore: true) _$$FidoStateImplCopyWith<_$FidoStateImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/fido/models.g.dart b/lib/fido/models.g.dart index 352515e0..54fe1a9a 100644 --- a/lib/fido/models.g.dart +++ b/lib/fido/models.g.dart @@ -10,12 +10,14 @@ _$FidoStateImpl _$$FidoStateImplFromJson(Map json) => _$FidoStateImpl( info: json['info'] as Map, unlocked: json['unlocked'] as bool, + pinRetries: json['pin_retries'] as int?, ); Map _$$FidoStateImplToJson(_$FidoStateImpl instance) => { 'info': instance.info, 'unlocked': instance.unlocked, + 'pin_retries': instance.pinRetries, }; _$FingerprintImpl _$$FingerprintImplFromJson(Map json) => diff --git a/lib/fido/views/key_actions.dart b/lib/fido/views/key_actions.dart index 6ca1987d..1263344e 100755 --- a/lib/fido/views/key_actions.dart +++ b/lib/fido/views/key_actions.dart @@ -48,6 +48,7 @@ Widget _fidoBuildActions(BuildContext context, DeviceNode node, FidoState state, final l10n = AppLocalizations.of(context)!; final colors = Theme.of(context).buttonTheme.colorScheme ?? Theme.of(context).colorScheme; + final authBlocked = state.pinBlocked; return Column( children: [ @@ -86,25 +87,34 @@ Widget _fidoBuildActions(BuildContext context, DeviceNode node, FidoState state, l10n.s_manage, children: [ ActionListItem( - key: keys.managePinAction, - feature: features.actionsPin, - icon: const Icon(Symbols.pin), - title: state.hasPin ? l10n.s_change_pin : l10n.s_set_pin, - subtitle: state.hasPin - ? (state.forcePinChange - ? l10n.s_pin_change_required - : l10n.s_fido_pin_protection) - : l10n.s_fido_pin_protection, - trailing: state.alwaysUv && !state.hasPin || state.forcePinChange - ? Icon(Symbols.warning_amber, color: colors.tertiary) - : null, - onTap: (context) { - Navigator.of(context).popUntil((route) => route.isFirst); - showBlurDialog( - context: context, - builder: (context) => FidoPinDialog(node.path, state), - ); - }), + key: keys.managePinAction, + feature: features.actionsPin, + icon: const Icon(Symbols.pin), + title: state.hasPin ? l10n.s_change_pin : l10n.s_set_pin, + subtitle: authBlocked + ? l10n.l_pin_blocked + : state.hasPin + ? (state.forcePinChange + ? l10n.s_pin_change_required + : state.pinRetries != null + ? l10n.l_attempts_remaining(state.pinRetries!) + : l10n.s_fido_pin_protection) + : l10n.s_fido_pin_protection, + trailing: authBlocked || + state.alwaysUv && !state.hasPin || + state.forcePinChange + ? Icon(Symbols.warning_amber, color: colors.tertiary) + : null, + onTap: !authBlocked + ? (context) { + Navigator.of(context).popUntil((route) => route.isFirst); + showBlurDialog( + context: context, + builder: (context) => FidoPinDialog(node.path, state), + ); + } + : null, + ), ], ) ], diff --git a/lib/fido/views/pin_dialog.dart b/lib/fido/views/pin_dialog.dart index 34c7c1cf..916087f2 100755 --- a/lib/fido/views/pin_dialog.dart +++ b/lib/fido/views/pin_dialog.dart @@ -87,6 +87,8 @@ class _FidoPinDialogState extends ConsumerState { final hasPinComplexity = ref.read(currentDeviceDataProvider).valueOrNull?.info.pinComplexity ?? false; + final pinRetries = ref.watch(fidoStateProvider(widget.devicePath) + .select((s) => s.whenOrNull(data: (state) => state.pinRetries))); return ResponsiveDialog( title: Text(hasPin ? l10n.s_change_pin : l10n.s_set_pin), @@ -115,6 +117,9 @@ class _FidoPinDialogState extends ConsumerState { enabled: !_isBlocked, border: const OutlineInputBorder(), labelText: l10n.s_current_pin, + helperText: pinRetries != null && pinRetries <= 3 + ? l10n.l_attempts_remaining(pinRetries) + : '', // Prevents dialog resizing errorText: _currentIsWrong ? _currentPinError : null, errorMaxLines: 3, prefixIcon: const Icon(Symbols.pin), @@ -249,8 +254,10 @@ class _FidoPinDialogState extends ConsumerState { extentOffset: _currentPinController.text.length); _currentPinFocus.requestFocus(); setState(() { - if (authBlocked) { - _currentPinError = l10n.l_pin_soft_locked; + if (authBlocked || retries == 0) { + _currentPinError = retries == 0 + ? l10n.l_pin_blocked_reset + : l10n.l_pin_soft_locked; _currentIsWrong = true; _isBlocked = true; } else { diff --git a/lib/fido/views/pin_entry_form.dart b/lib/fido/views/pin_entry_form.dart index 099e7783..aa0d9d23 100644 --- a/lib/fido/views/pin_entry_form.dart +++ b/lib/fido/views/pin_entry_form.dart @@ -82,7 +82,7 @@ class _PinEntryFormState extends ConsumerState { String? _getErrorText() { final l10n = AppLocalizations.of(context)!; - if (_retries == 0) { + if (widget._state.pinBlocked || _retries == 0) { return l10n.l_pin_blocked_reset; } if (_blocked) { @@ -98,6 +98,8 @@ class _PinEntryFormState extends ConsumerState { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final noFingerprints = widget._state.bioEnroll == false; + final authBlocked = widget._state.pinBlocked; + final pinRetries = widget._state.pinRetries; return Padding( padding: const EdgeInsets.only(left: 18.0, right: 18, top: 8), child: Column( @@ -113,12 +115,14 @@ class _PinEntryFormState extends ConsumerState { autofillHints: const [AutofillHints.password], controller: _pinController, focusNode: _pinFocus, - enabled: !_blocked && (_retries ?? 1) > 0, + enabled: !authBlocked && !_blocked && (_retries ?? 1) > 0, decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: l10n.s_pin, - helperText: '', // Prevents dialog resizing - errorText: _pinIsWrong ? _getErrorText() : null, + helperText: pinRetries != null && pinRetries <= 3 + ? l10n.l_attempts_remaining(pinRetries) + : '', // Prevents dialog resizing + errorText: _pinIsWrong || authBlocked ? _getErrorText() : null, errorMaxLines: 3, prefixIcon: const Icon(Symbols.pin), suffixIcon: IconButton( diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 24d918b7..19ab0a9c 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -295,6 +295,7 @@ "l_fido_pin_protection_optional": "Optionaler FIDO PIN Schutz", "l_enter_fido2_pin": "Geben Sie die FIDO2 PIN für Ihren YubiKey ein", "l_pin_blocked_reset": "PIN ist blockiert; setzen Sie die FIDO Anwendung auf Werkseinstellung zurück", + "l_pin_blocked": null, "l_set_pin_first": "Zuerst ist eine PIN erforderlich", "l_unlock_pin_first": "Zuerst mit PIN entsperren", "l_pin_soft_locked": "PIN wurde blockiert bis der YubiKey entfernt und wieder angeschlossen wird", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7bd1418a..0f6ec89c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -295,6 +295,7 @@ "l_fido_pin_protection_optional": "Optional FIDO PIN protection", "l_enter_fido2_pin": "Enter the FIDO2 PIN for your YubiKey", "l_pin_blocked_reset": "PIN is blocked; factory reset the FIDO application", + "l_pin_blocked": "PIN is blocked", "l_set_pin_first": "A PIN is required first", "l_unlock_pin_first": "Unlock with PIN first", "l_pin_soft_locked": "PIN has been blocked until the YubiKey is removed and reinserted", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 6e6ccd26..327a4bd9 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -295,6 +295,7 @@ "l_fido_pin_protection_optional": "PIN de protection optionnel FIDO", "l_enter_fido2_pin": "Entrez le PIN FIDO2 de votre YubiKey", "l_pin_blocked_reset": "PIN bloqué; Réinitialisez à l'état d'usine le FIDO", + "l_pin_blocked": null, "l_set_pin_first": "Un PIN est d'abord requis", "l_unlock_pin_first": "Débloquez avec un PIN d'abord", "l_pin_soft_locked": "Le PIN est bloqué tant que votre YubiKey ne sera pas réinsérée", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 2b9d072e..b9fdec60 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -295,6 +295,7 @@ "l_fido_pin_protection_optional": "任意FIDO PINによる保護", "l_enter_fido2_pin": "YubiKeyのFIDO2 PINを入力してください", "l_pin_blocked_reset": "PINはブロックされています。FIDOアプリケーションを出荷時設定にリセットしてください", + "l_pin_blocked": null, "l_set_pin_first": "最初にPINが必要です", "l_unlock_pin_first": "最初にPINでロックを解除してください", "l_pin_soft_locked": "YubiKeyを取り外して再挿入するまで、PINはブロックされています", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 84e00f94..6660de4f 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -295,6 +295,7 @@ "l_fido_pin_protection_optional": "Opcjonalna ochrona FIDO kodem PIN", "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, "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",