Merge branch 'binary_encoding'

master
Tom Alexander 5 years ago
commit 1504f38141

@ -6,8 +6,17 @@ use crypto::scrypt::{self, ScryptParams};
use crypto::sha2::Sha256; use crypto::sha2::Sha256;
use rand::rngs::OsRng; use rand::rngs::OsRng;
use rand::Rng; use rand::Rng;
use rusqlite::types::FromSql;
use rusqlite::types::FromSqlResult;
use rusqlite::types::ToSqlOutput;
use rusqlite::types::ToSqlOutput::Owned;
use rusqlite::types::Value::Blob;
use rusqlite::types::ValueRef;
use rusqlite::ToSql;
use rustc_serialize::base64; use rustc_serialize::base64;
use rustc_serialize::base64::{FromBase64, ToBase64}; use rustc_serialize::base64::{FromBase64, ToBase64};
use std::convert::TryFrom;
use std::convert::TryInto;
use std::io; use std::io;
pub struct EncryptedValue { pub struct EncryptedValue {
@ -16,6 +25,89 @@ pub struct EncryptedValue {
pub mac: MacResult, pub mac: MacResult,
} }
impl ToSql for EncryptedValue {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
// Format:
// 8 bytes: length of mac
// n bytes: mac
// 8 bytes: length of iv
// n bytes: iv
// 8 bytes: length of cihpertext
// n bytes: ciphertext
let length_of_ciphertext: u64 = self.ciphertext.len().try_into().unwrap();
let length_of_iv: u64 = self.iv.len().try_into().unwrap();
let mac_bytes = self.mac.code();
let length_of_mac: u64 = mac_bytes.len().try_into().unwrap();
let full_length = (8 * 3) + length_of_mac + length_of_iv + length_of_ciphertext;
let mut out: Vec<u8> = Vec::with_capacity(full_length.try_into().unwrap());
out.extend(&length_of_mac.to_le_bytes());
out.extend(mac_bytes);
out.extend(&length_of_iv.to_le_bytes());
out.extend(&self.iv);
out.extend(&length_of_ciphertext.to_le_bytes());
out.extend(&self.ciphertext);
Ok(Owned(Blob(out)))
}
}
impl FromSql for EncryptedValue {
fn column_result(value: ValueRef) -> FromSqlResult<Self> {
let bytes = value.as_blob().unwrap();
let length_of_mac: u64 =
u64::from_le_bytes(bytes[0..8].try_into().expect("Invalid number of bytes"));
let length_of_iv: u64 = u64::from_le_bytes(
bytes[usize::try_from(8 + length_of_mac).unwrap()
..usize::try_from(16 + length_of_mac).unwrap()]
.try_into()
.expect("Invalid number of bytes"),
);
let length_of_ciphertext: u64 = u64::from_le_bytes(
bytes[usize::try_from(16 + length_of_mac + length_of_iv).unwrap()
..usize::try_from(24 + length_of_mac + length_of_iv).unwrap()]
.try_into()
.expect("Invalid number of bytes"),
);
Ok(EncryptedValue {
ciphertext: bytes[usize::try_from(24 + length_of_mac + length_of_iv).unwrap()
..usize::try_from(24 + length_of_mac + length_of_iv + length_of_ciphertext)
.unwrap()]
.to_vec(),
iv: bytes[usize::try_from(16 + length_of_mac).unwrap()
..usize::try_from(16 + length_of_mac + length_of_iv).unwrap()]
.try_into()
.expect("Invalid number of bytes"),
mac: MacResult::new(&bytes[8..usize::try_from(8 + length_of_mac).unwrap()]),
})
}
}
impl EncryptedValue {
pub fn decrypt_to_bytes(&self, master_key: [u8; 32]) -> Vec<u8> {
let mut hmac = Hmac::new(Sha256::new(), &master_key);
hmac.input(&self.ciphertext);
if hmac.result() != self.mac {
panic!("Mac did not match, corrupted data");
}
let mut cipher = aes::ctr(KeySize::KeySize256, &master_key, &self.iv);
let mut output: Vec<u8> = vec![0; self.ciphertext.len()];
cipher.process(&self.ciphertext, output.as_mut_slice());
output
}
pub fn decrypt_to_string(
&self,
master_key: [u8; 32],
) -> Result<String, std::string::FromUtf8Error> {
let decrypted_bytes = self.decrypt_to_bytes(master_key);
String::from_utf8(decrypted_bytes)
}
}
pub fn get_master_key(db_conn: &db::DbHandle, master_password: &str) -> io::Result<[u8; 32]> { pub fn get_master_key(db_conn: &db::DbHandle, master_password: &str) -> io::Result<[u8; 32]> {
let scrypt_params: ScryptParams = ScryptParams::new(12, 16, 2); let scrypt_params: ScryptParams = ScryptParams::new(12, 16, 2);
let salt: Vec<u8> = get_salt(db_conn)?; let salt: Vec<u8> = get_salt(db_conn)?;
@ -77,3 +169,45 @@ pub fn encrypt_value(value: &str, master_key: [u8; 32]) -> EncryptedValue {
mac: hmac.result(), mac: hmac.result(),
} }
} }
#[cfg(test)]
mod tests {
use crate::crypt::{encrypt_value, EncryptedValue};
use rusqlite::{Connection, NO_PARAMS};
#[test]
fn test_encrypted_value_round_trip() {
// Test that writing a value to the DB and reading it back
// doesn't result in any corruption
let db = Connection::open_in_memory().expect("Failed to open DB");
db.execute_batch("CREATE TABLE test (content BLOB);")
.expect("Failed to create table");
let master_key: [u8; 32] = [0u8; 32];
let encrypted_value = encrypt_value("hunter2", master_key);
db.execute(
"INSERT INTO test (content) VALUES ($1)",
&[&encrypted_value],
)
.expect("Failed to insert value into DB");
let mut stmt = db
.prepare("SELECT * FROM test")
.expect("Failed to prepare statement");
let rows: Vec<Result<EncryptedValue, rusqlite::Error>> = stmt
.query_map(NO_PARAMS, |row| {
let val: EncryptedValue = row.get(0).expect("Failed to get element from row");
Ok(val)
})
.expect("Failed to get rows")
.collect();
assert_eq!(rows.len(), 1);
for returned_result in rows {
let returned_value: EncryptedValue = returned_result.expect("Bad value returned");
assert_eq!(returned_value.ciphertext, encrypted_value.ciphertext);
assert_eq!(returned_value.iv, encrypted_value.iv);
assert_eq!(returned_value.mac.code(), encrypted_value.mac.code());
}
}
}

