keyfork-mnemonic-util: split mnemonic utilities out of keyfork-mnemonic-generate
This commit is contained in:
parent
8e74c18135
commit
5d7a3c99ba
|
@ -88,6 +88,13 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
|||
[[package]]
|
||||
name = "keyfork-mnemonic-generate"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"keyfork-mnemonic-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyfork-mnemonic-util"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bip39",
|
||||
"hex",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
[workspace]
|
||||
|
||||
members = [
|
||||
"keyfork-mnemonic-generate"
|
||||
"keyfork-mnemonic-generate",
|
||||
"keyfork-mnemonic-util"
|
||||
]
|
||||
|
|
|
@ -6,9 +6,4 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
sha2 = "0.10.7"
|
||||
|
||||
[dev-dependencies]
|
||||
bip39 = "2.0.0"
|
||||
hex = "0.4.3"
|
||||
serde_json = "1.0.105"
|
||||
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util" }
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
fmt::Display,
|
||||
fs::{read_dir, read_to_string, File},
|
||||
io::Read,
|
||||
};
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use keyfork_mnemonic_util::{Mnemonic, Wordlist};
|
||||
|
||||
type Result<T, E = Box<dyn Error>> = std::result::Result<T, E>;
|
||||
|
||||
|
@ -19,36 +18,24 @@ type Result<T, E = Box<dyn Error>> = std::result::Result<T, E>;
|
|||
/// * --features kitchen-sink: load username; system time's most random, precise bits; hostname;
|
||||
/// kernel version; other env specific shit into a CSPRNG
|
||||
|
||||
struct Entropy(File);
|
||||
|
||||
/// An entropy source
|
||||
impl Entropy {
|
||||
fn new() -> Result<Self> {
|
||||
let file = File::open("/dev/random")?;
|
||||
Ok(Self(file))
|
||||
}
|
||||
|
||||
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/"];
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MnemonicGenerationError {
|
||||
InvalidByteCount(usize),
|
||||
InvalidByteLength(usize),
|
||||
}
|
||||
|
||||
impl MnemonicGenerationError {
|
||||
fn boxed(self) -> Box<Self> {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MnemonicGenerationError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MnemonicGenerationError::InvalidByteCount(count) => {
|
||||
write!(f, "Invalid byte count: {count}, must be divisible by 8")
|
||||
}
|
||||
MnemonicGenerationError::InvalidByteLength(count) => {
|
||||
write!(f, "Invalid byte length: {count}, must be 128 or 256")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for MnemonicGenerationError {}
|
||||
|
||||
fn ensure_safe_kernel_version() {
|
||||
let kernel_version = read_to_string("/proc/version").expect("/proc/version");
|
||||
let v = kernel_version
|
||||
|
@ -91,102 +78,6 @@ fn ensure_offline() {
|
|||
}
|
||||
}
|
||||
|
||||
struct Wordlist(Vec<String>);
|
||||
|
||||
impl Default for Wordlist {
|
||||
fn default() -> Self {
|
||||
// TODO: English is the only supported language.
|
||||
let wordlist_file = include_str!("wordlist.txt");
|
||||
Wordlist(
|
||||
wordlist_file
|
||||
.lines()
|
||||
.skip(1)
|
||||
.map(|x| x.trim().to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Wordlist {
|
||||
fn get_word(&self, word: usize) -> Option<String> {
|
||||
self.0.get(word).cloned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn into_inner(self) -> Vec<String> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
struct Mnemonic<'a> {
|
||||
pub words: Vec<usize>,
|
||||
wordlist: &'a Wordlist,
|
||||
}
|
||||
|
||||
impl<'a> Mnemonic<'a> {
|
||||
fn from_entropy(bytes: &[u8], wordlist: &'a Wordlist) -> Result<Mnemonic<'a>> {
|
||||
let bit_count = bytes.len() * 8;
|
||||
let hash = generate_slice_hash(bytes);
|
||||
|
||||
if bit_count % 32 != 0 {
|
||||
return Err(MnemonicGenerationError::InvalidByteCount(bit_count).boxed());
|
||||
}
|
||||
// 192 supported for test suite
|
||||
if ![128, 192, 256].contains(&bit_count) {
|
||||
return Err(MnemonicGenerationError::InvalidByteLength(bit_count).boxed());
|
||||
}
|
||||
assert_eq!(bit_count % 32, 0, "bit count must be in 32 bit increments");
|
||||
|
||||
let mut bits = vec![false; bit_count + bit_count / 32];
|
||||
|
||||
for byte_index in 0..bit_count / 8 {
|
||||
for bit_index in 0..8 {
|
||||
bits[byte_index * 8 + bit_index] = (bytes[byte_index] & (1 << (7 - bit_index))) > 0;
|
||||
}
|
||||
}
|
||||
for check_bit in 0..bit_count / 32 {
|
||||
bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0;
|
||||
}
|
||||
|
||||
assert_eq!(bits.len() % 11, 0, "unstable bit count");
|
||||
|
||||
let words = bits
|
||||
.chunks_exact(11)
|
||||
.map(|chunk| bitslice_to_usize(chunk.try_into().expect("11 bit chunks")))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Mnemonic { words, wordlist })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for Mnemonic<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut iter = self.words.iter().peekable();
|
||||
while let Some(word_index) = iter.next() {
|
||||
let word = self.wordlist.get_word(*word_index).expect("word");
|
||||
write!(f, "{word}")?;
|
||||
if iter.peek().is_some() {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_slice_hash(data: &[u8]) -> Vec<u8> {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(data);
|
||||
hasher.finalize().to_vec()
|
||||
}
|
||||
|
||||
fn bitslice_to_usize(bitslice: [bool; 11]) -> usize {
|
||||
let mut index = 0usize;
|
||||
for i in 0..11 {
|
||||
index += usize::from(bitslice[10 - i]) << i;
|
||||
}
|
||||
index
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
if !env::vars()
|
||||
.any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
|
||||
|
@ -205,9 +96,9 @@ fn main() -> Result<()> {
|
|||
"Only 12 or 24 word mnemonics are supported"
|
||||
);
|
||||
|
||||
let mut random_handle = File::open("/dev/urandom")?;
|
||||
let mut rng = Entropy::new()?;
|
||||
let entropy = &mut [0u8; 256 / 8];
|
||||
random_handle.read_exact(&mut entropy[..])?;
|
||||
rng.read_into(&mut entropy[..])?;
|
||||
|
||||
let wordlist = Wordlist::default();
|
||||
let mnemonic = Mnemonic::from_entropy(&entropy[..bit_size / 8], &wordlist)?;
|
||||
|
@ -219,71 +110,30 @@ fn main() -> Result<()> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn has_good_wordlist() {
|
||||
let wordlist = Wordlist::default().into_inner();
|
||||
assert_eq!(
|
||||
wordlist.len(),
|
||||
2usize.pow(11),
|
||||
"Wordlist did not include correct word count"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loads_mnemonics() -> Result<()> {
|
||||
let content = include_str!("test/vectors.json");
|
||||
let jsonobj: serde_json::Value = serde_json::from_str(content)?;
|
||||
|
||||
for test in jsonobj["english"].as_array().unwrap() {
|
||||
let [ref hex_, ref seed, ..] = test.as_array().unwrap()[..] else {
|
||||
panic!("bad test: {test}");
|
||||
};
|
||||
let hex = hex::decode(hex_.as_str().unwrap()).unwrap();
|
||||
|
||||
let wordlist = Wordlist::default();
|
||||
let mnemonic = Mnemonic::from_entropy(&hex, &wordlist)?;
|
||||
|
||||
assert_eq!(mnemonic.to_string(), seed.as_str().unwrap());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_bip39_crate() {
|
||||
let mut random_handle = File::open("/dev/random").unwrap();
|
||||
let entropy = &mut [0u8; 256 / 8];
|
||||
random_handle.read_exact(&mut entropy[..]).unwrap();
|
||||
let wordlist = Wordlist::default();
|
||||
let my_mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], &wordlist).unwrap();
|
||||
let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).unwrap();
|
||||
/*
|
||||
let my_words = my_mnemonic.words.clone();
|
||||
let their_words = their_mnemonic.word_iter().collect::<Vec<_>>();
|
||||
*/
|
||||
assert_eq!(my_mnemonic.to_string(), their_mnemonic.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_to_get_duplicate_words() {
|
||||
let mut count = 0.;
|
||||
let tests = 100_000;
|
||||
let mut random_handle = File::open("/dev/random").unwrap();
|
||||
let mut count = 0.;
|
||||
let entropy = &mut [0u8; 256 / 8];
|
||||
let wordlist = Wordlist::default();
|
||||
let mut rng = Entropy::new().unwrap();
|
||||
let mut hs = HashSet::<usize>::with_capacity(24);
|
||||
|
||||
for _ in 0..tests {
|
||||
random_handle.read_exact(&mut entropy[..]).unwrap();
|
||||
// let bits = generate_slice_hash(entropy);
|
||||
rng.read_into(&mut entropy[..]).unwrap();
|
||||
let mnemonic = Mnemonic::from_entropy(&entropy[..256 / 8], &wordlist).unwrap();
|
||||
let mut words = mnemonic.words.clone();
|
||||
words.sort();
|
||||
assert_eq!(words.len(), 24);
|
||||
words.dedup();
|
||||
if words.len() != 24 {
|
||||
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
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "keyfork-mnemonic-util"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
sha2 = "0.10.7"
|
||||
|
||||
[dev-dependencies]
|
||||
bip39 = "2.0.0"
|
||||
hex = "0.4.3"
|
||||
serde_json = "1.0.105"
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,187 @@
|
|||
use sha2::{Digest, Sha256};
|
||||
use std::{error::Error, fmt::Display};
|
||||
|
||||
/// The error type representing a failure to create a [`Mnemonic`]. These errors only occur during
|
||||
/// [`Mnemonic`] creation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MnemonicGenerationError {
|
||||
/// The amount of bits passed to a mnemonic must be divisible by 32.
|
||||
InvalidByteCount(usize),
|
||||
|
||||
/// The length of a mnemonic in bits must be within the BIP-0039 range, and supported by the
|
||||
/// library. Currently, only 128, 192 (for testing purposes), and 256 are supported.
|
||||
InvalidByteLength(usize),
|
||||
}
|
||||
|
||||
impl Display for MnemonicGenerationError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MnemonicGenerationError::InvalidByteCount(count) => {
|
||||
write!(f, "Invalid byte count: {count}, must be divisible by 8")
|
||||
}
|
||||
MnemonicGenerationError::InvalidByteLength(count) => {
|
||||
write!(f, "Invalid byte length: {count}, must be 128 or 256")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for MnemonicGenerationError {}
|
||||
|
||||
/// A BIP-0039 compatible list of words.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Wordlist(Vec<String>);
|
||||
|
||||
impl Default for Wordlist {
|
||||
/// Returns the English wordlist in the Bitcoin BIP-0039 specification.
|
||||
fn default() -> Self {
|
||||
// TODO: English is the only supported language.
|
||||
let wordlist_file = include_str!("data/wordlist.txt");
|
||||
Wordlist(
|
||||
wordlist_file
|
||||
.lines()
|
||||
// skip 1: comment at top of file to point to BIP-0039 source.
|
||||
.skip(1)
|
||||
.map(|x| x.trim().to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Wordlist {
|
||||
/// Given an index, get a word from the wordlist.
|
||||
fn get_word(&self, word: usize) -> Option<&String> {
|
||||
self.0.get(word)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn into_inner(self) -> Vec<String> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A BIP-0039 mnemonic with reference to a [`Wordlist`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Mnemonic<'a> {
|
||||
words: Vec<usize>,
|
||||
wordlist: &'a Wordlist,
|
||||
}
|
||||
|
||||
impl<'a> Display for Mnemonic<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut iter = self.words.iter().peekable();
|
||||
while let Some(word_index) = iter.next() {
|
||||
let word = self.wordlist.get_word(*word_index).expect("word");
|
||||
write!(f, "{word}")?;
|
||||
if iter.peek().is_some() {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_slice_hash(data: &[u8]) -> Vec<u8> {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(data);
|
||||
hasher.finalize().to_vec()
|
||||
}
|
||||
|
||||
impl<'a> Mnemonic<'a> {
|
||||
/// Generate a [`Mnemonic`] from the provided entropy and [`Wordlist`].
|
||||
///
|
||||
/// # Errors
|
||||
/// An error may be returned if the entropy is not within the acceptable lengths.
|
||||
pub fn from_entropy(
|
||||
bytes: &[u8],
|
||||
wordlist: &'a Wordlist,
|
||||
) -> Result<Mnemonic<'a>, MnemonicGenerationError> {
|
||||
let bit_count = bytes.len() * 8;
|
||||
let hash = generate_slice_hash(bytes);
|
||||
|
||||
if bit_count % 32 != 0 {
|
||||
return Err(MnemonicGenerationError::InvalidByteCount(bit_count));
|
||||
}
|
||||
// 192 supported for test suite
|
||||
if ![128, 192, 256].contains(&bit_count) {
|
||||
return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
|
||||
}
|
||||
|
||||
let mut bits = vec![false; bit_count + bit_count / 32];
|
||||
|
||||
for byte_index in 0..bit_count / 8 {
|
||||
for bit_index in 0..8 {
|
||||
bits[byte_index * 8 + bit_index] = (bytes[byte_index] & (1 << (7 - bit_index))) > 0;
|
||||
}
|
||||
}
|
||||
for check_bit in 0..bit_count / 32 {
|
||||
bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0;
|
||||
}
|
||||
|
||||
let words = bits
|
||||
// NOTE: Tested with all approved variants. Always divisible by 11.
|
||||
.chunks_exact(11)
|
||||
.map(|chunk| {
|
||||
// NOTE: usize to use for indexing wordlist later
|
||||
let mut num = 0usize;
|
||||
for i in 0..11 {
|
||||
num += usize::from(chunk[10 - i]) << i;
|
||||
}
|
||||
num
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Mnemonic { words, wordlist })
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn into_inner(self) -> (Vec<usize>, &'a Wordlist) {
|
||||
(self.words, self.wordlist)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wordlist_word_count_correct() {
|
||||
let wordlist = Wordlist::default().into_inner();
|
||||
assert_eq!(
|
||||
wordlist.len(),
|
||||
2usize.pow(11),
|
||||
"Wordlist did not include correct word count"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conforms_to_trezor_tests() {
|
||||
let content = include_str!("data/vectors.json");
|
||||
let jsonobj: serde_json::Value = serde_json::from_str(content).unwrap();
|
||||
|
||||
for test in jsonobj["english"].as_array().unwrap() {
|
||||
let [ref hex_, ref seed, ..] = test.as_array().unwrap()[..] else {
|
||||
panic!("bad test: {test}");
|
||||
};
|
||||
let hex = hex::decode(hex_.as_str().unwrap()).unwrap();
|
||||
|
||||
let wordlist = Wordlist::default();
|
||||
let mnemonic = Mnemonic::from_entropy(&hex, &wordlist).unwrap();
|
||||
|
||||
assert_eq!(mnemonic.to_string(), seed.as_str().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_bip39_crate() {
|
||||
let mut random_handle = File::open("/dev/random").unwrap();
|
||||
let entropy = &mut [0u8; 256 / 8];
|
||||
random_handle.read_exact(&mut entropy[..]).unwrap();
|
||||
let wordlist = Wordlist::default();
|
||||
let my_mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], &wordlist).unwrap();
|
||||
let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).unwrap();
|
||||
assert_eq!(my_mnemonic.to_string(), their_mnemonic.to_string());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue