Articol tehnic

Nu puteți corecta silențios un PDF criptat în Delphi

Luați un PDF de factură care poartă deja criptare AES-256 și cereți componentei PDFium pentru Delphi și C++Builder (PDFiumPas) să îl ștampileze PDF/A pentru retenție de arhivă, sau să îl semneze cu PAdES, printr-o actualizare incrementală, nu printr-o rescriere completă. Biblioteca nu va ajunge acolo corectând octeții criptați direct: cei șase injectori de marcatori de conformitate ai săi detectează o intrare /Encrypt existentă și trec sursa către destinație octet-cu-octet neschimbată, iar semnatarul său PAdES ridică o excepție, în loc să emită o semnătură pe care niciun validator nu o va accepta

Aceasta este o întrebare diferită de auditarea unui PDF pe care nu l-ați creat pentru risc ascuns, ceea ce este propriul său exercițiu doar-citire. Acest articol este despre partea de scriere a aceleiași granițe de încredere: ce are voie propriul dvs. cod să facă unui fișier ale cărui octeți sunt deja blocați în spatele parolei altcuiva, în momentul în care acel cod încearcă să adauge orice la el ulterior

Ce cere ISO 32000-1 când actualizați un PDF criptat?

ISO 32000-1 §7.5.6 cere ca trailer-ul unei actualizări incrementale să repete fiecare intrare din trailer-ul anterior, cu excepția /Prev, iar Tabelul 15 listează /Encrypt printre intrările pe care le poate purta un trailer. Eliminați-l din noul trailer, iar un cititor conform nu are niciun motiv să pună la îndoială omisiunea: cel mai nou trailer este autoritar, așa că un cititor care nu găsește niciun /Encrypt acolo decide că întregul fișier este necriptat și încearcă să analizeze corpul mai vechi, încă cifrat, ca octeți simpli. Păstrați /Encrypt în noul trailer, dar scrieți propriile obiecte ale actualizării ca text simplu, iar eșecul pur și simplu se mută cu un pas mai târziu: cititorul detectează corect criptarea, rulează fiecare obiect pe care îl atinge prin cifrul fișierului, inclusiv obiectele noi care nu au fost niciodată criptate în primul rând, și primește zgomot înapoi pentru conținut care era perfect lizibil înainte ca decriptarea să îl atingă. Oricare greșeală produce un fișier care arată ca o actualizare incrementală normală, bine formată la nivel de octet, chiar până când un cititor conform îl deschide

Șase injectori de marcatori, o singură poartă de criptare v2.14.2

PDFiumPas livrează șase injectori de marcatori la nivel de octet, câte unul pentru fiecare subset PDF ISO pe care îl poate eticheta: 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). Fiecare preia octeții pe care propriul FPDF_SaveAsCopy al PDFium i-a scris deja și stratifică o a doua actualizare incrementală, mai mică, deasupra lor: un nou flux de metadate XMP, o editare de dicționar de catalog care indică spre el, și pentru subseturile orientate spre tipar, un OutputIntent și un profil ICC. Începând cu v2.14.2, fiecare din InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers și InjectPdfVTMarkers citește mai întâi trailer-ul sursă, iar dacă acesta raportează o intrare /Encrypt existentă, copiază sursa către destinație nemodificată și revine imediat. Niciun XMP, niciun OutputIntent, nicio editare de catalog — apelantul primește înapoi fișierul original, octet cu octet

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;

Permis să fie criptat nu este același lucru cu sigur de injectat

PDF/E-1 și PDF/R-1 ambele permit explicit ca documentul lor gazdă să fie criptat la nivel de specificație, ceea ce se citește ca o excepție până când vă uitați la ce chiar trebuie să se întâmple pe disc. ISO 24517-1 §6.3 permite criptare pentru PDF/E-1, iar ISO 23504-1 §6.2.3 o permite pentru PDF/R-1 cu condiția ca antetul să declare %PDF-2.0. Niciuna din clauze nu spune nimic despre dacă un post-procesor la nivel de octet poate adăuga în siguranță un obiect text simplu la acel container criptat, iar acesta nu poate, din aceleași motive §7.5.6 care se aplică fiecărui alt subset. Proprii validatori de conformitate ai PDFiumPas pentru aceste două profiluri, ValidatePdfECompliance și ValidatePdfRCompliance, înregistrează prezența /Encrypt deliberat fără a o marca drept defect, ceea ce este corect pentru un validator doar-citire care nu scrie niciodată un octet. Este de asemenea un tipar ușor de trecut cu vederea și de presupus că injectorul soră nu are nevoie de o gardă separată, când injectorul este cea singură funcție din pereche care chiar trebuie să refuze

