snapper-box 0.0.4

Cryptographic storage for snapper
Documentation
#![doc = include_str!("../README.md")]
#![warn(
    clippy::all,
    clippy::pedantic,
    rust_2018_idioms,
    missing_docs,
    clippy::missing_docs_in_private_items
)]
#![allow(
    clippy::option_if_let_else,
    clippy::module_name_repetitions,
    clippy::shadow_unrelated,
    clippy::must_use_candidate,
    clippy::implicit_hasher
)]
#![doc(
    html_logo_url = "https://gitlab.com/rust-community-matrix/snapper/-/raw/trunk/static/snapper.png"
)]

use std::{
    collections::HashMap,
    fs::{create_dir_all, File},
    io::Write,
    path::{Path, PathBuf},
    sync::Arc,
};

use serde::{de::DeserializeOwned, Serialize};
use snafu::{ensure, OptionExt, ResultExt};
use tracing::{info, instrument};

use crate::{
    crypto::{DerivedKey, EncryptedRootKey, RootKey},
    entries::{Control, Namespace, Namespaces, Settings},
    error::{
        CryptoBoxError, DirectoryAlreadyExists, DirectoryDoesNotExist, FailedCreatingDirectory,
        Fetch, MissingConfiguration, MissingNamespaceDirectory, NamespaceOpen, RootKeyDecryption,
        RootKeyEncryption, RootKeyIO, RootKeySerial, RootNamespaceInit, RootNamespaceOpen, Store,
    },
    file::LsmFile,
};

#[cfg(feature = "experimental-async")]
pub mod async_wrapper;
pub mod crypto;
mod entries;
pub mod error;
pub mod file;

/// The `CryptoBox` encrypted, namespaced document store
///
/// See module level documentation for more information.
pub struct CryptoBox {
    /// The path for this CryptoBox
    path: PathBuf,
    /// The root key for this `CryptoBox`
    root_key: Arc<RootKey>,
    /// The root namespace for this `CryptoBox`
    root_namespace: LsmFile<File, RootKey>,
    /// Compression level for namespaces in this `CryptoBox`
    compression: Option<i32>,
    /// Maximum cache entries per namespace
    max_cache_entries: Option<usize>,
    /// Namespaces in this CryptoBox
    namespaces: HashMap<String, (Namespace, LsmFile<File, DerivedKey>)>,
}

impl CryptoBox {
    /// Initialize a new `CryptoBox`
    ///
    /// # Arguments
    ///
    ///   * `path`
    ///
    ///     The desired path to the `CryptoBox` directory. This must be a directory that the user
    ///     has write permission to, and _does not_ already exist.
    ///   * `compression`
    ///
    ///     The desired zstd compression level for this `CryptoBox`. Defaults to no compression.
    ///   * `max_cache_entries`
    ///
    ///     The desired maximum number of write-cache entries before a flush automatically occurs.
    ///     Defaults to 100.
    ///   * `password`
    ///
    ///     The password to encrypt the root string with. This is specified as a byte slice for
    ///     flexibility.
    ///
    ///
    ///
    /// # Errors
    ///   * [`CryptoBoxError::DirectoryAlreadyExists`] if the desired path already exists
    ///   * [`CryptoBoxError::FailedCreatingDirectory`], [`CryptoBoxError::RootKeyIO`], or
    ///     [`CryptoBoxError::RootNamespaceInit`] if an IO error creating the `CryptoBox` occurs
    #[instrument(skip(path, password), err)]
    pub fn init(
        path: impl AsRef<Path>,
        compression: Option<i32>,
        max_cache_entries: Option<usize>,
        password: impl AsRef<[u8]>,
    ) -> Result<Self, CryptoBoxError> {
        let path = path.as_ref();
        info!(?path, "Creating CryptoBox");
        let password = password.as_ref();
        // Bailout if the directory already exists
        ensure!(
            !path.exists(),
            DirectoryAlreadyExists {
                directory: format!("{:?}", path)
            }
        );
        // Create the directory
        create_dir_all(path).context(FailedCreatingDirectory {
            directory: format!("{:?}", path),
        })?;
        // Create the namespaces directory
        let namespaces_path = path.join("namespaces");
        create_dir_all(&namespaces_path).context(FailedCreatingDirectory {
            directory: format!("{:?}", namespaces_path),
        })?;
        // Generate the root key
        let root_key = Arc::new(RootKey::random());
        // Encrypt and write the key
        let encrypted_root_key = root_key.encrypt(password).context(RootKeyEncryption)?;
        let root_key_path = path.join("KEY");
        let mut key_file = File::create(&root_key_path).context(RootKeyIO {
            path: format!("{:?}", root_key_path),
        })?;
        serde_cbor::to_writer(&mut key_file, &encrypted_root_key).context(RootKeySerial)?;
        key_file.flush().context(RootKeyIO {
            path: format!("{:?}", root_key_path),
        })?;
        std::mem::drop(key_file);
        // Create root namespace
        let root_namespace_path = path.join("root");
        let mut root_namespace =
            LsmFile::create(&root_namespace_path, None, root_key.clone(), Some(0)).context(
                RootNamespaceInit {
                    path: format!("{:?}", root_namespace_path),
                },
            )?;
        // Write the init
        root_namespace
            .insert(
                &"",
                &Control::Settings(Settings {
                    compression,
                    max_cache_entries,
                }),
            )
            .context(RootNamespaceInit {
                path: format!("{:?}", root_namespace_path),
            })?;
        // Write the namespaces record
        root_namespace
            .insert(
                &"namespaces",
                &Control::Namespaces(Namespaces { namespaces: vec![] }),
            )
            .context(RootNamespaceInit {
                path: format!("{:?}", root_namespace_path),
            })?;
        root_namespace.flush().context(RootNamespaceInit {
            path: format!("{:?}", root_namespace_path),
        })?;

        Ok(CryptoBox {
            path: path.to_path_buf(),
            root_key,
            root_namespace,
            compression,
            namespaces: HashMap::new(),
            max_cache_entries,
        })
    }

