مدير الأسرار
يبدو “مدير الأسرار” غريبًا ، أقبية، وحدات عتاد، اختصارات حكومية. اجرد التسويق وستجده ادعاءً: شفّر كلمة مرور أو مفتاح API بحيث لا يستطيع مهاجم يملك وسيلة تخزينك كاملة (خادمًا مخترقًا، نسخة احتياطية مسروقة) قراءة السر؛ وفك تشفيره فقط عندما يسأل شيء مشروع؛ واحتفظ بسجل تدقيق لكل مرة سأل فيها شيء. يبني هذا المشروع الجوهر الصادق لذلك الوعد بمكتبة cryptography في بايثون وAES-256-GCM وقبو JSON على القرص ، واجهة CLI تشفر سرًّا وتخزّنه وتفك تشفيره وتدون كل وصول، وتثبت أنها لاحظت العبث برفض فك تشفير أي شيء عُدّل. لا سحابة، ولا إعداد، ولا امتثال ، لكن كل آلية تلمسها هي الآلية الحقيقية التي تستخدمها مخازن الأسرار الحقيقية.
يفترض هذا أساسيات بايثون 101 ، الإدخال/الإخراج للملفات والقواميس والدوال. لا خلفية تشفير مسبقة مطلوبة. إنه اختياري وغير مصنّف؛ راجع المشاريع الواقعية للقائمة الكاملة.
🎯 ما ستفعله
- شفّر سرًّا بـAES-256-GCM وراقبه يتحول إلى نص مشفر معتم.
- خزّن المادة في قبو JSON ، بمفتاح، بعلامة nonce، مقاومًا للعبث.
- فك تشفير سر عند الطلب، مرفقًا إدخال تدقيق بسجل.
- دوّر مفتاح القبو الرئيسي وأعد تشفير كل شيء في مكانه.
- أثبت أن القبو يكتشف العبث ، اقلب بايتًا واحدًا وراقب فك التشفير يرفض.
أين تُشغّل هذا
محليًا مع uv هو المسار الأساسي ، المشروع كله واجهة CLI طرفية وملفان على القرص (vault.json وaudit.log)، واختبار العبث («اقلب بايتًا») مرْضٍ جسديًّا فقط مع ملفات حقيقية يمكنك فتحها في محرر.
يعمل GitHub Codespaces بواجهة CLI نفسها: افتح codespaces.new/abderrahim-lectures/python-data-analysis-course وسيتصرف كل أمر تمامًا كما محليًّا، مع ملفات القبو جالسة في شجرة الملفات.
يشغّل Google Colab وKaggle Notebooks وBinder خط الأنابيب كله بأمانة ، AES-256-GCM تشفير محلي بلا مفاتيح أو شبكة، لذا تعمل عمليات التشفير والتخزين وفك التشفير والتدقيق والتدوير وكشف العبث في دفتر مليغ نفس الطريقة في الصدفة. التحفظ الوحيد فلسفي: التشفير جيد بقدر حسن التعامل مع المفاتيح، ومكان دفتر الملاحظات الصادق هو «تعلّم الأساسية وانضباط التدقيق» ، الدرس أن مفتاحًا على القرص بجوار البيانات مسرح، ويجب أن تختبره بقراءة الكود لا بالثقة في شارة.
الإعداد
اعتمادية، ومفتاح، وقرار عن الثقة.
ثبّت uv ومكتبة cryptography
macOS / Linux (الطرفية):
curl -LsSf https://astral.sh/uv/install.sh | sh
Windows (PowerShell):
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
أغلق وأعد فتح طرفيتك، ثم:
uv --version
mkdir secret-manager && cd secret-manager
uv init --bare
uv add cryptography
ولّد مفتاح قبو رئيسيًا جديدًا
mkdir -p keys vault
uv run python -c "from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC; from cryptography.hazmat.primitives import hashes; import os; open('keys/master.key','wb').write(os.urandom(32))"
ls -l keys/master.key
chmod 600 keys/master.key
⚠️ اقرأ هذا قبل أن ترى أي تشفير
تخزين الأسرار ليس فيزياء AES ، بل حدود الثقة في مكان عيش المفتاح. يقسم القبو الحقيقي مادة المفتاح إلى مسار وصول منفصل (نظام KMS، أو رمز عتاد، أو خادم منفصل) بحيث لا يسرق أحد يسرق vault.json أيضًا master.key. يبقي هذا المشروع المفتاح على القرص بجوار القبو لأنه أداة تعلّمية، وسيقول ذلك بصوت عالٍ ، في لحظة نسخ keys/master.key إلى نفس الاختراق الذي فيه vault.json، يصبح التشفير زخرفًا. احترام تلك الحدود بعدم مشاركة ملف واحد هو المهارة الفعلية.
✅ قائمة التحقق
- ✅ يطبع
uv --versionرقم نسخة؛ وcryptographyمثبتة عبرuv add. - ✅
keys/master.keyبطول 32 بايتًا (filesize= 32) ونجحchmod 600. - ✅ يمكنك التعبير عن أين سيعيش مفتاح القبو الحقيقي لو كان
master.keyوvault.jsonعلى نفس القرص.
الخطوة 1: شفّر سرًّا إلى نص مشفر
للتشفير شكل: تختار مفتاحًا (32 بايتًا عشوائيًّا = 256 بت)، وnonce لكل رسالة (12 بايتًا عشوائيًّا، لا يُعاد أبدًا مع نفس المفتاح)، وتسلم الثلاثة لشيفرة مصادَقة. يخرج AES-256-GCM نصًا مشفرًا وعلامة مصادقة بطول 16 بايتًا ، العلامة هي ما يسمح لفاك التشفير بالتحقق أن أحدًا لم يعدّل شيئًا. النقطة الكاملة في الخطوة الأولى أن ترى التحول: سر نص صريح يصبح بايتات غير معروفة يمكنك نشرها بأمان.
👟 تلميح البداية : ابدأ بكتابة encrypt_secret(plaintext) التي تضرب os.urandom(12) وتستدعي AESGCM(key()).encrypt(nonce, plaintext.encode(), None) وتعيد النص المشفر والـnonce ، ثم اطبعهما سداسيًّا وتحقق أن الطول هو len(plaintext) + 16.
# secret_manager.py
import json, os, subprocess
from pathlib import Path
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
BASE = Path(".")
KEY_PATH = BASE / "keys" / "master.key"
VAULT_PATH = BASE / "vault" / "vault.json"
AUDIT_PATH = BASE / "vault" / "audit.log"
def key() -> bytes:
return KEY_PATH.read_bytes()
def encrypt_secret(plaintext: str) -> tuple[bytes, bytes]:
nonce = os.urandom(12) # fresh random bytes, per message
ct = AESGCM(key()).encrypt(nonce, plaintext.encode(), None)
return ct, nonce
ct, nonce = encrypt_secret("sk-live-9f2e11")
print("nonce :", nonce.hex())
print("ciphertext:", ct.hex())
print("ct length :", len(ct), "bytes")
سطران من API (AESGCM(key()).encrypt) يخفّيان انضباط الأمان كله: الـnonce هو os.urandom(12) مرة واحدة لكل رسالة، لا يُعاد أبدًا تحت نفس المفتاح (إعادة استخدام nonce مع GCM تدمر السرية ومعنى العلامة معًا)، ويعيد encrypt نصًا مشفرًا مصادَقًا والعلامة في كائن واحد. وسيطة None الثالثة بيانات مصاحبة ، «نص إضافي تودّ تحصينه من العبث لكنه ليس سرًّا». والمخرج ct.hex() هو «مظهر التشفير» الصادق: سلسلة عشوائية المظهر أطول من كتلة دون أي شبه بالسر، مضمونة بجدول مفاتيح AES.
🎯 الناتج المتوقع : ct.hex() غير صفري يختلف كليًّا عن النص الصريح، مع len(ct) ≈ len(plaintext) + 16 (علامة GCM ترافقه؛ لسر بطول 12 حرفًا، نحو 28 بايتًا).
🩹 إذا لم يعمل : إذا رفع AESGCM(key()).encrypt(...) قيمة ValueError، فالمفتاح بطول خاطئ (master.key يجب أن يكون لنحو 32 بايتًا ، أعِد os.urandom(32)). إذا بدا النص المشفر مثل النص الصريح، فلم تستدعِ .encrypt على حمولة bytes ، شفّر كل سلسلة بـ.encode() قبل تسليمها للشيفرة. إذا أنتج تشغيلان بنفس السر سداسيًّا متطابقًا، فالـnonce أُعيد استخدامه أو مقسّى ، تلك بالضبط الثغرة التي تدمر GCM؛ أعِد فحص os.urandom(12) لكل استدعاء.
✅ قائمة التحقق
- ✅ النص الصريح نفسه ينتج نصًا مشفرًا مختلفًا عبر التشغيلات (نضارة nonce).
- ✅ يمكنك قراءة المدخلات الثلاث (المفتاح والـnonce والنص الصريح) من الاستدعاء.
- ✅ يمكنك قول لماذا لا يُخزَّن المفتاح أبدًا بجوار القبو (من ملاحظة الإعداد).
🤔 سؤال (أسئلة) سقراطي(ة)
- توجد علامة GCM للإمساك بأي تعديل للنص المشفر. لكن ماذا لو لم يستطع المهاجم تغيير النص المشفر، بل تبديل نصين مشفرين في القبو فقط (تراجع)؟ أي جزء من الإثبات يفشل عندما يبدّل أحدهم كتلتين تتحقق كلٌّ منهما من علامتها؟ ذلك معادل التشفير لثغرة تحكم بالنسخ.
- تحذير إعادة النظر في الـnonce: GCM بزوج مفتاح/كون إعادة استخدام يفشّي XOR النصين الصريحين ويبطل العلامة. ماذا يعني ذلك لكيفية تخزين الـnonces في قبو بكثير من الأسرار (تحتاجها لكل كتلة، عشوائية، ذرّية؟) ، وماذا سيفعل
nonce = b"0012"الكسول بنشر حقيقي؟
الخطوة 2: خزّن السر في قبو JSON
التشفير فن مؤقت حتى تحط المادة على القرص. القبو مستند JSON يربط اسم كل سر بآثاره الثلاثة ، النص المشفر والـnonce والعلامة ، بحيث يجد فاك التشفير لاحقًا ما يحتاجه تمامًا لذلك المفتاح. JSON هو الاختيار المتعمد: قابل للفحص البشري («vault.json ملف شرعي»، يقول مدقق)، ومنقول، ويدفع بسهولة بمهارات القاموس/JSON نفسها من بايثون 101.
👟 تلميح البداية : ابدأ بكتابة store(name, plaintext): حمّل JSON القبو الموجود (أو {} عند أول تشغيل)، وضَع {"ct": ..., "nonce": ...} تحت name، وأعد كتابة المستند كله بـindent=2.
# secret_manager.py (continued)
def store(name: str, plaintext: str) -> None:
ct, nonce = encrypt_secret(plaintext)
data = {}
if VAULT_PATH.exists():
data = json.loads(VAULT_PATH.read_text())
data[name] = {"ct": ct.hex(), "nonce": nonce.hex()}
VAULT_PATH.write_text(json.dumps(data, indent=2))
store("github_token", "ghp_1234567890abcdef")
print("vault now:")
print(VAULT_PATH.read_text())
store قراءة-تعديل-كتابة: حمّل ما يحمله القبو أصلًا (التراجع عنه إلى {} عند أول تشغيل)، وضع السر الجديد تحت اسمه، وأعد كتابة المستند كله. يلتزم تنسيق القبو بالشكل المبارك ، name → {ct, nonce} ، وهو بالضبط عقد المتانة الذي لمخزن أسرار حقيقي مع مفكّكات تشفيره. تحديث اسم موجود يكتب فوق مدخله ببساطة، وهو الدلالات المطلوبة لـ«أعدت تدوير هذه الاعتمادية».
🎯 الناتج المتوقع : vault/vault.json يحتوي على مدخل مفتاح واحد ، {"github_token": {"ct": "<hex>", "nonce": "<12-byte hex>"}} ، مع نص مشفر ظاهر لا علاقة له بـghp_....
🩹 إذا لم يعمل : إذا لم يُنشأ ملف القبو، فإن VAULT_PATH.parent غير موجود ، mkdir -p vault من الإعداد هو الحل (أو VAULT_PATH.parent.mkdir(parents=True)). إذا استبدل store القبو كله بسر واحد عند التشغيل المتكرر، فقراءة-تعديل-كتابة لا تحمّل JSON الموجود ، تحقق أن حمولة if VAULT_PATH.exists() تحدث قبل الكتابة، لا بعدها. إذا انهار json.loads، فقد تضرر القبو ، كتابة ضالة من محرر آخر؛ ابدأ من جديد بملف {} صفري البايتات.
✅ قائمة التحقق
- ✅
vault.jsonموجود بالمدخل الجديد، مقروء بالعين البشرية. - ✅ استدعاء
storeمرتين لـاسمين مختلفين يبقي المدخلين (لا استبدال). - ✅ الملف لا يحتوي نصًّا صريحًا ، سلسلة السر لا تظهر في أي مكان في JSON.
🤔 سؤال (أسئلة) سقراطي(ة)
- يقترن المخزَّن
ctبـnonceلكن يخزنهما مشفّرين سداسيًّا. يسأل مدقق: «لماذا ليست العلامة في هذا السجل؟» ، تعمّق في كيف يعيد واجهة برمجة الشيفرات العلامة، وما الذي سيغيّره تخزينها بشكل منفصل (أو عدم تخزينها أصلًا) في اكتشاف-ورفض لاحق. indent=2في JSON للبشر؛ قبو إنتاج سيخزن بايتات خام لا سداسيًّا. سمِّ كلفة المقروئية: ماذا يسمح الترميز السداسي + JSON القابل للفحص البشري للمهاجم أن يتعلمه من قبوّك (عن الأسماء أو الحجم أو العمر) مما يمنعه الثنائي الخام؟
الخطوة 3: فك التشفير عند الطلب مع سجل تدقيق
مدير أسرار لا يفك التشفير هو خزانة ملفات بقفل بلا ثقب مفتاح. مسار القراءة أهم بقدر مسار الكتابة ، ومسار التدقيق هو النقطة الكاملة للمدير لا للشيفرة البسيطة. تفك هذه الخطوة سرًّا من القبو وتسجل كل وصول كهذا في audit.log، بعلامات الوقت وما إليها. تبني مساءلة: السجل هو الجزء الذي يمسك بالأوغاد.
👟 تلميح البداية : ابدأ بكتابة load(name) ، حوّل المخزَّنين ct وnonce من السداسي إلى بايتات، وسلمهما إلى AESGCM(key()).decrypt(...)، و.decode() النتيجة ، ثم أضف audit(name) لإلحاق سطر قراءة بعلامة زمن UTC.
# secret_manager.py (continued)
from datetime import datetime, timezone
def load(name: str) -> str:
data = json.loads(VAULT_PATH.read_text())
entry = data.get(name)
if entry is None:
raise KeyError(f"no secret named {name!r} in vault")
ct = bytes.fromhex(entry["ct"])
nonce = bytes.fromhex(entry["nonce"])
return AESGCM(key()).decrypt(nonce, ct, None).decode()
def audit(name: str) -> None:
ts = datetime.now(timezone.utc).isoformat()
with AUDIT_PATH.open("a") as f:
f.write(f"{ts} read {name}\n")
secret = load("github_token")
audit("github_token")
print("secret:", secret)
print("audit :")
print(AUDIT_PATH.read_text())
يسير load عبر معكوس store بالضبط: حوّل كل أثر من السداسي إلى بايتات، وسلّم مفتاح + nonce + نص مشفر إلى AESGCM.decrypt، و.decode() النص الصريح. أنماط الفشل متعمدة ، الاسم المفقود يرفع KeyError (خطأ برمجة صاخب، لا None صامتًا)، والنص المشفر المُعدَّل يرفع InvalidTag (تستغل الخطوة 5 ذلك). audit منفصل عمدًا عن load لتستطيع استدعاء فك التشفير في REPL دون ضجيج تسجيل ، لكن الاقتران هو الانضباط: يدوّن مديرو الإنتاج كل load، وعلامة الوقت بـUTC (datetime.timezone.utc) حتى لا يفسد حاسوب بتوقيت منتصف الليل السجل.
🎯 الناتج المتوقع : يطبع السر بعد رحلة الذهاب والإياب (sk-live-9f2e11)، ويكسب audit.log سطرًا واحدًا، مثلًا 2026-09-06T14:02:11.123456+00:00 read github_token.
🩹 إذا لم يعمل : إذا أطلق KeyError على اسم تعرف أنه في القبو، ففي مفتاح JSON مسافة أو فارق حالة أحرف ، اطبع data.keys() وقارن بدقة. إذا ظهر InvalidTag على مخزن حديث، فملف المفتاح تغيّر بين store وload ، master.key مختلف يعني مجموعة نصوص مشفرة لا يمكن فك تشفيرها أبدًا؛ أعد توليد المفتاح وأعد تشفير كل سر (أو انسخ المفتاح القديم). إذا نما ملف التدقيق بلا حدود، فهذا السلوك الصحيح لتشغيل قصير ، رقصة «التدوير الذي يؤرشف السجلات القديمة» من حق الخطوة 4.
✅ قائمة التحقق
- ✅ يعيد فك التشفير النص الصريح نفسه بالضبط (
reprلا يظهر مسافات زائدة). - ✅ يحتوي
audit.logعلى علامة وقت UTC + سطرread <name>لكل وصول. - ✅ قراءة اسم غير موجود ترفع
KeyErrorصاخبًا، لاNone.
🤔 سؤال (أسئلة) سقراطي(ة)
loadوauditدالتان تقرنهما باستدعائهما معًا. في سكربت، ماذا يحدث لو انهار بينloadوaudit، هل قُرئ سر ليس مسجلًا؟ سمِّ النمط (اكتب سطر السجل قبل فك التشفير أم بعده، وأي فشل تفضل إخفاءه) الذي يختاره نظام الإنتاج.- سجلات التدقيق نص ملحق فقط. مهاجم يستطيع الكتابة إلى
vault/يستطيع أيضًا الكتابة إلىaudit/. ما الذي يميز سجل تدقيق مقاومًا للعبث (سلسلة تجزئة لكل سطر إلى السابق) عن هذا ، وتحت أي نموذج ثقة يهم النص الصريح أصلًا؟
الخطوة 4: دوّر المفتاح الرئيسي وأعد التشفير
تشيخ المفاتيح مثل كلمات المرور ، المفتاح الذي كان في اختراق ربما تعرّض للخطر، والتدوير هو عملية «غيّر القفل وأعد إصدار كل الأبواب». في القبو هذه رقصة خطوتين: أعد المفتاح لكل نص مشفر مخزَّن تحت مفتاح جديد (فك تشفير بالقديم، وأعد التشفير بالجديد)، ثم أمِّن المفتاح القديم حتى لا يفك تشفير مادة قديمة بصمت. تؤتمت الخطوة 4 إعادة التشفير وتجهّز قرار «المفتاح القديم إلى المهملات» بصوت عالٍ.
👟 تلميح البداية : ابدأ بكتابة rotate() التي تفك تشفير كل مدخل قبو بالمفتاح الحالي وتعاود تشفيره تحت مفتاح os.urandom(32) جديد، ثم تحيل ملف المفتاح القديم إلى التقاعد وتنقل الجديد إلى مساره القانوني.
# secret_manager.py (continued)
def rotate(new_key_path: Path = BASE / "keys" / "master2.key") -> None:
data = json.loads(VAULT_PATH.read_text())
new_key = os.urandom(32)
new_key_path.write_bytes(new_key)
for name, entry in data.items():
old = AESGCM(key()).decrypt(bytes.fromhex(entry["nonce"]),
bytes.fromhex(entry["ct"]), None)
nonce = os.urandom(12)
data[name] = {
"ct": AESGCM(new_key).encrypt(nonce, old, None).hex(),
"nonce": nonce.hex(),
}
VAULT_PATH.write_text(json.dumps(data, indent=2))
(KEY_PATH).unlink() # old key retired
new_key_path.replace(KEY_PATH)
rotate()
print("rotated; new key in place:", KEY_PATH.exists())
print("new key differs from old :", True)
الحلقة هي محرك إعادة المفتاح: لكل مدخل، فك تشفير بـmaster.key الحالي، وضرب nonce جديدًا، وأعد التشفير تحت new_key، واكتب القبو كله. خطوة التقاعد هي حيث يعيش الأمان ، unlink() ملف المفتاح القديم وreplace الجديد في مساره القانوني حتى لا يزال الاسم master.key يتحل، بينما البايتات جديدة كليًّا. يخزن القبو الآن نصوصًا مشفرة لا علاقة لها بالمفتاح القديم، ومادة المفتاح القديم رحلت، نهاية القصة ، لا «مخفية»، بل محذوفة.
🎯 الناتج المتوقع : يكتمل التدوير بمفتاح جديد بطول 32 بايتًا في keys/master.key، وقيمة True لفحصي الوجود، وما يزال كل مدخل قبو يفك تشفيره تحت المفتاح الجديد.
🩹 إذا لم يعمل : إذا انهار التدوير في منتصف الحلقة، فبعض المدخلات بمفتاح الجديد فيما يبقى الباقي بالقديم ، تشغيل rotate() مرة أخرى إذًا يعيد تشفير الجديدة مرتين. أعد المفتاح في قاموس مؤقت واكتب عند النجاح فقط؛ الكتابات الجزئية هي ثغرة التدوير. إذا بقي master2.key بعد replace، ففشل الاستبدال (نقل عبر نظامي ملفات) ، استخدم دلالات Path.replace التي تستبدل ذرّيًّا عندما يكون المساران في نفس مجلد keys/.
✅ قائمة التحقق
- ✅ تختلف بايتات
master.keyعما قبل التدوير (فرقkeysأو إعادة تجزئة). - ✅ ما يزال كل اسم يفك تشفيره تحت المفتاح المدوَّر (كل استدعاءات
loadتنجح). - ✅ لا ناجٍ من
master2.keyفيkeys/بعدreplace.
🤔 سؤال (أسئلة) سقراطي(ة)
- يعيد التدوير التشفير لكنه لا يغيّر الأسرار نفسها. ما يزال المفتاح المدوَّر يسمح لمفتاح API قديم بفك تشفير ، التدوير يغيّر من يستطيع قراءة النصوص المشفرة عبر التحكم بالمفتاح، لا ماذا تقول النصوص المشفرة. متى يجب أن يقترن التدوير بـإعادة إصدار السر نفسه (فكر «كانت هذه الاعتمادية في سجل»)، ولماذا يدوّر المدير بقوة مهما يكن؟
- فخ الذرّية ، انهيار في منتصف الحلقة يترك قبوًا هجينًا. صمّم الإصلاح بالسطرين (ابنِ القاموس الجديد في الذاكرة واكتب مرة واحدة) وسمِّ العاقبة الواقعية إذا تخطيتها (بعض الأسرار لا يفك تشفيرها إلا المفتاح القديم الذاهب إلى المهملات).
الخطوة 5: أثبت كشف العبث
الخطوة الأخيرة هي الخطوة العدائية ، والمردود لاستخدام AEAD أصلًا. أي شخص لديه وصول كتابة إلى vault يستطيع قلب بايتات النص المشفر، والدفاع الوحيد لفاك التشفير هو علامة المصادقة. تفسد هذه الخطوة عمدًا نصًا مشفرًا محفوظًا وتراقب AESGCM.decrypt يرفض ، InvalidTag هي قصة الأمان كلها في استثناء واحد: النص المشفر المُعدَّل لا يمكن أن يمر أبدًا كشريف.
👟 تلميح البداية : ابدأ بتحميل مدخل قبو واحد، وقلب بتة نص مشفر واحدة بـtampered[3] ^= 0x01، ولف استدعاء AESGCM.decrypt في try/except InvalidTag.
# secret_manager.py (continued)
from cryptography.exceptions import InvalidTag
data = json.loads(VAULT_PATH.read_text())
name = "github_token"
entry = data[name]
ct = bytes.fromhex(entry["ct"])
tampered = bytearray(ct)
tampered[3] ^= 0x01 # flip one bit in the ciphertext
print("tag check:", end=" ")
try:
AESGCM(key()).decrypt(bytes.fromhex(entry["nonce"]), bytes(tampered), None)
print("DECRYPTED (unexpected!)")
except InvalidTag:
print("rejected — ciphertext was tampered with")
قلب بتة واحدة، tampered[3] ^= 0x01. لأن GCM يصادق النص المشفر كله تحت العلامة المحسوبة عند التشفير، أي تعديل ، بايتًا واحدًا كان أو كل بايت ، يفشل فحص العلامة، ويرفع .decrypt InvalidTag بدل إعادة قمامة. هذا عقد AEAD في استثناء واحد: فك تشفير كل شيء أو لا شيء. شيفرة متناظرة دون علامة (AES/CBC خام) ستعيد بدل ذلك نصًّا صريحًا خاطئًا بصمت ، يستطيع مهاجم قلب بتات والحصول على سر خاطئ بثقة يظل «يفك تشفير». إن try/except هو سياسة كشف العبث كلها: لا ثقة جزئية، بل رفض صاخب.
🎯 الناتج المتوقع : rejected ، ciphertext was tampered with ، أبدًا السر المفكوك، ولا سلسلة قمامة؛ InvalidTag أمين يوقف خط الأنابيب.
🩹 إذا لم يعمل : إذا طبعت تجربة العبث السر على أي حال، فلن تُتحقق العلامة ، سبب كلاسيكي هو فك تشفير بـ«علامة» تشبه nonce أو استدعاء حمولة API خاطئة (AES الخام بلا علامة). إذا فشل استيراد InvalidTag (from cryptography.exceptions import InvalidTag)، فأنت على نسخة cryptography قديمة ، حدّث بـuv add cryptography@latest. إذا قلبت بالصدفة tampered[3] ^= 0 (XOR مع صفر)، فلن يتغير شيء ويفك التشفير صحيحًا ، ذلك تقرير الثغرة: لا طفرة، لا فشل، والدرس أن «البايتات غير المتغيرة لا تنبّه أبدًا».
✅ قائمة التحقق
- ✅ بتة مقلوبة واحدة تؤدي
InvalidTagولا يُطبع نص صريح. - ✅ ما يزال المدخل غير المُعدَّل يفك تشفيره (السيطرة الموجبة ما زالت تمر).
- ✅ يمكنك قول ضمانة AEAD في جملة واحدة: النص المشفر قابل للتحقق بلا أخطاء، لذا بايت واحد مُعدَّل يقطع فك التشفير.
🤔 سؤال (أسئلة) سقراطي(ة)
- تكتشف العلامة أي تعديل، لكنها تكتشفه فقط عند فك التشفير. قبو لا يفك تشفير ملفًا فاسدًا أبدًا «يبدو جيدًا» للأبد ، أين يعض الأمان فعلًا (لحظة الوصول)، وماذا يقول ذلك عن مراقبة محاولات فك التشفير لا التشفير فقط؟
- AES الخام (بلا علامة) سيقبل نصًا مشفرًا مقلوبًا ويعيد سرًّا مختلفًا يبدو معقولًا. تتبّع الهجوم الواقعي على قبو CBC من قلب بتات يتحكم به المهاجم يحول «amount=1» إلى «amount=100». ما الكلمة الواحدة لسبب كون رفض GCM ميزة لا إزعاجًا، عندما يعيش النص المشفر في تخزين غير موثوق؟
⚠️ مآزق شائعة
- مفتاح يعيش بجوار القبو. AES بلا معنى إذا سرق نفس الاختراق الذي سرق
vault.jsonأيضًاmaster.key، يحمي التشفير عند السكون، لا من الاختراق الكامل. افصل مسار الثقة (KMS/رمز/قرص منفصل) في أي شيء حقيقي. - إعادة استخدام nonce تحت مفتاح واحد. إعادة استخدام nonce من GCM تُفشّي XOR النصوص الصريحة وتبطل العلامة. استعمل دائمًا
os.urandom(12)لكل تشفير؛ ولا تقسِّ أبدًا nonce أو تستمدّه من الاسم. - قبو هجين بعد تدوير متقطع. انهيار في حلقة فك-إعادة التشفير يترك مدخلات قديمة تحت المفتاح القديم (المحذوف للتو). ابنِ القاموس الجديد كله في الذاكرة ثم اكتب مرة واحدة ، الكتابات الذرّية ليست رفاهية.
- مفاجآت
decode()من بايتات مهربة. يرتكّب التنقل السداسي بتقيد؛ بايت زائد ضال (سطر جديد من تحرير يدوي) يجعلbytes.fromhexيرفع قبل تشغيل فحص العلامة أصلًا. تحقق من السداسي وقت التخزين، أو اجنِ الأخطاء عند التحميل. - شيفرات خام «تفك تشفير أي شيء». شيفرة بلا علامة تعيد بعض النص الصريح للبيانات المُعدَّلة ، خاطئة بثقة. النقطة الكاملة لـ
AESGCMهيInvalidTagعند أول بتة مقلوبة؛ لا «تفضّلها» بعيدًا من أجل مسار كود أسرع.
ما بنيته للتو
مدير أسرار عامل: شفّر AES-256-GCM اعتمادية إلى نص مشفر مصادَق؛ وخزّنه قبو JSON بمفتاح اسم؛ وأعاد فك التشفير من الذهاب والإياب بسجل تدقيق UTC؛ وأعاد تدوير مفتاح القبو كله تحت مادة مفاتيح جديدة بطول 32 بايتًا؛ وأثبت اختبار عبث ببتة واحدة أن القبو يرفض النص المشفر المُعدَّل بـInvalidTag. الطبقات المنقولة تتجاوز واجهة CLI: تملك الآن انضباط الـnonce، وقناعة «المفتاح يعيش على حد ثقة مختلف»، والشعور الصادق الحقيقي لفك تشفير مصادَق يرفض بتة مقلوبة ، وهو سلوك الأمان الذي تعتمد عليه منصات حقيقية.
شغّل نسخة أكمل دون أي إعداد محلي
examples/secret-manager/ في مستودع المساق يحزم وحدة المدير، وهيكل keys/ متجاهَلًا بـ.gitignore، ودفترًا يشفّر ويخزّن ويفك تشفير ويدوّر ويختبر العبث داخليًّا. استنسخه، أو افتح المستودع كله في GitHub Codespaces، وشغّل الخطوات الخمس من البداية إلى النهاية.
إلى أين تذهب من هنا
- مسار مفتاح حقيقي: انقل
master.keyإلى مسار خارج مجلد القبو (أو متغير بيئة)، بحيث يفصل حد الثقة من الإعداد بين الشرفين فعلًا. - سجل تدقيق مربوط: سلاسل تجزئة
audit.log(كل سطر يضمّن تجزئة السطر السابق) حتى تسدّ فجوة «المهاجم يعدّل كلا الملفين» عبر الخطوة 3 إلى سجل مقاوم للعبث حقًا. - غلاف CLI:
argparseمعsecret get github_tokenوsecret setوsecret rotateوsecret ls، الدوال التي كتبتها، معروضة كأداة طرفية حقيقية. - تدوير زمني: شغّل
rotate()بجدول (scheduleأو سطر cron) وأرشفaudit.logالقديمة ، كلمة «مدير»، مستحقة.
شارك مشروعك مع الصف
بنيت قبوًا، أو دوّرت مفاتيحك حيًّا، أو حصلت على اختبار عبث أضحكك؟ examples/student-projects/ معرض لمشاريع قدّمها طلاب آخرون، وREADME يرشد إلى إضافة مشروعك عبر طلب سحب من البداية إلى النهاية: الشوكة والفرع والالتزام وفتح الـPR. لا خبرة git مسبقة مفترضة.
مرحبًا بك في كتابة Python خارج المتصفح. 🎓
أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.