parent
e80712f4d9
commit
61277bd80a
@ -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<F: Fn(usize) -> T, T, U> {
|
||||
page_swipe: Swipe,
|
||||
app_name: Label<T>,
|
||||
account_name: Label<T>,
|
||||
icon: Child<Image>,
|
||||
/// Function/closure that will return appropriate page on demand.
|
||||
get_account: F,
|
||||
scrollbar: ScrollBar,
|
||||
fade: bool,
|
||||
controls: U,
|
||||
}
|
||||
|
||||
impl<F, T, U> FidoConfirm<F, T, U>
|
||||
where
|
||||
F: Fn(usize) -> T,
|
||||
T: Deref<Target = str> + From<&'static str>,
|
||||
U: Component<Msg = CancelConfirmMsg>,
|
||||
{
|
||||
pub fn new(
|
||||
app_name: T,
|
||||
get_account: F,
|
||||
page_count: usize,
|
||||
icon_name: Option<T>,
|
||||
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<F, T, U> Component for FidoConfirm<F, T, U>
|
||||
where
|
||||
F: Fn(usize) -> T,
|
||||
T: Deref<Target = str> + From<&'static str>,
|
||||
U: Component<Msg = CancelConfirmMsg>,
|
||||
{
|
||||
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<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),
|
||||
}
|
||||
}
|
||||
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<F, T, U> crate::trace::Trace for FidoConfirm<F, T, U>
|
||||
where
|
||||
F: Fn(usize) -> T,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("FidoPaginatedPage");
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -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<T: AsRef<str>>(icon_name: Option<T>) -> &'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
|
||||
}
|
||||
}
|
@ -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<T: AsRef<str>>(icon_name: Option<T>) -> &'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
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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)
|
@ -0,0 +1 @@
|
||||
from .tt_v2.fido import * # noqa: F401,F403
|
@ -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
|
@ -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?",
|
||||
)
|
||||
)
|
||||
)
|
@ -1 +0,0 @@
|
||||
from .tt_v2.webauthn import * # noqa: F401,F403
|
Loading…
Reference in new issue