    /// Opens an existing `CryptoBox`
    ///
    /// Will read the default compression and cache settings from the root namespace.
    ///
    /// # Arguments
    ///
    ///   * `path`
    ///
    ///     The path to the `CryptoBox` directory.
    ///   * `password`
    ///
    ///     The password to encrypt the root string with. This is specified as a byte slice for
    ///     flexibility.
    ///
    /// # Errors
    ///
    ///   * [`CryptoBoxError::DirectoryDoesNotExist`] if the directory does not exist
    ///   * [`CryptoBoxError::RootKeyDecryption`] if the root key fails to decrypt
    ///   * [`CryptoBoxError::MissingConfiguration`] if the configuration control structure is missing
    ///   * [`CryptoBoxError::RootKeyIO`], [`CryptoBoxError::MissingNamespaceDirectory`], or
    ///     [`CryptoBoxError::RootNamespaceOpen`] if something goes wrong with IO while opening
    #[instrument(skip(path, password), err)]
    pub fn open(
        path: impl AsRef<Path>,
        password: impl AsRef<[u8]>,
    ) -> Result<Self, CryptoBoxError> {
        let path = path.as_ref().to_path_buf();
        let password = password.as_ref();
        info!(?path, "Opening CryptoBox");
        // Make sure the directory exists
        ensure!(
            path.exists() && path.is_dir(),
            DirectoryDoesNotExist {
                path: format!("{:?}", path)
            }
        );
        // Open up the root key and decrypt it
        let root_key_path = path.join("KEY");
        let mut root_key_file = File::open(&root_key_path).context(RootKeyIO {
            path: format!("{:?}", root_key_path),
        })?;
        let enc_root_key: EncryptedRootKey =
            serde_cbor::from_reader(&mut root_key_file).context(RootKeySerial)?;
        let root_key = Arc::new(enc_root_key.decrypt(password).context(RootKeyDecryption)?);
        // Close the file
        std::mem::drop(root_key_file);
        // Open the root namespace
        let root_namespace_path = path.join("root");
        let mut root_namespace =
            LsmFile::open(&root_namespace_path, None, root_key.clone(), Some(0)).context(
                RootNamespaceOpen {
                    path: format!("{:?}", root_namespace_path),
                },
            )?;
        // ensure the namespaces directory exists
        ensure!(path.join("namespaces").exists(), MissingNamespaceDirectory);
        // Get the configuration
        if let Control::Settings(settings) = root_namespace
            .get(&"")
            .ok()
            .context(MissingConfiguration)?
            .context(MissingConfiguration)?
        {
            // Get the namespaces
            if let Control::Namespaces(namespaces_raw) = root_namespace
                .get(&"namespaces")
                .ok()
                .context(MissingConfiguration)?
                .context(MissingConfiguration)?
            {
                let mut namespaces = HashMap::new();
                for namespace in namespaces_raw.namespaces {
                    let name = namespace.name.clone();
                    let path = path.join("namespaces").join(namespace.uuid.to_string());
                    let lsm_file = LsmFile::open(
                        &path,
                        settings.compression,
                        namespace.key.clone(),
                        settings.max_cache_entries,
                    )
                    .context(NamespaceOpen { name: name.clone() })?;
                    namespaces.insert(name, (namespace, lsm_file));
                }
                Ok(CryptoBox {
                    path,
                    root_key,
                    root_namespace,
                    compression: settings.compression,
                    namespaces,
                    max_cache_entries: settings.max_cache_entries,
                })
            } else {
                Err(CryptoBoxError::MissingConfiguration)
            }
        } else {
            Err(CryptoBoxError::MissingConfiguration)
        }
    }

