Skip to content

Commit

Permalink
bip85: confirm mnemonic words
Browse files Browse the repository at this point in the history
This moves the mnemonic word confirmation code from the api module to
the workflow module, so it can be used by both the show_mnemonic API
call and the bip85 API call.
  • Loading branch information
benma committed May 18, 2023
1 parent 18c96cc commit 679a61e
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 73 deletions.
2 changes: 1 addition & 1 deletion src/rust/bitbox02-rust/src/hww/api/bip85.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ pub async fn process(pb::Bip85Request {}: &pb::Bip85Request) -> Result<Response,

let mnemonic = keystore::bip85_bip39(num_words, index)?;
let words: Vec<&str> = mnemonic.split(' ').collect();
mnemonic::show_mnemonic(&words).await?;
mnemonic::show_and_confirm_mnemonic(&words).await?;

status::status("Finished", true).await;

Expand Down
71 changes: 1 addition & 70 deletions src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use alloc::string::String;
use alloc::vec::Vec;

use super::Error;
Expand All @@ -24,48 +23,6 @@ use pb::response::Response;
use crate::workflow::{mnemonic, status, unlock};
use bitbox02::keystore;

const NUM_RANDOM_WORDS: u8 = 5;

/// Return 5 words from the BIP39 wordlist, 4 of which are random, and
/// one of them is provided `word`. Returns the position of `word` in
/// the list of words, and the lis of words. This is used to test if
/// the user wrote down the seed words properly.
fn create_random_unique_words(word: &str, length: u8) -> (u8, Vec<zeroize::Zeroizing<String>>) {
fn rand16() -> u16 {
let mut rand = [0u8; 32];
bitbox02::random::mcu_32_bytes(&mut rand);
((rand[0] as u16) << 8) | (rand[1] as u16)
}

let index_word = (rand16() as u8) % length;
let mut picked_indices = Vec::new();
let result = (0..length)
.map(|i| {
// The correct word at the right index.
if i == index_word {
return zeroize::Zeroizing::new(word.into());
}

// A random word everywhere else.
// Loop until we get a unique word, we don't want repeated words in the list.
loop {
let idx = rand16() % keystore::BIP39_WORDLIST_LEN;
if picked_indices.contains(&idx) {
continue;
};
let random_word = keystore::get_bip39_word(idx).unwrap();
if random_word.as_str() == word {
continue;
}
picked_indices.push(idx);
return random_word;
}
})
.collect();

(index_word, result)
}

