Technical Article

Настоящее удаление конфиденциальных данных в PDF в Delphi: очистка и удаление контента

Настоящее удаление (redaction) конфиденциальных данных в PDF в Delphi означает физическое удаление байтов контента, а не просто их закрашивание. Библиотека losLab PDF Library разделяет эти задачи с помощью двух API: RedactRegion редактирует поток содержимого страницы, так что удаленный текст, векторная графика и изображения физически исчезают, а SanitizeDocument очищает метаданные, скрипты и связанные действия, где также могут скрываться конфиденциальные сведения. Ошибка на любом из этих этапов приведет к тому, что вы отправите файл, который на экране выглядит цензурированным, но позволит любому скопировать скрытый текст или прочесть его в свойствах документа

Подобные инциденты не раз предавались огласке. Газеты, суды и государственные органы публиковали файлы PDF, в которых поверх имени или номера был наложен черный прямоугольник, но читателям удавалось выделить скрытый текст под ним, скопировать и вставить якобы удаленные данные в обычный блокнот. Прямоугольник был просто графическим элементом. Текст по-прежнему находился в потоке содержимого уровнем ниже, целым и доступным для выделения. losLab PDF Library — движок PDF для Delphi и C++Builder, представленный классом TPDFlib, — рассматривает это различие как главную задачу удаления контента, а не как второстепенную деталь

Почему наложение черного прямоугольника поверх текста PDF не является настоящим удалением данных?

losLab PDF Library предоставляет метод MaskRect для тех случаев, когда наложение поверх контента — это действительно то, что вам нужно (например, водяной знак, визуальное скрытие, оттиск штампа). Метод MaskRect(Page, Left, Top, Width, Height, Red, Green, Blue) добавляет прямоугольник в поток контента страницы и закрашивает его оператором построения пути re с последующим оператором заливки f (ISO 32000-1 §8.5). Каждый байт, который уже находился на странице — глифы закрытого имени, векторные контуры, встроенные изображения — остается на своем месте, на одну инструкцию раньше в том же потоке. Совместимый просмотрщик просто рисует черный ящик последним, поэтому визуально он перекрывает все остальное. Наложение прямоугольника не меняет ничего под ним

Любой желающий может извлечь исходный контент тремя способами: выделить и скопировать текст прямо сквозь непрозрачный прямоугольник, запустить утилиту для извлечения текста со страницы или просто удалить закрывающий прямоугольник из потока содержимого. Метод MaskRect честно заявляет об этом в своем названии — он маскирует, а не удаляет. Результат аналогичен наклейке поверх слова, которую можно прочитать, просто приподняв ее

Как метод RedactRegion удаляет контент на уровне инструкций

Метод RedactRegion(Page, Left, Top, Width, Height, Options) работает на уровень глубже визуального слоя. losLab PDF Library разбирает каждый слой содержимого страницы в объект TPDFContentProgram, обходит список инструкций с помощью анализатора CTM (FindInstructionsInRect в модели контента) и находит каждый оператор вывода текста, отрисовки пути и отрисовки изображения Do, чья область пересекается с вашим прямоугольником. Эти инструкции удаляются с помощью TPDFContentProgram.Delete, а отредактированный слой записывается обратно методом WritePageContentLayer. Байты не сдвигаются за обложку — они полностью удаляются из потока. Позднее запущенное средство извлечения текста ничего не найдет, потому что искать будет нечего

Две особенности реализации стоит учесть в вашей логике. Метод RedactRegion принимает прямоугольник с координатами верхнего левого угла в текущей системе координат и единицах измерения и преобразует его во внутренние координаты нижнего левого угла пространства пользователя, используемого потоком содержимого, благодаря чему вы можете задавать область в том виде, в каком видите ее на странице. Когда прямоугольник охватывает несколько инструкций, RedactRegion удаляет их в порядке убывания индекса, так как удаление инструкции 4 перед инструкцией 7 сдвинуло бы последующие индексы и привело бы к ошибке диапазона. При передаче Options = 0 библиотека losLab PDF Library также накладывает сплошной черный прямоугольник поверх очищенной области в качестве визуального маркера — но этот маркер является исключительно косметическим, накладываемым после того, как исходный контент уже физически удален

На практике вы редко жестко задаете координаты прямоугольника. Вместо этого вы ищете на странице конфиденциальную строку и считываете ее ограничивающую рамку — аналогично поиску текста и перечислению элементов на странице, описанным в статье о поиске текста и элементов страниц в Delphi — а затем передаете этот прямоугольник в 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;

Где еще скрываются конфиденциальные данные в PDF?

Удаление, ограниченное только видимым слоем, оставляет след, поскольку PDF хранит информацию в местах, куда обычный читатель никогда не заглядывает. losLab PDF Library группирует подобные каналы утечки. Словарь информации о документе (Document Information Dictionary) и поток метаданных XMP регулярно содержат строки «Автор», «Создатель» или «Ключевые слова», которые называют именно того человека или дело, информацию о которых вы удалили из тела документа. Скрипты JavaScript на уровне документа и действие OpenAction каталога могут выполняться при открытии и обращаться к данным, которые вы считали удаленными. Каждая аннотация несет в себе необязательное действие /A, а сама аннотация — текстовая заметка, устаревшее поле формы, ссылка — может находиться прямо над очищенным местом и содержать собственную копию текста. Ничто из этого не защищено шифрованием; зашифрованный файл с незашифрованным пакетом XMP все равно раскрывает свое название любому поисковому индексатору, и этот вопрос заслуживает отдельного аудита шифрования и разрешений. Надежная очистка должна охватывать каждое из этих скрытых мест, а не только страницу, на которую вы смотрите

