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 }