use anyhow::Result;
use clap::ArgMatches;
use passwords::{analyzer, scorer};
use sha2::{Digest, Sha256};
use rucksack_db::records;
use rucksack_db::Status;
use rucksack_lib::time;
use crate::app::App;
use crate::input::{options, query, Flag};
use crate::output::{result, table, Column, Opts};
use super::{backup, dedupe};
pub fn all(matches: &ArgMatches, app: &App) -> Result<()> {
process_records(
matches,
app,
Opts {
skip_deleted: true,
..Default::default()
},
)
}
pub fn backups(matches: &ArgMatches, app: &App) -> Result<()> {
backup::list(matches, app)
}
pub fn deleted(matches: &ArgMatches, app: &App) -> Result<()> {
process_records(
matches,
app,
Opts {
only_deleted: true,
..Default::default()
},
)
}
pub fn duplicates(matches: &ArgMatches, app: &App) -> Result<()> {
let mut hash_fields: Vec<Column> = Vec::new();
let mut default_hash_fields = exact_hash_fields();
match matches.get_one::<String>("dedupe-type").map(|s| s.as_str()) {
Some("exact") => {
hash_fields.append(&mut exact_hash_fields());
}
Some("updated") => {
hash_fields.append(&mut updated_hash_fields());
}
Some("any") => {
hash_fields.append(&mut default_hash_fields);
}
Some(t) => {
log::error!(type_str = t, operation = "hash_fields"; "Got unexpected type; using default");
hash_fields.append(&mut default_hash_fields);
}
None => {
hash_fields.append(&mut default_hash_fields);
}
}
process_records(
matches,
app,
Opts {
group_by_hash: true,
hash_fields,
..Default::default()
},
)
}
pub fn keys(matches: &ArgMatches, app: &App) -> Result<()> {
process_records(
matches,
app,
Opts {
only_keys: true,
..Default::default()
},
)
}
pub fn passwords(matches: &ArgMatches, app: &App) -> Result<()> {
let mut opts = Opts {
password_history: true,
..Default::default()
};
opts.reveal = options::reveal(matches);
opts.decrypted = options::decrypt(matches);
let mut results: Vec<result::ResultRow> = Vec::new();
let record = query::record(app)?;
let analyzed = analyzer::analyze(record.password());
let score = scorer::score(&analyzed).trunc() as i64;
let md = record.metadata();
let mut pwd = record.password();
if !opts.reveal {
pwd = hidden();
}
results.push(result::password(
pwd,
score.to_string(),
md.created,
md.updated,
md.last_used,
));
log::debug!(length = record.history().len(), operation = "list_history"; "History length");
let mut history = record.history();
history.reverse();
for old in history {
pwd = old.secrets.password;
if !opts.reveal {
pwd = hidden();
}
results.push(result::password(
pwd,
score.to_string(),
old.metadata.created,
old.metadata.updated,
old.metadata.last_used,
));
}
log::debug!(length = results.len(), operation = "list"; "Results length");
let mut t = table::new(results.to_owned(), opts);
t.display();
println!();
Ok(())
}
fn process_records(matches: &ArgMatches, app: &App, mut opts: Opts) -> Result<()> {
let sort_by = matches.get_one::<String>("sort-by").map(|s| s.as_str());
opts.category = app.inputs.category(Flag::Many);
opts.all_tags = options::all_tags(matches);
opts.any_tags = options::any_tags(matches);
opts.kind = options::record_kind(matches);
opts.reveal = options::reveal(matches);
opts.decrypted = options::decrypt(matches);
if opts.reveal && opts.decrypted {
opts.with_passwd = true;
}
match matches.get_one::<String>("group-by").map(|s| s.as_str()) {
Some("name") => opts.group_by_name = true,
Some("password") => opts.group_by_password = true,
Some(_) => (),
None => (),
}
match matches.get_one::<bool>("with-status") {
Some(true) => {
opts.only_deleted = false;
opts.skip_deleted = false;
opts.with_status = true;
}
Some(_) => (),
None => (),
}
let mut results: Vec<result::ResultRow> = Vec::new();
let mut groups = result::GroupByString::new();
if results.is_empty() {
let rg = extract_results(matches, app, opts.clone())?;
results = rg.results;
groups = rg.groups;
}
sort(&mut results, sort_by);
let mut group_count: i32 = 0;
let mut count: usize = results.len();
let total: usize = app.db.hash_map().len();
if opts.group_by_name {
(group_count, count) = print_user_group(groups, sort_by, &opts);
} else if opts.group_by_password {
(group_count, count) = print_password_group(groups, sort_by, &opts);
} else if opts.group_by_hash {
groups = dedupe::post_process(groups);
(group_count, count) = print_hash_group(groups, sort_by, &opts);
} else {
let mut t = table::new(results.to_owned(), opts.clone());
t.display();
}
print_report(group_count, count, total, &opts);
for r in results {
if opts.reveal {
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)?;
}
}
}
app.db.close()?;
Ok(())
}
fn extract_results(
matches: &ArgMatches,
app: &App,
opts: Opts,
) -> Result<result::ResultsAndGroups> {
let mut results: Vec<result::ResultRow> = Vec::new();
let mut groups = result::GroupByString::new();
let filter = matches.get_one::<String>("filter");
let exclude = matches.get_one::<String>("exclude");
let max_score = matches.get_one::<f64>("max-score");
let min_score = matches.get_one::<f64>("min-score");
for i in app.db.iter() {
let record = records::decrypt_versioned(
i.value(),
app.db.store_pwd(),
app.inputs.salt(),
app.db.version(),
)?;
let analyzed = analyzer::analyze(record.password());
let score = scorer::score(&analyzed);
let mut result = result::new(record.key(), record.name_or_user(), record.metadata().url);
if opts.skip_deleted && record.metadata().state == Status::Deleted {
continue;
}
if opts.only_deleted && record.metadata().state != Status::Deleted {
continue;
}
if opts.kind != records::Kind::Any && opts.kind != record.metadata().kind {
continue;
}
if opts.category != *records::ANY_CATEGORY && record.metadata().category != opts.category {
continue;
};
if let Some(ref ts) = opts.all_tags {
if !rucksack_lib::util::all(ts.clone(), record.metadata().tag_values()) {
continue;
}
}
if let Some(ref ts) = opts.any_tags {
if !rucksack_lib::util::any(ts.clone(), record.metadata().tag_values()) {
continue;
}
}
if let Some(check) = filter {
if !i.key().contains(check) {
continue;
}
}
if let Some(check) = exclude {
if i.key().contains(check) {
continue;
}
}
if let Some(check) = max_score {
if &score.trunc() > check {
continue;
}
}
if let Some(check) = min_score {
if &score.trunc() < check {
continue;
}
}
let md = record.metadata();
result.add(Column::Key, record.key());
result.add(Column::Kind, md.kind.name());
result.add(Column::Category, md.category.clone());
result.add(Column::Count, md.access_count.to_string());
result.add(Column::Status, md.status().to_string());
result.add(Column::HistoryCount, format!("{:}", record.history().len()));
match opts.decrypted {
true => {
let pwd = if opts.reveal {
record.password()
} else {
hidden()
};
result.add(Column::Password, pwd);
result.add(Column::Score, (score.trunc() as i64).to_string());
}
false => result.add(Column::Password, hidden()),
}
if opts.group_by_name {
let entry = groups.entry(record.name_or_user()).or_default();
entry.push(result.clone());
} else if opts.group_by_password {
let entry = groups.entry(record.password()).or_default();
entry.push(result.clone());
} else if opts.group_by_hash {
result.add(Column::LastUpdated, record.metadata().last_used);
let entry = groups
.entry(hash_by_columns(record.clone(), opts.hash_fields.clone()))
.or_default();
entry.push(result.clone());
}
results.push(result);
}
Ok(result::ResultsAndGroups { results, groups })
}
fn print_password_group(
groups: result::GroupByString,
sort_by: Option<&str>,
opts: &Opts,
) -> (i32, usize) {
let mut group_count = 0;
let mut record_count = 0;
for (_, mut group) in groups {
if group.len() == 1 {
continue;
}
group_count += 1;
sort(&mut group, sort_by);
password_section(&group[0], opts);
println!("Records using: {}\nRecords:", group.len());
let mut t = table::new(group.to_owned(), opts.clone());
t.display();
record_count += group.len();
}
(group_count, record_count)
}
fn print_user_group(
groups: result::GroupByString,
sort_by: Option<&str>,
opts: &Opts,
) -> (i32, usize) {
let mut group_count = 0;
let mut record_count = 0;
for (_, mut group) in groups {
if group.len() == 1 {
continue;
}
group_count += 1;
sort(&mut group, sort_by);
user_section(&group[0], opts);
println!("Records using: {}\nRecords:", group.len());
let mut t = table::new(group.to_owned(), opts.clone());
t.display();
record_count += group.len();
}
(group_count, record_count)
}
fn print_hash_group(
groups: result::GroupByString,
sort_by: Option<&str>,
opts: &Opts,
) -> (i32, usize) {
let mut group_count = 0;
let mut record_count = 0;
for (_, mut group) in groups {
if group.len() == 1 {
continue;
}
group_count += 1;
sort(&mut group, sort_by);
hash_section(&group[0], opts);
println!("Duplicate record count: {}\nRecords:", group.len());
let mut t = table::new(group.to_owned(), opts.clone());
t.display();
record_count += group.len();
}
(group_count, record_count)
}
fn print_report(group_count: i32, records: usize, total: usize, opts: &Opts) {
if opts.group_by_name || opts.group_by_password || opts.group_by_hash {
println!("\n{group_count} groups (with {records} records out of {total} total)\n",)
} else {
println!("\n{records} records (of {total} total)\n")
}
}
fn password_section(r: &result::ResultRow, opts: &Opts) {
println!("\n\n+{}\n", "=".repeat(40 + 30 + 2));
match opts.decrypted {
true => {
if opts.reveal {
println!(
"Password: {} (Score: {})",
r.get(&Column::Password).unwrap(),
r.get(&Column::Score).unwrap()
)
} else {
println!(
"Password: {} (Score: {})",
hidden(),
r.get(&Column::Score).unwrap()
)
}
}
false => println!("Password Group"),
}
}
fn user_section(r: &result::ResultRow, opts: &Opts) {
match opts.decrypted {
true => {
println!("\n\n+{}\n", "=".repeat(40 + 20 + 16 + 5));
println!("User: {}", r.get(&Column::Name).unwrap())
}
false => {
println!("\n\n+{}\n", "=".repeat(40 - 1));
println!("User: {}", r.get(&Column::Name).unwrap())
}
}
}
fn hash_section(r: &result::ResultRow, opts: &Opts) {
match opts.decrypted {
true => {
println!("\n\n+{}\n", "=".repeat(40 + 20 + 16 + 5));
println!("ID: {}", r.get(&Column::Id).unwrap())
}
false => {
println!("\n\n+{}\n", "=".repeat(40 - 1));
println!("ID: {}", r.get(&Column::Id).unwrap())
}
}
}
fn hidden() -> String {
"*".repeat(10)
}
fn sort(results: &mut [result::ResultRow], sort_by: Option<&str>) {
if results.is_empty() {
return;
}
match sort_by {
Some("score") => results.sort_by(|a, b| {
b.get(&Column::Score)
.unwrap()
.parse::<i32>()
.unwrap()
.cmp(&a.get(&Column::Score).unwrap().parse::<i32>().unwrap())
}),
Some("url") => results.sort(),
Some("name") => results.sort_by(|a, b| {
a.get(&Column::Name)
.unwrap()
.cmp(b.get(&Column::Name).unwrap())
}),
Some(&_) => (),
None => (),
};
}
fn exact_hash_fields() -> Vec<Column> {
vec![
Column::Name,
Column::Url,
Column::Kind,
Column::Category,
Column::Password,
]
}
fn updated_hash_fields() -> Vec<Column> {
vec![Column::Name, Column::Url, Column::Kind, Column::Category]
}
fn get_by_column(rec: records::DecryptedRecord, col: &Column) -> String {
match col {
Column::Category => rec.metadata().category,
Column::Kind => rec.metadata().kind.to_string(),
Column::Name => rec.name_or_user(),
Column::Password => rec.password(),
Column::Url => rec.url(),
_ => unimplemented!(),
}
}
fn get_by_columns(rec: records::DecryptedRecord, cols: Vec<Column>) -> Vec<String> {
let mut vals: Vec<String> = vec![];
for col in cols.iter() {
vals.push(get_by_column(rec.clone(), col));
}
vals
}
fn hash_by_columns(rec: records::DecryptedRecord, cols: Vec<Column>) -> String {
format!(
"{:x}",
Sha256::new()
.chain_update(get_by_columns(rec, cols).join(":"))
.finalize()
)
}