Технічна стаття

Аудит шифрування та дозволів PDF у Delphi за допомогою PDF Library for Delphi

Прапорець дозволу — це не механізм безпеки. Біт, що каже «копіювання заборонено», живе в тому самому словнику /Encrypt, що й криптографія, і це надає йому ілюзію примусовості, якої він не має, і тієї миті, коли ви починаєте трактувати ці дві речі як одну, ваш аудит починає видавати неправильні відповіді. Єдине питання, яке варто ставити про PDF, — не «чи він зашифрований». Воно конкретніше й важче: який алгоритм, яка ревізія обробника безпеки, який із двох паролів встановлено, які біти дозволів заявлено і яких частин файлу шифрування справді торкається. Файл може бути формально зашифрованим і практично відкритим. Він може відмовлятися читатися, але лишати свої метадані відкритим текстом. Він може заблокувати друк прапорцем, який будь-який переглядач має право проігнорувати. Аудит PDF означає роздільне з'ясування всього цього, і PDF Library for Delphi, PDF-рушій losLab для Delphi та C++Builder, розкриває кожен з цих аспектів як через пласке API з цілочисельними дескрипторами, так і через типізований рівень класів

Що насправді фіксує словник /Encrypt

ISO 32000-1 §7.6 визначає безпеку документа через жменьку записів словника, і PDF Library for Delphi віддзеркалює їх один в один у записі TPDFEncryption. Версія фільтра V і ревізія R обирають родину алгоритму. Length несе розмір ключа. Біти дозволів сидять у P, рядки перевірки пароля власника та користувача — в O і U (з доданими OE та UE для AES-256), прапорець EncryptMetadata йде поруч, а ще три поля називають криптофільтри, застосовані відповідно до рядків, потоків і вбудованих файлів

Цінність цього запису в тому, що він нічого не інтерпретує за вас. Він віддає сирий словник і дозволяє вам самим робити висновки, а це саме те, що потрібно аудиту. Випадок «відкритий текст усередині зашифрованого» проявляється в StringFilterIdentity та StreamFilterIdentity: коли будь-яке з них істинне, відповідні дані проходять крізь фільтр Identity незмінними, хоч би що повідомляв статус шифрування документа. Сканер, який зупиняється на «словник /Encrypt присутній», назве такий файл захищеним, тоді як його рядки й потоки лежать відкритим текстом. Той самий нюанс керує метаданими. Коли EncryptMetadata хибне, пакет XMP лишається читабельним для будь-якого індексатора, тоді як вміст сторінки — ні, і це варто знати тієї ж миті, коли ваші правила маршрутизації спираються на поле заголовка чи автора

Діаграма PDF Library for Delphi: поля словника /Encrypt PDF відображені на аудиторські властивості TPDFEncryption, включно з пастками криптофільтра Identity
Кожен запис /Encrypt відображається на поле TPDFEncryption, а прапорці фільтра Identity показують, які рядки, потоки чи метадані залишаються читабельними незалежно від стану шифрування

Коротка перевірка безпеки пласким API

Для більшості конвеєрів на щоденні питання відповідають чотири пласкі виклики. LoadFromFile повертає 1 у разі успіху, і щойно документ відкрито, інспектори шифрування звітують за його розшифрованим станом:

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('contract.pdf', UserPassword) <> 1 then
      raise Exception.Create('Open failed: wrong password or damaged file');
    Writeln('status    : ', PDF.EncryptionStatus);     // розшифровано / зашифровано / невідомо
    Writeln('algorithm : ', PDF.EncryptionAlgorithm);  // RC4 чи родина AES
    Writeln('strength  : ', PDF.EncryptionStrength);   // клас довжини ключа
    Writeln('owner pw? : ', PDF.CheckPassword(CandidatePassword));
  finally
    PDF.Free;
  end;
end;

CheckPassword важить більше, ніж підказує його однорядкова сигнатура. PDF визначає два паролі з нерівною силою. Пароль користувача потрібен, щоб узагалі відкрити файл. Пароль власника надає повні права й перекриває кожен біт дозволів. Байти на диску ідентичні в обох випадках, але сесія, відкрита під паролем власника, може робити те, чого не може сесія з паролем користувача, тож аудит, який не фіксує, який саме обліковий запис було пред'явлено, фіксує лише половину правди. Рівень класів робить цю відмінність доступною для запиту. TPDFDocument.HasUserPassword і HasOwnerPassword повідомляють, що вимагає файл, тоді як IsUserPassword і IsOwnerPassword повідомляють, який пароль насправді відкрив поточну сесію. Фіксуйте цей факт у журналі. Ніколи не журналюйте самі значення паролів

