Continued implementing the FIDO2 security key management interface. eric7

Mon, 22 Jul 2024 10:15:41 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 22 Jul 2024 10:15:41 +0200
branch
eric7
changeset 10856
b19cefceca15
parent 10855
9082eb8f6571
child 10857
abcb288e7e17

Continued implementing the FIDO2 security key management interface.

eric7.epj file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Fido2InfoDialog.py file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Fido2InfoDialog.ui 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/Fido2PinDialog.py file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Fido2PinDialog.ui file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Ui_Fido2InfoDialog.py file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Ui_Fido2ManagementDialog.py file | annotate | diff | comparison | revisions
src/eric7/WebBrowser/WebAuth/Ui_Fido2PinDialog.py file | annotate | diff | comparison | revisions
src/eric7/eric7_fido2.py file | annotate | diff | comparison | revisions
--- a/eric7.epj	Sat Jul 20 11:14:51 2024 +0200
+++ b/eric7.epj	Mon Jul 22 10:15:41 2024 +0200
@@ -783,6 +783,7 @@
       "src/eric7/WebBrowser/VirusTotal/VirusTotalDomainReportDialog.ui",
       "src/eric7/WebBrowser/VirusTotal/VirusTotalIpReportDialog.ui",
       "src/eric7/WebBrowser/VirusTotal/VirusTotalWhoisDialog.ui",
+      "src/eric7/WebBrowser/WebAuth/Fido2InfoDialog.ui",
       "src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.ui",
       "src/eric7/WebBrowser/WebAuth/Fido2PasskeyEditDialog.ui",
       "src/eric7/WebBrowser/WebAuth/Fido2PinDialog.ui",
@@ -2452,6 +2453,7 @@
       "src/eric7/WebBrowser/VirusTotal/VirusTotalIpReportDialog.py",
       "src/eric7/WebBrowser/VirusTotal/VirusTotalWhoisDialog.py",
       "src/eric7/WebBrowser/VirusTotal/__init__.py",
+      "src/eric7/WebBrowser/WebAuth/Fido2InfoDialog.py",
       "src/eric7/WebBrowser/WebAuth/Fido2Management.py",
       "src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.py",
       "src/eric7/WebBrowser/WebAuth/Fido2PasskeyEditDialog.py",
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/WebBrowser/WebAuth/Fido2InfoDialog.py	Mon Jul 22 10:15:41 2024 +0200
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog showing information about the selected security key.
+"""
+
+from PyQt6.QtCore import Qt
+from PyQt6.QtWidgets import QDialog, QTreeWidgetItem
+
+from .Ui_Fido2InfoDialog import Ui_Fido2InfoDialog
+
+
+class Fido2InfoDialog(QDialog, Ui_Fido2InfoDialog):
+    """
+    Class implementing a dialog showing information about the selected security key.
+    """
+
+    def __init__(self, header, manager, parent=None):
+        """
+        Constructor
+
+        @param header header string
+        @type str
+        @param manager reference to the FIDO2 manager object
+        @type Fido2Management
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.headerLabel.setText(f"<b>{header}</b>")
+
+        data = manager.getSecurityKeyInfo()
+        if not data:
+            itm = QTreeWidgetItem(
+                self.infoWidget, [self.tr("No information available.")]
+            )
+            itm.setFirstColumnSpanned(True)
+            return
+
+        for key in data:
+            if data[key]:
+                topItem = QTreeWidgetItem(
+                    self.infoWidget, [manager.FidoInfoCategories2Str.get(key, key)]
+                )
+                topItem.setFirstColumnSpanned(True)
+                topItem.setExpanded(True)
+                for entry in data[key]:
+                    QTreeWidgetItem(topItem, list(entry))
+
+        self.infoWidget.sortItems(1, Qt.SortOrder.AscendingOrder)
+        self.infoWidget.sortItems(0, Qt.SortOrder.AscendingOrder)
+        self.infoWidget.resizeColumnToContents(0)
+        self.infoWidget.resizeColumnToContents(1)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/WebBrowser/WebAuth/Fido2InfoDialog.ui	Mon Jul 22 10:15:41 2024 +0200
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Fido2InfoDialog</class>
+ <widget class="QDialog" name="Fido2InfoDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>600</width>
+    <height>700</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Security Key Information</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>
+     <property name="alignment">
+      <set>Qt::AlignCenter</set>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QTreeWidget" name="infoWidget">
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+     <property name="columnCount">
+      <number>2</number>
+     </property>
+     <attribute name="headerVisible">
+      <bool>false</bool>
+     </attribute>
+     <column>
+      <property name="text">
+       <string notr="true">1</string>
+      </property>
+     </column>
+     <column>
+      <property name="text">
+       <string notr="true">2</string>
+      </property>
+     </column>
+    </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>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>Fido2InfoDialog</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>Fido2InfoDialog</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>
--- a/src/eric7/WebBrowser/WebAuth/Fido2Management.py	Sat Jul 20 11:14:51 2024 +0200
+++ b/src/eric7/WebBrowser/WebAuth/Fido2Management.py	Mon Jul 22 10:15:41 2024 +0200
@@ -6,11 +6,13 @@
 Module implementing a manager for FIDO2 security keys.
 """
 
+import time
+
 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
+from PyQt6.QtCore import QCoreApplication, QObject, QThread, pyqtSignal
 
 
 class Fido2PinError(Exception):
@@ -40,6 +42,34 @@
     deviceConnected = pyqtSignal()
     deviceDisconnected = pyqtSignal()
 
+    FidoVersion2Str = {
+        "FIDO_2_1": "CTAP 2.1 / FIDO2",
+        "FIDO_2_0": "CTAP 2.0 / FIDO2",
+        "FIDO_2_1_PRE": QCoreApplication.translate(
+            "Fido2Management", "CTAP2.1 Preview Features"
+        ),
+        "U2F_V2": "CTAP 1 / U2F",
+    }
+
+    FidoExtension2Str = {
+        "credBlob": QCoreApplication.translate("Fido2Management", "Credential BLOB"),
+        "credProtect": QCoreApplication.translate(
+            "Fido2Management", "Credential Protection"
+        ),
+        "hmac-secret": QCoreApplication.translate("Fido2Management", "HMAC Secret"),
+        "largeBlobKey": QCoreApplication.translate("Fido2Management", "Large Blob Key"),
+        "minPinLength": QCoreApplication.translate(
+            "Fido2Management", "Minimum PIN Length"
+        ),
+    }
+
+    FidoInfoCategories2Str = {
+        "pin": QCoreApplication.translate("Fido2Management", "PIN"),
+        "security_key": QCoreApplication.translate("Fido2Management", "Security Key"),
+        "options": QCoreApplication.translate("Fido2Management", "Options"),
+        "extensions": QCoreApplication.translate("Fido2Management", "Extensions"),
+    }
+
     def __init__(self, parent=None):
         """
         Constructor
