Skip to content

第 19 章:接口报文加密 — 前端加密入参、后端加密返回

示例工程:examples-middleware/api_crypto不需要 Docker,纯加密逻辑)

含一个真正的浏览器前端:cargo run -p api_crypto 后打开 http://127.0.0.1:3004, 用 Web Crypto API 完成加密请求/解密响应的全流程,DevTools 里亲眼确认线上只有密文。

学习目标

  1. 说清报文加密的威胁模型:它防什么(代理/网关/WAF 日志)、不防什么(不替代 TLS、不防端上逆向)
  2. 理解混合加密信封的结构:RSA-OAEP 包 AES 密钥 + AES-GCM 加密报文
  3. 能在浏览器端用 Web Crypto API 实现加密与解密,在 Rust 端实现开封与封回
  4. 知道防重放、nonce 不重用、错误不泄密这三个工程细节

19.1 威胁模型:TLS 之后,明文去了哪

「有 HTTPS 还要加密报文?」——要回答这个问题,先看企业环境里一个请求的真实路径:

text
浏览器 ──TLS──▶ 网关/WAF/企业代理(TLS 在这里终止)──明文──▶ 内网若干跳 ──▶ 服务

                     └─▶ 访问日志、审计抓包、APM 采样 …… 明文在此落盘

TLS 只保护「传输段」,在 TLS 终止点之后,手机号、身份证号这类字段会以明文出现在访问日志、抓包审计、第三方 APM 里——这些系统的查看权限往往远比数据库宽松。应用层报文加密的目标:让这些中间节点即使看到完整 HTTP,也只看到密文信封。

同样重要的是诚实的边界(示例注释里也标了):

  • 不替代 TLS,是叠加。没有 TLS,攻击者可以直接换掉你下发的公钥(中间人)
  • 不防端上攻击者:前端 JS 可被逆向,密钥就在浏览器内存里。防的是被动的日志侧泄露,不是主动的端上破解
  • 国内金融/政务场景常见同构方案的国密版(SM2 换 RSA、SM4 换 AES),结构完全一致

19.2 混合加密信封

非对称加密(RSA)安全但慢、有长度限制;对称加密(AES)快但要先安全地共享密钥。行业标准是两者组合——用 RSA 送钥匙,用 AES 运货

text
请求(前端 → 后端)
  1. 前端生成随机 AES-256 密钥(每次会话都新生成)
  2. 内层明文 {"ts": 时间戳, "data": {...业务参数}} 用 AES-GCM 加密 → ct
  3. AES 密钥用服务端 RSA 公钥(RSA-OAEP-SHA256)加密 → ek
  4. 发送信封 {"ek", "nonce", "ct"}          ◀── 代理日志里只有这个

响应(后端 → 前端)
  5. 后端用 RSA 私钥解出 AES 密钥,解密 ct,校验 ts,执行业务
  6. 用同一把 AES 密钥、【新的 nonce】加密响应 → 信封 {"nonce", "ct"}
  7. 前端用手里的 AES 密钥解密展示

三个必须做对的细节:

  • GCM 的 nonce 绝不能重用:同密钥重用 nonce 会直接破坏 GCM 的安全性,所以响应必须用新 nonce(每次加密都随机生成)
  • 防重放:内层的 ts 由服务端校验(±5 分钟窗口)。截获的密文原样重发也会因超窗被拒;要求更严时用 Redis SET NX 记一次性 nonce——和第 18 章幂等键是同一机制
  • 解密失败不说原因:格式错、解密失败、超窗,对外一律 400 通用错误。区分错误类型的响应曾催生 padding oracle 一类经典攻击——密码学错误处理的铁律是「失败即沉默」

19.3 两端实现速览

Rust 端src/envelope.rs,RustCrypto 系 rsa + aes-gcm):open_request 拆信封(RSA 解 AES 密钥 → GCM 解密 → 校验 ts),seal_response 封回程。crate 里还有 client_seal_request/client_open_response——用 Rust 模拟前端,让全链路可以在默认 cargo test 里跑通,不依赖浏览器。

