1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-19 05:58:09 +00:00

fix(core/ui/mercury): FIDO2 layouts

This commit is contained in:
Martin Milata 2024-07-30 00:12:28 +02:00
parent 167f567ab0
commit 7c8be6f0ea
54 changed files with 613 additions and 479 deletions

View File

@ -0,0 +1 @@
[T3T1] Redesigned FIDO2 UI.

View File

@ -301,6 +301,7 @@ static void _librust_qstrs(void) {
MP_QSTR_instructions__learn_more;
MP_QSTR_instructions__shares_continue_with_x_template;
MP_QSTR_instructions__shares_start_with_1;
MP_QSTR_instructions__swipe_down;
MP_QSTR_instructions__swipe_horizontally;
MP_QSTR_instructions__swipe_up;
MP_QSTR_instructions__tap_to_confirm;
@ -1008,15 +1009,20 @@ static void _librust_qstrs(void) {
MP_QSTR_fido__does_not_belong;
MP_QSTR_fido__erase_credentials;
MP_QSTR_fido__export_credentials;
MP_QSTR_fido__more_credentials;
MP_QSTR_fido__not_registered;
MP_QSTR_fido__not_registered_with_template;
MP_QSTR_fido__please_enable_pin_protection;
MP_QSTR_fido__select_intro;
MP_QSTR_fido__title_authenticate;
MP_QSTR_fido__title_credential_details;
MP_QSTR_fido__title_for_authentication;
MP_QSTR_fido__title_import_credential;
MP_QSTR_fido__title_list_credentials;
MP_QSTR_fido__title_register;
MP_QSTR_fido__title_remove_credential;
MP_QSTR_fido__title_reset;
MP_QSTR_fido__title_select_credential;
MP_QSTR_fido__title_u2f_auth;
MP_QSTR_fido__title_u2f_register;
MP_QSTR_fido__title_verify_user;

View File

@ -1358,6 +1358,17 @@ pub enum TranslatedString {
reset__slip39_checklist_more_info_threshold = 957, // "The threshold sets the minumum number of shares needed to recover your wallet."
reset__slip39_checklist_more_info_threshold_example_template = 958, // "If you set {0} out of {1} shares, you'll need {2} backup shares to recover your wallet."
passphrase__continue_with_empty_passphrase = 959, // "Continue with empty passphrase?"
#[cfg(feature = "universal_fw")]
fido__more_credentials = 960, // "More credentials"
#[cfg(feature = "universal_fw")]
fido__select_intro = 961, // "Select the credential that you would like to use for authentication."
#[cfg(feature = "universal_fw")]
fido__title_for_authentication = 962, // "for authentication"
#[cfg(feature = "universal_fw")]
fido__title_select_credential = 963, // "Select credential"
instructions__swipe_down = 964, // "Swipe down"
#[cfg(feature = "universal_fw")]
fido__title_credential_details = 965, // "Credential details"
}
impl TranslatedString {
@ -2710,6 +2721,17 @@ impl TranslatedString {
Self::reset__slip39_checklist_more_info_threshold => "The threshold sets the minumum number of shares needed to recover your wallet.",
Self::reset__slip39_checklist_more_info_threshold_example_template => "If you set {0} out of {1} shares, you'll need {2} backup shares to recover your wallet.",
Self::passphrase__continue_with_empty_passphrase => "Continue with empty passphrase?",
#[cfg(feature = "universal_fw")]
Self::fido__more_credentials => "More credentials",
#[cfg(feature = "universal_fw")]
Self::fido__select_intro => "Select the credential that you would like to use for authentication.",
#[cfg(feature = "universal_fw")]
Self::fido__title_for_authentication => "for authentication",
#[cfg(feature = "universal_fw")]
Self::fido__title_select_credential => "Select credential",
Self::instructions__swipe_down => "Swipe down",
#[cfg(feature = "universal_fw")]
Self::fido__title_credential_details => "Credential details",
}
}
@ -4063,6 +4085,17 @@ impl TranslatedString {
Qstr::MP_QSTR_reset__slip39_checklist_more_info_threshold => Some(Self::reset__slip39_checklist_more_info_threshold),
Qstr::MP_QSTR_reset__slip39_checklist_more_info_threshold_example_template => Some(Self::reset__slip39_checklist_more_info_threshold_example_template),
Qstr::MP_QSTR_passphrase__continue_with_empty_passphrase => Some(Self::passphrase__continue_with_empty_passphrase),
#[cfg(feature = "universal_fw")]
Qstr::MP_QSTR_fido__more_credentials => Some(Self::fido__more_credentials),
#[cfg(feature = "universal_fw")]
Qstr::MP_QSTR_fido__select_intro => Some(Self::fido__select_intro),
#[cfg(feature = "universal_fw")]
Qstr::MP_QSTR_fido__title_for_authentication => Some(Self::fido__title_for_authentication),
#[cfg(feature = "universal_fw")]
Qstr::MP_QSTR_fido__title_select_credential => Some(Self::fido__title_select_credential),
Qstr::MP_QSTR_instructions__swipe_down => Some(Self::instructions__swipe_down),
#[cfg(feature = "universal_fw")]
Qstr::MP_QSTR_fido__title_credential_details => Some(Self::fido__title_credential_details),
_ => None,
}
}

View File

@ -90,6 +90,19 @@ where
&mut self.source
}
pub fn area(&self) -> Rect {
let mut result: Option<Rect> = None;
Self::foreach_visible(
&self.source,
&self.visible,
self.offset,
&mut |layout, _content| {
result = result.map_or(Some(layout.bounds), |r| Some(r.union(layout.bounds)));
},
);
result.unwrap_or(self.area)
}
/// Update bounding boxes of paragraphs on the current page. First determine
/// the number of visible paragraphs and their sizes. These are then
/// arranged according to the layout.

View File

