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,
text: T,
font: Font,
/// Whether to show the text. Can be disabled.
show_content: bool,
line_alignment: LineAlignment,
}
@ -27,6 +29,7 @@ where
pad: Pad::with_background(theme::BG),
text,
font,
show_content: true,
line_alignment,
}
}
@ -35,9 +38,20 @@ where
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) {
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
@ -85,11 +99,17 @@ where
}
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();
match self.line_alignment {
LineAlignment::Left => self.paint_left(),
LineAlignment::Center => self.paint_center(),
LineAlignment::Right => self.paint_right(),
if self.show_content {
match self.line_alignment {
LineAlignment::Left => self.paint_left(),
LineAlignment::Center => self.paint_center(),
LineAlignment::Right => self.paint_right(),
}
}
}
}

View File

@ -1,5 +1,4 @@
use crate::{
micropython::buffer::StrBuffer,
trezorhal::random,
ui::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
@ -21,8 +20,6 @@ pub enum PinEntryMsg {
}
const MAX_PIN_LENGTH: usize = 50;
const MAX_VISIBLE_DOTS: usize = 18;
const MAX_VISIBLE_DIGITS: usize = 18;
const CHOICE_LENGTH: usize = 13;
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",
];
struct ChoiceFactoryPIN {
prompt: StrBuffer,
}
struct ChoiceFactoryPIN {}
impl ChoiceFactoryPIN {
fn new(prompt: StrBuffer) -> Self {
Self { prompt }
fn new() -> Self {
Self {}
}
}
@ -75,23 +70,32 @@ impl ChoiceFactory for ChoiceFactoryPIN {
}
/// Component for entering a PIN.
pub struct PinEntry {
pub struct PinEntry<T> {
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,
textbox: TextBox<MAX_PIN_LENGTH>,
}
impl PinEntry {
pub fn new(prompt: StrBuffer) -> Self {
let choices = ChoiceFactoryPIN::new(prompt);
impl<T> PinEntry<T>
where
T: AsRef<str> + Clone,
{
pub fn new(prompt: T, subprompt: T) -> Self {
let choices = ChoiceFactoryPIN::new();
Self {
// Starting at the digit 0
choice_page: ChoicePage::new(choices)
.with_initial_page_counter(NUMBER_START_INDEX as u8)
.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,
textbox: TextBox::empty(),
}
@ -106,23 +110,39 @@ impl PinEntry {
self.textbox.delete_last(ctx);
}
fn update_pin_dots(&mut self, ctx: &mut EventCtx) {
// TODO: this is the same action as for the passphrase entry,
// might do a common component that will handle this part,
// (something like `SecretTextLine`)
// 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 {
let pin = String::from(self.pin());
self.pin_dots.inner_mut().update_text(pin);
/// Performs overall update of the screen.
fn update(&mut self, ctx: &mut EventCtx) {
self.update_header_info(ctx);
ctx.request_paint();
}
/// Update the header information - (sub)prompt and visible PIN.
/// If PIN is empty, showing prompt in `pin_line` and sub-prompt in the
/// `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 {
let mut dots: String<MAX_PIN_LENGTH> = String::new();
for _ in 0..self.textbox.len() {
unwrap!(dots.push_str("*"));
}
self.pin_dots.inner_mut().update_text(dots);
}
self.pin_dots.request_complete_repaint(ctx);
dots
};
// 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 {
@ -138,13 +158,19 @@ impl PinEntry {
}
}
impl Component for PinEntry {
impl<T> Component for PinEntry<T>
where
T: AsRef<str> + Clone,
{
type Msg = PinEntryMsg;
fn place(&mut self, bounds: Rect) -> Rect {
let pin_area_height = self.pin_dots.inner().needed_height();
let (pin_area, choice_area) = bounds.split_top(pin_area_height);
self.pin_dots.place(pin_area);
let pin_height = self.pin_line.inner().needed_height();
let subtitle_height = self.subprompt_line.inner().needed_height();
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);
bounds
}
@ -153,7 +179,7 @@ impl Component for PinEntry {
// Any event when showing real PIN should hide it
if self.show_real_pin {
self.show_real_pin = false;
self.update_pin_dots(ctx);
self.update(ctx)
}
let msg = self.choice_page.event(ctx, event);
@ -162,19 +188,16 @@ impl Component for PinEntry {
match page_counter as usize {
DELETE_INDEX => {
self.delete_last_digit(ctx);
self.update_pin_dots(ctx);
ctx.request_paint();
self.update(ctx);
}
SHOW_INDEX => {
self.show_real_pin = true;
self.update_pin_dots(ctx);
ctx.request_paint();
self.update(ctx);
}
ENTER_INDEX => return Some(PinEntryMsg::Confirmed),
_ => {
if !self.is_full() {
self.append_new_digit(ctx, page_counter);
self.update_pin_dots(ctx);
// Choosing any random digit to be shown next
let new_page_counter = random::uniform_between(
NUMBER_START_INDEX as u32,
@ -182,7 +205,7 @@ impl Component for PinEntry {
);
self.choice_page
.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) {
self.pin_dots.paint();
self.pin_line.paint();
self.subprompt_line.paint();
self.choice_page.paint();
}
}
@ -200,7 +224,10 @@ impl Component for PinEntry {
use super::{ButtonAction, ButtonPos};
#[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> {
match pos {
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> {
match msg {
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 {
let block = |_args: &[Obj], kwargs: &Map| {
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> =
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())
};
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 curpin:
msg_screen = "You have successfully changed your PIN."
msg_screen = "PIN changed."
msg_wire = "PIN changed"
else:
msg_screen = "You have successfully enabled PIN protection."
msg_screen = "PIN protection enabled."
msg_wire = "PIN enabled"
else:
msg_screen = "You have successfully disabled PIN protection."
msg_screen = "PIN protection disabled."
msg_wire = "PIN removed"
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()
title = "PIN settings"
br_code = "set_pin"
if msg.remove and has_pin: # removing pin
return confirm_pin_action(
ctx,
"set_pin",
"Remove PIN",
br_code,
title,
"disable PIN protection?",
"Do you really want to",
)
if not msg.remove and has_pin: # changing pin
return confirm_pin_action(
ctx,
"set_pin",
"Change PIN",
br_code,
title,
"change your PIN?",
"Do you really want to",
)
if not msg.remove and not has_pin: # setting new pin
return confirm_set_new_pin(
ctx,
"set_pin",
"Enable PIN",
"Do you really want to",
br_code,
title,
"enable PIN protection?",
[
"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 has_wipe_code:
msg_screen = "You have successfully changed the wipe code."
msg_screen = "Wipe code changed."
msg_wire = "Wipe code changed"
else:
msg_screen = "You have successfully set the wipe code."
msg_screen = "Wipe code enabled."
msg_wire = "Wipe code set"
else:
msg_screen = "You have successfully disabled the wipe code."
msg_screen = "Wipe code disabled."
msg_wire = "Wipe code removed"
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
) -> Awaitable[None]:
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:
return confirm_pin_action(
ctx,
"disable_wipe_code",
"Disable wipe code",
title,
"disable wipe code protection?",
"Do you really want to",
)
if not msg.remove and has_wipe_code:
return confirm_pin_action(
ctx,
"change_wipe_code",
"Change wipe code",
title,
"change the wipe code?",
"Do you really want to",
)
if not msg.remove and not has_wipe_code:
return confirm_pin_action(
return confirm_set_new_pin(
ctx,
"set_wipe_code",
"Set wipe code",
"set the wipe code?",
"Do you really want to",
title,
"enable wipe code?",
[
"Wipe code will\nbe used to delete this device.",
],
)
# Removing non-existing wipe code.
@ -110,7 +112,7 @@ async def _request_wipe_code_confirm(ctx: Context, pin: str) -> str:
)
continue
code2 = await request_pin(ctx, "Re-enter new wipe code")
code2 = await request_pin(ctx, "Re-enter wipe code")
if code1 == code2:
return code1
# _wipe_code_mismatch

View File

@ -1254,7 +1254,6 @@ async def request_pin_on_device(
prompt: str,
attempts_remaining: int | None,
allow_cancel: bool,
shuffle: bool = False,
) -> str:
if attempts_remaining is None:
subprompt = ""
@ -1263,18 +1262,6 @@ async def request_pin_on_device(
else:
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)
dialog = RustLayout(
@ -1282,7 +1269,6 @@ async def request_pin_on_device(
prompt=prompt,
subprompt=subprompt,
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)
if result is trezorui2.CANCELLED:
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)
return result
@ -1350,19 +1333,25 @@ async def confirm_set_new_pin(
br_type: str,
title: str,
action: str,
description: str,
information: list[str],
description: str = "Do you want to",
br_code: ButtonRequestType = ButtonRequestType.Other,
) -> None:
await confirm_action(
ctx,
br_type,
title,
action=f"{description} {action}",
description=f"{description} {action}",
verb="ENABLE",
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(
"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,
title: str,
action: str | None,
description: str | None,
description: str | None = "Do you really want to",
br_code: ButtonRequestType = ButtonRequestType.Other,
) -> None:
return await confirm_action(
@ -1184,8 +1184,8 @@ async def confirm_set_new_pin(
br_type: str,
title: str,
action: str,
description: str,
information: list[str],
description: str = "Do you want to",
br_code: ButtonRequestType = ButtonRequestType.Other,
) -> None:
await confirm_action(