技术白皮书

PDF Pro 密码学架构的深度技术文档

发布日期: 2025年4月16日 最后更新: 2026年4月16日 版本: 2.0

白皮书索引

  1. 端到端加密文件传输
  2. 隐私优先的密码学文档签名

端到端加密文件传输

PDF Pro 白皮书 WP-001 — 架构与安全

1.1 摘要

本白皮书描述了 PDF Pro 安全传输系统的架构——一种端到端加密文件传输机制,文件在上传前通过 AES-256-GCM 在客户端进行加密。服务器仅存储不透明的密文,在任何时候都无法解密、检查或读取文件内容。该系统支持两种密钥模式:自动生成的随机密钥(通过 URL 片段传输,该片段从不发送至服务器)或使用 PBKDF2 派生的口令密钥。在两种模式下,加密密钥仅存在于发送方和接收方的浏览器中。本文档详细介绍了密码学原语、数据流、威胁模型以及系统的已知局限性。术语说明:本系统不使用零知识证明(ZKP)。当我们说服务器对您的文件具有"零知识"时,我们的意思是——作为客户端加密的结果——服务器持有的始终是其无法解密的密文。

1.2 架构概述

安全传输系统遵循严格的客户端-服务器分离原则,所有密码学操作均在浏览器中独立执行:

服务器被有意设计为加密数据的"哑管道"。它不知晓加密密钥、口令或文件内容。这是通过架构强制实现的,而非仅仅依赖策略。

设计原则:服务器即使被攻破也不会泄露用户数据。即使攻击者拥有完整的数据库访问权限和服务器端代码执行能力,也无法解密已传输的文件。

1.3 加密:通过 Web Crypto API 实现 AES-256-GCM

所有文件加密均使用 AES-256-GCM 认证加密算法,通过浏览器原生 Web Crypto API 调用。该算法同时提供机密性(数据不可读取)和完整性(数据不可被篡改而不被察觉)。

1.3.1 算法参数

参数依据
算法AES-256-GCMNIST 批准的带关联数据的认证加密(AEAD)
密钥长度256 位最大 AES 密钥长度;对暴力破解提供 128 位安全性
IV 长度96 位(12 字节)NIST 推荐的 GCM 模式 IV 长度
标签长度128 位(16 字节)全长认证标签,提供最大完整性保护
IV 生成crypto.getRandomValues()密码学安全随机数生成器

1.3.2 加密实现

// 简化的加密流程(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 认证标签的密文。该数据块即为服务器接收并存储的内容。

1.4 密钥生成 & 传输

安全传输支持两种互斥的密钥模式,发送方在创建传输时选择。

1.4.1 模式 A — 自动生成密钥(默认)

在默认模式下,不涉及口令。浏览器直接生成密码学随机的 256 位 AES 密钥:

1.4.2 模式 B — 口令保护传输

当发送方选择设置口令时,密钥通过 PBKDF2 从该口令派生:

1.4.3 PBKDF2 参数(模式 B)

参数依据
算法PBKDF2NIST SP 800-132 推荐;经过广泛审计
哈希函数SHA-256标准密码学哈希;256 位输出
迭代次数600,000符合 OWASP 2023 对 PBKDF2-SHA256 的建议
盐值长度128 位(16 字节)每次传输唯一;防止彩虹表攻击
输出密钥长度256 位匹配 AES-256 密钥需求

1.4.4 密钥派生实现(模式 B)

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 个字符以上,混合大写字母、小写字母、数字和符号的口令。

1.5 数据流

安全传输的完整生命周期遵循以下路径:

