- crypto.rs: AES-256-GCM 加密/解密 + HMAC-SHA256 索引 - create/update: id_number 加密存储, id_number_hash 索引 - list: 不返回 id_number, 手机号掩码 - detail: 解密后身份证掩码(前3后4), 手机号掩码 - 搜索: 改用 HMAC 精确匹配(不再模糊搜索加密列) - 迁移 m000048: 添加 patients.id_number_hash 列
91 lines
3.4 KiB
Rust
91 lines
3.4 KiB
Rust
use aes_gcm::aead::Aead;
|
|
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
|
|
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
|
|
use hmac::{Hmac, Mac};
|
|
use sha2::Sha256;
|
|
|
|
use erp_core::error::{AppError, AppResult};
|
|
|
|
type HmacSha256 = Hmac<Sha256>;
|
|
|
|
#[derive(Clone)]
|
|
pub struct HealthCrypto {
|
|
aes_key: [u8; 32],
|
|
hmac_key: [u8; 32],
|
|
}
|
|
|
|
impl HealthCrypto {
|
|
pub fn from_keys(aes_key_hex: &str, hmac_key_hex: &str) -> AppResult<Self> {
|
|
let aes_key = hex::decode(aes_key_hex)
|
|
.map_err(|e| AppError::Internal(format!("AES key hex decode failed: {}", e)))?;
|
|
let hmac_key = hex::decode(hmac_key_hex)
|
|
.map_err(|e| AppError::Internal(format!("HMAC key hex decode failed: {}", e)))?;
|
|
if aes_key.len() != 32 || hmac_key.len() != 32 {
|
|
return Err(AppError::Internal(
|
|
"Encryption keys must be 32 bytes each".into(),
|
|
));
|
|
}
|
|
let mut aes = [0u8; 32];
|
|
let mut hmac = [0u8; 32];
|
|
aes.copy_from_slice(&aes_key);
|
|
hmac.copy_from_slice(&hmac_key);
|
|
Ok(Self {
|
|
aes_key: aes,
|
|
hmac_key: hmac,
|
|
})
|
|
}
|
|
|
|
/// Dev fallback: derive deterministic keys from a single dev string.
|
|
/// DO NOT use in production.
|
|
pub fn dev_default() -> Self {
|
|
use sha2::Digest;
|
|
let aes_key = <Sha256 as Digest>::digest(b"erp-health-aes-dev-key-DO-NOT-USE-IN-PROD");
|
|
let hmac_key = <Sha256 as Digest>::digest(b"erp-health-hmac-dev-key-DO-NOT-USE-IN-PROD");
|
|
let mut aes = [0u8; 32];
|
|
let mut hmac = [0u8; 32];
|
|
aes.copy_from_slice(&aes_key);
|
|
hmac.copy_from_slice(&hmac_key);
|
|
Self {
|
|
aes_key: aes,
|
|
hmac_key: hmac,
|
|
}
|
|
}
|
|
|
|
pub fn encrypt(&self, plaintext: &str) -> AppResult<String> {
|
|
let cipher = Aes256Gcm::new_from_slice(&self.aes_key)
|
|
.map_err(|e| AppError::Internal(format!("AES init failed: {}", e)))?;
|
|
let nonce_bytes = uuid::Uuid::now_v7();
|
|
let nonce = Nonce::from_slice(&nonce_bytes.as_bytes()[..12]);
|
|
let ciphertext = cipher
|
|
.encrypt(nonce, plaintext.as_bytes())
|
|
.map_err(|e| AppError::Internal(format!("Encryption failed: {}", e)))?;
|
|
let mut combined = nonce_bytes.as_bytes()[..12].to_vec();
|
|
combined.extend_from_slice(&ciphertext);
|
|
Ok(BASE64.encode(&combined))
|
|
}
|
|
|
|
pub fn decrypt(&self, encoded: &str) -> AppResult<String> {
|
|
let combined = BASE64
|
|
.decode(encoded)
|
|
.map_err(|e| AppError::Internal(format!("Base64 decode failed: {}", e)))?;
|
|
if combined.len() < 12 {
|
|
return Err(AppError::Internal("Ciphertext too short".into()));
|
|
}
|
|
let (nonce_bytes, ciphertext) = combined.split_at(12);
|
|
let cipher = Aes256Gcm::new_from_slice(&self.aes_key)
|
|
.map_err(|e| AppError::Internal(format!("AES init failed: {}", e)))?;
|
|
let plaintext = cipher
|
|
.decrypt(Nonce::from_slice(nonce_bytes), ciphertext)
|
|
.map_err(|e| AppError::Internal(format!("Decryption failed: {}", e)))?;
|
|
String::from_utf8(plaintext)
|
|
.map_err(|e| AppError::Internal(format!("UTF-8 decode failed: {}", e)))
|
|
}
|
|
|
|
pub fn hmac_hash(&self, value: &str) -> String {
|
|
let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(&self.hmac_key)
|
|
.expect("HMAC key length is valid");
|
|
mac.update(value.as_bytes());
|
|
hex::encode(mac.finalize().into_bytes())
|
|
}
|
|
}
|