207 lines
6.7 KiB
Python
207 lines
6.7 KiB
Python
|
|
# -*- 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}")
|