Tehnični članak

Šifriranega PDF-ja v Delphiju ne morete neopazno popraviti

Vzemite PDF računa, ki že uporablja šifriranje AES-256, in od komponente PDFium Component za Delphi in C++Builder (PDFiumPas) zahtevajte, naj ga zaradi arhivskega hranjenja označi kot PDF/A ali ga podpiše s PAdES prek inkrementalne posodobitve namesto s popolnim prepisom. Knjižnica tega ne bo dosegla z neposrednim popravljanjem šifriranih bajtov: njenih šest vbrizgalnikov označevalnikov skladnosti zazna obstoječi vnos /Encrypt in izvor kopira v cilj bajt za bajtom nespremenjen, podpisnik PAdES pa sproži izjemo, namesto da bi izdal podpis, ki ga noben validator ne bi sprejel

To je drugačno vprašanje od preverjanja PDF-ja, ki ga niste ustvarili, zaradi skritega tveganja, kar je samostojen postopek samo za branje. Ta članek obravnava zapisovalno stran iste meje zaupanja: kaj lahko vaša koda stori z datoteko, katere bajti so že zaklenjeni za geslom nekoga drugega, ko ji poskuša naknadno karkoli dodati

Kaj zahteva ISO 32000-1 pri posodobitvi šifriranega PDF-ja

ISO 32000-1 §7.5.6 zahteva, da priklopnik inkrementalne posodobitve ponovi vsak vnos iz prejšnjega priklopnika, razen /Prev, tabela 15 pa med vnosi, ki jih lahko priklopnik vsebuje, navaja /Encrypt. Če ga izpustite iz novega priklopnika, skladni bralnik nima razloga podvomiti o izpustu: najnovejši priklopnik je avtoritativen, zato bralnik, ki tam ne najde /Encrypt, sklene, da je celotna datoteka nešifrirana, in poskuša starejše, še vedno šifrirano telo razčleniti kot navadne bajte. Če /Encrypt ohranite v novem priklopniku, vendar objekte posodobitve zapišete kot navadno besedilo, se napaka samo premakne za korak: bralnik pravilno zazna šifriranje, vsak objekt, ki se ga dotakne, obdela s šifrom datoteke, vključno z novimi objekti, ki nikoli niso bili šifrirani, in za vsebino, ki je bila pred dešifriranjem povsem berljiva, dobi šum. Obe napaki ustvarita datoteko, ki je na ravni bajtov videti kot običajna, dobro oblikovana inkrementalna posodobitev, vse dokler je ne odpre skladni bralnik

Šest vbrizgalnikov označevalnikov in en šifrirni prehod v v2.14.2

PDFiumPas dobavlja šest vbrizgalnikov označevalnikov na ravni bajtov, po enega za vsako podmnožico ISO PDF, ki jo lahko označi: PDF/A (ISO 19005), PDF/X (ISO 15930), PDF/UA (ISO 14289-1), PDF/E-1 (ISO 24517-1), PDF/R-1 (ISO 23504-1) in PDF/VT-1 (ISO 16612-2). Vsak vzame bajte, ki jih je že zapisal PDFiumov lastni FPDF_SaveAsCopy, in nanje naloži drugo, manjšo inkrementalno posodobitev: nov tok metapodatkov XMP, spremembo kataloškega slovarja, ki kaže nanj, za podmnožice, usmerjene v tisk, pa še OutputIntent in profil ICC. Od v2.14.2 vsak od InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers in InjectPdfVTMarkers najprej prebere izvorni priklopnik in če ta sporoča obstoječi vnos /Encrypt, izvor kopira v ciljni tok nespremenjen ter se takoj vrne. Brez XMP, brez OutputIntent, brez spremembe kataloga — klicatelj dobi izvirno datoteko bajt za bajtom

var
  Src, Dst: TFileStream;
  Opts: TPdfXSaveOptions;
begin
  Src := TFileStream.Create('signed-encrypted-proof.pdf', fmOpenRead);
  Dst := TFileStream.Create('pdfx-attempt.pdf', fmCreate);
  try
    Opts.Conformance := pxc4;
    InjectPdfXMarkers(Src, Dst, Opts);
    // pdfx-attempt.pdf is byte-identical to the source: still encrypted,
    // no /GTS_PDFXVersion, no OutputIntent. Nothing was written, and
    // nothing was corrupted either
  finally
    Dst.Free;
    Src.Free;
  end;
end;

Dovoljeno šifriranje ni isto kot varen vbrizg

PDF/E-1 in PDF/R-1 na ravni specifikacije izrecno dovoljujeta, da je njun gostiteljski dokument šifriran, kar zveni kot izjema, dokler ne pogledate, kaj se mora dejansko zgoditi na disku. ISO 24517-1 §6.3 dovoljuje šifriranje za PDF/E-1, ISO 23504-1 §6.2.3 pa ga dovoljuje za PDF/R-1, če glava razglaša %PDF-2.0. Nobena določba ne govori o tem, ali lahko poprocesor na ravni bajtov varno doda navaden objekt v ta šifrirani vsebnik, in tega ne more, iz istih razlogov §7.5.6, ki veljajo za vse druge podmnožice. Lastna validatorja skladnosti PDFiumPas za ta profila, ValidatePdfECompliance in ValidatePdfRCompliance, namenoma zabeležita prisotnost /Encrypt, ne da bi jo označila kot napako, kar je pravilno za validator samo za branje, ki ne zapiše niti enega bajta. To je tudi vzorec, ki ga je hitro spregledati in domnevati, da sorodni vbrizgalnik ne potrebuje ločene zaščite, čeprav je vbrizgalnik tista funkcija v paru, ki se mora dejansko odreči izvedbi