@@ -77,6 +107,13 @@
 
         self.deviceDisconnected.emit()
 
+    def reconnectToDevice(self):
+        """
+        Public method to reconnect the current security key.
+        """
+        if self.__ctap2 is not None:
+            self.connectToDevice(self.__ctap2.device)
+
     def unlockDevice(self, pin):
         """
         Public method to unlock the device (i.e. store the PIN for later use).
@@ -111,22 +148,382 @@
         """
         return list(CtapHidDevice.list_devices())
 
-    def getKeyInfo(self):
+    def getSecurityKeyInfo(self):
         """
         Public method to get information about the connected security key.
 
         @return dictionary containing the info data
-        @rtype dict[str, Any]
+        @rtype dict[str, list[tuple[str, str]]]
         """
-        # TODO: not implemented yet
-        return {}
+        if self.__ctap2 is None:
+            return {}
+
+        # each entry is a list of tuples containing the display name and the value
+        data = {
+            "pin": [],
+            "security_key": [],
+            "options": [],
+            "extensions": [],
+        }
+
+        # PIN related data
+        if self.__ctap2.info.options["clientPin"]:
+            if self.__ctap2.info.force_pin_change:
+                msg = self.tr(
+                    "PIN is disabled and must be changed before it can be used!"
+                )
+            pinRetries, powerCycle = self.getPinRetries()
+            if pinRetries:
+                if powerCycle:
+                    msg = self.tr(
+                        "PIN is temporarily blocked. Remove and re-insert the"
+                        " security keyto unblock it."
+                    )
+                else:
+                    msg = self.tr("%n attempts remaining", "", pinRetries)
+            else:
+                msg = self.tr("PIN is blocked. The security key needs to be reset.")
+        else:
+            msg = self.tr("A PIN has not been set.")
+        data["pin"].append((self.tr("PIN"), msg))
+
+        alwaysUv = self.__ctap2.info.options.get("alwaysUv")
+        msg = (
+            self.tr("not supported")
+            if alwaysUv is None
+            else self.tr("switched on") if alwaysUv else self.tr("switched off")
+        )
+        data["pin"].append((self.tr("Always require User Verification"), msg))
+
+        remainingPasskeys = self.__ctap2.info.remaining_disc_creds
+        if remainingPasskeys is not None:
+            data["pin"].append(
+                (self.tr("Passkeys storage remaining"), str(remainingPasskeys))
+            )
+
+        enterprise = self.__ctap2.info.options.get("ep")
+        if enterprise is not None:
+            data["pin"].append(
+                (
+                    self.tr("Enterprise Attestation"),
+                    self.tr("enabled") if enterprise else self.tr("disabled"),
+                )
+            )
+
+        # security key related data
+        data["security_key"].extend(
+            [
+                (self.tr("Name"), self.__ctap2.device.product_name),
+                (self.tr("Path"), self.__ctap2.device.descriptor.path),
+                (
+                    self.tr("Version"),
+                    ".".join(str(p) for p in self.__ctap2.device.device_version),
+                ),
+                (self.tr("Vendor ID"), f"0x{self.__ctap2.device.descriptor.vid:04x}"),
+                (self.tr("Product ID"), f"0x{self.__ctap2.device.descriptor.pid:04x}"),
+            ]
+        )
+        serial = self.__ctap2.device.serial_number
+        if serial is not None:
+            data["security_key"].append((self.tr("Serial Number"), serial))
+        data["security_key"].append(
+            (
+                self.tr("Supported Versions"),
+                "\n".join(
+                    self.FidoVersion2Str.get(v, v) for v in self.__ctap2.info.versions
+                ),
+            )
+        )
+        data["security_key"].append(
+            (self.tr("Supported Transports"), "\n".join(self.__ctap2.info.transports))
+        )
+
+        # extensions data
+        if self.__ctap2.info.extensions:
+            for ext in self.FidoExtension2Str:
+                data["extensions"].append(
+                    (
+                        self.FidoExtension2Str[ext],
+                        (
+                            self.tr("supported")
+                            if ext in self.__ctap2.info.extensions
+                            else self.tr("not supported")
+                        ),
+                    )
+                )
+
+        # options data
+        options = self.__ctap2.info.options
+        data["options"].append(
+            (
+                self.tr("Is Platform Device"),
+                self.tr("yes") if options.get("plat", False) else self.tr("no"),
+            )
+        )
+        data["options"].append(
+            (
+                self.tr("Resident Passkeys"),
+                (
+                    self.tr("supported")
+                    if options.get("rk", False)
+                    else self.tr("not supported")
+                ),
+            )
+        )
+        cp = options.get("clientPin")
+        data["options"].append(
+            (
+                self.tr("Client PIN"),
+                (
+                    self.tr("not supported")
+                    if cp is None
+                    else (
+                        self.tr("supported, PIN set")
+                        if cp is True
+                        else self.tr("supported, PIN not set")
+                    )
+                ),
+            )
+        )
+        data["options"].append(
+            (
+                self.tr("Detect User Presence"),
+                (
+                    self.tr("supported")
+                    if options.get("up", True)
+                    else self.tr("not supported")
+                ),
+            )
+        )
+        uv = options.get("uv")
+        data["options"].append(
+            (
+                self.tr("User Verification"),
+                (
+                    self.tr("not supported")
+                    if uv is None
+                    else (
+                        self.tr("supported, configured")
+                        if uv is True
+                        else self.tr("supported, not configured")
+                    )
+                ),
+            )
+        )
+        data["options"].append(
+            (
+                self.tr("Verify User with Client PIN"),
+                (
+                    self.tr("available")
+                    if options.get("pinUvAuthToken", False)
+                    else self.tr("not available")
+                ),
+            )
+        )
+        data["options"].append(
+            (
+                self.tr("Make Credential / Get Assertion"),
+                (
+                    self.tr("available")
+                    if options.get("noMcGaPermissionsWithClientPin", False)
+                    else self.tr("not available")
+                ),
+            )
+        )
+        data["options"].append(
+            (
+                self.tr("Large BLOBs"),
+                (
+                    self.tr("supported")
+                    if options.get("largeBlobs", False)
+                    else self.tr("not supported")
+                ),
+            )
+        )
+        ep = options.get("ep")
+        data["options"].append(
+            (
+                self.tr("Enterprise Attestation"),
+                (
+                    self.tr("not supported")
+                    if ep is None
+                    else (
+                        self.tr("supported, enabled")
+                        if ep is True
+                        else self.tr("supported, disabled")
+                    )
+                ),
+            )
+        )
+        be = options.get("bioEnroll")
+        data["options"].append(
+            (
+                self.tr("Fingerprint"),
+                (
+                    self.tr("not supported")
+                    if be is None
+                    else (
+                        self.tr("supported, registered")
+                        if be is True
+                        else self.tr("supported, not registered")
+                    )
+                ),
+            )
+        )
+        uvmp = options.get("userVerificationMgmtPreview")
+        data["options"].append(
+            (
+                self.tr("CTAP2.1 Preview Fingerprint"),
+                (
+                    self.tr("not supported")
+                    if uvmp is None
+                    else (
+                        self.tr("supported, registered")
+                        if uvmp is True
+                        else self.tr("supported, not registered")
+                    )
+                ),
+            )
+        )
+        data["options"].append(
+            (
+                self.tr("Verify User for Fingerprint Registration"),
+                (
+                    self.tr("supported")
+                    if options.get("uvBioEnroll", False)
+                    else self.tr("not supported")
+                ),
+            )
+        )
+        data["options"].append(
+            (
+                self.tr("Security Key Configuration"),
+                (
+                    self.tr("supported")
+                    if options.get("authnrCfg", False)
+                    else self.tr("not supported")
+                ),
+            )
+        )
+        data["options"].append(
+            (
+                self.tr("Verify User for Security Key Configuration"),
+                (
+                    self.tr("supported")
+                    if options.get("uvAcfg", False)
+                    else self.tr("not supported")
+                ),
+            )
+        )
+        data["options"].append(
+            (
+                self.tr("Credential Management"),
+                (
+                    self.tr("supported")
+                    if options.get("credMgmt", False)
+                    else self.tr("not supported")
+                ),
+            )
+        )
+        data["options"].append(
+            (
+                self.tr("CTAP2.1 Preview Credential Management"),
+                (
+                    self.tr("supported")
+                    if options.get("credentialMgmtPreview", False)
+                    else self.tr("not supported")
+                ),
+            )
+        )
+        data["options"].append(
+            (
+                self.tr("Set Minimum PIN Length"),
+                (
+                    self.tr("supported")
+                    if options.get("setMinPINLength", False)
+                    else self.tr("not supported")
+                ),
+            )
+        )
+        data["options"].append(
+            (
+                self.tr("Make Non-Resident Passkey without User Verification"),
+                (
+                    self.tr("allowed")
+                    if options.get("makeCredUvNotRqd", False)
+                    else self.tr("not allowed")
+                ),
+            )
+        )
+        auv = options.get("alwaysUv")
+        data["options"].append(
+            (
+                self.tr("Always Require User Verification"),
+                (
+                    self.tr("not supported")
+                    if auv is None
+                    else (
+                        self.tr("supported, enabled")
+                        if auv is True
+                        else self.tr("supported, disabled")
+                    )
+                ),
+            )
+        )
+
+        return data
 
     def resetDevice(self):
         """
         Public method to reset the connected security key.
+
+        @return flag indicating success and a message
+        @rtype tuple of (bool, str)
         """
