mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-07-07 07:12:34 +00:00
WIP - seed backup design
This commit is contained in:
parent
2376891b83
commit
70a2eb5ffb
@ -980,8 +980,12 @@ pub fn text_multiline(
|
|||||||
let mut line_text: String<50> = String::new();
|
let mut line_text: String<50> = String::new();
|
||||||
'characters: loop {
|
'characters: loop {
|
||||||
if let Some(character) = text.chars().nth(characters_drawn) {
|
if let Some(character) = text.chars().nth(characters_drawn) {
|
||||||
unwrap!(line_text.push(character));
|
|
||||||
characters_drawn += 1;
|
characters_drawn += 1;
|
||||||
|
if character == '\n' {
|
||||||
|
// The line is forced to end.
|
||||||
|
break 'characters;
|
||||||
|
}
|
||||||
|
unwrap!(line_text.push(character));
|
||||||
} else {
|
} else {
|
||||||
// No more characters to draw.
|
// No more characters to draw.
|
||||||
break 'characters;
|
break 'characters;
|
||||||
@ -994,11 +998,11 @@ pub fn text_multiline(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
text_left(baseline, &line_text, font, fg_color, bg_color);
|
text_left(baseline, &line_text, font, fg_color, bg_color);
|
||||||
|
taken_from_top += line_height;
|
||||||
if characters_drawn == characters_overall {
|
if characters_drawn == characters_overall {
|
||||||
// No more lines to draw.
|
// No more lines to draw.
|
||||||
break 'lines;
|
break 'lines;
|
||||||
}
|
}
|
||||||
taken_from_top += line_height;
|
|
||||||
}
|
}
|
||||||
// Some of the area was unused and is free to draw some further text.
|
// Some of the area was unused and is free to draw some further text.
|
||||||
Some(area.split_top(taken_from_top).1)
|
Some(area.split_top(taken_from_top).1)
|
||||||
|
@ -2,31 +2,28 @@ use crate::{
|
|||||||
micropython::buffer::StrBuffer,
|
micropython::buffer::StrBuffer,
|
||||||
ui::{
|
ui::{
|
||||||
component::{Component, Event, EventCtx, Never, Paginate},
|
component::{Component, Event, EventCtx, Never, Paginate},
|
||||||
display::Font,
|
display::{text_multiline, Font},
|
||||||
geometry::{Offset, Rect},
|
geometry::{Offset, Rect},
|
||||||
|
model_tr::theme,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use heapless::{String, Vec};
|
use heapless::{String, Vec};
|
||||||
|
|
||||||
use super::common::display_inverse;
|
use super::common::display;
|
||||||
|
|
||||||
const WORDS_PER_PAGE: usize = 3;
|
const WORDS_PER_PAGE: usize = 3;
|
||||||
const EXTRA_LINE_HEIGHT: i16 = 3;
|
const EXTRA_LINE_HEIGHT: i16 = 2;
|
||||||
const NUMBER_X_OFFSET: i16 = 5;
|
const NUMBER_X_OFFSET: i16 = 5;
|
||||||
const NUMBER_WORD_OFFSET: i16 = 20;
|
const NUMBER_WORD_OFFSET: i16 = 20;
|
||||||
const NUMBER_FONT: Font = Font::DEMIBOLD;
|
const NUMBER_FONT: Font = Font::DEMIBOLD;
|
||||||
const WORD_FONT: Font = Font::NORMAL;
|
const WORD_FONT: Font = Font::NORMAL;
|
||||||
|
|
||||||
/// Showing the given share words.
|
/// Showing the given share words.
|
||||||
///
|
|
||||||
/// Displays them in inverse colors - black text on white background.
|
|
||||||
/// It is that because of the OLED side attack - lot of white noise makes
|
|
||||||
/// the attack much harder.
|
|
||||||
pub struct ShareWords<const N: usize> {
|
pub struct ShareWords<const N: usize> {
|
||||||
area: Rect,
|
area: Rect,
|
||||||
share_words: Vec<StrBuffer, N>,
|
share_words: Vec<StrBuffer, N>,
|
||||||
word_index: usize,
|
page_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> ShareWords<N> {
|
impl<const N: usize> ShareWords<N> {
|
||||||
@ -34,7 +31,79 @@ impl<const N: usize> ShareWords<N> {
|
|||||||
Self {
|
Self {
|
||||||
area: Rect::zero(),
|
area: Rect::zero(),
|
||||||
share_words,
|
share_words,
|
||||||
word_index: 0,
|
page_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn word_index(&self) -> usize {
|
||||||
|
(self.page_index - 1) * WORDS_PER_PAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_entry_page(&self) -> bool {
|
||||||
|
self.page_index == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_final_page(&self) -> bool {
|
||||||
|
self.page_index == self.total_page_count() - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn total_page_count(&self) -> usize {
|
||||||
|
let word_screens = if self.share_words.len() % WORDS_PER_PAGE == 0 {
|
||||||
|
self.share_words.len() / WORDS_PER_PAGE
|
||||||
|
} else {
|
||||||
|
self.share_words.len() / WORDS_PER_PAGE + 1
|
||||||
|
};
|
||||||
|
// One page before the words, one after it
|
||||||
|
1 + word_screens + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display the first page with user information.
|
||||||
|
fn render_entry_page(&self) {
|
||||||
|
// TODO: will it be always 12, or do we need to check the length?
|
||||||
|
// It would need creating a String out of it, which is not ideal.
|
||||||
|
let free_area = text_multiline(
|
||||||
|
self.area,
|
||||||
|
"Write all 12\nwords in order on\nrecovery seed card.",
|
||||||
|
Font::BOLD,
|
||||||
|
theme::FG,
|
||||||
|
theme::BG,
|
||||||
|
);
|
||||||
|
if let Some(free_area) = free_area {
|
||||||
|
// Creating a small vertical distance
|
||||||
|
text_multiline(
|
||||||
|
free_area.split_top(3).1,
|
||||||
|
"Do NOT make\ndigital copies!",
|
||||||
|
Font::MONO,
|
||||||
|
theme::FG,
|
||||||
|
theme::BG,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display the final page with user confirmation.
|
||||||
|
fn render_final_page(&self) {
|
||||||
|
// Moving vertically down to avoid collision with the scrollbar
|
||||||
|
// and to look better.
|
||||||
|
text_multiline(
|
||||||
|
self.area.split_top(12).1,
|
||||||
|
"I wrote down all\n12 words in order.",
|
||||||
|
Font::MONO,
|
||||||
|
theme::FG,
|
||||||
|
theme::BG,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display current set of recovery words.
|
||||||
|
fn render_words(&self) {
|
||||||
|
let mut y_offset = 0;
|
||||||
|
// Showing the word index and the words itself
|
||||||
|
for i in 0..WORDS_PER_PAGE {
|
||||||
|
y_offset += NUMBER_FONT.line_height() + EXTRA_LINE_HEIGHT;
|
||||||
|
let index = self.word_index() + i;
|
||||||
|
let word = self.share_words[index].clone();
|
||||||
|
let baseline = self.area.top_left() + Offset::new(NUMBER_X_OFFSET, y_offset);
|
||||||
|
display(baseline, &inttostr!(index as u8 + 1), NUMBER_FONT);
|
||||||
|
display(baseline + Offset::x(NUMBER_WORD_OFFSET), &word, WORD_FONT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,30 +121,24 @@ impl<const N: usize> Component for ShareWords<N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
let mut y_offset = 0;
|
if self.is_entry_page() {
|
||||||
// Showing the word index and the words itself
|
self.render_entry_page();
|
||||||
for i in 0..WORDS_PER_PAGE {
|
} else if self.is_final_page() {
|
||||||
y_offset += NUMBER_FONT.line_height() + EXTRA_LINE_HEIGHT;
|
self.render_final_page();
|
||||||
let index = self.word_index + i;
|
} else {
|
||||||
let word = self.share_words[index].clone();
|
self.render_words();
|
||||||
let baseline = self.area.top_left() + Offset::new(NUMBER_X_OFFSET, y_offset);
|
|
||||||
display_inverse(baseline, &inttostr!(index as u8 + 1), NUMBER_FONT);
|
|
||||||
display_inverse(baseline + Offset::x(NUMBER_WORD_OFFSET), &word, WORD_FONT);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> Paginate for ShareWords<N> {
|
impl<const N: usize> Paginate for ShareWords<N> {
|
||||||
fn page_count(&mut self) -> usize {
|
fn page_count(&mut self) -> usize {
|
||||||
if self.share_words.len() % WORDS_PER_PAGE == 0 {
|
// Not defining the logic here, as we do not want it to be `&mut`.
|
||||||
self.share_words.len() / WORDS_PER_PAGE
|
self.total_page_count()
|
||||||
} else {
|
|
||||||
self.share_words.len() / WORDS_PER_PAGE + 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn change_page(&mut self, active_page: usize) {
|
fn change_page(&mut self, active_page: usize) {
|
||||||
self.word_index = active_page * WORDS_PER_PAGE;
|
self.page_index = active_page;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,12 +147,18 @@ impl<const N: usize> crate::trace::Trace for ShareWords<N> {
|
|||||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
t.open("ShareWords");
|
t.open("ShareWords");
|
||||||
t.content_flag();
|
t.content_flag();
|
||||||
|
if self.is_entry_page() {
|
||||||
|
t.string("entry page");
|
||||||
|
} else if self.is_final_page() {
|
||||||
|
t.string("final page");
|
||||||
|
} else {
|
||||||
for i in 0..WORDS_PER_PAGE {
|
for i in 0..WORDS_PER_PAGE {
|
||||||
let index = self.word_index + i;
|
let index = self.word_index() + i;
|
||||||
let word = self.share_words[index].clone();
|
let word = self.share_words[index].clone();
|
||||||
let content = build_string!(20, inttostr!(index as u8 + 1), " ", &word, "\n");
|
let content = build_string!(20, inttostr!(index as u8 + 1), " ", &word, "\n");
|
||||||
t.string(&content);
|
t.string(&content);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
t.content_flag();
|
t.content_flag();
|
||||||
t.close();
|
t.close();
|
||||||
}
|
}
|
||||||
|
@ -524,13 +524,11 @@ extern "C" fn show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map
|
|||||||
let share_words: Vec<StrBuffer, 24> = iter_into_vec(share_words_obj)?;
|
let share_words: Vec<StrBuffer, 24> = iter_into_vec(share_words_obj)?;
|
||||||
|
|
||||||
let confirm_btn =
|
let confirm_btn =
|
||||||
Some(ButtonDetails::text("CONFIRM").with_duration(Duration::from_secs(1)));
|
Some(ButtonDetails::text("HOLD TO CONFIRM").with_duration(Duration::from_secs(1)));
|
||||||
|
|
||||||
let obj = LayoutObj::new(
|
let obj = LayoutObj::new(
|
||||||
ButtonPage::new_str(ShareWords::new(share_words), theme::FG)
|
ButtonPage::new_str(ShareWords::new(share_words), theme::BG)
|
||||||
.with_cancel_btn(None)
|
.with_confirm_btn(confirm_btn),
|
||||||
.with_confirm_btn(confirm_btn)
|
|
||||||
.with_scrollbar(false),
|
|
||||||
)?;
|
)?;
|
||||||
Ok(obj.into())
|
Ok(obj.into())
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@ from trezor.enums import ButtonRequestType
|
|||||||
import trezorui2
|
import trezorui2
|
||||||
|
|
||||||
from ..common import interact
|
from ..common import interact
|
||||||
from . import RustLayout, _placeholder_confirm, confirm_action, get_bool
|
from . import RustLayout, confirm_action, get_bool
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from trezor import wire
|
from trezor import wire
|
||||||
@ -19,15 +19,6 @@ async def show_share_words(
|
|||||||
share_index: int | None = None,
|
share_index: int | None = None,
|
||||||
group_index: int | None = None,
|
group_index: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
await _placeholder_confirm(
|
|
||||||
ctx=ctx,
|
|
||||||
br_type="backup_words",
|
|
||||||
title="SHARE WORDS",
|
|
||||||
description="Write all words in order on recovery seed card.",
|
|
||||||
data="Do NOT make digital copies.",
|
|
||||||
br_code=ButtonRequestType.ResetDevice,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Showing words, asking for write down confirmation and preparing for check
|
# Showing words, asking for write down confirmation and preparing for check
|
||||||
# until user accepts everything.
|
# until user accepts everything.
|
||||||
while True:
|
while True:
|
||||||
@ -42,18 +33,6 @@ async def show_share_words(
|
|||||||
br_code=ButtonRequestType.ResetDevice,
|
br_code=ButtonRequestType.ResetDevice,
|
||||||
)
|
)
|
||||||
|
|
||||||
wrote_down = await get_bool(
|
|
||||||
ctx=ctx,
|
|
||||||
title="SHARE WORDS",
|
|
||||||
data=f"I wrote down all {len(share_words)} words in order.",
|
|
||||||
verb="I WROTE DOWN",
|
|
||||||
hold=True,
|
|
||||||
br_type="backup_words",
|
|
||||||
br_code=ButtonRequestType.ResetDevice,
|
|
||||||
)
|
|
||||||
if not wrote_down:
|
|
||||||
continue
|
|
||||||
|
|
||||||
ready_to_check = await get_bool(
|
ready_to_check = await get_bool(
|
||||||
ctx=ctx,
|
ctx=ctx,
|
||||||
title="CHECK PHRASE",
|
title="CHECK PHRASE",
|
||||||
|
Loading…
Reference in New Issue
Block a user