Enterprise edition
Steganography — Deep Reference
At a glance
Section titled “At a glance”This deep reference documents the NextPDF Enterprise steganographic channel. The channel hides an encrypted payload inside the numeric kern adjustments of a TJ text-showing array. It has four public symbols: SteganographyEncoder, SteganographyDecoder, SteganographyConfig, and SteganographyCapacity. The encoder derives a key with HKDF-SHA-256, encrypts the payload with an AEAD cipher, and returns per-position kern offsets. The decoder reverses the process from observed adjustments or from a raw content stream.
The channel is designed for internal document leak tracing. It is not adversarial-grade steganography. Encoded data may be destroyed by print-then-scan, PDF conversion, re-linearization, content-stream rewriting, or any operation that normalizes kerning.
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. Compare editions and get a license.
Public API surface
Section titled “Public API surface”The channel exposes four final classes. All entry points are public static, except the SteganographyConfig constructor and its effectiveMaxOffset accessor. The supporting NextPDF\Enterprise\Security\Steganography\SteganographyEncryptionException is thrown by the encoder; it is not a caller-constructed type.
| Symbol | Parameters | Default behavior | Returns | Throws or fails with | Notes |
|---|---|---|---|---|---|
SteganographyEncoder::encode | $payload, $text, $fontKey, $metrics (FontMetrics), $secretKey, $config (SteganographyConfig) | Empty $payload returns []; asserts key strength; encrypts; computes per-position kern offsets. | array<int, float> (position => adjustment in 1/1000 em, AFM convention) | InvalidArgumentException (key below floor); OverflowException (text under 2 characters, or payload over capacity); SteganographyEncryptionException (AEAD failure) | Pass the result to NextPDF\Content\TextRenderer::buildTjArrayOperator(). The API returns AFM-convention adjustments; buildTjArrayOperator() performs the PDF TJ numeric conversion (ISO 32000-2 subtracts the number from the current position). Manual content-stream writers must preserve that sign convention. |
SteganographyEncoder::assertSecretKeyStrength | $secretKey | Rejects a key shorter than the floor. | void | InvalidArgumentException (key below floor) | Shared write-path guard, mirrored on the read path. |
SteganographyEncoder::MIN_SECRET_KEY_LENGTH | constant | The 128-bit key-length floor in bytes. | int (16) | Not applicable | The library enforces length, not entropy. |
SteganographyDecoder::decode | $observedAdjustments, $text, $fontKey, $metrics (FontMetrics), $secretKey, $config (SteganographyConfig) | Asserts key strength; quantizes deviations; rebuilds the blob; AEAD-decrypts. | `string | null(payload, ornull` on wrong key or no payload) | InvalidArgumentException (key below floor) |
SteganographyDecoder::decodeFromContentStream | $contentStream, $fontKey, $metrics (FontMetrics), $secretKey, $config (SteganographyConfig) | Tokenizes the stream, reconstructs text and adjustments from TJ arrays, then delegates to decode. | `string | null(payload, ornullwhen noTJ` text or decryption fails) | InvalidArgumentException (key below floor, via decode) |
SteganographyConfig::__construct | $bitDepth, $maxAdjustmentEmRatio, $cipher, $requirePdfACompatibility | Validates each argument domain; produces an immutable value object. | SteganographyConfig instance | InvalidArgumentException (invalid $bitDepth, $maxAdjustmentEmRatio, or $cipher) | readonly class; the four arguments are public promoted properties. |
SteganographyConfig::effectiveMaxOffset | none | Returns $maxAdjustmentEmRatio * 1000, halved when PDF/A compatibility is requested. | float (offset in 1/1000 em) | Not applicable | The halving reduces width-mismatch detection risk. |
SteganographyConfig::CRYPTO_OVERHEAD | constant | The fixed per-payload encryption overhead in bytes. | int (32) | Not applicable | 4-byte length, 12-byte nonce, 16-byte tag. |
SteganographyCapacity::calculate | $text, $config (SteganographyConfig) | Computes usable payload bytes for the text, after overhead. | int (0 when the text is too short) | Not applicable | Capacity is positions * bitDepth / 8 minus overhead. |
SteganographyCapacity::minimumTextLength | $payloadBytes, $config (SteganographyConfig) | Computes the minimum UTF-8 character count for a payload. | int (character count) | Not applicable | Inverse of calculate. |
The verbatim signatures follow, each with source provenance.
public static function encode( string $payload, string $text, string $fontKey, FontMetrics $metrics, string $secretKey, SteganographyConfig $config = new SteganographyConfig(),): arraypublic static function assertSecretKeyStrength(string $secretKey): voidpublic const int MIN_SECRET_KEY_LENGTH = 16;public static function decode( array $observedAdjustments, string $text, string $fontKey, FontMetrics $metrics, string $secretKey, SteganographyConfig $config = new SteganographyConfig(),): ?stringpublic static function decodeFromContentStream( string $contentStream, string $fontKey, FontMetrics $metrics, string $secretKey, SteganographyConfig $config = new SteganographyConfig(),): ?stringpublic function __construct( public int $bitDepth = 1, public float $maxAdjustmentEmRatio = 0.02, public string $cipher = 'aes-256-gcm', public bool $requirePdfACompatibility = false,)public function effectiveMaxOffset(): floatpublic const int CRYPTO_OVERHEAD = 32;public static function calculate( string $text, SteganographyConfig $config = new SteganographyConfig(),): intpublic static function minimumTextLength( int $payloadBytes, SteganographyConfig $config = new SteganographyConfig(),): intBehavior contract
Section titled “Behavior contract”The encoder splits $text into UTF-8 characters and forms one position per consecutive character pair. Each position carries $config->bitDepth bits, which is one or two. The payload is first encrypted, then serialized to a blob, then converted to a bit sequence. Each position encodes its bits as a small non-negative offset added to the natural kern value for that character pair.
The offset is a fraction of the effective maximum offset. The effective maximum offset is $maxAdjustmentEmRatio * 1000 design units, halved when $requirePdfACompatibility is true. The natural kern is read from $metrics through FontMetrics::getKernPair. The returned map is sparse: a position whose final adjustment is exactly zero is omitted.
Encryption uses HKDF-SHA-256 to derive a 32-byte key. The HKDF salt is the non-secret $fontKey and the info label is a fixed constant. Therefore the caller’s $secretKey is the sole confidentiality boundary. The AEAD cipher is aes-256-gcm or chacha20-poly1305, selected by $config->cipher, run through openssl_encrypt with a fresh 12-byte nonce and a 16-byte tag. The serialized blob is a 4-byte big-endian length, the 12-byte nonce, the ciphertext, and the 16-byte tag; this fixed overhead is CRYPTO_OVERHEAD, which is 32 bytes.
The decoder reverses the transform. It computes the deviation of each observed adjustment from the natural kern, normalizes by the effective maximum offset, and quantizes to the nearest level. It reassembles the blob, validates the length header, and calls openssl_decrypt. A wrong key, a missing payload, or corrupted adjustments cause the AEAD authentication to fail, and the decoder returns null. decodeFromContentStream first tokenizes the raw stream with NextPDF\Pro\Projection\ContentProjectionWriter::tokenize, reconstructs the text and the numeric adjustments from each TJ array, and then delegates to decode.
SteganographyCapacity::calculate reports the usable payload size for a text and configuration, after subtracting CRYPTO_OVERHEAD; it returns zero when the text is too short. SteganographyCapacity::minimumTextLength is the inverse: the smallest UTF-8 character count that admits a payload of the requested size.
Edge cases & failure modes
Section titled “Edge cases & failure modes”- An empty
$payloadreturns an empty map fromencode; no bytes are written, and the key-strength guard is not reached. - For a non-empty payload, a
$textwith fewer than two characters raisesOverflowExceptioninencode(an empty payload short-circuits to[]before the length check); the same text yieldsnullindecodeand zero inSteganographyCapacity::calculate. - A
$payloadlarger than the text capacity raisesOverflowExceptionbefore any adjustment is emitted. - A
$secretKeyshorter thanMIN_SECRET_KEY_LENGTH(16 bytes) raisesInvalidArgumentExceptionon both the write and the read path. This is a contract violation, distinct from a normal wrong-key miss. - A wrong key, a corrupted adjustment set, or a truncated blob causes
decodeto returnnullthrough AEAD authentication failure, not an exception. - Positions absent from a sparse
$observedAdjustmentsmap are treated as a zero deviation during extraction. decodeFromContentStreamreturnsnullwhen the stream contains noTJtext.- The channel is fragile by design. Print-then-scan, PDF conversion, re-linearization, content-stream rewriting, or kerning normalization can destroy the encoded data. It is unsuitable for adversarial or archival use.
FIPS-mode behavior
Section titled “FIPS-mode behavior”The channel uses HKDF-SHA-256 for key derivation and one AEAD cipher for confidentiality and integrity. The module does not enforce a FIPS profile; cipher selection is the caller’s decision through $config->cipher. aes-256-gcm is AES in Galois/Counter Mode, an authenticated-encryption mode built on an approved 128-bit block cipher whose conformance is validated under the CMVP, per NIST SP 800-38D §2. chacha20-poly1305 is not defined by a NIST mode-of-operation recommendation, so a FIPS-constrained OpenSSL provider rejects it; openssl_encrypt then returns false and the encoder raises SteganographyEncryptionException. Whether a deployment meets a FIPS requirement is the operator’s determination against its validated provider.
Conformance
Section titled “Conformance”The embedding writes numeric elements into a TJ text-showing array. Per ISO 32000-2:2020 §9.4.3, a TJ array shows text and lets a numeric element adjust the glyph position; the number is expressed in thousandths of a text-space unit and is subtracted from the current position. After a glyph is painted, the text matrix is translated by the combined displacement, so a positioning number shifts the placement of subsequent glyphs — ISO 32000-2:2020 §9.4.4. The channel adds its offsets to the natural kern values in the same 1/1000 em (AFM) convention, where a negative value tightens spacing.
The AEAD grounding is limited to primitive selection: aes-256-gcm corresponds to the GCM mode of NIST SP 800-38D §2, which identifies the algorithm.
All clauses are paraphrased. Structural alignment with the TJ positioning model is a capability statement. The channel is designed for internal leak tracing.
Development notes
Section titled “Development notes”- Entry points are
public staticmethods inNextPDF\Enterprise\Security\Steganography, except theSteganographyConfigconstructor andeffectiveMaxOffset. SteganographyConfigis afinal readonlyvalue object. Its four properties are immutable after construction, and its argument domains are validated in the constructor:$bitDepthis 1 or 2,$maxAdjustmentEmRatiois in(0, 0.05], and$cipherisaes-256-gcmorchacha20-poly1305.- The encode output is consumed by
NextPDF\Content\TextRenderer::buildTjArrayOperator. Kern pairs come fromNextPDF\Typography\FontMetrics. Content-stream decoding reads throughNextPDF\Pro\Projection\ContentProjectionWriterand does not mutate the stream. - The key-length floor is enforced at the entry point and re-asserted at the private crypto boundary, so no internal path can reach HKDF with a weak key. The library enforces length, not entropy; supplying high-entropy key material is the integrator’s responsibility.
CRYPTO_OVERHEAD(32 bytes) is the fixed cost per payload and is already subtracted bySteganographyCapacity::calculate.- The documented since is
3.1.0for the aggregated Enterprise surface.SteganographyEncryptionExceptionextendsRuntimeException, so call sites that catch the generic runtime type continue to work.
See also
Section titled “See also”- Steganography (capability page) — the task-oriented overview of the leak-tracing channel.
- Security — Deep Reference — the sibling Enterprise security surface.
- Licensing and activation — how the Enterprise license envelope is applied.
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.