shared/utils/crypto/mod.rs
1mod aes;
2mod hmac;
3mod jwt;
4mod openssl;
5mod password_hasher;
6mod secure_token;
7
8use std::sync::{Arc, RwLock};
9
10use hmac::HmacSigner;
11use secrecy::SecretString;
12
13pub use aes::Aes;
14pub use jwt::JwtSigner;
15pub use openssl::Openssl;
16pub use password_hasher::{Argon2Password, BcryptPassword, Hashable};
17pub use secure_token::SecureToken;
18
19use crate::{
20 config::{AnzarConfiguration, AuthStrategy, HashingAlgorithm, JwtConfig},
21 domain::model::SigningKey,
22 error::{AuthError, CoreError, InternalError, Result},
23};
24
25#[derive(Clone)]
26pub struct Crypto {
27 pub password_hasher: Arc<dyn Hashable>,
28 pub token: SecureToken,
29 pub hmac: HmacSigner,
30 pub aes: Aes,
31 pub openssl: Openssl,
32 jwt: Arc<RwLock<Option<JwtSigner>>>,
33}
34
35impl Default for Crypto {
36 fn default() -> Self {
37 Self {
38 password_hasher: Arc::new(Argon2Password::default()),
39 token: SecureToken::default(),
40 hmac: HmacSigner::default(),
41 aes: Aes::default(),
42 openssl: Openssl::default(),
43 jwt: Arc::new(RwLock::new(None)),
44 }
45 }
46}
47
48impl Crypto {
49 pub fn with_argon(m_cost: u32, t_cost: u32, p_cost: u32) -> Self {
50 Self {
51 password_hasher: Arc::new(Argon2Password::new(m_cost, t_cost, p_cost)),
52 ..Self::default()
53 }
54 }
55 pub fn with_bcrypt(cost: u32) -> Self {
56 Self {
57 password_hasher: Arc::new(BcryptPassword::new(cost)),
58 ..Self::default()
59 }
60 }
61}
62
63impl Crypto {
64 pub fn with_initializations(mut self, configuration: &AnzarConfiguration) -> Self {
65 let key = &configuration.security.secret_key;
66
67 self.hmac = HmacSigner::new(&SecretString::from(key.clone()));
68 self.aes = Aes::new(&SecretString::from(key.clone()));
69
70 if let Ok(conf) = configuration.auth.jwt() {
71 self.openssl = Openssl::new(&conf.algorithm);
72 }
73 self
74 }
75}
76
77impl Crypto {
78 pub fn with_jwt(
79 mut self,
80 private: &str,
81 signing_key: &SigningKey,
82 jwt_config: &JwtConfig,
83 ) -> Self {
84 self.jwt = Arc::new(RwLock::new(Some(JwtSigner::new(
85 private,
86 signing_key,
87 jwt_config,
88 ))));
89 self
90 }
91}
92
93impl Crypto {
94 pub fn with_token_size32(mut self) -> Self {
95 self.token = SecureToken::with_size32();
96 self
97 }
98 pub fn with_token_size64(mut self) -> Self {
99 self.token = SecureToken::with_size64();
100 self
101 }
102}
103
104impl Crypto {
105 pub fn from_configuration(configuration: &AnzarConfiguration) -> Self {
106 match configuration.auth.password.algorithm {
107 HashingAlgorithm::Argon2 {
108 memory_kib,
109 iterations,
110 parallelism,
111 } => Crypto::with_argon(memory_kib, iterations, parallelism),
112 HashingAlgorithm::Bcrypt { cost } => Crypto::with_bcrypt(cost),
113 }
114 .with_initializations(configuration)
115 .with_token_size64()
116 }
117}
118
119impl Crypto {
120 pub fn validate(self, strategy: &AuthStrategy) -> Result<Self> {
121 if matches!(strategy, AuthStrategy::Jwt(..)) && self.jwt().is_err() {
122 return Err(CoreError::Internal(InternalError::MissingConfiguration(
123 "JWT strategy requires a JWT signer, but none was configured".into(),
124 )));
125 }
126
127 match self.hmac.secret_key.len() {
128 0 => {
129 return Err(CoreError::Internal(InternalError::MissingConfiguration(
130 "HMAC secret key is missing".into(),
131 )));
132 }
133 n if n < 32 => {
134 return Err(CoreError::Internal(InternalError::MissingConfiguration(
135 format!("HMAC secret key is too short ({n} bytes), minimum is 32 bytes"),
136 )));
137 }
138 _ => {}
139 }
140
141 Ok(self)
142 }
143
144 pub fn jwt(&self) -> Result<JwtSigner> {
145 let jwt = self.jwt.read().unwrap();
146
147 jwt.clone()
148 .ok_or(CoreError::Unauthenticated(AuthError::JwtNotConfigured))
149 }
150
151 pub fn rotate_jwt(&self, new_signer: JwtSigner) {
152 let mut jwt = self.jwt.write().unwrap();
153 *jwt = Some(new_signer);
154 }
155}
156
157// #[cfg(test)]
158// mod tests {
159// use crate::config::RateLimit;
160//
161// use super::*;
162//
163// fn base_crypto() -> Crypto {
164// pub const DEFAULT_M_COST: u32 = 19 * 1024; // ~19 MiB
165// pub const DEFAULT_T_COST: u32 = 2;
166// pub const DEFAULT_P_COST: u32 = 1;
167//
168// Crypto::with_argon(DEFAULT_M_COST, DEFAULT_T_COST, DEFAULT_P_COST)
169// .with_initializations(&mock_configuration())
170// .with_token_size64()
171// }
172// fn bcrypt_crypto() -> Crypto {
173// let cost = 12;
174// Crypto::with_bcrypt(cost)
175// .with_initializations(&mock_configuration())
176// .with_token_size64()
177// }
178// fn mock_configuration() -> AnzarConfiguration {
179// AnzarConfiguration {
180// app: crate::config::App {
181// environment: "dev".into(),
182// url: "localhost:3000".to_string(),
183// },
184// database: crate::config::Database {
185// driver: crate::config::database::DatabaseDriver::PostgreSQL,
186// connection_string: "postgres://hakou:password@localhost:5432/dev".into(),
187// cache: crate::config::Cache {
188// driver: crate::config::cache::CacheDriver::InMemory,
189// url: "".into(),
190// },
191// },
192// server: crate::config::Server::default(),
193// auth: crate::config::Authentication {
194// strategy: AuthStrategy::Jwt(crate::config::JwtConfig {
195// issuer: "localhost:3000".into(),
196// audience: "users".into(),
197// ..Default::default()
198// }),
199// ..Default::default()
200// },
201// security: crate::config::Security {
202// secret_key: "a".repeat(32),
203// rate_limit: RateLimit::default(),
204// headers: vec![],
205// },
206// }
207// }
208//
209// #[test]
210// fn test_valid_session_strategy() {
211// let crypto = base_crypto();
212// let strategy = &AuthStrategy::Session(crate::config::SessionConfig {
213// ..Default::default()
214// });
215//
216// assert!(crypto.validate(strategy).is_ok());
217// }
218//
219// #[test]
220// fn test_valid_jwt_strategy() {
221// let binding = mock_configuration();
222// let jwt_config = binding.auth.jwt().unwrap();
223// let crypto = base_crypto().with_jwt("", "", jwt_config);
224// let strategy = &AuthStrategy::Jwt(crate::config::JwtConfig {
225// ..Default::default()
226// });
227//
228// assert!(crypto.validate(strategy).is_ok());
229// }
230// #[test]
231// fn test_valid_session_strategy_with_bcrypt() {
232// let crypto = bcrypt_crypto();
233// let strategy = &AuthStrategy::Session(crate::config::SessionConfig {
234// ..Default::default()
235// });
236//
237// assert!(crypto.validate(strategy).is_ok());
238// }
239//
240// #[test]
241// fn test_valid_jwt_strategy_with_bcrypt() {
242// let binding = mock_configuration();
243// let jwt_config = binding.auth.jwt().unwrap();
244// let crypto = base_crypto().with_jwt("", "", jwt_config);
245// let strategy = &AuthStrategy::Jwt(crate::config::JwtConfig {
246// ..Default::default()
247// });
248//
249// assert!(crypto.validate(strategy).is_ok());
250// }
251//
252// #[test]
253// fn test_jwt_strategy_missing_signer() {
254// let crypto = base_crypto(); // no .with_jwt()
255// let strategy = &AuthStrategy::Jwt(crate::config::JwtConfig {
256// ..Default::default()
257// });
258//
259// assert!(crypto.validate(strategy).is_err());
260// }
261//
262// #[test]
263// fn test_hmac_secret_empty() {
264// pub const DEFAULT_M_COST: u32 = 19 * 1024; // ~19 MiB
265// pub const DEFAULT_T_COST: u32 = 2;
266// pub const DEFAULT_P_COST: u32 = 1;
267// let crypto = Crypto::with_argon(DEFAULT_M_COST, DEFAULT_T_COST, DEFAULT_P_COST)
268// .with_initializations(&mock_configuration())
269// .with_token_size64();
270// let strategy = &AuthStrategy::Session(crate::config::SessionConfig {
271// ..Default::default()
272// });
273//
274// assert!(crypto.validate(strategy).is_err());
275// }
276//
277// // #[test]
278// // fn test_hmac_secret_too_short() {
279// // let crypto = Crypto::with_argon()
280// // .with_hmac_secret("tooshort")
281// // .with_token_size64();
282// // let strategy = &AuthStrategy::Session(crate::config::SessionConfig {
283// // ..Default::default()
284// // });
285// //
286// // let err = crypto.validate(strategy).unwrap_err();
287// // assert!(err.to_string().contains("too short"));
288// // }
289// }