pwsec/
chacha.rs

1use super::Error;
2use chacha20poly1305::{
3    ChaCha20Poly1305,
4    aead::{Aead, AeadCore, KeyInit, OsRng, Payload},
5};
6use generic_array::GenericArray;
7use pbkdf2::{password_hash::SaltString, pbkdf2_hmac_array};
8use sha2::Sha256;
9
10const KEY_LENGTH: usize = 32;
11const SALT_LEN: usize = 22;
12const NONCE_LEN: usize = 12;
13
14/// A tool to encrypt and decrypt data with `chacha20poly1305`, with a key that is derived from the
15/// given password using `pbkdf2`.
16///
17/// The methods `encrypt` and `decrypt` just deal with the secret,
18/// the methods `encrypt_auth` and `decrypt_auth` use additionally an authentication tag.
19///
20/// The return value of the encrypt methods is of type [`Cipher`] and needs to be provided
21/// with the same content to the decrypt call.
22///
23/// # Example with authentication tag
24///
25/// ```rust
26/// use pwsec::Chacha;
27/// let secret = b"this is some serialized form of the secret data";
28/// let auth_tag = b"this is just some informal and nonconfidential summary";
29/// let pw = "LOIUo98zkjhB";
30/// let chacha = Chacha::with_pbkdf2_rounds(123_456);
31/// let cipher = chacha.encrypt_auth(secret, auth_tag, pw).unwrap();
32///
33/// // Decryption needs the cipher, the auth_tag, and the password:
34/// let decrypted_secret = chacha.decrypt_auth(&cipher, auth_tag, pw).unwrap();
35/// assert_eq!(secret.to_vec(), decrypted_secret);
36/// ```
37pub struct Chacha {
38    pbkdf_rounds: u32,
39}
40impl Chacha {
41    /// The constructor takes the number of rounds that pbkdf2 should use.
42    ///
43    /// Choose a random big value, like `125_642` or `101_864`, and use the same number
44    /// for encryption and decryption.
45    #[must_use]
46    pub fn with_pbkdf2_rounds(pbkdf_rounds: u32) -> Self {
47        Self { pbkdf_rounds }
48    }
49    fn derive_key_from_password(&self, password: &str, salt: &[u8]) -> [u8; KEY_LENGTH] {
50        pbkdf2_hmac_array::<Sha256, KEY_LENGTH>(password.as_bytes(), salt, self.pbkdf_rounds)
51    }
52
53    /// Encrypts a secret securely, based on a password.
54    ///
55    /// # Parameters:
56    /// - Input:
57    ///   - `secret`: the sensitive data you want to encrypt
58    ///   - `pw`: the password
59    /// - Output:
60    ///   - the encrypted secret
61    ///
62    /// # Errors
63    ///
64    /// `Error::Encrypt` can occur.
65    pub fn encrypt(&self, secret: &[u8], pw: &str) -> Result<Cipher, Error> {
66        self.encrypt_auth(secret, &[0_u8; 0], pw)
67    }
68
69    /// Encrypts a secret securely, based on a password, and using an authentication tag.
70    ///
71    /// # Parameters:
72    /// - Input:
73    ///   - `secret`: the sensitive data you want to encrypt
74    ///   - `auth`: optional additional data that will not be encrypted, but must be provided
75    ///     in unmodified form also to `decrypt_auth`
76    ///   - `pw`: the password
77    /// - Output:
78    ///   - the sensitive data you had encrypted
79    ///
80    /// # Errors
81    ///
82    /// `Error::Encrypt` can occur.
83    #[allow(clippy::missing_panics_doc)]
84    pub fn encrypt_auth(&self, secret: &[u8], auth: &[u8], pw: &str) -> Result<Cipher, Error> {
85        let salt = SaltString::generate(&mut OsRng);
86        debug_assert_eq!(salt.len(), SALT_LEN);
87
88        let key = self.derive_key_from_password(pw, salt.as_str().as_bytes());
89
90        let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
91        debug_assert_eq!(nonce.len(), NONCE_LEN);
92
93        Ok(Cipher {
94            salt: salt.to_string(),
95            ciphertext: ChaCha20Poly1305::new_from_slice(&key)
96            .unwrap(/*OK*/)
97            .encrypt(
98                &nonce,
99                Payload {
100                    msg: secret,
101                    aad: auth,
102                },
103            )
104            .map_err(|e| {
105                Error::Encrypt(format!("can't encrypt the connection store, due to {e}"))
106            })?,
107            nonce: nonce.to_vec(),
108        })
109    }
110
111    /// Decrypts data that were encrypted with `encrypt()`.
112    ///
113    /// # Parameters:
114    /// - Input:
115    ///   - `cipher`: the output from `encrypt`
116    ///   - `auth`: the same value as it was given to the encrypt call
117    ///   - `pw`: the password
118    /// - Output:
119    ///   - the sensitive data you had encrypted
120    ///
121    /// # Errors
122    ///
123    /// `Error::Decrypt` can occur.
124    #[allow(clippy::missing_panics_doc)]
125    pub fn decrypt(&self, cipher: &Cipher, pw: &str) -> Result<Vec<u8>, Error> {
126        self.decrypt_auth(cipher, "".as_bytes(), pw)
127    }
128
129    /// Decrypts data that were encrypted with `encrypt()`.
130    ///
131    /// # Parameters:
132    /// - Input:
133    ///   - `cipher`: the output from `encrypt`
134    ///   - `auth`: the same value as it was given to the encrypt call
135    ///   - `pw`: the password
136    /// - Output:
137    ///   - the sensitive data you had encrypted
138    ///
139    /// # Errors
140    ///
141    /// `Error::Decrypt` can occur.
142    #[allow(clippy::missing_panics_doc)]
143    pub fn decrypt_auth(&self, cipher: &Cipher, auth: &[u8], pw: &str) -> Result<Vec<u8>, Error> {
144        let key = self.derive_key_from_password(pw, cipher.salt.as_bytes());
145        let ccp = ChaCha20Poly1305::new_from_slice(&key).unwrap(/*ok*/);
146        ccp.decrypt(
147            GenericArray::from_slice(&cipher.nonce),
148            Payload {
149                msg: &cipher.ciphertext,
150                aad: auth,
151            },
152        )
153        .map_err(|e| Error::Decrypt(e.to_string()))
154    }
155}
156
157/// The values that need to be provided to the decrypt call,
158/// in addition to the password and optionally the auth tag.
159pub struct Cipher {
160    /// The salt that was randomly generated and used within the key derivation
161    /// within the encrypt call.
162    pub salt: String,
163    /// The encrypted and authenticated secret.
164    pub ciphertext: Vec<u8>,
165    /// The randomly generated nonce that was used for the encryption.
166    pub nonce: Vec<u8>,
167}
168
169#[cfg(test)]
170mod test {
171    use super::Chacha;
172
173    #[test]
174    fn test_encrypt_decrypt() {
175        let test_data = String::from("daewörjlser,dk gjxlre.t98i1df.lskejr lewiri23r9ß iu4ötirjf");
176        let pw = "LOIUo98zkjhB";
177        let chacha = Chacha::with_pbkdf2_rounds(123_456);
178        let cipher = chacha.encrypt(test_data.as_bytes(), pw).unwrap();
179        // ---
180        let result = String::from_utf8(chacha.decrypt(&cipher, pw).unwrap()).unwrap();
181        assert_eq!(test_data, result);
182    }
183
184    #[test]
185    fn test_encrypt_auth_decrypt_auth() {
186        let test_data = String::from("daewörjlser,dk gjxlre.t98i1df.lskejr lewiri23r9ß iu4ötirjf");
187        let test_auth = "oirlyrkfdösadkflsdkgnfm";
188        let pw = "LOIUo98zkjhB";
189        let chacha = Chacha::with_pbkdf2_rounds(123_456);
190        let cipher = chacha
191            .encrypt_auth(test_data.as_bytes(), test_auth.as_bytes(), pw)
192            .unwrap();
193        // ---
194        let result = String::from_utf8(
195            chacha
196                .decrypt_auth(&cipher, test_auth.as_bytes(), pw)
197                .unwrap(),
198        )
199        .unwrap();
200        assert_eq!(test_data, result);
201    }
202}