61 lines
2.1 KiB
Python
61 lines
2.1 KiB
Python
|
|
import pyotp
|
||
|
|
import qrcode
|
||
|
|
import io
|
||
|
|
import base64
|
||
|
|
import secrets
|
||
|
|
import hashlib
|
||
|
|
|
||
|
|
from typing import List, Optional
|
||
|
|
|
||
|
|
def generate_totp_secret() -> str:
|
||
|
|
"""Generates a random base32 TOTP secret."""
|
||
|
|
return pyotp.random_base32()
|
||
|
|
|
||
|
|
def generate_totp_uri(secret: str, account_name: str, issuer_name: str) -> str:
|
||
|
|
"""Generates a Google Authenticator-compatible TOTP URI."""
|
||
|
|
return pyotp.totp.TOTP(secret).provisioning_uri(name=account_name, issuer_name=issuer_name)
|
||
|
|
|
||
|
|
def generate_qr_code_base64(uri: str) -> str:
|
||
|
|
"""Generates a base64 encoded QR code image for a given URI."""
|
||
|
|
qr = qrcode.QRCode(
|
||
|
|
version=1,
|
||
|
|
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||
|
|
box_size=10,
|
||
|
|
border=4,
|
||
|
|
)
|
||
|
|
qr.add_data(uri)
|
||
|
|
qr.make(fit=True)
|
||
|
|
|
||
|
|
img = qr.make_image(fill_color="black", back_color="white")
|
||
|
|
buffered = io.BytesIO()
|
||
|
|
img.save(buffered, format="PNG")
|
||
|
|
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||
|
|
|
||
|
|
def verify_totp_code(secret: str, code: str) -> bool:
|
||
|
|
"""Verifies a TOTP code against a secret."""
|
||
|
|
totp = pyotp.TOTP(secret)
|
||
|
|
return totp.verify(code)
|
||
|
|
|
||
|
|
def generate_recovery_codes(num_codes: int = 10) -> List[str]:
|
||
|
|
"""Generates a list of random recovery codes."""
|
||
|
|
return [secrets.token_urlsafe(16) for _ in range(num_codes)]
|
||
|
|
|
||
|
|
def hash_recovery_code(code: str) -> str:
|
||
|
|
"""Hashes a single recovery code using SHA256."""
|
||
|
|
return hashlib.sha256(code.encode('utf-8')).hexdigest()
|
||
|
|
|
||
|
|
def verify_recovery_code(plain_code: str, hashed_code: str) -> bool:
|
||
|
|
"""Verifies a plain recovery code against its hashed version."""
|
||
|
|
return hash_recovery_code(plain_code) == hashed_code
|
||
|
|
|
||
|
|
def hash_recovery_codes(codes: List[str]) -> List[str]:
|
||
|
|
"""Hashes a list of recovery codes."""
|
||
|
|
return [hash_recovery_code(code) for code in codes]
|
||
|
|
|
||
|
|
def verify_recovery_codes(plain_code: str, hashed_codes: List[str]) -> bool:
|
||
|
|
"""Verifies if a plain recovery code matches any of the hashed recovery codes."""
|
||
|
|
for hashed_code in hashed_codes:
|
||
|
|
if verify_recovery_code(plain_code, hashed_code):
|
||
|
|
return True
|
||
|
|
return False
|