    /// Check to see if a namespace exists
    pub fn namespace_exists(&self, name: &str) -> bool {
        self.namespaces.contains_key(name)
    }

    /// Get the list of namespaces
    pub fn namespaces(&self) -> Vec<String> {
        self.namespaces.keys().cloned().collect()
    }

    /// Create a namespace
    ///
    /// This method will create the namespace with the given name. In the event that the namespace already
    /// exists, this method will sort circuit with `Ok()`.
    ///
    /// Generates a [`Uuid`](uuid::Uuid) and a derived key for the namespace automatically.
    ///
    /// # Errors
    ///
    /// Will return [`CryptoBoxError::NamespaceOpen`] if any underlying errors happen while initializing
    /// the new name space.
    #[instrument(skip(self), err)]
    pub fn create_namespace(&mut self, name: String) -> Result<(), CryptoBoxError> {
        if self.namespace_exists(&name) {
            Ok(())
        } else {
            info!("Creating namespace");
            let derived_key = Arc::new(self.root_key.derive(&name));
            let uuid = uuid::Uuid::new_v4();
            let path = self.path.join("namespaces").join(uuid.to_string());
            // Init lsm file
            let lsm_file = LsmFile::create(
                &path,
                self.compression,
                derived_key.clone(),
                self.max_cache_entries,
            )
            .context(NamespaceOpen { name: name.clone() })?;
            // Build the namespace
            let namespace = Namespace {
                name: name.clone(),
                key: derived_key,
                uuid,
            };
            // Add it to our local store
            self.namespaces.insert(name.clone(), (namespace, lsm_file));
            // Update the namespaces entry
            let namespaces: Vec<_> = self.namespaces.values().map(|(x, _)| x.clone()).collect();
            let namespaces = Control::Namespaces(Namespaces { namespaces });
            self.root_namespace
                .insert(&"namespaces", &namespaces)
                .context(NamespaceOpen { name: name.clone() })?;
            self.root_namespace
                .flush()
                .context(NamespaceOpen { name })?;
            Ok(())
        }
    }

    /// Gets the specified item from the specified namespace, if it exists
    ///
    /// # Arguments
    ///
    ///   * `key` - Key for the key/value pair to be retrieved
    ///   * `namespace` - The namespace the item was from
    ///
    /// # Errors
    ///
    ///   * [`CryptoBoxError::NoSuchNamespace`] if the namespace does not exist
    ///   * [`CryptoBoxError::Fetch`] if an underlying error occurs
    #[instrument(skip(self, key, namespace), err)]
    pub fn get<K, V>(&mut self, key: &K, namespace: &str) -> Result<Option<V>, CryptoBoxError>
    where
        K: Serialize,
        V: DeserializeOwned,
    {
        if let Some((_, lsm)) = self.namespaces.get_mut(namespace) {
            // Try to get the item from the lsm
            lsm.get(key).context(Fetch)
        } else {
            Err(CryptoBoxError::NoSuchNamespace {
                name: namespace.to_string(),
            })
        }
    }

