diff --git a/common/protob/messages-management.proto b/common/protob/messages-management.proto index a31cd5706..cc87c9338 100644 --- a/common/protob/messages-management.proto +++ b/common/protob/messages-management.proto @@ -122,6 +122,7 @@ message Features { optional bool experimental_features = 40; // are experimental message types enabled? optional bool busy = 41; // is the device busy, showing "Do not disconnect"? optional HomescreenFormat homescreen_format = 42; // format of the homescreen, 1 = TOIf 144x144, 2 = jpg 240x240 + optional bool hide_passphrase_from_host = 43; // should we hide the passphrase when it comes from host? } /** @@ -168,6 +169,7 @@ message ApplySettings { optional bool passphrase_always_on_device = 8; // do not prompt for passphrase, enforce device entry optional SafetyCheckLevel safety_checks = 9; // Safety check level, set to Prompt to limit path namespace enforcement optional bool experimental_features = 10; // enable experimental message types + optional bool hide_passphrase_from_host = 11; // do not show passphrase coming from host } /** diff --git a/core/src/apps/base.py b/core/src/apps/base.py index 90c3bb9c1..8bd26fe60 100644 --- a/core/src/apps/base.py +++ b/core/src/apps/base.py @@ -128,6 +128,7 @@ def get_features() -> Features: f.auto_lock_delay_ms = storage_device.get_autolock_delay_ms() f.display_rotation = storage_device.get_rotation() f.experimental_features = storage_device.get_experimental_features() + f.hide_passphrase_from_host = storage_device.get_hide_passphrase_from_host() return f diff --git a/core/src/apps/common/passphrase.py b/core/src/apps/common/passphrase.py index 1359bba3e..f78618f8b 100644 --- a/core/src/apps/common/passphrase.py +++ b/core/src/apps/common/passphrase.py @@ -59,19 +59,29 @@ async def _request_on_host(ctx: Context) -> str: if passphrase: from trezor.ui.layouts import confirm_action, confirm_blob - await confirm_action( - ctx, - "passphrase_host1", - "Hidden wallet", - description="Access hidden wallet?\n\nNext screen will show the passphrase!", - ) - - await confirm_blob( - ctx, - "passphrase_host2", - "Hidden wallet", - passphrase, - "Use this passphrase?\n", - ) + # We want to hide the passphrase, or show it, according to settings. + if storage_device.get_hide_passphrase_from_host(): + explanation = "Passphrase provided by host will be used but will not be displayed due to the device settings." + await confirm_action( + ctx, + "passphrase_host1_hidden", + "Hidden wallet", + description=f"Access hidden wallet?\n\n{explanation}", + ) + else: + await confirm_action( + ctx, + "passphrase_host1", + "Hidden wallet", + description="Access hidden wallet?\n\nNext screen will show the passphrase!", + ) + + await confirm_blob( + ctx, + "passphrase_host2", + "Hidden wallet", + passphrase, + "Use this passphrase?\n", + ) return passphrase diff --git a/core/src/apps/management/apply_settings.py b/core/src/apps/management/apply_settings.py index 8194a9eea..bde8846d8 100644 --- a/core/src/apps/management/apply_settings.py +++ b/core/src/apps/management/apply_settings.py @@ -58,6 +58,7 @@ async def apply_settings(ctx: Context, msg: ApplySettings) -> Success: display_rotation = msg.display_rotation # local_cache_attribute msg_safety_checks = msg.safety_checks # local_cache_attribute experimental_features = msg.experimental_features # local_cache_attribute + hide_passphrase_from_host = msg.hide_passphrase_from_host # local_cache_attribute if ( homescreen is None @@ -68,6 +69,7 @@ async def apply_settings(ctx: Context, msg: ApplySettings) -> Success: and auto_lock_delay_ms is None and msg_safety_checks is None and experimental_features is None + and hide_passphrase_from_host is None ): raise ProcessError("No setting provided") @@ -117,6 +119,12 @@ async def apply_settings(ctx: Context, msg: ApplySettings) -> Success: await _require_confirm_experimental_features(ctx, experimental_features) storage_device.set_experimental_features(experimental_features) + if hide_passphrase_from_host is not None: + if safety_checks.is_strict(): + raise ProcessError("Safety checks are strict") + await _require_confirm_hide_passphrase_from_host(ctx, hide_passphrase_from_host) + storage_device.set_hide_passphrase_from_host(hide_passphrase_from_host) + reload_settings_from_storage() return Success(message="Settings applied") @@ -269,3 +277,16 @@ async def _require_confirm_experimental_features( reverse=True, br_code=BRT_PROTECT_CALL, ) + + +async def _require_confirm_hide_passphrase_from_host( + ctx: GenericContext, enable: bool +) -> None: + if enable: + await confirm_action( + ctx, + "set_hide_passphrase_from_host", + "Hide passphrase", + description="Hide passphrase coming from host?", + br_code=BRT_PROTECT_CALL, + ) diff --git a/core/src/storage/device.py b/core/src/storage/device.py index 29ea50f95..8213a38da 100644 --- a/core/src/storage/device.py +++ b/core/src/storage/device.py @@ -34,6 +34,7 @@ _SD_SALT_AUTH_KEY = const(0x12) # bytes INITIALIZED = const(0x13) # bool (0x01 or empty) _SAFETY_CHECK_LEVEL = const(0x14) # int _EXPERIMENTAL_FEATURES = const(0x15) # bool (0x01 or empty) +_HIDE_PASSPHRASE_FROM_HOST = const(0x16) # bool (0x01 or empty) SAFETY_CHECK_LEVEL_STRICT : Literal[0] = const(0) SAFETY_CHECK_LEVEL_PROMPT : Literal[1] = const(1) @@ -334,3 +335,17 @@ def set_experimental_features(enabled: bool) -> None: cached_bytes = b"\x01" if enabled else b"" storage_cache.set(storage_cache.STORAGE_DEVICE_EXPERIMENTAL_FEATURES, cached_bytes) common.set_true_or_delete(_NAMESPACE, _EXPERIMENTAL_FEATURES, enabled) + + +def set_hide_passphrase_from_host(hide: bool) -> None: + """ + Whether we should hide the passphrase from the host. + """ + common.set_bool(_NAMESPACE, _HIDE_PASSPHRASE_FROM_HOST, hide) + + +def get_hide_passphrase_from_host() -> bool: + """ + Whether we should hide the passphrase from the host. + """ + return common.get_bool(_NAMESPACE, _HIDE_PASSPHRASE_FROM_HOST) diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index f3e10f699..773448a93 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -2104,6 +2104,7 @@ if TYPE_CHECKING: experimental_features: "bool | None" busy: "bool | None" homescreen_format: "HomescreenFormat | None" + hide_passphrase_from_host: "bool | None" def __init__( self, @@ -2147,6 +2148,7 @@ if TYPE_CHECKING: experimental_features: "bool | None" = None, busy: "bool | None" = None, homescreen_format: "HomescreenFormat | None" = None, + hide_passphrase_from_host: "bool | None" = None, ) -> None: pass @@ -2190,6 +2192,7 @@ if TYPE_CHECKING: passphrase_always_on_device: "bool | None" safety_checks: "SafetyCheckLevel | None" experimental_features: "bool | None" + hide_passphrase_from_host: "bool | None" def __init__( self, @@ -2203,6 +2206,7 @@ if TYPE_CHECKING: passphrase_always_on_device: "bool | None" = None, safety_checks: "SafetyCheckLevel | None" = None, experimental_features: "bool | None" = None, + hide_passphrase_from_host: "bool | None" = None, ) -> None: pass diff --git a/tests/device_tests/test_session_id_and_passphrase.py b/tests/device_tests/test_session_id_and_passphrase.py index 80fd6bc30..c7ab93592 100644 --- a/tests/device_tests/test_session_id_and_passphrase.py +++ b/tests/device_tests/test_session_id_and_passphrase.py @@ -18,9 +18,10 @@ import random import pytest -from trezorlib import exceptions, messages +from trezorlib import device, exceptions, messages from trezorlib.debuglink import TrezorClientDebugLink as Client -from trezorlib.messages import FailureType +from trezorlib.exceptions import TrezorFailure +from trezorlib.messages import FailureType, SafetyCheckLevel from trezorlib.tools import parse_path XPUB_PASSPHRASES = { @@ -375,6 +376,83 @@ def test_passphrase_length(client: Client): call(passphrase="A" * 49 + "ลก", expected_result=False) +@pytest.mark.skip_t1 +@pytest.mark.setup_client(passphrase=True) +def test_hide_passphrase_from_host(client: Client): + # Without safety checks, turning it on fails + with pytest.raises(TrezorFailure, match="Safety checks are strict"), client: + device.apply_settings(client, hide_passphrase_from_host=True) + + device.apply_settings(client, safety_checks=SafetyCheckLevel.PromptTemporarily) + + # Turning it on + device.apply_settings(client, hide_passphrase_from_host=True) + + passphrase = "abc" + + with client: + + def input_flow(): + yield + layout = client.debug.wait_layout() + assert ( + "Passphrase provided by host will be used but will not be displayed due to the device settings." + in layout.get_content() + ) + client.debug.press_yes() + + client.watch_layout() + client.set_input_flow(input_flow) + client.set_expected_responses( + [ + messages.PassphraseRequest, + messages.ButtonRequest, + messages.PublicKey, + ] + ) + client.use_passphrase(passphrase) + result = client.call(XPUB_REQUEST) + assert isinstance(result, messages.PublicKey) + xpub_hidden_passphrase = result.xpub + + # Turning it off + device.apply_settings(client, hide_passphrase_from_host=False) + + # Starting new session, otherwise the passphrase would be cached + _init_session(client) + + with client: + + def input_flow(): + yield + layout = client.debug.wait_layout() + assert "Next screen will show the passphrase!" in layout.get_content() + client.debug.press_yes() + + yield + layout = client.debug.wait_layout() + assert "Use this passphrase?" in layout.get_content() + assert passphrase in layout.get_content() + client.debug.press_yes() + + client.watch_layout() + client.set_input_flow(input_flow) + client.set_expected_responses( + [ + messages.PassphraseRequest, + messages.ButtonRequest, + messages.ButtonRequest, + messages.PublicKey, + ] + ) + client.use_passphrase(passphrase) + result = client.call(XPUB_REQUEST) + assert isinstance(result, messages.PublicKey) + xpub_shown_passphrase = result.xpub + + assert xpub_hidden_passphrase == xpub_shown_passphrase + + def _get_xpub_cardano(client: Client, passphrase): msg = messages.CardanoGetPublicKey( address_n=parse_path("m/44h/1815h/0h/0/0"), diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index a98f4aa24..aff224e1f 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -1705,6 +1705,7 @@ "TT_test_session.py::test_end_session_only_current": "bd83a31d0fc4c23953dfd0d138e4441984e34698ace96aad5308a4ae51b712ae", "TT_test_session.py::test_session_recycling": "07f265234a3269c8c99172e2f67abf626e17a542e6f4c00b45c5b685d0e4c240", "TT_test_session_id_and_passphrase.py::test_cardano_passphrase": "26e3b8bc6b1ba85fc89d5b237096685309a95f73350d2fca646720fb750d0e8f", +"TT_test_session_id_and_passphrase.py::test_hide_passphrase_from_host": "ccdb34cba3b759d91500b3c2b2ee5d427167d21b6aefa709b659aed298760605", "TT_test_session_id_and_passphrase.py::test_max_sessions_with_passphrases": "5c85cb246fe4bb07076ed88f426e6133368f3417472cf2dfbef2daac32a088aa", "TT_test_session_id_and_passphrase.py::test_multiple_passphrases": "12231bae2ea96b600c96ade946e5dda29d6625d128906cf14728869c1e697987", "TT_test_session_id_and_passphrase.py::test_multiple_sessions": "bd83a31d0fc4c23953dfd0d138e4441984e34698ace96aad5308a4ae51b712ae",