Fri, 19 Jul 2024 18:06:48 +0200
Started implementing a dialog to manage FIDO2 security keys.
--- 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>