From b198ec9d1f051e78b092254b13f8387daf7a88c9 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Sat, 7 Jan 2023 01:36:49 -0600 Subject: [PATCH 01/38] Bumped version for next dev cycle. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6b30986..69ecbec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rucksack" description = "A terminal-based password manager, generator, and importer (Firefox, Chrome)" -version = "0.5.0" +version = "0.6.0-dev" license = "Apache-2.0" authors = ["Duncan McGreggor "] repository = "https://github.com/oxur/rucksack" From b22ae17400fe0945e575fa48aaa6cc6a93670b88 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Sat, 7 Jan 2023 01:54:35 -0600 Subject: [PATCH 02/38] Updated project desc. --- Cargo.toml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 69ecbec..abfd208 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rucksack" -description = "A terminal-based password manager, generator, and importer (Firefox, Chrome)" +description = "A terminal-based password manager, generator, and importer (Firefox, Chrome) backed with a concurrent hashmap" version = "0.6.0-dev" license = "Apache-2.0" authors = ["Duncan McGreggor "] diff --git a/README.md b/README.md index fa893a2..27c617a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![][logo]][logo-large] -*A terminal-based password manager, generator, and importer (Firefox, Chrome)* +*A terminal-based password manager, generator, and importer (Firefox, Chrome) backed with a concurrent hashmap* ## Features From 845a298c99a4c1eb1e9c0c495912cb0ec779c290 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Sat, 7 Jan 2023 01:56:36 -0600 Subject: [PATCH 03/38] Updated wording in feature list. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 27c617a..8768449 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ * [x] Password generator * [x] Encrypted local storage * [x] Concurrent hashmap for use by daemons -* [x] Supports Firefox Sync +* [x] Supports Firefox and Chrome CSV formats (for importing and exporting) * [x] List secrets (encrypted and decrypted) * [x] Searching secrets (filtering) * [x] Reports (quality, duplicates, etc.) From 93e7e4d147c8a2822f5ac5b18c27fb502d2f27ef Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Sat, 7 Jan 2023 17:11:08 -0600 Subject: [PATCH 04/38] Fixed bug #37. --- Cargo.toml | 2 +- src/cli/command/arg.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index abfd208..8e5508c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ aes-gcm = "0.10.1" anyhow = "1.0" bincode = { version = "2.0.0-rc.2", features = ["serde"] } chrono = "0.4.23" -clap = "4.0.32" +clap = { version = "4.0.32", features = ["string"] } clap_complete = "4.0" confyg = "0.1.3" crc32fast = "1.3.2" diff --git a/src/cli/command/arg.rs b/src/cli/command/arg.rs index 58dbd46..e46a72e 100644 --- a/src/cli/command/arg.rs +++ b/src/cli/command/arg.rs @@ -18,7 +18,14 @@ pub fn pwd_arg() -> Arg { pub fn salt_arg() -> Arg { Arg::new("salt") .help("the salt to use for encrypting the database") - .default_value(env!("USER")) + .default_value(default_salt()) .short('s') .long("salt") } + +fn default_salt() -> String { + match std::env::var("USER") { + Ok(user) => user, + Err(_) => "rucksack".to_string(), + } +} From 6dbf03315221090c9fe2fcc3532a64c9ac86363a Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Sat, 7 Jan 2023 17:37:47 -0600 Subject: [PATCH 05/38] Added config and logging support. --- Cargo.toml | 2 ++ config.toml | 4 ++++ src/config/loader.rs | 8 ++++++++ src/config/mod.rs | 4 ++++ src/config/schema.rs | 6 ++++++ src/lib.rs | 1 + src/main.rs | 12 +++++++++++- 7 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 config.toml create mode 100644 src/config/loader.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/schema.rs diff --git a/Cargo.toml b/Cargo.toml index 8e5508c..1dae9f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,12 +27,14 @@ crc32fast = "1.3.2" csv = "1.1.6" dashmap = { version = "5.4.0", features = ["serde"] } lipsum = "0.8.2" +log = "0.4.17" passwords = "3.1.12" rand = "0.8" rpassword = "7.1" secrecy = "0.8.0" serde = { version = "1.0", features = ["derive"] } shellexpand = "3.0.0" +twyg = "0.1.10" url = "2.3.1" uuid = {version = "1.2.2", features = ["v4"]} diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..4f45e31 --- /dev/null +++ b/config.toml @@ -0,0 +1,4 @@ +[logging] +coloured = true +level = "debug" +report_caller = true diff --git a/src/config/loader.rs b/src/config/loader.rs new file mode 100644 index 0000000..d5bdd29 --- /dev/null +++ b/src/config/loader.rs @@ -0,0 +1,8 @@ +use confyg::Confygery; + +use super::schema; + +pub fn load() -> schema::Config { + let cfg: schema::Config = Confygery::new().add_file("./config.toml").build().unwrap(); + cfg +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..94af14a --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,4 @@ +pub mod loader; +pub mod schema; + +pub use loader::load; diff --git a/src/config/schema.rs b/src/config/schema.rs new file mode 100644 index 0000000..cdb7266 --- /dev/null +++ b/src/config/schema.rs @@ -0,0 +1,6 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Config { + pub logging: twyg::LoggerOpts, +} diff --git a/src/lib.rs b/src/lib.rs index 9b007be..43e63a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod cli; +pub mod config; pub mod csv; pub mod generator; pub mod store; diff --git a/src/main.rs b/src/main.rs index 1edad68..25476bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use log; use std::io; use anyhow::{Context, Result}; @@ -5,7 +6,7 @@ use clap::builder::EnumValueParser; use clap::{Arg, ArgAction, ArgMatches, Command}; use rucksack::cli::command::{arg, export, gen, import, list}; -use rucksack::util; +use rucksack::{config, util}; const NAME: &str = env!("CARGO_PKG_NAME"); const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -193,6 +194,15 @@ fn run(matches: &ArgMatches) -> Result<()> { } fn main() -> Result<()> { + let cfg = config::load(); + match twyg::setup_logger(&cfg.logging) { + Ok(_) => {} + Err(error) => { + panic!("Could not setup logger: {:?}", error) + } + } + log::debug!("Config setup complete."); + log::debug!("Logger setup complete."); let mut rucksack = cli(); let matches = rucksack.clone().get_matches(); From 5257f13c611b0ee0127ab67e3d69e4ac25ce69c6 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Sat, 7 Jan 2023 17:40:14 -0600 Subject: [PATCH 06/38] Addeed more logging. --- src/main.rs | 1 - src/time.rs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 25476bf..d3cc090 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -use log; use std::io; use anyhow::{Context, Result}; diff --git a/src/time.rs b/src/time.rs index ddae85e..236eb96 100644 --- a/src/time.rs +++ b/src/time.rs @@ -19,8 +19,7 @@ pub fn string_to_epoch(stamp: String) -> i64 { match DateTime::parse_from_rfc3339(&stamp) { Ok(dt) => dt.timestamp_millis(), Err(e) => { - // TODO: change to debug logging - println!("{:?}", e); + log::debug!("{:?}", e); Local::now().timestamp_millis() } } From 4455dacc72a217b5815363581d4d3e9b51073866 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Sat, 7 Jan 2023 17:56:56 -0600 Subject: [PATCH 07/38] Implemented app abstraction. --- src/app.rs | 6 ++++++ src/cli/command/import.rs | 14 +++++++------- src/cli/command/mod.rs | 2 ++ src/config/mod.rs | 1 + src/lib.rs | 1 + src/main.rs | 10 ++++++---- 6 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 src/app.rs diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..a2ce77e --- /dev/null +++ b/src/app.rs @@ -0,0 +1,6 @@ +use crate::{config, store}; + +pub struct App { + pub cfg: config::Config, + pub db: store::db::DB, +} diff --git a/src/cli/command/import.rs b/src/cli/command/import.rs index afbbae2..0e3d758 100644 --- a/src/cli/command/import.rs +++ b/src/cli/command/import.rs @@ -5,21 +5,21 @@ use crate::csv; use crate::csv::{chrome, firefox}; use crate::store; -use super::util; +use crate::app; -pub fn new(matches: &ArgMatches) -> Result<()> { +pub fn new(matches: &ArgMatches, app: &app::App) -> Result<()> { let import_file = matches.get_one::("file").unwrap().to_string(); - let db = util::setup_db(matches)?; + match matches.get_one::("type").map(|s| s.as_str()) { - Some("chrome") => from_chrome_csv(db, import_file)?, - Some("firefox") => from_firefox_csv(db, import_file)?, + Some("chrome") => from_chrome_csv(&app.db, import_file)?, + Some("firefox") => from_firefox_csv(&app.db, import_file)?, Some(_) => todo!(), None => todo!(), }; Ok(()) } -fn from_chrome_csv(db: store::db::DB, csv_path: String) -> Result<(), anyhow::Error> { +fn from_chrome_csv(db: &store::db::DB, csv_path: String) -> Result<(), anyhow::Error> { println!("Importing data from {}:", csv_path); let mut rdr = csv::reader::from_path(csv_path)?; let mut count = 0; @@ -33,7 +33,7 @@ fn from_chrome_csv(db: store::db::DB, csv_path: String) -> Result<(), anyhow::Er db.close() } -fn from_firefox_csv(db: store::db::DB, csv_path: String) -> Result<(), anyhow::Error> { +fn from_firefox_csv(db: &store::db::DB, csv_path: String) -> Result<(), anyhow::Error> { println!("Importing data from {}:", csv_path); let mut rdr = csv::reader::from_path(csv_path)?; let mut count: usize = 0; diff --git a/src/cli/command/mod.rs b/src/cli/command/mod.rs index 4aa21cf..72bd9da 100644 --- a/src/cli/command/mod.rs +++ b/src/cli/command/mod.rs @@ -5,3 +5,5 @@ pub mod import; pub mod list; pub mod prompt; pub mod util; + +pub use util::setup_db; diff --git a/src/config/mod.rs b/src/config/mod.rs index 94af14a..ad427ad 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -2,3 +2,4 @@ pub mod loader; pub mod schema; pub use loader::load; +pub use schema::Config; diff --git a/src/lib.rs b/src/lib.rs index 43e63a9..448109d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod app; pub mod cli; pub mod config; pub mod csv; diff --git a/src/main.rs b/src/main.rs index d3cc090..5580585 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; use clap::builder::EnumValueParser; use clap::{Arg, ArgAction, ArgMatches, Command}; -use rucksack::cli::command::{arg, export, gen, import, list}; +use rucksack::cli::command::{arg, export, gen, import, list, setup_db}; use rucksack::{config, util}; const NAME: &str = env!("CARGO_PKG_NAME"); @@ -180,11 +180,13 @@ fn cli() -> Command { } // fn run(matches: &ArgMatches, config: &kbs2::config::Config) -> Result<()> { -fn run(matches: &ArgMatches) -> Result<()> { +fn run(matches: &ArgMatches, cfg: config::Config) -> Result<()> { + let db = setup_db(matches)?; + let app = rucksack::app::App { cfg, db }; match matches.subcommand() { Some(("export", matches)) => export::new(matches)?, Some(("gen", matches)) => gen::new(matches)?, - Some(("import", matches)) => import::new(matches)?, + Some(("import", matches)) => import::new(matches, &app)?, Some(("list", matches)) => list::all(matches)?, Some((&_, _)) => todo!(), None => todo!(), @@ -224,5 +226,5 @@ fn main() -> Result<()> { } // match run(&matches, &config) { - run(&matches) + run(&matches, cfg) } From 8263063e50282ca3e55da5e8f827304caf6b8fdd Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Sat, 7 Jan 2023 23:33:35 -0600 Subject: [PATCH 08/38] Updated the rest of the commands to take the app. --- src/cli/command/export.rs | 20 +++++++++----------- src/cli/command/import.rs | 3 +-- src/cli/command/list.rs | 15 +++++++-------- src/main.rs | 6 ++---- 4 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/cli/command/export.rs b/src/cli/command/export.rs index e9bf2a3..0d8c345 100644 --- a/src/cli/command/export.rs +++ b/src/cli/command/export.rs @@ -1,26 +1,24 @@ use anyhow::{anyhow, Result}; use clap::ArgMatches; +use crate::app; use crate::csv::writer; use crate::csv::{chrome, firefox}; use crate::store; -use crate::util as crate_util; +use crate::util::write_file; -use super::util; - -pub fn new(matches: &ArgMatches) -> Result<()> { +pub fn new(matches: &ArgMatches, app: &app::App) -> Result<()> { let export_file = matches.get_one::("file").unwrap().to_string(); - let db = util::setup_db(matches)?; match matches.get_one::("type").map(|s| s.as_str()) { - Some("chrome") => to_chrome_csv(db, export_file)?, - Some("firefox") => to_firefox_csv(db, export_file)?, + Some("chrome") => to_chrome_csv(&app.db, export_file)?, + Some("firefox") => to_firefox_csv(&app.db, export_file)?, Some(_) => todo!(), None => todo!(), }; Ok(()) } -fn to_chrome_csv(db: store::db::DB, csv_path: String) -> Result<(), anyhow::Error> { +fn to_chrome_csv(db: &store::db::DB, csv_path: String) -> Result<(), anyhow::Error> { let mut wtr = writer::to_bytes()?; let mut count = 0; for dr in db.collect_decrypted()? { @@ -32,13 +30,13 @@ fn to_chrome_csv(db: store::db::DB, csv_path: String) -> Result<(), anyhow::Erro match wtr.into_inner() { Ok(data) => { print_report(count, db.hash_map().len()); - crate_util::write_file(data, csv_path) + write_file(data, csv_path) } Err(e) => Err(anyhow!(e)), } } -fn to_firefox_csv(db: store::db::DB, csv_path: String) -> Result<(), anyhow::Error> { +fn to_firefox_csv(db: &store::db::DB, csv_path: String) -> Result<(), anyhow::Error> { let mut wtr = writer::to_bytes()?; let mut count = 0; for dr in db.collect_decrypted()? { @@ -50,7 +48,7 @@ fn to_firefox_csv(db: store::db::DB, csv_path: String) -> Result<(), anyhow::Err match wtr.into_inner() { Ok(data) => { print_report(count, db.hash_map().len()); - crate_util::write_file(data, csv_path) + write_file(data, csv_path) } Err(e) => Err(anyhow!(e)), } diff --git a/src/cli/command/import.rs b/src/cli/command/import.rs index 0e3d758..36020cb 100644 --- a/src/cli/command/import.rs +++ b/src/cli/command/import.rs @@ -1,12 +1,11 @@ use anyhow::Result; use clap::ArgMatches; +use crate::app; use crate::csv; use crate::csv::{chrome, firefox}; use crate::store; -use crate::app; - pub fn new(matches: &ArgMatches, app: &app::App) -> Result<()> { let import_file = matches.get_one::("file").unwrap().to_string(); diff --git a/src/cli/command/list.rs b/src/cli/command/list.rs index 99b8a55..314ae9a 100644 --- a/src/cli/command/list.rs +++ b/src/cli/command/list.rs @@ -4,7 +4,7 @@ use anyhow::Result; use clap::ArgMatches; use passwords::{analyzer, scorer}; -use super::util; +use crate::app; #[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] struct ListResult { @@ -25,7 +25,7 @@ fn new_result(user: String, url: String) -> ListResult { type GroupByString = HashMap>; -pub fn all(matches: &ArgMatches) -> Result<()> { +pub fn all(matches: &ArgMatches, app: &app::App) -> Result<()> { let decrypt = matches.get_one::("decrypt"); let filter = matches.get_one::("filter"); let exclude = matches.get_one::("exclude"); @@ -34,11 +34,10 @@ pub fn all(matches: &ArgMatches) -> Result<()> { let reveal = matches.get_one::("reveal"); let sort_by = matches.get_one::("sort-by").map(|s| s.as_str()); let group_by = matches.get_one::("group-by").map(|s| s.as_str()); - let db = util::setup_db(matches)?; let mut results: Vec = Vec::new(); let mut groups = GroupByString::new(); - for i in db.iter() { - let record = i.value().decrypt(db.store_pwd(), db.salt())?; + for i in app.db.iter() { + let record = i.value().decrypt(app.db.store_pwd(), app.db.salt())?; let analyzed = analyzer::analyze(record.password()); let score = scorer::score(&analyzed); let mut result = new_result(record.user(), record.metadata().url); @@ -94,16 +93,16 @@ pub fn all(matches: &ArgMatches) -> Result<()> { Some("password") => { let (group_count, record_count) = print_password_group(groups, decrypt, reveal, sort_by); - print_group_report(group_count, record_count, db.hash_map().len()); + print_group_report(group_count, record_count, app.db.hash_map().len()); } Some("user") => { let (group_count, record_count) = print_user_group(groups, decrypt, sort_by); - print_group_report(group_count, record_count, db.hash_map().len()); + print_group_report(group_count, record_count, app.db.hash_map().len()); } Some(&_) => (), None => { print_results(&results, decrypt); - print_report(results.len(), db.hash_map().len()); + print_report(results.len(), app.db.hash_map().len()); } } diff --git a/src/main.rs b/src/main.rs index 5580585..098c9df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -179,15 +179,14 @@ fn cli() -> Command { ) } -// fn run(matches: &ArgMatches, config: &kbs2::config::Config) -> Result<()> { fn run(matches: &ArgMatches, cfg: config::Config) -> Result<()> { let db = setup_db(matches)?; let app = rucksack::app::App { cfg, db }; match matches.subcommand() { - Some(("export", matches)) => export::new(matches)?, + Some(("export", matches)) => export::new(matches, &app)?, Some(("gen", matches)) => gen::new(matches)?, Some(("import", matches)) => import::new(matches, &app)?, - Some(("list", matches)) => list::all(matches)?, + Some(("list", matches)) => list::all(matches, &app)?, Some((&_, _)) => todo!(), None => todo!(), } @@ -225,6 +224,5 @@ fn main() -> Result<()> { .with_context(|| "failed to print help".to_string()); } - // match run(&matches, &config) { run(&matches, cfg) } From c1882582583b4bd7d395546804cd2e38b38c1bd8 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Sat, 7 Jan 2023 23:41:55 -0600 Subject: [PATCH 09/38] Cleanup. --- src/cli/command/export.rs | 10 +++++----- src/cli/command/import.rs | 10 +++++----- src/cli/command/list.rs | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/cli/command/export.rs b/src/cli/command/export.rs index 0d8c345..164dc90 100644 --- a/src/cli/command/export.rs +++ b/src/cli/command/export.rs @@ -1,13 +1,13 @@ use anyhow::{anyhow, Result}; use clap::ArgMatches; -use crate::app; +use crate::app::App; use crate::csv::writer; use crate::csv::{chrome, firefox}; -use crate::store; +use crate::store::db::DB; use crate::util::write_file; -pub fn new(matches: &ArgMatches, app: &app::App) -> Result<()> { +pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { let export_file = matches.get_one::("file").unwrap().to_string(); match matches.get_one::("type").map(|s| s.as_str()) { Some("chrome") => to_chrome_csv(&app.db, export_file)?, @@ -18,7 +18,7 @@ pub fn new(matches: &ArgMatches, app: &app::App) -> Result<()> { Ok(()) } -fn to_chrome_csv(db: &store::db::DB, csv_path: String) -> Result<(), anyhow::Error> { +fn to_chrome_csv(db: &DB, csv_path: String) -> Result<(), anyhow::Error> { let mut wtr = writer::to_bytes()?; let mut count = 0; for dr in db.collect_decrypted()? { @@ -36,7 +36,7 @@ fn to_chrome_csv(db: &store::db::DB, csv_path: String) -> Result<(), anyhow::Err } } -fn to_firefox_csv(db: &store::db::DB, csv_path: String) -> Result<(), anyhow::Error> { +fn to_firefox_csv(db: &DB, csv_path: String) -> Result<(), anyhow::Error> { let mut wtr = writer::to_bytes()?; let mut count = 0; for dr in db.collect_decrypted()? { diff --git a/src/cli/command/import.rs b/src/cli/command/import.rs index 36020cb..58a9b0b 100644 --- a/src/cli/command/import.rs +++ b/src/cli/command/import.rs @@ -1,12 +1,12 @@ use anyhow::Result; use clap::ArgMatches; -use crate::app; +use crate::app::App; use crate::csv; use crate::csv::{chrome, firefox}; -use crate::store; +use crate::store::db::DB; -pub fn new(matches: &ArgMatches, app: &app::App) -> Result<()> { +pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { let import_file = matches.get_one::("file").unwrap().to_string(); match matches.get_one::("type").map(|s| s.as_str()) { @@ -18,7 +18,7 @@ pub fn new(matches: &ArgMatches, app: &app::App) -> Result<()> { Ok(()) } -fn from_chrome_csv(db: &store::db::DB, csv_path: String) -> Result<(), anyhow::Error> { +fn from_chrome_csv(db: &DB, csv_path: String) -> Result<(), anyhow::Error> { println!("Importing data from {}:", csv_path); let mut rdr = csv::reader::from_path(csv_path)?; let mut count = 0; @@ -32,7 +32,7 @@ fn from_chrome_csv(db: &store::db::DB, csv_path: String) -> Result<(), anyhow::E db.close() } -fn from_firefox_csv(db: &store::db::DB, csv_path: String) -> Result<(), anyhow::Error> { +fn from_firefox_csv(db: &DB, csv_path: String) -> Result<(), anyhow::Error> { println!("Importing data from {}:", csv_path); let mut rdr = csv::reader::from_path(csv_path)?; let mut count: usize = 0; diff --git a/src/cli/command/list.rs b/src/cli/command/list.rs index 314ae9a..047e17a 100644 --- a/src/cli/command/list.rs +++ b/src/cli/command/list.rs @@ -4,7 +4,7 @@ use anyhow::Result; use clap::ArgMatches; use passwords::{analyzer, scorer}; -use crate::app; +use crate::app::App; #[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] struct ListResult { @@ -25,7 +25,7 @@ fn new_result(user: String, url: String) -> ListResult { type GroupByString = HashMap>; -pub fn all(matches: &ArgMatches, app: &app::App) -> Result<()> { +pub fn all(matches: &ArgMatches, app: &App) -> Result<()> { let decrypt = matches.get_one::("decrypt"); let filter = matches.get_one::("filter"); let exclude = matches.get_one::("exclude"); From 30896d118c61bb7939cf74b1eeabf8c35275114d Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Sun, 8 Jan 2023 22:38:04 -0600 Subject: [PATCH 10/38] Moved about DB setup. --- src/app.rs | 3 +-- src/lib.rs | 3 +++ src/main.rs | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index a2ce77e..63cd38a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,5 @@ -use crate::{config, store}; +use crate::config; pub struct App { pub cfg: config::Config, - pub db: store::db::DB, } diff --git a/src/lib.rs b/src/lib.rs index 448109d..75e3a42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,3 +6,6 @@ pub mod generator; pub mod store; pub mod time; pub mod util; + +pub use app::App; +pub use config::Config; diff --git a/src/main.rs b/src/main.rs index 098c9df..cc098af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -179,9 +179,7 @@ fn cli() -> Command { ) } -fn run(matches: &ArgMatches, cfg: config::Config) -> Result<()> { - let db = setup_db(matches)?; - let app = rucksack::app::App { cfg, db }; +fn run(matches: &ArgMatches, app: &rucksack::App) -> Result<()> { match matches.subcommand() { Some(("export", matches)) => export::new(matches, &app)?, Some(("gen", matches)) => gen::new(matches)?, @@ -224,5 +222,7 @@ fn main() -> Result<()> { .with_context(|| "failed to print help".to_string()); } - run(&matches, cfg) + let db = setup_db(&matches)?; + let app = rucksack::app::App { cfg, db }; + run(&matches, &app) } From 07d3180a1068706af615c356e0f6e376e9210a68 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Sun, 8 Jan 2023 22:23:21 -0600 Subject: [PATCH 11/38] Added an id to the list result. --- src/app.rs | 3 ++- src/cli/command/list.rs | 8 +++++--- src/main.rs | 6 +++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/app.rs b/src/app.rs index 63cd38a..a2ce77e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,6 @@ -use crate::config; +use crate::{config, store}; pub struct App { pub cfg: config::Config, + pub db: store::db::DB, } diff --git a/src/cli/command/list.rs b/src/cli/command/list.rs index 047e17a..554f045 100644 --- a/src/cli/command/list.rs +++ b/src/cli/command/list.rs @@ -8,14 +8,16 @@ use crate::app::App; #[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] struct ListResult { - url: String, + id: String, user: String, + url: String, pwd: String, score: i64, } -fn new_result(user: String, url: String) -> ListResult { +fn new_result(id: String, user: String, url: String) -> ListResult { ListResult { + id, user, url, @@ -40,7 +42,7 @@ pub fn all(matches: &ArgMatches, app: &App) -> Result<()> { let record = i.value().decrypt(app.db.store_pwd(), app.db.salt())?; let analyzed = analyzer::analyze(record.password()); let score = scorer::score(&analyzed); - let mut result = new_result(record.user(), record.metadata().url); + let mut result = new_result(record.key(), record.user(), record.metadata().url); if let Some(check) = filter { if !i.key().contains(check) { continue; diff --git a/src/main.rs b/src/main.rs index cc098af..9d3a275 100644 --- a/src/main.rs +++ b/src/main.rs @@ -181,10 +181,10 @@ fn cli() -> Command { fn run(matches: &ArgMatches, app: &rucksack::App) -> Result<()> { match matches.subcommand() { - Some(("export", matches)) => export::new(matches, &app)?, + Some(("export", matches)) => export::new(matches, app)?, Some(("gen", matches)) => gen::new(matches)?, - Some(("import", matches)) => import::new(matches, &app)?, - Some(("list", matches)) => list::all(matches, &app)?, + Some(("import", matches)) => import::new(matches, app)?, + Some(("list", matches)) => list::all(matches, app)?, Some((&_, _)) => todo!(), None => todo!(), } From 9c84de6ca8629905f8e67f837b593a1f66429ec2 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Sun, 8 Jan 2023 23:14:17 -0600 Subject: [PATCH 12/38] Got around db locks. --- README.md | 16 +++++++++------- src/cli/command/list.rs | 21 ++++++++++++++++++++- src/main.rs | 12 +++--------- src/store/db.rs | 33 +++++++++++++++++++++++++++++++-- 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 8768449..72a3650 100644 --- a/README.md +++ b/README.md @@ -66,18 +66,18 @@ Password score: 100.00 Import login data from Firefox Sync: ```shell -./bin/rucksack import \ +./bin/rucksack --password abc123 \ + import \ --type firefox \ - --password abc123 \ --file ~/Downloads/logins.csv ``` Logins may be exported to files that can then be used to import into browsers: ```shell -./bin/rucksack export \ +./bin/rucksack --password abc123 \ + export \ --type chrome \ - --password abc123 \ --file /tmp/exported-logins.csv ``` @@ -98,7 +98,7 @@ Enter db password: Show URLs, accounts, passwords, and password scores for all secrets: ```shell -./bin/rucksack list --db --decrypt +./bin/rucksack list --decrypt ``` ```shell @@ -111,13 +111,15 @@ Note that without `--decrypt`, only the user and URL are displayed. With `--decr The default database location used is `./data/creds.db`. To use another location, the `--db` flag is available. +The flags `--db`, `--password`, and `--salt` must be set at the top-level, before any subcommands. + ### Search / Filter Secrets Simple filtering is also possible (done using a flag with the `list` command, with or without sorting): ```shell -./bin/rucksack list \ - --password abc123 \ +./bin/rucksack --password abc123 \ + list \ --filter exa \ --sort-by score \ --decrypt diff --git a/src/cli/command/list.rs b/src/cli/command/list.rs index 554f045..b545b7d 100644 --- a/src/cli/command/list.rs +++ b/src/cli/command/list.rs @@ -5,6 +5,7 @@ use clap::ArgMatches; use passwords::{analyzer, scorer}; use crate::app::App; +use crate::time; #[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] struct ListResult { @@ -25,6 +26,12 @@ fn new_result(id: String, user: String, url: String) -> ListResult { } } +impl ListResult { + pub fn id(&self) -> String { + self.id.clone() + } +} + type GroupByString = HashMap>; pub fn all(matches: &ArgMatches, app: &App) -> Result<()> { @@ -107,7 +114,19 @@ pub fn all(matches: &ArgMatches, app: &App) -> Result<()> { print_report(results.len(), app.db.hash_map().len()); } } - + for r in results { + match reveal { + Some(true) => { + if let Some(mut metadata) = app.db.get_metadata(r.id()) { + metadata.last_used = time::now(); + metadata.access_count += 1; + app.db.update_metadata(r.id(), metadata); + } + } + Some(false) => (), + None => unreachable!(), + } + } Ok(()) } diff --git a/src/main.rs b/src/main.rs index 9d3a275..8965557 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,9 @@ fn cli() -> Command { .long("version") .action(ArgAction::SetTrue) ) + .arg(arg::db_arg()) + .arg(arg::pwd_arg()) + .arg(arg::salt_arg()) .subcommand( Command::new("export") .about("export the rucksack db") @@ -47,9 +50,6 @@ fn cli() -> Command { .short('f') .long("file"), ) - .arg(arg::db_arg()) - .arg(arg::pwd_arg()) - .arg(arg::salt_arg()) ) .subcommand( Command::new("gen") @@ -110,9 +110,6 @@ fn cli() -> Command { .short('f') .long("file"), ) - .arg(arg::db_arg()) - .arg(arg::pwd_arg()) - .arg(arg::salt_arg()) ) .subcommand( Command::new("list") @@ -173,9 +170,6 @@ fn cli() -> Command { .default_value("url") .value_parser(["score", "url", "user"]), ) - .arg(arg::db_arg()) - .arg(arg::pwd_arg()) - .arg(arg::salt_arg()) ) } diff --git a/src/store/db.rs b/src/store/db.rs index a534055..d7b2b68 100644 --- a/src/store/db.rs +++ b/src/store/db.rs @@ -7,7 +7,7 @@ use dashmap::DashMap; use crate::{time, util}; use super::crypto::{decrypt, encrypt}; -use super::record::{DecryptedRecord, EncryptedRecord}; +use super::record::{DecryptedRecord, EncryptedRecord, Metadata}; #[derive(Clone, Default)] pub struct DB { @@ -80,16 +80,45 @@ impl DB { } pub fn insert(&self, record: DecryptedRecord) -> Option { + let key = record.key(); + log::debug!("Inserting record with key {} ...", key); self.hash_map - .insert(record.key(), record.encrypt(self.store_pwd(), self.salt())) + .insert(key, record.encrypt(self.store_pwd(), self.salt())) } pub fn get(&self, key: String) -> Option { + log::debug!("Getting record with key {} ...", key); self.hash_map .get(&key) .map(|encrypted| encrypted.decrypt(self.store_pwd(), self.salt()).unwrap()) } + pub fn get_metadata(&self, key: String) -> Option { + log::debug!("Getting metadata of record with key {} ...", key); + match self.get(key.clone()) { + Some(r) => Some(r.metadata()), + None => { + log::debug!("key {:} not found", key); + None + } + } + } + + pub fn update_metadata(&self, key: String, metadata: Metadata) { + log::debug!("Updating metadata on record with key {} ...", key); + match self.hash_map.try_entry(key) { + Some(entry) => { + entry.and_modify(|r| r.metadata = metadata); + log::debug!("updated!") + } + None => { + let msg = "Couldn't get lock for update"; + log::error!("{}", msg); + panic!("{}", msg) + } + } + } + pub fn iter(&self) -> dashmap::iter::Iter { self.hash_map.iter() } From dcab60c4ccea7e32232cb1c9afba829c8076b667 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Sun, 8 Jan 2023 23:18:31 -0600 Subject: [PATCH 13/38] Added debug export type. --- src/cli/command/export.rs | 17 +++++++++++------ src/main.rs | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/cli/command/export.rs b/src/cli/command/export.rs index 164dc90..1b21008 100644 --- a/src/cli/command/export.rs +++ b/src/cli/command/export.rs @@ -8,16 +8,21 @@ use crate::store::db::DB; use crate::util::write_file; pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { + log::debug!("Running 'export' subcommand ..."); + let export_type = matches.get_one::("type").map(|s| s.as_str()); + match export_type { + Some("debug") => to_stdout(app), + Some(_) => Ok(()), + None => Ok(()), + }?; let export_file = matches.get_one::("file").unwrap().to_string(); - match matches.get_one::("type").map(|s| s.as_str()) { - Some("chrome") => to_chrome_csv(&app.db, export_file)?, - Some("firefox") => to_firefox_csv(&app.db, export_file)?, + match export_type { + Some("chrome") => to_chrome_csv(app, export_file), + Some("firefox") => to_firefox_csv(app, export_file), Some(_) => todo!(), None => todo!(), - }; - Ok(()) + } } - fn to_chrome_csv(db: &DB, csv_path: String) -> Result<(), anyhow::Error> { let mut wtr = writer::to_bytes()?; let mut count = 0; diff --git a/src/main.rs b/src/main.rs index 8965557..fed7a4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,7 +42,7 @@ fn cli() -> Command { .short('t') .long("type") .default_value("firefox") - .value_parser(["chrome", "firefox"]), + .value_parser(["chrome", "debug", "firefox"]), ) .arg( Arg::new("file") From 9228daf7ac0acd7813e628ba06bb3e223372221d Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Sun, 8 Jan 2023 23:44:58 -0600 Subject: [PATCH 14/38] Re-added debug export. --- src/cli/command/export.rs | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/cli/command/export.rs b/src/cli/command/export.rs index 1b21008..a34e92e 100644 --- a/src/cli/command/export.rs +++ b/src/cli/command/export.rs @@ -4,17 +4,15 @@ use clap::ArgMatches; use crate::app::App; use crate::csv::writer; use crate::csv::{chrome, firefox}; -use crate::store::db::DB; use crate::util::write_file; pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { log::debug!("Running 'export' subcommand ..."); let export_type = matches.get_one::("type").map(|s| s.as_str()); - match export_type { - Some("debug") => to_stdout(app), - Some(_) => Ok(()), - None => Ok(()), - }?; + if export_type.is_some() { + to_stdout(app)?; + return Ok(()); + } let export_file = matches.get_one::("file").unwrap().to_string(); match export_type { Some("chrome") => to_chrome_csv(app, export_file), @@ -23,10 +21,25 @@ pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { None => todo!(), } } -fn to_chrome_csv(db: &DB, csv_path: String) -> Result<(), anyhow::Error> { + +fn to_stdout(app: &App) -> Result<()> { + match app.db.collect_decrypted() { + Ok(rs) => { + for r in rs { + println!("{:?}", r) + } + } + Err(e) => { + log::error!("{:?}", e) + } + } + Ok(()) +} + +fn to_chrome_csv(app: &App, csv_path: String) -> Result<(), anyhow::Error> { let mut wtr = writer::to_bytes()?; let mut count = 0; - for dr in db.collect_decrypted()? { + for dr in app.db.collect_decrypted()? { wtr.serialize(chrome::from_decrypted(dr))?; count += 1; print!("."); @@ -34,17 +47,17 @@ fn to_chrome_csv(db: &DB, csv_path: String) -> Result<(), anyhow::Error> { wtr.flush()?; match wtr.into_inner() { Ok(data) => { - print_report(count, db.hash_map().len()); + print_report(count, app.db.hash_map().len()); write_file(data, csv_path) } Err(e) => Err(anyhow!(e)), } } -fn to_firefox_csv(db: &DB, csv_path: String) -> Result<(), anyhow::Error> { +fn to_firefox_csv(app: &App, csv_path: String) -> Result<(), anyhow::Error> { let mut wtr = writer::to_bytes()?; let mut count = 0; - for dr in db.collect_decrypted()? { + for dr in app.db.collect_decrypted()? { wtr.serialize(firefox::from_decrypted(dr))?; count += 1; print!("."); @@ -52,7 +65,7 @@ fn to_firefox_csv(db: &DB, csv_path: String) -> Result<(), anyhow::Error> { wtr.flush()?; match wtr.into_inner() { Ok(data) => { - print_report(count, db.hash_map().len()); + print_report(count, app.db.hash_map().len()); write_file(data, csv_path) } Err(e) => Err(anyhow!(e)), From e4d5cf3a2ee7aed95866b64d9b524d8d19807f7c Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Tue, 10 Jan 2023 22:15:57 -0600 Subject: [PATCH 15/38] Added encoding flag and feature. --- Cargo.toml | 1 + src/cli/command/gen.rs | 34 +++++++++++++++++++--------------- src/generator/password.rs | 14 ++++++++++++-- src/main.rs | 7 +++++++ 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1dae9f2..03cf821 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ name = "rucksack" aead = "0.5.1" aes-gcm = "0.10.1" anyhow = "1.0" +base64 = "0.21" bincode = { version = "2.0.0-rc.2", features = ["serde"] } chrono = "0.4.23" clap = { version = "4.0.32", features = ["string"] } diff --git a/src/cli/command/gen.rs b/src/cli/command/gen.rs index 59afbfa..29b46bc 100644 --- a/src/cli/command/gen.rs +++ b/src/cli/command/gen.rs @@ -5,12 +5,13 @@ use crate::generator::{password, uuid}; // pub fn generate(matches: &ArgMatches, config: &config::Config) -> Result<()> { pub fn new(matches: &ArgMatches) -> Result<()> { + let encode = matches.get_one::("encode"); match matches.get_one::("type").map(|s| s.as_str()) { - Some("lipsum") => generate_pwd_lipsum(matches), - Some("random") => generate_pwd(matches), - Some("uuid") => generate_pwd_uuid(), - Some("uuid+") => generate_pwd_uuid_plus(), - Some("uuid++") => generate_pwd_uuid_special(), + Some("lipsum") => generate_pwd_lipsum(matches, encode), + Some("random") => generate_pwd(matches, encode), + Some("uuid") => generate_pwd_uuid(encode), + Some("uuid+") => generate_pwd_uuid_plus(encode), + Some("uuid++") => generate_pwd_uuid_special(encode), Some(_) => todo!(), None => todo!(), } @@ -18,30 +19,33 @@ pub fn new(matches: &ArgMatches) -> Result<()> { // Generator type dispatch functions -fn generate_pwd(matches: &ArgMatches) -> Result<()> { +fn generate_pwd(matches: &ArgMatches, encode: Option<&bool>) -> Result<()> { let length = matches.get_one::("length").unwrap(); - password::display_scored(&password::rand(length)) + password::display_scored(password::rand(length), encode) } -fn generate_pwd_lipsum(matches: &ArgMatches) -> Result<()> { +fn generate_pwd_lipsum(matches: &ArgMatches, encode: Option<&bool>) -> Result<()> { let delimiter = matches .get_one::("delimiter") .map(|s| s.as_str()) .unwrap(); let suffix_length = matches.get_one::("suffix-length").unwrap(); let word_count = matches.get_one::("word-count").unwrap(); - password::display_scored(&password::lipsum(word_count, suffix_length, delimiter)) + password::display_scored( + password::lipsum(word_count, suffix_length, delimiter), + encode, + ) } -fn generate_pwd_uuid() -> Result<()> { - password::display_scored(&uuid::v4_string()) +fn generate_pwd_uuid(encode: Option<&bool>) -> Result<()> { + password::display_scored(uuid::v4_string(), encode) } -fn generate_pwd_uuid_plus() -> Result<()> { - password::display_scored(&uuid::v4_with_uppers()) +fn generate_pwd_uuid_plus(encode: Option<&bool>) -> Result<()> { + password::display_scored(uuid::v4_with_uppers(), encode) } -fn generate_pwd_uuid_special() -> Result<()> { +fn generate_pwd_uuid_special(encode: Option<&bool>) -> Result<()> { let number_of_specials = 3; - password::display_scored(&uuid::v4_with_specials(number_of_specials)) + password::display_scored(uuid::v4_with_specials(number_of_specials), encode) } diff --git a/src/generator/password.rs b/src/generator/password.rs index 2e58340..e21a2b4 100644 --- a/src/generator/password.rs +++ b/src/generator/password.rs @@ -2,12 +2,22 @@ use rand::Rng; use std::str; use anyhow::Result; +use base64::engine::general_purpose as b64; +use base64::Engine; use passwords::{analyzer, scorer, PasswordGenerator}; use crate::util; -pub fn display_scored(pwd: &str) -> Result<()> { - let analyzed = analyzer::analyze(pwd); +pub fn display_scored(mut pwd: String, encode: Option<&bool>) -> Result<()> { + match encode { + Some(true) => { + let bytes = pwd.as_bytes(); + pwd = b64::URL_SAFE_NO_PAD.encode(bytes); + } + Some(false) => (), + None => (), + } + let analyzed = analyzer::analyze(pwd.clone()); let score = scorer::score(&analyzed); let msg = format!("\nNew password: {}\nPassword score: {:.2}\n", pwd, score); util::display(&msg) diff --git a/src/main.rs b/src/main.rs index fed7a4b..590d0b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -91,6 +91,13 @@ fn cli() -> Command { .short('d') .long("delimiter") .default_value("-"), + ) + .arg( + Arg::new("encode") + .help("encode the generated password (uses base64)") + .short('e') + .long("encode") + .action(ArgAction::SetTrue), ), ) .subcommand( From 949a2ae1bca44f59e807a7d947049dbbf97e2982 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Tue, 10 Jan 2023 22:29:59 -0600 Subject: [PATCH 16/38] Bumped description. --- Cargo.toml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 03cf821..17fa900 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rucksack" -description = "A terminal-based password manager, generator, and importer (Firefox, Chrome) backed with a concurrent hashmap" +description = "A terminal-based password manager, generator, and importer/exporter (Firefox, Chrome) backed with a concurrent hashmap" version = "0.6.0-dev" license = "Apache-2.0" authors = ["Duncan McGreggor "] diff --git a/README.md b/README.md index 72a3650..b932946 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![][logo]][logo-large] -*A terminal-based password manager, generator, and importer (Firefox, Chrome) backed with a concurrent hashmap* +*A terminal-based password manager, generator, and importer/exporter (Firefox, Chrome) backed with a concurrent hashmap* ## Features From fb15e7f374115c671e874029e47858f8a59cb532 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Tue, 10 Jan 2023 22:34:55 -0600 Subject: [PATCH 17/38] Updated docs. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index b932946..53cdc68 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,15 @@ New password: Esse-maius-amicitia,-nihil.-]9^, Password score: 100.00 ``` +Some systems can't handle special characters, so a flag is available for encoding with base64, with the generated encoding getting scored: + +```shell +./bin/rucksack gen --type lipsum --encode + +New password: VmVydW0sLW9waW5vciwtc2NyaXB0b3JlbS10YW1lbi4tLjYrfQ +Password score: 100.00 +``` + ### Importing and Exporting Import login data from Firefox Sync: From 7ce02338a012cbdabbb7f806163f9c374a5eb638 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Tue, 10 Jan 2023 23:13:23 -0600 Subject: [PATCH 18/38] Moved db setup back down the subcommand stack. --- README.md | 12 ++++++------ src/cli/command/util.rs | 18 +++++++++++------- src/main.rs | 15 +++++++++++---- src/store/db.rs | 13 +++++++++++++ 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 53cdc68..602498c 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,8 @@ Password score: 100.00 Import login data from Firefox Sync: ```shell -./bin/rucksack --password abc123 \ - import \ +./bin/rucksack import \ + --password abc123 \ --type firefox \ --file ~/Downloads/logins.csv ``` @@ -84,8 +84,8 @@ Import login data from Firefox Sync: Logins may be exported to files that can then be used to import into browsers: ```shell -./bin/rucksack --password abc123 \ - export \ +./bin/rucksack export \ + --password abc123 \ --type chrome \ --file /tmp/exported-logins.csv ``` @@ -127,8 +127,8 @@ The flags `--db`, `--password`, and `--salt` must be set at the top-level, befor Simple filtering is also possible (done using a flag with the `list` command, with or without sorting): ```shell -./bin/rucksack --password abc123 \ - list \ +./bin/rucksack list \ + --password abc123 \ --filter exa \ --sort-by score \ --decrypt diff --git a/src/cli/command/util.rs b/src/cli/command/util.rs index 7abb6dd..6f65784 100644 --- a/src/cli/command/util.rs +++ b/src/cli/command/util.rs @@ -7,11 +7,15 @@ use super::prompt; use crate::store::db; pub fn setup_db(matches: &ArgMatches) -> Result { - let db_file = matches.get_one::("db").unwrap().to_string(); - let pwd = match matches.get_one::("password") { - Some(flag_pwd) => SecretString::new(flag_pwd.to_owned()), - None => prompt::secret("Enter db password: ").unwrap(), - }; - let salt = matches.get_one::("salt").unwrap().to_string(); - db::open(db_file, pwd.expose_secret().to_string(), salt) + match matches.get_one::("db") { + Some(db_file) => { + let pwd = match matches.get_one::("password") { + Some(flag_pwd) => SecretString::new(flag_pwd.to_owned()), + None => prompt::secret("Enter db password: ").unwrap(), + }; + let salt = matches.get_one::("salt").unwrap().to_string(); + db::open(db_file.to_owned(), pwd.expose_secret().to_string(), salt) + } + None => Ok(db::new()), + } } diff --git a/src/main.rs b/src/main.rs index 590d0b2..8f261ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,9 +30,6 @@ fn cli() -> Command { .long("version") .action(ArgAction::SetTrue) ) - .arg(arg::db_arg()) - .arg(arg::pwd_arg()) - .arg(arg::salt_arg()) .subcommand( Command::new("export") .about("export the rucksack db") @@ -50,6 +47,9 @@ fn cli() -> Command { .short('f') .long("file"), ) + .arg(arg::db_arg()) + .arg(arg::pwd_arg()) + .arg(arg::salt_arg()) ) .subcommand( Command::new("gen") @@ -117,6 +117,9 @@ fn cli() -> Command { .short('f') .long("file"), ) + .arg(arg::db_arg()) + .arg(arg::pwd_arg()) + .arg(arg::salt_arg()) ) .subcommand( Command::new("list") @@ -177,6 +180,9 @@ fn cli() -> Command { .default_value("url") .value_parser(["score", "url", "user"]), ) + .arg(arg::db_arg()) + .arg(arg::pwd_arg()) + .arg(arg::salt_arg()) ) } @@ -223,7 +229,8 @@ fn main() -> Result<()> { .with_context(|| "failed to print help".to_string()); } - let db = setup_db(&matches)?; + let (_, subcmd_matches) = matches.subcommand().unwrap(); + let db = setup_db(subcmd_matches)?; let app = rucksack::app::App { cfg, db }; run(&matches, &app) } diff --git a/src/store/db.rs b/src/store/db.rs index d7b2b68..d7dd606 100644 --- a/src/store/db.rs +++ b/src/store/db.rs @@ -17,6 +17,7 @@ pub struct DB { salt: String, bincode_cfg: bincode::config::Configuration, hash_map: DashMap, + enabled: bool, } pub fn init(path: String, store_pwd: String, updated: String) -> Result<()> { @@ -24,6 +25,12 @@ pub fn init(path: String, store_pwd: String, updated: String) -> Result<()> { db.close() } +pub fn new() -> DB { + DB { + ..Default::default() + } +} + pub fn open(path: String, store_pwd: String, salt: String) -> Result { let mut hash_map: DashMap = DashMap::new(); let mut store_hash = 0; @@ -35,6 +42,7 @@ pub fn open(path: String, store_pwd: String, salt: String) -> Result { store_hash = crc32fast::hash(decrypted.as_ref()); (hash_map, _len) = bincode::serde::decode_from_slice(decrypted.as_ref(), bincode_cfg)?; } + let enabled = true; Ok(DB { path, store_hash, @@ -42,10 +50,15 @@ pub fn open(path: String, store_pwd: String, salt: String) -> Result { salt, bincode_cfg, hash_map, + enabled, }) } impl DB { + pub fn enabled(&self) -> bool { + self.enabled + } + pub fn path(&self) -> String { self.path.clone() } From 8dd5e115ead0b51fcad7eb77f5535c4c3a1cd535 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Wed, 11 Jan 2023 00:05:53 -0600 Subject: [PATCH 19/38] Added count column. --- config.toml | 2 +- src/cli/command/list.rs | 41 ++++++++++++++++++++++++++++------------- src/store/db.rs | 10 +++++----- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/config.toml b/config.toml index 4f45e31..b894a9a 100644 --- a/config.toml +++ b/config.toml @@ -1,4 +1,4 @@ [logging] coloured = true -level = "debug" +level = "trace" report_caller = true diff --git a/src/cli/command/list.rs b/src/cli/command/list.rs index b545b7d..270ecd3 100644 --- a/src/cli/command/list.rs +++ b/src/cli/command/list.rs @@ -13,6 +13,7 @@ struct ListResult { user: String, url: String, pwd: String, + access_count: u64, score: i64, } @@ -78,6 +79,7 @@ pub fn all(matches: &ArgMatches, app: &App) -> Result<()> { None => unreachable!(), }; result.pwd = pwd; + result.access_count = record.metadata().access_count; result.score = score.trunc() as i64; } Some(false) => result.pwd = hidden(), @@ -217,18 +219,20 @@ const URL_HEADER: &str = "URL"; const USER_HEADER: &str = "User / Account"; const PWD_HEADER: &str = "Password"; const SCORE_HEADER: &str = "Score / Strength"; +const COUNT_HEADER: &str = "Access Count"; fn decrypted_header() { println!( - "\n{: <40} | {: <30} | {: <20} | {}", - URL_HEADER, USER_HEADER, PWD_HEADER, SCORE_HEADER + "\n{: <40} | {: <30} | {: <20} | {: <15} | {}", + URL_HEADER, USER_HEADER, PWD_HEADER, SCORE_HEADER, COUNT_HEADER ); println!( - "{: <40}-+-{: <30}-+-{: <20}-+-{}", + "{: <40}-+-{: <30}-+-{: <20}-+-{: <15}-+-{}", "-".repeat(40), "-".repeat(30), "-".repeat(20), - "-".repeat(16) + "-".repeat(16), + "-".repeat(12), ) } @@ -246,19 +250,27 @@ fn decrypted_no_user_header() { } fn encrypted_header() { - println!("\n{: <40} | {: <30}", URL_HEADER, USER_HEADER); - println!("{: <40}-+-{}", "-".repeat(40), "-".repeat(30)) + println!( + "\n{: <40} | {: <30} | {}", + URL_HEADER, USER_HEADER, COUNT_HEADER + ); + println!( + "{:40}-+-{:30}-+-{}", + "-".repeat(40), + "-".repeat(30), + "-".repeat(12) + ) } fn encrypted_no_user_header() { - println!("\n{}", URL_HEADER); - println!("{}", "-".repeat(40)) + println!("\n{: <40} | {}", URL_HEADER, COUNT_HEADER); + println!("{:40}-+-{}", "-".repeat(40), "-".repeat(12)) } fn decrypted_result(r: &ListResult) { println!( - "{: <40} | {: <30} | {: <20} | {:.2}", - r.url, r.user, r.pwd, r.score + "{: <40} | {: <30} | {: <20} | {: ^16.2} | {: ^12}", + r.url, r.user, r.pwd, r.score, r.access_count ) } @@ -290,15 +302,18 @@ fn user_section(r: &ListResult, decrypted: Option<&bool>) { } fn encrypted_result(r: &ListResult) { - println!("{: <40} | {}", r.url, r.user) + println!("{: <40} | {: <30} | {: ^12}", r.url, r.user, r.access_count) } fn decrypted_no_user_result(r: &ListResult) { - println!("{: <40} | {: <20} | {:.2}", r.url, r.pwd, r.score) + println!( + "{: <40} | {: <20} | {: ^16.2} | {}", + r.url, r.pwd, r.score, r.access_count + ) } fn encrypted_no_user_result(r: &ListResult) { - println!("{}", r.url) + println!("{: <40} | {: ^12}", r.url, r.access_count) } fn hidden() -> String { diff --git a/src/store/db.rs b/src/store/db.rs index d7dd606..ce35941 100644 --- a/src/store/db.rs +++ b/src/store/db.rs @@ -94,20 +94,20 @@ impl DB { pub fn insert(&self, record: DecryptedRecord) -> Option { let key = record.key(); - log::debug!("Inserting record with key {} ...", key); + log::trace!("Inserting record with key {} ...", key); self.hash_map .insert(key, record.encrypt(self.store_pwd(), self.salt())) } pub fn get(&self, key: String) -> Option { - log::debug!("Getting record with key {} ...", key); + log::trace!("Getting record with key {} ...", key); self.hash_map .get(&key) .map(|encrypted| encrypted.decrypt(self.store_pwd(), self.salt()).unwrap()) } pub fn get_metadata(&self, key: String) -> Option { - log::debug!("Getting metadata of record with key {} ...", key); + log::trace!("Getting metadata of record with key {} ...", key); match self.get(key.clone()) { Some(r) => Some(r.metadata()), None => { @@ -118,11 +118,11 @@ impl DB { } pub fn update_metadata(&self, key: String, metadata: Metadata) { - log::debug!("Updating metadata on record with key {} ...", key); + log::trace!("Updating metadata on record with key {} ...", key); match self.hash_map.try_entry(key) { Some(entry) => { entry.and_modify(|r| r.metadata = metadata); - log::debug!("updated!") + log::trace!("updated!") } None => { let msg = "Couldn't get lock for update"; From 36d4ba9f933da32d963a05fb2e360b594ee2acc9 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Wed, 11 Jan 2023 00:46:31 -0600 Subject: [PATCH 20/38] Fixed data persistence when listing. --- config.toml | 2 +- src/cli/command/list.rs | 3 +++ src/store/db.rs | 6 ++++++ src/store/record.rs | 4 ++-- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/config.toml b/config.toml index b894a9a..4f45e31 100644 --- a/config.toml +++ b/config.toml @@ -1,4 +1,4 @@ [logging] coloured = true -level = "trace" +level = "debug" report_caller = true diff --git a/src/cli/command/list.rs b/src/cli/command/list.rs index 270ecd3..6b5457b 100644 --- a/src/cli/command/list.rs +++ b/src/cli/command/list.rs @@ -116,6 +116,8 @@ pub fn all(matches: &ArgMatches, app: &App) -> Result<()> { print_report(results.len(), app.db.hash_map().len()); } } + // With the dash_map iteration finished, the lock is gone, and we can + // now update records: for r in results { match reveal { Some(true) => { @@ -129,6 +131,7 @@ pub fn all(matches: &ArgMatches, app: &App) -> Result<()> { None => unreachable!(), } } + app.db.close()?; Ok(()) } diff --git a/src/store/db.rs b/src/store/db.rs index ce35941..9baf678 100644 --- a/src/store/db.rs +++ b/src/store/db.rs @@ -81,11 +81,17 @@ impl DB { let encoded = bincode::serde::encode_to_vec(self.hash_map(), self.bincode_cfg).unwrap(); let store_hash = crc32fast::hash(encoded.as_ref()); if store_hash == self.store_hash { + log::debug!("No change in store hash; not persisting ..."); return Ok(()); } fs::create_dir_all(path.parent().unwrap())?; if std::path::Path::new(&self.path).exists() { let backup_name = format!("{}-{}", self.path, time::simple_timestamp()); + log::debug!( + "Backing up db from {} to {:} ...", + path.display(), + backup_name + ); std::fs::copy(path, backup_name)?; } let encrypted = encrypt(encoded, self.store_pwd(), self.salt()); diff --git a/src/store/record.rs b/src/store/record.rs index 8868fe1..a7778ad 100644 --- a/src/store/record.rs +++ b/src/store/record.rs @@ -13,7 +13,7 @@ pub enum Kind { Password, } -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Encode, Decode)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Encode, Decode)] pub struct Metadata { pub kind: Kind, pub url: String, @@ -31,7 +31,7 @@ pub struct Creds { pub password: String, } -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Encode, Decode)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Encode, Decode)] pub struct EncryptedRecord { pub key: String, pub value: Vec, From 072f8cce4ab97527030ddae810a2d787d7135245 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Wed, 11 Jan 2023 09:59:39 -0600 Subject: [PATCH 21/38] Ooops; this commit is from an older change that was never saved. --- src/cli/command/list.rs | 2 +- src/store/db.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/cli/command/list.rs b/src/cli/command/list.rs index 6b5457b..b6a9c71 100644 --- a/src/cli/command/list.rs +++ b/src/cli/command/list.rs @@ -117,7 +117,7 @@ pub fn all(matches: &ArgMatches, app: &App) -> Result<()> { } } // With the dash_map iteration finished, the lock is gone, and we can - // now update records: + // now update all the records whose passwords were revealed: for r in results { match reveal { Some(true) => { diff --git a/src/store/db.rs b/src/store/db.rs index 9baf678..9cfcc98 100644 --- a/src/store/db.rs +++ b/src/store/db.rs @@ -88,8 +88,7 @@ impl DB { if std::path::Path::new(&self.path).exists() { let backup_name = format!("{}-{}", self.path, time::simple_timestamp()); log::debug!( - "Backing up db from {} to {:} ...", - path.display(), + "Path to db already exists; backing up to {} ...", backup_name ); std::fs::copy(path, backup_name)?; From 5df2a5ba4e8bfe67c822f31c9c056626dae15f93 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Wed, 11 Jan 2023 20:08:44 -0600 Subject: [PATCH 22/38] Added 'add' command. Fixes #27. --- README.md | 12 ++++++------ src/cli/command/add.rs | 43 +++++++++++++++++++++++++++++++++++++++++ src/cli/command/arg.rs | 37 ++++++++++++++++++++++++++++++++--- src/cli/command/mod.rs | 2 ++ src/cli/command/util.rs | 2 +- src/main.rs | 26 ++++++++++++++++++++++++- src/store/record.rs | 2 ++ 7 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 src/cli/command/add.rs diff --git a/README.md b/README.md index 602498c..1d148bc 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Import login data from Firefox Sync: ```shell ./bin/rucksack import \ - --password abc123 \ + --db-pass abc123 \ --type firefox \ --file ~/Downloads/logins.csv ``` @@ -85,7 +85,7 @@ Logins may be exported to files that can then be used to import into browsers: ```shell ./bin/rucksack export \ - --password abc123 \ + --db-pass abc123 \ --type chrome \ --file /tmp/exported-logins.csv ``` @@ -114,13 +114,13 @@ Show URLs, accounts, passwords, and password scores for all secrets: Enter db password: ``` -In both cases a password may be passed with the `--password` flag. By default, the salt is the value of the `USER` environment variable; it may be overridden with `--salt`. +In both cases a password may be passed with the `--db-pass` flag. By default, the salt is the value of the `USER` environment variable, but it may be overridden with the `--salt` flag. Note that without `--decrypt`, only the user and URL are displayed. With `--decrypt`, those as well as masked password and password score are displayed. To unmask the password, one must also set `--reveal`. The default database location used is `./data/creds.db`. To use another location, the `--db` flag is available. -The flags `--db`, `--password`, and `--salt` must be set at the top-level, before any subcommands. +The flags `--db`, `--db-pass`, and `--salt` may be set for any subcommand that access the database. ### Search / Filter Secrets @@ -128,7 +128,7 @@ Simple filtering is also possible (done using a flag with the `list` command, wi ```shell ./bin/rucksack list \ - --password abc123 \ + --db-pass abc123 \ --filter exa \ --sort-by score \ --decrypt @@ -160,7 +160,7 @@ For use in auditing, sites+user combinations that share the same password can be ```shell ./bin/rucksack list \ - --group-by password \ + --group-by db-pass \ --decrypt ``` diff --git a/src/cli/command/add.rs b/src/cli/command/add.rs new file mode 100644 index 0000000..fded45b --- /dev/null +++ b/src/cli/command/add.rs @@ -0,0 +1,43 @@ +use anyhow::Result; +use clap::ArgMatches; + +use crate::app::App; +use crate::store::record; +use crate::store::{Creds, DecryptedRecord, Metadata}; +use crate::time; + +pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { + log::debug!("Running 'add' subcommand ..."); + let account_type = matches.get_one::("type").map(|s| s.as_str()); + let default_kind = record::Kind::Password; + let kind = match account_type { + Some("account") => record::Kind::Account, + Some("creds") => record::Kind::Credential, + Some("credential") => record::Kind::Credential, + Some("password") => record::Kind::Password, + Some("") => default_kind, + Some(&_) => todo!(), + None => default_kind, + }; + + let user = matches.get_one::("user").unwrap().to_string(); + let password = matches.get_one::("password").unwrap().to_string(); + let url = matches.get_one::("url").unwrap().to_string(); + let now = time::now(); + + let creds = Creds { user, password }; + let metadata = Metadata { + kind, + url, + created: now.clone(), + imported: now.clone(), + updated: now.clone(), + password_changed: now.clone(), + last_used: now, + access_count: 0, + }; + let dr = DecryptedRecord { creds, metadata }; + app.db.insert(dr); + app.db.close()?; + Ok(()) +} diff --git a/src/cli/command/arg.rs b/src/cli/command/arg.rs index e46a72e..e34ccc7 100644 --- a/src/cli/command/arg.rs +++ b/src/cli/command/arg.rs @@ -1,5 +1,7 @@ use clap::Arg; +// Database Flags + pub fn db_arg() -> Arg { Arg::new("db") .help("path to the encrypted database to use") @@ -9,10 +11,9 @@ pub fn db_arg() -> Arg { } pub fn pwd_arg() -> Arg { - Arg::new("password") + Arg::new("db-pass") .help("password used to encrypt the database") - .short('p') - .long("password") + .long("db-pass") } pub fn salt_arg() -> Arg { @@ -29,3 +30,33 @@ fn default_salt() -> String { Err(_) => "rucksack".to_string(), } } + +// Account Flags + +pub fn account_type() -> Arg { + Arg::new("type") + .help("the type of secret to add") + .short('t') + .long("type") + // These next have not yet been defined/refined: + .value_parser(["", "account", "credential", "creds", "password"]) +} + +pub fn account_user() -> Arg { + Arg::new("user") + .help("the user, login, or account, identifier") + .short('u') + .long("user") + .required(true) +} + +pub fn account_pass() -> Arg { + Arg::new("password") + .help("the account / login password") + .long("password") + .required(true) +} + +pub fn account_url() -> Arg { + Arg::new("url").help("the login URL").long("url") +} diff --git a/src/cli/command/mod.rs b/src/cli/command/mod.rs index 72bd9da..3504057 100644 --- a/src/cli/command/mod.rs +++ b/src/cli/command/mod.rs @@ -1,9 +1,11 @@ +pub mod add; pub mod arg; pub mod export; pub mod gen; pub mod import; pub mod list; pub mod prompt; +pub mod update; pub mod util; pub use util::setup_db; diff --git a/src/cli/command/util.rs b/src/cli/command/util.rs index 6f65784..aacc260 100644 --- a/src/cli/command/util.rs +++ b/src/cli/command/util.rs @@ -9,7 +9,7 @@ use crate::store::db; pub fn setup_db(matches: &ArgMatches) -> Result { match matches.get_one::("db") { Some(db_file) => { - let pwd = match matches.get_one::("password") { + let pwd = match matches.get_one::("db-pass") { Some(flag_pwd) => SecretString::new(flag_pwd.to_owned()), None => prompt::secret("Enter db password: ").unwrap(), }; diff --git a/src/main.rs b/src/main.rs index 8f261ee..e7a0fb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; use clap::builder::EnumValueParser; use clap::{Arg, ArgAction, ArgMatches, Command}; -use rucksack::cli::command::{arg, export, gen, import, list, setup_db}; +use rucksack::cli::command::{add, arg, export, gen, import, list, setup_db, update}; use rucksack::{config, util}; const NAME: &str = env!("CARGO_PKG_NAME"); @@ -30,6 +30,17 @@ fn cli() -> Command { .long("version") .action(ArgAction::SetTrue) ) + .subcommand( + Command::new("add") + .about("add a new secret") + .arg(arg::account_type()) + .arg(arg::account_user()) + .arg(arg::account_pass()) + .arg(arg::account_url()) + .arg(arg::db_arg()) + .arg(arg::pwd_arg()) + .arg(arg::salt_arg()) + ) .subcommand( Command::new("export") .about("export the rucksack db") @@ -184,14 +195,27 @@ fn cli() -> Command { .arg(arg::pwd_arg()) .arg(arg::salt_arg()) ) + .subcommand( + Command::new("update") + .about("update an existing secret") + .arg(arg::account_type()) + .arg(arg::account_user()) + .arg(arg::account_pass()) + .arg(arg::account_url()) + .arg(arg::db_arg()) + .arg(arg::pwd_arg()) + .arg(arg::salt_arg()) + ) } fn run(matches: &ArgMatches, app: &rucksack::App) -> Result<()> { match matches.subcommand() { + Some(("add", matches)) => add::new(matches, app)?, Some(("export", matches)) => export::new(matches, app)?, Some(("gen", matches)) => gen::new(matches)?, Some(("import", matches)) => import::new(matches, app)?, Some(("list", matches)) => list::all(matches, app)?, + Some(("update", matches)) => update::new(matches, app)?, Some((&_, _)) => todo!(), None => todo!(), } diff --git a/src/store/record.rs b/src/store/record.rs index a7778ad..71ee0e1 100644 --- a/src/store/record.rs +++ b/src/store/record.rs @@ -10,6 +10,8 @@ use super::crypto::{decrypt, encrypt}; #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Encode, Decode)] pub enum Kind { #[default] + Account, + Credential, Password, } From acd7d1c3bf2d9bf3f8e3fd1c46f7bafb74859f9f Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Wed, 11 Jan 2023 21:26:13 -0600 Subject: [PATCH 23/38] Added 'update' command. Fixes #28. --- src/cli/command/arg.rs | 2 -- src/cli/command/update.rs | 40 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 8 ++++---- src/store/record.rs | 10 +++++++++- 4 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 src/cli/command/update.rs diff --git a/src/cli/command/arg.rs b/src/cli/command/arg.rs index e34ccc7..5c307a9 100644 --- a/src/cli/command/arg.rs +++ b/src/cli/command/arg.rs @@ -47,14 +47,12 @@ pub fn account_user() -> Arg { .help("the user, login, or account, identifier") .short('u') .long("user") - .required(true) } pub fn account_pass() -> Arg { Arg::new("password") .help("the account / login password") .long("password") - .required(true) } pub fn account_url() -> Arg { diff --git a/src/cli/command/update.rs b/src/cli/command/update.rs new file mode 100644 index 0000000..4057ffe --- /dev/null +++ b/src/cli/command/update.rs @@ -0,0 +1,40 @@ +use anyhow::{anyhow, Result}; +use clap::ArgMatches; + +use crate::app::App; +use crate::store::record; +use crate::time; + +pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { + log::debug!("Running 'update' subcommand ..."); + let user = matches.get_one::("user").unwrap().to_string(); + let url = matches.get_one::("url").unwrap().to_string(); + let key = record::key(&user, &url); + let dr = app.db.get(key.clone()); + if dr.is_none() { + return Err(anyhow!("no secret record for given key '{}'", key)); + } + let now = time::now(); + let mut record = dr.unwrap(); + let kind = match matches.get_one::("type").map(|s| s.as_str()) { + Some("account") => record::Kind::Account, + Some("creds") => record::Kind::Credential, + Some("credential") => record::Kind::Credential, + Some("password") => record::Kind::Password, + Some(&_) => record.metadata().kind, + None => record.metadata().kind, + }; + record.metadata.kind = kind; + record.metadata.updated = now.clone(); + let password = match matches.get_one::("password") { + Some(pwd) => { + record.metadata.password_changed = now; + pwd.to_owned() + } + None => record.creds.password, + }; + record.creds.password = password; + app.db.insert(record); + app.db.close()?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index e7a0fb3..2e3da67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,8 +34,8 @@ fn cli() -> Command { Command::new("add") .about("add a new secret") .arg(arg::account_type()) - .arg(arg::account_user()) - .arg(arg::account_pass()) + .arg(arg::account_user().required(true)) + .arg(arg::account_pass().required(true)) .arg(arg::account_url()) .arg(arg::db_arg()) .arg(arg::pwd_arg()) @@ -199,9 +199,9 @@ fn cli() -> Command { Command::new("update") .about("update an existing secret") .arg(arg::account_type()) - .arg(arg::account_user()) + .arg(arg::account_user().required(true)) .arg(arg::account_pass()) - .arg(arg::account_url()) + .arg(arg::account_url().required(true)) .arg(arg::db_arg()) .arg(arg::pwd_arg()) .arg(arg::salt_arg()) diff --git a/src/store/record.rs b/src/store/record.rs index 71ee0e1..c8c8bb7 100644 --- a/src/store/record.rs +++ b/src/store/record.rs @@ -68,7 +68,7 @@ impl std::fmt::Debug for Creds { impl DecryptedRecord { pub fn key(&self) -> String { - format!("{}:{}", self.creds.user, self.metadata.url) + key(&self.creds.user, &self.metadata.url) } pub fn metadata(&self) -> Metadata { @@ -116,6 +116,14 @@ impl EncryptedRecord { } } +// Utility functions + +pub fn key(user: &str, url: &str) -> String { + format!("{}:{}", user, url) +} + +// Tests + #[cfg(test)] mod tests { use crate::store::testing_data; From 77233bd3f86b9d3f452ff1332a84c1bdfa189242 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Wed, 11 Jan 2023 21:30:14 -0600 Subject: [PATCH 24/38] Bumped version for release. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 17fa900..7c80bd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rucksack" description = "A terminal-based password manager, generator, and importer/exporter (Firefox, Chrome) backed with a concurrent hashmap" -version = "0.6.0-dev" +version = "0.6.0" license = "Apache-2.0" authors = ["Duncan McGreggor "] repository = "https://github.com/oxur/rucksack" From 8fd262c151f6c67285097378cc8a4d1aa20c75b7 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Wed, 11 Jan 2023 21:54:17 -0600 Subject: [PATCH 25/38] Added missing required. --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 2e3da67..64a2f8f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,7 @@ fn cli() -> Command { .arg(arg::account_type()) .arg(arg::account_user().required(true)) .arg(arg::account_pass().required(true)) - .arg(arg::account_url()) + .arg(arg::account_url().required(true)) .arg(arg::db_arg()) .arg(arg::pwd_arg()) .arg(arg::salt_arg()) From a6d7771341fce91ae1d0a33c336d67d9986c1309 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Wed, 11 Jan 2023 21:54:25 -0600 Subject: [PATCH 26/38] Updated usage. --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 1d148bc..08e0e9b 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,28 @@ Logins may be exported to files that can then be used to import into browsers: For both importing and exporting, there are currently two supported types: `firefox` and `chrome`. +### Adding and Updating via Command + +To add a single record via the CLI: + +```shell +./bin/rucksack add \ + --url http://example.com \ + --user shelly \ + --password whyyyyyy +``` + +Note that `--user`, `--password`, and `--url` are all required when adding a new record. + +```shell +./bin/rucksack update \ + --url http://example.com \ + --user shelly \ + --password whyyyyyyyyyyyyyyyyyyyzzz +``` + +When updating a record, only the `--user` and `--url` flags are required (these comprise the key). Time stamps are managed automatically, so the amount of data that may be manually set is limited. Finer-grained control should use CSV file imports. + ### List Secrets Show URL/accounts for all secrets: From eabc26cb0e9f9cff72f8f63c9206ae63b5acd11c Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Wed, 11 Jan 2023 21:56:05 -0600 Subject: [PATCH 27/38] Updated completed feature listing. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 08e0e9b..c0c0a1e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ * [x] List secrets (encrypted and decrypted) * [x] Searching secrets (filtering) * [x] Reports (quality, duplicates, etc.) -* [ ] Add new records to the DB via CLI subcommand +* [x] Add new records to the DB (and support updating them) via CLI subcommand * [ ] Local network sync ## Usage From 38ddbe33f7fa646a1ba4b1ff4cba9e3222e26aea Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Wed, 11 Jan 2023 22:57:48 -0600 Subject: [PATCH 28/38] Default password changes. --- src/cli/command/add.rs | 25 ++++++++++++++++++++----- src/cli/command/update.rs | 25 ++++++++++++++++--------- src/cli/command/util.rs | 4 ++-- src/main.rs | 3 +-- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/cli/command/add.rs b/src/cli/command/add.rs index fded45b..f8a645e 100644 --- a/src/cli/command/add.rs +++ b/src/cli/command/add.rs @@ -1,5 +1,8 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use clap::ArgMatches; +use secrecy::{ExposeSecret, SecretString}; + +use super::prompt; use crate::app::App; use crate::store::record; @@ -8,6 +11,18 @@ use crate::time; pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { log::debug!("Running 'add' subcommand ..."); + let user = matches.get_one::("user").unwrap().to_string(); + let url = matches.get_one::("url").unwrap().to_string(); + let key = record::key(&user, &url); + if let Some(_dr) = app.db.get(key) { + return Err(anyhow!( + "Record already exists -- please use the 'update' command" + )); + } + let pwd = match matches.get_one::("password") { + Some(flag_pwd) => SecretString::new(flag_pwd.to_owned()), + None => prompt::secret("Enter password for new record: ").unwrap(), + }; let account_type = matches.get_one::("type").map(|s| s.as_str()); let default_kind = record::Kind::Password; let kind = match account_type { @@ -20,12 +35,12 @@ pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { None => default_kind, }; - let user = matches.get_one::("user").unwrap().to_string(); - let password = matches.get_one::("password").unwrap().to_string(); - let url = matches.get_one::("url").unwrap().to_string(); let now = time::now(); - let creds = Creds { user, password }; + let creds = Creds { + user, + password: pwd.expose_secret().to_string(), + }; let metadata = Metadata { kind, url, diff --git a/src/cli/command/update.rs b/src/cli/command/update.rs index 4057ffe..17453b9 100644 --- a/src/cli/command/update.rs +++ b/src/cli/command/update.rs @@ -1,10 +1,15 @@ use anyhow::{anyhow, Result}; use clap::ArgMatches; +// TODO: Move this into 'set password' +// use secrecy::{ExposeSecret, SecretString}; use crate::app::App; use crate::store::record; use crate::time; +// TODO: Move this into 'set password' +// use super::prompt; + pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { log::debug!("Running 'update' subcommand ..."); let user = matches.get_one::("user").unwrap().to_string(); @@ -14,6 +19,11 @@ pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { if dr.is_none() { return Err(anyhow!("no secret record for given key '{}'", key)); } + // TODO: Move this into 'set password' + // let pwd = match matches.get_one::("password") { + // Some(flag_pwd) => SecretString::new(flag_pwd.to_owned()), + // None => prompt::secret("Enter password for record: ").unwrap(), + // }; let now = time::now(); let mut record = dr.unwrap(); let kind = match matches.get_one::("type").map(|s| s.as_str()) { @@ -25,15 +35,12 @@ pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { None => record.metadata().kind, }; record.metadata.kind = kind; - record.metadata.updated = now.clone(); - let password = match matches.get_one::("password") { - Some(pwd) => { - record.metadata.password_changed = now; - pwd.to_owned() - } - None => record.creds.password, - }; - record.creds.password = password; + record.metadata.updated = now; + // TODO: Move this into 'set password' + // if pwd.expose_secret().to_string() != record.password() { + // record.creds.password = pwd.expose_secret().to_string(); + // record.metadata.password_changed = now; + // } app.db.insert(record); app.db.close()?; Ok(()) diff --git a/src/cli/command/util.rs b/src/cli/command/util.rs index aacc260..5d5f203 100644 --- a/src/cli/command/util.rs +++ b/src/cli/command/util.rs @@ -2,10 +2,10 @@ use anyhow::Result; use clap::ArgMatches; use secrecy::{ExposeSecret, SecretString}; -use super::prompt; - use crate::store::db; +use super::prompt; + pub fn setup_db(matches: &ArgMatches) -> Result { match matches.get_one::("db") { Some(db_file) => { diff --git a/src/main.rs b/src/main.rs index 64a2f8f..f102971 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,7 @@ fn cli() -> Command { .about("add a new secret") .arg(arg::account_type()) .arg(arg::account_user().required(true)) - .arg(arg::account_pass().required(true)) + .arg(arg::account_pass()) .arg(arg::account_url().required(true)) .arg(arg::db_arg()) .arg(arg::pwd_arg()) @@ -200,7 +200,6 @@ fn cli() -> Command { .about("update an existing secret") .arg(arg::account_type()) .arg(arg::account_user().required(true)) - .arg(arg::account_pass()) .arg(arg::account_url().required(true)) .arg(arg::db_arg()) .arg(arg::pwd_arg()) From 7579a6ba0221ef8af495289323ee3bdc1d352a88 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Wed, 11 Jan 2023 23:07:53 -0600 Subject: [PATCH 29/38] Updated docs in preparation for #46. --- README.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c0c0a1e..bc06e7f 100644 --- a/README.md +++ b/README.md @@ -103,16 +103,77 @@ To add a single record via the CLI: --password whyyyyyy ``` -Note that `--user`, `--password`, and `--url` are all required when adding a new record. +Note that `--user` and `--url` are required when adding a new record. A password is required, too: if one is not provided with `--password`, then you will be prompted: ```shell -./bin/rucksack update \ +./bin/rucksack add \ --url http://example.com \ - --user shelly \ - --password whyyyyyyyyyyyyyyyyyyyzzz + --user shelly +``` + +```shell +Enter db password: ``` -When updating a record, only the `--user` and `--url` flags are required (these comprise the key). Time stamps are managed automatically, so the amount of data that may be manually set is limited. Finer-grained control should use CSV file imports. +```shell +Enter password for record: +``` + +There are several types of changes to records that can't be made via an "update" subcommand due to how the data is used in the database. That did't leave too much data left for an "update" command, so the "record type" update was moved into the "set" group, too. The total list of `set` operations is: + +* changing the password +* changing the user (account name) +* changing the URL +* changing the type of record + +As such, these have their own sub commands (under `set`), as well as their flags and logic. + +Changing a password: + +```shell +./bin/rucksack set password \ + --url http://example.com \ + --user shelly + --old-password whyyyyyyyyyyyyyyyyyyyzzz + --new-password whyyyyyyyyyyyyyyyyyyy +``` + +If one or both of the passwords isn't provided, you will be prompted at the terminal: + +```shell +Enter OLD password for record: +``` + +```shell +Enter NEW password for record: +``` + +Changing a user: + +```shell +./bin/rucksack set user \ + --url http://example.com \ + --old-user shelly + --new-user clammy +``` + +Changing a URL: + +```shell +./bin/rucksack set url \ + --old-url http://example.com \ + --new-url http://shelly.com \ + --user clammy +``` + +Changing the record type: + +```shell +./bin/rucksack set type \ + --url http://example.com \ + --user clammy + --type account +``` ### List Secrets From ac4fd21e2858251f676f105e147b472a15fc1a90 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Wed, 11 Jan 2023 23:10:36 -0600 Subject: [PATCH 30/38] Updated feature description. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bc06e7f..7583443 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ * [x] List secrets (encrypted and decrypted) * [x] Searching secrets (filtering) * [x] Reports (quality, duplicates, etc.) -* [x] Add new records to the DB (and support updating them) via CLI subcommand +* [x] Add new records to the DB (and support updates) via CLI subcommands * [ ] Local network sync ## Usage From 9ac20211adcf73d9b123ece3fc83b54bd842c87a Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Wed, 11 Jan 2023 23:24:44 -0600 Subject: [PATCH 31/38] Updated feature list with version and links. --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7583443..111a058 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,16 @@ ## Features -* [x] Password generator -* [x] Encrypted local storage -* [x] Concurrent hashmap for use by daemons -* [x] Supports Firefox and Chrome CSV formats (for importing and exporting) -* [x] List secrets (encrypted and decrypted) -* [x] Searching secrets (filtering) -* [x] Reports (quality, duplicates, etc.) -* [x] Add new records to the DB (and support updates) via CLI subcommands -* [ ] Local network sync +* [x] Password generator (0.1.0) +* [x] Encrypted local storage (0.2.0) +* [x] Concurrent hashmap for use by daemons (0.2.0) +* [x] List secrets, both encrypted and decrypted (0.3.0) +* [x] Supports Firefox and Chrome CSV formats (for importing, 0.3.0 and exporting, 0.5.0) +* [x] Searching secrets via filtering (0.4.0) +* [x] Reports on password quality, duplicates, etc. (0.5.0) +* [x] Add new records to the DB (and support updates) via CLI subcommands (0.6.0) +* [ ] [Database restores](https://github.com/oxur/rucksack/issues/44) +* [ ] [Local network sync](https://github.com/oxur/rucksack/issues/41) ## Usage From 1ec12f2f150e1c7aa0c5fac6cd7535162de16eea Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Wed, 11 Jan 2023 23:34:38 -0600 Subject: [PATCH 32/38] More link updates. --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 111a058..55fd4eb 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,9 @@ * [x] Searching secrets via filtering (0.4.0) * [x] Reports on password quality, duplicates, etc. (0.5.0) * [x] Add new records to the DB (and support updates) via CLI subcommands (0.6.0) -* [ ] [Database restores](https://github.com/oxur/rucksack/issues/44) -* [ ] [Local network sync](https://github.com/oxur/rucksack/issues/41) +* [ ] [Database restores](https://github.com/oxur/rucksack/milestone/9) +* [ ] [Local network sync](https://github.com/oxur/rucksack/milestone/10) +* [ ] [Firefox Account Syncing](https://github.com/oxur/rucksack/milestone/11) ## Usage From 78698c8172921884d1b903f2e75a2aa95421971c Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Thu, 12 Jan 2023 19:56:53 -0600 Subject: [PATCH 33/38] First steps in 'set' with subcommsnds (and associated refactor). --- src/app.rs | 1 + src/cli/command/add.rs | 34 ++++---------------- src/cli/command/arg.rs | 26 +++++++++++++++ src/cli/command/mod.rs | 3 +- src/cli/command/prompt.rs | 8 ----- src/cli/command/set.rs | 44 +++++++++++++++++++++++++ src/cli/command/update.rs | 47 --------------------------- src/cli/command/util.rs | 68 ++++++++++++++++++++++++++++++++++++--- src/main.rs | 56 +++++++++++++++++++++++++------- src/store/db.rs | 11 ++++++- src/store/mod.rs | 2 +- src/store/record.rs | 2 ++ 12 files changed, 198 insertions(+), 104 deletions(-) delete mode 100644 src/cli/command/prompt.rs create mode 100644 src/cli/command/set.rs delete mode 100644 src/cli/command/update.rs diff --git a/src/app.rs b/src/app.rs index a2ce77e..8fc891d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,6 @@ use crate::{config, store}; +#[derive(Debug)] pub struct App { pub cfg: config::Config, pub db: store::db::DB, diff --git a/src/cli/command/add.rs b/src/cli/command/add.rs index f8a645e..baff6f4 100644 --- a/src/cli/command/add.rs +++ b/src/cli/command/add.rs @@ -1,49 +1,27 @@ use anyhow::{anyhow, Result}; use clap::ArgMatches; -use secrecy::{ExposeSecret, SecretString}; -use super::prompt; +use super::util; use crate::app::App; -use crate::store::record; use crate::store::{Creds, DecryptedRecord, Metadata}; use crate::time; pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { log::debug!("Running 'add' subcommand ..."); - let user = matches.get_one::("user").unwrap().to_string(); - let url = matches.get_one::("url").unwrap().to_string(); - let key = record::key(&user, &url); - if let Some(_dr) = app.db.get(key) { + if let Some(_dr) = util::record(&app.db, matches) { return Err(anyhow!( "Record already exists -- please use the 'update' command" )); } - let pwd = match matches.get_one::("password") { - Some(flag_pwd) => SecretString::new(flag_pwd.to_owned()), - None => prompt::secret("Enter password for new record: ").unwrap(), - }; - let account_type = matches.get_one::("type").map(|s| s.as_str()); - let default_kind = record::Kind::Password; - let kind = match account_type { - Some("account") => record::Kind::Account, - Some("creds") => record::Kind::Credential, - Some("credential") => record::Kind::Credential, - Some("password") => record::Kind::Password, - Some("") => default_kind, - Some(&_) => todo!(), - None => default_kind, - }; - let now = time::now(); - let creds = Creds { - user, - password: pwd.expose_secret().to_string(), + user: util::user(matches), + password: util::account_pwd_revealed(matches), }; let metadata = Metadata { - kind, - url, + kind: util::account_kind(matches), + url: util::url(matches), created: now.clone(), imported: now.clone(), updated: now.clone(), diff --git a/src/cli/command/arg.rs b/src/cli/command/arg.rs index 5c307a9..1447a42 100644 --- a/src/cli/command/arg.rs +++ b/src/cli/command/arg.rs @@ -49,6 +49,20 @@ pub fn account_user() -> Arg { .long("user") } +pub fn account_user_old() -> Arg { + Arg::new("old-user") + .help("the old user login name") + .short('u') + .long("old-user") +} + +pub fn account_user_new() -> Arg { + Arg::new("new-user") + .help("the new user login name to use") + .short('u') + .long("new-user") +} + pub fn account_pass() -> Arg { Arg::new("password") .help("the account / login password") @@ -58,3 +72,15 @@ pub fn account_pass() -> Arg { pub fn account_url() -> Arg { Arg::new("url").help("the login URL").long("url") } + +pub fn account_url_old() -> Arg { + Arg::new("old-url") + .help("the old login URL") + .long("old-url") +} + +pub fn account_url_new() -> Arg { + Arg::new("new-url") + .help("the new URL for the account / login") + .long("new-url") +} diff --git a/src/cli/command/mod.rs b/src/cli/command/mod.rs index 3504057..313ec8b 100644 --- a/src/cli/command/mod.rs +++ b/src/cli/command/mod.rs @@ -4,8 +4,7 @@ pub mod export; pub mod gen; pub mod import; pub mod list; -pub mod prompt; -pub mod update; +pub mod set; pub mod util; pub use util::setup_db; diff --git a/src/cli/command/prompt.rs b/src/cli/command/prompt.rs deleted file mode 100644 index d24df14..0000000 --- a/src/cli/command/prompt.rs +++ /dev/null @@ -1,8 +0,0 @@ -use anyhow::{anyhow, Result}; -use secrecy::SecretString; - -pub fn secret(prompt: &str) -> Result { - rpassword::prompt_password(prompt) - .map(SecretString::new) - .map_err(|e| anyhow!("password prompt failed: {}", e.to_string())) -} diff --git a/src/cli/command/set.rs b/src/cli/command/set.rs new file mode 100644 index 0000000..5739d90 --- /dev/null +++ b/src/cli/command/set.rs @@ -0,0 +1,44 @@ +use anyhow::{anyhow, Result}; +use clap::ArgMatches; + +use crate::app::App; +use crate::time; + +use super::util; + +pub fn account_type(matches: &ArgMatches, app: &App) -> Result<()> { + log::debug!("Setting account type ..."); + log::warn!("Not implemented!\nmatches: {:?}", matches); + app.db.close()?; + Ok(()) +} + +pub fn password(matches: &ArgMatches, app: &App) -> Result<()> { + log::debug!("Setting account password ..."); + let dr = util::record(&app.db, matches); + if dr.is_none() { + return Err(anyhow!( + "no secret record for given key '{}'", + util::key(matches) + )); + } + let mut record = dr.unwrap(); + record.creds.password = util::account_pwd_revealed(matches); + record.metadata.password_changed = time::now(); + app.db.close()?; + Ok(()) +} + +pub fn url(matches: &ArgMatches, app: &App) -> Result<()> { + log::debug!("Setting account URL ..."); + log::warn!("Not implemented!\nmatches: {:?}", matches); + app.db.close()?; + Ok(()) +} + +pub fn user(matches: &ArgMatches, app: &App) -> Result<()> { + log::debug!("Setting account user ..."); + log::warn!("Not implemented!\nmatches: {:?}", matches); + app.db.close()?; + Ok(()) +} diff --git a/src/cli/command/update.rs b/src/cli/command/update.rs deleted file mode 100644 index 17453b9..0000000 --- a/src/cli/command/update.rs +++ /dev/null @@ -1,47 +0,0 @@ -use anyhow::{anyhow, Result}; -use clap::ArgMatches; -// TODO: Move this into 'set password' -// use secrecy::{ExposeSecret, SecretString}; - -use crate::app::App; -use crate::store::record; -use crate::time; - -// TODO: Move this into 'set password' -// use super::prompt; - -pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { - log::debug!("Running 'update' subcommand ..."); - let user = matches.get_one::("user").unwrap().to_string(); - let url = matches.get_one::("url").unwrap().to_string(); - let key = record::key(&user, &url); - let dr = app.db.get(key.clone()); - if dr.is_none() { - return Err(anyhow!("no secret record for given key '{}'", key)); - } - // TODO: Move this into 'set password' - // let pwd = match matches.get_one::("password") { - // Some(flag_pwd) => SecretString::new(flag_pwd.to_owned()), - // None => prompt::secret("Enter password for record: ").unwrap(), - // }; - let now = time::now(); - let mut record = dr.unwrap(); - let kind = match matches.get_one::("type").map(|s| s.as_str()) { - Some("account") => record::Kind::Account, - Some("creds") => record::Kind::Credential, - Some("credential") => record::Kind::Credential, - Some("password") => record::Kind::Password, - Some(&_) => record.metadata().kind, - None => record.metadata().kind, - }; - record.metadata.kind = kind; - record.metadata.updated = now; - // TODO: Move this into 'set password' - // if pwd.expose_secret().to_string() != record.password() { - // record.creds.password = pwd.expose_secret().to_string(); - // record.metadata.password_changed = now; - // } - app.db.insert(record); - app.db.close()?; - Ok(()) -} diff --git a/src/cli/command/util.rs b/src/cli/command/util.rs index 5d5f203..fce6929 100644 --- a/src/cli/command/util.rs +++ b/src/cli/command/util.rs @@ -1,17 +1,18 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use clap::ArgMatches; -use secrecy::{ExposeSecret, SecretString}; +use secrecy::{ExposeSecret, Secret, SecretString}; +use crate::store; use crate::store::db; - -use super::prompt; +use crate::store::record; +use crate::store::record::DecryptedRecord; pub fn setup_db(matches: &ArgMatches) -> Result { match matches.get_one::("db") { Some(db_file) => { let pwd = match matches.get_one::("db-pass") { Some(flag_pwd) => SecretString::new(flag_pwd.to_owned()), - None => prompt::secret("Enter db password: ").unwrap(), + None => secret("Enter db password: ").unwrap(), }; let salt = matches.get_one::("salt").unwrap().to_string(); db::open(db_file.to_owned(), pwd.expose_secret().to_string(), salt) @@ -19,3 +20,60 @@ pub fn setup_db(matches: &ArgMatches) -> Result { None => Ok(db::new()), } } + +pub fn record(app_db: &db::DB, matches: &ArgMatches) -> Option { + app_db.get(key(matches)) +} + +pub fn user(matches: &ArgMatches) -> String { + matches.get_one::("user").unwrap().to_string() +} + +pub fn url(matches: &ArgMatches) -> String { + matches.get_one::("url").unwrap().to_string() +} + +pub fn key(matches: &ArgMatches) -> String { + store::key(&user(matches), &url(matches)) +} + +pub fn db_pwd(matches: &ArgMatches) -> Secret { + match matches.get_one::("db-pass") { + Some(flag_pwd) => SecretString::new(flag_pwd.to_owned()), + None => secret("Enter DB password: ").unwrap(), + } +} + +pub fn account_pwd(matches: &ArgMatches) -> Secret { + match matches.get_one::("password") { + Some(flag_pwd) => SecretString::new(flag_pwd.to_owned()), + None => secret("Enter account password: ").unwrap(), + } +} + +pub fn account_pwd_revealed(matches: &ArgMatches) -> String { + reveal(account_pwd(matches)) +} + +pub fn secret(prompt: &str) -> Result { + rpassword::prompt_password(prompt) + .map(SecretString::new) + .map_err(|e| anyhow!("password prompt failed: {}", e.to_string())) +} + +pub fn reveal(pwd: SecretString) -> String { + pwd.expose_secret().to_string() +} + +pub fn account_kind(matches: &ArgMatches) -> record::Kind { + let account_type = matches.get_one::("type").map(|s| s.as_str()); + match account_type { + Some("account") => record::Kind::Account, + Some("creds") => record::Kind::Credential, + Some("credential") => record::Kind::Credential, + Some("password") => record::Kind::Password, + Some("") => record::DEFAULT_KIND, + Some(&_) => todo!(), + None => record::DEFAULT_KIND, + } +} diff --git a/src/main.rs b/src/main.rs index f102971..ca53fe2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; use clap::builder::EnumValueParser; use clap::{Arg, ArgAction, ArgMatches, Command}; -use rucksack::cli::command::{add, arg, export, gen, import, list, setup_db, update}; +use rucksack::cli::command::{add, arg, export, gen, import, list, set, setup_db}; use rucksack::{config, util}; const NAME: &str = env!("CARGO_PKG_NAME"); @@ -196,25 +196,57 @@ fn cli() -> Command { .arg(arg::salt_arg()) ) .subcommand( - Command::new("update") - .about("update an existing secret") - .arg(arg::account_type()) - .arg(arg::account_user().required(true)) - .arg(arg::account_url().required(true)) + Command::new("set") + .about("perform various 'write' operations") .arg(arg::db_arg()) .arg(arg::pwd_arg()) .arg(arg::salt_arg()) + .subcommand( + Command::new("password") + .about("change the password for the given account") + .arg(arg::account_pass()) + .arg(arg::account_user().required(true)) + .arg(arg::account_url().required(true)) + ) + .subcommand( + Command::new("url") + .about("change the url for the given account") + .arg(arg::account_url_old().required(true)) + .arg(arg::account_url_new().required(true)) + .arg(arg::account_user().required(true)) + ) + .subcommand( + Command::new("user") + .about("change the user (login name) for the given account") + .arg(arg::account_user_old().required(true)) + .arg(arg::account_user_new().required(true)) + .arg(arg::account_url().required(true)) + ) + .subcommand( + Command::new("type") + .about("change the type of the given account") + .arg(arg::account_type().required(true)) + .arg(arg::account_user().required(true)) + .arg(arg::account_url().required(true)) + ) ) } fn run(matches: &ArgMatches, app: &rucksack::App) -> Result<()> { match matches.subcommand() { - Some(("add", matches)) => add::new(matches, app)?, - Some(("export", matches)) => export::new(matches, app)?, - Some(("gen", matches)) => gen::new(matches)?, - Some(("import", matches)) => import::new(matches, app)?, - Some(("list", matches)) => list::all(matches, app)?, - Some(("update", matches)) => update::new(matches, app)?, + Some(("add", add_matches)) => add::new(add_matches, app)?, + Some(("export", export_matches)) => export::new(export_matches, app)?, + Some(("gen", gen_matches)) => gen::new(gen_matches)?, + Some(("import", import_matches)) => import::new(import_matches, app)?, + Some(("list", list_matches)) => list::all(list_matches, app)?, + Some(("set", set_matches)) => match set_matches.subcommand() { + Some(("password", password_matches)) => set::password(password_matches, app)?, + Some(("url", url_matches)) => set::url(url_matches, app)?, + Some(("user", user_matches)) => set::user(user_matches, app)?, + Some(("type", type_matches)) => set::account_type(type_matches, app)?, + Some((&_, _)) => todo!(), + None => todo!(), + }, Some((&_, _)) => todo!(), None => todo!(), } diff --git a/src/store/db.rs b/src/store/db.rs index 9cfcc98..054ac2c 100644 --- a/src/store/db.rs +++ b/src/store/db.rs @@ -1,4 +1,4 @@ -use std::fs; +use std::{fmt, fs}; use anyhow::{Error, Result}; use bincode::config; @@ -20,6 +20,15 @@ pub struct DB { enabled: bool, } +impl fmt::Debug for DB { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DB") + .field("path", &self.path) + .field("hash_map", &self.hash_map) + .finish() + } +} + pub fn init(path: String, store_pwd: String, updated: String) -> Result<()> { let db = open(path, store_pwd, updated).unwrap(); db.close() diff --git a/src/store/mod.rs b/src/store/mod.rs index e30740b..115ea07 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -3,4 +3,4 @@ pub mod db; pub mod record; pub mod testing_data; -pub use record::{Creds, DecryptedRecord, EncryptedRecord, Metadata}; +pub use record::{key, Creds, DecryptedRecord, EncryptedRecord, Metadata}; diff --git a/src/store/record.rs b/src/store/record.rs index c8c8bb7..c0aff7c 100644 --- a/src/store/record.rs +++ b/src/store/record.rs @@ -15,6 +15,8 @@ pub enum Kind { Password, } +pub const DEFAULT_KIND: Kind = Kind::Password; + #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Encode, Decode)] pub struct Metadata { pub kind: Kind, From c9d817225552f98a6a75fe62d3b521168a6ed60a Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Thu, 12 Jan 2023 20:59:34 -0600 Subject: [PATCH 34/38] Cleanup. --- src/cli/command/set.rs | 8 ++++---- src/main.rs | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/cli/command/set.rs b/src/cli/command/set.rs index 5739d90..2461830 100644 --- a/src/cli/command/set.rs +++ b/src/cli/command/set.rs @@ -17,14 +17,14 @@ pub fn password(matches: &ArgMatches, app: &App) -> Result<()> { log::debug!("Setting account password ..."); let dr = util::record(&app.db, matches); if dr.is_none() { - return Err(anyhow!( - "no secret record for given key '{}'", - util::key(matches) - )); + let msg = format!("no secret record for given key '{}'", util::key(matches)); + log::error!("{}", msg); + return Err(anyhow!(msg)); } let mut record = dr.unwrap(); record.creds.password = util::account_pwd_revealed(matches); record.metadata.password_changed = time::now(); + app.db.insert(record); app.db.close()?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index ca53fe2..a54eeec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -228,6 +228,9 @@ fn cli() -> Command { .arg(arg::account_type().required(true)) .arg(arg::account_user().required(true)) .arg(arg::account_url().required(true)) + .arg(arg::db_arg()) + .arg(arg::pwd_arg()) + .arg(arg::salt_arg()) ) ) } @@ -286,6 +289,6 @@ fn main() -> Result<()> { let (_, subcmd_matches) = matches.subcommand().unwrap(); let db = setup_db(subcmd_matches)?; - let app = rucksack::app::App { cfg, db }; + let app = rucksack::App { cfg, db }; run(&matches, &app) } From 7e15073604ff6398f73b4c33a72ef1dee0d7107e Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Thu, 12 Jan 2023 23:47:20 -0600 Subject: [PATCH 35/38] Implemented 'set type'. --- src/cli/command/add.rs | 2 +- src/cli/command/set.rs | 19 +++++++++---------- src/cli/command/util.rs | 11 +++++++++-- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/cli/command/add.rs b/src/cli/command/add.rs index baff6f4..f269d8c 100644 --- a/src/cli/command/add.rs +++ b/src/cli/command/add.rs @@ -9,7 +9,7 @@ use crate::time; pub fn new(matches: &ArgMatches, app: &App) -> Result<()> { log::debug!("Running 'add' subcommand ..."); - if let Some(_dr) = util::record(&app.db, matches) { + if let Ok(_dr) = util::record(&app.db, matches) { return Err(anyhow!( "Record already exists -- please use the 'update' command" )); diff --git a/src/cli/command/set.rs b/src/cli/command/set.rs index 2461830..1928c05 100644 --- a/src/cli/command/set.rs +++ b/src/cli/command/set.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::Result; use clap::ArgMatches; use crate::app::App; @@ -8,22 +8,21 @@ use super::util; pub fn account_type(matches: &ArgMatches, app: &App) -> Result<()> { log::debug!("Setting account type ..."); - log::warn!("Not implemented!\nmatches: {:?}", matches); + let mut record = util::record(&app.db, matches)?; + record.metadata.kind = util::account_kind(matches); + record.metadata.updated = time::now(); + app.db.insert(record); app.db.close()?; Ok(()) } pub fn password(matches: &ArgMatches, app: &App) -> Result<()> { log::debug!("Setting account password ..."); - let dr = util::record(&app.db, matches); - if dr.is_none() { - let msg = format!("no secret record for given key '{}'", util::key(matches)); - log::error!("{}", msg); - return Err(anyhow!(msg)); - } - let mut record = dr.unwrap(); + let now = time::now(); + let mut record = util::record(&app.db, matches)?; record.creds.password = util::account_pwd_revealed(matches); - record.metadata.password_changed = time::now(); + record.metadata.password_changed = now.clone(); + record.metadata.updated = now; app.db.insert(record); app.db.close()?; Ok(()) diff --git a/src/cli/command/util.rs b/src/cli/command/util.rs index fce6929..6dc5047 100644 --- a/src/cli/command/util.rs +++ b/src/cli/command/util.rs @@ -21,8 +21,15 @@ pub fn setup_db(matches: &ArgMatches) -> Result { } } -pub fn record(app_db: &db::DB, matches: &ArgMatches) -> Option { - app_db.get(key(matches)) +pub fn record(app_db: &db::DB, matches: &ArgMatches) -> Result { + match app_db.get(key(matches)) { + Some(dr) => Ok(dr), + None => { + let msg = format!("no secret record for given key '{}'", key(matches)); + log::error!("{}", msg); + Err(anyhow!(msg)) + } + } } pub fn user(matches: &ArgMatches) -> String { From 3da9ec6e18ec166232f924533d3e4e1aa6cf9b72 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Fri, 13 Jan 2023 00:37:37 -0600 Subject: [PATCH 36/38] Implemented 'set url'. --- src/cli/command/set.rs | 15 ++++++++++++++- src/cli/command/util.rs | 16 ++++++++++++++-- src/store/db.rs | 8 ++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/cli/command/set.rs b/src/cli/command/set.rs index 1928c05..ee7493d 100644 --- a/src/cli/command/set.rs +++ b/src/cli/command/set.rs @@ -2,6 +2,7 @@ use anyhow::Result; use clap::ArgMatches; use crate::app::App; +use crate::store; use crate::time; use super::util; @@ -30,7 +31,19 @@ pub fn password(matches: &ArgMatches, app: &App) -> Result<()> { pub fn url(matches: &ArgMatches, app: &App) -> Result<()> { log::debug!("Setting account URL ..."); - log::warn!("Not implemented!\nmatches: {:?}", matches); + let old_url = util::url_old(matches); + let new_url = util::url_new(matches); + let user = util::user(matches); + let key = store::key(&user, &old_url); + let mut record = util::record_by_key(&app.db, key.clone())?; + record.metadata.url = new_url; + record.metadata.updated = time::now(); + match app.db.delete(key) { + Some(false) => log::error!("there was a problem deleting the record"), + Some(_) => (), + None => log::error!("there was a problem deleting the record"), + } + app.db.insert(record); app.db.close()?; Ok(()) } diff --git a/src/cli/command/util.rs b/src/cli/command/util.rs index 6dc5047..1706ed8 100644 --- a/src/cli/command/util.rs +++ b/src/cli/command/util.rs @@ -22,10 +22,14 @@ pub fn setup_db(matches: &ArgMatches) -> Result { } pub fn record(app_db: &db::DB, matches: &ArgMatches) -> Result { - match app_db.get(key(matches)) { + record_by_key(app_db, key(matches)) +} + +pub fn record_by_key(app_db: &db::DB, key: String) -> Result { + match app_db.get(key.clone()) { Some(dr) => Ok(dr), None => { - let msg = format!("no secret record for given key '{}'", key(matches)); + let msg = format!("no secret record for given key '{}'", key); log::error!("{}", msg); Err(anyhow!(msg)) } @@ -40,6 +44,14 @@ pub fn url(matches: &ArgMatches) -> String { matches.get_one::("url").unwrap().to_string() } +pub fn url_old(matches: &ArgMatches) -> String { + matches.get_one::("old-url").unwrap().to_string() +} + +pub fn url_new(matches: &ArgMatches) -> String { + matches.get_one::("new-url").unwrap().to_string() +} + pub fn key(matches: &ArgMatches) -> String { store::key(&user(matches), &url(matches)) } diff --git a/src/store/db.rs b/src/store/db.rs index 054ac2c..9f1d6f5 100644 --- a/src/store/db.rs +++ b/src/store/db.rs @@ -106,6 +106,14 @@ impl DB { util::write_file(encrypted, self.path()) } + pub fn delete(&self, key: String) -> Option { + log::trace!("Deleting record with key {} ...", key); + match self.hash_map.remove(&key) { + Some(_) => Some(true), + None => Some(false), + } + } + pub fn insert(&self, record: DecryptedRecord) -> Option { let key = record.key(); log::trace!("Inserting record with key {} ...", key); From bf7409dbc2c35f56a87257f8baef16af6438fcf4 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Fri, 13 Jan 2023 00:44:48 -0600 Subject: [PATCH 37/38] Implemented 'set user'. --- src/cli/command/set.rs | 14 +++++++++++++- src/cli/command/util.rs | 8 ++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/cli/command/set.rs b/src/cli/command/set.rs index ee7493d..2bb7484 100644 --- a/src/cli/command/set.rs +++ b/src/cli/command/set.rs @@ -50,7 +50,19 @@ pub fn url(matches: &ArgMatches, app: &App) -> Result<()> { pub fn user(matches: &ArgMatches, app: &App) -> Result<()> { log::debug!("Setting account user ..."); - log::warn!("Not implemented!\nmatches: {:?}", matches); + let old_user = util::user_old(matches); + let new_user = util::user_new(matches); + let url = util::url(matches); + let key = store::key(&old_user, &url); + let mut record = util::record_by_key(&app.db, key.clone())?; + record.creds.user = new_user; + record.metadata.updated = time::now(); + match app.db.delete(key) { + Some(false) => log::error!("there was a problem deleting the record"), + Some(_) => (), + None => log::error!("there was a problem deleting the record"), + } + app.db.insert(record); app.db.close()?; Ok(()) } diff --git a/src/cli/command/util.rs b/src/cli/command/util.rs index 1706ed8..fcfff76 100644 --- a/src/cli/command/util.rs +++ b/src/cli/command/util.rs @@ -40,6 +40,14 @@ pub fn user(matches: &ArgMatches) -> String { matches.get_one::("user").unwrap().to_string() } +pub fn user_old(matches: &ArgMatches) -> String { + matches.get_one::("old-user").unwrap().to_string() +} + +pub fn user_new(matches: &ArgMatches) -> String { + matches.get_one::("new-user").unwrap().to_string() +} + pub fn url(matches: &ArgMatches) -> String { matches.get_one::("url").unwrap().to_string() } From 127ef3f03d79a1db252d9fa8a6488e7a0a922ed6 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Fri, 13 Jan 2023 00:49:15 -0600 Subject: [PATCH 38/38] Updated docs. --- README.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 55fd4eb..a84bd43 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ * [x] Add new records to the DB (and support updates) via CLI subcommands (0.6.0) * [ ] [Database restores](https://github.com/oxur/rucksack/milestone/9) * [ ] [Local network sync](https://github.com/oxur/rucksack/milestone/10) -* [ ] [Firefox Account Syncing](https://github.com/oxur/rucksack/milestone/11) +* [ ] [Firefox Account Client Syncing](https://github.com/oxur/rucksack/milestone/11) ## Usage @@ -136,18 +136,13 @@ Changing a password: ./bin/rucksack set password \ --url http://example.com \ --user shelly - --old-password whyyyyyyyyyyyyyyyyyyyzzz - --new-password whyyyyyyyyyyyyyyyyyyy + --password whyyyyyyyyyyyyyyyyyyy ``` -If one or both of the passwords isn't provided, you will be prompted at the terminal: +If the password isn't provided, you will be prompted at the terminal: ```shell -Enter OLD password for record: -``` - -```shell -Enter NEW password for record: +Enter account password: ``` Changing a user: @@ -177,6 +172,8 @@ Changing the record type: --type account ``` +Note that for all of this, should you want to pass the DB pass, file, or salt, you will need to make sure those flags come after `set` but before the following subcommmand. + ### List Secrets Show URL/accounts for all secrets: