rucksack_db/db/
encrypted.rs

1use anyhow::{anyhow, Result};
2use secrecy::{ExposeSecret, Secret, SecretString};
3
4use rucksack_lib::file;
5
6use crate::crypto;
7
8pub struct EncryptedDB {
9    bytes: Vec<u8>,
10    decrypted: Secret<Vec<u8>>,
11    path: String,
12    pwd: SecretString,
13    salt: SecretString,
14}
15
16impl EncryptedDB {
17    pub fn from_decrypted(
18        decrypted: Vec<u8>,
19        path: String,
20        pwd: String,
21        salt: String,
22    ) -> Result<EncryptedDB> {
23        EncryptedDB::new(None, Some(decrypted), path, pwd, salt)
24    }
25
26    pub fn from_encrypted(
27        encrypted: Vec<u8>,
28        path: String,
29        pwd: String,
30        salt: String,
31    ) -> Result<EncryptedDB> {
32        EncryptedDB::new(Some(encrypted), None, path, pwd, salt)
33    }
34
35    pub fn from_file(path: String, pwd: String, salt: String) -> Result<EncryptedDB> {
36        EncryptedDB::new(None, None, path, pwd, salt)
37    }
38
39    pub fn new(
40        bytes: Option<Vec<u8>>,
41        decrypted: Option<Vec<u8>>,
42        path: String,
43        pwd: String,
44        salt: String,
45    ) -> Result<EncryptedDB> {
46        let mut edb = EncryptedDB {
47            bytes: Vec::new(),
48            decrypted: Secret::new(Vec::new()),
49            path,
50            pwd: SecretString::new(pwd),
51            salt: SecretString::new(salt),
52        };
53        if bytes.is_none() && decrypted.is_none() {
54            log::debug!(source = "file", path = edb.path.as_str(), operation = "init"; "No bytes provided; reading from file");
55            edb.read()?;
56            edb.decrypt()?;
57        } else if let Some(b) = bytes {
58            log::debug!(source = "bytes", operation = "decrypt"; "Got encrypted bytes; decrypting");
59            edb.bytes = b;
60            edb.decrypt()?;
61        } else if let Some(d) = decrypted {
62            log::debug!(source = "bytes", operation = "encrypt"; "Got decrypted bytes; encrypting");
63            edb.decrypted = Secret::new(d);
64            edb.encrypt()?;
65        }
66        Ok(edb)
67    }
68
69    pub fn bytes(&self) -> Vec<u8> {
70        self.bytes.clone()
71    }
72
73    pub fn decrypt(&mut self) -> Result<()> {
74        log::debug!(operation = "decrypt"; "Decrypting stored bytes");
75        log::trace!(pwd_len = self.pwd().len(), salt_len = self.salt().len(); "Credentials info");
76        match crypto::decrypt(self.bytes.clone(), self.pwd(), self.salt()) {
77            Ok(bytes) => {
78                log::trace!(bytes_len = bytes.len(), operation = "decrypt_success"; "Decrypted bytes");
79                self.decrypted = Secret::new(bytes);
80                Ok(())
81            }
82            Err(e) => {
83                let msg = format!("Could not decrypt data: {e:?}");
84                log::error!(error = e.to_string().as_str(), operation = "decrypt"; "{}", msg);
85                Err(anyhow!("{}", msg))
86            }
87        }
88    }
89
90    pub fn decrypted(&self) -> Vec<u8> {
91        self.decrypted.expose_secret().to_vec()
92    }
93
94    pub fn encrypt(&mut self) -> Result<()> {
95        log::trace!(bytes_len_before = self.bytes.len(), operation = "encrypt"; "Byte length before encryption");
96        self.bytes = crypto::encrypt(self.decrypted(), self.pwd(), self.salt())?;
97        log::trace!(bytes_len_after = self.bytes.len(), operation = "encrypt"; "Byte length after encryption");
98        Ok(())
99    }
100
101    pub fn path(&self) -> String {
102        self.path.clone()
103    }
104
105    pub fn pwd(&self) -> String {
106        self.pwd.expose_secret().to_string()
107    }
108
109    pub fn read(&mut self) -> Result<()> {
110        log::trace!(bytes_len_before = self.bytes.len(), operation = "read"; "Byte length before read");
111        self.bytes = file::read(self.path())?;
112        log::trace!(bytes_len_after = self.bytes.len(), operation = "read"; "Byte length after read");
113        Ok(())
114    }
115
116    pub fn salt(&self) -> String {
117        self.salt.expose_secret().to_string()
118    }
119
120    pub fn write(&self) -> Result<()> {
121        log::debug!(operation = "write", path = self.path().as_str(); "Writing encrypted DB");
122        file::write(self.bytes(), self.path())
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use std::fs;
130    use std::path::PathBuf;
131    use tempfile::TempDir;
132
133    fn setup_test_dir() -> (TempDir, PathBuf) {
134        let dir = TempDir::new().unwrap();
135        let path = dir.path().join("test_db.bin");
136        (dir, path)
137    }
138
139    #[test]
140    fn test_from_decrypted_basic() {
141        let data = b"Hello, World!".to_vec();
142        let pwd = "test_password".to_string();
143        let salt = "test_salt".to_string();
144        let (_dir, path) = setup_test_dir();
145
146        let edb = EncryptedDB::from_decrypted(
147            data.clone(),
148            path.to_str().unwrap().to_string(),
149            pwd.clone(),
150            salt.clone(),
151        )
152        .unwrap();
153
154        assert_eq!(edb.decrypted(), data);
155        assert_eq!(edb.pwd(), pwd);
156        assert_eq!(edb.salt(), salt);
157        assert!(
158            !edb.bytes().is_empty(),
159            "Encrypted bytes should not be empty"
160        );
161        assert_ne!(edb.bytes(), data, "Encrypted should differ from decrypted");
162    }
163
164    #[test]
165    fn test_from_decrypted_empty() {
166        let data = Vec::new();
167        let (_dir, path) = setup_test_dir();
168
169        let edb = EncryptedDB::from_decrypted(
170            data.clone(),
171            path.to_str().unwrap().to_string(),
172            "pwd".to_string(),
173            "salt".to_string(),
174        )
175        .unwrap();
176
177        assert_eq!(edb.decrypted(), data);
178    }
179
180    #[test]
181    fn test_from_encrypted_basic() {
182        let data = b"Test data".to_vec();
183        let pwd = "password".to_string();
184        let salt = "salt123".to_string();
185        let (_dir, path) = setup_test_dir();
186
187        // First encrypt some data
188        let encrypted = crypto::encrypt(data.clone(), pwd.clone(), salt.clone()).unwrap();
189
190        // Create from encrypted
191        let edb = EncryptedDB::from_encrypted(
192            encrypted.clone(),
193            path.to_str().unwrap().to_string(),
194            pwd,
195            salt,
196        )
197        .unwrap();
198
199        assert_eq!(edb.decrypted(), data);
200        assert_eq!(edb.bytes(), encrypted);
201    }
202
203    #[test]
204    fn test_from_encrypted_decryption_failure() {
205        let invalid_encrypted = vec![1, 2, 3, 4, 5];
206        let (_dir, path) = setup_test_dir();
207
208        let result = EncryptedDB::from_encrypted(
209            invalid_encrypted,
210            path.to_str().unwrap().to_string(),
211            "pwd".to_string(),
212            "salt".to_string(),
213        );
214
215        assert!(result.is_err(), "Should fail to decrypt invalid data");
216        if let Err(e) = result {
217            assert!(e.to_string().contains("Could not decrypt"));
218        }
219    }
220
221    #[test]
222    fn test_from_file_success() {
223        let data = b"File data".to_vec();
224        let pwd = "file_pwd".to_string();
225        let salt = "file_salt".to_string();
226        let (_dir, path) = setup_test_dir();
227
228        // Write encrypted data to file
229        let encrypted = crypto::encrypt(data.clone(), pwd.clone(), salt.clone()).unwrap();
230        fs::write(&path, encrypted).unwrap();
231
232        // Load from file
233        let edb = EncryptedDB::from_file(path.to_str().unwrap().to_string(), pwd, salt).unwrap();
234
235        assert_eq!(edb.decrypted(), data);
236    }
237
238    #[test]
239    fn test_from_file_not_exists() {
240        let path = "/nonexistent/path/to/file.bin";
241
242        let result =
243            EncryptedDB::from_file(path.to_string(), "pwd".to_string(), "salt".to_string());
244
245        assert!(result.is_err(), "Should fail when file doesn't exist");
246    }
247
248    #[test]
249    fn test_new_with_decrypted() {
250        let data = b"New decrypted".to_vec();
251        let (_dir, path) = setup_test_dir();
252
253        let edb = EncryptedDB::new(
254            None,
255            Some(data.clone()),
256            path.to_str().unwrap().to_string(),
257            "pwd".to_string(),
258            "salt".to_string(),
259        )
260        .unwrap();
261
262        assert_eq!(edb.decrypted(), data);
263        assert!(!edb.bytes().is_empty());
264    }
265
266    #[test]
267    fn test_new_with_encrypted() {
268        let data = b"Original".to_vec();
269        let pwd = "pwd".to_string();
270        let salt = "salt".to_string();
271        let encrypted = crypto::encrypt(data.clone(), pwd.clone(), salt.clone()).unwrap();
272        let (_dir, path) = setup_test_dir();
273
274        let edb = EncryptedDB::new(
275            Some(encrypted.clone()),
276            None,
277            path.to_str().unwrap().to_string(),
278            pwd,
279            salt,
280        )
281        .unwrap();
282
283        assert_eq!(edb.decrypted(), data);
284        assert_eq!(edb.bytes(), encrypted);
285    }
286
287    #[test]
288    fn test_new_from_file() {
289        let data = b"File content".to_vec();
290        let pwd = "pwd".to_string();
291        let salt = "salt".to_string();
292        let (_dir, path) = setup_test_dir();
293
294        // Prepare file
295        let encrypted = crypto::encrypt(data.clone(), pwd.clone(), salt.clone()).unwrap();
296        fs::write(&path, encrypted).unwrap();
297
298        // Create from file
299        let edb =
300            EncryptedDB::new(None, None, path.to_str().unwrap().to_string(), pwd, salt).unwrap();
301
302        assert_eq!(edb.decrypted(), data);
303    }
304
305    #[test]
306    fn test_bytes_getter() {
307        let data = b"Data".to_vec();
308        let (_dir, path) = setup_test_dir();
309
310        let edb = EncryptedDB::from_decrypted(
311            data,
312            path.to_str().unwrap().to_string(),
313            "pwd".to_string(),
314            "salt".to_string(),
315        )
316        .unwrap();
317
318        let bytes1 = edb.bytes();
319        let bytes2 = edb.bytes();
320        assert_eq!(bytes1, bytes2, "bytes() should return same value");
321        assert!(!bytes1.is_empty());
322    }
323
324    #[test]
325    fn test_decrypted_getter() {
326        let data = b"Decrypted data".to_vec();
327        let (_dir, path) = setup_test_dir();
328
329        let edb = EncryptedDB::from_decrypted(
330            data.clone(),
331            path.to_str().unwrap().to_string(),
332            "pwd".to_string(),
333            "salt".to_string(),
334        )
335        .unwrap();
336
337        assert_eq!(edb.decrypted(), data);
338    }
339
340    #[test]
341    fn test_path_getter() {
342        let data = b"Data".to_vec();
343        let (_dir, path) = setup_test_dir();
344        let path_str = path.to_str().unwrap().to_string();
345
346        let edb = EncryptedDB::from_decrypted(
347            data,
348            path_str.clone(),
349            "pwd".to_string(),
350            "salt".to_string(),
351        )
352        .unwrap();
353
354        assert_eq!(edb.path(), path_str);
355    }
356
357    #[test]
358    fn test_pwd_getter() {
359        let data = b"Data".to_vec();
360        let pwd = "my_password".to_string();
361        let (_dir, path) = setup_test_dir();
362
363        let edb = EncryptedDB::from_decrypted(
364            data,
365            path.to_str().unwrap().to_string(),
366            pwd.clone(),
367            "salt".to_string(),
368        )
369        .unwrap();
370
371        assert_eq!(edb.pwd(), pwd);
372    }
373
374    #[test]
375    fn test_salt_getter() {
376        let data = b"Data".to_vec();
377        let salt = "my_salt".to_string();
378        let (_dir, path) = setup_test_dir();
379
380        let edb = EncryptedDB::from_decrypted(
381            data,
382            path.to_str().unwrap().to_string(),
383            "pwd".to_string(),
384            salt.clone(),
385        )
386        .unwrap();
387
388        assert_eq!(edb.salt(), salt);
389    }
390
391    #[test]
392    fn test_encrypt_decrypt_roundtrip() {
393        let original = b"Roundtrip data".to_vec();
394        let pwd = "pwd".to_string();
395        let salt = "salt".to_string();
396        let (_dir, path) = setup_test_dir();
397
398        let mut edb = EncryptedDB::from_decrypted(
399            original.clone(),
400            path.to_str().unwrap().to_string(),
401            pwd.clone(),
402            salt.clone(),
403        )
404        .unwrap();
405
406        let encrypted = edb.bytes();
407        assert_ne!(encrypted, original);
408
409        // Manually decrypt
410        edb.decrypt().unwrap();
411        assert_eq!(edb.decrypted(), original);
412
413        // Re-encrypt
414        edb.encrypt().unwrap();
415        assert_ne!(edb.bytes(), original);
416    }
417
418    #[test]
419    fn test_write_and_read() {
420        let data = b"Write test".to_vec();
421        let pwd = "pwd".to_string();
422        let salt = "salt".to_string();
423        let (_dir, path) = setup_test_dir();
424        let path_str = path.to_str().unwrap().to_string();
425
426        // Create and write
427        let edb =
428            EncryptedDB::from_decrypted(data.clone(), path_str.clone(), pwd.clone(), salt.clone())
429                .unwrap();
430
431        edb.write().unwrap();
432
433        // Verify file exists
434        assert!(path.exists(), "File should exist after write");
435
436        // Read back
437        let edb2 = EncryptedDB::from_file(path_str, pwd, salt).unwrap();
438        assert_eq!(edb2.decrypted(), data);
439    }
440
441    #[test]
442    fn test_write_invalid_path() {
443        let data = b"Data".to_vec();
444        let (_dir, _path) = setup_test_dir();
445
446        let edb = EncryptedDB::from_decrypted(
447            data,
448            "/invalid/path/that/does/not/exist/file.bin".to_string(),
449            "pwd".to_string(),
450            "salt".to_string(),
451        )
452        .unwrap();
453
454        let result = edb.write();
455        assert!(result.is_err(), "Should fail to write to invalid path");
456    }
457
458    #[test]
459    fn test_read_updates_bytes() {
460        let data = b"Read test".to_vec();
461        let pwd = "pwd".to_string();
462        let salt = "salt".to_string();
463        let (_dir, path) = setup_test_dir();
464        let path_str = path.to_str().unwrap().to_string();
465
466        // Write encrypted data to file
467        let encrypted = crypto::encrypt(data.clone(), pwd.clone(), salt.clone()).unwrap();
468        fs::write(&path, encrypted.clone()).unwrap();
469
470        // Create empty EncryptedDB and read
471        let mut edb = EncryptedDB {
472            bytes: Vec::new(),
473            decrypted: Secret::new(Vec::new()),
474            path: path_str,
475            pwd: SecretString::new(pwd),
476            salt: SecretString::new(salt),
477        };
478
479        assert_eq!(edb.bytes().len(), 0, "Should start empty");
480        edb.read().unwrap();
481        assert_eq!(edb.bytes(), encrypted, "Should match file contents");
482    }
483
484    #[test]
485    fn test_decrypt_error_handling() {
486        let (_dir, path) = setup_test_dir();
487
488        let mut edb = EncryptedDB {
489            bytes: vec![1, 2, 3], // Invalid encrypted data
490            decrypted: Secret::new(Vec::new()),
491            path: path.to_str().unwrap().to_string(),
492            pwd: SecretString::new("pwd".to_string()),
493            salt: SecretString::new("salt".to_string()),
494        };
495
496        let result = edb.decrypt();
497        assert!(result.is_err());
498        if let Err(e) = result {
499            assert!(e.to_string().contains("Could not decrypt"));
500        }
501    }
502
503    #[test]
504    fn test_multiple_encrypt_decrypt_cycles() {
505        let original = b"Cycle test".to_vec();
506        let (_dir, path) = setup_test_dir();
507
508        let mut edb = EncryptedDB::from_decrypted(
509            original.clone(),
510            path.to_str().unwrap().to_string(),
511            "pwd".to_string(),
512            "salt".to_string(),
513        )
514        .unwrap();
515
516        // Cycle 1
517        edb.encrypt().unwrap();
518        edb.decrypt().unwrap();
519        assert_eq!(edb.decrypted(), original);
520
521        // Cycle 2
522        edb.encrypt().unwrap();
523        edb.decrypt().unwrap();
524        assert_eq!(edb.decrypted(), original);
525    }
526
527    #[test]
528    fn test_large_data() {
529        let large_data = vec![42u8; 10000];
530        let (_dir, path) = setup_test_dir();
531
532        let edb = EncryptedDB::from_decrypted(
533            large_data.clone(),
534            path.to_str().unwrap().to_string(),
535            "pwd".to_string(),
536            "salt".to_string(),
537        )
538        .unwrap();
539
540        assert_eq!(edb.decrypted(), large_data);
541        assert!(
542            edb.bytes().len() > large_data.len(),
543            "Encrypted should be larger"
544        );
545    }
546}