רכיב ה-PDF של HotPDF ל-Delphi מממש את מודל מסנני ה-Crypt של ISO 32000-1 §7.6.5 כשלוש מדיניות בלתי תלויות במקום מתג אחד: ConfigureCryptFilterDefaults מקצה בנפרד את מסנן המחרוזות /StrF, את מסנן הזרמים /StmF ואת מסנן הקבצים המוטמעים /EFF, SetStreamCryptFilter עוקפת זרם יחיד, ו-GetLoadedCryptFilterInfo מדווחת מה קובץ נכנס מצהיר. רוב באגי ה-interoperability של PDF מוצפן חיים בפערים שבין שלושתם
הנה הכשל ששולח אנשים לשכבה הזו. צוות מפיץ מסמך שבו תוכן העמוד חייב להישאר קריא לכלי downstream אך המטען המצורף לא, ולכן מגדיר /EFF /StdCF ומשאיר /StmF /Identity. Acrobat פותחת אותו היטב. קורא צד-שלישי תואם מחזיר את הקובץ המצורף כ-garbage מוצפן, מפני ש-/EFF הוא מדיניות בצד היצרן לגבי המסנן שחל על קבצים מוטמעים וקורא כללי עדיין פותר זרם ללא סימון דרך /StmF. התיקון אינו ערך /EFF אחר. התיקון הוא מסנן /Crypt מפורש על זרם הקובץ המוטמע עצמו
במה שכבת מסנני ה-Crypt באמת שולטת
מסנני Crypt יושבים בין אלגוריתם ההצפנה לגרף האובייקטים, והם קובעים אילו אובייקטים האלגוריתם נוגע בהם ולא כיצד הוא פועל. מילון /CF בתוך מילון ההצפנה ממפה שמות להגדרות מסנן, שכל אחת נושאת שיטת /CFM, /Length אופציונלי ו-/AuthEvent. שלושת השדות ברמה העליונה, /StrF, /StmF ו-/EFF, בוחרים לאחר מכן איזה מהמסננים בעלי השם חל על מחרוזות, על זרמים ללא מסנן מפורש ועל קבצים מוטמעים. HotPDF מגבילה בכוונה את מה שה-handlers המובנים שלה יכתבו. ConfigureCryptFilterDefaults מקבלת רק את השמות השמורים עבור ה-handler הפעיל: ה-handler הסטנדרטי מפיק /StdCF או /Identity, ה-handler של מפתח ציבורי מפיק /DefaultCryptFilter או /Identity, וכל דבר אחר מעלה EArgumentException באתר הקריאה. מסננים שיצרנים חיצוניים כתבו תחת שמות אחרים עדיין נשמרים בנתיבי טעינה, inspection ו-compatibility-rewrite, כך ש-HotPDF שמרנית ככותבת ומתירנית כקוראת. שתי הגנות נוספות חלות: הקריאה מעלה EInvalidOpException מרגע שהסריאליזציה של המסמך החלה, ושוב אם המסמך נמצא בעדכון אינקרמנטלי, מפני שמדיניות הצפנה אינה יכולה להשתנות בין גרסאות של אותו קובץ
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'wrapper.pdf';
Pdf.OwnerPassword := 'owner-secret';
Pdf.UserPassword := 'open-secret';
Pdf.CryptKeyLength := aes128;
// מחרוזות מוצפנות, זרמי עמוד plaintext, קבצים מצורפים מוצפנים
Pdf.ConfigureCryptFilterDefaults('StdCF', 'Identity', 'StdCF');
Pdf.ActivateProtection := True;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(50, 50, 0, 'Visible stream operators');
Pdf.AddDocumentAttachment('payload.bin', 'Encrypted payload');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
מגבלה אחת כדאי לציין מראש, מפני שהיא נבדקת מאוחר ומפתיעה אנשים. מסנני Crypt בעלי שם ב-HotPDF דורשים הצפנת מסמך מסוג aes128, aes256 או aesgcm. הגדירו מדיניות מסנן מעל RC4 k40 או k128, ומעבר האימות שרץ כשההצפנה מופעלת מעלה שגיאה במקום לקדם בשקט את סוג המפתח. זו אותה עמדה תכנונית של שאר נתיב הצפנת PDF ב-AES-256 ב-Delphi: לדחות תצורה עמומה במקום לנחש למה התכוון ה-caller
מדוע ערך /Length אומר שני דברים שונים
מפני שהמפרט מגדיר אותו בשתי יחידות שונות לפי ה-security handler, ו-HotPDF חייבת לכבד את שתיהן. במילון מסנן Crypt שבו /CFM הוא /V2, ערך /Length מבוטא בבתים תחת ה-handler הסטנדרטי ובביטים תחת handler של מפתח ציבורי. /Length של מילון ההצפנה, שיושב לצד /V (ISO 32000-1 §7.6.2), הוא תמיד בביטים. קראו מילון מסנן שנושא /Length 16 וקיבלתם מפתח בן 128 ביט בקובץ של handler סטנדרטי וקובץ שנדחה בקובץ של מפתח ציבורי. HotPDF מנרמלת זאת כשהיא לוכדת את התצורה הטעונה. היא מכפילה את /Length של מסנן /V2 בשמונה רק כאשר הקובץ אינו מוצפן במפתח ציבורי, חוזרת ל-/Length ברמת המסמך כאשר המסנן משמיט את שלו ושומרת את התוצאה ב-THPDFCryptFilterInfo.KeyLengthBits. AESV2 מקובע ל-128 ביט ו-AESV3 ו-AESV4 ל-256, מפני שלשיטות האלה אין גודל מפתח שניתן למשא ומתן. החלק הנוקשה מגיע אחר כך: מתקבלים רק /V2 של 40 ביט ושל 128 ביט. מסנן שנפתר לאורך אחר מדווח כלא זמין והפעולה נכשלת, במקום להתעגל ל-128 מתוך ההנחה שרוב היצרנים התכוונו ל-128. נרמול שקט של אורך מפתח הוא הדרך לשלוח קובץ שמתפענח אצלכם ולא בשום מקום אחר
var
Reader: THotPDF;
Info: THPDFCryptFilterInfo;
I: Integer;
begin
Reader := THotPDF.Create(nil);
try
Reader.AutoLaunch := False;
if Reader.LoadFromFile('incoming.pdf', 'open-secret') <> 1 then
Exit;
// /StrF ו-/StmF הם ברירת מחדל Identity; /EFF הוא ברירת מחדל /StmF
WriteLn(Reader.LoadedStringCryptFilterName); // StdCF
WriteLn(Reader.LoadedStreamCryptFilterName); // Identity
WriteLn(Reader.LoadedEmbeddedFileCryptFilterName); // StdCF
for I := 0 to Reader.GetLoadedCryptFilterCount - 1 do
if Reader.GetLoadedCryptFilterInfo(I, Info) then
if (Info.Method = hcfmV2) and
not (Info.KeyLengthBits in [40, 128]) then
raise Exception.CreateFmt(
'crypt filter /%s: unsupported V2 key length %d',
[String(Info.Name), Info.KeyLengthBits]);
finally
Reader.Free;
end;
end;
מה /CFM /None מבטיח ואיך /Identity שונה
הם מגיעים לאותה תוצאה במסלולים שונים, וערבוב ביניהם שובר lookups. מסנן בעל שם ש-/CFM שלו הוא /None, ומסנן בעל שם שמשמיט את /CFM לחלוטין, פירושם ששום הצפנה או פענוח אינם מתבצעים במסנן הזה — HotPDF ממפה את הערך החסר ל-None לפני הפתרון, ולכן שניהם מגיעים ל-hcfmNone עם אורך מפתח מתועד של אפס. /Identity שונה במהותו: זהו השם השמור שעוקף את lookup של /CF לחלוטין, ולכן מסמך יכול להפנות ל-/Identity בלי להגדיר אותו כלל ב-/CF. שמות PDF רגישים לאותיות גדולות וקטנות, ולכן פרט מימוש נוסף אינו נתון למשא ומתן: שום lookup של מסנן Crypt אינו יכול להיות case-insensitive. HotPDF פותרת שמות בתת-המילון /CF, את ערך /Length של המסנן ואת בדיקת /Type של הזרם באמצעות lookups רגישים ל-case. קובץ שמגדיר /stdcf בעוד /StmF מצביע על /StdCF פגום, והתייחסות לשניהם כאותו key תהפוך באג כתיבה שניתן לזהות למפתח שגוי שמוחל בשקט על כל זרם במסמך
איך גורמים ל-/EFF להיצמד לזרמי קבצים מוטמעים
כאשר /EFF שונה מ-/StmF, זרם הקובץ המוטמע זקוק לאיבר /Crypt מוביל מפורש ב-/Filter שלו ולמילון /DecodeParms תואם שנושא /Name באותו מיקום במערך. HotPDF מחשבת זאת לכל זרם בזמן השמירה: היא מזהה /Type /EmbeddedFile, יורשת את מסנן הקובץ המוטמע שהוגדר ופולטת את הסמן המפורש /Crypt רק כאשר השם שיורש שונה מברירת המחדל האפקטיבית של הזרם. כאשר /EFF ו-/StmF זהים, לא נכתב סמן, מפני שקורא יפתור בכל מקרה את אותו מסנן. מיקום המערך חשוב אז לא פחות מהשם. כאשר HotPDF קוראת זרם חזרה, היא סורקת את /Filter עבור איבר /Crypt, מתעדת את האינדקס שלו ולאחר מכן מחפשת את אותו אינדקס במערך /DecodeParms כדי למצוא את /Name. /Crypt באינדקס 0 עם פרמטרים באינדקס 1 נפתר ל-/Identity, לא למסנן שלכם. זו גם הסיבה שהכותבת מרפדת את מערך הפרמטרים ב-null כאשר לזרם היה קודם /Filter אך לא היה /DecodeParms: המיקומים חייבים להישאר מיושרים
מתחת לזה יש מלכודת חדה יותר. אם /Filter או /DecodeParms הקיימים הם אובייקט עקיף — דבר נפוץ בקבצים ממחוללים שחולקים מערך מסננים אחד בין זרמים רבים — הכנסת /Crypt במקום תשנה גרף מסננים משותף ותשחית כל זרם אחר שמצביע אליו. HotPDF פותרת את האובייקט העקיף ומשכפלת אותו תחילה לאובייקט ישיר ופרטי לזרם, תוך ניקוי מספרי האובייקט וה-generation כך שהשורש העקיף המקורי לעולם אינו מוטמע בתוך המערך החדש. עבור זרם שכבר השתמש ב-ASCIIHexDecode התוצאה המסוריאלית היא /Filter [ /Crypt /ASCIIHexDecode ] עם /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ]. אותה משמעת מיקומית מנהלת כל שרשרת מסננים אחרת, כולל אלה שבהם אתם עוברים כאשר מחלצים תמונות מ-PDF טעון דרך מסנני הפענוח שלהן
// לעורך כבר יש מסמך טעון, ו-ContentStream הוא
// THPDFStreamObject שה-/Filter שלו הוא שם עקיף של /ASCIIHexDecode
Editor.OwnerPassword := 'owner-secret';
Editor.UserPassword := 'open-secret';
Editor.CryptKeyLength := aes128;
Editor.ConfigureCryptFilterDefaults('StdCF', 'Identity');
Editor.SetStreamCryptFilter(ContentStream, 'StdCF');
Editor.ActivateProtection := True;
Editor.SaveLoadedDocument('out.pdf');
// שם ריק מנקה את העקיפה ומסיר את ה-/Crypt המיושן
// יחד עם פרמטרי הפענוח שלו בשמירה הבאה
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');
האם זרמי אובייקטים יורשים את מדיניות /Encrypt של המסמך
לא, והנחה כזו היא דרך בטוחה לייצר garbage. זרם אובייקטים חייב לעקוב אחר מדיניות /StmF בפועל או אחר סמן /Crypt מפורש משלו: עצם קיומו של מילון /Encrypt אינו הופך כל מיכל /ObjStm ל-ciphertext. מסמך עם /StmF /Identity מחזיק זרמי אובייקטים גלויים אף שהמחרוזות שלו מוצפנות במלואן, ומפענח שמפענח גם אותם מזין לשלב ה-inflate קלט שמעולם לא היה פלט deflate
התוצאה עבור אובייקטי החברים היא החלק שכדאי לקרוא פעמיים. לפי ISO 32000-1 §7.5.7, מחרוזות בתוך זרם אובייקטים מוצפן כבר plaintext ברגע שהמיכל עצמו מפוענח, ולכן פענוח נוסף שלהן יהיה double-decrypt. HotPDF מגינה על כך בכך שהיא שואלת אם המיכל של כל אובייקט מסוג 2 היה מוצפן ומדלגת על האובייקט כאשר היה כזה, תוך ספירת הדילוגים ב-XRefProbeDecryptObjStmSkips כראיה ישירה שההגנה הופעלה. כאשר המיכל היה plaintext, מחרוזות החברים מעולם לא כוסו בדבר, ולכן HotPDF מממשת את החברים ומחילה /StrF על כל אחד מהם בנפרד — עם keying, כפי שהמימוש עושה בפועל, לפי מספר האובייקט וה-generation של החבר ולא לפי מספר האובייקט /ObjStm המכיל. הופכים את זה בקובץ עם מדיניות מעורבת וכל מחרוזת בכל אובייקט דחוס מתפענחת לרעש. כללי המיכל ברמה הזו מכוסים בהמשך בהערות על זרמי אובייקטים של PDF ועדכונים אינקרמנטליים
היכן HotPDF מסרבת לנחש
סמנטיקת מסנני Crypt אינה קיימת מתחת ל-/V 4, ולכן HotPDF דוחה כל עקיפה לפי-זרם בקובץ כזה עם שגיאה מפורשת במקום לכתוב סמן /Crypt שאף קורא תואם לא יכבד. אותו דבר בצד הקריאה: מילון הצפנה עם /V מתחת ל-4 מנקה את כל שלושת שמות המסננים הטעונים, מפני שאין שם דבר לדווח עליו. שלושה גבולות נוספים נאכפים בכוונה:
- מסנן לפי-זרם שאינו
Identityבמסמך מוצפן במפתח ציבורי נדחה, מפני שמדיניות ספציפית לזרם תחת handler של מפתח ציבורי דורשת מעטפת recipient ספציפית לזרם ש-HotPDF עדיין אינה מפיקה - קבצים מוטמעים מוצפנים במפתח ציבורי שבהם
/EFFשונה מ-/StmFהאפקטיבי נדחים מאותה סיבה, במקום להיכתב בצורה שאינה מתפענחת עבור אף אחד - מסלול הקובץ הישיר המהיר של AES-256 חל רק כאשר מחרוזות, זרמים וקבצים מוטמעים נפתרים לאותה שיטת מסנן Crypt ושום אובייקט בקובץ אינו נושא
/Cryptמפורש; מדיניות מעורבת או מטא-נתונים plaintext מכריחים fallback לנתיב המלא של גרף האובייקטים
אלה אינם שיקולי ביצועים. הם מסמנים מקומות שבהם ניחוש שגוי יוצר PDF שנפתח ב-viewer אחד, נכשל באחר ואינו נותן למפתח שום אות עד שלקוח מדווח עליו. סירוב ב-ConfigureCryptFilterDefaults או בזמן שמירה עולה חריגה אחת; קובץ מוטמע שהוצפן במפתח שגוי בשקט עולה מחזור תמיכה. אם אתם בונים תוכנת Delphi או C++Builder שמייצרת או צורכת PDF מוצפן — תוכן עמודים plaintext באופן סלקטיבי עם קבצים מצורפים מוצפנים, wrappers של מטענים מוצפנים ב-PDF 2.0 או interoperability עם קבצים שמדיניות מסנני ה-Crypt שלהם לא בחרתם — ה-API של מסנני Crypt שמתואר כאן נשלח ברכיב HotPDF Delphi PDF component הנוכחי, לצד נתיבי ההצפנה, זרמי האובייקטים והעדכונים האינקרמנטליים שעליהם הוא נשען