duralumin/src/bin/duralumin-keygen.rs
2024-07-30 15:26:08 +02:00

211 lines
5.9 KiB
Rust

use clap::Parser;
/// program that generates ed25519 keypairs seeded by a passphrase and an optional ID.
#[derive(Parser)]
#[clap(
name = "duralumin-keygen",
version = "0.2.0",
author = "No One <noonebtw@nirgendwo.xyz>"
)]
struct Opts {
#[clap(short, long, default_value = "duralumin")]
file: String,
}
pub mod keygen {
use std::str::FromStr;
use osshkeys::{error::OsshResult, keys::ed25519::Ed25519KeyPair, KeyPair};
use rand::{Rng, SeedableRng};
use sha2::Digest;
use thiserror::Error;
use zeroize::Zeroizing;
#[derive(Debug, Error)]
pub enum Error {
#[error("Failed to parse")]
ParseError,
}
#[derive(Debug, Clone)]
pub enum KeyType {
SSH,
PGP,
}
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 mut seed = [0u8; 32];
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.as_slice());
}
seed.copy_from_slice(&digest[..32]);
}
HashType::Argon2 => {
// TODO: make more random salt
let salt = b"thissaltneedsupdating";
let argon = argon2::Argon2::new(
argon2::Algorithm::Argon2i,
argon2::Version::V0x13,
argon2::Params::new(65536, self.iterations as u32, 4, Some(32))
.expect("argon2 params"),
);
argon
.hash_password_into(hash_seed.as_bytes(), salt, &mut seed)
.expect("hash passphrase");
}
};
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 main() -> anyhow::Result<()> {
let opts = Opts::parse();
println!("Generating ed25519 ssh keypair:");
let desc = libduralumin::key_gen::cli::keygen_desc_from_stdin()?;
let keypair = libduralumin::key_gen::generate_key(desc)?;
println!(
"Your key fingerprint is: Sha256:{}",
keypair.fingerprint_base64()
);
println!(
"RandomArt:\n{}",
keypair.randomart().render("ED25519 256", "SHA256")?
);
let private_path = opts.file.clone();
let public_path = opts.file.clone() + ".pub";
let (private_key, public_key) = keypair.encode_keys()?;
std::fs::write(&private_path, private_key)?;
std::fs::write(&public_path, public_key)?;
#[cfg(target_family = "unix")]
{
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(private_path, Permissions::from_mode(0o0600))?;
std::fs::set_permissions(public_path, Permissions::from_mode(0o0600))?;
}
Ok(())
}