Files
Hamza-AyedandClaude Opus 5 92dc6b3641 chore: استيراد أولي من سيرو (ecfe7568) — بلا أي تعديل
نسخة كاملة من مستودع سيرو عند ecfe7568 لتكون أساس تطبيق «انطلق».
نُسخ المتعقَّب في git فقط (12,509 ملفاً / 302 م.ب) بـ git archive، لا
`cp -r` — فاستُثنيت تلقائياً مخلفات البناء (build · node_modules ·
.dart_tool · .gradle · Pods ≈ 10.7 غ.ب) وكل ما يستثنيه .gitignore.

هذا الكوميت **بلا أي تعديل عمداً** حتى يكون كل ما يليه فرقاً مقروءاً
مقابل سيرو الأصلي. سيرو نفسه لم يُمسّ.

⚠️ لا يبني بعد: `.env` و`lib/env/env.g.dart` غير متعقَّبين في سيرو (وهذا
صحيح — أسرار لكل مستأجر). كل تطبيق فلاتر هنا يحتاج .env خاصاً بانطلق ثم
توليد env.g.dart عبر build_runner. لا تُنسخ أسرار سيرو.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:10:29 +03:00

102 lines
4.3 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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);
}
}