snapper-box 0.0.4

Cryptographic storage for snapper
Documentation
//! Utility binary for diagnostics on `snapper-box` archives.
#![cfg(feature = "binary")]

use std::{collections::HashMap, path::PathBuf};

use color_eyre::eyre::{eyre, Result, WrapErr};
use structopt::{clap::AppSettings, StructOpt};
use tracing::{debug, instrument};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};

use snapper_box::CryptoBox;

const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
const AFTER_HELP: &str =
    "This is dangerous, experimental software. It is fully entitled to eat your laudry. Use with caution";

#[derive(Debug, StructOpt)]
#[structopt(
    name = "Snapper CryptoBox Diagnostic Utility",
    about = "Utility for exploring and diagnosing snapper-box archives",
    after_help = AFTER_HELP,
    author = AUTHORS,
    global_settings = &[AppSettings::ColoredHelp],
)]
struct Opt {
    /// Location of the repository
    #[structopt(name = "PATH", parse(from_os_str))]
    path: PathBuf,
    /// Password for the repository
    #[structopt(long, short, env = "BOX_PASSWORD", hide_env_values = true)]
    password: String,
    #[structopt(subcommand)]
    cmd: Subcommand,
}

#[derive(Debug, StructOpt)]
enum Subcommand {
    /// Initialize a repository
    #[structopt(
        about = "Creates a CryptoBox",
        after_help = AFTER_HELP,
        author = AUTHORS,
    )]
    Init {
        /// Compression level for the repository.
        ///
        /// Defaults to no compression
        #[structopt(long, short)]
        compression: Option<i32>,
        /// Maximum write cache entries per namespace before an automatic flush is triggered.
        ///
        /// Defaults to 100.
        #[structopt(long, short)]
        max_cache_entries: Option<usize>,
    },
    /// List the namespaces in a repository
    #[structopt(
        about = "List namespaces in a CryptoBox",
        after_help = AFTER_HELP,
        author = AUTHORS,
    )]
    ListNamespace,
    /// Add a namespace to the repository
    #[structopt(
        about = "Adds a namespace to a CryptoBox",
        after_help = AFTER_HELP,
        author = AUTHORS,
    )]
    AddNamespace {
        /// Name of the namespace to add
        #[structopt(name = "NAMESPACE")]
        name: String,
    },
    /// Dump a namespace as JSON
    #[structopt(
        about = "Dumps a CryptoBox namespace to the console as JSON",
        after_help = AFTER_HELP,
        author = AUTHORS,
    )]
    Dump {
        /// Name of the namespace to dump
        #[structopt(name = "NAMESPACE")]
        name: String,
    },
    /// Insert a JSON formatted key/value pair
    #[structopt(
        about = "Inserts a json formatted key/value pair into the specified namespace",
        after_help = AFTER_HELP,
        author = AUTHORS,
    )]
    Insert {
        /// Name of the namespace to modify
        #[structopt(name = "NAMESPACE")]
        name: String,
        /// Key to modify
        #[structopt(name = "KEY")]
        key: String,
        /// Value to set
        #[structopt(name = "VALUE")]
        value: String,
    },
    /// Dump the root namespace
    #[structopt(
        about = "Dumps the CryptoBox root namespace to the console as JSON",
        after_help = AFTER_HELP,
        author = AUTHORS,
    )]
    DumpRoot,
}

