use anyhow::Result;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use zeroize::Zeroize;
use crate::core::store::SecretsStore;
pub fn execute_with_secrets(
command: &str,
store: &SecretsStore,
key: &[u8],
) -> Result<std::process::Output> {
let mut env_vars = store.decrypt_all(key)?;
let output = Command::new("sh")
.arg("-c")
.arg(command)
.envs(&env_vars)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()?;
for (_, mut value) in env_vars.drain() {
value.zeroize();
}
Ok(output)
}
#[allow(dead_code)]
pub fn generate_python_wrapper(script_path: &str, locker_path: &std::path::Path) -> String {
format!(
r#"#!/usr/bin/env python3
"""
Secure wrapper generated by lazy-locker.
This script injects secrets in memory before executing the target script.
"""
import subprocess
import sys
import os
def main():
# Call lazy-locker to get secrets (via secure pipe)
result = subprocess.run(
['lazy-locker', 'export', '--format', 'env'],
capture_output=True,
text=True,
cwd='{locker_dir}'
)
if result.returncode != 0:
print("Error: Unable to load secrets", file=sys.stderr)
sys.exit(1)
# Parse environment variables
env = os.environ.copy()
for line in result.stdout.strip().split('\n'):
if '=' in line:
key, value = line.split('=', 1)
env[key] = value
# Execute target script with injected secrets
subprocess.run([sys.executable, '{script}'] + sys.argv[1:], env=env)
if __name__ == '__main__':
main()
"#,
locker_dir = locker_path.display(),
script = script_path
)
}
#[allow(dead_code)]
pub fn generate_env_reference(store: &SecretsStore, output_path: &PathBuf) -> Result<()> {
let mut content = String::from("# File generated by lazy-locker\n");
content.push_str("# Values are stored securely in the locker.\n");
content.push_str("# Use 'lazy-locker run <command>' to execute with secrets.\n\n");
for secret in store.list_secrets() {
let expiration = secret.expiration_display();
content.push_str(&format!("# {} - {}\n", secret.name, expiration));
content.push_str(&format!(
"{}=${{LAZY_LOCKER:{}}}\n\n",
secret.name, secret.name
));
}
std::fs::write(output_path, content)?;
Ok(())
}
#[allow(dead_code)]
pub fn export_env_format(store: &SecretsStore, key: &[u8]) -> Result<String> {
let secrets = store.decrypt_all(key)?;
let mut output = String::new();
for (name, mut value) in secrets {
let escaped_value = value.replace('\\', "\\\\").replace('"', "\\\"");
output.push_str(&format!("{}=\"{}\"\n", name, escaped_value));
value.zeroize();
}
Ok(output)
}
pub fn copy_to_clipboard(value: &str) -> Result<()> {
#[cfg(target_os = "linux")]
{
let result = Command::new("xclip")
.args(["-selection", "clipboard"])
.stdin(Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(value.as_bytes())?;
}
child.wait()
});
if result.is_ok() {
return Ok(());
}
let result = Command::new("xsel")
.args(["--clipboard", "--input"])
.stdin(Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(value.as_bytes())?;
}
child.wait()
});
if result.is_ok() {
return Ok(());
}
let result = Command::new("wl-copy")
.stdin(Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(value.as_bytes())?;
}
child.wait()
});
result
.map_err(|_| anyhow::anyhow!("No clipboard tool available (xclip, xsel, wl-copy)"))?;
}
#[cfg(target_os = "macos")]
{
Command::new("pbcopy")
.stdin(Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(value.as_bytes())?;
}
child.wait()
})
.map_err(|e| anyhow::anyhow!("pbcopy error: {}", e))?;
}
#[cfg(target_os = "windows")]
{
Command::new("clip")
.stdin(Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(value.as_bytes())?;
}
child.wait()
})
.map_err(|e| anyhow::anyhow!("clip error: {}", e))?;
}
Ok(())
}
const SHELL_MARKER_START: &str = "# >>> lazy-locker exports >>>";
const SHELL_MARKER_END: &str = "# <<< lazy-locker exports <<<";
pub fn generate_env_file(
store: &SecretsStore,
key: &[u8],
output_path: &std::path::PathBuf,
) -> Result<()> {
let secrets = store.decrypt_all(key)?;
let mut content = String::from("# Generated by lazy-locker\n");
content.push_str("# WARNING: This file contains secrets in plain text!\n");
content.push_str("# Do not commit this file to version control.\n\n");
for (name, mut value) in secrets {
let escaped_value = value.replace('\\', "\\\\").replace('"', "\\\"");
content.push_str(&format!("{}=\"{}\"\n", name, escaped_value));
value.zeroize();
}
std::fs::write(output_path, content)?;
Ok(())
}
pub fn export_to_shell_profile(
store: &SecretsStore,
key: &[u8],
shell: &str,
) -> Result<std::path::PathBuf> {
let home =
std::env::var("HOME").map_err(|_| anyhow::anyhow!("HOME environment variable not set"))?;
let profile_path = match shell {
"bash" => std::path::PathBuf::from(&home).join(".bashrc"),
"zsh" => std::path::PathBuf::from(&home).join(".zshrc"),
"fish" => std::path::PathBuf::from(&home).join(".config/fish/config.fish"),
_ => return Err(anyhow::anyhow!("Unsupported shell: {}", shell)),
};
let secrets = store.decrypt_all(key)?;
let mut exports = String::new();
exports.push_str(&format!("\n{}\n", SHELL_MARKER_START));
exports.push_str("# WARNING: Secrets in plain text - generated by lazy-locker\n");
for (name, mut value) in secrets {
let escaped_value = value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('$', "\\$");
if shell == "fish" {
exports.push_str(&format!("set -gx {} \"{}\"\n", name, escaped_value));
} else {
exports.push_str(&format!("export {}=\"{}\"\n", name, escaped_value));
}
value.zeroize();
}
exports.push_str(&format!("{}\n", SHELL_MARKER_END));
let existing = std::fs::read_to_string(&profile_path).unwrap_or_default();
let cleaned = remove_shell_exports_from_content(&existing);
let new_content = format!("{}{}", cleaned.trim_end(), exports);
if let Some(parent) = profile_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&profile_path, new_content)?;
Ok(profile_path)
}
fn remove_shell_exports_from_content(content: &str) -> String {
let mut result = String::new();
let mut in_marker = false;
for line in content.lines() {
if line.trim() == SHELL_MARKER_START {
in_marker = true;
continue;
}
if line.trim() == SHELL_MARKER_END {
in_marker = false;
continue;
}
if !in_marker {
result.push_str(line);
result.push('\n');
}
}
result
}
pub fn clear_shell_exports() -> Result<Vec<std::path::PathBuf>> {
let home =
std::env::var("HOME").map_err(|_| anyhow::anyhow!("HOME environment variable not set"))?;
let profiles = [
std::path::PathBuf::from(&home).join(".bashrc"),
std::path::PathBuf::from(&home).join(".zshrc"),
std::path::PathBuf::from(&home).join(".config/fish/config.fish"),
];
let mut cleared = Vec::new();
for profile_path in profiles {
if profile_path.exists() {
let content = std::fs::read_to_string(&profile_path)?;
if content.contains(SHELL_MARKER_START) {
let cleaned = remove_shell_exports_from_content(&content);
std::fs::write(&profile_path, cleaned)?;
cleared.push(profile_path);
}
}
}
Ok(cleared)
}
pub fn export_to_json(
store: &SecretsStore,
key: &[u8],
output_path: &std::path::PathBuf,
) -> Result<()> {
let secrets = store.decrypt_all(key)?;
let json = serde_json::to_string_pretty(&secrets)?;
std::fs::write(output_path, json)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_generate_env_reference_creates_file() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let key = [0x42u8; 32];
let mut store = crate::core::store::SecretsStore::new();
store
.add_secret(
"TEST_VAR".to_string(),
"value".to_string(),
None,
temp_dir.path(),
&key,
)
.expect("Failed to add secret");
let output_path = temp_dir.path().join(".env.encrypted");
generate_env_reference(&store, &output_path).expect("Failed to generate reference");
assert!(output_path.exists());
let content = fs::read_to_string(&output_path).unwrap();
assert!(content.contains("TEST_VAR"));
assert!(content.contains("LAZY_LOCKER:TEST_VAR"));
assert!(content.contains("# File generated by lazy-locker"));
}
#[test]
fn test_generate_env_reference_no_plaintext_values() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let key = [0x42u8; 32];
let mut store = crate::core::store::SecretsStore::new();
let secret_value = "super_secret_password_123";
store
.add_secret(
"PASSWORD".to_string(),
secret_value.to_string(),
None,
temp_dir.path(),
&key,
)
.expect("Failed to add secret");
let output_path = temp_dir.path().join(".env.ref");
generate_env_reference(&store, &output_path).expect("Failed to generate reference");
let content = fs::read_to_string(&output_path).unwrap();
assert!(!content.contains(secret_value));
assert!(content.contains("${LAZY_LOCKER:PASSWORD}"));
}
#[test]
fn test_generate_python_wrapper_structure() {
let locker_path = PathBuf::from("/home/user/.lazy-locker");
let wrapper = generate_python_wrapper("app.py", &locker_path);
assert!(wrapper.contains("#!/usr/bin/env python3"));
assert!(wrapper.contains("lazy-locker"));
assert!(wrapper.contains("app.py"));
assert!(wrapper.contains("/home/user/.lazy-locker"));
assert!(wrapper.contains("def main()"));
}
}