Articol tehnic

Auditarea criptării și permisiunilor PDF în Delphi cu PDF Library for Delphi

Un indicator de permisiune nu este un mecanism de securitate. Bitul care spune „fără copiere” stă în același dicționar /Encrypt ca și criptografia, ceea ce îi dă un aer de constrângere pe care nu îl are, iar în clipa în care tratezi cele două ca pe un singur lucru auditul tău începe să dea răspunsuri greșite. Singura întrebare care merită pusă despre un PDF nu este „este criptat?”. Este una mai precisă și mai grea: care algoritm, care revizie a handler-ului de securitate, care dintre cele două parole a fost setată, care biți de permisiune sunt revendicați și ce părți din fișier atinge criptarea cu adevărat. Un fișier poate fi criptat formal și deschis practic. Poate refuza să fie citit și totuși să-și lase metadatele în clar. Poate bloca tipărirea printr-un indicator pe care orice vizualizator este liber să-l ignore. Auditarea unui PDF înseamnă lămurirea separată a tuturor acestor aspecte, iar PDF Library for Delphi, motorul PDF de la losLab pentru Delphi și C++Builder, le expune pe fiecare atât printr-un API plat cu handle-uri întregi, cât și printr-un strat de clase tipizate

Ce înregistrează de fapt dicționarul /Encrypt

ISO 32000-1 §7.6 definește securitatea documentului prin câteva intrări de dicționar, iar PDF Library for Delphi le oglindește una câte una în înregistrarea TPDFEncryption. Versiunea de filtru V și revizia R aleg familia de algoritmi. Length poartă dimensiunea cheii. Biții de permisiune stau în P, șirurile de validare a parolelor de proprietar și de utilizator în O și U (cu OE și UE adăugate pentru AES-256), un indicator EncryptMetadata le însoțește, iar alte trei câmpuri numesc filtrele de criptare aplicate șirurilor, fluxurilor și, respectiv, fișierelor încorporate

Valoarea acestei înregistrări stă în faptul că nu interpretează nimic în locul tău. Îți dă înapoi dicționarul brut și te lasă pe tine să tragi concluziile, exact ce îi trebuie unui audit. Cazul „text în clar în interiorul unui fișier criptat” se vede în StringFilterIdentity și StreamFilterIdentity: când oricare dintre ele este adevărat, datele corespunzătoare trec neatinse prin filtrul Identity, indiferent ce raportează starea de criptare a documentului. Un scaner care se oprește la „există un dicționar /Encrypt” va declara protejat un astfel de fișier, deși șirurile și fluxurile lui stau la vedere. Aceeași nuanță guvernează metadatele. Când EncryptMetadata este fals, pachetul XMP rămâne lizibil pentru orice indexator, în timp ce conținutul paginilor nu, ceea ce merită știut în clipa în care regulile tale de rutare se bazează pe un câmp de titlu sau de autor

Diagramă PDF Library for Delphi a câmpurilor dicționarului PDF /Encrypt mapate la proprietățile de audit TPDFEncryption, inclusiv capcanele filtrului criptografic Identity
Fiecare intrare /Encrypt se mapează pe un câmp TPDFEncryption, iar indicatoarele filtrului Identity dezvăluie ce șiruri, stream-uri sau metadate rămân lizibile indiferent de starea de criptare

O sondă scurtă de securitate cu API-ul plat

Pentru majoritatea fluxurilor, patru apeluri plate răspund la întrebările de zi cu zi. LoadFromFile returnează 1 în caz de succes, iar odată ce documentul este deschis, inspectorii de criptare raportează în raport cu starea lui decriptată:

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('contract.pdf', UserPassword) <> 1 then
      raise Exception.Create('Open failed: wrong password or damaged file');
    Writeln('status    : ', PDF.EncryptionStatus);     // decriptat / criptat / necunoscut
    Writeln('algorithm : ', PDF.EncryptionAlgorithm);  // familia RC4 vs AES
    Writeln('strength  : ', PDF.EncryptionStrength);   // clasa de lungime a cheii
    Writeln('owner pw? : ', PDF.CheckPassword(CandidatePassword));
  finally
    PDF.Free;
  end;
end;

CheckPassword contează mai mult decât lasă să se vadă semnătura lui de o singură linie. PDF definește două parole cu puteri inegale. Parola de utilizator este necesară pentru a deschide fișierul în general. Parola de proprietar acordă drepturi depline și trece peste orice bit de permisiune. Octeții de pe disc sunt identici în ambele cazuri, dar o sesiune deschisă cu parola de proprietar poate face lucruri pe care o sesiune cu parolă de utilizator nu le poate face, așa că un audit care nu consemnează ce credențial a fost prezentat consemnează doar jumătate de adevăr. Stratul de clase face distincția interogabilă. TPDFDocument.HasUserPassword și HasOwnerPassword raportează ce cere fișierul, în timp ce IsUserPassword și IsOwnerPassword raportează ce parolă a deschis efectiv sesiunea curentă. Consemnează faptul acesta. Nu consemna niciodată valorile parolelor

