Как работать с движком шифрования PDF в Python

Как работать с движком шифрования PDF в Python

Aspose.PDF FOSS для Python (import aspose_pdf) реализует защиту PDF паролем на уровне байтов через небольшой внутренний класс движка, EncryptionUtils, в aspose_pdf.engine.encryption. Здесь находятся шифр AES-CBC и алгоритмы вывода ключей PDF Standard Security Handler (ISO 32000 Ревизии 2-6), работающие независимо от высокоуровневого Document API. В этом руководстве пакет устанавливается и рассматриваются три задачи движка: шифрование и расшифрование сырых байтов с помощью AES-CBC, вывод ключей Standard Security Handler для пароля (Ревизии 2-4) и вывод ключей AES-256 для документов Ревизии 5/6.

Пошаговое руководство

Шаг 2: Импортировать необходимые классы

EncryptionUtils не экспортируется повторно из верхнеуровневого пакета aspose_pdf — импортируйте его напрямую из подпакета engine.encryption:

import os
from aspose_pdf.engine.encryption import EncryptionUtils

Шаг 3: Шифрование и расшифрование данных с помощью AES-CBC

encrypt_aes_cbc и decrypt_aes_cbc работают напрямую с любым 16-, 24- или 32-байтовым ключом (AES-128, AES-192 или AES-256), обходя вывод ключа на основе пароля. encrypt_aes_cbc применяет внутреннее заполнение PKCS7, а decrypt_aes_cbc удаляет его.

key = os.urandom(32)  # AES-256; 16- and 24-byte keys are also accepted
plaintext = b"Confidential PDF content"

ciphertext = EncryptionUtils.encrypt_aes_cbc(key, plaintext)
decrypted = EncryptionUtils.decrypt_aes_cbc(key, ciphertext)

assert decrypted == plaintext
print("Roundtrip OK, ciphertext length:", len(ciphertext))

Шаг 4: Вычисление ключей стандартного обработчика безопасности для пароля (ревизии 2-4)

compute_owner_key_v4 вычисляет значение пароля владельца (O) согласно Алгоритму 3.3, а compute_user_key_v4 вычисляет значение пароля пользователя (U) и ключ шифрования файла согласно Алгоритмам 3.4/3.5. verify_password_v4 повторно выводит ключ из предполагаемого пароля, чтобы вы могли убедиться, что он открывает документ, прежде чем доверять ему.

owner_password = "owner-secret"
user_password = "user-secret"
key_length = 16   # 128-bit (AES/RC4); Revision 2 uses 5 (40-bit)
revision = 4
file_id = os.urandom(16)
permissions = -4  # standard PDF permissions value

o_value = EncryptionUtils.compute_owner_key_v4(
    owner_password, user_password, key_length, revision
)

u_value, encryption_key = EncryptionUtils.compute_user_key_v4(
    user_password, o_value, permissions, file_id, key_length, revision
)

verified_key = EncryptionUtils.verify_password_v4(
    user_password, u_value, o_value, permissions, file_id, key_length, revision
)
assert verified_key == encryption_key

ciphertext = EncryptionUtils.encrypt_aes_cbc(verified_key, b"Confidential PDF content")
decrypted = EncryptionUtils.decrypt_aes_cbc(verified_key, ciphertext)
assert decrypted == b"Confidential PDF content"

o_value и u_value всегда имеют размер 32 байта независимо от key_length. verify_password_v4 возвращает None — не исключение — когда пароль не совпадает.


Шаг 5: Вычисление ключей AES-256 для документов ревизий 5/6

Обработчики безопасности ревизий 5 и 6 (AES-256) не используют compute_owner_key_v4/compute_user_key_v4 — они хешируют пароль с помощью compute_hash_v5, реализующего «Алгоритм 2.B» стандарта ISO 32000-2. Используйте generate_file_id для нового идентификатора файла и derive_object_key для преобразования ключа шифрования файла в ключ для конкретного косвенного объекта.

