src/eric7/WebBrowser/WebAuth/Fido2Management.py

branch
eric7
changeset 10856
b19cefceca15
parent 10854
30c45bd597e6
child 10857
abcb288e7e17
equal deleted inserted replaced
10855:9082eb8f6571 10856:b19cefceca15
3 # 3 #
4 4
5 """ 5 """
6 Module implementing a manager for FIDO2 security keys. 6 Module implementing a manager for FIDO2 security keys.
7 """ 7 """
8
9 import time
8 10
9 from fido2.ctap import CtapError 11 from fido2.ctap import CtapError
10 from fido2.ctap2 import ClientPin, CredentialManagement, Ctap2 12 from fido2.ctap2 import ClientPin, CredentialManagement, Ctap2
11 from fido2.hid import CtapHidDevice 13 from fido2.hid import CtapHidDevice
12 from fido2.webauthn import PublicKeyCredentialUserEntity 14 from fido2.webauthn import PublicKeyCredentialUserEntity
13 from PyQt6.QtCore import QObject, pyqtSignal 15 from PyQt6.QtCore import QCoreApplication, QObject, QThread, pyqtSignal
14 16
15 17
16 class Fido2PinError(Exception): 18 class Fido2PinError(Exception):
17 """ 19 """
18 Class signaling an issue with the PIN. 20 Class signaling an issue with the PIN.
38 """ 40 """
39 41
40 deviceConnected = pyqtSignal() 42 deviceConnected = pyqtSignal()
41 deviceDisconnected = pyqtSignal() 43 deviceDisconnected = pyqtSignal()
42 44
45 FidoVersion2Str = {
46 "FIDO_2_1": "CTAP 2.1 / FIDO2",
47 "FIDO_2_0": "CTAP 2.0 / FIDO2",
48 "FIDO_2_1_PRE": QCoreApplication.translate(
49 "Fido2Management", "CTAP2.1 Preview Features"
50 ),
51 "U2F_V2": "CTAP 1 / U2F",
52 }
53
54 FidoExtension2Str = {
55 "credBlob": QCoreApplication.translate("Fido2Management", "Credential BLOB"),
56 "credProtect": QCoreApplication.translate(
57 "Fido2Management", "Credential Protection"
58 ),
59 "hmac-secret": QCoreApplication.translate("Fido2Management", "HMAC Secret"),
60 "largeBlobKey": QCoreApplication.translate("Fido2Management", "Large Blob Key"),
61 "minPinLength": QCoreApplication.translate(
62 "Fido2Management", "Minimum PIN Length"
63 ),
64 }
65
66 FidoInfoCategories2Str = {
67 "pin": QCoreApplication.translate("Fido2Management", "PIN"),
68 "security_key": QCoreApplication.translate("Fido2Management", "Security Key"),
69 "options": QCoreApplication.translate("Fido2Management", "Options"),
70 "extensions": QCoreApplication.translate("Fido2Management", "Extensions"),
71 }
72
43 def __init__(self, parent=None): 73 def __init__(self, parent=None):
44 """ 74 """
45 Constructor 75 Constructor
46 76
47 @param parent reference to the parent object (defaults to None) 77 @param parent reference to the parent object (defaults to None)
75 self.__clientPin = None 105 self.__clientPin = None
76 self.__pin = None 106 self.__pin = None
77 107
78 self.deviceDisconnected.emit() 108 self.deviceDisconnected.emit()
79 109
110 def reconnectToDevice(self):
111 """
112 Public method to reconnect the current security key.
113 """
114 if self.__ctap2 is not None:
115 self.connectToDevice(self.__ctap2.device)
116
80 def unlockDevice(self, pin): 117 def unlockDevice(self, pin):
81 """ 118 """
82 Public method to unlock the device (i.e. store the PIN for later use). 119 Public method to unlock the device (i.e. store the PIN for later use).
83 120
84 @param pin PIN to be stored 121 @param pin PIN to be stored
109 @return list of connected security keys 146 @return list of connected security keys
110 @rtype list of CtapHidDevice 147 @rtype list of CtapHidDevice
111 """ 148 """
112 return list(CtapHidDevice.list_devices()) 149 return list(CtapHidDevice.list_devices())
113 150
114 def getKeyInfo(self): 151 def getSecurityKeyInfo(self):
115 """ 152 """
116 Public method to get information about the connected security key. 153 Public method to get information about the connected security key.
117 154
118 @return dictionary containing the info data 155 @return dictionary containing the info data
119 @rtype dict[str, Any] 156 @rtype dict[str, list[tuple[str, str]]]
120 """ 157 """
121 # TODO: not implemented yet 158 if self.__ctap2 is None:
122 return {} 159 return {}
160
161 # each entry is a list of tuples containing the display name and the value
162 data = {
163 "pin": [],
164 "security_key": [],
165 "options": [],
166 "extensions": [],
167 }
168
169 # PIN related data
170 if self.__ctap2.info.options["clientPin"]:
171 if self.__ctap2.info.force_pin_change:
172 msg = self.tr(
173 "PIN is disabled and must be changed before it can be used!"
174 )
175 pinRetries, powerCycle = self.getPinRetries()
176 if pinRetries:
177 if powerCycle:
178 msg = self.tr(
179 "PIN is temporarily blocked. Remove and re-insert the"
180 " security keyto unblock it."
181 )
182 else:
183 msg = self.tr("%n attempts remaining", "", pinRetries)
184 else:
185 msg = self.tr("PIN is blocked. The security key needs to be reset.")
186 else:
187 msg = self.tr("A PIN has not been set.")
188 data["pin"].append((self.tr("PIN"), msg))
189
190 alwaysUv = self.__ctap2.info.options.get("alwaysUv")
191 msg = (
192 self.tr("not supported")
193 if alwaysUv is None
194 else self.tr("switched on") if alwaysUv else self.tr("switched off")
195 )
196 data["pin"].append((self.tr("Always require User Verification"), msg))
197
198 remainingPasskeys = self.__ctap2.info.remaining_disc_creds
199 if remainingPasskeys is not None:
200 data["pin"].append(
201 (self.tr("Passkeys storage remaining"), str(remainingPasskeys))
202 )
203
204 enterprise = self.__ctap2.info.options.get("ep")
205 if enterprise is not None:
206 data["pin"].append(
207 (
208 self.tr("Enterprise Attestation"),
209 self.tr("enabled") if enterprise else self.tr("disabled"),
210 )
211 )
212
213 # security key related data
214 data["security_key"].extend(
215 [
216 (self.tr("Name"), self.__ctap2.device.product_name),
217 (self.tr("Path"), self.__ctap2.device.descriptor.path),
218 (
219 self.tr("Version"),
220 ".".join(str(p) for p in self.__ctap2.device.device_version),
221 ),
222 (self.tr("Vendor ID"), f"0x{self.__ctap2.device.descriptor.vid:04x}"),
223 (self.tr("Product ID"), f"0x{self.__ctap2.device.descriptor.pid:04x}"),
224 ]
225 )
226 serial = self.__ctap2.device.serial_number
227 if serial is not None:
228 data["security_key"].append((self.tr("Serial Number"), serial))
229 data["security_key"].append(
230 (
231 self.tr("Supported Versions"),
232 "\n".join(
233 self.FidoVersion2Str.get(v, v) for v in self.__ctap2.info.versions
234 ),
235 )
236 )
237 data["security_key"].append(
238 (self.tr("Supported Transports"), "\n".join(self.__ctap2.info.transports))
239 )
240
241 # extensions data
242 if self.__ctap2.info.extensions:
243 for ext in self.FidoExtension2Str:
244 data["extensions"].append(
245 (
246 self.FidoExtension2Str[ext],
247 (
248 self.tr("supported")
249 if ext in self.__ctap2.info.extensions
250 else self.tr("not supported")
251 ),
252 )
253 )
254
255 # options data
256 options = self.__ctap2.info.options
257 data["options"].append(
258 (
259 self.tr("Is Platform Device"),
260 self.tr("yes") if options.get("plat", False) else self.tr("no"),
261 )
262 )
263 data["options"].append(
264 (
265 self.tr("Resident Passkeys"),
266 (
267 self.tr("supported")
268 if options.get("rk", False)
269 else self.tr("not supported")
270 ),
271 )
272 )
273 cp = options.get("clientPin")
274 data["options"].append(
275 (
276 self.tr("Client PIN"),
277 (
278 self.tr("not supported")
279 if cp is None
280 else (
281 self.tr("supported, PIN set")
282 if cp is True
283 else self.tr("supported, PIN not set")
284 )
285 ),
286 )
287 )
288 data["options"].append(
289 (
290 self.tr("Detect User Presence"),
291 (
292 self.tr("supported")
293 if options.get("up", True)
294 else self.tr("not supported")
295 ),
296 )
297 )
298 uv = options.get("uv")
299 data["options"].append(
300 (
301 self.tr("User Verification"),
302 (
303 self.tr("not supported")
304 if uv is None
305 else (
306 self.tr("supported, configured")
307 if uv is True
308 else self.tr("supported, not configured")
309 )
310 ),
311 )
312 )
313 data["options"].append(
314 (
315 self.tr("Verify User with Client PIN"),
316 (
317 self.tr("available")
318 if options.get("pinUvAuthToken", False)
319 else self.tr("not available")
320 ),
321 )
322 )
323 data["options"].append(
324 (
325 self.tr("Make Credential / Get Assertion"),
326 (
327 self.tr("available")
328 if options.get("noMcGaPermissionsWithClientPin", False)
329 else self.tr("not available")
330 ),
331 )
332 )
333 data["options"].append(
334 (
335 self.tr("Large BLOBs"),
336 (
337 self.tr("supported")
338 if options.get("largeBlobs", False)
339 else self.tr("not supported")
340 ),
341 )
342 )
343 ep = options.get("ep")
344 data["options"].append(
345 (
346 self.tr("Enterprise Attestation"),
347 (
348 self.tr("not supported")
349 if ep is None
350 else (
351 self.tr("supported, enabled")
352 if ep is True
353 else self.tr("supported, disabled")
354 )
355 ),
356 )
357 )
358 be = options.get("bioEnroll")
359 data["options"].append(
360 (
361 self.tr("Fingerprint"),
362 (
363 self.tr("not supported")
364 if be is None
365 else (
366 self.tr("supported, registered")
367 if be is True
368 else self.tr("supported, not registered")
369 )
370 ),
371 )
372 )
373 uvmp = options.get("userVerificationMgmtPreview")
374 data["options"].append(
375 (
376 self.tr("CTAP2.1 Preview Fingerprint"),
377 (
378 self.tr("not supported")
379 if uvmp is None
380 else (
381 self.tr("supported, registered")
382 if uvmp is True
383 else self.tr("supported, not registered")
384 )
385 ),
386 )
387 )
388 data["options"].append(
389 (
390 self.tr("Verify User for Fingerprint Registration"),
391 (
392 self.tr("supported")
393 if options.get("uvBioEnroll", False)
394 else self.tr("not supported")
395 ),
396 )
397 )
398 data["options"].append(
399 (
400 self.tr("Security Key Configuration"),
401 (
402 self.tr("supported")
403 if options.get("authnrCfg", False)
404 else self.tr("not supported")
405 ),
406 )
407 )
408 data["options"].append(
409 (
410 self.tr("Verify User for Security Key Configuration"),
411 (
412 self.tr("supported")
413 if options.get("uvAcfg", False)
414 else self.tr("not supported")
415 ),
416 )
417 )
418 data["options"].append(
419 (
420 self.tr("Credential Management"),
421 (
422 self.tr("supported")
423 if options.get("credMgmt", False)
424 else self.tr("not supported")
425 ),
426 )
427 )
428 data["options"].append(
429 (
430 self.tr("CTAP2.1 Preview Credential Management"),
431 (
432 self.tr("supported")
433 if options.get("credentialMgmtPreview", False)
434 else self.tr("not supported")
435 ),
436 )
437 )
438 data["options"].append(
439 (
440 self.tr("Set Minimum PIN Length"),
441 (
442 self.tr("supported")
443 if options.get("setMinPINLength", False)
444 else self.tr("not supported")
445 ),
446 )
447 )
448 data["options"].append(
449 (
450 self.tr("Make Non-Resident Passkey without User Verification"),
451 (
452 self.tr("allowed")
453 if options.get("makeCredUvNotRqd", False)
454 else self.tr("not allowed")
455 ),
456 )
457 )
458 auv = options.get("alwaysUv")
459 data["options"].append(
460 (
461 self.tr("Always Require User Verification"),
462 (
463 self.tr("not supported")
464 if auv is None
465 else (
466 self.tr("supported, enabled")
467 if auv is True
468 else self.tr("supported, disabled")
469 )
470 ),
471 )
472 )
473
474 return data
123 475
124 def resetDevice(self): 476 def resetDevice(self):
125 """ 477 """
126 Public method to reset the connected security key. 478 Public method to reset the connected security key.
127 """ 479
128 # TODO: not implemented yet 480 @return flag indicating success and a message
129 pass 481 @rtype tuple of (bool, str)
482 """
483 if self.__ctap2 is None:
484 return False, self.tr("No security key connected.")
485
486 removed = False
487 startTime = time.monotonic()
488 while True:
489 QThread.msleep(500)
490 try:
491 securityKeys = self.getDevices()
492 except OSError:
493 securityKeys = []
494 if not securityKeys:
495 removed = True
496 if removed and len(securityKeys) == 1:
497 ctap2 = Ctap2(securityKeys[0])
498 break
499 if time.monotonic() - startTime >= 30:
500 return False, self.tr(
501 "Reset failed. The security key was not removed and re-inserted"
502 " within 30 seconds."
503 )
504
505 try:
506 ctap2.reset()
507 return True, "The security key has been reset."
508 except CtapError as err:
509 if err.code == CtapError.ERR.ACTION_TIMEOUT:
510 msg = self.tr(
511 "You need to touch your security key to confirm the reset."
512 )
513 elif err.code in (
514 CtapError.ERR.NOT_ALLOWED,
515 CtapError.ERR.PIN_AUTH_BLOCKED,
516 ):
517 msg = self.tr(
518 "Reset must be triggered within 5 seconds after the security"
519 "key is inserted."
520 )
521 else:
522 msg = str(err)
523
524 return False, self.tr("Reset failed. {0}").format(msg)
525 except Exception:
526 return False, self.tr("Reset failed.")
130 527
131 ############################################################################ 528 ############################################################################
132 ## methods related to PIN handling 529 ## methods related to PIN handling
133 ############################################################################ 530 ############################################################################
134 531
173 """ 570 """
174 Public method to get the number of PIN retries left and an indication for the 571 Public method to get the number of PIN retries left and an indication for the
175 need of a power cycle. 572 need of a power cycle.
176 573
177 @return tuple containing the number of retries left and a flag indicating a 574 @return tuple containing the number of retries left and a flag indicating a
178 power cycle is required 575 power cycle is required. A retry value of -1 indicates, that no PIN was
576 set yet.
179 @rtype tuple of (int, bool) 577 @rtype tuple of (int, bool)
180 """ 578 """
181 if self.__ctap2 is None or self.__clientPin is None: 579 if self.__ctap2 is None or self.__clientPin is None:
182 return (None, None) 580 return (None, None)
183 581
184 return self.__clientPin.get_pin_retries() 582 try:
185 583 return self.__clientPin.get_pin_retries()
186 def changePin(self, pin, newPin): 584 except CtapError as err:
585 if err.code == CtapError.ERR.PIN_NOT_SET:
586 # return -1 retries to indicate a missing PIN
587 return (-1, False)
588
589 def changePin(self, oldPin, newPin):
187 """ 590 """
188 Public method to change the PIN of the connected security key. 591 Public method to change the PIN of the connected security key.
189 592
190 @param pin current PIN 593 @param oldPin current PIN
191 @type str 594 @type str
192 @param newPin new PIN 595 @param newPin new PIN
193 @type str 596 @type str
194 """ 597 @return flag indicating success and a message
195 # TODO: not implemented yet 598 @rtype tuple of (bool, str)
196 pass 599 """
600 if self.__ctap2 is None or self.__clientPin is None:
601 return False, self.tr("No security key connected.")
602
603 try:
604 self.__clientPin.change_pin(old_pin=oldPin, new_pin=newPin)
605 return True, self.tr("PIN was changed successfully.")
606 except CtapError as err:
607 return (
608 False,
609 self.tr("<p>Failed to change the PIN.</p><p>Reason: {0}</p>").format(
610 self.__pinErrorMessage(err)
611 ),
612 )
197 613
198 def setPin(self, pin): 614 def setPin(self, pin):
199 """ 615 """
200 Public method to set a PIN for the connected security key. 616 Public method to set a PIN for the connected security key.
201 617
202 @param pin PIN to be set 618 @param pin PIN to be set
203 @type str 619 @type str
204 """ 620 @return flag indicating success and a message
205 # TODO: not implemented yet 621 @rtype tuple of (bool, str)
206 pass 622 """
623 if self.__ctap2 is None or self.__clientPin is None:
624 return False, self.tr("No security key connected.")
625
626 try:
627 self.__clientPin.set_pin(pin=pin)
628 return True, self.tr("PIN was set successfully.")
629 except CtapError as err:
630 return (
631 False,
632 self.tr("<p>Failed to set the PIN.</p><p>Reason: {0}</p>").format(
633 self.__pinErrorMessage(err)
634 ),
635 )
207 636
208 def verifyPin(self, pin): 637 def verifyPin(self, pin):
209 """ 638 """
210 Public method to verify a given PIN. 639 Public method to verify a given PIN.
211 640
215 @type str 644 @type str
216 @return flag indicating successful verification and a verification message 645 @return flag indicating successful verification and a verification message
217 @rtype tuple of (bool, str) 646 @rtype tuple of (bool, str)
218 """ 647 """
219 if self.__ctap2 is None or self.__clientPin is None: 648 if self.__ctap2 is None or self.__clientPin is None:
220 return False 649 return False, self.tr("No security key connected.")
221 650
222 try: 651 try:
223 self.__clientPin.get_pin_token( 652 self.__clientPin.get_pin_token(
224 pin, ClientPin.PERMISSION.GET_ASSERTION, "eric-ide.python-projects.org" 653 pin, ClientPin.PERMISSION.GET_ASSERTION, "eric-ide.python-projects.org"
225 ) 654 )
226 return True, self.tr("PIN verified") 655 return True, self.tr("PIN was verified.")
227 except CtapError as err: 656 except CtapError as err:
228 return ( 657 return (
229 False, 658 False,
230 self.tr("<p>PIN verification failed.</p><p>Reason: {0}").format( 659 self.tr("<p>PIN verification failed.</p><p>Reason: {0}</p>").format(
231 self.__pinErrorMessage(err) 660 self.__pinErrorMessage(err)
232 ), 661 ),
233 ) 662 )
234 663
235 def __pinErrorMessage(self, err): 664 def __pinErrorMessage(self, err):
246 msg = self.tr("Invalid PIN") 675 msg = self.tr("Invalid PIN")
247 elif errorCode == CtapError.ERR.PIN_BLOCKED: 676 elif errorCode == CtapError.ERR.PIN_BLOCKED:
248 msg = self.tr("PIN is blocked.") 677 msg = self.tr("PIN is blocked.")
249 elif errorCode == CtapError.ERR.PIN_NOT_SET: 678 elif errorCode == CtapError.ERR.PIN_NOT_SET:
250 msg = self.tr("No PIN set.") 679 msg = self.tr("No PIN set.")
680 elif errorCode == CtapError.ERR.PIN_POLICY_VIOLATION:
681 msg = self.tr("New PIN doesn't meet complexity requirements.")
251 else: 682 else:
252 msg = str(err) 683 msg = str(err)
253 return msg 684 return msg
254 685
255 ############################################################################ 686 ############################################################################

eric ide

mercurial