Enterprise edition
Digital watermarking and steganographic embedding
At a glance
Section titled “At a glance”NextPDF Enterprise embeds a hidden, encrypted payload into a generated PDF by making small, controlled adjustments to the spacing between letter pairs. You supply a payload — typically a per-recipient identifier — and a secret key; the encoder writes the payload as imperceptible deviations from the natural kerning of the text. A matching decoder, given the same key, recovers the payload. This page is behaviour-level: it states what the encoder writes, the cryptography it uses, and the boundary of the technique.
The intended use is internal document leak tracing: when a controlled document leaks, the recovered marker identifies the recipient copy.
Prerequisites are stated in the front matter and repeated under Prerequisites.
Availability & licensing
Section titled “Availability & licensing”This capability ships in NextPDF Enterprise (nextpdf/enterprise) and activates with an Enterprise-tier license envelope. A deployment without that entitlement does not load the capability’s classes. The capability runs entirely in process during document generation; no document content leaves the host. Compare editions and get a license.
What this capability does
Section titled “What this capability does”PDF text drawn with a kerning array carries a numeric adjustment between glyphs. The encoder treats each adjustment position as a carrier for a few bits:
- It encrypts the payload with an authenticated-encryption-with-associated-data (AEAD) cipher — AES-256-GCM by default, or ChaCha20-Poly1305. AEAD provides both confidentiality and integrity, so a tampered carrier fails decryption rather than yielding a wrong payload silently.
- It derives the 32-byte encryption key from your secret key and the font key using the HMAC-based Key Derivation Function (HKDF) with SHA-256. HKDF extracts a fixed-length pseudorandom key from the input keying material, then expands it to the required length, per RFC 5869 §2.
- It generates a fresh random 12-byte initialization vector (IV) per encryption. AES-GCM requires the IV to be unique for a given key, or the authentication assurance is lost, per NIST SP 800-38D §5.2.1.
- It maps the encrypted bytes to a bit sequence and distributes the bits across the available letter-pair positions, encoding one or two bits per position. The deviation it adds to the natural kerning is bounded by a configurable fraction of the em — small enough to stay visually imperceptible.
The decoder reverses the process: it reads the kerning adjustments from a content stream, subtracts the natural kerning, quantises the deviations back to bits, reassembles the encrypted blob, and decrypts it with the same key. If the key is wrong or the carrier was destroyed, decryption returns nothing rather than a wrong payload.
Capacity scales with text length: each letter-pair position carries one or two bits, so a payload must fit the positions the text provides. The encoder raises a typed overflow error when the payload exceeds capacity.
A PDF/A-compatibility mode halves the maximum deviation to stay under a validator’s width-tolerance threshold, trading capacity for stricter conformance.
Why it works this way
Section titled “Why it works this way”The load-bearing choice is to hide the marker in kerning rather than in a visible overlay or a metadata field. A metadata marker is trivial to strip, and a visible stamp alters the page. Kerning deviations instead ride inside the text the recipient must keep, and stay imperceptible. Authenticated encryption is the second pillar: a tampered or partial carrier fails authentication, so the decoder returns nothing rather than a wrong recipient. The key is derived per font with HKDF, binding the marker to the document context, not to a bare shared secret. Its robustness follows directly: the marker survives ordinary redistribution but not deliberate content-stream rewriting, so its scope is internal leak tracing.
Design background: Redaction is not a black rectangle.
Prerequisites
Section titled “Prerequisites”- Install NextPDF Core and the Enterprise package, and hold an active Enterprise license.
- Generate the document with a font that exposes kerning-pair metrics; the encoder reads natural kerning from the font metrics.
- Supply the secret key from your secret manager, not from source. The same key is required to decode.
- Decide the bit depth (one or two bits per position) and whether PDF/A compatibility is required, based on your capacity and conformance needs.
Configuration
Section titled “Configuration”The encoding configuration is immutable and validated on construction:
- Bit depth — one or two bits per letter-pair position. Higher depth gives more capacity but larger deviations.
- Maximum adjustment ratio — the deviation ceiling as a fraction of the em, within a bounded range. Larger values give more headroom but risk visibility.
- Cipher — AES-256-GCM (default) or ChaCha20-Poly1305. Both are AEAD.
- PDF/A compatibility — when enabled, halves the effective maximum deviation.
Use the same configuration for encoding and decoding; a mismatch yields no recovered payload.
Step-by-step
Section titled “Step-by-step”- Read the secret key from your secret manager.
- Build the encoding configuration (bit depth, deviation ratio, cipher, PDF/A flag).
- Compute the kerning adjustments for the text you are about to render, passing the payload, the text, the font key, the font metrics, the secret key, and the configuration.
- Apply the returned adjustments when you write the text run, so the marker is embedded during generation.
- To trace a leaked copy, run the decoder over the suspect document’s content stream with the same font key, font metrics, secret key, and configuration, and read the recovered payload.
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
/** * Reject a payload that cannot fit the carrier text before encoding. * * Each letter-pair position carries $bitDepth bits. Guarding capacity up * front turns an unencodable payload into a clear caller-side error instead * of relying on the encoder's overflow exception alone. * * @param non-empty-string $payload The bytes to embed (already minimal). * @param positive-int $textLength The character count of the carrier text. * @param int<1, 2> $bitDepth Bits encoded per letter-pair position. * * @throws \OverflowException When the payload cannot fit the available positions. */function assertPayloadFits(string $payload, int $textLength, int $bitDepth): void{ $positions = $textLength - 1; $capacityBytes = \intdiv($positions * $bitDepth, 8);
if (\strlen($payload) > $capacityBytes) { throw new \OverflowException(\sprintf( 'Payload of %d bytes exceeds carrier capacity of %d bytes.', \strlen($payload), $capacityBytes, )); }}<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Enterprise\Security\Steganography\SteganographyDecoder;use NextPDF\Enterprise\Security\Steganography\SteganographyConfig;use NextPDF\Typography\FontMetrics;use Psr\Log\LoggerInterface;
final readonly class LeakTracer{ public function __construct(private LoggerInterface $logger) {}
/** * Recover the embedded marker from a suspect document's content stream. * * Decoding returns null on a wrong key or a destroyed carrier rather than * a wrong payload, so the caller treats null as "no marker recovered". * * @param string $contentStream The suspect content-stream bytes. * @param non-empty-string $fontKey The font key used at generation. * @param FontMetrics $metrics Font metrics with kerning pairs. * @param string $secretKey The same secret key used to encode. * @param SteganographyConfig $config The same configuration used to encode. * * @return string|null The recovered marker, or null when none is found. */ public function trace( string $contentStream, string $fontKey, FontMetrics $metrics, string $secretKey, SteganographyConfig $config, ): ?string { $marker = SteganographyDecoder::decodeFromContentStream( $contentStream, $fontKey, $metrics, $secretKey, $config, );
if ($marker === null) { $this->logger->info('No steganographic marker recovered from content stream.'); }
return $marker; }}Verification
Section titled “Verification”- Encode a known payload into a known text run, then decode it back with the same key and configuration; confirm the recovered payload matches.
- Decode with a deliberately wrong key and confirm the result is null, not a wrong payload — this is the AEAD integrity guarantee at work.
- Inspect the rendered page and confirm the spacing change is not visually apparent at the configured deviation ratio.
- When PDF/A compatibility is required, validate the output against your PDF/A profile and confirm the width tolerance is not tripped.
Security and compliance
Section titled “Security and compliance”- Authenticated encryption. The payload is encrypted with AES-256-GCM or ChaCha20-Poly1305. A tampered or truncated carrier fails authentication on decryption; it does not yield a wrong payload.
- Per-encryption IV. A fresh random 12-byte IV is generated for every encryption, satisfying the AES-GCM uniqueness requirement per NIST SP 800-38D §5.2.1.
- Derived key. The encryption key is derived with HKDF-SHA-256 from your secret and the font key (RFC 5869 §2). Keep the secret in your secret manager; treat it like any signing secret.
- The marker is document content. The embedded bytes are part of the page content, not log content. Do not write the payload or the secret key to logs.
Robustness
Section titled “Robustness”The marker is carried in kerning adjustments. It may be destroyed by printing and rescanning, by PDF conversion tools, by re-linearisation, or by any content-stream rewriting that normalises kerning. The technique is best suited to internal leak tracing of documents distributed in their generated form.
Failure handling
Section titled “Failure handling”- Payload too large. The encoder raises a typed overflow error when the payload exceeds the text capacity. Shorten the payload or lengthen the carrier text.
- Too little carrier text. Text shorter than two characters offers no carrier position and raises an error.
- Wrong key on decode. Decoding returns null. Treat null as “no marker recovered”, not as a partial result.
- Configuration mismatch. Encoding and decoding must use the same bit depth, deviation ratio, cipher, and PDF/A flag; a mismatch yields no recovered payload.
Publication boundary
Section titled “Publication boundary”This page documents externally observable behavior and the supported public API surface only. Internal namespace paths, helper classes, mechanism tables, runbook filenames, and ticket prefixes are out of scope.
See also
Section titled “See also”- Steganography — NextPDF Enterprise — the API reference for the steganographic encoder and decoder.
- Security — NextPDF Enterprise — the combined Enterprise security surface.
- Branding — NextPDF Enterprise — visible watermarks and on-page stamps.
- Forensics — NextPDF Enterprise — document examination and tracing.
- Security — NextPDF Core — the core encryption and signature surface.
- AEAD · HKDF · kerning — glossary terms.