Merge branch 'binary_encoding'

This commit is contained in:
Tom Alexander 2019-06-02 23:58:15 -04:00
commit 1504f38141
4 changed files with 347 additions and 39 deletions

View File

@ -6,8 +6,17 @@ use crypto::scrypt::{self, ScryptParams};
use crypto::sha2::Sha256;
use rand::rngs::OsRng;
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::{FromBase64, ToBase64};
use std::convert::TryFrom;
use std::convert::TryInto;
use std::io;
pub struct EncryptedValue {
@ -16,6 +25,89 @@ pub struct EncryptedValue {
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]> {
let scrypt_params: ScryptParams = ScryptParams::new(12, 16, 2);
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(),
}
}
#[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());
}
}
}

124
src/db.rs
View File

@ -1,6 +1,6 @@
use super::crypt;
use crate::crypt::EncryptedValue;
use rusqlite::{Connection, NO_PARAMS};
use rusqlite::{params, Connection, NO_PARAMS};
use rustc_serialize::base64;
use rustc_serialize::base64::{FromBase64, ToBase64};
use std::error::Error;
@ -26,6 +26,28 @@ pub struct Account {
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 {
pub fn new(db_path: &Option<String>) -> DbHandle {
let path: PathBuf = db_path
@ -39,6 +61,106 @@ impl DbHandle {
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>> {
let mut stmt = self
.conn

View File

@ -1,3 +1,5 @@
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS encrypted_values (
id INTEGER PRIMARY KEY AUTOINCREMENT,
iv TEXT,
@ -21,3 +23,17 @@ CREATE TABLE IF NOT EXISTS accounts(
user 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)
);

View File

@ -18,16 +18,19 @@ static USAGE: &'static str = "
foil
Usage:
foil set [--db=<db>]
foil get [--db=<db>]
foil list [--db=<db>]
foil set [--namespace=<ns>] [--db=<db>]
foil get [--namespace=<ns>] [--db=<db>]
foil list [--namespace=<ns>] [--db=<db>]
foil transfer [--db=<db>]
foil dump [--db=<db>]
foil generate <spec>
foil (-h | --help)
Options:
--db=<db> The path to the sqlite database [default: db.sqlite3].
-h --help Show this screen.
--version Show version.
--db=<db> The path to the 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)]
@ -36,7 +39,10 @@ struct Args {
cmd_get: bool,
cmd_list: bool,
cmd_generate: bool,
cmd_transfer: bool,
cmd_dump: bool,
flag_db: Option<String>,
flag_namespace: 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_password = PasswordInput::with_theme(&ColorfulTheme::default())
.with_prompt("Master password")
.with_confirmation(
"Repeat master password",
"Error: the passwords don't match.",
)
.interact()
.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
}
fn list(mut db_conn: db::DbHandle, master_key: [u8; 32]) {
for host in db_conn
.list_accounts(master_key)
.into_iter()
.map(|account: db::Account| account.host)
{
println!("{}", host);
fn list(mut db_conn: 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(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");
let host: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("hostname")
.interact()
.unwrap();
for account in db_conn.list_accounts(master_key) {
if account.host == host {
println!("username: {}", account.user);
println!("password: {}", account.password);
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(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");
let host: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("hostname")
@ -125,21 +128,50 @@ fn set(mut db_conn: db::DbHandle, master_key: [u8; 32]) {
.with_prompt("username")
.interact()
.unwrap();
let encrypted_host: crypt::EncryptedValue = crypt::encrypt_value(&host, master_key);
let encrypted_username: crypt::EncryptedValue = crypt::encrypt_value(&username, master_key);
let encrypted_password: crypt::EncryptedValue = {
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();
crypt::encrypt_value(&password, master_key)
};
db_conn.delete_account_with_host(master_key, &host);
let _account_id = db_conn.write_account(encrypted_host, encrypted_username, encrypted_password);
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(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>> {
pretty_env_logger::init();
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);
if args.cmd_set {
set(db_conn, master_key);
set(db_conn, master_key, &args.flag_namespace);
} else if args.cmd_get {
get(db_conn, master_key);
get(db_conn, master_key, &args.flag_namespace);
} 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(())