PDF Library for Delphi יכולה לפתוח PDF מוצפן ממפתח הצפנת הקובץ הגולמי שלו במקום מסיסמה. DAOpenFileWithEncryptionKey מקבלת את המפתח כטקסט הקסדצימלי, בודקת אותו מול המאמת שכבר שמור במילון ההצפנה ומחזירה handle של Direct Access לקריאה בלבד; DAOpenFromStreamWithEncryptionKey עושה את אותו הדבר עבור TStream שבבעלות ה-caller. שתיהן נוספו בגרסה 3.496.0
התרחיש ממוקד, והוא אמיתי. עבודת פורנזיקה מוסרת לכם מפתח שנשלף מתמונת זיכרון בלי סיסמה. ריצת ארכוב המונית כוללת עשרת אלפים מסמכים שמפתחות הקבצים שלהם נמצאים במסד escrow, מפני שמערכת ה-DRM המקורית הפסיקה להנפיק סיסמאות לפני שנים. הגירה ממוצר ניהול זכויות שיצא משימוש משאירה חומר מפתח ולא שום דבר אחר. בכל המקרים האלה האישור שבידיכם הוא תוצר של גזירת מפתח, לא הקלט שלה, ואף פרמטר סיסמה ב-API לא יקבל אותו
מדוע מפתח הצפנת קובץ אינו סיסמה
סיסמה ומפתח הצפנת קובץ נמצאים בצדדים מנוגדים של גזירת המפתח במטפל האבטחה הסטנדרטי של PDF (ISO 32000-1 §7.6.3). המטפל לוקח סיסמה, מערבב אותה עם /O, /P, מזהה הקובץ ו-hash שתלוי בגרסה, ומפיק את מפתח הקובץ. הזנת מפתח קובץ במשבצת הסיסמה נותנת nonsense שנגזר ממנו nonsense אחר, ולכן נחוצה נקודת כניסה נפרדת ולא דגל על DAOpenFile
המקום שבו אפשר להזרים את המפתח הגולמי נקבע לפי הגרסה. גרסאות 2 עד 4 עדיין גוזרות מפתח נפרד לכל אובייקט ממפתח הקובץ, ממספר האובייקט, ממספר ה-generation וב-AESV2 גם ממלח AES, ולכן החזקת מפתח הקובץ אינה מאפשרת לדלג כלל על הגזירה ברמת האובייקט. גרסאות 5 עד 7 משתמשות ישירות במפתח הקובץ בן 32 הבתים עבור AES-256, ללא שלב לפי אובייקט. השכבה המשותפת לשתיהן היא מפתח הקובץ עצמו, ולכן זהו המקום היחיד שבו PDF Library for Delphi מקבלת מפתח חיצוני. אם הסיסמה עדיין בידיכם, הישארו בנתיב הרגיל ותנו למחזור החיים של ניסיון הסיסמה לטפל בניסיון שגוי ראשון, מפני שנתיב המפתח הגולמי מוותר בכוונה על כמה נוחות שהנתיב מבוסס-הסיסמה משמר
איזה קלט DAOpenFileWithEncryptionKey מקבלת
רק hex ASCII ללא prefix וללא רווחים, באורך זוגי, ומספר הבתים המפוענחים חייב להתאים בדיוק לגרסת ההצפנה. בגרסאות 2 עד 4 האורך הצפוי מגיע מ-/Length במילון ההצפנה: כפולה של 8 ביטים שנופלת בין 5 ל-16 בתים, עם ברירת מחדל של 40 ביט כאשר /Length חסר. בגרסאות 5 עד 7 האורך הוא בדיוק 32 בתים, בלי משא ומתן. Prefix של 0x, מספר ספרות אי-זוגי, יותר מ-64 תווי hex, ביט לא מוכר ב-Options או מסמך שאינו מוצפן כלל מובילים לאותה תוצאה: handle של 0 ו-LastErrorCode שמוגדר ל-PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, שהוא 425. הנוקשות היא העיקר. Parser סלחני שמסיר רווחים ומשלים אפסים לקלט קצר יהפוך בשמחה הדבקה קטועה מהלוח לאישור, ואז ייכשל במקום שקשה הרבה יותר לקרוא
Var
Lib: TPDFlib;
FileHandle, PageRef: Integer;
Begin
Lib:= TPDFlib.Create;
Try
// KeyHex הוא 32 תווי hex עבור AES-128 R4 ו-64 עבור AES-256 R6
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If FileHandle= 0 Then
Raise Exception.CreateFmt('raw key refused (error %d)', [Lib.LastErrorCode]);
Try
PageRef:= Lib.DAFindPage(FileHandle, 1);
WriteLn(Lib.DAExtractPageText(FileHandle, PageRef, 0));
Finally
Lib.DACloseFile(FileHandle);
End;
Finally
Lib.Free;
End;
End;
קודי הכשל האחרים נשארים נפרדים כדי שעבודת אצווה תוכל להבדיל בין טעות מפעיל לבעיות ראיה: 411 כשהקובץ אינו קיים, 401 כאשר אי אפשר לפתוח אותו לקריאה ו-409 כאשר מבנה ה-cross-reference שבור. כל מה שקשור למפתח מתכנס ל-425, בכוונה, מפני שנקודת כניסה למפתח גולמי שמדווחת איזה חלק מהמפתח היה שגוי היא oracle
מה האימות באמת מוכיח
PDF Library for Delphi מוכיחה שהמפתח שסופק שייך למסמך הזה, באמצעות המאמת שמילון ההצפנה כבר נושא, והבדיקה משתנה לפי הגרסה. גרסה 2 מחשבת מחדש את הצפנת RC4 של מחרוזת ה-padding הסטנדרטית בת 32 הבתים ומשווה את כל 32 הבתים מול /U. גרסאות 3 ו-4 מגבבות את ה-padding יחד עם מזהה הקובץ, מריצות את סבב ה-RC4 ועוד 19 סבבים הנגזרים באמצעות XOR ומשוות את 16 הבתים הראשונים של /U. גרסאות 5 עד 7 מפענחות את מחרוזת /Perms בת 16 הבתים עם IV של אפס ובודקות ארבעה דברים בלתי תלויים יחד: את מילת ההרשאות ב-little-endian מול /P, את ארבעת בתי FF במיקומים 5 עד 8, את דגל הצפנת המטא-נתונים כ-T או F ואת הסמן adb במיקומים 10 עד 12
כאשר יש מאמת אך הוא אינו תואם, הפתיחה מסורבת ללא תנאי. חשוב לומר זאת במפורש, מפני שזו ההבטחה שעליה נשענת כל התכונה. שימו לב גם למה שהאימות אינו: הוא אומר שהמפתח מפענח את הקובץ הזה, לא שמישהו הרשה לכם להשתמש בו. מילת ההרשאות ששוחזרה מ-/Perms היא ראיה לגבי המפתח ולא הרשאה, ואם אתם רוצים לדעת מה המסמך עצמו טוען שמותר לעשות, זו משימה נפרדת עבור מעבר ביקורת הצפנה והרשאות. בעיות נרמול בצד הסיסמה, כגון טיפול SASLprep בסיסמאות AES-256 שאינן ASCII, פשוט אינן קיימות כאן, מפני שאף מחרוזת אינה מגיעה ל-hash
מתי PDF_RAW_KEY_ALLOW_UNVERIFIED חל
PDF_RAW_KEY_ALLOW_UNVERIFIED מכסה מצב אחד בדיוק: למסמך אין מאמת שמיש, מפני ש-/Perms חסר או אינו באורך 16 בתים, או ש-/U קצר מדי להשוואה. הוא אינו יכול לעקוף ראיה שנכשלה. השחיתו ספרת hex אחת בתוך /Perms והעבירו את המפתח הנכון כשהאפשרות מוגדרת, ו-PDF Library for Delphi עדיין מחזירה 0 ו-425. העבירו מפתח של 32 בתים אפסיים מול מאמת שלם כשהאפשרות מוגדרת, והתשובה זהה. האפשרות מקלה על היעדר הוכחה, לעולם לא על סתירה לה. מפני שפתיחת התאוששות ופתיחה מאומתת הן מצבים אפיסטמיים שונים, הן גם מדווחות בנפרד ולא מקופלות לערך ההחזרה, ו-DAGetEncryptionKeyValidation מקבלת את ה-handle הפתוח ומשיבה אחד משלושה קבועים:
PDF_RAW_KEY_VALIDATION_VERIFIED(1) אומר שמאמת היה קיים והתאיםPDF_RAW_KEY_VALIDATION_UNVERIFIED(2) אומר שהמפתח התקבל רק מפני שלא ניתן היה להעריך מאמת וה-caller ביקש במפורש את המדיניות הזוPDF_RAW_KEY_VALIDATION_NONE(0) הוא מה שמחזיר handle שנפתח בסיסמה רגילה
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
(Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
// אין מאמת בקובץ הזה? נסה שוב תחת מדיניות התאוששות מפורשת
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex,
PDF_RAW_KEY_ALLOW_UNVERIFIED);
If FileHandle<> 0 Then
Begin
Case Lib.DAGetEncryptionKeyValidation(FileHandle) Of
PDF_RAW_KEY_VALIDATION_VERIFIED:
Chain.Note('key verified against the encryption dictionary');
PDF_RAW_KEY_VALIDATION_UNVERIFIED:
Chain.Note('no verifier available: extraction is unattested');
End;
End;
לקריאה בלבד כבר בבנייה, ומי הבעלים של הזרם
נקודת הכניסה לקובץ עם מפתח גולמי תמיד פותחת את המקור עם fmOpenRead or fmShareDenyWrite ומסמנת את כל שרשרת Direct Access כקריאה בלבד, ולכן DAAppendFile מסרבת לכתיבה במקום ומחזירה 2 במקום לנסות עדכון אינקרמנטלי. זו אינה מדיניות שאפשר לשכנע את ה-handle לוותר עליה; היא נקבעת בבנאי עוד לפני שהקובץ מפוענח. בעבודת ראיות התכונה הרצויה היא שבתי המקור יהיו זהים בייט-לבייט אחרי סגירת ה-handle, וחבילת הרגרסיה טוענת בדיוק זאת גם עבור fixture של AES-128 revision 4 וגם עבור AES-256 revision 6. כניסת הזרם מתנהגת באותו אופן בכתיבות ומוסיפה כלל אחד: DAOpenFromStreamWithEncryptionKey לעולם אינה נוטלת בעלות, ולכן DACloseFile משאירה את TStream שלכם חי ואתם משחררים אותו בעצמכם
Source:= TMemoryStream.Create;
Try
Source.LoadFromFile(ArchivePath);
Source.Position:= 0;
FileHandle:= Lib.DAOpenFromStreamWithEncryptionKey(Source, KeyHex);
If FileHandle<> 0 Then
Try
// 2 = handle לקריאה בלבד: ייצא למקום אחר, לעולם אל תוסיף לראיה
Assert(Lib.DAAppendFile(FileHandle)= 2);
Harvest(Lib, FileHandle);
Finally
Lib.DACloseFile(FileHandle);
End;
Finally
Source.Free; // ה-handle מעולם לא היה הבעלים של הזרם הזה
End;
היגיינת מפתחות ומה ה-DLL מייצא
סגירת השרשרת דורסה את מפתח הקובץ, את מטמון הסיסמה ואת מפתחות האובייקטים הנגזרים, והמפתח המפוענח נמחק בתוך בלוק Finally של נקודת הכניסה עצמה, בין אם הפתיחה הצליחה ובין אם לא. מאחורי זה יש עדינות שעלתה בזמן דיבוג אמיתי: כל עותק שצריך לשרוד משוכפל במפורש באמצעות SetLength ו-Move במקום assignment. אם תציבו AnsiString אחד באחר ב-Delphi, שני השמות חולקים מאגר אחד תחת copy-on-write, ולכן מחיקה מצד ה-caller תאפס את המפתח שבו מטפל ההצפנה עדיין משתמש, והמסמך יפוענח לזבל מסיבות שאין להן הסבר ב-stack trace. רק נקודות הכניסה מבוססות-הקובץ חוצות את גבול ה-DLL, בצורות wide ו-ANSI, יחד עם accessor של סטטוס האימות; וריאנט TStream נשאר Delphi-only מפני שהוא תלוי במחזור החיים של אובייקטי Delphi ובסמנטיקת reference שאין לה ייצוג כן ב-C ABI שטוח. אם כלי ההתאוששות שלכם הוא client של DLL, תכננו staging לקובץ זמני ומחיקה שלו תחת אותם בקרות שאתם מפעילים על המפתח
התייחסו למפתח ההקסדצימלי כחומר אישורים, עם כללי הטיפול שהייתם נותנים לסיסמת מסמך, ושמרו את סטטוס האימות בכל log ששרשרת המשמורת שלכם מפיקה, כדי שקורא מאוחר יותר יוכל להבדיל בין חילוץ מאומת לחילוץ ללא אישוש. אם אתם מעריכים רכיב PDF של Delphi לפורנזיקה, לארכוב המוני או להגירת DRM, נקודות הכניסה למפתח הגולמי, ההבטחה לקריאה בלבד ומשטח ביקורת ההצפנה הם כולם חלק מאותה ספרייה, ואת רשימת התכונות המלאה אפשר לקרוא בדף המוצר PDF Library for Delphi