diff --git a/backend/auth/driver/checkPhoneNumberISVerfied.php b/backend/auth/driver/checkPhoneNumberISVerfied.php index a0b5d61b..c5a0867e 100644 --- a/backend/auth/driver/checkPhoneNumberISVerfied.php +++ b/backend/auth/driver/checkPhoneNumberISVerfied.php @@ -4,7 +4,7 @@ require_once __DIR__ . '/../../connect.php'; // استقبال وتشفير رقم الهاتف $phoneNumber = filterRequest("phone_number"); -$phoneNumber = $encryptionHelper->encryptData($phoneNumber); +$phoneNumber = otpPhoneKey($phoneNumber); // تجهيز الاستعلام باستخدام bindParam للحماية $sql = "SELECT * FROM `phone_verification` WHERE `phone_number` = :phone_number"; diff --git a/backend/auth/driver/isPhoneVerified.php b/backend/auth/driver/isPhoneVerified.php index 50dcacdd..5b0414a9 100644 --- a/backend/auth/driver/isPhoneVerified.php +++ b/backend/auth/driver/isPhoneVerified.php @@ -4,7 +4,7 @@ require_once __DIR__ . '/../../../connect.php'; $phoneNumber = filterRequest("phone_number"); // تشفير الرقم قبل البحث -$phoneNumber_encrypted = $encryptionHelper->encryptData($phoneNumber); +$phoneNumber_encrypted = otpPhoneKey($phoneNumber); try { // الاستعلام عن السائق حسب رقم الهاتف وحالة التحقق diff --git a/backend/auth/driver/token/send_otp.php b/backend/auth/driver/token/send_otp.php index 988d1ffd..a3a92ec8 100644 --- a/backend/auth/driver/token/send_otp.php +++ b/backend/auth/driver/token/send_otp.php @@ -51,8 +51,8 @@ $sentOK = ($httpCode === 200 && ($decoded['success'] ?? false)); if ($sentOK) { /* 3) تشفير البيانات وحفظها في DB ----------------------------------- */ - $receiver_enc = $encryptionHelper->encryptData($receiver); - $otp_enc = $encryptionHelper->encryptData($otp); + $receiver_enc = otpPhoneKey($receiver); + $otp_enc = otpPhoneKey($otp); // يجب أن يطابق صيغة المقارنة في verify_otp $exp = date('Y-m-d H:i:s', strtotime('+5 minutes')); $now = date('Y-m-d H:i:s'); diff --git a/backend/auth/driver/token/verify_otp.php b/backend/auth/driver/token/verify_otp.php index d8502918..7b04c0f2 100644 --- a/backend/auth/driver/token/verify_otp.php +++ b/backend/auth/driver/token/verify_otp.php @@ -9,8 +9,9 @@ if (empty($phoneNumber) || empty($otp)) { exit(); } -$phoneNumber_encrypted = $encryptionHelper->encryptData($phoneNumber); -$otp_encrypted = $encryptionHelper->encryptData($otp); +$phoneNumber_encrypted = otpPhoneKey($phoneNumber); +// الرمز يُقارن بالتساوي أيضاً، فيحتاج نفس الصيغة الثابتة +$otp_encrypted = otpPhoneKey($otp); try { $stmt = $con->prepare(" diff --git a/backend/auth/otp/request.php b/backend/auth/otp/request.php index b508ca21..1b0afc6c 100644 --- a/backend/auth/otp/request.php +++ b/backend/auth/otp/request.php @@ -119,7 +119,7 @@ switch (strtolower($country)) { // 6. DB Storage on Success if ($sentSuccessfully) { - $encryptedPhone = $encryptionHelper->encryptData($receiver); // Deterministic CBC + $encryptedPhone = otpPhoneKey($receiver); // مفتاح بحث ثابت مستقل عن نمط التشفير $encryptedOtp = $encryptionHelper->encryptDataGCM($otp); // Random GCM $encryptedEmail = !empty($email) ? $encryptionHelper->encryptData($email) : ''; diff --git a/backend/auth/otp/verify.php b/backend/auth/otp/verify.php index 63992771..a6d5b826 100644 --- a/backend/auth/otp/verify.php +++ b/backend/auth/otp/verify.php @@ -59,7 +59,7 @@ try { // 3. Encrypt data to query // 4. Verify based on user type try { - $encryptedPhoneSearch = $encryptionHelper->encryptData($phone_number); + $encryptedPhoneSearch = otpPhoneKey($phone_number); if ($user_type === 'admin') { $sql = "SELECT * FROM token_verification_admin diff --git a/backend/auth/passenger/token/send_otp.php b/backend/auth/passenger/token/send_otp.php index 5d0d421c..37e5a50c 100644 --- a/backend/auth/passenger/token/send_otp.php +++ b/backend/auth/passenger/token/send_otp.php @@ -51,8 +51,8 @@ $sentOK = ($httpCode === 200 && ($decoded['success'] ?? false)); if ($sentOK) { /* 3) حفظ الرمز في Redis + قاعدة البيانات */ - $receiver_enc = $encryptionHelper->encryptData($receiver); - $otp_enc = $encryptionHelper->encryptData($otp); + $receiver_enc = otpPhoneKey($receiver); + $otp_enc = otpPhoneKey($otp); // يجب أن يطابق صيغة المقارنة في verify_otp $exp = date('Y-m-d H:i:s', strtotime('+5 minutes')); $now = date('Y-m-d H:i:s'); diff --git a/backend/auth/passenger/token/verify_otp.php b/backend/auth/passenger/token/verify_otp.php index 72d704e7..3b0b6c26 100644 --- a/backend/auth/passenger/token/verify_otp.php +++ b/backend/auth/passenger/token/verify_otp.php @@ -18,8 +18,9 @@ if (empty($phoneNumber) || empty($otp)) { exit(); } -$phoneNumber_encrypted = $encryptionHelper->encryptData($phoneNumber); -$otp_encrypted = $encryptionHelper->encryptData($otp); +$phoneNumber_encrypted = otpPhoneKey($phoneNumber); +// الرمز يُقارن بالتساوي أيضاً، فيحتاج نفس الصيغة الثابتة +$otp_encrypted = otpPhoneKey($otp); try { // 1. التحقق من Redis بدلاً من MySQL diff --git a/backend/core/Security/EncryptionHelper.php b/backend/core/Security/EncryptionHelper.php index 3771e4ef..fee222a0 100644 --- a/backend/core/Security/EncryptionHelper.php +++ b/backend/core/Security/EncryptionHelper.php @@ -14,7 +14,16 @@ class EncryptionHelper private const TAG_LEN = 16; private const PREFIX_GCM = 'GCM:'; // للتمييز بين الجديد والقديم - public function __construct(string $key, ?string $cbcIv = null) + /** + * وضع الكتابة: '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.'); @@ -22,10 +31,34 @@ class EncryptionHelper $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'; } - // ─── تشفير نص باستخدام AES-256-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); diff --git a/backend/core/helpers.php b/backend/core/helpers.php index 91b06f58..425dfcf2 100644 --- a/backend/core/helpers.php +++ b/backend/core/helpers.php @@ -38,6 +38,31 @@ function filterRequest(string $name, string $type = 'string'): mixed }; } + +/** + * مفتاح بحث ثابت لجداول التحقق (token_verification*, phone_verification*). + * + * هذه الجداول تستخدم رقم الهاتف كمفتاح بحث لا كبيان يُعرض: يُكتب عند الإرسال + * ويُقرأ عند التحقق. تخزينه مشفّراً كان يعمل فقط لأن التشفير حتمي — ومع + * AES-GCM العشوائي يُنتج الإرسال والتحقق قيمتين مختلفتين فلا ينجح أي رمز. + * + * البديل: بصمة HMAC حتمية للرقم بعد تطبيعه. لا تحتاج تعديل المخطط (العمود + * نصي أصلاً)، وتوحّد صيغ الرقم المحلية والدولية، ولا يمكن عكسها بلا المفتاح. + */ +function otpPhoneKey(?string $phone): string +{ + if ($phone === null || trim($phone) === '') return ''; + + global $blindIndex, $encryptionHelper; + + if ($blindIndex) { + return 'K:' . $blindIndex->index('otp.phone', $phone); + } + + // بلا BLIND_INDEX_PEPPER نعود للسلوك القديم حتى لا يتعطل التحقق + return $encryptionHelper ? $encryptionHelper->encryptData($phone) : $phone; +} + // ── ردود JSON موحدة ───────────────────────────────────────── function jsonSuccess(mixed $data = null, string $message = 'success', int $code = 200): never {