From 8097eba396b1148a64d910f0da0ef3e119bb134c Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Tue, 9 Mar 2021 12:08:30 +0000 Subject: [PATCH 1/8] Make serde dependency no_std as well --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ab2add6..7e134e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ path = "src/lib.rs" [features] default = [ "std" ] -std = [] +std = [ "serde/std" ] # Note: English is the standard for bip39 so always included chinese-simplified = [] @@ -44,7 +44,7 @@ bitcoin_hashes = "0.9.4" unicode-normalization = "=0.1.9" rand = { version = "0.6.0", optional = true } -serde = { version = "1.0", optional = true } +serde = { version = "1.0", default-features = false, optional = true } [dev-dependencies] rand = { version = "0.6.0", optional = false } From d41e0722dcbd12f41979a31ca9da49f9bb1d5936 Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 24 Mar 2021 16:39:53 +0000 Subject: [PATCH 2/8] Simplify language selection --- src/lib.rs | 42 +++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3e69c5b..f96359d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -240,18 +240,8 @@ impl Mnemonic { return Err(Error::BadWordCount(0)); } - // For efficiency reasons, we add a special case for when there's - // only a single language enabled. - if langs.len() == 1 { - let lang = langs[0]; - return if lang.find_word(first_word).is_some() { - Ok(lang) - } else { - Err(Error::UnknownWord(0)) - }; - } - - // Otherwise we first try wordlists that have guaranteed unique words. + // We first try find the first word in wordlists that + // have guaranteed unique words. for language in langs.iter().filter(|l| l.unique_words()) { if language.find_word(first_word).is_some() { return Ok(*language); @@ -271,21 +261,23 @@ impl Mnemonic { for (idx, word) in words.enumerate() { // Scrap languages that don't have this word. for (i, lang) in langs.iter().enumerate() { - if possible[i] { - possible[i] = lang.find_word(word).is_some(); + possible[i] &= lang.find_word(word).is_some(); + } + + // Get an iterator over remaining possible languages. + let mut iter = possible.iter().zip(langs.iter()).filter(|(p, _)| **p).map(|(_, l)| l); + + match iter.next() { + // If all languages were eliminated, it's an invalid word. + None => return Err(Error::UnknownWord(idx)), + // If not, see if there is a second one remaining. + Some(remaining) => { + if iter.next().is_none() { + // No second remaining, we found our language. + return Ok(*remaining); + } } } - - // If there is just one language left, return it. - let nb_possible = possible.iter().filter(|p| **p).count(); - if nb_possible == 1 { - return Ok(*possible.iter().zip(langs.iter()).find(|&(p, _)| *p).map(|(_, l)| l).unwrap()); - } - - // If all languages were eliminated, it's an invalid word. - if nb_possible == 0 { - return Err(Error::UnknownWord(idx)); - } } return Err(Error::AmbiguousLanguages(AmbiguousLanguages(possible))); From ca454327b2433381e965e06ec957e3f2da0ca63a Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 24 Mar 2021 16:59:32 +0000 Subject: [PATCH 3/8] Differentiate std and core --- .travis.yml | 2 ++ Cargo.toml | 5 +++-- src/language/mod.rs | 6 +++--- src/lib.rs | 36 +++++++++++++++++++++++++++--------- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 50a563a..318d4d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,8 @@ script: - cargo test --verbose - cargo build --verbose --no-default-features - cargo test --verbose --no-default-features + - cargo build --verbose --no-default-features --features core,all-languages + - cargo test --verbose --no-default-features --features core,all-languages - cargo build --verbose --features rand,all-languages - cargo test --verbose --features rand,all-languages # benchmarks diff --git a/Cargo.toml b/Cargo.toml index 7e134e1..c1fc5f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,8 @@ path = "src/lib.rs" [features] default = [ "std" ] -std = [ "serde/std" ] +core = [] +std = [ "core", "unicode-normalization", "serde/std" ] # Note: English is the standard for bip39 so always included chinese-simplified = [] @@ -41,8 +42,8 @@ all-languages = [ [dependencies] bitcoin_hashes = "0.9.4" -unicode-normalization = "=0.1.9" +unicode-normalization = { version = "=0.1.9", optional = true } rand = { version = "0.6.0", optional = true } serde = { version = "1.0", default-features = false, optional = true } diff --git a/src/language/mod.rs b/src/language/mod.rs index 5e9a6bb..587e0b7 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -1,6 +1,6 @@ -#[cfg(feature = "std")] -use std::fmt; +#[cfg(feature = "core")] +use core::fmt; mod english; #[cfg(feature = "chinese-simplified")] @@ -153,7 +153,7 @@ impl Language { } } -#[cfg(feature = "std")] +#[cfg(feature = "core")] impl fmt::Display for Language { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Debug::fmt(self, f) diff --git a/src/lib.rs b/src/lib.rs index f96359d..59c9a51 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,9 @@ #![deny(missing_docs)] #![cfg_attr(all(not(test), not(feature = "std")), no_std)] -#[cfg(any(test, feature = "std"))] pub extern crate core; + +#[cfg(any(test, feature = "std"))] +pub extern crate core; extern crate bitcoin_hashes; #[cfg(feature = "std")] @@ -38,8 +40,11 @@ extern crate rand; #[cfg(feature = "serde")] pub extern crate serde; +#[cfg(feature = "core")] +use core::{fmt, str}; + #[cfg(feature = "std")] -use std::{error, fmt, str}; +use std::error; #[cfg(feature = "std")] use std::borrow::Cow; @@ -75,7 +80,7 @@ impl AmbiguousLanguages { } /// An iterator over the possible languages. - #[cfg(feature = "std")] + #[cfg(feature = "core")] pub fn iter(&self) -> impl Iterator + '_ { Language::all().iter().enumerate().filter(move |(i, _)| self.0[*i]).map(|(_, l)| *l) } @@ -106,7 +111,7 @@ pub enum Error { AmbiguousLanguages(AmbiguousLanguages), } -#[cfg(feature = "std")] +#[cfg(feature = "core")] impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { @@ -120,7 +125,17 @@ impl fmt::Display for Error { "entropy was not between 128-256 bits or not a multiple of 32 bits: {} bits", c, ), Error::InvalidChecksum => write!(f, "the mnemonic has an invalid checksum"), - Error::AmbiguousLanguages(a) => write!(f, "ambiguous word list: {:?}", a.to_vec()), + Error::AmbiguousLanguages(a) => { + write!(f, "ambiguous word list: ")?; + for (i, lang) in a.iter().enumerate() { + if i == 0 { + write!(f, "{}", lang)?; + } else { + write!(f, ", {}", lang)?; + } + } + Ok(()) + } } } } @@ -130,7 +145,7 @@ impl error::Error for Error {} /// A mnemonic code. /// -/// The [std::str::FromStr] implementation will try to determine the language of the +/// The [core::str::FromStr] implementation will try to determine the language of the /// mnemonic from all the supported languages. (Languages have to be explicitly enabled using /// the Cargo features.) /// @@ -443,7 +458,7 @@ impl Mnemonic { } } -#[cfg(feature = "std")] +#[cfg(feature = "core")] impl fmt::Display for Mnemonic { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for i in 0..self.0.len() { @@ -460,12 +475,15 @@ impl fmt::Display for Mnemonic { } } -#[cfg(feature = "std")] +#[cfg(feature = "core")] impl str::FromStr for Mnemonic { type Err = Error; fn from_str(s: &str) -> Result { - Mnemonic::parse(s) + #[cfg(feature = "std")] + { Mnemonic::parse(s) } + #[cfg(not(feature = "std"))] + { Mnemonic::parse_normalized(s) } } } From 8b3e901b35363403600185ebb74da6a99f267c07 Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 24 Mar 2021 18:35:02 +0000 Subject: [PATCH 4/8] Add a generation method that doesn't need rand --- Cargo.toml | 1 + src/lib.rs | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c1fc5f4..0fafd5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ all-languages = [ [dependencies] bitcoin_hashes = "0.9.4" +rand_core = "0.4.0" unicode-normalization = { version = "=0.1.9", optional = true } rand = { version = "0.6.0", optional = true } diff --git a/src/lib.rs b/src/lib.rs index 59c9a51..6812fa4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,8 @@ pub extern crate core; extern crate bitcoin_hashes; +extern crate rand_core; + #[cfg(feature = "std")] extern crate unicode_normalization; @@ -217,21 +219,29 @@ impl Mnemonic { Mnemonic::from_entropy_in(Language::English, entropy) } - /// Generate a new [Mnemonic] in the given language. + /// Generate a new [Mnemonic] in the given language + /// with the given randomness source. /// For the different supported word counts, see documentation on [Mnemonic]. - #[cfg(feature = "rand")] - pub fn generate_in(language: Language, word_count: usize) -> Result { + pub fn generate_in_with(rng: &mut R, language: Language, word_count: usize) -> Result + where R: rand_core::RngCore + rand_core::CryptoRng, + { if word_count < MIN_NB_WORDS || word_count % 6 != 0 || word_count > MAX_NB_WORDS { return Err(Error::BadWordCount(word_count)); } let entropy_bytes = (word_count / 3) * 4; - let mut rng = rand::thread_rng(); let mut entropy = [0u8; (MAX_NB_WORDS / 3) * 4]; - rand::RngCore::fill_bytes(&mut rng, &mut entropy[0..entropy_bytes]); + rand_core::RngCore::fill_bytes(rng, &mut entropy[0..entropy_bytes]); Mnemonic::from_entropy_in(language, &entropy[0..entropy_bytes]) } + /// Generate a new [Mnemonic] in the given language. + /// For the different supported word counts, see documentation on [Mnemonic]. + #[cfg(feature = "rand")] + pub fn generate_in(language: Language, word_count: usize) -> Result { + Mnemonic::generate_in_with(&mut rand::thread_rng(), language, word_count) + } + /// Generate a new [Mnemonic] in English. /// For the different supported word counts, see documentation on [Mnemonic]. #[cfg(feature = "rand")] From 44397cc255e6af1bd6a6ca02dad8727d34f4a0e4 Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 24 Mar 2021 18:36:32 +0000 Subject: [PATCH 5/8] Remove pointless debug_assert --- src/pbkdf2.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pbkdf2.rs b/src/pbkdf2.rs index aadbff5..c0c432a 100644 --- a/src/pbkdf2.rs +++ b/src/pbkdf2.rs @@ -88,8 +88,6 @@ fn create_hmac_engine(mnemonic: &[&'static str]) -> hmac::HmacEngine [u8; 4] { - debug_assert_eq!(::core::mem::size_of::(), 4); // size_of isn't a constfn in 1.22 - let mut res = [0; 4]; for i in 0..4 { res[i] = ((val >> (4 - i - 1)*8) & 0xff) as u8; From 5fe9d2b094a7d8f5a5b4a09d61c2f8f0bfe71d49 Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 24 Mar 2021 18:39:03 +0000 Subject: [PATCH 6/8] Only pass actual mnemonic into pbkdf2 --- src/lib.rs | 3 ++- src/pbkdf2.rs | 9 --------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6812fa4..7e0719d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -406,8 +406,9 @@ impl Mnemonic { const PBKDF2_ROUNDS: usize = 2048; const PBKDF2_BYTES: usize = 64; + let nb_words = self.word_count(); let mut seed = [0u8; PBKDF2_BYTES]; - pbkdf2::pbkdf2(&self.0, normalized_passphrase.as_bytes(), PBKDF2_ROUNDS, &mut seed); + pbkdf2::pbkdf2(&self.0[0..nb_words], normalized_passphrase.as_bytes(), PBKDF2_ROUNDS, &mut seed); seed } diff --git a/src/pbkdf2.rs b/src/pbkdf2.rs index c0c432a..36e9f77 100644 --- a/src/pbkdf2.rs +++ b/src/pbkdf2.rs @@ -8,9 +8,6 @@ fn mnemonic_byte_len(mnemonic: &[&'static str]) -> usize { let mut len = 0; for i in 0..mnemonic.len() { let word = &mnemonic[i]; - if word.is_empty() { - break; - } if i > 0 { len += 1; } @@ -23,9 +20,6 @@ fn mnemonic_byte_len(mnemonic: &[&'static str]) -> usize { fn mnemonic_write_into(mnemonic: &[&'static str], engine: &mut sha512::HashEngine) { for i in 0..mnemonic.len() { let word = &mnemonic[i]; - if word.is_empty() { - break; - } if i > 0 { engine.input(" ".as_bytes()); } @@ -61,9 +55,6 @@ fn create_hmac_engine(mnemonic: &[&'static str]) -> hmac::HmacEngine 0 { ipad[cursor] ^= ' ' as u8; opad[cursor] ^= ' ' as u8; From 0313e97134fde8ef36f1062a7365b1ba6ca720fe Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 24 Mar 2021 19:02:11 +0000 Subject: [PATCH 7/8] Remove core feature --- .travis.yml | 6 ++---- Cargo.toml | 3 +-- src/language/mod.rs | 2 -- src/lib.rs | 5 ----- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 318d4d4..156baa2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,10 +19,8 @@ before_install: script: - cargo build --verbose - cargo test --verbose - - cargo build --verbose --no-default-features - - cargo test --verbose --no-default-features - - cargo build --verbose --no-default-features --features core,all-languages - - cargo test --verbose --no-default-features --features core,all-languages + - cargo build --verbose --no-default-features --features all-languages + - cargo test --verbose --no-default-features --features all-languages - cargo build --verbose --features rand,all-languages - cargo test --verbose --features rand,all-languages # benchmarks diff --git a/Cargo.toml b/Cargo.toml index 0fafd5c..36e2e0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,7 @@ path = "src/lib.rs" [features] default = [ "std" ] -core = [] -std = [ "core", "unicode-normalization", "serde/std" ] +std = [ "unicode-normalization", "serde/std" ] # Note: English is the standard for bip39 so always included chinese-simplified = [] diff --git a/src/language/mod.rs b/src/language/mod.rs index 587e0b7..b9796e4 100644 --- a/src/language/mod.rs +++ b/src/language/mod.rs @@ -1,5 +1,4 @@ -#[cfg(feature = "core")] use core::fmt; mod english; @@ -153,7 +152,6 @@ impl Language { } } -#[cfg(feature = "core")] impl fmt::Display for Language { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Debug::fmt(self, f) diff --git a/src/lib.rs b/src/lib.rs index 7e0719d..3215a1d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,6 @@ extern crate rand; #[cfg(feature = "serde")] pub extern crate serde; -#[cfg(feature = "core")] use core::{fmt, str}; #[cfg(feature = "std")] @@ -82,7 +81,6 @@ impl AmbiguousLanguages { } /// An iterator over the possible languages. - #[cfg(feature = "core")] pub fn iter(&self) -> impl Iterator + '_ { Language::all().iter().enumerate().filter(move |(i, _)| self.0[*i]).map(|(_, l)| *l) } @@ -113,7 +111,6 @@ pub enum Error { AmbiguousLanguages(AmbiguousLanguages), } -#[cfg(feature = "core")] impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { @@ -469,7 +466,6 @@ impl Mnemonic { } } -#[cfg(feature = "core")] impl fmt::Display for Mnemonic { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for i in 0..self.0.len() { @@ -486,7 +482,6 @@ impl fmt::Display for Mnemonic { } } -#[cfg(feature = "core")] impl str::FromStr for Mnemonic { type Err = Error; From 04e139ca27251067c497fc055b50b4bb77a93faf Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 24 Mar 2021 19:09:39 +0000 Subject: [PATCH 8/8] Add generate doctests --- src/lib.rs | 51 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3215a1d..68a8bf8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -219,6 +219,18 @@ impl Mnemonic { /// Generate a new [Mnemonic] in the given language /// with the given randomness source. /// For the different supported word counts, see documentation on [Mnemonic]. + /// + /// Example: + /// + /// ``` + /// extern crate rand; + /// extern crate bip39; + /// + /// use bip39::{Mnemonic, Language}; + /// + /// let mut rng = rand::thread_rng(); + /// let m = Mnemonic::generate_in_with(&mut rng, Language::English, 24).unwrap(); + /// ``` pub fn generate_in_with(rng: &mut R, language: Language, word_count: usize) -> Result where R: rand_core::RngCore + rand_core::CryptoRng, { @@ -234,6 +246,16 @@ impl Mnemonic { /// Generate a new [Mnemonic] in the given language. /// For the different supported word counts, see documentation on [Mnemonic]. + /// + /// Example: + /// + /// ``` + /// extern crate bip39; + /// + /// use bip39::{Mnemonic, Language}; + /// + /// let m = Mnemonic::generate_in(Language::English, 24).unwrap(); + /// ``` #[cfg(feature = "rand")] pub fn generate_in(language: Language, word_count: usize) -> Result { Mnemonic::generate_in_with(&mut rand::thread_rng(), language, word_count) @@ -241,6 +263,16 @@ impl Mnemonic { /// Generate a new [Mnemonic] in English. /// For the different supported word counts, see documentation on [Mnemonic]. + /// + /// Example: + /// + /// ``` + /// extern crate bip39; + /// + /// use bip39::{Mnemonic,}; + /// + /// let m = Mnemonic::generate(24).unwrap(); + /// ``` #[cfg(feature = "rand")] pub fn generate(word_count: usize) -> Result { Mnemonic::generate_in(Language::English, word_count) @@ -499,17 +531,6 @@ mod tests { use bitcoin_hashes::hex::FromHex; - #[cfg(feature = "rand")] - #[test] - fn test_bit_counts() { - let m = Mnemonic::generate(12).unwrap(); - assert_eq!(m.word_count(), 12); - let m = Mnemonic::generate(18).unwrap(); - assert_eq!(m.word_count(), 18); - let m = Mnemonic::generate(24).unwrap(); - assert_eq!(m.word_count(), 24); - } - #[cfg(feature = "rand")] #[test] fn test_language_of() { @@ -540,6 +561,14 @@ mod tests { assert_eq!(amb.iter().collect::>(), present_vec); } + #[cfg(feature = "rand")] + #[test] + fn test_generate() { + let _ = Mnemonic::generate(24).unwrap(); + let _ = Mnemonic::generate_in(Language::English, 24).unwrap(); + let _ = Mnemonic::generate_in_with(&mut rand::thread_rng(), Language::English, 24).unwrap(); + } + #[test] fn test_vectors_english() { // These vectors are tuples of