diff --git a/admin/index.php b/admin/index.php index d167e3c..d28d569 100644 --- a/admin/index.php +++ b/admin/index.php @@ -1,6 +1,7 @@ syncVouchersToDatabase($db, $site['id']); diff --git a/admin/settings.php b/admin/settings.php index 9bc2255..296d30e 100644 --- a/admin/settings.php +++ b/admin/settings.php @@ -1,6 +1,7 @@ execute( "UPDATE sites SET name = ?, site_id = ?, unifi_controller_url = ?, unifi_username = ?, unifi_password = ?, public_access = ? WHERE id = ?", - [$name, $siteIdStr, $controllerUrl, $username, $password, $publicAccess, $siteId] + [$name, $siteIdStr, $controllerUrl, $username, Crypto::encrypt($password), $publicAccess, $siteId] ); } else { // Ohne Passwort-Änderung @@ -88,7 +89,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_site'])) { $db->execute( "INSERT INTO sites (name, site_id, unifi_controller_url, unifi_username, unifi_password, public_access) VALUES (?, ?, ?, ?, ?, ?)", - [$name, $siteId, $controllerUrl, $username, $password, $publicAccess] + [$name, $siteId, $controllerUrl, $username, Crypto::encrypt($password), $publicAccess] ); $success = 'Site erfolgreich hinzugefügt!'; diff --git a/admin/users.php b/admin/users.php index 84cb827..afb23db 100644 --- a/admin/users.php +++ b/admin/users.php @@ -1,6 +1,7 @@ syncVouchersToDatabase($db, $siteId); @@ -149,7 +150,7 @@ if (isset($_POST['ajax_delete']) && isset($_POST['voucher_id']) && isset($_POST[ $controller = new UniFiController( $site['unifi_controller_url'], $site['unifi_username'], - $site['unifi_password'], + Crypto::decrypt($site['unifi_password']), $site['site_id'] ); diff --git a/config.php b/config.php index 978061b..4195330 100644 --- a/config.php +++ b/config.php @@ -6,6 +6,11 @@ define('DB_NAME', ''); define('DB_USER', ''); define('DB_PASS', ''); +// Anwendungs-Schluessel fuer Verschluesselung-at-rest (UniFi-Passwoerter). +// Wird vom Installer automatisch mit einem zufaelligen Wert befuellt. +// Leer = keine Verschluesselung (Klartext, Legacy-Verhalten). +define('APP_KEY', ''); + // Sitzungs-Einstellungen define('SESSION_LIFETIME', 3600); // 1 Stunde diff --git a/cron_sync.php b/cron_sync.php index 91996e9..bd9ddc5 100644 --- a/cron_sync.php +++ b/cron_sync.php @@ -172,7 +172,7 @@ try { $controller = new UniFiController( $site['unifi_controller_url'], $site['unifi_username'], - $site['unifi_password'], + Crypto::decrypt($site['unifi_password']), $site['site_id'] ); diff --git a/includes/Auth.php b/includes/Auth.php index ac0198e..4bb0b79 100644 --- a/includes/Auth.php +++ b/includes/Auth.php @@ -159,7 +159,19 @@ class Auth { // Prüfen ob eingeloggt public function isLoggedIn() { - return isset($_SESSION['user_id']) && isset($_SESSION['login_time']); + if (!isset($_SESSION['user_id']) || !isset($_SESSION['login_time'])) { + return false; + } + + // Absolutes Session-Timeout durchsetzen (SESSION_LIFETIME aus config.php). + // Bisher wurde die Lebensdauer nie geprueft – Sessions liefen unbegrenzt. + $lifetime = defined('SESSION_LIFETIME') ? (int)SESSION_LIFETIME : 3600; + if ($lifetime > 0 && (time() - (int)$_SESSION['login_time']) > $lifetime) { + $this->logout(); + return false; + } + + return true; } // Prüfen ob Admin diff --git a/includes/Crypto.php b/includes/Crypto.php new file mode 100644 index 0000000..ae42b4b --- /dev/null +++ b/includes/Crypto.php @@ -0,0 +1,117 @@ + Klartext (Legacy-Verhalten) + } + + // Bevorzugt libsodium (PHP-Core seit 7.2), sonst OpenSSL. + if (function_exists('sodium_crypto_secretbox')) { + $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + $cipher = sodium_crypto_secretbox($plaintext, $nonce, $key); + return self::PREFIX . base64_encode($nonce . $cipher); + } + if (function_exists('openssl_encrypt')) { + $ivLen = openssl_cipher_iv_length('aes-256-gcm'); + $iv = random_bytes($ivLen); + $tag = ''; + $cipher = openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag); + if ($cipher === false) { + return $plaintext; + } + return self::PREFIX . base64_encode($iv . $tag . $cipher); + } + + // Keine Krypto-Funktion verfuegbar -> Klartext (besser als Datenverlust) + return $plaintext; + } + + /** + * Entschluesselt einen Wert. Nicht-verschluesselte Werte (Legacy/Klartext) + * werden unveraendert zurueckgegeben. + */ + public static function decrypt($value) { + if ($value === null || $value === '' || strpos($value, self::PREFIX) !== 0) { + return $value; // Klartext-Passthrough + } + $key = self::key(); + if ($key === null) { + return $value; + } + + $raw = base64_decode(substr($value, strlen(self::PREFIX)), true); + if ($raw === false) { + return $value; + } + + if (function_exists('sodium_crypto_secretbox_open')) { + $nonceLen = SODIUM_CRYPTO_SECRETBOX_NONCEBYTES; + if (strlen($raw) <= $nonceLen) { + return $value; + } + $nonce = substr($raw, 0, $nonceLen); + $cipher = substr($raw, $nonceLen); + $plain = sodium_crypto_secretbox_open($cipher, $nonce, $key); + return $plain === false ? $value : $plain; + } + if (function_exists('openssl_decrypt')) { + $ivLen = openssl_cipher_iv_length('aes-256-gcm'); + $tagLen = 16; + if (strlen($raw) <= $ivLen + $tagLen) { + return $value; + } + $iv = substr($raw, 0, $ivLen); + $tag = substr($raw, $ivLen, $tagLen); + $cipher = substr($raw, $ivLen + $tagLen); + $plain = openssl_decrypt($cipher, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag); + return $plain === false ? $value : $plain; + } + + return $value; + } + + /** Prueft, ob ein Wert bereits in unserem verschluesselten Format vorliegt. */ + public static function isEncrypted($value) { + return is_string($value) && strpos($value, self::PREFIX) === 0; + } +} diff --git a/includes/UniFiController.php b/includes/UniFiController.php index d0da04b..bac24ce 100644 --- a/includes/UniFiController.php +++ b/includes/UniFiController.php @@ -1,4 +1,6 @@ getVouchers(); - + if (empty($vouchers)) { throw new Exception("Voucher-Code konnte nicht abgerufen werden"); } - - // Neuesten Voucher zurückgeben - $latestVoucher = reset($vouchers); - + + $latestVoucher = null; + foreach ($vouchers as $voucher) { + // Nur Voucher mit passender Notiz beruecksichtigen + if (($voucher['note'] ?? null) !== $voucherName) { + continue; + } + if ($latestVoucher === null + || ($voucher['create_time'] ?? 0) > ($latestVoucher['create_time'] ?? 0)) { + $latestVoucher = $voucher; + } + } + + // Fallback: falls keine note-Uebereinstimmung (z.B. Sonderzeichen), + // den global neuesten Voucher nehmen. + if ($latestVoucher === null) { + foreach ($vouchers as $voucher) { + if ($latestVoucher === null + || ($voucher['create_time'] ?? 0) > ($latestVoucher['create_time'] ?? 0)) { + $latestVoucher = $voucher; + } + } + } + + if ($latestVoucher === null || empty($latestVoucher['code'])) { + throw new Exception("Voucher-Code konnte nicht abgerufen werden"); + } + return [ 'code' => $latestVoucher['code'], 'formatted_code' => $this->formatVoucherCode($latestVoucher['code']), diff --git a/index.php b/index.php index 79f1716..ad41e26 100644 --- a/index.php +++ b/index.php @@ -1,7 +1,8 @@ = $maxRequests) { + $_SESSION['voucher_create_times'] = $timestamps; + return true; + } + + $timestamps[] = $now; + $_SESSION['voucher_create_times'] = $timestamps; + return false; +} + // Settings laden $appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); $logoUrl = $db->getSetting('logo_url', ''); @@ -67,8 +94,14 @@ if ($auth->isLoggedIn()) { if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_voucher'])) { if (!$publicAccess && !$auth->isLoggedIn()) { $error = 'Sie müssen angemeldet sein'; - } elseif ($auth->isLoggedIn() && !$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { + } elseif (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { + // CSRF wird jetzt fuer ALLE geprueft – auch fuer anonyme oeffentliche + // Erstellung (Token wird per Session auch ohne Login vergeben). $error = 'Ungültiges Sicherheits-Token'; + } elseif (!$auth->isLoggedIn() && isVoucherRateLimited()) { + // Einfacher Session-basierter Throttle gegen Missbrauch/Spam im + // oeffentlichen Modus (kein Login = kein Benutzerkontext). + $error = 'Zu viele Anfragen. Bitte warten Sie einen Moment.'; } else { try { $siteId = (int)($_POST['site_id'] ?? 0); @@ -114,7 +147,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_voucher'])) { $controller = new UniFiController( $site['unifi_controller_url'], $site['unifi_username'], - $site['unifi_password'], + Crypto::decrypt($site['unifi_password']), $site['site_id'] ); @@ -558,9 +591,8 @@ $autoSelectSite = (count($sites) === 1) ? $sites[0]['id'] : 0; - isLoggedIn()): ?> - - + +
Client ID: = !empty($clientId) ? '✓ Gesetzt' : '✗ Fehlt' ?>
Client Secret: = !empty($clientSecret) ? '✓ Gesetzt' : '✗ Fehlt' ?>
-Tenant ID: = !empty($tenant \ No newline at end of file +
Tenant ID: = !empty($tenantId) ? '✓ Gesetzt' : '✗ Fehlt' ?>
+Diese URI muss exakt in der Azure-App-Registrierung hinterlegt sein:
+openid, profile, email, User.Read.