跳转到内容
getnextpdf.com

Enterprise 版本

数字水印与隐写嵌入

NextPDF Enterprise 通过对字母对之间的间距做出微小、受控的调整,把一段隐藏的、加密的负载嵌入到所生成的 PDF 中。你提供一段负载——通常是一个按收件者划分的标识符——以及一个密钥;编码器将该负载写为对文本自然字距的不可察觉偏离。一个匹配的解码器在拿到同一个密钥后即可恢复该负载。本页处于行为层面:它陈述编码器写入什么、它使用的密码学,以及该技术的边界。

其预期用途是内部文件泄漏溯源:当一份受控文件泄漏时,所恢复的标记会指出泄漏的那份收件者副本。它不是对抗性隐写,也不保证能在任意处理中存活。

前置条件在 front matter 中陈述,并在前置条件下重复说明。

此能力随 NextPDF Enterprisenextpdf/enterprise)发布,并由一个 Enterprise 层级的许可信封激活。没有该授权的部署不会加载此能力的类。该能力在文件生成期间完全在进程内运行;没有任何文件内容离开宿主。对比各版本并获取许可

用一个字距数组绘制的 PDF 文本,在字形之间携带一个数值调整。编码器将每个调整位置当作可承载若干比特的载体:

  1. 它用一个带关联数据的认证加密(AEAD)密码——默认 AES-256-GCM,或 ChaCha20-Poly1305——加密该负载。AEAD 同时提供机密性与完整性,因此一个被篡改的载体会令解密失败,而非静默地产出一个错误的负载。
  2. 它使用带 SHA-256 的基于 HMAC 的密钥派生函数(HKDF),从你的密钥与字体密钥派生出 32 字节的加密密钥。HKDF 从输入密钥材料提取出一个固定长度的伪随机密钥,再将其扩展到所需长度,依据 RFC 5869 §2。
  3. 它为每次加密生成一个全新的随机 12 字节初始化向量(IV)。AES-GCM 要求对给定密钥而言 IV 是唯一的,否则认证保证会丧失,依据 NIST SP 800-38D §5.2.1。
  4. 它把加密后的字节映射为一个比特序列,并将这些比特分布到可用的字母对位置上,每个位置编码一或两个比特。它叠加到自然字距上的偏离量受一个可配置的 em 分数限制——小到足以保持视觉上不可察觉。

解码器逆转这个过程:它从一个内容流读取字距调整、减去自然字距、把偏离量量化回比特、重新组装加密块,并用同一个密钥解密它。如果密钥错误或载体被破坏,解密会返回空,而非一个错误的负载。

容量随文本长度而扩展:每个字母对位置承载一或两个比特,因此负载必须容纳于文本所提供的位置之内。当负载超出容量时,编码器会引发一个有类型的溢出错误。

一种 PDF/A 兼容模式会将最大偏离量减半,以保持低于验证器的宽度容差阈值,用容量换取更严格的符合性。

承重的选择是把标记隐藏在字距之中,而非隐藏在可见的叠加层或某个元数据字段里。元数据标记极易被剥除,而可见的戳记会改变页面。字距偏离则内嵌于收件者必须保留的文本之中,并保持不可察觉。认证加密是第二根支柱:一个被篡改或不完整的载体会导致认证失败,因此解码器返回的是空,而非一个错误的收件者。密钥用 HKDF 按字体派生,把标记绑定到文件上下文,而非绑定到一个裸的共享密钥。诚实的鲁棒性披露由此直接得出:标记能在寻常的再分发中存活,但无法在蓄意的内容流重写中存活,因此所陈述的范围是内部泄漏溯源,而非对抗性隐写。

设计背景:Redaction is not a black rectangle

  1. 安装 NextPDF Core 与 Enterprise 包,并持有一份有效的 Enterprise 授权。
  2. 用一种暴露字距对度量的字体生成文件;编码器从字体度量读取自然字距。
  3. 从你的密钥管理器、而非从源代码提供密钥。解码时需要同一个密钥。
  4. 根据你的容量与符合性需求,决定比特深度(每个位置一或两个比特)以及是否需要 PDF/A 兼容。

