PDFlibPas försöker igen med ett fel lösenord på en krypterad PDF genom att kasta bort det TPDFDocument som just misslyckades och skapa ett helt nytt för nästa försök, drivet av en OnPassword-callback (TPDFlibPasswordEvent) som körs upp till sexton försök innan den ger upp. Det är en medveten avvikelse från instinkten de flesta Delphi-utvecklare griper efter först: behåll dokumentobjektet som redan sitter i minnet, mata det med ett korrigerat lösenord, och ladda igen på plats snarare än att börja om från ingenting. PDFlibPas försök-igen-loop, tillagd i v3.245.0, tar den motsatta positionen, av skäl specifika för vad ett misslyckat lösenordsförsök lämnar efter sig. Scenariot bakom det är vanligt nog att de flesta dokumenttunga Delphi-applikationer stöter på det förr eller senare: en intagsskärm accepterar en PDF, en krypterad efterskrift tvingar fram en lösenordsdialog, operatören klantar sig med strängen, och dialogen dyker upp igen för ett andra försök. Inget om den användarupplevelsen är ovanligt, så koden bakom den måste acceptera mer än ett kandidatlösenord för samma fil, och den måste göra det säkert, utan att läcka tillstånd från det avvisade försöket in i det som följer
Varför kan du inte bara försöka igen på samma dokumentobjekt?
Att återanvända ett TPDFDocument över lösenordsförsök fungerar inte, eftersom ett misslyckat försök redan har rivit ner det objektet internt snarare än att lämna det i något pausat, återupptagbart tillstånd. Att öppna en krypterad PDF innebär att tolka korsreferenstabellen, bygga en läsare över den underliggande källan, och konstruera en krypteringshanterare från vilket lösenord som än tillhandahölls, allt innan PDFlibPas ens kan testa om det lösenordet är korrekt. När lösenordet visar sig vara fel städar dokumentets interna laddningsrutin upp läsaren, korsreferenstabellen, och krypteringshanteraren som en del av att misslyckas, precis som den ska, vilket betyder att det inte finns någon halvbyggd parser sittande där och väntar på ett korrigerat lösenord vid ett andra anrop. Kör samma objekt genom ett annat laddningsförsök ändå och felläget är precis den typen som är eländigt att felsöka: ett fel dyker upp från internt tillstånd byggt för en annan, redan misslyckad tolkning, med inget om det uppenbart pekande tillbaka på lösenordet tre anrop uppströms. PDFlibPas undviker hela problemkategorin genom att aldrig försöka återhämta ett dokumentobjekt när det väl misslyckats öppna; varje försök får ett dokument som aldrig sett ett fel lösenord, läsare och korsreferenstabell inräknade
Hur ber OnPassword-callbacken om nästa lösenord?
TPDFlibPasswordEvent är callback-typen PDFlibPas anropar genom TPDFlib.LoadFromFile, LoadFromStream, och LoadFromString närhelst lösenordet som just testades visar sig vara fel, och den lämnar hanteraren tre saker: vilket försök som är på väg att köras, en Password-parameter att skriva över med nästa kandidat, och en Retry-flagga som defaultar till false
TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
var Password: WideString; var Retry: Boolean) of object;
property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;
Lösenordet som skickades in i det ursprungliga LoadFromFile-anropet räknas som försök ett, så första gången OnPassword överhuvudtaget utlöses anländer AttemptNumber som 2. Lämna Retry ostilld och laddningen misslyckas rent med LastErrorCode 404; sätt den till true och PDFlibPas försöker igen med vad hanteraren just skrev in i Password
Inuti försök-igen-loopen: ett nytt TPDFDocument för varje försök
Internt svarar PDFlibPas på objektlivscykelfrågan på samma sätt för LoadFromFile, LoadFromStream, och LoadFromString: varje försök, inklusive det första, konstruerar ett färskt TPDFDocument, kör det genom den kompletta öppningssekvensen med vilket lösenord det försöket än använder, och behåller bara objektet om lösenordet verifieras. Ett avvisat försöks TPDFDocument frigörs omedelbart, och tar dess läsare, korsreferenstabell, och krypteringshanterare med sig, och nästa försök börjar om med ett objekt som inte har någon historia alls
// 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;
Den Doc := nil-raden precis innan Finally-blocket är hela objektlivscykelkontraktet i en sats. Ett dokument som misslyckas bär sitt halvbyggda parsertillstånd till graven med sig, med avsikt, och ett dokument som lyckas är det enda som någonsin läggs till i FDocs, samlingen TPDFlib håller för varje dokument anroparen har öppet. Inget om ett avvisat försök är synligt utifrån försök-igen-loopen: ingen halvinitierad läsare, ingen inaktuell sidräkning, ingen krypteringshanterare byggd från fel nyckel
Hur många gånger kommer PDFlibPas försöka igen med ett fel lösenord?
PDFlibPas tillåter sexton totala försök mot ett enda LoadFromFile-, LoadFromStream-, eller LoadFromString-anrop, och räknar lösenordet skickat in i själva anropet som försök ett. OnPassword utlöses bara för försök två till sexton, vilket begränsar callbacken till femton anrop; be om ett sjuttonde försök och PDFlibPas avböjer utan att ens anropa hanteraren. Lämna Retry vid dess standard av false vid vilken punkt som helst, eller uttöm alla sexton försök utan ett korrekt lösenord, och LoadFromFile returnerar 0 med LastErrorCode satt till 404, PDFlibPas kod för ett avvisat lösenord. Taket existerar av skäl bortom städighet: en obegränsad försök-igen-loop är ett enkelt sätt att förvandla ett feltypat lösenord till en oavsiktlig överbelastningsattack mot vilken tråd som helst som kör laddningen, särskilt när en hanterare är kopplad till något automatiserat, som en lista över tidigare sedda lösenord, snarare än en människa som klickar genom en dialog. PDFlibPas respekterar också Abort anropad på TPDFlib-instansen inifrån hanteraren, eftersom Sender anländer som det objektet, användbart bakom en Avbryt-knapp på en lösenordsdialog, och stoppar försök-igen-loopen vid nästa kontroll oavsett vad Retry sattes till. En laddning som misslyckas av en annan anledning än ett fel lösenord, en skadad korsreferenstabell till exempel, går aldrig in i försök-igen-loopen alls: PDFlibPas rapporterar LastErrorCode 401 och stannar efter det första försöket, eftersom inget antal lösenordsgissningar fixar en strukturellt trasig fil
Fungerar försök-igen-loopen samma för filer, strömmar, och strängar?
OnPassword-callbacken och sexton-försöks-taket beter sig identiskt över LoadFromFile, LoadFromStream, och LoadFromString, även om de tre ingångspunkterna håller i sin källa olika mellan försök. En filsökväg är billig att besöka igen, eftersom varje försök helt enkelt öppnar den namngivna filen igen, och en strängkälla sitter redan i minnet som anroparens egen kopia, så ingendera behöver någon hjälp från anroparen mellan försök. En anroparetillhandahållen ström är det enda fallet värt att stanna upp vid: LoadFromStream söker den strömmen tillbaka till position noll och kopierar den internt innan det första tolkningsförsöket, så varje efterföljande försök, och det nyligen konstruerade TPDFDocument bakom det, spelar upp från den interna kopian snarare än från vart en misslyckad tolkning än lämnade strömmens position. Ge PDFlibPas en TFileStream eller TMemoryStream för ett lösenordsskyddat dokument och det finns inget behov av att spola tillbaka den mellan försök; PDFlibPas tar redan hänsyn till en position ett första, misslyckat försök kan ha flyttat
Att passa in lösenordsförsök-igen i en dokumentintagsskärm
Ett dokumentintagsarbetsflöde är det naturliga hemmet för den här callbacken, eftersom det är precis den formen av problem OnPassword byggdes för att lösa: en fil anländer utifrån applikationen, dess lösenord är inte känt med säkerhet i förväg, och personen som tillhandahåller kandidater behöver mer än en gissning utan att den omgivande koden skriver sin egen försök-igen-loop runt 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 tar bara emot Lib när LoadFromFile väl har returnerat 1, vilket betyder att något lösenord i det utbytet faktiskt verifierades mot filens krypteringshanterare; ett avvisat försök når aldrig den raden, och inte heller ett halvöppet dokument. Vad som kommer härnäst, när ett dokument som det här väl bekräftats öppet, är värt en andra titt på dess skyddsinställningar snarare än ett antagande att lösenordet som fungerade är hela säkerhetshistorien: att granska vad ett dokuments /Encrypt-ordbok faktiskt deklarerar täcker att läsa algoritmen, revisionen, och behörighetsbitarna PDFlibPas exponerar när en fil som den här väl laddas
Lösenordsförsök-igen är också en smal instans av en bredare disciplin PDFlibPas tillämpar genom hela sitt tolkningslager: en fil som ännu inte bevisat sig själv får ingen tvivelsvinst, oavsett om frågan är vilket lösenord som låser upp den eller om ett längdfält inuti den ljuger om storleken på buffert den behöver. Att härda en Pascal-PDF-parser mot skadliga filer täcker den andra hälften av den disciplinen, avkodarna som behandlar varje typsnittsprogram och bildström i en inkommande PDF som fientlig indata snarare än ett välformat dokument som bara glömde sitt lösenord
OnPassword och försök-igen-loopen bakom den är del av standard-PDFlibPas PDF-biblioteket för Delphi och C++Builder, tillgängligt överallt LoadFromFile, LoadFromStream, eller LoadFromString redan är, utan någon separat modul eller licensnivå som krävs för ett dokument som bara behöver en andra gissning på sitt lösenord