axum_gate/secrets/mod.rs
1//! Secrets hashing and verification models.
2use crate::hashing::HashingOperation;
3pub mod errors;
4use crate::errors::{Error, Result};
5use crate::hashing::{HashedValue, HashingService};
6use crate::verification_result::VerificationResult;
7pub use errors::SecretError;
8pub use secret_repository::SecretRepository;
9
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13mod secret_repository;
14
15/// A cryptographically secure secret (password) bound to a specific user account.
16///
17/// This type represents a hashed password or other sensitive authentication data that is
18/// cryptographically bound to a specific account through its unique identifier. The secret
19/// is automatically hashed using the Argon2 algorithm when created, ensuring that plaintext
20/// passwords are never stored in memory or persistent storage.
21///
22/// # Security Properties
23///
24/// - **Irreversible hashing**: Plaintext secrets cannot be recovered from the hash
25/// - **Unique salting**: Each secret has a cryptographically random salt
26/// - **Timing attack resistance**: Verification operations run in constant time
27/// - **Account binding**: Secrets are tied to specific account IDs for additional security
28///
29/// # Usage in Authentication Flows
30///
31/// Secrets are typically created during user registration and verified during login:
32///
33/// ```rust
34/// use axum_gate::secrets::Secret; use axum_gate::hashing::argon2::Argon2Hasher; use axum_gate::verification_result::VerificationResult;
35/// use axum_gate::accounts::AccountInsertService;
36/// use uuid::Uuid;
37///
38/// # tokio_test::block_on(async {
39/// // During registration
40/// let account_id = Uuid::now_v7();
41/// let user_password = "user_entered_password";
42/// let hasher = Argon2Hasher::new_recommended().unwrap();
43///
44/// let secret = Secret::new(&account_id, user_password, hasher.clone())?;
45///
46/// // During login verification
47/// let login_attempt = "user_entered_password";
48/// match secret.verify(login_attempt, hasher)? {
49/// VerificationResult::Ok => {
50/// // Grant access - password is correct
51/// println!("Authentication successful");
52/// },
53/// VerificationResult::Unauthorized => {
54/// // Deny access - password is incorrect
55/// println!("Authentication failed");
56/// }
57/// }
58/// # Ok::<(), Box<dyn std::error::Error>>(())
59/// # });
60/// ```
61///
62/// # Storage Considerations
63///
64/// Secrets should be stored separately from account data for enhanced security:
65///
66/// - Use dedicated secret repositories ([`SecretRepository`])
67/// - Consider separate databases for account metadata vs. authentication secrets
68/// - Implement appropriate access controls on secret storage
69/// - Regular backup and recovery procedures for authentication data
70///
71/// # Performance Notes
72///
73/// Secret hashing and verification are intentionally computationally expensive operations
74/// (typically 100-500ms) to resist brute-force attacks. Consider:
75///
76/// - Implementing rate limiting on authentication endpoints
77/// - Using async/await to prevent blocking during verification
78/// - Caching verification results appropriately (with security considerations)
79///
80/// The `account_id` must correspond to a valid account in an
81/// [`AccountRepository`](crate::accounts::AccountRepository) to create a correct secret.
82#[derive(Serialize, Deserialize, Debug, Clone)]
83pub struct Secret {
84 /// The [account id](crate::accounts::Account::account_id) that this secret belongs to.
85 pub account_id: Uuid,
86 /// The actual secret.
87 pub secret: HashedValue,
88}
89
90impl Secret {
91 /// Creates a new secret by hashing the provided plaintext password.
92 ///
93 /// This method takes a plaintext secret (typically a user password) and creates a new
94 /// [`Secret`] instance with the secret cryptographically hashed using the provided
95 /// hashing service. The original plaintext is never stored.
96 ///
97 /// # Parameters
98 ///
99 /// - `account_id`: The unique identifier of the account this secret belongs to
100 /// - `plain_secret`: The plaintext password or secret to be hashed
101 /// - `hasher`: The hashing service implementation (typically [`Argon2Hasher`](crate::hashing::argon2::Argon2Hasher))
102 ///
103 /// # Security
104 ///
105 /// - The plaintext secret is immediately hashed and the original value is dropped
106 /// - A cryptographically secure random salt is generated for each secret
107 /// - The resulting hash is computationally infeasible to reverse
108 ///
109 /// # Errors
110 ///
111 /// Returns an error if the hashing operation fails, which may occur due to:
112 /// - Insufficient system resources for the hashing algorithm
113 /// - Invalid parameters in the hashing service configuration
114 /// - System-level cryptographic failures
115 ///
116 /// # Example
117 ///
118 /// ```rust
119 /// use axum_gate::secrets::Secret; use axum_gate::hashing::argon2::Argon2Hasher;
120 /// use uuid::Uuid;
121 ///
122 /// # tokio_test::block_on(async {
123 /// let account_id = Uuid::now_v7();
124 /// let password = "user_password_123";
125 /// let hasher = Argon2Hasher::new_recommended().unwrap();
126 ///
127 /// let secret = Secret::new(&account_id, password, hasher)?;
128 /// // The plaintext password is now securely hashed and cannot be recovered
129 /// # Ok::<(), Box<dyn std::error::Error>>(())
130 /// # });
131 /// ```
132 pub fn new<Hasher: HashingService>(
133 account_id: &Uuid,
134 plain_secret: &str,
135 hasher: Hasher,
136 ) -> Result<Self> {
137 let secret = hasher.hash_value(plain_secret).map_err(|e| {
138 Error::Secrets(SecretError::hashing_with_context(
139 HashingOperation::Hash,
140 e.to_string(),
141 Some("Argon2".to_string()),
142 Some("PHC".to_string()),
143 ))
144 })?;
145 Ok(Self {
146 account_id: *account_id,
147 secret,
148 })
149 }
150
151 /// Creates a secret from an already-hashed value.
152 ///
153 /// This constructor is used when you already have a properly hashed secret value,
154 /// typically when loading secrets from persistent storage or when migrating from
155 /// other authentication systems.
156 ///
157 /// # Parameters
158 ///
159 /// - `account_id`: The unique identifier of the account this secret belongs to
160 /// - `hashed_secret`: A previously computed hash value from a compatible hashing algorithm
161 ///
162 /// # Security Warning
163 ///
164 /// This method bypasses the normal hashing process and directly uses the provided hash.
165 /// Ensure that:
166 /// - The hash was created using a secure, compatible algorithm (preferably Argon2)
167 /// - The hash includes proper salting
168 /// - The source of the hash is trusted and verified
169 ///
170 /// # Use Cases
171 ///
172 /// - Loading secrets from database storage
173 /// - Migrating authentication data between systems
174 /// - Testing with pre-computed hash values
175 /// - Bulk operations where hashing has already been performed
176 ///
177 /// # Example
178 ///
179 /// ```rust
180 /// use axum_gate::secrets::Secret; use axum_gate::hashing::argon2::Argon2Hasher; use axum_gate::hashing::HashedValue;
181 /// use uuid::Uuid;
182 ///
183 /// # tokio_test::block_on(async {
184 /// // Typically this hash would come from your database
185 /// let account_id = Uuid::now_v7();
186 /// let hasher = Argon2Hasher::new_recommended().unwrap();
187 /// let original_secret = Secret::new(&account_id, "password", hasher.clone())?;
188 /// let stored_hash = &original_secret.secret;
189 ///
190 /// // Reconstruct the secret from the stored hash
191 /// let reconstructed = Secret::from_hashed(&account_id, stored_hash);
192 /// # Ok::<(), Box<dyn std::error::Error>>(())
193 /// # });
194 /// ```
195 pub fn from_hashed(account_id: &Uuid, hashed_secret: &HashedValue) -> Self {
196 Self {
197 account_id: account_id.to_owned(),
198 secret: hashed_secret.to_owned(),
199 }
200 }
201
202 /// Verifies a plaintext secret against the stored hash.
203 ///
204 /// This method performs constant-time verification of a plaintext secret (typically
205 /// a password entered by a user) against the stored cryptographic hash. The verification
206 /// process is designed to be resistant to timing attacks.
207 ///
208 /// # Parameters
209 ///
210 /// - `plain_secret`: The plaintext secret to verify (e.g., user-entered password)
211 /// - `hasher`: The hashing service to use for verification (must be compatible with the stored hash)
212 ///
213 /// # Returns
214 ///
215 /// - [`VerificationResult::Ok`] if the plaintext secret matches the stored hash
216 /// - [`VerificationResult::Unauthorized`] if the secrets don't match or verification fails
217 ///
218 /// # Security Properties
219 ///
220 /// - **Constant-time operation**: Verification takes the same time regardless of correctness
221 /// - **No information leakage**: Incorrect attempts don't reveal information about the stored secret
222 /// - **Timing attack resistance**: Prevents attackers from using timing differences to guess secrets
223 ///
224 /// # Performance
225 ///
226 /// Secret verification is computationally intensive by design (typically 100-500ms)
227 /// to make brute-force attacks impractical. This is normal and expected behavior
228 /// for secure password hashing algorithms like Argon2.
229 ///
230 /// # Errors
231 ///
232 /// Returns an error if the verification process fails due to:
233 /// - Incompatible hashing algorithms between creation and verification
234 /// - Corrupted hash data
235 /// - System-level cryptographic failures
236 ///
237 /// # Example
238 ///
239 /// ```rust
240 /// use axum_gate::secrets::Secret; use axum_gate::hashing::argon2::Argon2Hasher; use axum_gate::verification_result::VerificationResult;
241 /// use uuid::Uuid;
242 ///
243 /// # tokio_test::block_on(async {
244 /// let account_id = Uuid::now_v7();
245 /// let correct_password = "secure_password_123";
246 /// let hasher = Argon2Hasher::new_recommended().unwrap();
247 ///
248 /// // Create secret during registration
249 /// let secret = Secret::new(&account_id, correct_password, hasher.clone())?;
250 ///
251 /// // Verify during login - correct password
252 /// let result = secret.verify(correct_password, hasher.clone())?;
253 /// assert_eq!(result, VerificationResult::Ok);
254 ///
255 /// // Verify during login - incorrect password
256 /// let result = secret.verify("wrong_password", hasher)?;
257 /// assert_eq!(result, VerificationResult::Unauthorized);
258 /// # Ok::<(), Box<dyn std::error::Error>>(())
259 /// # });
260 /// ```
261 pub fn verify<Hasher: HashingService>(
262 &self,
263 plain_secret: &str,
264 hasher: Hasher,
265 ) -> Result<VerificationResult> {
266 hasher.verify_value(plain_secret, &self.secret)
267 }
268}
269
270#[test]
271#[allow(clippy::unwrap_used)]
272fn secret_verification() {
273 use crate::hashing::argon2::Argon2Hasher;
274
275 let id = Uuid::now_v7();
276 let correct_password = "admin_password";
277 let wrong_password = "admin_wrong_password";
278 let secret = Secret::new(
279 &id,
280 correct_password,
281 Argon2Hasher::new_recommended().unwrap(),
282 )
283 .unwrap();
284
285 assert_eq!(
286 VerificationResult::Unauthorized,
287 secret
288 .verify(wrong_password, Argon2Hasher::new_recommended().unwrap())
289 .unwrap()
290 );
291 assert_eq!(
292 VerificationResult::Ok,
293 secret
294 .verify(correct_password, Argon2Hasher::new_recommended().unwrap())
295 .unwrap()
296 );
297}