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}