import { MigrationInterface, QueryRunner } from 'typeorm'; import { encrypt, blindIndex } from '../../common/crypto/crypto.util'; /** * تشفير الهاتف at-rest + فهرس أعمى للبحث (docs/16، docs/17 — D1 إضافة). * * **لماذا فهرس منفصل**: `phone` بعد التشفير غير قابل للبحث بالمساواة إطلاقاً * — AES-256-GCM يستعمل IV عشوائياً لكل عملية، فنفس الرقم الحقيقي ينتج شيفرة * مختلفة كل مرة. القيد الفريد ونقاط `WHERE phone = …` (تسجيل الدخول بأكمله) * ينتقلان إلى `phone_bidx` — بصمة حتمية (HMAC) لا تكشف الرقم لكنها تكرّر * نفسها لنفس الرقم دائماً. * * تُشفَّر الصفوف القائمة في نفس الهجرة (لا تُترك نصّاً صافياً) — الحماية * التلقائية عبر `decrypt()` المتسامح كانت ستُبقيها صافية للأبد بلا هذا. */ export class EncryptPhone1721900000000 implements MigrationInterface { public async up(q: QueryRunner): Promise { await q.query(`ALTER TABLE tripz_users ADD COLUMN IF NOT EXISTS phone_bidx varchar`); const rows: { id: string; phone: string }[] = await q.query( `SELECT id, phone FROM tripz_users`, ); for (const row of rows) { // تسامح إعادة التشغيل الجزئي: لا نُشفّر قيمة مشفّرة أصلاً مرة أخرى. const alreadyEncrypted = row.phone?.startsWith('v1:'); const canonicalPhone = alreadyEncrypted ? null : row.phone; const bidx = blindIndex(canonicalPhone ?? row.phone); if (alreadyEncrypted) { await q.query(`UPDATE tripz_users SET phone_bidx = $1 WHERE id = $2`, [bidx, row.id]); } else { await q.query(`UPDATE tripz_users SET phone = $1, phone_bidx = $2 WHERE id = $3`, [ encrypt(row.phone), bidx, row.id, ]); } } await q.query(`ALTER TABLE tripz_users ALTER COLUMN phone_bidx SET NOT NULL`); await q.query(`DROP INDEX IF EXISTS "UQ_tripz_users_tenant_phone"`); await q.query(` CREATE UNIQUE INDEX IF NOT EXISTS "UQ_tripz_users_tenant_phone_bidx" ON tripz_users (tenant_id, phone_bidx) `); } public async down(q: QueryRunner): Promise { // لا رجوع لفكّ التشفير جماعياً هنا عمداً — decrypt() يبقى متاحاً عند // القراءة عبر الـORM؛ التراجع عن هذه الهجرة يعني قبول عدم قابلية البحث // إن أُعيد phone فهرساً فريداً وحده، فلا نعيد بناء ذلك القيد تلقائياً. await q.query(`DROP INDEX IF EXISTS "UQ_tripz_users_tenant_phone_bidx"`); await q.query(`ALTER TABLE tripz_users ALTER COLUMN phone_bidx DROP NOT NULL`); await q.query(`ALTER TABLE tripz_users DROP COLUMN IF EXISTS phone_bidx`); } }