feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل

قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:14:13 +03:00
co-authored by Claude Opus 5
parent 9909d9b4c1
commit 4d8414c96b
3198 changed files with 766859 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
<?php
/**
* core/Security/BlindIndex.php
*
* فهرس أعمى للبحث فوق حقول مشفّرة.
*
* المشكلة: التشفير الآمن (AES-GCM) عشوائي — نفس النص ينتج تشفيراً مختلفاً في
* كل مرة، فلا يمكن البحث بمقارنة النص المشفّر. والحل القديم (CBC بـ IV ثابت)
* يجعل التشفير حتمياً فينجح البحث، لكنه يسرّب المساواة والبادئات المشتركة.
*
* الحل: نفصل التخزين عن البحث.
* - التخزين: AES-GCM عشوائي (لا يسرّب شيئاً).
* - البحث: عمود إضافي يحمل HMAC-SHA256 حتمياً للقيمة بعد تطبيعها.
*
* لماذا HMAC وليس sha256 عارياً؟ لأن مساحة أرقام الهواتف صغيرة (ملايين
* قليلة) — جدول عكسي لكل الأرقام يُبنى في ثوانٍ. المفتاح السرّي (pepper)
* المخزَّن في البيئة وحده يمنع ذلك، فمن يسرق قاعدة البيانات لا يملكه.
*/
final class BlindIndex
{
private string $pepper;
public function __construct(?string $pepper = null)
{
$pepper = $pepper ?: (getenv('BLIND_INDEX_PEPPER') ?: '');
if ($pepper === '') {
throw new RuntimeException(
'BLIND_INDEX_PEPPER is not set. Generate one with: openssl rand -hex 32'
);
}
$this->pepper = $pepper;
}
/**
* يحسب الفهرس لقيمة داخل حقل محدد.
*
* $scope يشمل الجدول والحقل (مثل "driver.phone") عمداً: بدونه يكون فهرس
* نفس الرقم متطابقاً في جدول السائقين والركاب، فيستطيع من يقرأ القاعدة
* ربط الحسابات ببعضها دون فك أي تشفير.
*/
public function index(string $scope, ?string $value): ?string
{
$normalized = self::normalize($scope, $value);
if ($normalized === null || $normalized === '') {
return null;
}
return hash_hmac('sha256', $scope . ':' . $normalized, $this->pepper);
}
/**
* فهرس مبتور للبحث الجزئي (مثل الأسماء).
*
* البتر مقصود: يُنتج تطابقات كاذبة تُصفّى بعد فك التشفير، وهذه الضبابية
* هي ما يمنع استخدام الفهرس نفسه في تحليل التكرارات.
*/
public function bucket(string $scope, ?string $value, int $length = 8): ?string
{
$full = $this->index($scope, $value);
return $full === null ? null : substr($full, 0, $length);
}
/**
* التطبيع قبل الحساب — بدونه يُنتج 0791234567 و+962791234567 فهرسين
* مختلفين ويفشل البحث.
*/
public static function normalize(string $scope, ?string $value): ?string
{
if ($value === null) return null;
$value = trim($value);
if ($value === '') return null;
if (str_contains($scope, 'phone')) {
$digits = preg_replace('/\D+/', '', $value);
// توحيد الصيغة المحلية والدولية على شكل واحد
$digits = preg_replace('/^00/', '', $digits);
if (str_starts_with($digits, '0')) {
$cc = getenv('DEFAULT_COUNTRY_CODE') ?: '962';
$digits = $cc . substr($digits, 1);
}
return $digits;
}
if (str_contains($scope, 'email')) {
return mb_strtolower($value, 'UTF-8');
}
// الأسماء: توحيد حالة الأحرف والمسافات، وتوحيد أشكال الألف والياء
// والتاء المربوطة العربية حتى لا يتوقف البحث على شكل الكتابة.
$value = mb_strtolower($value, 'UTF-8');
$value = preg_replace('/\s+/u', ' ', $value);
$value = str_replace(
['أ', 'إ', 'آ', 'ٱ', 'ى', 'ة', 'ؤ', 'ئ'],
['ا', 'ا', 'ا', 'ا', 'ي', 'ه', 'و', 'ي'],
$value
);
// إزالة التشكيل
$value = preg_replace('/[\x{064B}-\x{0652}\x{0640}]/u', '', $value);
return trim($value);
}
}
+141
View File
@@ -0,0 +1,141 @@
<?php
// ============================================================
// core/Security/EncryptionHelper.php
// يدعم AES-256-GCM الجديد + AES-256-CBC القديم (توافقية)
// ============================================================
class EncryptionHelper
{
private string $key;
private string $cbcIv;
private const ALGO_GCM = 'aes-256-gcm';
private const ALGO_CBC = 'AES-256-CBC'; // للتوافقية
private const IV_LEN_GCM = 12;
private const TAG_LEN = 16;
private const PREFIX_GCM = 'GCM:'; // للتمييز بين الجديد والقديم
/**
* وضع الكتابة: 'cbc' (افتراضي) أو 'gcm'.
*
* القراءة غير متأثرة بهذا الوضع إطلاقاً — decryptData تتعرّف على الصيغتين
* عبر البادئة، فالسجلات القديمة تبقى مقروءة بلا ترحيل، والرجوع عن التحويل
* لا يُفقد أي سجل كُتب بـ GCM.
*/
private string $writeMode;
public function __construct(string $key, ?string $cbcIv = null, ?string $writeMode = null)
{
if (strlen($key) !== 32) {
throw new InvalidArgumentException('Encryption key must be exactly 32 bytes.');
}
$this->key = $key;
// IV القديم للتوافقية أثناء مرحلة المايغريشن
$this->cbcIv = $cbcIv ?: getenv('initializationVector') ?: str_repeat('0', 16);
$mode = strtolower($writeMode ?: (getenv('ENCRYPTION_MODE') ?: 'cbc'));
$this->writeMode = $mode === 'gcm' ? 'gcm' : 'cbc';
}
public function writeMode(): string
{
return $this->writeMode;
}
/**
* نقطة التشفير الموحّدة لكل التطبيق.
*
* حتى الآن كانت CBC بـ IV ثابت، أي حتمية: نفس النص ينتج نفس التشفير، وهو
* ما كان يسمح بالبحث عبر مقارنة النص المشفّر، لكنه يسرّب المساواة
* والبادئات المشتركة. مع ENCRYPTION_MODE=gcm يصبح التشفير عشوائياً
* وموثَّقاً، ويتكفّل الفهرس الأعمى (BlindIndex) بالبحث.
*/
public function encryptData(string $plainText): string
{
if ($this->writeMode === 'gcm') {
return $this->encryptDataGCM($plainText);
}
return $this->encryptDataCBC($plainText);
}
// ─── تشفير نص باستخدام AES-256-CBC الحتمي (للتوافقية والرجوع) ──
public function encryptDataCBC(string $plainText): string
{
$plainText = mb_convert_encoding($plainText, 'UTF-8');
$padded = $this->addPadding($plainText);
$encrypted = openssl_encrypt($padded, self::ALGO_CBC, $this->key, OPENSSL_RAW_DATA, $this->cbcIv);
return base64_encode($encrypted);
}
// ─── تشفير نص باستخدام AES-256-GCM العشوائي (عالي الأمان) ──
public function encryptDataGCM(string $plainText): string
{
$plainText = mb_convert_encoding($plainText, 'UTF-8');
$iv = random_bytes(self::IV_LEN_GCM);
$tag = '';
$encrypted = openssl_encrypt($plainText, self::ALGO_GCM, $this->key, OPENSSL_RAW_DATA, $iv, $tag, "", self::TAG_LEN);
return self::PREFIX_GCM . base64_encode($iv . $tag . $encrypted);
}
// ─── فك تشفير نص (يدعم CBC والـ GCM المستقبلي) ───────────
public function decryptData(?string $cipherText): string|false
{
if (empty($cipherText)) return '';
// تحقق إن كان مشفر بالنظام الجديد
if (str_starts_with($cipherText, self::PREFIX_GCM)) {
$raw = base64_decode(substr($cipherText, strlen(self::PREFIX_GCM)), true);
if ($raw === false || strlen($raw) < self::IV_LEN_GCM + self::TAG_LEN) return false;
$iv = substr($raw, 0, self::IV_LEN_GCM);
$tag = substr($raw, self::IV_LEN_GCM, self::TAG_LEN);
$cipher = substr($raw, self::IV_LEN_GCM + self::TAG_LEN);
$plain = openssl_decrypt($cipher, self::ALGO_GCM, $this->key, OPENSSL_RAW_DATA, $iv, $tag);
return $plain !== false ? $plain : false;
}
// وإلا استخدم CBC القديم
$decoded = base64_decode($cipherText, true);
if ($decoded === false) return false;
$decrypted = openssl_decrypt($decoded, self::ALGO_CBC, $this->key, OPENSSL_RAW_DATA, $this->cbcIv);
if ($decrypted === false) return false;
$pad = ord($decrypted[strlen($decrypted) - 1]);
if ($pad < 1 || $pad > 16) return false;
return substr($decrypted, 0, -$pad);
}
// ─── تشفير/فك تشفير Binary (صور، ملفات) ───────────────
// تُستخدم الـ GCM مع IV عشوائي (كما في encryptData)
public function encryptBinary(string $data): string
{
$iv = random_bytes(self::IV_LEN_GCM);
$tag = '';
$encrypted = openssl_encrypt($data, self::ALGO_GCM, $this->key, OPENSSL_RAW_DATA, $iv, $tag, "", self::TAG_LEN);
return base64_encode($iv . $tag . $encrypted);
}
public function decryptBinary(string $data): string|false
{
$raw = base64_decode($data, true);
if ($raw === false || strlen($raw) < self::IV_LEN_GCM + self::TAG_LEN) return false;
$iv = substr($raw, 0, self::IV_LEN_GCM);
$tag = substr($raw, self::IV_LEN_GCM, self::TAG_LEN);
$cipher = substr($raw, self::IV_LEN_GCM + self::TAG_LEN);
return openssl_decrypt($cipher, self::ALGO_GCM, $this->key, OPENSSL_RAW_DATA, $iv, $tag);
}
// --------- دوال الـ Padding للـ CBC ----------
private function addPadding($data, $blockSize = 16) {
$pad = $blockSize - (strlen($data) % $blockSize);
return $data . str_repeat(chr($pad), $pad);
}
private function removePadding($data) {
$pad = ord($data[strlen($data) - 1]);
return substr($data, 0, -$pad);
}
}