diff --git a/.gitignore b/.gitignore index 6acae790..bce53ca6 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,7 @@ GoogleService-Info.plist # --- Audit/Scan Output --- semgrep_*.json nuclei_results.txt + +# سجل لوحة المفاتيح — محلي للسيرفر، لا يُدفع +backend/tools/panel/panel_audit.log +*.env.bak.* diff --git a/backend/tools/env_keys.php b/backend/tools/env_keys.php index fff7a620..953c83fc 100644 --- a/backend/tools/env_keys.php +++ b/backend/tools/env_keys.php @@ -23,82 +23,10 @@ $envFile = getenv('ENV_FILE_PATH') ?: "$root/docker/.env"; $command = $argv[1] ?? 'audit'; $dryRun = in_array('--dry-run', $argv, true); -// ═══════════════════════════════════════════════════════════════ -// التصنيف -// ═══════════════════════════════════════════════════════════════ - -/** أسرار داخلية: طرفاها كلاهما لنا. توليدها وتدويرها آمنان. */ -const GENERATED = [ - 'S2S_SHARED_KEY' => 32, // خادم ↔ خادم داخلي - 'WEBHOOK_SECRET_KEY' => 32, // هاتف الرسائل ↔ الويبهوك - 'INTERNAL_SOCKET_KEY' => 32, // الباك إند ↔ خوادم السوكيت - 'BOT_SECRET_KEY' => 24, // بوت الأسعار - 'FCM_INTERNAL_API_KEY' => 32, - 'MIGRATION_ADMIN_KEY' => 24, - 'PAYMENT_KEY' => 32, -]; - -/** - * تدويرها يبطل كل الجلسات القائمة: كل سائق وكل راكب سيُخرَج ويُطالَب - * بتسجيل دخول جديد. ليس عطلاً لكنه حدث تشغيلي — لا يُفعل وقت الذروة. - */ -const SESSION_SCOPED = [ - 'JWT_SECRET_KEY' => 64, - 'SECRET_KEY_HMAC' => 32, -]; - -/** - * ⛔ مجمّدة. هذه ليست «مفاتيح يمكن تغييرها من عندنا» رغم أنها ملكنا: - * - * ENC_KEY / ENCRYPTION_KEY_PATH - * تُشفَّر به أسماء الركّاب وهواتفهم وبرديهم ولوحات السيارات - * **المخزَّنة في القاعدة**. تغييره لا يعيد تشفير شيء — يجعل كل - * صف قديم رطانةً لا تُفكّ. لا استرجاع، ولا نسخة احتياطية تنفع - * لأن النسخة نفسها مشفّرة بالمفتاح القديم. - * - * BLIND_INDEX_PEPPER - * أعمدة `*_bidx` محسوبة به ومخزَّنة. تغييره يعني أن كل بحث - * بالهاتف أو البريد يفشل — لا يجد المستخدم حسابه، ويُنشأ له - * حساب مكرر عند أول تسجيل. - * - * TRACKING_SECRET_SALT / FP_PEPPER - * قيم مشتقّة منها مخزَّنة أيضاً. - * - * تدويرها ممكن نظرياً لكنه **ترحيل بيانات** لا تغيير إعداد: فكّ بالقديم - * وإعادة تشفير بالجديد على كل صف، بتوقّف مخطَّط ونسخة احتياطية أولاً. - */ -const FROZEN = [ - 'ENC_KEY', 'ENCRYPTION_KEY_PATH', 'ENCRYPTION_MODE', - 'BLIND_INDEX_PEPPER', 'TRACKING_SECRET_SALT', 'FP_PEPPER', -]; - -/** لا تُولَّد — تُنسخ من لوحة المزوّد. */ -const EXTERNAL = [ - 'GEMINI_API_KEY', 'NABEH_API_KEY', 'NABEH_PASSWORD', 'NABEH_OTP_APP_KEY', - 'ELEVENLABS_API_KEY', 'CREATOMATE_API_KEY', 'SOCIAL_BOT_TOKEN', - 'MAIL_PASSWORD', 'SIRO_SMTP_PASSWORD', 'SMS_PASSWORD_EGYPT', - 'VOICE_CALL_API_KEY', 'RASEEL_DRIVER_ACCESS_TOKEN', - 'FIREBASE_SERVICE_ACCOUNT_PATH', 'SERVICE_ACCOUNT_FILE_PATH', -]; - -/** - * كلمات مرور قواعد البيانات — صنف قائم بذاته. - * - * تغييرها في .env **لا يغيّر شيئاً في MySQL**. متغيّرات صورة mysql - * (ومنها MYSQL_ROOT_PASSWORD) تُقرأ مرة واحدة فقط عند تهيئة مجلّد - * البيانات الفارغ أول مرة. بعد ذلك كلمة المرور تعيش داخل جدول - * mysql.user لا في .env — فتعديل .env وحده يعني تطبيقاً يحمل كلمة - * لا يقبلها الخادم: انقطاع كامل بلا أي تغيير في القاعدة نفسها. - * - * الترتيب الصحيح: ALTER USER في MySQL أولاً، ثم .env، ثم إعادة تشغيل - * حاويات php (لا mysql، ولا إعادة بناء، ولا مساس بالبيانات إطلاقاً). - */ -const DB_CREDENTIALS = [ - 'DB_PRIMARY_PASS_V2', 'DB_PRIMARY_PASS', 'DB_TRACKING_PASS', - 'DB_RIDE_PASS', 'DB_TRANSIT_PASS', 'DB_FOOD_PASS', 'DB_PAYMENT_PASS', - 'MYSQL_ROOT_PASSWORD', 'REDIS_PASSWORD', 'REDIS_MAIN_PASSWORD', - 'REDIS_LOCATION_PASSWORD', 'REDIS_AUTH', 'REDIS_MAIN_AUTH', -]; +// التصنيف كله في env_registry.php — مصدر واحد تشترك فيه هذه الأداة +// واللوحة. نسختان منه كانتا ستتباعدان، وأخطر تباعد هو قائمة المجمّدة: +// مفتاح يسقط منها في نسخة يعني لوحةً تعرضه قابلاً للتعديل. +require_once __DIR__ . '/env_registry.php'; // ═══════════════════════════════════════════════════════════════ // القراءة @@ -148,11 +76,11 @@ $report = static function (string $title, array $keys, array $present, } }; -$report('أسرار داخلية (تُولَّد وتُدوَّر بحرية)', GENERATED, $present, $isPlaceholder, $toSeed, true); -$report('مرتبطة بالجلسات (تدويرها يُخرج الجميع)', SESSION_SCOPED, $present, $isPlaceholder, $toSeed, true); -$report('⛔ مجمّدة — لا تُدوَّر (بيانات مخزَّنة مشتقّة منها)', FROZEN, $present, $isPlaceholder); -$report('اعتماد قواعد البيانات (يُغيَّر في MySQL أولاً لا هنا)', DB_CREDENTIALS, $present, $isPlaceholder); -$report('طرف ثالث (تُنسخ من لوحة المزوّد)', EXTERNAL, $present, $isPlaceholder); +$report('أسرار داخلية (تُولَّد وتُدوَّر بحرية)', ENV_GENERATED, $present, $isPlaceholder, $toSeed, true); +$report('مرتبطة بالجلسات (تدويرها يُخرج الجميع)', ENV_SESSION_SCOPED, $present, $isPlaceholder, $toSeed, true); +$report('⛔ مجمّدة — لا تُدوَّر (بيانات مخزَّنة مشتقّة منها)', ENV_FROZEN, $present, $isPlaceholder); +$report('اعتماد قواعد البيانات (يُغيَّر في MySQL أولاً لا هنا)', ENV_DB_CREDENTIALS, $present, $isPlaceholder); +$report('طرف ثالث (تُنسخ من لوحة المزوّد)', ENV_EXTERNAL, $present, $isPlaceholder); echo "\n" . str_repeat('─', 62) . "\n"; diff --git a/backend/tools/env_registry.php b/backend/tools/env_registry.php new file mode 100644 index 00000000..6b68b7a0 --- /dev/null +++ b/backend/tools/env_registry.php @@ -0,0 +1,142 @@ + 32, + 'WEBHOOK_SECRET_KEY' => 32, + 'INTERNAL_SOCKET_KEY' => 32, + 'BOT_SECRET_KEY' => 24, + 'FCM_INTERNAL_API_KEY' => 32, + 'MIGRATION_ADMIN_KEY' => 24, + 'PAYMENT_KEY' => 32, +]; + +/** تدويرها يبطل كل الجلسات القائمة — حدث تشغيلي واعٍ لا روتين. */ +const ENV_SESSION_SCOPED = [ + 'JWT_SECRET_KEY' => 64, + 'SECRET_KEY_HMAC' => 32, +]; + +/** + * ⛔ مجمّدة. بيانات مخزَّنة في القاعدة مشتقّة منها: + * ENC_KEY — أسماء الركّاب وهواتفهم ولوحات السيارات مشفّرة به + * BLIND_INDEX_PEPPER — أعمدة *_bidx محسوبة به ومخزَّنة + * TRACKING_SECRET_SALT / FP_PEPPER — قيم مشتقّة مخزَّنة + * + * تغييرها لا يعيد تشفير شيء: يجعل الصفوف القديمة غير قابلة للقراءة + * إلى الأبد، ولا نسخة احتياطية تنفع لأنها مشفّرة بالمفتاح القديم. + * تدويرها ترحيلُ بيانات لا تغييرُ إعداد. + * + * اللوحة تعرض حالتها فقط — لا قيمتها ولا حقل تعديل. + */ +const ENV_FROZEN = [ + 'ENC_KEY', 'ENCRYPTION_KEY_PATH', 'ENCRYPTION_MODE', + 'BLIND_INDEX_PEPPER', 'TRACKING_SECRET_SALT', 'FP_PEPPER', +]; + +/** لا تُولَّد — تُنسخ من لوحة المزوّد. قابلة للتحرير يدوياً. */ +const ENV_EXTERNAL = [ + 'GEMINI_API_KEY', 'NABEH_API_KEY', 'NABEH_PASSWORD', 'NABEH_OTP_APP_KEY', + 'ELEVENLABS_API_KEY', 'CREATOMATE_API_KEY', 'SOCIAL_BOT_TOKEN', + 'MAIL_PASSWORD', 'SIRO_SMTP_PASSWORD', 'SMS_PASSWORD_EGYPT', + 'VOICE_CALL_API_KEY', 'RASEEL_DRIVER_ACCESS_TOKEN', +]; + +/** + * كلمات مرور القواعد ← اسم المستخدم ومتغيّر اسم القاعدة. + * + * تغييرها في .env وحده لا يفعل شيئاً: متغيّرات صورة mysql تُقرأ مرة + * واحدة عند تهيئة مجلّد بيانات فارغ، وبعدها كلمة المرور تعيش في + * mysql.user. الترتيب الصحيح ALTER USER ثم .env — واللوحة تفعله + * بهذا الترتيب وتتحقّق قبل أن تكتب. + */ +const ENV_DB_CREDENTIALS = [ + 'DB_PRIMARY_PASS_V2' => ['user' => 'DB_PRIMARY_USER_V2', 'db' => 'DB_PRIMARY_NAME_V2'], + 'DB_PRIMARY_PASS' => ['user' => 'DB_PRIMARY_USER', 'db' => 'DB_PRIMARY_NAME'], + 'DB_TRACKING_PASS' => ['user' => 'DB_TRACKING_USER', 'db' => 'DB_TRACKING_NAME'], + 'DB_RIDE_PASS' => ['user' => 'DB_RIDE_USER', 'db' => 'DB_RIDE_NAME'], + 'DB_TRANSIT_PASS' => ['user' => 'DB_TRANSIT_USER', 'db' => 'DB_TRANSIT_NAME'], + 'DB_FOOD_PASS' => ['user' => 'DB_FOOD_USER', 'db' => 'DB_FOOD_NAME'], +]; + +/** يقرأ .env إلى مصفوفة. */ +function envRead(string $file): array +{ + if (!is_readable($file)) return []; + $out = []; + foreach (file($file, FILE_IGNORE_NEW_LINES) as $line) { + if (preg_match('/^\s*([A-Z0-9_]+)\s*=\s*(.*)$/', $line, $m)) { + $out[$m[1]] = trim($m[2], "\"' "); + } + } + return $out; +} + +/** قيمة نائبة: موجودة شكلاً وغائبة فعلاً. */ +function envIsPlaceholder(?string $v): bool +{ + if ($v === null || $v === '') return true; + return (bool) preg_match('/^( $value) { + $line = "$name=$value"; + if (preg_match('/^\s*' . preg_quote($name, '/') . '\s*=/m', $out)) { + $out = preg_replace( + '/^\s*' . preg_quote($name, '/') . '\s*=.*$/m', + str_replace('$', '\\$', $line), + $out, + 1 + ); + } else { + $append[] = $line; + } + } + + if ($append) { + $out .= "\n# ── " . date('Y-m-d H:i') . " ──\n" . implode("\n", $append) . "\n"; + } + + if (file_put_contents($file, $out) === false) return null; + @chmod($file, 0600); + + return $backup; +} + +/** سرّ عشوائي بصيغة hex. */ +function envGenerate(int $bytes = 32): string +{ + return bin2hex(random_bytes($bytes)); +} diff --git a/backend/tools/panel/index.php b/backend/tools/panel/index.php new file mode 100644 index 00000000..b10a6099 --- /dev/null +++ b/backend/tools/panel/index.php @@ -0,0 +1,526 @@ + true, + 'cookie_samesite' => 'Strict', + 'use_strict_mode' => true, +]); + +/** يسجّل حدثاً. القيم لا تُسجَّل أبداً — أسماء المفاتيح فقط. */ +function auditLog(string $file, string $event, string $detail = ''): void +{ + $line = sprintf("[%s] %s | %s | %s\n", + date('Y-m-d H:i:s'), $_SERVER['REMOTE_ADDR'] ?? '?', $event, $detail); + @file_put_contents($file, $line, FILE_APPEND); + @chmod($file, 0600); +} + +// ═══════════════════════════════════════════════════════════════ +// القيد ٢ — عبارة المرور +// ═══════════════════════════════════════════════════════════════ +$env = envRead($envFile); +$hash = $env['ENV_PANEL_PASSPHRASE'] ?? ''; + +if ($hash === '' || !str_starts_with($hash, '$2y$')) { + http_response_code(503); + echo '
'; + echo "اللوحة معطّلة: لم تُضبط ENV_PANEL_PASSPHRASE.\n\n"; + echo "ولّد تجزئة وضعها في docker/.env:\n\n"; + echo " php -r 'echo password_hash(\"عبارتك\", PASSWORD_BCRYPT), PHP_EOL;'\n\n"; + echo "ثم: ENV_PANEL_PASSPHRASE='\$2y\$...'\n\n"; + echo "اجعلها عبارة لا تشبه أي كلمة مرور أخرى عندك.\n"; + echo ''; + exit; +} + +$error = ''; + +if (($_POST['action'] ?? '') === 'login') { + // تأخير ثابت: يجعل تخمين العبارة بطيئاً بما يكفي، وbcrypt نفسه + // يتكفّل بالباقي. + usleep(400_000); + if (password_verify((string) ($_POST['passphrase'] ?? ''), $hash)) { + session_regenerate_id(true); + $_SESSION['ok'] = true; + $_SESSION['csrf'] = bin2hex(random_bytes(16)); + auditLog($logFile, 'LOGIN_OK'); + } else { + $error = 'عبارة غير صحيحة.'; + auditLog($logFile, 'LOGIN_FAIL'); + } +} + +if (($_GET['logout'] ?? '') === '1') { + session_destroy(); + header('Location: ?'); + exit; +} + +$authed = !empty($_SESSION['ok']); + +// ═══════════════════════════════════════════════════════════════ +// العمليات +// ═══════════════════════════════════════════════════════════════ +$notice = ''; + +if ($authed && ($_POST['action'] ?? '') !== '' && ($_POST['action'] ?? '') !== 'login') { + if (!hash_equals((string) ($_SESSION['csrf'] ?? ''), (string) ($_POST['csrf'] ?? ''))) { + http_response_code(400); + exit('CSRF mismatch'); + } + + $key = (string) ($_POST['key'] ?? ''); + $action = (string) $_POST['action']; + + // ── الحارس الجامع ── + // قبل أي فرع: المجمّدة ممنوعة. الفحص هنا لا في كل فرع، فلا يمكن + // لفرع جديد يُضاف لاحقاً أن ينساه. + if (in_array($key, ENV_FROZEN, true)) { + http_response_code(403); + auditLog($logFile, 'BLOCKED_FROZEN', $key); + exit('هذا المفتاح مجمّد ولا يمكن تعديله من اللوحة.'); + } + + $rotatable = ENV_GENERATED + ENV_SESSION_SCOPED; + + if ($action === 'rotate' && isset($rotatable[$key])) { + $backup = envWrite($envFile, [$key => envGenerate($rotatable[$key])]); + if ($backup) { + auditLog($logFile, 'ROTATE', $key); + $notice = "دُوِّر $key. نسخة احتياطية: " . basename($backup) + . ' — أعد تشغيل حاويات php.'; + } else { + $error = 'تعذّرت الكتابة.'; + } + + } elseif ($action === 'set' && in_array($key, ENV_EXTERNAL, true)) { + $value = trim((string) ($_POST['value'] ?? '')); + if ($value === '') { + $error = 'القيمة فارغة.'; + } else { + $backup = envWrite($envFile, [$key => $value]); + if ($backup) { + auditLog($logFile, 'SET_EXTERNAL', $key); + $notice = "حُدِّث $key."; + } else { + $error = 'تعذّرت الكتابة.'; + } + } + + } elseif ($action === 'rotate_db' && isset(ENV_DB_CREDENTIALS[$key])) { + [$notice, $error] = rotateDbPassword($envFile, $logFile, $env, $key); + + } else { + $error = 'عملية غير معروفة أو مفتاح غير قابل للتعديل.'; + } + + $env = envRead($envFile); // إعادة القراءة بعد أي تغيير +} + +/** + * يدوّر كلمة مرور مستخدم قاعدة بيانات بالترتيب الصحيح. + * + * الترتيب هو كل شيء: ALTER USER أولاً، ثم **تحقّق باتصال فعلي**، ثم + * كتابة .env. لو كتبنا .env أولاً وفشل ALTER، لصار التطبيق يحمل كلمة + * لا يقبلها الخادم — انقطاع كامل. ولو نجح ALTER وفشلت الكتابة، نرجع + * كلمة المرور القديمة فوراً بدل ترك الطرفين متنافرين. + * + * ولا تُلمس البيانات في أي خطوة: هذا تغيير في mysql.user لا غير. + */ +function rotateDbPassword(string $envFile, string $logFile, array $env, string $key): array +{ + $map = ENV_DB_CREDENTIALS[$key]; + $user = $env[$map['user']] ?? ''; + $dbName = $env[$map['db']] ?? ''; + $rootPass = $env['MYSQL_ROOT_PASSWORD'] ?? ''; + $oldPass = $env[$key] ?? ''; + + if ($user === '' || $dbName === '' || $rootPass === '') { + return ['', "ينقص {$map['user']} أو {$map['db']} أو MYSQL_ROOT_PASSWORD."]; + } + + // كلمة مرور بلا رموز تحتاج هروباً في الشل — hex آمنة في كل سياق. + $newPass = envGenerate(20); + + $alter = sprintf( + "ALTER USER %s@'%%' IDENTIFIED BY %s; FLUSH PRIVILEGES;", + quoteIdent($user), quoteLiteral($newPass) + ); + + [$code] = runMysql($rootPass, $alter); + if ($code !== 0) { + auditLog($logFile, 'DB_ROTATE_FAIL_ALTER', $key); + return ['', 'فشل ALTER USER — لم يتغيّر شيء.']; + } + + // تحقّق: هل تعمل الكلمة الجديدة فعلاً؟ الثقة بأن ALTER نجح لا تكفي + // (قد يكون المستخدم معرَّفاً بمضيف آخر مثلاً). + if (!verifyDbLogin($user, $newPass, $dbName)) { + runMysql($rootPass, sprintf( + "ALTER USER %s@'%%' IDENTIFIED BY %s; FLUSH PRIVILEGES;", + quoteIdent($user), quoteLiteral($oldPass) + )); + auditLog($logFile, 'DB_ROTATE_ROLLBACK', $key); + return ['', 'الكلمة الجديدة لم تعمل — أُعيدت القديمة.']; + } + + $backup = envWrite($envFile, [$key => $newPass]); + if (!$backup) { + runMysql($rootPass, sprintf( + "ALTER USER %s@'%%' IDENTIFIED BY %s; FLUSH PRIVILEGES;", + quoteIdent($user), quoteLiteral($oldPass) + )); + auditLog($logFile, 'DB_ROTATE_ROLLBACK_WRITE', $key); + return ['', 'تعذّرت كتابة .env — أُعيدت كلمة المرور القديمة.']; + } + + auditLog($logFile, 'DB_ROTATE', "$key ($user@$dbName)"); + return ["دُوِّرت كلمة مرور $user. أعد تشغيل حاويات php الآن.", '']; +} + +/** هروب معرّف MySQL بالعلامة الخلفية. */ +function quoteIdent(string $s): string +{ + return '`' . str_replace('`', '``', $s) . '`'; +} + +/** هروب سلسلة نصية لـ MySQL. */ +function quoteLiteral(string $s): string +{ + return "'" . addcslashes($s, "\\'\"\0\n\r\032") . "'"; +} + +/** + * ينفّذ SQL في حاوية mysql. + * + * كلمة المرور تمرّ عبر متغيّر بيئة لا وسيطاً في سطر الأوامر: الوسيط + * يظهر في `ps` لأي مستخدم على الجهاز. + */ +function runMysql(string $rootPass, string $sql): array +{ + $cmd = 'docker compose -f ' . escapeshellarg(dirname(__DIR__, 3) . '/docker/docker-compose.yml') + . ' exec -T -e MYSQL_PWD mysql mysql -uroot -e ' . escapeshellarg($sql) . ' 2>&1'; + + $descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $proc = proc_open($cmd, $descriptors, $pipes, null, ['MYSQL_PWD' => $rootPass]); + if (!is_resource($proc)) return [1, 'proc_open failed']; + + $out = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + + return [proc_close($proc), $out]; +} + +/** يتأكد أن الاعتماد الجديد يسجّل دخولاً فعلياً. */ +function verifyDbLogin(string $user, string $pass, string $db): bool +{ + $cmd = 'docker compose -f ' . escapeshellarg(dirname(__DIR__, 3) . '/docker/docker-compose.yml') + . ' exec -T -e MYSQL_PWD mysql mysql -u' . escapeshellarg($user) + . ' ' . escapeshellarg($db) . ' -e "SELECT 1" 2>&1'; + + $descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $proc = proc_open($cmd, $descriptors, $pipes, null, ['MYSQL_PWD' => $pass]); + if (!is_resource($proc)) return false; + + stream_get_contents($pipes[1]); + stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + + return proc_close($proc) === 0; +} + +/** حالة المفتاح بلا كشف قيمته. */ +function keyState(array $env, string $name): string +{ + if (!array_key_exists($name, $env)) return 'مفقود'; + return envIsPlaceholder($env[$name]) ? 'نائب' : 'مضبوط'; +} + +/** بصمة قصيرة تكفي للتمييز ولا تكفي للاستعمال. */ +function keyFingerprint(array $env, string $name): string +{ + $v = $env[$name] ?? ''; + return $v === '' ? '—' : substr(hash('sha256', $v), 0, 8); +} + +$csrf = $_SESSION['csrf'] ?? ''; +?> + + + + + + +
وصول محلي فقط عبر نفق SSH.
+ +
+ = htmlspecialchars($envFile) ?> ·
+ خروج
+
طرفاها كلاهما لنا. التدوير آمن — يكفي إعادة تشغيل ما يحملها.
+| المفتاح | الحالة | بصمة | |
|---|---|---|---|
= htmlspecialchars($name) ?> |
+ = $s ?> | += keyFingerprint($env, $name) ?> |
+ + + | +
+ تدويرها يُخرج كل السائقين والركّاب من حساباتهم ويطالبهم + بتسجيل دخول جديد. ليس عطلاً، لكنه ليس شيئاً يُفعل وقت الذروة. +
+| المفتاح | الحالة | بصمة | |
|---|---|---|---|
= htmlspecialchars($name) ?> |
+ = $s ?> | += keyFingerprint($env, $name) ?> |
+ + + | +
+ التدوير يجري بالترتيب الصحيح: ALTER USER ثم تحقّق باتصال
+ فعلي ثم كتابة .env — وإن فشلت أي خطوة تُستعاد الكلمة
+ القديمة فوراً. لا مساس بالبيانات ولا إعادة بناء؛ هذا
+ تغيير في mysql.user لا غير.
+
| المفتاح | المستخدم | القاعدة | الحالة | |
|---|---|---|---|---|
= htmlspecialchars($name) ?> |
+ = htmlspecialchars($env[$map['user']] ?? '—') ?> |
+ = htmlspecialchars($env[$map['db']] ?? '—') ?> |
+ = $s ?> | ++ + + — + | +
لا تُولَّد — تُنسخ من لوحة المزوّد وتُلصق هنا.
+| المفتاح | الحالة | قيمة جديدة | |
|---|---|---|---|
+ بيانات مخزَّنة في القاعدة مشتقّة من هذه المفاتيح: أسماء الركّاب
+ وهواتفهم ولوحات السيارات مشفّرة بـ ENC_KEY، وأعمدة
+ *_bidx محسوبة بـ BLIND_INDEX_PEPPER.
+ تغيير أيٍّ منها لا يعيد تشفير شيء — يجعل كل صف قديم
+ غير قابل للقراءة إلى الأبد، ولا نسخة احتياطية تنفع لأنها هي نفسها
+ مشفّرة بالمفتاح القديم. تدويرها ترحيلُ بيانات لا تغييرُ إعداد،
+ ولذلك لا يوجد لها زر هنا ولا مسار يكتبها في هذا الملف.
+
| المفتاح | الحالة | بصمة |
|---|---|---|
= htmlspecialchars($name) ?> |
+ = $s ?> | += keyFingerprint($env, $name) ?> |
+
سجلّ التغييرات: backend/tools/panel/panel_audit.log