Articol tehnic

Deschiderea PDF-urilor criptate cu o cheie raw în Delphi

PDF Library for Delphi poate deschide un PDF criptat din cheia raw de criptare a fișierului, în loc de o parolă. DAOpenFileWithEncryptionKey acceptă cheia ca text hexadecimal, o verifică prin verifier-ul deja stocat în encryption dictionary și returnează un Direct Access handle read-only; DAOpenFromStreamWithEncryptionKey face același lucru pentru un TStream deținut de caller. Ambele au apărut în v3.496.0

Scenariul este îngust și real. O misiune de forensics îți dă o cheie recuperată dintr-o imagine de memorie și nicio parolă. O rulare de arhivare în masă are zece mii de documente ale căror file keys stau într-o bază de escrow, deoarece sistemul DRM de origine nu mai emite parole de ani de zile. O migrare de la un produs retired de rights management are materialul de cheie și nimic altceva. În toate aceste cazuri credential-ul pe care îl deții este rezultatul derivării cheii, nu input-ul, iar niciun parameter de parolă din API nu îl poate primi

De ce nu este file encryption key o parolă?

O parolă și un file encryption key se află pe părți opuse ale key derivation în standardul PDF security handler (ISO 32000-1 §7.6.3). Handler-ul ia parola, o combină cu /O, /P, file ID și un hash specific reviziei și produce file key-ul. Dacă introduci un file key în slotul de parolă, obții un nonsens hash-uit într-un alt nonsens, de aceea este nevoie de un entry point separat, nu de un flag pe DAOpenFile

Locul în care poate fi injectată cheia raw este fixat de revision. Revisions 2 până la 4 derivă în continuare o cheie distinctă per object din file key, object number, generation number și, pentru AESV2, salt-ul AES, deci deținerea file key-ului nu îți permite deloc să sari peste derivarea la nivel de object. Revisions 5 până la 7 folosesc direct cheia de fișier de 32 bytes pentru AES-256, fără pas per object. Singurul layer comun este file key-ul, deci acolo este singurul loc în care PDF Library for Delphi acceptă o cheie furnizată extern. Dacă ai totuși parola, rămâi pe ruta obișnuită și lasă password retry lifecycle să trateze prima încercare greșită, deoarece entry point-ul raw-key renunță deliberat la mai multe facilități păstrate de ruta cu parolă

Ce input acceptă DAOpenFileWithEncryptionKey?

Doar hex ASCII fără prefix și fără whitespace, cu lungime pară, iar numărul de bytes decodați trebuie să corespundă exact reviziei de criptare. Pentru revisions 2 până la 4, lungimea așteptată vine din /Length din encryption dictionary: un multiplu de 8 biți între 5 și 16 bytes, cu default de 40 bits când /Length lipsește. Pentru revisions 5 până la 7 sunt exact 32 de bytes, fără negociere. Un prefix 0x, un număr impar de cifre, peste 64 de caractere hex, un bit necunoscut în Options sau un document care nu este criptat produc toate același rezultat: handle 0 și LastErrorCode setat la PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, adică 425. Strictness-ul este intenționat. Un parser permisiv care taie whitespace și completează cu zero input-ul scurt va transforma cu plăcere un paste trunchiat din clipboard într-un credential și apoi va eșua într-un loc mult mai greu de înțeles

Var
  Lib: TPDFlib;
  FileHandle, PageRef: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    // KeyHex are 32 de caractere hex pentru AES-128 R4, 64 pentru AES-256 R6
    FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
    If FileHandle= 0 Then
      Raise Exception.CreateFmt('raw key refused (error %d)', [Lib.LastErrorCode]);
    Try
      PageRef:= Lib.DAFindPage(FileHandle, 1);
      WriteLn(Lib.DAExtractPageText(FileHandle, PageRef, 0));
    Finally
      Lib.DACloseFile(FileHandle);
    End;
  Finally
    Lib.Free;
  End;
End;

Celelalte coduri de failure rămân distincte pentru ca un batch job să poată separa eroarea operatorului de problemele din evidence: 411 când fișierul nu există, 401 când nu poate fi deschis pentru citire, 409 când structura cross-reference este stricată. Tot ce ține de cheie se reduce intenționat la 425, deoarece un entry point raw-key care spune ce parte a cheii era greșită este un oracle

Ce demonstrează de fapt verificarea?

PDF Library for Delphi demonstrează că cheia furnizată aparține acestui document, folosind verifier-ul pe care îl poartă deja encryption dictionary, iar verificarea diferă după revision. Revision 2 recalculează criptarea RC4 a stringului standard de padding de 32 de bytes și compară toți cei 32 de bytes cu /U. Revisions 3 și 4 hash-uiesc padding-ul împreună cu file ID, rulează pass-ul RC4 plus cele 19 runde derivate prin XOR și compară primii 16 bytes din /U. Revisions 5 până la 7 decriptează stringul /Perms de 16 bytes cu un IV zero și verifică simultan patru lucruri independente: permission word little-endian față de /P, cei patru bytes FF de la pozițiile 5 până la 8, flag-ul encrypt-metadata ca T sau F și marker-ul adb la pozițiile 10 până la 12

Când există un verifier, dar nu se potrivește, deschiderea este refuzată necondiționat. Merită spus direct, deoarece aceasta este garanția pe care se sprijină toată funcționalitatea. Observă și ce nu este verificarea: spune că cheia decriptează acest fișier, nu că cineva te-a autorizat să îl folosești. Permission word recuperat din /Perms este dovadă despre cheie, nu un grant, iar dacă vrei să știi ce pretinde efectiv documentul că permite, acesta este un job separat pentru un pass de audit al criptării și permisiunilor. Problemele de normalizare de pe partea parolei, precum SASLprep pentru parole AES-256 non-ASCII, pur și simplu nu apar aici, deoarece niciun string nu ajunge la un hash

