Files
hms/docs/discussions/2026-04-26-pii-encryption-audit.md
iven 7ab57ea1b2
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
fix(health): PII 加密安全审计修复 — 2 Critical + 6 Medium + 4 Low
审计发现 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
2026-04-26 13:34:25 +08:00

103 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 字段 ✅
- 详情接口解密+脱敏 ✅
- 审计日志存密文(安全最佳实践)✅
- 错误响应无敏感信息泄露 ✅