keyfork/keyfork-mnemonic-generate/src/main.rs

145 lines
4.2 KiB
Rust

use std::{
env,
error::Error,
fs::{read_dir, read_to_string, File},
io::Read,
};
use sha2::{Digest, Sha256};
/// 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
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");
}
}
fn build_wordlist() -> Vec<String> {
let wordlist_file = include_str!("wordlist.txt");
wordlist_file
.lines()
.skip(1)
.map(|x| x.trim().to_string())
.collect()
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn u8_to_bitslice(byte: &u8) -> [bool; 8] {
[
byte & (1 << 0) != 0,
byte & (1 << 1) != 0,
byte & (1 << 2) != 0,
byte & (1 << 3) != 0,
byte & (1 << 4) != 0,
byte & (1 << 5) != 0,
byte & (1 << 6) != 0,
byte & (1 << 7) != 0,
]
}
fn bitslice_to_usize(bitslice: [bool; 11]) -> usize {
usize::from(bitslice[0])
+ (usize::from(bitslice[1]) << 1)
+ (usize::from(bitslice[2]) << 2)
+ (usize::from(bitslice[3]) << 3)
+ (usize::from(bitslice[4]) << 4)
+ (usize::from(bitslice[5]) << 5)
+ (usize::from(bitslice[6]) << 6)
+ (usize::from(bitslice[7]) << 7)
+ (usize::from(bitslice[8]) << 8)
+ (usize::from(bitslice[9]) << 9)
+ (usize::from(bitslice[10]) << 10)
}
fn main() -> Result<(), Box<dyn Error>> {
if !env::vars()
.any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
{
ensure_safe_kernel_version();
ensure_offline();
}
let wordlist = build_wordlist();
assert_eq!(
wordlist.len(),
2usize.pow(11),
"Wordlist did not include correct word count"
);
let bit_size: usize = env::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 mut random_handle = File::open("/dev/urandom")?;
let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..])?;
let mut hasher = Sha256::new();
hasher.update(&entropy);
let hash = hasher.finalize();
let checksum = &hash[..bit_size / 32 / 8];
let seed = [&entropy[..bit_size / 8], checksum].concat();
let seed_bits = seed.iter().flat_map(u8_to_bitslice).collect::<Vec<_>>();
let words = seed_bits
.chunks_exact(11)
.map(|chunk| wordlist[bitslice_to_usize(chunk.try_into().expect("11 bit chunks"))].clone())
.collect::<Vec<_>>();
println!("{}", words.join(" "));
Ok(())
}