From 6259840c43659c2322ff1da7694d5a558111c5c0 Mon Sep 17 00:00:00 2001 From: NoOneBtw Date: Wed, 17 Nov 2021 13:42:09 +0100 Subject: [PATCH] added duralumin-keygen --- Cargo.toml | 17 ++- src/bin/duralumin-keygen.rs | 75 ++++++++++ src/bin/duralumin.rs | 2 +- src/ed25519.rs | 284 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 161 +------------------- src/passphrase_gen.rs | 158 ++++++++++++++++++++ 6 files changed, 538 insertions(+), 159 deletions(-) create mode 100644 src/bin/duralumin-keygen.rs create mode 100644 src/ed25519.rs create mode 100644 src/passphrase_gen.rs diff --git a/Cargo.toml b/Cargo.toml index 39132bd..6a6c7a5 100644 --- a/Cargo.toml +++ b/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" \ No newline at end of file +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} \ No newline at end of file diff --git a/src/bin/duralumin-keygen.rs b/src/bin/duralumin-keygen.rs new file mode 100644 index 0000000..96af9a0 --- /dev/null +++ b/src/bin/duralumin-keygen.rs @@ -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 " +)] +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(()) +} diff --git a/src/bin/duralumin.rs b/src/bin/duralumin.rs index 9ee3c4b..bf981f0 100644 --- a/src/bin/duralumin.rs +++ b/src/bin/duralumin.rs @@ -1,5 +1,5 @@ use clap::Parser; -use libduralumin::{PassPhraseGenerator, Words}; +use libduralumin::passphrase_gen::{PassPhraseGenerator, Words}; /// program that generates random passphrases. #[derive(Parser)] diff --git a/src/ed25519.rs b/src/ed25519.rs new file mode 100644 index 0000000..0c17f3e --- /dev/null +++ b/src/ed25519.rs @@ -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(text: S) -> Option<()> + where + S: AsRef, + { + 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( + passphrase: S1, + id: Option, +) -> OsshResult +where + S1: AsRef, + S2: AsRef, +{ + 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::::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 { + 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); + } +} diff --git a/src/lib.rs b/src/lib.rs index 251dd7d..2961f60 100644 --- a/src/lib.rs +++ b/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, -} - -impl Words { - pub fn new() -> Self { - Self { words: Vec::new() } - } - - pub fn from_text_file(file: File) -> std::io::Result { - 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, -} - -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) -> Self { - self.min_word_length = length; - - self - } - - pub fn generate_n( - &self, - words: &Words, - count: usize, - ) -> Option> { - (0..count).map(|_| self.generate(words)).collect() - } - - pub fn generate(&self, words: &Words) -> Option { - 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::() - + chars.as_str() - } - } - } else { - word.to_string() - } - }) - .collect::>() - .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; diff --git a/src/passphrase_gen.rs b/src/passphrase_gen.rs new file mode 100644 index 0000000..ba77de6 --- /dev/null +++ b/src/passphrase_gen.rs @@ -0,0 +1,158 @@ +use std::{ + fs::File, + io::{BufReader, Read}, +}; + +use rand::{prelude::*, rngs::OsRng}; + +pub struct Words { + words: Vec, +} + +impl Words { + pub fn new() -> Self { + Self { words: Vec::new() } + } + + pub fn from_text_file(file: File) -> std::io::Result { + 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, +} + +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) -> Self { + self.min_word_length = length; + + self + } + + pub fn generate_n( + &self, + words: &Words, + count: usize, + ) -> Option> { + (0..count).map(|_| self.generate(words)).collect() + } + + pub fn generate(&self, words: &Words) -> Option { + 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::() + + chars.as_str() + } + } + } else { + word.to_string() + } + }) + .collect::>() + .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())); + } +}