Technical Article

True PDF Redaction in Delphi: Delete and Sanitize

Echte PDF-Schwärzung in Delphi bedeutet, die zugrunde liegenden Bytes zu löschen und sie nicht bloß zu übermalen. Die losLab PDF Library zieht diese Grenze mit zwei APIs: RedactRegion editiert den Seiteninhalt-Stream, sodass gelöschte Texte, Vektoren und Bilder physisch verschwunden sind, und SanitizeDocument löscht Metadaten, Skripte und Aktionen, in denen sich ebenfalls sensible Informationen verbergen können. Machen Sie in einer der Hälften Fehler, versenden Sie eine Datei, die auf dem Bildschirm zwar zensiert aussieht, das Geheimnis aber an jeden weitergibt, der den Text kopiert oder die Dokumenteigenschaften öffnet

Dieses Fehlverhalten ist ein altbekanntes Problem. Zeitungen, Gerichte und Regierungsbehörden haben immer wieder PDFs veröffentlicht, bei denen ein schwarzes Rechteck über einen Namen oder eine Nummer gelegt wurde, nur damit ein Leser den darunter liegenden Text auswählen, kopieren und die vermeintlich geschwärzte Zeichenfolge im Klartext wieder einfügen konnte. Das Rechteck war nur eine Zeichnung. Der Text befand sich weiterhin im Content-Stream eine Ebene unter der Farbe, völlig unversehrt und kopierbar. Die losLab PDF Library, das Delphi- und C++Builder-PDF-Triebwerk, das über die Klasse TPDFlib bereitgestellt wird, behandelt diesen Unterschied als den Kern der Schwärzung und nicht als Nebensache

Warum ist das Zeichnen einer schwarzen Box über PDF-Text keine echte Schwärzung?

Die losLab PDF Library stellt MaskRect für jene Fälle bereit, in denen das Überdecken tatsächlich gewünscht ist: ein Wasserzeichen, ein visueller Blackout oder ein Prüfstempel. MaskRect(Page, Left, Top, Width, Height, Red, Green, Blue) fügt dem Seiten-Content-Stream ein Rechteck hinzu und füllt es mit dem Pfadkonstruktionsoperator re gefolgt vom Fülloperator f (ISO 32000-1 §8.5). Jedes Byte, das sich bereits auf der Seite befand — die Zeichen des abgedeckten Namens, die Vektorstriche, das eingebettete Bild —, verbleibt genau dort, wo es war, eine Anweisung früher im selben Stream. Ein standardkonformer Viewer zeichnet die schwarze Box zuletzt, sodass sie optisch gewinnt. Die Abdeckung ändert darunter jedoch nichts

Jeder kann den Originalinhalt auf drei Wegen wiederherstellen: den Text direkt durch die deckende Box hindurch auswählen und kopieren, einen Textextraktor über die Seite laufen lassen oder das abdeckende Rechteck aus dem Content-Stream löschen. MaskRect trägt diese Wahrheit schon im Namen — es maskiert, es entfernt nicht. Das Ergebnis ist wie ein Aufkleber auf einem Wort, das man immer noch lesen kann, indem man den Aufkleber einfach abzieht

Wie RedactRegion Inhalte auf Befehlsebene löscht

RedactRegion(Page, Left, Top, Width, Height, Options) arbeitet eine Ebene unter der Farbschicht. Die losLab PDF Library parst jede Inhaltsebene der Seite in ein TPDFContentProgram, durchläuft die Anweisungsliste mit einem CTM-verfolgenden Analysator (FindInstructionsInRect im Inhaltsmodell) und markiert jeden Textanzeige-, Pfadzeichnungs- und Bild-Do-Operator, dessen gezeichneter Bereich Ihr Rechteck schneidet. Diese Anweisungen werden mit TPDFContentProgram.Delete entfernt, und die bearbeitete Ebene wird mit WritePageContentLayer zurückgeschrieben. Die Bytes verschieben sich nicht hinter eine Abdeckung — sie verlassen den Stream. Ein danach ausgeführter Textextraktor findet nichts, weil nichts mehr da ist

Zwei Implementierungsdetails sind für das eigene Verständnis wichtig. RedactRegion nimmt ein Koordinatenrechteck bezogen auf den aktuellen Ursprung oben links entgegen und konvertiert es intern in den vom Content-Stream verwendeten Benutzerbereich unten links. Sie geben den Bereich also so an, wie Sie ihn auf der Seite sehen. Wenn ein Rechteck mehrere Anweisungen abdeckt, löscht RedactRegion diese in absteigender Indexreihenfolge, da das Entfernen der Anweisung 4 vor der Anweisung 7 jeden nachfolgenden Index verschieben und den Bereich beschädigen würde. Übergeben Sie Options = 0, stempelt die losLab PDF Library zusätzlich eine solide schwarze Box über den gelöschten Bereich als sichtbare Markierung — dieser Marker ist jedoch kosmetischer Natur und wird erst aufgetragen, nachdem der eigentliche Inhalt bereits gelöscht wurde

