diff --git a/src/crypt.rs b/src/crypt.rs index a37bffb..5b9465c 100644 --- a/src/crypt.rs +++ b/src/crypt.rs @@ -60,3 +60,20 @@ pub fn decrypt_value(value: Vec, master_key: [u8; 32], iv: [u8; 32], mac: [u cipher.process(&value, output.as_mut_slice()); output } + +pub fn encrypt_value(value: &str, master_key: [u8; 32]) -> EncryptedValue { + let mut random = OsRng::new().unwrap(); + let iv: [u8; 32] = random.gen::<[u8; 32]>(); + + let mut cipher = aes::ctr(KeySize::KeySize256, &master_key, &iv); + let mut output: Vec = vec![0; value.len() as usize]; + cipher.process(value.as_bytes(), output.as_mut_slice()); + + let mut hmac = Hmac::new(Sha256::new(), &master_key); + hmac.input(&output[..]); + EncryptedValue { + ciphertext: output, + iv: iv, + mac: hmac.result(), + } +} diff --git a/src/db.rs b/src/db.rs index 403cbfb..753d1ee 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,4 +1,5 @@ use super::crypt; +use crate::crypt::EncryptedValue; use rusqlite::{Connection, NO_PARAMS}; use rustc_serialize::base64; use rustc_serialize::base64::{FromBase64, ToBase64}; @@ -119,6 +120,64 @@ impl DbHandle { }; 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 = 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 { diff --git a/src/main.rs b/src/main.rs index 4bef0a5..8c2fb31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use crypto::hmac::Hmac; use crypto::mac::{Mac, MacResult}; use crypto::sha2::Sha256; -use dialoguer::{theme::ColorfulTheme, PasswordInput}; +use dialoguer::{theme::ColorfulTheme, Input, PasswordInput}; use docopt::Docopt; use log::debug; use rand::rngs::OsRng; @@ -100,6 +100,46 @@ fn list(mut db_conn: db::DbHandle, master_key: [u8; 32]) { } } +fn get(mut db_conn: db::DbHandle, master_key: [u8; 32]) { + 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); + } + } +} + +fn set(mut db_conn: db::DbHandle, master_key: [u8; 32]) { + println!("Adding a site to the database"); + let host: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("hostname") + .interact() + .unwrap(); + let username: String = Input::with_theme(&ColorfulTheme::default()) + .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); + println!("Successfully added password"); +} + fn main() -> Result<(), Box> { pretty_env_logger::init(); let args: Args = Docopt::new(USAGE) @@ -116,7 +156,11 @@ fn main() -> Result<(), Box> { let master_key: [u8; 32] = get_master_key(&mut db_conn); - if args.cmd_list { + if args.cmd_set { + set(db_conn, master_key); + } else if args.cmd_get { + get(db_conn, master_key); + } else if args.cmd_list { list(db_conn, master_key); }