1import unicodedata
2from hmac import compare_digest
3from typing import Dict, Optional, Union
4from urllib.parse import quote, urlencode, urlparse
5
6
7def build_uri(secret: str, name: str, initial_count: Optional[int] = None, issuer: Optional[str] = None,
8              algorithm: Optional[str] = None, digits: Optional[int] = None, period: Optional[int] = None,
9              image: Optional[str] = None) -> str:
10    """
11    Returns the provisioning URI for the OTP; works for either TOTP or HOTP.
12
13    This can then be encoded in a QR Code and used to provision the Google
14    Authenticator app.
15
16    For module-internal use.
17
18    See also:
19        https://github.com/google/google-authenticator/wiki/Key-Uri-Format
20
21    :param secret: the hotp/totp secret used to generate the URI
22    :param name: name of the account
23    :param initial_count: starting counter value, defaults to None.
24        If none, the OTP type will be assumed as TOTP.
25    :param issuer: the name of the OTP issuer; this will be the
26        organization title of the OTP entry in Authenticator
27    :param algorithm: the algorithm used in the OTP generation.
28    :param digits: the length of the OTP generated code.
29    :param period: the number of seconds the OTP generator is set to
30        expire every code.
31    :param image: optional logo image url
32    :returns: provisioning uri
33    """
34    # initial_count may be 0 as a valid param
35    is_initial_count_present = (initial_count is not None)
36
37    # Handling values different from defaults
38    is_algorithm_set = (algorithm is not None and algorithm != 'sha1')
39    is_digits_set = (digits is not None and digits != 6)
40    is_period_set = (period is not None and period != 30)
41
42    otp_type = 'hotp' if is_initial_count_present else 'totp'
43    base_uri = 'otpauth://{0}/{1}?{2}'
44
45    url_args = {'secret': secret}  # type: Dict[str, Union[None, int, str]]
46
47    label = quote(name)
48    if issuer is not None:
49        label = quote(issuer) + ':' + label
50        url_args['issuer'] = issuer
51
52    if is_initial_count_present:
53        url_args['counter'] = initial_count
54    if is_algorithm_set:
55        url_args['algorithm'] = algorithm.upper()  # type: ignore
56    if is_digits_set:
57        url_args['digits'] = digits
58    if is_period_set:
59        url_args['period'] = period
60    if image:
61        image_uri = urlparse(image)
62        if image_uri.scheme != 'https' or not image_uri.netloc or not image_uri.path:
63            raise ValueError('{} is not a valid url'.format(image_uri))
64        url_args['image'] = image
65
66    uri = base_uri.format(otp_type, label, urlencode(url_args).replace("+", "%20"))
67    return uri
68
69
70def strings_equal(s1: str, s2: str) -> bool:
71    """
72    Timing-attack resistant string comparison.
73
74    Normal comparison using == will short-circuit on the first mismatching
75    character. This avoids that by scanning the whole string, though we
76    still reveal to a timing attack whether the strings are the same
77    length.
78    """
79    s1 = unicodedata.normalize('NFKC', s1)
80    s2 = unicodedata.normalize('NFKC', s2)
81    return compare_digest(s1.encode("utf-8"), s2.encode("utf-8"))
82