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', ]; // ═══════════════════════════════════════════════════════════════ // القراءة // ═══════════════════════════════════════════════════════════════ if (!is_readable($envFile)) { fwrite(STDERR, "لا يمكن قراءة $envFile\n"); exit(1); } $lines = file($envFile, FILE_IGNORE_NEW_LINES); $present = []; foreach ($lines as $line) { if (preg_match('/^\s*([A-Z0-9_]+)\s*=\s*(.*)$/', $line, $m)) { $present[$m[1]] = trim($m[2], "\"' "); } } /** ‏قيمة نائبة لا سرّاً: موجودة شكلاً وغائبة فعلاً. */ $isPlaceholder = static function (string $v): bool { if ($v === '') return true; return (bool) preg_match('/^( $v) { $name = is_int($k) ? $v : $k; $bytes = is_int($k) ? 0 : $v; $exists = array_key_exists($name, $present); $blank = $exists && $isPlaceholder($present[$name]); $state = !$exists ? 'مفقود' : ($blank ? 'نائب' : 'مضبوط'); printf(" %-30s %s\n", $name, $state); if ($seedable && ($state !== 'مضبوط') && $toSeed !== null) { $toSeed[$name] = $bytes ?: 32; } } }; $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); echo "\n" . str_repeat('─', 62) . "\n"; if ($command !== 'seed') { echo empty($toSeed) ? "لا ينقص شيء قابل للتوليد.\n" : 'قابل للزراعة: ' . count($toSeed) . " مفتاحاً. شغّل: php env_keys.php seed\n"; exit(0); } // ═══════════════════════════════════════════════════════════════ // الزراعة // ═══════════════════════════════════════════════════════════════ if (empty($toSeed)) { echo "لا ينقص شيء قابل للتوليد.\n"; exit(0); } // ‏لا نلمس مفتاحاً مضبوطاً أبداً: هذه الأداة تملأ الفراغ فقط. التدوير // ‏قرار واعٍ له تبعات (جلسات، خوادم أخرى تحمل نفس السرّ)، ولا يجوز أن // ‏يقع كأثر جانبي لتشغيل أداة جرد. $additions = []; foreach ($toSeed as $name => $bytes) { $value = bin2hex(random_bytes($bytes)); $additions[$name] = $value; echo "توليد $name (" . ($bytes * 2) . " حرفاً)\n"; } if ($dryRun) { echo "\n[dry-run] لم يُكتب شيء.\n"; exit(0); } // ‏نسخة احتياطية قبل أي كتابة. ملف .env تالف يعني توقف كل شيء، وهو // ‏غير موجود في git بطبيعته فلا مصدر آخر لاستعادته. $backup = $envFile . '.bak.' . date('Ymd_His'); if (!copy($envFile, $backup)) { fwrite(STDERR, "تعذّرت النسخة الاحتياطية — أُلغيت العملية.\n"); exit(1); } @chmod($backup, 0600); echo "\nنسخة احتياطية: $backup\n"; $out = rtrim(file_get_contents($envFile), "\n") . "\n"; // ‏نستبدل السطر النائب في مكانه، ونُلحق المفقود في النهاية — الاستبدال // ‏في المكان يبقي التعليقات المجاورة مفيدة. foreach ($additions as $name => $value) { if (array_key_exists($name, $present)) { $out = preg_replace( '/^\s*' . preg_quote($name, '/') . '\s*=.*$/m', "$name=$value", $out, 1 ); unset($additions[$name]); } } if (!empty($additions)) { $out .= "\n# ── زُرعت تلقائياً بـ tools/env_keys.php في " . date('Y-m-d H:i') . " ──\n"; foreach ($additions as $name => $value) { $out .= "$name=$value\n"; } } if (file_put_contents($envFile, $out) === false) { fwrite(STDERR, "تعذّرت الكتابة. النسخة الاحتياطية سليمة: $backup\n"); exit(1); } @chmod($envFile, 0600); echo "\nتمّت الزراعة.\n"; echo "الخطوة التالية — أعد تشغيل حاويات php لتقرأ الجديد:\n"; echo " docker compose restart php php_food php_transit\n\n"; echo "تذكير: WEBHOOK_SECRET_KEY يجب أن يطابق RELAY_SECRET في\n"; echo "sms_relay/app/build.gradle.kts — وهو يحتاج إعادة بناء للتطبيق.\n";