Skip to main content

keybear_core/crypto/
mod.rs

1// Export the encryption primitives here so no extra libraries need to be included
2pub use chacha20poly1305::Nonce;
3pub use x25519_dalek::{PublicKey, SharedSecret, StaticSecret};
4
5use anyhow::{anyhow, bail, Result};
6use chacha20poly1305::{
7    aead::{Aead, NewAead},
8    ChaCha20Poly1305, Key,
9};
10use log::{debug, trace};
11use rand_core::OsRng;
12use serde::{de::DeserializeOwned, Serialize};
13use std::{
14    any,
15    fs::{self, File},
16    io::Read,
17    path::Path,
18};
19
20/// Add functions to the crypto secret key to make it easier to use.
21pub trait StaticSecretExt {
22    /// Check whether there is a file containing the crypto keys.
23    fn verify_file<P>(file: P) -> bool
24    where
25        P: AsRef<Path>;
26
27    /// Generate a new secret key with the OS random number generator.
28    fn new_with_os_rand() -> StaticSecret;
29
30    /// Try to load the crypto keys from our file on the disk.
31    fn from_file<P>(file: P) -> Result<StaticSecret>
32    where
33        P: AsRef<Path>;
34
35    /// Save the crypto keys to the file on the disk.
36    fn save<P>(&self, file: P) -> Result<()>
37    where
38        P: AsRef<Path>;
39
40    /// Try to load the crypto key or generate a new one.
41    fn from_file_or_generate<P>(file: P) -> Result<StaticSecret>
42    where
43        P: AsRef<Path>,
44    {
45        if Self::verify_file(&file) {
46            // The file exists, open it
47            Self::from_file(file)
48        } else {
49            // The file doesn't exist, generate a new one and save it
50            let key = Self::new_with_os_rand();
51            key.save(file)?;
52
53            Ok(key)
54        }
55    }
56}
57
58impl StaticSecretExt for StaticSecret {
59    fn verify_file<P>(file: P) -> bool
60    where
61        P: AsRef<Path>,
62    {
63        // Get the generic as the actual reference so it's traits can be used
64        let file = file.as_ref();
65
66        debug!("Verifying file \"{}\"", file.display());
67
68        // TODO: add more checks
69        file.is_file()
70    }
71
72    fn new_with_os_rand() -> StaticSecret {
73        // Get the generic as the actual reference so it's traits can be used
74        debug!("Generating new secret key");
75
76        // Generate a secret key
77        StaticSecret::new(OsRng)
78    }
79
80    fn from_file<P>(file: P) -> Result<StaticSecret>
81    where
82        P: AsRef<Path>,
83    {
84        // Get the generic as the actual reference so it's traits can be used
85        let file = file.as_ref();
86
87        debug!("Loading secret key from file \"{}\"", file.display());
88
89        // Cannot load from disk if the file is not a valid one
90        if !Self::verify_file(file) {
91            bail!("Reading crypto keys from file {:?} failed", file);
92        }
93
94        // Read the file
95        let mut f = File::open(file)
96            .map_err(|err| anyhow!("Reading crypto keys from file {:?} failed: {}", file, err))?;
97
98        // Read exactly the bytes from the file
99        let mut bytes = [0; 32];
100        f.read_exact(&mut bytes).map_err(|err| {
101            anyhow!(
102                "Crypto keys file {:?} has wrong size, it might be corrupt: {}",
103                file,
104                err
105            )
106        })?;
107
108        // Try to construct the secret key from the bytes
109        Ok(StaticSecret::from(bytes))
110    }
111
112    fn save<P>(&self, file: P) -> Result<()>
113    where
114        P: AsRef<Path>,
115    {
116        // Get the generic as the actual reference so it's traits can be used
117        let file = file.as_ref();
118
119        debug!("Saving secret key to file \"{}\"", file.display());
120
121        // Try to write the keys as raw bytes to the disk
122        fs::write(file, self.to_bytes())
123            .map_err(|err| anyhow!("Could not write crypto keys to file {:?}: {}", file, err))
124    }
125}
126
127/// Encrypt a serializable object into a chacha20poly1305 encoded JSON string.
128pub fn encrypt<T>(shared_secret_key: &SharedSecret, nonce: &Nonce, obj: &T) -> Result<Vec<u8>>
129where
130    T: Serialize,
131{
132    trace!("Encrypting \"{}\" into bytes", any::type_name::<T>());
133
134    // Serialize the object into a JSON byte array
135    let json = serde_json::to_vec(obj)?;
136
137    cipher(shared_secret_key)
138        // Encrypt the message
139        .encrypt(nonce, json.as_slice())
140        .map_err(|err| anyhow!("Encrypting message: {}", err))
141}
142
143/// Decrypt a chacha20poly1305 encoded JSON string into an object.
144pub fn decrypt<T>(shared_secret_key: &SharedSecret, nonce: &Nonce, cipher_bytes: &[u8]) -> Result<T>
145where
146    T: DeserializeOwned,
147{
148    trace!("Trying to decrypt bytes into \"{}\"", any::type_name::<T>());
149
150    cipher(shared_secret_key)
151        // Decrypt the message
152        .decrypt(nonce, cipher_bytes)
153        .map_err(|err| anyhow!("Decrypting message: {}", err))
154        // Try to convert it to a JSON object
155        .map(|bytes| {
156            serde_json::from_slice(&bytes).map_err(|err| {
157                trace!(
158                    "JSON resulting in error \"{}\":\n{}",
159                    err,
160                    String::from_utf8_lossy(&bytes)
161                );
162
163                anyhow!("Decrypted JSON is invalid: {}", err)
164            })
165        })?
166}
167
168/// Create a cipher from the shared secret key of a client and the server.
169fn cipher(shared_secret_key: &SharedSecret) -> ChaCha20Poly1305 {
170    let key = Key::from_slice(shared_secret_key.as_bytes());
171
172    ChaCha20Poly1305::new(key)
173}
174
175#[cfg(test)]
176mod tests {
177    use crate::crypto::{self, StaticSecretExt};
178    use anyhow::Result;
179    use chacha20poly1305::Nonce;
180    use rand_core::OsRng;
181    use serde::{Deserialize, Serialize};
182    use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret};
183
184    #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
185    struct TestObject {
186        string: String,
187        int: i64,
188        vec: Vec<String>,
189    }
190
191    #[test]
192    fn default() -> Result<()> {
193        // Create a temporary directory for the test database
194        let dir = tempfile::tempdir()?;
195        // Create the temporary file to save the key in
196        let file = dir.path().join("key");
197
198        // Try to load the file, which will fail and generate a new file
199        StaticSecret::from_file_or_generate(file)?;
200
201        Ok(())
202    }
203
204    #[test]
205    fn verify() {
206        // A non-existing file means it's not a valid file for the keys
207        assert_eq!(
208            StaticSecret::verify_file("/definitily/should/not/exist"),
209            false
210        );
211    }
212
213    #[test]
214    fn save_and_load() -> Result<()> {
215        // Create a temporary directory for the test database
216        let dir = tempfile::tempdir()?;
217        // Create the temporary file to save the key in
218        let file = dir.path().join("key");
219
220        // Generate a new pair of keys.
221        let secret = StaticSecret::new_with_os_rand();
222
223        // Save the secret key
224        secret.save(&file)?;
225
226        // Load the saved secret key from disk
227        let disk_secret = StaticSecret::from_file(file)?;
228
229        // Check if they are the same
230        assert_eq!(secret.to_bytes(), disk_secret.to_bytes());
231
232        // Close the directory
233        dir.close()?;
234
235        Ok(())
236    }
237
238    #[test]
239    fn encrypt_decrypt() -> Result<()> {
240        // Generate a new shared key
241        let alice_secret = EphemeralSecret::new(OsRng);
242        let bob_secret = EphemeralSecret::new(OsRng);
243        let bob_public = PublicKey::from(&bob_secret);
244        let shared_secret = alice_secret.diffie_hellman(&bob_public);
245
246        let obj = TestObject {
247            string: "HI".to_string(),
248            int: 1234,
249            vec: vec!["Hi!".to_string(), "there".to_string()],
250        };
251
252        // Create a random nonce
253        let nonce = Nonce::from_slice(b"abcdefghijkl");
254
255        // Encrypt the object
256        let cipher_bytes = crypto::encrypt(&shared_secret, &nonce, &obj)?;
257
258        // Decrypt the encrypted string
259        let decrypted: TestObject = crypto::decrypt(&shared_secret, &nonce, &cipher_bytes)?;
260
261        assert_eq!(obj, decrypted);
262
263        Ok(())
264    }
265}