1.5.1 发送(上传)— 模式 A(自动生成密钥)

  1. 发送方在浏览器中选择文件。
  2. 浏览器使用 crypto.getRandomValues() 生成随机 256 位 AES-GCM 密钥和随机 12 字节 IV。
  3. 浏览器使用 AES-256-GCM 加密文件,生成密文 + 认证标签。
  4. 浏览器将 [IV | 密文+标签] 拼接为单个数据块。
  5. 浏览器通过 HTTPS 将加密数据块上传至服务器。
  6. 服务器将数据块存储在 Supabase Storage 中,并创建元数据记录(传输 ID、过期时间、下载限制、文件名、文件大小)。
  7. 服务器返回传输 URL。浏览器将导出的密钥附加到 URL 片段(#)中。
  8. 发送方将完整 URL(含片段)分享给接收方。

1.5.2 发送(上传)— 模式 B(口令)

  1. 发送方在浏览器中选择文件并输入口令。
  2. 浏览器使用 crypto.getRandomValues() 生成随机 16 字节盐值和 12 字节 IV。
  3. 浏览器使用 PBKDF2(600K 次迭代)从口令派生 AES-256 密钥。
  4. 浏览器使用 AES-256-GCM 加密文件,生成密文 + 认证标签。
  5. 浏览器将 [盐值 | IV | 密文+标签] 拼接为单个数据块。
  6. 浏览器通过 HTTPS 将加密数据块上传至服务器。
  7. 服务器将数据块存储在 Supabase Storage 中,并创建元数据记录(传输 ID、过期时间、下载限制、文件名、文件大小)。
  8. 服务器返回包含传输 ID 的传输 URL。
  9. 发送方通过不同渠道将传输 URL 和口令分别分享给接收方。

1.5.3 接收(下载)— 模式 A

  1. 接收方打开完整传输 URL(片段中包含密钥)。
  2. 浏览器从 URL 片段中提取 AES 密钥(从不发送至服务器)。
  3. 浏览器通过 HTTPS 从服务器下载加密数据块。
  4. 浏览器从数据块中提取 IV(前 12 字节)。
  5. 浏览器使用提取的密钥和 IV,通过 AES-256-GCM 解密密文。
  6. 若解密成功(GCM 标签验证通过),明文文件呈现给用户。
  7. 服务器更新下载计数,若启用了即读即焚,则删除数据块。

1.5.4 接收(下载)— 模式 B

  1. 接收方打开传输 URL,并在浏览器中输入口令。
  2. 浏览器通过 HTTPS 从服务器下载加密数据块。
  3. 浏览器从数据块中提取盐值(前 16 字节)和 IV(接下来的 12 字节)。
  4. 浏览器使用 PBKDF2(600K 次迭代)从口令 + 盐值派生 AES-256 密钥。
  5. 浏览器使用派生密钥和 IV,通过 AES-256-GCM 解密密文。
  6. 若解密成功(GCM 标签验证通过),明文文件呈现给用户。
  7. 若解密失败(口令错误 = 密钥错误 = GCM 标签无效),则显示错误。
  8. 服务器更新下载计数,若启用了即读即焚,则删除数据块。

1.6 服务器存储的内容与从未获知的内容

数据元素服务器是否可访问?详情
加密数据块(盐值 + IV + 密文 + 标签)不透明二进制数据;服务器无法解读内容
传输 ID传输标识符为 PostgreSQL gen_random_uuid() 生成的 UUID v4 值,从服务器的 CSPRNG 提供 122 位密码学随机性
原始文件名存储在元数据中供接收方显示
原始文件大小存储在元数据中用于显示
过期时间戳用于自动删除执行
下载次数 / 限制用于即读即焚和下载限制执行
发送方用户 ID(若已认证)将传输关联至账户以供管理
口令否 — 从不从不发送至服务器;从不离开浏览器
派生加密密钥否 — 从不仅在加密/解密操作期间存在于浏览器内存中
明文文件内容否 — 从不仅加密密文到达服务器
PBKDF2 迭代次数在客户端硬编码;不传输至服务器

1.7 自动过期与即读即焚

所有安全传输在设计上都是短暂的。没有永久存储的选项。

1.7.1 过期选项

选项行为执行方式
即读即焚首次成功下载后立即删除加密数据块服务器端:下载流完成后从存储中删除数据块
1 小时无论下载状态如何,1 小时后自动删除服务器端:定时清理任务 + 访问时过期检查
24 小时24 小时后自动删除同上
7 天最长保留期;7 天后自动删除同上

1.7.2 删除保证

当传输过期或即读即焚时,加密数据块将从 Supabase Storage 中永久删除。元数据记录以软删除状态保留 30 天(用于滥用调查),然后永久清除。元数据记录不包含加密密钥、口令或任何可用于重建文件的信息。

1.8 威胁模型与缓解措施

威胁攻击向量缓解措施
服务器被攻破 攻击者获得对数据库和存储的完全访问权限 所有存储数据均为 AES-256-GCM 密文。攻击者只能获取不透明数据块。没有口令,暴力破解 256 位 AES 在计算上不可行。
网络拦截(中间人攻击) 攻击者拦截传输中的数据 所有流量使用 TLS 1.3。即使 TLS 被破解,攻击者也只能获取加密数据块(与服务器被攻破情况相同)。
弱口令 攻击者暴力破解短口令或常见口令 PBKDF2 的 600K 次迭代使每次猜测在计算上代价高昂。4 个字符的口令仍需大量计算资源。UI 强制执行最小口令长度并提供强度反馈。
口令拦截 攻击者拦截发送方和接收方之间共享的口令 口令通过带外方式共享(不通过我们的系统)。我们建议通过与传输 URL 不同的渠道共享。这是用户责任。
客户端代码篡改 攻击者修改提供给用户的 JavaScript 所有资源通过 Vercel CDN 的 HTTPS 提供。子资源完整性(SRI)哈希防止 CDN 级别的篡改。用户可在浏览器开发者工具中验证源代码。
内存提取 攻击者从浏览器内存中提取加密密钥 Web Crypto API 密钥在可能的情况下被标记为不可提取。派生密钥仅在加密/解密操作期间存在于内存中。浏览器内存隔离提供操作系统级别的保护。
重放攻击 攻击者重放捕获的加密数据块 每次传输具有唯一 ID,并受到下载限制和过期时间的保护。即读即焚传输在首次访问后被删除。

1.8.1 假设条件

1.8.2 超出范围

1.9 局限性与诚实披露

已知局限性:没有任何安全系统是完美的。我们相信对已知局限性进行透明披露。


隐私优先的密码学文档签名

PDF Pro 白皮书 WP-002 — 架构与安全

2.1 摘要

本白皮书描述了 PDF Pro 隐私签名系统的架构——一种密码学文档签名机制,PDF 文档从不离开签名者的浏览器。该系统使用带 P-256 曲线的 ECDSA 和 SHA-256 哈希来生成分离式数字签名。只有文档哈希、密码学签名和公钥被传输至服务器。私钥在用户浏览器中生成、加密并存储在 IndexedDB 中,受 PBKDF2 密钥派生(600,000 次迭代)和 AES-256-GCM 加密保护。本文档详细介绍了完整的签名和验证架构、密钥管理模型、签名载荷模式、审计跟踪设计、威胁模型以及已知局限性。

2.2 架构概述

隐私签名系统围绕一个基本约束设计:PDF 文档绝不能传输至服务器。这通过分离式签名模型在架构上强制执行。

核心保证:服务器从不查看、接收或处理 PDF 文档。服务器接收的唯一与文档相关的数据是 SHA-256 哈希 — 一个 256 位的固定大小值,原始文档无法从中重建。

2.3 签名算法:ECDSA P-256 / SHA-256

2.3.1 算法参数

参数依据
签名算法ECDSA(椭圆曲线数字签名算法)NIST FIPS 186-4;签名紧凑;每位密钥安全性强
曲线P-256(secp256r1 / prime256v1)NIST 批准;128 位安全级别;广泛支持 Web Crypto API
哈希函数SHA-256NIST 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 格式。

2.3.2 签名流程

// 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
});

