Technisch artikel

Versleutelde PDF-wachtwoorden opnieuw proberen in Delphi met PDF Library for Delphi

PDF Library for Delphi probeert een verkeerd wachtwoord op een versleutelde PDF opnieuw door het TPDFDocument dat net faalde weg te gooien en een compleet nieuw exemplaar aan te maken voor de volgende poging, aangedreven door een OnPassword-callback (TPDFlibPasswordEvent) die tot zestien pogingen lang draait voordat het opgeeft. Dat is een doelbewuste afwijking van het instinct waar de meeste Delphi-ontwikkelaars als eerste naar grijpen: het documentobject dat al in het geheugen zit behouden, er een gecorrigeerd wachtwoord aan voeren, en opnieuw ter plekke laden in plaats van vanaf niets opnieuw te beginnen. De retry-lus van PDF Library for Delphi, toegevoegd in v3.245.0, neemt het tegenovergestelde standpunt in, om redenen specifiek voor wat een mislukte wachtwoordpoging achterlaat. Het scenario erachter is gewoon genoeg dat de meeste document-zware Delphi-toepassingen het uiteindelijk tegenkomen: een intakescherm accepteert een PDF, een versleutelde trailer forceert een wachtwoorddialoog, de operator typt de string verkeerd, en de dialoog verschijnt opnieuw voor een tweede poging. Niets aan die gebruikerservaring is ongewoon, dus de code erachter moet meer dan één kandidaatwachtwoord voor hetzelfde bestand accepteren, en dat moet veilig gebeuren, zonder dat toestand van de afgewezen poging lekt naar de poging die erop volgt

Waarom kunt u niet gewoon opnieuw proberen op hetzelfde documentobject?

Een TPDFDocument hergebruiken over wachtwoordpogingen heen werkt niet, omdat een mislukte poging dat object intern al heeft afgebroken in plaats van het in een gepauzeerde, hervatbare toestand achter te laten. Een versleutelde PDF openen betekent de cross-reference-tabel parsen, een lezer bouwen over de onderliggende bron, en een crypt-handler construeren uit welk wachtwoord ook is aangeleverd, allemaal voordat PDF Library for Delphi zelfs maar kan testen of dat wachtwoord correct is. Wanneer het wachtwoord verkeerd blijkt, ruimt de interne laadroutine van het document de lezer, de cross-reference-tabel, en de crypt-handler op als onderdeel van het falen, precies zoals het hoort, wat betekent dat er geen halfgebouwde parser klaarstaat om te wachten op een gecorrigeerd wachtwoord bij een tweede aanroep. Drijf datzelfde object toch door een volgende laadpoging en de faalmodus is precies het soort dat ellendig is om te debuggen: een fout komt naar boven vanuit interne toestand gebouwd voor een andere, al mislukte parse, zonder dat er duidelijk iets naar het wachtwoord drie aanroepen stroomopwaarts wijst. PDF Library for Delphi vermijdt de hele klasse probleem door nooit te proberen een documentobject te herstellen zodra het niet is geopend; elke poging krijgt een document dat nog nooit een verkeerd wachtwoord heeft gezien, lezer en cross-reference-tabel inbegrepen

Hoe vraagt de OnPassword-callback om het volgende wachtwoord?

TPDFlibPasswordEvent is het callbacktype dat PDF Library for Delphi aanroept via TPDFlib.LoadFromFile, LoadFromStream, en LoadFromString telkens wanneer het net geprobeerde wachtwoord verkeerd blijkt, en het geeft de handler drie dingen: welke poging op het punt staat te draaien, een Password-parameter om te overschrijven met de volgende kandidaat, en een Retry-vlag die standaard false is

TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean) of object;

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

Het wachtwoord dat in de oorspronkelijke LoadFromFile-aanroep is doorgegeven, telt als poging één, dus de eerste keer dat OnPassword überhaupt afgaat, komt AttemptNumber binnen als 2. Laat Retry ongewijzigd en het laden mislukt netjes met LastErrorCode 404; zet het op true en PDF Library for Delphi probeert het opnieuw met wat de handler net in Password heeft geschreven

Binnen in de retry-lus: een nieuw TPDFDocument voor elke poging

