smart-password-manager-cli 0.1.0

Command line interface for smart password management built on smartpasslib. Generate deterministic passwords from secret phrases without storing them.
pub mod add;
pub mod delete;
pub mod edit;
pub mod get;
pub mod list;
pub mod verify;

use anyhow::{Result, bail};
use smartpasslib::{SmartPassword, SmartPasswordManager};

pub fn find_entry(
    manager: &SmartPasswordManager,
    key: Option<String>,
    description: Option<String>,
) -> Result<SmartPassword> {
    if let Some(k) = key {
        if let Some(entry) = manager.get(&k) {
            return Ok(entry.clone());
        }
        let matches = resolve_short_key(manager, &k);
        match matches.len() {
            0 => bail!("No entry found for key '{k}'"),
            1 => return Ok(matches[0].clone()),
            _ => bail!("Ambiguous key '{k}' — matches {} entries", matches.len()),
        }
    }

    if let Some(d) = description {
        let mut matches: Vec<SmartPassword> = manager
            .passwords()
            .values()
            .filter(|e| e.description == d)
            .cloned()
            .collect();

        match matches.len() {
            0 => bail!("No entry found with description '{d}'"),
            1 => return Ok(matches.remove(0)),
            _ => bail!(
                "Ambiguous description '{d}' — matches {} entries. Use --key.",
                matches.len() + 1
            ),
        }
    }

    bail!("Either --key or --description is required");
}

fn resolve_short_key(manager: &SmartPasswordManager, short: &str) -> Vec<SmartPassword> {
    let lower = short.to_lowercase();
    manager
        .passwords()
        .values()
        .filter(|e| {
            let k = e.public_key.to_lowercase();
            k == lower || k.starts_with(&lower) || k.ends_with(&lower) || short_matches(&k, &lower)
        })
        .cloned()
        .collect()
}

fn short_matches(full: &str, short: &str) -> bool {
    if let Some((head, tail)) = short.split_once('…') {
        full.starts_with(head) && full.ends_with(tail)
    } else {
        false
    }
}

pub fn short_key(key: &str) -> String {
    if key.len() <= 8 {
        return key.to_string();
    }
    format!("{}…{}", &key[..4], &key[key.len() - 4..])
}

pub fn confirm(prompt: &str) -> bool {
    use std::io::{self, Write};
    print!("{prompt} [y/N]: ");
    let _ = io::stdout().flush();
    let mut buf = String::new();
    if io::stdin().read_line(&mut buf).is_err() {
        return false;
    }
    matches!(buf.trim().to_lowercase().as_str(), "y" | "yes")
}