Technický článek

Otevření šifrovaného PDF v Delphi pomocí surového klíče

PDF Library for Delphi dokáže otevřít šifrované PDF z jeho surového klíče šifrování souboru namísto hesla. DAOpenFileWithEncryptionKey přijímá klíč jako hexadecimální text, ověří ho proti verifieru, který už je uložen v encryption dictionary, a vrací handle Direct Access pouze pro čtení; DAOpenFromStreamWithEncryptionKey dělá totéž pro TStream vlastněný volajícím. Obě metody přibyly ve v3.496.0

Scénář je úzký, ale skutečný. Forenzní zakázka vám předá klíč získaný z paměťového obrazu a žádné heslo. Hromadná archivace má deset tisíc dokumentů, jejichž file keys leží v escrow databázi, protože původní DRM systém přestal před lety vydávat hesla. Migrace z vyřazeného systému správy práv má klíčový materiál a nic dalšího. V každém z těchto případů je credential, který držíte, výstupem odvození klíče, nikoli jeho vstupem, a žádný parametr hesla v API ho nepřijme

Proč není file encryption key heslem

Heslo a file encryption key leží na opačných stranách odvození klíče v PDF standard security handleru (ISO 32000-1 §7.6.3). Handler vezme heslo, smíchá ho s /O, /P, file ID a hashem závislým na revizi a vytvoří file key. Když file key vložíte do slotu pro heslo, získáte nesmysl zahashovaný do jiného nesmyslu, proto potřebuje tato funkce vlastní vstupní bod místo příznaku na DAOpenFile

Místo, kam lze surový klíč vložit, určuje revize. Revize 2 až 4 stále odvozují od file key odlišný klíč pro každý objekt z čísla objektu, čísla generace a u AESV2 ještě AES salt, takže vlastnictví file key vám vůbec neumožní obejít odvození na úrovni objektu. Revize 5 až 7 používají 32bajtový file key přímo pro AES-256 bez kroku pro jednotlivé objekty. Jedinou vrstvou společnou oběma cestám je samotný file key, a právě ten PDF Library for Delphi přijímá zvenčí. Pokud heslo stále máte, zůstaňte na běžné cestě a nechte lifecycle opakování hesla zpracovat první chybný pokus, protože vstupní bod pro surový klíč se záměrně vzdává několika pohodlných vlastností, které cesta s heslem zachovává

Jaký vstup přijímá DAOpenFileWithEncryptionKey

Pouze neprefixedovaný ASCII hex bez mezer, sudé délky, jehož počet dekódovaných bajtů musí přesně odpovídat revizi šifrování. U revizí 2 až 4 se očekávaná délka bere z /Length v encryption dictionary: násobek 8 bitů mezi 5 a 16 bajty, při chybějícím /Length s výchozí hodnotou 40 bitů. U revizí 5 až 7 je délka přesně 32 bajtů a o ničem se nevyjednává. Prefix 0x, lichý počet číslic, více než 64 hex znaků, neznámý bit v Options nebo dokument, který vůbec není šifrovaný, všechny vedou ke stejnému výsledku: handle 0 a LastErrorCode nastavený na PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, což je 425. Přísnost je záměrná. Benevolentní parser, který ořízne mezery a krátký vstup doplní nulami, ochotně promění zkrácený paste ze schránky v credential a potom selže někde mnohem hůře čitelně

Var
  Lib: TPDFlib;
  FileHandle, PageRef: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    // KeyHex má 32 hex znaků pro AES-128 R4 a 64 pro 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 zůstávají odlišné, aby dávková úloha dokázala rozlišit chybu operátora od problémů s důkazním materiálem: 411, když soubor neexistuje, 401, když ho nelze otevřít ke čtení, a 409, když je poškozená cross-reference struktura. Všechno související s klíčem se záměrně slévá do 425, protože vstupní bod pro surový klíč, který prozradí, která část klíče byla špatně, je oracle

Co skutečně dokazuje ověření

PDF Library for Delphi prokáže, že dodaný klíč patří k tomuto dokumentu, pomocí verifieru, který už encryption dictionary obsahuje, a kontrola se podle revize liší. Revize 2 znovu vypočítá RC4 šifrování 32bajtového standardního paddingu a porovná všech 32 bajtů s /U. Revize 3 a 4 zahashují padding společně s file ID, provedou RC4 průchod a 19 kol odvozených přes XOR a porovnají prvních 16 bajtů /U. Revize 5 až 7 dešifrují 16bajtový řetězec /Perms s nulovým IV a současně zkontrolují čtyři nezávislé věci: little-endian permission word proti /P, čtyři bajty FF na pozicích 5 až 8, příznak šifrování metadat jako T nebo F a marker adb na pozicích 10 až 12