-        # TODO: not implemented yet
-        pass
+        if self.__ctap2 is None:
+            return False, self.tr("No security key connected.")
+
+        removed = False
+        startTime = time.monotonic()
+        while True:
+            QThread.msleep(500)
+            try:
+                securityKeys = self.getDevices()
+            except OSError:
+                securityKeys = []
+            if not securityKeys:
+                removed = True
+            if removed and len(securityKeys) == 1:
+                ctap2 = Ctap2(securityKeys[0])
+                break
+            if time.monotonic() - startTime >= 30:
+                return False, self.tr(
+                    "Reset failed. The security key was not removed and re-inserted"
+                    " within 30 seconds."
+                )
+
+        try:
+            ctap2.reset()
+            return True, "The security key has been reset."
+        except CtapError as err:
+            if err.code == CtapError.ERR.ACTION_TIMEOUT:
+                msg = self.tr(
+                    "You need to touch your security key to confirm the reset."
+                )
+            elif err.code in (
+                CtapError.ERR.NOT_ALLOWED,
+                CtapError.ERR.PIN_AUTH_BLOCKED,
+            ):
+                msg = self.tr(
+                    "Reset must be triggered within 5 seconds after the security"
+                    "key is inserted."
+                )
+            else:
+                msg = str(err)
+
+            return False, self.tr("Reset failed. {0}").format(msg)
+        except Exception:
+            return False, self.tr("Reset failed.")
 
     ############################################################################
     ## methods related to PIN handling
