PDF Library for Delphi kan een versleutelde PDF openen met de raw file encryption key in plaats van met een wachtwoord. DAOpenFileWithEncryptionKey accepteert de key als hexadecimale tekst, controleert die tegen de verifier die al in het encryption dictionary staat en retourneert een read-only Direct Access-handle; DAOpenFromStreamWithEncryptionKey doet hetzelfde voor een door de caller beheerde TStream. Beide zijn in v3.496.0 toegevoegd
Het scenario is klein maar echt. Bij forensisch onderzoek krijg je een key die uit een memory image is teruggewonnen, maar geen wachtwoord. Een bulkarchief bevat tienduizend documenten waarvan de file keys in een escrowdatabase staan omdat het oorspronkelijke DRM-systeem al jaren geen wachtwoorden meer uitdeelt. Bij een migratie weg van een uitgefaseerd rights-managementproduct heb je keymateriaal en verder niets. In al die gevallen is het credential dat je hebt de uitvoer van key derivation en niet de invoer, en geen enkele password-parameter in de API kan het aannemen
Waarom is een file encryption key geen wachtwoord?
Een wachtwoord en een file encryption key bevinden zich aan tegenovergestelde kanten van key derivation in de standaard security handler van PDF (ISO 32000-1 §7.6.3). De handler neemt een wachtwoord, combineert dat met /O, /P, de file ID en een revision-specifieke hash en produceert de file key. Stop je een file key in de password-slot, dan wordt er onzin in andere onzin gehasht. Daarom heeft dit een eigen entrypoint nodig en geen flag op DAOpenFile
Waar de raw key kan worden geïnjecteerd, staat vast per revision. Revisions 2 tot en met 4 leiden nog een afzonderlijke key per object af uit de file key, het objectnummer, het generation number en voor AESV2 het AES-salt. Alleen de file key hebben laat je de derivation per object dus helemaal niet overslaan. Revisions 5 tot en met 7 gebruiken de 32-byte file key rechtstreeks voor AES-256, zonder stap per object. De enige laag die beide gemeen hebben is de file key, en precies daar accepteert PDF Library for Delphi een extern aangeleverde key. Heb je het wachtwoord nog wel, blijf dan op het gewone pad en laat de lifecycle voor wachtwoordpogingen een verkeerde eerste poging afhandelen, want de raw-key-entry geeft bewust meerdere gemakken op die het wachtwoordpad behoudt
Welke input accepteert DAOpenFileWithEncryptionKey?
Alleen ASCII-hex zonder prefix of whitespace en met een even aantal tekens, waarbij het aantal gedecodeerde bytes exact bij de encryption revision moet passen. Voor revisions 2 tot en met 4 komt de verwachte lengte uit /Length in het encryption dictionary: een veelvoud van 8 bits tussen 5 en 16 bytes, met standaard 40 bits wanneer /Length ontbreekt. Voor revisions 5 tot en met 7 is de lengte exact 32 bytes, zonder onderhandeling. Een prefix 0x, een oneven aantal cijfers, meer dan 64 hextekens, een onbekende bit in Options of een document dat helemaal niet versleuteld is, levert steeds hetzelfde op: een handle van 0 en LastErrorCode met de waarde PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, oftewel 425. Die strengheid is juist het doel. Een soepele parser die whitespace trimt en korte input met nullen aanvult, maakt zonder protest van een afgebroken clipboard-plakactie een credential en faalt daarna op een veel minder leesbare plek
Var
Lib: TPDFlib;
FileHandle, PageRef: Integer;
Begin
Lib:= TPDFlib.Create;
Try
// KeyHex bevat 32 hextekens voor AES-128 R4 en 64 voor 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;
De andere foutcodes blijven onderscheiden, zodat een batchjob een operatorfout kan scheiden van problemen met het bewijsmateriaal: 411 wanneer het bestand niet bestaat, 401 wanneer het niet geopend kan worden om te lezen en 409 wanneer de cross-referencestructuur beschadigd is. Alles wat met de key te maken heeft, valt bewust samen in 425, want een raw-key-entrypoint dat meldt welk deel van de key fout was, vormt een oracle
Wat bewijst de verificatie werkelijk?
PDF Library for Delphi bewijst dat de aangeleverde key bij dit document hoort, met de verifier die het encryption dictionary al bevat, en de controle verschilt per revision. Revision 2 berekent de RC4-encryptie van de standaard paddingstring van 32 bytes opnieuw en vergelijkt alle 32 bytes met /U. Revisions 3 en 4 hashen de padding samen met de file ID, voeren de RC4-pass plus de 19 XOR-afgeleide rondes uit en vergelijken de eerste 16 bytes van /U. Revisions 5 tot en met 7 decrypten de 16-byte /Perms-string met een nul-IV en controleren vier onafhankelijke zaken tegelijk: het little-endian permission word tegen /P, de vier FF-bytes op posities 5 tot en met 8, de encrypt-metadataflag als T of F en de adb-marker op posities 10 tot en met 12
Wanneer er een verifier bestaat maar die niet overeenkomt, wordt het openen zonder voorbehoud geweigerd. Dat moet expliciet worden gezegd, want op die garantie rust de hele functie. Let ook op wat de verificatie niet bewijst: ze zegt dat de key dit bestand kan decrypten, niet dat iemand je toestemming gaf om hem te gebruiken. Het permission word uit /Perms is bewijs over de key en geen machtiging, en als je wilt weten wat het document werkelijk beweert toe te staan, is dat een aparte taak voor een auditpass voor encryptie en permissions. Problemen met normalisatie aan de wachtwoordkant, zoals SASLprep-verwerking van niet-ASCII AES-256-wachtwoorden, doen zich hier simpelweg niet voor omdat geen enkele string een hash bereikt
Wanneer is PDF_RAW_KEY_ALLOW_UNVERIFIED van toepassing?
PDF_RAW_KEY_ALLOW_UNVERIFIED dekt precies één situatie: het document bevat geen bruikbare verifier omdat /Perms ontbreekt of geen 16 bytes lang is, of omdat /U te kort is om te vergelijken. De optie kan tegenstrijdig bewijs niet overrulen. Corrumpeert één hexteken binnen /Perms en geef je met de optie ingesteld de juiste key mee, dan retourneert PDF Library for Delphi nog steeds 0 en 425. Geef je met de optie ingesteld een key van 32 nulbytes tegen een intacte verifier, dan is het antwoord hetzelfde. De optie versoepelt het ontbreken van bewijs, nooit een tegenspraak ermee. Omdat een recovery-open en een geverifieerde open verschillende epistemische toestanden zijn, worden ze ook afzonderlijk gemeld in plaats van in de return value te worden samengevoegd, en DAGetEncryptionKeyValidation neemt de geopende handle aan en antwoordt met één van drie constanten:
PDF_RAW_KEY_VALIDATION_VERIFIED(1) betekent dat er een verifier aanwezig was en overeenkwamPDF_RAW_KEY_VALIDATION_UNVERIFIED(2) betekent dat de key alleen is geaccepteerd omdat geen verifier kon worden geëvalueerd en de caller expliciet om dat beleid vroegPDF_RAW_KEY_VALIDATION_NONE(0) is wat een handle die met een gewoon wachtwoord is geopend rapporteert
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
(Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
// geen verifier in dit bestand? opnieuw proberen met expliciet recoverybeleid
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 by construction, en wie bezit de stream?
De raw-key-file-entry opent de bron altijd met fmOpenRead or fmShareDenyWrite en markeert de hele Direct Access-chain als read-only. Daardoor weigert DAAppendFile schrijven in place en retourneert het 2 in plaats van een incrementele update te proberen. Dat is geen policy waar je de handle van kunt overtuigen; de instelling gebeurt in de constructor voordat het bestand überhaupt wordt geparsed. Voor evidencewerk is de eigenschap die je wilt dat de bronbytes byte-identiek zijn nadat de handle sluit, en de regressiesuite controleert dat precies op zowel een AES-128-revision-4-fixture als een AES-256-revision-6-fixture. De stream-entry gedraagt zich hetzelfde bij writes en voegt nog één regel toe: DAOpenFromStreamWithEncryptionKey neemt nooit ownership over, dus DACloseFile laat jouw TStream leven en je ruimt hem zelf op
Source:= TMemoryStream.Create;
Try
Source.LoadFromFile(ArchivePath);
Source.Position:= 0;
FileHandle:= Lib.DAOpenFromStreamWithEncryptionKey(Source, KeyHex);
If FileHandle<> 0 Then
Try
// 2 = read-only handle: elders exporteren, nooit aan het bewijsmateriaal toevoegen
Assert(Lib.DAAppendFile(FileHandle)= 2);
Harvest(Lib, FileHandle);
Finally
Lib.DACloseFile(FileHandle);
End;
Finally
Source.Free; // de handle heeft deze stream nooit beheerd
End;
Key hygiene en wat de DLL exporteert
Bij het sluiten van de chain worden de file key, de password cache en de afgeleide object keys overschreven, en de gedecodeerde key wordt in het Finally-blok van het entrypoint zelf gewist, ongeacht of het openen slaagt. Daarachter zit een subtiliteit die veel echte debugtijd kostte: elke kopie die moet blijven bestaan, wordt expliciet gekloond met SetLength plus Move in plaats van toegewezen. Wijs in Delphi de ene AnsiString aan de andere toe, dan delen beide namen onder copy-on-write één buffer. De caller-kant wissen zou dan de key op nul zetten die de crypt-handler nog gebruikt, waarna het document om redenen die geen stacktrace uitlegt in garbage decrypt. Alleen de file-based entrypoints gaan over de DLL-grens, in wide- en ANSI-vorm, samen met de accessor voor de validatiestatus. De TStream-variant blijft Delphi-only omdat die afhangt van de levensduur van Delphi-objecten en referentiesemantiek die geen eerlijke representatie in een flat C ABI hebben. Als je recoverytooling een DLL-client is, plan dan staging naar een tijdelijk bestand en verwijder dat onder dezelfde controles die je op de key toepast
Behandel de hex key als credentialmateriaal met de regels die je voor een documentwachtwoord zou hanteren, en bewaar de validatiestatus in het log dat je chain of custody produceert, zodat een latere lezer een geverifieerde extractie kan onderscheiden van een extractie zonder attest. Als je een Delphi PDF-component beoordeelt voor forensics, bulkarchivering of een DRM-migratie, horen de raw-key-entrypoints, de read-only-garantie en het auditoppervlak voor encryptie bij dezelfde library. De volledige featurelijst staat op de PDF Library for Delphi-productpagina