Teknisk artikkel

Å prøve krypterte PDF-passord på nytt i Delphi med PDF Library for Delphi

PDF Library for Delphi prøver et feil passord på en kryptert PDF på nytt ved å forkaste den TPDFDocument-en som nettopp feilet og opprette en helt ny en for neste forsøk, drevet av en OnPassword-callback (TPDFlibPasswordEvent) som kjører for opptil seksten forsøk før den gir opp. Det er et bevisst avvik fra instinktet de fleste Delphi-utviklere griper til først: behold dokumentobjektet som allerede sitter i minnet, mat det med et korrigert passord, og last inn på nytt på stedet i stedet for å starte på nytt fra ingenting. PDF Library for Delphi' retry-løkke, lagt til i v3.245.0, tar den motsatte posisjonen, av grunner spesifikke for hva et mislykket passordforsøk etterlater. Scenarioet bak den er vanlig nok til at de fleste dokument-tunge Delphi-applikasjoner treffer den til slutt: en inntaksskjerm aksepterer en PDF, en kryptert trailer tvinger frem en passorddialog, operatøren skriver feil i strengen, og dialogen dukker opp igjen for et andre forsøk. Ingenting ved den brukeropplevelsen er uvanlig, så koden bak den må akseptere mer enn ett kandidatpassord for den samme filen, og den må gjøre det trygt, uten å lekke tilstand fra det avviste forsøket inn i det som følger

Hvorfor kan man ikke bare prøve på nytt på det samme dokumentobjektet?

Å gjenbruke en TPDFDocument på tvers av passordforsøk fungerer ikke, fordi et mislykket forsøk allerede har revet det objektet ned internt i stedet for å la det stå i en pauset, gjenopptakbar tilstand. Å åpne en kryptert PDF betyr å parse kryssreferansetabellen, bygge en leser over den underliggende kilden, og konstruere en krypto-håndterer fra hvilket som helst passord som ble levert, alt før PDF Library for Delphi i det hele tatt kan teste om det passordet er korrekt. Når passordet viser seg å være feil, rydder dokumentets interne last-rutine opp leseren, kryssreferansetabellen, og krypto-håndtereren som en del av å feile ut, nøyaktig slik den skal, noe som betyr at det ikke finnes noen halvbygget parser som sitter der og venter på et korrigert passord ved et andre kall. Kjør det samme objektet gjennom et annet lastforsøk uansett, og feilmodusen er nøyaktig den typen som er elendig å feilsøke: en feil dukker opp fra intern tilstand bygget for en annen, allerede mislykket parsing, uten at noe ved den åpenbart peker tilbake til passordet tre kall oppstrøms. PDF Library for Delphi unngår hele den klassen av problem ved aldri å prøve å gjenopprette et dokumentobjekt når det først har feilet å åpne; hvert forsøk får et dokument som aldri har sett et feil passord, leser og kryssreferansetabell inkludert

Hvordan spør OnPassword-callbacken om neste passord?

TPDFlibPasswordEvent er callback-typen PDF Library for Delphi påkaller gjennom TPDFlib.LoadFromFile, LoadFromStream, og LoadFromString hver gang passordet som nettopp ble prøvd, viser seg å være feil, og den gir håndtereren tre ting: hvilket forsøk som er i ferd med å kjøre, en Password-parameter å overskrive med neste kandidat, og et Retry-flagg som som standard er false

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

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

Passordet sendt inn i det opprinnelige LoadFromFile-kallet teller som forsøk én, så første gang OnPassword i det hele tatt utløses, ankommer AttemptNumber som 2. La Retry stå usatt, og lastingen feiler rent med LastErrorCode 404; sett den til true, og PDF Library for Delphi prøver igjen med hva som enn håndtereren nettopp skrev inn i Password

Inne i retry-løkken: en ny TPDFDocument for hvert forsøk

Internt svarer PDF Library for Delphi på objekt-livssyklus-spørsmålet på samme måte for LoadFromFile, LoadFromStream, og LoadFromString: hvert forsøk, inkludert det første, konstruerer en fersk TPDFDocument, kjører den gjennom hele åpne-sekvensen med hvilket som helst passord det forsøket bruker, og beholder objektet bare hvis passordet verifiseres. Et avvist forsøks TPDFDocument frigjøres umiddelbart, og river ned leseren, kryssreferansetabellen, og krypto-håndtereren med seg, og det neste forsøket starter på nytt med et objekt som ikke har noen historie i det hele tatt

PDF Library for Delphi-flytskjema over passordgjenta-sløyfen der hvert forsøk konstruerer en fersk TPDFDocument, et avvist forsøk frigir leseren, kryssreferansetabellen og krypteringsbehandleren, og OnPassword-callbacken bestemmer om et nytt forsøk kjører eller innlastingen feiler med LastErrorCode 404
En mislykket kandidat overlever aldri løkken: dokumentet frigjøres mens parser-tilstanden fortsatt er halvferdig. Å sette Retry skriver neste kandidat inn i Password og gjentar med et objekt som aldri har sett et feil passord
// Forenklet utdrag fra inne i LoadFromFile: hvert forsøk får et
// dokument som aldri har sett et tidligere avvist passord. FileName,
// AttemptNumber og AttemptPassword kommer fra den omsluttende metoden.
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);            // gi det verifiserte dokumentet til
        Doc := nil;                 // den kallendes samling; hopp over Free nedenfor
      end;
    Finally
      Doc.Free;                     // et avvist forsøks leser, xref-tabell
    End;                            // og krypteringshåndterer rives ned akkurat her
    if Success or (LoadResult <> lrWrongPassword) then
      Break;                        // suksess, eller en feil som ikke skyldes passord: stopp
    Inc(AttemptNumber);
  Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;

Den Doc := nil-linjen rett før Finally-blokken, er hele objekt-livssyklus-kontrakten i én setning. Et dokument som feiler, bærer sin halvbygde parser-tilstand med seg i graven, med hensikt, og et dokument som lykkes, er det eneste som noensinne legges til i FDocs, samlingen TPDFlib holder for hvert dokument kalleren har åpent. Ingenting ved et avvist forsøk er synlig utenfra retry-løkken: ingen halvinitialisert leser, ingen utdatert sideantall, ingen krypto-håndterer bygget fra feil nøkkel

Hvor mange ganger vil PDF Library for Delphi prøve et feil passord på nytt?

PDF Library for Delphi tillater seksten totale forsøk mot ett enkelt LoadFromFile-, LoadFromStream-, eller LoadFromString-kall, og teller passordet sendt inn i selve kallet som forsøk én. OnPassword utløses bare noensinne for forsøk to til seksten, noe som gir callbacken et tak på femten påkallinger; be om et syttende forsøk, og PDF Library for Delphi avslår uten engang å påkalle håndtereren. La Retry stå på sin standard false på noe tidspunkt, eller uttøm alle seksten forsøk uten et korrekt passord, og LoadFromFile returnerer 0 med LastErrorCode satt til 404, PDF Library for Delphi' kode for et avvist passord. Taket finnes av grunner utover ryddighet: en ubegrenset retry-løkke er en lett måte å gjøre ett feiltastet passord om til en utilsiktet denial-of-service mot hvilken som helst tråd som kjører lastingen, spesielt når en håndterer er koblet til noe automatisert, som en liste over tidligere sette passord, snarere enn et menneske som klikker gjennom en dialog. PDF Library for Delphi respekterer også Abort kalt på TPDFlib-instansen fra inne i håndtereren, ettersom Sender ankommer som det samme objektet, nyttig bak en Avbryt-knapp på en passorddialog, og stopper retry-løkken ved neste sjekk uansett hva Retry var satt til. En lasting som feiler av en annen grunn enn et feil passord, en skadet kryssreferansetabell for eksempel, går aldri inn i retry-løkken i det hele tatt: PDF Library for Delphi rapporterer LastErrorCode 401 og stopper etter det første forsøket, fordi ingen mengde passordgjettinger fikser en strukturelt ødelagt fil

PDF Library for Delphi: Forsøksspor fra én til seksten som viser OnPassword utløst fra forsøk to og utover, forsøk sytten avslått uten å kalle behandleren, og de separate utfallene for suksess, LastErrorCode 404 og LastErrorCode 401
Bare passordet som gis til selve last-kallet teller som forsøk én, og callback-en kjører aldri mer enn femten ganger per innlasting. En strukturelt ødelagt fil omgår løkken helt og rapporterer 401 etter ett enkelt forsøk

