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)
This commit is contained in:
parent
7e8702a150
commit
d059c21b7d
|
@ -515,11 +515,29 @@ name = "keyfork"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"keyfork-mnemonic-generate",
|
"keyfork-derive-key",
|
||||||
"keyfork-seed",
|
"keyfork-entropy",
|
||||||
|
"keyfork-mnemonic-from-seed",
|
||||||
|
"smex",
|
||||||
"thiserror",
|
"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]]
|
[[package]]
|
||||||
name = "keyfork-derive-util"
|
name = "keyfork-derive-util"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -536,6 +554,13 @@ dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "keyfork-entropy"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"smex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-frame"
|
name = "keyfork-frame"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -548,10 +573,11 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-mnemonic-generate"
|
name = "keyfork-mnemonic-from-seed"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"keyfork-mnemonic-util",
|
"keyfork-mnemonic-util",
|
||||||
|
"smex",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -564,22 +590,6 @@ dependencies = [
|
||||||
"sha2",
|
"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]]
|
[[package]]
|
||||||
name = "keyforkd"
|
name = "keyforkd"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -994,6 +1004,10 @@ version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
|
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smex"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|
10
Cargo.toml
10
Cargo.toml
|
@ -3,10 +3,12 @@
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"keyfork",
|
"keyfork",
|
||||||
"keyfork-mnemonic-generate",
|
|
||||||
"keyfork-mnemonic-util",
|
|
||||||
"keyfork-derive-util",
|
"keyfork-derive-util",
|
||||||
"keyfork-seed",
|
"keyfork-derive-key",
|
||||||
|
"keyfork-entropy",
|
||||||
|
"keyfork-frame",
|
||||||
|
"keyfork-mnemonic-from-seed",
|
||||||
|
"keyfork-mnemonic-util",
|
||||||
"keyforkd",
|
"keyforkd",
|
||||||
"keyfork-frame"
|
"smex",
|
||||||
]
|
]
|
||||||
|
|
15
README.md
15
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
|
and unique keypairs. This ensures only the agent has control over the mnemonic
|
||||||
itself, and other components can simply request deterministic data.
|
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.
|
Note: The following document is all proposed, and not yet implemented.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-seed"
|
name = "keyfork-derive-key"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
use crate::client::Client;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use keyfork_derive_util::{request::*, DerivationPath};
|
use keyfork_derive_util::{request::*, DerivationPath};
|
||||||
use crate::client::Client;
|
|
||||||
|
|
||||||
#[derive(Parser, Clone, Debug)]
|
#[derive(Parser, Clone, Debug)]
|
||||||
pub struct Command {
|
pub struct Command {
|
|
@ -1,7 +1,7 @@
|
||||||
use keyfork_frame::*;
|
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use std::os::unix::net::UnixStream;
|
|
||||||
use keyfork_derive_util::request::*;
|
use keyfork_derive_util::request::*;
|
||||||
|
use keyfork_frame::*;
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
|
@ -1,9 +1,9 @@
|
||||||
|
use keyfork_frame::{DecodeError, EncodeError};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use keyfork_frame::{EncodeError, DecodeError};
|
|
||||||
|
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod socket;
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
pub mod socket;
|
||||||
|
|
||||||
pub use client::Client;
|
pub use client::Client;
|
||||||
|
|
|
@ -3,11 +3,10 @@ use clap::Parser;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use keyfork_seed::*;
|
use keyfork_derive_key::*;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let args = cli::Command::parse();
|
let args = cli::Command::parse();
|
||||||
let response = args.handle()?;
|
args.handle()?;
|
||||||
dbg!(&response);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::client::Client;
|
use crate::client::Client;
|
||||||
use hex_literal::hex;
|
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::sync::mpsc::channel;
|
||||||
use std::{os::unix::net::UnixStream, str::FromStr};
|
use std::{os::unix::net::UnixStream, str::FromStr};
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
|
@ -82,10 +82,12 @@ fn misc_multi_requests() {
|
||||||
&response.data,
|
&response.data,
|
||||||
response.depth,
|
response.depth,
|
||||||
response.chain_code,
|
response.chain_code,
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
for i in 0..255 {
|
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();
|
handle.abort();
|
||||||
}
|
}
|
|
@ -121,6 +121,8 @@ fn panics_at_depth() {
|
||||||
let seed = hex!("000102030405060708090a0b0c0d0e0f");
|
let seed = hex!("000102030405060708090a0b0c0d0e0f");
|
||||||
let mut xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap();
|
let mut xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap();
|
||||||
for i in 0..u32::from(u8::MAX) + 1 {
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" }
|
|
@ -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::<Vec<u32>>();
|
||||||
|
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<Vec<u8>, 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)
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
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(())
|
||||||
|
}
|
|
@ -121,6 +121,8 @@ pub fn try_decode_from(readable: &mut impl Read) -> Result<Vec<u8>, DecodeError>
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{try_decode, try_encode, DecodeError};
|
use super::{try_decode, try_encode, DecodeError};
|
||||||
|
|
||||||
|
const LEN_SIZE: usize = (u32::BITS / 8) as usize;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stable_interface() {
|
fn stable_interface() {
|
||||||
let data = (0..255).collect::<Vec<u8>>();
|
let data = (0..255).collect::<Vec<u8>>();
|
||||||
|
@ -153,7 +155,7 @@ mod tests {
|
||||||
assert!(error.is_err());
|
assert!(error.is_err());
|
||||||
|
|
||||||
// Data includes length and checksum
|
// 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());
|
assert!(error.is_err());
|
||||||
|
|
||||||
// Data only includes length
|
// Data only includes length
|
||||||
|
@ -166,8 +168,8 @@ mod tests {
|
||||||
fn error_on_invalid_checksum() {
|
fn error_on_invalid_checksum() {
|
||||||
let data = (0..255).collect::<Vec<u8>>();
|
let data = (0..255).collect::<Vec<u8>>();
|
||||||
let mut encoded = try_encode(&data[..]).unwrap();
|
let mut encoded = try_encode(&data[..]).unwrap();
|
||||||
assert_ne!(encoded[super::LEN_SIZE + 1], 0);
|
assert_ne!(encoded[LEN_SIZE + 1], 0);
|
||||||
encoded[super::LEN_SIZE + 1] = 0;
|
encoded[LEN_SIZE + 1] = 0;
|
||||||
|
|
||||||
let error = try_decode(&data[..]);
|
let error = try_decode(&data[..]);
|
||||||
assert!(error.is_err());
|
assert!(error.is_err());
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-mnemonic-generate"
|
name = "keyfork-mnemonic-from-seed"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
description = "A tool to generate BIP-0039 mnemonics."
|
description = "A tool to format BIP-0039 mnemonics from hex data."
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
repository = "https://git.distrust.co/public/keyfork"
|
repository = "https://git.distrust.co/public/keyfork"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
@ -10,3 +10,4 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util", registry = "distrust" }
|
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util", registry = "distrust" }
|
||||||
|
smex = { version = "0.1.0", path = "../smex" }
|
|
@ -0,0 +1,49 @@
|
||||||
|
use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError, Wordlist};
|
||||||
|
|
||||||
|
pub fn generate_mnemonic(entropy: &[u8]) -> Result<Mnemonic, MnemonicGenerationError> {
|
||||||
|
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::<usize>::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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
use keyfork_mnemonic_from_seed::*;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
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(())
|
||||||
|
}
|
|
@ -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<T, E = Box<dyn Error>> = std::result::Result<T, E>;
|
|
||||||
|
|
||||||
/// 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<Self> {
|
|
||||||
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::<Vec<u32>>();
|
|
||||||
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<Mnemonic> {
|
|
||||||
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::<usize>::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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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(())
|
|
||||||
}
|
|
|
@ -7,6 +7,8 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.4.2", features = ["derive", "env"] }
|
clap = { version = "4.4.2", features = ["derive", "env"] }
|
||||||
keyfork-mnemonic-generate = { version = "0.2.0", path = "../keyfork-mnemonic-generate" }
|
keyfork-mnemonic-from-seed = { version = "0.2.0", path = "../keyfork-mnemonic-from-seed" }
|
||||||
keyfork-seed = { version = "0.1.0", path = "../keyfork-seed" }
|
keyfork-derive-key = { version = "0.1.0", path = "../keyfork-derive-key" }
|
||||||
thiserror = "1.0.48"
|
thiserror = "1.0.48"
|
||||||
|
smex = { version = "0.1.0", path = "../smex" }
|
||||||
|
keyfork-entropy = { version = "0.1.0", path = "../keyfork-entropy" }
|
||||||
|
|
|
@ -1,46 +1,117 @@
|
||||||
use super::{Keyfork, KeyforkCommands};
|
use super::Keyfork;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
use keyfork_mnemonic_generate::generate_mnemonic;
|
use std::fmt::Display;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub enum EntropySize {
|
pub enum SeedSize {
|
||||||
Bits128,
|
Bits128,
|
||||||
|
|
||||||
|
#[default]
|
||||||
Bits256,
|
Bits256,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug, Clone)]
|
#[derive(thiserror::Error, Debug, Clone)]
|
||||||
pub enum EntropySizeError {
|
pub enum SeedSizeError {
|
||||||
#[error("Expected one of 128, 256")]
|
#[error("Expected one of 128, 256")]
|
||||||
InvalidChoice,
|
InvalidChoice,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::str::FromStr for EntropySize {
|
impl Display for SeedSize {
|
||||||
type Err = EntropySizeError;
|
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<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
Ok(match s {
|
Ok(match s {
|
||||||
"128" => EntropySize::Bits128,
|
"128" => SeedSize::Bits128,
|
||||||
"256" => EntropySize::Bits256,
|
"256" => SeedSize::Bits256,
|
||||||
_ => return Err(EntropySizeError::InvalidChoice),
|
_ => return Err(SeedSizeError::InvalidChoice),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&EntropySize> for usize {
|
impl From<&SeedSize> for usize {
|
||||||
fn from(value: &EntropySize) -> Self {
|
fn from(value: &SeedSize) -> Self {
|
||||||
match value {
|
match value {
|
||||||
EntropySize::Bits128 => 128,
|
SeedSize::Bits128 => 128,
|
||||||
EntropySize::Bits256 => 256,
|
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<Self, Self::Err> {
|
||||||
|
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<String, Box<dyn std::error::Error>> {
|
||||||
|
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)]
|
#[derive(Subcommand, Clone, Debug)]
|
||||||
pub enum MnemonicSubcommands {
|
pub enum MnemonicSubcommands {
|
||||||
/// Generate a mnemonic using OS entropy.
|
/// Generate a mnemonic using a given entropy source.
|
||||||
Generate {
|
Generate {
|
||||||
/// The size in bits to generate entropy for.
|
/// The source from where a seed is created.
|
||||||
entropy: EntropySize,
|
#[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(
|
pub fn handle(
|
||||||
&self,
|
&self,
|
||||||
_m: &Mnemonic,
|
_m: &Mnemonic,
|
||||||
_p: &KeyforkCommands,
|
|
||||||
_keyfork: &Keyfork,
|
_keyfork: &Keyfork,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
match self {
|
match self {
|
||||||
MnemonicSubcommands::Generate { entropy } => {
|
MnemonicSubcommands::Generate { source, size } => source.handle(size),
|
||||||
let mnemonic = generate_mnemonic(usize::from(entropy))?;
|
|
||||||
println!("{mnemonic}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,6 @@ pub struct Keyfork {
|
||||||
pub enum KeyforkCommands {
|
pub enum KeyforkCommands {
|
||||||
/// Mnemonic generation and persistence utilities.
|
/// Mnemonic generation and persistence utilities.
|
||||||
Mnemonic(mnemonic::Mnemonic),
|
Mnemonic(mnemonic::Mnemonic),
|
||||||
|
|
||||||
/// Seeded data generation utility.
|
|
||||||
Seed(keyfork_seed::cli::Command),
|
|
||||||
|
|
||||||
/// Keyforkd background daemon to manage seed creation.
|
/// Keyforkd background daemon to manage seed creation.
|
||||||
Daemon,
|
Daemon,
|
||||||
|
@ -27,11 +24,8 @@ impl KeyforkCommands {
|
||||||
pub fn handle(&self, keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn handle(&self, keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
match self {
|
match self {
|
||||||
KeyforkCommands::Mnemonic(m) => {
|
KeyforkCommands::Mnemonic(m) => {
|
||||||
m.command.handle(m, self, keyfork)?;
|
let response = m.command.handle(m, keyfork)?;
|
||||||
}
|
println!("{response}");
|
||||||
KeyforkCommands::Seed(s) => {
|
|
||||||
let response = s.handle()?;
|
|
||||||
println!("{response:?}");
|
|
||||||
}
|
}
|
||||||
KeyforkCommands::Daemon => {
|
KeyforkCommands::Daemon => {
|
||||||
todo!()
|
todo!()
|
||||||
|
|
|
@ -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]
|
|
@ -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<u8, DecodeError> {
|
||||||
|
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<Vec<u8>, 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()
|
||||||
|
}
|
Loading…
Reference in New Issue