> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# التشفير للمطورين: ماذا تستخدم ولماذا لا تخترع خوارزميتك
- URL: https://arabroot.io/articles/التشفير-للمطورين-الهاش-وكلمات-المرور-والمفاتيح/
- Published: 2026-10-06T03:30:00.000Z
- Updated: 2026-10-09T09:43:28.000Z
- Description: أي هاش تستخدم لكلمات المرور، ومتى تحتاج التشفير المتماثل أو غير المتماثل، وأين تحفظ المفاتيح بأمان في تطبيقك.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الأمن السيبراني, DevOps وCI/CD, الاستضافة الذاتية

كل مطور يستخدم التشفير كل يوم تقريباً حتى لو لم يكتب سطراً واحداً منه بنفسه، فهو يحفظ كلمات مرور المستخدمين في جدول، ويتحقق من توقيع Webhook قادم من بوابة الدفع، ويولد رمزاً لإعادة تعيين كلمة المرور، ويخزن رقم الهوية مشفراً في عمود، ويضع مفتاح API في مكان ما حتى يقرأه التطبيق. وفي مقال [الترميز Base64 وBase32 وBase58](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B1%D9%85%D9%8A%D8%B2-base64-%D9%88-base32-%D9%88-base58/#not-encryption) وضعنا القاعدة الأولى: الترميز ليس تشفيراً وليس هاش، ورأيت الفرق بين الثلاثة على المدخل نفسه. وفي مقال [OWASP Top 10 للمبتدئين](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#a04) رأيت أن أخطاء التشفير Cryptographic Failures في المرتبة الرابعة A04:2025، وهذا المقال هو تفصيلها للمطور. وفيه سوف ننزل طبقة أعمق، فالسؤال هنا ليس «ما الفرق بين الهاش والتشفير؟» وإنما أي هاش تستخدم ولماذا لا يصلح SHA-256 لكلمات المرور، وما الذي يحدث إذا تكرر ال Nonce في AES-GCM، ولماذا يظهر شكل الصورة من خلال التشفير في وضع ECB، وأين تحفظ المفتاح، وما المكتبة التي تستخدمها في Python وC# وJava وNode.js بدلاً من أن تكتب الخوارزمية بنفسك.

وسوف نناقش في هذا المقال ما يلي:

- أربع أدوات لأربعة أسئلة مختلفة: الهاش Hash وHMAC والتشفير Encryption والتوقيع Signature، ومتى تستخدم كلاً منها.
- دوال الهاش Hash Functions وخصائصها، ولماذا سقط MD5 وSHA-1، وما البديل: SHA-256 وSHA-3 وBLAKE2 وBLAKE3.
- سلامة الرسالة Integrity: لماذا لا يكفي الهاش وحده، وكيف يحل HMAC المشكلة.
- تخزين كلمات المرور Password Storage خطوة بخطوة: من النص الصريح إلى Argon2id بمعاملات OWASP، مع حد 72 بايت في bcrypt، وPBKDF2 عندما تحتاج FIPS.
- التشفير المتماثل Symmetric Encryption مع AES-GCM وChaCha20-Poly1305، ولماذا يعتبر تكرار ال Nonce كارثة، ووضع ECB بصورة نولدها بأنفسنا.
- التشفير غير المتماثل Asymmetric Encryption: RSA مقابل المنحنيات البيضاوية ECC، وأطوال المفاتيح بحسب NIST، والفرق بين التوقيع والتشفير، وتبادل المفاتيح Key Exchange.
- العشوائية Randomness: لماذا لا يصلح `random` و`Math.random` لأي شيء أمني، مع تجربة نتوقع فيها القيمة التالية.
- إدارة المفاتيح Key Management: KMS وHSM وتشفير الظرف Envelope Encryption والتدوير Rotation.
- التشفير ما بعد الحوسبة الكمية Post-Quantum Cryptography باختصار: FIPS 203 و204 و205 وTLS الهجين.
- لماذا لا تخترع خوارزميتك، والمكتبات المناسبة لكل لغة بتراخيصها، مع مثال كامل في C# وآخر في Java.

## أربع أدوات لأربعة أسئلة: الهاش Hash وHMAC والتشفير Encryption والتوقيع Signature

أغلب الأخطاء في هذا الموضوع لا تأتي من خوارزمية ضعيفة، وإنما من أداة صحيحة استخدمت للسؤال الخطأ، فمطور يشفر كلمات المرور وهو يحتاج هاش، وآخر يرسل هاش الرسالة معها وهو يحتاج HMAC، وثالث يظن أن التوقيع يخفي محتوى الرسالة. لذلك قبل أي تفاصيل حدد السؤال الذي تريد الإجابة عنه:

- **الهاش Hash:** هل تغيرت هذه البيانات؟ دالة باتجاه واحد دون مفتاح، يحسبها أي شخص، وتنفعك فقط إذا وصلك الهاش الصحيح من مكان تثق به.
- **رمز التحقق HMAC:** هل تغيرت الرسالة، وهل جاءت من أحد يملك السر المشترك Shared Secret؟ هاش ممزوج بمفتاح، فمن لا يملك المفتاح لا يستطيع حسابه.
- **التشفير Encryption:** من يستطيع قراءة البيانات؟ دالة باتجاهين بمفتاح، والهدف منها السرية Confidentiality، والأنماط الحديثة منه تكشف أي تغيير أيضاً.
- **التوقيع الرقمي Digital Signature:** من كتب هذه الرسالة، وهل يستطيع أي شخص أن يتحقق من ذلك؟ المفتاح الخاص Private Key يوقع والمفتاح العام Public Key يتحقق، والرسالة نفسها تبقى مقروءة.

الصورة التالية تبين الأدوات الأربع جنباً إلى جنب، والقيم القصيرة فيها مأخوذة من مخرجات التجربة في هذا المقال:

![أربعة أعمدة: Hash بلون رمادي دون مفتاح وباتجاه واحد ويحسبه أي شخص، وHMAC-SHA256 بلون أزرق بسر مشترك ويحسبه من يملك المفتاح فقط، وEncryption بلون أخضر مع AES-GCM يعود بالمفتاح ويقرؤه من يملك المفتاح، وSignature بلون برتقالي مع Ed25519 بمفتاح خاص يوقع ومفتاح عام يتحقق والرسالة تبقى مقروءة، وفي أسفل كل عمود السؤال الذي يجيب عنه](https://arabroot.io/content/images/2026/10/crypto-for-devs-01-four-tools.webp)

كل أداة تجيب عن سؤال مختلف، والخطأ الأشهر أن تستخدم واحدة منها لسؤال الأخرى

## تجهيز التجربة Lab: Container واحد وثلاث مكتبات

كل أمثلة Python في هذا المقال تعمل داخل Container واحد من Docker Image الرسمية `python:3.12-slim` مع ثلاث مكتبات معروفة، وهي مكتبة [cryptography](https://cryptography.io/?ref=arabroot.io) للتشفير والتوقيع، ومكتبة [argon2-cffi](https://github.com/hynek/argon2-cffi?ref=arabroot.io) لخوارزمية Argon2، ومكتبة [bcrypt](https://github.com/pyca/bcrypt?ref=arabroot.io). ولا يحتاج ال Container أي منفذ Port لأنه لا يشغل أي خدمة، والخيار `--rm` يحذفه عند الخروج:

```bash
mkdir crypto-lab && cd crypto-lab
docker run --rm -it --name lab-crypto-python -v "$PWD:/lab" -v "$PWD/out:/out" -w /lab python:3.12-slim bash

# داخل ال Container
pip install --no-cache-dir cryptography argon2-cffi bcrypt
python -c "import sys, ssl, cryptography, bcrypt; from importlib.metadata import version; print('Python', sys.version.split()[0], '| cryptography', cryptography.__version__, '| argon2-cffi', version('argon2-cffi'), '| bcrypt', bcrypt.__version__, '|', ssl.OPENSSL_VERSION)"
```

والمخرج سوف يكون كما يلي:

```text
Python 3.12.15 | cryptography 50.0.2 | argon2-cffi 25.1.0 | bcrypt 5.0.0 | OpenSSL 3.5.7 9 Jun 2026
```

ثم نحفظ كل مثال في ملف داخل المجلد ونشغله بـ `python اسم_الملف.py`، وفي النهاية نخرج من ال Container بالأمر `exit` فيحذف تلقائياً. وأمثلة C# وJava وNode.js في القسم الخاص بالمكتبات تعمل بنفس الطريقة في Docker Images الرسمية لكل لغة.

## دوال الهاش Hash Functions: البصمة التي لا تعود

دالة الهاش Hash Function تأخذ أي مدخل بأي طول، حرفاً واحداً أو ملفاً من عشرة جيجابايت، وتخرج بصمة Digest بطول ثابت، 32 بايت في SHA-256 مثلاً، وهي مثل بصمة الإصبع للإنسان: صغيرة وثابتة وتدل على صاحبها، لكنك لا تستطيع أن ترسم الإنسان من بصمته. وقد رأيت هذا في مقال [البلوكتشين للمبتدئين](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D9%84%D9%88%D9%83%D8%AA%D8%B4%D9%8A%D9%86-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D9%88%D9%85%D8%AA%D9%89-%D9%84%D8%A7-%D8%AA%D8%AD%D8%AA%D8%A7%D8%AC%D9%87/#hash) حيث تحمل كل كتلة هاش ما قبلها، وفي [سلسلة الهاش في سجل التدقيق](https://arabroot.io/articles/%D8%B3%D8%AC%D9%84-%D8%A7%D9%84%D8%AA%D8%AF%D9%82%D9%8A%D9%82-audit-trail-%D9%85%D9%86-%D9%81%D8%B9%D9%84-%D9%85%D8%A7%D8%B0%D8%A7-%D9%88%D9%85%D8%AA%D9%89/#tamper-evidence) التي تكشف أي تعديل في حدث قديم، وفي [Git](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#snapshots) الذي يسمي كل Commit بهاش محتواه.

### خصائص الهاش الآمن Secure Hash Properties

حتى تصلح الدالة للاستخدام الأمني يجب أن تحقق ثلاث خصائص، وكل خاصية منها تحمي استخداماً محدداً:

- **مقاومة الرجوع إلى المدخل Preimage Resistance:** إذا رأيت الهاش فلا تستطيع أن تجد مدخلاً يعطيه إلا بالتجربة العمياء، وهذا ما يجعل الهاش «باتجاه واحد One-Way».
- **مقاومة إيجاد مدخل ثان Second Preimage Resistance:** إذا أعطيتك ملفاً وهاشه فلا تستطيع أن تصنع ملفاً آخر له الهاش نفسه، وهذا ما يحمي ملف التحميل الذي تتحقق من هاشه.
- **مقاومة التصادم Collision Resistance:** لا تستطيع أن تجد أي مدخلين مختلفين لهما الهاش نفسه، حتى لو اخترت المدخلين بنفسك، وهذه أصعب الخصائص وأول ما يسقط عندما تضعف الدالة، وهي التي تحمي التوقيعات والشهادات.

وبالإضافة لذلك فإن تغيير بت واحد في المدخل يغير نصف بتات الناتج تقريباً، وهذا ما يسمى أثر الانهيار Avalanche Effect. والمثال التالي يبين ذلك، ويحسب بصمة الكلمة نفسها بثلاث دوال مختلفة:

```python
import hashlib
for word in ["ArabRoot", "Arabroot"]:
    print(f"SHA-256  {word}  {hashlib.sha256(word.encode()).hexdigest()}")
print(f"SHA3-256 ArabRoot  {hashlib.sha3_256(b'ArabRoot').hexdigest()}")
print(f"BLAKE2b  ArabRoot  {hashlib.blake2b(b'ArabRoot', digest_size=32).hexdigest()}")
print(f"SHA-256  1 MB of zeros  {hashlib.sha256(bytes(1_000_000)).hexdigest()}")
```

والمخرج سوف يكون كما يلي:

```text
SHA-256  ArabRoot  145b7aa7713ff18611d79dcc24dcc734b84a22066fc96d413aa88912487912a7
SHA-256  Arabroot  f67bd948bffb297845e39fe5d348ec82b00043ea53c2048d7bf14623d38055e3
SHA3-256 ArabRoot  bbd7f73ab3fc9552ba77003ebfaa74d345a176c0fda016a629eb07107cf85460
BLAKE2b  ArabRoot  70469b4193815788e4f41917f8013e7f5e0561776edef5c279df07e619004ffe
SHA-256  1 MB of zeros  d29751f2649b32ff572b5e0a9f541ea660a50f94ff0beedfb0b692b924cc8025
```

في المخرج أعلاه لاحظ التالي:

- الفرق بين `ArabRoot` و`Arabroot` حرف واحد صغير، ومع ذلك لا يوجد أي تشابه بين البصمتين، فلا تستطيع أن تستنتج من قرب البصمتين قرب المدخلين.
- كل دالة تعطي بصمة مختلفة تماماً للكلمة نفسها، لذلك عندما تخزن هاشاً اكتب معه اسم الدالة، وإلا فلن يعرف أحد بعد سنتين كيف يتحقق منه.
- مليون بايت من الأصفار أعطت 32 بايت كما أعطتها كلمة من ثمانية حروف، فطول البصمة ثابت مهما كان طول المدخل.

### لماذا لم يعد MD5 وSHA-1 آمنين؟

الدالتان MD5 وSHA-1 سقطتا في الخاصية الثالثة، أي مقاومة التصادم. ففي 23 فبراير 2017 أعلن فريق من Google ومعهد CWI في أمستردام [أول تصادم عملي في SHA-1](https://security.googleblog.com/2017/02/announcing-first-sha1-collision.html?ref=arabroot.io) باسم SHAttered، ونشروا ملفي PDF مختلفين في المحتوى لهما بصمة SHA-1 نفسها، والتفاصيل في [الورقة البحثية](https://eprint.iacr.org/2017/190?ref=arabroot.io). وهذا يعني عملياً أن من يستطيع أن يجعلك توقع على الملف الأول يستطيع أن يقدم توقيعك على الملف الثاني، لأن أغلب أنظمة التوقيع توقع على الهاش وليس على الملف نفسه. وبعد ذلك أعلنت NIST في ديسمبر 2022 [إيقاف SHA-1](https://www.nist.gov/news-events/news/2022/12/nist-retires-sha-1-cryptographic-algorithm?ref=arabroot.io) وطلبت الانتقال إلى SHA-2 وSHA-3، مع موعد نهائي في 31 ديسمبر 2030\. أما MD5 فوضعه أسوأ لأن تصادماته أسهل بكثير، ولذلك لا يصلح لأي استخدام أمني على الإطلاق.

وقد يتساءل البعض: هل أحذف MD5 من كل مكان في الكود؟ والإجابة أن المهم هو السؤال الذي تستخدمه للإجابة عنه، فإذا كان الهدف اكتشاف ملف تالف أثناء النسخ بين قرصين، ولا يوجد طرف يحاول أن يخدعك، فإن مجموع التحقق Checksum يكفي. لكن بمجرد أن يصبح السؤال «هل غير أحدهم هذا الملف عمداً؟» أو تدخل البصمة في توقيع أو شهادة أو تخزين كلمة مرور، فإن MD5 وSHA-1 خارج النقاش، ولذلك الأسهل أن تستخدم SHA-256 في كل مكان فلا تحتاج أن تفكر في كل حالة على حدة.

### ماذا تستخدم اليوم؟ SHA-256 وSHA-3 وBLAKE2 وBLAKE3

- **SHA-256 وSHA-512 من عائلة SHA-2:** المعيار [FIPS 180-4](https://csrc.nist.gov/pubs/fips/180-4/upd1/final?ref=arabroot.io)، موجودة في كل لغة وكل نظام وكل جهاز، وتسرعها المعالجات الحديثة، وهي الاختيار الافتراضي إذا لم يكن لديك سبب لغيرها.
- **SHA-3:** المعيار [FIPS 202](https://csrc.nist.gov/pubs/fips/202/final?ref=arabroot.io)، مبنية على تصميم مختلف تماماً اسمه Keccak، فإذا ظهر ضعف في بنية SHA-2 يوماً ما فلن ينتقل إليها، ومن مزاياها بحسب [فريق Keccak](https://keccak.team/keccak%5Fstrengths.html?ref=arabroot.io) أنها لا تعاني من هجوم إطالة الرسالة Length Extension الذي نشرحه في القسم التالي.
- **BLAKE2 وBLAKE3:** دوال سريعة جداً بتصميم حديث، وBLAKE2 معرفة في [RFC 7693](https://www.rfc-editor.org/rfc/rfc7693?ref=arabroot.io) وموجودة في مكتبة Python القياسية كما رأيت، وBLAKE3 أسرع منها ويمكن تقسيم حسابه على عدة أنوية، ومكتبته الرسمية في [مستودع BLAKE3](https://github.com/BLAKE3-team/BLAKE3?ref=arabroot.io). وتناسب البصمات الكثيرة مثل فحص ملايين الملفات، لكنها ليست في قائمة FIPS، فإذا كان مشروعك يطلب FIPS فالتزم بـ SHA-2 أو SHA-3.

🛑

لا تقم باستخدام أي دالة من هذه الدوال السريعة لتخزين كلمات المرور، لا SHA-256 ولا SHA-3 ولا BLAKE3، والسبب أن سرعتها هي بالضبط ما يحتاجه المخترق ليجرب ملايين الكلمات في الثانية، كما سوف ترى بالأرقام في قسم كلمات المرور.

## سلامة الرسالة Integrity: لماذا لا يكفي الهاش، وما هو HMAC؟

### الطريقة البسيطة: نرسل الهاش مع الرسالة

لنفرض أن بوابة الدفع ترسل إلى تطبيقك Webhook فيه رقم الطلب والمبلغ، وتريد أن تتأكد أن أحداً في الطريق لم يغير المبلغ. والحل الأول الذي يخطر على البال أن ترسل البوابة مع الرسالة بصمة SHA-256 لها، فيحسب التطبيق البصمة ويقارن. والمشكلة أن المخترق الذي يستطيع تغيير الرسالة يستطيع أيضاً أن يحسب بصمتها الجديدة ويضعها مكان القديمة، فالهاش لا يحتاج أي سر، وبالتالي فالتطبيق سوف يجد بصمة صحيحة لرسالة مزورة.

والحل الثاني أن نضيف سراً مشتركاً قبل الرسالة ثم نحسب الهاش، أي `sha256(secret + message)`، وهذا يبدو صحيحاً لأن المخترق لا يعرف السر، لكنه يفشل مع SHA-256 وSHA-1 وMD5 بسبب هجوم إطالة الرسالة Length Extension Attack: فبنية هذه الدوال تسمح لمن يرى البصمة وطول الرسالة أن يضيف بيانات في نهايتها ويحسب البصمة الجديدة دون أن يعرف السر، وهذا ما يشير إليه [فريق Keccak](https://keccak.team/keccak%5Fstrengths.html?ref=arabroot.io) عندما يذكر أن SHA-1 وSHA-2 تحتاجان بنية HMAC المتداخلة لهذا السبب.

### الطريقة الصحيحة: HMAC والمقارنة في زمن ثابت

الحل الأنسب هو رمز التحقق من الرسالة المبني على الهاش Hash-based Message Authentication Code واختصاراً HMAC، المعرف في [RFC 2104](https://www.rfc-editor.org/rfc/rfc2104?ref=arabroot.io)، وهو يمزج المفتاح بالرسالة على مرحلتين بطريقة تمنع هجوم الإطالة، فلا يستطيع أحد أن يحسب الرمز الصحيح لرسالة جديدة دون المفتاح. وهذا بالضبط ما يفعله توقيع `HS256` في JWT كما شرحنا في [مقال الجلسات وJWT](https://arabroot.io/articles/%D8%A7%D9%84%D8%AC%D9%84%D8%B3%D8%A7%D8%AA-%D9%88%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D8%AA%D8%B9%D8%B1%D9%8A%D9%81-%D8%A7%D9%84%D8%A7%D8%B1%D8%AA%D8%A8%D8%A7%D8%B7-%D9%88-jwt-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#jwt-algorithms)، وما تستخدمه أغلب منصات الدفع والكود لتوقيع ال Webhooks. والمثال التالي يبين الفرق:

```python
import hmac, hashlib, secrets
key = secrets.token_bytes(32)
msg = b'{"order": 1042, "amount": 150}'
tag = hmac.new(key, msg, hashlib.sha256).hexdigest()
print("message     :", msg.decode())
print("SHA-256     :", hashlib.sha256(msg).hexdigest())
print("HMAC-SHA256 :", tag)

forged = b'{"order": 1042, "amount": 1}'
print("attacker recomputes SHA-256 of forged message:", hashlib.sha256(forged).hexdigest())
print("server checks HMAC of original:", hmac.compare_digest(tag, hmac.new(key, msg, hashlib.sha256).hexdigest()))
print("server checks HMAC of forged  :", hmac.compare_digest(tag, hmac.new(key, forged, hashlib.sha256).hexdigest()))
```

والمخرج سوف يكون كما يلي:

```text
message     : {"order": 1042, "amount": 150}
SHA-256     : 9d760e8cc93e419779f43dd35e65c0b0308400ee6da7e6d8ee983efdef00861d
HMAC-SHA256 : 368a92e49b683d33300fbaac02d8aead246fbc782de4f9801adfb73736d28925
attacker recomputes SHA-256 of forged message: 95178f4e4f1627145a62982619fb5e39b2a84cf8bc3c00e7e9adbc2262889486
server checks HMAC of original: True
server checks HMAC of forged  : False
```

في الإعداد أعلاه لاحظ التالي:

- المخترق غير المبلغ إلى 1 وحسب SHA-256 الجديد بسهولة، ولو كان التطبيق يتحقق بالهاش وحده لقبل الرسالة المزورة، أما HMAC للرسالة المزورة فلم يطابق لأن المخترق لا يملك المفتاح.
- المقارنة تمت بالدالة `hmac.compare_digest` وليس بـ `==`، والسبب أن المقارنة العادية تتوقف عند أول بايت مختلف، فيختلف زمنها قليلاً بحسب عدد البايتات الصحيحة، ومن يقيس هذا الفرق مرات كثيرة يستطيع أن يخمن الرمز بايتاً بعد بايت، وهذا هجوم التوقيت Timing Attack. والمقابل في .NET هو `CryptographicOperations.FixedTimeEquals`، وفي Java `MessageDigest.isEqual`، وفي Node.js `crypto.timingSafeEqual`.
- المفتاح ولد بـ `secrets.token_bytes(32)`، أي 32 بايت عشوائية حقيقية، وليس كلمة يكتبها إنسان.

## تخزين كلمات المرور Password Storage: من النص الصريح إلى Argon2id

هذا هو المثال الذي يجمع أغلب ما سبق، وفيه سوف نمشي مع الحلول التي تخطر على البال واحداً بعد الآخر، ونكسر كل واحد منها قبل أن نصل إلى الحل الصحيح. والهدف هنا واضح: إذا سرق أحدهم جدول المستخدمين كاملاً، عبر SQL Injection أو نسخة احتياطية منسية، فيجب أن يكون الوصول إلى كلمات المرور نفسها مكلفاً جداً.

وقبل أن نبدأ تذكر أن أفضل كلمة مرور هي التي لا تحتاج أن تخزنها أصلاً، وهذا ما شرحناه في [الدخول دون كلمة مرور Passkeys وFIDO2](https://arabroot.io/articles/%D8%A7%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%AF%D9%88%D9%86-%D9%83%D9%84%D9%85%D8%A9-%D9%85%D8%B1%D9%88%D8%B1-passkeys-%D9%88-fido2/)، لكن أغلب التطبيقات ما زالت تحتاج كلمات المرور اليوم ولو كخيار احتياطي.

### النص الصريح Plain Text، ثم التشفير

الطريقة الأولى أن تحفظ كلمة المرور كما هي، وهي أسوأ الطرق على الإطلاق، فكل من يرى الجدول يرى كلمات المرور، ومنهم موظف الدعم ومن يقرأ النسخة الاحتياطية، وبما أن أغلب الناس يكررون كلمة المرور نفسها في مواقع كثيرة فإن التسريب لا يتوقف عند تطبيقك. والطريقة الثانية أن تشفر كلمة المرور بمفتاح، وهي تبدو أفضل لكنها تفشل لسبب بسيط: التشفير باتجاهين، فالتطبيق يملك المفتاح ليتحقق، وبالتالي فكل من يصل إلى السيرفر يصل إلى المفتاح غالباً، ويفك كل الكلمات دفعة واحدة. والتطبيق في الحقيقة لا يحتاج أن يعرف كلمة المرور أبداً، وإنما يحتاج فقط أن يتحقق أن ما كتبه المستخدم الآن هو نفس ما كتبه عند التسجيل.

📌

في أكتوبر 2013 أعلنت Adobe عن اختراق وصل فيه المهاجمون إلى بيانات حسابات عملائها، وقالت المتحدثة باسم الشركة كما نقل [Krebs on Security](https://krebsonsecurity.com/2013/10/adobe-breach-impacted-at-least-38-million-users/?ref=arabroot.io) إن المهاجمين حصلوا على معرفات Adobe ID و«كلمات مرور مشفرة Encrypted» لنحو 38 مليون مستخدم نشط، فاضطرت الشركة إلى إعادة تعيين كلمات مرور كل هذه الحسابات. والدرس أن الكلمة المشفرة ليست أكثر أماناً من مفتاحها، وأن كلمات المرور لا تشفر وإنما تمرر بدالة هاش بطيئة مع Salt.

### الهاش السريع Fast Hash: لماذا يفشل SHA-256 هنا؟

الطريقة الثالثة أن تحفظ `SHA-256(password)`، وعند الدخول تحسب البصمة وتقارن، وهذه خطوة صحيحة في الاتجاه لأن الهاش لا يعود. لكن المخترق لا يحتاج أن يعكس الهاش، فهو يأخذ قائمة بملايين الكلمات المسربة سابقاً ويحسب بصمة كل واحدة ويقارن، والسؤال الوحيد هو كم كلمة يستطيع أن يجرب في الثانية. والمثال التالي يقيس ذلك على نواة واحدة داخل ال Container، والأرقام توضيحية Illustrative على جهازنا فقط، والمهم فيها هو النسبة بين الأسطر وليس الرقم نفسه:

```python
import hashlib, time
from argon2 import PasswordHasher

def per_second(name, fn, seconds=1.0):
    n, start = 0, time.perf_counter()
    while time.perf_counter() - start < seconds:
        fn(b"S3cret!%d" % n); n += 1
    print(f"{name:<10} {n / seconds:>12,.0f} hashes/second")

per_second("MD5", lambda p: hashlib.md5(p).digest())
per_second("SHA-1", lambda p: hashlib.sha1(p).digest())
per_second("SHA-256", lambda p: hashlib.sha256(p).digest())

ph = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)   # OWASP minimum
start = time.perf_counter()
for i in range(20):
    ph.hash(f"S3cret!{i}")
ms = (time.perf_counter() - start) / 20 * 1000
print(f"{'Argon2id':<10} {ms:>9.1f} ms per hash  ->  {1000 / ms:,.0f} hashes/second")
```

والمخرج سوف يكون كما يلي:

```text
MD5           1,174,887 hashes/second
SHA-1         1,110,383 hashes/second
SHA-256         604,521 hashes/second
Argon2id        32.5 ms per hash  ->  31 hashes/second
```

نواة واحدة في Python جربت أكثر من 600 ألف كلمة في الثانية مع SHA-256، وأكثر من مليون مع MD5، والمخترق الحقيقي لا يستخدم Python ولا نواة واحدة، وإنما بطاقات رسوميات GPU مصممة لهذا النوع من الحساب المتوازي، فالأرقام عنده أكبر بكثير. أما Argon2id بالمعاملات التي توصي بها OWASP فاحتاج 32 مللي ثانية للكلمة الواحدة، أي نحو 31 محاولة في الثانية على النواة نفسها، وهذا لا يزعج المستخدم الذي يدخل مرة واحدة، لكنه يغير حسابات المخترق الذي يريد أن يجرب مليار كلمة.

### الملح Salt والفلفل Pepper

وهناك مشكلة ثانية في الهاش السريع غير السرعة، والمثال التالي يبينها مع مستخدمين اختارا كلمة المرور نفسها:

```python
import hashlib
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

users = {"sara": "S3cret!", "omar": "S3cret!"}
print("1) plain SHA-256, no salt")
for u, p in users.items():
    print(f"   {u:<5} {hashlib.sha256(p.encode()).hexdigest()}")

ph = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)
print("2) Argon2id with a random salt per user")
stored = {}
for u, p in users.items():
    stored[u] = ph.hash(p)
    print(f"   {u:<5} {stored[u]}")

print("3) login")
print("   sara + S3cret!  ->", ph.verify(stored["sara"], "S3cret!"))
try:
    ph.verify(stored["sara"], "s3cret!")
except VerifyMismatchError as e:
    print("   sara + s3cret!  ->", type(e).__name__)
print("   needs rehash with stronger params?", PasswordHasher(time_cost=3, memory_cost=19456, parallelism=1).check_needs_rehash(stored["sara"]))
```

والمخرج سوف يكون كما يلي:

```text
1) plain SHA-256, no salt
   sara  88d3ff09991bae39cb844746c78ab09d4c62050857d336751e37c4b1d84a1d44
   omar  88d3ff09991bae39cb844746c78ab09d4c62050857d336751e37c4b1d84a1d44
2) Argon2id with a random salt per user
   sara  $argon2id$v=19$m=19456,t=2,p=1$YZap5FSHy4LI1VtL6wYXlg$0BIc8IKbR9X55h5GRvVxxiDA9BKSjian28Nmj6Bq08I
   omar  $argon2id$v=19$m=19456,t=2,p=1$Aykik+FPWNMjTbv3FDgDlg$MbfPNIR4MoV0oG7f3yzSuIlnX/lbWgAsXESKXfnnxWI
3) login
   sara + S3cret!  -> True
   sara + s3cret!  -> VerifyMismatchError
   needs rehash with stronger params? True
```

في المخرج أعلاه لاحظ التالي:

- مع SHA-256 دون Salt حصلت سارة وعمر على البصمة نفسها تماماً، بل هي نفس البصمة التي رأيتها في [مقال الترميز](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B1%D9%85%D9%8A%D8%B2-base64-%D9%88-base32-%D9%88-base58/#decode-demo) للكلمة `S3cret!`، أي أن أي شخص في العالم يحسب هذه البصمة يحصل على الرقم نفسه. وهذا يعني أن المخترق يحسب بصمات القائمة مرة واحدة فقط ويقارنها بكل المستخدمين معاً، أو يستخدم جداول جاهزة محسوبة مسبقاً Rainbow Tables، ويعرف فوراً من يشترك في كلمة واحدة.
- مع Argon2id اختلف الناتج تماماً للكلمة نفسها، والسبب الملح Salt، وهو قيمة عشوائية من 16 بايت تولد لكل مستخدم عند التسجيل وتدخل في الحساب. ولاحظ أن الملح ليس سراً، فهو مكتوب في النص المخزن نفسه بعد `$m=19456,t=2,p=1$`، ووظيفته أن يجعل كل مستخدم حالة مستقلة يحتاج المخترق أن يهاجمها وحدها، وأن يلغي الجداول المحسوبة مسبقاً.
- النص المخزن يحمل كل ما يلزم للتحقق: اسم الخوارزمية والإصدار والمعاملات والملح والبصمة، لذلك لا تحتاج أعمدة منفصلة، ولا تحتاج أن تتذكر في الكود المعاملات التي استخدمتها قبل سنتين.
- الدالة `check_needs_rehash` أعادت `True` عندما قارنا النص المخزن بمعاملات أقوى، وهذا ما تستخدمه لرفع المعاملات تدريجياً: عند كل دخول ناجح تتحقق، فإذا كانت المعاملات قديمة تحسب الهاش من جديد بالمعاملات الحالية وتحفظه، لأن كلمة المرور الصريحة بين يديك في هذه اللحظة فقط.

أما الفلفل Pepper فهو سر واحد مشترك لكل كلمات المرور، يحفظ خارج قاعدة البيانات في خزنة أسرار أو HSM، ويضاف إلى الحساب، فإذا سرق المخترق قاعدة البيانات وحدها، عبر SQL Injection مثلاً، فلن يستطيع أن يجرب أي كلمة لأنه لا يعرف الفلفل. وتصف [ورقة OWASP لتخزين كلمات المرور](https://cheatsheetseries.owasp.org/cheatsheets/Password%5FStorage%5FCheat%5FSheet.html?ref=arabroot.io) طريقتين له، إما قبل الهاش أو بعده بـ HMAC، مع تنبيه مهم: تغيير الفلفل يحتاج كلمات المرور الصريحة، فإذا تسرب فلا بد أن يعيد كل المستخدمين تعيين كلماتهم. لذلك الفلفل طبقة إضافية مفيدة وليس بديلاً عن الملح والخوارزمية البطيئة.

### Argon2id ومعاملاته Parameters بحسب OWASP وRFC 9106

الحل الأنسب اليوم هو دالة صممت لكلمات المرور تحديداً، أي دالة بطيئة عمداً وقابلة للضبط، وتحتاج ذاكرة كبيرة في كل محاولة Memory-Hard حتى لا تستفيد بطاقات GPU من آلاف أنويتها الصغيرة التي تملك ذاكرة قليلة لكل نواة. وهذه الدالة هي Argon2 التي فازت بمسابقة Password Hashing Competition في 2015، ونسختها الموصى بها Argon2id، وهي معرفة في [RFC 9106](https://www.rfc-editor.org/rfc/rfc9106.html?ref=arabroot.io). ولها ثلاثة معاملات: الذاكرة `m` بالكيلوبايت KiB، وعدد المرور على الذاكرة `t`، ودرجة التوازي `p`.

وتوصي [ورقة OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password%5FStorage%5FCheat%5FSheet.html?ref=arabroot.io#argon2id) باستخدام Argon2id بحد أدنى 19 ميجابايت من الذاكرة ومرورين ودرجة توازي واحدة، أي `m=19456, t=2, p=1`، وتذكر خيارات متكافئة تبادل الذاكرة بعدد المرور، مثل `m=47104 (46 MiB), t=1, p=1` و`m=12288 (12 MiB), t=3, p=1` و`m=9216 (9 MiB), t=4, p=1` و`m=7168 (7 MiB), t=5, p=1`. أما [RFC 9106](https://www.rfc-editor.org/rfc/rfc9106.html?ref=arabroot.io#section-4) نفسه فخياره الأول الموصى به أقوى بكثير، وهو `t=1` و`p=4` مع 2 جيجابايت من الذاكرة، وخياره الثاني لمن لديه ذاكرة أقل هو `t=3` و`p=4` مع 64 ميجابايت، وكلاهما بملح 128 بت وبصمة 256 بت.

وقد يتساءل البعض: أي الرقمين أختار؟ والإجابة أن معاملات OWASP حد أدنى لسيرفر ويب عادي يستقبل دخولاً متزامناً من مستخدمين كثيرين، فكل محاولة دخول تحجز 19 ميجابايت، ومئة محاولة في الوقت نفسه تحجز نحو 2 جيجابايت. لذلك ابدأ بمعاملات OWASP، ثم قس على سيرفرك الحقيقي، وارفع الذاكرة ما دام زمن الدخول مقبولاً والسيرفر يتحمل الحمل، وتنصح OWASP بأن يبقى حساب الهاش أقل من ثانية. ولا تنس أن تحمي صفحة الدخول بتحديد عدد المحاولات Rate Limiting، لأن الدالة البطيئة نفسها قد تستخدم لإنهاك السيرفر.

الصورة التالية تبين المسار كاملاً عند التسجيل وعند الدخول، والقيم فيها من مخرجات التجربة:

![مخطط بمسارين: في التسجيل يكتب المستخدم S3cret! ثم يولد Salt عشوائي من 16 بايت ثم Argon2id بالمعاملات m=19456 وt=2 وp=1 ثم يحفظ في جدول المستخدمين نص واحد مقسم بألوان إلى argon2id والإصدار والمعاملات والملح والبصمة، وفي الدخول يقرأ التطبيق النص المخزن ويأخذ منه الملح والمعاملات ثم يحسب Argon2id ثم يقارن في زمن ثابت فإما دخول مع إعادة الحساب إذا كانت المعاملات قديمة أو رفض، وفي الأسفل شريط يبين أن كل تخمين يكلف تشغيلاً كاملاً لكل مستخدم مقابل 600 ألف تخمين SHA-256 في الثانية](https://arabroot.io/content/images/2026/10/crypto-for-devs-02-password-flow.webp)

الملح يجعل كل مستخدم حالة مستقلة، وArgon2id يجعل كل محاولة مكلفة، والنص المخزن يحمل معاملاته معه

### bcrypt وحد 72 بايت

إذا كان نظامك يستخدم bcrypt فهو ليس كارثة، فـ OWASP تعتبره مقبولاً للأنظمة القديمة بعامل عمل Work Factor لا يقل عن 10، لكنه يحمل قيداً يجب أن تعرفه: أغلب تطبيقاته تقرأ أول 72 بايت فقط من كلمة المرور. والمثال التالي يبين كيف تتعامل المكتبة الحديثة مع هذا القيد:

```python
import bcrypt
base = "a" * 72
h = bcrypt.hashpw(base.encode(), bcrypt.gensalt(rounds=12))
print("bcrypt version:", bcrypt.__version__)
print("hash:", h.decode())
print("72 bytes, same password     ->", bcrypt.checkpw(base.encode(), h))
try:
    print("73 bytes, extra 'X' at end  ->", bcrypt.checkpw((base + "X").encode(), h))
except ValueError as e:
    print("73 bytes, extra 'X' at end  -> ValueError:", e)
arabic = "كلمة" * 9    # 36 Arabic letters, 2 bytes each in UTF-8
print("36 Arabic letters =", len(arabic.encode()), "bytes")
```

والمخرج سوف يكون كما يلي:

```text
bcrypt version: 5.0.0
hash: $2b$12$2BISf3aAa3OHs3vaw6whjO8a/ybMhRBS4oVVTzhyE/rf6/rwukX0G
72 bytes, same password     -> True
73 bytes, extra 'X' at end  -> ValueError: password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])
36 Arabic letters = 72 bytes
```

في المخرج أعلاه لاحظ التالي:

- الإصدار 5.0.0 من مكتبة bcrypt في Python يرفض صراحة أي كلمة أطول من 72 بايت بخطأ `ValueError`، وهذا سلوك جيد لأنه يجبرك على أن تقرر بنفسك. لكن مكتبات كثيرة في لغات أخرى، والإصدارات القديمة من هذه المكتبة نفسها، تقطع الكلمة بصمت، فتصبح كل كلمة تبدأ بنفس 72 بايت صحيحة.
- الحد بالبايت وليس بالحرف، والحرف العربي في UTF-8 بايتان كما شرحنا في [مقال الترميز](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B1%D9%85%D9%8A%D8%B2-base64-%D9%88-base32-%D9%88-base58/#charsets)، فكلمة مرور عربية من 36 حرفاً فقط تصل إلى الحد.
- إذا اضطررت لتمرير كلمات أطول فلا تقم بتمرير SHA-256 الخام لها إلى bcrypt، لأن البايت الصفري في الناتج يقطع الكلمة في بعض التطبيقات، وتصف ورقة OWASP الطريقة الآمنة بـ HMAC ثم Base64 قبل bcrypt، والحل الأبسط أن تنتقل إلى Argon2id.

📌

في 30 أكتوبر 2024 اكتشفت Okta داخلياً ثغرة في خدمة الدخول المفوض إلى AD/LDAP، وبحسب [إعلانها الأمني](https://trust.okta.com/security-advisories/okta-ad-ldap-delegated-authentication-username/?ref=arabroot.io) كانت الخدمة تولد مفتاح التخزين المؤقت Cache Key بحساب bcrypt على نص يجمع معرف المستخدم واسم المستخدم وكلمة المرور معاً، فإذا كان اسم المستخدم 52 حرفاً أو أكثر وكانت هناك جلسة ناجحة سابقة في الذاكرة المؤقتة، فقد يدخل المستخدم دون أن تتحقق كلمة المرور. والسبب كما يظهر من الأرقام أن المعرف واسم المستخدم الطويل يملآن أول 72 بايت تقريباً، فلا يصل شيء من كلمة المرور إلى bcrypt. وقد أصلحت Okta الثغرة في اليوم نفسه بالانتقال من bcrypt إلى PBKDF2.

### PBKDF2 عندما تحتاج FIPS

إذا كان مشروعك يطلب الالتزام بمعيار FIPS 140، وهو شائع في الجهات الحكومية والقطاع المالي، فإن Argon2 ليست في قائمة الخوارزميات المعتمدة، والخيار هنا PBKDF2، وتوصي OWASP بـ 600,000 تكرار Iterations مع HMAC-SHA-256، أو 220,000 مع HMAC-SHA-512\. وPBKDF2 ليس دالة تحتاج ذاكرة كبيرة، لذلك يستفيد المخترق من بطاقات GPU معه أكثر من Argon2id، فهو الخيار الأنسب عندما يفرضه المعيار فقط. وفي .NET تجد الدالة الثابتة `Rfc2898DeriveBytes.Pbkdf2`، وسوف ترى في [مثال C#](#csharp-example) أنها احتاجت نحو 337 مللي ثانية لـ 600,000 تكرار في تجربتنا.

⚠️

إذا كنت تستخدم ASP.NET Core Identity فراجع عدد التكرارات، فالقيمة الافتراضية في [PasswordHasherOptions.IterationCount](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions.iterationcount?ref=arabroot.io) هي 100,000 مع HMAC-SHA512 كما في [الكود المصدري](https://github.com/dotnet/aspnetcore/blob/main/src/Identity/Extensions.Core/src/PasswordHasherOptions.cs?ref=arabroot.io)، وهي أقل من 220,000 التي توصي بها OWASP لهذه الدالة، ويمكنك رفعها من الإعدادات دون كسر الكلمات القديمة لأن عدد التكرارات يحفظ مع كل هاش.

## التشفير المتماثل Symmetric Encryption: AES-GCM وChaCha20-Poly1305

ننتقل الآن من البيانات التي لا تحتاج أن ترجع إليها إلى البيانات التي تحتاج أن تقرأها لاحقاً، مثل رقم الهوية في عمود أو ملف يرفعه العميل أو نسخة احتياطية. والتشفير المتماثل يستخدم مفتاحاً واحداً للتشفير وفك التشفير، وهو سريع ويصلح لأي حجم من البيانات، والسؤال الذي يغفل عنه كثيرون: هل يكفي أن تكون البيانات مخفية؟

### التشفير الموثق Authenticated Encryption واختصاراً AEAD

التشفير القديم مثل AES في وضع CBC يخفي البيانات فقط، ولا يكشف إذا غير أحدهم بتاً في النص المشفر، فيفك التطبيق التشفير ويحصل على بيانات مختلفة دون أن يعلم، بل إن رسائل الخطأ عند فك تشفير نص معدل استخدمت تاريخياً لاستخراج النص الأصلي كاملاً فيما يسمى هجوم Padding Oracle. لذلك الحل الأنسب اليوم هو التشفير الموثق مع بيانات مرتبطة Authenticated Encryption with Associated Data واختصاراً AEAD، الذي يخفي البيانات ويضيف وسماً Tag يكشف أي تغيير فيها، وأشهر نوعين منه:

- **AES-GCM:** المعرف في [NIST SP 800-38D](https://csrc.nist.gov/pubs/sp/800/38/d/final?ref=arabroot.io)، وهو الأكثر انتشاراً، وأغلب المعالجات الحديثة تملك تعليمات خاصة تسرعه.
- **ChaCha20-Poly1305:** المعرف في [RFC 8439](https://www.rfc-editor.org/rfc/rfc8439?ref=arabroot.io)، ويعمل بسرعة جيدة حتى على الأجهزة التي لا تملك تسريعاً لـ AES، ونسخته XChaCha20-Poly1305 في [libsodium](https://doc.libsodium.org/secret-key%5Fcryptography/aead/chacha20-poly1305/xchacha20-poly1305%5Fconstruction?ref=arabroot.io) تستخدم Nonce بطول 192 بت يمكن توليده عشوائياً دون قلق من التكرار.

الصورة التالية تبين مدخلات AES-GCM ومخرجاته، وما يحدث عند فك التشفير:

![على اليسار أربعة مدخلات: مفتاح 256 بت سري من KMS، وNonce بطول 96 بت لا يتكرر مع المفتاح نفسه، والنص الصريح، والبيانات المرتبطة مثل user_id=42 غير مخفية، تدخل إلى صندوق AES-256-GCM، ويخرج منه ثلاثة أجزاء تخزن معاً: Nonce من 12 بايت ونص مشفر بطول النص الصريح ووسم من 16 بايت، ثم فك التشفير يتحقق من الوسم أولاً فإما النص الصريح أو InvalidTag دون أي مخرج، وفي الأسفل مربع أحمر يبين أن تكرار المفتاح والNonce يجعل XOR النصين المشفرين يساوي XOR النصين الصريحين ويكشف رسالة التحويل الثانية](https://arabroot.io/content/images/2026/10/crypto-for-devs-03-aes-gcm.webp)

ال Nonce والوسم يخزنان مع النص المشفر، والمفتاح وحده هو السر

### تجربة AES-GCM: التشفير ثم العبث بالنص

المثال التالي يشفر رقم هوية مع ربطه بمعرف المستخدم كبيانات مرتبطة Associated Data، ثم يحاول فك التشفير بعد ثلاثة أنواع من العبث:

```python
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag

key = AESGCM.generate_key(bit_length=256)
aes = AESGCM(key)
nonce = os.urandom(12)                      # 96-bit nonce, new for every message
aad = b"user_id=42"                         # authenticated, not encrypted
plain = "رقم الهوية 1234567890".encode()

ct = aes.encrypt(nonce, plain, aad)
print("plaintext       :", len(plain), "bytes")
print("ciphertext+tag  :", len(ct), "bytes (the last 16 are the tag)")
print("nonce (hex)     :", nonce.hex())
print("ciphertext (hex):", ct.hex())
print("decrypt         :", aes.decrypt(nonce, ct, aad).decode())

tampered = bytearray(ct); tampered[0] ^= 0x01         # flip one bit
cases = [("one bit flipped", aes, bytes(tampered), aad),
         ("other user_id  ", aes, ct, b"user_id=43"),
         ("wrong key      ", AESGCM(AESGCM.generate_key(bit_length=256)), ct, aad)]
for label, cipher, data, ad in cases:
    try:
        cipher.decrypt(nonce, data, ad)
        print(label, "-> decrypted?!")
    except InvalidTag:
        print(label, "-> InvalidTag")
```

والمخرج سوف يكون كما يلي:

```text
plaintext       : 30 bytes
ciphertext+tag  : 46 bytes (the last 16 are the tag)
nonce (hex)     : 9a203ceb34200be8d2a0d458
ciphertext (hex): 1570a9d603ac2b4fa23b261051acabb4e3aad50c8c284fb98783c0fa96d964489f7b02c58d601fe2b77d0d5915a4
decrypt         : رقم الهوية 1234567890
one bit flipped -> InvalidTag
other user_id   -> InvalidTag
wrong key       -> InvalidTag
```

في الإعداد أعلاه لاحظ التالي:

- النص الصريح 30 بايت لأن الحروف العربية بايتان، والناتج 46 بايت، أي النص المشفر بالطول نفسه ثم 16 بايت للوسم في النهاية، ولا يوجد حشو Padding كما في CBC.
- قلب بت واحد في النص المشفر جعل فك التشفير يفشل بخطأ `InvalidTag`، ولم يرجع أي جزء من النص، فالتطبيق لا يرى بيانات معدلة أبداً.
- البيانات المرتبطة `user_id=42` لم تشفر لكنها دخلت في الوسم، فعندما حاول أحدهم فك تشفير السجل نفسه باسم المستخدم 43 فشل، وهذا ما يمنع من ينسخ العمود المشفر من صف مستخدم إلى صف مستخدم آخر.
- ال Nonce ولد بـ `os.urandom(12)` لكل رسالة، وطوله 12 بايت أي 96 بت، وهو ليس سراً ويخزن بجانب النص المشفر لأنك تحتاجه عند فك التشفير.

### ال Nonce رقم لا يتكرر أبداً، فماذا يحدث إذا تكرر؟

كلمة Nonce اختصار لـ «Number used once»، أي رقم يستخدم مرة واحدة مع المفتاح نفسه، والسبب أن AES-GCM يولد من المفتاح وال Nonce سلسلة بايتات عشوائية الشكل Keystream ثم يمزجها مع النص بعملية XOR، فإذا تكرر المفتاح وال Nonce تكررت السلسلة نفسها. والمثال التالي يبين النتيجة على رسالتي تحويل مالي من صنعنا:

```python
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

key = AESGCM.generate_key(bit_length=256)
aes = AESGCM(key)
nonce = os.urandom(12)                         # the bug: one nonce reused for two messages
p1 = b"TRANSFER 0500 SAR TO ACCT 11111"
p2 = b"TRANSFER 9900 SAR TO ACCT 66666"
c1 = aes.encrypt(nonce, p1, None)[:-16]       # drop the tag, keep the ciphertext
c2 = aes.encrypt(nonce, p2, None)[:-16]

xor = lambda a, b: bytes(x ^ y for x, y in zip(a, b))
print("c1 xor c2   :", xor(c1, c2).hex())
print("p1 xor p2   :", xor(p1, p2).hex())
print("equal       :", xor(c1, c2) == xor(p1, p2))
# the attacker knows p1 (a fixed template or his own transfer) and never sees the key
print("recovered p2:", xor(xor(c1, c2), p1).decode())
```

والمخرج سوف يكون كما يلي:

```text
c1 xor c2   : 000000000000000000090c0000000000000000000000000000000707070707
p1 xor p2   : 000000000000000000090c0000000000000000000000000000000707070707
equal       : True
recovered p2: TRANSFER 9900 SAR TO ACCT 66666
```

المخترق لم يعرف المفتاح، ومع ذلك فإن XOR النصين المشفرين ساوى XOR النصين الصريحين بالضبط، فالسلسلة السرية ألغت نفسها، ولاحظ أن الأصفار في الناتج هي الأماكن التي تتطابق فيها الرسالتان، فمجرد النظر إليه يكشف أين تختلف الرسالتان. وبما أن الرسالة الأولى معروفة، لأنها قالب ثابت أو تحويل قام به المخترق نفسه، فقد استخرج الرسالة الثانية كاملة. والأسوأ أن تكرار ال Nonce في GCM تحديداً يكشف أيضاً مفتاح التحقق الداخلي Hash Subkey الذي يحسب به الوسم كما يشرح الملحق A في [NIST SP 800-38D](https://csrc.nist.gov/pubs/sp/800/38/d/final?ref=arabroot.io)، فيستطيع المخترق بعدها أن يزور رسائل يقبلها التطبيق، لذلك فهو ليس تسريباً لرسالة واحدة وإنما انهيار للنظام مع هذا المفتاح.

وكيف تضمن ألا يتكرر؟ هناك ثلاث طرق عملية:

- **Nonce عشوائي من 96 بت مع حد للرسائل:** وهو ما فعلناه، لكن احتمال التكرار يزيد مع عدد الرسائل، لذلك يشترط [NIST SP 800-38D](https://csrc.nist.gov/pubs/sp/800/38/d/final?ref=arabroot.io) في القسم 8.3 ألا يتجاوز عدد مرات التشفير بالمفتاح نفسه 2 أس 32 عندما يكون ال Nonce عشوائياً، أي نحو أربعة مليارات رسالة، ثم يجب تغيير المفتاح.
- **عداد Counter:** رقم يزيد مع كل رسالة، وهو مضمون عدم التكرار ما دام محفوظاً بشكل صحيح، لكنه يصبح خطيراً عندما تعمل عدة نسخ من التطبيق بالمفتاح نفسه أو تستعاد نسخة احتياطية قديمة من العداد.
- **خوارزمية تتحمل الخطأ:** XChaCha20-Poly1305 بـ Nonce من 192 بت يكفي توليده عشوائياً، أو AES-GCM-SIV المعرف في [RFC 8452](https://www.rfc-editor.org/rfc/rfc8452?ref=arabroot.io) الذي لا ينهار عند التكرار، فكل ما يخسره أن الرسالتين المتطابقتين تعطيان نصاً مشفراً متطابقاً.

### وضع ECB: لماذا يظهر الشكل من خلال التشفير؟

AES نفسه يشفر كتلة Block واحدة من 16 بايت فقط، ووضع التشغيل Mode of Operation هو الذي يحدد كيف تشفر رسالة أطول. وأبسط الأوضاع اسمه Electronic Codebook واختصاراً ECB، وهو يشفر كل كتلة وحدها بالمفتاح نفسه، ويبدو منطقياً للوهلة الأولى، والمشكلة أن الكتلتين المتطابقتين في النص تعطيان كتلتين متطابقتين في الناتج. ولنرى ذلك بأعيننا نرسم صورة بسيطة فيها قفل ومفتاح وخطوط بألوان مسطحة، ثم نشفر بكسلاتها الخام بـ ECB مرة وبـ GCM مرة بالمفتاح نفسه:

```python
# Draws our own pattern (a padlock and a key on stripes), then encrypts the raw pixels with AES-ECB and AES-GCM.
import os, zlib, struct
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

W, H = 480, 320                     # 480 * 3 = 1440 bytes per row, a multiple of the 16-byte AES block
ORANGE, DARK, WHITE, GREEN = (233, 84, 32), (44, 0, 30), (255, 255, 255), (31, 138, 85)

def pixel(x, y):
    # padlock body
    if 90 <= x < 230 and 140 <= y < 260: return ORANGE
    # padlock shackle (a thick ring, upper half)
    dx, dy = x - 160, y - 140
    if y < 140 and 45**2 <= dx*dx + dy*dy < 70**2: return DARK
    # key: ring and blade
    dx, dy = x - 320, y - 120
    if 25**2 <= dx*dx + dy*dy < 45**2: return GREEN
    if 300 <= x < 340 and 165 <= y < 280: return GREEN
    if 340 <= x < 380 and (210 <= y < 228 or 248 <= y < 266): return GREEN
    # stripes at the bottom
    if y >= 290: return DARK if (x // 40) % 2 == 0 else WHITE
    return WHITE

raw = bytes(c for y in range(H) for x in range(W) for c in pixel(x, y))

def png(path, data):
    rows = b"".join(b"\x00" + data[y*W*3:(y+1)*W*3] for y in range(H))
    chunk = lambda t, d: struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d))
    with open(path, "wb") as f:
        f.write(b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", W, H, 8, 2, 0, 0, 0))
                + chunk(b"IDAT", zlib.compress(rows, 9)) + chunk(b"IEND", b""))

key = os.urandom(32)
enc = Cipher(algorithms.AES(key), modes.ECB()).encryptor()
ecb = enc.update(raw) + enc.finalize()
gcm = AESGCM(key).encrypt(os.urandom(12), raw, None)[:len(raw)]

blocks = [raw[i:i+16] for i in range(0, len(raw), 16)]
eblocks = [ecb[i:i+16] for i in range(0, len(ecb), 16)]
print("pixel bytes          :", len(raw), "=", len(blocks), "AES blocks")
print("distinct plain blocks:", len(set(blocks)))
print("distinct ECB blocks  :", len(set(eblocks)))
print("distinct GCM blocks  :", len(set(gcm[i:i+16] for i in range(0, len(gcm), 16))))
for name, data in [("original", raw), ("ecb", ecb), ("gcm", gcm)]:
    png(f"/out/ecb-{name}.png", data)
print("written: /out/ecb-original.png /out/ecb-ecb.png /out/ecb-gcm.png")
```

والمخرج سوف يكون كما يلي:

```text
pixel bytes          : 460800 = 28800 AES blocks
distinct plain blocks: 72
distinct ECB blocks  : 72
distinct GCM blocks  : 28800
written: /out/ecb-original.png /out/ecb-ecb.png /out/ecb-gcm.png
```

الصورة الأصلية فيها 28,800 كتلة لكن 72 كتلة مختلفة فقط، لأن المساحات الملونة تتكرر، ومع ECB بقي العدد 72 بالضبط، أي أن كل كتلة بيضاء أصبحت الكتلة المشفرة نفسها في كل مكان، أما مع GCM فأصبحت كل الكتل مختلفة. والصورة التالية هي الملفات الثلاثة التي كتبها المثال كما هي:

![ثلاث صور متجاورة من تجربتنا: الأصلية فيها قفل برتقالي بحلقة داكنة ومفتاح أخضر وخطوط داكنة في الأسفل على خلفية بيضاء، ثم الصورة نفسها بعد AES-256-ECB تظهر فيها خطوط ملونة عمودية لكن شكل القفل والمفتاح والخطوط واضح تماماً، ثم بعد AES-256-GCM ضجيج ملون عشوائي لا يظهر فيه أي شكل](https://arabroot.io/content/images/2026/10/crypto-for-devs-04-ecb-vs-gcm.webp)

ECB أخفى الألوان لكنه أبقى الشكل، وGCM لم يترك شيئاً يرى

وهذه هي الفكرة نفسها التي اشتهرت بصورة البطريق في مراجع التشفير، وقد رسمنا صورتنا بأنفسنا حتى نعرف كل بكسل فيها. والدرس أن ECB لا يصلح لتشفير أي بيانات أطول من كتلة واحدة. والمشكلة أنه قد يصلك دون أن تطلبه، ففي Java إذا كتبت اسم الخوارزمية وحده دون الوضع اختار المزود الوضع الافتراضي، والمثال التالي يشفر 32 بايت من الأصفار بـ `Cipher.getInstance("AES")`:

```java
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import java.util.HexFormat;
public class EcbCheck {
    public static void main(String[] a) throws Exception {
        Cipher c = Cipher.getInstance("AES");
        c.init(Cipher.ENCRYPT_MODE, KeyGenerator.getInstance("AES").generateKey());
        byte[] out = c.doFinal(new byte[32]);
        String h = HexFormat.of().formatHex(out);
        System.out.println("provider " + c.getProvider().getName() + ", output " + out.length + " bytes");
        System.out.println("block1 " + h.substring(0, 32));
        System.out.println("block2 " + h.substring(32, 64));
        System.out.println("iv " + (c.getIV() == null ? "none" : "present"));
    }
}
```

```bash
docker run --rm --name lab-crypto-java-ecb -v "$PWD:/lab" eclipse-temurin:21-jdk-alpine java /lab/EcbCheck.java
```

والمخرج سوف يكون كما يلي:

```text
provider SunJCE, output 48 bytes
block1 ba8208e3ce150b5969a382d362d0b9ac
block2 ba8208e3ce150b5969a382d362d0b9ac
iv none
```

الكتلتان المشفرتان متطابقتان تماماً، ولا يوجد IV، والناتج 48 بايت لأن المزود أضاف كتلة حشو Padding، أي أن مزود SunJCE أعطانا ECB مع حشو PKCS5 دون أي تحذير. لذلك إذا رأيت `Cipher.getInstance("AES")` أو `AES/ECB` في كود Java فاعتبره خطأ أمنياً يجب إصلاحه.

## التشفير غير المتماثل Asymmetric: RSA والمنحنيات البيضاوية ECC

المشكلة التي تبقى مع التشفير المتماثل هي المفتاح نفسه: كيف يصل إلى الطرف الآخر دون أن يراه أحد؟ والتشفير غير المتماثل يحلها بزوج من المفاتيح، مفتاح عام Public Key تنشره لأي شخص، ومفتاح خاص Private Key لا يغادر جهازك أبداً، وما يفعله أحدهما لا يعكسه إلا الآخر. وقد شرحنا الفكرة الأساسية مع الشهادات في [مقال الشهادات الرقمية](https://arabroot.io/articles/%D8%A7%D9%84%D8%B4%D9%87%D8%A7%D8%AF%D8%A7%D8%AA-%D8%A7%D9%84%D8%B1%D9%82%D9%85%D9%8A%D8%A9-tls-%D9%88-x509-%D9%88%D8%B5%D9%8A%D8%BA-pem-%D9%88-der/#keys-signatures)، وهنا سوف نركز على ما يحتاجه المطور: أي خوارزمية، وبأي طول، ولأي غرض.

### RSA أم ECC؟ وأطوال المفاتيح Key Sizes بحسب NIST

هناك عائلتان رئيسيتان: RSA التي تعتمد على صعوبة تحليل عدد كبير إلى عوامله الأولية، والمنحنيات البيضاوية Elliptic Curve Cryptography واختصاراً ECC التي تعطي الأمان نفسه بمفاتيح أصغر بكثير. ويقيس المعيار [NIST SP 800-57 الجزء الأول](https://csrc.nist.gov/pubs/sp/800/57/pt1/r5/final?ref=arabroot.io) قوة كل خوارزمية بوحدة اسمها قوة الأمان Security Strength بالبت، والجدول التالي من الجدول 2 فيه:

| قوة الأمان Security Strength | المتماثل Symmetric | RSA (طول المعامل) | ECC (طول المنحنى) |
| ---------------------------- | ------------------ | ----------------- | ----------------- |
| 112 بت                       | 3TDEA (متوقف)      | 2048              | 224 إلى 255       |
| 128 بت                       | AES-128            | 3072              | 256 إلى 383       |
| 192 بت                       | AES-192            | 7680              | 384 إلى 511       |
| 256 بت                       | AES-256            | 15360             | 512 فأكثر         |

وبحسب الجدول 4 في المعيار نفسه فإن قوة 112 بت مقبولة لحماية البيانات حتى نهاية 2030، ومن 2031 يجب أن تكون 128 بت على الأقل لأي حماية جديدة، أي أن RSA-2048 الشائع اليوم على حدود عمره، وأن RSA-3072 أو منحنى بطول 256 بت هو الحد الأدنى لأي نظام جديد. ولاحظ الفرق في الحجم: منحنى بطول 256 بت يعطي ما يعطيه RSA بطول 3072 بت، والمثال التالي يقارن الأحجام الفعلية:

```python
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.exceptions import InvalidSignature

priv = Ed25519PrivateKey.generate()
pub = priv.public_key()
msg = b"release v2.4.1 sha256=9f2c41e1"
sig = priv.sign(msg)
raw = pub.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
print("Ed25519 public key:", len(raw), "bytes, signature:", len(sig), "bytes")
pub.verify(sig, msg); print("original message  -> valid")
try:
    pub.verify(sig, b"release v2.4.2 sha256=9f2c41e1")
except InvalidSignature:
    print("modified message  -> InvalidSignature")

rsa_key = rsa.generate_private_key(public_exponent=65537, key_size=3072)
pss = padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH)
rsa_sig = rsa_key.sign(msg, pss, hashes.SHA256())
print("RSA-3072 modulus  :", rsa_key.public_key().key_size // 8, "bytes, signature:", len(rsa_sig), "bytes")
```

والمخرج سوف يكون كما يلي:

```text
Ed25519 public key: 32 bytes, signature: 64 bytes
original message  -> valid
modified message  -> InvalidSignature
RSA-3072 modulus  : 384 bytes, signature: 384 bytes
```

المفتاح العام في Ed25519 كان 32 بايت والتوقيع 64 بايت، بينما في RSA-3072 كان المعامل 384 بايت والتوقيع 384 بايت أيضاً، وهذا الفرق يظهر في حجم التوكنات والشهادات وزمن التوليد. ولذلك فالاختيار الافتراضي اليوم:

- **Ed25519 للتوقيع:** المعرف في [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032?ref=arabroot.io)، سريع وتوقيعه حتمي Deterministic فلا يحتاج رقماً عشوائياً عند كل توقيع، وهذا يزيل نوعاً كاملاً من الأخطاء.
- **X25519 لتبادل المفاتيح:** المعرف في [RFC 7748](https://www.rfc-editor.org/rfc/rfc7748?ref=arabroot.io)، وهو ما تستخدمه أغلب اتصالات TLS 1.3 اليوم.
- **ECDSA P-256 وRSA-PSS بطول 3072:** عندما يفرضها نظام آخر أو معيار أو جهاز لا يدعم Ed25519، وفي RSA استخدم الحشو OAEP للتشفير وPSS للتوقيع، وليس الحشو القديم PKCS#1 v1.5 في أي نظام جديد.

### التوقيع ليس تشفيراً Signature vs Encryption

وهنا خلط شائع يجب أن نزيله: في التشفير غير المتماثل يشفر المرسل بالمفتاح العام للمستقبل، فلا يفك الرسالة إلا صاحب المفتاح الخاص، والهدف السرية. أما في التوقيع فالعكس، فصاحب المفتاح الخاص يوقع، وأي شخص يملك المفتاح العام يتحقق، والرسالة نفسها تبقى مقروءة للجميع، والهدف إثبات المصدر وسلامة الرسالة. وفي المخرج أعلاه رأيت التوقيع يمر على الرسالة الأصلية، ثم يفشل بخطأ `InvalidSignature` عندما تغير رقم الإصدار من `v2.4.1` إلى `v2.4.2`، رغم أن الرسالة لم تكن مخفية أصلاً.

وهذا التوقيع هو ما يحمي Docker Image في سلسلة التوريد كما رأيت مع [Cosign في مقال أمن سلسلة التوريد](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D8%B3%D9%84%D8%B3%D9%84%D8%A9-%D8%A7%D9%84%D8%AA%D9%88%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#lab-sign)، وما يحمي معاملات البلوكتشين في [قسم التوقيع](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D9%84%D9%88%D9%83%D8%AA%D8%B4%D9%8A%D9%86-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D9%88%D9%85%D8%AA%D9%89-%D9%84%D8%A7-%D8%AA%D8%AD%D8%AA%D8%A7%D8%AC%D9%87/#signatures)، وما يجعل Passkey لا يرسل أي سر إلى الموقع كما في [مقال Passkeys](https://arabroot.io/articles/%D8%A7%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%AF%D9%88%D9%86-%D9%83%D9%84%D9%85%D8%A9-%D9%85%D8%B1%D9%88%D8%B1-passkeys-%D9%88-fido2/#the-idea)، وما يثبت توكنات `RS256` و`ES256` في JWT.

### تبادل المفاتيح Key Exchange والتشفير الهجين Hybrid Encryption

التشفير غير المتماثل بطيء ولا يصلح إلا لمدخلات صغيرة، لذلك لا يشفر به أحد ملفاً من جيجابايت، وإنما يستخدم للاتفاق على مفتاح متماثل ثم يكمل AES أو ChaCha20 العمل، وهذا ما يسمى التشفير الهجين Hybrid Encryption. والمثال التالي يبين الاتفاق على المفتاح مع X25519، ثم يشتق منه مفتاح جلسة بدالة [HKDF](https://www.rfc-editor.org/rfc/rfc5869?ref=arabroot.io)، والجزء الثاني منه عن تشفير الظرف الذي سوف نشرحه في قسم إدارة المفاتيح:

```python
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes

print("== X25519 key exchange")
alice, bob = X25519PrivateKey.generate(), X25519PrivateKey.generate()
s1 = alice.exchange(bob.public_key())
s2 = bob.exchange(alice.public_key())
print("alice shared secret:", s1.hex()[:32], "...")
print("bob   shared secret:", s2.hex()[:32], "...")
k = HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=b"arabroot demo v1").derive(s1)
print("same secret:", s1 == s2, "| session key from HKDF:", k.hex()[:16], "...")

print("== envelope encryption")
kek_v1 = AESGCM.generate_key(bit_length=256)          # lives in the KMS/HSM and never leaves it
dek = AESGCM.generate_key(bit_length=256)             # one data key per file or record
n = os.urandom(12)
data = AESGCM(dek).encrypt(n, b"customer export 2026-10.csv ...", None)
wn = os.urandom(12)
wrapped = AESGCM(kek_v1).encrypt(wn, dek, b"kek=v1")
print("stored: ciphertext", len(data), "bytes + wrapped DEK", len(wrapped), "bytes + key id v1")

kek_v2 = AESGCM.generate_key(bit_length=256)          # rotation: unwrap with v1, wrap with v2
dek_again = AESGCM(kek_v1).decrypt(wn, wrapped, b"kek=v1")
wn2 = os.urandom(12)
wrapped2 = AESGCM(kek_v2).encrypt(wn2, dek_again, b"kek=v2")
print("rotation to v2: the data stays as is, only the", len(wrapped2), "byte wrapped DEK is replaced")
dek2 = AESGCM(kek_v2).decrypt(wn2, wrapped2, b"kek=v2")
print("read with v2:", AESGCM(dek2).decrypt(n, data, None).decode())
```

والمخرج سوف يكون كما يلي:

```text
== X25519 key exchange
alice shared secret: 754306b3174bac8324fe3aff40e29391 ...
bob   shared secret: 754306b3174bac8324fe3aff40e29391 ...
same secret: True | session key from HKDF: dd6e11b2fb3822ac ...
== envelope encryption
stored: ciphertext 47 bytes + wrapped DEK 48 bytes + key id v1
rotation to v2: the data stays as is, only the 48 byte wrapped DEK is replaced
read with v2: customer export 2026-10.csv ...
```

في الجزء الأول لاحظ أن Alice وBob لم يرسلا إلا المفتاحين العامين، ومع ذلك حسب كل منهما السر نفسه، ومن يرى المفتاحين العامين فقط لا يستطيع حسابه. ولاحظ أيضاً أننا لم نستخدم السر الناتج مباشرة كمفتاح AES، وإنما مررناه على HKDF مع وصف لغرضه، لأن ناتج X25519 ليس موزعاً بشكل متساو على كل البتات، وHKDF يحوله إلى مفتاح مناسب ويسمح باشتقاق مفاتيح مختلفة لأغراض مختلفة من السر نفسه. وهذا بالضبط ما يحدث في مصافحة TLS 1.3 التي شرحناها في [مقال الشهادات](https://arabroot.io/articles/%D8%A7%D9%84%D8%B4%D9%87%D8%A7%D8%AF%D8%A7%D8%AA-%D8%A7%D9%84%D8%B1%D9%82%D9%85%D9%8A%D8%A9-tls-%D9%88-x509-%D9%88%D8%B5%D9%8A%D8%BA-pem-%D9%88-der/#tls-handshake)، مع سؤال لا يحله تبادل المفاتيح وحده: هل هذا المفتاح العام فعلاً لمن يدعي أنه له؟ وهنا يأتي التوقيع والشهادة.

الصورة التالية تجمع الأنواع الثلاثة: المتماثل وغير المتماثل والجمع بينهما في تشفير الظرف:

![ثلاث لوحات: الأولى Symmetric فيها Alice وBob بمفتاح أخضر واحد مشترك، سريع لأي حجم والمشكلة كيف وصل المفتاح إلى Bob، والثانية Asymmetric فيها Alice وBob يتبادلان المفتاحين العامين ويحتفظ كل منهما بمفتاحه الخاص الأحمر ويحسبان سراً واحداً ثم HKDF، والمشكلة البطء وهل المفتاح العام فعلاً لـ Bob، والثالثة Envelope encryption فيها KMS أو HSM يحتفظ بمفتاح KEK لا يغادره، ومفتاح DEK عشوائي يشفر الملف إلى data.enc، والمفتاح المغلف مع معرف المفتاح يخزن بجانب البيانات، وتدوير KEK يعيد تغليف DEK فقط، والسارق يحصل على بيانات مشفرة ومفتاح مغلف لا فائدة منهما دون KMS](https://arabroot.io/content/images/2026/10/crypto-for-devs-05-sym-asym-envelope.webp)

المتماثل للبيانات، وغير المتماثل للاتفاق على المفتاح وللتوقيع، والظرف يبقي المفتاح الرئيسي داخل KMS

## العشوائية Randomness: CSPRNG مقابل random وMath.random

كل ما سبق يعتمد على أرقام عشوائية: الملح والمفتاح وال Nonce ورمز إعادة تعيين كلمة المرور ومعرف الجلسة، فإذا استطاع المخترق أن يتوقع هذه الأرقام سقط كل شيء مهما كانت الخوارزمية قوية. والخطأ الشائع أن يستخدم المطور مولد الأرقام العادي في لغته، مثل `random` في Python و`Math.random` في JavaScript و`rand` في C، وهذه مولدات صممت للمحاكاة والألعاب، فهي سريعة ومتساوية التوزيع لكنها قابلة للتوقع. وقد رأيت هذا الخطأ تحديداً في [مقال تحليل الكود الثابت SAST](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D9%83%D9%88%D8%AF-%D8%A7%D9%84%D8%AB%D8%A7%D8%A8%D8%AA-sast-%D9%85%D8%B9-semgrep-%D9%88-sonarqube/#lab-sonarqube)، حيث أضفنا ثغرة VULN-13 فيها رمز إعادة تعيين كلمة المرور يولد بـ `random.randint`، وأمسكها SonarQube بالقاعدة `python:S2245`. والسؤال الآن: هل هي ثغرة حقيقية أم تحذير نظري؟

توثيق [وحدة random](https://docs.python.org/3/library/random.html?ref=arabroot.io) نفسه يقول إنها تعتمد على مولد Mersenne Twister، وإنه غير مناسب إطلاقاً للأغراض التشفيرية. والمثال التالي يبين لماذا: المخترق يرى 624 قيمة من المولد، مثل رموز طلبها لحسابه هو، ثم يعيد بناء حالته الداخلية ويتوقع القيم التالية:

```python
# Python's random is Mersenne Twister (MT19937): 624 outputs of 32 bits reveal its whole state.
import random, secrets

def untemper(y):
    y ^= y >> 18
    y ^= (y << 15) & 0xEFC60000
    x = y
    for _ in range(5):
        x = y ^ ((x << 7) & 0x9D2C5680)
    y = x
    x = y
    for _ in range(3):
        x = y ^ (x >> 11)
    return x & 0xFFFFFFFF

server = random.Random()          # seeded from the OS, exactly like the module-level random functions
observed = [server.getrandbits(32) for _ in range(624)]   # values the attacker saw, e.g. his own tokens

clone = random.Random()
clone.setstate((3, tuple(untemper(v) for v in observed) + (624,), None))

print("next 3 server values :", [server.getrandbits(32) for _ in range(3)])
print("attacker predictions :", [clone.getrandbits(32) for _ in range(3)])
print("next reset code      :", server.randint(100000, 999999), "| predicted:", clone.randint(100000, 999999))
print("secrets.token_urlsafe:", secrets.token_urlsafe(32))
```

والمخرج سوف يكون كما يلي:

```text
next 3 server values : [1330436998, 917878504, 2916138287]
attacker predictions : [1330436998, 917878504, 2916138287]
next reset code      : 442723 | predicted: 442723
secrets.token_urlsafe: 3NXvx3JedvdlQp54AxFd3erOnMJzu86ao32hvegldcQ
```

توقع المخترق القيم الثلاث التالية بالضبط، ثم توقع رمز إعادة التعيين التالي `442723` الذي سوف يرسل إلى بريد مستخدم آخر، وكل ذلك دون أن يعرف البذرة Seed الأولى التي جاءت من نظام التشغيل. والسبب أن هذا المولد دالة خطية يمكن عكسها، فكل قيمة تخرج منه تكشف جزءاً من حالته. أما المولد الآمن تشفيرياً Cryptographically Secure Pseudo-Random Number Generator واختصاراً CSPRNG فيأخذ عشوائيته من نظام التشغيل، ولا تكشف قيمه السابقة شيئاً عن القادمة، وفي كل لغة له اسم واضح:

| اللغة                | لا تستخدم                       | استخدم                                                                                                                  |
| -------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Python               | random                          | [secrets](https://docs.python.org/3/library/secrets.html?ref=arabroot.io) مثل secrets.token\_urlsafe(32)، أو os.urandom |
| JavaScript / Node.js | Math.random()                   | crypto.getRandomValues() في المتصفح، وcrypto.randomBytes() وcrypto.randomUUID() في Node.js                              |
| C# / .NET            | System.Random                   | RandomNumberGenerator.GetBytes() وRandomNumberGenerator.GetInt32()                                                      |
| Java                 | java.util.Random وMath.random() | java.security.SecureRandom                                                                                              |
| C / Linux            | rand() مع srand(time(NULL))     | getrandom() أو /dev/urandom                                                                                             |

وتوثيق [Math.random في MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Math/random?ref=arabroot.io) يحذر صراحة من استخدامه لأي شيء له علاقة بالأمان. ولاحظ أن المولد الآمن قد يكون مكسوراً أيضاً إذا عبث أحد بمصدره، كما في القصة التالية.

📌

في مايو 2008 تبين أن نسخة OpenSSL في Debian والتوزيعات المبنية عليها، من الإصدار 0.9.8c-1 حتى ما قبل 0.9.8g-9، تولد أرقاماً عشوائية يمكن توقعها، وهذا بحسب وصف الثغرة يجعل تخمين المفاتيح المولدة على هذه الأنظمة بالتجربة Brute Force أسهل بكثير، ومنها مفاتيح SSH ومفاتيح الشهادات التي تولدها OpenSSL. والثغرة مسجلة باسم [CVE-2008-0166](https://nvd.nist.gov/vuln/detail/cve-2008-0166?ref=arabroot.io)، وتحديث الحزمة وحده لا يكفي في مثل هذه الحالة، لأن المفاتيح الضعيفة التي ولدت قبل التحديث تبقى ضعيفة حتى تستبدل. والدرس أن تترك العشوائية لمصدر نظام التشغيل دون تعديل، وأن تعرف أين ولد كل مفتاح حتى تستطيع أن تستبدله.

## إدارة المفاتيح Key Management: أين يعيش المفتاح؟

بعد أن اخترت AES-GCM بشكل صحيح يبقى السؤال الذي يحدد أمان كل ما سبق: أين المفتاح؟ فالتشفير لا يزيل السر، وإنما يحول مشكلة حماية بيانات كبيرة إلى مشكلة حماية مفتاح صغير، فإذا كان المفتاح مكتوباً بجانب البيانات فلم تحم أي شيء.

### الطريقة البسيطة: المفتاح في الكود أو ملف الإعداد

أسهل مكان يضع فيه المطور المفتاح هو الكود نفسه، ثم ملف `.env`، ثم متغير بيئة في ملف Compose داخل المستودع، وكلها تنتهي بالطريقة نفسها: المفتاح في تاريخ Git، أو في طبقة من طبقات ال Docker Image، أو في سجل خط CI، وقد شرحنا طرق التسرب هذه وكيف تكتشفها وماذا تفعل بعدها في [مقال اكتشاف الأسرار المسربة](https://arabroot.io/articles/%D8%A7%D9%83%D8%AA%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%B3%D8%B1%D8%A7%D8%B1-%D8%A7%D9%84%D9%85%D8%B3%D8%B1%D8%A8%D8%A9-%D9%85%D8%B9-gitleaks-%D9%88-trufflehog/#leak-paths). والقاعدة: لا تقم بكتابة أي مفتاح في الكود أو في ملف داخل المستودع، والسبب أن الكود ينسخ ويشارك ويبقى في التاريخ إلى الأبد، والمفتاح يجب أن يكون له مكان واحد يتحكم فيه من يملكه.

### KMS وHSM وتشفير الظرف Envelope Encryption

المكان الصحيح للمفتاح الرئيسي هو نظام مخصص لإدارة المفاتيح Key Management Service واختصاراً KMS، أو وحدة أمان عتادية Hardware Security Module واختصاراً HSM، أو خزنة أسرار مثل OpenBao أو Infisical التي قارنا بينها في [مقال إدارة الهوية والوصول](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%87%D9%88%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-iam-%D9%88%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%85%D9%8A%D8%B2-pam/#tools). والفكرة المهمة في هذه الأنظمة أن المفتاح الرئيسي لا يغادرها أبداً، فالتطبيق يرسل لها البيانات فتشفرها أو تفكها وترد النتيجة، ويسجل كل طلب في سجل تدقيق.

لكن إرسال كل ملف إلى KMS لتشفيره بطيء ومكلف، والحل هو تشفير الظرف Envelope Encryption الذي يصفه توثيق [AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/kms-cryptography.html?ref=arabroot.io) و[Google Cloud KMS](https://cloud.google.com/kms/docs/envelope-encryption?ref=arabroot.io): يولد التطبيق مفتاح بيانات عشوائياً Data Encryption Key واختصاراً DEK لكل ملف أو سجل، ويشفر به البيانات محلياً، ثم يطلب من KMS أن يغلف DEK بالمفتاح الرئيسي Key Encryption Key واختصاراً KEK، ويخزن DEK المغلف بجانب البيانات ويحذف النسخة الصريحة من الذاكرة. وقد رأيت ذلك في الجزء الثاني من مخرج `envelope.py` أعلاه، فالمخزن هو النص المشفر مع DEK مغلف من 48 بايت ومعرف المفتاح `v1`، ومن يسرق قاعدة البيانات يحصل على بيانات مشفرة ومفتاح مغلف لا يفيدانه دون طلب إلى KMS، وهذا الطلب يحتاج صلاحية ويسجل.

### التدوير Rotation ودورة حياة المفتاح

وميزة الظرف الكبرى تظهر عند تدوير المفتاح، ففي المخرج نفسه انتقلنا من KEK الإصدار `v1` إلى `v2` دون أن نعيد تشفير البيانات نفسها، وإنما فككنا غلاف DEK بالإصدار القديم وغلفناه بالجديد، أي 48 بايت لكل سجل بدلاً من إعادة تشفير قاعدة البيانات كاملة. ولذلك احفظ دائماً معرف المفتاح Key ID مع كل نص مشفر، حتى يعرف التطبيق بأي إصدار يفك التشفير خلال فترة الانتقال. ويحدد [NIST SP 800-57](https://csrc.nist.gov/pubs/sp/800/57/pt1/r5/final?ref=arabroot.io) لكل نوع مفتاح مدة استخدام Cryptoperiod ينتهي بعدها، وتجمع [ورقة OWASP لإدارة المفاتيح](https://cheatsheetseries.owasp.org/cheatsheets/Key%5FManagement%5FCheat%5FSheet.html?ref=arabroot.io) ما يحتاجه المطور منه. والصورة التالية تبين مراحل حياة المفتاح:

![حلقة من ست مراحل: Generate بمولد آمن أو داخل HSM، ثم Store في KMS أو HSM أو OpenBao وليس في الكود أو Git، ثم Use بغرض واحد لكل مفتاح وأقل صلاحية مع تسجيل كل استخدام، ثم Rotate بإصدار جديد ومعرف المفتاح في البيانات حسب مدة الاستخدام، ثم Retire لفك أو التحقق من البيانات القديمة فقط، ثم Destroy لكل النسخ والنسخ الاحتياطية، وعلى اليسار مربع أحمر Leaked لمفتاح تسرب في Commit أو سجل أو تذكرة أو طبقة Image يقفز بسهم متقطع أحمر إلى Rotate مباشرة](https://arabroot.io/content/images/2026/10/crypto-for-devs-06-key-lifecycle.webp)

المفتاح المسرب لا ينتظر موعد تدويره، وإتلاف المفتاح يعني إتلاف كل ما شفر به

وفي هذه الصورة لاحظ مرحلتين يغفل عنهما كثيرون: التقاعد Retire، حيث يبقى المفتاح القديم متاحاً لفك البيانات القديمة أو التحقق من التوقيعات القديمة فقط، دون أن يستخدم لأي تشفير جديد، والإتلاف Destroy الذي يعني أن كل البيانات المشفرة به تصبح غير قابلة للقراءة، وهذا مفيد عمداً عندما تريد حذف بيانات عميل من كل النسخ الاحتياطية دفعة واحدة، فتتلف مفتاحه فقط، وخطير جداً إذا حدث بالخطأ.

## التشفير ما بعد الحوسبة الكمية Post-Quantum Cryptography باختصار

الحاسوب الكمي Quantum Computer الكبير بما يكفي، إذا بني يوماً ما، يستطيع أن يكسر RSA والمنحنيات البيضاوية، ولذلك يضع معيار NIST SP 800-57 علامة على أعمدة RSA وECC في جدوله مع ملاحظة أن تقديرات قوتها سوف تتأثر بشدة عندما تصبح الحوسبة الكمية واقعاً عملياً، بينما لا يضع العلامة نفسها على عمود الخوارزميات المتماثلة. والخطر ليس في المستقبل فقط، فمن يسجل حركة مشفرة اليوم يستطيع أن يفكها بعد سنوات، وهذا ما يسمى «احفظ الآن وفك لاحقاً Harvest Now, Decrypt Later»، وهو مهم للبيانات التي يجب أن تبقى سرية لفترة طويلة.

وفي 13 أغسطس 2024 [نشرت NIST](https://www.nist.gov/news-events/news/2024/08/nist-releases-first-3-finalized-post-quantum-encryption-standards?ref=arabroot.io) أول ثلاثة معايير نهائية:

- [FIPS 203](https://csrc.nist.gov/pubs/fips/203/final?ref=arabroot.io) لخوارزمية ML-KEM، وهي لتبادل المفاتيح، ومبنية على CRYSTALS-Kyber.
- [FIPS 204](https://csrc.nist.gov/pubs/fips/204/final?ref=arabroot.io) لخوارزمية ML-DSA، وهي التوقيع الرئيسي، ومبنية على CRYSTALS-Dilithium.
- [FIPS 205](https://csrc.nist.gov/pubs/fips/205/final?ref=arabroot.io) لخوارزمية SLH-DSA، وهي توقيع احتياطي مبني على الهاش، ومبنية على SPHINCS+.

والانتقال يحدث الآن بالطريقة الهجينة Hybrid، أي أن مصافحة TLS تجمع X25519 التقليدية مع ML-KEM-768 في آلية واحدة اسمها X25519MLKEM768، فإذا ظهر ضعف في إحداهما بقيت الأخرى، وقد أصبحت هذه الآليات معياراً في [RFC 10024](https://www.rfc-editor.org/rfc/rfc10024.html?ref=arabroot.io) في أغسطس 2026، وجعلتها [OpenSSL 3.5](https://openssl-library.org/news/openssl-3.5-notes/?ref=arabroot.io) من المفاتيح المعروضة افتراضياً في TLS، وبدأ Chrome استخدام ML-KEM بدلاً من Kyber من الإصدار 131 كما في [مدونة Google الأمنية](https://security.googleblog.com/2024/09/a-new-path-for-kyber-on-web.html?ref=arabroot.io). وفي مسودة [NIST IR 8547](https://csrc.nist.gov/pubs/ir/8547/ipd?ref=arabroot.io) المنشورة في نوفمبر 2024 تقترح NIST إيقاف التوقيعات وتبادل المفاتيح التقليدية بقوة 112 بت بعد 2030، ومنع RSA وECC كلياً بعد 2035.

وماذا يعني هذا للمطور اليوم؟ لا تكتب ML-KEM بنفسك، وإنما حدث مكتبة TLS ونظام التشغيل، فأغلب الفائدة تأتيك من هناك دون تعديل في الكود. وإذا أردت استخدام هذه الخوارزميات مباشرة فهي موجودة في [.NET 10](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10/libraries?ref=arabroot.io) بالأنواع `MLKem` و`MLDsa` و`SlhDsa`، وفي Java من الإصدار 24 مع [JEP 496](https://openjdk.org/jeps/496?ref=arabroot.io) و[JEP 497](https://openjdk.org/jeps/497?ref=arabroot.io). لكن لاحظ في [مثال C#](#csharp-example) أن `MLKem.IsSupported` أعاد `False` في Docker Image الرسمية لـ .NET SDK 10، والسبب أن نظامها يحمل OpenSSL 3.0.13، بينما يحتاج .NET نسخة 3.5 أو أحدث على Linux، فالدعم في المكتبة لا يكفي إذا كان نظام التشغيل تحتها قديماً. وأهم خطوة عملية الآن هي المرونة التشفيرية Crypto Agility: اكتب اسم الخوارزمية ومعرف المفتاح مع كل نص مشفر وتوقيع، حتى تستطيع أن تغير الخوارزمية لاحقاً دون أن تفقد بياناتك القديمة.

## لماذا لا تخترع خوارزميتك؟ المكتبات Libraries لكل لغة

كل الأخطاء التي رأيتها في هذا المقال حدثت بخوارزميات قوية، فـ AES لم ينكسر في تجربة ECB، وGCM لم ينكسر عندما تكرر ال Nonce، وMersenne Twister يعمل تماماً كما صمم. والمشكلة كانت دائماً في طريقة الاستخدام: وضع خاطئ، أو رقم تكرر، أو مولد لغرض آخر، أو مقارنة بـ `==`. فإذا كانت هذه الأخطاء تحدث مع الخوارزميات المعروفة، فتخيل ما يحدث مع خوارزمية كتبها مطور في أسبوع، دون أن يراجعها أحد ممن يعرفون كيف تكسر. وقد يبدو لك أن الخوارزمية «السرية» أصعب على المخترق، والإجابة أن الأمان يجب أن يأتي من سرية المفتاح وحده، أما الخوارزمية فتفترض أن المخترق يعرفها، وهذا ما يجعل AES مفتوحاً للجميع منذ عقود ولم يكسره أحد.

والقاعدة العملية: لا تقم بكتابة خوارزمية تشفير، ولا تجمع الأجزاء المنخفضة المستوى بنفسك إذا وجدت دالة عالية المستوى تفعل ما تريد، مثل `PasswordHasher` في argon2-cffi أو `AESGCM` في cryptography أو `crypto_secretbox` في libsodium، لأن هذه الدوال تختار الوضع والطول والحشو نيابة عنك.

### جدول المكتبات Libraries وتراخيصها

| اللغة     | التشفير والتوقيع                                                                                                                                                                                                        | كلمات المرور                                                                                                                                                                                                                                                                                   | ملاحظات                                                                         |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| أي لغة    | [libsodium](https://doc.libsodium.org/?ref=arabroot.io) بترخيص ISC، وله واجهات في أغلب اللغات                                                                                                                           | crypto\_pwhash بـ Argon2id                                                                                                                                                                                                                                                                     | واجهة عالية المستوى صعبة الاستخدام الخاطئ                                       |
| Python    | [cryptography](https://cryptography.io/?ref=arabroot.io) بترخيص Apache-2.0 أو BSD، و[PyNaCl](https://github.com/pyca/pynacl?ref=arabroot.io) بترخيص Apache-2.0 فوق libsodium                                            | [argon2-cffi](https://github.com/hynek/argon2-cffi?ref=arabroot.io) بترخيص MIT، وArgon2id في cryptography من الإصدار 44.0.0                                                                                                                                                                    | Argon2id في cryptography يحتاج OpenSSL 3.2 أو أحدث                              |
| C# / .NET | المدمجة System.Security.Cryptography: AesGcm وChaCha20Poly1305 وECDsa وRSA، وفي .NET 10 MLKem وMLDsa                                                                                                                    | Rfc2898DeriveBytes.Pbkdf2 مدمجة، وArgon2id عبر [NSec](https://nsec.rocks/?ref=arabroot.io) أو [Konscious.Security.Cryptography.Argon2](https://github.com/kmaragon/Konscious.Security.Cryptography?ref=arabroot.io)، وكلاهما MIT                                                               | لا يوجد Ed25519 ولا Argon2 مدمجان حتى .NET 10، وNSec يوفر الاثنين فوق libsodium |
| Java      | JCA المدمجة: AES/GCM/NoPadding وEd25519 من Java 15 مع [JEP 339](https://openjdk.org/jeps/339?ref=arabroot.io) وX25519 من Java 11 مع [JEP 324](https://openjdk.org/jeps/324?ref=arabroot.io)، وML-KEM وML-DSA من Java 24 | PBKDF2 مدمج، وArgon2id عبر [Argon2BytesGenerator](https://downloads.bouncycastle.org/java/docs/bcprov-jdk18on-javadoc/org/bouncycastle/crypto/generators/Argon2BytesGenerator.html?ref=arabroot.io) في Bouncy Castle بترخيص [MIT](https://www.bouncycastle.org/about/license/?ref=arabroot.io) | حدد الوضع دائماً، فـ "AES" وحدها تعني ECB                                       |
| Node.js   | الوحدة المدمجة [node:crypto](https://nodejs.org/api/crypto.html?ref=arabroot.io): aes-256-gcm وEd25519 وX25519                                                                                                          | crypto.argon2 مدمجة من Node.js 24.7.0، أو حزمة [argon2](https://github.com/ranisalt/node-argon2?ref=arabroot.io) بترخيص MIT للإصدارات الأقدم                                                                                                                                                   | في المتصفح استخدم Web Crypto API                                                |

والنقطة الأخيرة في الجدول تحققنا منها مباشرة، فالمثال التالي يحسب Argon2id بالدالة المدمجة في Node.js:

```javascript
import { argon2Sync, randomBytes } from 'node:crypto';
const t = performance.now();
const hash = argon2Sync('argon2id', { message: 'S3cret!', nonce: randomBytes(16), parallelism: 1, memory: 19456, passes: 2, tagLength: 32 });
console.log(`Node ${process.version} OpenSSL ${process.versions.openssl}: argon2id ${hash.length} bytes in ${(performance.now() - t).toFixed(1)} ms`);
```

```bash
docker run --rm --name lab-crypto-node -v "$PWD:/lab" node:26-alpine node /lab/argon2.mjs
docker run --rm --name lab-crypto-node22 -v "$PWD:/lab" node:22-alpine node /lab/argon2.mjs
```

والمخرج سوف يكون كما يلي:

```text
Node v26.10.0 OpenSSL 3.5.8: argon2id 32 bytes in 32.9 ms
file:///lab/argon2.mjs:1
import { argon2Sync, randomBytes } from 'node:crypto';
         ^^^^^^^^^^
file:///lab/argon2.mjs:1
import { argon2Sync, randomBytes } from 'node:crypto';
         ^^^^^^^^^^
SyntaxError: The requested module 'node:crypto' does not provide an export named 'argon2Sync'
    at ModuleJob._instantiate (node:internal/modules/esm/module_job:226:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:335:5)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:681:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5)

Node.js v22.23.3
```

نجح Node.js 26 في 32.9 مللي ثانية، قريباً من رقم argon2-cffi في Python لأن كلاهما يستدعي كوداً أصلياً، أما Node.js 22 فلم يجد الدالة أصلاً، لذلك إذا كان مشروعك على إصدار أقدم من 24.7.0 فاستخدم حزمة argon2، ولا تقم بتمرير الكلمة على `crypto.pbkdf2` بعدد تكرارات قليل لأنها «موجودة».

### مثال C#: AesGcm وPBKDF2 وML-KEM في .NET 10

المثال التالي ملف `Program.cs` كامل لمشروع Console، يشفر بـ `AesGcm` ويعبث بالنص، ثم يحسب PBKDF2 بمعاملات OWASP، ثم يجرب ML-KEM:

```csharp
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;

Console.WriteLine($".NET {Environment.Version}");

// AES-256-GCM with System.Security.Cryptography.AesGcm
byte[] key = RandomNumberGenerator.GetBytes(32);
byte[] nonce = RandomNumberGenerator.GetBytes(AesGcm.NonceByteSizes.MaxSize);   // 12 bytes
byte[] aad = Encoding.UTF8.GetBytes("user_id=42");
byte[] plain = Encoding.UTF8.GetBytes("رقم الهوية 1234567890");
byte[] cipher = new byte[plain.Length];
byte[] tag = new byte[16];

using var aes = new AesGcm(key, tagSizeInBytes: 16);
aes.Encrypt(nonce, plain, cipher, tag, aad);
Console.WriteLine($"ciphertext: {Convert.ToHexString(cipher)} tag: {Convert.ToHexString(tag)}");

byte[] back = new byte[cipher.Length];
aes.Decrypt(nonce, cipher, tag, back, aad);
Console.WriteLine($"decrypt   : {Encoding.UTF8.GetString(back)}");

cipher[0] ^= 1;   // flip one bit
try { aes.Decrypt(nonce, cipher, tag, back, aad); }
catch (AuthenticationTagMismatchException e) { Console.WriteLine($"one bit flipped -> {e.GetType().Name}"); }

// PBKDF2-HMAC-SHA256 with the OWASP iteration count (FIPS-friendly password hashing)
byte[] salt = RandomNumberGenerator.GetBytes(16);
var sw = Stopwatch.StartNew();
byte[] hash = Rfc2898DeriveBytes.Pbkdf2("S3cret!", salt, 600_000, HashAlgorithmName.SHA256, 32);
sw.Stop();
Console.WriteLine($"PBKDF2-SHA256 600000 iterations: {sw.ElapsedMilliseconds} ms, hash {Convert.ToHexString(hash)[..16]}...");
Console.WriteLine($"same password, same salt, FixedTimeEquals: {CryptographicOperations.FixedTimeEquals(hash, Rfc2898DeriveBytes.Pbkdf2("S3cret!", salt, 600_000, HashAlgorithmName.SHA256, 32))}");

// Post-quantum: ML-KEM is in .NET 10 when the OS crypto library supports it (OpenSSL 3.5+)
Console.WriteLine($"MLKem.IsSupported: {MLKem.IsSupported}");
if (MLKem.IsSupported)
{
    using MLKem server = MLKem.GenerateKey(MLKemAlgorithm.MLKem768);
    byte[] encapKey = server.ExportEncapsulationKey();
    using MLKem client = MLKem.ImportEncapsulationKey(MLKemAlgorithm.MLKem768, encapKey);
    client.Encapsulate(out byte[] ciphertext, out byte[] secretClient);
    byte[] secretServer = server.Decapsulate(ciphertext);
    Console.WriteLine($"ML-KEM-768 encapsulation key {encapKey.Length} bytes, ciphertext {ciphertext.Length} bytes, same secret: {secretClient.AsSpan().SequenceEqual(secretServer)}");
}
```

ونشغله في Docker Image الرسمية لـ .NET SDK 10:

```bash
docker run --rm -it --name lab-crypto-dotnet -v "$PWD:/lab" mcr.microsoft.com/dotnet/sdk:10.0 bash

# داخل ال Container
cd /tmp && dotnet new console -o app && cp /lab/Program.cs app/ && cd app
dotnet --version && openssl version
dotnet run -c Release
```

والمخرج سوف يكون كما يلي:

```text
10.0.401
OpenSSL 3.0.13 30 Jan 2024 (Library: OpenSSL 3.0.13 30 Jan 2024)
.NET 10.0.12
ciphertext: B6982D23C4E8B63DFB6B4EE5BA5461198812C08505290C4943971CDAAC75 tag: DF44B8114CC1EEC8537084077A1B7480
decrypt   : رقم الهوية 1234567890
one bit flipped -> AuthenticationTagMismatchException
PBKDF2-SHA256 600000 iterations: 337 ms, hash 9D16757AB1130D78...
same password, same salt, FixedTimeEquals: True
MLKem.IsSupported: False
```

في الإعداد أعلاه لاحظ التالي:

- في `AesGcm` يحجز المطور مكان النص المشفر والوسم بنفسه، وطول الوسم يحدد في الإنشاء بـ `tagSizeInBytes: 16`، وال Nonce من `RandomNumberGenerator` وطوله من `AesGcm.NonceByteSizes.MaxSize` أي 12 بايت.
- العبث ببت واحد أعطى `AuthenticationTagMismatchException`، وهو المقابل في .NET لـ `InvalidTag` في Python.
- استخدمنا الدالة الثابتة `Rfc2898DeriveBytes.Pbkdf2` وليس `new Rfc2898DeriveBytes(...)`، والسبب أن البناة Constructors في هذا الكلاس أصبحت متروكة في .NET 10 بالتحذير [SYSLIB0060](https://learn.microsoft.com/en-us/dotnet/fundamentals/syslib-diagnostics/syslib0060?ref=arabroot.io)، وعندما جربنا الباني القديم `new Rfc2898DeriveBytes("x", 16)` كانت قيمه الافتراضية SHA1 مع 1000 تكرار فقط، وهي قيم من زمن آخر.
- المقارنة بـ `CryptographicOperations.FixedTimeEquals` وليس بـ `SequenceEqual` لنفس سبب `hmac.compare_digest` في Python.
- `MLKem.IsSupported` أعاد `False` لأن نظام ال Docker Image يحمل OpenSSL 3.0.13 كما في السطر الثاني من المخرج، فالكود صحيح لكنه لن يعمل حتى يعمل التطبيق على نظام فيه OpenSSL 3.5 أو أحدث، ولذلك يفحص الكود `IsSupported` قبل الاستخدام.

وللتحقق من آخر نقطتين فحصنا الأنواع المصدرة في مكتبة التشفير نفسها، فوجدنا `ChaCha20Poly1305` و`MLDsa` و`MLKem` و`SlhDsa` مع نسخها لـ OpenSSL وCNG، ولم نجد أي نوع باسم Ed25519 أو Argon2، وهذا ما يجعل NSec أو Konscious ضرورياً لهما في .NET.

### مثال Java: AES-GCM وEd25519 في JCA

وفي Java يكفي ملف واحد يشغل مباشرة دون Maven، فمنذ Java 11 يستطيع الأمر `java` أن يترجم ملف المصدر ويشغله كما في [JEP 330](https://openjdk.org/jeps/330?ref=arabroot.io):

```java
import javax.crypto.AEADBadTagException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.HexFormat;

public class CryptoLab {
    public static void main(String[] args) throws Exception {
        System.out.println("Java " + System.getProperty("java.version"));

        // AES-256-GCM: a new 12-byte nonce from SecureRandom for every message
        KeyGenerator kg = KeyGenerator.getInstance("AES");
        kg.init(256);
        SecretKey key = kg.generateKey();
        byte[] nonce = new byte[12];
        SecureRandom.getInstanceStrong().nextBytes(nonce);

        Cipher enc = Cipher.getInstance("AES/GCM/NoPadding");
        enc.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, nonce));
        enc.updateAAD("user_id=42".getBytes(StandardCharsets.UTF_8));
        byte[] ct = enc.doFinal("رقم الهوية 1234567890".getBytes(StandardCharsets.UTF_8));
        System.out.println("ciphertext+tag: " + ct.length + " bytes " + HexFormat.of().formatHex(ct));

        System.out.println("decrypt       : " + decrypt(key, nonce, ct));
        ct[0] ^= 1;                                   // flip one bit
        try {
            decrypt(key, nonce, ct);
        } catch (AEADBadTagException e) {
            System.out.println("one bit flipped -> " + e.getClass().getSimpleName() + ": " + e.getMessage());
        }

        // Ed25519 is built into the JDK since Java 15
        KeyPair kp = KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
        Signature s = Signature.getInstance("Ed25519");
        s.initSign(kp.getPrivate());
        byte[] msg = "release v2.4.1".getBytes(StandardCharsets.UTF_8);
        s.update(msg);
        byte[] sig = s.sign();
        System.out.println("Ed25519 signature: " + sig.length + " bytes");
        System.out.println("original message  -> " + verify(kp.getPublic(), msg, sig));
        System.out.println("modified message  -> " + verify(kp.getPublic(), "release v2.4.2".getBytes(StandardCharsets.UTF_8), sig));
    }

    static String decrypt(SecretKey key, byte[] nonce, byte[] ct) throws Exception {
        Cipher dec = Cipher.getInstance("AES/GCM/NoPadding");
        dec.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, nonce));
        dec.updateAAD("user_id=42".getBytes(StandardCharsets.UTF_8));
        return new String(dec.doFinal(ct), StandardCharsets.UTF_8);
    }

    static boolean verify(PublicKey pub, byte[] msg, byte[] sig) throws Exception {
        Signature v = Signature.getInstance("Ed25519");
        v.initVerify(pub);
        v.update(msg);
        return v.verify(sig);
    }
}
```

```bash
docker run --rm --name lab-crypto-java -e LANG=C.UTF-8 -v "$PWD:/lab" eclipse-temurin:21-jdk-alpine java -Dstdout.encoding=UTF-8 /lab/CryptoLab.java
```

والمخرج سوف يكون كما يلي:

```text
Java 21.0.12.1
ciphertext+tag: 46 bytes dd80ba7a5387d057dfa6a3e5b763bc6d7878d191d85685eea113a49996b318a1330c0e056d70fe60263c12c0edd0
decrypt       : رقم الهوية 1234567890
one bit flipped -> AEADBadTagException: Tag mismatch
Ed25519 signature: 64 bytes
original message  -> true
modified message  -> false
```

في الإعداد أعلاه لاحظ التالي:

- كتبنا الوضع كاملاً `AES/GCM/NoPadding`، ولم نكتب `"AES"` وحدها التي تعني ECB في مزود Java الافتراضي.
- طول الوسم يمرر بالبت في `new GCMParameterSpec(128, nonce)`، والعبث أعطى `AEADBadTagException: Tag mismatch`.
- ال Nonce من `SecureRandom` وليس من `java.util.Random`.
- Ed25519 مدمج في JDK دون أي مكتبة خارجية، وعكس Python يعيد `verify` هنا `false` بدلاً من رمي استثناء، فلا تنس أن تفحص القيمة المرجعة.

## ماذا تستخدم لكل مهمة؟ جدول القرار

الصورة التالية تجمع ما سبق في جدول واحد ترجع إليه عندما تكتب الكود:

![جدول بثلاثة أعمدة Task وUse وAvoid: تخزين كلمات المرور بـ Argon2id m=19456 t=2 p=1 أو PBKDF2-HMAC-SHA256 600000 لـ FIPS وتجنب MD5 وSHA-256 وحده والتشفير وBase64، وبصمة الملف بـ SHA-256 وSHA-512 وSHA-3 وBLAKE2 وBLAKE3 وتجنب MD5 وSHA-1، والتحقق من رسالة بين الخدمات بـ HMAC-SHA256 مع مقارنة في زمن ثابت وتجنب sha256 للسر مع الرسالة والمقارنة بـ ==، وتشفير البيانات بـ AES-256-GCM أو ChaCha20-Poly1305 مع Nonce فريد ومفتاح في KMS وتجنب ECB وCBC دون MAC وXOR، وإثبات من أصدر الملف بـ Ed25519 أو ECDSA P-256 أو RSA-PSS 3072 وتجنب RSA-1024 وتوقيعات MD5 وSHA-1، والاتفاق على مفتاح بـ TLS 1.3 مع X25519 وX25519MLKEM768 وتجنب بروتوكول خاص، والتوكنات والرموز بـ secrets وRandomNumberGenerator وSecureRandom وcrypto.randomBytes وتجنب random وMath.random وrand، والمفتاح الرئيسي في KMS أو HSM أو OpenBao مع الظرف والتدوير وتجنب المفاتيح في الكود وGit و.env داخل ال Image](https://arabroot.io/content/images/2026/10/crypto-for-devs-07-what-to-use.webp)

إذا لم تجد مهمتك في الجدول فالقاعدة واحدة: مكتبة معروفة بإعدادات موثقة

## أخطاء شائعة Common Mistakes

- **تخزين كلمات المرور بـ SHA-256 أو MD5 ولو مع Salt:** الملح يمنع الجداول الجاهزة لكنه لا يبطئ التخمين، فنواة واحدة جربت 600 ألف كلمة في الثانية في تجربتنا، والحل Argon2id.
- **تشفير كلمات المرور بدلاً من الهاش:** من يصل إلى المفتاح يفك كل الكلمات دفعة واحدة، كما في حادثة Adobe.
- **المقارنة بـ `==`:** عند فحص HMAC أو رمز أو بصمة، والحل `compare_digest` أو `FixedTimeEquals` أو `MessageDigest.isEqual` أو `timingSafeEqual`.
- **Nonce ثابت أو محسوب من المعرف:** مثل استخدام رقم الصف أو صفر، وتكراره مع المفتاح نفسه يكشف النصوص ويسمح بالتزوير في GCM.
- **`Cipher.getInstance("AES")` في Java:** يعطيك ECB بصمت، وصورة القفل في تجربتنا تبين ما يعنيه ذلك.
- **bcrypt مع نص طويل:** دمج اسم المستخدم أو أي بادئة مع كلمة المرور قبل bcrypt يدفع الكلمة خارج أول 72 بايت، كما في ثغرة Okta.
- **`random` أو `Math.random` لأي قيمة أمنية:** رمز إعادة تعيين أو توكن أو ملح أو مفتاح، والمولد قابل للتوقع كما رأيت بعد 624 قيمة فقط.
- **المفتاح بجانب البيانات:** في الكود أو `.env` داخل المستودع أو ال Docker Image، فالتشفير هنا شكلي فقط.
- **نص مشفر دون معرف المفتاح والخوارزمية:** فيصبح التدوير والانتقال إلى خوارزمية جديدة مستحيلاً دون فك كل البيانات القديمة.
- **اعتبار Base64 أو التوقيع وسيلة إخفاء:** Base64 يفك بأمر واحد، والرسالة الموقعة مقروءة للجميع.
- **مكتبة تشفير مكتوبة يدوياً أو منسوخة من إجابة قديمة:** أغلب الإجابات القديمة تستخدم ECB أو CBC دون MAC أو MD5، فارجع إلى توثيق المكتبة الحالية.

## قائمة تحقق Checklist

- كلمات المرور بـ Argon2id بمعاملات لا تقل عن `m=19456, t=2, p=1`، أو PBKDF2-HMAC-SHA256 بـ 600,000 تكرار إذا كان FIPS مطلوباً، مع إعادة الحساب عند الدخول إذا تغيرت المعاملات.
- إذا بقي bcrypt في نظام قديم: عامل عمل 10 أو أكثر، وحد أقصى للكلمة 72 بايت محسوب بالبايت وليس بالحرف، ولا شيء يضاف قبل كلمة المرور.
- تحديد عدد محاولات الدخول، وفلفل في خزنة أسرار إذا كان مناسباً لنظامك.
- الهاش للبصمات بـ SHA-256 أو أحدث، ولا يوجد MD5 أو SHA-1 في أي مكان أمني.
- الرسائل بين الخدمات وال Webhooks بـ HMAC-SHA256 ومقارنة في زمن ثابت.
- التشفير بـ AES-256-GCM أو ChaCha20-Poly1305 فقط، مع Nonce لا يتكرر، وبيانات مرتبطة تربط النص المشفر بصاحبه.
- التوقيع بـ Ed25519، أو ECDSA P-256 أو RSA-PSS بطول 3072 عند الحاجة، ولا يوجد RSA-2048 في أي نظام جديد يجب أن يعيش بعد 2030.
- كل قيمة عشوائية أمنية من CSPRNG، وقاعدة SAST تمنع `random` و`Math.random` في الكود الأمني.
- المفاتيح في KMS أو HSM أو OpenBao، مع تشفير الظرف، ومعرف المفتاح محفوظ مع كل نص مشفر، وخطة تدوير مكتوبة، وفحص أسرار في خط CI.
- مكتبة TLS ونظام التشغيل محدثان، والخوارزمية قابلة للتغيير دون إعادة كتابة التطبيق.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- الهاش يجيب عن «هل تغيرت؟»، وHMAC يضيف «ومن أحدنا؟»، والتشفير يجيب عن «من يقرأ؟»، والتوقيع عن «من كتب؟»، والخطأ الأشهر أن تستخدم واحدة منها لسؤال الأخرى.
- MD5 وSHA-1 سقطا في مقاومة التصادم منذ SHAttered في 2017، والبديل SHA-256 أو SHA-3 أو BLAKE2 وBLAKE3، لكن لا شيء منها يصلح لكلمات المرور لأنها سريعة.
- كلمات المرور تخزن بـ Argon2id مع ملح لكل مستخدم بمعاملات OWASP `m=19456, t=2, p=1` كحد أدنى، وbcrypt يقرأ 72 بايت فقط، وPBKDF2 بـ 600,000 تكرار عندما يفرض FIPS.
- التشفير المتماثل اليوم هو AES-GCM أو ChaCha20-Poly1305، والوسم يرفض أي تعديل، وتكرار ال Nonce مع المفتاح نفسه كشف رسالة التحويل الثانية كاملة في تجربتنا، وECB أبقى شكل الصورة واضحاً بعد التشفير.
- غير المتماثل للاتفاق على المفتاح وللتوقيع، وEd25519 وX25519 بمفاتيح من 32 بايت تعطي أماناً أكبر من RSA-2048، وقوة 112 بت مقبولة حتى نهاية 2030 فقط بحسب NIST.
- العشوائية الأمنية من CSPRNG فقط، فقد توقعنا رمز إعادة التعيين التالي من `random` بعد 624 قيمة.
- المفتاح في KMS أو HSM أو خزنة أسرار، مع تشفير الظرف ومعرف المفتاح والتدوير، والمرونة التشفيرية تجهزك للانتقال إلى ML-KEM وML-DSA بعد معايير FIPS 203 و204 و205.
- لا تخترع خوارزميتك، واستخدم الدوال عالية المستوى في libsodium وcryptography وSystem.Security.Cryptography وJCA وnode:crypto.

وكثير من هذه القواعد تجده على شكل متطلبات يمكن التحقق منها في معيار ASVS، وقد شرحنا كيف تحولها إلى أسئلة تراجعها في كل طلب دمج في مقال [متطلبات الأمان ومراجعة الكود مع OWASP ASVS](https://arabroot.io/articles/%D9%85%D8%AA%D8%B7%D9%84%D8%A8%D8%A7%D8%AA-%D8%A7%D9%84%D8%A7%D9%85%D8%A7%D9%86-%D9%88%D9%85%D8%B1%D8%A7%D8%AC%D8%B9%D8%A9-%D8%A7%D9%84%D9%83%D9%88%D8%AF-%D9%85%D8%B9-owasp-asvs/#pr-checklist).

[![](https://arabroot.io/content/images/2026/10/encodings-cover-cover.webp)دليلالترميز Encoding للمطورين: Base64 وأخواتهلماذا نحول البايتات إلى حروف حتى تمر في البريد والروابط، وكيف تفك أي سلسلة منها، ولماذا لا يحمي Base64 أي سر.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B1%D9%85%D9%8A%D8%B2-base64-%D9%88-base32-%D9%88-base58/)[![](https://arabroot.io/content/images/2026/10/certificates-cover-cover.webp)دليلالشهادات الرقمية TLS للمبتدئين وصيغ ملفاتهاكيف يعرف المتصفح أنه يتحدث مع الموقع الحقيقي، وماذا يوجد داخل الشهادة، ولماذا تأتي ملفاتها بأكثر من صيغة.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B4%D9%87%D8%A7%D8%AF%D8%A7%D8%AA-%D8%A7%D9%84%D8%B1%D9%82%D9%85%D9%8A%D8%A9-tls-%D9%88-x509-%D9%88%D8%B5%D9%8A%D8%BA-pem-%D9%88-der/)[![](https://arabroot.io/content/images/2026/10/sessions-cookies-jwt-cover-cover.webp)دليلكيف يتذكرك الموقع بعد تسجيل الدخول؟ الجلسات وJWTبروتوكول HTTP لا يتذكر أحداً، فكيف يبقى المستخدم مسجلاً بين الصفحات، ومتى تختار الجلسة على السيرفر ومتى تختار JWT.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AC%D9%84%D8%B3%D8%A7%D8%AA-%D9%88%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D8%AA%D8%B9%D8%B1%D9%8A%D9%81-%D8%A7%D9%84%D8%A7%D8%B1%D8%AA%D8%A8%D8%A7%D8%B7-%D9%88-jwt-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/passkeys-cover-cover.webp)دليلالدخول دون كلمة مرور: شرح Passkeys وWebAuthnلماذا لا تحميك رموز SMS وTOTP من التصيد الحديث، وكيف تحل Passkeys المشكلة بمفتاح خاص لا يغادر جهازك.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%AF%D9%88%D9%86-%D9%83%D9%84%D9%85%D8%A9-%D9%85%D8%B1%D9%88%D8%B1-passkeys-%D9%88-fido2/)[![](https://arabroot.io/content/images/2026/10/owasp-top10-cover-cover.webp)دليلOWASP Top 10 للمبتدئين: أخطر عشر مشكلات في تطبيقات الويبأخطر عشر مشكلات أمنية في تطبيقات الويب، كل واحدة بمثال ضعيف ثم مصلح، وكيف تمنعها في الكود وأين تكتشفها.](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/secure-coding-cover-cover.webp)دليلالبرمجة الآمنة Secure Coding بأمثلة من أربع لغاتكيف تكتب الكود الذي يتعامل مع مدخلات المستخدم والملفات والأخطاء بحيث لا يحتاج إصلاحاً أمنياً لاحقاً، بأمثلة في أربع لغات.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%A7%D9%85%D9%86%D8%A9-secure-coding-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة على Python 3.12.15 مع cryptography 50.0.2 وargon2-cffi 25.1.0 وbcrypt 5.0.0، و.NET SDK 10.0.401، وEclipse Temurin 21.0.12، وNode.js 26.10.0 و22.23.3، والتحقق من معاملات Argon2id في ورقة OWASP وRFC 9106، وأطوال المفاتيح في NIST SP 800-57، ومعايير FIPS 203 و204 و205 وRFC 10024.