    /// Gets the specified item from the root namespace, if it exists
    ///
    /// # Arguments
    ///
    ///   * `key` - Key for the key/value pair to be retrieved
    ///
    /// # Errors
    ///
    ///   * [`CryptoBoxError::Fetch`] if an underlying error occurs
    #[instrument(skip(self, key), err)]
    pub fn get_root<K, V>(&mut self, key: &K) -> Result<Option<V>, CryptoBoxError>
    where
        K: Serialize,
        V: DeserializeOwned,
    {
        self.root_namespace.get(key).context(Fetch)
    }

    /// Stores the specified item from the specified namespace
    ///
    /// # Arguments
    ///
    ///   * `key` - Key for the key/value pair to be stored
    ///   * `value` - Value for the key/value pair to be stored
    ///   * `namespace` - The namespace the item will be stored in
    ///
    /// # Errors
    ///
    ///   * [`CryptoBoxError::NoSuchNamespace`] if the namespace does not exist
    ///   * [`CryptoBoxError::Store`] if an underlying error occurs
    #[instrument(skip(self, key, value, namespace), err)]
    pub fn insert<K, V>(
        &mut self,
        key: &K,
        value: &V,
        namespace: &str,
    ) -> Result<(), CryptoBoxError>
    where
        K: Serialize,
        V: Serialize,
    {
        if let Some((_, lsm)) = self.namespaces.get_mut(namespace) {
            // Store the key in the lsmfile
            lsm.insert(key, value).context(Store)
        } else {
            Err(CryptoBoxError::NoSuchNamespace {
                name: namespace.to_string(),
            })
        }
    }

    /// Stores the specified item in the root namespace
    ///
    /// # Arguments
    ///
    ///   * `key` - Key for the key/value pair to be stored
    ///   * `value` - Value for the key/value pair to be stored
    ///
    /// # Errors
    ///
    ///   * [`CryptoBoxError::Store`] if an underlying error occurs
    #[instrument(skip(self, key, value), err)]
    pub fn insert_root<K, V>(&mut self, key: &K, value: &V) -> Result<(), CryptoBoxError>
    where
        K: Serialize,
        V: Serialize,
    {
        self.root_namespace.insert(key, value).context(Store)
    }

    /// Returns true if the specified namespace contains the provided key
    ///
    /// # Arguments
    ///
    ///   * `key` - Key for the key/value pair to be retrieved
    ///   * `namespace` - The namespace the item was from
    ///
    /// # Errors
    ///
    ///   * [`CryptoBoxError::NoSuchNamespace`] if the namespace does not exist
    ///   * [`CryptoBoxError::Fetch`] if an underlying error occurs
    #[instrument(skip(self, key, namespace), err)]
    pub fn contains_key<K, V>(&mut self, key: &K, namespace: &str) -> Result<bool, CryptoBoxError>
    where
        K: Serialize,
        V: DeserializeOwned,
    {
        let res = self.get::<K, V>(key, namespace)?;
        Ok(res.is_some())
    }

    /// Will flush all of the namespaces in the `CryptoBox`, including the root namespace.
    ///
    /// # Errors
    ///
    /// Will return [`CryptoBoxError::Flush`] if any underlying errors occur.
    #[instrument(skip(self))]
    pub fn flush(&mut self) -> Result<(), CryptoBoxError> {
        let mut errors = vec![];
        // First try the root namespace
        let res = self.root_namespace.flush();
        if let Err(e) = res {
            errors.push((None, e));
        }
        // Now try all the namespaces
        for (name, (_, lsm)) in &mut self.namespaces {
            if let Err(e) = lsm.flush() {
                errors.push((Some(name.clone()), e));
            }
        }

        if errors.is_empty() {
            Ok(())
        } else {
            Err(CryptoBoxError::Flush { sources: errors })
        }
    }

    /// Gets all the key/value pairs in a namespace as [`HashMap`]
    ///
    /// # Errors
    ///
    /// Will return [`CryptoBoxError::Fetch`] if there are any underlying errors.
    pub fn to_hashmap<K, V>(&mut self, namespace: &str) -> Result<HashMap<K, V>, CryptoBoxError>
    where
        K: DeserializeOwned + Serialize + std::hash::Hash + Eq,
        V: DeserializeOwned,
    {
        if let Some((_, lsm)) = self.namespaces.get_mut(namespace) {
            // Store the key in the lsmfile
            lsm.to_hashmap().context(Fetch)
        } else {
            Err(CryptoBoxError::NoSuchNamespace {
                name: namespace.to_string(),
            })
        }
    }

