added duralumin-keygen

This commit is contained in:
NoOneBtw 2021-11-17 13:42:09 +01:00
parent eea64e7ef6
commit 6259840c43
6 changed files with 538 additions and 159 deletions

View file

@ -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"
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}

View file

@ -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 <noonebtw@nirgendwo.xyz>"
)]
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(())
}

View file

@ -1,5 +1,5 @@
use clap::Parser;
use libduralumin::{PassPhraseGenerator, Words};
use libduralumin::passphrase_gen::{PassPhraseGenerator, Words};
/// program that generates random passphrases.
#[derive(Parser)]

284
src/ed25519.rs Normal file
View file

@ -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<S>(text: S) -> Option<()>
where
S: AsRef<str>,
{
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<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;
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<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 }
}
}
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 = "Id just like to interject for a moment. What youre refering to as Linux, is in fact, GNU/Linux, or as Ive 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);
}
}

View file

@ -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<String>,
}
impl Words {
pub fn new() -> Self {
Self { words: Vec::new() }
}
pub fn from_text_file(file: File) -> std::io::Result<Self> {
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<i16>,
}
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<i16>) -> Self {
self.min_word_length = length;
self
}
pub fn generate_n(
&self,
words: &Words,
count: usize,
) -> Option<Vec<String>> {
(0..count).map(|_| self.generate(words)).collect()
}
pub fn generate(&self, words: &Words) -> Option<String> {
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::<String>()
+ chars.as_str()
}
}
} else {
word.to_string()
}
})
.collect::<Vec<_>>()
.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;

158
src/passphrase_gen.rs Normal file
View file

@ -0,0 +1,158 @@
use std::{
fs::File,
io::{BufReader, Read},
};
use rand::{prelude::*, rngs::OsRng};
pub struct Words {
words: Vec<String>,
}
impl Words {
pub fn new() -> Self {
Self { words: Vec::new() }
}
pub fn from_text_file(file: File) -> std::io::Result<Self> {
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<i16>,
}
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<i16>) -> Self {
self.min_word_length = length;
self
}
pub fn generate_n(
&self,
words: &Words,
count: usize,
) -> Option<Vec<String>> {
(0..count).map(|_| self.generate(words)).collect()
}
pub fn generate(&self, words: &Words) -> Option<String> {
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::<String>()
+ chars.as_str()
}
}
} else {
word.to_string()
}
})
.collect::<Vec<_>>()
.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()));
}
}