Skip to main content

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}