--- a/src/eric7/WebBrowser/WebAuth/Fido2Management.py Mon Jul 22 10:15:41 2024 +0200 +++ b/src/eric7/WebBrowser/WebAuth/Fido2Management.py Mon Jul 22 15:24:27 2024 +0200 @@ -9,7 +9,7 @@ import time from fido2.ctap import CtapError -from fido2.ctap2 import ClientPin, CredentialManagement, Ctap2 +from fido2.ctap2 import ClientPin, Config, CredentialManagement, Ctap2 from fido2.hid import CtapHidDevice from fido2.webauthn import PublicKeyCredentialUserEntity from PyQt6.QtCore import QCoreApplication, QObject, QThread, pyqtSignal @@ -168,10 +168,13 @@ # PIN related data if self.__ctap2.info.options["clientPin"]: - if self.__ctap2.info.force_pin_change: - msg = self.tr( + msg1 = ( + self.tr( "PIN is disabled and must be changed before it can be used!" ) + if self.__ctap2.info.force_pin_change + else "" + ) pinRetries, powerCycle = self.getPinRetries() if pinRetries: if powerCycle: @@ -183,10 +186,16 @@ msg = self.tr("%n attempts remaining", "", pinRetries) else: msg = self.tr("PIN is blocked. The security key needs to be reset.") + if msg1: + msg += "\n" + msg1 else: msg = self.tr("A PIN has not been set.") data["pin"].append((self.tr("PIN"), msg)) + data["pin"].append( + (self.tr("Minimum PIN length"), str(self.__ctap2.info.min_pin_length)) + ) + alwaysUv = self.__ctap2.info.options.get("alwaysUv") msg = ( self.tr("not supported") @@ -554,7 +563,7 @@ return self.__ctap2.info.options.get("clientPin") - def forcedPinChange(self): + def pinChangeRequired(self): """ Public method to check for a forced PIN change. @@ -602,6 +611,7 @@ try: self.__clientPin.change_pin(old_pin=oldPin, new_pin=newPin) + self.reconnectToDevice() return True, self.tr("PIN was changed successfully.") except CtapError as err: return ( @@ -625,6 +635,7 @@ try: self.__clientPin.set_pin(pin=pin) + self.reconnectToDevice() return True, self.tr("PIN was set successfully.") except CtapError as err: return ( @@ -804,3 +815,162 @@ ) return CredentialManagement(self.__ctap2, self.__clientPin.protocol, pinToken) + + ############################################################################ + ## methods related to configuration handling + ############################################################################ + + def __initConfig(self, pin): + """ + Private method to initialize a configuration object. + + @param pin PIN to unlock the connected security key + @type str + @return reference to the configuration object + @rtype Config + @exception Fido2DeviceError raised to indicate an issue with the selected + security key + @exception Fido2PinError raised to indicate an issue with the PIN + """ + if self.__clientPin is None: + self.__clientPin = ClientPin(self.__ctap2) + + if pin == "": + pin = self.__pin + if pin is None: + # Error + raise Fido2PinError( + self.tr( + "The selected security key is not unlocked or no PIN was entered." + ) + ) + + if not Config.is_supported(self.__ctap2.info): + raise Fido2DeviceError( + self.tr("The selected security key does not support configuration.") + ) + + try: + pinToken = self.__clientPin.get_pin_token( + pin, ClientPin.PERMISSION.AUTHENTICATOR_CFG + ) + except CtapError as err: + raise Fido2PinError( + self.tr("PIN error: {0}").format(self.__pinErrorMessage(err)) + ) + except OSError: + raise Fido2DeviceError( + self.tr("Connected security key unplugged. Reinsert and try again.") + ) + + return Config(self.__ctap2, self.__clientPin.protocol, pinToken) + + def forcePinChangeSupported(self): + """ + Public method to check, if the 'forcePinChange' function is supported by the + selected security key. + + @return flag indicating support + @rtype bool + """ + if ( + self.__ctap2 is None + or self.__ctap2.info is None + or not self.__ctap2.info.options.get("setMinPINLength") + ): + return False + else: + return True + + def forcePinChange(self, pin): + """ + Public method to force the PIN to be changed to a new value before use. + + @param pin PIN to unlock the connected security key + @type str + """ + config = self.__initConfig(pin) + config.set_min_pin_length(force_change_pin=True) + self.reconnectToDevice() + + def canSetMinimumPinLength(self): + """ + Public method to check, if the 'setMinPINLength' function is available. + + @return flag indicating availability + @rtype bool + """ + if ( + self.__ctap2 is None + or self.__ctap2.info is None + or not self.__ctap2.info.options.get("setMinPINLength") + or ( + self.__ctap2.info.options.get("alwaysUv") + and not self.__ctap2.info.options.get("clientPin") + ) + ): + return False + else: + return True + + def setMinimumPinLength(self, pin, minLength): + """ + Public method to set the minimum PIN length. + + @param pin PIN to unlock the connected security key + @type str + @param minLength minimum PIN length + @type int + """ + if minLength < 4 or minLength > 63: + raise Fido2PinError( + self.tr("The minimum PIN length must be between 4 and 63.") + ) + if minLength < self.__ctap2.info.min_pin_length: + raise Fido2PinError( + self.tr("The minimum PIN length must be at least {0}.").format( + self.__ctap2.info.min_pin_length + ) + ) + + config = self.__initConfig(pin) + config.set_min_pin_length(min_pin_length=minLength) + self.reconnectToDevice() + + def canToggleAlwaysUv(self): + """ + Public method to check, if the 'toggleAlwaysUv' function is available. + + @return flag indicating availability + @rtype bool + """ + if ( + self.__ctap2 is None + or self.__ctap2.info is None + or "alwaysUv" not in self.__ctap2.info.options + ): + return False + else: + return True + + def getAlwaysUv(self): + """ + Public method to get the value of the 'alwaysUv' flag of the current security + key. + """ + if self.__ctap2 is None: + return False + + info = self.__ctap2.get_info() + return info is not None and info.options.get("alwaysUv", False) + + def toggleAlwaysUv(self, pin): + """ + Public method to toggle the 'alwaysUv' flag of the selected security key. + + @param pin PIN to unlock the connected security key + @type str + """ + config = self.__initConfig(pin) + config.toggle_always_uv() + self.reconnectToDevice()