Started implementing a dialog to manage FIDO2 security keys. eric7

Fri, 19 Jul 2024 18:06:48 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 19 Jul 2024 18:06:48 +0200
branch
eric7
changeset 10854
30c45bd597e6
parent 10853
1f651b204780
child 10855
9082eb8f6571

Started implementing a dialog to manage FIDO2 security keys.

eric7.epj file | annotate | diff | comparison | revisions
pyproject.toml file | annotate | diff | comparison | revisions
scripts/install-dependencies.py file | annotate | diff | comparison | revisions
scripts/install.py file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Fido2Management.py file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.py file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.ui file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Fido2PasskeyEditDialog.py file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Fido2PasskeyEditDialog.ui file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Fido2PinDialog.py file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Fido2PinDialog.ui file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Ui_Fido2ManagementDialog.py file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Ui_Fido2PasskeyEditDialog.py file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Ui_Fido2PinDialog.py file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/WebBrowserWebAuthDialog.py file | annotate | diff | comparison | revisions
src/eric7/eric7_fido2.py file | annotate | diff | comparison | revisions
src/eric7/icons/breeze-dark/locked.svg file | annotate | diff | comparison | revisions
src/eric7/icons/breeze-dark/unlocked.svg file | annotate | diff | comparison | revisions
src/eric7/icons/breeze-light/locked.svg file | annotate | diff | comparison | revisions
src/eric7/icons/breeze-light/unlocked.svg file | annotate | diff | comparison | revisions
--- a/eric7.epj	Fri Jul 19 11:54:29 2024 +0200
+++ b/eric7.epj	Fri Jul 19 18:06:48 2024 +0200
@@ -783,6 +783,9 @@
       "src/eric7/WebBrowser/VirusTotal/VirusTotalDomainReportDialog.ui",
       "src/eric7/WebBrowser/VirusTotal/VirusTotalIpReportDialog.ui",
       "src/eric7/WebBrowser/VirusTotal/VirusTotalWhoisDialog.ui",
+      "src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.ui",
+      "src/eric7/WebBrowser/WebAuth/Fido2PasskeyEditDialog.ui",
+      "src/eric7/WebBrowser/WebAuth/Fido2PinDialog.ui",
       "src/eric7/WebBrowser/WebAuth/WebBrowserWebAuthDialog.ui",
       "src/eric7/WebBrowser/WebBrowserClearPrivateDataDialog.ui",
       "src/eric7/WebBrowser/WebBrowserLanguagesDialog.ui",
@@ -2449,6 +2452,10 @@
       "src/eric7/WebBrowser/VirusTotal/VirusTotalIpReportDialog.py",
       "src/eric7/WebBrowser/VirusTotal/VirusTotalWhoisDialog.py",
       "src/eric7/WebBrowser/VirusTotal/__init__.py",
+      "src/eric7/WebBrowser/WebAuth/Fido2Management.py",
+      "src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.py",
+      "src/eric7/WebBrowser/WebAuth/Fido2PasskeyEditDialog.py",
+      "src/eric7/WebBrowser/WebAuth/Fido2PinDialog.py",
       "src/eric7/WebBrowser/WebAuth/WebBrowserWebAuthDialog.py",
       "src/eric7/WebBrowser/WebAuth/__init__.py",
       "src/eric7/WebBrowser/WebBrowserArgumentsCreator.py",
@@ -2485,6 +2492,7 @@
       "src/eric7/eric7_doc.py",
       "src/eric7/eric7_editor.py",
       "src/eric7/eric7_editor.pyw",
+      "src/eric7/eric7_fido2.py",
       "src/eric7/eric7_hexeditor.py",
       "src/eric7/eric7_hexeditor.pyw",
       "src/eric7/eric7_iconeditor.py",
--- a/pyproject.toml	Fri Jul 19 11:54:29 2024 +0200
+++ b/pyproject.toml	Fri Jul 19 18:06:48 2024 +0200
@@ -86,6 +86,7 @@
     "pipdeptree",
     "watchdog>= 3.0.0",
     "psutil",
+    "fido2",
     "pywin32>=1.0;platform_system=='Windows'",
 ]
 dynamic = ["version"]
--- a/scripts/install-dependencies.py	Fri Jul 19 11:54:29 2024 +0200
+++ b/scripts/install-dependencies.py	Fri Jul 19 18:06:48 2024 +0200
@@ -94,6 +94,7 @@
         "pyenchant",
         "wheel",
         "esprima",
+        "fido2",
     )
 
     if "--proxy" in sys.argv:
--- a/scripts/install.py	Fri Jul 19 11:54:29 2024 +0200
+++ b/scripts/install.py	Fri Jul 19 18:06:48 2024 +0200
@@ -1734,6 +1734,7 @@
         "pyenchant": ("enchant", ""),
         "wheel": ("wheel", ""),
         "esprima": ("esprima", ""),
+        "fido2": ("fido2", ""),
     }
     if withPyqt6Tools:
         optionalModulesList["qt6-applications"] = ("qt6_applications", "")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/WebBrowser/WebAuth/Fido2Management.py	Fri Jul 19 18:06:48 2024 +0200
