diff --git a/core/SConscript.firmware b/core/SConscript.firmware index 92b1d6762..0353e7804 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -579,13 +579,13 @@ if FROZEN: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/reset.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/recovery.py')) if EVERYTHING: - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/webauthn.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/fido.py')) if TREZOR_MODEL in ('T',): SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/__init__.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/reset.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/recovery.py')) if EVERYTHING: - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/webauthn.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/fido.py')) elif TREZOR_MODEL in ('1',): SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/t1.py')) elif TREZOR_MODEL in ('R',): diff --git a/core/SConscript.unix b/core/SConscript.unix index 03f23ba0c..6f9c8e91e 100644 --- a/core/SConscript.unix +++ b/core/SConscript.unix @@ -533,13 +533,13 @@ if FROZEN: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/reset.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/recovery.py')) if EVERYTHING: - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/webauthn.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/fido.py')) if TREZOR_MODEL in ('T',): SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/__init__.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/reset.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/recovery.py')) if EVERYTHING: - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/webauthn.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/fido.py')) elif TREZOR_MODEL in ('1',): SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/t1.py')) elif TREZOR_MODEL in ('R',): diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index d9851623e..e2902214b 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -20,6 +20,7 @@ static void _librust_qstrs(void) { MP_QSTR_confirm_blob; MP_QSTR_confirm_properties; MP_QSTR_confirm_coinjoin; + MP_QSTR_confirm_fido; MP_QSTR_confirm_joint_total; MP_QSTR_confirm_modify_fee; MP_QSTR_confirm_modify_output; @@ -91,4 +92,7 @@ static void _librust_qstrs(void) { MP_QSTR_active; MP_QSTR_info_button; MP_QSTR_time_ms; + MP_QSTR_app_name; + MP_QSTR_icon_name; + MP_QSTR_accounts; } diff --git a/core/embed/rust/src/ui/model_tt/component/fido.rs b/core/embed/rust/src/ui/model_tt/component/fido.rs new file mode 100644 index 000000000..eacabdcfc --- /dev/null +++ b/core/embed/rust/src/ui/model_tt/component/fido.rs @@ -0,0 +1,210 @@ +use core::ops::Deref; + +use crate::ui::{ + component::{Child, Component, Event, EventCtx, Image, Label}, + display, + geometry::{Alignment, Insets, Rect}, + model_tt::component::{ + fido_icons::get_fido_icon_data, + swipe::{Swipe, SwipeDirection}, + theme, ScrollBar, + }, +}; + +use super::CancelConfirmMsg; + +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 T, T, U> { + page_swipe: Swipe, + app_name: Label, + account_name: Label, + icon: Child, + /// Function/closure that will return appropriate page on demand. + get_account: F, + scrollbar: ScrollBar, + fade: bool, + controls: U, +} + +impl FidoConfirm +where + F: Fn(usize) -> T, + T: Deref + From<&'static str>, + U: Component, +{ + pub fn new( + app_name: T, + get_account: F, + page_count: usize, + icon_name: Option, + controls: U, + ) -> Self { + let icon_data = get_fido_icon_data(icon_name.as_deref()); + + // 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(); + + Self { + app_name: Label::new(app_name, Alignment::Center, theme::TEXT_BOLD), + account_name: Label::new("".into(), Alignment::Center, theme::TEXT_BOLD), + page_swipe, + icon: Child::new(Image::new(icon_data)), + get_account, + scrollbar, + fade: 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(); + + // Redraw the page. + ctx.request_paint(); + + // Reset backlight to normal level on next paint. + self.fade = true; + } + + fn active_page(&self) -> usize { + self.scrollbar.active_page + } +} + +impl Component for FidoConfirm +where + F: Fn(usize) -> T, + T: Deref + From<&'static str>, + U: Component, +{ + type Msg = FidoMsg; + + 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); + + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + 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), + } + } + None + } + + fn paint(&mut self) { + self.icon.paint(); + self.controls.paint(); + self.app_name.paint(); + + if self.scrollbar.page_count > 1 { + self.scrollbar.paint(); + } + + let current_account = (self.get_account)(self.active_page()); + + // 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.) + if !current_account.is_empty() && current_account.deref() != self.app_name.text().deref() { + self.account_name.set_text(current_account); + self.account_name.paint(); + } + + if self.fade { + self.fade = false; + // Note that this is blocking and takes some time. + display::fade_backlight(theme::BACKLIGHT_NORMAL); + } + } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + self.icon.bounds(sink); + self.app_name.bounds(sink); + self.account_name.bounds(sink); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for FidoConfirm +where + F: Fn(usize) -> T, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("FidoPaginatedPage"); + t.close(); + } +} diff --git a/core/embed/rust/src/ui/model_tt/component/fido_icons.rs b/core/embed/rust/src/ui/model_tt/component/fido_icons.rs new file mode 100644 index 000000000..386154516 --- /dev/null +++ b/core/embed/rust/src/ui/model_tt/component/fido_icons.rs @@ -0,0 +1,77 @@ +//! generated from webauthn_icons.rs.mako +//! (by running `make templates` in `core`) +//! do not edit manually! + +const ICON_AWS: &[u8] = include_res!("model_tt/res/fido/icon_aws.toif"); +const ICON_BINANCE: &[u8] = include_res!("model_tt/res/fido/icon_binance.toif"); +const ICON_BITBUCKET: &[u8] = include_res!("model_tt/res/fido/icon_bitbucket.toif"); +const ICON_BITFINEX: &[u8] = include_res!("model_tt/res/fido/icon_bitfinex.toif"); +const ICON_BITWARDEN: &[u8] = include_res!("model_tt/res/fido/icon_bitwarden.toif"); +const ICON_CLOUDFLARE: &[u8] = include_res!("model_tt/res/fido/icon_cloudflare.toif"); +const ICON_COINBASE: &[u8] = include_res!("model_tt/res/fido/icon_coinbase.toif"); +const ICON_DASHLANE: &[u8] = include_res!("model_tt/res/fido/icon_dashlane.toif"); +const ICON_DROPBOX: &[u8] = include_res!("model_tt/res/fido/icon_dropbox.toif"); +const ICON_DUO: &[u8] = include_res!("model_tt/res/fido/icon_duo.toif"); +const ICON_FACEBOOK: &[u8] = include_res!("model_tt/res/fido/icon_facebook.toif"); +const ICON_FASTMAIL: &[u8] = include_res!("model_tt/res/fido/icon_fastmail.toif"); +const ICON_FEDORA: &[u8] = include_res!("model_tt/res/fido/icon_fedora.toif"); +const ICON_GANDI: &[u8] = include_res!("model_tt/res/fido/icon_gandi.toif"); +const ICON_GEMINI: &[u8] = include_res!("model_tt/res/fido/icon_gemini.toif"); +const ICON_GITHUB: &[u8] = include_res!("model_tt/res/fido/icon_github.toif"); +const ICON_GITLAB: &[u8] = include_res!("model_tt/res/fido/icon_gitlab.toif"); +const ICON_GOOGLE: &[u8] = include_res!("model_tt/res/fido/icon_google.toif"); +const ICON_INVITY: &[u8] = include_res!("model_tt/res/fido/icon_invity.toif"); +const ICON_KEEPER: &[u8] = include_res!("model_tt/res/fido/icon_keeper.toif"); +const ICON_KRAKEN: &[u8] = include_res!("model_tt/res/fido/icon_kraken.toif"); +const ICON_LOGIN_GOV: &[u8] = include_res!("model_tt/res/fido/icon_login.gov.toif"); +const ICON_MICROSOFT: &[u8] = include_res!("model_tt/res/fido/icon_microsoft.toif"); +const ICON_MOJEID: &[u8] = include_res!("model_tt/res/fido/icon_mojeid.toif"); +const ICON_NAMECHEAP: &[u8] = include_res!("model_tt/res/fido/icon_namecheap.toif"); +const ICON_PROTON: &[u8] = include_res!("model_tt/res/fido/icon_proton.toif"); +const ICON_SLUSHPOOL: &[u8] = include_res!("model_tt/res/fido/icon_slushpool.toif"); +const ICON_STRIPE: &[u8] = include_res!("model_tt/res/fido/icon_stripe.toif"); +const ICON_TUTANOTA: &[u8] = include_res!("model_tt/res/fido/icon_tutanota.toif"); +/// Default icon when app does not have its own +const ICON_WEBAUTHN: &[u8] = include_res!("model_tt/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) -> &'static [u8] { + if let Some(icon_name) = icon_name { + match icon_name.as_ref() { + "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, + } + } else { + ICON_WEBAUTHN + } +} diff --git a/core/embed/rust/src/ui/model_tt/component/fido_icons.rs.mako b/core/embed/rust/src/ui/model_tt/component/fido_icons.rs.mako new file mode 100644 index 000000000..3de5e4af8 --- /dev/null +++ b/core/embed/rust/src/ui/model_tt/component/fido_icons.rs.mako @@ -0,0 +1,34 @@ +//! generated from webauthn_icons.rs.mako +//! (by running `make templates` in `core`) +//! do not edit manually! +<% +icons: list[tuple[str, str]] = [] +for app in fido: + if app.icon is not None: + # Variable names cannot have a dot in themselves + icon_name = app.key + var_name = icon_name.replace(".", "_").upper() + icons.append((icon_name, var_name)) +%>\ + +% for icon_name, var_name in icons: +const ICON_${var_name}: &[u8] = include_res!("model_tt/res/fido/icon_${icon_name}.toif"); +% endfor +/// Default icon when app does not have its own +const ICON_WEBAUTHN: &[u8] = include_res!("model_tt/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) -> &'static [u8] { + if let Some(icon_name) = icon_name { + match icon_name.as_ref() { +% for icon_name, var_name in icons: + "${icon_name}" => ICON_${var_name}, +% endfor + _ => ICON_WEBAUTHN, + } + } else { + ICON_WEBAUTHN + } +} diff --git a/core/embed/rust/src/ui/model_tt/component/mod.rs b/core/embed/rust/src/ui/model_tt/component/mod.rs index e3e789138..55f40330d 100644 --- a/core/embed/rust/src/ui/model_tt/component/mod.rs +++ b/core/embed/rust/src/ui/model_tt/component/mod.rs @@ -1,5 +1,7 @@ mod button; mod dialog; +mod fido; +mod fido_icons; mod frame; mod hold_to_confirm; mod keyboard; @@ -14,6 +16,7 @@ pub use button::{ CancelInfoConfirmMsg, SelectWordMsg, }; pub use dialog::{Dialog, DialogMsg, IconDialog}; +pub use fido::{FidoConfirm, FidoMsg}; pub use frame::{Frame, NotificationFrame}; pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg}; pub use keyboard::{ diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index 393175b6a..191e8e08b 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -6,7 +6,9 @@ use crate::{ error::Error, micropython::{ buffer::StrBuffer, + gc::Gc, iter::{Iter, IterBuf}, + list::List, map::Map, module::Module, obj::Obj, @@ -37,11 +39,11 @@ use crate::{ use super::{ component::{ Bip39Input, Button, ButtonMsg, ButtonStyleSheet, CancelConfirmMsg, CancelInfoConfirmMsg, - Dialog, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, IconDialog, MnemonicInput, - MnemonicKeyboard, MnemonicKeyboardMsg, NotificationFrame, NumberInputDialog, - NumberInputDialogMsg, PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, - PinKeyboardMsg, SelectWordCount, SelectWordCountMsg, SelectWordMsg, Slip39Input, - SwipeHoldPage, SwipePage, + Dialog, DialogMsg, FidoConfirm, FidoMsg, Frame, HoldToConfirm, HoldToConfirmMsg, + IconDialog, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, NotificationFrame, + NumberInputDialog, NumberInputDialogMsg, PassphraseKeyboard, PassphraseKeyboardMsg, + PinKeyboard, PinKeyboardMsg, SelectWordCount, SelectWordCountMsg, SelectWordMsg, + Slip39Input, SwipeHoldPage, SwipePage, }, theme, }; @@ -89,6 +91,20 @@ impl TryFrom for Obj { } } +impl ComponentMsgObj for FidoConfirm +where + F: Fn(usize) -> T, + T: Deref + From<&'static str>, + U: Component, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + FidoMsg::Confirmed(page) => Ok((page as u8).into()), + FidoMsg::Cancelled => Ok(CANCELLED.as_obj()), + } + } +} + impl ComponentMsgObj for Dialog where T: ComponentMsgObj, @@ -661,6 +677,37 @@ 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: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let app_name: StrBuffer = kwargs.get(Qstr::MP_QSTR_app_name)?.try_into()?; + let icon: Option = kwargs.get(Qstr::MP_QSTR_icon_name)?.try_into_option()?; + let accounts: Gc = 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).styled(theme::button_cancel()), + Button::with_text("CONFIRM").styled(theme::button_confirm()), + 2, + ); + + let fido_page = FidoConfirm::new(app_name, get_page, page_count, icon, controls); + + let obj = LayoutObj::new(Frame::new(title, fido_page).with_border(theme::borders()))?; + 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 icon = BlendedImage::new( @@ -1172,6 +1219,19 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Decrease or increase transaction fee.""" Qstr::MP_QSTR_confirm_modify_fee => obj_fn_kw!(0, new_confirm_modify_fee).as_obj(), + /// def confirm_fido( + /// *, + /// title: str, + /// app_name: str, + /// icon_name: str | None, + /// accounts: list[str | None], + /// ) -> int | object: + /// """FIDO confirmation. + /// + /// Returns page index in case of confirmation and CANCELLED otherwise. + /// """ + Qstr::MP_QSTR_confirm_fido => obj_fn_kw!(0, new_confirm_fido).as_obj(), + /// def show_error( /// *, /// title: str, diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_aws.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_aws.toif new file mode 100644 index 000000000..898afe3e0 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_aws.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_binance.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_binance.toif new file mode 100644 index 000000000..8d9ba3649 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_binance.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_bitbucket.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_bitbucket.toif new file mode 100644 index 000000000..61c4b67f2 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_bitbucket.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_bitfinex.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_bitfinex.toif new file mode 100644 index 000000000..9fe4a9a3c Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_bitfinex.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_bitwarden.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_bitwarden.toif new file mode 100644 index 000000000..6779de1d2 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_bitwarden.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_cloudflare.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_cloudflare.toif new file mode 100644 index 000000000..5c5f803e5 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_cloudflare.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_coinbase.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_coinbase.toif new file mode 100644 index 000000000..5550238a1 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_coinbase.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_dashlane.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_dashlane.toif new file mode 100644 index 000000000..02d618c40 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_dashlane.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_dropbox.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_dropbox.toif new file mode 100644 index 000000000..f82ac6173 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_dropbox.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_duo.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_duo.toif new file mode 100644 index 000000000..173f1d037 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_duo.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_facebook.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_facebook.toif new file mode 100644 index 000000000..78542cbdd Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_facebook.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_fastmail.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_fastmail.toif new file mode 100644 index 000000000..859d3b6f6 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_fastmail.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_fedora.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_fedora.toif new file mode 100644 index 000000000..7b6409304 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_fedora.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_gandi.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_gandi.toif new file mode 100644 index 000000000..19c806316 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_gandi.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_gemini.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_gemini.toif new file mode 100644 index 000000000..10e81eab1 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_gemini.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_github.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_github.toif new file mode 100644 index 000000000..7092c7eaf Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_github.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_gitlab.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_gitlab.toif new file mode 100644 index 000000000..a7eeab8f6 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_gitlab.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_google.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_google.toif new file mode 100644 index 000000000..5c628be29 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_google.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_invity.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_invity.toif new file mode 100644 index 000000000..d13db2667 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_invity.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_keeper.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_keeper.toif new file mode 100644 index 000000000..e15958b4f Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_keeper.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_kraken.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_kraken.toif new file mode 100644 index 000000000..9aaacd6e1 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_kraken.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_login.gov.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_login.gov.toif new file mode 100644 index 000000000..fc02cd70d Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_login.gov.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_microsoft.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_microsoft.toif new file mode 100644 index 000000000..fd54d3ec5 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_microsoft.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_mojeid.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_mojeid.toif new file mode 100644 index 000000000..f08b3544c Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_mojeid.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_namecheap.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_namecheap.toif new file mode 100644 index 000000000..23de6ffcb Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_namecheap.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_proton.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_proton.toif new file mode 100644 index 000000000..478a38a57 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_proton.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_slushpool.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_slushpool.toif new file mode 100644 index 000000000..27bd873d7 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_slushpool.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_stripe.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_stripe.toif new file mode 100644 index 000000000..a198d7a38 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_stripe.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_tutanota.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_tutanota.toif new file mode 100644 index 000000000..80edb2ca3 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_tutanota.toif differ diff --git a/core/embed/rust/src/ui/model_tt/res/fido/icon_webauthn.toif b/core/embed/rust/src/ui/model_tt/res/fido/icon_webauthn.toif new file mode 100644 index 000000000..58817cf1d Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/fido/icon_webauthn.toif differ diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 9059097ed..a107947a7 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -159,6 +159,19 @@ def confirm_modify_fee( """Decrease or increase transaction fee.""" +# rust/src/ui/model_tt/layout.rs +def confirm_fido( + *, + title: str, + app_name: str, + icon_name: str | None, + accounts: list[str | None], +) -> int | object: + """FIDO confirmation. + Returns page index in case of confirmation and CANCELLED otherwise. + """ + + # rust/src/ui/model_tt/layout.rs def show_error( *, diff --git a/core/src/all_modules.py b/core/src/all_modules.py index b98f6cf13..ee72fdd31 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -155,6 +155,8 @@ trezor.ui.layouts import trezor.ui.layouts trezor.ui.layouts.common import trezor.ui.layouts.common +trezor.ui.layouts.fido +import trezor.ui.layouts.fido trezor.ui.layouts.recovery import trezor.ui.layouts.recovery trezor.ui.layouts.reset @@ -165,6 +167,8 @@ trezor.ui.layouts.tr import trezor.ui.layouts.tr trezor.ui.layouts.tt_v2 import trezor.ui.layouts.tt_v2 +trezor.ui.layouts.tt_v2.fido +import trezor.ui.layouts.tt_v2.fido trezor.ui.layouts.tt_v2.recovery import trezor.ui.layouts.tt_v2.recovery trezor.ui.layouts.tt_v2.reset @@ -405,12 +409,6 @@ if not utils.BITCOIN_ONLY: import trezor.enums.TezosBallotType trezor.enums.TezosContractType import trezor.enums.TezosContractType - trezor.ui.components.common.webauthn - import trezor.ui.components.common.webauthn - trezor.ui.layouts.tt_v2.webauthn - import trezor.ui.layouts.tt_v2.webauthn - trezor.ui.layouts.webauthn - import trezor.ui.layouts.webauthn apps.binance import apps.binance apps.binance.get_address diff --git a/core/src/apps/webauthn/add_resident_credential.py b/core/src/apps/webauthn/add_resident_credential.py index a0098dd91..6e2da04c4 100644 --- a/core/src/apps/webauthn/add_resident_credential.py +++ b/core/src/apps/webauthn/add_resident_credential.py @@ -1,27 +1,8 @@ from typing import TYPE_CHECKING -from trezor.ui.components.common.webauthn import ConfirmInfo - if TYPE_CHECKING: from trezor.messages import WebAuthnAddResidentCredential, Success from trezor.wire import Context - from .credential import Fido2Credential - - -class ConfirmAddCredential(ConfirmInfo): - def __init__(self, cred: Fido2Credential): - super().__init__() - self._cred = cred - self.load_icon(cred.rp_id_hash) - - def get_header(self) -> str: - return "Import credential" - - def app_name(self) -> str: - return self._cred.app_name() - - def account_name(self) -> str | None: - return self._cred.account_name() async def add_resident_credential( @@ -30,7 +11,7 @@ async def add_resident_credential( import storage.device as storage_device from trezor import wire from trezor.ui.layouts import show_error_and_raise - from trezor.ui.layouts.webauthn import confirm_webauthn + from trezor.ui.layouts.fido import confirm_fido from trezor.messages import Success from .credential import Fido2Credential from .resident_credentials import store_resident_credential @@ -49,8 +30,13 @@ async def add_resident_credential( "The credential you are trying to import does\nnot belong to this authenticator.", ) - if not await confirm_webauthn(ctx, ConfirmAddCredential(cred)): - raise wire.ActionCancelled + await confirm_fido( + ctx, + "Import credential", + cred.app_name(), + cred.icon_name(), + [cred.account_name()], + ) if store_resident_credential(cred): return Success(message="Credential added") diff --git a/core/src/apps/webauthn/credential.py b/core/src/apps/webauthn/credential.py index aa7eae6c5..0b066bf6b 100644 --- a/core/src/apps/webauthn/credential.py +++ b/core/src/apps/webauthn/credential.py @@ -71,6 +71,12 @@ class Credential: def app_name(self) -> str: raise NotImplementedError + def icon_name(self) -> str | None: + from . import knownapps + + fido_app = knownapps.by_rp_id_hash(self.rp_id_hash) + return None if fido_app is None else fido_app.icon_name + def account_name(self) -> str | None: return None diff --git a/core/src/apps/webauthn/fido2.py b/core/src/apps/webauthn/fido2.py index f669fba58..c62bf247c 100644 --- a/core/src/apps/webauthn/fido2.py +++ b/core/src/apps/webauthn/fido2.py @@ -5,13 +5,10 @@ from micropython import const from typing import TYPE_CHECKING import storage.device as storage_device -from trezor import config, io, log, loop, utils, workflow +from trezor import config, io, log, loop, utils, wire, workflow from trezor.crypto import hashlib from trezor.crypto.curve import nist256p1 -from trezor.ui.components.common.confirm import Pageable -from trezor.ui.components.common.webauthn import ConfirmInfo from trezor.ui.layouts import show_popup -from trezor.ui.layouts.webauthn import confirm_webauthn from apps.base import set_homescreen from apps.common import cbor @@ -20,7 +17,7 @@ from . import common from .credential import Credential, Fido2Credential if TYPE_CHECKING: - from typing import Any, Callable, Coroutine, Iterable, Iterator + from typing import Any, Awaitable, Callable, Coroutine, Iterable, Iterator from .credential import U2fCredential HID = io.HID @@ -587,6 +584,33 @@ async def verify_user(keepalive_callback: KeepaliveCallback) -> bool: return ret +def _confirm_fido_choose(title: str, credentials: list[Credential]) -> Awaitable[int]: + from trezor.ui.layouts.fido import confirm_fido + from . import knownapps + + assert len(credentials) > 0 + repr_credential = credentials[0] + + if __debug__: + for cred in credentials: + assert cred.rp_id_hash == repr_credential.rp_id_hash + + app_name = repr_credential.app_name() + app = knownapps.by_rp_id_hash(repr_credential.rp_id_hash) + icon_name = None if app is None else app.icon_name + return confirm_fido( + None, title, app_name, icon_name, [c.account_name() for c in credentials] + ) + + +async def _confirm_fido(title: str, credential: Credential) -> bool: + try: + await _confirm_fido_choose(title, [credential]) + return True + except wire.ActionCancelled: + return False + + class State: def __init__(self, cid: int, iface: HID) -> None: self.cid = cid @@ -616,13 +640,11 @@ class State: pass -class U2fState(State, ConfirmInfo): +class U2fState(State): def __init__(self, cid: int, iface: HID, req_data: bytes, cred: Credential) -> None: State.__init__(self, cid, iface) - ConfirmInfo.__init__(self) self._cred = cred self._req_data = req_data - self.load_icon(self._cred.rp_id_hash) def timeout_ms(self) -> int: return _U2F_CONFIRM_TIMEOUT_MS @@ -658,10 +680,7 @@ class U2fConfirmRegister(U2fState): ) return False else: - return await confirm_webauthn(None, self) - - def get_header(self) -> str: - return "U2F Register" + return await _confirm_fido("U2F Register", self._cred) def __eq__(self, other: object) -> bool: return ( @@ -675,11 +694,8 @@ class U2fConfirmAuthenticate(U2fState): def __init__(self, cid: int, iface: HID, req_data: bytes, cred: Credential) -> None: super().__init__(cid, iface, req_data, cred) - def get_header(self) -> str: - return "U2F Authenticate" - async def confirm_dialog(self) -> bool: - return await confirm_webauthn(None, self) + return await _confirm_fido("U2F Authenticate", self._cred) def __eq__(self, other: object) -> bool: return ( @@ -767,7 +783,7 @@ class Fido2Unlock(Fido2State): await send_cmd(self.resp, self.iface) -class Fido2ConfirmMakeCredential(Fido2State, ConfirmInfo): +class Fido2ConfirmMakeCredential(Fido2State): def __init__( self, cid: int, @@ -778,24 +794,13 @@ class Fido2ConfirmMakeCredential(Fido2State, ConfirmInfo): user_verification: bool, ) -> None: Fido2State.__init__(self, cid, iface) - ConfirmInfo.__init__(self) self._client_data_hash = client_data_hash self._cred = cred self._resident = resident self._user_verification = user_verification - self.load_icon(cred.rp_id_hash) - - def get_header(self) -> str: - return "FIDO2 Register" - - def app_name(self) -> str: - return self._cred.app_name() - - def account_name(self) -> str | None: - return self._cred.account_name() async def confirm_dialog(self) -> bool: - if not await confirm_webauthn(None, self): + if not await _confirm_fido("FIDO2 Register", self._cred): return False if self._user_verification: return await verify_user(KeepaliveCallback(self.cid, self.iface)) @@ -839,7 +844,7 @@ class Fido2ConfirmExcluded(Fido2ConfirmMakeCredential): ) -class Fido2ConfirmGetAssertion(Fido2State, ConfirmInfo, Pageable): +class Fido2ConfirmGetAssertion(Fido2State): def __init__( self, cid: int, @@ -851,30 +856,21 @@ class Fido2ConfirmGetAssertion(Fido2State, ConfirmInfo, Pageable): user_verification: bool, ) -> None: Fido2State.__init__(self, cid, iface) - ConfirmInfo.__init__(self) - Pageable.__init__(self) self._client_data_hash = client_data_hash self._creds = creds self._hmac_secret = hmac_secret self._resident = resident self._user_verification = user_verification - self.load_icon(self._creds[0].rp_id_hash) - - def get_header(self) -> str: - return "FIDO2 Authenticate" - - def app_name(self) -> str: - return self._creds[self.page()].app_name() - - def account_name(self) -> str | None: - return self._creds[self.page()].account_name() - - def page_count(self) -> int: - return len(self._creds) + self._selected_cred: Credential | None = None async def confirm_dialog(self) -> bool: - if not await confirm_webauthn(None, self, pageable=self): + # There is a choice from more than one credential. + try: + index = await _confirm_fido_choose("FIDO2 Authenticate", self._creds) + except wire.ActionCancelled: return False + + self._selected_cred = self._creds[index] if self._user_verification: return await verify_user(KeepaliveCallback(self.cid, self.iface)) return True @@ -882,13 +878,13 @@ class Fido2ConfirmGetAssertion(Fido2State, ConfirmInfo, Pageable): async def on_confirm(self) -> None: cid = self.cid # local_cache_attribute - cred = self._creds[self.page()] + assert self._selected_cred is not None try: send_cmd_sync(cmd_keepalive(cid, _KEEPALIVE_STATUS_PROCESSING), self.iface) response_data = cbor_get_assertion_sign( self._client_data_hash, - cred.rp_id_hash, - cred, + self._selected_cred.rp_id_hash, + self._selected_cred, self._hmac_secret, self._resident, True, @@ -954,9 +950,9 @@ class Fido2ConfirmReset(Fido2State): super().__init__(cid, iface) async def confirm_dialog(self) -> bool: - from trezor.ui.layouts.webauthn import confirm_webauthn_reset + from trezor.ui.layouts.fido import confirm_fido_reset - return await confirm_webauthn_reset() + return await confirm_fido_reset() async def on_confirm(self) -> None: import storage.resident_credentials diff --git a/core/src/apps/webauthn/knownapps.py b/core/src/apps/webauthn/knownapps.py index 72c62b6cb..096186cf9 100644 --- a/core/src/apps/webauthn/knownapps.py +++ b/core/src/apps/webauthn/knownapps.py @@ -9,12 +9,12 @@ class FIDOApp: def __init__( self, label: str, - icon: str | None, + icon_name: str | None, use_sign_count: bool | None, use_self_attestation: bool | None, ) -> None: self.label = label - self.icon = icon + self.icon_name = icon_name self.use_sign_count = use_sign_count self.use_self_attestation = use_self_attestation @@ -25,7 +25,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Amazon Web Services return FIDOApp( "aws.amazon.com", # label - "apps/webauthn/res/icon_aws.toif", # icon + "aws", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -33,7 +33,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for Binance return FIDOApp( "www.binance.com", # label - "apps/webauthn/res/icon_binance.toif", # icon + "binance", # icon_name False, # use_sign_count True, # use_self_attestation ) @@ -41,7 +41,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for Binance return FIDOApp( "binance.com", # label - "apps/webauthn/res/icon_binance.toif", # icon + "binance", # icon_name False, # use_sign_count True, # use_self_attestation ) @@ -49,7 +49,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Bitbucket return FIDOApp( "bitbucket.org", # label - "apps/webauthn/res/icon_bitbucket.toif", # icon + "bitbucket", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -57,7 +57,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Bitfinex return FIDOApp( "www.bitfinex.com", # label - "apps/webauthn/res/icon_bitfinex.toif", # icon + "bitfinex", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -65,7 +65,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Bitwarden return FIDOApp( "vault.bitwarden.com", # label - "apps/webauthn/res/icon_bitwarden.toif", # icon + "bitwarden", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -73,7 +73,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for Cloudflare return FIDOApp( "dash.cloudflare.com", # label - "apps/webauthn/res/icon_cloudflare.toif", # icon + "cloudflare", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -81,7 +81,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for Coinbase return FIDOApp( "coinbase.com", # label - "apps/webauthn/res/icon_coinbase.toif", # icon + "coinbase", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -89,7 +89,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Dashlane return FIDOApp( "www.dashlane.com", # label - "apps/webauthn/res/icon_dashlane.toif", # icon + "dashlane", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -97,7 +97,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Dropbox return FIDOApp( "www.dropbox.com", # label - "apps/webauthn/res/icon_dropbox.toif", # icon + "dropbox", # icon_name False, # use_sign_count None, # use_self_attestation ) @@ -105,7 +105,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for Dropbox return FIDOApp( "www.dropbox.com", # label - "apps/webauthn/res/icon_dropbox.toif", # icon + "dropbox", # icon_name False, # use_sign_count None, # use_self_attestation ) @@ -113,7 +113,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Duo return FIDOApp( "duosecurity.com", # label - "apps/webauthn/res/icon_duo.toif", # icon + "duo", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -121,7 +121,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for Facebook return FIDOApp( "facebook.com", # label - "apps/webauthn/res/icon_facebook.toif", # icon + "facebook", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -129,7 +129,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for FastMail return FIDOApp( "www.fastmail.com", # label - "apps/webauthn/res/icon_fastmail.toif", # icon + "fastmail", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -137,7 +137,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for FastMail return FIDOApp( "fastmail.com", # label - "apps/webauthn/res/icon_fastmail.toif", # icon + "fastmail", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -145,7 +145,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Fedora return FIDOApp( "fedoraproject.org", # label - "apps/webauthn/res/icon_fedora.toif", # icon + "fedora", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -153,7 +153,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Gandi return FIDOApp( "gandi.net", # label - "apps/webauthn/res/icon_gandi.toif", # icon + "gandi", # icon_name False, # use_sign_count None, # use_self_attestation ) @@ -161,7 +161,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for Gandi return FIDOApp( "gandi.net", # label - "apps/webauthn/res/icon_gandi.toif", # icon + "gandi", # icon_name False, # use_sign_count None, # use_self_attestation ) @@ -169,7 +169,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for Gemini return FIDOApp( "gemini.com", # label - "apps/webauthn/res/icon_gemini.toif", # icon + "gemini", # icon_name False, # use_sign_count True, # use_self_attestation ) @@ -177,7 +177,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for GitHub return FIDOApp( "github.com", # label - "apps/webauthn/res/icon_github.toif", # icon + "github", # icon_name True, # use_sign_count None, # use_self_attestation ) @@ -185,7 +185,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for GitHub return FIDOApp( "github.com", # label - "apps/webauthn/res/icon_github.toif", # icon + "github", # icon_name True, # use_sign_count None, # use_self_attestation ) @@ -193,7 +193,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for GitLab return FIDOApp( "gitlab.com", # label - "apps/webauthn/res/icon_gitlab.toif", # icon + "gitlab", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -201,7 +201,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Google return FIDOApp( "google.com", # label - "apps/webauthn/res/icon_google.toif", # icon + "google", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -209,7 +209,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for Google return FIDOApp( "google.com", # label - "apps/webauthn/res/icon_google.toif", # icon + "google", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -217,7 +217,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for Invity return FIDOApp( "invity.io", # label - "apps/webauthn/res/icon_invity.toif", # icon + "invity", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -225,7 +225,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Keeper return FIDOApp( "keepersecurity.com", # label - "apps/webauthn/res/icon_keeper.toif", # icon + "keeper", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -233,7 +233,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Keeper return FIDOApp( "keepersecurity.eu", # label - "apps/webauthn/res/icon_keeper.toif", # icon + "keeper", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -241,7 +241,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for Kraken return FIDOApp( "kraken.com", # label - "apps/webauthn/res/icon_kraken.toif", # icon + "kraken", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -249,7 +249,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for login.gov return FIDOApp( "secure.login.gov", # label - "apps/webauthn/res/icon_login.gov.toif", # icon + "login.gov", # icon_name False, # use_sign_count None, # use_self_attestation ) @@ -257,7 +257,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for Microsoft return FIDOApp( "login.microsoft.com", # label - "apps/webauthn/res/icon_microsoft.toif", # icon + "microsoft", # icon_name False, # use_sign_count False, # use_self_attestation ) @@ -265,7 +265,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for mojeID return FIDOApp( "mojeid.cz", # label - "apps/webauthn/res/icon_mojeid.toif", # icon + "mojeid", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -273,7 +273,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for Namecheap return FIDOApp( "www.namecheap.com", # label - "apps/webauthn/res/icon_namecheap.toif", # icon + "namecheap", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -281,7 +281,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for Proton return FIDOApp( "proton.me", # label - "apps/webauthn/res/icon_proton.toif", # icon + "proton", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -289,7 +289,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Slush Pool return FIDOApp( "slushpool.com", # label - "apps/webauthn/res/icon_slushpool.toif", # icon + "slushpool", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -297,7 +297,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Slush Pool return FIDOApp( "slushpool.com", # label - "apps/webauthn/res/icon_slushpool.toif", # icon + "slushpool", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -305,7 +305,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Stripe return FIDOApp( "stripe.com", # label - "apps/webauthn/res/icon_stripe.toif", # icon + "stripe", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -313,7 +313,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for Tutanota return FIDOApp( "tutanota.com", # label - "apps/webauthn/res/icon_tutanota.toif", # icon + "tutanota", # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -321,7 +321,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # U2F key for u2f.bin.coffee return FIDOApp( "u2f.bin.coffee", # label - None, # icon + None, # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -329,7 +329,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for webauthn.bin.coffee return FIDOApp( "webauthn.bin.coffee", # label - None, # icon + None, # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -337,7 +337,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for WebAuthn.io return FIDOApp( "webauthn.io", # label - None, # icon + None, # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -345,7 +345,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for WebAuthn.me return FIDOApp( "webauthn.me", # label - None, # icon + None, # icon_name None, # use_sign_count None, # use_self_attestation ) @@ -353,7 +353,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # WebAuthn key for demo.yubico.com return FIDOApp( "demo.yubico.com", # label - None, # icon + None, # icon_name None, # use_sign_count None, # use_self_attestation ) diff --git a/core/src/apps/webauthn/knownapps.py.mako b/core/src/apps/webauthn/knownapps.py.mako index 99102dfdf..93cf9e8f5 100644 --- a/core/src/apps/webauthn/knownapps.py.mako +++ b/core/src/apps/webauthn/knownapps.py.mako @@ -9,12 +9,12 @@ class FIDOApp: def __init__( self, label: str, - icon: str | None, + icon_name: str | None, use_sign_count: bool | None, use_self_attestation: bool | None, ) -> None: self.label = label - self.icon = icon + self.icon_name = icon_name self.use_sign_count = use_sign_count self.use_self_attestation = use_self_attestation @@ -30,9 +30,9 @@ for app in fido: rp_id_hash = sha256(origin.encode()).digest() fido_entries.append((origin, rp_id_hash, "WebAuthn", app)) if app.icon is not None: - app.icon_res = f"apps/webauthn/res/icon_{app.key}.toif" + app.icon_name = app.key else: - app.icon_res = None + app.icon_name = None %>\ # fmt: off def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: @@ -41,7 +41,7 @@ def by_rp_id_hash(rp_id_hash: bytes) -> FIDOApp | None: # ${type} key for ${app.name} return FIDOApp( ${black_repr(label)}, # label - ${black_repr(app.icon_res)}, # icon + ${black_repr(app.icon_name)}, # icon_name ${black_repr(app.use_sign_count)}, # use_sign_count ${black_repr(app.use_self_attestation)}, # use_self_attestation ) diff --git a/core/src/apps/webauthn/remove_resident_credential.py b/core/src/apps/webauthn/remove_resident_credential.py index dc6c380f0..6a5714897 100644 --- a/core/src/apps/webauthn/remove_resident_credential.py +++ b/core/src/apps/webauthn/remove_resident_credential.py @@ -1,29 +1,10 @@ from typing import TYPE_CHECKING -from trezor.ui.components.common.webauthn import ConfirmInfo - if TYPE_CHECKING: from trezor.messages import WebAuthnRemoveResidentCredential, Success - from .credential import Fido2Credential from trezor.wire import Context -class ConfirmRemoveCredential(ConfirmInfo): - def __init__(self, cred: Fido2Credential): - super().__init__() - self._cred = cred - self.load_icon(cred.rp_id_hash) - - def get_header(self) -> str: - return "Remove credential" - - def app_name(self) -> str: - return self._cred.app_name() - - def account_name(self) -> str | None: - return self._cred.account_name() - - async def remove_resident_credential( ctx: Context, msg: WebAuthnRemoveResidentCredential ) -> Success: @@ -31,7 +12,7 @@ async def remove_resident_credential( import storage.resident_credentials from trezor import wire from trezor.messages import Success - from trezor.ui.layouts.webauthn import confirm_webauthn + from trezor.ui.layouts.fido import confirm_fido from .resident_credentials import get_resident_credential if not storage.device.is_initialized(): @@ -43,8 +24,13 @@ async def remove_resident_credential( if cred is None: raise wire.ProcessError("Invalid credential index.") - if not await confirm_webauthn(ctx, ConfirmRemoveCredential(cred)): - raise wire.ActionCancelled + await confirm_fido( + ctx, + "Remove credential", + cred.app_name(), + cred.icon_name(), + [cred.account_name()], + ) assert cred.index is not None storage.resident_credentials.delete(cred.index) diff --git a/core/src/trezor/ui/components/common/webauthn.py b/core/src/trezor/ui/components/common/webauthn.py deleted file mode 100644 index 35b7feae6..000000000 --- a/core/src/trezor/ui/components/common/webauthn.py +++ /dev/null @@ -1,25 +0,0 @@ -DEFAULT_ICON = "apps/webauthn/res/icon_webauthn.toif" - - -class ConfirmInfo: - def __init__(self) -> None: - self.app_icon: bytes | None = None - - def get_header(self) -> str: - raise NotImplementedError - - def app_name(self) -> str: - raise NotImplementedError - - def account_name(self) -> str | None: - return None - - def load_icon(self, rp_id_hash: bytes) -> None: - from trezor import res - from apps.webauthn import knownapps - - fido_app = knownapps.by_rp_id_hash(rp_id_hash) - if fido_app is not None and fido_app.icon is not None: - self.app_icon = res.load(fido_app.icon) - else: - self.app_icon = res.load(DEFAULT_ICON) diff --git a/core/src/trezor/ui/layouts/fido.py b/core/src/trezor/ui/layouts/fido.py new file mode 100644 index 000000000..49d21b2de --- /dev/null +++ b/core/src/trezor/ui/layouts/fido.py @@ -0,0 +1 @@ +from .tt_v2.fido import * # noqa: F401,F403 diff --git a/core/src/trezor/ui/layouts/tt_v2/fido.py b/core/src/trezor/ui/layouts/tt_v2/fido.py new file mode 100644 index 000000000..0e5067289 --- /dev/null +++ b/core/src/trezor/ui/layouts/tt_v2/fido.py @@ -0,0 +1,96 @@ +from typing import TYPE_CHECKING + +from trezor.enums import ButtonRequestType + +import trezorui2 + +from ..common import interact +from . import _RustLayout + +if TYPE_CHECKING: + from trezor.loop import AwaitableTask + from trezor.wire import GenericContext + + +if __debug__: + from trezor import io + from ... import Result + + class _RustFidoLayoutImpl(_RustLayout): + def create_tasks(self) -> tuple[AwaitableTask, ...]: + return ( + self.handle_timers(), + self.handle_input_and_rendering(), + self.handle_swipe(), + self.handle_debug_confirm(), + ) + + async def handle_debug_confirm(self) -> None: + from apps.debug import confirm_signal + + try: + await confirm_signal() + except Result as r: + if r.value is not trezorui2.CONFIRMED: + raise + else: + return + + for event, x, y in ( + (io.TOUCH_START, 220, 220), + (io.TOUCH_END, 220, 220), + ): + msg = self.layout.touch_event(event, x, y) + self.layout.paint() + if msg is not None: + raise Result(msg) + + _RustFidoLayout = _RustFidoLayoutImpl + +else: + _RustFidoLayout = _RustLayout + + +async def confirm_fido( + ctx: GenericContext | None, + header: str, + app_name: str, + icon_name: str | None, + accounts: list[str | None], +) -> int: + """Webauthn confirmation for one or more credentials.""" + confirm = _RustFidoLayout( + trezorui2.confirm_fido( + title=header.upper(), + app_name=app_name, + icon_name=icon_name, + accounts=accounts, + ) + ) + + if ctx is None: + result = await confirm + else: + result = await interact(ctx, confirm, "confirm_fido", ButtonRequestType.Other) + + # The Rust side returns either an int or `CANCELLED`. We detect the int situation + # and assume cancellation otherwise. + if isinstance(result, int): + return result + + # Late import won't get executed on the happy path. + from trezor.wire import ActionCancelled + + raise ActionCancelled + + +async def confirm_fido_reset() -> bool: + confirm = _RustLayout( + trezorui2.confirm_action( + title="FIDO2 RESET", + action="erase all credentials?", + description="Do you really want to", + reverse=True, + ) + ) + return (await confirm) is trezorui2.CONFIRMED diff --git a/core/src/trezor/ui/layouts/tt_v2/webauthn.py b/core/src/trezor/ui/layouts/tt_v2/webauthn.py deleted file mode 100644 index d70caa09f..000000000 --- a/core/src/trezor/ui/layouts/tt_v2/webauthn.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import TYPE_CHECKING - -from trezor.enums import ButtonRequestType - -import trezorui2 - -from ...components.common.confirm import is_confirmed -from ...components.common.webauthn import ConfirmInfo -from ..common import interact -from . import _RustLayout - -if TYPE_CHECKING: - from trezor.wire import GenericContext - - Pageable = object - - -async def confirm_webauthn( - ctx: GenericContext | None, - info: ConfirmInfo, - pageable: Pageable | None = None, -) -> bool: - if pageable is not None: - raise NotImplementedError - - confirm = _RustLayout( - trezorui2.confirm_blob( - title=info.get_header().upper(), - data=f"{info.app_name()}\n{info.account_name()}", - ) - ) - - if ctx is None: - return is_confirmed(await confirm) - else: - return is_confirmed( - await interact(ctx, confirm, "confirm_webauthn", ButtonRequestType.Other) - ) - - -async def confirm_webauthn_reset() -> bool: - return is_confirmed( - await _RustLayout( - trezorui2.confirm_blob( - title="FIDO2 RESET", - data="Do you really want to\nerase all credentials?", - ) - ) - ) diff --git a/core/src/trezor/ui/layouts/webauthn.py b/core/src/trezor/ui/layouts/webauthn.py deleted file mode 100644 index 3cd18dfb1..000000000 --- a/core/src/trezor/ui/layouts/webauthn.py +++ /dev/null @@ -1 +0,0 @@ -from .tt_v2.webauthn import * # noqa: F401,F403 diff --git a/core/tools/build_icons.py b/core/tools/build_icons.py index 15c0fc635..e4f4f478a 100755 --- a/core/tools/build_icons.py +++ b/core/tools/build_icons.py @@ -12,7 +12,9 @@ HERE = Path(__file__).resolve().parent ROOT = HERE.parent.parent ICON_SIZE = (64, 64) -DESTINATION = ROOT / "core" / "src" / "apps" / "webauthn" / "res" +DESTINATION = ( + ROOT / "core" / "embed" / "rust" / "src" / "ui" / "model_tt" / "res" / "fido" +) EXCLUDE = {"icon_webauthn"} # insert ../../common/tools to sys.path, so that we can import coin_info diff --git a/core/tools/build_templates b/core/tools/build_templates index 41a54210c..e6e9af2e4 100755 --- a/core/tools/build_templates +++ b/core/tools/build_templates @@ -4,7 +4,8 @@ set -e CWD=`dirname "$0"` RENDER="$CWD/../vendor/trezor-common/tools/cointool.py render" -FIND_TEMPLATES="find $CWD/../src -name *.mako -not -name _proto*" +# Search both in `core/src` and `core/embed` +FIND_TEMPLATES="find $CWD/.. -name *.mako -not -name _proto*" check_results() { CHECK_FAIL=0 diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index 257c3b47f..acf09074a 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -1667,7 +1667,7 @@ "TT_tezos-test_sign_tx.py::test_tezos_smart_contract_delegation": "683a47bb078a50bf08a16446c90fab70a68a3cec8689abca494fd99b31da2ea6", "TT_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer": "295eac0f3667c0af660575fedefa98cae89ec0882892dc8769adf61751dc12fb", "TT_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer_to_contract": "3e781bd83c0019ba51b9034b8bc53c5a09ea9c5c6fe73570004d46a061ba43ea", -"TT_webauthn-test_msg_webauthn.py::test_add_remove": "7ac75b93373119b69a38c4bacfa51579d7052abc8b10f74f937a78e9a4afb5d9", +"TT_webauthn-test_msg_webauthn.py::test_add_remove": "943e29255d6738cc6824c2565001c05906c2cd032aa2eb09993782564e890a16", "TT_webauthn-test_u2f_counter.py::test_u2f_counter": "c6a8e270ce726c7693e2ff88f9af57c56f2d3d8b1cc9c04b6f1dc71e13fcb88e", "TT_zcash-test_sign_tx.py::test_external_presigned": "8781b601169bd64c90ee4dd9c517af905e2cf5fe10bdb474116d17f3d633e06a", "TT_zcash-test_sign_tx.py::test_one_two": "003f12c0ff194bde070fdeb6c8663bf322efca094e9787b2304231c25938b715",