From 73fa60320007fe247e141371efb19fdc7899ff7e Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Thu, 5 Oct 2023 19:07:51 +0200 Subject: [PATCH] Never store 'seeds' in ephemeral settings (cherry picked from commit e1ac204e04b14c52db8581f38e9d39dbe9772a81) --- shared/nvstore.py | 38 +++++++++--- shared/pincodes.py | 15 ++++- shared/seed.py | 120 ++++++++++++++++++++++-------------- testing/devtest/set_seed.py | 3 + testing/test_ephemeral.py | 6 +- 5 files changed, 120 insertions(+), 62 deletions(-) diff --git a/shared/nvstore.py b/shared/nvstore.py index 92a050eb..4e707f5c 100644 --- a/shared/nvstore.py +++ b/shared/nvstore.py @@ -70,12 +70,10 @@ from version import mk_num, is_devmode # settings linked to seed # LINKED_SETTINGS = ["multisig", "tp", "ovc", "xfp", "xpub", "words"] -# settings that does not make sense to copy to ephemeral secret +# settings that does not make sense to copy to temporary secret # LINKED_SETTINGS += ["sd2fa", "usr", "axi", "hsmcmd"] # prelogin settings - do not need to be part of other saved settings # PRELOGIN_SETTINGS = ["_skip_pin", "nick", "rngk", "lgto", "kbtn", "terms_ok"] -# seed vault is singleton -SINGLE_SETTINGS = ["seedvault", "seeds"] # keep these settings only if unspecified on the other end KEEP_IF_BLANK_SETTINGS = ["bkpw", "wa", "sighshchk", "emu", "rz", "axskip", "del", "pms", "idle_to", "b39skip"] @@ -91,6 +89,31 @@ def MK4_FILENAME(slot): return MK4_WORKDIR + ('%03x.aes' % slot) +# master current dict +master_sv_data = None +master_nvram_key = None + + +def extract_master_sv_data(settings_dict): + # allows us to specify what we want to keep + # in global 'master_sv_data' variable + # and save RAM + if settings_dict is None: + return + seeds = settings_dict.get("seeds", []) + seedvault = settings_dict.get("seedvault", 0) + if (not seedvault) and (not seeds): + return + return {"seeds": seeds, "seedvault": seedvault, + "xfp": settings_dict.get("xfp", 0)} + + +def set_master_sv_data(data, key): + global master_sv_data, master_nvram_key + master_sv_data = extract_master_sv_data(data) + master_nvram_key = key + + class SettingsObject: def __init__(self, dis=None): @@ -307,6 +330,9 @@ class SettingsObject: self.current['chain'] = 'XTN' def get(self, kn, default=None): + if master_sv_data and kn in ["seeds", "seedvault"]: + # we are in temporary mode as global 'master_sv_data' is populated + return master_sv_data.get(kn, default) return self.current.get(kn, default) def changed(self): @@ -331,12 +357,6 @@ class SettingsObject: def merge_previous_active(self, previous): if previous: - for k in SINGLE_SETTINGS: - if k not in previous: - self.current.pop(k, None) - else: - self.current[k] = previous[k] - for k in KEEP_IF_BLANK_SETTINGS: if previous.get(k, None) and not self.current.get(k, None): self.current[k] = previous[k] diff --git a/shared/pincodes.py b/shared/pincodes.py index f5a8b725..000907e7 100644 --- a/shared/pincodes.py +++ b/shared/pincodes.py @@ -416,6 +416,7 @@ class PinAttempt: # Main secret has changed: reset the settings+their key, # and capture xfp/xpub # if None is provided as raw_secret -> restore to main seed + import nvstore from glob import settings stash.SensitiveValues.clear_cache() @@ -447,9 +448,10 @@ class PinAttempt: sv.chain = chain if raw_secret is None: # restore to main wallet - nv = sv.encoded_secret() - settings.set_key(nv) + settings.nvram_key = nvstore.master_nvram_key settings.load() + # blank global as we return to main seed + nvstore.set_master_sv_data(None, None) else: sv.capture_xpub() except stash.ZeroSecretException: @@ -461,15 +463,22 @@ class PinAttempt: self.state_flags |= (1 << 4) return - settings.merge_previous_active(old_values) def tmp_secret(self, encoded, chain=None, bip39pw=''): # Use indicated secret and stop using the SE; operate like this until reboot val = bytes(encoded + bytes(AE_SECRET_LEN - len(encoded))) if self.tmp_value == val: + # noop - already enabled return False + if not self.tmp_value: + # going from master seed + import nvstore + from glob import settings + nvstore.set_master_sv_data(settings.current, + settings.nvram_key) + self.tmp_value = val # We're no longer blank. hard to say about duress secret and stuff tho diff --git a/shared/seed.py b/shared/seed.py index 0068e72a..d09c7042 100644 --- a/shared/seed.py +++ b/shared/seed.py @@ -416,6 +416,8 @@ async def in_seed_vault(xfp=None, seeds=None): return False async def add_seed_to_vault(encoded, meta=None): + import nvstore + if not settings.get("seedvault", False): # seed vault disabled return @@ -426,24 +428,21 @@ async def add_seed_to_vault(encoded, meta=None): _,_,node = SecretStash.decode(encoded) new_xfp = swab32(node.my_fp()) new_xfp_str = xfp2str(new_xfp) - tmp_val = None # do not offer to store secrets that are already in vault if await in_seed_vault(new_xfp_str, settings.get("seeds", [])): return - if pa.tmp_value: - dis.fullscreen("Wait...") - tmp_val = pa.tmp_value[:] - await restore_to_main_secret(preserve_settings=True) - # do not offer to store main seed - if new_xfp == settings.get("xfp", 0): - if tmp_val: - pa.tmp_secret(tmp_val) + if nvstore.master_sv_data: + main_xfp = nvstore.master_sv_data.get("xfp", 0) + else: + main_xfp = settings.get("xfp", 0) + if new_xfp == main_xfp: return + # if in ephemeral 'seeds' are grabbed from master_sv_data global var seeds = settings.get("seeds", []) xfp_ui = "[%s]" % new_xfp_str @@ -454,26 +453,34 @@ async def add_seed_to_vault(encoded, meta=None): ch = await ux_show_story(story, escape="1") if ch == "1": + eph_nvram_key = None seeds.append((new_xfp_str, stash.SecretStash.storage_encode(encoded), xfp_ui, meta)) + if nvstore.master_sv_data: + # in ephemeral + nvstore.master_sv_data["seeds"] = seeds + eph_nvram_key = settings.nvram_key[:] + settings.nvram_key = nvstore.master_nvram_key + settings.load() + settings.set("seeds", seeds) settings.save() + + if eph_nvram_key: + # restore ephemerlal settings after save to main settings + settings.nvram_key = eph_nvram_key + settings.load() + await ux_show_story(xfp_ui + "\nSaved to Seed Vault") - - if tmp_val: - dis.fullscreen("Wait...") - pa.tmp_secret(tmp_val) - settings.set("seeds", seeds) - settings.save() - return True async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='', is_restore=False, meta=None): if not is_restore: await add_seed_to_vault(encoded, meta=meta) + dis.fullscreen("Wait...") applied = pa.tmp_secret(encoded, chain=chain, bip39pw=bip39pw) dis.progress_bar_show(1) @@ -551,6 +558,7 @@ async def set_seed_extended_key(extended_key): async def set_ephemeral_seed_extended_key(extended_key, meta=None): encoded, chain = xprv_to_encoded_secret(extended_key) + dis.fullscreen("Applying...") await set_ephemeral_seed(encoded=encoded, chain=chain, meta=meta) goto_top_menu() @@ -805,6 +813,7 @@ class SeedVaultMenu(MenuSystem): @staticmethod async def _clear(menu, label, item): + import nvstore from glob import dis, settings idx, xfp_str, encoded = item.arg @@ -822,13 +831,11 @@ class SeedVaultMenu(MenuSystem): dis.fullscreen("Saving...") - tmp_val = None wipe_slot = (ch != "1") + tmp_val = False if pa.tmp_value: - # active ephemeral seed - assert (encoded == stash.SecretStash.storage_encode(pa.tmp_value)) - tmp_val = pa.tmp_value[:] + tmp_val = True if wipe_slot: # are we deleting current active ephemeral wallet @@ -836,31 +843,47 @@ class SeedVaultMenu(MenuSystem): # slot wiping if tmp_val: # wipe current settings + master_nvram_key = nvstore.master_nvram_key settings.blank() else: # in main settings settings.save() - pa.tmp_secret(pad_raw_secret(encoded)) + master_nvram_key = settings.nvram_key[:] + settings.set_key(pad_raw_secret(encoded)) + settings.load() settings.blank() - if pa.tmp_value: - await restore_to_main_secret(preserve_settings=True) + # back to master settings + pa.tmp_value = False + settings.nvram_key = master_nvram_key + settings.load() + nvstore.set_master_sv_data(None, None) + eph_nvram_key = None + # will get global master_sv_data from nvstore if in ephemeral seeds = settings.get("seeds", []) try: del seeds[idx] - settings.set("seeds", seeds) - settings.save() - except IndexError: pass - finally: - if tmp_val and (not wipe_slot): - # we were in ephemeral mode before and have not - # wiped seed - return back to ephemeral - pa.tmp_secret(tmp_val) - settings.set("seeds", seeds) - settings.save() - elif tmp_val and wipe_slot: - goto_top_menu() + except IndexError: + pass + + if nvstore.master_sv_data: + # in ephemeral + nvstore.master_sv_data["seeds"] = seeds + eph_nvram_key = settings.nvram_key[:] + settings.nvram_key = nvstore.master_nvram_key + settings.load() + + settings.set("seeds", seeds) + settings.save() + + if tmp_val and (not wipe_slot): + # we were in ephemeral mode before and have not + # wiped seed - return back to ephemeral + settings.nvram_key = eph_nvram_key + settings.load() + elif tmp_val and wipe_slot: + goto_top_menu() # pop menu stack the_ux.pop() @@ -881,17 +904,12 @@ class SeedVaultMenu(MenuSystem): @staticmethod async def _rename(menu, label, item): # let them edit the name + import nvstore from glob import dis idx, xfp_str = item.arg - tmp_val = None - if pa.tmp_value: - tmp_val = pa.tmp_value[:] - dis.fullscreen("Wait...") - settings.save() - await restore_to_main_secret(preserve_settings=True) - + # will get global master_sv_data from nvstore if in ephemeral seeds = settings.get("seeds", []) chk_xfp, encoded, old_name, meta = seeds[idx] assert chk_xfp == xfp_str @@ -900,18 +918,26 @@ class SeedVaultMenu(MenuSystem): new_name = await ux_input_text(old_name, confirm_exit=False, max_len=40) if not new_name: - new_name = old_name + return dis.fullscreen("Saving...") # save it + eph_nvram_key = None seeds[idx] = (chk_xfp, encoded, new_name, meta) + if nvstore.master_sv_data: + # in ephemeral + nvstore.master_sv_data["seeds"] = seeds + eph_nvram_key = settings.nvram_key[:] + settings.nvram_key = nvstore.master_nvram_key + settings.load() + settings.set("seeds", seeds) settings.save() - if tmp_val: - pa.tmp_secret(tmp_val) - settings.set("seeds", seeds) - settings.save() + if eph_nvram_key: + # restore ephemerlal settings after save to main settings + settings.nvram_key = eph_nvram_key + settings.load() # update label in sub-menu menu.items[0].label = new_name diff --git a/testing/devtest/set_seed.py b/testing/devtest/set_seed.py index 088f8d87..26cbe8fc 100644 --- a/testing/devtest/set_seed.py +++ b/testing/devtest/set_seed.py @@ -9,6 +9,7 @@ import stash from seed import set_seed_value from utils import xfp2str from actions import goto_top_menu +import nvstore tn = chains.BitcoinTestnet @@ -21,6 +22,8 @@ settings.set('idle_to', 0) import main pa.tmp_value = None +nvstore.master_sv_data = None +nvstore.master_nvram_key = None set_seed_value(main.WORDS) print("New key in effect: %s" % settings.get('xpub', 'MISSING')) diff --git a/testing/test_ephemeral.py b/testing/test_ephemeral.py index 378eaf40..6b2934cf 100644 --- a/testing/test_ephemeral.py +++ b/testing/test_ephemeral.py @@ -750,7 +750,7 @@ def test_activate_current_tmp_secret(reset_seed_words, goto_eph_seed_menu, ]) def test_seed_vault_menus(dev, data, settings_set, settings_get, pick_menu_item, need_keypress, cap_story, cap_menu, reset_seed_words, get_identity_story, get_seed_value_ux, fake_txn, - try_sign, sim_exec, goto_home, goto_eph_seed_menu): + try_sign, sim_exec, goto_home): # Verify "seed vault" feature works as intended reset_seed_words() xfp, entropy, mnemonic = data @@ -775,8 +775,8 @@ def test_seed_vault_menus(dev, data, settings_set, settings_get, pick_menu_item, pick_menu_item("Seed Vault") time.sleep(.1) _, story = cap_story() - assert "Enable Seed Vault?" in story - need_keypress("y") + if "Enable Seed Vault?" in story: + need_keypress("y") time.sleep(.1) pick_menu_item("Enable") time.sleep(.5)