Ali SaveAsPdfX vaš dokument neopazno dešifrira

Da, kadar uporabite javne priročne metode namesto neposrednega klica vbrizgalnika. Vsaka od metod TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR in SaveAsPdfVT upodobi trenutni dokument v začasni tok z SaveAs(Tmp, saRemoveSecurity), preden te bajte preda ustreznemu vbrizgalniku. saRemoveSecurity se preslika v PDFiumovo lastno zastavico FPDF_REMOVE_SECURITY, zato začasna kopija, ki jo prejme vbrizgalnik, sploh ni bila šifrirana in se zaščita vbrizgalnika pred /Encrypt nima razloga sprožiti. Izhod vsebuje vaše označevalnike PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 ali PDF/VT-1, vendar ni več zaščiten z geslom, ki je odprlo izvor

Ta kompromis ostane neopažen, dokler nekdo naknadno ne odpre »zaščitene« arhivske kopije brez gesla in opazi, da deluje brez težav. Popravek ni drugačen klic metode; PDFiumPas nima ustreznice saAddSecurity, ki bi jo lahko združili z saRemoveSecurity, ker osnovni pogon PDFium nikoli ni bil izdelan za zapisovanje novega šifriranja, temveč samo za njegovo odstranjevanje. Če sta za eno datoteko pomembni obe lastnosti, mora biti šifriranje ločen korak, za katerega poskrbite sami, izveden po dodajanju označevalnikov skladnosti in ne vključen v isti klic SaveAsPdfA

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.Password := 'open-secret';           // needed to open the source at all
    Pdf.FileName := 'signed-encrypted-invoice.pdf';
    Pdf.LoadDocument;
    Pdf.SaveAsPdfA('invoice-pdfa.pdf', pac2b);
    // invoice-pdfa.pdf now declares PDF/A-2b, but SaveAs(saRemoveSecurity)
    // ran first inside SaveAsPdfA: the output opens without a password
  finally
    Pdf.Free;
  end;
end;

Kaj se zgodi, ko šifriran PDF podpišete s PAdES

PDFiumPas to izrecno zavrne, namesto da bi zahtevo tiho izpustil, kot to naredi vbrizgalnik označevalnikov. TPdf.SignPades in SignPadesToStream oba vodita prek notranje metode SignPadesBytes, ki po razčlenitvi izvornega priklopnika najprej preveri /Encrypt. Če je vnos prisoten, sproži EPadesCrypto s sporočilom »SignPadesBytes: the source document is encrypted; remove encryption before signing«, namesto da bi nadaljevala. InjectPadesDssMarkers, funkcija, ki za dolgoročno preverjanje vgradi potrdila, odzive OCSP in CRL, izvede enako preverjanje iz istega razloga, s svojim sporočilom: »InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material«

Razlogovanje je tu strožje kot pri prehodu brez sprememb vbrizgalnikov označevalnikov in namenoma je tako. Tihi prehod je varen pri žigu PDF/A, ker preskok pusti isti veljavni PDF, s katerim ste začeli, samo brez oznake. Podpis ne sme tako tiho odpovedati: podpis, ki ni bil nikoli dodan, je za klicateljsko kodo, ki preveri samo logično vrednost, videti popolnoma enako kot uspešno dodan podpis. EPadesCrypto izhaja iz običajnega razreda Exception, zato je njegovo prestrezanje običajna obravnava izjem in ne posebna konvencija nadzora toka, ki bi se je morali naučiti

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.Password := 'open-secret';
    Pdf.FileName := 'encrypted-contract.pdf';
    Pdf.LoadDocument;
    try
      Pdf.SignPades('encrypted-contract-signed.pdf', 'A1B2C3D4E5F6...');
    except
      on E: EPadesCrypto do
        // E.Message: 'SignPadesBytes: the source document is encrypted;
        // remove encryption before signing'
        raise Exception.Create('Decrypt the contract before signing: '+ E.Message);
    end;
  finally
    Pdf.Free;
  end;
end;

Zaporedje označevalnikov skladnosti, podpisov in šifriranja

Praktični popravek je vrstni red in ne drugačna knjižnica. Najprej uporabite označevalnike PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 ali PDF/VT-1, nato dodajte morebitni podpis PAdES in šele potem izvedite korak v svojem cevovodu, ki dejansko skrbi za šifriranje, naj bo to namenski zapisovalnik PDF, naprava za podpisovanje ali vaša izvedba AES. Inkrementalna plast posodobitev PDFiumPas se naravno prilega na sredino tega zaporedja, saj na sicer dokončano datoteko pripne majhne, ciljno usmerjene objekte, šifriranje pa spada na konec prav zato, ker je edina operacija v verigi, ki je PDFiumPas sam ne more izvesti ali razveljaviti

Nič od tega ne spremeni načina, kako PDFiumPas bere priklopnik in podatke navzkrižnih sklicev, od katerih je odvisna vsaka inkrementalna posodobitev, kar je samo po sebi vir podrobnih posebnosti, ko se pojavijo tokovi xref; članek preverjanje objektnih tokov in tokov xref v PDF-ju pojasnjuje, kako ista pot branja priklopnika obravnava stisnjene strukture PDF 1.5+. Ko je dokument pripravljen na nekaj močnejšega od označevalnika skladnosti, pa je članek podpisovanje PDF-ja s podpisom PAdES B-B v Delphiju mesto, kjer SignPades prevzame delo natanko tam, kjer ta članek konča

Tu opisani vbrizgalniki označevalnikov in metode SignPades so del komponente PDFium Component za Delphi in C++Builder, skupaj z upodabljanjem in pregledovanjem samo za branje, ki ju PDFium zagotavlja izvorno