Το HotPDF Delphi PDF component υλοποιεί το crypt filter model του ISO 32000-1 §7.6.5 ως τρεις ανεξάρτητες policies αντί για έναν διακόπτη: το ConfigureCryptFilterDefaults ορίζει ξεχωριστά το string filter /StrF, το stream filter /StmF και το embedded-file filter /EFF, το SetStreamCryptFilter κάνει override σε ένα stream και το GetLoadedCryptFilterInfo αναφέρει τι δηλώνει ένα incoming file. Τα περισσότερα interop bugs σε encrypted PDF βρίσκονται στα κενά ανάμεσα σε αυτά τα τρία
Αυτό είναι το failure που στέλνει τον κόσμο σε αυτό το layer. Μια ομάδα παραδίδει document όπου το page content πρέπει να παραμένει readable από downstream tool αλλά το attached payload όχι, οπότε ορίζει /EFF /StdCF και αφήνει /StmF /Identity. Το Acrobat το ανοίγει κανονικά. Ένας conforming third-party reader επιστρέφει το attachment ως ciphertext garbage, επειδή το /EFF είναι producer-side policy για το ποιο filter ισχύει στα embedded files, ενώ ένας γενικός reader εξακολουθεί να επιλύει ένα unmarked stream μέσω του /StmF. Η διόρθωση δεν είναι διαφορετική τιμή /EFF. Είναι explicit /Crypt filter πάνω στο ίδιο το embedded-file stream
Τι ελέγχει στην πράξη το crypt filter layer
Τα crypt filters κάθονται ανάμεσα στον encryption algorithm και στο object graph, και αποφασίζουν ποια objects αγγίζει ο algorithm και όχι πώς λειτουργεί. Το /CF dictionary μέσα στο encryption dictionary αντιστοιχίζει names σε filter definitions, καθεμία με method /CFM, προαιρετικό /Length και /AuthEvent. Τα τρία top-level entries /StrF, /StmF και /EFF επιλέγουν στη συνέχεια ποιο από αυτά τα named filters εφαρμόζεται σε strings, σε streams χωρίς explicit filter και σε embedded files. Το HotPDF περιορίζει σκόπιμα όσα γράφουν οι built-in handlers του. Το ConfigureCryptFilterDefaults δέχεται μόνο τα reserved names του active handler: ο Standard security handler εκπέμπει /StdCF ή /Identity, ο public-key handler εκπέμπει /DefaultCryptFilter ή /Identity και οτιδήποτε άλλο σηκώνει EArgumentException στο call site. Filters που γράφτηκαν από external producers με άλλα names διατηρούνται στο load, inspection και compatibility-rewrite paths, οπότε το HotPDF είναι conservative ως writer και permissive ως reader. Ισχύουν ακόμη δύο guards: το call σηκώνει EInvalidOpException αφού αρχίσει το document serialization και ξανά όταν το document βρίσκεται σε incremental update, επειδή η encryption policy δεν μπορεί να αλλάξει ανάμεσα σε revisions του ίδιου file
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'wrapper.pdf';
Pdf.OwnerPassword := 'owner-secret';
Pdf.UserPassword := 'open-secret';
Pdf.CryptKeyLength := aes128;
// κρυπτογραφημένα strings, plaintext page streams, κρυπτογραφημένα attachments
Pdf.ConfigureCryptFilterDefaults('StdCF', 'Identity', 'StdCF');
Pdf.ActivateProtection := True;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(50, 50, 0, 'Visible stream operators');
Pdf.AddDocumentAttachment('payload.bin', 'Encrypted payload');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Ένας περιορισμός αξίζει να ειπωθεί εξαρχής, επειδή ελέγχεται αργά και εκπλήσσει. Τα named crypt filters στο HotPDF απαιτούν document encryption aes128, aes256 ή aesgcm. Ρύθμισε filter policy πάνω σε RC4 k40 ή k128 και το validation pass που τρέχει όταν ενεργοποιείται η encryption θα σηκώσει error αντί να προωθήσει σιωπηρά τον key type. Είναι η ίδια σχεδιαστική στάση με την υπόλοιπη AES-256 PDF encryption path στο Delphi: απόρριψε την αμφίσημη ρύθμιση αντί να μαντέψεις τι εννοούσε ο caller
Γιατί το entry /Length σημαίνει δύο διαφορετικά πράγματα
Επειδή το spec το ορίζει σε δύο διαφορετικές μονάδες, ανάλογα με τον security handler, και το HotPDF πρέπει να τιμήσει και τις δύο. Σε crypt filter dictionary του οποίου το /CFM είναι /V2, το entry /Length εκφράζεται σε bytes στον Standard security handler και σε bits στον public-key handler. Το /Length του encryption dictionary που βρίσκεται δίπλα στο /V (ISO 32000-1 §7.6.2) είναι πάντοτε σε bits. Διάβασε filter dictionary με /Length 16 και έχεις 128-bit key σε Standard-handler file αλλά rejected file σε public-key file. Το HotPDF το κανονικοποιεί όταν καταγράφει την loaded configuration. Πολλαπλασιάζει το /Length ενός /V2 filter επί οκτώ μόνο όταν το file δεν είναι public-key encrypted, επιστρέφει στο document-level /Length όταν το filter παραλείπει το δικό του και αποθηκεύει το αποτέλεσμα στο THPDFCryptFilterInfo.KeyLengthBits. Το AESV2 καθηλώνεται στα 128 bits και τα AESV3 και AESV4 στα 256, επειδή αυτές οι methods δεν έχουν negotiable key size. Μετά έρχεται το αυστηρό μέρος: γίνονται δεκτά μόνο 40-bit και 128-bit /V2. Filter που επιλύεται σε οποιοδήποτε άλλο length αναφέρεται ως unavailable και η operation αποτυγχάνει, αντί να στρογγυλοποιηθεί σε 128 με την υπόθεση ότι οι περισσότεροι producers εννοούσαν 128. Το σιωπηρό normalization key length είναι ο τρόπος να παραδώσεις file που decrypts στο δικό σου machine και πουθενά αλλού
var
Reader: THotPDF;
Info: THPDFCryptFilterInfo;
I: Integer;
begin
Reader := THotPDF.Create(nil);
try
Reader.AutoLaunch := False;
if Reader.LoadFromFile('incoming.pdf', 'open-secret') <> 1 then
Exit;
// /StrF και /StmF default σε Identity· /EFF default σε /StmF
WriteLn(Reader.LoadedStringCryptFilterName); // StdCF
WriteLn(Reader.LoadedStreamCryptFilterName); // Identity
WriteLn(Reader.LoadedEmbeddedFileCryptFilterName); // StdCF
for I := 0 to Reader.GetLoadedCryptFilterCount - 1 do
if Reader.GetLoadedCryptFilterInfo(I, Info) then
if (Info.Method = hcfmV2) and
not (Info.KeyLengthBits in [40, 128]) then
raise Exception.CreateFmt(
'crypt filter /%s: unsupported V2 key length %d',
[String(Info.Name), Info.KeyLengthBits]);
finally
Reader.Free;
end;
end;
Τι εγγυάται το /CFM /None και πώς διαφέρει από το /Identity
Φτάνουν στο ίδιο αποτέλεσμα από διαφορετικές διαδρομές, και η σύγχυσή τους χαλά τα lookups. Ένα named filter του οποίου το /CFM είναι /None και ένα named filter που παραλείπει εντελώς το /CFM σημαίνουν και τα δύο ότι αυτό το filter δεν κάνει encryption ή decryption — το HotPDF αντιστοιχίζει το missing entry σε None πριν από το resolving, οπότε και τα δύο καταλήγουν στο hcfmNone με recorded key length μηδέν. Το /Identity διαφέρει ως προς το είδος: είναι το reserved name που παρακάμπτει εξ ολοκλήρου το lookup του /CF, άρα ένα document μπορεί να αναφέρει /Identity χωρίς να το ορίζει πουθενά στο /CF. Τα PDF names κάνουν case-sensitive το matching, γεγονός που κάνει ακόμη μία implementation detail αδιαπραγμάτευτη: κανένα crypt filter lookup δεν μπορεί να είναι case-insensitive. Το HotPDF επιλύει τα names του /CF sub-dictionary, το entry /Length του filter και τον έλεγχο /Type του stream με case-sensitive dictionary lookups. File που ορίζει /stdcf ενώ το /StmF δείχνει στο /StdCF είναι malformed, και αν αντιμετωπίσεις τα δύο ως το ίδιο key θα μετατρέψεις ένα ανιχνεύσιμο authoring bug σε λάθος key που εφαρμόζεται σιωπηρά σε κάθε stream του document
Πώς γίνεται να ισχύει το /EFF σε embedded-file streams
Όταν το /EFF διαφέρει από το /StmF, το embedded-file stream χρειάζεται explicit leading /Crypt entry στο /Filter του και matching /DecodeParms dictionary που μεταφέρει /Name στην ίδια θέση του array. Το HotPDF το υπολογίζει ανά stream κατά το save: εντοπίζει /Type /EmbeddedFile, κληρονομεί το configured embedded-file filter και εκπέμπει explicit /Crypt marker μόνο όταν αυτό το inherited name διαφέρει από το effective stream default. Όταν /EFF και /StmF συμφωνούν, δεν γράφεται marker, επειδή ο reader θα επέλυε ούτως ή άλλως το ίδιο filter. Η θέση στο array μετρά τότε όσο και το name. Όταν το HotPDF ξαναδιαβάζει ένα stream, σαρώνει το /Filter για το /Crypt entry, καταγράφει το index του και μετά ψάχνει το ίδιο index στο /DecodeParms array για να βρει το /Name. Ένα /Crypt στο index 0 με parameters στο index 1 επιλύεται σε /Identity και όχι στο δικό σου filter. Γι’ αυτό ο writer γεμίζει το parameter array με null όταν το stream είχε προηγουμένως /Filter αλλά όχι /DecodeParms: οι θέσεις πρέπει να παραμείνουν aligned
Κάτω από αυτό υπάρχει ακόμη πιο απότομη παγίδα. Αν το υπάρχον /Filter ή /DecodeParms είναι indirect object — συνηθισμένο σε files από generators που μοιράζονται το ίδιο filter array σε πολλά streams — η εισαγωγή του /Crypt in place θα μετάλλαζε ένα shared filter graph και θα κατέστρεφε κάθε άλλο stream που δείχνει σε αυτό. Το HotPDF επιλύει το indirect object και το κάνει clone πρώτα σε stream-private direct object, καθαρίζοντας τα object και generation numbers ώστε το αρχικό indirect root να μην ενσωματωθεί ποτέ στο νέο array. Για stream που ήδη χρησιμοποιούσε ASCIIHexDecode, το serialized result είναι /Filter [ /Crypt /ASCIIHexDecode ] με /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ]. Η ίδια positional discipline διέπει κάθε άλλη filter chain, συμπεριλαμβανομένων εκείνων που διατρέχεις όταν εξάγεις images από loaded PDF μέσω των decode filters τους
// Ο Editor κρατά ήδη loaded document και το ContentStream είναι
// THPDFStreamObject του οποίου το /Filter είναι indirect /ASCIIHexDecode name
Editor.OwnerPassword := 'owner-secret';
Editor.UserPassword := 'open-secret';
Editor.CryptKeyLength := aes128;
Editor.ConfigureCryptFilterDefaults('StdCF', 'Identity');
Editor.SetStreamCryptFilter(ContentStream, 'StdCF');
Editor.ActivateProtection := True;
Editor.SaveLoadedDocument('out.pdf');
// Empty name καθαρίζει το override και αφαιρεί το stale /Crypt
// entry μαζί με τα decode parameters στο επόμενο save
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');
Κληρονομούν τα object streams την /Encrypt policy του document
Όχι, και η υπόθεση ότι την κληρονομούν είναι αξιόπιστος τρόπος να παράγεις garbage. Ένα object stream πρέπει να ακολουθεί την πραγματική policy /StmF ή το δικό του explicit /Crypt marker: η απλή παρουσία ενός /Encrypt dictionary δεν κάνει κάθε container /ObjStm ciphertext. Document με /StmF /Identity έχει plaintext object streams παρότι τα strings του είναι πλήρως encrypted, και decoder που τα decrypts ούτως ή άλλως δίνει στο inflate stage input που δεν ήταν ποτέ deflate output
Η συνέπεια για τα member objects είναι το σημείο που αξίζει να διαβαστεί δύο φορές. Κατά το ISO 32000-1 §7.5.7, strings μέσα σε encrypted object stream είναι ήδη plaintext μόλις γίνει decrypt το container, οπότε δεύτερο decrypt θα ήταν double-decrypt. Το HotPDF το προστατεύει ρωτώντας αν το container κάθε type-2 object ήταν encrypted και παραλείποντας το object όταν ήταν, ενώ μετρά τα skips στο XRefProbeDecryptObjStmSkips ως άμεση απόδειξη ότι ενεργοποιήθηκε ο guard. Όταν το container ήταν plaintext, τα member strings δεν καλύφθηκαν ποτέ από τίποτε, οπότε το HotPDF υλοποιεί αυτά τα members και εφαρμόζει το /StrF σε καθένα ξεχωριστά — με keying, όπως πράγματι κάνει η υλοποίηση, βάσει member object number και generation και όχι βάσει του object number του containing /ObjStm. Αν το αντιστρέψεις σε mixed-policy file, κάθε string σε κάθε compressed object αποκωδικοποιείται σε noise. Οι container-level κανόνες για αυτό καλύπτονται αναλυτικότερα στις σημειώσεις για PDF object streams και incremental updates
Πού αρνείται το HotPDF να μαντέψει
Τα crypt filter semantics δεν υπάρχουν κάτω από /V 4, οπότε το HotPDF απορρίπτει κάθε per-stream override σε τέτοιο file με explicit error αντί να γράψει /Crypt marker που κανένας conforming reader δεν θα τιμούσε. Το ίδιο ισχύει στην πλευρά της ανάγνωσης: encryption dictionary με /V κάτω από 4 καθαρίζει και τα τρία loaded filter names, επειδή δεν υπάρχει τίποτε εκεί για να αναφέρει. Τρία ακόμη boundaries εφαρμόζονται σκόπιμα:
- Per-stream filter διαφορετικό από
Identityσε public-key encrypted document απορρίπτεται, επειδή stream-specific policy κάτω από public-key handler χρειάζεται stream-specific recipient envelope που το HotPDF δεν εκπέμπει ακόμη - Public-key encrypted embedded files των οποίων το
/EFFδιαφέρει από το effective/StmFαπορρίπτονται για τον ίδιο λόγο, αντί να γραφτούν σε σχήμα που δεν decrypts για κανέναν - Το AES-256 direct-file fast path εφαρμόζεται μόνο όταν strings, streams και embedded files επιλύονται στην ίδια crypt filter method και κανένα object στο file δεν μεταφέρει explicit
/Crypt· mixed policy ή plaintext metadata επιβάλλει fallback στο full object-graph path
Κανένα από αυτά δεν είναι απόφαση performance. Σηματοδοτούν τα σημεία όπου ένα λάθος guess παράγει PDF που ανοίγει σε έναν viewer, αποτυγχάνει σε άλλον και δεν δίνει στον developer κανένα σήμα μέχρι να το αναφέρει customer. Μία άρνηση στο ConfigureCryptFilterDefaults ή κατά το save κοστίζει ένα exception· ένα silently mis-keyed embedded file κοστίζει έναν support cycle. Αν χτίζεις software Delphi ή C++Builder που παράγει ή καταναλώνει encrypted PDF — επιλεκτικά plaintext page content με encrypted attachments, PDF 2.0 encrypted payload wrappers ή interop με files των οποίων τις crypt filter policies δεν επέλεξες εσύ — το crypt filter API που περιγράφεται εδώ διατίθεται στο τρέχον HotPDF Delphi PDF component, δίπλα στα encryption, object stream και incremental update paths στα οποία βασίζεται