Merge branch 'binary_encoding'
This commit is contained in:
commit
1504f38141
134
src/crypt.rs
134
src/crypt.rs
@ -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
124
src/db.rs
@ -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(¬e.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(¬e.title, master_key);
|
||||
let encrypted_value = crypt::encrypt_value(¬e.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
|
||||
|
16
src/init.sql
16
src/init.sql
@ -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)
|
||||
);
|
||||
|
112
src/main.rs
112
src/main.rs
@ -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(())
|
||||
|
Loading…
Reference in New Issue
Block a user