importhashlibfromreimportsplitfromtypingimportAny,Dict,Sequencefromurllib.parseimportparse_qsl,unquote,urlparsefrom.importcontrib# noqa:F401from.compatimportrandomfrom.hotpimportHOTPasHOTPfrom.otpimportOTPasOTPfrom.totpimportTOTPasTOTPdefrandom_base32(length:int=32,chars:Sequence[str]="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.iflength<32:raiseValueError("Secrets should be at least 160 bits")return"".join(random.choice(chars)for_inrange(length))defrandom_hex(length:int=40,chars:Sequence[str]="ABCDEF0123456789")->str:iflength<40:raiseValueError("Secrets should be at least 160 bits")returnrandom_base32(length=length,chars=chars)
[docs]defparse_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 constructorotp_data:Dict[str,Any]={}# Parse with URLlibparsed_uri=urlparse(unquote(uri))ifparsed_uri.scheme!="otpauth":raiseValueError("Not an otpauth URI")# Parse issuer/accountname infoaccountinfo_parts=split(":|%3A",parsed_uri.path[1:],maxsplit=1)iflen(accountinfo_parts)==1:otp_data["name"]=accountinfo_parts[0]else:otp_data["issuer"]=accountinfo_parts[0]otp_data["name"]=accountinfo_parts[1]# Parse valuesforkey,valueinparse_qsl(parsed_uri.query):ifkey=="secret":secret=valueelifkey=="issuer":if"issuer"inotp_dataandotp_data["issuer"]isnotNoneandotp_data["issuer"]!=value:raiseValueError("If issuer is specified in both label and parameters, it should be equal.")otp_data["issuer"]=valueelifkey=="algorithm":ifvalue=="SHA1":otp_data["digest"]=hashlib.sha1elifvalue=="SHA256":otp_data["digest"]=hashlib.sha256elifvalue=="SHA512":otp_data["digest"]=hashlib.sha512else:raiseValueError("Invalid value for algorithm, must be SHA1, SHA256 or SHA512")elifkey=="encoder":encoder=valueelifkey=="digits":digits=int(value)otp_data["digits"]=digitselifkey=="period":otp_data["interval"]=int(value)elifkey=="counter":otp_data["initial_count"]=int(value)ifencoder!="steam":ifdigitsisnotNoneanddigitsnotin[6,7,8]:raiseValueError("Digits may only be 6, 7, or 8")ifnotsecret:raiseValueError("No secret found in URI")# Create objectsifencoder=="steam":returncontrib.Steam(secret,**otp_data)ifparsed_uri.netloc=="totp":returnTOTP(secret,**otp_data)elifparsed_uri.netloc=="hotp":returnHOTP(secret,**otp_data)raiseValueError("Not a supported OTP type")