import base64 import hashlib import hmac import secrets import struct import time from io import BytesIO from urllib.parse import quote from django.utils.http import urlencode import qrcode from qrcode.image.svg import SvgPathImage from allauth.core import context from allauth.mfa import app_settings from allauth.mfa.models import Authenticator from allauth.mfa.utils import decrypt, encrypt SECRET_SESSION_KEY = "mfa.totp.secret" def generate_totp_secret(length=20): random_bytes = secrets.token_bytes(length) return base64.b32encode(random_bytes).decode("utf-8") def get_totp_secret(regenerate=False): secret = None if not regenerate: secret = context.request.session.get(SECRET_SESSION_KEY) if not secret: secret = context.request.session[SECRET_SESSION_KEY] = generate_totp_secret() return secret def hotp_counter_from_time(): current_time = int(time.time()) # Get the current Unix timestamp return current_time // app_settings.TOTP_PERIOD def hotp_value(secret, counter): # Convert the counter to a byte array using big-endian encoding counter_bytes = struct.pack(">Q", counter) secret_enc = base64.b32decode(secret.encode("ascii"), casefold=True) # Calculate the HMAC-SHA1 hash using the secret and counter hmac_result = hmac.new(secret_enc, counter_bytes, hashlib.sha1).digest() # Get the last 4 bits of the HMAC result to determine the offset offset = hmac_result[-1] & 0x0F # Extract an 31-bit slice from the HMAC result starting at the offset + 1 bit truncated_hash = bytearray(hmac_result[offset : offset + 4]) truncated_hash[0] = truncated_hash[0] & 0x7F # Convert the truncated hash to an integer value value = struct.unpack(">I", truncated_hash)[0] # Apply modulo to get a value within the specified number of digits value %= 10**app_settings.TOTP_DIGITS return value def build_totp_url(label, issuer, secret): params = { "secret": secret, # This is the default # "algorithm": "SHA1", "issuer": issuer, } if app_settings.TOTP_DIGITS != 6: params["digits"] = app_settings.TOTP_DIGITS if app_settings.TOTP_PERIOD != 30: params["period"] = app_settings.TOTP_PERIOD return f"otpauth://totp/{quote(label)}?{urlencode(params)}" def build_totp_svg(url): img = qrcode.make(url, image_factory=SvgPathImage) buf = BytesIO() img.save(buf) return buf.getvalue().decode("utf8") def format_hotp_value(value): return f"{value:0{app_settings.TOTP_DIGITS}}" def validate_totp_code(secret, code): value = hotp_value(secret, hotp_counter_from_time()) return code == format_hotp_value(value) class TOTP: def __init__(self, instance): self.instance = instance @classmethod def activate(cls, user, secret): instance = Authenticator( user=user, type=Authenticator.Type.TOTP, data={"secret": encrypt(secret)} ) instance.save() return cls(instance) def deactivate(self): self.instance.delete() Authenticator.objects.delete_dangling_recovery_codes(self.instance.user) def validate_code(self, code): secret = decrypt(self.instance.data["secret"]) return validate_totp_code(secret, code)