Skip to main content

envvault/cli/commands/
auth.rs

1//! `envvault auth` — manage authentication methods (keyring, keyfile).
2//!
3//! Subcommands:
4//! - `envvault auth keyring`          — save password to OS keyring
5//! - `envvault auth keyring --delete` — remove password from keyring
6//! - `envvault auth keyfile-generate`  — generate a new random keyfile
7//!
8//! When the keyring feature is not compiled in, keyring commands return
9//! a helpful error message.
10
11use crate::cli::output;
12use crate::cli::Cli;
13#[cfg(not(feature = "keyring-store"))]
14use crate::errors::EnvVaultError;
15use crate::errors::Result;
16
17/// Execute `envvault auth keyring` — save or delete password in OS keyring.
18pub fn execute_keyring(cli: &Cli, delete: bool) -> Result<()> {
19    #[cfg(feature = "keyring-store")]
20    {
21        let path = crate::cli::vault_path(cli)?;
22        let vault_id = path.to_string_lossy().to_string();
23
24        if delete {
25            crate::keyring::delete_password(&vault_id)?;
26            output::success("Password removed from OS keyring.");
27        } else {
28            // Verify the password works before storing it.
29            // Don't use keyring lookup here — user is explicitly setting the password.
30            let keyfile = crate::cli::load_keyfile(cli)?;
31            let password = crate::cli::prompt_password_for_vault(None)?;
32            let _store =
33                crate::vault::VaultStore::open(&path, password.as_bytes(), keyfile.as_deref())?;
34
35            crate::keyring::store_password(&vault_id, &password)?;
36            output::success("Password saved to OS keyring. Future opens will be automatic.");
37        }
38
39        Ok(())
40    }
41
42    #[cfg(not(feature = "keyring-store"))]
43    {
44        let _ = (cli, delete);
45        Err(EnvVaultError::KeyringError(
46            "keyring support not compiled — rebuild with `cargo build --features keyring-store`"
47                .into(),
48        ))
49    }
50}
51
52/// Execute `envvault auth keyfile-generate` — create a new random keyfile.
53pub fn execute_keyfile_generate(cli: &Cli, keyfile_path: Option<&str>) -> Result<()> {
54    let cwd = std::env::current_dir()?;
55
56    let path = match keyfile_path {
57        Some(p) => std::path::PathBuf::from(p),
58        None => cwd.join(&cli.vault_dir).join("keyfile"),
59    };
60
61    crate::crypto::keyfile::generate_keyfile(&path)?;
62
63    let path_display = path.display();
64    output::success(&format!("Keyfile generated at {path_display}"));
65    output::warning("Keep this file secret! Anyone with it can help unlock your vault.");
66    output::tip("Add the keyfile path to .gitignore to prevent accidental commits.");
67
68    // Auto-patch .gitignore for the keyfile.
69    let relative = path.strip_prefix(&cwd).map_or_else(
70        |_| path.to_string_lossy().to_string(),
71        |p| p.to_string_lossy().to_string(),
72    );
73
74    crate::cli::gitignore::patch_gitignore(&cwd, &relative);
75
76    Ok(())
77}
78
79#[cfg(test)]
80mod tests {
81    use tempfile::TempDir;
82
83    #[test]
84    fn keyring_disabled_returns_error() {
85        // When compiled without keyring-store feature, execute_keyring should error.
86        // This test always passes because we compile tests without the feature.
87        #[cfg(not(feature = "keyring-store"))]
88        {
89            use clap::Parser;
90            let cli = crate::cli::Cli::parse_from(["envvault", "auth", "keyring"]);
91            let result = super::execute_keyring(&cli, false);
92            assert!(result.is_err());
93            let msg = result.unwrap_err().to_string();
94            assert!(
95                msg.contains("keyring support not compiled"),
96                "unexpected error: {msg}"
97            );
98        }
99    }
100
101    #[test]
102    fn keyfile_generate_creates_file() {
103        use clap::Parser;
104
105        let dir = TempDir::new().unwrap();
106        let kf_path = dir.path().join("my.keyfile");
107
108        let cli = crate::cli::Cli::parse_from([
109            "envvault",
110            "--vault-dir",
111            dir.path().to_str().unwrap(),
112            "auth",
113            "keyfile-generate",
114            kf_path.to_str().unwrap(),
115        ]);
116
117        super::execute_keyfile_generate(&cli, Some(kf_path.to_str().unwrap())).unwrap();
118
119        assert!(kf_path.exists(), "keyfile should be created");
120        let data = std::fs::read(&kf_path).unwrap();
121        assert_eq!(data.len(), 32, "keyfile should be 32 bytes");
122    }
123
124    #[test]
125    fn keyfile_generate_patches_gitignore() {
126        // Test the underlying functions directly to avoid set_current_dir(),
127        // which is process-global and races with parallel tests.
128        let dir = TempDir::new().unwrap();
129        let dir_path = dir.path().canonicalize().unwrap();
130        let kf_path = dir_path.join("vault.keyfile");
131
132        // Generate the keyfile.
133        crate::crypto::keyfile::generate_keyfile(&kf_path).unwrap();
134
135        // Patch .gitignore with the keyfile path (relative to project dir).
136        let relative = kf_path.strip_prefix(&dir_path).map_or_else(
137            |_| kf_path.to_string_lossy().to_string(),
138            |p| p.to_string_lossy().to_string(),
139        );
140        crate::cli::gitignore::patch_gitignore(&dir_path, &relative);
141
142        let gitignore = std::fs::read_to_string(dir_path.join(".gitignore")).unwrap_or_default();
143        assert!(
144            gitignore.contains("keyfile"),
145            "gitignore should contain keyfile entry: {gitignore}"
146        );
147    }
148}