From c57491263502c86f0be772005d3882cdda56609b Mon Sep 17 00:00:00 2001 From: Dipun Mistry Date: Tue, 26 Apr 2022 23:06:46 +0100 Subject: [PATCH 1/4] created implementation of seed save and restore --- shared/flow.py | 5 +- shared/pincodes.py | 3 ++ shared/xor_seed.py | 61 ++++++++++++++++++++-- shared/xor_seedsave.py | 113 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 shared/xor_seedsave.py diff --git a/shared/flow.py b/shared/flow.py index 56fa0037..e9738a9b 100644 --- a/shared/flow.py +++ b/shared/flow.py @@ -13,7 +13,7 @@ from users import make_users_menu from drv_entro import drv_entro_start from backups import clone_start, clone_write_data -from xor_seed import xor_split_start, xor_restore_start +from xor_seed import xor_save_start, xor_split_start, xor_restore_start from countdowns import countdown_pin_submenu, countdown_chooser # Optional feature: HSM @@ -230,7 +230,8 @@ def vdisk_enabled(): SeedXORMenu = [ # xxxxxxxxxxxxxxxx MenuItem("Split Existing", f=xor_split_start), - MenuItem("Restore Seed XOR", f=xor_restore_start), + MenuItem("Restore Seed XOR", menu=xor_restore_start), + MenuItem("Create XOR file", menu=xor_save_start) ] SeedFunctionsMenu = [ diff --git a/shared/pincodes.py b/shared/pincodes.py index f832e55b..de696c3b 100644 --- a/shared/pincodes.py +++ b/shared/pincodes.py @@ -328,6 +328,9 @@ def has_duress_pin(self): def has_brickme_pin(self): return bool(self.state_flags & PA_HAS_BRICKME) + def has_tmp_seed(self): + return not self.tmp_value == False + def reset(self): # start over, like when you commit a new seed return self.setup(self.pin, self.is_secondary) diff --git a/shared/xor_seed.py b/shared/xor_seed.py index 5bbef423..13b8f856 100644 --- a/shared/xor_seed.py +++ b/shared/xor_seed.py @@ -5,6 +5,8 @@ # - for secret spliting on paper # - all combination of partial XOR seed phrases are working wallets # +from menu import MenuItem, MenuSystem +from xor_seedsave import XORSeedSaver import stash, ngu, chains, bip39, random from ux import ux_show_story, ux_enter_number, the_ux, ux_confirm, ux_dramatic_pause from seed import word_quiz, WordNestMenu, set_seed_value @@ -107,8 +109,10 @@ async def xor_split_start(*a): continue for ws, part in enumerate(word_parts): + print('ws, part, %s, %s'%(ws, part)) ch = await word_quiz(part, title='Word %s%%d is?' % chr(65+ws)) - if ch == 'x': break + if ch == 'x': + break else: break @@ -153,7 +157,7 @@ async def all_done(new_words): return None elif ch == '1': # do another list of words - nxt = XORWordNestMenu(num_words=24) + nxt = XORSourceMenu() the_ux.push(nxt) elif ch == '2': # done; import on temp basis, or be the main secret @@ -169,6 +173,7 @@ async def all_done(new_words): else: pa.tmp_secret(enc) await ux_show_story("New master key in effect until next power down.") + goto_top_menu() return None @@ -177,6 +182,32 @@ def tr_label(self): pn = len(import_xor_parts) return chr(65+pn) + ' Word' + + + +class XORSourceMenu(MenuSystem): + def __init__(self): + items = [ + MenuItem('Manually Enter', menu=self.manual_entry), + MenuItem('From SDCard', f=self.from_sdcard) + ] + + super(XORSourceMenu, self).__init__(items) + + async def manual_entry(*a): + return XORWordNestMenu(num_words=24) + + async def from_sdcard(*a): + new_words = await XORSeedSaver().read_from_card() + if not new_words: + return None + + return await XORWordNestMenu.all_done(new_words) + + + + + async def show_n_parts(parts, chk_word): num_parts = len(parts) msg = 'Record these %d lists of 24-words each.\n\n' % num_parts @@ -224,6 +255,30 @@ async def xor_restore_start(*a): if len(words) == 24: import_xor_parts.append(words) - return XORWordNestMenu(num_words=24) + return XORSourceMenu() + +async def xor_save_start(*a): + from pincodes import pa + if pa.has_tmp_seed(): + ch = await ux_show_story('''\ +The current master key is a temporary one; the file will be encrypted with this key. + +Press OK to continue. X to cancel. +''') + if ch == 'x': return + + ch = await ux_show_story('''\ +Have your 24-word phrase ready. You will enter the 24 words which will then be encrypted using the master key and stored on your SDCard. + +Press OK to continue. X to cancel. +''') + if ch == 'x': return + + + async def callback(new_words): + WordNestMenu.pop_all() + return await XORSeedSaver().save_to_card(new_words) + + return WordNestMenu(num_words=24, done_cb=callback) # EOF diff --git a/shared/xor_seedsave.py b/shared/xor_seedsave.py new file mode 100644 index 00000000..669911cd --- /dev/null +++ b/shared/xor_seedsave.py @@ -0,0 +1,113 @@ +# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# xor_seedsave.py - Save xor seedwords into encrypted file on MicroSD (if desired) +# +import sys, stash, ujson, os, ngu +from actions import file_picker +from files import CardSlot, CardMissingError +from ux import ux_show_story + +class XORSeedSaver: + # Encrypts a 12-word seed very carefully, and appends + # to a file on MicroSD card. + # AES-256 CTR with key=SHA256(SHA256(salt + derived key off master + salt)) + # where: salt=sha256(microSD serial # details) + + def _calc_key(self, card): + # calculate the key to be used. + if getattr(self, 'key', None): return + + try: + salt = card.get_id_hash() + + with stash.SensitiveValues(bypass_pw=True) as sv: + self.key = bytearray(sv.encryption_key(salt)) + + except: + self.key = None + + def _read(self, filename): + # Return 24 words from encrypted file, or empty list if fail. + # Fail silently in all cases. + decrypt = ngu.aes.CTR(self.key) + + try: + msg = open(filename, 'rb').read() + txt = decrypt.cipher(msg) + val = ujson.loads(txt) + + # If contents are not what we expect, return nothing + if not type(val) is list: + return [] + if not len(val) == 24: + return [] + + return val + except: + return [] + + def _write(self, filename, words): + # Encrypt and save words to file. + # Allow exceptions to throw as validation should + # have been performed before calling. + encrypt = ngu.aes.CTR(self.key) + json = ujson.dumps(words) + contents = encrypt.cipher(json) + open(filename, 'wb').write(contents) + + async def read_from_card(self): + import pyb + if not pyb.SDCard().present(): + await ux_show_story("Insert an SDCard and try again.") + return None + + choices = await file_picker(None, suffix='xor') + filename = await file_picker('Choose your XOR file.', choices=choices) + + if not filename: + return None + + # Read file, decrypt and make a menu to show; OR return None + # if any error hit. + try: + with CardSlot() as card: + self._calc_key(card) + if not self.key: + await ux_show_story("Failed to read file!\n\nNo action has been taken.") + return None + + data = self._read(filename) + if not data: + await ux_show_story("Failed to read file!\n\nNo action has been taken.") + return None + + return data + except CardMissingError: + # not an error: they just aren't using feature + await ux_show_story("Failed to read file!\n\nNo action has been taken.") + return None + + async def save_to_card(self, words): + msg = ('Confirm these %d secret words:\n') % len(words) + msg += '\n'.join('%2d: %s' % (i+1, w) for i,w in enumerate(words)) + ch = await ux_show_story(msg, sensitive=True) + if ch == 'x': return + + import pyb + while not pyb.SDCard().present(): + ch = await ux_show_story('Please insert an SDCard!') + if ch == 'x': return + + from glob import dis + # Show progress: + dis.fullscreen('Encrypting...') + + with CardSlot() as card: + filename, nice = card.pick_filename('seedwords.xor') + self._calc_key(card) + self._write(filename, words) + await ux_show_story('XOR file written:\n\n%s' % nice) + + return None + +# EOF From 2983d8d7a9aacd11eafe1443824c3b26d0504529 Mon Sep 17 00:00:00 2001 From: Dipun Mistry Date: Wed, 27 Apr 2022 01:30:05 +0100 Subject: [PATCH 2/4] fixed existing tests and tweaked messages/comments/gitignore --- shared/xor_seed.py | 12 +++++++++--- testing/test_seed_xor.py | 8 +++++++- unix/work/MicroSD/.gitignore | 1 + 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/shared/xor_seed.py b/shared/xor_seed.py index 13b8f856..383b2261 100644 --- a/shared/xor_seed.py +++ b/shared/xor_seed.py @@ -156,8 +156,10 @@ async def all_done(new_words): import_xor_parts.clear() # concern: we are contaminated w/ secrets return None elif ch == '1': - # do another list of words - nxt = XORSourceMenu() + # do another list of words. + # fast-track to manual entry if no secret set yet. + from pincodes import pa + nxt = XORWordNestMenu(num_words=24) if pa.is_secret_blank() else XORSourceMenu() the_ux.push(nxt) elif ch == '2': # done; import on temp basis, or be the main secret @@ -188,7 +190,7 @@ def tr_label(self): class XORSourceMenu(MenuSystem): def __init__(self): items = [ - MenuItem('Manually Enter', menu=self.manual_entry), + MenuItem('Enter Manually', menu=self.manual_entry), MenuItem('From SDCard', f=self.from_sdcard) ] @@ -255,6 +257,10 @@ async def xor_restore_start(*a): if len(words) == 24: import_xor_parts.append(words) + # fast-track to manual entry if no secret set yet. + if pa.is_secret_blank(): + return XORWordNestMenu(num_words=24) + return XORSourceMenu() async def xor_save_start(*a): diff --git a/testing/test_seed_xor.py b/testing/test_seed_xor.py index 209d8f81..2c670cfa 100644 --- a/testing/test_seed_xor.py +++ b/testing/test_seed_xor.py @@ -22,7 +22,7 @@ ( [ones32]*7, ones32), ( [ones32]*4, zero32), ]) -def test_import_xor(incl_self, parts, expect, goto_home, pick_menu_item, cap_story, need_keypress, cap_menu, word_menu_entry, get_secrets, reset_seed_words, set_seed_words): +def test_import_xor_manual(incl_self, parts, expect, goto_home, pick_menu_item, cap_story, need_keypress, cap_menu, word_menu_entry, get_secrets, reset_seed_words, set_seed_words): # values from docs/seed-xor.md, and some easy cases @@ -50,6 +50,8 @@ def test_import_xor(incl_self, parts, expect, goto_home, pick_menu_item, cap_sto else: need_keypress('y') + pick_menu_item('Enter Manually') + #time.sleep(0.01) for n, part in enumerate(parts): @@ -65,6 +67,7 @@ def test_import_xor(incl_self, parts, expect, goto_home, pick_menu_item, cap_sto if n != len(parts)-1: need_keypress('1') + pick_menu_item('Enter Manually') else: # correct anticipated checksum word chk_word = expect.split()[-1] @@ -165,6 +168,7 @@ def test_import_zero_set(goto_home, pick_menu_item, cap_story, need_keypress, ca assert 'you have a seed already' in body assert '(1) to include this Coldcard' in body need_keypress('y') + pick_menu_item('Enter Manually') #time.sleep(0.01) @@ -181,6 +185,8 @@ def test_import_zero_set(goto_home, pick_menu_item, cap_story, need_keypress, ca return need_keypress('1') + pick_menu_item('Enter Manually') + raise pytest.fail("reached") diff --git a/unix/work/MicroSD/.gitignore b/unix/work/MicroSD/.gitignore index 7ac52166..a5df5c78 100644 --- a/unix/work/MicroSD/.gitignore +++ b/unix/work/MicroSD/.gitignore @@ -6,4 +6,5 @@ *.pdf *.dfu *.txn +*.xor .tmp.tmp From 55aa09cd78c10f58ca7593b9edb410fa1385e8c9 Mon Sep 17 00:00:00 2001 From: Dipun Mistry Date: Wed, 27 Apr 2022 01:58:23 +0100 Subject: [PATCH 3/4] tweaked message --- shared/xor_seedsave.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/xor_seedsave.py b/shared/xor_seedsave.py index 669911cd..dc44117e 100644 --- a/shared/xor_seedsave.py +++ b/shared/xor_seedsave.py @@ -95,7 +95,7 @@ async def save_to_card(self, words): import pyb while not pyb.SDCard().present(): - ch = await ux_show_story('Please insert an SDCard!') + ch = await ux_show_story('Please insert an SDCard!\n\nPress OK to continue, X to cancel') if ch == 'x': return from glob import dis From c6fd1f2537d9f1d43bfa5fc67e835b8ea7d8576d Mon Sep 17 00:00:00 2001 From: Dipun Mistry Date: Fri, 29 Apr 2022 17:56:44 +0100 Subject: [PATCH 4/4] added xor_seedsave to manifest --- shared/manifest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shared/manifest.py b/shared/manifest.py index c9d4a6ee..a5664e1e 100644 --- a/shared/manifest.py +++ b/shared/manifest.py @@ -53,6 +53,7 @@ 'version.py', 'xor_seed.py', 'ftux.py', + 'xor_seedsave.py', ], opt=0) # Optimize data-like files, since no need to debug them.