2.4 密钥管理:临时密钥与持久密钥

密钥类型使用场景生命周期存储
临时密钥 访客用户(未登录) 每个会话生成;标签页关闭时销毁 仅在内存中(CryptoKey 对象);从不持久化
持久密钥 已认证用户(已登录) 生成一次;跨会话持久化;可撤销 IndexedDB(使用 PBKDF2 + AES-256-GCM 加密)

2.4.1 密钥生成

// 生成 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 值。

2.5 私钥保护:PBKDF2 + AES-GCM → IndexedDB

持久私钥从不以明文形式存储。在写入 IndexedDB 之前,私钥(以 JWK JSON 格式导出)使用与安全传输相同的模式进行加密:

2.5.1 保护参数

参数
密钥派生PBKDF2-SHA256,600,000 次迭代
盐值随机 16 字节(每个密钥唯一)
加密AES-256-GCM
IV随机 12 字节(每次加密唯一)
输入私钥 JWK(JSON 字符串,UTF-8 编码)
存储在 IndexedDB 中的输出{ salt, iv, ciphertext, publicKeyJWK, keyId, createdAt }

2.5.2 存储模式

// 加密签名密钥的 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 保护的机制。用户应导出密钥备份。

2.6 分离式签名模型

PDF Pro 使用分离式签名模型,即签名与文档分开存储。这是关键的隐私机制:文档从不离开签名者的设备。

