codekingpro/portable-devtools
114k
1import unicodedata2from hmac import compare_digest3from typing import Dict, Optional, Union4from urllib.parse import quote, urlencode, urlparse5 6 7def build_uri(8 secret: str,9 name: str,10 initial_count: Optional[int] = None,11 issuer: Optional[str] = None,12 algorithm: Optional[str] = None,13 digits: Optional[int] = None,14 period: Optional[int] = None,15 image: Optional[str] = None,16) -> str:17 """18 Returns the provisioning URI for the OTP; works for either TOTP or HOTP.19 20 This can then be encoded in a QR Code and used to provision the Google21 Authenticator app.22 23 For module-internal use.24 25 See also:26 https://github.com/google/google-authenticator/wiki/Key-Uri-Format27 28 :param secret: the hotp/totp secret used to generate the URI29 :param name: name of the account30 :param initial_count: starting counter value, defaults to None.31 If none, the OTP type will be assumed as TOTP.32 :param issuer: the name of the OTP issuer; this will be the33 organization title of the OTP entry in Authenticator34 :param algorithm: the algorithm used in the OTP generation.35 :param digits: the length of the OTP generated code.36 :param period: the number of seconds the OTP generator is set to37 expire every code.38 :param image: optional logo image url39 :returns: provisioning uri40 """41 # initial_count may be 0 as a valid param42 is_initial_count_present = initial_count is not None43 44 # Handling values different from defaults45 is_algorithm_set = algorithm is not None and algorithm != "sha1"46 is_digits_set = digits is not None and digits != 647 is_period_set = period is not None and period != 3048 49 otp_type = "hotp" if is_initial_count_present else "totp"50 base_uri = "otpauth://{0}/{1}?{2}"51 52 url_args: Dict[str, Union[None, int, str]] = {"secret": secret}53 54 label = quote(name)55 if issuer is not None:56 label = quote(issuer) + ":" + label57 url_args["issuer"] = issuer58 59 if is_initial_count_present:60 url_args["counter"] = initial_count61 if is_algorithm_set:62 url_args["algorithm"] = algorithm.upper() # type: ignore63 if is_digits_set:64 url_args["digits"] = digits65 if is_period_set:66 url_args["period"] = period67 if image:68 image_uri = urlparse(image)69 if image_uri.scheme != "https" or not image_uri.netloc or not image_uri.path:70 raise ValueError("{} is not a valid url".format(image_uri))71 url_args["image"] = image72 73 uri = base_uri.format(otp_type, label, urlencode(url_args).replace("+", "%20"))74 return uri75 76 77def strings_equal(s1: str, s2: str) -> bool:78 """79 Timing-attack resistant string comparison.80 81 Normal comparison using == will short-circuit on the first mismatching82 character. This avoids that by scanning the whole string, though we83 still reveal to a timing attack whether the strings are the same84 length.85 """86 s1 = unicodedata.normalize("NFKC", s1)87 s2 = unicodedata.normalize("NFKC", s2)88 return compare_digest(s1.encode("utf-8"), s2.encode("utf-8"))89 