    /// Gets all the key/value pairs in a namespace as [`Vec`] of pairs
    ///
    /// # Errors
    ///
    /// Will return [`CryptoBoxError::Fetch`] if there are any underlying errors.
    pub fn to_pairs<K, V>(&mut self, namespace: &str) -> Result<Vec<(K, V)>, CryptoBoxError>
    where
        K: DeserializeOwned + Serialize + Eq + Clone,
        V: DeserializeOwned + Clone,
    {
        if let Some((_, lsm)) = self.namespaces.get_mut(namespace) {
            // Store the key in the lsmfile
            lsm.to_pairs().context(Fetch)
        } else {
            Err(CryptoBoxError::NoSuchNamespace {
                name: namespace.to_string(),
            })
        }
    }

    /// Gets all the key/value pairs in the root namespace as a [`Vec`] of pairs
    ///
    /// # Errors
    ///
    /// Will return [`CryptoBoxError::Fetch`] if there are any underlying errors.
    pub fn root_to_pairs<K, V>(&mut self) -> Result<Vec<(K, V)>, CryptoBoxError>
    where
        K: DeserializeOwned + Serialize + Eq + Clone,
        V: DeserializeOwned + Clone,
    {
        // Store the key in the lsmfile
        self.root_namespace.to_pairs().context(Fetch)
    }
}

