added duralumin-keygen
This commit is contained in:
parent
eea64e7ef6
commit
6259840c43
17
Cargo.toml
17
Cargo.toml
|
@ -7,11 +7,26 @@ edition = "2021"
|
|||
name = "libduralumin"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = ["passphrase-gen", "ed25519", "clap", "rpassword", "base64"]
|
||||
ed25519 = ["osshkeys", "sha2"]
|
||||
passphrase-gen = []
|
||||
|
||||
[[bin]]
|
||||
name = "duralumin"
|
||||
required-features = ["passphrase-gen", "clap"]
|
||||
|
||||
[[bin]]
|
||||
name = "duralumin-keygen"
|
||||
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"
|
||||
clap = "3.0.0-beta.5"
|
||||
clap = {version = "3.0.0-beta.5", optional = true}
|
||||
base64 = {version = "0.13.0", optional = true}
|
||||
bytes = {version = "1.1.0", optional = true}
|
||||
osshkeys = {path = "../rust-osshkeys", optional = true}
|
||||
sha2 = {version = "0.9.8", optional = true}
|
||||
rpassword = {version = "5.0.1", optional = true}
|
75
src/bin/duralumin-keygen.rs
Normal file
75
src/bin/duralumin-keygen.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use std::io::Write;
|
||||
|
||||
use clap::Parser;
|
||||
use libduralumin::ed25519::{generate_ed25519_keypair, randomart};
|
||||
use osshkeys::{error::OsshResult, Key, PublicParts};
|
||||
|
||||
/// program that generates ed25519 keypairs seeded by a passphrase and an optional ID.
|
||||
#[derive(Parser)]
|
||||
#[clap(
|
||||
name = "duralumin",
|
||||
version = "0.1.0",
|
||||
author = "No One <noonebtw@nirgendwo.xyz>"
|
||||
)]
|
||||
struct Opts {
|
||||
#[clap(short, long, default_value = "id_ed25519")]
|
||||
file: String,
|
||||
}
|
||||
|
||||
fn main() -> OsshResult<()> {
|
||||
let opts = Opts::parse();
|
||||
println!("Generating ed25519 keypair:");
|
||||
|
||||
print!("Enter a passphrase: ");
|
||||
std::io::stdout().flush()?;
|
||||
let passphrase = rpassword::read_password()?;
|
||||
|
||||
print!("Enter an optional ID []: ");
|
||||
std::io::stdout().flush()?;
|
||||
let id = {
|
||||
let mut id = String::new();
|
||||
std::io::stdin().read_line(&mut id)?;
|
||||
|
||||
if id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(id)
|
||||
}
|
||||
};
|
||||
|
||||
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)?;
|
||||
|
||||
line.to_lowercase() == "y"
|
||||
};
|
||||
|
||||
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")?;
|
||||
|
||||
println!("RandomArt:\n{}", randomart);
|
||||
|
||||
let private_key = keypair.serialize_openssh(
|
||||
Some(&passphrase),
|
||||
osshkeys::cipher::Cipher::Aes256_Ctr,
|
||||
)?;
|
||||
std::fs::write(&opts.file, private_key)?;
|
||||
|
||||
let public_key = keypair.serialize_publickey()?;
|
||||
std::fs::write(opts.file + ".pub", public_key)?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
use clap::Parser;
|
||||
use libduralumin::{PassPhraseGenerator, Words};
|
||||
use libduralumin::passphrase_gen::{PassPhraseGenerator, Words};
|
||||
|
||||
/// program that generates random passphrases.
|
||||
#[derive(Parser)]
|
||||
|
|
284
src/ed25519.rs
Normal file
284
src/ed25519.rs
Normal file
|
@ -0,0 +1,284 @@
|
|||
use osshkeys::{error::OsshResult, keys::ed25519::Ed25519KeyPair, KeyPair};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
#[cfg(all(feature = "base64", feature = "bytes"))]
|
||||
mod asdf {
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use bytes::Buf;
|
||||
|
||||
pub struct Ed25519PrivateKey {}
|
||||
|
||||
const OPENSSH_BEGIN: &str = "-----BEGIN OPENSSH PRIVATE KEY-----";
|
||||
const OPENSSH_END: &str = "-----END OPENSSH PRIVATE KEY-----";
|
||||
const OPENSSH_MAGIC: &str = "openssh-key-v1\0";
|
||||
|
||||
impl Ed25519PrivateKey {
|
||||
pub fn parse<S>(text: S) -> Option<()>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let mut lines = text.as_ref().lines();
|
||||
|
||||
let data = (lines.next() == Some(OPENSSH_BEGIN))
|
||||
.then(|| {
|
||||
let base64_content = lines
|
||||
.take_while(|&line| line != OPENSSH_END)
|
||||
.fold(String::new(), |mut acc, line| {
|
||||
acc.push_str(line);
|
||||
acc
|
||||
});
|
||||
|
||||
base64::decode(&base64_content).ok()
|
||||
})
|
||||
.flatten()
|
||||
.map(|data| Cursor::new(data))
|
||||
.and_then(|mut data| {
|
||||
let mut magic = vec![0u8; OPENSSH_MAGIC.len()];
|
||||
data.read_exact(&mut magic).unwrap();
|
||||
|
||||
// cypher name
|
||||
let cypher_len = data.get_u32();
|
||||
let mut cypher_name = vec![0u8; cypher_len as usize];
|
||||
data.read_exact(&mut cypher_name).unwrap();
|
||||
|
||||
// kdf name
|
||||
let kdf_name_len = data.get_u32();
|
||||
let mut kdf_name = vec![0u8; kdf_name_len as usize];
|
||||
data.read_exact(&mut kdf_name).unwrap();
|
||||
|
||||
// kdf
|
||||
let kdf_len = data.get_u32();
|
||||
let kdf = (kdf_len > 0).then(|| {
|
||||
let mut kdf = vec![0u8; kdf_len as usize];
|
||||
data.read_exact(&mut kdf).unwrap();
|
||||
kdf
|
||||
});
|
||||
|
||||
// key_count should always be `1`
|
||||
let key_count = data.get_u32();
|
||||
assert_eq!(key_count, 1);
|
||||
|
||||
// ssh public key
|
||||
|
||||
let pubkey_len = data.get_u32() as usize;
|
||||
|
||||
let keytype_len = data.get_u32() as usize;
|
||||
let mut keytype = vec![0u8; keytype_len];
|
||||
data.read_exact(&mut keytype).unwrap();
|
||||
|
||||
let pub1_len = data.get_u32() as usize;
|
||||
let mut pub1 = vec![0u8; pub1_len];
|
||||
data.read_exact(&mut pub1).unwrap();
|
||||
|
||||
let pub2_len = data.get_u32() as usize;
|
||||
let mut pub2 = vec![0u8; pub2_len];
|
||||
data.read_exact(&mut pub2).unwrap();
|
||||
|
||||
println!("magic: {}", String::from_utf8_lossy(&magic));
|
||||
println!(
|
||||
"cypher: {}",
|
||||
String::from_utf8_lossy(&cypher_name)
|
||||
);
|
||||
println!("kdf: {}", String::from_utf8_lossy(&kdf_name));
|
||||
|
||||
println!("keytype: {}", String::from_utf8_lossy(&keytype));
|
||||
println!("pub1[{}]: {:?}", pub1_len, &pub1);
|
||||
println!("pub2[{}]: {:?}", pub2_len, &pub2);
|
||||
|
||||
Some(())
|
||||
});
|
||||
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
mod tests {
|
||||
use super::Ed25519PrivateKey;
|
||||
|
||||
#[test]
|
||||
fn test_ed25519() {
|
||||
Ed25519PrivateKey::parse(include_str!("../ed25519"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ed25519_passphrased() {
|
||||
Ed25519PrivateKey::parse(include_str!("../ed25519-passphrased"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_ed25519_keypair<S1, S2>(
|
||||
passphrase: S1,
|
||||
id: Option<S2>,
|
||||
) -> OsshResult<KeyPair>
|
||||
where
|
||||
S1: AsRef<str>,
|
||||
S2: AsRef<str>,
|
||||
{
|
||||
let hash = {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(passphrase.as_ref());
|
||||
if let Some(id) = id {
|
||||
hasher.update(id.as_ref());
|
||||
}
|
||||
|
||||
hasher.finalize()
|
||||
};
|
||||
|
||||
Ed25519KeyPair::from_seed(&hash).map(|key| Into::<KeyPair>::into(key))
|
||||
}
|
||||
|
||||
pub mod randomart {
|
||||
use std::fmt::Write;
|
||||
|
||||
use osshkeys::error::OsshResult;
|
||||
|
||||
const WIDTH: usize = 17;
|
||||
const HEIGHT: usize = 9;
|
||||
const TILES: &[char] = &[
|
||||
' ', '.', 'o', '+', '=', '*', 'B', 'O', 'X', '@', '%', '&', '#', '/',
|
||||
'^',
|
||||
];
|
||||
|
||||
pub struct RandomArt {
|
||||
tiles: [[i8; WIDTH]; HEIGHT],
|
||||
}
|
||||
|
||||
impl RandomArt {
|
||||
pub fn render(
|
||||
&self,
|
||||
title: &str,
|
||||
subtitle: &str,
|
||||
) -> Result<String, std::fmt::Error> {
|
||||
let mut string = String::new();
|
||||
|
||||
string
|
||||
.write_str(&format!("+{:-^17}+\n", format!("[{}]", title)))?;
|
||||
|
||||
for row in self.tiles {
|
||||
string.write_char('|')?;
|
||||
for cell in row {
|
||||
if cell == -1 {
|
||||
string.write_char('S')?;
|
||||
} else if cell == -2 {
|
||||
string.write_char('E')?;
|
||||
} else {
|
||||
let cell = (TILES.len() - 1).min(cell as usize);
|
||||
string.write_char(TILES[cell])?;
|
||||
}
|
||||
}
|
||||
string.write_char('|')?;
|
||||
string.write_char('\n')?;
|
||||
}
|
||||
|
||||
string.write_str(&format!(
|
||||
"+{:-^17}+\n",
|
||||
format!("[{}]", subtitle)
|
||||
))?;
|
||||
|
||||
Ok(string)
|
||||
}
|
||||
|
||||
pub fn from_digest(digest: &[u8]) -> Self {
|
||||
let mut tiles = [[0i8; WIDTH]; HEIGHT];
|
||||
let mut x = WIDTH / 2;
|
||||
let mut y = HEIGHT / 2;
|
||||
|
||||
tiles[y][x] = -1;
|
||||
|
||||
for byte in digest.iter() {
|
||||
for i in 0..4 {
|
||||
let b = (*byte >> (i * 2)) & 3;
|
||||
match b {
|
||||
0 | 1 => {
|
||||
if y > 0 {
|
||||
y -= 1;
|
||||
}
|
||||
}
|
||||
2 | 3 => {
|
||||
if y < HEIGHT - 1 {
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
match b {
|
||||
0 | 2 => {
|
||||
if x > 0 {
|
||||
x -= 1;
|
||||
}
|
||||
}
|
||||
1 | 3 => {
|
||||
if x < WIDTH - 1 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if tiles[y][x] >= 0 {
|
||||
tiles[y][x] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tiles[y][x] = -2;
|
||||
|
||||
Self { tiles }
|
||||
}
|
||||
}
|
||||
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const FINGERPRINT: &str =
|
||||
"L5N7A2PETaGegW5P3qh/Vjd8AW6Mn4B+VB2SHK+eZCY=";
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
let randomart =
|
||||
RandomArt::from_digest(&base64::decode(FINGERPRINT).unwrap())
|
||||
.render("Title", "Subtitle")
|
||||
.unwrap();
|
||||
|
||||
println!("{}", randomart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod tests {
|
||||
#![allow(dead_code)]
|
||||
#![allow(unused_imports)]
|
||||
use osshkeys::{
|
||||
keys::ed25519::Ed25519KeyPair, KeyPair, PrivateParts, PublicParts,
|
||||
};
|
||||
use sha2::Digest;
|
||||
|
||||
use super::randomart::RandomArt;
|
||||
|
||||
const PASSPHRASE: &str = "the spice must flow";
|
||||
|
||||
const DATA: &str = "I’d just like to interject for a moment. What you’re refering to as Linux, is in fact, GNU/Linux, or as I’ve recently taken to calling it, GNU plus Linux. Linux is not an operating system unto itself, but rather another free component of a fully functioning GNU system made useful by the GNU corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX.";
|
||||
|
||||
const SIGNATURE: &[u8] = &[
|
||||
115, 18, 156, 87, 192, 149, 107, 105, 13, 87, 219, 90, 26, 146, 41,
|
||||
114, 20, 143, 253, 206, 216, 236, 222, 66, 252, 136, 38, 216, 184, 127,
|
||||
94, 255, 68, 246, 64, 228, 141, 64, 63, 64, 236, 222, 184, 214, 3, 157,
|
||||
73, 186, 73, 156, 20, 100, 76, 241, 113, 81, 38, 131, 174, 31, 103,
|
||||
181, 220, 11,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(PASSPHRASE);
|
||||
|
||||
let hash = hasher.finalize();
|
||||
|
||||
let keypair: KeyPair = Ed25519KeyPair::from_seed(&hash).unwrap().into();
|
||||
|
||||
let asdf = keypair.sign(DATA.as_bytes()).unwrap();
|
||||
assert_eq!(keypair.verify(DATA.as_bytes(), SIGNATURE).unwrap(), true);
|
||||
}
|
||||
}
|
161
src/lib.rs
161
src/lib.rs
|
@ -1,158 +1,5 @@
|
|||
use std::{
|
||||
fs::File,
|
||||
io::{BufReader, Read},
|
||||
};
|
||||
#[cfg(feature = "passphrase-gen")]
|
||||
pub mod passphrase_gen;
|
||||
|
||||
use rand::{prelude::*, rngs::OsRng};
|
||||
|
||||
pub struct Words {
|
||||
words: Vec<String>,
|
||||
}
|
||||
|
||||
impl Words {
|
||||
pub fn new() -> Self {
|
||||
Self { words: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn from_text_file(file: File) -> std::io::Result<Self> {
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut contents = String::new();
|
||||
reader.read_to_string(&mut contents)?;
|
||||
|
||||
Ok(Self::from_str(&contents))
|
||||
}
|
||||
|
||||
pub fn from_str(words: &str) -> Self {
|
||||
Self {
|
||||
words: words.split_whitespace().map(|s| s.to_owned()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn random_words_with_min_length(
|
||||
&self,
|
||||
word_count: usize,
|
||||
min_word_length: usize,
|
||||
) -> Vec<&str> {
|
||||
self.words
|
||||
.iter()
|
||||
.filter(|word| word.chars().count() >= min_word_length)
|
||||
.map(|word| word.as_str())
|
||||
.choose_multiple(&mut OsRng, word_count)
|
||||
}
|
||||
|
||||
pub fn random_word(&self) -> &str {
|
||||
let i = OsRng.gen_range(0..self.words.len());
|
||||
|
||||
self.words[i].as_str()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PassPhraseGenerator {
|
||||
capitalized: bool,
|
||||
length: i16,
|
||||
min_word_length: Option<i16>,
|
||||
}
|
||||
|
||||
impl PassPhraseGenerator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
capitalized: false,
|
||||
length: 6,
|
||||
min_word_length: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_capitalized(mut self, capitalized: bool) -> Self {
|
||||
self.capitalized = capitalized;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_length(mut self, length: i16) -> Self {
|
||||
self.length = length;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_min_word_length(mut self, length: Option<i16>) -> Self {
|
||||
self.min_word_length = length;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn generate_n(
|
||||
&self,
|
||||
words: &Words,
|
||||
count: usize,
|
||||
) -> Option<Vec<String>> {
|
||||
(0..count).map(|_| self.generate(words)).collect()
|
||||
}
|
||||
|
||||
pub fn generate(&self, words: &Words) -> Option<String> {
|
||||
let random_words = words.random_words_with_min_length(
|
||||
self.length as usize,
|
||||
self.min_word_length.unwrap_or(0) as usize,
|
||||
);
|
||||
|
||||
if random_words.len() == self.length as usize {
|
||||
Some(
|
||||
random_words
|
||||
.into_iter()
|
||||
.map(|word| {
|
||||
if self.capitalized {
|
||||
let mut chars = word.chars();
|
||||
|
||||
match chars.next() {
|
||||
None => String::new(),
|
||||
Some(c) => {
|
||||
c.to_uppercase().collect::<String>()
|
||||
+ chars.as_str()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
word.to_string()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{PassPhraseGenerator, Words};
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
//let words = Words::fr
|
||||
let words = Words::from_str(include_str!("../words_alpha.txt"));
|
||||
let gen = PassPhraseGenerator::new().with_length(4);
|
||||
|
||||
let passphrase = gen.generate(&words);
|
||||
let mut pass_words = passphrase.split_whitespace();
|
||||
assert!(pass_words.next().unwrap().chars().all(|c| c.is_lowercase()));
|
||||
assert!(pass_words.next().is_some());
|
||||
assert!(pass_words.next().is_some());
|
||||
assert!(pass_words.next().is_some());
|
||||
assert!(pass_words.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capitalize() {
|
||||
//let words = Words::fr
|
||||
let words = Words::from_str(include_str!("../words_alpha.txt"));
|
||||
let gen = PassPhraseGenerator::new()
|
||||
.with_length(1)
|
||||
.with_capitalized(true);
|
||||
|
||||
let passphrase = gen.generate(&words);
|
||||
let mut pass_words = passphrase.split_whitespace();
|
||||
let mut first_word = pass_words.next().unwrap().chars();
|
||||
assert!(first_word.next().unwrap().is_uppercase());
|
||||
assert!(first_word.all(|c| c.is_lowercase()));
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "ed25519")]
|
||||
pub mod ed25519;
|
||||
|
|
158
src/passphrase_gen.rs
Normal file
158
src/passphrase_gen.rs
Normal file
|
@ -0,0 +1,158 @@
|
|||
use std::{
|
||||
fs::File,
|
||||
io::{BufReader, Read},
|
||||
};
|
||||
|
||||
use rand::{prelude::*, rngs::OsRng};
|
||||
|
||||
pub struct Words {
|
||||
words: Vec<String>,
|
||||
}
|
||||
|
||||
impl Words {
|
||||
pub fn new() -> Self {
|
||||
Self { words: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn from_text_file(file: File) -> std::io::Result<Self> {
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut contents = String::new();
|
||||
reader.read_to_string(&mut contents)?;
|
||||
|
||||
Ok(Self::from_str(&contents))
|
||||
}
|
||||
|
||||
pub fn from_str(words: &str) -> Self {
|
||||
Self {
|
||||
words: words.split_whitespace().map(|s| s.to_owned()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn random_words_with_min_length(
|
||||
&self,
|
||||
word_count: usize,
|
||||
min_word_length: usize,
|
||||
) -> Vec<&str> {
|
||||
self.words
|
||||
.iter()
|
||||
.filter(|word| word.chars().count() >= min_word_length)
|
||||
.map(|word| word.as_str())
|
||||
.choose_multiple(&mut OsRng, word_count)
|
||||
}
|
||||
|
||||
pub fn random_word(&self) -> &str {
|
||||
let i = OsRng.gen_range(0..self.words.len());
|
||||
|
||||
self.words[i].as_str()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PassPhraseGenerator {
|
||||
capitalized: bool,
|
||||
length: i16,
|
||||
min_word_length: Option<i16>,
|
||||
}
|
||||
|
||||
impl PassPhraseGenerator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
capitalized: false,
|
||||
length: 6,
|
||||
min_word_length: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_capitalized(mut self, capitalized: bool) -> Self {
|
||||
self.capitalized = capitalized;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_length(mut self, length: i16) -> Self {
|
||||
self.length = length;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_min_word_length(mut self, length: Option<i16>) -> Self {
|
||||
self.min_word_length = length;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn generate_n(
|
||||
&self,
|
||||
words: &Words,
|
||||
count: usize,
|
||||
) -> Option<Vec<String>> {
|
||||
(0..count).map(|_| self.generate(words)).collect()
|
||||
}
|
||||
|
||||
pub fn generate(&self, words: &Words) -> Option<String> {
|
||||
let random_words = words.random_words_with_min_length(
|
||||
self.length as usize,
|
||||
self.min_word_length.unwrap_or(0) as usize,
|
||||
);
|
||||
|
||||
if random_words.len() == self.length as usize {
|
||||
Some(
|
||||
random_words
|
||||
.into_iter()
|
||||
.map(|word| {
|
||||
if self.capitalized {
|
||||
let mut chars = word.chars();
|
||||
|
||||
match chars.next() {
|
||||
None => String::new(),
|
||||
Some(c) => {
|
||||
c.to_uppercase().collect::<String>()
|
||||
+ chars.as_str()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
word.to_string()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
//let words = Words::fr
|
||||
let words = Words::from_str(include_str!("../words_alpha.txt"));
|
||||
let gen = PassPhraseGenerator::new().with_length(4);
|
||||
|
||||
let passphrase = gen.generate(&words).unwrap();
|
||||
let mut pass_words = passphrase.split_whitespace();
|
||||
assert!(pass_words.next().unwrap().chars().all(|c| c.is_lowercase()));
|
||||
assert!(pass_words.next().is_some());
|
||||
assert!(pass_words.next().is_some());
|
||||
assert!(pass_words.next().is_some());
|
||||
assert!(pass_words.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capitalize() {
|
||||
//let words = Words::fr
|
||||
let words = Words::from_str(include_str!("../words_alpha.txt"));
|
||||
let gen = PassPhraseGenerator::new()
|
||||
.with_length(1)
|
||||
.with_capitalized(true);
|
||||
|
||||
let passphrase = gen.generate(&words).unwrap();
|
||||
let mut pass_words = passphrase.split_whitespace();
|
||||
let mut first_word = pass_words.next().unwrap().chars();
|
||||
assert!(first_word.next().unwrap().is_uppercase());
|
||||
assert!(first_word.all(|c| c.is_lowercase()));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue