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}