Secrets ביישומי Windows: DPAPI במקום plaintext ב-config
· עודכן בתאריך: · Go Komura · פיתוח Windows, אבטחה, DPAPI, C# / .NET, Win32
היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 16 Mar 2026)
- פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22173498)
מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.
Go Komura (2026). Secrets ביישומי Windows: DPAPI במקום plaintext ב-config. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173498 https://comcomponent.com/he/blog/windows-app-secret-storage-best-practices-dpapi/
- DOI (הגרסה האחרונה)
- 10.5281/zenodo.22173498
- DOI (הגרסה הזו)
- 10.5281/zenodo.22173499
במאמר הקודם, “רשימת בדיקה מינימלית לאבטחה בפיתוח יישומי Windows”,
כתבנו את הרף המינימלי: “לא לשים secrets בקוד או ב-plaintext ב-config” ו-“ב-Win32 / .NET, להשתמש ב-DPAPI / ProtectedData”.
הפעם נעמיק בחלק מתוך זה: “להשתמש ב-DPAPI כדי להיות לפחות טובים יותר מ-plaintext”.
היעד הוא יישומי Windows מהסוג הזה:
- אפליקציות desktop ב-WPF / WinForms / WinUI
- לקוחות Windows ב-C# / .NET
- יישומים שרוצים לשמור בקובץ config מקומי credentials של חיבור או API tokens
מה שמטופל כאן הוא תכנון מעשי ל“secret שאין ברירה אלא לשמור מקומית — לפחות לא להשאיר אותו ב-plaintext ב-appsettings.json“.
זה לא סיפור של “הגנה מושלמת שמנצחת כל תוקף”. אם מגזימים שם, יוצאים מדיון אבטחה מעשי.
1. קודם כל, המסקנה
בפועל, נוח לחשוב לפי הסדר הזה:
- מלכתחילה, לא לשים secret ארוך-טווח אצל הלקוח
- מעדיפים Windows authentication, integrated authentication, logon אינטראקטיבי של המשתמש, וניהול secrets בצד השרת
- אם באמת נדרשת שמירה מקומית, לא ב-plaintext
- ב-Windows, המועמד הראשון הוא DPAPI /
ProtectedData
- ב-Windows, המועמד הראשון הוא DPAPI /
- ביישום desktop רגיל,
DataProtectionScope.CurrentUserהוא הבסיס- השימוש ב-
LocalMachineמוגבל מאוד
- השימוש ב-
- DPAPI אינו מגן עד “המכשיר נפרץ לגמרי”
- קוד שרץ באותן הרשאות משתמש יכול, ככלל, לפענח מה שאותו משתמש יכול לפענח
והנקודה החשובה ביותר במאמר הזה היא כאן.
“בכל מקרה צריך לשמור את המפתח איפשהו, אז מבחינת אבטחה plaintext ו-DPAPI זה אותו דבר?”
זה חצי נכון, והמסקנה שגויה.
- AES עצמאי + המפתח באותו יישום או באותו config — זה קרוב מאוד ל-plaintext
- אבל DPAPI מעביר את ניהול המפתח ל-OS, וקושר את מי שיכול לפענח ל”משתמש Windows הזה” או ל”מחשב הזה”
- כתוצאה מכך, החוסן מול תקריות כמו דליפה של קובץ ה-config לבדו, הוצאה למחשב אחר, שליחה בטעות, דליפת גיבוי, והכנסה ל-repository משתנה משמעותית
כלומר: אם מסתכלים רק על הטענה האבסטרקטית “המפתח נמצא איפשהו”, זה נראה אותו דבר, אבל “מי יכול להשתמש, באיזה context, ובאיזו קלות” זה לגמרי אחר.
לשים מפתח מתחת לשטיח מול למסור מפתח אחרי זיהוי במשרד — לקרוא לזה אותו דבר זה מוגזם.
flowchart TB
accTitle: "המפתח נמצא איפשהו" לבדו לא הופך את זה לזהה
accDescr: תרשים שמראה שברמת האבסטרקציה "המפתח נמצא איפשהו" גם plaintext וגם DPAPI נראים זהים, אבל כשמסתכלים מי יכול להשתמש, באיזה context ובאיזו קלות, ההבדל גדול לגמרי, וזו הטענה המרכזית של המאמר.
abs1["המפתח נמצא איפשהו (אבסטרקציה)"] -.->|"אם מסתכלים רק על זה"| same1["נראה אותו דבר"]
who1["מי, באיזה context, באיזו קלות"] -->|"אם מסתכלים על זה"| diff1["לגמרי אחר"]
איור 1: ברמת האבסטרקציה זה נראה זהה, אבל “מי יכול להשתמש באיזה context” מפריד בין plaintext ל-DPAPI.
ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 21, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle
2. למה plaintext ב-config מסוכן
הסיבה ששמירה ב-plaintext מסוכנת היא לא תיאוריית הצפנה. בפועל זה דולף במסלולים יומיומיים לגמרי:
- מכניסים את קובץ ה-config כמו שהוא ל-Git
- קובץ ה-config נכנס בשלמותו ל-ZIP לחקירת תקלות
- מבקשים מהלקוח לצרף את קובץ ה-config לפנייה לתמיכה
- צד שלישי יכול לקרוא דרך גיבוי או שיתוף קבצים
- connection string או token יוצאים ללוג כמו שהם
- עובד שעזב או משתמש אחר יכול לקרוא קובץ באותו מכשיר
ב-plaintext, ברגע שצד שלישי קרא, זה כבר לא secret.
- פתחו את הקובץ — זהו
- העתיקו — זהו
- צירפו למייל — זהו
- נשאר ב-repository — מטפלים בזה כמעט לנצח
התוקף אפילו לא צריך להיות מתוחכם. זה שאפשר לפתוח ב-text editor כבר חלש כשלעצמו.
flowchart TB
accTitle: plaintext מאבד את הסודיות ברגע שקוראים אותו
accDescr: תרשים שמראה שדרך מסלולים יומיומיים כמו הכנסה ל-Git, ZIP לחקירה, דליפת גיבוי או צירוף, קובץ ה-config נקרא, וברגע שצד שלישי קרא את ה-plaintext הסודיות אובדת.
rt1["הכנסה ל-Git / ZIP לחקירה / צירוף / גיבוי"] --> read1["קובץ ה-config נקרא"]
read1 --> end1["ברגע שצד שלישי קרא, הסודיות אובדת"]
end1 -.-> low1["התוקף אפילו לא צריך להיות מתוחכם"]
איור 2: החולשה של plaintext היא שבמסלול תקרית יומיומי, ברגע שצד שלישי קרא, הסודיות אובדת.
3. התשובה ל”המפתח נשמר איפשהו בכל מקרה, אז זה אותו דבר?”
השאלה הזו הגיונית לגמרי. ואם עונים עליה ברשלנות, מאמר האבטחה הופך מיד לערפילי.
התשובה: “צריך מפתח איפשהו” — כן. “ולכן זה אותו דבר” — לא.
3.1. מה זהה ומה שונה
נכון, להצפנה בסוף נדרש root of trust כלשהו. כלומר, הצפנה תמיד נשענת בסוף על נקודת אמון כלשהי.
עם זאת, ההבדל מבחינת אבטחה נקבע לפי שלוש הנקודות האלה:
- האם היישום מחזיק את המפתח ישירות
- למי או למה המפתח קשור
- האם אפשר לפענח כשגונבים רק את הקובץ
אם מציגים את ההבדל הזה בטבלה גסה, זה נראה כך:
| שיטה | קראו את קובץ ה-config | הוציאו רק את הקובץ למחשב אחר | משתמש אחר באותו מחשב קרא | קוד שרץ באותן הרשאות משתמש |
|---|---|---|---|---|
| plaintext | דולף במקום | דולף כמו שהוא | דולף כמו שהוא | ברור שקורא |
| הצפנה עצמאית + המפתח באותו config / binary | דולף במידה רבה | דולף במידה רבה | דולף במידה רבה | ברור שמפענח |
DPAPI + CurrentUser |
לא ניתן לקריאה מיידית מהקובץ לבדו | בדרך כלל קשה לפענח | בדרך כלל קשה לפענח | מפענח |
DPAPI + LocalMachine |
לא ניתן לקריאה מיידית מהקובץ לבדו | מחוץ לאותו מחשב, בדרך כלל קשה לפענח | על אותו מחשב, ניתן לפענח באופן רחב | מפענח |
הנקודה החשובה כאן: DPAPI מפריד בין “אפשר לקרוא את הקובץ” לבין “אפשר להשתמש ב-secret”.
flowchart TB
accTitle: איפה נשאר ה-master key מול ה-ciphertext
accDescr: תרשים שמראה שה-ciphertext הוא מה שאפשר להוציא מהמכשיר, ואילו master key של המשתמש או של המחשב נשאר בצד ה-OS ואינו נכלל ב-ciphertext, וכיצד כל אחד מהם קובע מי יכול לפענח.
CT["ciphertext (נכנס לקובץ config / עמודה ב-DB / ZIP לחקירה = אפשר להוציא)"]
subgraph WIN["מה שנדרש לפענוח — נשאר בצד ה-OS, לא נכלל ב-ciphertext"]
UMK["master key של המשתמש (כשההגנה היא CurrentUser)"]
MMK["master key של המחשב (כשההגנה היא LocalMachine)"]
end
CT -->|"הגנה ב-CurrentUser"| UMK
CT -->|"הגנה ב-LocalMachine"| MMK
UMK --> A1["קוד שרץ כאותו משתמש: מפענח"]
UMK --> A2["משתמש אחר באותו מחשב: לא מפענח"]
MMK --> B1["קוד על אותו מחשב: מפענח גם למשתמש אחר"]
UMK --> C1["העתקת ciphertext בלבד למחשב אחר: לא מפענח"]
MMK --> C1
UMK --> C2["מעבר עם roaming profile: key material זז יחד, ולכן מפענח (סעיף 6.5)"]
איור 3: ה-ciphertext נמצא בצד שאפשר להוציא, וה-master key שנדרש לפענוח נשאר בצד ה-OS. עם זאת, טווח LocalMachine הוא כל המחשב, ואי אפשר לחסום ממנו משתמש אחר באותו מחשב.
ב-plaintext, שני אלה זהים. אם אפשר לקרוא את הקובץ, אפשר לקרוא גם את ה-secret.
אבל ב-DPAPI, לפחות ב-CurrentUser, צריך לפענח:
- כמשתמש Windows הזה
- ב-context של אותו Windows
- דרך מנגנון ההגנה של ה-OS
ההבדל הזה משמעותי מאוד בשטח, ברגע של תקרית.
3.2. “אבל אם זה אותו משתמש, בכל זאת אפשר לפענח” — נכון
זו נקודה שצריך לכתוב בלי להסתיר.
קוד שרץ באותן הרשאות משתמש יכול, ככלל, לפענח כל מה שאותו משתמש יכול לפענח.
כלומר, DPAPI לא מכוון בעיקר למצבים האלה:
- המכשיר כבר נפרץ על ידי malware
- התוקף יכול להריץ קוד כאותו משתמש
- השתלטות מלאה ברמת Administrator של המכשיר
במצב הזה, מכיוון שהיישום עצמו יכול לפענח, גם קוד התוקף יכול לפענח. כאן, “אבל זה מוצפן” לא ממש מרגיע.
DPAPI יעיל בעיקר מול “דליפת קבצים, מיקום שגוי, הוצאה offline, וקריאה על ידי משתמש אחר”.
אם מתבלבלים כאן, קורים שני הדברים:
- מזלזלים במה שאפשר להגן עליו, ולא משתמשים
- מעריכים יתר על המידה את מה שאי אפשר להגן עליו, ומרגישים בטוחים
שניהם מסוכנים, גם אם זה לא נשמע דרמטי.
flowchart TB
accTitle: איפה DPAPI יעיל ואיפה לא
accDescr: תרשים שמראה ש-DPAPI יעיל בצד של דליפת קבצים, מיקום שגוי וקריאה על ידי משתמש אחר, אך לא יעיל מול קוד תקיפה שרץ באותן הרשאות משתמש או מכשיר שכבר נפרץ.
dp2["טווח ההגנה של DPAPI"] -->|"יעיל בצד הזה"| eff1["דליפה, מיקום שגוי, משתמש אחר"]
dp2 -.->|"לא יעיל בצד הזה"| noef1["קוד תקיפה באותן הרשאות"]
dp2 -.-> mis1["בלבול כאן מוביל להערכה שגויה"]
איור 4: בלי לדעת את קו הגבול בין יעיל ללא יעיל, מגיעים לזלזול או לביטחון יתר.
3.3. אז מה היתרון
בקצרה, היתרון של DPAPI הוא זה:
“אפשר להפריד את ה-secret עצמו מקריאות של קובץ ה-config”
זה הכול.
למשל, בתקריות מהסוג הזה יש הבדל בין plaintext ל-DPAPI:
- המשתמש שלח בטעות את קובץ ה-config לתמיכה
- קובץ ה-config נכנס ל-ZIP של חקירת תקלות
- רק קובץ ה-config דלף מגיבוי
- הועתק לתיקייה משותפת
- המפתח יכול לראות רק ciphertext, בלי לקרוא את התוכן
זה יתרון מעשי משמעותי. בלי להפוך את התוקף לגיבור-על, אפשר לצמצם את היקף הנזק של תקריות יומיומיות.
flowchart TB
accTitle: איך מצטמצם היקף הנזק של התקרית
accDescr: תרשים שמראה שגם בתקרית יומיומית כמו שליחה בטעות לתמיכה, ZIP לחקירה או דליפת גיבוי, מה שיוצא החוצה הוא רק ciphertext, ולכן זה לא הופך לדליפת secret והיקף הנזק מצטמצם.
ac1["שליחה בטעות, ZIP לחקירה, דליפת גיבוי"] --> out3["מה שיוצא החוצה הוא רק ciphertext"]
out3 --> sm2["לא הופך לדליפת secret כמו שהוא"]
sm2 --> rad1["היקף הנזק של תקרית יומיומית מצטמצם"]
איור 5: תקרית שבה קובץ יוצא החוצה לא נמנעת, אבל אפשר להפוך את מה שיוצא ל-ciphertext.
4. למה DPAPI מתאים כאן בפועל
כשמטפלים ב-Windows ב-secret שנשמר מקומית, אלה הסיבות ש-DPAPI מתאים בפועל.
4.1. אפשר להעביר את ניהול המפתח ל-OS
ליצור מפתח AES בעצמכם, לשמור אותו, לתת הרשאות, לעשות rotation, לחשוב על ההשפעה בעת דליפה, ואפילו להוסיף זיהוי שינוי. זה כבד יותר ממה שנראה. ובנוסף, אם עושים את זה ברשלנות, בדרך כלל נגמר בכך שהמפתח נמצא באותו מקום.
עם DPAPI, אפשר להפריד את הבעיה “איך יוצרים מפתח הצפנה ואיפה שמים אותו” ממימוש היישום.
במובן הזה, נכון יותר להתייחס ל-DPAPI לא כ“API לבחירת אלגוריתם הצפנה”, אלא כ“API שמעביר את ניהול המפתחות ל-OS”.
flowchart TB
accTitle: איך נכון להסתכל על DPAPI
accDescr: תרשים שמראה שנכון יותר לראות ב-DPAPI לא API לבחירת אלגוריתם הצפנה, אלא API שמעביר את בעיית ניהול המפתחות ל-OS, ובכך מפריד אותה ממימוש היישום.
v1["API לבחירת אלגוריתם"] -.->|"לא זו הגישה"| dpv1["DPAPI"]
v2["API שמעביר ניהול מפתחות ל-OS"] -->|"זו הגישה הקרובה למהות"| dpv1
dpv1 --> off1["מפריד יצירה ואחסון מפתח ממימוש היישום"]
איור 6: כשמסתכלים על DPAPI כיעד להעברת ניהול המפתחות, ברור מה אפשר להפריד מהיישום.
4.2. אפשר לקשור את מי שיכול לפענח למשתמש Windows או למחשב
ביישום desktop רגיל, ברוב המצבים מספיק לבחור CurrentUser.
אפשר לפענח בהנחות האלה:
- שהמשתמש הזה מחובר (logon)
- שהעיבוד רץ ב-context של אותו משתמש
לכן מתקבלת התכונה ש-גם אם מעתיקים רק את ה-ciphertext למחשב אחר, קשה להשתמש בו כמו שהוא.
4.3. קל לכלול גם זיהוי שינוי
טעות נפוצה בהצפנה עצמאית היא לחשוב “הצפנו ב-AES, זהו”, ולשכוח זיהוי שינוי.
ל-DPAPI יש גם integrity protection על הנתונים המוצפנים, ולכן יש יתרון מעשי: קל לכלול גם זיהוי של שינוי לא מורשה ב-ciphertext במנגנון בצד ה-OS.
flowchart TB
accTitle: ההבדל בזיהוי שינוי
accDescr: תרשים שמראה שבהצפנה עצמאית נוטים לשכוח זיהוי שינוי אחרי שהצפינו ב-AES, בעוד ש-DPAPI מחזיק integrity protection ולכן קל לכלול גם זיהוי שינוי לא מורשה במנגנון בצד ה-OS.
diy1["הצפנה עצמאית"] -.-> forget1["נוטים לשכוח זיהוי שינוי"]
dpi1["DPAPI"] --> integ1["יש integrity protection"]
integ1 --> det2["גם זיהוי שינוי נכנס למנגנון ה-OS"]
איור 7: היתרון של DPAPI הוא שאפשר לכלול לא רק הצפנה אלא גם זיהוי שינוי במנגנון בצד ה-OS.
4.4. אפשר להשתמש בטבעיות מ-C# / .NET
ב-C# אפשר להשתמש ישירות ב-System.Security.Cryptography.ProtectedData.
זה שלא צריך להוסיף ספריות מיותרות עוזר מאוד ביישום ייעודי ל-Windows.
5. מה DPAPI מגן עליו ומה לא
כאן בטוח יותר להפריד בבירור.
5.1. במה קל יותר להגן
DPAPI יעיל לפחות במצבים האלה:
- דליפת קובץ config ב-plaintext
- הוצאת קובץ למחשב אחר
- קריאה על ידי משתמש אחר באותו מחשב (בהנחת
CurrentUser) - דליפה כגיבוי או כקובץ מצורף
- מצב “נקרא בטעות” בסביבת פיתוח ותחזוקה
5.2. מה לא ניתן להגן עליו, או שההגנה חלשה
מצד שני, במצבים האלה עדיף לא להיות בטוחים מדי.
- קוד תקיפה שרץ באותן הרשאות משתמש
- פריצה מלאה של המכשיר עצמו
- השתלטות בהרשאות Administrator
- ה-plaintext בזיכרון אחרי שהיישום פענח
- secret ארוך-טווח שמופץ זהה לכל הלקוחות
הפריט האחרון, “secret ארוך-טווח משותף לכל הלקוחות”, חשוב במיוחד.
למשל, תכנון מהסוג הזה:
- הטמעת אותו API key בכל הלקוחות
- אותה סיסמה משותפת בכל המכשירים
- הפצת מפתח פענוח קבוע שמסתיים בצד הלקוח בלבד
נוטה להתפשט לכל המערכת ברגע שחילצו אותו ממחשב אחד. ההיגיון פשוט: אם מחשב אחד יכול לפענח, אפשר לחלץ ממנו את ה-secret.
DPAPI יעיל כדי להפוך את מקום השמירה לטוב יותר מ-plaintext, אבל הוא לא מצדיק secret שמלכתחילה לא צריך להיות אצל הלקוח.
flowchart TB
accTitle: איך secret משותף מתפשט
accDescr: תרשים שמראה שכשמפיצים secret ארוך-טווח משותף לכל הלקוחות, כיוון שאפשר לפענח אותו ממחשב אחד לפחות, אפשר לחלץ ממנו את ה-secret, וזה מתפשט לכל המערכת ברגע שגונבים אותו ממחשב אחד.
com1["secret ארוך-טווח משותף לכל הלקוחות"] --> one4["ניתן לפענוח לפחות ממחשב אחד"]
one4 --> ext1["אפשר לחלץ את ה-secret מאותו מחשב"]
ext1 --> all1["מתפשט לכל המערכת"]
com1 -.-> np2["שמירה ב-DPAPI לא פותרת מהשורש"]
איור 8: secret משותף מתפשט לכולם ברגע שנפל מחשב אחד, ולכן בודקים קודם איפה הוא יושב, לא איך שומרים אותו.
עבור סוג ה-secret הזה, במקום להשקיע בשיטת השמירה, נכון יותר להעביר אותו לכיוונים האלה:
- שמירה בצד השרת
- הלקוח מחזיק רק token
- להפוך ל-credentials אישיים לפי משתמש
- להפוך ל-token עם תוקף
6. איך מחלקים בין CurrentUser ל-LocalMachine
זו נקודה חשובה מאוד. בחירה רשלנית משנה את המשמעות.
6.1. הבסיס הוא CurrentUser
ביישום desktop רגיל של Windows, קודם חושבים על CurrentUser כבסיס.
דוגמאות מתאימות:
- אפליקציית desktop למשתמש ב-WPF / WinForms / WinUI
- יישום שמחזיק config או credentials לפי משתמש
- יישום שמחזיק config תחת
%LocalAppData%או%AppData%
במקרה כזה, קל יותר להתייחס לזה כ-“secret של משתמש Windows הזה”.
6.2. השימוש ב-LocalMachine מוגבל מאוד
LocalMachine נראה נוח, אבל ביישום desktop רגיל הוא רחב מדי.
מה שמתאים, למשל, זה מצבים כאלה:
- Windows service על מכונה ייעודית ומהימנה
- secret שמשמש רק process ספציפי באותה מכונה
- מקרה שדורש שימוש באותו מכשיר גם כשמחליפים משתמש מחובר
עם זאת, נקודת הזהירות כבדה.
- ניתן לפענוח באופן רחב מכל process שרץ על אותו מחשב
- מסוכן במכשיר משותף, RDS, jump server, וסביבה עם כמה משתמשים
- אם בוחרים כי “בינתיים כולם יכולים להשתמש, זה נוח”, בדרך כלל זה חוזר כבעיה אחר כך
והמניעים שגורמים לרצות לבחור LocalMachine, ברוב המקרים, הם שלושת אלה:
- אפשר לקרוא גם אחרי החלפת משתמש
- אפשר לקרוא גם מה-service
- אם זה עובד, זה נוח
כולם “נוח”, לא “מוגן”.
אם בוחרים LocalMachine ביישום desktop רגיל, מרחיבים את אפשרות הפענוח גם ל-processes אחרים על אותו מחשב, ולכן המשמעות משתנה משמעותית.
flowchart TB
accTitle: מה קורה כשבוחרים LocalMachine בגלל נוחות
accDescr: תרשים שמראה שאם בוחרים LocalMachine בגלל נוחות של קריאה בין משתמשים או מה-service, אפשרות הפענוח מתרחבת גם ל-processes אחרים על אותו מחשב, וזה נוחות ולא הגנה.
ease1["נוחות: קריאה בין משתמשים / מה-service"] --> pick4["בוחרים LocalMachine"]
pick4 --> wide1["אפשרות הפענוח מתרחבת ל-processes אחרים"]
wide1 -.-> not1["זה נוחות, לא הגנה"]
איור 9: בחירה ב-LocalMachine מתוך נוחות מרחיבה את טווח הפענוח לכל המחשב.
6.3. אם מתלבטים, חושבים כך
- יישום UI רגיל ←
CurrentUser - מקרה מיוחד שבאמת רוצים להגן ברמת מכונה ←
LocalMachine - צריך לפענח מכל משתמש, אבל יש גם משתמשים אחרים על המכשיר ← בדרך כלל עדיף לבחון מחדש את התכנון
6.4. ב-service או ב-impersonation נדרשת זהירות נוספת
כשמעורבים Windows service או impersonation, המשמעות של CurrentUser נהיית כבדה יותר.
- מי חשבון ההרצה
- האם ה-profile של אותו חשבון טעון
- באיזה context מתבצע הפענוח
אם הנקודות האלה לא מדויקות, קל להגיע ל-“הצלחנו להצפין אבל לא מצליחים לפענח”. שימוש ב-service לא תמיד מסתדר עם “פשוט CurrentUser”.
במקרה של impersonation, הכשלון הטיפוסי שכתוב במפורש גם ב-Microsoft Learn הוא “Key not valid for use in specified state.”. DPAPI מחזיק את נתוני המפתח ב-user profile, ולכן אם ה-profile לא טעון, אי אפשר לפענח. לפני ה-impersonate, טוענים מראש את ה-profile של המשתמש הרלוונטי.
flowchart TB
accTitle: הכשלון הטיפוסי ב-impersonation
accDescr: תרשים שמראה ש-DPAPI מחזיק את נתוני המפתח ב-user profile, ולכן impersonate בלי profile טעון גורם לכשלון פענוח טיפוסי, ולכן צריך לטעון קודם את ה-profile של המשתמש הרלוונטי.
imp1["impersonate בלי profile טעון"] --> ferr1["הפענוח נכשל בשגיאה"]
ld1["טעינת ה-profile קודם"] --> okp1["ניתן לפענח גם אחרי impersonate"]
imp1 -.-> whyp1["המפתח נמצא ב-profile"]
איור 10: כיוון שהמפתח נמצא בצד ה-profile, טוענים את ה-profile לפני ה-impersonate.
6.5. כדאי להכיר מראש מקרים שבהם “אי אפשר לפענח” בתפעול
יותר מההצפנה עצמה, תקרית של אי-יכולת לפענח כואבת יותר בפועל. DPAPI הוא מנגנון שקושר את מי שיכול לפענח למשתמש Windows / למחשב, ולכן אם הקשר הזה נשבר, אי אפשר לקרוא.
חמישה דברים שכדאי להכיר מראש:
| מקרה | מה קורה | איך מתכוננים |
|---|---|---|
| איפוס סיסמה בידי Administrator | ההגנה שקשורה לסיסמת המשתמש מתנתקת, ולפעמים אי אפשר יותר לגשת לנתונים שהוגנו ב-DPAPI. גם בחומרי התמיכה של Microsoft יש תיעוד של תופעה כזו אחרי איפוס סיסמה על ידי Administrator | לתכנן כך שאפשר “לקבל מחדש” את ה-secret. כשהפענוח נכשל, מפנים להזנה מחדש |
| יצירה מחדש של ה-profile | ל-profile החדש יש key material אחר, ולכן אי אפשר לפענח את ה-ciphertext הישן | מחזיקים גרסה לקובץ ה-config, ולא הופכים כשלון פענוח לקריסה |
| העתקת ciphertext בלבד למחשב אחר | ה-key material הדרוש לפענוח נמצא בצד ה-user profile, ולכן אי אפשר לקרוא רק עם ciphertext של CurrentUser (זה הצד השני של “החוזק” שהוזכר ב-3.1) |
מתכננים מתוך הנחה שצריך לשמור מחדש בכל מכשיר |
| roaming profile | את זה כן אפשר לקרוא. מכיוון ש-key material זז יחד עם ה-profile, גם Microsoft Learn כותב במפורש ש”משתמש עם roaming profile יכול לפענח ממחשב אחר ברשת”. אם מתייחסים לזה כמו לשורה הקודמת, זה גורם ליצירה מחדש מיותרת של credentials בהליך ההעברה | לא להניח “כי זה מחשב אחר, אי אפשר לקרוא”. קובעים את הליך ההעברה אחרי בדיקה אם יש roaming |
| שינוי חשבון ההרצה של ה-service | אם חשבון ההרצה שונה בין זמן ה-Protect לזמן ה-Unprotect, אי אפשר לקרוא ב-CurrentUser |
כוללים בתפעול נוהל של Protect מחדש בעת שינוי חשבון |
בקיצור, כותבים קוד מתוך הנחה ש-ProtectedData.Unprotect עלול להיכשל. כשלון פענוח זורק CryptographicException, ולכן תופסים אותו ומפנים להזנה מחדש.
using System;
using System.Security.Cryptography;
using System.Text;
// protectedBase64: ciphertext שנקרא מקובץ ה-config (Base64)
// entropy: מעבירים את אותו ערך כמו ב-Protect. אפשר גם 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["שימוש ב-secret"]
upx1 -->|"כשלון בחריגה"| ctc1["תופסים את החריגה בלי לקרוס"]
ctc1 --> rein1["הפניה להזנה מחדש"]
upx1 -.-> why2["הפענוח עלול להיכשל אם הקשר נשבר"]
איור 11: כותבים בהנחה שהפענוח עלול להיכשל, ומחברים את הכשלון להזנה מחדש ולא לקריסה.
7. קווים מנחים מינימליים למימוש
אם רוצים רק “להפסיק עם plaintext בקובץ ה-config” ביישום Windows, אין צורך לסבך את התכנון יותר מדי. עם זאת, יש כמה נקודות שלא כדאי לפספס.
7.1. מגנים רק על ה-secret
במקום להצפין את כל ה-config כמקשה אחת, נוח יותר להגן רק על פריטי ה-secret.
למשל, מפרידים כך:
- URL של השרת
- שם משתמש
- שם מסד נתונים
- feature flags
אלה בדרך כלל יכולים להישאר ב-plaintext.
מצד שני,
- סיסמה
- API token
- refresh token
- credentials לתיקייה משותפת
אלה יעד ההגנה.
עם החלוקה הזו מתקבל:
- עריכת config נוחה
- קל לבדוק diff
- ברור איפה ה-secret
- תפעול כולל פשוט
flowchart TB
accTitle: החלוקה שמגנה רק על פריטי ה-secret
accDescr: תרשים שמראה שבמקום להצפין את כל ה-config, משאירים URL ושם משתמש ב-plaintext ומגנים רק על פריטים כמו סיסמה ו-token, כך שקל יותר לערוך וברור איפה ה-secret.
cfg2["קובץ ה-config"] -->|"נשאר ב-plaintext"| pl1["URL, שם משתמש, flags"]
cfg2 -->|"מוגן"| sc1["סיסמה, token וכדומה"]
sc1 --> mr1["ברור איפה ה-secret, ותפעול פשוט"]
איור 12: לא הצפנה כוללת, אלא הגנה רק על פריטי ה-secret — כך משיגים גם נוחות וגם אבטחה.
7.2. מקום השמירה — בסיס per-user
ביישום desktop רגיל, מקום השמירה הבסיסי הוא מיקום per-user.
%LocalAppData%\Vendor\App\settings.json%AppData%\Vendor\App\settings.json
לפחות, בטוח יותר לא לשים ברשלנות תחת תיקיית ההתקנה או במקום שקל לשתף.
גם עם הגנת DPAPI, אם ה-ACL של מקום השמירה רשלני, מגיעים למצב של “ה-ciphertext נקרא”, “מבנה ה-config נראה”, “יש טעות תפעול”. ההגנה אינה שכבה אחת; ככל שיש יותר שכבות זה עוזר יותר.
7.3. optionalEntropy אינו מפתח שני כל-יכול
ל-ProtectedData אפשר להעביר optionalEntropy.
זה נוח, אבל זה לא “קסם שהופך לבטוח אם מטמיעים את זה ב-binary” כמפתח שני.
- אם שמים באותו קובץ, זה לא הופך ל-secret
- גם אם מטמיעים ב-binary כערך קבוע, זה לא secret חזק
- ובכל זאת, זה מועיל לזיהוי שימוש ולמניעת שימוש שגוי
בפועל, נוח להעביר
- שם היישום
- שם השימוש
- מזהה גרסה
כרצף בתים קבוע, כדי “לא לקבל בטעות ciphertext ממטרה אחרת” — זה בערך המידה הנכונה.
flowchart TB
accTitle: המקום הנכון של optionalEntropy
accDescr: תרשים שמראה ש-optionalEntropy אינו מפתח שני קסם שהופך לבטוח בהטמעה ב-binary, אלא מתאים כתגית זיהוי שימוש שמעבירים כרצף בתים קבוע של שם היישום כדי למנוע קבלה בטעות של ciphertext ממטרה אחרת.
ent1["optionalEntropy"] -.->|"לא זו הציפייה"| key2["מפתח שני קסם"]
ent1 -->|"זה השימוש הנכון"| tag1["מזהה שם יישום / שימוש"]
tag1 --> guard1["מניעת קבלה בטעות של ciphertext ממטרה אחרת"]
איור 13: entropy אינו מפתח סודי, אלא תגית לזיהוי שימוש ולמניעת שימוש שגוי.
7.4. אפשר להכניס ciphertext ל-Git — לא
גם זו נקודה חשובה בשקט.
ה-ciphertext של DPAPI טוב בהרבה מ-plaintext, אבל זה לא אומר שאפשר להכניס את קובץ ה-config כולו ל-repository.
הסיבה פשוטה:
- ciphertext נשאר לזמן ארוך
- ייתכן שאותו מכשיר או אותו context יתקיים שוב יום אחד
- הקובץ מכיל גם מידע שאינו ה-secret
- נוצרת תרבות של “כי זה מוגן, אפשר לטפל בזה ברשלנות”
בגלל אלה.
“טוב יותר מ-plaintext” ו- “בטוח בכל מקום שהוא” הם שני דברים שונים לגמרי.
flowchart TB
accTitle: למה גם ciphertext לא נכנס ל-Git
accDescr: תרשים שמראה שה-ciphertext של DPAPI טוב יותר מ-plaintext, אבל אם מכניסים אותו ל-repository הוא נשאר זמן רב, כולל מידע נוסף, ויוצר תרבות של רשלנות, ולכן זה לא אותו דבר כמו בטוח בכל מקום.
enc1["ciphertext של DPAPI"] -->|"טוב יותר מ-plaintext"| bet2["חוסן טוב יותר לתקרית"]
enc1 -.->|"ובכל זאת"| git1["לא נכנס ל-repository"]
git1 --> rs1["נשאר זמן רב, מידע נוסף, תרבות רשלנית"]
איור 14: “טוב יותר מ-plaintext” אינו סיבה לטפל ברשלנות במקום השמירה.
7.5. לא מוציאים ללוג
התופעה שקורית לא מעט: אחרי הפענוח מוציאים הכול ללוג, והכול יורד לטמיון.
- הוצאת ה-connection string כולו בעת כשלון חיבור
- השארת כותרת Authorization ב-API בעת שגיאת 401
- ערבוב secret בהודעת exception
אם עושים את זה, גם אם הפסיקו עם plaintext בקובץ ה-config, הלוג הופך בסוף למחסן plaintext. עצוב, אבל זה לגמרי קורה בפועל.
8. דוגמת מימוש מינימלית ב-C# / .NET
8.1. קודם נדרשת הוספת reference
ProtectedData נראה כאילו כלול ב-BCL, אבל “מאיפה זה מגיע” משתנה לפי ה-target framework. אם נתקעים כאן, שם הסוג ProtectedData פשוט לא נפתר.
| יעד | פעולה נדרשת | מקור |
|---|---|---|
| .NET Framework | להוסיף לפרויקט reference ל-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, נדרש reference מפורש.
dotnet add package System.Security.Cryptography.ProtectedData
עוד נקודה שכדאי לדעת לפני המימוש.
ProtectedData ייעודי ל-Windows. מכיוון שהוא תלוי ב-DPAPI, אם קוראים לו מ-.NET על פלטפורמה שאינה Windows, נזרקת PlatformNotSupportedException. ב-codebase שמניח cross-platform, מתכננים אחרת מלכתחילה, כפי שכתוב ב-10.1.
flowchart TB
accTitle: מה בודקים לפני שימוש ב-ProtectedData
accDescr: תרשים שמראה שלפני שימוש ב-ProtectedData נדרשת הוספת reference לפי היעד, אחרת שם הסוג לא נפתר, וגם הבנה שזה ייעודי ל-Windows, אחרת קריאה מחוץ ל-Windows גורמת לחריגה.
use3["רוצים להשתמש ב-ProtectedData"] --> ref1["הוספת reference לפי היעד"]
ref1 -.->|"בלי זה"| unres1["שם הסוג לא נפתר"]
use3 --> winonly1["מבינים שזה ייעודי ל-Windows"]
winonly1 -.->|"קריאה מחוץ ל-Windows"| pnse1["חריגה בזמן ריצה"]
איור 15: לפני המימוש מוודאים שתי הנחות — הוספת ה-reference, וזה ייעודי ל-Windows.
8.2. מימוש מינימלי
להלן דוגמה מינימלית להגנה על מחרוזת שנשמרת בקובץ config עם 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);
קובץ ה-config יכול, למשל, להיראות כך:
{
"ApiBaseUrl": "https://api.example.com/",
"UserName": "app-user",
"PasswordProtected": "AQAAANCMnd8BFdERjHoAwE..."
}
היתרון בצורה הזו:
- URL ושם משתמש ניתנים לעריכה רגילה
- רק הסיסמה מוגנת
- מבנה ה-config קריא
- פחות תקלות משמירה ב-plaintext
9. תכנון שעדיין מסוכן
גם עם שימוש ב-DPAPI, התכנונים הבאים עדיין מסוכנים.
9.1. מחזיקים את הערך המפוענח זמן רב
את הערך שפוענח, אם
- מוציאים ללוג
- מציגים על המסך
- כוללים ב-exception
- משאירים על אובייקט ארוך-חיים
עדיף להימנע.
“בשמירה — מוצפן” ו- “גם בזמן שימוש — בטוח” הם עניינים נפרדים.
9.2. מחזיקים secret משותף לכל ההתקנות
תכנון שבו כל המשתמשים מחזיקים את אותו API key, גם אם שומרים אותו ב-DPAPI, לא פותר את הבעיה מהשורש. הסיבה, ולאן להעביר אותו במקום, מרוכזות ב-5.2.
9.3. בוחרים LocalMachine בגלל “נוחות”
גם זו תופעה נפוצה מאוד. אבל זו “נוחות”, לא “הגנה”. המניע לבחירה, ומה מתרחב באותו רגע, כתוב ב-6.2. אם מתלבטים, ראו את שלוש השורות ב-6.3.
9.4. מוסיפים הצפנה עצמאית ומרגישים בטוחים
במקום DPAPI, להכניס מימוש כמו
- הטמעת מפתח AES בקוד
- הצבת מפתח AES בפריט נפרד בקובץ ה-config
- התייחסות ל”מחרוזת שעברה obfuscation קל” כאל מפתח
בדרך כלל האפקטיביות חלשה.
יש פער גדול בין “לא plaintext” לבין “בטוח”.
flowchart TB
accTitle: הסכנה בהרגשת ביטחון עם הצפנה עצמאית
accDescr: תרשים שמראה שהצפנה עצמאית שמטמיעה מפתח AES בקוד או בפריט נפרד ב-config, או מתייחסת למחרוזת אחרי obfuscation כמפתח, אפקטיביות חלשה, ויש פער גדול בין לא plaintext לבין בטוח.
hm1["הצפנה עצמאית: מפתח בקוד או ב-config"] --> npl1["מצב של 'לא plaintext'"]
npl1 -.->|"פער גדול ביניהם"| sfe1["מצב של 'בטוח'"]
hm1 -.-> thin1["האפקטיביות בדרך כלל חלשה"]
איור 16: הצפנה עצמאית עם מפתח באותו מקום היא רק “לא plaintext”, ולא מגיעה ל”בטוח”.
10. מקרים שבהם DPAPI לא מספיק
DPAPI נוח, אבל הוא לא כל-יכול. במקרים הבאים כדאי לחשוב על אפשרות אחרת.
10.1. רוצים להריץ גם מחוץ ל-Windows
DPAPI / ProtectedData מיועדים ל-Windows.
ביישום cross-platform אי אפשר לבנות על ההנחה הזו.
10.2. רוצים לטפל באותו secret בכמה מכונות / כמה משתמשים
דרישה כמו רצון לפענח את אותו ciphertext בכמה מחשבים, או לשתף בין כמה משתמשים, נמצאת מחוץ לתחום החוזק של DPAPI ש”קושר לאותו מכשיר, לאותו משתמש”.
במקרה הזה, כדאי לחשוב על תכנון אחר שמתאים לדרישה, כמו:
- ניהול secrets בצד השרת
- תשתית credentials
- Windows authentication / integrated authentication
- מאגר credentials ייעודי ליישום
10.3. מה ששומרים הוא בעצמו credentials של המשתמש
אם מה שרוצים לשמור הוא בבירור צמד של
- שם משתמש
- סיסמה
טבעי יותר להשתמש במאגר ה-credentials ש-Windows מציע, במקום לכתוב ב-DPAPI לקובץ עצמכם. זו נקודה שנוטים להתלבט בה בפועל, ולכן נציג השוואה.
| היבט | DPAPI (ProtectedData) |
Credential Locker (PasswordVault) |
Credential Manager (CredWrite / CredRead) |
|---|---|---|---|
| מקום השמירה | קובץ שקובעים בעצמכם (איך שמים את ה-ciphertext תלוי ביישום) | מאגר credentials שמנוהל על ידי Windows | מאגר credentials שמנוהל על ידי Windows |
| מה אפשר לשמור | כל רצף בתים (connection string, token, גם חלק מ-config) | צמד שם משתמש + סיסמה | credentials (מבנה לפי סוג) |
| API | System.Security.Cryptography |
Windows.Security.Credentials של WinRT |
Win32 (wincred.h / Advapi32.dll) |
| האם ניתן להשתמש מיישום desktop | ניתן לשימוש כמו שהוא | ניתן לשימוש לא רק מ-WinUI, אלא גם מ-WPF / WinForms (נדרשת הגדרה לקריאת WinRT API) | ניתן לשימוש כמו שהוא |
| סנכרון | אין | מסתנכרן בין מכשירים עם Microsoft account | אין (סט credentials מקומי למשתמש) |
| מגבלה | בפועל אין | עד 20 רשומות ליישום. לא מיועד לנתונים גדולים | קשור ל-logon session של ה-token הנוכחי |
| גורם הניהול | היישום (גם מקום השמירה וגם ה-ACL נקבעים לבד) | ה-OS (לא צריך לתכנן מקום שמירה) | ה-OS (אפשר לנהל מ-Credential Manager בלוח הבקרה) |
הקווים המנחים לבחירה:
- מה שרוצים לשמור הוא צמד שם משתמש + סיסמה, ומספר הרשומות קטן ← Credential Locker / Credential Manager הם המועמד הראשון. לא צריך להחזיק בעצמכם תכנון מקום שמירה ולא ACL
- מה שרוצים לשמור לא בצורת “שם משתמש + סיסמה” ← connection string, API token, refresh token, חלק מקובץ config — כאלה, צד DPAPI טבעי יותר. זה מה שהמאמר הזה עוסק בו
- רוצים להעביר בין מכשירים ← ה-roaming של Credential Locker יעיל. ה-
CurrentUserשל DPAPI, להפך, יתרונו הוא ש”לא מועבר”, ולכן כאן המטרה הפוכה - מספר הרשומות גדול / הגודל גדול ← נתקלים במגבלת 20 הרשומות של Credential Locker. עוברים ל-DPAPI עם קובץ עצמכם
בנוסף, גם תוכן מאגר ה-credentials בנוי על מנגנון ההגנה של ה-OS, אז זה לא סדר של “בטוח יותר מ-DPAPI” או “DPAPI נחות”. נכון לבחור לפי צורת מה שרוצים לשמור, ולפי הצורך ב-roaming.
ולא משנה מה בוחרים, ביישום חדש שווה לבדוק קודם גם פתרון בלי סיסמה כמו Windows Hello / passkey. אם אפשר מלכתחילה לא להחזיק סיסמה ארוכת-טווח, זה החזק ביותר.
מוקד המאמר הזה נשאר, בסופו של דבר, בקו המעשי של DPAPI ל“להפסיק עם קובץ config ב-plaintext בלקוח Windows”.
11. סדר עדיפויות מומלץ בפועל
לבסוף, כשמתלבטים בפועל, נוח לחשוב לפי הסדר הזה. בודקים מלמעלה למטה, ויורדים למטה רק כשלא מתקיים התנאי.
flowchart TD
accTitle: זרימת ההחלטה לבחירת שיטת שמירה
accDescr: תרשים שמראה שבוחרים תחילה אם אפשר בלי secret ארוך-טווח, אחר כך אם אפשר להפריד לפי משתמש, אחר כך את צורת מה שרוצים לשמור ואת הצורך בהעברה בין מכשירים, ומגיעים ל-DPAPI עם CurrentUser או LocalMachine רק כשלא מתקיימות האפשרויות הקודמות.
Q1{"אפשר בלי secret ארוך-טווח אצל הלקוח?"} -->|"כן"| A1["עדיפות 1: לא מחזיקים (Windows authentication / token קצר-חיים)"]
Q1 -->|"לא"| Q2{"אפשר להפריד את ה-secret לפי משתמש?"}
Q2 -->|"לא"| A2["בדקו אם זה מפתח משותף — תכננו מחדש"]
Q2 -->|"כן"| QF{"הצורה: צמד שם משתמש + סיסמה? (10.3)"}
QF -->|"כן, מעט רשומות"| QR1{"נדרשת העברה בין מכשירים?"}
QR1 -->|"כן"| QA{"מסתנכרן עם Microsoft account? (10.3)"}
QA -->|"כן"| CL2["Credential Locker עם roaming"]
QA -->|"domain / local account"| SRV
QR1 -->|"לא"| CL["Credential Locker / Credential Manager"]
QF -->|"token וכדומה"| QR2{"נדרשת העברה בין מכשירים?"}
QR2 -->|"כן"| SRV["DPAPI לא מעביר. ניהול בצד שרת (10.2)"]
QR2 -->|"לא"| Q3{"כמה חשבונות מפענחים את אותו secret?"}
Q3 -->|"אחד (המשתמש עצמו, או service account ייעודי)"| A3["עדיפות 3: DPAPI + CurrentUser. בהרצה ללא נוכחות, בדקו טעינת profile (6.4)"]
Q3 -->|"כמה חשבונות"| Q4{"בטוח שלא נכנסים משתמשים אחרים?"}
Q4 -->|"בטוח"| A4["עדיפות 4: DPAPI + LocalMachine כחריג מתועד"]
Q4 -->|"לא בטוח"| A5["גם משתמשים אחרים יפענחו — בדקו את שיטת האימות"]
איור 17: סדר בחירת שיטת השמירה. קודם בודקים את צורת ה-secret ואת הצורך ב-roaming, ורק אז נכנסים ל-DPAPI. LocalMachine נבחר לא כי “זה נוח”, אלא כחריג כשאין אפשרות אחרת.
עדיפות 1: מלכתחילה לא להחזיק
- Windows authentication
- integrated authentication
- logon אינטראקטיבי
- החזקת ה-secret בצד השרת
- token קצר-חיים
עדיפות 2: מעבירים ל-secret per-user
- per-user עדיף על secret משותף
- token שניתן לעדכון עדיף על credential קבוע ארוך-טווח
- נמנעים ממפתח משותף לכל הלקוחות
עדיפות 3: אם נדרשת שמירה מקומית, DPAPI
- בדרך כלל
CurrentUser - מקום השמירה — per-user
- מגנים רק על פריטי ה-secret
- לא מוציאים ללוג
עדיפות 4: LocalMachine כחריג
- האם באמת צריך הגנה ברמת מכונה
- האם לא נכנסים משתמשים אחרים באותו מכשיר
- האם זה הגיוני כתכנון service
12. סיכום
כשצריך לשמור מידע רגיש בקובץ config של יישום Windows, כדאי להימנע משמירה ב-plaintext.
ולשאלה:
“בכל מקרה שומרים את המפתח איפשהו, אז זה אותו דבר?”
התשובה המעשית היא:
- בהצפנה עצמאית עם מפתח באותו מקום, זה כמעט אותו דבר
- DPAPI אינו אותו דבר
- אפשר להעביר את ניהול המפתח ל-OS
- אפשר לקשור את מי שיכול לפענח למשתמש Windows / למחשב
- אפשר להימנע מכך שדליפת קובץ בודד תהפוך ישירות לדליפת secret
- עם זאת, זה לא פותר
- קוד שרץ באותן הרשאות משתמש
- מכשיר שנפרץ לגמרי
- secret משותף ארוך-טווח שמלכתחילה לא צריך להיות אצל הלקוח
בקיצור, DPAPI אינו חומת מגן מושלמת. אבל יש לו אפקט של החלפת חלון plaintext פתוח לרווחה בקובץ ה-config, בחלון סביר מינימלית.
בלקוח Windows בפועל, ההבדל הזה גדול למדי. הכי מעשי להתחיל מלא לפספס את הנקודה הזו.
flowchart TB
accTitle: המיקום המעשי של DPAPI
accDescr: תרשים שמראה ש-DPAPI אינו חומת מגן מושלמת, אבל הוא מחליף config ב-plaintext — חלון פתוח לרווחה — בחלון סביר מינימלית, ומשם נכון להתחיל.
glass1["plaintext ב-config = חלון פתוח לרווחה"] -->|"מחליפים ב-DPAPI"| win2["חלון סביר מינימלית"]
win2 -.-> notwall1["לא חומת מגן מושלמת"]
win2 --> first1["משם נכון להתחיל בפועל"]
איור 18: DPAPI אינו חומה אלא החלפת חלון, אבל בפועל זה ההבדל שהכי משפיע.
13. מקורות
- מאמר קודם: https://comcomponent.com/he/blog/windows-app-security-minimum-checklist/
- 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 API של Windows Credential Manager) - NuGet, System.Security.Cryptography.ProtectedData
- Microsoft Support, You cannot access DPAPI data after an administrator resets your password
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
UAC ב-Windows: איך מפרידים רק פעולות שדורשות Administrator
באפליקציית Windows משאירים את ה-UI ב-asInvoker ומפרידים רק פעולות שדורשות הרשאות Administrator ל-helper EXE. המאמר עובר בפירוט על UAC, ru...
Checklist מינימלי לאבטחת אפליקציות Windows
Checklist לאפליקציות WPF / WinForms / WinUI / C++ / C#: admin rights, code signing, updates, secrets, HTTPS, validation של קלט, טעינת DLL...
Named Pipes בפועל — ה-IPC הסטנדרטי של Windows, מתכנון עד אבטחה
מדריך מעשי ל-Named Pipes, ה-IPC הסטנדרטי ב-Windows. המאמר מסדר לפי מקורות ראשוניים את הבחירה בין byte mode ל-message mode, תכנון server ל...
Exception לא צפוי ב-.NET: מתי מסיימים תהליך ומתי ממשיכים
מתי מסיימים אפליקציית Windows אחרי exception לא צפוי ומתי אפשר להמשיך: לפי state corruption, side effect חיצוני, threads וגבול native.
הרשת עובדת אבל Windows מציג "אין אינטרנט" — לבודד NCSI, DNS, Proxy ו-VPN ב-Windows
למה Windows מציג "אין אינטרנט" בזמן שהרשת עובדת — נקודת המוצא היא קביעת הקישוריות של NCSI. כאן מפרידים בין DNS, proxy, VPN ו-captive port...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
אופן שמירת credentials, איפה נשמר config per-user, ומה יוצא ללוגים — זה נוגע בתכנון של יישום Windows כולו, ולכן זה מתאים ל-Windows Custom Software Development.
ייעוץ טכני וסקירת תכנון
אם רוצים להתחיל מבחינה מחדש של plaintext ב-config ביישום קיים, או לסידור מתי להשתמש ב-DPAPI ומתי ב-Credential Locker, זה נושא שנוח לקדם כייעוץ טכני וסקירת תכנון.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מה זה DPAPI?
- DPAPI (Data Protection API) הוא מנגנון להגנת נתונים ש-Windows מספק. הוא מעביר את ניהול מפתחות ההצפנה ל-OS, וקושר את מי שיכול לפענח למשתמש Windows הספציפי או למחשב הספציפי. מ-C# / .NET משתמשים בו דרך המחלקה System.Security.Cryptography.ProtectedData, בלי להוסיף ספריות מיותרות. נכון יותר לראות בו לא API לבחירת אלגוריתם הצפנה, אלא API שמעביר את ניהול המפתחות ל-OS. זו בחירה מעשית כדי לא להשאיר סיסמאות ו-API tokens ב-plaintext בקובץ config.
- המפתח נשמר איפשהו בכל מקרה, אז plaintext ו-DPAPI זה אותו דבר?
- לא. אם מצפינים ב-AES עצמאי ושומרים את המפתח באותו יישום או באותו קובץ config, זה קרוב מאוד ל-plaintext. DPAPI מעביר את ניהול המפתח ל-OS, וקושר את מי שיכול לפענח למשתמש Windows או למחשב. בגלל זה החוסן משתנה משמעותית מול תקריות כמו דליפה של קובץ ה-config לבדו, הוצאה למחשב אחר, שליחה בטעות, דליפת גיבוי, או הכנסה ל-repository. ההבדל המכריע מול plaintext: DPAPI מפריד בין 'אפשר לקרוא את הקובץ' לבין 'אפשר להשתמש ב-secret'.
- מה DPAPI לא מגן עליו?
- קוד שרץ באותן הרשאות משתמש יכול, ככלל, לפענח כל מה שאותו משתמש יכול לפענח. לכן DPAPI לא מגן על מצב שבו המכשיר כבר נפרץ על ידי malware, על השתלטות בהרשאות Administrator, או על ה-plaintext בזיכרון אחרי פענוח. בנוסף, secret ארוך-טווח שמפיצים זהה לכל הלקוחות נוטה להתפשט לכולם ברגע שחילצו אותו ממחשב אחד, ולכן שמירה ב-DPAPI לא פותרת את זה מהשורש. DPAPI יעיל בעיקר מול דליפת קבצים, מיקום שגוי, הוצאה offline, וקריאה על ידי משתמש אחר.
- ב-DataProtectionScope, לבחור CurrentUser או LocalMachine?
- ביישום desktop רגיל של Windows, CurrentUser הוא הבסיס. מתייחסים לזה כ-secret של אותו משתמש, ומקבלים את התכונה ש-ciphertext שהועתק למחשב אחר קשה לשימוש כמו שהוא. LocalMachine ניתן לפענוח באופן רחב מכל process שרץ על אותו מחשב, ולכן הוא מסוכן במכשיר משותף או בסביבה עם כמה משתמשים. השימוש בו מוגבל מאוד — למשל Windows service על מכונה ייעודית ומהימנה. אם בוחרים LocalMachine כי 'כולם יכולים להשתמש, זה נוח' — בדרך כלל זה חוזר כבעיה אחר כך.