use std::path::{Path, PathBuf};
use zeroize::Zeroize;
use crate::audit;
use crate::error::Error;
use crate::secret_health;
use crate::security_config;
use crate::vault::store::MAX_SECRET_SIZE_BYTES;
use crate::vault::Vault;
use crate::{gui, policy};
pub fn revoke_with_policy(vault: &Vault, name: &str) -> Result<(), Error> {
let policy_path = vault.policy_path();
let sealed_path = policy::sealed_path_for(&policy_path);
if sealed_path.exists() || policy_path.exists() {
let mut policy = policy::Policy::load_sealed(&policy_path, vault.master_key_bytes())?;
let before_count = policy.rules.len();
policy.revoke_secret(name);
let removed = before_count - policy.rules.len();
if removed > 0 {
eprintln!("envseal: removed {removed} policy rule(s) referencing '{name}'");
}
policy.save_sealed(&policy_path, vault.master_key_bytes())?;
}
vault.revoke(name)?;
audit::log_required_at(
vault.root(),
&audit::AuditEvent::SecretRevoked {
name: name.to_string(),
},
)?;
Ok(())
}
pub fn copy_secret(vault: &Vault, source: &str, dest: &str, force: bool) -> Result<(), Error> {
let value = vault.decrypt(source)?;
vault.store(dest, &value, force)?;
audit::log_required_at(
vault.root(),
&audit::AuditEvent::SecretStored {
name: dest.to_string(),
},
)?;
Ok(())
}
pub fn copy_secret_default(source: &str, dest: &str, force: bool) -> Result<(), Error> {
let vault = Vault::open_default()?;
copy_secret(&vault, source, dest, force)
}
pub fn rename_secret(
vault: &Vault,
old_name: &str,
new_name: &str,
force: bool,
) -> Result<(), Error> {
let value = vault.decrypt(old_name)?;
vault.store(new_name, &value, force)?;
let policy_path = vault.policy_path();
let sealed_path = policy::sealed_path_for(&policy_path);
if sealed_path.exists() || policy_path.exists() {
let mut policy = policy::Policy::load_sealed(&policy_path, vault.master_key_bytes())?;
let old_rules: Vec<_> = policy
.rules
.iter()
.filter(|r| r.scope == policy::RuleScope::Key && r.secret == old_name)
.cloned()
.collect();
for mut rule in old_rules {
rule.secret = new_name.to_string();
if !policy.rules.iter().any(|r| {
r.binary == rule.binary
&& r.scope == rule.scope
&& r.secret == rule.secret
&& r.binary_hash == rule.binary_hash
}) {
policy.rules.push(rule);
}
}
policy.revoke_secret(old_name);
policy.save_sealed(&policy_path, vault.master_key_bytes())?;
}
vault.revoke(old_name)?;
audit::log_required_at(
vault.root(),
&audit::AuditEvent::SecretStored {
name: new_name.to_string(),
},
)?;
audit::log_required_at(
vault.root(),
&audit::AuditEvent::SecretRevoked {
name: old_name.to_string(),
},
)?;
Ok(())
}
pub fn rename_secret_default(old_name: &str, new_name: &str, force: bool) -> Result<(), Error> {
let vault = Vault::open_default()?;
rename_secret(&vault, old_name, new_name, force)
}
pub fn store_secret_in(
vault: &Vault,
name: &str,
value: &[u8],
force: bool,
) -> Result<Vec<crate::guard::Signal>, Error> {
if value.len() > MAX_SECRET_SIZE_BYTES {
return Err(Error::CryptoFailure(format!(
"secret exceeds max size: {} bytes > {} bytes",
value.len(),
MAX_SECRET_SIZE_BYTES
)));
}
vault.store(name, value, force)?;
let warnings = secret_health::check_entropy(name, value);
let _ = crate::guard::emit_signals_inline(
warnings.clone(),
&crate::security_config::load_system_defaults(),
);
audit::log_required_at(
vault.root(),
&audit::AuditEvent::SecretStored {
name: name.to_string(),
},
)?;
Ok(warnings)
}
pub fn store_secret(
name: &str,
value: &[u8],
force: bool,
) -> Result<Vec<crate::guard::Signal>, Error> {
let vault = Vault::open_default()?;
store_secret_in(&vault, name, value, force)
}
pub fn request_secret_value_default(
name: &str,
description: &str,
) -> Result<zeroize::Zeroizing<String>, Error> {
let secret = gui::request_secret_value(
name,
description,
&security_config::SecurityConfig::default(),
)?;
if secret.is_empty() {
return Err(Error::CryptoFailure(
"refusing to store an empty secret".to_string(),
));
}
Ok(secret)
}
pub fn revoke_secret(name: &str) -> Result<(), Error> {
let vault = Vault::open_default()?;
revoke_with_policy(&vault, name)
}
#[derive(Debug, Clone, Default)]
pub struct EmergencyRevokeResult {
pub attempted: Vec<String>,
pub revoked: usize,
pub failures: Vec<(String, String)>,
}
pub fn emergency_revoke_all() -> Result<EmergencyRevokeResult, Error> {
let vault = Vault::open_default()?;
let names = vault.list()?;
let mut result = EmergencyRevokeResult {
attempted: names.clone(),
revoked: 0,
failures: Vec::new(),
};
for name in &names {
match revoke_with_policy(&vault, name) {
Ok(()) => {
result.revoked += 1;
}
Err(e) => result.failures.push((name.clone(), e.to_string())),
}
}
Ok(result)
}
pub fn shred_file(path: &Path) -> Result<(), Error> {
crate::guard::verify_not_symlink(path)?;
#[cfg(unix)]
{
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
if let Ok(meta) = std::fs::symlink_metadata(path) {
let len = usize::try_from(meta.len()).unwrap_or(0);
if len > 0 {
let cap = len.min(crate::vault::store::MAX_SECRET_SIZE_BYTES);
let mut file = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
.open(path)
.map_err(Error::StorageIo)?;
file.write_all(&vec![0u8; cap]).map_err(Error::StorageIo)?;
file.sync_all().map_err(Error::StorageIo)?;
}
}
}
#[cfg(not(unix))]
{
if let Ok(meta) = std::fs::metadata(path) {
let len = usize::try_from(meta.len()).unwrap_or(0);
if len > 0 {
let cap = len.min(crate::vault::store::MAX_SECRET_SIZE_BYTES);
std::fs::write(path, vec![0u8; cap]).map_err(Error::StorageIo)?;
}
}
}
std::fs::remove_file(path).map_err(Error::StorageIo)?;
Ok(())
}
pub fn rotate_secret(name: &str) -> Result<(), Error> {
let vault = Vault::open_default()?;
if !vault.has_secret(name) {
return Err(Error::CryptoFailure(format!(
"secret '{name}' does not exist in vault. use `envseal store` to create it."
)));
}
let config = security_config::load_config(vault.root(), vault.master_key_bytes())?;
let secret = gui::request_secret_value(
name,
&format!("Enter new value for secret rotation: {name}"),
&config,
)?;
vault.store(name, secret.as_bytes(), true)?;
let warnings = secret_health::check_entropy(name, secret.as_bytes());
let _ = crate::guard::emit_signals_inline(warnings, &config);
audit::log_required_at(
vault.root(),
&audit::AuditEvent::SecretStored {
name: name.to_string(),
},
)?;
Ok(())
}
#[derive(Debug, Clone, Default)]
pub struct ImportResult {
pub entries: Vec<ImportEntry>,
pub envseal_lines: Vec<String>,
pub output_path: PathBuf,
pub imported: usize,
}
#[derive(Debug, Clone)]
pub struct ImportEntry {
pub env_var: String,
pub secret_name: String,
pub status: Result<Vec<crate::guard::Signal>, String>,
}
pub fn import_env_file(env_path: &Path) -> Result<ImportResult, Error> {
let content = std::fs::read_to_string(env_path).map_err(Error::StorageIo)?;
let mut parsed: Vec<(String, String, String)> = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let parts: Vec<&str> = trimmed.splitn(2, '=').collect();
if parts.len() != 2 || parts[0].is_empty() {
continue;
}
let env_var = parts[0].trim().to_string();
let raw_value = parts[1].trim().to_string();
let value = raw_value
.strip_prefix('"')
.and_then(|v| v.strip_suffix('"'))
.or_else(|| {
raw_value
.strip_prefix('\'')
.and_then(|v| v.strip_suffix('\''))
})
.unwrap_or(&raw_value)
.to_string();
if value.is_empty() {
continue;
}
let secret_name = env_var.to_lowercase().replace('_', "-");
parsed.push((env_var, secret_name, value));
}
if parsed.is_empty() {
return Err(Error::CryptoFailure(format!(
"no valid key-value pairs found in {}",
env_path.display()
)));
}
let vault = Vault::open_default()?;
let mut entries = Vec::with_capacity(parsed.len());
let mut envseal_lines = Vec::with_capacity(parsed.len());
let mut imported = 0_usize;
for (env_var, secret_name, value) in parsed {
let status = match vault.store(&secret_name, value.as_bytes(), false) {
Ok(()) => {
let warnings = secret_health::check_entropy(&secret_name, value.as_bytes());
audit::log_required_at(
vault.root(),
&audit::AuditEvent::SecretStored {
name: secret_name.clone(),
},
)?;
imported += 1;
Ok(warnings)
}
Err(e) => Err(e.to_string()),
};
let mut value_bytes = value.into_bytes();
value_bytes.zeroize();
envseal_lines.push(format!("{env_var}={secret_name}"));
entries.push(ImportEntry {
env_var,
secret_name,
status,
});
}
let output_path = if env_path.file_name().map(|f| f.to_str()) == Some(Some(".env")) {
env_path.with_file_name(".envseal")
} else {
env_path.with_extension("envseal")
};
Ok(ImportResult {
entries,
envseal_lines,
output_path,
imported,
})
}