"""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")