lazy-locker 0.0.4

A secure local secrets manager with TUI interface and SDK support
use std::path::PathBuf;
use anyhow::Result;
use directories::BaseDirs;
use argon2::{
    password_hash::{
        rand_core::OsRng,
        PasswordHash, PasswordHasher, PasswordVerifier, SaltString
    },
    Argon2
};
use zeroize::Zeroize;

pub struct Locker {
    base_dir: PathBuf,
    key: Option<Vec<u8>>, // Key derived from passphrase, zeroized at end
}

impl Locker {
    /// Tries to create the locker without prompt (checks if already initialized).
    pub fn try_new() -> Result<Self> {
        let base_dirs = BaseDirs::new().ok_or_else(|| anyhow::anyhow!("Unable to determine user directories"))?;
        let config_dir = base_dirs.config_dir();
        
        #[cfg(unix)]
        let sub_dir = ".lazy-locker";
        #[cfg(not(unix))]
        let sub_dir = "lazy-locker";
        
        let locker_dir = config_dir.join(sub_dir);
        std::fs::create_dir_all(&locker_dir)?;
        
        let salt_path = locker_dir.join("salt");
        if !salt_path.exists() {
            return Err(anyhow::anyhow!("Locker not initialized"));
        }
        
        // To load, we need passphrase, so return error to indicate it's needed
        Err(anyhow::anyhow!("Passphrase required to load locker"))
    }
    
    /// Initializes or loads the locker with the provided passphrase.
    pub fn init_or_load_with_passphrase(passphrase: &str) -> Result<Self> {
        let base_dirs = BaseDirs::new().ok_or_else(|| anyhow::anyhow!("Unable to determine user directories"))?;
        let config_dir = base_dirs.config_dir();
        
        #[cfg(unix)]
        let sub_dir = ".lazy-locker";
        #[cfg(not(unix))]
        let sub_dir = "lazy-locker";
        
        let locker_dir = config_dir.join(sub_dir);
        std::fs::create_dir_all(&locker_dir)?;
        
        let salt_path = locker_dir.join("salt");
        let key = if salt_path.exists() {
            Self::load_key(&locker_dir, passphrase)?
        } else {
            Self::init_key(&locker_dir, passphrase)?
        };
        
        Ok(Self { base_dir: locker_dir, key: Some(key) })
    }
    
    /// Initializes the key for the first time: generates salt, asks passphrase, derives key.
    fn init_key(locker_dir: &PathBuf, passphrase: &str) -> Result<Vec<u8>> {
        let salt = SaltString::generate(&mut OsRng);
        std::fs::write(locker_dir.join("salt"), salt.as_str())?;
        
        let argon2 = Argon2::default();
        let hash = argon2.hash_password(passphrase.as_bytes(), &salt).map_err(|e| anyhow::anyhow!("Hash error: {}", e))?.to_string();
        std::fs::write(locker_dir.join("hash"), &hash)?;
        
        let mut key = [0u8; 32];
        let mut salt_bytes = [0u8; 16];
        salt.decode_b64(&mut salt_bytes).map_err(|e| anyhow::anyhow!("Salt decoding error: {}", e))?;
        argon2.hash_password_into(passphrase.as_bytes(), &salt_bytes, &mut key).map_err(|e| anyhow::anyhow!("Key derivation error: {}", e))?;
        
        Ok(key.to_vec())
    }
    
    /// Loads existing key: reads salt, asks passphrase, verifies and derives.
    fn load_key(locker_dir: &PathBuf, passphrase: &str) -> Result<Vec<u8>> {
        let salt_str = std::fs::read_to_string(locker_dir.join("salt"))?;
        let salt = SaltString::from_b64(&salt_str).map_err(|e| anyhow::anyhow!("Salt error: {}", e))?;

        let hash_str = std::fs::read_to_string(locker_dir.join("hash"))?;
        let expected_hash = PasswordHash::new(&hash_str).map_err(|e| anyhow::anyhow!("Hash error: {}", e))?;

        let argon2 = Argon2::default();
        argon2.verify_password(passphrase.as_bytes(), &expected_hash).map_err(|e| anyhow::anyhow!("Incorrect passphrase: {}", e))?;

        let mut salt_bytes = [0u8; 16];
        salt.decode_b64(&mut salt_bytes).map_err(|e| anyhow::anyhow!("Salt decoding error: {}", e))?;
        let mut key = [0u8; 32];
        argon2.hash_password_into(passphrase.as_bytes(), &salt_bytes, &mut key).map_err(|e| anyhow::anyhow!("Key derivation error: {}", e))?;

        Ok(key.to_vec())
    }
    
    
    /// Returns the path to a file in the locker.
    pub fn get_path(&self, filename: &str) -> PathBuf {
        self.base_dir.join(filename)
    }
    
    /// Returns the key for encryption/decryption (use temporarily).
    pub fn get_key(&self) -> Option<&[u8]> {
        self.key.as_ref().map(|k| k.as_slice())
    }
    
    /// Returns the locker base directory.
    pub fn base_dir(&self) -> &PathBuf {
        &self.base_dir
    }
}

impl Drop for Locker {
    fn drop(&mut self) {
        if let Some(ref mut key) = self.key {
            key.zeroize();
        }
    }
}