שמירת מידע רגיש ביישומי Windows - נמנעים מתצורה גלויה עם DPAPI
· עודכן בתאריך: · Go Komura · פיתוח Windows, אבטחה, DPAPI, C# / .NET, Win32
במאמר הקודם, “רשימת בדיקה מינימלית לאבטחה בפיתוח יישומי 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.
מפת הידע של המאמר
המאמר הזה עוסק בשימוש ב-DPAPI (ProtectedData) כדי לשמור סודות של יישומי שולחן עבודה ב-Windows — כמו connection strings וטוקני API — באופן בטוח יותר מתצורה בטקסט גלוי. שמירה בטקסט גלוי, וכן הצפנה עצמית שבה המפתח נמצא באותו מקום כמו הטקסט המוצפן, חושפות את הסוד מיד עם דליפה של קובץ התצורה לבדו; לעומת זאת DPAPI מקטין סיכון זה על ידי קשירת גורם הפענוח למשתמש Windows מסוים או למחשב מסוים. ביישום שולחן עבודה רגיל כדאי להתבסס על הסקופ CurrentUser, ואת LocalMachine כדאי לשמור לשימושים מוגבלים כמו שירות Windows מהימן וייעודי. עם זאת, איפוס סיסמה, יצירה מחדש של פרופיל, או העתקת הטקסט המוצפן בלבד למחשב אחר גורמים לכשל פענוח — ולהפך, בפרופיל נודד (roaming) עובר גם חומר המפתח ולכן ניתן לפענח גם במחשב אחר. כאשר רוצים לשמור צמד שם משתמש וסיסמה, או כאשר נדרשת שיתוף סוד בין כמה מחשבים, Credential Locker או Credential Manager מתאימים יותר מ-DPAPI.
flowchart LR
accTitle: מפת הידע של הגנה על סודות ביישומי Windows עם DPAPI
accDescr: תרשים המראה כיצד DPAPI מאפשר להחליף בין הסקופים CurrentUser ו-LocalMachine עבור גורם הפענוח, כיצד DPAPI מקטין את סיכון הדליפה שנובע משמירה בטקסט גלוי או מהצפנה עצמית, את החלוקה מול 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["פרופיל נודד"]
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, ZIP לחקירה, דליפת גיבוי או צירוף, קובץ התצורה נקרא, וברגע הזה הסוד הגלוי נגמר כסוד.
rt1["הכנסה ל-Git / ZIP לחקירה / צירוף / דליפת גיבוי"] --> read1["קובץ התצורה נקרא"]
read1 --> end1["הסוד הגלוי נגמר באותו רגע"]
end1 -.-> low1["התוקף אפילו לא צריך להיות מתוחכם"]
איור 2: החולשה של טקסט גלוי היא שהסוד נגמר ברגע שהוא נקרא, במסלול תקרית יומיומי.
3. תשובה ל”בכל מקרה שומרים את מפתח הסוד איפשהו, אז זה לא אותו דבר?”
השאלה הזו הגיונית לגמרי. ואם עונים עליה ברשלנות, מאמר האבטחה הופך לפתע לערפילי.
התשובה: במובן ש”צריך מפתח איפשהו” — כן, ובמובן ש”לכן זה זהה” — לא.
3.1. מה זהה ומה שונה
נכון, להצפנה נדרש בסופו של דבר root of trust כלשהו. סוד לא צומח בחינם משום מקום ביקום. זה עולם קשה.
עם זאת, ההבדל מבחינת אבטחה נקבע לפי שלוש הנקודות האלה:
- האם היישום מחזיק את המפתח ישירות
- למי או למה המפתח קשור
- האם אפשר לפענח כשגונבים רק את הקובץ
אם מציגים את ההבדל הזה בטבלה גסה, זה נראה כך:
| שיטה | הקובץ נקרא | הקובץ בלבד הוצא למחשב אחר | נקרא על ידי משתמש אחר באותו מחשב | קוד שרץ באותה הרשאת משתמש |
|---|---|---|---|---|
| טקסט גלוי | דולף במקום | דולף כמו שהוא | דולף כמו שהוא | ברור שקורא |
| הצפנה עצמית + מפתח באותה תצורה / קובץ הרצה | דולף במידה רבה | דולף במידה רבה | דולף במידה רבה | ברור שמפענח |
DPAPI + CurrentUser |
לא ניתן לקריאה מיידית רק מהקובץ | בדרך כלל קשה לפענח | בדרך כלל קשה לפענח | מפענח |
DPAPI + LocalMachine |
לא ניתן לקריאה מיידית רק מהקובץ | מחוץ לאותו מחשב, בדרך כלל קשה לפענח | על אותו מחשב, ניתן לפענח באופן רחב | מפענח |
הנקודה החשובה כאן היא ש-DPAPI מפריד בין “יכולת לקרוא את הקובץ” ל”יכולת להשתמש בסוד”.
flowchart TB
accTitle: היכן נשארת המפתח הראשי מול הטקסט המוצפן
accDescr: תרשים המראה שהטקסט המוצפן הוא מה שניתן להוציא מהמכשיר, ואילו מפתח המשתמש או מפתח המחשב נשארים בצד מערכת ההפעלה ולא כלולים בטקסט המוצפן, וכיצד כל אחד מהם קובע מי יכול לפענח.
CT["טקסט מוצפן (ניתן להוצאה)"] -->|"הגנת CurrentUser"| UMK["מפתח המשתמש (בצד ה-OS)"]
CT -->|"הגנת LocalMachine"| MMK["מפתח המחשב (בצד ה-OS)"]
UMK --> A1["קוד שרץ כאותו משתמש: מפענח"]
UMK --> A2["משתמש אחר באותו מחשב: לא מפענח"]
MMK --> B1["כל קוד באותו מחשב: מפענח"]
UMK --> C1["העתקה למחשב אחר: לא מפענח"]
MMK --> C1
UMK --> C2["פרופיל נודד: מפענח (ראו 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:
- המשתמש שלח בטעות את קובץ התצורה לתמיכה
- קובץ התצורה נכנס ל-ZIP של חקירת תקלות
- קובץ התצורה בלבד דלף מגיבוי
- הועתק לתיקייה משותפת
- המפתח יכל להביא את המצב שבו רואים רק את הטקסט המוצפן בלי יכולת לקרוא את התוכן
זה יתרון מעשי משמעותי למדי. בלי להפוך את התוקף לגיבור-על מסרט, אפשר לצמצם את רדיוס התקרית היומיומית.
flowchart TB
accTitle: המנגנון שמצמצם את רדיוס התקרית
accDescr: תרשים המראה שגם בתקרית יומיומית כמו שליחה בטעות לתמיכה, ZIP לחקירה או דליפת גיבוי, מה שיוצא החוצה הוא רק הטקסט המוצפן, ולכן זה לא הופך לדליפת סוד ורדיוס התקרית מצטמצם.
ac1["שליחה בטעות · ZIP לחקירה · דליפת גיבוי"] --> out3["מה שיוצא החוצה הוא רק הטקסט המוצפן"]
out3 --> sm2["לא הופך לדליפת סוד כמו שהוא"]
sm2 --> rad1["רדיוס התקרית היומיומית מצטמצם"]
איור 5: תקרית של קובץ שיוצא החוצה לא נמנעת, אבל אפשר להפוך את מה שיוצא לטקסט מוצפן.
4. למה DPAPI מתאים בול
כשמטפלים ב-Windows בסוד ששמור מקומית, אלה הסיבות ש-DPAPI מתאים בול בעבודה מעשית.
4.1. אפשר להעביר את ניהול המפתח למערכת ההפעלה
ליצור מפתח AES בעצמכם, לשמור אותו, לתת לו הרשאות, לסובב אותו, לחשוב על ההשפעה בעת דליפה, ואפילו להוסיף גילוי שיבוש. זה כבד יותר ממה שנראה. ובנוסף, אם עושים את זה ברשלנות, בדרך כלל מסתיים בכך שהמפתח נמצא באותו מקום.
עם DPAPI, אפשר להפריד את הבעיה “איך יוצרים ואיפה שמים מפתח הצפנה” ממימוש היישום.
במובן הזה, נכון יותר להתייחס ל-DPAPI לא כ“API לבחירת אלגוריתם הצפנה”, אלא כ“API שמעביר את ניהול המפתחות למערכת ההפעלה”.
flowchart TB
accTitle: הגישה המהותית ל-DPAPI
accDescr: תרשים המראה שקרוב יותר למהות להסתכל על DPAPI לא כ-API לבחירת אלגוריתם הצפנה, אלא כ-API שמעביר את בעיית ניהול המפתחות למערכת ההפעלה, ובכך מפריד את זה ממימוש היישום.
v1["API לבחירת אלגוריתם"] -.->|"לא זו הגישה"| dpv1["DPAPI"]
v2["API שמעביר ניהול מפתחות ל-OS"] -->|"זו הגישה המהותית"| 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["גם גילוי שינוי כלול במנגנון ה-OS"]
איור 7: היתרון של DPAPI הוא שאפשר לכלול לא רק הצפנה אלא גם גילוי שיבוש במנגנון בצד מערכת ההפעלה.
4.4. אפשר להשתמש בטבעיות מ-C# / .NET
ב-C# אפשר להשתמש ישירות ב-System.Security.Cryptography.ProtectedData.
זה שלא צריך להוסיף ספריות מיותרות עוזר מאוד ביישום ייעודי ל-Windows.
5. מה DPAPI מגן עליו ומה לא
כאן בטוח יותר להפריד בבירור.
5.1. מה קל יותר להגן עליו
DPAPI יעיל לפחות במצבים הבאים:
- דליפת קובץ תצורה בטקסט גלוי
- הוצאת קובץ למחשב אחר
- עיון על ידי משתמש אחר באותו מחשב (בהנחת
CurrentUser) - דליפה כגיבוי או כקובץ מצורף
- מצב “נקרא בטעות” בסביבת פיתוח ותחזוקה
5.2. מה לא ניתן להגן עליו, או שההגנה חלשה
מצד שני, במצבים הבאים בטוח יותר לא להיות בטוחים מדי.
- קוד תקיפה שרץ באותה הרשאת משתמש
- פריצה מלאה של המכשיר עצמו
- השתלטות בהרשאות מנהל
- הטקסט הגלוי בזיכרון אחרי שהיישום פענח
- סוד ארוך-טווח שמופץ באופן זהה לכל הלקוחות
הפריט האחרון, “סוד ארוך-טווח משותף לכל הלקוחות”, חשוב במיוחד.
לדוגמה, תכנון מהסוג הבא:
- הטמעת אותו מפתח API בכל הלקוחות
- החזקת אותה סיסמה משותפת בכל המכשירים
- הפצת מפתח פענוח קבוע שמסתיים בצד הלקוח בלבד
נוטה להתפשט לכל המערכת ברגע שהוא נגנב ממכשיר אחד. ההיגיון פשוט: אם ה-1 מכשיר יכול לפענח, אפשר לחלץ ממנו את הסוד.
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. אם מתלבטים, חושבים כך
- יישום UI רגיל ←
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 יש תיעוד של תופעה כזו אחרי איפוס סיסמה על ידי מנהל | לתכנן כך שאפשר “לקבל מחדש” את הסוד. להפנות להזנה מחדש כשהפענוח נכשל |
| יצירה מחדש של הפרופיל | לפרופיל החדש יש חומר מפתחות שונה, ולכן לא ניתן לפענח את הטקסט המוצפן הישן | להחזיק גרסה לקובץ התצורה, ולא להפוך כשלון פענוח לסיום חריג |
| העתקת הטקסט המוצפן בלבד למחשב אחר | חומר המפתחות הדרוש לפענוח נמצא בצד פרופיל המשתמש, ולכן אי אפשר לקרוא רק עם הטקסט המוצפן של 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: תרשים המראה שכותבים את הקוד בהנחה ש-Unprotect עלול להיכשל עקב שינוי סביבה, תופסים את CryptographicException בלי להפיל את התהליך, ומפנים להזנה מחדש.
upx1["ניסיון Unprotect"] -->|"הצלחה"| use2["שימוש בסוד"]
upx1 -->|"כשלון בחריגה"| ctc1["תפיסת החריגה בלי להפיל"]
ctc1 --> rein1["הפניה להזנה מחדש"]
upx1 -.-> why2["הפענוח עלול להיכשל אם הקשר נשבר"]
איור 11: כותבים בהנחה שהפענוח עלול להיכשל, ומחברים את הכשלון להזנה מחדש ולא לסיום חריג.
7. הנחיות מינימליות למימוש
אם רוצים רק “להפסיק להשתמש בטקסט גלוי בקובץ התצורה” ביישום Windows, לא חייבים לסבך את התכנון יותר מדי. עם זאת, יש כמה נקודות שלא כדאי לפספס.
7.1. מגנים רק על הסוד
במקום להצפין את כל התצורה כמקשה אחת, נוח יותר להגן רק על פריטי הסוד.
לדוגמה, מפרידים כך:
- כתובת URL של השרת
- שם משתמש
- שם מסד נתונים
- דגלי תכונות
אלה בדרך כלל יכולים להישאר בטקסט גלוי.
מצד שני,
- סיסמה
- טוקן API
- טוקן רענון
- אישורים לתיקייה משותפת
אלה יעד ההגנה.
עם החלוקה הזו, מתקבל:
- עריכת תצורה נוחה
- קל לבדוק הפרש
- ברור היכן הסוד
- תפעול כולל פשוט
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 הוא לא מפתח שני שכל-יכול
ל-ProtectedData אפשר להעביר optionalEntropy.
זה נוח, אבל זה לא “קסם שהופך לבטוח אם מטמיעים את זה בבינארי” כמפתח שני.
- אם שמים באותו קובץ, זה לא הופך לסוד
- גם אם מטמיעים בבינארי כערך קבוע, זה לא סוד חזק
- ובכל זאת, זה מועיל לזיהוי שימוש ולמניעת שימוש שגוי
בעבודה מעשית, נוח להעביר
- שם היישום
- שם השימוש
- מזהה גרסה
כרצף בייטים קבוע, כדי “לא לקבל בטעות טקסט מוצפן ממטרה אחרת” — זה בערך המידה הנכונה.
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, אבל “מאיפה זה מגיע” משתנה לפי ה-target framework. אם נתקעים כאן, שם הסוג ProtectedData פשוט לא נפתר.
| יעד | פעולה נדרשת | מקור |
|---|---|---|
| .NET Framework | להוסיף לפרויקט הפניה ל-assembly בשם System.Security |
System.Security.dll |
| .NET Core / .NET 5 ואילך (כולל .NET 6 / 8) | להוסיף את חבילת ה-NuGet System.Security.Cryptography.ProtectedData |
System.Security.Cryptography.ProtectedData.dll |
החבילה הזו לא כלולה באף shared framework מ-.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. מה ששומרים הוא בעצמו אישורי המשתמש
אם מה שרוצים לשמור הוא בבירור צמד של
- שם משתמש
- סיסמה
טבעי יותר להשתמש במאגר האישורים שמוכן ב-Windows, במקום לכתוב ב-DPAPI לקובץ עצמכם. זו נקודה שנוטים להתלבט בה בעבודה מעשית, ולכן נציג השוואה.
| היבט | DPAPI (ProtectedData) |
Credential Locker (PasswordVault) |
Credential Manager (CredWrite / CredRead) |
|---|---|---|---|
| מיקום השמירה | קובץ שקובעים בעצמכם (איך שמים את הטקסט המוצפן תלוי ביישום) | מאגר אישורים שמנוהל על ידי Windows | מאגר אישורים שמנוהל על ידי Windows |
| מה אפשר לשמור | כל רצף בייטים (מחרוזת חיבור, טוקן, גם חלק מתצורה) | צמד שם משתמש + סיסמה | אישורים (מבנה לפי סוג) |
| API | System.Security.Cryptography |
Windows.Security.Credentials של WinRT |
Win32 (wincred.h / Advapi32.dll) |
| האם ניתן להשתמש מיישום שולחני | ניתן לשימוש כמו שהוא | ניתן לשימוש לא רק מ-WinUI, אלא גם מ-WPF / WinForms (נדרשת הגדרה לקריאת WinRT API) | ניתן לשימוש כמו שהוא |
| סנכרון | אין | מסתנכרן בין מכשירים עם חשבון Microsoft | אין (סט אישורים מקומי למשתמש) |
| מגבלה | בפועל אין | עד 20 רשומות ליישום. לא מיועד לנתונים גדולים | קשור לפגישת הכניסה של הטוקן הנוכחי |
| גורם הניהול | היישום (גם מיקום השמירה וגם ה-ACL נקבעים לבד) | מערכת ההפעלה (לא צריך לתכנן מיקום שמירה) | מערכת ההפעלה (אפשר לנהל מ-Credential Manager בלוח הבקרה) |
הקווים המנחים לבחירה:
- מה שרוצים לשמור הוא צמד שם משתמש + סיסמה, ומספר הרשומות קטן ← Credential Locker / Credential Manager הם המועמד הראשון. לא צריך להחזיק בעצמכם תכנון מיקום שמירה ולא ACL
- מה שרוצים לשמור לא בצורת “שם משתמש + סיסמה” ← מחרוזת חיבור, טוקן API, טוקן רענון, חלק מקובץ תצורה — כאלה, צד DPAPI טבעי יותר. זה מה שהמאמר הזה עוסק בו
- רוצים להעביר בין מכשירים ← הרומינג של Credential Locker יעיל. ה-
CurrentUserשל DPAPI, להפך, יתרונו הוא ש”לא מועבר”, ולכן כאן המטרה הפוכה - מספר הרשומות גדול / הגודל גדול ← נתקלים במגבלת 20 הרשומות של Credential Locker. עוברים ל-DPAPI עם קובץ עצמכם
בנוסף, גם תוכן מאגר האישורים בנוי על מנגנון ההגנה של מערכת ההפעלה, אז זה לא סדר של “בטוח יותר מ-DPAPI” או “DPAPI נחות”. נכון לבחור לפי צורת מה שרוצים לשמור, ולפי הצורך ברומינג.
ולא משנה מה בוחרים, ביישום חדש שווה לבדוק קודם גם פתרון בלי סיסמה כמו Windows Hello / passkey. אם אפשר מלכתחילה לא להחזיק סיסמה ארוכת-טווח, זה החזק ביותר.
מוקד המאמר הזה נשאר, בסופו של דבר, בקו המעשי של DPAPI ל“להפסיק עם קובץ תצורה בטקסט גלוי בלקוח Windows”.
11. סדר עדיפויות מומלץ לעבודה מעשית
לבסוף, כשמתלבטים בעבודה מעשית, נוח יותר לחשוב לפי הסדר הזה. בודקים מלמעלה למטה, ויורדים למטה רק כשלא מתקיים התנאי.
flowchart TD
accTitle: זרימת ההחלטה לבחירת שיטת שמירה
accDescr: תרשים המראה שבוחרים תחילה אם אפשר בלי סוד ארוך-טווח, אחר כך אם אפשר להפריד לפי משתמש, אחר כך את צורת מה שרוצים לשמור ואת הצורך בהעברה בין מכשירים, ומגיעים ל-DPAPI עם CurrentUser או LocalMachine רק כשלא מתקיימות האפשרויות הקודמות.
Q1{"אפשר בלי סוד ארוך-טווח בלקוח?"} -->|"כן"| A1["עדיפות 1: לא מחזיקים (אימות Windows / טוקן קצר)"]
Q1 -->|"לא"| Q2{"אפשר להפריד את הסוד לפי משתמש?"}
Q2 -->|"לא"| A2["בדקו אם זה מפתח משותף — תכננו מחדש"]
Q2 -->|"כן"| QF{"הצורה: צמד שם משתמש + סיסמה? (10.3)"}
QF -->|"כן, מעט רשומות"| QR1{"נדרשת העברה בין מכשירים?"}
QR1 -->|"כן"| QA{"מסתנכרן עם חשבון Microsoft? (10.3)"}
QA -->|"כן"| CL2["Credential Locker עם רומינג"]
QA -->|"דומיין / חשבון מקומי"| SRV
QR1 -->|"לא"| CL["Credential Locker / Credential Manager"]
QF -->|"טוקן וכדומה"| QR2{"נדרשת העברה בין מכשירים?"}
QR2 -->|"כן"| SRV["DPAPI לא מעביר. ניהול בצד שרת (10.2)"]
QR2 -->|"לא"| Q3{"כמה חשבונות מפענחים את אותו סוד?"}
Q3 -->|"אחד (המשתמש עצמו, או חשבון שירות ייעודי)"| A3["עדיפות 3: DPAPI + CurrentUser. בהרצה ללא נוכחות, בדקו טעינת פרופיל (6.4)"]
Q3 -->|"כמה חשבונות"| Q4{"בטוח שלא נכנסים משתמשים אחרים?"}
Q4 -->|"בטוח"| A4["עדיפות 4: DPAPI + LocalMachine כחריג מתועד"]
Q4 -->|"לא בטוח"| A5["גם משתמשים אחרים יפענחו — בדקו את שיטת האימות"]
איור 17: סדר בחירת שיטת השמירה. קודם בודקים את צורת הסוד ואת הצורך ברומינג, ורק אז נכנסים ל-DPAPI. LocalMachine נבחר לא כי “זה נוח”, אלא כחריג כשאין אפשרות אחרת.
עדיפות 1: מלכתחילה לא להחזיק
- אימות Windows
- אימות משולב
- כניסה אינטראקטיבית
- החזקת הסוד בצד השרת
- טוקן קצר-חיים
עדיפות 2: מעבירים לסוד per-user
- 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. מקורות
- המאמר הקודם: https://comcomponent.com/he/blog/windows-app-security-minimum-checklist/
- Microsoft Learn:
CryptProtectDatahttps://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata - Microsoft Learn:
ProtectedDatahttps://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata?view=windowsdesktop-10.0 - Microsoft Learn:
DataProtectionScopehttps://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.dataprotectionscope?view=windowsdesktop-10.0 - Microsoft Learn: How to: Use Data Protection https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection
- Microsoft Learn: Credential locker for Windows apps https://learn.microsoft.com/en-us/windows/apps/develop/security/credential-locker
- Microsoft Learn:
CredWrite (Win32 API של Windows Credential Manager) https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credwritew - NuGet: System.Security.Cryptography.ProtectedData https://www.nuget.org/packages/System.Security.Cryptography.ProtectedData
- Microsoft Support: תופעה שבה לא ניתן לגשת לנתוני DPAPI אחרי שמנהל מערכת איפס את הסיסמה https://support.microsoft.com/en-us/topic/you-cannot-access-dpapi-data-after-an-administrator-resets-your-password-on-a-windows-server-2012-based-domain-controller-4aa890cd-12b5-fe5c-9e68-06244e70673d
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
איך כותבים בפועל הפרדה של "רק תהליכים שדורשים הרשאות מנהל" ביישום Windows
מסדרים בפירוט, כולל UAC, runas, pipe בעל שם ואימות קלט, תכנון שבו ה-UI נשאר asInvoker ורק התהליכים שדורשים הרשאות מנהל מופרדים ל-helper ...
רשימת בדיקה מינימלית לאבטחה בפיתוח יישומי Windows
מסדרים בצורת רשימת בדיקה את היסודות של הרשאות, חתימה, עדכונים, סודות, HTTPS, אימות קלט, טעינת DLL ולוגים ביישומים עסקיים ב-WPF / WinForm...
המלכודות של יישום תקשורת טורית - עד לתכנון חיבור מחדש ויומן
המאמר מסדר, מנקודת מבט מעשית, את המלכודות שכדאי להימנע מהן ביישום תקשורת טורית לחיבור ציוד ובקרת מכשירי מדידה - מבניית מסגרות, timeout, ...
תכנון שמירת יומנים ו-dump בקריסת יישום Windows
המאמר מסדר איך לשלב יומן רגיל, סמן קריסה סופי, WER LocalDumps ותהליך ניטור, כדי שגם כשיישום Windows קורס מחריגה בלתי צפויה או מבאג בתוכני...
עיצוב UX ליישומי Windows - סדרי עדיפות לפי סביבת שימוש
המאמר מסדר מה צריך להעדיף בעיצוב UX ליישומי Windows, בטבלת החלטה לפי ToC/ToB, אמצעי קלט, סביבת שימוש ועלות טעות.
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
זה נושא שנוגע בתכנון כולל של יישום Windows, כולל שיטת שמירת אישורים, מיקום שמירת תצורה לפי משתמש, ואופן הוצאת מידע ללוגים, ולכן הוא מתאים היטב לפיתוח יישומי Windows.
ייעוץ טכני וסקירת תכנון
אם רוצים להתחיל מבחינה מחדש של תצורה גלויה ביישום קיים או מסידור השימוש בין DPAPI ל-Credential Locker, זה נושא שקל להתקדם בו כייעוץ טכני וסקירת תכנון.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מה זה DPAPI?
- DPAPI (Data Protection API) הוא מנגנון הגנת נתונים שמספק Windows, שמעביר את ניהול מפתחות ההצפנה למערכת ההפעלה, וקושר את מי שיכול לפענח למשתמש Windows הספציפי או למחשב הספציפי. מ-C# / .NET אפשר להשתמש בו דרך המחלקה System.Security.Cryptography.ProtectedData בלי להוסיף ספריות מיותרות. קרוב יותר למהות לראות בו לא API לבחירת אלגוריתם הצפנה, אלא API שמעביר את ניהול המפתחות למערכת ההפעלה. זו בחירה מעשית כדי לא להשאיר סיסמאות וטוקני API בתצורה גלויה.
- המפתח נשמר איפשהו בכל מקרה, אז זה לא אותו דבר בטקסט גלוי או ב-DPAPI?
- זה לא אותו דבר. אם שומרים את המפתח באותו יישום או קובץ תצורה עם הצפנת AES עצמית, זה קרוב מאוד לטקסט גלוי, אבל DPAPI מעביר את ניהול המפתח למערכת ההפעלה, וקושר את מי שיכול לפענח למשתמש Windows או למחשב. כתוצאה מכך, החוסן משתנה משמעותית מול תקריות כמו דליפת קובץ תצורה בודד, הוצאה למחשב אחר, שליחה בטעות, דליפת גיבוי, או הכנסה למאגר קוד. ההבדל המכריע של DPAPI מטקסט גלוי הוא שהוא יכול להפריד בין 'יכולת לקרוא את הקובץ' ל'יכולת להשתמש בסוד'.
- מה DPAPI לא מגן עליו?
- קוד שרץ באותה הרשאת משתמש יכול בעיקרון לפענח כל מה שאותו משתמש יכול לפענח. לכן זה לא מגן על מצב שבו המכשיר כבר נפרץ על ידי תוכנה זדונית, מצב של השתלטות בהרשאות מנהל, או על הטקסט הגלוי בזיכרון אחרי הפענוח. בנוסף, סוד ארוך-טווח שמפיצים באופן זהה לכל הלקוחות נוטה להתפשט לכולם ברגע שהוא נגנב ממכשיר אחד, ולכן שמירתו ב-DPAPI לא פותרת את הבעיה מהשורש. DPAPI יעיל בעיקר מול דליפת קבצים, מיקום שגוי, הוצאה במצב לא מקוון, ועיון על ידי משתמש אחר.
- איזה מבין CurrentUser ו-LocalMachine ב-DataProtectionScope כדאי להשתמש בו?
- ביישום שולחני רגיל של Windows, CurrentUser הוא הבסיס. הוא מטופל כסוד של אותו משתמש, ומקבלים את התכונה שקשה להשתמש בטקסט מוצפן שהועתק למחשב אחר כמו שהוא. LocalMachine ניתן לפענוח באופן רחב מכל תהליך שרץ על אותו מחשב, ולכן הוא נוטה להיות מסוכן במכשיר משותף או בסביבה עם כמה משתמשים, והשימוש בו מוגבל מאוד למקרים כמו שירות Windows על מכונה ייעודית ומהימנה. אם בוחרים ב-LocalMachine כי 'זה נוח לכולם', זה בדרך כלל גורם לבעיה בהמשך.