Decriptează silențios SaveAsPdfX documentul dvs.?

Da, ori de câte ori treceți prin metodele publice de conveniență în loc să apelați direct un injector. Fiecare din TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR și SaveAsPdfVT randează documentul curent într-un flux temporar cu SaveAs(Tmp, saRemoveSecurity) înainte de a preda acei octeți injectorului corespunzător. saRemoveSecurity se mapează la propriul steag FPDF_REMOVE_SECURITY al PDFium, așa că copia temporară pe care o primește injectorul nu a fost niciodată criptată în primul rând, iar garda /Encrypt a injectorului nu are niciodată un motiv să se declanșeze. Ieșirea poartă marcatorii dvs. PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, sau PDF/VT-1, dar nu mai este protejată de orice parolă a deschis sursa

Acel compromis este invizibil până când cineva din aval deschide copia de arhivă „protejată” fără o parolă și observă că pur și simplu funcționează. Soluția nu este un apel de metodă diferit; PDFiumPas nu are niciun corespondent saAddSecurity care să se împerecheze cu saRemoveSecurity, pentru că motorul PDFium de bază nu a fost niciodată construit pentru a scrie criptare nouă, doar pentru a o elimina. Dacă ambele proprietăți contează pentru un fișier, criptarea trebuie să fie un pas separat pe care îl dețineți dvs., aplicat după marcatorii de conformitate, nu pliat în același apel 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;

Ce se întâmplă când semnați un PDF criptat cu PAdES?

PDFiumPas refuză direct, în loc să renunțe silențios la cerere așa cum face un injector de marcatori. Atât TPdf.SignPades, cât și SignPadesToStream trec printr-un SignPadesBytes intern, iar primul lucru pe care îl face după analizarea trailer-ului sursă este verificarea pentru /Encrypt. Dacă intrarea este prezentă, ridică EPadesCrypto cu mesajul „SignPadesBytes: the source document is encrypted; remove encryption before signing” (documentul sursă este criptat; eliminați criptarea înainte de semnare), în loc să continue mai departe. InjectPadesDssMarkers, funcția care încorporează certificate, răspunsuri OCSP și CRL-uri pentru validare pe termen lung, aplică verificarea identică pentru motivul identic, cu propriul său mesaj: „InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material” (documentul sursă este criptat; eliminați criptarea înainte de a încorpora materialul de validare DSS)

Raționamentul de aici este mai strict decât trecerea directă a injectorilor de marcatori, și în mod deliberat. O trecere directă silențioasă este sigură pentru o ștampilă PDF/A, pentru că omiterea ei vă lasă cu același PDF valid cu care ați început, doar neetichetat. Semnarea nu poate eșua la fel de tăcut: o semnătură care silențios nu a fost niciodată adăugată arată, pentru orice cod apelant care doar verifică un rezultat boolean, exact ca o semnătură care a fost adăugată cu succes. EPadesCrypto descinde din clasa obișnuită Exception, așa că prinderea ei este tratare normală de excepții, nu o convenție specială de flux de control pe care trebuie să o învățați

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;

Secvențierea ștampilelor de conformitate, semnăturilor și criptării

Soluția practică este ordonarea, nu o bibliotecă diferită. Aplicați marcatorii PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, sau PDF/VT-1 primii, adăugați orice semnătură PAdES apoi, și doar după aceea rulați orice pas din conducta dvs. care efectiv deține criptarea, fie că este un scriitor PDF dedicat, un dispozitiv de semnare, sau propria dvs. implementare AES. Stratul de actualizare incrementală al PDFiumPas se potrivește natural la mijlocul acelei secvențe, adăugând obiecte mici, țintite, peste un fișier altfel terminat, iar criptarea aparține la sfârșit tocmai pentru că este singura operație din lanț pe care PDFiumPas însuși nu o poate efectua sau inversa

Nimic din toate acestea nu schimbă modul în care PDFiumPas citește datele de trailer și referință încrucișată de care depinde fiecare actualizare incrementală, ceea ce este propria sa sursă de subtilitate odată ce fluxurile xref intră în discuție; validarea fluxurilor de obiecte și xref ale unui PDF acoperă cum gestionează aceeași cale de citire a trailer-ului structurile comprimate PDF 1.5+. Iar odată ce un document este gata pentru ceva mai puternic decât o ștampilă de conformitate, semnarea unui PDF cu o semnătură PAdES B-B în Delphi este locul unde SignPades preia exact din punctul unde acest articol se oprește

Injectorii de marcatori și metodele SignPades descrise aici sunt livrate ca parte a componentei PDFium pentru Delphi și C++Builder, alături de randarea și inspecția doar-citire pe care PDFium le oferă nativ