Compare commits

...

10 Commits

@ -165,7 +165,7 @@ pub fn encrypt_value(value: &str, master_key: [u8; 32]) -> EncryptedValue {
hmac.input(&output[..]); hmac.input(&output[..]);
EncryptedValue { EncryptedValue {
ciphertext: output, ciphertext: output,
iv: iv, iv,
mac: hmac.result(), mac: hmac.result(),
} }
} }

@ -1,12 +1,13 @@
use super::crypt; use super::crypt;
use crate::crypt::EncryptedValue; use crate::crypt::EncryptedValue;
use rusqlite::{params, Connection, NO_PARAMS}; use rusqlite::{params, Connection};
use rustc_serialize::base64; use rustc_serialize::base64;
use rustc_serialize::base64::{FromBase64, ToBase64}; use rustc_serialize::base64::{FromBase64, ToBase64};
use std::error::Error; use std::error::Error;
use std::fmt;
use std::path::PathBuf; use std::path::PathBuf;
static DB_INIT_QUERY: &'static str = include_str!("init.sql"); static DB_INIT_QUERY: &str = include_str!("init.sql");
pub struct DbHandle { pub struct DbHandle {
conn: Connection, conn: Connection,
@ -18,17 +19,14 @@ struct DbProperty {
value: Option<String>, value: Option<String>,
} }
#[derive(Debug)] pub struct DbNamespace {
pub struct Account {
pub id: i64, pub id: i64,
pub host: String, pub name: EncryptedValue,
pub user: String,
pub password: String,
} }
pub struct DbNamespace { pub struct Namespace {
pub id: i64, pub id: i64,
pub name: EncryptedValue, pub name: String,
} }
pub struct DbNote { pub struct DbNote {
@ -52,13 +50,13 @@ impl DbHandle {
pub fn new(db_path: &Option<String>) -> DbHandle { pub fn new(db_path: &Option<String>) -> DbHandle {
let path: PathBuf = db_path let path: PathBuf = db_path
.as_ref() .as_ref()
.map(|path: &String| PathBuf::from(path)) .map(PathBuf::from)
.unwrap_or_else(|| dirs::home_dir().unwrap().join(".foil").to_path_buf()); .unwrap_or_else(|| dirs::home_dir().unwrap().join(".foil").to_path_buf());
let mut conn: Connection = Connection::open(path).unwrap(); let mut conn: Connection = Connection::open(path).unwrap();
let tx = conn.transaction().unwrap(); let tx = conn.transaction().unwrap();
tx.execute_batch(DB_INIT_QUERY).unwrap(); tx.execute_batch(DB_INIT_QUERY).unwrap();
tx.commit().unwrap(); tx.commit().unwrap();
DbHandle { conn: conn } DbHandle { conn }
} }
pub fn get_namespace_id( pub fn get_namespace_id(
@ -95,7 +93,7 @@ impl DbHandle {
) )
.unwrap(); .unwrap();
let rowid: i64 = tx.last_insert_rowid(); let rowid: i64 = tx.last_insert_rowid();
let _ = tx.commit().unwrap(); tx.commit().unwrap();
Ok(rowid) Ok(rowid)
} }
@ -127,7 +125,7 @@ impl DbHandle {
) )
.unwrap(); .unwrap();
let _ = tx.commit().unwrap(); tx.commit().unwrap();
} }
pub fn read_notes(&mut self, master_key: [u8; 32]) -> rusqlite::Result<Vec<Note>> { pub fn read_notes(&mut self, master_key: [u8; 32]) -> rusqlite::Result<Vec<Note>> {
@ -154,13 +152,37 @@ impl DbHandle {
title: note.title.decrypt_to_string(master_key).unwrap(), title: note.title.decrypt_to_string(master_key).unwrap(),
value: note.value.decrypt_to_string(master_key).unwrap(), value: note.value.decrypt_to_string(master_key).unwrap(),
}), }),
Err(e) => return Err(e), Err(e) => Err(e),
}) })
.collect(); .collect();
notes notes
} }
pub fn list_namespaces(&mut self, master_key: [u8; 32]) -> rusqlite::Result<Vec<Namespace>> {
let mut stmt = self
.conn
.prepare("SELECT id, name FROM namespaces")
.unwrap();
let rows = stmt.query_map(params![], |row| {
Ok(DbNamespace {
id: row.get(0)?,
name: row.get(1)?,
})
})?;
let namespaces: Result<Vec<Namespace>, _> = rows
.map(|namespace_result| match namespace_result {
Ok(namespace) => Ok(Namespace {
id: namespace.id,
name: namespace.name.decrypt_to_string(master_key).unwrap(),
}),
Err(e) => Err(e),
})
.collect();
namespaces
}
pub fn get_db_property(&self, name: &str) -> Result<Option<String>, Box<dyn Error>> { pub fn get_db_property(&self, name: &str) -> Result<Option<String>, Box<dyn Error>> {
let mut stmt = self let mut stmt = self
.conn .conn
@ -193,127 +215,14 @@ impl DbHandle {
.map(|option| option.map(|prop| prop.from_base64().unwrap())) .map(|option| option.map(|prop| prop.from_base64().unwrap()))
} }
pub fn set_db_property_bytes(&self, name: &str, value: &Vec<u8>) { pub fn set_db_property_bytes(&self, name: &str, value: &[u8]) {
let b64value: String = value.to_base64(base64::STANDARD); let b64value: String = value.to_base64(base64::STANDARD);
self.set_db_property(name, &b64value); self.set_db_property(name, &b64value);
} }
pub fn list_accounts(&mut self, master_key: [u8; 32]) -> Vec<Account> {
let mut stmt = self.conn
.prepare("SELECT h.iv, h.ciphertext, h.mac, u.iv, u.ciphertext, u.mac, p.iv, p.ciphertext, p.mac, a.id FROM accounts a \
LEFT JOIN encrypted_values h ON a.server=h.id \
LEFT JOIN encrypted_values u ON a.user=u.id \
LEFT JOIN encrypted_values p ON a.password=p.id")
.unwrap();
let result: Vec<Account> = {
let props = stmt
.query_map(NO_PARAMS, |row| {
let host: String = String::from_utf8(decrypt_base64(
row.get(0).unwrap(),
row.get(1).unwrap(),
row.get(2).unwrap(),
master_key,
))
.unwrap();
let user: String = String::from_utf8(decrypt_base64(
row.get(3).unwrap(),
row.get(4).unwrap(),
row.get(5).unwrap(),
master_key,
))
.unwrap();
let password: String = String::from_utf8(decrypt_base64(
row.get(6).unwrap(),
row.get(7).unwrap(),
row.get(8).unwrap(),
master_key,
))
.unwrap();
Ok(Account {
id: row.get(9).unwrap(),
host: host,
user: user,
password: password,
})
})
.unwrap();
props.map(move |x| x.unwrap()).collect()
};
result
}
pub fn write_encrypted_value(&mut self, val: EncryptedValue) -> i64 {
let b64ciphertext: String = val.ciphertext.to_base64(base64::STANDARD);
let b64iv: String = val.iv.to_base64(base64::STANDARD);
let b64mac: String = val.mac.code().to_base64(base64::STANDARD);
let tx = self.conn.transaction().unwrap();
tx.execute(
"INSERT INTO encrypted_values (iv, ciphertext, mac) VALUES ($1, $2, $3)",
&[&b64iv, &b64ciphertext, &b64mac],
)
.unwrap();
let rowid: i64 = tx.last_insert_rowid();
let _ = tx.commit().unwrap();
rowid
}
pub fn write_account(
&mut self,
host: EncryptedValue,
username: EncryptedValue,
password: EncryptedValue,
) -> i64 {
// TODO: This should be a transaction
let host_id = self.write_encrypted_value(host);
let user_id = self.write_encrypted_value(username);
let password_id = self.write_encrypted_value(password);
self.conn
.execute(
"INSERT INTO accounts (server, user, password) VALUES ($1, $2, $3)",
&[&host_id, &user_id, &password_id],
)
.unwrap();
self.conn.last_insert_rowid()
}
pub fn delete_account_with_host(&mut self, master_key: [u8; 32], host: &str) {
let accounts: Vec<Account> = self.list_accounts(master_key);
let tx = self.conn.transaction().unwrap();
for account in accounts {
if account.host == host {
tx.execute(
"DELETE FROM encrypted_values WHERE exists \
(SELECT 1 FROM accounts WHERE \
(accounts.server = encrypted_values.id OR \
accounts.user = encrypted_values.id OR \
accounts.password = encrypted_values.id) AND \
accounts.id=$1);",
&[&account.id],
)
.unwrap();
tx.execute("DELETE FROM accounts WHERE id=$1;", &[&account.id])
.unwrap();
}
}
let _ = tx.commit().unwrap();
}
} }
fn decrypt_base64(iv: String, ciphertext: String, mac: String, master_key: [u8; 32]) -> Vec<u8> { impl fmt::Display for Namespace {
let mut iv_bytes: [u8; 32] = [0; 32]; fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut mac_bytes: [u8; 32] = [0; 32]; write!(f, "{}", self.name)
}
iv_bytes.clone_from_slice(&iv.from_base64().unwrap());
mac_bytes.clone_from_slice(&mac.from_base64().unwrap());
let decrypted: Vec<u8> = crypt::decrypt_value(
ciphertext.from_base64().unwrap(),
master_key,
iv_bytes,
mac_bytes,
);
decrypted
} }

