From 3d449a5672a58b03cc6e34d212a183f29d6ff436 Mon Sep 17 00:00:00 2001 From: Tom Hempel Date: Mon, 1 Jun 2026 15:02:20 +0200 Subject: [PATCH] enhanced debug --- cli/diagnose_db.php | 44 +++++++++++++++++++----- cli/diagnose_login.php | 77 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 cli/diagnose_login.php diff --git a/cli/diagnose_db.php b/cli/diagnose_db.php index c3f3ae2..347d08a 100644 --- a/cli/diagnose_db.php +++ b/cli/diagnose_db.php @@ -1,7 +1,7 @@ 0 ? 'yes (' . strlen($b64) . ' bytes)' : 'no (uses first 32 ASCII chars)') . "\n"; -$dbPath = QDB_PATH; +$dbPath = $argv[1] ?? QDB_PATH; +if ($dbPath !== QDB_PATH && !is_file($dbPath)) { + echo "File not found: $dbPath\n"; + exit(1); +} +if ($dbPath !== QDB_PATH) { + echo "Note: probing alternate file (app still uses " . QDB_PATH . " at runtime)\n"; +} $uploads = dirname($dbPath); echo "uploads dir: $uploads\n"; echo " exists: " . (is_dir($uploads) ? 'yes' : 'no') . "\n"; @@ -83,34 +90,55 @@ if ($b64 !== false && strlen($b64) > 0) { } echo "\nDecrypt probe (which key matches questionnaire_database?):\n"; -$matched = null; +$matchedLabel = null; +$matchedKey = null; +$appKey = get_master_key_bytes(); foreach ($candidates as $label => $keyBytes) { $probe = qdb_probe_decrypt($dbPath, $keyBytes); if ($probe['ok']) { echo " MATCH: $label\n"; - $matched = $label; + if ($matchedLabel === null) { + $matchedLabel = $label; + $matchedKey = $keyBytes; + } } else { echo " no: $label\n"; } } -if ($matched === null) { +if ($matchedLabel === null) { echo "\nNo known key opened the file. Likely causes:\n"; echo " - DB was encrypted with a different QDB_MASTER_KEY than in .env now\n"; echo " - File was corrupted (e.g. interrupted save); try an older uploads/backups copy\n"; echo " - Wrong DB path (another install under /var/www/...)\n"; + echo "\nProbe other copies, e.g.:\n"; + echo " php cli/diagnose_db.php /var/www/html/qdb/uploads/questionnaire_database\n"; echo "\nWiping is NOT required for schema updates. Only wipe if you accept data loss\n"; echo "and run: mv uploads/questionnaire_database uploads/questionnaire_database.bak\n"; echo "then: php seed_admin.php \n"; exit(1); } -if ($matched !== 'current .env (get_master_key_bytes)') { - echo "\nThe DB does NOT match your current .env key but matches: $matched\n"; - echo "Fix: put the matching key in .env, open the DB once, then re-save — or restore a backup.\n"; +$dbMatchesAppKey = $matchedKey !== null && hash_equals($matchedKey, $appKey); + +if ($dbPath !== QDB_PATH && $dbMatchesAppKey) { + echo "\nThis file decrypts with your .env. Restore it into the app uploads dir:\n"; + echo " cp " . escapeshellarg($dbPath) . ' ' . escapeshellarg(QDB_PATH) . "\n"; + echo " chown www-data:www-data " . escapeshellarg(QDB_PATH) . "\n"; + exit(0); +} + +if (!$dbMatchesAppKey) { + echo "\nThe DB does NOT match get_master_key_bytes() but matches: $matchedLabel\n"; + echo "Fix: align QDB_MASTER_KEY in .env and php-fpm pool env, then re-run diagnose.\n"; exit(1); } +if ($dbPath !== QDB_PATH) { + echo "\nDecrypt OK for alternate path. Run without arguments after copying into uploads/.\n"; + exit(0); +} + try { [$pdo, $tmpDb, $lockFp] = qdb_open(false); $version = (int)$pdo->query('PRAGMA user_version')->fetchColumn(); diff --git a/cli/diagnose_login.php b/cli/diagnose_login.php new file mode 100644 index 0000000..fae7372 --- /dev/null +++ b/cli/diagnose_login.php @@ -0,0 +1,77 @@ + + */ +if (php_sapi_name() !== 'cli') { + exit(1); +} + +require_once __DIR__ . '/../common.php'; +require_once __DIR__ . '/../db_init.php'; + +$username = $argv[1] ?? ''; +$password = $argv[2] ?? ''; +if ($username === '' || $password === '') { + fwrite(STDERR, "Usage: php cli/diagnose_login.php \n"); + fwrite(STDERR, "Run as www-data to match php-fpm: sudo -u www-data php cli/diagnose_login.php ...\n"); + exit(1); +} + +echo 'User: ' . (function_exists('posix_getpwuid') ? (posix_getpwuid(posix_geteuid())['name'] ?? '?') : '?') . "\n"; +echo 'QDB_MASTER_KEY env length: ' . strlen(getenv('QDB_MASTER_KEY') ?: '') . "\n"; +echo 'Derived key sha256: ' . hash('sha256', get_master_key_bytes()) . "\n\n"; + +try { + echo "1) qdb_open(read-only)... "; + [$pdo, $tmpDb, $lockFp] = qdb_open(false); + echo "OK\n"; + + echo "2) lookup user... "; + $stmt = $pdo->prepare( + "SELECT userID, passwordHash, role, entityID, mustChangePassword FROM users WHERE username = :u" + ); + $stmt->execute([':u' => $username]); + $user = $stmt->fetch(PDO::FETCH_ASSOC); + if (!$user) { + qdb_discard($tmpDb, $lockFp); + echo "FAIL — user not found\n"; + exit(1); + } + echo "OK (role={$user['role']}, mustChange={$user['mustChangePassword']})\n"; + + echo "3) password_verify... "; + if (!password_verify($password, $user['passwordHash'])) { + qdb_discard($tmpDb, $lockFp); + echo "FAIL — wrong password\n"; + exit(1); + } + echo "OK\n"; + + qdb_discard($tmpDb, $lockFp); + + echo "4) token_add (writable save, same as login)... "; + $token = bin2hex(random_bytes(32)); + token_add($token, 120, [ + 'role' => $user['role'], + 'entityID' => $user['entityID'], + 'userID' => $user['userID'], + 'temp' => (int)$user['mustChangePassword'] === 1, + ]); + echo "OK\n"; + + echo "5) token_get_record... "; + $rec = token_get_record($token); + if (!$rec) { + echo "FAIL — session not readable after save\n"; + exit(1); + } + echo "OK\n"; + + token_revoke($token); + echo "\nAll login steps succeeded. If the website still fails, check nginx routing (curl below).\n"; +} catch (Throwable $e) { + echo "FAIL\n " . $e->getMessage() . "\n"; + echo " " . $e->getFile() . ':' . $e->getLine() . "\n"; + exit(1); +}