Очистка документа с помощью SanitizeDocument

losLab PDF Library решает проблему утечки этих данных с помощью метода SanitizeDocument(Flags). Это комплексный вызов, который запускает набор независимых процедур очистки, выбранных по битовой маске, и возвращает число примененных очисток. Флаги можно комбинировать. SANITIZE_JAVASCRIPT удаляет каждый пакет JavaScript на уровне документа и любые скрипты OpenAction. SANITIZE_ACTIONS удаляет словарь действий /A из каждой аннотации с помощью нового метода TPDFAnnots.RemoveAnnotAction и очищает OpenAction каталога. SANITIZE_METADATA очищает поля Author, Subject, Keywords, Creator и Producer как в словаре информации о документе (ISO 32000-1 §14.3.3), так и в потоке метаданных XMP (§14.3.2). SANITIZE_ANNOTATIONS удаляет все аннотации во всем документе, а SANITIZE_OPEN_ACTION удаляет запись /OpenAction каталога. Флаг SANITIZE_ALL объединяет все пять операций побитовым сложением

Правила работы с индексами в этих операциях — как раз то, что может легко нарушить работу написанного вручную цикла, из-за чего и существует удобный метод SanitizeDocument. Параметр GlobalJavaScriptPackageName отсчитывается от 0, и RemoveGlobalJavaScript уменьшает список по мере работы, поэтому процедура очистки JavaScript всегда считывает пакет с индексом 0 и удаляет его, пока счетчик не достигнет нуля. Метод DeleteAnnotation, напротив, использует индексацию от 1. Метод AnnotationCount сообщает количество для выбранной в данный момент страницы, поэтому процедура очистки аннотаций последовательно выбирает каждую страницу перед подсчетом. losLab PDF Library корректно обрабатывает эти тонкости на внутреннем уровне, позволяя вам передать один набор флагов и получить одно итоговое число выполненных удалений вместо реализации пяти различных правил итерации

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;

Полный процесс удаления контента и очистки данных

Объединение обеих операций делает процедуру очистки короткой. losLab PDF Library удаляет видимый контент с помощью RedactRegion, страница за страницей, а затем очищает каждый скрытый канал с помощью SanitizeDocument(SANITIZE_ALL) перед записью файла. Поскольку удалению конфиденциальных данных почти всегда подвергаются документы, полученные из внешних источников, стоит загружать их безопасным путем — в соответствии с рекомендациями по безопасному анализу недоверенных PDF, чтобы некорректный файл вызывал контролируемый сбой, а не падал на середине процесса удаления данных

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;

Ограничения удаления контента из потока содержимого

Метод RedactRegion работает с потоком содержимого, в котором находится исходный текстовый контент, но не отсканированный текст. Когда страница представляет собой отсканированное изображение, слова являются пикселями внутри графического объекта XObject, а не текстовыми операторами, и библиотека losLab PDF Library не может вырезать имя из середины растра так же, как она удаляет глиф. Работа с изображениями реализована консервативно: любое изображение, область которого пересекается с прямоугольником удаления, удаляется целиком, так что удаление области на отсканированной странице сотрет весь скан, а не его часть. Удаление данных в области внутри изображения требует растрирования, закрашивания и повторного внедрения на пиксельном уровне, что является совершенно иной операцией, нежели удаление из потока контента. То же предостережение относится и к распознанному слою OCR поверх сканированной страницы: RedactRegion удалит видимые текстовые операторы, но изображение под ними останется нетронутым, если только графический объект XObject также не пересекает рамку удаления

Две детали позволяют гарантировать корректность результата. Тест на пересечение оценивает ширину каждого фрагмента текста на основе количества символов и размера шрифта, а не вычисляет точные метрики глифов, поэтому очень короткое слово, набранное пропорциональным шрифтом, может не попасть в границы прямоугольника, если они очерчены слишком плотно — задавайте прямоугольник с небольшим запасом. Удаление данных также выполняется с точностью до инструкции: RedactRegion удаляет текстовые операторы целиком, поэтому прямоугольник, задевающий только часть текстовой строки, может удалить соседние символы, которые находились в том же операторе. Ни одно из этих ограничений не является поводом не доверять механизму — это лишь повод тщательно задавать границы прямоугольника и проверять результат

Правило, завершающее любой процесс удаления конфиденциальных данных, — это верификация, а не просто доверие. Выполните удаление контента в области, очистите документ, сохраните его в новый файл, откройте заново и попробуйте извлечь как удаленный текст, так и очищенные метаданные; если в обоих случаях данные отсутствуют, операция прошла успешно. Описанные здесь API RedactRegion и SanitizeDocument поставляются вместе с библиотекой losLab PDF Library для Delphi и C++Builder вместе с полной документацией по модели контента и очистке данных