use anyhow::{Context, Result, anyhow, bail};
use sefy_core::agent::{Client, LockState, Seat};
use sefy_core::crypto::{DerivedKey, SALT_LEN, random_bytes};
use sefy_core::{Error, FreshKey, KeySource, Opener, Vault};
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use zeroize::Zeroizing;
pub fn vault_path(argument: Option<PathBuf>) -> Result<PathBuf> {
argument.ok_or_else(|| {
anyhow!(
"no vault given\n\
pass --vault <FILE>, or set SEFY_VAULT to the path of your vault.\n\
sefy has no default location on purpose: a predictable path would\n\
give the file away."
)
})
}
#[derive(Clone)]
pub enum Master {
Typed(Zeroizing<String>),
Held(Arc<HeldKeys>),
}
impl Master {
pub fn opener(&self) -> Opener<'_> {
match self {
Self::Typed(password) => Opener::Password(password.as_bytes()),
Self::Held(keys) => Opener::Source(keys.as_ref()),
}
}
}
pub struct HeldKeys {
client: Client,
opened: AtomicBool,
opened_with: Mutex<Option<([u8; SALT_LEN], DerivedKey)>>,
typed: Mutex<Option<Zeroizing<String>>>,
}
const ATTEMPTS: usize = 3;
impl HeldKeys {
fn new(client: Client) -> Self {
Self {
client,
opened: AtomicBool::new(false),
opened_with: Mutex::new(None),
typed: Mutex::new(None),
}
}
fn typed(&self) -> sefy_core::Result<Zeroizing<String>> {
let mut typed = self.typed.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(password) = typed.as_ref() {
return Ok(password.clone());
}
let opened_with = self
.opened_with
.lock()
.unwrap_or_else(PoisonError::into_inner);
let (salt, key) = opened_with.as_ref().ok_or(Error::KeysGone)?;
let mut question = "The vault locked while this was running. Master password: ";
for _ in 0..ATTEMPTS {
let answer = Zeroizing::new(prompt(question).map_err(|_| Error::KeysGone)?);
if DerivedKey::derive(answer.as_bytes(), salt)?.same_as(key) {
*typed = Some(answer.clone());
return Ok(answer);
}
question = "That is not it. Master password: ";
}
Err(Error::KeysChanged)
}
fn may_ask(&self) -> bool {
self.opened.load(Ordering::SeqCst)
}
fn holder(&self) -> Option<(Seat, std::time::Duration)> {
match self.client.state() {
Some(LockState::Unlocked { left, .. }) => Some((self.client.seat(), left)),
_ => None,
}
}
}
impl KeySource for HeldKeys {
fn key_for(&self, salt: &[u8; SALT_LEN]) -> sefy_core::Result<DerivedKey> {
let key = match self.client.key_for(salt) {
Err(Error::KeysGone) if self.may_ask() => {
DerivedKey::derive(self.typed()?.as_bytes(), salt)
}
other => other,
}?;
if !self.may_ask() {
*self
.opened_with
.lock()
.unwrap_or_else(PoisonError::into_inner) = Some((*salt, key.duplicate()));
}
Ok(key)
}
fn fresh(&self, opened_with: &[u8; SALT_LEN]) -> sefy_core::Result<FreshKey> {
match self.client.fresh(opened_with) {
Err(Error::KeysGone) if self.may_ask() => {
let password = self.typed()?;
let salt = random_bytes::<SALT_LEN>()?;
Ok(FreshKey {
salt,
key: DerivedKey::derive(password.as_bytes(), &salt)?,
proof: DerivedKey::derive(password.as_bytes(), opened_with)?,
})
}
other => other,
}
}
}
pub fn unlock(path: &Path, password_env: Option<&str>) -> Result<(Vault, Master)> {
if password_env.is_none()
&& let Some(opened) = through_agent(path)?
{
return Ok(opened);
}
typed(path, password_env)
}
pub fn typed(path: &Path, password_env: Option<&str>) -> Result<(Vault, Master)> {
let password = password(password_env)?;
let vault = open(path, &password)?;
Ok((vault, Master::Typed(Zeroizing::new(password))))
}
pub fn through_agent(path: &Path) -> Result<Option<(Vault, Master)>> {
open_through_agent(path, false)
}
pub fn looking_through_agent(path: &Path) -> Result<Option<(Vault, Master)>> {
open_through_agent(path, true)
}
fn open_through_agent(path: &Path, looking: bool) -> Result<Option<(Vault, Master)>> {
for seat in Seat::ALL {
let Some(client) = Client::of(seat) else {
continue;
};
let client = if looking {
client.only_looking()
} else {
client
};
let keys = Arc::new(HeldKeys::new(client));
match Vault::open_through(path, keys.clone()) {
Ok(mut vault) => {
vault.set_device(crate::device::name());
keys.opened.store(true, Ordering::SeqCst);
return Ok(Some((vault, Master::Held(keys))));
}
Err(Error::KeysGone | Error::WrongPasswordOrNotAVault | Error::TooSmall) => {}
Err(other) => return Err(other.into()),
}
}
Ok(None)
}
pub fn lock_everywhere() -> Vec<&'static str> {
Seat::ALL
.into_iter()
.filter(|seat| Client::of(*seat).and_then(|client| client.lock()) == Some(true))
.map(Seat::name)
.collect()
}
pub fn holders() -> Vec<(Seat, std::time::Duration)> {
Seat::ALL
.into_iter()
.filter_map(|seat| {
let client = Client::of(seat)?;
match client.state()? {
LockState::Unlocked { left, .. } => Some((seat, left)),
LockState::Locked => None,
}
})
.collect()
}
pub fn lock_line(path: &Path, master: Option<&Master>) -> Result<String> {
let held = match master {
Some(Master::Held(keys)) => keys.holder(),
_ => match looking_through_agent(path)? {
Some((_, Master::Held(keys))) => keys.holder(),
_ => None,
},
};
Ok(match held {
Some((seat, left)) => format!(
"unlocked by the {}, locks in {}",
seat.name(),
crate::when::span(left)
),
None if holders().is_empty() => "locked".to_owned(),
None => "locked (what is running holds another vault's password)".to_owned(),
})
}
pub fn password(password_env: Option<&str>) -> Result<String> {
if let Some(variable) = password_env {
return from_env(variable);
}
prompt("Master password: ")
}
pub fn new_password(password_env: Option<&str>) -> Result<String> {
if let Some(variable) = password_env {
return from_env(variable);
}
let first = prompt("New master password: ")?;
let second = prompt("Repeat it: ")?;
if first != second {
bail!("the two passwords do not match");
}
Ok(first)
}
pub fn secret(prompt_text: &str, password_env: Option<&str>) -> Result<String> {
if let Some(variable) = password_env {
return from_env(variable);
}
prompt(prompt_text)
}
fn from_env(variable: &str) -> Result<String> {
std::env::var(variable).with_context(|| format!("cannot read the password from ${variable}"))
}
fn prompt(text: &str) -> Result<String> {
let someone_is_there = std::io::stdin().is_terminal()
|| std::io::stdout().is_terminal()
|| std::io::stderr().is_terminal();
if !someone_is_there {
bail!(
"cannot ask for a password: sefy is not attached to a terminal\n\
use --password-env <VAR> to pass it through the environment,\n\
or run `sefy agent` first to keep the vault unlocked for a while."
);
}
rpassword::prompt_password(text).context("cannot read the password")
}
pub fn open(path: &Path, password: &str) -> Result<Vault> {
match Vault::open(path, password.as_bytes()) {
Ok(mut vault) => {
vault.set_device(crate::device::name());
Ok(vault)
}
Err(Error::WrongPasswordOrNotAVault) => bail!(
"wrong password, or {} is not a vault\n\
(an encrypted file cannot tell the two apart)",
path.display()
),
Err(other) => Err(other.into()),
}
}