/ crypto / aead / src / lib.rs
lib.rs
  1  pub mod envs;
  2  
  3  use std::fs;
  4  use std::io::Write;
  5  use std::path::PathBuf;
  6  
  7  use anyhow::{bail, format_err, Result};
  8  use argon2::password_hash::SaltString;
  9  use argon2::{Argon2, Params};
 10  use rand::rngs::OsRng;
 11  use rand::Rng;
 12  use ring::aead::Nonce;
 13  pub use ring::aead::{Aad, LessSafeKey, UnboundKey, NONCE_LEN};
 14  
 15  use crate::envs::FM_TEST_FAST_WEAK_CRYPTO_ENV;
 16  
 17  /// Get a random nonce.
 18  pub fn get_random_nonce() -> ring::aead::Nonce {
 19      Nonce::assume_unique_for_key(OsRng.gen())
 20  }
 21  
 22  /// Encrypt `plaintext` using `key`.
 23  ///
 24  /// Prefixes the ciphertext with a nonce.
 25  pub fn encrypt(mut plaintext: Vec<u8>, key: &LessSafeKey) -> Result<Vec<u8>> {
 26      let nonce = get_random_nonce();
 27      // prefix ciphertext with nonce
 28      let mut ciphertext: Vec<u8> = nonce.as_ref().to_vec();
 29  
 30      key.seal_in_place_append_tag(nonce, Aad::empty(), &mut plaintext)
 31          .map_err(|_| anyhow::format_err!("Encryption failed due to unspecified aead error"))?;
 32  
 33      ciphertext.append(&mut plaintext);
 34  
 35      Ok(ciphertext)
 36  }
 37  
 38  /// Decrypts a `ciphertext` using `key`.
 39  ///
 40  /// Expect nonce in the prefix, like [`encrypt`] produces.
 41  pub fn decrypt<'c>(ciphertext: &'c mut [u8], key: &LessSafeKey) -> Result<&'c [u8]> {
 42      if ciphertext.len() < NONCE_LEN {
 43          bail!("Ciphertext too short: {}", ciphertext.len());
 44      }
 45  
 46      let (nonce_bytes, encrypted_bytes) = ciphertext.split_at_mut(NONCE_LEN);
 47  
 48      key.open_in_place(
 49          Nonce::assume_unique_for_key(nonce_bytes.try_into().expect("nonce size known")),
 50          Aad::empty(),
 51          encrypted_bytes,
 52      )
 53      .map_err(|_| format_err!("Decryption failed due to unspecified aead error"))?;
 54  
 55      Ok(&encrypted_bytes[..encrypted_bytes.len() - key.algorithm().tag_len()])
 56  }
 57  
 58  /// Write `data` encrypted to a `file` with a random `nonce` that will be
 59  /// encoded in the file
 60  pub fn encrypted_write(data: Vec<u8>, key: &LessSafeKey, file: PathBuf) -> Result<()> {
 61      Ok(fs::File::options()
 62          .write(true)
 63          .create_new(true)
 64          .open(file)?
 65          .write_all(hex::encode(encrypt(data, key)?).as_bytes())?)
 66  }
 67  
 68  /// Reads encrypted data from a file
 69  pub fn encrypted_read(key: &LessSafeKey, file: PathBuf) -> Result<Vec<u8>> {
 70      let hex = fs::read_to_string(file)?;
 71      let mut bytes = hex::decode(hex)?;
 72  
 73      Ok(decrypt(&mut bytes, key)?.to_vec())
 74  }
 75  
 76  /// Key used to encrypt and authenticate data stored on the filesystem with a
 77  /// user password.
 78  ///
 79  /// We encrypt certain configs to prevent attackers from learning the private
 80  /// keys if they gain file access.  We authenticate the configs to prevent
 81  /// attackers from manipulating the encrypted files.
 82  ///
 83  /// Users can safely back-up config and salt files on other media the attacker
 84  /// accesses if they do not learn the password and the password has enough
 85  /// entropy to prevent brute-forcing (e.g. 6 random words).
 86  ///
 87  /// We use the ChaCha20 stream cipher with Poly1305 message authentication
 88  /// standardized in IETF RFC 8439.  Argon2 is used for memory-hard key
 89  /// stretching along with a 128-bit salt that is randomly generated to
 90  /// discourage rainbow attacks.
 91  ///
 92  /// * `password` - Strong user-created password
 93  /// * `salt` - Nonce >8 bytes to discourage rainbow attacks
 94  pub fn get_encryption_key(password: &str, salt: &str) -> Result<LessSafeKey> {
 95      let mut key = [0u8; ring::digest::SHA256_OUTPUT_LEN];
 96  
 97      argon2()
 98          .hash_password_into(password.as_bytes(), salt.as_bytes(), &mut key)
 99          .map_err(|e| format_err!("could not hash password").context(e))?;
100      let key = UnboundKey::new(&ring::aead::CHACHA20_POLY1305, &key)
101          .map_err(|_| anyhow::Error::msg("Unable to create key"))?;
102      Ok(LessSafeKey::new(key))
103  }
104  
105  /// Generates a B64-encoded random salt string of the recommended 16 byte length
106  pub fn random_salt() -> String {
107      SaltString::generate(OsRng).to_string()
108  }
109  
110  /// Constructs Argon2 with default params, easier if the weak crypto flag is set
111  /// for testing
112  fn argon2() -> Argon2<'static> {
113      let mut params = argon2::ParamsBuilder::default();
114      if let Ok("1") = std::env::var(FM_TEST_FAST_WEAK_CRYPTO_ENV).as_deref() {
115          params.m_cost(Params::MIN_M_COST);
116      }
117      Argon2::from(params.build().expect("valid params"))
118  }
119  
120  #[cfg(test)]
121  mod tests {
122      use crate::{decrypt, encrypt, get_encryption_key};
123  
124      #[test]
125      fn encrypts_and_decrypts() {
126          let password = "test123";
127          let salt = "salt1235";
128          let message = "hello world";
129  
130          let key = get_encryption_key(password, salt).unwrap();
131          let mut cipher_text = encrypt(message.as_bytes().to_vec(), &key).unwrap();
132          let decrypted = decrypt(&mut cipher_text, &key).unwrap();
133  
134          assert_eq!(decrypted, message.as_bytes());
135      }
136  }