לדלג לתוכן

פתרון שמירת סודות

פתרון התרגיל הראשון

במצב המשתמש, ההצלחה בתהליך השני מראה שאין תלות במשתנה חי מהתהליך הראשון. היא לא מוכיחה שכל תהליך בכל זהות יכול לפענח. המשתמש השני עשוי להצליח לפתוח את הקובץ ואז להיכשל בCryptUnprotectData, וזה בדיוק ההבדל שחיפשנו בין הרשאת אחסון לבין יכולת פענוח.

בbuild של המכונה נשנה רק את הדגלים בקריאת ההגנה.

CRYPTPROTECT_UI_FORBIDDEN | CRYPTPROTECT_LOCAL_MACHINE

בקריאת הפענוח נשאיר CRYPTPROTECT_UI_FORBIDDEN. משתמש אחר באותה מכונה שמחזיק בblob יכול לפענח אותו לפי חוזה המצב הזה. לכן שירות שמשתמש בו צריך עדיין DACL מתאים לקובץ, ובדיקת הרשאות על כל פקודה שמחזירה מידע. שינוי המשתמש לבדו אינו בדיקת הפרדה מספקת עבור blob מוגן למכונה.

פתרון התרגיל השני

הסקריפט הבא מיועד לתיקיית הניסוי בלבד, עם הקובץ שנוצר מהסוד המדומה. הוא שומר את המקור בזיכרון ומשחזר אותו גם כשבדיקה נכשלת.

$labPath = Join-Path (Get-Location) 'course-secret.bin'
$originalBlob = [IO.File]::ReadAllBytes($labPath)
if ($originalBlob.Length -lt 2) { throw 'Need a valid protected blob' }
try {
    $changedBlob = [byte[]]$originalBlob.Clone()
    $middle = [int][Math]::Floor($changedBlob.Length / 2)
    $changedBlob[$middle] = $changedBlob[$middle] -bxor 1
    $cases = @(
        [byte[]]@(),
        [byte[]]$originalBlob[0..($originalBlob.Length - 2)],
        $changedBlob
    )
    foreach ($case in $cases) {
        [IO.File]::WriteAllBytes($labPath, [byte[]]$case)
        $text = (& .\secret.exe open 2>&1 | Out-String)
        $exitCode = $LASTEXITCODE
        if ($exitCode -eq 0 -or $text.Contains('matches=1')) {
            throw 'Malformed input accepted'
        }
    }
} finally {
    [IO.File]::WriteAllBytes($labPath, $originalBlob)
}
& .\secret.exe open
if ($LASTEXITCODE -ne 0) { throw 'Valid input failed after restore' }

הקובץ הריק נדחה אצלנו לפני קריאת הפענוח. זאת בדיקה של גבול הקלט המקומי, לא ראיה על DPAPI. הקלט הקטוע והמשונה בודקים מסלולים נוספים; אין צורך לנחש באיזה שדה פנימי פגענו. במוצר לא ננסה להציל חלק מהplaintext אחרי שפענוח נכשל.

פתרון התרגיל השלישי

נשמור מזהה רשומה, גרסת schema, מזהי המפתח הישן והחדש, הblob המוגן של החדש ומצב מעבר. את ערך המפתח עצמו לא נכניס ללוג. נעדכן את הרשומה בדרך המתמשכת שלמדנו בפרק הקבצים, עם הגדרה ברורה של מה נחשב commit.

המצב הראשון הוא עבודה עם הישן בלבד. אם התהליך קרס אחרי בקשת חדש ולפני שמירתו, נבקש מהשרת לזהות את בקשת היצירה לפי מזהה idempotency, או נבצע reconciliation לפי הממשק שהוא מספק. אם אין דרך כזאת, נציין שהפעולה יכולה ליצור מפתח יתום ונבנה נוהל ביטול עבורו.

המצב השני הוא חדש שמור אך עדיין לא מאומת. בעלייה מחדש ננסה פעולה מוגדרת עם החדש ונשמור את תוצאת המעבר. נישאר עם הישן כל עוד מותר להשתמש בו, בלי לסמן הצלחה רק מפני שהblob נשמר.

המצב השלישי הוא חדש פעיל וישן שעדיין תקף. אפשר לחזור על בקשת ביטול הישן לפי חוזה השרת. אם התשובה לביטול אבדה, נבדוק מצב ולא ניצור מפתח נוסף. רק לאחר אישור שהישן בוטל נסיר את הרשומה המקומית שלו.

המקרה המעניין הוא קריסה אחרי הביטול ולפני עדכון המצב המקומי. ההתאוששות צריכה לזהות שהחדש כבר פעיל ושהישן כבר אינו תקף, ולהשלים את הניקוי. החזרה לגרסה קודמת של הקובץ לבדה לא יכולה להחיות סוד שבוטל בשרת.