Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for 12 or 24 words. #1179

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions bip32/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment on lines +18 to +28
Copy link
Member

Choose a reason for hiding this comment

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

Instead of making several Bip39-prefixed variants, I think it would be better to have Bip39(bip39::Error) and a subdivided error type concerned with just those failure modes, even if that's a breaking change.



/// Child number-related errors.
ChildNumber,

Expand All @@ -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"),
Expand Down
1 change: 1 addition & 0 deletions bip32/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,4 @@ type HmacSha512 = hmac::Hmac<sha2::Sha512>;

/// Size of input key material and derived keys.
pub const KEY_SIZE: usize = 32;
pub const WORD_SIZE_24: usize = 32;
104 changes: 71 additions & 33 deletions bip32/src/mnemonic/phrase.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand All @@ -17,35 +20,52 @@ 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)]
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<u8>,

/// 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<u8> = 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<u8> , language: Language) -> Result<Self, Error> {
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).
Expand All @@ -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.
Expand All @@ -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<S>(phrase: S, language: Language) -> Result<Self, Error>
where
S: AsRef<str>,
{

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<u8> {
& self.entropy
}

/// Get the mnemonic phrase as a string reference.
Expand Down
85 changes: 83 additions & 2 deletions bip32/tests/bip39_vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -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]
Expand All @@ -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::<XPrv>().unwrap();
let derived_xprv = XPrv::new(&seed).unwrap();
assert_eq!(expected_xprv, derived_xprv);
}
}