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 tmpfile.seek(SeekFrom::Start(0))?;
69
70 tmpfile.read_to_end(buf)?;
72
73 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 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 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 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}