From 5b8ca01b090e012e653c316a7f16f88373611650 Mon Sep 17 00:00:00 2001 From: Stepan Snigirev Date: Wed, 16 Nov 2022 00:16:29 +0100 Subject: [PATCH] improve flash and sd keystore, remove legacy export to SD --- src/keystore/flash.py | 180 +++++++++++++++++++++++++++++------------ src/keystore/sdcard.py | 107 +----------------------- 2 files changed, 132 insertions(+), 155 deletions(-) diff --git a/src/keystore/flash.py b/src/keystore/flash.py index c9e84bd..25bee8d 100644 --- a/src/keystore/flash.py +++ b/src/keystore/flash.py @@ -12,7 +12,7 @@ from rng import get_random_bytes from embit import ec, bip39, bip32 from helpers import tagged_hash -from gui.screens import Alert, PinScreen, Menu, MnemonicScreen +from gui.screens import Alert, PinScreen, Menu, MnemonicScreen, InputScreen class FlashKeyStore(RAMKeyStore): @@ -30,7 +30,7 @@ class FlashKeyStore(RAMKeyStore): NOTE = "Uses internal memory of the microcontroller for all keys." # Button to go to storage menu # Menu is implemented in async storage_menu function - storage_button = "Reckless" + storage_button = "Flash storage" load_button = "Load key from internal memory" def __init__(self): @@ -187,64 +187,144 @@ def _set_pin(self, pin): # call unlock now self._unlock(pin) + @property + def flashpath(self): + """Path to store bitcoin key""" + return self.path + + async def init(self, show_fn, show_loader): + """ + Waits for keystore media + and loads internal secret and PIN state + """ + self.show = show_fn + self.show_loader = show_loader + platform.maybe_mkdir(self.path) + self.load_secret(self.path) + self.load_state() + # the rest we can get from parent + await super().init(show_fn, show_loader) + + def fileprefix(self, path): + if path is self.flashpath: + return 'reckless' + + hexid = hexlify(tagged_hash("sdid", self.secret)[:4]).decode() + return "specterdiy%s" % hexid + + async def save_mnemonic(self): if self.is_locked: raise KeyStoreError("Keystore is locked") if self.mnemonic is None: raise KeyStoreError("Recovery phrase is not loaded") - self.save_aead( - self.flashpath, plaintext=self.mnemonic.encode(), key=self.enc_secret - ) - # check it's ok - await self.load_mnemonic() - @property - def flashpath(self): - """Path to store bitcoin key""" - return self.path + path = self.flashpath + filename = await self.get_input(suggestion=self.mnemonic.split()[0]) + if filename is None: + return + + fullpath = "%s/%s.%s" % (path, self.fileprefix(path), filename) + + if platform.file_exists(fullpath): + scr = Prompt( + "\n\nFile already exists: %s\n" % filename, + "Would you like to overwrite this file?", + ) + res = await self.show(scr) + if res is False: + return + + self.save_aead(fullpath, plaintext=self.mnemonic.encode(), + key=self.enc_secret) + # check it's ok + await self.load_mnemonic(fullpath) + # return the full file name incl. prefix if saved to SD card, just the name if on flash + return filename @property def is_key_saved(self): - return platform.file_exists(self.flashpath) + flash_files = [ + f[0] for f in os.ilistdir(self.flashpath) + if f[0].lower().startswith(self.fileprefix(self.flashpath)) + ] + flash_exists = (len(flash_files) > 0) + return flash_exists - async def load_mnemonic(self): + async def load_mnemonic(self, file=None): if self.is_locked: raise KeyStoreError("Keystore is locked") - if not platform.file_exists(self.flashpath): + + if file is None: + file = await self.select_file() + if file is None: + return False + + if not platform.file_exists(file): raise KeyStoreError("Key is not saved") - _, data = self.load_aead(self.flashpath, self.enc_secret) + _, data = self.load_aead(file, self.enc_secret) + self.set_mnemonic(data.decode(), "") return True + async def select_file(self): + + buttons = [(None, 'Internal storage')] + buttons += self.load_files(self.flashpath) + + return await self.show(Menu(buttons, title="Select a file", last=(None, "Cancel"))) + + def load_files(self, path): + buttons = [] + files = [f[0] for f in os.ilistdir(path) if f[0].startswith(self.fileprefix(path))] + + if len(files) == 0: + buttons += [(None, 'No files found')] + else: + files.sort() + for file in files: + displayname = file.replace(self.fileprefix(path), "") + if displayname is "": + displayname = "Default" + else: + displayname = displayname[1:] # strip first character + buttons += [("%s/%s" % (path, file), displayname)] + return buttons + async def delete_mnemonic(self): - if not platform.file_exists(self.flashpath): - raise KeyStoreError("Secret is not saved. No need to delete anything.") + file = await self.select_file() + if file is None: + return False + if not platform.file_exists(file): + raise KeyStoreError("File not found.") try: - os.remove(self.flashpath) - except: - raise KeyStoreError("Failed to delete from memory") + os.remove(file) + except Exception as e: + print(e) + raise KeyStoreError("Failed to delete file '%s'" % file) + finally: + return True + + async def get_input( + self, + title="Enter a name for this seed", + note="Naming your seeds allows you to store multiple.\n" + "Give each seed a unique name!", + suggestion="", + ): + scr = InputScreen(title, note, suggestion, min_length=1, strip=True) + await self.show(scr) + return scr.get_value() - async def init(self, show_fn, show_loader): - """ - Waits for keystore media - and loads internal secret and PIN state - """ - self.show = show_fn - self.show_loader = show_loader - platform.maybe_mkdir(self.path) - self.load_secret(self.path) - self.load_state() - # the rest we can get from parent - await super().init(show_fn, show_loader) - async def storage_menu(self): + async def storage_menu(self, title="Manage keys on internal flash"): """Manage storage, return True if new key was loaded""" buttons = [ # id, text - (None, "Key management"), - (0, "Save key to flash"), - (1, "Load key from flash"), - (2, "Delete key from flash"), + (None, title), + (0, "Save key"), + (1, "Load key"), + (2, "Delete key"), ] # we stay in this menu until back is pressed @@ -256,23 +336,19 @@ async def storage_menu(self): if menuitem == 255: return False elif menuitem == 0: - await self.save_mnemonic() - await self.show( - Alert( - "Success!", "Your key is stored in flash now.", button_text="OK" + filename = await self.save_mnemonic() + if filename: + await self.show( + Alert("Success!", "Your key is stored now.\n\nName: %s" % filename, button_text="OK") ) - ) elif menuitem == 1: - await self.load_mnemonic() - await self.show( - Alert("Success!", "Your key is loaded.", button_text="OK") - ) + if await self.load_mnemonic(): + await self.show( + Alert("Success!", "Your key is loaded.", button_text="OK") + ) return True elif menuitem == 2: - await self.delete_mnemonic() - await self.show( - Alert( - "Success!", "Your key is deleted from flash.", button_text="OK" + if await self.delete_mnemonic(): + await self.show( + Alert("Success!", "Your key is deleted.", button_text="OK") ) - ) - \ No newline at end of file diff --git a/src/keystore/sdcard.py b/src/keystore/sdcard.py index 240732d..0c09097 100644 --- a/src/keystore/sdcard.py +++ b/src/keystore/sdcard.py @@ -3,7 +3,7 @@ import platform from rng import get_random_bytes from embit import bip39 -from gui.screens import Alert, Progress, Menu, MnemonicScreen, InputScreen, Prompt +from gui.screens import Alert, Progress, Menu, MnemonicScreen, Prompt import asyncio from io import BytesIO from helpers import tagged_hash @@ -102,11 +102,8 @@ async def save_mnemonic(self): @property def is_key_saved(self): - flash_files = [ - f[0] for f in os.ilistdir(self.flashpath) - if f[0].lower().startswith(self.fileprefix(self.flashpath)) - ] - flash_exists = (len(flash_files) > 0) + flash_exists = super().is_key_saved + if not platform.is_sd_present(): return flash_exists @@ -157,23 +154,6 @@ async def select_file(self): return await self.show(Menu(buttons, title="Select a file", last=(None, "Cancel"))) - def load_files(self, path): - buttons = [] - files = [f[0] for f in os.ilistdir(path) if f[0].startswith(self.fileprefix(path))] - - if len(files) == 0: - buttons += [(None, 'No files found')] - else: - files.sort() - for file in files: - displayname = file.replace(self.fileprefix(path), "") - if displayname is "": - displayname = "Default" - else: - displayname = displayname[1:] # strip first character - buttons += [("%s/%s" % (path, file), displayname)] - return buttons - async def delete_mnemonic(self): file = await self.select_file() @@ -194,85 +174,6 @@ async def delete_mnemonic(self): platform.unmount_sdcard() return True - async def get_input( - self, - title="Enter a name for this seed", - note="Naming your seeds allows you to store multiple.\n" - "Give each seed a unique name!", - suggestion="", - ): - scr = InputScreen(title, note, suggestion, min_length=1, strip=True) - await self.show(scr) - return scr.get_value() - - async def export_mnemonic(self): - if await self.show(Prompt("Warning", - "You need to confirm your PIN code " - "to export your recovery phrase.\n\n" - "Your recovery phrase will be saved " - "to the SD card as plain text.\n\n" - "Anybody who has access to this SD card " - "will be able to read your recovery phrase!\n\n" - "Continue?")): - self.lock() - await self.unlock() - - filename = "seed-export-%s.txt" % self.mnemonic.split()[0] - filepath = "%s/%s" % (self.sdpath, filename) - - if not platform.is_sd_present(): - raise KeyStoreError("SD card is not present") - - platform.mount_sdcard() - - with open(filepath, "w") as f: - f.write("bip39: ") - f.write(self.mnemonic) - - platform.unmount_sdcard() - - await self.show( - Alert("Success!", "Your seed is exported.\n\nName: %s" % filename, button_text="OK") - ) - - async def storage_menu(self): """Manage storage, return True if new key was loaded""" - buttons = [ - # id, text - (None, "Manage keys on SD card and internal flash"), - (0, "Save key"), - (1, "Load key"), - (2, "Delete key"), - ] - - # disabled if SD card is not present - buttons.append((3, "Export recovery phrase to SD", platform.is_sd_present())) - - # we stay in this menu until back is pressed - while True: - # wait for menu selection - menuitem = await self.show(Menu(buttons, last=(255, None))) - # process the menu button: - # back button - if menuitem == 255: - return False - elif menuitem == 0: - filename = await self.save_mnemonic() - if filename: - await self.show( - Alert("Success!", "Your key is stored now.\n\nName: %s" % filename, button_text="OK") - ) - elif menuitem == 1: - if await self.load_mnemonic(): - await self.show( - Alert("Success!", "Your key is loaded.", button_text="OK") - ) - return True - elif menuitem == 2: - if await self.delete_mnemonic(): - await self.show( - Alert("Success!", "Your key is deleted.", button_text="OK") - ) - elif menuitem == 3: - await self.export_mnemonic() + return await super().storage_menu(title="Manage keys on SD card and internal flash")