use std::path::{Path, PathBuf};
use std::process::Command;
use crate::error::Result;
pub const SOPS_AGE_KEY_FILE_ENV: &str = "SOPS_AGE_KEY_FILE";
pub const SOPS_AGE_KEY_ENV: &str = "SOPS_AGE_KEY";
pub const KEYS_FILE_ENV: &str = "SOPSY_KEYS_FILE";
pub(crate) fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME")
.map(PathBuf::from)
.filter(|p| !p.as_os_str().is_empty())
}
pub fn keys_file() -> PathBuf {
if let Some(explicit) = std::env::var_os(KEYS_FILE_ENV) {
return PathBuf::from(explicit);
}
if let Some(explicit) = std::env::var_os(SOPS_AGE_KEY_FILE_ENV) {
return PathBuf::from(explicit);
}
let home = home_dir().unwrap_or_else(|| PathBuf::from("."));
#[cfg(target_os = "macos")]
let base = home.join("Library").join("Application Support");
#[cfg(not(target_os = "macos"))]
let base = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| home.join(".config"));
base.join("sops").join("age").join("keys.txt")
}
pub fn store_identity(label: &str, public_key: &str, identity: &str) -> Result<PathBuf> {
let path = keys_file();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
restrict(parent, 0o700);
}
let existing = std::fs::read_to_string(&path).unwrap_or_default();
let identity = identity.trim();
if existing.lines().any(|line| line.trim() == identity) {
return Ok(path);
}
let mut updated = existing;
if !updated.is_empty() && !updated.ends_with('\n') {
updated.push('\n');
}
updated.push_str(&format!(
"# sopsy: {label}\n# public key: {public_key}\n{identity}\n"
));
std::fs::write(&path, updated)?;
restrict(&path, 0o600);
Ok(path)
}
pub fn configure_sops_env(cmd: &mut Command) {
if std::env::var_os(SOPS_AGE_KEY_FILE_ENV).is_some()
|| std::env::var_os(SOPS_AGE_KEY_ENV).is_some()
{
return;
}
let path = keys_file();
if path.exists() {
cmd.env(SOPS_AGE_KEY_FILE_ENV, path);
}
}
fn restrict(path: &Path, mode: u32) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = std::fs::metadata(path) {
let mut perms = metadata.permissions();
perms.set_mode(mode);
let _ = std::fs::set_permissions(path, perms);
}
}
#[cfg(not(unix))]
let _ = (path, mode);
}
#[cfg(test)]
mod tests {
use super::*;
#[serial_test::serial]
#[test]
fn keys_file_honors_explicit_override() {
let prev = std::env::var_os(SOPS_AGE_KEY_FILE_ENV);
unsafe { std::env::set_var(SOPS_AGE_KEY_FILE_ENV, "/tmp/custom/keys.txt") };
assert_eq!(keys_file(), PathBuf::from("/tmp/custom/keys.txt"));
match prev {
Some(v) => unsafe { std::env::set_var(SOPS_AGE_KEY_FILE_ENV, v) },
None => unsafe { std::env::remove_var(SOPS_AGE_KEY_FILE_ENV) },
}
}
#[serial_test::serial]
#[test]
fn store_identity_appends_once() {
let dir = assert_fs::TempDir::new().unwrap();
let keys = dir.path().join("keys.txt");
let prev = std::env::var_os(SOPS_AGE_KEY_FILE_ENV);
unsafe { std::env::set_var(SOPS_AGE_KEY_FILE_ENV, &keys) };
let id = "AGE-PLUGIN-SE-1QXAMPLE";
store_identity("admin", "age1se1abc", id).unwrap();
store_identity("admin", "age1se1abc", id).unwrap();
let contents = std::fs::read_to_string(&keys).unwrap();
assert_eq!(
contents.matches(id).count(),
1,
"identity written exactly once"
);
assert!(contents.contains("# public key: age1se1abc"));
match prev {
Some(v) => unsafe { std::env::set_var(SOPS_AGE_KEY_FILE_ENV, v) },
None => unsafe { std::env::remove_var(SOPS_AGE_KEY_FILE_ENV) },
}
}
}