@@ -175,25 +572,44 @@
         need of a power cycle.
 
         @return tuple containing the number of retries left and a flag indicating a
-            power cycle is required
+            power cycle is required. A retry value of -1 indicates, that no PIN was
+            set yet.
         @rtype tuple of (int, bool)
         """
         if self.__ctap2 is None or self.__clientPin is None:
             return (None, None)
 
-        return self.__clientPin.get_pin_retries()
+        try:
+            return self.__clientPin.get_pin_retries()
+        except CtapError as err:
+            if err.code == CtapError.ERR.PIN_NOT_SET:
+                # return -1 retries to indicate a missing PIN
+                return (-1, False)
 
-    def changePin(self, pin, newPin):
+    def changePin(self, oldPin, newPin):
         """
         Public method to change the PIN of the connected security key.
 
-        @param pin current PIN
+        @param oldPin current PIN
         @type str
         @param newPin new PIN
         @type str
+        @return flag indicating success and a message
+        @rtype tuple of (bool, str)
         """
-        # TODO: not implemented yet
-        pass
+        if self.__ctap2 is None or self.__clientPin is None:
+            return False, self.tr("No security key connected.")
+
+        try:
+            self.__clientPin.change_pin(old_pin=oldPin, new_pin=newPin)
+            return True, self.tr("PIN was changed successfully.")
+        except CtapError as err:
+            return (
+                False,
+                self.tr("<p>Failed to change the PIN.</p><p>Reason: {0}</p>").format(
+                    self.__pinErrorMessage(err)
+                ),
+            )
 
     def setPin(self, pin):
         """
@@ -201,9 +617,22 @@
 
         @param pin PIN to be set
         @type str
+        @return flag indicating success and a message
+        @rtype tuple of (bool, str)
         """
-        # TODO: not implemented yet
-        pass
+        if self.__ctap2 is None or self.__clientPin is None:
+            return False, self.tr("No security key connected.")
+
+        try:
+            self.__clientPin.set_pin(pin=pin)
+            return True, self.tr("PIN was set successfully.")
+        except CtapError as err:
+            return (
+                False,
+                self.tr("<p>Failed to set the PIN.</p><p>Reason: {0}</p>").format(
+                    self.__pinErrorMessage(err)
+                ),
+            )
 
     def verifyPin(self, pin):
         """
@@ -217,17 +646,17 @@
         @rtype tuple of (bool, str)
         """
         if self.__ctap2 is None or self.__clientPin is None:
-            return False
+            return False, self.tr("No security key connected.")
 
         try:
             self.__clientPin.get_pin_token(
                 pin, ClientPin.PERMISSION.GET_ASSERTION, "eric-ide.python-projects.org"
             )
-            return True, self.tr("PIN verified")
+            return True, self.tr("PIN was verified.")
         except CtapError as err:
             return (
                 False,
-                self.tr("<p>PIN verification failed.</p><p>Reason: {0}").format(
+                self.tr("<p>PIN verification failed.</p><p>Reason: {0}</p>").format(
                     self.__pinErrorMessage(err)
                 ),
             )
@@ -248,6 +677,8 @@
             msg = self.tr("PIN is blocked.")
         elif errorCode == CtapError.ERR.PIN_NOT_SET:
             msg = self.tr("No PIN set.")
+        elif errorCode == CtapError.ERR.PIN_POLICY_VIOLATION:
+            msg = self.tr("New PIN doesn't meet complexity requirements.")
         else:
             msg = str(err)
         return msg
--- a/src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.py	Sat Jul 20 11:14:51 2024 +0200
+++ b/src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.py	Mon Jul 22 10:15:41 2024 +0200
@@ -7,7 +7,13 @@
 """
 
 from PyQt6.QtCore import Qt, QTimer, pyqtSlot
