Tekninen artikkeli

Salatun PDF:n salasanan uudelleenyrittäminen Delphissä PDFlibPas:lla

PDFlibPas yrittää väärää salasanaa uudelleen salatussa PDF:ssä hylkäämällä TPDFDocument-olion, joka juuri epäonnistui, ja luomalla täysin uuden seuraavaa yritystä varten, ohjattuna OnPassword-takaisinkutsulla (TPDFlibPasswordEvent), joka ajetaan jopa kuudentoista yrityksen ajan ennen luovuttamista. Se on tarkoituksellinen poikkeama vaistosta, johon useimmat Delphi-kehittäjät turvautuvat ensin: pidä muistissa jo istuva asiakirjaolio, syötä sille korjattu salasana, ja lataa uudelleen paikallaan alusta aloittamisen sijaan. PDFlibPas:n uudelleenyrityssilmukka, lisätty v3.245.0:ssa, ottaa vastakkaisen kannan, syistä, jotka ovat spesifisiä sille, mitä epäonnistunut salasanayritys jättää jälkeensä. Sen takana oleva skenaario on riittävän tavallinen, että useimmat asiakirjapainotteiset Delphi-sovellukset osuvat siihen lopulta: vastaanottonäyttö hyväksyy PDF:n, salattu jälki pakottaa salasanadialogin, operaattori näppäilee merkkijonon väärin, ja dialogi ilmestyy uudelleen toista yritystä varten. Mikään tuossa käyttäjäkokemuksessa ei ole epätavallista, joten sen takana olevan koodin on hyväksyttävä useampi kuin yksi ehdokassalasana samalle tiedostolle, ja sen on tehtävä se turvallisesti, vuotamatta tilaa hylätystä yrityksestä seuraavaan

Mikset voi vain yrittää uudelleen samalla asiakirjaoliolla?

TPDFDocument-olion uudelleenkäyttö salasanayritysten yli ei toimi, koska epäonnistunut yritys on jo purkanut tuon olion sisäisesti sen sijaan, että jättäisi sen johonkin keskeytettyyn, jatkettavissa olevaan tilaan. Salatun PDF:n avaaminen tarkoittaa ristiviittaustaulun jäsentämistä, lukijan rakentamista taustalla olevan lähteen yli, ja salauskäsittelijän rakentamista mistä tahansa toimitetusta salasanasta, kaikki ennen kuin PDFlibPas voi edes testata, onko tuo salasana oikea. Kun salasana osoittautuu vääräksi, asiakirjan sisäinen latausrutiini siivoaa lukijan, ristiviittaustaulun ja salauskäsittelijän osana epäonnistumista, täsmälleen kuten sen pitäisi, mikä tarkoittaa, ettei siellä ole mitään puoliksi rakennettua jäsennintä odottamassa korjattua salasanaa toisella kutsulla. Aja sama olio silti läpi toisen latausyrityksen, ja vikatila on juuri sellainen, joka on kurja debugata: virhe paljastuu sisäisestä tilasta, joka on rakennettu eri, jo epäonnistuneelle jäsennykselle, ilman mitään ilmeisesti osoittamassa takaisin salasanaan kolme kutsua ylävirtaan. PDFlibPas välttää koko ongelmaluokan yrittämättä koskaan palauttaa asiakirjaoliota, kun se on kerran epäonnistunut avautumaan; jokainen yritys saa asiakirjan, joka ei ole koskaan nähnyt väärää salasanaa, lukija ja ristiviittaustaulu mukaan lukien

Miten OnPassword-takaisinkutsu pyytää seuraavaa salasanaa?

TPDFlibPasswordEvent on takaisinkutsutyyppi, jonka PDFlibPas kutsuu TPDFlib.LoadFromFile-, LoadFromStream- ja LoadFromString-metodien kautta aina, kun juuri kokeiltu salasana osoittautuu vääräksi, ja se antaa käsittelijälle kolme asiaa: mikä yritys on menossa käynnistymään, Password-parametrin ylikirjoitettavaksi seuraavalla ehdokkaalla, ja Retry-lipun, joka oletusarvoistuu falseksi

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

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

