diff --git a/Cargo.lock b/Cargo.lock index 1abae61..93d44d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "block-buffer" version = "0.10.3" @@ -46,6 +61,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -158,9 +194,10 @@ dependencies = [ [[package]] name = "ipass" -version = "0.1.0" +version = "0.3.0" dependencies = [ "aes-gcm", + "brotli", "hex", "home", "rand", diff --git a/Cargo.toml b/Cargo.toml index bd90d8c..a218feb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ipass" -version = "0.1.0" +version = "0.3.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -11,4 +11,5 @@ rpassword = "7.2" rand = "0.8.5" aes-gcm = "0.10.1" sha2 = "0.10.6" -hex = "0.4.3" \ No newline at end of file +hex = "0.4.3" +brotli = "3.3.4" \ No newline at end of file diff --git a/IPass_logo.png b/IPass_logo.png new file mode 100644 index 0000000..21a7419 Binary files /dev/null and b/IPass_logo.png differ diff --git a/src/main.rs b/src/main.rs index 5363d35..d6269a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,11 @@ use std::collections::HashMap; use rand::rngs::OsRng; use rand::RngCore; use std::io::Write; +use std::io::Read; mod utils; fn main() { - let version = "0.2.1"; + let version = option_env!("CARGO_PKG_VERSION").unwrap_or("x.x.x"); println!("IPass v{}\n", version); let args = utils::get_args(); @@ -21,12 +22,14 @@ fn main() { "list" => list(), "add" => add(&args), "get" => get(&args), - "edit" => edit(&args), + "changepw" => changepw(&args), + "changeuser" => changeuser(&args), "remove" => remove(&args), "import" => import(&args), "export" => export(&args), "rename" => rename(&args), "version" => version_help(version), + "clear" => clear(), _ => help_message(&args), } } @@ -39,7 +42,6 @@ fn version_help(version: &str) { } fn help_message(args: &Vec) { - let mut help_messages:HashMap = HashMap::new(); help_messages.insert( "list".to_string(), @@ -58,8 +60,8 @@ fn help_message(args: &Vec) { "tells you this message, takes an optional {command name}".to_string(), ); help_messages.insert( - "edit".to_string(), - "lets you edit an existing entry, given the name and the new password".to_string(), + "changepw".to_string(), + "changes the password of the specified entry".to_string(), ); help_messages.insert( "remove".to_string(), @@ -81,12 +83,19 @@ fn help_message(args: &Vec) { "version".to_string(), "explains the current version".to_string() ); - + help_messages.insert( + "changeuser".to_string(), + "changes the username of the specified entry".to_string(), + ); + help_messages.insert( + "clear".to_string(), + "clears all entries".to_string(), + ); if args.len() < 3 { println!("You can use the following commands:"); - for i in help_messages.keys() { - println!("\"{i}\"{}- {}"," ".repeat(8-i.len()),help_messages[i]); + for (cmd, expl) in &help_messages { + println!("\"{cmd}\"{}- {expl}"," ".repeat(12-cmd.len())); } return; } @@ -110,19 +119,20 @@ fn list() { } } -fn add(args: &Vec) { //TODO: format: [specifier] [email or username] {password} +fn add(args: &Vec) { - if args.len() < 3 || args.len() > 4 { + if args.len() < 4 || args.len() > 5 { println!("Incorrect usage of \"add\""); return; } let pw: String; + let username:String = args[3].to_string(); - if args.len() > 3 { - pw = args[3].trim().to_owned(); + if args.len() > 4 { + pw = username+";"+args[4].trim(); } else { - let alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!\"§$%&/()=?´`²³{[]}\\,.-;:_><|+*#'"; + let alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!\"$%&/()=?{[]}\\,.-;:_><|+*#'"; let alph_len = alphabet.chars().count(); let char_set:Vec = alphabet.chars().collect(); let mut chars_index: Vec = vec![0;20]; @@ -133,7 +143,7 @@ fn add(args: &Vec) { //TODO: format: [specifier] [email or username] {pa // println!("{} - {} - {}",index,(index as usize)%(alph_len-1),alph_len); chars += &char_set[(index as usize)%(alph_len-1)].to_string(); } - pw = chars; + pw = username+";"+chars.as_str(); println!("Using auto generated password"); // println!("pw: {pw}"); @@ -158,16 +168,18 @@ fn get(args: &Vec) { let filepath = &(utils::get_ipass_folder()+name+".ipass"); if std::path::Path::new(filepath).exists() { println!("Getting entry"); - println!("{}",utils::get_entry(name)); + let entry = utils::get_entry(name); + let mut data = entry.split(";"); + println!("Username: '{}' Password: '{}'",data.next().unwrap(),data.next().unwrap()); } else { println!("No such entry!"); return; } } -fn edit(args: &Vec) { +fn changepw(args: &Vec) { //rename func to changepw if args.len() < 3 { - println!("Invalid usage of \"edit\""); + println!("Invalid usage of \"changepw\""); return; } let filepath = &(utils::get_ipass_folder()+&args[2]+".ipass"); @@ -179,8 +191,7 @@ fn edit(args: &Vec) { output = args[3].clone(); } - let mut file = std::fs::File::create(format!("{}/{}.ipass",utils::get_ipass_folder(),args[2])).unwrap(); - file.write_all(output.replace("\n", "").replace("\r","").as_bytes()).unwrap(); + utils::edit_password(&args[2], output); println!("Changed Password of {}!", args[2]); } else { @@ -188,6 +199,28 @@ fn edit(args: &Vec) { } } +fn changeuser(args: &Vec) { + if args.len() < 3 { + println!("Invalid usage of \"changeuser\""); + return; + } + let filepath = &(utils::get_ipass_folder()+&args[2]+".ipass"); + if std::path::Path::new(filepath).exists() { + let output: String; + if args.len() != 4 { + output = utils::prompt_answer("Enter new Username: ".to_string()); + } else { + output = args[3].clone(); + } + + utils::edit_username(&args[2], output); + + println!("Changed Username of {}!", args[2]); + } else { + println!("No such file!"); + } +} + fn rename(args: &Vec) { // prog ren old new if args.len() < 4 { println!("Invalid usage of \"rename\""); @@ -233,7 +266,29 @@ fn import(args: &Vec) { } } if std::path::Path::new(&(location.clone()+"/export.ipassx")).exists() { - let content = &mut std::fs::read_to_string(location.clone()+"/export.ipassx").expect("Should have been able to read the file"); + let mut reader = brotli::Decompressor::new( + std::fs::File::open(location.clone()+"/export.ipassx").unwrap(), + 4096, // buffer size + ); + let mut content: String = String::new(); + let mut buf = [0u8; 4096]; + loop { + match reader.read(&mut buf[..]) { + Err(e) => { + if let std::io::ErrorKind::Interrupted = e.kind() { + continue; + } + panic!("{}", e); + } + Ok(size) => { + if size == 0 { + break; + } + content += &std::str::from_utf8(&buf[..size]).unwrap(); + } + } + } + let lines = content.lines(); let mut name = ""; for i in lines { @@ -275,12 +330,39 @@ fn export(args: &Vec) { //TODO: compress data } } - if let Ok(mut file) = std::fs::File::create(location.clone()+"/export.ipassx") { - file.write_all(collected_data.as_bytes()).unwrap(); + + + if let Ok(file) = std::fs::File::create(location.clone()+"/export.ipassx") { + let mut writer = brotli::CompressorWriter::new( + file, + 4096, + 11, + 22); + + match writer.write_all(collected_data.as_bytes()) { + Err(e) => panic!("{}", e), + Ok(_) => {}, + } println!("Saved at: '{}/export.ipassx'", location); } else { println!("Failed saving at '{}/export.ipassx' does it exist?",location) } +} + +fn clear() { + if utils::prompt_answer("Are you sure you want to clear everything? [y/N] ".to_string()) != "y" { + println!("operation cancelled!"); + return; + } + + let paths = std::fs::read_dir(utils::get_ipass_folder()).unwrap(); + + for path in paths { + if let Ok(p) = path { + std::fs::remove_file(utils::get_ipass_folder()+"/"+p.file_name().into_string().unwrap().as_str()).unwrap(); + } + } + println!("Cleared all entries!"); } \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs index 61ebf0b..132dd7b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -10,15 +10,23 @@ pub fn get_args() -> Vec { } fn vecu8_to_string(vec: Vec) -> String { + let mut do_print_warning = false; let mut out: String = String::new(); for ind in vec { if let Ok(a) = std::str::from_utf8(&[ind]) { out += a; } else { - panic!("malformed character"); + do_print_warning = true; + eprintln!("[WARNING] malformed character {}",ind); + let mut temp_vec: Vec = Vec::new(); + temp_vec.insert(0,ind%128); + out += vecu8_to_string(temp_vec).as_str(); } } - return out + if do_print_warning { + println!("[WARNING] Output may be corrupt"); + } + return out; } fn encrypt_pass(nonce_arg:String, pass: String,mpw: String) -> String { @@ -29,13 +37,20 @@ fn encrypt_pass(nonce_arg:String, pass: String,mpw: String) -> String { if nonce_arg.len() > 12 { nonce_argument = nonce_arg[0..12].to_string(); } + + let mut nonce_hasher = Sha256::new(); + nonce_hasher.update(nonce_argument.as_bytes()); + + let nonce_final = &nonce_hasher.finalize()[0..12]; + + let mut hasher = Sha256::new(); hasher.update(mpw.as_bytes()); let master_pw = &hasher.finalize(); let cipher = Aes256Gcm::new(master_pw); - let nonce = Nonce::from_slice(nonce_argument.as_bytes()); // 96-bits; unique per message + let nonce = Nonce::from_slice(nonce_final); // 96-bits; unique per message let ciphertext = cipher.encrypt(nonce, pass.as_ref()).unwrap(); return hex::encode(ciphertext); } @@ -48,15 +63,29 @@ fn decrypt_pass(nonce_arg:String, pass: Vec,mpw: String) -> String { if nonce_arg.len() > 12 { nonce_argument = nonce_arg[0..12].to_string(); } + + let mut nonce_hasher = Sha256::new(); + nonce_hasher.update(nonce_argument.as_bytes()); + + let nonce_final = &nonce_hasher.finalize()[0..12]; + let mut hasher = Sha256::new(); hasher.update(mpw.as_bytes()); let master_pw = &hasher.finalize(); let cipher = Aes256Gcm::new(master_pw); - let nonce = Nonce::from_slice(nonce_argument.as_bytes()); // 96-bits; unique per message + let nonce = Nonce::from_slice(nonce_final); // 96-bits; unique per message - let plaintext = cipher.decrypt(nonce, pass.as_ref()).unwrap(); - return vecu8_to_string(plaintext); + let plaintext = cipher.decrypt(nonce, pass.as_ref()); + match plaintext { + Ok(res) => { + return vecu8_to_string(res); + } + Err(_) => { + eprintln!("[ERROR] Error decrypting data, check your master password"); + std::process::exit(1); + } + } } pub fn get_home_folder_str() -> String { @@ -82,21 +111,46 @@ pub fn create_entry(name: &String, pw: String) -> bool { if std::path::Path::new(&(get_ipass_folder()+name+".ipass")).exists() { return false; } - edit_entry(name, pw); + let mpw = ask_for_pw(); + // println!("{pw}"); + let pw = encrypt_pass(name.to_owned(), pw,mpw); + let mut file = std::fs::File::create(get_ipass_folder()+name+".ipass").unwrap(); + file.write_all(pw.as_bytes()).unwrap(); return true; } -pub fn get_entry(name:&String) -> String { +fn read_entry(name:&String,mpw:String) -> String { let content = &mut std::fs::read_to_string(get_ipass_folder()+name+".ipass").expect("Should have been able to read the file"); - let mpw = ask_for_pw(); return decrypt_pass(name.to_owned(),hex::decode(content).unwrap(),mpw).to_owned(); } -pub fn edit_entry(name:&String,mut pw:String) { +pub fn get_entry(name:&String) -> String { let mpw = ask_for_pw(); - pw = encrypt_pass(name.to_owned(), pw,mpw); + return read_entry(name,mpw); +} + +pub fn edit_password(name:&String, password:String) { + let mpw = ask_for_pw(); + let entry = read_entry(name, mpw.clone()); + // println!("entry: {entry}"); + let mut parts = entry.split(";"); + let username = parts.next().unwrap().to_string(); + let _old_password = parts.next().unwrap(); + let data = encrypt_pass(name.to_owned(), username+";"+password.as_str(),mpw); let mut file = std::fs::File::create(get_ipass_folder()+name+".ipass").unwrap(); - file.write_all(pw.as_bytes()).unwrap(); + file.write_all(data.as_bytes()).unwrap(); +} + +pub fn edit_username(name:&String, username: String) { + let mpw = ask_for_pw(); + let entry = read_entry(name, mpw.clone()); + // println!("entry: {entry}"); + let mut parts = entry.split(";"); + let _old_username = parts.next().unwrap(); + let password = parts.next().unwrap(); + let data = encrypt_pass(name.to_owned(), username+";"+password,mpw); + let mut file = std::fs::File::create(get_ipass_folder()+name+".ipass").unwrap(); + file.write_all(data.as_bytes()).unwrap(); } fn ask_for_pw() -> String {