diff --git a/Cargo.toml b/Cargo.toml index 31eed69..f084e0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,13 +8,14 @@ name = "libduralumin" path = "src/lib.rs" [features] -default = ["passphrase-gen", "ed25519", "clap", "rpassword", "base64"] +default = ["passphrase-gen", "password-gen", "ed25519", "clap", "rpassword", "base64"] ed25519 = ["osshkeys", "sha2"] passphrase-gen = [] +password-gen = [] [[bin]] name = "duralumin" -required-features = ["passphrase-gen", "clap"] +required-features = ["passphrase-gen", "password-gen", "clap", "clap/derive"] [[bin]] name = "duralumin-keygen" diff --git a/src/bin/duralumin.rs b/src/bin/duralumin.rs index 802b275..4e81b8b 100644 --- a/src/bin/duralumin.rs +++ b/src/bin/duralumin.rs @@ -1,7 +1,10 @@ use std::path::PathBuf; -use clap::Parser; -use libduralumin::passphrase_gen::{PassPhraseGenerator, Words}; +use clap::{Parser, Subcommand}; +use libduralumin::{ + passphrase_gen::{PassPhraseGenerator, Words}, + password_gen::PasswordGenerator, +}; /// program that generates random passphrases. #[derive(Parser)] @@ -11,6 +14,18 @@ use libduralumin::passphrase_gen::{PassPhraseGenerator, Words}; author = "No One " )] struct Opts { + #[clap(subcommand)] + command: Subcommands, +} + +#[derive(Subcommand)] +enum Subcommands { + Passphrase(PassphraseGenArgs), + Password(PasswordGenArgs), +} + +#[derive(Parser)] +struct PassphraseGenArgs { #[clap(short = 'l', long)] num_words: Option, #[clap(long)] @@ -27,7 +42,35 @@ struct Opts { backend: String, } -impl Opts { +#[derive(Parser)] +struct PasswordGenArgs { + /// number of passwords to generate + #[clap(short = 'c', long = "count", default_value = "1")] + count: u16, + /// if given, generate a password with an exact length, otherwise choses a length between min and max length. + #[clap(long = "length")] + exact_length: Option, + /// minimum length of the generated password + #[clap(long = "min", default_value = "10")] + min_length: u16, + /// maximum length of the generated password + #[clap(long = "max", default_value = "20")] + max_length: u16, + /// whether to use lowercase letters in the password + #[clap(short = 'l', long)] + use_letters: bool, + /// whether to use uppercase letters in the password + #[clap(short = 'L', long)] + use_capital_letters: bool, + /// whether to use numbers in the password + #[clap(short = 'N', long)] + use_numbers: bool, + /// whether to use special symbols `(!"ยง$%$%&\/\\()=?_:;,.-*+#'\\)` in the password + #[clap(short = 'S', long)] + use_symbols: bool, +} + +impl PassphraseGenArgs { /// gets the number of words per passphrase pub fn words_per_passphrase(&self) -> i16 { self.num_words.unwrap_or(6) @@ -76,28 +119,53 @@ impl Backend { } fn main() { - let opts = Opts::parse(); - let gen = PassPhraseGenerator::new() - .with_capitalized(opts.capitalize()) - .with_min_word_length(opts.min_word_length) - .with_length(opts.words_per_passphrase()); + let opts: Opts = Opts::parse(); - let words = match opts.backend() { - Some(Backend::UseAPassPhrase) => Words::from_str(include_str!("../../useapassphrase.txt")), - Some(Backend::EnglishWords) => Words::from_str(include_str!("../../words_alpha.txt")), - Some(path) => { - Words::from_text_file(std::fs::File::open(path).expect("failed to read word list.")) - .expect("word list from path.") - } - None => { - panic!("invalid backend.") - } - }; + match opts.command { + Subcommands::Passphrase(opts) => { + let gen = PassPhraseGenerator::new() + .with_capitalized(opts.capitalize()) + .with_min_word_length(opts.min_word_length) + .with_length(opts.words_per_passphrase()); - for passphrase in gen - .generate_n(&words, opts.passphrase_count() as usize) - .expect("failed to generate passphrases. this is bad.") - { - println!("{}", passphrase); + let words = match opts.backend() { + Some(Backend::UseAPassPhrase) => { + Words::from_str(include_str!("../../useapassphrase.txt")) + } + Some(Backend::EnglishWords) => { + Words::from_str(include_str!("../../words_alpha.txt")) + } + Some(Backend::File(path)) => Words::from_text_file( + std::fs::File::open(path).expect("failed to read word list."), + ) + .expect("word list from path."), + None => { + panic!("invalid backend.") + } + }; + + for passphrase in gen + .generate_n(&words, opts.passphrase_count() as usize) + .expect("failed to generate passphrases. this is bad.") + { + println!("{}", passphrase); + } + } + Subcommands::Password(args) => { + let min_length = args.exact_length.unwrap_or(args.min_length); + let max_length = args.exact_length.unwrap_or(args.max_length); + let gen = PasswordGenerator::new( + min_length, + max_length, + args.use_letters, + args.use_capital_letters, + args.use_numbers, + args.use_symbols, + ); + + for password in gen.generate_n(args.count as usize) { + println!("{}", password); + } + } } } diff --git a/src/lib.rs b/src/lib.rs index 2961f60..c3da522 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,8 @@ #[cfg(feature = "passphrase-gen")] pub mod passphrase_gen; +#[cfg(feature = "password-gen")] +pub mod password_gen; + #[cfg(feature = "ed25519")] pub mod ed25519; diff --git a/src/passphrase_gen.rs b/src/passphrase_gen.rs index ba77de6..0b3fa54 100644 --- a/src/passphrase_gen.rs +++ b/src/passphrase_gen.rs @@ -80,11 +80,7 @@ impl PassPhraseGenerator { self } - pub fn generate_n( - &self, - words: &Words, - count: usize, - ) -> Option> { + pub fn generate_n(&self, words: &Words, count: usize) -> Option> { (0..count).map(|_| self.generate(words)).collect() } @@ -104,10 +100,7 @@ impl PassPhraseGenerator { match chars.next() { None => String::new(), - Some(c) => { - c.to_uppercase().collect::() - + chars.as_str() - } + Some(c) => c.to_uppercase().collect::() + chars.as_str(), } } else { word.to_string() diff --git a/src/password_gen.rs b/src/password_gen.rs new file mode 100644 index 0000000..6ad76e9 --- /dev/null +++ b/src/password_gen.rs @@ -0,0 +1,71 @@ +use rand::{prelude::SliceRandom, rngs::OsRng, Rng}; + +const LETTERS: &[u8] = b"abcdefghijklmnopqrstuvwxyz"; +const CAPITAL_LETTERS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const NUMBERS: &[u8] = b"0123456789"; +const SYMBOLS: &[u8] = br#" !#$%"&'()*+,-./:;<=>?@[\]^_`{|}~"#; + +pub struct PasswordGenerator { + min_length: u16, + max_length: u16, + letters: bool, + capital_letters: bool, + numbers: bool, + symbols: bool, +} + +impl PasswordGenerator { + pub fn new_exact_length( + length: u16, + letters: bool, + capital_letters: bool, + numbers: bool, + symbols: bool, + ) -> Self { + Self::new(length, length, letters, capital_letters, numbers, symbols) + } + + pub fn new( + min_length: u16, + max_length: u16, + letters: bool, + capital_letters: bool, + numbers: bool, + symbols: bool, + ) -> Self { + Self { + min_length, + max_length, + letters, + capital_letters, + numbers, + symbols, + } + } + + pub fn generate(&self) -> String { + let length = OsRng.gen_range(self.min_length..=self.max_length) as usize; + + let set = [ + self.letters.then(|| LETTERS.iter()), + self.capital_letters.then(|| CAPITAL_LETTERS.iter()), + self.numbers.then(|| NUMBERS.iter()), + self.symbols.then(|| SYMBOLS.iter()), + ] + .into_iter() + .flatten() + .flatten() + .collect::>(); + + let mut bytes = std::iter::repeat_with(|| **set.choose(&mut OsRng).unwrap()) + .take(length) + .collect::>(); + bytes.shuffle(&mut OsRng); + + String::from_utf8(bytes).expect("password bytes to utf8 string.") + } + + pub fn generate_n(&self, n: usize) -> Vec { + std::iter::repeat_with(|| self.generate()).take(n).collect() + } +}