@@ -32,11 +32,25 @@ pub struct ApiKeyManagerV0 {
3232}
3333
3434// FIXME: Need better naming
35- /// Hash can be safely stored as String
36- /// in memory without having to worry about
37- /// zeroizing. Hashes are not secrets and are meant to be stored.
38- #[ derive( Debug ) ]
39- pub struct Hash ( String ) ;
35+ /// Contains the Argon2 hash and the salt used to generate it.
36+ ///
37+ /// The hash can be safely stored in your database without special security measures
38+ /// since it's already cryptographically hashed. However, avoid unnecessary cloning
39+ /// or logging to minimize exposure.
40+ ///
41+ /// # Fields
42+ ///
43+ /// - `hash`: The Argon2id PHC-formatted hash string (e.g., "$argon2id$v=19$m=...")
44+ /// - `salt`: The base64-encoded salt used during hashing (32 bytes when encoded)
45+ ///
46+ /// Both fields can be accessed using the auto-generated getter methods `hash()` and `salt()`
47+ /// provided by the `Getters` derive macro.
48+ #[ derive( Debug , Getters , PartialEq ) ]
49+ pub struct Hash {
50+ hash : String ,
51+ salt : String ,
52+ }
53+
4054#[ derive( Debug ) ]
4155pub struct NoHash ;
4256
@@ -64,7 +78,7 @@ impl ApiKeyManagerV0 {
6478
6579 // Generate dummy key and its hash for timing attack protection
6680 let dummy_key = generator. dummy_key ( ) . clone ( ) ;
67- let dummy_hash = hasher. hash ( & dummy_key) ?;
81+ let ( dummy_hash, _salt ) = hasher. hash ( & dummy_key) ?;
6882
6983 let validator = KeyValidator :: new ( include_checksum, dummy_key, dummy_hash) ?;
7084
@@ -190,7 +204,7 @@ impl ApiKeyManagerV0 {
190204 /// # use std::time::Duration;
191205 /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
192206 /// # let key = manager.generate(Environment::production()).unwrap();
193- /// match manager.verify(key.key(), key.hash())? {
207+ /// match manager.verify(key.key(), key.expose_hash(). hash())? {
194208 /// KeyStatus::Valid => { /* grant access */ },
195209 /// KeyStatus::Invalid => { /* reject - wrong key or expired */ },
196210 /// }
@@ -235,31 +249,112 @@ impl<T> ApiKey<T> {
235249}
236250
237251impl ApiKey < NoHash > {
252+ /// Creates a new API key without a hash.
253+ ///
254+ /// This is typically used internally before converting to a hashed key.
238255 pub fn new ( key : SecureString ) -> ApiKey < NoHash > {
239256 ApiKey { key, hash : NoHash }
240257 }
258+
259+ /// Converts this unhashed key into a hashed key by generating a new random salt
260+ /// and computing the Argon2 hash.
261+ ///
262+ /// This method is automatically called by `ApiKeyManagerV0::generate()` and
263+ /// `ApiKeyManagerV0::generate_with_expiry()`.
241264 pub fn into_hashed ( self , hasher : & KeyHasher ) -> Result < ApiKey < Hash > > {
242- let hash = hasher. hash ( & self . key ) ?;
265+ let ( hash, salt) = hasher. hash ( & self . key ) ?;
266+
267+ Ok ( ApiKey {
268+ key : self . key ,
269+ hash : Hash { hash, salt } ,
270+ } )
271+ }
272+
273+ /// Converts this unhashed key into a hashed key using a specific salt.
274+ ///
275+ /// This is useful when you need to regenerate the same hash from the same key,
276+ /// for example in testing or when verifying hash consistency.
277+ ///
278+ /// # Parameters
279+ ///
280+ /// * `hasher` - The key hasher to use
281+ /// * `salt` - Base64-encoded salt string (32 bytes when decoded)
282+ ///
283+ /// # Example
284+ ///
285+ /// ```rust
286+ /// # use api_keys_simplified::{ApiKeyManagerV0, Environment, ExposeSecret};
287+ /// # use api_keys_simplified::{SecureString, ApiKey};
288+ /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
289+ /// let key1 = manager.generate(Environment::production()).unwrap();
290+ ///
291+ /// // Regenerate hash with the same salt
292+ /// let key2 = ApiKey::new(SecureString::from(key1.key().expose_secret()))
293+ /// .into_hashed_with_salt(manager.hasher(), key1.expose_hash().salt())
294+ /// .unwrap();
295+ ///
296+ /// // Both hashes should be identical
297+ /// assert_eq!(key1.expose_hash(), key2.expose_hash());
298+ /// # Ok::<(), Box<dyn std::error::Error>>(())
299+ /// ```
300+ pub fn into_hashed_with_salt ( self , hasher : & KeyHasher , salt : & str ) -> Result < ApiKey < Hash > > {
301+ let hash = hasher. hash_with_salt ( & self . key , salt) ?;
243302
244303 Ok ( ApiKey {
245304 key : self . key ,
246- hash : Hash ( hash) ,
305+ hash : Hash {
306+ hash,
307+ salt : salt. to_string ( ) ,
308+ } ,
247309 } )
248310 }
311+
312+ /// Consumes the API key and returns the underlying secure string.
249313 pub fn into_key ( self ) -> SecureString {
250314 self . key
251315 }
252316}
253317
254318impl ApiKey < Hash > {
255- /// Returns hash.
256- /// SECURITY:
257- /// Although it's safe to store hash,
258- /// do NOT make unnecessary clones
259- /// and avoid logging the hash.
260- pub fn hash ( & self ) -> & str {
261- & self . hash . 0
319+ /// Returns a reference to the hash and salt.
320+ ///
321+ /// The returned `Hash` struct contains both the Argon2 hash string and the
322+ /// base64-encoded salt used to generate it. The hash should be stored in your
323+ /// database for later verification.
324+ ///
325+ /// # Accessing Fields
326+ ///
327+ /// Use the auto-generated getter methods:
328+ /// - `.hash()` - Returns the Argon2 hash string as `&str`
329+ /// - `.salt()` - Returns the base64-encoded salt as `&str`
330+ ///
331+ /// # Security Note
332+ ///
333+ /// Although it's safe to store the hash, avoid making unnecessary clones
334+ /// or logging the hash to minimize exposure.
335+ ///
336+ /// # Example
337+ ///
338+ /// ```rust
339+ /// # use api_keys_simplified::{ApiKeyManagerV0, Environment};
340+ /// # let manager = ApiKeyManagerV0::init_default_config("sk").unwrap();
341+ /// # let api_key = manager.generate(Environment::production()).unwrap();
342+ /// // Get the hash for storage
343+ /// let hash_struct = api_key.expose_hash();
344+ ///
345+ /// // Access the hash string for database storage
346+ /// let hash_str: &str = hash_struct.hash();
347+ /// println!("Store this hash: {}", hash_str);
348+ ///
349+ /// // Access the salt (if needed for hash regeneration)
350+ /// let salt: &str = hash_struct.salt();
351+ /// # Ok::<(), Box<dyn std::error::Error>>(())
352+ /// ```
353+ pub fn expose_hash ( & self ) -> & Hash {
354+ & self . hash
262355 }
356+
357+ /// Consumes the API key and returns the underlying secure string.
263358 pub fn into_key ( self ) -> SecureString {
264359 self . key
265360 }
@@ -276,7 +371,7 @@ mod tests {
276371 let api_key = generator. generate ( Environment :: production ( ) ) . unwrap ( ) ;
277372
278373 let key_str = api_key. key ( ) ;
279- let hash_str = api_key. hash ( ) ;
374+ let hash_str = api_key. expose_hash ( ) . hash ( ) ;
280375
281376 assert ! ( key_str. expose_secret( ) . starts_with( "sk-live-" ) ) ;
282377 assert ! ( hash_str. starts_with( "$argon2id$" ) ) ;
@@ -318,4 +413,15 @@ mod tests {
318413 let key = generator. generate ( Environment :: production ( ) ) . unwrap ( ) ;
319414 assert ! ( generator. verify_checksum( key. key( ) ) . unwrap( ) ) ;
320415 }
416+
417+ #[ test]
418+ fn compare_hash ( ) {
419+ let manager = ApiKeyManagerV0 :: init_default_config ( "sk" ) . unwrap ( ) ;
420+ let key = manager. generate ( Environment :: production ( ) ) . unwrap ( ) ;
421+ let new_secret = ApiKey :: new ( SecureString :: from ( key. key ( ) . expose_secret ( ) ) )
422+ . into_hashed_with_salt ( manager. hasher ( ) , key. expose_hash ( ) . salt ( ) )
423+ . unwrap ( ) ;
424+
425+ assert_eq ! ( new_secret. expose_hash( ) , key. expose_hash( ) ) ;
426+ }
321427}
0 commit comments