1use crate::CipherGeneration;
2use crate::MessageEncryption;
3use anyhow::{Context, anyhow};
4use std::env;
5use std::ffi::OsStr;
6use std::path::{Path, PathBuf};
7use std::process;
8use std::{fs, io};
9
10static EMPTY_AAD_STRING: &str = "";
11
12pub struct FileEncryption {
36 file_path: String,
37 key: String,
38}
39
40impl FileEncryption {
41 pub fn new(file_path: String, key: String) -> Self {
57 FileEncryption {
58 file_path: shellexpand::tilde(&file_path).to_string(),
59 key,
60 }
61 }
62
63 pub fn create(path: &str) -> anyhow::Result<()> {
77 let (filename, key_path, encrypted_file_path) = Self::output_info_for_create(path)?;
78
79 if !key_path.exists() && !encrypted_file_path.exists() {
80 let key = CipherGeneration::random_key();
81
82 fs::write(key_path, &key)?;
83
84 let template_string = "CHANGE ME";
85
86 let fc = FileEncryption::new(filename, key);
87 let encrypted_contents = fc.encrypt(template_string.as_bytes())?;
88
89 fs::write(encrypted_file_path, encrypted_contents)?;
90 } else {
91 return Err(anyhow!(
92 "It seems you may have already initialized this directory. Either master.key and/or credentials.yml.enc already exist."
93 ));
94 }
95
96 Ok(())
97 }
98
99 pub fn edit(&self) -> anyhow::Result<()> {
102 match self.decrypt() {
103 Ok(contents) => {
104 let temp_file_path = self.temp_file_location()?;
105
106 self.write_file(temp_file_path.clone(), contents.clone())?;
107
108 Self::launch_editor_for_path(&temp_file_path)?;
109
110 let old_file_contents = contents;
111 let temp_file_contents = fs::read_to_string(temp_file_path.clone())?;
112
113 if old_file_contents != temp_file_contents {
114 let encrypted_contents = self.encrypt(temp_file_contents.as_bytes())?;
115
116 self.write_file(temp_file_path, encrypted_contents)?;
117 self.replace_file_atomically()?;
118 } else {
119 fs::remove_file(temp_file_path)?;
120 }
121 }
122
123 Err(why) => {
124 panic!("Decryption failed: {}", why);
125 }
126 }
127
128 Ok(())
129 }
130
131 pub fn decrypt(&self) -> anyhow::Result<String> {
145 let contents = self.read_file()?;
146 let split_contents = MessageEncryption::split_encrypted_contents(&contents)?;
147 let message = split_contents[0];
148 let iv = split_contents[1];
149 let encrypted_aad = split_contents[2];
150
151 let decryptor =
152 MessageEncryption::new(message.as_bytes().to_vec(), &self.key, EMPTY_AAD_STRING);
153
154 match decryptor.decrypt(iv, encrypted_aad) {
155 Ok(decrypted_contents) => Ok(decrypted_contents),
156 Err(why) => Err(anyhow!("Invalid encrypted contents in decrypt: {}", why)),
157 }
158 }
159
160 pub fn encrypt(&self, contents: &[u8]) -> anyhow::Result<String> {
175 let encryptor = MessageEncryption::new(contents.to_vec(), &self.key, EMPTY_AAD_STRING);
176
177 match encryptor.encrypt() {
178 Ok(encrypted_contents) => Ok(encrypted_contents),
179 Err(why) => Err(anyhow!("{}", why)),
180 }
181 }
182
183 fn launch_editor_for_path(path: &Path) -> anyhow::Result<()> {
184 let mut editor = match std::env::var("EDITOR") {
185 Ok(editor) => editor,
186 Err(_) => String::from("vim"),
187 };
188
189 editor.push(' ');
190 editor.push_str(&path.to_string_lossy());
191
192 #[cfg(test)]
193 editor.insert_str(0, "vim(){ :; }; ");
194
195 std::process::Command::new("/usr/bin/env")
196 .arg("sh")
197 .arg("-c")
198 .arg(&editor)
199 .spawn()
200 .expect("Error: Failed to run editor")
201 .wait()
202 .expect("Error: Editor returned a non-zero status");
203
204 Ok(())
205 }
206
207 fn read_file(&self) -> anyhow::Result<String> {
208 let path = Path::new(&self.file_path);
209
210 let contents = fs::read_to_string(path)?;
211
212 Ok(contents)
213 }
214
215 fn write_file<T, U>(&self, path: T, contents: U) -> io::Result<()>
216 where
217 T: AsRef<Path>,
218 U: AsRef<[u8]>,
219 {
220 fs::write(path, contents)?;
221
222 Ok(())
223 }
224
225 fn replace_file_atomically(&self) -> anyhow::Result<()> {
226 let path = PathBuf::from(&self.file_path);
227 let temp_file_path = self.temp_file_location()?;
228
229 fs::rename(temp_file_path, path)?;
230
231 Ok(())
232 }
233
234 fn temp_file_location(&self) -> anyhow::Result<PathBuf> {
235 let mut temp_directory_path = env::temp_dir();
236 let original_filename = PathBuf::from(&self.file_path)
237 .file_name()
238 .context("Could not generate absolute path for encrypted file")?
239 .to_owned();
240
241 let final_path = format!("{}.{}", process::id(), original_filename.to_string_lossy());
242 let mut final_path = PathBuf::from(final_path);
243
244 if let Some(extension) = final_path.extension() {
245 if OsStr::new("enc") == extension {
246 final_path.set_extension("");
247 }
248 }
249
250 temp_directory_path.push(final_path);
251
252 Ok(temp_directory_path)
253 }
254
255 fn output_info_for_create(path: &str) -> anyhow::Result<(String, PathBuf, PathBuf)> {
256 let mut pathbuf = PathBuf::from(path);
257
258 let mut key_path;
259 let mut encrypted_file_path;
260
261 if pathbuf.is_dir() {
262 encrypted_file_path = pathbuf.clone();
263 encrypted_file_path.push("credentials.yml.enc");
264
265 pathbuf.push("master.key");
266 key_path = pathbuf;
267 } else {
268 key_path = pathbuf
269 .parent()
270 .context("Could not get parent directory for output")?
271 .to_path_buf();
272 encrypted_file_path = pathbuf;
273
274 key_path.push("master.key");
275 }
276
277 let filename = encrypted_file_path
278 .file_name()
279 .context("Could not get filename for output")?
280 .to_string_lossy()
281 .to_string();
282
283 Ok((filename, key_path, encrypted_file_path))
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use assert_fs::prelude::*;
291 use lazy_static::lazy_static;
292 use std::env::VarError;
293 use std::panic::{RefUnwindSafe, UnwindSafe};
294 use std::sync::Mutex;
295 use std::{env, panic};
296
297 lazy_static! {
298 static ref SERIAL_TEST: Mutex<()> = Default::default();
299 }
300
301 pub fn with_env_vars<F>(kvs: Vec<(&str, Option<&str>)>, closure: F)
304 where
305 F: Fn() + UnwindSafe + RefUnwindSafe,
306 {
307 let guard = SERIAL_TEST.lock().unwrap();
308 let mut old_kvs: Vec<(&str, Result<String, VarError>)> = Vec::new();
309
310 for (k, v) in kvs {
311 let old_v = env::var(k);
312 old_kvs.push((k, old_v));
313 match v {
314 None => unsafe { env::remove_var(k) },
315 Some(v) => unsafe { env::set_var(k, v) },
316 }
317 }
318
319 match panic::catch_unwind(|| {
320 closure();
321 }) {
322 Ok(_) => {
323 for (k, v) in old_kvs {
324 reset_env(k, v);
325 }
326 }
327 Err(err) => {
328 for (k, v) in old_kvs {
329 reset_env(k, v);
330 }
331 drop(guard);
332 panic::resume_unwind(err);
333 }
334 };
335 }
336
337 fn reset_env(k: &str, old: Result<String, VarError>) {
338 if let Ok(v) = old {
339 unsafe { env::set_var(k, v) };
340 } else {
341 unsafe { env::remove_var(k) };
342 }
343 }
344
345 #[test]
346 fn test_edit() {
347 with_env_vars(vec![("EDITOR", Some("echo"))], || {
348 let temp = assert_fs::TempDir::new().unwrap();
349 let input_file = temp.child("encoded.txt.enc");
350 temp
351 .copy_from("./tests/fixtures/", &["*.txt", "*.enc"])
352 .unwrap();
353
354 let file_encryption = FileEncryption::new(
355 input_file.to_string_lossy().to_string(),
356 String::from("200a0e90e538d17390c8c4bc3bc71e44"),
357 );
358
359 assert!(file_encryption.edit().is_ok());
360 });
361 }
362
363 #[test]
364 fn test_edit_no_editor_environment_variable() {
365 with_env_vars(vec![("EDITOR", None)], || {
366 let temp = assert_fs::TempDir::new().unwrap();
367 let input_file = temp.child("encoded.txt.enc");
368 temp
369 .copy_from("./tests/fixtures/", &["*.txt", "*.enc"])
370 .unwrap();
371
372 let file_encryption = FileEncryption::new(
373 input_file.to_string_lossy().to_string(),
374 String::from("200a0e90e538d17390c8c4bc3bc71e44"),
375 );
376
377 assert!(file_encryption.edit().is_ok());
378 });
379 }
380
381 #[test]
382 #[should_panic]
383 fn test_broken_encryption_edit() {
384 with_env_vars(vec![("EDITOR", Some("echo"))], || {
385 let temp = assert_fs::TempDir::new().unwrap();
386 let input_file = temp.child("no_encryption.txt");
387 temp.copy_from("./tests/fixtures/", &["*.txt"]).unwrap();
388
389 let file_encryption = FileEncryption::new(
390 input_file.to_string_lossy().to_string(),
391 String::from("200a0e90e538d17390c8c4bc3bc71e44"),
392 );
393
394 let _ = file_encryption.edit();
395 });
396 }
397
398 #[test]
399 fn test_broken_decryption_decrypt() {
400 with_env_vars(vec![("EDITOR", Some("echo"))], || {
401 let temp = assert_fs::TempDir::new().unwrap();
402 let input_file = temp.child("encoded.txt");
403 temp.copy_from("./tests/fixtures/", &["*.txt"]).unwrap();
404
405 let file_encryption = FileEncryption::new(
406 input_file.to_string_lossy().to_string(),
407 String::from("200a0e80e538d17390c8c4bc3bc71e44"),
408 );
409
410 let result = file_encryption.decrypt();
411
412 assert!(result.is_err());
413 });
414 }
415
416 #[test]
417 fn test_broken_encryption_encrypt() {
418 with_env_vars(vec![("EDITOR", Some("echo"))], || {
419 let temp = assert_fs::TempDir::new().unwrap();
420 let input_file = temp.child("encoded.txt");
421 temp.copy_from("./tests/fixtures/", &["*.txt"]).unwrap();
422
423 let file_encryption = FileEncryption::new(
424 input_file.to_string_lossy().to_string(),
425 String::from("v200a0e80e538d17390c8c4bc3bc71e44"),
426 );
427
428 let message = String::from("super secret contents");
429
430 let result = file_encryption.encrypt(message.as_bytes());
431
432 assert!(result.is_err());
433 });
434 }
435
436 #[test]
437 fn test_edit_with_file_changes() {
438 with_env_vars(vec![("EDITOR", Some("echo 'another' >> "))], || {
439 let temp = assert_fs::TempDir::new().unwrap();
440 let input_file = temp.child("encoded.txt");
441 temp.copy_from("./tests/fixtures/", &["*.txt"]).unwrap();
442
443 let file_encryption = FileEncryption::new(
444 input_file.to_string_lossy().to_string(),
445 String::from("200a0e90e538d17390c8c4bc3bc71e44"),
446 );
447
448 assert!(file_encryption.edit().is_ok());
449 });
450 }
451
452 #[test]
453 fn test_create_with_dir() -> anyhow::Result<()> {
454 let temp = assert_fs::TempDir::new().unwrap();
455 let temp_path_string = temp.to_string_lossy().to_string();
456
457 let _ = FileEncryption::create(&temp_path_string);
458
459 assert!(temp.child("credentials.yml.enc").exists());
460 assert!(temp.child("master.key").exists());
461
462 Ok(())
463 }
464
465 #[test]
466 fn test_create_with_filename() -> anyhow::Result<()> {
467 let temp = assert_fs::TempDir::new().unwrap();
468 let temp_file = temp.child("encrypted.txt");
469 let temp_path_string = temp_file.to_string_lossy().to_string();
470
471 let _ = FileEncryption::create(&temp_path_string);
472
473 assert!(temp.child("encrypted.txt").exists());
474 assert!(temp.child("master.key").exists());
475
476 Ok(())
477 }
478
479 #[test]
480 fn test_create_with_invalid_path() -> anyhow::Result<()> {
481 let result = FileEncryption::create("/not/real/path");
482
483 assert!(result.is_err());
484
485 Ok(())
486 }
487
488 #[test]
489 fn test_create_after_create() -> anyhow::Result<()> {
490 let temp = assert_fs::TempDir::new().unwrap();
491 let temp_path_string = temp.to_string_lossy().to_string();
492
493 let _first = FileEncryption::create(&temp_path_string);
494 let second = FileEncryption::create(&temp_path_string);
495
496 assert!(second.is_err());
497
498 Ok(())
499 }
500
501 #[test]
502 fn test_temp_file_location_with_invalid_path() {
503 let temp = assert_fs::TempDir::new().unwrap();
504 let mut temp_path_string = temp.to_string_lossy().to_string();
505 temp_path_string.push_str("/..");
506 let key = String::from("200a0e90e538d17390c8c4bc3bc71e44");
507
508 let fc = FileEncryption::new(temp_path_string, key);
509
510 let result = fc.temp_file_location();
511
512 assert!(result.is_err());
513 }
514}