-from PyQt6.QtWidgets import QDialog, QTreeWidgetItem
+from PyQt6.QtWidgets import (
+    QDialog,
+    QDialogButtonBox,
+    QMenu,
+    QToolButton,
+    QTreeWidgetItem,
+)
 
 from eric7.EricGui import EricPixmapCache
 from eric7.EricGui.EricOverrideCursor import EricOverrideCursor
@@ -31,10 +37,13 @@
     DisplayNameColumn = 2
     UserNameColumn = 3
 
-    def __init__(self, parent=None):
+    def __init__(self, standalone=False, parent=None):
         """
         Constructor
 
+        @param standalone flag indicating the standalone management application
+            (defaults to False)
+        @type bool (optional)
         @param parent reference to the parent widget (defaults to None)
         @type QWidget (optional)
         """
@@ -44,14 +53,56 @@
         self.reloadButton.setIcon(EricPixmapCache.getIcon("reload"))
         self.lockButton.setIcon(EricPixmapCache.getIcon("locked"))
 
+        self.menuButton.setObjectName("fido2_supermenu_button")
+        self.menuButton.setIcon(EricPixmapCache.getIcon("superMenu"))
+        self.menuButton.setToolTip(self.tr("Security Key Management Menu"))
+        self.menuButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
+        self.menuButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
+        self.menuButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
+        self.menuButton.setAutoRaise(True)
+        self.menuButton.setShowMenuInside(True)
+
+        self.__initManagementMenu()
+
+        if standalone:
+            self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setText(
+                self.tr("Quit")
+            )
+
         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)
 
+    def __initManagementMenu(self):
+        """
+        Private method to initialize the security key management menu with
+        actions not needed so much.
+        """
+        self.__mgmtMenu = QMenu()
+        self.__mgmtMenu.addAction(self.tr("Show Info"), self.__showSecurityKeyInfo)
+        self.__mgmtMenu.addSeparator()
+        self.__mgmtMenu.addAction(
+            self.tr("Reset Security Key"), self.__resetSecurityKey
+        )
+        # TODO: potentially add these 'config' actions
+        #       - Force PIN Change
+        #       - Set Minimum PIN Length
+        #       - Toggle 'Always Require UV'
+
+        self.__mgmtMenu.aboutToShow.connect(self.__aboutToShowManagementMenu)
+
+        self.menuButton.setMenu(self.__mgmtMenu)
+
+    @pyqtSlot()
+    def __aboutToShowManagementMenu(self):
+        """
+        Private slot to prepare the security key management menu before it is shown.
+        """
+        # TODO: not implemented yet
+        pass
+
     ############################################################################
     ## methods related to device handling
     ############################################################################
@@ -104,8 +155,7 @@
 
         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.menuButton.setEnabled(securityKey is not None)
         self.loadPasskeysButton.setEnabled(securityKey is not None)
 
         if securityKey is not None:
@@ -166,12 +216,61 @@
             self.__manager.lockDevice()
 
     @pyqtSlot()
-    def on_showInfoButton_clicked(self):
+    def __showSecurityKeyInfo(self):
+        """
+        Private slot to show some info about the selected security key.
+        """
+        from .Fido2InfoDialog import Fido2InfoDialog
+
+        securityKey = self.securityKeysComboBox.currentData()
+        dlg = Fido2InfoDialog(
+            header=securityKey.product_name, manager=self.__manager, parent=self
+        )
+        dlg.exec()
+
+    @pyqtSlot()
+    def __resetSecurityKey(self):
+        """
+        Private slot to reset the selected security key.
         """
