حفظ أسرار تطبيقات Windows - تجنّب إعداد النصّ الصريح بـ DPAPI
· آخر تحديث: · 小村 豪 · تطوير Windows, الأمان, DPAPI, C# / .NET, Win32
سجل التعديلات (2 تحديثات، آخر تحديث 2 Sep، 2026)
سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.
- أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
- أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
- النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621374)
هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.
小村 豪 (2026). حفظ أسرار تطبيقات Windows - تجنّب إعداد النصّ الصريح بـ DPAPI. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621374 https://comcomponent.com/ar/blog/2026/03/16/000-windows-app-secret-storage-best-practices-dpapi/
- DOI (أحدث نسخة)
- 10.5281/zenodo.21621374
- DOI (هذه النسخة)
- 10.5281/zenodo.22240842
في المقال السابق «قائمة تحقّق للحدّ الأدنى من الأمان في تطوير تطبيقات Windows» كتبتُ الخطّ الأدنى:
«لا تضع الأسرار في الشيفرة المصدريّة أو في إعداد نصّ صريح»، و«في Win32 / .NET استخدم DPAPI / ProtectedData».
هذه المرّة أتعمّق أكثر في «استخدام DPAPI ليصير الأمر على الأقلّ أفضل من النصّ الصريح».
الهدف تطبيقات Windows من هذا النوع.
- تطبيقات سطح مكتب WPF / WinForms / WinUI
- عميل Windows بـ C# / .NET
- تطبيقات تميل إلى حفظ معلومات اعتماد الاتّصال أو رموز API في ملفّ إعداد محلّيّ
ما نعالجه هنا تصميم واقعيّ من أجل «سرّ لا مفرّ من حفظه محلّيّاً، على الأقلّ لا يُترَك نصّاً صريحاً في appsettings.json».
ليس حديث «دفاع كامل ينتصر على أيّ مهاجم». إن حُشي النقاش أكثر من اللازم ابتعد عن نقاش أمن واقعيّ.
1. الخلاصة أوّلاً
في العمل يسهل الفهم بهذا الترتيب.
- لا تعطِ العميل أصلاً سرّاً طويل العمر
- قدّم مصادقة Windows، أو المصادقة المتكاملة، أو تسجيل الدخول التفاعليّ، أو إدارة الأسرار في جهة الخادم
- إن لزم الحفظ المحلّيّ حتماً، فلا تضعه نصّاً صريحاً
- على Windows اجعل DPAPI /
ProtectedDataالمرشّح الأوّل
- على Windows اجعل DPAPI /
- تطبيقات سطح المكتب العاديّة تتّخذ
DataProtectionScope.CurrentUserأساساً- استخدامات
LocalMachineضيّقة جدّاً
- استخدامات
- DPAPI لا يحمي حتّى «حالة اختراق الجهاز بالكامل»
- الشيفرة التي تعمل بصلاحيّات المستخدم نفسه تستطيع في الأساس فكّ تشفير ما يستطيع ذلك المستخدم فكّ تشفيره
وأهمّ نقطة في هذا المقال هي هذه.
«على أيّ حال يجب حفظ المفتاح السرّيّ في مكان ما، أليس النصّ الصريح و DPAPI سواء من جهة الأمن؟»
هذا نصف صحيح، والنتيجة خاطئة.
- AES ذاتيّ + وضع المفتاح في نفس التطبيق أو نفس الإعداد قريب جدّاً من النصّ الصريح
- لكن DPAPI يقرّب إدارة المفاتيح إلى نظام التشغيل، ويربط جهة فكّ التشفير بـ«مستخدم Windows ذلك» أو بـ«ذلك الحاسوب»
- لذلك تتغيّر القوّة كثيراً أمام حوادث مثل تسرّب ملفّ الإعداد وحده، أو نقله إلى حاسوب آخر، أو إرساله خطأ، أو تسرّب نسخة احتياطيّة، أو دخوله المستودع
أي أنّه حتّى إن بدا الأمر سواء بالنظر إلى التجريد «المفتاح في مكان ما»، فـ«من يستطيع استخدامه، وفي أيّ سياق، وبأيّ سهولة» مختلف تماماً.
القول إنّ وضع المفتاح تحت ممسحة الباب وتسليمه بعد التحقّق من الهويّة في غرفة الإدارة شيء واحد قول متسرّع قليلاً.
flowchart TB
accTitle: «المفتاح في مكان ما» وحده لا يجعل الأمر سواء
accDescr: مخطّط يبيّن النقطة المركزيّة في المقال: بالنظر إلى تجريد أنّ المفتاح في مكان ما يبدو النصّ الصريح و DPAPI سواء، لكن من يستطيع الاستخدام وفي أيّ سياق وبأيّ سهولة مختلف تماماً.
abs1["المفتاح في مكان ما (تجريد)"] -.->|"إن نُظر إلى هذا وحده"| same1["يبدو سواء"]
who1["من يستطيع، وفي أيّ سياق، وبأيّ سهولة"] -->|"إن نُظر إلى هذا"| diff1["مختلف تماماً"]
الشكل 1: في التجريد يبدوان سواء، لكن «من يستطيع الاستخدام وفي أيّ سياق» يفصل النصّ الصريح عن DPAPI.
خريطة المعرفة لهذه المقالة
يعالج هذا المقال حفظ أسرار تطبيقات سطح مكتب Windows (مثل سلاسل الاتّصال ورموز API) بأمان أكبر من إعداد النصّ الصريح باستخدام DPAPI (ProtectedData). الحفظ بالنصّ الصريح والتشفير الذاتي الذي يضع المفتاح في نفس مكان النصّ المشفَّر يسرّبان السرّ فور تسرّب ملفّ الإعداد وحده، بينما يخفّف DPAPI ذلك بربط جهة فكّ التشفير بمستخدم Windows أو بالحاسوب. تطبيقات سطح المكتب العاديّة تتّخذ نطاق CurrentUser أساساً، ويُضيَّق LocalMachine إلى حالات مثل خدمة Windows موثوقة لغرض واحد. غير أنّ إعادة تعيين كلمة المرور وإعادة إنشاء الملفّ الشخصيّ ونسخ النصّ المشفَّر وحده إلى حاسوب آخر أسباب لفشل فكّ التشفير، وبالمقابل إن وُجد ملفّ شخصيّ جوّال انتقلت مادّة المفتاح معه فيمكن فكّ التشفير من حاسوب آخر. إن أردت حفظ زوج اسم مستخدم وكلمة مرور، أو مشاركة السرّ عبر عدّة أجهزة، فأنسب من DPAPI هما Credential Locker أو Credential Manager.
flowchart LR
accTitle: خريطة معرفة حماية أسرار تطبيقات Windows بـ DPAPI
accDescr: مخطّط يبيّن أنّ DPAPI يبدّل جهة فكّ التشفير بنطاقَي CurrentUser و LocalMachine، وأنّه يخفّف مخاطر التسرّب الناجمة عن الحفظ بالنصّ الصريح والتشفير الذاتي، والتفريق بينه وبين Credential Locker و Credential Manager، ومطبات التشغيل التي تسبّب فشل فكّ التشفير
dpapi["DPAPI"]
app_secrets["أسرار التطبيق"]
plaintext_secret_storage["حفظ الأسرار بالنصّ الصريح"]
secret_config_file_leak["تسرّب السرّ بتسرّب ملفّ الإعداد وحده"]
colocated_key_encryption["تشفير ذاتيّ يضع المفتاح في نفس مكان النصّ المشفَّر"]
common_client_secret["سرّ طويل العمر مشترك لكلّ العملاء"]
dpapi_currentuser_scope["DataProtectionScope.CurrentUser"]
dpapi_localmachine_scope["DataProtectionScope.LocalMachine"]
desktop_app["تطبيق سطح مكتب لمستخدم تفاعليّ"]
windows_service["خدمة Windows"]
dpapi_decryption_failure["فشل فكّ تشفير DPAPI"]
credential_pair["زوج اسم مستخدم وكلمة مرور"]
credential_locker["Credential Locker (PasswordVault)"]
credential_manager_api["Credential Manager (CredWrite / CredRead)"]
microsoft_account["حساب Microsoft"]
cross_machine_secret_sharing["مشاركة السرّ عبر أجهزة أو مستخدمين متعدّدين"]
admin_password_reset["إعادة تعيين كلمة المرور من المسؤول"]
profile_recreation["إعادة إنشاء الملفّ الشخصيّ"]
ciphertext_cross_pc_copy["نسخ النصّ المشفَّر وحده إلى حاسوب آخر"]
roaming_user_profile["الملفّ الشخصيّ الجوّال (roaming profile)"]
plaintext_secret_storage -->|"قد يسبّب"| secret_config_file_leak
dpapi -.->|"يخفّف"| secret_config_file_leak
colocated_key_encryption -->|"قد يسبّب"| secret_config_file_leak
dpapi -->|"غير موصى به لـ"| common_client_secret
dpapi -->|"يُكوَّن بـ"| dpapi_currentuser_scope
dpapi -->|"يُكوَّن بـ"| dpapi_localmachine_scope
dpapi_currentuser_scope -->|"موصى به لـ"| desktop_app
dpapi_localmachine_scope -->|"موصى به لـ"| windows_service
dpapi_localmachine_scope -->|"غير موصى به لـ"| desktop_app
windows_service -.->|"قد يسبّب"| dpapi_decryption_failure
dpapi -->|"موصى به لـ"| app_secrets
dpapi -->|"غير موصى به لـ"| credential_pair
credential_locker -->|"موصى به لـ"| credential_pair
credential_manager_api -->|"موصى به لـ"| credential_pair
credential_locker -.->|"يشترط"| microsoft_account
dpapi -.->|"لا يتوافق مع"| cross_machine_secret_sharing
credential_locker -.->|"موصى به لـ"| cross_machine_secret_sharing
admin_password_reset -.->|"قد يسبّب"| dpapi_decryption_failure
profile_recreation -->|"قد يسبّب"| dpapi_decryption_failure
ciphertext_cross_pc_copy -->|"قد يسبّب"| dpapi_decryption_failure
roaming_user_profile -.->|"يمنع"| dpapi_decryption_failure
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 21، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
2. لماذا إعداد النصّ الصريح خطر
سبب خطر الحفظ بالنصّ الصريح أوضح بكثير من نظريّة التشفير. في العمل يتسرّب تقريباً عبر مسارات كهذه.
- إدخال ملفّ الإعداد كما هو إلى Git
- دخول ملفّ الإعداد كاملاً في أرشيف ZIP للتحقيق في عطل
- طلب إرفاق ملفّ الإعداد في استعلام الدعم
- قراءة طرف ثالث عبر نسخة احتياطيّة أو مشاركة ملفّات
- ظهور سلسلة الاتّصال أو الرمز كما هو في السجلّ
- قراءة موظّف سابق أو مستخدم آخر للملفّ على نفس الجهاز
النصّ الصريح يفقد السرّيّة لحظة قراءته من طرف ثالث.
- فُتح الملفّ فانتهى الأمر
- أمكن نسخه فانتهى الأمر
- أُرفق بالبريد فانتهى الأمر
- بقي في المستودع فصار عبئاً شبه دائم
لا يحتاج المهاجم حتّى إلى براعة. أن يُفتَح بمحرّر نصوص وحده ضعف كبير.
flowchart TB
accTitle: النصّ الصريح يفقد السرّيّة لحظة قراءته
accDescr: مخطّط يبيّن أنّ ملفّ الإعداد إن قرأه طرف ثالث عبر مسارات يوميّة مثل دخول Git أو أرشيف التحقيق أو تسرّب النسخة الاحتياطيّة أو الإرفاق، فقد النصّ الصريح سرّيّته.
rt1["دخول Git وأرشيف التحقيق والإرفاق والنسخة الاحتياطيّة"] --> read1["يُقرأ ملفّ الإعداد"]
read1 --> end1["تُفقَد السرّيّة لحظة قراءة طرف ثالث"]
end1 -.-> low1["لا يحتاج المهاجم حتّى إلى براعة"]
الشكل 2: ضعف النصّ الصريح أنّ السرّيّة تُفقَد لحظة قراءة طرف ثالث عبر مسارات حوادث يوميّة.
3. الجواب على «المفتاح سيُحفَظ في مكان ما، أليس الأمر سواء؟»
هذا السؤال وجيه. وإن أُجيب عنه باستخفاف صار مقال الأمن ضبابيّاً دفعة واحدة.
الجواب: بمعنى «يلزم مفتاح في مكان ما» نعم، وبمعنى «إذن هما سواء» لا.
3.1. ما الذي يستوي، وما الذي يختلف
صحيح أنّ التشفير يحتاج في النهاية إلى نوع من root of trust. أي أنّ التشفير يحتاج في النهاية إلى نقطة انطلاق للثقة.
لكن الفرق الأمنيّ يُحسَم بهذه الثلاث.
- هل يحمل التطبيق المفتاح مباشرة
- بأيّ جهة يرتبط المفتاح
- هل يمكن فكّ التشفير إن سُرق الملفّ وحده
إن لخّصنا هذا الفرق في جدول، فهو كالتالي.
| الأسلوب | قُرئ ملفّ الإعداد | نُقل الملفّ وحده إلى حاسوب آخر | قرأه مستخدم آخر على نفس الحاسوب | شيفرة تعمل بصلاحيّات المستخدم نفسه |
|---|---|---|---|---|
| نصّ صريح | يتسرّب في الحال | يتسرّب كما هو | يتسرّب كما هو | يُقرأ طبعاً |
| تشفير ذاتيّ + المفتاح في نفس الإعداد / الثنائيّ | يتسرّب إلى حدّ بعيد | يتسرّب إلى حدّ بعيد | يتسرّب إلى حدّ بعيد | يمكن فكّ التشفير طبعاً |
DPAPI + CurrentUser |
لا يُقرأ فوراً من الملفّ وحده | يصعب فكّ التشفير عادةً | يصعب فكّ التشفير عادةً | يمكن فكّ التشفير |
DPAPI + LocalMachine |
لا يُقرأ فوراً من الملفّ وحده | يصعب فكّ التشفير عادةً خارج ذلك الحاسوب | يمكن فكّ التشفير على نطاق واسع على نفس الحاسوب | يمكن فكّ التشفير |
المهمّ هنا أنّ DPAPI يفصل «يمكن قراءة الملفّ» عن «يمكن استخدام السرّ».
flowchart TB
CT["النصّ المشفَّر<br/>يدخل ملفّ الإعداد / عمود قاعدة البيانات / أرشيف التحقيق<br/>= ما يمكن نقله"]
subgraph WIN["ما يلزم لفكّ التشفير ── يبقى في جهة نظام التشغيل ولا يدخل النصّ المشفَّر"]
UMK["المفتاح الرئيسيّ للمستخدم<br/>عند الحماية بـ CurrentUser"]
MMK["المفتاح الرئيسيّ للحاسوب<br/>عند الحماية بـ LocalMachine"]
end
CT -->|"حماية CurrentUser"| UMK
CT -->|"حماية LocalMachine"| MMK
UMK --> A1["شيفرة تعمل بصفة ذلك المستخدم<br/>← يمكن فكّ التشفير"]
UMK --> A2["مستخدم آخر على نفس الحاسوب<br/>← لا يمكن فكّ التشفير"]
MMK --> B1["شيفرة على نفس الحاسوب<br/>← يمكن فكّ التشفير حتّى من مستخدم آخر"]
UMK --> C1["نسخ النصّ المشفَّر وحده إلى حاسوب آخر<br/>← لا يمكن فكّ التشفير"]
MMK --> C1
UMK --> C2["الانتقال مع الملفّ الشخصيّ الجوّال<br/>← تنتقل مادّة المفتاح أيضاً فيمكن فكّ التشفير (القسم 6.5)"]
الشكل 3: النصّ المشفَّر وحده في الجهة التي يمكن نقلها، والمفتاح الرئيسيّ اللازم لفكّ التشفير يبقى في جهة نظام التشغيل. غير أنّ نطاق وصول LocalMachine هو الحاسوب كلّه، ولا يُقصَى مستخدم آخر على نفس الحاسوب
في النصّ الصريح هذان الشيءان واحد. إن أمكن قراءة الملفّ أمكن قراءة السرّ.
أمّا في DPAPI، فعلى الأقلّ مع CurrentUser، يلزم فكّ التشفير:
- بصفة مستخدم Windows ذلك
- في سياق Windows ذلك
- عبر آليّة حماية نظام التشغيل
هذا الفرق كبير جدّاً في مسرح الحوادث.
3.2. «حتّى ذلك، إن كان المستخدم نفسه يمكن فكّ التشفير؟» صحيح
هذه نقطة ينبغي كتابتها دون مراوغة.
الشيفرة التي تعمل بصلاحيّات المستخدم نفسه تستطيع في الأساس فكّ تشفير ما يستطيع ذلك المستخدم فكّ تشفيره.
أي أنّ DPAPI لا يستهدف أساساً حالات كهذه.
- الجهاز مخترَق أصلاً ببرمجيّة خبيثة
- المهاجم يستطيع تنفيذ شيفرة بصفة ذلك المستخدم
- الاستيلاء كامل على مستوى مسؤول الجهاز
في هذه الحالة يستطيع التطبيق نفسه فكّ التشفير، فتستطيع شيفرة المهاجم ذلك أيضاً. هنا لا يُعتَمد كثيراً على «لكنّه مشفَّر».
ما ينفع فيه DPAPI أساساً هو جانب «تسرّب الملفّات وسوء الوضع والنقل دون اتصال والقراءة من مستخدم آخر».
إن خُلط هذا:
- قُلِّل تقدير ما يُحمى فلم يُستخدم
- أو بولغ في تقدير ما لا يُحمى فاطمأنّ الناس
كلاهما خطر بهدوء.
flowchart TB
accTitle: الجانب الذي ينفع فيه DPAPI والجانب الذي لا ينفع
accDescr: مخطّط يبيّن الخطّ الفاصل: DPAPI ينفع في تسرّب الملفّات وسوء الوضع والنقل دون اتصال والقراءة من مستخدم آخر، ولا ينفع ضدّ شيفرة هجوم تعمل بصلاحيّات المستخدم نفسه أو جهاز مخترَق.
dp2["نطاق دفاع DPAPI"] -->|"جانب ينفع"| eff1["التسرّب وسوء الوضع ومستخدم آخر"]
dp2 -.->|"جانب لا ينفع"| noef1["شيفرة بصلاحيّات المستخدم نفسه"]
dp2 -.-> mis1["الخلط يُخطئ التقييم"]
الشكل 4: إن جُهل خطّ ما ينفع وما لا ينفع، صار التقييم إمّا تقليلاً فلا يُستخدم أو مبالغة في الاطمئنان.
3.3. إذن ما المفيد
إن قيلت فائدة DPAPI في جملة:
«يمكن فصل السرّ نفسه عن قابليّة قراءة ملفّ الإعداد»
مثلاً، في حوادث كهذه يظهر فرق بين النصّ الصريح و DPAPI.
- أرسل المستخدم ملفّ الإعداد إلى الدعم
- دخل ملفّ الإعداد أرشيف تحقيق
- تسرّب ملفّ الإعداد وحده من نسخة احتياطيّة
- نُسخ على مجلّد مشترك
- أمكن جعل المطوّر يرى النصّ المشفَّر وحده دون قراءة المحتوى
هذه فائدة واقعيّة جدّاً. يمكن تصغير نصف قطر الحوادث اليوميّة دون جعل المهاجم بطلاً سينمائيّاً خارقاً.
flowchart TB
accTitle: آليّة تصغير نصف قطر الحادث
accDescr: مخطّط يبيّن أنّ الملفّ إن خرج في حوادث يوميّة مثل الإرسال الخطأ أو أرشيف التحقيق أو تسرّب النسخة الاحتياطيّة، فما يخرج نصّ مشفَّر فقط فلا يصير تسرّب سرّ، فيصغر نصف قطر الحادث اليوميّ.
ac1["إرسال خطأ وأرشيف تحقيق وتسرّب نسخة احتياطيّة"] --> out3["ما يخرج نصّ مشفَّر فقط"]
out3 --> sm2["لا يصير تسرّب سرّ كما هو"]
sm2 --> rad1["يصغر نصف قطر الحادث اليوميّ"]
الشكل 5: حتّى إن تعذّر منع خروج الملفّ، يمكن تحويل ما يخرج إلى نصّ مشفَّر.
4. لماذا DPAPI مناسب تماماً
عند التعامل مع سرّ محفوظ محلّيّاً على Windows، أسباب مناسبة DPAPI للعمل كالتالي.
4.1. يمكن تقريب إدارة المفاتيح إلى نظام التشغيل
توليد مفتاح AES بنفسك، وحفظه، وإعطاء صلاحيّات، وتدويره، وتقدير أثر التسرّب، وإدخال كشف التلاعب أيضاً. هذا أثقل ممّا يُظَنّ. وإن عُمل باستخفاف انتهى الأمر عادةً بوضع المفتاح في نفس المكان.
باستخدام DPAPI يمكن فصل مشكلة «كيف يُصنَع مفتاح التشفير وأين يُوضَع» عن تنفيذ التطبيق.
بهذا المعنى، أقرب إلى الجوهر أن يُرى DPAPI لا «واجهة اختيار خوارزميّة التشفير» بل «واجهة تفويض إدارة المفاتيح إلى نظام التشغيل».
flowchart TB
accTitle: النظرة الجوهريّة إلى DPAPI
accDescr: مخطّط يبيّن أنّ DPAPI ليس واجهة اختيار خوارزميّة التشفير، بل أقرب إلى الجوهر أن يُرى واجهة تفويض مشكلة كيف يُصنَع المفتاح وأين يُوضَع إلى نظام التشغيل.
v1["واجهة اختيار خوارزميّة"] -.->|"ليست هذه النظرة"| dpv1["DPAPI"]
v2["واجهة تفويض إدارة المفاتيح إلى نظام التشغيل"] -->|"هذه النظرة أقرب إلى الجوهر"| dpv1
dpv1 --> off1["يمكن فصل توليد المفتاح وحفظه عن التنفيذ"]
الشكل 6: إن رُئي DPAPI جهة تفويض لإدارة المفاتيح، اتّضح ماذا يُفصَل عن التطبيق.
4.2. يمكن ربط جهة فكّ التشفير بمستخدم Windows أو بالحاسوب
في تطبيقات سطح المكتب العاديّة، كثيراً ما يكفي اختيار CurrentUser.
يمكن فكّ التشفير بفرضيّة:
- أن يكون ذلك المستخدم مسجّل الدخول
- أن تعمل المعالجة في سياق ذلك المستخدم
لذلك تُكتسَب خاصّيّة أنّ نسخ النصّ المشفَّر وحده إلى حاسوب آخر لا يسهّل استخدامه كما هو.
4.3. يسهل إدراج كشف التلاعب أيضاً
الشائع في التشفير الذاتيّ: اعتبار الأمر منتهياً بعد «التشفير بـ AES» ونسيان كشف التلاعب.
DPAPI يحمل أيضاً حماية سلامة لبيانات التشفير، فثمّة فائدة عمليّة: يسهل وضع كشف إعادة كتابة النصّ المشفَّر على آليّة جهة نظام التشغيل.
flowchart TB
accTitle: فرق كشف التلاعب
accDescr: مخطّط يبيّن أنّ التشفير الذاتيّ ينسى كشف التلاعب غالباً بعد اعتبار الأمر منتهياً بالتشفير بـ AES، بينما يحمل DPAPI حماية سلامة لبيانات التشفير فيسهل وضع كشف إعادة الكتابة على آليّة جهة نظام التشغيل.
diy1["تشفير ذاتيّ"] -.-> forget1["يسهل نسيان كشف التلاعب"]
dpi1["DPAPI"] --> integ1["يحمل حماية سلامة"]
integ1 --> det2["يركب كشف إعادة الكتابة أيضاً على جهة نظام التشغيل"]
الشكل 7: فائدة DPAPI أنّه يضع على آليّة نظام التشغيل لا التشفير وحده بل كشف التلاعب أيضاً.
4.4. يُستخدم مباشرة من C# / .NET
في C# يمكن استخدام System.Security.Cryptography.ProtectedData كما هو.
الاستغناء عن مكتبات زائدة عون كبير أيضاً في تطبيقات حصريّة لـ Windows.
5. ما يحميه DPAPI وما لا يحميه
أأمن فصل هذا بوضوح.
5.1. ما يصير أسهل حماية
DPAPI نافذ على الأقلّ في مواضع كهذه.
- تسرّب النصّ الصريح من ملفّ الإعداد
- نقل الملفّ إلى حاسوب آخر
- القراءة من مستخدم آخر على نفس الحاسوب (بفرضيّة
CurrentUser) - التسرّب كنسخة احتياطيّة أو ملفّ مرفق
- حالة «يُقرأ سهواً» في مواقع التطوير والصيانة
5.2. ما لا يُحمى، أو تكون الحماية فيه ضعيفة
بالمقابل، لا تبالغ في الاعتماد في الحالات التالية.
- شيفرة هجوم تعمل بصلاحيّات المستخدم نفسه
- اختراق الجهاز نفسه بالكامل
- الاستيلاء بصلاحيّات المسؤول
- النصّ الصريح في الذاكرة بعد أن يفكّ التطبيق التشفير
- سرّ طويل العمر يُوزَّع مشتركاً على كلّ العملاء
الأخير، «السرّ الطويل العمر المشترك لكلّ العملاء»، مهمّ خصوصاً.
مثلاً، تصاميم مثل:
- تضمين نفس مفتاح API لكلّ العملاء
- حمل نفس كلمة المرور المشتركة على كلّ الأجهزة
- توزيع مفتاح فكّ تشفير ثابت يكتمل في العميل وحده
يسهل امتدادها إلى الكلّ لحظة سحبها من جهاز واحد. المنطق بسيط: إن أمكن للتطبيق فكّ التشفير على جهاز واحد، أمكن استخراج ذلك السرّ.
DPAPI نافذ في «جعل موضع الحفظ ذلك أفضل من النصّ الصريح»، لكنّه لا يبرّر سرّاً لا ينبغي وضعه في العميل أصلاً.
flowchart TB
accTitle: آليّة امتداد السرّ الطويل العمر المشترك
accDescr: مخطّط يبيّن أنّ توزيع سرّ طويل العمر مشترك على كلّ العملاء يعني أنّ التطبيق يستطيع فكّ التشفير على جهاز واحد فيمكن استخراج السرّ، فيسهل الامتداد إلى الكلّ لحظة السحب من جهاز واحد.
com1["سرّ طويل العمر مشترك لكلّ العملاء"] --> one4["التطبيق يستطيع فكّ التشفير على جهاز واحد"]
one4 --> ext1["يمكن استخراج السرّ من ذلك الجهاز"]
ext1 --> all1["يمتدّ إلى الكلّ"]
com1 -.-> np2["الحفظ بـ DPAPI لا يحلّ الأصل"]
الشكل 8: السرّ المشترك يمتدّ إلى الكلّ بسقوط جهاز واحد، فراجِع موضع الوضع لا أسلوب الحفظ.
أصل هذا النوع من الأسرار ليس ابتكار أسلوب الحفظ، بل تهريبه في الاتّجاهات التالية.
- وضعه في جهة الخادم
- أن يحمل العميل رمزاً فقط
- جعله معلومات اعتماد لكلّ مستخدم
- جعله رمزاً محدود الأجل
6. التفريق بين CurrentUser و LocalMachine
هذه نقطة مهمّة جدّاً. الاختيار باستخفاف يغيّر المعنى.
6.1. الأساس CurrentUser
في تطبيقات سطح مكتب Windows العاديّة فكّر أوّلاً بـ CurrentUser أساساً.
أمثلة مناسبة:
- تطبيقات سطح مكتب موجَّهة للمستخدم بـ WPF / WinForms / WinUI
- تطبيقات تحمل إعداداً أو معلومات اعتماد لكلّ مستخدم
- تطبيقات تحمل الإعداد تحت
%LocalAppData%أو%AppData%
عندئذ يسهل معاملته «سرّ مستخدم Windows ذلك».
6.2. استخدامات LocalMachine ضيّقة جدّاً
LocalMachine يبدو مريحاً، لكنّه أوسع ممّا يلزم في تطبيقات سطح المكتب العاديّة.
يناسبه مثلاً حالات كهذه.
- خدمة Windows على جهاز موثوق لغرض واحد
- سرّ تستخدمه عمليّة معيّنة فقط على ذلك الجهاز
- حالة تحتاج الاستخدام على نفس الجهاز عبر مستخدمي تسجيل الدخول
لكن التنبيهات ثقيلة.
- يمكن فكّ التشفير على نطاق واسع من العمليّات العاملة على ذلك الحاسوب
- يسهل أن يصير خطراً على أجهزة مشتركة و RDS وأجهزة وسيطة وبيئات يدخلها أكثر من مستخدم
- إن اختير لأنّ «الجميع يستطيع الاستخدام فهو أسهل»، ضاق الأمر لاحقاً في الغالب
ودوافع الرغبة في اختيار LocalMachine عادةً هذه الثلاث.
- يمكن القراءة بعد تبديل المستخدم
- يمكن القراءة من الخدمة أيضاً
- مريح إن عمل
كلّها «سهولة»، وليست «محميّ».
إن اخترتَ LocalMachine في تطبيق سطح مكتب عاديّ، وسّعتَ إمكان فكّ التشفير إلى عمليّات أخرى على ذلك الحاسوب، فيتغيّر المعنى كثيراً.
flowchart TB
accTitle: ماذا يحدث إن اختير LocalMachine للسهولة
accDescr: مخطّط يبيّن أنّ اختيار LocalMachine لسهولة مثل القراءة عبر المستخدمين أو من الخدمة أيضاً يوسّع إمكان فكّ التشفير إلى عمليّات أخرى على ذلك الحاسوب، فيصير الأمر سهولة لا حماية.
ease1["سهولة عبر المستخدمين ودعم الخدمة"] --> pick4["اختيار LocalMachine"]
pick4 --> wide1["يتّسع إمكان فكّ التشفير إلى عمليّات أخرى على الحاسوب"]
wide1 -.-> not1["سهولة وليس حماية"]
الشكل 9: إن اختير LocalMachine للسهولة، اتّسع نطاق من يستطيع فكّ التشفير إلى الحاسوب كلّه.
6.3. عند التردّد فكّر هكذا
- تطبيق واجهة عاديّ ->
CurrentUser - حالة خاصّة تريد الحماية حقّاً بوحدة الجهاز ->
LocalMachine - يلزم أن يستطيع أيّ مستخدم فكّ التشفير، لكن على الجهاز مستخدمون آخرون أيضاً -> الأولى في الغالب مراجعة التصميم من أصله
6.4. مع الخدمات و impersonation يزداد الانتباه قليلاً
إن دخلت خدمة Windows أو impersonation، ثقل معنى CurrentUser قليلاً.
- من هو حساب التنفيذ
- هل حُمِّل الملفّ الشخصيّ لذلك الحساب
- في أيّ سياق لحظة فكّ التشفير
إن انزاح هذا المحيط، سهل أن يصير الأمر «نجح التشفير وتعذّر فكّه».
استخدامات الخدمة قد لا تُنهى بـ «CurrentUser مؤقّتاً».
مع impersonation، الفشل النموذجيّ المذكور أيضاً في Microsoft Learn هو «Key not valid for use in specified state.». DPAPI يحفظ بيانات المفتاح في الملفّ الشخصيّ للمستخدم، فلا يمكن فكّ التشفير إن لم يُحمَّل الملفّ الشخصيّ. يلزم تحميل الملفّ الشخصيّ للمستخدم المستهدَف قبل الـ impersonate.
flowchart TB
accTitle: فشل نموذجيّ مع impersonation
accDescr: مخطّط يبيّن أنّ DPAPI يحفظ بيانات المفتاح في الملفّ الشخصيّ للمستخدم، لذا إن فُكّ التشفير بعد impersonate دون تحميل الملفّ الشخصيّ حدث خطأ نموذجيّ، ويلزم تحميل الملفّ الشخصيّ للمستخدَم المستهدَف أوّلاً.
imp1["impersonate دون تحميل"] --> ferr1["يفشل فكّ التشفير بخطأ"]
ld1["تحميل الملفّ الشخصيّ أوّلاً"] --> okp1["يمكن فكّ التشفير بعد impersonate أيضاً"]
imp1 -.-> whyp1["المفتاح في الملفّ الشخصيّ"]
الشكل 10: المفتاح في جهة الملفّ الشخصيّ، لذا يلزم تحميل الملفّ الشخصيّ قبل impersonate.
6.5. اعرف حالات «يتعذّر فكّ التشفير» في التشغيل
في العمل، حادث تعذّر فكّ التشفير أوجع من التشفير نفسه. DPAPI آليّة تربط جهة فكّ التشفير بمستخدم Windows / بالحاسوب، فإن انقطع ذلك الربط تعذّرت القراءة.
ما تريد معرفته مسبقاً هذه الخمس.
| الحالة | ماذا يحدث | كيف تستعدّ |
|---|---|---|
| إعادة تعيين كلمة المرور من المسؤول | تنفكّ الحماية المرتبطة بكلمة مرور المستخدم، وقد يتعذّر الوصول إلى البيانات المحميّة بـ DPAPI. معلومات دعم Microsoft تسجّل أيضاً حالة تعذّر الوصول إلى بيانات DPAPI بعد إعادة تعيين المسؤول لكلمة المرور | صمّم السرّ بحيث «يمكن إعادة الحصول عليه». وجّه إلى إعادة الإدخال عند فشل فكّ التشفير |
| إعادة إنشاء الملفّ الشخصيّ | الملفّ الشخصيّ الجديد يحمل مادّة مفتاح أخرى، فلا يمكن فكّ تشفير النصّ المشفَّر السابق | احمل إصداراً لملفّ الإعداد، ولا تجعل فشل فكّ التشفير إنهاءً غير طبيعيّ |
| نسخ النصّ المشفَّر وحده إلى حاسوب آخر | مادّة المفتاح اللازمة لفكّ التشفير في جهة الملفّ الشخصيّ للمستخدم، فنقل نصّ CurrentUser المشفَّر وحده لا يكفي للقراءة (هذا الوجه الآخر لـ«القوّة» المذكورة في 3.1) |
صمّم بفرضيّة إعادة الحفظ لكلّ جهاز |
| الملفّ الشخصيّ الجوّال | هنا يمكن القراءة. لأنّ مادّة المفتاح تنتقل مع الملفّ الشخصيّ، تنصّ Microsoft Learn أيضاً على أنّ «المستخدم ذا الملفّ الشخصيّ الجوّال يستطيع فكّ التشفير من حاسوب آخر على الشبكة». إن عومل معاملة الصفّ السابق، أُعيد إنشاء معلومات الاعتماد بلا داعٍ في إجراء الانتقال | لا تحسم أنّ «حاسوباً آخر يعني تعذّر القراءة». أكّد وجود التجوال ثمّ قرّر إجراء الانتقال |
| تغيير حساب تنفيذ الخدمة | إن اختلف حساب التنفيذ بين الحماية وفكّ التشفير، تعذّرت القراءة بـ CurrentUser |
أدخل في التشغيل إجراء إعادة حماية عند تغيير الحساب |
الخلاصة: اكتب الشيفرة بفرضيّة أنّ ProtectedData.Unprotect قد يفشل. عند فشل فكّ التشفير تُرمى CryptographicException، فأمسكها ووجّه إلى إعادة الإدخال.
using System;
using System.Security.Cryptography;
using System.Text;
// protectedBase64: النص المشفّر المقروء من ملف الإعداد (Base64)
// entropy: مرِّر القيمة نفسها المستخدمة عند الحماية. null جائز
static bool TryUnprotect(string protectedBase64, byte[]? entropy, out string plaintext)
{
plaintext = string.Empty;
try
{
byte[] plainBytes = ProtectedData.Unprotect(
Convert.FromBase64String(protectedBase64),
optionalEntropy: entropy,
scope: DataProtectionScope.CurrentUser);
plaintext = Encoding.UTF8.GetString(plainBytes);
return true;
}
catch (CryptographicException)
{
// تعذّر فك التشفير = يرجّح أن البيئة تغيّرت.
// لا تُسقط هنا؛ وجِّه المستدعي إلى مسار إعادة الإدخال
return false;
}
catch (FormatException)
{
// عندما تكون سلسلة Base64 تالفة
return false;
}
}
الاستعلامات التي تأتي بـ «شفّرنا وتعذّر فكّ التشفير» تكون في الغالب واحدة من صفوف هذا الجدول.
flowchart TB
accTitle: تدفّق الشيفرة بفرضيّة فشل فكّ التشفير
accDescr: مخطّط يبيّن كتابة الشيفرة بفرضيّة أنّ ProtectedData.Unprotect قد يفشل لتغيّر البيئة، وإمساك CryptographicException دون إنهاء غير طبيعيّ، والتوجيه إلى مسار إعادة الإدخال.
upx1["محاولة Unprotect"] -->|"نجاح"| use2["استخدام السرّ"]
upx1 -->|"فشل باستثناء"| ctc1["إمساك الاستثناء دون الإسقاط"]
ctc1 --> rein1["التوجيه إلى مسار إعادة الإدخال"]
upx1 -.-> why2["قد يفشل إن انقطع الربط"]
الشكل 11: اكتب بفرضيّة أنّ فكّ التشفير قد يفشل، وصِل الفشل بإعادة الإدخال لا بإنهاء غير طبيعيّ.
7. حدّ أدنى من إرشادات التنفيذ
إن كان المراد في تطبيق Windows «إيقاف النصّ الصريح في ملفّ الإعداد» فقط، فلا حاجة لتعقيد التصميم أكثر من اللازم. لكن ثمّة نقاط لا تريد تجاوزها.
7.1. احمِ الأسرار وحدها
أسهل معاملة أن تحمي بنود السرّ وحدها أوّلاً، لا تشفير ملفّ الإعداد كلّه.
مثلاً، افصل كالتالي.
- عنوان URL للخادم
- اسم المستخدم
- اسم قاعدة البيانات
- أعلام الوظائف
كثيراً ما يكفي إبقاؤها نصّاً صريحاً.
بالمقابل،
- كلمة المرور
- رمز API
- رمز التحديث (refresh token)
- معلومات اعتماد مجلّد مشترك
أهداف حماية.
بهذا الفصل يصير:
- تحرير الإعداد أسهل
- تأكيد الفروق أسهل
- موضع السرّ واضحاً
- التشغيل كلّه أبسط
flowchart TB
accTitle: فصل حماية بنود السرّ وحدها
accDescr: مخطّط يبيّن أنّ حماية بنود السرّ وحدها مثل كلمة المرور والرموز، مع إبقاء URL واسم المستخدم ونحوهما نصّاً صريحاً بدل تشفير الإعداد كلّه، تسهّل التحرير وتوضّح موضع السرّ.
cfg2["ملفّ الإعداد"] -->|"يبقى نصّاً صريحاً"| pl1["URL واسم المستخدم والأعلام وغيرها"]
cfg2 -->|"يُحمى"| sc1["كلمة المرور والرموز وغيرها"]
sc1 --> mr1["موضع السرّ واضح والتشغيل أبسط"]
الشكل 12: حماية بنود السرّ وحدها لا التشفير الكلّيّ تجمع سهولة المعاملة والأمن.
7.2. جهة الحفظ أساسها per-user
في تطبيقات سطح المكتب العاديّة اجعل جهة الحفظ أساساً موضعاً per-user.
%LocalAppData%\Vendor\App\settings.json%AppData%\Vendor\App\settings.json
على الأقلّ، لا تضعه باستخفاف تحت مجلّد التثبيت أو في موضع سهل المشاركة.
حتّى مع حماية DPAPI، إن كانت ACL جهة الحفظ مستخفّة، صار الحديث: «يُقرأ النصّ المشفَّر» و«تُرى بنية الإعداد» و«تحدث أخطاء تشغيل». الدفاع أنجع حين يُرصَف طبقات لا طبقة واحدة.
7.3. optionalEntropy ليس مفتاحاً ثانياً سحريّاً لكلّ شيء
يمكن تمرير optionalEntropy إلى ProtectedData.
هذا مريح، لكنّه ليس «مفتاحاً ثانياً سحريّاً يصير آمناً إن دُفن في الثنائيّ».
- إن وُضع في نفس الملفّ لم يصر سرّاً
- دفنه قيمة ثابتة في الثنائيّ لا يجعله سرّاً قويّاً
- ومع ذلك ينفع في تمييز الغرض ومنع إساءة الاستخدام
عمليّاً، المناسب تقريباً تمرير
- اسم التطبيق
- اسم الغرض
- معرّف الإصدار
بايتات ثابتة، واستخدامه من أجل «عدم قبول نصّ مشفَّر لغرض آخر خطأ».
flowchart TB
accTitle: الموضع الصحيح لـ optionalEntropy
accDescr: مخطّط يبيّن أنّ optionalEntropy ليس مفتاحاً ثانياً سحريّاً يصير آمناً إن دُفن في الثنائيّ، بل المناسب استخدامه لتمييز الغرض بتمرير اسم التطبيق واسم الغرض بايتات ثابتة كي لا يُقبَل نصّ مشفَّر لغرض آخر خطأ.
ent1["optionalEntropy"] -.->|"لا تنتظر هذا"| key2["مفتاح ثانٍ سحريّ"]
ent1 -->|"هذا الاستخدام مناسب"| tag1["معرّف اسم التطبيق واسم الغرض"]
tag1 --> guard1["عدم قبول نصّ مشفَّر لغرض آخر خطأ"]
الشكل 13: entropy ليس مفتاحاً سرّيّاً؛ يُستخدم وسماً لتمييز الغرض ومنع إساءة الاستخدام.
7.4. ليس مسموحاً إدخال النصّ المشفَّر إلى Git
هذه أيضاً مهمّة بهدوء.
نصّ DPAPI المشفَّر أفضل بكثير من النصّ الصريح، لكن ذلك لا يعني جواز إدخال ملفّ الإعداد كلّه إلى المستودع.
السبب بسيط:
- النصّ المشفَّر يبقى طويلاً
- قد يُعاد إنتاج نفس الجهاز أو نفس السياق يوماً ما
- الملفّ يحمل معلومات غير السرّ أيضاً
- تنشأ ثقافة «محميّ إذن يمكن التعامل معه باستخفاف»
«أفضل من النصّ الصريح» و «آمن أينما وُضع» أمران مختلفان تماماً.
flowchart TB
accTitle: سبب عدم إدخال النصّ المشفَّر إلى Git أيضاً
accDescr: مخطّط يبيّن أنّ نصّ DPAPI المشفَّر أفضل من النصّ الصريح، لكن إدخاله المستودع يبقيه طويلاً ويشمل معلومات غير السرّ وينشئ ثقافة تعامل مستخفّ لأنّه محميّ، فهو غير «آمن أينما وُضع».
enc1["نصّ DPAPI المشفَّر"] -->|"أفضل من النصّ الصريح"| bet2["يصير أقوى أمام الحوادث"]
enc1 -.->|"ومع ذلك"| git1["لا يُدخَل المستودع"]
git1 --> rs1["يبقى طويلاً وتدخل معلومات أخرى وتلين الثقافة"]
الشكل 14: «أفضل من النصّ الصريح» ليس سبباً لاستخفاف موضع الوضع.
7.5. لا تخرجه إلى السجلّ
نمط شائع على غير توقّع: فكّ التشفير ثمّ الإخراج إلى السجلّ فيفسد الكلّ.
- إخراج سلسلة الاتّصال كاملة عند فشل الاتّصال
- إبقاء ترويسة Authorization عند API 401
- خلط السرّ في رسالة الاستثناء
إن فُعل هذا، حتّى بعد إيقاف النصّ الصريح في ملفّ الإعداد، صار السجلّ مستودع نصّ صريح. محزن، لكنّه عمل كثير الوقوع.
8. مثال تنفيذ أدنى بـ C# / .NET
8.1. أوّلاً، يلزم إضافة مرجع
ProtectedData يبدو داخلاً في BCL، لكن «من أين يأتي» يختلف حسب إطار الهدف. إن تعثّرت هنا، تعذّر حلّ اسم النوع ProtectedData.
| الهدف | العمل اللازم | جهة التوفير |
|---|---|---|
| .NET Framework | إضافة مرجع تجميعة System.Security إلى المشروع |
System.Security.dll |
| .NET Core / .NET 5 فما فوق (بما في ذلك .NET 6 / 8) | إضافة حزمة NuGet System.Security.Cryptography.ProtectedData |
System.Security.Cryptography.ProtectedData.dll |
هذه الحزمة غير مضمَّنة في أيّ إطار عمل مشترك لـ .NET Core / .NET 5 فما فوق. حتّى مع هدف موجَّه إلى Windows مثل net8.0-windows يلزم مرجع صريح.
dotnet add package System.Security.Cryptography.ProtectedData
ونقطة أخرى تريد معرفتها قبل التنفيذ.
ProtectedData حصريّ لـ Windows. لأنّه يعتمد على DPAPI، فإن استُدعي على .NET لمنصّة غير Windows رُميت PlatformNotSupportedException. إن كان أساس الشيفرة بفرضيّة عابر المنصّات، فصمّم تصميماً آخر من البداية كما في 10.1.
flowchart TB
accTitle: ما يُؤكَّد قبل استخدام ProtectedData
accDescr: مخطّط يبيّن نقطتين ينبغي تأكيدهما قبل التنفيذ: ProtectedData يحتاج إضافة مرجع حسب الهدف وإلا تعذّر حلّ اسم النوع، وهو حصريّ لـ Windows فإن استُدعي خارج Windows صار استثناء وقت التشغيل.
use3["نريد استخدام ProtectedData"] --> ref1["إضافة مرجع حسب الهدف"]
ref1 -.->|"إن لم تُضَف"| unres1["يتعذّر حلّ اسم النوع"]
use3 --> winonly1["فهم أنّه حصريّ لـ Windows"]
winonly1 -.->|"إن استُدعي خارج Windows"| pnse1["يصير استثناء وقت التشغيل"]
الشكل 15: قبل التنفيذ أمسك فرضيّتين: إضافة المرجع، والحصرية لـ Windows.
8.2. تنفيذ أدنى
التالي مثال أدنى لحماية سلسلة تُحفَظ في ملفّ الإعداد بـ CurrentUser.
أدخلنا optionalEntropy ثابتاً لتمييز الغرض، لكن لا تحسبه مفتاحاً سرّيّاً.
using System;
using System.Security.Cryptography;
using System.Text;
public static class DpapiSecretProtector
{
// 用途識別用。第二の秘密鍵ではない。
private static readonly byte[] Entropy =
Encoding.UTF8.GetBytes("ComComponent:DesktopApp:SettingsSecret:v1");
public static string ProtectToBase64(string plaintext)
{
ArgumentNullException.ThrowIfNull(plaintext);
byte[] plainBytes = Encoding.UTF8.GetBytes(plaintext);
byte[] protectedBytes = Array.Empty<byte>();
try
{
protectedBytes = ProtectedData.Protect(
plainBytes,
optionalEntropy: Entropy,
scope: DataProtectionScope.CurrentUser);
return Convert.ToBase64String(protectedBytes);
}
finally
{
Array.Clear(plainBytes, 0, plainBytes.Length);
if (protectedBytes.Length > 0)
{
Array.Clear(protectedBytes, 0, protectedBytes.Length);
}
}
}
public static string UnprotectFromBase64(string protectedBase64)
{
ArgumentNullException.ThrowIfNull(protectedBase64);
byte[] protectedBytes = Convert.FromBase64String(protectedBase64);
byte[] plainBytes = Array.Empty<byte>();
try
{
plainBytes = ProtectedData.Unprotect(
protectedBytes,
optionalEntropy: Entropy,
scope: DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(plainBytes);
}
finally
{
Array.Clear(protectedBytes, 0, protectedBytes.Length);
if (plainBytes.Length > 0)
{
Array.Clear(plainBytes, 0, plainBytes.Length);
}
}
}
}
الاستخدام بسيط.
string protectedPassword = DpapiSecretProtector.ProtectToBase64(password);
// JSON などへ保存
// settings.DbPasswordProtected = protectedPassword;
string password = DpapiSecretProtector.UnprotectFromBase64(settings.DbPasswordProtected);
ملفّ الإعداد يمكن أن يصير مثلاً بهذا الشكل.
{
"ApiBaseUrl": "https://api.example.com/",
"UserName": "app-user",
"PasswordProtected": "AQAAANCMnd8BFdERjHoAwE..."
}
حسن هذا الشكل:
- يمكن تحرير URL واسم المستخدم كالعادة
- يمكن حماية كلمة المرور وحدها
- بنية الإعداد واضحة
- أقلّ عرضة للحوادث من الوضع نصّاً صريحاً
9. تصاميم ما زالت خطرة رغم ذلك
حتّى مع استخدام DPAPI، التصاميم التالية ما زالت خطرة.
9.1. تدوير القيمة طويلاً بعد فكّ التشفير
ما ينبغي تجنّبه إخراج القيمة بعد فكّ التشفير إلى:
- السجلّ
- الشاشة
- الاستثناء
- كائن طويل العمر وتركها عليه
«التشفير عند الحفظ» و «آمن أيضاً أثناء الاستخدام» مشكلتان منفصلتان.
9.2. إعطاء كلّ التثبيتات سرّاً مشتركاً
تصميم إعطاء كلّ المستخدمين نفس مفتاح API لا يحلّ الأصل حتّى إن حُفظ بـ DPAPI. السبب وإلى أين يُهرَّب بدل ذلك ملخَّص في 5.2.
9.3. اختيار LocalMachine لأنّه «أسهل»
هذا أيضاً شائع حقّاً. لكنّه «سهولة» لا «محميّ». دوافع الرغبة في الاختيار وما يتّسع عندئذ كما في 6.2. إن تردّدت فانظر أسطر 6.3 الثلاثة.
9.4. الاطمئنان بإضافة تشفير ذاتيّ
إدخال تنفيذ بدل DPAPI مثل:
- دفن مفتاح AES في الشيفرة المصدريّة
- وضع مفتاح AES في بند آخر من ملفّ الإعداد
- معاملة «سلسلة غُمِّضت قليلاً» معاملة مفتاح
غالباً ضعيف الأثر.
بين «ليس نصّاً صريحاً» و «آمن» هوّة كبيرة.
flowchart TB
accTitle: خطر الاطمئنان بالتشفير الذاتيّ
accDescr: مخطّط يبيّن أنّ التشفير الذاتيّ الذي يدفن مفتاح AES في الشيفرة أو في بند آخر من ملفّ الإعداد أو يعامل سلسلة غُمِّضت معاملة مفتاح ضعيف الأثر غالباً، وأنّ بين «ليس نصّاً صريحاً» و «آمن» هوّة كبيرة.
hm1["تشفير ذاتيّ يدفن المفتاح في الشيفرة أو الإعداد"] --> npl1["حالة ليست نصّاً صريحاً"]
npl1 -.->|"هوّة كبيرة بينهما"| sfe1["حالة آمنة"]
hm1 -.-> thin1["الأثر ضعيف غالباً"]
الشكل 16: التشفير الذاتيّ الذي يضع المفتاح في نفس المكان يصل إلى «ليس نصّاً صريحاً» فقط، ولا يبلغ «آمناً».
10. حالات لا يكفي فيها DPAPI
DPAPI مريح لكنّه ليس لكلّ شيء. في الحالات التالية أولى التفكير في خيار آخر.
10.1. تريد التشغيل خارج Windows أيضاً
DPAPI / ProtectedData موجَّهان إلى Windows.
لا يمكن البناء على هذه الفرضيّة في تطبيق عابر المنصّات.
10.2. تريد معاملة نفس السرّ عبر أجهزة أو مستخدمين متعدّدين
متطلّب فكّ تشفير نفس النصّ المشفَّر على عدّة حواسيب، أو مشاركته بين عدّة مستخدمين، يخرج عن مجال براعة DPAPI في «الربط بذلك الجهاز وبذلك المستخدم».
عندئذ ينبغي التفكير في تصميم آخر يوافق المتطلّب، مثل:
- إدارة الأسرار في جهة الخادم
- أساس معلومات الاعتماد
- مصادقة Windows / المصادقة المتكاملة
- مخزن معلومات اعتماد للتطبيق
10.3. هدف الحفظ هو معلومات اعتماد المستخدم نفسها
إن كان ما تريد حفظه بوضوح زوجاً من:
- اسم المستخدم
- كلمة المرور
فأوضح من الكتابة إلى ملفّك بـ DPAPI مخزن معلومات الاعتماد الذي يوفّره Windows. هذا موضع تردّد شائع في العمل، لذا نضع مقارنة.
| الزاوية | DPAPI (ProtectedData) |
Credential Locker (PasswordVault) |
Credential Manager (CredWrite / CredRead) |
|---|---|---|---|
| موضع الحفظ | ملفّ تقرّره أنت (كيف يُوضَع النصّ المشفَّر شأن التطبيق) | مخزن معلومات اعتماد يديره Windows | مخزن معلومات اعتماد يديره Windows |
| ما يمكن حفظه | أيّ تسلسل بايتات (سلسلة اتّصال، رمز، جزء من الإعداد أيضاً) | زوج اسم مستخدم + كلمة مرور | معلومات اعتماد (بنية حسب النوع) |
| الواجهة | System.Security.Cryptography |
WinRT Windows.Security.Credentials |
Win32 (wincred.h / Advapi32.dll) |
| هل يُستخدم من تطبيق سطح المكتب | يُستخدم كما هو | يُستخدم لا من WinUI وحده بل من WPF / WinForms أيضاً (يلزم إعداد استدعاء WinRT API) | يُستخدم كما هو |
| المزامنة | لا | تجوال بين الأجهزة بحساب Microsoft | لا (مجموعة معلومات اعتماد المستخدم المحلّيّة) |
| القيود | لا قيود تُذكَر عمليّاً | حتّى 20 عنصراً لكلّ تطبيق. ليس للبيانات الكبيرة | يرتبط بجلسة تسجيل دخول الرمز الحالي |
| جهة الإدارة | التطبيق (تقرّر جهة الحفظ و ACL أيضاً) | نظام التشغيل (لا تصمّم موضع الحفظ) | نظام التشغيل (يمكن الإدارة من مدير معلومات الاعتماد في لوحة التحكّم) |
مؤشّر التفريق كالتالي.
- هدف الحفظ زوج اسم مستخدم + كلمة مرور والعدد قليل -> المرشّح الأوّل Credential Locker / Credential Manager. تستغني عن تصميم موضع الحفظ وعن حمل ACL بنفسك
- هدف الحفظ ليس شكل «اسم مستخدم + كلمة مرور» -> سلسلة اتّصال، رمز API، رمز تحديث، جزء من ملفّ الإعداد أوضح في جهة DPAPI. وهذا ما يعالجه المقال
- تريد التوريث بين الأجهزة -> تجوال Credential Locker ينفع.
CurrentUserفي DPAPI بالعكس ميزته «لا يُورَّث»، فالغرض هنا عكس ذلك - العدد كبير / الحجم كبير -> تصطدم بحدّ 20 عنصراً في Credential Locker. قرّب إلى ملفّ ذاتيّ بـ DPAPI
ومخزن معلومات الاعتماد أيضاً يركب آليّة حماية نظام التشغيل، فلا ترتيب من نوع «أأمن من DPAPI» أو «DPAPI أدنى». العمليّ الاختيار حسب شكل ما تريد حفظه وحاجة التجوال.
وأيّاً اخترت، يستحقّ في تطبيق جديد النظر أوّلاً إلى بلا كلمة مرور مثل Windows Hello / مفاتيح المرور (passkeys). إن أمكن الاستغناء عن كلمة مرور طويلة العمر أصلاً، فذلك الأقوى.
مركز هذا المقال، على أيّ حال، خطّ DPAPI العمليّ من أجل «إيقاف النصّ الصريح في ملفّ إعداد عميل Windows».
11. ترتيب أولويّة موصى به في العمل
أخيراً، إن تردّدت في العمل يسهل الترتيب بهذا التسلسل. انظر من الأعلى بالترتيب، وانزل إلى الأسفل فقط حين لا تُستوفى الشروط.
flowchart TD
Q1{"هل يمكن الاستغناء عن حمل<br/>سرّ طويل العمر على الجهاز"}
Q1 -->|"يمكن"| A1["الأولويّة 1: لا نحمل<br/>(مصادقة Windows ورموز قصيرة العمر)"]
Q1 -->|"لا يمكن"| Q2{"هل يمكن فصل السرّ<br/>لكلّ مستخدم"}
Q2 -->|"لا يمكن الفصل"| A2["راجِع التصميم من أصله<br/>ألّا يصير مفتاحاً مشتركاً"]
Q2 -->|"يمكن الفصل"| QF{"شكل ما يُحفَظ<br/>زوج اسم مستخدم + كلمة مرور؟ (القسم 10.3)"}
QF -->|"ذلك الزوج والعدد قليل"| QR1{"هل يلزم التوريث بين الأجهزة"}
QR1 -->|"نعم"| QA{"أجهزة تُزامَن<br/>بحساب Microsoft؟ (القسم 10.3)"}
QA -->|"نعم"| CL2["استخدام تجوال<br/>Credential Locker"]
QA -->|"حساب مجال / محلّيّ"| SRV
QR1 -->|"لا"| CL["Credential Locker /<br/>Credential Manager"]
QF -->|"شكل آخر مثل الرموز"| QR2{"هل يلزم التوريث بين الأجهزة"}
QR2 -->|"نعم"| SRV["DPAPI لا يورّث.<br/>الإدارة في جهة الخادم (القسم 10.2)"]
QR2 -->|"لا"| Q3{"كم حساباً يفكّ تشفير<br/>نفس السرّ"}
Q3 -->|"حساب واحد يكفي (المستخدم نفسه،<br/>أو حساب خدمة مخصّص)"| A3["الأولويّة 3: DPAPI + CurrentUser<br/>إن كان التنفيذ بلا مراقبة فأكّد<br/>تحميل الملفّ الشخصيّ (القسم 6.4)"]
Q3 -->|"يلزم فكّ التشفير<br/>من حسابات متعدّدة"| Q4{"هل يمكن الجزم بأنّ<br/>مستخدمين آخرين لا يدخلون"}
Q4 -->|"يمكن الجزم"| A4["الأولويّة 4: DPAPI + LocalMachine<br/>استثناء مع إبقاء الحجّة"]
Q4 -->|"لا يمكن الجزم"| A5["يمكن لمستخدم آخر فكّ التشفير أيضاً.<br/>راجِع أسلوب المصادقة"]
الشكل 17: ترتيب اختيار أسلوب الحفظ. انظر أوّلاً إلى شكل السرّ وحاجة التجوال ثمّ ادخل DPAPI. LocalMachine يُختار استثناء حين لا تقوم الخيارات الأخرى، لا لأنّه «أسهل»
الأولويّة 1: لا تحمل أصلاً
- مصادقة Windows
- المصادقة المتكاملة
- تسجيل الدخول التفاعليّ
- إبقاء السرّ في جهة الخادم
- رموز قصيرة العمر
الأولويّة 2: قرّب إلى سرّ لكلّ مستخدم
- per-user أفضل من سرّ مشترك
- رمز قابل للتحديث أفضل من معلومات اعتماد ثابتة طويلة العمر
- تجنّب مفتاحاً مشتركاً لكلّ العملاء
الأولويّة 3: إن لزم الحفظ المحلّيّ فـ DPAPI
- عادةً
CurrentUser - جهة الحفظ per-user
- حماية بنود السرّ وحدها
- لا تخرجه إلى السجلّ
الأولويّة 4: LocalMachine استثناء
- هل يلزم حقّاً بوحدة الجهاز
- هل لا يدخل ذلك الجهاز مستخدمون آخرون
- هل سليم كتصميم خدمة
12. الخلاصة
حين يحتاج تطبيق Windows إلى حفظ معلومات سرّيّة في ملفّ إعداد، تريد تجنّب الوضع نصّاً صريحاً.
وعلى سؤال:
«على أيّ حال سيُحفَظ المفتاح في مكان ما، أليس الأمر سواء؟»
الجواب العمليّ كالتالي.
- إن وضعتَ المفتاح في نفس المكان بتشفير ذاتيّ، فالأمر قريب جدّاً من السواء
- DPAPI ليس سواء
- يمكن تقريب إدارة المفاتيح إلى نظام التشغيل
- يمكن ربط جهة فكّ التشفير بمستخدم Windows / بالحاسوب
- يمكن ألّا يصير تسرّب الملفّ وحده تسرّب سرّ كما هو
- لكنّه لا يحلّ
- الشيفرة التي تعمل بصلاحيّات المستخدم نفسه
- جهازاً مخترَقاً بالكامل
- سرّاً مشتركاً طويل العمر لا ينبغي وضعه في العميل
الخلاصة: DPAPI ليس سوراً لكلّ شيء. لكن له أثر استبدال زجاج نافذة إعداد النصّ الصريح الشفّاف بنافذة مقبولة في الحدّ الأدنى.
في عمل عميل Windows هذا الفرق كبير جدّاً. أواقعيّ أن تبدأ من عدم تجاوز هذا الموضع.
flowchart TB
accTitle: موضع DPAPI الواقعيّ
accDescr: مخطّط يبيّن أنّ DPAPI ليس سوراً لكلّ شيء، لكن له أثر استبدال زجاج نافذة إعداد النصّ الصريح الشفّاف بنافذة مقبولة في الحدّ الأدنى، وأنّ البدء من هناك واقعيّ.
glass1["إعداد نصّ صريح = زجاج نافذة شفّاف"] -->|"الاستبدال بـ DPAPI"| win2["نافذة مقبولة في الحدّ الأدنى"]
win2 -.-> notwall1["ليس سوراً لكلّ شيء"]
win2 --> first1["البدء من هنا واقعيّ"]
الشكل 18: DPAPI ليس سوراً بل استبدال نافذة، لكن في العمل هذا الفرق هو الأكثر نفعاً.
13. روابط مرجعية
- المقال السابق: قائمة تحقّق للحدّ الأدنى من الأمان في تطوير تطبيقات Windows
- Microsoft Learn,
CryptProtectData - Microsoft Learn,
ProtectedData - Microsoft Learn,
DataProtectionScope - Microsoft Learn, How to: Use Data Protection
- Microsoft Learn, Credential locker for Windows apps
- Microsoft Learn,
CredWrite(واجهة Win32 لـ Windows Credential Manager) - NuGet, System.Security.Cryptography.ProtectedData
- Microsoft Support, You cannot access DPAPI data after an administrator resets your password
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
كيفيّة فصل «المعالجات التي تحتاج امتيازات المسؤول فقط» في تطبيقات Windows
نرتّب هنا تصميماً ملموساً يُبقي واجهة تطبيق Windows عند asInvoker ويفصل المعالجات التي تحتاج امتيازات المسؤول إلى helper EXE، شاملاً UAC ...
قائمة التحقّق الدنيا لأمان تطبيقات Windows
ننظّم في صورة قائمة تحقّق أساسيات الصلاحيات والتوقيع والتحديث والأسرار و HTTPS والتحقّق من الإدخال وتحميل DLL والسجلات لتطبيقات أعمال WPF...
الأنابيب المسمّاة عمليّاً ── IPC القياسيّ في Windows من التصميم إلى الأمان
دليل عمليّ للأنابيب المسمّاة، وسيلة الاتّصال القياسيّة بين العمليّات في Windows. ينظّم المقال، من المصادر الأوّليّة، الاختيار بين وضع الب...
عندما يُعامَل تطبيق Windows الذي طوّرته شركتك بوصفه فيروساً ── التعامل مع الكشف الخاطئ في Microsoft Defender والتعايش مع أثره على الأداء
نرتّب الإجراء الرسميّ عند كشف Microsoft Defender تطبيق Windows الذي طوّرته شركتك خطأً. نشرح آليّة برامج مكافحة الفيروسات الحديثة، والإبلا...
MAX_PATH ومطبّات المسارات وأسماء الملفّات في Windows ── حدّ 260 حرفاً، الأسماء المحجوزة، النقطة الأخيرة، حساسيّة الأحرف
ننظّم مشكلات حدود المسارات وأسماء الملفّات، وهي سبب شائع لظهور «الملفّ غير موجود». نشرح تفاصيل MAX_PATH=260 حرفاً، وتفعيل المسارات الطويل...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- ما هو DPAPI؟
- DPAPI (Data Protection API) آليّة لحماية البيانات يوفّرها Windows، تُفوَّض فيها إدارة مفاتيح التشفير إلى نظام التشغيل، وتُربَط جهة فكّ التشفير بمستخدم Windows ذلك أو بذلك الحاسوب. من C# / .NET تُستخدم عبر الصنف System.Security.Cryptography.ProtectedData دون إضافة مكتبات زائدة. أقرب إلى جوهرها أن تُرى واجهة تفويض إدارة المفاتيح إلى نظام التشغيل، لا واجهة اختيار خوارزميّة التشفير. وهي خيار واقعيّ لعدم ترك كلمات المرور ورموز API المحفوظة في ملفّ الإعداد بالنصّ الصريح.
- المفتاح سيُحفَظ في مكان ما على أيّ حال، أليس النصّ الصريح و DPAPI سواء؟
- ليسا سواء. إن وضعتَ مفتاح AES ذاتيّ في نفس التطبيق أو نفس ملفّ الإعداد فأنت قريب جدّاً من النصّ الصريح، أمّا DPAPI فيقرّب إدارة المفاتيح إلى نظام التشغيل ويربط جهة فكّ التشفير بمستخدم Windows أو بالحاسوب. لذلك تتغيّر القوّة كثيراً أمام حوادث مثل تسرّب ملفّ الإعداد وحده، أو نقله إلى حاسوب آخر، أو إرساله خطأ، أو تسرّب نسخة احتياطيّة، أو دخوله المستودع. الفرق الحاسم عن النصّ الصريح أنّ DPAPI يفصل «يمكن قراءة الملفّ» عن «يمكن استخدام السرّ».
- ما الذي لا يحميه DPAPI؟
- الشيفرة التي تعمل بصلاحيّات المستخدم نفسه تستطيع في الأساس فكّ تشفير ما يستطيع ذلك المستخدم فكّ تشفيره. لذلك لا يحمي من حالة اختراق الجهاز ببرمجيّة خبيثة، أو الاستيلاء بصلاحيّات المسؤول، أو النصّ الصريح في الذاكرة بعد فكّ التشفير. كذلك السرّ الطويل العمر الموزَّع مشتركاً على كلّ العملاء يسهل امتداده إلى الكلّ لحظة سحبه من جهاز واحد، فحفظه بـ DPAPI لا يحلّ الأصل. ما ينفع فيه DPAPI أساساً هو جانب تسرّب الملفّات وسوء الوضع والنقل دون اتصال والقراءة من مستخدم آخر.
- أيّهما يُستخدم في DataProtectionScope: CurrentUser أم LocalMachine؟
- في تطبيقات سطح مكتب Windows العاديّة الأساس CurrentUser. يُعامَل السرّ سرّ ذلك المستخدم، وتصير خاصّيّة أنّ نسخ النصّ المشفَّر وحده إلى حاسوب آخر لا يسهّل استخدامه كما هو. LocalMachine يمكن فكّ تشفيره على نطاق واسع من العمليّات على ذلك الحاسوب، فيسهل أن يصير خطراً على أجهزة مشتركة وبيئات يدخلها أكثر من مستخدم، وتضيق استخداماته كثيراً إلى خدمات Windows على جهاز موثوق لغرض واحد. إن اخترتَ LocalMachine لأنّ «الجميع يستطيع الاستخدام فهو أسهل»، فستضيق لاحقاً في الغالب.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.