keybear_core/crypto/
mod.rs1pub 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
20pub trait StaticSecretExt {
22 fn verify_file<P>(file: P) -> bool
24 where
25 P: AsRef<Path>;
26
27 fn new_with_os_rand() -> StaticSecret;
29
30 fn from_file<P>(file: P) -> Result<StaticSecret>
32 where
33 P: AsRef<Path>;
34
35 fn save<P>(&self, file: P) -> Result<()>
37 where
38 P: AsRef<Path>;
39
40 fn from_file_or_generate<P>(file: P) -> Result<StaticSecret>
42 where
43 P: AsRef<Path>,
44 {
45 if Self::verify_file(&file) {
46 Self::from_file(file)
48 } else {
49 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 let file = file.as_ref();
65
66 debug!("Verifying file \"{}\"", file.display());
67
68 file.is_file()
70 }
71
72 fn new_with_os_rand() -> StaticSecret {
73 debug!("Generating new secret key");
75
76 StaticSecret::new(OsRng)
78 }
79
80 fn from_file<P>(file: P) -> Result<StaticSecret>
81 where
82 P: AsRef<Path>,
83 {
84 let file = file.as_ref();
86
87 debug!("Loading secret key from file \"{}\"", file.display());
88
89 if !Self::verify_file(file) {
91 bail!("Reading crypto keys from file {:?} failed", file);
92 }
93
94 let mut f = File::open(file)
96 .map_err(|err| anyhow!("Reading crypto keys from file {:?} failed: {}", file, err))?;
97
98 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 Ok(StaticSecret::from(bytes))
110 }
111
112 fn save<P>(&self, file: P) -> Result<()>
113 where
114 P: AsRef<Path>,
115 {
116 let file = file.as_ref();
118
119 debug!("Saving secret key to file \"{}\"", file.display());
120
121 fs::write(file, self.to_bytes())
123 .map_err(|err| anyhow!("Could not write crypto keys to file {:?}: {}", file, err))
124 }
125}
126
127pub 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 let json = serde_json::to_vec(obj)?;
136
137 cipher(shared_secret_key)
138 .encrypt(nonce, json.as_slice())
140 .map_err(|err| anyhow!("Encrypting message: {}", err))
141}
142
143pub 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(nonce, cipher_bytes)
153 .map_err(|err| anyhow!("Decrypting message: {}", err))
154 .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
168fn 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 let dir = tempfile::tempdir()?;
195 let file = dir.path().join("key");
197
198 StaticSecret::from_file_or_generate(file)?;
200
201 Ok(())
202 }
203
204 #[test]
205 fn verify() {
206 assert_eq!(
208 StaticSecret::verify_file("/definitily/should/not/exist"),
209 false
210 );
211 }
212
213 #[test]
214 fn save_and_load() -> Result<()> {
215 let dir = tempfile::tempdir()?;
217 let file = dir.path().join("key");
219
220 let secret = StaticSecret::new_with_os_rand();
222
223 secret.save(&file)?;
225
226 let disk_secret = StaticSecret::from_file(file)?;
228
229 assert_eq!(secret.to_bytes(), disk_secret.to_bytes());
231
232 dir.close()?;
234
235 Ok(())
236 }
237
238 #[test]
239 fn encrypt_decrypt() -> Result<()> {
240 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 let nonce = Nonce::from_slice(b"abcdefghijkl");
254
255 let cipher_bytes = crypto::encrypt(&shared_secret, &nonce, &obj)?;
257
258 let decrypted: TestObject = crypto::decrypt(&shared_secret, &nonce, &cipher_bytes)?;
260
261 assert_eq!(obj, decrypted);
262
263 Ok(())
264 }
265}