Compare commits

...

10 Commits

5 changed files with 131 additions and 187 deletions

View File

@ -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(),
} }
} }

173
src/db.rs
View File

@ -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,19 +19,16 @@ struct DbProperty {
value: Option<String>, value: Option<String>,
} }
#[derive(Debug)]
pub struct Account {
pub id: i64,
pub host: String,
pub user: String,
pub password: String,
}
pub struct DbNamespace { pub struct DbNamespace {
pub id: i64, pub id: i64,
pub name: EncryptedValue, pub name: EncryptedValue,
} }
pub struct Namespace {
pub id: i64,
pub name: String,
}
pub struct DbNote { pub struct DbNote {
pub id: i64, pub id: i64,
pub namespace: DbNamespace, pub namespace: DbNamespace,
@ -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 { impl fmt::Display for Namespace {
let b64ciphertext: String = val.ciphertext.to_base64(base64::STANDARD); fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let b64iv: String = val.iv.to_base64(base64::STANDARD); write!(f, "{}", self.name)
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> {
let mut iv_bytes: [u8; 32] = [0; 32];
let mut mac_bytes: [u8; 32] = [0; 32];
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
}

View File

@ -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)

View File

@ -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

View File

@ -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(())