@ -1,6 +1,6 @@
use super::crypt; use super::crypt;
use crate::crypt::EncryptedValue; use crate::crypt::EncryptedValue;
use rusqlite::{Connection, NO_PARAMS}; use rusqlite::{params, Connection, NO_PARAMS};
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;
@ -26,6 +26,28 @@ pub struct Account {
pub password: String, pub password: String,
} }
pub struct DbNamespace {
pub id: i64,
pub name: EncryptedValue,
}
pub struct DbNote {
pub id: i64,
pub namespace: DbNamespace,
pub category: String,
pub title: EncryptedValue,
pub value: EncryptedValue,
}
#[derive(Debug)]
pub struct Note {
pub id: i64,
pub namespace: String,
pub category: String,
pub title: String,
pub value: String,
}
impl DbHandle { 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
@ -39,6 +61,106 @@ impl DbHandle {
DbHandle { conn: conn } DbHandle { conn: conn }
} }
pub fn get_namespace_id(
&mut self,
name: &str,
master_key: [u8; 32],
) -> Result<i64, Box<dyn Error>> {
{
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)?,
})
})?;
for row_result in rows {
let row: DbNamespace = row_result?;
let row_name: String = row.name.decrypt_to_string(master_key)?;
if name == row_name {
return Ok(row.id);
}
}
}
let new_namespace = crypt::encrypt_value(name, master_key);
let tx = self.conn.transaction().unwrap();
tx.execute(
"INSERT INTO namespaces (name) VALUES ($1)",
&[&new_namespace],
)
.unwrap();
let rowid: i64 = tx.last_insert_rowid();
let _ = tx.commit().unwrap();
Ok(rowid)
}
pub fn write_note(&mut self, master_key: [u8; 32], note: Note) {
let existing_notes = self.read_notes(master_key).unwrap();
let namespace_id = self.get_namespace_id(&note.namespace, master_key).unwrap();
let tx = self.conn.transaction().unwrap();
for existing_note in existing_notes {
if existing_note.namespace == note.namespace
&& existing_note.category == note.category
&& existing_note.title == note.title
{
tx.execute("DELETE FROM notes WHERE id=$1;", params![existing_note.id])
.unwrap();
}
}
let encrypted_title = crypt::encrypt_value(&note.title, master_key);
let encrypted_value = crypt::encrypt_value(&note.value, master_key);
tx.execute(
"INSERT INTO notes (namespace, category, title, value) VALUES ($1, $2, $3, $4);",
params![
namespace_id,
note.category,
encrypted_title,
encrypted_value
],
)
.unwrap();
let _ = tx.commit().unwrap();
}
pub fn read_notes(&mut self, master_key: [u8; 32]) -> rusqlite::Result<Vec<Note>> {
let mut stmt = self.conn.prepare("SELECT notes.id, notes.category, notes.title, notes.value, namespaces.id, namespaces.name FROM notes JOIN namespaces ON notes.namespace=namespaces.id").unwrap();
let rows = stmt.query_map(params![], |row| {
Ok(DbNote {
id: row.get(0)?,
category: row.get(1)?,
title: row.get(2)?,
value: row.get(3)?,
namespace: DbNamespace {
id: row.get(4)?,
name: row.get(5)?,
},
})
})?;
let notes: Result<Vec<Note>, _> = rows
.map(|note_result| match note_result {
Ok(note) => Ok(Note {
id: note.id,
namespace: note.namespace.name.decrypt_to_string(master_key).unwrap(),
category: note.category,
title: note.title.decrypt_to_string(master_key).unwrap(),
value: note.value.decrypt_to_string(master_key).unwrap(),
}),
Err(e) => return Err(e),
})
.collect();
notes
}
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

