Compare commits
3 commits
76ebacacf2
...
9e7d8c3494
Author | SHA1 | Date | |
---|---|---|---|
|
9e7d8c3494 | ||
|
edc25af9de | ||
|
8c049c4790 |
22
Cargo.toml
22
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "duralumin"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
@ -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"
|
||||
|
@ -23,10 +24,15 @@ required-features = ["ed25519", "clap", "rpassword", "base64"]
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
rand = "0.8.4"
|
||||
rand = "0.8"
|
||||
rand_chacha = "0.3"
|
||||
clap = {version = "3.0.0-beta.5", optional = true, features = ["derive"]}
|
||||
base64 = {version = "0.13.0", optional = true}
|
||||
bytes = {version = "1.1.0", optional = true}
|
||||
base64 = {version = "0.13", optional = true}
|
||||
bytes = {version = "1.1", optional = true}
|
||||
osshkeys = {git = "https://github.com/noonebtw/rust-osshkeys.git", branch = "master", optional = true}
|
||||
sha2 = {version = "0.9.8", optional = true}
|
||||
rpassword = {version = "5.0.1", optional = true}
|
||||
sha2 = {version = "0.9", optional = true}
|
||||
rpassword = {version = "5.0", optional = true}
|
||||
zeroize = {version = "1.5"}
|
||||
rust-argon2 = "1.0"
|
||||
thiserror = "1.0"
|
||||
anyhow = "1.0"
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
use std::io::Write;
|
||||
use std::{io::Write, str::FromStr};
|
||||
|
||||
#[macro_use]
|
||||
use clap::Parser;
|
||||
use libduralumin::ed25519::{generate_ed25519_keypair, randomart};
|
||||
use osshkeys::{error::OsshResult, PublicParts};
|
||||
use libduralumin::ed25519::randomart;
|
||||
use osshkeys::PublicParts;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::keygen::HashType;
|
||||
|
||||
/// program that generates ed25519 keypairs seeded by a passphrase and an optional ID.
|
||||
#[derive(Parser)]
|
||||
#[clap(
|
||||
name = "duralumin",
|
||||
version = "0.1.0",
|
||||
name = "duralumin-keygen",
|
||||
version = "0.2.0",
|
||||
author = "No One <noonebtw@nirgendwo.xyz>"
|
||||
)]
|
||||
struct Opts {
|
||||
|
@ -34,78 +36,269 @@ fn fix_newline(mut line: String) -> String {
|
|||
line
|
||||
}
|
||||
|
||||
fn main() -> OsshResult<()> {
|
||||
let opts = Opts::parse();
|
||||
println!("Generating ed25519 keypair:");
|
||||
pub mod keygen {
|
||||
use std::str::FromStr;
|
||||
|
||||
print!("Enter a passphrase: ");
|
||||
std::io::stdout().flush()?;
|
||||
let passphrase = rpassword::read_password()?;
|
||||
print!("Re-enter the same passphrase: ");
|
||||
std::io::stdout().flush()?;
|
||||
let passphrase2 = rpassword::read_password()?;
|
||||
use osshkeys::{error::OsshResult, keys::ed25519::Ed25519KeyPair, KeyPair};
|
||||
use rand::{Rng, SeedableRng};
|
||||
use sha2::Digest;
|
||||
use thiserror::Error;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
if passphrase != passphrase2 {
|
||||
println!("passphrases do not match.");
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"passphrase did not match.",
|
||||
))?;
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Failed to parse")]
|
||||
ParseError,
|
||||
}
|
||||
|
||||
print!("Enter an optional ID []: ");
|
||||
std::io::stdout().flush()?;
|
||||
let id = {
|
||||
let mut id = String::new();
|
||||
std::io::stdin().read_line(&mut id)?;
|
||||
fix_newline_ref(&mut id);
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum KeyType {
|
||||
SSH,
|
||||
PGP,
|
||||
}
|
||||
|
||||
if id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(id)
|
||||
impl FromStr for KeyType {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"ssh" => Ok(Self::SSH),
|
||||
"pgp" => Ok(Self::PGP),
|
||||
_ => Err(Error::ParseError),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let keypair = generate_ed25519_keypair(&passphrase, id.as_ref())?;
|
||||
|
||||
print!("Encrypt keypair with passphrase? [Y/n]: ");
|
||||
std::io::stdout().flush()?;
|
||||
|
||||
let encrypt = {
|
||||
let mut line = String::new();
|
||||
std::io::stdin().read_line(&mut line)?;
|
||||
fix_newline_ref(&mut line);
|
||||
|
||||
line.to_lowercase() != "n"
|
||||
};
|
||||
|
||||
if encrypt {
|
||||
println!("Using passphrase to encrypt key..");
|
||||
}
|
||||
|
||||
let fingerprint = keypair.fingerprint(osshkeys::keys::FingerprintHash::SHA256)?;
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HashType {
|
||||
Sha256,
|
||||
Argon2,
|
||||
}
|
||||
|
||||
impl FromStr for HashType {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"sha256" => Ok(Self::Sha256),
|
||||
"argon2" | "argon" => Ok(Self::Argon2),
|
||||
_ => Err(Error::ParseError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SshKeyBuilder {
|
||||
hash_type: HashType,
|
||||
iterations: i16,
|
||||
tag: Zeroizing<String>,
|
||||
passphrase: Zeroizing<String>,
|
||||
encrypt_key: bool,
|
||||
}
|
||||
|
||||
impl Default for SshKeyBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hash_type: HashType::Argon2,
|
||||
iterations: 4,
|
||||
tag: Default::default(),
|
||||
passphrase: Default::default(),
|
||||
encrypt_key: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SshKeyBuilder {
|
||||
pub fn with_hash_type(mut self, hash_type: HashType) -> Self {
|
||||
self.hash_type = hash_type;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_encrypt(mut self, encrypt: bool) -> Self {
|
||||
self.encrypt_key = encrypt;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_hash_type_and_iterations(
|
||||
self,
|
||||
hash_type: HashType,
|
||||
iterations: i16,
|
||||
) -> Self {
|
||||
self.with_hash_type(hash_type).with_iterations(iterations)
|
||||
}
|
||||
|
||||
pub fn with_iterations(mut self, iterations: i16) -> Self {
|
||||
self.iterations = iterations;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_maybe_tag(self, tag: Option<String>) -> Self {
|
||||
match tag {
|
||||
Some(tag) => self.with_tag(tag),
|
||||
None => self,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_tag(mut self, tag: String) -> Self {
|
||||
self.tag = Zeroizing::new(tag);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_passphrase<Z: Into<Zeroizing<String>>>(
|
||||
mut self,
|
||||
passphrase: Z,
|
||||
) -> Self {
|
||||
self.passphrase = passphrase.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build_keypair(
|
||||
self,
|
||||
) -> OsshResult<((Zeroizing<String>, String), KeyPair)> {
|
||||
let hash_seed = Zeroizing::new(
|
||||
self.passphrase
|
||||
.chars()
|
||||
.chain(self.tag.chars())
|
||||
.collect::<String>(),
|
||||
);
|
||||
|
||||
let seed = match self.hash_type {
|
||||
HashType::Sha256 => {
|
||||
let mut digest = sha2::Sha256::digest(hash_seed.as_bytes());
|
||||
|
||||
assert!(self.iterations <= 1);
|
||||
for _ in 0..(self.iterations - 1) {
|
||||
digest = sha2::Sha256::digest(&digest);
|
||||
}
|
||||
digest.to_vec()
|
||||
}
|
||||
HashType::Argon2 => {
|
||||
// TODO: make more random salt
|
||||
let salt = b"thissaltneedsupdating";
|
||||
|
||||
let config = argon2::Config {
|
||||
variant: argon2::Variant::Argon2i,
|
||||
version: argon2::Version::Version13,
|
||||
mem_cost: 65536,
|
||||
time_cost: self.iterations as u32,
|
||||
lanes: 4,
|
||||
thread_mode: argon2::ThreadMode::Parallel,
|
||||
hash_length: 32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
argon2::hash_raw(hash_seed.as_bytes(), salt, &config)
|
||||
.expect("hash passphrase")
|
||||
}
|
||||
}
|
||||
.try_into()
|
||||
.expect("unwrap seed into [u8; 32]");
|
||||
|
||||
let rng = rand_chacha::ChaChaRng::from_seed(seed).gen::<[u8; 32]>();
|
||||
|
||||
let keypair = Ed25519KeyPair::from_seed(&rng)
|
||||
.map(|key| Into::<KeyPair>::into(key))?;
|
||||
|
||||
let private_key = Zeroizing::new(keypair.serialize_openssh(
|
||||
self.encrypt_key.then(|| self.passphrase.as_str()),
|
||||
osshkeys::cipher::Cipher::Aes256_Ctr,
|
||||
)?);
|
||||
|
||||
Ok(((private_key, keypair.serialize_publickey()?), keypair))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_line() -> std::io::Result<String> {
|
||||
let mut line = String::new();
|
||||
std::io::stdin().read_line(&mut line)?;
|
||||
fix_newline_ref(&mut line);
|
||||
|
||||
Ok(line)
|
||||
}
|
||||
|
||||
fn read_non_empty_line() -> std::io::Result<Option<String>> {
|
||||
let line = read_line()?;
|
||||
|
||||
Ok(if line.is_empty() { None } else { Some(line) })
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let opts = Opts::parse();
|
||||
println!("Generating ed25519 ssh keypair:");
|
||||
|
||||
let ((private_key, public_key), keypair) = keygen::SshKeyBuilder::default()
|
||||
.with_passphrase({
|
||||
print!("Enter a passphrase: ");
|
||||
std::io::stdout().flush()?;
|
||||
let passphrase = Zeroizing::new(rpassword::read_password()?);
|
||||
print!("Re-enter the same passphrase: ");
|
||||
std::io::stdout().flush()?;
|
||||
let passphrase2 = Zeroizing::new(rpassword::read_password()?);
|
||||
|
||||
if passphrase == passphrase2 {
|
||||
Ok(passphrase)
|
||||
} else {
|
||||
println!("passphrases do not match.");
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"passphrase did not match.",
|
||||
))
|
||||
}
|
||||
}?)
|
||||
.with_maybe_tag({
|
||||
print!("Enter an optional ID []: ");
|
||||
std::io::stdout().flush()?;
|
||||
|
||||
read_non_empty_line()?
|
||||
})
|
||||
.with_encrypt({
|
||||
print!("Encrypt keypair with passphrase? [Y/n]: ");
|
||||
std::io::stdout().flush()?;
|
||||
|
||||
let encrypting = read_line()? != "n";
|
||||
if encrypting {
|
||||
println!("Encrypting keypair..")
|
||||
};
|
||||
|
||||
encrypting
|
||||
})
|
||||
.with_hash_type({
|
||||
print!("Use hash algorithm (sha256, argon2)[argon2]: ");
|
||||
std::io::stdout().flush()?;
|
||||
|
||||
read_non_empty_line()?
|
||||
.map(|line| HashType::from_str(&line))
|
||||
.unwrap_or(Ok(HashType::Argon2))?
|
||||
})
|
||||
.with_iterations({
|
||||
print!("Number of hashing iterations [4]: ");
|
||||
std::io::stdout().flush()?;
|
||||
|
||||
read_non_empty_line()?
|
||||
.map(|line| i16::from_str(&line))
|
||||
.unwrap_or(Ok(4))?
|
||||
})
|
||||
.build_keypair()?;
|
||||
|
||||
let fingerprint =
|
||||
keypair.fingerprint(osshkeys::keys::FingerprintHash::SHA256)?;
|
||||
|
||||
println!(
|
||||
"Your key fingerprint is: Sha256:{}",
|
||||
base64::encode(&fingerprint)
|
||||
);
|
||||
|
||||
let randomart =
|
||||
randomart::RandomArt::from_digest(&fingerprint).render("ED25519 256", "SHA256")?;
|
||||
let randomart = randomart::RandomArt::from_digest(&fingerprint)
|
||||
.render("ED25519 256", "SHA256")?;
|
||||
|
||||
println!("RandomArt:\n{}", randomart);
|
||||
|
||||
let private_path = opts.file.clone();
|
||||
let public_path = opts.file.clone() + ".pub";
|
||||
|
||||
let private_key = keypair.serialize_openssh(
|
||||
encrypt.then(|| passphrase.as_str()),
|
||||
osshkeys::cipher::Cipher::Aes256_Ctr,
|
||||
)?;
|
||||
std::fs::write(&private_path, private_key)?;
|
||||
|
||||
let public_key = keypair.serialize_publickey()?;
|
||||
std::fs::write(&public_path, public_key)?;
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
|
|
|
@ -1,15 +1,31 @@
|
|||
#[macro_use]
|
||||
use clap::Parser;
|
||||
use libduralumin::passphrase_gen::{PassPhraseGenerator, Words};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use libduralumin::{
|
||||
passphrase_gen::{PassPhraseGenerator, Words},
|
||||
password_gen::PasswordGenerator,
|
||||
};
|
||||
|
||||
/// program that generates random passphrases.
|
||||
#[derive(Parser)]
|
||||
#[clap(
|
||||
name = "duralumin",
|
||||
version = "0.1.0",
|
||||
version = "0.2.0",
|
||||
author = "No One <noonebtw@nirgendwo.xyz>"
|
||||
)]
|
||||
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<i16>,
|
||||
#[clap(long)]
|
||||
|
@ -18,11 +34,43 @@ struct Opts {
|
|||
min_word_length: Option<i16>,
|
||||
#[clap(short, long)]
|
||||
count: Option<i16>,
|
||||
#[clap(long, default_value = "useapassphrase")]
|
||||
#[clap(
|
||||
long,
|
||||
default_value = "useapassphrase",
|
||||
long_help = "word list to use for passphrase generation, a filepath or any of `useapassphrase`, `english`"
|
||||
)]
|
||||
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<u16>,
|
||||
/// 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)
|
||||
|
@ -47,6 +95,7 @@ impl Opts {
|
|||
enum Backend {
|
||||
UseAPassPhrase,
|
||||
EnglishWords,
|
||||
File(PathBuf),
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
|
@ -57,30 +106,66 @@ impl Backend {
|
|||
match s.as_ref() {
|
||||
"useapassphrase" => Some(Backend::UseAPassPhrase),
|
||||
"english" => Some(Backend::EnglishWords),
|
||||
_ => None,
|
||||
_ => {
|
||||
let path = PathBuf::from(s.as_ref());
|
||||
if path.exists() {
|
||||
Some(Backend::File(path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")),
|
||||
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());
|
||||
|
||||
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 passphrase in gen
|
||||
.generate_n(&words, opts.passphrase_count() as usize)
|
||||
.expect("failed to generate passphrases. this is bad.")
|
||||
{
|
||||
println!("{}", passphrase);
|
||||
for password in gen.generate_n(args.count as usize) {
|
||||
println!("{}", password);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -80,11 +80,7 @@ impl PassPhraseGenerator {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn generate_n(
|
||||
&self,
|
||||
words: &Words,
|
||||
count: usize,
|
||||
) -> Option<Vec<String>> {
|
||||
pub fn generate_n(&self, words: &Words, count: usize) -> Option<Vec<String>> {
|
||||
(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::<String>()
|
||||
+ chars.as_str()
|
||||
}
|
||||
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
|
||||
}
|
||||
} else {
|
||||
word.to_string()
|
||||
|
|
71
src/password_gen.rs
Normal file
71
src/password_gen.rs
Normal file
|
@ -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::<Vec<_>>();
|
||||
|
||||
let mut bytes = std::iter::repeat_with(|| **set.choose(&mut OsRng).unwrap())
|
||||
.take(length)
|
||||
.collect::<Vec<_>>();
|
||||
bytes.shuffle(&mut OsRng);
|
||||
|
||||
String::from_utf8(bytes).expect("password bytes to utf8 string.")
|
||||
}
|
||||
|
||||
pub fn generate_n(&self, n: usize) -> Vec<String> {
|
||||
std::iter::repeat_with(|| self.generate()).take(n).collect()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue