@@ -33,7 +33,6 @@ impl KeyValidator {
3333 const MAX_HASH_LENGTH : usize = 512 ;
3434
3535 pub fn new (
36- _hash_config : & crate :: HashConfig ,
3736 has_checksum : bool ,
3837 dummy_key : SecureString ,
3938 dummy_hash : String ,
@@ -48,27 +47,39 @@ impl KeyValidator {
4847 } )
4948 }
5049
51- fn verify_expiry ( & self , parts : Parts ) -> Result < KeyStatus > {
50+ fn verify_expiry (
51+ & self ,
52+ parts : Parts ,
53+ expiry_grace_period : std:: time:: Duration ,
54+ ) -> Result < KeyStatus > {
5255 if let Some ( expiry) = parts. expiry_b64 {
5356 let decoded = URL_SAFE_NO_PAD
5457 . decode ( expiry)
5558 . or ( Err ( Error :: InvalidFormat ) ) ?;
56- let expiry = i64:: from_be_bytes ( decoded. try_into ( ) . or ( Err ( Error :: InvalidFormat ) ) ?) ;
57-
58- // TODO(ARCHITECTURE): time libs are platform dependent.
59- // We should set an `infra` layer and abstract
60- // out these libs.
61- if chrono:: Utc :: now ( ) . timestamp ( ) <= expiry {
62- Ok ( KeyStatus :: Valid )
63- } else {
64- Ok ( KeyStatus :: Invalid )
59+ let expiry_timestamp =
60+ i64:: from_be_bytes ( decoded. try_into ( ) . or ( Err ( Error :: InvalidFormat ) ) ?) ;
61+
62+ let current_time = chrono:: Utc :: now ( ) . timestamp ( ) ;
63+ let grace_seconds = expiry_grace_period. as_secs ( ) as i64 ;
64+
65+ // Key is invalid if it expired more than grace_period ago
66+ // This ensures once a key expires beyond the grace period, it stays expired
67+ // even if the clock goes backwards
68+ if expiry_timestamp + grace_seconds < current_time {
69+ return Ok ( KeyStatus :: Invalid ) ;
6570 }
71+ Ok ( KeyStatus :: Valid )
6672 } else {
6773 Ok ( KeyStatus :: Valid )
6874 }
6975 }
7076
71- pub fn verify ( & self , provided_key : & str , stored_hash : & str ) -> Result < KeyStatus > {
77+ pub fn verify (
78+ & self ,
79+ provided_key : & str ,
80+ stored_hash : & str ,
81+ expiry_grace_period : std:: time:: Duration ,
82+ ) -> Result < KeyStatus > {
7283 // Input length validation to prevent DoS attacks
7384 if provided_key. len ( ) > Self :: MAX_KEY_LENGTH {
7485 self . dummy_load ( ) ;
@@ -109,7 +120,7 @@ impl KeyValidator {
109120 // SECURITY: Force evaluation of expiry check BEFORE the match to ensure
110121 // constant-time execution. This prevents the compiler from short-circuiting
111122 // the expiry check when argon_result is Invalid, which would create a timing oracle.
112- let expiry_result = self . verify_expiry ( token_parts) ?;
123+ let expiry_result = self . verify_expiry ( token_parts, expiry_grace_period ) ?;
113124
114125 match ( argon_result, expiry_result) {
115126 ( KeyStatus :: Invalid , _) | ( _, KeyStatus :: Invalid ) => Ok ( KeyStatus :: Invalid ) ,
@@ -150,26 +161,30 @@ mod tests {
150161 let hash = hasher. hash ( & key) . unwrap ( ) ;
151162
152163 let ( dummy_key, dummy_hash) = dummy_key_and_hash ( ) ;
153- let validator =
154- KeyValidator :: new ( & HashConfig :: default ( ) , true , dummy_key, dummy_hash) . unwrap ( ) ;
164+ let validator = KeyValidator :: new ( true , dummy_key, dummy_hash) . unwrap ( ) ;
155165 assert_eq ! (
156166 validator
157- . verify( key. expose_secret( ) , hash. as_ref( ) )
167+ . verify(
168+ key. expose_secret( ) ,
169+ hash. as_ref( ) ,
170+ std:: time:: Duration :: ZERO
171+ )
158172 . unwrap( ) ,
159173 KeyStatus :: Valid
160174 ) ;
161175 assert_eq ! (
162- validator. verify( "wrong_key" , hash. as_ref( ) ) . unwrap( ) ,
176+ validator
177+ . verify( "wrong_key" , hash. as_ref( ) , std:: time:: Duration :: ZERO )
178+ . unwrap( ) ,
163179 KeyStatus :: Invalid
164180 ) ;
165181 }
166182
167183 #[ test]
168184 fn test_invalid_hash_format ( ) {
169185 let ( dummy_key, dummy_hash) = dummy_key_and_hash ( ) ;
170- let validator =
171- KeyValidator :: new ( & HashConfig :: default ( ) , true , dummy_key, dummy_hash) . unwrap ( ) ;
172- let result = validator. verify ( "any_key" , "invalid_hash" ) ;
186+ let validator = KeyValidator :: new ( true , dummy_key, dummy_hash) . unwrap ( ) ;
187+ let result = validator. verify ( "any_key" , "invalid_hash" , std:: time:: Duration :: ZERO ) ;
173188 // After timing oracle fix: invalid hash format returns Ok(Invalid) instead of Err
174189 // to prevent timing-based user enumeration attacks
175190 assert ! ( result. is_ok( ) ) ;
@@ -184,9 +199,8 @@ mod tests {
184199 let hash = hasher. hash ( & valid_key) . unwrap ( ) ;
185200
186201 let ( dummy_key, dummy_hash) = dummy_key_and_hash ( ) ;
187- let validator =
188- KeyValidator :: new ( & HashConfig :: default ( ) , true , dummy_key, dummy_hash) . unwrap ( ) ;
189- let result = validator. verify ( & oversized_key, hash. as_ref ( ) ) ;
202+ let validator = KeyValidator :: new ( true , dummy_key, dummy_hash) . unwrap ( ) ;
203+ let result = validator. verify ( & oversized_key, hash. as_ref ( ) , std:: time:: Duration :: ZERO ) ;
190204 assert ! ( result. is_err( ) ) ;
191205 assert ! ( matches!( result. unwrap_err( ) , Error :: InvalidFormat ) ) ;
192206 }
@@ -196,9 +210,8 @@ mod tests {
196210 let oversized_hash = "a" . repeat ( 513 ) ; // Exceeds MAX_HASH_LENGTH
197211
198212 let ( dummy_key, dummy_hash) = dummy_key_and_hash ( ) ;
199- let validator =
200- KeyValidator :: new ( & HashConfig :: default ( ) , true , dummy_key, dummy_hash) . unwrap ( ) ;
201- let result = validator. verify ( "valid_key" , & oversized_hash) ;
213+ let validator = KeyValidator :: new ( true , dummy_key, dummy_hash) . unwrap ( ) ;
214+ let result = validator. verify ( "valid_key" , & oversized_hash, std:: time:: Duration :: ZERO ) ;
202215 assert ! ( result. is_err( ) ) ;
203216 assert ! ( matches!( result. unwrap_err( ) , Error :: InvalidFormat ) ) ;
204217 }
@@ -210,17 +223,16 @@ mod tests {
210223 let hash = hasher. hash ( & valid_key) . unwrap ( ) ;
211224
212225 let ( dummy_key, dummy_hash) = dummy_key_and_hash ( ) ;
213- let validator =
214- KeyValidator :: new ( & HashConfig :: default ( ) , true , dummy_key, dummy_hash) . unwrap ( ) ;
226+ let validator = KeyValidator :: new ( true , dummy_key, dummy_hash) . unwrap ( ) ;
215227
216228 // Test at boundary (512 chars - should pass)
217229 let max_key = "a" . repeat ( 512 ) ;
218- let result = validator. verify ( & max_key, hash. as_ref ( ) ) ;
230+ let result = validator. verify ( & max_key, hash. as_ref ( ) , std :: time :: Duration :: ZERO ) ;
219231 assert ! ( result. is_ok( ) ) ; // Should not error on length check
220232
221233 // Test just over boundary (513 chars - should fail)
222234 let over_max_key = "a" . repeat ( 513 ) ;
223- let result = validator. verify ( & over_max_key, hash. as_ref ( ) ) ;
235+ let result = validator. verify ( & over_max_key, hash. as_ref ( ) , std :: time :: Duration :: ZERO ) ;
224236 assert ! ( result. is_err( ) ) ;
225237 assert ! ( matches!( result. unwrap_err( ) , Error :: InvalidFormat ) ) ;
226238 }
@@ -232,18 +244,25 @@ mod tests {
232244 let valid_hash = hasher. hash ( & valid_key) . unwrap ( ) ;
233245
234246 let ( dummy_key, dummy_hash) = dummy_key_and_hash ( ) ;
235- let validator =
236- KeyValidator :: new ( & HashConfig :: default ( ) , true , dummy_key, dummy_hash) . unwrap ( ) ;
247+ let validator = KeyValidator :: new ( true , dummy_key, dummy_hash) . unwrap ( ) ;
237248
238- let result1 = validator. verify ( "wrong_key" , valid_hash. as_ref ( ) ) ;
249+ let result1 = validator. verify ( "wrong_key" , valid_hash. as_ref ( ) , std :: time :: Duration :: ZERO ) ;
239250 assert ! ( result1. is_ok( ) ) ;
240251 assert_eq ! ( result1. unwrap( ) , KeyStatus :: Invalid ) ;
241252
242- let result2 = validator. verify ( valid_key. expose_secret ( ) , "invalid_hash_format" ) ;
253+ let result2 = validator. verify (
254+ valid_key. expose_secret ( ) ,
255+ "invalid_hash_format" ,
256+ std:: time:: Duration :: ZERO ,
257+ ) ;
243258 assert ! ( result2. is_ok( ) ) ;
244259 assert_eq ! ( result2. unwrap( ) , KeyStatus :: Invalid ) ;
245260
246- let result3 = validator. verify ( valid_key. expose_secret ( ) , "not even close to valid" ) ;
261+ let result3 = validator. verify (
262+ valid_key. expose_secret ( ) ,
263+ "not even close to valid" ,
264+ std:: time:: Duration :: ZERO ,
265+ ) ;
247266 assert ! ( result3. is_ok( ) ) ;
248267 assert_eq ! ( result3. unwrap( ) , KeyStatus :: Invalid ) ;
249268 }
0 commit comments