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}