Skip to content

Продвинутое шифрование

Pro — Commercial License Required
Продвинутые внутренние механизмы шифрования требуют пакет Pro.

На этой странице описана внутренняя реализация шифрования в TCPDF-Next Pro. Она охватывает обработчик AES-256 AESV3, алгоритм деривации ключей, нормализацию паролей и безопасную обработку параметров. Для базового использования шифрования см. Шифрование AES-256.

AES-256 с обработчиком AESV3

TCPDF-Next Pro реализует Standard Security Handler ISO 32000-2 (PDF 2.0) ревизии 6, который обязывает использовать AES-256-CBC для шифрования всех потоков и строк. Обработчик идентифицируется как /V 5 и /R 6 в словаре шифрования.

php
use Yeeefang\TcpdfNext\Pro\Security\Aes256Encryptor;

$encryptor = new Aes256Encryptor(
    ownerPassword: 'Str0ng!OwnerP@ss',
    userPassword:  'reader2026',
);

Почему нет RC4 или AES-128

TCPDF-Next Pro намеренно исключает RC4 (40-бит и 128-бит) и AES-128:

АлгоритмПричина исключения
RC4-40Взломан с 1995 года; тривиально атакуем
RC4-128Смещения в потоке ключей; запрещён PDF 2.0
AES-128Заменён AES-256 в ревизии 6; не совместим вперёд

PDF 2.0 (ISO 32000-2:2020) требует AESV3 для новых документов. Поддержка более слабых алгоритмов подорвала бы безопасность и нарушила спецификацию.

Algorithm 2.B: деривация ключей

Ключ шифрования файла деривируется из пароля с помощью Algorithm 2.B (ISO 32000-2, clause 7.6.4.3.4). Это итеративный процесс, объединяющий SHA-256, SHA-384 и SHA-512:

function computeHash(password, salt, userKey = ''):
    K = SHA-256(password || salt || userKey)

    round = 0
    lastByte = 0

    while round < 64 OR lastByte > round - 32:
        K1 = (password || K || userKey) repeated 64 times
        E  = AES-128-CBC(key = K[0..15], iv = K[16..31], data = K1)

        mod3 = (sum of all bytes in E) mod 3
        if   mod3 == 0: K = SHA-256(E)
        elif mod3 == 1: K = SHA-384(E)
        else:           K = SHA-512(E)

        lastByte = E[len(E) - 1]
        round += 1

    return K[0..31]   // 32-byte file encryption key

Этот итеративный хешинг делает атаки перебором вычислительно затратными, оставаясь при этом достаточно быстрым для легитимного использования.

Компоненты ключей в словаре шифрования

ЗаписьДлинаНазначение
/O48 байтВалидация пароля владельца (хеш + соль валидации)
/U48 байтВалидация пароля пользователя (хеш + соль валидации)
/OE32 байтаКлюч шифрования файла, зашифрованный паролем владельца
/UE32 байтаКлюч шифрования файла, зашифрованный паролем пользователя
/Perms16 байтФлаги разрешений, зашифрованные AES-256

Нормализация паролей SASLprep

Перед любой криптографической операцией пароли нормализуются с помощью SASLprep (RFC 4013), который является профилем stringprep (RFC 3454). Это обеспечивает единообразную обработку паролей независимо от формы нормализации Unicode, используемой операционной системой или методом ввода.

php
use Yeeefang\TcpdfNext\Pro\Security\SaslPrep;

$normalized = SaslPrep::prepare('P\u{00E4}ssw\u{00F6}rd');
// Normalizes composed/decomposed forms, maps certain characters,
// and rejects prohibited codepoints.

Что делает SASLprep

  1. Маппинг -- Символы, обычно отображаемые в ничто (например, мягкий дефис U+00AD), удаляются.
  2. Нормализация -- Строка преобразуется в Unicode NFC (каноническая декомпозиция с последующей канонической композицией).
  3. Запрет -- Символы из RFC 3454 Table C.1.2 -- C.9 отклоняются (управляющие символы, private use, суррогаты, не-символы и т.д.).
  4. Проверка двунаправленности -- Строки с символами как слева направо, так и справа налево валидируются по RFC 3454 clause 6.

Это означает, что пользователь, вводящий U+00FC (LATIN SMALL LETTER U WITH DIAERESIS), и другой пользователь, вводящий U+0075 U+0308 (LATIN SMALL LETTER U + COMBINING DIAERESIS), получат одинаковый ключ шифрования.

Кодирование разрешений

Разрешения хранятся в записи /Perms как 16-байтовый блок, зашифрованный AES-256-ECB. Структура открытого текста:

Bytes 0-3:   Permission flags (little-endian int32)
Bytes 4-7:   0xFFFFFFFF
Byte  8:     'T' if EncryptMetadata is true, 'F' otherwise
Bytes 9-11:  'adb'
Bytes 12-15: Random padding

Флаги разрешений следуют той же битовой раскладке, определённой в ISO 32000-2 Table 22.

Обработка зашифрованных потоков

Каждый поток контента и строка в PDF шифруются индивидуально:

  1. Уникальный 16-байтовый вектор инициализации (IV) генерируется для каждого потока/строки с помощью random_bytes(16).
  2. IV предваряет шифротекст.
  3. Перед шифрованием применяется выравнивание PKCS#7.
  4. Фильтр /Crypt с /AESV3 устанавливается для всех параметров декодирования потоков.
php
// Internal -- handled automatically by the writer
$iv        = random_bytes(16);
$padded    = pkcs7_pad($plaintext, blockSize: 16);
$encrypted = openssl_encrypt($padded, 'aes-256-cbc', $fileKey, OPENSSL_RAW_DATA, $iv);
$output    = $iv . $encrypted;

WARNING

TCPDF-Next Pro всегда шифрует данные потоков. Флаг /EncryptMetadata по умолчанию равен true. Если установить в false, поток метаданных XMP остаётся незашифрованным (полезно для поисковой индексации), но все остальные потоки по-прежнему шифруются.

Безопасная обработка параметров

Все методы, принимающие пароли, аннотированы атрибутом PHP 8.2 #[\SensitiveParameter]. Это предотвращает появление паролей в стек-трейсах, отладочном выводе и логах ошибок:

php
public function setOwnerPassword(
    #[\SensitiveParameter] string $password,
): self {
    $this->ownerPassword = SaslPrep::prepare($password);
    return $this;
}

При возникновении исключения стек-трейс покажет Object(SensitiveParameterValue) вместо фактической строки пароля.

Полный пример

php
use Yeeefang\TcpdfNext\Core\Document;
use Yeeefang\TcpdfNext\Pro\Security\Aes256Encryptor;
use Yeeefang\TcpdfNext\Pro\Security\Permissions;

$pdf = Document::create()
    ->setTitle('Confidential Report')
    ->addPage()
    ->setFont('Helvetica', size: 12)
    ->multiCell(0, 6, 'This document is protected with AES-256 encryption.');

$encryptor = new Aes256Encryptor(
    ownerPassword: 'Str0ng!OwnerP@ss',
    userPassword:  'reader2026',
    permissions:   new Permissions(
        print:            true,
        printHighQuality: false,
        copy:             false,
        modify:           false,
        annotate:         true,
        fillForms:        true,
        extractForAccess: true,
        assemble:         false,
    ),
);

$pdf->encrypt($encryptor)
    ->save(__DIR__ . '/encrypted.pdf');

Распространяется по лицензии LGPL-3.0-or-later.