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
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use axum::extract::{FromRef, Path, State};
|
||||
use axum::Extension;
|
||||
use axum::Json;
|
||||
use sea_orm::{ConnectionTrait, Statement, DatabaseBackend};
|
||||
use serde_json::{json, Value};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -11,7 +12,7 @@ use erp_core::types::{ApiResponse, TenantContext};
|
||||
use crate::state::AppState;
|
||||
|
||||
/// POST /api/v1/admin/tenants/:id/rotate-key
|
||||
/// 密钥轮换 — 生成新 DEK 并使缓存失效
|
||||
/// 密钥轮换 — 生成新 DEK,持久化到 tenant_crypto_keys,使缓存失效
|
||||
pub async fn rotate_tenant_key<S>(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -23,25 +24,53 @@ where
|
||||
{
|
||||
require_permission(&ctx, "tenant.manage")?;
|
||||
|
||||
// 读取当前最大版本号
|
||||
let max_version: Option<i32> = {
|
||||
let row = state.db.query_one(Statement::from_sql_and_values(
|
||||
DatabaseBackend::Postgres,
|
||||
"SELECT COALESCE(MAX(key_version), 0) as v FROM tenant_crypto_keys WHERE tenant_id = $1 AND deleted_at IS NULL",
|
||||
[tenant_id.into()],
|
||||
)).await.map_err(|e| AppError::Internal(format!("查询密钥版本失败: {}", e)))?;
|
||||
row.and_then(|r| r.try_get_by_index::<i32>(0).ok())
|
||||
};
|
||||
let current_version = max_version.unwrap_or(0);
|
||||
let new_version = current_version + 1;
|
||||
|
||||
// 将旧版本标记为不活跃
|
||||
state.db.execute(Statement::from_sql_and_values(
|
||||
DatabaseBackend::Postgres,
|
||||
"UPDATE tenant_crypto_keys SET is_active = false, updated_at = now() WHERE tenant_id = $1 AND is_active = true AND deleted_at IS NULL",
|
||||
[tenant_id.into()],
|
||||
)).await.map_err(|e| AppError::Internal(format!("停用旧 DEK 失败: {}", e)))?;
|
||||
|
||||
// 生成新 DEK 并用 KEK 加密
|
||||
let kek = state.pii_crypto.kek();
|
||||
let (_new_dek, _encrypted_dek) = erp_core::crypto::DekManager::generate_new_dek(kek)
|
||||
let (_new_dek, encrypted_dek) = erp_core::crypto::DekManager::generate_new_dek(kek)
|
||||
.map_err(|e| AppError::Internal(format!("生成新 DEK 失败: {}", e)))?;
|
||||
|
||||
let new_version = 1i32; // TODO: 从 tenant_crypto_keys 表读取当前版本 +1
|
||||
// 持久化新 DEK
|
||||
let new_id = Uuid::now_v7();
|
||||
state.db.execute(Statement::from_sql_and_values(
|
||||
DatabaseBackend::Postgres,
|
||||
"INSERT INTO tenant_crypto_keys (id, tenant_id, encrypted_dek, key_version, is_active, created_at, updated_at, version) VALUES ($1, $2, $3, $4, true, now(), now(), 1)",
|
||||
[new_id.into(), tenant_id.into(), encrypted_dek.into(), new_version.into()],
|
||||
)).await.map_err(|e| AppError::Internal(format!("存储新 DEK 失败: {}", e)))?;
|
||||
|
||||
// 使 DEK 缓存失效
|
||||
state.pii_crypto.dek_manager.invalidate(tenant_id);
|
||||
state.pii_crypto.invalidate_dek(tenant_id);
|
||||
|
||||
tracing::info!(
|
||||
tenant_id = %tenant_id,
|
||||
old_version = current_version,
|
||||
new_version = new_version,
|
||||
"密钥轮换已执行(DEK 缓存已清除)"
|
||||
"密钥轮换完成(新 DEK 已持久化,缓存已清除)"
|
||||
);
|
||||
|
||||
Ok(Json(ApiResponse::ok(json!({
|
||||
"message": "密钥轮换已启动",
|
||||
"message": "密钥轮换已完成",
|
||||
"tenant_id": tenant_id,
|
||||
"old_version": current_version,
|
||||
"new_version": new_version,
|
||||
"note": "后台重加密任务需要单独触发(当前为单 KEK 模式,轮换后新数据使用新 DEK)"
|
||||
"note": "后台重加密任务需要单独触发,旧数据仍可用旧 DEK 解密"
|
||||
}))))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user