use anyhow::Result;
use argon2::{
Argon2,
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
};
use directories::BaseDirs;
use std::path::PathBuf;
use zeroize::Zeroize;
pub struct Locker {
base_dir: PathBuf,
key: Option<Vec<u8>>, }
impl Locker {
#[allow(dead_code)]
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: &std::path::Path, 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: &std::path::Path, 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())
}
#[allow(dead_code)]
pub fn get_path(&self, filename: &str) -> PathBuf {
self.base_dir.join(filename)
}
pub fn get_key(&self) -> Option<&[u8]> {
self.key.as_deref()
}
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();
}
}
}