diff --git a/core/src/apps/base.py b/core/src/apps/base.py index d4cd2ca37..38b8394f3 100644 --- a/core/src/apps/base.py +++ b/core/src/apps/base.py @@ -95,6 +95,7 @@ def get_features() -> Features: f.safety_checks = safety_checks.read_setting() 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() return f @@ -264,4 +265,6 @@ def boot() -> None: wire.register(MessageType.DoPreauthorized, handle_DoPreauthorized) wire.register(MessageType.CancelAuthorization, handle_CancelAuthorization) + wire.experimental_enabled = storage.device.get_experimental_features() + workflow.idle_timer.set(storage.device.get_autolock_delay_ms(), lock_device) diff --git a/core/src/apps/homescreen/homescreen.py b/core/src/apps/homescreen/homescreen.py index 7bc74a0a3..1adfc26ab 100644 --- a/core/src/apps/homescreen/homescreen.py +++ b/core/src/apps/homescreen/homescreen.py @@ -25,6 +25,8 @@ class Homescreen(HomescreenBase): ui.header_warning("NEEDS BACKUP!") elif storage.device.is_initialized() and not config.has_pin(): ui.header_warning("PIN NOT SET!") + elif storage.device.get_experimental_features(): + ui.header_warning("EXPERIMENTAL MODE!") else: ui.display.bar(0, 0, ui.WIDTH, ui.HEIGHT, ui.BG) diff --git a/core/src/apps/management/apply_settings.py b/core/src/apps/management/apply_settings.py index 1999a72dd..1e83985db 100644 --- a/core/src/apps/management/apply_settings.py +++ b/core/src/apps/management/apply_settings.py @@ -45,6 +45,7 @@ async def apply_settings(ctx: wire.Context, msg: ApplySettings): and msg.display_rotation is None and msg.auto_lock_delay_ms is None and msg.safety_checks is None + and msg.experimental_features is None ): raise wire.ProcessError("No setting provided") @@ -91,6 +92,11 @@ async def apply_settings(ctx: wire.Context, msg: ApplySettings): storage.device.set_rotation(msg.display_rotation) ui.display.orientation(storage.device.get_rotation()) + if msg.experimental_features is not None: + await require_confirm_experimental_features(ctx, msg.experimental_features) + storage.device.set_experimental_features(msg.experimental_features) + wire.experimental_enabled = msg.experimental_features + return Success(message="Settings applied") @@ -182,3 +188,12 @@ async def require_confirm_safety_checks(ctx, level: EnumTypeSafetyCheckLevel) -> await require_confirm(ctx, text, ButtonRequestType.ProtectCall) else: raise ValueError # enum value out of range + + +async def require_confirm_experimental_features(ctx, enable: bool) -> None: + if enable: + text = Text("Experimental mode", ui.ICON_CONFIG) + text.normal("Enable experimental", "features?") + text.br_half() + text.bold("Only for development", "and beta testing!") + await require_confirm(ctx, text, ButtonRequestType.ProtectCall) diff --git a/core/src/protobuf.py b/core/src/protobuf.py index b55176f43..6e42aa785 100644 --- a/core/src/protobuf.py +++ b/core/src/protobuf.py @@ -142,6 +142,7 @@ class UnicodeType: class MessageType: WIRE_TYPE = 2 + UNSTABLE = False # Type id for the wire codec. # Technically, not every protobuf message has this. @@ -198,7 +199,10 @@ if False: def load_message( - reader: Reader, msg_type: Type[LoadedMessageType], field_cache: FieldCache = None + reader: Reader, + msg_type: Type[LoadedMessageType], + field_cache: FieldCache = None, + experimental_enabled: bool = True, ) -> LoadedMessageType: if field_cache is None: field_cache = {} @@ -207,6 +211,9 @@ def load_message( fields = msg_type.get_fields() field_cache[msg_type] = fields + if msg_type.UNSTABLE and not experimental_enabled: + raise ValueError # experimental messages not enabled + # we need to avoid calling __init__, which enforces required arguments msg = object.__new__(msg_type) # type: LoadedMessageType # pre-seed the object with defaults @@ -263,7 +270,9 @@ def load_message( reader.readinto(fvalue) fvalue = bytes(fvalue).decode() elif issubclass(ftype, MessageType): - fvalue = load_message(LimitedReader(reader, ivalue), ftype, field_cache) + fvalue = load_message( + LimitedReader(reader, ivalue), ftype, field_cache, experimental_enabled + ) else: raise TypeError # field type is unknown diff --git a/core/src/storage/device.py b/core/src/storage/device.py index 87c624e8b..b785115fb 100644 --- a/core/src/storage/device.py +++ b/core/src/storage/device.py @@ -36,6 +36,7 @@ _SLIP39_ITERATION_EXPONENT = const(0x11) # int _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) _DEFAULT_BACKUP_TYPE = BackupType.Bip39 @@ -304,3 +305,11 @@ def set_safety_check_level(level: StorageSafetyCheckLevel) -> None: if level not in (SAFETY_CHECK_LEVEL_STRICT, SAFETY_CHECK_LEVEL_PROMPT): raise ValueError common.set_uint8(_NAMESPACE, _SAFETY_CHECK_LEVEL, level) + + +def get_experimental_features() -> bool: + return common.get_bool(_NAMESPACE, _EXPERIMENTAL_FEATURES) + + +def set_experimental_features(enabled: bool) -> None: + common.set_true_or_delete(_NAMESPACE, _EXPERIMENTAL_FEATURES, enabled) diff --git a/core/src/trezor/wire/__init__.py b/core/src/trezor/wire/__init__.py index 9783045bb..454f9ea05 100644 --- a/core/src/trezor/wire/__init__.py +++ b/core/src/trezor/wire/__init__.py @@ -73,6 +73,9 @@ workflow_handlers = {} # type: Dict[int, Handler] # to be dynamically imported when such message arrives. workflow_packages = {} # type: Dict[int, Tuple[str, str]] +# If set to False protobuf messages marked with "unstable" option are rejected. +experimental_enabled = False # type: bool + def add(wire_type: int, pkgname: str, modname: str) -> None: """Shortcut for registering a dynamically-imported Protobuf workflow.""" @@ -123,7 +126,9 @@ def _wrap_protobuf_load( field_cache: protobuf.FieldCache = None, ) -> protobuf.LoadedMessageType: try: - return protobuf.load_message(reader, expected_type, field_cache) + return protobuf.load_message( + reader, expected_type, field_cache, experimental_enabled + ) except Exception as e: if e.args: raise DataError("Failed to decode message: {}".format(e.args[0])) diff --git a/tests/conftest.py b/tests/conftest.py index ca9f2f1f5..a8e2ed34f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,7 @@ import pytest from trezorlib import debuglink, log from trezorlib.debuglink import TrezorClientDebugLink -from trezorlib.device import wipe as wipe_device +from trezorlib.device import apply_settings, wipe as wipe_device from trezorlib.transport import enumerate_devices, get_transport from . import ui_tests @@ -132,6 +132,9 @@ def client(request): no_backup=setup_params["no_backup"], ) + if client.features.model == "T": + apply_settings(client, experimental_features=True) + if use_passphrase and isinstance(setup_params["passphrase"], str): client.use_passphrase(setup_params["passphrase"]) diff --git a/tests/device_tests/test_msg_applysettings.py b/tests/device_tests/test_msg_applysettings.py index f9ac72d3f..960d45333 100644 --- a/tests/device_tests/test_msg_applysettings.py +++ b/tests/device_tests/test_msg_applysettings.py @@ -219,3 +219,41 @@ class TestMsgApplysettings: with client: client.set_expected_responses([messages.ButtonRequest, messages.Address]) get_bad_address() + + @pytest.mark.skip_t1 + def test_experimental_features(self, client): + def experimental_call(): + btc.authorize_coinjoin( + client, + coordinator="www.example.com", + max_total_fee=10010, + fee_per_anonymity=5000000, # 0.005 % + n=parse_path("m/84'/1'/0'"), + coin_name="Testnet", + script_type=messages.InputScriptType.SPENDWITNESS, + ) + + assert client.features.experimental_features is None + + # unlock + with client: + _set_expected_responses(client) + device.apply_settings(client, label="new label") + + assert client.features.experimental_features + + with client: + client.set_expected_responses( + [messages.ButtonRequest, messages.ButtonRequest, messages.Success] + ) + experimental_call() + + with client: + client.set_expected_responses([messages.Success, messages.Features]) + device.apply_settings(client, experimental_features=False) + + assert not client.features.experimental_features + + with pytest.raises(exceptions.TrezorFailure, match="DataError"), client: + client.set_expected_responses([messages.Failure]) + experimental_call() diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index e73ebefca..bfdbfe4d0 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -38,33 +38,34 @@ "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters7-result7]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters8-result8]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", "cardano-test_sign_tx.py::test_cardano_sign_tx_failed[parameters9-result9]": "612dad8ab8762162a186ec9279d7de0bdfc589c52b4e4f4eba0545a00f21c3f0", -"test_autolock.py::test_apply_auto_lock_delay": "1997527e85989f3ca5719f93cd76bcfb8f125fb96ef3025073b13fd4de7a5fa2", -"test_autolock.py::test_apply_auto_lock_delay_valid[10]": "d03aca645ebd8a44c8a1bb24a275e31441245c3211f7c82365b2f432370b05bc", -"test_autolock.py::test_apply_auto_lock_delay_valid[123]": "7588a745923a732cd8715ab76f95b7abb9c2caf043075ec1a73e31b2453743a1", -"test_autolock.py::test_apply_auto_lock_delay_valid[3601]": "904323c7f7a27393c4a192680340d571a2a4899f0300da4d0e439f55f32768e8", -"test_autolock.py::test_apply_auto_lock_delay_valid[536870]": "ff8b1d62a60d581504779cb7bb3dcf7882e960914bac4981d52fa714880c3d1e", -"test_autolock.py::test_apply_auto_lock_delay_valid[60]": "02f813f809bec7b303998fe288f02bfa4cd1a30990c0dc071ad51ff86f2739e6", -"test_autolock.py::test_apply_auto_lock_delay_valid[7227]": "41bca947f7834baa9968cbb164b70005aee4dbf23586187ab3bef336ff42a422", -"test_autolock.py::test_autolock_cancels_ui": "eedc6196565bf6d53bc9c3f8984acc2bb91d2d71e57f3a28f8afbe35b02fb4dc", -"test_autolock.py::test_autolock_default_value": "4e564ee7f060684b4a0e8c7439fc867dc221f59c35b68f84d7a641d9e466d3e6", +"test_autolock.py::test_apply_auto_lock_delay": "38c720e0d29b7487060f2a0f8d256a5e5b4f735511e049c43f6ea62e560603ae", +"test_autolock.py::test_apply_auto_lock_delay_valid[10]": "a751228f82166c107a8e8872919e2b010ef3079763adc473066e7a3ada36f864", +"test_autolock.py::test_apply_auto_lock_delay_valid[123]": "caf130bf5fa66fa5ac17432689046c1b6cd8b6a495bac3abef3c414d89b81e3f", +"test_autolock.py::test_apply_auto_lock_delay_valid[3601]": "b2a9a7f3e50afb04fb174627a07b939284aa0acc8b3b53af56f75a35ff1b32c9", +"test_autolock.py::test_apply_auto_lock_delay_valid[536870]": "ca2b4707227cc15089f4d6ba710706d2d5c270f19a4668c09f04f175143b202e", +"test_autolock.py::test_apply_auto_lock_delay_valid[60]": "af8d06c92fc5f9aad5685bf56e59b26ec44418a6174ff73db69803f23785802a", +"test_autolock.py::test_apply_auto_lock_delay_valid[7227]": "437cc6b0780d482a835c23f0935683c40177ae4b0ff31da4fc99eba603545ffe", +"test_autolock.py::test_autolock_cancels_ui": "bb4776bfea145528544554b11bdf13ae99f63a371e8eb0885b0a9bd5b608e027", +"test_autolock.py::test_autolock_default_value": "b9f4cd94638f5f8f4c02026b0ccaee89b42406ab63ce7fcef5c9164467de939b", "test_basic.py-test_device_id_different": "bc6acd0386b9d009e6550519917d6e08632b3badde0b0cf04c95abe5f773038a", "test_basic.py-test_device_id_same": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_basic.py-test_features": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_basic.py-test_ping": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", -"test_msg_applysettings.py-test_apply_homescreen_toif": "d129829fe8c6c30125151b134826b6a11eb96cca2158c1cad951290ac314a5cc", -"test_msg_applysettings.py-test_apply_settings": "2cc8bf660f3be815d19a4bf1265936162a58386fbe632ca4be01541245b79134", -"test_msg_applysettings.py-test_apply_settings_passphrase": "5c1ed9a0be3d14475102d447da0b5d51bbb6dfaaeceff5ea9179064609db7870", +"test_msg_applysettings.py-test_apply_homescreen_toif": "408bdb69368ebdf1d299c6d43c1571f86cb1a0f1f606c5badd2f05ce7731f121", +"test_msg_applysettings.py-test_apply_settings": "8f9f6013bb8a44fda279e9c7d091328fd7ccb39222a02bee701918528355083a", +"test_msg_applysettings.py-test_apply_settings_passphrase": "40de0143b32b5d06ece43d47be27bb91499f0c2417754ddb8e9e03ff41a7f6d4", "test_msg_applysettings.py-test_apply_settings_passphrase_on_device": "3e6527e227bdde54f51bc9c417b176d0d87fdb6c40c4761368f50eb201b4beed", -"test_msg_applysettings.py-test_apply_settings_rotation": "e4a544dfc55550e340a3879d23ffab7b8ccc6362060ec941c3a7d545675d3cbd", +"test_msg_applysettings.py-test_apply_settings_rotation": "6f0fa323dd2c82d01994273c023d3ed5e43d43c9c562664a10266f4a7f7ba4cc", +"test_msg_applysettings.py-test_experimental_features": "3127d41bd8615097295b917110ece9dd364986809288c7f958ff71d52106e346", "test_msg_applysettings.py-test_safety_checks": "4eb00e8d3bce08e800f3524f9a03960865c9725a08df0ebe57853602cd84b6a5", "test_msg_authorize_coinjoin.py::test_cancel_authorization": "d8a608beb6165f5667cc44dcff6bdc17ebb4638ddd3bd09e7f0e1e75d1e21135", "test_msg_authorize_coinjoin.py::test_no_anonymity": "fd09da284b650e893990b95047b63a35b6b695fc5301d595f17a6d2cf9d90bcb", -"test_msg_authorize_coinjoin.py::test_sign_tx": "2838d4062333c241b6bbef7e680ec8a5764fe7bcaa41419e4141e146d3586a5d", +"test_msg_authorize_coinjoin.py::test_sign_tx": "5d53448397ff5cf4f951e2ac7f37c34e2ca9be99a3d3d2d31499397e10e4b157", "test_msg_authorize_coinjoin.py::test_unfair_fee": "62314e936de46a6caaf02c8eb20f6f471be6e79ca0c5450cad6f67f9cb823f2b", "test_msg_authorize_coinjoin.py::test_wrong_coordinator": "d8a608beb6165f5667cc44dcff6bdc17ebb4638ddd3bd09e7f0e1e75d1e21135", -"test_msg_backup_device.py::test_backup_bip39": "42325cccfc0bd54db180a01a076437ec981022338307339bb5e0f6463d842e23", -"test_msg_backup_device.py::test_backup_slip39_advanced": "9a01aa5ecdafa52571ed2149575f86ffea6c2984a2541ea8e5f9a7c37cf4c9fe", -"test_msg_backup_device.py::test_backup_slip39_basic": "57d841257b10c4f67cf76487c8f0bc95947a93a8e0b8c03d7a11894da3c233da", +"test_msg_backup_device.py::test_backup_bip39": "e9398e4a6ac419c06345e27654463a4576b5d3e468afd043e8e265a5c5367185", +"test_msg_backup_device.py::test_backup_slip39_advanced": "c5d95cd68074b8f46a797269c37e9cb8f7d389de50d4c36021bdc6d0a0e950ff", +"test_msg_backup_device.py::test_backup_slip39_basic": "6ff28aa2bdc45643c7205da2e0828a850053a5e59bdd322c6abdd809a5a4711c", "test_msg_backup_device.py::test_interrupt_backup_fails": "8dc5c385fec6dd871a141e2efd83f767a5f3da85b2857c8ac27e054f9fa4b384", "test_msg_backup_device.py::test_no_backup_fails": "93039a9472cfc9058563bd56e4a3dbe2e41af64744a61f6ee3255a04bd3a9366", "test_msg_backup_device.py::test_no_backup_show_entropy_fails": "14fcdd2ded299ca099a35966cc9f21204b31de8d6bab9ec91cb64537bd70440c", @@ -74,15 +75,15 @@ "test_msg_binance_sign_tx.py::test_binance_sign_message[message0-expected_response0]": "d41ee5e01a50f0f96fd7881db1750fab31cfe62c25b4eabbc092cc3daa039c7f", "test_msg_binance_sign_tx.py::test_binance_sign_message[message1-expected_response1]": "7b8bbe5ba7d7b07c95065608fb1cf9aeafcb3f9671835a6e5e5a6997ff4ff99b", "test_msg_binance_sign_tx.py::test_binance_sign_message[message2-expected_response2]": "813ad1b802dee1ace4dfa378edd840dbcea57c1a1b8eed67134def024c40a6e9", -"test_msg_change_wipe_code_t2.py::test_set_pin_to_wipe_code": "b9c44290bfef91baa76a43fcf72e41092c716c79e5c21ccc368a817e8e52739f", -"test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "d280ed129a2ea4781af9e35542aa31ecf63da75fc6812ed3bd05107809f836a4", -"test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "c128b6317db1219a88cea645aabead10eae7ba478f344af97b0b09b5cd4b1404", -"test_msg_change_wipe_code_t2.py::test_set_wipe_code_to_pin": "3c4072fa7e3def16258b2ed9c4fadf74a634b691889d78bbb771df616ea547af", -"test_msg_changepin_t2.py::test_change_failed": "6f759f27c33ba5bb43731f7532e3ec5b21a5c06c5b8a64e4d1d69e760b0003f0", -"test_msg_changepin_t2.py::test_change_pin": "5e3251031e1b221b34ad47c643c6e15af3480c22a0e44bfd89df08643267c76f", -"test_msg_changepin_t2.py::test_remove_pin": "b0eb22819b27451cf5807fd31894fc4674b9b5eb6f177eebcd3d0c33701efc4e", -"test_msg_changepin_t2.py::test_set_failed": "8c0844d3ec6e9d22a8e85e87c491e360291c26dc6ac0b6643e6292c3f6cb03a3", -"test_msg_changepin_t2.py::test_set_pin": "7957e51d5fd6ac93d4b508bcb7b170304c6548ab859cc445bb076074fd0a62d1", +"test_msg_change_wipe_code_t2.py::test_set_pin_to_wipe_code": "ff7af3e6c6e280d380200c2d919c5a474f1e875af9a5440cb8511e20b911c4ac", +"test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "9cb01ba3996dfcddfadc9e7e187455ed12ce51049ea1e0754cb7ce68bc03d410", +"test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "3f29784c14941de5fe0595780e09bd68830e3d95c981cc99e4ebd1418f875748", +"test_msg_change_wipe_code_t2.py::test_set_wipe_code_to_pin": "65d64d32e4fcc694e95e675bc2b4fbbf2d967cfc8f8fc852aeadbf58acefacf9", +"test_msg_changepin_t2.py::test_change_failed": "63c1d90ce9bd9d7fdd627ce8b70b60518b373e235dcdb6713a1b5bc020e4466d", +"test_msg_changepin_t2.py::test_change_pin": "4cdff56add70b77cd901654fcdef6098cd38214567060c870865ee697efa6df5", +"test_msg_changepin_t2.py::test_remove_pin": "64701aa15082e4d3f9639799c9d12c129dd60d1aed5f7203bfac2fd3665d1d19", +"test_msg_changepin_t2.py::test_set_failed": "6ad935b038f00177fea7e7221204ca13b188ff2eb2e699b367592d1567c5bcd6", +"test_msg_changepin_t2.py::test_set_pin": "d7a7eeff208c7080d3606a2f66170de841e429e856fff4c0e3dafa449eedd86f", "test_msg_cipherkeyvalue.py-test_decrypt": "166d85b1bf11aeaeb5b93ef5d047b6f8910c28b8fce1d853e6912d89d7bfca2f", "test_msg_cipherkeyvalue.py-test_decrypt_badlen": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_cipherkeyvalue.py-test_encrypt": "3a37e4004c87bc6df6a8fa7c93b6fe3e3524986914709fda2f9c99ba0ff69775", @@ -413,6 +414,6 @@ "test_reset_backup.py::test_skip_backup_msg[2-backup_flow_slip39_advanced]": "cd6c1248d9ee4d6416c57026a96190a84ac8608af04fd42c9c8c6b7275226aba", "test_sdcard.py::test_sd_format": "6bb7486932a5d38cdbb9b1368ee92aca3fad384115c744feadfade80c1605dd8", "test_sdcard.py::test_sd_no_format": "f47e897caee95cf98c1b4506732825f853c4b8afcdc2713e38e3b4055973c9ac", -"test_sdcard.py::test_sd_protect_unlock": "9b98ad83499e38acaa9d73b0ef3261abde6e3b4b46194c32f323f28a79705077", +"test_sdcard.py::test_sd_protect_unlock": "52a0a4b847ceab2ef5bc9b22898e14df4e4b703227f4eda9807947702da28af8", "test_u2f_counter.py::test_u2f_counter": "7d96a4d262b9d8a2c1158ac1e5f0f7b2c3ed5f2ba9d6235a014320313f9488fe" }