You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

261 lines
8.1 KiB
Rust

#![warn(clippy::all)]
use crypto::hmac::Hmac;
use crypto::mac::{Mac, MacResult};
use crypto::sha2::Sha256;
use dialoguer::{theme::ColorfulTheme, Input, PasswordInput, Select};
use docopt::Docopt;
use log::debug;
use rand::rngs::OsRng;
use rand::seq::IteratorRandom;
use rand::seq::SliceRandom;
use serde::Deserialize;
use std::error::Error;
pub mod crypt;
pub mod db;
pub mod generate;
static USAGE: &str = "
foil
Usage:
foil set [--namespace=<ns>] [--db=<db>]
foil get [--namespace=<ns>] [--db=<db>]
foil list [--namespace=<ns>] [--db=<db>]
foil shell [--namespace=<ns>] [--db=<db>]
foil dump [--db=<db>]
foil generate <spec>
foil (-h | --help)
Options:
--db=<db> The path to the sqlite database [default: db.sqlite3].
--src=<db> The path to the old sqlite database [default: db.sqlite3].
-n DB, --namespace=<db> An identifier for a group of secrets [default: main]
-h, --help Show this screen.
--version Show version.
";
#[derive(Debug, Deserialize)]
struct Args {
cmd_set: bool,
cmd_get: bool,
cmd_list: bool,
cmd_generate: bool,
cmd_dump: bool,
cmd_shell: bool,
flag_db: Option<String>,
flag_src: Option<String>,
flag_namespace: String,
arg_spec: Option<String>,
}
fn get_master_key(db_conn: &mut db::DbHandle) -> [u8; 32] {
let known_string = db_conn
.get_db_property("known_string")
.expect("There was a problem reading from the db")
.unwrap_or_else(|| {
println!("No master password set yet, create new one:");
let mut random = OsRng::new().unwrap();
let new_known: String = {
let mut new_chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz"
.chars()
.choose_multiple(&mut random, 64);
new_chars.shuffle(&mut random);
new_chars.into_iter().collect()
};
db_conn.set_db_property("known_string", &new_known);
new_known
});
let master_key: [u8; 32] = {
let master_password = PasswordInput::with_theme(&ColorfulTheme::default())
.with_prompt("Master password")
.interact()
.unwrap();
crypt::get_master_key(&db_conn, &master_password).unwrap()
};
let mut hmac = Hmac::new(Sha256::new(), &master_key);
hmac.input(known_string.as_bytes());
let existing_hmac = db_conn
.get_db_property_bytes("known_hmac")
.expect("There was a problem reading from the db")
.unwrap_or_else(|| {
let mut raw_result: Vec<u8> = std::iter::repeat(0).take(hmac.output_bytes()).collect();
hmac.raw_result(&mut raw_result);
db_conn.set_db_property_bytes("known_hmac", &raw_result);
raw_result
});
if hmac.result() != MacResult::new(&existing_hmac[..]) {
panic!("Incorrect master password");
}
master_key
}
fn list(db_conn: &mut db::DbHandle, master_key: [u8; 32], namespace: &str) {
for note in db_conn.read_notes(master_key).unwrap() {
if note.namespace == namespace && note.category == "account" {
println!("{}", note.title);
}
}
}
fn get(db_conn: &mut db::DbHandle, master_key: [u8; 32], namespace: &str) {
println!("Reading a site from the database");
let host: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("hostname")
.interact()
.unwrap();
for note in db_conn.read_notes(master_key).unwrap() {
if note.namespace == namespace && note.title == host && note.category == "account" {
println!("===== note =====");
println!("namespace: {}", note.namespace);
println!("category: {}", note.category);
println!("title: {}", note.title);
println!("{}", note.value);
}
}
}
fn set(db_conn: &mut db::DbHandle, master_key: [u8; 32], namespace: &str) {
println!("Adding a site to the database");
let host: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("hostname")
.interact()
.unwrap();
let username: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("username")
.interact()
.unwrap();
let password: String = PasswordInput::with_theme(&ColorfulTheme::default())
.with_prompt("Site password")
.with_confirmation("Repeat site password", "Error: the passwords don't match.")
.interact()
.unwrap();
db_conn.write_note(
master_key,
db::Note {
id: 0,
namespace: namespace.to_string(),
category: "account".to_string(),
title: host,
value: format!("username: {}\npassword: {}\n", username, password),
},
);
println!("Successfully added password");
}
fn dump(db_conn: &mut db::DbHandle, master_key: [u8; 32]) {
for note in db_conn.read_notes(master_key).unwrap() {
println!("===== note =====");
println!("namespace: {}", note.namespace);
println!("category: {}", note.category);
println!("title: {}", note.title);
println!("{}", note.value);
}
}
fn shell_generate() {
let spec: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Password Spec")
.default("30,AAAa111..".to_owned())
.interact()
.unwrap();
generate::generate(&spec);
}
fn change_namespace(db_conn: &mut db::DbHandle, master_key: [u8; 32]) -> String {
let namespaces = db_conn.list_namespaces(master_key).unwrap();
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Change Namespace")
.default(0)
.items(&namespaces[..])
.interact()
.unwrap();
namespaces[selection].name.to_owned()
}
fn create_namespace(db_conn: &mut db::DbHandle, master_key: [u8; 32]) -> String {
let namespace: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Namespace")
.default("main".to_owned())
.interact()
.unwrap();
namespace
}
fn shell(db_conn: &mut db::DbHandle, master_key: [u8; 32], _namespace: &str) {
let mut namespace: String = _namespace.to_owned();
loop {
let selections = &[
"get",
"list",
"set",
"generate",
"change namespace",
"create namespace",
"exit",
];
let main_menu_prompt = format!("Main Menu ({})", namespace);
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt(&main_menu_prompt)
.default(0)
.items(&selections[..])
.interact()
.unwrap();
match selections[selection] {
"get" => get(db_conn, master_key, &namespace),
"list" => list(db_conn, master_key, &namespace),
"set" => set(db_conn, master_key, &namespace),
"generate" => shell_generate(),
"change namespace" => {
namespace = change_namespace(db_conn, master_key);
}
"create namespace" => {
namespace = create_namespace(db_conn, master_key);
}
"exit" => break,
_ => panic!("Unrecognized command"),
};
}
}
fn main() -> Result<(), Box<dyn Error>> {
pretty_env_logger::init();
let args: Args = Docopt::new(USAGE)
.and_then(|dopt| dopt.deserialize())
.unwrap_or_else(|e| e.exit());
debug!("{:?}", args);
if args.cmd_generate {
generate::generate(&args.arg_spec.unwrap());
return Ok(());
}
let mut db_conn: db::DbHandle = db::DbHandle::new(&args.flag_db);
let master_key: [u8; 32] = get_master_key(&mut db_conn);
if args.cmd_set {
set(&mut db_conn, master_key, &args.flag_namespace);
} else if args.cmd_get {
get(&mut db_conn, master_key, &args.flag_namespace);
} else if args.cmd_list {
list(&mut db_conn, master_key, &args.flag_namespace);
} else if args.cmd_dump {
dump(&mut db_conn, master_key);
} else if args.cmd_shell {
shell(&mut db_conn, master_key, &args.flag_namespace);
}
Ok(())
}