Драбина Strength, де «AES-256» означає дві різні речі

Пласкі функції Encrypt та EncryptFile приймають ціле число Strength із п'ятьма значущими значеннями: 0 для 40-бітного RC4, 1 для 128-бітного RC4, 2 для 128-бітного AES, читного з Acrobat 7, 3 для 256-бітного AES, як його запровадив Acrobat 9, і 4 для 256-бітного AES, як цього вимагає Acrobat X і новіші

Цікаво те, що і 3, і 4 позначені як AES-256, хоча це не одна й та сама схема. Strength 3 відповідає ревізії обробника безпеки 5 — тимчасовому дизайну, який випустив Acrobat 9 і який ISO так і не прийняв. Strength 4 відповідає ревізії 6, чию функцію виведення ключа посилили й стандартизували в ISO 32000-2. Для документа, який ви створюєте сьогодні, немає причин обирати 3 замість 4. Для аудиту цей розрив вирішальний: політика, що звучить як «AES-256 за ISO 32000-2», задовольняється лише R6, а файл R5, який називає себе AES-256, провалює цю політику, водночас проходячи наївну перевірку силою шифрування. Рівень класів тримає ці два окремо за назвою — esAES256Bit для R5 проти esAES256BitAcroX для R6, — а властивість EncryptionAcroX відповідає на питання про ревізію одним булевим значенням

Драбина сили шифрування PDF від 40-бітного RC4 до AES-256 ревізії 5 проти ревізії 6 для аудитів Delphi
Рівні стійкості 3 і 4 обидва називаються AES-256, та лише ревізія 6 задовольняє політику ISO 32000-2, тому аудити мають фіксувати ревізію обробника, а не лише мітку

Біти дозволів і дрібний друк про довжину ключа

EncodePermissions пакує вісім прапорців у ціле число, якого очікують Encrypt та EncryptFile. Друк, копіювання, зміну й додавання приміток становлять базовий набір; заповнення полів, копіювання для доступності, компонування та повноякісний друк становлять розширений набір. Дрібний друк, про який прямо каже власна демонстрація шифрування бібліотеки, полягає в тому, що ці чотири розширені набувають чинності лише від 128-бітної сили й вище. Прапорець повноякісного друку підпорядковується тому самому правилу: скиньте його, щоб примусити низьку роздільність друку, і 40-бітний документ вас проігнорує, бо це пониження теж вимагає 128-бітного чи сильнішого шифрування. Закодуйте політику «лише низька роздільність друку» у 40-бітний файл — і кожен переглядач однаково друкуватиме в повній якості

Глибше питання в тому, хто взагалі забезпечує виконання цих бітів, і відповідь — ніхто, кому можна довіряти. Дозволи — це інструкції для відповідних програм перегляду, а не криптографічні обмеження. Ключ розшифрування ідентичний незалежно від того, дозволено копіювання чи заборонено, тож заблокований набір дозволів тримає чесними лише чесних. Переглядач, який вирішує проігнорувати ці біти, взагалі не стикається з жодною криптографічною перешкодою. Якщо зобов'язання — не дозволити вилучення, а не просто відмовити від нього, файлу потрібен пароль користувача, а робочому процесу — контролі на рівні процесу навколо нього, і звіт аудиту має називати, під яким саме з двох режимів фактично перебуває кожен файл, а не трактувати прапорець дозволу як замок

Встановлення політики і доведення, що вона застосувалася

Застосування шифрування до наявних файлів не вимагає завантажувати їх у дерево об'єктів. EncryptFile обробляє вхід у вихід одним викликом, а цикл аудиту повторно відкриває результат, щоб підтвердити, що саме потрапило на диск. Комплектна демонстрація шифрування виконує ту саму послідовність «запиши, потім прочитай назад»:

var
  PDF: TPDFlib;
  R: Integer;
begin
  PDF := TPDFlib.Create;
  try
    R := PDF.EncryptFile('in.pdf', 'out.pdf', 'owner-secret', 'user-secret', 4,
      PDF.EncodePermissions(1, 0, 0, 0,    // друк дозволено; копіювання/зміну/примітки заборонено
                            0, 0, 0, 1));  // розширений набір: лише повноякісний друк
    if (R = 1) and (PDF.LoadFromFile('out.pdf', 'user-secret') = 1) then
    begin
      Writeln('algorithm = ', PDF.EncryptionAlgorithm);
      Writeln('strength  = ', PDF.EncryptionStrength);
      Writeln('owner pw accepted: ', PDF.CheckPassword('owner-secret'));
    end;
  finally
    PDF.Free;
  end;
end;