Alkuperäiseen LoadFromFile-kutsuun välitetty salasana lasketaan yritykseksi yksi, joten ensimmäisellä kerralla, kun OnPassword laukeaa lainkaan, AttemptNumber saapuu arvolla 2. Jätä Retry asettamatta, ja lataus epäonnistuu puhtaasti LastErrorCode-arvolla 404; aseta se todeksi, ja PDFlibPas yrittää uudelleen millä tahansa, mitä käsittelijä juuri kirjoitti Password-kenttään

Uudelleenyrityssilmukan sisällä: uusi TPDFDocument jokaiselle yritykselle

Sisäisesti PDFlibPas vastaa oliokiertokysymykseen samalla tavalla LoadFromFile-, LoadFromStream- ja LoadFromString-metodeille: jokainen yritys, ensimmäinen mukaan lukien, rakentaa tuoreen TPDFDocument-olion, ajaa sen läpi täyden avaussekvenssin millä tahansa salasanalla, jota tuo yritys käyttää, ja pitää olion vain, jos salasana todentuu. Hylätyn yrityksen TPDFDocument vapautetaan välittömästi, viedessään mukanaan lukijansa, ristiviittaustaulunsa ja salauskäsittelijänsä, ja seuraava yritys alkaa alusta oliolla, jolla ei ole lainkaan historiaa

// 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;

Tuo Doc := nil -rivi juuri ennen Finally-lohkoa on koko oliokiertosopimus yhdessä lauseessa. Asiakirja, joka epäonnistuu, kantaa puoliksi rakennetun jäsennintilansa hautaan mukanaan, suunnitellusti, ja asiakirja, joka onnistuu, on ainoa koskaan lisätty FDocs:iin, kokoelmaan, jota TPDFlib pitää jokaiselle asiakirjalle, jonka kutsuja pitää auki. Mikään hylätystä yrityksestä ei ole näkyvissä uudelleenyrityssilmukan ulkopuolelta: ei puoliksi alustettu lukija, ei vanhentunut sivumäärä, ei väärästä avaimesta rakennettu salauskäsittelijä

Kuinka monta kertaa PDFlibPas yrittää väärää salasanaa uudelleen?

PDFlibPas sallii yhteensä kuusitoista yritystä yhtä LoadFromFile-, LoadFromStream- tai LoadFromString-kutsua vastaan, laskien itse kutsuun välitetyn salasanan yritykseksi yksi. OnPassword laukeaa vain koskaan yrityksille kaksi kuuteentoista, mikä rajaa takaisinkutsun viiteentoista kutsuun; pyydä seitsemättätoista yritystä, ja PDFlibPas kieltäytyy jopa kutsumatta käsittelijää. Jätä Retry oletusarvoonsa false missä tahansa kohdassa, tai kuluta kaikki kuusitoista yritystä ilman oikeaa salasanaa, ja LoadFromFile palauttaa 0:n LastErrorCode-arvolla 404, PDFlibPas:n koodilla hylätylle salasanalle. Katto on olemassa muistakin syistä kuin siisteydestä: rajoittamaton uudelleenyrityssilmukka on helppo tapa muuttaa yksi väärin kirjoitettu salasana vahingossa tapahtuvaksi palvelunestohyökkäykseksi mitä tahansa säiettä vastaan, joka ajaa latausta, varsinkin heti kun käsittelijä on kytketty johonkin automatisoituun, kuten listaan aiemmin nähtyjä salasanoja, ihmisen klikkaillessa läpi dialogia sen sijaan. PDFlibPas kunnioittaa myös Abort-kutsua, joka tehdään TPDFlib-instanssille käsittelijän sisältä, koska Sender saapuu samana oliona, hyödyllinen Cancel-painikkeen takana salasanadialogissa, ja pysäyttää uudelleenyrityssilmukan seuraavassa tarkistuksessa riippumatta siitä, mihin Retry oli asetettu. Lataus, joka epäonnistuu muusta syystä kuin väärästä salasanasta, vioittuneesta ristiviittaustaulusta esimerkiksi, ei koskaan astu uudelleenyrityssilmukkaan lainkaan: PDFlibPas raportoi LastErrorCode 401:n ja pysähtyy ensimmäisen yrityksen jälkeen, koska mikään määrä salasana-arvauksia ei korjaa rakenteellisesti rikkinäistä tiedostoa

