shell_gpt/
encryption.rs

1//! Shamelessly copied (and slightly updated) from https://blog.azerpas.com/posts/rust-encryption-1
2//!
3//! See https://gist.github.com/azerpas/b0820999293ec4c1b5e0dc6f66f1f545
4
5use anyhow::{bail, ensure, Context};
6use orion::aead::SecretKey;
7use rand::RngCore;
8
9/// Encrypts the plaintext with the given password and returns the ciphertext. The nonce is generated at each call to strengthen the encryption.
10/// Otherwise there's a chance the key is weakened if the same nonce is used.
11/// The nonce is 24 byte (following the XCHACHA_NONCESIZE property).
12/// The ciphertext will be 40 bytes longer than the plaintext because of the XCHACHA_NONCESIZE + POLY1305_OUTSIZE size.
13///
14/// ## Format
15///
16/// {0,24: nonce} {24,: ciphertext} ...
17///
18/// ## Arguments
19/// - `plaintext`: The plaintext to encrypt
20/// - `password`: The password to use for the encryption
21///
22/// ## Returns
23/// The ciphertext
24pub fn encrypt(plaintext: impl AsRef<[u8]>, password: impl AsRef<str>) -> anyhow::Result<Vec<u8>> {
25    use orion::hazardous::{
26        aead::xchacha20poly1305::{seal, Nonce, SecretKey as XSecretKey},
27        mac::poly1305::POLY1305_OUTSIZE,
28        stream::xchacha20::XCHACHA_NONCESIZE,
29    };
30    // Fetch param as refs
31    let plaintext = plaintext.as_ref();
32    let password = password.as_ref();
33    let mut nonce = [0u8; 24];
34    rand::thread_rng().fill_bytes(&mut nonce);
35    let nonce = nonce.as_ref();
36    // Get high-level API key
37    let key = get_key_from_password(password, nonce)?;
38    // Convert high-level API key to low-level API key
39    let key =
40        XSecretKey::from_slice(key.unprotected_as_bytes()).with_context(|| "Key is invalid")?;
41
42    // Create a Nonce struct from the generated nonce
43    let nonce = Nonce::from_slice(nonce).with_context(|| "Nonce is too short")?;
44
45    // Get the output length
46    let output_len = match plaintext
47        .len()
48        .checked_add(XCHACHA_NONCESIZE + POLY1305_OUTSIZE)
49    {
50        Some(min_output_len) => min_output_len,
51        None => bail!("Plaintext is too long"),
52    };
53
54    // Allocate a buffer for the output
55    let mut output = vec![0u8; output_len];
56    output[..XCHACHA_NONCESIZE].copy_from_slice(nonce.as_ref());
57
58    // Encrypt the plaintext and add it to the end of output buffer
59    seal(
60        &key,
61        &nonce,
62        plaintext,
63        None,
64        &mut output[XCHACHA_NONCESIZE..],
65    )
66    .with_context(|| "Could not convert key")?;
67
68    Ok(output)
69}
70
71/// Decrypts the ciphertext with the given password and returns the plaintext.
72///
73/// ## Arguments
74/// - `ciphertext`: The ciphertext to decrypt
75/// - `password`: The password to use for the decryption
76///
77/// ## Returns
78/// The plaintext as bytes
79pub fn decrypt(ciphertext: impl AsRef<[u8]>, password: impl AsRef<str>) -> anyhow::Result<Vec<u8>> {
80    use orion::aead::open;
81    use orion::hazardous::stream::xchacha20::XCHACHA_NONCESIZE;
82
83    let ciphertext = ciphertext.as_ref();
84    let password = password.as_ref();
85
86    ensure!(
87        ciphertext.len() > XCHACHA_NONCESIZE,
88        "Ciphertext is too short"
89    );
90
91    // Get the key from the password and salt
92    let key = get_key_from_password(password, &ciphertext[..XCHACHA_NONCESIZE])?;
93    open(&key, ciphertext).with_context(|| "Ciphertext was tampered with")
94}
95
96/// Get a SecretKey that will be used to encrypt/decrypt the data
97///
98/// # Arguments
99/// - `password` - The password used to encrypt/decrypt the data
100/// - `salt` - The salt used to strengthen the encryption
101fn get_key_from_password(password: &str, salt: &[u8]) -> anyhow::Result<SecretKey> {
102    use orion::hazardous::stream::chacha20::CHACHA_KEYSIZE;
103    use orion::kdf::{derive_key, Password, Salt};
104    let password = Password::from_slice(password.as_bytes()).with_context(|| "Password error")?;
105    let salt = Salt::from_slice(salt).with_context(|| "Salt is too short")?;
106    let kdf_key = derive_key(&password, &salt, 15, 1024, CHACHA_KEYSIZE as u32)
107        .with_context(|| "Could not derive key from password")?;
108    let key = SecretKey::from_slice(kdf_key.unprotected_as_bytes())
109        .with_context(|| "Could not convert key")?;
110    Ok(key)
111}