/// Handle the ShowMnemonic API call. This shows the seed encoded as
/// 12/18/24 BIP39 English words. Afterwards, for each word, the user
/// is asked to pick the right word among 5 words, to check if they
Expand All @@ -87,33 +44,7 @@ pub async fn process() -> Result<Response, Error> {

let words: Vec<&str> = mnemonic_sentence.split(' ').collect();

// Part 1) Scroll through words
mnemonic::show_mnemonic(&words).await?;

confirm::confirm(&confirm::Params {
title: "",
body: "Please confirm\neach word",
accept_only: true,
accept_is_nextarrow: true,
..Default::default()
})
.await?;

// Part 2) Confirm words
for (word_idx, word) in words.iter().enumerate() {
let title = format!("{:02}", word_idx + 1);
let (correct_idx, choices) = create_random_unique_words(word, NUM_RANDOM_WORDS);
let mut choices: Vec<&str> = choices.iter().map(|c| c.as_ref()).collect();
choices.push("Back to\nrecovery words");
let back_idx = (choices.len() - 1) as u8;
loop {
match mnemonic::confirm_word(&choices, &title).await? {
selected_idx if selected_idx == correct_idx => break,
selected_idx if selected_idx == back_idx => mnemonic::show_mnemonic(&words).await?,
_ => status::status("Incorrect word\nTry again", false).await,
}
}
}
mnemonic::show_and_confirm_mnemonic(&words).await?;

bitbox02::memory::set_initialized().or(Err(Error::Memory))?;

Expand Down
79 changes: 77 additions & 2 deletions src/rust/bitbox02-rust/src/workflow/mnemonic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,54 @@ use core::cell::RefCell;

use sha2::{Digest, Sha256};

const NUM_RANDOM_WORDS: u8 = 5;

fn as_str_vec(v: &[zeroize::Zeroizing<String>]) -> Vec<&str> {
v.iter().map(|s| s.as_str()).collect()
}

/// Return 5 words from the BIP39 wordlist, 4 of which are random, and
/// one of them is provided `word`. Returns the position of `word` in
/// the list of words, and the lis of words. This is used to test if
/// the user wrote down the seed words properly.
fn create_random_unique_words(word: &str, length: u8) -> (u8, Vec<zeroize::Zeroizing<String>>) {
fn rand16() -> u16 {
let mut rand = [0u8; 32];
bitbox02::random::mcu_32_bytes(&mut rand);
((rand[0] as u16) << 8) | (rand[1] as u16)
}

let index_word = (rand16() as u8) % length;
let mut picked_indices = Vec::new();
let result = (0..length)
.map(|i| {
// The correct word at the right index.
if i == index_word {
return zeroize::Zeroizing::new(word.into());
}

// A random word everywhere else.
// Loop until we get a unique word, we don't want repeated words in the list.
loop {
let idx = rand16() % bitbox02::keystore::BIP39_WORDLIST_LEN;
if picked_indices.contains(&idx) {
continue;
};
let random_word = bitbox02::keystore::get_bip39_word(idx).unwrap();
if random_word.as_str() == word {
continue;
}
picked_indices.push(idx);
return random_word;
}
})
.collect();

(index_word, result)
}

/// Displays all mnemonic words in a scroll-through screen.
pub async fn show_mnemonic(words: &[&str]) -> Result<(), CancelError> {
async fn show_mnemonic(words: &[&str]) -> Result<(), CancelError> {
let result = RefCell::new(None);
let mut component = bitbox02::ui::menu_create(bitbox02::ui::MenuParams {
words,
Expand All @@ -49,7 +91,7 @@ pub async fn show_mnemonic(words: &[&str]) -> Result<(), CancelError> {
}

/// Displays the `choices` to the user, returning the index of the selected choice.
pub async fn confirm_word(choices: &[&str], title: &str) -> Result<u8, CancelError> {
async fn confirm_word(choices: &[&str], title: &str) -> Result<u8, CancelError> {
let result = RefCell::new(None);
let mut component = bitbox02::ui::menu_create(bitbox02::ui::MenuParams {
words: choices,
Expand All @@ -65,6 +107,39 @@ pub async fn confirm_word(choices: &[&str], title: &str) -> Result<u8, CancelErr
with_cancel("Recovery\nwords", &mut component, &result).await
}

pub async fn show_and_confirm_mnemonic(words: &[&str]) -> Result<(), CancelError> {
// Part 1) Scroll through words
show_mnemonic(words).await?;

// Can only succeed due to `accept_only`.
let _ = confirm::confirm(&confirm::Params {
title: "",
body: "Please confirm\neach word",
accept_only: true,
accept_is_nextarrow: true,
..Default::default()
})
.await;

// Part 2) Confirm words
for (word_idx, word) in words.iter().enumerate() {
let title = format!("{:02}", word_idx + 1);
let (correct_idx, choices) = create_random_unique_words(word, NUM_RANDOM_WORDS);
let mut choices: Vec<&str> = choices.iter().map(|c| c.as_ref()).collect();
choices.push("Back to\nrecovery words");
let back_idx = (choices.len() - 1) as u8;
loop {
match confirm_word(&choices, &title).await? {
selected_idx if selected_idx == correct_idx => break,
selected_idx if selected_idx == back_idx => show_mnemonic(&words).await?,
_ => status("Incorrect word\nTry again", false).await,
}
}
}

Ok(())
}

/// Given 23 initial words, this function returns list of candidate words for the last word, such
/// that the resulting bip39 phrase has a valid checksum. There are always exactly 8 such words.
/// `entered_words` must contain 23 words from the BIP39 wordlist.
Expand Down

0 comments on commit 679a61e

Please sign in to comment.