Path: blob/trunk/py/selenium/webdriver/common/virtual_authenticator.py
3998 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 Any212223class 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"""58self.protocol: str = protocol59self.transport: str = transport60self.has_resident_key: bool = has_resident_key61self.has_user_verification: bool = has_user_verification62self.is_user_consenting: bool = is_user_consenting63self.is_user_verified: bool = is_user_verified6465def to_dict(self) -> dict[str, str | bool]:66return {67"protocol": self.protocol,68"transport": self.transport,69"hasResidentKey": self.has_resident_key,70"hasUserVerification": self.has_user_verification,71"isUserConsenting": self.is_user_consenting,72"isUserVerified": self.is_user_verified,73}747576class Credential:77def __init__(78self,79credential_id: bytes,80is_resident_credential: bool,81rp_id: str | None,82user_handle: bytes | None,83private_key: bytes,84sign_count: int,85):86"""Constructor. A credential stored in a virtual authenticator.8788https://w3c.github.io/webauthn/#credential-parameters.8990Args:91credential_id (bytes): Unique base64 encoded string.92is_resident_credential (bool): Whether the credential is client-side discoverable.93rp_id (str): Relying party identifier.94user_handle (bytes): userHandle associated to the credential. Must be Base64 encoded string. Can be None.95private_key (bytes): Base64 encoded PKCS#8 private key.96sign_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 | None:115return self._rp_id116117@property118def user_handle(self) -> str | None: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.134135Args:136id (bytes): Unique base64 encoded string.137rp_id (str): Relying party identifier.138private_key (bytes): Base64 encoded PKCS139sign_count (int): initial value for a signature counter.140141Returns:142Credential: 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: bytes | None, private_key: bytes, sign_count: int149) -> "Credential":150"""Creates a resident (i.e. stateful) credential.151152Args:153id (bytes): Unique base64 encoded string.154rp_id (str): Relying party identifier.155user_handle (bytes): userHandle associated to the credential. Must be Base64 encoded string.156private_key (bytes): Base64 encoded PKCS157sign_count (int): initial value for a signature counter.158159Returns:160Credential: 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.get("rpId", None)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"""Decorator to ensure that the client used is a chromium-based browser."""196197@functools.wraps(func)198def wrapper(self, *args, **kwargs):199assert self.caps["browserName"].lower() not in [200"firefox",201"safari",202], "This only currently works in Chromium based browsers"203return func(self, *args, **kwargs)204205return wrapper206207208def required_virtual_authenticator(func):209"""Decorator to ensure that the function is called with a virtual authenticator."""210211@functools.wraps(func)212@required_chromium_based_browser213def wrapper(self, *args, **kwargs):214if not self.virtual_authenticator_id:215raise ValueError("This function requires a virtual authenticator to be set.")216return func(self, *args, **kwargs)217218return wrapper219220221