Τεχνικό Άρθρο

Άνοιγμα κρυπτογραφημένων PDF με raw file key στο Delphi

Το PDF Library for Delphi μπορεί να ανοίξει ένα encrypted PDF από το raw file encryption key του αντί για password. Το DAOpenFileWithEncryptionKey δέχεται το key ως hexadecimal text, το ελέγχει έναντι του verifier που υπάρχει ήδη στο encryption dictionary και επιστρέφει read-only Direct Access handle· το DAOpenFromStreamWithEncryptionKey κάνει το ίδιο για ένα TStream που ανήκει στον caller. Και τα δύο προστέθηκαν στο v3.496.0

Το σενάριο είναι συγκεκριμένο και υπαρκτό. Μια forensics έρευνα σου παραδίδει key που ανακτήθηκε από memory image και κανένα password. Μια μαζική archival job έχει δέκα χιλιάδες documents των οποίων τα file keys βρίσκονται σε escrow database επειδή το αρχικό DRM system έπαψε να εκδίδει passwords πριν από χρόνια. Μια μετάβαση από παλιό rights-management product έχει key material και τίποτε άλλο. Σε όλες αυτές τις περιπτώσεις το credential που έχεις είναι αποτέλεσμα key derivation και όχι input, και καμία password parameter στην API δεν μπορεί να το δεχτεί

Γιατί ένα file encryption key δεν είναι password

Το password και το file encryption key βρίσκονται σε αντίθετες πλευρές του key derivation στον PDF standard security handler (ISO 32000-1 §7.6.3). Ο handler παίρνει password, το αναμειγνύει με τα /O, /P, το file ID και ένα revision-specific hash και παράγει το file key. Αν βάλεις file key στη θέση του password, παίρνεις ανοησίες hashed μέσα σε διαφορετικές ανοησίες, γι’ αυτό χρειάζεται ξεχωριστό entry point αντί για flag στο DAOpenFile

Το σημείο όπου μπορεί να εισαχθεί το raw key καθορίζεται από το revision. Τα revisions 2 έως 4 εξακολουθούν να παράγουν ξεχωριστό per-object key από το file key, το object number, το generation number και, για AESV2, το AES salt, οπότε το file key δεν σου επιτρέπει να παρακάμψεις καθόλου το object-level derivation. Τα revisions 5 έως 7 χρησιμοποιούν απευθείας το file key των 32 bytes για AES-256, χωρίς per-object στάδιο. Το κοινό layer και στα δύο είναι το ίδιο το file key, και μόνο σε αυτό το σημείο δέχεται εξωτερικά supplied key το PDF Library for Delphi. Αν έχεις ακόμη το password, μείνε στην κανονική διαδρομή και άφησε το password retry lifecycle να χειριστεί μια λανθασμένη πρώτη προσπάθεια, επειδή η raw-key entry point εγκαταλείπει σκόπιμα αρκετές ευκολίες που κρατά η password path

Ποιο input δέχεται το DAOpenFileWithEncryptionKey

Μόνο ASCII hex χωρίς prefix και whitespace, με άρτιο πλήθος χαρακτήρων, ενώ ο αριθμός των decoded bytes πρέπει να ταιριάζει ακριβώς με το encryption revision. Για revisions 2 έως 4 το αναμενόμενο μήκος προκύπτει από το /Length στο encryption dictionary: πολλαπλάσιο των 8 bits που δίνει από 5 έως 16 bytes, με προεπιλογή τα 40 bits όταν λείπει το /Length. Για revisions 5 έως 7 είναι ακριβώς 32 bytes, χωρίς διαπραγμάτευση. Prefix 0x, περιττός αριθμός ψηφίων, πάνω από 64 hex characters, άγνωστο bit στο Options ή document που δεν είναι encrypted παράγουν όλα το ίδιο αποτέλεσμα: handle 0 και LastErrorCode ίσο με PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, δηλαδή 425. Η αυστηρότητα είναι το ζητούμενο. Ένας lenient parser που κάνει trim σε whitespace και zero-pad σε σύντομο input θα μπορούσε εύκολα να μετατρέψει ένα κομμένο paste από το clipboard σε credential και να αποτύχει αργότερα σε πολύ λιγότερο ευανάγνωστο σημείο

