Articol tehnic

Crypt filters PDF în Delphi: politici StmF, StrF și EFF

Componenta PDF HotPDF pentru Delphi implementează modelul crypt filter din ISO 32000-1 §7.6.5 ca trei politici independente, nu ca un singur switch: ConfigureCryptFilterDefaults atribuie separat string filter /StrF, stream filter /StmF și embedded-file filter /EFF, SetStreamCryptFilter suprascrie un singur stream, iar GetLoadedCryptFilterInfo raportează ce declară un fișier de intrare. Majoritatea bug-urilor de interoperabilitate ale PDF-urilor criptate trăiesc în spațiile dintre cele trei

Iată failure-ul care îi trimite pe oameni în acest layer. O echipă livrează un document în care content-ul paginii trebuie să rămână lizibil pentru un tool downstream, dar payload-ul atașat nu, așa că setează /EFF /StdCF și lasă /StmF /Identity. Acrobat îl deschide fără probleme. Un reader terț conform întoarce attachment-ul ca garbage ciphertext, deoarece /EFF este o politică de producer despre filtrul aplicat fișierelor embedded, iar un reader general rezolvă în continuare un stream nemarcat prin /StmF. Fix-ul nu este o altă valoare /EFF. Fix-ul este un filter /Crypt explicit pe stream-ul embedded-file însuși

Ce controlează de fapt layer-ul crypt filter

Crypt filters stau între algoritmul de criptare și object graph și decid ce obiecte atinge algoritmul, nu cum lucrează. Dicționarul /CF din encryption dictionary mapează nume la definiții de filtre, fiecare cu o metodă /CFM, un /Length opțional și un /AuthEvent. Cele trei entry-uri de top /StrF, /StmF și /EFF selectează apoi care dintre filtrele numite se aplică string-urilor, stream-urilor fără filtru explicit și fișierelor embedded. HotPDF limitează deliberat ce vor scrie handler-ele incluse. ConfigureCryptFilterDefaults acceptă doar numele rezervate pentru handler-ul activ: standard security handler emite /StdCF sau /Identity, public-key handler emite /DefaultCryptFilter sau /Identity, iar orice altceva ridică EArgumentException la call site. Filtrele scrise de producători externi sub alte nume sunt păstrate la load, inspection și compatibility-rewrite, așa că HotPDF este conservator ca writer și permisiv ca reader. Se aplică încă două guard-uri: apelul ridică EInvalidOpException după ce a început serializarea documentului și din nou dacă documentul se află într-un update incremental, deoarece politica de criptare nu poate fi schimbată între revisions ale aceluiași fișier

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'wrapper.pdf';
    Pdf.OwnerPassword := 'owner-secret';
    Pdf.UserPassword := 'open-secret';
    Pdf.CryptKeyLength := aes128;
    // string-urile criptate, page streams în clar, attachment-urile criptate
    Pdf.ConfigureCryptFilterDefaults('StdCF', 'Identity', 'StdCF');
    Pdf.ActivateProtection := True;
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(50, 50, 0, 'Visible stream operators');
    Pdf.AddDocumentAttachment('payload.bin', 'Encrypted payload');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

O limitare merită spusă de la început, deoarece este verificată târziu și surprinde oamenii. Crypt filters numite în HotPDF cer criptare de document aes128, aes256 sau aesgcm. Configurează o politică de filtru peste RC4 k40 sau k128 și pass-ul de validare care rulează când este activată criptarea ridică o eroare în loc să promoveze în tăcere tipul cheii. Este aceeași atitudine de design ca în restul traseului de criptare PDF AES-256 în Delphi: refuză configurația ambiguă în loc să ghicească intenția caller-ului

De ce înseamnă entry-ul /Length două lucruri diferite?

Deoarece spec-ul îl definește în două unități diferite în funcție de security handler, iar HotPDF trebuie să le respecte pe ambele. Într-un crypt filter dictionary al cărui /CFM este /V2, entry-ul /Length este exprimat în bytes sub standard security handler și în bits sub public-key handler. /Length din encryption dictionary, aflat lângă /V (ISO 32000-1 §7.6.2), este mereu în bits. Citește un filter dictionary cu /Length 16 și ai o cheie de 128 de biți într-un fișier cu standard handler și un fișier respins într-unul public-key. HotPDF normalizează asta când capturează configurația loaded. Înmulțește /Length al unui filtru /V2 cu opt doar când fișierul nu este criptat public-key, revine la /Length de la nivelul documentului când filtrul nu îl are pe al său și stochează rezultatul în THPDFCryptFilterInfo.KeyLengthBits. AESV2 este fixat la 128 bits, iar AESV3 și AESV4 la 256, deoarece acele metode nu au key size negociabil. Partea strictă vine apoi: sunt acceptate doar /V2 de 40 și 128 bits. Un filtru care se rezolvă la orice altă lungime este raportat ca unavailable, iar operația eșuează, în loc să fie rotunjit la 128 sub teoria că majoritatea producătorilor au vrut oricum 128. Normalizarea silențioasă a key length-ului este modul în care livrezi un fișier care se decriptează pe mașina ta și nicăieri altundeva

