From d059c21b7d7bb8638a3b0d920f81b11749b91da3 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 21 Sep 2023 17:30:48 -0500 Subject: [PATCH] Project refactoring * keyfork-seed has become keyfork-derive-key * Create keyfork-entropy as a way to pull entropy from system * Fix tests in keyfork-derive-util and keyfork-frame * Remove keyfork-mnemonic-generate * Add keyfork-mnemonic-from-seed * Refactor keyfork to only include highest level utilities * Add smex (small hex) --- Cargo.lock | 52 ++++--- Cargo.toml | 10 +- README.md | 15 ++ .../Cargo.toml | 2 +- .../src/cli.rs | 2 +- .../src/client.rs | 4 +- .../src/lib.rs | 4 +- .../src/main.rs | 5 +- .../src/socket.rs | 0 .../src/tests.rs | 8 +- keyfork-derive-util/src/tests.rs | 4 +- keyfork-entropy/Cargo.toml | 9 ++ keyfork-entropy/src/lib.rs | 65 ++++++++ keyfork-entropy/src/main.rs | 21 +++ keyfork-frame/src/lib.rs | 8 +- .../Cargo.lock | 0 .../Cargo.toml | 7 +- .../README.md | 0 keyfork-mnemonic-from-seed/src/lib.rs | 49 ++++++ keyfork-mnemonic-from-seed/src/main.rs | 14 ++ .../src/test/vectors.json | 0 .../src/wordlist.txt | 0 keyfork-mnemonic-generate/src/lib.rs | 140 ------------------ keyfork-mnemonic-generate/src/main.rs | 20 --- keyfork/Cargo.toml | 6 +- keyfork/src/cli/mnemonic.rs | 116 +++++++++++---- keyfork/src/cli/mod.rs | 10 +- smex/Cargo.toml | 8 + smex/src/lib.rs | 52 +++++++ 29 files changed, 394 insertions(+), 237 deletions(-) rename {keyfork-seed => keyfork-derive-key}/Cargo.toml (96%) rename {keyfork-seed => keyfork-derive-key}/src/cli.rs (100%) rename {keyfork-seed => keyfork-derive-key}/src/client.rs (100%) rename {keyfork-seed => keyfork-derive-key}/src/lib.rs (95%) rename {keyfork-seed => keyfork-derive-key}/src/main.rs (61%) rename {keyfork-seed => keyfork-derive-key}/src/socket.rs (100%) rename {keyfork-seed => keyfork-derive-key}/src/tests.rs (96%) create mode 100644 keyfork-entropy/Cargo.toml create mode 100644 keyfork-entropy/src/lib.rs create mode 100644 keyfork-entropy/src/main.rs rename {keyfork-mnemonic-generate => keyfork-mnemonic-from-seed}/Cargo.lock (100%) rename {keyfork-mnemonic-generate => keyfork-mnemonic-from-seed}/Cargo.toml (65%) rename {keyfork-mnemonic-generate => keyfork-mnemonic-from-seed}/README.md (100%) create mode 100644 keyfork-mnemonic-from-seed/src/lib.rs create mode 100644 keyfork-mnemonic-from-seed/src/main.rs rename {keyfork-mnemonic-generate => keyfork-mnemonic-from-seed}/src/test/vectors.json (100%) rename {keyfork-mnemonic-generate => keyfork-mnemonic-from-seed}/src/wordlist.txt (100%) delete mode 100644 keyfork-mnemonic-generate/src/lib.rs delete mode 100644 keyfork-mnemonic-generate/src/main.rs create mode 100644 smex/Cargo.toml create mode 100644 smex/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index fd60b2d..a8d6195 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -515,11 +515,29 @@ name = "keyfork" version = "0.1.0" dependencies = [ "clap", - "keyfork-mnemonic-generate", - "keyfork-seed", + "keyfork-derive-key", + "keyfork-entropy", + "keyfork-mnemonic-from-seed", + "smex", "thiserror", ] +[[package]] +name = "keyfork-derive-key" +version = "0.1.0" +dependencies = [ + "bincode", + "clap", + "ed25519-dalek", + "hex-literal", + "keyfork-derive-util", + "keyfork-frame", + "keyforkd", + "tempdir", + "thiserror", + "tokio", +] + [[package]] name = "keyfork-derive-util" version = "0.1.0" @@ -536,6 +554,13 @@ dependencies = [ "thiserror", ] +[[package]] +name = "keyfork-entropy" +version = "0.1.0" +dependencies = [ + "smex", +] + [[package]] name = "keyfork-frame" version = "0.1.0" @@ -548,10 +573,11 @@ dependencies = [ ] [[package]] -name = "keyfork-mnemonic-generate" +name = "keyfork-mnemonic-from-seed" version = "0.2.0" dependencies = [ "keyfork-mnemonic-util", + "smex", ] [[package]] @@ -564,22 +590,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "keyfork-seed" -version = "0.1.0" -dependencies = [ - "bincode", - "clap", - "ed25519-dalek", - "hex-literal", - "keyfork-derive-util", - "keyfork-frame", - "keyforkd", - "tempdir", - "thiserror", - "tokio", -] - [[package]] name = "keyforkd" version = "0.1.0" @@ -994,6 +1004,10 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +[[package]] +name = "smex" +version = "0.1.0" + [[package]] name = "socket2" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index ab5631c..143194e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,12 @@ resolver = "2" members = [ "keyfork", - "keyfork-mnemonic-generate", - "keyfork-mnemonic-util", "keyfork-derive-util", - "keyfork-seed", + "keyfork-derive-key", + "keyfork-entropy", + "keyfork-frame", + "keyfork-mnemonic-from-seed", + "keyfork-mnemonic-util", "keyforkd", - "keyfork-frame" + "smex", ] diff --git a/README.md b/README.md index b2e5062..e078e06 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,21 @@ This toolchain uses a bip32 seed loaded into an agent to generate deterministic and unique keypairs. This ensures only the agent has control over the mnemonic itself, and other components can simply request deterministic data. +## Dependency Policy + +Dependencies must not be added to core utilities such as seed generation and +path derivation without a _really_ good reason we can't implement it ourselves, +such as cryptography libraries. For instance, `keyfork-derive-util` _only_ +utilizes cryptography libraries, `serde`, and `thiserror`, with the latter two +being audited dependencies. Utilities such as forklets (applications that +use derived data, such as an OpenPGP keychain generator) and the kitchen-sink +`keyfork` utility may pull in additional dependencies _as needed_, but should +strive to use the standard library as much as possible. To avoid code reuse, +additional crates (such as the `smex` crate) may be used to share functionality +across several crates. + +--- + Note: The following document is all proposed, and not yet implemented. ## Features diff --git a/keyfork-seed/Cargo.toml b/keyfork-derive-key/Cargo.toml similarity index 96% rename from keyfork-seed/Cargo.toml rename to keyfork-derive-key/Cargo.toml index 88e67a7..9f2f329 100644 --- a/keyfork-seed/Cargo.toml +++ b/keyfork-derive-key/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "keyfork-seed" +name = "keyfork-derive-key" version = "0.1.0" edition = "2021" diff --git a/keyfork-seed/src/cli.rs b/keyfork-derive-key/src/cli.rs similarity index 100% rename from keyfork-seed/src/cli.rs rename to keyfork-derive-key/src/cli.rs index 4ba6186..b912d19 100644 --- a/keyfork-seed/src/cli.rs +++ b/keyfork-derive-key/src/cli.rs @@ -1,6 +1,6 @@ +use crate::client::Client; use clap::Parser; use keyfork_derive_util::{request::*, DerivationPath}; -use crate::client::Client; #[derive(Parser, Clone, Debug)] pub struct Command { diff --git a/keyfork-seed/src/client.rs b/keyfork-derive-key/src/client.rs similarity index 100% rename from keyfork-seed/src/client.rs rename to keyfork-derive-key/src/client.rs index 1fd151b..96a6298 100644 --- a/keyfork-seed/src/client.rs +++ b/keyfork-derive-key/src/client.rs @@ -1,7 +1,7 @@ -use keyfork_frame::*; use crate::Result; -use std::os::unix::net::UnixStream; use keyfork_derive_util::request::*; +use keyfork_frame::*; +use std::os::unix::net::UnixStream; #[derive(Debug)] pub struct Client { diff --git a/keyfork-seed/src/lib.rs b/keyfork-derive-key/src/lib.rs similarity index 95% rename from keyfork-seed/src/lib.rs rename to keyfork-derive-key/src/lib.rs index 41a9b22..39f9be4 100644 --- a/keyfork-seed/src/lib.rs +++ b/keyfork-derive-key/src/lib.rs @@ -1,9 +1,9 @@ +use keyfork_frame::{DecodeError, EncodeError}; use std::path::PathBuf; -use keyfork_frame::{EncodeError, DecodeError}; pub mod cli; -pub mod socket; pub mod client; +pub mod socket; pub use client::Client; diff --git a/keyfork-seed/src/main.rs b/keyfork-derive-key/src/main.rs similarity index 61% rename from keyfork-seed/src/main.rs rename to keyfork-derive-key/src/main.rs index fd2b723..250c511 100644 --- a/keyfork-seed/src/main.rs +++ b/keyfork-derive-key/src/main.rs @@ -3,11 +3,10 @@ use clap::Parser; #[cfg(test)] mod tests; -use keyfork_seed::*; +use keyfork_derive_key::*; fn main() -> Result<()> { let args = cli::Command::parse(); - let response = args.handle()?; - dbg!(&response); + args.handle()?; Ok(()) } diff --git a/keyfork-seed/src/socket.rs b/keyfork-derive-key/src/socket.rs similarity index 100% rename from keyfork-seed/src/socket.rs rename to keyfork-derive-key/src/socket.rs diff --git a/keyfork-seed/src/tests.rs b/keyfork-derive-key/src/tests.rs similarity index 96% rename from keyfork-seed/src/tests.rs rename to keyfork-derive-key/src/tests.rs index 42d98ad..761656b 100644 --- a/keyfork-seed/src/tests.rs +++ b/keyfork-derive-key/src/tests.rs @@ -1,6 +1,6 @@ use crate::client::Client; use hex_literal::hex; -use keyfork_derive_util::{request::*, DerivationPath, DerivationIndex, ExtendedPrivateKey}; +use keyfork_derive_util::{request::*, DerivationIndex, DerivationPath, ExtendedPrivateKey}; use std::sync::mpsc::channel; use std::{os::unix::net::UnixStream, str::FromStr}; use tempdir::TempDir; @@ -82,10 +82,12 @@ fn misc_multi_requests() { &response.data, response.depth, response.chain_code, - ).unwrap(); + ) + .unwrap(); for i in 0..255 { - key.derive_child(&DerivationIndex::new(i, true).unwrap()).unwrap(); + key.derive_child(&DerivationIndex::new(i, true).unwrap()) + .unwrap(); } handle.abort(); } diff --git a/keyfork-derive-util/src/tests.rs b/keyfork-derive-util/src/tests.rs index 69ea610..813fdd9 100644 --- a/keyfork-derive-util/src/tests.rs +++ b/keyfork-derive-util/src/tests.rs @@ -121,6 +121,8 @@ fn panics_at_depth() { let seed = hex!("000102030405060708090a0b0c0d0e0f"); let mut xkey = ExtendedPrivateKey::::new(seed).unwrap(); for i in 0..u32::from(u8::MAX) + 1 { - xkey = xkey.derive_child(&DerivationIndex::new(i, true).unwrap()).unwrap(); + xkey = xkey + .derive_child(&DerivationIndex::new(i, true).unwrap()) + .unwrap(); } } diff --git a/keyfork-entropy/Cargo.toml b/keyfork-entropy/Cargo.toml new file mode 100644 index 0000000..480bad2 --- /dev/null +++ b/keyfork-entropy/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "keyfork-entropy" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +smex = { version = "0.1.0", path = "../smex" } diff --git a/keyfork-entropy/src/lib.rs b/keyfork-entropy/src/lib.rs new file mode 100644 index 0000000..465001d --- /dev/null +++ b/keyfork-entropy/src/lib.rs @@ -0,0 +1,65 @@ +use std::{ + fs::{read_dir, read_to_string}, + io::Read, +}; + +static WARNING_LINKS: [&str; 1] = + ["https://lore.kernel.org/lkml/20211223141113.1240679-2-Jason@zx2c4.com/"]; + +fn ensure_safe_kernel_version() { + let kernel_version = read_to_string("/proc/version").expect("/proc/version"); + let v = kernel_version + .split(' ') + .nth(2) + .expect("Unable to parse kernel version") + .split('.') + .take(2) + .map(str::parse) + .map(|x| x.expect("Unable to parse kernel version number")) + .collect::>(); + let [major, minor, ..] = v.as_slice() else { + panic!("Unable to determine major and minor: {kernel_version}"); + }; + assert!( + [major, minor] > [&5, &4], + "kernel can't generate clean entropy: {}", + WARNING_LINKS[0] + ); +} + +fn ensure_offline() { + let paths = read_dir("/sys/class/net").expect("Unable to read network interfaces"); + for entry in paths { + let mut path = entry.expect("Unable to read directory entry").path(); + if path + .as_os_str() + .to_str() + .expect("Unable to decode UTF-8 filepath") + .split('/') + .last() + .unwrap() + == "lo" + { + continue; + } + path.push("operstate"); + let isup = read_to_string(&path).expect("Unable to read operstate of network interfaces"); + assert_ne!(isup.trim(), "up", "No network interfaces should be up"); + } +} + +pub fn ensure_safe() { + if !std::env::vars() + .any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED") + { + ensure_safe_kernel_version(); + ensure_offline(); + } +} + +pub fn generate_entropy_of_size(byte_count: usize) -> Result, std::io::Error> { + let mut vec = vec![0u8; byte_count]; + let mut entropy_file = std::fs::File::open("/dev/urandom")?; + entropy_file.read_exact(&mut vec[..])?; + Ok(vec) +} diff --git a/keyfork-entropy/src/main.rs b/keyfork-entropy/src/main.rs new file mode 100644 index 0000000..61075b0 --- /dev/null +++ b/keyfork-entropy/src/main.rs @@ -0,0 +1,21 @@ +fn main() -> Result<(), Box> { + let bit_size: usize = std::env::args() + .nth(1) + .unwrap_or(String::from("256")) + .parse() + .expect("Expected integer bit size"); + assert!( + bit_size % 8 == 0, + "Bit size must be divisible by 8, got: {bit_size}" + ); + assert!( + bit_size <= 256, + "Maximum supported bit size is 256, got: {bit_size}" + ); + + keyfork_entropy::ensure_safe(); + let entropy = keyfork_entropy::generate_entropy_of_size(bit_size / 8)?; + println!("{}", smex::encode(&entropy)); + + Ok(()) +} diff --git a/keyfork-frame/src/lib.rs b/keyfork-frame/src/lib.rs index 0f4d58a..3d18bc6 100644 --- a/keyfork-frame/src/lib.rs +++ b/keyfork-frame/src/lib.rs @@ -121,6 +121,8 @@ pub fn try_decode_from(readable: &mut impl Read) -> Result, DecodeError> mod tests { use super::{try_decode, try_encode, DecodeError}; + const LEN_SIZE: usize = (u32::BITS / 8) as usize; + #[test] fn stable_interface() { let data = (0..255).collect::>(); @@ -153,7 +155,7 @@ mod tests { assert!(error.is_err()); // Data includes length and checksum - let error = try_decode(&encoded[..super::LEN_SIZE + 256 / 8]); + let error = try_decode(&encoded[..LEN_SIZE + 256 / 8]); assert!(error.is_err()); // Data only includes length @@ -166,8 +168,8 @@ mod tests { fn error_on_invalid_checksum() { let data = (0..255).collect::>(); let mut encoded = try_encode(&data[..]).unwrap(); - assert_ne!(encoded[super::LEN_SIZE + 1], 0); - encoded[super::LEN_SIZE + 1] = 0; + assert_ne!(encoded[LEN_SIZE + 1], 0); + encoded[LEN_SIZE + 1] = 0; let error = try_decode(&data[..]); assert!(error.is_err()); diff --git a/keyfork-mnemonic-generate/Cargo.lock b/keyfork-mnemonic-from-seed/Cargo.lock similarity index 100% rename from keyfork-mnemonic-generate/Cargo.lock rename to keyfork-mnemonic-from-seed/Cargo.lock diff --git a/keyfork-mnemonic-generate/Cargo.toml b/keyfork-mnemonic-from-seed/Cargo.toml similarity index 65% rename from keyfork-mnemonic-generate/Cargo.toml rename to keyfork-mnemonic-from-seed/Cargo.toml index ab628f2..839ca3d 100644 --- a/keyfork-mnemonic-generate/Cargo.toml +++ b/keyfork-mnemonic-from-seed/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "keyfork-mnemonic-generate" -version = "0.2.0" -description = "A tool to generate BIP-0039 mnemonics." +name = "keyfork-mnemonic-from-seed" +version = "0.1.0" +description = "A tool to format BIP-0039 mnemonics from hex data." license = "GPL-3.0" repository = "https://git.distrust.co/public/keyfork" edition = "2021" @@ -10,3 +10,4 @@ edition = "2021" [dependencies] keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util", registry = "distrust" } +smex = { version = "0.1.0", path = "../smex" } diff --git a/keyfork-mnemonic-generate/README.md b/keyfork-mnemonic-from-seed/README.md similarity index 100% rename from keyfork-mnemonic-generate/README.md rename to keyfork-mnemonic-from-seed/README.md diff --git a/keyfork-mnemonic-from-seed/src/lib.rs b/keyfork-mnemonic-from-seed/src/lib.rs new file mode 100644 index 0000000..d6654ba --- /dev/null +++ b/keyfork-mnemonic-from-seed/src/lib.rs @@ -0,0 +1,49 @@ +use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError, Wordlist}; + +pub fn generate_mnemonic(entropy: &[u8]) -> Result { + let wordlist = Wordlist::default().arc(); + Mnemonic::from_entropy(entropy, wordlist) +} + +#[cfg(test)] +mod tests { + use keyfork_mnemonic_util::{Mnemonic, Wordlist}; + use std::{collections::HashSet, io::Read}; + + #[test] + fn count_to_get_duplicate_words() { + let tests = 100_000; + let mut count = 0.; + let entropy = &mut [0u8; 256 / 8]; + let wordlist = Wordlist::default().arc(); + let mut random = std::fs::File::open("/dev/urandom").unwrap(); + let mut hs = HashSet::::with_capacity(24); + + for _ in 0..tests { + random.read_exact(&mut entropy[..]).unwrap(); + let mnemonic = Mnemonic::from_entropy(&entropy[..256 / 8], wordlist.clone()).unwrap(); + let (words, _) = mnemonic.into_inner(); + hs.clear(); + hs.extend(words); + if hs.len() != 24 { + count += 1.; + } + } + + // NOTE: Birthday problem math is: 0.126532 + // Set values to (about) 1 below, 1 above + // Source: https://en.wikipedia.org/wiki/Birthday_problem + let min = 11.5; + let max = 13.5; + assert!( + count > f64::from(tests) * min / 100., + "{count} probability should be more than {min}%: {}", + count / f64::from(tests) + ); + assert!( + count < f64::from(tests) * max / 100., + "{count} probability should be more than {max}%: {}", + count / f64::from(tests) + ); + } +} diff --git a/keyfork-mnemonic-from-seed/src/main.rs b/keyfork-mnemonic-from-seed/src/main.rs new file mode 100644 index 0000000..8d08e59 --- /dev/null +++ b/keyfork-mnemonic-from-seed/src/main.rs @@ -0,0 +1,14 @@ +use keyfork_mnemonic_from_seed::*; + +fn main() -> Result<(), Box> { + let input = std::io::stdin(); + let mut line = String::new(); + input.read_line(&mut line)?; + let decoded = smex::decode(line.trim())?; + + let mnemonic = generate_mnemonic(&decoded)?; + + println!("{mnemonic}"); + + Ok(()) +} diff --git a/keyfork-mnemonic-generate/src/test/vectors.json b/keyfork-mnemonic-from-seed/src/test/vectors.json similarity index 100% rename from keyfork-mnemonic-generate/src/test/vectors.json rename to keyfork-mnemonic-from-seed/src/test/vectors.json diff --git a/keyfork-mnemonic-generate/src/wordlist.txt b/keyfork-mnemonic-from-seed/src/wordlist.txt similarity index 100% rename from keyfork-mnemonic-generate/src/wordlist.txt rename to keyfork-mnemonic-from-seed/src/wordlist.txt diff --git a/keyfork-mnemonic-generate/src/lib.rs b/keyfork-mnemonic-generate/src/lib.rs deleted file mode 100644 index f766602..0000000 --- a/keyfork-mnemonic-generate/src/lib.rs +++ /dev/null @@ -1,140 +0,0 @@ -use std::{ - env::vars, - error::Error, - fs::{read_dir, read_to_string, File}, - io::Read, -}; - -use keyfork_mnemonic_util::{Mnemonic, Wordlist}; - -pub type Result> = std::result::Result; - -/// Usage: keyfork-mnemonic-generate [bitsize: 128 or 256] -/// CHECKS: -/// * If the system is online -/// * If a kernel is running post-BLAKE2 -/// -/// TODO: -/// * --features kitchen-sink: load username; system time's most random, precise bits; hostname; -/// kernel version; other env specific shit into a CSPRNG - -pub struct Entropy(File); - -/// An entropy source -impl Entropy { - pub fn new() -> Result { - let file = File::open("/dev/random")?; - Ok(Self(file)) - } - - pub fn read_into(&mut self, bytes: &mut [u8]) -> Result<()> { - self.0.read_exact(bytes)?; - Ok(()) - } -} - -static WARNING_LINKS: [&str; 1] = - ["https://lore.kernel.org/lkml/20211223141113.1240679-2-Jason@zx2c4.com/"]; - -fn ensure_safe_kernel_version() { - let kernel_version = read_to_string("/proc/version").expect("/proc/version"); - let v = kernel_version - .split(' ') - .nth(2) - .expect("Unable to parse kernel version") - .split('.') - .take(2) - .map(str::parse) - .map(|x| x.expect("Unable to parse kernel version number")) - .collect::>(); - let [major, minor, ..] = v.as_slice() else { - panic!("Unable to determine major and minor: {kernel_version}"); - }; - assert!( - [major, minor] > [&5, &4], - "kernel can't generate clean entropy: {}", - WARNING_LINKS[0] - ); -} - -fn ensure_offline() { - let paths = read_dir("/sys/class/net").expect("Unable to read network interfaces"); - for entry in paths { - let mut path = entry.expect("Unable to read directory entry").path(); - if path - .as_os_str() - .to_str() - .expect("Unable to decode UTF-8 filepath") - .split('/') - .last() - .unwrap() - == "lo" - { - continue; - } - path.push("operstate"); - let isup = read_to_string(&path).expect("Unable to read operstate of network interfaces"); - assert_ne!(isup.trim(), "up", "No network interfaces should be up"); - } -} - -pub fn generate_mnemonic(bit_size: usize) -> Result { - if !vars() - .any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED") - { - ensure_safe_kernel_version(); - ensure_offline(); - } - - let mut rng = Entropy::new()?; - let entropy = &mut [0u8; 256 / 8]; - rng.read_into(&mut entropy[..])?; - - let wordlist = Wordlist::default().arc(); - Mnemonic::from_entropy(&entropy[..bit_size / 8], wordlist).map_err(From::from) -} - -#[cfg(test)] -mod tests { - use keyfork_mnemonic_util::{Mnemonic, Wordlist}; - use std::collections::HashSet; - - use super::*; - - #[test] - fn count_to_get_duplicate_words() { - let tests = 100_000; - let mut count = 0.; - let entropy = &mut [0u8; 256 / 8]; - let wordlist = Wordlist::default().arc(); - let mut rng = Entropy::new().unwrap(); - let mut hs = HashSet::::with_capacity(24); - - for _ in 0..tests { - rng.read_into(&mut entropy[..]).unwrap(); - let mnemonic = Mnemonic::from_entropy(&entropy[..256 / 8], wordlist.clone()).unwrap(); - let (words, _) = mnemonic.into_inner(); - hs.clear(); - hs.extend(words); - if hs.len() != 24 { - count += 1.; - } - } - - // NOTE: Birthday problem math is: 0.126532 - // Set values to (about) 1 below, 1 above - // Source: https://en.wikipedia.org/wiki/Birthday_problem - let min = 11.5; - let max = 13.5; - assert!( - count > f64::from(tests) * min / 100., - "{count} probability should be more than {min}%: {}", - count / f64::from(tests) - ); - assert!( - count < f64::from(tests) * max / 100., - "{count} probability should be more than {max}%: {}", - count / f64::from(tests) - ); - } -} diff --git a/keyfork-mnemonic-generate/src/main.rs b/keyfork-mnemonic-generate/src/main.rs deleted file mode 100644 index ade3f68..0000000 --- a/keyfork-mnemonic-generate/src/main.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::env::args; -use keyfork_mnemonic_generate::*; - -fn main() -> Result<()> { - let bit_size: usize = args() - .nth(1) - .unwrap_or(String::from("256")) - .parse() - .expect("Expected integer bit size"); - assert!( - bit_size == 128 || bit_size == 256, - "Only 12 or 24 word mnemonics are supported" - ); - - let mnemonic = generate_mnemonic(bit_size)?; - - println!("{mnemonic}"); - - Ok(()) -} diff --git a/keyfork/Cargo.toml b/keyfork/Cargo.toml index 1e722f3..0cd934a 100644 --- a/keyfork/Cargo.toml +++ b/keyfork/Cargo.toml @@ -7,6 +7,8 @@ edition = "2021" [dependencies] clap = { version = "4.4.2", features = ["derive", "env"] } -keyfork-mnemonic-generate = { version = "0.2.0", path = "../keyfork-mnemonic-generate" } -keyfork-seed = { version = "0.1.0", path = "../keyfork-seed" } +keyfork-mnemonic-from-seed = { version = "0.2.0", path = "../keyfork-mnemonic-from-seed" } +keyfork-derive-key = { version = "0.1.0", path = "../keyfork-derive-key" } thiserror = "1.0.48" +smex = { version = "0.1.0", path = "../smex" } +keyfork-entropy = { version = "0.1.0", path = "../keyfork-entropy" } diff --git a/keyfork/src/cli/mnemonic.rs b/keyfork/src/cli/mnemonic.rs index a046d18..55d4490 100644 --- a/keyfork/src/cli/mnemonic.rs +++ b/keyfork/src/cli/mnemonic.rs @@ -1,46 +1,117 @@ -use super::{Keyfork, KeyforkCommands}; -use clap::{Parser, Subcommand}; -use keyfork_mnemonic_generate::generate_mnemonic; +use super::Keyfork; +use clap::{Parser, Subcommand, ValueEnum}; +use std::fmt::Display; -#[derive(Clone, Debug)] -pub enum EntropySize { +#[derive(Clone, Debug, Default)] +pub enum SeedSize { Bits128, + + #[default] Bits256, } #[derive(thiserror::Error, Debug, Clone)] -pub enum EntropySizeError { +pub enum SeedSizeError { #[error("Expected one of 128, 256")] InvalidChoice, } -impl std::str::FromStr for EntropySize { - type Err = EntropySizeError; +impl Display for SeedSize { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SeedSize::Bits128 => write!(f, "128"), + SeedSize::Bits256 => write!(f, "256"), + } + } +} + +impl std::str::FromStr for SeedSize { + type Err = SeedSizeError; fn from_str(s: &str) -> Result { Ok(match s { - "128" => EntropySize::Bits128, - "256" => EntropySize::Bits256, - _ => return Err(EntropySizeError::InvalidChoice), + "128" => SeedSize::Bits128, + "256" => SeedSize::Bits256, + _ => return Err(SeedSizeError::InvalidChoice), }) } } -impl From<&EntropySize> for usize { - fn from(value: &EntropySize) -> Self { +impl From<&SeedSize> for usize { + fn from(value: &SeedSize) -> Self { match value { - EntropySize::Bits128 => 128, - EntropySize::Bits256 => 256, + SeedSize::Bits128 => 128, + SeedSize::Bits256 => 256, } } } +#[derive(Clone, Debug, thiserror::Error)] +pub enum MnemonicSeedSourceParseError { + #[error("Expected one of system, playing, tarot, dice")] + InvalidChoice, +} + +#[derive(Clone, Debug, Default, ValueEnum)] +pub enum MnemonicSeedSource { + /// System entropy + #[default] + System, + + /// Playing cards + Playing, + + /// Tarot cards + Tarot, + + /// Dice + Dice, +} + +impl std::str::FromStr for MnemonicSeedSource { + type Err = MnemonicSeedSourceParseError; + + fn from_str(s: &str) -> Result { + Ok(match s { + "system" => Self::System, + "playing" => Self::Playing, + "tarot" => Self::Tarot, + "dice" => Self::Dice, + _ => return Err(Self::Err::InvalidChoice), + }) + } +} + +impl MnemonicSeedSource { + pub fn handle(&self, size: &SeedSize) -> Result> { + let size = match size { + SeedSize::Bits128 => 128, + SeedSize::Bits256 => 256, + }; + let seed = match self { + MnemonicSeedSource::System => { + keyfork_entropy::ensure_safe(); + keyfork_entropy::generate_entropy_of_size(size / 8)? + } + MnemonicSeedSource::Playing => todo!(), + MnemonicSeedSource::Tarot => todo!(), + MnemonicSeedSource::Dice => todo!(), + }; + let mnemonic = keyfork_mnemonic_from_seed::generate_mnemonic(&seed)?; + Ok(mnemonic.to_string()) + } +} #[derive(Subcommand, Clone, Debug)] pub enum MnemonicSubcommands { - /// Generate a mnemonic using OS entropy. + /// Generate a mnemonic using a given entropy source. Generate { - /// The size in bits to generate entropy for. - entropy: EntropySize, + /// The source from where a seed is created. + #[arg(long, value_enum, default_value_t = Default::default())] + source: MnemonicSeedSource, + + /// The size of the mnemonic, in bits. + #[arg(long, default_value_t = Default::default())] + size: SeedSize, }, } @@ -48,15 +119,10 @@ impl MnemonicSubcommands { pub fn handle( &self, _m: &Mnemonic, - _p: &KeyforkCommands, _keyfork: &Keyfork, - ) -> Result<(), Box> { + ) -> Result> { match self { - MnemonicSubcommands::Generate { entropy } => { - let mnemonic = generate_mnemonic(usize::from(entropy))?; - println!("{mnemonic}"); - Ok(()) - } + MnemonicSubcommands::Generate { source, size } => source.handle(size), } } } diff --git a/keyfork/src/cli/mod.rs b/keyfork/src/cli/mod.rs index 4502922..bdbe8a5 100644 --- a/keyfork/src/cli/mod.rs +++ b/keyfork/src/cli/mod.rs @@ -15,9 +15,6 @@ pub struct Keyfork { pub enum KeyforkCommands { /// Mnemonic generation and persistence utilities. Mnemonic(mnemonic::Mnemonic), - - /// Seeded data generation utility. - Seed(keyfork_seed::cli::Command), /// Keyforkd background daemon to manage seed creation. Daemon, @@ -27,11 +24,8 @@ impl KeyforkCommands { pub fn handle(&self, keyfork: &Keyfork) -> Result<(), Box> { match self { KeyforkCommands::Mnemonic(m) => { - m.command.handle(m, self, keyfork)?; - } - KeyforkCommands::Seed(s) => { - let response = s.handle()?; - println!("{response:?}"); + let response = m.command.handle(m, keyfork)?; + println!("{response}"); } KeyforkCommands::Daemon => { todo!() diff --git a/smex/Cargo.toml b/smex/Cargo.toml new file mode 100644 index 0000000..bcd7903 --- /dev/null +++ b/smex/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "smex" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/smex/src/lib.rs b/smex/src/lib.rs new file mode 100644 index 0000000..b36d8e6 --- /dev/null +++ b/smex/src/lib.rs @@ -0,0 +1,52 @@ +use std::fmt::Write; + +#[derive(Debug)] +pub enum DecodeError { + InvalidCharacter(u8), + InvalidCharacterCount(usize), +} + +impl std::fmt::Display for DecodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidCharacter(c) => { + write!(f, "Invalid character: {c} not in [0123456789ABCDEF]") + } + Self::InvalidCharacterCount(n) => { + write!(f, "Invalid character count: {n} % 2 != 0") + } + } + } +} + +impl std::error::Error for DecodeError {} + +pub fn encode(input: &[u8]) -> String { + let mut s = String::new(); + for byte in input { + write!(s, "{byte:02x}").unwrap(); + } + s +} + +fn val(c: u8) -> Result { + match c { + b'A'..=b'F' => Ok(c - b'A' + 10), + b'a'..=b'f' => Ok(c - b'a' + 10), + b'0'..=b'9' => Ok(c - b'0'), + _ => Err(DecodeError::InvalidCharacter(c)), + } +} + +pub fn decode(input: &str) -> Result, DecodeError> { + let len = input.len(); + if len % 2 != 0 { + return Err(DecodeError::InvalidCharacterCount(len)); + } + + input + .as_bytes() + .chunks_exact(2) + .map(|pair| Ok(val(pair[0])? << 4 | val(pair[1])?)) + .collect() +}