Files
Messenger/app/utils/totp.py
benya 27d3340a37
Some checks failed
CI / test (push) Failing after 21s
feat(auth): add TOTP 2FA setup and login verification
- 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
2026-03-08 11:43:51 +03:00

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