Files
hms/crates/erp-core/src/crypto/key_manager.rs
iven 6d5a711d2c
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: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
功能修复:
1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查
2. 仪表盘统计容错:单个查询失败返回零值而非 500
3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致
4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径
5. 积分端点权限码:health.health-data.list → health.points.list
6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage
7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档

Clippy 全 workspace 清零(14→0 errors):
- erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处
- erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处
- erp-ai: 修复 dead_code、unused import 等 11 处
- erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处
- erp-server-migration: 修复 enum_variant_names 5 处
- erp-auth/config/workflow/message: 各 1-3 处

工程改进:
- lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy)
- cargo fmt 统一格式化
2026-05-07 23:43:14 +08:00

226 lines
6.5 KiB
Rust
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.
use std::time::Instant;
use dashmap::DashMap;
use uuid::Uuid;
use crate::error::{AppError, AppResult};
use super::engine;
/// DEK 缓存条目 — Drop 时清零密钥材料
#[derive(Clone)]
struct CachedDek {
dek: [u8; 32],
version: u32,
loaded_at: Instant,
}
impl Drop for CachedDek {
fn drop(&mut self) {
self.dek.fill(0);
}
}
/// DEK 缓存管理 — 每租户独立 DEKLRU + TTL
#[derive(Clone)]
pub struct DekManager {
cache: DashMap<Uuid, CachedDek>,
ttl_secs: u64,
max_entries: usize,
}
impl DekManager {
pub fn new(ttl_secs: u64, max_entries: usize) -> Self {
Self {
cache: DashMap::new(),
ttl_secs,
max_entries,
}
}
/// 获取或创建租户的 DEK
pub fn get_or_create_dek(
&self,
tenant_id: Uuid,
encrypted_dek: Option<&str>,
kek: &[u8; 32],
) -> AppResult<([u8; 32], u32)> {
// 检查缓存
if let Some(entry) = self.cache.get(&tenant_id)
&& entry.loaded_at.elapsed().as_secs() < self.ttl_secs
{
return Ok((entry.dek, entry.version));
}
// 从加密 DEK 解密
if let Some(enc_dek) = encrypted_dek {
let dek_hex = engine::decrypt(kek, enc_dek).map_err(AppError::Internal)?;
let dek_bytes = hex::decode(&dek_hex).map_err(|e| AppError::Internal(e.to_string()))?;
if dek_bytes.len() != 32 {
return Err(AppError::Internal("DEK must be 32 bytes".into()));
}
let mut dek = [0u8; 32];
dek.copy_from_slice(&dek_bytes);
// 缓存(版本从外部传入时无法确定,使用默认值 1
self.evict_if_full();
self.cache.insert(
tenant_id,
CachedDek {
dek,
version: 1,
loaded_at: Instant::now(),
},
);
return Ok((dek, 1));
}
// 无现有 DEK → 生成新的
let dek = Self::generate_dek();
self.evict_if_full();
self.cache.insert(
tenant_id,
CachedDek {
dek,
version: 1,
loaded_at: Instant::now(),
},
);
Ok((dek, 1))
}
/// 使用 KEK 加密 DEK 以便存储
pub fn encrypt_dek_for_storage(dek: &[u8; 32], kek: &[u8; 32]) -> AppResult<String> {
let dek_hex = hex::encode(dek);
engine::encrypt(kek, &dek_hex).map_err(AppError::Internal)
}
/// 生成新 DEK 并用 KEK 加密,返回 (新 DEK, 加密后的 DEK)
pub fn generate_new_dek(kek: &[u8; 32]) -> AppResult<([u8; 32], String)> {
let dek = Self::generate_dek();
let encrypted = Self::encrypt_dek_for_storage(&dek, kek)?;
Ok((dek, encrypted))
}
/// 使缓存失效(轮换后调用)
pub fn invalidate(&self, tenant_id: Uuid) {
self.cache.remove(&tenant_id);
}
fn generate_dek() -> [u8; 32] {
use rand::RngCore;
let mut dek = [0u8; 32];
rand::thread_rng().fill_bytes(&mut dek);
dek
}
fn evict_if_full(&self) {
if self.cache.len() >= self.max_entries {
let to_remove: Vec<Uuid> = self
.cache
.iter()
.filter(|e| e.loaded_at.elapsed().as_secs() > self.ttl_secs / 2)
.map(|e| *e.key())
.take(self.max_entries / 2)
.collect();
for id in to_remove {
self.cache.remove(&id);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::PiiCrypto;
fn test_kek() -> [u8; 32] {
*PiiCrypto::dev_default().kek()
}
fn test_uuid(i: u8) -> Uuid {
let s = format!("00000000-0000-0000-0000-0000000000{:02x}", i);
Uuid::parse_str(&s).unwrap()
}
#[test]
fn generate_new_dek_returns_32_bytes() {
let (dek, _enc) = DekManager::generate_new_dek(&test_kek()).unwrap();
assert_eq!(dek.len(), 32);
}
#[test]
fn generate_new_dek_produces_unique_keys() {
let (dek1, _) = DekManager::generate_new_dek(&test_kek()).unwrap();
let (dek2, _) = DekManager::generate_new_dek(&test_kek()).unwrap();
assert_ne!(dek1, dek2);
}
#[test]
fn encrypt_dek_roundtrip() {
let kek = test_kek();
let (original_dek, encrypted) = DekManager::generate_new_dek(&kek).unwrap();
let mgr = DekManager::new(300, 100);
let tenant_id = test_uuid(1);
let (recovered_dek, _ver) = mgr
.get_or_create_dek(tenant_id, Some(&encrypted), &kek)
.unwrap();
assert_eq!(original_dek, recovered_dek);
}
#[test]
fn get_or_create_generates_when_none() {
let mgr = DekManager::new(300, 100);
let tenant_id = test_uuid(2);
let (dek1, ver1) = mgr.get_or_create_dek(tenant_id, None, &test_kek()).unwrap();
assert_eq!(ver1, 1);
let (dek2, ver2) = mgr.get_or_create_dek(tenant_id, None, &test_kek()).unwrap();
assert_eq!(dek1, dek2);
assert_eq!(ver2, 1);
}
#[test]
fn invalidate_removes_cached_dek() {
let mgr = DekManager::new(300, 100);
let tenant_id = test_uuid(3);
let (dek1, _) = mgr.get_or_create_dek(tenant_id, None, &test_kek()).unwrap();
mgr.invalidate(tenant_id);
let (dek2, _) = mgr.get_or_create_dek(tenant_id, None, &test_kek()).unwrap();
assert_ne!(dek1, dek2);
}
#[test]
fn decrypt_with_wrong_kek_fails() {
let kek1 = test_kek();
let kek2 = [0xffu8; 32];
let (_, encrypted) = DekManager::generate_new_dek(&kek1).unwrap();
let mgr = DekManager::new(300, 100);
let tenant_id = test_uuid(4);
assert!(
mgr.get_or_create_dek(tenant_id, Some(&encrypted), &kek2)
.is_err()
);
}
#[test]
fn expired_entry_not_returned() {
let mgr = DekManager::new(0, 100);
let tenant_id = test_uuid(5);
let (dek1, _) = mgr.get_or_create_dek(tenant_id, None, &test_kek()).unwrap();
let (dek2, _) = mgr.get_or_create_dek(tenant_id, None, &test_kek()).unwrap();
assert_ne!(dek1, dek2);
}
#[test]
fn max_entries_eviction() {
let mgr = DekManager::new(300, 3);
for i in 0..5u8 {
let _ = mgr
.get_or_create_dek(test_uuid(i), None, &test_kek())
.unwrap();
}
assert!(mgr.cache.len() <= 6);
}
}