Skip to main content

envvault/cli/commands/
get.rs

1//! `envvault get` — retrieve and print a single secret's value.
2
3use crate::cli::{load_keyfile, prompt_password_for_vault, vault_path, Cli};
4use crate::errors::{EnvVaultError, Result};
5use crate::vault::VaultStore;
6
7/// Execute the `get` command.
8pub fn execute(cli: &Cli, key: &str, clipboard: bool) -> Result<()> {
9    let path = vault_path(cli)?;
10    let keyfile = load_keyfile(cli)?;
11
12    // Open the vault (requires password).
13    let vault_id = path.to_string_lossy();
14    let password = prompt_password_for_vault(Some(&vault_id))?;
15    let store = match VaultStore::open(&path, password.as_bytes(), keyfile.as_deref()) {
16        Ok(store) => store,
17        Err(e) => {
18            #[cfg(feature = "audit-log")]
19            crate::audit::log_auth_failure(cli, &e.to_string());
20            return Err(e);
21        }
22    };
23
24    // Decrypt the secret value.
25    let value = store.get_secret(key)?;
26
27    if clipboard {
28        copy_to_clipboard(&value)?;
29        crate::cli::output::success(&format!("Copied '{key}' to clipboard (clears in 30s)"));
30
31        // Spawn a background process to clear the clipboard after 30 seconds.
32        spawn_clipboard_clear();
33    } else {
34        println!("{value}");
35    }
36
37    #[cfg(feature = "audit-log")]
38    crate::audit::log_read_audit(cli, "get", Some(key), None);
39
40    Ok(())
41}
42
43/// Copy a value to the system clipboard using arboard.
44fn copy_to_clipboard(value: &str) -> Result<()> {
45    let mut clip = arboard::Clipboard::new()
46        .map_err(|e| EnvVaultError::ClipboardError(format!("failed to access clipboard: {e}")))?;
47    clip.set_text(value)
48        .map_err(|e| EnvVaultError::ClipboardError(format!("failed to copy to clipboard: {e}")))?;
49    Ok(())
50}
51
52/// Spawn a detached background process to clear the clipboard after 30 seconds.
53///
54/// Best-effort: if it fails, we just warn — the secret was already copied.
55#[cfg(unix)]
56fn spawn_clipboard_clear() {
57    use std::process::{Command, Stdio};
58
59    // Try xclip first, fall back to xsel, then pbcopy (macOS).
60    let clear_cmd = "sleep 30 && \
61        (printf '' | xclip -selection clipboard 2>/dev/null || \
62         xsel --clipboard --delete 2>/dev/null || \
63         printf '' | pbcopy 2>/dev/null || true)";
64
65    let result = Command::new("sh")
66        .args(["-c", clear_cmd])
67        .stdin(Stdio::null())
68        .stdout(Stdio::null())
69        .stderr(Stdio::null())
70        .spawn();
71
72    if result.is_err() {
73        crate::cli::output::warning("Could not schedule clipboard auto-clear");
74    }
75}
76
77#[cfg(not(unix))]
78fn spawn_clipboard_clear() {
79    crate::cli::output::warning(
80        "Clipboard auto-clear is not supported on this platform — clear manually",
81    );
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn clipboard_copy_returns_error_on_invalid_clipboard() {
90        // In a headless CI environment, clipboard access may fail.
91        // This tests that our error wrapping works correctly.
92        let result = copy_to_clipboard("test-value");
93        // We can't assert success because CI may not have a display server,
94        // but we CAN assert that any error is correctly wrapped.
95        if let Err(e) = result {
96            let msg = e.to_string();
97            assert!(msg.contains("clipboard") || msg.contains("Clipboard"));
98        }
99    }
100}