password = b"SecurePassword123"
salt = os.urandom(8)

password_hash = EncryptionUtils.compute_hash_v5(password, salt)
assert len(password_hash) == 32

file_id = EncryptionUtils.generate_file_id()
assert len(file_id) == 16

file_key = os.urandom(32)
object_key = EncryptionUtils.derive_object_key(file_key, 12, 0, use_aes=True)
assert len(object_key) == 16

derive_object_key смешивает номер объекта и номер поколения в ключ файла, поэтому каждый косвенный объект получает собственный ключ, хотя ключ файла общий.


Распространённые проблемы и решения

encrypt_aes_cbc/decrypt_aes_cbc выдает “AES key must be 16, 24, or 32 bytes”. Аргумент key должен быть реальным AES-ключом, а не строкой пароля. Сгенерируйте его с помощью os.urandom(16 | 24 | 32), или сначала получить его с помощью compute_user_key_v4 (Revisions 2-4) или compute_hash_v5 (Ревизии 5/6).

verify_password_v4 returns None вместо возбуждения. Это означает, что пароль не совпал — это не ошибка, которую нужно отлавливать, а просто результат, который следует проверять. Также убедитесь, что permissions, file_id, key_length, и revision являются точно теми же значениями, которые использовались при документе o_value and u_value были изначально вычислены; несоответствие любого из них тихо приводит к получению неправильного ключа вместо того, чтобы вызвать явную ошибку.

compute_owner_key_v4 с пустым паролем владельца. Passing "" for owner_password это поведение, определённое спецификацией, а не ошибка — алгоритм переходит к использованию пароля пользователя, поэтому пароли владельца и пользователя становятся эквивалентными.

Ключи, полученные для неправильной ревизии или длины ключа. Ревизия 2 использует 40-битные (5-байтовые) RC4-ключи, ревизии 3/4 используют 128-битные (16-байтовые) RC4/AES-ключи, а ревизии 5/6 используют compute_hash_v5 вместо compute_owner_key_v4/compute_user_key_v4 полностью. Смешивание этих вариантов не вызывает исключения — оно просто генерирует ключ, который не откроет документ.

compute_hash_v5 вывод меняется неожиданно. Третий аргумент (user_key) смешивается в хеш. Передайте b"" явно, когда вам нужен простой хеш проверки пароля, поскольку его опущение не то же самое, что передача пустого значения во всех точках вызова.


Часто задаваемые вопросы

Почему EncryptionUtils нельзя импортировать напрямую из aspose_pdf?

Он находится в подпакете engine, потому что является внутренней деталью реализации защиты паролем на уровне документа, а не частью публичного высокоуровневого интерфейса API. Импортируйте его явно из aspose_pdf.engine.encryption.

Является ли compute_hash_v5 тем же, что один вызов SHA-256?

Нет. Он реализует алгоритм 2.B стандарта ISO 32000-2, который выполняет несколько раундов, используя разные хеш-алгоритмы, выбранные на основе промежуточных значений — преднамеренно дороже, чем один вызов хеша.

Возвращает ли generate_file_id() одинаковые байты каждый раз?

Нет — он возвращает новое случайное 16-байтовое значение при каждом вызове. Другой идентификатор файла изменяет каждый ключ, полученный из него, даже если пароль одинаковый.

Все ли косвенные объекты в зашифрованном PDF используют один ключ?

Нет. derive_object_key объединяет номер объекта и номер генерации с ключом шифрования файла, поэтому каждый косвенный объект получает отдельный ключ для данного объекта.

Должен ли код приложения вызывать EncryptionUtils напрямую для установки пароля PDF?

Обычно нет. EncryptionUtils — это низкоуровневый движок, на котором построены API паролей уровня документа. Большинству приложений следует защищать PDF через API уровня документа и обращаться к EncryptionUtils только для пользовательских инструментов или судебно-технической проверки зашифрованных файлов.


См. также:

 Русский