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 |