Skip to main content

webgates_secrets/
lib.rs

1#![deny(missing_docs)]
2#![deny(unsafe_code)]
3#![deny(clippy::unwrap_used)]
4#![deny(clippy::expect_used)]
5/*!
6# webgates-secrets
7
8Secret and hashing primitives for `webgates` applications.
9
10This crate provides the building blocks used to hash secrets, verify them, and
11bind stored hashed values to account identifiers.
12
13## When to use this crate
14
15Use `webgates-secrets` when you want:
16
17- the [`Secret`] value object for stored hashed credentials
18- the [`hashing`] module for hashing and verification primitives
19- the [`hashing::argon2::Argon2Hasher`] implementation with secure presets
20- structured secret and hashing error types
21
22The crate is intentionally focused on the secret and hashing boundary. Storage
23and repository concerns live in sibling crates.
24
25## Quick start
26
27```rust
28use webgates_core::verification_result::VerificationResult;
29use webgates_secrets::hashing::argon2::Argon2Hasher;
30use webgates_secrets::hashing::hashing_service::HashingService;
31
32let hasher = Argon2Hasher::new_recommended().unwrap();
33let hashed = hasher.hash_value("user_password").unwrap();
34let result = hasher.verify_value("user_password", &hashed).unwrap();
35
36assert_eq!(result, VerificationResult::Ok);
37```
38
39## Getting started on docs.rs
40
41A good reading order is:
42
431. [`hashing::hashing_service::HashingService`]
442. [`hashing::argon2::Argon2Hasher`]
453. [`hashing::HashedValue`]
464. [`Secret`]
475. [`errors`] and [`hashing::errors`]
48*/
49
50use crate::errors::SecretError;
51use crate::hashing::HashedValue;
52use crate::hashing::errors::HashingOperation;
53use crate::hashing::hashing_service::HashingService;
54use serde::{Deserialize, Serialize};
55use uuid::Uuid;
56use webgates_core::verification_result::VerificationResult;
57
58/// Convenience result alias for secrets and hashing operations.
59///
60/// Use concrete, caller-relevant error types at public boundaries when you need
61/// more specific failure handling.
62pub type Result<T, E = Box<dyn std::error::Error + Send + Sync>> = std::result::Result<T, E>;
63
64pub mod errors;
65pub mod hashing;
66
67/// A hashed secret bound to a single account identifier.
68///
69/// `Secret` is the main value object of this crate. It stores only the hashed
70/// representation of a credential. Callers create a value from plaintext with
71/// [`Secret::new`] or reconstruct it from storage with [`Secret::from_hashed`].
72///
73/// # Security notes
74///
75/// - Plaintext input is hashed before storage in the returned value.
76/// - Verification is delegated to the configured [`HashingService`] implementation.
77/// - The stored value is self-contained and suitable for persistence.
78///
79/// # Examples
80///
81/// Secrets are typically created during registration and verified during login:
82///
83/// ```rust
84/// use webgates_core::verification_result::VerificationResult;
85/// use webgates_secrets::hashing::argon2::Argon2Hasher;
86/// use webgates_secrets::Secret;
87/// use uuid::Uuid;
88///
89/// let account_id = Uuid::now_v7();
90/// let hasher = Argon2Hasher::new_recommended().unwrap();
91/// let secret = Secret::new(&account_id, "user_entered_password", hasher.clone())
92///     .map_err(|e| e.to_string())?;
93///
94/// let verification = secret
95///     .verify("user_entered_password", hasher)
96///     .map_err(|e| e.to_string())?;
97///
98/// assert_eq!(verification, VerificationResult::Ok);
99/// # Ok::<(), String>(())
100/// ```
101///
102/// # Usage notes
103///
104/// Persist only the hashed value and its associated `account_id`. Plaintext secrets
105/// must never be stored or logged.
106#[derive(Serialize, Deserialize, Debug, Clone)]
107pub struct Secret {
108    /// The account identifier this secret belongs to.
109    pub account_id: Uuid,
110    /// The persisted hashed secret value.
111    pub secret: HashedValue,
112}
113
114impl Secret {
115    /// Creates a new secret by hashing the provided plaintext input.
116    ///
117    /// # Errors
118    ///
119    /// Returns an error when hashing fails because the configured hashing backend
120    /// cannot produce a valid hash.
121    ///
122    /// # Examples
123    ///
124    /// ```rust
125    /// use webgates_secrets::hashing::argon2::Argon2Hasher;
126    /// use webgates_secrets::Secret;
127    /// use uuid::Uuid;
128    ///
129    /// let account_id = Uuid::now_v7();
130    /// let hasher = Argon2Hasher::new_recommended().unwrap();
131    /// let secret = Secret::new(&account_id, "user_password_123", hasher)
132    ///     .map_err(|e| e.to_string())?;
133    ///
134    /// assert_eq!(secret.account_id, account_id);
135    /// # Ok::<(), String>(())
136    /// ```
137    pub fn new<Hasher: HashingService>(
138        account_id: &Uuid,
139        plain_secret: &str,
140        hasher: Hasher,
141    ) -> std::result::Result<Self, SecretError> {
142        let secret = hasher.hash_value(plain_secret).map_err(|e| {
143            SecretError::hashing_with_context(
144                HashingOperation::Hash,
145                e.to_string(),
146                Some("Argon2".to_string()),
147                Some("PHC".to_string()),
148            )
149        })?;
150        Ok(Self {
151            account_id: *account_id,
152            secret,
153        })
154    }
155
156    /// Reconstructs a secret from a previously hashed value.
157    ///
158    /// Use this constructor when loading persisted secrets from storage.
159    ///
160    /// # Examples
161    ///
162    /// ```rust
163    /// use webgates_secrets::hashing::argon2::Argon2Hasher;
164    /// use webgates_secrets::hashing::HashedValue;
165    /// use webgates_secrets::Secret;
166    /// use uuid::Uuid;
167    ///
168    /// let account_id = Uuid::now_v7();
169    /// let hasher = Argon2Hasher::new_recommended().unwrap();
170    /// let original_secret = Secret::new(&account_id, "password", hasher.clone())
171    ///     .map_err(|e| e.to_string())?;
172    /// let stored_hash: &HashedValue = &original_secret.secret;
173    ///
174    /// let reconstructed = Secret::from_hashed(&account_id, stored_hash);
175    ///
176    /// assert_eq!(reconstructed.account_id, account_id);
177    /// # Ok::<(), String>(())
178    /// ```
179    pub fn from_hashed(account_id: &Uuid, hashed_secret: &HashedValue) -> Self {
180        Self {
181            account_id: *account_id,
182            secret: hashed_secret.clone(),
183        }
184    }
185
186    /// Verifies a plaintext secret against the stored hash.
187    ///
188    /// Returns [`VerificationResult::Ok`] when the plaintext matches the stored
189    /// hash and [`VerificationResult::Unauthorized`] when it does not match.
190    ///
191    /// # Errors
192    ///
193    /// Returns an error when the stored hash cannot be parsed or verified by the
194    /// provided hashing backend.
195    ///
196    /// # Examples
197    ///
198    /// ```rust
199    /// use webgates_core::verification_result::VerificationResult;
200    /// use webgates_secrets::hashing::argon2::Argon2Hasher;
201    /// use webgates_secrets::Secret;
202    /// use uuid::Uuid;
203    ///
204    /// let account_id = Uuid::now_v7();
205    /// let correct_password = "secure_password_123";
206    /// let hasher = Argon2Hasher::new_recommended().unwrap();
207    ///
208    /// let secret = Secret::new(&account_id, correct_password, hasher.clone())
209    ///     .map_err(|e| e.to_string())?;
210    ///
211    /// let result = secret
212    ///     .verify(correct_password, hasher.clone())
213    ///     .map_err(|e| e.to_string())?;
214    /// assert_eq!(result, VerificationResult::Ok);
215    ///
216    /// let result = secret
217    ///     .verify("wrong_password", hasher)
218    ///     .map_err(|e| e.to_string())?;
219    /// assert_eq!(result, VerificationResult::Unauthorized);
220    /// # Ok::<(), String>(())
221    /// ```
222    pub fn verify<Hasher: HashingService>(
223        &self,
224        plain_secret: &str,
225        hasher: Hasher,
226    ) -> std::result::Result<VerificationResult, SecretError> {
227        hasher
228            .verify_value(plain_secret, &self.secret)
229            .map_err(|error| {
230                SecretError::hashing_with_context(
231                    HashingOperation::Verify,
232                    error.to_string(),
233                    Some("Argon2".to_string()),
234                    Some("PHC".to_string()),
235                )
236            })
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::Secret;
243    use crate::hashing::argon2::Argon2Hasher;
244    use uuid::Uuid;
245    use webgates_core::verification_result::VerificationResult;
246
247    #[test]
248    fn secret_verification_returns_unauthorized_for_wrong_secret() {
249        let id = Uuid::now_v7();
250        let correct_password = "admin_password";
251        let wrong_password = "admin_wrong_password";
252        let hasher = match Argon2Hasher::new_recommended() {
253            Ok(hasher) => hasher,
254            Err(error) => panic!(
255                "recommended Argon2 hasher should be constructible in tests: {}",
256                error
257            ),
258        };
259        let secret = match Secret::new(&id, correct_password, hasher.clone()) {
260            Ok(secret) => secret,
261            Err(error) => panic!(
262                "secret construction should hash the provided test password: {}",
263                error
264            ),
265        };
266
267        let verification = match secret.verify(wrong_password, hasher) {
268            Ok(verification) => verification,
269            Err(error) => panic!(
270                "verification should return an authorization result for a valid stored hash: {}",
271                error
272            ),
273        };
274
275        assert_eq!(VerificationResult::Unauthorized, verification);
276    }
277
278    #[test]
279    fn secret_verification_returns_ok_for_matching_secret() {
280        let id = Uuid::now_v7();
281        let correct_password = "admin_password";
282        let hasher = match Argon2Hasher::new_recommended() {
283            Ok(hasher) => hasher,
284            Err(error) => panic!(
285                "recommended Argon2 hasher should be constructible in tests: {}",
286                error
287            ),
288        };
289        let secret = match Secret::new(&id, correct_password, hasher.clone()) {
290            Ok(secret) => secret,
291            Err(error) => panic!(
292                "secret construction should hash the provided test password: {}",
293                error
294            ),
295        };
296
297        let verification = match secret.verify(correct_password, hasher) {
298            Ok(verification) => verification,
299            Err(error) => panic!(
300                "verification should return success for the original plaintext secret: {}",
301                error
302            ),
303        };
304
305        assert_eq!(VerificationResult::Ok, verification);
306    }
307
308    #[test]
309    fn from_hashed_preserves_account_id_and_hash() {
310        let id = Uuid::now_v7();
311        let hasher = match Argon2Hasher::new_recommended() {
312            Ok(hasher) => hasher,
313            Err(error) => panic!(
314                "recommended Argon2 hasher should be constructible in tests: {}",
315                error
316            ),
317        };
318        let secret = match Secret::new(&id, "admin_password", hasher) {
319            Ok(secret) => secret,
320            Err(error) => panic!(
321                "secret construction should hash the provided test password: {}",
322                error
323            ),
324        };
325
326        let reconstructed = Secret::from_hashed(&id, &secret.secret);
327
328        assert_eq!(reconstructed.account_id, id);
329        assert_eq!(reconstructed.secret, secret.secret);
330    }
331}