-        Slot documentation goes here.
-        """
-        # TODO: not implemented yet
-        pass
+        title = self.tr("Reset Security Key")
+
+        yes = EricMessageBox.yesNo(
+            parent=self,
+            title=title,
+            text=self.tr(
+                "<p>Shall the selected security key really be reset?</p><p><b>WARNING"
+                ":</b> This will delete all passkeys and restore factory settings.</p>"
+            ),
+        )
+        if yes:
+            if len(self.__manager.getDevices()) != 1:
+                EricMessageBox.critical(
+                    self,
+                    title=title,
+                    text=self.tr(
+                        "Only one security key can be connected to perform a reset."
+                        " Remove all other security keys and try again."
+                    ),
+                )
+                return
+
+            EricMessageBox.information(
+                self,
+                title=title,
+                text=self.tr(
+                    "Confirm this dialog then remove and re-insert the security key."
+                    " Confirm the reset by touching it."
+                ),
+            )
+
+            ok, msg = self.__manager.resetDevice()
+            if ok:
+                EricMessageBox.information(self, title, msg)
+            else:
+                EricMessageBox.warning(self, title, msg)
+
+            self.__populateDeviceSelector()
 
     ############################################################################
     ## methods related to PIN handling
@@ -241,13 +340,9 @@
                 dlg = Fido2PinDialog(
                     mode=Fido2PinDialogMode.GET,
                     title=title,
-                    message=self.tr(
-                        "Enter the PIN to unlock the security key (%n attempt(s)"
-                        " remaining.",
-                        "",
-                        retries,
-                    ),
+                    message=self.tr("Enter the PIN to unlock the security key."),
                     minLength=self.__manager.getMinimumPinLength(),
+                    retries=retries,
                     parent=self,
                 )
                 if dlg.exec() == QDialog.DialogCode.Accepted:
@@ -262,23 +357,60 @@
         """
         Private slot to set a PIN for the selected security key.
         """
-        # TODO: not implemented yet
-        pass
+        retries = self.__manager.getPinRetries()[0]
+        title = self.tr("Set PIN")
+
+        dlg = Fido2PinDialog(
+            mode=Fido2PinDialogMode.SET,
+            title=title,
+            message=self.tr("Enter the PIN for the security key."),
+            minLength=self.__manager.getMinimumPinLength(),
+            retries=retries,
+            parent=self,
+        )
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            newPin = dlg.getPins()[1]
+            ok, msg = self.__manager.setPin(newPin)
+            if ok:
+                self.lockButton.setEnabled(True)
+                self.lockButton.setChecked(False)
+                self.pinButton.setText(self.tr("Change PIN"))
+                self.loadPasskeysButton.setEnabled(True)
+                self.__manager.reconnectToDevice()
+                EricMessageBox.information(self, title, msg)
+            else:
+                EricMessageBox.warning(self, title, msg)
 
     @pyqtSlot()
     def __changePin(self):
         """
-        Private slot to set a PIN for the selected security key.
+        Private slot to change the PIN of the selected security key.
         """
-        # TODO: not implemented yet
-        pass
+        retries = self.__manager.getPinRetries()[0]
+        title = self.tr("Change PIN")
+
+        dlg = Fido2PinDialog(
+            mode=Fido2PinDialogMode.CHANGE,
+            title=title,
+            message=self.tr("Enter the current and new PINs."),
+            minLength=self.__manager.getMinimumPinLength(),
+            retries=retries,
+            parent=self,
+        )
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            oldPin, newPin = dlg.getPins()
+            ok, msg = self.__manager.changePin(oldPin, newPin)
+            if ok:
+                self.lockButton.setChecked(False)
+                EricMessageBox.information(self, title, msg)
+            else:
+                EricMessageBox.warning(self, title, msg)
 
     @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:
@@ -345,14 +477,14 @@
     @pyqtSlot()
     def on_loadPasskeysButton_clicked(self):
         """
-        Slot documentation goes here.
+        Private slot to (re-)populate the passkeys list.
         """
         self.__populatePasskeysList()
 
     @pyqtSlot()
     def on_passkeysList_itemSelectionChanged(self):
         """
-        Slot documentation goes here.
+        Private slot handling the selection of a passkey.
         """
         enableButtons = (
             len(self.passkeysList.selectedItems()) == 1
--- a/src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.ui	Sat Jul 20 11:14:51 2024 +0200
+++ b/src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.ui	Mon Jul 22 10:15:41 2024 +0200
@@ -60,29 +60,9 @@
       </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>
+      <widget class="EricToolButton" name="menuButton">
+       <property name="popupMode">
+        <enum>QToolButton::InstantPopup</enum>
        </property>
       </widget>
      </item>
@@ -292,16 +272,23 @@
    </item>
   </layout>
  </widget>
+ <customwidgets>
+  <customwidget>
+   <class>EricToolButton</class>
+   <extends>QToolButton</extends>
+   <header>eric7/EricWidgets/EricToolButton.h</header>
+  </customwidget>
+ </customwidgets>
  <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>
+  <tabstop>menuButton</tabstop>
  </tabstops>
  <resources/>
  <connections>
--- a/src/eric7/WebBrowser/WebAuth/Fido2PinDialog.py	Sat Jul 20 11:14:51 2024 +0200
+++ b/src/eric7/WebBrowser/WebAuth/Fido2PinDialog.py	Mon Jul 22 10:15:41 2024 +0200
@@ -31,7 +31,7 @@
     Class implementing a dialog to enter the current and potentially new PIN.
     """
 
-    def __init__(self, mode, title, message, minLength, parent=None):
+    def __init__(self, mode, title, message, minLength, retries, parent=None):
         """
         Constructor
 
@@ -43,6 +43,8 @@
         @type str
         @param minLength minimum PIN length
         @type int
+        @param retries number of attempts remaining before the security key get locked
+        @type int
         @param parent reference to the parent widget (defaults to None)
         @type QWidget (optional)
         """
@@ -63,6 +65,11 @@
             self.descriptionLabel.setText(message)
         else:
             self.descriptionLabel.setVisible(False)
+        if self.__mode == Fido2PinDialogMode.SET:
+            self.remainingWidget.setVisible(False)
+        else:
+            self.remainingWidget.setVisible(True)
+            self.remainingLabel.setText(str(retries))
         self.pinErrorLabel.setVisible(False)
 
         if mode == Fido2PinDialogMode.GET:
