keyfork-mnemonic-util: .seed() => .entropy(), make new bip39-compliant .seed() method
This commit is contained in:
parent
ea0a207a5f
commit
0f31cd2424
|
@ -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"
|
||||
|
|
|
@ -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<DerivationResponse> {
|
||||
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<u8>) -> Result<DerivationResponse> {
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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<u8> {
|
||||
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<Wordlist>,
|
||||
) -> Result<Mnemonic, MnemonicGenerationError> {
|
||||
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<u8> {
|
||||
pub fn entropy(&self) -> Vec<u8> {
|
||||
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::<Vec<_>>()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn seed<'a>(&self, passphrase: impl Into<Option<&'a str>>) -> Result<Vec<u8>, MnemonicGenerationError> {
|
||||
let passphrase = passphrase.into();
|
||||
|
||||
let mut seed = [0u8; 64];
|
||||
let mnemonic = self.to_string();
|
||||
let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
|
||||
pbkdf2::<Hmac<Sha512>>(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<usize>, Arc<Wordlist>) {
|
||||
(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];
|
||||
|
|
|
@ -23,7 +23,8 @@ pub async fn start_and_run_server_on(
|
|||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue