111 lines
3.7 KiB
Python
111 lines
3.7 KiB
Python
import hashlib
|
|
from re import split
|
|
from typing import Any, Dict, Sequence
|
|
from urllib.parse import parse_qsl, unquote, urlparse
|
|
|
|
from . import contrib # noqa:F401
|
|
from .compat import random
|
|
from .hotp import HOTP as HOTP
|
|
from .otp import OTP as OTP
|
|
from .totp import TOTP as TOTP
|
|
|
|
|
|
def random_base32(length: int = 32, chars: Sequence[str] = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567")) -> str:
|
|
# Note: the otpauth scheme DOES NOT use base32 padding for secret lengths not divisible by 8.
|
|
# Some third-party tools have bugs when dealing with such secrets.
|
|
# We might consider warning the user when generating a secret of length not divisible by 8.
|
|
if length < 32:
|
|
raise ValueError("Secrets should be at least 160 bits")
|
|
|
|
return "".join(random.choice(chars) for _ in range(length))
|
|
|
|
|
|
def random_hex(length: int = 40, chars: Sequence[str] = list("ABCDEF0123456789")) -> str:
|
|
if length < 40:
|
|
raise ValueError("Secrets should be at least 160 bits")
|
|
return random_base32(length=length, chars=chars)
|
|
|
|
|
|
def parse_uri(uri: str) -> OTP:
|
|
"""
|
|
Parses the provisioning URI for the OTP; works for either TOTP or HOTP.
|
|
|
|
See also:
|
|
https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
|
|
|
:param uri: the hotp/totp URI to parse
|
|
:returns: OTP object
|
|
"""
|
|
|
|
# Secret (to be filled in later)
|
|
secret = None
|
|
|
|
# Encoder (to be filled in later)
|
|
encoder = None
|
|
|
|
# Digits (to be filled in later)
|
|
digits = None
|
|
|
|
# Data we'll parse to the correct constructor
|
|
otp_data: Dict[str, Any] = {}
|
|
|
|
# Parse with URLlib
|
|
parsed_uri = urlparse(unquote(uri))
|
|
|
|
if parsed_uri.scheme != "otpauth":
|
|
raise ValueError("Not an otpauth URI")
|
|
|
|
# Parse issuer/accountname info
|
|
accountinfo_parts = split(":|%3A", parsed_uri.path[1:], maxsplit=1)
|
|
if len(accountinfo_parts) == 1:
|
|
otp_data["name"] = accountinfo_parts[0]
|
|
else:
|
|
otp_data["issuer"] = accountinfo_parts[0]
|
|
otp_data["name"] = accountinfo_parts[1]
|
|
|
|
# Parse values
|
|
for key, value in parse_qsl(parsed_uri.query):
|
|
if key == "secret":
|
|
secret = value
|
|
elif key == "issuer":
|
|
if "issuer" in otp_data and otp_data["issuer"] is not None and otp_data["issuer"] != value:
|
|
raise ValueError("If issuer is specified in both label and parameters, it should be equal.")
|
|
otp_data["issuer"] = value
|
|
elif key == "algorithm":
|
|
if value == "SHA1":
|
|
otp_data["digest"] = hashlib.sha1
|
|
elif value == "SHA256":
|
|
otp_data["digest"] = hashlib.sha256
|
|
elif value == "SHA512":
|
|
otp_data["digest"] = hashlib.sha512
|
|
else:
|
|
raise ValueError("Invalid value for algorithm, must be SHA1, SHA256 or SHA512")
|
|
elif key == "encoder":
|
|
encoder = value
|
|
elif key == "digits":
|
|
digits = int(value)
|
|
otp_data["digits"] = digits
|
|
elif key == "period":
|
|
otp_data["interval"] = int(value)
|
|
elif key == "counter":
|
|
otp_data["initial_count"] = int(value)
|
|
elif key != "image":
|
|
raise ValueError("{} is not a valid parameter".format(key))
|
|
|
|
if encoder != "steam":
|
|
if digits is not None and digits not in [6, 7, 8]:
|
|
raise ValueError("Digits may only be 6, 7, or 8")
|
|
|
|
if not secret:
|
|
raise ValueError("No secret found in URI")
|
|
|
|
# Create objects
|
|
if encoder == "steam":
|
|
return contrib.Steam(secret, **otp_data)
|
|
if parsed_uri.netloc == "totp":
|
|
return TOTP(secret, **otp_data)
|
|
elif parsed_uri.netloc == "hotp":
|
|
return HOTP(secret, **otp_data)
|
|
|
|
raise ValueError("Not a supported OTP type")
|