Die HotPDF Delphi PDF component implementiert das Crypt-Filter-Modell aus ISO 32000-1 §7.6.5 als drei unabhängige Richtlinien statt als einen Schalter: ConfigureCryptFilterDefaults weist Stringfilter /StrF, Streamfilter /StmF und Embedded-File-Filter /EFF getrennt zu, SetStreamCryptFilter überschreibt einen einzelnen Stream, und GetLoadedCryptFilterInfo meldet, was eine eingehende Datei deklariert. Die meisten Interoperabilitätsfehler bei verschlüsselten PDFs liegen in den Lücken zwischen diesen drei
Das ist der Fehler, der Menschen in diese Schicht führt. Ein Team liefert ein Dokument, dessen Seiteninhalt für ein nachgelagertes Werkzeug lesbar bleiben muss, während der angehängte Payload geheim bleiben soll. Es setzt daher /EFF /StdCF und lässt /StmF /Identity. Acrobat öffnet die Datei problemlos. Ein konformer Fremd-Reader liefert den Anhang als Ciphertext-Müll zurück, weil /EFF eine producerseitige Richtlinie dafür ist, welcher Filter auf eingebettete Dateien angewendet wird, während ein allgemeiner Reader einen nicht markierten Stream weiterhin über /StmF auflöst. Die Lösung ist kein anderer /EFF-Wert, sondern ein ausdrücklicher /Crypt-Filter direkt auf dem Embedded-File-Stream
Was steuert die Crypt-Filter-Schicht tatsächlich?
Crypt-Filter liegen zwischen Verschlüsselungsalgorithmus und Objektgraph und entscheiden, welche Objekte der Algorithmus berührt, nicht wie er arbeitet. Das /CF-Wörterbuch im Verschlüsselungswörterbuch ordnet Namen Filterdefinitionen zu; jede trägt eine /CFM-Methode, optional /Length und /AuthEvent. Die drei Einträge auf oberster Ebene, /StrF, /StmF und /EFF, wählen dann den Filter, der auf Strings, Streams ohne ausdrücklichen Filter und Embedded Files angewendet wird. HotPDF beschränkt bewusst, was seine integrierten Handler schreiben. ConfigureCryptFilterDefaults akzeptiert nur die reservierten Namen des aktiven Handlers: Der Standard-Sicherheits-Handler schreibt /StdCF oder /Identity, der Public-Key-Handler /DefaultCryptFilter oder /Identity, und jeder andere Name löst direkt am Aufrufort EArgumentException aus. Filter, die externe Producer unter anderen Namen geschrieben haben, bleiben beim Laden, bei der Inspektion und bei Kompatibilitäts-Rewrites erhalten. Als Writer ist HotPDF also konservativ, als Reader tolerant. Zwei weitere Schutzprüfungen gelten: Der Aufruf löst EInvalidOpException aus, sobald die Dokumentserialisierung begonnen hat, und erneut, wenn das Dokument ein inkrementelles Update ist, weil sich die Verschlüsselungsrichtlinie zwischen Revisionen derselben Datei nicht ändern darf
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 verschlüsselt, Seiten-Streams im Klartext, Anhänge verschlüsselt
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;
Eine Einschränkung sollte man vorwegnehmen, weil sie spät geprüft wird und überrascht. Benannte Crypt-Filter in HotPDF verlangen eine Dokumentverschlüsselung mit aes128, aes256 oder aesgcm. Konfigurieren Sie eine Filterrichtlinie auf k40 oder k128 für RC4, löst der Validierungslauf beim Aktivieren der Verschlüsselung einen Fehler aus, statt den Schlüsseltyp still auf AES hochzustufen. Das ist dieselbe Haltung wie beim AES-256-PDF-Verschlüsselungsweg in Delphi: eine unklare Konfiguration zurückweisen, statt zu erraten, was der Aufrufer gemeint hat
Warum bedeutet der /Length-Eintrag zwei verschiedene Dinge?
Weil die Spezifikation ihn abhängig vom Security Handler in zwei verschiedenen Einheiten definiert, und HotPDF beide beachten muss. In einem Crypt-Filter-Wörterbuch mit /CFM gleich /V2 wird der Eintrag /Length unter dem Standard-Security-Handler in Bytes und unter dem Public-Key-Handler in Bits angegeben. Das /Length des Verschlüsselungswörterbuchs neben /V (ISO 32000-1 §7.6.2) ist immer in Bits. Liest man ein Filterwörterbuch mit /Length 16, bedeutet das in einer Standard-Handler-Datei einen 128-Bit-Schlüssel und in einer Public-Key-Datei eine zurückgewiesene Datei. HotPDF normalisiert dies beim Erfassen der geladenen Konfiguration. Die Länge eines /V2-Filters wird nur bei nicht Public-Key-verschlüsselten Dateien mit acht multipliziert, wenn der Filter sein eigenes /Length auslässt, wird auf das Dokument-/Length zurückgefallen, und das Ergebnis landet in THPDFCryptFilterInfo.KeyLengthBits. AESV2 ist auf 128 Bit und AESV3 sowie AESV4 auf 256 Bit festgelegt, weil diese Methoden keine verhandelbare Schlüssellänge besitzen. Danach wird es streng: Nur 40 Bit und 128 Bit für /V2 werden akzeptiert. Ein Filter, der auf eine andere Länge aufgelöst wird, wird als nicht verfügbar gemeldet und die Operation schlägt fehl, statt ihn auf die Theorie zu runden, dass die meisten Producer wohl 128 gemeint hätten. Eine still normalisierte Schlüssellänge ist der Weg zu einer Datei, die auf Ihrem Rechner entschlüsselt und sonst nirgends
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 und /StmF defaulten auf Identity; /EFF defaultet auf /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;
Was garantiert /CFM /None, und wie unterscheidet sich /Identity?
Beide führen über unterschiedliche Wege zum selben Ergebnis, und ihre Verwechslung zerstört Lookups. Ein benannter Filter mit /CFM /None und ein benannter Filter, der /CFM vollständig auslässt, bedeuten beide, dass dieser Filter weder verschlüsselt noch entschlüsselt. HotPDF bildet den fehlenden Eintrag vor der Auflösung auf None ab, daher landen beide bei hcfmNone mit aufgezeichneter Schlüssellänge null. /Identity ist kategorial anders: Es ist der reservierte Name, der die /CF-Suche vollständig umgeht, daher darf ein Dokument auf /Identity verweisen, ohne es irgendwo in /CF zu definieren. PDF-Namen unterscheiden Groß-/Kleinschreibung, daher ist ein weiteres Implementierungsdetail nicht verhandelbar: Kein Crypt-Filter-Lookup darf die Schreibweise ignorieren. HotPDF löst die Namen der /CF-Unterwörterbücher, den /Length-Eintrag des Filters und die /Type-Prüfung des Streams mit case-sensitiven Dictionary-Lookups auf. Eine Datei, die /stdcf definiert, während /StmF auf /StdCF zeigt, ist fehlerhaft. Behandelt man beide als denselben Schlüssel, wird ein erkennbarer Authoring-Fehler zu einem still auf jeden Stream des Dokuments angewendeten falschen Schlüssel
/EFF auf Embedded-File-Streams wirksam machen
Wenn sich /EFF von /StmF unterscheidet, benötigt der Embedded-File-Stream einen ausdrücklichen führenden /Crypt-Eintrag in seinem /Filter und ein passendes /DecodeParms-Wörterbuch mit /Name an derselben Arrayposition. HotPDF berechnet das beim Speichern für jeden Stream: Es erkennt /Type /EmbeddedFile, übernimmt den konfigurierten Embedded-File-Filter und schreibt den expliziten /Crypt-Marker nur dann, wenn sich der übernommene Name vom effektiven Stream-Default unterscheidet. Stimmen /EFF und /StmF überein, wird kein Marker geschrieben, weil ein Reader denselben Filter auflösen würde. Die Arrayposition ist dabei genauso wichtig wie der Name. Beim Einlesen eines Streams scannt HotPDF /Filter nach dem /Crypt-Eintrag, merkt sich dessen Index und sucht anschließend an genau diesem Index im /DecodeParms-Array nach /Name. Ein /Crypt an Index 0 mit Parametern an Index 1 wird zu /Identity aufgelöst und nicht zu Ihrem Filter. Deshalb füllt der Writer das Parameterarray mit null auf, wenn der Stream früher ein /Filter, aber kein /DecodeParms besaß: Die Positionen müssen ausgerichtet bleiben
Darunter lauert eine schärfere Falle. Wenn /Filter oder /DecodeParms ein indirektes Objekt ist – üblich bei Dateien aus Generatoren, die ein Filterarray über viele Streams teilen –, würde das Einfügen von /Crypt an Ort und Stelle einen gemeinsam genutzten Filtergraph verändern und jeden anderen darauf verweisenden Stream beschädigen. HotPDF löst das indirekte Objekt auf und klont es zuerst in ein streamprivates direktes Objekt. Dabei werden Objekt- und Generationsnummern gelöscht, sodass die ursprüngliche indirekte Wurzel niemals in das neue Array eingebettet wird. Bei einem Stream, der bereits ASCIIHexDecode verwendete, lautet das serialisierte Ergebnis /Filter [ /Crypt /ASCIIHexDecode ] mit /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ]. Dieselbe Positionsdisziplin gilt für jede andere Filterkette, auch für jene, die Sie beim Extrahieren von Bildern aus einem geladenen PDF über ihre Decode-Filter durchlaufen
// Editor hält bereits ein geladenes Dokument, und ContentStream ist ein
// THPDFStreamObject, dessen /Filter ein indirekter /ASCIIHexDecode-Name ist
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');
// Ein leerer Name löscht den Override und entfernt beim nächsten Speichern
// den veralteten /Crypt-Eintrag samt seinen Decode-Parametern
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');
Erben Object Streams die /Encrypt-Richtlinie des Dokuments?
Nein, und wer das annimmt, erzeugt zuverlässig Datenmüll. Ein Object Stream muss der tatsächlichen /StmF-Richtlinie oder seinem eigenen ausdrücklichen /Crypt-Marker folgen. Das bloße Vorhandensein eines /Encrypt-Wörterbuchs macht nicht jeden /ObjStm-Container zu Ciphertext. Ein Dokument mit /StmF /Identity besitzt Klartext-Object-Streams, obwohl seine Strings vollständig verschlüsselt sind. Ein Decoder, der sie trotzdem entschlüsselt, übergibt der Inflate-Phase Eingaben, die nie Deflate-Ausgaben waren
Die Konsequenz für Member-Objekte ist der Teil, den man zweimal lesen sollte. Nach ISO 32000-1 §7.5.7 liegen Strings in einem verschlüsselten Object Stream bereits im Klartext vor, sobald der Container selbst entschlüsselt wurde; sie nochmals zu entschlüsseln wäre ein Double-Decrypt. HotPDF schützt davor, indem es für jedes Objekt vom Typ 2 abfragt, ob der Container verschlüsselt war, und das Objekt dann überspringt. Die Übersprünge werden in XRefProbeDecryptObjStmSkips gezählt und liefern direkten Beleg dafür, dass der Schutz gegriffen hat. War der Container unverschlüsselt, waren die Member-Strings nie geschützt. HotPDF materialisiert diese Member daher und wendet /StrF einzeln auf jeden an – so wie es die Implementierung tatsächlich tut, mit der Member-Objektnummer und Generation als Schlüssel und nicht mit der Objektnummer des umschließenden /ObjStm. Dreht man diese Regeln bei einer Datei mit gemischter Richtlinie um, wird jeder String in jedem komprimierten Objekt zu Rauschen dekodiert. Die Regeln auf Containerebene werden ausführlicher in den Notizen zu PDF-Object-Streams und inkrementellen Updates behandelt
Wo HotPDF nicht rät
Crypt-Filter-Semantik existiert unterhalb von /V 4 nicht, daher weist HotPDF jeden Stream-Override in einer solchen Datei mit einem ausdrücklichen Fehler zurück, statt einen /Crypt-Marker zu schreiben, den kein konformer Reader beachten würde. Dasselbe gilt beim Lesen: Ein Verschlüsselungswörterbuch mit /V unter 4 löscht alle drei geladenen Filternamen, weil es dort nichts zu melden gibt. Drei weitere Grenzen werden bewusst durchgesetzt:
- Ein Streamfilter ungleich
Identityauf einem Public-Key-verschlüsselten Dokument wird abgelehnt, weil eine streamspezifische Richtlinie unter dem Public-Key-Handler ein streamspezifisches Recipient-Envelope benötigt, das HotPDF noch nicht ausgibt - Public-Key-verschlüsselte Embedded Files, deren
/EFFvom effektiven/StmFabweicht, werden aus demselben Grund abgelehnt, statt in einer Form geschrieben zu werden, die niemand entschlüsseln kann - Der direkte AES-256-Datei-Fast-Path gilt nur, wenn Strings, Streams und Embedded Files alle auf dieselbe Crypt-Filtermethode aufgelöst werden und kein Objekt der Datei einen ausdrücklichen
/Crypt-Marker trägt. Eine gemischte Richtlinie oder Klartext-Metadaten erzwingen den Fallback auf den vollständigen Objektgraph-Weg
Das sind keine Performanceentscheidungen. Sie markieren Stellen, an denen eine falsche Vermutung ein PDF erzeugt, das in einem Viewer öffnet, in einem anderen scheitert und dem Entwickler überhaupt kein Signal gibt, bis ein Kunde es meldet. Eine Zurückweisung bei ConfigureCryptFilterDefaults oder beim Speichern kostet eine Exception; ein still falsch verschlüsselter Embedded File kostet einen Supportzyklus. Wenn Sie Delphi- oder C++Builder-Software bauen, die verschlüsselte PDFs erzeugt oder verarbeitet – selektiv klaren Seiteninhalt mit verschlüsselten Anhängen, verschlüsselte Payload-Wrapper nach PDF 2.0 oder Interoperabilität mit Dateien, deren Crypt-Filter-Richtlinien Sie nicht gewählt haben –, gehört die hier beschriebene Crypt-Filter-API zur aktuellen HotPDF Delphi PDF component, zusammen mit den Verschlüsselungs-, Object-Stream- und inkrementellen Update-Pfaden, auf denen sie aufbaut