# -*- coding: utf-8 -*- """ AZA Aktivierungsschlüssel-System. Ermöglicht dem Entwickler, Freigabeschlüssel für beliebige Geräte zu erzeugen. Die App prüft beim Start: 1. Hartes Ablaufdatum (APP_HARD_EXPIRY) 2. Gültigen Aktivierungsschlüssel (optional, verlängert über APP_HARD_EXPIRY hinaus) Schlüssel-Format: AZA-YYYYMMDD- - YYYY-MM-DD = Ablaufdatum des Schlüssels - HMAC-SHA256(expiry_str, secret)[:12] = Signatur """ import hmac import hashlib import json import os import time from datetime import datetime, date from typing import Optional, Tuple from aza_config import ( get_writable_data_dir, ACTIVATION_CONFIG_FILENAME, ACTIVATION_HMAC_SECRET, APP_HARD_EXPIRY, APP_TRIAL_DAYS, ) def _activation_path() -> str: return os.path.join(get_writable_data_dir(), ACTIVATION_CONFIG_FILENAME) def _sign(expiry_str: str) -> str: """12 Zeichen HMAC-Hex-Signatur für ein Ablaufdatum.""" sig = hmac.new( ACTIVATION_HMAC_SECRET.encode("utf-8"), expiry_str.encode("utf-8"), hashlib.sha256, ).hexdigest() return sig[:12] # ── Schlüssel generieren (für den Entwickler) ────────────────────── def generate_key(expiry_date: str) -> str: """Erzeugt einen Aktivierungsschlüssel. Args: expiry_date: Ablaufdatum als 'YYYY-MM-DD'. Returns: Schlüssel im Format 'AZA-YYYYMMDD-'. """ dt = datetime.strptime(expiry_date, "%Y-%m-%d") tag = dt.strftime("%Y%m%d") sig = _sign(tag) return f"AZA-{tag}-{sig}" # ── Schlüssel validieren ─────────────────────────────────────────── def validate_key(key: str) -> Tuple[bool, Optional[date], str]: """Prüft einen Aktivierungsschlüssel. Returns: (valid, expiry_date_or_None, reason_text) """ if not key or not isinstance(key, str): return False, None, "Kein Schlüssel eingegeben." key = key.strip().upper() parts = key.split("-") if len(parts) != 3 or parts[0] != "AZA": return False, None, "Ungültiges Schlüsselformat." date_part, sig_part = parts[1], parts[2].lower() try: expiry = datetime.strptime(date_part, "%Y%m%d").date() except ValueError: return False, None, "Ungültiges Datum im Schlüssel." expected_sig = _sign(date_part) if not hmac.compare_digest(sig_part, expected_sig): return False, None, "Schlüssel-Signatur ungültig." if expiry < date.today(): return False, expiry, f"Schlüssel abgelaufen am {expiry.strftime('%d.%m.%Y')}." return True, expiry, f"Gültig bis {expiry.strftime('%d.%m.%Y')}." # ── Persistenz ───────────────────────────────────────────────────── def save_activation_key(key: str) -> None: path = _activation_path() data = {"key": key.strip(), "saved_at": int(time.time())} with open(path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def load_activation_key() -> Optional[str]: path = _activation_path() if not os.path.isfile(path): return None try: with open(path, "r", encoding="utf-8") as f: data = json.load(f) return data.get("key") except Exception: return None # ── Startup-Check ────────────────────────────────────────────────── def _get_install_date_file() -> str: return os.path.join(get_writable_data_dir(), "install_date.json") def _get_install_date() -> date: """Liefert das Erstinstallations-Datum; legt es beim ersten Aufruf an.""" path = _get_install_date_file() if os.path.isfile(path): try: with open(path, "r", encoding="utf-8") as f: data = json.load(f) return datetime.strptime(data["install_date"], "%Y-%m-%d").date() except Exception: pass install_d = date.today() try: os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w", encoding="utf-8") as f: json.dump({"install_date": install_d.strftime("%Y-%m-%d")}, f) except Exception: pass return install_d def check_app_access() -> Tuple[bool, str]: """Prüft ob die App gestartet werden darf. Logik: 1. Gespeicherter Aktivierungsschlüssel vorhanden und gültig? -> OK 2. Testphase (APP_TRIAL_DAYS ab Erstinstallation) noch nicht abgelaufen? -> OK 3. Hartes Ablaufdatum (APP_HARD_EXPIRY) als Sicherheitsnetz -> Gesperrt 4. Sonst -> Gesperrt Returns: (allowed, user_message) """ stored_key = load_activation_key() if stored_key: valid, expiry, reason = validate_key(stored_key) if valid: days_left = (expiry - date.today()).days return True, f"Aktiviert bis {expiry.strftime('%d.%m.%Y')} ({days_left} Tage verbleibend)." install_d = _get_install_date() from datetime import timedelta trial_end = install_d + timedelta(days=APP_TRIAL_DAYS) today = date.today() try: hard_expiry = datetime.strptime(APP_HARD_EXPIRY, "%Y-%m-%d").date() except ValueError: hard_expiry = None if hard_expiry and today > hard_expiry: if stored_key: _, _, reason = validate_key(stored_key) return False, f"Testphase und Aktivierung abgelaufen.\n{reason}\nBitte neuen Aktivierungsschlüssel eingeben." return False, "Testphase abgelaufen.\nBitte Aktivierungsschlüssel eingeben, um fortzufahren." if today <= trial_end: days_left = (trial_end - today).days return True, f"Testphase: noch {days_left} Tag(e) verbleibend (bis {trial_end.strftime('%d.%m.%Y')})." if stored_key: _, _, reason = validate_key(stored_key) return False, f"Testphase abgelaufen.\n{reason}\nBitte neuen Aktivierungsschlüssel eingeben." return False, f"Die {APP_TRIAL_DAYS}-tägige Testphase ist abgelaufen.\nBitte Aktivierungsschlüssel eingeben, um fortzufahren." # ── CLI-Hilfsmittel zum Generieren (für den Entwickler) ──────────── if __name__ == "__main__": import sys as _sys if len(_sys.argv) < 2: print("Verwendung: python aza_activation.py ") print("Beispiel: python aza_activation.py 2026-06-30") _sys.exit(1) exp = _sys.argv[1] key = generate_key(exp) print(f"\nAktivierungsschlüssel generiert:") print(f" Ablaufdatum: {exp}") print(f" Schlüssel: {key}") ok, dt, msg = validate_key(key) print(f" Validierung: {'OK' if ok else 'FEHLER'} – {msg}")