import hashlib import secrets from datetime import timedelta from django.utils import timezone from infrasynth.shared.settings_utils import get_setting from ..models import ALTCHAChallenge class ALTCHAService: """Self-hosted proof-of-work challenge (anti-spam). The client must find an integer ``number`` such that the SHA-256 of ``salt + number`` is divisible by ``difficulty``. Expected work is ``difficulty`` hashes, so the default of 10 000 is a strong-enough speed bump while staying cheap for a real browser. """ def create_challenge(self) -> dict: difficulty = int(get_setting("INFRASYNTH_SECURITY", "ALTCHA_DIFFICULTY", 10000)) expiry_seconds = int(get_setting("INFRASYNTH_SECURITY", "ALTCHA_CHALLENGE_EXPIRY_SECONDS", 300)) challenge_id = secrets.token_hex(32) salt = secrets.token_hex(16) expires_at = timezone.now() + timedelta(seconds=expiry_seconds) ALTCHAChallenge.objects.create( challenge_id=challenge_id, salt=salt, difficulty=difficulty, expires_at=expires_at, ) return { "challenge_id": challenge_id, "salt": salt, "difficulty": difficulty, "algorithm": "SHA-256", } def verify(self, challenge_id: str, solution: str, number) -> bool: try: number = int(number) except (TypeError, ValueError): return False try: challenge = ALTCHAChallenge.objects.get(challenge_id=challenge_id, is_verified=False) except ALTCHAChallenge.DoesNotExist: return False if challenge.expires_at < timezone.now(): return False expected = hashlib.sha256(f"{challenge.salt}{number}".encode()).hexdigest() if not solution or solution != expected: return False if int(expected, 16) % challenge.difficulty != 0: return False challenge.is_verified = True challenge.save(update_fields=["is_verified"]) return True def compute_solution(self, salt: str, difficulty: int, *, max_iterations: int = 5_000_000) -> tuple[str, int]: """Reference solver (clients do this in JS; used in tests/tools).""" difficulty = max(1, int(difficulty)) number = 0 while number < max_iterations: h = hashlib.sha256(f"{salt}{number}".encode()).hexdigest() if int(h, 16) % difficulty == 0: return h, number number += 1 raise RuntimeError("Could not find an ALTCHA solution within max_iterations.")