# Licensed to the Software Freedom Conservancy (SFC) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The SFC licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. import functools import typing from base64 import urlsafe_b64decode from base64 import urlsafe_b64encode from enum import Enum class Protocol(Enum): """ Protocol to communicate with the authenticator. """ CTAP2 = "ctap2" U2F = "ctap1/u2f" class Transport(Enum): """ Transport method to communicate with the authenticator. """ BLE = "ble" USB = "usb" NFC = "nfc" INTERNAL = "internal" class VirtualAuthenticatorOptions: Protocol = Protocol Transport = Transport def __init__(self) -> None: """Constructor. Initialize VirtualAuthenticatorOptions object. :default: - protocol: Protocol.CTAP2 - transport: Transport.USB - hasResidentKey: False - hasUserVerification: False - isUserConsenting: True - isUserVerified: False """ self._protocol: Protocol = Protocol.CTAP2 self._transport: Transport = Transport.USB self._has_resident_key: bool = False self._has_user_verification: bool = False self._is_user_consenting: bool = True self._is_user_verified: bool = False @property def protocol(self) -> str: return self._protocol.value @protocol.setter def protocol(self, protocol: Protocol) -> None: self._protocol = protocol @property def transport(self) -> str: return self._transport.value @transport.setter def transport(self, transport: Transport) -> None: self._transport = transport @property def has_resident_key(self) -> bool: return self._has_resident_key @has_resident_key.setter def has_resident_key(self, value: bool) -> None: self._has_resident_key = value @property def has_user_verification(self) -> bool: return self._has_user_verification @has_user_verification.setter def has_user_verification(self, value: bool) -> None: self._has_user_verification = value @property def is_user_consenting(self) -> bool: return self._is_user_consenting @is_user_consenting.setter def is_user_consenting(self, value: bool) -> None: self._is_user_consenting = value @property def is_user_verified(self) -> bool: return self._is_user_verified @is_user_verified.setter def is_user_verified(self, value: bool) -> None: self._is_user_verified = value def to_dict(self) -> typing.Dict[str, typing.Any]: return { "protocol": self.protocol, "transport": self.transport, "hasResidentKey": self.has_resident_key, "hasUserVerification": self.has_user_verification, "isUserConsenting": self.is_user_consenting, "isUserVerified": self.is_user_verified, } class Credential: def __init__( self, credential_id: bytes, is_resident_credential: bool, rp_id: str, user_handle: typing.Optional[bytes], private_key: bytes, sign_count: int, ): """Constructor. A credential stored in a virtual authenticator. https://w3c.github.io/webauthn/#credential-parameters :Args: - credential_id (bytes): Unique base64 encoded string. is_resident_credential (bool): Whether the credential is client-side discoverable. rp_id (str): Relying party identifier. user_handle (bytes): userHandle associated to the credential. Must be Base64 encoded string. Can be None. private_key (bytes): Base64 encoded PKCS#8 private key. sign_count (int): intital value for a signature counter. """ self._id = credential_id self._is_resident_credential = is_resident_credential self._rp_id = rp_id self._user_handle = user_handle self._private_key = private_key self._sign_count = sign_count @property def id(self) -> str: return urlsafe_b64encode(self._id).decode() @property def is_resident_credential(self) -> bool: return self._is_resident_credential @property def rp_id(self) -> str: return self._rp_id @property def user_handle(self) -> typing.Optional[str]: if self._user_handle: return urlsafe_b64encode(self._user_handle).decode() return None @property def private_key(self) -> str: return urlsafe_b64encode(self._private_key).decode() @property def sign_count(self) -> int: return self._sign_count @classmethod def create_non_resident_credential(cls, id: bytes, rp_id: str, private_key: bytes, sign_count: int) -> "Credential": """Creates a non-resident (i.e. stateless) credential. :Args: - id (bytes): Unique base64 encoded string. - rp_id (str): Relying party identifier. - private_key (bytes): Base64 encoded PKCS - sign_count (int): intital value for a signature counter. :Returns: - Credential: A non-resident credential. """ return cls(id, False, rp_id, None, private_key, sign_count) @classmethod def create_resident_credential( cls, id: bytes, rp_id: str, user_handle: typing.Optional[bytes], private_key: bytes, sign_count: int ) -> "Credential": """Creates a resident (i.e. stateful) credential. :Args: - id (bytes): Unique base64 encoded string. - rp_id (str): Relying party identifier. - user_handle (bytes): userHandle associated to the credential. Must be Base64 encoded string. - private_key (bytes): Base64 encoded PKCS - sign_count (int): intital value for a signature counter. :returns: - Credential: A resident credential. """ return cls(id, True, rp_id, user_handle, private_key, sign_count) def to_dict(self) -> typing.Dict[str, typing.Any]: credential_data = { "credentialId": self.id, "isResidentCredential": self._is_resident_credential, "rpId": self.rp_id, "privateKey": self.private_key, "signCount": self.sign_count, } if self.user_handle: credential_data["userHandle"] = self.user_handle return credential_data @classmethod def from_dict(cls, data: typing.Dict[str, typing.Any]) -> "Credential": _id = urlsafe_b64decode(f"{data['credentialId']}==") is_resident_credential = bool(data["isResidentCredential"]) rp_id = data.get("rpId", None) private_key = urlsafe_b64decode(f"{data['privateKey']}==") sign_count = int(data["signCount"]) user_handle = urlsafe_b64decode(f"{data['userHandle']}==") if data.get("userHandle", None) else None return cls(_id, is_resident_credential, rp_id, user_handle, private_key, sign_count) def __str__(self) -> str: return f"Credential(id={self.id}, is_resident_credential={self.is_resident_credential}, rp_id={self.rp_id},\ user_handle={self.user_handle}, private_key={self.private_key}, sign_count={self.sign_count})" def required_chromium_based_browser(func): """ A decorator to ensure that the client used is a chromium based browser. """ @functools.wraps(func) def wrapper(self, *args, **kwargs): assert self.caps["browserName"].lower() not in [ "firefox", "safari", ], "This only currently works in Chromium based browsers" return func(self, *args, **kwargs) return wrapper def required_virtual_authenticator(func): """ A decorator to ensure that the function is called with a virtual authenticator. """ @functools.wraps(func) @required_chromium_based_browser def wrapper(self, *args, **kwargs): if not self.virtual_authenticator_id: raise ValueError("This function requires a virtual authenticator to be set.") return func(self, *args, **kwargs) return wrapper