PDFlibPas מנסה מחדש סיסמה שגויה על PDF מוצפן על ידי השלכת ה-TPDFDocument שהרגע נכשל ויצירת אחד חדש לגמרי עבור הניסיון הבא, מונע על ידי callback OnPassword (TPDFlibPasswordEvent) שרץ עד לשש-עשרה ניסיונות לפני שהוא מוותר. זו סטייה מכוונת מהאינסטינקט שרוב מפתחי ה-Delphi פונים אליו קודם: שמור על אובייקט המסמך שכבר יושב בזיכרון, הזן לו סיסמה מתוקנת, וטען שוב במקום במקום להתחיל מחדש מכלום. לולאת הניסיון-החוזר של PDFlibPas, שנוספה ב-v3.245.0, נוקטת בעמדה ההפוכה, מסיבות ספציפיות למה שניסיון-סיסמה כושל משאיר מאחוריו. התרחיש מאחורי זה רגיל מספיק שרוב אפליקציות ה-Delphi כבדות-המסמכים נתקלות בו בסופו של דבר: מסך קליטה מקבל PDF, טריילר מוצפן כופה תיבת דו-שיח סיסמה, המפעיל מקליד שגוי, ותיבת הדו-שיח מופיעה מחדש לניסיון שני. שום דבר בחוויית המשתמש הזו לא חריג, כך שהקוד מאחוריה חייב לקבל יותר מסיסמת-מועמד אחת עבור אותו קובץ, והוא חייב לעשות זאת בבטחה, בלי לדלוף מצב מהניסיון הנדחה לתוך זה שבא אחריו
למה אי אפשר סתם לנסות שוב על אותו אובייקט מסמך?
שימוש חוזר ב-TPDFDocument על פני ניסיונות סיסמה לא עובד, משום שניסיון כושל כבר פירק את האובייקט ההוא פנימית במקום להשאיר אותו במצב מושהה, ניתן-להמשך. פתיחת PDF מוצפן אומרת פענוח טבלת ההפניה-הצולבת, בניית קורא מעל המקור הבסיסי, ובניית מטפל-הצפנה מכל סיסמה שנמסרה, כל זה עוד לפני ש-PDFlibPas בכלל יכולה לבדוק אם הסיסמה ההיא נכונה. כאשר הסיסמה מתבררת שגויה, שגרת הטעינה הפנימית של המסמך מנקה את הקורא, טבלת ההפניה-הצולבת, ומטפל-ההצפנה כחלק מהיכשלות-החוצה, בדיוק כפי שהיא צריכה, מה שאומר שאין מפענח חצי-בנוי שיושב שם וממתין לסיסמה מתוקנת בקריאה שנייה. הנע את אותו אובייקט דרך עוד ניסיון-טעינה בכל זאת ומצב-הכשל הוא בדיוק הסוג שעלוב לנפות: שגיאה עולה על פני השטח ממצב פנימי שנבנה עבור פענוח שונה, שכבר נכשל, ללא שום דבר בה שמצביע בבירור בחזרה אל הסיסמה שלוש קריאות במעלה-הזרם. PDFlibPas נמנעת מכל מחלקת-הבעיה הזו על ידי כך שהיא אף פעם לא מנסה לשחזר אובייקט מסמך ברגע שהוא נכשל בפתיחה; כל ניסיון מקבל מסמך שאף פעם לא ראה סיסמה שגויה, קורא וטבלת הפניה-צולבת כלולים
איך callback OnPassword מבקש את הסיסמה הבאה?
TPDFlibPasswordEvent הוא סוג ה-callback ש-PDFlibPas מפעילה דרך TPDFlib.LoadFromFile, LoadFromStream, ו-LoadFromString בכל פעם שהסיסמה שהרגע נוסתה מתבררת שגויה, והוא מוסר ל-handler שלושה דברים: איזה ניסיון עומד לרוץ, פרמטר Password לכתוב-מעל עם המועמד הבא, ודגל Retry שברירת המחדל שלו false
TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
var Password: WideString; var Retry: Boolean) of object;
property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;
הסיסמה שנמסרה לתוך קריאת ה-LoadFromFile המקורית נספרת כניסיון אחד, כך שבפעם הראשונה ש-OnPassword בכלל מופעל, AttemptNumber מגיע כ-2. השאר את Retry בלתי-מוגדר וה-load נכשל נקי עם LastErrorCode 404; הגדר אותו true ו-PDFlibPas מנסה שוב עם מה שה-handler הרגע כתב לתוך Password
בתוך לולאת הניסיון-החוזר: TPDFDocument חדש לכל ניסיון
פנימית, PDFlibPas עונה על שאלת-מחזור-חיי-האובייקט באותו אופן עבור LoadFromFile, LoadFromStream, ו-LoadFromString: כל ניסיון, כולל הראשון, בונה TPDFDocument טרי, מריץ אותו דרך רצף הפתיחה המלא עם איזו סיסמה שהניסיון ההוא משתמש בה, ורק שומר על האובייקט אם הסיסמה מאומתת. ה-TPDFDocument של ניסיון שנדחה משוחרר מיד, לוקח איתו את הקורא, טבלת ההפניה-הצולבת, ומטפל-ההצפנה שלו, והניסיון הבא מתחיל מחדש עם אובייקט ללא היסטוריה בכלל
// Simplified excerpt from inside LoadFromFile: every attempt gets a
// document that has never seen a previously rejected password. FileName,
// AttemptNumber and AttemptPassword come from the enclosing method.
Var
Doc: TPDFDocument;
LoadResult: TPLLoadResult;
Success: Boolean;
Begin
Success := False;
Repeat
Doc := TPDFDocument.Create;
Doc.DecodeMode := FDefaultDecodeMode;
Try
LoadResult := Doc.LoadFromFile(FileName, AttemptPassword);
Success := LoadResult = lrOkay;
if Success then
begin
FDocs.Add(Doc); // hand the verified document to the
Doc := nil; // caller's collection; skip the Free below
end;
Finally
Doc.Free; // a rejected attempt's reader, xref table
End; // and crypt handler are torn down right here
if Success or (LoadResult <> lrWrongPassword) then
Break; // success, or a non-password failure: stop
Inc(AttemptNumber);
Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;
שורת ה-Doc := nil ההיא ממש לפני בלוק ה-Finally היא כל חוזה מחזור-חיי-האובייקט בהוראה אחת. מסמך שנכשל נושא את מצב-המפענח החצי-בנוי שלו לקבר איתו, בעיצוב, ומסמך שמצליח הוא היחיד שאי-פעם נוסף ל-FDocs, האוסף ש-TPDFlib שומרת עבור כל מסמך שהקוד הקורא פתח. שום דבר מניסיון שנדחה לא נראה מחוץ ללולאת הניסיון-החוזר: לא קורא חצי-מאותחל, לא ספירת-עמודים מיושנת, לא מטפל-הצפנה שנבנה מהמפתח הלא-נכון
כמה פעמים PDFlibPas תנסה שוב סיסמה שגויה?
PDFlibPas מאפשרת שש-עשרה ניסיונות כוללים מול קריאת LoadFromFile, LoadFromStream, או LoadFromString בודדת, סופרת את הסיסמה שנמסרה לתוך הקריאה עצמה כניסיון אחד. OnPassword אף פעם לא מופעל אלא עבור ניסיונות שתיים עד שש-עשרה, מה שכובל את ה-callback לחמש-עשרה הפעלות; בקש ניסיון שבע-עשרה ו-PDFlibPas מסרבת בלי אפילו לקרוא ל-handler. השאר Retry בברירת המחדל שלו false בכל נקודה, או מצה את כל שש-עשרה הניסיונות בלי סיסמה נכונה, ו-LoadFromFile מחזירה 0 עם LastErrorCode מוגדר ל-404, הקוד של PDFlibPas עבור סיסמה שנדחתה. התקרה קיימת מסיבות מעבר לסדר: לולאת ניסיון-חוזר בלתי-מוגבלת היא דרך קלה להפוך סיסמה שהוקלדה שגוי בטעות למניעת-שירות מקרית נגד איזה תהליכון שמריץ את הטעינה, במיוחד ברגע ש-handler מחווט למשהו אוטומטי, כמו רשימת סיסמאות שנראו קודם, במקום בן-אדם שלוחץ דרך תיבת דו-שיח. PDFlibPas גם מכבדת Abort שנקרא על מופע ה-TPDFlib מתוך ה-handler, שכן Sender מגיע כאותו אובייקט עצמו, שימושי מאחורי כפתור Cancel בתיבת דו-שיח סיסמה, ועוצרת את לולאת הניסיון-החוזר בבדיקה הבאה ללא קשר למה ש-Retry הוגדר. טעינה שנכשלת מסיבה שאינה סיסמה שגויה, טבלת הפניה-צולבת פגומה למשל, אף פעם לא נכנסת ללולאת הניסיון-החוזר בכלל: PDFlibPas מדווחת LastErrorCode 401 ועוצרת אחרי הניסיון הראשון, משום ששום מספר של ניחושי-סיסמה לא מתקן קובץ שבור מבנית
האם לולאת הניסיון-החוזר עובדת אותו דבר עבור קבצים, זרמים, ומחרוזות?
ה-callback OnPassword ותקרת שש-העשרה-הניסיונות מתנהגים זהה על פני LoadFromFile, LoadFromStream, ו-LoadFromString, אף על פי ששלוש נקודות הכניסה מחזיקות במקור שלהן אחרת בין ניסיונות. נתיב קובץ זול לבקר מחדש, שכן כל ניסיון פשוט פותח מחדש את הקובץ הנקוב, ומקור-מחרוזת כבר יושב בזיכרון כעותק של הקוד הקורא עצמו, כך שאף אחד מהם לא זקוק לעזרה מהקוד הקורא בין ניסיונות. זרם שנמסר-על-ידי-הקוד-הקורא הוא המקרה האחד ששווה לעצור עליו: LoadFromStream מחזירה את הזרם ההוא לעמדה אפס ומעתיקה אותו פנימית לפני ניסיון-הפענוח הראשון, כך שכל ניסיון עוקב, וה-TPDFDocument שנבנה טרי מאחוריו, מתנגן מחדש מהעותק הפנימי ההוא במקום מכל מקום שפענוח כושל השאיר את מיקום הזרם. מסור ל-PDFlibPas TFileStream או TMemoryStream עבור מסמך מוגן-סיסמה ואין צורך לגלול אותו אחורה בין ניסיונות-חוזרים; PDFlibPas כבר מתחשבת בעמדה שניסיון ראשון, כושל, אולי הזיז
שילוב ניסיון-חוזר של סיסמה לתוך מסך קליטת מסמכים
זרימת עבודה של קליטת מסמכים היא הבית הטבעי עבור ה-callback הזה, משום שזו בדיוק צורת הבעיה ש-OnPassword נבנתה כדי לפתור: קובץ מגיע מחוץ לאפליקציה, הסיסמה שלו לא ידועה בוודאות מראש, והאדם שמספק מועמדים זקוק ליותר מניחוש אחד בלי שהקוד שמסביב יכתוב לולאת ניסיון-חוזר משלו סביב LoadFromFile
procedure TIntakeForm.SupplyPassword(Sender: TObject; AttemptNumber: Integer;
var Password: WideString; var Retry: Boolean);
var
Typed: string;
begin
// AttemptNumber counts from 2: the password already tried was attempt 1.
Typed := '';
Retry := InputQuery('Password required',
Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
if Retry then
Password := Typed;
// Retry is False when the operator cancels, which leaves
// LastErrorCode at 404 for the caller to report.
end;
procedure TIntakeForm.LoadInboundDocument;
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.OnPassword := SupplyPassword;
if Lib.LoadFromFile('inbound-invoice.pdf', '') = 1 then
RegisterIntakeDocument(Lib) // only a verified document reaches here
else
LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
finally
Lib.Free;
end;
end;
RegisterIntakeDocument אף פעם לא מקבלת Lib אלא ברגע ש-LoadFromFile החזירה 1, כלומר סיסמה כלשהי בחילופין ההוא בפועל אומתה מול מטפל-ההצפנה של הקובץ; ניסיון שנדחה אף פעם לא מגיע לשורה ההיא, וגם לא מסמך חצי-פתוח. מה שבא אחר-כך, ברגע שמסמך כזה מאושר כפתוח, שווה מבט שני על הגדרות ההגנה שלו במקום הנחה שהסיסמה שעבדה היא כל סיפור האבטחה: ביקורת מה שמילון ה-/Encrypt של מסמך בפועל מצהיר מכסה קריאת האלגוריתם, הרוויזיה, וסיביות ההרשאה ש-PDFlibPas חושפת ברגע שקובץ כזה נטען
ניסיון-חוזר של סיסמה הוא גם מופע צר של משמעת רחבה יותר ש-PDFlibPas מיישמת לאורך כל שכבת הפענוח שלה: קובץ שעדיין לא הוכיח את עצמו לא מקבל טובת-הספק, בין אם השאלה היא איזו סיסמה פותחת אותו או אם שדה-אורך בתוכו משקר לגבי גודל המאגר שהוא זקוק לו. חיזוק מפענח PDF ב-Pascal מפני קבצים זדוניים מכסה את המחצית האחרת של המשמעת ההיא, המפענחים שמתייחסים לכל תוכנית גופן וזרם תמונה ב-PDF נכנס כקלט עוין ולא כמסמך מעוצב-היטב שסתם שכח את הסיסמה שלו
OnPassword ולולאת הניסיון-החוזר שמאחוריה הם חלק מספריית ה-PDF PDFlibPas הסטנדרטית עבור Delphi ו-C++Builder, זמינה בכל מקום ש-LoadFromFile, LoadFromStream, או LoadFromString כבר נמצאים, ללא מודול נפרד או רמת רישיון נדרשת עבור מסמך שסתם זקוק לניחוש שני לסיסמה שלו