PDF Library for Delphi 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 PDF Library for Delphi, 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 PDF Library for Delphi 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. PDF Library for Delphi 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 PDF Library for Delphi 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 PDF Library for Delphi 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 PDF Library for Delphi 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
// Uproszczony fragment z wnętrza LoadFromFile: każda próba dostaje
// dokument, który nigdy nie widział wcześniej odrzuconego hasła. FileName,
// AttemptNumber i AttemptPassword pochodzą z otaczającej metody.
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); // przekaż zweryfikowany dokument do
Doc := nil; // kolekcji wywołującego; pomiń Free poniżej
end;
Finally
Doc.Free; // czytnik, tabela xref i handler szyfrowania
End; // odrzuconej próby są tu natychmiast niszczone
if Success or (LoadResult <> lrWrongPassword) then
Break; // sukces albo błąd niezwiązany z hasłem: zatrzymaj się
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 PDF Library for Delphi ponowi błędne hasło?
PDF Library for Delphi 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 PDF Library for Delphi 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 PDF Library for Delphi 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. PDF Library for Delphi 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: PDF Library for Delphi 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 PDF Library for Delphi TFileStream lub TMemoryStream dla dokumentu chronionego hasłem, a nie ma potrzeby przewijania go między ponowieniami; PDF Library for Delphi 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 liczy od 2: hasło już wypróbowane było próbą 1.
Typed := '';
Retry := InputQuery('Password required',
Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
if Retry then
Password := Typed;
// Retry ma wartość False, gdy operator anuluje, co pozostawia
// LastErrorCode równe 404 do zgłoszenia przez wywołującego.
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) // tylko zweryfikowany dokument dociera tutaj
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 PDF Library for Delphi udostępnia, gdy taki plik zostanie wczytany
Ponawianie hasła to też wąska instancja szerszej dyscypliny, którą PDF Library for Delphi 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 PDF Library for Delphi 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