var
  Reader: THotPDF;
  Info: THPDFCryptFilterInfo;
  I: Integer;
begin
  Reader := THotPDF.Create(nil);
  try
    Reader.AutoLaunch := False;
    if Reader.LoadFromFile('incoming.pdf', 'open-secret') <> 1 then
      Exit;
    // /StrF și /StmF au default Identity; /EFF are default /StmF
    WriteLn(Reader.LoadedStringCryptFilterName);        // StdCF
    WriteLn(Reader.LoadedStreamCryptFilterName);        // Identity
    WriteLn(Reader.LoadedEmbeddedFileCryptFilterName);  // StdCF
    for I := 0 to Reader.GetLoadedCryptFilterCount - 1 do
      if Reader.GetLoadedCryptFilterInfo(I, Info) then
        if (Info.Method = hcfmV2) and
           not (Info.KeyLengthBits in [40, 128]) then
          raise Exception.CreateFmt(
            'crypt filter /%s: unsupported V2 key length %d',
            [String(Info.Name), Info.KeyLengthBits]);
  finally
    Reader.Free;
  end;
end;

Ce garantează /CFM /None și prin ce diferă /Identity?

Ajung la același rezultat pe rute diferite, iar confundarea lor strică lookup-urile. Un filtru numit al cărui /CFM este /None și un filtru numit care omite complet /CFM înseamnă ambele că filtrul nu face criptare sau decriptare — HotPDF mapează entry-ul lipsă la None înainte de rezolvare, așa că ambele ajung la hcfmNone cu key length înregistrat ca zero. /Identity este diferit ca natură: este numele rezervat care ocolește complet lookup-ul în /CF, așa că un document poate referi /Identity fără să îl definească nicăieri în /CF. PDF names sunt case-sensitive, ceea ce face nenegociabil un detaliu de implementare: niciun crypt filter lookup nu poate fi case-insensitive. HotPDF rezolvă numele sub-dictionary-ului /CF, entry-ul filter /Length și verificarea stream /Type prin lookup-uri case-sensitive. Un fișier care definește /stdcf în timp ce /StmF indică /StdCF este malformed, iar tratarea lor ca aceeași cheie ar transforma o eroare de authoring detectabilă într-o cheie greșită aplicată silențios fiecărui stream din document

Cum faci ca /EFF să se aplice pe stream-urile embedded-file

Când /EFF diferă de /StmF, stream-ul embedded-file are nevoie de un entry /Crypt explicit la începutul lui /Filter și de un dictionary /DecodeParms corespunzător, cu /Name la aceeași poziție în array. HotPDF calculează asta per stream la save: detectează /Type /EmbeddedFile, moștenește filtrul embedded-file configurat și emite marker-ul explicit /Crypt numai când numele moștenit diferă de default-ul efectiv al stream-ului. Când /EFF și /StmF coincid, nu este scris niciun marker, deoarece reader-ul ar rezolva oricum același filtru. Poziția în array contează atunci la fel de mult ca numele. Când HotPDF citește stream-ul înapoi, caută entry-ul /Crypt în /Filter, reține indexul și caută apoi în același index din array-ul /DecodeParms pentru a găsi /Name. Un /Crypt la index 0 asociat cu parameters la index 1 se rezolvă la /Identity, nu la filtrul tău. Acesta este și motivul pentru care writer-ul completează array-ul de parameters cu un null când stream-ul avea anterior /Filter, dar nu și /DecodeParms: pozițiile trebuie să rămână aliniate

Sub aceasta există o capcană și mai ascuțită. Dacă /Filter sau /DecodeParms existent este un indirect object — frecvent în fișiere generate de producători care partajează un filter array între multe stream-uri — inserarea lui /Crypt in-place ar muta un filter graph partajat și ar corupe fiecare alt stream care îl indică. HotPDF rezolvă indirect object-ul și îl clonează mai întâi într-un direct object privat stream-ului, ștergând object și generation numbers astfel încât rădăcina indirectă originală să nu fie niciodată embedded în array-ul nou. Pentru un stream care folosea deja ASCIIHexDecode, rezultatul serializat este /Filter [ /Crypt /ASCIIHexDecode ] cu /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ]. Aceeași disciplină pozițională guvernează orice alt filter chain, inclusiv cele parcurse când extragi imagini dintr-un PDF încărcat prin filtrele lor de decode

// Editor deține deja un document loaded, iar ContentStream este un
// THPDFStreamObject al cărui /Filter este un nume /ASCIIHexDecode indirect
Editor.OwnerPassword := 'owner-secret';
Editor.UserPassword := 'open-secret';
Editor.CryptKeyLength := aes128;
Editor.ConfigureCryptFilterDefaults('StdCF', 'Identity');
Editor.SetStreamCryptFilter(ContentStream, 'StdCF');
Editor.ActivateProtection := True;
Editor.SaveLoadedDocument('out.pdf');

// Un nume gol șterge override-ul și elimină /Crypt stale
// împreună cu decode parameters la următorul save
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');

Moștenesc object streams politica /Encrypt a documentului?

Nu, iar presupunerea că o fac este o cale sigură către garbage. Un object stream trebuie să urmeze politica efectivă /StmF sau propriul marker /Crypt explicit: simpla prezență a unui dictionary /Encrypt nu transformă fiecare container /ObjStm în ciphertext. Un document cu /StmF /Identity are object streams plaintext chiar dacă string-urile sale sunt complet criptate, iar un decoder care le decriptează oricum transmite către etapa inflate input care nu a fost niciodată output deflate

Consecința pentru member objects este partea care merită citită de două ori. Conform ISO 32000-1 §7.5.7, string-urile dintr-un object stream criptat sunt deja plaintext după ce container-ul însuși este decriptat, așa că decriptarea lor din nou ar fi un double-decrypt. HotPDF previne asta întrebând dacă fiecare container al unui object de tip 2 a fost criptat și sărind peste object când răspunsul este da, numărând skip-urile în XRefProbeDecryptObjStmSkips ca dovadă directă că guard-ul a funcționat. Când container-ul era plaintext, string-urile member nu erau acoperite de nimic, așa că HotPDF materializează acei members și aplică /StrF fiecăruia individual — keyed, așa cum o face efectiv implementarea, după object number și generation ale member-ului, nu după object number al /ObjStm-ului care îl conține. Inversează asta pe un fișier cu politică mixtă și fiecare string din fiecare object comprimat se decodează în noise. Regulile la nivel de container sunt acoperite mai departe în notițele despre object streams PDF și incremental updates

Unde refuză HotPDF să ghicească

Semantica crypt filter nu există sub /V 4, așa că HotPDF respinge orice override per stream pe un asemenea fișier cu o eroare explicită, în loc să scrie un marker /Crypt pe care niciun reader conform nu l-ar respecta. Același lucru este valabil la read: un encryption dictionary cu /V sub 4 golește toate cele trei nume de filtre încărcate, deoarece nu există nimic de raportat. Alte trei limite sunt impuse deliberat:

  • Un filtru per stream diferit de Identity pe un document criptat cu public key este refuzat, deoarece o politică specifică stream-ului sub public-key handler are nevoie de un recipient envelope specific stream-ului, pe care HotPDF nu îl emite încă
  • Fișierele embedded criptate cu public key al căror /EFF diferă de /StmF efectiv sunt refuzate din același motiv, nu scrise într-o formă pe care nimeni nu o poate decripta
  • Fast path-ul direct-file AES-256 se aplică doar când string-urile, stream-urile și fișierele embedded se rezolvă toate la aceeași metodă de crypt filter și niciun object din fișier nu poartă un /Crypt explicit; o politică mixtă sau metadata plaintext forțează fallback-ul la ruta completă de object graph

Niciuna dintre acestea nu este o decizie de performanță. Ele marchează locurile în care o presupunere greșită produce un PDF care se deschide într-un viewer, eșuează în altul și nu îi dă dezvoltatorului niciun semnal până când un client nu raportează problema. Un refuz la ConfigureCryptFilterDefaults sau la save costă o excepție; un embedded file cu key greșit în tăcere costă un support cycle. Dacă construiești software Delphi sau C++Builder care produce sau consumă PDF criptat — content de pagină selectiv plaintext cu attachment-uri criptate, payload wrappers criptate PDF 2.0 sau interop cu fișiere ale căror politici crypt filter nu le-ai ales — API-ul crypt filter descris aici este livrat în componenta PDF HotPDF pentru Delphi, alături de rutele de criptare, object streams și incremental updates pe care se bazează