This commit is contained in:
Elias Bonnici 2024-04-04 17:10:06 +02:00
commit de99f6bd32
No known key found for this signature in database
GPG Key ID: 5EAC28EA3F980CCF
17 changed files with 128 additions and 60 deletions

View File

@ -112,6 +112,8 @@ class FidoManager(
private val pinStore = FidoPinStore() private val pinStore = FidoPinStore()
private var pinRetries : Int? = null
private val resetHelper = private val resetHelper =
FidoResetHelper(deviceManager, fidoViewModel, mainViewModel, connectionHelper, pinStore) FidoResetHelper(deviceManager, fidoViewModel, mainViewModel, connectionHelper, pinStore)
@ -124,6 +126,8 @@ class FidoManager(
} }
init { init {
pinRetries = null
deviceManager.addDeviceListener(this) deviceManager.addDeviceListener(this)
fidoChannel.setHandler(coroutineScope) { method, args -> fidoChannel.setHandler(coroutineScope) { method, args ->
@ -239,10 +243,14 @@ class FidoManager(
connectionHelper.cancelPending() 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( fidoViewModel.setSessionState(
Session( Session(infoData, pinStore.hasPin(), pinRetries)
fidoSession.cachedInfo, pinStore.hasPin()
)
) )
// Update deviceInfo since the deviceId has changed // Update deviceInfo since the deviceId has changed
@ -275,8 +283,6 @@ class FidoManager(
pin: CharArray pin: CharArray
): String { ): String {
//fidoViewModel.setSessionLoadingState()
val pinPermissionsCM = getPinPermissionsCM(fidoSession) val pinPermissionsCM = getPinPermissionsCM(fidoSession)
val pinPermissionsBE = getPinPermissionsBE(fidoSession) val pinPermissionsBE = getPinPermissionsBE(fidoSession)
val permissions = pinPermissionsCM or pinPermissionsBE val permissions = pinPermissionsCM or pinPermissionsBE
@ -298,16 +304,23 @@ class FidoManager(
pinStore.setPin(pin) pinStore.setPin(pin)
pinRetries = clientPin.pinRetries.count
fidoViewModel.setSessionState( fidoViewModel.setSessionState(
Session( Session(
fidoSession.info, fidoSession.info,
pinStore.hasPin() pinStore.hasPin(),
pinRetries
) )
) )
return JSONObject(mapOf("success" to true)).toString() 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 { try {
block() block()
} catch (ctapException: CtapException) { } catch (ctapException: CtapException) {
@ -318,6 +331,15 @@ class FidoManager(
) { ) {
pinStore.setPin(null) pinStore.setPin(null)
fidoViewModel.updateCredentials(emptyList()) fidoViewModel.updateCredentials(emptyList())
pinRetries = clientPin.pinRetries.count
fidoViewModel.setSessionState(
Session(
fidoSession.info,
pinStore.hasPin(),
pinRetries
)
)
if (ctapException.ctapError == CtapException.ERR_PIN_POLICY_VIOLATION) { if (ctapException.ctapError == CtapException.ERR_PIN_POLICY_VIOLATION) {
JSONObject( JSONObject(
@ -330,7 +352,7 @@ class FidoManager(
JSONObject( JSONObject(
mapOf( mapOf(
"success" to false, "success" to false,
"pinRetries" to clientPin.pinRetries.count, "pinRetries" to pinRetries,
"authBlocked" to (ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED), "authBlocked" to (ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED),
) )
).toString() ).toString()
@ -347,7 +369,7 @@ class FidoManager(
val clientPin = val clientPin =
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)) ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
catchPinErrors(clientPin) { catchPinErrors(fidoSession, clientPin) {
unlockSession(fidoSession, clientPin, pin) unlockSession(fidoSession, clientPin, pin)
} }
} catch (e: IOException) { } catch (e: IOException) {
@ -383,7 +405,7 @@ class FidoManager(
val clientPin = val clientPin =
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)) ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
catchPinErrors(clientPin) { catchPinErrors(fidoSession, clientPin) {
setOrChangePin(fidoSession, clientPin, pin, newPin) setOrChangePin(fidoSession, clientPin, pin, newPin)
unlockSession(fidoSession, clientPin, newPin) unlockSession(fidoSession, clientPin, newPin)
} }
@ -489,7 +511,7 @@ class FidoManager(
val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token) val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token)
bioEnrollment.removeEnrollment(HexCodec.hexStringToBytes(templateId)) bioEnrollment.removeEnrollment(HexCodec.hexStringToBytes(templateId))
fidoViewModel.removeFingerprint(templateId) fidoViewModel.removeFingerprint(templateId)
fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin())) fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin(), pinRetries))
return@useSession JSONObject( return@useSession JSONObject(
mapOf( mapOf(
"success" to true, "success" to true,
@ -513,7 +535,7 @@ class FidoManager(
val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token) val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token)
bioEnrollment.setName(HexCodec.hexStringToBytes(templateId), name) bioEnrollment.setName(HexCodec.hexStringToBytes(templateId), name)
fidoViewModel.renameFingerprint(templateId, name) fidoViewModel.renameFingerprint(templateId, name)
fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin())) fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin(), pinRetries))
return@useSession JSONObject( return@useSession JSONObject(
mapOf( mapOf(
"success" to true, "success" to true,
@ -592,7 +614,7 @@ class FidoManager(
val templateIdHexString = HexCodec.bytesToHexString(templateId) val templateIdHexString = HexCodec.bytesToHexString(templateId)
fidoViewModel.addFingerprint(FidoFingerprint(templateIdHexString, name)) fidoViewModel.addFingerprint(FidoFingerprint(templateIdHexString, name))
fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin())) fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin(), pinRetries))
return@useSession JSONObject( return@useSession JSONObject(
mapOf( mapOf(

View File

@ -221,7 +221,7 @@ class FidoResetHelper(
private fun doReset(fidoSession: YubiKitFidoSession) { private fun doReset(fidoSession: YubiKitFidoSession) {
logger.debug("Calling FIDO reset") logger.debug("Calling FIDO reset")
fidoSession.reset(resetCommandState) fidoSession.reset(resetCommandState)
fidoViewModel.setSessionState(Session(fidoSession.info, true)) fidoViewModel.setSessionState(Session(fidoSession.info, true, null))
fidoViewModel.updateCredentials(emptyList()) fidoViewModel.updateCredentials(emptyList())
pinStore.setPin(null) pinStore.setPin(null)
} }

View File

@ -23,7 +23,6 @@ import com.yubico.authenticator.ViewModelData
import com.yubico.authenticator.fido.data.FidoCredential import com.yubico.authenticator.fido.data.FidoCredential
import com.yubico.authenticator.fido.data.FidoFingerprint import com.yubico.authenticator.fido.data.FidoFingerprint
import com.yubico.authenticator.fido.data.Session import com.yubico.authenticator.fido.data.Session
import org.json.JSONObject
class FidoViewModel : ViewModel() { class FidoViewModel : ViewModel() {
private val _sessionState = MutableLiveData<ViewModelData>() private val _sessionState = MutableLiveData<ViewModelData>()
@ -39,10 +38,6 @@ class FidoViewModel : ViewModel() {
_sessionState.postValue(ViewModelData.Empty) _sessionState.postValue(ViewModelData.Empty)
} }
fun setSessionLoadingState() {
_sessionState.postValue(ViewModelData.Loading)
}
private val _credentials = MutableLiveData<List<FidoCredential>>() private val _credentials = MutableLiveData<List<FidoCredential>>()
val credentials: LiveData<List<FidoCredential>> = _credentials val credentials: LiveData<List<FidoCredential>> = _credentials

View File

@ -86,12 +86,13 @@ data class SessionInfo(
@Serializable @Serializable
data class Session( data class Session(
@SerialName("info")
val info: SessionInfo, val info: SessionInfo,
val unlocked: Boolean val unlocked: Boolean,
@SerialName("pin_retries")
val pinRetries: Int?
) : JsonSerializable { ) : JsonSerializable {
constructor(infoData: InfoData, unlocked: Boolean) : this( constructor(infoData: InfoData, unlocked: Boolean, pinRetries: Int?) : this(
SessionInfo(infoData), unlocked SessionInfo(infoData), unlocked, pinRetries
) )
override fun toJson(): String { override fun toJson(): String {

View File

@ -88,7 +88,6 @@ class Ctap2Node(RpcNode):
self.ctap = Ctap2(connection) self.ctap = Ctap2(connection)
self._info = self.ctap.info self._info = self.ctap.info
self.client_pin = ClientPin(self.ctap) self.client_pin = ClientPin(self.ctap)
self._auth_blocked = False
self._token = None self._token = None
def get_data(self): def get_data(self):
@ -96,7 +95,6 @@ class Ctap2Node(RpcNode):
logger.debug(f"Info: {self._info}") logger.debug(f"Info: {self._info}")
data = dict( data = dict(
info=asdict(self._info), info=asdict(self._info),
auth_blocked=self._auth_blocked,
unlocked=self._token is not None, unlocked=self._token is not None,
) )
if self._info.options.get("clientPin"): if self._info.options.get("clientPin"):
@ -190,7 +188,6 @@ class Ctap2Node(RpcNode):
if e.code == CtapError.ERR.USER_ACTION_TIMEOUT: if e.code == CtapError.ERR.USER_ACTION_TIMEOUT:
raise InactivityException() raise InactivityException()
self._info = self.ctap.get_info() self._info = self.ctap.get_info()
self._auth_blocked = False
self._token = None self._token = None
return dict() return dict()

View File

@ -153,6 +153,7 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
return unlock(newPin); return unlock(newPin);
} on RpcError catch (e) { } on RpcError catch (e) {
if (e.status == 'pin-validation') { if (e.status == 'pin-validation') {
ref.invalidateSelf();
return PinResult.failed(FidoPinFailureReason.invalidPin( return PinResult.failed(FidoPinFailureReason.invalidPin(
e.body['retries'], e.body['auth_blocked'])); e.body['retries'], e.body['auth_blocked']));
} }
@ -176,6 +177,7 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
} on RpcError catch (e) { } on RpcError catch (e) {
if (e.status == 'pin-validation') { if (e.status == 'pin-validation') {
_pinController.state = null; _pinController.state = null;
ref.invalidateSelf();
return PinResult.failed(FidoPinFailureReason.invalidPin( return PinResult.failed(FidoPinFailureReason.invalidPin(
e.body['retries'], e.body['auth_blocked'])); e.body['retries'], e.body['auth_blocked']));
} }

View File

@ -27,7 +27,8 @@ class FidoState with _$FidoState {
factory FidoState( factory FidoState(
{required Map<String, dynamic> info, {required Map<String, dynamic> info,
required bool unlocked}) = _FidoState; required bool unlocked,
int? pinRetries}) = _FidoState;
factory FidoState.fromJson(Map<String, dynamic> json) => factory FidoState.fromJson(Map<String, dynamic> json) =>
_$FidoStateFromJson(json); _$FidoStateFromJson(json);
@ -47,6 +48,8 @@ class FidoState with _$FidoState {
bool get alwaysUv => info['options']['alwaysUv'] == true; bool get alwaysUv => info['options']['alwaysUv'] == true;
bool get forcePinChange => info['force_pin_change'] == true; bool get forcePinChange => info['force_pin_change'] == true;
bool get pinBlocked => pinRetries == 0;
} }
@freezed @freezed

View File

@ -22,6 +22,7 @@ FidoState _$FidoStateFromJson(Map<String, dynamic> json) {
mixin _$FidoState { mixin _$FidoState {
Map<String, dynamic> get info => throw _privateConstructorUsedError; Map<String, dynamic> get info => throw _privateConstructorUsedError;
bool get unlocked => throw _privateConstructorUsedError; bool get unlocked => throw _privateConstructorUsedError;
int? get pinRetries => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
@ -34,7 +35,7 @@ abstract class $FidoStateCopyWith<$Res> {
factory $FidoStateCopyWith(FidoState value, $Res Function(FidoState) then) = factory $FidoStateCopyWith(FidoState value, $Res Function(FidoState) then) =
_$FidoStateCopyWithImpl<$Res, FidoState>; _$FidoStateCopyWithImpl<$Res, FidoState>;
@useResult @useResult
$Res call({Map<String, dynamic> info, bool unlocked}); $Res call({Map<String, dynamic> info, bool unlocked, int? pinRetries});
} }
/// @nodoc /// @nodoc
@ -52,6 +53,7 @@ class _$FidoStateCopyWithImpl<$Res, $Val extends FidoState>
$Res call({ $Res call({
Object? info = null, Object? info = null,
Object? unlocked = null, Object? unlocked = null,
Object? pinRetries = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
info: null == info info: null == info
@ -62,6 +64,10 @@ class _$FidoStateCopyWithImpl<$Res, $Val extends FidoState>
? _value.unlocked ? _value.unlocked
: unlocked // ignore: cast_nullable_to_non_nullable : unlocked // ignore: cast_nullable_to_non_nullable
as bool, as bool,
pinRetries: freezed == pinRetries
? _value.pinRetries
: pinRetries // ignore: cast_nullable_to_non_nullable
as int?,
) as $Val); ) as $Val);
} }
} }
@ -74,7 +80,7 @@ abstract class _$$FidoStateImplCopyWith<$Res>
__$$FidoStateImplCopyWithImpl<$Res>; __$$FidoStateImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({Map<String, dynamic> info, bool unlocked}); $Res call({Map<String, dynamic> info, bool unlocked, int? pinRetries});
} }
/// @nodoc /// @nodoc
@ -90,6 +96,7 @@ class __$$FidoStateImplCopyWithImpl<$Res>
$Res call({ $Res call({
Object? info = null, Object? info = null,
Object? unlocked = null, Object? unlocked = null,
Object? pinRetries = freezed,
}) { }) {
return _then(_$FidoStateImpl( return _then(_$FidoStateImpl(
info: null == info info: null == info
@ -100,6 +107,10 @@ class __$$FidoStateImplCopyWithImpl<$Res>
? _value.unlocked ? _value.unlocked
: unlocked // ignore: cast_nullable_to_non_nullable : unlocked // ignore: cast_nullable_to_non_nullable
as bool, as bool,
pinRetries: freezed == pinRetries
? _value.pinRetries
: pinRetries // ignore: cast_nullable_to_non_nullable
as int?,
)); ));
} }
} }
@ -108,7 +119,9 @@ class __$$FidoStateImplCopyWithImpl<$Res>
@JsonSerializable() @JsonSerializable()
class _$FidoStateImpl extends _FidoState { class _$FidoStateImpl extends _FidoState {
_$FidoStateImpl( _$FidoStateImpl(
{required final Map<String, dynamic> info, required this.unlocked}) {required final Map<String, dynamic> info,
required this.unlocked,
this.pinRetries})
: _info = info, : _info = info,
super._(); super._();
@ -125,10 +138,12 @@ class _$FidoStateImpl extends _FidoState {
@override @override
final bool unlocked; final bool unlocked;
@override
final int? pinRetries;
@override @override
String toString() { String toString() {
return 'FidoState(info: $info, unlocked: $unlocked)'; return 'FidoState(info: $info, unlocked: $unlocked, pinRetries: $pinRetries)';
} }
@override @override
@ -138,13 +153,15 @@ class _$FidoStateImpl extends _FidoState {
other is _$FidoStateImpl && other is _$FidoStateImpl &&
const DeepCollectionEquality().equals(other._info, _info) && const DeepCollectionEquality().equals(other._info, _info) &&
(identical(other.unlocked, unlocked) || (identical(other.unlocked, unlocked) ||
other.unlocked == unlocked)); other.unlocked == unlocked) &&
(identical(other.pinRetries, pinRetries) ||
other.pinRetries == pinRetries));
} }
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(runtimeType,
runtimeType, const DeepCollectionEquality().hash(_info), unlocked); const DeepCollectionEquality().hash(_info), unlocked, pinRetries);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@ -163,7 +180,8 @@ class _$FidoStateImpl extends _FidoState {
abstract class _FidoState extends FidoState { abstract class _FidoState extends FidoState {
factory _FidoState( factory _FidoState(
{required final Map<String, dynamic> info, {required final Map<String, dynamic> info,
required final bool unlocked}) = _$FidoStateImpl; required final bool unlocked,
final int? pinRetries}) = _$FidoStateImpl;
_FidoState._() : super._(); _FidoState._() : super._();
factory _FidoState.fromJson(Map<String, dynamic> json) = factory _FidoState.fromJson(Map<String, dynamic> json) =
@ -174,6 +192,8 @@ abstract class _FidoState extends FidoState {
@override @override
bool get unlocked; bool get unlocked;
@override @override
int? get pinRetries;
@override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$FidoStateImplCopyWith<_$FidoStateImpl> get copyWith => _$$FidoStateImplCopyWith<_$FidoStateImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;

View File

@ -10,12 +10,14 @@ _$FidoStateImpl _$$FidoStateImplFromJson(Map<String, dynamic> json) =>
_$FidoStateImpl( _$FidoStateImpl(
info: json['info'] as Map<String, dynamic>, info: json['info'] as Map<String, dynamic>,
unlocked: json['unlocked'] as bool, unlocked: json['unlocked'] as bool,
pinRetries: json['pin_retries'] as int?,
); );
Map<String, dynamic> _$$FidoStateImplToJson(_$FidoStateImpl instance) => Map<String, dynamic> _$$FidoStateImplToJson(_$FidoStateImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'info': instance.info, 'info': instance.info,
'unlocked': instance.unlocked, 'unlocked': instance.unlocked,
'pin_retries': instance.pinRetries,
}; };
_$FingerprintImpl _$$FingerprintImplFromJson(Map<String, dynamic> json) => _$FingerprintImpl _$$FingerprintImplFromJson(Map<String, dynamic> json) =>

View File

@ -48,6 +48,7 @@ Widget _fidoBuildActions(BuildContext context, DeviceNode node, FidoState state,
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).buttonTheme.colorScheme ?? final colors = Theme.of(context).buttonTheme.colorScheme ??
Theme.of(context).colorScheme; Theme.of(context).colorScheme;
final authBlocked = state.pinBlocked;
return Column( return Column(
children: [ children: [
@ -90,21 +91,30 @@ Widget _fidoBuildActions(BuildContext context, DeviceNode node, FidoState state,
feature: features.actionsPin, feature: features.actionsPin,
icon: const Icon(Symbols.pin), icon: const Icon(Symbols.pin),
title: state.hasPin ? l10n.s_change_pin : l10n.s_set_pin, title: state.hasPin ? l10n.s_change_pin : l10n.s_set_pin,
subtitle: state.hasPin subtitle: authBlocked
? l10n.l_pin_blocked
: state.hasPin
? (state.forcePinChange ? (state.forcePinChange
? l10n.s_pin_change_required ? l10n.s_pin_change_required
: state.pinRetries != null
? l10n.l_attempts_remaining(state.pinRetries!)
: l10n.s_fido_pin_protection) : l10n.s_fido_pin_protection)
: l10n.s_fido_pin_protection, : l10n.s_fido_pin_protection,
trailing: state.alwaysUv && !state.hasPin || state.forcePinChange trailing: authBlocked ||
state.alwaysUv && !state.hasPin ||
state.forcePinChange
? Icon(Symbols.warning_amber, color: colors.tertiary) ? Icon(Symbols.warning_amber, color: colors.tertiary)
: null, : null,
onTap: (context) { onTap: !authBlocked
? (context) {
Navigator.of(context).popUntil((route) => route.isFirst); Navigator.of(context).popUntil((route) => route.isFirst);
showBlurDialog( showBlurDialog(
context: context, context: context,
builder: (context) => FidoPinDialog(node.path, state), builder: (context) => FidoPinDialog(node.path, state),
); );
}), }
: null,
),
], ],
) )
], ],

View File

@ -87,6 +87,8 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
final hasPinComplexity = final hasPinComplexity =
ref.read(currentDeviceDataProvider).valueOrNull?.info.pinComplexity ?? ref.read(currentDeviceDataProvider).valueOrNull?.info.pinComplexity ??
false; false;
final pinRetries = ref.watch(fidoStateProvider(widget.devicePath)
.select((s) => s.whenOrNull(data: (state) => state.pinRetries)));
return ResponsiveDialog( return ResponsiveDialog(
title: Text(hasPin ? l10n.s_change_pin : l10n.s_set_pin), title: Text(hasPin ? l10n.s_change_pin : l10n.s_set_pin),
@ -115,6 +117,9 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
enabled: !_isBlocked, enabled: !_isBlocked,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
labelText: l10n.s_current_pin, labelText: l10n.s_current_pin,
helperText: pinRetries != null && pinRetries <= 3
? l10n.l_attempts_remaining(pinRetries)
: '', // Prevents dialog resizing
errorText: _currentIsWrong ? _currentPinError : null, errorText: _currentIsWrong ? _currentPinError : null,
errorMaxLines: 3, errorMaxLines: 3,
prefixIcon: const Icon(Symbols.pin), prefixIcon: const Icon(Symbols.pin),
@ -249,8 +254,10 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
extentOffset: _currentPinController.text.length); extentOffset: _currentPinController.text.length);
_currentPinFocus.requestFocus(); _currentPinFocus.requestFocus();
setState(() { setState(() {
if (authBlocked) { if (authBlocked || retries == 0) {
_currentPinError = l10n.l_pin_soft_locked; _currentPinError = retries == 0
? l10n.l_pin_blocked_reset
: l10n.l_pin_soft_locked;
_currentIsWrong = true; _currentIsWrong = true;
_isBlocked = true; _isBlocked = true;
} else { } else {

View File

@ -82,7 +82,7 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
String? _getErrorText() { String? _getErrorText() {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
if (_retries == 0) { if (widget._state.pinBlocked || _retries == 0) {
return l10n.l_pin_blocked_reset; return l10n.l_pin_blocked_reset;
} }
if (_blocked) { if (_blocked) {
@ -98,6 +98,8 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final noFingerprints = widget._state.bioEnroll == false; final noFingerprints = widget._state.bioEnroll == false;
final authBlocked = widget._state.pinBlocked;
final pinRetries = widget._state.pinRetries;
return Padding( return Padding(
padding: const EdgeInsets.only(left: 18.0, right: 18, top: 8), padding: const EdgeInsets.only(left: 18.0, right: 18, top: 8),
child: Column( child: Column(
@ -113,12 +115,14 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
autofillHints: const [AutofillHints.password], autofillHints: const [AutofillHints.password],
controller: _pinController, controller: _pinController,
focusNode: _pinFocus, focusNode: _pinFocus,
enabled: !_blocked && (_retries ?? 1) > 0, enabled: !authBlocked && !_blocked && (_retries ?? 1) > 0,
decoration: AppInputDecoration( decoration: AppInputDecoration(
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
labelText: l10n.s_pin, labelText: l10n.s_pin,
helperText: '', // Prevents dialog resizing helperText: pinRetries != null && pinRetries <= 3
errorText: _pinIsWrong ? _getErrorText() : null, ? l10n.l_attempts_remaining(pinRetries)
: '', // Prevents dialog resizing
errorText: _pinIsWrong || authBlocked ? _getErrorText() : null,
errorMaxLines: 3, errorMaxLines: 3,
prefixIcon: const Icon(Symbols.pin), prefixIcon: const Icon(Symbols.pin),
suffixIcon: IconButton( suffixIcon: IconButton(

View File

@ -295,6 +295,7 @@
"l_fido_pin_protection_optional": "Optionaler FIDO PIN Schutz", "l_fido_pin_protection_optional": "Optionaler FIDO PIN Schutz",
"l_enter_fido2_pin": "Geben Sie die FIDO2 PIN für Ihren YubiKey ein", "l_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_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_set_pin_first": "Zuerst ist eine PIN erforderlich",
"l_unlock_pin_first": "Zuerst mit PIN entsperren", "l_unlock_pin_first": "Zuerst mit PIN entsperren",
"l_pin_soft_locked": "PIN wurde blockiert bis der YubiKey entfernt und wieder angeschlossen wird", "l_pin_soft_locked": "PIN wurde blockiert bis der YubiKey entfernt und wieder angeschlossen wird",

View File

@ -295,6 +295,7 @@
"l_fido_pin_protection_optional": "Optional FIDO PIN protection", "l_fido_pin_protection_optional": "Optional FIDO PIN protection",
"l_enter_fido2_pin": "Enter the FIDO2 PIN for your YubiKey", "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_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_set_pin_first": "A PIN is required first",
"l_unlock_pin_first": "Unlock with PIN first", "l_unlock_pin_first": "Unlock with PIN first",
"l_pin_soft_locked": "PIN has been blocked until the YubiKey is removed and reinserted", "l_pin_soft_locked": "PIN has been blocked until the YubiKey is removed and reinserted",

View File

@ -295,6 +295,7 @@
"l_fido_pin_protection_optional": "PIN de protection optionnel FIDO", "l_fido_pin_protection_optional": "PIN de protection optionnel FIDO",
"l_enter_fido2_pin": "Entrez le PIN FIDO2 de votre YubiKey", "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_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_set_pin_first": "Un PIN est d'abord requis",
"l_unlock_pin_first": "Débloquez avec un PIN d'abord", "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", "l_pin_soft_locked": "Le PIN est bloqué tant que votre YubiKey ne sera pas réinsérée",

View File

@ -295,6 +295,7 @@
"l_fido_pin_protection_optional": "任意FIDO PINによる保護", "l_fido_pin_protection_optional": "任意FIDO PINによる保護",
"l_enter_fido2_pin": "YubiKeyのFIDO2 PINを入力してください", "l_enter_fido2_pin": "YubiKeyのFIDO2 PINを入力してください",
"l_pin_blocked_reset": "PINはブロックされています。FIDOアプリケーションを出荷時設定にリセットしてください", "l_pin_blocked_reset": "PINはブロックされています。FIDOアプリケーションを出荷時設定にリセットしてください",
"l_pin_blocked": null,
"l_set_pin_first": "最初にPINが必要です", "l_set_pin_first": "最初にPINが必要です",
"l_unlock_pin_first": "最初にPINでロックを解除してください", "l_unlock_pin_first": "最初にPINでロックを解除してください",
"l_pin_soft_locked": "YubiKeyを取り外して再挿入するまで、PINはブロックされています", "l_pin_soft_locked": "YubiKeyを取り外して再挿入するまで、PINはブロックされています",

View File

@ -295,6 +295,7 @@
"l_fido_pin_protection_optional": "Opcjonalna ochrona FIDO kodem PIN", "l_fido_pin_protection_optional": "Opcjonalna ochrona FIDO kodem PIN",
"l_enter_fido2_pin": "Wprowadź kod PIN FIDO2 klucza YubiKey", "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_reset": "PIN jest zablokowany; przywróć ustawienia fabryczne funkcji FIDO",
"l_pin_blocked": null,
"l_set_pin_first": "Najpierw wymagany jest kod PIN", "l_set_pin_first": "Najpierw wymagany jest kod PIN",
"l_unlock_pin_first": "Najpierw odblokuj kodem 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_soft_locked": "PIN został zablokowany do momentu ponownego podłączenia klucza YubiKey",