@ -1,3 +1,5 @@
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS encrypted_values ( CREATE TABLE IF NOT EXISTS encrypted_values (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
iv TEXT, iv TEXT,
@ -21,3 +23,17 @@ CREATE TABLE IF NOT EXISTS accounts(
user INTEGER NOT NULL, user INTEGER NOT NULL,
password INTEGER NOT NULL password INTEGER NOT NULL
); );
CREATE TABLE IF NOT EXISTS namespaces(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name BLOB NOT NULL
);
CREATE TABLE IF NOT EXISTS notes(
id INTEGER PRIMARY KEY AUTOINCREMENT,
namespace INTEGER,
category TEXT NOT NULL CHECK(category IN ('account', 'note')),
title BLOB NOT NULL,
value BLOB NOT NULL,
FOREIGN KEY(namespace) REFERENCES namespaces(id)
);

@ -18,16 +18,19 @@ static USAGE: &'static str = "
foil foil
Usage: Usage:
foil set [--db=<db>] foil set [--namespace=<ns>] [--db=<db>]
foil get [--db=<db>] foil get [--namespace=<ns>] [--db=<db>]
foil list [--db=<db>] foil list [--namespace=<ns>] [--db=<db>]
foil transfer [--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].
-h --help Show this screen. -n DB, --namespace=<db> An identifier for a group of secrets [default: main]
--version Show version. -h, --help Show this screen.
--version Show version.
"; ";
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -36,7 +39,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,
flag_db: Option<String>, flag_db: Option<String>,
flag_namespace: String,
arg_spec: Option<String>, arg_spec: Option<String>,
} }
@ -61,10 +67,6 @@ fn get_master_key(db_conn: &mut db::DbHandle) -> [u8; 32] {
let master_key: [u8; 32] = { let master_key: [u8; 32] = {
let master_password = PasswordInput::with_theme(&ColorfulTheme::default()) let master_password = PasswordInput::with_theme(&ColorfulTheme::default())
.with_prompt("Master password") .with_prompt("Master password")
.with_confirmation(
"Repeat master password",
"Error: the passwords don't match.",
)
.interact() .interact()
.unwrap(); .unwrap();
crypt::get_master_key(&db_conn, &master_password).unwrap() crypt::get_master_key(&db_conn, &master_password).unwrap()
@ -90,32 +92,33 @@ 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(mut db_conn: 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]) { fn get(mut db_conn: 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")
.interact() .interact()
.unwrap(); .unwrap();
for account in db_conn.list_accounts(master_key) { for note in db_conn.read_notes(master_key).unwrap() {
if account.host == host { if note.namespace == namespace && note.title == host && note.category == "account" {
println!("username: {}", account.user); println!("===== note =====");
println!("password: {}", account.password); println!("namespace: {}", note.namespace);
println!("category: {}", note.category);
println!("title: {}", note.title);
println!("{}", note.value);
} }
} }
} }
fn set(mut db_conn: db::DbHandle, master_key: [u8; 32]) { fn set(mut db_conn: 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")
@ -125,21 +128,50 @@ fn set(mut db_conn: db::DbHandle, master_key: [u8; 32]) {
.with_prompt("username") .with_prompt("username")
.interact() .interact()
.unwrap(); .unwrap();
let encrypted_host: crypt::EncryptedValue = crypt::encrypt_value(&host, master_key); let password: String = PasswordInput::with_theme(&ColorfulTheme::default())
let encrypted_username: crypt::EncryptedValue = crypt::encrypt_value(&username, master_key); .with_prompt("Site password")
let encrypted_password: crypt::EncryptedValue = { .with_confirmation("Repeat site password", "Error: the passwords don't match.")
let password: String = PasswordInput::with_theme(&ColorfulTheme::default()) .interact()
.with_prompt("Site password") .unwrap();
.with_confirmation("Repeat site password", "Error: the passwords don't match.") db_conn.write_note(
.interact() master_key,
.unwrap(); db::Note {
crypt::encrypt_value(&password, master_key) id: 0,
}; namespace: namespace.to_string(),
db_conn.delete_account_with_host(master_key, &host); category: "account".to_string(),
let _account_id = db_conn.write_account(encrypted_host, encrypted_username, encrypted_password); title: host,
value: format!("username: {}\npassword: {}\n", username, password),
},
);
println!("Successfully added password"); println!("Successfully added password");
} }
fn dump(mut db_conn: 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 transfer(mut db_conn: db::DbHandle, master_key: [u8; 32]) {
for account in db_conn.list_accounts(master_key).into_iter() {
let new_note = db::Note {
id: 0,
namespace: "main".to_owned(),
category: "account".to_owned(),
title: account.host,
value: format!(
"username: {}\npassword: {}\n",
account.user, account.password
),
};
db_conn.write_note(master_key, new_note);
}
}
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
pretty_env_logger::init(); pretty_env_logger::init();
let args: Args = Docopt::new(USAGE) let args: Args = Docopt::new(USAGE)
@ -157,11 +189,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); set(db_conn, master_key, &args.flag_namespace);
} else if args.cmd_get { } else if args.cmd_get {
get(db_conn, master_key); get(db_conn, master_key, &args.flag_namespace);
} else if args.cmd_list { } else if args.cmd_list {
list(db_conn, master_key); list(db_conn, master_key, &args.flag_namespace);
} else if args.cmd_transfer {
transfer(db_conn, master_key);
} else if args.cmd_dump {
dump(db_conn, master_key);
} }
Ok(()) Ok(())

Loading…
Cancel
Save