Tehnički članak

Ne možete nečujno izmeniti šifrovani PDF u Delphiju

Ako uzmete fakturu u PDF-u koja već koristi AES-256 šifrovanje i zatražite od PDFium Component za Delphi i C++Builder (PDFiumPas) da je obeleži kao PDF/A radi arhivskog čuvanja ili da je potpiše pomoću PAdES-a, kroz inkrementalno ažuriranje umesto potpunog prepisivanja, biblioteka to neće postići direktnim menjanjem šifrovanih bajtova: njenih šest ubrizgivača oznaka usaglašenosti otkrivaju postojeći unos /Encrypt i kopiraju izvor bajt po bajt bez izmena, a potpisnik za PAdES podiže izuzetak umesto da emituje potpis koji nijedan validator neće prihvatiti

To je drugo pitanje u odnosu na proveru PDF-a koji niste sami napravili zbog skrivenih rizika, što je zaseban postupak samo za čitanje. Ovaj članak bavi se stranom upisivanja iste granice poverenja: šta vaš sopstveni kôd sme da uradi sa datotekom čiji su bajtovi već zaključani tuđom lozinkom kada pokuša naknadno da joj doda bilo šta

Šta ISO 32000-1 zahteva pri ažuriranju šifrovanog PDF-a?

ISO 32000-1 §7.5.6 zahteva da trailer inkrementalnog ažuriranja ponovi svaki unos iz prethodnog trailera osim /Prev, a Tabela 15 navodi /Encrypt među unosima koje trailer može da sadrži. Ako ga izostavite iz novog trailera, usaglašeni čitač nema razlog da posumnja u izostavljanje: najnoviji trailer je merodavan, pa čitač koji tamo ne pronađe /Encrypt zaključuje da cela datoteka nije šifrovana i pokušava da staro, i dalje šifrovano telo tumači kao obične bajtove. Ako zadržite /Encrypt u novom traileru, ali objekte ažuriranja upišete kao otvoreni tekst, greška se samo pomera za jedan korak: čitač ispravno otkriva šifrovanje, provlači svaki objekat koji dodirne kroz šifru datoteke, uključujući nove objekte koji nikada nisu bili šifrovani, i za sadržaj koji je pre dešifrovanja bio potpuno čitljiv dobija besmislene podatke. Bilo koja greška proizvodi datoteku koja na nivou bajtova izgleda kao normalno, dobro oblikovano inkrementalno ažuriranje, sve dok je ne otvori usaglašeni čitač

Šest ubrizgivača oznaka i jedna provera šifrovanja u v2.14.2

PDFiumPas isporučuje šest ubrizgivača oznaka na nivou bajtova, po jedan za svaki ISO PDF podskup koji može da obelež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) i PDF/VT-1 (ISO 16612-2). Svaki uzima bajtove koje je već upisao sam PDFium pozivom FPDF_SaveAsCopy i preko njih dodaje drugo, manje inkrementalno ažuriranje: novi XMP tok metapodataka, izmenu kataloškog rečnika koja pokazuje na njega i, za podskupove usmerene na štampu, OutputIntent i ICC profil. Od v2.14.2 svaki od InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers i InjectPdfVTMarkers najpre čita trailer izvora i, ako prijavi postojeći unos /Encrypt, kopira izvor u odredišni tok bez izmene i odmah vraća rezultat. Nema XMP-a, nema OutputIntent-a, nema izmene kataloga — pozivalac dobija originalnu datoteku, bajt po bajt

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;

To što je dozvoljeno šifrovanje ne znači da je bezbedno ubrizgati sadržaj

PDF/E-1 i PDF/R-1 na nivou specifikacije izričito dozvoljavaju da njihov dokument domaćin bude šifrovan, što zvuči kao izuzetak sve dok ne pogledate šta zaista mora da se upiše na disk. ISO 24517-1 §6.3 dopušta šifrovanje za PDF/E-1, a ISO 23504-1 §6.2.3 dopušta ga za PDF/R-1 ako zaglavlje navodi %PDF-2.0. Nijedna odredba ne govori da li procesor na nivou bajtova može bezbedno da doda objekat u otvorenom tekstu u taj šifrovani kontejner, a ne može, iz istih razloga prema §7.5.6 koji važe za svaki drugi podskup. Sopstveni validatori usaglašenosti PDFiumPas-a za ova dva profila, ValidatePdfECompliance i ValidatePdfRCompliance, namerno beleže prisustvo /Encrypt bez prijavljivanja greške, što je ispravno za validator samo za čitanje koji nikada ne upisuje bajt. Isti obrazac je lako prevideti i pretpostaviti da pomoćnom ubrizgivaču nije potrebna posebna provera, iako je upravo ubrizgivač ona funkcija u paru koja mora da odbije zahtev

