Skip to main content

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}