@@ -91,15 +98,10 @@
         """
         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.tr("New PIN is too short (minimum length: {0}).").format(
                         self.__minLength
                     )
                 )
@@ -133,7 +135,7 @@
             if len(errorMessages) == 1:
                 msg = errorMessages[0]
             else:
-                msg = "<ul><li>{0}</li></ul>".format("</li><li>".join(errorMessages))
+                msg = "- {0}".format("\n- ".join(errorMessages))
             self.pinErrorLabel.setText(msg)
             self.pinErrorLabel.setVisible(True)
 
@@ -169,9 +171,9 @@
             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())
+        self.confirmNewPinLabel.setVisible(not checked)
+        self.confirmNewPinEdit.setVisible(not checked)
+        self.__checkPins()
 
     def getPins(self):
         """
@@ -184,7 +186,7 @@
             return self.pinEdit.text(), None
         elif self.__mode == Fido2PinDialogMode.SET:
             return None, self.newPinEdit.text()
-        elif self.__mode == Fido2PinDialogMode.GET:
+        elif self.__mode == Fido2PinDialogMode.CHANGE:
             return self.pinEdit.text(), self.newPinEdit.text()
         else:
             return None, None
--- a/src/eric7/WebBrowser/WebAuth/Fido2PinDialog.ui	Sat Jul 20 11:14:51 2024 +0200
+++ b/src/eric7/WebBrowser/WebAuth/Fido2PinDialog.ui	Mon Jul 22 10:15:41 2024 +0200
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>614</width>
-    <height>251</height>
+    <width>400</width>
+    <height>280</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -29,9 +29,47 @@
      <property name="text">
       <string notr="true">Description</string>
      </property>
-     <property name="wordWrap">
-      <bool>true</bool>
-     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QWidget" name="remainingWidget" native="true">
+     <layout class="QHBoxLayout" name="horizontalLayout_2">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>0</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>0</number>
+      </property>
+      <item>
+       <widget class="QLabel" name="label">
+        <property name="text">
+         <string>Attempts remaining:</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QLabel" name="remainingLabel"/>
+      </item>
+      <item>
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>251</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+     </layout>
     </widget>
    </item>
    <item>
@@ -143,6 +181,13 @@
    </item>
   </layout>
  </widget>
+ <tabstops>
+  <tabstop>pinEdit</tabstop>
+  <tabstop>newPinEdit</tabstop>
+  <tabstop>confirmNewPinEdit</tabstop>
+  <tabstop>pinButton</tabstop>
+  <tabstop>newPinButton</tabstop>
+ </tabstops>
  <resources/>
  <connections>
   <connection>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/WebBrowser/WebAuth/Ui_Fido2InfoDialog.py	Mon Jul 22 10:15:41 2024 +0200
@@ -0,0 +1,45 @@
+# Form implementation generated from reading ui file 'src/eric7/WebBrowser/WebAuth/Fido2InfoDialog.ui'
+#
+# Created by: PyQt6 UI code generator 6.7.1
+#
+# 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_Fido2InfoDialog(object):
+    def setupUi(self, Fido2InfoDialog):
+        Fido2InfoDialog.setObjectName("Fido2InfoDialog")
+        Fido2InfoDialog.resize(600, 700)
+        Fido2InfoDialog.setSizeGripEnabled(True)
+        self.verticalLayout = QtWidgets.QVBoxLayout(Fido2InfoDialog)
+        self.verticalLayout.setObjectName("verticalLayout")
+        self.headerLabel = QtWidgets.QLabel(parent=Fido2InfoDialog)
+        self.headerLabel.setText("Header")
+        self.headerLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
+        self.headerLabel.setObjectName("headerLabel")
+        self.verticalLayout.addWidget(self.headerLabel)
+        self.infoWidget = QtWidgets.QTreeWidget(parent=Fido2InfoDialog)
+        self.infoWidget.setAlternatingRowColors(True)
+        self.infoWidget.setColumnCount(2)
+        self.infoWidget.setObjectName("infoWidget")
+        self.infoWidget.headerItem().setText(0, "1")
+        self.infoWidget.headerItem().setText(1, "2")
+        self.infoWidget.header().setVisible(False)
+        self.verticalLayout.addWidget(self.infoWidget)
+        self.buttonBox = QtWidgets.QDialogButtonBox(parent=Fido2InfoDialog)
+        self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal)
+        self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Close)
+        self.buttonBox.setObjectName("buttonBox")
+        self.verticalLayout.addWidget(self.buttonBox)
+
+        self.retranslateUi(Fido2InfoDialog)
+        self.buttonBox.accepted.connect(Fido2InfoDialog.accept) # type: ignore
+        self.buttonBox.rejected.connect(Fido2InfoDialog.reject) # type: ignore
+        QtCore.QMetaObject.connectSlotsByName(Fido2InfoDialog)
+
+    def retranslateUi(self, Fido2InfoDialog):
+        _translate = QtCore.QCoreApplication.translate
+        Fido2InfoDialog.setWindowTitle(_translate("Fido2InfoDialog", "Security Key Information"))
--- a/src/eric7/WebBrowser/WebAuth/Ui_Fido2ManagementDialog.py	Sat Jul 20 11:14:51 2024 +0200
+++ b/src/eric7/WebBrowser/WebAuth/Ui_Fido2ManagementDialog.py	Mon Jul 22 10:15:41 2024 +0200
@@ -1,6 +1,6 @@
 # Form implementation generated from reading ui file 'src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.ui'
 #
