diff --git a/core/src/apps/management/get_next_u2f_counter.py b/core/src/apps/management/get_next_u2f_counter.py index 78415ee7cb..63950d9f94 100644 --- a/core/src/apps/management/get_next_u2f_counter.py +++ b/core/src/apps/management/get_next_u2f_counter.py @@ -2,9 +2,7 @@ import storage.device from trezor import ui, wire from trezor.enums import ButtonRequestType from trezor.messages import GetNextU2FCounter, NextU2FCounter -from trezor.ui.components.tt.text import Text - -from apps.common.confirm import require_confirm +from trezor.ui.layouts import confirm_action async def get_next_u2f_counter( @@ -12,10 +10,15 @@ async def get_next_u2f_counter( ) -> NextU2FCounter: if not storage.device.is_initialized(): raise wire.NotInitialized("Device is not initialized") - text = Text("Get next U2F counter", ui.ICON_CONFIG) - text.normal("Do you really want to") - text.bold("increase and retrieve") - text.normal("the U2F counter?") - await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall) + + await confirm_action( + ctx, + "get_u2f_counter", + title="Get next U2F counter", + description="Do you really want to\n{}\nthe U2F counter?", + description_param="increase and retrieve", + icon=ui.ICON_CONFIG, + br_code=ButtonRequestType.ProtectCall, + ) return NextU2FCounter(u2f_counter=storage.device.next_u2f_counter()) diff --git a/core/src/apps/management/set_u2f_counter.py b/core/src/apps/management/set_u2f_counter.py index bcf6c39bd6..4897b06192 100644 --- a/core/src/apps/management/set_u2f_counter.py +++ b/core/src/apps/management/set_u2f_counter.py @@ -2,9 +2,7 @@ import storage.device from trezor import ui, wire from trezor.enums import ButtonRequestType from trezor.messages import SetU2FCounter, Success -from trezor.ui.components.tt.text import Text - -from apps.common.confirm import require_confirm +from trezor.ui.layouts import confirm_action async def set_u2f_counter(ctx: wire.Context, msg: SetU2FCounter) -> Success: @@ -13,10 +11,15 @@ async def set_u2f_counter(ctx: wire.Context, msg: SetU2FCounter) -> Success: if msg.u2f_counter is None: raise wire.ProcessError("No value provided") - text = Text("Set U2F counter", ui.ICON_CONFIG) - text.normal("Do you really want to", "set the U2F counter") - text.bold("to %d?" % msg.u2f_counter) - await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall) + await confirm_action( + ctx, + "set_u2f_counter", + title="Set U2F counter", + description="Do you really want to\nset the U2F counter\nto {}?", + description_param=str(msg.u2f_counter), + icon=ui.ICON_CONFIG, + br_code=ButtonRequestType.ProtectCall, + ) storage.device.set_u2f_counter(msg.u2f_counter) diff --git a/core/src/apps/webauthn/add_resident_credential.py b/core/src/apps/webauthn/add_resident_credential.py index a9a2642fd8..2a72e67df6 100644 --- a/core/src/apps/webauthn/add_resident_credential.py +++ b/core/src/apps/webauthn/add_resident_credential.py @@ -1,11 +1,8 @@ import storage.device from trezor import wire from trezor.messages import Success, WebAuthnAddResidentCredential -from trezor.ui.layouts import show_error_and_raise +from trezor.ui.layouts import confirm_webauthn, show_error_and_raise -from apps.common.confirm import require_confirm - -from .confirm import ConfirmContent, ConfirmInfo from .credential import Fido2Credential from .resident_credentials import store_resident_credential @@ -46,8 +43,8 @@ async def add_resident_credential( red=True, ) - content = ConfirmContent(ConfirmAddCredential(cred)) - await require_confirm(ctx, content) + if not await confirm_webauthn(ctx, ConfirmAddCredential(cred)): + raise wire.ActionCancelled if store_resident_credential(cred): return Success(message="Credential added") diff --git a/core/src/apps/webauthn/fido2.py b/core/src/apps/webauthn/fido2.py index 8e64ad0cf1..248928f48d 100644 --- a/core/src/apps/webauthn/fido2.py +++ b/core/src/apps/webauthn/fido2.py @@ -9,20 +9,14 @@ from storage.fido2 import KEY_AGREEMENT_PRIVKEY, KEY_AGREEMENT_PUBKEY from trezor import config, io, log, loop, ui, utils, workflow from trezor.crypto import aes, der, hashlib, hmac, random from trezor.crypto.curve import nist256p1 -from trezor.ui.components.tt.confirm import ( - CONFIRMED, - Confirm, - ConfirmPageable, - Pageable, -) -from trezor.ui.components.tt.text import Text -from trezor.ui.popup import Popup +from trezor.ui.components.common.confirm import Pageable +from trezor.ui.components.common.webauthn import ConfirmInfo +from trezor.ui.layouts import confirm_webauthn, confirm_webauthn_reset, show_popup from apps.base import set_homescreen from apps.common import cbor from . import common -from .confirm import ConfirmContent, ConfirmInfo from .credential import CRED_ID_MAX_LENGTH, Credential, Fido2Credential, U2fCredential from .resident_credentials import find_by_rp_id_hash, store_resident_credential @@ -596,14 +590,6 @@ async def verify_user(keepalive_callback: KeepaliveCallback) -> bool: return ret -async def confirm(*args: Any, **kwargs: Any) -> bool: - return await Confirm(*args, **kwargs) is CONFIRMED - - -async def confirm_pageable(*args: Any, **kwargs: Any) -> bool: - return await ConfirmPageable(*args, **kwargs) is CONFIRMED - - class State: def __init__(self, cid: int, iface: io.HID) -> None: self.cid = cid @@ -661,23 +647,23 @@ class U2fConfirmRegister(U2fState): async def confirm_dialog(self) -> bool: if self._cred.rp_id_hash in _BOGUS_APPIDS: - text = Text("U2F", ui.ICON_WRONG, ui.RED) if self.cid == _last_good_auth_check_cid: - text.bold("Already registered.") - text.br_half() - text.normal( - "This device is already", "registered with this", "application." + await show_popup( + title="U2F", + subtitle="Already registered.", + description="This device is already\nregistered with this\napplication.", + timeout_ms=_POPUP_TIMEOUT_MS, ) else: - text.bold("Not registered.") - text.br_half() - text.normal( - "This device is not", "registered with this", "application." + await show_popup( + title="U2F", + subtitle="Not registered.", + description="This device is not\nregistered with this\napplication.", + timeout_ms=_POPUP_TIMEOUT_MS, ) - return await Popup(text, _POPUP_TIMEOUT_MS) + return False else: - content = ConfirmContent(self) - return await confirm(content) + return await confirm_webauthn(None, self) def get_header(self) -> str: return "U2F Register" @@ -700,8 +686,7 @@ class U2fConfirmAuthenticate(U2fState): return "U2F Authenticate" async def confirm_dialog(self) -> bool: - content = ConfirmContent(self) - return await confirm(content) + return await confirm_webauthn(None, self) def __eq__(self, other: object) -> bool: return ( @@ -817,8 +802,7 @@ class Fido2ConfirmMakeCredential(Fido2State, ConfirmInfo): return self._cred.account_name() async def confirm_dialog(self) -> bool: - content = ConfirmContent(self) - if not await confirm(content): + if not await confirm_webauthn(None, self): return False if self._user_verification: return await verify_user(KeepaliveCallback(self.cid, self.iface)) @@ -851,11 +835,13 @@ class Fido2ConfirmExcluded(Fido2ConfirmMakeCredential): await send_cmd(cmd, self.iface) self.finished = True - text = Text("FIDO2 Register", ui.ICON_WRONG, ui.RED) - text.bold("Already registered.") - text.br_half() - text.normal("This device is already", "registered with", self._cred.rp_id + ".") - await Popup(text, _POPUP_TIMEOUT_MS) + await show_popup( + title="FIDO2 Register", + subtitle="Already registered.", + description="This device is already\nregistered with {}.", + description_param=self._cred.rp_id, + timeout_ms=_POPUP_TIMEOUT_MS, + ) class Fido2ConfirmGetAssertion(Fido2State, ConfirmInfo, Pageable): @@ -892,8 +878,7 @@ class Fido2ConfirmGetAssertion(Fido2State, ConfirmInfo, Pageable): return len(self._creds) async def confirm_dialog(self) -> bool: - content = ConfirmContent(self) - if not await confirm_pageable(self, content): + if not await confirm_webauthn(None, self, pageable=self): return False if self._user_verification: return await verify_user(KeepaliveCallback(self.cid, self.iface)) @@ -938,11 +923,13 @@ class Fido2ConfirmNoPin(State): await send_cmd(cmd, self.iface) self.finished = True - text = Text("FIDO2 Verify User", ui.ICON_WRONG, ui.RED) - text.bold("Unable to verify user.") - text.br_half() - text.normal("Please enable PIN", "protection.") - return await Popup(text, _POPUP_TIMEOUT_MS) + await show_popup( + title="FIDO2 Verify User", + subtitle="Unable to verify user.", + description="Please enable PIN\nprotection.", + timeout_ms=_POPUP_TIMEOUT_MS, + ) + return False class Fido2ConfirmNoCredentials(Fido2ConfirmGetAssertion): @@ -958,13 +945,13 @@ class Fido2ConfirmNoCredentials(Fido2ConfirmGetAssertion): await send_cmd(cmd, self.iface) self.finished = True - text = Text("FIDO2 Authenticate", ui.ICON_WRONG, ui.RED) - text.bold("Not registered.") - text.br_half() - text.normal( - "This device is not", "registered with", self._creds[0].app_name() + "." + await show_popup( + title="FIDO2 Authenticate", + subtitle="Not registered.", + description="This device is not\nregistered with\n{}.", + description_param=self._creds[0].app_name(), + timeout_ms=_POPUP_TIMEOUT_MS, ) - await Popup(text, _POPUP_TIMEOUT_MS) class Fido2ConfirmReset(Fido2State): @@ -972,10 +959,7 @@ class Fido2ConfirmReset(Fido2State): super().__init__(cid, iface) async def confirm_dialog(self) -> bool: - text = Text("FIDO2 Reset", ui.ICON_CONFIG) - text.normal("Do you really want to") - text.bold("erase all credentials?") - return await confirm(text) + return await confirm_webauthn_reset() async def on_confirm(self) -> None: storage.resident_credentials.delete_all() diff --git a/core/src/apps/webauthn/remove_resident_credential.py b/core/src/apps/webauthn/remove_resident_credential.py index 5e102f1384..cf6583503b 100644 --- a/core/src/apps/webauthn/remove_resident_credential.py +++ b/core/src/apps/webauthn/remove_resident_credential.py @@ -2,10 +2,9 @@ import storage.device import storage.resident_credentials from trezor import wire from trezor.messages import Success, WebAuthnRemoveResidentCredential +from trezor.ui.components.common.webauthn import ConfirmInfo +from trezor.ui.layouts import confirm_webauthn -from apps.common.confirm import require_confirm - -from .confirm import ConfirmContent, ConfirmInfo from .credential import Fido2Credential from .resident_credentials import get_resident_credential @@ -38,8 +37,8 @@ async def remove_resident_credential( if cred is None: raise wire.ProcessError("Invalid credential index.") - content = ConfirmContent(ConfirmRemoveCredential(cred)) - await require_confirm(ctx, content) + if not await confirm_webauthn(ctx, ConfirmRemoveCredential(cred)): + raise wire.ActionCancelled assert cred.index is not None storage.resident_credentials.delete(cred.index) diff --git a/core/src/trezor/ui/components/common/confirm.py b/core/src/trezor/ui/components/common/confirm.py index f57a42abc6..cdca5149e5 100644 --- a/core/src/trezor/ui/components/common/confirm.py +++ b/core/src/trezor/ui/components/common/confirm.py @@ -52,3 +52,26 @@ class ConfirmBase(ui.Layout): from apps.debug import confirm_signal return super().create_tasks() + (confirm_signal(),) + + +class Pageable: + def __init__(self) -> None: + self._page = 0 + + def page(self) -> int: + return self._page + + def page_count(self) -> int: + raise NotImplementedError + + def is_first(self) -> bool: + return self._page == 0 + + def is_last(self) -> bool: + return self._page == self.page_count() - 1 + + def next(self) -> None: + self._page = min(self._page + 1, self.page_count() - 1) + + def prev(self) -> None: + self._page = max(self._page - 1, 0) diff --git a/core/src/trezor/ui/components/common/webauthn.py b/core/src/trezor/ui/components/common/webauthn.py new file mode 100644 index 0000000000..35b7feae6e --- /dev/null +++ b/core/src/trezor/ui/components/common/webauthn.py @@ -0,0 +1,25 @@ +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/components/tt/confirm.py b/core/src/trezor/ui/components/tt/confirm.py index f35f5385a9..30f4c4bb91 100644 --- a/core/src/trezor/ui/components/tt/confirm.py +++ b/core/src/trezor/ui/components/tt/confirm.py @@ -3,7 +3,7 @@ from micropython import const from trezor import loop, res, ui, utils from trezor.ui.loader import Loader, LoaderDefault -from ..common.confirm import CANCELLED, CONFIRMED, INFO, ConfirmBase +from ..common.confirm import CANCELLED, CONFIRMED, INFO, ConfirmBase, Pageable from .button import Button, ButtonAbort, ButtonCancel, ButtonConfirm, ButtonDefault if False: @@ -54,29 +54,6 @@ class Confirm(ConfirmBase): super().__init__(content, button_confirm, button_cancel) -class Pageable: - def __init__(self) -> None: - self._page = 0 - - def page(self) -> int: - return self._page - - def page_count(self) -> int: - raise NotImplementedError - - def is_first(self) -> bool: - return self._page == 0 - - def is_last(self) -> bool: - return self._page == self.page_count() - 1 - - def next(self) -> None: - self._page = min(self._page + 1, self.page_count() - 1) - - def prev(self) -> None: - self._page = max(self._page - 1, 0) - - class ConfirmPageable(Confirm): def __init__(self, pageable: Pageable, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) diff --git a/core/src/apps/webauthn/confirm.py b/core/src/trezor/ui/components/tt/webauthn.py similarity index 59% rename from core/src/apps/webauthn/confirm.py rename to core/src/trezor/ui/components/tt/webauthn.py index 416d9a295d..6196d30358 100644 --- a/core/src/apps/webauthn/confirm.py +++ b/core/src/trezor/ui/components/tt/webauthn.py @@ -1,31 +1,7 @@ from trezor import ui -from trezor.ui.components.tt.text import text_center_trim_left, text_center_trim_right -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 . 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) +from ..common.webauthn import ConfirmInfo +from .text import text_center_trim_left, text_center_trim_right class ConfirmContent(ui.Component): diff --git a/core/src/trezor/ui/layouts/tt.py b/core/src/trezor/ui/layouts/tt.py index 4f6a37b985..cdfb8f815c 100644 --- a/core/src/trezor/ui/layouts/tt.py +++ b/core/src/trezor/ui/layouts/tt.py @@ -5,13 +5,15 @@ from trezor import ui, wire from trezor.enums import ButtonRequestType from trezor.ui.container import Container from trezor.ui.loader import LoaderDanger +from trezor.ui.popup import Popup from trezor.ui.qr import Qr from trezor.utils import chunks, chunks_intersperse from ..components.common import break_path_to_lines from ..components.common.confirm import is_confirmed, raise_if_cancelled +from ..components.common.webauthn import ConfirmInfo from ..components.tt.button import ButtonCancel, ButtonDefault -from ..components.tt.confirm import Confirm, HoldToConfirm +from ..components.tt.confirm import Confirm, ConfirmPageable, HoldToConfirm, Pageable from ..components.tt.scroll import ( PAGEBREAK, Paginated, @@ -19,6 +21,7 @@ from ..components.tt.scroll import ( paginate_text, ) from ..components.tt.text import LINE_WIDTH_PAGINATED, Span, Text +from ..components.tt.webauthn import ConfirmContent from ..constants.tt import ( MONO_ADDR_PER_LINE, MONO_HEX_PER_LINE, @@ -72,6 +75,9 @@ __all__ = ( "confirm_coinjoin", "confirm_timebounds_stellar", "confirm_transfer_binance", + "show_popup", + "confirm_webauthn", + "confirm_webauthn_reset", ) @@ -1009,3 +1015,43 @@ async def confirm_transfer_binance( ctx, Paginated(pages), "confirm_transfer", ButtonRequestType.ConfirmOutput ) ) + + +async def show_popup( + title: str, + description: str, + subtitle: Optional[str] = None, + description_param: str = "", + timeout_ms: int = 3000, +) -> None: + text = Text(title, ui.ICON_WRONG, ui.RED) + if subtitle is not None: + text.bold(subtitle) + text.br_half() + text.format_parametrized(description, description_param) + await Popup(text, timeout_ms) + + +async def confirm_webauthn( + ctx: Optional[wire.GenericContext], + info: ConfirmInfo, + pageable: Optional[Pageable] = None, +) -> bool: + if pageable is not None: + confirm: ui.Layout = ConfirmPageable(pageable, ConfirmContent(info)) + else: + confirm = Confirm(ConfirmContent(info)) + + 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: + text = Text("FIDO2 Reset", ui.ICON_CONFIG) + text.normal("Do you really want to") + text.bold("erase all credentials?") + return is_confirmed(await Confirm(text)) diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index f5937320a3..9d607dc5f7 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -719,5 +719,5 @@ "test_session_id_and_passphrase.py::test_passphrase_on_device": "c9ca2c9cf6dd416dad4de311266690ec2266b551d74f9d3619301305b3dbe81e", "test_session_id_and_passphrase.py::test_session_enable_passphrase": "b27321ed372b8ade7c4941a80f1f945851046b039a1b43c43a6953106bd1619e", "test_session_id_and_passphrase.py::test_session_with_passphrase": "a044d7a42229ab7cd74651e03dd64edcceb86d72d50bd63a40336decf0b25d3d", -"test_u2f_counter.py::test_u2f_counter": "7d96a4d262b9d8a2c1158ac1e5f0f7b2c3ed5f2ba9d6235a014320313f9488fe" +"test_u2f_counter.py::test_u2f_counter": "15865e4364347ac56ea45bbff5ded28fd72f58ad2427d1ef190f125dea10ece0" }