use anyhow::{bail, format_err, Error};
use pwbox::{sodium::Sodium, ErasedPwBox, Eraser, Suite};
use rand::thread_rng;
use rpassword::read_password_from_tty;
use structopt::StructOpt;
use pwbox::sodium::Scrypt;
use std::{
env::{self, VarError},
fs,
io::{self, Read},
str::FromStr,
};
const HELP: &str =
"Simple key management utility, allowing to encrypt and decrypt arbitrary bytes.";
fn parse_scrypt(s: &str) -> Result<Scrypt, Error> {
Ok(match s {
"light" | "M" => Scrypt::light(),
"interactive" | "L" => Scrypt::interactive(),
"sensitive" | "XL" => Scrypt::sensitive(),
_ => bail!("Invalid scrypt setting"),
})
}
#[derive(Debug, StructOpt)]
struct PassphraseArgs {
#[structopt(name = "pass", long, short, default_value = "tty")]
source: PassphraseSource,
}
#[derive(Debug)]
enum PassphraseSource {
Tty,
Value(String),
Env(String),
}
impl FromStr for PassphraseSource {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"tty" => Ok(PassphraseSource::Tty),
s if s.starts_with("pass:") => Ok(PassphraseSource::Value(s[5..].to_owned())),
s if s.starts_with("env:") => Ok(PassphraseSource::Env(s[4..].to_owned())),
_ => bail!("Invalid passphrase spec"),
}
}
}
impl PassphraseSource {
fn get_passphrase(self) -> Result<String, Error> {
match self {
PassphraseSource::Value(passphrase) => Ok(passphrase),
PassphraseSource::Env(var_name) => {
let passphrase = env::var(&var_name).map_err(|e| match e {
VarError::NotPresent => format_err!(
"Env variable `{}` is not set \
(should contain encryption passphrase)",
var_name
),
VarError::NotUnicode(_) => {
format_err!("Cannot decode encryption passphrase from env variable")
}
})?;
Ok(passphrase)
}
PassphraseSource::Tty => {
read_password_from_tty(Some("Enter passphrase: ")).map_err(Into::into)
}
}
}
}
#[derive(Debug, Clone, Copy)]
enum PointerComponent<'a> {
Index(usize),
Name(&'a str),
}
impl<'a> PointerComponent<'a> {
fn from_str(s: &'a str) -> Vec<Self> {
s.split('.')
.filter_map(|raw_component| {
if raw_component.is_empty() {
None
} else {
Some(
raw_component
.parse::<usize>()
.map(PointerComponent::Index)
.unwrap_or_else(|_| PointerComponent::Name(raw_component)),
)
}
})
.collect()
}
fn traverse<'v>(mut object: &'v toml::Value, pointer: &[Self]) -> Option<&'v toml::Value> {
for component in pointer {
object = match component {
PointerComponent::Index(index) => object.get(index)?,
PointerComponent::Name(name) => object.get(name)?,
};
}
Some(object)
}
}
#[derive(Debug, StructOpt)]
#[structopt(
name = "Simple key util",
after_help = HELP,
set_term_width = 80
)]
enum Args {
#[structopt(name = "enc")]
Encrypt {
#[structopt(name = "data")]
data: String,
#[structopt(
name = "scrypt",
long,
short,
parse(try_from_str = parse_scrypt),
default_value = "interactive"
)]
scrypt: Scrypt,
#[structopt(flatten)]
passphrase: PassphraseArgs,
},
#[structopt(name = "dec")]
Decrypt {
#[structopt(name = "file")]
file: String,
#[structopt(name = "ptr", long, short = "@", default_value = "")]
pointer: String,
#[structopt(name = "check", long, short)]
check: bool,
#[structopt(flatten)]
passphrase: PassphraseArgs,
},
}
impl Args {
fn execute(self) -> Result<(), Error> {
let mut eraser = Eraser::new();
eraser.add_suite::<Sodium>();
match self {
Args::Encrypt {
data,
scrypt,
passphrase,
} => {
let data = hex::decode(data)?;
let passphrase = passphrase.source.get_passphrase()?;
let encrypted = Sodium::build_box(&mut thread_rng())
.kdf(scrypt)
.seal(&passphrase, &data)?;
let encrypted = eraser.erase(&encrypted)?;
println!("{}", toml::to_string_pretty(&encrypted)?);
}
Args::Decrypt {
file,
pointer,
check,
passphrase,
} => {
let input = if file.is_empty() || file == "-" {
let mut buffer = vec![];
io::stdin().read_to_end(&mut buffer)?;
buffer
} else {
fs::read(&file)?
};
let pointer = PointerComponent::from_str(&pointer);
let toml_object: toml::Value = toml::from_slice(&input)?;
let encrypted = PointerComponent::traverse(&toml_object, &pointer)
.ok_or_else(|| format_err!("Specified path does not exist in the input"))?;
let encrypted: ErasedPwBox = encrypted.clone().try_into()?;
let passphrase = passphrase.source.get_passphrase()?;
let data = eraser.restore(&encrypted)?.open(&passphrase)?;
if check {
println!("OK")
} else {
println!("{}", hex::encode(&data[..]));
}
}
}
Ok(())
}
}
fn main() -> Result<(), Error> {
Args::from_args().execute()
}