diff --git a/README.md b/README.md index ea4cc75..0985d36 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Base commads (simple CRUD): - delete app - clear database -```s +```sh $ totp dash $ totp list $ totp show-all diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..bab7e2f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,218 @@ +extern crate base32; +extern crate oath; +extern crate rand; +extern crate serde_json; + +#[macro_use] +extern crate serde_derive; + +use std::collections::HashMap; +use std::fs::{create_dir_all, File, OpenOptions}; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; + +const DATABASE_VERSION: u8 = 1; + +pub struct RusTOTPony { + database: DB, + applications: HashMap, +} + +impl RusTOTPony { + pub fn new(db: DB) -> RusTOTPony { + RusTOTPony { + applications: db.get_applications(), + database: db, + } + } + + pub fn create_application( + &mut self, + name: &str, + username: &str, + secret: &str, + ) -> Result<(), String> { + if let Some(secret_bytes) = GenApp::base32_to_bytes(secret) { + let new_app = GenApp::new(name, username, secret, secret_bytes); + if self.applications.contains_key(name) { + Err(format!("Application with name '{}' already exists!", name)) + } else { + &self.applications.insert(String::from(name), new_app); + Ok(()) + } + } else { + return Err(String::from("Couldn't decode secret key")); + } + } + + pub fn delete_application(&mut self, name: &str) -> Result<(), String> { + if let Some(_) = self.applications.remove(name) { + Ok(()) + } else { + Err(format!( + "Application with the name '{}' doesn't exist", + name + )) + } + } + + pub fn rename_application(&mut self, name: &str, newname: &str) -> Result<(), String> { + if let Some(app) = self.applications.get_mut(name) { + app.name = String::from(newname); + Ok(()) + } else { + Err(format!("Application '{}' wasn't found", name)) + } + } + + pub fn get_applications(&self) -> Result<&HashMap, String> { + if self.applications.len() == 0 { + Err(String::from("There are no applications")) + } else { + Ok(&self.applications) + } + } + + pub fn get_application(&self, name: &str) -> Result<&GenApp, String> { + if let Some(app) = self.applications.get(name) { + Ok(app) + } else { + Err(format!("Application '{}' wasn't found", name)) + } + } + + pub fn delete_all_applications(&mut self) { + self.applications = HashMap::new(); + } + + pub fn flush(&self) { + &self.database.save_applications(&self.applications); + } +} + +pub trait Database { + fn get_applications(&self) -> HashMap; + fn save_applications(&self, applications: &HashMap); +} + +impl Database for JsonDatabase { + fn get_applications(&self) -> HashMap { + let db_content = self.read_database_file(); + db_content.content.applications + } + + fn save_applications(&self, applications: &HashMap) { + let mut db_content = Self::get_empty_schema(); + db_content.content.applications = applications.clone(); + self.save_database_file(db_content); + } +} + +#[derive(Serialize, Deserialize)] +struct JsonDatabaseSchema { + version: u8, + content: DatabaseContentSchema, +} + +#[derive(Serialize, Deserialize)] +struct DatabaseContentSchema { + applications: HashMap, +} + +pub struct JsonDatabase { + file_path: PathBuf, +} + +impl JsonDatabase { + pub fn new(path: PathBuf) -> JsonDatabase { + JsonDatabase { file_path: path } + } + + fn read_database_file(&self) -> JsonDatabaseSchema { + let file = match File::open(&self.file_path) { + Ok(f) => f, + 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") + } + + fn save_database_file(&self, content: JsonDatabaseSchema) { + let 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"); + } + + fn create_database_file(&self) -> Result { + let dir = std::env::home_dir().unwrap_or(PathBuf::from(".")); + if let Some(parent_dir) = Path::new(&self.file_path).parent() { + let dir = dir.join(parent_dir); + create_dir_all(dir)?; + } + self.open_database_file_for_write() + } + + fn open_database_file_for_write(&self) -> Result { + OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .open(&self.file_path) + } + + fn get_empty_schema() -> JsonDatabaseSchema { + JsonDatabaseSchema { + version: DATABASE_VERSION, + content: DatabaseContentSchema { + applications: HashMap::new(), + }, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GenApp { + name: String, + secret: String, + username: String, + secret_bytes: Vec, +} + +impl GenApp { + fn new(name: &str, username: &str, secret: &str, secret_bytes: Vec) -> Self { + GenApp { + name: String::from(name), + secret: String::from(secret), + username: String::from(username), + secret_bytes: secret_bytes, + } + } + + pub fn get_name(&self) -> &str { + self.name.as_str() + } + + pub fn get_secret(&self) -> &str { + self.secret.as_str() + } + + pub fn get_username(&self) -> &str { + self.username.as_str() + } + + pub fn get_code(&self) -> u64 { + Self::totp(&self.secret_bytes) + } + + fn base32_to_bytes(secret: &str) -> Option> { + base32::decode(base32::Alphabet::RFC4648 { padding: false }, secret) + } + + fn totp(secret_bytes: &[u8]) -> u64 { + oath::totp_raw_now(&secret_bytes, 6, 0, 30, &oath::HashType::SHA1) + } +} diff --git a/src/main.rs b/src/main.rs index f22a1e8..7f3da0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,211 +1,18 @@ -extern crate base32; extern crate clap; -extern crate oath; -extern crate rand; extern crate rpassword; -extern crate serde_json; - -#[macro_use] -extern crate serde_derive; +extern crate rustotpony; use clap::{App, Arg, SubCommand}; +use rustotpony::*; use std::collections::HashMap; -use std::fs::{create_dir_all, File, OpenOptions}; -use std::io::ErrorKind; use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH, Duration}; use std::thread; - +use std::time::{Duration, SystemTime, UNIX_EPOCH}; const CONFIG_PATH: &str = ".rustotpony/db.json"; -const DATABASE_VERSION: u8 = 1; - -struct RusTOTPony { - database: DB, - applications: HashMap, -} - -impl RusTOTPony { - fn new(db: DB) -> RusTOTPony { - RusTOTPony { - applications: db.get_applications(), - database: db, - } - } - - fn create_application(&mut self, name: &str, username: &str, secret: &str) -> Result<(), String> { - if let Some(secret_bytes) = GenApp::base32_to_bytes(secret) { - let new_app = GenApp::new(name, username, secret, secret_bytes); - if self.applications.contains_key(name) { - Err(format!("Application with name '{}' already exists!", name)) - } else { - &self.applications.insert(String::from(name), new_app); - Ok(()) - } - } else { - return Err(String::from("Couldn't decode secret key")) - } - } - - fn delete_application(&mut self, name: &str) -> Result<(), String> { - if let Some(_) = self.applications.remove(name) { - Ok(()) - } else { - Err(format!( - "Application with the name '{}' doesn't exist", - name - )) - } - } - - fn rename_application(&mut self, name: &str, newname: &str) -> Result<(), String> { - if let Some(app) = self.applications.get_mut(name) { - app.name = String::from(newname); - Ok(()) - } else { - Err(format!("Application '{}' wasn't found", name)) - } - } - - fn get_applications(&self) -> Result<&HashMap, String> { - if self.applications.len() == 0 { - Err(String::from("There are no applications")) - } else { - Ok(&self.applications) - } - } - - fn get_application(&self, name:&str) -> Result<&GenApp, String> { - if let Some(app) = self.applications.get(name) { - Ok(app) - } else { - Err(format!("Application '{}' wasn't found", name)) - } - } - - fn delete_all_applications(&mut self) { - self.applications = HashMap::new(); - } - - fn flush(&self) { - &self.database.save_applications(&self.applications); - } -} - -trait Database { - fn get_applications(&self) -> HashMap; - fn save_applications(&self, applications: &HashMap); -} - -impl Database for JsonDatabase { - fn get_applications(&self) -> HashMap { - let db_content = self.read_database_file(); - db_content.content.applications - } - - fn save_applications(&self, applications: &HashMap) { - let mut db_content = Self::get_empty_schema(); - db_content.content.applications = applications.clone(); - self.save_database_file(db_content); - } -} - -#[derive(Serialize, Deserialize)] -struct JsonDatabaseSchema { - version: u8, - content: DatabaseContentSchema, -} - -#[derive(Serialize, Deserialize)] -struct DatabaseContentSchema { - applications: HashMap, -} - -struct JsonDatabase { - file_path: PathBuf, -} -impl JsonDatabase { - fn new(path: PathBuf) -> JsonDatabase { - JsonDatabase { file_path: path } - } - - fn read_database_file(&self) -> JsonDatabaseSchema { - let file = match File::open(&self.file_path) { - Ok(f) => f, - 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") - } - - fn save_database_file(&self, content: JsonDatabaseSchema) { - let 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"); - } - - fn create_database_file(&self) -> Result { - let dir = std::env::home_dir().unwrap_or(PathBuf::from(".")); - if let Some(parent_dir) = Path::new(&self.file_path).parent() { - let dir = dir.join(parent_dir); - create_dir_all(dir)?; - } - self.open_database_file_for_write() - } - - fn open_database_file_for_write(&self) -> Result { - OpenOptions::new() - .write(true) - .truncate(true) - .create(true) - .open(&self.file_path) - } - - fn get_empty_schema() -> JsonDatabaseSchema { - JsonDatabaseSchema { - version: DATABASE_VERSION, - content: DatabaseContentSchema { - applications: HashMap::new(), - }, - } - } -} - - -#[derive(Serialize, Deserialize, Debug, Clone)] -struct GenApp { - name: String, - secret: String, - username: String, - secret_bytes: Vec, -} - -impl GenApp { - fn new(name: &str, username: &str, secret: &str, secret_bytes: Vec) -> Self { - GenApp { - name: String::from(name), - secret: String::from(secret), - username: String::from(username), - secret_bytes: secret_bytes, - } - } - - fn get_code(&self) -> u64 { - Self::totp(&self.secret_bytes) - } - - fn base32_to_bytes(secret:&str) -> Option> { - base32::decode(base32::Alphabet::RFC4648 {padding: false}, secret) - } - - fn totp(secret_bytes: &[u8]) -> u64 { - oath::totp_raw_now(&secret_bytes, 6, 0, 30, &oath::HashType::SHA1) - } +fn main() { + Cli::new().run(); } struct Cli { @@ -313,7 +120,7 @@ impl Cli { } fn show_dashboard(&self) { - match self.app.get_applications(){ + match self.app.get_applications() { Ok(apps) => { let mut is_first_iteration = true; let lines_count = apps.len() + 1; @@ -326,11 +133,11 @@ impl Cli { } Self::print_progress_bar(); for (_, app) in apps { - println!{"{:06} {}", app.get_code(), app.name}; + println!{"{:06} {}", app.get_code(), app.get_name()}; } thread::sleep(Duration::from_millis(100)); } - }, + } Err(err) => println!("{}", err), } } @@ -339,7 +146,8 @@ impl Cli { let width = 60; let now = SystemTime::now(); let since_the_epoch = now.duration_since(UNIX_EPOCH).unwrap(); - let in_ms = since_the_epoch.as_secs() * 1000 + since_the_epoch.subsec_nanos() as u64 / 1_000_000; + let in_ms = + since_the_epoch.as_secs() * 1000 + since_the_epoch.subsec_nanos() as u64 / 1_000_000; let step = in_ms % 30_000; let idx = step * width / 30_000; println!("[{:60}]", "=".repeat(idx as usize)); @@ -347,29 +155,29 @@ impl Cli { fn show_applications_list(&self, _: bool) { // TODO Create Table structure with HashMap as follows and metadata about columns - width, titles, names - let mut output_table: HashMap<&str, Vec> = HashMap::new(); + let mut output_table: HashMap<&str, Vec<&str>> = HashMap::new(); let mut applications_count = 0; let apps = match self.app.get_applications() { Ok(v) => v, Err(e) => { println!("{}", e); return; - }, + } }; for (_, application) in apps { applications_count += 1; output_table .entry("name") .or_insert(Vec::new()) - .push(application.name.clone()); + .push(application.get_name()); output_table .entry("key") .or_insert(Vec::new()) - .push(application.secret.clone()); + .push(application.get_secret()); output_table .entry("username") .or_insert(Vec::new()) - .push(application.username.clone()); + .push(application.get_username()); } let name_max_length = output_table["name"] .iter() @@ -390,12 +198,12 @@ impl Cli { println!("{}", header_row_delimiter); println!( "| {name: { self.app.flush(); println!("New application created: {}", name) - }, + } Err(err) => println!("{} Aborting…", err), } } @@ -436,7 +244,7 @@ impl Cli { Ok(_) => { self.app.flush(); println!("Application '{}' successfully deleted", name) - }, + } Err(err) => println!("Couldn't delete application '{}': {}", name, err), }; } @@ -445,10 +253,13 @@ impl Cli { match self.app.rename_application(name, newname) { Ok(_) => { self.app.flush(); - println!("Application '{}' successfully renamed to '{}'", name, newname) - }, + println!( + "Application '{}' successfully renamed to '{}'", + name, newname + ) + } Err(err) => println!("Couldn't rename application '{}': {}", name, err), - }; + }; } fn eradicate_database(&mut self) { @@ -457,7 +268,3 @@ impl Cli { println!("Done."); } } - -fn main() { - Cli::new().run(); -}