Skip to content

Cifrado avanzado

Pro — Commercial License Required
Los internos de cifrado avanzado requieren el paquete Pro.

Esta página documenta la implementación interna de cifrado en TCPDF-Next Pro. Cubre el handler AES-256 AESV3, el algoritmo de derivación de claves, la normalización de contraseñas y el manejo seguro de parámetros. Si buscas el uso básico de cifrado, consulta el ejemplo de cifrado AES-256.

AES-256 con handler AESV3

TCPDF-Next Pro implementa el Standard Security Handler de ISO 32000-2 (PDF 2.0) revisión 6, que exige AES-256-CBC para todo el cifrado de streams y cadenas. El handler se identifica por /V 5 y /R 6 en el diccionario de cifrado.

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

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

Por qué no RC4 ni AES-128

TCPDF-Next Pro excluye deliberadamente RC4 (40-bit y 128-bit) y AES-128:

AlgoritmoRazón de exclusión
RC4-40Roto desde 1995; atacado trivialmente
RC4-128Sesgos en el keystream; prohibido por PDF 2.0
AES-128Superado por AES-256 en revisión 6; no compatible hacia adelante

PDF 2.0 (ISO 32000-2:2020) requiere AESV3 para documentos nuevos. Soportar algoritmos más débiles comprometería la postura de seguridad y violaría la especificación.

Algorithm 2.B: Derivación de claves

La clave de cifrado de archivo se deriva de la contraseña usando Algorithm 2.B (ISO 32000-2, cláusula 7.6.4.3.4). Es un proceso iterativo que encadena SHA-256, SHA-384 y 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

Este hashing iterativo hace que los ataques de fuerza bruta sean computacionalmente costosos mientras permanece lo suficientemente rápido para uso legítimo.

Componentes clave en el diccionario de cifrado

EntradaLongitudPropósito
/O48 bytesValidación de contraseña del propietario (hash + salt de validación)
/U48 bytesValidación de contraseña de usuario (hash + salt de validación)
/OE32 bytesClave de cifrado de archivo cifrada con propietario
/UE32 bytesClave de cifrado de archivo cifrada con usuario
/Perms16 bytesFlags de permisos cifrados con AES-256

Normalización de contraseñas SASLprep

Antes de cualquier operación criptográfica, las contraseñas se normalizan usando SASLprep (RFC 4013), que es un perfil de stringprep (RFC 3454). Esto asegura un manejo consistente de contraseñas independientemente de la forma de normalización Unicode usada por el sistema operativo o método de entrada.

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.

Qué hace SASLprep

  1. Map -- Los caracteres comúnmente mapeados a nada (ej., guión suave U+00AD) se eliminan.
  2. Normalize -- La cadena se convierte a Unicode NFC (Canonical Decomposition seguida de Canonical Composition).
  3. Prohibit -- Los caracteres de las tablas RFC 3454 C.1.2 hasta C.9 se rechazan (caracteres de control, uso privado, surrogates, non-characters, etc.).
  4. Bidirectional check -- Las cadenas con caracteres tanto de izquierda a derecha como de derecha a izquierda se validan según la cláusula 6 de RFC 3454.

Esto significa que un usuario que escribe U+00FC (LATIN SMALL LETTER U WITH DIAERESIS) y otro que escribe U+0075 U+0308 (LATIN SMALL LETTER U + COMBINING DIAERESIS) producirán la misma clave de cifrado.

Codificación de permisos

Los permisos se almacenan en la entrada /Perms como un bloque cifrado AES-256-ECB de 16 bytes. El layout del texto plano es:

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

Los flags de permisos siguen el mismo layout de bits definido en ISO 32000-2 Tabla 22.

Manejo de streams cifrados

Cada content stream y cadena en el PDF se cifra individualmente:

  1. Un Initialization Vector (IV) único de 16 bytes se genera por stream/cadena usando random_bytes(16).
  2. El IV se antepone al texto cifrado.
  3. Se aplica padding PKCS#7 antes del cifrado.
  4. El filtro /Crypt con /AESV3 se establece en todos los parámetros de decodificación de stream.
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 siempre cifra los datos de stream. El flag /EncryptMetadata por defecto es true. Si se establece en false, el stream de metadatos XMP permanece sin cifrar (útil para indexación de búsqueda), pero todos los demás streams siguen cifrados.

Manejo de parámetros sensibles

Todos los métodos que aceptan contraseñas están anotados con el atributo #[\SensitiveParameter] de PHP 8.2. Esto previene que las contraseñas aparezcan en stack traces, salida de depuración y logs de error:

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

Si ocurre una excepción, el stack trace mostrará Object(SensitiveParameterValue) en lugar de la cadena de contraseña real.

Ejemplo completo

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

Distribuido bajo la licencia LGPL-3.0-or-later.