/// Unit tests for `CryptoBox`
#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashSet;
    use tempfile::tempdir;
    /// initialization
    mod init {
        use super::*;
        /// Make sure it produces the correct files and directories
        #[test]
        fn directory_layout() -> Result<(), CryptoBoxError> {
            let tempdir = tempdir().context(FailedCreatingDirectory {
                directory: "tempdir".to_string(),
            })?;
            let path = tempdir.path().join("box");
            let _crypto_box = CryptoBox::init(&path, None, None, "testing")?;
            assert!(path.join("KEY").exists());
            assert!(path.join("root").exists());
            assert!(path.join("namespaces").exists());
            Ok(())
        }
        /// Create a box and make sure we can open it back up
        #[test]
        fn init_open() -> Result<(), CryptoBoxError> {
            let tempdir = tempdir().context(FailedCreatingDirectory {
                directory: "tempdir".to_string(),
            })?;
            let path = tempdir.path().join("box");
            // Initialize the box
            let crypto_box = CryptoBox::init(&path, None, None, "testing")?;
            // Drop it
            std::mem::drop(crypto_box);
            // Open it back up
            let _crypto_box = CryptoBox::open(&path, "testing")?;
            Ok(())
        }
        /// Create a box, add some namespaces, close it, open it, and verify the name spaces
        #[test]
        fn namespaces() -> Result<(), CryptoBoxError> {
            let namespace_names = ["one", "two", "three"]
                .into_iter()
                .map(std::string::ToString::to_string)
                .collect::<HashSet<_>>();
            let tempdir = tempdir().context(FailedCreatingDirectory {
                directory: "tempdir".to_string(),
            })?;
            let path = tempdir.path().join("box");
            // Initialize the box
            let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
            // Add the namespaces
            for namespace in &namespace_names {
                crypto_box.create_namespace(namespace.to_string())?;
            }
            // Drop it
            std::mem::drop(crypto_box);
            // Open it back up
            let crypto_box = CryptoBox::open(&path, "testing")?;
            // Make sure the namespaces match
            assert_eq!(
                namespace_names,
                crypto_box.namespaces().into_iter().collect::<HashSet<_>>()
            );

            Ok(())
        }
    }
    /// Basic functionality aka smoke testing
    mod box_smoke {
        use super::*;
        /// Test basic insertions
        ///
        /// A handful of pre baked insertions into a single namespace, without flushing or reloading
        #[test]
        fn basic_insertions() -> Result<(), CryptoBoxError> {
            let tempdir = tempdir().context(FailedCreatingDirectory {
                directory: "tempdir".to_string(),
            })?;
            let path = tempdir.path().join("box");
            let pairs = [(1, 2), (3, 4), (5, 6)];
            // Initialize the box
            let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
            // Add the empty string namespace
            crypto_box.create_namespace("".to_string())?;
            // do the insertions
            let namespace = "";
            for (key, value) in &pairs {
                crypto_box.insert(key, value, namespace)?;
            }
            // Do the comparison
            for (key, value) in &pairs {
                let res = crypto_box.get(key, namespace)?;
                if Some(*value) != res {
                    panic!("Unable to retrieve pair k: {} v: {}", key, value);
                }
            }
            Ok(())
        }
        /// Test basic insertions with flush
        ///
        /// A handful of pre baked insertions into a single namespace, with a flush and reload
        #[test]
        fn basic_insertions_flush() -> Result<(), CryptoBoxError> {
            let tempdir = tempdir().context(FailedCreatingDirectory {
                directory: "tempdir".to_string(),
            })?;
            let path = tempdir.path().join("box");
            let pairs = [(1, 2), (3, 4), (5, 6)];
            // Initialize the box
            let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
            // Add the empty string namespace
            crypto_box.create_namespace("".to_string())?;
            // do the insertions
            let namespace = "";
            for (key, value) in &pairs {
                crypto_box.insert(key, value, namespace)?;
            }
            // Flush and drop the box
            crypto_box.flush()?;
            std::mem::drop(crypto_box);
            // Reopen the box
            let mut crypto_box = CryptoBox::open(&path, "testing")?;
            // Do the comparison
            for (key, value) in &pairs {
                let res = crypto_box.get(key, namespace)?;
                if Some(*value) != res {
                    panic!("Unable to retrieve pair k: {} v: {}", key, value);
                }
            }
            Ok(())
        }
        /// Test basic insertions with `HashMap` export
        #[test]
        fn basic_insertions_hashmap() -> Result<(), CryptoBoxError> {
            let tempdir = tempdir().context(FailedCreatingDirectory {
                directory: "tempdir".to_string(),
            })?;
            let path = tempdir.path().join("box");
            let pairs = [(1, 2), (3, 4), (5, 6)];
            // Initialize the box
            let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
            // Add the empty string namespace
            crypto_box.create_namespace("".to_string())?;
            // do the insertions
            let namespace = "";
            for (key, value) in &pairs {
                crypto_box.insert(key, value, namespace)?;
            }
            assert_eq!(
                crypto_box.to_hashmap("")?,
                pairs.into_iter().collect::<HashMap<_, _>>()
            );
            Ok(())
        }
        /// Test basic insertions with `pairs` export
        #[test]
        fn basic_insertions_pairs() -> Result<(), CryptoBoxError> {
            let tempdir = tempdir().context(FailedCreatingDirectory {
                directory: "tempdir".to_string(),
            })?;
            let path = tempdir.path().join("box");
            let pairs = [(1, 2), (3, 4), (5, 6)];
            // Initialize the box
            let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
            // Add the empty string namespace
            crypto_box.create_namespace("".to_string())?;
            // do the insertions
            let namespace = "";
            for (key, value) in &pairs {
                crypto_box.insert(key, value, namespace)?;
            }
            let comparison = pairs.into_iter().collect::<HashMap<i32, i32>>();
            assert_eq!(crypto_box.to_hashmap("")?, comparison,);
            assert_eq!(
                crypto_box
                    .to_pairs::<i32, i32>("")?
                    .into_iter()
                    .collect::<HashMap<_, _>>(),
                comparison
            );
            Ok(())
        }
    }
    /// Test failure modes of the box
    mod failures {
        use super::*;
        #[test]
        fn bad_password() -> Result<(), CryptoBoxError> {
            let tempdir = tempdir().context(FailedCreatingDirectory {
                directory: "tempdir".to_string(),
            })?;
            let path = tempdir.path().join("box");
            // Initialize the box
            let crypto_box = CryptoBox::init(&path, None, None, "testing")?;
            // Drop it
            std::mem::drop(crypto_box);
            // Open it back up with the wrong password
            let crypto_box = CryptoBox::open(&path, "testing 2");
            assert!(matches!(
                crypto_box,
                Err(CryptoBoxError::RootKeyDecryption { .. })
            ));
            Ok(())
        }
    }
}