fedimint_aead/
lib.rs

1pub mod envs;
2
3use std::fs;
4use std::io::Write;
5use std::path::PathBuf;
6
7use anyhow::{Result, bail, format_err};
8use argon2::password_hash::SaltString;
9use argon2::{Argon2, Params};
10use rand::Rng;
11use rand::rngs::OsRng;
12use ring::aead::Nonce;
13pub use ring::aead::{Aad, LessSafeKey, NONCE_LEN, UnboundKey};
14
15use crate::envs::FM_TEST_FAST_WEAK_CRYPTO_ENV;
16
17/// Get a random nonce.
18pub fn get_random_nonce() -> ring::aead::Nonce {
19    Nonce::assume_unique_for_key(OsRng.r#gen())
20}
21
22/// Encrypt `plaintext` using `key`.
23///
24/// Prefixes the ciphertext with a nonce.
25pub 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.
41pub 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
60pub fn encrypted_write(data: Vec<u8>, key: &LessSafeKey, file: PathBuf) -> Result<()> {
61    let mut file = fs::File::options()
62        .write(true)
63        .create_new(true)
64        .open(file)?;
65
66    file.write_all(hex::encode(encrypt(data, key)?).as_bytes())?;
67    file.sync_all()?;
68
69    Ok(())
70}
71
72/// Reads encrypted data from a file
73pub fn encrypted_read(key: &LessSafeKey, file: PathBuf) -> Result<Vec<u8>> {
74    let hex = fs::read_to_string(file)?;
75    let mut bytes = hex::decode(hex)?;
76
77    Ok(decrypt(&mut bytes, key)?.to_vec())
78}
79
80/// Key used to encrypt and authenticate data stored on the filesystem with a
81/// user password.
82///
83/// We encrypt certain configs to prevent attackers from learning the private
84/// keys if they gain file access.  We authenticate the configs to prevent
85/// attackers from manipulating the encrypted files.
86///
87/// Users can safely back-up config and salt files on other media the attacker
88/// accesses if they do not learn the password and the password has enough
89/// entropy to prevent brute-forcing (e.g. 6 random words).
90///
91/// We use the ChaCha20 stream cipher with Poly1305 message authentication
92/// standardized in IETF RFC 8439.  Argon2 is used for memory-hard key
93/// stretching along with a 128-bit salt that is randomly generated to
94/// discourage rainbow attacks.
95///
96/// * `password` - Strong user-created password
97/// * `salt` - Nonce >8 bytes to discourage rainbow attacks
98pub fn get_encryption_key(password: &str, salt: &str) -> Result<LessSafeKey> {
99    let mut key = [0u8; ring::digest::SHA256_OUTPUT_LEN];
100
101    argon2()
102        .hash_password_into(password.as_bytes(), salt.as_bytes(), &mut key)
103        .map_err(|e| format_err!("could not hash password").context(e))?;
104    let key = UnboundKey::new(&ring::aead::CHACHA20_POLY1305, &key)
105        .map_err(|_| anyhow::Error::msg("Unable to create key"))?;
106    Ok(LessSafeKey::new(key))
107}
108
109/// Generates a B64-encoded random salt string of the recommended 16 byte length
110pub fn random_salt() -> String {
111    SaltString::generate(OsRng).to_string()
112}
113
114/// Constructs Argon2 with default params, easier if the weak crypto flag is set
115/// for testing
116fn argon2() -> Argon2<'static> {
117    let mut params = argon2::ParamsBuilder::default();
118    if let Ok("1") = std::env::var(FM_TEST_FAST_WEAK_CRYPTO_ENV).as_deref() {
119        params.m_cost(Params::MIN_M_COST);
120    }
121    Argon2::from(params.build().expect("valid params"))
122}
123
124#[cfg(test)]
125mod tests;