street_cred/encryption/
file_encryptor.rs

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
12/// Represents an encryped file that we can edit the contents of while
13/// preserving the encryption.
14///
15/// # Examples
16///
17/// You can create a `FileEncryption` using the following code:
18///
19/// ```
20/// use street_cred::FileEncryption;
21///
22/// let file_name = String::from("encrypted.txt");
23/// let key = String::from("16 byte key line");
24/// let file_encryption = FileEncryption::new(file_name, key);
25///
26/// // File is decrypted, opened in your EDITOR for modification
27/// // and once closed, re-encrypts the file if it's contents have changed.
28/// // let result = file_encryption.edit();
29///
30/// // match result {
31/// //   Ok(_) -> {},
32/// //   Err(why) => println!("{}", why),
33/// // }
34/// ```
35pub struct FileEncryption {
36  file_path: String,
37  key: String,
38}
39
40impl FileEncryption {
41  /// Create a new instance of FileEncryption.
42  ///
43  /// # Arguments
44  /// * `file_path` - Path to the encrypted file.
45  /// * `key` - Key to use for encryption/decryption.
46  ///
47  /// # Examples
48  ///
49  /// ```
50  /// use street_cred::FileEncryption;
51  ///
52  /// let file_path = String::from("some_file.txt");
53  /// let key = String::from("425D76994EE6101105DDDA2EE2604AA0");
54  /// let file_encryption = FileEncryption::new(file_path, key);
55  /// ```
56  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  /// Initialize a new credentials file and master key in the current directory.
64  ///
65  /// # Example
66  ///
67  /// ```
68  /// use street_cred::FileEncryption;
69  /// # use std::fs;
70  /// # use assert_fs::prelude::*;
71  ///
72  ///
73  /// # let file_path = assert_fs::TempDir::new().unwrap().to_string_lossy().to_string();
74  /// let _ = FileEncryption::create(&file_path);
75  /// ```
76  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  /// Edit the contents of an encrypted file via your preferred EDITOR.
100  /// If no EDITOR environment variable is set, will default to vim.
101  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  /// Decrypts the contents of the `FileEncryption` and returns them as a tuple
132  /// with three Strings.
133  ///
134  /// # Examples
135  ///
136  /// ```
137  /// use street_cred::FileEncryption;
138  ///
139  /// let file_path = String::from("some_file.txt");
140  /// let key = String::from("425D76994EE6101105DDDA2EE2604AA0");
141  /// let file_encryption = FileEncryption::new(file_path, key);
142  /// // let contents = file_encryption.decrypt()?;
143  /// ```
144  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  /// Encrypts the contents of the `FileEncryption` and returns them as a `String`
161  ///
162  /// # Examples
163  ///
164  /// ```
165  /// use street_cred::FileEncryption;
166  ///
167  /// let file_path = String::from("some_file.txt");
168  /// let key = String::from("425D76994EE6101105DDDA2EE2604AA0");
169  /// let file_encryption = FileEncryption::new(file_path, key);
170  /// let contents = "a secret message";
171  ///
172  /// // let encrypted_contents = file_encryption.encrypt(contents)?;
173  /// ```
174  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  /// Sets environment variables to the given value for the duration of the closure.
302  /// Restores the previous values when the closure completes or panics, before unwinding the panic.
303  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}