From 8c049c4790e59178a242d4d6b45556addacd8fca Mon Sep 17 00:00:00 2001 From: Janis Date: Fri, 27 May 2022 14:09:43 +0200 Subject: [PATCH] version bump, breaking change to ssh keygen generation --- Cargo.toml | 17 +- src/bin/duralumin-keygen.rs | 305 +++++++++++++++++++++++++++++------- src/bin/duralumin.rs | 11 +- 3 files changed, 267 insertions(+), 66 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d3a6f78..31eed69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "duralumin" -version = "0.1.0" +version = "0.2.0" edition = "2021" [lib] @@ -23,10 +23,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} \ No newline at end of file +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" diff --git a/src/bin/duralumin-keygen.rs b/src/bin/duralumin-keygen.rs index 530fcb3..d44b6d9 100644 --- a/src/bin/duralumin-keygen.rs +++ b/src/bin/duralumin-keygen.rs @@ -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 " )] 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 { + 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 { + 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, + passphrase: Zeroizing, + 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) -> 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>>( + mut self, + passphrase: Z, + ) -> Self { + self.passphrase = passphrase.into(); + self + } + + pub fn build_keypair( + self, + ) -> OsshResult<((Zeroizing, String), KeyPair)> { + let hash_seed = Zeroizing::new( + self.passphrase + .chars() + .chain(self.tag.chars()) + .collect::(), + ); + + 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::::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 { + 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> { + 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")] diff --git a/src/bin/duralumin.rs b/src/bin/duralumin.rs index 868bbd4..ac54fa9 100644 --- a/src/bin/duralumin.rs +++ b/src/bin/duralumin.rs @@ -1,4 +1,3 @@ -#[macro_use] use clap::Parser; use libduralumin::passphrase_gen::{PassPhraseGenerator, Words}; @@ -6,7 +5,7 @@ use libduralumin::passphrase_gen::{PassPhraseGenerator, Words}; #[derive(Parser)] #[clap( name = "duralumin", - version = "0.1.0", + version = "0.2.0", author = "No One " )] struct Opts { @@ -70,8 +69,12 @@ fn main() { .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::UseAPassPhrase) => { + Words::from_str(include_str!("../../useapassphrase.txt")) + } + Some(Backend::EnglishWords) => { + Words::from_str(include_str!("../../words_alpha.txt")) + } None => { panic!("invalid backend.") }