@ -5,7 +5,7 @@ use std::iter::FromIterator;
pub fn generate(spec: &str) { pub fn generate(spec: &str) {
let (len, rest) = { let (len, rest) = {
let mut separated = spec.splitn(2, ","); let mut separated = spec.splitn(2, ',');
let len = separated.next().unwrap(); let len = separated.next().unwrap();
let rest = separated.next().unwrap(); let rest = separated.next().unwrap();
(len, rest) (len, rest)

@ -1,29 +1,10 @@
PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS encrypted_values (
id INTEGER PRIMARY KEY AUTOINCREMENT,
iv TEXT,
ciphertext TEXT,
mac TEXT
);
CREATE TABLE IF NOT EXISTS props( CREATE TABLE IF NOT EXISTS props(
name TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL PRIMARY KEY,
value TEXT value TEXT
); );
CREATE TABLE IF NOT EXISTS encrypted_props(
name TEXT NOT NULL PRIMARY KEY,
encrypted_value INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS accounts(
id INTEGER PRIMARY KEY AUTOINCREMENT,
server INTEGER NOT NULL,
user INTEGER NOT NULL,
password INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS namespaces( CREATE TABLE IF NOT EXISTS namespaces(
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name BLOB NOT NULL name BLOB NOT NULL

@ -1,7 +1,9 @@
#![warn(clippy::all)]
use crypto::hmac::Hmac; use crypto::hmac::Hmac;
use crypto::mac::{Mac, MacResult}; use crypto::mac::{Mac, MacResult};
use crypto::sha2::Sha256; use crypto::sha2::Sha256;
use dialoguer::{theme::ColorfulTheme, Input, PasswordInput}; use dialoguer::{theme::ColorfulTheme, Input, PasswordInput, Select};
use docopt::Docopt; use docopt::Docopt;
use log::debug; use log::debug;
use rand::rngs::OsRng; use rand::rngs::OsRng;
@ -14,20 +16,21 @@ pub mod crypt;
pub mod db; pub mod db;
pub mod generate; pub mod generate;
static USAGE: &'static str = " static USAGE: &str = "
foil foil
Usage: Usage:
foil set [--namespace=<ns>] [--db=<db>] foil set [--namespace=<ns>] [--db=<db>]
foil get [--namespace=<ns>] [--db=<db>] foil get [--namespace=<ns>] [--db=<db>]
foil list [--db=<db>] foil list [--namespace=<ns>] [--db=<db>]
foil transfer [--db=<db>] foil shell [--namespace=<ns>] [--db=<db>]
foil dump [--db=<db>] foil dump [--db=<db>]
foil generate <spec> foil generate <spec>
foil (-h | --help) foil (-h | --help)
Options: Options:
--db=<db> The path to the sqlite database [default: db.sqlite3]. --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] -n DB, --namespace=<db> An identifier for a group of secrets [default: main]
-h, --help Show this screen. -h, --help Show this screen.
--version Show version. --version Show version.
@ -39,9 +42,10 @@ struct Args {
cmd_get: bool, cmd_get: bool,
cmd_list: bool, cmd_list: bool,
cmd_generate: bool, cmd_generate: bool,
cmd_transfer: bool,
cmd_dump: bool, cmd_dump: bool,
cmd_shell: bool,
flag_db: Option<String>, flag_db: Option<String>,
flag_src: Option<String>,
flag_namespace: String, flag_namespace: String,
arg_spec: Option<String>, arg_spec: Option<String>,
} }
@ -92,17 +96,15 @@ fn get_master_key(db_conn: &mut db::DbHandle) -> [u8; 32] {
master_key master_key
} }
fn list(mut db_conn: db::DbHandle, master_key: [u8; 32]) { fn list(db_conn: &mut db::DbHandle, master_key: [u8; 32], namespace: &str) {
for host in db_conn for note in db_conn.read_notes(master_key).unwrap() {
.list_accounts(master_key) if note.namespace == namespace && note.category == "account" {
.into_iter() println!("{}", note.title);
.map(|account: db::Account| account.host) }
{
println!("{}", host);
} }
} }
fn get(mut db_conn: db::DbHandle, master_key: [u8; 32], namespace: &str) { fn get(db_conn: &mut db::DbHandle, master_key: [u8; 32], namespace: &str) {
println!("Reading a site from the database"); println!("Reading a site from the database");
let host: String = Input::with_theme(&ColorfulTheme::default()) let host: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("hostname") .with_prompt("hostname")
@ -120,7 +122,7 @@ fn get(mut db_conn: db::DbHandle, master_key: [u8; 32], namespace: &str) {
} }
} }
fn set(mut db_conn: db::DbHandle, master_key: [u8; 32], namespace: &str) { fn set(db_conn: &mut db::DbHandle, master_key: [u8; 32], namespace: &str) {
println!("Adding a site to the database"); println!("Adding a site to the database");
let host: String = Input::with_theme(&ColorfulTheme::default()) let host: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("hostname") .with_prompt("hostname")
@ -148,7 +150,7 @@ fn set(mut db_conn: db::DbHandle, master_key: [u8; 32], namespace: &str) {
println!("Successfully added password"); println!("Successfully added password");
} }
fn dump(mut db_conn: db::DbHandle, master_key: [u8; 32]) { fn dump(db_conn: &mut db::DbHandle, master_key: [u8; 32]) {
for note in db_conn.read_notes(master_key).unwrap() { for note in db_conn.read_notes(master_key).unwrap() {
println!("===== note ====="); println!("===== note =====");
println!("namespace: {}", note.namespace); println!("namespace: {}", note.namespace);
@ -158,19 +160,71 @@ fn dump(mut db_conn: db::DbHandle, master_key: [u8; 32]) {
} }
} }
fn transfer(mut db_conn: db::DbHandle, master_key: [u8; 32]) { fn shell_generate() {
for account in db_conn.list_accounts(master_key).into_iter() { let spec: String = Input::with_theme(&ColorfulTheme::default())
let new_note = db::Note { .with_prompt("Password Spec")
id: 0, .default("30,AAAa111..".to_owned())
namespace: "main".to_owned(), .interact()
category: "account".to_owned(), .unwrap();
title: account.host,
value: format!( generate::generate(&spec);
"username: {}\npassword: {}\n", }
account.user, account.password
), 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"),
}; };
db_conn.write_note(master_key, new_note);
} }
} }
@ -191,15 +245,15 @@ fn main() -> Result<(), Box<dyn Error>> {
let master_key: [u8; 32] = get_master_key(&mut db_conn); let master_key: [u8; 32] = get_master_key(&mut db_conn);
if args.cmd_set { if args.cmd_set {
set(db_conn, master_key, &args.flag_namespace); set(&mut db_conn, master_key, &args.flag_namespace);
} else if args.cmd_get { } else if args.cmd_get {
get(db_conn, master_key, &args.flag_namespace); get(&mut db_conn, master_key, &args.flag_namespace);
} else if args.cmd_list { } else if args.cmd_list {
list(db_conn, master_key); list(&mut db_conn, master_key, &args.flag_namespace);
} else if args.cmd_transfer {
transfer(db_conn, master_key);
} else if args.cmd_dump { } else if args.cmd_dump {
dump(db_conn, master_key); dump(&mut db_conn, master_key);
} else if args.cmd_shell {
shell(&mut db_conn, master_key, &args.flag_namespace);
} }
Ok(()) Ok(())

Loading…
Cancel
Save