v_9.3
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
"""Time-based one-time passwords (TOTP, RFC 6238) using only the standard
|
||||
library. Compatible with Google Authenticator, Authy, 1Password, etc."""
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
import struct
|
||||
import time
|
||||
from urllib.parse import quote
|
||||
|
||||
ISSUER = "MartinhalApprovalFlow"
|
||||
|
||||
|
||||
def new_secret() -> str:
|
||||
"""A new base32 secret to enroll in an authenticator app."""
|
||||
return base64.b32encode(secrets.token_bytes(20)).decode()
|
||||
|
||||
|
||||
def _code_at(secret: str, counter: int) -> str:
|
||||
key = base64.b32decode(secret + "=" * (-len(secret) % 8), casefold=True)
|
||||
digest = hmac.new(key, struct.pack(">Q", counter), hashlib.sha1).digest()
|
||||
offset = digest[-1] & 15
|
||||
number = (struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF) % 1_000_000
|
||||
return f"{number:06d}"
|
||||
|
||||
|
||||
def verify(secret: str, code: str) -> bool:
|
||||
"""Check a 6-digit code, allowing one 30s step of clock drift each way."""
|
||||
code = (code or "").strip().replace(" ", "")
|
||||
if not (secret and code.isdigit() and len(code) == 6):
|
||||
return False
|
||||
counter = int(time.time() // 30)
|
||||
return any(hmac.compare_digest(_code_at(secret, counter + drift), code)
|
||||
for drift in (-1, 0, 1))
|
||||
|
||||
|
||||
def otpauth_uri(username: str, secret: str) -> str:
|
||||
"""URI encoded as a QR code for authenticator apps."""
|
||||
return (f"otpauth://totp/{quote(ISSUER)}:{quote(username)}"
|
||||
f"?secret={secret}&issuer={quote(ISSUER)}&algorithm=SHA1&digits=6&period=30")
|
||||
Reference in New Issue
Block a user