@@ -0,0 +1,375 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a manager for FIDO2 security keys.
+"""
+
+from fido2.ctap import CtapError
+from fido2.ctap2 import ClientPin, CredentialManagement, Ctap2
+from fido2.hid import CtapHidDevice
+from fido2.webauthn import PublicKeyCredentialUserEntity
+from PyQt6.QtCore import QObject, pyqtSignal
+
+
+class Fido2PinError(Exception):
+    """
+    Class signaling an issue with the PIN.
+    """
+
+    pass
+
+
+class Fido2DeviceError(Exception):
+    """
+    Class signaling an issue with the device.
+    """
+
+    pass
+
+
+class Fido2Management(QObject):
+    """
+    Class implementing a manager for FIDO2 security keys.
+
+    @signal deviceConnected() emitted to indicate a connect to the security key
+    @signal deviceDisconnected() emitted to indicate a disconnect from the security key
+    """
+
+    deviceConnected = pyqtSignal()
+    deviceDisconnected = pyqtSignal()
+
+    def __init__(self, parent=None):
+        """
+        Constructor
+
+        @param parent reference to the parent object (defaults to None)
+        @type QObject (optional)
+        """
+        super().__init__(parent)
+
+        self.disconnectFromDevice()
+
+    def connectToDevice(self, device):
+        """
+        Public method to connect to a given security key.
+
+        @param device reference to the security key device class
+        @type CtapHidDevice
+        """
+        if self.__ctap2 is not None:
+            self.disconnectFromDevice()
+
+        self.__ctap2 = Ctap2(device)
+        self.__clientPin = ClientPin(self.__ctap2)
+        self.__pin = None
+
+        self.deviceConnected.emit()
+
+    def disconnectFromDevice(self):
+        """
+        Public method to disconnect from the current device.
+        """
+        self.__ctap2 = None
+        self.__clientPin = None
+        self.__pin = None
+
+        self.deviceDisconnected.emit()
+
+    def unlockDevice(self, pin):
+        """
+        Public method to unlock the device (i.e. store the PIN for later use).
+
+        @param pin PIN to be stored
+        @type str
+        """
+        self.__pin = pin
+
+    def lockDevice(self):
+        """
+        Public method to lock the device (i.e. delete the stored PIN).
+        """
+        self.__pin = None
+
+    def isDeviceLocked(self):
+        """
+        Public method to check, if the device is in locked state (i.e. the stored PIN
+        is None).
+
+        @return flag indicating the locked state
+        @rtype bool
+        """
+        return self.__pin is None
+
+    def getDevices(self):
+        """
+        Public method to get a list of connected security keys.
+
+        @return list of connected security keys
+        @rtype list of CtapHidDevice
+        """
+        return list(CtapHidDevice.list_devices())
+
+    def getKeyInfo(self):
+        """
+        Public method to get information about the connected security key.
+
+        @return dictionary containing the info data
+        @rtype dict[str, Any]
+        """
+        # TODO: not implemented yet
+        return {}
+
+    def resetDevice(self):
+        """
+        Public method to reset the connected security key.
+        """
+        # TODO: not implemented yet
+        pass
+
+    ############################################################################
+    ## methods related to PIN handling
+    ############################################################################
+
+    def getMinimumPinLength(self):
+        """
+        Public method to get the minimum PIN length defined by the security key.
+
+        @return minimum length for the PIN
+        @rtype int
+        """
+        if self.__ctap2 is None:
+            return None
+        else:
+            return self.__ctap2.info.min_pin_length
+
+    def hasPin(self):
+        """
+        Public method to check, if the connected security key has a PIN set.
+
+        @return flag indicating that a PIN has been set or None in case no device
+            was connected yet or it does not support PIN
+        @rtype bool or None
+        """
+        if self.__ctap2 is None:
+            return None
+
+        return self.__ctap2.info.options.get("clientPin")
+
+    def forcedPinChange(self):
+        """
+        Public method to check for a forced PIN change.
+
+        @return flag indicating a forced PIN change is required
+        @rtype bool
+        """
+        if self.__ctap2 is None:
+            return False
+
+        return self.__ctap2.info.force_pin_change
+
+    def getPinRetries(self):
+        """
+        Public method to get the number of PIN retries left and an indication for the
+        need of a power cycle.
+
+        @return tuple containing the number of retries left and a flag indicating a
+            power cycle is required
+        @rtype tuple of (int, bool)
+        """
+        if self.__ctap2 is None or self.__clientPin is None:
+            return (None, None)
+
+        return self.__clientPin.get_pin_retries()
+
+    def changePin(self, pin, newPin):
+        """
+        Public method to change the PIN of the connected security key.
+
+        @param pin current PIN
+        @type str
+        @param newPin new PIN
+        @type str
+        """
+        # TODO: not implemented yet
+        pass
+
+    def setPin(self, pin):
+        """
+        Public method to set a PIN for the connected security key.
+
+        @param pin PIN to be set
+        @type str
+        """
+        # TODO: not implemented yet
+        pass
+
+    def verifyPin(self, pin):
+        """
+        Public method to verify a given PIN.
+
+        A successful verification of the PIN will reset the "retries" counter.
+
+        @param pin PIN to be verified
+        @type str
+        @return flag indicating successful verification and a verification message
+        @rtype tuple of (bool, str)
+        """
+        if self.__ctap2 is None or self.__clientPin is None:
+            return False
+
+        try:
+            self.__clientPin.get_pin_token(
+                pin, ClientPin.PERMISSION.GET_ASSERTION, "eric-ide.python-projects.org"
+            )
+            return True, self.tr("PIN verified")
+        except CtapError as err:
+            return (
+                False,
+                self.tr("<p>PIN verification failed.</p><p>Reason: {0}").format(
+                    self.__pinErrorMessage(err)
+                ),
+            )
+
+    def __pinErrorMessage(self, err):
+        """
+        Private method to get a message for a PIN error.
+
+        @param err reference to the exception object
+        @type CtapError
+        @return message for the given PIN error
+        @rtype str
+        """
+        errorCode = err.code
+        if errorCode == CtapError.ERR.PIN_INVALID:
+            msg = self.tr("Invalid PIN")
+        elif errorCode == CtapError.ERR.PIN_BLOCKED:
+            msg = self.tr("PIN is blocked.")
+        elif errorCode == CtapError.ERR.PIN_NOT_SET:
+            msg = self.tr("No PIN set.")
+        else:
+            msg = str(err)
+        return msg
+
+    ############################################################################
+    ## methods related to passkey (credential) handling
+    ############################################################################
+
+    def getPasskeys(self, pin):
+        """
+        Public method to get all stored passkeys.
+
+        @param pin PIN to unlock the connected security key
+        @type str
+        @return tuple containing a dictionary containing the stored passkeys grouped
+            by Relying Party ID, the count of used credential slots and the count
+            of available credential slots
+        @rtype tuple of [dict[str, list[dict[str, Any]]], int, int]
+        """
+        credentials = {}
+
+        credentialManager = self.__initializeCredentialManager(pin)
+        data = credentialManager.get_metadata()
+        if data.get(CredentialManagement.RESULT.EXISTING_CRED_COUNT) > 0:
+            for relyingParty in credentialManager.enumerate_rps():
+                relyingPartyId = relyingParty[CredentialManagement.RESULT.RP]["id"]
+                credentials[relyingPartyId] = []
+                for credential in credentialManager.enumerate_creds(
+                    relyingParty[CredentialManagement.RESULT.RP_ID_HASH]
+                ):
+                    credentials[relyingPartyId].append(
+                        {
+                            "credentialId": credential[
+                                CredentialManagement.RESULT.CREDENTIAL_ID
+                            ],
+                            "userId": credential[CredentialManagement.RESULT.USER][
+                                "id"
+                            ],
+                            "userName": credential[
+                                CredentialManagement.RESULT.USER
+                            ].get("name", ""),
+                            "displayName": credential[
+                                CredentialManagement.RESULT.USER
+                            ].get("displayName", ""),
+                        }
+                    )
+
+        return (
+            credentials,
+            data.get(CredentialManagement.RESULT.EXISTING_CRED_COUNT),
+            data.get(CredentialManagement.RESULT.MAX_REMAINING_COUNT),
+        )
+
+    def deletePasskey(self, pin, credentialId):
+        """
+        Public method to delete the passkey of the given ID.
+
+        @param pin PIN to unlock the connected security key
+        @type str
+        @param credentialId ID of the passkey to be deleted
+        @type fido2.webauthn.PublicKeyCredentialDescriptor
+        """
+        credentialManager = self.__initializeCredentialManager(pin)
+        credentialManager.delete_cred(cred_id=credentialId)
+
+    def changePasskeyUserInfo(self, pin, credentialId, userId, userName, displayName):
+        """
+        Public method to change the user info of a stored passkey.
+
+        @param pin PIN to unlock the connected security key
+        @type str
+        @param credentialId ID of the passkey to change
+        @type fido2.webauthn.PublicKeyCredentialDescriptor
+        @param userId ID of the user
+        @type bytes
+        @param userName user name to set
+        @type str
+        @param displayName display name to set
+        @type str
+        """
+        userInfo = PublicKeyCredentialUserEntity(
+            name=userName, id=userId, display_name=displayName
+        )
+        credentialManager = self.__initializeCredentialManager(pin)
+        credentialManager.update_user_info(cred_id=credentialId, user_info=userInfo)
+
+    def __initializeCredentialManager(self, pin):
+        """
+        Private method to initialize a credential manager object.
+
+        @param pin PIN to unlock the connected security key
+        @type str
+        @return reference to the credential manager object
+        @rtype CredentialManagement
+        @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."
+                )
+            )
+
+        try:
+            pinToken = self.__clientPin.get_pin_token(
+                pin, ClientPin.PERMISSION.CREDENTIAL_MGMT
+            )
+        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 CredentialManagement(self.__ctap2, self.__clientPin.protocol, pinToken)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.py	Fri Jul 19 18:06:48 2024 +0200
@@ -0,0 +1,472 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to manage FIDO2 security keys.
+"""
+
+from PyQt6.QtCore import Qt, QTimer, pyqtSlot
+from PyQt6.QtWidgets import QDialog, QTreeWidgetItem
+
+from eric7.EricGui import EricPixmapCache
+from eric7.EricGui.EricOverrideCursor import EricOverrideCursor
+from eric7.EricWidgets import EricMessageBox
+
+from .Fido2Management import Fido2DeviceError, Fido2Management, Fido2PinError
+from .Fido2PinDialog import Fido2PinDialog, Fido2PinDialogMode
+from .Ui_Fido2ManagementDialog import Ui_Fido2ManagementDialog
+
+
+class Fido2ManagementDialog(QDialog, Ui_Fido2ManagementDialog):
+    """
+    Class implementing a dialog to manage FIDO2 security keys.
+    """
+
+    CredentialIdRole = Qt.ItemDataRole.UserRole
+    UserIdRole = Qt.ItemDataRole.UserRole + 1
+
+    RelyingPartyColumn = 0
+    CredentialIdColumn = 1
+    DisplayNameColumn = 2
+    UserNameColumn = 3
+
+    def __init__(self, parent=None):
+        """
+        Constructor
+
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.reloadButton.setIcon(EricPixmapCache.getIcon("reload"))
+        self.lockButton.setIcon(EricPixmapCache.getIcon("locked"))
+
+        self.reloadButton.clicked.connect(self.__populateDeviceSelector)
+
+        self.__manager = Fido2Management(parent=self)
+        ##self.__manager.deviceConnected.connect(self.__deviceConnected)
+        ##self.__manager.deviceDisconnected.connect(self.__deviceDisconnected)
+
+        QTimer.singleShot(0, self.__populateDeviceSelector)
+
+    ############################################################################
+    ## methods related to device handling
+    ############################################################################
+
+    @pyqtSlot()
+    def __populateDeviceSelector(self):
+        """
+        Private slot to populate the device selector combo box.
+        """
+        self.__manager.disconnectFromDevice()
+        self.securityKeysComboBox.clear()
+        self.reloadButton.setEnabled(False)
+
+        securityKeys = self.__manager.getDevices()
+
+        if len(securityKeys) != 1:
+            self.securityKeysComboBox.addItem("")
+        for securityKey in securityKeys:
+            self.securityKeysComboBox.addItem(
+                self.tr("{0} ({1})").format(
+                    securityKey.product_name, securityKey.descriptor.path
+                ),
+                securityKey,
+            )
+
+        self.reloadButton.setEnabled(True)
+
+        if len(securityKeys) == 0:
+            EricMessageBox.information(
+                self,
+                self.tr("FIDO2 Security Key Management"),
+                self.tr(
+                    """No security key could be detected. Attach a key and press"""
+                    """ the "Reload" button."""
+                ),
+            )
+
+    @pyqtSlot(int)
+    def on_securityKeysComboBox_currentIndexChanged(self, index):
+        """
+        Private slot handling the selection of security key.
+
+        @param index index of the selected security key
+        @type int
+        """
+        self.lockButton.setChecked(False)
+        self.__manager.disconnectFromDevice()
+
+        securityKey = self.securityKeysComboBox.itemData(index)
+
+        self.lockButton.setEnabled(securityKey is not None)
+        self.pinButton.setEnabled(securityKey is not None)
+        self.showInfoButton.setEnabled(securityKey is not None)
+        self.resetButton.setEnabled(securityKey is not None)
+        self.loadPasskeysButton.setEnabled(securityKey is not None)
+
+        if securityKey is not None:
+            self.__manager.connectToDevice(securityKey)
+            hasPin = self.__manager.hasPin()
+            forcedPinChange = self.__manager.forcedPinChange()
+            if hasPin is True:
+                self.pinButton.setText(self.tr("Change PIN"))
+            elif hasPin is False:
+                self.pinButton.setText(self.tr("Set PIN"))
+            else:
+                self.pinButton.setEnabled(False)
+            if forcedPinChange or hasPin is False:
+                self.lockButton.setEnabled(False)
+                self.loadPasskeysButton.setEnabled(False)
+                msg = (
+                    self.tr("A PIN change is required.")
+                    if forcedPinChange
+                    else self.tr("You must set a PIN first.")
+                )
+                EricMessageBox.information(
+                    self,
+                    self.tr("FIDO2 Security Key Management"),
+                    msg,
+                )
+
+        self.passkeysList.clear()
+        self.on_passkeysList_itemSelectionChanged()
+
+    @pyqtSlot(bool)
+    def on_lockButton_toggled(self, checked):
+        """
+        Private slot to handle the toggling of the device locked status.
+
+        @param checked state of the lock/unlock button
+        @type bool
+        """
+        if checked:
+            # unlock the selected security key
+            pin = self.__getRequiredPin(self.tr("Unlock Security Key"))
+            if pin:
+                ok, msg = self.__manager.verifyPin(pin=pin)
+                if ok:
+                    self.lockButton.setIcon(EricPixmapCache.getIcon("unlocked"))
+                    self.__manager.unlockDevice(pin)
+                else:
+                    EricMessageBox.critical(
+                        self,
+                        self.tr("Unlock Security Key"),
+                        msg,
+                    )
+                    self.lockButton.setChecked(False)
+            else:
+                self.lockButton.setChecked(False)
+        else:
+            # lock the selected security key
+            self.lockButton.setIcon(EricPixmapCache.getIcon("locked"))
+            self.__manager.lockDevice()
+
+    @pyqtSlot()
+    def on_showInfoButton_clicked(self):
+        """
+        Slot documentation goes here.
+        """
+        # TODO: not implemented yet
+        pass
+
+    ############################################################################
+    ## methods related to PIN handling
+    ############################################################################
+
+    def __checkPinStatus(self, feature):
+        """
+        Private method to check the PIN status of the connected security key.
+
+        @param feature name of the feature requesting the PIN (defaults to None)
+        @type str (optional)
+        @return flag indicating a positive status
+        @rtype bool
+        """
+        feature = self.tr("This feature") if feature is None else f"'{feature}'"
+
+        hasPin = self.__manager.hasPin()
+        retries, powerCycle = self.__manager.getPinRetries()
+
+        if hasPin is None:
+            msg = self.tr("{0} is not supported by the selected security key.").format(
+                feature
+            )
+        elif not hasPin:
+            msg = self.tr("{0} requires having a PIN. Set a PIN first.").format(feature)
+        elif self.__manager.forcedPinChange():
+            msg = self.tr("The security key is locked. Change the PIN first.")
+        elif powerCycle:
+            msg = self.tr(
+                "The security key is locked because the wrong PIN was entered "
+                "too many times. To unlock it, remove and reinsert it."
+            )
+        elif retries == 0:
+            msg = self.tr(
+                "The security key is locked because the wrong PIN was entered too"
+                " many times. You will need to reset the security key."
+            )
+        else:
+            msg = ""
+
+        if msg:
+            EricMessageBox.critical(
+                self,
+                self.tr("FIDO2 Security Key Management"),
+                msg,
+            )
+            return False
+        else:
+            return True
+
+    def __getRequiredPin(self, feature=None):
+        """
+        Private method to check, if a pin has been set for the selected device, and
+        ask the user to enter it.
+
+        @param feature name of the feature requesting the PIN (defaults to None)
+        @type str (optional)
+        @return PIN of the selected security key or None in case of an issue
+        @rtype str or None
+        """
+        if not self.__checkPinStatus(feature=feature):
+            return None
+        else:
+            if self.__manager.isDeviceLocked():
+                retries = self.__manager.getPinRetries()[0]
+                title = self.tr("PIN required") if feature is None else feature
+                dlg = Fido2PinDialog(
+                    mode=Fido2PinDialogMode.GET,
+                    title=title,
+                    message=self.tr(
+                        "Enter the PIN to unlock the security key (%n attempt(s)"
+                        " remaining.",
+                        "",
+                        retries,
+                    ),
+                    minLength=self.__manager.getMinimumPinLength(),
+                    parent=self,
+                )
+                if dlg.exec() == QDialog.DialogCode.Accepted:
+                    return dlg.getPins()[0]
+                else:
+                    return None
+            else:
+                return ""
+
+    @pyqtSlot()
+    def __setPin(self):
+        """
+        Private slot to set a PIN for the selected security key.
+        """
+        # TODO: not implemented yet
+        pass
+
+    @pyqtSlot()
+    def __changePin(self):
+        """
+        Private slot to set a PIN for the selected security key.
+        """
+        # TODO: not implemented yet
+        pass
+
+    @pyqtSlot()
+    def on_pinButton_clicked(self):
+        """
+        Private slot to set or change the PIN for the selected security key.
+        """
+        # TODO: not implemented yet
+        if self.__manager.hasPin():
+            self.__changePin()
+        else:
+            self.__setPin()
+
+    ############################################################################
+    ## methods related to passkeys handling
+    ############################################################################
+
+    @pyqtSlot()
+    def __populatePasskeysList(self):
+        """
+        Private slot to populate the list of store passkeys of the selected security
+        key.
+        """
+        keyIndex = self.securityKeysComboBox.currentData()
+        if keyIndex is None:
+            return
+
+        pin = self.__getRequiredPin(feature=self.tr("Credential Management"))
+        if pin is None:
+            return
+
+        self.passkeysList.clear()
+
+        try:
+            with EricOverrideCursor():
+                passkeys, existingCount, remainingCount = self.__manager.getPasskeys(
+                    pin=pin
+                )
+        except (Fido2DeviceError, Fido2PinError) as err:
+            self.__handleError(
+                error=err,
+                title=self.tr("Load Passkeys"),
+                message=self.tr("The stored passkeys could not be loaded."),
+            )
+            return
+
+        self.existingCountLabel.setText(str(existingCount))
+        self.remainingCountLabel.setText(str(remainingCount))
+
+        for relyingParty in passkeys:
+            rpItem = QTreeWidgetItem(self.passkeysList, [relyingParty])
+            rpItem.setFirstColumnSpanned(True)
+            rpItem.setExpanded(True)
+            for passDict in passkeys[relyingParty]:
+                item = QTreeWidgetItem(
+                    rpItem,
+                    [
+                        "",
+                        passDict["credentialId"]["id"].hex(),
+                        passDict["displayName"],
+                        passDict["userName"],
+                    ],
+                )
+                item.setData(0, self.CredentialIdRole, passDict["credentialId"])
+                item.setData(0, self.UserIdRole, passDict["userId"])
+
+        self.passkeysList.sortItems(self.DisplayNameColumn, Qt.SortOrder.AscendingOrder)
+        self.passkeysList.sortItems(
+            self.RelyingPartyColumn, Qt.SortOrder.AscendingOrder
+        )
+
+    @pyqtSlot()
+    def on_loadPasskeysButton_clicked(self):
+        """
+        Slot documentation goes here.
+        """
+        self.__populatePasskeysList()
+
+    @pyqtSlot()
+    def on_passkeysList_itemSelectionChanged(self):
+        """
+        Slot documentation goes here.
+        """
+        enableButtons = (
+            len(self.passkeysList.selectedItems()) == 1
+            and self.passkeysList.selectedItems()[0].parent() is not None
+        )
+        self.editButton.setEnabled(enableButtons)
+        self.deleteButton.setEnabled(enableButtons)
+
+    @pyqtSlot()
+    def on_editButton_clicked(self):
+        """
+        Private slot to edit the selected passkey.
+        """
+        from .Fido2PasskeyEditDialog import Fido2PasskeyEditDialog
+
+        selectedItem = self.passkeysList.selectedItems()[0]
+        dlg = Fido2PasskeyEditDialog(
+            displayName=selectedItem.text(self.DisplayNameColumn),
+            userName=selectedItem.text(self.UserNameColumn),
+            relyingParty=selectedItem.parent().text(self.RelyingPartyColumn),
+            parent=self,
+        )
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            displayName, userName = dlg.getData()
+            if displayName != selectedItem.text(
+                self.DisplayNameColumn
+            ) or userName != selectedItem.text(self.UserNameColumn):
+                # only change on the security key, if there is really a change
+                pin = self.__getRequiredPin(feature=self.tr("Change User Info"))
+                try:
+                    self.__manager.changePasskeyUserInfo(
+                        pin=pin,
+                        credentialId=selectedItem.data(0, self.CredentialIdRole),
+                        userId=selectedItem.data(0, self.UserIdRole),
+                        userName=userName,
+                        displayName=displayName,
+                    )
+                except (Fido2DeviceError, Fido2PinError) as err:
+                    self.__handleError(
+                        error=err,
+                        title=self.tr("Change User Info"),
+                        message=self.tr("The user info could not be changed."),
+                    )
+                    return
+
+                selectedItem.setText(self.DisplayNameColumn, displayName)
+                selectedItem.setText(self.UserNameColumn, userName)
+
+    @pyqtSlot()
+    def on_deleteButton_clicked(self):
+        """
+        Private slot to delete the selected passkey.
+        """
+        selectedItem = self.passkeysList.selectedItems()[0]
+
+        ok = EricMessageBox.yesNo(
+            self,
+            self.tr("Delete Passkey"),
+            self.tr(
+                "<p>Shall the selected passkey really be deleted?</p>"
+                "<ul>"
+                "<li>Relying Party: {0}</li>"
+                "<li>Display Name: {1}</li>"
+                "<li>User Name: {2}</li>"
+                "</ul>"
+            ).format(
+                selectedItem.parent().text(self.RelyingPartyColumn),
+                selectedItem.text(self.DisplayNameColumn),
+                selectedItem.text(self.UserNameColumn),
+            ),
+        )
+        if ok:
+            pin = self.__getRequiredPin(feature=self.tr("Delete Passkey"))
+            try:
+                self.__manager.deletePasskey(
+                    pin=pin,
+                    credentialId=selectedItem.data(0, self.CredentialIdRole),
+                )
+            except (Fido2DeviceError, Fido2PinError) as err:
+                self.__handleError(
+                    error=err,
+                    title=self.tr("Delete Passkey"),
+                    message=self.tr("The passkey could not be deleted."),
+                )
+                return
+
+            rpItem = selectedItem.parent()
+            index = rpItem.indexOfChild(selectedItem)
+            rpItem.takeChild(index)
+            del selectedItem
+            if rpItem.childCount() == 0:
+                index = self.passkeysList.indexOfTopLevelItem(rpItem)
+                self.passkeysList.takeTopLevelItem(index)
+                del rpItem
+
+    ############################################################################
+    ## utility methods
+    ############################################################################
+
+    def __handleError(self, error, title, message):
+        """
+        Private method to handle an error reported by the manager.
+
+        @param error reference to the exception object
+        @type Exception
+        @param title tirle of the message box
+        @type str
+        @param message message to be shown
+        @type str
+        """
+        EricMessageBox.critical(
+            self,
+            title,
+            self.tr("<p>{0}</p><p>Reason: {1}</p>").format(message, str(error)),
+        )
+        if isinstance(error, Fido2DeviceError):
+            self.__populateDeviceSelector()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.ui	Fri Jul 19 18:06:48 2024 +0200
@@ -0,0 +1,341 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Fido2ManagementDialog</class>
+ <widget class="QDialog" name="Fido2ManagementDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>800</width>
+    <height>700</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>FIDO2 Security Key Management</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QToolButton" name="reloadButton">
+       <property name="toolTip">
+        <string>Press to reload the list of detected security keys.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QComboBox" name="securityKeysComboBox">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="toolTip">
+        <string>Select the security keys to be managed.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="lockButton">
+       <property name="toolTip">
+        <string>Press to unlock the security key, release to lock it again.</string>
+       </property>
+       <property name="checkable">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="pinButton">
+       <property name="toolTip">
+        <string>Press to set or change the PIN of the selected security key.</string>
+       </property>
+       <property name="text">
+        <string>Set PIN</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="showInfoButton">
+       <property name="toolTip">
+        <string>Press to show a dialog with technical data of the selected security key.</string>
+       </property>
+       <property name="text">
+        <string>Show Info</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="Line" name="line_2">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="resetButton">
+       <property name="toolTip">
+        <string>Press to reset the selected security key.</string>
+       </property>
+       <property name="text">
+        <string>Reset Key</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string>Passkeys</string>
+     </property>
+     <layout class="QGridLayout" name="gridLayout">
+      <item row="0" column="0">
+       <layout class="QHBoxLayout" name="horizontalLayout_3">
+        <item>
+         <spacer name="horizontalSpacer">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <widget class="QPushButton" name="loadPasskeysButton">
+          <property name="toolTip">
+           <string>Press ro load the passkeys of the selected security key.</string>
+          </property>
+          <property name="text">
+           <string>Load Passkeys</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_2">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </item>
+      <item row="1" column="0">
+       <widget class="QTreeWidget" name="passkeysList">
+        <property name="editTriggers">
+         <set>QAbstractItemView::NoEditTriggers</set>
+        </property>
+        <property name="alternatingRowColors">
+         <bool>true</bool>
+        </property>
+        <property name="sortingEnabled">
+         <bool>true</bool>
+        </property>
+        <attribute name="headerDefaultSectionSize">
+         <number>150</number>
+        </attribute>
+        <column>
+         <property name="text">
+          <string>Domain</string>
+         </property>
+        </column>
+        <column>
+         <property name="text">
+          <string>Credential ID</string>
+         </property>
+        </column>
+        <column>
+         <property name="text">
+          <string>Display Name</string>
+         </property>
+        </column>
+        <column>
+         <property name="text">
+          <string>User Name</string>
+         </property>
+        </column>
+       </widget>
+      </item>
+      <item row="1" column="1" rowspan="2">
+       <widget class="Line" name="line">
+        <property name="orientation">
+         <enum>Qt::Vertical</enum>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="2" rowspan="2">
+       <layout class="QVBoxLayout" name="verticalLayout">
+        <item>
+         <widget class="QPushButton" name="editButton">
+          <property name="toolTip">
+           <string>Press to change the user info of the selected passkey.</string>
+          </property>
+          <property name="text">
+           <string>Edit...</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPushButton" name="deleteButton">
+          <property name="toolTip">
+           <string>Press to delete the selected passkey.</string>
+          </property>
+          <property name="text">
+           <string>Delete</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <spacer name="verticalSpacer">
+          <property name="orientation">
+           <enum>Qt::Vertical</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>20</width>
+            <height>40</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </item>
+      <item row="2" column="0">
+       <layout class="QHBoxLayout" name="horizontalLayout_2">
+        <item>
+         <spacer name="horizontalSpacer_3">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <widget class="QLabel" name="label">
+          <property name="text">
+           <string>Existing Passkeys:</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QLabel" name="existingCountLabel"/>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_5">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <widget class="QLabel" name="label_2">
+          <property name="text">
+           <string>Max. Remaining Passkeys:</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QLabel" name="remainingCountLabel"/>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_4">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Close</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>securityKeysComboBox</tabstop>
+  <tabstop>lockButton</tabstop>
+  <tabstop>pinButton</tabstop>
+  <tabstop>showInfoButton</tabstop>
+  <tabstop>loadPasskeysButton</tabstop>
+  <tabstop>passkeysList</tabstop>
+  <tabstop>editButton</tabstop>
+  <tabstop>deleteButton</tabstop>
+  <tabstop>reloadButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>Fido2ManagementDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>Fido2ManagementDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/WebBrowser/WebAuth/Fido2PasskeyEditDialog.py	Fri Jul 19 18:06:48 2024 +0200
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog for editing passkey parameters.
+"""
+
+from PyQt6.QtCore import Qt, pyqtSlot
+from PyQt6.QtWidgets import QDialog, QDialogButtonBox
+
+from .Ui_Fido2PasskeyEditDialog import Ui_Fido2PasskeyEditDialog
+
+
+class Fido2PasskeyEditDialog(QDialog, Ui_Fido2PasskeyEditDialog):
+    """
+    Class implementing a dialog for editing passkey parameters.
+    """
+
+    def __init__(self, displayName, userName, relyingParty, parent=None):
+        """
+        Constructor
+
+        @param displayName string to be shown for this passkey
+        @type str
+        @param userName user name of this passkey
+        @type str
+        @param relyingParty relying part this passkey belongs to
+        @type str
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.displayNameEdit.textChanged.connect(self.__updateOk)
+        self.userNameEdit.textChanged.connect(self.__updateOk)
+
+        self.headerLabel.setText(
+            self.tr("<b>Passkey Parameters for {0}</b>").format(relyingParty)
+        )
+        self.displayNameEdit.setText(displayName)
+        self.userNameEdit.setText(userName)
+
+        self.displayNameEdit.setFocus(Qt.FocusReason.OtherFocusReason)
+        self.displayNameEdit.selectAll()
+
+    @pyqtSlot()
+    def __updateOk(self):
+        """
+        Private method to update the state of the OK button.
+        """
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(
+            bool(self.displayNameEdit.text()) and bool(self.userNameEdit.text())
+        )
+
+    def getData(self):
+        """
+        Public method to get the entered data.
+
+        @return tuple containing the display and user names
+        @rtype tuple[str, str]
+        """
+        return self.displayNameEdit.text(), self.userNameEdit.text()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/WebBrowser/WebAuth/Fido2PasskeyEditDialog.ui	Fri Jul 19 18:06:48 2024 +0200
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Fido2PasskeyEditDialog</class>
+ <widget class="QDialog" name="Fido2PasskeyEditDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>174</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Edit Passkey Data</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QLabel" name="headerLabel">
+     <property name="text">
+      <string/>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Display Name:</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLineEdit" name="displayNameEdit">
+     <property name="toolTip">
+      <string>Enter the display name.</string>
+     </property>
+     <property name="clearButtonEnabled">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>User Name:</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLineEdit" name="userNameEdit">
+     <property name="toolTip">
+      <string>Enter the user name.</string>
+     </property>
+     <property name="clearButtonEnabled">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>Fido2PasskeyEditDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>Fido2PasskeyEditDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/WebBrowser/WebAuth/Fido2PinDialog.py	Fri Jul 19 18:06:48 2024 +0200
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to enter the current and potentially new PIN.
+"""
+
+import enum
+
+from PyQt6.QtCore import pyqtSlot
+from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QLineEdit
+
+from eric7.EricGui import EricPixmapCache
+
+from .Ui_Fido2PinDialog import Ui_Fido2PinDialog
+
+
+class Fido2PinDialogMode(enum.Enum):
+    """
+    Class defining the various PIN dialog mode.
+    """
+
+    GET = 0
+    SET = 1
+    CHANGE = 2
+
+
+class Fido2PinDialog(QDialog, Ui_Fido2PinDialog):
+    """
+    Class implementing a dialog to enter the current and potentially new PIN.
+    """
+
+    def __init__(self, mode, title, message, minLength, parent=None):
+        """
+        Constructor
+
+        @param mode mode of the dialog
+        @type Fido2PinDialogMode
+        @param title header title to be shown
+        @type str
+        @param message more decriptive text to be shown
+        @type str
+        @param minLength minimum PIN length
+        @type int
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.pinButton.setIcon(EricPixmapCache.getIcon("showPassword"))
+        self.newPinButton.setIcon(EricPixmapCache.getIcon("showPassword"))
+
+        self.__minLength = minLength
+        self.__mode = mode
+
+        if title:
+            self.headerLabel.setText(f"<b>{title}</b>")
+        else:
+            self.headerLabel.setVisible(False)
+        if message:
+            self.descriptionLabel.setText(message)
+        else:
+            self.descriptionLabel.setVisible(False)
+        self.pinErrorLabel.setVisible(False)
+
+        if mode == Fido2PinDialogMode.GET:
+            self.newPinGroupBox.setVisible(False)
+        elif mode == Fido2PinDialogMode.SET:
+            self.pinLabel.setVisible(False)
+            self.pinEdit.setVisible(False)
+            self.pinButton.setVisible(False)
+        elif mode == Fido2PinDialogMode.CHANGE:
+            # all entries visible
+            pass
+
+        self.pinEdit.textEdited.connect(self.__checkPins)
+        self.newPinEdit.textEdited.connect(self.__checkPins)
+        self.confirmNewPinEdit.textEdited.connect(self.__checkPins)
+
+        self.__checkPins()
+
+    @pyqtSlot()
+    def __checkPins(self):
+        """
+        Private slot to check the entered PIN(s).
+
+        Appropriate error messages are shown in case of issues and the state of
+        the OK button is set accordingly.
+        """
+        messages = []
+
+        if (
+            self.__mode in (Fido2PinDialogMode.GET, Fido2PinDialogMode.CHANGE)
+            and not self.pinEdit.text()
+        ):
+            messages.append(self.tr("PIN must not be empty."))
+        if self.__mode in (Fido2PinDialogMode.SET, Fido2PinDialogMode.CHANGE):
+            if len(self.newPinEdit.text()) < self.__minLength:
+                messages.append(
+                    self.tr("New PIN is TOO short (minimum length: {0}).").format(
+                        self.__minLength
+                    )
+                )
+            if (
+                self.confirmNewPinEdit.isVisible()
+                and self.confirmNewPinEdit.text() != self.newPinEdit.text()
+            ):
+                messages.append("New PIN confirmation does not match.")
+        if (
+            self.__mode == Fido2PinDialogMode.CHANGE
+            and self.pinEdit.text() == self.newPinEdit.text()
+        ):
+            messages.append(self.tr("Old and new PIN must not be identical."))
+
+        self.__showPinErrors(messages)
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(
+            not bool(messages)
+        )
+
+    def __showPinErrors(self, errorMessages):
+        """
+        Private method to show some error messages.
+
+        @param errorMessages list of error messages
+        @type list of str
+        """
+        if not errorMessages:
+            self.pinErrorLabel.clear()
+            self.pinErrorLabel.setVisible(False)
+        else:
+            if len(errorMessages) == 1:
+                msg = errorMessages[0]
+            else:
+                msg = "<ul><li>{0}</li></ul>".format("</li><li>".join(errorMessages))
+            self.pinErrorLabel.setText(msg)
+            self.pinErrorLabel.setVisible(True)
+
+        self.adjustSize()
+
+    @pyqtSlot(bool)
+    def on_pinButton_toggled(self, checked):
+        """
+        Private slot to handle the toggling of the PIN visibility.
+
+        @param checked state of the PIN visibility button
+        @type bool
+        """
+        if checked:
+            self.pinButton.setIcon(EricPixmapCache.getIcon("hidePassword"))
+            self.pinEdit.setEchoMode(QLineEdit.EchoMode.Normal)
+        else:
+            self.pinButton.setIcon(EricPixmapCache.getIcon("showPassword"))
+            self.pinEdit.setEchoMode(QLineEdit.EchoMode.Password)
+
+    @pyqtSlot(bool)
+    def on_newPinButton_toggled(self, checked):
+        """
+        Private slot to handle the toggling of the new PIN visibility.
+
+        @param checked state of the new PIN visibility button
+        @type bool
+        """
+        if checked:
+            self.newPinButton.setIcon(EricPixmapCache.getIcon("hidePassword"))
+            self.newPinEdit.setEchoMode(QLineEdit.EchoMode.Normal)
+        else:
+            self.newPinButton.setIcon(EricPixmapCache.getIcon("showPassword"))
+            self.newPinEdit.setEchoMode(QLineEdit.EchoMode.Password)
+
+        self.confirmnewPinLabel.setVisible(not checked)
+        self.confirmPinnewEdit.setVisible(not checked)
+        self.on_newPinEdit_textEdited(self.newPinEdit.text())
+
+    def getPins(self):
+        """
+        Public method to get the entered PINs.
+
+        @return tuple containing the current and new PIN
+        @rtype tuple of (str, str)
+        """
+        if self.__mode == Fido2PinDialogMode.GET:
+            return self.pinEdit.text(), None
+        elif self.__mode == Fido2PinDialogMode.SET:
+            return None, self.newPinEdit.text()
+        elif self.__mode == Fido2PinDialogMode.GET:
+            return self.pinEdit.text(), self.newPinEdit.text()
+        else:
+            return None, None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/WebBrowser/WebAuth/Fido2PinDialog.ui	Fri Jul 19 18:06:48 2024 +0200
@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Fido2PinDialog</class>
+ <widget class="QDialog" name="Fido2PinDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>614</width>
+    <height>251</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>PIN Entry</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QLabel" name="headerLabel">
+     <property name="text">
+      <string notr="true">Header</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="descriptionLabel">
+     <property name="text">
+      <string notr="true">Description</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="pinLabel">
+       <property name="text">
+        <string>PIN:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLineEdit" name="pinEdit">
+       <property name="toolTip">
+        <string>Enter the PIN</string>
+       </property>
+       <property name="echoMode">
+        <enum>QLineEdit::Password</enum>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="pinButton">
+       <property name="toolTip">
+        <string>Press to show or hide the PIN.</string>
+       </property>
+       <property name="checkable">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="newPinGroupBox">
+     <property name="title">
+      <string>New PIN</string>
+     </property>
+     <property name="flat">
+      <bool>true</bool>
+     </property>
+     <layout class="QGridLayout" name="gridLayout">
+      <item row="0" column="0">
+       <widget class="QLabel" name="newPinLabel">
+        <property name="text">
+         <string>PIN:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="QLabel" name="confirmNewPinLabel">
+        <property name="text">
+         <string>Confirm PIN:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="1">
+       <widget class="QLineEdit" name="confirmNewPinEdit">
+        <property name="toolTip">
+         <string>Enter the same PIN again.</string>
+        </property>
+        <property name="echoMode">
+         <enum>QLineEdit::Password</enum>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="QLineEdit" name="newPinEdit">
+        <property name="toolTip">
+         <string>Enter the new PIN</string>
+        </property>
+        <property name="echoMode">
+         <enum>QLineEdit::Password</enum>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="2">
+       <widget class="QToolButton" name="newPinButton">
+        <property name="toolTip">
+         <string>Press to show or hide the new PIN.</string>
+        </property>
+        <property name="checkable">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="pinErrorLabel">
+     <property name="text">
+      <string notr="true">PIN Error</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>Fido2PinDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>Fido2PinDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/WebBrowser/WebAuth/Ui_Fido2ManagementDialog.py	Fri Jul 19 18:06:48 2024 +0200
@@ -0,0 +1,153 @@
+# Form implementation generated from reading ui file 'src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.ui'
+#
+# Created by: PyQt6 UI code generator 6.7.0
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic6 is
+# run again.  Do not edit this file unless you know what you are doing.
+
+
+from PyQt6 import QtCore, QtGui, QtWidgets
+
+
+class Ui_Fido2ManagementDialog(object):
+    def setupUi(self, Fido2ManagementDialog):
+        Fido2ManagementDialog.setObjectName("Fido2ManagementDialog")
+        Fido2ManagementDialog.resize(800, 700)
+        Fido2ManagementDialog.setSizeGripEnabled(True)
+        self.verticalLayout_2 = QtWidgets.QVBoxLayout(Fido2ManagementDialog)
+        self.verticalLayout_2.setObjectName("verticalLayout_2")
+        self.horizontalLayout = QtWidgets.QHBoxLayout()
+        self.horizontalLayout.setObjectName("horizontalLayout")
+        self.reloadButton = QtWidgets.QToolButton(parent=Fido2ManagementDialog)
+        self.reloadButton.setObjectName("reloadButton")
+        self.horizontalLayout.addWidget(self.reloadButton)
+        self.securityKeysComboBox = QtWidgets.QComboBox(parent=Fido2ManagementDialog)
+        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
+        sizePolicy.setHorizontalStretch(0)
+        sizePolicy.setVerticalStretch(0)
+        sizePolicy.setHeightForWidth(self.securityKeysComboBox.sizePolicy().hasHeightForWidth())
+        self.securityKeysComboBox.setSizePolicy(sizePolicy)
+        self.securityKeysComboBox.setObjectName("securityKeysComboBox")
+        self.horizontalLayout.addWidget(self.securityKeysComboBox)
+        self.lockButton = QtWidgets.QToolButton(parent=Fido2ManagementDialog)
+        self.lockButton.setCheckable(True)
+        self.lockButton.setObjectName("lockButton")
+        self.horizontalLayout.addWidget(self.lockButton)
+        self.pinButton = QtWidgets.QPushButton(parent=Fido2ManagementDialog)
+        self.pinButton.setObjectName("pinButton")
+        self.horizontalLayout.addWidget(self.pinButton)
+        self.showInfoButton = QtWidgets.QPushButton(parent=Fido2ManagementDialog)
+        self.showInfoButton.setObjectName("showInfoButton")
+        self.horizontalLayout.addWidget(self.showInfoButton)
+        self.line_2 = QtWidgets.QFrame(parent=Fido2ManagementDialog)
+        self.line_2.setFrameShape(QtWidgets.QFrame.Shape.VLine)
+        self.line_2.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
+        self.line_2.setObjectName("line_2")
+        self.horizontalLayout.addWidget(self.line_2)
+        self.resetButton = QtWidgets.QPushButton(parent=Fido2ManagementDialog)
+        self.resetButton.setObjectName("resetButton")
+        self.horizontalLayout.addWidget(self.resetButton)
+        self.verticalLayout_2.addLayout(self.horizontalLayout)
+        self.groupBox = QtWidgets.QGroupBox(parent=Fido2ManagementDialog)
+        self.groupBox.setObjectName("groupBox")
+        self.gridLayout = QtWidgets.QGridLayout(self.groupBox)
+        self.gridLayout.setObjectName("gridLayout")
+        self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
+        self.horizontalLayout_3.setObjectName("horizontalLayout_3")
+        spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
+        self.horizontalLayout_3.addItem(spacerItem)
+        self.loadPasskeysButton = QtWidgets.QPushButton(parent=self.groupBox)
+        self.loadPasskeysButton.setObjectName("loadPasskeysButton")
+        self.horizontalLayout_3.addWidget(self.loadPasskeysButton)
+        spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
+        self.horizontalLayout_3.addItem(spacerItem1)
+        self.gridLayout.addLayout(self.horizontalLayout_3, 0, 0, 1, 1)
+        self.passkeysList = QtWidgets.QTreeWidget(parent=self.groupBox)
+        self.passkeysList.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
+        self.passkeysList.setAlternatingRowColors(True)
+        self.passkeysList.setObjectName("passkeysList")
+        self.passkeysList.header().setDefaultSectionSize(150)
+        self.gridLayout.addWidget(self.passkeysList, 1, 0, 1, 1)
+        self.line = QtWidgets.QFrame(parent=self.groupBox)
+        self.line.setFrameShape(QtWidgets.QFrame.Shape.VLine)
+        self.line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
+        self.line.setObjectName("line")
+        self.gridLayout.addWidget(self.line, 1, 1, 2, 1)
+        self.verticalLayout = QtWidgets.QVBoxLayout()
+        self.verticalLayout.setObjectName("verticalLayout")
+        self.editButton = QtWidgets.QPushButton(parent=self.groupBox)
+        self.editButton.setObjectName("editButton")
+        self.verticalLayout.addWidget(self.editButton)
+        self.deleteButton = QtWidgets.QPushButton(parent=self.groupBox)
+        self.deleteButton.setObjectName("deleteButton")
+        self.verticalLayout.addWidget(self.deleteButton)
+        spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding)
+        self.verticalLayout.addItem(spacerItem2)
+        self.gridLayout.addLayout(self.verticalLayout, 1, 2, 2, 1)
+        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
+        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
+        spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
+        self.horizontalLayout_2.addItem(spacerItem3)
+        self.label = QtWidgets.QLabel(parent=self.groupBox)
+        self.label.setObjectName("label")
+        self.horizontalLayout_2.addWidget(self.label)
+        self.existingCountLabel = QtWidgets.QLabel(parent=self.groupBox)
+        self.existingCountLabel.setObjectName("existingCountLabel")
+        self.horizontalLayout_2.addWidget(self.existingCountLabel)
+        spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
+        self.horizontalLayout_2.addItem(spacerItem4)
+        self.label_2 = QtWidgets.QLabel(parent=self.groupBox)
+        self.label_2.setObjectName("label_2")
+        self.horizontalLayout_2.addWidget(self.label_2)
+        self.remainingCountLabel = QtWidgets.QLabel(parent=self.groupBox)
+        self.remainingCountLabel.setObjectName("remainingCountLabel")
+        self.horizontalLayout_2.addWidget(self.remainingCountLabel)
+        spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
+        self.horizontalLayout_2.addItem(spacerItem5)
+        self.gridLayout.addLayout(self.horizontalLayout_2, 2, 0, 1, 1)
+        self.verticalLayout_2.addWidget(self.groupBox)
+        self.buttonBox = QtWidgets.QDialogButtonBox(parent=Fido2ManagementDialog)
+        self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal)
+        self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Close)
+        self.buttonBox.setObjectName("buttonBox")
+        self.verticalLayout_2.addWidget(self.buttonBox)
+
+        self.retranslateUi(Fido2ManagementDialog)
+        self.buttonBox.accepted.connect(Fido2ManagementDialog.accept) # type: ignore
+        self.buttonBox.rejected.connect(Fido2ManagementDialog.reject) # type: ignore
+        QtCore.QMetaObject.connectSlotsByName(Fido2ManagementDialog)
+        Fido2ManagementDialog.setTabOrder(self.securityKeysComboBox, self.lockButton)
+        Fido2ManagementDialog.setTabOrder(self.lockButton, self.pinButton)
+        Fido2ManagementDialog.setTabOrder(self.pinButton, self.showInfoButton)
+        Fido2ManagementDialog.setTabOrder(self.showInfoButton, self.loadPasskeysButton)
+        Fido2ManagementDialog.setTabOrder(self.loadPasskeysButton, self.passkeysList)
+        Fido2ManagementDialog.setTabOrder(self.passkeysList, self.editButton)
+        Fido2ManagementDialog.setTabOrder(self.editButton, self.deleteButton)
+        Fido2ManagementDialog.setTabOrder(self.deleteButton, self.reloadButton)
+
+    def retranslateUi(self, Fido2ManagementDialog):
+        _translate = QtCore.QCoreApplication.translate
+        Fido2ManagementDialog.setWindowTitle(_translate("Fido2ManagementDialog", "FIDO2 Security Key Management"))
+        self.reloadButton.setToolTip(_translate("Fido2ManagementDialog", "Press to reload the list of detected security keys."))
+        self.securityKeysComboBox.setToolTip(_translate("Fido2ManagementDialog", "Select the security keys to be managed."))
+        self.lockButton.setToolTip(_translate("Fido2ManagementDialog", "Press to unlock the security key, release to lock it again."))
+        self.pinButton.setToolTip(_translate("Fido2ManagementDialog", "Press to set or change the PIN of the selected security key."))
+        self.pinButton.setText(_translate("Fido2ManagementDialog", "Set PIN"))
+        self.showInfoButton.setToolTip(_translate("Fido2ManagementDialog", "Press to show a dialog with technical data of the selected security key."))
+        self.showInfoButton.setText(_translate("Fido2ManagementDialog", "Show Info"))
+        self.resetButton.setToolTip(_translate("Fido2ManagementDialog", "Press to reset the selected security key."))
+        self.resetButton.setText(_translate("Fido2ManagementDialog", "Reset Key"))
+        self.groupBox.setTitle(_translate("Fido2ManagementDialog", "Passkeys"))
+        self.loadPasskeysButton.setToolTip(_translate("Fido2ManagementDialog", "Press ro load the passkeys of the selected security key."))
+        self.loadPasskeysButton.setText(_translate("Fido2ManagementDialog", "Load Passkeys"))
+        self.passkeysList.setSortingEnabled(True)
+        self.passkeysList.headerItem().setText(0, _translate("Fido2ManagementDialog", "Domain"))
+        self.passkeysList.headerItem().setText(1, _translate("Fido2ManagementDialog", "Credential ID"))
+        self.passkeysList.headerItem().setText(2, _translate("Fido2ManagementDialog", "Display Name"))
+        self.passkeysList.headerItem().setText(3, _translate("Fido2ManagementDialog", "User Name"))
+        self.editButton.setToolTip(_translate("Fido2ManagementDialog", "Press to change the user info of the selected passkey."))
+        self.editButton.setText(_translate("Fido2ManagementDialog", "Edit..."))
+        self.deleteButton.setToolTip(_translate("Fido2ManagementDialog", "Press to delete the selected passkey."))
+        self.deleteButton.setText(_translate("Fido2ManagementDialog", "Delete"))
+        self.label.setText(_translate("Fido2ManagementDialog", "Existing Passkeys:"))
+        self.label_2.setText(_translate("Fido2ManagementDialog", "Max. Remaining Passkeys:"))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/WebBrowser/WebAuth/Ui_Fido2PasskeyEditDialog.py	Fri Jul 19 18:06:48 2024 +0200
@@ -0,0 +1,54 @@
+# Form implementation generated from reading ui file 'src/eric7/WebBrowser/WebAuth/Fido2PasskeyEditDialog.ui'
+#
+# Created by: PyQt6 UI code generator 6.7.0
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic6 is
+# run again.  Do not edit this file unless you know what you are doing.
+
+
+from PyQt6 import QtCore, QtGui, QtWidgets
+
+
+class Ui_Fido2PasskeyEditDialog(object):
+    def setupUi(self, Fido2PasskeyEditDialog):
+        Fido2PasskeyEditDialog.setObjectName("Fido2PasskeyEditDialog")
+        Fido2PasskeyEditDialog.resize(400, 174)
+        Fido2PasskeyEditDialog.setSizeGripEnabled(True)
+        self.verticalLayout = QtWidgets.QVBoxLayout(Fido2PasskeyEditDialog)
+        self.verticalLayout.setObjectName("verticalLayout")
+        self.headerLabel = QtWidgets.QLabel(parent=Fido2PasskeyEditDialog)
+        self.headerLabel.setText("")
+        self.headerLabel.setObjectName("headerLabel")
+        self.verticalLayout.addWidget(self.headerLabel)
+        self.label = QtWidgets.QLabel(parent=Fido2PasskeyEditDialog)
+        self.label.setObjectName("label")
+        self.verticalLayout.addWidget(self.label)
+        self.displayNameEdit = QtWidgets.QLineEdit(parent=Fido2PasskeyEditDialog)
+        self.displayNameEdit.setClearButtonEnabled(True)
+        self.displayNameEdit.setObjectName("displayNameEdit")
+        self.verticalLayout.addWidget(self.displayNameEdit)
+        self.label_2 = QtWidgets.QLabel(parent=Fido2PasskeyEditDialog)
+        self.label_2.setObjectName("label_2")
+        self.verticalLayout.addWidget(self.label_2)
+        self.userNameEdit = QtWidgets.QLineEdit(parent=Fido2PasskeyEditDialog)
+        self.userNameEdit.setClearButtonEnabled(True)
+        self.userNameEdit.setObjectName("userNameEdit")
+        self.verticalLayout.addWidget(self.userNameEdit)
+        self.buttonBox = QtWidgets.QDialogButtonBox(parent=Fido2PasskeyEditDialog)
+        self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal)
+        self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Ok)
+        self.buttonBox.setObjectName("buttonBox")
+        self.verticalLayout.addWidget(self.buttonBox)
+
+        self.retranslateUi(Fido2PasskeyEditDialog)
+        self.buttonBox.accepted.connect(Fido2PasskeyEditDialog.accept) # type: ignore
+        self.buttonBox.rejected.connect(Fido2PasskeyEditDialog.reject) # type: ignore
+        QtCore.QMetaObject.connectSlotsByName(Fido2PasskeyEditDialog)
+
+    def retranslateUi(self, Fido2PasskeyEditDialog):
+        _translate = QtCore.QCoreApplication.translate
+        Fido2PasskeyEditDialog.setWindowTitle(_translate("Fido2PasskeyEditDialog", "Edit Passkey Data"))
+        self.label.setText(_translate("Fido2PasskeyEditDialog", "Display Name:"))
+        self.displayNameEdit.setToolTip(_translate("Fido2PasskeyEditDialog", "Enter the display name."))
+        self.label_2.setText(_translate("Fido2PasskeyEditDialog", "User Name:"))
+        self.userNameEdit.setToolTip(_translate("Fido2PasskeyEditDialog", "Enter the user name."))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/WebBrowser/WebAuth/Ui_Fido2PinDialog.py	Fri Jul 19 18:06:48 2024 +0200
@@ -0,0 +1,93 @@
+# Form implementation generated from reading ui file 'src/eric7/WebBrowser/WebAuth/Fido2PinDialog.ui'
+#
+# Created by: PyQt6 UI code generator 6.7.0
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic6 is
+# run again.  Do not edit this file unless you know what you are doing.
+
+
+from PyQt6 import QtCore, QtGui, QtWidgets
+
+
+class Ui_Fido2PinDialog(object):
+    def setupUi(self, Fido2PinDialog):
+        Fido2PinDialog.setObjectName("Fido2PinDialog")
+        Fido2PinDialog.resize(614, 251)
+        Fido2PinDialog.setSizeGripEnabled(True)
+        self.verticalLayout = QtWidgets.QVBoxLayout(Fido2PinDialog)
+        self.verticalLayout.setObjectName("verticalLayout")
+        self.headerLabel = QtWidgets.QLabel(parent=Fido2PinDialog)
+        self.headerLabel.setText("Header")
+        self.headerLabel.setObjectName("headerLabel")
+        self.verticalLayout.addWidget(self.headerLabel)
+        self.descriptionLabel = QtWidgets.QLabel(parent=Fido2PinDialog)
+        self.descriptionLabel.setText("Description")
+        self.descriptionLabel.setWordWrap(True)
+        self.descriptionLabel.setObjectName("descriptionLabel")
+        self.verticalLayout.addWidget(self.descriptionLabel)
+        self.horizontalLayout = QtWidgets.QHBoxLayout()
+        self.horizontalLayout.setObjectName("horizontalLayout")
+        self.pinLabel = QtWidgets.QLabel(parent=Fido2PinDialog)
+        self.pinLabel.setObjectName("pinLabel")
+        self.horizontalLayout.addWidget(self.pinLabel)
+        self.pinEdit = QtWidgets.QLineEdit(parent=Fido2PinDialog)
+        self.pinEdit.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
+        self.pinEdit.setObjectName("pinEdit")
+        self.horizontalLayout.addWidget(self.pinEdit)
+        self.pinButton = QtWidgets.QToolButton(parent=Fido2PinDialog)
+        self.pinButton.setCheckable(True)
+        self.pinButton.setObjectName("pinButton")
+        self.horizontalLayout.addWidget(self.pinButton)
+        self.verticalLayout.addLayout(self.horizontalLayout)
+        self.newPinGroupBox = QtWidgets.QGroupBox(parent=Fido2PinDialog)
+        self.newPinGroupBox.setFlat(True)
+        self.newPinGroupBox.setObjectName("newPinGroupBox")
+        self.gridLayout = QtWidgets.QGridLayout(self.newPinGroupBox)
+        self.gridLayout.setObjectName("gridLayout")
+        self.newPinLabel = QtWidgets.QLabel(parent=self.newPinGroupBox)
+        self.newPinLabel.setObjectName("newPinLabel")
+        self.gridLayout.addWidget(self.newPinLabel, 0, 0, 1, 1)
+        self.confirmNewPinLabel = QtWidgets.QLabel(parent=self.newPinGroupBox)
+        self.confirmNewPinLabel.setObjectName("confirmNewPinLabel")
+        self.gridLayout.addWidget(self.confirmNewPinLabel, 1, 0, 1, 1)
+        self.confirmNewPinEdit = QtWidgets.QLineEdit(parent=self.newPinGroupBox)
+        self.confirmNewPinEdit.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
+        self.confirmNewPinEdit.setObjectName("confirmNewPinEdit")
+        self.gridLayout.addWidget(self.confirmNewPinEdit, 1, 1, 1, 1)
+        self.newPinEdit = QtWidgets.QLineEdit(parent=self.newPinGroupBox)
+        self.newPinEdit.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
+        self.newPinEdit.setObjectName("newPinEdit")
+        self.gridLayout.addWidget(self.newPinEdit, 0, 1, 1, 1)
+        self.newPinButton = QtWidgets.QToolButton(parent=self.newPinGroupBox)
+        self.newPinButton.setCheckable(True)
+        self.newPinButton.setObjectName("newPinButton")
+        self.gridLayout.addWidget(self.newPinButton, 0, 2, 1, 1)
+        self.verticalLayout.addWidget(self.newPinGroupBox)
+        self.pinErrorLabel = QtWidgets.QLabel(parent=Fido2PinDialog)
+        self.pinErrorLabel.setText("PIN Error")
+        self.pinErrorLabel.setWordWrap(True)
+        self.pinErrorLabel.setObjectName("pinErrorLabel")
+        self.verticalLayout.addWidget(self.pinErrorLabel)
+        self.buttonBox = QtWidgets.QDialogButtonBox(parent=Fido2PinDialog)
+        self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal)
+        self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Ok)
+        self.buttonBox.setObjectName("buttonBox")
+        self.verticalLayout.addWidget(self.buttonBox)
+
+        self.retranslateUi(Fido2PinDialog)
+        self.buttonBox.accepted.connect(Fido2PinDialog.accept) # type: ignore
+        self.buttonBox.rejected.connect(Fido2PinDialog.reject) # type: ignore
+        QtCore.QMetaObject.connectSlotsByName(Fido2PinDialog)
+
+    def retranslateUi(self, Fido2PinDialog):
+        _translate = QtCore.QCoreApplication.translate
+        Fido2PinDialog.setWindowTitle(_translate("Fido2PinDialog", "PIN Entry"))
+        self.pinLabel.setText(_translate("Fido2PinDialog", "PIN:"))
+        self.pinEdit.setToolTip(_translate("Fido2PinDialog", "Enter the PIN"))
+        self.pinButton.setToolTip(_translate("Fido2PinDialog", "Press to show or hide the PIN."))
+        self.newPinGroupBox.setTitle(_translate("Fido2PinDialog", "New PIN"))
+        self.newPinLabel.setText(_translate("Fido2PinDialog", "PIN:"))
+        self.confirmNewPinLabel.setText(_translate("Fido2PinDialog", "Confirm PIN:"))
+        self.confirmNewPinEdit.setToolTip(_translate("Fido2PinDialog", "Enter the same PIN again."))
+        self.newPinEdit.setToolTip(_translate("Fido2PinDialog", "Enter the new PIN"))
+        self.newPinButton.setToolTip(_translate("Fido2PinDialog", "Press to show or hide the new PIN."))
--- a/src/eric7/WebBrowser/WebAuth/WebBrowserWebAuthDialog.py	Fri Jul 19 11:54:29 2024 +0200
+++ b/src/eric7/WebBrowser/WebAuth/WebBrowserWebAuthDialog.py	Fri Jul 19 18:06:48 2024 +0200
@@ -2,7 +2,6 @@
 # Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
 #
 
