Skip to content

BIP39 Implementation - #644

Closed
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039
Closed

BIP39 Implementation#644
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039

Conversation

@evanlinjin

@evanlinjinevanlinjin commented Jun 29, 2022

Copy link
Copy Markdown
Member

Description

This is a continuation of PR #607 which closes#561

This PR includes commits for own implementation of PBKFD2.
I've also modified the .gitignore, I hope that is okay.

Notes to the reviewers

Although complete, I still have some security concerns for the current implementation (please check my comment below).

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • Add pbkfd2 implementation
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@evanlinjinevanlinjin mentioned this pull request Jun 29, 2022
9 tasks
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 05b53d1 to 61b7deaCompareJune 29, 2022 14:24
@evanlinjinevanlinjin changed the title WIP: pbkfd2 implementation for BIP39BIP-39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjinevanlinjin changed the title BIP-39 Implementation (with own PBKFD2)BIP39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from dce62db to 2136bd2CompareJune 29, 2022 14:34
@evanlinjin
evanlinjin marked this pull request as ready for review June 29, 2022 14:35
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 82da908 to 29ae147CompareJune 29, 2022 15:02
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from d4f35e7 to 0b5c558CompareJune 29, 2022 17:25

@danielabrozzonidanielabrozzoni left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy-pasted some review comments from #607 that it seems still need to be addressed

Comment threadCHANGELOG.md Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
}

