Skip to content

Advanced Encryption

Pro — Commercial License Required
Advanced encryption internals require the Pro package.

This page documents the internal encryption implementation in TCPDF-Next Pro. It covers the AES-256 AESV3 handler, the key derivation algorithm, password normalization, and secure parameter handling. If you are looking for basic encryption usage, see the AES-256 Encryption example.

AES-256 with AESV3 Handler

TCPDF-Next Pro implements the ISO 32000-2 (PDF 2.0) Standard Security Handler revision 6, which mandates AES-256-CBC for all stream and string encryption. The handler is identified by /V 5 and /R 6 in the encryption dictionary.

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

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

Why No RC4 or AES-128

TCPDF-Next Pro deliberately excludes RC4 (40-bit and 128-bit) and AES-128:

AlgorithmReason for Exclusion
RC4-40Broken since 1995; trivially attacked
RC4-128Biases in keystream; prohibited by PDF 2.0
AES-128Superseded by AES-256 in revision 6; not forward-compatible

PDF 2.0 (ISO 32000-2:2020) requires AESV3 for new documents. Supporting weaker algorithms would compromise the security posture and violate the specification.

Algorithm 2.B: Key Derivation

The file encryption key is derived from the password using Algorithm 2.B (ISO 32000-2, clause 7.6.4.3.4). This is an iterative process that chains SHA-256, SHA-384, and 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

This iterative hashing makes brute-force attacks computationally expensive while remaining fast enough for legitimate use.

Key Components in the Encryption Dictionary

EntryLengthPurpose
/O48 bytesOwner password validation (hash + validation salt)
/U48 bytesUser password validation (hash + validation salt)
/OE32 bytesOwner-encrypted file encryption key
/UE32 bytesUser-encrypted file encryption key
/Perms16 bytesAES-256 encrypted permission flags

SASLprep Password Normalization

Before any cryptographic operation, passwords are normalized using SASLprep (RFC 4013), which is a profile of stringprep (RFC 3454). This ensures consistent password handling regardless of the Unicode normalization form used by the operating system or input method.

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.

What SASLprep Does

  1. Map -- Commonly mapped-to-nothing characters (e.g., soft hyphen U+00AD) are removed.
  2. Normalize -- The string is converted to Unicode NFC (Canonical Decomposition followed by Canonical Composition).
  3. Prohibit -- Characters from RFC 3454 Table C.1.2 through C.9 are rejected (control characters, private use, surrogates, non-characters, etc.).
  4. Bidirectional check -- Strings with both left-to-right and right-to-left characters are validated per RFC 3454 clause 6.

This means a user typing U+00FC (LATIN SMALL LETTER U WITH DIAERESIS) and another typing U+0075 U+0308 (LATIN SMALL LETTER U + COMBINING DIAERESIS) will produce the same encryption key.

Permission Encoding

Permissions are stored in the /Perms entry as a 16-byte AES-256-ECB encrypted block. The plaintext layout is:

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

The permission flags follow the same bit layout as defined in ISO 32000-2 Table 22.

Encrypted Stream Handling

Every content stream and string in the PDF is individually encrypted:

  1. A unique 16-byte Initialization Vector (IV) is generated per stream/string using random_bytes(16).
  2. The IV is prepended to the ciphertext.
  3. PKCS#7 padding is applied before encryption.
  4. The /Crypt filter with /AESV3 is set on all stream decode parameters.
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 always encrypts stream data. The /EncryptMetadata flag defaults to true. If set to false, the XMP metadata stream remains unencrypted (useful for search indexing), but all other streams are still encrypted.

Sensitive Parameter Handling

All methods that accept passwords are annotated with PHP 8.2's #[\SensitiveParameter] attribute. This prevents passwords from appearing in stack traces, debug output, and error logs:

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

If an exception occurs, the stack trace will show Object(SensitiveParameterValue) instead of the actual password string.

Full Example

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');

Released under the LGPL-3.0-or-later License.