Path: blob/trunk/py/selenium/webdriver/common/virtual_authenticator.py
1864 views
# Licensed to the Software Freedom Conservancy (SFC) under one1# or more contributor license agreements. See the NOTICE file2# distributed with this work for additional information3# regarding copyright ownership. The SFC licenses this file4# to you under the Apache License, Version 2.0 (the5# "License"); you may not use this file except in compliance6# with the License. You may obtain a copy of the License at7#8# http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing,11# software distributed under the License is distributed on an12# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13# KIND, either express or implied. See the License for the14# specific language governing permissions and limitations15# under the License.1617import functools18from base64 import urlsafe_b64decode, urlsafe_b64encode19from enum import Enum20from typing import Any, Optional, Union212223class Protocol(str, Enum):24"""Protocol to communicate with the authenticator."""2526CTAP2 = "ctap2"27U2F = "ctap1/u2f"282930class Transport(str, Enum):31"""Transport method to communicate with the authenticator."""3233BLE = "ble"34USB = "usb"35NFC = "nfc"36INTERNAL = "internal"373839class VirtualAuthenticatorOptions:40# These are so unnecessary but are now public API so we can't remove them without deprecating first.41# These should not be class level state in here.42Protocol = Protocol43Transport = Transport4445def __init__(46self,47protocol: str = Protocol.CTAP2,48transport: str = Transport.USB,49has_resident_key: bool = False,50has_user_verification: bool = False,51is_user_consenting: bool = True,52is_user_verified: bool = False,53) -> None:54"""Constructor.5556Initialize VirtualAuthenticatorOptions object.57"""5859self.protocol: str = protocol60self.transport: str = transport61self.has_resident_key: bool = has_resident_key62self.has_user_verification: bool = has_user_verification63self.is_user_consenting: bool = is_user_consenting64self.is_user_verified: bool = is_user_verified6566def to_dict(self) -> dict[str, Union[str, bool]]:67return {68"protocol": self.protocol,69"transport": self.transport,70"hasResidentKey": self.has_resident_key,71"hasUserVerification": self.has_user_verification,72"isUserConsenting": self.is_user_consenting,73"isUserVerified": self.is_user_verified,74}757677class Credential:78def __init__(79self,80credential_id: bytes,81is_resident_credential: bool,82rp_id: str,83user_handle: Optional[bytes],84private_key: bytes,85sign_count: int,86):87"""Constructor. A credential stored in a virtual authenticator.88https://w3c.github.io/webauthn/#credential-parameters.8990:Args:91- credential_id (bytes): Unique base64 encoded string.92- is_resident_credential (bool): Whether the credential is client-side discoverable.93- rp_id (str): Relying party identifier.94- user_handle (bytes): userHandle associated to the credential. Must be Base64 encoded string. Can be None.95- private_key (bytes): Base64 encoded PKCS#8 private key.96- sign_count (int): initial value for a signature counter.97"""98self._id = credential_id99self._is_resident_credential = is_resident_credential100self._rp_id = rp_id101self._user_handle = user_handle102self._private_key = private_key103self._sign_count = sign_count104105@property106def id(self) -> str:107return urlsafe_b64encode(self._id).decode()108109@property110def is_resident_credential(self) -> bool:111return self._is_resident_credential112113@property114def rp_id(self) -> str:115return self._rp_id116117@property118def user_handle(self) -> Optional[str]:119if self._user_handle:120return urlsafe_b64encode(self._user_handle).decode()121return None122123@property124def private_key(self) -> str:125return urlsafe_b64encode(self._private_key).decode()126127@property128def sign_count(self) -> int:129return self._sign_count130131@classmethod132def create_non_resident_credential(cls, id: bytes, rp_id: str, private_key: bytes, sign_count: int) -> "Credential":133"""Creates a non-resident (i.e. stateless) credential.134135:Args:136- id (bytes): Unique base64 encoded string.137- rp_id (str): Relying party identifier.138- private_key (bytes): Base64 encoded PKCS139- sign_count (int): initial value for a signature counter.140141:Returns:142- Credential: A non-resident credential.143"""144return cls(id, False, rp_id, None, private_key, sign_count)145146@classmethod147def create_resident_credential(148cls, id: bytes, rp_id: str, user_handle: Optional[bytes], private_key: bytes, sign_count: int149) -> "Credential":150"""Creates a resident (i.e. stateful) credential.151152:Args:153- id (bytes): Unique base64 encoded string.154- rp_id (str): Relying party identifier.155- user_handle (bytes): userHandle associated to the credential. Must be Base64 encoded string.156- private_key (bytes): Base64 encoded PKCS157- sign_count (int): initial value for a signature counter.158159:returns:160- Credential: A resident credential.161"""162return cls(id, True, rp_id, user_handle, private_key, sign_count)163164def to_dict(self) -> dict[str, Any]:165credential_data = {166"credentialId": self.id,167"isResidentCredential": self._is_resident_credential,168"rpId": self.rp_id,169"privateKey": self.private_key,170"signCount": self.sign_count,171}172173if self.user_handle:174credential_data["userHandle"] = self.user_handle175176return credential_data177178@classmethod179def from_dict(cls, data: dict[str, Any]) -> "Credential":180_id = urlsafe_b64decode(f"{data['credentialId']}==")181is_resident_credential = bool(data["isResidentCredential"])182rp_id = data["rpId"]183private_key = urlsafe_b64decode(f"{data['privateKey']}==")184sign_count = int(data["signCount"])185user_handle = urlsafe_b64decode(f"{data['userHandle']}==") if data.get("userHandle", None) else None186187return cls(_id, is_resident_credential, rp_id, user_handle, private_key, sign_count)188189def __str__(self) -> str:190return f"Credential(id={self.id}, is_resident_credential={self.is_resident_credential}, rp_id={self.rp_id},\191user_handle={self.user_handle}, private_key={self.private_key}, sign_count={self.sign_count})"192193194def required_chromium_based_browser(func):195"""A decorator to ensure that the client used is a chromium based196browser."""197198@functools.wraps(func)199def wrapper(self, *args, **kwargs):200assert self.caps["browserName"].lower() not in [201"firefox",202"safari",203], "This only currently works in Chromium based browsers"204return func(self, *args, **kwargs)205206return wrapper207208209def required_virtual_authenticator(func):210"""A decorator to ensure that the function is called with a virtual211authenticator."""212213@functools.wraps(func)214@required_chromium_based_browser215def wrapper(self, *args, **kwargs):216if not self.virtual_authenticator_id:217raise ValueError("This function requires a virtual authenticator to be set.")218return func(self, *args, **kwargs)219220return wrapper221222223