Skip to main content

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// }