Compare commits
	
		
			No commits in common. "0d242d16d7979d45450d4ef535b857521091471c" and "9e7d8c3494e8eb33398dda8e92a20b8504927a1b" have entirely different histories.
		
	
	
		
			0d242d16d7
			...
			9e7d8c3494
		
	
		
							
								
								
									
										16
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								Cargo.toml
									
									
									
									
									
								
							| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "duralumin"
 | 
					name = "duralumin"
 | 
				
			||||||
version = "0.3.0"
 | 
					version = "0.2.0"
 | 
				
			||||||
edition = "2021"
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[lib]
 | 
					[lib]
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ path = "src/lib.rs"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[features]
 | 
					[features]
 | 
				
			||||||
default = ["passphrase-gen", "password-gen", "ed25519", "clap", "rpassword", "base64"]
 | 
					default = ["passphrase-gen", "password-gen", "ed25519", "clap", "rpassword", "base64"]
 | 
				
			||||||
ed25519 = ["sha2"]
 | 
					ed25519 = ["osshkeys", "sha2"]
 | 
				
			||||||
passphrase-gen = []
 | 
					passphrase-gen = []
 | 
				
			||||||
password-gen = []
 | 
					password-gen = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,16 +25,14 @@ required-features = ["ed25519", "clap", "rpassword", "base64"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
rand = "0.8"
 | 
					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", optional = true}
 | 
					base64 = {version = "0.13", optional = true}
 | 
				
			||||||
bytes = {version = "1.1", 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", optional = true}
 | 
					sha2 = {version = "0.9", optional = true}
 | 
				
			||||||
rpassword = {version = "7.0", optional = true}
 | 
					rpassword = {version = "5.0", optional = true}
 | 
				
			||||||
zeroize = {version = "1.8"}
 | 
					zeroize = {version = "1.5"}
 | 
				
			||||||
argon2 = "0.5.3"
 | 
					rust-argon2 = "1.0"
 | 
				
			||||||
thiserror = "1.0"
 | 
					thiserror = "1.0"
 | 
				
			||||||
anyhow = "1.0"
 | 
					anyhow = "1.0"
 | 
				
			||||||
 | 
					 | 
				
			||||||
itertools = "0.13.0"
 | 
					 | 
				
			||||||
ssh-key = {version = "0.6.6", features = ["ed25519", "encryption"]}
 | 
					 | 
				
			||||||
ed25519-dalek = "2.1.1"
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,59 +1,315 @@
 | 
				
			||||||
 | 
					use std::{io::Write, str::FromStr};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use clap::Parser;
 | 
					use clap::Parser;
 | 
				
			||||||
 | 
					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.
 | 
					/// program that generates ed25519 keypairs seeded by a passphrase and an optional ID.
 | 
				
			||||||
#[derive(Parser)]
 | 
					#[derive(Parser)]
 | 
				
			||||||
#[clap(
 | 
					#[clap(
 | 
				
			||||||
    name = "duralumin-keygen",
 | 
					    name = "duralumin-keygen",
 | 
				
			||||||
    version = "0.3.0",
 | 
					    version = "0.2.0",
 | 
				
			||||||
    author = "No One <noonebtw@nirgendwo.xyz>"
 | 
					    author = "No One <noonebtw@nirgendwo.xyz>"
 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
struct Opts {
 | 
					struct Opts {
 | 
				
			||||||
    #[clap(short, long)]
 | 
					    #[clap(short, long, default_value = "id_ed25519")]
 | 
				
			||||||
    file: Option<String>,
 | 
					    file: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn fix_newline_ref(line: &mut String) {
 | 
				
			||||||
 | 
					    if line.ends_with('\n') {
 | 
				
			||||||
 | 
					        line.pop();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if line.ends_with('\r') {
 | 
				
			||||||
 | 
					            line.pop();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[allow(dead_code)]
 | 
				
			||||||
 | 
					fn fix_newline(mut line: String) -> String {
 | 
				
			||||||
 | 
					    fix_newline_ref(&mut line);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    line
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 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::<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();
 | 
				
			||||||
 | 
					    std::io::stdin().read_line(&mut line)?;
 | 
				
			||||||
 | 
					    fix_newline_ref(&mut line);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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<()> {
 | 
					fn main() -> anyhow::Result<()> {
 | 
				
			||||||
    let opts = Opts::parse();
 | 
					    let opts = Opts::parse();
 | 
				
			||||||
    println!("Generating ed25519 ssh keypair:");
 | 
					    println!("Generating ed25519 ssh keypair:");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let desc = libduralumin::key_gen::cli::keygen_desc_from_stdin()?;
 | 
					    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()?);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let base_path = opts.file.unwrap_or_else(|| {
 | 
					            if passphrase == passphrase2 {
 | 
				
			||||||
        if let Some(tag) = desc.tag.as_ref() {
 | 
					                Ok(passphrase)
 | 
				
			||||||
            format!("duralumin_{}", tag.as_str())
 | 
					            } else {
 | 
				
			||||||
        } else {
 | 
					                println!("passphrases do not match.");
 | 
				
			||||||
            "duralumin".to_owned()
 | 
					                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()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let keypair = libduralumin::key_gen::generate_key(desc)?;
 | 
					            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!(
 | 
					    println!(
 | 
				
			||||||
        "Your key fingerprint is: Sha256:{}",
 | 
					        "Your key fingerprint is: Sha256:{}",
 | 
				
			||||||
        keypair.fingerprint_base64()
 | 
					        base64::encode(&fingerprint)
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    println!(
 | 
					    let randomart = randomart::RandomArt::from_digest(&fingerprint)
 | 
				
			||||||
        "RandomArt:\n{}",
 | 
					        .render("ED25519 256", "SHA256")?;
 | 
				
			||||||
        keypair.randomart().render("ED25519 256", "SHA256")?
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let private_path = base_path.clone();
 | 
					    println!("RandomArt:\n{}", randomart);
 | 
				
			||||||
    let public_path = base_path.clone() + ".pub";
 | 
					
 | 
				
			||||||
 | 
					    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(&private_path, private_key)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    std::fs::write(&public_path, public_key)?;
 | 
					    std::fs::write(&public_path, public_key)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[cfg(target_family = "unix")]
 | 
					    #[cfg(target_family = "unix")]
 | 
				
			||||||
    {
 | 
					    use std::fs::Permissions;
 | 
				
			||||||
        use std::fs::Permissions;
 | 
					    #[cfg(target_family = "unix")]
 | 
				
			||||||
        use std::os::unix::fs::PermissionsExt;
 | 
					    use std::os::unix::fs::PermissionsExt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        std::fs::set_permissions(private_path, Permissions::from_mode(0o0600))?;
 | 
					    #[cfg(target_family = "unix")]
 | 
				
			||||||
        std::fs::set_permissions(public_path, Permissions::from_mode(0o0600))?;
 | 
					    std::fs::set_permissions(private_path, Permissions::from_mode(0o0600))?;
 | 
				
			||||||
    }
 | 
					    #[cfg(target_family = "unix")]
 | 
				
			||||||
 | 
					    std::fs::set_permissions(public_path, Permissions::from_mode(0o0600))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										176
									
								
								src/ed25519.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								src/ed25519.rs
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,176 @@
 | 
				
			||||||
 | 
					use osshkeys::{error::OsshResult, keys::ed25519::Ed25519KeyPair, KeyPair};
 | 
				
			||||||
 | 
					use sha2::{Digest, Sha256};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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 }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[cfg(test)]
 | 
				
			||||||
 | 
					    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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										377
									
								
								src/key_gen.rs
									
									
									
									
									
								
							
							
						
						
									
										377
									
								
								src/key_gen.rs
									
									
									
									
									
								
							| 
						 | 
					@ -1,377 +0,0 @@
 | 
				
			||||||
use sha2::Digest;
 | 
					 | 
				
			||||||
use zeroize::Zeroizing;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::randomart;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub mod cli {
 | 
					 | 
				
			||||||
    use std::io::Write;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    use zeroize::Zeroizing;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    use crate::key_gen::{HashDesc, KeygenDesc};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn fix_newline_ref(line: &mut String) {
 | 
					 | 
				
			||||||
        if line.ends_with('\n') {
 | 
					 | 
				
			||||||
            line.pop();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if line.ends_with('\r') {
 | 
					 | 
				
			||||||
                line.pop();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #[allow(dead_code)]
 | 
					 | 
				
			||||||
    fn fix_newline(mut line: String) -> String {
 | 
					 | 
				
			||||||
        fix_newline_ref(&mut line);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        line
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    pub fn read_line() -> std::io::Result<String> {
 | 
					 | 
				
			||||||
        std::io::stdout().flush()?;
 | 
					 | 
				
			||||||
        let mut line = String::new();
 | 
					 | 
				
			||||||
        std::io::stdin().read_line(&mut line)?;
 | 
					 | 
				
			||||||
        fix_newline_ref(&mut line);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(line)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn read_non_empty_line() -> std::io::Result<Option<String>> {
 | 
					 | 
				
			||||||
        let line = read_line()?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(if line.is_empty() { None } else { Some(line) })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #[derive(thiserror::Error, Debug)]
 | 
					 | 
				
			||||||
    pub enum Error {
 | 
					 | 
				
			||||||
        #[error(transparent)]
 | 
					 | 
				
			||||||
        Io(#[from] std::io::Error),
 | 
					 | 
				
			||||||
        #[error(transparent)]
 | 
					 | 
				
			||||||
        ParseInt(#[from] std::num::ParseIntError),
 | 
					 | 
				
			||||||
        #[error("Mismatching secret.")]
 | 
					 | 
				
			||||||
        MismatchingSecret,
 | 
					 | 
				
			||||||
        #[error("Invalid hash method.")]
 | 
					 | 
				
			||||||
        InvalidHashMethod,
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub type Result<T> = std::result::Result<T, Error>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn read_passphrase() -> Result<Zeroizing<String>> {
 | 
					 | 
				
			||||||
        let passphrase = Zeroizing::new(rpassword::prompt_password("Enter a passphrase: ")?);
 | 
					 | 
				
			||||||
        let passphrase2 = Zeroizing::new(rpassword::prompt_password("Re-enter your passphrase: ")?);
 | 
					 | 
				
			||||||
        std::io::stdout().flush().expect("flush stdout");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if passphrase == passphrase2 {
 | 
					 | 
				
			||||||
            println!(
 | 
					 | 
				
			||||||
                "Passphrase entropy (rough estimate): {:.2}",
 | 
					 | 
				
			||||||
                crate::entropy(&passphrase)
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            Ok(passphrase)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            Err(Error::MismatchingSecret)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn read_argon_desc() -> Result<HashDesc> {
 | 
					 | 
				
			||||||
        print!("Use argon2 variant (argon2i, argon2d, argon2id) [argon2id]: ");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let variant = match read_non_empty_line()?.as_ref().map(|s| s.as_str()) {
 | 
					 | 
				
			||||||
            Some("argon2i") => argon2::Algorithm::Argon2i,
 | 
					 | 
				
			||||||
            Some("argon2d") => argon2::Algorithm::Argon2d,
 | 
					 | 
				
			||||||
            Some("argon2id") | None => argon2::Algorithm::Argon2id,
 | 
					 | 
				
			||||||
            _ => {
 | 
					 | 
				
			||||||
                return Err(Error::InvalidHashMethod);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        print!("Use argon2 version (16,19) [19]: ");
 | 
					 | 
				
			||||||
        let version = match read_non_empty_line()?.as_ref().map(|s| s.as_str()) {
 | 
					 | 
				
			||||||
            Some("16") | Some("10") => argon2::Version::V0x10,
 | 
					 | 
				
			||||||
            Some("19") | Some("13") | None => argon2::Version::V0x13,
 | 
					 | 
				
			||||||
            _ => {
 | 
					 | 
				
			||||||
                return Err(Error::InvalidHashMethod);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        print!("memory size (memory cost) (KiB if no unit specified) (min 19 MiB) [64 MiB]: ");
 | 
					 | 
				
			||||||
        let memory = 'mem: {
 | 
					 | 
				
			||||||
            let Some(line) = read_non_empty_line()? else {
 | 
					 | 
				
			||||||
                break 'mem 64 * 1024;
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
            let line = line.trim();
 | 
					 | 
				
			||||||
            let chars = line.char_indices();
 | 
					 | 
				
			||||||
            let digits_end = chars
 | 
					 | 
				
			||||||
                .take_while(|(_, c)| ('0'..='9').contains(c))
 | 
					 | 
				
			||||||
                .map(|(i, _)| i)
 | 
					 | 
				
			||||||
                .last()
 | 
					 | 
				
			||||||
                .unwrap_or(line.len());
 | 
					 | 
				
			||||||
            let digits = &line[..digits_end];
 | 
					 | 
				
			||||||
            let unit = &line[digits_end..];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let number = digits.parse::<u32>()?;
 | 
					 | 
				
			||||||
            let factor = match unit {
 | 
					 | 
				
			||||||
                "KiB" | "kib" | "" => 1,
 | 
					 | 
				
			||||||
                "MiB" | "mib" => 1024,
 | 
					 | 
				
			||||||
                _ => {
 | 
					 | 
				
			||||||
                    return Err(Error::InvalidHashMethod);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            number * factor
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        print!("Iterations (time cost) [4]: ");
 | 
					 | 
				
			||||||
        let iterations = read_non_empty_line()?
 | 
					 | 
				
			||||||
            .map(|s| s.parse::<u16>())
 | 
					 | 
				
			||||||
            .unwrap_or(Ok(4))?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        print!("Lanes (parallelism cost) [1]: ");
 | 
					 | 
				
			||||||
        let lanes = read_non_empty_line()?
 | 
					 | 
				
			||||||
            .map(|s| s.parse::<u16>())
 | 
					 | 
				
			||||||
            .unwrap_or(Ok(1))?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(HashDesc::Argon {
 | 
					 | 
				
			||||||
            variant,
 | 
					 | 
				
			||||||
            version,
 | 
					 | 
				
			||||||
            memory,
 | 
					 | 
				
			||||||
            time: iterations as u32,
 | 
					 | 
				
			||||||
            lanes: lanes as u32,
 | 
					 | 
				
			||||||
            hash_length: 32,
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn keygen_desc_from_stdin() -> Result<super::KeygenDesc> {
 | 
					 | 
				
			||||||
        let passphrase = read_passphrase()?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        print!("Enter an optional ID []: ");
 | 
					 | 
				
			||||||
        let tag = read_non_empty_line()?.map(|s| Zeroizing::new(s));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        print!("Encrypt keypair with passphrase? [Y/n]: ");
 | 
					 | 
				
			||||||
        let encrypt = read_line()? != "n";
 | 
					 | 
				
			||||||
        if encrypt {
 | 
					 | 
				
			||||||
            println!("Will encrypt keypair.");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        print!("Use hash algorithm (sha256, argon2) [argon2]: ");
 | 
					 | 
				
			||||||
        let hash = match read_non_empty_line()?.as_ref().map(|s| s.as_str()) {
 | 
					 | 
				
			||||||
            Some("argon2") | None => read_argon_desc()?,
 | 
					 | 
				
			||||||
            Some("sha256") => {
 | 
					 | 
				
			||||||
                print!("Iterations [4]: ");
 | 
					 | 
				
			||||||
                let iterations = read_non_empty_line()?
 | 
					 | 
				
			||||||
                    .map(|s| s.parse::<u16>())
 | 
					 | 
				
			||||||
                    .unwrap_or(Ok(4))?;
 | 
					 | 
				
			||||||
                HashDesc::Sha256 {
 | 
					 | 
				
			||||||
                    iterations: iterations as u32,
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            _ => return Err(Error::InvalidHashMethod),
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(KeygenDesc {
 | 
					 | 
				
			||||||
            hash,
 | 
					 | 
				
			||||||
            salt: KeygenDesc::SALT.to_vec(),
 | 
					 | 
				
			||||||
            tag,
 | 
					 | 
				
			||||||
            passphrase,
 | 
					 | 
				
			||||||
            encrypt,
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug, thiserror::Error)]
 | 
					 | 
				
			||||||
pub enum Error {
 | 
					 | 
				
			||||||
    #[error("Argon2 Error: {0}")]
 | 
					 | 
				
			||||||
    Argon2(argon2::Error),
 | 
					 | 
				
			||||||
    #[error(transparent)]
 | 
					 | 
				
			||||||
    SshKeys(#[from] ssh_key::Error),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl From<argon2::Error> for Error {
 | 
					 | 
				
			||||||
    fn from(value: argon2::Error) -> Self {
 | 
					 | 
				
			||||||
        Self::Argon2(value)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub type Result<T> = core::result::Result<T, Error>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct KeyPair {
 | 
					 | 
				
			||||||
    pub passphrase: Option<Zeroizing<String>>,
 | 
					 | 
				
			||||||
    pub inner: ssh_key::private::Ed25519Keypair,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl KeyPair {
 | 
					 | 
				
			||||||
    fn public_key(&self) -> &[u8; 32] {
 | 
					 | 
				
			||||||
        &self.inner.public.0
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    fn private_key(&self) -> [u8; 32] {
 | 
					 | 
				
			||||||
        self.inner.private.to_bytes()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    pub fn fingerprint(&self) -> Vec<u8> {
 | 
					 | 
				
			||||||
        sha2::Sha256::digest(self.public_key()).to_vec()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn fingerprint_base64(&self) -> String {
 | 
					 | 
				
			||||||
        base64::encode(&self.fingerprint())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn randomart(&self) -> randomart::RandomArt {
 | 
					 | 
				
			||||||
        randomart::RandomArt::from_digest(&self.fingerprint())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn encode_keys(&self) -> Result<(Zeroizing<String>, String)> {
 | 
					 | 
				
			||||||
        let keydata = ssh_key::private::KeypairData::Ed25519(self.inner.clone());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let keypair = match &self.passphrase {
 | 
					 | 
				
			||||||
            Some(passphrase) => ssh_key::PrivateKey::new(keydata, "")?
 | 
					 | 
				
			||||||
                .encrypt(&mut rand::rngs::OsRng, passphrase.as_bytes())?,
 | 
					 | 
				
			||||||
            None => ssh_key::PrivateKey::new(keydata, "")?,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let public_key = keypair.public_key();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok((
 | 
					 | 
				
			||||||
            keypair.to_openssh(ssh_key::LineEnding::LF)?,
 | 
					 | 
				
			||||||
            public_key.to_openssh()?,
 | 
					 | 
				
			||||||
        ))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn generate_key(desc: KeygenDesc) -> Result<KeyPair> {
 | 
					 | 
				
			||||||
    let seed = Zeroizing::new(
 | 
					 | 
				
			||||||
        desc.passphrase
 | 
					 | 
				
			||||||
            .chars()
 | 
					 | 
				
			||||||
            .chain(desc.tag.unwrap_or_default().chars())
 | 
					 | 
				
			||||||
            .collect::<String>(),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut hash = [0u8; 32];
 | 
					 | 
				
			||||||
    match desc.hash {
 | 
					 | 
				
			||||||
        HashDesc::Sha256 { iterations } => {
 | 
					 | 
				
			||||||
            let mut hasher = sha2::Sha256::new();
 | 
					 | 
				
			||||||
            hasher.update(seed.as_bytes());
 | 
					 | 
				
			||||||
            hasher.update(desc.salt.as_slice());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let mut digest = hasher.finalize();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for _ in 0..(iterations - 1) {
 | 
					 | 
				
			||||||
                digest = sha2::Sha256::digest(digest.as_slice());
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            hash.copy_from_slice(&digest[..32]);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        HashDesc::Argon {
 | 
					 | 
				
			||||||
            variant,
 | 
					 | 
				
			||||||
            version,
 | 
					 | 
				
			||||||
            memory,
 | 
					 | 
				
			||||||
            time,
 | 
					 | 
				
			||||||
            lanes,
 | 
					 | 
				
			||||||
            hash_length,
 | 
					 | 
				
			||||||
        } => {
 | 
					 | 
				
			||||||
            let argon = argon2::Argon2::new(
 | 
					 | 
				
			||||||
                variant,
 | 
					 | 
				
			||||||
                version,
 | 
					 | 
				
			||||||
                argon2::Params::new(memory, time, lanes, Some(hash_length as usize))?,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            argon.hash_password_into(seed.as_bytes(), &desc.salt, &mut hash)?;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // let hash = rand_chacha::ChaChaRng::from_seed(hash).gen::<[u8; 32]>();
 | 
					 | 
				
			||||||
    let private_key = ed25519_dalek::SigningKey::from_bytes(&hash);
 | 
					 | 
				
			||||||
    let public_key = private_key.verifying_key();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let keypair = ssh_key::private::Ed25519Keypair {
 | 
					 | 
				
			||||||
        public: ssh_key::public::Ed25519PublicKey(public_key.to_bytes()),
 | 
					 | 
				
			||||||
        private: ssh_key::private::Ed25519PrivateKey::from_bytes(private_key.as_bytes()),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return Ok(KeyPair {
 | 
					 | 
				
			||||||
        passphrase: desc.encrypt.then_some(desc.passphrase),
 | 
					 | 
				
			||||||
        inner: keypair,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    // let keypair = russh_keys::key::KeyPair::Ed25519(private_key);
 | 
					 | 
				
			||||||
    // let public_key = keypair.clone_public_key().expect("pubkey");
 | 
					 | 
				
			||||||
    // let fingerprint = public_key.fingerprint();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // let pubbytes = public_key.public_key_bytes();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // let randomart = randomart::RandomArt::from_digest(sha2::Sha256::digest(&pubbytes).as_slice())
 | 
					 | 
				
			||||||
    //     .render("ED25519 256", "SHA256")
 | 
					 | 
				
			||||||
    //     .expect("randomart");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // let mut private_key = Vec::new();
 | 
					 | 
				
			||||||
    // if desc.encrypt {
 | 
					 | 
				
			||||||
    //     russh_keys::encode_pkcs8_pem_encrypted(
 | 
					 | 
				
			||||||
    //         &keypair,
 | 
					 | 
				
			||||||
    //         desc.passphrase.as_bytes(),
 | 
					 | 
				
			||||||
    //         1,
 | 
					 | 
				
			||||||
    //         &mut private_key,
 | 
					 | 
				
			||||||
    //     )
 | 
					 | 
				
			||||||
    //     .expect("writing private key");
 | 
					 | 
				
			||||||
    // } else {
 | 
					 | 
				
			||||||
    //     russh_keys::encode_pkcs8_pem(&keypair, &mut private_key).expect("writing private key");
 | 
					 | 
				
			||||||
    // }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub struct KeygenDesc {
 | 
					 | 
				
			||||||
    pub hash: HashDesc,
 | 
					 | 
				
			||||||
    pub salt: Vec<u8>,
 | 
					 | 
				
			||||||
    pub tag: Option<Zeroizing<String>>,
 | 
					 | 
				
			||||||
    pub passphrase: Zeroizing<String>,
 | 
					 | 
				
			||||||
    pub encrypt: bool,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Default for KeygenDesc {
 | 
					 | 
				
			||||||
    fn default() -> Self {
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            hash: Default::default(),
 | 
					 | 
				
			||||||
            salt: Self::SALT.to_vec(),
 | 
					 | 
				
			||||||
            tag: None,
 | 
					 | 
				
			||||||
            passphrase: Zeroizing::new("password123".to_string()),
 | 
					 | 
				
			||||||
            encrypt: true,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl KeygenDesc {
 | 
					 | 
				
			||||||
    /// known random salt.
 | 
					 | 
				
			||||||
    pub const SALT: &'static [u8; 32] = b"10ru4YBD5wvdQZc2Mw7Brx45jCxGUM3F";
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub enum HashDesc {
 | 
					 | 
				
			||||||
    Sha256 {
 | 
					 | 
				
			||||||
        iterations: u32,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    Argon {
 | 
					 | 
				
			||||||
        variant: argon2::Algorithm,
 | 
					 | 
				
			||||||
        version: argon2::Version,
 | 
					 | 
				
			||||||
        memory: u32,
 | 
					 | 
				
			||||||
        time: u32,
 | 
					 | 
				
			||||||
        lanes: u32,
 | 
					 | 
				
			||||||
        hash_length: u32,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Default for HashDesc {
 | 
					 | 
				
			||||||
    fn default() -> Self {
 | 
					 | 
				
			||||||
        Self::Argon {
 | 
					 | 
				
			||||||
            variant: argon2::Algorithm::Argon2id,
 | 
					 | 
				
			||||||
            version: argon2::Version::V0x13,
 | 
					 | 
				
			||||||
            memory: 65536,
 | 
					 | 
				
			||||||
            time: 4,
 | 
					 | 
				
			||||||
            lanes: 1,
 | 
					 | 
				
			||||||
            hash_length: 32,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[cfg(test)]
 | 
					 | 
				
			||||||
mod tests {
 | 
					 | 
				
			||||||
    use super::{generate_key, KeygenDesc};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_edkey() {
 | 
					 | 
				
			||||||
        generate_key(KeygenDesc::default());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										52
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								src/lib.rs
									
									
									
									
									
								
							| 
						 | 
					@ -1,58 +1,8 @@
 | 
				
			||||||
use itertools::Itertools;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[cfg(feature = "passphrase-gen")]
 | 
					#[cfg(feature = "passphrase-gen")]
 | 
				
			||||||
pub mod passphrase_gen;
 | 
					pub mod passphrase_gen;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[cfg(feature = "password-gen")]
 | 
					#[cfg(feature = "password-gen")]
 | 
				
			||||||
pub mod password_gen;
 | 
					pub mod password_gen;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub mod randomart;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[cfg(feature = "ed25519")]
 | 
					#[cfg(feature = "ed25519")]
 | 
				
			||||||
pub mod key_gen;
 | 
					pub mod ed25519;
 | 
				
			||||||
 | 
					 | 
				
			||||||
fn entropy(pass: &str) -> f32 {
 | 
					 | 
				
			||||||
    let mut r = 0;
 | 
					 | 
				
			||||||
    if pass.chars().any(|c| c.is_ascii_lowercase()) {
 | 
					 | 
				
			||||||
        r += 26;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if pass.chars().any(|c| c.is_ascii_uppercase()) {
 | 
					 | 
				
			||||||
        r += 26;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if pass.chars().any(|c| c.is_ascii_punctuation()) {
 | 
					 | 
				
			||||||
        r += 36;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if pass.chars().any(|c| c.is_ascii_digit()) {
 | 
					 | 
				
			||||||
        r += 10;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    let len = pass.chars().count();
 | 
					 | 
				
			||||||
    let other = pass
 | 
					 | 
				
			||||||
        .chars()
 | 
					 | 
				
			||||||
        .filter(|c| {
 | 
					 | 
				
			||||||
            !(c.is_ascii_lowercase()
 | 
					 | 
				
			||||||
                || c.is_ascii_uppercase()
 | 
					 | 
				
			||||||
                || c.is_ascii_digit()
 | 
					 | 
				
			||||||
                || c.is_ascii_punctuation())
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .dedup()
 | 
					 | 
				
			||||||
        .count();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ((r + other) as f32).log2() * len as f32
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[cfg(test)]
 | 
					 | 
				
			||||||
mod tests {
 | 
					 | 
				
			||||||
    use crate::entropy;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_entropy() {
 | 
					 | 
				
			||||||
        println!(
 | 
					 | 
				
			||||||
            "{}",
 | 
					 | 
				
			||||||
            entropy("this is a long passphrase with quite a few words!")
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        println!(
 | 
					 | 
				
			||||||
            "{}",
 | 
					 | 
				
			||||||
            entropy("This Is A Long Passphrase With Quite A Few Words!")
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										103
									
								
								src/randomart.rs
									
									
									
									
									
								
							
							
						
						
									
										103
									
								
								src/randomart.rs
									
									
									
									
									
								
							| 
						 | 
					@ -1,103 +0,0 @@
 | 
				
			||||||
use std::fmt::Write;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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 }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[cfg(test)]
 | 
					 | 
				
			||||||
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);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
		Loading…
	
		Reference in a new issue