2.6.1 发送至服务器的内容

2.6.2 不发送至服务器的内容

密码学保证:SHA-256 是单向哈希函数。仅凭哈希值 e3b0c44298fc1c14...,在 ECDSA 和 SHA-256 的安全假设下,它提供强密码学保证,表明原始文档无法被重建。哈希不揭示文档的任何内容、长度或结构,仅在重新哈希时确认身份。

2.7 签名载荷模式 v1.0

以下 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"
  }
}

2.8 验证流程

签名验证是一个两阶段过程:客户端密码学验证,随后是服务器端交叉核验。

2.8.1 第一阶段:客户端密码学验证

  1. 用户将 PDF 加载到浏览器中。
  2. 浏览器计算已加载 PDF 的 SHA-256 哈希。
  3. 浏览器向服务器查询匹配该哈希的所有签名记录。
  4. 对于返回的每条签名记录,浏览器执行 ECDSA 验证:
    const isValid = await crypto.subtle.verify(
      { name: "ECDSA", hash: "SHA-256" },
      importedPublicKey,
      signatureBuffer,
      hashBuffer
    );
  5. isValid === true,则签名在密码学上有效:文档自签名以来未被修改,且签名由对应私钥的持有者生成。

2.8.2 第二阶段:服务器端交叉核验

  1. 服务器确认签名记录中的公钥已注册至已知用户(针对已认证签名)。
  2. 服务器确认签名记录未被撤销。
  3. 服务器确认审计跟踪完整性(哈希链验证)。
  4. 服务器返回签名者的身份级别和注册状态。

2.8.3 验证结果

结果含义
有效(已认证)签名在密码学上有效,且公钥属于已注册的已认证 PDF Pro 用户。
有效(自我声明)签名在密码学上有效,但签名者身份为自我声明(访客用户或未经验证的姓名)。
无效密码学验证失败。文档自签名以来已被修改,或签名已损坏。
已撤销签名曾经有效,但已被签名者明确撤销。
未找到签名该文档哈希不存在签名记录。

2.9 审计跟踪:哈希链事件

每个签名和验证事件都记录在防篡改审计跟踪中。事件通过哈希链接:每个事件包含前一事件的 SHA-256 哈希,形成类似区块链的仅追加链。

2.9.1 审计事件类型

