1#![doc = include_str!("../README.md")]
2#![warn(
3 clippy::all,
4 clippy::pedantic,
5 rust_2018_idioms,
6 missing_docs,
7 clippy::missing_docs_in_private_items
8)]
9#![allow(
10 clippy::option_if_let_else,
11 clippy::module_name_repetitions,
12 clippy::shadow_unrelated,
13 clippy::must_use_candidate,
14 clippy::implicit_hasher
15)]
16#![doc(
17 html_logo_url = "https://gitlab.com/rust-community-matrix/snapper/-/raw/trunk/static/snapper.png"
18)]
19
20use std::{
21 collections::HashMap,
22 fs::{create_dir_all, File},
23 io::Write,
24 path::{Path, PathBuf},
25 sync::Arc,
26};
27
28use serde::{de::DeserializeOwned, Serialize};
29use snafu::{ensure, OptionExt, ResultExt};
30use tracing::{info, instrument};
31
32use crate::{
33 crypto::{DerivedKey, EncryptedRootKey, RootKey},
34 entries::{Control, Namespace, Namespaces, Settings},
35 error::{
36 CryptoBoxError, DirectoryAlreadyExists, DirectoryDoesNotExist, FailedCreatingDirectory,
37 Fetch, MissingConfiguration, MissingNamespaceDirectory, NamespaceOpen, RootKeyDecryption,
38 RootKeyEncryption, RootKeyIO, RootKeySerial, RootNamespaceInit, RootNamespaceOpen, Store,
39 },
40 file::LsmFile,
41};
42
43#[cfg(feature = "experimental-async")]
44pub mod async_wrapper;
45pub mod crypto;
46mod entries;
47pub mod error;
48pub mod file;
49
50pub struct CryptoBox {
54 path: PathBuf,
56 root_key: Arc<RootKey>,
58 root_namespace: LsmFile<File, RootKey>,
60 compression: Option<i32>,
62 max_cache_entries: Option<usize>,
64 namespaces: HashMap<String, (Namespace, LsmFile<File, DerivedKey>)>,
66}
67
68impl CryptoBox {
69 #[instrument(skip(path, password), err)]
96 pub fn init(
97 path: impl AsRef<Path>,
98 compression: Option<i32>,
99 max_cache_entries: Option<usize>,
100 password: impl AsRef<[u8]>,
101 ) -> Result<Self, CryptoBoxError> {
102 let path = path.as_ref();
103 info!(?path, "Creating CryptoBox");
104 let password = password.as_ref();
105 ensure!(
107 !path.exists(),
108 DirectoryAlreadyExists {
109 directory: format!("{:?}", path)
110 }
111 );
112 create_dir_all(path).context(FailedCreatingDirectory {
114 directory: format!("{:?}", path),
115 })?;
116 let namespaces_path = path.join("namespaces");
118 create_dir_all(&namespaces_path).context(FailedCreatingDirectory {
119 directory: format!("{:?}", namespaces_path),
120 })?;
121 let root_key = Arc::new(RootKey::random());
123 let encrypted_root_key = root_key.encrypt(password).context(RootKeyEncryption)?;
125 let root_key_path = path.join("KEY");
126 let mut key_file = File::create(&root_key_path).context(RootKeyIO {
127 path: format!("{:?}", root_key_path),
128 })?;
129 serde_cbor::to_writer(&mut key_file, &encrypted_root_key).context(RootKeySerial)?;
130 key_file.flush().context(RootKeyIO {
131 path: format!("{:?}", root_key_path),
132 })?;
133 std::mem::drop(key_file);
134 let root_namespace_path = path.join("root");
136 let mut root_namespace =
137 LsmFile::create(&root_namespace_path, None, root_key.clone(), Some(0)).context(
138 RootNamespaceInit {
139 path: format!("{:?}", root_namespace_path),
140 },
141 )?;
142 root_namespace
144 .insert(
145 &"",
146 &Control::Settings(Settings {
147 compression,
148 max_cache_entries,
149 }),
150 )
151 .context(RootNamespaceInit {
152 path: format!("{:?}", root_namespace_path),
153 })?;
154 root_namespace
156 .insert(
157 &"namespaces",
158 &Control::Namespaces(Namespaces { namespaces: vec![] }),
159 )
160 .context(RootNamespaceInit {
161 path: format!("{:?}", root_namespace_path),
162 })?;
163 root_namespace.flush().context(RootNamespaceInit {
164 path: format!("{:?}", root_namespace_path),
165 })?;
166
167 Ok(CryptoBox {
168 path: path.to_path_buf(),
169 root_key,
170 root_namespace,
171 compression,
172 namespaces: HashMap::new(),
173 max_cache_entries,
174 })
175 }
176
177 #[instrument(skip(path, password), err)]
199 pub fn open(
200 path: impl AsRef<Path>,
201 password: impl AsRef<[u8]>,
202 ) -> Result<Self, CryptoBoxError> {
203 let path = path.as_ref().to_path_buf();
204 let password = password.as_ref();
205 info!(?path, "Opening CryptoBox");
206 ensure!(
208 path.exists() && path.is_dir(),
209 DirectoryDoesNotExist {
210 path: format!("{:?}", path)
211 }
212 );
213 let root_key_path = path.join("KEY");
215 let mut root_key_file = File::open(&root_key_path).context(RootKeyIO {
216 path: format!("{:?}", root_key_path),
217 })?;
218 let enc_root_key: EncryptedRootKey =
219 serde_cbor::from_reader(&mut root_key_file).context(RootKeySerial)?;
220 let root_key = Arc::new(enc_root_key.decrypt(password).context(RootKeyDecryption)?);
221 std::mem::drop(root_key_file);
223 let root_namespace_path = path.join("root");
225 let mut root_namespace =
226 LsmFile::open(&root_namespace_path, None, root_key.clone(), Some(0)).context(
227 RootNamespaceOpen {
228 path: format!("{:?}", root_namespace_path),
229 },
230 )?;
231 ensure!(path.join("namespaces").exists(), MissingNamespaceDirectory);
233 if let Control::Settings(settings) = root_namespace
235 .get(&"")
236 .ok()
237 .context(MissingConfiguration)?
238 .context(MissingConfiguration)?
239 {
240 if let Control::Namespaces(namespaces_raw) = root_namespace
242 .get(&"namespaces")
243 .ok()
244 .context(MissingConfiguration)?
245 .context(MissingConfiguration)?
246 {
247 let mut namespaces = HashMap::new();
248 for namespace in namespaces_raw.namespaces {
249 let name = namespace.name.clone();
250 let path = path.join("namespaces").join(namespace.uuid.to_string());
251 let lsm_file = LsmFile::open(
252 &path,
253 settings.compression,
254 namespace.key.clone(),
255 settings.max_cache_entries,
256 )
257 .context(NamespaceOpen { name: name.clone() })?;
258 namespaces.insert(name, (namespace, lsm_file));
259 }
260 Ok(CryptoBox {
261 path,
262 root_key,
263 root_namespace,
264 compression: settings.compression,
265 namespaces,
266 max_cache_entries: settings.max_cache_entries,
267 })
268 } else {
269 Err(CryptoBoxError::MissingConfiguration)
270 }
271 } else {
272 Err(CryptoBoxError::MissingConfiguration)
273 }
274 }
275
276 pub fn namespace_exists(&self, name: &str) -> bool {
278 self.namespaces.contains_key(name)
279 }
280
281 pub fn namespaces(&self) -> Vec<String> {
283 self.namespaces.keys().cloned().collect()
284 }
285
286 #[instrument(skip(self), err)]
298 pub fn create_namespace(&mut self, name: String) -> Result<(), CryptoBoxError> {
299 if self.namespace_exists(&name) {
300 Ok(())
301 } else {
302 info!("Creating namespace");
303 let derived_key = Arc::new(self.root_key.derive(&name));
304 let uuid = uuid::Uuid::new_v4();
305 let path = self.path.join("namespaces").join(uuid.to_string());
306 let lsm_file = LsmFile::create(
308 &path,
309 self.compression,
310 derived_key.clone(),
311 self.max_cache_entries,
312 )
313 .context(NamespaceOpen { name: name.clone() })?;
314 let namespace = Namespace {
316 name: name.clone(),
317 key: derived_key,
318 uuid,
319 };
320 self.namespaces.insert(name.clone(), (namespace, lsm_file));
322 let namespaces: Vec<_> = self.namespaces.values().map(|(x, _)| x.clone()).collect();
324 let namespaces = Control::Namespaces(Namespaces { namespaces });
325 self.root_namespace
326 .insert(&"namespaces", &namespaces)
327 .context(NamespaceOpen { name: name.clone() })?;
328 self.root_namespace
329 .flush()
330 .context(NamespaceOpen { name })?;
331 Ok(())
332 }
333 }
334
335 #[instrument(skip(self, key, namespace), err)]
347 pub fn get<K, V>(&mut self, key: &K, namespace: &str) -> Result<Option<V>, CryptoBoxError>
348 where
349 K: Serialize,
350 V: DeserializeOwned,
351 {
352 if let Some((_, lsm)) = self.namespaces.get_mut(namespace) {
353 lsm.get(key).context(Fetch)
355 } else {
356 Err(CryptoBoxError::NoSuchNamespace {
357 name: namespace.to_string(),
358 })
359 }
360 }
361
362 #[instrument(skip(self, key), err)]
372 pub fn get_root<K, V>(&mut self, key: &K) -> Result<Option<V>, CryptoBoxError>
373 where
374 K: Serialize,
375 V: DeserializeOwned,
376 {
377 self.root_namespace.get(key).context(Fetch)
378 }
379
380 #[instrument(skip(self, key, value, namespace), err)]
393 pub fn insert<K, V>(
394 &mut self,
395 key: &K,
396 value: &V,
397 namespace: &str,
398 ) -> Result<(), CryptoBoxError>
399 where
400 K: Serialize,
401 V: Serialize,
402 {
403 if let Some((_, lsm)) = self.namespaces.get_mut(namespace) {
404 lsm.insert(key, value).context(Store)
406 } else {
407 Err(CryptoBoxError::NoSuchNamespace {
408 name: namespace.to_string(),
409 })
410 }
411 }
412
413 #[instrument(skip(self, key, value), err)]
424 pub fn insert_root<K, V>(&mut self, key: &K, value: &V) -> Result<(), CryptoBoxError>
425 where
426 K: Serialize,
427 V: Serialize,
428 {
429 self.root_namespace.insert(key, value).context(Store)
430 }
431
432 #[instrument(skip(self, key, namespace), err)]
444 pub fn contains_key<K, V>(&mut self, key: &K, namespace: &str) -> Result<bool, CryptoBoxError>
445 where
446 K: Serialize,
447 V: DeserializeOwned,
448 {
449 let res = self.get::<K, V>(key, namespace)?;
450 Ok(res.is_some())
451 }
452
453 #[instrument(skip(self))]
459 pub fn flush(&mut self) -> Result<(), CryptoBoxError> {
460 let mut errors = vec![];
461 let res = self.root_namespace.flush();
463 if let Err(e) = res {
464 errors.push((None, e));
465 }
466 for (name, (_, lsm)) in &mut self.namespaces {
468 if let Err(e) = lsm.flush() {
469 errors.push((Some(name.clone()), e));
470 }
471 }
472
473 if errors.is_empty() {
474 Ok(())
475 } else {
476 Err(CryptoBoxError::Flush { sources: errors })
477 }
478 }
479
480 pub fn to_hashmap<K, V>(&mut self, namespace: &str) -> Result<HashMap<K, V>, CryptoBoxError>
486 where
487 K: DeserializeOwned + Serialize + std::hash::Hash + Eq,
488 V: DeserializeOwned,
489 {
490 if let Some((_, lsm)) = self.namespaces.get_mut(namespace) {
491 lsm.to_hashmap().context(Fetch)
493 } else {
494 Err(CryptoBoxError::NoSuchNamespace {
495 name: namespace.to_string(),
496 })
497 }
498 }
499
500 pub fn to_pairs<K, V>(&mut self, namespace: &str) -> Result<Vec<(K, V)>, CryptoBoxError>
506 where
507 K: DeserializeOwned + Serialize + Eq + Clone,
508 V: DeserializeOwned + Clone,
509 {
510 if let Some((_, lsm)) = self.namespaces.get_mut(namespace) {
511 lsm.to_pairs().context(Fetch)
513 } else {
514 Err(CryptoBoxError::NoSuchNamespace {
515 name: namespace.to_string(),
516 })
517 }
518 }
519
520 pub fn root_to_pairs<K, V>(&mut self) -> Result<Vec<(K, V)>, CryptoBoxError>
526 where
527 K: DeserializeOwned + Serialize + Eq + Clone,
528 V: DeserializeOwned + Clone,
529 {
530 self.root_namespace.to_pairs().context(Fetch)
532 }
533}
534
535#[cfg(test)]
537mod tests {
538 use super::*;
539 use std::collections::HashSet;
540 use tempfile::tempdir;
541 mod init {
543 use super::*;
544 #[test]
546 fn directory_layout() -> Result<(), CryptoBoxError> {
547 let tempdir = tempdir().context(FailedCreatingDirectory {
548 directory: "tempdir".to_string(),
549 })?;
550 let path = tempdir.path().join("box");
551 let _crypto_box = CryptoBox::init(&path, None, None, "testing")?;
552 assert!(path.join("KEY").exists());
553 assert!(path.join("root").exists());
554 assert!(path.join("namespaces").exists());
555 Ok(())
556 }
557 #[test]
559 fn init_open() -> Result<(), CryptoBoxError> {
560 let tempdir = tempdir().context(FailedCreatingDirectory {
561 directory: "tempdir".to_string(),
562 })?;
563 let path = tempdir.path().join("box");
564 let crypto_box = CryptoBox::init(&path, None, None, "testing")?;
566 std::mem::drop(crypto_box);
568 let _crypto_box = CryptoBox::open(&path, "testing")?;
570 Ok(())
571 }
572 #[test]
574 fn namespaces() -> Result<(), CryptoBoxError> {
575 let namespace_names = ["one", "two", "three"]
576 .into_iter()
577 .map(std::string::ToString::to_string)
578 .collect::<HashSet<_>>();
579 let tempdir = tempdir().context(FailedCreatingDirectory {
580 directory: "tempdir".to_string(),
581 })?;
582 let path = tempdir.path().join("box");
583 let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
585 for namespace in &namespace_names {
587 crypto_box.create_namespace(namespace.to_string())?;
588 }
589 std::mem::drop(crypto_box);
591 let crypto_box = CryptoBox::open(&path, "testing")?;
593 assert_eq!(
595 namespace_names,
596 crypto_box.namespaces().into_iter().collect::<HashSet<_>>()
597 );
598
599 Ok(())
600 }
601 }
602 mod box_smoke {
604 use super::*;
605 #[test]
609 fn basic_insertions() -> Result<(), CryptoBoxError> {
610 let tempdir = tempdir().context(FailedCreatingDirectory {
611 directory: "tempdir".to_string(),
612 })?;
613 let path = tempdir.path().join("box");
614 let pairs = [(1, 2), (3, 4), (5, 6)];
615 let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
617 crypto_box.create_namespace("".to_string())?;
619 let namespace = "";
621 for (key, value) in &pairs {
622 crypto_box.insert(key, value, namespace)?;
623 }
624 for (key, value) in &pairs {
626 let res = crypto_box.get(key, namespace)?;
627 if Some(*value) != res {
628 panic!("Unable to retrieve pair k: {} v: {}", key, value);
629 }
630 }
631 Ok(())
632 }
633 #[test]
637 fn basic_insertions_flush() -> Result<(), CryptoBoxError> {
638 let tempdir = tempdir().context(FailedCreatingDirectory {
639 directory: "tempdir".to_string(),
640 })?;
641 let path = tempdir.path().join("box");
642 let pairs = [(1, 2), (3, 4), (5, 6)];
643 let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
645 crypto_box.create_namespace("".to_string())?;
647 let namespace = "";
649 for (key, value) in &pairs {
650 crypto_box.insert(key, value, namespace)?;
651 }
652 crypto_box.flush()?;
654 std::mem::drop(crypto_box);
655 let mut crypto_box = CryptoBox::open(&path, "testing")?;
657 for (key, value) in &pairs {
659 let res = crypto_box.get(key, namespace)?;
660 if Some(*value) != res {
661 panic!("Unable to retrieve pair k: {} v: {}", key, value);
662 }
663 }
664 Ok(())
665 }
666 #[test]
668 fn basic_insertions_hashmap() -> Result<(), CryptoBoxError> {
669 let tempdir = tempdir().context(FailedCreatingDirectory {
670 directory: "tempdir".to_string(),
671 })?;
672 let path = tempdir.path().join("box");
673 let pairs = [(1, 2), (3, 4), (5, 6)];
674 let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
676 crypto_box.create_namespace("".to_string())?;
678 let namespace = "";
680 for (key, value) in &pairs {
681 crypto_box.insert(key, value, namespace)?;
682 }
683 assert_eq!(
684 crypto_box.to_hashmap("")?,
685 pairs.into_iter().collect::<HashMap<_, _>>()
686 );
687 Ok(())
688 }
689 #[test]
691 fn basic_insertions_pairs() -> Result<(), CryptoBoxError> {
692 let tempdir = tempdir().context(FailedCreatingDirectory {
693 directory: "tempdir".to_string(),
694 })?;
695 let path = tempdir.path().join("box");
696 let pairs = [(1, 2), (3, 4), (5, 6)];
697 let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
699 crypto_box.create_namespace("".to_string())?;
701 let namespace = "";
703 for (key, value) in &pairs {
704 crypto_box.insert(key, value, namespace)?;
705 }
706 let comparison = pairs.into_iter().collect::<HashMap<i32, i32>>();
707 assert_eq!(crypto_box.to_hashmap("")?, comparison,);
708 assert_eq!(
709 crypto_box
710 .to_pairs::<i32, i32>("")?
711 .into_iter()
712 .collect::<HashMap<_, _>>(),
713 comparison
714 );
715 Ok(())
716 }
717 }
718 mod failures {
720 use super::*;
721 #[test]
722 fn bad_password() -> Result<(), CryptoBoxError> {
723 let tempdir = tempdir().context(FailedCreatingDirectory {
724 directory: "tempdir".to_string(),
725 })?;
726 let path = tempdir.path().join("box");
727 let crypto_box = CryptoBox::init(&path, None, None, "testing")?;
729 std::mem::drop(crypto_box);
731 let crypto_box = CryptoBox::open(&path, "testing 2");
733 assert!(matches!(
734 crypto_box,
735 Err(CryptoBoxError::RootKeyDecryption { .. })
736 ));
737 Ok(())
738 }
739 }
740}