Var
  Lib: TPDFlib;
  FileHandle, PageRef: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    // Το KeyHex έχει 32 hex χαρακτήρες για AES-128 R4 και 64 για 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;

Οι άλλοι failure codes παραμένουν ξεχωριστοί ώστε ένα batch job να μπορεί να διακρίνει operator error από προβλήματα των evidence: 411 όταν το αρχείο δεν υπάρχει, 401 όταν δεν μπορεί να ανοιχτεί για ανάγνωση και 409 όταν είναι κατεστραμμένη η cross-reference structure. Όλα τα key-related προβλήματα συγχωνεύονται σκόπιμα στο 425, επειδή ένα raw-key entry point που αναφέρει ποιο μέρος του key ήταν λάθος λειτουργεί ως oracle

Τι αποδεικνύει πράγματι η επαλήθευση

Το PDF Library for Delphi αποδεικνύει ότι το supplied key ανήκει σε αυτό το document, χρησιμοποιώντας τον verifier που ήδη μεταφέρει το encryption dictionary, και ο έλεγχος διαφέρει ανά revision. Το revision 2 επαναϋπολογίζει την RC4 encryption του 32-byte standard padding string και συγκρίνει και τα 32 bytes με το /U. Τα revisions 3 και 4 κάνουν hash στο padding μαζί με το file ID, εκτελούν το RC4 pass και τους 19 XOR-derived γύρους και συγκρίνουν τα πρώτα 16 bytes του /U. Τα revisions 5 έως 7 αποκρυπτογραφούν το 16-byte /Perms string με zero IV και ελέγχουν ταυτόχρονα τέσσερα ανεξάρτητα στοιχεία: το little-endian permission word έναντι του /P, τα τέσσερα bytes FF στις θέσεις 5 έως 8, το encrypt-metadata flag ως T ή F και το marker adb στις θέσεις 10 έως 12

Όταν υπάρχει verifier αλλά δεν ταιριάζει, το open απορρίπτεται χωρίς εξαίρεση. Αυτό χρειάζεται να ειπωθεί καθαρά, επειδή πάνω του στηρίζεται ολόκληρη η εγγύηση της δυνατότητας. Σημείωσε επίσης τι δεν είναι η verification: λέει ότι το key αποκρυπτογραφεί αυτό το αρχείο, όχι ότι κάποιος σε εξουσιοδότησε να το χρησιμοποιήσεις. Το permission word που ανακτάται από το /Perms είναι evidence για το key και όχι grant, ενώ για να δεις τι ισχυρίζεται πραγματικά ότι επιτρέπει το document χρειάζεται ξεχωριστή δουλειά, όπως ένα encryption και permissions audit pass. Προβλήματα normalization στην πλευρά των passwords, όπως ο χειρισμός SASLprep σε non-ASCII AES-256 passwords, απλώς δεν εμφανίζονται εδώ, αφού κανένα string δεν φτάνει σε hash

Πότε εφαρμόζεται το PDF_RAW_KEY_ALLOW_UNVERIFIED

