PDFlibPas ponawia próbę błędnego hasła na zaszyfrowanym PDF, odrzucając TPDFDocument, który właśnie zawiódł, i tworząc zupełnie nowy dla następnej próby, sterowany callbackiem OnPassword (TPDFlibPasswordEvent), który uruchamia się przez maksymalnie szesnaście prób, zanim się podda. To celowe odejście od instynktu, po który sięga najpierw większość programistów Delphi: zachować obiekt dokumentu już siedzący w pamięci, podać mu poprawione hasło i wczytać ponownie w miejscu, zamiast zaczynać od nowa z niczego. Pętla ponawiania w PDFlibPas, dodana w v3.245.0, przyjmuje przeciwne stanowisko, z powodów specyficznych dla tego, co pozostawia po sobie nieudana próba hasła. Scenariusz stojący za tym jest na tyle zwyczajny, że większość aplikacji Delphi intensywnie pracujących z dokumentami w końcu na niego trafia: ekran przyjęcia akceptuje PDF, zaszyfrowany trailer wymusza okno dialogowe hasła, operator się myli w łańcuchu znaków, a okno dialogowe pojawia się ponownie na drugą próbę. Nic w tym doświadczeniu użytkownika nie jest niezwykłe, więc kod za nim musi zaakceptować więcej niż jedno hasło-kandydata dla tego samego pliku, i musi to zrobić bezpiecznie, bez wycieku stanu z odrzuconej próby do tej, która następuje
Dlaczego nie można po prostu ponowić próby na tym samym obiekcie dokumentu?
Ponowne użycie TPDFDocument między próbami hasła nie działa, ponieważ nieudana próba już zburzyła ten obiekt wewnętrznie, zamiast zostawić go w jakimś wstrzymanym, wznawialnym stanie. Otwarcie zaszyfrowanego PDF oznacza sparsowanie tabeli odniesień krzyżowych, zbudowanie czytnika nad bazowym źródłem i skonstruowanie handlera szyfrowania z dowolnego podanego hasła, wszystko to, zanim PDFlibPas w ogóle może sprawdzić, czy to hasło jest poprawne. Gdy hasło okazuje się błędne, wewnętrzna procedura wczytywania dokumentu sprząta czytnik, tabelę odniesień krzyżowych i handler szyfrowania jako część zawodzenia, dokładnie tak, jak powinna, co oznacza, że nie ma tam żadnego pół-zbudowanego parsera czekającego na poprawione hasło przy drugim wywołaniu. Poprowadź mimo to ten sam obiekt przez kolejną próbę wczytania, a tryb awarii jest dokładnie tym rodzajem, który jest nieszczęściem do debugowania: błąd ujawnia się z wewnętrznego stanu zbudowanego dla innego, już nieudanego parsowania, z niczym w nim, co oczywiście wskazywałoby z powrotem na hasło trzy wywołania wcześniej. PDFlibPas unika całej tej klasy problemu, nigdy nie próbując odzyskać obiektu dokumentu, gdy tylko zawiódł w otwarciu; każda próba dostaje dokument, który nigdy nie widział błędnego hasła, wliczając czytnik i tabelę odniesień krzyżowych
Jak callback OnPassword prosi o kolejne hasło?
TPDFlibPasswordEvent to typ callbacku, który PDFlibPas wywołuje przez TPDFlib.LoadFromFile, LoadFromStream i LoadFromString za każdym razem, gdy właśnie wypróbowane hasło okazuje się błędne, i przekazuje handlerowi trzy rzeczy: która próba ma się zaraz uruchomić, parametr Password do nadpisania kolejnym kandydatem, oraz flagę Retry domyślnie ustawioną na false
TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
var Password: WideString; var Retry: Boolean) of object;
property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;
Hasło podane w oryginalnym wywołaniu LoadFromFile liczy się jako próba pierwsza, więc za pierwszym razem, gdy OnPassword w ogóle się uruchomi, AttemptNumber przychodzi jako 2. Zostaw Retry nieustawione, a wczytywanie zawodzi czysto z LastErrorCode 404; ustaw je na true, a PDFlibPas próbuje ponownie z tym, co handler właśnie zapisał do Password
Wnętrze pętli ponawiania: nowy TPDFDocument na każdą próbę
Wewnętrznie PDFlibPas odpowiada na pytanie o cykl życia obiektu w ten sam sposób dla LoadFromFile, LoadFromStream i LoadFromString: każda próba, w tym pierwsza, konstruuje świeży TPDFDocument, przepuszcza go przez kompletną sekwencję otwarcia z dowolnym hasłem, jakiego ta próba używa, i zachowuje obiekt tylko wtedy, gdy hasło się zweryfikuje. TPDFDocument odrzuconej próby jest natychmiast zwalniany, ściągając ze sobą swój czytnik, tabelę odniesień krzyżowych i handler szyfrowania, a następna próba zaczyna od nowa z obiektem, który nie ma żadnej historii
// 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;
Ta linia Doc := nil tuż przed blokiem Finally to cały kontrakt cyklu życia obiektu w jednej instrukcji. Dokument, który zawodzi, niesie swój pół-zbudowany stan parsera do grobu razem ze sobą, celowo, a dokument, który się powiedzie, jest jedynym, jaki kiedykolwiek trafia do FDocs, kolekcji, którą TPDFlib utrzymuje dla każdego dokumentu, jaki wywołujący ma otwarty. Nic z odrzuconej próby nie jest widoczne spoza pętli ponawiania: żaden pół-zainicjalizowany czytnik, żadna nieaktualna liczba stron, żaden handler szyfrowania zbudowany z niewłaściwego klucza
Ile razy PDFlibPas ponowi błędne hasło?
PDFlibPas pozwala na szesnaście prób łącznie wobec pojedynczego wywołania LoadFromFile, LoadFromStream lub LoadFromString, licząc hasło podane w samym wywołaniu jako próbę pierwszą. OnPassword uruchamia się tylko dla prób od drugiej do szesnastej, co ogranicza callback do piętnastu wywołań; poproś o siedemnastą próbę, a PDFlibPas odmawia, nawet nie wywołując handlera. Zostaw Retry na domyślnym false w dowolnym momencie, albo wyczerp wszystkie szesnaście prób bez poprawnego hasła, a LoadFromFile zwraca 0 z LastErrorCode ustawionym na 404, kodem PDFlibPas dla odrzuconego hasła. Ten limit istnieje z powodów wykraczających poza schludność: nieograniczona pętla ponawiania to łatwy sposób, by zamienić jedno błędnie wpisane hasło w przypadkową odmowę usługi wobec dowolnego wątku uruchamiającego wczytywanie, zwłaszcza gdy handler jest podłączony do czegoś zautomatyzowanego, jak lista wcześniej widzianych haseł, zamiast człowieka klikającego przez okno dialogowe. PDFlibPas honoruje też Abort wywołane na instancji TPDFlib z wnętrza handlera, ponieważ Sender przychodzi jako ten sam obiekt, przydatne za przyciskiem Anuluj na oknie dialogowym hasła, i zatrzymuje pętlę ponawiania przy następnym sprawdzeniu niezależnie od tego, na co ustawiono Retry. Wczytywanie, które zawodzi z powodu innego niż błędne hasło, na przykład uszkodzona tabela odniesień krzyżowych, w ogóle nie wchodzi do pętli ponawiania: PDFlibPas zgłasza LastErrorCode 401 i zatrzymuje się po pierwszej próbie, ponieważ żadna liczba zgadywań hasła nie naprawia strukturalnie zepsutego pliku
Czy pętla ponawiania działa tak samo dla plików, strumieni i łańcuchów znaków?
Callback OnPassword i limit szesnastu prób zachowują się identycznie dla LoadFromFile, LoadFromStream i LoadFromString, choć te trzy punkty wejścia trzymają swoje źródło inaczej między próbami. Ścieżka pliku jest tania do ponownego odwiedzenia, ponieważ każda próba po prostu ponownie otwiera nazwany plik, a źródło łańcuchowe już siedzi w pamięci jako własna kopia wywołującego, więc żadne z nich nie potrzebuje żadnej pomocy od wywołującego między próbami. Strumień dostarczony przez wywołującego to jedyny przypadek wart zatrzymania się nad nim: LoadFromStream przewija ten strumień z powrotem do pozycji zero i kopiuje go wewnętrznie przed pierwszą próbą parsowania, więc każda kolejna próba, i świeżo skonstruowany za nią TPDFDocument, odtwarza z tej wewnętrznej kopii, a nie z tego, gdziekolwiek nieudane parsowanie zostawiło pozycję strumienia. Podaj PDFlibPas TFileStream lub TMemoryStream dla dokumentu chronionego hasłem, a nie ma potrzeby przewijania go między ponowieniami; PDFlibPas już uwzględnia pozycję, którą mogła przesunąć pierwsza, nieudana próba
Dopasowanie ponawiania hasła do ekranu przyjęcia dokumentów
Przepływ pracy przyjęcia dokumentów to naturalne miejsce dla tego callbacku, ponieważ to dokładnie ten kształt problemu, który OnPassword zostało zbudowane rozwiązać: plik przychodzi spoza aplikacji, jego hasło nie jest znane z pewnością z góry, a osoba dostarczająca kandydatów potrzebuje więcej niż jednego zgadywania bez pisania przez otaczający kod własnej pętli ponawiania wokół 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 otrzymuje Lib tylko wtedy, gdy LoadFromFile zwróciło 1, co oznacza, że jakieś hasło w tej wymianie faktycznie zweryfikowało się względem handlera szyfrowania pliku; odrzucona próba nigdy nie dociera do tej linii, i podobnie nie dociera do niej pół-otwarty dokument. To, co następuje dalej, gdy tylko taki dokument jest potwierdzony jako otwarty, jest warte drugiego spojrzenia na jego ustawienia ochrony, zamiast założenia, że hasło, które zadziałało, to cała historia bezpieczeństwa: audyt tego, co faktycznie deklaruje słownik /Encrypt dokumentu omawia odczyt algorytmu, rewizji i bitów uprawnień, które PDFlibPas udostępnia, gdy taki plik zostanie wczytany
Ponawianie hasła to też wąska instancja szerszej dyscypliny, którą PDFlibPas stosuje w całej swojej warstwie parsowania: plik, który jeszcze się nie udowodnił, nie dostaje korzyści z wątpliwości, czy to pytanie o to, które hasło go odblokowuje, czy o to, czy pole długości w jego wnętrzu kłamie na temat rozmiaru bufora, jakiego potrzebuje. Utwardzanie parsera PDF w Pascalu przeciwko złośliwym plikom omawia drugą połowę tej dyscypliny, dekodery, które traktują każdy program czcionki i strumień obrazu w przychodzącym PDF jako wrogie dane wejściowe, a nie dobrze sformułowany dokument, który po prostu zapomniał swojego hasła
OnPassword i stojąca za nim pętla ponawiania są częścią standardowej biblioteki PDF PDFlibPas dla Delphi i C++Buildera, dostępnej wszędzie tam, gdzie już są LoadFromFile, LoadFromStream lub LoadFromString, bez potrzeby osobnego modułu czy poziomu licencji dla dokumentu, który po prostu potrzebuje drugiego zgadywania hasła