Intern beantwoordt PDF Library for Delphi de vraag over de objectlevenscyclus op dezelfde manier voor LoadFromFile, LoadFromStream, en LoadFromString: elke poging, inclusief de eerste, construeert een vers TPDFDocument, laat het door de volledige openingssequentie lopen met welk wachtwoord die poging ook gebruikt, en behoudt het object alleen als het wachtwoord verifieert. Het TPDFDocument van een afgewezen poging wordt onmiddellijk vrijgegeven, en neemt zijn lezer, cross-reference-tabel, en crypt-handler daarbij mee naar beneden, en de volgende poging begint opnieuw met een object zonder enige geschiedenis

Stroomdiagram van PDF Library for Delphi met de wachtwoord-herhaallus waarbij elke poging een verse TPDFDocument construeert, een geweigerde poging zijn reader, kruisverwijzingstabel en crypt-handler vrijgeeft, en de OnPassword-callback beslist of er nog een poging komt of dat het laden mislukt met LastErrorCode 404
Een mislukte kandidaat overleeft de lus nooit: zijn document wordt vrijgegeven terwijl de parser-toestand nog half is opgebouwd. Retry instellen schrijft de volgende kandidaat naar Password en herhaalt met een object dat nooit een verkeerd wachtwoord heeft gezien
// Vereenvoudigd fragment van binnen LoadFromFile: elke poging krijgt een
// document dat nooit een eerder afgewezen wachtwoord heeft gezien. FileName,
// AttemptNumber en AttemptPassword komen van de omsluitende methode.
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);            // geef het geverifieerde document door aan de
        Doc := nil;                 // collectie van de aanroeper; sla de Free hieronder over
      end;
    Finally
      Doc.Free;                     // de lezer, xref-tabel en crypt-handler van een
    End;                            // afgewezen poging worden hier direct afgebroken
    if Success or (LoadResult <> lrWrongPassword) then
      Break;                        // succes, of een fout die niets met het wachtwoord te maken heeft: stoppen
    Inc(AttemptNumber);
  Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;

Die regel Doc := nil vlak voor het Finally-blok is het hele objectlevenscycluscontract in één statement. Een document dat faalt, draagt zijn halfgebouwde parsertoestand met zich mee het graf in, doelbewust, en een document dat slaagt, is het enige dat ooit aan FDocs wordt toegevoegd, de verzameling die TPDFlib bijhoudt voor elk document dat de aanroeper open heeft. Niets van een afgewezen poging is zichtbaar van buiten de retry-lus: geen half-geïnitialiseerde lezer, geen verouderde paginatelling, geen crypt-handler gebouwd uit de verkeerde sleutel

Hoe vaak zal PDF Library for Delphi een verkeerd wachtwoord opnieuw proberen?

PDF Library for Delphi staat zestien totale pogingen toe tegen één enkele aanroep van LoadFromFile, LoadFromStream, of LoadFromString, waarbij het wachtwoord dat in de aanroep zelf is doorgegeven als poging één telt. OnPassword gaat alleen af voor pogingen twee tot en met zestien, wat de callback beperkt tot vijftien aanroepen; vraag om een zeventiende poging en PDF Library for Delphi weigert zonder zelfs maar de handler aan te roepen. Laat Retry op enig moment op zijn standaard false staan, of put alle zestien pogingen uit zonder correct wachtwoord, en LoadFromFile geeft 0 terug met LastErrorCode ingesteld op 404, PDF Library for Delphi's code voor een afgewezen wachtwoord. Het plafond bestaat om redenen die verder gaan dan netheid: een onbegrensde retry-lus is een gemakkelijke manier om één verkeerd getypt wachtwoord te veranderen in een onbedoelde denial-of-service tegen welke thread ook het laden draait, vooral zodra een handler is aangesloten op iets geautomatiseerds, zoals een lijst met eerder geziene wachtwoorden, in plaats van een mens die door een dialoog klikt. PDF Library for Delphi eerbiedigt ook Abort aangeroepen op de TPDFlib-instantie van binnen de handler, aangezien Sender als datzelfde object binnenkomt, nuttig achter een Cancel-knop op een wachtwoorddialoog, en stopt de retry-lus bij de volgende controle, ongeacht wat Retry was ingesteld. Een laadbewerking die om een andere reden dan een verkeerd wachtwoord mislukt, bijvoorbeeld een beschadigde cross-reference-tabel, komt de retry-lus helemaal nooit binnen: PDF Library for Delphi rapporteert LastErrorCode 401 en stopt na de eerste poging, omdat geen enkel aantal wachtwoordgissingen een structureel kapot bestand repareert

