# This file is part of the Trezor project. # # Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 # as published by the Free Software Foundation. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the License along with this library. # If not, see . import math import time from typing import TYPE_CHECKING import pytest from trezorlib import btc, device, exceptions, messages from trezorlib.debuglink import LayoutType from trezorlib.protobuf import MessageType from trezorlib.tools import parse_path from .. import buttons, common from .. import translations as TR from ..device_tests.bitcoin.payment_req import make_coinjoin_request from ..tx_cache import TxCache from . import recovery from .common import go_next, tap_to_confirm, unlock_gesture if TYPE_CHECKING: from trezorlib.debuglink import DebugLink, LayoutContent from ..device_handler import BackgroundDeviceHandler TX_CACHE_MAINNET = TxCache("Bitcoin") TX_CACHE_TESTNET = TxCache("Testnet") FAKE_TXHASH_e5b7e2 = bytes.fromhex( "e5b7e21b5ba720e81efd6bfa9f854ababdcddc75a43bfa60bf0fe069cfd1bb8a" ) FAKE_TXHASH_f982c0 = bytes.fromhex( "f982c0a283bd65a59aa89eded9e48f2a3319cb80361dfab4cf6192a03badb60a" ) TXHASH_d5f65e = bytes.fromhex( "d5f65ee80147b4bcc70b75e4bbf2d7382021b871bd8867ef8fa525ef50864882" ) PIN4 = "1234" WORDS_20 = buttons.grid34(2, 2) CENTER_BUTTON = buttons.grid35(1, 2) def set_autolock_delay(device_handler: "BackgroundDeviceHandler", delay_ms: int): debug = device_handler.debuglink() device_handler.run(device.apply_settings, auto_lock_delay_ms=delay_ms) # type: ignore assert "PinKeyboard" in debug.read_layout().all_components() debug.input("1234") TR.assert_template( debug.read_layout().text_content(), "auto_lock__change_template", ) layout = go_next(debug) if debug.layout_type is LayoutType.Mercury: layout = tap_to_confirm(debug) assert layout.main_component() == "Homescreen" assert device_handler.result() == "Settings applied" @pytest.mark.setup_client(pin=PIN4) def test_autolock_interrupts_signing(device_handler: "BackgroundDeviceHandler"): """Autolock will lock the device that is waiting for the user to confirm transaction.""" set_autolock_delay(device_handler, 10_000) debug = device_handler.debuglink() # try to sign a transaction inp1 = messages.TxInputType( address_n=parse_path("44h/0h/0h/0/0"), amount=390000, prev_hash=TXHASH_d5f65e, prev_index=0, ) out1 = messages.TxOutputType( address="1MJ2tj2ThBE62zXbBYA5ZaN3fdve5CPAz1", amount=390000 - 10000, script_type=messages.OutputScriptType.PAYTOADDRESS, ) device_handler.run(btc.sign_tx, "Bitcoin", [inp1], [out1], prev_txes=TX_CACHE_MAINNET) # type: ignore assert ( "1MJ2tj2ThBE62zXbBYA5ZaN3fdve5CPAz1" in debug.read_layout().text_content().replace(" ", "") ) if debug.layout_type is LayoutType.TT: debug.click(buttons.OK) layout = debug.click(buttons.OK) TR.assert_in(layout.text_content(), "send__total_amount") assert "0.0039 BTC" in layout.text_content() elif debug.layout_type is LayoutType.Mercury: debug.swipe_up() layout = debug.swipe_up() TR.assert_in(layout.text_content(), "send__total_amount") assert "0.0039 BTC" in layout.text_content() elif debug.layout_type is LayoutType.TR: debug.press_right() layout = debug.press_right() TR.assert_in(layout.text_content(), "send__total_amount") assert "0.0039 BTC" in layout.text_content() # wait for autolock to kick in time.sleep(10.1) with pytest.raises(exceptions.Cancelled): device_handler.result() @pytest.mark.setup_client(pin=PIN4) def test_autolock_does_not_interrupt_signing(device_handler: "BackgroundDeviceHandler"): """Autolock will NOT lock the device once transaction is confirmed.""" set_autolock_delay(device_handler, 10_000) debug = device_handler.debuglink() # try to sign a transaction inp1 = messages.TxInputType( address_n=parse_path("86h/0h/0h/0/0"), amount=390000, script_type=messages.InputScriptType.SPENDTAPROOT, prev_hash=TXHASH_d5f65e, prev_index=0, ) out1 = messages.TxOutputType( address="1MJ2tj2ThBE62zXbBYA5ZaN3fdve5CPAz1", amount=390000 - 10000, script_type=messages.OutputScriptType.PAYTOADDRESS, ) device_handler.run( btc.sign_tx, "Bitcoin", [inp1], [out1], prev_txes=TX_CACHE_MAINNET ) assert ( "1MJ2tj2ThBE62zXbBYA5ZaN3fdve5CPAz1" in debug.read_layout().text_content().replace(" ", "") ) if debug.layout_type is LayoutType.TT: debug.click(buttons.OK) layout = debug.click(buttons.OK) TR.assert_in(layout.text_content(), "send__total_amount") assert "0.0039 BTC" in layout.text_content() elif debug.layout_type is LayoutType.Mercury: debug.swipe_up() layout = debug.swipe_up() TR.assert_in(layout.text_content(), "send__total_amount") assert "0.0039 BTC" in layout.text_content() debug.swipe_up() elif debug.layout_type is LayoutType.TR: debug.press_right() layout = debug.press_right() TR.assert_in(layout.text_content(), "send__total_amount") assert "0.0039 BTC" in layout.text_content() def sleepy_filter(msg: MessageType) -> MessageType: time.sleep(10.1) device_handler.client.set_filter(messages.TxAck, None) return msg with device_handler.client: device_handler.client.set_filter(messages.TxAck, sleepy_filter) # confirm transaction # In all cases we set wait=False to avoid waiting for the screen and triggering # the layout deadlock detection. In reality there is no deadlock but the # `sleepy_filter` delays the response by 10 secs while the layout deadlock # timeout is 3. In this test we don't need the result of the input event so # waiting for it is not necessary. if debug.layout_type is LayoutType.TT: debug.click(buttons.OK, wait=False) elif debug.layout_type is LayoutType.Mercury: debug.click(buttons.TAP_TO_CONFIRM, wait=False) elif debug.layout_type is LayoutType.TR: debug.press_middle(wait=False) signatures, tx = device_handler.result() assert len(signatures) == 1 assert tx assert device_handler.features().unlocked is False @pytest.mark.setup_client(pin=PIN4, passphrase=True) def test_autolock_passphrase_keyboard(device_handler: "BackgroundDeviceHandler"): set_autolock_delay(device_handler, 10_000) debug = device_handler.debuglink() # get address device_handler.run(common.get_test_address) # type: ignore assert "PassphraseKeyboard" in debug.read_layout().all_components() if debug.layout_type is LayoutType.TR: # Going into the selected character category debug.press_middle() # enter passphrase - slowly # keep clicking for long enough to trigger the autolock if it incorrectly ignored key presses for _ in range(math.ceil(11 / 1.5)): if debug.layout_type is LayoutType.TT: # click at "j" debug.click(CENTER_BUTTON) elif debug.layout_type is LayoutType.Mercury: # click at "j" debug.click((20, 120)) elif debug.layout_type is LayoutType.TR: # just go right # NOTE: because of passphrase randomization it would be a pain to input # a specific passphrase, which is not in scope for this test. debug.press_right() time.sleep(1.5) # Send the passphrase to the client (TT has it clicked already, TR needs to input it) if debug.layout_type is LayoutType.TT: debug.click(buttons.OK) elif debug.layout_type is LayoutType.Mercury: debug.click(buttons.CORNER_BUTTON) elif debug.layout_type is LayoutType.TR: debug.input("j" * 8) # address corresponding to "jjjjjjjj" passphrase assert device_handler.result() == "mnF4yRWJXmzRB6EuBzuVigqeqTqirQupxJ" @pytest.mark.setup_client(pin=PIN4, passphrase=True) def test_autolock_interrupts_passphrase(device_handler: "BackgroundDeviceHandler"): set_autolock_delay(device_handler, 10_000) debug = device_handler.debuglink() # get address device_handler.run(common.get_test_address) # type: ignore assert "PassphraseKeyboard" in debug.read_layout().all_components() if debug.layout_type is LayoutType.TR: # Going into the selected character category debug.press_middle() # enter passphrase - slowly # autolock must activate even if we pressed some buttons for _ in range(math.ceil(6 / 1.5)): if debug.layout_type in (LayoutType.TT, LayoutType.Mercury): debug.click(CENTER_BUTTON) elif debug.layout_type is LayoutType.TR: debug.press_middle() time.sleep(1.5) # wait for autolock to kick in time.sleep(10.1) assert debug.read_layout().main_component() == "Lockscreen" with pytest.raises(exceptions.Cancelled): device_handler.result() def unlock_dry_run(debug: "DebugLink") -> "LayoutContent": TR.assert_in(debug.read_layout().text_content(), "recovery__check_dry_run") layout = go_next(debug) assert "PinKeyboard" in layout.all_components() layout = debug.input(PIN4) assert layout is not None return layout @pytest.mark.setup_client(pin=PIN4) def test_dryrun_locks_at_number_of_words(device_handler: "BackgroundDeviceHandler"): set_autolock_delay(device_handler, 10_000) debug = device_handler.debuglink() device_handler.run(device.recover, dry_run=True) # type: ignore layout = unlock_dry_run(debug) TR.assert_in(debug.read_layout().text_content(), "recovery__num_of_words") if debug.layout_type is LayoutType.TR: debug.press_right() # wait for autolock to trigger time.sleep(10.1) assert debug.read_layout().main_component() == "Lockscreen" with pytest.raises(exceptions.Cancelled): device_handler.result() # unlock # lockscreen triggered automatically # debug.wait_layout() layout = unlock_gesture(debug) assert "PinKeyboard" in layout.all_components() layout = debug.input(PIN4) assert layout is not None # we are back at homescreen TR.assert_in(debug.read_layout().text_content(), "recovery__num_of_words") @pytest.mark.setup_client(pin=PIN4) def test_dryrun_locks_at_word_entry(device_handler: "BackgroundDeviceHandler"): set_autolock_delay(device_handler, 10_000) debug = device_handler.debuglink() device_handler.run(device.recover, dry_run=True) # type: ignore unlock_dry_run(debug) # select 20 words recovery.select_number_of_words(debug, 20) if debug.layout_type in (LayoutType.TT, LayoutType.Mercury): layout = go_next(debug) assert layout.main_component() == "MnemonicKeyboard" elif debug.layout_type is LayoutType.TR: layout = debug.press_right() assert "MnemonicKeyboard" in layout.all_components() # make sure keyboard locks time.sleep(10.1) assert debug.read_layout().main_component() == "Lockscreen" with pytest.raises(exceptions.Cancelled): device_handler.result() @pytest.mark.setup_client(pin=PIN4) def test_dryrun_enter_word_slowly(device_handler: "BackgroundDeviceHandler"): set_autolock_delay(device_handler, 10_000) debug = device_handler.debuglink() device_handler.run(device.recover, dry_run=True) # type: ignore unlock_dry_run(debug) # select 20 words recovery.select_number_of_words(debug, 20) if debug.layout_type is LayoutType.TT: layout = debug.click(buttons.OK) assert layout.main_component() == "MnemonicKeyboard" # type the word OCEAN slowly for coords in buttons.type_word("ocea", is_slip39=True): time.sleep(9) debug.click(coords) layout = debug.click(buttons.CONFIRM_WORD) # should not have locked, even though we took 9 seconds to type each letter assert layout.main_component() == "MnemonicKeyboard" elif debug.layout_type is LayoutType.Mercury: layout = debug.swipe_up() assert layout.main_component() == "MnemonicKeyboard" # type the word OCEAN slowly for coords in buttons.type_word("ocea", is_slip39=True): time.sleep(9) debug.click(coords) layout = debug.click(buttons.CONFIRM_WORD) # should not have locked, even though we took 9 seconds to type each letter assert layout.main_component() == "MnemonicKeyboard" elif debug.layout_type is LayoutType.TR: layout = debug.press_right() assert "MnemonicKeyboard" in layout.all_components() # pressing middle button three times for _ in range(3): time.sleep(9) debug.press_middle() layout = debug.read_layout() # should not have locked, even though we took 9 seconds to type each letter assert "MnemonicKeyboard" in layout.all_components() else: raise ValueError(f"Unsupported layout type: {debug.layout_type}") with pytest.raises(exceptions.Cancelled): device_handler.result() @pytest.mark.setup_client(pin=PIN4) def test_autolock_does_not_interrupt_preauthorized( device_handler: "BackgroundDeviceHandler", ): # NOTE: FAKE input tx # NOTE: mostly copy-pasted from test_authorize_coinjoin.py::test_sign_tx set_autolock_delay(device_handler, 10_000) debug = device_handler.debuglink() device_handler.run( btc.authorize_coinjoin, coordinator="www.example.com", max_rounds=2, max_coordinator_fee_rate=500_000, # 0.5 % max_fee_per_kvbyte=3500, n=parse_path("m/10025h/1h/0h/1h"), coin_name="Testnet", script_type=messages.InputScriptType.SPENDTAPROOT, ) debug.press_yes() device_handler.result() inputs = [ messages.TxInputType( # seed "alcohol woman abuse must during monitor noble actual mixed trade anger aisle" # m/10025h/1h/0h/1h/0/0 # tb1pkw382r3plt8vx6e22mtkejnqrxl4z7jugh3w4rjmfmgezzg0xqpsdaww8z amount=100_000, prev_hash=FAKE_TXHASH_e5b7e2, prev_index=0, script_type=messages.InputScriptType.EXTERNAL, script_pubkey=bytes.fromhex( "5120b3a2750e21facec36b2a56d76cca6019bf517a5c45e2ea8e5b4ed191090f3003" ), ownership_proof=bytearray.fromhex( "534c001901019cf1b0ad730100bd7a69e987d55348bb798e2b2096a6a5713e9517655bd2021300014052d479f48d34f1ca6872d4571413660040c3e98841ab23a2c5c1f37399b71bfa6f56364b79717ee90552076a872da68129694e1b4fb0e0651373dcf56db123c5" ), commitment_data=b"\x0fwww.example.com" + (1).to_bytes(32, "big"), ), messages.TxInputType( address_n=parse_path("m/10025h/1h/0h/1h/1/0"), amount=7_289_000, prev_hash=FAKE_TXHASH_f982c0, prev_index=1, script_type=messages.InputScriptType.SPENDTAPROOT, ), ] input_script_pubkeys = [ bytes.fromhex( "5120b3a2750e21facec36b2a56d76cca6019bf517a5c45e2ea8e5b4ed191090f3003" ), bytes.fromhex( "51202f436892d90fb2665519efa3d9f0f5182859124f179486862c2cd7a78ea9ac19" ), ] outputs = [ # Other's coinjoined output. messages.TxOutputType( # seed "alcohol woman abuse must during monitor noble actual mixed trade anger aisle" # m/10025h/1h/0h/1h/1/0 address="tb1pupzczx9cpgyqgtvycncr2mvxscl790luqd8g88qkdt2w3kn7ymhsrdueu2", amount=50_000, script_type=messages.OutputScriptType.PAYTOADDRESS, ), # Our coinjoined output. messages.TxOutputType( # tb1phkcspf88hge86djxgtwx2wu7ddghsw77d6sd7txtcxncu0xpx22shcydyf address_n=parse_path("m/10025h/1h/0h/1h/1/1"), amount=50_000, script_type=messages.OutputScriptType.PAYTOTAPROOT, ), # Our change output. messages.TxOutputType( # tb1pchruvduckkwuzm5hmytqz85emften5dnmkqu9uhfxwfywaqhuu0qjggqyp address_n=parse_path("m/10025h/1h/0h/1h/1/2"), amount=7_289_000 - 50_000 - 36_445 - 490, script_type=messages.OutputScriptType.PAYTOTAPROOT, ), # Other's change output. messages.TxOutputType( # seed "alcohol woman abuse must during monitor noble actual mixed trade anger aisle" # m/10025h/1h/0h/1h/1/1 address="tb1pvt7lzserh8xd5m6mq0zu9s5wxkpe5wgf5ts56v44jhrr6578hz8saxup5m", amount=100_000 - 50_000 - 500 - 490, script_type=messages.OutputScriptType.PAYTOADDRESS, ), # Coordinator's output. messages.TxOutputType( address="mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q", amount=36_945, script_type=messages.OutputScriptType.PAYTOADDRESS, ), ] output_script_pubkeys = [ bytes.fromhex( "5120e0458118b80a08042d84c4f0356d86863fe2bffc034e839c166ad4e8da7e26ef" ), bytes.fromhex( "5120bdb100a4e7ba327d364642dc653b9e6b51783bde6ea0df2ccbc1a78e3cc13295" ), bytes.fromhex( "5120c5c7c63798b59dc16e97d916011e99da5799d1b3dd81c2f2e93392477417e71e" ), bytes.fromhex( "512062fdf14323b9ccda6f5b03c5c2c28e35839a3909a2e14d32b595c63d53c7b88f" ), bytes.fromhex("76a914a579388225827d9f2fe9014add644487808c695d88ac"), ] coinjoin_req = make_coinjoin_request( "www.example.com", inputs, input_script_pubkeys, outputs, output_script_pubkeys, no_fee_indices=[], ) def sleepy_filter(msg: MessageType) -> MessageType: time.sleep(10.1) device_handler.client.set_filter(messages.SignTx, None) return msg with device_handler.client: # Start DoPreauthorized flow when device is unlocked. Wait 10s before # delivering SignTx, by that time autolock timer should have fired. device_handler.client.set_filter(messages.SignTx, sleepy_filter) device_handler.run( btc.sign_tx, "Testnet", inputs, outputs, prev_txes=TX_CACHE_TESTNET, coinjoin_request=coinjoin_req, preauthorized=True, serialize=False, ) signatures, _ = device_handler.result() assert len(signatures) == 2 assert signatures[0] is None assert ( signatures[1].hex() == "c017fce789fa8db54a2ae032012d2dd6d7c76cc1c1a6f00e29b86acbf93022da8aa559009a574792c7b09b2535d288d6e03c6ed169902ed8c4c97626a83fbc11" ) assert device_handler.features().unlocked is False