Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

# v1.0.1

- Add `Mnemonic::language` getter.
- Make `Mnemonic::language_of` static method public.
- Change internal representation of `Mnemonic`, making it slightly smaller.

# v1.0.0

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bip39"
version = "1.0.0"
version = "1.0.1"
authors = ["Steven Roose <steven@stevenroose.org>"]
license = "CC0-1.0"
homepage = "https://github.com/rust-bitcoin/rust-bip39/"
Expand Down
4 changes: 2 additions & 2 deletions src/language/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ impl Language {

/// Get the index of the word in the word list.
#[inline]
pub(crate) fn find_word(self, word: &str) -> Option<usize> {
self.word_list().iter().position(|w| *w == word)
pub(crate) fn find_word(self, word: &str) -> Option<u16> {
self.word_list().iter().position(|w| *w == word).map(|i| i as u16)
}
}

Expand Down
52 changes: 34 additions & 18 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ const MIN_NB_WORDS: usize = 12;
/// The maximum number of words in a mnemonic.
const MAX_NB_WORDS: usize = 24;

/// The index used to indicate the mnemonic ended.
const EOF: u16 = u16::max_value();

/// A structured used in the [Error::AmbiguousLanguages] variant that iterates
/// over the possible languages.
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
Expand Down Expand Up @@ -149,7 +152,13 @@ impl error::Error for Error {}
///
/// Supported number of words are 12, 18 and 24.
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Mnemonic([&'static str; MAX_NB_WORDS]);
pub struct Mnemonic {
/// The language the mnemonic.
lang: Language,
/// The indiced of the words.
/// Mnemonics with less than the max nb of words are terminated with EOF.
words: [u16; MAX_NB_WORDS],
}

serde_string_impl!(Mnemonic, "a BIP-39 Mnemonic Code");

Expand Down Expand Up @@ -194,7 +203,7 @@ impl Mnemonic {
bits[8 * nb_bytes + i] = (check[i / 8] & (1 << (7 - (i % 8)))) > 0;
}

let mut words: [&'static str; MAX_NB_WORDS] = Default::default();
let mut words = [EOF; MAX_NB_WORDS];
let nb_words = nb_bytes * 3 / 4;
for i in 0..nb_words {
let mut idx = 0;
Expand All @@ -203,10 +212,13 @@ impl Mnemonic {
idx += 1 << (10 - j);
}
}
words[i] = language.word_list()[idx];
words[i] = idx;
}

Ok(Mnemonic(words))
Ok(Mnemonic {
lang: language,
words: words,
})
}

/// Create a new English [Mnemonic] from the given entropy.
Expand Down Expand Up @@ -282,9 +294,15 @@ impl Mnemonic {
Mnemonic::generate_in(Language::English, word_count)
}

/// Get the language of the [Mnemonic].
pub fn language(&self) -> Language {
self.lang
}

/// Get an iterator over the words.
pub fn word_iter(&self) -> impl Iterator<Item = &'static str> + '_ {
self.0.iter().take_while(|w| !w.is_empty()).map(|w| *w)
pub fn word_iter(&self) -> impl Iterator<Item = &'static str> + Clone + '_ {
let list = self.lang.word_list();
self.words.iter().take_while(|w| **w != EOF).map(move |w| list[*w as usize])
}

/// Determine the language of the mnemonic as a word iterator.
Expand Down Expand Up @@ -352,7 +370,7 @@ impl Mnemonic {
/// word lists. In the extremely unlikely case that a word list can be
/// interpreted in multiple languages, an [Error::AmbiguousLanguages] is
/// returned, containing the possible languages.
fn language_of<S: AsRef<str>>(mnemonic: S) -> Result<Language, Error> {
pub fn language_of<S: AsRef<str>>(mnemonic: S) -> Result<Language, Error> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think your observation from #10 is still true that you can use the narrower error type here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No because here S is a string so it can be an invalid mnemonic. This is the static method used for parsing.

Mnemonic::language_of_iter(mnemonic.as_ref().split_whitespace())
}

Expand All @@ -364,7 +382,7 @@ impl Mnemonic {
}

// Here we will store the eventual words.
let mut words: [&'static str; MAX_NB_WORDS] = Default::default();
let mut words = [EOF; MAX_NB_WORDS];

// And here we keep track of the bits to calculate and validate the checksum.
// We only use `nb_words * 11` elements in this array.
Expand All @@ -373,7 +391,7 @@ impl Mnemonic {
for (i, word) in s.split_whitespace().enumerate() {
let idx = language.find_word(word).ok_or(Error::UnknownWord(i))?;

words[i] = language.word_list()[idx];
words[i] = idx;

for j in 0..11 {
bits[i * 11 + j] = idx >> (10 - j) & 1 == 1;
Expand All @@ -398,7 +416,10 @@ impl Mnemonic {
}
}

Ok(Mnemonic(words))
Ok(Mnemonic {
lang: language,
words: words,
})
}

/// Parse a mnemonic in normalized UTF8.
Expand Down Expand Up @@ -435,18 +456,17 @@ impl Mnemonic {

/// Get the number of words in the mnemonic.
pub fn word_count(&self) -> usize {
self.0.iter().take_while(|w| !w.is_empty()).count()
self.words.iter().take_while(|w| **w != EOF).count()
}

/// Convert to seed bytes with a passphrase in normalized UTF8.
pub fn to_seed_normalized(&self, normalized_passphrase: &str) -> [u8; 64] {
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[0..nb_words],
self.word_iter(),
normalized_passphrase.as_bytes(),
PBKDF2_ROUNDS,
&mut seed,
Expand Down Expand Up @@ -513,11 +533,7 @@ impl Mnemonic {

impl fmt::Display for Mnemonic {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for i in 0..self.0.len() {
let word = &self.0[i];
if word.is_empty() {
break;
}
for (i, word) in self.word_iter().enumerate() {
if i > 0 {
f.write_str(" ")?;
}
Expand Down
27 changes: 16 additions & 11 deletions src/pbkdf2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ use bitcoin_hashes::{hmac, sha512, Hash, HashEngine};
const SALT_PREFIX: &'static str = "mnemonic";

/// Calculate the binary size of the mnemonic.
fn mnemonic_byte_len(mnemonic: &[&'static str]) -> usize {
fn mnemonic_byte_len<M>(mnemonic: M) -> usize
where M: Iterator<Item = &'static str> + Clone,
{
let mut len = 0;
for i in 0..mnemonic.len() {
let word = &mnemonic[i];
for (i, word) in mnemonic.enumerate() {
if i > 0 {
len += 1;
}
Expand All @@ -16,9 +17,10 @@ fn mnemonic_byte_len(mnemonic: &[&'static str]) -> usize {
}

/// Wrote the mnemonic in binary form into the hash engine.
fn mnemonic_write_into(mnemonic: &[&'static str], engine: &mut sha512::HashEngine) {
for i in 0..mnemonic.len() {
let word = &mnemonic[i];
fn mnemonic_write_into<M>(mnemonic: M, engine: &mut sha512::HashEngine)
where M: Iterator<Item = &'static str> + Clone,
{
for (i, word) in mnemonic.enumerate() {
if i > 0 {
engine.input(" ".as_bytes());
}
Expand All @@ -29,14 +31,16 @@ fn mnemonic_write_into(mnemonic: &[&'static str], engine: &mut sha512::HashEngin
/// Create an HMAC engine from the passphrase.
/// We need a special method because we can't allocate a new byte
/// vector for the entire serialized mnemonic.
fn create_hmac_engine(mnemonic: &[&'static str]) -> hmac::HmacEngine<sha512::Hash> {
fn create_hmac_engine<M>(mnemonic: M) -> hmac::HmacEngine<sha512::Hash>
where M: Iterator<Item = &'static str> + Clone,
{
// Inner code is borrowed from the bitcoin_hashes::hmac::HmacEngine::new method.
let mut ipad = [0x36u8; 128];
let mut opad = [0x5cu8; 128];
let mut iengine = sha512::Hash::engine();
let mut oengine = sha512::Hash::engine();

if mnemonic_byte_len(mnemonic) > sha512::HashEngine::BLOCK_SIZE {
if mnemonic_byte_len(mnemonic.clone()) > sha512::HashEngine::BLOCK_SIZE {
let hash = {
let mut engine = sha512::Hash::engine();
mnemonic_write_into(mnemonic, &mut engine);
Expand All @@ -52,8 +56,7 @@ fn create_hmac_engine(mnemonic: &[&'static str]) -> hmac::HmacEngine<sha512::Has
} else {
// First modify the first elements from the prefix.
let mut cursor = 0;
for i in 0..mnemonic.len() {
let word = &mnemonic[i];
for (i, word) in mnemonic.enumerate() {
if i > 0 {
ipad[cursor] ^= ' ' as u8;
opad[cursor] ^= ' ' as u8;
Expand Down Expand Up @@ -93,7 +96,9 @@ fn xor(res: &mut [u8], salt: &[u8]) {
}

/// PBKDF2-HMAC-SHA512 implementation using bitcoin_hashes.
pub(crate) fn pbkdf2(mnemonic: &[&'static str], unprefixed_salt: &[u8], c: usize, res: &mut [u8]) {
pub(crate) fn pbkdf2<M>(mnemonic: M, unprefixed_salt: &[u8], c: usize, res: &mut [u8])
where M: Iterator<Item = &'static str> + Clone,
{
let prf = create_hmac_engine(mnemonic);

for (i, chunk) in res.chunks_mut(sha512::Hash::LEN).enumerate() {
Expand Down