tc-lib-pdf-encrypt

Technical overview and integration notes for tc-lib-pdf-encrypt

Overview

tc-lib-pdf-encrypt implements core PDF encryption routines, including password handling and permission flag control.

The security mechanics of the PDF specification sit behind a small API, so a document generator applies a policy without touching the cryptography underneath it.

Repository and API Docs

Project Metadata

ItemValue
Namespace\Com\Tecnick\Pdf\Encrypt
LicenseGNU LGPL v3

Installation

composer require tecnickcom/tc-lib-pdf-encrypt

Where It Fits

When a generated PDF has to carry an access policy: an open or owner password, restrictions on printing and modification, or certificate-based recipients.

Security Notice

RC4 modes (0 and 1) are cryptographically broken and deprecated. RC4-40 (mode 0) and RC4-128 (mode 1) are no longer considered secure. Both modes emit an E_USER_DEPRECATED notice at runtime. Use AES-128 (mode 2), AES-256 R5 (mode 3), or AES-256 R6 / PDF 2.0 (mode 4) for all new documents.

ModeAlgorithmSecurity
0RC4-40Broken: do not use
1RC4-128Broken: do not use
2AES-128Acceptable for legacy compatibility
3AES-256 R5 (PDF 1.7 ext.)Recommended
4AES-256 R6 (PDF 2.0 / ISO 32000-2)Recommended (most current)

Features

Encryption

  • RC4 and AES variants for PDF object/string encryption (modes 0–4; see Security Notice above)
  • AES-256 R6 (PDF 2.0 / ISO 32000-2, mode 4) support with Algorithm 2.B (ISO 32000-2 §7.6.4.3.4) key derivation
  • User and owner password workflows
  • Permission flag handling for document operations
  • Optional metadata encryption control ($encryptMetadata, requires mode 2, 3 or 4)
  • Embedded-file crypt filter selection ($encryptEmbeddedFiles, /EFF dictionary entry, V 4 and V 5 only). False writes /EFF /Identity, and the caller must then write those streams without calling encryptString() on them.
  • Public-key (certificate) encryption for multiple recipients, with signed permissions

Decryption

  • Password authentication for all encryption modes (RC4-40, RC4-128, AES-128, AES-256 R5/R6)
  • Public-key (PKCS#7 / S/MIME) decryption for recipient private keys, including passphrase-protected private keys
  • Per-object key derivation for RC4 and AES-128 streams, with object generation numbers
  • Decrypt::fromEncryptionDictionary() maps /V, /R and /CFM to the algorithm, and recognizes the public-key handler, which carries no /R, from /Filter or /Recipients
  • Round-trip decryptString() companion to encryptString()
  • /Perms verification for AES-256: a document whose permission bits were rewritten fails authentication
  • getAuthenticatedRole() reports whether the user or the owner password matched. The owner password is tried first, so a document that uses one string for both is reported as the owner.
  • getRecipientPermissions() returns the permission bits of the matching recipient in public-key mode, where the document carries no /P entry

Interoperability

Encryption dictionaries are verified against qpdf fixtures and against vectors computed from ISO 32000, in both directions, for every revision from R2 to R6.

Passwords are used as supplied. ISO 32000-2 calls for SASLprep (RFC 4013) on R5/R6 passwords and ISO 32000-1 expects PDFDocEncoding for R2 to R4; neither is applied, so non-ASCII passwords may not match other implementations. ASCII passwords are unaffected.

Integration

  • Designed for direct use by PDF writer and reader components
  • Object numbers and generation numbers are both taken into account by getObjectKey(), encryptString(), decryptString(), escapeDataString() and getFormattedDate()
  • Helpers for PDF date formatting and hexadecimal/string conversion
  • Exception-driven error handling

OpenSSL 3 Note

On OpenSSL 3 systems, legacy providers may be disabled by default. Enable legacy support only when required by your runtime policy.

Requirements

  • PHP 8.2 or later
  • Extensions: ctype, hash, openssl, pcre
  • Composer

Integration Notes

The mode decides which readers can open the file. Mode 4 needs a reader that implements ISO 32000-2, mode 3 one that implements the PDF 1.7 AES-256 extension, and mode 2 opens almost anywhere. Start at mode 4 and step down only if a reader in your recipient list forces it; the RC4 modes are broken and emit a deprecation notice.

Keep certificates and private keys out of the application source tree and out of container images. Mount them, or fetch them from a secret store at run time.

PDF permission flags are advisory. A cooperating reader honours them; nothing stops a determined one from ignoring them. Treat them as a statement of intent, and use encryption where the content genuinely has to stay unreadable.

Example

<?php

require_once __DIR__ . '/vendor/autoload.php';

// AES-256 R6 (mode 4, recommended)
$encrypt = new \Com\Tecnick\Pdf\Encrypt\Encrypt(
    enabled: true,
    file_id: md5('unique-file-id'),
    mode: 4,
    // permissions lists what to BLOCK: this document cannot be printed or copied
    permissions: ['print', 'copy'],
    user_pass: 'userpassword',
    owner_pass: 'ownerpassword',
);

$objectNumber = 1;
$cipher = $encrypt->encryptString('secret payload', $objectNumber);

echo bin2hex($cipher);

// The first element of the trailer /ID array must carry this value:
// revisions 2 to 4 derive the key from it.
echo $encrypt->getFileId();

Round-tripping the same payload through Decrypt:

<?php

// Pass the encryption dictionary produced by the Encrypt instance.
$decrypt = new \Com\Tecnick\Pdf\Encrypt\Decrypt($encrypt->getEncryptionData());

if ($decrypt->authenticate('userpassword')) {
    echo $decrypt->getAuthenticatedRole(); // 'user', 'owner' or 'recipient'

    // For AES modes the PKCS#7 padding is stripped automatically, so the exact
    // original plaintext is returned. RC4 modes are symmetric.
    echo $decrypt->decryptString($cipher, $objectNumber, $generationNumber = 0);
}

A document written elsewhere is decrypted by passing the raw dictionary entries and letting the library derive the mode from /V, /R and /CFM:

<?php

$decrypt = \Com\Tecnick\Pdf\Encrypt\Decrypt::fromEncryptionDictionary([
    'V' => 5,
    'R' => 6,
    'O' => $oBytes,      // raw binary, not hexadecimal
    'U' => $uBytes,
    'OE' => $oeBytes,
    'UE' => $ueBytes,
    'Perms' => $permsBytes,
    'P' => -3904,
    'fileid' => $firstTrailerIdElement,
]);

Development and Packaging

  • QA and local checks: make deps, make help, make qa
  • Coverage report: make qa-coverage
  • Packaging: make rpm, make deb

Support and Contribution