ssh_vault/cli/actions/
mod.rs

1pub mod create;
2pub mod edit;
3pub mod fingerprint;
4pub mod view;
5
6use crate::tools;
7use anyhow::{Result, anyhow};
8use secrecy::{ExposeSecret, SecretString};
9use std::{
10    env,
11    io::{Read, Seek, SeekFrom, Write},
12    process::Command,
13};
14use tempfile::Builder;
15
16#[derive(Debug)]
17pub enum Action {
18    Fingerprint {
19        key: Option<String>,
20        user: Option<String>,
21    },
22    Create {
23        fingerprint: Option<String>,
24        input: Option<String>,
25        json: bool,
26        key: Option<String>,
27        user: Option<String>,
28        vault: Option<String>,
29    },
30    View {
31        key: Option<String>,
32        output: Option<String>,
33        passphrase: Option<SecretString>,
34        vault: Option<String>,
35    },
36    Edit {
37        key: Option<String>,
38        passphrase: Option<SecretString>,
39        vault: String,
40    },
41    Help,
42}
43
44pub fn process_input(buf: &mut Vec<u8>, data: Option<SecretString>) -> Result<usize> {
45    let mut tmpfile = Builder::new()
46        .prefix(".vault-")
47        .suffix(".ssh")
48        .tempfile_in(tools::get_home()?)?;
49
50    if let Some(data) = data {
51        write!(tmpfile, "{}", data.expose_secret())?;
52    }
53
54    let editor = env::var("EDITOR").unwrap_or_else(|_| String::from("vi"));
55
56    let editor_parts = shell_words::split(&editor)?;
57
58    let status = Command::new(&editor_parts[0])
59        .args(&editor_parts[1..])
60        .arg(tmpfile.path())
61        .status()?;
62
63    if !status.success() {
64        return Err(anyhow!("Editor exited with non-zero status code"));
65    }
66
67    // Seek to start
68    tmpfile.seek(SeekFrom::Start(0))?;
69
70    // read the file
71    tmpfile.read_to_end(buf)?;
72
73    // Fill the file with zeros
74    let zeros = vec![0u8; buf.len()];
75    tmpfile.write_all(&zeros)?;
76
77    Ok(buf.len())
78}
79
80#[cfg(test)]
81mod tests {
82    use crate::cli::actions::{Action, create, edit, fingerprint, view};
83    use serde_json::Value;
84    use std::io::Write;
85    use tempfile::NamedTempFile;
86
87    struct Test {
88        input: &'static str,
89        public_key: &'static str,
90        private_key: &'static str,
91        header: &'static str,
92    }
93
94    #[test]
95    fn test_create_view_edit_with_input() {
96        let tests = [
97            Test {
98                input: "Machs na",
99                public_key: "test_data/ed25519.pub",
100                private_key: "test_data/ed25519",
101                header: "SSH-VAULT;CHACHA20-POLY1305",
102            },
103            Test {
104                input: "Machs na",
105                public_key: "test_data/id_rsa.pub",
106                private_key: "test_data/id_rsa",
107                header: "SSH-VAULT;AES256",
108            },
109            Test {
110                input: "Arrachera is a Mexican dish made from marinated and grilled skirt steak. The steak is seasoned with a mixture of spices and marinades, giving it a rich and savory flavor. Commonly served in tacos or fajitas, arrachera is known for its tenderness and versatility in Mexican cuisine",
111                public_key: "test_data/ed25519.pub",
112                private_key: "test_data/ed25519",
113                header: "SSH-VAULT;CHACHA20-POLY1305",
114            },
115        ];
116
117        for test in tests.iter() {
118            let input = test.input;
119            let mut temp_file = NamedTempFile::new().unwrap();
120            temp_file.write_all(input.as_bytes()).unwrap();
121            let vault_file = NamedTempFile::new().unwrap();
122
123            let create = Action::Create {
124                fingerprint: None,
125                key: Some(test.public_key.to_string()),
126                user: None,
127                vault: Some(vault_file.path().to_str().unwrap().to_string()),
128                json: false,
129                input: Some(temp_file.path().to_str().unwrap().to_string()),
130            };
131            let vault = create::handle(create);
132            assert!(vault.is_ok());
133
134            let vault_contents = std::fs::read_to_string(&vault_file).unwrap();
135            assert!(vault_contents.starts_with(test.header));
136
137            let output = NamedTempFile::new().unwrap();
138            let view = Action::View {
139                key: Some(test.private_key.to_string()),
140                output: Some(output.path().to_str().unwrap().to_string()),
141                passphrase: None,
142                vault: Some(vault_file.path().to_str().unwrap().to_string()),
143            };
144            let vault_view = view::handle(view);
145            assert!(vault_view.is_ok());
146
147            let output = std::fs::read_to_string(output).unwrap();
148            assert_eq!(input, output);
149
150            let edit = Action::Edit {
151                key: Some(test.private_key.to_string()),
152                passphrase: None,
153                vault: vault_file.path().to_str().unwrap().to_string(),
154            };
155
156            // set EDITOR to cat instead of vi
157            temp_env::with_vars([("EDITOR", Some("cat"))], || {
158                let vault_edit = edit::handle(edit);
159                assert!(vault_edit.is_ok());
160            });
161
162            let vault_contents_after_edit = std::fs::read_to_string(&vault_file).unwrap();
163            assert_ne!(vault_contents, vault_contents_after_edit);
164
165            // check if we can still view the vault
166            let output = NamedTempFile::new().unwrap();
167            let view = Action::View {
168                key: Some(test.private_key.to_string()),
169                output: Some(output.path().to_str().unwrap().to_string()),
170                passphrase: None,
171                vault: Some(vault_file.path().to_str().unwrap().to_string()),
172            };
173            let vault_view = view::handle(view);
174            assert!(vault_view.is_ok());
175
176            let output = std::fs::read_to_string(output).unwrap();
177            assert_eq!(input, output);
178
179            // try to create again with the same vault (should fail)
180            let create = Action::Create {
181                fingerprint: None,
182                key: Some(test.public_key.to_string()),
183                user: None,
184                vault: Some(vault_file.path().to_str().unwrap().to_string()),
185                json: false,
186                input: Some(temp_file.path().to_str().unwrap().to_string()),
187            };
188            let vault = create::handle(create);
189            assert!(vault.is_err());
190        }
191    }
192
193    #[test]
194    fn test_create_with_json() {
195        let tests = [
196            Test {
197                input: "Three may keep a secret, if two of them are dead",
198                public_key: "test_data/ed25519.pub",
199                private_key: "test_data/ed25519",
200                header: "SSH-VAULT;CHACHA20-POLY1305",
201            },
202            Test {
203                input: "Hello World!",
204                public_key: "test_data/ed25519.pub",
205                private_key: "test_data/ed25519",
206                header: "SSH-VAULT;CHACHA20-POLY1305",
207            },
208        ];
209
210        for test in tests.iter() {
211            let input = test.input;
212            let mut temp_file = NamedTempFile::new().unwrap();
213            temp_file.write_all(input.as_bytes()).unwrap();
214            let vault_json = NamedTempFile::new().unwrap();
215
216            let create = Action::Create {
217                fingerprint: None,
218                key: Some(test.public_key.to_string()),
219                user: None,
220                vault: Some(vault_json.path().to_str().unwrap().to_string()),
221                json: true,
222                input: Some(temp_file.path().to_str().unwrap().to_string()),
223            };
224            let vault = create::handle(create);
225            assert!(vault.is_ok());
226
227            let vault_contents = std::fs::read_to_string(&vault_json).unwrap();
228            let json: Value = serde_json::from_str(&vault_contents).unwrap();
229            let vault = json["vault"].as_str().unwrap();
230
231            let mut vault_file = NamedTempFile::new().unwrap();
232            vault_file.write_all(vault.as_bytes()).unwrap();
233            let output = NamedTempFile::new().unwrap();
234
235            let view = Action::View {
236                key: Some(test.private_key.to_string()),
237                output: Some(output.path().to_str().unwrap().to_string()),
238                passphrase: None,
239                vault: Some(vault_file.path().to_str().unwrap().to_string()),
240            };
241            let vault_view = view::handle(view);
242            assert!(vault_view.is_ok());
243
244            let output = std::fs::read_to_string(output).unwrap();
245            assert_eq!(input, output);
246        }
247    }
248
249    #[test]
250    fn test_fingerprint() {
251        let fingerprint = Action::Fingerprint {
252            key: Some("test_data/ed25519.pub".to_string()),
253            user: None,
254        };
255
256        let fingerprint = fingerprint::handle(fingerprint);
257        assert!(fingerprint.is_ok());
258    }
259}