passgen-cli 0.2.0

Password generator with a regular-expression-like syntax
Documentation
use clap::{Args, Parser, ValueEnum};
use passgen::random::Random;
use std::{path::PathBuf, str::FromStr};

#[derive(Clone)]
pub struct WordlistSpec {
    pub name: String,
    pub path: PathBuf,
}

#[derive(thiserror::Error, Debug)]
pub enum WordlistSpecParseError {
    #[error("missing colon")]
    MissingColon,
}

impl FromStr for WordlistSpec {
    type Err = WordlistSpecParseError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        let (name, path) = input
            .split_once(':')
            .ok_or(WordlistSpecParseError::MissingColon)?;
        Ok(Self {
            name: name.into(),
            path: path.into(),
        })
    }
}

#[derive(ValueEnum, Clone)]
pub enum Format {
    Text,
    Json,
    Csv,
}

/// Generate random sequences from a regex-like pattern.
#[derive(Parser)]
#[command(author, version)]
pub struct Options {
    /// Use the given named preset instead of specifying a pattern.
    #[clap(short, long)]
    pub preset: Option<String>,

    /// Amount of sequences to generate.
    #[clap(short, long, default_value = "1")]
    pub amount: usize,

    /// Source of randomness to use.
    ///
    /// Possible sources are:
    ///
    /// `system`: use the secure system random number generator (default)
    ///
    /// `null`: use a dummy randomness source (insecure)
    ///
    /// `xoshiro:<seed>`: use the xoshiro randomness source with the given seed (insecure)
    #[clap(short, long)]
    pub random: Option<Random>,

    #[clap(flatten)]
    pub master: MasterOptions,

    /// Output format to use when emitting generated sequences.
    ///
    /// This defaults to a text-based format, but you can also specify 'json' or 'csv'.
    #[clap(short, long, default_value = "text")]
    pub format: Format,

    /// Amount of threads to use, enables multi-threaded generation.
    #[clap(short = 'j', long)]
    pub threads: Option<usize>,

    /// Enable debug log output.
    #[clap(short, long)]
    pub verbose: Option<usize>,

    /// Load wordlist for generating wordlist or markov-chain based words.
    ///
    /// The format of this is `<name>:<path>`. For example, to load a wordlist with the name
    /// `english` that is located at `/usr/share/dict/words`, specify `-w
    /// english:/usr/share/dict/words`.
    #[clap(short, long)]
    pub wordlist: Vec<WordlistSpec>,

    #[clap(required_unless_present("preset"))]
    pub pattern: Option<String>,

    /// Load configuration file.
    pub config: Vec<PathBuf>,
}

/// Options for master-pass mode.
#[derive(Args)]
#[group()]
pub struct MasterOptions {
    /// Set the master passphrase to use for generating passwords.
    ///
    /// In master passphrase mode, Passgen uses the master passphrase you supply here to generate
    /// deterministic outputs. With this functionality, you can use Passgen as a password manager,
    /// only having to remember a single master passphrase and using it to generate unique
    /// passphrases per domain and login.
    ///
    /// This also enables the master-passphrase randomness source. You cannot combine this with
    /// any other randomness source.
    #[clap(short, long, conflicts_with("random"))]
    pub master: Option<String>,

    /// Set the domain for the master passphrase mode.
    ///
    /// Requires that you specify a master passphrase.
    #[clap(short, long, requires("master"))]
    pub domain: Option<String>,

    /// Set the token for the master passphrase.
    ///
    /// This allows you to generate multiple, unique passphrases for a single domain, for example
    /// if you have multiple accounts.
    #[clap(short, long, requires("master"))]
    pub token: Option<String>,
}

impl Options {
    pub fn random(&self) -> Random {
        self.master
            .random()
            .or(self.random.clone())
            .unwrap_or_default()
    }
}

impl MasterOptions {
    pub fn random(&self) -> Option<Random> {
        self.master
            .as_deref()
            .map(|master| Random::from_master_pass(master, self.domain.as_deref().unwrap_or("")))
    }
}