diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/device/Info.kt b/android/app/src/main/kotlin/com/yubico/authenticator/device/Info.kt index e27e4add..c8f93683 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/device/Info.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/device/Info.kt @@ -49,8 +49,10 @@ data class Info( val isNfc: Boolean, @SerialName("usb_pid") val usbPid: Int?, + @SerialName("pin_complexity") + val pinComplexity: Boolean, @SerialName("supported_capabilities") - val supportedCapabilities: Capabilities + val supportedCapabilities: Capabilities, ) { constructor(name: String, isNfc: Boolean, usbPid: Int?, deviceInfo: DeviceInfo) : this( config = Config(deviceInfo.config), @@ -63,6 +65,7 @@ data class Info( name = name, isNfc = isNfc, usbPid = usbPid, + pinComplexity = deviceInfo.pinComplexity, supportedCapabilities = Capabilities( nfc = deviceInfo.capabilitiesFor(Transport.NFC), usb = deviceInfo.capabilitiesFor(Transport.USB), diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/device/UnknownDevice.kt b/android/app/src/main/kotlin/com/yubico/authenticator/device/UnknownDevice.kt index f457bf31..6fc29f9c 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/device/UnknownDevice.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/device/UnknownDevice.kt @@ -18,5 +18,6 @@ val UnknownDevice = Info( name = "Unrecognized device", isNfc = false, usbPid = null, + pinComplexity = false, supportedCapabilities = Capabilities() ) \ No newline at end of file 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 423e07d8..c2e27a4c 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 @@ -313,18 +313,28 @@ class FidoManager( } catch (ctapException: CtapException) { if (ctapException.ctapError == CtapException.ERR_PIN_INVALID || ctapException.ctapError == CtapException.ERR_PIN_BLOCKED || - ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED + ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED || + ctapException.ctapError == CtapException.ERR_PIN_POLICY_VIOLATION ) { pinStore.setPin(null) fidoViewModel.updateCredentials(emptyList()) - val pinRetriesResult = clientPin.pinRetries - JSONObject( - mapOf( - "success" to false, - "pinRetries" to pinRetriesResult.count, - "authBlocked" to (ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED) - ) - ).toString() + + if (ctapException.ctapError == CtapException.ERR_PIN_POLICY_VIOLATION) { + JSONObject( + mapOf( + "success" to false, + "pinViolation" to true + ) + ).toString() + } else { + JSONObject( + mapOf( + "success" to false, + "pinRetries" to clientPin.pinRetries.count, + "authBlocked" to (ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED), + ) + ).toString() + } } else { throw ctapException } diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/SkyHelper.kt b/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/SkyHelper.kt index ec760fd1..05458eb5 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/SkyHelper.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/yubikit/SkyHelper.kt @@ -74,6 +74,7 @@ class SkyHelper(private val compatUtil: CompatUtil) { name = (device.usbDevice.productName ?: "Yubico Security Key"), isNfc = false, usbPid = pid.value, + pinComplexity = false, supportedCapabilities = Capabilities(usb = 0) ) } diff --git a/helper/helper/base.py b/helper/helper/base.py index 78b9b653..3d021276 100644 --- a/helper/helper/base.py +++ b/helper/helper/base.py @@ -75,6 +75,11 @@ class AuthRequiredException(RpcException): super().__init__("auth-required", "Authentication is required") +class PinComplexityException(RpcException): + def __init__(self): + super().__init__("pin-complexity", "PIN does not meet complexity requirements") + + class ChildResetException(Exception): def __init__(self, message): self.message = message diff --git a/helper/helper/fido.py b/helper/helper/fido.py index 184bd72a..5291cabc 100644 --- a/helper/helper/fido.py +++ b/helper/helper/fido.py @@ -19,6 +19,7 @@ from .base import ( RpcException, TimeoutException, AuthRequiredException, + PinComplexityException, ) from fido2.ctap import CtapError from fido2.ctap2 import Ctap2, ClientPin @@ -76,6 +77,8 @@ def _handle_pin_error(e, client_pin): raise PinValidationException( pin_retries, e.code == CtapError.ERR.PIN_AUTH_BLOCKED ) + if e.code == CtapError.ERR.PIN_POLICY_VIOLATION: + raise PinComplexityException() raise e diff --git a/helper/helper/piv.py b/helper/helper/piv.py index 42787226..46827120 100644 --- a/helper/helper/piv.py +++ b/helper/helper/piv.py @@ -20,6 +20,7 @@ from .base import ( ChildResetException, TimeoutException, AuthRequiredException, + PinComplexityException, ) from yubikit.core import NotSupportedError, BadResponseError, InvalidPinError from yubikit.core.smartcard import ApduError, SW @@ -80,6 +81,15 @@ class GENERATE_TYPE(str, Enum): CERTIFICATE = "certificate" +def _handle_pin_puk_error(e): + if isinstance(e, ApduError): + if e.sw == SW.CONDITIONS_NOT_SATISFIED: + raise PinComplexityException() + if isinstance(e, InvalidPinError): + raise InvalidPinException(cause=e) + raise e + + class PivNode(RpcNode): def __init__(self, connection): super().__init__() @@ -208,21 +218,30 @@ class PivNode(RpcNode): def change_pin(self, params, event, signal): old_pin = params.pop("pin") new_pin = params.pop("new_pin") - pivman_change_pin(self.session, old_pin, new_pin) + try: + pivman_change_pin(self.session, old_pin, new_pin) + except Exception as e: + _handle_pin_puk_error(e) return dict() @action def change_puk(self, params, event, signal): old_puk = params.pop("puk") new_puk = params.pop("new_puk") - self.session.change_puk(old_puk, new_puk) + try: + self.session.change_puk(old_puk, new_puk) + except Exception as e: + _handle_pin_puk_error(e) return dict() @action def unblock_pin(self, params, event, signal): puk = params.pop("puk") new_pin = params.pop("new_pin") - self.session.unblock_pin(puk, new_pin) + try: + self.session.unblock_pin(puk, new_pin) + except Exception as e: + _handle_pin_puk_error(e) return dict() @action diff --git a/helper/poetry.lock b/helper/poetry.lock index 232c5893..3759493b 100644 --- a/helper/poetry.lock +++ b/helper/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "altgraph" @@ -717,13 +717,13 @@ files = [ [[package]] name = "yubikey-manager" -version = "5.3.0" +version = "5.4.0" description = "Tool for managing your YubiKey configuration." optional = false -python-versions = ">=3.8,<4.0" +python-versions = "<4.0,>=3.8" files = [ - {file = "yubikey_manager-5.3.0-py3-none-any.whl", hash = "sha256:9a809620f5c910c1047323570095e10b885002f6b0a2e4d8ced7f62d7c2ce628"}, - {file = "yubikey_manager-5.3.0.tar.gz", hash = "sha256:5492c36a10ce6a5995b8ea1d32cf5bd60db7587201b2aa3e63e0c1da2334b8b6"}, + {file = "yubikey_manager-5.4.0-py3-none-any.whl", hash = "sha256:d53acb06c4028a833be7a05ca4145833afef1affa67aaab4347bc50ecce37985"}, + {file = "yubikey_manager-5.4.0.tar.gz", hash = "sha256:53726a186722cd2683b2f5fd781fc0a2861f47ce62ba9d3527960832c8fabec8"}, ] [package.dependencies] @@ -787,4 +787,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "6664f12e752d8b41c996d11e43a8572ed47f7fbfaaf84fa894be725fe2208d80" +content-hash = "7543cc0ac90ea4eb701a7f52321d831bfe05c65e0ae896a6107eb7a9540d8543" diff --git a/helper/pyproject.toml b/helper/pyproject.toml index 9aec1f12..18fe63fc 100644 --- a/helper/pyproject.toml +++ b/helper/pyproject.toml @@ -10,7 +10,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.8" -yubikey-manager = "^5.2" +yubikey-manager = "^5.4" mss = "^9.0.1" Pillow = "^10.2.0" zxing-cpp = "^2.2.0" diff --git a/lib/android/fido/state.dart b/lib/android/fido/state.dart index ee610f16..8cd1e41b 100644 --- a/lib/android/fido/state.dart +++ b/lib/android/fido/state.dart @@ -109,15 +109,18 @@ class _FidoStateNotifier extends FidoStateNotifier { }, )); if (response['success'] == true) { - _log.debug('FIDO pin set/change successful'); + _log.debug('FIDO PIN set/change successful'); return PinResult.success(); } - _log.debug('FIDO pin set/change failed'); - return PinResult.failed( - response['pinRetries'], - response['authBlocked'], - ); + if (response['pinViolation'] == true) { + _log.debug('FIDO PIN violation'); + return PinResult.failed(const FidoPinFailureReason.weakPin()); + } + + _log.debug('FIDO PIN set/change failed'); + return PinResult.failed(FidoPinFailureReason.invalidPin( + response['pinRetries'], response['authBlocked'])); } on PlatformException catch (pe) { var decodedException = pe.decode(); if (decodedException is CancellationException) { @@ -141,10 +144,8 @@ class _FidoStateNotifier extends FidoStateNotifier { } _log.debug('FIDO applet unlock failed'); - return PinResult.failed( - response['pinRetries'], - response['authBlocked'], - ); + return PinResult.failed(FidoPinFailureReason.invalidPin( + response['pinRetries'], response['authBlocked'])); } on PlatformException catch (pe) { var decodedException = pe.decode(); if (decodedException is! CancellationException) { diff --git a/lib/desktop/fido/state.dart b/lib/desktop/fido/state.dart index d974f199..21e2072d 100755 --- a/lib/desktop/fido/state.dart +++ b/lib/desktop/fido/state.dart @@ -153,7 +153,11 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier { return unlock(newPin); } on RpcError catch (e) { if (e.status == 'pin-validation') { - return PinResult.failed(e.body['retries'], e.body['auth_blocked']); + return PinResult.failed(FidoPinFailureReason.invalidPin( + e.body['retries'], e.body['auth_blocked'])); + } + if (e.status == 'pin-complexity') { + return PinResult.failed(const FidoPinFailureReason.weakPin()); } rethrow; } @@ -172,7 +176,8 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier { } on RpcError catch (e) { if (e.status == 'pin-validation') { _pinController.state = null; - return PinResult.failed(e.body['retries'], e.body['auth_blocked']); + return PinResult.failed(FidoPinFailureReason.invalidPin( + e.body['retries'], e.body['auth_blocked'])); } rethrow; } diff --git a/lib/desktop/piv/state.dart b/lib/desktop/piv/state.dart index 6ba6638d..2f194c75 100644 --- a/lib/desktop/piv/state.dart +++ b/lib/desktop/piv/state.dart @@ -233,7 +233,12 @@ class _DesktopPivStateNotifier extends PivStateNotifier { return const PinVerificationStatus.success(); } on RpcError catch (e) { if (e.status == 'invalid-pin') { - return PinVerificationStatus.failure(e.body['attempts_remaining']); + return PinVerificationStatus.failure( + PivPinFailureReason.invalidPin(e.body['attempts_remaining'])); + } + if (e.status == 'pin-complexity') { + return PinVerificationStatus.failure( + const PivPinFailureReason.weakPin()); } rethrow; } finally { @@ -251,7 +256,12 @@ class _DesktopPivStateNotifier extends PivStateNotifier { return const PinVerificationStatus.success(); } on RpcError catch (e) { if (e.status == 'invalid-pin') { - return PinVerificationStatus.failure(e.body['attempts_remaining']); + return PinVerificationStatus.failure( + PivPinFailureReason.invalidPin(e.body['attempts_remaining'])); + } + if (e.status == 'pin-complexity') { + return PinVerificationStatus.failure( + const PivPinFailureReason.weakPin()); } rethrow; } finally { @@ -286,7 +296,12 @@ class _DesktopPivStateNotifier extends PivStateNotifier { return const PinVerificationStatus.success(); } on RpcError catch (e) { if (e.status == 'invalid-pin') { - return PinVerificationStatus.failure(e.body['attempts_remaining']); + return PinVerificationStatus.failure( + PivPinFailureReason.invalidPin(e.body['attempts_remaining'])); + } + if (e.status == 'pin-complexity') { + return PinVerificationStatus.failure( + const PivPinFailureReason.weakPin()); } rethrow; } finally { diff --git a/lib/fido/models.dart b/lib/fido/models.dart index b6d2cff4..254025c6 100755 --- a/lib/fido/models.dart +++ b/lib/fido/models.dart @@ -52,7 +52,14 @@ class FidoState with _$FidoState { @freezed class PinResult with _$PinResult { factory PinResult.success() = _PinSuccess; - factory PinResult.failed(int retries, bool authBlocked) = _PinFailure; + factory PinResult.failed(FidoPinFailureReason reason) = _PinFailure; +} + +@freezed +class FidoPinFailureReason with _$FidoPinFailureReason { + factory FidoPinFailureReason.invalidPin(int retries, bool authBlocked) = + FidoInvalidPin; + const factory FidoPinFailureReason.weakPin() = FidoWeakPin; } @freezed diff --git a/lib/fido/models.freezed.dart b/lib/fido/models.freezed.dart index acfc641c..da069362 100644 --- a/lib/fido/models.freezed.dart +++ b/lib/fido/models.freezed.dart @@ -184,19 +184,19 @@ mixin _$PinResult { @optionalTypeArgs TResult when({ required TResult Function() success, - required TResult Function(int retries, bool authBlocked) failed, + required TResult Function(FidoPinFailureReason reason) failed, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? success, - TResult? Function(int retries, bool authBlocked)? failed, + TResult? Function(FidoPinFailureReason reason)? failed, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ TResult Function()? success, - TResult Function(int retries, bool authBlocked)? failed, + TResult Function(FidoPinFailureReason reason)? failed, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -277,7 +277,7 @@ class _$PinSuccessImpl implements _PinSuccess { @optionalTypeArgs TResult when({ required TResult Function() success, - required TResult Function(int retries, bool authBlocked) failed, + required TResult Function(FidoPinFailureReason reason) failed, }) { return success(); } @@ -286,7 +286,7 @@ class _$PinSuccessImpl implements _PinSuccess { @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? success, - TResult? Function(int retries, bool authBlocked)? failed, + TResult? Function(FidoPinFailureReason reason)? failed, }) { return success?.call(); } @@ -295,7 +295,7 @@ class _$PinSuccessImpl implements _PinSuccess { @optionalTypeArgs TResult maybeWhen({ TResult Function()? success, - TResult Function(int retries, bool authBlocked)? failed, + TResult Function(FidoPinFailureReason reason)? failed, required TResult orElse(), }) { if (success != null) { @@ -346,7 +346,9 @@ abstract class _$$PinFailureImplCopyWith<$Res> { _$PinFailureImpl value, $Res Function(_$PinFailureImpl) then) = __$$PinFailureImplCopyWithImpl<$Res>; @useResult - $Res call({int retries, bool authBlocked}); + $Res call({FidoPinFailureReason reason}); + + $FidoPinFailureReasonCopyWith<$Res> get reason; } /// @nodoc @@ -360,35 +362,36 @@ class __$$PinFailureImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? retries = null, - Object? authBlocked = null, + Object? reason = null, }) { return _then(_$PinFailureImpl( - null == retries - ? _value.retries - : retries // ignore: cast_nullable_to_non_nullable - as int, - null == authBlocked - ? _value.authBlocked - : authBlocked // ignore: cast_nullable_to_non_nullable - as bool, + null == reason + ? _value.reason + : reason // ignore: cast_nullable_to_non_nullable + as FidoPinFailureReason, )); } + + @override + @pragma('vm:prefer-inline') + $FidoPinFailureReasonCopyWith<$Res> get reason { + return $FidoPinFailureReasonCopyWith<$Res>(_value.reason, (value) { + return _then(_value.copyWith(reason: value)); + }); + } } /// @nodoc class _$PinFailureImpl implements _PinFailure { - _$PinFailureImpl(this.retries, this.authBlocked); + _$PinFailureImpl(this.reason); @override - final int retries; - @override - final bool authBlocked; + final FidoPinFailureReason reason; @override String toString() { - return 'PinResult.failed(retries: $retries, authBlocked: $authBlocked)'; + return 'PinResult.failed(reason: $reason)'; } @override @@ -396,13 +399,11 @@ class _$PinFailureImpl implements _PinFailure { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PinFailureImpl && - (identical(other.retries, retries) || other.retries == retries) && - (identical(other.authBlocked, authBlocked) || - other.authBlocked == authBlocked)); + (identical(other.reason, reason) || other.reason == reason)); } @override - int get hashCode => Object.hash(runtimeType, retries, authBlocked); + int get hashCode => Object.hash(runtimeType, reason); @JsonKey(ignore: true) @override @@ -414,29 +415,29 @@ class _$PinFailureImpl implements _PinFailure { @optionalTypeArgs TResult when({ required TResult Function() success, - required TResult Function(int retries, bool authBlocked) failed, + required TResult Function(FidoPinFailureReason reason) failed, }) { - return failed(retries, authBlocked); + return failed(reason); } @override @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? success, - TResult? Function(int retries, bool authBlocked)? failed, + TResult? Function(FidoPinFailureReason reason)? failed, }) { - return failed?.call(retries, authBlocked); + return failed?.call(reason); } @override @optionalTypeArgs TResult maybeWhen({ TResult Function()? success, - TResult Function(int retries, bool authBlocked)? failed, + TResult Function(FidoPinFailureReason reason)? failed, required TResult orElse(), }) { if (failed != null) { - return failed(retries, authBlocked); + return failed(reason); } return orElse(); } @@ -474,16 +475,322 @@ class _$PinFailureImpl implements _PinFailure { } abstract class _PinFailure implements PinResult { - factory _PinFailure(final int retries, final bool authBlocked) = - _$PinFailureImpl; + factory _PinFailure(final FidoPinFailureReason reason) = _$PinFailureImpl; + + FidoPinFailureReason get reason; + @JsonKey(ignore: true) + _$$PinFailureImplCopyWith<_$PinFailureImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$FidoPinFailureReason { + @optionalTypeArgs + TResult when({ + required TResult Function(int retries, bool authBlocked) invalidPin, + required TResult Function() weakPin, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int retries, bool authBlocked)? invalidPin, + TResult? Function()? weakPin, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int retries, bool authBlocked)? invalidPin, + TResult Function()? weakPin, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(FidoInvalidPin value) invalidPin, + required TResult Function(FidoWeakPin value) weakPin, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(FidoInvalidPin value)? invalidPin, + TResult? Function(FidoWeakPin value)? weakPin, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(FidoInvalidPin value)? invalidPin, + TResult Function(FidoWeakPin value)? weakPin, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $FidoPinFailureReasonCopyWith<$Res> { + factory $FidoPinFailureReasonCopyWith(FidoPinFailureReason value, + $Res Function(FidoPinFailureReason) then) = + _$FidoPinFailureReasonCopyWithImpl<$Res, FidoPinFailureReason>; +} + +/// @nodoc +class _$FidoPinFailureReasonCopyWithImpl<$Res, + $Val extends FidoPinFailureReason> + implements $FidoPinFailureReasonCopyWith<$Res> { + _$FidoPinFailureReasonCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$FidoInvalidPinImplCopyWith<$Res> { + factory _$$FidoInvalidPinImplCopyWith(_$FidoInvalidPinImpl value, + $Res Function(_$FidoInvalidPinImpl) then) = + __$$FidoInvalidPinImplCopyWithImpl<$Res>; + @useResult + $Res call({int retries, bool authBlocked}); +} + +/// @nodoc +class __$$FidoInvalidPinImplCopyWithImpl<$Res> + extends _$FidoPinFailureReasonCopyWithImpl<$Res, _$FidoInvalidPinImpl> + implements _$$FidoInvalidPinImplCopyWith<$Res> { + __$$FidoInvalidPinImplCopyWithImpl( + _$FidoInvalidPinImpl _value, $Res Function(_$FidoInvalidPinImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? retries = null, + Object? authBlocked = null, + }) { + return _then(_$FidoInvalidPinImpl( + null == retries + ? _value.retries + : retries // ignore: cast_nullable_to_non_nullable + as int, + null == authBlocked + ? _value.authBlocked + : authBlocked // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$FidoInvalidPinImpl implements FidoInvalidPin { + _$FidoInvalidPinImpl(this.retries, this.authBlocked); + + @override + final int retries; + @override + final bool authBlocked; + + @override + String toString() { + return 'FidoPinFailureReason.invalidPin(retries: $retries, authBlocked: $authBlocked)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$FidoInvalidPinImpl && + (identical(other.retries, retries) || other.retries == retries) && + (identical(other.authBlocked, authBlocked) || + other.authBlocked == authBlocked)); + } + + @override + int get hashCode => Object.hash(runtimeType, retries, authBlocked); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$FidoInvalidPinImplCopyWith<_$FidoInvalidPinImpl> get copyWith => + __$$FidoInvalidPinImplCopyWithImpl<_$FidoInvalidPinImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(int retries, bool authBlocked) invalidPin, + required TResult Function() weakPin, + }) { + return invalidPin(retries, authBlocked); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int retries, bool authBlocked)? invalidPin, + TResult? Function()? weakPin, + }) { + return invalidPin?.call(retries, authBlocked); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int retries, bool authBlocked)? invalidPin, + TResult Function()? weakPin, + required TResult orElse(), + }) { + if (invalidPin != null) { + return invalidPin(retries, authBlocked); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(FidoInvalidPin value) invalidPin, + required TResult Function(FidoWeakPin value) weakPin, + }) { + return invalidPin(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(FidoInvalidPin value)? invalidPin, + TResult? Function(FidoWeakPin value)? weakPin, + }) { + return invalidPin?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(FidoInvalidPin value)? invalidPin, + TResult Function(FidoWeakPin value)? weakPin, + required TResult orElse(), + }) { + if (invalidPin != null) { + return invalidPin(this); + } + return orElse(); + } +} + +abstract class FidoInvalidPin implements FidoPinFailureReason { + factory FidoInvalidPin(final int retries, final bool authBlocked) = + _$FidoInvalidPinImpl; int get retries; bool get authBlocked; @JsonKey(ignore: true) - _$$PinFailureImplCopyWith<_$PinFailureImpl> get copyWith => + _$$FidoInvalidPinImplCopyWith<_$FidoInvalidPinImpl> get copyWith => throw _privateConstructorUsedError; } +/// @nodoc +abstract class _$$FidoWeakPinImplCopyWith<$Res> { + factory _$$FidoWeakPinImplCopyWith( + _$FidoWeakPinImpl value, $Res Function(_$FidoWeakPinImpl) then) = + __$$FidoWeakPinImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$FidoWeakPinImplCopyWithImpl<$Res> + extends _$FidoPinFailureReasonCopyWithImpl<$Res, _$FidoWeakPinImpl> + implements _$$FidoWeakPinImplCopyWith<$Res> { + __$$FidoWeakPinImplCopyWithImpl( + _$FidoWeakPinImpl _value, $Res Function(_$FidoWeakPinImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$FidoWeakPinImpl implements FidoWeakPin { + const _$FidoWeakPinImpl(); + + @override + String toString() { + return 'FidoPinFailureReason.weakPin()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$FidoWeakPinImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(int retries, bool authBlocked) invalidPin, + required TResult Function() weakPin, + }) { + return weakPin(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int retries, bool authBlocked)? invalidPin, + TResult? Function()? weakPin, + }) { + return weakPin?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int retries, bool authBlocked)? invalidPin, + TResult Function()? weakPin, + required TResult orElse(), + }) { + if (weakPin != null) { + return weakPin(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(FidoInvalidPin value) invalidPin, + required TResult Function(FidoWeakPin value) weakPin, + }) { + return weakPin(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(FidoInvalidPin value)? invalidPin, + TResult? Function(FidoWeakPin value)? weakPin, + }) { + return weakPin?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(FidoInvalidPin value)? invalidPin, + TResult Function(FidoWeakPin value)? weakPin, + required TResult orElse(), + }) { + if (weakPin != null) { + return weakPin(this); + } + return orElse(); + } +} + +abstract class FidoWeakPin implements FidoPinFailureReason { + const factory FidoWeakPin() = _$FidoWeakPinImpl; +} + Fingerprint _$FingerprintFromJson(Map json) { return _Fingerprint.fromJson(json); } diff --git a/lib/fido/views/pin_dialog.dart b/lib/fido/views/pin_dialog.dart index 168c6019..34c7c1cf 100755 --- a/lib/fido/views/pin_dialog.dart +++ b/lib/fido/views/pin_dialog.dart @@ -48,7 +48,8 @@ class FidoPinDialog extends ConsumerStatefulWidget { class _FidoPinDialogState extends ConsumerState { final _currentPinController = TextEditingController(); final _currentPinFocus = FocusNode(); - String _newPin = ''; + final _newPinController = TextEditingController(); + final _newPinFocus = FocusNode(); String _confirmPin = ''; String? _currentPinError; String? _newPinError; @@ -63,6 +64,8 @@ class _FidoPinDialogState extends ConsumerState { void dispose() { _currentPinController.dispose(); _currentPinFocus.dispose(); + _newPinController.dispose(); + _newPinFocus.dispose(); super.dispose(); } @@ -77,8 +80,13 @@ class _FidoPinDialogState extends ConsumerState { : (widget.state.forcePinChange ? 4 : widget.state.minPinLength); final currentPinLenOk = _currentPinController.text.length >= currentMinPinLen; - final newPinLenOk = _newPin.length >= minPinLength; - final isValid = currentPinLenOk && newPinLenOk && _newPin == _confirmPin; + final newPinLenOk = _newPinController.text.length >= minPinLength; + final isValid = + currentPinLenOk && newPinLenOk && _newPinController.text == _confirmPin; + + final hasPinComplexity = + ref.read(currentDeviceDataProvider).valueOrNull?.info.pinComplexity ?? + false; return ResponsiveDialog( title: Text(hasPin ? l10n.s_change_pin : l10n.s_set_pin), @@ -130,11 +138,15 @@ class _FidoPinDialogState extends ConsumerState { }, ).init(), ], - Text(l10n.p_enter_new_fido2_pin(minPinLength)), + Text(hasPinComplexity + ? l10n.p_enter_new_fido2_pin_complexity_active( + minPinLength, 2, '123456') + : l10n.p_enter_new_fido2_pin(minPinLength)), // TODO: Set max characters based on UTF-8 bytes AppTextFormField( key: newPin, - initialValue: _newPin, + controller: _newPinController, + focusNode: _newPinFocus, autofocus: !hasPin, obscureText: _isObscureNew, autofillHints: const [AutofillHints.password], @@ -160,7 +172,6 @@ class _FidoPinDialogState extends ConsumerState { onChanged: (value) { setState(() { _newIsWrong = false; - _newPin = value; }); }, ).init(), @@ -186,10 +197,11 @@ class _FidoPinDialogState extends ConsumerState { _isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin, ), enabled: !_isBlocked && currentPinLenOk && newPinLenOk, - errorText: _newPin.length == _confirmPin.length && - _newPin != _confirmPin - ? l10n.l_pin_mismatch - : null, + errorText: + _newPinController.text.length == _confirmPin.length && + _newPinController.text != _confirmPin + ? l10n.l_pin_mismatch + : null, helperText: '', // Prevents resizing when errorText shown ), onChanged: (value) { @@ -219,28 +231,47 @@ class _FidoPinDialogState extends ConsumerState { final oldPin = _currentPinController.text.isNotEmpty ? _currentPinController.text : null; + final newPin = _newPinController.text; try { final result = await ref .read(fidoStateProvider(widget.devicePath).notifier) - .setPin(_newPin, oldPin: oldPin); - result.when(success: () { - Navigator.of(context).pop(true); - showMessage(context, l10n.s_pin_set); - }, failed: (retries, authBlocked) { - setState(() { - _currentPinController.selection = TextSelection( - baseOffset: 0, extentOffset: _currentPinController.text.length); - _currentPinFocus.requestFocus(); - if (authBlocked) { - _currentPinError = l10n.l_pin_soft_locked; - _currentIsWrong = true; - _isBlocked = true; - } else { - _currentPinError = l10n.l_wrong_pin_attempts_remaining(retries); - _currentIsWrong = true; - } - }); - }); + .setPin(newPin, oldPin: oldPin); + result.whenOrNull( + success: () { + Navigator.of(context).pop(true); + showMessage(context, l10n.s_pin_set); + }, + failed: (reason) { + reason.when( + invalidPin: (retries, authBlocked) { + _currentPinController.selection = TextSelection( + baseOffset: 0, + extentOffset: _currentPinController.text.length); + _currentPinFocus.requestFocus(); + setState(() { + if (authBlocked) { + _currentPinError = l10n.l_pin_soft_locked; + _currentIsWrong = true; + _isBlocked = true; + } else { + _currentPinError = + l10n.l_wrong_pin_attempts_remaining(retries); + _currentIsWrong = true; + } + }); + }, + weakPin: () { + _newPinController.selection = TextSelection( + baseOffset: 0, extentOffset: _newPinController.text.length); + _newPinFocus.requestFocus(); + setState(() { + _newPinError = l10n.p_pin_puk_complexity_failure(l10n.s_pin); + _newIsWrong = true; + }); + }, + ); + }, + ); } on CancellationException catch (_) { // ignored } catch (e) { diff --git a/lib/fido/views/pin_entry_form.dart b/lib/fido/views/pin_entry_form.dart index 45c5d506..099e7783 100644 --- a/lib/fido/views/pin_entry_form.dart +++ b/lib/fido/views/pin_entry_form.dart @@ -60,15 +60,20 @@ class _PinEntryFormState extends ConsumerState { final result = await ref .read(fidoStateProvider(widget._deviceNode.path).notifier) .unlock(_pinController.text); - result.whenOrNull(failed: (retries, authBlocked) { - _pinController.selection = TextSelection( - baseOffset: 0, extentOffset: _pinController.text.length); - _pinFocus.requestFocus(); - setState(() { - _pinIsWrong = true; - _retries = retries; - _blocked = authBlocked; - }); + result.whenOrNull(failed: (reason) { + reason.maybeWhen( + invalidPin: (retries, authBlocked) { + _pinController.selection = TextSelection( + baseOffset: 0, extentOffset: _pinController.text.length); + _pinFocus.requestFocus(); + setState(() { + _pinIsWrong = true; + _retries = retries; + _blocked = authBlocked; + }); + }, + orElse: () {}, + ); }); } on CancellationException catch (_) { // ignored diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f4d7a3fc..6eacd233 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -252,6 +252,18 @@ "message": {} } }, + "l_set_puk_failed": null, + "@l_set_puk_failed": { + "placeholders": { + "message": {} + } + }, + "l_unblock_pin_failed": null, + "@l_unblock_pin_failed": { + "placeholders": { + "message": {} + } + }, "l_attempts_remaining": null, "@l_attempts_remaining": { "placeholders": { @@ -288,6 +300,14 @@ "length": {} } }, + "p_enter_new_fido2_pin_complexity_active": null, + "@p_enter_new_fido2_pin_complexity_active": { + "placeholders": { + "length": {}, + "unique_characters": {}, + "common_pin": {} + } + }, "s_pin_required": null, "p_pin_required_desc": null, "l_piv_pin_blocked": null, @@ -298,6 +318,19 @@ "name": {} } }, + "p_enter_new_piv_pin_puk_complexity_active": null, + "@p_enter_new_piv_pin_puk_complexity_active": { + "placeholders": { + "name": {}, + "common": {} + } + }, + "p_pin_puk_complexity_failure": null, + "@p_pin_puk_complexity_failure": { + "placeholders": { + "name": {} + } + }, "l_warning_default_pin": null, "l_warning_default_puk": null, "l_default_pin_used": null, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2687e551..d90e8671 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -252,6 +252,18 @@ "message": {} } }, + "l_set_puk_failed": "Failed to set PUK: {message}", + "@l_set_puk_failed": { + "placeholders": { + "message": {} + } + }, + "l_unblock_pin_failed": "Failed to unblock PIN: {message}", + "@l_unblock_pin_failed": { + "placeholders": { + "message": {} + } + }, "l_attempts_remaining": "{retries} attempt(s) remaining", "@l_attempts_remaining": { "placeholders": { @@ -288,6 +300,14 @@ "length": {} } }, + "p_enter_new_fido2_pin_complexity_active": "Enter your new PIN. A PIN must be at least {length} characters long, contain at least {unique_characters} unique characters, and not be a commonly used PIN, like \"{common_pin}\". It may contain letters, numbers, and special characters.", + "@p_enter_new_fido2_pin_complexity_active": { + "placeholders": { + "length": {}, + "unique_characters": {}, + "common_pin": {} + } + }, "s_pin_required": "PIN required", "p_pin_required_desc": "The action you are about to perform requires the PIV PIN to be entered.", "l_piv_pin_blocked": "Blocked, use PUK to reset", @@ -298,6 +318,19 @@ "name": {} } }, + "p_enter_new_piv_pin_puk_complexity_active": "Enter a new {name} to set. Must be 6-8 characters, contain at least 2 unique characters, and not be a commonly used {name}, like \"{common}\".", + "@p_enter_new_piv_pin_puk_complexity_active": { + "placeholders": { + "name": {}, + "common": {} + } + }, + "p_pin_puk_complexity_failure": "New {name} doesn't meet complexity requirements.", + "@p_pin_puk_complexity_failure": { + "placeholders": { + "name": {} + } + }, "l_warning_default_pin": "Warning: Default PIN used", "l_warning_default_puk": "Warning: Default PUK used", "l_default_pin_used": "Default PIN used", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index b667f00d..688d694b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -252,6 +252,18 @@ "message": {} } }, + "l_set_puk_failed": null, + "@l_set_puk_failed": { + "placeholders": { + "message": {} + } + }, + "l_unblock_pin_failed": null, + "@l_unblock_pin_failed": { + "placeholders": { + "message": {} + } + }, "l_attempts_remaining": "Nombre de tentative(s) restante(s) : {retries}", "@l_attempts_remaining": { "placeholders": { @@ -288,6 +300,14 @@ "length": {} } }, + "p_enter_new_fido2_pin_complexity_active": null, + "@p_enter_new_fido2_pin_complexity_active": { + "placeholders": { + "length": {}, + "unique_characters": {}, + "common_pin": {} + } + }, "s_pin_required": "PIN requis", "p_pin_required_desc": "L'action que vous allez faire demande d'entrer le code PIN du PIV.", "l_piv_pin_blocked": "Vous êtes bloqué, utilisez le code PUK pour réinitialiser", @@ -298,6 +318,19 @@ "name": {} } }, + "p_enter_new_piv_pin_puk_complexity_active": null, + "@p_enter_new_piv_pin_puk_complexity_active": { + "placeholders": { + "name": {}, + "common": {} + } + }, + "p_pin_puk_complexity_failure": null, + "@p_pin_puk_complexity_failure": { + "placeholders": { + "name": {} + } + }, "l_warning_default_pin": null, "l_warning_default_puk": null, "l_default_pin_used": null, diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index d1f6d8cc..fc659869 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -252,6 +252,18 @@ "message": {} } }, + "l_set_puk_failed": null, + "@l_set_puk_failed": { + "placeholders": { + "message": {} + } + }, + "l_unblock_pin_failed": null, + "@l_unblock_pin_failed": { + "placeholders": { + "message": {} + } + }, "l_attempts_remaining": "あと{retries}回試行できます", "@l_attempts_remaining": { "placeholders": { @@ -288,6 +300,14 @@ "length": {} } }, + "p_enter_new_fido2_pin_complexity_active": null, + "@p_enter_new_fido2_pin_complexity_active": { + "placeholders": { + "length": {}, + "unique_characters": {}, + "common_pin": {} + } + }, "s_pin_required": "PINが必要", "p_pin_required_desc": "実行しようとしている操作には、PIV PINの入力が必要です", "l_piv_pin_blocked": "ブロックされています。PUK を使用してリセットしてください", @@ -298,6 +318,19 @@ "name": {} } }, + "p_enter_new_piv_pin_puk_complexity_active": null, + "@p_enter_new_piv_pin_puk_complexity_active": { + "placeholders": { + "name": {}, + "common": {} + } + }, + "p_pin_puk_complexity_failure": null, + "@p_pin_puk_complexity_failure": { + "placeholders": { + "name": {} + } + }, "l_warning_default_pin": null, "l_warning_default_puk": null, "l_default_pin_used": null, diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 4ca19a9a..6141ee75 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -252,6 +252,18 @@ "message": {} } }, + "l_set_puk_failed": null, + "@l_set_puk_failed": { + "placeholders": { + "message": {} + } + }, + "l_unblock_pin_failed": null, + "@l_unblock_pin_failed": { + "placeholders": { + "message": {} + } + }, "l_attempts_remaining": "Pozostało prób: {retries}", "@l_attempts_remaining": { "placeholders": { @@ -288,6 +300,14 @@ "length": {} } }, + "p_enter_new_fido2_pin_complexity_active": null, + "@p_enter_new_fido2_pin_complexity_active": { + "placeholders": { + "length": {}, + "unique_characters": {}, + "common_pin": {} + } + }, "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ć", @@ -298,6 +318,19 @@ "name": {} } }, + "p_enter_new_piv_pin_puk_complexity_active": null, + "@p_enter_new_piv_pin_puk_complexity_active": { + "placeholders": { + "name": {}, + "common": {} + } + }, + "p_pin_puk_complexity_failure": null, + "@p_pin_puk_complexity_failure": { + "placeholders": { + "name": {} + } + }, "l_warning_default_pin": null, "l_warning_default_puk": null, "l_default_pin_used": null, diff --git a/lib/management/models.dart b/lib/management/models.dart index 867f0174..eddb4623 100755 --- a/lib/management/models.dart +++ b/lib/management/models.dart @@ -86,7 +86,8 @@ class DeviceInfo with _$DeviceInfo { Map supportedCapabilities, bool isLocked, bool isFips, - bool isSky) = _DeviceInfo; + bool isSky, + bool pinComplexity) = _DeviceInfo; factory DeviceInfo.fromJson(Map json) => _$DeviceInfoFromJson(json); diff --git a/lib/management/models.freezed.dart b/lib/management/models.freezed.dart index 19450e65..776e1b83 100644 --- a/lib/management/models.freezed.dart +++ b/lib/management/models.freezed.dart @@ -245,6 +245,7 @@ mixin _$DeviceInfo { bool get isLocked => throw _privateConstructorUsedError; bool get isFips => throw _privateConstructorUsedError; bool get isSky => throw _privateConstructorUsedError; + bool get pinComplexity => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -266,7 +267,8 @@ abstract class $DeviceInfoCopyWith<$Res> { Map supportedCapabilities, bool isLocked, bool isFips, - bool isSky}); + bool isSky, + bool pinComplexity}); $DeviceConfigCopyWith<$Res> get config; $VersionCopyWith<$Res> get version; @@ -293,6 +295,7 @@ class _$DeviceInfoCopyWithImpl<$Res, $Val extends DeviceInfo> Object? isLocked = null, Object? isFips = null, Object? isSky = null, + Object? pinComplexity = null, }) { return _then(_value.copyWith( config: null == config @@ -327,6 +330,10 @@ class _$DeviceInfoCopyWithImpl<$Res, $Val extends DeviceInfo> ? _value.isSky : isSky // ignore: cast_nullable_to_non_nullable as bool, + pinComplexity: null == pinComplexity + ? _value.pinComplexity + : pinComplexity // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } @@ -363,7 +370,8 @@ abstract class _$$DeviceInfoImplCopyWith<$Res> Map supportedCapabilities, bool isLocked, bool isFips, - bool isSky}); + bool isSky, + bool pinComplexity}); @override $DeviceConfigCopyWith<$Res> get config; @@ -390,6 +398,7 @@ class __$$DeviceInfoImplCopyWithImpl<$Res> Object? isLocked = null, Object? isFips = null, Object? isSky = null, + Object? pinComplexity = null, }) { return _then(_$DeviceInfoImpl( null == config @@ -424,6 +433,10 @@ class __$$DeviceInfoImplCopyWithImpl<$Res> ? _value.isSky : isSky // ignore: cast_nullable_to_non_nullable as bool, + null == pinComplexity + ? _value.pinComplexity + : pinComplexity // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -439,7 +452,8 @@ class _$DeviceInfoImpl implements _DeviceInfo { final Map supportedCapabilities, this.isLocked, this.isFips, - this.isSky) + this.isSky, + this.pinComplexity) : _supportedCapabilities = supportedCapabilities; factory _$DeviceInfoImpl.fromJson(Map json) => @@ -468,10 +482,12 @@ class _$DeviceInfoImpl implements _DeviceInfo { final bool isFips; @override final bool isSky; + @override + final bool pinComplexity; @override String toString() { - return 'DeviceInfo(config: $config, serial: $serial, version: $version, formFactor: $formFactor, supportedCapabilities: $supportedCapabilities, isLocked: $isLocked, isFips: $isFips, isSky: $isSky)'; + return 'DeviceInfo(config: $config, serial: $serial, version: $version, formFactor: $formFactor, supportedCapabilities: $supportedCapabilities, isLocked: $isLocked, isFips: $isFips, isSky: $isSky, pinComplexity: $pinComplexity)'; } @override @@ -489,7 +505,9 @@ class _$DeviceInfoImpl implements _DeviceInfo { (identical(other.isLocked, isLocked) || other.isLocked == isLocked) && (identical(other.isFips, isFips) || other.isFips == isFips) && - (identical(other.isSky, isSky) || other.isSky == isSky)); + (identical(other.isSky, isSky) || other.isSky == isSky) && + (identical(other.pinComplexity, pinComplexity) || + other.pinComplexity == pinComplexity)); } @JsonKey(ignore: true) @@ -503,7 +521,8 @@ class _$DeviceInfoImpl implements _DeviceInfo { const DeepCollectionEquality().hash(_supportedCapabilities), isLocked, isFips, - isSky); + isSky, + pinComplexity); @JsonKey(ignore: true) @override @@ -528,7 +547,8 @@ abstract class _DeviceInfo implements DeviceInfo { final Map supportedCapabilities, final bool isLocked, final bool isFips, - final bool isSky) = _$DeviceInfoImpl; + final bool isSky, + final bool pinComplexity) = _$DeviceInfoImpl; factory _DeviceInfo.fromJson(Map json) = _$DeviceInfoImpl.fromJson; @@ -550,6 +570,8 @@ abstract class _DeviceInfo implements DeviceInfo { @override bool get isSky; @override + bool get pinComplexity; + @override @JsonKey(ignore: true) _$$DeviceInfoImplCopyWith<_$DeviceInfoImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/management/models.g.dart b/lib/management/models.g.dart index d3a4b011..dc33ea9d 100644 --- a/lib/management/models.g.dart +++ b/lib/management/models.g.dart @@ -42,6 +42,7 @@ _$DeviceInfoImpl _$$DeviceInfoImplFromJson(Map json) => json['is_locked'] as bool, json['is_fips'] as bool, json['is_sky'] as bool, + json['pin_complexity'] as bool, ); Map _$$DeviceInfoImplToJson(_$DeviceInfoImpl instance) => @@ -55,6 +56,7 @@ Map _$$DeviceInfoImplToJson(_$DeviceInfoImpl instance) => 'is_locked': instance.isLocked, 'is_fips': instance.isFips, 'is_sky': instance.isSky, + 'pin_complexity': instance.pinComplexity, }; const _$FormFactorEnumMap = { diff --git a/lib/piv/models.dart b/lib/piv/models.dart index bd28e24d..d255f8db 100644 --- a/lib/piv/models.dart +++ b/lib/piv/models.dart @@ -209,7 +209,14 @@ class PinMetadata with _$PinMetadata { @freezed class PinVerificationStatus with _$PinVerificationStatus { const factory PinVerificationStatus.success() = PinSuccess; - factory PinVerificationStatus.failure(int attemptsRemaining) = PinFailure; + factory PinVerificationStatus.failure(PivPinFailureReason reason) = + PinFailure; +} + +@freezed +class PivPinFailureReason with _$PivPinFailureReason { + factory PivPinFailureReason.invalidPin(int attemptsRemaining) = PivInvalidPin; + const factory PivPinFailureReason.weakPin() = PivWeakPin; } @freezed diff --git a/lib/piv/models.freezed.dart b/lib/piv/models.freezed.dart index f895b9f0..4344848f 100644 --- a/lib/piv/models.freezed.dart +++ b/lib/piv/models.freezed.dart @@ -193,19 +193,19 @@ mixin _$PinVerificationStatus { @optionalTypeArgs TResult when({ required TResult Function() success, - required TResult Function(int attemptsRemaining) failure, + required TResult Function(PivPinFailureReason reason) failure, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? success, - TResult? Function(int attemptsRemaining)? failure, + TResult? Function(PivPinFailureReason reason)? failure, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ TResult Function()? success, - TResult Function(int attemptsRemaining)? failure, + TResult Function(PivPinFailureReason reason)? failure, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -288,7 +288,7 @@ class _$PinSuccessImpl implements PinSuccess { @optionalTypeArgs TResult when({ required TResult Function() success, - required TResult Function(int attemptsRemaining) failure, + required TResult Function(PivPinFailureReason reason) failure, }) { return success(); } @@ -297,7 +297,7 @@ class _$PinSuccessImpl implements PinSuccess { @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? success, - TResult? Function(int attemptsRemaining)? failure, + TResult? Function(PivPinFailureReason reason)? failure, }) { return success?.call(); } @@ -306,7 +306,7 @@ class _$PinSuccessImpl implements PinSuccess { @optionalTypeArgs TResult maybeWhen({ TResult Function()? success, - TResult Function(int attemptsRemaining)? failure, + TResult Function(PivPinFailureReason reason)? failure, required TResult orElse(), }) { if (success != null) { @@ -357,7 +357,9 @@ abstract class _$$PinFailureImplCopyWith<$Res> { _$PinFailureImpl value, $Res Function(_$PinFailureImpl) then) = __$$PinFailureImplCopyWithImpl<$Res>; @useResult - $Res call({int attemptsRemaining}); + $Res call({PivPinFailureReason reason}); + + $PivPinFailureReasonCopyWith<$Res> get reason; } /// @nodoc @@ -371,28 +373,36 @@ class __$$PinFailureImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? attemptsRemaining = null, + Object? reason = null, }) { return _then(_$PinFailureImpl( - null == attemptsRemaining - ? _value.attemptsRemaining - : attemptsRemaining // ignore: cast_nullable_to_non_nullable - as int, + null == reason + ? _value.reason + : reason // ignore: cast_nullable_to_non_nullable + as PivPinFailureReason, )); } + + @override + @pragma('vm:prefer-inline') + $PivPinFailureReasonCopyWith<$Res> get reason { + return $PivPinFailureReasonCopyWith<$Res>(_value.reason, (value) { + return _then(_value.copyWith(reason: value)); + }); + } } /// @nodoc class _$PinFailureImpl implements PinFailure { - _$PinFailureImpl(this.attemptsRemaining); + _$PinFailureImpl(this.reason); @override - final int attemptsRemaining; + final PivPinFailureReason reason; @override String toString() { - return 'PinVerificationStatus.failure(attemptsRemaining: $attemptsRemaining)'; + return 'PinVerificationStatus.failure(reason: $reason)'; } @override @@ -400,12 +410,11 @@ class _$PinFailureImpl implements PinFailure { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PinFailureImpl && - (identical(other.attemptsRemaining, attemptsRemaining) || - other.attemptsRemaining == attemptsRemaining)); + (identical(other.reason, reason) || other.reason == reason)); } @override - int get hashCode => Object.hash(runtimeType, attemptsRemaining); + int get hashCode => Object.hash(runtimeType, reason); @JsonKey(ignore: true) @override @@ -417,29 +426,29 @@ class _$PinFailureImpl implements PinFailure { @optionalTypeArgs TResult when({ required TResult Function() success, - required TResult Function(int attemptsRemaining) failure, + required TResult Function(PivPinFailureReason reason) failure, }) { - return failure(attemptsRemaining); + return failure(reason); } @override @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? success, - TResult? Function(int attemptsRemaining)? failure, + TResult? Function(PivPinFailureReason reason)? failure, }) { - return failure?.call(attemptsRemaining); + return failure?.call(reason); } @override @optionalTypeArgs TResult maybeWhen({ TResult Function()? success, - TResult Function(int attemptsRemaining)? failure, + TResult Function(PivPinFailureReason reason)? failure, required TResult orElse(), }) { if (failure != null) { - return failure(attemptsRemaining); + return failure(reason); } return orElse(); } @@ -477,14 +486,310 @@ class _$PinFailureImpl implements PinFailure { } abstract class PinFailure implements PinVerificationStatus { - factory PinFailure(final int attemptsRemaining) = _$PinFailureImpl; + factory PinFailure(final PivPinFailureReason reason) = _$PinFailureImpl; - int get attemptsRemaining; + PivPinFailureReason get reason; @JsonKey(ignore: true) _$$PinFailureImplCopyWith<_$PinFailureImpl> get copyWith => throw _privateConstructorUsedError; } +/// @nodoc +mixin _$PivPinFailureReason { + @optionalTypeArgs + TResult when({ + required TResult Function(int attemptsRemaining) invalidPin, + required TResult Function() weakPin, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int attemptsRemaining)? invalidPin, + TResult? Function()? weakPin, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int attemptsRemaining)? invalidPin, + TResult Function()? weakPin, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(PivInvalidPin value) invalidPin, + required TResult Function(PivWeakPin value) weakPin, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(PivInvalidPin value)? invalidPin, + TResult? Function(PivWeakPin value)? weakPin, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(PivInvalidPin value)? invalidPin, + TResult Function(PivWeakPin value)? weakPin, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PivPinFailureReasonCopyWith<$Res> { + factory $PivPinFailureReasonCopyWith( + PivPinFailureReason value, $Res Function(PivPinFailureReason) then) = + _$PivPinFailureReasonCopyWithImpl<$Res, PivPinFailureReason>; +} + +/// @nodoc +class _$PivPinFailureReasonCopyWithImpl<$Res, $Val extends PivPinFailureReason> + implements $PivPinFailureReasonCopyWith<$Res> { + _$PivPinFailureReasonCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$PivInvalidPinImplCopyWith<$Res> { + factory _$$PivInvalidPinImplCopyWith( + _$PivInvalidPinImpl value, $Res Function(_$PivInvalidPinImpl) then) = + __$$PivInvalidPinImplCopyWithImpl<$Res>; + @useResult + $Res call({int attemptsRemaining}); +} + +/// @nodoc +class __$$PivInvalidPinImplCopyWithImpl<$Res> + extends _$PivPinFailureReasonCopyWithImpl<$Res, _$PivInvalidPinImpl> + implements _$$PivInvalidPinImplCopyWith<$Res> { + __$$PivInvalidPinImplCopyWithImpl( + _$PivInvalidPinImpl _value, $Res Function(_$PivInvalidPinImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? attemptsRemaining = null, + }) { + return _then(_$PivInvalidPinImpl( + null == attemptsRemaining + ? _value.attemptsRemaining + : attemptsRemaining // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc + +class _$PivInvalidPinImpl implements PivInvalidPin { + _$PivInvalidPinImpl(this.attemptsRemaining); + + @override + final int attemptsRemaining; + + @override + String toString() { + return 'PivPinFailureReason.invalidPin(attemptsRemaining: $attemptsRemaining)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PivInvalidPinImpl && + (identical(other.attemptsRemaining, attemptsRemaining) || + other.attemptsRemaining == attemptsRemaining)); + } + + @override + int get hashCode => Object.hash(runtimeType, attemptsRemaining); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$PivInvalidPinImplCopyWith<_$PivInvalidPinImpl> get copyWith => + __$$PivInvalidPinImplCopyWithImpl<_$PivInvalidPinImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(int attemptsRemaining) invalidPin, + required TResult Function() weakPin, + }) { + return invalidPin(attemptsRemaining); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int attemptsRemaining)? invalidPin, + TResult? Function()? weakPin, + }) { + return invalidPin?.call(attemptsRemaining); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int attemptsRemaining)? invalidPin, + TResult Function()? weakPin, + required TResult orElse(), + }) { + if (invalidPin != null) { + return invalidPin(attemptsRemaining); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(PivInvalidPin value) invalidPin, + required TResult Function(PivWeakPin value) weakPin, + }) { + return invalidPin(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(PivInvalidPin value)? invalidPin, + TResult? Function(PivWeakPin value)? weakPin, + }) { + return invalidPin?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(PivInvalidPin value)? invalidPin, + TResult Function(PivWeakPin value)? weakPin, + required TResult orElse(), + }) { + if (invalidPin != null) { + return invalidPin(this); + } + return orElse(); + } +} + +abstract class PivInvalidPin implements PivPinFailureReason { + factory PivInvalidPin(final int attemptsRemaining) = _$PivInvalidPinImpl; + + int get attemptsRemaining; + @JsonKey(ignore: true) + _$$PivInvalidPinImplCopyWith<_$PivInvalidPinImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$PivWeakPinImplCopyWith<$Res> { + factory _$$PivWeakPinImplCopyWith( + _$PivWeakPinImpl value, $Res Function(_$PivWeakPinImpl) then) = + __$$PivWeakPinImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$PivWeakPinImplCopyWithImpl<$Res> + extends _$PivPinFailureReasonCopyWithImpl<$Res, _$PivWeakPinImpl> + implements _$$PivWeakPinImplCopyWith<$Res> { + __$$PivWeakPinImplCopyWithImpl( + _$PivWeakPinImpl _value, $Res Function(_$PivWeakPinImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$PivWeakPinImpl implements PivWeakPin { + const _$PivWeakPinImpl(); + + @override + String toString() { + return 'PivPinFailureReason.weakPin()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$PivWeakPinImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(int attemptsRemaining) invalidPin, + required TResult Function() weakPin, + }) { + return weakPin(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int attemptsRemaining)? invalidPin, + TResult? Function()? weakPin, + }) { + return weakPin?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int attemptsRemaining)? invalidPin, + TResult Function()? weakPin, + required TResult orElse(), + }) { + if (weakPin != null) { + return weakPin(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(PivInvalidPin value) invalidPin, + required TResult Function(PivWeakPin value) weakPin, + }) { + return weakPin(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(PivInvalidPin value)? invalidPin, + TResult? Function(PivWeakPin value)? weakPin, + }) { + return weakPin?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(PivInvalidPin value)? invalidPin, + TResult Function(PivWeakPin value)? weakPin, + required TResult orElse(), + }) { + if (weakPin != null) { + return weakPin(this); + } + return orElse(); + } +} + +abstract class PivWeakPin implements PivPinFailureReason { + const factory PivWeakPin() = _$PivWeakPinImpl; +} + ManagementKeyMetadata _$ManagementKeyMetadataFromJson( Map json) { return _ManagementKeyMetadata.fromJson(json); diff --git a/lib/piv/views/manage_key_dialog.dart b/lib/piv/views/manage_key_dialog.dart index 700c28a9..7df10a3a 100644 --- a/lib/piv/views/manage_key_dialog.dart +++ b/lib/piv/views/manage_key_dialog.dart @@ -106,14 +106,19 @@ class _ManageKeyDialogState extends ConsumerState { if (_usesStoredKey) { final status = (await notifier.verifyPin(_currentController.text)).when( success: () => true, - failure: (attemptsRemaining) { - _currentController.selection = TextSelection( - baseOffset: 0, extentOffset: _currentController.text.length); - _currentFocus.requestFocus(); - setState(() { - _attemptsRemaining = attemptsRemaining; - _currentIsWrong = true; - }); + failure: (reason) { + reason.maybeWhen( + invalidPin: (attemptsRemaining) { + _currentController.selection = TextSelection( + baseOffset: 0, extentOffset: _currentController.text.length); + _currentFocus.requestFocus(); + setState(() { + _attemptsRemaining = attemptsRemaining; + _currentIsWrong = true; + }); + }, + orElse: () {}, + ); return false; }, ); diff --git a/lib/piv/views/manage_pin_puk_dialog.dart b/lib/piv/views/manage_pin_puk_dialog.dart index a874751c..f0a51a96 100644 --- a/lib/piv/views/manage_pin_puk_dialog.dart +++ b/lib/piv/views/manage_pin_puk_dialog.dart @@ -21,6 +21,7 @@ import 'package:material_symbols_icons/symbols.dart'; import '../../app/message.dart'; import '../../app/models.dart'; +import '../../app/state.dart'; import '../../widgets/app_input_decoration.dart'; import '../../widgets/app_text_field.dart'; import '../../widgets/responsive_dialog.dart'; @@ -46,10 +47,13 @@ class ManagePinPukDialog extends ConsumerStatefulWidget { class _ManagePinPukDialogState extends ConsumerState { final _currentPinController = TextEditingController(); final _currentPinFocus = FocusNode(); - String _newPin = ''; + final _newPinController = TextEditingController(); + final _newPinFocus = FocusNode(); String _confirmPin = ''; bool _pinIsBlocked = false; bool _currentIsWrong = false; + bool _newIsWrong = false; + String? _newPinError; int _attemptsRemaining = -1; bool _isObscureCurrent = true; bool _isObscureNew = true; @@ -80,23 +84,26 @@ class _ManagePinPukDialogState extends ConsumerState { void dispose() { _currentPinController.dispose(); _currentPinFocus.dispose(); + _newPinController.dispose(); + _newPinFocus.dispose(); super.dispose(); } _submit() async { final notifier = ref.read(pivStateProvider(widget.path).notifier); + final l10n = AppLocalizations.of(context)!; + final result = await switch (widget.target) { ManageTarget.pin => - notifier.changePin(_currentPinController.text, _newPin), + notifier.changePin(_currentPinController.text, _newPinController.text), ManageTarget.puk => - notifier.changePuk(_currentPinController.text, _newPin), + notifier.changePuk(_currentPinController.text, _newPinController.text), ManageTarget.unblock => - notifier.unblockPin(_currentPinController.text, _newPin), + notifier.unblockPin(_currentPinController.text, _newPinController.text), }; result.when(success: () { if (!mounted) return; - final l10n = AppLocalizations.of(context)!; Navigator.of(context).pop(); showMessage( context, @@ -104,17 +111,31 @@ class _ManagePinPukDialogState extends ConsumerState { ManageTarget.puk => l10n.s_puk_set, _ => l10n.s_pin_set, }); - }, failure: (attemptsRemaining) { - _currentPinController.selection = TextSelection( - baseOffset: 0, extentOffset: _currentPinController.text.length); - _currentPinFocus.requestFocus(); - setState(() { - _attemptsRemaining = attemptsRemaining; - _currentIsWrong = true; - if (_attemptsRemaining == 0) { - _pinIsBlocked = true; - } - }); + }, failure: (reason) { + reason.when( + invalidPin: (attemptsRemaining) { + _currentPinController.selection = TextSelection( + baseOffset: 0, extentOffset: _currentPinController.text.length); + _currentPinFocus.requestFocus(); + setState(() { + _attemptsRemaining = attemptsRemaining; + _currentIsWrong = true; + if (_attemptsRemaining == 0) { + _pinIsBlocked = true; + } + }); + }, + weakPin: () { + _newPinController.selection = TextSelection( + baseOffset: 0, extentOffset: _newPinController.text.length); + _newPinFocus.requestFocus(); + setState(() { + _newPinError = l10n.p_pin_puk_complexity_failure( + widget.target == ManageTarget.puk ? l10n.s_puk : l10n.s_pin); + _newIsWrong = true; + }); + }, + ); }); } @@ -123,10 +144,11 @@ class _ManagePinPukDialogState extends ConsumerState { final l10n = AppLocalizations.of(context)!; final currentPin = _currentPinController.text; final currentPinLen = byteLength(currentPin); - final newPinLen = byteLength(_newPin); + final newPin = _newPinController.text; + final newPinLen = byteLength(newPin); final isValid = !_currentIsWrong && - _newPin.isNotEmpty && - _newPin == _confirmPin && + newPin.isNotEmpty && + newPin == _confirmPin && currentPin.isNotEmpty; final titleText = switch (widget.target) { @@ -140,6 +162,10 @@ class _ManagePinPukDialogState extends ConsumerState { final showDefaultPukUsed = widget.target != ManageTarget.pin && _defaultPukUsed; + final hasPinComplexity = + ref.read(currentDeviceDataProvider).valueOrNull?.info.pinComplexity ?? + false; + return ResponsiveDialog( title: Text(titleText), actions: [ @@ -213,21 +239,29 @@ class _ManagePinPukDialogState extends ConsumerState { }); }, ).init(), - Text(l10n.p_enter_new_piv_pin_puk( - widget.target == ManageTarget.puk ? l10n.s_puk : l10n.s_pin)), + Text(hasPinComplexity + ? l10n.p_enter_new_piv_pin_puk_complexity_active( + widget.target == ManageTarget.puk ? l10n.s_puk : l10n.s_pin, + '123456') + : l10n.p_enter_new_piv_pin_puk(widget.target == ManageTarget.puk + ? l10n.s_puk + : l10n.s_pin)), AppTextField( key: keys.newPinPukField, autofocus: showDefaultPinUsed || showDefaultPukUsed, obscureText: _isObscureNew, + controller: _newPinController, + focusNode: _newPinFocus, maxLength: 8, inputFormatters: [limitBytesLength(8)], - buildCounter: buildByteCounterFor(_newPin), + buildCounter: buildByteCounterFor(newPin), autofillHints: const [AutofillHints.newPassword], decoration: AppInputDecoration( border: const OutlineInputBorder(), labelText: widget.target == ManageTarget.puk ? l10n.s_new_puk : l10n.s_new_pin, + errorText: _newIsWrong ? _newPinError : null, prefixIcon: const Icon(Symbols.password), suffixIcon: IconButton( icon: Icon(_isObscureNew @@ -247,7 +281,7 @@ class _ManagePinPukDialogState extends ConsumerState { textInputAction: TextInputAction.next, onChanged: (value) { setState(() { - _newPin = value; + _newIsWrong = false; }); }, onSubmitted: (_) { @@ -284,8 +318,9 @@ class _ManagePinPukDialogState extends ConsumerState { ), enabled: currentPinLen >= _minPinLen && newPinLen >= 6, errorText: - newPinLen == _confirmPin.length && _newPin != _confirmPin - ? (widget.target == ManageTarget.pin + newPinLen == _confirmPin.length && newPin != _confirmPin + ? (widget.target == ManageTarget.pin || + widget.target == ManageTarget.unblock ? l10n.l_pin_mismatch : l10n.l_puk_mismatch) : null, diff --git a/lib/piv/views/pin_dialog.dart b/lib/piv/views/pin_dialog.dart index fab45816..d0f09581 100644 --- a/lib/piv/views/pin_dialog.dart +++ b/lib/piv/views/pin_dialog.dart @@ -60,14 +60,19 @@ class _PinDialogState extends ConsumerState { success: () { navigator.pop(true); }, - failure: (attemptsRemaining) { - _pinController.selection = TextSelection( - baseOffset: 0, extentOffset: _pinController.text.length); - _pinFocus.requestFocus(); - setState(() { - _attemptsRemaining = attemptsRemaining; - _pinIsWrong = true; - }); + failure: (reason) { + reason.maybeWhen( + invalidPin: (attemptsRemaining) { + _pinController.selection = TextSelection( + baseOffset: 0, extentOffset: _pinController.text.length); + _pinFocus.requestFocus(); + setState(() { + _attemptsRemaining = attemptsRemaining; + _pinIsWrong = true; + }); + }, + orElse: () {}, + ); }, ); } on CancellationException catch (_) {