ri/auth/security.rs
1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of Ri.
4//! The Ri project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//! http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18//! # Security Utilities Module
19//!
20//! This module provides security-related utilities for Ri, including:
21//! - Configuration encryption and decryption using AES-256-GCM
22//! - Sensitive data protection with HMAC-SHA256 signing
23//! - Cryptographic key generation and management
24//!
25//! ## Encryption
26//!
27//! The module uses AES-256-GCM (Galois/Counter Mode) for symmetric encryption,
28//! providing both confidentiality and authenticity. Nonce values are generated
29//! randomly for each encryption operation.
30//!
31//! ## HMAC Signing
32//!
33//! HMAC-SHA256 is used for message authentication, ensuring data integrity
34//! and authenticity. Both signing and verification functions are provided.
35//!
36//! ## Key Management
37//!
38//! Encryption and HMAC keys are loaded from environment variables:
39//! - `Ri_ENCRYPTION_KEY`: 32-byte hex-encoded key for encryption
40//! - `Ri_HMAC_KEY`: 32-byte hex-encoded key for HMAC
41//!
42//! If not set, keys are generated randomly using cryptographically secure
43//! random number generators.
44//!
45//! ## Security Considerations
46//!
47//! - Keys should be stored securely in production environments
48//! - Randomly generated keys are lost on application restart
49//! - Consider using a secrets management solution for production
50//! - Encrypted data includes a random nonce, so the same plaintext encrypts differently each time
51
52use aes_gcm::aead::Aead;
53use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
54use base64::{engine::general_purpose::STANDARD, Engine as _};
55use generic_array::GenericArray;
56use rand::RngCore;
57use ring::hmac;
58use std::env;
59
60use crate::core::error::RiError;
61use crate::core::error::RiResult;
62
63#[cfg(feature = "pyo3")]
64pub(crate) fn ri_error_to_py_err(e: RiError) -> pyo3::prelude::PyErr {
65 use pyo3::exceptions::*;
66
67 match e {
68 RiError::InvalidInput(_) | RiError::InvalidState(_) | RiError::SecurityViolation(_)
69 | RiError::TomlError(_) | RiError::YamlError(_) | RiError::FrameError(_) => {
70 PyValueError::new_err(e.to_string())
71 }
72 RiError::DeviceNotFound { .. } | RiError::AllocationNotFound { .. }
73 | RiError::ModuleNotFound { .. } | RiError::MissingDependency { .. } => {
74 PyKeyError::new_err(e.to_string())
75 }
76 RiError::CircularDependency { .. } => {
77 PyValueError::new_err(e.to_string())
78 }
79 RiError::Io(_) | RiError::Config(_) | RiError::Serde(_) | RiError::Hook(_)
80 | RiError::Prometheus(_) | RiError::ServiceMesh(_) | RiError::DeviceAllocationFailed { .. }
81 | RiError::ModuleInitFailed { .. } | RiError::ModuleStartFailed { .. } | RiError::ModuleShutdownFailed { .. }
82 | RiError::Other(_) | RiError::ExternalError(_) | RiError::PoolError(_) | RiError::DeviceError(_)
83 | RiError::RedisError(_) | RiError::HttpClientError(_) | RiError::Queue(_)
84 | RiError::Database(_) => {
85 PyRuntimeError::new_err(e.to_string())
86 }
87 }
88}
89
90const ENCRYPTION_KEY_ENV: &str = "Ri_ENCRYPTION_KEY";
91const HMAC_KEY_ENV: &str = "Ri_HMAC_KEY";
92const DEFAULT_KEY_LENGTH: usize = 32;
93const NONCE_LENGTH: usize = 12;
94
95static ENCRYPTION_KEY_WARNED: std::sync::Once = std::sync::Once::new();
96static HMAC_KEY_WARNED: std::sync::Once = std::sync::Once::new();
97
98fn load_or_generate_key(env_var: &str, length: usize, key_name: &str, warned: &std::sync::Once) -> Vec<u8> {
99 if let Ok(s) = env::var(env_var) {
100 if let Ok(key) = hex::decode(&s) {
101 if key.len() >= 16 {
102 return key;
103 }
104 tracing::warn!(
105 "{} from {} is too short ({} bytes), minimum 16 bytes required",
106 key_name, env_var, key.len()
107 );
108 }
109 }
110
111 warned.call_once(|| {
112 tracing::warn!(
113 "SECURITY WARNING: {} not set or invalid. Using ephemeral random key. \
114 Encrypted data will be lost on restart! Set {} environment variable.",
115 key_name, env_var
116 );
117 });
118
119 let mut key = vec![0u8; length];
120 rand::thread_rng().fill_bytes(&mut key);
121 key
122}
123
124fn load_encryption_key() -> Vec<u8> {
125 load_or_generate_key(ENCRYPTION_KEY_ENV, DEFAULT_KEY_LENGTH, "Encryption key", &ENCRYPTION_KEY_WARNED)
126}
127
128fn load_hmac_key() -> Vec<u8> {
129 load_or_generate_key(HMAC_KEY_ENV, DEFAULT_KEY_LENGTH, "HMAC key", &HMAC_KEY_WARNED)
130}
131
132/// Checks if encryption keys are properly configured.
133///
134/// Returns `Ok(())` if both encryption and HMAC keys are set via environment variables.
135/// Returns an error if any key is missing, with instructions on how to set them.
136#[allow(dead_code)]
137pub fn check_encryption_keys() -> RiResult<()> {
138 let encryption_key_set = env::var(ENCRYPTION_KEY_ENV)
139 .ok()
140 .and_then(|s| hex::decode(&s).ok())
141 .map(|k| k.len() >= 16)
142 .unwrap_or(false);
143
144 let hmac_key_set = env::var(HMAC_KEY_ENV)
145 .ok()
146 .and_then(|s| hex::decode(&s).ok())
147 .map(|k| k.len() >= 16)
148 .unwrap_or(false);
149
150 if !encryption_key_set || !hmac_key_set {
151 let mut missing = Vec::new();
152 if !encryption_key_set {
153 missing.push(ENCRYPTION_KEY_ENV);
154 }
155 if !hmac_key_set {
156 missing.push(HMAC_KEY_ENV);
157 }
158
159 return Err(RiError::SecurityViolation(format!(
160 "Encryption keys not configured: {}. \
161 Generate keys using RiSecurityManager::generate_encryption_key() and \
162 RiSecurityManager::generate_hmac_key(), then set them as environment variables. \
163 WARNING: Without proper keys, encrypted data will be lost on restart!",
164 missing.join(", ")
165 )));
166 }
167
168 Ok(())
169}
170
171/// Security utilities manager for Ri.
172///
173/// This struct provides static methods for encryption, decryption, HMAC signing,
174/// and key management operations. It is designed as a singleton utility class
175/// with no instance state.
176///
177/// ## Thread Safety
178///
179/// All methods are stateless and can be safely called concurrently from multiple threads.
180///
181/// ## Usage
182///
183/// ```rust,ignore
184/// use ri::auth::security::RiSecurityManager;
185///
186/// // Encrypt sensitive data
187/// let encrypted = RiSecurityManager::encrypt("secret data");
188///
189/// // Decrypt data
190/// let decrypted = RiSecurityManager::decrypt(&encrypted);
191///
192/// // Sign data with HMAC
193/// let signature = RiSecurityManager::hmac_sign("data to sign");
194///
195/// // Verify HMAC signature
196/// let is_valid = RiSecurityManager::hmac_verify("data to verify", &signature);
197/// ```
198#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
199pub struct RiSecurityManager;
200
201#[cfg(feature = "pyo3")]
202#[pyo3::prelude::pymethods]
203impl RiSecurityManager {
204 #[new]
205 fn py_new() -> Self {
206 Self
207 }
208
209 #[staticmethod]
210 fn encrypt_py(plaintext: &str) -> pyo3::prelude::PyResult<String> {
211 Self::encrypt(plaintext).map_err(ri_error_to_py_err)
212 }
213
214 #[staticmethod]
215 fn decrypt_py(encrypted: &str) -> pyo3::prelude::PyResult<String> {
216 Self::decrypt(encrypted).map_err(ri_error_to_py_err)
217 }
218
219 #[staticmethod]
220 fn hmac_sign_py(data: &str) -> String {
221 Self::hmac_sign(data)
222 }
223
224 #[staticmethod]
225 fn hmac_verify_py(data: &str, signature: &str) -> bool {
226 Self::hmac_verify(data, signature)
227 }
228
229 #[staticmethod]
230 fn generate_encryption_key_py() -> String {
231 Self::generate_encryption_key()
232 }
233
234 #[staticmethod]
235 fn generate_hmac_key_py() -> String {
236 Self::generate_hmac_key()
237 }
238}
239
240impl RiSecurityManager {
241 /// Encrypts plaintext data using AES-256-GCM.
242 ///
243 /// This method encrypts the input string using AES-256-GCM (Galois/Counter Mode),
244 /// which provides both confidentiality and authenticity. A random nonce is generated
245 /// for each encryption operation, so the same plaintext produces different ciphertext
246 /// each time it is encrypted.
247 ///
248 /// ## Output Format
249 ///
250 /// The output is Base64-encoded and contains:
251 /// - 12-byte nonce (randomly generated)
252 /// - Encrypted data with authentication tag
253 ///
254 /// # Parameters
255 ///
256 /// - `plaintext`: The text string to encrypt
257 ///
258 /// # Returns
259 ///
260 /// `RiResult<String>` containing Base64-encoded encrypted data on success
261 ///
262 /// # Examples
263 ///
264 /// ```rust,ignore
265 /// use ri::auth::security::RiSecurityManager;
266 ///
267 /// let encrypted = RiSecurityManager::encrypt("sensitive data");
268 /// println!("Encrypted: {}", encrypted);
269 /// ```
270 pub fn encrypt(plaintext: &str) -> RiResult<String> {
271 let key = load_encryption_key();
272 let nonce = {
273 let mut n = [0u8; NONCE_LENGTH];
274 rand::thread_rng().fill_bytes(&mut n);
275 n
276 };
277
278 let cipher = Aes256Gcm::new(GenericArray::from_slice(&key));
279 let ciphertext = cipher
280 .encrypt(Nonce::from_slice(&nonce), plaintext.as_bytes())
281 .map_err(|e| RiError::SecurityViolation(format!("encryption failed: {}", e)))?;
282
283 let mut result = Vec::with_capacity(nonce.len() + ciphertext.len());
284 result.extend_from_slice(&nonce);
285 result.extend_from_slice(&ciphertext);
286
287 Ok(STANDARD.encode(result))
288 }
289
290 /// Decrypts encrypted data using AES-256-GCM.
291 ///
292 /// This method decrypts data that was encrypted using the `encrypt` method.
293 /// It verifies the authentication tag and returns the original plaintext.
294 ///
295 /// ## Failure Conditions
296 ///
297 /// Returns `Err(RiError::SecurityViolation(...))` if:
298 /// - The input is not valid Base64
299 /// - The input is shorter than the nonce length
300 /// - The authentication tag verification fails (wrong key or tampered data)
301 /// - UTF-8 decoding of the decrypted data fails
302 ///
303 /// # Parameters
304 ///
305 /// - `encrypted`: Base64-encoded encrypted data
306 ///
307 /// # Returns
308 ///
309 /// `RiResult<String>` containing the decrypted plaintext on success
310 ///
311 /// # Examples
312 ///
313 /// ```rust,ignore
314 /// use ri::auth::security::RiSecurityManager;
315 ///
316 /// let encrypted = RiSecurityManager::encrypt("secret")?;
317 /// let decrypted = RiSecurityManager::decrypt(&encrypted)?;
318 /// println!("Decrypted: {}", decrypted);
319 /// ```
320 pub fn decrypt(encrypted: &str) -> RiResult<String> {
321 let key = load_encryption_key();
322 let data = STANDARD.decode(encrypted)
323 .map_err(|e| RiError::SecurityViolation(format!("Base64 decode failed: {}", e)))?;
324
325 if data.len() < NONCE_LENGTH {
326 return Err(RiError::SecurityViolation(
327 format!("Encrypted data too short: expected at least {} bytes, got {}",
328 NONCE_LENGTH, data.len())
329 ));
330 }
331
332 let (nonce, ciphertext) = data.split_at(NONCE_LENGTH);
333 let cipher = Aes256Gcm::new(GenericArray::from_slice(&key));
334
335 let plaintext = cipher
336 .decrypt(Nonce::from_slice(nonce), ciphertext)
337 .map_err(|e| RiError::SecurityViolation(format!("Decryption failed: {}", e)))?;
338
339 String::from_utf8(plaintext)
340 .map_err(|e| RiError::SecurityViolation(format!("UTF-8 decode failed: {}", e)))
341 }
342
343 /// Signs data using HMAC-SHA256.
344 ///
345 /// This method creates an HMAC signature using the configured HMAC key
346 /// and SHA-256 hash algorithm. The signature is returned as a hex-encoded string.
347 ///
348 /// ## Security
349 ///
350 /// HMAC provides message integrity and authenticity verification. Only parties
351 /// with access to the HMAC key can create or verify signatures.
352 ///
353 /// # Parameters
354 ///
355 /// - `data`: The data string to sign
356 ///
357 /// # Returns
358 ///
359 /// Hex-encoded HMAC signature
360 ///
361 /// # Examples
362 ///
363 /// ```rust,ignore
364 /// use ri::auth::security::RiSecurityManager;
365 ///
366 /// let data = "important message";
367 /// let signature = RiSecurityManager::hmac_sign(data);
368 /// println!("Signature: {}", signature);
369 /// ```
370 pub fn hmac_sign(data: &str) -> String {
371 let key = load_hmac_key();
372 let signing_key = hmac::Key::new(hmac::HMAC_SHA256, &key);
373 let signature = hmac::sign(&signing_key, data.as_bytes());
374 hex::encode(signature)
375 }
376
377 /// Verifies an HMAC-SHA256 signature.
378 ///
379 /// This method verifies that the provided signature matches the data using
380 /// constant-time comparison to prevent timing attacks.
381 ///
382 /// ## Signature Format
383 ///
384 /// The signature must be a valid hex-encoded string as produced by `hmac_sign`.
385 ///
386 /// # Parameters
387 ///
388 /// - `data`: The original data that was signed
389 /// - `signature`: The hex-encoded signature to verify
390 ///
391 /// # Returns
392 ///
393 /// `true` if the signature is valid, `false` otherwise
394 ///
395 /// # Examples
396 ///
397 /// ```rust,ignore
398 /// use ri::auth::security::RiSecurityManager;
399 ///
400 /// let data = "important message";
401 /// let signature = RiSecurityManager::hmac_sign(data);
402 ///
403 /// if RiSecurityManager::hmac_verify(data, &signature) {
404 /// println!("Signature is valid!");
405 /// } else {
406 /// println!("Signature is invalid!");
407 /// }
408 /// ```
409 pub fn hmac_verify(data: &str, signature: &str) -> bool {
410 let expected = match hex::decode(signature) {
411 Ok(sig) => sig,
412 Err(_) => {
413 log::warn!("[Ri.Security] Invalid hex signature format");
414 return false;
415 }
416 };
417 let key = load_hmac_key();
418 let signing_key = hmac::Key::new(hmac::HMAC_SHA256, &key);
419 hmac::verify(&signing_key, data.as_bytes(), &expected).is_ok()
420 }
421
422 /// Generates a new encryption key.
423 ///
424 /// This method generates a cryptographically secure random 32-byte (256-bit) key
425 /// suitable for AES-256 encryption. The key is returned as a hex-encoded string.
426 ///
427 /// ## Usage
428 ///
429 /// This method can be used to generate keys for initial configuration or key rotation.
430 /// Store the generated key securely and set it via the `Ri_ENCRYPTION_KEY` environment variable.
431 ///
432 /// # Returns
433 ///
434 /// Hex-encoded 32-byte encryption key
435 ///
436 /// # Examples
437 ///
438 /// ```rust,ignore
439 /// use ri::auth::security::RiSecurityManager;
440 ///
441 /// let key = RiSecurityManager::generate_encryption_key();
442 /// println!("New encryption key: {}", key);
443 /// ```
444 pub fn generate_encryption_key() -> String {
445 let mut key = vec![0u8; DEFAULT_KEY_LENGTH];
446 rand::thread_rng().fill_bytes(&mut key);
447 hex::encode(key)
448 }
449
450 /// Generates a new HMAC key.
451 ///
452 /// This method generates a cryptographically secure random 32-byte (256-bit) key
453 /// suitable for HMAC-SHA256 signing. The key is returned as a hex-encoded string.
454 ///
455 /// ## Usage
456 ///
457 /// This method can be used to generate keys for initial configuration or key rotation.
458 /// Store the generated key securely and set it via the `Ri_HMAC_KEY` environment variable.
459 ///
460 /// # Returns
461 ///
462 /// Hex-encoded 32-byte HMAC key
463 ///
464 /// # Examples
465 ///
466 /// ```rust,ignore
467 /// use ri::auth::security::RiSecurityManager;
468 ///
469 /// let key = RiSecurityManager::generate_hmac_key();
470 /// println!("New HMAC key: {}", key);
471 /// ```
472 pub fn generate_hmac_key() -> String {
473 let mut key = vec![0u8; DEFAULT_KEY_LENGTH];
474 rand::thread_rng().fill_bytes(&mut key);
475 hex::encode(key)
476 }
477}