Команди, що працюють на рівні документа, отримують ту саму операцію з типізованими наборами замість пакування бітів, і це переживає код-рев'ю з набагато меншим мружінням очей:

if not Doc.Encrypt('owner-secret', 'user-secret', esAES256BitAcroX,
  [ppCanPrint], [ppCanPrintFull]) then
  raise Exception.Create('Encryption failed');

У будь-якому разі крок повторного читання — не необов'язковий ритуал. Він ловить помилки розгортання, які інакше спливуть через місяці на машині клієнта: стара збірка бібліотеки, яка тихцем понижує запитану силу шифрування, вихідний шлях, який так і не був записаний, бо каталог виявився доступним лише для читання, ціле число дозволів, чиї аргументи потрапили не в тому порядку. Усі три проходять локальний димовий тест і провалюються в полі, а повторне відкриття виходу перетворює кожен з них на виключення, яке ви бачите просто під час запуску, що створив файл. GetEncryptionFingerprint повертає компактне значення, яке можна зберегти разом із записом завдання, тож пізніше порівняння здатне сказати, чи два виходи мають ту саму конфігурацію шифрування, не відкриваючи жодного з них повторно

Хибнопозитивні результати аудиту, варті кодування

Кілька патернів надійно штовхають сканери безпеки до неправильного висновку, і кожен з них походить від згортання багатоскладового питання у відповідь «так чи ні». Криптофільтр Identity — найчистіший приклад. Словник /Encrypt присутній, файл повідомляється як зашифрований, і водночас рядки та потоки проходять крізь фільтр Identity незмінними, тож справжній вміст — відкритий текст. Виправлення — читати StringFilterIdentity та StreamFilterIdentity, перш ніж оголошувати щось захищеним

Розкол метаданих тонший. EncryptMetadata може розходитися з рештою документа в обидва боки, лишаючи зашифрований файл із читабельним пакетом XMP або, рідше, навпаки. Фраза «файл зашифровано» нічого не каже про те, чи зашифровані його метадані, а це важить тієї миті, коли індексатор чи правило маршрутизації сягає по заголовок. Вбудовані файли додають третю вісь: PDF дозволяє окремий криптофільтр лише для вкладень, тож вкладення можуть бути єдиною зашифрованою частиною інакше відкритого документа або єдиною відкритою частиною зашифрованого. Фіксуйте три призначення фільтрів як окремі поля для рядків, потоків і вбудованих файлів, і жодна з цих пасток вас не спіймає. Зберігайте єдине булеве значення — і неправильний виклик стане лише питанням часу

PDF Library for Delphi: аудиторський потік, що перевіряє StringFilterIdentity, StreamFilterIdentity, EncryptMetadata і криптофільтр вбудованих файлів, перш ніж назвати PDF захищеним
Чотири незалежні осі визначають, чи справді запечатаний файл, що лише виглядає зашифрованим, а згортання їх в один булевий прапорець рано чи пізно помилково класифікує файл

Видалення шифрування і вибір його для нових файлів

Аудит часто завершується рішенням зняти захист, і механіка тут не перешкода. DecryptFile(InputFileName, OutputFileName, Password) записує розшифровану копію без повного завантаження, а Decrypt для вже завантаженого документа робить те саме в пам'яті, щойно файл уже відкрито. Обидва вимагають дійсний пароль; жоден не обходить криптографію. Справжній шлагбаум — це політика, а не код, тож нехай ваші правила прийому чітко визначають, коли дозволено видалення, і фіксують клас пароля, який це авторизував, бо сам технічний крок не лишає жодного сліду

Вибір для нового виходу вужчий, ніж натякають п'ять значень Strength. Використовуйте Strength 4, AES-256 ревізії 6, якщо тільки вам не потрібно відкривати файли в переглядачах старіших за Acrobat X. Strength 2, AES-128, — прагматичний мінімум для застарілого парку переглядачів, який не можна оновити. Варіанти RC4 під 0 і 1 існують, щоб ви могли читати й аудитувати історичні архіви, а не щоб виробляти на них щось нове; тягнутися по них у дизайні 2026 року — ознака того, що вимога вище за потоком застаріла

Стан шифрування напряму впливає на рішення про підписання, оскільки середовищу, що перевіряє й підписує документи, потрібна та сама дисципліна повторного читання, на яку спирається цей аудит. Це висвітлено в статті про середовище відповідності та підписання. Коли пакетна операція застосовує EncryptFile до тисяч великих документів, посібник із прямого доступу для великих PDF показує, як тримати пам'ять пласкою під час її виконання. Повна довідка з API шифрування розміщена на сторінці продукту PDF Library for Delphi