#![allow(dead_code)]
use anyhow::{anyhow, Result};
use rand::RngCore;
use std::path::{Path, PathBuf};
use crate::crypto::{self, VaultKey, SALT_SIZE};
use crate::vault::SVAULT_DIR;
const MASTER_FILE: &str = "master.enc";
const MASTER_SESSION: &str = ".master.session";
const MASTER_RECOVERY: &str = "master.recovery.enc";
pub const VAULT_KEYSLOT: &str = "keyslot.enc";
pub const KEYRING_KEYSLOT: &str = "keyring.keyslot.enc";
fn master_path() -> PathBuf {
PathBuf::from(SVAULT_DIR).join(MASTER_FILE)
}
fn session_path() -> PathBuf {
PathBuf::from(SVAULT_DIR).join(MASTER_SESSION)
}
fn keyslot_path(vault_dir: &Path) -> PathBuf {
vault_dir.join(VAULT_KEYSLOT)
}
fn keyring_keyslot_path() -> PathBuf {
PathBuf::from(SVAULT_DIR).join(KEYRING_KEYSLOT)
}
fn master_recovery_path() -> PathBuf {
PathBuf::from(SVAULT_DIR).join(MASTER_RECOVERY)
}
pub fn master_recovery_exists() -> bool {
master_recovery_path().exists()
}
pub fn exists() -> bool {
master_path().exists()
}
pub fn vault_has_keyslot(vault_dir: &Path) -> bool {
keyslot_path(vault_dir).exists()
}
pub fn keyring_has_keyslot() -> bool {
keyring_keyslot_path().exists()
}
pub struct Master {
mk: VaultKey,
}
impl Master {
pub fn init(passphrase: &str) -> Result<Self> {
let path = master_path();
if path.exists() {
return Err(anyhow!("a master passphrase is already set"));
}
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
crate::secfile::create_dir_owner_only(parent)?;
}
}
let mut mk_bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut mk_bytes);
let mk = VaultKey::from_bytes(mk_bytes);
write_master_slot(&path, &mk, passphrase)?;
Ok(Self { mk })
}
pub fn open(passphrase: &str) -> Result<Self> {
let blob = std::fs::read(master_path())
.map_err(|_| anyhow!("no master passphrase set — run 'svault master init'"))?;
if blob.len() < SALT_SIZE {
return Err(anyhow!("master.enc is too short — may be corrupted"));
}
let salt = &blob[..SALT_SIZE];
let kek = VaultKey::derive(passphrase, salt)?;
let mk_bytes =
crypto::decrypt(&kek, &blob).map_err(|_| anyhow!("wrong master passphrase"))?;
let mk_bytes: [u8; 32] = mk_bytes
.try_into()
.map_err(|_| anyhow!("master.enc holds an unexpected key length"))?;
Ok(Self {
mk: VaultKey::from_bytes(mk_bytes),
})
}
pub fn open_with_key(mk: [u8; 32]) -> Self {
Self {
mk: VaultKey::from_bytes(mk),
}
}
pub fn rekey(&self, new_passphrase: &str) -> Result<()> {
write_master_slot(&master_path(), &self.mk, new_passphrase)
}
pub fn write_recovery(&self) -> Result<String> {
let code = crate::recovery::generate_code();
crate::recovery::write_at(&master_recovery_path(), &self.mk, &code)?;
Ok(code)
}
pub fn key_bytes(&self) -> &[u8; 32] {
self.mk.bytes()
}
pub fn wrap_dek(&self, vault_dir: &Path, dek: &VaultKey) -> Result<()> {
self.wrap_dek_at(&keyslot_path(vault_dir), dek)
}
pub fn unwrap_dek(&self, vault_dir: &Path) -> Result<VaultKey> {
self.unwrap_dek_at(
&keyslot_path(vault_dir),
"vault is not wrapped under the master key (no keyslot.enc)",
)
}
pub fn wrap_keyring_dek(&self, dek: &VaultKey) -> Result<()> {
self.wrap_dek_at(&keyring_keyslot_path(), dek)
}
pub fn unwrap_keyring_dek(&self) -> Result<VaultKey> {
self.unwrap_dek_at(
&keyring_keyslot_path(),
"keyring is not wrapped under the master key (no keyring.keyslot.enc)",
)
}
fn wrap_dek_at(&self, path: &Path, dek: &VaultKey) -> Result<()> {
let mut salt = [0u8; SALT_SIZE];
rand::thread_rng().fill_bytes(&mut salt);
let blob = crypto::encrypt(&self.mk, &salt, dek.bytes())?;
crate::secfile::write_owner_only(path, &blob)?;
Ok(())
}
fn unwrap_dek_at(&self, path: &Path, missing: &str) -> Result<VaultKey> {
let blob = std::fs::read(path).map_err(|_| anyhow!("{missing}"))?;
let dek_bytes = crypto::decrypt(&self.mk, &blob)
.map_err(|_| anyhow!("could not unwrap the data key with this master"))?;
let dek_bytes: [u8; 32] = dek_bytes
.try_into()
.map_err(|_| anyhow!("keyslot holds an unexpected key length"))?;
Ok(VaultKey::from_bytes(dek_bytes))
}
}
pub fn recover(code: &str, new_passphrase: &str) -> Result<Master> {
let path = master_recovery_path();
if !path.exists() {
return Err(anyhow!(
"no master recovery code on this machine (master.recovery.enc missing)"
));
}
let mk = crate::recovery::unlock_at(&path, code)?;
write_master_slot(&master_path(), &mk, new_passphrase)?;
Ok(Master { mk })
}
pub fn new_dek() -> VaultKey {
let mut bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut bytes);
VaultKey::from_bytes(bytes)
}
fn write_master_slot(path: &Path, mk: &VaultKey, passphrase: &str) -> Result<()> {
let mut salt = [0u8; SALT_SIZE];
rand::thread_rng().fill_bytes(&mut salt);
let kek = VaultKey::derive(passphrase, &salt)?;
let blob = crypto::encrypt(&kek, &salt, mk.bytes())?;
crate::secfile::write_owner_only(path, &blob)?;
Ok(())
}
pub fn unlock_session(mk: &[u8; 32]) -> Result<()> {
crate::secfile::write_owner_only(&session_path(), hex::encode(mk).as_bytes())?;
Ok(())
}
pub fn lock_session() -> Result<()> {
let path = session_path();
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 session_key() -> Option<[u8; 32]> {
let contents = std::fs::read_to_string(session_path()).ok()?;
hex::decode(contents.trim()).ok()?.try_into().ok()
}
pub fn is_unlocked() -> bool {
session_key().is_some()
}
pub fn open_from_session() -> Option<Master> {
Some(Master::open_with_key(session_key()?))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testlock::CWD_LOCK;
use std::sync::MutexGuard;
fn in_temp_cwd() -> (MutexGuard<'static, ()>, tempfile::TempDir, PathBuf) {
let guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let tmp = tempfile::TempDir::new().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(tmp.path()).unwrap();
(guard, tmp, prev)
}
#[test]
fn init_then_open_recovers_the_same_master_key() {
let (_g, _tmp, prev) = in_temp_cwd();
let m = Master::init("Master!Pass#1").unwrap();
let mk = *m.key_bytes();
drop(m);
assert!(Master::open("nope").is_err());
let reopened = Master::open("Master!Pass#1").unwrap();
assert_eq!(reopened.key_bytes(), &mk);
std::env::set_current_dir(prev).unwrap();
}
#[test]
fn dek_wraps_under_master_and_unwraps_back() {
let (_g, _tmp, prev) = in_temp_cwd();
let m = Master::init("Master!Pass#2").unwrap();
let vault_dir = PathBuf::from(SVAULT_DIR).join("v");
crate::secfile::create_dir_owner_only(&vault_dir).unwrap();
let dek = new_dek();
let dek_bytes = *dek.bytes();
m.wrap_dek(&vault_dir, &dek).unwrap();
assert!(vault_has_keyslot(&vault_dir));
let unwrapped = m.unwrap_dek(&vault_dir).unwrap();
assert_eq!(unwrapped.bytes(), &dek_bytes);
std::env::set_current_dir(prev).unwrap();
}
#[test]
fn keyring_dek_wraps_under_master_and_unwraps_back() {
let (_g, _tmp, prev) = in_temp_cwd();
let m = Master::init("Master!Pass#KR").unwrap();
crate::secfile::create_dir_owner_only(&PathBuf::from(SVAULT_DIR)).unwrap();
let dek = new_dek();
let dek_bytes = *dek.bytes();
m.wrap_keyring_dek(&dek).unwrap();
assert!(keyring_has_keyslot());
let unwrapped = m.unwrap_keyring_dek().unwrap();
assert_eq!(unwrapped.bytes(), &dek_bytes);
std::env::set_current_dir(prev).unwrap();
}
#[test]
fn rekey_keeps_master_key_and_keeps_unwrapping_vaults() {
let (_g, _tmp, prev) = in_temp_cwd();
let m = Master::init("Old!Master#1").unwrap();
let vault_dir = PathBuf::from(SVAULT_DIR).join("v");
crate::secfile::create_dir_owner_only(&vault_dir).unwrap();
let dek = new_dek();
let dek_bytes = *dek.bytes();
m.wrap_dek(&vault_dir, &dek).unwrap();
m.rekey("New!Master#2").unwrap();
assert!(Master::open("Old!Master#1").is_err());
let reopened = Master::open("New!Master#2").unwrap();
assert_eq!(reopened.unwrap_dek(&vault_dir).unwrap().bytes(), &dek_bytes);
std::env::set_current_dir(prev).unwrap();
}
#[test]
fn wrong_master_cannot_unwrap_a_dek() {
let (_g, _tmp, prev) = in_temp_cwd();
let m = Master::init("Right!Master#1").unwrap();
let vault_dir = PathBuf::from(SVAULT_DIR).join("v");
crate::secfile::create_dir_owner_only(&vault_dir).unwrap();
m.wrap_dek(&vault_dir, &new_dek()).unwrap();
let other = Master::open_with_key([0x11u8; 32]);
assert!(other.unwrap_dek(&vault_dir).is_err());
std::env::set_current_dir(prev).unwrap();
}
#[test]
fn recovery_code_resets_the_master_and_keeps_unwrapping_stores() {
let (_g, _tmp, prev) = in_temp_cwd();
let m = Master::init("Old!Master#R").unwrap();
let vault_dir = PathBuf::from(SVAULT_DIR).join("v");
crate::secfile::create_dir_owner_only(&vault_dir).unwrap();
let dek = new_dek();
let dek_bytes = *dek.bytes();
m.wrap_dek(&vault_dir, &dek).unwrap();
let code = m.write_recovery().unwrap();
assert!(master_recovery_exists());
drop(m);
let recovered = recover(&code, "New!Master#R").unwrap();
assert_eq!(
recovered.unwrap_dek(&vault_dir).unwrap().bytes(),
&dek_bytes
);
assert!(Master::open("New!Master#R").is_ok());
assert!(recover("0000-0000-0000-0000-0000-0000-0000-0000-0000-0000", "X").is_err());
std::env::set_current_dir(prev).unwrap();
}
#[test]
fn session_caches_mk_then_lock_clears() {
let (_g, _tmp, prev) = in_temp_cwd();
crate::secfile::create_dir_owner_only(&PathBuf::from(SVAULT_DIR)).unwrap();
assert!(!is_unlocked());
unlock_session(&[5u8; 32]).unwrap();
assert!(is_unlocked());
assert_eq!(session_key(), Some([5u8; 32]));
lock_session().unwrap();
assert!(!is_unlocked());
std::env::set_current_dir(prev).unwrap();
}
}