5 """ |
5 """ |
6 Module implementing a dialog to manage FIDO2 security keys. |
6 Module implementing a dialog to manage FIDO2 security keys. |
7 """ |
7 """ |
8 |
8 |
9 from PyQt6.QtCore import Qt, QTimer, pyqtSlot |
9 from PyQt6.QtCore import Qt, QTimer, pyqtSlot |
10 from PyQt6.QtWidgets import QDialog, QTreeWidgetItem |
10 from PyQt6.QtWidgets import ( |
|
11 QDialog, |
|
12 QDialogButtonBox, |
|
13 QMenu, |
|
14 QToolButton, |
|
15 QTreeWidgetItem, |
|
16 ) |
11 |
17 |
12 from eric7.EricGui import EricPixmapCache |
18 from eric7.EricGui import EricPixmapCache |
13 from eric7.EricGui.EricOverrideCursor import EricOverrideCursor |
19 from eric7.EricGui.EricOverrideCursor import EricOverrideCursor |
14 from eric7.EricWidgets import EricMessageBox |
20 from eric7.EricWidgets import EricMessageBox |
15 |
21 |
29 RelyingPartyColumn = 0 |
35 RelyingPartyColumn = 0 |
30 CredentialIdColumn = 1 |
36 CredentialIdColumn = 1 |
31 DisplayNameColumn = 2 |
37 DisplayNameColumn = 2 |
32 UserNameColumn = 3 |
38 UserNameColumn = 3 |
33 |
39 |
34 def __init__(self, parent=None): |
40 def __init__(self, standalone=False, parent=None): |
35 """ |
41 """ |
36 Constructor |
42 Constructor |
37 |
43 |
|
44 @param standalone flag indicating the standalone management application |
|
45 (defaults to False) |
|
46 @type bool (optional) |
38 @param parent reference to the parent widget (defaults to None) |
47 @param parent reference to the parent widget (defaults to None) |
39 @type QWidget (optional) |
48 @type QWidget (optional) |
40 """ |
49 """ |
41 super().__init__(parent) |
50 super().__init__(parent) |
42 self.setupUi(self) |
51 self.setupUi(self) |
43 |
52 |
44 self.reloadButton.setIcon(EricPixmapCache.getIcon("reload")) |
53 self.reloadButton.setIcon(EricPixmapCache.getIcon("reload")) |
45 self.lockButton.setIcon(EricPixmapCache.getIcon("locked")) |
54 self.lockButton.setIcon(EricPixmapCache.getIcon("locked")) |
46 |
55 |
|
56 self.menuButton.setObjectName("fido2_supermenu_button") |
|
57 self.menuButton.setIcon(EricPixmapCache.getIcon("superMenu")) |
|
58 self.menuButton.setToolTip(self.tr("Security Key Management Menu")) |
|
59 self.menuButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) |
|
60 self.menuButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) |
|
61 self.menuButton.setFocusPolicy(Qt.FocusPolicy.NoFocus) |
|
62 self.menuButton.setAutoRaise(True) |
|
63 self.menuButton.setShowMenuInside(True) |
|
64 |
|
65 self.__initManagementMenu() |
|
66 |
|
67 if standalone: |
|
68 self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setText( |
|
69 self.tr("Quit") |
|
70 ) |
|
71 |
47 self.reloadButton.clicked.connect(self.__populateDeviceSelector) |
72 self.reloadButton.clicked.connect(self.__populateDeviceSelector) |
48 |
73 |
49 self.__manager = Fido2Management(parent=self) |
74 self.__manager = Fido2Management(parent=self) |
50 ##self.__manager.deviceConnected.connect(self.__deviceConnected) |
|
51 ##self.__manager.deviceDisconnected.connect(self.__deviceDisconnected) |
|
52 |
75 |
53 QTimer.singleShot(0, self.__populateDeviceSelector) |
76 QTimer.singleShot(0, self.__populateDeviceSelector) |
|
77 |
|
78 def __initManagementMenu(self): |
|
79 """ |
|
80 Private method to initialize the security key management menu with |
|
81 actions not needed so much. |
|
82 """ |
|
83 self.__mgmtMenu = QMenu() |
|
84 self.__mgmtMenu.addAction(self.tr("Show Info"), self.__showSecurityKeyInfo) |
|
85 self.__mgmtMenu.addSeparator() |
|
86 self.__mgmtMenu.addAction( |
|
87 self.tr("Reset Security Key"), self.__resetSecurityKey |
|
88 ) |
|
89 # TODO: potentially add these 'config' actions |
|
90 # - Force PIN Change |
|
91 # - Set Minimum PIN Length |
|
92 # - Toggle 'Always Require UV' |
|
93 |
|
94 self.__mgmtMenu.aboutToShow.connect(self.__aboutToShowManagementMenu) |
|
95 |
|
96 self.menuButton.setMenu(self.__mgmtMenu) |
|
97 |
|
98 @pyqtSlot() |
|
99 def __aboutToShowManagementMenu(self): |
|
100 """ |
|
101 Private slot to prepare the security key management menu before it is shown. |
|
102 """ |
|
103 # TODO: not implemented yet |
|
104 pass |
54 |
105 |
55 ############################################################################ |
106 ############################################################################ |
56 ## methods related to device handling |
107 ## methods related to device handling |
57 ############################################################################ |
108 ############################################################################ |
58 |
109 |
102 |
153 |
103 securityKey = self.securityKeysComboBox.itemData(index) |
154 securityKey = self.securityKeysComboBox.itemData(index) |
104 |
155 |
105 self.lockButton.setEnabled(securityKey is not None) |
156 self.lockButton.setEnabled(securityKey is not None) |
106 self.pinButton.setEnabled(securityKey is not None) |
157 self.pinButton.setEnabled(securityKey is not None) |
107 self.showInfoButton.setEnabled(securityKey is not None) |
158 self.menuButton.setEnabled(securityKey is not None) |
108 self.resetButton.setEnabled(securityKey is not None) |
|
109 self.loadPasskeysButton.setEnabled(securityKey is not None) |
159 self.loadPasskeysButton.setEnabled(securityKey is not None) |
110 |
160 |
111 if securityKey is not None: |
161 if securityKey is not None: |
112 self.__manager.connectToDevice(securityKey) |
162 self.__manager.connectToDevice(securityKey) |
113 hasPin = self.__manager.hasPin() |
163 hasPin = self.__manager.hasPin() |
164 # lock the selected security key |
214 # lock the selected security key |
165 self.lockButton.setIcon(EricPixmapCache.getIcon("locked")) |
215 self.lockButton.setIcon(EricPixmapCache.getIcon("locked")) |
166 self.__manager.lockDevice() |
216 self.__manager.lockDevice() |
167 |
217 |
168 @pyqtSlot() |
218 @pyqtSlot() |
169 def on_showInfoButton_clicked(self): |
219 def __showSecurityKeyInfo(self): |
170 """ |
220 """ |
171 Slot documentation goes here. |
221 Private slot to show some info about the selected security key. |
172 """ |
222 """ |
173 # TODO: not implemented yet |
223 from .Fido2InfoDialog import Fido2InfoDialog |
174 pass |
224 |
|
225 securityKey = self.securityKeysComboBox.currentData() |
|
226 dlg = Fido2InfoDialog( |
|
227 header=securityKey.product_name, manager=self.__manager, parent=self |
|
228 ) |
|
229 dlg.exec() |
|
230 |
|
231 @pyqtSlot() |
|
232 def __resetSecurityKey(self): |
|
233 """ |
|
234 Private slot to reset the selected security key. |
|
235 """ |
|
236 title = self.tr("Reset Security Key") |
|
237 |
|
238 yes = EricMessageBox.yesNo( |
|
239 parent=self, |
|
240 title=title, |
|
241 text=self.tr( |
|
242 "<p>Shall the selected security key really be reset?</p><p><b>WARNING" |
|
243 ":</b> This will delete all passkeys and restore factory settings.</p>" |
|
244 ), |
|
245 ) |
|
246 if yes: |
|
247 if len(self.__manager.getDevices()) != 1: |
|
248 EricMessageBox.critical( |
|
249 self, |
|
250 title=title, |
|
251 text=self.tr( |
|
252 "Only one security key can be connected to perform a reset." |
|
253 " Remove all other security keys and try again." |
|
254 ), |
|
255 ) |
|
256 return |
|
257 |
|
258 EricMessageBox.information( |
|
259 self, |
|
260 title=title, |
|
261 text=self.tr( |
|
262 "Confirm this dialog then remove and re-insert the security key." |
|
263 " Confirm the reset by touching it." |
|
264 ), |
|
265 ) |
|
266 |
|
267 ok, msg = self.__manager.resetDevice() |
|
268 if ok: |
|
269 EricMessageBox.information(self, title, msg) |
|
270 else: |
|
271 EricMessageBox.warning(self, title, msg) |
|
272 |
|
273 self.__populateDeviceSelector() |
175 |
274 |
176 ############################################################################ |
275 ############################################################################ |
177 ## methods related to PIN handling |
276 ## methods related to PIN handling |
178 ############################################################################ |
277 ############################################################################ |
179 |
278 |
239 retries = self.__manager.getPinRetries()[0] |
338 retries = self.__manager.getPinRetries()[0] |
240 title = self.tr("PIN required") if feature is None else feature |
339 title = self.tr("PIN required") if feature is None else feature |
241 dlg = Fido2PinDialog( |
340 dlg = Fido2PinDialog( |
242 mode=Fido2PinDialogMode.GET, |
341 mode=Fido2PinDialogMode.GET, |
243 title=title, |
342 title=title, |
244 message=self.tr( |
343 message=self.tr("Enter the PIN to unlock the security key."), |
245 "Enter the PIN to unlock the security key (%n attempt(s)" |
|
246 " remaining.", |
|
247 "", |
|
248 retries, |
|
249 ), |
|
250 minLength=self.__manager.getMinimumPinLength(), |
344 minLength=self.__manager.getMinimumPinLength(), |
|
345 retries=retries, |
251 parent=self, |
346 parent=self, |
252 ) |
347 ) |
253 if dlg.exec() == QDialog.DialogCode.Accepted: |
348 if dlg.exec() == QDialog.DialogCode.Accepted: |
254 return dlg.getPins()[0] |
349 return dlg.getPins()[0] |
255 else: |
350 else: |
260 @pyqtSlot() |
355 @pyqtSlot() |
261 def __setPin(self): |
356 def __setPin(self): |
262 """ |
357 """ |
263 Private slot to set a PIN for the selected security key. |
358 Private slot to set a PIN for the selected security key. |
264 """ |
359 """ |
265 # TODO: not implemented yet |
360 retries = self.__manager.getPinRetries()[0] |
266 pass |
361 title = self.tr("Set PIN") |
|
362 |
|
363 dlg = Fido2PinDialog( |
|
364 mode=Fido2PinDialogMode.SET, |
|
365 title=title, |
|
366 message=self.tr("Enter the PIN for the security key."), |
|
367 minLength=self.__manager.getMinimumPinLength(), |
|
368 retries=retries, |
|
369 parent=self, |
|
370 ) |
|
371 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
372 newPin = dlg.getPins()[1] |
|
373 ok, msg = self.__manager.setPin(newPin) |
|
374 if ok: |
|
375 self.lockButton.setEnabled(True) |
|
376 self.lockButton.setChecked(False) |
|
377 self.pinButton.setText(self.tr("Change PIN")) |
|
378 self.loadPasskeysButton.setEnabled(True) |
|
379 self.__manager.reconnectToDevice() |
|
380 EricMessageBox.information(self, title, msg) |
|
381 else: |
|
382 EricMessageBox.warning(self, title, msg) |
267 |
383 |
268 @pyqtSlot() |
384 @pyqtSlot() |
269 def __changePin(self): |
385 def __changePin(self): |
270 """ |
386 """ |
271 Private slot to set a PIN for the selected security key. |
387 Private slot to change the PIN of the selected security key. |
272 """ |
388 """ |
273 # TODO: not implemented yet |
389 retries = self.__manager.getPinRetries()[0] |
274 pass |
390 title = self.tr("Change PIN") |
|
391 |
|
392 dlg = Fido2PinDialog( |
|
393 mode=Fido2PinDialogMode.CHANGE, |
|
394 title=title, |
|
395 message=self.tr("Enter the current and new PINs."), |
|
396 minLength=self.__manager.getMinimumPinLength(), |
|
397 retries=retries, |
|
398 parent=self, |
|
399 ) |
|
400 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
401 oldPin, newPin = dlg.getPins() |
|
402 ok, msg = self.__manager.changePin(oldPin, newPin) |
|
403 if ok: |
|
404 self.lockButton.setChecked(False) |
|
405 EricMessageBox.information(self, title, msg) |
|
406 else: |
|
407 EricMessageBox.warning(self, title, msg) |
275 |
408 |
276 @pyqtSlot() |
409 @pyqtSlot() |
277 def on_pinButton_clicked(self): |
410 def on_pinButton_clicked(self): |
278 """ |
411 """ |
279 Private slot to set or change the PIN for the selected security key. |
412 Private slot to set or change the PIN for the selected security key. |
280 """ |
413 """ |
281 # TODO: not implemented yet |
|
282 if self.__manager.hasPin(): |
414 if self.__manager.hasPin(): |
283 self.__changePin() |
415 self.__changePin() |
284 else: |
416 else: |
285 self.__setPin() |
417 self.__setPin() |
286 |
418 |
343 ) |
475 ) |
344 |
476 |
345 @pyqtSlot() |
477 @pyqtSlot() |
346 def on_loadPasskeysButton_clicked(self): |
478 def on_loadPasskeysButton_clicked(self): |
347 """ |
479 """ |
348 Slot documentation goes here. |
480 Private slot to (re-)populate the passkeys list. |
349 """ |
481 """ |
350 self.__populatePasskeysList() |
482 self.__populatePasskeysList() |
351 |
483 |
352 @pyqtSlot() |
484 @pyqtSlot() |
353 def on_passkeysList_itemSelectionChanged(self): |
485 def on_passkeysList_itemSelectionChanged(self): |
354 """ |
486 """ |
355 Slot documentation goes here. |
487 Private slot handling the selection of a passkey. |
356 """ |
488 """ |
357 enableButtons = ( |
489 enableButtons = ( |
358 len(self.passkeysList.selectedItems()) == 1 |
490 len(self.passkeysList.selectedItems()) == 1 |
359 and self.passkeysList.selectedItems()[0].parent() is not None |
491 and self.passkeysList.selectedItems()[0].parent() is not None |
360 ) |
492 ) |