From a8f2f7b1e3a4a0bbba5e9da648d442d331abf91a Mon Sep 17 00:00:00 2001 From: ciny Date: Mon, 23 Sep 2019 14:49:54 +0200 Subject: [PATCH] tests: expand backup device tests --- tests/device_tests/test_msg_backup_device.py | 212 +++++++++++++++ .../test_msg_resetdevice_bip39_t2.py | 5 + .../test_msg_resetdevice_slip39_advanced.py | 5 + .../test_msg_resetdevice_slip39_basic.py | 5 + tests/device_tests/test_reset_backup_bip39.py | 167 ++++++++++++ .../test_reset_backup_slip39_advanced.py | 250 ++++++++++++++++++ .../test_reset_backup_slip39_basic.py | 193 ++++++++++++++ 7 files changed, 837 insertions(+) create mode 100644 tests/device_tests/test_reset_backup_bip39.py create mode 100644 tests/device_tests/test_reset_backup_slip39_advanced.py create mode 100644 tests/device_tests/test_reset_backup_slip39_basic.py diff --git a/tests/device_tests/test_msg_backup_device.py b/tests/device_tests/test_msg_backup_device.py index ae85ebab3e..c0ad6585c6 100644 --- a/tests/device_tests/test_msg_backup_device.py +++ b/tests/device_tests/test_msg_backup_device.py @@ -19,10 +19,12 @@ import pytest import shamir_mnemonic as shamir from trezorlib import device, messages +from trezorlib.exceptions import TrezorFailure from trezorlib.messages import ButtonRequestType as B from ..common import ( MNEMONIC12, + MNEMONIC_SLIP39_ADVANCED_20, MNEMONIC_SLIP39_BASIC_20_3of6, click_through, read_and_confirm_mnemonic, @@ -61,7 +63,11 @@ def test_backup_bip39(client): assert mnemonic == MNEMONIC12 client.init_device() + assert client.features.initialized is True assert client.features.needs_backup is False + assert client.features.unfinished_backup is False + assert client.features.no_backup is False + assert client.features.backup_type is messages.BackupType.Bip39 @pytest.mark.skip_t1 @@ -118,8 +124,214 @@ def test_backup_slip39_basic(client): device.backup(client) client.init_device() + assert client.features.initialized is True assert client.features.needs_backup is False + assert client.features.unfinished_backup is False + assert client.features.no_backup is False + assert client.features.backup_type is messages.BackupType.Slip39_Basic expected_ms = shamir.combine_mnemonics(MNEMONIC_SLIP39_BASIC_20_3of6) actual_ms = shamir.combine_mnemonics(mnemonics[:3]) assert expected_ms == actual_ms + + +@pytest.mark.skip_t1 +@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_ADVANCED_20) +def test_backup_slip39_advanced(client): + assert client.features.needs_backup is True + mnemonics = [] + + def input_flow(): + # 1. Checklist + # 2. Set and confirm group count + # 3. Checklist + # 4. Set and confirm group threshold + # 5. Checklist + # 6-15: for each of 5 groups: + # 1. Set & Confirm number of shares + # 2. Set & confirm share threshold value + # 16. Confirm show seeds + yield from click_through(client.debug, screens=16, code=B.ResetDevice) + + # Mnemonic phrases + for _ in range(5): + for _ in range(5): + yield # Phrase screen + mnemonic = read_and_confirm_mnemonic(client.debug, words=20) + mnemonics.append(mnemonic) + yield # Confirm continue to next + client.debug.press_yes() + + # Confirm backup + yield + client.debug.press_yes() + + with client: + client.set_input_flow(input_flow) + client.set_expected_responses( + [ + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), # group #1 counts + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), # group #2 counts + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), # group #3 counts + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), # group #4 counts + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), # group #5 counts + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), # show seeds + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), # show seeds ends here + messages.ButtonRequest(code=B.Success), + messages.Success(), + ] + ) + device.backup(client) + + client.init_device() + assert client.features.initialized is True + assert client.features.needs_backup is False + assert client.features.unfinished_backup is False + assert client.features.no_backup is False + assert client.features.backup_type is messages.BackupType.Slip39_Advanced + + expected_ms = shamir.combine_mnemonics(MNEMONIC_SLIP39_ADVANCED_20) + actual_ms = shamir.combine_mnemonics( + mnemonics[:3] + mnemonics[5:8] + mnemonics[10:13] + ) + assert expected_ms == actual_ms + + +# we only test this with bip39 because the code path is always the same +@pytest.mark.skip_t1 +@pytest.mark.setup_client(uninitialized=True) +def test_no_backup_fails(client): + device.reset( + client, + display_random=False, + strength=128, + passphrase_protection=False, + pin_protection=False, + label="test", + language="english", + no_backup=True, + ) + + assert client.features.initialized is True + assert client.features.no_backup is True + assert client.features.needs_backup is False + + # backup attempt should fail because no_backup=True + with pytest.raises(TrezorFailure, match="ProcessError: Seed already backed up"): + device.backup(client) + + +# we only test this with bip39 because the code path is always the same +@pytest.mark.skip_t1 +@pytest.mark.setup_client(uninitialized=True) +def test_interupt_backup_fails(client): + device.reset( + client, + display_random=False, + strength=128, + passphrase_protection=False, + pin_protection=False, + label="test", + language="english", + skip_backup=True, + ) + + assert client.features.initialized is True + assert client.features.needs_backup is True + assert client.features.unfinished_backup is False + assert client.features.no_backup is False + + # start backup + client.call_raw(messages.BackupDevice()) + + # interupt backup by sending initialize + client.init_device() + + # check that device state is as expected + assert client.features.initialized is True + assert client.features.needs_backup is False + assert client.features.unfinished_backup is True + assert client.features.no_backup is False + + # Second attempt at backup should fail + with pytest.raises(TrezorFailure, match="ProcessError: Seed already backed up"): + device.backup(client) + + +# we only test this with bip39 because the code path is always the same +@pytest.mark.skip_t1 +@pytest.mark.setup_client(uninitialized=True) +def test_no_backup_show_entropy_fails(client): + with pytest.raises( + TrezorFailure, + match="ProcessError: Can't show internal entropy when backup is skipped", + ): + device.reset( + client, + display_random=True, + strength=128, + passphrase_protection=False, + pin_protection=False, + label="test", + language="english", + no_backup=True, + ) diff --git a/tests/device_tests/test_msg_resetdevice_bip39_t2.py b/tests/device_tests/test_msg_resetdevice_bip39_t2.py index 862ba460cd..11371fb617 100644 --- a/tests/device_tests/test_msg_resetdevice_bip39_t2.py +++ b/tests/device_tests/test_msg_resetdevice_bip39_t2.py @@ -20,6 +20,7 @@ import pytest from mnemonic import Mnemonic from trezorlib import device, messages as proto +from trezorlib.exceptions import TrezorFailure from trezorlib.messages import ButtonRequestType as B from ..common import ( @@ -105,6 +106,10 @@ class TestMsgResetDeviceT2: assert resp.passphrase_protection is False assert resp.backup_type is proto.BackupType.Bip39 + # backup attempt fails because backup was done in reset + with pytest.raises(TrezorFailure, match="ProcessError: Seed already backed up"): + device.backup(client) + @pytest.mark.setup_client(uninitialized=True) def test_reset_device_pin(self, client): mnemonic = None diff --git a/tests/device_tests/test_msg_resetdevice_slip39_advanced.py b/tests/device_tests/test_msg_resetdevice_slip39_advanced.py index 50d386285c..f8ae17bd2a 100644 --- a/tests/device_tests/test_msg_resetdevice_slip39_advanced.py +++ b/tests/device_tests/test_msg_resetdevice_slip39_advanced.py @@ -20,6 +20,7 @@ import pytest import shamir_mnemonic as shamir from trezorlib import device, messages as proto +from trezorlib.exceptions import TrezorFailure from trezorlib.messages import BackupType, ButtonRequestType as B from ..common import click_through, generate_entropy, read_and_confirm_mnemonic @@ -176,6 +177,10 @@ class TestMsgResetDeviceT2: assert client.features.passphrase_protection is False assert client.features.backup_type is BackupType.Slip39_Advanced + # backup attempt fails because backup was done in reset + with pytest.raises(TrezorFailure, match="ProcessError: Seed already backed up"): + device.backup(client) + def validate_mnemonics(mnemonics, threshold, expected_ems): # 3of5 shares 3of5 groups diff --git a/tests/device_tests/test_msg_resetdevice_slip39_basic.py b/tests/device_tests/test_msg_resetdevice_slip39_basic.py index 664fad9f74..94f24b508b 100644 --- a/tests/device_tests/test_msg_resetdevice_slip39_basic.py +++ b/tests/device_tests/test_msg_resetdevice_slip39_basic.py @@ -22,6 +22,7 @@ import shamir_mnemonic as shamir from shamir_mnemonic import MnemonicError from trezorlib import device, messages as proto +from trezorlib.exceptions import TrezorFailure from trezorlib.messages import BackupType, ButtonRequestType as B from ..common import click_through, generate_entropy, read_and_confirm_mnemonic @@ -123,6 +124,10 @@ class TestMsgResetDeviceT2: assert client.features.passphrase_protection is False assert client.features.backup_type is BackupType.Slip39_Basic + # backup attempt fails because backup was done in reset + with pytest.raises(TrezorFailure, match="ProcessError: Seed already backed up"): + device.backup(client) + def validate_mnemonics(mnemonics, threshold, expected_ems): # We expect these combinations to recreate the secret properly diff --git a/tests/device_tests/test_reset_backup_bip39.py b/tests/device_tests/test_reset_backup_bip39.py new file mode 100644 index 0000000000..8aeea3b35e --- /dev/null +++ b/tests/device_tests/test_reset_backup_bip39.py @@ -0,0 +1,167 @@ +# 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 . + +from unittest import mock + +import pytest +from mnemonic import Mnemonic + +from trezorlib import device, messages +from trezorlib.messages import BackupType, ButtonRequestType as B + +from ..common import click_through, generate_entropy, read_and_confirm_mnemonic + +EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2 +OS_URANDOM = mock.Mock(return_value=EXTERNAL_ENTROPY) + + +@pytest.mark.skip_t1 +@pytest.mark.setup_client(uninitialized=True) +def test_skip_backup_msg(client): + + with mock.patch("os.urandom", OS_URANDOM), client: + device.reset( + client, + display_random=False, + strength=128, + skip_backup=True, + passphrase_protection=False, + pin_protection=False, + label="test", + language="english", + backup_type=BackupType.Bip39, + ) + + assert client.features.initialized is True + assert client.features.needs_backup is True + assert client.features.unfinished_backup is False + assert client.features.no_backup is False + assert client.features.backup_type is BackupType.Bip39 + + mnemonic = backup_flow(client) + + client.init_device() + assert client.features.initialized is True + assert client.features.needs_backup is False + assert client.features.unfinished_backup is False + assert client.features.backup_type is BackupType.Bip39 + + assert mnemonic is not None + + # generate mnemonic locally + internal_entropy = client.debug.state().reset_entropy + entropy = generate_entropy(128, internal_entropy, EXTERNAL_ENTROPY) + expected_mnemonic = Mnemonic("english").to_mnemonic(entropy) + + # Compare that device generated proper mnemonic for given entropies + assert mnemonic == expected_mnemonic + + +@pytest.mark.skip_t1 +@pytest.mark.setup_client(uninitialized=True) +def test_skip_backup_manual(client): + def reset_skip_input_flow(): + yield # Confirm Recovery + client.debug.press_yes() + + yield # Skip Backup + client.debug.press_no() + + yield # Confirm skip backup + client.debug.press_no() + + with mock.patch("os.urandom", OS_URANDOM), client: + client.set_input_flow(reset_skip_input_flow) + client.set_expected_responses( + [ + messages.ButtonRequest(code=B.ResetDevice), + messages.EntropyRequest(), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.Success(), + messages.Features(), + ] + ) + device.reset( + client, + pin_protection=False, + passphrase_protection=False, + backup_type=BackupType.Bip39, + ) + + assert client.features.initialized is True + assert client.features.needs_backup is True + assert client.features.unfinished_backup is False + assert client.features.no_backup is False + assert client.features.backup_type is BackupType.Bip39 + + mnemonic = backup_flow(client) + + client.init_device() + assert client.features.initialized is True + assert client.features.needs_backup is False + assert client.features.unfinished_backup is False + assert client.features.backup_type is BackupType.Bip39 + + assert mnemonic is not None + + # generate mnemonic locally + internal_entropy = client.debug.state().reset_entropy + entropy = generate_entropy(128, internal_entropy, EXTERNAL_ENTROPY) + expected_mnemonic = Mnemonic("english").to_mnemonic(entropy) + + # Compare that device generated proper mnemonic for given entropies + assert mnemonic == expected_mnemonic + + +def backup_flow(client): + mnemonic = None + + def input_flow(): + nonlocal mnemonic + + # 1. Confirm Reset + yield from click_through(client.debug, screens=1, code=B.ResetDevice) + + # mnemonic phrases + btn_code = yield + assert btn_code == B.ResetDevice + mnemonic = read_and_confirm_mnemonic(client.debug, words=12) + + # confirm recovery seed check + btn_code = yield + assert btn_code == B.Success + client.debug.press_yes() + + # confirm success + btn_code = yield + assert btn_code == B.Success + client.debug.press_yes() + + with client: + client.set_expected_responses( + [ + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.Success), + messages.Success(), + ] + ) + client.set_input_flow(input_flow) + device.backup(client) + + return mnemonic diff --git a/tests/device_tests/test_reset_backup_slip39_advanced.py b/tests/device_tests/test_reset_backup_slip39_advanced.py new file mode 100644 index 0000000000..a4d0097f63 --- /dev/null +++ b/tests/device_tests/test_reset_backup_slip39_advanced.py @@ -0,0 +1,250 @@ +# 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 . + +from unittest import mock + +import pytest +import shamir_mnemonic as shamir + +from trezorlib import device, messages +from trezorlib.messages import BackupType, ButtonRequestType as B + +from ..common import click_through, generate_entropy, read_and_confirm_mnemonic + +EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2 +OS_URANDOM = mock.Mock(return_value=EXTERNAL_ENTROPY) + + +@pytest.mark.skip_t1 +@pytest.mark.setup_client(uninitialized=True) +def test_skip_backup_msg(client): + with mock.patch("os.urandom", OS_URANDOM), client: + device.reset( + client, + display_random=False, + strength=128, + skip_backup=True, + passphrase_protection=False, + pin_protection=False, + label="test", + language="english", + backup_type=BackupType.Slip39_Advanced, + ) + + assert client.features.initialized is True + assert client.features.needs_backup is True + assert client.features.unfinished_backup is False + assert client.features.no_backup is False + assert client.features.backup_type is BackupType.Slip39_Advanced + + # generate secret locally + internal_entropy = client.debug.state().reset_entropy + secret = generate_entropy(128, internal_entropy, EXTERNAL_ENTROPY) + + mnemonics = backup_flow(client) + + client.init_device() + assert client.features.initialized is True + assert client.features.needs_backup is False + assert client.features.unfinished_backup is False + assert client.features.backup_type is BackupType.Slip39_Advanced + + assert mnemonics is not [] + + validate_mnemonics(mnemonics, secret) + + +@pytest.mark.skip_t1 +@pytest.mark.setup_client(uninitialized=True) +def test_skip_backup_manual(client): + def reset_skip_input_flow(): + yield # Confirm Recovery + client.debug.press_yes() + + yield # Skip Backup + client.debug.press_no() + + yield # Confirm skip backup + client.debug.press_no() + + with mock.patch("os.urandom", OS_URANDOM), client: + client.set_input_flow(reset_skip_input_flow) + client.set_expected_responses( + [ + messages.ButtonRequest(code=B.ResetDevice), + messages.EntropyRequest(), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.Success(), + messages.Features(), + ] + ) + device.reset( + client, + strength=128, + pin_protection=False, + passphrase_protection=False, + backup_type=BackupType.Slip39_Advanced, + ) + + assert client.features.initialized is True + assert client.features.needs_backup is True + assert client.features.unfinished_backup is False + assert client.features.no_backup is False + assert client.features.backup_type is BackupType.Slip39_Advanced + + # generate secret locally + internal_entropy = client.debug.state().reset_entropy + secret = generate_entropy(128, internal_entropy, EXTERNAL_ENTROPY) + + mnemonics = backup_flow(client) + + client.init_device() + assert client.features.initialized is True + assert client.features.needs_backup is False + assert client.features.unfinished_backup is False + assert client.features.backup_type is BackupType.Slip39_Advanced + + assert mnemonics is not [] + + # generate secret locally + internal_entropy = client.debug.state().reset_entropy + secret = generate_entropy(128, internal_entropy, EXTERNAL_ENTROPY) + + validate_mnemonics(mnemonics, secret) + + +def backup_flow(client): + all_mnemonics = [] + + def input_flow(): + # 1. Confirm Reset + # 2. shares info + # 3. Set & Confirm number of groups + # 4. threshold info + # 5. Set & confirm group threshold value + # 6-15: for each of 5 groups: + # 1. Set & Confirm number of shares + # 2. Set & confirm share threshold value + # 16. Confirm show seeds + yield from click_through(client.debug, screens=16, code=B.ResetDevice) + + # show & confirm shares for all groups + for g in range(5): + for h in range(5): + # mnemonic phrases + btn_code = yield + assert btn_code == B.ResetDevice + mnemonic = read_and_confirm_mnemonic(client.debug, words=20) + all_mnemonics.append(mnemonic) + + # Confirm continue to next share + btn_code = yield + assert btn_code == B.Success + client.debug.press_yes() + + # safety warning + btn_code = yield + assert btn_code == B.Success + client.debug.press_yes() + + with client: + client.set_expected_responses( + [ + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), # group #1 counts + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), # group #2 counts + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), # group #3 counts + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), # group #4 counts + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), # group #5 counts + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), # show seeds + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), # show seeds ends here + messages.ButtonRequest(code=B.Success), + messages.Success(), + ] + ) + client.set_input_flow(input_flow) + + device.backup(client) + + return all_mnemonics + + +def validate_mnemonics(mnemonics, expected_ems): + # 3of5 shares 3of5 groups + test_combination = mnemonics[0:3] + mnemonics[5:8] + mnemonics[10:13] + ms = shamir.combine_mnemonics(test_combination) + identifier, iteration_exponent, _, _, _ = shamir._decode_mnemonics(test_combination) + ems = shamir._encrypt(ms, b"", iteration_exponent, identifier) + assert ems == expected_ems diff --git a/tests/device_tests/test_reset_backup_slip39_basic.py b/tests/device_tests/test_reset_backup_slip39_basic.py new file mode 100644 index 0000000000..a5aec2cd2e --- /dev/null +++ b/tests/device_tests/test_reset_backup_slip39_basic.py @@ -0,0 +1,193 @@ +# 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 . + +from itertools import combinations +from unittest import mock + +import pytest +import shamir_mnemonic as shamir +from shamir_mnemonic import MnemonicError + +from trezorlib import device, messages +from trezorlib.messages import BackupType, ButtonRequestType as B + +from ..common import click_through, generate_entropy, read_and_confirm_mnemonic + +EXTERNAL_ENTROPY = b"zlutoucky kun upel divoke ody" * 2 +OS_URANDOM = mock.Mock(return_value=EXTERNAL_ENTROPY) + + +@pytest.mark.skip_t1 +@pytest.mark.setup_client(uninitialized=True) +def test_skip_backup_msg(client): + with mock.patch("os.urandom", OS_URANDOM), client: + device.reset( + client, + display_random=False, + strength=128, + skip_backup=True, + passphrase_protection=False, + pin_protection=False, + label="test", + language="english", + backup_type=BackupType.Slip39_Basic, + ) + + assert client.features.initialized is True + assert client.features.needs_backup is True + assert client.features.unfinished_backup is False + assert client.features.no_backup is False + assert client.features.backup_type is BackupType.Slip39_Basic + + # generate secret locally + internal_entropy = client.debug.state().reset_entropy + secret = generate_entropy(128, internal_entropy, EXTERNAL_ENTROPY) + + mnemonics = backup_flow(client) + + client.init_device() + assert client.features.initialized is True + assert client.features.needs_backup is False + assert client.features.unfinished_backup is False + assert client.features.backup_type is BackupType.Slip39_Basic + + assert mnemonics is not [] + + validate_mnemonics(mnemonics, 3, secret) + + +@pytest.mark.skip_t1 +@pytest.mark.setup_client(uninitialized=True) +def test_skip_backup_manual(client): + def reset_skip_input_flow(): + yield # Confirm Recovery + client.debug.press_yes() + + yield # Skip Backup + client.debug.press_no() + + yield # Confirm skip backup + client.debug.press_no() + + with mock.patch("os.urandom", OS_URANDOM), client: + client.set_input_flow(reset_skip_input_flow) + client.set_expected_responses( + [ + messages.ButtonRequest(code=B.ResetDevice), + messages.EntropyRequest(), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.Success(), + messages.Features(), + ] + ) + device.reset( + client, + pin_protection=False, + passphrase_protection=False, + backup_type=BackupType.Slip39_Basic, + ) + + assert client.features.initialized is True + assert client.features.needs_backup is True + assert client.features.unfinished_backup is False + assert client.features.no_backup is False + assert client.features.backup_type is BackupType.Slip39_Basic + + # generate secret locally + internal_entropy = client.debug.state().reset_entropy + secret = generate_entropy(128, internal_entropy, EXTERNAL_ENTROPY) + + mnemonics = backup_flow(client) + + client.init_device() + assert client.features.initialized is True + assert client.features.needs_backup is False + assert client.features.unfinished_backup is False + assert client.features.backup_type is BackupType.Slip39_Basic + + assert mnemonics is not [] + + validate_mnemonics(mnemonics, 3, secret) + + +def backup_flow(client): + mnemonics = [] + + def input_flow(): + # 1. Checklist + # 2. Number of shares (5) + # 3. Checklist + # 4. Threshold (3) + # 5. Checklist + # 6. Confirm show seeds + yield from click_through(client.debug, screens=6, code=B.ResetDevice) + + # Mnemonic phrases + for _ in range(5): + yield # Phrase screen + mnemonic = read_and_confirm_mnemonic(client.debug, words=20) + mnemonics.append(mnemonic) + yield # Confirm continue to next + client.debug.press_yes() + + # Confirm backup + yield + client.debug.press_yes() + + with client: + client.set_input_flow(input_flow) + client.set_expected_responses( + [ + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.ResetDevice), + messages.ButtonRequest(code=B.Success), + messages.ButtonRequest(code=B.Success), + messages.Success(), + ] + ) + device.backup(client) + + return mnemonics + + +def validate_mnemonics(mnemonics, threshold, expected_ems): + # We expect these combinations to recreate the secret properly + for test_group in combinations(mnemonics, threshold): + # TODO: HOTFIX, we should fix this properly by modifying and unifying the python-shamir-mnemonic API + ms = shamir.combine_mnemonics(test_group) + identifier, iteration_exponent, _, _, _ = shamir._decode_mnemonics(test_group) + ems = shamir._encrypt(ms, b"", iteration_exponent, identifier) + assert ems == expected_ems + # We expect these combinations to raise MnemonicError + for test_group in combinations(mnemonics, threshold - 1): + with pytest.raises( + MnemonicError, match=r".*Expected {} mnemonics.*".format(threshold) + ): + shamir.combine_mnemonics(test_group)