Some checks failed
CI / test (push) Failing after 21s
- add user twofa fields and migration - add 2FA setup/enable/disable endpoints - enforce OTP on login when 2FA enabled - add web login OTP field and settings UI
45 lines
1.6 KiB
Python
45 lines
1.6 KiB
Python
import base64
|
|
import hashlib
|
|
import hmac
|
|
import secrets
|
|
import struct
|
|
import time
|
|
import urllib.parse
|
|
|
|
|
|
def generate_totp_secret(length: int = 20) -> str:
|
|
raw = secrets.token_bytes(length)
|
|
return base64.b32encode(raw).decode("ascii").rstrip("=")
|
|
|
|
|
|
def build_otpauth_uri(*, secret: str, account_name: str, issuer: str) -> str:
|
|
label = urllib.parse.quote(f"{issuer}:{account_name}")
|
|
issuer_q = urllib.parse.quote(issuer)
|
|
secret_q = urllib.parse.quote(secret)
|
|
return f"otpauth://totp/{label}?secret={secret_q}&issuer={issuer_q}&algorithm=SHA1&digits=6&period=30"
|
|
|
|
|
|
def _totp_code(secret: str, for_time: int | None = None, step: int = 30, digits: int = 6) -> str:
|
|
now = int(time.time()) if for_time is None else int(for_time)
|
|
counter = now // step
|
|
padded = secret + "=" * ((8 - len(secret) % 8) % 8)
|
|
key = base64.b32decode(padded, casefold=True)
|
|
msg = struct.pack(">Q", counter)
|
|
digest = hmac.new(key, msg, hashlib.sha1).digest()
|
|
offset = digest[-1] & 0x0F
|
|
binary = struct.unpack(">I", digest[offset : offset + 4])[0] & 0x7FFFFFFF
|
|
return str(binary % (10**digits)).zfill(digits)
|
|
|
|
|
|
def verify_totp_code(secret: str, code: str, *, window: int = 1, step: int = 30, digits: int = 6) -> bool:
|
|
prepared = "".join(ch for ch in code if ch.isdigit())
|
|
if len(prepared) != digits:
|
|
return False
|
|
now = int(time.time())
|
|
for delta in range(-window, window + 1):
|
|
expected = _totp_code(secret, for_time=now + delta * step, step=step, digits=digits)
|
|
if secrets.compare_digest(expected, prepared):
|
|
return True
|
|
return False
|
|
|