第 20 章:数据库敏感数据存储 — 静态加密与盲索引
示例工程:
examples-middleware/db_crypto(需要 Postgres,见第 14.2 节)第 19 章加密「传输中」的数据,本章加密「静态存储」的数据——两者合起来覆盖 敏感数据的完整生命周期:传输(in transit)与落盘(at rest)。
学习目标
- 说清数据库敏感字段面临的泄露途径,以及应用层加密防住哪些
- 能实现字段级 AES-GCM 加密,并用密钥版本前缀支持轮换
- 理解「加密后还能查询」的矛盾,用盲索引(blind index)解决精确匹配
- 分清 TDE、pgcrypto、应用层加密各自的防护边界
- 建立密钥管理的分层认知:DEK/KEK 信封加密与 KMS
20.1 威胁:明文躺在数据库里,会从哪泄露
手机号、身份证、银行卡明文存库,暴露途径远不止「黑客拖库」:
- 拖库 / SQL 注入:一条
SELECT * FROM users带走全部 - 备份泄露:数据库备份文件往往权限松、还会被复制到测试环境
- 磁盘 / 快照:云盘快照、废弃硬盘
- 内部越权:DBA、运维、数据分析师的查询权限通常远宽于「业务上该看到的」
- 日志:慢查询日志、binlog 里可能带上参数值
应用层字段加密的目标:数据库里存的就是密文,上述任何一条途径拿到的都是密文。明确边界(示例注释里也标了)——它防的是「数据静态泄露」,不防「已经拿到应用解密密钥的攻击者」;所以密钥绝不能和数据存在一起,密钥管理是整个方案的成败关键。
20.2 三种手段,先分清各自的位置
| 手段 | 谁来加密 | 防得住 | 防不住 | 本教程 |
|---|---|---|---|---|
| TDE(透明数据加密) | 数据库/磁盘层 | 磁盘、备份文件被偷 | 任何能连库的人(对 SQL 透明) | 概念 |
| pgcrypto | 数据库函数 | 部分场景 | 密钥常要传进 SQL,易进日志 | 概念 |
| 应用层字段加密 | 应用代码 | 拖库、备份、越权 SQL、DBA | 拿到应用密钥者 | ✅ db_crypto |
关键区别:TDE 对 SQL 是透明的——任何能执行 SELECT 的人拿到的还是明文,它只防物理介质失窃。要防住「能连库但不该看明文」的场景(这恰恰是内部泄露的主体),必须让密文在数据库之外才被解密,也就是应用层加密。三者可叠加:TDE 保底,应用层加密护敏感字段。
20.3 字段级加密 + 密钥版本化
db_crypto/src/crypto.rs 的存储格式:
v2:{base64(nonce)}:{base64(密文+GCM tag)}
│ └ 每次写入随机生成,绝不重用
└ 密钥版本号——解密时据此选对密钥为什么密文要带版本前缀:密钥需要定期轮换(合规要求、或怀疑泄露时)。带上版本号后,老数据(v1:...)和新数据(v2:...)能在同一张表里共存,各自用对应版本的密钥解密——轮换不需要停机、不需要一次性重刷全表。KeyRing 同时持有多个版本的密钥,写入永远用 active 版本,读取按前缀选版本。
同明文每次加密得到不同密文(随机 nonce)——这是安全的必需,但也正因如此,加密列没法直接用来查询。
20.4 盲索引:加密了还要能 WHERE phone = ?
业务要「按手机号查用户」,但 phone_enc 每行密文都不同,WHERE phone_enc = ? 永远查不中。解决方案是额外存一列盲索引:
phone_enc = v2:nonce:ct ← 可逆,给展示用
phone_idx = HMAC-SHA256(规范化手机号, 索引专用密钥) 的 hex ← 给查询用查询时先对输入手机号算同样的 HMAC,WHERE phone_idx = $1 精确命中——这一列是普通 B-tree 索引,查询性能和明文索引同级。三个设计要点(源码注释都讲了):
- 为什么不用确定性加密当索引:确定性加密同明文得同密文,虽然也能查,但会泄露「哪些行手机号相同」的相等关系。HMAC + 独立密钥 + 只支持精确匹配,是安全与功能的折衷
- 索引密钥必须独立于加密密钥:两把钥匙分开,一把泄露不会连累另一把
- 规范化:
"138 0000 1234"和"13800001234"要先归一(去空格、连字符)再算 HMAC,否则查不中;代价是盲索引只能做精确匹配,不支持模糊/范围查询(那是它的固有限制)
cd examples-middleware
docker compose up -d postgres
cargo run -p db_crypto # 打印「库里实际存的密文行」+ 盲索引查询 + 密钥轮换演示20.5 密钥管理:方案的真正命门
加密算法都是公开的,安全性全压在密钥上。分层是行业标准做法——信封加密:
DEK(数据密钥):真正加密字段的密钥,可以有很多个
└─ 被 KEK(主密钥)加密后,才存进配置/数据库
└ KEK 只存在于 KMS(AWS KMS、阿里云 KMS、Vault)里,永不离开好处:轮换 KEK 不用重刷数据(只需重新加密那些 DEK);应用拿到的是「被 KEK 包裹的 DEK」,解开要调 KMS,权限和审计都收口到 KMS。教学示例用环境变量存密钥(FIELD_KEY_V1/V2,带显眼警告),生产必须接 KMS。
绝对红线:密钥不进代码仓库、不和数据存同一处、不进日志(呼应第 16.3 节)。
本章小结
- 明文存库的泄露途径远超「黑客拖库」:备份、快照、越权 SQL、DBA、日志
- TDE 对 SQL 透明只防介质失窃;防越权访问必须靠「密文在库外才解密」的应用层加密
- 字段格式
v{版本}:{nonce}:{密文},版本前缀让密钥轮换时新老数据共存 - 随机 nonce 导致加密列无法查询,用盲索引(HMAC + 独立密钥)解决精确匹配
- 盲索引只支持精确匹配、需先规范化明文,不支持模糊/范围查询
- 密钥管理是命门:信封加密(DEK/KEK)+ KMS,密钥永不与数据同处
自测清单
- [ ] 我能列出明文存库的至少四种泄露途径
- [ ] 我能解释 TDE 为什么防不住越权 SQL 查询
- [ ] 我能说出密文带版本前缀对密钥轮换的意义
- [ ] 我能解释为什么加密列不能直接查询,盲索引怎么解决
- [ ] 我能说出盲索引为什么用 HMAC 而不是确定性加密、密钥为什么要独立
- [ ] 我能画出 DEK/KEK 信封加密的分层结构
练习
- 给
todo_api_saas的 users 表增加加密的phone字段和盲索引,实现「按手机号找用户」——把本章方案接进真实项目。 - 实现分批轮换:
rotate_all现在是全表遍历,改成按主键分页(每批 100 行)+ 可中断续跑,模拟生产上亿级表的轮换。 - 思考并写注释:如果需求变成「按身份证前 6 位(地区码)统计分布」,盲索引为什么做不到?可以怎么折衷(提示:额外存一列地区码的盲索引)。
- 把加密密钥的来源从环境变量改成「启动时从一个模拟 KMS 接口拉取被 KEK 包裹的 DEK 并在内存解开」,体会信封加密的落地形态。
动手验证:
cd examples-middleware
docker compose up -d postgres
cargo test -p db_crypto -- --ignored # 含「库里存的确实是密文」的断言