编码配置是不可变的,并在构造时被验证:

  • 比特深度 — 每个字母对位置一或两个比特。更高的深度带来更大的容量,但偏离量也更大。
  • 最大调整比率 — 作为 em 分数的偏离上限,处于一个受限范围之内。更大的值带来更多余量,但有可见性风险。
  • 密码 — AES-256-GCM(默认)或 ChaCha20-Poly1305。两者都是 AEAD。
  • PDF/A 兼容 — 启用时,将有效最大偏离量减半。

编码与解码请使用同一份配置;不匹配会导致恢复不出任何负载。

  1. 从你的密钥管理器读取密钥。
  2. 构建编码配置(比特深度、偏离比率、密码、PDF/A 标志)。
  3. 为你即将渲染的文本计算字距调整,并传入负载、文本、字体密钥、字体度量、密钥与配置。
  4. 在你写入文本运行时应用所返回的调整,使标记在生成期间被嵌入。
  5. 要溯源一份泄漏的副本,请用同样的字体密钥、字体度量、密钥与配置,在可疑文件的内容流上运行解码器,并读取所恢复的负载。
examples/enterprise/steganography-capacity-guard.php
<?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,
));
}
}
examples/enterprise/steganography-decode-guarded.php
<?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;
}
}
  1. 把一个已知负载编码进一段已知文本运行,再用同一个密钥与配置把它解码回来;确认所恢复的负载相符。
  2. 故意用一个错误的密钥解码,并确认结果为 null、而非一个错误的负载——这正是 AEAD 完整性保证在起作用。
  3. 检查渲染后的页面,确认在所配置的偏离比率下间距变化在视觉上不明显。
  4. 当需要 PDF/A 兼容时,针对你的 PDF/A 配置文件验证输出,并确认未触发宽度容差。
  • 认证加密。 负载用 AES-256-GCM 或 ChaCha20-Poly1305 加密。一个被篡改或被截断的载体会在解密时令认证失败;它不会产出一个错误的负载。
  • 每次加密的 IV。 每次加密都生成一个全新的随机 12 字节 IV,满足 AES-GCM 唯一性要求,依据 NIST SP 800-38D §5.2.1。
  • 派生密钥。 加密密钥由 HKDF-SHA-256 从你的密钥与字体密钥派生(RFC 5869 §2)。请把密钥保留在你的密钥管理器中;像对待任何签名密钥那样对待它。
  • 标记即文件内容。 嵌入的字节是页面内容的一部分,而非日志内容。不要把负载或密钥写入日志。

本页涉及密码学嵌入。每一项规范性来源均为转述;不复述任何规范性文本。 ### 鲁棒性披露

标记承载于字距调整之中。它可能被打印再重新扫描、被 PDF 转换工具、被重新线性化,或被任何归一化字距的内容流重写所破坏。该技术最适合用于对以其生成形式分发的文件进行内部泄漏溯源。它不是对抗性隐写,也不能在任意的下游处理中存活。当威胁模型包括蓄意剥除时,不要把它作为唯一的控制手段来依赖。

  • 负载过大。 当负载超出文本容量时,编码器会引发一个有类型的溢出错误。请缩短负载或加长载体文本。
  • 载体文本过少。 短于两个字符的文本不提供任何载体位置,并会引发一个错误。
  • 解码时密钥错误。 解码返回 null。请把 null 当作“恢复不出标记”,而非一个部分结果。
  • 配置不匹配。 编码与解码必须使用同一个比特深度、偏离比率、密码与 PDF/A 标志;不匹配会导致恢复不出任何负载。

本页仅记录外部可观察的行为与受支持的公共 API 表面。内部命名空间路径、辅助类、机制表、运行手册文件名与工单前缀均不在范围之内。