Το PDF_RAW_KEY_ALLOW_UNVERIFIED καλύπτει ακριβώς μία κατάσταση: το document δεν διαθέτει usable verifier, επειδή το /Perms λείπει ή δεν έχει 16 bytes ή το /U είναι πολύ μικρό για σύγκριση. Δεν μπορεί να παρακάμψει failing evidence. Κάνε corrupt ένα hex digit μέσα στο /Perms και δώσε το σωστό key με ενεργή την επιλογή, και το PDF Library for Delphi εξακολουθεί να επιστρέφει 0 και 425. Δώσε key 32 μηδενικών bytes απέναντι σε intact verifier με ενεργή την επιλογή και η απάντηση είναι η ίδια. Η επιλογή χαλαρώνει την απουσία απόδειξης, ποτέ μια αντίφαση με αυτή. Επειδή ένα recovery open και ένα verified open είναι διαφορετικές epistemic states, αναφέρονται και ξεχωριστά αντί να συγχωνεύονται στην return value, ενώ το DAGetEncryptionKeyValidation παίρνει το open handle και απαντά με μία από τρεις constants:

  • PDF_RAW_KEY_VALIDATION_VERIFIED (1) σημαίνει ότι υπήρχε verifier και ταίριαξε
  • PDF_RAW_KEY_VALIDATION_UNVERIFIED (2) σημαίνει ότι το key έγινε αποδεκτό μόνο επειδή δεν μπορούσε να αξιολογηθεί verifier και ο caller ζήτησε ρητά αυτή την policy
  • PDF_RAW_KEY_VALIDATION_NONE (0) είναι αυτό που αναφέρει ένα handle που άνοιξε με συνηθισμένο password
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
   (Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
  // δεν υπάρχει verifier στο αρχείο; επανάλαβε με ρητή 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;

Read-only από κατασκευή και ποιος κατέχει το stream

Η raw-key file entry ανοίγει πάντα την source με fmOpenRead or fmShareDenyWrite και χαρακτηρίζει ολόκληρη την αλυσίδα Direct Access ως read-only, οπότε το DAAppendFile αρνείται το in-place writing και επιστρέφει 2 αντί να επιχειρήσει incremental update. Δεν είναι policy από την οποία μπορείς να μεταπείσεις το handle· ορίζεται στον constructor πριν καν γίνει parse το αρχείο. Για evidence work, αυτό που θέλεις είναι τα source bytes να είναι byte-identical μετά το κλείσιμο του handle, και η regression suite το ελέγχει ακριβώς αυτό τόσο σε AES-128 revision 4 fixture όσο και σε AES-256 revision 6. Η stream entry συμπεριφέρεται το ίδιο στα writes και προσθέτει έναν ακόμη κανόνα: το DAOpenFromStreamWithEncryptionKey δεν παίρνει ποτέ ownership, άρα το DACloseFile αφήνει το δικό σου TStream ζωντανό και το αποδεσμεύεις εσύ

Source:= TMemoryStream.Create;
Try
  Source.LoadFromFile(ArchivePath);
  Source.Position:= 0;
  FileHandle:= Lib.DAOpenFromStreamWithEncryptionKey(Source, KeyHex);
  If FileHandle<> 0 Then
  Try
    // 2 = read-only handle: export αλλού, ποτέ append στο evidence
    Assert(Lib.DAAppendFile(FileHandle)= 2);
    Harvest(Lib, FileHandle);
  Finally
    Lib.DACloseFile(FileHandle);
  End;
Finally
  Source.Free;   // το handle δεν απέκτησε ποτέ ownership αυτού του stream
End;

Υγιεινή των keys και τι εξάγει η DLL

Το κλείσιμο της chain κάνει overwrite στο file key, στο password cache και στα derived object keys, ενώ το decoded key γίνεται wipe μέσα στο Finally block του ίδιου του entry point είτε το open πέτυχε είτε όχι. Πίσω από αυτό υπάρχει μια λεπτομέρεια που κόστισε πραγματικό debugging time: κάθε copy που πρέπει να επιβιώσει γίνεται clone ρητά με SetLength και Move αντί να γίνει assignment. Αν αναθέσεις ένα AnsiString σε άλλο στο Delphi, και τα δύο ονόματα μοιράζονται ένα buffer μέσω copy-on-write, οπότε το wipe στην πλευρά του caller θα μηδενίσει το key που εξακολουθεί να χρησιμοποιεί ο crypt handler και το document θα αποκρυπτογραφηθεί σε σκουπίδια για λόγους που κανένα stack trace δεν θα εξηγούσε. Μόνο τα file-based entry points περνούν το DLL boundary, σε wide και ANSI μορφές, μαζί με τον accessor του validation status· η παραλλαγή TStream μένει Delphi-only, επειδή εξαρτάται από Delphi object lifetime και reference semantics που δεν έχουν ειλικρινή αναπαράσταση σε flat C ABI. Αν το recovery tooling σου είναι DLL client, σχεδίασε staging σε προσωρινό αρχείο και διαγραφή του με τους ίδιους ελέγχους που εφαρμόζεις στο key

Αντιμετώπισε το hex key ως credential material με τους κανόνες χειρισμού που θα έδινες σε document password και κράτα το validation status στο log της chain of custody, ώστε ένας μεταγενέστερος αναγνώστης να ξεχωρίζει verified extraction από unattested extraction. Αν αξιολογείς Delphi PDF component για forensics, bulk archival ή DRM migration, τα raw-key entry points, η read-only εγγύηση και το encryption audit surface ανήκουν στην ίδια library, και η πλήρης λίστα δυνατοτήτων βρίσκεται στη σελίδα προϊόντος PDF Library for Delphi