事件类型触发条件记录数据
KEY_REGISTERED用户注册新公钥公钥 JWK、用户 ID、时间戳
DOCUMENT_SIGNED用户签署文档文档哈希、签名、公钥、签名者身份、时间戳
SIGNATURE_VERIFIED任何用户验证签名文档哈希、验证结果、验证者信息(若已认证)、时间戳
SIGNATURE_REVOKED签名者撤销签名签名 ID、撤销原因、时间戳
KEY_REVOKED用户撤销公钥公钥 ID、撤销原因、时间戳

2.9.2 哈希链结构

// 每个审计事件包含:
{
  "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 匹配。

如果链中的任何事件被修改,从该点开始的所有后续哈希链接都将断裂,使篡改立即可检测。这提供了对审计跟踪完整性的强力保证。

重要局限性:哈希链使篡改在记录的事件序列中可检测。然而,具有直接访问权限的数据库管理员理论上可以删除并重建链。更强的保证需要外部时间戳或第三方证明,本版本未实现。

2.10 身份级别

级别要求信任属性使用场景
已认证 已登录的 PDF Pro 用户,具有已验证的电子邮件;持久密钥对已注册至账户 电子邮件由 Supabase Auth 验证;公钥绑定至已认证账户;审计跟踪关联至用户 ID 商业文件、合同、正式协议
自我声明 访客用户或具有自填姓名的已认证用户;临时或持久密钥对 密码学完整性有保证;签名者身份为自我声明,未经独立验证 快速签名、个人文件、非正式协议

注意:在产品 UI 中,'self_asserted' 可能显示为"自我声明身份"或"访客签名"。'authenticated' 可能显示为"已认证账户"。这些是相同底层身份级别的显示标签。

身份绑定:"已认证"签名意味着公钥已注册至具有已验证电子邮件的 PDF Pro 账户。这并不意味着签名者的现实世界身份已通过政府证件、生物特征或亲身核实得到验证。我们不执行"了解您的客户"(KYC)检查。

2.11 威胁模型

威胁攻击向量缓解措施
重放攻击 攻击者复制有效签名并将其应用于不同文档 签名绑定至特定文档的 SHA-256 哈希。不同文档将有不同哈希,ECDSA 验证将失败。
伪造 攻击者在没有私钥的情况下创建有效签名 ECDSA P-256 安全性基于椭圆曲线离散对数问题(ECDLP)。在没有私钥的情况下伪造签名在计算上不可行(128 位安全级别)。
密钥替换 攻击者注册自己的公钥并声称签名由他人制作 已认证签名将公钥绑定至已验证的电子邮件。审计跟踪记录哪个密钥签署了哪个文档。密钥注册事件通过哈希链接。
文档替换 攻击者修改已签名的 PDF 并声称签名仍然有效 对 PDF 的任何修改都会改变其 SHA-256 哈希。现有签名对新哈希的验证将失败。找到碰撞(具有相同哈希的不同文档)需要约 2^128 次操作。
私钥窃取 攻击者从 IndexedDB 提取加密私钥 私钥使用 AES-256-GCM 加密,由 PBKDF2(600K 次迭代)派生密钥保护。没有口令,解密密钥在计算上不可行。
服务器被攻破 攻击者获得完整的服务器访问权限 服务器只有公钥和签名记录。私钥从不到达服务器。攻击者无法伪造新签名。他们可以删除或修改现有记录,但哈希链断裂将可被检测。
审计跟踪篡改 攻击者修改服务器上的审计跟踪事件 哈希链事件:修改任何事件都会从该点向后断裂链。独立验证工具可以检测链断裂。

2.11.1 假设条件

2.11.2 超出范围

2.12 诚实披露

PDF Pro 签名不是什么:

2.12.1 PDF Pro 签名适用于什么

我们的承诺:我们通过架构而非营销来构建安全性。这些白皮书准确描述了我们系统的工作方式,包括其局限性。我们相信注重安全的用户应获得完整的技术透明度。如果您对我们架构的任何方面有疑问,请通过 info@webdesign9.com 联系我们。