-
 """
 Module implementing a dialog to handle the various WebAuth requests.
 """
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/eric7_fido2.py	Fri Jul 19 18:06:48 2024 +0200
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+eric FIDO2 Token Management.
+
+This is the main Python script that performs the necessary initialization
+of the FIDO2 Security Key Management module and starts the Qt event loop.
+This is a standalone version of the integrated FIDO2 Security Key Management
+module.
+"""
+
+import argparse
+import importlib
+import os
+import sys
+
+from PyQt6.QtGui import QGuiApplication
+
+
+def createArgparseNamespace():
+    """
+    Function to create an argument parser.
+
+    @return created argument parser object
+    @rtype argparse.ArgumentParser
+    """
+    from eric7.__version__ import Version
+
+    # 1. create the argument parser
+    parser = argparse.ArgumentParser(
+        description="Management tool for FIDO2 Security Keys.",
+        epilog="Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>.",
+    )
+
+    # 2. add the arguments
+    parser.add_argument(
+        "-V",
+        "--version",
+        action="version",
+        version="%(prog)s {0}".format(Version),
+        help="show version information and exit",
+    )
+    parser.add_argument(
+        "--config",
+        metavar="config_dir",
+        help="use the given directory as the one containing the config files",
+    )
+    parser.add_argument(
+        "--settings",
+        metavar="settings_dir",
+        help="use the given directory to store the settings files",
+    )
+
+    # 3. create the Namespace object by parsing the command line
+    args = parser.parse_args()
+    return args
+
+
+args = createArgparseNamespace()
+if args.config:
+    from eric7 import Globals
+
+    Globals.setConfigDir(args.config)
+if args.settings:
+    from PyQt6.QtCore import QSettings
+
+    SettingsDir = os.path.expanduser(args.settings)
+    if not os.path.isdir(SettingsDir):
+        os.makedirs(SettingsDir)
+    QSettings.setPath(
+        QSettings.Format.IniFormat, QSettings.Scope.UserScope, SettingsDir
+    )
+
+if importlib.util.find_spec("fido2") is None:
+    from PyQt6.QtCore import QTimer
+    from PyQt6.QtWidgets import QApplication
+
+    from eric7.EricWidgets import EricMessageBox
+
+    app = QApplication([])
+    QTimer.singleShot(
+        0,
+        lambda: EricMessageBox.critical(
+            None,
+            "FIDO2 Security Key Management",
+            "The required 'fido2' package is not installed. Aborting...",
+        ),
+    )
+    app.exec()
+    sys.exit(100)
+
+from eric7.Toolbox import Startup
+
+
+def createMainWidget(_args):
+    """
+    Function to create the main widget.
+
+    @param _args namespace object containing the parsed command line parameters
+        (unused)
+    @type argparse.Namespace
+    @return reference to the main widget
+    @rtype QWidget
+    """
+    from eric7.WebBrowser.WebAuth.Fido2ManagementDialog import Fido2ManagementDialog
+    
+    return Fido2ManagementDialog()
+
+
+def main():
+    """
+    Main entry point into the application.
+    """
+    QGuiApplication.setDesktopFileName("eric7_fido2")
+
+    res = Startup.appStartup(args, createMainWidget)
+    sys.exit(res)
+
+
+if __name__ == "__main__":
+    main()
--- a/src/eric7/icons/breeze-dark/locked.svg	Fri Jul 19 11:54:29 2024 +0200
+++ b/src/eric7/icons/breeze-dark/locked.svg	Fri Jul 19 18:06:48 2024 +0200
@@ -1,13 +1,46 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
-  <defs id="defs3051">
-    <style type="text/css" id="current-color-scheme">
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 22 22"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="locked.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="11.599805"
+     inkscape:cy="11.289192"
+     inkscape:window-width="2580"
+     inkscape:window-height="1289"
+     inkscape:window-x="861"
+     inkscape:window-y="92"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg6" />
+  <defs
+     id="defs3051">
+    <style
+       type="text/css"
+       id="current-color-scheme">
       .ColorScheme-Text {
         color:#eff0f1
       }
       </style>
   </defs>
-    <path
-       style="fill:currentColor"
-       d="M 11 3 C 8.784 3 7 4.784 7 7 L 7 11 L 5 11 L 5 19 L 17 19 L 17 11 L 15 11 L 15 7 C 15 4.784 13.216 3 11 3 z M 11 4 C 12.662 4 14 5.561 14 7.5 L 14 11 L 8 11 L 8 7.5 C 8 5.561 9.338 4 11 4 z "
-       class="ColorScheme-Text" />
+  <path
+     style="fill:currentColor;stroke-width:1.29099"
+     d="M 11,1 C 8.0453333,1 5.6666667,3.23 5.6666667,6 v 5 H 3 V 21 H 19 V 11 H 16.333333 V 6 C 16.333333,3.23 13.954667,1 11,1 Z m 0,1.25 c 2.216,0 4,1.95125 4,4.375 V 11 H 7 V 6.625 C 7,4.20125 8.784,2.25 11,2.25 Z"
+     class="ColorScheme-Text"
+     id="path4" />
 </svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-dark/unlocked.svg	Fri Jul 19 18:06:48 2024 +0200
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 22 22"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="unlocked.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="11.278481"
+     inkscape:cy="11.235638"
+     inkscape:window-width="2620"
+     inkscape:window-height="1308"
+     inkscape:window-x="821"
+     inkscape:window-y="73"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg6" />
+  <defs
+     id="defs3051">
+    <style
+       type="text/css"
+       id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#eff0f1;
+      }
+      </style>
+  </defs>
+  <path
+     style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099"
+     d="M 11,1 C 8.045333,1 5.6666667,3.23 5.6666667,6 V 7.25 H 7 V 6.625 C 7,4.20125 8.784,2.25 11,2.25 c 2.216,0 4,1.95125 4,4.375 V 11 H 8.333333 7 5.6666667 4.3333333 3 V 12.25 21 H 4.3333333 17.666667 19 V 11 H 17.666667 16.333333 V 6 C 16.333333,3.23 13.954667,1 11,1 M 4.3333333,12.25 H 17.666667 v 7.5 H 4.3333333 v -7.5"
+     class="ColorScheme-Text"
+     id="path4" />
+</svg>
--- a/src/eric7/icons/breeze-light/locked.svg	Fri Jul 19 11:54:29 2024 +0200
+++ b/src/eric7/icons/breeze-light/locked.svg	Fri Jul 19 18:06:48 2024 +0200
@@ -1,13 +1,46 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
-  <defs id="defs3051">
-    <style type="text/css" id="current-color-scheme">
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 22 22"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="locked.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="12.092502"
+     inkscape:cy="11.514119"
+     inkscape:window-width="2580"
+     inkscape:window-height="1340"
+     inkscape:window-x="861"
+     inkscape:window-y="41"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg6" />
+  <defs
+     id="defs3051">
+    <style
+       type="text/css"
+       id="current-color-scheme">
       .ColorScheme-Text {
         color:#232629
       }
       </style>
   </defs>
-    <path
-       style="fill:currentColor"
-       d="M 11 3 C 8.784 3 7 4.784 7 7 L 7 11 L 5 11 L 5 19 L 17 19 L 17 11 L 15 11 L 15 7 C 15 4.784 13.216 3 11 3 z M 11 4 C 12.662 4 14 5.561 14 7.5 L 14 11 L 8 11 L 8 7.5 C 8 5.561 9.338 4 11 4 z "
-       class="ColorScheme-Text" />
+  <path
+     style="fill:currentColor;stroke-width:1.29099"
+     d="M 11,1 C 8.0453333,1 5.6666667,3.23 5.6666667,6 v 5 H 3 V 21 H 19 V 11 H 16.333333 V 6 C 16.333333,3.23 13.954667,1 11,1 Z m 0,1.25 c 2.216,0 4,1.95125 4,4.375 V 11 H 7 V 6.625 C 7,4.20125 8.784,2.25 11,2.25 Z"
+     class="ColorScheme-Text"
+     id="path4" />
 </svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-light/unlocked.svg	Fri Jul 19 18:06:48 2024 +0200
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 22 22"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="unlocked.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="11.492697"
+     inkscape:cy="11.26777"
+     inkscape:window-width="2580"
+     inkscape:window-height="1321"
+     inkscape:window-x="861"
+     inkscape:window-y="60"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg6" />
+  <defs
+     id="defs3051">
+    <style
+       type="text/css"
+       id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#232629;
+      }
+      </style>
+  </defs>
+  <path
+     style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099"
+     d="M 11,1 C 8.0453333,1 5.6666667,3.23 5.6666667,6 V 7.25 H 7 V 6.625 C 7,4.20125 8.784,2.25 11,2.25 c 2.216,0 4,1.95125 4,4.375 V 11 H 8.3333333 7 5.6666667 4.3333333 3 V 12.25 21 H 4.3333333 17.666667 19 V 11 H 17.666667 16.333333 V 6 C 16.333333,3.23 13.954667,1 11,1 M 4.3333333,12.25 H 17.666667 v 7.5 H 4.3333333 v -7.5"
+     class="ColorScheme-Text"
+     id="path4" />
+</svg>

eric ide

mercurial