PDF Library for Delphi: Pogingsspoor van één tot zestien dat toont dat OnPassword afgaat vanaf poging twee, poging zeventien geweigerd wordt zonder de handler aan te roepen, en de aparte uitkomsten voor succes, LastErrorCode 404 en LastErrorCode 401
Alleen het wachtwoord dat aan de laadaanroep zelf wordt meegegeven telt als poging één, en de callback draait nooit meer dan vijftien keer per laadactie. Een structureel beschadigd bestand omzeilt de lus volledig en meldt 401 na één enkele poging

Werkt de retry-lus hetzelfde voor bestanden, streams, en strings?

De OnPassword-callback en het plafond van zestien pogingen gedragen zich identiek over LoadFromFile, LoadFromStream, en LoadFromString, hoewel de drie toegangspunten hun bron tussen pogingen door verschillend vasthouden. Een bestandspad is goedkoop om opnieuw te bezoeken, aangezien elke poging simpelweg het genoemde bestand opnieuw opent, en een string-bron zit al in het geheugen als de eigen kopie van de aanroeper, dus geen van beide heeft hulp van de aanroeper tussen pogingen nodig. Een door de aanroeper aangeleverde stream is het ene geval dat het waard is om bij stil te staan: LoadFromStream zoekt die stream terug naar positie nul en kopieert deze intern vóór de eerste parse-poging, dus elke volgende poging, en het vers geconstrueerde TPDFDocument erachter, speelt af vanuit die interne kopie in plaats van vanaf waar een mislukte parse de positie van de stream ook achterliet. Geef PDF Library for Delphi een TFileStream of TMemoryStream voor een met wachtwoord beveiligd document en er is geen noodzaak om deze tussen pogingen door terug te spoelen; PDF Library for Delphi houdt al rekening met een positie die een eerste, mislukte poging mogelijk heeft verplaatst

Wachtwoord-retry inpassen in een documentintakescherm

Een documentintake-workflow is het natuurlijke thuis voor deze callback, omdat het precies de vorm probleem is waarvoor OnPassword is gebouwd om op te lossen: een bestand komt van buiten de toepassing binnen, het wachtwoord ervan is vooraf niet met zekerheid bekend, en degene die kandidaten aanlevert, heeft meer dan één gok nodig zonder dat de omringende code zijn eigen retry-lus rond LoadFromFile schrijft

procedure TIntakeForm.SupplyPassword(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean);
var
  Typed: string;
begin
  // AttemptNumber telt vanaf 2: het wachtwoord dat al is geprobeerd was poging 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 wanneer de operator annuleert, waardoor
  // LastErrorCode op 404 blijft staan zodat de aanroeper dit kan rapporteren.
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)        // alleen een geverifieerd document bereikt dit punt
    else
      LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
  finally
    Lib.Free;
  end;
end;

RegisterIntakeDocument ontvangt Lib pas zodra LoadFromFile 1 heeft teruggegeven, wat betekent dat een of ander wachtwoord in die uitwisseling daadwerkelijk verifieerde tegen de crypt-handler van het bestand; een afgewezen poging bereikt die regel nooit, en evenmin een half-open document. Wat er hierna komt, zodra een document zoals dit bevestigd is geopend, is een tweede blik op zijn beschermingsinstellingen waard, in plaats van de aanname dat het wachtwoord dat werkte het hele beveiligingsverhaal is: het auditen van wat de /Encrypt-dictionary van een document daadwerkelijk verklaart behandelt het lezen van het algoritme, de revisie, en de toestemmingsbits die PDF Library for Delphi blootstelt zodra een bestand zoals dit laadt

Wachtwoord-retry is ook een smalle instantie van een bredere discipline die PDF Library for Delphi overal in zijn parselaag toepast: een bestand dat zichzelf nog niet heeft bewezen, krijgt geen voordeel van de twijfel, of de vraag nu is welk wachtwoord het ontgrendelt of of een lengteveld erin liegt over de grootte van de buffer die het nodig heeft. Het verharden van een Pascal-PDF-parser tegen kwaadaardige bestanden behandelt de andere helft van die discipline, de decoders die elk lettertypeprogramma en elke beeldstream in een binnenkomende PDF als vijandige invoer behandelen in plaats van een welgevormd document dat gewoon zijn wachtwoord vergat

OnPassword en de retry-lus erachter maken deel uit van de standaard PDF Library for Delphi PDF-bibliotheek voor Delphi en C++Builder, beschikbaar overal waar LoadFromFile, LoadFromStream, of LoadFromString al beschikbaar zijn, zonder aparte module of licentieniveau vereist voor een document dat gewoon een tweede gok op zijn wachtwoord nodig heeft