浏览器端static/index.html,原生 Web Crypto API,零依赖):

text
fetch /crypto/public-key            → importKey("spki", RSA-OAEP SHA-256)
crypto.getRandomValues(32 字节)     → importKey("raw", AES-GCM)
encrypt({name: "AES-GCM", iv}, …)  → ct;encrypt("RSA-OAEP", aesKeyRaw) → ek
POST 信封 → 解密响应信封 → 渲染

演示页特意选了姓名/手机号/身份证做入参——正是最不想出现在网关日志里的字段。页面并排展示「线上传输的密文信封」与「解密后的明文」,再打开 DevTools 的 Network 面板看请求体,你会得到直观的结论:代理能记录的只有 base64 密文

bash
cd examples-middleware
cargo run -p api_crypto     # 打开 http://127.0.0.1:3004
cargo test -p api_crypto    # 全链路 + 篡改/重放/明文泄露断言,无需任何服务

19.4 密钥管理与工程化清单

  • 私钥的家:示例内嵌教学密钥(注释有显眼警告),生产私钥只能来自密钥管理系统/环境变量注入(第 16.3 节),且要有轮换机制——公钥端点天然支持平滑轮换(新旧并行一个过渡期)
  • 哪些接口加密:全站加密成本高(调试、网关鉴权都受影响),主流做法是敏感接口白名单(登录、实名、支付)
  • 压缩与长度:密文不可压缩,大报文考虑先压缩再加密
  • 与鉴权的关系:信封只管机密性;身份还是靠第 17 章的 JWT(放 header,不放加密体内,网关才能做统一鉴权)
  • 完整性与签名:GCM 自带完整性校验(AEAD);若需「不可抵赖」(证明请求确实来自持钥方),再叠加客户端签名——即第 18 章 webhook 验签的镜像方向

本章小结

  • 报文加密防的是 TLS 终止点之后的日志/代理侧明文泄露,叠加 TLS 而非替代
  • 混合信封 = RSA-OAEP 送 AES 密钥 + AES-GCM 运数据,每会话随机密钥
  • GCM nonce 每次加密必须新生成;响应复用密钥但绝不复用 nonce
  • 防重放靠内层时间戳窗口,更严格用 Redis 一次性 nonce(同幂等键机制)
  • 解密失败对外一律通用 400——错误信息差异本身就是攻击面
  • 浏览器端 Web Crypto API 原生支持 RSA-OAEP/AES-GCM,无需第三方库
  • 私钥进 KMS、敏感接口白名单、先压缩后加密,是落地时的三个工程决策

自测清单

  • [ ] 我能画出请求路径图并指出明文泄露发生在哪一段
  • [ ] 我能说出报文加密「防什么、不防什么」各两条
  • [ ] 我能默写混合信封的三个字段及各自的加密方式
  • [ ] 我能解释 GCM nonce 重用的后果和响应端的正确做法
  • [ ] 我能说出重放攻击的手法和两级防御
  • [ ] 我能解释为什么解密失败不能返回具体原因

练习

  1. 把一次性 nonce 防重放做实:内层加 nonce 字段,服务端用 Redis SET NX EX 300 判重(复用第 18 章幂等键思路),写测试证明「同一信封第二次发送被拒」。
  2. todo_api_saas 的登录接口套上本章信封(密码是最该加密的入参),体会「敏感接口白名单」的集成方式。
  3. 实现公钥轮换:/crypto/public-key 返回 {kid, spki_b64},信封带 kid,服务端同时持有新旧两把私钥平滑过渡。
  4. 用 DevTools 把演示页请求的 ct 改一个字符重发,观察服务端返回与日志——验证「失败即沉默」。

导航:上一章 | 返回目录 | 下一章