ansible/
vault.rs

1//! Ansible Vault encryption and decryption operations.
2//!
3//! This module provides the [`AnsibleVault`] struct for managing encrypted files
4//! and strings using Ansible Vault, along with specialized error types.
5
6use crate::command_config::CommandConfig;
7use crate::errors::{AnsibleError, Result};
8use std::fmt::{Display, Formatter};
9use std::process;
10
11/// Ansible Vault encryption and decryption utility.
12///
13/// The `AnsibleVault` struct provides a comprehensive interface for managing
14/// encrypted files and strings using Ansible Vault. It supports all major
15/// vault operations including encryption, decryption, viewing, editing, and rekeying.
16///
17/// # Examples
18///
19/// ## Basic File Operations
20///
21/// ```rust,no_run
22/// use ansible::AnsibleVault;
23///
24/// let mut vault = AnsibleVault::new();
25/// vault.set_vault_password_file("vault_pass.txt");
26///
27/// // Encrypt a file
28/// vault.encrypt("secrets.yml")?;
29///
30/// // Decrypt a file
31/// vault.decrypt("secrets.yml")?;
32///
33/// // View encrypted content
34/// let content = vault.view("secrets.yml")?;
35/// println!("Content: {}", content);
36/// # Ok::<(), ansible::AnsibleError>(())
37/// ```
38///
39/// ## String Encryption
40///
41/// ```rust,no_run
42/// use ansible::AnsibleVault;
43///
44/// let mut vault = AnsibleVault::new();
45/// vault.set_vault_password_file("vault_pass.txt");
46///
47/// // Encrypt a string
48/// let encrypted = vault.encrypt_string("my_secret_password")?;
49/// println!("Encrypted: {}", encrypted);
50/// # Ok::<(), ansible::AnsibleError>(())
51/// ```
52///
53/// ## Multiple Vault IDs
54///
55/// ```rust,no_run
56/// use ansible::AnsibleVault;
57///
58/// let mut vault = AnsibleVault::new();
59/// vault.set_vault_id("prod@vault_pass.txt");
60///
61/// // Operations will use the specified vault ID
62/// vault.encrypt("production_secrets.yml")?;
63/// # Ok::<(), ansible::AnsibleError>(())
64/// ```
65///
66/// ## Rekeying Files
67///
68/// ```rust,no_run
69/// use ansible::AnsibleVault;
70///
71/// let mut vault = AnsibleVault::new();
72/// vault
73///     .set_vault_password_file("old_pass.txt")
74///     .set_new_vault_password_file("new_pass.txt");
75///
76/// // Change the encryption key
77/// vault.rekey("secrets.yml")?;
78/// # Ok::<(), ansible::AnsibleError>(())
79/// ```
80#[derive(Debug, Clone)]
81pub struct AnsibleVault {
82    pub(crate) command: String,
83    pub(crate) cfg: CommandConfig,
84    pub(crate) vault_id: Option<String>,
85    pub(crate) vault_password_file: Option<String>,
86}
87
88impl Default for AnsibleVault {
89    fn default() -> Self {
90        Self {
91            command: "ansible-vault".into(),
92            cfg: CommandConfig::default(),
93            vault_id: None,
94            vault_password_file: None,
95        }
96    }
97}
98
99impl Display for AnsibleVault {
100    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
101        write!(f, "{}", self.command)?;
102
103        if let Some(ref vault_id) = self.vault_id {
104            write!(f, " --vault-id {}", vault_id)?;
105        }
106
107        if let Some(ref password_file) = self.vault_password_file {
108            write!(f, " --vault-password-file {}", password_file)?;
109        }
110
111        if !self.cfg.args.is_empty() {
112            write!(f, " {}", self.cfg.args.join(" "))?;
113        }
114
115        Ok(())
116    }
117}
118
119impl AnsibleVault {
120    /// Create a new AnsibleVault instance
121    pub fn new() -> Self {
122        Self::default()
123    }
124
125    /// Set vault ID for encryption/decryption
126    pub fn set_vault_id(&mut self, vault_id: impl Into<String>) -> &mut Self {
127        self.vault_id = Some(vault_id.into());
128        self
129    }
130
131    /// Set vault password file
132    pub fn set_vault_password_file(&mut self, file_path: impl Into<String>) -> &mut Self {
133        self.vault_password_file = Some(file_path.into());
134        self
135    }
136
137    /// Set new vault password file for rekeying operations
138    pub fn set_new_vault_password_file(&mut self, file_path: impl Into<String>) -> &mut Self {
139        self.arg("--new-vault-password-file").arg(file_path.into());
140        self
141    }
142
143    /// Add a custom argument
144    pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
145        self.cfg.arg(arg.into());
146        self
147    }
148
149    /// Add multiple arguments
150    pub fn args<I, S>(&mut self, args: I) -> &mut Self
151    where
152        I: IntoIterator<Item = S>,
153        S: Into<String>,
154    {
155        let args_vec: Vec<String> = args.into_iter().map(|s| s.into()).collect();
156        self.cfg.args(args_vec);
157        self
158    }
159
160    /// Set environment variables from the system
161    pub fn set_system_envs(&mut self) -> &mut Self {
162        self.cfg.set_system_envs();
163        self
164    }
165
166    /// Add an environment variable
167    pub fn add_env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
168        self.cfg.add_env(key, value);
169        self
170    }
171
172    /// Execute a vault command with the given action and arguments
173    fn execute_vault_command(&self, action: &str, args: &[String]) -> Result<String> {
174        let mut cmd = process::Command::new(&self.command);
175        cmd.envs(&self.cfg.envs);
176        cmd.arg(action);
177
178        // Add vault-specific options
179        if let Some(ref vault_id) = self.vault_id {
180            cmd.args(["--vault-id", vault_id]);
181        }
182
183        if let Some(ref password_file) = self.vault_password_file {
184            cmd.args(["--vault-password-file", password_file]);
185        }
186
187        // Add custom arguments
188        cmd.args(&self.cfg.args);
189
190        // Add action-specific arguments
191        cmd.args(args);
192
193        let output = cmd.output()?;
194
195        if !output.status.success() {
196            let stdout = String::from_utf8_lossy(&output.stdout).to_string();
197            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
198            return Err(AnsibleError::command_failed(
199                format!("Ansible vault {} command failed", action),
200                output.status.code(),
201                Some(stdout),
202                Some(stderr),
203            ));
204        }
205
206        let result = [output.stdout, "\n".as_bytes().to_vec(), output.stderr].concat();
207        let s = String::from_utf8_lossy(&result);
208
209        Ok(s.to_string())
210    }
211
212    /// Create and encrypt a new file
213    pub fn create(&self, file_path: impl Into<String>) -> Result<String> {
214        let file_path = file_path.into();
215        self.execute_vault_command("create", &[file_path])
216    }
217
218    /// Encrypt an existing file
219    pub fn encrypt(&self, file_path: impl Into<String>) -> Result<String> {
220        let file_path = file_path.into();
221        self.execute_vault_command("encrypt", &[file_path])
222    }
223
224    /// Decrypt a file
225    pub fn decrypt(&self, file_path: impl Into<String>) -> Result<String> {
226        let file_path = file_path.into();
227        self.execute_vault_command("decrypt", &[file_path])
228    }
229
230    /// Decrypt and view a file without modifying it
231    pub fn view(&self, file_path: impl Into<String>) -> Result<String> {
232        let file_path = file_path.into();
233        self.execute_vault_command("view", &[file_path])
234    }
235
236    /// Edit an encrypted file
237    pub fn edit(&self, file_path: impl Into<String>) -> Result<String> {
238        let file_path = file_path.into();
239        self.execute_vault_command("edit", &[file_path])
240    }
241
242    /// Re-encrypt a file with a new password
243    pub fn rekey(&self, file_path: impl Into<String>) -> Result<String> {
244        let file_path = file_path.into();
245        self.execute_vault_command("rekey", &[file_path])
246    }
247
248    /// Encrypt a string and output it in a format suitable for inclusion in YAML
249    pub fn encrypt_string(&self, string_to_encrypt: impl Into<String>) -> Result<String> {
250        let string_to_encrypt = string_to_encrypt.into();
251        self.execute_vault_command("encrypt_string", &[string_to_encrypt])
252    }
253
254    /// Encrypt a string with a variable name
255    pub fn encrypt_string_with_name(
256        &self,
257        string_to_encrypt: impl Into<String>,
258        var_name: impl Into<String>,
259    ) -> Result<String> {
260        let string_to_encrypt = string_to_encrypt.into();
261        let var_name = var_name.into();
262        self.execute_vault_command("encrypt_string", &[
263            "--name".to_string(),
264            var_name,
265            string_to_encrypt,
266        ])
267    }
268
269    /// Encrypt a string and prompt for input
270    pub fn encrypt_string_prompt(&self) -> Result<String> {
271        self.execute_vault_command("encrypt_string", &["--prompt".to_string()])
272    }
273
274    /// Encrypt a string with stdin input
275    pub fn encrypt_string_stdin(&self, stdin_name: impl Into<String>) -> Result<String> {
276        let stdin_name = stdin_name.into();
277        self.execute_vault_command("encrypt_string", &[
278            "--stdin-name".to_string(),
279            stdin_name,
280        ])
281    }
282
283    /// Decrypt a file to a specific output location
284    pub fn decrypt_to_file(
285        &self,
286        input_file: impl Into<String>,
287        output_file: impl Into<String>,
288    ) -> Result<String> {
289        let input_file = input_file.into();
290        let output_file = output_file.into();
291        self.execute_vault_command("decrypt", &[
292            "--output".to_string(),
293            output_file,
294            input_file,
295        ])
296    }
297
298    /// Encrypt a file to a specific output location
299    pub fn encrypt_to_file(
300        &self,
301        input_file: impl Into<String>,
302        output_file: impl Into<String>,
303    ) -> Result<String> {
304        let input_file = input_file.into();
305        let output_file = output_file.into();
306        self.execute_vault_command("encrypt", &[
307            "--output".to_string(),
308            output_file,
309            input_file,
310        ])
311    }
312
313    /// Set the vault ID used for encryption (when multiple vault IDs are available)
314    pub fn set_encrypt_vault_id(&mut self, vault_id: impl Into<String>) -> &mut Self {
315        self.cfg.arg("--encrypt-vault-id");
316        self.cfg.arg(vault_id.into());
317        self
318    }
319
320    /// Ask for vault password interactively
321    pub fn ask_vault_password(&mut self) -> &mut Self {
322        self.cfg.arg("--ask-vault-password");
323        self
324    }
325
326    /// Enable verbose output
327    pub fn verbose(&mut self) -> &mut Self {
328        self.cfg.arg("-v");
329        self
330    }
331
332    /// Set multiple levels of verbosity
333    pub fn verbosity(&mut self, level: u8) -> &mut Self {
334        let v_arg = "-".to_string() + &"v".repeat(level as usize);
335        self.cfg.arg(v_arg);
336        self
337    }
338
339    /// Get a reference to the command configuration (for testing)
340    pub fn get_config(&self) -> &CommandConfig {
341        &self.cfg
342    }
343}
344
345/// Specialized error types for Ansible Vault operations.
346///
347/// These errors provide specific context for vault-related failures,
348/// making it easier to handle different types of vault errors appropriately.
349///
350/// # Examples
351///
352/// ```rust
353/// use ansible::{VaultError, AnsibleError};
354///
355/// // Handle specific vault errors
356/// let vault_result: Result<(), AnsibleError> = Ok(());
357/// match vault_result {
358///     Err(AnsibleError::ConfigError(message)) if message.contains("password") => {
359///         eprintln!("Please provide a vault password");
360///     }
361///     Err(AnsibleError::CommandFailed { message, .. }) if message.contains("decrypt") => {
362///         eprintln!("Failed to decrypt - check your password");
363///     }
364///     _ => {}
365/// }
366/// ```
367#[derive(Debug, Clone)]
368pub enum VaultError {
369    /// Vault password was not provided when required
370    ///
371    /// This error occurs when attempting vault operations without
372    /// specifying a password file, vault ID, or interactive password.
373    NoPassword,
374
375    /// The vault file has an invalid or corrupted format
376    ///
377    /// This error occurs when the encrypted file doesn't match
378    /// the expected Ansible Vault format.
379    InvalidFormat,
380
381    /// The specified vault ID was not found
382    ///
383    /// This error occurs when using multiple vault IDs and the
384    /// specified ID is not available or configured.
385    VaultIdNotFound,
386
387    /// Decryption operation failed
388    ///
389    /// This error occurs when the vault password is incorrect
390    /// or the encrypted data is corrupted.
391    DecryptionFailed,
392
393    /// Encryption operation failed
394    ///
395    /// This error occurs when the encryption process fails,
396    /// possibly due to file permissions or disk space issues.
397    EncryptionFailed,
398}
399
400impl std::fmt::Display for VaultError {
401    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402        match self {
403            VaultError::NoPassword => write!(f, "Vault password not provided"),
404            VaultError::InvalidFormat => write!(f, "Invalid vault file format"),
405            VaultError::VaultIdNotFound => write!(f, "Vault ID not found"),
406            VaultError::DecryptionFailed => write!(f, "Decryption failed"),
407            VaultError::EncryptionFailed => write!(f, "Encryption failed"),
408        }
409    }
410}
411
412impl std::error::Error for VaultError {}