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
60const ENCRYPTION_KEY_ENV: &str = "Ri_ENCRYPTION_KEY";
61const HMAC_KEY_ENV: &str = "Ri_HMAC_KEY";
62const DEFAULT_KEY_LENGTH: usize = 32;
63const NONCE_LENGTH: usize = 12;
64
65fn load_or_generate_key(env_var: &str, length: usize) -> Vec<u8> {
66    env::var(env_var)
67        .ok()
68        .and_then(|s| hex::decode(s).ok())
69        .unwrap_or_else(|| {
70            let mut key = vec![0u8; length];
71            rand::thread_rng().fill_bytes(&mut key);
72            key
73        })
74}
75
76fn load_encryption_key() -> Vec<u8> {
77    load_or_generate_key(ENCRYPTION_KEY_ENV, DEFAULT_KEY_LENGTH)
78}
79
80fn load_hmac_key() -> Vec<u8> {
81    load_or_generate_key(HMAC_KEY_ENV, DEFAULT_KEY_LENGTH)
82}
83
84/// Security utilities manager for Ri.
85///
86/// This struct provides static methods for encryption, decryption, HMAC signing,
87/// and key management operations. It is designed as a singleton utility class
88/// with no instance state.
89///
90/// ## Thread Safety
91///
92/// All methods are stateless and can be safely called concurrently from multiple threads.
93///
94/// ## Usage
95///
96/// ```rust,ignore
97/// use ri::auth::security::RiSecurityManager;
98///
99/// // Encrypt sensitive data
100/// let encrypted = RiSecurityManager::encrypt("secret data");
101///
102/// // Decrypt data
103/// let decrypted = RiSecurityManager::decrypt(&encrypted);
104///
105/// // Sign data with HMAC
106/// let signature = RiSecurityManager::hmac_sign("data to sign");
107///
108/// // Verify HMAC signature
109/// let is_valid = RiSecurityManager::hmac_verify("data to verify", &signature);
110/// ```
111#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
112pub struct RiSecurityManager;
113
114#[cfg(feature = "pyo3")]
115#[pyo3::prelude::pymethods]
116impl RiSecurityManager {
117    #[new]
118    fn py_new() -> Self {
119        Self
120    }
121
122    #[staticmethod]
123    fn encrypt_py(plaintext: &str) -> String {
124        Self::encrypt(plaintext)
125    }
126
127    #[staticmethod]
128    fn decrypt_py(encrypted: &str) -> Option<String> {
129        Self::decrypt(encrypted)
130    }
131
132    #[staticmethod]
133    fn hmac_sign_py(data: &str) -> String {
134        Self::hmac_sign(data)
135    }
136
137    #[staticmethod]
138    fn hmac_verify_py(data: &str, signature: &str) -> bool {
139        Self::hmac_verify(data, signature)
140    }
141
142    #[staticmethod]
143    fn generate_encryption_key_py() -> String {
144        Self::generate_encryption_key()
145    }
146
147    #[staticmethod]
148    fn generate_hmac_key_py() -> String {
149        Self::generate_hmac_key()
150    }
151}
152
153impl RiSecurityManager {
154    /// Encrypts plaintext data using AES-256-GCM.
155    ///
156    /// This method encrypts the input string using AES-256-GCM (Galois/Counter Mode),
157    /// which provides both confidentiality and authenticity. A random nonce is generated
158    /// for each encryption operation, so the same plaintext produces different ciphertext
159    /// each time it is encrypted.
160    ///
161    /// ## Output Format
162    ///
163    /// The output is Base64-encoded and contains:
164    /// - 12-byte nonce (randomly generated)
165    /// - Encrypted data with authentication tag
166    ///
167    /// # Parameters
168    ///
169    /// - `plaintext`: The text string to encrypt
170    ///
171    /// # Returns
172    ///
173    /// Base64-encoded encrypted data
174    ///
175    /// # Examples
176    ///
177    /// ```rust,ignore
178    /// use ri::auth::security::RiSecurityManager;
179    ///
180    /// let encrypted = RiSecurityManager::encrypt("sensitive data");
181    /// println!("Encrypted: {}", encrypted);
182    /// ```
183    pub fn encrypt(plaintext: &str) -> String {
184        let key = load_encryption_key();
185        let nonce = {
186            let mut n = [0u8; NONCE_LENGTH];
187            rand::thread_rng().fill_bytes(&mut n);
188            n
189        };
190
191        let cipher = Aes256Gcm::new(GenericArray::from_slice(&key));
192        let ciphertext = cipher
193            .encrypt(Nonce::from_slice(&nonce), plaintext.as_bytes())
194            .expect("encryption failure");
195
196        let mut result = Vec::with_capacity(nonce.len() + ciphertext.len());
197        result.extend_from_slice(&nonce);
198        result.extend_from_slice(&ciphertext);
199
200        STANDARD.encode(result)
201    }
202
203    /// Decrypts encrypted data using AES-256-GCM.
204    ///
205    /// This method decrypts data that was encrypted using the `encrypt` method.
206    /// It verifies the authentication tag and returns the original plaintext.
207    ///
208    /// ## Failure Conditions
209    ///
210    /// Returns `None` if:
211    /// - The input is not valid Base64
212    /// - The input is shorter than the nonce length
213    /// - The authentication tag verification fails (wrong key or tampered data)
214    ///
215    /// # Parameters
216    ///
217    /// - `encrypted`: Base64-encoded encrypted data
218    ///
219    /// # Returns
220    ///
221    /// `Some(String)` containing the decrypted plaintext, or `None` if decryption fails
222    ///
223    /// # Examples
224    ///
225    /// ```rust,ignore
226    /// use ri::auth::security::RiSecurityManager;
227    ///
228    /// let encrypted = RiSecurityManager::encrypt("secret");
229    /// let decrypted = RiSecurityManager::decrypt(&encrypted);
230    ///
231    /// match decrypted {
232    ///     Some(text) => println!("Decrypted: {}", text),
233    ///     None => println!("Decryption failed!"),
234    /// }
235    /// ```
236    pub fn decrypt(encrypted: &str) -> Option<String> {
237        let key = load_encryption_key();
238        let data = STANDARD.decode(encrypted).ok()?;
239
240        if data.len() < NONCE_LENGTH {
241            return None;
242        }
243
244        let (nonce, ciphertext) = data.split_at(NONCE_LENGTH);
245        let cipher = Aes256Gcm::new(GenericArray::from_slice(&key));
246
247        cipher
248            .decrypt(Nonce::from_slice(nonce), ciphertext)
249            .ok()
250            .map(|v| String::from_utf8(v).ok())?
251    }
252
253    /// Signs data using HMAC-SHA256.
254    ///
255    /// This method creates an HMAC signature using the configured HMAC key
256    /// and SHA-256 hash algorithm. The signature is returned as a hex-encoded string.
257    ///
258    /// ## Security
259    ///
260    /// HMAC provides message integrity and authenticity verification. Only parties
261    /// with access to the HMAC key can create or verify signatures.
262    ///
263    /// # Parameters
264    ///
265    /// - `data`: The data string to sign
266    ///
267    /// # Returns
268    ///
269    /// Hex-encoded HMAC signature
270    ///
271    /// # Examples
272    ///
273    /// ```rust,ignore
274    /// use ri::auth::security::RiSecurityManager;
275    ///
276    /// let data = "important message";
277    /// let signature = RiSecurityManager::hmac_sign(data);
278    /// println!("Signature: {}", signature);
279    /// ```
280    pub fn hmac_sign(data: &str) -> String {
281        let key = load_hmac_key();
282        let signing_key = hmac::Key::new(hmac::HMAC_SHA256, &key);
283        let signature = hmac::sign(&signing_key, data.as_bytes());
284        hex::encode(signature)
285    }
286
287    /// Verifies an HMAC-SHA256 signature.
288    ///
289    /// This method verifies that the provided signature matches the data using
290    /// constant-time comparison to prevent timing attacks.
291    ///
292    /// ## Signature Format
293    ///
294    /// The signature must be a valid hex-encoded string as produced by `hmac_sign`.
295    ///
296    /// # Parameters
297    ///
298    /// - `data`: The original data that was signed
299    /// - `signature`: The hex-encoded signature to verify
300    ///
301    /// # Returns
302    ///
303    /// `true` if the signature is valid, `false` otherwise
304    ///
305    /// # Examples
306    ///
307    /// ```rust,ignore
308    /// use ri::auth::security::RiSecurityManager;
309    ///
310    /// let data = "important message";
311    /// let signature = RiSecurityManager::hmac_sign(data);
312    ///
313    /// if RiSecurityManager::hmac_verify(data, &signature) {
314    ///     println!("Signature is valid!");
315    /// } else {
316    ///     println!("Signature is invalid!");
317    /// }
318    /// ```
319    pub fn hmac_verify(data: &str, signature: &str) -> bool {
320        let expected = hex::decode(signature).ok().unwrap_or_default();
321        let key = load_hmac_key();
322        let signing_key = hmac::Key::new(hmac::HMAC_SHA256, &key);
323        hmac::verify(&signing_key, data.as_bytes(), &expected).is_ok()
324    }
325
326    /// Generates a new encryption key.
327    ///
328    /// This method generates a cryptographically secure random 32-byte (256-bit) key
329    /// suitable for AES-256 encryption. The key is returned as a hex-encoded string.
330    ///
331    /// ## Usage
332    ///
333    /// This method can be used to generate keys for initial configuration or key rotation.
334    /// Store the generated key securely and set it via the `Ri_ENCRYPTION_KEY` environment variable.
335    ///
336    /// # Returns
337    ///
338    /// Hex-encoded 32-byte encryption key
339    ///
340    /// # Examples
341    ///
342    /// ```rust,ignore
343    /// use ri::auth::security::RiSecurityManager;
344    ///
345    /// let key = RiSecurityManager::generate_encryption_key();
346    /// println!("New encryption key: {}", key);
347    /// ```
348    pub fn generate_encryption_key() -> String {
349        let mut key = vec![0u8; DEFAULT_KEY_LENGTH];
350        rand::thread_rng().fill_bytes(&mut key);
351        hex::encode(key)
352    }
353
354    /// Generates a new HMAC key.
355    ///
356    /// This method generates a cryptographically secure random 32-byte (256-bit) key
357    /// suitable for HMAC-SHA256 signing. The key is returned as a hex-encoded string.
358    ///
359    /// ## Usage
360    ///
361    /// This method can be used to generate keys for initial configuration or key rotation.
362    /// Store the generated key securely and set it via theRi_HMAC_KEY` environment variable.
363    ///
364    /// # Returns
365    ///
366    /// Hex-encoded 32-byte HMAC key
367    ///
368    /// # Examples
369    ///
370    /// ```rust,ignore
371    /// use ri::auth::security::RiSecurityManager;
372    ///
373    /// let key = RiSecurityManager::generate_hmac_key();
374    /// println!("New HMAC key: {}", key);
375    /// ```
376    pub fn generate_hmac_key() -> String {
377        let mut key = vec![0u8; DEFAULT_KEY_LENGTH];
378        rand::thread_rng().fill_bytes(&mut key);
379        hex::encode(key)
380    }
381}