Odborný článok

Otvorte šifrované PDF v Delphi pomocou surového kľúča

PDF Library for Delphi dokáže otvoriť šifrované PDF zo surového file encryption key namiesto hesla. DAOpenFileWithEncryptionKey prijíma kľúč ako hexadecimálny text, overí ho voči verifieru, ktorý je už uložený v encryption dictionary, a vráti Direct Access handle iba na čítanie; DAOpenFromStreamWithEncryptionKey robí to isté pre TStream vlastnený volajúcim. Obe metódy pribudli vo verzii v3.496.0

Scenár je úzky, ale skutočný. Forenzné vyšetrovanie vám odovzdá kľúč obnovený z memory image a žiadne heslo. Hromadná archivácia má desaťtisíc dokumentov, ktorých file keys ležia v escrow databáze, pretože pôvodný DRM systém prestal pred rokmi vydávať heslá. Migrácia z vyradeného rights-management produktu má key material a nič iné. Vo všetkých týchto prípadoch je credential, ktorý držíte, výsledkom derivácie kľúča, nie jej vstupom, a žiadny parameter pre heslo v API ho neprijme

Prečo file encryption key nie je heslo?

Heslo a file encryption key stoja na opačných stranách derivácie kľúča v PDF standard security handleri (ISO 32000-1 §7.6.3). Handler vezme heslo, zmieša ho s /O, /P, file ID a hashom závislým od revision a vytvorí file key. Ak vložíte file key do slotu pre heslo, dostanete nezmysel zahashovaný do iného nezmyslu, preto to potrebuje vlastný vstupný bod namiesto príznaku na DAOpenFile

Miesto, kam možno surový kľúč vložiť, určuje revision. Revisions 2 až 4 stále odvodzujú odlišný per-object key z file key, čísla objektu, čísla generácie a pri AESV2 aj AES salt, takže držanie file key vôbec neumožňuje preskočiť deriváciu na úrovni objektu. Revisions 5 až 7 používajú 32-bajtový file key priamo pre AES-256, bez kroku pre každý objekt. Jedinou spoločnou vrstvou je samotný file key, a práve tam PDF Library for Delphi prijíma externe dodaný kľúč. Ak heslo stále máte, zostaňte radšej na bežnej ceste a nechajte password retry lifecycle spracovať prvý chybný pokus, pretože raw-key vstup zámerne obetuje niekoľko pohodlných vlastností, ktoré password path zachováva

Aký vstup prijíma DAOpenFileWithEncryptionKey?

Iba ASCII hex bez prefixu a medzier, s párnym počtom znakov, pričom počet dekódovaných bajtov sa musí presne zhodovať s encryption revision. Pri revisions 2 až 4 pochádza očakávaná dĺžka z /Length v encryption dictionary: ide o násobok 8 bitov medzi 5 a 16 bajtmi, pričom pri chýbajúcom /Length sa použije 40 bitov. Pri revisions 5 až 7 je to presne 32 bajtov bez vyjednávania. Prefix 0x, nepárny počet číslic, viac než 64 hexadecimálnych znakov, neznámy bit v Options alebo dokument, ktorý vôbec nie je šifrovaný, všetko vedie k rovnakému výsledku: handle 0 a LastErrorCode nastavený na PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, čo je 425. Prísnosť je zámerná. Zhovievavý parser, ktorý odstrihne medzery a krátky vstup doplní nulami, ochotne premení skrátené vloženie zo schránky na credential a potom zlyhá niekde, kde sa to vysvetľuje oveľa ťažšie

Var
  Lib: TPDFlib;
  FileHandle, PageRef: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    // KeyHex má 32 hexadecimálnych znakov pre AES-128 R4 a 64 pre 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;

Ostatné chybové kódy zostávajú odlíšené, aby batch job vedel rozlíšiť chybu operátora od problému s dôkazom: 411, keď súbor neexistuje, 401, keď ho nemožno otvoriť na čítanie, 409, keď je poškodená cross-reference štruktúra. Všetko súvisiace s kľúčom sa zámerne zlieva do 425, pretože raw-key vstupný bod, ktorý oznámi, ktorá časť kľúča bola nesprávna, je oracle

Čo overenie v skutočnosti dokazuje?

PDF Library for Delphi dokazuje, že dodaný kľúč patrí k tomuto dokumentu, pomocou verifiera, ktorý už nesie encryption dictionary, pričom kontrola sa líši podľa revision. Revision 2 nanovo vypočíta RC4 šifrovanie 32-bajtového štandardného padding stringu a porovná všetkých 32 bajtov s /U. Revisions 3 a 4 zahashujú padding spolu s file ID, spustia RC4 pass a 19 kôl odvodených cez XOR a porovnajú prvých 16 bajtov /U. Revisions 5 až 7 dešifrujú 16-bajtový string /Perms s nulovým IV a naraz overia štyri nezávislé veci: little-endian permission word voči /P, štyri bajty FF na pozíciách 5 až 8, príznak encrypt-metadata ako T alebo F a marker adb na pozíciách 10 až 12

Keď verifier existuje, ale nesúhlasí, otvorenie sa bezpodmienečne odmietne. To treba povedať priamo, pretože na tejto garancii stojí celá funkcia. Všimnite si tiež, čo overenie nie je: hovorí, že kľúč dešifruje tento súbor, nie že vám niekto povolil jeho použitie. Permission word obnovený z /Perms je dôkazom o kľúči, nie povolením, a ak chcete vedieť, čo dokument skutočne tvrdí, že povoľuje, je to samostatná úloha pre audit šifrovania a oprávnení. Problémy s normalizáciou na strane hesla, napríklad SASLprep pri non-ASCII AES-256 heslách, tu jednoducho nevzniknú, pretože žiadny string sa nedostane do hashu

