version bump, breaking change to ssh keygen generation

This commit is contained in:
Janis 2022-05-27 14:09:43 +02:00
parent 76ebacacf2
commit 8c049c4790
3 changed files with 267 additions and 66 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "duralumin" name = "duralumin"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
[lib] [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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
rand = "0.8.4" rand = "0.8"
rand_chacha = "0.3"
clap = {version = "3.0.0-beta.5", optional = true, features = ["derive"]} clap = {version = "3.0.0-beta.5", optional = true, features = ["derive"]}
base64 = {version = "0.13.0", optional = true} base64 = {version = "0.13", optional = true}
bytes = {version = "1.1.0", optional = true} bytes = {version = "1.1", optional = true}
osshkeys = {git = "https://github.com/noonebtw/rust-osshkeys.git", branch = "master", optional = true} osshkeys = {git = "https://github.com/noonebtw/rust-osshkeys.git", branch = "master", optional = true}
sha2 = {version = "0.9.8", optional = true} sha2 = {version = "0.9", optional = true}
rpassword = {version = "5.0.1", optional = true} rpassword = {version = "5.0", optional = true}
zeroize = {version = "1.5"}
rust-argon2 = "1.0"
thiserror = "1.0"
anyhow = "1.0"

View file

@ -1,15 +1,17 @@
use std::io::Write; use std::{io::Write, str::FromStr};
#[macro_use]
use clap::Parser; use clap::Parser;
use libduralumin::ed25519::{generate_ed25519_keypair, randomart}; use libduralumin::ed25519::randomart;
use osshkeys::{error::OsshResult, PublicParts}; use osshkeys::PublicParts;
use zeroize::Zeroizing;
use crate::keygen::HashType;
/// program that generates ed25519 keypairs seeded by a passphrase and an optional ID. /// program that generates ed25519 keypairs seeded by a passphrase and an optional ID.
#[derive(Parser)] #[derive(Parser)]
#[clap( #[clap(
name = "duralumin", name = "duralumin-keygen",
version = "0.1.0", version = "0.2.0",
author = "No One <noonebtw@nirgendwo.xyz>" author = "No One <noonebtw@nirgendwo.xyz>"
)] )]
struct Opts { struct Opts {
@ -34,78 +36,269 @@ fn fix_newline(mut line: String) -> String {
line line
} }
fn main() -> OsshResult<()> { pub mod keygen {
let opts = Opts::parse(); use std::str::FromStr;
println!("Generating ed25519 keypair:");
print!("Enter a passphrase: "); use osshkeys::{error::OsshResult, keys::ed25519::Ed25519KeyPair, KeyPair};
std::io::stdout().flush()?; use rand::{Rng, SeedableRng};
let passphrase = rpassword::read_password()?; use sha2::Digest;
print!("Re-enter the same passphrase: "); use thiserror::Error;
std::io::stdout().flush()?; use zeroize::Zeroizing;
let passphrase2 = rpassword::read_password()?;
if passphrase != passphrase2 { #[derive(Debug, Error)]
println!("passphrases do not match."); pub enum Error {
Err(std::io::Error::new( #[error("Failed to parse")]
std::io::ErrorKind::InvalidInput, ParseError,
"passphrase did not match.",
))?;
} }
print!("Enter an optional ID []: "); #[derive(Debug, Clone)]
std::io::stdout().flush()?; pub enum KeyType {
let id = { SSH,
let mut id = String::new(); PGP,
std::io::stdin().read_line(&mut id)?;
fix_newline_ref(&mut id);
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),
}
}
}
#[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()
}; };
let keypair = generate_ed25519_keypair(&passphrase, id.as_ref())?; argon2::hash_raw(hash_seed.as_bytes(), salt, &config)
.expect("hash passphrase")
}
}
.try_into()
.expect("unwrap seed into [u8; 32]");
print!("Encrypt keypair with passphrase? [Y/n]: "); let rng = rand_chacha::ChaChaRng::from_seed(seed).gen::<[u8; 32]>();
std::io::stdout().flush()?;
let encrypt = { 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(); let mut line = String::new();
std::io::stdin().read_line(&mut line)?; std::io::stdin().read_line(&mut line)?;
fix_newline_ref(&mut line); fix_newline_ref(&mut line);
line.to_lowercase() != "n" 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..")
}; };
if encrypt { encrypting
println!("Using passphrase to encrypt key.."); })
} .with_hash_type({
print!("Use hash algorithm (sha256, argon2)[argon2]: ");
std::io::stdout().flush()?;
let fingerprint = keypair.fingerprint(osshkeys::keys::FingerprintHash::SHA256)?; 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!( println!(
"Your key fingerprint is: Sha256:{}", "Your key fingerprint is: Sha256:{}",
base64::encode(&fingerprint) base64::encode(&fingerprint)
); );
let randomart = let randomart = randomart::RandomArt::from_digest(&fingerprint)
randomart::RandomArt::from_digest(&fingerprint).render("ED25519 256", "SHA256")?; .render("ED25519 256", "SHA256")?;
println!("RandomArt:\n{}", randomart); println!("RandomArt:\n{}", randomart);
let private_path = opts.file.clone(); let private_path = opts.file.clone();
let public_path = opts.file.clone() + ".pub"; 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)?; std::fs::write(&private_path, private_key)?;
let public_key = keypair.serialize_publickey()?;
std::fs::write(&public_path, public_key)?; std::fs::write(&public_path, public_key)?;
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]

View file

@ -1,4 +1,3 @@
#[macro_use]
use clap::Parser; use clap::Parser;
use libduralumin::passphrase_gen::{PassPhraseGenerator, Words}; use libduralumin::passphrase_gen::{PassPhraseGenerator, Words};
@ -6,7 +5,7 @@ use libduralumin::passphrase_gen::{PassPhraseGenerator, Words};
#[derive(Parser)] #[derive(Parser)]
#[clap( #[clap(
name = "duralumin", name = "duralumin",
version = "0.1.0", version = "0.2.0",
author = "No One <noonebtw@nirgendwo.xyz>" author = "No One <noonebtw@nirgendwo.xyz>"
)] )]
struct Opts { struct Opts {
@ -70,8 +69,12 @@ fn main() {
.with_length(opts.words_per_passphrase()); .with_length(opts.words_per_passphrase());
let words = match opts.backend() { let words = match opts.backend() {
Some(Backend::UseAPassPhrase) => Words::from_str(include_str!("../../useapassphrase.txt")), Some(Backend::UseAPassPhrase) => {
Some(Backend::EnglishWords) => Words::from_str(include_str!("../../words_alpha.txt")), Words::from_str(include_str!("../../useapassphrase.txt"))
}
Some(Backend::EnglishWords) => {
Words::from_str(include_str!("../../words_alpha.txt"))
}
None => { None => {
panic!("invalid backend.") panic!("invalid backend.")
} }