Files
aza/AzA march 2026 - Kopie (10)/aza_activation.py
2026-04-16 13:32:32 +02:00

207 lines
6.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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-<hmac_hex[:12]>
- 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-<sig>'.
"""
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 <YYYY-MM-DD>")
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}")