2026-03-25 22:03:39 +01:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
2026-05-04 21:34:19 +02:00
|
|
|
|
AzA Aktivierungsschlüssel-System.
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
2026-05-04 21:34:19 +02:00
|
|
|
|
Erzeugt, prüft und persistiert Aktivierungsschlüssel.
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
2026-05-04 21:34:19 +02:00
|
|
|
|
Schlüssel-Format
|
|
|
|
|
|
----------------
|
|
|
|
|
|
|
|
|
|
|
|
Kanonisch:
|
|
|
|
|
|
|
|
|
|
|
|
AZA-YYYYMMDD-<hmac_hex[:12]>
|
|
|
|
|
|
|
|
|
|
|
|
Beispiel: ``AZA-20271231-1a2b3c4d5e6f``
|
|
|
|
|
|
|
|
|
|
|
|
Damit ältere oder von Hand abgetippte Schlüssel nicht unnötig am
|
|
|
|
|
|
Format scheitern, akzeptiert der Validator ausserdem:
|
|
|
|
|
|
|
|
|
|
|
|
* gross-/kleingeschrieben (``aza-...`` ist gleichwertig zu ``AZA-...``);
|
|
|
|
|
|
* Whitespace, Tabulatoren, Zeilenumbrüche im / vor / nach dem Schlüssel;
|
|
|
|
|
|
* Unicode-Bindestriche (en-dash, em-dash, geschützter Bindestrich);
|
|
|
|
|
|
* Datum mit Trennzeichen (``YYYY-MM-DD``, ``YYYY.MM.DD``, ``YYYY/MM/DD``);
|
|
|
|
|
|
* Signatur-Längen 8, 12 oder 16 (frühere Builds).
|
|
|
|
|
|
|
|
|
|
|
|
Damit funktioniert ein bereits einmal vergebener Schlüssel auf
|
|
|
|
|
|
demselben Computer auch dann weiter, wenn er kopierte Leerzeichen,
|
|
|
|
|
|
PDF-Bindestriche oder leichte Schreibvarianten enthält.
|
2026-03-25 22:03:39 +01:00
|
|
|
|
"""
|
|
|
|
|
|
|
2026-05-04 21:34:19 +02:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-03-25 22:03:39 +01:00
|
|
|
|
import hmac
|
|
|
|
|
|
import hashlib
|
|
|
|
|
|
import json
|
|
|
|
|
|
import os
|
2026-05-04 21:34:19 +02:00
|
|
|
|
import re
|
2026-03-25 22:03:39 +01:00
|
|
|
|
import time
|
2026-05-04 21:34:19 +02:00
|
|
|
|
import unicodedata
|
|
|
|
|
|
from datetime import datetime, date, timedelta
|
2026-03-25 22:03:39 +01:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-04 21:34:19 +02:00
|
|
|
|
# ── Pfade ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-03-25 22:03:39 +01:00
|
|
|
|
def _activation_path() -> str:
|
|
|
|
|
|
return os.path.join(get_writable_data_dir(), ACTIVATION_CONFIG_FILENAME)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-04 21:34:19 +02:00
|
|
|
|
# ── Signatur ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def _sign(expiry_compact: str, length: int = 12) -> str:
|
|
|
|
|
|
"""HMAC-Hex-Signatur über das Ablaufdatum (compact ``YYYYMMDD``)."""
|
2026-03-25 22:03:39 +01:00
|
|
|
|
sig = hmac.new(
|
|
|
|
|
|
ACTIVATION_HMAC_SECRET.encode("utf-8"),
|
2026-05-04 21:34:19 +02:00
|
|
|
|
expiry_compact.encode("utf-8"),
|
2026-03-25 22:03:39 +01:00
|
|
|
|
hashlib.sha256,
|
|
|
|
|
|
).hexdigest()
|
2026-05-04 21:34:19 +02:00
|
|
|
|
if length <= 0:
|
|
|
|
|
|
return sig
|
|
|
|
|
|
return sig[:length]
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-04 21:34:19 +02:00
|
|
|
|
# ── Schlüssel generieren ──────────────────────────────────────────────
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
def generate_key(expiry_date: str) -> str:
|
2026-05-04 21:34:19 +02:00
|
|
|
|
"""Erzeugt einen kanonischen Aktivierungsschlüssel ``AZA-YYYYMMDD-<sig12>``."""
|
2026-03-25 22:03:39 +01:00
|
|
|
|
dt = datetime.strptime(expiry_date, "%Y-%m-%d")
|
|
|
|
|
|
tag = dt.strftime("%Y%m%d")
|
2026-05-04 21:34:19 +02:00
|
|
|
|
return f"AZA-{tag}-{_sign(tag, 12)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Normalisierung ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
# Alle Zeichen, die wir wie einen ASCII-Bindestrich behandeln.
|
|
|
|
|
|
_DASH_CHARS = (
|
|
|
|
|
|
"-", # ASCII
|
|
|
|
|
|
"\u2010", # hyphen
|
|
|
|
|
|
"\u2011", # non-breaking hyphen
|
|
|
|
|
|
"\u2012", # figure dash
|
|
|
|
|
|
"\u2013", # en dash
|
|
|
|
|
|
"\u2014", # em dash
|
|
|
|
|
|
"\u2015", # horizontal bar
|
|
|
|
|
|
"\u2212", # minus sign
|
|
|
|
|
|
"\uFE58", # small em dash
|
|
|
|
|
|
"\uFE63", # small hyphen-minus
|
|
|
|
|
|
"\uFF0D", # fullwidth hyphen-minus
|
|
|
|
|
|
)
|
|
|
|
|
|
_DASH_TRANSLATE = {ord(c): "-" for c in _DASH_CHARS}
|
|
|
|
|
|
_KEY_PREFIX_PATTERN = re.compile(r"^A\W*Z\W*A\b", re.IGNORECASE)
|
|
|
|
|
|
_DATE_BLOCK_PATTERN = re.compile(
|
|
|
|
|
|
r"(?P<y>\d{4})\s*[-./ ]?\s*(?P<m>\d{2})\s*[-./ ]?\s*(?P<d>\d{2})"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_key(raw: str) -> Optional[str]:
|
|
|
|
|
|
"""Bringt einen frei eingegebenen Schlüssel in das kanonische Format
|
|
|
|
|
|
``AZA-YYYYMMDD-<sigHEX>`` (Signatur lower-case).
|
|
|
|
|
|
|
|
|
|
|
|
Liefert ``None`` zurück, wenn schon strukturell nichts brauchbares
|
|
|
|
|
|
erkennbar ist – das ruft dann sauber den Format-Fehler aus."""
|
|
|
|
|
|
if not raw or not isinstance(raw, str):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# Unicode-NFKC, Bindestriche normalisieren, Whitespace komplett raus.
|
|
|
|
|
|
s = unicodedata.normalize("NFKC", raw).translate(_DASH_TRANSLATE)
|
|
|
|
|
|
s = re.sub(r"\s+", "", s)
|
|
|
|
|
|
s = s.upper()
|
|
|
|
|
|
|
|
|
|
|
|
if not s:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# Optionalen "AZA"-Präfix tolerant erkennen (auch z. B. "A-Z-A").
|
|
|
|
|
|
m = _KEY_PREFIX_PATTERN.match(s)
|
|
|
|
|
|
if m:
|
|
|
|
|
|
body = s[m.end():]
|
|
|
|
|
|
else:
|
|
|
|
|
|
body = s
|
|
|
|
|
|
body = body.lstrip("-")
|
|
|
|
|
|
|
|
|
|
|
|
# Datum extrahieren – akzeptiert YYYYMMDD ebenso wie 2027-12-31.
|
|
|
|
|
|
md = _DATE_BLOCK_PATTERN.search(body)
|
|
|
|
|
|
if not md:
|
|
|
|
|
|
return None
|
|
|
|
|
|
tag = f"{md.group('y')}{md.group('m')}{md.group('d')}"
|
|
|
|
|
|
|
|
|
|
|
|
# Alles ab dem Datum entfernen, Rest = Signatur (nur hex behalten).
|
|
|
|
|
|
rest = body[md.end():]
|
|
|
|
|
|
sig = re.sub(r"[^0-9A-F]", "", rest).lower()
|
|
|
|
|
|
if not sig:
|
|
|
|
|
|
# Manchmal sitzt die Signatur direkt vor dem Datum (sehr alte Builds)
|
|
|
|
|
|
head = body[:md.start()]
|
|
|
|
|
|
sig = re.sub(r"[^0-9A-F]", "", head).lower()
|
|
|
|
|
|
|
|
|
|
|
|
if not sig:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-03-25 22:03:39 +01:00
|
|
|
|
return f"AZA-{tag}-{sig}"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-04 21:34:19 +02:00
|
|
|
|
def _try_signature_lengths(date_compact: str, sig: str) -> bool:
|
|
|
|
|
|
"""Prüft die Signatur exakt gegen ihre eigene Länge.
|
|
|
|
|
|
|
|
|
|
|
|
Erlaubt frühere Builds mit 8 oder 16 Zeichen Signatur; vermeidet aber
|
|
|
|
|
|
bewusst, eine 12-Zeichen-Signatur durch einen 8-Zeichen-Match zu
|
|
|
|
|
|
akzeptieren (Manipulationen müssen scheitern)."""
|
|
|
|
|
|
sig_len = len(sig)
|
|
|
|
|
|
if sig_len not in (8, 12, 16, 32, 64):
|
|
|
|
|
|
return False
|
|
|
|
|
|
try:
|
|
|
|
|
|
expected = _sign(date_compact, sig_len)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False
|
|
|
|
|
|
return hmac.compare_digest(sig, expected)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Schlüssel validieren ──────────────────────────────────────────────
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
def validate_key(key: str) -> Tuple[bool, Optional[date], str]:
|
2026-05-04 21:34:19 +02:00
|
|
|
|
"""Prüft einen Aktivierungsschlüssel tolerant.
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
2026-05-04 21:34:19 +02:00
|
|
|
|
``(valid, expiry_date_or_None, reason_text)``
|
2026-03-25 22:03:39 +01:00
|
|
|
|
"""
|
|
|
|
|
|
if not key or not isinstance(key, str):
|
|
|
|
|
|
return False, None, "Kein Schlüssel eingegeben."
|
|
|
|
|
|
|
2026-05-04 21:34:19 +02:00
|
|
|
|
canonical = _normalize_key(key)
|
|
|
|
|
|
if not canonical:
|
|
|
|
|
|
return False, None, (
|
|
|
|
|
|
"Ungültiges Schlüsselformat. Erwartet wird ein "
|
|
|
|
|
|
"Schlüssel der Form AzA-YYYYMMDD-...."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
parts = canonical.split("-")
|
2026-03-25 22:03:39 +01:00
|
|
|
|
if len(parts) != 3 or parts[0] != "AZA":
|
|
|
|
|
|
return False, None, "Ungültiges Schlüsselformat."
|
|
|
|
|
|
|
2026-05-04 21:34:19 +02:00
|
|
|
|
date_part, sig_part = parts[1], parts[2]
|
2026-03-25 22:03:39 +01:00
|
|
|
|
try:
|
|
|
|
|
|
expiry = datetime.strptime(date_part, "%Y%m%d").date()
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return False, None, "Ungültiges Datum im Schlüssel."
|
|
|
|
|
|
|
2026-05-04 21:34:19 +02:00
|
|
|
|
if not _try_signature_lengths(date_part, sig_part):
|
2026-03-25 22:03:39 +01:00
|
|
|
|
return False, None, "Schlüssel-Signatur ungültig."
|
|
|
|
|
|
|
|
|
|
|
|
if expiry < date.today():
|
2026-05-04 21:34:19 +02:00
|
|
|
|
return False, expiry, (
|
|
|
|
|
|
f"Schlüssel abgelaufen am {expiry.strftime('%d.%m.%Y')}."
|
|
|
|
|
|
)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
return True, expiry, f"Gültig bis {expiry.strftime('%d.%m.%Y')}."
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-04 21:34:19 +02:00
|
|
|
|
# ── Persistenz ────────────────────────────────────────────────────────
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
def save_activation_key(key: str) -> None:
|
2026-05-04 21:34:19 +02:00
|
|
|
|
"""Speichert den Schlüssel in kanonischer Form."""
|
|
|
|
|
|
canonical = _normalize_key(key) or (key or "").strip()
|
2026-03-25 22:03:39 +01:00
|
|
|
|
path = _activation_path()
|
2026-05-04 21:34:19 +02:00
|
|
|
|
data = {"key": canonical, "saved_at": int(time.time())}
|
|
|
|
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
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)
|
2026-05-04 21:34:19 +02:00
|
|
|
|
raw = data.get("key")
|
|
|
|
|
|
if not raw:
|
|
|
|
|
|
return None
|
|
|
|
|
|
return _normalize_key(raw) or raw
|
2026-03-25 22:03:39 +01:00
|
|
|
|
except Exception:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-04 21:34:19 +02:00
|
|
|
|
# ── Demo-Modus-Marker ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
DEMO_OPT_OUT_FILENAME = "kg_diktat_demo_opt_in.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _demo_opt_in_path() -> str:
|
|
|
|
|
|
return os.path.join(get_writable_data_dir(), DEMO_OPT_OUT_FILENAME)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mark_demo_opt_in() -> None:
|
|
|
|
|
|
"""Merkt sich, dass der Benutzer bewusst im Demomodus weiterfährt."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
path = _demo_opt_in_path()
|
|
|
|
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
|
|
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
|
|
|
|
json.dump({"opted_in": True, "ts": int(time.time())}, f)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def has_demo_opt_in() -> bool:
|
|
|
|
|
|
try:
|
|
|
|
|
|
path = _demo_opt_in_path()
|
|
|
|
|
|
if not os.path.isfile(path):
|
|
|
|
|
|
return False
|
|
|
|
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
|
|
|
|
data = json.load(f)
|
|
|
|
|
|
return bool(data.get("opted_in"))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Startup-Check ─────────────────────────────────────────────────────
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
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:
|
2026-05-04 21:34:19 +02:00
|
|
|
|
``(allowed, user_message)``
|
2026-03-25 22:03:39 +01:00
|
|
|
|
"""
|
|
|
|
|
|
stored_key = load_activation_key()
|
|
|
|
|
|
if stored_key:
|
2026-05-04 21:34:19 +02:00
|
|
|
|
valid, expiry, _ = validate_key(stored_key)
|
|
|
|
|
|
if valid and expiry is not None:
|
2026-03-25 22:03:39 +01:00
|
|
|
|
days_left = (expiry - date.today()).days
|
2026-05-04 21:34:19 +02:00
|
|
|
|
return True, (
|
|
|
|
|
|
f"Aktiviert bis {expiry.strftime('%d.%m.%Y')} "
|
|
|
|
|
|
f"({days_left} Tage verbleibend)."
|
|
|
|
|
|
)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
install_d = _get_install_date()
|
|
|
|
|
|
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)
|
2026-05-04 21:34:19 +02:00
|
|
|
|
return False, (
|
|
|
|
|
|
f"Testphase und Aktivierung abgelaufen.\n{reason}\n"
|
|
|
|
|
|
"Bitte neuen Aktivierungsschlüssel eingeben."
|
|
|
|
|
|
)
|
|
|
|
|
|
return False, (
|
|
|
|
|
|
"Testphase abgelaufen.\n"
|
|
|
|
|
|
"Bitte Aktivierungsschlüssel eingeben, um fortzufahren."
|
|
|
|
|
|
)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
if today <= trial_end:
|
|
|
|
|
|
days_left = (trial_end - today).days
|
2026-05-04 21:34:19 +02:00
|
|
|
|
return True, (
|
|
|
|
|
|
f"Testphase: noch {days_left} Tag(e) verbleibend "
|
|
|
|
|
|
f"(bis {trial_end.strftime('%d.%m.%Y')})."
|
|
|
|
|
|
)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
if stored_key:
|
|
|
|
|
|
_, _, reason = validate_key(stored_key)
|
2026-05-04 21:34:19 +02:00
|
|
|
|
return False, (
|
|
|
|
|
|
f"Testphase abgelaufen.\n{reason}\n"
|
|
|
|
|
|
"Bitte neuen Aktivierungsschlüssel eingeben."
|
|
|
|
|
|
)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
2026-05-04 21:34:19 +02:00
|
|
|
|
return False, (
|
|
|
|
|
|
f"Die {APP_TRIAL_DAYS}-tägige Testphase ist abgelaufen.\n"
|
|
|
|
|
|
"Bitte Aktivierungsschlüssel eingeben, um fortzufahren."
|
|
|
|
|
|
)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-04 21:34:19 +02:00
|
|
|
|
# ── CLI-Hilfsmittel zum Generieren (für den Entwickler) ──────────────
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
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)
|
2026-05-04 21:34:19 +02:00
|
|
|
|
print("\nAktivierungsschlüssel generiert:")
|
2026-03-25 22:03:39 +01:00
|
|
|
|
print(f" Ablaufdatum: {exp}")
|
|
|
|
|
|
print(f" Schlüssel: {key}")
|
|
|
|
|
|
|
|
|
|
|
|
ok, dt, msg = validate_key(key)
|
|
|
|
|
|
print(f" Validierung: {'OK' if ok else 'FEHLER'} – {msg}")
|