1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-10 08:38:07 +00:00

WIP - show prompts in PIN entry

This commit is contained in:
grdddj 2022-11-14 15:16:41 +01:00
parent 2ac07b6159
commit e9cd9e8ef3
7 changed files with 134 additions and 93 deletions

View File

@ -14,6 +14,8 @@ pub struct ChangingTextLine<T> {
pad: Pad, pad: Pad,
text: T, text: T,
font: Font, font: Font,
/// Whether to show the text. Can be disabled.
show_content: bool,
line_alignment: LineAlignment, line_alignment: LineAlignment,
} }
@ -27,6 +29,7 @@ where
pad: Pad::with_background(theme::BG), pad: Pad::with_background(theme::BG),
text, text,
font, font,
show_content: true,
line_alignment, line_alignment,
} }
} }
@ -35,9 +38,20 @@ where
Self::new(text, Font::MONO, LineAlignment::Center) Self::new(text, Font::MONO, LineAlignment::Center)
} }
pub fn center_bold(text: T) -> Self {
Self::new(text, Font::BOLD, LineAlignment::Center)
}
// Update the text to be displayed in the line.
pub fn update_text(&mut self, text: T) { pub fn update_text(&mut self, text: T) {
self.text = text; self.text = text;
self.pad.clear(); }
// Whether we should display the text content.
// If not, the whole area (Pad) will still be cleared.
// Is valid until this function is called again.
pub fn show_or_not(&mut self, show: bool) {
self.show_content = show;
} }
/// Gets the height that is needed for this line to fit perfectly /// Gets the height that is needed for this line to fit perfectly
@ -85,11 +99,17 @@ where
} }
fn paint(&mut self) { fn paint(&mut self) {
// Always re-painting from scratch.
// Effectively clearing the line completely
// when `self.show_content` is set to `false`.
self.pad.clear();
self.pad.paint(); self.pad.paint();
match self.line_alignment { if self.show_content {
LineAlignment::Left => self.paint_left(), match self.line_alignment {
LineAlignment::Center => self.paint_center(), LineAlignment::Left => self.paint_left(),
LineAlignment::Right => self.paint_right(), LineAlignment::Center => self.paint_center(),
LineAlignment::Right => self.paint_right(),
}
} }
} }
} }

View File