Toimiiko uudelleenyrityssilmukka samalla tavalla tiedostoille, virroille ja merkkijonoille?

OnPassword-takaisinkutsu ja kuudentoista yrityksen katto käyttäytyvät identtisesti LoadFromFile-, LoadFromStream- ja LoadFromString-metodeissa, vaikka nämä kolme sisääntulopistettä pitävät lähteestään kiinni eri tavoin yritysten välillä. Tiedostopolku on halpa vierailla uudelleen, koska jokainen yritys yksinkertaisesti avaa nimetyn tiedoston uudelleen, ja merkkijonolähde jo istuu muistissa kutsujan omana kopiona, joten kumpikaan ei tarvitse mitään apua kutsujalta yritysten välillä. Kutsujan toimittama virta on se yksi tapaus, joka kannattaa pysähtyä miettimään: LoadFromStream hakeutuu tuon virran takaisin nollapositioon ja kopioi sen sisäisesti ennen ensimmäistä jäsennysyritystä, joten jokainen sitä seuraava yritys, ja sen takana oleva juuri rakennettu TPDFDocument, toistaa tuosta sisäisestä kopiosta eikä siitä, mihin epäonnistunut jäsennys jätti virran position. Anna PDFlibPas:lle TFileStream tai TMemoryStream salasanasuojatulle asiakirjalle, eikä sitä tarvitse kelata takaisin uudelleenyritysten välillä; PDFlibPas ottaa jo huomioon position, jonka ensimmäinen, epäonnistunut yritys on saattanut siirtää

Salasanan uudelleenyrityksen sovittaminen asiakirjan vastaanottonäyttöön

Asiakirjan vastaanottotyönkulku on luonnollinen koti tälle takaisinkutsulle, koska se on juuri sellainen ongelmamuoto, jota varten OnPassword rakennettiin: tiedosto saapuu sovelluksen ulkopuolelta, sen salasanaa ei tiedetä varmuudella etukäteen, ja ehdokkaita toimittavan henkilön on saatava useampi kuin yksi arvaus ilman että ympäröivä koodi kirjoittaa oman uudelleenyrityssilmukkansa LoadFromFile:n ympärille

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 vastaanottaa Lib:n vain koskaan sen jälkeen, kun LoadFromFile on palauttanut 1:n, mikä tarkoittaa, että jokin salasana tuossa vaihdossa todella todentui tiedoston salauskäsittelijää vasten; hylätty yritys ei koskaan saavuta tuota riviä, eikä myöskään puoliksi avoin asiakirja. Se, mitä seuraavaksi tulee, heti kun tällainen asiakirja on vahvistettu auki, kannattaa vielä toinen katsaus sen suojausasetuksiin sen sijaan, että oletettaisiin salasanan, joka toimi, olevan koko tietoturvatarina: asiakirjan /Encrypt-sanakirjan todellisen ilmoituksen tarkastaminen käsittelee algoritmin, revision ja käyttöoikeusbittien lukemisen, jotka PDFlibPas paljastaa heti, kun tällainen tiedosto latautuu

Salasanan uudelleenyritys on myös kapea esimerkki laajemmasta kurista, jota PDFlibPas soveltaa koko jäsennyskerroksessaan: tiedosto, joka ei ole vielä todistanut itseään, ei saa epäilyksen etua, olipa kysymys siitä, mikä salasana avaa sen, tai siitä, valehteleeko sen sisällä oleva pituuskenttä puskurin koosta, jota se tarvitsee. Pascal-PDF-jäsentimen kovettaminen haitallisia tiedostoja vastaan käsittelee tuon kurin toisen puoliskon, dekooderit, jotka kohtelevat jokaista fonttiohjelmaa ja kuvavirtaa saapuvassa PDF:ssä vihamielisenä syötteenä eikä hyvin muodostettuna asiakirjana, joka vain unohti salasanansa

OnPassword ja sen takana oleva uudelleenyrityssilmukka ovat osa vakiomuotoista PDFlibPas PDF-kirjastoa Delphille ja C++Builderille, saatavilla missä tahansa, missä LoadFromFile, LoadFromStream tai LoadFromString jo ovat, ilman erillistä moduulia tai lisenssitasoa vaadittuna asiakirjalle, joka vain tarvitsee toisen arvauksen salasanaansa