use anyhow::Result;
use std::path::{Path, PathBuf};
fn session_path(vault_dir: &Path) -> PathBuf {
vault_dir.join(".session")
}
pub fn unlock_with_key(vault_dir: &Path, key: &[u8; 32]) -> Result<()> {
let path = session_path(vault_dir);
let encoded = hex::encode(key);
#[cfg(unix)]
{
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut f = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&path)?;
f.write_all(encoded.as_bytes())?;
}
#[cfg(not(unix))]
std::fs::write(&path, &encoded)?;
Ok(())
}
pub fn lock(vault_dir: &Path) -> Result<()> {
let path = session_path(vault_dir);
if path.exists() {
let len = std::fs::metadata(&path)?.len() as usize;
std::fs::write(&path, vec![0u8; len])?;
std::fs::remove_file(&path)?;
}
Ok(())
}
pub fn is_unlocked(vault_dir: &Path) -> bool {
get_key(vault_dir).is_some()
}
pub fn get_key(vault_dir: &Path) -> Option<[u8; 32]> {
let path = session_path(vault_dir);
let contents = std::fs::read_to_string(&path).ok()?;
let bytes = hex::decode(contents.trim()).ok()?;
bytes.try_into().ok()
}
pub fn lock_all(svault_dir: &Path) -> Result<usize> {
let mut count = 0;
let Ok(entries) = std::fs::read_dir(svault_dir) else {
return Ok(0);
};
for entry in entries.flatten() {
let vault_dir = entry.path();
if vault_dir.is_dir() && session_path(&vault_dir).exists() {
lock(&vault_dir)?;
count += 1;
}
}
Ok(count)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn unlock_caches_key_then_lock_clears() {
let dir = TempDir::new().unwrap();
let vault_dir = dir.path().join("v");
std::fs::create_dir_all(&vault_dir).unwrap();
assert!(!is_unlocked(&vault_dir));
let key = [7u8; 32];
unlock_with_key(&vault_dir, &key).unwrap();
assert!(is_unlocked(&vault_dir));
assert_eq!(get_key(&vault_dir), Some(key));
lock(&vault_dir).unwrap();
assert!(!is_unlocked(&vault_dir));
assert_eq!(get_key(&vault_dir), None);
}
#[test]
fn session_never_contains_a_passphrase() {
let dir = TempDir::new().unwrap();
let vault_dir = dir.path().join("v");
std::fs::create_dir_all(&vault_dir).unwrap();
unlock_with_key(&vault_dir, &[0xABu8; 32]).unwrap();
let raw = std::fs::read_to_string(session_path(&vault_dir)).unwrap();
assert_eq!(raw.trim(), "ab".repeat(32));
std::fs::write(session_path(&vault_dir), "hunter2").unwrap();
assert_eq!(get_key(&vault_dir), None);
}
#[test]
fn lock_all_locks_every_unlocked_vault() {
let svault = TempDir::new().unwrap();
let a = svault.path().join("a");
let b = svault.path().join("b");
std::fs::create_dir_all(&a).unwrap();
std::fs::create_dir_all(&b).unwrap();
unlock_with_key(&a, &[1u8; 32]).unwrap();
unlock_with_key(&b, &[2u8; 32]).unwrap();
assert_eq!(lock_all(svault.path()).unwrap(), 2);
assert!(!is_unlocked(&a));
assert!(!is_unlocked(&b));
assert_eq!(lock_all(svault.path()).unwrap(), 0);
}
}