lazy-locker 0.0.1

A secure local secrets manager with TUI interface and SDK support
//! Secure process execution module with secret injection.
//!
//! This module provides a secure wrapper for executing scripts
//! with decrypted tokens injected in memory, without ever writing
//! plain text values to disk.

use std::path::PathBuf;
use std::process::{Command, Stdio};
use anyhow::Result;
use zeroize::Zeroize;

use crate::core::store::SecretsStore;

/// Result of a file search for files using a token
#[derive(Debug, Clone)]
pub struct TokenUsage {
    pub file_path: String,
    pub line_number: usize,
    pub line_content: String,
}

/// Searches for files in the current directory that reference a token
pub fn find_token_usages(token_name: &str, search_dir: &PathBuf) -> Vec<TokenUsage> {
    let mut usages = Vec::new();
    
    // Patterns to search for
    let patterns = vec![
        format!("${{{}}}", token_name),           // ${TOKEN_NAME}
        format!("${}",  token_name),              // $TOKEN_NAME
        format!("process.env.{}", token_name),    // process.env.TOKEN_NAME
        format!("os.environ[\"{}\"]", token_name), // os.environ["TOKEN_NAME"]
        format!("os.getenv(\"{}\")", token_name), // os.getenv("TOKEN_NAME")
        format!("ENV[\"{}\"]", token_name),       // ENV["TOKEN_NAME"]
        token_name.to_string(),                   // Direct reference
    ];
    
    if let Ok(entries) = walkdir(search_dir) {
        for entry in entries {
            if let Ok(content) = std::fs::read_to_string(&entry) {
                for (line_num, line) in content.lines().enumerate() {
                    for pattern in &patterns {
                        if line.contains(pattern) {
                            usages.push(TokenUsage {
                                file_path: entry.to_string_lossy().to_string(),
                                line_number: line_num + 1,
                                line_content: line.trim().to_string(),
                            });
                            break; // One occurrence per line only
                        }
                    }
                }
            }
        }
    }
    
    usages
}

/// Recursively walks a directory ignoring hidden folders and node_modules
fn walkdir(dir: &PathBuf) -> Result<Vec<PathBuf>> {
    let mut files = Vec::new();
    
    if !dir.is_dir() {
        return Ok(files);
    }
    
    for entry in std::fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        let name = path.file_name().unwrap_or_default().to_string_lossy();
        
        // Ignore hidden folders, node_modules, target, .git, etc.
        if name.starts_with('.') || name == "node_modules" || name == "target" || name == "__pycache__" {
            continue;
        }
        
        if path.is_dir() {
            files.extend(walkdir(&path)?);
        } else {
            // Filter by extension
            let ext = path.extension().unwrap_or_default().to_string_lossy();
            if matches!(ext.as_ref(), "py" | "js" | "ts" | "jsx" | "tsx" | "sh" | "bash" | "env" | "yaml" | "yml" | "json" | "toml" | "rb" | "go" | "rs") {
                files.push(path);
            }
        }
    }
    
    Ok(files)
}

/// Executes a command with secrets injected as environment variables.
/// Secrets are decrypted in memory and zeroized after execution.
pub fn execute_with_secrets(
    command: &str,
    store: &SecretsStore,
    key: &[u8],
) -> Result<std::process::Output> {
    // Decrypt all secrets in memory
    let mut env_vars = store.decrypt_all(key)?;
    
    // Execute the command with environment variables
    let output = Command::new("sh")
        .arg("-c")
        .arg(command)
        .envs(&env_vars)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()?;
    
    // Zeroize secrets after use
    for (_, mut value) in env_vars.drain() {
        value.zeroize();
    }
    
    Ok(output)
}

/// Generates a Python wrapper script that uses lazy-locker to inject secrets.
/// This wrapper calls lazy-locker in subprocess to decrypt on the fly.
pub fn generate_python_wrapper(script_path: &str, locker_path: &PathBuf) -> 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
    )
}

/// Generates a .env.encrypted file with token references.
/// This file can be versioned as it only contains names, not values.
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(())
}

/// Exports secrets in .env compatible format (for temporary use only).
/// WARNING: This function writes secrets in plain text. Use with caution.
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 {
        // Escape special characters
        let escaped_value = value.replace('\\', "\\\\").replace('"', "\\\"");
        output.push_str(&format!("{}=\"{}\"\n", name, escaped_value));
        value.zeroize();
    }
    
    Ok(output)
}

/// Copies a value to clipboard (cross-platform).
pub fn copy_to_clipboard(value: &str) -> Result<()> {
    #[cfg(target_os = "linux")]
    {
        // Try xclip then xsel
        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(());
        }
        
        // Fallback to xsel
        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(());
        }
        
        // Try wl-copy for Wayland
        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(())
}