fix(health): PII 加密安全审计修复 — 2 Critical + 6 Medium + 4 Low
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

审计发现 55 检查点,46 PASS / 7 WARN / 2 FAIL,修复内容:

Critical:
- C1: 密钥轮换端点现在持久化新 DEK 到 tenant_crypto_keys 表
- C2: CachedDek 实现 Drop trait,释放时清零密钥材料

Medium:
- M1: 密文格式添加版本前缀 0x01,向后兼容旧格式
- M2: HMAC 索引使用独立子密钥,与加密 KEK 分离
- M4: 脱敏函数使用 chars() 迭代器,UTF-8 安全
- M5-M6: 医生执业证号详情响应脱敏 (mask_license_number)

Low:
- L1: dek_manager 改为 pub(crate),暴露 invalidate_dek() 方法
- L3: 合并 patient 列表搜索中冗余的重复 HMAC 计算
- L4: update_family_member/update_doctor 更新时设置 key_version
This commit is contained in:
iven
2026-04-26 13:34:25 +08:00
parent 3723cd93c0
commit 7ab57ea1b2
9 changed files with 324 additions and 36 deletions

View File

@@ -0,0 +1,102 @@
# PII 加密功能穷尽审计报告
> 日期: 2026-04-26 | 审计范围: erp-core/crypto + erp-health 全 Entity + API 响应
## 审计概要
| 维度 | 检查点数 | PASS | WARN | FAIL |
|------|---------|------|------|------|
| 加密引擎 | 5 | 4 | 0 | 1 |
| HMAC/搜索索引 | 2 | 1 | 1 | 0 |
| 密钥管理 | 5 | 2 | 2 | 1 |
| 脱敏函数 | 3 | 1 | 2 | 0 |
| 字段覆盖 (8 Entity) | 16 字段 | 16 | 0 | 0 |
| 搜索适配 | 6 | 6 | 0 | 0 |
| 响应泄露 | 17 | 15 | 2 | 0 |
| 错误处理 | 1 | 1 | 0 | 0 |
| **总计** | **55** | **46** | **7** | **2** |
## Critical — 立即修复
### C1: 密钥轮换端点未持久化新 DEK
- **文件**: `crates/erp-server/src/handlers/crypto_admin.rs:27-28`
- **问题**: `rotate_tenant_key` 生成新 DEK 后丢弃(`_new_dek`, `_encrypted_dek`),版本号硬编码为 1
- **影响**: 调用轮换 API 后缓存清空,但 DB 中无新 DEK下次 `get_or_create_dek` 生成全新 DEK导致旧数据不可解密
- **修复**: 将 encrypted_dek 写入 `tenant_crypto_keys` 表,版本 = max(version) + 1
### C2: DEK 密钥材料在内存中未 zeroize
- **文件**: `crates/erp-core/src/crypto/key_manager.rs:12-16`
- **问题**: `CachedDek.dek: [u8; 32]``Drop` 实现,过期后密钥残留在堆内存
- **影响**: crash dump 或内存扫描可能泄露密钥材料
- **修复**: 添加 `impl Drop for CachedDek { fn drop(&mut self) { self.dek.zeroize(); } }`
## Medium — 本迭代修复
### M1: 密文缺少版本标识符
- **文件**: `crates/erp-core/src/crypto/engine.rs`
- **问题**: 密文格式为裸 `Base64(nonce || ciphertext)`,无算法版本前缀
- **影响**: 未来从 AES-256-GCM 迁移到其他算法时,无法区分新旧密文
- **修复**: 输出格式改为 `Base64(0x01 || nonce || ciphertext)`
### M2: HMAC 和加密共用 KEK
- **文件**: `crates/erp-core/src/crypto/hmac_index.rs` + 调用处
- **问题**: HMAC 索引和 AES 加密使用同一密钥,违反密钥分离原则
- **影响**: KEK 泄露后攻击者可同时解密和伪造搜索索引
- **修复**: 派生独立 HMAC 子密钥 `hmac_key = HKDF(kek, "pii-hmac-v1")`
### M3: DEK 版本号硬编码为 1
- **文件**: `crates/erp-core/src/crypto/key_manager.rs:64,76`
- **问题**: 版本始终为 1轮换功能不完整
- **影响**: 无法按版本解密旧数据
- **修复**: 从 `tenant_crypto_keys` 表读取当前版本
### M4: 脱敏函数字节切片对 UTF-8 不安全
- **文件**: `crates/erp-core/src/crypto/masking.rs:3,13`
- **问题**: `&s[..3]``&s[s.len()-4..]` 按字节切片,多字节 UTF-8 字符可能 panic
- **影响**: 国际化数据传入时服务崩溃
- **修复**: 改用 `.chars().take(3).collect::<String>()`
### M5: doctor license_number 详情返回明文未脱敏
- **文件**: `crates/erp-health/src/service/doctor_service.rs:243-244`
- **问题**: `model_to_resp_decrypted` 解密后返回完整执照号,对比 patient 的 id_number 做了 mask
- **影响**: Tier 1 敏感字段通过 API 完整暴露
- **修复**: 添加 `mask_license_number` 脱敏函数
### M6: doctor create/update 返回 license_number 明文
- **同 M5**: `create_doctor``update_doctor` 均使用 `model_to_resp_decrypted`
## Low — 下迭代改进
### L1: dek_manager 字段可见性
- **文件**: `crates/erp-core/src/crypto/mod.rs:18`
- **问题**: `pub dek_manager` 暴露内部缓存操作
- **建议**: 改为 `pub(crate)` 或提供受控公共方法
### L2: DEK 缓存无后台清理
- **文件**: `crates/erp-core/src/crypto/key_manager.rs`
- **问题**: 过期 DEK 仅在访问时评估,无后台 zeroize
- **建议**: 添加 tokio 定时任务定期扫描并清理过期条目
### L3: 冗余 HMAC 计算
- **文件**: `crates/erp-health/src/service/patient_service.rs:61-62`
- **问题**: `search_hash``phone_hash` 对同一输入计算两次相同 HMAC
- **修复**: 合并为一个变量
### L4: update_family_member / update_doctor 未显式重设 key_version
- **文件**: patient_service.rs L584-641, doctor_service.rs L132-181
- **问题**: UPDATE 路径未显式 `Set(Some(1))`,依赖 ActiveModel 保留的原始值
- **风险**: 功能上无影响,但风格不一致,密钥轮换后需修改
## PASS 项摘要
- AES-256-GCM nonce 使用 CSPRNG ✅
- 密文格式正确包含 nonce可分离
- 密钥长度编译期强制 32 字节 ✅
- GCM auth tag 完整性校验 ✅
- KEK 通过环境变量注入,生产构建拒绝 dev_default ✅
- 所有 8 个 Entity 的 16 个 PII 字段全覆盖15 写路径 + 20 读路径)✅
- 搜索适配正确HMAC 精确替代 SQL LIKE
- 列表接口隐藏 Tier 1 字段 ✅
- 详情接口解密+脱敏 ✅
- 审计日志存密文(安全最佳实践)✅
- 错误响应无敏感信息泄露 ✅