In der Praxis werden Sie das Rechteck selten hart kodieren. Sie durchsuchen die Seite nach der sensiblen Zeichenfolge und lesen deren Begrenzungsrahmen (Bounding Box) aus — dieselbe Seitensuche und Elementaufzählung, die in Lokalisieren von Text und Seitenelementen in Delphi behandelt wird — und übergeben dieses Rechteck an RedactRegion:

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('contract.pdf', '') = 1 then
    begin
      // Delete everything painted inside a 220 x 14 pt box on page 1.
      // Coordinates are top-left, in the current origin and units.
      PDF.RedactRegion(1, 90, 705, 220, 14, 0);   // Options=0 stamps a black marker
      PDF.SaveToFile('contract-redacted.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

Wo verbergen sich geschwärzte Informationen sonst noch in einer PDF-Datei?

Eine Schwärzung, die auf der sichtbaren Ebene aufhört, hinterlässt Spuren, da eine PDF-Datei Informationen an Orten speichert, an denen ein normaler Leser niemals sucht. Die losLab PDF Library gruppiert diese Lecks in relevante Kanäle. Das Document Information Dictionary und der XMP-Metadaten-Stream enthalten routinemäßig Angaben zum Autor (Author), dem Ersteller (Producer) oder Schlüsselwörter (Keywords), die genau die Person oder den Fall benennen, den Sie aus dem Text gelöscht haben. JavaScript auf Dokumentenebene und OpenAction-Katalogeinträge können beim Öffnen ausgeführt werden und Daten abrufen, die Sie eigentlich gelöscht glaubten. Jede Anmerkung (Annotation) enthält eine optionale /A-Aktion, und eine Anmerkung selbst — ein Notizzettel, ein altes Formularfeld, ein Link — kann direkt über der geschwärzten Stelle liegen und eine eigene Kopie des Textes enthalten. Nichts davon wird durch eine Verschlüsselung berührt. Eine verschlüsselte Datei mit einem Klartext-XMP-Paket gibt ihren Titel dennoch an jeden Indexierer weiter — eine Sicherheitslücke, die ein Auditieren von Verschlüsselung und Berechtigungen für sich genommen ratsam macht. Eine vertretbare Bereinigung muss jeden dieser Verstecke bereinigen, nicht nur die Seite, die Sie gerade betrachten

Bereinigen des Dokuments mit SanitizeDocument

Die losLab PDF Library begegnet diesem Spektrum an Lecks mit der Methode SanitizeDocument(Flags). Dies ist ein Orchestrierungsaufruf, der eine Reihe von unabhängigen Bereinigungsschritten ausführt, die über eine Bitmaske ausgewählt werden, und die Anzahl der angewendeten Operationen zurückgibt. Die Flags sind kombinierbar. SANITIZE_JAVASCRIPT entfernt jedes JavaScript-Paket auf Dokumentenebene und alle OpenAction-Skripte. SANITIZE_ACTIONS entfernt das /A-Aktionsverzeichnis aus jeder Anmerkung über das neue TPDFAnnots.RemoveAnnotAction und leert die OpenAction des Katalogs. SANITIZE_METADATA löscht Autor, Thema, Schlüsselwörter, Ersteller (Creator) und Produzent (Producer) sowohl im Document Information Dictionary (ISO 32000-1 §14.3.3) als auch im XMP-Metadaten-Stream (§14.3.2). SANITIZE_ANNOTATIONS löscht jede Anmerkung im gesamten Dokument, und SANITIZE_OPEN_ACTION entfernt den /OpenAction-Eintrag des Katalogs. SANITIZE_ALL ist die bitweise Union aller fünf

Die Indexkonventionen innerhalb dieser Bereinigungsschritte sind genau das, woran handgeschriebene Schleifen oft unbemerkt scheitern, weshalb SanitizeDocument als einzelner Aufruf existiert. GlobalJavaScriptPackageName ist 0-basiert und RemoveGlobalJavaScript verkürzt die Liste zur Laufzeit. Daher liest der JavaScript-Entferner immer den Paketindex 0 und entfernt so lange, bis die Anzahl null erreicht. DeleteAnnotation hingegen ist 1-basiert. AnnotationCount bezieht sich auf die aktuell ausgewählte Seite, sodass die Anmerkungsbereinigung jede Seite nacheinander auswählt, bevor sie zählt. Die losLab PDF Library regelt diese Grenzen intern korrekt, sodass Sie einen einzigen Parametersatz übergeben und ein einziges Ergebnis zurückbekommen, statt fünf verschiedene Iterationsregeln nachbauen zu müssen

var
  Applied: Integer;
begin
  // Strip metadata, scripts, and actions but keep the annotations:
  Applied := PDF.SanitizeDocument(SANITIZE_METADATA or SANITIZE_JAVASCRIPT or
    SANITIZE_ACTIONS or SANITIZE_OPEN_ACTION);

  // Or clear every hidden channel, annotations included:
  Applied := PDF.SanitizeDocument(SANITIZE_ALL);
end;

Der vollständige Arbeitsablauf aus Schwärzung und Bereinigung

Setzt man beides zusammen, ist das Verfahren erfreulich kurz. Die losLab PDF Library entfernt die sichtbaren Inhalte mit RedactRegion Seite für Seite und löscht dann jeden verborgenen Kanal mit SanitizeDocument(SANITIZE_ALL), bevor die Datei geschrieben wird. Da Schwärzungen fast immer auf Dokumenten ausgeführt werden, die von außerhalb Ihrer Kontrolle stammen, empfiehlt es sich, diese über einen gehärteten Pfad zu laden — dieselbe defensive Haltung, die in Sicheres Parsen nicht vertrauenswürdiger PDFs beschrieben ist —, damit eine fehlerhafte Eingabe sauber abbricht, statt mitten im Schwärzungslauf abzustürzen

var
  PDF: TPDFlib;
  Applied: Integer;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('incoming.pdf', '') = 1 then
    begin
      PDF.RedactRegion(1, 90, 705, 220, 14, 0);       // delete the sensitive line
      Applied := PDF.SanitizeDocument(SANITIZE_ALL);   // clear metadata, JS, actions, annots
      PDF.SaveToFile('published.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

Wo die Schwärzung im Content-Stream an ihre Grenzen stößt

RedactRegion arbeitet auf dem Content-Stream, in dem digital erstellte Texte liegen und in dem gescannte Texte gerade nicht liegen. Wenn eine Seite ein gescanntes Bild ist, bestehen die Wörter aus Pixeln in einem Bild-XObject und nicht aus Textoperatoren. Die losLab PDF Library kann einen Namen nicht aus der Mitte einer Rastergrafik herausschneiden, so wie sie ein Zeichen löscht. Ihre Bildbehandlung ist bewusst konservativ: Jedes Bild, dessen Positionierung das Rechteck schneidet, wird als Ganzes entfernt. Eine Schwärzung auf einer gescannten Seite entfernt somit die gesamte Grafik und nicht nur einen Ausschnitt davon. Die Schwärzung eines Bereichs innerhalb eines Bildes erfordert das Rastern, Übermalen und erneute Einbetten auf Pixelebene — eine völlig andere Operation als das Löschen aus dem Content-Stream. Dieselbe Vorsicht gilt für eine OCR-Textschicht über einer gescannten Seite: RedactRegion entfernt die sichtbaren Textoperatoren, aber das darunter liegende Bild bleibt unangetastet, es sei denn, sein Bild-XObject schneidet ebenfalls das Rechteck

Zwei weitere Punkte sorgen für ehrliche Ergebnisse. Der Schnittmengentest schätzt die Breite jedes Textlaufs anhand seiner Zeichenanzahl und Schriftgröße, anstatt die genauen Metriken der Zeichen zu messen. Ein sehr kurzes Wort in einer proportionalen Schrift kann daher an einem eng gezogenen Rechteck vorbeirutschen — geben Sie dem Bereich daher einen kleinen Sicherheitsabstand. Das Löschen erfolgt zudem auf Anweisungsebene: RedactRegion entfernt ganze Textanzeige-Operatoren, sodass ein Rechteck, das nur einen Teil eines Laufs erfasst, benachbarte Zeichen mitnehmen kann, die denselben Operator nutzen. Keine dieser Einschränkungen ist ein Grund, dem Mechanismus zu misstrauen — sie sind nur ein Grund, das Rechteck sorgfältig zu wählen und das Ergebnis zu überprüfen

Der abschließende Schritt jedes Schwärzungs-Workflows ist die Verifizierung, nicht das Vertrauen. Schwärzen Sie die Region, bereinigen Sie das Dokument, speichern Sie es unter einer neuen Datei, öffnen Sie diese erneut und versuchen Sie, sowohl den entfernten Text als auch die gelöschten Metadaten wieder auszulesen. Wenn beides leer bleibt, war die Schwärzung erfolgreich. Die hier gezeigten APIs RedactRegion und SanitizeDocument werden mit der losLab PDF Library für Delphi und C++Builder ausgeliefert, zusammen mit der vollständigen Inhaltsmodell- und Bereinigungsreferenz