#![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 {
#[structopt(name = "PATH", parse(from_os_str))]
path: PathBuf,
#[structopt(long, short, env = "BOX_PASSWORD", hide_env_values = true)]
password: String,
#[structopt(subcommand)]
cmd: Subcommand,
}
#[derive(Debug, StructOpt)]
enum Subcommand {
#[structopt(
about = "Creates a CryptoBox",
after_help = AFTER_HELP,
author = AUTHORS,
)]
Init {
#[structopt(long, short)]
compression: Option<i32>,
#[structopt(long, short)]
max_cache_entries: Option<usize>,
},
#[structopt(
about = "List namespaces in a CryptoBox",
after_help = AFTER_HELP,
author = AUTHORS,
)]
ListNamespace,
#[structopt(
about = "Adds a namespace to a CryptoBox",
after_help = AFTER_HELP,
author = AUTHORS,
)]
AddNamespace {
#[structopt(name = "NAMESPACE")]
name: String,
},
#[structopt(
about = "Dumps a CryptoBox namespace to the console as JSON",
after_help = AFTER_HELP,
author = AUTHORS,
)]
Dump {
#[structopt(name = "NAMESPACE")]
name: String,
},
#[structopt(
about = "Inserts a json formatted key/value pair into the specified namespace",
after_help = AFTER_HELP,
author = AUTHORS,
)]
Insert {
#[structopt(name = "NAMESPACE")]
name: String,
#[structopt(name = "KEY")]
key: String,
#[structopt(name = "VALUE")]
value: String,
},
#[structopt(
about = "Dumps the CryptoBox root namespace to the console as JSON",
after_help = AFTER_HELP,
author = AUTHORS,
)]
DumpRoot,
}
#[instrument]
fn main() -> Result<()> {
color_eyre::install()?;
tracing_subscriber::registry()
.with(EnvFilter::from_default_env())
.with(fmt::layer().pretty())
.init();
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,
} => {
let mut crypto_box = CryptoBox::init(&path, compression, max_cache_entries, password)
.wrap_err("Failed to initialize box")?;
crypto_box.flush().wrap_err("Failed to flush box")?;
println!("Initialized CryptoBox at {}", path.to_string_lossy());
Ok(())
}
Subcommand::ListNamespace => {
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 } => {
let mut crypto_box = CryptoBox::open(&path, password).wrap_err("Failed to open box")?;
crypto_box
.create_namespace(name)
.wrap_err("Failed to create namespace")?;
crypto_box.flush().wrap_err("Failed to flush box")?;
let namespaces = crypto_box.namespaces();
println!("Namespaces ({}):", namespaces.len());
for namespace in namespaces {
println!(" - {}", namespace);
}
Ok(())
}
Subcommand::Dump { name } => {
let mut crypto_box = CryptoBox::open(&path, password).wrap_err("Failed to open box")?;
if !crypto_box.namespace_exists(&name) {
return Err(eyre!("Namespace does not exist: {}", name));
}
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)?));
}
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 => {
let mut crypto_box = CryptoBox::open(&path, password).wrap_err("Failed to open box")?;
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)?));
}
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 } => {
let mut crypto_box = CryptoBox::open(&path, password).wrap_err("Failed to open box")?;
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)
};
crypto_box
.insert(&key, &value, &name)
.wrap_err("Failed to set value")?;
crypto_box.flush().wrap_err("Failed to flush box")?;
Ok(())
}
}
}
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) => {
let mut result = serde_json::map::Map::new();
for (key, value) in values {
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)),
}
}
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
}