mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-07-08 23:58:09 +00:00
WIP - Shamir wallet recovery
This commit is contained in:
parent
d7b1f79f8b
commit
9f62e4467d
@ -313,6 +313,8 @@ fn generate_trezorhal_bindings() {
|
|||||||
// slip39
|
// slip39
|
||||||
.allowlist_function("slip39_word_completion_mask")
|
.allowlist_function("slip39_word_completion_mask")
|
||||||
.allowlist_function("button_sequence_to_word")
|
.allowlist_function("button_sequence_to_word")
|
||||||
|
.allowlist_var("SLIP39_WORDLIST")
|
||||||
|
.allowlist_var("SLIP39_WORD_COUNT")
|
||||||
// random
|
// random
|
||||||
.allowlist_function("random_uniform")
|
.allowlist_function("random_uniform")
|
||||||
// rgb led
|
// rgb led
|
||||||
|
@ -1,40 +1,10 @@
|
|||||||
use super::ffi;
|
use super::{ffi, wordlist::Wordlist};
|
||||||
use core::cmp::Ordering;
|
|
||||||
use cstr_core::CStr;
|
|
||||||
|
|
||||||
unsafe fn from_utf8_unchecked<'a>(word: *const cty::c_char) -> &'a str {
|
|
||||||
// SAFETY: caller must pass a valid 0-terminated UTF-8 string.
|
|
||||||
// This assumption holds for usage on words of the BIP-39 wordlist.
|
|
||||||
unsafe {
|
|
||||||
let word = CStr::from_ptr(word);
|
|
||||||
core::str::from_utf8_unchecked(word.to_bytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compare word from wordlist to a prefix.
|
|
||||||
///
|
|
||||||
/// The comparison returns Less if the word comes lexicographically before all
|
|
||||||
/// possible words starting with `prefix`, and Greater if it comes after.
|
|
||||||
/// Equal is returned if the word starts with `prefix`.
|
|
||||||
unsafe fn prefix_cmp(prefix: &str, word: *const cty::c_char) -> Ordering {
|
|
||||||
// SAFETY: we assume `word` is a pointer to a 0-terminated string.
|
|
||||||
for (i, prefix_char) in prefix.as_bytes().iter().enumerate() {
|
|
||||||
let word_char = unsafe { *(word.add(i)) } as u8;
|
|
||||||
if word_char == 0 {
|
|
||||||
// Prefix is longer than word.
|
|
||||||
return Ordering::Less;
|
|
||||||
} else if *prefix_char != word_char {
|
|
||||||
return word_char.cmp(prefix_char);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ordering::Equal
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn complete_word(prefix: &str) -> Option<&'static str> {
|
pub fn complete_word(prefix: &str) -> Option<&'static str> {
|
||||||
if prefix.is_empty() {
|
if prefix.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Wordlist::all().filter_prefix(prefix).iter().next()
|
Wordlist::bip39().filter_prefix(prefix).iter().next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +12,7 @@ pub fn options_num(prefix: &str) -> Option<usize> {
|
|||||||
if prefix.is_empty() {
|
if prefix.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(Wordlist::all().filter_prefix(prefix).len())
|
Some(Wordlist::bip39().filter_prefix(prefix).len())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,201 +21,3 @@ pub fn word_completion_mask(prefix: &str) -> u32 {
|
|||||||
// passed byte string, making the call safe.
|
// passed byte string, making the call safe.
|
||||||
unsafe { ffi::mnemonic_word_completion_mask(prefix.as_ptr() as _, prefix.len() as _) }
|
unsafe { ffi::mnemonic_word_completion_mask(prefix.as_ptr() as _, prefix.len() as _) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns all possible letters that form a valid word together with some
|
|
||||||
/// prefix.
|
|
||||||
pub fn get_available_letters(prefix: &str) -> impl Iterator<Item = char> {
|
|
||||||
const CHARS: [char; 26] = [
|
|
||||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
|
|
||||||
's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
|
||||||
];
|
|
||||||
|
|
||||||
let mask = word_completion_mask(prefix);
|
|
||||||
CHARS
|
|
||||||
.iter()
|
|
||||||
.filter(move |ch| bitmask_from_char(ch) & mask != 0)
|
|
||||||
.copied()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bitmask_from_char(ch: &char) -> u32 {
|
|
||||||
1 << (*ch as u8 - b'a')
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Wordlist(&'static [*const cty::c_char]);
|
|
||||||
|
|
||||||
impl Wordlist {
|
|
||||||
pub fn all() -> Self {
|
|
||||||
Self(unsafe { &ffi::BIP39_WORDLIST_ENGLISH })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn empty() -> Self {
|
|
||||||
Self(&[])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn filter_prefix(&self, prefix: &str) -> Self {
|
|
||||||
let mut start = 0usize;
|
|
||||||
let mut end = self.0.len();
|
|
||||||
for (i, word) in self.0.iter().enumerate() {
|
|
||||||
// SAFETY: We assume our slice is an array of 0-terminated strings.
|
|
||||||
match unsafe { prefix_cmp(prefix, *word) } {
|
|
||||||
Ordering::Less => {
|
|
||||||
start = i + 1;
|
|
||||||
}
|
|
||||||
Ordering::Greater => {
|
|
||||||
end = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self(&self.0[start..end])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, index: usize) -> Option<&'static str> {
|
|
||||||
// SAFETY: we assume every word in the wordlist is a valid 0-terminated UTF-8
|
|
||||||
// string.
|
|
||||||
self.0
|
|
||||||
.get(index)
|
|
||||||
.map(|word| unsafe { from_utf8_unchecked(*word) })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn len(&self) -> usize {
|
|
||||||
self.0.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = &'static str> {
|
|
||||||
self.0
|
|
||||||
.iter()
|
|
||||||
.map(|word| unsafe { from_utf8_unchecked(*word) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use cstr_core::cstr;
|
|
||||||
|
|
||||||
const BIP39_WORD_COUNT: usize = ffi::BIP39_WORD_COUNT as usize;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_prefix_cmp() {
|
|
||||||
assert_eq!(
|
|
||||||
unsafe { prefix_cmp("", cstr!("").as_ptr()) },
|
|
||||||
Ordering::Equal
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
unsafe { prefix_cmp("b", cstr!("").as_ptr()) },
|
|
||||||
Ordering::Less
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
unsafe { prefix_cmp("b", cstr!("a").as_ptr()) },
|
|
||||||
Ordering::Less
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
unsafe { prefix_cmp("b", cstr!("b").as_ptr()) },
|
|
||||||
Ordering::Equal
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
unsafe { prefix_cmp("b", cstr!("below").as_ptr()) },
|
|
||||||
Ordering::Equal
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
unsafe { prefix_cmp("b", cstr!("c").as_ptr()) },
|
|
||||||
Ordering::Greater
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
unsafe { prefix_cmp("bartender", cstr!("bar").as_ptr()) },
|
|
||||||
Ordering::Less
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_filter_prefix_empty() {
|
|
||||||
let words = Wordlist::all().filter_prefix("");
|
|
||||||
assert_eq!(words.len(), BIP39_WORD_COUNT);
|
|
||||||
let iter = words.iter();
|
|
||||||
assert_eq!(iter.size_hint(), (BIP39_WORD_COUNT, Some(BIP39_WORD_COUNT)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_filter_prefix() {
|
|
||||||
let expected_result = vec!["strategy", "street", "strike", "strong", "struggle"];
|
|
||||||
let result = Wordlist::all()
|
|
||||||
.filter_prefix("str")
|
|
||||||
.iter()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
assert_eq!(result, expected_result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_filter_prefix_refine() {
|
|
||||||
let expected_result = vec!["strategy", "street", "strike", "strong", "struggle"];
|
|
||||||
let words = Wordlist::all().filter_prefix("st");
|
|
||||||
let result_a = words.filter_prefix("str").iter().collect::<Vec<_>>();
|
|
||||||
let result_b = Wordlist::all()
|
|
||||||
.filter_prefix("str")
|
|
||||||
.iter()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
assert_eq!(result_a, expected_result);
|
|
||||||
assert_eq!(result_b, expected_result);
|
|
||||||
|
|
||||||
let empty = words.filter_prefix("c");
|
|
||||||
assert_eq!(empty.len(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_wordlist_get() {
|
|
||||||
let words = Wordlist::all();
|
|
||||||
assert_eq!(words.get(0), Some("abandon"));
|
|
||||||
assert_eq!(words.get(BIP39_WORD_COUNT - 1), Some("zoo"));
|
|
||||||
assert_eq!(words.get(BIP39_WORD_COUNT), None);
|
|
||||||
assert_eq!(words.get(BIP39_WORD_COUNT + 1), None);
|
|
||||||
|
|
||||||
let filtered = words.filter_prefix("str");
|
|
||||||
assert_eq!(filtered.get(0), Some("strategy"));
|
|
||||||
assert_eq!(filtered.get(filtered.len()), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_filter_prefix_just_one() {
|
|
||||||
let expected_result = vec!["stick"];
|
|
||||||
let result = Wordlist::all()
|
|
||||||
.filter_prefix("stick")
|
|
||||||
.iter()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
assert_eq!(result, expected_result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_word_completion_mask() {
|
|
||||||
let result = word_completion_mask("ab");
|
|
||||||
assert_eq!(result, 0b101000100100100000001);
|
|
||||||
let result = word_completion_mask("zoo");
|
|
||||||
assert_eq!(result, 0b0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_available_letters() {
|
|
||||||
let expected_result = vec!['a', 'i', 'l', 'o', 's', 'u'];
|
|
||||||
let result = get_available_letters("ab").collect::<Vec<_>>();
|
|
||||||
assert_eq!(result, expected_result);
|
|
||||||
|
|
||||||
let expected_result = vec!['a', 'e', 'i', 'o', 'u'];
|
|
||||||
let result = get_available_letters("str").collect::<Vec<_>>();
|
|
||||||
assert_eq!(result, expected_result);
|
|
||||||
|
|
||||||
let result = get_available_letters("zoo").collect::<Vec<_>>();
|
|
||||||
assert_eq!(result.len(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_bitmask_from_char() {
|
|
||||||
assert_eq!(bitmask_from_char(&'a'), 0b1);
|
|
||||||
assert_eq!(bitmask_from_char(&'b'), 0b10);
|
|
||||||
assert_eq!(bitmask_from_char(&'c'), 0b100);
|
|
||||||
assert_eq!(bitmask_from_char(&'m'), 0b1000000000000);
|
|
||||||
assert_eq!(bitmask_from_char(&'z'), 0b10000000000000000000000000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -15,6 +15,7 @@ pub mod slip39;
|
|||||||
pub mod storage;
|
pub mod storage;
|
||||||
pub mod usb;
|
pub mod usb;
|
||||||
pub mod uzlib;
|
pub mod uzlib;
|
||||||
|
pub mod wordlist;
|
||||||
|
|
||||||
pub mod buffers;
|
pub mod buffers;
|
||||||
#[cfg(not(feature = "micropython"))]
|
#[cfg(not(feature = "micropython"))]
|
||||||
|
199
core/embed/rust/src/trezorhal/wordlist.rs
Normal file
199
core/embed/rust/src/trezorhal/wordlist.rs
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
use super::ffi;
|
||||||
|
use core::cmp::Ordering;
|
||||||
|
use cstr_core::CStr;
|
||||||
|
|
||||||
|
/// Holds all the possible words with the possibility to interact
|
||||||
|
/// with the "list" - filtering it further, getting their count, etc.
|
||||||
|
pub struct Wordlist(&'static [*const cty::c_char]);
|
||||||
|
|
||||||
|
impl Wordlist {
|
||||||
|
/// Initialize BIP39 wordlist.
|
||||||
|
pub fn bip39() -> Self {
|
||||||
|
Self(unsafe { &ffi::BIP39_WORDLIST_ENGLISH })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize SLIP39 wordlist.
|
||||||
|
pub fn slip39() -> Self {
|
||||||
|
Self(unsafe { &ffi::SLIP39_WORDLIST })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all possible letters that form a valid word together with some
|
||||||
|
/// prefix. Alphabetically sorted.
|
||||||
|
pub fn get_available_letters(&self, prefix: &str) -> impl Iterator<Item = char> {
|
||||||
|
// Fill a "set" of all unique characters, not sorted yet
|
||||||
|
let mut suffixes: heapless::Vec<char, 26> = heapless::Vec::new();
|
||||||
|
for word in self.iter() {
|
||||||
|
if word.starts_with(prefix) && word.len() > prefix.len() {
|
||||||
|
let following_char = word.chars().nth(prefix.len()).unwrap();
|
||||||
|
if !suffixes.contains(&following_char) {
|
||||||
|
suffixes.push(following_char).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suffixes.sort_unstable();
|
||||||
|
suffixes.into_iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only leaves words that have a specified prefix. Throw away others.
|
||||||
|
pub fn filter_prefix(&self, prefix: &str) -> Self {
|
||||||
|
let mut start = 0usize;
|
||||||
|
let mut end = self.0.len();
|
||||||
|
for (i, word) in self.0.iter().enumerate() {
|
||||||
|
// SAFETY: We assume our slice is an array of 0-terminated strings.
|
||||||
|
match unsafe { prefix_cmp(prefix, *word) } {
|
||||||
|
Ordering::Less => {
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
Ordering::Greater => {
|
||||||
|
end = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self(&self.0[start..end])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a word at the certain position.
|
||||||
|
pub fn get(&self, index: usize) -> Option<&'static str> {
|
||||||
|
// SAFETY: we assume every word in the wordlist is a valid 0-terminated UTF-8
|
||||||
|
// string.
|
||||||
|
self.0
|
||||||
|
.get(index)
|
||||||
|
.map(|word| unsafe { from_utf8_unchecked(*word) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many words are currently in the list.
|
||||||
|
pub const fn len(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator of all current words.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &'static str> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.map(|word| unsafe { from_utf8_unchecked(*word) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare word from wordlist to a prefix.
|
||||||
|
///
|
||||||
|
/// The comparison returns Less if the word comes lexicographically before all
|
||||||
|
/// possible words starting with `prefix`, and Greater if it comes after.
|
||||||
|
/// Equal is returned if the word starts with `prefix`.
|
||||||
|
unsafe fn prefix_cmp(prefix: &str, word: *const cty::c_char) -> Ordering {
|
||||||
|
// SAFETY: we assume `word` is a pointer to a 0-terminated string.
|
||||||
|
for (i, prefix_char) in prefix.as_bytes().iter().enumerate() {
|
||||||
|
let word_char = unsafe { *(word.add(i)) } as u8;
|
||||||
|
if word_char == 0 {
|
||||||
|
// Prefix is longer than word.
|
||||||
|
return Ordering::Less;
|
||||||
|
} else if *prefix_char != word_char {
|
||||||
|
return word_char.cmp(prefix_char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ordering::Equal
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn from_utf8_unchecked<'a>(word: *const cty::c_char) -> &'a str {
|
||||||
|
// SAFETY: caller must pass a valid 0-terminated UTF-8 string.
|
||||||
|
// This assumption holds for usage on words of the BIP-39 wordlist.
|
||||||
|
unsafe {
|
||||||
|
let word = CStr::from_ptr(word);
|
||||||
|
core::str::from_utf8_unchecked(word.to_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const BIP39_WORD_COUNT: usize = ffi::BIP39_WORD_COUNT as usize;
|
||||||
|
const SLIP39_WORD_COUNT: usize = ffi::SLIP39_WORD_COUNT as usize;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_filter_prefix_empty() {
|
||||||
|
let words = Wordlist::bip39().filter_prefix("");
|
||||||
|
assert_eq!(words.len(), BIP39_WORD_COUNT);
|
||||||
|
let iter = words.iter();
|
||||||
|
assert_eq!(iter.size_hint(), (BIP39_WORD_COUNT, Some(BIP39_WORD_COUNT)));
|
||||||
|
|
||||||
|
let words = Wordlist::slip39().filter_prefix("");
|
||||||
|
assert_eq!(words.len(), SLIP39_WORD_COUNT);
|
||||||
|
let iter = words.iter();
|
||||||
|
assert_eq!(
|
||||||
|
iter.size_hint(),
|
||||||
|
(SLIP39_WORD_COUNT, Some(SLIP39_WORD_COUNT))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_filter_prefix() {
|
||||||
|
let expected_result = vec!["strategy", "street", "strike", "strong", "struggle"];
|
||||||
|
let result = Wordlist::bip39()
|
||||||
|
.filter_prefix("str")
|
||||||
|
.iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(result, expected_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_filter_prefix_refine() {
|
||||||
|
let expected_result = vec!["strategy", "street", "strike", "strong", "struggle"];
|
||||||
|
let words = Wordlist::bip39().filter_prefix("st");
|
||||||
|
let result_a = words.filter_prefix("str").iter().collect::<Vec<_>>();
|
||||||
|
let result_b = Wordlist::bip39()
|
||||||
|
.filter_prefix("str")
|
||||||
|
.iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(result_a, expected_result);
|
||||||
|
assert_eq!(result_b, expected_result);
|
||||||
|
|
||||||
|
let empty = words.filter_prefix("c");
|
||||||
|
assert_eq!(empty.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wordlist_get() {
|
||||||
|
let words = Wordlist::bip39();
|
||||||
|
assert_eq!(words.get(0), Some("abandon"));
|
||||||
|
assert_eq!(words.get(BIP39_WORD_COUNT - 1), Some("zoo"));
|
||||||
|
assert_eq!(words.get(BIP39_WORD_COUNT), None);
|
||||||
|
assert_eq!(words.get(BIP39_WORD_COUNT + 1), None);
|
||||||
|
|
||||||
|
let filtered = words.filter_prefix("str");
|
||||||
|
assert_eq!(filtered.get(0), Some("strategy"));
|
||||||
|
assert_eq!(filtered.get(filtered.len()), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_filter_prefix_just_one() {
|
||||||
|
let expected_result = vec!["stick"];
|
||||||
|
let result = Wordlist::bip39()
|
||||||
|
.filter_prefix("stick")
|
||||||
|
.iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(result, expected_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_available_letters() {
|
||||||
|
let expected_result = vec!['a', 'i', 'l', 'o', 's', 'u'];
|
||||||
|
let result = Wordlist::bip39()
|
||||||
|
.get_available_letters("ab")
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(result, expected_result);
|
||||||
|
|
||||||
|
let expected_result = vec!['a', 'e', 'i', 'o', 'u'];
|
||||||
|
let result = Wordlist::bip39()
|
||||||
|
.get_available_letters("str")
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(result, expected_result);
|
||||||
|
|
||||||
|
let result = Wordlist::bip39()
|
||||||
|
.get_available_letters("zoo")
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(result.len(), 0);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
pub mod bip39;
|
|
||||||
pub mod choice;
|
pub mod choice;
|
||||||
pub mod choice_item;
|
pub mod choice_item;
|
||||||
pub mod number_input;
|
pub mod number_input;
|
||||||
pub mod passphrase;
|
pub mod passphrase;
|
||||||
pub mod pin;
|
pub mod pin;
|
||||||
pub mod simple_choice;
|
pub mod simple_choice;
|
||||||
|
pub mod wordlist;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
trezorhal::bip39,
|
trezorhal::wordlist::Wordlist,
|
||||||
ui::{
|
ui::{
|
||||||
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
||||||
display::Icon,
|
display::Icon,
|
||||||
@ -14,26 +14,32 @@ use super::super::{
|
|||||||
};
|
};
|
||||||
use heapless::{String, Vec};
|
use heapless::{String, Vec};
|
||||||
|
|
||||||
pub enum Bip39EntryMsg {
|
pub enum WordlistEntryMsg {
|
||||||
ResultWord(String<15>),
|
ResultWord(String<15>),
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_WORD_LENGTH: usize = 10;
|
const MAX_WORD_LENGTH: usize = 10;
|
||||||
const MAX_CHOICE_LENGTH: usize = 26;
|
const MAX_LETTERS_LENGTH: usize = 26;
|
||||||
|
|
||||||
/// Offer words when there will be fewer of them than this
|
/// Offer words when there will be fewer of them than this
|
||||||
const OFFER_WORDS_THRESHOLD: usize = 10;
|
const OFFER_WORDS_THRESHOLD: usize = 10;
|
||||||
|
|
||||||
const PROMPT: &str = "_";
|
const PROMPT: &str = "_";
|
||||||
|
|
||||||
|
/// Type of the wordlist, deciding the list of words to be used
|
||||||
|
pub enum WordlistType {
|
||||||
|
Bip39,
|
||||||
|
Slip39,
|
||||||
|
}
|
||||||
|
|
||||||
/// We are offering either letters or words.
|
/// We are offering either letters or words.
|
||||||
enum ChoiceFactoryBIP39 {
|
enum ChoiceFactoryWordlist {
|
||||||
Letters(Vec<char, MAX_CHOICE_LENGTH>),
|
Letters(Vec<char, MAX_LETTERS_LENGTH>),
|
||||||
Words(Vec<&'static str, OFFER_WORDS_THRESHOLD>),
|
Words(Vec<&'static str, OFFER_WORDS_THRESHOLD>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChoiceFactoryBIP39 {
|
impl ChoiceFactoryWordlist {
|
||||||
fn letters(letter_choices: Vec<char, MAX_CHOICE_LENGTH>) -> Self {
|
fn letters(letter_choices: Vec<char, MAX_LETTERS_LENGTH>) -> Self {
|
||||||
Self::Letters(letter_choices)
|
Self::Letters(letter_choices)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +48,7 @@ impl ChoiceFactoryBIP39 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChoiceFactory for ChoiceFactoryBIP39 {
|
impl ChoiceFactory for ChoiceFactoryWordlist {
|
||||||
type Item = ChoiceItem;
|
type Item = ChoiceItem;
|
||||||
|
|
||||||
fn count(&self) -> u8 {
|
fn count(&self) -> u8 {
|
||||||
@ -88,21 +94,23 @@ impl ChoiceFactory for ChoiceFactoryBIP39 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Component for entering a BIP39 mnemonic.
|
/// Component for entering a mnemonic from a wordlist - BIP39 or SLIP39.
|
||||||
pub struct Bip39Entry {
|
pub struct WordlistEntry {
|
||||||
choice_page: ChoicePage<ChoiceFactoryBIP39>,
|
choice_page: ChoicePage<ChoiceFactoryWordlist>,
|
||||||
chosen_letters: Child<ChangingTextLine<String<{ MAX_WORD_LENGTH + 1 }>>>,
|
chosen_letters: Child<ChangingTextLine<String<{ MAX_WORD_LENGTH + 1 }>>>,
|
||||||
letter_choices: Vec<char, MAX_CHOICE_LENGTH>,
|
letter_choices: Vec<char, MAX_LETTERS_LENGTH>,
|
||||||
textbox: TextBox<MAX_WORD_LENGTH>,
|
textbox: TextBox<MAX_WORD_LENGTH>,
|
||||||
offer_words: bool,
|
offer_words: bool,
|
||||||
words_list: bip39::Wordlist,
|
words_list: Wordlist,
|
||||||
|
wordlist_type: WordlistType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Bip39Entry {
|
impl WordlistEntry {
|
||||||
pub fn new() -> Self {
|
pub fn new(wordlist_type: WordlistType) -> Self {
|
||||||
let letter_choices: Vec<char, MAX_CHOICE_LENGTH> =
|
let words_list = Self::get_fresh_wordlist(&wordlist_type);
|
||||||
bip39::get_available_letters("").collect();
|
let letter_choices: Vec<char, MAX_LETTERS_LENGTH> =
|
||||||
let choices = ChoiceFactoryBIP39::letters(letter_choices.clone());
|
words_list.get_available_letters("").collect();
|
||||||
|
let choices = ChoiceFactoryWordlist::letters(letter_choices.clone());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
choice_page: ChoicePage::new(choices)
|
choice_page: ChoicePage::new(choices)
|
||||||
@ -112,12 +120,21 @@ impl Bip39Entry {
|
|||||||
letter_choices,
|
letter_choices,
|
||||||
textbox: TextBox::empty(),
|
textbox: TextBox::empty(),
|
||||||
offer_words: false,
|
offer_words: false,
|
||||||
words_list: bip39::Wordlist::all(),
|
words_list,
|
||||||
|
wordlist_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get appropriate wordlist with all possible words
|
||||||
|
fn get_fresh_wordlist(wordlist_type: &WordlistType) -> Wordlist {
|
||||||
|
match wordlist_type {
|
||||||
|
WordlistType::Bip39 => Wordlist::bip39(),
|
||||||
|
WordlistType::Slip39 => Wordlist::slip39(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets up-to-date choices for letters or words.
|
/// Gets up-to-date choices for letters or words.
|
||||||
fn get_current_choices(&mut self) -> ChoiceFactoryBIP39 {
|
fn get_current_choices(&mut self) -> ChoiceFactoryWordlist {
|
||||||
// Narrowing the word list
|
// Narrowing the word list
|
||||||
self.words_list = self.words_list.filter_prefix(self.textbox.content());
|
self.words_list = self.words_list.filter_prefix(self.textbox.content());
|
||||||
|
|
||||||
@ -126,11 +143,14 @@ impl Bip39Entry {
|
|||||||
if self.words_list.len() < OFFER_WORDS_THRESHOLD {
|
if self.words_list.len() < OFFER_WORDS_THRESHOLD {
|
||||||
self.offer_words = true;
|
self.offer_words = true;
|
||||||
let word_choices = self.words_list.iter().collect();
|
let word_choices = self.words_list.iter().collect();
|
||||||
ChoiceFactoryBIP39::words(word_choices)
|
ChoiceFactoryWordlist::words(word_choices)
|
||||||
} else {
|
} else {
|
||||||
self.offer_words = false;
|
self.offer_words = false;
|
||||||
self.letter_choices = bip39::get_available_letters(self.textbox.content()).collect();
|
self.letter_choices = self
|
||||||
ChoiceFactoryBIP39::letters(self.letter_choices.clone())
|
.words_list
|
||||||
|
.get_available_letters(self.textbox.content())
|
||||||
|
.collect();
|
||||||
|
ChoiceFactoryWordlist::letters(self.letter_choices.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +181,7 @@ impl Bip39Entry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn reset_wordlist(&mut self) {
|
fn reset_wordlist(&mut self) {
|
||||||
self.words_list = bip39::Wordlist::all();
|
self.words_list = Self::get_fresh_wordlist(&self.wordlist_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the index of DELETE item, which is always at the end.
|
/// Get the index of DELETE item, which is always at the end.
|
||||||
@ -174,8 +194,8 @@ impl Bip39Entry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Bip39Entry {
|
impl Component for WordlistEntry {
|
||||||
type Msg = Bip39EntryMsg;
|
type Msg = WordlistEntryMsg;
|
||||||
|
|
||||||
fn place(&mut self, bounds: Rect) -> Rect {
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
let letters_area_height = self.chosen_letters.inner().needed_height();
|
let letters_area_height = self.chosen_letters.inner().needed_height();
|
||||||
@ -201,7 +221,7 @@ impl Component for Bip39Entry {
|
|||||||
.words_list
|
.words_list
|
||||||
.get(page_counter as usize)
|
.get(page_counter as usize)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
return Some(Bip39EntryMsg::ResultWord(String::from(word)));
|
return Some(WordlistEntryMsg::ResultWord(String::from(word)));
|
||||||
} else {
|
} else {
|
||||||
let new_letter = self.letter_choices[page_counter as usize];
|
let new_letter = self.letter_choices[page_counter as usize];
|
||||||
self.append_letter(ctx, new_letter);
|
self.append_letter(ctx, new_letter);
|
||||||
@ -226,7 +246,7 @@ use super::super::{ButtonAction, ButtonPos};
|
|||||||
use crate::ui::util;
|
use crate::ui::util;
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
impl crate::trace::Trace for Bip39Entry {
|
impl crate::trace::Trace for WordlistEntry {
|
||||||
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
||||||
match pos {
|
match pos {
|
||||||
ButtonPos::Left => ButtonAction::PrevPage.string(),
|
ButtonPos::Left => ButtonAction::PrevPage.string(),
|
@ -33,13 +33,13 @@ pub use flow_pages::{FlowPages, Page};
|
|||||||
pub use frame::Frame;
|
pub use frame::Frame;
|
||||||
pub use homescreen::{Homescreen, HomescreenMsg, Lockscreen};
|
pub use homescreen::{Homescreen, HomescreenMsg, Lockscreen};
|
||||||
pub use input_methods::{
|
pub use input_methods::{
|
||||||
bip39::{Bip39Entry, Bip39EntryMsg},
|
|
||||||
choice::{Choice, ChoiceFactory, ChoicePage, ChoicePageMsg},
|
choice::{Choice, ChoiceFactory, ChoicePage, ChoicePageMsg},
|
||||||
choice_item::ChoiceItem,
|
choice_item::ChoiceItem,
|
||||||
number_input::{NumberInput, NumberInputMsg},
|
number_input::{NumberInput, NumberInputMsg},
|
||||||
passphrase::{PassphraseEntry, PassphraseEntryMsg},
|
passphrase::{PassphraseEntry, PassphraseEntryMsg},
|
||||||
pin::{PinEntry, PinEntryMsg},
|
pin::{PinEntry, PinEntryMsg},
|
||||||
simple_choice::{SimpleChoice, SimpleChoiceMsg},
|
simple_choice::{SimpleChoice, SimpleChoiceMsg},
|
||||||
|
wordlist::{WordlistEntry, WordlistEntryMsg, WordlistType},
|
||||||
};
|
};
|
||||||
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
|
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
|
||||||
pub use no_btn_dialog::{NoBtnDialog, NoBtnDialogMsg};
|
pub use no_btn_dialog::{NoBtnDialog, NoBtnDialogMsg};
|
||||||
|
@ -37,10 +37,10 @@ use crate::{
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
component::{
|
component::{
|
||||||
Bip39Entry, Bip39EntryMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow,
|
ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, Flow, FlowMsg, FlowPages, Frame,
|
||||||
FlowMsg, FlowPages, Frame, Homescreen, HomescreenMsg, Lockscreen, NoBtnDialog,
|
Homescreen, HomescreenMsg, Lockscreen, NoBtnDialog, NoBtnDialogMsg, NumberInput,
|
||||||
NoBtnDialogMsg, NumberInput, NumberInputMsg, Page, PassphraseEntry, PassphraseEntryMsg,
|
NumberInputMsg, Page, PassphraseEntry, PassphraseEntryMsg, PinEntry, PinEntryMsg, Progress,
|
||||||
PinEntry, PinEntryMsg, Progress, ShareWords, SimpleChoice, SimpleChoiceMsg,
|
ShareWords, SimpleChoice, SimpleChoiceMsg, WordlistEntry, WordlistEntryMsg, WordlistType,
|
||||||
},
|
},
|
||||||
constant, theme,
|
constant, theme,
|
||||||
};
|
};
|
||||||
@ -127,10 +127,10 @@ impl<const N: usize> ComponentMsgObj for SimpleChoice<N> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ComponentMsgObj for Bip39Entry {
|
impl ComponentMsgObj for WordlistEntry {
|
||||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||||
match msg {
|
match msg {
|
||||||
Bip39EntryMsg::ResultWord(word) => word.as_str().try_into(),
|
WordlistEntryMsg::ResultWord(word) => word.as_str().try_into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -766,7 +766,21 @@ extern "C" fn new_request_bip39(n_args: usize, args: *const Obj, kwargs: *mut Ma
|
|||||||
let block = |_args: &[Obj], kwargs: &Map| {
|
let block = |_args: &[Obj], kwargs: &Map| {
|
||||||
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
||||||
|
|
||||||
let obj = LayoutObj::new(Frame::new(prompt, Bip39Entry::new()).with_title_center(true))?;
|
let obj = LayoutObj::new(
|
||||||
|
Frame::new(prompt, WordlistEntry::new(WordlistType::Bip39)).with_title_center(true),
|
||||||
|
)?;
|
||||||
|
Ok(obj.into())
|
||||||
|
};
|
||||||
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" fn new_request_slip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||||
|
let block = |_args: &[Obj], kwargs: &Map| {
|
||||||
|
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
|
||||||
|
|
||||||
|
let obj = LayoutObj::new(
|
||||||
|
Frame::new(prompt, WordlistEntry::new(WordlistType::Slip39)).with_title_center(true),
|
||||||
|
)?;
|
||||||
Ok(obj.into())
|
Ok(obj.into())
|
||||||
};
|
};
|
||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
@ -1024,6 +1038,13 @@ pub static mp_module_trezorui2: Module = obj_module! {
|
|||||||
/// """Get recovery word for BIP39."""
|
/// """Get recovery word for BIP39."""
|
||||||
Qstr::MP_QSTR_request_bip39 => obj_fn_kw!(0, new_request_bip39).as_obj(),
|
Qstr::MP_QSTR_request_bip39 => obj_fn_kw!(0, new_request_bip39).as_obj(),
|
||||||
|
|
||||||
|
/// def request_slip39(
|
||||||
|
/// *,
|
||||||
|
/// prompt: str,
|
||||||
|
/// ) -> str:
|
||||||
|
/// """SLIP39 word input keyboard."""
|
||||||
|
Qstr::MP_QSTR_request_slip39 => obj_fn_kw!(0, new_request_slip39).as_obj(),
|
||||||
|
|
||||||
/// def request_passphrase(
|
/// def request_passphrase(
|
||||||
/// *,
|
/// *,
|
||||||
/// prompt: str,
|
/// prompt: str,
|
||||||
|
@ -166,6 +166,14 @@ def request_bip39(
|
|||||||
"""Get recovery word for BIP39."""
|
"""Get recovery word for BIP39."""
|
||||||
|
|
||||||
|
|
||||||
|
# rust/src/ui/model_tr/layout.rs
|
||||||
|
def request_slip39(
|
||||||
|
*,
|
||||||
|
prompt: str,
|
||||||
|
) -> str:
|
||||||
|
"""SLIP39 word input keyboard."""
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/model_tr/layout.rs
|
# rust/src/ui/model_tr/layout.rs
|
||||||
def request_passphrase(
|
def request_passphrase(
|
||||||
*,
|
*,
|
||||||
|
@ -759,7 +759,7 @@ def show_warning(
|
|||||||
ctx,
|
ctx,
|
||||||
br_type,
|
br_type,
|
||||||
"",
|
"",
|
||||||
subheader or "Warning",
|
subheader or "WARNING",
|
||||||
content,
|
content,
|
||||||
button_confirm=button,
|
button_confirm=button,
|
||||||
button_cancel=None,
|
button_cancel=None,
|
||||||
|
@ -5,7 +5,7 @@ from trezor.enums import ButtonRequestType
|
|||||||
import trezorui2
|
import trezorui2
|
||||||
|
|
||||||
from ..common import button_request, interact
|
from ..common import button_request, interact
|
||||||
from . import RustLayout, get_bool
|
from . import RustLayout, confirm_action, get_bool
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from trezor import wire
|
from trezor import wire
|
||||||
@ -20,7 +20,7 @@ async def request_word_count(ctx: wire.GenericContext, dry_run: bool) -> int:
|
|||||||
"word_count",
|
"word_count",
|
||||||
ButtonRequestType.MnemonicWordCount,
|
ButtonRequestType.MnemonicWordCount,
|
||||||
)
|
)
|
||||||
# It can be returning a string
|
# It can be returning a string (for example for __debug__ in tests)
|
||||||
return int(count)
|
return int(count)
|
||||||
|
|
||||||
|
|
||||||
@ -30,15 +30,11 @@ async def request_word(
|
|||||||
prompt = f"WORD {word_index + 1} OF {word_count}"
|
prompt = f"WORD {word_index + 1} OF {word_count}"
|
||||||
|
|
||||||
if is_slip39:
|
if is_slip39:
|
||||||
raise NotImplementedError
|
word_choice = RustLayout(trezorui2.request_slip39(prompt=prompt))
|
||||||
else:
|
else:
|
||||||
word = await interact(
|
word_choice = RustLayout(trezorui2.request_bip39(prompt=prompt))
|
||||||
ctx,
|
|
||||||
RustLayout(trezorui2.request_bip39(prompt=prompt)),
|
|
||||||
"request_word",
|
|
||||||
ButtonRequestType.MnemonicInput,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
word: str = await ctx.wait(word_choice)
|
||||||
return word
|
return word
|
||||||
|
|
||||||
|
|
||||||
@ -54,7 +50,14 @@ async def show_remaining_shares(
|
|||||||
async def show_group_share_success(
|
async def show_group_share_success(
|
||||||
ctx: wire.GenericContext, share_index: int, group_index: int
|
ctx: wire.GenericContext, share_index: int, group_index: int
|
||||||
) -> None:
|
) -> None:
|
||||||
raise NotImplementedError
|
await confirm_action(
|
||||||
|
ctx,
|
||||||
|
"share_success",
|
||||||
|
"Success",
|
||||||
|
description=f"You have entered\nShare {share_index + 1} from\nGroup {group_index + 1}",
|
||||||
|
verb="CONTINUE",
|
||||||
|
verb_cancel=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def continue_recovery(
|
async def continue_recovery(
|
||||||
@ -65,19 +68,25 @@ async def continue_recovery(
|
|||||||
info_func: Callable | None,
|
info_func: Callable | None,
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
# NOTE: no need to implement `info_func`, as it is used only in
|
# TODO: implement info_func?
|
||||||
# Shamir backup, which is not implemented for TR.
|
# There is very limited space on the screen
|
||||||
|
# (and having middle button would mean shortening the right button text)
|
||||||
|
|
||||||
description = text
|
description = text
|
||||||
if subtext:
|
if subtext:
|
||||||
description += f"\n\n{subtext}"
|
description += f"\n\n{subtext}"
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
title = "SEED CHECK"
|
||||||
|
else:
|
||||||
|
title = "RECOVERY MODE"
|
||||||
|
|
||||||
return await get_bool(
|
return await get_bool(
|
||||||
ctx,
|
ctx,
|
||||||
"recovery",
|
"recovery",
|
||||||
"START RECOVERY",
|
title,
|
||||||
None,
|
None,
|
||||||
description,
|
description,
|
||||||
verb="HOLD TO BEGIN",
|
verb=button_label.upper(),
|
||||||
hold=True,
|
|
||||||
br_code=ButtonRequestType.RecoveryHomepage,
|
br_code=ButtonRequestType.RecoveryHomepage,
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user