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> {
51 let mut tmpfile = Builder::new()
52 .prefix(".vault-")
53 .suffix(".ssh")
54 .tempfile_in(tools::get_home()?)?;
55
56 if let Some(data) = data {
57 write!(tmpfile, "{}", data.expose_secret())?;
58 }
59
60 let editor = env::var("EDITOR").unwrap_or_else(|_| String::from("vi"));
61
62 let editor_parts = shell_words::split(&editor)?;
63 let command = editor_parts
64 .first()
65 .ok_or_else(|| anyhow!("EDITOR command is empty"))?;
66
67 let status = Command::new(command)
68 .args(editor_parts.get(1..).unwrap_or(&[]))
69 .arg(tmpfile.path())
70 .status()?;
71
72 if !status.success() {
73 return Err(anyhow!("Editor exited with non-zero status code"));
74 }
75
76 tmpfile.seek(SeekFrom::Start(0))?;
78
79 tmpfile.read_to_end(buf)?;
81
82 let zeros = vec![0u8; buf.len()];
84 tmpfile.write_all(&zeros)?;
85
86 Ok(buf.len())
87}
88
89#[cfg(test)]
90#[allow(clippy::unwrap_used)]
91mod tests {
92 use crate::cli::actions::{Action, create, edit, fingerprint, view};
93 use serde_json::Value;
94 use std::io::Write;
95 use tempfile::NamedTempFile;
96
97 struct Test {
98 input: &'static str,
99 public_key: &'static str,
100 private_key: &'static str,
101 header: &'static str,
102 }
103
104 #[test]
105 fn test_create_view_edit_with_input() {
106 let tests = [
107 Test {
108 input: "Machs na",
109 public_key: "test_data/ed25519.pub",
110 private_key: "test_data/ed25519",
111 header: "SSH-VAULT;CHACHA20-POLY1305",
112 },
113 Test {
114 input: "Machs na",
115 public_key: "test_data/id_rsa.pub",
116 private_key: "test_data/id_rsa",
117 header: "SSH-VAULT;AES256",
118 },
119 Test {
120 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",
121 public_key: "test_data/ed25519.pub",
122 private_key: "test_data/ed25519",
123 header: "SSH-VAULT;CHACHA20-POLY1305",
124 },
125 ];
126
127 for test in &tests {
128 let input = test.input;
129 let mut temp_file = NamedTempFile::new().unwrap();
130 temp_file.write_all(input.as_bytes()).unwrap();
131 let vault_file = NamedTempFile::new().unwrap();
132
133 let create = Action::Create {
134 fingerprint: None,
135 key: Some(test.public_key.to_string()),
136 user: None,
137 vault: Some(vault_file.path().to_str().unwrap().to_string()),
138 json: false,
139 input: Some(temp_file.path().to_str().unwrap().to_string()),
140 };
141 let vault = create::handle(create);
142 assert!(vault.is_ok());
143
144 let vault_contents = std::fs::read_to_string(&vault_file).unwrap();
145 assert!(vault_contents.starts_with(test.header));
146
147 let output = NamedTempFile::new().unwrap();
148 let view = Action::View {
149 key: Some(test.private_key.to_string()),
150 output: Some(output.path().to_str().unwrap().to_string()),
151 passphrase: None,
152 vault: Some(vault_file.path().to_str().unwrap().to_string()),
153 };
154 let vault_view = view::handle(view);
155 assert!(vault_view.is_ok());
156
157 let output = std::fs::read_to_string(output).unwrap();
158 assert_eq!(input, output);
159
160 let edit = Action::Edit {
161 key: Some(test.private_key.to_string()),
162 passphrase: None,
163 vault: vault_file.path().to_str().unwrap().to_string(),
164 };
165
166 temp_env::with_vars([("EDITOR", Some("cat"))], || {
168 let vault_edit = edit::handle(edit);
169 assert!(vault_edit.is_ok());
170 });
171
172 let vault_contents_after_edit = std::fs::read_to_string(&vault_file).unwrap();
173 assert_ne!(vault_contents, vault_contents_after_edit);
174
175 let output = NamedTempFile::new().unwrap();
177 let view = Action::View {
178 key: Some(test.private_key.to_string()),
179 output: Some(output.path().to_str().unwrap().to_string()),
180 passphrase: None,
181 vault: Some(vault_file.path().to_str().unwrap().to_string()),
182 };
183 let vault_view = view::handle(view);
184 assert!(vault_view.is_ok());
185
186 let output = std::fs::read_to_string(output).unwrap();
187 assert_eq!(input, output);
188
189 let create = Action::Create {
191 fingerprint: None,
192 key: Some(test.public_key.to_string()),
193 user: None,
194 vault: Some(vault_file.path().to_str().unwrap().to_string()),
195 json: false,
196 input: Some(temp_file.path().to_str().unwrap().to_string()),
197 };
198 let vault = create::handle(create);
199 assert!(vault.is_err());
200 }
201 }
202
203 #[test]
204 fn test_create_with_json() -> Result<(), Box<dyn std::error::Error>> {
205 let tests = [
206 Test {
207 input: "Three may keep a secret, if two of them are dead",
208 public_key: "test_data/ed25519.pub",
209 private_key: "test_data/ed25519",
210 header: "SSH-VAULT;CHACHA20-POLY1305",
211 },
212 Test {
213 input: "Hello World!",
214 public_key: "test_data/ed25519.pub",
215 private_key: "test_data/ed25519",
216 header: "SSH-VAULT;CHACHA20-POLY1305",
217 },
218 ];
219
220 for test in &tests {
221 let input = test.input;
222 let mut temp_file = NamedTempFile::new().unwrap();
223 temp_file.write_all(input.as_bytes()).unwrap();
224 let vault_json = NamedTempFile::new().unwrap();
225
226 let create = Action::Create {
227 fingerprint: None,
228 key: Some(test.public_key.to_string()),
229 user: None,
230 vault: Some(vault_json.path().to_str().unwrap().to_string()),
231 json: true,
232 input: Some(temp_file.path().to_str().unwrap().to_string()),
233 };
234 let vault = create::handle(create);
235 assert!(vault.is_ok());
236
237 let vault_contents = std::fs::read_to_string(&vault_json).unwrap();
238 let json: Value = serde_json::from_str(&vault_contents).unwrap();
239 let vault_str = json
240 .get("vault")
241 .and_then(|v| v.as_str())
242 .ok_or("Failed to get vault from JSON")?;
243
244 let mut vault_file = NamedTempFile::new().unwrap();
245 vault_file.write_all(vault_str.as_bytes()).unwrap();
246 let output = NamedTempFile::new().unwrap();
247
248 let view = Action::View {
249 key: Some(test.private_key.to_string()),
250 output: Some(output.path().to_str().unwrap().to_string()),
251 passphrase: None,
252 vault: Some(vault_file.path().to_str().unwrap().to_string()),
253 };
254 let vault_view = view::handle(view);
255 assert!(vault_view.is_ok());
256
257 let output = std::fs::read_to_string(output).unwrap();
258 assert_eq!(input, output);
259 }
260 Ok(())
261 }
262
263 #[test]
264 fn test_fingerprint() {
265 let fingerprint = Action::Fingerprint {
266 key: Some("test_data/ed25519.pub".to_string()),
267 user: None,
268 };
269
270 let fingerprint = fingerprint::handle(fingerprint);
271 assert!(fingerprint.is_ok());
272 }
273}