Skip to main content

fedimint_aead/
lib.rs

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