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();
|
||||
'characters: loop {
|
||||
if let Some(character) = text.chars().nth(characters_drawn) {
|
||||
unwrap!(line_text.push(character));
|
||||
characters_drawn += 1;
|
||||
if character == '\n' {
|
||||
// The line is forced to end.
|
||||
break 'characters;
|
||||
}
|
||||
unwrap!(line_text.push(character));
|
||||
} else {
|
||||
// No more characters to draw.
|
||||
break 'characters;
|
||||
@ -994,11 +998,11 @@ pub fn text_multiline(
|
||||
}
|
||||
}
|
||||
text_left(baseline, &line_text, font, fg_color, bg_color);
|
||||
taken_from_top += line_height;
|
||||
if characters_drawn == characters_overall {
|
||||
// No more lines to draw.
|
||||
break 'lines;
|
||||
}
|
||||
taken_from_top += line_height;
|
||||
}
|
||||
// Some of the area was unused and is free to draw some further text.
|
||||
Some(area.split_top(taken_from_top).1)
|
||||
|
@ -2,31 +2,28 @@ use crate::{
|
||||
micropython::buffer::StrBuffer,
|
||||
ui::{
|
||||
component::{Component, Event, EventCtx, Never, Paginate},
|
||||
display::Font,
|
||||
display::{text_multiline, Font},
|
||||
geometry::{Offset, Rect},
|
||||
model_tr::theme,
|
||||
},
|
||||
};
|
||||
|
||||
use heapless::{String, Vec};
|
||||
|
||||
use super::common::display_inverse;
|
||||
use super::common::display;
|
||||
|
||||
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_WORD_OFFSET: i16 = 20;
|
||||
const NUMBER_FONT: Font = Font::DEMIBOLD;
|
||||
const WORD_FONT: Font = Font::NORMAL;
|
||||
|
||||
/// 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> {
|
||||
area: Rect,
|
||||
share_words: Vec<StrBuffer, N>,
|
||||
word_index: usize,
|
||||
page_index: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize> ShareWords<N> {
|
||||
@ -34,7 +31,79 @@ impl<const N: usize> ShareWords<N> {
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
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) {
|
||||
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_inverse(baseline, &inttostr!(index as u8 + 1), NUMBER_FONT);
|
||||
display_inverse(baseline + Offset::x(NUMBER_WORD_OFFSET), &word, WORD_FONT);
|
||||
if self.is_entry_page() {
|
||||
self.render_entry_page();
|
||||
} else if self.is_final_page() {
|
||||
self.render_final_page();
|
||||
} else {
|
||||
self.render_words();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Paginate for ShareWords<N> {
|
||||
fn page_count(&mut self) -> usize {
|
||||
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
|
||||
}
|
||||
// Not defining the logic here, as we do not want it to be `&mut`.
|
||||
self.total_page_count()
|
||||
}
|
||||
|
||||
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) {
|
||||
t.open("ShareWords");
|
||||
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 {
|
||||
let index = self.word_index + i;
|
||||
let index = self.word_index() + i;
|
||||
let word = self.share_words[index].clone();
|
||||
let content = build_string!(20, inttostr!(index as u8 + 1), " ", &word, "\n");
|
||||
t.string(&content);
|
||||
}
|
||||
}
|
||||
t.content_flag();
|
||||
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 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(
|
||||
ButtonPage::new_str(ShareWords::new(share_words), theme::FG)
|
||||
.with_cancel_btn(None)
|
||||
.with_confirm_btn(confirm_btn)
|
||||
.with_scrollbar(false),
|
||||
ButtonPage::new_str(ShareWords::new(share_words), theme::BG)
|
||||
.with_confirm_btn(confirm_btn),
|
||||
)?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ from trezor.enums import ButtonRequestType
|
||||
import trezorui2
|
||||
|
||||
from ..common import interact
|
||||
from . import RustLayout, _placeholder_confirm, confirm_action, get_bool
|
||||
from . import RustLayout, confirm_action, get_bool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from trezor import wire
|
||||
@ -19,15 +19,6 @@ async def show_share_words(
|
||||
share_index: int | None = None,
|
||||
group_index: int | 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
|
||||
# until user accepts everything.
|
||||
while True:
|
||||
@ -42,18 +33,6 @@ async def show_share_words(
|
||||
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(
|
||||
ctx=ctx,
|
||||
title="CHECK PHRASE",
|
||||
|
Loading…
Reference in New Issue
Block a user