diff --git a/src/bin/totp.rs b/src/bin/totp.rs index 26c8719..e5df92d 100644 --- a/src/bin/totp.rs +++ b/src/bin/totp.rs @@ -13,20 +13,16 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; const CONFIG_PATH: &str = ".rustotpony/db.json"; fn main() { - Cli::new().run(); + Cli::run(); } -struct Cli { - app: RusTOTPony, -} +struct Cli {} impl Cli { - fn new() -> Self { + fn app() -> RusTOTPony { let secret = Self::get_secret(); let db = JsonDatabase::new(Self::get_database_path(), &secret); - Self { - app: RusTOTPony::new(db), - } + RusTOTPony::new(db) } fn get_secret() -> String { @@ -35,35 +31,35 @@ impl Cli { // fn get_secret_from_storage() -> String { } - fn run(&mut self) { - match self.get_cli_api_matches().subcommand() { + fn run() { + match Self::get_cli_api_matches().subcommand() { ("dash", Some(_)) => { - self.show_dashboard(); + Self::show_dashboard(); } ("list", Some(_)) => { - self.show_applications_list(false); + Self::show_applications_list(false); } // ("show-all", Some(_)) => { - // self.show_applications_list(true); + // Self::show_applications_list(true); // } // ("show", Some(sub_app)) => { // let app_name: &str = sub_app // .value_of("APPNAME") // .expect("Couldn't read APPNAME for 'show' command"); - // self.show_application(app_name); + // Self::show_application(app_name); // } ("add", Some(sub_app)) => { let app_name: &str = sub_app .value_of("APPNAME") .expect("Couldn't read APPNAME for 'add' command"); let key: &str = sub_app.value_of("USERNAME").unwrap_or(""); - self.create_application(app_name, key); + Self::create_application(app_name, key); } ("delete", Some(sub_app)) => { let app_name: &str = sub_app .value_of("APPNAME") .expect("Couldn't read APPNAME for 'delete' command"); - self.delete_application(app_name); + Self::delete_application(app_name); } ("rename", Some(sub_app)) => { let app_name: &str = sub_app @@ -72,18 +68,18 @@ impl Cli { let new_name: &str = sub_app .value_of("NEWNAME") .expect("Couldn't read NEWNAME for 'rename' command"); - self.rename_application(app_name, new_name); + Self::rename_application(app_name, new_name); } ("eradicate", Some(_)) => { - self.eradicate_database(); + Self::eradicate_database(); } _ => { - self.show_dashboard(); + Self::show_dashboard(); } } } - fn get_cli_api_matches(&self) -> clap::ArgMatches<'static> { + fn get_cli_api_matches() -> clap::ArgMatches<'static> { App::new("🐴 RusTOTPony") .version(env!("CARGO_PKG_VERSION")) .author("German Lashevich ") @@ -128,8 +124,8 @@ impl Cli { home.join(Path::new(CONFIG_PATH)) } - fn show_dashboard(&self) { - match self.app.get_applications() { + fn show_dashboard() { + match Self::app().get_applications() { Ok(apps) => { let mut is_first_iteration = true; let lines_count = apps.len() + 1; @@ -167,11 +163,12 @@ impl Cli { println!("[{:60}]", "=".repeat(idx as usize)); } - fn show_applications_list(&self, _: bool) { + fn show_applications_list(_: bool) { // TODO Create Table structure with HashMap as follows and metadata about columns - width, titles, names + let app = Self::app(); let mut output_table: HashMap<&str, Vec<&str>> = HashMap::new(); let mut applications_count = 0; - let apps = match self.app.get_applications() { + let apps = match app.get_applications() { Ok(v) => v, Err(e) => { println!("{}", e); @@ -238,35 +235,35 @@ impl Cli { println!("{}", header_row_delimiter); } - fn show_application(&self, name: &str) { - println!("{:?}", self.app.get_application(name)); + fn show_application(name: &str) { + println!("{:?}", Self::app().get_application(name)); } - fn create_application(&mut self, name: &str, username: &str) { + fn create_application(name: &str, username: &str) { let secret = rpassword::prompt_password_stdout("Enter your secret code: ").unwrap(); - match self.app.create_application(name, username, &secret) { + match Self::app().create_application(name, username, &secret) { Ok(_) => { - self.app.flush(); + Self::app().flush(); println!("New application created: {}", name) } Err(err) => println!("{} Aborting…", err), } } - fn delete_application(&mut self, name: &str) { - match self.app.delete_application(name) { + fn delete_application(name: &str) { + match Self::app().delete_application(name) { Ok(_) => { - self.app.flush(); + Self::app().flush(); println!("Application '{}' successfully deleted", name) } Err(err) => println!("Couldn't delete application '{}': {}", name, err), }; } - fn rename_application(&mut self, name: &str, newname: &str) { - match self.app.rename_application(name, newname) { + fn rename_application(name: &str, newname: &str) { + match Self::app().rename_application(name, newname) { Ok(_) => { - self.app.flush(); + Self::app().flush(); println!( "Application '{}' successfully renamed to '{}'", name, newname @@ -276,9 +273,9 @@ impl Cli { }; } - fn eradicate_database(&mut self) { - self.app.delete_all_applications(); - self.app.flush(); + fn eradicate_database() { + Self::app().delete_all_applications(); + Self::app().flush(); println!("Done."); } } diff --git a/src/lib.rs b/src/lib.rs index fff0348..6d04bfd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(fs_read_write)] + extern crate base32; extern crate crypto; extern crate oath; @@ -15,6 +17,7 @@ use rand::{OsRng, Rng}; use std::collections::HashMap; use std::fs::{create_dir_all, File, OpenOptions}; use std::io::ErrorKind; +use std::io::Write; use std::path::{Path, PathBuf}; const DATABASE_VERSION: u8 = 1; @@ -130,6 +133,7 @@ pub struct JsonDatabase { secret: String, } +const IV_SIZE: usize = 16; impl JsonDatabase { pub fn new(path: PathBuf, secret: &str) -> JsonDatabase { JsonDatabase { @@ -139,22 +143,50 @@ impl JsonDatabase { } fn read_database_file(&self) -> JsonDatabaseSchema { - let file = match File::open(&self.file_path) { - Ok(f) => f, + let data = match std::fs::read_to_string(&self.file_path) { + Ok(d) => d, Err(ref err) if err.kind() == ErrorKind::NotFound => return Self::get_empty_schema(), Err(err) => panic!("There was a problem opening file: {:?}", err), }; - serde_json::from_reader(file).expect("Couldn't parse JSON from database file") + let decrypted_data = Self::decrypt_data(&data, self.secret.as_str()); + serde_json::from_str(decrypted_data.as_str()) + .expect("Couldn't parse JSON from database file") + } + + fn decrypt_data(data: &str, key: &str) -> String { + let bytes = data.as_bytes(); + String::from_utf8( + Self::decrypt(&bytes[IV_SIZE..], key.as_bytes(), &bytes[..IV_SIZE]) + .expect("Couldn't decrypt data"), + ).ok() + .unwrap() + } + + fn encrypt_data(data: &str, key: &str) -> Vec { + let mut data_with_iv = Self::create_iv(); + data_with_iv.extend(data.as_bytes()); + return Self::encrypt(&data_with_iv, key.as_bytes(), &data_with_iv[..IV_SIZE]) + .expect("Couldn't encrypt data"); + } + + fn create_iv() -> Vec { + let mut iv = vec![0; IV_SIZE]; + let mut rng = OsRng::new().ok().unwrap(); + rng.fill_bytes(&mut iv); + iv } fn save_database_file(&self, content: JsonDatabaseSchema) { - let file = match self.open_database_file_for_write() { + let mut file = match self.open_database_file_for_write() { Ok(f) => f, Err(ref err) if err.kind() == ErrorKind::NotFound => self.create_database_file() .expect("Couldn't create database file"), Err(err) => panic!("Couldn't open database file: {:?}", err), }; - serde_json::to_writer(file, &content).expect("Couldn't write JSON data to database file"); + let data = serde_json::to_string(&content).expect("Couldn't serialize data to JSON"); + let encrypted_data = Self::encrypt_data(&data, &self.secret); + file.write_all(&encrypted_data) + .expect("Couldn't write data to database file"); } // Encrypt a buffer with the given key and iv using