Decoupled app into main and lib

feature/database-encription
German Lashevich 6 years ago
parent 7a56afbaec
commit 593530834b

@ -14,7 +14,7 @@ Base commads (simple CRUD):
- delete app
- clear database
```s
```sh
$ totp dash
$ totp list
$ totp show-all

@ -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<DB: Database> {
database: DB,
applications: HashMap<String, GenApp>,
}
impl<DB: Database> RusTOTPony<DB> {
pub fn new(db: DB) -> RusTOTPony<DB> {
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, GenApp>, 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<String, GenApp>;
fn save_applications(&self, applications: &HashMap<String, GenApp>);
}
impl Database for JsonDatabase {
fn get_applications(&self) -> HashMap<String, GenApp> {
let db_content = self.read_database_file();
db_content.content.applications
}
fn save_applications(&self, applications: &HashMap<String, GenApp>) {
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<String, GenApp>,
}
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<File, std::io::Error> {
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<File, std::io::Error> {
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<u8>,
}
impl GenApp {
fn new(name: &str, username: &str, secret: &str, secret_bytes: Vec<u8>) -> 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<Vec<u8>> {
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)
}
}

@ -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<DB: Database> {
database: DB,
applications: HashMap<String, GenApp>,
}
impl<DB: Database> RusTOTPony<DB> {
fn new(db: DB) -> RusTOTPony<DB> {
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, GenApp>, 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<String, GenApp>;
fn save_applications(&self, applications: &HashMap<String, GenApp>);
}
impl Database for JsonDatabase {
fn get_applications(&self) -> HashMap<String, GenApp> {
let db_content = self.read_database_file();
db_content.content.applications
}
fn save_applications(&self, applications: &HashMap<String, GenApp>) {
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<String, GenApp>,
}
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<File, std::io::Error> {
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<File, std::io::Error> {
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<u8>,
}
impl GenApp {
fn new(name: &str, username: &str, secret: &str, secret_bytes: Vec<u8>) -> 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<Vec<u8>> {
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<String>> = 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:<name_width$} | {key: <key_width$} | {username: <username_width$} |",
name_width=name_max_length,
name="name",
key_width=key_max_length,
key="key",
username_width=username_max_length,
username="username"
name_width = name_max_length,
name = "name",
key_width = key_max_length,
key = "key",
username_width = username_max_length,
username = "username"
);
println!("{}", header_row_delimiter);
@ -405,12 +213,12 @@ impl Cli {
let username = &output_table["username"][i];
println!(
"| {name:<name_width$} | {key: <key_width$} | {username: <username_width$} |",
name_width=name_max_length,
name=name,
key_width=key_max_length,
key=key,
username_width=username_max_length,
username=username
name_width = name_max_length,
name = name,
key_width = key_max_length,
key = key,
username_width = username_max_length,
username = username
);
}
println!("{}", header_row_delimiter);
@ -426,7 +234,7 @@ impl Cli {
Ok(_) => {
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();
}

Loading…
Cancel
Save