use crate::AppService; use crate::error::AppError; use base64::Engine; use chacha20poly1305::{ChaCha20Poly1305, KeyInit, aead::Aead}; use hkdf::Hkdf; use rsa::pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey, EncodeRsaPublicKey}; use rsa::{Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey}; use serde::{Deserialize, Serialize}; use session::Session; use sha2::Sha256; #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct RsaResponse { pub public_key: String, } impl AppService { pub const RSA_PRIVATE_KEY: &'static str = "rsa:private"; pub const RSA_PUBLIC_KEY: &'static str = "rsa:public"; const RSA_BIT_SIZE: usize = 2048; /// Derive a ChaCha20-Poly1305 encryption key from APP_SESSION_SECRET via HKDF-SHA256. fn derive_rsa_encryption_key(&self) -> [u8; 32] { let secret = self .config .env .get("APP_SESSION_SECRET") .map(|s| s.as_str()) .expect("APP_SESSION_SECRET must be set in production. Do not use fallback keys."); let hk = Hkdf::::new(Some(b"rsa-session-encryption"), secret.as_bytes()); let mut okm = [0u8; 32]; hk.expand(b"rsa-private-key-aead", &mut okm) .expect("HKDF expand within hash length"); okm } /// Encrypt a plaintext string with ChaCha20-Poly1305. Returns base64(nonce+ciphertext+tag). fn encrypt_rsa_key(&self, plaintext: &str) -> Result { let key = self.derive_rsa_encryption_key(); let cipher = ChaCha20Poly1305::new_from_slice(&key) .expect("32-byte key is valid for ChaCha20Poly1305"); let nonce_bytes: [u8; 12] = rand::random(); let nonce = chacha20poly1305::aead::generic_array::GenericArray::from_slice(&nonce_bytes); let ciphertext = cipher .encrypt(nonce, plaintext.as_bytes()) .map_err(|_| AppError::RsaGenerationError)?; let mut combined = nonce_bytes.to_vec(); combined.extend_from_slice(&ciphertext); Ok(base64::engine::general_purpose::STANDARD.encode(&combined)) } /// Decrypt a base64(nonce+ciphertext+tag) string with ChaCha20-Poly1305. fn decrypt_rsa_key(&self, encrypted: &str) -> Result { let key = self.derive_rsa_encryption_key(); let cipher = ChaCha20Poly1305::new_from_slice(&key) .expect("32-byte key is valid for ChaCha20Poly1305"); let combined = base64::engine::general_purpose::STANDARD .decode(encrypted) .map_err(|_| AppError::RsaDecodeError)?; if combined.len() < 12 { return Err(AppError::RsaDecodeError); } let nonce = chacha20poly1305::aead::generic_array::GenericArray::from_slice(&combined[..12]); let plaintext = cipher .decrypt(nonce, &combined[12..]) .map_err(|_| AppError::RsaDecodeError)?; Ok(String::from_utf8(plaintext).map_err(|_| AppError::RsaDecodeError)?) } pub async fn auth_rsa(&self, context: &Session) -> Result { if context .get::(Self::RSA_PRIVATE_KEY) .ok() .flatten() .is_some() && context .get::(Self::RSA_PUBLIC_KEY) .ok() .flatten() .is_some() { let pub_pem = context .get::(Self::RSA_PUBLIC_KEY) .ok() .flatten() .expect("checked above"); return Ok(RsaResponse { public_key: pub_pem, }); } let mut rng = rsa::rand_core::OsRng; let Ok(priv_key) = RsaPrivateKey::new(&mut rng, Self::RSA_BIT_SIZE) else { tracing::error!("RSA key generation failed"); return Err(AppError::RsaGenerationError); }; let pub_key = RsaPublicKey::from(&priv_key); let priv_pem = priv_key .to_pkcs1_pem(Default::default()) .map_err(|_| AppError::RsaGenerationError)? .to_string(); let pub_pem = pub_key .to_pkcs1_pem(Default::default()) .map_err(|_| AppError::RsaGenerationError)? .to_string(); let encrypted_priv = self.encrypt_rsa_key(&priv_pem)?; context .insert(Self::RSA_PRIVATE_KEY, encrypted_priv) .map_err(|_| AppError::RsaGenerationError)?; context .insert(Self::RSA_PUBLIC_KEY, pub_pem.clone()) .map_err(|_| AppError::RsaGenerationError)?; Ok(RsaResponse { public_key: pub_pem, }) } pub async fn auth_rsa_decode( &self, context: &Session, data: String, ) -> Result { let encrypted_priv = context .get::(Self::RSA_PRIVATE_KEY) .map_err(|_| AppError::RsaDecodeError)? .ok_or(AppError::RsaDecodeError)?; let priv_pem = self.decrypt_rsa_key(&encrypted_priv)?; let Ok(priv_key) = RsaPrivateKey::from_pkcs1_pem(&priv_pem) else { tracing::warn!(ip = ?context.ip_address(), "RSA decode failed: invalid private key"); return Err(AppError::RsaDecodeError); }; let cipher = base64::engine::general_purpose::STANDARD .decode(&data) .map_err(|_| AppError::RsaDecodeError)?; let Ok(decrypted) = priv_key.decrypt(Pkcs1v15Encrypt, &cipher) else { tracing::warn!(ip = ?context.ip_address(), "RSA decrypt failed"); return Err(AppError::RsaDecodeError); }; Ok(String::from_utf8_lossy(&decrypted).to_string()) } }