@ -4,9 +4,7 @@ use crate::{
strutil::TString,
time::Duration,
ui::{
component::{
Component, ComponentExt, Event, EventCtx, FixedHeightBar, MsgMap, Split, TimerToken,
},
component::{Component, Event, EventCtx, TimerToken},
display::{self, toif::Icon, Color, Font},
event::TouchEvent,
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
@ -139,10 +137,9 @@ impl Button {
matches!(self.state, State::Disabled)
}
pub fn set_content(&mut self, ctx: &mut EventCtx, content: ButtonContent) {
pub fn set_content(&mut self, content: ButtonContent) {
if self.content != content {
self.content = content;
ctx.request_paint();
self.content = content
}
}
@ -454,41 +451,6 @@ pub struct ButtonStyle {
pub background_color: Color,
}
impl Button {
pub fn cancel_confirm(
left: Button,
right: Button,
left_is_small: bool,
) -> CancelConfirm<
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
impl Fn(ButtonMsg) -> Option<CancelConfirmMsg>,
> {
let width = if left_is_small {
theme::BUTTON_WIDTH
} else {
0
};
theme::button_bar(Split::left(
width,
theme::BUTTON_SPACING,
left.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Cancelled)
}),
right.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
}),
))
}
}
#[derive(Copy, Clone)]
pub enum CancelConfirmMsg {
Cancelled,
Confirmed,
}
type CancelConfirm<F0, F1> = FixedHeightBar<Split<MsgMap<Button, F0>, MsgMap<Button, F1>>>;
#[derive(Clone, Copy)]
pub enum CancelInfoConfirmMsg {
Cancelled,

View File

@ -1,251 +1,88 @@
use crate::{
strutil::TString,
ui::{
component::{image::Image, Component, Event, EventCtx, Label, Swipe, SwipeDirection},
display,
geometry::{Insets, Rect},
model_mercury::component::{fido_icons::get_fido_icon_data, theme, ScrollBar},
shape,
component::{
image::Image,
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs},
Component, Event, EventCtx,
},
geometry::{Insets, Offset, Rect},
model_mercury::component::{fido_icons::get_fido_icon_data, theme},
shape::Renderer,
},
};
use super::CancelConfirmMsg;
use core::cell::Cell;
const ICON_HEIGHT: i16 = 70;
const SCROLLBAR_INSET_TOP: i16 = 5;
const SCROLLBAR_HEIGHT: i16 = 10;
const APP_NAME_PADDING: i16 = 12;
const APP_NAME_HEIGHT: i16 = 30;
pub enum FidoMsg {
Confirmed(usize),
Cancelled,
}
pub struct FidoConfirm<F: Fn(usize) -> TString<'static>, U> {
page_swipe: Swipe,
app_name: Label<'static>,
account_name: Label<'static>,
icon: Image,
/// Function/closure that will return appropriate page on demand.
pub struct FidoCredential<F: Fn() -> TString<'static>> {
app_icon: Option<Image>,
text: Paragraphs<ParagraphVecShort<'static>>,
get_account: F,
scrollbar: ScrollBar,
fade: Cell<bool>,
controls: U,
}
impl<F, U> FidoConfirm<F, U>
where
F: Fn(usize) -> TString<'static>,
U: Component<Msg = CancelConfirmMsg>,
{
impl<F: Fn() -> TString<'static>> FidoCredential<F> {
const ICON_SIZE: i16 = 32;
const SPACING: i16 = 8;
pub fn new(
icon_name: Option<TString<'static>>,
app_name: TString<'static>,
get_account: F,
page_count: usize,
icon_name: Option<TString<'static>>,
controls: U,
) -> Self {
let icon_data = get_fido_icon_data(icon_name);
// Preparing scrollbar and setting its page-count.
let mut scrollbar = ScrollBar::horizontal();
scrollbar.set_count_and_active_page(page_count, 0);
// Preparing swipe component and setting possible initial
// swipe directions according to number of pages.
let mut page_swipe = Swipe::horizontal();
page_swipe.allow_right = scrollbar.has_previous_page();
page_swipe.allow_left = scrollbar.has_next_page();
// NOTE: This is an ugly hotfix for the erroneous behavior of
// TextLayout used in the account_name Label. In this
// particular case, TextLayout calculates the wrong height of
// fitted text that's higher than the TextLayout bound itself.
//
// The following two lines should be swapped when the problem with
// TextLayout is fixed.
//
// See also, continuation of this hotfix in the place() function.
// let current_account = get_account(scrollbar.active_page);
let current_account = "".into();
let app_icon = get_fido_icon_data(icon_name).map(Image::new);
let text = ParagraphVecShort::from_iter([
Paragraph::new(&theme::TEXT_SUB_GREY, app_name),
Paragraph::new(&theme::TEXT_MAIN_GREY_EXTRA_LIGHT, (get_account)()),
])
.into_paragraphs();
Self {
app_name: Label::centered(app_name, theme::TEXT_DEMIBOLD),
account_name: Label::centered(current_account, theme::TEXT_DEMIBOLD),
page_swipe,
icon: Image::new(icon_data),
app_icon,
text,
get_account,
scrollbar,
fade: Cell::new(false),
controls,
}
}
fn on_page_swipe(&mut self, ctx: &mut EventCtx, swipe: SwipeDirection) {
// Change the page number.
match swipe {
SwipeDirection::Left if self.scrollbar.has_next_page() => {
self.scrollbar.go_to_next_page();
}
SwipeDirection::Right if self.scrollbar.has_previous_page() => {
self.scrollbar.go_to_previous_page();
}
_ => {} // page did not change
};
// Disable swipes on the boundaries. Not allowing carousel effect.
self.page_swipe.allow_right = self.scrollbar.has_previous_page();
self.page_swipe.allow_left = self.scrollbar.has_next_page();
let current_account = (self.get_account)(self.active_page());
self.account_name.set_text(current_account);
// Redraw the page.
ctx.request_paint();
// Reset backlight to normal level on next paint.
self.fade.set(true);
}
fn active_page(&self) -> usize {
self.scrollbar.active_page
}
}
impl<F, U> Component for FidoConfirm<F, U>
where
F: Fn(usize) -> TString<'static>,
U: Component<Msg = CancelConfirmMsg>,
{
type Msg = FidoMsg;
impl<F: Fn() -> TString<'static>> Component for FidoCredential<F> {
type Msg = ();
fn place(&mut self, bounds: Rect) -> Rect {
self.page_swipe.place(bounds);
// Place the control buttons.
let controls_area = self.controls.place(bounds);
// Get the image and content areas.
let content_area = bounds.inset(Insets::bottom(controls_area.height()));
let (image_area, content_area) = content_area.split_top(ICON_HEIGHT);
// In case of showing a scrollbar, getting its area and placing it.
let remaining_area = if self.scrollbar.page_count > 1 {
let (scrollbar_area, remaining_area) = content_area
.inset(Insets::top(SCROLLBAR_INSET_TOP))
.split_top(SCROLLBAR_HEIGHT);
self.scrollbar.place(scrollbar_area);
remaining_area
} else {
content_area
};
// Place the icon image.
self.icon.place(image_area);
// Place the text labels.
let (app_name_area, account_name_area) = remaining_area
.inset(Insets::top(APP_NAME_PADDING))
.split_top(APP_NAME_HEIGHT);
self.app_name.place(app_name_area);
self.account_name.place(account_name_area);
// NOTE: This is a hotfix used due to the erroneous behavior of TextLayout.
// This line should be removed when the problem with TextLayout is fixed.
// See also the code for FidoConfirm::new().
self.account_name
.set_text((self.get_account)(self.scrollbar.active_page));
let icon_size = self.app_icon.map_or(Offset::zero(), |i| i.toif.size());
let (icon_area, text_area) = bounds.split_top(icon_size.y);
let text_area = text_area.inset(Insets::top(Self::SPACING));
self.text.place(text_area);
let text_height = self.text.area().height();
let vertical_space = bounds.height() - icon_size.y - Self::SPACING - text_height;
let off = Offset::y(vertical_space / 2);
let icon_area = icon_area.with_width(icon_size.x).translate(off);
let text_area = text_area.with_height(text_height).translate(off);
self.app_icon.place(icon_area);
self.text.place(text_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(swipe) = self.page_swipe.event(ctx, event) {
// Swipe encountered, update the page.
self.on_page_swipe(ctx, swipe);
}
if let Some(msg) = self.controls.event(ctx, event) {
// Some button was clicked, send results.
match msg {
CancelConfirmMsg::Confirmed => return Some(FidoMsg::Confirmed(self.active_page())),
CancelConfirmMsg::Cancelled => return Some(FidoMsg::Cancelled),
}
if let Event::Attach(_) = event {
self.text.inner_mut()[1].update((self.get_account)());
ctx.request_paint();
}
self.app_icon.event(ctx, event);
self.text.event(ctx, event);
None
}
fn paint(&mut self) {
self.icon.paint();
self.controls.paint();
self.app_name.paint();
if self.scrollbar.page_count > 1 {
self.scrollbar.paint();
}
// Erasing the old text content before writing the new one.
let account_name_area = self.account_name.area();
let real_area = account_name_area
.with_height(account_name_area.height() + self.account_name.font().text_baseline() + 1);
display::rect_fill(real_area, theme::BG);
// Account name is optional.
// Showing it only if it differs from app name.
// (Dummy requests usually have some text as both app_name and account_name.)
let account_name = self.account_name.text();
let app_name = self.app_name.text();
if !account_name.is_empty() && account_name != app_name {
self.account_name.paint();
}
if self.fade.take() {
// Note that this is blocking and takes some time.
display::fade_backlight(theme::backlight::get_backlight_normal());
}
unimplemented!()
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.icon.render(target);
self.controls.render(target);
self.app_name.render(target);
if self.scrollbar.page_count > 1 {
self.scrollbar.render(target);
}
// Erasing the old text content before writing the new one.
let account_name_area = self.account_name.area();
let real_area = account_name_area
.with_height(account_name_area.height() + self.account_name.font().text_baseline() + 1);
shape::Bar::new(real_area).with_bg(theme::BG).render(target);
// Account name is optional.
// Showing it only if it differs from app name.
// (Dummy requests usually have some text as both app_name and account_name.)
let account_name = self.account_name.text();
let app_name = self.app_name.text();
if !account_name.is_empty() && account_name != app_name {
self.account_name.render(target);
}
if self.fade.take() {
// Note that this is blocking and takes some time.
display::fade_backlight(theme::backlight::get_backlight_normal());
}
self.app_icon.render(target);
self.text.render(target);
}
}
#[cfg(feature = "ui_debug")]
impl<F, T> crate::trace::Trace for FidoConfirm<F, T>
where
F: Fn(usize) -> TString<'static>,
{
impl<F: Fn() -> TString<'static>> crate::trace::Trace for FidoCredential<F> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("FidoConfirm");
t.component("FidoCredential");
}
}

View File

@ -37,48 +37,44 @@ const ICON_PROTON: &[u8] = include_res!("model_mercury/res/fido/icon_proton.toif
const ICON_SLUSHPOOL: &[u8] = include_res!("model_mercury/res/fido/icon_slushpool.toif");
const ICON_STRIPE: &[u8] = include_res!("model_mercury/res/fido/icon_stripe.toif");
const ICON_TUTANOTA: &[u8] = include_res!("model_mercury/res/fido/icon_tutanota.toif");
/// Default icon when app does not have its own
const ICON_WEBAUTHN: &[u8] = include_res!("model_mercury/res/fido/icon_webauthn.toif");
/// Translates icon name into its data.
/// Returns default `ICON_WEBAUTHN` when the icon is not found or name not
/// supplied.
pub fn get_fido_icon_data(icon_name: Option<TString<'static>>) -> &'static [u8] {
pub fn get_fido_icon_data(icon_name: Option<TString<'static>>) -> Option< &'static [u8]> {
if let Some(icon_name) = icon_name {
icon_name.map(|c| match c {
"apple" => ICON_APPLE,
"aws" => ICON_AWS,
"binance" => ICON_BINANCE,
"bitbucket" => ICON_BITBUCKET,
"bitfinex" => ICON_BITFINEX,
"bitwarden" => ICON_BITWARDEN,
"cloudflare" => ICON_CLOUDFLARE,
"coinbase" => ICON_COINBASE,
"dashlane" => ICON_DASHLANE,
"dropbox" => ICON_DROPBOX,
"duo" => ICON_DUO,
"facebook" => ICON_FACEBOOK,
"fastmail" => ICON_FASTMAIL,
"fedora" => ICON_FEDORA,
"gandi" => ICON_GANDI,
"gemini" => ICON_GEMINI,
"github" => ICON_GITHUB,
"gitlab" => ICON_GITLAB,
"google" => ICON_GOOGLE,
"invity" => ICON_INVITY,
"keeper" => ICON_KEEPER,
"kraken" => ICON_KRAKEN,
"login.gov" => ICON_LOGIN_GOV,
"microsoft" => ICON_MICROSOFT,
"mojeid" => ICON_MOJEID,
"namecheap" => ICON_NAMECHEAP,
"proton" => ICON_PROTON,
"slushpool" => ICON_SLUSHPOOL,
"stripe" => ICON_STRIPE,
"tutanota" => ICON_TUTANOTA,
_ => ICON_WEBAUTHN,
"apple" => Some(ICON_APPLE),
"aws" => Some(ICON_AWS),
"binance" => Some(ICON_BINANCE),
"bitbucket" => Some(ICON_BITBUCKET),
"bitfinex" => Some(ICON_BITFINEX),
"bitwarden" => Some(ICON_BITWARDEN),
"cloudflare" => Some(ICON_CLOUDFLARE),
"coinbase" => Some(ICON_COINBASE),
"dashlane" => Some(ICON_DASHLANE),
"dropbox" => Some(ICON_DROPBOX),
"duo" => Some(ICON_DUO),
"facebook" => Some(ICON_FACEBOOK),
"fastmail" => Some(ICON_FASTMAIL),
"fedora" => Some(ICON_FEDORA),
"gandi" => Some(ICON_GANDI),
"gemini" => Some(ICON_GEMINI),
"github" => Some(ICON_GITHUB),
"gitlab" => Some(ICON_GITLAB),
"google" => Some(ICON_GOOGLE),
"invity" => Some(ICON_INVITY),
"keeper" => Some(ICON_KEEPER),
"kraken" => Some(ICON_KRAKEN),
"login.gov" => Some(ICON_LOGIN_GOV),
"microsoft" => Some(ICON_MICROSOFT),
"mojeid" => Some(ICON_MOJEID),
"namecheap" => Some(ICON_NAMECHEAP),
"proton" => Some(ICON_PROTON),
"slushpool" => Some(ICON_SLUSHPOOL),
"stripe" => Some(ICON_STRIPE),
"tutanota" => Some(ICON_TUTANOTA),
_ => None,
})
} else {
ICON_WEBAUTHN
None
}
}

View File

@ -19,21 +19,17 @@ for app in fido:
% for icon_name, var_name in icons:
const ICON_${var_name}: &[u8] = include_res!("model_mercury/res/fido/icon_${icon_name}.toif");
% endfor
/// Default icon when app does not have its own
const ICON_WEBAUTHN: &[u8] = include_res!("model_mercury/res/fido/icon_webauthn.toif");
/// Translates icon name into its data.
/// Returns default `ICON_WEBAUTHN` when the icon is not found or name not
/// supplied.
pub fn get_fido_icon_data(icon_name: Option<TString<'static>>) -> &'static [u8] {
pub fn get_fido_icon_data(icon_name: Option<TString<'static>>) -> Option< &'static [u8]> {
if let Some(icon_name) = icon_name {
icon_name.map(|c| match c {
% for icon_name, var_name in icons:
"${icon_name}" => ICON_${var_name},
"${icon_name}" => Some(ICON_${var_name}),
% endfor
_ => ICON_WEBAUTHN,
_ => None,
})
} else {
ICON_WEBAUTHN
None
}
}

View File

@ -198,14 +198,14 @@ impl PassphraseKeyboard {
}
fn replace_keys_contents(&mut self, ctx: &mut EventCtx) {
self.next_btn
.set_content(ctx, self.active_layout.next().into());
self.next_btn.set_content(self.active_layout.next().into());
for (i, btn) in self.keys.iter_mut().enumerate() {
let text = KEYBOARD[self.active_layout.to_usize().unwrap()][i];
let content = Self::key_content(text);
btn.set_content(ctx, content);
btn.set_content(content);
btn.request_complete_repaint(ctx);
}
ctx.request_paint();
}
/// Possibly changing the buttons' state after change of the input.

View File

@ -288,12 +288,13 @@ impl Slip39Input {
// Confirm button.
self.button.enable(ctx);
self.button
.set_content(ctx, ButtonContent::Icon(theme::ICON_SIMPLE_CHECKMARK24));
.set_content(ButtonContent::Icon(theme::ICON_SIMPLE_CHECKMARK24));
} else {
// Disabled button.
self.button.disable(ctx);
self.button.set_content(ctx, ButtonContent::Text("".into()));
self.button.set_content(ButtonContent::Text("".into()));
}
ctx.request_paint();
}
fn input_sequence(&self) -> Option<u16> {

View File

@ -46,13 +46,12 @@ pub use address_details::AddressDetails;
#[cfg(feature = "ui_overlay")]
pub use binary_selection::{BinarySelection, BinarySelectionMsg};
pub use button::{
Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, CancelConfirmMsg,
CancelInfoConfirmMsg, IconText,
Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, CancelInfoConfirmMsg, IconText,
};
#[cfg(feature = "translations")]
pub use coinjoin_progress::CoinJoinProgress;
pub use error::ErrorScreen;
pub use fido::{FidoConfirm, FidoMsg};
pub use fido::FidoCredential;
pub use footer::Footer;
pub use frame::{Frame, FrameMsg};
pub use header::Header;
@ -88,7 +87,7 @@ pub use swipe_up_screen::{SwipeUpScreen, SwipeUpScreenMsg};
#[cfg(feature = "translations")]
pub use tap_to_confirm::TapToConfirm;
pub use updatable_more_info::UpdatableMoreInfo;
pub use vertical_menu::{VerticalMenu, VerticalMenuChoiceMsg};
pub use vertical_menu::{PagedVerticalMenu, VerticalMenu, VerticalMenuChoiceMsg};
pub use welcome_screen::WelcomeScreen;
use super::{constant, theme};

View File

@ -7,13 +7,13 @@ use crate::{
ui::{
component::{
base::{AttachType, Component},
Event, EventCtx, SwipeDirection,
Event, EventCtx, Paginate, SwipeDirection,
},
constant::screen,
display::{Color, Icon},
geometry::{Offset, Rect},
lerp::Lerp,
model_mercury::component::button::{Button, ButtonMsg, IconText},
model_mercury::component::button::{Button, ButtonContent, ButtonMsg, IconText},
shape::{Bar, Renderer},
util::animation_disabled,
},
@ -25,11 +25,7 @@ pub enum VerticalMenuChoiceMsg {
/// Number of buttons.
/// Presently, VerticalMenu holds only fixed number of buttons.
/// TODO: for scrollable menu, the implementation must change.
const N_ITEMS: usize = 3;
/// Number of visual separators between buttons.
const N_SEPS: usize = N_ITEMS - 1;
const MAX_ITEMS: usize = 3;
/// Fixed height of each menu button.
const MENU_BUTTON_HEIGHT: i16 = 64;
@ -37,8 +33,7 @@ const MENU_BUTTON_HEIGHT: i16 = 64;
/// Fixed height of a separator.
const MENU_SEP_HEIGHT: i16 = 2;
type VerticalMenuButtons = Vec<Button, N_ITEMS>;
type AreasForSeparators = Vec<Rect, N_SEPS>;
type VerticalMenuButtons = Vec<Button, MAX_ITEMS>;
#[derive(Default, Clone)]
struct AttachAnimation {
@ -174,11 +169,10 @@ impl AttachAnimation {
#[derive(Clone)]
pub struct VerticalMenu {
area: Rect,
/// buttons placed vertically from top to bottom
buttons: VerticalMenuButtons,
/// areas for visual separators between buttons
areas_sep: AreasForSeparators,
/// length of `buttons` prefix that is currently active, set by `place()`
n_items: usize,
attach_animation: AttachAnimation,
}
@ -186,9 +180,8 @@ pub struct VerticalMenu {
impl VerticalMenu {
fn new(buttons: VerticalMenuButtons) -> Self {
Self {
area: Rect::zero(),
buttons,
areas_sep: AreasForSeparators::new(),
n_items: MAX_ITEMS,
attach_animation: AttachAnimation::default(),
}
}
@ -226,26 +219,23 @@ impl Component for VerticalMenu {
fn place(&mut self, bounds: Rect) -> Rect {
// VerticalMenu is supposed to be used in Frame, the remaining space is just
// enought to fit 3 buttons separated by thin bars
let height_bounds_expected = 3 * MENU_BUTTON_HEIGHT + 2 * MENU_SEP_HEIGHT;
assert!(bounds.height() == height_bounds_expected);
// enought to fit 3 buttons separated by thin bars. If there's footer only 2
// buttons fit.
let n_items = (bounds.height() + MENU_SEP_HEIGHT) / (MENU_BUTTON_HEIGHT + MENU_SEP_HEIGHT);
self.n_items = n_items as usize;
self.area = bounds;
self.areas_sep.clear();
let mut remaining = bounds;
let n_seps = self.buttons.len() - 1;
for (i, button) in self.buttons.iter_mut().enumerate() {
for (i, button) in self.buttons.iter_mut().take(self.n_items).enumerate() {
let (area_button, new_remaining) = remaining.split_top(MENU_BUTTON_HEIGHT);
button.place(area_button);
remaining = new_remaining;
if i < n_seps {
let (area_sep, new_remaining) = remaining.split_top(MENU_SEP_HEIGHT);
unwrap!(self.areas_sep.push(area_sep));
let (_area_sep, new_remaining) = remaining.split_top(MENU_SEP_HEIGHT);
remaining = new_remaining;
}
}
self.area
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
@ -278,7 +268,7 @@ impl Component for VerticalMenu {
target.with_origin(offset, &|target| {
// render buttons separated by thin bars
for (i, button) in (&self.buttons).into_iter().enumerate() {
for (i, button) in (&self.buttons).into_iter().take(self.n_items).enumerate() {
button.render(target);
Bar::new(button.area())
@ -286,18 +276,22 @@ impl Component for VerticalMenu {
.with_bg(Color::black())
.with_alpha(opacities[i])
.render(target);
}
for (i, area) in self.areas_sep.iter().enumerate() {
Bar::new(*area)
.with_thickness(MENU_SEP_HEIGHT)
.with_fg(theme::GREY_EXTRA_DARK)
.render(target);
Bar::new(*area)
.with_fg(Color::black())
.with_bg(Color::black())
.with_alpha(opacities[i])
.render(target);
if i + 1 < self.buttons.len().min(self.n_items) {
let area = button
.area()
.translate(Offset::y(MENU_BUTTON_HEIGHT))
.with_height(MENU_SEP_HEIGHT);
Bar::new(area)
.with_thickness(MENU_SEP_HEIGHT)
.with_fg(theme::GREY_EXTRA_DARK)
.render(target);
Bar::new(area)
.with_fg(Color::black())
.with_bg(Color::black())
.with_alpha(opacities[i])
.render(target);
}
}
// todo screen here is incorrect
@ -322,3 +316,90 @@ impl crate::trace::Trace for VerticalMenu {
});
}
}
// Polymorphic struct, avoid adding code as it gets duplicated, prefer
// extending VerticalMenu instead.
pub struct PagedVerticalMenu<F: Fn(usize) -> TString<'static>> {
inner: VerticalMenu,
page: usize,
item_count: usize,
label_fn: F,
}
impl<F: Fn(usize) -> TString<'static>> PagedVerticalMenu<F> {
pub fn new(item_count: usize, label_fn: F) -> Self {
let mut result = Self {
inner: VerticalMenu::select_word(["".into(), "".into(), "".into()]),
page: 0,
item_count,
label_fn,
};
result.change_page(0);
result
}
}
impl<F: Fn(usize) -> TString<'static>> Paginate for PagedVerticalMenu<F> {
fn page_count(&mut self) -> usize {
self.num_pages()
}
fn change_page(&mut self, active_page: usize) {
for b in 0..self.inner.n_items {
let i = active_page * self.inner.n_items + b;
let text = if i < self.item_count {
(self.label_fn)(i)
} else {
"".into()
};
let mut dummy_ctx = EventCtx::new();
self.inner.buttons[b].enable_if(&mut dummy_ctx, !text.is_empty());
self.inner.buttons[b].set_content(ButtonContent::Text(text));
}
self.page = active_page
}
}
impl<F: Fn(usize) -> TString<'static>> Component for PagedVerticalMenu<F> {
type Msg = VerticalMenuChoiceMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.inner.place(bounds)
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let msg = self.inner.event(ctx, event);
if let Some(VerticalMenuChoiceMsg::Selected(i)) = msg {
return Some(VerticalMenuChoiceMsg::Selected(
self.inner.n_items * self.page + i,
));
}
msg
}
fn paint(&mut self) {
// TODO remove when ui-t3t1 done
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.inner.render(target)
}
}
impl<F: Fn(usize) -> TString<'static>> InternallySwipable for PagedVerticalMenu<F> {
fn current_page(&self) -> usize {
self.page
}
fn num_pages(&self) -> usize {
(self.item_count / self.inner.n_items) + (self.item_count % self.inner.n_items).min(1)
}
}
#[cfg(feature = "ui_debug")]
impl<F: Fn(usize) -> TString<'static>> crate::trace::Trace for PagedVerticalMenu<F> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
self.inner.trace(t)
}
}

View File

@ -0,0 +1,202 @@
use crate::{
error,
micropython::{gc::Gc, list::List, map::Map, obj::Obj, qstr::Qstr, util},
strutil::TString,
translations::TR,
ui::{
component::{
swipe_detect::SwipeSettings,
text::paragraphs::{Paragraph, Paragraphs},
ComponentExt, SwipeDirection,
},
flow::{
base::{DecisionBuilder as _, StateChange},
FlowMsg, FlowState, SwipeFlow, SwipePage,
},
layout::obj::LayoutObj,
model_mercury::component::{FidoCredential, SwipeContent},
},
};
use super::super::{
component::{
Frame, FrameMsg, PagedVerticalMenu, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg,
},
theme,
};
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ConfirmFido {
Intro,
ChooseCredential,
Details,
Tap,
Menu,
}
static CRED_SELECTED: AtomicUsize = AtomicUsize::new(0);
static SINGLE_CRED: AtomicBool = AtomicBool::new(false);
impl FlowState for ConfirmFido {
#[inline]
fn index(&'static self) -> usize {
*self as usize
}
fn handle_swipe(&'static self, direction: SwipeDirection) -> StateChange {
match (self, direction) {
(Self::Intro, SwipeDirection::Left) => Self::Menu.swipe(direction),
(Self::Intro, SwipeDirection::Up) => Self::ChooseCredential.swipe(direction),
(Self::Details, SwipeDirection::Up) => Self::Tap.swipe(direction),
(Self::Tap, SwipeDirection::Down) => Self::Details.swipe(direction),
_ => self.do_nothing(),
}
}
fn handle_event(&'static self, msg: FlowMsg) -> StateChange {
match (self, msg) {
(_, FlowMsg::Info) => Self::Menu.transit(),
(Self::Menu, FlowMsg::Choice(0)) => self.return_msg(FlowMsg::Cancelled),
(Self::Menu, FlowMsg::Cancelled) => {
if Self::single_cred() {
Self::Details.swipe_right()
} else {
Self::Intro.swipe_right()
}
}
(Self::ChooseCredential, FlowMsg::Choice(i)) => {
CRED_SELECTED.store(i, Ordering::Relaxed);
Self::Details.swipe_left()
}
(Self::Details, FlowMsg::Cancelled) => Self::ChooseCredential.swipe_right(),
(Self::Tap, FlowMsg::Confirmed) => {
self.return_msg(FlowMsg::Choice(CRED_SELECTED.load(Ordering::Relaxed)))
}
_ => self.do_nothing(),
}
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn new_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, ConfirmFido::new_obj) }
}
impl ConfirmFido {
const EXTRA_PADDING: i16 = 6;
fn single_cred() -> bool {
SINGLE_CRED.load(Ordering::Relaxed)
}
fn new_obj(_args: &[Obj], kwargs: &Map) -> Result<Obj, error::Error> {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let app_name: TString = kwargs.get(Qstr::MP_QSTR_app_name)?.try_into()?;
let icon_name: Option<TString> = kwargs.get(Qstr::MP_QSTR_icon_name)?.try_into_option()?;
let accounts: Gc<List> = kwargs.get(Qstr::MP_QSTR_accounts)?.try_into()?;
let num_accounts = accounts.len();
SINGLE_CRED.store(num_accounts <= 1, Ordering::Relaxed);
CRED_SELECTED.store(0, Ordering::Relaxed);
let content_intro = Frame::left_aligned(
title,
SwipeContent::new(Paragraphs::new(Paragraph::new::<TString>(
&theme::TEXT_MAIN_GREY_LIGHT,
TR::fido__select_intro.into(),
))),
)
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None)
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info));
// Closure to lazy-load the information on given page index.
// Done like this to allow arbitrarily many pages without
// the need of any allocation here in Rust.
let label_fn = move |page_index| {
let account = unwrap!(accounts.get(page_index));
account
.try_into()
.unwrap_or_else(|_| TString::from_str("-"))
};
let content_choose_credential = Frame::left_aligned(
TR::fido__title_select_credential.into(),
SwipeContent::new(SwipePage::vertical(PagedVerticalMenu::new(
num_accounts,
label_fn,
))),
)
.with_subtitle(TR::fido__title_for_authentication.into())
.with_menu_button()
.with_footer(
TR::instructions__swipe_up.into(),
(num_accounts > 2).then_some(TR::fido__more_credentials.into()),
)
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.with_vertical_pages()
.map(|msg| match msg {
FrameMsg::Button(_) => Some(FlowMsg::Info),
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
});
let get_account = move || {
let current = CRED_SELECTED.load(Ordering::Relaxed);
let account = unwrap!(accounts.get(current));
account.try_into().unwrap_or_else(|_| TString::from_str(""))
};
let content_details = Frame::left_aligned(
TR::fido__title_credential_details.into(),
SwipeContent::new(FidoCredential::new(icon_name, app_name, get_account)),
)
.with_footer(TR::instructions__swipe_up.into(), Some(title))
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate());
let content_details = if Self::single_cred() {
content_details.with_menu_button()
} else {
content_details.with_cancel_button()
}
.map(|msg| match msg {
FrameMsg::Button(bm) => Some(bm),
_ => None,
});
let content_tap = Frame::left_aligned(title, PromptScreen::new_tap_to_confirm())
.with_menu_button()
.with_footer(TR::instructions__tap_to_confirm.into(), None)
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(()) => Some(FlowMsg::Confirmed),
FrameMsg::Button(_) => Some(FlowMsg::Info),
});
let content_menu = Frame::left_aligned(
"".into(),
VerticalMenu::empty().danger(theme::ICON_CANCEL, TR::buttons__cancel.into()),
)
.with_cancel_button()
.with_swipe(SwipeDirection::Right, SwipeSettings::immediate())
.map(|msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});
let initial_page = if Self::single_cred() {
&ConfirmFido::Details
} else {
&ConfirmFido::Intro
};
let res = SwipeFlow::new(initial_page)?
.with_page(&ConfirmFido::Intro, content_intro)?
.with_page(&ConfirmFido::ChooseCredential, content_choose_credential)?
.with_page(&ConfirmFido::Details, content_details)?
.with_page(&ConfirmFido::Tap, content_tap)?
.with_page(&ConfirmFido::Menu, content_menu)?;
Ok(LayoutObj::new(res)?.into())
}
}

View File

@ -1,4 +1,6 @@
pub mod confirm_action;
#[cfg(feature = "universal_fw")]
pub mod confirm_fido;
pub mod confirm_firmware_update;
pub mod confirm_output;
pub mod confirm_reset;
@ -14,9 +16,11 @@ pub mod show_share_words;
pub mod show_tutorial;
pub mod warning_hi_prio;
pub use confirm_action::{new_confirm_action, new_confirm_action_simple};
mod util;
pub use confirm_action::{new_confirm_action, new_confirm_action_simple};
#[cfg(feature = "universal_fw")]
pub use confirm_fido::new_confirm_fido;
pub use confirm_firmware_update::new_confirm_firmware_update;
pub use confirm_output::new_confirm_output;
pub use confirm_reset::new_confirm_reset;

View File

@ -14,9 +14,7 @@ use crate::{
error::{value_error, Error},
io::BinaryData,
micropython::{
gc::Gc,
iter::IterBuf,
list::List,
macros::{obj_fn_1, obj_fn_kw, obj_module},
map::Map,
module::Module,
@ -58,17 +56,6 @@ use crate::{
},
};
impl TryFrom<CancelConfirmMsg> for Obj {
type Error = Error;
fn try_from(value: CancelConfirmMsg) -> Result<Self, Self::Error> {
match value {
CancelConfirmMsg::Cancelled => Ok(CANCELLED.as_obj()),
CancelConfirmMsg::Confirmed => Ok(CONFIRMED.as_obj()),
}
}
}
impl TryFrom<CancelInfoConfirmMsg> for Obj {
type Error = Error;
@ -101,19 +88,6 @@ impl TryFrom<VerticalMenuChoiceMsg> for Obj {
}
}
impl<F, U> ComponentMsgObj for FidoConfirm<F, U>
where
F: Fn(usize) -> TString<'static>,
U: Component<Msg = CancelConfirmMsg>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
FidoMsg::Confirmed(page) => Ok((page as u8).into()),
FidoMsg::Cancelled => Ok(CANCELLED.as_obj()),
}
}
}
impl ComponentMsgObj for PinKeyboard<'_> {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
@ -716,37 +690,6 @@ extern "C" fn new_show_error(n_args: usize, args: *const Obj, kwargs: *mut Map)
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let app_name: TString = kwargs.get(Qstr::MP_QSTR_app_name)?.try_into()?;
let icon: Option<TString> = kwargs.get(Qstr::MP_QSTR_icon_name)?.try_into_option()?;
let accounts: Gc<List> = kwargs.get(Qstr::MP_QSTR_accounts)?.try_into()?;
// Cache the page count so that we can move `accounts` into the closure.
let page_count = accounts.len();
// Closure to lazy-load the information on given page index.
// Done like this to allow arbitrarily many pages without
// the need of any allocation here in Rust.
let get_page = move |page_index| {
let account = unwrap!(accounts.get(page_index));
account.try_into().unwrap_or_else(|_| "".into())
};
let controls = Button::cancel_confirm(
Button::with_icon(theme::ICON_CANCEL),
Button::with_text(TR::buttons__confirm.into()).styled(theme::button_confirm()),
true,
);
let fido_page = FidoConfirm::new(app_name, get_page, page_count, icon, controls);
let obj = LayoutObj::new(Frame::centered(title, fido_page))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_warning(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
@ -1213,6 +1156,13 @@ extern "C" fn new_show_wait_text(message: Obj) -> Obj {
unsafe { util::try_or_raise(block) }
}
extern "C" fn new_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
#[cfg(feature = "universal_fw")]
return flow::confirm_fido::new_confirm_fido(n_args, args, kwargs);
#[cfg(not(feature = "universal_fw"))]
panic!();
}
#[no_mangle]
pub static mp_module_trezorui2: Module = obj_module! {
/// from trezor import utils

View File

@ -335,15 +335,20 @@ class TR:
fido__does_not_belong: str = "The credential you are trying to import does\nnot belong to this authenticator."
fido__erase_credentials: str = "erase all credentials?"
fido__export_credentials: str = "Export information about the credentials stored on this device?"
fido__more_credentials: str = "More credentials"
fido__not_registered: str = "Not registered"
fido__not_registered_with_template: str = "This device is not registered with\n{0}."
fido__please_enable_pin_protection: str = "Please enable PIN protection."
fido__select_intro: str = "Select the credential that you would like to use for authentication."
fido__title_authenticate: str = "FIDO2 authenticate"
fido__title_credential_details: str = "Credential details"
fido__title_for_authentication: str = "for authentication"
fido__title_import_credential: str = "Import credential"
fido__title_list_credentials: str = "List credentials"
fido__title_register: str = "FIDO2 register"
fido__title_remove_credential: str = "Remove credential"
fido__title_reset: str = "FIDO2 reset"
fido__title_select_credential: str = "Select credential"
fido__title_u2f_auth: str = "U2F authenticate"
fido__title_u2f_register: str = "U2F register"
fido__title_verify_user: str = "FIDO2 verify user"
@ -386,6 +391,7 @@ class TR:
instructions__learn_more: str = "Learn more"
instructions__shares_continue_with_x_template: str = "Continue with Share #{0}"
instructions__shares_start_with_1: str = "Start with share #1"
instructions__swipe_down: str = "Swipe down"
instructions__swipe_horizontally: str = "Swipe horizontally"
instructions__swipe_up: str = "Swipe up"
instructions__tap_to_confirm: str = "Tap to confirm"

View File

@ -9,18 +9,19 @@ from . import RustLayout
if TYPE_CHECKING:
from trezor.loop import AwaitableTask
if __debug__:
from trezor import io, ui
from ... import Result
# needed solely for test_emu_u2f
class _RustFidoLayoutImpl(RustLayout):
def create_tasks(self) -> tuple[AwaitableTask, ...]:
return (
self.handle_input_and_rendering(),
self.handle_timers(),
self.handle_swipe(),
self.handle_click_signal(),
self.handle_debug_confirm(),
)
@ -32,8 +33,11 @@ if __debug__:
raise Result(result)
for event, x, y in (
(io.TOUCH_START, 220, 220),
(io.TOUCH_END, 220, 220),
(io.TOUCH_START, 120, 160),
(io.TOUCH_MOVE, 120, 130),
(io.TOUCH_END, 120, 100),
(io.TOUCH_START, 120, 120),
(io.TOUCH_END, 120, 120),
):
msg = self.layout.touch_event(event, x, y)
if self.layout.paint():
@ -66,8 +70,8 @@ async def confirm_fido(
# The Rust side returns either an int or `CANCELLED`. We detect the int situation
# and assume cancellation otherwise.
if isinstance(result, int):
return result
if isinstance(result, tuple):
return result[1]
# Late import won't get executed on the happy path.
from trezor.wire import ActionCancelled

View File

@ -11,10 +11,10 @@ from trezorlib import toif
HERE = Path(__file__).resolve().parent
ROOT = HERE.parent.parent
ICON_SIZE = (64, 64)
DESTINATION = (
ROOT / "core" / "embed" / "rust" / "src" / "ui" / "model_tt" / "res" / "fido"
)
DESTINATIONS = {
ROOT / "core" / "embed" / "rust" / "src" / "ui" / "model_tt" / "res" / "fido": 64,
ROOT / "core" / "embed" / "rust" / "src" / "ui" / "model_mercury" / "res" / "fido": 32,
}
EXCLUDE = {"icon_webauthn"}
# insert ../../common/tools to sys.path, so that we can import coin_info
@ -31,9 +31,15 @@ import coin_info
@click.command()
@click.option("-c", "--check", is_flag=True, help="Do not write, only check.")
@click.option("-r", "--remove", is_flag=True, help="Remove unrecognized files.")
def build_icons(check, remove):
def build_icons(check: bool, remove: bool):
"""Build FIDO app icons in the source tree."""
for path, size in DESTINATIONS.items():
build_icons_size(path, size, check, remove)
def build_icons_size(destination: Path, size: int, check: bool, remove: bool):
icon_size = (size, size)
checks_ok = True
apps = coin_info.fido_info()
@ -47,9 +53,9 @@ def build_icons(check, remove):
continue
im = Image.open(app["icon"])
resized = im.resize(ICON_SIZE, Image.BOX)
resized = im.resize(icon_size, Image.BOX)
toi = toif.from_image(resized)
dest_path = DESTINATION / f"icon_{app['key']}.toif"
dest_path = destination / f"icon_{app['key']}.toif"
total_size += len(toi.to_bytes())
@ -65,11 +71,11 @@ def build_icons(check, remove):
print(f"Icon different from source: {dest_path}")
checks_ok = False
print(f"Total icon size: {total_size} bytes")
print(f"{destination.parts[-3]} icon size: {total_size} bytes")
keys = EXCLUDE | {"icon_" + app["key"] for app in apps}
unrecognized_files = False
for icon_file in DESTINATION.glob("*.toif"):
for icon_file in destination.glob("*.toif"):
name = icon_file.stem
if name not in keys:
unrecognized_files = True

View File

@ -337,15 +337,20 @@
"fido__does_not_belong": "The credential you are trying to import does\nnot belong to this authenticator.",
"fido__erase_credentials": "erase all credentials?",
"fido__export_credentials": "Export information about the credentials stored on this device?",
"fido__more_credentials": "More credentials",
"fido__not_registered": "Not registered",
"fido__not_registered_with_template": "This device is not registered with\n{0}.",
"fido__please_enable_pin_protection": "Please enable PIN protection.",
"fido__select_intro": "Select the credential that you would like to use for authentication.",
"fido__title_authenticate": "FIDO2 authenticate",
"fido__title_credential_details": "Credential details",
"fido__title_for_authentication": "for authentication",
"fido__title_import_credential": "Import credential",
"fido__title_list_credentials": "List credentials",
"fido__title_register": "FIDO2 register",
"fido__title_remove_credential": "Remove credential",
"fido__title_reset": "FIDO2 reset",
"fido__title_select_credential": "Select credential",
"fido__title_u2f_auth": "U2F authenticate",
"fido__title_u2f_register": "U2F register",
"fido__title_verify_user": "FIDO2 verify user",
@ -388,6 +393,7 @@
"instructions__learn_more": "Learn more",
"instructions__shares_continue_with_x_template": "Continue with Share #{0}",
"instructions__shares_start_with_1": "Start with share #1",
"instructions__swipe_down": "Swipe down",
"instructions__swipe_horizontally": "Swipe horizontally",
"instructions__swipe_up": "Swipe up",
"instructions__tap_to_confirm": "Tap to confirm",

View File

@ -958,5 +958,11 @@
"956": "words__title_done",
"957": "reset__slip39_checklist_more_info_threshold",
"958": "reset__slip39_checklist_more_info_threshold_example_template",
"959": "passphrase__continue_with_empty_passphrase"
"959": "passphrase__continue_with_empty_passphrase",
"960": "fido__more_credentials",
"961": "fido__select_intro",
"962": "fido__title_for_authentication",
"963": "fido__title_select_credential",
"964": "instructions__swipe_down",
"965": "fido__title_credential_details"
}

View File

@ -1,8 +1,8 @@
{
"current": {
"merkle_root": "6803ea2d1eb1555ae32a5b32689cdf40d7626a8b3717f6ae9418e0948f9da746",
"datetime": "2024-08-27T15:18:26.530958",
"commit": "6ebe850fe24f5ec894328d82eec97aacccb3d3f5"
"merkle_root": "0682f8041f5d002800da51d3c3a36351d326b89ddf8fff6c3e70cd1943f3e064",
"datetime": "2024-08-29T14:44:39.968325",
"commit": "c5e520fd1e34182fb19044baa190dbc81fcf5cad"
},
"history": [
{

View File

@ -21,6 +21,7 @@ from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.exceptions import Cancelled, TrezorFailure
from ...common import MNEMONIC12
from ...input_flows import InputFlowFidoConfirm
from .data_webauthn import CRED1, CRED2, CRED3, CREDS
RK_CAPACITY = 100
@ -30,73 +31,77 @@ RK_CAPACITY = 100
@pytest.mark.altcoin
@pytest.mark.setup_client(mnemonic=MNEMONIC12)
def test_add_remove(client: Client):
# Remove index 0 should fail.
with pytest.raises(TrezorFailure):
fido.remove_credential(client, 0)
with client:
IF = InputFlowFidoConfirm(client)
client.set_input_flow(IF.get())
# List should be empty.
assert fido.list_credentials(client) == []
# Remove index 0 should fail.
with pytest.raises(TrezorFailure):
fido.remove_credential(client, 0)
# Add valid credential #1.
fido.add_credential(client, CRED1)
# List should be empty.
assert fido.list_credentials(client) == []
# Check that the credential was added and parameters are correct.
creds = fido.list_credentials(client)
assert len(creds) == 1
assert creds[0].rp_id == "example.com"
assert creds[0].rp_name == "Example"
assert creds[0].user_id == bytes.fromhex(
"3082019330820138A0030201023082019330820138A003020102308201933082"
)
assert creds[0].user_name == "johnpsmith@example.com"
assert creds[0].user_display_name == "John P. Smith"
assert creds[0].creation_time == 3
assert creds[0].hmac_secret is True
# Add valid credential #1.
fido.add_credential(client, CRED1)
# Add valid credential #2, which has same rpId and userId as credential #1.
fido.add_credential(client, CRED2)
# Check that the credential was added and parameters are correct.
creds = fido.list_credentials(client)
assert len(creds) == 1
assert creds[0].rp_id == "example.com"
assert creds[0].rp_name == "Example"
assert creds[0].user_id == bytes.fromhex(
"3082019330820138A0030201023082019330820138A003020102308201933082"
)
assert creds[0].user_name == "johnpsmith@example.com"
assert creds[0].user_display_name == "John P. Smith"
assert creds[0].creation_time == 3
assert creds[0].hmac_secret is True
# Check that the credential #2 replaced credential #1 and parameters are correct.
creds = fido.list_credentials(client)
assert len(creds) == 1
assert creds[0].rp_id == "example.com"
assert creds[0].rp_name is None
assert creds[0].user_id == bytes.fromhex(
"3082019330820138A0030201023082019330820138A003020102308201933082"
)
assert creds[0].user_name == "johnpsmith@example.com"
assert creds[0].user_display_name is None
assert creds[0].creation_time == 2
assert creds[0].hmac_secret is True
# Add valid credential #2, which has same rpId and userId as credential #1.
fido.add_credential(client, CRED2)
# Adding an invalid credential should appear as if user cancelled.
with pytest.raises(Cancelled):
fido.add_credential(client, CRED1[:-2])
# Check that the credential #2 replaced credential #1 and parameters are correct.
creds = fido.list_credentials(client)
assert len(creds) == 1
assert creds[0].rp_id == "example.com"
assert creds[0].rp_name is None
assert creds[0].user_id == bytes.fromhex(
"3082019330820138A0030201023082019330820138A003020102308201933082"
)
assert creds[0].user_name == "johnpsmith@example.com"
assert creds[0].user_display_name is None
assert creds[0].creation_time == 2
assert creds[0].hmac_secret is True
# Check that the invalid credential was not added.
creds = fido.list_credentials(client)
assert len(creds) == 1
# Adding an invalid credential should appear as if user cancelled.
with pytest.raises(Cancelled):
fido.add_credential(client, CRED1[:-2])
# Add valid credential, which has same userId as #2, but different rpId.
fido.add_credential(client, CRED3)
# Check that the invalid credential was not added.
creds = fido.list_credentials(client)
assert len(creds) == 1
# Check that the credential was added.
creds = fido.list_credentials(client)
assert len(creds) == 2
# Add valid credential, which has same userId as #2, but different rpId.
fido.add_credential(client, CRED3)
# Fill up the credential storage to maximum capacity.
for cred in CREDS[: RK_CAPACITY - 2]:
fido.add_credential(client, cred)
# Check that the credential was added.
creds = fido.list_credentials(client)
assert len(creds) == 2
# Adding one more valid credential to full storage should fail.
with pytest.raises(TrezorFailure):
# Fill up the credential storage to maximum capacity.
for cred in CREDS[: RK_CAPACITY - 2]:
fido.add_credential(client, cred)
# Adding one more valid credential to full storage should fail.
with pytest.raises(TrezorFailure):
fido.add_credential(client, CREDS[-1])
# Removing the index, which is one past the end, should fail.
with pytest.raises(TrezorFailure):
fido.remove_credential(client, RK_CAPACITY)
# Remove index 2.
fido.remove_credential(client, 2)
# Adding another valid credential should succeed now.
fido.add_credential(client, CREDS[-1])
# Removing the index, which is one past the end, should fail.
with pytest.raises(TrezorFailure):
fido.remove_credential(client, RK_CAPACITY)
# Remove index 2.
fido.remove_credential(client, 2)
# Adding another valid credential should succeed now.
fido.add_credential(client, CREDS[-1])

View File

@ -2335,3 +2335,23 @@ class InputFlowTutorial(InputFlowBase):
self.debug.swipe_up(wait=True)
self.debug.click(buttons.TAP_TO_CONFIRM, wait=True)
self.debug.swipe_up(wait=True)
class InputFlowFidoConfirm(InputFlowBase):
def __init__(self, client: Client, cancel: bool = False):
super().__init__(client)
self.cancel = cancel
def input_flow_tt(self) -> BRGeneratorType:
while True:
yield
self.debug.press_yes()
def input_flow_tr(self) -> BRGeneratorType:
yield from self.input_flow_tt()
def input_flow_t3t1(self) -> BRGeneratorType:
while True:
yield
self.debug.swipe_up(wait=True)
self.debug.click(buttons.TAP_TO_CONFIRM, wait=True)

View File

@ -16855,7 +16855,7 @@
"T3T1_cs_tezos-test_sign_tx.py::test_tezos_smart_contract_delegation": "c86f8a5e634d46bb9e213275ca59ebb7dd57624660c1f8381e3f37b44b3687cb",
"T3T1_cs_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer": "a0ec5adc925d8546a9ee52224f874029ca989882daf762e24eb72701722755a2",
"T3T1_cs_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer_to_contract": "9938e23983b3f11af45521aa0c2488ad21dffd3f75b28f3ef30885da3bfb7123",
"T3T1_cs_webauthn-test_msg_webauthn.py::test_add_remove": "e3a00be2a98bbb17b2b2a13742de59f88011ca85efef334b4a475a48a03dad94",
"T3T1_cs_webauthn-test_msg_webauthn.py::test_add_remove": "0910f0f00f36470da0475b1f8fdd918d6a550b050a32642639481105664cc043",
"T3T1_cs_webauthn-test_u2f_counter.py::test_u2f_counter": "748b990a8eb65c96e9720ac5a339a058390c1546db631dd1cd65aac6ad848251",
"T3T1_cs_zcash-test_sign_tx.py::test_external_presigned": "8e8f8d7c9e18312b93627a202e55bb6b4ed48d2a7c30bc89493bc945fa47fb37",
"T3T1_cs_zcash-test_sign_tx.py::test_one_two": "e4375b012b97230a765d04857674758533540eda29111ba36ccedeb6008472dc",
@ -18197,7 +18197,7 @@
"T3T1_de_tezos-test_sign_tx.py::test_tezos_smart_contract_delegation": "534db7b1cba3a2a0a1cf4d2eba3cf8652001b5c4db1310891871234fcb555e08",
"T3T1_de_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer": "e787f8ef4c14ddcdaae214d30f3499335d9cab6e2066b31a53d47858a407a7e1",
"T3T1_de_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer_to_contract": "fc619426393e344424721750988489067b17a7a85861bc2d7703053a1b99fa46",
"T3T1_de_webauthn-test_msg_webauthn.py::test_add_remove": "e64d049fb19083b94e6333eb1af6e524b24d5c3f1d06da3d286ffcbe806c1072",
"T3T1_de_webauthn-test_msg_webauthn.py::test_add_remove": "03f7773f30726020e07dbc7ab3c5a3a6eaf9d63bb954e13e5022d51539718221",
"T3T1_de_webauthn-test_u2f_counter.py::test_u2f_counter": "688352aeccbd7fbbee6f3ed7e4dbcc1362b78d965a0c854e27e756b9871d43da",
"T3T1_de_zcash-test_sign_tx.py::test_external_presigned": "85f2067de3f218e718ae164e97dc030c4aef065c29755f6138afe10d664a2d56",
"T3T1_de_zcash-test_sign_tx.py::test_one_two": "10258e39046685e302d403a9df4f82cc4b851cfa07d8103f2a52f4afd81fa99d",
@ -19539,7 +19539,7 @@
"T3T1_en_tezos-test_sign_tx.py::test_tezos_smart_contract_delegation": "2488848ff080a1de419fdc6bc7064bf4a0504b306f70df555863e9ddcac66a93",
"T3T1_en_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer": "23ccb252c1ff5847c1db298a36dae6cb48fa2ae0962c10acc05bcf666cbe9875",
"T3T1_en_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer_to_contract": "ba87e90e6d810d1a8e39febbc867d89a69007b1ce7bdcc63224627e14914ab66",
"T3T1_en_webauthn-test_msg_webauthn.py::test_add_remove": "3d20b34ed2f65f424ca962dbd5470d7078b6b01af415aba19ba7c3fd11e5202b",
"T3T1_en_webauthn-test_msg_webauthn.py::test_add_remove": "49a2ce71a462f645ababd1dea2fc88fcf3d1b291391a12209342bf4e36e19b5c",
"T3T1_en_webauthn-test_u2f_counter.py::test_u2f_counter": "eb0670c196dacdf4d35fe8d03ba368ace9b3e90471a0f892ccec5e1ea7b42c10",
"T3T1_en_zcash-test_sign_tx.py::test_external_presigned": "ea19880720b16fe8a730e908b2abfb34cf507940c156e882853a7632b842b329",
"T3T1_en_zcash-test_sign_tx.py::test_one_two": "7346debbcbd3b808e607c9f6a75c638307c976d4c3249fb3ec1dc6c416826a38",
@ -20881,7 +20881,7 @@
"T3T1_es_tezos-test_sign_tx.py::test_tezos_smart_contract_delegation": "290251b9cc8901f682b5238649d59148f2e38f8bf600c9a7371c8704d1ebf531",
"T3T1_es_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer": "8c3138d7b797bcaea06d66998cc049d19448030b5c0f3c93112e5fd046422e67",
"T3T1_es_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer_to_contract": "f91fca0f393aaaa079d872dd72286065f0df707dc66b7cf70b030e63ac4fec45",
"T3T1_es_webauthn-test_msg_webauthn.py::test_add_remove": "6a7c3882c247b42f236385b8b7e913d98d6959b033b16b87692e411873894c0e",
"T3T1_es_webauthn-test_msg_webauthn.py::test_add_remove": "1beb0994c99deb80b0a5a95752ac4453df19abad5e960a4e20d6db2a9bdbae56",
"T3T1_es_webauthn-test_u2f_counter.py::test_u2f_counter": "2e7ce8f4c09f1c65cb451f9784b4f9973bb53ca2ccb9ad4282addc9d0b2beb65",
"T3T1_es_zcash-test_sign_tx.py::test_external_presigned": "43b0985d5076d4b3a9962384e688d6b5bac71f2b493547cfa6cf41a45bb405d2",
"T3T1_es_zcash-test_sign_tx.py::test_one_two": "156d2d776997b10e5b8a85c231e734a95508a6df77ce9c5241333d9adcd71a97",
@ -22223,7 +22223,7 @@
"T3T1_fr_tezos-test_sign_tx.py::test_tezos_smart_contract_delegation": "ddaa5b732f252125d32b0f5a37e7921bcc1ff294d77b09c112e4e3f563b8855c",
"T3T1_fr_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer": "e05975469765465d77085adce8e3adc259e5c8e653a2be6fcdaf428277805f22",
"T3T1_fr_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer_to_contract": "01bdb5b8ceac205728755f3f9e555c30ab7a6caceb03c6156542f2955bf08ea8",
"T3T1_fr_webauthn-test_msg_webauthn.py::test_add_remove": "45717859ce16c0e10b6aad7fe4dadedba449fbad3578c263d33324f936c1bca2",
"T3T1_fr_webauthn-test_msg_webauthn.py::test_add_remove": "e7292add524a8fb87e18786704b7e809e17fa00bf828370e572a61932c90fbee",
"T3T1_fr_webauthn-test_u2f_counter.py::test_u2f_counter": "dc90f97d311358870f5ff032a1af0cc677a8966ea236c82bcd7f7d3f58990c93",
"T3T1_fr_zcash-test_sign_tx.py::test_external_presigned": "ed968fc6f87e77bf1062997e2ac7b1881aef2361331bd4551f823a7f9eb6e639",
"T3T1_fr_zcash-test_sign_tx.py::test_one_two": "24180452ff7c2caec1587e3f0c9cdde8f760fdb060bf0a7a41bcaf80841d0aa6",