Kedy sa uplatní PDF_RAW_KEY_ALLOW_UNVERIFIED?

PDF_RAW_KEY_ALLOW_UNVERIFIED pokrýva presne jednu situáciu: dokument nemá použiteľný verifier, pretože /Perms chýba alebo nemá 16 bajtov, prípadne je /U príliš krátke na porovnanie. Nedokáže obísť dôkaz, ktorý zlyháva. Poškoďte jednu hexadecimálnu číslicu v /Perms, odovzdajte správny kľúč s nastavenou voľbou a PDF Library for Delphi aj tak vráti 0 a 425. Odovzdajte kľúč pozostávajúci z 32 nulových bajtov voči neporušenému verifieru s nastavenou voľbou a odpoveď bude rovnaká. Voľba uvoľňuje absenciu dôkazu, nikdy nie rozpor s ním. Keďže recovery open a verified open sú odlišné epistemické stavy, hlásia sa oddelene namiesto zliatia do návratovej hodnoty a DAGetEncryptionKeyValidation prijíma otvorený handle a odpovedá jednou z troch konštánt:

  • PDF_RAW_KEY_VALIDATION_VERIFIED (1) znamená, že verifier existoval a zhodoval sa
  • PDF_RAW_KEY_VALIDATION_UNVERIFIED (2) znamená, že kľúč bol prijatý iba preto, že nebolo možné vyhodnotiť žiadny verifier a volajúci si túto politiku výslovne vyžiadal
  • PDF_RAW_KEY_VALIDATION_NONE (0) hlási obyčajný handle otvorený heslom
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
   (Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
  // v tomto súbore nie je verifier? zopakuj pod explicitnou recovery policy
  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;

Iba na čítanie už z konštrukcie a kto vlastní stream

File entry s raw key vždy otvorí zdroj s fmOpenRead or fmShareDenyWrite a celý Direct Access chain označí iba na čítanie, takže DAAppendFile odmietne zápis na mieste a vráti 2 namiesto pokusu o inkrementálnu aktualizáciu. Nie je to policy, o ktorej by ste mohli handle presvedčiť; nastaví sa v konštruktore ešte pred samotným parsovaním súboru. Pri práci s dôkazmi chcete vlastnosť, že zdrojové bajty zostanú po zatvorení handle byte-identické, a regresná suite presne toto overuje na fixture AES-128 revision 4 aj AES-256 revision 6. Stream entry sa pri zápisoch správa rovnako a pridáva ešte jedno pravidlo: DAOpenFromStreamWithEncryptionKey nikdy nepreberá ownership, takže DACloseFile nechá váš TStream živý a uvoľníte ho sami

Source:= TMemoryStream.Create;
Try
  Source.LoadFromFile(ArchivePath);
  Source.Position:= 0;
  FileHandle:= Lib.DAOpenFromStreamWithEncryptionKey(Source, KeyHex);
  If FileHandle<> 0 Then
  Try
    // 2 = handle iba na čítanie: exportuj inde, do dôkazu nikdy nepridávaj
    Assert(Lib.DAAppendFile(FileHandle)= 2);
    Harvest(Lib, FileHandle);
  Finally
    Lib.DACloseFile(FileHandle);
  End;
Finally
  Source.Free;   // handle tento stream nikdy nevlastnil
End;

Hygiena kľúčov a čo exportuje DLL

Zatvorenie chainu prepíše file key, cache hesla aj odvodené object keys a dekódovaný kľúč sa vynuluje v bloku Finally samotného vstupného bodu bez ohľadu na to, či otvorenie uspelo. Za tým je detail, ktorý stál skutočný čas pri debugovaní: každá kópia, ktorá musí prežiť, sa klonuje explicitne cez SetLength a Move, nie priradením. Keď v Delphi priradíte jeden AnsiString druhému, oba názvy zdieľajú jeden buffer v režime copy-on-write, takže vynulovanie strany volajúceho by vynulovalo kľúč, ktorý crypt handler stále používa, a dokument by sa dešifroval na odpad z dôvodov, ktoré by nevysvetlil žiadny stack trace. Hranicu DLL prekračujú iba file-based entry points vo wide aj ANSI formách spolu s accessorom validačného stavu; varianta TStream zostáva iba v Delphi, pretože závisí od životnosti objektu Delphi a referenčnej sémantiky, ktorá nemá poctivé vyjadrenie v plochom C ABI. Ak je váš recovery tooling klientom DLL, naplánujte staging do dočasného súboru a jeho odstránenie pod rovnakými kontrolami, aké používate na kľúč

S hexadecimálnym kľúčom zaobchádzajte ako s credential material a použite pravidlá manipulácie, ktoré by ste dali heslu dokumentu, pričom validačný stav uchovajte v logu, ktorý vytvára chain of custody, aby neskorší čitateľ rozoznal overenú extrakciu od neoverenej. Ak hodnotíte Delphi PDF komponent pre forenzné vyšetrovanie, hromadnú archiváciu alebo migráciu DRM, raw-key vstupné body, garancia iba na čítanie aj auditná plocha šifrovania patria do tej istej knižnice a úplný zoznam funkcií nájdete na produktovej stránke PDF Library for Delphi