From 49ffb08499338e87aec189192a4c2077b8c14f42 Mon Sep 17 00:00:00 2001 From: German Lashevich Date: Fri, 23 Mar 2018 00:13:57 +0100 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=9A=A7=20Just=20fix=20intermediate=20?= =?UTF-8?q?state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 45 ++++++++----------- Cargo.toml | 8 +++- TODO | 21 +++++++++ src/bin/totp.rs | 9 +++- src/lib.rs | 113 +++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 165 insertions(+), 31 deletions(-) create mode 100644 TODO diff --git a/Cargo.lock b/Cargo.lock index 373a23c..6408953 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,6 +42,11 @@ name = "byte-tools" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "byteorder" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "cfg-if" version = "0.1.2" @@ -148,6 +153,11 @@ dependencies = [ "typenum 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "hex" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "hmac" version = "0.1.1" @@ -174,13 +184,14 @@ dependencies = [ [[package]] name = "keyring" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "advapi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.29.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rpassword 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "secret-service 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -320,17 +331,6 @@ dependencies = [ "redox_syscall 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "rpassword" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", - "termios 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "rpassword" version = "2.0.0" @@ -378,10 +378,11 @@ dependencies = [ "base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.29.0 (registry+https://github.com/rust-lang/crates.io-index)", "ctrlc 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "keyring 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "keyring 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "oath 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", @@ -491,14 +492,6 @@ dependencies = [ "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "termios" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "textwrap" version = "0.9.0" @@ -580,6 +573,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" "checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf" "checksum byte-tools 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0919189ba800c7ffe8778278116b7e0de3905ab81c72abb69c85cbfef7991279" +"checksum byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "652805b7e73fada9d85e9a6682a4abd490cb52d96aeecc12e33a0de34dfd0d23" "checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" "checksum clap 2.29.0 (registry+https://github.com/rust-lang/crates.io-index)" = "110d43e343eb29f4f51c1db31beb879d546db27998577e5715270a54bcf41d3f" "checksum constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff012e225ce166d4422e0e78419d901719760f62ae2b7969ca6b564d1b54a9e" @@ -594,10 +588,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum fuchsia-zircon-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "08b3a6f13ad6b96572b53ce7af74543132f1a7055ccceb6d073dd36c54481859" "checksum gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)" = "5e33ec290da0d127825013597dbdfc28bee4964690c7ce1166cbc2a7bd08b1bb" "checksum generic-array 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe043cf9b85297937897087de81f590361686e1ac2d4d471b45435de5dfb6a6" +"checksum hex 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "459d3cf58137bb02ad4adeef5036377ff59f066dbb82517b7192e3a5462a2abc" "checksum hmac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bdb5aa9647ba4711e9d6968dc1c810cd23989ed435443ca962e1bf6d8b8b83ff" "checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -"checksum keyring 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a13a4970024d20522520ecf2218f31fa9fc12dd4497a3901f79a137b425a184e" +"checksum keyring 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c503cc96b828521df71d8ef94edb17d3b7c0ef2caededd064b78fc5e1205de7b" "checksum libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "36fbc8a8929c632868295d0178dd8f63fc423fd7537ad0738372bd010b3ac9b0" "checksum nix 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a2c5afeb0198ec7be8569d666644b574345aad2e95a53baf3a532da3e0f3fb32" "checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2" @@ -614,7 +609,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum rand 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9d5f78082e6a6d042862611e9640cf20776185fee506cf6cf67e93c6225cee31" "checksum redox_syscall 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)" = "07b8f011e3254d5a9b318fde596d409a0001c9ae4c6e7907520c2eaa4d988c99" "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" -"checksum rpassword 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8f0eee8d16609fa0ec2a13c8d812fc6049f71d3cdd93f6f713fedf6397868c5c" "checksum rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d127299b02abda51634f14025aec43ae87a7aa7a95202b6a868ec852607d1451" "checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" "checksum rust-gmp 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4cd7d57377b309a73f69e164109203aa9ab3fee6ea68ac5fb76e2edb50662e9b" @@ -631,7 +625,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" -"checksum termios 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a" "checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693" "checksum time 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)" = "d5d788d3aa77bc0ef3e9621256885555368b47bd495c13dd2e7413c89f845520" "checksum typenum 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a99dc6780ef33c78780b826cf9d2a78840b72cae9474de4bcaf9051e60ebbd" diff --git a/Cargo.toml b/Cargo.toml index 27a6603..fd805bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,14 +14,18 @@ version = "0.2.1" [dependencies] base32 = "0.3.1" clap = "^2.29.0" -ctrlc = { version = "3.0", features = ["termination"] } -keyring = "0.5.1" +keyring = "0.6.0" oath = "0.10.2" rand = "0.4.1" rpassword = "2.0.0" +rust-crypto = "0.2.36" serde = "1.0.24" serde_derive = "1.0.24" serde_json = "1.0.8" + +[dependencies.ctrlc] +features = ["termination"] +version = "3.0" [profile.release] debug = false lto = true diff --git a/TODO b/TODO new file mode 100644 index 0000000..7c538c0 --- /dev/null +++ b/TODO @@ -0,0 +1,21 @@ +# Change approach for saving and loading data + +## On load + +1. Read encrypted data into memory +2. Invoke decrypt function, which takes IV from the beginning of the data +3. Retrieve secret +4. Decrypt data +5. Unserialize data + +## On save + +1. Serialize data +2. Generate IV +3. Retrieve secret +4. Encrypt data +5. Concatenate IV + encrypted data +6. Save data to file + +# Implement secret retrieval from OS's keyring + diff --git a/src/bin/totp.rs b/src/bin/totp.rs index 8fb6297..26c8719 100644 --- a/src/bin/totp.rs +++ b/src/bin/totp.rs @@ -22,12 +22,19 @@ struct Cli { impl Cli { fn new() -> Self { - let db = JsonDatabase::new(Self::get_database_path()); + let secret = Self::get_secret(); + let db = JsonDatabase::new(Self::get_database_path(), &secret); Self { app: RusTOTPony::new(db), } } + fn get_secret() -> String { + return rpassword::prompt_password_stdout("Enter your database pass: ").unwrap(); + } + + // fn get_secret_from_storage() -> String { } + fn run(&mut self) { match self.get_cli_api_matches().subcommand() { ("dash", Some(_)) => { diff --git a/src/lib.rs b/src/lib.rs index bab7e2f..fff0348 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ extern crate base32; +extern crate crypto; extern crate oath; extern crate rand; extern crate serde_json; @@ -6,6 +7,11 @@ extern crate serde_json; #[macro_use] extern crate serde_derive; +use crypto::{aes, blockmodes, buffer, symmetriccipher}; +use crypto::buffer::{BufferResult, ReadBuffer, WriteBuffer}; + +use rand::{OsRng, Rng}; + use std::collections::HashMap; use std::fs::{create_dir_all, File, OpenOptions}; use std::io::ErrorKind; @@ -121,11 +127,15 @@ struct DatabaseContentSchema { pub struct JsonDatabase { file_path: PathBuf, + secret: String, } impl JsonDatabase { - pub fn new(path: PathBuf) -> JsonDatabase { - JsonDatabase { file_path: path } + pub fn new(path: PathBuf, secret: &str) -> JsonDatabase { + JsonDatabase { + file_path: path, + secret: String::from(secret), + } } fn read_database_file(&self) -> JsonDatabaseSchema { @@ -147,6 +157,105 @@ impl JsonDatabase { serde_json::to_writer(file, &content).expect("Couldn't write JSON data to database file"); } + // Encrypt a buffer with the given key and iv using + // AES-256/CBC/Pkcs encryption. + fn encrypt( + data: &[u8], + key: &[u8], + iv: &[u8], + ) -> Result, symmetriccipher::SymmetricCipherError> { + // Create an encryptor instance of the best performing + // type available for the platform. + let mut encryptor = + aes::cbc_encryptor(aes::KeySize::KeySize256, key, iv, blockmodes::PkcsPadding); + + // Each encryption operation encrypts some data from + // an input buffer into an output buffer. Those buffers + // must be instances of RefReaderBuffer and RefWriteBuffer + // (respectively) which keep track of how much data has been + // read from or written to them. + let mut final_result = Vec::::new(); + let mut read_buffer = buffer::RefReadBuffer::new(data); + let mut buffer = [0; 4096]; + let mut write_buffer = buffer::RefWriteBuffer::new(&mut buffer); + + // Each encryption operation will "make progress". "Making progress" + // is a bit loosely defined, but basically, at the end of each operation + // either BufferUnderflow or BufferOverflow will be returned (unless + // there was an error). If the return value is BufferUnderflow, it means + // that the operation ended while wanting more input data. If the return + // value is BufferOverflow, it means that the operation ended because it + // needed more space to output data. As long as the next call to the encryption + // operation provides the space that was requested (either more input data + // or more output space), the operation is guaranteed to get closer to + // completing the full operation - ie: "make progress". + // + // Here, we pass the data to encrypt to the enryptor along with a fixed-size + // output buffer. The 'true' flag indicates that the end of the data that + // is to be encrypted is included in the input buffer (which is true, since + // the input data includes all the data to encrypt). After each call, we copy + // any output data to our result Vec. If we get a BufferOverflow, we keep + // going in the loop since it means that there is more work to do. We can + // complete as soon as we get a BufferUnderflow since the encryptor is telling + // us that it stopped processing data due to not having any more data in the + // input buffer. + loop { + let result = try!(encryptor.encrypt(&mut read_buffer, &mut write_buffer, true)); + + // "write_buffer.take_read_buffer().take_remaining()" means: + // from the writable buffer, create a new readable buffer which + // contains all data that has been written, and then access all + // of that data as a slice. + final_result.extend( + write_buffer + .take_read_buffer() + .take_remaining() + .iter() + .map(|&i| i), + ); + + match result { + BufferResult::BufferUnderflow => break, + BufferResult::BufferOverflow => {} + } + } + + Ok(final_result) + } + + // Decrypts a buffer with the given key and iv using + // AES-256/CBC/Pkcs encryption. + fn decrypt( + encrypted_data: &[u8], + key: &[u8], + iv: &[u8], + ) -> Result, symmetriccipher::SymmetricCipherError> { + let mut decryptor = + aes::cbc_decryptor(aes::KeySize::KeySize256, key, iv, blockmodes::PkcsPadding); + + let mut final_result = Vec::::new(); + let mut read_buffer = buffer::RefReadBuffer::new(encrypted_data); + let mut buffer = [0; 4096]; + let mut write_buffer = buffer::RefWriteBuffer::new(&mut buffer); + + loop { + let result = try!(decryptor.decrypt(&mut read_buffer, &mut write_buffer, true)); + final_result.extend( + write_buffer + .take_read_buffer() + .take_remaining() + .iter() + .map(|&i| i), + ); + match result { + BufferResult::BufferUnderflow => break, + BufferResult::BufferOverflow => {} + } + } + + Ok(final_result) + } + 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() { From 4c25bad63c02a5232b9e523dbc8ca70ebe11df60 Mon Sep 17 00:00:00 2001 From: German Lashevich Date: Tue, 3 Apr 2018 18:55:32 +0200 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=A8=20Database=20encryption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bin/totp.rs | 73 ++++++++++++++++++++++++------------------------- src/lib.rs | 42 ++++++++++++++++++++++++---- 2 files changed, 72 insertions(+), 43 deletions(-) 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 From 58dea954312d8c3ce46e4159eacbd243b11c2729 Mon Sep 17 00:00:00 2001 From: German Lashevich Date: Tue, 3 Apr 2018 20:53:21 +0200 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=9A=B8=20Ask=20for=20database=20passw?= =?UTF-8?q?ord=20lazily?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bin/totp.rs | 3 +-- src/lib.rs | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/bin/totp.rs b/src/bin/totp.rs index e5df92d..7e63d22 100644 --- a/src/bin/totp.rs +++ b/src/bin/totp.rs @@ -20,8 +20,7 @@ struct Cli {} impl Cli { fn app() -> RusTOTPony { - let secret = Self::get_secret(); - let db = JsonDatabase::new(Self::get_database_path(), &secret); + let db = JsonDatabase::new(Self::get_database_path(), &Self::get_secret); RusTOTPony::new(db) } diff --git a/src/lib.rs b/src/lib.rs index 6d04bfd..4f9150e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -130,15 +130,15 @@ struct DatabaseContentSchema { pub struct JsonDatabase { file_path: PathBuf, - secret: String, + secret_fn: &'static Fn() -> String, } const IV_SIZE: usize = 16; impl JsonDatabase { - pub fn new(path: PathBuf, secret: &str) -> JsonDatabase { + pub fn new(path: PathBuf, secret_fn: &'static Fn() -> String) -> JsonDatabase { JsonDatabase { file_path: path, - secret: String::from(secret), + secret_fn: secret_fn, } } @@ -148,7 +148,7 @@ impl JsonDatabase { Err(ref err) if err.kind() == ErrorKind::NotFound => return Self::get_empty_schema(), Err(err) => panic!("There was a problem opening file: {:?}", err), }; - let decrypted_data = Self::decrypt_data(&data, self.secret.as_str()); + let decrypted_data = Self::decrypt_data(&data, (self.secret_fn)().as_str()); serde_json::from_str(decrypted_data.as_str()) .expect("Couldn't parse JSON from database file") } @@ -184,7 +184,7 @@ impl JsonDatabase { Err(err) => panic!("Couldn't open database file: {:?}", err), }; let data = serde_json::to_string(&content).expect("Couldn't serialize data to JSON"); - let encrypted_data = Self::encrypt_data(&data, &self.secret); + let encrypted_data = Self::encrypt_data(&data, &(self.secret_fn)()); file.write_all(&encrypted_data) .expect("Couldn't write data to database file"); } From 240667797b71be345133cf0b34469ca574d3e418 Mon Sep 17 00:00:00 2001 From: German Lashevich Date: Tue, 3 Apr 2018 22:38:18 +0200 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=90=9B=20Fix=20after=20refactoring=20?= =?UTF-8?q?Cli=20to=20static?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bin/totp.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/bin/totp.rs b/src/bin/totp.rs index 7e63d22..44f0deb 100644 --- a/src/bin/totp.rs +++ b/src/bin/totp.rs @@ -240,9 +240,10 @@ impl Cli { 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) { + let mut app = Self::app(); + match app.create_application(name, username, &secret) { Ok(_) => { - Self::app().flush(); + app.flush(); println!("New application created: {}", name) } Err(err) => println!("{} Abortingā€¦", err), @@ -250,9 +251,10 @@ impl Cli { } fn delete_application(name: &str) { - match Self::app().delete_application(name) { + let mut app = Self::app(); + match app.delete_application(name) { Ok(_) => { - Self::app().flush(); + app.flush(); println!("Application '{}' successfully deleted", name) } Err(err) => println!("Couldn't delete application '{}': {}", name, err), @@ -260,9 +262,10 @@ impl Cli { } fn rename_application(name: &str, newname: &str) { - match Self::app().rename_application(name, newname) { + let mut app = Self::app(); + match app.rename_application(name, newname) { Ok(_) => { - Self::app().flush(); + app.flush(); println!( "Application '{}' successfully renamed to '{}'", name, newname @@ -273,8 +276,9 @@ impl Cli { } fn eradicate_database() { - Self::app().delete_all_applications(); - Self::app().flush(); + let mut app = Self::app(); + app.delete_all_applications(); + app.flush(); println!("Done."); } } From 3f78dfc9473a5a5d742fc3b4ed5b0a424463e817 Mon Sep 17 00:00:00 2001 From: German Lashevich Date: Tue, 3 Apr 2018 22:40:42 +0200 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=90=9B=20Fix=20encryption=20key=20len?= =?UTF-8?q?gth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib.rs | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4f9150e..85921ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,8 +9,10 @@ extern crate serde_json; #[macro_use] extern crate serde_derive; -use crypto::{aes, blockmodes, buffer, symmetriccipher}; use crypto::buffer::{BufferResult, ReadBuffer, WriteBuffer}; +use crypto::digest::Digest; +use crypto::sha2::Sha256; +use crypto::{aes, blockmodes, buffer, symmetriccipher}; use rand::{OsRng, Rng}; @@ -134,6 +136,7 @@ pub struct JsonDatabase { } const IV_SIZE: usize = 16; +const KEY_SIZE: usize = 32; impl JsonDatabase { pub fn new(path: PathBuf, secret_fn: &'static Fn() -> String) -> JsonDatabase { JsonDatabase { @@ -142,31 +145,38 @@ impl JsonDatabase { } } + fn form_secret_key(input: &str) -> [u8; KEY_SIZE] { + let mut sha = Sha256::new(); + sha.input_str(input); + let mut res: [u8; KEY_SIZE] = [0; KEY_SIZE]; + sha.result(&mut res); + return res; + } + fn read_database_file(&self) -> JsonDatabaseSchema { - let data = match std::fs::read_to_string(&self.file_path) { + let data = match std::fs::read(&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), }; - let decrypted_data = Self::decrypt_data(&data, (self.secret_fn)().as_str()); + let decrypted_data = + Self::decrypt_data(&data, &Self::form_secret_key((self.secret_fn)().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() + fn decrypt_data(data: &[u8], key: &[u8]) -> String { + let iv = &data[..IV_SIZE]; + String::from_utf8(Self::decrypt(&data[IV_SIZE..], key, iv).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 encrypt_data(data: &str, key: &[u8]) -> Vec { + let iv = Self::create_iv(); + let encrypted_data = + Self::encrypt(data.as_bytes(), key, &iv).expect("Couldn't encrypt data"); + [&iv, &encrypted_data[..]].concat() } fn create_iv() -> Vec { @@ -184,7 +194,8 @@ impl JsonDatabase { Err(err) => panic!("Couldn't open database file: {:?}", err), }; let data = serde_json::to_string(&content).expect("Couldn't serialize data to JSON"); - let encrypted_data = Self::encrypt_data(&data, &(self.secret_fn)()); + let encrypted_data = + Self::encrypt_data(&data, &Self::form_secret_key((self.secret_fn)().as_str())); file.write_all(&encrypted_data) .expect("Couldn't write data to database file"); } From be3b84b1849675baf543cac5c90904ee8342aa24 Mon Sep 17 00:00:00 2001 From: German Lashevich Date: Tue, 3 Apr 2018 22:52:11 +0200 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=93=9D=20Update=20TODO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/TODO b/TODO index 7c538c0..2174496 100644 --- a/TODO +++ b/TODO @@ -1,21 +1,5 @@ -# Change approach for saving and loading data - -## On load - -1. Read encrypted data into memory -2. Invoke decrypt function, which takes IV from the beginning of the data -3. Retrieve secret -4. Decrypt data -5. Unserialize data - -## On save - -1. Serialize data -2. Generate IV -3. Retrieve secret -4. Encrypt data -5. Concatenate IV + encrypted data -6. Save data to file - +# Cache password ? +# Update README # Implement secret retrieval from OS's keyring - +# Decompose lib into modules +# Implement tests