Advanced Encryption
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.
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:
| Algorithm | Reason for Exclusion |
|---|---|
| RC4-40 | Broken since 1995; trivially attacked |
| RC4-128 | Biases in keystream; prohibited by PDF 2.0 |
| AES-128 | Superseded 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 keyThis iterative hashing makes brute-force attacks computationally expensive while remaining fast enough for legitimate use.
Key Components in the Encryption Dictionary
| Entry | Length | Purpose |
|---|---|---|
/O | 48 bytes | Owner password validation (hash + validation salt) |
/U | 48 bytes | User password validation (hash + validation salt) |
/OE | 32 bytes | Owner-encrypted file encryption key |
/UE | 32 bytes | User-encrypted file encryption key |
/Perms | 16 bytes | AES-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.
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
- Map -- Commonly mapped-to-nothing characters (e.g., soft hyphen
U+00AD) are removed. - Normalize -- The string is converted to Unicode NFC (Canonical Decomposition followed by Canonical Composition).
- Prohibit -- Characters from RFC 3454 Table C.1.2 through C.9 are rejected (control characters, private use, surrogates, non-characters, etc.).
- 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 paddingThe 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:
- A unique 16-byte Initialization Vector (IV) is generated per stream/string using
random_bytes(16). - The IV is prepended to the ciphertext.
- PKCS#7 padding is applied before encryption.
- The
/Cryptfilter with/AESV3is set on all stream decode parameters.
// 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:
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
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');