Când se aplică PDF_RAW_KEY_ALLOW_UNVERIFIED?

PDF_RAW_KEY_ALLOW_UNVERIFIED acoperă exact o situație: documentul nu are un verifier utilizabil, deoarece /Perms lipsește sau nu are 16 bytes, ori /U este prea scurt pentru comparație. Nu poate suprascrie dovezi care eșuează. Corupe o singură cifră hex în /Perms și transmite cheia corectă cu opțiunea activată, iar PDF Library for Delphi returnează tot 0 și 425. Transmite o cheie de 32 de bytes zero împotriva unui verifier intact cu opțiunea activată, iar răspunsul este același. Opțiunea relaxează lipsa dovezii, niciodată contradicția ei. Deoarece o deschidere de recovery și una verificată sunt stări epistemice diferite, ele sunt raportate separat, nu înglobate în valoarea de retur, iar DAGetEncryptionKeyValidation primește handle-ul deschis și răspunde cu una dintre trei constante:

  • PDF_RAW_KEY_VALIDATION_VERIFIED (1) înseamnă că un verifier era prezent și s-a potrivit
  • PDF_RAW_KEY_VALIDATION_UNVERIFIED (2) înseamnă că cheia a fost acceptată doar deoarece niciun verifier nu putea fi evaluat și caller-ul a cerut explicit politica
  • PDF_RAW_KEY_VALIDATION_NONE (0) este ceea ce raportează un handle deschis în mod obișnuit cu parolă
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
   (Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
  // nu există verifier în acest fișier? reîncearcă sub o politică explicită de recovery
  FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex,
    PDF_RAW_KEY_ALLOW_UNVERIFIED);
If FileHandle<> 0 Then
Begin
  Case Lib.DAGetEncryptionKeyValidation(FileHandle) Of
    PDF_RAW_KEY_VALIDATION_VERIFIED:
      Chain.Note('key verified against the encryption dictionary');
    PDF_RAW_KEY_VALIDATION_UNVERIFIED:
      Chain.Note('no verifier available: extraction is unattested');
  End;
End;

Read-only prin construcție și cine deține stream-ul

Entry-ul de fișier raw-key deschide mereu sursa cu fmOpenRead or fmShareDenyWrite și marchează întregul lanț Direct Access read-only, așa că DAAppendFile refuză scrierea in-place și returnează 2 în loc să încerce un update incremental. Nu este o politică de care poți convinge handle-ul să se răzgândească; este setată în constructor înainte ca fișierul să fie măcar parsat. Pentru lucrul cu evidence, proprietatea importantă este că bytes din sursă rămân identici byte cu byte după închiderea handle-ului, iar regression suite verifică exact asta pe un fixture AES-128 revision 4 și pe unul AES-256 revision 6. Entry-ul pentru stream se comportă la fel la scrieri și adaugă încă o regulă: DAOpenFromStreamWithEncryptionKey nu preia niciodată ownership-ul, așa că DACloseFile îți lasă TStream-ul în viață și îl eliberezi tu

Source:= TMemoryStream.Create;
Try
  Source.LoadFromFile(ArchivePath);
  Source.Position:= 0;
  FileHandle:= Lib.DAOpenFromStreamWithEncryptionKey(Source, KeyHex);
  If FileHandle<> 0 Then
  Try
    // 2 = handle read-only: exportă în altă parte, nu atașa niciodată la evidence
    Assert(Lib.DAAppendFile(FileHandle)= 2);
    Harvest(Lib, FileHandle);
  Finally
    Lib.DACloseFile(FileHandle);
  End;
Finally
  Source.Free;   // handle-ul nu a deținut niciodată acest stream
End;

Igiena cheii și ce exportă DLL-ul

Închiderea lanțului suprascrie file key-ul, cache-ul parolei și object keys derivate, iar cheia decodată este ștearsă în blocul Finally al entry point-ului însuși, indiferent dacă deschiderea a reușit sau nu. Există o subtilitate aici care a consumat timp real de debugging: orice copie care trebuie să supraviețuiască este clonată explicit cu SetLength plus Move, nu atribuită. Dacă atribui un AnsiString altui AnsiString în Delphi, ambele nume împart același buffer prin copy-on-write, așa că ștergerea părții caller-ului ar pune la zero cheia pe care o folosește încă crypt handler-ul, iar documentul s-ar decripta în garbage din motive pe care niciun stack trace nu le-ar explica. Doar entry point-urile bazate pe fișier trec granița DLL-ului, în forme wide și ANSI, împreună cu accessor-ul pentru statusul validării; varianta TStream rămâne Delphi-only, deoarece depinde de lifetime-ul obiectelor Delphi și de semantica referințelor, care nu au o reprezentare onestă într-un C ABI plat. Dacă tooling-ul tău de recovery este client DLL, planifică staging într-un fișier temporar și ștergerea lui sub aceleași controale pe care le aplici cheii

Tratează cheia hex ca material de credential cu regulile de manipulare pe care le-ai aplica unei parole de document și păstrează statusul validării în log-ul produs de chain of custody, astfel încât un cititor ulterior să poată distinge o extragere verificată de una neatestată. Dacă evaluezi un component PDF Delphi pentru forensics, arhivare în masă sau migrare DRM, entry point-urile raw-key, garanția read-only și suprafața de audit a criptării fac toate parte din aceeași bibliotecă, iar lista completă de funcționalități este pe pagina de produs PDF Library for Delphi