@@ -2,7 +2,6 @@ use super::audit;
22use super :: constants:: * ;
33use super :: crypto;
44use super :: locking;
5- use super :: rate_limiter:: { RateLimitError , RateLimiter } ;
65use super :: security;
76use crate :: core:: models:: * ;
87use argon2:: {
@@ -15,23 +14,9 @@ use std::env;
1514use std:: fs;
1615use std:: io:: Write ;
1716use std:: path:: Path ;
18- use std:: sync:: OnceLock ;
1917
2018use thiserror:: Error ;
2119
22- static RATE_LIMITER : OnceLock < RateLimiter > = OnceLock :: new ( ) ;
23-
24- fn get_rate_limiter ( ) -> & ' static RateLimiter {
25- RATE_LIMITER . get_or_init ( RateLimiter :: with_default_config)
26- }
27-
28- #[ allow( dead_code) ]
29- pub fn reset_rate_limit ( identifier : & str ) {
30- if let Some ( limiter) = RATE_LIMITER . get ( ) {
31- limiter. reset ( identifier) ;
32- }
33- }
34-
3520#[ derive( Error , Debug ) ]
3621pub enum PersistenceError {
3722 #[ error( "IO error: {source}" ) ]
@@ -189,14 +174,49 @@ pub fn lock_file(filename: &str) -> Result<LockedFile, PersistenceError> {
189174 source : std:: io:: Error :: new ( std:: io:: ErrorKind :: InvalidInput , e) ,
190175 } ) ?;
191176
192- let path = Path :: new ( NARU_DIR ) . join ( sanitized_filename) ;
177+ let path = Path :: new ( NARU_DIR ) . join ( & sanitized_filename) ;
193178
194- let _lock =
195- locking:: FileLock :: acquire_exclusive ( & path) . map_err ( |e| PersistenceError :: IoError {
196- source : std:: io:: Error :: other ( format ! ( "Could not acquire file lock: {}" , e) ) ,
197- } ) ?;
179+ // Ensure the config file exists before locking
180+ // This prevents "No such file or directory" errors during concurrent access
181+ if !path. exists ( ) {
182+ // Try to create the file with default content
183+ let default_config = ConfigFile {
184+ project_name : "My Project" . to_string ( ) ,
185+ version : "0.1.0" . to_string ( ) ,
186+ environments : std:: collections:: HashMap :: new ( ) ,
187+ salt : None ,
188+ } ;
189+ if let Some ( parent) = path. parent ( ) {
190+ let _ = fs:: create_dir_all ( parent) ;
191+ }
192+ let _ = fs:: write ( & path, serde_json:: to_string_pretty ( & default_config) . unwrap_or_default ( ) ) ;
193+ }
198194
199- Ok ( LockedFile { _lock, path } )
195+ // Retry logic for acquiring lock - helps with concurrent access
196+ let max_retries = 5 ;
197+ let mut last_error = None ;
198+
199+ for attempt in 0 ..max_retries {
200+ match locking:: FileLock :: acquire_exclusive ( & path) {
201+ Ok ( _lock) => {
202+ return Ok ( LockedFile { _lock, path } ) ;
203+ }
204+ Err ( e) => {
205+ last_error = Some ( e) ;
206+ // Wait a bit before retrying (exponential backoff)
207+ if attempt < max_retries - 1 {
208+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 10 * ( 1 << attempt) ) ) ;
209+ }
210+ }
211+ }
212+ }
213+
214+ Err ( PersistenceError :: IoError {
215+ source : std:: io:: Error :: other ( format ! (
216+ "Could not acquire file lock after {} attempts: {:?}" ,
217+ max_retries, last_error
218+ ) ) ,
219+ } )
200220}
201221
202222pub fn atomic_update_config < F > ( update_fn : F ) -> Result < ( ) , PersistenceError >
@@ -570,29 +590,9 @@ pub fn export_to_yaml(
570590}
571591
572592// Helper function to get encryption key using Argon2 with salt
593+ // Note: Key derivation is intentionally slow (Argon2). Rate limiting is NOT applied here
594+ // because the computation itself provides adequate protection against brute-force.
573595fn get_encryption_key ( ) -> Result < [ u8 ; 32 ] , PersistenceError > {
574- // Rate limit encryption key access to prevent brute-force attacks
575- let rate_limiter = get_rate_limiter ( ) ;
576- let identifier = format ! (
577- "{}_{}" ,
578- std:: process:: id( ) ,
579- std:: env:: var( "USER" ) . unwrap_or_else( |_| "unknown" . to_string( ) )
580- ) ;
581-
582- if let Err ( e) = rate_limiter. check_rate_limit ( & identifier) {
583- let error_msg = match e {
584- RateLimitError :: LockedOut ( _) => {
585- format ! ( "Rate limit exceeded: {}. Too many decryption attempts." , e)
586- }
587- } ;
588- return Err ( PersistenceError :: IoError {
589- source : std:: io:: Error :: other ( error_msg) ,
590- } ) ;
591- }
592-
593- // Periodically cleanup expired entries to prevent memory growth
594- rate_limiter. cleanup_expired ( ) ;
595-
596596 let key_str =
597597 env:: var ( "NARU_ENCRYPTION_KEY" ) . map_err ( |_| PersistenceError :: MissingEncryptionKey ) ?;
598598
0 commit comments