#[instrument]
fn main() -> Result<()> {
    // Install error handler
    color_eyre::install()?;
    // Setup tracing
    tracing_subscriber::registry()
        .with(EnvFilter::from_default_env())
        .with(fmt::layer().pretty())
        .init();
    // Parse commandline arguments
    let opt = Opt::from_args();
    debug!(?opt);
    if [".", "./", "./.", "..", "../"].contains(&opt.path.to_string_lossy().as_ref()) {
        return Err(eyre!("Invalid path specified"));
    }
    let parent = &opt.path.parent().ok_or_else(|| {
        eyre!(
            "Path has no parent component: {}",
            opt.path.to_string_lossy()
        )
    })?;
    let child = &opt.path.file_name().ok_or_else(|| {
        eyre!(
            "Path has no final component: {}",
            opt.path.to_string_lossy()
        )
    })?;
    if parent.as_os_str().is_empty() || child.is_empty() {
        return Err(eyre!(
            "Invalid path specified. Must point to a directory that does not yet exist: {}",
            opt.path.to_string_lossy()
        ));
    }
    let parent = std::fs::canonicalize(&parent)
        .wrap_err_with(|| format!("Failed to expand path: {:?}", parent))?;
    let path = parent.join(child);
    debug!(?path);
    let password = opt.password.as_bytes();
    match opt.cmd {
        Subcommand::Init {
            compression,
            max_cache_entries,
        } => {
            // Initialize a repository at this location
            let mut crypto_box = CryptoBox::init(&path, compression, max_cache_entries, password)
                .wrap_err("Failed to initialize box")?;
            // Flush it
            crypto_box.flush().wrap_err("Failed to flush box")?;
            println!("Initialized CryptoBox at {}", path.to_string_lossy());
            Ok(())
        }
        Subcommand::ListNamespace => {
            // Open up the repository
            let crypto_box = CryptoBox::open(&path, password).wrap_err("Failed to open box")?;
            let namespaces = crypto_box.namespaces();
            println!("Namespaces ({}):", namespaces.len());
            for namespace in namespaces {
                println!("  - {}", namespace);
            }
            Ok(())
        }
        Subcommand::AddNamespace { name } => {
            // Open up the repository
            let mut crypto_box = CryptoBox::open(&path, password).wrap_err("Failed to open box")?;
            // Add the namespace
            crypto_box
                .create_namespace(name)
                .wrap_err("Failed to create namespace")?;
            // flush the box
            crypto_box.flush().wrap_err("Failed to flush box")?;
            // Print out the updated namespaces
            let namespaces = crypto_box.namespaces();
            println!("Namespaces ({}):", namespaces.len());
            for namespace in namespaces {
                println!("  - {}", namespace);
            }
            Ok(())
        }
        Subcommand::Dump { name } => {
            // Open up the repository
            let mut crypto_box = CryptoBox::open(&path, password).wrap_err("Failed to open box")?;
            // Makesure the namespace exists
            if !crypto_box.namespace_exists(&name) {
                return Err(eyre!("Namespace does not exist: {}", name));
            }
            // Dump the namespace as a map of raw CBOR values
            let raw_pairs: Vec<(serde_cbor::value::Value, serde_cbor::value::Value)> = crypto_box
                .to_pairs(&name)
                .wrap_err_with(|| format!("Failed to dump namespace: {}", name))?;
            let mut json_pairs: Vec<(serde_json::Value, serde_json::Value)> = vec![];
            for (key, value) in raw_pairs {
                json_pairs.push((cbor_to_json(key)?, cbor_to_json(value)?));
            }

            // Stringify the keys and convert to a hashmap
            let mut pairs: HashMap<String, serde_json::Value> = HashMap::new();
            for (key, value) in json_pairs {
                let key = serde_json::to_string(&key).wrap_err("Unable to transcode key")?;
                pairs.insert(key, value);
            }
            let final_output =
                serde_json::to_string(&pairs).wrap_err("Failed to serialize dump")?;
            println!("{}", final_output);
            Ok(())
        }
        Subcommand::DumpRoot => {
            // Open up the repository
            let mut crypto_box = CryptoBox::open(&path, password).wrap_err("Failed to open box")?;
            // Dump the namespace as a map of raw CBOR values
            let raw_pairs: Vec<(serde_cbor::value::Value, serde_cbor::value::Value)> = crypto_box
                .root_to_pairs()
                .wrap_err("Failed to dump root namespace")?;
            let mut json_pairs: Vec<(serde_json::Value, serde_json::Value)> = vec![];
            for (key, value) in raw_pairs {
                json_pairs.push((cbor_to_json(key)?, cbor_to_json(value)?));
            }
            // Stringify the keys and convert to a hashmap
            let mut pairs: HashMap<String, serde_json::Value> = HashMap::new();
            for (key, value) in json_pairs {
                let key = serde_json::to_string(&key).wrap_err("Unable to transcode key")?;
                pairs.insert(key, value);
            }
            let final_output =
                serde_json::to_string(&pairs).wrap_err("Failed to serialize dump")?;
            println!("{}", final_output);
            Ok(())
        }
        Subcommand::Insert { name, key, value } => {
            // Open up the repository
            let mut crypto_box = CryptoBox::open(&path, password).wrap_err("Failed to open box")?;
            // Makesure the namespace exists
            if !crypto_box.namespace_exists(&name) {
                return Err(eyre!("Namespace does not exist: {}", name));
            }
            let key: serde_json::Value = if let Ok(value) = serde_json::from_str(&key) {
                value
            } else {
                serde_json::Value::String(key)
            };
            let value: serde_json::Value = if let Ok(value) = serde_json::from_str(&value) {
                value
            } else {
                serde_json::Value::String(value)
            };
            // Insert the changed value
            crypto_box
                .insert(&key, &value, &name)
                .wrap_err("Failed to set value")?;
            // flush the box
            crypto_box.flush().wrap_err("Failed to flush box")?;
            Ok(())
        }
    }
}

/// Converts a CBOR node into a json node
fn cbor_to_json(cbor: serde_cbor::value::Value) -> Result<serde_json::Value> {
    use serde_cbor::value::Value::*;
    match cbor {
        Null => Ok(serde_json::Value::Null),
        Bool(b) => Ok(serde_json::Value::Bool(b)),
        Integer(big) => {
            let little: i64 = big.try_into().wrap_err("Number was too big or too small")?;
            Ok(serde_json::Value::from(little))
        }
        Float(f) => Ok(serde_json::Value::from(f)),
        Bytes(b) => {
            let string = bytes_to_hex(&b);
            Ok(serde_json::Value::String(string))
        }
        Text(string) => Ok(serde_json::Value::String(string)),
        Array(values) => {
            let new_values = values
                .into_iter()
                .map(cbor_to_json)
                .collect::<Result<Vec<_>>>()?;
            Ok(serde_json::Value::Array(new_values))
        }
        Map(values) => {
            // Json only suports strings as keys, so we have to pull some trickery
            let mut result = serde_json::map::Map::new();
            for (key, value) in values {
                // serialize the key
                let key_string = serde_json::to_string(&cbor_to_json(key)?)
                    .wrap_err("Failed to serialize key")?;
                let json_value = cbor_to_json(value)?;
                result.insert(key_string, json_value);
            }
            Ok(serde_json::Value::Object(result))
        }
        other => Err(eyre!("Failed to parse cbor value: {:?}", other)),
    }
}

/// Converts a byte slice into a a hex string
fn bytes_to_hex(bytes: &[u8]) -> String {
    let mut result = "0x".to_string();
    for byte in bytes {
        let string = format!("{:X?}", byte);
        result.push_str(&string);
    }
    result
}