Files
Siro/backend/tools/env_keys.php
T

234 lines
11 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* tools/env_keys.php — جرد مفاتيح البيئة وزراعة الناقص منها
* ─────────────────────────────────────────────────────────────
* ‏الاستعمال:
* php env_keys.php audit # جرد فقط، لا يكتب شيئاً
* php env_keys.php seed # يزرع الناقص القابل للتوليد
* php env_keys.php seed --dry-run # يُظهر ما سيفعله بلا كتابة
*
* ‏المفاتيح ليست صنفاً واحداً، والخلط بينها هو ما يحوّل «تدوير مفتاح»
* ‏إلى فقدان بيانات لا رجعة فيه. أربعة أصناف:
*
* • generated — سرّ بيننا وبين أنفسنا. نولّده ونُدوّره متى شئنا.
* • session — تدويره يُخرج كل المستخدمين من حساباتهم. مقبول بوعي.
* • frozen — ‏**لا يُدوَّر إطلاقاً**. بيانات مخزَّنة مشتقّة منه،
* وتغييره يجعلها غير قابلة للقراءة إلى الأبد.
* • external — من طرف ثالث. لا يمكن توليده، يُنسخ من لوحته.
*/
$root = dirname(__DIR__, 2);
$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',
];
// ═══════════════════════════════════════════════════════════════
// القراءة
// ═══════════════════════════════════════════════════════════════
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('/^(<?CHANGE_ME|changeme|your[_-]|xxx+|TODO|REPLACE)/i', $v);
};
echo "ملف البيئة: $envFile\n";
echo str_repeat('─', 62) . "\n";
$toSeed = [];
$report = static function (string $title, array $keys, array $present,
callable $isPlaceholder, ?array &$toSeed = null,
bool $seedable = false) {
echo "\n▸ $title\n";
foreach ($keys as $k => $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";