From d4f13317fc550fc818cdc8ec2133309351679652 Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Thu, 7 Dec 2023 14:47:47 +0100 Subject: [PATCH] bugfix: do not allow to import master seed as temporary --- shared/nvstore.py | 26 +++++++----- shared/pincodes.py | 35 ++++++++++++--- shared/seed.py | 4 +- testing/test_ephemeral.py | 89 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 131 insertions(+), 23 deletions(-) diff --git a/shared/nvstore.py b/shared/nvstore.py index 5b4acb84..9ab4f3da 100644 --- a/shared/nvstore.py +++ b/shared/nvstore.py @@ -116,12 +116,25 @@ class SettingsObject: ctr = ustruct.pack('<4I', 4, 3, 2, pos) return aes256ctr.new(self.nvram_key, ctr) + @staticmethod + def hash_key(secret): + # hash up the secret... without decoding it or similar + assert len(secret) >= 32 + + s = sha256(secret) + + for round in range(5): + s.update('pad') + s = sha256(s.digest()) + + return s.digest() + def set_key(self, new_secret=None): # System settings (not secrets) are stored in flash, encrypted with this # key that is derived from main wallet secret. Call this method when the secret # is first loaded, or changes for some reason. from pincodes import pa - from stash import blank_object, SensitiveValues + from stash import blank_object key = None mine = False @@ -136,16 +149,7 @@ class SettingsObject: mine = True if new_secret: - # hash up the secret... without decoding it or similar - assert len(new_secret) >= 32 - - s = sha256(new_secret) - - for round in range(5): - s.update('pad') - s = sha256(s.digest()) - - key = s.digest() + key = self.hash_key(new_secret) if mine: blank_object(new_secret) diff --git a/shared/pincodes.py b/shared/pincodes.py index df08432a..62688b87 100644 --- a/shared/pincodes.py +++ b/shared/pincodes.py @@ -412,7 +412,8 @@ class PinAttempt: self.roundtrip(7, fw_upgrade=(start, length)) # not-reached - def new_main_secret(self, raw_secret=None, chain=None, bip39pw='', blank=False): + def new_main_secret(self, raw_secret=None, chain=None, bip39pw='', blank=False, + target_nvram_key = None): # Main secret has changed: reset the settings+their key, # and capture xfp/xpub # if None is provided as raw_secret -> restore to main seed @@ -437,7 +438,13 @@ class PinAttempt: settings.blank() old_values = None else: - settings.set_key(raw_secret) + if target_nvram_key is None: + settings.set_key(raw_secret) + else: + # we already have hashed nvram key calculated + # from self.tmp_secret - use it + settings.nvram_key = target_nvram_key + settings.load() # Recalculate xfp/xpub values (depends both on secret and chain) @@ -465,17 +472,30 @@ class PinAttempt: settings.load() self.state_flags |= PA_ZERO_SECRET - def tmp_secret(self, encoded, chain=None, bip39pw=''): # Use indicated secret and stop using the SE; operate like this until reboot + from glob import settings + val = bytes(encoded + bytes(AE_SECRET_LEN - len(encoded))) if self.tmp_value == val: # noop - already enabled - return False + return False, "Temporary master key already in use." + + target_nvram_key = None + if encoded is not None: + # disallow using master seed as temporary + master_err = "Cannot use master seed as temporary." + target_nvram_key = settings.hash_key(encoded) + if settings.master_nvram_key: + assert self.tmp_value + if target_nvram_key == settings.master_nvram_key: + return False, master_err + else: + if target_nvram_key == settings.nvram_key: + return False, master_err if not self.tmp_value: # leaving from master seed, might capture some useful values - from glob import settings settings.leaving_master_seed() self.tmp_value = val @@ -485,13 +505,14 @@ class PinAttempt: # Copies system settings to new encrypted-key value, calculates # XFP, XPUB and saves into that, and starts using them. - self.new_main_secret(self.tmp_value, chain=chain, bip39pw=bip39pw) + self.new_main_secret(self.tmp_value, chain=chain, bip39pw=bip39pw, + target_nvram_key=target_nvram_key) # On Q1, update status icons from glob import dis dis.draw_status(bip39=1 if bip39pw else 0, tmp=1) - return True + return True, None def trick_request(self, method_num, data): # send/recv a trick-pin related request (mk4 only) diff --git a/shared/seed.py b/shared/seed.py index 443371a0..2aab1672 100644 --- a/shared/seed.py +++ b/shared/seed.py @@ -466,13 +466,13 @@ async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='', await add_seed_to_vault(encoded, meta=meta) dis.fullscreen("Wait...") - applied = pa.tmp_secret(encoded, chain=chain, bip39pw=bip39pw) + applied, err_msg = pa.tmp_secret(encoded, chain=chain, bip39pw=bip39pw) dis.progress_bar_show(1) xfp = settings.get("xfp", None) if xfp: xfp = "[" + xfp2str(xfp) + "]" if not applied: - await ux_show_story(title=xfp, msg="Temporary master key already in use.") + await ux_show_story(title="FAILED", msg=err_msg) return if summarize_ux: diff --git a/testing/test_ephemeral.py b/testing/test_ephemeral.py index fdf67113..280813ca 100644 --- a/testing/test_ephemeral.py +++ b/testing/test_ephemeral.py @@ -4,7 +4,8 @@ # import pytest, time, re, os, shutil, pdb, hashlib -from constants import simulator_fixed_tpub, simulator_fixed_words, simulator_fixed_xfp, simulator_fixed_xpub +from constants import simulator_fixed_tpub, simulator_fixed_xfp, simulator_fixed_xpub +from constants import simulator_fixed_words, simulator_fixed_tprv from ckcc.protocol import CCProtocolPacker from txn import fake_txn from test_ux import word_menu_entry @@ -763,8 +764,8 @@ def test_activate_current_tmp_secret(reset_seed_words, goto_eph_seed_menu, title, story = cap_story() assert "Temporary master key already in use" in story - already_used_xfp = title[1:-1] - assert already_used_xfp == in_effect_xfp == expected_xfp + assert title == "FAILED" + assert in_effect_xfp == expected_xfp need_keypress("y") @@ -1280,4 +1281,86 @@ def test_temporary_from_backup(multisig, backup_system, import_ms_wallet, get_se else: restore_main_seed(False) + +def test_import_master_as_tmp(reset_seed_words, goto_eph_seed_menu, cap_story, + ephemeral_seed_disabled, pick_menu_item, goto_home, + need_keypress, word_menu_entry, settings_set, + confirm_tmp_seed, cap_menu, microsd_path, + restore_main_seed, get_identity_story): + reset_seed_words() + + goto_eph_seed_menu() + ephemeral_seed_disabled() + + # try import same seed as current simulator master + words, expected_xfp = simulator_fixed_words, simulator_fixed_xfp + xfp_str = xfp2str(expected_xfp) + pick_menu_item("Import Words") + pick_menu_item(f"24 Words") + time.sleep(0.1) + + word_menu_entry(words.split()) + time.sleep(.1) + title, story = cap_story() + assert "FAILED" == title + assert 'Cannot use master seed as temporary.' in story + need_keypress("x") + + # go to ephemeral seed and then try to create new ephemeral seed from master + # when in different temporary seed whatsoever + goto_eph_seed_menu() + + # random temporary seed + pick_menu_item("Generate Words") + pick_menu_item(f"12 Words") + need_keypress("6") # skip quiz + need_keypress("y") # yes - I'm sure + confirm_tmp_seed(seedvault=False) + + goto_home() + time.sleep(0.1) + menu = cap_menu() + # ephemeral seed chosen + assert "[" in menu[0] + goto_eph_seed_menu() + pick_menu_item("Import Words") + pick_menu_item(f"24 Words") + time.sleep(0.1) + + word_menu_entry(words.split()) + time.sleep(.1) + title, story = cap_story() + assert "FAILED" == title + assert 'Cannot use master seed as temporary.' in story + need_keypress("x") + + # now import same seed but represented as master extended key + # this works and does not delete master settings as encoded + # secret is different and therefore nvram_key too + fname = "ek_sim.txt" + with open(microsd_path(fname), "w") as f: + f.write(simulator_fixed_tprv) + + goto_eph_seed_menu() + pick_menu_item("Import XPRV") + title, story = cap_story() + if "Press (1)" in story: + need_keypress("1") + + need_keypress("y") # Select file containing... + pick_menu_item(fname) + confirm_tmp_seed(seedvault=False) # allowed + + # verify we are in temporary seed + goto_home() + time.sleep(0.1) + menu = cap_menu() + # ephemeral seed chosen + assert "[" in menu[0] + assert xfp_str in menu[0] + restore_main_seed(preserve_settings=False, seed_vault=False) + story = get_identity_story() + assert "00000000" not in story + assert xfp_str in story + # EOF