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 let encrypted = crypto::encrypt(data.clone(), pwd.clone(), salt.clone()).unwrap();
189
190 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 let encrypted = crypto::encrypt(data.clone(), pwd.clone(), salt.clone()).unwrap();
230 fs::write(&path, encrypted).unwrap();
231
232 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 let encrypted = crypto::encrypt(data.clone(), pwd.clone(), salt.clone()).unwrap();
296 fs::write(&path, encrypted).unwrap();
297
298 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 edb.decrypt().unwrap();
411 assert_eq!(edb.decrypted(), original);
412
413 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 let edb =
428 EncryptedDB::from_decrypted(data.clone(), path_str.clone(), pwd.clone(), salt.clone())
429 .unwrap();
430
431 edb.write().unwrap();
432
433 assert!(path.exists(), "File should exist after write");
435
436 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 let encrypted = crypto::encrypt(data.clone(), pwd.clone(), salt.clone()).unwrap();
468 fs::write(&path, encrypted.clone()).unwrap();
469
470 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], 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 edb.encrypt().unwrap();
518 edb.decrypt().unwrap();
519 assert_eq!(edb.decrypted(), original);
520
521 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}