pub mod keys;
pub mod store;
pub mod util;
pub mod value;
use anyhow::{Context, Result};
use std::{
env, fs,
path::{Path, PathBuf},
};
pub use keys::{Identity, Recipient};
pub fn default_key_dir() -> PathBuf {
env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| env::var_os("APPDATA").map(PathBuf::from))
.unwrap_or_else(|| {
PathBuf::from(env::var("HOME").unwrap_or_else(|_| ".".into())).join(".config")
})
.join("open-envault")
.join("keys")
}
fn legacy_key_dir() -> PathBuf {
default_key_dir()
.parent()
.unwrap_or_else(|| Path::new("."))
.join("open_envault")
.join("keys")
}
fn atomic_write(path: &Path, content: &[u8]) -> Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
fs::create_dir_all(parent)?;
let file_name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "out".into());
let tmp = parent.join(format!(
".{file_name}.{}.{}.tmp",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)?;
std::io::Write::write_all(&mut file, content)?;
file.sync_all()?;
drop(file);
replace_file(&tmp, path).inspect_err(|_error| {
let _ = fs::remove_file(&tmp);
})?;
#[cfg(unix)]
fs::File::open(parent)?.sync_all()?;
Ok(())
}
#[cfg(not(windows))]
fn replace_file(source: &Path, destination: &Path) -> std::io::Result<()> {
fs::rename(source, destination)
}
#[cfg(windows)]
fn replace_file(source: &Path, destination: &Path) -> std::io::Result<()> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Storage::FileSystem::{MOVEFILE_REPLACE_EXISTING, MoveFileExW};
let source: Vec<u16> = source.as_os_str().encode_wide().chain(Some(0)).collect();
let destination: Vec<u16> = destination
.as_os_str()
.encode_wide()
.chain(Some(0))
.collect();
if unsafe {
MoveFileExW(
source.as_ptr(),
destination.as_ptr(),
MOVEFILE_REPLACE_EXISTING,
)
} == 0
{
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
pub fn encrypt(content: &str, path: &Path, recipients: &[String]) -> Result<()> {
let text = store::encrypt(content, &store::recipients_from_strings(recipients)?)?;
atomic_write(path, text.as_bytes())
}
pub fn decrypt_for(path: &Path, environment: Option<&str>) -> Result<String> {
decrypt_for_with_key_file(path, environment, None)
}
pub fn decrypt_for_with_key_file(
path: &Path,
environment: Option<&str>,
configured_key_file: Option<&Path>,
) -> Result<String> {
let mut identities = keys::identities_from_env();
if let Some(key_path) = configured_key_file
&& key_path.is_file()
{
identities.push(keys::read_identity_file(key_path)?);
}
if let Some(name) = environment {
for key_dir in [default_key_dir(), legacy_key_dir()] {
let key_path = key_dir.join(format!("{name}.txt"));
if key_path.is_file()
&& let Ok(identity) = keys::read_identity_file(&key_path)
{
identities.push(identity);
break;
}
}
}
if identities.is_empty() {
anyhow::bail!(
"no age identity available; set OPENENVAULT_AGE_KEY or SOPS_AGE_KEY, configure a key file, or run `oenv setup`"
);
}
let text = fs::read_to_string(path)
.with_context(|| format!("read encrypted file {}", path.display()))?;
store::decrypt(&text, &identities)
}
pub fn decrypt(path: &Path) -> Result<String> {
decrypt_for(path, None)
}
pub fn recipients_of(path: &Path) -> Result<Vec<String>> {
let text = fs::read_to_string(path)
.with_context(|| format!("read encrypted file {}", path.display()))?;
store::list_recipients(&text)
}
pub fn generate_key(path: &Path) -> Result<String> {
let (text, _public) = keys::generate_identity()?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.with_context(|| {
if path.exists() {
format!(
"key file already exists; refusing to overwrite {}",
path.display()
)
} else {
format!("write key file {}", path.display())
}
})?;
std::io::Write::write_all(&mut file, text.as_bytes())?;
file.sync_all()?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o600))
.with_context(|| format!("chmod key file {}", path.display()))?;
}
Ok(text)
}
pub fn ensure_key(path: &Path) -> Result<(String, String, bool)> {
if path.is_file() {
let identity = keys::read_identity_file(path)
.with_context(|| format!("validate existing age key {}", path.display()))?;
return Ok((
identity.to_public().to_string(),
path.display().to_string(),
false,
));
}
let text = generate_key(path)?;
let identity = keys::identity_from_key_text(&text)?;
Ok((
identity.to_public().to_string(),
path.display().to_string(),
true,
))
}