diff --git a/Cargo.lock b/Cargo.lock index 16b1132b..691ff7a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1126,9 +1126,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", diff --git a/bip32/src/error.rs b/bip32/src/error.rs index 99455f67..2b43d539 100644 --- a/bip32/src/error.rs +++ b/bip32/src/error.rs @@ -15,6 +15,19 @@ pub enum Error { /// BIP39-related errors. Bip39, + /// BIP39 can't find the word in the selected wordlist. + Bip39InvalidWord, + + /// BIP39-related errors. + Bip39InvalidPhraseSize, + + ///BIP39 entropy must have 16 or 32 bytes + Bip39InvalidEntropySize, + + ///BIP39 invalid checksum + Bip39InvalidChecksum, + + /// Child number-related errors. ChildNumber, @@ -36,6 +49,10 @@ impl Display for Error { match self { Error::Base58 => f.write_str("base58 error"), Error::Bip39 => f.write_str("bip39 error"), + Error::Bip39InvalidPhraseSize => f.write_str("bip39 invalid phrase size error"), + Error::Bip39InvalidEntropySize => f.write_str("bip39 entropy must have 16 or 32 bytes"), + Error::Bip39InvalidChecksum => f.write_str("bip39 invalid checksum"), + Error::Bip39InvalidWord => f.write_str("bip39 can't find the word in the selected wordlist"), Error::ChildNumber => f.write_str("invalid child number"), Error::Crypto => f.write_str("cryptographic error"), Error::Decode => f.write_str("decoding error"), diff --git a/bip32/src/lib.rs b/bip32/src/lib.rs index e3d215e6..9da1779a 100644 --- a/bip32/src/lib.rs +++ b/bip32/src/lib.rs @@ -156,3 +156,4 @@ type HmacSha512 = hmac::Hmac; /// Size of input key material and derived keys. pub const KEY_SIZE: usize = 32; +pub const WORD_SIZE_24: usize = 32; diff --git a/bip32/src/mnemonic/phrase.rs b/bip32/src/mnemonic/phrase.rs index 251d185e..4abc6447 100644 --- a/bip32/src/mnemonic/phrase.rs +++ b/bip32/src/mnemonic/phrase.rs @@ -1,13 +1,16 @@ //! BIP39 mnemonic phrases +use core::borrow::Borrow; +use std::println; + use super::{ bits::{BitWriter, IterExt}, language::Language, }; -use crate::{Error, KEY_SIZE}; -use alloc::{format, string::String}; +use crate::{Error, KEY_SIZE, WORD_SIZE_24}; +use alloc::{format, string::String, vec::{self, Vec}}; use rand_core::{CryptoRng, RngCore}; -use sha2::{Digest, Sha256}; +use sha2::{digest::typenum::Bit, Digest, Sha256}; use zeroize::{Zeroize, Zeroizing}; #[cfg(feature = "bip39")] @@ -17,8 +20,14 @@ use {super::seed::Seed, sha2::Sha512}; #[cfg(feature = "bip39")] const PBKDF2_ROUNDS: u32 = 2048; -/// Source entropy for a BIP39 mnemonic phrase -pub type Entropy = [u8; KEY_SIZE]; +#[derive(Copy, Clone)] +pub enum Words { + Use12 = 16, + Use24 = 32 +} +fn usize(words: &Words) -> usize { + *words as usize +} /// BIP39 mnemonic phrases: sequences of words representing cryptographic keys. #[derive(Clone)] @@ -26,26 +35,37 @@ pub struct Phrase { /// Language language: Language, + /// Number of words used in entropy 12 or 24 + nwords: Words, + /// Source entropy for this phrase - entropy: Entropy, + entropy: Vec, /// Mnemonic phrase phrase: String, + } + impl Phrase { /// Create a random BIP39 mnemonic phrase. pub fn random(mut rng: impl RngCore + CryptoRng, language: Language) -> Self { - let mut entropy = Entropy::default(); + let mut entropy: Vec = Vec::with_capacity(32); rng.fill_bytes(&mut entropy); - Self::from_entropy(entropy, language) + Self::from_entropy(entropy, language).unwrap() } + /// Create a new BIP39 mnemonic phrase from the given entropy - pub fn from_entropy(entropy: Entropy, language: Language) -> Self { + pub fn from_entropy(entropy: Vec , language: Language) -> Result { let wordlist = language.wordlist(); - let checksum_byte = Sha256::digest(entropy.as_ref()).as_slice()[0]; - + let nwords = match entropy.capacity() { + 16 => Words::Use12, + 32 => Words::Use24, + _ => return Err(Error::Bip39InvalidEntropySize) + }; + let checksum_byte = Sha256::digest(entropy.clone()).as_slice()[0]; + // First, create a byte iterator for the given entropy and the first byte of the // hash of the entropy that will serve as the checksum (up to 8 bits for biggest // entropy source). @@ -55,18 +75,19 @@ impl Phrase { // // Given the entropy is of correct size, this ought to give us the correct word // count. - let phrase = entropy + let phrase: String = entropy.clone() .iter() .chain(Some(&checksum_byte)) .bits() .map(|bits| wordlist.get_word(bits)) .join(" "); - Phrase { + Ok(Phrase { language, - entropy, - phrase, - } + entropy: entropy.to_vec(), + phrase: phrase, + nwords, + }) } /// Create a new BIP39 mnemonic phrase from the given string. @@ -77,46 +98,63 @@ impl Phrase { /// To use the default language, English, (the only one supported by this /// library and also the only one standardized for BIP39) you can supply /// `Default::default()` as the language. + /// pub fn new(phrase: S, language: Language) -> Result where S: AsRef, { + let phrase = phrase.as_ref(); + let nwords = match phrase.split(' ').count() { + d if d == 12 => Words::Use12, + d if d == 24 => Words::Use24, + _ => return Err(Error::Bip39InvalidPhraseSize), + }; + let wordmap = language.wordmap(); // Preallocate enough space for the longest possible word list - let mut bits = BitWriter::with_capacity(264); + let mut bits = BitWriter::with_capacity(match nwords { + Words::Use12 => 132, + Words::Use24 => 264, + }); for word in phrase.split(' ') { - bits.push(wordmap.get_bits(word).ok_or(Error::Bip39)?); + bits.push(wordmap.get_bits(word).ok_or(Error::Bip39InvalidWord)?); } - + let mut entropy = Zeroizing::new(bits.into_bytes()); - - if entropy.len() != KEY_SIZE + 1 { - return Err(Error::Bip39); + println!("Entropy len {}",entropy.len()); + if entropy.len() != usize(&nwords) + 1 { + return Err(Error::Bip39InvalidEntropySize); } - let actual_checksum = entropy[KEY_SIZE]; + + let actual_checksum = entropy[usize(&nwords)]; - // Truncate to get rid of the byte containing the checksum - entropy.truncate(KEY_SIZE); - - let expected_checksum = Sha256::digest(&entropy).as_slice()[0]; + // Truncate to get rid of the byte containing the checksum + entropy.truncate(usize(&nwords)); + + let expected_checksum = Sha256::digest(&entropy).as_slice()[0] + & match nwords { + Words::Use12 => 0b11110000, //for 12 words the checksum is represented by 4 bits + Words::Use24 => 0b11111111, //for 24 words the checksum is represented by 8 bits + }; + if actual_checksum != expected_checksum { - return Err(Error::Bip39); + return Err(Error::Bip39InvalidChecksum); } - Ok(Self::from_entropy( + Self::from_entropy( entropy.as_slice().try_into().map_err(|_| Error::Bip39)?, - language, - )) + language + ) } /// Get source entropy for this phrase. - pub fn entropy(&self) -> &Entropy { - &self.entropy + pub fn entropy(&self) -> &Vec { + & self.entropy } /// Get the mnemonic phrase as a string reference. diff --git a/bip32/tests/bip39_vectors.rs b/bip32/tests/bip39_vectors.rs index 9a87db68..12b53706 100644 --- a/bip32/tests/bip39_vectors.rs +++ b/bip32/tests/bip39_vectors.rs @@ -68,11 +68,78 @@ const TEST_VECTORS: &[TestVector] = &[ } ]; + +struct TestVector12 { + entropy: [u8; 16], + phrase: &'static str, + seed: [u8; 64], + xprv: &'static str, +} + +const TEST_VECTORS12: &[TestVector12] = &[ + +TestVector12 { + entropy: hex!("00000000000000000000000000000000"), + phrase: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + seed: hex!("c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04"), + xprv: "xprv9s21ZrQH143K3h3fDYiay8mocZ3afhfULfb5GX8kCBdno77K4HiA15Tg23wpbeF1pLfs1c5SPmYHrEpTuuRhxMwvKDwqdKiGJS9XFKzUsAF" + }, + + TestVector12 { + entropy: hex!("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"), + phrase: "legal winner thank year wave sausage worth useful legal winner thank yellow", + seed: hex!("2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607"), + xprv: "xprv9s21ZrQH143K2gA81bYFHqU68xz1cX2APaSq5tt6MFSLeXnCKV1RVUJt9FWNTbrrryem4ZckN8k4Ls1H6nwdvDTvnV7zEXs2HgPezuVccsq" + }, + + TestVector12 { + entropy: hex!("80808080808080808080808080808080"), + phrase: "letter advice cage absurd amount doctor acoustic avoid letter advice cage above", + seed: hex!("d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8"), + xprv: "xprv9s21ZrQH143K2shfP28KM3nr5Ap1SXjz8gc2rAqqMEynmjt6o1qboCDpxckqXavCwdnYds6yBHZGKHv7ef2eTXy461PXUjBFQg6PrwY4Gzq" + }, + TestVector12 { + entropy: hex!("ffffffffffffffffffffffffffffffff"), + phrase: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong", + seed: hex!("ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069"), + xprv: "xprv9s21ZrQH143K2V4oox4M8Zmhi2Fjx5XK4Lf7GKRvPSgydU3mjZuKGCTg7UPiBUD7ydVPvSLtg9hjp7MQTYsW67rZHAXeccqYqrsx8LcXnyd" + }, + TestVector12 { + entropy: hex!("9e885d952ad362caeb4efe34a8e91bd2"), + phrase: "ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic", + seed: hex!("274ddc525802f7c828d8ef7ddbcdc5304e87ac3535913611fbbfa986d0c9e5476c91689f9c8a54fd55bd38606aa6a8595ad213d4c9c9f9aca3fb217069a41028"), + xprv: "xprv9s21ZrQH143K2oZ9stBYpoaZ2ktHj7jLz7iMqpgg1En8kKFTXJHsjxry1JbKH19YrDTicVwKPehFKTbmaxgVEc5TpHdS1aYhB2s9aFJBeJH" + }, + +TestVector12 { + entropy: hex!("23db8160a31d3e0dca3688ed941adbf3"), + phrase: "cat swing flag economy stadium alone churn speed unique patch report train", + seed: hex!("deb5f45449e615feff5640f2e49f933ff51895de3b4381832b3139941c57b59205a42480c52175b6efcffaa58a2503887c1e8b363a707256bdd2b587b46541f5"), + xprv: "xprv9s21ZrQH143K4G28omGMogEoYgDQuigBo8AFHAGDaJdqQ99QKMQ5J6fYTMfANTJy6xBmhvsNZ1CJzRZ64PWbnTFUn6CDV2FxoMDLXdk95DQ" +}, + +TestVector12 { + entropy: hex!("f30f8c1da665478f49b001d94c5fc452"), + phrase: "vessel ladder alter error federal sibling chat ability sun glass valve picture", + seed: hex!("2aaa9242daafcee6aa9d7269f17d4efe271e1b9a529178d7dc139cd18747090bf9d60295d0ce74309a78852a9caadf0af48aae1c6253839624076224374bc63f"), + xprv:"xprv9s21ZrQH143K2QWV9Wn8Vvs6jbqfF1YbTCdURQW9dLFKDovpKaKrqS3SEWsXCu6ZNky9PSAENg6c9AQYHcg4PjopRGGKmdD313ZHszymnps" +}, + +]; + + + #[test] fn test_mnemonic() { for vector in TEST_VECTORS { - let mnemonic = Mnemonic::from_entropy(vector.entropy, Default::default()); - assert_eq!(mnemonic.phrase(), vector.phrase); + let mnemonic = Mnemonic::from_entropy(vector.entropy.to_vec(), Default::default()); + assert_eq!(mnemonic.is_ok(),true); + assert_eq!(mnemonic.unwrap().phrase(), vector.phrase); + } + for vector in TEST_VECTORS12 { + let mnemonic = Mnemonic::from_entropy(vector.entropy.to_vec(), Default::default()); + assert_eq!(mnemonic.is_ok(),true); + assert_eq!(mnemonic.unwrap().phrase(), vector.phrase); } } @@ -85,6 +152,13 @@ fn test_seed() { mnemonic.to_seed(TEST_VECTOR_PASSWORD).as_bytes() ); } + for vector in TEST_VECTORS12 { + let mnemonic = Mnemonic::new(vector.phrase, Default::default()).unwrap(); + assert_eq!( + &vector.seed, + mnemonic.to_seed(TEST_VECTOR_PASSWORD).as_bytes() + ); + } } #[test] @@ -95,4 +169,11 @@ fn test_xprv() { let derived_xprv = XPrv::new(&seed).unwrap(); assert_eq!(expected_xprv, derived_xprv); } + + for vector in TEST_VECTORS12 { + let seed = Seed::new(vector.seed); + let expected_xprv = vector.xprv.parse::().unwrap(); + let derived_xprv = XPrv::new(&seed).unwrap(); + assert_eq!(expected_xprv, derived_xprv); + } }