Scara Strength, unde „AES-256” înseamnă două lucruri

Funcțiile plate Encrypt și EncryptFile primesc un Strength întreg cu cinci valori cu sens: 0 pentru RC4 pe 40 de biți, 1 pentru RC4 pe 128 de biți, 2 pentru AES pe 128 de biți, lizibil începând cu Acrobat 7, 3 pentru AES pe 256 de biți așa cum a fost introdus cu Acrobat 9 și 4 pentru AES pe 256 de biți așa cum cere Acrobat X și versiunile ulterioare

Partea interesantă este că 3 și 4 sunt amândouă etichetate AES-256 și nu sunt aceeași schemă. Strength 3 corespunde reviziei 5 a handler-ului de securitate, un proiect intermediar pe care Acrobat 9 l-a livrat, iar ISO nu l-a adoptat niciodată. Strength 4 corespunde reviziei 6, a cărei funcție de derivare a cheii a fost întărită și standardizată în ISO 32000-2. Pentru un document pe care îl creezi astăzi nu există niciun motiv să alegi 3 în locul lui 4. Pentru un audit, diferența este decisivă: o politică formulată ca „AES-256 conform ISO 32000-2” este îndeplinită doar de R6, iar un fișier R5 care se autointitulează AES-256 pică politica aceea, deși trece o verificare naivă de putere. Stratul de clase le ține separate prin nume, esAES256Bit pentru R5 față de esAES256BitAcroX pentru R6, iar proprietatea EncryptionAcroX răspunde la întrebarea despre revizie cu un singur boolean

Scara de rezistență a criptării PDF, de la RC4 pe 40 de biți la AES-256 revizia 5 versus revizia 6, pentru audituri Delphi
Nivelurile 3 și 4 se numesc ambele AES-256, dar doar revizia 6 satisface o politică ISO 32000-2, deci auditurile trebuie să înregistreze revizia handler-ului, nu doar eticheta

Biții de permisiune și clauzele mărunte despre lungimea cheii

EncodePermissions împachetează opt indicatori în întregul pe care îl așteaptă Encrypt și EncryptFile. Tipărirea, copierea, modificarea și adăugarea de note alcătuiesc setul de bază; completarea câmpurilor, copierea pentru accesibilitate, asamblarea și tipărirea la calitate maximă alcătuiesc setul extins. Clauza măruntă, pe care demonstrația de criptare a bibliotecii o spune pe față, este că cele patru din setul extins au efect doar de la 128 de biți în sus. Indicatorul de tipărire la calitate maximă cade sub aceeași regulă: șterge-l ca să forțezi tipărirea la rezoluție joasă și un document pe 40 de biți te va ignora, fiindcă și declasarea aceea cere criptare pe 128 de biți sau mai puternică. Codifică o politică de tipul „doar tipărire la rezoluție joasă” într-un fișier pe 40 de biți și orice vizualizator va tipări oricum la calitate maximă

Întrebarea mai adâncă este cine impune vreunul dintre acești biți, iar răspunsul este: nimeni în care să te poți încrede. Permisiunile sunt instrucțiuni pentru cititoarele conforme, nu restricții criptografice. Cheia de decriptare este identică indiferent dacă copierea este permisă sau refuzată, așa că un set de permisiuni blocat nu face decât să țină cinstite vizualizatoarele cinstite. Un cititor care alege să ignore biții nu întâmpină niciun obstacol criptografic. Dacă obligația este să împiedici extragerea, nu doar să o descurajezi, fișierul are nevoie de o parolă de utilizator, iar fluxul de lucru are nevoie de controale la nivel de proces în jurul ei, iar un raport de audit ar trebui să numească sub care dintre cele două regimuri se află de fapt fiecare fișier, în loc să trateze un indicator de permisiune drept lacăt

Stabilirea politicii și dovada că a prins

Aplicarea criptării pe fișiere existente nu cere încărcarea lor în arborele de obiecte. EncryptFile prelucrează intrarea în ieșire printr-un singur apel, iar bucla de audit redeschide rezultatul ca să confirme ce a ajuns pe disc. Demonstrația de criptare livrată cu biblioteca urmează aceeași formă de scriere urmată de recitire:

var
  PDF: TPDFlib;
  R: Integer;
begin
  PDF := TPDFlib.Create;
  try
    R := PDF.EncryptFile('in.pdf', 'out.pdf', 'owner-secret', 'user-secret', 4,
      PDF.EncodePermissions(1, 0, 0, 0,    // tipărire permisă; copiere/modificare/note refuzate
                            0, 0, 0, 1));  // set extins: doar tipărire la calitate maximă
    if (R = 1) and (PDF.LoadFromFile('out.pdf', 'user-secret') = 1) then
    begin
      Writeln('algorithm = ', PDF.EncryptionAlgorithm);
      Writeln('strength  = ', PDF.EncryptionStrength);
      Writeln('owner pw accepted: ', PDF.CheckPassword('owner-secret'));
    end;
  finally
    PDF.Free;
  end;
end;

Echipele care lucrează la nivelul documentului primesc aceeași operație cu mulțimi tipizate în locul împachetării de biți, ceea ce trece prin code review cu mult mai puțină mijire a ochilor:

if not Doc.Encrypt('owner-secret', 'user-secret', esAES256BitAcroX,
  [ppCanPrint], [ppCanPrintFull]) then
  raise Exception.Create('Encryption failed');

Oricum ai proceda, pasul de recitire nu este o ceremonie opțională. El prinde greșelile de implementare care altfel ies la iveală peste luni de zile pe mașina unui client: o versiune veche de bibliotecă ce declasează tăcut puterea cerută, o cale de ieșire care nu a fost niciodată scrisă fiindcă directorul era doar în citire, un întreg de permisiuni ale cărui argumente au intrat în ordine greșită. Toate trei trec un smoke test local și cad pe teren, iar redeschiderea ieșirii transformă fiecare dintre ele într-o excepție pe care o vezi chiar în timpul rulării care a creat fișierul. GetEncryptionFingerprint returnează o valoare compactă pe care o poți stoca odată cu înregistrarea lucrării, astfel încât o comparație ulterioară să poată spune dacă două ieșiri au aceeași configurație de criptare, fără să redeschidă vreuna dintre ele

Fals pozitive de audit pentru care merită să scrii cod

Câteva tipare împing constant scanerele de securitate spre concluzia greșită, iar fiecare vine din reducerea unei întrebări cu mai multe părți la un răspuns de tip da sau nu. Filtrul de criptare Identity este exemplul cel mai curat. Dicționarul /Encrypt există, fișierul se raportează drept criptat și totuși șirurile și fluxurile trec neschimbate prin filtrul Identity, așa că adevăratul conținut este text în clar. Citirea StringFilterIdentity și StreamFilterIdentity înainte de a declara ceva protejat este soluția

Separarea metadatelor este mai subtilă. EncryptMetadata poate fi în dezacord cu restul documentului în ambele sensuri, lăsând un fișier criptat cu un pachet XMP lizibil sau, mai rar, invers. „Fișierul este criptat” nu spune nimic despre metadatele lui, ceea ce contează în clipa în care un indexator sau o regulă de rutare întinde mâna după titlu. Fișierele încorporate adaugă o a treia axă: PDF permite un filtru de criptare dedicat doar atașamentelor, deci atașamentele pot fi singura parte criptată dintr-un document altfel deschis sau singura parte în clar dintr-unul criptat. Captează cele trei atribuiri de filtre ca pe câmpuri separate pentru șiruri, fluxuri și fișiere încorporate și niciuna dintre aceste capcane nu te mai poate prinde. Stochează un singur boolean și decizia greșită este doar o chestiune de timp

PDF Library for Delphi: flux de audit care verifică StringFilterIdentity, StreamFilterIdentity, EncryptMetadata și filtrul criptografic al fișierelor încorporate înainte de a declara un PDF protejat
Patru axe independente decid dacă un fișier cu aspect criptat este cu adevărat sigilat, iar comprimarea lor într-un singur boolean clasifică în cele din urmă greșit un fișier

Eliminarea criptării și alegerea ei pentru fișiere noi

Un audit se termină adesea cu decizia de a elimina protecția, iar mecanica nu este obstacolul acolo. DecryptFile(InputFileName, OutputFileName, Password) scrie o copie decriptată fără o încărcare completă, iar Decrypt pe documentul încărcat face același lucru în memorie, odată ce fișierul este deja deschis. Ambele cer o parolă validă; niciuna nu ocolește criptografia. Adevărata poartă ține de politică, nu de cod, așa că fă-ți regulile de recepție să spună limpede când este permisă eliminarea și consemnează clasa de parolă care a autorizat-o, fiindcă pasul tehnic în sine nu lasă nicio urmă

Alegerea pentru ieșirile noi este mai îngustă decât lasă să se creadă cele cinci valori Strength. Folosește Strength 4, AES-256 revizia 6, în afară de cazul în care trebuie să deschizi fișiere în vizualizatoare mai vechi decât Acrobat X. Strength 2, AES-128, este pragul pragmatic pentru un parc îmbătrânit de vizualizatoare care nu poate fi actualizat. Opțiunile RC4 de la 0 și 1 există ca să poți citi și audita arhive istorice, nu ca să produci ceva nou cu ele; să apelezi la ele într-un proiect din 2026 este semn că o cerință din amonte a rămas în urmă

Starea criptării alimentează direct deciziile de semnare, fiindcă un banc de lucru care validează și semnează documente are nevoie de aceeași disciplină de recitire pe care se sprijină acest audit. Terenul acela este acoperit în articolul despre bancul de lucru pentru conformitate și semnare. Când un lot aplică EncryptFile pe mii de documente mari, ghidul de acces direct pentru PDF-uri mari arată cum să menții memoria plată în timp ce rulează. Referința completă a API-ului de criptare se află pe pagina de produs PDF Library for Delphi