/// Convert a mnemonic to a seed with an optional passphrase
fn to_seed(&self, passphrase: Option<String>) -> Seed {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From #607:

@vladimirfomene: It might not be a good idea to change the type of passphrase from &str to option as that has the potential of breaking code which consumes this method.

@atalw: It makes sense to have the passphrase as Option as it really is optional, so if a breaking change is okay we can go ahead with this.


Personally, I agree that we should try not to break the API, and leave P: Into<Cow<'a, str> (https://docs.rs/bip39/latest/src/bip39/lib.rs.html#479-486) here

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

In my opinion, having a P: Into<Cow<'a, str>> makes little sense based on our implementation. I propose just using a &str, this way for most people, the API shouldn't break.

@evanlinjinevanlinjinJul 1, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Changed in dd42594. Let me know if it is sufficient!

pubfnto_seed(&self,passphrase:&str) -> Seed{
letmut seed = [0_u8;SEED_LEN];
pbkdf2::generate_seed(self.word_iter(), passphrase,&mut seed);
seed
}

@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 5 times, most recently from b003391 to 8952808CompareJuly 1, 2022 12:43
atalwand others added 5 commits July 1, 2022 20:46
There are a couple of features that have been implemented in this
commit:
- Parse mnemonic string to Mnenomic type
- Generate Mnemonic from entropy
- Derive seed from Mnemonic (with and without passphrase)
- All language wordlists (with verification test to ensure they were
untampered)
- Mnemonic test vectors from BIP39
- Error handling
Function names have mostly been kept the same to maintain backwards
compatability.
Co-authored-by: Vladimir Fomene <vladimirfomene@gmail.com>
* `Mnemonic::parse_in` now verifies the checksum against the entropy.
* Add test: Make sure `from_entropy_in` produces error if length of
entropy bits is less than 128, greater than 256 and not a multipe of 32.
* Add test: invalid mnemonic sentence.Throw error if mnemonic sentence
is less than 12 words or greater than 24 words or number of words is
not a multiple of six or the contains a word not in wordlist or has
invalid checksum.
Also removed unused dependencies
@evanlinjinevanlinjin changed the title BIP39 Implementation (with own PBKFD2)BIP39 ImplementationJul 1, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 2399c6b to 75fa8f6CompareJuly 2, 2022 02:36
* Introduce `Language::word_map` method for faster word index finding.
* Readability changes to various `Mnemonic` methods and tests.
* Re-introduce various methods back into `Mnemonic`.
* `bip39::Error` no longer includes sensitive information.
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from c4334a1 to cdb3e25CompareJuly 2, 2022 18:23
@evanlinjin

evanlinjin commented Jul 2, 2022

Copy link
Copy Markdown
MemberAuthor

I still have some security concerns regarding the current implementation (although, I am no security expert, just based on what I've read on the internet). I will list them here, and hopefully someone knowledgeable enough could provide some clarity.

  1. Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

  2. Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

Thank you all in advance!

P.S. Test blockchain::esplora::bdk_blockchain_tests::test_sync_stop_gap_20 seems to fail occasionally.

Fixes:
* Fixed implementations of `GeneratableKey` to work for all word lengths
* Fixed example in `rpcwallet`
Changes:
* Added various `derive`s for bip39 structures
* Added `Mnemonic::with_passphrase` method
* Added `TryFrom<uszie>` implementation for `WordCount`
* Introduced `Bip39TestVector` struct for more comprehensive testing
* Various refactoring
CI/CC Changes:
* Added `all-languages` feature to `[package.metadata.doc.rs]`
* Added `all-languages` feature to code coverage and CI tests
@danielabrozzoni

Copy link
Copy Markdown
Contributor

Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

We could look into that, but it's better if we do so in a new PR. This one is already quite big, and the bigger it gets, the more difficult it is to collect reviews :)

Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Another aspect I've been thinking about, is the great majority of the time people will be using English (which shouldn't require Unicode normalization). For the passphrase, we can do a check only (and fail if not normalized).

Since normalization sometimes requires resizing the vector (so it's a heap operation), and for most people, it also means one less dependably.

@evanlinjin

evanlinjin commented Jul 4, 2022

Copy link
Copy Markdown
MemberAuthor

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

Addressed in 08e1cc9.

`Mnemonic` contains sensitive data so we should ensure internal fields
are not easily leaked.
* Explicitly implement `fmt::Debug` and redact all fields.
* Explicitly implement `ToString` instead of `Display`.
* Remove various comparative `derive()`s.
@afilini

Copy link
Copy Markdown
Member

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

@afiliniafilini left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is just a partial review, I still haven't looked at all the files.

I just wanted to post this comments so that you could start thinking about them and see if they make any sense.

/// Password is the UTF8-NFKD-normalized result of mnemonic words separated by space.
fn make_password<'a, W>(words: W) -> String
where
W: Iterator<Item = &'a str>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could define Item as another generic that implements AsRef<str>. This should allow you to pass a vec of strings as well if you want


/// Salt is the UTF8-NFKD-normalized result of (SALT_PREFIX + passphrase).
fn make_salt(passphrase: &str) -> Cow<'static, str> {
let mut salt = Cow::from(SALT_PREFIX);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I understand the small performance benefit of reusing the ref as-is, but I don't think it's worth using Cow here especially considering that moving forward we'd like to avoid using the heap (even if we don't manage to finalize that transition in this PR)

/// Make hmac-sha512 engine from password.
/// The hmac engine is used as the pseudo-random function.
fn make_prf(password: &str) -> HmacPRF {
HmacEngine::new(password.as_bytes())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was gonna comment here that for extra safety we should re-normalize the string, but then I realized: wouldn't it be better to immediately convert strings to &[u8]immediately after normalization?

This would be like a marker for us, anything that's &str or similar is potentially not normalized, but as soon as we are done we just convert to bytes and forget about it.

With this change I guess you would make this function take a &[u8] directly, and do the conversion in the caller which as far as I can see is already normalizing correctly.

/// Generate block (of given block_index) by calculating xor sum of iterations of PRF.
fn xor_sum(hmac_prf: &HmacPRF, salt: &str, iter_count: u32, block_index: u32, block: &mut [u8]) {
// for the first iteration, we concat: salt + block_index (as big-endian bytes)
let mut prev_u = Vec::with_capacity(salt.len() + 4);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since the length is fixed i guess this could also be an array with static length (there should be a constant in bitcoin_hashes for this).

I think the code would probably still look decently good with copy_from_slice: https://doc.rust-lang.org/std/primitive.slice.html#method.copy_from_slice

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

Less than a week. But I'm stuck into multi descriptor wallet business 😅😂

@afilini

Copy link
Copy Markdown
Member

Yes, multi-descriptor is definitely the priority right now. We'll get back to this once you are done there :)

@vladimirfomenevladimirfomene left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for helping us move this forward! Just a couple of questions and comments.

Comment thread.gitignore
*.swp
.idea

# IDE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nits: .idea is for IntelliJ. I don't know if it is necessary to have that IDE comment.

Comment threadsrc/keys/bip39/mod.rs
// parse word indexes and ENT+CS bits from mnemonic words
let parse_result = sentence_words
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not throw an invalid word error here if the word is not in the HashMap? What is the utility of having &utils::U11_EOF as the default value here?

Comment threadsrc/keys/bip39/mod.rs
Comment on lines +164 to +165
let mut word_indexes = Vec::with_capacity(MS_MAX); // word indexes
let mut ent_cs_bits = Vec::with_capacity(MS_MAX * utils::U11_BITS); // ENT+CS bits

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not just use ms as your vector size instead of MS_MAX?

Comment threadsrc/keys/bip39/mod.rs
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))
.try_for_each(|word_index| {
if *word_index > utils::U11_MAX {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that you are getting the word_index from the word_map is there a scenario where the word_index will be greater than utils::U11_MAX. I'm thinking if you throw an error for invalid words there will be no need for this if/else logic.

Comment threadexamples/rpcwallet.rs
Comment on lines +224 to +226
let mnemonic_with_passphrase: GeneratedKey<_, _> =
MnemonicWithPassphrase::generate((WordCount::Words12, Language::English, password))?;
Ok(mnemonic_with_passphrase)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that there is a change in the examples, I believe this will affect users. Is it possible to implement the BIP in such a way that it doesn't change anything for users.

// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It will be great to have a reference to the PBKDF2 RFC link as part of this module's documentation. https://datatracker.ietf.org/doc/html/rfc2898

}

/// Generate word map for given language.
pub fn word_map(&self) -> HashMap<&str, u16> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we write a test for this method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What kind of test are you suggesting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was thinking of writing a test to make sure we have correct word to indices mapping in the hashmap.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Is this a good idea to have it in bdk_core eventually? Or we wanna do key generation outside of core separately?

@danielabrozzoni

Copy link
Copy Markdown
Contributor

We closed #561, let's close this one as well :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

Write own BIP39 implementation

7 participants

@evanlinjin@danielabrozzoni@afilini@rajarshimaitra@vladimirfomene@notmandatory@atalw
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
BIP39 Implementation by evanlinjin · Pull Request #644 · bitcoindevkit/bdk · GitHub
Skip to content

BIP39 Implementation - #644

Closed
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039
Closed

BIP39 Implementation#644
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039

Conversation

@evanlinjin

@evanlinjinevanlinjin commented Jun 29, 2022

Copy link
Copy Markdown
Member

Description

This is a continuation of PR #607 which closes#561

This PR includes commits for own implementation of PBKFD2.
I've also modified the .gitignore, I hope that is okay.

Notes to the reviewers

Although complete, I still have some security concerns for the current implementation (please check my comment below).

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • Add pbkfd2 implementation
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@evanlinjinevanlinjin mentioned this pull request Jun 29, 2022
9 tasks
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 05b53d1 to 61b7deaCompareJune 29, 2022 14:24
@evanlinjinevanlinjin changed the title WIP: pbkfd2 implementation for BIP39BIP-39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjinevanlinjin changed the title BIP-39 Implementation (with own PBKFD2)BIP39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from dce62db to 2136bd2CompareJune 29, 2022 14:34
@evanlinjin
evanlinjin marked this pull request as ready for review June 29, 2022 14:35
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 82da908 to 29ae147CompareJune 29, 2022 15:02
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from d4f35e7 to 0b5c558CompareJune 29, 2022 17:25

@danielabrozzonidanielabrozzoni left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy-pasted some review comments from #607 that it seems still need to be addressed

Comment threadCHANGELOG.md Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
}

/// Convert a mnemonic to a seed with an optional passphrase
fn to_seed(&self, passphrase: Option<String>) -> Seed {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From #607:

@vladimirfomene: It might not be a good idea to change the type of passphrase from &str to option as that has the potential of breaking code which consumes this method.

@atalw: It makes sense to have the passphrase as Option as it really is optional, so if a breaking change is okay we can go ahead with this.


Personally, I agree that we should try not to break the API, and leave P: Into<Cow<'a, str> (https://docs.rs/bip39/latest/src/bip39/lib.rs.html#479-486) here

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

In my opinion, having a P: Into<Cow<'a, str>> makes little sense based on our implementation. I propose just using a &str, this way for most people, the API shouldn't break.

@evanlinjinevanlinjinJul 1, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Changed in dd42594. Let me know if it is sufficient!

pubfnto_seed(&self,passphrase:&str) -> Seed{
letmut seed = [0_u8;SEED_LEN];
pbkdf2::generate_seed(self.word_iter(), passphrase,&mut seed);
seed
}

@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 5 times, most recently from b003391 to 8952808CompareJuly 1, 2022 12:43
atalwand others added 5 commits July 1, 2022 20:46
There are a couple of features that have been implemented in this
commit:
- Parse mnemonic string to Mnenomic type
- Generate Mnemonic from entropy
- Derive seed from Mnemonic (with and without passphrase)
- All language wordlists (with verification test to ensure they were
untampered)
- Mnemonic test vectors from BIP39
- Error handling
Function names have mostly been kept the same to maintain backwards
compatability.
Co-authored-by: Vladimir Fomene <vladimirfomene@gmail.com>
* `Mnemonic::parse_in` now verifies the checksum against the entropy.
* Add test: Make sure `from_entropy_in` produces error if length of
entropy bits is less than 128, greater than 256 and not a multipe of 32.
* Add test: invalid mnemonic sentence.Throw error if mnemonic sentence
is less than 12 words or greater than 24 words or number of words is
not a multiple of six or the contains a word not in wordlist or has
invalid checksum.
Also removed unused dependencies
@evanlinjinevanlinjin changed the title BIP39 Implementation (with own PBKFD2)BIP39 ImplementationJul 1, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 2399c6b to 75fa8f6CompareJuly 2, 2022 02:36
* Introduce `Language::word_map` method for faster word index finding.
* Readability changes to various `Mnemonic` methods and tests.
* Re-introduce various methods back into `Mnemonic`.
* `bip39::Error` no longer includes sensitive information.
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from c4334a1 to cdb3e25CompareJuly 2, 2022 18:23
@evanlinjin

evanlinjin commented Jul 2, 2022

Copy link
Copy Markdown
MemberAuthor

I still have some security concerns regarding the current implementation (although, I am no security expert, just based on what I've read on the internet). I will list them here, and hopefully someone knowledgeable enough could provide some clarity.

  1. Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

  2. Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

Thank you all in advance!

P.S. Test blockchain::esplora::bdk_blockchain_tests::test_sync_stop_gap_20 seems to fail occasionally.

Fixes:
* Fixed implementations of `GeneratableKey` to work for all word lengths
* Fixed example in `rpcwallet`
Changes:
* Added various `derive`s for bip39 structures
* Added `Mnemonic::with_passphrase` method
* Added `TryFrom<uszie>` implementation for `WordCount`
* Introduced `Bip39TestVector` struct for more comprehensive testing
* Various refactoring
CI/CC Changes:
* Added `all-languages` feature to `[package.metadata.doc.rs]`
* Added `all-languages` feature to code coverage and CI tests
@danielabrozzoni

Copy link
Copy Markdown
Contributor

Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

We could look into that, but it's better if we do so in a new PR. This one is already quite big, and the bigger it gets, the more difficult it is to collect reviews :)

Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Another aspect I've been thinking about, is the great majority of the time people will be using English (which shouldn't require Unicode normalization). For the passphrase, we can do a check only (and fail if not normalized).

Since normalization sometimes requires resizing the vector (so it's a heap operation), and for most people, it also means one less dependably.

@evanlinjin

evanlinjin commented Jul 4, 2022

Copy link
Copy Markdown
MemberAuthor

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

Addressed in 08e1cc9.

`Mnemonic` contains sensitive data so we should ensure internal fields
are not easily leaked.
* Explicitly implement `fmt::Debug` and redact all fields.
* Explicitly implement `ToString` instead of `Display`.
* Remove various comparative `derive()`s.
@afilini

Copy link
Copy Markdown
Member

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

@afiliniafilini left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is just a partial review, I still haven't looked at all the files.

I just wanted to post this comments so that you could start thinking about them and see if they make any sense.

/// Password is the UTF8-NFKD-normalized result of mnemonic words separated by space.
fn make_password<'a, W>(words: W) -> String
where
W: Iterator<Item = &'a str>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could define Item as another generic that implements AsRef<str>. This should allow you to pass a vec of strings as well if you want


/// Salt is the UTF8-NFKD-normalized result of (SALT_PREFIX + passphrase).
fn make_salt(passphrase: &str) -> Cow<'static, str> {
let mut salt = Cow::from(SALT_PREFIX);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I understand the small performance benefit of reusing the ref as-is, but I don't think it's worth using Cow here especially considering that moving forward we'd like to avoid using the heap (even if we don't manage to finalize that transition in this PR)

/// Make hmac-sha512 engine from password.
/// The hmac engine is used as the pseudo-random function.
fn make_prf(password: &str) -> HmacPRF {
HmacEngine::new(password.as_bytes())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was gonna comment here that for extra safety we should re-normalize the string, but then I realized: wouldn't it be better to immediately convert strings to &[u8]immediately after normalization?

This would be like a marker for us, anything that's &str or similar is potentially not normalized, but as soon as we are done we just convert to bytes and forget about it.

With this change I guess you would make this function take a &[u8] directly, and do the conversion in the caller which as far as I can see is already normalizing correctly.

/// Generate block (of given block_index) by calculating xor sum of iterations of PRF.
fn xor_sum(hmac_prf: &HmacPRF, salt: &str, iter_count: u32, block_index: u32, block: &mut [u8]) {
// for the first iteration, we concat: salt + block_index (as big-endian bytes)
let mut prev_u = Vec::with_capacity(salt.len() + 4);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since the length is fixed i guess this could also be an array with static length (there should be a constant in bitcoin_hashes for this).

I think the code would probably still look decently good with copy_from_slice: https://doc.rust-lang.org/std/primitive.slice.html#method.copy_from_slice

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

Less than a week. But I'm stuck into multi descriptor wallet business 😅😂

@afilini

Copy link
Copy Markdown
Member

Yes, multi-descriptor is definitely the priority right now. We'll get back to this once you are done there :)

@vladimirfomenevladimirfomene left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for helping us move this forward! Just a couple of questions and comments.

Comment thread.gitignore
*.swp
.idea

# IDE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nits: .idea is for IntelliJ. I don't know if it is necessary to have that IDE comment.

Comment threadsrc/keys/bip39/mod.rs
// parse word indexes and ENT+CS bits from mnemonic words
let parse_result = sentence_words
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not throw an invalid word error here if the word is not in the HashMap? What is the utility of having &utils::U11_EOF as the default value here?

Comment threadsrc/keys/bip39/mod.rs
Comment on lines +164 to +165
let mut word_indexes = Vec::with_capacity(MS_MAX); // word indexes
let mut ent_cs_bits = Vec::with_capacity(MS_MAX * utils::U11_BITS); // ENT+CS bits

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not just use ms as your vector size instead of MS_MAX?

Comment threadsrc/keys/bip39/mod.rs
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))
.try_for_each(|word_index| {
if *word_index > utils::U11_MAX {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that you are getting the word_index from the word_map is there a scenario where the word_index will be greater than utils::U11_MAX. I'm thinking if you throw an error for invalid words there will be no need for this if/else logic.

Comment threadexamples/rpcwallet.rs
Comment on lines +224 to +226
let mnemonic_with_passphrase: GeneratedKey<_, _> =
MnemonicWithPassphrase::generate((WordCount::Words12, Language::English, password))?;
Ok(mnemonic_with_passphrase)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that there is a change in the examples, I believe this will affect users. Is it possible to implement the BIP in such a way that it doesn't change anything for users.

// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It will be great to have a reference to the PBKDF2 RFC link as part of this module's documentation. https://datatracker.ietf.org/doc/html/rfc2898

}

/// Generate word map for given language.
pub fn word_map(&self) -> HashMap<&str, u16> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we write a test for this method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What kind of test are you suggesting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was thinking of writing a test to make sure we have correct word to indices mapping in the hashmap.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Is this a good idea to have it in bdk_core eventually? Or we wanna do key generation outside of core separately?

@danielabrozzoni

Copy link
Copy Markdown
Contributor

We closed #561, let's close this one as well :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

Write own BIP39 implementation

7 participants

@evanlinjin@danielabrozzoni@afilini@rajarshimaitra@vladimirfomene@notmandatory@atalw
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' BIP39 Implementation by evanlinjin · Pull Request #644 · bitcoindevkit/bdk · GitHub
Skip to content

BIP39 Implementation - #644

Closed
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039
Closed

BIP39 Implementation#644
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039

Conversation

@evanlinjin

@evanlinjinevanlinjin commented Jun 29, 2022

Copy link
Copy Markdown
Member

Description

This is a continuation of PR #607 which closes#561

This PR includes commits for own implementation of PBKFD2.
I've also modified the .gitignore, I hope that is okay.

Notes to the reviewers

Although complete, I still have some security concerns for the current implementation (please check my comment below).

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • Add pbkfd2 implementation
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@evanlinjinevanlinjin mentioned this pull request Jun 29, 2022
9 tasks
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 05b53d1 to 61b7deaCompareJune 29, 2022 14:24
@evanlinjinevanlinjin changed the title WIP: pbkfd2 implementation for BIP39BIP-39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjinevanlinjin changed the title BIP-39 Implementation (with own PBKFD2)BIP39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from dce62db to 2136bd2CompareJune 29, 2022 14:34
@evanlinjin
evanlinjin marked this pull request as ready for review June 29, 2022 14:35
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 82da908 to 29ae147CompareJune 29, 2022 15:02
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from d4f35e7 to 0b5c558CompareJune 29, 2022 17:25

@danielabrozzonidanielabrozzoni left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy-pasted some review comments from #607 that it seems still need to be addressed

Comment threadCHANGELOG.md Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
}

/// Convert a mnemonic to a seed with an optional passphrase
fn to_seed(&self, passphrase: Option<String>) -> Seed {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From #607:

@vladimirfomene: It might not be a good idea to change the type of passphrase from &str to option as that has the potential of breaking code which consumes this method.

@atalw: It makes sense to have the passphrase as Option as it really is optional, so if a breaking change is okay we can go ahead with this.


Personally, I agree that we should try not to break the API, and leave P: Into<Cow<'a, str> (https://docs.rs/bip39/latest/src/bip39/lib.rs.html#479-486) here

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

In my opinion, having a P: Into<Cow<'a, str>> makes little sense based on our implementation. I propose just using a &str, this way for most people, the API shouldn't break.

@evanlinjinevanlinjinJul 1, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Changed in dd42594. Let me know if it is sufficient!

pubfnto_seed(&self,passphrase:&str) -> Seed{
letmut seed = [0_u8;SEED_LEN];
pbkdf2::generate_seed(self.word_iter(), passphrase,&mut seed);
seed
}

@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 5 times, most recently from b003391 to 8952808CompareJuly 1, 2022 12:43
atalwand others added 5 commits July 1, 2022 20:46
There are a couple of features that have been implemented in this
commit:
- Parse mnemonic string to Mnenomic type
- Generate Mnemonic from entropy
- Derive seed from Mnemonic (with and without passphrase)
- All language wordlists (with verification test to ensure they were
untampered)
- Mnemonic test vectors from BIP39
- Error handling
Function names have mostly been kept the same to maintain backwards
compatability.
Co-authored-by: Vladimir Fomene <vladimirfomene@gmail.com>
* `Mnemonic::parse_in` now verifies the checksum against the entropy.
* Add test: Make sure `from_entropy_in` produces error if length of
entropy bits is less than 128, greater than 256 and not a multipe of 32.
* Add test: invalid mnemonic sentence.Throw error if mnemonic sentence
is less than 12 words or greater than 24 words or number of words is
not a multiple of six or the contains a word not in wordlist or has
invalid checksum.
Also removed unused dependencies
@evanlinjinevanlinjin changed the title BIP39 Implementation (with own PBKFD2)BIP39 ImplementationJul 1, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 2399c6b to 75fa8f6CompareJuly 2, 2022 02:36
* Introduce `Language::word_map` method for faster word index finding.
* Readability changes to various `Mnemonic` methods and tests.
* Re-introduce various methods back into `Mnemonic`.
* `bip39::Error` no longer includes sensitive information.
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from c4334a1 to cdb3e25CompareJuly 2, 2022 18:23
@evanlinjin

evanlinjin commented Jul 2, 2022

Copy link
Copy Markdown
MemberAuthor

I still have some security concerns regarding the current implementation (although, I am no security expert, just based on what I've read on the internet). I will list them here, and hopefully someone knowledgeable enough could provide some clarity.

  1. Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

  2. Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

Thank you all in advance!

P.S. Test blockchain::esplora::bdk_blockchain_tests::test_sync_stop_gap_20 seems to fail occasionally.

Fixes:
* Fixed implementations of `GeneratableKey` to work for all word lengths
* Fixed example in `rpcwallet`
Changes:
* Added various `derive`s for bip39 structures
* Added `Mnemonic::with_passphrase` method
* Added `TryFrom<uszie>` implementation for `WordCount`
* Introduced `Bip39TestVector` struct for more comprehensive testing
* Various refactoring
CI/CC Changes:
* Added `all-languages` feature to `[package.metadata.doc.rs]`
* Added `all-languages` feature to code coverage and CI tests
@danielabrozzoni

Copy link
Copy Markdown
Contributor

Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

We could look into that, but it's better if we do so in a new PR. This one is already quite big, and the bigger it gets, the more difficult it is to collect reviews :)

Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Another aspect I've been thinking about, is the great majority of the time people will be using English (which shouldn't require Unicode normalization). For the passphrase, we can do a check only (and fail if not normalized).

Since normalization sometimes requires resizing the vector (so it's a heap operation), and for most people, it also means one less dependably.

@evanlinjin

evanlinjin commented Jul 4, 2022

Copy link
Copy Markdown
MemberAuthor

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

Addressed in 08e1cc9.

`Mnemonic` contains sensitive data so we should ensure internal fields
are not easily leaked.
* Explicitly implement `fmt::Debug` and redact all fields.
* Explicitly implement `ToString` instead of `Display`.
* Remove various comparative `derive()`s.
@afilini

Copy link
Copy Markdown
Member

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

@afiliniafilini left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is just a partial review, I still haven't looked at all the files.

I just wanted to post this comments so that you could start thinking about them and see if they make any sense.

/// Password is the UTF8-NFKD-normalized result of mnemonic words separated by space.
fn make_password<'a, W>(words: W) -> String
where
W: Iterator<Item = &'a str>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could define Item as another generic that implements AsRef<str>. This should allow you to pass a vec of strings as well if you want


/// Salt is the UTF8-NFKD-normalized result of (SALT_PREFIX + passphrase).
fn make_salt(passphrase: &str) -> Cow<'static, str> {
let mut salt = Cow::from(SALT_PREFIX);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I understand the small performance benefit of reusing the ref as-is, but I don't think it's worth using Cow here especially considering that moving forward we'd like to avoid using the heap (even if we don't manage to finalize that transition in this PR)

/// Make hmac-sha512 engine from password.
/// The hmac engine is used as the pseudo-random function.
fn make_prf(password: &str) -> HmacPRF {
HmacEngine::new(password.as_bytes())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was gonna comment here that for extra safety we should re-normalize the string, but then I realized: wouldn't it be better to immediately convert strings to &[u8]immediately after normalization?

This would be like a marker for us, anything that's &str or similar is potentially not normalized, but as soon as we are done we just convert to bytes and forget about it.

With this change I guess you would make this function take a &[u8] directly, and do the conversion in the caller which as far as I can see is already normalizing correctly.

/// Generate block (of given block_index) by calculating xor sum of iterations of PRF.
fn xor_sum(hmac_prf: &HmacPRF, salt: &str, iter_count: u32, block_index: u32, block: &mut [u8]) {
// for the first iteration, we concat: salt + block_index (as big-endian bytes)
let mut prev_u = Vec::with_capacity(salt.len() + 4);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since the length is fixed i guess this could also be an array with static length (there should be a constant in bitcoin_hashes for this).

I think the code would probably still look decently good with copy_from_slice: https://doc.rust-lang.org/std/primitive.slice.html#method.copy_from_slice

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

Less than a week. But I'm stuck into multi descriptor wallet business 😅😂

@afilini

Copy link
Copy Markdown
Member

Yes, multi-descriptor is definitely the priority right now. We'll get back to this once you are done there :)

@vladimirfomenevladimirfomene left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for helping us move this forward! Just a couple of questions and comments.

Comment thread.gitignore
*.swp
.idea

# IDE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nits: .idea is for IntelliJ. I don't know if it is necessary to have that IDE comment.

Comment threadsrc/keys/bip39/mod.rs
// parse word indexes and ENT+CS bits from mnemonic words
let parse_result = sentence_words
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not throw an invalid word error here if the word is not in the HashMap? What is the utility of having &utils::U11_EOF as the default value here?

Comment threadsrc/keys/bip39/mod.rs
Comment on lines +164 to +165
let mut word_indexes = Vec::with_capacity(MS_MAX); // word indexes
let mut ent_cs_bits = Vec::with_capacity(MS_MAX * utils::U11_BITS); // ENT+CS bits

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not just use ms as your vector size instead of MS_MAX?

Comment threadsrc/keys/bip39/mod.rs
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))
.try_for_each(|word_index| {
if *word_index > utils::U11_MAX {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that you are getting the word_index from the word_map is there a scenario where the word_index will be greater than utils::U11_MAX. I'm thinking if you throw an error for invalid words there will be no need for this if/else logic.

Comment threadexamples/rpcwallet.rs
Comment on lines +224 to +226
let mnemonic_with_passphrase: GeneratedKey<_, _> =
MnemonicWithPassphrase::generate((WordCount::Words12, Language::English, password))?;
Ok(mnemonic_with_passphrase)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that there is a change in the examples, I believe this will affect users. Is it possible to implement the BIP in such a way that it doesn't change anything for users.

// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It will be great to have a reference to the PBKDF2 RFC link as part of this module's documentation. https://datatracker.ietf.org/doc/html/rfc2898

}

/// Generate word map for given language.
pub fn word_map(&self) -> HashMap<&str, u16> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we write a test for this method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What kind of test are you suggesting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was thinking of writing a test to make sure we have correct word to indices mapping in the hashmap.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Is this a good idea to have it in bdk_core eventually? Or we wanna do key generation outside of core separately?

@danielabrozzoni

Copy link
Copy Markdown
Contributor

We closed #561, let's close this one as well :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

Write own BIP39 implementation

7 participants

@evanlinjin@danielabrozzoni@afilini@rajarshimaitra@vladimirfomene@notmandatory@atalw
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' BIP39 Implementation by evanlinjin · Pull Request #644 · bitcoindevkit/bdk · GitHub
Skip to content

BIP39 Implementation - #644

Closed
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039
Closed

BIP39 Implementation#644
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039

Conversation

@evanlinjin

@evanlinjinevanlinjin commented Jun 29, 2022

Copy link
Copy Markdown
Member

Description

This is a continuation of PR #607 which closes#561

This PR includes commits for own implementation of PBKFD2.
I've also modified the .gitignore, I hope that is okay.

Notes to the reviewers

Although complete, I still have some security concerns for the current implementation (please check my comment below).

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • Add pbkfd2 implementation
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@evanlinjinevanlinjin mentioned this pull request Jun 29, 2022
9 tasks
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 05b53d1 to 61b7deaCompareJune 29, 2022 14:24
@evanlinjinevanlinjin changed the title WIP: pbkfd2 implementation for BIP39BIP-39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjinevanlinjin changed the title BIP-39 Implementation (with own PBKFD2)BIP39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from dce62db to 2136bd2CompareJune 29, 2022 14:34
@evanlinjin
evanlinjin marked this pull request as ready for review June 29, 2022 14:35
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 82da908 to 29ae147CompareJune 29, 2022 15:02
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from d4f35e7 to 0b5c558CompareJune 29, 2022 17:25

@danielabrozzonidanielabrozzoni left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy-pasted some review comments from #607 that it seems still need to be addressed

Comment threadCHANGELOG.md Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
}

/// Convert a mnemonic to a seed with an optional passphrase
fn to_seed(&self, passphrase: Option<String>) -> Seed {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From #607:

@vladimirfomene: It might not be a good idea to change the type of passphrase from &str to option as that has the potential of breaking code which consumes this method.

@atalw: It makes sense to have the passphrase as Option as it really is optional, so if a breaking change is okay we can go ahead with this.


Personally, I agree that we should try not to break the API, and leave P: Into<Cow<'a, str> (https://docs.rs/bip39/latest/src/bip39/lib.rs.html#479-486) here

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

In my opinion, having a P: Into<Cow<'a, str>> makes little sense based on our implementation. I propose just using a &str, this way for most people, the API shouldn't break.

@evanlinjinevanlinjinJul 1, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Changed in dd42594. Let me know if it is sufficient!

pubfnto_seed(&self,passphrase:&str) -> Seed{
letmut seed = [0_u8;SEED_LEN];
pbkdf2::generate_seed(self.word_iter(), passphrase,&mut seed);
seed
}

@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 5 times, most recently from b003391 to 8952808CompareJuly 1, 2022 12:43
atalwand others added 5 commits July 1, 2022 20:46
There are a couple of features that have been implemented in this
commit:
- Parse mnemonic string to Mnenomic type
- Generate Mnemonic from entropy
- Derive seed from Mnemonic (with and without passphrase)
- All language wordlists (with verification test to ensure they were
untampered)
- Mnemonic test vectors from BIP39
- Error handling
Function names have mostly been kept the same to maintain backwards
compatability.
Co-authored-by: Vladimir Fomene <vladimirfomene@gmail.com>
* `Mnemonic::parse_in` now verifies the checksum against the entropy.
* Add test: Make sure `from_entropy_in` produces error if length of
entropy bits is less than 128, greater than 256 and not a multipe of 32.
* Add test: invalid mnemonic sentence.Throw error if mnemonic sentence
is less than 12 words or greater than 24 words or number of words is
not a multiple of six or the contains a word not in wordlist or has
invalid checksum.
Also removed unused dependencies
@evanlinjinevanlinjin changed the title BIP39 Implementation (with own PBKFD2)BIP39 ImplementationJul 1, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 2399c6b to 75fa8f6CompareJuly 2, 2022 02:36
* Introduce `Language::word_map` method for faster word index finding.
* Readability changes to various `Mnemonic` methods and tests.
* Re-introduce various methods back into `Mnemonic`.
* `bip39::Error` no longer includes sensitive information.
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from c4334a1 to cdb3e25CompareJuly 2, 2022 18:23
@evanlinjin

evanlinjin commented Jul 2, 2022

Copy link
Copy Markdown
MemberAuthor

I still have some security concerns regarding the current implementation (although, I am no security expert, just based on what I've read on the internet). I will list them here, and hopefully someone knowledgeable enough could provide some clarity.

  1. Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

  2. Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

Thank you all in advance!

P.S. Test blockchain::esplora::bdk_blockchain_tests::test_sync_stop_gap_20 seems to fail occasionally.

Fixes:
* Fixed implementations of `GeneratableKey` to work for all word lengths
* Fixed example in `rpcwallet`
Changes:
* Added various `derive`s for bip39 structures
* Added `Mnemonic::with_passphrase` method
* Added `TryFrom<uszie>` implementation for `WordCount`
* Introduced `Bip39TestVector` struct for more comprehensive testing
* Various refactoring
CI/CC Changes:
* Added `all-languages` feature to `[package.metadata.doc.rs]`
* Added `all-languages` feature to code coverage and CI tests
@danielabrozzoni

Copy link
Copy Markdown
Contributor

Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

We could look into that, but it's better if we do so in a new PR. This one is already quite big, and the bigger it gets, the more difficult it is to collect reviews :)

Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Another aspect I've been thinking about, is the great majority of the time people will be using English (which shouldn't require Unicode normalization). For the passphrase, we can do a check only (and fail if not normalized).

Since normalization sometimes requires resizing the vector (so it's a heap operation), and for most people, it also means one less dependably.

@evanlinjin

evanlinjin commented Jul 4, 2022

Copy link
Copy Markdown
MemberAuthor

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

Addressed in 08e1cc9.

`Mnemonic` contains sensitive data so we should ensure internal fields
are not easily leaked.
* Explicitly implement `fmt::Debug` and redact all fields.
* Explicitly implement `ToString` instead of `Display`.
* Remove various comparative `derive()`s.
@afilini

Copy link
Copy Markdown
Member

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

@afiliniafilini left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is just a partial review, I still haven't looked at all the files.

I just wanted to post this comments so that you could start thinking about them and see if they make any sense.

/// Password is the UTF8-NFKD-normalized result of mnemonic words separated by space.
fn make_password<'a, W>(words: W) -> String
where
W: Iterator<Item = &'a str>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could define Item as another generic that implements AsRef<str>. This should allow you to pass a vec of strings as well if you want


/// Salt is the UTF8-NFKD-normalized result of (SALT_PREFIX + passphrase).
fn make_salt(passphrase: &str) -> Cow<'static, str> {
let mut salt = Cow::from(SALT_PREFIX);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I understand the small performance benefit of reusing the ref as-is, but I don't think it's worth using Cow here especially considering that moving forward we'd like to avoid using the heap (even if we don't manage to finalize that transition in this PR)

/// Make hmac-sha512 engine from password.
/// The hmac engine is used as the pseudo-random function.
fn make_prf(password: &str) -> HmacPRF {
HmacEngine::new(password.as_bytes())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was gonna comment here that for extra safety we should re-normalize the string, but then I realized: wouldn't it be better to immediately convert strings to &[u8]immediately after normalization?

This would be like a marker for us, anything that's &str or similar is potentially not normalized, but as soon as we are done we just convert to bytes and forget about it.

With this change I guess you would make this function take a &[u8] directly, and do the conversion in the caller which as far as I can see is already normalizing correctly.

/// Generate block (of given block_index) by calculating xor sum of iterations of PRF.
fn xor_sum(hmac_prf: &HmacPRF, salt: &str, iter_count: u32, block_index: u32, block: &mut [u8]) {
// for the first iteration, we concat: salt + block_index (as big-endian bytes)
let mut prev_u = Vec::with_capacity(salt.len() + 4);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since the length is fixed i guess this could also be an array with static length (there should be a constant in bitcoin_hashes for this).

I think the code would probably still look decently good with copy_from_slice: https://doc.rust-lang.org/std/primitive.slice.html#method.copy_from_slice

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

Less than a week. But I'm stuck into multi descriptor wallet business 😅😂

@afilini

Copy link
Copy Markdown
Member

Yes, multi-descriptor is definitely the priority right now. We'll get back to this once you are done there :)

@vladimirfomenevladimirfomene left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for helping us move this forward! Just a couple of questions and comments.

Comment thread.gitignore
*.swp
.idea

# IDE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nits: .idea is for IntelliJ. I don't know if it is necessary to have that IDE comment.

Comment threadsrc/keys/bip39/mod.rs
// parse word indexes and ENT+CS bits from mnemonic words
let parse_result = sentence_words
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not throw an invalid word error here if the word is not in the HashMap? What is the utility of having &utils::U11_EOF as the default value here?

Comment threadsrc/keys/bip39/mod.rs
Comment on lines +164 to +165
let mut word_indexes = Vec::with_capacity(MS_MAX); // word indexes
let mut ent_cs_bits = Vec::with_capacity(MS_MAX * utils::U11_BITS); // ENT+CS bits

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not just use ms as your vector size instead of MS_MAX?

Comment threadsrc/keys/bip39/mod.rs
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))
.try_for_each(|word_index| {
if *word_index > utils::U11_MAX {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that you are getting the word_index from the word_map is there a scenario where the word_index will be greater than utils::U11_MAX. I'm thinking if you throw an error for invalid words there will be no need for this if/else logic.

Comment threadexamples/rpcwallet.rs
Comment on lines +224 to +226
let mnemonic_with_passphrase: GeneratedKey<_, _> =
MnemonicWithPassphrase::generate((WordCount::Words12, Language::English, password))?;
Ok(mnemonic_with_passphrase)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that there is a change in the examples, I believe this will affect users. Is it possible to implement the BIP in such a way that it doesn't change anything for users.

// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It will be great to have a reference to the PBKDF2 RFC link as part of this module's documentation. https://datatracker.ietf.org/doc/html/rfc2898

}

/// Generate word map for given language.
pub fn word_map(&self) -> HashMap<&str, u16> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we write a test for this method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What kind of test are you suggesting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was thinking of writing a test to make sure we have correct word to indices mapping in the hashmap.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Is this a good idea to have it in bdk_core eventually? Or we wanna do key generation outside of core separately?

@danielabrozzoni

Copy link
Copy Markdown
Contributor

We closed #561, let's close this one as well :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

Write own BIP39 implementation

7 participants

@evanlinjin@danielabrozzoni@afilini@rajarshimaitra@vladimirfomene@notmandatory@atalw
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' BIP39 Implementation by evanlinjin · Pull Request #644 · bitcoindevkit/bdk · GitHub
Skip to content

BIP39 Implementation - #644

Closed
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039
Closed

BIP39 Implementation#644
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039

Conversation

@evanlinjin

@evanlinjinevanlinjin commented Jun 29, 2022

Copy link
Copy Markdown
Member

Description

This is a continuation of PR #607 which closes#561

This PR includes commits for own implementation of PBKFD2.
I've also modified the .gitignore, I hope that is okay.

Notes to the reviewers

Although complete, I still have some security concerns for the current implementation (please check my comment below).

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • Add pbkfd2 implementation
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@evanlinjinevanlinjin mentioned this pull request Jun 29, 2022
9 tasks
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 05b53d1 to 61b7deaCompareJune 29, 2022 14:24
@evanlinjinevanlinjin changed the title WIP: pbkfd2 implementation for BIP39BIP-39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjinevanlinjin changed the title BIP-39 Implementation (with own PBKFD2)BIP39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from dce62db to 2136bd2CompareJune 29, 2022 14:34
@evanlinjin
evanlinjin marked this pull request as ready for review June 29, 2022 14:35
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 82da908 to 29ae147CompareJune 29, 2022 15:02
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from d4f35e7 to 0b5c558CompareJune 29, 2022 17:25

@danielabrozzonidanielabrozzoni left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy-pasted some review comments from #607 that it seems still need to be addressed

Comment threadCHANGELOG.md Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
}

/// Convert a mnemonic to a seed with an optional passphrase
fn to_seed(&self, passphrase: Option<String>) -> Seed {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From #607:

@vladimirfomene: It might not be a good idea to change the type of passphrase from &str to option as that has the potential of breaking code which consumes this method.

@atalw: It makes sense to have the passphrase as Option as it really is optional, so if a breaking change is okay we can go ahead with this.


Personally, I agree that we should try not to break the API, and leave P: Into<Cow<'a, str> (https://docs.rs/bip39/latest/src/bip39/lib.rs.html#479-486) here

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

In my opinion, having a P: Into<Cow<'a, str>> makes little sense based on our implementation. I propose just using a &str, this way for most people, the API shouldn't break.

@evanlinjinevanlinjinJul 1, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Changed in dd42594. Let me know if it is sufficient!

pubfnto_seed(&self,passphrase:&str) -> Seed{
letmut seed = [0_u8;SEED_LEN];
pbkdf2::generate_seed(self.word_iter(), passphrase,&mut seed);
seed
}

@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 5 times, most recently from b003391 to 8952808CompareJuly 1, 2022 12:43
atalwand others added 5 commits July 1, 2022 20:46
There are a couple of features that have been implemented in this
commit:
- Parse mnemonic string to Mnenomic type
- Generate Mnemonic from entropy
- Derive seed from Mnemonic (with and without passphrase)
- All language wordlists (with verification test to ensure they were
untampered)
- Mnemonic test vectors from BIP39
- Error handling
Function names have mostly been kept the same to maintain backwards
compatability.
Co-authored-by: Vladimir Fomene <vladimirfomene@gmail.com>
* `Mnemonic::parse_in` now verifies the checksum against the entropy.
* Add test: Make sure `from_entropy_in` produces error if length of
entropy bits is less than 128, greater than 256 and not a multipe of 32.
* Add test: invalid mnemonic sentence.Throw error if mnemonic sentence
is less than 12 words or greater than 24 words or number of words is
not a multiple of six or the contains a word not in wordlist or has
invalid checksum.
Also removed unused dependencies
@evanlinjinevanlinjin changed the title BIP39 Implementation (with own PBKFD2)BIP39 ImplementationJul 1, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 2399c6b to 75fa8f6CompareJuly 2, 2022 02:36
* Introduce `Language::word_map` method for faster word index finding.
* Readability changes to various `Mnemonic` methods and tests.
* Re-introduce various methods back into `Mnemonic`.
* `bip39::Error` no longer includes sensitive information.
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from c4334a1 to cdb3e25CompareJuly 2, 2022 18:23
@evanlinjin

evanlinjin commented Jul 2, 2022

Copy link
Copy Markdown
MemberAuthor

I still have some security concerns regarding the current implementation (although, I am no security expert, just based on what I've read on the internet). I will list them here, and hopefully someone knowledgeable enough could provide some clarity.

  1. Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

  2. Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

Thank you all in advance!

P.S. Test blockchain::esplora::bdk_blockchain_tests::test_sync_stop_gap_20 seems to fail occasionally.

Fixes:
* Fixed implementations of `GeneratableKey` to work for all word lengths
* Fixed example in `rpcwallet`
Changes:
* Added various `derive`s for bip39 structures
* Added `Mnemonic::with_passphrase` method
* Added `TryFrom<uszie>` implementation for `WordCount`
* Introduced `Bip39TestVector` struct for more comprehensive testing
* Various refactoring
CI/CC Changes:
* Added `all-languages` feature to `[package.metadata.doc.rs]`
* Added `all-languages` feature to code coverage and CI tests
@danielabrozzoni

Copy link
Copy Markdown
Contributor

Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

We could look into that, but it's better if we do so in a new PR. This one is already quite big, and the bigger it gets, the more difficult it is to collect reviews :)

Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Another aspect I've been thinking about, is the great majority of the time people will be using English (which shouldn't require Unicode normalization). For the passphrase, we can do a check only (and fail if not normalized).

Since normalization sometimes requires resizing the vector (so it's a heap operation), and for most people, it also means one less dependably.

@evanlinjin

evanlinjin commented Jul 4, 2022

Copy link
Copy Markdown
MemberAuthor

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

Addressed in 08e1cc9.

`Mnemonic` contains sensitive data so we should ensure internal fields
are not easily leaked.
* Explicitly implement `fmt::Debug` and redact all fields.
* Explicitly implement `ToString` instead of `Display`.
* Remove various comparative `derive()`s.
@afilini

Copy link
Copy Markdown
Member

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

@afiliniafilini left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is just a partial review, I still haven't looked at all the files.

I just wanted to post this comments so that you could start thinking about them and see if they make any sense.

/// Password is the UTF8-NFKD-normalized result of mnemonic words separated by space.
fn make_password<'a, W>(words: W) -> String
where
W: Iterator<Item = &'a str>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could define Item as another generic that implements AsRef<str>. This should allow you to pass a vec of strings as well if you want


/// Salt is the UTF8-NFKD-normalized result of (SALT_PREFIX + passphrase).
fn make_salt(passphrase: &str) -> Cow<'static, str> {
let mut salt = Cow::from(SALT_PREFIX);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I understand the small performance benefit of reusing the ref as-is, but I don't think it's worth using Cow here especially considering that moving forward we'd like to avoid using the heap (even if we don't manage to finalize that transition in this PR)

/// Make hmac-sha512 engine from password.
/// The hmac engine is used as the pseudo-random function.
fn make_prf(password: &str) -> HmacPRF {
HmacEngine::new(password.as_bytes())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was gonna comment here that for extra safety we should re-normalize the string, but then I realized: wouldn't it be better to immediately convert strings to &[u8]immediately after normalization?

This would be like a marker for us, anything that's &str or similar is potentially not normalized, but as soon as we are done we just convert to bytes and forget about it.

With this change I guess you would make this function take a &[u8] directly, and do the conversion in the caller which as far as I can see is already normalizing correctly.

/// Generate block (of given block_index) by calculating xor sum of iterations of PRF.
fn xor_sum(hmac_prf: &HmacPRF, salt: &str, iter_count: u32, block_index: u32, block: &mut [u8]) {
// for the first iteration, we concat: salt + block_index (as big-endian bytes)
let mut prev_u = Vec::with_capacity(salt.len() + 4);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since the length is fixed i guess this could also be an array with static length (there should be a constant in bitcoin_hashes for this).

I think the code would probably still look decently good with copy_from_slice: https://doc.rust-lang.org/std/primitive.slice.html#method.copy_from_slice

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

Less than a week. But I'm stuck into multi descriptor wallet business 😅😂

@afilini

Copy link
Copy Markdown
Member

Yes, multi-descriptor is definitely the priority right now. We'll get back to this once you are done there :)

@vladimirfomenevladimirfomene left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for helping us move this forward! Just a couple of questions and comments.

Comment thread.gitignore
*.swp
.idea

# IDE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nits: .idea is for IntelliJ. I don't know if it is necessary to have that IDE comment.

Comment threadsrc/keys/bip39/mod.rs
// parse word indexes and ENT+CS bits from mnemonic words
let parse_result = sentence_words
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not throw an invalid word error here if the word is not in the HashMap? What is the utility of having &utils::U11_EOF as the default value here?

Comment threadsrc/keys/bip39/mod.rs
Comment on lines +164 to +165
let mut word_indexes = Vec::with_capacity(MS_MAX); // word indexes
let mut ent_cs_bits = Vec::with_capacity(MS_MAX * utils::U11_BITS); // ENT+CS bits

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not just use ms as your vector size instead of MS_MAX?

Comment threadsrc/keys/bip39/mod.rs
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))
.try_for_each(|word_index| {
if *word_index > utils::U11_MAX {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that you are getting the word_index from the word_map is there a scenario where the word_index will be greater than utils::U11_MAX. I'm thinking if you throw an error for invalid words there will be no need for this if/else logic.

Comment threadexamples/rpcwallet.rs
Comment on lines +224 to +226
let mnemonic_with_passphrase: GeneratedKey<_, _> =
MnemonicWithPassphrase::generate((WordCount::Words12, Language::English, password))?;
Ok(mnemonic_with_passphrase)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that there is a change in the examples, I believe this will affect users. Is it possible to implement the BIP in such a way that it doesn't change anything for users.

// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It will be great to have a reference to the PBKDF2 RFC link as part of this module's documentation. https://datatracker.ietf.org/doc/html/rfc2898

}

/// Generate word map for given language.
pub fn word_map(&self) -> HashMap<&str, u16> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we write a test for this method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What kind of test are you suggesting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was thinking of writing a test to make sure we have correct word to indices mapping in the hashmap.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Is this a good idea to have it in bdk_core eventually? Or we wanna do key generation outside of core separately?

@danielabrozzoni

Copy link
Copy Markdown
Contributor

We closed #561, let's close this one as well :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

Write own BIP39 implementation

7 participants

@evanlinjin@danielabrozzoni@afilini@rajarshimaitra@vladimirfomene@notmandatory@atalw
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' BIP39 Implementation by evanlinjin · Pull Request #644 · bitcoindevkit/bdk · GitHub
Skip to content

BIP39 Implementation - #644

Closed
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039
Closed

BIP39 Implementation#644
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039

Conversation

@evanlinjin

@evanlinjinevanlinjin commented Jun 29, 2022

Copy link
Copy Markdown
Member

Description

This is a continuation of PR #607 which closes#561

This PR includes commits for own implementation of PBKFD2.
I've also modified the .gitignore, I hope that is okay.

Notes to the reviewers

Although complete, I still have some security concerns for the current implementation (please check my comment below).

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • Add pbkfd2 implementation
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@evanlinjinevanlinjin mentioned this pull request Jun 29, 2022
9 tasks
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 05b53d1 to 61b7deaCompareJune 29, 2022 14:24
@evanlinjinevanlinjin changed the title WIP: pbkfd2 implementation for BIP39BIP-39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjinevanlinjin changed the title BIP-39 Implementation (with own PBKFD2)BIP39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from dce62db to 2136bd2CompareJune 29, 2022 14:34
@evanlinjin
evanlinjin marked this pull request as ready for review June 29, 2022 14:35
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 82da908 to 29ae147CompareJune 29, 2022 15:02
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from d4f35e7 to 0b5c558CompareJune 29, 2022 17:25

@danielabrozzonidanielabrozzoni left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy-pasted some review comments from #607 that it seems still need to be addressed

Comment threadCHANGELOG.md Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
}

/// Convert a mnemonic to a seed with an optional passphrase
fn to_seed(&self, passphrase: Option<String>) -> Seed {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From #607:

@vladimirfomene: It might not be a good idea to change the type of passphrase from &str to option as that has the potential of breaking code which consumes this method.

@atalw: It makes sense to have the passphrase as Option as it really is optional, so if a breaking change is okay we can go ahead with this.


Personally, I agree that we should try not to break the API, and leave P: Into<Cow<'a, str> (https://docs.rs/bip39/latest/src/bip39/lib.rs.html#479-486) here

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

In my opinion, having a P: Into<Cow<'a, str>> makes little sense based on our implementation. I propose just using a &str, this way for most people, the API shouldn't break.

@evanlinjinevanlinjinJul 1, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Changed in dd42594. Let me know if it is sufficient!

pubfnto_seed(&self,passphrase:&str) -> Seed{
letmut seed = [0_u8;SEED_LEN];
pbkdf2::generate_seed(self.word_iter(), passphrase,&mut seed);
seed
}

@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 5 times, most recently from b003391 to 8952808CompareJuly 1, 2022 12:43
atalwand others added 5 commits July 1, 2022 20:46
There are a couple of features that have been implemented in this
commit:
- Parse mnemonic string to Mnenomic type
- Generate Mnemonic from entropy
- Derive seed from Mnemonic (with and without passphrase)
- All language wordlists (with verification test to ensure they were
untampered)
- Mnemonic test vectors from BIP39
- Error handling
Function names have mostly been kept the same to maintain backwards
compatability.
Co-authored-by: Vladimir Fomene <vladimirfomene@gmail.com>
* `Mnemonic::parse_in` now verifies the checksum against the entropy.
* Add test: Make sure `from_entropy_in` produces error if length of
entropy bits is less than 128, greater than 256 and not a multipe of 32.
* Add test: invalid mnemonic sentence.Throw error if mnemonic sentence
is less than 12 words or greater than 24 words or number of words is
not a multiple of six or the contains a word not in wordlist or has
invalid checksum.
Also removed unused dependencies
@evanlinjinevanlinjin changed the title BIP39 Implementation (with own PBKFD2)BIP39 ImplementationJul 1, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 2399c6b to 75fa8f6CompareJuly 2, 2022 02:36
* Introduce `Language::word_map` method for faster word index finding.
* Readability changes to various `Mnemonic` methods and tests.
* Re-introduce various methods back into `Mnemonic`.
* `bip39::Error` no longer includes sensitive information.
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from c4334a1 to cdb3e25CompareJuly 2, 2022 18:23
@evanlinjin

evanlinjin commented Jul 2, 2022

Copy link
Copy Markdown
MemberAuthor

I still have some security concerns regarding the current implementation (although, I am no security expert, just based on what I've read on the internet). I will list them here, and hopefully someone knowledgeable enough could provide some clarity.

  1. Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

  2. Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

Thank you all in advance!

P.S. Test blockchain::esplora::bdk_blockchain_tests::test_sync_stop_gap_20 seems to fail occasionally.

Fixes:
* Fixed implementations of `GeneratableKey` to work for all word lengths
* Fixed example in `rpcwallet`
Changes:
* Added various `derive`s for bip39 structures
* Added `Mnemonic::with_passphrase` method
* Added `TryFrom<uszie>` implementation for `WordCount`
* Introduced `Bip39TestVector` struct for more comprehensive testing
* Various refactoring
CI/CC Changes:
* Added `all-languages` feature to `[package.metadata.doc.rs]`
* Added `all-languages` feature to code coverage and CI tests
@danielabrozzoni

Copy link
Copy Markdown
Contributor

Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

We could look into that, but it's better if we do so in a new PR. This one is already quite big, and the bigger it gets, the more difficult it is to collect reviews :)

Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Another aspect I've been thinking about, is the great majority of the time people will be using English (which shouldn't require Unicode normalization). For the passphrase, we can do a check only (and fail if not normalized).

Since normalization sometimes requires resizing the vector (so it's a heap operation), and for most people, it also means one less dependably.

@evanlinjin

evanlinjin commented Jul 4, 2022

Copy link
Copy Markdown
MemberAuthor

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

Addressed in 08e1cc9.

`Mnemonic` contains sensitive data so we should ensure internal fields
are not easily leaked.
* Explicitly implement `fmt::Debug` and redact all fields.
* Explicitly implement `ToString` instead of `Display`.
* Remove various comparative `derive()`s.
@afilini

Copy link
Copy Markdown
Member

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

@afiliniafilini left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is just a partial review, I still haven't looked at all the files.

I just wanted to post this comments so that you could start thinking about them and see if they make any sense.

/// Password is the UTF8-NFKD-normalized result of mnemonic words separated by space.
fn make_password<'a, W>(words: W) -> String
where
W: Iterator<Item = &'a str>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could define Item as another generic that implements AsRef<str>. This should allow you to pass a vec of strings as well if you want


/// Salt is the UTF8-NFKD-normalized result of (SALT_PREFIX + passphrase).
fn make_salt(passphrase: &str) -> Cow<'static, str> {
let mut salt = Cow::from(SALT_PREFIX);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I understand the small performance benefit of reusing the ref as-is, but I don't think it's worth using Cow here especially considering that moving forward we'd like to avoid using the heap (even if we don't manage to finalize that transition in this PR)

/// Make hmac-sha512 engine from password.
/// The hmac engine is used as the pseudo-random function.
fn make_prf(password: &str) -> HmacPRF {
HmacEngine::new(password.as_bytes())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was gonna comment here that for extra safety we should re-normalize the string, but then I realized: wouldn't it be better to immediately convert strings to &[u8]immediately after normalization?

This would be like a marker for us, anything that's &str or similar is potentially not normalized, but as soon as we are done we just convert to bytes and forget about it.

With this change I guess you would make this function take a &[u8] directly, and do the conversion in the caller which as far as I can see is already normalizing correctly.

/// Generate block (of given block_index) by calculating xor sum of iterations of PRF.
fn xor_sum(hmac_prf: &HmacPRF, salt: &str, iter_count: u32, block_index: u32, block: &mut [u8]) {
// for the first iteration, we concat: salt + block_index (as big-endian bytes)
let mut prev_u = Vec::with_capacity(salt.len() + 4);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since the length is fixed i guess this could also be an array with static length (there should be a constant in bitcoin_hashes for this).

I think the code would probably still look decently good with copy_from_slice: https://doc.rust-lang.org/std/primitive.slice.html#method.copy_from_slice

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

Less than a week. But I'm stuck into multi descriptor wallet business 😅😂

@afilini

Copy link
Copy Markdown
Member

Yes, multi-descriptor is definitely the priority right now. We'll get back to this once you are done there :)

@vladimirfomenevladimirfomene left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for helping us move this forward! Just a couple of questions and comments.

Comment thread.gitignore
*.swp
.idea

# IDE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nits: .idea is for IntelliJ. I don't know if it is necessary to have that IDE comment.

Comment threadsrc/keys/bip39/mod.rs
// parse word indexes and ENT+CS bits from mnemonic words
let parse_result = sentence_words
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not throw an invalid word error here if the word is not in the HashMap? What is the utility of having &utils::U11_EOF as the default value here?

Comment threadsrc/keys/bip39/mod.rs
Comment on lines +164 to +165
let mut word_indexes = Vec::with_capacity(MS_MAX); // word indexes
let mut ent_cs_bits = Vec::with_capacity(MS_MAX * utils::U11_BITS); // ENT+CS bits

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not just use ms as your vector size instead of MS_MAX?

Comment threadsrc/keys/bip39/mod.rs
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))
.try_for_each(|word_index| {
if *word_index > utils::U11_MAX {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that you are getting the word_index from the word_map is there a scenario where the word_index will be greater than utils::U11_MAX. I'm thinking if you throw an error for invalid words there will be no need for this if/else logic.

Comment threadexamples/rpcwallet.rs
Comment on lines +224 to +226
let mnemonic_with_passphrase: GeneratedKey<_, _> =
MnemonicWithPassphrase::generate((WordCount::Words12, Language::English, password))?;
Ok(mnemonic_with_passphrase)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that there is a change in the examples, I believe this will affect users. Is it possible to implement the BIP in such a way that it doesn't change anything for users.

// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It will be great to have a reference to the PBKDF2 RFC link as part of this module's documentation. https://datatracker.ietf.org/doc/html/rfc2898

}

/// Generate word map for given language.
pub fn word_map(&self) -> HashMap<&str, u16> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we write a test for this method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What kind of test are you suggesting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was thinking of writing a test to make sure we have correct word to indices mapping in the hashmap.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Is this a good idea to have it in bdk_core eventually? Or we wanna do key generation outside of core separately?

@danielabrozzoni

Copy link
Copy Markdown
Contributor

We closed #561, let's close this one as well :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

Write own BIP39 implementation

7 participants

@evanlinjin@danielabrozzoni@afilini@rajarshimaitra@vladimirfomene@notmandatory@atalw
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' BIP39 Implementation by evanlinjin · Pull Request #644 · bitcoindevkit/bdk · GitHub
Skip to content

BIP39 Implementation - #644

Closed
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039
Closed

BIP39 Implementation#644
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039

Conversation

@evanlinjin

@evanlinjinevanlinjin commented Jun 29, 2022

Copy link
Copy Markdown
Member

Description

This is a continuation of PR #607 which closes#561

This PR includes commits for own implementation of PBKFD2.
I've also modified the .gitignore, I hope that is okay.

Notes to the reviewers

Although complete, I still have some security concerns for the current implementation (please check my comment below).

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • Add pbkfd2 implementation
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@evanlinjinevanlinjin mentioned this pull request Jun 29, 2022
9 tasks
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 05b53d1 to 61b7deaCompareJune 29, 2022 14:24
@evanlinjinevanlinjin changed the title WIP: pbkfd2 implementation for BIP39BIP-39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjinevanlinjin changed the title BIP-39 Implementation (with own PBKFD2)BIP39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from dce62db to 2136bd2CompareJune 29, 2022 14:34
@evanlinjin
evanlinjin marked this pull request as ready for review June 29, 2022 14:35
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 82da908 to 29ae147CompareJune 29, 2022 15:02
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from d4f35e7 to 0b5c558CompareJune 29, 2022 17:25

@danielabrozzonidanielabrozzoni left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy-pasted some review comments from #607 that it seems still need to be addressed

Comment threadCHANGELOG.md Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
}

/// Convert a mnemonic to a seed with an optional passphrase
fn to_seed(&self, passphrase: Option<String>) -> Seed {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From #607:

@vladimirfomene: It might not be a good idea to change the type of passphrase from &str to option as that has the potential of breaking code which consumes this method.

@atalw: It makes sense to have the passphrase as Option as it really is optional, so if a breaking change is okay we can go ahead with this.


Personally, I agree that we should try not to break the API, and leave P: Into<Cow<'a, str> (https://docs.rs/bip39/latest/src/bip39/lib.rs.html#479-486) here

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

In my opinion, having a P: Into<Cow<'a, str>> makes little sense based on our implementation. I propose just using a &str, this way for most people, the API shouldn't break.

@evanlinjinevanlinjinJul 1, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Changed in dd42594. Let me know if it is sufficient!

pubfnto_seed(&self,passphrase:&str) -> Seed{
letmut seed = [0_u8;SEED_LEN];
pbkdf2::generate_seed(self.word_iter(), passphrase,&mut seed);
seed
}

@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 5 times, most recently from b003391 to 8952808CompareJuly 1, 2022 12:43
atalwand others added 5 commits July 1, 2022 20:46
There are a couple of features that have been implemented in this
commit:
- Parse mnemonic string to Mnenomic type
- Generate Mnemonic from entropy
- Derive seed from Mnemonic (with and without passphrase)
- All language wordlists (with verification test to ensure they were
untampered)
- Mnemonic test vectors from BIP39
- Error handling
Function names have mostly been kept the same to maintain backwards
compatability.
Co-authored-by: Vladimir Fomene <vladimirfomene@gmail.com>
* `Mnemonic::parse_in` now verifies the checksum against the entropy.
* Add test: Make sure `from_entropy_in` produces error if length of
entropy bits is less than 128, greater than 256 and not a multipe of 32.
* Add test: invalid mnemonic sentence.Throw error if mnemonic sentence
is less than 12 words or greater than 24 words or number of words is
not a multiple of six or the contains a word not in wordlist or has
invalid checksum.
Also removed unused dependencies
@evanlinjinevanlinjin changed the title BIP39 Implementation (with own PBKFD2)BIP39 ImplementationJul 1, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 2399c6b to 75fa8f6CompareJuly 2, 2022 02:36
* Introduce `Language::word_map` method for faster word index finding.
* Readability changes to various `Mnemonic` methods and tests.
* Re-introduce various methods back into `Mnemonic`.
* `bip39::Error` no longer includes sensitive information.
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from c4334a1 to cdb3e25CompareJuly 2, 2022 18:23
@evanlinjin

evanlinjin commented Jul 2, 2022

Copy link
Copy Markdown
MemberAuthor

I still have some security concerns regarding the current implementation (although, I am no security expert, just based on what I've read on the internet). I will list them here, and hopefully someone knowledgeable enough could provide some clarity.

  1. Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

  2. Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

Thank you all in advance!

P.S. Test blockchain::esplora::bdk_blockchain_tests::test_sync_stop_gap_20 seems to fail occasionally.

Fixes:
* Fixed implementations of `GeneratableKey` to work for all word lengths
* Fixed example in `rpcwallet`
Changes:
* Added various `derive`s for bip39 structures
* Added `Mnemonic::with_passphrase` method
* Added `TryFrom<uszie>` implementation for `WordCount`
* Introduced `Bip39TestVector` struct for more comprehensive testing
* Various refactoring
CI/CC Changes:
* Added `all-languages` feature to `[package.metadata.doc.rs]`
* Added `all-languages` feature to code coverage and CI tests
@danielabrozzoni

Copy link
Copy Markdown
Contributor

Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

We could look into that, but it's better if we do so in a new PR. This one is already quite big, and the bigger it gets, the more difficult it is to collect reviews :)

Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Another aspect I've been thinking about, is the great majority of the time people will be using English (which shouldn't require Unicode normalization). For the passphrase, we can do a check only (and fail if not normalized).

Since normalization sometimes requires resizing the vector (so it's a heap operation), and for most people, it also means one less dependably.

@evanlinjin

evanlinjin commented Jul 4, 2022

Copy link
Copy Markdown
MemberAuthor

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

Addressed in 08e1cc9.

`Mnemonic` contains sensitive data so we should ensure internal fields
are not easily leaked.
* Explicitly implement `fmt::Debug` and redact all fields.
* Explicitly implement `ToString` instead of `Display`.
* Remove various comparative `derive()`s.
@afilini

Copy link
Copy Markdown
Member

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

@afiliniafilini left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is just a partial review, I still haven't looked at all the files.

I just wanted to post this comments so that you could start thinking about them and see if they make any sense.

/// Password is the UTF8-NFKD-normalized result of mnemonic words separated by space.
fn make_password<'a, W>(words: W) -> String
where
W: Iterator<Item = &'a str>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could define Item as another generic that implements AsRef<str>. This should allow you to pass a vec of strings as well if you want


/// Salt is the UTF8-NFKD-normalized result of (SALT_PREFIX + passphrase).
fn make_salt(passphrase: &str) -> Cow<'static, str> {
let mut salt = Cow::from(SALT_PREFIX);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I understand the small performance benefit of reusing the ref as-is, but I don't think it's worth using Cow here especially considering that moving forward we'd like to avoid using the heap (even if we don't manage to finalize that transition in this PR)

/// Make hmac-sha512 engine from password.
/// The hmac engine is used as the pseudo-random function.
fn make_prf(password: &str) -> HmacPRF {
HmacEngine::new(password.as_bytes())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was gonna comment here that for extra safety we should re-normalize the string, but then I realized: wouldn't it be better to immediately convert strings to &[u8]immediately after normalization?

This would be like a marker for us, anything that's &str or similar is potentially not normalized, but as soon as we are done we just convert to bytes and forget about it.

With this change I guess you would make this function take a &[u8] directly, and do the conversion in the caller which as far as I can see is already normalizing correctly.

/// Generate block (of given block_index) by calculating xor sum of iterations of PRF.
fn xor_sum(hmac_prf: &HmacPRF, salt: &str, iter_count: u32, block_index: u32, block: &mut [u8]) {
// for the first iteration, we concat: salt + block_index (as big-endian bytes)
let mut prev_u = Vec::with_capacity(salt.len() + 4);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since the length is fixed i guess this could also be an array with static length (there should be a constant in bitcoin_hashes for this).

I think the code would probably still look decently good with copy_from_slice: https://doc.rust-lang.org/std/primitive.slice.html#method.copy_from_slice

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

Less than a week. But I'm stuck into multi descriptor wallet business 😅😂

@afilini

Copy link
Copy Markdown
Member

Yes, multi-descriptor is definitely the priority right now. We'll get back to this once you are done there :)

@vladimirfomenevladimirfomene left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for helping us move this forward! Just a couple of questions and comments.

Comment thread.gitignore
*.swp
.idea

# IDE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nits: .idea is for IntelliJ. I don't know if it is necessary to have that IDE comment.

Comment threadsrc/keys/bip39/mod.rs
// parse word indexes and ENT+CS bits from mnemonic words
let parse_result = sentence_words
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not throw an invalid word error here if the word is not in the HashMap? What is the utility of having &utils::U11_EOF as the default value here?

Comment threadsrc/keys/bip39/mod.rs
Comment on lines +164 to +165
let mut word_indexes = Vec::with_capacity(MS_MAX); // word indexes
let mut ent_cs_bits = Vec::with_capacity(MS_MAX * utils::U11_BITS); // ENT+CS bits

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not just use ms as your vector size instead of MS_MAX?

Comment threadsrc/keys/bip39/mod.rs
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))
.try_for_each(|word_index| {
if *word_index > utils::U11_MAX {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that you are getting the word_index from the word_map is there a scenario where the word_index will be greater than utils::U11_MAX. I'm thinking if you throw an error for invalid words there will be no need for this if/else logic.

Comment threadexamples/rpcwallet.rs
Comment on lines +224 to +226
let mnemonic_with_passphrase: GeneratedKey<_, _> =
MnemonicWithPassphrase::generate((WordCount::Words12, Language::English, password))?;
Ok(mnemonic_with_passphrase)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that there is a change in the examples, I believe this will affect users. Is it possible to implement the BIP in such a way that it doesn't change anything for users.

// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It will be great to have a reference to the PBKDF2 RFC link as part of this module's documentation. https://datatracker.ietf.org/doc/html/rfc2898

}

/// Generate word map for given language.
pub fn word_map(&self) -> HashMap<&str, u16> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we write a test for this method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What kind of test are you suggesting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was thinking of writing a test to make sure we have correct word to indices mapping in the hashmap.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Is this a good idea to have it in bdk_core eventually? Or we wanna do key generation outside of core separately?

@danielabrozzoni

Copy link
Copy Markdown
Contributor

We closed #561, let's close this one as well :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

Write own BIP39 implementation

7 participants

@evanlinjin@danielabrozzoni@afilini@rajarshimaitra@vladimirfomene@notmandatory@atalw
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); BIP39 Implementation by evanlinjin · Pull Request #644 · bitcoindevkit/bdk · GitHub
Skip to content

BIP39 Implementation - #644

Closed
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039
Closed

BIP39 Implementation#644
evanlinjin wants to merge 9 commits into
bitcoindevkit:masterfrom
evanlinjin:bip-0039

Conversation

@evanlinjin

@evanlinjinevanlinjin commented Jun 29, 2022

Copy link
Copy Markdown
Member

Description

This is a continuation of PR #607 which closes#561

This PR includes commits for own implementation of PBKFD2.
I've also modified the .gitignore, I hope that is okay.

Notes to the reviewers

Although complete, I still have some security concerns for the current implementation (please check my comment below).

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • Add pbkfd2 implementation
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@evanlinjinevanlinjin mentioned this pull request Jun 29, 2022
9 tasks
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 05b53d1 to 61b7deaCompareJune 29, 2022 14:24
@evanlinjinevanlinjin changed the title WIP: pbkfd2 implementation for BIP39BIP-39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjinevanlinjin changed the title BIP-39 Implementation (with own PBKFD2)BIP39 Implementation (with own PBKFD2)Jun 29, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from dce62db to 2136bd2CompareJune 29, 2022 14:34
@evanlinjin
evanlinjin marked this pull request as ready for review June 29, 2022 14:35
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 82da908 to 29ae147CompareJune 29, 2022 15:02
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from d4f35e7 to 0b5c558CompareJune 29, 2022 17:25

@danielabrozzonidanielabrozzoni left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy-pasted some review comments from #607 that it seems still need to be addressed

Comment threadCHANGELOG.md Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
Comment threadsrc/keys/bip39/mod.rs Outdated
}

/// Convert a mnemonic to a seed with an optional passphrase
fn to_seed(&self, passphrase: Option<String>) -> Seed {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From #607:

@vladimirfomene: It might not be a good idea to change the type of passphrase from &str to option as that has the potential of breaking code which consumes this method.

@atalw: It makes sense to have the passphrase as Option as it really is optional, so if a breaking change is okay we can go ahead with this.


Personally, I agree that we should try not to break the API, and leave P: Into<Cow<'a, str> (https://docs.rs/bip39/latest/src/bip39/lib.rs.html#479-486) here

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

In my opinion, having a P: Into<Cow<'a, str>> makes little sense based on our implementation. I propose just using a &str, this way for most people, the API shouldn't break.

@evanlinjinevanlinjinJul 1, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Changed in dd42594. Let me know if it is sufficient!

pubfnto_seed(&self,passphrase:&str) -> Seed{
letmut seed = [0_u8;SEED_LEN];
pbkdf2::generate_seed(self.word_iter(), passphrase,&mut seed);
seed
}

@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 5 times, most recently from b003391 to 8952808CompareJuly 1, 2022 12:43
atalwand others added 5 commits July 1, 2022 20:46
There are a couple of features that have been implemented in this
commit:
- Parse mnemonic string to Mnenomic type
- Generate Mnemonic from entropy
- Derive seed from Mnemonic (with and without passphrase)
- All language wordlists (with verification test to ensure they were
untampered)
- Mnemonic test vectors from BIP39
- Error handling
Function names have mostly been kept the same to maintain backwards
compatability.
Co-authored-by: Vladimir Fomene <vladimirfomene@gmail.com>
* `Mnemonic::parse_in` now verifies the checksum against the entropy.
* Add test: Make sure `from_entropy_in` produces error if length of
entropy bits is less than 128, greater than 256 and not a multipe of 32.
* Add test: invalid mnemonic sentence.Throw error if mnemonic sentence
is less than 12 words or greater than 24 words or number of words is
not a multiple of six or the contains a word not in wordlist or has
invalid checksum.
Also removed unused dependencies
@evanlinjinevanlinjin changed the title BIP39 Implementation (with own PBKFD2)BIP39 ImplementationJul 1, 2022
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 2 times, most recently from 2399c6b to 75fa8f6CompareJuly 2, 2022 02:36
* Introduce `Language::word_map` method for faster word index finding.
* Readability changes to various `Mnemonic` methods and tests.
* Re-introduce various methods back into `Mnemonic`.
* `bip39::Error` no longer includes sensitive information.
@evanlinjin
evanlinjinforce-pushed the bip-0039 branch 3 times, most recently from c4334a1 to cdb3e25CompareJuly 2, 2022 18:23
@evanlinjin

evanlinjin commented Jul 2, 2022

Copy link
Copy Markdown
MemberAuthor

I still have some security concerns regarding the current implementation (although, I am no security expert, just based on what I've read on the internet). I will list them here, and hopefully someone knowledgeable enough could provide some clarity.

  1. Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

  2. Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

Thank you all in advance!

P.S. Test blockchain::esplora::bdk_blockchain_tests::test_sync_stop_gap_20 seems to fail occasionally.

Fixes:
* Fixed implementations of `GeneratableKey` to work for all word lengths
* Fixed example in `rpcwallet`
Changes:
* Added various `derive`s for bip39 structures
* Added `Mnemonic::with_passphrase` method
* Added `TryFrom<uszie>` implementation for `WordCount`
* Introduced `Bip39TestVector` struct for more comprehensive testing
* Various refactoring
CI/CC Changes:
* Added `all-languages` feature to `[package.metadata.doc.rs]`
* Added `all-languages` feature to code coverage and CI tests
@danielabrozzoni

Copy link
Copy Markdown
Contributor

Should we avoid using heap memory, and keep everything on the stack? Apparently, heap memory is more prone to exploints. Reference: github.com/shellphish/how2heap

We could look into that, but it's better if we do so in a new PR. This one is already quite big, and the bigger it gets, the more difficult it is to collect reviews :)

Is implementing std::fmt::Display and Debug a good idea? As it may leak the secrets to logs. We can potentially remove these implementations completely, or provide implementations with redacted secrets, or use an a crate such as secrey.

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Another aspect I've been thinking about, is the great majority of the time people will be using English (which shouldn't require Unicode normalization). For the passphrase, we can do a check only (and fail if not normalized).

Since normalization sometimes requires resizing the vector (so it's a heap operation), and for most people, it also means one less dependably.

@evanlinjin

evanlinjin commented Jul 4, 2022

Copy link
Copy Markdown
MemberAuthor

This, instead, I think should be tackled here: for now, avoiding Debug and Display (or manually implementing a really generic one) should be enough (with an appropriate comment on why we do so). I'd avoid adding YA dependency :)

Addressed in 08e1cc9.

`Mnemonic` contains sensitive data so we should ensure internal fields
are not easily leaked.
* Explicitly implement `fmt::Debug` and redact all fields.
* Explicitly implement `ToString` instead of `Display`.
* Remove various comparative `derive()`s.
@afilini

Copy link
Copy Markdown
Member

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

@afiliniafilini left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is just a partial review, I still haven't looked at all the files.

I just wanted to post this comments so that you could start thinking about them and see if they make any sense.

/// Password is the UTF8-NFKD-normalized result of mnemonic words separated by space.
fn make_password<'a, W>(words: W) -> String
where
W: Iterator<Item = &'a str>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could define Item as another generic that implements AsRef<str>. This should allow you to pass a vec of strings as well if you want


/// Salt is the UTF8-NFKD-normalized result of (SALT_PREFIX + passphrase).
fn make_salt(passphrase: &str) -> Cow<'static, str> {
let mut salt = Cow::from(SALT_PREFIX);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I understand the small performance benefit of reusing the ref as-is, but I don't think it's worth using Cow here especially considering that moving forward we'd like to avoid using the heap (even if we don't manage to finalize that transition in this PR)

/// Make hmac-sha512 engine from password.
/// The hmac engine is used as the pseudo-random function.
fn make_prf(password: &str) -> HmacPRF {
HmacEngine::new(password.as_bytes())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was gonna comment here that for extra safety we should re-normalize the string, but then I realized: wouldn't it be better to immediately convert strings to &[u8]immediately after normalization?

This would be like a marker for us, anything that's &str or similar is potentially not normalized, but as soon as we are done we just convert to bytes and forget about it.

With this change I guess you would make this function take a &[u8] directly, and do the conversion in the caller which as far as I can see is already normalizing correctly.

/// Generate block (of given block_index) by calculating xor sum of iterations of PRF.
fn xor_sum(hmac_prf: &HmacPRF, salt: &str, iter_count: u32, block_index: u32, block: &mut [u8]) {
// for the first iteration, we concat: salt + block_index (as big-endian bytes)
let mut prev_u = Vec::with_capacity(salt.len() + 4);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since the length is fixed i guess this could also be an array with static length (there should be a constant in bitcoin_hashes for this).

I think the code would probably still look decently good with copy_from_slice: https://doc.rust-lang.org/std/primitive.slice.html#method.copy_from_slice

@evanlinjin

Copy link
Copy Markdown
MemberAuthor

Should we avoid using heap memory, and keep everything on the stack?

One advantage of this (on top of the extra safety) is that it would be much easier to then port to embedded hardware. We have many features in bdk which I guess are not really fit for hardware wallets, but mnemonics are for sure one that we'll need to have.

Are you able to do a rough estimation of how much longer/how much harder it would be to implement in this way?

Less than a week. But I'm stuck into multi descriptor wallet business 😅😂

@afilini

Copy link
Copy Markdown
Member

Yes, multi-descriptor is definitely the priority right now. We'll get back to this once you are done there :)

@vladimirfomenevladimirfomene left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for helping us move this forward! Just a couple of questions and comments.

Comment thread.gitignore
*.swp
.idea

# IDE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nits: .idea is for IntelliJ. I don't know if it is necessary to have that IDE comment.

Comment threadsrc/keys/bip39/mod.rs
// parse word indexes and ENT+CS bits from mnemonic words
let parse_result = sentence_words
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not throw an invalid word error here if the word is not in the HashMap? What is the utility of having &utils::U11_EOF as the default value here?

Comment threadsrc/keys/bip39/mod.rs
Comment on lines +164 to +165
let mut word_indexes = Vec::with_capacity(MS_MAX); // word indexes
let mut ent_cs_bits = Vec::with_capacity(MS_MAX * utils::U11_BITS); // ENT+CS bits

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not just use ms as your vector size instead of MS_MAX?

Comment threadsrc/keys/bip39/mod.rs
.iter()
.map(|&word| word_to_index_map.get(word).unwrap_or(&utils::U11_EOF))
.try_for_each(|word_index| {
if *word_index > utils::U11_MAX {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that you are getting the word_index from the word_map is there a scenario where the word_index will be greater than utils::U11_MAX. I'm thinking if you throw an error for invalid words there will be no need for this if/else logic.

Comment threadexamples/rpcwallet.rs
Comment on lines +224 to +226
let mnemonic_with_passphrase: GeneratedKey<_, _> =
MnemonicWithPassphrase::generate((WordCount::Words12, Language::English, password))?;
Ok(mnemonic_with_passphrase)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given that there is a change in the examples, I believe this will affect users. Is it possible to implement the BIP in such a way that it doesn't change anything for users.

// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It will be great to have a reference to the PBKDF2 RFC link as part of this module's documentation. https://datatracker.ietf.org/doc/html/rfc2898

}

/// Generate word map for given language.
pub fn word_map(&self) -> HashMap<&str, u16> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we write a test for this method?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

What kind of test are you suggesting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was thinking of writing a test to make sure we have correct word to indices mapping in the hashmap.

@rajarshimaitra

Copy link
Copy Markdown
Contributor

Is this a good idea to have it in bdk_core eventually? Or we wanna do key generation outside of core separately?

@danielabrozzoni

Copy link
Copy Markdown
Contributor

We closed #561, let's close this one as well :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new featureNew feature or request

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

Write own BIP39 implementation

7 participants

@evanlinjin@danielabrozzoni@afilini@rajarshimaitra@vladimirfomene@notmandatory@atalw