Da li SaveAsPdfX nečujno dešifruje vaš dokument?

Da, kada koristite javne pomoćne metode umesto direktnog poziva ubrizgivača. Svaka od TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR i SaveAsPdfVT prikazuje trenutni dokument u privremeni tok pomoću SaveAs(Tmp, saRemoveSecurity) pre nego što te bajtove prosledi odgovarajućem ubrizgivaču. saRemoveSecurity preslikava se na sopstvenu PDFium oznaku FPDF_REMOVE_SECURITY, pa privremena kopija koju ubrizgivač dobija nikada nije bila šifrovana i provera /Encrypt u ubrizgivaču nema razlog da se aktivira. Izlaz nosi vaše oznake PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 ili PDF/VT-1, ali više nije zaštićen lozinkom kojom je izvor otvoren

Ta razmena postaje vidljiva tek kada neko kasnije otvori „zaštićenu“ arhivsku kopiju bez lozinke i primeti da se otvara bez problema. Rešenje nije drugi poziv metode; PDFiumPas nema pandan saAddSecurity uz saRemoveSecurity, zato što osnovni PDFium mehanizam nikada nije napravljen da upisuje novo šifrovanje, već samo da ga ukloni. Ako su oba svojstva važna za jednu datoteku, šifrovanje mora biti zaseban korak koji vi kontrolišete, primenjen posle oznaka usaglašenosti, a ne uklopljen u isti poziv 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;

Šta se dešava kada šifrovani PDF potpišete pomoću PAdES-a?

PDFiumPas zahtev odmah odbija, umesto da ga tiho ignoriše kao ubrizgivač oznaka. TPdf.SignPades i SignPadesToStream oba prolaze kroz interni SignPadesBytes, a prva stvar koju on radi nakon čitanja izvornog trailera jeste provera unosa /Encrypt. Ako je unos prisutan, podiže EPadesCrypto sa porukom "SignPadesBytes: the source document is encrypted; remove encryption before signing" umesto da nastavi dalje. InjectPadesDssMarkers, funkcija koja ugrađuje sertifikate, OCSP odgovore i CRL-ove za dugoročnu validaciju, primenjuje istu proveru iz istog razloga, sa sopstvenom porukom: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"

Obrazloženje je ovde strože nego propusno ponašanje ubrizgivača oznaka, i to namerno. Tiho propuštanje bez izmene bezbedno je za PDF/A oznaku jer preskakanje ostavlja isti važeći PDF koji ste imali, samo bez oznake. Potpis ne sme tako tiho da podbaci: potpis koji nikada nije dodat izgleda, svakom kodu koji poziva funkciju i proverava samo logičku vrednost, potpuno isto kao uspešno dodat potpis. EPadesCrypto nasleđuje običnu klasu Exception, pa je hvatanje tog izuzetka uobičajena obrada izuzetaka, a ne posebna konvencija toka izvršavanja koju morate da učite

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;

Redosled oznaka usaglašenosti, potpisa i šifrovanja

Praktična ispravka je redosled, a ne druga biblioteka. Najpre primenite oznake PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 ili PDF/VT-1, zatim dodajte PAdES potpis i tek onda pokrenite korak u svom toku obrade koji zaista upravlja šifrovanjem, bilo da je to namenski PDF pisač, uređaj za potpisivanje ili sopstvena AES implementacija. Sloj inkrementalnih ažuriranja PDFiumPas-a prirodno se uklapa u sredinu tog redosleda, dodajući male, ciljane objekte u datoteku koja je inače završena, a šifrovanje pripada na kraj upravo zato što je to jedina operacija u lancu koju PDFiumPas sam ne može ni da izvrši ni da poništi

Sve ovo ne menja način na koji PDFiumPas čita trailer i podatke unakrsnih referenci od kojih zavisi svako inkrementalno ažuriranje, a ta putanja postaje suptilna kada se pojave xref tokovi; provera tokova objekata i xref tokova u PDF-u objašnjava kako ista putanja čitanja trailera obrađuje kompresovane strukture PDF 1.5+. A kada je dokument spreman za nešto jače od oznake usaglašenosti, potpisivanje PDF-a PAdES B-B potpisom u Delphiju je mesto na kojem SignPades preuzima posao tačno tamo gde ovaj članak završava

Ubrizgivači oznaka i metode SignPades opisane ovde isporučuju se kao deo PDFium Component za Delphi i C++Builder, uz mogućnosti iscrtavanja i pregleda samo za čitanje koje PDFium izvorno pruža