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>>, }
impl Locker {
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"));
}
Err(anyhow::anyhow!("Passphrase required to load locker"))
}
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) })
}
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())
}
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())
}
pub fn get_path(&self, filename: &str) -> PathBuf {
self.base_dir.join(filename)
}
pub fn get_key(&self) -> Option<&[u8]> {
self.key.as_ref().map(|k| k.as_slice())
}
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();
}
}
}