-# Created by: PyQt6 UI code generator 6.7.0
+# Created by: PyQt6 UI code generator 6.7.1
 #
 # 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.
@@ -36,17 +36,10 @@
         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.menuButton = EricToolButton(parent=Fido2ManagementDialog)
+        self.menuButton.setPopupMode(QtWidgets.QToolButton.ToolButtonPopupMode.InstantPopup)
+        self.menuButton.setObjectName("menuButton")
+        self.horizontalLayout.addWidget(self.menuButton)
         self.verticalLayout_2.addLayout(self.horizontalLayout)
         self.groupBox = QtWidgets.QGroupBox(parent=Fido2ManagementDialog)
         self.groupBox.setObjectName("groupBox")
@@ -118,12 +111,12 @@
         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.pinButton, 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)
+        Fido2ManagementDialog.setTabOrder(self.reloadButton, self.menuButton)
 
     def retranslateUi(self, Fido2ManagementDialog):
         _translate = QtCore.QCoreApplication.translate
@@ -133,10 +126,6 @@
         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"))
@@ -151,3 +140,4 @@
         self.deleteButton.setText(_translate("Fido2ManagementDialog", "Delete"))
         self.label.setText(_translate("Fido2ManagementDialog", "Existing Passkeys:"))
         self.label_2.setText(_translate("Fido2ManagementDialog", "Max. Remaining Passkeys:"))
+from eric7.EricWidgets.EricToolButton import EricToolButton
--- a/src/eric7/WebBrowser/WebAuth/Ui_Fido2PinDialog.py	Sat Jul 20 11:14:51 2024 +0200
+++ b/src/eric7/WebBrowser/WebAuth/Ui_Fido2PinDialog.py	Mon Jul 22 10:15:41 2024 +0200
@@ -1,6 +1,6 @@
 # Form implementation generated from reading ui file 'src/eric7/WebBrowser/WebAuth/Fido2PinDialog.ui'
 #
-# Created by: PyQt6 UI code generator 6.7.0
+# Created by: PyQt6 UI code generator 6.7.1
 #
 # 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.
@@ -12,7 +12,7 @@
 class Ui_Fido2PinDialog(object):
     def setupUi(self, Fido2PinDialog):
         Fido2PinDialog.setObjectName("Fido2PinDialog")
-        Fido2PinDialog.resize(614, 251)
+        Fido2PinDialog.resize(400, 280)
         Fido2PinDialog.setSizeGripEnabled(True)
         self.verticalLayout = QtWidgets.QVBoxLayout(Fido2PinDialog)
         self.verticalLayout.setObjectName("verticalLayout")
@@ -22,9 +22,22 @@
         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.remainingWidget = QtWidgets.QWidget(parent=Fido2PinDialog)
+        self.remainingWidget.setObjectName("remainingWidget")
+        self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.remainingWidget)
+        self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
+        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
+        self.label = QtWidgets.QLabel(parent=self.remainingWidget)
+        self.label.setObjectName("label")
+        self.horizontalLayout_2.addWidget(self.label)
+        self.remainingLabel = QtWidgets.QLabel(parent=self.remainingWidget)
+        self.remainingLabel.setObjectName("remainingLabel")
+        self.horizontalLayout_2.addWidget(self.remainingLabel)
+        spacerItem = QtWidgets.QSpacerItem(251, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
+        self.horizontalLayout_2.addItem(spacerItem)
+        self.verticalLayout.addWidget(self.remainingWidget)
         self.horizontalLayout = QtWidgets.QHBoxLayout()
         self.horizontalLayout.setObjectName("horizontalLayout")
         self.pinLabel = QtWidgets.QLabel(parent=Fido2PinDialog)
@@ -78,10 +91,15 @@
         self.buttonBox.accepted.connect(Fido2PinDialog.accept) # type: ignore
         self.buttonBox.rejected.connect(Fido2PinDialog.reject) # type: ignore
         QtCore.QMetaObject.connectSlotsByName(Fido2PinDialog)
+        Fido2PinDialog.setTabOrder(self.pinEdit, self.newPinEdit)
+        Fido2PinDialog.setTabOrder(self.newPinEdit, self.confirmNewPinEdit)
+        Fido2PinDialog.setTabOrder(self.confirmNewPinEdit, self.pinButton)
+        Fido2PinDialog.setTabOrder(self.pinButton, self.newPinButton)
 
     def retranslateUi(self, Fido2PinDialog):
         _translate = QtCore.QCoreApplication.translate
         Fido2PinDialog.setWindowTitle(_translate("Fido2PinDialog", "PIN Entry"))
+        self.label.setText(_translate("Fido2PinDialog", "Attempts remaining:"))
         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."))
--- a/src/eric7/eric7_fido2.py	Sat Jul 20 11:14:51 2024 +0200
+++ b/src/eric7/eric7_fido2.py	Mon Jul 22 10:15:41 2024 +0200
@@ -108,7 +108,7 @@
     """
     from eric7.WebBrowser.WebAuth.Fido2ManagementDialog import Fido2ManagementDialog
     
-    return Fido2ManagementDialog()
+    return Fido2ManagementDialog(standalone=True)
 
 
 def main():

eric ide

mercurial