'287082', 1111111109 => '081804', 1111111111 => '050471', 1234567890 => '005924', 2000000000 => '279037', ]; foreach ($vectors as $timestamp => $expected) { totp_check_assert( "RFC-6238-Testvektor bei t={$timestamp}", app_totp_code($rfcSecret, intdiv($timestamp, APP_TOTP_PERIOD)) === $expected, $failures, $passes ); } // --- Base32 hin und zurueck --------------------------------------------- $binary = random_bytes(20); totp_check_assert( 'Base32 kodiert und dekodiert verlustfrei', app_totp_base32_decode(app_totp_base32_encode($binary)) === $binary, $failures, $passes ); totp_check_assert( 'Von Hand abgetipptes Geheimnis mit Leerzeichen wird verstanden', app_totp_base32_decode(trim(chunk_split(app_totp_base32_encode($binary), 4, ' '))) === $binary, $failures, $passes ); // --- Toleranzfenster ----------------------------------------------------- $secret = app_totp_generate_secret(); $now = time(); $step = intdiv($now, APP_TOTP_PERIOD); totp_check_assert( 'Aktueller Code wird angenommen', app_totp_verify_code($secret, app_totp_code($secret, $step), $now) === $step, $failures, $passes ); totp_check_assert( 'Code des vorigen Zeitschritts wird noch angenommen', app_totp_verify_code($secret, app_totp_code($secret, $step - 1), $now) === $step - 1, $failures, $passes ); totp_check_assert( 'Zu alter Code wird abgelehnt', app_totp_verify_code($secret, app_totp_code($secret, $step - 5), $now) === null, $failures, $passes ); totp_check_assert( 'Falscher Code wird abgelehnt', app_totp_verify_code($secret, '000000', $now) === null || app_totp_code($secret, $step) === '000000', $failures, $passes ); totp_check_assert( 'Nicht-numerische Eingabe wird abgelehnt', app_totp_verify_code($secret, 'abcdef', $now) === null, $failures, $passes ); // --- QR-Code und URI ----------------------------------------------------- $uri = app_totp_provisioning_uri($secret, 'test@example.com'); totp_check_assert( 'Provisioning-URI traegt Geheimnis und Aussteller', str_starts_with($uri, 'otpauth://totp/') && str_contains($uri, 'secret=' . $secret) && str_contains($uri, 'issuer=Kaffeeliste'), $failures, $passes ); $svg = app_totp_qr_svg($uri); totp_check_assert( 'QR-Code entsteht als einbettbares SVG', str_starts_with($svg, ' "TOTP Check {$suffix}", 'tenant_slug' => $slug, 'display_name' => 'TOTP Checker', 'email' => $email, 'password' => 'geheim-genug-123', 'password_confirm' => 'geheim-genug-123', 'customer_type' => 'business', 'accept_terms' => true, 'acknowledge_privacy' => true, 'accept_dpa' => true, ]); if (empty($registration['ok'])) { fwrite(STDERR, "Setup fehlgeschlagen: " . implode('; ', $registration['errors'] ?? []) . "\n"); exit(1); } $userId = (int)$registration['identity']['user_id']; $tenantId = (int)$registration['identity']['tenant_id']; try { totp_check_assert('Frisches Konto hat keinen zweiten Faktor', !app_totp_is_active($pdo, $userId), $failures, $passes); $setupSecret = app_totp_begin_setup($pdo, $userId); totp_check_assert('Einrichtung liefert ein Geheimnis', is_string($setupSecret) && $setupSecret !== '', $failures, $passes); totp_check_assert( 'Angefangene Einrichtung ist noch nicht scharf', !app_totp_is_active($pdo, $userId), $failures, $passes ); totp_check_assert( 'Bestaetigung mit falschem Code schlaegt fehl', app_totp_confirm_setup($pdo, $userId, '000000') === null || app_totp_code((string)$setupSecret, intdiv(time(), APP_TOTP_PERIOD)) === '000000', $failures, $passes ); $currentStep = intdiv(time(), APP_TOTP_PERIOD); $recoveryCodes = app_totp_confirm_setup($pdo, $userId, app_totp_code((string)$setupSecret, $currentStep)); totp_check_assert( 'Bestaetigung mit richtigem Code liefert Wiederherstellungscodes', is_array($recoveryCodes) && count($recoveryCodes) === APP_TOTP_RECOVERY_CODE_COUNT, $failures, $passes ); totp_check_assert('Zweiter Faktor ist jetzt aktiv', app_totp_is_active($pdo, $userId), $failures, $passes); // Der bei der Einrichtung benutzte Zeitschritt darf nicht noch einmal gehen. totp_check_assert( 'Bereits eingeloester Code wird nicht wiederverwendet', !app_totp_verify_for_user($pdo, $userId, app_totp_code((string)$setupSecret, $currentStep)), $failures, $passes ); totp_check_assert( 'Naechster Code wird angenommen', app_totp_verify_for_user($pdo, $userId, app_totp_code((string)$setupSecret, $currentStep + 1)), $failures, $passes ); // --- Wiederherstellungscodes --- $codes = (array)$recoveryCodes; totp_check_assert( 'Alle Wiederherstellungscodes sind zunaechst unbenutzt', app_totp_unused_recovery_code_count($pdo, $userId) === APP_TOTP_RECOVERY_CODE_COUNT, $failures, $passes ); totp_check_assert( 'Wiederherstellungscode wird angenommen', app_totp_consume_recovery_code($pdo, $userId, (string)$codes[0]), $failures, $passes ); totp_check_assert( 'Derselbe Wiederherstellungscode gilt kein zweites Mal', !app_totp_consume_recovery_code($pdo, $userId, (string)$codes[0]), $failures, $passes ); totp_check_assert( 'Wiederherstellungscode wird auch mit Grossbuchstaben und ohne Bindestrich erkannt', app_totp_consume_recovery_code($pdo, $userId, strtoupper(str_replace('-', '', (string)$codes[1]))), $failures, $passes ); totp_check_assert( 'Erfundener Wiederherstellungscode wird abgelehnt', !app_totp_consume_recovery_code($pdo, $userId, 'zzzz-zzzz'), $failures, $passes ); totp_check_assert( 'Verbrauchte Codes sind abgezogen', app_totp_unused_recovery_code_count($pdo, $userId) === APP_TOTP_RECOVERY_CODE_COUNT - 2, $failures, $passes ); // --- Passwortbestaetigung und Abschalten --- totp_check_assert( 'Richtiges Passwort wird bestaetigt', saas_password_matches($pdo, $userId, 'geheim-genug-123'), $failures, $passes ); totp_check_assert( 'Falsches Passwort wird abgelehnt', !saas_password_matches($pdo, $userId, 'falsch'), $failures, $passes ); app_totp_disable($pdo, $userId); totp_check_assert('Abschalten deaktiviert den zweiten Faktor', !app_totp_is_active($pdo, $userId), $failures, $passes); totp_check_assert( 'Abschalten entfernt die Wiederherstellungscodes', app_totp_unused_recovery_code_count($pdo, $userId) === 0, $failures, $passes ); } finally { $pdo->prepare('DELETE FROM totp_recovery_codes WHERE user_id = ?')->execute([$userId]); $pdo->prepare('DELETE FROM legal_acceptances WHERE tenant_id = ?')->execute([$tenantId]); $pdo->prepare('DELETE FROM participants WHERE tenant_id = ?')->execute([$tenantId]); $pdo->prepare('DELETE FROM tenant_memberships WHERE tenant_id = ?')->execute([$tenantId]); $pdo->prepare('DELETE FROM tenant_settings WHERE tenant_id = ?')->execute([$tenantId]); $pdo->prepare('DELETE FROM tenants WHERE id = ?')->execute([$tenantId]); $pdo->prepare('DELETE FROM users WHERE id = ?')->execute([$userId]); } echo "\n"; if ($failures !== []) { fwrite(STDERR, 'TOTP-Check fehlgeschlagen: ' . implode('; ', $failures) . "\n"); exit(1); } echo "TOTP-Check bestanden mit {$passes} Zusicherungen.\n";