false, 'error' => 'file missing']; } $storedEnc = @file_get_contents($dbPath); if ($storedEnc === false || strlen($storedEnc) < 17) { return ['ok' => false, 'error' => 'unreadable or too short']; } try { aes256_cbc_decrypt_bytes($storedEnc, $keyBytes); return ['ok' => true]; } catch (Throwable $e) { return ['ok' => false, 'error' => $e->getMessage()]; } } $envPath = __DIR__ . '/../.env'; echo "Project: " . realpath(__DIR__ . '/..') . "\n"; echo ".env: " . ($envPath && is_readable($envPath) ? "readable" : "MISSING or not readable") . "\n"; $keyEnv = getenv('QDB_MASTER_KEY'); if ($keyEnv === false || $keyEnv === '') { echo "QDB_MASTER_KEY: NOT SET (set in .env or Apache/PHP-FPM env)\n"; if (!qdb_is_mysql()) { exit(1); } } else { echo "QDB_MASTER_KEY: set (" . strlen($keyEnv) . " chars)\n"; $b64 = base64_decode($keyEnv, true); echo " parses as base64: " . ($b64 !== false && strlen($b64) > 0 ? 'yes (' . strlen($b64) . ' bytes)' : 'no (uses first 32 ASCII chars)') . "\n"; } if (qdb_is_mysql()) { echo "QDB_DRIVER: mysql\n"; echo " host: " . (qdb_env_get('QDB_MYSQL_HOST') ?: '127.0.0.1') . "\n"; echo " database: " . (qdb_env_get('QDB_MYSQL_DATABASE') ?: 'nat-as-db') . "\n"; try { [$pdo] = qdb_mysql_open(); qdb_mysql_ensure_ready($pdo); $version = qdb_schema_version($pdo); $users = (int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn(); $hasSession = qdb_table_exists($pdo, 'session'); echo "\nOK: MySQL connection + migrations succeeded.\n"; echo " schema_version: $version (expected " . QDB_VERSION . ")\n"; echo " users: $users\n"; echo " session table: " . ($hasSession ? 'yes' : 'MISSING') . "\n"; exit(0); } catch (Throwable $e) { echo "\nMySQL open failed: " . $e->getMessage() . "\n"; exit(1); } } echo "QDB_DRIVER: sqlite\n"; $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"; echo " writable: " . (is_writable($uploads) ? 'yes' : 'NO — migrations cannot save') . "\n"; echo "DB file: $dbPath\n"; if (is_file($dbPath)) { $mtime = filemtime($dbPath); echo " exists: yes (" . filesize($dbPath) . " bytes)\n"; echo " modified: " . ($mtime ? date('Y-m-d H:i:s T', $mtime) : '?') . "\n"; } else { echo " exists: no (will create on first write)\n"; } $backupDir = $uploads . '/backups'; if (is_dir($backupDir)) { $backups = glob($backupDir . '/questionnaire_database.*') ?: []; rsort($backups); echo "Backups in uploads/backups: " . count($backups) . "\n"; foreach (array_slice($backups, 0, 5) as $bp) { $mt = filemtime($bp); echo " - " . basename($bp) . ' (' . filesize($bp) . " bytes, " . ($mt ? date('Y-m-d H:i:s', $mt) : '?') . ")\n"; } } else { echo "Backups: none (uploads/backups missing)\n"; } if (!is_file($dbPath)) { exit(0); } $candidates = [ 'current .env (get_master_key_bytes)' => get_master_key_bytes(), 'legacy fallback (pre-April 2026 default)' => str_pad('12345678901234567890123456789012', 32, "\0"), 'raw first 32 chars of QDB_MASTER_KEY' => str_pad(substr($keyEnv, 0, 32), 32, "\0"), ]; if ($b64 !== false && strlen($b64) > 0) { $candidates['base64-decoded .env only'] = str_pad(substr($b64, 0, 32), 32, "\0"); } echo "\nDecrypt probe (which key matches questionnaire_database?):\n"; $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"; if ($matchedLabel === null) { $matchedLabel = $label; $matchedKey = $keyBytes; } } else { echo " no: $label\n"; } } 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); } $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(); $users = (int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn(); $hasSession = qdb_table_exists($pdo, 'session'); qdb_discard($tmpDb, $lockFp); echo "\nOK: full open + migrations succeeded.\n"; echo " PRAGMA user_version: $version (expected " . QDB_VERSION . ")\n"; echo " users: $users\n"; echo " session table: " . ($hasSession ? 'yes' : 'MISSING') . "\n"; exit(0); } catch (Throwable $e) { echo "\nDecrypt OK but qdb_open failed: " . $e->getMessage() . "\n"; exit(1); }