@ -1,5 +1,4 @@
use crate::{ use crate::{
micropython::buffer::StrBuffer,
trezorhal::random, trezorhal::random,
ui::{ ui::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx}, component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
@ -21,8 +20,6 @@ pub enum PinEntryMsg {
} }
const MAX_PIN_LENGTH: usize = 50; const MAX_PIN_LENGTH: usize = 50;
const MAX_VISIBLE_DOTS: usize = 18;
const MAX_VISIBLE_DIGITS: usize = 18;
const CHOICE_LENGTH: usize = 13; const CHOICE_LENGTH: usize = 13;
const DELETE_INDEX: usize = 0; const DELETE_INDEX: usize = 0;
@ -33,13 +30,11 @@ const CHOICES: [&str; CHOICE_LENGTH] = [
"DELETE", "SHOW", "ENTER", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "DELETE", "SHOW", "ENTER", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
]; ];
struct ChoiceFactoryPIN { struct ChoiceFactoryPIN {}
prompt: StrBuffer,
}
impl ChoiceFactoryPIN { impl ChoiceFactoryPIN {
fn new(prompt: StrBuffer) -> Self { fn new() -> Self {
Self { prompt } Self {}
} }
} }
@ -75,23 +70,32 @@ impl ChoiceFactory for ChoiceFactoryPIN {
} }
/// Component for entering a PIN. /// Component for entering a PIN.
pub struct PinEntry { pub struct PinEntry<T> {
choice_page: ChoicePage<ChoiceFactoryPIN>, choice_page: ChoicePage<ChoiceFactoryPIN>,
pin_dots: Child<ChangingTextLine<String<MAX_PIN_LENGTH>>>, pin_line: Child<ChangingTextLine<String<MAX_PIN_LENGTH>>>,
subprompt_line: Child<ChangingTextLine<T>>,
prompt: T,
show_real_pin: bool, show_real_pin: bool,
textbox: TextBox<MAX_PIN_LENGTH>, textbox: TextBox<MAX_PIN_LENGTH>,
} }
impl PinEntry { impl<T> PinEntry<T>
pub fn new(prompt: StrBuffer) -> Self { where
let choices = ChoiceFactoryPIN::new(prompt); T: AsRef<str> + Clone,
{
pub fn new(prompt: T, subprompt: T) -> Self {
let choices = ChoiceFactoryPIN::new();
Self { Self {
// Starting at the digit 0 // Starting at the digit 0
choice_page: ChoicePage::new(choices) choice_page: ChoicePage::new(choices)
.with_initial_page_counter(NUMBER_START_INDEX as u8) .with_initial_page_counter(NUMBER_START_INDEX as u8)
.with_carousel(true), .with_carousel(true),
pin_dots: Child::new(ChangingTextLine::center_mono(String::new())), pin_line: Child::new(ChangingTextLine::center_bold(String::from(
prompt.clone().as_ref(),
))),
subprompt_line: Child::new(ChangingTextLine::center_mono(subprompt)),
prompt,
show_real_pin: false, show_real_pin: false,
textbox: TextBox::empty(), textbox: TextBox::empty(),
} }
@ -106,23 +110,39 @@ impl PinEntry {
self.textbox.delete_last(ctx); self.textbox.delete_last(ctx);
} }
fn update_pin_dots(&mut self, ctx: &mut EventCtx) { /// Performs overall update of the screen.
// TODO: this is the same action as for the passphrase entry, fn update(&mut self, ctx: &mut EventCtx) {
// might do a common component that will handle this part, self.update_header_info(ctx);
// (something like `SecretTextLine`) ctx.request_paint();
// also with things like shifting the dots when too many etc. }
// TODO: when the PIN is longer than fits the screen, we might show ellipsis
if self.show_real_pin { /// Update the header information - (sub)prompt and visible PIN.
let pin = String::from(self.pin()); /// If PIN is empty, showing prompt in `pin_line` and sub-prompt in the
self.pin_dots.inner_mut().update_text(pin); /// `subprompt_line`. Otherwise disabling the `subprompt_line` and showing
/// the PIN - either in real numbers or masked in asterisks.
fn update_header_info(&mut self, ctx: &mut EventCtx) {
let show_prompts = self.is_empty();
let text = if show_prompts {
String::from(self.prompt.as_ref())
} else if self.show_real_pin {
String::from(self.pin())
} else { } else {
let mut dots: String<MAX_PIN_LENGTH> = String::new(); let mut dots: String<MAX_PIN_LENGTH> = String::new();
for _ in 0..self.textbox.len() { for _ in 0..self.textbox.len() {
unwrap!(dots.push_str("*")); unwrap!(dots.push_str("*"));
} }
self.pin_dots.inner_mut().update_text(dots); dots
} };
self.pin_dots.request_complete_repaint(ctx);
// Putting the current text into the PIN line.
self.pin_line.inner_mut().update_text(text);
// Showing subprompt only conditionally.
self.subprompt_line.inner_mut().show_or_not(show_prompts);
// Force repaint of the whole header.
self.pin_line.request_complete_repaint(ctx);
self.subprompt_line.request_complete_repaint(ctx);
} }
pub fn pin(&self) -> &str { pub fn pin(&self) -> &str {
@ -138,13 +158,19 @@ impl PinEntry {
} }
} }
impl Component for PinEntry { impl<T> Component for PinEntry<T>
where
T: AsRef<str> + Clone,
{
type Msg = PinEntryMsg; type Msg = PinEntryMsg;
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
let pin_area_height = self.pin_dots.inner().needed_height(); let pin_height = self.pin_line.inner().needed_height();
let (pin_area, choice_area) = bounds.split_top(pin_area_height); let subtitle_height = self.subprompt_line.inner().needed_height();
self.pin_dots.place(pin_area); let (title_area, subtitle_and_choice_area) = bounds.split_top(pin_height);
let (subtitle_area, choice_area) = subtitle_and_choice_area.split_top(subtitle_height);
self.pin_line.place(title_area);
self.subprompt_line.place(subtitle_area);
self.choice_page.place(choice_area); self.choice_page.place(choice_area);
bounds bounds
} }
@ -153,7 +179,7 @@ impl Component for PinEntry {
// Any event when showing real PIN should hide it // Any event when showing real PIN should hide it
if self.show_real_pin { if self.show_real_pin {
self.show_real_pin = false; self.show_real_pin = false;
self.update_pin_dots(ctx); self.update(ctx)
} }
let msg = self.choice_page.event(ctx, event); let msg = self.choice_page.event(ctx, event);
@ -162,19 +188,16 @@ impl Component for PinEntry {
match page_counter as usize { match page_counter as usize {
DELETE_INDEX => { DELETE_INDEX => {
self.delete_last_digit(ctx); self.delete_last_digit(ctx);
self.update_pin_dots(ctx); self.update(ctx);
ctx.request_paint();
} }
SHOW_INDEX => { SHOW_INDEX => {
self.show_real_pin = true; self.show_real_pin = true;
self.update_pin_dots(ctx); self.update(ctx);
ctx.request_paint();
} }
ENTER_INDEX => return Some(PinEntryMsg::Confirmed), ENTER_INDEX => return Some(PinEntryMsg::Confirmed),
_ => { _ => {
if !self.is_full() { if !self.is_full() {
self.append_new_digit(ctx, page_counter); self.append_new_digit(ctx, page_counter);
self.update_pin_dots(ctx);
// Choosing any random digit to be shown next // Choosing any random digit to be shown next
let new_page_counter = random::uniform_between( let new_page_counter = random::uniform_between(
NUMBER_START_INDEX as u32, NUMBER_START_INDEX as u32,
@ -182,7 +205,7 @@ impl Component for PinEntry {
); );
self.choice_page self.choice_page
.set_page_counter(ctx, new_page_counter as u8); .set_page_counter(ctx, new_page_counter as u8);
ctx.request_paint(); self.update(ctx);
} }
} }
} }
@ -191,7 +214,8 @@ impl Component for PinEntry {
} }
fn paint(&mut self) { fn paint(&mut self) {
self.pin_dots.paint(); self.pin_line.paint();
self.subprompt_line.paint();
self.choice_page.paint(); self.choice_page.paint();
} }
} }
@ -200,7 +224,10 @@ impl Component for PinEntry {
use super::{ButtonAction, ButtonPos}; use super::{ButtonAction, ButtonPos};
#[cfg(feature = "ui_debug")] #[cfg(feature = "ui_debug")]
impl crate::trace::Trace for PinEntry { impl<T> crate::trace::Trace for PinEntry<T>
where
T: AsRef<str> + Clone,
{
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(),

View File

@ -82,7 +82,10 @@ where
} }
} }
impl ComponentMsgObj for PinEntry { impl<T> ComponentMsgObj for PinEntry<T>
where
T: AsRef<str> + Clone,
{
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 {
PinEntryMsg::Confirmed => self.pin().try_into(), PinEntryMsg::Confirmed => self.pin().try_into(),
@ -504,11 +507,11 @@ extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj
extern "C" fn request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { extern "C" fn request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
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 _subprompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_subprompt)?.try_into()?; let subprompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_subprompt)?.try_into()?;
let _allow_cancel: Option<bool> = let _allow_cancel: Option<bool> =
kwargs.get(Qstr::MP_QSTR_allow_cancel)?.try_into_option()?; kwargs.get(Qstr::MP_QSTR_allow_cancel)?.try_into_option()?;
let obj = LayoutObj::new(PinEntry::new(prompt))?; let obj = LayoutObj::new(PinEntry::new(prompt, subprompt))?;
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) }

View File

@ -50,13 +50,13 @@ async def change_pin(ctx: Context, msg: ChangePin) -> Success:
if newpin: if newpin:
if curpin: if curpin:
msg_screen = "You have successfully changed your PIN." msg_screen = "PIN changed."
msg_wire = "PIN changed" msg_wire = "PIN changed"
else: else:
msg_screen = "You have successfully enabled PIN protection." msg_screen = "PIN protection enabled."
msg_wire = "PIN enabled" msg_wire = "PIN enabled"
else: else:
msg_screen = "You have successfully disabled PIN protection." msg_screen = "PIN protection disabled."
msg_wire = "PIN removed" msg_wire = "PIN removed"
await show_success(ctx, "success_pin", msg_screen) await show_success(ctx, "success_pin", msg_screen)
@ -68,30 +68,30 @@ def _require_confirm_change_pin(ctx: Context, msg: ChangePin) -> Awaitable[None]
has_pin = config.has_pin() has_pin = config.has_pin()
title = "PIN settings"
br_code = "set_pin"
if msg.remove and has_pin: # removing pin if msg.remove and has_pin: # removing pin
return confirm_pin_action( return confirm_pin_action(
ctx, ctx,
"set_pin", br_code,
"Remove PIN", title,
"disable PIN protection?", "disable PIN protection?",
"Do you really want to",
) )
if not msg.remove and has_pin: # changing pin if not msg.remove and has_pin: # changing pin
return confirm_pin_action( return confirm_pin_action(
ctx, ctx,
"set_pin", br_code,
"Change PIN", title,
"change your PIN?", "change your PIN?",
"Do you really want to",
) )
if not msg.remove and not has_pin: # setting new pin if not msg.remove and not has_pin: # setting new pin
return confirm_set_new_pin( return confirm_set_new_pin(
ctx, ctx,
"set_pin", br_code,
"Enable PIN", title,
"Do you really want to",
"enable PIN protection?", "enable PIN protection?",
[ [
"PIN will be used to access this device.", "PIN will be used to access this device.",

View File

@ -46,13 +46,13 @@ async def change_wipe_code(ctx: Context, msg: ChangeWipeCode) -> Success:
if wipe_code: if wipe_code:
if has_wipe_code: if has_wipe_code:
msg_screen = "You have successfully changed the wipe code." msg_screen = "Wipe code changed."
msg_wire = "Wipe code changed" msg_wire = "Wipe code changed"
else: else:
msg_screen = "You have successfully set the wipe code." msg_screen = "Wipe code enabled."
msg_wire = "Wipe code set" msg_wire = "Wipe code set"
else: else:
msg_screen = "You have successfully disabled the wipe code." msg_screen = "Wipe code disabled."
msg_wire = "Wipe code removed" msg_wire = "Wipe code removed"
await show_success(ctx, "success_wipe_code", msg_screen) await show_success(ctx, "success_wipe_code", msg_screen)
@ -63,33 +63,35 @@ def _require_confirm_action(
ctx: Context, msg: ChangeWipeCode, has_wipe_code: bool ctx: Context, msg: ChangeWipeCode, has_wipe_code: bool
) -> Awaitable[None]: ) -> Awaitable[None]:
from trezor.wire import ProcessError from trezor.wire import ProcessError
from trezor.ui.layouts import confirm_pin_action from trezor.ui.layouts import confirm_pin_action, confirm_set_new_pin
title = "Wipe code settings"
if msg.remove and has_wipe_code: if msg.remove and has_wipe_code:
return confirm_pin_action( return confirm_pin_action(
ctx, ctx,
"disable_wipe_code", "disable_wipe_code",
"Disable wipe code", title,
"disable wipe code protection?", "disable wipe code protection?",
"Do you really want to",
) )
if not msg.remove and has_wipe_code: if not msg.remove and has_wipe_code:
return confirm_pin_action( return confirm_pin_action(
ctx, ctx,
"change_wipe_code", "change_wipe_code",
"Change wipe code", title,
"change the wipe code?", "change the wipe code?",
"Do you really want to",
) )
if not msg.remove and not has_wipe_code: if not msg.remove and not has_wipe_code:
return confirm_pin_action( return confirm_set_new_pin(
ctx, ctx,
"set_wipe_code", "set_wipe_code",
"Set wipe code", title,
"set the wipe code?", "enable wipe code?",
"Do you really want to", [
"Wipe code will\nbe used to delete this device.",
],
) )
# Removing non-existing wipe code. # Removing non-existing wipe code.
@ -110,7 +112,7 @@ async def _request_wipe_code_confirm(ctx: Context, pin: str) -> str:
) )
continue continue
code2 = await request_pin(ctx, "Re-enter new wipe code") code2 = await request_pin(ctx, "Re-enter wipe code")
if code1 == code2: if code1 == code2:
return code1 return code1
# _wipe_code_mismatch # _wipe_code_mismatch

View File

@ -1254,7 +1254,6 @@ async def request_pin_on_device(
prompt: str, prompt: str,
attempts_remaining: int | None, attempts_remaining: int | None,
allow_cancel: bool, allow_cancel: bool,
shuffle: bool = False,
) -> str: ) -> str:
if attempts_remaining is None: if attempts_remaining is None:
subprompt = "" subprompt = ""
@ -1263,18 +1262,6 @@ async def request_pin_on_device(
else: else:
subprompt = f"{attempts_remaining} tries left" subprompt = f"{attempts_remaining} tries left"
if attempts_remaining is not None:
await confirm_action(
ctx,
"pin_device_info",
"PIN entry",
action=prompt,
description=subprompt,
verb="BEGIN",
verb_cancel=None,
br_code=ButtonRequestType.Other, # cannot use BRT.PinEntry, as debuglink would be sending PIN to this screen
)
await button_request(ctx, "pin_device", code=ButtonRequestType.PinEntry) await button_request(ctx, "pin_device", code=ButtonRequestType.PinEntry)
dialog = RustLayout( dialog = RustLayout(
@ -1282,7 +1269,6 @@ async def request_pin_on_device(
prompt=prompt, prompt=prompt,
subprompt=subprompt, subprompt=subprompt,
allow_cancel=allow_cancel, allow_cancel=allow_cancel,
shuffle=shuffle, # type: ignore [No parameter named "shuffle"]
) )
) )
@ -1290,9 +1276,6 @@ async def request_pin_on_device(
result = await ctx.wait(dialog) result = await ctx.wait(dialog)
if result is trezorui2.CANCELLED: if result is trezorui2.CANCELLED:
raise wire.PinCancelled raise wire.PinCancelled
# TODO: strangely sometimes in UI tests, the result is `CONFIRMED`
# For example in `test_set_remove_wipe_code`, `test_set_pin_to_wipe_code` or
# `test_change_pin`
assert isinstance(result, str) assert isinstance(result, str)
return result return result
@ -1350,19 +1333,25 @@ async def confirm_set_new_pin(
br_type: str, br_type: str,
title: str, title: str,
action: str, action: str,
description: str,
information: list[str], information: list[str],
description: str = "Do you want to",
br_code: ButtonRequestType = ButtonRequestType.Other, br_code: ButtonRequestType = ButtonRequestType.Other,
) -> None: ) -> None:
await confirm_action( await confirm_action(
ctx, ctx,
br_type, br_type,
title, title,
action=f"{description} {action}", description=f"{description} {action}",
verb="ENABLE", verb="ENABLE",
br_code=br_code, br_code=br_code,
) )
# TODO: this is a hack to put the next info on new screen in case of wipe code
# TODO: there should be a possibility to give a list of strings and each of them
# would be rendered on a new screen
if len(information) == 1:
information.append("\n")
information.append( information.append(
"Position of individual numbers will change between entries for more security." "Position of individual numbers will change between entries for more security."
) )

View File

@ -1134,7 +1134,7 @@ async def confirm_pin_action(
br_type: str, br_type: str,
title: str, title: str,
action: str | None, action: str | None,
description: str | None, description: str | None = "Do you really want to",
br_code: ButtonRequestType = ButtonRequestType.Other, br_code: ButtonRequestType = ButtonRequestType.Other,
) -> None: ) -> None:
return await confirm_action( return await confirm_action(
@ -1184,8 +1184,8 @@ async def confirm_set_new_pin(
br_type: str, br_type: str,
title: str, title: str,
action: str, action: str,
description: str,
information: list[str], information: list[str],
description: str = "Do you want to",
br_code: ButtonRequestType = ButtonRequestType.Other, br_code: ButtonRequestType = ButtonRequestType.Other,
) -> None: ) -> None:
await confirm_action( await confirm_action(