Skip to main content

fr_rust/crypto/
crypto.rs

1use aes_gcm::{
2    aead::{Aead, KeyInit},
3    Aes256Gcm, Nonce,
4};
5use argon2::{
6    password_hash::{
7        rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString,
8    },
9    Argon2,
10};
11use base64::{engine::general_purpose, Engine as _};
12use rand::Rng;
13use sha2::{Digest, Sha256};
14use thiserror::Error;
15use tokio::task;
16
17// --- ERROR HANDLING ---
18
19#[derive(Error, Debug)]
20pub enum CryptoError {
21    #[error("Invalid encryption key length")]
22    InvalidKeyLength,
23
24    #[error("Encryption failed")]
25    EncryptionFailed,
26
27    #[error("Decryption failed")]
28    DecryptionFailed,
29
30    #[error("Invalid encrypted data: too short")]
31    InvalidDataLength,
32
33    #[error("Base64 decode error: {0}")]
34    Base64(#[from] base64::DecodeError),
35
36    #[error("Invalid UTF-8 sequence: {0}")]
37    Utf8(#[from] std::string::FromUtf8Error),
38
39    #[error("Argon2 hashing error: {0}")]
40    Argon2(#[from] argon2::password_hash::Error),
41
42    #[error("Async task join error: {0}")]
43    Join(#[from] tokio::task::JoinError),
44}
45
46pub type Result<T> = std::result::Result<T, CryptoError>;
47
48// --- DATA STRUCTURES ---
49
50pub struct EncryptedData {
51    pub encrypted_text: String,
52}
53
54pub struct DecryptedData {
55    pub text: String,
56}
57
58pub struct HashedData {
59    pub hash: String,
60}
61
62// --- OOP SERVICE ---
63
64#[derive(Clone)]
65pub struct CryptoService {
66    cipher: Aes256Gcm,
67}
68
69impl CryptoService {
70    /// Constructor: Initializes the AES cipher once.
71    /// Fixes the performance issue of expanding the key schedule on every request.
72    pub fn new(encryption_key: &[u8; 32]) -> Result<Self> {
73        let cipher = Aes256Gcm::new_from_slice(encryption_key)
74            .map_err(|_| CryptoError::InvalidKeyLength)?;
75
76        Ok(Self { cipher })
77    }
78
79    /// Encrypts plaintext into a base64-encoded string (Nonce + Ciphertext)
80    /// Purely CPU-bound and fast: Kept synchronous to avoid async executor overhead.
81    pub fn encrypt_text(&self, text: &str) -> Result<EncryptedData> {
82        let mut nonce_bytes = [0u8; 12];
83        rand::rng().fill_bytes(&mut nonce_bytes);
84        let nonce = Nonce::from_slice(&nonce_bytes);
85
86        // Encrypt the data
87        let ciphertext = self
88            .cipher
89            .encrypt(nonce, text.as_bytes())
90            .map_err(|_| CryptoError::EncryptionFailed)?;
91
92        // Pre-allocate exact capacity to prevent reallocation vectors
93        let mut combined = Vec::with_capacity(12 + ciphertext.len());
94        combined.extend_from_slice(&nonce_bytes);
95        combined.extend_from_slice(&ciphertext);
96
97        Ok(EncryptedData {
98            encrypted_text: general_purpose::STANDARD.encode(combined),
99        })
100    }
101
102    /// Decrypts a base64-encoded string back to plaintext
103    /// Purely CPU-bound and fast: Kept synchronous.
104    pub fn decrypt_text(&self, encrypted_text: &str) -> Result<DecryptedData> {
105        let decoded = general_purpose::STANDARD.decode(encrypted_text)?;
106
107        if decoded.len() < 12 {
108            return Err(CryptoError::InvalidDataLength);
109        }
110
111        let (nonce_bytes, ciphertext) = decoded.split_at(12);
112        let nonce = Nonce::from_slice(nonce_bytes);
113
114        // Decrypt the data
115        let plaintext = self
116            .cipher
117            .decrypt(nonce, ciphertext)
118            .map_err(|_| CryptoError::DecryptionFailed)?;
119
120        Ok(DecryptedData {
121            text: String::from_utf8(plaintext)?,
122        })
123    }
124
125    /// Hashes a string using SHA-256 and returns a hex-encoded string.
126    /// Fast, non-blocking: Kept synchronous.
127    pub fn sha256_hash(&self, data: &str) -> Result<HashedData> {
128        let mut hasher = Sha256::new();
129        hasher.update(data.as_bytes());
130        let result = hasher.finalize();
131
132        // Format raw bytes directly into a 64-character lowercase hex string
133        let hash = format!("{:x}", result);
134
135        Ok(HashedData { hash })
136    }
137
138    /// Hashes a string using Argon2.
139    /// Heavy CPU/Memory usage: Must remain async and run on a blocking thread pool.
140    pub async fn hash_data(&self, data: &str) -> Result<HashedData> {
141        let data = data.to_string();
142
143        let hash = task::spawn_blocking(move || -> Result<String> {
144            let salt = SaltString::generate(&mut OsRng);
145            let argon2 = Argon2::default();
146
147            let hashed = argon2
148                .hash_password(data.as_bytes(), &salt)?; // `?` cleanly converts to CryptoError::Argon2
149            
150            Ok(hashed.to_string())
151        })
152        .await??;
153
154        Ok(HashedData { hash })
155    }
156
157    /// Verifies a string against an Argon2 hash.
158    /// Heavy CPU usage: Must remain async and run on a blocking thread pool.
159    pub async fn verify_hash(&self, data: &str, hash: &str) -> Result<bool> {
160        let data = data.to_string();
161        let hash = hash.to_string();
162
163        let is_valid = task::spawn_blocking(move || {
164            match PasswordHash::new(&hash) {
165                Ok(parsed) => Argon2::default()
166                    .verify_password(data.as_bytes(), &parsed)
167                    .is_ok(),
168                Err(_) => false,
169            }
170        })
171        .await?;
172
173        Ok(is_valid)
174    }
175}