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#[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
48pub 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#[derive(Clone)]
65pub struct CryptoService {
66 cipher: Aes256Gcm,
67}
68
69impl CryptoService {
70 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 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 let ciphertext = self
88 .cipher
89 .encrypt(nonce, text.as_bytes())
90 .map_err(|_| CryptoError::EncryptionFailed)?;
91
92 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 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 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 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 let hash = format!("{:x}", result);
134
135 Ok(HashedData { hash })
136 }
137
138 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)?; Ok(hashed.to_string())
151 })
152 .await??;
153
154 Ok(HashedData { hash })
155 }
156
157 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}