PDF Pro 密码学架构的深度技术文档
端到端加密文件传输
PDF Pro 白皮书 WP-001 — 架构与安全
本白皮书描述了 PDF Pro 安全传输系统的架构——一种端到端加密文件传输机制,文件在上传前通过 AES-256-GCM 在客户端进行加密。服务器仅存储不透明的密文,在任何时候都无法解密、检查或读取文件内容。该系统支持两种密钥模式:自动生成的随机密钥(通过 URL 片段传输,该片段从不发送至服务器)或使用 PBKDF2 派生的口令密钥。在两种模式下,加密密钥仅存在于发送方和接收方的浏览器中。本文档详细介绍了密码学原语、数据流、威胁模型以及系统的已知局限性。术语说明:本系统不使用零知识证明(ZKP)。当我们说服务器对您的文件具有"零知识"时,我们的意思是——作为客户端加密的结果——服务器持有的始终是其无法解密的密文。
安全传输系统遵循严格的客户端-服务器分离原则,所有密码学操作均在浏览器中独立执行:
服务器被有意设计为加密数据的"哑管道"。它不知晓加密密钥、口令或文件内容。这是通过架构强制实现的,而非仅仅依赖策略。
设计原则:服务器即使被攻破也不会泄露用户数据。即使攻击者拥有完整的数据库访问权限和服务器端代码执行能力,也无法解密已传输的文件。
所有文件加密均使用 AES-256-GCM 认证加密算法,通过浏览器原生 Web Crypto API 调用。该算法同时提供机密性(数据不可读取)和完整性(数据不可被篡改而不被察觉)。
| 参数 | 值 | 依据 |
|---|---|---|
| 算法 | AES-256-GCM | NIST 批准的带关联数据的认证加密(AEAD) |
| 密钥长度 | 256 位 | 最大 AES 密钥长度;对暴力破解提供 128 位安全性 |
| IV 长度 | 96 位(12 字节) | NIST 推荐的 GCM 模式 IV 长度 |
| 标签长度 | 128 位(16 字节) | 全长认证标签,提供最大完整性保护 |
| IV 生成 | crypto.getRandomValues() | 密码学安全随机数生成器 |
// 简化的加密流程(Web Crypto API) const iv = crypto.getRandomValues(new Uint8Array(12)); const salt = crypto.getRandomValues(new Uint8Array(16)); // 从口令派生密钥(见第 1.4 节) const key = await deriveKey(passphrase, salt); // 加密文件 const ciphertext = await crypto.subtle.encrypt( { name: "AES-GCM", iv: iv }, key, fileArrayBuffer ); // 打包:[salt (16B)] + [iv (12B)] + [ciphertext + tag] const blob = concatenate(salt, iv, ciphertext);
最终加密数据块是三个组件的拼接:16 字节的盐值(用于密钥派生)、12 字节的 IV(用于 AES-GCM),以及附带 16 字节 GCM 认证标签的密文。该数据块即为服务器接收并存储的内容。
安全传输支持两种互斥的密钥模式,发送方在创建传输时选择。
在默认模式下,不涉及口令。浏览器直接生成密码学随机的 256 位 AES 密钥:
crypto.getRandomValues() 和 crypto.subtle.generateKey() 生成随机 256 位 AES-GCM 密钥。#)中。当发送方选择设置口令时,密钥通过 PBKDF2 从该口令派生:
| 参数 | 值 | 依据 |
|---|---|---|
| 算法 | PBKDF2 | NIST SP 800-132 推荐;经过广泛审计 |
| 哈希函数 | SHA-256 | 标准密码学哈希;256 位输出 |
| 迭代次数 | 600,000 | 符合 OWASP 2023 对 PBKDF2-SHA256 的建议 |
| 盐值长度 | 128 位(16 字节) | 每次传输唯一;防止彩虹表攻击 |
| 输出密钥长度 | 256 位 | 匹配 AES-256 密钥需求 |
async function deriveKey(passphrase, salt) { // 将口令作为原始密钥材料导入 const keyMaterial = await crypto.subtle.importKey( "raw", new TextEncoder().encode(passphrase), { name: "PBKDF2" }, false, ["deriveKey"] ); // 派生 AES-256-GCM 密钥 return crypto.subtle.deriveKey( { name: "PBKDF2", salt: salt, iterations: 600000, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"] ); }
安全说明:模式 B 的安全性取决于口令的熵值。我们建议使用 12 个字符以上,混合大写字母、小写字母、数字和符号的口令。
安全传输的完整生命周期遵循以下路径:
crypto.getRandomValues() 生成随机 256 位 AES-GCM 密钥和随机 12 字节 IV。#)中。crypto.getRandomValues() 生成随机 16 字节盐值和 12 字节 IV。| 数据元素 | 服务器是否可访问? | 详情 |
|---|---|---|
| 加密数据块(盐值 + IV + 密文 + 标签) | 是 | 不透明二进制数据;服务器无法解读内容 |
| 传输 ID | 是 | 传输标识符为 PostgreSQL gen_random_uuid() 生成的 UUID v4 值,从服务器的 CSPRNG 提供 122 位密码学随机性 |
| 原始文件名 | 是 | 存储在元数据中供接收方显示 |
| 原始文件大小 | 是 | 存储在元数据中用于显示 |
| 过期时间戳 | 是 | 用于自动删除执行 |
| 下载次数 / 限制 | 是 | 用于即读即焚和下载限制执行 |
| 发送方用户 ID(若已认证) | 是 | 将传输关联至账户以供管理 |
| 口令 | 否 — 从不 | 从不发送至服务器;从不离开浏览器 |
| 派生加密密钥 | 否 — 从不 | 仅在加密/解密操作期间存在于浏览器内存中 |
| 明文文件内容 | 否 — 从不 | 仅加密密文到达服务器 |
| PBKDF2 迭代次数 | 否 | 在客户端硬编码;不传输至服务器 |
所有安全传输在设计上都是短暂的。没有永久存储的选项。
| 选项 | 行为 | 执行方式 |
|---|---|---|
| 即读即焚 | 首次成功下载后立即删除加密数据块 | 服务器端:下载流完成后从存储中删除数据块 |
| 1 小时 | 无论下载状态如何,1 小时后自动删除 | 服务器端:定时清理任务 + 访问时过期检查 |
| 24 小时 | 24 小时后自动删除 | 同上 |
| 7 天 | 最长保留期;7 天后自动删除 | 同上 |
当传输过期或即读即焚时,加密数据块将从 Supabase Storage 中永久删除。元数据记录以软删除状态保留 30 天(用于滥用调查),然后永久清除。元数据记录不包含加密密钥、口令或任何可用于重建文件的信息。
| 威胁 | 攻击向量 | 缓解措施 |
|---|---|---|
| 服务器被攻破 | 攻击者获得对数据库和存储的完全访问权限 | 所有存储数据均为 AES-256-GCM 密文。攻击者只能获取不透明数据块。没有口令,暴力破解 256 位 AES 在计算上不可行。 |
| 网络拦截(中间人攻击) | 攻击者拦截传输中的数据 | 所有流量使用 TLS 1.3。即使 TLS 被破解,攻击者也只能获取加密数据块(与服务器被攻破情况相同)。 |
| 弱口令 | 攻击者暴力破解短口令或常见口令 | PBKDF2 的 600K 次迭代使每次猜测在计算上代价高昂。4 个字符的口令仍需大量计算资源。UI 强制执行最小口令长度并提供强度反馈。 |
| 口令拦截 | 攻击者拦截发送方和接收方之间共享的口令 | 口令通过带外方式共享(不通过我们的系统)。我们建议通过与传输 URL 不同的渠道共享。这是用户责任。 |
| 客户端代码篡改 | 攻击者修改提供给用户的 JavaScript | 所有资源通过 Vercel CDN 的 HTTPS 提供。子资源完整性(SRI)哈希防止 CDN 级别的篡改。用户可在浏览器开发者工具中验证源代码。 |
| 内存提取 | 攻击者从浏览器内存中提取加密密钥 | Web Crypto API 密钥在可能的情况下被标记为不可提取。派生密钥仅在加密/解密操作期间存在于内存中。浏览器内存隔离提供操作系统级别的保护。 |
| 重放攻击 | 攻击者重放捕获的加密数据块 | 每次传输具有唯一 ID,并受到下载限制和过期时间的保护。即读即焚传输在首次访问后被删除。 |
已知局限性:没有任何安全系统是完美的。我们相信对已知局限性进行透明披露。
隐私优先的密码学文档签名
PDF Pro 白皮书 WP-002 — 架构与安全
本白皮书描述了 PDF Pro 隐私签名系统的架构——一种密码学文档签名机制,PDF 文档从不离开签名者的浏览器。该系统使用带 P-256 曲线的 ECDSA 和 SHA-256 哈希来生成分离式数字签名。只有文档哈希、密码学签名和公钥被传输至服务器。私钥在用户浏览器中生成、加密并存储在 IndexedDB 中,受 PBKDF2 密钥派生(600,000 次迭代)和 AES-256-GCM 加密保护。本文档详细介绍了完整的签名和验证架构、密钥管理模型、签名载荷模式、审计跟踪设计、威胁模型以及已知局限性。
隐私签名系统围绕一个基本约束设计:PDF 文档绝不能传输至服务器。这通过分离式签名模型在架构上强制执行。
核心保证:服务器从不查看、接收或处理 PDF 文档。服务器接收的唯一与文档相关的数据是 SHA-256 哈希 — 一个 256 位的固定大小值,原始文档无法从中重建。
| 参数 | 值 | 依据 |
|---|---|---|
| 签名算法 | ECDSA(椭圆曲线数字签名算法) | NIST FIPS 186-4;签名紧凑;每位密钥安全性强 |
| 曲线 | P-256(secp256r1 / prime256v1) | NIST 批准;128 位安全级别;广泛支持 Web Crypto API |
| 哈希函数 | SHA-256 | NIST FIPS 180-4;256 位摘要;抗碰撞 |
| 密钥长度 | 256 位私钥,512 位公钥(未压缩) | P-256 标准;等效于约 3072 位 RSA |
| 签名长度 | 64 字节(r:32 字节,s:32 字节) | 紧凑;适合存储和传输 |
| 签名格式 | IEEE P1363(原始 r || s) | Web Crypto API 原生输出;base64url 编码存储 |
签名编码:Web Crypto API 的 ECDSA P-256 生成原始 64 字节签名,由两个 32 字节整数(r || s)以大端固定宽度格式组成。该原始输出以 base64url 编码存储。这不是 DER 编码 — 而是 Web Crypto API 原生生成的 IEEE P1363 格式。
// 1. 哈希 PDF 文档(客户端) const fileBuffer = await file.arrayBuffer(); const hashBuffer = await crypto.subtle.digest("SHA-256", fileBuffer); const hashHex = Array.from(new Uint8Array(hashBuffer)) .map(b => b.toString(16).padStart(2, '0')).join(''); // 2. 用私钥对哈希进行签名(客户端) const signature = await crypto.subtle.sign( { name: "ECDSA", hash: "SHA-256" }, privateKey, // 来自 IndexedDB 的 CryptoKey(已解密) hashBuffer ); // 3. 向服务器发送:哈希 + 签名 + 公钥(而非 PDF) await submitSignature({ documentHash: hashHex, signature: base64Encode(signature), publicKey: exportedPublicKeyJWK });
| 密钥类型 | 使用场景 | 生命周期 | 存储 |
|---|---|---|---|
| 临时密钥 | 访客用户(未登录) | 每个会话生成;标签页关闭时销毁 | 仅在内存中(CryptoKey 对象);从不持久化 |
| 持久密钥 | 已认证用户(已登录) | 生成一次;跨会话持久化;可撤销 | IndexedDB(使用 PBKDF2 + AES-256-GCM 加密) |
// 生成 ECDSA P-256 密钥对(Web Crypto API) const keyPair = await crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-256" }, true, // 可提取(加密 + 存储所需) ["sign", "verify"] ); // 以 JWK 格式导出公钥用于服务器注册 const publicKeyJWK = await crypto.subtle.exportKey("jwk", keyPair.publicKey); // 以 JWK 格式导出私钥用于加密存储 const privateKeyJWK = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
公钥格式:公钥以 JWK(JSON Web Key)格式导出并存储。密钥指纹计算为仅包含公共字段 {crv, kty, x, y}(按字母顺序排列)的规范 JWK 的 SHA-256 值。
持久私钥从不以明文形式存储。在写入 IndexedDB 之前,私钥(以 JWK JSON 格式导出)使用与安全传输相同的模式进行加密:
| 参数 | 值 |
|---|---|
| 密钥派生 | PBKDF2-SHA256,600,000 次迭代 |
| 盐值 | 随机 16 字节(每个密钥唯一) |
| 加密 | AES-256-GCM |
| IV | 随机 12 字节(每次加密唯一) |
| 输入 | 私钥 JWK(JSON 字符串,UTF-8 编码) |
| 存储在 IndexedDB 中的输出 | { salt, iv, ciphertext, publicKeyJWK, keyId, createdAt } |
// 加密签名密钥的 IndexedDB 记录结构 { "keyId": "uuid-v4-unique-identifier", "publicKey": { /* JWK 格式,未加密 */ }, "encryptedPrivateKey": { "salt": "base64-encoded-16-bytes", "iv": "base64-encoded-12-bytes", "ciphertext": "base64-encoded-aes-gcm-ciphertext" }, "algorithm": "ECDSA", "curve": "P-256", "createdAt": "2026-04-16T00:00:00.000Z", "userId": "supabase-auth-user-id" }
密钥恢复:如果用户忘记了签名口令,私钥将无法恢复。我们没有口令、派生密钥或任何绕过 PBKDF2 + AES-GCM 保护的机制。用户应导出密钥备份。
PDF Pro 使用分离式签名模型,即签名与文档分开存储。这是关键的隐私机制:文档从不离开签名者的设备。
密码学保证:SHA-256 是单向哈希函数。仅凭哈希值 e3b0c44298fc1c14...,在 ECDSA 和 SHA-256 的安全假设下,它提供强密码学保证,表明原始文档无法被重建。哈希不揭示文档的任何内容、长度或结构,仅在重新哈希时确认身份。
以下 JSON 模式定义了存储在服务器上的完整签名记录:
{
"schemaVersion": "1.0",
"signatureId": "uuid-v4",
"documentHash": "sha256-hex-64-chars",
"hashAlgorithm": "SHA-256",
"signature": "base64-encoded-ecdsa-signature",
"signatureAlgorithm": "ECDSA",
"curve": "P-256",
"publicKey": {
"kty": "EC",
"crv": "P-256",
"x": "base64url-encoded-x-coordinate",
"y": "base64url-encoded-y-coordinate"
},
"signer": {
"identityLevel": "authenticated | self-asserted",
"displayName": "string or null",
"email": "string or null",
"userId": "supabase-uid or null"
},
"timestamp": "ISO-8601-UTC",
"metadata": {
"fileName": "original-file-name.pdf",
"fileSize": 123456,
"pageCount": 12,
"clientVersion": "2.0.0",
"userAgent": "browser-user-agent-string"
},
"auditChain": {
"previousEventHash": "sha256-of-previous-audit-event or null",
"eventHash": "sha256-of-this-record"
}
}签名验证是一个两阶段过程:客户端密码学验证,随后是服务器端交叉核验。
const isValid = await crypto.subtle.verify( { name: "ECDSA", hash: "SHA-256" }, importedPublicKey, signatureBuffer, hashBuffer );
isValid === true,则签名在密码学上有效:文档自签名以来未被修改,且签名由对应私钥的持有者生成。| 结果 | 含义 |
|---|---|
| 有效(已认证) | 签名在密码学上有效,且公钥属于已注册的已认证 PDF Pro 用户。 |
| 有效(自我声明) | 签名在密码学上有效,但签名者身份为自我声明(访客用户或未经验证的姓名)。 |
| 无效 | 密码学验证失败。文档自签名以来已被修改,或签名已损坏。 |
| 已撤销 | 签名曾经有效,但已被签名者明确撤销。 |
| 未找到签名 | 该文档哈希不存在签名记录。 |
每个签名和验证事件都记录在防篡改审计跟踪中。事件通过哈希链接:每个事件包含前一事件的 SHA-256 哈希,形成类似区块链的仅追加链。
| 事件类型 | 触发条件 | 记录数据 |
|---|---|---|
KEY_REGISTERED | 用户注册新公钥 | 公钥 JWK、用户 ID、时间戳 |
DOCUMENT_SIGNED | 用户签署文档 | 文档哈希、签名、公钥、签名者身份、时间戳 |
SIGNATURE_VERIFIED | 任何用户验证签名 | 文档哈希、验证结果、验证者信息(若已认证)、时间戳 |
SIGNATURE_REVOKED | 签名者撤销签名 | 签名 ID、撤销原因、时间戳 |
KEY_REVOKED | 用户撤销公钥 | 公钥 ID、撤销原因、时间戳 |
// 每个审计事件包含: { "eventId": "uuid-v4", "eventType": "DOCUMENT_SIGNED", "timestamp": "ISO-8601-UTC", "data": { /* 事件特定载荷 */ }, "previousEventHash": "sha256-of-previous-event-json", "eventHash": "sha256-of-this-event-json-without-eventHash" } // 篡改检测:要验证链,请计算: // SHA-256(JSON.stringify(不含 eventHash 字段的事件)) // 并确认其与 eventHash 匹配。 // 然后确认 previousEventHash 与前一事件的 eventHash 匹配。
如果链中的任何事件被修改,从该点开始的所有后续哈希链接都将断裂,使篡改立即可检测。这提供了对审计跟踪完整性的强力保证。
重要局限性:哈希链使篡改在记录的事件序列中可检测。然而,具有直接访问权限的数据库管理员理论上可以删除并重建链。更强的保证需要外部时间戳或第三方证明,本版本未实现。
| 级别 | 要求 | 信任属性 | 使用场景 |
|---|---|---|---|
| 已认证 | 已登录的 PDF Pro 用户,具有已验证的电子邮件;持久密钥对已注册至账户 | 电子邮件由 Supabase Auth 验证;公钥绑定至已认证账户;审计跟踪关联至用户 ID | 商业文件、合同、正式协议 |
| 自我声明 | 访客用户或具有自填姓名的已认证用户;临时或持久密钥对 | 密码学完整性有保证;签名者身份为自我声明,未经独立验证 | 快速签名、个人文件、非正式协议 |
注意:在产品 UI 中,'self_asserted' 可能显示为"自我声明身份"或"访客签名"。'authenticated' 可能显示为"已认证账户"。这些是相同底层身份级别的显示标签。
身份绑定:"已认证"签名意味着公钥已注册至具有已验证电子邮件的 PDF Pro 账户。这并不意味着签名者的现实世界身份已通过政府证件、生物特征或亲身核实得到验证。我们不执行"了解您的客户"(KYC)检查。
| 威胁 | 攻击向量 | 缓解措施 |
|---|---|---|
| 重放攻击 | 攻击者复制有效签名并将其应用于不同文档 | 签名绑定至特定文档的 SHA-256 哈希。不同文档将有不同哈希,ECDSA 验证将失败。 |
| 伪造 | 攻击者在没有私钥的情况下创建有效签名 | ECDSA P-256 安全性基于椭圆曲线离散对数问题(ECDLP)。在没有私钥的情况下伪造签名在计算上不可行(128 位安全级别)。 |
| 密钥替换 | 攻击者注册自己的公钥并声称签名由他人制作 | 已认证签名将公钥绑定至已验证的电子邮件。审计跟踪记录哪个密钥签署了哪个文档。密钥注册事件通过哈希链接。 |
| 文档替换 | 攻击者修改已签名的 PDF 并声称签名仍然有效 | 对 PDF 的任何修改都会改变其 SHA-256 哈希。现有签名对新哈希的验证将失败。找到碰撞(具有相同哈希的不同文档)需要约 2^128 次操作。 |
| 私钥窃取 | 攻击者从 IndexedDB 提取加密私钥 | 私钥使用 AES-256-GCM 加密,由 PBKDF2(600K 次迭代)派生密钥保护。没有口令,解密密钥在计算上不可行。 |
| 服务器被攻破 | 攻击者获得完整的服务器访问权限 | 服务器只有公钥和签名记录。私钥从不到达服务器。攻击者无法伪造新签名。他们可以删除或修改现有记录,但哈希链断裂将可被检测。 |
| 审计跟踪篡改 | 攻击者修改服务器上的审计跟踪事件 | 哈希链事件:修改任何事件都会从该点向后断裂链。独立验证工具可以检测链断裂。 |
PDF Pro 签名不是什么:
我们的承诺:我们通过架构而非营销来构建安全性。这些白皮书准确描述了我们系统的工作方式,包括其局限性。我们相信注重安全的用户应获得完整的技术透明度。如果您对我们架构的任何方面有疑问,请通过 info@webdesign9.com 联系我们。