Když verifier existuje, ale neodpovídá, otevření je bezpodmínečně odmítnuto. Stojí za to to říct jasně, protože na této záruce stojí celá funkce. Ověření také není důkazem, že vás někdo oprávnil soubor použít: říká, že klíč dešifruje tento soubor, nikoli že máte oprávnění s ním pracovat. Permission word získané z /Perms je důkazem o klíči, nikoli udělením oprávnění, a pokud chcete vědět, co dokument skutečně tvrdí o povolených operacích, je to samostatná práce pro audit šifrování a oprávnění. Problémy s normalizací hesla, například zpracování SASLprep u ne-ASCII hesel AES-256, zde jednoduše nevznikají, protože žádný řetězec nedorazí do hashe

Kdy se uplatní PDF_RAW_KEY_ALLOW_UNVERIFIED

PDF_RAW_KEY_ALLOW_UNVERIFIED pokrývá právě jednu situaci: dokument nemá použitelný verifier, protože /Perms chybí nebo nemá 16 bajtů, případně je /U příliš krátké pro porovnání. Nemůže obejít důkaz, který selhal. Když změníte jednu hex číslici uvnitř /Perms a s nastavenou volbou předáte správný klíč, PDF Library for Delphi stále vrátí 0 a 425. Když s volbou předáte klíč z 32 nulových bajtů proti neporušenému verifieru, odpověď je stejná. Volba uvolňuje absenci důkazu, nikdy rozpor s důkazem. Protože recovery open a ověřené open jsou odlišné epistemické stavy, hlásí se také odděleně místo slévání do návratové hodnoty a DAGetEncryptionKeyValidation přijímá otevřený handle a odpovídá jednou ze tří konstant:

  • PDF_RAW_KEY_VALIDATION_VERIFIED (1) znamená, že verifier existoval a odpovídal
  • PDF_RAW_KEY_VALIDATION_UNVERIFIED (2) znamená, že klíč byl přijat jen proto, že nebylo možné vyhodnotit žádný verifier a volající si tuto politiku výslovně vyžádal
  • PDF_RAW_KEY_VALIDATION_NONE (0) hlásí běžný handle otevřený heslem
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
   (Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
  // v tomto souboru není verifier? opakuj pod explicitní recovery politikou
  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;

Jen pro čtení z konstrukce a kdo vlastní stream

Vstup pro raw-key soubor vždy otevírá zdroj s fmOpenRead or fmShareDenyWrite a celý řetězec Direct Access označí jako pouze pro čtení, takže DAAppendFile odmítne zápis na místě a vrátí 2 místo pokusu o inkrementální update. Není to politika, o které by šlo handle přesvědčit; nastaví se v konstruktoru ještě před parsováním souboru. Pro práci s důkazy je důležitou vlastností to, že zdrojové bajty jsou po zavření handle bajtově identické, a regresní sada to přesně ověřuje na fixture revize AES-128 4 i AES-256 6. Streamová varianta se při zápisech chová stejně a přidává jedno pravidlo: DAOpenFromStreamWithEncryptionKey nikdy nepřebírá vlastnictví, takže DACloseFile ponechá váš TStream živý a uvolní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 jen pro čtení: exportuj jinam, do důkazu nikdy nepřidávej
    Assert(Lib.DAAppendFile(FileHandle)= 2);
    Harvest(Lib, FileHandle);
  Finally
    Lib.DACloseFile(FileHandle);
  End;
Finally
  Source.Free;   // handle tento stream nikdy nevlastnil
End;

Hygiena klíče a co exportuje DLL

Zavření řetězce přepíše file key, cache hesla i odvozené klíče objektů a dekódovaný klíč se vymaže v bloku Finally samotného vstupního bodu bez ohledu na to, zda open uspěl. Za tím se skrývá drobnost, která zabrala skutečný čas při ladění: každá kopie, která musí přežít, se klonuje explicitně přes SetLength a Move, nikoli přiřazením. Když v Delphi přiřadíte jeden AnsiString druhému, obě jména sdílejí jeden buffer přes copy-on-write, takže vymazání strany volajícího vynuluje klíč, který kryptografický handler stále používá, a dokument se dešifruje na nesmysly z důvodu, který by žádný stack trace nevysvětlil. Hranici DLL překračují jen file-based vstupní body v wide i ANSI podobě společně s accessorrem validačního stavu; varianta TStream zůstává pouze v Delphi, protože závisí na životnosti objektu Delphi a referenční sémantice, které nemají poctivé vyjádření v plochém C ABI. Pokud je váš recovery nástroj klientem DLL, počítejte s přípravou do dočasného souboru a jeho odstraněním pod stejnými kontrolami, jaké používáte pro klíč

S hex klíčem zacházejte jako s credential materiálem a uplatněte pravidla, která byste dali heslu dokumentu, a validační stav ukládejte do logu řetězce custody, aby pozdější čtenář rozlišil ověřenou extrakci od nepotvrzené. Pokud v Delphi vybíráte PDF komponentu pro forenzní práci, hromadnou archivaci nebo migraci DRM, vstupní body pro raw key, záruka jen pro čtení i plocha auditu šifrování patří do téže knihovny a úplný seznam funkcí najdete na produktové stránce PDF Library for Delphi