diff --git a/Cargo.lock b/Cargo.lock index 31d5b14..39ba73f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -927,6 +927,8 @@ version = "0.1.0" dependencies = [ "bip39", "hex", + "hmac", + "pbkdf2", "serde_json", "sha2", ] @@ -1226,6 +1228,16 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + [[package]] name = "peeking_take_while" version = "0.1.2" diff --git a/keyfork-derive-util/src/request.rs b/keyfork-derive-util/src/request.rs index 255069e..d26e03e 100644 --- a/keyfork-derive-util/src/request.rs +++ b/keyfork-derive-util/src/request.rs @@ -1,7 +1,7 @@ use crate::{ extended_key::private_key::Error as XPrvError, DerivationPath, ExtendedPrivateKey, PrivateKey, }; -use keyfork_mnemonic_util::Mnemonic; +use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError}; use serde::{Deserialize, Serialize}; #[derive(Debug, thiserror::Error)] @@ -9,6 +9,9 @@ pub enum DerivationError { #[error("algorithm not supported")] Algorithm, + #[error("Unable to create seed from mnemonic: {0}")] + Mnemonic(#[from] MnemonicGenerationError), + #[error("{0}")] ExtendedPrivateKey(#[from] XPrvError), } @@ -79,7 +82,8 @@ impl DerivationRequest { } pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result { - self.derive_with_master_seed(mnemonic.seed()) + // TODO: passphrase support and/or store passphrase within mnemonic + self.derive_with_master_seed(mnemonic.seed(None)?) } pub fn derive_with_master_seed(&self, seed: Vec) -> Result { diff --git a/keyfork-mnemonic-util/Cargo.toml b/keyfork-mnemonic-util/Cargo.toml index a0791af..769b346 100644 --- a/keyfork-mnemonic-util/Cargo.toml +++ b/keyfork-mnemonic-util/Cargo.toml @@ -7,6 +7,8 @@ repository = "https://git.distrust.co/public/keyfork" edition = "2021" [dependencies] +hmac = "0.12.1" +pbkdf2 = "0.12.2" # Included in rust sha2 = "0.10.7" diff --git a/keyfork-mnemonic-util/src/lib.rs b/keyfork-mnemonic-util/src/lib.rs index b20c65b..f446b5d 100644 --- a/keyfork-mnemonic-util/src/lib.rs +++ b/keyfork-mnemonic-util/src/lib.rs @@ -1,7 +1,8 @@ -use std::{collections::HashMap, str::FromStr, sync::Arc}; +use std::{collections::HashMap, str::FromStr, sync::Arc, error::Error, fmt::Display}; -use sha2::{Digest, Sha256}; -use std::{error::Error, fmt::Display}; +use sha2::{Digest, Sha256, Sha512}; +use pbkdf2::pbkdf2; +use hmac::Hmac; /// The error type representing a failure to create a [`Mnemonic`]. These errors only occur during /// [`Mnemonic`] creation. @@ -13,6 +14,9 @@ pub enum MnemonicGenerationError { /// 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), + + /// Invalid length resulting from PBKDF2. + InvalidPbkdf2Length, } impl Display for MnemonicGenerationError { @@ -24,6 +28,9 @@ impl Display for MnemonicGenerationError { MnemonicGenerationError::InvalidByteLength(count) => { write!(f, "Invalid byte length: {count}, must be 128 or 256") } + MnemonicGenerationError::InvalidPbkdf2Length => { + f.write_str("Invalid length from PBKDF2") + }, } } } @@ -144,12 +151,6 @@ impl FromStr for Mnemonic { } } -fn generate_slice_hash(data: &[u8]) -> Vec { - let mut hasher = Sha256::new(); - hasher.update(data); - hasher.finalize().to_vec() -} - impl Mnemonic { /// Generate a [`Mnemonic`] from the provided entropy and [`Wordlist`]. /// @@ -160,7 +161,6 @@ impl Mnemonic { wordlist: Arc, ) -> Result { let bit_count = bytes.len() * 8; - let hash = generate_slice_hash(bytes); if bit_count % 32 != 0 { return Err(MnemonicGenerationError::InvalidByteCount(bit_count)); @@ -177,6 +177,10 @@ impl Mnemonic { bits[byte_index * 8 + bit_index] = (bytes[byte_index] & (1 << (7 - bit_index))) > 0; } } + + let mut hasher = Sha256::new(); + hasher.update(bytes); + let hash = hasher.finalize().to_vec(); for check_bit in 0..bit_count / 32 { bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0; } @@ -185,7 +189,6 @@ impl Mnemonic { // 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; @@ -197,15 +200,17 @@ impl Mnemonic { Ok(Mnemonic { words, wordlist }) } - #[must_use] - pub fn seed(&self) -> Vec { + pub fn entropy(&self) -> Vec { let mut bits = vec![false; self.words.len() * 11]; for (index, word) in self.words.iter().enumerate() { for bit in 0..11 { bits[index * 11 + bit] = (word & (1 << (10 - bit))) > 0; } } + + // remove checksum bits bits.truncate(bits.len() - bits.len() % 32); + bits.chunks_exact(8) .map(|chunk| { let mut num = 0u8; @@ -214,10 +219,20 @@ impl Mnemonic { } num }) - .collect::>() + .collect() + } + + pub fn seed<'a>(&self, passphrase: impl Into>) -> Result, MnemonicGenerationError> { + let passphrase = passphrase.into(); + + let mut seed = [0u8; 64]; + let mnemonic = self.to_string(); + let salt = ["mnemonic", passphrase.unwrap_or("")].join(""); + pbkdf2::>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed) + .map_err(|_| MnemonicGenerationError::InvalidPbkdf2Length)?; + Ok(seed.to_vec()) } - #[must_use] pub fn into_inner(self) -> (Vec, Arc) { (self.words, self.wordlist) } @@ -246,8 +261,8 @@ mod tests { random_handle.read_exact(&mut entropy[..]).unwrap(); let wordlist = Wordlist::default().arc(); let mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap(); - let seed = mnemonic.seed(); - assert_eq!(&seed, entropy); + let new_entropy = mnemonic.entropy(); + assert_eq!(&new_entropy, entropy); } #[test] @@ -277,10 +292,13 @@ mod tests { 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()); + assert_eq!(my_mnemonic.seed(None).unwrap(), their_mnemonic.to_seed("")); + assert_eq!(my_mnemonic.seed("testing").unwrap(), their_mnemonic.to_seed("testing")); + assert_ne!(my_mnemonic.seed("test1").unwrap(), their_mnemonic.to_seed("test2")); } #[test] - fn count_to_get_duplicate_words() { + fn count_rate_of_duplicate_words() { let tests = 100_000; let mut count = 0.; let entropy = &mut [0u8; 256 / 8]; diff --git a/keyforkd/src/lib.rs b/keyforkd/src/lib.rs index 75b0a5d..4e44844 100644 --- a/keyforkd/src/lib.rs +++ b/keyforkd/src/lib.rs @@ -23,7 +23,8 @@ pub async fn start_and_run_server_on( ) -> Result<(), Box> { let service = ServiceBuilder::new() .layer(middleware::BincodeLayer::new()) - .service(Keyforkd::new(mnemonic.seed())); + // TODO: passphrase support and/or store passphrase with mnemonic + .service(Keyforkd::new(mnemonic.seed(None)?)); let mut server = match UnixServer::bind(socket_path) { Ok(s) => s, diff --git a/keyforkd/src/service.rs b/keyforkd/src/service.rs index b56d691..a550bb9 100644 --- a/keyforkd/src/service.rs +++ b/keyforkd/src/service.rs @@ -61,7 +61,6 @@ mod tests { use super::*; use hex_literal::hex; use keyfork_derive_util::{request::*, DerivationPath}; - use keyfork_mnemonic_util::{Mnemonic, Wordlist}; use keyfork_slip10_test_data::test_data; use std::str::FromStr; use tower::ServiceExt; @@ -73,28 +72,9 @@ mod tests { .remove(&"secp256k1".to_string()) .unwrap(); - let wordlist = Wordlist::default().arc(); - for per_seed in tests { let seed = &per_seed.seed; - // Test mnemonic path - if [128 / 8, 256 / 8].contains(&seed.len()) { - let mnemonic = Mnemonic::from_entropy(seed, wordlist.clone()).unwrap(); - let mut keyforkd = Keyforkd::new(mnemonic.seed()); - for test in &per_seed.tests { - let chain = DerivationPath::from_str(test.chain).unwrap(); - if chain.len() < 2 { - continue; - } - let req = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain); - let response = keyforkd.ready().await.unwrap().call(req).await.unwrap(); - assert_eq!(response.data, test.private_key); - assert_eq!(response.chain_code.as_slice(), test.chain_code); - } - } - - // Test seed path let mut keyforkd = Keyforkd::new(seed.to_vec()); for test in &per_seed.tests { let chain = DerivationPath::from_str(test.chain).unwrap(); @@ -116,27 +96,9 @@ mod tests { .remove(&"ed25519".to_string()) .unwrap(); - let wordlist = Wordlist::default().arc(); - for per_seed in tests { let seed = &per_seed.seed; - // Test mnemonic path - if [128 / 8, 256 / 8].contains(&seed.len()) { - let mnemonic = Mnemonic::from_entropy(seed, wordlist.clone()).unwrap(); - let mut keyforkd = Keyforkd::new(mnemonic.seed()); - for test in &per_seed.tests { - let chain = DerivationPath::from_str(test.chain).unwrap(); - if chain.len() < 2 { - continue; - } - let req = DerivationRequest::new(DerivationAlgorithm::Ed25519, &chain); - let response = keyforkd.ready().await.unwrap().call(req).await.unwrap(); - assert_eq!(response.data, test.private_key); - assert_eq!(response.chain_code.as_slice(), test.chain_code); - } - } - // Test seed path let mut keyforkd = Keyforkd::new(seed.to_vec()); for test in &per_seed.tests { @@ -162,12 +124,9 @@ mod tests { hex!("2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7"), hex!("00a4b2856bfec510abab89753fac1ac0e1112364e7d250545963f135f2a33188ed"), )]; - let wordlist = Wordlist::default().arc(); for (seed, path, _, private_key, _) in tests { - let mnemonic = Mnemonic::from_entropy(&seed[..], wordlist.clone()).unwrap(); - assert_eq!(mnemonic.seed(), seed); let req = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path); - let mut keyforkd = Keyforkd::new(mnemonic.seed()); + let mut keyforkd = Keyforkd::new(seed.to_vec()); let response = keyforkd.ready().await.unwrap().call(req).await.unwrap(); assert_eq!(response.data, private_key) } @@ -183,12 +142,9 @@ mod tests { hex!("68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3"), hex!("008c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c"), )]; - let wordlist = Wordlist::default().arc(); for (seed, path, _, private_key, _) in tests { - let mnemonic = Mnemonic::from_entropy(&seed[..], wordlist.clone()).unwrap(); - assert_eq!(mnemonic.seed(), seed); let req = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path); - let mut keyforkd = Keyforkd::new(mnemonic.seed()); + let mut keyforkd = Keyforkd::new(seed.to_vec()); let response = keyforkd.ready().await.unwrap().call(req).await.unwrap(); assert_eq!(response.data, private_key) }