@@ -197,13 +197,66 @@ where
197197 hasher. finalize ( )
198198}
199199
200- /// Derive a key from the given input and context
200+ /// Derive a key from the given input and context using BLAKE3 KDF
201+ ///
202+ /// Note: For password-based or credential-based key derivation where brute-force
203+ /// resistance is needed, use `derive_key_argon2id` instead.
201204pub fn derive_key ( context : & str , input : & [ u8 ] ) -> Blake3Hash {
202205 let mut hasher = IncrementalHasher :: new_derive_key ( context) ;
203206 hasher. update ( input) ;
204207 hasher. finalize ( )
205208}
206209
210+ /// Derive a key from the given input and context using Argon2id
211+ ///
212+ /// This provides brute-force resistance for credential-based key derivation.
213+ /// Uses memory-hard Argon2id algorithm with secure parameters:
214+ /// - Memory: 64 MiB
215+ /// - Iterations: 3
216+ /// - Parallelism: 1 (for cross-platform consistency)
217+ /// - Output: 32 bytes
218+ ///
219+ /// The context string is used as the salt to provide domain separation.
220+ ///
221+ /// # Example
222+ /// ```
223+ /// use fula_crypto::hashing::derive_key_argon2id;
224+ ///
225+ /// let key = derive_key_argon2id("fula-files-v1", b"google:123456:user@example.com");
226+ /// assert_eq!(key.len(), 32);
227+ /// ```
228+ pub fn derive_key_argon2id ( context : & str , input : & [ u8 ] ) -> [ u8 ; 32 ] {
229+ use argon2:: { Argon2 , Algorithm , Version , Params } ;
230+
231+ // Secure parameters for credential-based key derivation
232+ // Memory: 64 MiB (65536 KiB) - provides memory-hardness
233+ // Iterations: 3 - time cost
234+ // Parallelism: 1 - for consistent cross-platform results
235+ let params = Params :: new ( 65536 , 3 , 1 , Some ( 32 ) )
236+ . expect ( "Invalid Argon2 parameters" ) ;
237+
238+ let argon2 = Argon2 :: new ( Algorithm :: Argon2id , Version :: V0x13 , params) ;
239+
240+ let mut output = [ 0u8 ; 32 ] ;
241+
242+ // Use context as salt for domain separation
243+ // Pad or hash the context to ensure minimum 8-byte salt requirement
244+ let salt = if context. len ( ) >= 8 {
245+ context. as_bytes ( ) . to_vec ( )
246+ } else {
247+ // Pad short contexts with zeros
248+ let mut padded = vec ! [ 0u8 ; 8 ] ;
249+ padded[ ..context. len ( ) ] . copy_from_slice ( context. as_bytes ( ) ) ;
250+ padded
251+ } ;
252+
253+ argon2
254+ . hash_password_into ( input, & salt, & mut output)
255+ . expect ( "Argon2 hashing failed" ) ;
256+
257+ output
258+ }
259+
207260/// Calculate an MD5 hash for S3 ETag compatibility
208261pub fn md5_hash ( data : & [ u8 ] ) -> String {
209262 use md5:: { Md5 , Digest } ;
@@ -308,4 +361,30 @@ mod tests {
308361 let key2 = derive_key ( "context2" , b"input" ) ;
309362 assert_ne ! ( key1, key2) ;
310363 }
364+
365+ #[ test]
366+ fn test_derive_key_argon2id ( ) {
367+ // Test basic functionality
368+ let key1 = derive_key_argon2id ( "fula-files-v1" , b"google:123456:user@example.com" ) ;
369+ assert_eq ! ( key1. len( ) , 32 ) ;
370+
371+ // Test consistency - same input produces same output
372+ let key2 = derive_key_argon2id ( "fula-files-v1" , b"google:123456:user@example.com" ) ;
373+ assert_eq ! ( key1, key2) ;
374+
375+ // Test different context produces different key
376+ let key3 = derive_key_argon2id ( "fula-files-v2" , b"google:123456:user@example.com" ) ;
377+ assert_ne ! ( key1, key3) ;
378+
379+ // Test different input produces different key
380+ let key4 = derive_key_argon2id ( "fula-files-v1" , b"google:789012:other@example.com" ) ;
381+ assert_ne ! ( key1, key4) ;
382+ }
383+
384+ #[ test]
385+ fn test_derive_key_argon2id_short_context ( ) {
386+ // Test with short context (< 8 bytes) - should still work
387+ let key = derive_key_argon2id ( "short" , b"input" ) ;
388+ assert_eq ! ( key. len( ) , 32 ) ;
389+ }
311390}
0 commit comments