Fungerer retry-løkken likt for filer, strømmer, og strenger?

OnPassword-callbacken og seksten-forsøks-taket oppfører seg identisk på tvers av LoadFromFile, LoadFromStream, og LoadFromString, selv om de tre inngangspunktene holder på kilden sin forskjellig mellom forsøk. En filsti er billig å besøke på nytt, ettersom hvert forsøk ganske enkelt åpner den navngitte filen på nytt, og en strengkilde sitter allerede i minnet som kallerens egen kopi, så ingen av dem trenger noen hjelp fra kalleren mellom forsøk. En kaller-levert strøm er det ene tilfellet verdt å stoppe ved: LoadFromStream søker den strømmen tilbake til posisjon null og kopierer den internt før det første parse-forsøket, så hvert etterfølgende forsøk, og den ferskt konstruerte TPDFDocument-en bak det, spiller av på nytt fra den interne kopien i stedet for fra hvor enn et mislykket parse-forsøk etterlot strømmens posisjon. Overlever PDF Library for Delphi en TFileStream eller TMemoryStream for et passordbeskyttet dokument, og det er ikke nødvendig å spole den tilbake mellom forsøk; PDF Library for Delphi tar allerede høyde for en posisjon et første, mislykket forsøk kan ha flyttet

Å tilpasse passord-retry inn i en dokument-inntaksskjerm

En dokument-inntaks-arbeidsflyt er det naturlige hjemmet for denne callbacken, fordi det er nøyaktig den formen problem OnPassword ble bygget for å løse: en fil ankommer utenfra applikasjonen, passordet dets er ikke kjent med sikkerhet på forhånd, og personen som leverer kandidater, trenger mer enn ett gjettforsøk uten at den omkringliggende koden skriver sin egen retry-løkke rundt LoadFromFile

procedure TIntakeForm.SupplyPassword(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean);
var
  Typed: string;
begin
  // AttemptNumber telles fra 2: passordet som allerede ble prøvd, var forsøk 1.
  Typed := '';
  Retry := InputQuery('Password required',
    Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
  if Retry then
    Password := Typed;
  // Retry er False når operatøren avbryter, noe som lar
  // LastErrorCode stå på 404 for den kallende å rapportere.
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)        // bare et verifisert dokument når hit
    else
      LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
  finally
    Lib.Free;
  end;
end;

RegisterIntakeDocument mottar bare noensinne Lib når LoadFromFile har returnert 1, noe som betyr at et passord i den utvekslingen faktisk verifiserte mot filens krypto-håndterer; et avvist forsøk når aldri den linjen, og det gjør heller ikke et halvåpent dokument. Hva som kommer neste, når et dokument som dette er bekreftet åpent, er verdt et andre blikk på beskyttelsesinnstillingene dets, snarere enn en antakelse om at passordet som virket, er hele sikkerhetshistorien: å revidere hva et dokuments /Encrypt-ordbok faktisk deklarerer dekker å lese algoritmen, revisjonen, og tillatelsesbitene PDF Library for Delphi eksponerer når en fil som denne lastes inn

Passord-retry er også en snever instans av en bredere disiplin PDF Library for Delphi anvender gjennom hele parsingslaget sitt: en fil som ennå ikke har bevist seg selv, får ingen tvilens fordel, uansett om spørsmålet er hvilket passord som låser den opp, eller om et lengdefelt inni den lyver om størrelsen på bufferen den trenger. Å herde en Pascal-PDF-parser mot ondsinnede filer dekker den andre halvparten av den disiplinen, dekoderne som behandler hvert skrifttypeprogram og bildestrøm i en innkommende PDF som fiendtlig inndata snarere enn et velformet dokument som bare glemte passordet sitt

OnPassword og retry-løkken bak den er en del av den standard PDF Library for Delphi PDF-biblioteket for Delphi og C++Builder, tilgjengelig overalt LoadFromFile, LoadFromStream, eller LoadFromString allerede er, uten at noen separat modul eller lisensklasse kreves for et dokument som bare trenger et andre gjettforsøk på passordet sitt